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);
res.send(options);
res.send({ options: options });
});
export default {

View File

@ -7,7 +7,7 @@ const autofillFunctionMap = {
function getLanduseClassOptions(value: string) {
return db.manyOrNone(`
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
ORDER BY similarity DESC, description
`, [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 {
slug: string;
name?: string;
onChange?: (key: string, val: any) => void;
maxLength?: number;
disabled?: boolean;
placeholder?: 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 (
<input className="form-control" type="text"
id={props.slug}
name={props.slug}
value={props.value || ''}
maxLength={props.maxLength}
disabled={props.disabled}
placeholder={props.placeholder}
onChange={e => {
const transform = props.valueTransform || (x => x);
const val = e.target.value === '' ?
null :
transform(e.target.value);
props.onChange(props.slug, val);
}}
/>
<div
onFocus={e => setEditing(true)}
onBlur={e => setEditing(false)}
>
<input className="form-control" type="text"
name={props.slug}
value={props.value || ''}
maxLength={props.maxLength}
disabled={props.disabled}
placeholder={props.placeholder}
onChange={e => handleChange(e.target.value)}
/>
{
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 { TextDataEntryInput, TextDataEntryInputProps } from './data-entry-input';
import { DataEntryInput, TextDataEntryInputProps } from './data-entry-input';
import { DataTitleCopyable } from './data-title';
interface BaseDataEntryProps {
@ -29,7 +29,7 @@ const DataEntry: React.FC<DataEntryProps> = (props) => {
disabled={props.disabled || props.value == undefined || props.value == ''}
copy={props.copy}
/>
<TextDataEntryInput
<DataEntryInput
slug={props.slug}
value={props.value}
onChange={props.onChange}

View File

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

View File

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