Skip to content

Commit bedc777

Browse files
authored
Autocomplete with create entries & metadata title field with help interactive icon (#1906)
1 parent 4013071 commit bedc777

File tree

7 files changed

+185
-57
lines changed

7 files changed

+185
-57
lines changed

geonode_mapstore_client/client/js/components/Autocomplete/Autocomplete.jsx

Lines changed: 25 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,25 @@
88

99
import React from 'react';
1010
import isArray from 'lodash/isArray';
11+
import isEmpty from 'lodash/isEmpty';
1112
import PropTypes from 'prop-types';
1213

1314
import SelectInfiniteScroll from '@js/components/SelectInfiniteScroll/SelectInfiniteScroll';
15+
import tooltip from '@mapstore/framework/components/misc/enhancers/tooltip';
16+
import FaIcon from '@js/components/FaIcon/FaIcon';
17+
18+
const IconWithTooltip = tooltip((props) => <div {...props}><FaIcon name="info-circle" /></div>);
1419

1520
const Autocomplete = ({
1621
className,
17-
clearable = false,
22+
description,
23+
helpTitleIcon,
1824
id,
1925
labelKey,
20-
multi = false,
2126
name,
2227
title,
2328
value,
2429
valueKey,
25-
placeholder,
26-
onChange,
27-
onLoadOptions,
2830
...props
2931
}) => {
3032
const getValue = () => {
@@ -39,36 +41,42 @@ const Autocomplete = ({
3941
}
4042
return value;
4143
};
44+
45+
const defaultNewOptionCreator = (option) => ({
46+
[valueKey]: option.label,
47+
[labelKey]: option.label
48+
});
49+
4250
return (
4351
<div className={`autocomplete${className ? " " + className : ""}`}>
44-
<label className="control-label" htmlFor={id}>{title || name}</label>
52+
<div className="title-container">
53+
<label className="control-label" htmlFor={id}>{title || name}</label>
54+
{helpTitleIcon && !isEmpty(description) && <IconWithTooltip className="help-title" tooltip={description} tooltipPosition={"right"} />}
55+
</div>
4556
<SelectInfiniteScroll
4657
{...props}
4758
id={id}
4859
value={getValue()}
49-
multi={multi}
50-
clearable={clearable}
51-
placeholder={placeholder}
52-
loadOptions={onLoadOptions}
53-
onChange={onChange}
60+
valueKey={valueKey}
61+
labelKey={labelKey}
62+
{...props.creatable && {
63+
newOptionCreator: props.newOptionCreator ?? defaultNewOptionCreator
64+
}}
5465
/>
5566
</div>
5667
);
5768
};
5869

5970
Autocomplete.propTypes = {
6071
className: PropTypes.string,
61-
clearable: PropTypes.bool,
72+
description: PropTypes.string,
73+
helpTitleIcon: PropTypes.bool,
6274
id: PropTypes.string.isRequired,
6375
labelKey: PropTypes.string,
64-
multi: PropTypes.bool,
6576
name: PropTypes.string,
6677
title: PropTypes.string,
6778
value: PropTypes.any.isRequired,
68-
valueKey: PropTypes.string,
69-
placeholder: PropTypes.string,
70-
onChange: PropTypes.func.isRequired,
71-
onLoadOptions: PropTypes.func.isRequired
79+
valueKey: PropTypes.string
7280
};
7381

7482
export default Autocomplete;

geonode_mapstore_client/client/js/components/SelectInfiniteScroll/SelectInfiniteScroll.jsx

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import React, { useRef, useState, useEffect } from 'react';
1010
import axios from '@mapstore/framework/libs/ajax';
1111
import debounce from 'lodash/debounce';
12+
import isEmpty from 'lodash/isEmpty';
1213
import ReactSelect from 'react-select';
1314
import localizedProps from '@mapstore/framework/components/misc/enhancers/localizedProps';
1415

@@ -18,6 +19,9 @@ function SelectInfiniteScroll({
1819
loadOptions,
1920
pageSize = 20,
2021
debounceTime = 500,
22+
labelKey,
23+
valueKey,
24+
newOptionPromptText = "Create option",
2125
...props
2226
}) {
2327

@@ -40,6 +44,23 @@ function SelectInfiniteScroll({
4044
source.current = cancelToken.source();
4145
};
4246

47+
const updateNewOption = (newOptions, query) => {
48+
if (props.creatable && !isEmpty(query)) {
49+
const isValueExist = props.value?.some(v => v[labelKey] === query);
50+
const isOptionExist = newOptions.some((o) => o[labelKey] === query);
51+
52+
// Add new option if it doesn't exist and `creatable` is enabled
53+
if (!isValueExist && !isOptionExist) {
54+
return [{
55+
[labelKey]: `${newOptionPromptText} "${query}"`, value: query,
56+
result: { [valueKey]: query, [labelKey]: query }
57+
}].concat(newOptions);
58+
}
59+
return newOptions;
60+
}
61+
return newOptions;
62+
};
63+
4364
const handleUpdateOptions = useRef();
4465
handleUpdateOptions.current = (args = {}) => {
4566
createToken();
@@ -56,8 +77,10 @@ function SelectInfiniteScroll({
5677
}
5778
})
5879
.then((response) => {
59-
const newOptions = response.results.map(({ selectOption }) => selectOption);
60-
setOptions(newPage === 1 ? newOptions : [...options, ...newOptions]);
80+
let newOptions = response.results.map(({ selectOption }) => selectOption);
81+
newOptions = newPage === 1 ? newOptions : [...options, ...newOptions];
82+
newOptions = updateNewOption(newOptions, query);
83+
setOptions(newOptions);
6184
setIsNextPageAvailable(response.isNextPageAvailable);
6285
setLoading(false);
6386
source.current = undefined;
@@ -89,7 +112,7 @@ function SelectInfiniteScroll({
89112
handleUpdateOptions.current({ q: value, page: 1 });
90113
}
91114
}, debounceTime);
92-
}, []);
115+
}, [text]);
93116

94117
useEffect(() => {
95118
if (open) {
@@ -106,16 +129,21 @@ function SelectInfiniteScroll({
106129
}
107130
}, [page]);
108131

132+
const filterOptions = (currentOptions) => {
133+
return currentOptions.map(option=> {
134+
const match = /\"(.*?)\"/.exec(text);
135+
return match ? match[1] : option;
136+
});
137+
};
138+
109139
return (
110140
<SelectSync
111141
{...props}
112142
isLoading={loading}
113143
options={options}
114144
onOpen={() => setOpen(true)}
115145
onClose={() => setOpen(false)}
116-
filterOptions={(currentOptions) => {
117-
return currentOptions;
118-
}}
146+
filterOptions={filterOptions}
119147
onInputChange={(q) => handleInputChange(q)}
120148
onMenuScrollToBottom={() => {
121149
if (!loading && isNextPageAvailable) {

geonode_mapstore_client/client/js/plugins/MetadataEditor/components/_fields/SchemaField.jsx

Lines changed: 34 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -42,17 +42,22 @@ const SchemaField = (props) => {
4242
const valueKey = autocompleteOptions?.valueKey || 'id';
4343
const labelKey = autocompleteOptions?.labelKey || 'label';
4444
const placeholder = autocompleteOptions?.placeholder ?? '...';
45+
const creatable = !!autocompleteOptions?.creatable;
4546

4647
let autoCompleteProps = {
48+
className: "gn-metadata-autocomplete",
49+
clearable: !isMultiSelect,
50+
creatable,
4751
id: idSchema.$id,
52+
labelKey,
53+
multi: isMultiSelect,
4854
name,
55+
placeholder,
4956
title: schema.title,
5057
value: formData,
5158
valueKey,
52-
labelKey,
53-
placeholder,
54-
multi: isMultiSelect,
55-
clearable: !isMultiSelect,
59+
helpTitleIcon: true,
60+
description: schema.description,
5661
onChange: (selected) => {
5762
let _selected = selected?.result ?? null;
5863
if (isMultiSelect) {
@@ -67,39 +72,34 @@ const SchemaField = (props) => {
6772
});
6873
}
6974
onChange(_selected);
75+
},
76+
loadOptions: ({ q, config, ...params }) => {
77+
return axios.get(autocompleteUrl, {
78+
...config,
79+
params: {
80+
...params,
81+
...(q && { [queryKey]: q }),
82+
page: params.page
83+
}
84+
})
85+
.then(({ data }) => {
86+
return {
87+
isNextPageAvailable: !!data.pagination?.more,
88+
results: data?.[resultsKey].map((result) => {
89+
return {
90+
selectOption: {
91+
result,
92+
value: result[valueKey],
93+
label: result[labelKey]
94+
}
95+
};
96+
})
97+
};
98+
});
7099
}
71100
};
72101

73-
return (
74-
<Autocomplete
75-
{...autoCompleteProps}
76-
className={"form-group gn-metadata-autocomplete"}
77-
onLoadOptions={({ q, config, ...params }) => {
78-
return axios.get(autocompleteUrl, {
79-
...config,
80-
params: {
81-
...params,
82-
...(q && { [queryKey]: q }),
83-
page: params.page
84-
}
85-
})
86-
.then(({ data }) => {
87-
return {
88-
isNextPageAvailable: !!data.pagination?.more,
89-
results: data?.[resultsKey].map((result) => {
90-
return {
91-
selectOption: {
92-
result,
93-
value: result[valueKey],
94-
label: result[labelKey]
95-
}
96-
};
97-
})
98-
};
99-
});
100-
}}
101-
/>
102-
);
102+
return <Autocomplete {...autoCompleteProps}/>;
103103
}
104104
return <DefaultSchemaField {...props}/>;
105105
};
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*
2+
* Copyright 2024, GeoSolutions Sas.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
import React from "react";
10+
import isEmpty from "lodash/isEmpty";
11+
12+
import FaIcon from "@js/components/FaIcon/FaIcon";
13+
import tooltip from "@mapstore/framework/components/misc/enhancers/tooltip";
14+
15+
const IconWithTooltip = tooltip((props) => <div {...props}><FaIcon name="info-circle" /></div>);
16+
17+
const DescriptionFieldTemplate = (props) => {
18+
const { description, id } = props;
19+
if (isEmpty(description)) {
20+
return null;
21+
}
22+
return (
23+
<IconWithTooltip
24+
className="gn-metadata-form-description"
25+
id={id}
26+
tooltip={description}
27+
tooltipPosition={"right"}
28+
/>
29+
);
30+
};
31+
32+
export default DescriptionFieldTemplate;
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*
2+
* Copyright 2024, GeoSolutions Sas.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
import React from 'react';
10+
11+
const TitleFieldTemplate = (props) => {
12+
const { id, required, title } = props;
13+
return (
14+
<div className="gn-metadata-form-title" id={id}>
15+
{title}
16+
{required && <mark>*</mark>}
17+
</div>
18+
);
19+
};
20+
21+
export default TitleFieldTemplate;

geonode_mapstore_client/client/js/plugins/MetadataEditor/components/_templates/index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,12 @@
77
*/
88

99
import ObjectFieldTemplate from './ObjectFieldTemplate';
10+
import DescriptionFieldTemplate from './DescriptionFieldTemplate';
11+
import TitleFieldTemplate from './TitleFieldTemplate';
1012

1113
export default {
1214
ObjectFieldTemplate,
15+
TitleFieldTemplate,
16+
DescriptionFieldTemplate,
1317
ErrorListTemplate: () => null
1418
};

geonode_mapstore_client/client/themes/geonode/less/_metadata.less

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@
159159
padding: 0.75rem;
160160
border: 1px solid transparent;
161161
border-radius: 8px;
162+
margin: 0.75rem;
162163
}
163164
legend {
164165
font-weight: bold;
@@ -196,12 +197,46 @@
196197
}
197198
.gn-metadata-autocomplete {
198199
padding: 0 0.75rem;
200+
width: 100%;
201+
margin-bottom: 15px;
202+
.title-container {
203+
display: flex;
204+
gap: 10px;
205+
align-items: center;
206+
}
199207
.Select--multi {
200208
.Select-value {
201209
margin-top: 3px;
202210
margin-bottom: 3px;
203211
}
204212
}
213+
.help-title {
214+
margin-bottom: 5px;
215+
}
216+
}
217+
.gn-metadata-group {
218+
.form-group.field {
219+
display: flex;
220+
flex-wrap: wrap;
221+
column-gap: 0.5rem;
222+
text-transform: capitalize;
223+
align-items: center;
224+
.gn-metadata-form-description {
225+
margin-bottom: 5px;
226+
}
227+
fieldset {
228+
display: flex;
229+
flex-wrap: wrap;
230+
gap: 0.5rem;
231+
width: 100%;
232+
.gn-metadata-autocomplete {
233+
margin-bottom: 0;
234+
}
235+
}
236+
.gn-metadata-form-title {
237+
font-weight: 700;
238+
}
239+
}
205240
}
206241
}
207242

0 commit comments

Comments
 (0)