From bd357e3632c2396f9e19e83e22c542cfd98aa7d7 Mon Sep 17 00:00:00 2001 From: Suren Date: Wed, 13 Nov 2024 16:28:38 +0530 Subject: [PATCH 1/2] Autocomplete - Allow creation of new entries --- .../components/Autocomplete/Autocomplete.jsx | 26 +++----- .../SelectInfiniteScroll.jsx | 17 +++-- .../components/_fields/SchemaField.jsx | 66 +++++++++---------- .../client/themes/geonode/less/_metadata.less | 1 + 4 files changed, 53 insertions(+), 57 deletions(-) diff --git a/geonode_mapstore_client/client/js/components/Autocomplete/Autocomplete.jsx b/geonode_mapstore_client/client/js/components/Autocomplete/Autocomplete.jsx index f2745456f0..09ac71b8ed 100644 --- a/geonode_mapstore_client/client/js/components/Autocomplete/Autocomplete.jsx +++ b/geonode_mapstore_client/client/js/components/Autocomplete/Autocomplete.jsx @@ -14,17 +14,12 @@ import SelectInfiniteScroll from '@js/components/SelectInfiniteScroll/SelectInfi const Autocomplete = ({ className, - clearable = false, id, labelKey, - multi = false, name, title, value, valueKey, - placeholder, - onChange, - onLoadOptions, ...props }) => { const getValue = () => { @@ -39,6 +34,12 @@ const Autocomplete = ({ } return value; }; + + const defaultNewOptionCreator = (option) => ({ + [valueKey]: option.label, + [labelKey]: option.label + }); + return (
@@ -46,11 +47,9 @@ const Autocomplete = ({ {...props} id={id} value={getValue()} - multi={multi} - clearable={clearable} - placeholder={placeholder} - loadOptions={onLoadOptions} - onChange={onChange} + {...props.creatable && { + newOptionCreator: props.newOptionCreator ?? defaultNewOptionCreator} + } />
); @@ -58,17 +57,12 @@ const Autocomplete = ({ Autocomplete.propTypes = { className: PropTypes.string, - clearable: PropTypes.bool, id: PropTypes.string.isRequired, labelKey: PropTypes.string, - multi: PropTypes.bool, name: PropTypes.string, title: PropTypes.string, value: PropTypes.any.isRequired, - valueKey: PropTypes.string, - placeholder: PropTypes.string, - onChange: PropTypes.func.isRequired, - onLoadOptions: PropTypes.func.isRequired + valueKey: PropTypes.string }; export default Autocomplete; diff --git a/geonode_mapstore_client/client/js/components/SelectInfiniteScroll/SelectInfiniteScroll.jsx b/geonode_mapstore_client/client/js/components/SelectInfiniteScroll/SelectInfiniteScroll.jsx index e05fa7a958..8a4068669a 100644 --- a/geonode_mapstore_client/client/js/components/SelectInfiniteScroll/SelectInfiniteScroll.jsx +++ b/geonode_mapstore_client/client/js/components/SelectInfiniteScroll/SelectInfiniteScroll.jsx @@ -9,10 +9,11 @@ import React, { useRef, useState, useEffect } from 'react'; import axios from '@mapstore/framework/libs/ajax'; import debounce from 'lodash/debounce'; -import ReactSelect from 'react-select'; +import Select from 'react-select'; import localizedProps from '@mapstore/framework/components/misc/enhancers/localizedProps'; -const SelectSync = localizedProps('placeholder')(ReactSelect); +const ReactSelect = localizedProps(['placeholder'])(Select); +const ReactSelectCreatable = localizedProps(['placeholder'])(Select.Creatable); function SelectInfiniteScroll({ loadOptions, @@ -106,22 +107,24 @@ function SelectInfiniteScroll({ } }, [page]); + const SelectComponent = props.creatable ? ReactSelectCreatable : ReactSelect; + return ( - setOpen(true)} onClose={() => setOpen(false)} - filterOptions={(currentOptions) => { - return currentOptions; - }} - onInputChange={(q) => handleInputChange(q)} onMenuScrollToBottom={() => { if (!loading && isNextPageAvailable) { setPage(page + 1); } }} + {...!props.creatable && ({ + filterOptions: (currentOptions) => currentOptions, + onInputChange: handleInputChange + })} /> ); } diff --git a/geonode_mapstore_client/client/js/plugins/MetadataEditor/components/_fields/SchemaField.jsx b/geonode_mapstore_client/client/js/plugins/MetadataEditor/components/_fields/SchemaField.jsx index 002d22b632..f757f53fe1 100644 --- a/geonode_mapstore_client/client/js/plugins/MetadataEditor/components/_fields/SchemaField.jsx +++ b/geonode_mapstore_client/client/js/plugins/MetadataEditor/components/_fields/SchemaField.jsx @@ -42,17 +42,20 @@ const SchemaField = (props) => { const valueKey = autocompleteOptions?.valueKey || 'id'; const labelKey = autocompleteOptions?.labelKey || 'label'; const placeholder = autocompleteOptions?.placeholder ?? '...'; + const creatable = !!autocompleteOptions?.creatable; let autoCompleteProps = { + className: "form-group gn-metadata-autocomplete", + clearable: !isMultiSelect, + creatable, id: idSchema.$id, + labelKey, + multi: isMultiSelect, name, + placeholder, title: schema.title, value: formData, valueKey, - labelKey, - placeholder, - multi: isMultiSelect, - clearable: !isMultiSelect, onChange: (selected) => { let _selected = selected?.result ?? null; if (isMultiSelect) { @@ -67,39 +70,34 @@ const SchemaField = (props) => { }); } onChange(_selected); + }, + loadOptions: ({ q, config, ...params }) => { + return axios.get(autocompleteUrl, { + ...config, + params: { + ...params, + ...(q && { [queryKey]: q }), + page: params.page + } + }) + .then(({ data }) => { + return { + isNextPageAvailable: !!data.pagination?.more, + results: data?.[resultsKey].map((result) => { + return { + selectOption: { + result, + value: result[valueKey], + label: result[labelKey] + } + }; + }) + }; + }); } }; - return ( - { - return axios.get(autocompleteUrl, { - ...config, - params: { - ...params, - ...(q && { [queryKey]: q }), - page: params.page - } - }) - .then(({ data }) => { - return { - isNextPageAvailable: !!data.pagination?.more, - results: data?.[resultsKey].map((result) => { - return { - selectOption: { - result, - value: result[valueKey], - label: result[labelKey] - } - }; - }) - }; - }); - }} - /> - ); + return ; } return ; }; diff --git a/geonode_mapstore_client/client/themes/geonode/less/_metadata.less b/geonode_mapstore_client/client/themes/geonode/less/_metadata.less index 5004821196..91bb9c58c7 100644 --- a/geonode_mapstore_client/client/themes/geonode/less/_metadata.less +++ b/geonode_mapstore_client/client/themes/geonode/less/_metadata.less @@ -159,6 +159,7 @@ padding: 0.75rem; border: 1px solid transparent; border-radius: 8px; + margin: 0.75rem; } legend { font-weight: bold; From 05ceba1bdb5e4c34196509c7d05fc5c311a53f02 Mon Sep 17 00:00:00 2001 From: Suren Date: Thu, 14 Nov 2024 11:44:47 +0530 Subject: [PATCH 2/2] Update autocomplete --- .../components/Autocomplete/Autocomplete.jsx | 6 ++- .../SelectInfiniteScroll.jsx | 49 ++++++++++++++----- 2 files changed, 41 insertions(+), 14 deletions(-) diff --git a/geonode_mapstore_client/client/js/components/Autocomplete/Autocomplete.jsx b/geonode_mapstore_client/client/js/components/Autocomplete/Autocomplete.jsx index 09ac71b8ed..5b17b83f31 100644 --- a/geonode_mapstore_client/client/js/components/Autocomplete/Autocomplete.jsx +++ b/geonode_mapstore_client/client/js/components/Autocomplete/Autocomplete.jsx @@ -47,9 +47,11 @@ const Autocomplete = ({ {...props} id={id} value={getValue()} + valueKey={valueKey} + labelKey={labelKey} {...props.creatable && { - newOptionCreator: props.newOptionCreator ?? defaultNewOptionCreator} - } + newOptionCreator: props.newOptionCreator ?? defaultNewOptionCreator + }} /> ); diff --git a/geonode_mapstore_client/client/js/components/SelectInfiniteScroll/SelectInfiniteScroll.jsx b/geonode_mapstore_client/client/js/components/SelectInfiniteScroll/SelectInfiniteScroll.jsx index 8a4068669a..cd1c251755 100644 --- a/geonode_mapstore_client/client/js/components/SelectInfiniteScroll/SelectInfiniteScroll.jsx +++ b/geonode_mapstore_client/client/js/components/SelectInfiniteScroll/SelectInfiniteScroll.jsx @@ -9,16 +9,19 @@ import React, { useRef, useState, useEffect } from 'react'; import axios from '@mapstore/framework/libs/ajax'; import debounce from 'lodash/debounce'; -import Select from 'react-select'; +import isEmpty from 'lodash/isEmpty'; +import ReactSelect from 'react-select'; import localizedProps from '@mapstore/framework/components/misc/enhancers/localizedProps'; -const ReactSelect = localizedProps(['placeholder'])(Select); -const ReactSelectCreatable = localizedProps(['placeholder'])(Select.Creatable); +const SelectSync = localizedProps('placeholder')(ReactSelect); function SelectInfiniteScroll({ loadOptions, pageSize = 20, debounceTime = 500, + labelKey, + valueKey, + newOptionPromptText = "Create option", ...props }) { @@ -41,6 +44,23 @@ function SelectInfiniteScroll({ source.current = cancelToken.source(); }; + const updateNewOption = (newOptions, query) => { + if (props.creatable && !isEmpty(query)) { + const isValueExist = props.value?.some(v => v[labelKey] === query); + const isOptionExist = newOptions.some((o) => o[labelKey] === query); + + // Add new option if it doesn't exist and `creatable` is enabled + if (!isValueExist && !isOptionExist) { + return [{ + [labelKey]: `${newOptionPromptText} "${query}"`, value: query, + result: { [valueKey]: query, [labelKey]: query } + }].concat(newOptions); + } + return newOptions; + } + return newOptions; + }; + const handleUpdateOptions = useRef(); handleUpdateOptions.current = (args = {}) => { createToken(); @@ -57,8 +77,10 @@ function SelectInfiniteScroll({ } }) .then((response) => { - const newOptions = response.results.map(({ selectOption }) => selectOption); - setOptions(newPage === 1 ? newOptions : [...options, ...newOptions]); + let newOptions = response.results.map(({ selectOption }) => selectOption); + newOptions = newPage === 1 ? newOptions : [...options, ...newOptions]; + newOptions = updateNewOption(newOptions, query); + setOptions(newOptions); setIsNextPageAvailable(response.isNextPageAvailable); setLoading(false); source.current = undefined; @@ -90,7 +112,7 @@ function SelectInfiniteScroll({ handleUpdateOptions.current({ q: value, page: 1 }); } }, debounceTime); - }, []); + }, [text]); useEffect(() => { if (open) { @@ -107,24 +129,27 @@ function SelectInfiniteScroll({ } }, [page]); - const SelectComponent = props.creatable ? ReactSelectCreatable : ReactSelect; + const filterOptions = (currentOptions) => { + return currentOptions.map(option=> { + const match = /\"(.*?)\"/.exec(text); + return match ? match[1] : option; + }); + }; return ( - setOpen(true)} onClose={() => setOpen(false)} + filterOptions={filterOptions} + onInputChange={(q) => handleInputChange(q)} onMenuScrollToBottom={() => { if (!loading && isNextPageAvailable) { setPage(page + 1); } }} - {...!props.creatable && ({ - filterOptions: (currentOptions) => currentOptions, - onInputChange: handleInputChange - })} /> ); }