diff --git a/geonode_mapstore_client/client/js/components/Autocomplete/Autocomplete.jsx b/geonode_mapstore_client/client/js/components/Autocomplete/Autocomplete.jsx index f2745456f0..deef564b02 100644 --- a/geonode_mapstore_client/client/js/components/Autocomplete/Autocomplete.jsx +++ b/geonode_mapstore_client/client/js/components/Autocomplete/Autocomplete.jsx @@ -8,23 +8,25 @@ import React from 'react'; import isArray from 'lodash/isArray'; +import isEmpty from 'lodash/isEmpty'; import PropTypes from 'prop-types'; import SelectInfiniteScroll from '@js/components/SelectInfiniteScroll/SelectInfiniteScroll'; +import tooltip from '@mapstore/framework/components/misc/enhancers/tooltip'; +import FaIcon from '@js/components/FaIcon/FaIcon'; + +const IconWithTooltip = tooltip((props) =>
); const Autocomplete = ({ className, - clearable = false, + description, + helpTitleIcon, id, labelKey, - multi = false, name, title, value, valueKey, - placeholder, - onChange, - onLoadOptions, ...props }) => { const getValue = () => { @@ -39,18 +41,27 @@ const Autocomplete = ({ } return value; }; + + const defaultNewOptionCreator = (option) => ({ + [valueKey]: option.label, + [labelKey]: option.label + }); + return (
- +
+ + {helpTitleIcon && !isEmpty(description) && } +
); @@ -58,17 +69,14 @@ const Autocomplete = ({ Autocomplete.propTypes = { className: PropTypes.string, - clearable: PropTypes.bool, + description: PropTypes.string, + helpTitleIcon: 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..cd1c251755 100644 --- a/geonode_mapstore_client/client/js/components/SelectInfiniteScroll/SelectInfiniteScroll.jsx +++ b/geonode_mapstore_client/client/js/components/SelectInfiniteScroll/SelectInfiniteScroll.jsx @@ -9,6 +9,7 @@ import React, { useRef, useState, useEffect } from 'react'; import axios from '@mapstore/framework/libs/ajax'; import debounce from 'lodash/debounce'; +import isEmpty from 'lodash/isEmpty'; import ReactSelect from 'react-select'; import localizedProps from '@mapstore/framework/components/misc/enhancers/localizedProps'; @@ -18,6 +19,9 @@ function SelectInfiniteScroll({ loadOptions, pageSize = 20, debounceTime = 500, + labelKey, + valueKey, + newOptionPromptText = "Create option", ...props }) { @@ -40,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(); @@ -56,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; @@ -89,7 +112,7 @@ function SelectInfiniteScroll({ handleUpdateOptions.current({ q: value, page: 1 }); } }, debounceTime); - }, []); + }, [text]); useEffect(() => { if (open) { @@ -106,6 +129,13 @@ function SelectInfiniteScroll({ } }, [page]); + const filterOptions = (currentOptions) => { + return currentOptions.map(option=> { + const match = /\"(.*?)\"/.exec(text); + return match ? match[1] : option; + }); + }; + return ( setOpen(true)} onClose={() => setOpen(false)} - filterOptions={(currentOptions) => { - return currentOptions; - }} + filterOptions={filterOptions} onInputChange={(q) => handleInputChange(q)} onMenuScrollToBottom={() => { if (!loading && isNextPageAvailable) { 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..fdd7764353 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,22 @@ const SchemaField = (props) => { const valueKey = autocompleteOptions?.valueKey || 'id'; const labelKey = autocompleteOptions?.labelKey || 'label'; const placeholder = autocompleteOptions?.placeholder ?? '...'; + const creatable = !!autocompleteOptions?.creatable; let autoCompleteProps = { + className: "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, + helpTitleIcon: true, + description: schema.description, onChange: (selected) => { let _selected = selected?.result ?? null; if (isMultiSelect) { @@ -67,39 +72,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/js/plugins/MetadataEditor/components/_templates/DescriptionFieldTemplate.jsx b/geonode_mapstore_client/client/js/plugins/MetadataEditor/components/_templates/DescriptionFieldTemplate.jsx new file mode 100644 index 0000000000..389c4a770b --- /dev/null +++ b/geonode_mapstore_client/client/js/plugins/MetadataEditor/components/_templates/DescriptionFieldTemplate.jsx @@ -0,0 +1,32 @@ +/* + * Copyright 2024, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from "react"; +import isEmpty from "lodash/isEmpty"; + +import FaIcon from "@js/components/FaIcon/FaIcon"; +import tooltip from "@mapstore/framework/components/misc/enhancers/tooltip"; + +const IconWithTooltip = tooltip((props) =>
); + +const DescriptionFieldTemplate = (props) => { + const { description, id } = props; + if (isEmpty(description)) { + return null; + } + return ( + + ); +}; + +export default DescriptionFieldTemplate; diff --git a/geonode_mapstore_client/client/js/plugins/MetadataEditor/components/_templates/TitleFieldTemplate.jsx b/geonode_mapstore_client/client/js/plugins/MetadataEditor/components/_templates/TitleFieldTemplate.jsx new file mode 100644 index 0000000000..cbb053585c --- /dev/null +++ b/geonode_mapstore_client/client/js/plugins/MetadataEditor/components/_templates/TitleFieldTemplate.jsx @@ -0,0 +1,21 @@ +/* + * Copyright 2024, GeoSolutions Sas. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. + */ + +import React from 'react'; + +const TitleFieldTemplate = (props) => { + const { id, required, title } = props; + return ( +
+ {title} + {required && *} +
+ ); +}; + +export default TitleFieldTemplate; diff --git a/geonode_mapstore_client/client/js/plugins/MetadataEditor/components/_templates/index.js b/geonode_mapstore_client/client/js/plugins/MetadataEditor/components/_templates/index.js index f6ce38d2cc..450c609494 100644 --- a/geonode_mapstore_client/client/js/plugins/MetadataEditor/components/_templates/index.js +++ b/geonode_mapstore_client/client/js/plugins/MetadataEditor/components/_templates/index.js @@ -7,8 +7,12 @@ */ import ObjectFieldTemplate from './ObjectFieldTemplate'; +import DescriptionFieldTemplate from './DescriptionFieldTemplate'; +import TitleFieldTemplate from './TitleFieldTemplate'; export default { ObjectFieldTemplate, + TitleFieldTemplate, + DescriptionFieldTemplate, ErrorListTemplate: () => null }; diff --git a/geonode_mapstore_client/client/themes/geonode/less/_metadata.less b/geonode_mapstore_client/client/themes/geonode/less/_metadata.less index 5004821196..c8b2ba1004 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; @@ -196,12 +197,46 @@ } .gn-metadata-autocomplete { padding: 0 0.75rem; + width: 100%; + margin-bottom: 15px; + .title-container { + display: flex; + gap: 10px; + align-items: center; + } .Select--multi { .Select-value { margin-top: 3px; margin-bottom: 3px; } } + .help-title { + margin-bottom: 5px; + } + } + .gn-metadata-group { + .form-group.field { + display: flex; + flex-wrap: wrap; + column-gap: 0.5rem; + text-transform: capitalize; + align-items: center; + .gn-metadata-form-description { + margin-bottom: 5px; + } + fieldset { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + width: 100%; + .gn-metadata-autocomplete { + margin-bottom: 0; + } + } + .gn-metadata-form-title { + font-weight: 700; + } + } } }