diff --git a/web/client/actions/__tests__/featuregrid-test.js b/web/client/actions/__tests__/featuregrid-test.js
index 995a17cf300..c54e54a1880 100644
--- a/web/client/actions/__tests__/featuregrid-test.js
+++ b/web/client/actions/__tests__/featuregrid-test.js
@@ -71,8 +71,6 @@ import {
OPEN_ADVANCED_SEARCH,
initPlugin,
INIT_PLUGIN,
- sizeChange,
- SIZE_CHANGE,
START_SYNC_WMS,
startSyncWMS,
storeAdvancedSearchFilter,
@@ -310,15 +308,6 @@ describe('Test correctness of featurgrid actions', () => {
expect(retval.type).toBe(UPDATE_FILTER);
expect(retval.update).toBe(update);
});
- it('Test sizeChange', () => {
- const size = 0.5;
- const dockProps = {maxDockSize: 0.7, minDockSize: 0.1};
- const retval = sizeChange(size, dockProps);
- expect(retval).toExist();
- expect(retval.type).toBe(SIZE_CHANGE);
- expect(retval.size).toBe(size);
- expect(retval.dockProps).toEqual(dockProps);
- });
it('Test storeAdvancedSearchFilter', () => {
const filterObj = {name: "A"};
const retval = storeAdvancedSearchFilter(filterObj);
diff --git a/web/client/actions/featuregrid.js b/web/client/actions/featuregrid.js
index a90803c6bd2..c1645bb2cf0 100644
--- a/web/client/actions/featuregrid.js
+++ b/web/client/actions/featuregrid.js
@@ -32,7 +32,6 @@ export const SET_LAYER = 'FEATUREGRID:SET_LAYER';
export const UPDATE_FILTER = 'QUERY:UPDATE_FILTER';
export const CHANGE_PAGE = 'FEATUREGRID:CHANGE_PAGE';
export const GEOMETRY_CHANGED = 'FEATUREGRID:GEOMETRY_CHANGED';
-export const DOCK_SIZE_FEATURES = 'DOCK_SIZE_FEATURES';
export const TOGGLE_TOOL = 'FEATUREGRID:TOGGLE_TOOL';
export const CUSTOMIZE_ATTRIBUTE = 'FEATUREGRID:CUSTOMIZE_ATTRIBUTE';
export const CLOSE_FEATURE_GRID_CONFIRM = 'ASK_CLOSE_FEATURE_GRID_CONFIRM';
@@ -47,7 +46,6 @@ export const DEACTIVATE_GEOMETRY_FILTER = 'FEATUREGRID:DEACTIVATE_GEOMETRY_FILTE
export const OPEN_ADVANCED_SEARCH = 'FEATUREGRID:ADVANCED_SEARCH';
export const ZOOM_ALL = 'FEATUREGRID:ZOOM_ALL';
export const INIT_PLUGIN = 'FEATUREGRID:INIT_PLUGIN';
-export const SIZE_CHANGE = 'FEATUREGRID:SIZE_CHANGE';
export const TOGGLE_SHOW_AGAIN_FLAG = 'FEATUREGRID:TOGGLE_SHOW_AGAIN_FLAG';
export const UPDATE_EDITORS_OPTIONS = 'FEATUREGRID:UPDATE_EDITORS_OPTIONS';
export const LAUNCH_UPDATE_FILTER_FUNC = 'FEATUREGRID:LAUNCH_UPDATE_FILTER_FUNC';
@@ -194,12 +192,6 @@ export function setFeatures(features) {
};
}
-export function dockSizeFeatures(dockSize) {
- return {
- type: DOCK_SIZE_FEATURES,
- dockSize: dockSize
- };
-}
export function sort(sortBy, sortOrder) {
return {
type: SORT_BY,
@@ -346,13 +338,6 @@ export function startSyncWMS() {
type: START_SYNC_WMS
};
}
-export function sizeChange(size, dockProps) {
- return {
- type: SIZE_CHANGE,
- size,
- dockProps
- };
-}
export const moreFeatures = (pages) => {
return {
type: LOAD_MORE_FEATURES,
diff --git a/web/client/components/data/featuregrid/FeatureEditorFallback.jsx b/web/client/components/data/featuregrid/FeatureEditorFallback.jsx
index 146ac5f7d42..d21e941739b 100644
--- a/web/client/components/data/featuregrid/FeatureEditorFallback.jsx
+++ b/web/client/components/data/featuregrid/FeatureEditorFallback.jsx
@@ -6,27 +6,15 @@
* LICENSE file in the root directory of this source tree.
*/
-import React, { useMemo } from 'react';
-import { connect } from 'react-redux';
-import { createSelector } from 'reselect';
-import { mapLayoutValuesSelector } from '../../../selectors/maplayout';
+import React from 'react';
import Loader from '../../misc/Loader';
-const FeatureEditorFallback = ({ size, dockStyle }) => {
- const containerStyle = useMemo(() => {
- return { height: `${size * 100}%`, ...dockStyle };
- }, [size, dockStyle]);
+const FeatureEditorFallback = () => {
return (
-
+
);
};
-export default connect(createSelector(
- state => state?.featuregrid?.dockSize,
- state => mapLayoutValuesSelector(state, { transform: true }),
- (size, dockStyle) => ({
- size,
- dockStyle
- })))(FeatureEditorFallback);
+export default FeatureEditorFallback;
diff --git a/web/client/components/data/featuregrid/toolbars/Toolbar.jsx b/web/client/components/data/featuregrid/toolbars/Toolbar.jsx
index a9571203f14..37fe00ab3b9 100644
--- a/web/client/components/data/featuregrid/toolbars/Toolbar.jsx
+++ b/web/client/components/data/featuregrid/toolbars/Toolbar.jsx
@@ -128,7 +128,7 @@ const standardButtons = {
visible={selectedCount <= 1 && mode === "VIEW"}
onClick={events.settings}
glyph="features-grid-set"/>),
- syncGridFilterToMap: ({disabled, isSyncActive = false, showSyncOnMapButton = true, events = {}, syncPopover = { dockSize: "32.2%" }, showPopoverSync, hideSyncPopover}) => (
(
- ,
- style: {
- bottom: syncPopover.dockSize
- }
+
}}
} />),
syncTimeParameter: ({timeSync, showTimeSyncButton = false, events = {}}) => ( {
+ const contentResizeRef = useResizeObserver({
+ onResize,
+ watch: ['bottom']
+ });
+ return (
+
+ {header}
+
+ {background}
+ {top}
+
+ {leftColumn}
+
+ {children}
+
+ {rightColumn}
+ {columns}
+
+ {bottom}
+
+ {footer}
+
+ );
+};
+
+export default MapViewerLayout;
diff --git a/web/client/components/layout/__tests__/MapViewerLayout-test.jsx b/web/client/components/layout/__tests__/MapViewerLayout-test.jsx
new file mode 100644
index 00000000000..1119cead730
--- /dev/null
+++ b/web/client/components/layout/__tests__/MapViewerLayout-test.jsx
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2026, GeoSolutions Sas.
+ * All rights reserved.
+ *
+ * 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 from 'react';
+
+import expect from 'expect';
+import ReactDOM from 'react-dom';
+import MapViewerLayout from '../MapViewerLayout';
+
+describe("Test MapViewerLayout Component", () => {
+ beforeEach((done) => {
+ document.body.innerHTML = '';
+ setTimeout(done);
+ });
+ afterEach((done) => {
+ ReactDOM.unmountComponentAtNode(document.getElementById("container"));
+ document.body.innerHTML = '';
+ setTimeout(done);
+ });
+
+ it('renders with basic props', () => {
+ ReactDOM.render(
+
+
+ ,
+ document.getElementById("container")
+ );
+
+ expect(document.getElementById('MAPVIEWER')).toExist();
+ expect(document.getElementsByClassName('MAP_CLASS')[0]).toExist();
+ expect(document.getElementsByClassName('ms-map-viewer-layout-body')[0]).toExist();
+ expect(document.getElementsByClassName('ms-map-viewer-layout-content')[0]).toExist();
+ expect(document.getElementsByClassName('content')[0]).toExist();
+ });
+
+ it('renders header and footer', () => {
+ ReactDOM.render(
+ }
+ footer={}
+ >
+
+ ,
+ document.getElementById("container")
+ );
+
+ expect(document.getElementsByClassName('header')[0]).toExist();
+ expect(document.getElementsByClassName('footer')[0]).toExist();
+ expect(document.getElementsByClassName('content')[0]).toExist();
+ });
+
+ it('renders background, top and bottom containers', () => {
+ ReactDOM.render(
+ }
+ top={}
+ bottom={}
+ >
+
+ ,
+ document.getElementById("container")
+ );
+
+ // background is inside a _fill _absolute container
+ expect(document.getElementsByClassName('background')[0]).toExist();
+ // top and bottom containers
+ expect(document.getElementsByClassName('top')[0]).toExist();
+ expect(document.getElementsByClassName('bottom')[0]).toExist();
+ });
+
+ it('renders left and right columns', () => {
+ ReactDOM.render(
+ }
+ rightColumn={}
+ >
+
+ ,
+ document.getElementById("container")
+ );
+
+ expect(document.getElementsByClassName('ms-map-viewer-layout-left-column')[0]).toExist();
+ expect(document.getElementsByClassName('left-column')[0]).toExist();
+ expect(document.getElementsByClassName('ms-map-viewer-layout-right-column')[0]).toExist();
+ expect(document.getElementsByClassName('right-column')[0]).toExist();
+ });
+
+ it('renders additional columns container', () => {
+ ReactDOM.render(
+ ,
+
+ ]}
+ >
+
+ ,
+ document.getElementById("container")
+ );
+
+ expect(document.getElementsByClassName('ms-map-viewer-layout-columns')[0]).toExist();
+ expect(document.getElementsByClassName('extra-col-1')[0]).toExist();
+ expect(document.getElementsByClassName('extra-col-2')[0]).toExist();
+ });
+});
+
diff --git a/web/client/components/layout/__tests__/hooks/useResizeObserver-test.jsx b/web/client/components/layout/__tests__/hooks/useResizeObserver-test.jsx
new file mode 100644
index 00000000000..ed815778104
--- /dev/null
+++ b/web/client/components/layout/__tests__/hooks/useResizeObserver-test.jsx
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2026, GeoSolutions Sas.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+import expect from 'expect';
+import React from 'react';
+import ReactDOM from 'react-dom';
+import { act } from 'react-dom/test-utils';
+import useResizeObserver from '../../hooks/useResizeObserver';
+
+describe('useResizeObserver', () => {
+ beforeEach((done) => {
+ document.body.innerHTML = '';
+ setTimeout(done);
+ });
+
+ afterEach((done) => {
+ ReactDOM.unmountComponentAtNode(document.getElementById("container"));
+ document.body.innerHTML = '';
+ setTimeout(done);
+ });
+
+ it('should observe element and call onResize when size changes', (done) => {
+ const onResizeSpy = expect.createSpy();
+
+ const Component = () => {
+ const elementRef = useResizeObserver({
+ onResize: onResizeSpy
+ });
+
+ return Test
;
+ };
+
+ act(() => {
+ ReactDOM.render(, document.getElementById('container'));
+ });
+
+ // Wait for ResizeObserver to trigger and debounce
+ setTimeout(() => {
+ expect(onResizeSpy).toHaveBeenCalled();
+ done();
+ }, 200);
+ });
+
+ it('should debounce resize callbacks', (done) => {
+ const onResizeSpy = expect.createSpy();
+
+ const Component = () => {
+ const elementRef = useResizeObserver({
+ onResize: onResizeSpy,
+ debounceTime: 200
+ });
+
+ return Test
;
+ };
+
+ act(() => {
+ ReactDOM.render(, document.getElementById('container'));
+ });
+
+ // Wait for debounce
+ setTimeout(() => {
+ expect(onResizeSpy).toHaveBeenCalled();
+ done();
+ }, 250);
+ });
+
+ it('should disconnect observer on unmount', () => {
+ const Component = () => {
+ const elementRef = useResizeObserver({
+ onResize: () => {}
+ });
+
+ return Test
;
+ };
+
+ act(() => {
+ ReactDOM.render(, document.getElementById('container'));
+ });
+
+ act(() => {
+ ReactDOM.unmountComponentAtNode(document.getElementById('container'));
+ });
+
+ // Test passes if unmount completes without errors
+ expect(true).toBe(true);
+ });
+});
+
diff --git a/web/client/components/layout/hooks/useResizeObserver.js b/web/client/components/layout/hooks/useResizeObserver.js
new file mode 100644
index 00000000000..693fb05b456
--- /dev/null
+++ b/web/client/components/layout/hooks/useResizeObserver.js
@@ -0,0 +1,71 @@
+import { useRef, useEffect, useMemo } from 'react';
+import { debounce } from 'lodash';
+
+const DEFAULT_KEYS = ['width', 'height'];
+
+const useResizeObserver = ({
+ onResize = () => {},
+ watch = DEFAULT_KEYS,
+ debounceTime = 100
+} = {}) => {
+ const elementRef = useRef(null);
+ const prevRef = useRef({});
+
+ const debouncedResize = useMemo(
+ () =>
+ debounce((element) => {
+ if (!element) return;
+
+ const rect = element.getBoundingClientRect();
+ const viewportWidth = window.innerWidth;
+ const viewportHeight = window.innerHeight;
+
+ const values = {
+ width: rect.width,
+ height: rect.height,
+ top: rect.top,
+ left: rect.left,
+ right: viewportWidth - rect.right,
+ bottom: viewportHeight - rect.bottom
+ };
+
+ const changed = {};
+
+ watch.forEach((key) => {
+ const value = values[key];
+ if (prevRef.current[key] !== value) {
+ changed[key] = value;
+ }
+ });
+
+ if (Object.keys(changed).length > 0) {
+ prevRef.current = {
+ ...prevRef.current,
+ ...changed
+ };
+ onResize(changed);
+ }
+ }, debounceTime),
+ [onResize, watch, debounceTime]
+ );
+
+ useEffect(() => {
+ const element = elementRef.current;
+ if (!element || watch.length === 0) return null;
+
+ const observer = new ResizeObserver(() => {
+ debouncedResize(element);
+ });
+
+ observer.observe(element);
+
+ return () => {
+ observer.disconnect();
+ debouncedResize.cancel();
+ };
+ }, [debouncedResize, watch]);
+
+ return elementRef;
+};
+
+export default useResizeObserver;
diff --git a/web/client/configs/localConfig.json b/web/client/configs/localConfig.json
index 556e4dc4b6e..6f391b7ba56 100644
--- a/web/client/configs/localConfig.json
+++ b/web/client/configs/localConfig.json
@@ -409,6 +409,7 @@
{
"name": "Map",
"cfg": {
+ "containerPosition": "background",
"mapOptions": {
"openlayers": {
"interactions": {
@@ -475,7 +476,12 @@
}
},
"Home",
- "FeatureEditor",
+ {
+ "name": "FeatureEditor",
+ "cfg": {
+ "containerPosition": "bottom"
+ }
+ },
"LayerDownload",
{
"name": "QueryPanel",
diff --git a/web/client/containers/MapViewer.jsx b/web/client/containers/MapViewer.jsx
index fe8e816f939..c7f9159d2fc 100644
--- a/web/client/containers/MapViewer.jsx
+++ b/web/client/containers/MapViewer.jsx
@@ -16,9 +16,10 @@ const urlQuery = url.parse(window.location.href, true).query;
import ConfigUtils from '../utils/ConfigUtils';
import { getMonitoredState } from '../utils/PluginsUtils';
import ModulePluginsContainer from "../product/pages/containers/ModulePluginsContainer";
-import { createShallowSelectorCreator } from '../utils/ReselectUtils';
-import BorderLayout from '../components/layout/BorderLayout';
+import MapViewerLayout from '../components/layout/MapViewerLayout';
+import { updateMapLayout } from '../actions/maplayout';
+import { createShallowSelectorCreator } from '../utils/ReselectUtils';
const PluginsContainer = connect(
createShallowSelectorCreator(isEqual)(
state => state.plugins,
@@ -45,7 +46,9 @@ class MapViewer extends React.Component {
plugins: PropTypes.object,
loaderComponent: PropTypes.func,
onLoaded: PropTypes.func,
- component: PropTypes.any
+ component: PropTypes.any,
+ onContentResize: PropTypes.func,
+ mapLayout: PropTypes.object
};
static defaultProps = {
@@ -59,6 +62,22 @@ class MapViewer extends React.Component {
this.props.loadMapConfig();
}
+ handleContentResize = (changed) => {
+ if (changed.bottom !== undefined) {
+ const bottomOffset = Math.max(0, changed.bottom - 35);
+ const {boundingMapRect, layout, boundingSidebarRect} = this.props.mapLayout;
+
+ this.props.onContentResize({
+ ...layout,
+ ...boundingSidebarRect,
+ boundingMapRect: {
+ ...boundingMapRect,
+ bottom: bottomOffset
+ }
+ });
+ }
+ };
+
render() {
return ( )}
/>);
}
}
-export default MapViewer;
+export default connect((state) => ({
+ mapLayout: state.maplayout
+}), { onContentResize: updateMapLayout })(MapViewer);
diff --git a/web/client/epics/__tests__/maplayout-test.js b/web/client/epics/__tests__/maplayout-test.js
index 47da19f9576..4c7f256c425 100644
--- a/web/client/epics/__tests__/maplayout-test.js
+++ b/web/client/epics/__tests__/maplayout-test.js
@@ -277,8 +277,8 @@ describe('map layout epics', () => {
actions.map((action) => {
expect(action.type).toBe(UPDATE_MAP_LAYOUT);
expect(action.layout).toEqual({
- left: 0, right: 0, bottom: '100%', dockSize: 100, transform: "translate(0, -0px)", height: "calc(100% - 0px)",
- boundingMapRect: {bottom: "100%", dockSize: 100, left: 0, right: 0},
+ bottom: 0, left: 0, right: 0, transform: "translate(0, -0px)", height: "calc(100% - 0px)",
+ boundingMapRect: {bottom: 0, left: 0, right: 0},
boundingSidebarRect: { right: 0, left: 0, bottom: 0 },
leftPanel: false,
rightPanel: false
@@ -289,7 +289,7 @@ describe('map layout epics', () => {
}
done();
};
- const state = {featuregrid: {open: true, dockSize: 1}};
+ const state = {featuregrid: {open: true}};
testEpic(updateMapLayoutEpic, 1, openFeatureGrid(), epicResult, state);
});
});
diff --git a/web/client/epics/maplayout.js b/web/client/epics/maplayout.js
index 05b9067e448..04c946a4b7d 100644
--- a/web/client/epics/maplayout.js
+++ b/web/client/epics/maplayout.js
@@ -10,7 +10,7 @@ import Rx from 'rxjs';
import {UPDATE_DOCK_PANELS, updateMapLayout, FORCE_UPDATE_MAP_LAYOUT} from '../actions/maplayout';
import {TOGGLE_CONTROL, SET_CONTROL_PROPERTY, SET_CONTROL_PROPERTIES, setControlProperty} from '../actions/controls';
import { MAP_CONFIG_LOADED } from '../actions/config';
-import {SIZE_CHANGE, CLOSE_FEATURE_GRID, OPEN_FEATURE_GRID, closeFeatureGrid} from '../actions/featuregrid';
+import {CLOSE_FEATURE_GRID, OPEN_FEATURE_GRID, closeFeatureGrid} from '../actions/featuregrid';
import {
CLOSE_IDENTIFY,
@@ -32,14 +32,14 @@ import { mapInfoDetailsSettingsFromIdSelector, isMouseMoveIdentifyActiveSelector
import {head, get, findIndex, keys} from 'lodash';
-import { isFeatureGridOpen, getDockSize } from '../selectors/featuregrid';
+import { isFeatureGridOpen } from '../selectors/featuregrid';
import {DEFAULT_MAP_LAYOUT} from "../utils/LayoutUtils";
import {dockPanelsSelector} from "../selectors/maplayout";
/**
* Capture that cause layout change to update the proper object.
* Configures a map layout based on state of panels.
- * @param {external:Observable} action$ manages `MAP_CONFIG_LOADED`, `SIZE_CHANGE`, `CLOSE_FEATURE_GRID`, `OPEN_FEATURE_GRID`, `CLOSE_IDENTIFY`, `NO_QUERYABLE_LAYERS`, `LOAD_FEATURE_INFO`, `TOGGLE_MAPINFO_STATE`, `TOGGLE_CONTROL`, `SET_CONTROL_PROPERTY`.
+ * @param {external:Observable} action$ manages `MAP_CONFIG_LOADED`, `CLOSE_FEATURE_GRID`, `OPEN_FEATURE_GRID`, `CLOSE_IDENTIFY`, `NO_QUERYABLE_LAYERS`, `LOAD_FEATURE_INFO`, `TOGGLE_MAPINFO_STATE`, `TOGGLE_CONTROL`, `SET_CONTROL_PROPERTY`.
* @param store
* @memberof epics.mapLayout
* @return {external:Observable} emitting {@link #actions.map.updateMapLayout} action
@@ -49,7 +49,6 @@ export const updateMapLayoutEpic = (action$, store) =>
action$.ofType(
MAP_CONFIG_LOADED,
- SIZE_CHANGE,
CLOSE_FEATURE_GRID,
OPEN_FEATURE_GRID,
CLOSE_IDENTIFY,
@@ -125,21 +124,17 @@ export const updateMapLayoutEpic = (action$, store) =>
mapInfoEnabledSelector(state) && isMapInfoOpen(state) && !isMouseMoveIdentifyActiveSelector(state) && {right: mapLayout.right.md} || null
].filter(panel => panel)) || {right: 0};
- const dockSize = getDockSize(state) * 100;
- const bottom = isFeatureGridOpen(state) && {bottom: dockSize + '%', dockSize}
- || {bottom: 0}; // To avoid map from de-centering when performing scale zoom
-
const transform = isFeatureGridOpen(state) && {transform: 'translate(0, -' + mapLayout.bottom.sm + 'px)'} || {transform: 'none'};
const height = {height: 'calc(100% - ' + mapLayout.bottom.sm + 'px)'};
const boundingMapRect = {
- ...bottom,
+ bottom: 0, // To avoid map from de-centering when performing scale zoom
...leftPanels,
...rightPanels
};
Object.keys(boundingMapRect).forEach(key => {
- if (['left', 'right', 'dockSize'].includes(key)) {
+ if (['left', 'right'].includes(key)) {
boundingMapRect[key] = boundingMapRect[key] + (boundingSidebarRect[key] ?? 0);
} else {
const totalOffset = (parseFloat(boundingMapRect[key]) + parseFloat(boundingSidebarRect[key] ?? 0));
diff --git a/web/client/plugins/featuregrid/FeatureEditor.jsx b/web/client/plugins/featuregrid/FeatureEditor.jsx
index 835d7940491..17c1105f896 100644
--- a/web/client/plugins/featuregrid/FeatureEditor.jsx
+++ b/web/client/plugins/featuregrid/FeatureEditor.jsx
@@ -7,37 +7,27 @@
*/
import React, { useMemo } from 'react';
import {connect} from 'react-redux';
-import {createSelector, createStructuredSelector} from 'reselect';
+import {createStructuredSelector} from 'reselect';
import {bindActionCreators} from 'redux';
import { get, pick, isEqual } from 'lodash';
import {compose, lifecycle, defaultProps } from 'recompose';
-import ReactDock from 'react-dock';
import ContainerDimensions from 'react-container-dimensions';
import Grid from '../../components/data/featuregrid/FeatureGrid';
import BorderLayout from '../../components/layout/BorderLayout';
import { toChangesMap} from '../../utils/FeatureGridUtils';
-import { sizeChange, setUp, setSyncTool } from '../../actions/featuregrid';
-import {mapLayoutValuesSelector} from '../../selectors/maplayout';
+import { setUp, setSyncTool } from '../../actions/featuregrid';
import {paginationInfo, describeSelector, attributesJSONSchemaSelector, wfsURLSelector, typeNameSelector, isSyncWmsActive} from '../../selectors/query';
-import {modeSelector, changesSelector, newFeaturesSelector, hasChangesSelector, selectedLayerFieldsSelector, selectedFeaturesSelector, getDockSize} from '../../selectors/featuregrid';
+import {modeSelector, changesSelector, newFeaturesSelector, hasChangesSelector, selectedLayerFieldsSelector, selectedFeaturesSelector} from '../../selectors/featuregrid';
import {getPanels, getHeader, getFooter, getDialogs, getEmptyRowsView, getFilterRenderers} from './panels/index';
import {gridTools, gridEvents, pageEvents, toolbarEvents} from './index';
import useFeatureValidation from './hooks/useFeatureValidation';
+import withResize from './hoc/withResize';
const EMPTY_ARR = [];
const EMPTY_OBJ = {};
-const Dock = connect(createSelector(
- getDockSize,
- state => mapLayoutValuesSelector(state, {transform: true}),
- (size, dockStyle) => ({
- size,
- dockStyle
- })
-)
-)(ReactDock);
/**
* @name FeatureEditor
* @memberof plugins
@@ -171,24 +161,13 @@ const Dock = connect(createSelector(
* ```
*
*/
-const FeatureDock = (props = {
+const Editor = (props = {
tools: EMPTY_OBJ,
dialogs: EMPTY_OBJ,
select: EMPTY_ARR
}) => {
const virtualScroll = props.virtualScroll ?? true;
const maxZoom = props?.pluginCfg?.maxZoom;
- const dockProps = {
- dimMode: "none",
- defaultSize: 0.35,
- fluid: true,
- isVisible: props.open,
- maxDockSize: 0.7,
- minDockSize: 0.1,
- position: "bottom",
- setDockSize: () => {},
- zIndex: 1060
- };
const items = props?.items ?? [];
const toolbarItems = items.filter(({target}) => target === 'toolbar');
const filterRenderers = useMemo(() => {
@@ -208,72 +187,70 @@ const FeatureDock = (props = {
});
return (
-
- { props.onSizeChange(size, dockProps); }}>
- {props.open &&
- (
- { ({ height }) =>
- // added height to solve resize issue in firefox, edge and ie
-
- {getDialogs(props.tools)}
-
- }
-
- )
- }
-
-
);
+
+ { ({ height }) =>
+ // added height to solve resize issue in firefox, edge and ie
+
+ {getDialogs(props.tools)}
+
+ }
+
+ );
};
+
+// Wrap Editor with resize HOC
+const ResizableEditor = withResize(Editor);
+
export const selector = createStructuredSelector({
open: state => get(state, "featuregrid.open"),
customEditorsOptions: state => get(state, "featuregrid.customEditorsOptions"),
@@ -340,10 +317,9 @@ const EditorPlugin = compose(
gridTools: gridTools.map((t) => ({
...t,
events: bindActionCreators(t.events, dispatch)
- })),
- onSizeChange: (...params) => dispatch(sizeChange(...params))
+ }))
})
)
-)(FeatureDock);
+)(ResizableEditor);
export default EditorPlugin;
diff --git a/web/client/plugins/featuregrid/__tests__/FeatureEditor-test.jsx b/web/client/plugins/featuregrid/__tests__/FeatureEditor-test.jsx
index d748c5ae538..8edea45a05f 100644
--- a/web/client/plugins/featuregrid/__tests__/FeatureEditor-test.jsx
+++ b/web/client/plugins/featuregrid/__tests__/FeatureEditor-test.jsx
@@ -23,8 +23,7 @@ describe('FeatureEditor plugin component', () => {
multiselect: false,
drawing: false,
newFeatures: [],
- features: [],
- dockSize: 0.35
+ features: []
}
};
const props = {
diff --git a/web/client/plugins/featuregrid/hoc/style.less b/web/client/plugins/featuregrid/hoc/style.less
new file mode 100644
index 00000000000..6a020479a20
--- /dev/null
+++ b/web/client/plugins/featuregrid/hoc/style.less
@@ -0,0 +1,27 @@
+.ms-resize-container {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+
+ .ms-resize-handle {
+ height: 4px;
+ cursor: row-resize;
+ background-color: transparent;
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ z-index: 10;
+ border-top: 2px solid transparent;
+ transition: border-color 0.2s;
+ &:hover {
+ border-top-color: #ccc;
+ }
+ }
+
+ .ms-resize-content {
+ flex: 1;
+ overflow: hidden;
+ }
+}
\ No newline at end of file
diff --git a/web/client/plugins/featuregrid/hoc/withResize.jsx b/web/client/plugins/featuregrid/hoc/withResize.jsx
new file mode 100644
index 00000000000..a53ff2044a1
--- /dev/null
+++ b/web/client/plugins/featuregrid/hoc/withResize.jsx
@@ -0,0 +1,88 @@
+import React, { useEffect, useRef, useState } from "react";
+import './style.less';
+
+/**
+ * HOC that wraps a component in a resizable container.
+ * @param {React.Component} Component - The component to wrap
+ * @returns {React.Component} A component wrapped in a resizable div
+ *
+ * Props:
+ * @prop {boolean} resizeContainer - If true, enables resize functionality (default: true)
+ * @prop {number} defaultHeight - Initial height in pixels (default: 300)
+ * @prop {number} minHeight - Minimum height in pixels (default: 75)
+ * @prop {number} maxHeight - Maximum height in pixels (default: 70% of the window inner height)
+ */
+const withResize = (Component) => {
+ return (props) => {
+ const { resizeContainer = true, defaultHeight = 300, minHeight = 75, maxHeight = '70%' } = props;
+ const [height, setHeight] = useState(defaultHeight);
+ const [isResizing, setIsResizing] = useState(false);
+ const containerRef = useRef(null);
+ const startYRef = useRef(0);
+ const startHeightRef = useRef(0);
+
+ useEffect(() => {
+ const maxAllowedHeight = typeof maxHeight === 'number'
+ ? maxHeight
+ : (window.innerHeight * (maxHeight.replace('%', '')) / 100);
+
+ const handlePointerMove = (e) => {
+ if (!isResizing) return;
+
+ const deltaY = e.clientY - startYRef.current;
+ const newHeight = startHeightRef.current - deltaY;
+ const clampedHeight = Math.max(minHeight, Math.min(newHeight, maxAllowedHeight));
+ setHeight(clampedHeight);
+ };
+
+ const handlePointerUp = () => {
+ setIsResizing(false);
+ };
+
+ if (isResizing) {
+ document.addEventListener('pointermove', handlePointerMove);
+ document.addEventListener('pointerup', handlePointerUp);
+ }
+
+ return () => {
+ document.removeEventListener('pointermove', handlePointerMove);
+ document.removeEventListener('pointerup', handlePointerUp);
+ };
+ }, [isResizing, minHeight, maxHeight]);
+
+ const handleMouseDown = (e) => {
+ e.preventDefault();
+ setIsResizing(true);
+ startYRef.current = e.clientY;
+ startHeightRef.current = height;
+ };
+
+ // If resizeContainer is false, just render in a normal div
+ if (!resizeContainer) {
+ return (
+
+
+
+ );
+ }
+
+ // Render with resize functionality
+ return (
+
+ );
+ };
+};
+
+export default withResize;
diff --git a/web/client/plugins/featuregrid/panels/index.jsx b/web/client/plugins/featuregrid/panels/index.jsx
index beb296a2020..f0414fba343 100644
--- a/web/client/plugins/featuregrid/panels/index.jsx
+++ b/web/client/plugins/featuregrid/panels/index.jsx
@@ -28,7 +28,6 @@ import HeaderComp from '../../../components/data/featuregrid/Header';
import ToolbarComp from '../../../components/data/featuregrid/toolbars/Toolbar';
import {
getAttributeFilter,
- getDockSize,
getTitleSelector,
hasChangesSelector,
hasGeometrySelector,
@@ -49,7 +48,6 @@ import {
isFilterByViewportSupported,
selectedLayerSelector
} from '../../../selectors/featuregrid';
-import { mapLayoutValuesSelector } from '../../../selectors/maplayout';
import {isCesium, mapTypeSelector} from '../../../selectors/maptype';
import {
featureCollectionResultSelector,
@@ -82,8 +80,7 @@ const Toolbar = connect(
hasNewFeatures: hasNewFeaturesSelector,
hasGeometry: hasGeometrySelector,
syncPopover: (state) => ({
- showAgain: showAgainSelector(state),
- dockSize: mapLayoutValuesSelector(state, {dockSize: true}).dockSize + 3.2 + "%"
+ showAgain: showAgainSelector(state)
}),
isDrawing: isDrawingSelector,
isSimpleGeom: isSimpleGeomSelector,
@@ -104,7 +101,6 @@ const Toolbar = connect(
isSnappingLoading,
snappingConfig,
mapType: mapTypeSelector,
- editorHeight: getDockSize,
viewportFilter: isViewportFilterActive,
isFilterByViewportSupported,
layer: selectedLayerSelector
diff --git a/web/client/reducers/__tests__/featuregrid-test.js b/web/client/reducers/__tests__/featuregrid-test.js
index 7b7d20a22ef..83657ad1469 100644
--- a/web/client/reducers/__tests__/featuregrid-test.js
+++ b/web/client/reducers/__tests__/featuregrid-test.js
@@ -47,7 +47,6 @@ import featuregrid from '../featuregrid';
import {
setFeatures,
- dockSizeFeatures,
setLayer,
toggleTool,
customizeAttribute,
@@ -76,7 +75,6 @@ import {
closeFeatureGrid,
toggleShowAgain,
initPlugin,
- sizeChange,
storeAdvancedSearchFilter,
setUp,
setTimeSync,
@@ -215,10 +213,6 @@ describe('Test the featuregrid reducer', () => {
expect(state.features).toExist();
expect(state.features.length).toBe(1);
});
- it('dockSizeFeatures', () => {
- let state = featuregrid( {}, dockSizeFeatures(200));
- expect(state.dockSize).toBe(200);
- });
it('toggleEditMode edit', () => {
let state = featuregrid( {}, toggleEditMode());
expect(state.multiselect).toBeTruthy();
@@ -378,16 +372,6 @@ describe('Test the featuregrid reducer', () => {
expect(state.localType).toBe("Point");
});
- it('SIZE_CHANGE', () => {
- let state = featuregrid({}, sizeChange(0.5, {maxDockSize: 0.7, minDockSize: 0.1}));
- expect(state.dockSize).toBe(0.5);
- state = featuregrid({}, sizeChange(0.8, {maxDockSize: 0.7, minDockSize: 0.1}));
- expect(state.dockSize).toBe(0.7);
- state = featuregrid({}, sizeChange(0.05, {maxDockSize: 0.7, minDockSize: 0.1}));
- expect(state.dockSize).toBe(0.1);
- state = featuregrid({}, sizeChange(0.5));
- expect(state.dockSize).toBe(0.5);
- });
it("storeAdvancedSearchFilter", () => {
const filterObj = {test: 'test'};
let state = featuregrid({selectedLayer: "test_layer"}, storeAdvancedSearchFilter(filterObj));
diff --git a/web/client/reducers/featuregrid.js b/web/client/reducers/featuregrid.js
index 13a20138850..3c84642fca2 100644
--- a/web/client/reducers/featuregrid.js
+++ b/web/client/reducers/featuregrid.js
@@ -21,7 +21,6 @@ import {
SAVE_ERROR,
CLEAR_CHANGES,
CHANGE_PAGE,
- DOCK_SIZE_FEATURES,
SET_LAYER,
TOGGLE_TOOL,
CUSTOMIZE_ATTRIBUTE,
@@ -38,7 +37,6 @@ import {
CLOSE_FEATURE_GRID,
UPDATE_FILTER,
INIT_PLUGIN,
- SIZE_CHANGE,
STORE_ADVANCED_SEARCH_FILTER,
GRID_QUERY_RESULT,
LOAD_MORE_FEATURES,
@@ -77,7 +75,6 @@ const emptyResultsState = {
drawing: false,
newFeatures: [],
features: [],
- dockSize: 0.35,
customEditorsOptions: {
"rules": []
},
@@ -142,8 +139,7 @@ const applyNewChanges = (features, changedFeatures, updates, updatesGeom) =>
* multiselect: false,
* drawing: false,
* newFeatures: [],
- * features: [],
- * dockSize: 0.35
+ * features: []
* }
*
* @memberof reducers
@@ -208,8 +204,6 @@ function featuregrid(state = emptyResultsState, action) {
return Object.assign({}, state, {select: [], changes: []});
case SET_FEATURES:
return Object.assign({}, state, {features: action.features});
- case DOCK_SIZE_FEATURES:
- return Object.assign({}, state, {dockSize: action.dockSize});
case SET_LAYER:
return Object.assign({}, state, {selectedLayer: action.id});
case TOGGLE_TOOL:
@@ -411,17 +405,6 @@ function featuregrid(state = emptyResultsState, action) {
useLayerFilter: action.useLayerFilter ?? state.useLayerFilter // if not present, keep current
};
}
- case SIZE_CHANGE : {
- const maxDockSize = action.dockProps && action.dockProps.maxDockSize;
- const minDockSize = action.dockProps && action.dockProps.minDockSize;
- const size = maxDockSize && minDockSize && minDockSize <= action.size && maxDockSize >= action.size && action.size
- || maxDockSize && maxDockSize < action.size && maxDockSize
- || minDockSize && minDockSize > action.size && minDockSize
- || action.size;
- return Object.assign({}, state, {
- dockSize: size
- });
- }
case STORE_ADVANCED_SEARCH_FILTER : {
return Object.assign({}, state, {advancedFilters: Object.assign({}, state.advancedFilters, {[state.selectedLayer]: action.filterObj})});
}
diff --git a/web/client/selectors/__tests__/featuregrid-test.js b/web/client/selectors/__tests__/featuregrid-test.js
index 54a0f7fce93..6b69e8864fc 100644
--- a/web/client/selectors/__tests__/featuregrid-test.js
+++ b/web/client/selectors/__tests__/featuregrid-test.js
@@ -29,7 +29,6 @@ import {
canEditSelector,
showAgainSelector,
hasSupportedGeometry,
- getDockSize,
selectedLayerNameSelector,
queryOptionsSelector,
showTimeSync,
@@ -474,11 +473,6 @@ describe('Test featuregrid selectors', () => {
});
- it('test getDockSize', () => {
- expect(getDockSize({ featuregrid: {dockSize: 0.5} })).toBe(0.5);
- expect(getDockSize({})).toBe(undefined);
- });
-
it('showTimeSync', () => {
expect(showTimeSync({featuregrid: initialState.featuregrid})).toBeFalsy();
const state = {
diff --git a/web/client/selectors/featuregrid.js b/web/client/selectors/featuregrid.js
index 8f460fa4831..67d9e5dc95c 100644
--- a/web/client/selectors/featuregrid.js
+++ b/web/client/selectors/featuregrid.js
@@ -166,7 +166,6 @@ export const isSimpleGeomSelector = state => isSimpleGeomType(geomTypeSelectedFe
* @param {object} state applications state
* @return {boolean} true if the geometry is supported, false otherwise
*/
-export const getDockSize = state => state.featuregrid && state.featuregrid.dockSize;
/**
* get selected layer name
* @function
diff --git a/web/client/test-resources/geostore/data/context_1.json b/web/client/test-resources/geostore/data/context_1.json
index f1d0a33c5f4..8bb36391605 100644
--- a/web/client/test-resources/geostore/data/context_1.json
+++ b/web/client/test-resources/geostore/data/context_1.json
@@ -27,7 +27,8 @@
"scalebar": {
"container": "#footer-scalebar-container"
}
- }
+ },
+ "containerPosition": "background"
}
},
{
diff --git a/web/client/themes/default/less/common.less b/web/client/themes/default/less/common.less
index cdd0a48df47..6344ea47d74 100644
--- a/web/client/themes/default/less/common.less
+++ b/web/client/themes/default/less/common.less
@@ -136,6 +136,19 @@
box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22);
}
+// Map Viewer Layout - Disable pointer events on the main content container but enable them for all its children
+// This allows the map underneath to receive pointer events while still allowing interaction with layout children
+.ms-map-viewer-layout-main-content {
+ pointer-events: none;
+
+ .ms-map-viewer-layout-content > *,
+ .ms-map-viewer-layout-left-column > *,
+ .ms-map-viewer-layout-right-column > *,
+ .ms-map-viewer-layout-columns > * {
+ pointer-events: auto;
+ }
+}
+
.no-border {
border: none !important;
border-top: none !important;
diff --git a/web/client/utils/ContextCreatorUtils.js b/web/client/utils/ContextCreatorUtils.js
index 37b69713b19..0b9e5ff5559 100644
--- a/web/client/utils/ContextCreatorUtils.js
+++ b/web/client/utils/ContextCreatorUtils.js
@@ -53,6 +53,30 @@ export const migrateContextConfiguration = (context) => {
};
}
}
+ // migration for FeatureEditor to add containerPosition: 'footer' if not present
+ if (plugin.name === 'FeatureEditor') {
+ if (plugin?.cfg?.containerPosition !== 'footer') {
+ return {
+ ...plugin,
+ cfg: {
+ ...plugin.cfg,
+ containerPosition: 'footer'
+ }
+ };
+ }
+ }
+ // migrate for Map to add containerPosition: 'background' if not present
+ if (plugin.name === 'Map') {
+ if (plugin?.cfg?.containerPosition !== 'background') {
+ return {
+ ...plugin,
+ cfg: {
+ ...plugin.cfg,
+ containerPosition: 'background'
+ }
+ };
+ }
+ }
return plugin;
})];
}))
diff --git a/web/client/utils/__tests__/ContextCreatorUtils-test.js b/web/client/utils/__tests__/ContextCreatorUtils-test.js
index 4e9c2c27c93..b82d734701e 100644
--- a/web/client/utils/__tests__/ContextCreatorUtils-test.js
+++ b/web/client/utils/__tests__/ContextCreatorUtils-test.js
@@ -30,7 +30,7 @@ describe('Test the ContextCreatorUtils', () => {
});
expect(newContext).toEqual({
plugins: {
- desktop: [{ name: 'Map' }, { name: 'DeleteResource' }, { name: 'MapFooter', cfg: { containerPosition: 'footer' }}]
+ desktop: [{ name: 'Map', cfg: { containerPosition: 'background' } }, { name: 'DeleteResource' }, { name: 'MapFooter', cfg: { containerPosition: 'footer' }}]
}
});
});