diff --git a/docs/developer-guide/local-config.md b/docs/developer-guide/local-config.md
index 24d5f792c0..cf704a2ea6 100644
--- a/docs/developer-guide/local-config.md
+++ b/docs/developer-guide/local-config.md
@@ -180,6 +180,21 @@ Set `selectedService` value to one of the ID of the services object ("Demo CSW S
"format": "image/png8", // image format needs to be configured also inside layerOptions
"tileSize": 512 // determine the default tile size for the catalog, valid for WMS and CSW catalogs
},
+ "filter": { // applicable only for CSW service
+ "staticFilter": "filter is always applied, even when search text is NOT PRESENT",
+ "dynamicFilter": "filter is used when search text is PRESENT and is applied in `AND` with staticFilter. The template is used with ${searchText} placeholder to append search string"
+ }
+}
+```
+CSW service
+
`filter` - For both static and dynamic filter, input only xml element contained within (i.e. Do not enclose the filter value in )
+
Example:
+```javascript
+{
+ "filter": { // Default filter values
+ "staticFilter": "dc:typedatasetdc:typehttp://purl.org/dc/dcmitype/Dataset",
+ "dynamicFilter": "csw:AnyText%${searchText}%"
+ }
}
```
diff --git a/web/client/api/CSW.js b/web/client/api/CSW.js
index e282e3454a..79d15e4a20 100644
--- a/web/client/api/CSW.js
+++ b/web/client/api/CSW.js
@@ -8,7 +8,7 @@
import urlUtil from 'url';
-import { head, last } from 'lodash';
+import { head, last, template } from 'lodash';
import assign from 'object-assign';
import axios from '../libs/ajax';
@@ -25,22 +25,8 @@ const parseUrl = (url) => {
}));
};
-export const constructXMLBody = (startPosition, maxRecords, searchText) => {
- if (!searchText) {
- return `
-
- full
-
-
-
+const defaultStaticFilter =
+ `
dc:type
dataset
@@ -49,45 +35,50 @@ export const constructXMLBody = (startPosition, maxRecords, searchText) => {
dc:type
http://purl.org/dc/dcmitype/Dataset
-
-
-
-
- `;
- }
- return `
-
- full
-
-
-
-
- csw:AnyText
- %${searchText}%
-
-
-
- dc:type
- dataset
-
-
- dc:type
- http://purl.org/dc/dcmitype/Dataset
-
-
-
-
-
-
-`;
+ `;
+
+const defaultDynamicFilter = "" +
+ "csw:AnyText " +
+ "%${searchText}% " +
+ " ";
+
+export const cswGetRecordsXml = ' ' +
+ ' ' +
+ 'full ' +
+ ' ' +
+ ' ' +
+ '${filterXml} ' +
+ ' ' +
+ ' ' +
+ ' ' +
+ '';
+
+/**
+ * Construct XML body to get records from the CSW service
+ * @param {object} [options] the options to pass to withIntersectionObserver enhancer.
+ * @param {number} startPosition
+ * @param {number} maxRecords
+ * @param {string} searchText
+ * @param {object} filter object holds static and dynamic filter configured for the CSW service
+ * @param {object} filter.staticFilter filter to fetch all record applied always i.e even when no search text is present
+ * @param {object} filter.dynamicFilter filter when search text is present and is applied in conjunction with static filter
+ * @return {string} constructed xml string
+ */
+export const constructXMLBody = (startPosition, maxRecords, searchText, {filter} = {}) => {
+ const staticFilter = filter?.staticFilter || defaultStaticFilter;
+ const dynamicFilter = `
+ ${template(filter?.dynamicFilter || defaultDynamicFilter)({searchText})}
+ ${staticFilter}
+ `;
+ return template(cswGetRecordsXml)({filterXml: !searchText ? staticFilter : dynamicFilter, startPosition, maxRecords});
};
/**
@@ -148,7 +139,7 @@ var Api = {
});
});
},
- getRecords: function(url, startPosition, maxRecords, filter) {
+ getRecords: function(url, startPosition, maxRecords, filter, options) {
return new Promise((resolve) => {
require.ensure(['../utils/ogc/CSW', '../utils/ogc/Filter'], () => {
const {CSW, marshaller, unmarshaller } = require('../utils/ogc/CSW');
@@ -157,7 +148,7 @@ var Api = {
value: CSW.getRecords(startPosition, maxRecords, typeof filter !== "string" && filter)
});
if (!filter || typeof filter === "string") {
- body = constructXMLBody(startPosition, maxRecords, filter);
+ body = constructXMLBody(startPosition, maxRecords, filter, options);
}
resolve(axios.post(parseUrl(url), body, { headers: {
'Content-Type': 'application/xml'
@@ -270,9 +261,9 @@ var Api = {
});
});
},
- textSearch: function(url, startPosition, maxRecords, text) {
+ textSearch: function(url, startPosition, maxRecords, text, options) {
return new Promise((resolve) => {
- resolve(Api.getRecords(url, startPosition, maxRecords, text));
+ resolve(Api.getRecords(url, startPosition, maxRecords, text, options));
});
},
workspaceSearch: function(url, startPosition, maxRecords, text, workspace) {
diff --git a/web/client/api/__tests__/CSW-test.js b/web/client/api/__tests__/CSW-test.js
index 25381a4273..7a454d18cb 100644
--- a/web/client/api/__tests__/CSW-test.js
+++ b/web/client/api/__tests__/CSW-test.js
@@ -156,4 +156,19 @@ describe("constructXMLBody", () => {
const body2 = constructXMLBody(1, 5, null);
expect(body2.indexOf("PropertyIsEqualTo")).toNotBe(-1);
});
+ it("construct body with custom filter", () => {
+ const filter = {
+ staticFilter: "dc:typedataset",
+ dynamicFilter: "csw:AnyText${searchText}*"
+ };
+ // With search text
+ let body = constructXMLBody(1, 5, "text", {filter});
+
+ expect(body.indexOf("text*")).toNotBe(-1); // Dynamic filter
+
+ // Empty search
+ body = constructXMLBody(1, 5, null);
+ expect(body.indexOf("dc:type")).toNotBe(-1); // Static filter
+ expect(body.indexOf("text*")).toBe(-1); // Dynamic filter
+ });
});
diff --git a/web/client/components/catalog/CompactCatalog.jsx b/web/client/components/catalog/CompactCatalog.jsx
index 95d3435e24..ca3cdd4da8 100644
--- a/web/client/components/catalog/CompactCatalog.jsx
+++ b/web/client/components/catalog/CompactCatalog.jsx
@@ -74,10 +74,20 @@ const PAGE_SIZE = 10;
/*
* retrieves data from a catalog service and converts to props
*/
-const loadPage = ({text, catalog = {}}, page = 0) => Rx.Observable
- .fromPromise(API[catalog.type].textSearch(catalog.url, page * PAGE_SIZE + (catalog.type === "csw" ? 1 : 0), PAGE_SIZE, text, catalog.type === 'tms' ? {options: {service: catalog}} : {}))
- .map((result) => ({ result, records: getCatalogRecords(catalog.type, result || [], { url: catalog && catalog.url, service: catalog })}))
- .map(({records, result}) => resToProps({records, result, catalog}));
+const loadPage = ({text, catalog = {}}, page = 0) => {
+ const type = catalog.type;
+ const _tempOption = {options: {service: catalog}};
+ let options = {};
+ if (type === 'csw') {
+ options = {..._tempOption, filter: catalog.filter};
+ } else if (type === 'tms') {
+ options = _tempOption;
+ }
+ return Rx.Observable
+ .fromPromise(API[type].textSearch(catalog.url, page * PAGE_SIZE + (type === "csw" ? 1 : 0), PAGE_SIZE, text, options))
+ .map((result) => ({ result, records: getCatalogRecords(type, result || [], { url: catalog && catalog.url, service: catalog })}))
+ .map(({records, result}) => resToProps({records, result, catalog}));
+};
const scrollSpyOptions = {querySelector: ".ms2-border-layout-body .ms2-border-layout-content", pageSize: PAGE_SIZE};
/**
* Compat catalog : Reusable catalog component, with infinite scroll.
diff --git a/web/client/components/catalog/editor/AdvancedSettings/CSWFilters.jsx b/web/client/components/catalog/editor/AdvancedSettings/CSWFilters.jsx
new file mode 100644
index 0000000000..7aaf5de9d8
--- /dev/null
+++ b/web/client/components/catalog/editor/AdvancedSettings/CSWFilters.jsx
@@ -0,0 +1,129 @@
+import React, { useState, useEffect } from "react";
+import {
+ Col,
+ ControlLabel,
+ FormGroup,
+ Glyphicon,
+ Tooltip,
+ HelpBlock
+} from "react-bootstrap";
+import { Controlled as CodeMirror } from "react-codemirror2";
+import "codemirror/lib/codemirror.css";
+import "codemirror/theme/material.css";
+import "codemirror/mode/xml/xml";
+import "codemirror/addon/lint/lint";
+import "codemirror/addon/display/autorefresh";
+import template from "lodash/template";
+import isEqual from "lodash/isEqual";
+import { cswGetRecordsXml } from "../../../../api/CSW";
+import OverlayTrigger from "../../../misc/OverlayTrigger";
+import Message from "../../../I18N/Message";
+
+const options = {
+ mode: "xml",
+ theme: "material",
+ lineNumbers: true,
+ lineWrapping: true,
+ autoRefresh: true,
+ indentUnit: 2,
+ tabSize: 2
+};
+const FilterInfo = ({ glyph = "info-sign", className = "", tooltip }) => (
+
+
+
+);
+
+const tooltip = _type => {
+ let msgId = `catalog.filter.${_type}.info`;
+ if (_type === "error") msgId = `catalog.filter.error`;
+ return (
+
+
+
+ );
+};
+const renderError = (
+
+);
+const renderHelpText = (
+
+
+
+);
+
+const FilterCode = ({ type, code, setCode, error }) => {
+ const filterProp = `${type}Filter`;
+ return (
+
+
+
+
+
+
+ {error[type] && renderError}
+
+
+ {
+ setCode({ ...code, [filterProp]: value });
+ }}
+ />
+ {type === 'dynamic' && renderHelpText}
+
+
+ );
+};
+
+export default ({
+ onChangeServiceProperty = () => {},
+ filter: { staticFilter, dynamicFilter } = {}
+} = {}) => {
+ const [error, setError] = useState({});
+ const [code, setCode] = useState({ staticFilter, dynamicFilter });
+
+ const cmProps = { code, setCode, error };
+ const isValid = value => {
+ const _filter = template(cswGetRecordsXml)({
+ filterXml: value,
+ startPosition: 1,
+ maxRecords: 4
+ });
+ return !new DOMParser()
+ .parseFromString(_filter, "application/xml")
+ ?.getElementsByTagName("parsererror")?.length;
+ };
+
+ useEffect(() => {
+ const validFt = isValid(code.staticFilter);
+ validFt &&
+ !isEqual(code.staticFilter, staticFilter) &&
+ onChangeServiceProperty("filter.staticFilter", code.staticFilter);
+ setError({ ...error, "static": !validFt });
+ }, [code.staticFilter]);
+
+ useEffect(() => {
+ const validFt = isValid(code.dynamicFilter);
+ validFt &&
+ !isEqual(code.dynamicFilter, dynamicFilter) &&
+ onChangeServiceProperty("filter.dynamicFilter", code.dynamicFilter);
+ setError({ ...error, dynamic: !validFt });
+ }, [code.dynamicFilter]);
+
+ return (
+
+
+
+
+ );
+};
diff --git a/web/client/components/catalog/editor/AdvancedSettings/CommonAdvancedSettings.jsx b/web/client/components/catalog/editor/AdvancedSettings/CommonAdvancedSettings.jsx
index a13b1aa83a..18f9ee1041 100644
--- a/web/client/components/catalog/editor/AdvancedSettings/CommonAdvancedSettings.jsx
+++ b/web/client/components/catalog/editor/AdvancedSettings/CommonAdvancedSettings.jsx
@@ -12,6 +12,7 @@ import { FormGroup, Checkbox, Col } from "react-bootstrap";
import Message from "../../../I18N/Message";
import InfoPopover from '../../../widgets/widget/InfoPopover';
+import CSWFilters from "./CSWFilters";
/**
* Common Advanced settings form, used by WMS/CSW/WMTS
@@ -96,5 +97,8 @@ export default ({
)}
{children}
+ {!isNil(service.type) && service.type === "csw" &&
+
+ }
);
diff --git a/web/client/components/catalog/editor/AdvancedSettings/__tests__/CSWFilters-test.js b/web/client/components/catalog/editor/AdvancedSettings/__tests__/CSWFilters-test.js
new file mode 100644
index 0000000000..555327f606
--- /dev/null
+++ b/web/client/components/catalog/editor/AdvancedSettings/__tests__/CSWFilters-test.js
@@ -0,0 +1,70 @@
+/**
+ * Copyright 2021, 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 ReactDOM from 'react-dom';
+import expect from 'expect';
+import CSWFilters from '../CSWFilters';
+
+describe('Test CSWFilters', () => {
+ beforeEach((done) => {
+ document.body.innerHTML = '';
+ setTimeout(done);
+ });
+ afterEach((done) => {
+ ReactDOM.unmountComponentAtNode(document.getElementById("container"));
+ document.body.innerHTML = '';
+ setTimeout(done);
+ });
+ it('creates the component with defaults', () => {
+ ReactDOM.render(, document.getElementById("container"));
+ const [catalog] = document.getElementsByClassName("catalog-csw-filters");
+ expect(catalog).toBeTruthy();
+ });
+ it('render component with filter fields', () => {
+ const filterProp = {staticFilter: "test
", dynamicFilter: "Test
"};
+ ReactDOM.render(, document.getElementById("container"));
+ const [catalog] = document.getElementsByClassName("catalog-csw-filters");
+ expect(catalog).toBeTruthy();
+ const filters = document.getElementsByClassName('react-codemirror2');
+ expect(filters.length).toBe(2);
+
+ const labels = document.getElementsByClassName('control-label');
+ expect(labels.length).toBe(2);
+ expect(labels[0].textContent).toBe('catalog.filter.static.label');
+ expect(labels[1].textContent).toBe('catalog.filter.dynamic.label');
+
+ const codes = document.getElementsByClassName('CodeMirror-line');
+ expect(codes.length).toBe(2);
+ expect(codes[0].textContent).toBe(filterProp.staticFilter);
+ expect(codes[1].textContent).toBe(filterProp.dynamicFilter);
+
+ });
+ it('render component with invalid filter', () => {
+ const filterProp = {dynamicFilter: 'test
'};
+ ReactDOM.render(, document.getElementById("container"));
+ const [catalog] = document.getElementsByClassName("catalog-csw-filters");
+ expect(catalog).toBeTruthy();
+ const filters = document.getElementsByClassName('react-codemirror2');
+ expect(filters.length).toBe(2);
+ const error = document.querySelector('.glyphicon-exclamation-mark');
+ expect(error).toBeTruthy();
+ });
+ it('test component onChangeServiceProperty', () => {
+ const action = {
+ onChangeServiceProperty: () => {}
+ };
+ const spyOn = expect.spyOn(action, 'onChangeServiceProperty');
+ ReactDOM.render(test'}}/>, document.getElementById("container"));
+ const [catalog] = document.getElementsByClassName("catalog-csw-filters");
+ expect(catalog).toBeTruthy();
+ const filters = document.getElementsByClassName('react-codemirror2');
+ expect(filters.length).toBe(2);
+ expect(spyOn).toNotHaveBeenCalled();
+ });
+});
diff --git a/web/client/epics/__tests__/catalog-test.js b/web/client/epics/__tests__/catalog-test.js
index 6449fd2a83..9899225afb 100644
--- a/web/client/epics/__tests__/catalog-test.js
+++ b/web/client/epics/__tests__/catalog-test.js
@@ -68,6 +68,7 @@ describe('catalog Epics', () => {
testEpic(autoSearchEpic, NUM_ACTIONS, changeText(""), (actions) => {
expect(actions.length).toBe(NUM_ACTIONS);
expect(actions[0].type).toBe(TEXT_SEARCH);
+ expect(actions[0].options).toEqual({filter: "test"});
done();
}, {
catalog: {
@@ -76,7 +77,8 @@ describe('catalog Epics', () => {
services: {
"cswCatalog": {
type: "csw",
- url: "url"
+ url: "url",
+ filter: "test"
}
},
pageSize: 2
diff --git a/web/client/epics/catalog.js b/web/client/epics/catalog.js
index 3f59658685..516aeb2f2a 100644
--- a/web/client/epics/catalog.js
+++ b/web/client/epics/catalog.js
@@ -84,8 +84,9 @@ export default (API) => ({
recordSearchEpic: (action$, store) =>
action$.ofType(TEXT_SEARCH)
.switchMap(({ format, url, startPosition, maxRecords, text, options }) => {
+ const filter = get(options, 'service.filter') || get(options, 'filter');
return Rx.Observable.defer(() =>
- API[format].textSearch(url, startPosition, maxRecords, text, { options, ...catalogSearchInfoSelector(store.getState()) })
+ API[format].textSearch(url, startPosition, maxRecords, text, { options, filter, ...catalogSearchInfoSelector(store.getState()) })
)
.switchMap((result) => {
if (result.error) {
@@ -121,10 +122,10 @@ export default (API) => ({
const actions = layers
.filter((l, i) => !!services[sources[i]]) // ignore wrong catalog name
.map((l, i) => {
- const { type: format, url } = services[sources[i]];
+ const { type: format, url, ...service } = services[sources[i]];
const text = layers[i];
return Rx.Observable.defer(() =>
- API[format].textSearch(url, startPosition, maxRecords, text, addLayerOptions).catch(() => ({ results: [] }))
+ API[format].textSearch(url, startPosition, maxRecords, text, {...addLayerOptions, ...service}).catch(() => ({ results: [] }))
).map(r => ({ ...r, format, url, text }));
});
return Rx.Observable.forkJoin(actions)
@@ -377,8 +378,8 @@ export default (API) => ({
.switchMap(({ text }) => {
const state = getState();
const pageSize = pageSizeSelector(state);
- const { type, url } = selectedCatalogSelector(state);
- return Rx.Observable.of(textSearch({ format: type, url, startPosition: 1, maxRecords: pageSize, text }));
+ const { type, url, filter } = selectedCatalogSelector(state);
+ return Rx.Observable.of(textSearch({ format: type, url, startPosition: 1, maxRecords: pageSize, text, options: {filter}}));
}),
catalogCloseEpic: (action$, store) =>
diff --git a/web/client/plugins/widgetbuilder/CatalogServiceEditor.jsx b/web/client/plugins/widgetbuilder/CatalogServiceEditor.jsx
index 8c9c0c93c6..d8fe4c789e 100644
--- a/web/client/plugins/widgetbuilder/CatalogServiceEditor.jsx
+++ b/web/client/plugins/widgetbuilder/CatalogServiceEditor.jsx
@@ -2,6 +2,7 @@ import React, { useState } from 'react';
import { isEmpty } from 'lodash';
import uuid from 'uuid';
+import {set} from "../../utils/ImmutableUtils";
import CatalogServiceEditorComponent from '../../components/catalog/CatalogServiceEditor';
import { DEFAULT_ALLOWED_PROVIDERS } from '../MetadataExplorer';
@@ -48,7 +49,7 @@ export default ({service: defaultService, catalogServices,
if (currentData[property]) {
currentData[property] = typeof value === 'boolean' ? !(currentData[property]) : value;
} else {
- currentData = {...currentData, [property]: value};
+ currentData = set(`${property}`, value, currentData);
}
setService(currentData);
};
diff --git a/web/client/reducers/__tests__/catalog-test.js b/web/client/reducers/__tests__/catalog-test.js
index b94ec0a21e..ac0da650cc 100644
--- a/web/client/reducers/__tests__/catalog-test.js
+++ b/web/client/reducers/__tests__/catalog-test.js
@@ -135,8 +135,12 @@ describe('Test the catalog reducer', () => {
});
it('CHANGE_SERVICE_PROPERTY', () => {
let autoload = true;
- const state = catalog({newService: {}}, {type: CHANGE_SERVICE_PROPERTY, property: "autoload", value: true});
+ let state = catalog({newService: {}}, {type: CHANGE_SERVICE_PROPERTY, property: "autoload", value: true});
expect(state.newService.autoload).toBe(autoload);
+
+ // Path as property value
+ state = catalog({newService: {}}, {type: CHANGE_SERVICE_PROPERTY, property: "filter.staticFilter", value: "test"});
+ expect(state.newService.filter.staticFilter).toBe("test");
});
it('SAVING_SERVICE', () => {
let saving = true;
diff --git a/web/client/reducers/catalog.js b/web/client/reducers/catalog.js
index 7d159ecc1b..c7b5082730 100644
--- a/web/client/reducers/catalog.js
+++ b/web/client/reducers/catalog.js
@@ -127,7 +127,7 @@ function catalog(state = {
case CHANGE_TEXT:
return set("searchOptions.text", action.text, state);
case CHANGE_SERVICE_PROPERTY: {
- return set(`newService["${action.property}"]`, action.value, state);
+ return set(`newService.${action.property}`, action.value, state);
}
case CHANGE_TITLE:
return set("newService.title", action.title, state);
diff --git a/web/client/translations/data.de-DE.json b/web/client/translations/data.de-DE.json
index ae1f370686..454863da98 100644
--- a/web/client/translations/data.de-DE.json
+++ b/web/client/translations/data.de-DE.json
@@ -1459,6 +1459,18 @@
"format": {
"noOption": "Keine Option",
"loading": "Wird geladen..."
+ },
+ "filter": {
+ "static": {
+ "label": "Statischer Filter",
+ "info": "Filter wird immer angewendet, auch bei leerer Suche"
+ },
+ "dynamic": {
+ "label": "Dynamischer Filter",
+ "info": "Filter wird verwendet, wenn Suchtext vorhanden ist und wird in 'UND' mit statischem Filter angewendet",
+ "helpText": "Verwenden Sie eine Vorlage mit dem Platzhalter ${searchText}, um die Suchzeichenfolge zu erfassen"
+ },
+ "error": "Ungültige XML-Syntax"
}
},
"uploader": {
diff --git a/web/client/translations/data.en-US.json b/web/client/translations/data.en-US.json
index 2eb2903d1f..e4e5fdc58d 100644
--- a/web/client/translations/data.en-US.json
+++ b/web/client/translations/data.en-US.json
@@ -1421,6 +1421,18 @@
"format": {
"noOption": "No option",
"loading": "Loading..."
+ },
+ "filter": {
+ "static": {
+ "label": "Static Filter",
+ "info": "Filter is applied always, even in empty search"
+ },
+ "dynamic": {
+ "label": "Dynamic Filter",
+ "info": "Filter is used when search text is present and is applied in 'AND' with static filter",
+ "helpText": "Use template with ${searchText} placeholder to capture search string"
+ },
+ "error": "Invalid xml syntax"
}
},
"uploader": {
diff --git a/web/client/translations/data.es-ES.json b/web/client/translations/data.es-ES.json
index bf20e98519..00f62d7612 100644
--- a/web/client/translations/data.es-ES.json
+++ b/web/client/translations/data.es-ES.json
@@ -1421,6 +1421,18 @@
"format": {
"noOption": "Sin opcion",
"loading": "Cargando..."
+ },
+ "filter": {
+ "static": {
+ "label": "Filtro estático",
+ "info": "El filtro se aplica siempre, incluso en la búsqueda vacía"
+ },
+ "dynamic": {
+ "label": "Filtro dinámico",
+ "info": "El filtro se usa cuando el texto de búsqueda está presente y se aplica en 'AND' con un filtro estático",
+ "helpText": "Utilice la plantilla con el marcador de posición $ {searchText} para capturar la cadena de búsqueda"
+ },
+ "error": "Sintaxis XML no válida"
}
},
"uploader": {
diff --git a/web/client/translations/data.fr-FR.json b/web/client/translations/data.fr-FR.json
index 7459a2716c..62caf14fb9 100644
--- a/web/client/translations/data.fr-FR.json
+++ b/web/client/translations/data.fr-FR.json
@@ -1422,6 +1422,18 @@
"format": {
"noOption": "Pas d'option",
"loading": "Chargement..."
+ },
+ "filter": {
+ "static": {
+ "label": "Filtre statique",
+ "info": "Le filtre est toujours appliqué, même dans une recherche vide"
+ },
+ "dynamic": {
+ "label": "Filtre dynamique",
+ "info": "Le filtre est utilisé lorsque le texte de recherche est présent et est appliqué dans 'AND' avec un filtre statique",
+ "helpText": "Utilisez un modèle avec un espace réservé ${searchText} pour capturer la chaîne de recherche"
+ },
+ "error": "Syntaxe xml non valide"
}
},
"uploader": {
diff --git a/web/client/translations/data.it-IT.json b/web/client/translations/data.it-IT.json
index f67b3a582e..14d37f852e 100644
--- a/web/client/translations/data.it-IT.json
+++ b/web/client/translations/data.it-IT.json
@@ -1422,6 +1422,18 @@
"format": {
"noOption": "Nessuna opzione",
"loading": "Caricamento in corso..."
+ },
+ "filter": {
+ "static": {
+ "label": "Filtro statico",
+ "info": "Il filtro viene applicato sempre, anche nella ricerca vuota"
+ },
+ "dynamic": {
+ "label": "Filtro dinamico",
+ "info": "Il filtro viene utilizzato quando è presente il testo di ricerca e viene applicato in 'AND' con filtro statico",
+ "helpText": "Usa il segnaposto ${searchText} nel modello per inserire la stringa di ricerca"
+ },
+ "error": "Sintassi xml non valida"
}
},
"uploader": {