Skip to content

Commit 0fcc062

Browse files
authored
geosolutions-it#10034: Include SortyBy parameter in the CSW catalog query (geosolutions-it#10055)
* geosolutions-it#10034: Include SortyBy parameter in the CSW catalog query * Translation updated * Code refactor * Debounced FC
1 parent aa5c993 commit 0fcc062

File tree

14 files changed

+135
-22
lines changed

14 files changed

+135
-22
lines changed

web/client/api/CSW.js

+12-2
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,13 @@ const defaultDynamicFilter = "<ogc:PropertyIsLike wildCard='%' singleChar='_' es
4444
"<ogc:Literal>%${searchText}%</ogc:Literal> " +
4545
"</ogc:PropertyIsLike> ";
4646

47+
export const sortBy = "<ogc:SortBy>" +
48+
"<ogc:SortProperty>" +
49+
"<ogc:PropertyName>${name}</ogc:PropertyName>" +
50+
"<ogc:SortOrder>${order}</ogc:SortOrder>" +
51+
"</ogc:SortProperty>" +
52+
"</ogc:SortBy>";
53+
4754
export const cswGetRecordsXml = '<csw:GetRecords xmlns:csw="http://www.opengis.net/cat/csw/2.0.2" ' +
4855
'xmlns:ogc="http://www.opengis.net/ogc" ' +
4956
'xmlns:gml="http://www.opengis.net/gml" ' +
@@ -60,6 +67,7 @@ export const cswGetRecordsXml = '<csw:GetRecords xmlns:csw="http://www.opengis.n
6067
'${filterXml} ' +
6168
'</ogc:Filter> ' +
6269
'</csw:Constraint> ' +
70+
'${sortBy} ' +
6371
'</csw:Query> ' +
6472
'</csw:GetRecords>';
6573

@@ -74,13 +82,15 @@ export const cswGetRecordsXml = '<csw:GetRecords xmlns:csw="http://www.opengis.n
7482
* @param {object} filter.dynamicFilter filter when search text is present and is applied in conjunction with static filter
7583
* @return {string} constructed xml string
7684
*/
77-
export const constructXMLBody = (startPosition, maxRecords, searchText, { filter } = {}) => {
85+
export const constructXMLBody = (startPosition, maxRecords, searchText, { options: { service } = {} } = {}) => {
86+
const { filter, sortBy: sortObj } = service ?? {};
7887
const staticFilter = filter?.staticFilter || defaultStaticFilter;
7988
const dynamicFilter = `<ogc:And>
8089
${template(filter?.dynamicFilter || defaultDynamicFilter)({ searchText })}
8190
${staticFilter}
8291
</ogc:And>`;
83-
return template(cswGetRecordsXml)({ filterXml: !searchText ? staticFilter : dynamicFilter, startPosition, maxRecords });
92+
const sortExp = sortObj?.name ? template(sortBy)({ name: sortObj?.name, order: sortObj?.order ?? "ASC"}) : '';
93+
return template(cswGetRecordsXml)({ filterXml: !searchText ? staticFilter : dynamicFilter, startPosition, maxRecords, sortBy: sortExp});
8494
};
8595

8696
// Extract the relevant information from the wms URL for (RNDT / INSPIRE)

web/client/api/__tests__/CSW-test.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -402,7 +402,7 @@ describe("constructXMLBody", () => {
402402
dynamicFilter: "<ogc:PropertyIsLike wildCard='*' singleChar='_' escapeChar='\\'><ogc:PropertyName>csw:AnyText</ogc:PropertyName><ogc:Literal>${searchText}*</ogc:Literal></ogc:PropertyIsLike>"
403403
};
404404
// With search text
405-
let body = constructXMLBody(1, 5, "text", {filter});
405+
let body = constructXMLBody(1, 5, "text", {options: {service: {filter}}});
406406

407407
expect(body.indexOf("text*")).toNotBe(-1); // Dynamic filter
408408

@@ -411,6 +411,11 @@ describe("constructXMLBody", () => {
411411
expect(body.indexOf("dc:type")).toNotBe(-1); // Static filter
412412
expect(body.indexOf("text*")).toBe(-1); // Dynamic filter
413413
});
414+
it("construct body with sortBy properties", () => {
415+
const body = constructXMLBody(1, 5, "text", {options: {service: {sortBy: {name: "dc:title", order: "DESC"}}}});
416+
expect(body.indexOf("dc:title")).toNotBe(-1);
417+
expect(body.indexOf("DESC")).toNotBe(-1);
418+
});
414419
});
415420

416421
describe("getLayerReferenceFromDc", () => {

web/client/components/catalog/CompactCatalog.jsx

+2-5
Original file line numberDiff line numberDiff line change
@@ -75,12 +75,9 @@ const PAGE_SIZE = 10;
7575
*/
7676
const loadPage = ({text, catalog = {}}, page = 0) => {
7777
const type = catalog.type;
78-
const _tempOption = {options: {service: catalog}};
7978
let options = {};
80-
if (type === 'csw') {
81-
options = {..._tempOption, filter: catalog.filter};
82-
} else if (type === 'tms') {
83-
options = _tempOption;
79+
if (['csw', 'tms'].includes(type)) {
80+
options = {options: {service: catalog}};
8481
}
8582
return Rx.Observable
8683
.fromPromise(API[type].textSearch(catalog.url, page * PAGE_SIZE + (type === "csw" ? 1 : 0), PAGE_SIZE, text, options))

web/client/components/catalog/__tests__/CatalogServiceEditor-test.jsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ describe('Test CatalogServiceEditor', () => {
5959
/>, document.getElementById("container"));
6060

6161
const formatFormGroups = [...document.querySelectorAll('.form-group-flex')];
62-
expect(formatFormGroups.length).toBe(5);
62+
expect(formatFormGroups.length).toBe(6);
6363
const formatSelect = formatFormGroups[2].querySelector('.Select-value-label');
6464
expect(formatSelect).toExist();
6565
expect(formatSelect.textContent).toBe('image/png8');

web/client/components/catalog/editor/AdvancedSettings/CSWFilters.jsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,8 @@ export default ({
9191
const _filter = template(cswGetRecordsXml)({
9292
filterXml: value,
9393
startPosition: 1,
94-
maxRecords: 4
94+
maxRecords: 4,
95+
sortBy: ''
9596
});
9697
return !new DOMParser()
9798
.parseFromString(_filter, "application/xml")

web/client/components/catalog/editor/AdvancedSettings/RasterAdvancedSettings.js

+37-3
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
* LICENSE file in the root directory of this source tree.
77
*/
88
import React, {useEffect} from 'react';
9-
import {FormGroup, ControlLabel, Checkbox, Button as ButtonRB, Glyphicon, InputGroup } from "react-bootstrap";
9+
import { FormGroup, ControlLabel, Checkbox, Button as ButtonRB, Glyphicon, InputGroup, Tooltip } from "react-bootstrap";
1010
import RS from 'react-select';
1111
import {isNil, camelCase} from "lodash";
1212

@@ -19,6 +19,8 @@ import CSWFilters from "./CSWFilters";
1919
import Message from "../../../I18N/Message";
2020
import WMSDomainAliases from "./WMSDomainAliases";
2121
import tooltip from '../../../misc/enhancers/buttonTooltip';
22+
import OverlayTrigger from '../../../misc/OverlayTrigger';
23+
import FormControl from '../../../misc/DebouncedFormControl';
2224

2325
const Button = tooltip(ButtonRB);
2426
const Select = localizedProps('noResultsText')(RS);
@@ -48,6 +50,7 @@ const getServerTypeOptions = () => {
4850
* - Filters (Option allows user to configure the ogcFilter with custom filtering conditions
4951
* *staticFilter: filter to fetch all record applied always i.e even when no search text is present
5052
* *dynamicFilter: filter when search text is present and is applied in conjunction with static filter
53+
* - sortBy: Configure sort by operation using the propert name (with namespace prefixed) and the sort order. By default the sort order is 'ASC'
5154
*
5255
* **WMS|CSW**
5356
* - tileSize: Option allows to select and configure the default tile size of the layer to be requested with
@@ -218,9 +221,40 @@ export default ({
218221
onChange={event => onChangeServiceProperty("layerOptions", { ...service.layerOptions, tileSize: event && event.value })} />
219222
</InputGroup>
220223
</FormGroup>
224+
221225
{!isNil(service.type) && service.type === "csw" &&
222-
<CSWFilters filter={service?.filter} onChangeServiceProperty={onChangeServiceProperty}/>
223-
}
226+
<>
227+
<hr style={{margin: "8px 0"}}/>
228+
<FormGroup className="form-group-flex sort-by">
229+
<ControlLabel className="strong" style={{ display: 'flex', alignItems: "center" }}>
230+
<Message msgId="catalog.sortBy.label" />
231+
<OverlayTrigger placement={"bottom"} overlay={<Tooltip id={"sortby"}>
232+
<Message msgId={"catalog.sortBy.tooltip"} />
233+
</Tooltip>}>
234+
<Glyphicon
235+
style={{ marginLeft: 4 }}
236+
glyph={"info-sign"}
237+
/>
238+
</OverlayTrigger>
239+
</ControlLabel>
240+
<InputGroup style={{display: "flex"}}>
241+
<FormControl
242+
type="text"
243+
placeholder={"catalog.sortBy.placeholder"}
244+
style={{ textOverflow: "ellipsis", flex: 1.5 }}
245+
value={service?.sortBy?.name}
246+
onChange={value => onChangeServiceProperty("sortBy", {...service?.sortBy, name: value})}
247+
/>
248+
<Select
249+
clearable={false}
250+
wrapperStyle={{ flex: 1 }}
251+
value={service?.sortBy?.order ?? "ASC"}
252+
options={["ASC", "DESC"].map(value => ({value, label: value}))}
253+
onChange={event => onChangeServiceProperty("sortBy", {...service?.sortBy, order: event && event.value})} />
254+
</InputGroup>
255+
</FormGroup>
256+
<CSWFilters filter={service?.filter} onChangeServiceProperty={onChangeServiceProperty}/>
257+
</>}
224258
{!isNil(service.type) && service.type === "wms" && (<WMSDomainAliases service={service} onChangeServiceProperty={onChangeServiceProperty} />)}
225259
</CommonAdvancedSettings>);
226260
};

web/client/components/catalog/editor/AdvancedSettings/__tests__/RasterAdvancedSettings-test.js

+44-1
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,10 @@ describe('Test Raster advanced settings', () => {
4141
expect(advancedSettingPanel).toBeTruthy();
4242
const fields = document.querySelectorAll(".form-group");
4343
const cswFilters = document.getElementsByClassName("catalog-csw-filters");
44-
expect(fields.length).toBe(11);
44+
const sortBy = document.getElementsByClassName("sort-by");
45+
expect(fields.length).toBe(12);
4546
expect(cswFilters).toBeTruthy();
47+
expect(sortBy).toBeTruthy();
4648
});
4749
it('test component onChangeServiceProperty autoload', () => {
4850
const action = {
@@ -262,4 +264,45 @@ describe('Test Raster advanced settings', () => {
262264
expect(spyOn).toHaveBeenCalled();
263265
expect(spyOn.calls[0].arguments).toEqual([ 'infoFormat', 'application/json' ]);
264266
});
267+
it('test component onChangeServiceProperty sortBy change property name', (done) => {
268+
TestUtils.act(() => {
269+
ReactDOM.render(<RasterAdvancedSettings
270+
onChangeServiceProperty={(field, value) => {
271+
try {
272+
expect(field).toBe('sortBy');
273+
expect(value).toEqual({ name: "dc:value", order: 'DESC' });
274+
} catch (e) {
275+
done(e);
276+
}
277+
done();
278+
}}
279+
service={{type: "csw", sortBy: {name: "dc:title", order: "DESC"}}}/>, document.getElementById("container"));
280+
});
281+
282+
const advancedSettingsPanel = document.getElementsByClassName("mapstore-switch-panel");
283+
expect(advancedSettingsPanel).toBeTruthy();
284+
const sortOrder = document.querySelectorAll('input[role="combobox"]')[4];
285+
const sortName = document.querySelectorAll('input[type="text"]')[0];
286+
expect(sortOrder).toBeTruthy();
287+
TestUtils.Simulate.focus(sortName);
288+
TestUtils.Simulate.change(sortName, { target: { value: "dc:value" }});
289+
});
290+
it('test component onChangeServiceProperty sortBy, change sort order', () => {
291+
const action = {
292+
onChangeServiceProperty: () => {}
293+
};
294+
const spyOn = expect.spyOn(action, 'onChangeServiceProperty');
295+
ReactDOM.render(<RasterAdvancedSettings
296+
onChangeServiceProperty={action.onChangeServiceProperty}
297+
service={{type: "csw", sortBy: {name: "dc:title", order: "DESC"}}}/>, document.getElementById("container"));
298+
299+
const advancedSettingsPanel = document.getElementsByClassName("mapstore-switch-panel");
300+
expect(advancedSettingsPanel).toBeTruthy();
301+
const sortOrder = document.querySelectorAll('input[role="combobox"]')[4];
302+
expect(sortOrder).toBeTruthy();
303+
TestUtils.Simulate.change(sortOrder, { target: { value: "ASC" }});
304+
TestUtils.Simulate.keyDown(sortOrder, { keyCode: 9, key: 'Tab' });
305+
expect(spyOn).toHaveBeenCalled();
306+
expect(spyOn.calls[0].arguments).toEqual([ 'sortBy', { name: "dc:title", order: 'ASC' } ]);
307+
});
265308
});

web/client/epics/__tests__/catalog-test.js

-1
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,6 @@ describe('catalog Epics', () => {
165165
expect(actions.length).toBe(NUM_ACTIONS);
166166
expect(actions[0].type).toBe(TEXT_SEARCH);
167167
expect(actions[0].options).toEqual({
168-
filter: "test",
169168
service: {
170169
type: "csw",
171170
url: "url",

web/client/epics/catalog.js

+3-4
Original file line numberDiff line numberDiff line change
@@ -101,10 +101,9 @@ export default (API) => ({
101101
recordSearchEpic: (action$, store) =>
102102
action$.ofType(TEXT_SEARCH)
103103
.switchMap(({ format, url, startPosition, maxRecords, text, options }) => {
104-
const filter = get(options, 'service.filter') || get(options, 'filter');
105104
const isNewService = get(options, 'isNewService', false);
106105
return Rx.Observable.defer(() =>
107-
API[format].textSearch(url, startPosition, maxRecords, text, { options, filter, ...catalogSearchInfoSelector(store.getState()) })
106+
API[format].textSearch(url, startPosition, maxRecords, text, { options, ...catalogSearchInfoSelector(store.getState()) })
108107
)
109108
.switchMap((result) => {
110109
if (result.error) {
@@ -539,8 +538,8 @@ export default (API) => ({
539538
const state = getState();
540539
const pageSize = pageSizeSelector(state);
541540
const service = selectedCatalogSelector(state);
542-
const { type, url, filter } = service;
543-
return Rx.Observable.of(textSearch({ format: type, url, startPosition: 1, maxRecords: pageSize, text, options: { service, filter }}));
541+
const { type, url } = service;
542+
return Rx.Observable.of(textSearch({ format: type, url, startPosition: 1, maxRecords: pageSize, text, options: { service }}));
544543
}),
545544

546545
catalogCloseEpic: (action$, store) =>

web/client/translations/data.de-DE.json

+5
Original file line numberDiff line numberDiff line change
@@ -1687,6 +1687,11 @@
16871687
"allowUnsecureLayers": {
16881688
"label": "Nicht sichere Ebenen zulassen",
16891689
"tooltip": "Das Hinzufügen einer Ebene zur Karte mit aktivierter Option zwingt die Anwendung, Proxy anzuwenden"
1690+
},
1691+
"sortBy": {
1692+
"label": "Sortiere nach",
1693+
"tooltip": "Namespace-Präfix zur Namenseigenschaft hinzufügen. d.h. ${namespace}:${name}",
1694+
"placeholder": "Name eingeben"
16901695
}
16911696
},
16921697
"uploader": {

web/client/translations/data.en-US.json

+5
Original file line numberDiff line numberDiff line change
@@ -1648,6 +1648,11 @@
16481648
"helpTooltip": "This option is used to split content across multiple subdomains",
16491649
"addAliasTooltip": "Add alias",
16501650
"removeAliasTooltip": "Remove alias"
1651+
},
1652+
"sortBy": {
1653+
"label": "Sort By",
1654+
"tooltip": "Add namespace prefix to name property. i.e. ${namespace}:${name}",
1655+
"placeholder": "Enter a name"
16511656
}
16521657
},
16531658
"uploader": {

web/client/translations/data.es-ES.json

+6-1
Original file line numberDiff line numberDiff line change
@@ -1649,7 +1649,12 @@
16491649
"allowUnsecureLayers": {
16501650
"label": "Permitir capas no seguras",
16511651
"tooltip": "Agregar una capa al mapa con esta opción habilitada obliga a la aplicación a aplicar proxy"
1652-
}
1652+
},
1653+
"sortBy": {
1654+
"label": "Ordenar por",
1655+
"tooltip": "Agregue el prefijo del espacio de nombres a la propiedad del nombre. es decir, ${namespace}:${name}",
1656+
"placeholder": "Ingresar nombre"
1657+
}
16531658
},
16541659
"uploader": {
16551660
"filename": "Nombre del fichero",

web/client/translations/data.fr-FR.json

+6-1
Original file line numberDiff line numberDiff line change
@@ -1649,7 +1649,12 @@
16491649
"allowUnsecureLayers": {
16501650
"label": "Autoriser les couches non sécurisées",
16511651
"tooltip": "L'ajout d'une couche à la carte avec cette option activée force l'application à appliquer un proxy"
1652-
}
1652+
},
1653+
"sortBy": {
1654+
"label": "Trier par",
1655+
"tooltip": "Ajoutez un préfixe d’espace de noms à la propriété name. c'est-à-dire ${namespace}:${name}",
1656+
"placeholder": "Entrer un nom"
1657+
}
16531658
},
16541659
"uploader": {
16551660
"filename": "Nom du fichier",

web/client/translations/data.it-IT.json

+6-1
Original file line numberDiff line numberDiff line change
@@ -1648,7 +1648,12 @@
16481648
"allowUnsecureLayers": {
16491649
"label": "Consenti livelli non sicuri",
16501650
"tooltip": "L'aggiunta di un livello da mappare con questa opzione abilitata, costringe l'applicazione ad applicare il proxy"
1651-
}
1651+
},
1652+
"sortBy": {
1653+
"label": "Ordina per",
1654+
"tooltip": "Aggiungi il prefisso dello spazio dei nomi alla proprietà del nome. cioè ${namespace}:${name}",
1655+
"placeholder": "Inserisci un nome"
1656+
}
16521657
},
16531658
"uploader": {
16541659
"filename": "File Name",

0 commit comments

Comments
 (0)