Repository: geosolutions-it/MapStore2
Pull request #10955: #10936: Add resource name in the window title of the browser
Author: login: mahmoudadel54
Labels: name: enhancement
name: BackportNeeded
description: Commits provided for an issue need to be backported to the milestone's stable branch
Created: 2025-03-21T14:39:19Z | Merged: 2025-03-31T16:53:55Z

## Description
<!-- A few sentences describing the overall goals of the pull request' s commits. -->
This PR includes handling edit document title in case open different stored resources: maps, dashboards, geostories to be equal the resource name and handling restore document title while leaving the resource [map, dashboard, geostory] page. This is for the stored resources with id.

**Please check if the PR fulfills these requirements**
- [x] The commit message follows our guidelines: https://github.com/geosolutions-it/MapStore2/blob/master/CONTRIBUTING.md
- [x] Tests for the changes have been added (for bug fixes / features)
- [ ] Docs have been added / updated (for bug fixes / features)


**What kind of change does this PR introduce?** (check one with "x", remove the others)
 - [ ] Bugfix
 - [ ] Feature
 - [ ] Code style update (formatting, local variables)
 - [ ] Refactoring (no functional changes, no api changes)
 - [ ] Build related changes
 - [ ] CI related changes
 - [x] Other... Please describe: Enhancement

<!-- add here the ReadTheDocs link (if needed) -->

## Issue

**What is the current behavior?**
<!-- You can also link to an existing issue here -->
#10936 

**What is the new behavior?**
<!-- Describe here the new behaviour based on your changes -->

https://github.com/user-attachments/assets/ba2b0870-422e-4784-bbef-f4ce8fa22b2d



## Breaking change
**Does this PR introduce a breaking change?** (check one with "x", remove the other)
 - [ ] Yes, and I documented them in migration notes
 - [x] No

<!-- If this PR contains a breaking change, please describe the impact and migration path for existing applications -->

## Other useful information



Diff:
diff --git a/web/client/plugins/Dashboard.jsx b/web/client/plugins/Dashboard.jsx
index 61f0db925fe..16ead04bb76 100644
--- a/web/client/plugins/Dashboard.jsx
+++ b/web/client/plugins/Dashboard.jsx
@@ -31,7 +31,8 @@ import {
     isBrowserMobile,
     isDashboardLoading,
     showConnectionsSelector,
-    isDashboardAvailable
+    isDashboardAvailable,
+    dashboardTitleSelector
 } from '../selectors/dashboard';
 import { currentLocaleLanguageSelector, currentLocaleSelector } from '../selectors/locale';
 import { isLocalizedLayerStylesEnabledSelector, localizedLayerStylesEnvSelector } from '../selectors/localizedLayerStyles';
@@ -158,13 +159,32 @@ class DashboardPlugin extends React.Component {
         cols: PropTypes.object,
         minLayoutWidth: PropTypes.number,
         widgetOpts: PropTypes.object,
-        enableZoomInTblWidget: PropTypes.bool
+        enableZoomInTblWidget: PropTypes.bool,
+        dashboardTitle: PropTypes.string
     };
     static defaultProps = {
         enabled: true,
         minLayoutWidth: 480,
         enableZoomInTblWidget: true
     };
+    componentDidMount() {
+        let isExistingDashbaordResource = this.props?.did;
+        if (isExistingDashbaordResource) {
+            this.oldDocumentTitle = document.title;
+        }
+    }
+    componentDidUpdate() {
+        let isExistingDashbaordResource = this.props?.did;
+        if (this.props.dashboardTitle && isExistingDashbaordResource) {
+            document.title = this.props.dashboardTitle;
+        }
+    }
+    componentWillUnmount() {
+        let isExistingDashbaordResource = this.props?.did;
+        if (isExistingDashbaordResource) {
+            document.title = this.oldDocumentTitle;
+        }
+    }
     render() {
         return this.props.enabled
             ? <WidgetsView
@@ -182,7 +202,7 @@ class DashboardPlugin extends React.Component {
 }
 
 export default createPlugin("Dashboard", {
-    component: withResizeDetector(DashboardPlugin),
+    component: connect((state) => ({dashboardTitle: dashboardTitleSelector(state)}))(withResizeDetector(DashboardPlugin)),
     reducers: {
         dashboard: dashboardReducers,
         widgets: widgetsReducers
diff --git a/web/client/plugins/GeoStory.jsx b/web/client/plugins/GeoStory.jsx
index 7df356f398c..4443aae9a6c 100644
--- a/web/client/plugins/GeoStory.jsx
+++ b/web/client/plugins/GeoStory.jsx
@@ -6,7 +6,7 @@
  * LICENSE file in the root directory of this source tree.
  */
 
-import React, { useEffect, useCallback } from 'react';
+import React, { useEffect, useCallback, useRef } from 'react';
 import { connect } from 'react-redux';
 import { createStructuredSelector } from 'reselect';
 import WebFont from 'webfontloader';
@@ -35,7 +35,8 @@ import {
     isFocusOnContentSelector,
     settingsSelector,
     currentStoryFonts,
-    isDrawControlEnabled
+    isDrawControlEnabled,
+    geostoryTitleSelector
 } from '../selectors/geostory';
 import { currentMessagesSelector } from '../selectors/locale';
 import geostory from '../reducers/geostory';
@@ -57,8 +58,10 @@ const GeoStory = ({
     onBasicError = () => {},
     onUpdateMediaEditorSetting,
     mediaEditorSettings,
+    geostoryTitle,
     ...props
 }) => {
+    const oldDocumentTitle = useRef();
     const localize = useCallback((id) => getMessageById(messages, id), [messages]);
     const addFunc = (path, position, element, id) => onAdd(path, position, element, id ? localize(id) : localize);
 
@@ -93,6 +96,21 @@ const GeoStory = ({
         }
     }, [ storyFonts ]);
 
+    // for update document title by geostoryTitle
+    useEffect(() => {
+        let isExistingGeostoryResource = props?.gid;
+        if (!oldDocumentTitle.current) {
+            oldDocumentTitle.current = document.title;
+        }
+        if (geostoryTitle && isExistingGeostoryResource) {
+            document.title = geostoryTitle;
+        }
+        return () => {
+            if (isExistingGeostoryResource) {
+                document.title = oldDocumentTitle.current;
+            }
+        };
+    }, [geostoryTitle]);
     return (<BorderLayout
         className="ms-geostory"
         columns={[<MapEditor {...props} buttonItems={props.items?.filter(item => item.target === 'mapEditorToolbar')} add={addFunc} update={onUpdate} mode={mode} hideIdentifyOptions={props.hideIdentifyOptions} />]}>
@@ -205,7 +223,8 @@ export default createPlugin("GeoStory", {
             isContentFocused: isFocusOnContentSelector,
             theme: storyThemeSelector,
             storyFonts: currentStoryFonts,
-            isDrawEnabled: isDrawControlEnabled
+            isDrawEnabled: isDrawControlEnabled,
+            geostoryTitle: geostoryTitleSelector
         }), {
             onAdd: add,
             onUpdate: update,
diff --git a/web/client/plugins/Map.jsx b/web/client/plugins/Map.jsx
index d66ecd7e274..62a25466a55 100644
--- a/web/client/plugins/Map.jsx
+++ b/web/client/plugins/Map.jsx
@@ -211,7 +211,8 @@ class MapPlugin extends React.Component {
         items: PropTypes.array,
         onLoadingMapPlugins: PropTypes.func,
         onMapTypeLoaded: PropTypes.func,
-        pluginsCreator: PropTypes.func
+        pluginsCreator: PropTypes.func,
+        mapTitle: PropTypes.string
     };
 
     static defaultProps = {
@@ -253,7 +254,18 @@ class MapPlugin extends React.Component {
     };
 
     state = {};
-
+    componentDidMount() {
+        let isMapResource = this.props?.mapId;
+        if (isMapResource) {
+            this.oldDocumentTitle = document.title;
+        }
+    }
+    componentDidUpdate() {
+        let isMapResource = this.props?.mapId;
+        if (this.props.mapTitle && isMapResource) {
+            document.title = this.props.mapTitle;
+        }
+    }
     UNSAFE_componentWillMount() {
         // moved the font load of FontAwesome only to styleParseUtils (#9653)
         this.updatePlugins(this.props);
@@ -268,6 +280,10 @@ class MapPlugin extends React.Component {
 
     componentWillUnmount() {
         this._isMounted = false;
+        let isMapResource = this.props?.mapId;
+        if (isMapResource) {
+            document.title = this.oldDocumentTitle;
+        }
     }
 
     getHighlightLayer = (projection, index, env) => {
diff --git a/web/client/plugins/map/__tests__/selector-test.jsx b/web/client/plugins/map/__tests__/selector-test.jsx
index c21653db541..aaa135c2c52 100644
--- a/web/client/plugins/map/__tests__/selector-test.jsx
+++ b/web/client/plugins/map/__tests__/selector-test.jsx
@@ -18,6 +18,17 @@ import { registerEventListener } from '../../../actions/map';
 
 const stateMocker = createStateMocker({ map, layers, additionallayers});
 describe('Map plugin state to pros selector', () => {
+    it('test getting mapTitle from selector', () => {
+        const stateWithMapTitle = selector({map: {
+            info: {
+                attributes: {
+                    "title": "map title01"
+                }
+            }
+        }});
+        expect(stateWithMapTitle.mapTitle).toBeTruthy();
+        expect(stateWithMapTitle.mapTitle).toEqual("map title01");
+    });
     describe('elevationEnabled', () => {
         it('elevation is active when mouseposition is listening to map', () => {
             const elevationEnabledState = selector(stateMocker(registerEventListener('mousemove', 'mouseposition')));
diff --git a/web/client/plugins/map/selector.js b/web/client/plugins/map/selector.js
index 585c125c528..235cbb76d78 100644
--- a/web/client/plugins/map/selector.js
+++ b/web/client/plugins/map/selector.js
@@ -1,4 +1,4 @@
-import { mapSelector, projectionDefsSelector, isMouseMoveCoordinatesActiveSelector } from '../../selectors/map';
+import { mapSelector, projectionDefsSelector, isMouseMoveCoordinatesActiveSelector, mapNameSelector } from '../../selectors/map';
 import { mapTypeSelector } from '../../selectors/maptype';
 import { layerSelectorWithMarkers } from '../../selectors/layers';
 import { highlighedFeatures } from '../../selectors/highlight';
@@ -24,7 +24,8 @@ export default createShallowSelectorCreator(isEqual)(
     isLocalizedLayerStylesEnabledSelector,
     localizedLayerStylesNameSelector,
     currentLocaleLanguageSelector,
-    (projectionDefs, map, mapType, layers, features, loadingError, securityToken, elevationEnabled, isLocalizedLayerStylesEnabled, localizedLayerStylesName, currentLocaleLanguage) => ({
+    mapNameSelector,
+    (projectionDefs, map, mapType, layers, features, loadingError, securityToken, elevationEnabled, isLocalizedLayerStylesEnabled, localizedLayerStylesName, currentLocaleLanguage, mapTitle) => ({
         projectionDefs,
         map,
         mapType,
@@ -35,6 +36,7 @@ export default createShallowSelectorCreator(isEqual)(
         elevationEnabled,
         isLocalizedLayerStylesEnabled,
         localizedLayerStylesName,
-        currentLocaleLanguage
+        currentLocaleLanguage,
+        mapTitle
     })
 );
diff --git a/web/client/product/pages/Context.jsx b/web/client/product/pages/Context.jsx
index c1f29f6c7ac..25ae920b80a 100644
--- a/web/client/product/pages/Context.jsx
+++ b/web/client/product/pages/Context.jsx
@@ -94,17 +94,22 @@ class Context extends React.Component {
     componentDidUpdate(oldProps) {
         const paramsChanged = !isEqual(this.props.match.params, oldProps.match.params);
         const newParams = this.props.match.params;
+        const isMapContext = newParams?.mapId;
 
         if (paramsChanged && this.state.pluginsAreLoaded) {
             this.props.loadContext(newParams);
         }
 
-        if (this.props.windowTitle) {
+        if (this.props.windowTitle && !isMapContext) {
             document.title = this.props.windowTitle;
         }
     }
     componentWillUnmount() {
-        document.title = this.oldTitle;
+        const params = this.props.match.params;
+        const isMapContext = params?.mapId;
+        if (!isMapContext) {
+            document.title = this.oldTitle;
+        }
         this.props.reset();
     }
     render() {
@@ -120,7 +125,10 @@ class Context extends React.Component {
         if (pluginsAreLoaded && !this.state.pluginsAreLoaded) {
             this.setState({pluginsAreLoaded: true}, () => {
                 const params = this.props.match.params;
-                this.oldTitle = document.title;
+                const isMapContext = params?.mapId;
+                if (!isMapContext) {
+                    this.oldTitle = document.title;
+                }
                 this.props.loadContext(params);
             });
         }
diff --git a/web/client/selectors/__tests__/dashboard-test.js b/web/client/selectors/__tests__/dashboard-test.js
index 45b1ff2e509..13d56a410e5 100644
--- a/web/client/selectors/__tests__/dashboard-test.js
+++ b/web/client/selectors/__tests__/dashboard-test.js
@@ -26,7 +26,8 @@ import {
     dashboardResourceInfoSelector,
     dashboardInfoDetailsUriFromIdSelector,
     dashboardInfoDetailsSettingsFromIdSelector,
-    canEditServiceSelector
+    canEditServiceSelector,
+    dashboardTitleSelector
 } from '../dashboard';
 
 describe('dashboard selectors', () => {
@@ -157,6 +158,13 @@ describe('dashboard selectors', () => {
             }
         }})).toBe("detailsSettings");
     });
+    it("test dashboardTitleSelector", () => {
+        expect(dashboardTitleSelector({dashboard: {
+            resource: {
+                name: "dashbaord title1"
+            }
+        }})).toBe("dashbaord title1");
+    });
     describe("canEditServiceSelector", () => {
         it('test ADMIN role ', () => {
             const canEdit = canEditServiceSelector({
diff --git a/web/client/selectors/__tests__/geostory-test.js b/web/client/selectors/__tests__/geostory-test.js
index 27b4ff5bf7b..bf55f915d5c 100644
--- a/web/client/selectors/__tests__/geostory-test.js
+++ b/web/client/selectors/__tests__/geostory-test.js
@@ -39,7 +39,8 @@ import {
     getAllCarouselContentsOfSection,
     isDrawControlEnabled,
     geoCarouselSettings,
-    getPageIndex
+    getPageIndex,
+    geostoryTitleSelector
 } from "../geostory";
 import TEST_STORY from "../../test-resources/geostory/sampleStory_1.json";
 
@@ -191,4 +192,7 @@ describe('geostory selectors', () => { // TODO: check default
         const settings = geoCarouselSettings({ geostory: { geoCarouselSettings: _settings }});
         expect(settings).toBe(_settings);
     });
+    it('geostoryTitleSelector ', () => {
+        expect(geostoryTitleSelector({ geostory: { resource: { name: "geostory title1" } } })).toBe("geostory title1");
+    });
 });
diff --git a/web/client/selectors/dashboard.js b/web/client/selectors/dashboard.js
index 6d5cc0a092f..31f419332c5 100644
--- a/web/client/selectors/dashboard.js
+++ b/web/client/selectors/dashboard.js
@@ -44,4 +44,10 @@ export const canEditServiceSelector = state => {
         allowedGroups
     })(state);
 };
+/**
+ * Get name/title of current dashboard
+ * @param {object} state the application state
+ * @returns {string} name/title of the dashboard
+ */
+export const dashboardTitleSelector = state => state?.dashboard?.resource?.name;
 
diff --git a/web/client/selectors/geostory.js b/web/client/selectors/geostory.js
index cc9af6b84cf..545169421b7 100644
--- a/web/client/selectors/geostory.js
+++ b/web/client/selectors/geostory.js
@@ -72,6 +72,12 @@ export const canEditSelector = state => get(resourceSelector(state), 'canEdit',
  * @param {object} state the application state
  */
 export const geostoryIdSelector = state => get(resourceSelector(state), 'id');
+/**
+ * Get name/title of current story
+ * @param {object} state the application state
+ * @returns {string} name/title of the geostory
+ */
+export const geostoryTitleSelector = state => get(resourceSelector(state), 'name');
 /**
  * Selects the edit permission of the resource
  * @param {object} state the application state


Reviews:
user: login: MV88
body: There is something wrong when you open a map associated to a context
like https://dev-mapstore.geosolutionsgroup.com/mapstore/#/context/demo_context/46500 

Before 
  - the window title it was showing `<context name>, `

Now
  - it shows `<context name>` and then it gets replaces by `<map name>`
  - also when now you go back to homepage the title is not restored to MapStore, but it is restored to context name. 

Probably when opening a map from a context like before is better to either show only map title or a concatenation of both titles with a - as separator

@tdipisa do you agree?
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-24T10:44:54Z
user: login: tdipisa
body: @mahmoudadel54 (FYI @MV88)

>it shows <context name> and then it gets replaces by <map name>

This is not a good behavior, let's avoid on the fly replacements.
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-03-24T15:51:40Z
user: login: mahmoudadel54
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T03:15:26Z
user: login: mahmoudadel54
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T03:24:25Z
user: login: MV88
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-25T07:42:14Z
user: login: MV88
body: LGTM 
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-31T16:47:54Z
user: login: tdipisa
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-31T16:49:38Z

Review comments:
diff_hunk: @@ -44,4 +44,5 @@ export const canEditServiceSelector = state => {
         allowedGroups
     })(state);
 };
+export const dashboardTitleSelector = state => state?.dashboard?.resource?.name;
path: web/client/selectors/dashboard.js
user: login: MV88
body: jsdoc
created_at: 2025-03-24T10:06:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -72,6 +72,11 @@ export const canEditSelector = state => get(resourceSelector(state), 'canEdit',
  * @param {object} state the application state
  */
 export const geostoryIdSelector = state => get(resourceSelector(state), 'id');
+/**
+ * Get name/title of current story
+ * @param {object} state the application state
path: web/client/selectors/geostory.js
user: login: MV88
body: is this an object or a string?
created_at: 2025-03-24T10:07:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -24,7 +24,8 @@ export default createShallowSelectorCreator(isEqual)(
     isLocalizedLayerStylesEnabledSelector,
     localizedLayerStylesNameSelector,
     currentLocaleLanguageSelector,
-    (projectionDefs, map, mapType, layers, features, loadingError, securityToken, elevationEnabled, isLocalizedLayerStylesEnabled, localizedLayerStylesName, currentLocaleLanguage) => ({
+    mapNameSelector,
path: web/client/plugins/map/selector.js
user: login: MV88
body: no tests for this ?
created_at: 2025-03-24T10:07:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -72,6 +72,11 @@ export const canEditSelector = state => get(resourceSelector(state), 'canEdit',
  * @param {object} state the application state
  */
 export const geostoryIdSelector = state => get(resourceSelector(state), 'id');
+/**
+ * Get name/title of current story
+ * @param {object} state the application state
path: web/client/selectors/geostory.js
user: login: mahmoudadel54
body: state is object and return is string, I have updated with return
created_at: 2025-03-25T03:15:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -24,7 +24,8 @@ export default createShallowSelectorCreator(isEqual)(
     isLocalizedLayerStylesEnabledSelector,
     localizedLayerStylesNameSelector,
     currentLocaleLanguageSelector,
-    (projectionDefs, map, mapType, layers, features, loadingError, securityToken, elevationEnabled, isLocalizedLayerStylesEnabled, localizedLayerStylesName, currentLocaleLanguage) => ({
+    mapNameSelector,
path: web/client/plugins/map/selector.js
user: login: mahmoudadel54
body: test for what exactly ? there is already a unit test for 'mapNameSelector' here: web\client\plugins\map\__tests__\selector-test.jsx

created_at: 2025-03-25T03:24:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -24,7 +24,8 @@ export default createShallowSelectorCreator(isEqual)(
     isLocalizedLayerStylesEnabledSelector,
     localizedLayerStylesNameSelector,
     currentLocaleLanguageSelector,
-    (projectionDefs, map, mapType, layers, features, loadingError, securityToken, elevationEnabled, isLocalizedLayerStylesEnabled, localizedLayerStylesName, currentLocaleLanguage) => ({
+    mapNameSelector,
path: web/client/plugins/map/selector.js
user: login: MV88
body: for createShallowSelectorCreator that you have extended
created_at: 2025-03-25T07:42:14Z
author_association: CONTRIBUTOR

PR comments:
user: login: MV88
created_at: 2025-03-24T15:56:13Z
body: > @mahmoudadel54 (FYI @MV88)
> 
> > it shows  and then it gets replaces by
> 
> This is not a good behavior, let's avoid on the fly replacements.

ok @mahmoudadel54  please

- show directly map name instead of passing by the context name in case of a map opened and based on a context?
- when opening context resources it will show resource name i.e. context name
author_association: CONTRIBUTOR
user: login: MV88
created_at: 2025-03-31T16:54:04Z
body: @ElenaGallo please test it in DEV
author_association: CONTRIBUTOR