From 435be1bfc2bc54cd0c25d2dabb6614b490f11f5c Mon Sep 17 00:00:00 2001 From: Rohit Gautam Date: Tue, 3 Dec 2024 20:58:23 +0545 Subject: [PATCH 01/10] save settings to session like mapSave, able to remove specific session --- .../actions/__tests__/usersession-test.js | 13 +- web/client/actions/usersession.js | 4 +- .../epics/__tests__/usersession-test.js | 60 ++++- web/client/epics/config.js | 14 +- web/client/epics/context.js | 5 +- web/client/epics/maptemplates.js | 7 +- web/client/epics/usersession.js | 60 ++++- web/client/plugins/UserSession.jsx | 153 +++++++++++- web/client/plugins/session/Tree.jsx | 203 +++++++++++++++ .../plugins/session/__tests__/tree-test.jsx | 235 ++++++++++++++++++ .../reducers/__tests__/usersession-test.js | 5 +- web/client/reducers/context.js | 9 +- web/client/reducers/maptemplates.js | 4 + web/client/reducers/playback.js | 5 + web/client/reducers/usersession.js | 35 ++- .../selectors/__tests__/usersession-test.js | 19 +- web/client/selectors/mapsave.js | 3 + web/client/selectors/maptemplates.js | 3 + web/client/selectors/playback.js | 5 + web/client/selectors/usersession.js | 35 ++- web/client/translations/data.en-US.json | 28 ++- web/client/utils/ConfigUtils.js | 120 ++++++++- web/client/utils/LayersUtils.js | 2 +- web/client/utils/MapUtils.js | 18 +- 24 files changed, 968 insertions(+), 77 deletions(-) create mode 100644 web/client/plugins/session/Tree.jsx create mode 100644 web/client/plugins/session/__tests__/tree-test.jsx diff --git a/web/client/actions/__tests__/usersession-test.js b/web/client/actions/__tests__/usersession-test.js index ce5ed38039..da78699e1f 100644 --- a/web/client/actions/__tests__/usersession-test.js +++ b/web/client/actions/__tests__/usersession-test.js @@ -11,7 +11,9 @@ import {SAVE_USER_SESSION, USER_SESSION_SAVED, LOAD_USER_SESSION, USER_SESSION_L REMOVE_USER_SESSION, USER_SESSION_REMOVED, SAVE_MAP_CONFIG, USER_SESSION_START_SAVING, USER_SESSION_STOP_SAVING, SET_USER_SESSION, saveUserSession, userSessionSaved, loadUserSession, userSessionLoaded, loading, setUserSession, - removeUserSession, userSessionRemoved, saveMapConfig, userSessionStartSaving, userSessionStopSaving} from "../usersession"; + removeUserSession, userSessionRemoved, saveMapConfig, userSessionStartSaving, userSessionStopSaving, + setCheckedSessionToClear, + SET_CHECKED_SESSION_TO_CLEAR} from "../usersession"; describe('Test correctness of the usersession actions', () => { @@ -51,8 +53,9 @@ describe('Test correctness of the usersession actions', () => { expect(action.type).toBe(REMOVE_USER_SESSION); }); it('user session removed', () => { - const action = userSessionRemoved(); + const action = userSessionRemoved({map: { zoom: 20}}); expect(action.type).toBe(USER_SESSION_REMOVED); + expect(action.newSession).toExist(); }); it('user session start saving', () => { const action = userSessionStartSaving(); @@ -75,4 +78,10 @@ describe('Test correctness of the usersession actions', () => { expect(action.type).toBe(SAVE_MAP_CONFIG); expect(action.config).toExist(); }); + it("set Checked session to remove", () => { + const action = setCheckedSessionToClear(["map_pos"]); + expect(action.type).toBe(SET_CHECKED_SESSION_TO_CLEAR); + expect(action.checks).toExist(); + expect(action.checks.length).toBe(1); + }); }); diff --git a/web/client/actions/usersession.js b/web/client/actions/usersession.js index e65c34666d..fa985322a6 100644 --- a/web/client/actions/usersession.js +++ b/web/client/actions/usersession.js @@ -18,17 +18,19 @@ export const USER_SESSION_START_SAVING = "USER_SESSION:START_SAVING"; export const USER_SESSION_STOP_SAVING = "USER_SESSION:STOP_SAVING"; export const SET_USER_SESSION = "USER_SESSION:SET"; export const ENABLE_AUTO_SAVE = "USER_SESSION:ENABLE_AUTO_SAVE"; +export const SET_CHECKED_SESSION_TO_CLEAR = "USER_SESSION:SET_CHECKED_SESSION_TO_CLEAR"; export const saveUserSession = () => ({type: SAVE_USER_SESSION}); export const userSessionSaved = (id, session) => ({type: USER_SESSION_SAVED, id, session}); export const loadUserSession = (name = "") => ({type: LOAD_USER_SESSION, name}); export const userSessionLoaded = (id, session) => ({type: USER_SESSION_LOADED, id, session}); export const removeUserSession = () => ({type: REMOVE_USER_SESSION}); -export const userSessionRemoved = () => ({type: USER_SESSION_REMOVED}); +export const userSessionRemoved = (newSession) => ({type: USER_SESSION_REMOVED, newSession}); export const userSessionStartSaving = () => ({type: USER_SESSION_START_SAVING}); export const userSessionStopSaving = () => ({type: USER_SESSION_STOP_SAVING}); export const saveMapConfig = (config) => ({type: SAVE_MAP_CONFIG, config}); export const setUserSession = (session) => ({type: SET_USER_SESSION, session}); +export const setCheckedSessionToClear = (checks) => ({type: SET_CHECKED_SESSION_TO_CLEAR, checks}); /** * Action to enable/disable the auto-save functionality. * @param {boolean} enabled flag to enable/disable the auto-save for session diff --git a/web/client/epics/__tests__/usersession-test.js b/web/client/epics/__tests__/usersession-test.js index 26d2e80b7d..24eb8c91cb 100644 --- a/web/client/epics/__tests__/usersession-test.js +++ b/web/client/epics/__tests__/usersession-test.js @@ -124,11 +124,67 @@ describe('usersession Epics', () => { testEpic(removeUserSessionEpicCreator(idSelector), 6, removeUserSession(), (actions) => { expect(actions[0].type).toBe(USER_SESSION_LOADING); expect(actions[1].type).toBe(USER_SESSION_REMOVED); - expect(actions[1].id).toBeFalsy(); - expect(actions[1].session).toBeFalsy(); + expect(actions[1].newSession).toBeTruthy(); }, initialState, done); }); + it("user Session Update on Partial Session Remove", (done) => { + const states = { + ...initialState, + map: { + present: { + center: { + x: 118.91601562499996, + y: 42.617791432823395, + crs: 'EPSG:4326' + }, + zoom: 16 + } + }, + layers: [{id: "layer1", group: 'background'}, {id: "layer2"}, {id: "layer3]"}], + toc: {test: false}, + usersession: { + checkedSessionToClear: ['background_layers'] + } + }; + + // remove background layers + testEpic(removeUserSessionEpicCreator(idSelector), 6, removeUserSession(), (actions) => { + // only background layers are removed + expect(actions[1].newSession.map.zoom).toBe(16); + expect(actions[1].newSession.map.center).toEqual({ + x: 118.91601562499996, + y: 42.617791432823395, + crs: 'EPSG:4326' + }); + expect(actions[1].newSession.map.layers.some(l=> l.group === 'background')).toBe(false); + }, states, done); + + + // remove annotation layers + testEpic(removeUserSessionEpicCreator(idSelector), 6, removeUserSession(), (actions) => { + expect(actions[1].newSession.map.layers.some(l=> l.id === 'annotations')).toBe(false); + }, { + ...states, + usersession: { + checkedSessionToClear: ['annotations_layer'] + } + }, done); + + + // remove map positions + testEpic(removeUserSessionEpicCreator(idSelector), 6, removeUserSession(), (actions) => { + expect(actions[1].newSession.map.zoom).toBeFalsy(); + expect(actions[1].newSession.map.center).toBeFalsy(); + }, { + ...states, + usersession: { + checkedSessionToClear: ['map_pos'] + } + }, done); + + }); + it('CLOSE_FEATURE_GRID and TEXT_SEARCH_RESET actions are triggered', (done) => { testEpic(removeUserSessionEpicCreator(idSelector), 6, removeUserSession(), (actions) => { expect(actions[2].type).toBe(CLOSE_FEATURE_GRID); diff --git a/web/client/epics/config.js b/web/client/epics/config.js index 1a92c174f2..fdab536091 100644 --- a/web/client/epics/config.js +++ b/web/client/epics/config.js @@ -7,7 +7,7 @@ */ import { Observable } from 'rxjs'; import axios from '../libs/ajax'; -import { get, merge, isNaN, find, head } from 'lodash'; +import { get, isNaN, find, head } from 'lodash'; import { LOAD_NEW_MAP, LOAD_MAP_CONFIG, @@ -48,10 +48,12 @@ import { import { getSupportedFormat } from '../api/WMS'; import { wrapStartStop } from '../observables/epics'; import { error } from '../actions/notifications'; +import { applyOverrides } from '../utils/ConfigUtils'; + const prepareMapConfiguration = (data, override, state) => { const queryParamsMap = getRequestParameterValue('map', state); - let mapConfig = merge({}, data, override); + let mapConfig = applyOverrides(data, override); mapConfig = { ...mapConfig, ...(queryParamsMap ?? {}), @@ -104,7 +106,7 @@ const mapFlowWithOverride = (configName, mapId, config, mapInfo, state, override const isNumberId = !isNaN(parseFloat(mapId)); return ( config ? - Observable.of({data: merge({}, config, overrideConfig), staticConfig: true}).delay(100) : + Observable.of({data: applyOverrides(config, overrideConfig ), staticConfig: true}).delay(100) : Observable.defer(() => axios.get(configName))) .switchMap(response => { // added !config in order to avoid showing login modal when a new.json mapConfig is used in a public context @@ -161,12 +163,8 @@ export const loadMapConfigAndConfigureMap = (action$, store) => const userName = userSelector(store.getState())?.name; return Observable.of(loadUserSession(buildSessionName(null, mapId, userName))).merge( action$.ofType(USER_SESSION_LOADED).switchMap(({session}) => { - const sessionData = { - ...(session?.map && {map: session.map}), - ...(session?.featureGrid && {featureGrid: session.featureGrid}) - }; return Observable.merge( - mapFlowWithOverride(configName, mapId, config, mapInfo, store.getState(), sessionData), + mapFlowWithOverride(configName, mapId, config, mapInfo, store.getState(), session), Observable.of(userSessionStartSaving()) ); }) diff --git a/web/client/epics/context.js b/web/client/epics/context.js index 31c4afb4fe..48450c4f8f 100644 --- a/web/client/epics/context.js +++ b/web/client/epics/context.js @@ -116,11 +116,10 @@ const createSessionFlow = (mapId, contextName, resourceCategory, action$, getSta (mapId ? Observable.of(null) : getResourceDataByName(resourceCategory, contextName)) ).flatMap(([id, data]) => { const userName = userSelector(getState())?.name; - return Observable.of(loadUserSession(buildSessionName(id, mapId, userName))).merge( + return Observable.of(loadUserSession(buildSessionName(id, mapId, userName))).delay(2000).merge( action$.ofType(USER_SESSION_LOADED).take(1).switchMap(({session}) => { const sessionData = { - ...(session?.map && {map: session.map}), - ...(session?.featureGrid && {featureGrid: session.featureGrid}) + ...session }; const contextSession = session?.context && { ...session.context diff --git a/web/client/epics/maptemplates.js b/web/client/epics/maptemplates.js index 30e1cec367..08c06d916f 100644 --- a/web/client/epics/maptemplates.js +++ b/web/client/epics/maptemplates.js @@ -72,7 +72,8 @@ export const setAllowedTemplatesEpic = (action$, store) => action$ [attr.name]: attr.value }), {}); }; - + // Since templates comes from api, override with session attributes + const sessionMapTemplates = store?.getState().usersession?.session?.mapTemplates; return templates.length > 0 ? Observable .defer(() => Api.searchListByAttributes(makeFilter(), {params: { includeAttributes: true }}, '/resources/search/list')) @@ -84,7 +85,9 @@ export const setAllowedTemplatesEpic = (action$, store) => action$ ...pick(resource, 'id', 'name', 'description'), ...extractAttributes(resource), dataLoaded: false, - loading: false + loading: false, + // override properties from userSession if any + ...sessionMapTemplates?.find(template => template.id === resource.id) })); return Observable.of(setTemplates(newTemplates), setMapTemplatesLoaded(true)); }) diff --git a/web/client/epics/usersession.js b/web/client/epics/usersession.js index 2a16dc24eb..d0cdfa8d45 100644 --- a/web/client/epics/usersession.js +++ b/web/client/epics/usersession.js @@ -23,9 +23,17 @@ import {LOGOUT} from '../actions/security'; import {userSelector} from '../selectors/security'; import { wrapStartStop } from '../observables/epics'; import {originalConfigSelector, userSessionNameSelector, userSessionIdSelector, - userSessionSaveFrequencySelector, userSessionToSaveSelector, isAutoSaveEnabled} from "../selectors/usersession"; + userSessionSaveFrequencySelector, userSessionToSaveSelector, isAutoSaveEnabled, + checkedSessionToClear} from "../selectors/usersession"; +import { REDUCERS_LOADED } from '../actions/storemanager'; +import { setSearchBookmarkConfig } from '../actions/searchbookmarkconfig'; +import { onInitPlayback } from '../actions/playback'; +import { setSearchConfigProp } from '../actions/searchconfig'; +import { updateOverrideConfigToClean } from '../utils/ConfigUtils'; +import { setTemplates } from '../actions/maptemplates'; +import { getRegisterHandlers } from '../selectors/mapsave'; -const {getSession, writeSession, removeSession} = UserSession; +const {getSession, writeSession} = UserSession; const saveUserSessionErrorStatusToMessage = (status) => { switch (status) { @@ -133,14 +141,21 @@ export const loadUserSessionEpicCreator = (nameSelector = userSessionNameSelecto * In order to clean up all plugins state as and where expected, * closeFeatureGrid and resetSearch actions are included in the stream */ -export const removeUserSessionEpicCreator = (idSelector = userSessionIdSelector) => (action$, store) => +export const removeUserSessionEpicCreator = (idSelector = userSessionIdSelector, nameSelector = userSessionNameSelector) => (action$, store) => action$.ofType(REMOVE_USER_SESSION).switchMap(() => { const state = store.getState(); - const sessionId = idSelector(state); - return removeSession(sessionId).switchMap(() => Rx.Observable.of(userSessionRemoved(), closeFeatureGrid(), resetSearch(), success({ + const checks = checkedSessionToClear(store.getState()); + const id = idSelector(state); + const name = nameSelector(state); + const userName = userSelector(state)?.name; + const mapConfig = originalConfigSelector(store.getState()); + // update new Session + const newSession = updateOverrideConfigToClean(userSessionToSaveSelector(state), checks, mapConfig, getRegisterHandlers()); + // TODO: check whether to remove or update session on session serviceListOpenSelector(browser, server) + return writeSession(id, name, userName, newSession).switchMap(() => Rx.Observable.of(userSessionRemoved(newSession), closeFeatureGrid(), resetSearch(), success({ title: "success", - message: "userSession.successRemoved" + message: "userSession.successUpdated" }))).let(wrapStartStop( loading(true, 'userSessionRemoving'), loading(false, 'userSessionRemoving'), @@ -160,10 +175,10 @@ export const removeUserSessionEpicCreator = (idSelector = userSessionIdSelector) * @param {object} store */ export const reloadOriginalConfigEpic = (action$, { getState = () => { } } = {}) => - action$.ofType(USER_SESSION_REMOVED).switchMap(() => { + action$.ofType(USER_SESSION_REMOVED).switchMap(({newSession}) => { const mapConfig = originalConfigSelector(getState()); const mapId = getState()?.mapInitialConfig?.mapId; - return Rx.Observable.of(loadMapConfig(null, mapId, mapConfig, undefined, {}), userSessionStartSaving()); + return Rx.Observable.of(loadMapConfig(null, mapId, mapConfig, undefined, newSession || {}), userSessionStartSaving()); }); export const stopSaveSessionEpic = (action$) => @@ -171,3 +186,32 @@ export const stopSaveSessionEpic = (action$) => action$.ofType(USER_SESSION_START_SAVING).switchMap(() => action$.ofType(USER_SESSION_REMOVED, LOCATION_CHANGE, LOGOUT) .switchMap(() => Rx.Observable.of(userSessionStopSaving()))); + +// some of the reducer are not ready when MAP_CONFIG_LOADED where merging of states takes place +// to handle initial update of states whose reducer are later initialized +// TODO: find better way to handle this, MAP_CONFIG_LOADED is loading before reducer initialization +export const setSessionToDynamicReducers = (action$, store) => { + return action$.ofType(REDUCERS_LOADED).switchMap(() => { + const state = store.getState(); + let observables = []; + + // only enabled in context map and has session + if (!state.context?.resource || !state.usersession?.session) return Rx.Observable.empty(); + + if (state.usersession?.session?.map?.bookmark_search_config) { + observables.push(Rx.Observable.of(setSearchBookmarkConfig('bookmarkSearchConfig', state.usersession?.session?.map?.bookmark_search_config))); + } + if (state.usersession?.session?.playback) { + observables.push(Rx.Observable.of(onInitPlayback({ ...state.usersession.session.playback }))); + } + if (state.usersession?.session?.map?.text_search_config) { + observables.push(Rx.Observable.of(setSearchConfigProp('textSearchConfig', state.usersession?.session?.map?.text_search_config))); + } + // mapTemplates + if (state.usersession?.session?.mapTemplates) { + observables.push(Rx.Observable.of(setTemplates(state.usersession?.session.mapTemplates))); + } + + return observables.length > 0 ? Rx.Observable.merge(...observables) : Rx.Observable.empty(); + }); +}; diff --git a/web/client/plugins/UserSession.jsx b/web/client/plugins/UserSession.jsx index e361a0598f..7dde93e149 100644 --- a/web/client/plugins/UserSession.jsx +++ b/web/client/plugins/UserSession.jsx @@ -5,25 +5,130 @@ * 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, { useEffect } from "react"; +import React, { useEffect, useState } from "react"; import {connect} from "react-redux"; import {createPlugin} from "../utils/PluginsUtils"; import usersession from "../reducers/usersession"; import {saveUserSessionEpicCreator, autoSaveSessionEpicCreator, loadUserSessionEpicCreator, - removeUserSessionEpicCreator, stopSaveSessionEpic, reloadOriginalConfigEpic} from "../epics/usersession"; + removeUserSessionEpicCreator, stopSaveSessionEpic, reloadOriginalConfigEpic, + setSessionToDynamicReducers} from "../epics/usersession"; import Message from "../components/I18N/Message"; import {Glyphicon} from "react-bootstrap"; import {toggleControl} from "../actions/controls"; -import { removeUserSession, enableAutoSave} from "../actions/usersession"; +import { removeUserSession, enableAutoSave, setCheckedSessionToClear} from "../actions/usersession"; import ConfirmModal from "../components/resources/modals/ConfirmModal"; +import Tree from './session/Tree'; +import { getRegisterHandlers } from "../selectors/mapsave"; +import { SESSION_IDS as IDS } from "../utils/ConfigUtils"; + + +const treeData = [ + { + id: IDS.EVERYTHING, + checked: true, + disabled: false, + children: [ + { + id: IDS.MAP, + label: 'Map', + checked: true, + disabled: false, + children: [ + { + id: IDS.MAP_POS, + checked: true, + disabled: false + }, + { + id: IDS.VISUALIZATION_MODE, + checked: true, + disabled: false + }, + { + id: IDS.LAYERS, + checked: true, + disabled: false, + children: [ + { + id: IDS.ANNOTATIONS_LAYER, + checked: true, + disabled: false + }, + { + id: IDS.MEASUREMENTS_LAYER, + checked: true, + disabled: false + }, + { + id: IDS.BACKGROUND_LAYERS, + checked: true, + disabled: false + }, + { + id: IDS.OTHER_LAYERS, + checked: true, + disabled: false + } + ] + } + ] + }, + { + id: IDS.CATALOG_SERVICES, + checked: true, + disabled: false + }, + { + id: IDS.WIDGETS, + checked: true, + disabled: false + }, + { + id: IDS.SEARCH, + checked: true, + disabled: false, + children: [ + { + id: IDS.TEXT_SEARCH_SERVICES, + checked: true, + disabled: false + }, + { + id: IDS.BOOKMARKS, + checked: true, + disabled: false + } + ] + }, + { + id: IDS.FEATURE_GRID, + checked: true, + disabled: false + }, + { + id: IDS.OTHER, + checked: true, + disabled: false, + children: [ + // Additional children can be added here + // handled in userEffect below since customSavedConfig are not ready here + ] + } + ] + } +]; + const ResetUserSession = connect((state) => ({ enabled: state?.controls?.resetUserSession?.enabled ?? false }), { setAutoSave: enableAutoSave, onClose: toggleControl.bind(null, 'resetUserSession', null), - onConfirm: removeUserSession -})(({ enabled = false, onClose, onConfirm, setAutoSave = () => {}}) => { + onConfirm: () => removeUserSession(), + onTreeUpdate: (tree) => setCheckedSessionToClear(tree) +})(({ enabled = false, onClose, onConfirm, setAutoSave = () => {}, onTreeUpdate}) => { + const [trees, setTrees] = useState(treeData); + const confirm = () => { onClose(); onConfirm(); @@ -35,9 +140,43 @@ const ResetUserSession = connect((state) => ({ setAutoSave(false); }; }, []); + + useEffect(() =>{ + if (!enabled) return; + // console.log(getRegisterHandlers(), 'Hello getRegisterHandlers'); + const tree = [...trees]; + // update the children of first element which is `everything` + // `getRegisterHandlers` may not give all handlers at the load time so updating after dialog open + tree[0].children.forEach((child) => { + if (child.id === IDS.OTHER) { + child.children = [ + ...getRegisterHandlers().map((key) => { + return { + id: key, + label: key, + checked: true + }; + }), + { + id: IDS.USER_PLUGINS, + checked: true, + disabled: false + } + ]; + } + }); + setTrees(tree); + }, [enabled]); return ( - ); + +
+ + { + onTreeUpdate(updatedTree); + }}/> +
+ ); }); const hasSession = (state) => state?.usersession?.session; @@ -123,6 +262,6 @@ export default createPlugin('UserSession', { usersession }, epics: { - saveUserSessionEpic, autoSaveSessionEpic, stopSaveSessionEpic, loadUserSessionEpic, removeUserSessionEpic, reloadOriginalConfigEpic + saveUserSessionEpic, autoSaveSessionEpic, stopSaveSessionEpic, loadUserSessionEpic, removeUserSessionEpic, reloadOriginalConfigEpic, setSessionToDynamicReducers } }); diff --git a/web/client/plugins/session/Tree.jsx b/web/client/plugins/session/Tree.jsx new file mode 100644 index 0000000000..aa70176fa8 --- /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..46caaa2236 100644 --- a/web/client/reducers/context.js +++ b/web/client/reducers/context.js @@ -5,9 +5,10 @@ * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. */ -import { SET_CURRENT_CONTEXT, LOADING, SET_RESOURCE, CLEAR_CONTEXT, UPDATE_USER_PLUGIN } from "../actions/context"; +import { SET_CURRENT_CONTEXT, LOADING, SET_RESOURCE, CLEAR_CONTEXT, UPDATE_USER_PLUGIN, SET_USER_PLUGINS } 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.en-US.json b/web/client/translations/data.en-US.json index ecfcea85b3..88c78968a4 100644 --- a/web/client/translations/data.en-US.json +++ b/web/client/translations/data.en-US.json @@ -4000,9 +4000,33 @@ }, "tooltip": "Clear the current user session saved", "successRemoved": "User session removed", + "successUpdated": "User session Updated", "remove": "Reset User Session", - "confirmRemove": "Are you sure you want to restore the default configuration?" - }, + "confirmRemove": "Are you certain you want to revert to the default settings? Choose what you would like to reset:", + "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/utils/ConfigUtils.js b/web/client/utils/ConfigUtils.js index fad8a2dd03..3ec2ad4af5 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, merge } from 'lodash'; import assign from 'object-assign'; import { Promise } from 'es6-promise'; import isMobile from 'ismobilejs'; @@ -487,6 +487,124 @@ 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 + * @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 updateOverrideConfigToClean = (override, thingsToClear = [], originalConfig = {}, customHandlers = {}) => { + let overrideConfig = 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; + } + // annotation layers + if (thingsToClear?.includes(SESSION_IDS.ANNOTATIONS_LAYER)) { + // merge(loadash) on array has problem with arrays with different object in same index + // so putting default layers here + overrideConfig.map.layers = overrideConfig.map.layers.filter((l)=>!l.id.includes('annotations')); + } + // measurements layers + if (thingsToClear?.includes(SESSION_IDS.MEASUREMENTS_LAYER)) { + // merge(loadash) on array has problem with arrays with different object in same index + // so putting default layers here + + overrideConfig.map.layers = overrideConfig.map.layers.filter((l)=>originalConfig?.map?.layers.some(layer=>layer.id === l.id) || !l?.name?.includes('measurements')); + } + // background layers + if (thingsToClear?.includes(SESSION_IDS.BACKGROUND_LAYERS)) { + overrideConfig.map.layers = overrideConfig.map.layers.filter((l)=>originalConfig?.map?.layers.some(layer=>layer.id === l.id) || !l?.group?.includes('background')); + } + // other layers + if (thingsToClear?.includes(SESSION_IDS.OTHER_LAYERS)) { + // merge(loadash) on array has problem with arrays with different object in same index + // so putting default layers here + overrideConfig.map.layers = overrideConfig.map.layers.filter((l)=>{ + return originalConfig?.map?.layers.some(layer=>layer.id === l.id) || l.group?.includes("background") || l?.name?.includes('measurements') || l?.id?.includes('annotations'); + }); + } + // 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)) { + delete overrideConfig.featureGrid; + } + + if (thingsToClear?.includes(SESSION_IDS.USER_PLUGINS)) { + delete overrideConfig.context.userPlugins; + } + + // handle config from registerCustomSaveConfig + // const customHandlers = thingsToClear?.filter(v => getRegisterHandlers().includes(v)); + customHandlers?.forEach((k) => { + delete overrideConfig[k]; // Assuming overrideConfig is an object and k is the key + }); + + return overrideConfig; + +}; +/** + Merge two configurations + * @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 = merge({}, config, override); + 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/MapUtils.js b/web/client/utils/MapUtils.js index bf7a7b5e79..937aec9ed4 100644 --- a/web/client/utils/MapUtils.js +++ b/web/client/utils/MapUtils.js @@ -500,15 +500,15 @@ export const groupSaveFormatted = (node) => { export function saveMapConfiguration(currentMap, currentLayers, currentGroups, currentBackgrounds, textSearchConfig, bookmarkSearchConfig, additionalOptions) { const map = { - center: currentMap.center, - maxExtent: currentMap.maxExtent, - projection: currentMap.projection, - units: currentMap.units, - mapInfoControl: currentMap.mapInfoControl, - zoom: currentMap.zoom, - mapOptions: currentMap.mapOptions || {}, - ...(currentMap.visualizationMode && { visualizationMode: currentMap.visualizationMode }), - ...(currentMap.viewerOptions && { viewerOptions: currentMap.viewerOptions }) + center: currentMap?.center, + maxExtent: currentMap?.maxExtent, + projection: currentMap?.projection, + units: currentMap?.units, + mapInfoControl: currentMap?.mapInfoControl, + zoom: currentMap?.zoom, + mapOptions: currentMap?.mapOptions || {}, + ...(currentMap?.visualizationMode && { visualizationMode: currentMap?.visualizationMode }), + ...(currentMap?.viewerOptions && { viewerOptions: currentMap?.viewerOptions }) }; const layers = currentLayers.map((layer) => { From a7fa7e25ec410aaa20953df13d8ba0c69f15aa73 Mon Sep 17 00:00:00 2001 From: Rohit Gautam Date: Wed, 4 Dec 2024 10:29:56 +0545 Subject: [PATCH 02/10] fix: lint fix --- web/client/reducers/context.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/client/reducers/context.js b/web/client/reducers/context.js index 46caaa2236..30eed0b9dd 100644 --- a/web/client/reducers/context.js +++ b/web/client/reducers/context.js @@ -5,7 +5,7 @@ * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. */ -import { SET_CURRENT_CONTEXT, LOADING, SET_RESOURCE, CLEAR_CONTEXT, UPDATE_USER_PLUGIN, SET_USER_PLUGINS } from "../actions/context"; +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"; From c3d7c731dffcf45c8bea58ea6f9fc1f461ed521f Mon Sep 17 00:00:00 2001 From: Rohit Gautam Date: Wed, 4 Dec 2024 10:45:44 +0545 Subject: [PATCH 03/10] fix: localize in all language --- web/client/translations/data.da-DK.json | 26 ++++++++++++++++++++++++- web/client/translations/data.de-DE.json | 26 ++++++++++++++++++++++++- web/client/translations/data.es-ES.json | 26 ++++++++++++++++++++++++- web/client/translations/data.fr-FR.json | 26 ++++++++++++++++++++++++- web/client/translations/data.is-IS.json | 26 ++++++++++++++++++++++++- web/client/translations/data.it-IT.json | 26 ++++++++++++++++++++++++- web/client/translations/data.nl-NL.json | 26 ++++++++++++++++++++++++- web/client/translations/data.sk-SK.json | 26 ++++++++++++++++++++++++- web/client/translations/data.sv-SE.json | 26 ++++++++++++++++++++++++- 9 files changed, 225 insertions(+), 9 deletions(-) diff --git a/web/client/translations/data.da-DK.json b/web/client/translations/data.da-DK.json index bca47de03e..a612b65fc7 100644 --- a/web/client/translations/data.da-DK.json +++ b/web/client/translations/data.da-DK.json @@ -3726,7 +3726,31 @@ "tooltip": "Clear the current user session saved", "successRemoved": "User session removed", "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..a7624ed134 100644 --- a/web/client/translations/data.de-DE.json +++ b/web/client/translations/data.de-DE.json @@ -4028,7 +4028,31 @@ "tooltip": "Aktuelle gespeicherte Benutzersitzung löschen user", "successRemoved": "Benutzersitzung entfernt", "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.es-ES.json b/web/client/translations/data.es-ES.json index ca6ee04960..9a1ba69c3e 100644 --- a/web/client/translations/data.es-ES.json +++ b/web/client/translations/data.es-ES.json @@ -3990,7 +3990,31 @@ "tooltip": "Borrar la sesión de usuario actual guardada", "successRemoved": "Sesión de usuario eliminada", "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..1c6c23f4d5 100644 --- a/web/client/translations/data.fr-FR.json +++ b/web/client/translations/data.fr-FR.json @@ -3990,7 +3990,31 @@ "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?" + "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..d3b1fa60b2 100644 --- a/web/client/translations/data.is-IS.json +++ b/web/client/translations/data.is-IS.json @@ -3750,7 +3750,31 @@ "tooltip": "Clear the current user session saved", "successRemoved": "User session removed", "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..5e7a668724 100644 --- a/web/client/translations/data.it-IT.json +++ b/web/client/translations/data.it-IT.json @@ -3990,7 +3990,31 @@ "tooltip": "Cancella la sessione utente corrente salvata", "successRemoved": "Sessione utente cancellata", "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..00880cdafb 100644 --- a/web/client/translations/data.nl-NL.json +++ b/web/client/translations/data.nl-NL.json @@ -3994,7 +3994,31 @@ "tooltip": "Reset de huidige gebruikerssessie", "successRemoved": "Gebruikerssessie verwijderd", "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..15631addad 100644 --- a/web/client/translations/data.sk-SK.json +++ b/web/client/translations/data.sk-SK.json @@ -3357,7 +3357,31 @@ }, "successRemoved": "Relácia používateľa bola odstránená", "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..2b44db5903 100644 --- a/web/client/translations/data.sv-SE.json +++ b/web/client/translations/data.sv-SE.json @@ -3396,7 +3396,31 @@ "tooltip": "Rensa den aktuella användarsessionen sparad", "successRemoved": "Användarsession borttagen", "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" + } } } } From 677692672f7fe4b7c24ef9feac1fdf090f81c3bf Mon Sep 17 00:00:00 2001 From: Rohit Gautam Date: Wed, 4 Dec 2024 12:05:26 +0545 Subject: [PATCH 04/10] fix: old tests --- web/client/epics/__tests__/usersession-test.js | 18 +++++++++++++++++- web/client/epics/context.js | 2 +- web/client/utils/MapUtils.js | 18 +++++++++--------- 3 files changed, 27 insertions(+), 11 deletions(-) diff --git a/web/client/epics/__tests__/usersession-test.js b/web/client/epics/__tests__/usersession-test.js index 24eb8c91cb..2af3a165bc 100644 --- a/web/client/epics/__tests__/usersession-test.js +++ b/web/client/epics/__tests__/usersession-test.js @@ -125,7 +125,23 @@ describe('usersession Epics', () => { expect(actions[0].type).toBe(USER_SESSION_LOADING); expect(actions[1].type).toBe(USER_SESSION_REMOVED); expect(actions[1].newSession).toBeTruthy(); - }, initialState, done); + }, {...initialState, + map: { + present: { + center: { + x: -71.88845339541245, + y: 37.25911173702324, + crs: 'EPSG:4326' + }, + maxExtent: [ + -20037508.34, + -20037508.34, + 20037508.34, + 20037508.34 + ] + } + } + }, done); }); it("user Session Update on Partial Session Remove", (done) => { diff --git a/web/client/epics/context.js b/web/client/epics/context.js index 48450c4f8f..235b52dbf8 100644 --- a/web/client/epics/context.js +++ b/web/client/epics/context.js @@ -116,7 +116,7 @@ const createSessionFlow = (mapId, contextName, resourceCategory, action$, getSta (mapId ? Observable.of(null) : getResourceDataByName(resourceCategory, contextName)) ).flatMap(([id, data]) => { const userName = userSelector(getState())?.name; - return Observable.of(loadUserSession(buildSessionName(id, mapId, userName))).delay(2000).merge( + return Observable.of(loadUserSession(buildSessionName(id, mapId, userName))).merge( action$.ofType(USER_SESSION_LOADED).take(1).switchMap(({session}) => { const sessionData = { ...session diff --git a/web/client/utils/MapUtils.js b/web/client/utils/MapUtils.js index 937aec9ed4..bf7a7b5e79 100644 --- a/web/client/utils/MapUtils.js +++ b/web/client/utils/MapUtils.js @@ -500,15 +500,15 @@ export const groupSaveFormatted = (node) => { export function saveMapConfiguration(currentMap, currentLayers, currentGroups, currentBackgrounds, textSearchConfig, bookmarkSearchConfig, additionalOptions) { const map = { - center: currentMap?.center, - maxExtent: currentMap?.maxExtent, - projection: currentMap?.projection, - units: currentMap?.units, - mapInfoControl: currentMap?.mapInfoControl, - zoom: currentMap?.zoom, - mapOptions: currentMap?.mapOptions || {}, - ...(currentMap?.visualizationMode && { visualizationMode: currentMap?.visualizationMode }), - ...(currentMap?.viewerOptions && { viewerOptions: currentMap?.viewerOptions }) + center: currentMap.center, + maxExtent: currentMap.maxExtent, + projection: currentMap.projection, + units: currentMap.units, + mapInfoControl: currentMap.mapInfoControl, + zoom: currentMap.zoom, + mapOptions: currentMap.mapOptions || {}, + ...(currentMap.visualizationMode && { visualizationMode: currentMap.visualizationMode }), + ...(currentMap.viewerOptions && { viewerOptions: currentMap.viewerOptions }) }; const layers = currentLayers.map((layer) => { From 90b57641a94e1d47809c4b9cbd7b510018543235 Mon Sep 17 00:00:00 2001 From: Rohit Gautam Date: Wed, 4 Dec 2024 12:20:15 +0545 Subject: [PATCH 05/10] fix: tests --- web/client/epics/__tests__/usersession-test.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/web/client/epics/__tests__/usersession-test.js b/web/client/epics/__tests__/usersession-test.js index 2af3a165bc..f5ac327870 100644 --- a/web/client/epics/__tests__/usersession-test.js +++ b/web/client/epics/__tests__/usersession-test.js @@ -205,7 +205,21 @@ describe('usersession Epics', () => { testEpic(removeUserSessionEpicCreator(idSelector), 6, removeUserSession(), (actions) => { expect(actions[2].type).toBe(CLOSE_FEATURE_GRID); expect(actions[3].type).toBe(TEXT_SEARCH_RESET); - }, initialState, done); + }, {...initialState, map: { + present: { + center: { + x: -71.88845339541245, + y: 37.25911173702324, + crs: 'EPSG:4326' + }, + maxExtent: [ + -20037508.34, + -20037508.34, + 20037508.34, + 20037508.34 + ] + } + }}, done); }); }); From 1835f9316299845962e69c5731fda60d24a6ff02 Mon Sep 17 00:00:00 2001 From: Rohit Gautam Date: Thu, 16 Jan 2025 15:45:59 +0545 Subject: [PATCH 06/10] fix: simplify override logic for user Session, docs, test --- web/client/epics/usersession.js | 4 +- web/client/plugins/UserSession.jsx | 57 ++++++- web/client/plugins/session/Tree.jsx | 2 +- web/client/translations/data.da-DK.json | 1 + web/client/translations/data.de-DE.json | 1 + web/client/translations/data.en-US.json | 3 +- web/client/translations/data.es-ES.json | 1 + web/client/translations/data.fr-FR.json | 1 + web/client/translations/data.is-IS.json | 1 + web/client/translations/data.it-IT.json | 1 + web/client/translations/data.nl-NL.json | 1 + web/client/translations/data.sk-SK.json | 1 + web/client/translations/data.sv-SE.json | 1 + web/client/utils/ConfigUtils.js | 53 +++--- web/client/utils/LayersUtils.js | 2 +- .../utils/__tests__/OverrideConfig-test.js | 156 ++++++++++++++++++ 16 files changed, 253 insertions(+), 33 deletions(-) create mode 100644 web/client/utils/__tests__/OverrideConfig-test.js diff --git a/web/client/epics/usersession.js b/web/client/epics/usersession.js index d0cdfa8d45..e6f1b8a651 100644 --- a/web/client/epics/usersession.js +++ b/web/client/epics/usersession.js @@ -29,7 +29,7 @@ import { REDUCERS_LOADED } from '../actions/storemanager'; import { setSearchBookmarkConfig } from '../actions/searchbookmarkconfig'; import { onInitPlayback } from '../actions/playback'; import { setSearchConfigProp } from '../actions/searchconfig'; -import { updateOverrideConfigToClean } from '../utils/ConfigUtils'; +import { updateOverrideConfig } from '../utils/ConfigUtils'; import { setTemplates } from '../actions/maptemplates'; import { getRegisterHandlers } from '../selectors/mapsave'; @@ -151,7 +151,7 @@ export const removeUserSessionEpicCreator = (idSelector = userSessionIdSelector, const userName = userSelector(state)?.name; const mapConfig = originalConfigSelector(store.getState()); // update new Session - const newSession = updateOverrideConfigToClean(userSessionToSaveSelector(state), checks, mapConfig, getRegisterHandlers()); + const newSession = updateOverrideConfig(userSessionToSaveSelector(state), checks, mapConfig, getRegisterHandlers()); // TODO: check whether to remove or update session on session serviceListOpenSelector(browser, server) return writeSession(id, name, userName, newSession).switchMap(() => Rx.Observable.of(userSessionRemoved(newSession), closeFeatureGrid(), resetSearch(), success({ title: "success", diff --git a/web/client/plugins/UserSession.jsx b/web/client/plugins/UserSession.jsx index 7dde93e149..f84af2548b 100644 --- a/web/client/plugins/UserSession.jsx +++ b/web/client/plugins/UserSession.jsx @@ -128,6 +128,7 @@ const ResetUserSession = connect((state) => ({ onTreeUpdate: (tree) => setCheckedSessionToClear(tree) })(({ enabled = false, onClose, onConfirm, setAutoSave = () => {}, onTreeUpdate}) => { const [trees, setTrees] = useState(treeData); + const [othersRetrieved, setOthersRetrieved] = useState(false); const confirm = () => { onClose(); @@ -142,8 +143,7 @@ const ResetUserSession = connect((state) => ({ }, []); useEffect(() =>{ - if (!enabled) return; - // console.log(getRegisterHandlers(), 'Hello getRegisterHandlers'); + if (!enabled || othersRetrieved) return; const tree = [...trees]; // update the children of first element which is `everything` // `getRegisterHandlers` may not give all handlers at the load time so updating after dialog open @@ -166,9 +166,11 @@ const ResetUserSession = connect((state) => ({ } }); setTrees(tree); + setOthersRetrieved(true); }, [enabled]); return ( + title ={} + onConfirm={confirm} show={enabled} buttonSize="medium">
@@ -187,7 +189,7 @@ 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. @@ -197,9 +199,30 @@ const removeUserSessionEpic = removeUserSessionEpicCreator(); * 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 + * * a session is loaded from the store and if it exists, it overrides the standard map configuration; + * Following config are saved in session and can be restored to default config individually + * 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 * * the session is automatically saved at a configurable interval - * * an item in the `BurgerMenu` allows to restore the session to the default map configuration + * * an item with Brush icon in the `BurgerMenu` and `SidebarMenu` allows to open Session Tree which can be used what to restore 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. @@ -223,6 +246,24 @@ const removeUserSessionEpic = removeUserSessionEpicCreator(); * writeSession: ..., * removeSession: ... * } + * ```mermaid + * graph TD + * S[Start] --> A + * A[Load Context] --> C + * C[LOAD Map Config] + * D[User Session] ---> |Load Saved Session| E + * A ---> D + * E[Retrieved Session] --> C + * C[Load Map Config] ---> |Merge Config, Give priority to override value than original config if both values are arrays| F + * F[Updated all states from config] + * D ----> |Save sessions on interval| D + * D --> | Reset Session Individually| G + * G[handle partially resetting items like layers. include layers of original config also] --> H + * H[updated override config] --> C + * H---> |Save Updated Config to Session|D + * D ----> I + * I[Session store] ---> |Handle states of Dynamic Reducers which are not loaded at the time of LOAD Map Config, Handle them at REDUCER_LOADED|J + * J[Updated states of Dynamic Reducers] * ``` * @memberof plugins * @name UserSession @@ -235,7 +276,7 @@ export default createPlugin('UserSession', { name: 'UserSession', position: 1500, text: , - icon: , + icon: , action: toggleControl.bind(null, 'resetUserSession', null), tooltip: , selector: (state) => { @@ -247,7 +288,7 @@ export default createPlugin('UserSession', { SidebarMenu: { name: 'UserSession', position: 1500, - icon: , + icon: , text: , action: toggleControl.bind(null, 'resetUserSession', null), tooltip: "userSession.tooltip", diff --git a/web/client/plugins/session/Tree.jsx b/web/client/plugins/session/Tree.jsx index aa70176fa8..aa2825c2c0 100644 --- a/web/client/plugins/session/Tree.jsx +++ b/web/client/plugins/session/Tree.jsx @@ -113,7 +113,7 @@ const TreeNode = ({ node, onToggle, onCheck }) => { }, [node.indeterminate]); return ( -
+
{node.children && ( { - let overrideConfig = override; +export const updateOverrideConfig = (override = {}, thingsToClear = [], originalConfig = {}, customHandlers = []) => { + let overrideConfig = JSON.parse(JSON.stringify(override)); if (thingsToClear?.includes(SESSION_IDS.EVERYTHING)) { overrideConfig = {}; @@ -533,31 +533,31 @@ export const updateOverrideConfigToClean = (override, thingsToClear = [], origin 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)) { - // merge(loadash) on array has problem with arrays with different object in same index - // so putting default layers here - overrideConfig.map.layers = overrideConfig.map.layers.filter((l)=>!l.id.includes('annotations')); + 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)) { - // merge(loadash) on array has problem with arrays with different object in same index - // so putting default layers here - - overrideConfig.map.layers = overrideConfig.map.layers.filter((l)=>originalConfig?.map?.layers.some(layer=>layer.id === l.id) || !l?.name?.includes('measurements')); + 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)=>originalConfig?.map?.layers.some(layer=>layer.id === l.id) || !l?.group?.includes('background')); + 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)) { - // merge(loadash) on array has problem with arrays with different object in same index - // so putting default layers here - overrideConfig.map.layers = overrideConfig.map.layers.filter((l)=>{ - return originalConfig?.map?.layers.some(layer=>layer.id === l.id) || l.group?.includes("background") || l?.name?.includes('measurements') || l?.id?.includes('annotations'); - }); + 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; @@ -579,7 +579,10 @@ export const updateOverrideConfigToClean = (override, thingsToClear = [], origin // feature grid if (thingsToClear?.includes(SESSION_IDS.FEATURE_GRID)) { - delete overrideConfig.featureGrid; + // 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)) { @@ -587,11 +590,13 @@ export const updateOverrideConfigToClean = (override, thingsToClear = [], origin } // handle config from registerCustomSaveConfig - // const customHandlers = thingsToClear?.filter(v => getRegisterHandlers().includes(v)); customHandlers?.forEach((k) => { - delete overrideConfig[k]; // Assuming overrideConfig is an object and k is the key + if (thingsToClear?.includes(k)) { + delete overrideConfig[k]; + } }); + return overrideConfig; }; @@ -602,7 +607,15 @@ export const updateOverrideConfigToClean = (override, thingsToClear = [], origin * @returns {object} */ export const applyOverrides = (config, override) => { - const merged = merge({}, config, override); + const merged = mergeWith({}, config, override, (objValue, srcValue) => { + // Till now layers is the only case that get partially reset, so merging with original config happens in updateOverrideConfig(above in the this file) while reset + if (Array.isArray(objValue) && Array.isArray(srcValue)) { + return [...srcValue]; + } + // default merge rules for other cases + // eslint-disable-next-line consistent-return + return undefined; + }); return merged; }; const ConfigUtils = { diff --git a/web/client/utils/LayersUtils.js b/web/client/utils/LayersUtils.js index e5bd5ba4c6..f80ad6b938 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__/OverrideConfig-test.js b/web/client/utils/__tests__/OverrideConfig-test.js new file mode 100644 index 0000000000..0a2bd5c013 --- /dev/null +++ b/web/client/utils/__tests__/OverrideConfig-test.js @@ -0,0 +1,156 @@ +// import { updateOverrideConfig } from './yourModule'; // Import the function +// import { SESSION_IDS } from './yourConstants'; // Assuming SESSION_IDS is imported from another file +import expect from 'expect'; +import { SESSION_IDS, updateOverrideConfig } from "../ConfigUtils"; + +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 SESSION_IDS.EVERYTHING is in thingsToClear', () => { + const result = updateOverrideConfig(override, [SESSION_IDS.EVERYTHING], originalConfig); + expect(result).toEqual({}); + }); + + it('should clear map zoom and center when SESSION_IDS.MAP_POS 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 SESSION_IDS.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 SESSION_IDS.ANNOTATIONS_LAYER 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 SESSION_IDS.MEASUREMENTS_LAYER 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 layer and add background layers from OriginalConfig when SESSION_IDS.MEASUREMENTS_LAYER 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 SESSION_IDS.CATALOG_SERVICES is in thingsToClear', () => { + const result = updateOverrideConfig(override, [SESSION_IDS.CATALOG_SERVICES], originalConfig); + expect(result.catalogServices).toBe(undefined); + }); + + it('should clear widgetsConfig when SESSION_IDS.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 SESSION_IDS.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 SESSION_IDS.FEATURE_GRID is in thingsToClear', () => { + const result = updateOverrideConfig(override, [SESSION_IDS.FEATURE_GRID], originalConfig); + expect(result.featureGrid.attributes).toEqual({}); + }); + + it('should clear userPlugins when SESSION_IDS.USER_PLUGINS 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); + }); +}); + From c7134a75c9443b935325e480c3384afb5239f42e Mon Sep 17 00:00:00 2001 From: Rohit Gautam Date: Thu, 16 Jan 2025 17:30:30 +0545 Subject: [PATCH 07/10] fix: newSession to save update, unit tests --- .../epics/__tests__/usersession-test.js | 118 +++++++++++++++--- web/client/epics/usersession.js | 5 +- web/client/utils/ConfigUtils.js | 10 +- web/client/utils/LayersUtils.js | 2 +- .../utils/__tests__/OverrideConfig-test.js | 24 ++-- 5 files changed, 122 insertions(+), 37 deletions(-) diff --git a/web/client/epics/__tests__/usersession-test.js b/web/client/epics/__tests__/usersession-test.js index f5ac327870..f144b4cfaa 100644 --- a/web/client/epics/__tests__/usersession-test.js +++ b/web/client/epics/__tests__/usersession-test.js @@ -144,7 +144,7 @@ describe('usersession Epics', () => { }, done); }); - it("user Session Update on Partial Session Remove", (done) => { + it("user Session Update on Partial Session Remove - Background Layers", (done) => { const states = { ...initialState, map: { @@ -160,7 +160,12 @@ describe('usersession Epics', () => { layers: [{id: "layer1", group: 'background'}, {id: "layer2"}, {id: "layer3]"}], toc: {test: false}, usersession: { - checkedSessionToClear: ['background_layers'] + checkedSessionToClear: ['background_layers'], + config: { + map: { + layers: [] + } + } } }; @@ -173,34 +178,113 @@ describe('usersession Epics', () => { y: 42.617791432823395, crs: 'EPSG:4326' }); - expect(actions[1].newSession.map.layers.some(l=> l.group === 'background')).toBe(false); + expect(actions[1].newSession.map.layers.some(l => l.group === 'background')).toBe(false); }, states, done); + }); - + it("user Session Update on Partial Session Remove - Annotation Layers", (done) => { + const states = { + ...initialState, + map: { + present: { + center: { + x: 118.91601562499996, + y: 42.617791432823395, + crs: 'EPSG:4326' + }, + zoom: 16 + } + }, + layers: [{id: "annotations-1", group: 'background'}, {id: "layer2"}, {id: "layer3]"}], + toc: {test: false}, + usersession: { + checkedSessionToClear: ['annotations_layer'], + config: { + map: { + layers: [{ + id: "annotations-from-original" + }] + } + } + } + }; // remove annotation layers testEpic(removeUserSessionEpicCreator(idSelector), 6, removeUserSession(), (actions) => { - expect(actions[1].newSession.map.layers.some(l=> l.id === 'annotations')).toBe(false); - }, { - ...states, + // removed from session + expect(actions[1].newSession.map.layers.some(l => l.id === 'annotations-1')).toBe(false); + // retrieved from original Config + expect(actions[1].newSession.map.layers.some(l => l.id === 'annotations-from-original')).toBe(true); + }, states, done); + }); + + it("Check background layers after reset: find background layer of original config after reset", (done) => { + const states = { + ...initialState, + map: { + }, + layers: [{id: "layer1", group: 'background'}, {id: "layer2"}, {id: "layer3]"}], + toc: {test: false}, usersession: { - checkedSessionToClear: ['annotations_layer'] + checkedSessionToClear: ['background_layers'], + config: { + map: { + layers: [{id: "background_from_original_config", group: 'background'}] + } + } } - }, done); - + }; - // remove map positions + // reset background layers testEpic(removeUserSessionEpicCreator(idSelector), 6, removeUserSession(), (actions) => { - expect(actions[1].newSession.map.zoom).toBeFalsy(); - expect(actions[1].newSession.map.center).toBeFalsy(); - }, { - ...states, + expect(actions[1].newSession.map.layers.some(l => l.id === 'layer1')).toBe(false); + expect(actions[1].newSession.map.layers.some(l => l.id === 'background_from_original_config')).toBe(true); + }, states, done); + }); + + + it("user Session Update on Partial Session Remove - Map Positions", (done) => { + const states = { + ...initialState, + map: { + present: { + center: { + x: 118.91601562499996, + y: 42.617791432823395, + crs: 'EPSG:4326' + }, + zoom: 16 + } + }, + layers: [{id: "layer1", group: 'background'}, {id: "layer2"}, {id: "layer3"}], + toc: {test: false}, usersession: { - checkedSessionToClear: ['map_pos'] + checkedSessionToClear: ['map_pos'], + config: { + map: { + layers: [], + center: { + "x": 11.26, + "y": 43.77, + "crs": "EPSG:4326" + }, + zoom: 12 + } + } } - }, done); + }; + // remove map positions + testEpic(removeUserSessionEpicCreator(idSelector), 6, removeUserSession(), (actions) => { + // console.log(actions[1].newSession, "newSessionTest"); + expect(actions[1].newSession.map.center.x).toBe(11.26); + expect(actions[1].newSession.map.center.y).toBe(43.77); + expect(actions[1].newSession.map.zoom).toBe(12); + // check others session is same + expect(actions[1].newSession.map.layers.length).toEqual([3]); + }, states, done); }); + it('CLOSE_FEATURE_GRID and TEXT_SEARCH_RESET actions are triggered', (done) => { testEpic(removeUserSessionEpicCreator(idSelector), 6, removeUserSession(), (actions) => { expect(actions[2].type).toBe(CLOSE_FEATURE_GRID); diff --git a/web/client/epics/usersession.js b/web/client/epics/usersession.js index e6f1b8a651..46bbca03a7 100644 --- a/web/client/epics/usersession.js +++ b/web/client/epics/usersession.js @@ -29,7 +29,7 @@ import { REDUCERS_LOADED } from '../actions/storemanager'; import { setSearchBookmarkConfig } from '../actions/searchbookmarkconfig'; import { onInitPlayback } from '../actions/playback'; import { setSearchConfigProp } from '../actions/searchconfig'; -import { updateOverrideConfig } from '../utils/ConfigUtils'; +import { applyOverrides, updateOverrideConfig } from '../utils/ConfigUtils'; import { setTemplates } from '../actions/maptemplates'; import { getRegisterHandlers } from '../selectors/mapsave'; @@ -151,7 +151,8 @@ export const removeUserSessionEpicCreator = (idSelector = userSessionIdSelector, const userName = userSelector(state)?.name; const mapConfig = originalConfigSelector(store.getState()); // update new Session - const newSession = updateOverrideConfig(userSessionToSaveSelector(state), checks, mapConfig, getRegisterHandlers()); + const overrideConfig = updateOverrideConfig(userSessionToSaveSelector(state), checks, mapConfig, getRegisterHandlers()); + const newSession = applyOverrides(mapConfig, overrideConfig); // TODO: check whether to remove or update session on session serviceListOpenSelector(browser, server) return writeSession(id, name, userName, newSession).switchMap(() => Rx.Observable.of(userSessionRemoved(newSession), closeFeatureGrid(), resetSearch(), success({ title: "success", diff --git a/web/client/utils/ConfigUtils.js b/web/client/utils/ConfigUtils.js index dec0a89677..5bd2b4d60d 100644 --- a/web/client/utils/ConfigUtils.js +++ b/web/client/utils/ConfigUtils.js @@ -539,24 +539,24 @@ export const updateOverrideConfig = (override = {}, thingsToClear = [], original // 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'))]; + 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'))]; + 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'))]; + 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"))]; + 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) + 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)) { 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__/OverrideConfig-test.js b/web/client/utils/__tests__/OverrideConfig-test.js index 0a2bd5c013..a87401bb2c 100644 --- a/web/client/utils/__tests__/OverrideConfig-test.js +++ b/web/client/utils/__tests__/OverrideConfig-test.js @@ -67,23 +67,23 @@ describe('updateOverrideConfig', () => { }; }); - it('should clear everything when SESSION_IDS.EVERYTHING is in thingsToClear', () => { + 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 SESSION_IDS.MAP_POS is in thingsToClear', () => { + 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 SESSION_IDS.VISUALIZATION_MODE is in thingsToClear', () => { + 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 SESSION_IDS.ANNOTATIONS_LAYER is in thingsToClear', () => { + 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' }, @@ -92,7 +92,7 @@ describe('updateOverrideConfig', () => { ]); }); - it('should clear measurement layers when SESSION_IDS.MEASUREMENTS_LAYER is in thingsToClear', () => { + 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' }, @@ -100,7 +100,7 @@ describe('updateOverrideConfig', () => { {id: 'annotations'} ]); }); - it('should clear background layer and add background layers from OriginalConfig when SESSION_IDS.MEASUREMENTS_LAYER is in thingsToClear', () => { + 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' }, @@ -111,12 +111,12 @@ describe('updateOverrideConfig', () => { ]); }); - it('should clear catalogServices when SESSION_IDS.CATALOG_SERVICES is in thingsToClear', () => { + 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 SESSION_IDS.WIDGETS is in thingsToClear', () => { + it('should clear widgetsConfig when "Widgets" is in thingsToClear', () => { const result = updateOverrideConfig(override, [SESSION_IDS.WIDGETS], originalConfig); expect(result.widgetsConfig).toBe(undefined); }); @@ -126,17 +126,17 @@ describe('updateOverrideConfig', () => { expect(result.map.text_search_config).toBe(undefined); }); - it('should clear bookmark search config when SESSION_IDS.BOOKMARKS is in thingsToClear', () => { + 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 SESSION_IDS.FEATURE_GRID is in thingsToClear', () => { + 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 SESSION_IDS.USER_PLUGINS is in thingsToClear', () => { + 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); }); @@ -145,7 +145,7 @@ describe('updateOverrideConfig', () => { const customHandlers = [ "toc" ]; - const customOverride = { ...override, toc: { + const customOverride = {...override, toc: { "theme": "legend" } }; From 64242215d82be6846de9db24bed84a847a4e006a Mon Sep 17 00:00:00 2001 From: Rohit Gautam Date: Fri, 17 Jan 2025 12:19:59 +0545 Subject: [PATCH 08/10] fix: update docs, tests --- web/client/plugins/UserSession.jsx | 91 ++++++++----------- web/client/utils/ConfigUtils.js | 20 +++- .../utils/__tests__/ApplyOverride-test.js | 70 ++++++++++++++ 3 files changed, 124 insertions(+), 57 deletions(-) create mode 100644 web/client/utils/__tests__/ApplyOverride-test.js diff --git a/web/client/plugins/UserSession.jsx b/web/client/plugins/UserSession.jsx index f84af2548b..f6230dfd99 100644 --- a/web/client/plugins/UserSession.jsx +++ b/web/client/plugins/UserSession.jsx @@ -194,50 +194,49 @@ const removeUserSessionEpic = removeUserSessionEpicCreator(); * 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; - * Following config are saved in session and can be restored to default config individually - * 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 - * * the session is automatically saved at a configurable interval - * * an item with Brush icon in the `BurgerMenu` and `SidebarMenu` allows to open Session Tree which can be used what to restore individually + * - 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" @@ -246,25 +245,9 @@ const removeUserSessionEpic = removeUserSessionEpicCreator(); * writeSession: ..., * removeSession: ... * } - * ```mermaid - * graph TD - * S[Start] --> A - * A[Load Context] --> C - * C[LOAD Map Config] - * D[User Session] ---> |Load Saved Session| E - * A ---> D - * E[Retrieved Session] --> C - * C[Load Map Config] ---> |Merge Config, Give priority to override value than original config if both values are arrays| F - * F[Updated all states from config] - * D ----> |Save sessions on interval| D - * D --> | Reset Session Individually| G - * G[handle partially resetting items like layers. include layers of original config also] --> H - * H[updated override config] --> C - * H---> |Save Updated Config to Session|D - * D ----> I - * I[Session store] ---> |Handle states of Dynamic Reducers which are not loaded at the time of LOAD Map Config, Handle them at REDUCER_LOADED|J - * J[Updated states of Dynamic Reducers] * ``` + * + * * @memberof plugins * @name UserSession * @class diff --git a/web/client/utils/ConfigUtils.js b/web/client/utils/ConfigUtils.js index 5bd2b4d60d..addc657830 100644 --- a/web/client/utils/ConfigUtils.js +++ b/web/client/utils/ConfigUtils.js @@ -510,6 +510,13 @@ export const SESSION_IDS = { /* * 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 @@ -601,16 +608,23 @@ export const updateOverrideConfig = (override = {}, thingsToClear = [], original }; /** - Merge two configurations +* 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, so merging with original config happens in updateOverrideConfig(above in the this file) while reset + // 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)) { - return [...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 diff --git a/web/client/utils/__tests__/ApplyOverride-test.js b/web/client/utils/__tests__/ApplyOverride-test.js new file mode 100644 index 0000000000..4a65b1d827 --- /dev/null +++ b/web/client/utils/__tests__/ApplyOverride-test.js @@ -0,0 +1,70 @@ +import expect from 'expect'; +import { applyOverrides } from '../ConfigUtils'; +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); + }); + +}); From f89af694dcdfe7ab1436df90bb1162ca7f29dd07 Mon Sep 17 00:00:00 2001 From: Rohit Gautam Date: Fri, 17 Jan 2025 12:54:18 +0545 Subject: [PATCH 09/10] fix: unit tests for applyOverride --- web/client/utils/__tests__/ApplyOverride-test.js | 4 ++++ ...{OverrideConfig-test.js => UpdateOverrideConfig-test.js} | 6 ++++-- 2 files changed, 8 insertions(+), 2 deletions(-) rename web/client/utils/__tests__/{OverrideConfig-test.js => UpdateOverrideConfig-test.js} (97%) diff --git a/web/client/utils/__tests__/ApplyOverride-test.js b/web/client/utils/__tests__/ApplyOverride-test.js index 4a65b1d827..556ac43a51 100644 --- a/web/client/utils/__tests__/ApplyOverride-test.js +++ b/web/client/utils/__tests__/ApplyOverride-test.js @@ -1,5 +1,9 @@ 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', () => { diff --git a/web/client/utils/__tests__/OverrideConfig-test.js b/web/client/utils/__tests__/UpdateOverrideConfig-test.js similarity index 97% rename from web/client/utils/__tests__/OverrideConfig-test.js rename to web/client/utils/__tests__/UpdateOverrideConfig-test.js index a87401bb2c..6b05962470 100644 --- a/web/client/utils/__tests__/OverrideConfig-test.js +++ b/web/client/utils/__tests__/UpdateOverrideConfig-test.js @@ -1,8 +1,10 @@ -// import { updateOverrideConfig } from './yourModule'; // Import the function -// import { SESSION_IDS } from './yourConstants'; // Assuming SESSION_IDS is imported from another file + 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; From e1da172c8b38f533f4203d6f6b1d3d5b7f0b5de7 Mon Sep 17 00:00:00 2001 From: Rohit Gautam Date: Tue, 21 Jan 2025 13:51:34 +0545 Subject: [PATCH 10/10] fix: remove previously saved session if plugin is removed --- web/client/actions/usersession.js | 2 + .../epics/__tests__/usersession-test.js | 23 +++++++++++- web/client/epics/usersession.js | 37 +++++++++++++++++-- web/client/plugins/UserSession.jsx | 5 ++- 4 files changed, 60 insertions(+), 7 deletions(-) diff --git a/web/client/actions/usersession.js b/web/client/actions/usersession.js index fa985322a6..e4fe6742d9 100644 --- a/web/client/actions/usersession.js +++ b/web/client/actions/usersession.js @@ -19,6 +19,7 @@ export const USER_SESSION_STOP_SAVING = "USER_SESSION:STOP_SAVING"; export const SET_USER_SESSION = "USER_SESSION:SET"; export const ENABLE_AUTO_SAVE = "USER_SESSION:ENABLE_AUTO_SAVE"; export const SET_CHECKED_SESSION_TO_CLEAR = "USER_SESSION:SET_CHECKED_SESSION_TO_CLEAR"; +export const CLEAR_SESSION_IF_PLUGIN_MISSING = "USER_SESSION:CLEAR_SESSION_IF_PLUGIN_MISSING"; export const saveUserSession = () => ({type: SAVE_USER_SESSION}); export const userSessionSaved = (id, session) => ({type: USER_SESSION_SAVED, id, session}); @@ -31,6 +32,7 @@ export const userSessionStopSaving = () => ({type: USER_SESSION_STOP_SAVING}); export const saveMapConfig = (config) => ({type: SAVE_MAP_CONFIG, config}); export const setUserSession = (session) => ({type: SET_USER_SESSION, session}); export const setCheckedSessionToClear = (checks) => ({type: SET_CHECKED_SESSION_TO_CLEAR, checks}); +export const clearSessionIfPluginMissing = (id, currentSession) => ({type: CLEAR_SESSION_IF_PLUGIN_MISSING, id, currentSession}); /** * Action to enable/disable the auto-save functionality. * @param {boolean} enabled flag to enable/disable the auto-save for session diff --git a/web/client/epics/__tests__/usersession-test.js b/web/client/epics/__tests__/usersession-test.js index f144b4cfaa..2eef4c000d 100644 --- a/web/client/epics/__tests__/usersession-test.js +++ b/web/client/epics/__tests__/usersession-test.js @@ -6,9 +6,9 @@ * LICENSE file in the root directory of this source tree. */ import { testEpic } from './epicTestUtils'; -import { saveUserSessionEpicCreator, autoSaveSessionEpicCreator, loadUserSessionEpicCreator, removeUserSessionEpicCreator } from "../usersession"; +import { saveUserSessionEpicCreator, autoSaveSessionEpicCreator, loadUserSessionEpicCreator, removeUserSessionEpicCreator, clearSessionIfPluginMissingEpic } from "../usersession"; import { saveUserSession, loadUserSession, - USER_SESSION_SAVED, USER_SESSION_LOADING, SAVE_USER_SESSION, USER_SESSION_LOADED, USER_SESSION_REMOVED, userSessionStartSaving, userSessionStopSaving, removeUserSession + USER_SESSION_SAVED, USER_SESSION_LOADING, SAVE_USER_SESSION, USER_SESSION_LOADED, USER_SESSION_REMOVED, userSessionStartSaving, userSessionStopSaving, removeUserSession, clearSessionIfPluginMissing } from "../../actions/usersession"; import { CLOSE_FEATURE_GRID } from '../../actions/featuregrid'; import { TEXT_SEARCH_RESET } from '../../actions/search'; @@ -16,6 +16,7 @@ import expect from "expect"; import {Providers} from "../../api/usersession"; import {Observable} from "rxjs"; import ConfigUtils from "../../utils/ConfigUtils"; +import { LOAD_MAP_CONFIG } from '../../actions/config'; describe('usersession Epics', () => { const initialState = { @@ -306,4 +307,22 @@ describe('usersession Epics', () => { }}, done); }); + it("clearSessionIfPluginMissingEpic: re-update mapConfig with default Config ", (done) => { + testEpic(clearSessionIfPluginMissingEpic, 1, clearSessionIfPluginMissing("sessionId", {dummySession: "yes"} ), (actions) =>{ + // after removing session removes session and updates with default config(LOAD_MAP_CONFIG) + expect(actions[0].type).toBe(LOAD_MAP_CONFIG); + expect(actions[0].overrideConfig).toEqual({}); + expect(actions[0].config.map.layers.length).toBe(2); + }, + { + ...initialState, + usersession: { + // this indicates userSession plugin is missing + autoSave: false, + config: {map: {layers: [{id: "1"}, {id: "2"}]}} + } + }, done); + + }); + }); diff --git a/web/client/epics/usersession.js b/web/client/epics/usersession.js index 46bbca03a7..7c78c6f40f 100644 --- a/web/client/epics/usersession.js +++ b/web/client/epics/usersession.js @@ -11,7 +11,9 @@ import { error, success } from '../actions/notifications'; import { SAVE_USER_SESSION, LOAD_USER_SESSION, REMOVE_USER_SESSION, USER_SESSION_REMOVED, USER_SESSION_START_SAVING, USER_SESSION_STOP_SAVING, userSessionSaved, userSessionLoaded, loading, saveUserSession, userSessionRemoved, - userSessionStartSaving, userSessionStopSaving + userSessionStartSaving, userSessionStopSaving, + clearSessionIfPluginMissing, + CLEAR_SESSION_IF_PLUGIN_MISSING } from "../actions/usersession"; import { closeFeatureGrid } from '../actions/featuregrid'; import { resetSearch } from '../actions/search'; @@ -33,7 +35,7 @@ import { applyOverrides, updateOverrideConfig } from '../utils/ConfigUtils'; import { setTemplates } from '../actions/maptemplates'; import { getRegisterHandlers } from '../selectors/mapsave'; -const {getSession, writeSession} = UserSession; +const {getSession, writeSession, removeSession} = UserSession; const saveUserSessionErrorStatusToMessage = (status) => { switch (status) { @@ -119,7 +121,8 @@ export const loadUserSessionEpicCreator = (nameSelector = userSessionNameSelecto const sessionName = name || nameSelector(state); return getSession(sessionName) .switchMap(([id, session]) => Rx.Observable.of( - userSessionLoaded(id, session) + userSessionLoaded(id, session), + clearSessionIfPluginMissing(id, session) )) .let(wrapStartStop( loading(true, 'userSessionLoading'), @@ -128,6 +131,34 @@ export const loadUserSessionEpicCreator = (nameSelector = userSessionNameSelecto )); }); +/** + * Epic to clear the session if the plugin is missing. Here checking of `userSession` plugin is done checking autoSave state. If 3 secs after loading session autoSave is false then, UserSession plugin is missing + * + * This function listens for the `CLEAR_SESSION_IF_PLUGIN_MISSING` action and introduces a delay + * should be cleared based on the `autoSave` state(UserSession plugin is active or not). If the session is exists and `autoSave` is false(ensures UserSession plugin is not active), it dispatches an action to remove + * the current session and loads the map configuration with default Config. + */ +export const clearSessionIfPluginMissingEpic = (action$, store) => { + return action$.ofType(CLEAR_SESSION_IF_PLUGIN_MISSING).switchMap(({id, currentSession}) => { + + // Introduce a delay using Rx.Observable.timer + return Rx.Observable.timer(1800).switchMap(() => { + const autoSave = store.getState().usersession.autoSave; + // if !autoSave shows UserSession plugin is missing, currentSession shows in the past plugin was active and was saving session + // following check says userSession plugin may have been removed now, so time to remove session and fall back to default config + if (!autoSave && currentSession) { + return removeSession(id).switchMap(() =>{ + const mapConfig = originalConfigSelector(store.getState()); + const mapId = store.getState()?.mapInitialConfig?.mapId; + return Rx.Observable.of(loadMapConfig(null, mapId, mapConfig, undefined, {})); + }); + } + // else do nothing + return Rx.Observable.empty(); // No action, return empty observable + + }); + }); +}; /** * Returns a user session remove epic. * The epic triggers on a REMOVE_USER_SESSION action. diff --git a/web/client/plugins/UserSession.jsx b/web/client/plugins/UserSession.jsx index f6230dfd99..b9b2c092dc 100644 --- a/web/client/plugins/UserSession.jsx +++ b/web/client/plugins/UserSession.jsx @@ -11,7 +11,8 @@ import {createPlugin} from "../utils/PluginsUtils"; import usersession from "../reducers/usersession"; import {saveUserSessionEpicCreator, autoSaveSessionEpicCreator, loadUserSessionEpicCreator, removeUserSessionEpicCreator, stopSaveSessionEpic, reloadOriginalConfigEpic, - setSessionToDynamicReducers} from "../epics/usersession"; + setSessionToDynamicReducers, + clearSessionIfPluginMissingEpic} from "../epics/usersession"; import Message from "../components/I18N/Message"; import {Glyphicon} from "react-bootstrap"; import {toggleControl} from "../actions/controls"; @@ -286,6 +287,6 @@ export default createPlugin('UserSession', { usersession }, epics: { - saveUserSessionEpic, autoSaveSessionEpic, stopSaveSessionEpic, loadUserSessionEpic, removeUserSessionEpic, reloadOriginalConfigEpic, setSessionToDynamicReducers + saveUserSessionEpic, autoSaveSessionEpic, stopSaveSessionEpic, loadUserSessionEpic, removeUserSessionEpic, reloadOriginalConfigEpic, setSessionToDynamicReducers, clearSessionIfPluginMissingEpic } });