From 6437567d5e0410c4ed1a40737774a93d0b9f92c7 Mon Sep 17 00:00:00 2001 From: Joseph John Aas Cooper <33054985+cooper-joe@users.noreply.github.com> Date: Fri, 10 Apr 2026 19:45:02 +0200 Subject: [PATCH 1/3] feat: ui prototype --- i18n/en.pot | 13 ++- src/assets/ArrowDown.jsx | 16 ++- src/assets/OptionsIcon.jsx | 18 +++ src/components/App.css | 4 - src/components/App.jsx | 8 -- .../DimensionsPanel/DndDimensionsPanel.jsx | 17 ++- .../styles/DimensionsPanel.style.js | 6 +- .../styles/DndDimensionList.module.css | 10 +- .../styles/DndDimensionsPanel.module.css | 23 ++-- .../styles/DefaultAxis.module.css | 2 + .../DefaultLayout/styles/DefaultAxis.style.js | 8 +- src/components/Layout/Layout.jsx | 24 +++- src/components/Layout/Layout.module.css | 32 ++++++ src/components/Layout/styles/Chip.module.css | 1 - src/components/Layout/styles/style.js | 2 +- .../MenuBar/InterpretationsButton.jsx | 39 ++++++- src/components/MenuBar/MenuBar.jsx | 86 +++++++++++--- src/components/MenuBar/MenuBar.module.css | 105 ++++++++++++++++++ .../TitleBar/styles/TitleBar.style.js | 17 ++- .../VisualizationOptions/OptionsPopover.jsx | 85 ++++++++++++++ .../OptionsPopover.module.css | 54 +++++++++ .../VisualizationOptionsManager.jsx | 68 ++++++------ .../VisualizationOptionsManager.module.css | 37 ++++++ .../VisualizationTypeSelector.jsx | 50 ++++----- .../VisualizationTypeSelector.module.css | 16 +-- src/components/scrollbar.css | 11 -- 26 files changed, 602 insertions(+), 150 deletions(-) create mode 100644 src/assets/OptionsIcon.jsx create mode 100644 src/components/Layout/Layout.module.css create mode 100644 src/components/MenuBar/MenuBar.module.css create mode 100644 src/components/VisualizationOptions/OptionsPopover.jsx create mode 100644 src/components/VisualizationOptions/OptionsPopover.module.css create mode 100644 src/components/VisualizationOptions/VisualizationOptionsManager.module.css diff --git a/i18n/en.pot b/i18n/en.pot index c8942e421e..130fe08e32 100644 --- a/i18n/en.pot +++ b/i18n/en.pot @@ -5,8 +5,8 @@ msgstr "" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" -"POT-Creation-Date: 2026-03-11T11:54:22.309Z\n" -"PO-Revision-Date: 2026-03-11T11:54:22.310Z\n" +"POT-Creation-Date: 2026-04-10T09:19:34.910Z\n" +"PO-Revision-Date: 2026-04-10T09:19:34.910Z\n" msgid "All items" msgstr "All items" @@ -217,6 +217,15 @@ msgstr "Select a period" msgid "Select years" msgstr "Select years" +msgid "New" +msgstr "New" + +msgid "Open" +msgstr "Open" + +msgid "Save" +msgstr "Save" + msgid "" "This visualization can't be deleted because it is used on one or more " "dashboards" diff --git a/src/assets/ArrowDown.jsx b/src/assets/ArrowDown.jsx index 436dfa335d..0d222141d4 100644 --- a/src/assets/ArrowDown.jsx +++ b/src/assets/ArrowDown.jsx @@ -3,17 +3,15 @@ import React from 'react' const ArrowDown = ({ style = { width: 16, height: 16 } }) => ( - - - + ) diff --git a/src/assets/OptionsIcon.jsx b/src/assets/OptionsIcon.jsx new file mode 100644 index 0000000000..36d7e0cc77 --- /dev/null +++ b/src/assets/OptionsIcon.jsx @@ -0,0 +1,18 @@ +import React from 'react' + +export const OptionsIcon = () => ( + + + +) diff --git a/src/components/App.css b/src/components/App.css index fd418e559c..e32cca1de8 100644 --- a/src/components/App.css +++ b/src/components/App.css @@ -65,7 +65,3 @@ body { overflow: hidden; position: relative; } - -.main-center-layout { - box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.03); -} diff --git a/src/components/App.jsx b/src/components/App.jsx index 3e5c1e0449..861c10358e 100644 --- a/src/components/App.jsx +++ b/src/components/App.jsx @@ -26,11 +26,8 @@ import DndContext from './DndContext.jsx' import { InterpretationModal } from './InterpretationModal/index.js' import Layout from './Layout/Layout.jsx' import { MenuBar } from './MenuBar/MenuBar.jsx' -import { TitleBar } from './TitleBar/TitleBar.jsx' import { Visualization } from './Visualization/Visualization.jsx' -import { VisualizationTypeSelector } from './VisualizationTypeSelector/VisualizationTypeSelector.jsx' import './App.css' -import './scrollbar.css' // Used to avoid repeating `history` listener calls -- see below let lastLocation @@ -229,10 +226,8 @@ class UnconnectedApp extends Component { <>
- { if (this.props.ui.rightSidebarOpen) { this.setState((prevState) => ({ @@ -252,9 +247,6 @@ class UnconnectedApp extends Component {
-
- -
{this.state.initialLoadIsComplete && ( diff --git a/src/components/DimensionsPanel/DndDimensionsPanel.jsx b/src/components/DimensionsPanel/DndDimensionsPanel.jsx index 545cbae6d5..1463ce3827 100644 --- a/src/components/DimensionsPanel/DndDimensionsPanel.jsx +++ b/src/components/DimensionsPanel/DndDimensionsPanel.jsx @@ -1,5 +1,6 @@ -import { DimensionFilter } from '@dhis2/analytics' import i18n from '@dhis2/d2-i18n' +import { InputField } from '@dhis2/ui' +import { IconSearch16 } from '@dhis2/ui-icons' import PropTypes from 'prop-types' import React, { Component } from 'react' import { default as DndDimensionList } from './DndDimensionList.jsx' @@ -20,13 +21,17 @@ export class DndDimensionsPanel extends Component { return (
- + value.length + ? this.onFilterTextChange(value) + : this.onClearFilter() + } + dense type="search" + prefixIcon={} dataTest="dimensions-panel-filter" />
diff --git a/src/components/DimensionsPanel/styles/DimensionsPanel.style.js b/src/components/DimensionsPanel/styles/DimensionsPanel.style.js index 2216436a4c..277d873ea3 100644 --- a/src/components/DimensionsPanel/styles/DimensionsPanel.style.js +++ b/src/components/DimensionsPanel/styles/DimensionsPanel.style.js @@ -1,7 +1,11 @@ export const styles = { divContainer: { - height: '100%', + height: '99%', display: 'flex', flexDirection: 'column', + margin: '4px', + borderRadius: '5px', + overflow: 'hidden', + border: '1px solid var(--colors-grey400)', }, } diff --git a/src/components/DimensionsPanel/styles/DndDimensionList.module.css b/src/components/DimensionsPanel/styles/DndDimensionList.module.css index d57f26d2b1..9844c36e87 100644 --- a/src/components/DimensionsPanel/styles/DndDimensionList.module.css +++ b/src/components/DimensionsPanel/styles/DndDimensionList.module.css @@ -9,11 +9,11 @@ inline-size: 100%; block-size: 100%; overflow: auto; + scrollbar-width: thin; + scrollbar-color: #c7cfd6 transparent; margin-block-start: 0px; - padding: var(--spacers-dp8); + padding: 0 var(--spacers-dp8); background: var(--colors-white); - border-block-start: 1px solid var(--colors-grey300); - border-block-end: 1px solid var(--colors-grey300); } .list { @@ -28,8 +28,8 @@ text-transform: uppercase; font-size: 11px; color: var(--colors-grey600); - margin-block-start: 0; - margin-block-end: var(--spacers-dp8); + margin-block-start: var(--spacers-dp4); + margin-block-end: var(--spacers-dp4); margin-inline-start: 0; margin-inline-end: 0; letter-spacing: 0.3px; diff --git a/src/components/DimensionsPanel/styles/DndDimensionsPanel.module.css b/src/components/DimensionsPanel/styles/DndDimensionsPanel.module.css index 1adc0f539a..111dbeaed3 100644 --- a/src/components/DimensionsPanel/styles/DndDimensionsPanel.module.css +++ b/src/components/DimensionsPanel/styles/DndDimensionsPanel.module.css @@ -2,17 +2,26 @@ block-size: 100%; display: flex; flex-direction: column; - background-color: var(--colors-grey100); - border-inline-end: 1px solid var(--colors-grey400); - box-shadow: 1px 0 2px 0 rgba(0, 0, 0, 0.03); padding: 0; overflow: hidden; } .filter { - padding-block-start: var(--spacers-dp8); - padding-block-end: 0; - padding-inline-start: var(--spacers-dp8); - padding-inline-end: var(--spacers-dp8); + padding-block-start: 6px; + padding-block-end: 8px; + padding-inline-start: 6px; + padding-inline-end: 6px; background: var(--colors-white); } + +.filter input { + font-size: 13px; + line-height: 16px; + font-weight: 400; + border-color: var(--colors-grey300); + height: 30px; +} + +.filter input::placeholder { + color: var(--colors-grey600); +} diff --git a/src/components/Layout/DefaultLayout/styles/DefaultAxis.module.css b/src/components/Layout/DefaultLayout/styles/DefaultAxis.module.css index fff6bf53ff..43b087c7ac 100644 --- a/src/components/Layout/DefaultLayout/styles/DefaultAxis.module.css +++ b/src/components/Layout/DefaultLayout/styles/DefaultAxis.module.css @@ -1,6 +1,8 @@ .content { inline-size: 100%; display: flex; + gap: 4px; + padding-block-start: 2px; align-items: flex-start; align-content: flex-start; flex-wrap: wrap; diff --git a/src/components/Layout/DefaultLayout/styles/DefaultAxis.style.js b/src/components/Layout/DefaultLayout/styles/DefaultAxis.style.js index 73b8dce7ae..a7b4e4e15b 100644 --- a/src/components/Layout/DefaultLayout/styles/DefaultAxis.style.js +++ b/src/components/Layout/DefaultLayout/styles/DefaultAxis.style.js @@ -4,6 +4,7 @@ import * as layoutStyle from '../../styles/style.js' export default { axisContainer: { display: 'flex', + flexDirection: 'column', backgroundColor: layoutStyle.AXIS_BACKGROUND_COLOR, borderColor: layoutStyle.AXIS_BORDER_COLOR, borderStyle: layoutStyle.AXIS_BORDER_STYLE, @@ -17,10 +18,11 @@ export default { minWidth: 55, maxWidth: 55, padding: '2px 0px 0px 0px', - fontSize: 11, - color: colors.grey700, + fontSize: 10, + color: colors.grey600, userSelect: 'none', - letterSpacing: '0.2px', + fontFamily: `'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace`, + textTransform: 'uppercase', }, } // TODO: Refactor this file and all other affected files (DefaultLayout + everything in ../../styles/) to css modules diff --git a/src/components/Layout/Layout.jsx b/src/components/Layout/Layout.jsx index b61f2425ae..daff60df19 100644 --- a/src/components/Layout/Layout.jsx +++ b/src/components/Layout/Layout.jsx @@ -6,11 +6,16 @@ import { LAYOUT_TYPE_SCATTER, LAYOUT_TYPE_OUTLIER_TABLE, getLayoutTypeByVisType, + UpdateButton, } from '@dhis2/analytics' import PropTypes from 'prop-types' import React from 'react' import { connect } from 'react-redux' import { sGetUiType } from '../../reducers/ui.js' +import UpdateVisualizationContainer from '../UpdateButton/UpdateVisualizationContainer.js' +import VisualizationOptionsManager from '../VisualizationOptions/VisualizationOptionsManager.jsx' +import { VisualizationTypeSelector } from '../VisualizationTypeSelector/VisualizationTypeSelector.jsx' +import styles from './Layout.module.css' import DefaultLayout from './DefaultLayout/DefaultLayout.jsx' import OutlierTableLayout from './OutlierTable/OutlierTableLayout.jsx' import PieLayout from './PieLayout/PieLayout.jsx' @@ -31,7 +36,24 @@ const Layout = ({ visType }) => { const layoutType = getLayoutTypeByVisType(visType) const LayoutComponent = componentMap[layoutType] - return + return ( +
+
+
+ ( + + )} + /> +
+ + +
+
+ +
+
+ ) } Layout.propTypes = { diff --git a/src/components/Layout/Layout.module.css b/src/components/Layout/Layout.module.css new file mode 100644 index 0000000000..c0add434dc --- /dev/null +++ b/src/components/Layout/Layout.module.css @@ -0,0 +1,32 @@ +.layout { + margin: 4px 4px 4px 0; + display: flex; + flex-direction: column; + border: 1px solid var(--colors-grey400); + border-radius: 5px; + background-color: var(--colors-white); +} + +.topBar { + display: flex; + align-items: stretch; + background-color: var(--colors-white); + border-radius: 5px 5px 0 0; +} + +.topBar > button, +.topBar > div > button { + border-radius: 0; +} + +.updateButtonWrapper { + display: flex; + /* border-right: 1px solid var(--colors-grey300); */ + margin-inline-end: 4px; +} + +.content { + flex: 1; + overflow: hidden; + border-radius: 0 0 5px 5px; +} diff --git a/src/components/Layout/styles/Chip.module.css b/src/components/Layout/styles/Chip.module.css index c6e200bf5e..859b288106 100644 --- a/src/components/Layout/styles/Chip.module.css +++ b/src/components/Layout/styles/Chip.module.css @@ -13,7 +13,6 @@ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; - margin: 2px; border-radius: 2px; border: 1px solid var(--colors-teal200); } diff --git a/src/components/Layout/styles/style.js b/src/components/Layout/styles/style.js index ff4a2f6257..67cd23315e 100644 --- a/src/components/Layout/styles/style.js +++ b/src/components/Layout/styles/style.js @@ -8,5 +8,5 @@ export const AXIS_PADDING = '4px 4px 4px 6px' export const AXIS_LABEL_PADDING = '2px 0px 0px 4px' export const AXIS_BORDER_COLOR = colors.grey300 export const AXIS_BORDER_STYLE = 'solid' -export const AXIS_BORDER_WIDTH = '0px 0px 1px 1px' +export const AXIS_BORDER_WIDTH = '1px 0px 0px 1px' export const AXIS_BACKGROUND_COLOR = colors.white diff --git a/src/components/MenuBar/InterpretationsButton.jsx b/src/components/MenuBar/InterpretationsButton.jsx index 41fd8b3203..1e906cee21 100644 --- a/src/components/MenuBar/InterpretationsButton.jsx +++ b/src/components/MenuBar/InterpretationsButton.jsx @@ -1,9 +1,38 @@ -import { InterpretationsAndDetailsToggler } from '@dhis2/analytics' import React from 'react' import { useDispatch, useSelector } from 'react-redux' import { acToggleUiRightSidebarOpen } from '../../actions/ui.js' import { sGetCurrent } from '../../reducers/current.js' import { sGetUiRightSidebarOpen } from '../../reducers/ui.js' +import styles from './MenuBar.module.css' + +const OpenIcon = () => ( + + + + + +) + +const CloseIcon = () => ( + + + + + + +) export const InterpretationsButton = () => { const isShowing = useSelector(sGetUiRightSidebarOpen) @@ -15,10 +44,12 @@ export const InterpretationsButton = () => { } return ( - + > + {isShowing ? : } + ) } diff --git a/src/components/MenuBar/MenuBar.jsx b/src/components/MenuBar/MenuBar.jsx index dae5d1f566..b0dbda950a 100644 --- a/src/components/MenuBar/MenuBar.jsx +++ b/src/components/MenuBar/MenuBar.jsx @@ -1,14 +1,16 @@ import { FileMenu, HoverMenuBar, - UpdateButton, + OpenFileDialog, + SaveAsDialog, VIS_TYPE_GROUP_ALL, VIS_TYPE_GROUP_CHARTS, useCachedDataQuery, } from '@dhis2/analytics' import i18n from '@dhis2/d2-i18n' +import { IconAdd16, IconFolderOpen16, IconSave16 } from '@dhis2/ui' import PropTypes from 'prop-types' -import React from 'react' +import React, { useState } from 'react' import { connect } from 'react-redux' import * as fromActions from '../../actions/index.js' import { getErrorVariantByStatusCode } from '../../modules/error.js' @@ -23,9 +25,9 @@ import { import { sGetCurrent } from '../../reducers/current.js' import { sGetVisualization } from '../../reducers/visualization.js' import { ToolbarDownloadDropdown } from '../DownloadMenu/ToolbarDownloadDropdown.jsx' -import UpdateVisualizationContainer from '../UpdateButton/UpdateVisualizationContainer.js' -import VisualizationOptionsManager from '../VisualizationOptions/VisualizationOptionsManager.jsx' +import { TitleBar } from '../TitleBar/TitleBar.jsx' import { InterpretationsButton } from './InterpretationsButton.jsx' +import styles from './MenuBar.module.css' const onOpen = (id) => { const path = `/${id}` @@ -55,7 +57,8 @@ const getOnDelete = (props) => () => props.onDeleteVisualization() const getOnError = (props) => (error) => props.onError(error) -const UnconnectedMenuBar = ({ dataTest, onFileMenuAction, ...props }) => { +const UnconnectedMenuBar = ({ onFileMenuAction, ...props }) => { + const [dialog, setDialog] = useState(null) const { currentUser } = useCachedDataQuery() const filterVisTypesByVersion = useVisTypesFilterByVersion() @@ -68,16 +71,39 @@ const UnconnectedMenuBar = ({ dataTest, onFileMenuAction, ...props }) => { })), ] + const canSave = [STATE_UNSAVED, STATE_DIRTY].includes( + getVisualizationState(props.visualization, props.current) + ) + + const onSaveClick = () => { + if (props.current?.id) { + getOnSave(props)() + } else { + setDialog('saveas') + } + } + return ( <> - ( - - )} - /> + + + { onDelete={getOnDelete(props)} onError={getOnError(props)} /> - - + + + {dialog === 'open' && ( + setDialog(null)} + onFileSelect={(id) => { + onOpen(id) + setDialog(null) + }} + onNew={() => { + onNew() + setDialog(null) + }} + /> + )} + {dialog === 'saveas' && ( + { + getOnSaveAs(props)(details) + setDialog(null) + }} + onClose={() => setDialog(null)} + /> + )} ) } @@ -114,7 +169,6 @@ const UnconnectedMenuBar = ({ dataTest, onFileMenuAction, ...props }) => { UnconnectedMenuBar.propTypes = { apiObjectName: PropTypes.string, current: PropTypes.object, - dataTest: PropTypes.string, visualization: PropTypes.object, onFileMenuAction: PropTypes.func, } diff --git a/src/components/MenuBar/MenuBar.module.css b/src/components/MenuBar/MenuBar.module.css new file mode 100644 index 0000000000..698c386d04 --- /dev/null +++ b/src/components/MenuBar/MenuBar.module.css @@ -0,0 +1,105 @@ +.menuButton { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 4px 8px; + border: none; + border-radius: 3px; + background: #fff; + color: #212934; + font-size: 13px; + line-height: 16px; + cursor: pointer; + white-space: nowrap; + user-select: none; + transition: background-color 0.15s, border-color 0.15s; +} + +.menuButton:hover { + background: #f3f5f7; + border-color: transparent; +} + +.menuButton:active { + background: #e8edf2; +} + +.menuButton:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.menuButton:disabled:hover { + background: #fff; + border-color: transparent; +} + +.menuButton svg { + width: 16px; + height: 16px; + flex-shrink: 0; +} + +:global([data-test='dhis2-analytics-toolbar']) { + height: 36px !important; + min-height: 36px !important; +} + +:global([data-test='dhis2-analytics-hovermenubar']) { + position: relative; + margin-inline-start: 8px; + padding-inline-start: 8px; +} + +:global([data-test='dhis2-analytics-hovermenubar'])::before { + content: ''; + position: absolute; + inset-inline-start: 0; + top: 50%; + transform: translateY(-50%); + height: 20px; + width: 1px; + background: var(--colors-grey300); +} + +:global([data-test='dhis2-analytics-hovermenubar']) > button { + font-size: 13px; +} + +.iconButton { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 4px; + margin: 4px; + border: none; + border-radius: 3px; + background: none; + color: #4a5768; + cursor: pointer; + margin-inline-start: auto; + transition: background-color 0.15s; +} + +.iconButton:hover { + background: #f3f5f7; +} + +.iconButton:active { + background: #e8edf2; +} + +.iconButton:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.iconButton:disabled:hover { + background: none; +} + +.iconButton svg { + width: 16px; + height: 16px; + flex-shrink: 0; +} diff --git a/src/components/TitleBar/styles/TitleBar.style.js b/src/components/TitleBar/styles/TitleBar.style.js index a6fe0af69e..dc55530e53 100644 --- a/src/components/TitleBar/styles/TitleBar.style.js +++ b/src/components/TitleBar/styles/TitleBar.style.js @@ -3,18 +3,23 @@ import { colors } from '@dhis2/ui' export default { titleBar: { display: 'flex', - justifyContent: 'center', + alignItems: 'center', + paddingInlineStart: '12px', + marginInlineStart: '8px', + borderInlineStart: '1px solid var(--colors-grey400)', }, cell: { display: 'flex', alignItems: 'center', - background: colors.white, - padding: '6px', - borderRadius: '5px', - margin: '4px', }, title: { - fontSize: '14px', + fontSize: '13px', + fontWeight: '500', + lineHeight: '16px', + whiteSpace: 'nowrap', + overflow: 'hidden', + textOverflow: 'ellipsis', + maxWidth: '360px', }, suffix: { paddingInlineStart: '4px', diff --git a/src/components/VisualizationOptions/OptionsPopover.jsx b/src/components/VisualizationOptions/OptionsPopover.jsx new file mode 100644 index 0000000000..69e27539cf --- /dev/null +++ b/src/components/VisualizationOptions/OptionsPopover.jsx @@ -0,0 +1,85 @@ +import i18n from '@dhis2/d2-i18n' +import { TabBar, Tab, FieldSet, Legend, Help, Button, ButtonStrip } from '@dhis2/ui' +import PropTypes from 'prop-types' +import React, { useState } from 'react' +import styles from './OptionsPopover.module.css' + +export const OptionsPopover = ({ optionsConfig, onUpdate, onClose }) => { + const [activeTabKey, setActiveTabKey] = useState( + optionsConfig[0]?.key ?? null + ) + + let activeTabIndex = optionsConfig.findIndex( + (tab) => tab.key === activeTabKey + ) + if (activeTabIndex < 0) { + activeTabIndex = 0 + } + + const activeTab = optionsConfig[activeTabIndex] + + return ( +
+
+ + {optionsConfig.map(({ key, label }, index) => ( + setActiveTabKey(key)} + selected={index === activeTabIndex} + > + {label} + + ))} + +
+
+ {activeTab?.content?.map( + ({ key, label, content, helpText }) => ( +
+
+ {label && ( + + + {label} + + + )} + {content} + {helpText && ( + + {helpText} + + )} +
+
+ ) + )} +
+
+ + + + +
+
+ ) +} + +OptionsPopover.propTypes = { + optionsConfig: PropTypes.array.isRequired, + onClose: PropTypes.func, + onUpdate: PropTypes.func, +} diff --git a/src/components/VisualizationOptions/OptionsPopover.module.css b/src/components/VisualizationOptions/OptionsPopover.module.css new file mode 100644 index 0000000000..c55425a5c3 --- /dev/null +++ b/src/components/VisualizationOptions/OptionsPopover.module.css @@ -0,0 +1,54 @@ +.popover { + position: absolute; + top: 100%; + left: 0; + z-index: 200; + width: 680px; + max-height: 80vh; + display: flex; + flex-direction: column; + background: var(--colors-white); + border: 1px solid var(--colors-grey300); + border-radius: 4px; + box-shadow: 0 4px 24px rgba(0, 0, 0, 0.18); + overflow: hidden; +} + +.tabBar { + flex-shrink: 0; + padding: 0 16px; + border-bottom: 1px solid var(--colors-grey300); +} + +.tabContent { + flex: 1; + overflow-y: auto; + padding: 0 24px; +} + +.section { + padding: 16px 0; +} + +.section:not(:last-child) { + border-bottom: 1px solid var(--colors-grey300); + margin-bottom: 8px; +} + +.sectionTitle { + display: inline-block; + padding-bottom: 12px; + font-size: 15px; + color: var(--colors-grey900); + font-weight: 500; + letter-spacing: 0.2px; +} + +.actions { + flex-shrink: 0; + display: flex; + justify-content: flex-end; + padding: 12px 16px; + border-top: 1px solid var(--colors-grey300); + background: var(--colors-grey050); +} diff --git a/src/components/VisualizationOptions/VisualizationOptionsManager.jsx b/src/components/VisualizationOptions/VisualizationOptionsManager.jsx index bae0102753..170e8b8242 100644 --- a/src/components/VisualizationOptions/VisualizationOptionsManager.jsx +++ b/src/components/VisualizationOptions/VisualizationOptionsManager.jsx @@ -4,15 +4,12 @@ import { hasCustomAxes, hasRelativeItems, isDualAxisType, - HoverMenuDropdown, - HoverMenuList, - HoverMenuListItem, - VisualizationOptions, VIS_TYPE_PIVOT_TABLE, } from '@dhis2/analytics' import i18n from '@dhis2/d2-i18n' +import { OptionsIcon } from '../../assets/OptionsIcon.jsx' import PropTypes from 'prop-types' -import React, { useState } from 'react' +import React, { useState, useRef, useEffect, useCallback } from 'react' import { connect } from 'react-redux' import { getOptionsByType } from '../../modules/options/config.js' import { @@ -22,6 +19,8 @@ import { sGetUiLayout, } from '../../reducers/ui.js' import UpdateVisualizationContainer from '../UpdateButton/UpdateVisualizationContainer.js' +import { OptionsPopover } from './OptionsPopover.jsx' +import optionsStyles from './VisualizationOptionsManager.module.css' const VisualizationOptionsManager = ({ visualizationType, @@ -31,11 +30,22 @@ const VisualizationOptionsManager = ({ series, cumulativeValues, }) => { - const [selectedOptionConfigKey, setSelectedOptionConfigKey] = useState(null) - const onOptionsUpdate = (handler) => { - handler() - setSelectedOptionConfigKey(null) - } + const [popoverOpen, setPopoverOpen] = useState(false) + const wrapperRef = useRef(null) + + const handleClose = useCallback(() => setPopoverOpen(false), []) + + useEffect(() => { + if (!popoverOpen) return + const handleClickOutside = (e) => { + if (wrapperRef.current && !wrapperRef.current.contains(e.target)) { + setPopoverOpen(false) + } + } + document.addEventListener('mousedown', handleClickOutside) + return () => + document.removeEventListener('mousedown', handleClickOutside) + }, [popoverOpen]) const filteredSeries = series.filter((seriesItem) => columnDimensionItems.some( @@ -61,36 +71,30 @@ const VisualizationOptionsManager = ({ }) return ( - <> - + + {popoverOpen && ( ( - onOptionsUpdate(handler)} - onClose={() => setSelectedOptionConfigKey(null)} - initiallyActiveTabKey={selectedOptionConfigKey} + onUpdate={() => { + handler() + setPopoverOpen(false) + }} + onClose={handleClose} /> )} /> )} - +
) } diff --git a/src/components/VisualizationOptions/VisualizationOptionsManager.module.css b/src/components/VisualizationOptions/VisualizationOptionsManager.module.css new file mode 100644 index 0000000000..0eae40ff0f --- /dev/null +++ b/src/components/VisualizationOptions/VisualizationOptionsManager.module.css @@ -0,0 +1,37 @@ +.wrapper { + position: relative; + display: flex; + align-items: stretch; +} + +.optionsButton { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 0 12px; + margin: 4px 0; + border: none; + border-radius: 5px; + background: var(--colors-white); + color: #212934; + font-size: 13px; + line-height: 16px; + cursor: pointer; + white-space: nowrap; + user-select: none; +} + +.optionsButton:hover { + background: var(--colors-grey200); + border-radius: 5px; +} + +.optionsButton:active { + background: var(--colors-grey200); +} + +.optionsButton svg { + width: 16px; + height: 16px; + flex-shrink: 0; +} diff --git a/src/components/VisualizationTypeSelector/VisualizationTypeSelector.jsx b/src/components/VisualizationTypeSelector/VisualizationTypeSelector.jsx index e0ec9f9dfd..2b33b0a083 100644 --- a/src/components/VisualizationTypeSelector/VisualizationTypeSelector.jsx +++ b/src/components/VisualizationTypeSelector/VisualizationTypeSelector.jsx @@ -1,4 +1,4 @@ -import { visTypeDisplayNames, ToolbarSidebar } from '@dhis2/analytics' +import { visTypeDisplayNames } from '@dhis2/analytics' import { useConfig } from '@dhis2/app-runtime' import { useSetting } from '@dhis2/app-service-datastore' import i18n from '@dhis2/d2-i18n' @@ -105,34 +105,32 @@ const UnconnectedVisualizationTypeSelector = ({ return ( <> - -
+ + + {visTypeDisplayNames[visualizationType]} + + - - - {visTypeDisplayNames[visualizationType]} - - - - -
-
+ + +
{listIsOpen && ( diff --git a/src/components/VisualizationTypeSelector/styles/VisualizationTypeSelector.module.css b/src/components/VisualizationTypeSelector/styles/VisualizationTypeSelector.module.css index 390492cf2d..63b59648e0 100644 --- a/src/components/VisualizationTypeSelector/styles/VisualizationTypeSelector.module.css +++ b/src/components/VisualizationTypeSelector/styles/VisualizationTypeSelector.module.css @@ -1,6 +1,6 @@ .arrowIcon { display: flex; - margin-inline-start: auto; + margin-inline-start: 8px; } .arrowIcon.listIsOpen { @@ -8,16 +8,17 @@ } .button { - padding-block-start: 7px; - padding-block-end: 7px; - padding-inline-start: var(--spacers-dp8); + padding-block-start: 2px; + padding-block-end: 2px; + padding-inline-start: 6px; padding-inline-end: var(--spacers-dp8); + margin: 4px 0; display: flex; align-items: center; justify-content: flex-start; background-color: var(--colors-white); cursor: pointer; - flex-grow: 1; + border-radius: 5px; } .button:hover, @@ -26,8 +27,9 @@ } .button > .selectedVizTypeLabel { - font-size: 14px; - padding-block-start: 1px; + font-size: 13px; + font-weight: 500; + padding-block-start: 2px; user-select: none; color: var(--colors-grey900); margin-inline-start: 4px; diff --git a/src/components/scrollbar.css b/src/components/scrollbar.css index 7d2bd63195..e69de29bb2 100644 --- a/src/components/scrollbar.css +++ b/src/components/scrollbar.css @@ -1,11 +0,0 @@ -/* Width */ -::-webkit-scrollbar { - inline-size: 7px; - block-size: 7px; -} - -/* Handle */ -::-webkit-scrollbar-thumb { - background: #d1d1d1; - border-radius: 3px; -} From c19e9c9b4abbbadb34c9d44ecfd38017427dbee0 Mon Sep 17 00:00:00 2001 From: Joseph John Aas Cooper <33054985+cooper-joe@users.noreply.github.com> Date: Mon, 13 Apr 2026 13:38:29 +0200 Subject: [PATCH 2/3] feat: horizontal layout --- .../Layout/DefaultLayout/DefaultLayout.jsx | 29 +------- .../DefaultLayout/styles/DefaultAxis.style.js | 3 - .../styles/DefaultLayout.style.js | 27 ++----- .../OutlierTable/OutlierTableLayout.jsx | 21 ++---- src/components/Layout/PieLayout/PieLayout.jsx | 37 +++------- .../PivotTableLayout/PivotTableLayout.jsx | 48 ++++--------- .../Layout/ScatterLayout/ScatterLayout.jsx | 63 ++++++---------- .../YearOverYearLayout/YearOverYearLayout.jsx | 72 +++++++------------ 8 files changed, 76 insertions(+), 224 deletions(-) diff --git a/src/components/Layout/DefaultLayout/DefaultLayout.jsx b/src/components/Layout/DefaultLayout/DefaultLayout.jsx index 3d2e17339d..61ac80f115 100644 --- a/src/components/Layout/DefaultLayout/DefaultLayout.jsx +++ b/src/components/Layout/DefaultLayout/DefaultLayout.jsx @@ -5,36 +5,13 @@ import { } from '@dhis2/analytics' import React from 'react' import DefaultAxis from './DefaultAxis.jsx' -import defaultAxisStyles from './styles/DefaultAxis.style.js' import styles from './styles/DefaultLayout.style.js' const Layout = () => (
-
- - -
-
- -
+ + +
) diff --git a/src/components/Layout/DefaultLayout/styles/DefaultAxis.style.js b/src/components/Layout/DefaultLayout/styles/DefaultAxis.style.js index a7b4e4e15b..a4223396df 100644 --- a/src/components/Layout/DefaultLayout/styles/DefaultAxis.style.js +++ b/src/components/Layout/DefaultLayout/styles/DefaultAxis.style.js @@ -11,9 +11,6 @@ export default { borderWidth: layoutStyle.AXIS_BORDER_WIDTH, padding: layoutStyle.AXIS_PADDING, }, - axisContainerLeft: { - borderInlineStartWidth: 0, - }, label: { minWidth: 55, maxWidth: 55, diff --git a/src/components/Layout/DefaultLayout/styles/DefaultLayout.style.js b/src/components/Layout/DefaultLayout/styles/DefaultLayout.style.js index ce3c0c4c22..7e44f3f4ee 100644 --- a/src/components/Layout/DefaultLayout/styles/DefaultLayout.style.js +++ b/src/components/Layout/DefaultLayout/styles/DefaultLayout.style.js @@ -1,34 +1,17 @@ import { LAYOUT_HEIGHT } from '../../styles/style.js' -// Axis -export const FILTER_AXIS_WIDTH = '50%' export const DIMENSION_AXIS_CONTENT_HEIGHT = '36px' -// Axis (generated) -export const DIMENSION_AXIS_WIDTH = `${100 - parseInt(FILTER_AXIS_WIDTH, 10)}%` - export default { ct: { display: 'flex', minHeight: LAYOUT_HEIGHT, }, - axisGroup: { - display: 'flex', - flexDirection: 'column', - }, - axisGroupLeft: { - flexBasis: DIMENSION_AXIS_WIDTH, - }, - axisGroupRight: { - flexBasis: FILTER_AXIS_WIDTH, - }, - columns: { - flexBasis: '50%', - }, - rows: { - flexBasis: '50%', + axis: { + flex: 1, }, - filters: { - flexBasis: '100%', + firstAxis: { + flex: 1, + borderInlineStartWidth: 0, }, } diff --git a/src/components/Layout/OutlierTable/OutlierTableLayout.jsx b/src/components/Layout/OutlierTable/OutlierTableLayout.jsx index bc1c6e4d06..57d54758f7 100644 --- a/src/components/Layout/OutlierTable/OutlierTableLayout.jsx +++ b/src/components/Layout/OutlierTable/OutlierTableLayout.jsx @@ -1,27 +1,14 @@ import { AXIS_ID_COLUMNS } from '@dhis2/analytics' import React from 'react' import DefaultAxis from '../DefaultLayout/DefaultAxis.jsx' -import defaultAxisStyles from '../DefaultLayout/styles/DefaultAxis.style.js' import defaultLayoutStyles from '../DefaultLayout/styles/DefaultLayout.style.js' -import outlierTableLayoutStyles from './styles/OutlierTableLayout.style.js' const Layout = () => (
-
- -
+
) diff --git a/src/components/Layout/PieLayout/PieLayout.jsx b/src/components/Layout/PieLayout/PieLayout.jsx index a1ebddaa8f..bfa43e2678 100644 --- a/src/components/Layout/PieLayout/PieLayout.jsx +++ b/src/components/Layout/PieLayout/PieLayout.jsx @@ -1,39 +1,18 @@ import { AXIS_ID_COLUMNS, AXIS_ID_FILTERS } from '@dhis2/analytics' import React from 'react' import DefaultAxis from '../DefaultLayout/DefaultAxis.jsx' -import defaultAxisStyles from '../DefaultLayout/styles/DefaultAxis.style.js' import defaultLayoutStyles from '../DefaultLayout/styles/DefaultLayout.style.js' -import pieLayoutStyles from './styles/PieLayout.style.js' const Layout = () => (
-
- -
-
- -
+ +
) diff --git a/src/components/Layout/PivotTableLayout/PivotTableLayout.jsx b/src/components/Layout/PivotTableLayout/PivotTableLayout.jsx index fd5ee4d087..4a35396ec4 100644 --- a/src/components/Layout/PivotTableLayout/PivotTableLayout.jsx +++ b/src/components/Layout/PivotTableLayout/PivotTableLayout.jsx @@ -5,46 +5,22 @@ import { } from '@dhis2/analytics' import React from 'react' import DefaultAxis from '../DefaultLayout/DefaultAxis.jsx' -import defaultAxisStyles from '../DefaultLayout/styles/DefaultAxis.style.js' import defaultLayoutStyles from '../DefaultLayout/styles/DefaultLayout.style.js' -import pivotTableLayoutStyles from './styles/PivotTableLayout.style.js' const Layout = () => (
-
- - -
-
- -
+ + +
) diff --git a/src/components/Layout/ScatterLayout/ScatterLayout.jsx b/src/components/Layout/ScatterLayout/ScatterLayout.jsx index 7a4b3e3929..fa45f5635e 100644 --- a/src/components/Layout/ScatterLayout/ScatterLayout.jsx +++ b/src/components/Layout/ScatterLayout/ScatterLayout.jsx @@ -9,54 +9,31 @@ import { } from '../../../modules/ui.js' import { sGetUiItemsByAttribute } from '../../../reducers/ui.js' import DefaultAxis from '../DefaultLayout/DefaultAxis.jsx' -import defaultAxisStyles from '../DefaultLayout/styles/DefaultAxis.style.js' import defaultLayoutStyles from '../DefaultLayout/styles/DefaultLayout.style.js' import ScatterAxis from './ScatterAxis.jsx' -import scatterLayoutStyles from './styles/ScatterLayout.style.js' const Layout = ({ getItemsByAttribute }) => (
-
- - -
-
- - -
+ + + +
) diff --git a/src/components/Layout/YearOverYearLayout/YearOverYearLayout.jsx b/src/components/Layout/YearOverYearLayout/YearOverYearLayout.jsx index 86621d6d63..91a5b40cfa 100644 --- a/src/components/Layout/YearOverYearLayout/YearOverYearLayout.jsx +++ b/src/components/Layout/YearOverYearLayout/YearOverYearLayout.jsx @@ -19,63 +19,39 @@ import { sGetUiYearOverYearCategory, } from '../../../reducers/ui.js' import DefaultAxis from '../DefaultLayout/DefaultAxis.jsx' -import defaultAxisStyles from '../DefaultLayout/styles/DefaultAxis.style.js' import defaultLayoutStyles from '../DefaultLayout/styles/DefaultLayout.style.js' -import YearOverYearLayoutStyles from './styles/YearOverYearLayout.style.js' import YearOverYearAxis from './YearOverYearAxis.jsx' import YearOverYearSelect from './YearOverYearSelect.jsx' const Layout = (props) => (
-
- - - - - - -
-
+ + - -
+ +
) From 69b0db605a131860feb1e3b1b42f22b8cb486cfa Mon Sep 17 00:00:00 2001 From: Joseph John Aas Cooper <33054985+cooper-joe@users.noreply.github.com> Date: Mon, 13 Apr 2026 13:41:22 +0200 Subject: [PATCH 3/3] fix: layout min height --- src/components/Layout/styles/style.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Layout/styles/style.js b/src/components/Layout/styles/style.js index 67cd23315e..1eb2267ded 100644 --- a/src/components/Layout/styles/style.js +++ b/src/components/Layout/styles/style.js @@ -1,7 +1,7 @@ import { colors } from '@dhis2/ui' // Layout -export const LAYOUT_HEIGHT = '70px' +export const LAYOUT_HEIGHT = '50px' // Axis export const AXIS_PADDING = '4px 4px 4px 6px'