+ );
});
const hasSession = (state) => state?.usersession?.session;
@@ -48,34 +190,54 @@ const loadUserSessionEpic = loadUserSessionEpicCreator();
const removeUserSessionEpic = removeUserSessionEpicCreator();
/**
- * `UserSession` plugin allows the user to automatically save the current map and restore it on the second access to the same map/context, without the necessity to save.
+ * `UserSession` plugin allows the user to automatically save the current configuration and restore it on the second access to the same map/context, without the necessity to save.
*
* User sessions persistence can be configured to use different storages. The default implementation is based on `localStorage`,
* so the session is saved on the user's browser.
*
- * It is also possible to save the session on a database, so it can be shared on different browsers / devices.
+ * It is also possible to save the session on a database, so it can be shared on different browsers/devices.
*
* The user session workflow works this way:
*
- * * a session is identified by the combination of the current map and user identifiers (so that a session exists for each user / map combination)
- * * a session is loaded from the store and if it exists, it overrides the standard map configuration partially; by default current map centering and zoom are overridden
- * * the session is automatically saved at a configurable interval
- * * an item in the `BurgerMenu` allows to restore the session to the default map configuration
+ * - A session is identified by the combination of the current map and user identifiers (so that a session exists for each user/map combination).
+ * - A session is loaded from the store and, if it exists, it overrides the standard map configuration.
+ * - The session is automatically saved at a configurable interval:
+ * - **Map**:
+ * - Zoom and center
+ * - Visualization Mode (3D/2D)
+ * - **Layers**:
+ * - Annotations Layer
+ * - Measurements Layer
+ * - Background Layers
+ * - Other Layers
+ * - **Catalog Services**
+ * - **Widgets**
+ * - **Search**:
+ * - Text Search Services
+ * - Bookmarks
+ * - **Feature Grid**
+ * - **Other**:
+ * - Table of Contents Configuration
+ * - Playback Configuration
+ * - Map Templates
+ * - Map Views
+ * - User Plugins
+ * - An item with a Brush icon in the `BurgerMenu` and `SidebarMenu` allows opening the Session Tree, which can be used to restore parts of the session individually.
*
- * Since user session handling works very low level, its basic features needs to be configured at the `localConfig.json`, globally, in the dedicated `userSession` object.
- * Then including or not including the plugin `UserSession` in your application context will determine the possibility to save (and so restore) the session.
+ * Since user session handling works at a very low level, its basic features need to be configured in the `localConfig.json` file, globally, in the dedicated `userSession` object.
+ * Including or excluding the `UserSession` plugin in your application context determines the possibility to save (and restore) the session.
*
* The `userSession` object in `localConfig.json` can be configured with the following properties:
*
- * * `enabled`: 'false' / 'true'. Enables the functionality at global level.
- * * `saveFrequency`: interval (in milliseconds) between saves
- * * `provider`: the name of the storage provider to use. The options are:
- * * `browser`: (default) localStorage based
- * * `server`: database storage (based on MapStore backend services)
- * * `serverbackup`: combination of browser and server, with a configurable backupFrequency interval, so that browser saving it's more frequent than server one
- * * `contextOnly`: true / false, when true each MapStore context will share only one session, if false each context sub-map will have its own session
+ * - `enabled`: `false` / `true`. Enables the functionality at the global level.
+ * - `saveFrequency`: Interval (in milliseconds) between saves.
+ * - `provider`: The name of the storage provider to use. The options are:
+ * - `browser`: (default) localStorage based
+ * - `server`: Database storage (based on MapStore backend services).
+ * - `serverbackup`: Combination of browser and server, with a configurable `backupFrequency` interval, so that browser saving is more frequent than server saving.
+ * - `contextOnly`: `true` / `false`. When `true`, each MapStore context will share only one session; if `false`, each context sub-map will have its own session.
*
- * You can also implement your own, by defining its API and registering it on the Providers object:
+ * You can also implement your own storage provider by defining its API and registering it on the Providers object:
*
* ```javascript
* import {Providers} from "api/usersession"
@@ -85,6 +247,8 @@ const removeUserSessionEpic = removeUserSessionEpicCreator();
* removeSession: ...
* }
* ```
+ *
+ *
* @memberof plugins
* @name UserSession
* @class
@@ -96,7 +260,7 @@ export default createPlugin('UserSession', {
name: 'UserSession',
position: 1500,
text: ,
- icon: ,
+ icon: ,
action: toggleControl.bind(null, 'resetUserSession', null),
tooltip: ,
selector: (state) => {
@@ -108,7 +272,7 @@ export default createPlugin('UserSession', {
SidebarMenu: {
name: 'UserSession',
position: 1500,
- icon: ,
+ icon: ,
text: ,
action: toggleControl.bind(null, 'resetUserSession', null),
tooltip: "userSession.tooltip",
@@ -123,6 +287,6 @@ export default createPlugin('UserSession', {
usersession
},
epics: {
- saveUserSessionEpic, autoSaveSessionEpic, stopSaveSessionEpic, loadUserSessionEpic, removeUserSessionEpic, reloadOriginalConfigEpic
+ saveUserSessionEpic, autoSaveSessionEpic, stopSaveSessionEpic, loadUserSessionEpic, removeUserSessionEpic, reloadOriginalConfigEpic, setSessionToDynamicReducers, clearSessionIfPluginMissingEpic
}
});
diff --git a/web/client/plugins/session/Tree.jsx b/web/client/plugins/session/Tree.jsx
new file mode 100644
index 0000000000..aa2825c2c0
--- /dev/null
+++ b/web/client/plugins/session/Tree.jsx
@@ -0,0 +1,203 @@
+import React, { useEffect, useState, useRef } from "react";
+import { Glyphicon } from "react-bootstrap";
+import Message from "../../components/I18N/Message";
+
+// Helper to find a node by id in the tree
+function findNodeById(tree, id) {
+ for (let node of tree) {
+ if (node.id === id) return node;
+ if (node.children) {
+ const found = findNodeById(node.children, id);
+ if (found) return found;
+ }
+ }
+ return null;
+}
+
+// Handle checked states of children and parents
+function updateCheckedStatus(data, updatedId, newChecked) {
+ // Helper function to propagate changes up to parents
+ function updateParents(nodeId) {
+ const parentNode = findNodeById(data, nodeId);
+ if (!parentNode) return;
+
+ const childStates = parentNode.children.map((child) => ({
+ checked: child.checked,
+ indeterminate: child.indeterminate || false
+ }));
+
+ const allChildrenChecked = childStates.every((state) => state.checked);
+ const someChildrenChecked = childStates.some(
+ (state) => state.checked || state.indeterminate
+ );
+
+ parentNode.checked = allChildrenChecked;
+ parentNode.indeterminate = !allChildrenChecked && someChildrenChecked;
+
+ updateParents(parentNode.parentId); // Recurse upward by parentId
+ }
+
+ // Helper function to propagate changes down to children
+ // eslint-disable-next-line no-shadow
+ function updateChildren(node, newChecked) {
+ if (node.children) {
+ node.children.forEach((child) => {
+ child.checked = newChecked; // Set each child's checked status
+ child.indeterminate = false; // Reset indeterminate state for all children
+ updateChildren(child, newChecked); // Recurse down to children
+ });
+ }
+ }
+
+ // Helper function to process each node
+ function processNode(node, parentId) {
+ // Attach reference to parentId for upward flow
+ node.parentId = parentId;
+
+ if (node.id === updatedId) {
+ // Update this node's checked status
+ node.checked = newChecked;
+
+ // Update children
+ updateChildren(node, newChecked);
+
+ // Update ancestors (parents, grandparents)
+ updateParents(parentId);
+ } else if (node.children) {
+ // Recursively process children
+ node.children.forEach((child) => processNode(child, node.id));
+ }
+
+ // If this node has children, calculate its indeterminate state
+ if (node.children) {
+ const childStates = node.children.map((child) => ({
+ checked: child.checked,
+ indeterminate: child.indeterminate || false
+ }));
+
+ const allChildrenChecked = childStates.every((state) => state.checked);
+ const someChildrenChecked = childStates.some(
+ (state) => state.checked || state.indeterminate
+ );
+
+ node.checked = allChildrenChecked;
+ node.indeterminate = !allChildrenChecked && someChildrenChecked;
+ }
+ }
+
+ // Process the root-level nodes
+ data.forEach((node) => processNode(node, null));
+ return data;
+}
+
+const TreeNode = ({ node, onToggle, onCheck }) => {
+ const [isExpanded, setIsExpanded] = useState(true);
+ const inputRef = useRef(null);
+
+ const handleToggle = () => {
+ setIsExpanded(!isExpanded);
+ onToggle(node);
+ };
+
+ const handleCheck = (event) => {
+ onCheck(node, event.target.checked);
+ };
+
+ // Since indeterminate is not a React state property, handle it through ref
+ useEffect(() => {
+ if (node.indeterminate) {
+ inputRef.current.indeterminate = true;
+ } else {
+ inputRef.current.indeterminate = false;
+ }
+ }, [node.indeterminate]);
+
+ return (
+
+
+ {node.children && (
+
+ )}
+
+
+
+ {isExpanded && node.children && (
+
+ {node.children.map((childNode) => (
+
+ ))}
+
+ )}
+
+ );
+};
+
+const Tree = ({ data = [], onTreeUpdate = () => {} }) => {
+ const [treeData, setTreeData] = useState(data);
+
+ const handleToggle = (node) => {
+ const updateTree = (nodes) =>
+ nodes.map((n) => {
+ if (n.id === node.id) {
+ n.isExpanded = !n.isExpanded;
+ }
+ if (n.children) {
+ n.children = updateTree(n.children);
+ }
+ return n;
+ });
+ setTreeData(updateTree(treeData));
+ };
+
+ const handleCheck = (node, isChecked) => {
+ const updateTree = (nodes) =>
+ nodes.map((n) => {
+ if (n.id === node.id) {
+ n.checked = isChecked;
+ }
+ if (n.children) {
+ n.children = updateTree(n.children);
+ }
+ return n;
+ });
+ const nodeToUpdate = updateTree(treeData);
+ // updateCheckedStatus is used to update children's and parent's checked, indeterminate state if needed
+ setTreeData(updateCheckedStatus(nodeToUpdate, node.id, isChecked));
+ };
+
+ // If treeData is changed in any way, pass treeData to parent using onTreeUpdate
+ useEffect(() => {
+ onTreeUpdate(treeData);
+ }, [treeData]);
+
+ return (
+
+ {treeData.map((node) => (
+
+ ))}
+
+ );
+};
+
+export default Tree;
diff --git a/web/client/plugins/session/__tests__/tree-test.jsx b/web/client/plugins/session/__tests__/tree-test.jsx
new file mode 100644
index 0000000000..619e00e56a
--- /dev/null
+++ b/web/client/plugins/session/__tests__/tree-test.jsx
@@ -0,0 +1,235 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import TestUtils from 'react-dom/test-utils';
+import Tree from '../Tree';
+import expect from 'expect';
+
+
+describe('Session Tree', () => {
+ beforeEach((done) => {
+ document.body.innerHTML = '';
+ setTimeout(done);
+ });
+
+ afterEach((done) => {
+ ReactDOM.unmountComponentAtNode(document.getElementById("container"));
+ document.body.innerHTML = '';
+ setTimeout(done);
+ });
+ it('Render Trees from props with default checked: true for all', () => {
+ const handlers = { onTreeUpdate: () => {} };
+
+ ReactDOM.render(
+ ,
+ document.getElementById("container")
+ );
+
+ // Check that all checkboxes are rendered and checked by default
+ expect(document.getElementById("node-checkbox-everything").checked).toBe(true);
+ expect(document.getElementById("node-checkbox-layers").checked).toBe(true);
+ expect(document.getElementById("node-checkbox-annotations").checked).toBe(true);
+ expect(document.getElementById("node-checkbox-measurements").checked).toBe(true);
+ expect(document.getElementById("node-checkbox-Toc").checked).toBe(true);
+ });
+
+ it('If parent is checked, children should be checked', () => {
+
+ ReactDOM.render(
+ ,
+ document.getElementById("container")
+ );
+
+ // Initially, check child checkboxes for 'layers'
+ expect(document.getElementById("node-checkbox-layers").checked).toBe(false);
+
+ // Initially, check parent checkbox for 'everything'
+ TestUtils.Simulate.change(document.getElementById("node-checkbox-everything"), { target: { checked: true } });
+
+ // Check that all child checkboxes are checked
+ expect(document.getElementById("node-checkbox-layers").checked).toBe(true);
+ expect(document.getElementById("node-checkbox-annotations").checked).toBe(true);
+ expect(document.getElementById("node-checkbox-measurements").checked).toBe(true);
+ expect(document.getElementById("node-checkbox-Toc").checked).toBe(true);
+ });
+
+ it('If a child is checked, parent should be indeterminate', () => {
+
+
+ ReactDOM.render(
+ ,
+ document.getElementById("container")
+ );
+
+ // Initially, 'everything' parent checkbox is checked, 'annotations' child checkbox is checked
+ TestUtils.Simulate.change(document.getElementById("node-checkbox-annotations"), { target: { checked: false } });
+
+ // Check that the 'everything' parent checkbox is indeterminate
+ const everythingCheckbox = document.getElementById("node-checkbox-everything");
+ expect(everythingCheckbox.indeterminate).toBe(true);
+ });
+
+ it('If parent is unchecked, children should be unchecked', () => {
+
+ ReactDOM.render(
+ ,
+ document.getElementById("container"));
+
+ TestUtils.Simulate.change(document.getElementById("node-checkbox-everything"), { target: { checked: false } });
+
+ // Ensure all child checkboxes are unchecked
+ expect(document.getElementById("node-checkbox-layers").checked).toBe(false);
+ expect(document.getElementById("node-checkbox-annotations").checked).toBe(false);
+ expect(document.getElementById("node-checkbox-measurements").checked).toBe(false);
+ expect(document.getElementById("node-checkbox-Toc").checked).toBe(false);
+ });
+
+ it('If all children are checked, parent should be checked', () => {
+
+
+ ReactDOM.render(
+ ,
+ document.getElementById("container"));
+
+
+ // Initially check all child checkboxes
+ TestUtils.Simulate.change(document.getElementById("node-checkbox-annotations"), { target: { checked: true } });
+ TestUtils.Simulate.change(document.getElementById("node-checkbox-measurements"), { target: { checked: true } });
+
+ // Check that the 'layers' parent checkbox is checked
+ expect(document.getElementById("node-checkbox-layers").checked).toBe(true);
+ expect(document.getElementById("node-checkbox-everything").checked).toBe(true);
+ });
+
+ it('On Tree Update check callback', () => {
+ const onTreeUpdate = () => {
+
+ };
+ const handlers = {
+ onTreeUpdate
+ };
+ const spyOnTreeUpdate = expect.spyOn(handlers, 'onTreeUpdate');
+ ReactDOM.render(, document.getElementById("container"));
+
+ TestUtils.Simulate.change(document.getElementById(`node-checkbox-layers`), {target: {checked: true}});
+ expect(document.getElementById("node-checkbox-layers").checked).toBe(true);
+ expect(spyOnTreeUpdate).toHaveBeenCalled();
+ expect(spyOnTreeUpdate.calls[0].arguments[0]).toExist();
+
+ });
+
+
+});
diff --git a/web/client/reducers/__tests__/usersession-test.js b/web/client/reducers/__tests__/usersession-test.js
index eae2b4ed7b..e8de736f48 100644
--- a/web/client/reducers/__tests__/usersession-test.js
+++ b/web/client/reducers/__tests__/usersession-test.js
@@ -36,9 +36,8 @@ describe('Test the usersession reducer', () => {
expect(state.loading.name).toBe("loading");
});
it('user session removed', () => {
- const state = usersession({id: 1, session: {attribute: "mysession"}}, { type: USER_SESSION_REMOVED });
- expect(state.session).toNotExist();
- expect(state.id).toNotExist();
+ const state = usersession({id: 1, session: {attribute: "mysession"}}, { type: USER_SESSION_REMOVED, newSession: {attribute: "myNewSession"} });
+ expect(state.session).toEqual({attribute: "myNewSession"});
});
it('save map config', () => {
const state = usersession({}, { type: SAVE_MAP_CONFIG, config: {} });
diff --git a/web/client/reducers/context.js b/web/client/reducers/context.js
index 17213f8f6a..30eed0b9dd 100644
--- a/web/client/reducers/context.js
+++ b/web/client/reducers/context.js
@@ -8,6 +8,7 @@
import { SET_CURRENT_CONTEXT, LOADING, SET_RESOURCE, CLEAR_CONTEXT, UPDATE_USER_PLUGIN } from "../actions/context";
import { find, get } from 'lodash';
import {set, arrayUpdate} from '../utils/ImmutableUtils';
+import { MAP_CONFIG_LOADED } from "../actions/config";
/**
* Reducers for context page and configs.
@@ -31,6 +32,12 @@ import {set, arrayUpdate} from '../utils/ImmutableUtils';
*/
export default (state = {}, action) => {
switch (action.type) {
+ case MAP_CONFIG_LOADED: {
+
+ return set('currentContext.userPlugins',
+ action.config?.context?.userPlugins ?? state.resource?.data?.userPlugins
+ , state);
+ }
case SET_CURRENT_CONTEXT: {
return set('currentContext', action.context, state);
}
diff --git a/web/client/reducers/maptemplates.js b/web/client/reducers/maptemplates.js
index d551979f1f..37261207f3 100644
--- a/web/client/reducers/maptemplates.js
+++ b/web/client/reducers/maptemplates.js
@@ -10,12 +10,16 @@ import { CLEAR_MAP_TEMPLATES, SET_TEMPLATES, TOGGLE_FAVOURITE_TEMPLATE, SET_TEMP
SET_MAP_TEMPLATES_LOADED, SET_ALLOWED_TEMPLATES } from "../actions/maptemplates";
import { get } from 'lodash';
import { set } from '../utils/ImmutableUtils';
+import { MAP_CONFIG_LOADED } from "../actions/config";
export default (state = {}, action) => {
switch (action.type) {
case CLEAR_MAP_TEMPLATES: {
return {};
}
+ case MAP_CONFIG_LOADED: {
+ return set('templates', action.config?.mapTemplates, state) ?? [];
+ }
case SET_TEMPLATES: {
return set('templates', action.templates, state);
}
diff --git a/web/client/reducers/playback.js b/web/client/reducers/playback.js
index e4e879bfb9..6cb820eb6e 100644
--- a/web/client/reducers/playback.js
+++ b/web/client/reducers/playback.js
@@ -16,6 +16,7 @@ import {
import { RESET_CONTROLS } from '../actions/controls';
import { set } from '../utils/ImmutableUtils';
+import { MAP_CONFIG_LOADED } from '../actions/config';
const DEFAULT_SETTINGS = {
timeStep: 1,
@@ -29,6 +30,10 @@ export default (state = { status: STATUS.STOP, currentFrame: -1, settings: DEFAU
case INIT: {
return {...state, ...action.payload};
}
+ case MAP_CONFIG_LOADED: {
+ const playbackConfig = action.config?.playback || {};
+ return {...playbackConfig};
+ }
case PLAY: {
return set(`status`, STATUS.PLAY, state);
}
diff --git a/web/client/reducers/usersession.js b/web/client/reducers/usersession.js
index 509a5edc61..197f177c30 100644
--- a/web/client/reducers/usersession.js
+++ b/web/client/reducers/usersession.js
@@ -7,7 +7,27 @@
*/
import {
USER_SESSION_SAVED, USER_SESSION_LOADING, USER_SESSION_LOADED, USER_SESSION_REMOVED, ENABLE_AUTO_SAVE,
- SAVE_MAP_CONFIG } from "../actions/usersession";
+ SAVE_MAP_CONFIG, SET_CHECKED_SESSION_TO_CLEAR } from "../actions/usersession";
+
+// move to utils
+function getCheckedIds(nodes) {
+ let ids = [];
+
+ // Iterate over each node in the list
+ nodes.forEach(node => {
+ // If the node is checked, add its ID to the result array
+ if (node.checked) {
+ ids.push(node.id);
+ }
+
+ // If the node has children, recursively check them
+ if (node.children) {
+ ids = ids.concat(getCheckedIds(node.children));
+ }
+ });
+
+ return ids;
+}
/**
* Handles state for userSession
@@ -23,7 +43,10 @@ import {
* @name usersession
* @memberof reducers
*/
-export default (state = {}, action) => {
+export default (state = {
+ autoSave: false,
+ checkedSessionToClear: []
+}, action) => {
switch (action.type) {
case ENABLE_AUTO_SAVE: {
return {
@@ -54,14 +77,18 @@ export default (state = {}, action) => {
case USER_SESSION_REMOVED:
return {
...state,
- id: undefined,
- session: undefined
+ session: action.newSession
};
case SAVE_MAP_CONFIG:
return {
...state,
config: action.config
};
+ case SET_CHECKED_SESSION_TO_CLEAR:
+ return {
+ ...state,
+ checkedSessionToClear: getCheckedIds(action.checks)
+ };
default:
return state;
}
diff --git a/web/client/selectors/__tests__/usersession-test.js b/web/client/selectors/__tests__/usersession-test.js
index 3156aa9441..65ad7a508f 100644
--- a/web/client/selectors/__tests__/usersession-test.js
+++ b/web/client/selectors/__tests__/usersession-test.js
@@ -91,15 +91,26 @@ describe('Test usersession selector', () => {
expect(userSessionNameSelector({context: {resource: {id: "c"}}, mapInitialConfig: {mapId: "m"}, security: { user: {name: "user"} }})).toBe("c.m.user");
});
it('test userSessionToSaveSelector', () => {
- const state = userSessionToSaveSelector({layers: {flat: [{}], groups: [{}]}, map: { center: {x: 10, y: 40}, zoom: 6}, featuregrid: {attributes: {col1: {hide: true}}}});
+ const state = userSessionToSaveSelector({
+ map: {
+ present: {
+ center: {
+ x: 10,
+ y: 40,
+ crs: 'EPSG:4326'
+ },
+ zoom: 3
+ }
+ },
+ layers: {flat: [{}], groups: [{}]},
+ featuregrid: {attributes: {col1: {hide: true}}}
+ });
expect(state).toBeTruthy();
expect(state.map).toBeTruthy();
- expect(state.map.zoom).toBe(6);
+ expect(state.map.zoom).toBe(3);
expect(state.map.center.x).toBe(10);
expect(state.map.center.y).toBe(40);
expect(state.map.layers).toBeTruthy();
- expect(state.map.layers.length).toBe(1);
- expect(state.map.groups.length).toBe(1);
expect(state.featureGrid).toBeTruthy();
expect(state.featureGrid.attributes).toBeTruthy();
});
diff --git a/web/client/selectors/mapsave.js b/web/client/selectors/mapsave.js
index 237b4b3f4b..11e8d0630c 100644
--- a/web/client/selectors/mapsave.js
+++ b/web/client/selectors/mapsave.js
@@ -34,6 +34,9 @@ export const registerCustomSaveHandler = (section, handler) => {
delete customSaveHandlers[section];
}
};
+export const getRegisterHandlers = () => {
+ return Object.keys(customSaveHandlers);
+};
export const basicMapOptionsToSaveSelector = createStructuredSelector({
catalogServices: createStructuredSelector({
diff --git a/web/client/selectors/maptemplates.js b/web/client/selectors/maptemplates.js
index 3cb679c95c..829fe6df5e 100644
--- a/web/client/selectors/maptemplates.js
+++ b/web/client/selectors/maptemplates.js
@@ -8,6 +8,7 @@
import { createSelector } from 'reselect';
import { templatesSelector as contextTemplatesSelector } from './context';
import {get} from "lodash";
+import { registerCustomSaveHandler } from './mapsave';
export const isActiveSelector = (state) => get(state, "controls.mapTemplates.enabled");
export const mapTemplatesLoadedSelector = state => state.maptemplates && state.maptemplates.mapTemplatesLoaded;
@@ -16,6 +17,8 @@ export const templatesSelector = state => state.maptemplates && state.maptemplat
export const allowedTemplatesSelector = state => state.maptemplates && state.maptemplates.allowedTemplates;
+registerCustomSaveHandler('mapTemplates', templatesSelector);
+
// This selector checks state for localConfigTemplates that were loaded into state from localConfig
// when MapTemplates plugin was mounting. At the moment, for retro-compatibility it also checks context for plugins
// that're stored there and then merges all the templates.
diff --git a/web/client/selectors/playback.js b/web/client/selectors/playback.js
index 5ac20ad487..b63d67d28c 100644
--- a/web/client/selectors/playback.js
+++ b/web/client/selectors/playback.js
@@ -6,6 +6,7 @@
* LICENSE file in the root directory of this source tree.
*/
import { createSelector } from 'reselect';
+import { registerCustomSaveHandler } from './mapsave';
export const playbackSettingsSelector = state => state && state.playback && state.playback.settings;
export const frameDurationSelector = state => ((playbackSettingsSelector(state) || {}).frameDuration || 5); // seconds
@@ -35,3 +36,7 @@ export const hasPrevNextAnimationSteps = createSelector(
hasPrevious: frames[index - 1]
})
);
+
+export const playbackSelector = state => state.playback;
+
+registerCustomSaveHandler('playback', playbackSelector);
diff --git a/web/client/selectors/usersession.js b/web/client/selectors/usersession.js
index 3d02ec19a9..1c30585d89 100644
--- a/web/client/selectors/usersession.js
+++ b/web/client/selectors/usersession.js
@@ -8,36 +8,31 @@
import ConfigUtils from '../utils/ConfigUtils';
import { createSelector } from "reselect";
-import { contextResourceSelector } from "./context";
+import { contextResourceSelector, userPluginsSelector } from "./context";
import { userSelector } from "./security";
-import { mapSelector } from "./map";
-import { layersSelector, rawGroupsSelector } from "./layers";
+
import { mapIdSelector } from "./mapInitialConfig";
-import { customAttributesSettingsSelector } from "./featuregrid";
+import { mapSaveSelector } from './mapsave';
export const userSessionIdSelector = (state) => state.usersession && state.usersession.id || null;
export const userSessionSelector = (state) => state.usersession && state.usersession.session || null;
export const userSessionToSaveSelector = createSelector(
[
- mapSelector,
- layersSelector,
- rawGroupsSelector,
- customAttributesSettingsSelector
+
+ userPluginsSelector,
+ mapSaveSelector
],
- (map, layers, groups, featureGridAttributes) => {
- const {center, zoom} = map;
- return {
- map: {
- center,
- zoom,
- layers,
- groups
- },
- featureGrid: {
- attributes: featureGridAttributes
+
+ (userPlugins, mapSave) => {
+ // Using mapSaveSelector to add all config like saving map
+ const newConfig = {
+ ...mapSave,
+ context: {
+ userPlugins
}
};
+ return newConfig;
});
const getMapName = (contextId, mapId) => {
@@ -65,3 +60,5 @@ export const userSessionNameSelector = createSelector([
userSelector
], (context, mapId, user) => buildSessionName(context?.id, mapId, user?.name));
+export const checkedSessionToClear = (state) => state?.usersession?.checkedSessionToClear;
+
diff --git a/web/client/translations/data.da-DK.json b/web/client/translations/data.da-DK.json
index bca47de03e..44721c76dd 100644
--- a/web/client/translations/data.da-DK.json
+++ b/web/client/translations/data.da-DK.json
@@ -3725,8 +3725,33 @@
},
"tooltip": "Clear the current user session saved",
"successRemoved": "User session removed",
+ "warningTitle": "Clear user session",
"remove": "Reset User Session",
- "confirmRemove": "Are you sure you want to restore the default configuration?"
+ "confirmRemove": "Are you sure you want to restore the default configuration?",
+ "successUpdated": "User session Updated",
+ "sessionLabels":{
+ "everything": "Everything",
+ "map": "Map",
+ "map_pos": "Zoom and center",
+ "visualization_mode": "Visualization Mode (3D/2D)",
+ "layers": "Layers",
+ "annotations_layer": "Annotations Layer",
+ "measurements_layer": "Measurements Layer",
+ "background_layers": "Background Layers",
+ "other_layers": "Other Layers",
+ "catalog_services": "Catalog Services",
+ "widgets": "Widgets",
+ "search": "Search",
+ "text_search_services": "Text Search Services",
+ "bookmarks": "Bookmarks",
+ "feature_grid": "Feature Grid",
+ "other": "Other",
+ "toc": "Table of Contents Configuration",
+ "playback": "Playback Configuration",
+ "mapTemplates": "Map Templates",
+ "userPlugins": "User Plugins",
+ "mapViews": "Map Views"
+ }
},
"streetView": {
"title": "Street View",
diff --git a/web/client/translations/data.de-DE.json b/web/client/translations/data.de-DE.json
index 770dd84292..f5d0a4d9b4 100644
--- a/web/client/translations/data.de-DE.json
+++ b/web/client/translations/data.de-DE.json
@@ -4027,8 +4027,33 @@
},
"tooltip": "Aktuelle gespeicherte Benutzersitzung löschen user",
"successRemoved": "Benutzersitzung entfernt",
+ "warningTitle": "Clear user session",
"remove": "Benutzersitzung zurücksetzen",
- "confirmRemove": "Möchten Sie die Standardkonfiguration wirklich wiederherstellen?"
+ "confirmRemove": "Möchten Sie die Standardkonfiguration wirklich wiederherstellen?",
+ "successUpdated": "User session Updated",
+ "sessionLabels":{
+ "everything": "Everything",
+ "map": "Map",
+ "map_pos": "Zoom and center",
+ "visualization_mode": "Visualization Mode (3D/2D)",
+ "layers": "Layers",
+ "annotations_layer": "Annotations Layer",
+ "measurements_layer": "Measurements Layer",
+ "background_layers": "Background Layers",
+ "other_layers": "Other Layers",
+ "catalog_services": "Catalog Services",
+ "widgets": "Widgets",
+ "search": "Search",
+ "text_search_services": "Text Search Services",
+ "bookmarks": "Bookmarks",
+ "feature_grid": "Feature Grid",
+ "other": "Other",
+ "toc": "Table of Contents Configuration",
+ "playback": "Playback Configuration",
+ "mapTemplates": "Map Templates",
+ "userPlugins": "User Plugins",
+ "mapViews": "Map Views"
+ }
},
"streetView": {
"title": "Street-View",
diff --git a/web/client/translations/data.en-US.json b/web/client/translations/data.en-US.json
index ecfcea85b3..b6aa8fad1b 100644
--- a/web/client/translations/data.en-US.json
+++ b/web/client/translations/data.en-US.json
@@ -4000,9 +4000,34 @@
},
"tooltip": "Clear the current user session saved",
"successRemoved": "User session removed",
+ "successUpdated": "User session Updated",
+ "warningTitle": "Clear user session",
"remove": "Reset User Session",
- "confirmRemove": "Are you sure you want to restore the default configuration?"
- },
+ "confirmRemove": "Are you sure to remove the current user session? This will restore the original map/context configuration, removing your current changes. Please select what you want to remove and unselect what you want to preserve.",
+ "sessionLabels":{
+ "everything": "Everything",
+ "map": "Map",
+ "map_pos": "Zoom and center",
+ "visualization_mode": "Visualization Mode (3D/2D)",
+ "layers": "Layers",
+ "annotations_layer": "Annotations Layer",
+ "measurements_layer": "Measurements Layer",
+ "background_layers": "Background Layers",
+ "other_layers": "Other Layers",
+ "catalog_services": "Catalog Services",
+ "widgets": "Widgets",
+ "search": "Search",
+ "text_search_services": "Text Search Services",
+ "bookmarks": "Bookmarks",
+ "feature_grid": "Feature Grid",
+ "other": "Other",
+ "toc": "Table of Contents Configuration",
+ "playback": "Playback Configuration",
+ "mapTemplates": "Map Templates",
+ "userPlugins": "User Plugins",
+ "mapViews": "Map Views"
+ }
+ },
"streetView": {
"title": "Street View",
"description": "Street view tool for browsing Google street view images from the map",
diff --git a/web/client/translations/data.es-ES.json b/web/client/translations/data.es-ES.json
index ca6ee04960..cc5feb3a8c 100644
--- a/web/client/translations/data.es-ES.json
+++ b/web/client/translations/data.es-ES.json
@@ -3989,8 +3989,33 @@
},
"tooltip": "Borrar la sesión de usuario actual guardada",
"successRemoved": "Sesión de usuario eliminada",
+ "warningTitle": "Clear user session",
"remove": "Restablecer sesión de usuario",
- "confirmRemove": "¿Está seguro de que desea restaurar la configuración predeterminada?"
+ "confirmRemove": "¿Está seguro de que desea restaurar la configuración predeterminada?",
+ "successUpdated": "User session Updated",
+ "sessionLabels":{
+ "everything": "Everything",
+ "map": "Map",
+ "map_pos": "Zoom and center",
+ "visualization_mode": "Visualization Mode (3D/2D)",
+ "layers": "Layers",
+ "annotations_layer": "Annotations Layer",
+ "measurements_layer": "Measurements Layer",
+ "background_layers": "Background Layers",
+ "other_layers": "Other Layers",
+ "catalog_services": "Catalog Services",
+ "widgets": "Widgets",
+ "search": "Search",
+ "text_search_services": "Text Search Services",
+ "bookmarks": "Bookmarks",
+ "feature_grid": "Feature Grid",
+ "other": "Other",
+ "toc": "Table of Contents Configuration",
+ "playback": "Playback Configuration",
+ "mapTemplates": "Map Templates",
+ "userPlugins": "User Plugins",
+ "mapViews": "Map Views"
+ }
},
"streetView": {
"title": "Street View",
diff --git a/web/client/translations/data.fr-FR.json b/web/client/translations/data.fr-FR.json
index c6c237b591..a859a662ad 100644
--- a/web/client/translations/data.fr-FR.json
+++ b/web/client/translations/data.fr-FR.json
@@ -3990,7 +3990,32 @@
"tooltip": "Effacer la session utilisateur actuelle enregistrée",
"successRemoved": "Session utilisateur supprimée",
"remove": "Réinitialiser la session utilisateur",
- "confirmRemove": "Êtes-vous sûr de vouloir restaurer la configuration par défaut?"
+ "warningTitle": "Clear user session",
+ "confirmRemove": "Êtes-vous sûr de vouloir restaurer la configuration par défaut?",
+ "successUpdated": "User session Updated",
+ "sessionLabels":{
+ "everything": "Everything",
+ "map": "Map",
+ "map_pos": "Zoom and center",
+ "visualization_mode": "Visualization Mode (3D/2D)",
+ "layers": "Layers",
+ "annotations_layer": "Annotations Layer",
+ "measurements_layer": "Measurements Layer",
+ "background_layers": "Background Layers",
+ "other_layers": "Other Layers",
+ "catalog_services": "Catalog Services",
+ "widgets": "Widgets",
+ "search": "Search",
+ "text_search_services": "Text Search Services",
+ "bookmarks": "Bookmarks",
+ "feature_grid": "Feature Grid",
+ "other": "Other",
+ "toc": "Table of Contents Configuration",
+ "playback": "Playback Configuration",
+ "mapTemplates": "Map Templates",
+ "userPlugins": "User Plugins",
+ "mapViews": "Map Views"
+ }
},
"streetView": {
"title": "Street View",
diff --git a/web/client/translations/data.is-IS.json b/web/client/translations/data.is-IS.json
index 0a63e5d63a..1d9f01f663 100644
--- a/web/client/translations/data.is-IS.json
+++ b/web/client/translations/data.is-IS.json
@@ -3749,8 +3749,33 @@
},
"tooltip": "Clear the current user session saved",
"successRemoved": "User session removed",
+ "warningTitle": "Clear user session",
"remove": "Reset User Session",
- "confirmRemove": "Are you sure you want to restore the default configuration?"
+ "confirmRemove": "Are you sure you want to restore the default configuration?",
+ "successUpdated": "User session Updated",
+ "sessionLabels":{
+ "everything": "Everything",
+ "map": "Map",
+ "map_pos": "Zoom and center",
+ "visualization_mode": "Visualization Mode (3D/2D)",
+ "layers": "Layers",
+ "annotations_layer": "Annotations Layer",
+ "measurements_layer": "Measurements Layer",
+ "background_layers": "Background Layers",
+ "other_layers": "Other Layers",
+ "catalog_services": "Catalog Services",
+ "widgets": "Widgets",
+ "search": "Search",
+ "text_search_services": "Text Search Services",
+ "bookmarks": "Bookmarks",
+ "feature_grid": "Feature Grid",
+ "other": "Other",
+ "toc": "Table of Contents Configuration",
+ "playback": "Playback Configuration",
+ "mapTemplates": "Map Templates",
+ "userPlugins": "User Plugins",
+ "mapViews": "Map Views"
+ }
},
"streetView": {
"title": "Street View",
diff --git a/web/client/translations/data.it-IT.json b/web/client/translations/data.it-IT.json
index 3f8147e03c..1c00d5e0ab 100644
--- a/web/client/translations/data.it-IT.json
+++ b/web/client/translations/data.it-IT.json
@@ -3989,8 +3989,33 @@
},
"tooltip": "Cancella la sessione utente corrente salvata",
"successRemoved": "Sessione utente cancellata",
+ "warningTitle": "Clear user session",
"remove": "Reset Sessione Utente",
- "confirmRemove": "Sei sicuro di voler ripristinare la configurazione di default?"
+ "confirmRemove": "Sei sicuro di voler ripristinare la configurazione di default?",
+ "successUpdated": "User session Updated",
+ "sessionLabels":{
+ "everything": "Everything",
+ "map": "Map",
+ "map_pos": "Zoom and center",
+ "visualization_mode": "Visualization Mode (3D/2D)",
+ "layers": "Layers",
+ "annotations_layer": "Annotations Layer",
+ "measurements_layer": "Measurements Layer",
+ "background_layers": "Background Layers",
+ "other_layers": "Other Layers",
+ "catalog_services": "Catalog Services",
+ "widgets": "Widgets",
+ "search": "Search",
+ "text_search_services": "Text Search Services",
+ "bookmarks": "Bookmarks",
+ "feature_grid": "Feature Grid",
+ "other": "Other",
+ "toc": "Table of Contents Configuration",
+ "playback": "Playback Configuration",
+ "mapTemplates": "Map Templates",
+ "userPlugins": "User Plugins",
+ "mapViews": "Map Views"
+ }
},
"streetView": {
"title": "Street View",
diff --git a/web/client/translations/data.nl-NL.json b/web/client/translations/data.nl-NL.json
index f88e2c7bf2..83083dd69a 100644
--- a/web/client/translations/data.nl-NL.json
+++ b/web/client/translations/data.nl-NL.json
@@ -3993,8 +3993,33 @@
},
"tooltip": "Reset de huidige gebruikerssessie",
"successRemoved": "Gebruikerssessie verwijderd",
+ "warningTitle": "Clear user session",
"remove": "Gebruikerssessie resetten",
- "confirmRemove": "Weet u zeker dat u de standaardconfiguratie wilt herstellen?"
+ "confirmRemove": "Weet u zeker dat u de standaardconfiguratie wilt herstellen?",
+ "successUpdated": "User session Updated",
+ "sessionLabels":{
+ "everything": "Everything",
+ "map": "Map",
+ "map_pos": "Zoom and center",
+ "visualization_mode": "Visualization Mode (3D/2D)",
+ "layers": "Layers",
+ "annotations_layer": "Annotations Layer",
+ "measurements_layer": "Measurements Layer",
+ "background_layers": "Background Layers",
+ "other_layers": "Other Layers",
+ "catalog_services": "Catalog Services",
+ "widgets": "Widgets",
+ "search": "Search",
+ "text_search_services": "Text Search Services",
+ "bookmarks": "Bookmarks",
+ "feature_grid": "Feature Grid",
+ "other": "Other",
+ "toc": "Table of Contents Configuration",
+ "playback": "Playback Configuration",
+ "mapTemplates": "Map Templates",
+ "userPlugins": "User Plugins",
+ "mapViews": "Map Views"
+ }
},
"streetView": {
"title": "Street View",
diff --git a/web/client/translations/data.sk-SK.json b/web/client/translations/data.sk-SK.json
index 66949e87a5..02108d559e 100644
--- a/web/client/translations/data.sk-SK.json
+++ b/web/client/translations/data.sk-SK.json
@@ -3356,8 +3356,33 @@
"defaultMessage": "Neznáma chyba"
},
"successRemoved": "Relácia používateľa bola odstránená",
+ "warningTitle": "Clear user session",
"remove": "Obnoviť reláciu používateľa",
- "confirmRemove": "Naozaj chceš obnoviť predvolenú konfiguráciu?"
+ "confirmRemove": "Naozaj chceš obnoviť predvolenú konfiguráciu?",
+ "successUpdated": "User session Updated",
+ "sessionLabels":{
+ "everything": "Everything",
+ "map": "Map",
+ "map_pos": "Zoom and center",
+ "visualization_mode": "Visualization Mode (3D/2D)",
+ "layers": "Layers",
+ "annotations_layer": "Annotations Layer",
+ "measurements_layer": "Measurements Layer",
+ "background_layers": "Background Layers",
+ "other_layers": "Other Layers",
+ "catalog_services": "Catalog Services",
+ "widgets": "Widgets",
+ "search": "Search",
+ "text_search_services": "Text Search Services",
+ "bookmarks": "Bookmarks",
+ "feature_grid": "Feature Grid",
+ "other": "Other",
+ "toc": "Table of Contents Configuration",
+ "playback": "Playback Configuration",
+ "mapTemplates": "Map Templates",
+ "userPlugins": "User Plugins",
+ "mapViews": "Map Views"
+ }
}
}
}
diff --git a/web/client/translations/data.sv-SE.json b/web/client/translations/data.sv-SE.json
index 41e8505b9f..8db10e49b0 100644
--- a/web/client/translations/data.sv-SE.json
+++ b/web/client/translations/data.sv-SE.json
@@ -3395,8 +3395,33 @@
},
"tooltip": "Rensa den aktuella användarsessionen sparad",
"successRemoved": "Användarsession borttagen",
+ "warningTitle": "Clear user session",
"remove": "Återställ användarsession",
- "confirmRemove": "Är du säker på att du vill återställa standardkonfigurationen?"
+ "confirmRemove": "Är du säker på att du vill återställa standardkonfigurationen?",
+ "successUpdated": "User session Updated",
+ "sessionLabels":{
+ "everything": "Everything",
+ "map": "Map",
+ "map_pos": "Zoom and center",
+ "visualization_mode": "Visualization Mode (3D/2D)",
+ "layers": "Layers",
+ "annotations_layer": "Annotations Layer",
+ "measurements_layer": "Measurements Layer",
+ "background_layers": "Background Layers",
+ "other_layers": "Other Layers",
+ "catalog_services": "Catalog Services",
+ "widgets": "Widgets",
+ "search": "Search",
+ "text_search_services": "Text Search Services",
+ "bookmarks": "Bookmarks",
+ "feature_grid": "Feature Grid",
+ "other": "Other",
+ "toc": "Table of Contents Configuration",
+ "playback": "Playback Configuration",
+ "mapTemplates": "Map Templates",
+ "userPlugins": "User Plugins",
+ "mapViews": "Map Views"
+ }
}
}
}
diff --git a/web/client/utils/ConfigUtils.js b/web/client/utils/ConfigUtils.js
index fad8a2dd03..addc657830 100644
--- a/web/client/utils/ConfigUtils.js
+++ b/web/client/utils/ConfigUtils.js
@@ -9,7 +9,7 @@ import Proj4js from 'proj4';
import PropTypes from 'prop-types';
import url from 'url';
import axios from 'axios';
-import { castArray, isArray, isObject, endsWith, isNil, get } from 'lodash';
+import { castArray, isArray, isObject, endsWith, isNil, get, mergeWith } from 'lodash';
import assign from 'object-assign';
import { Promise } from 'es6-promise';
import isMobile from 'ismobilejs';
@@ -487,6 +487,151 @@ export const getMiscSetting = (name, defaultVal) => {
return get(getConfigProp('miscSettings') ?? {}, name, defaultVal);
};
+// IDs for userSession
+export const SESSION_IDS = {
+ EVERYTHING: "everything",
+ MAP: "map",
+ MAP_POS: "map_pos",
+ VISUALIZATION_MODE: "visualization_mode",
+ LAYERS: "layers",
+ ANNOTATIONS_LAYER: "annotations_layer",
+ MEASUREMENTS_LAYER: "measurements_layer",
+ BACKGROUND_LAYERS: "background_layers",
+ OTHER_LAYERS: "other_layers",
+ CATALOG_SERVICES: "catalog_services",
+ WIDGETS: "widgets",
+ SEARCH: "search",
+ TEXT_SEARCH_SERVICES: "text_search_services",
+ BOOKMARKS: "bookmarks",
+ FEATURE_GRID: "feature_grid",
+ OTHER: "other",
+ USER_PLUGINS: "userPlugins"
+};
+
+/*
+ * Function to update overrideConfig to clean specific settings
+ * After the introduction of individual reset of the session, override Config is updated here. Previously it was clearing all session.
+ * This function handles to update the overrideConfig(from session) to clean specific settings
+ *
+ * Layers are handled differently as they are partially updating properties(annotations, measurements, backgrounds, others) and merging function from loadash has problem in merging arrays of objects(it merges all properties of object from both arrays) which is used in `applyOverrides` function in this same file below
+ * So for layers merge of original Config and overrideConfig happens here. And applyOverrides use the value of overrideConfig
+ * No Problem for arrays that gets entirely reset
+ *
+ * @param {object} override - current overrideConfig
+ * @param {Array} thingsToClear - IDs of settings to be cleared
+ * @param {object} originalConfig - original config
+ * @param {object} customHandlers - session Saved By registerCustomSaveHandler
+ * @returns {object} updated overrideConfig
+*/
+export const updateOverrideConfig = (override = {}, thingsToClear = [], originalConfig = {}, customHandlers = []) => {
+ let overrideConfig = JSON.parse(JSON.stringify(override));
+
+ if (thingsToClear?.includes(SESSION_IDS.EVERYTHING)) {
+ overrideConfig = {};
+ return overrideConfig;
+ }
+
+ // zoom and center
+ if (thingsToClear.includes(SESSION_IDS.MAP_POS)) {
+ delete overrideConfig.map.zoom;
+ delete overrideConfig.map.center;
+ }
+ // visualization mode
+ if (thingsToClear.includes(SESSION_IDS.VISUALIZATION_MODE)) {
+ delete overrideConfig.map.visualizationMode;
+ }
+
+ // layers is the only case that partially gets reset, and there is problem to merge arrays with different index(it merges properties from two array on same index)
+ // so merging original layers here. And while merging override config with original config: Override config is given priority. Check applyOverrides function below in this file.
+
+ // annotation layers
+ if (thingsToClear?.includes(SESSION_IDS.ANNOTATIONS_LAYER)) {
+ overrideConfig.map.layers = [...overrideConfig.map?.layers?.filter((l)=>!l.id?.includes('annotations')), ...originalConfig?.map?.layers.filter((l)=>l.id?.includes('annotations'))];
+ }
+ // measurements layers
+ if (thingsToClear?.includes(SESSION_IDS.MEASUREMENTS_LAYER)) {
+ overrideConfig.map.layers = [...overrideConfig.map?.layers?.filter((l)=>!l?.name?.includes('measurements')), ...originalConfig?.map?.layers.filter(l=> l?.name?.includes('measurements'))];
+ }
+ // background layers
+ if (thingsToClear?.includes(SESSION_IDS.BACKGROUND_LAYERS)) {
+ overrideConfig.map.layers = [...overrideConfig.map?.layers?.filter((l)=>!l?.group?.includes('background')), ...originalConfig?.map?.layers.filter(l=> l?.group?.includes('background'))];
+ }
+ // other layers
+ if (thingsToClear?.includes(SESSION_IDS.OTHER_LAYERS)) {
+ overrideConfig.map.layers = [...overrideConfig.map?.layers?.filter(l=> l?.id?.includes('annotations') || l?.name?.includes('measurements') || l.group?.includes("background")), ...originalConfig?.map?.layers?.filter(l=> !l?.id?.includes('annotations') && !l?.name?.includes('measurements') && !l.group?.includes("background"))];
+ }
+
+ // reorder layers based on existing order of layers: since layers are modified in different orders, so sorting is important
+ overrideConfig.map.layers = overrideConfig.map.layers.sort((a, b) =>
+ override?.map?.layers.findIndex(item => item.id === a.id) - override?.map?.layers?.findIndex(item => item.id === b.id)
+ );
+ // catalog services
+ if (thingsToClear?.includes(SESSION_IDS.CATALOG_SERVICES)) {
+ delete overrideConfig.catalogServices;
+ }
+ // widgets
+ if (thingsToClear?.includes(SESSION_IDS.WIDGETS)) {
+ delete overrideConfig.widgetsConfig;
+ }
+
+ // search services
+ if (thingsToClear?.includes(SESSION_IDS.TEXT_SEARCH_SERVICES)) {
+ delete overrideConfig.map.text_search_config;
+ }
+
+ // bookmarks
+ if (thingsToClear?.includes(SESSION_IDS.BOOKMARKS)) {
+ delete overrideConfig.map.bookmark_search_config;
+ }
+
+ // feature grid
+ if (thingsToClear?.includes(SESSION_IDS.FEATURE_GRID)) {
+ // each properties are updated dynamically in featureGrid reducer(MAP_CONFIG_LOADED), so each attribute of featureGrid should be reset
+ Object.keys(overrideConfig?.featureGrid ?? {}).forEach((fg)=>{
+ overrideConfig.featureGrid[fg] = {}; // all properties's value are in object
+ });
+ }
+
+ if (thingsToClear?.includes(SESSION_IDS.USER_PLUGINS)) {
+ delete overrideConfig.context.userPlugins;
+ }
+
+ // handle config from registerCustomSaveConfig
+ customHandlers?.forEach((k) => {
+ if (thingsToClear?.includes(k)) {
+ delete overrideConfig[k];
+ }
+ });
+
+
+ return overrideConfig;
+
+};
+/**
+* Merge two configurations
+* While overriding, overrideConfig properties and original config has arrays then overrideConfig gets more priority
+* merge from loadash has problem while merging arrays of objects(it merges properties of objects from both), So merge logic has been changed
+ * @param {object} config the configuration to override
+ * @param {object} override the data to use for override
+ * @returns {object}
+ */
+export const applyOverrides = (config, override) => {
+ const merged = mergeWith({}, config, override, (objValue, srcValue) => {
+ // Till now layers is the only case that get partially reset and has case where two array with objects tries to merge, so merging with original config happens in updateOverrideConfig(above in the this file) while reset
+ if (Array.isArray(objValue) && Array.isArray(srcValue)) {
+ // Give priority if there are some elements in override
+ if (srcValue.length > 0) {
+ return [...srcValue];
+ }
+ return [...objValue];
+
+ }
+ // default merge rules for other cases
+ // eslint-disable-next-line consistent-return
+ return undefined;
+ });
+ return merged;
+};
const ConfigUtils = {
PropTypes: {
center: centerPropType,
diff --git a/web/client/utils/LayersUtils.js b/web/client/utils/LayersUtils.js
index f80ad6b938..e5bd5ba4c6 100644
--- a/web/client/utils/LayersUtils.js
+++ b/web/client/utils/LayersUtils.js
@@ -535,7 +535,7 @@ export const getDerivedLayersVisibility = (layers = [], groups = []) => {
export const denormalizeGroups = (allLayers, groups) => {
const flattenGroups = flattenArrayOfObjects(groups).filter(isObject);
let getNormalizedGroup = (group, layers) => {
- const nodes = group.nodes.map((node) => {
+ const nodes = group?.nodes?.map((node) => {
if (isObject(node)) {
return getNormalizedGroup(node, layers);
}
diff --git a/web/client/utils/__tests__/ApplyOverride-test.js b/web/client/utils/__tests__/ApplyOverride-test.js
new file mode 100644
index 0000000000..556ac43a51
--- /dev/null
+++ b/web/client/utils/__tests__/ApplyOverride-test.js
@@ -0,0 +1,74 @@
+import expect from 'expect';
+import { applyOverrides } from '../ConfigUtils';
+
+/*
+Tests to check override logic for original Config and override config
+*/
+describe('applyOverrides', () => {
+
+ it('should merge simple objects', () => {
+ const config = { key1: 'value1', key2: 'value2' };
+ const override = { key2: 'overriddenValue', key3: 'newKey' };
+ const expected = { key1: 'value1', key2: 'overriddenValue', key3: 'newKey' };
+
+ const result = applyOverrides(config, override);
+ expect(result).toEqual(expected);
+ });
+
+ it('should prioritize values from the override when arrays are empty', () => {
+ const config = { layers: [{ id: 1, name: 'layer1' }] };
+ const override = { layers: [] };
+ const expected = { layers: [{ id: 1, name: 'layer1' }] };
+
+ const result = applyOverrides(config, override);
+ expect(result).toEqual(expected);
+ });
+
+ it('should keep original array if override array is empty', () => {
+ const config = { layers: [{ id: 1, name: 'layer1' }] };
+ const override = { layers: [] };
+ const expected = { layers: [{ id: 1, name: 'layer1' }] };
+
+ const result = applyOverrides(config, override);
+ expect(result).toEqual(expected);
+ });
+
+ it('should return an empty object when both config and override are empty', () => {
+ const config = {};
+ const override = {};
+ const expected = {};
+
+ const result = applyOverrides(config, override);
+ expect(result).toEqual(expected);
+ });
+
+ it('should merge arrays with no overrides gracefully', () => {
+ const config = { layers: [{ id: 1, name: 'layer1' }] };
+ const override = {};
+ const expected = { layers: [{ id: 1, name: 'layer1' }] };
+
+ const result = applyOverrides(config, override);
+ expect(result).toEqual(expected);
+ });
+
+
+ it('should handle deep merging where the array is part of the config object', () => {
+ const config = { settings: { layers: [{ id: 1, name: 'layer1' }] } };
+ const override = { settings: { layers: [{ id: 2, name: 'layer2' }] } };
+ const expected = { settings: { layers: [{ id: 2, name: 'layer2' }] } };
+
+ const result = applyOverrides(config, override);
+ expect(result).toEqual(expected);
+ });
+
+
+ it('should return merged result even with deeply nested objects', () => {
+ const config = { settings: { layers: [{ id: 1, name: 'layer1' }] } };
+ const override = { settings: { layers: [{ id: 2, name: 'layer2' }] } };
+ const expected = { settings: { layers: [{ id: 2, name: 'layer2' }] } };
+
+ const result = applyOverrides(config, override);
+ expect(result).toEqual(expected);
+ });
+
+});
diff --git a/web/client/utils/__tests__/UpdateOverrideConfig-test.js b/web/client/utils/__tests__/UpdateOverrideConfig-test.js
new file mode 100644
index 0000000000..6b05962470
--- /dev/null
+++ b/web/client/utils/__tests__/UpdateOverrideConfig-test.js
@@ -0,0 +1,158 @@
+
+import expect from 'expect';
+import { SESSION_IDS, updateOverrideConfig } from "../ConfigUtils";
+
+/*
+Test updateOverrideConfig, a function that updates the override config based on the selected settings to restore,
+*/
+describe('updateOverrideConfig', () => {
+ let originalConfig;
+ let override;
+
+ beforeEach(() => {
+ originalConfig = {
+ catalogServices: {
+ services: {
+ gs_stable_csw: { url: 'https://gs-stable.geo-solutions.it/geoserver/csw', type: 'csw' },
+ gs_stable_wms: { url: 'https://gs-stable.geo-solutions.it/geoserver/wms', type: 'wms' }
+ }
+ },
+ widgetsConfig: { widgets: [], layouts: { md: [], xxs: [] } },
+ featureGrid: { attributes: {} },
+ context: { userPlugins: [{ name: 'Tutorial', active: false }] },
+ map: {
+ layers: [
+ { id: 'osm:osm_simple_light__0', group: 'background' },
+ { id: 'osm:osm__2', group: 'background', name: 'osm:osm' }
+ ],
+ zoom: 6,
+ center: { x: 14.186730225464526, y: 41.59351689233117, crs: 'EPSG:4326' },
+ text_search_config: null,
+ bookmark_search_config: {
+ bookmarks: [
+ {
+ options: { west: -28.43026376769848, south: 25.350469614189482, east: 54.45059560730153, north: 54.99431210280781 },
+ title: 'ewu',
+ layerVisibilityReload: false
+ }
+ ]
+ }
+ }
+ };
+
+ override = {
+ version: 2,
+ map: {
+ zoom: 6,
+ center: { x: 14.186730225464526, y: 41.59351689233117, crs: 'EPSG:4326' },
+ layers: [
+ { id: 'background1', group: 'background' },
+ {name: "measurements"},
+ { id: 'otherLayer' },
+ {id: 'annotations'}
+ ],
+ text_search_config: { query: 'some search query' },
+ bookmark_search_config: {
+ bookmarks: [
+ {
+ options: { west: -28.43026376769848, south: 25.350469614189482, east: 54.45059560730153, north: 54.99431210280781 },
+ title: 'ewu',
+ layerVisibilityReload: false
+ }
+ ]
+ }
+ },
+ catalogServices: { services: { gs_stable_csw: { url: 'https://gs-stable.geo-solutions.it/geoserver/csw', type: 'csw' } } },
+ widgetsConfig: { widgets: [{ type: 'widget1', value: 'value1' }], layouts: { md: [], xxs: [] } },
+ featureGrid: { attributes: { col1: {}, col2: {} } },
+ context: { userPlugins: [{ name: 'Tutorial', active: false }] }
+ };
+ });
+
+ it('should clear everything when everything is in thingsToClear', () => {
+ const result = updateOverrideConfig(override, [SESSION_IDS.EVERYTHING], originalConfig);
+ expect(result).toEqual({});
+ });
+
+ it('should clear map zoom and center when "Zoom And Center" is in thingsToClear', () => {
+ const result = updateOverrideConfig(override, [SESSION_IDS.MAP_POS], originalConfig);
+ expect(result.map.zoom).toBe(undefined);
+ expect(result.map.center).toBe(undefined);
+ });
+
+ it('should clear visualization mode when "Visualization Mode" is in thingsToClear', () => {
+ const result = updateOverrideConfig(override, [SESSION_IDS.VISUALIZATION_MODE], originalConfig);
+ expect(result.map.visualizationMode).toBe(undefined);
+ });
+
+ it('should clear annotation layers when "Annotations Layers" is in thingsToClear', () => {
+ const result = updateOverrideConfig(override, [SESSION_IDS.ANNOTATIONS_LAYER], originalConfig);
+ expect(result.map.layers).toEqual([
+ { id: 'background1', group: 'background' },
+ {name: "measurements"},
+ { id: 'otherLayer' }
+ ]);
+ });
+
+ it('should clear measurement layers when "Measurements Layers" is in thingsToClear', () => {
+ const result = updateOverrideConfig(override, [SESSION_IDS.MEASUREMENTS_LAYER], originalConfig);
+ expect(result.map.layers).toEqual([
+ { id: 'background1', group: 'background' },
+ { id: 'otherLayer' },
+ {id: 'annotations'}
+ ]);
+ });
+ it('should clear background layers and add background layers from OriginalConfig when "Background Layers" is in thingsToClear', () => {
+ const result = updateOverrideConfig(override, [SESSION_IDS.BACKGROUND_LAYERS], originalConfig);
+ expect(result.map.layers).toEqual([
+ { id: 'osm:osm_simple_light__0', group: 'background' },
+ { id: 'osm:osm__2', group: 'background', name: 'osm:osm' },
+ {name: "measurements"},
+ { id: 'otherLayer' },
+ {id: 'annotations'}
+ ]);
+ });
+
+ it('should clear catalogServices when "Catalog Services" is in thingsToClear', () => {
+ const result = updateOverrideConfig(override, [SESSION_IDS.CATALOG_SERVICES], originalConfig);
+ expect(result.catalogServices).toBe(undefined);
+ });
+
+ it('should clear widgetsConfig when "Widgets" is in thingsToClear', () => {
+ const result = updateOverrideConfig(override, [SESSION_IDS.WIDGETS], originalConfig);
+ expect(result.widgetsConfig).toBe(undefined);
+ });
+
+ it('should clear text search config when SESSION_IDS.TEXT_SEARCH_SERVICES is in thingsToClear', () => {
+ const result = updateOverrideConfig(override, [SESSION_IDS.TEXT_SEARCH_SERVICES], originalConfig);
+ expect(result.map.text_search_config).toBe(undefined);
+ });
+
+ it('should clear bookmark search config when "Bookmarks" is in thingsToClear', () => {
+ const result = updateOverrideConfig(override, [SESSION_IDS.BOOKMARKS], originalConfig);
+ expect(result.map.bookmark_search_config).toBe(undefined);
+ });
+
+ it('should clear feature grid when "FeatureGrid" is in thingsToClear', () => {
+ const result = updateOverrideConfig(override, [SESSION_IDS.FEATURE_GRID], originalConfig);
+ expect(result.featureGrid.attributes).toEqual({});
+ });
+
+ it('should clear userPlugins when "User Plugin" is in thingsToClear', () => {
+ const result = updateOverrideConfig(override, [SESSION_IDS.USER_PLUGINS], originalConfig);
+ expect(result.context.userPlugins).toBe(undefined);
+ });
+
+ it('should apply custom handlers correctly', () => {
+ const customHandlers = [
+ "toc"
+ ];
+ const customOverride = {...override, toc: {
+ "theme": "legend"
+ } };
+
+ const result = updateOverrideConfig(customOverride, ['toc'], originalConfig, customHandlers);
+ expect(result.customConfig).toBe(undefined);
+ });
+});
+