diff --git a/web/client/components/layout/MapViewerLayout.jsx b/web/client/components/layout/MapViewerLayout.jsx
new file mode 100644
index 0000000000..f9bfc428b1
--- /dev/null
+++ b/web/client/components/layout/MapViewerLayout.jsx
@@ -0,0 +1,37 @@
+import React from "react";
+import FlexBox, { FlexFill } from "./FlexBox";
+
+export default ({
+ id,
+ header,
+ footer,
+ background,
+ leftColumn,
+ rightColumn,
+ columns,
+ className,
+ top,
+ bottom,
+ children,
+ bodyClassName
+}) => {
+ return (
+
+ {header}
+
+ {background}
+ {top}
+
+ {leftColumn}
+
+ {children}
+
+ {rightColumn}
+ {columns}
+
+ {bottom}
+
+ {footer}
+
+ );
+};
diff --git a/web/client/configs/localConfig.json b/web/client/configs/localConfig.json
index 556e4dc4b6..6f391b7ba5 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 fe8e816f93..fda38f9902 100644
--- a/web/client/containers/MapViewer.jsx
+++ b/web/client/containers/MapViewer.jsx
@@ -16,9 +16,9 @@ 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 { createShallowSelectorCreator } from '../utils/ReselectUtils';
const PluginsContainer = connect(
createShallowSelectorCreator(isEqual)(
state => state.plugins,
@@ -66,7 +66,7 @@ class MapViewer extends React.Component {
params={this.props.params}
loaderComponent={this.props.loaderComponent}
onLoaded={this.props.onLoaded}
- component={this.props.component || BorderLayout}
+ component={this.props.component || MapViewerLayout}
/>);
}
}
diff --git a/web/client/plugins/featuregrid/FeatureEditor.jsx b/web/client/plugins/featuregrid/FeatureEditor.jsx
index 835d794049..72415d5181 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 {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"),
@@ -344,6 +321,6 @@ const EditorPlugin = compose(
onSizeChange: (...params) => dispatch(sizeChange(...params))
})
)
-)(FeatureDock);
+)(ResizableEditor);
export default EditorPlugin;
diff --git a/web/client/plugins/featuregrid/hoc/withResize.jsx b/web/client/plugins/featuregrid/hoc/withResize.jsx
new file mode 100644
index 0000000000..18a070cb69
--- /dev/null
+++ b/web/client/plugins/featuregrid/hoc/withResize.jsx
@@ -0,0 +1,115 @@
+import React, { useEffect, useRef, useState } from "react";
+
+/**
+ * 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 handleMouseMove = (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 handleMouseUp = () => {
+ setIsResizing(false);
+ };
+
+ if (isResizing) {
+ document.addEventListener('mousemove', handleMouseMove);
+ document.addEventListener('mouseup', handleMouseUp);
+ document.body.style.cursor = 'row-resize';
+ document.body.style.userSelect = 'none';
+ }
+
+ return () => {
+ document.removeEventListener('mousemove', handleMouseMove);
+ document.removeEventListener('mouseup', handleMouseUp);
+ document.body.style.cursor = '';
+ document.body.style.userSelect = '';
+ };
+ }, [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 (
+
+
{
+ e.currentTarget.style.borderTopColor = '#ccc';
+ }}
+ onMouseLeave={(e) => {
+ if (!isResizing) {
+ e.currentTarget.style.borderTopColor = 'transparent';
+ }
+ }}
+ />
+
+
+
+
+ );
+ };
+};
+
+export default withResize;
diff --git a/web/client/product/assets/css/viewer.css b/web/client/product/assets/css/viewer.css
index fb66444ef1..688935ae24 100644
--- a/web/client/product/assets/css/viewer.css
+++ b/web/client/product/assets/css/viewer.css
@@ -154,3 +154,14 @@ html, body, #container, .fill {
max-height: 250px;
overflow-y: auto;
}
+
+/* Disable pointer events on the main content container but enable them for all its children */
+.ms2-layout-main-content {
+ pointer-events: none;
+}
+.ms2-layout-main-content .ms2-layout-content > *,
+.ms2-layout-main-content .ms2-layout-left-column > *,
+.ms2-layout-main-content .ms2-layout-right-column > *,
+.ms2-layout-main-content .ms2-layout-columns > * {
+ pointer-events: auto;
+}
diff --git a/web/client/reducers/featuregrid.js b/web/client/reducers/featuregrid.js
index 13a2013885..26e04a987b 100644
--- a/web/client/reducers/featuregrid.js
+++ b/web/client/reducers/featuregrid.js
@@ -77,7 +77,7 @@ const emptyResultsState = {
drawing: false,
newFeatures: [],
features: [],
- dockSize: 0.35,
+ dockSize: 0,
customEditorsOptions: {
"rules": []
},