Add autofill dropdown to data entry

This commit is contained in:
Maciej Ziarkowski 2020-01-06 19:48:47 +00:00
parent cf5906dc89
commit 4a098ad57c
8 changed files with 126 additions and 33 deletions

View File

@ -6,7 +6,7 @@ const getAutofillOptions = asyncController(async (req, res) => {
const options = await autofillService.getAutofillOptions(field_name, field_value); const options = await autofillService.getAutofillOptions(field_name, field_value);
res.send(options); res.send({ options: options });
}); });
export default { export default {

View File

@ -7,7 +7,7 @@ const autofillFunctionMap = {
function getLanduseClassOptions(value: string) { function getLanduseClassOptions(value: string) {
return db.manyOrNone(` return db.manyOrNone(`
SELECT landuse_id AS id, description as value, similarity(description, $1) AS similarity SELECT landuse_id AS id, description as value, similarity(description, $1) AS similarity
FROM reference_tables.building_landuse_class FROM reference_tables.buildings_landuse_class
WHERE description % $1 WHERE description % $1
ORDER BY similarity DESC, description ORDER BY similarity DESC, description
`, [value] `, [value]

View File

@ -0,0 +1,9 @@
.autofill-dropdown {
position: absolute;
z-index: 1000;
background-color: white;
max-height: 5em;
width: 100%;
overflow-y: scroll;
overflow-x: hidden;
}

View File

@ -0,0 +1,59 @@
import React, { useEffect, useState } from 'react';
import './autofillDropdown.css';
import { apiGet } from '../../../apiHelpers';
interface AutofillDropdownProps {
fieldName: string;
fieldValue: string;
editing: boolean;
onSelect: (val: string) => void;
onClose: () => void;
}
interface AutofillOption {
id: string;
value: string;
similarity: number;
}
export const AutofillDropdown: React.FC<AutofillDropdownProps> = props => {
const [options, setOptions] = useState<AutofillOption[]>(null);
useEffect(() => {
const doAsync = async () => {
if (!props.editing || props.fieldValue === '') return setOptions(null);
const url = `/api/autofill?field_name=${props.fieldName}&field_value=${props.fieldValue}`;
const { options } = await apiGet(url);
if (!props.editing) return;
setOptions(options);
};
doAsync();
}, [props.editing, props.fieldName, props.fieldValue]);
if (!props.editing || options == undefined) return null;
return (
<div className="autofill-dropdown">
{
options.map(option =>
<div
onMouseDown={e => /* prevent input blur */ e.preventDefault()}
onClick={e => {
props.onSelect(option.value);
// close dropdown manually rather than through input blur
props.onClose();
}}
>
{option.value} ({option.id})
</div>)
}
</div>
);
};

View File

@ -1,30 +1,54 @@
import React from 'react'; import React, { useState } from 'react';
import { AutofillDropdown } from './autofill/autofillDropdown';
export interface TextDataEntryInputProps { export interface TextDataEntryInputProps {
slug: string; slug: string;
name?: string;
onChange?: (key: string, val: any) => void;
maxLength?: number; maxLength?: number;
disabled?: boolean; disabled?: boolean;
placeholder?: string; placeholder?: string;
valueTransform?: (val: string) => string; valueTransform?: (val: string) => string;
onChange?: (key: string, val: any) => void; autofill?: boolean;
} }
export const TextDataEntryInput: React.FC<TextDataEntryInputProps & {value?: string}> = props => { export const DataEntryInput: React.FC<TextDataEntryInputProps & {value?: string}> = props => {
const [isEditing, setEditing] = useState(false);
const handleChange = (value: string) => {
console.log(value);
const transform = props.valueTransform || (x => x);
const transformedValue = value === '' ?
null :
transform(value);
props.onChange(props.slug, transformedValue);
};
return ( return (
<input className="form-control" type="text" <div
id={props.slug} onFocus={e => setEditing(true)}
name={props.slug} onBlur={e => setEditing(false)}
value={props.value || ''} >
maxLength={props.maxLength} <input className="form-control" type="text"
disabled={props.disabled} name={props.slug}
placeholder={props.placeholder} value={props.value || ''}
onChange={e => { maxLength={props.maxLength}
const transform = props.valueTransform || (x => x); disabled={props.disabled}
const val = e.target.value === '' ? placeholder={props.placeholder}
null : onChange={e => handleChange(e.target.value)}
transform(e.target.value); />
props.onChange(props.slug, val); {
}} props.autofill &&
/> <AutofillDropdown
editing={isEditing}
onSelect={value => handleChange(value)}
onClose={() => setEditing(false)}
fieldName={props.slug}
fieldValue={props.value}
/>
}
</div>
); );
}; };

View File

@ -2,7 +2,7 @@ import React, { Fragment } from 'react';
import { CopyProps } from '../data-containers/category-view-props'; import { CopyProps } from '../data-containers/category-view-props';
import { TextDataEntryInput, TextDataEntryInputProps } from './data-entry-input'; import { DataEntryInput, TextDataEntryInputProps } from './data-entry-input';
import { DataTitleCopyable } from './data-title'; import { DataTitleCopyable } from './data-title';
interface BaseDataEntryProps { interface BaseDataEntryProps {
@ -29,7 +29,7 @@ const DataEntry: React.FC<DataEntryProps> = (props) => {
disabled={props.disabled || props.value == undefined || props.value == ''} disabled={props.disabled || props.value == undefined || props.value == ''}
copy={props.copy} copy={props.copy}
/> />
<TextDataEntryInput <DataEntryInput
slug={props.slug} slug={props.slug}
value={props.value} value={props.value}
onChange={props.onChange} onChange={props.onChange}

View File

@ -1,9 +1,7 @@
import React, { Component, Fragment } from 'react'; import React, { Component, Fragment } from 'react';
import { sanitiseURL } from '../../helpers';
import { BaseDataEntryProps } from './data-entry'; import { BaseDataEntryProps } from './data-entry';
import { TextDataEntryInput, TextDataEntryInputProps } from './data-entry-input'; import { DataEntryInput, TextDataEntryInputProps } from './data-entry-input';
import { DataTitleCopyable } from './data-title'; import { DataTitleCopyable } from './data-title';
@ -75,8 +73,8 @@ class MultiDataEntry extends Component<MultiDataEntryProps, MultiDataEntryState>
{ {
values.map((val, i) => ( values.map((val, i) => (
<li className="input-group" key={i}> <li className="input-group" key={i}>
<TextDataEntryInput <DataEntryInput
slug={`${props.slug}-${i}`} slug={props.slug}
value={val} value={val}
disabled={isDisabled} disabled={isDisabled}
onChange={(key, val) => this.edit(i, val)} onChange={(key, val) => this.edit(i, val)}
@ -84,6 +82,7 @@ class MultiDataEntry extends Component<MultiDataEntryProps, MultiDataEntryState>
maxLength={props.maxLength} maxLength={props.maxLength}
placeholder={props.placeholder} placeholder={props.placeholder}
valueTransform={props.valueTransform} valueTransform={props.valueTransform}
autofill={props.autofill}
/> />
{ {
!isDisabled && !isDisabled &&
@ -100,8 +99,8 @@ class MultiDataEntry extends Component<MultiDataEntryProps, MultiDataEntryState>
{ {
!isDisabled && !isDisabled &&
<div className="input-group"> <div className="input-group">
<TextDataEntryInput <DataEntryInput
slug='new' slug={props.slug}
value={this.state.newValue} value={this.state.newValue}
disabled={props.disabled} disabled={props.disabled}
onChange={(key, val) => this.setNewValue(val)} onChange={(key, val) => this.setNewValue(val)}
@ -109,6 +108,7 @@ class MultiDataEntry extends Component<MultiDataEntryProps, MultiDataEntryState>
maxLength={props.maxLength} maxLength={props.maxLength}
placeholder={props.placeholder} placeholder={props.placeholder}
valueTransform={props.valueTransform} valueTransform={props.valueTransform}
autofill={props.autofill}
/> />
<div className="input-group-append"> <div className="input-group-append">
<button type="button" onClick={this.addNew} <button type="button" onClick={this.addNew}

View File

@ -13,22 +13,22 @@ import { CategoryViewProps } from './category-view-props';
*/ */
const UseView: React.FunctionComponent<CategoryViewProps> = (props) => ( const UseView: React.FunctionComponent<CategoryViewProps> = (props) => (
<Fragment> <Fragment>
<InfoBox msg="This category is currently read-only. We are working on enabling its editing soon." />
<MultiDataEntry <MultiDataEntry
title={dataFields.current_landuse_class.title} title={dataFields.current_landuse_class.title}
slug="current_landuse_class" slug="current_landuse_class"
value={props.building.current_landuse_class} value={props.building.current_landuse_class}
mode="view" mode={props.mode}
copy={props.copy} copy={props.copy}
onChange={props.onChange} onChange={props.onChange}
// tooltip={dataFields.current_landuse_class.tooltip} // tooltip={dataFields.current_landuse_class.tooltip}
placeholder="New land use class..." placeholder="New land use class..."
autofill={true}
/> />
<MultiDataEntry <MultiDataEntry
title={dataFields.current_landuse_group.title} title={dataFields.current_landuse_group.title}
slug="current_landuse_group" slug="current_landuse_group"
value={props.building.current_landuse_group} value={props.building.current_landuse_group}
mode="view" mode={props.mode}
copy={props.copy} copy={props.copy}
onChange={props.onChange} onChange={props.onChange}
// tooltip={dataFields.current_landuse_class.tooltip} // tooltip={dataFields.current_landuse_class.tooltip}
@ -38,7 +38,8 @@ const UseView: React.FunctionComponent<CategoryViewProps> = (props) => (
title={dataFields.current_landuse_order.title} title={dataFields.current_landuse_order.title}
slug="current_landuse_order" slug="current_landuse_order"
value={props.building.current_landuse_order} value={props.building.current_landuse_order}
mode="view" mode={props.mode}
disabled={true}
copy={props.copy} copy={props.copy}
onChange={props.onChange} onChange={props.onChange}
/> />