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;
+ }
+ }
}
}