Add autofill dropdown to data entry
This commit is contained in:
parent
cf5906dc89
commit
4a098ad57c
@ -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 {
|
||||||
|
@ -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]
|
||||||
|
@ -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;
|
||||||
|
}
|
@ -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>
|
||||||
|
);
|
||||||
|
|
||||||
|
};
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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}
|
||||||
|
@ -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}
|
||||||
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
Loading…
Reference in New Issue
Block a user