• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

geosolutions-it / MapStore2 / 30263629182

27 Jul 2026 11:52AM UTC coverage: 75.764% (-0.03%) from 75.789%
30263629182

Pull #12664

github

web-flow
Merge d4ed16c34 into 85310a94b
Pull Request #12664: Multi View Identify #12595

36066 of 56768 branches covered (63.53%)

165 of 229 new or added lines in 8 files covered. (72.05%)

116 existing lines in 5 files now uncovered.

44874 of 59229 relevant lines covered (75.76%)

123.14 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

39.31
/web/client/api/GeoNode.js
1
/*
2
 * Copyright 2026, 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 isEmpty from "lodash/isEmpty";
10
import mergeWith from 'lodash/mergeWith';
11
import isArray from 'lodash/isArray';
12
import isString from 'lodash/isString';
13
import castArray from 'lodash/castArray';
14
import omit from 'lodash/omit';
15
import axios from '../libs/ajax';
16
import { addFilters, getFilterByField } from "../utils/ResourcesFiltersUtils";
17
import get from 'lodash/get';
18
import { handleExpression } from "../utils/PluginsUtils";
19
import { getConfigProp } from '../utils/ConfigUtils';
20
import { resolveApiPresetParams, paramsSerializer, mergePresetParams } from '../utils/GeoNodeUtils';
21

22
export const GEONODE_RESOURCE_TYPE_FILTER = 'filter{resource_type.in}';
3✔
23
// default sort applied to the catalog resources request when the caller does not provide one
24
// (matches the "Most recent" default shown in the catalog toolbar)
25
export const GEONODE_DEFAULT_SORT = '-date';
3✔
26

27

28
export const RESOURCES = 'resources';
3✔
29
export const DATASETS = 'datasets';
3✔
30
export const DOCUMENTS = 'documents';
3✔
31
export const MAPS = 'maps';
3✔
32
export const FACETS = 'facets';
3✔
33

34
let endpoints = {
3✔
35
    // default values
36
    'resources': '/api/v2/resources',
37
    'documents': '/api/v2/documents',
38
    'datasets': '/api/v2/datasets',
39
    'maps': '/api/v2/maps',
40
    'geoapps': '/api/v2/geoapps',
41
    'users': '/api/v2/users',
42
    'resource_types': '/api/v2/resources/resource_types',
43
    'categories': '/api/v2/categories',
44
    'owners': '/api/v2/owners',
45
    'keywords': '/api/v2/keywords',
46
    'regions': '/api/v2/regions',
47
    'groups': '/api/v2/groups',
48
    'executionrequest': '/api/v2/executionrequest',
49
    'facets': '/api/v2/facets',
50
    'uploads': '/api/v2/uploads',
51
    'metadata': '/api/v2/metadata',
52
    'assets': '/api/v2/assets',
53
    'rules': '/api/v2/reqrules'
54
};
55

56
function mergeCustomQuery(params, customQuery) {
57
    if (customQuery) {
21!
58
        return mergeWith(
21✔
59
            { ...params },
60
            { ...customQuery },
61
            (objValue, srcValue) => {
62
                if (isArray(objValue) && isArray(srcValue)) {
15!
UNCOV
63
                    return [...objValue, ...srcValue];
×
64
                }
65
                if (isString(objValue) && isArray(srcValue)) {
15✔
66
                    return [objValue, ...srcValue];
3✔
67
                }
68
                if (isArray(objValue) && isString(srcValue)) {
12!
UNCOV
69
                    return [...objValue, srcValue];
×
70
                }
71
                if (isString(objValue) && isString(srcValue)) {
12!
UNCOV
72
                    return [ objValue, srcValue ];
×
73
                }
74
                return undefined; // eslint-disable-line consistent-return
12✔
75
            }
76
        );
77
    }
UNCOV
78
    return params;
×
79
}
80

81
export const getQueryParams = (params, customFilters) => {
3✔
82
    const customQuery = customFilters
15!
83
        .filter(({ id }) => castArray(params?.f ?? []).indexOf(id) !== -1)
6!
84
        .reduce((acc, filter) => mergeCustomQuery(acc, filter.query || {}), {}) || {};
6!
85
    return {
15✔
86
        ...mergeCustomQuery(omit(params, "f"), customQuery)
87
    };
88
};
89

90
export const parseIcon = (item) => {
3✔
91
    let value;
92
    if (typeof item === 'object') {
×
UNCOV
93
        value = item.icon ?? item?.fa_class;
×
94
    } else {
UNCOV
95
        value = item;
×
96
    }
UNCOV
97
    return value?.replace("fa-", "");
×
98
};
99

100
export const mapObjectFunc = func => {
3✔
UNCOV
101
    const iter = value => value && typeof value === 'object'
×
102
        ? Array.isArray(value)
×
103
            ? value.map(iter)
UNCOV
104
            : Object.fromEntries(Object.entries(value).map(([key, val]) => [key, iter(val, func)]))
×
105
        : func(value);
UNCOV
106
    return iter;
×
107
};
108

109
/**
110
 * return all the custom filters available in the GeoNode configuration from localConfig
111
 * @param {object} monitoredState monitored state
112
 */
113
export const getCustomMenuFilters = (monitoredState) => {
3✔
114
    const geoNodeCustomFilters = getConfigProp('geoNodeCustomFilters');
×
115
    const getMonitorState = (path) => {
×
UNCOV
116
        return get(monitoredState, path);
×
117
    };
118
    const parsedGeoNodeCustomFilters = mapObjectFunc(v => handleExpression(getMonitorState, {}, v))(geoNodeCustomFilters || {});
×
119
    const menuFilters = Object.keys(parsedGeoNodeCustomFilters).reduce((acc, id) => {
×
UNCOV
120
        return [...acc, { id, query: parsedGeoNodeCustomFilters[id] }];
×
121
    }, []);
UNCOV
122
    return menuFilters;
×
123
};
124

125

126
export const getEndpointUrl = (baseUrl, endpoint, pk) => {
3✔
127
    const url = endpoints[endpoint] || endpoint;
36✔
128
    if (baseUrl && baseUrl !== FACETS && baseUrl !== RESOURCES) {
36✔
129
        // baseUrl is the full server URL
130
        const cleanBase = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
33✔
131
        return `${cleanBase}${url}${pk ? `/${pk}` : ''}`;
33✔
132
    }
133
    // baseUrl is FACETS or RESOURCES constant, use the endpoint directly
134
    return url;
3✔
135
};
136

137
export { paramsSerializer, mergePresetParams };
138

139
export const getResourceByPk = (baseUrl, pk) => {
3✔
UNCOV
140
    return axios.get(getEndpointUrl(baseUrl, RESOURCES, pk), {
×
141
        params: mergePresetParams('VIEWER_COMMON'),
142
        ...paramsSerializer()
143
    })
UNCOV
144
        .then(({ data }) => data.resource);
×
145
};
146

147

148
export const getDatasetByPk = (baseUrl, pk) => {
3✔
149
    return axios.get(getEndpointUrl(baseUrl, DATASETS, pk), {
3✔
150
        params: mergePresetParams('VIEWER_COMMON', 'DATASET'),
151
        ...paramsSerializer()
152
    })
UNCOV
153
        .then(({ data }) => data.dataset);
×
154
};
155

156
export const getDocumentByPk = (baseUrl, pk) => {
3✔
157
    return axios.get(getEndpointUrl(baseUrl, DOCUMENTS, pk), {
9✔
158
        params: mergePresetParams('VIEWER_COMMON', 'DOCUMENT'),
159
        ...paramsSerializer()
160
    })
161
        .then(({ data }) => data.document);
9✔
162
};
163

164
export const getMapByPk = (baseUrl, pk) => {
3✔
165
    return axios.get(getEndpointUrl(baseUrl, MAPS, pk), {
3✔
166
        params: mergePresetParams('VIEWER_COMMON', 'MAP'),
167
        ...paramsSerializer()
168
    })
169
        .then(({ data }) => data.map);
3✔
170
};
171

172
export const getResources = ({
3✔
173
    q,
174
    pageSize = 10,
×
175
    page = 1,
×
176
    sort,
177
    f,
178
    customFilters = [],
12✔
179
    config,
180
    baseUrl,
181
    apiPresetKey = 'CATALOGS',
12✔
182
    ...params
183
}) => {
184
    const _params = {
12✔
185
        ...getQueryParams({...params, f}, customFilters),
186
        ...(q && {
18✔
187
            search: q,
188
            search_fields: ['title', 'abstract']
189
        }),
190
        ...(sort && { sort: isArray(sort) ? sort : [ sort ]}),
36!
191
        page,
192
        page_size: pageSize,
193
        'filter{metadata_only}': false,
194
        ...resolveApiPresetParams(apiPresetKey)
195
    };
196
    return axios.get(getEndpointUrl(baseUrl, RESOURCES), {
12✔
197
        params: _params,
198
        ...config,
199
        ...paramsSerializer()
200
    }).then(({ data }) => {
201
        const resources = (data.resources || []).map((resource) => resource);
12!
202
        const recordsReturned = Math.min(pageSize, resources.length);
12✔
203
        const nextRecord = data?.links?.next
12✔
204
            ? (data.page * data.page_size) + 1
205
            : 0;
206
        return {
12✔
207
            numberOfRecordsMatched: data.total ?? resources.length,
12!
208
            numberOfRecordsReturned: recordsReturned,
209
            nextRecord,
210
            records: resources
211
        };
212
    });
213
};
214

215

216
export const getRecords = (url, startPosition, maxRecords, text, options) => {
3✔
217
    const service = options?.options?.service;
9✔
218
    const resourceTypes = service?.resourceTypes ?? ['dataset'];
9✔
219
    return getResources({
9✔
220
        q: text,
221
        pageSize: maxRecords,
222
        page: Math.floor((startPosition - 1) / maxRecords) + 1,
223
        baseUrl: url,
224
        ...(resourceTypes.length && { [GEONODE_RESOURCE_TYPE_FILTER]: resourceTypes }),
18✔
225
        ...options?.options?.filters,
226
        sort: options?.options?.sort ?? service?.defaultSort ?? GEONODE_DEFAULT_SORT,
18✔
227
        ...(service?.apiPresetKey && { apiPresetKey: service.apiPresetKey })
9!
228
    });
229
};
230

231
// facets
232
const parseTopicsItems = (items = [], { facet, style }) => {
3!
UNCOV
233
    return items.map((item) => {
×
UNCOV
234
        const value = String(item.key);
×
UNCOV
235
        return {
×
236
            type: "filter",
237
            // TODO remove when api send isLocalized for all facets response
238
            ...(item.is_localized ? { labelId: item.label } : { label: item.label }),
×
239
            count: item.count ?? 0,
×
240
            filterKey: facet.filter,
241
            filterValue: value,
242
            value,
243
            style,
244
            facetName: facet.name,
245
            icon: parseIcon(item.fa_class),
246
            image: item.image
247
        };
248
    });
249
};
250

251
const applyFacetToFields = (fields, facets = [], { customFilters, baseUrl = '' }) => {
3!
252
    return fields.map((field) => {
×
253
        if (field.facet) {
×
UNCOV
254
            const filteredFacetsByType = facets
×
UNCOV
255
                .filter(f => f.type === field.facet)
×
256
                .filter(f => field.include
×
257
                    ? field.include?.includes(f.name)
258
                    : field.exclude
×
259
                        ? !field.exclude?.includes(f.name)
260
                        : true);
261
            if (!filteredFacetsByType.length) {
×
262
                return null;
×
263
            }
264

UNCOV
265
            return filteredFacetsByType.map((facet) => {
×
UNCOV
266
                const facetConfig = facet.config || {};
×
UNCOV
267
                const style = facetConfig.style || field.style;
×
UNCOV
268
                const type = facetConfig.type || field.type;
×
UNCOV
269
                const order = facetConfig.order || field.order;
×
UNCOV
270
                const isLocalized = !!facet.is_localized;
×
UNCOV
271
                const label = facet.label;
×
UNCOV
272
                return {
×
273
                    id: facet.name,
274
                    name: facet.name,
275
                    type,
276
                    style,
277
                    order,
278
                    facet: field.facet,
279
                    ...(isLocalized ? { labelId: label } : { label }),
×
280
                    key: facet.filter,
281
                    loadItems: ({ params, config }) => {
UNCOV
282
                        const { q, ...updatedParams } = getQueryParams(params, customFilters);
×
283
                        return axios.get(getEndpointUrl(baseUrl, `/api/v2/facets/${facet.name}`), {
×
284
                            ...config,
285
                            params: {
286
                                ...(q && { topic_contains: q }),
×
287
                                ...updatedParams
288
                            },
289
                            ...paramsSerializer()
290
                        })
291
                            .then(({ data }) => {
UNCOV
292
                                const topics = data?.topics ?? {};
×
UNCOV
293
                                const pageSize = topics?.page_size;
×
UNCOV
294
                                const page = Number(topics.page);
×
295
                                const total = topics?.total;
×
296
                                const isNextPageAvailable = (Math.ceil(Number(total) / Number(pageSize)) - (page + 1)) !== 0;
×
UNCOV
297
                                const items = parseTopicsItems(topics.items, { facet, style });
×
298

299
                                const filterField = { key: facet.filter, style, name: facet.name };
×
300
                                // if the items are empty and items are selected
301
                                // we should still see them with count equal 0
302
                                // to allow user to deselect the filter
303
                                // TODO: review if possible to move this control in accordion
304
                                const facetQuery = updatedParams[facet.filter];
×
UNCOV
305
                                if (!isEmpty(facetQuery) && items.length === 0) {
×
306

UNCOV
307
                                    const appliedFilters = castArray(facetQuery)
×
UNCOV
308
                                        .map((val) => getFilterByField(filterField, val))
×
UNCOV
309
                                        .map(appliedFilter => ({ ...appliedFilter, count: 0 }));
×
310
                                    // store all filters information
311
                                    addFilters(filterField, appliedFilters);
×
312

313
                                    return {
×
314
                                        isNextPageAvailable,
315
                                        items: appliedFilters
316
                                    };
317
                                }
318

319
                                // store all filters information
UNCOV
320
                                addFilters(filterField, items);
×
321

322
                                return {
×
323
                                    isNextPageAvailable,
324
                                    items
325
                                };
326
                            });
327
                    }
328
                };
329
            });
330
        }
UNCOV
331
        if (field.items) {
×
UNCOV
332
            return {
×
333
                ...field,
334
                items: applyFacetToFields(field.items, facets, { customFilters, baseUrl })
335
            };
336
        }
UNCOV
337
        return field;
×
UNCOV
338
    }).flat().filter(val => val).sort((a, b) => a.order - b.order);
×
339
};
340

341
let facetsCache;
342

343
const findFacetField = (facet, fields) => {
3✔
344
    return fields.filter((field) => field.facet && field.id === facet.name
×
345
        ? true
346
        : field.items
×
347
            ? findFacetField(facet, field.items)
348
            : false).flat()[0];
349
};
350

351
const updateFacets = (fields, facets = [], query = {}, baseUrl = '') => {
3!
352
    const queryFacets = facets.filter(facet => query[facet.filter]);
×
353
    if (queryFacets.length) {
×
354
        return axios.all(queryFacets.map((queryFacet) => {
×
UNCOV
355
            const field = findFacetField(queryFacet, fields);
×
UNCOV
356
            if (!field) {
×
UNCOV
357
                return Promise.resolve({});
×
358
            }
UNCOV
359
            const keys = castArray(query[queryFacet.filter]);
×
UNCOV
360
            const style = queryFacet?.config?.style || field.style;
×
UNCOV
361
            return keys.map((key) => {
×
UNCOV
362
                const { q, ...params } = query;
×
UNCOV
363
                return axios.get(getEndpointUrl(baseUrl, `/api/v2/facets/${queryFacet.name}`), {
×
364
                    params: {
365
                        ...params,
366
                        ...(q && { topic_contains: q }),
×
367
                        include_topics: true,
368
                        key
369
                    },
370
                    ...paramsSerializer()
371
                })
372
                    .then(({ data } = {}) => {
×
UNCOV
373
                        const filterField = { key: queryFacet.filter, style, name: queryFacet.name };
×
UNCOV
374
                        const topics = data?.topics ?? {};
×
375
                        const items = parseTopicsItems(topics.items, { facet: queryFacet, style });
×
376
                        // store all filters information
UNCOV
377
                        addFilters(filterField, items);
×
UNCOV
378
                        return {};
×
379
                    })
UNCOV
380
                    .catch(() => ({}));
×
381
            });
382
        }).flat());
383
    }
384
    return Promise.resolve({});
×
385
};
386

387
export const getFacetItems = ({
3✔
388
    fields,
389
    query,
390
    monitoredState,
391
    baseUrl = ''
×
392
}) => {
393
    const customFilters = getCustomMenuFilters(monitoredState);
×
UNCOV
394
    const updatedParams = getQueryParams(query, customFilters);
×
UNCOV
395
    return (
×
396
        !facetsCache
×
397
            ? axios.get(getEndpointUrl(baseUrl, 'facets'), { params: { include_config: true }})
398
                .then(({ data } = {}) => {
×
UNCOV
399
                    facetsCache = data?.facets;
×
UNCOV
400
                    return { fields: applyFacetToFields(fields, facetsCache, { customFilters, baseUrl }) };
×
401
                })
UNCOV
402
                .catch(() => ({ fields: applyFacetToFields(fields, [], { customFilters, baseUrl }) }))
×
403
            : Promise.resolve({ fields: applyFacetToFields(fields, facetsCache, { customFilters, baseUrl }) })
404
    ).then((payload) => {
405
        // update information of the current selected facet filters
UNCOV
406
        return updateFacets(payload.fields, facetsCache, updatedParams, baseUrl).then(() => payload);
×
407
    });
408
};
409

410

411
export const textSearch = ( url, startPosition, maxRecords, text,  options) =>{
3✔
UNCOV
412
    return getRecords( url, startPosition, maxRecords, text,  options);
×
413
};
414

415
const Api = {
3✔
416
    getRecords,
417
    textSearch
418
};
419

420
export default Api;
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc