Skip to content

Commit 240a7c4

Browse files
authored
Fix geosolutions-it#11826 - namespaced localstorage usage in core components (geosolutions-it#11874)
1 parent 3bfe32b commit 240a7c4

12 files changed

Lines changed: 162 additions & 40 deletions

File tree

web/client/api/userPersistedStorage/__tests__/index-test.js

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99
import expect from 'expect';
10-
import { setApi, getApi } from '..';
10+
import { setApi, getApi, getNameSpace, getItemKey } from '..';
1111

1212

1313
describe('UserPersistedSession', () => {
@@ -43,6 +43,50 @@ describe('UserPersistedSession', () => {
4343
done();
4444
}
4545
});
46+
47+
});
48+
describe('getItemKey and namespace', () => {
49+
it('test namespace creation', () => {
50+
expect(getNameSpace('http://hello.world/')).toEqual('');
51+
expect(getNameSpace('http://hello.world/index.html')).toEqual('');
52+
expect(getNameSpace('')).toEqual('');
53+
expect(getNameSpace('/')).toEqual('');
54+
expect(getNameSpace('https://hello.world/mapstore')).toEqual('');
55+
expect(getNameSpace('https://hello.world/mapstore/')).toEqual('');
56+
expect(getNameSpace('https://hello.world/mapstore/index.html')).toEqual('');
57+
expect(getNameSpace('https://hello.world/mapstore/embedded.html?something/#/complicated')).toEqual('');
58+
expect(getNameSpace('https://hello.world/MApStore/embedded.html?something/#/complicated')).toEqual('');
59+
expect(getNameSpace('/mapstore-ctx-1/embedded.html?something/#/complicated')).toEqual('mapstore-ctx-1');
60+
expect(getNameSpace('/app')).toEqual('app');
61+
expect(getNameSpace('/app/')).toEqual('app');
62+
expect(getNameSpace('/app/index.html')).toEqual('app');
63+
expect(getNameSpace('https://hello.world/app')).toEqual('app');
64+
expect(getNameSpace('https://hello.world/app/')).toEqual('app');
65+
expect(getNameSpace('/mapstore-ctx-2/')).toEqual('mapstore-ctx-2');
66+
expect(getNameSpace('https://hello.world/mapstore2/embedded.html?something/#/complicated')).toEqual('mapstore2');
67+
});
68+
it('test getItemKey', () => {
69+
expect(getItemKey('a', 'b')).toEqual('mapstore.a.b');
70+
expect(getItemKey('a.b', 'c')).toEqual('mapstore.a.b.c');
71+
expect(getItemKey('a', 'b', {ns: 'app'})).toEqual('app:mapstore.a.b');
72+
expect(getItemKey('a', 'b', {base: 'other'})).toEqual('other.a.b');
73+
expect(getItemKey('a', 'b', {ns: 'app', base: 'other'})).toEqual('app:other.a.b');
74+
// check MAPSTORE_STORAGE_NAMESPACE
75+
const original = window.MAPSTORE_STORAGE_NAMESPACE;
76+
window.MAPSTORE_STORAGE_NAMESPACE = 'fromGlobal';
77+
expect(getItemKey('a', 'b')).toEqual('fromGlobal:mapstore.a.b');
78+
window.MAPSTORE_STORAGE_NAMESPACE = undefined;
79+
expect(getItemKey('a', 'b')).toEqual('mapstore.a.b');
80+
// restore
81+
window.MAPSTORE_STORAGE_NAMESPACE = original;
82+
});
83+
it('getItemKey backward conpatibility', () => {
84+
// geostory tutorial
85+
expect(getItemKey("plugin.tutorial", "geostory.disabled")).toEqual("mapstore.plugin.tutorial.geostory.disabled");
86+
// tutorial
87+
const ID = "ID_ACT";
88+
expect(getItemKey("plugin.tutorial", ID + '.disabled')).toEqual('mapstore.plugin.tutorial.' + ID + '.disabled');
89+
});
4690
});
4791

4892
});

web/client/api/userPersistedStorage/index.js

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,51 @@ import get from 'lodash/get';
1111
import set from 'lodash/set';
1212
import unset from 'lodash/unset';
1313

14+
/**
15+
* This utility function extract a namespace from the application to
16+
* make a local persistence space for each application.
17+
* @param {string} url URL provider
18+
* @returns
19+
*/
20+
export const getNameSpace = (urlInput) => {
21+
try {
22+
// handle also relative URL
23+
const firstSegment = new URL(urlInput, "https://sample.com")?.pathname?.split('/')?.[1];
24+
25+
// clean path
26+
if (!firstSegment || firstSegment.toLowerCase() === 'mapstore' || firstSegment.endsWith('.html')) {
27+
return "";
28+
}
29+
return firstSegment;
30+
} catch (e) {
31+
console.error("Invalid URL", e);
32+
return null;
33+
}
34+
};
35+
/**
36+
* Creates a standardized key for the items to store in the storage API.
37+
* Usually the key is made this way `ns:base.section.fragment`.
38+
* When `config` 3rd parameter is not passed, the string will be typically `mapstore.section.fragment`.
39+
* Typical usage is `getApi().getItem(getItemKey('plugins.myPlugin', 'myOption'))`.
40+
* This will produce a key that is typically `mapstore.plugins.myPlugin.myOption`.
41+
* Depending on execution context `ns` can be different.
42+
* @param {string} section the first part of the string where store the data.
43+
* @param {string} fragment the last part of the key where to store the effective data.
44+
* @param {object} config an object with the parameters for the itemkey generation
45+
* @param {string} config.ns when defined is the namespace. When not passed as parameter, it is derived from the local environment base path, to provide different namespaces for different applications.
46+
* - If the application is deployed on `/` or `/mapstore/` the namespace is empty (so the `:` separator will not be included).
47+
* - if the application is deployed on `/app1` or `/app2` (different from mapstore), the namespace will become respectively `/app1` and `/app2`.
48+
* - if `window.MAPSTORE_STORAGE_NAMESPACE` is defined, the value of this variable will be used as namespace.
49+
* This is implemented this way to allow 2 different instances of mapstore deployed on the same domain to continue using separate namespaces.
50+
* @param {string} [config.base='mapstore'] the base of the key to generate
51+
* @returns {string}
52+
*/
53+
export const getItemKey = ( section, fragment, {ns, base = "mapstore"} = {}) => {
54+
// parse path to see if
55+
const NS = ns ?? window?.MAPSTORE_STORAGE_NAMESPACE ?? getNameSpace(window?.location?.href);
56+
return `${ NS ? NS + ":" : ""}${base}.${section}.${fragment}`;
57+
};
58+
1459
let UserPersistedSession = {
1560
};
1661
let ls;
@@ -37,7 +82,7 @@ const ApiProviders = {
3782
if (ApiProviders.memoryStorage.accessDenied) {
3883
throw Error("Cannot Access memoryStorage");
3984
}
40-
return get(MemoryStorage, path);
85+
return get(MemoryStorage, path) || null;
4186
},
4287
setItem: (path, value) => {
4388
if (ApiProviders.memoryStorage.accessDenied) {

web/client/components/cookie/Cookie.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { Glyphicon, Col } from 'react-bootstrap';
1212
import Button from '../misc/Button';
1313
import Message from '../../components/I18N/Message';
1414
import MoreDetails from './MoreDetails';
15-
import { getApi } from '../../api/userPersistedStorage';
15+
import { getApi, getItemKey } from '../../api/userPersistedStorage';
1616

1717
/**
1818
* Component used to show a panel with the information about cookies
@@ -131,7 +131,7 @@ class Cookie extends React.Component {
131131
}
132132
accept = () => {
133133
try {
134-
getApi().setItem("cookies-policy-approved", true);
134+
getApi().setItem(getItemKey('cookie', 'approved'), true);
135135
} catch (e) {
136136
console.error(e);
137137
}

web/client/epics/cookies.js

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,12 @@ import Rx from 'rxjs';
1212
import { SET_MORE_DETAILS_VISIBILITY, setCookieVisibility, setDetailsCookieHtml } from '../actions/cookie';
1313
import { CHANGE_LOCALE } from '../actions/locale';
1414
import axios from '../libs/ajax';
15-
import { getApi } from '../api/userPersistedStorage';
15+
import { getApi, getItemKey } from '../api/userPersistedStorage';
16+
17+
const checkCookiesPolicyApproved = () => {
18+
return getApi().getItem(getItemKey('cookie', 'approved'))
19+
|| getApi().getItem("cookies-policy-approved"); // legacy old item key
20+
};
1621

1722
/**
1823
* Show the cookie policy notification
@@ -21,13 +26,12 @@ import { getApi } from '../api/userPersistedStorage';
2126
* @memberof epics.cookies
2227
* @return {external:Observable} the steam of actions to trigger to display the noitification.
2328
*/
24-
2529
export const cookiePolicyChecker = (action$) =>
2630
action$.ofType(LOCATION_CHANGE )
2731
.take(1)
2832
.filter( () => {
2933
try {
30-
return !getApi().getItem("cookies-policy-approved");
34+
return !checkCookiesPolicyApproved();
3135
} catch (e) {
3236
console.error(e);
3337
return false;
@@ -39,7 +43,7 @@ export const cookiePolicyChecker = (action$) =>
3943

4044
export const loadCookieDetailsPage = (action$, store) =>
4145
action$.ofType(SET_MORE_DETAILS_VISIBILITY, CHANGE_LOCALE )
42-
.filter( () => !getApi().getItem("cookies-policy-approved") && store.getState().cookie.seeMore && !store.getState().cookie.html[store.getState().locale.current])
46+
.filter( () => !checkCookiesPolicyApproved() && store.getState().cookie.seeMore && !store.getState().cookie.html[store.getState().locale.current])
4347
.switchMap(() => Rx.Observable.fromPromise(
4448
axios.get("translations/fragments/cookie/cookieDetails-" + store.getState().locale.current + ".html", null, {
4549
timeout: 60000,

web/client/epics/tutorial.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ import { creationStepSelector } from '../selectors/contextcreator';
2525
import { CONTEXT_TUTORIALS } from '../actions/contextcreator';
2626
import { LOCATION_CHANGE } from 'connected-react-router';
2727
import { isEmpty, isArray, isObject } from 'lodash';
28-
import { getApi } from '../api/userPersistedStorage';
28+
import { getApi, getItemKey } from '../api/userPersistedStorage';
2929
import {REDUCERS_LOADED} from "../actions/storemanager";
3030
import { VISUALIZATION_MODE_CHANGED } from '../actions/maptype';
3131
import { detailsSettingsSelector } from '../selectors/details';
@@ -112,7 +112,7 @@ export const switchGeostoryTutorialEpic = (action$, store) =>
112112
const steps = !isEmpty(presetList) ? presetList[id + geostoryMode + '_tutorial'] : null;
113113
let isGeostoryTutorialDisabled = false;
114114
try {
115-
isGeostoryTutorialDisabled = getApi().getItem("mapstore.plugin.tutorial.geostory.disabled") === "true";
115+
isGeostoryTutorialDisabled = getApi().getItem(getItemKey("plugin.tutorial", "geostory.disabled")) === "true";
116116
} catch (e) {
117117
console.error(e);
118118
}

web/client/plugins/ResourcesCatalog/components/__tests__/FilterAccordion-test.jsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import expect from 'expect';
1313
import FilterAccordion from '../FilterAccordion';
1414
import { waitFor } from '@testing-library/react';
1515
import { Simulate } from 'react-dom/test-utils';
16+
import { removeValue } from '../../hooks/useLocalStorage';
1617

1718
describe('FilterAccordion component', () => {
1819
beforeEach((done) => {
@@ -22,7 +23,7 @@ describe('FilterAccordion component', () => {
2223
afterEach((done) => {
2324
ReactDOM.unmountComponentAtNode(document.getElementById('container'));
2425
document.body.innerHTML = '';
25-
localStorage.removeItem('accordionsExpanded');
26+
removeValue('accordionsExpanded');
2627
setTimeout(done);
2728
});
2829
it('should render with default', () => {

web/client/plugins/ResourcesCatalog/hooks/__tests__/useCardLayoutStyle-test.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@
88

99
import React from 'react';
1010
import ReactDOM from 'react-dom';
11-
import useCardLayoutStyle from '../useCardLayoutStyle';
11+
import useCardLayoutStyle, { STORAGE_FRAGMENT } from '../useCardLayoutStyle';
1212
import expect from 'expect';
1313
import { Simulate, act } from 'react-dom/test-utils';
14+
import { getApi, getItemKey } from '../../../../api/userPersistedStorage';
15+
import { removeValue, USE_LOCAL_STORAGE_SECTION } from '../useLocalStorage';
1416

1517
const Component = (props) => {
1618
const { cardLayoutStyle, setCardLayoutStyle, hideCardLayoutButton } = useCardLayoutStyle(props);
@@ -26,7 +28,7 @@ describe('useCardLayoutStyle', () => {
2628
afterEach((done) => {
2729
ReactDOM.unmountComponentAtNode(document.getElementById('container'));
2830
document.body.innerHTML = '';
29-
localStorage.removeItem('layoutCardsStyle');
31+
removeValue('layoutCardsStyle');
3032
setTimeout(done);
3133
});
3234
it('should store layoutCardsStyle in localStorage', () => {
@@ -37,7 +39,7 @@ describe('useCardLayoutStyle', () => {
3739
expect(button.innerHTML).toBe('grid-false');
3840
Simulate.click(button);
3941
expect(button.innerHTML).toBe('list-false');
40-
expect(localStorage.getItem('layoutCardsStyle')).toBe('"list"');
42+
expect(getApi().getItem(getItemKey(USE_LOCAL_STORAGE_SECTION, STORAGE_FRAGMENT))).toBe('"list"');
4143
});
4244
it('should force the value if cardLayoutStyle is passed', () => {
4345
act(() => {

web/client/plugins/ResourcesCatalog/hooks/__tests__/useLocalStorage-test.js

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@
1010
import React from 'react';
1111
import ReactDOM from 'react-dom';
1212
import expect from 'expect';
13-
import useLocalStorage from '../useLocalStorage';
13+
import useLocalStorage, { removeValue, USE_LOCAL_STORAGE_SECTION } from '../useLocalStorage';
1414
import { Simulate, act } from 'react-dom/test-utils';
15+
import { getApi, getItemKey } from '../../../../api/userPersistedStorage';
1516

1617
const VALUE_KEY = 'test';
1718

@@ -28,7 +29,7 @@ describe('useLocalStorage', () => {
2829
afterEach((done) => {
2930
ReactDOM.unmountComponentAtNode(document.getElementById("container"));
3031
document.body.innerHTML = '';
31-
localStorage.removeItem(VALUE_KEY);
32+
removeValue(VALUE_KEY);
3233
setTimeout(done);
3334
});
3435
it('should store new value in localStorage', () => {
@@ -39,11 +40,18 @@ describe('useLocalStorage', () => {
3940
);
4041
});
4142
let button = document.querySelector('button');
42-
expect(button.innerHTML).toBe('defaultValue');
43-
expect(localStorage.getItem(VALUE_KEY)).toBe(null);
44-
Simulate.click(button);
45-
expect(button.innerHTML).toBe('newValue');
46-
expect(localStorage.getItem(VALUE_KEY)).toBe('"newValue"');
43+
act(() => {
44+
expect(button.innerHTML).toBe('defaultValue');
45+
});
46+
expect(getApi().getItem(getItemKey(USE_LOCAL_STORAGE_SECTION, VALUE_KEY))).toBe(null);
47+
act(() => {
48+
Simulate.click(button);
49+
});
50+
act(() => {
51+
expect(button.innerHTML).toBe('newValue');
52+
expect(getApi().getItem(getItemKey(USE_LOCAL_STORAGE_SECTION, VALUE_KEY))).toBe('"newValue"');
53+
});
54+
4755
act(() => {
4856
ReactDOM.render(
4957
<div id="unmount"/>,
@@ -60,5 +68,7 @@ describe('useLocalStorage', () => {
6068
});
6169
button = document.querySelector('button');
6270
expect(button.innerHTML).toBe('newValue');
71+
// cleanup
72+
getApi().removeItem(getItemKey(USE_LOCAL_STORAGE_SECTION, VALUE_KEY));
6373
});
6474
});

web/client/plugins/ResourcesCatalog/hooks/useCardLayoutStyle.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
*/
88

99
import useLocalStorage from './useLocalStorage';
10-
10+
export const STORAGE_FRAGMENT = 'layoutCardsStyle';
1111
/**
1212
* menage the card layout using the localStorage
1313
* @param {string} props.cardLayoutStyle one of `list` or `grid`, if not `undefined` it forces the type of cards style
@@ -18,7 +18,7 @@ const useCardLayoutStyle = ({
1818
cardLayoutStyle,
1919
defaultCardLayoutStyle
2020
} = {}) => {
21-
const [_cardLayoutStyleState, setCardLayoutStyle] = useLocalStorage('layoutCardsStyle', defaultCardLayoutStyle);
21+
const [_cardLayoutStyleState, setCardLayoutStyle] = useLocalStorage(STORAGE_FRAGMENT, defaultCardLayoutStyle);
2222
const cardLayoutStyleState = cardLayoutStyle || _cardLayoutStyleState; // Force style when `cardLayoutStyle` is configured
2323
const hideCardLayoutButton = !!cardLayoutStyle;
2424
return {

web/client/plugins/ResourcesCatalog/hooks/useLocalStorage.js

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,27 @@
77
*/
88

99
import { useState } from 'react';
10+
import { getApi, getItemKey } from '../../../api/userPersistedStorage';
1011

12+
export const USE_LOCAL_STORAGE_SECTION = 'useLocalStorageHook';
13+
export const removeValue = (key) => {
14+
getApi().removeItem(getItemKey(USE_LOCAL_STORAGE_SECTION, key));
15+
};
1116
const getValue = (key, defaultValue) => {
1217
if (typeof window === 'undefined') {
1318
return defaultValue;
1419
}
1520
try {
16-
const item = window.localStorage.getItem(key);
21+
const item = getApi().getItem(getItemKey(USE_LOCAL_STORAGE_SECTION, key));
1722
return item ? JSON.parse(item) : defaultValue;
1823
} catch (error) {
1924
return defaultValue;
2025
}
2126
};
2227

23-
const setValue = (key, value) => {
28+
const saveValue = (key, value) => {
2429
try {
25-
window.localStorage.setItem(key, JSON.stringify(value));
30+
getApi().setItem(getItemKey(USE_LOCAL_STORAGE_SECTION, key), JSON.stringify(value));
2631
} catch (error) {
2732
//
2833
}
@@ -39,13 +44,14 @@ const setValue = (key, value) => {
3944
* }
4045
*/
4146
const useLocalStorage = (key, defaultValue) => {
42-
const [storedValue, setStoredValue] = useState(getValue(key, defaultValue));
43-
const [prevStoredValue, setPrevStoredValue] = useState(storedValue);
44-
if (storedValue !== prevStoredValue) {
45-
setPrevStoredValue(storedValue);
46-
setValue(key, storedValue);
47-
}
48-
return [storedValue, setStoredValue];
47+
const [storedValue, setStoredValue] = useState(() => getValue(key, defaultValue));
48+
49+
const setValue = (value) => {
50+
setStoredValue(value);
51+
saveValue(key, value);
52+
};
53+
54+
return [storedValue, setValue];
4955
};
5056

5157
export default useLocalStorage;

0 commit comments

Comments
 (0)