diff --git a/.claude/settings.json b/.claude/settings.json index b0b834f9..4f66b6fc 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -2,8 +2,8 @@ "permissions": { "allow": [ "mcp__grep__*", - "mcp__plugin_context7_*", - "mcp__plugin_chrome-devtools-mcp_*" + "mcp__plugin_context7_context7__*", + "mcp__plugin_chrome-devtools-mcp_chrome-devtools__*" ] }, "hooks": { diff --git a/i18n/en.pot b/i18n/en.pot index 6f5787c3..29a84557 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-06-01T14:14:08.533Z\n" -"PO-Revision-Date: 2026-06-01T14:14:08.533Z\n" +"POT-Creation-Date: 2026-06-03T06:57:55.329Z\n" +"PO-Revision-Date: 2026-06-03T06:57:55.331Z\n" msgid "Add to {{- axisName}}" msgstr "Add to {{- axisName}}" @@ -656,12 +656,8 @@ msgstr "" "used in the layout ({{- tetName}}). Remove the existing dimensions to use " "these." -msgid "" -"Program indicators are not valid with {{visType}}. Switch to Line list to " -"use them." -msgstr "" -"Program indicators are not valid with {{visType}}. Switch to Line list to " -"use them." +msgid "Cannot be used with {{visType}}." +msgstr "Cannot be used with {{visType}}." msgid "" "This dimension is used as the custom value. Remove the custom value to use " diff --git a/src/components/app-wrapper/drag-and-drop-provider/dimension-drag-overlay.tsx b/src/components/app-wrapper/drag-and-drop-provider/dimension-drag-overlay.tsx index aeb013c7..5bd0e093 100644 --- a/src/components/app-wrapper/drag-and-drop-provider/dimension-drag-overlay.tsx +++ b/src/components/app-wrapper/drag-and-drop-provider/dimension-drag-overlay.tsx @@ -28,12 +28,13 @@ const DragOverlayItem: FC< chipClasses.dragging, classes.overlay, { - [chipClasses.chipEmpty]: - !!data.overlayItemProps.itemsText, + [chipClasses.chipEmpty]: data.overlayItemProps.isEmpty, } )} > - +
+ +
) } diff --git a/src/components/app-wrapper/drag-and-drop-provider/dnd-context-provider.tsx b/src/components/app-wrapper/drag-and-drop-provider/dnd-context-provider.tsx index b3077633..014dd3df 100644 --- a/src/components/app-wrapper/drag-and-drop-provider/dnd-context-provider.tsx +++ b/src/components/app-wrapper/drag-and-drop-provider/dnd-context-provider.tsx @@ -1,4 +1,10 @@ -import { DndContext, useSensor, useSensors, PointerSensor } from '@dnd-kit/core' +import { + DndContext, + useSensor, + useSensors, + PointerSensor, + type AutoScrollOptions, +} from '@dnd-kit/core' import { type FC, type PropsWithChildren } from 'react' import { collisionDetector } from './collision-detector' import { DimensionDragOverlay } from './dimension-drag-overlay' @@ -10,6 +16,12 @@ const activateAt15pixels = { }, } +const DISABLE_AUTO_SCROLL_SELECTOR = '[data-dnd-auto-scroll="disabled"]' + +const autoScrollOptions: AutoScrollOptions = { + canScroll: (element) => !element.matches(DISABLE_AUTO_SCROLL_SELECTOR), +} + export const DndContextProvider: FC = ({ children }) => { // Wait 15px movement before starting drag, so that click event isn't overridden const sensor = useSensor(PointerSensor, activateAt15pixels) @@ -18,6 +30,7 @@ export const DndContextProvider: FC = ({ children }) => { return ( ', () => { assertTooltipContent(['None selected']) }) + it('renders an empty filter chip with a muted suffix', () => { + const dimension: LayoutDimension = { + id: 'ZzYYXq4fJie.X8zyunlgUfM', + name: 'MCH Infant Feeding', + dimensionType: 'DATA_ELEMENT', + valueType: 'TEXT', + optionSet: 'x31y45jvIQL', + dimensionId: 'X8zyunlgUfM', + programStageId: 'ZzYYXq4fJie', + suffix: 'Baby Postnatal', + } + + const appWrapperProps = createMockOptions({ + itemsByDimension: {}, + conditionsByDimension: {}, + }) + + cy.mount( + + + + ) + + cy.window().then((win) => { + const probe = win.document.createElement('span') + probe.style.color = 'var(--colors-grey600)' + win.document.body.appendChild(probe) + const expectedSuffixColor = win.getComputedStyle(probe).color + probe.remove() + + cy.getByDataTest('chip-suffix') + .should('contain.text', '· Baby Postnatal') + .and('have.css', 'color', expectedSuffixColor) + }) + }) + it('renders a chip in filters that has items', () => { const activeStatus = 'ACTIVE' const completedStatus = 'COMPLETED' diff --git a/src/components/layout-panel/axis/chip-base.tsx b/src/components/layout-panel/axis/chip-base.tsx index d1cf163b..ead2aeee 100644 --- a/src/components/layout-panel/axis/chip-base.tsx +++ b/src/components/layout-panel/axis/chip-base.tsx @@ -11,6 +11,7 @@ export interface ChipBaseProps { dimensionName: string itemsText: string suffix?: string + isEmpty?: boolean isDragging?: boolean onClick: () => void } @@ -20,12 +21,16 @@ export const ChipBase: React.FC = ({ dimensionName, itemsText, suffix, + isEmpty, isDragging, onClick, }) => ( ) diff --git a/src/components/layout-panel/custom-value-modal/styles/custom-value-option.module.css b/src/components/layout-panel/custom-value-modal/styles/custom-value-option.module.css index a93229c5..cc9680f9 100644 --- a/src/components/layout-panel/custom-value-modal/styles/custom-value-option.module.css +++ b/src/components/layout-panel/custom-value-modal/styles/custom-value-option.module.css @@ -18,6 +18,10 @@ background-color: var(--colors-grey200); } +.option:active { + background-color: var(--colors-grey300); +} + .option.active { background-color: var(--colors-teal050); cursor: auto; @@ -38,20 +42,10 @@ text-overflow: ellipsis; } -.stageChip { +.stageLabel { flex-shrink: 0; margin-inline-start: var(--spacers-dp8); - padding: 5px 6px; - background-color: var(--colors-grey300); - border-radius: 3px; - color: var(--colors-grey900); + color: var(--colors-grey700); font-size: 13px; white-space: nowrap; } -.option.active .stageChip { - background-color: var(--colors-teal200); -} - -.option:hover:not(.active) .stageChip { - background-color: var(--colors-grey400); -} diff --git a/src/components/layout-panel/custom-value-modal/use-custom-value-data-elements.ts b/src/components/layout-panel/custom-value-modal/use-custom-value-data-elements.ts index 9f9bf7c8..9ea6da72 100644 --- a/src/components/layout-panel/custom-value-modal/use-custom-value-data-elements.ts +++ b/src/components/layout-panel/custom-value-modal/use-custom-value-data-elements.ts @@ -32,6 +32,9 @@ const getStageIdFromDimensionId = (id: string | undefined): string | null => { return idParts.length === 2 ? idParts[0] : null } +const compareByName = (a: DataElementDimension, b: DataElementDimension) => + a.name.localeCompare(b.name) + export const useCustomValueDataElements = () => { const { settings: { displayNameProperty }, @@ -101,24 +104,28 @@ export const useCustomValueDataElements = () => { } if (layoutStageId) { - return data.dimensions.filter( - (dim) => getStageIdFromDimensionId(dim.id) === layoutStageId - ) + return data.dimensions + .filter( + (dim) => getStageIdFromDimensionId(dim.id) === layoutStageId + ) + .sort(compareByName) } - return data.dimensions.map((dim) => { - const stageId = getStageIdFromDimensionId(dim.id) - if (!stageId || !programHasMultipleStages) { - return dim - } - const stage = metadataStore.getProgramStageMetadataItem(stageId) - if (!stage) { - throw new Error( - `Could not find stage with ID "${stageId}" in the metadata store` - ) - } - return { ...dim, stageName: stage.name } - }) + return data.dimensions + .map((dim) => { + const stageId = getStageIdFromDimensionId(dim.id) + if (!stageId || !programHasMultipleStages) { + return dim + } + const stage = metadataStore.getProgramStageMetadataItem(stageId) + if (!stage) { + throw new Error( + `Could not find stage with ID "${stageId}" in the metadata store` + ) + } + return { ...dim, stageName: stage.name } + }) + .sort(compareByName) }, [data, layoutStageId, metadataStore, programHasMultipleStages]) return { diff --git a/src/components/layout-panel/top-bar/visualization-type-selector/styles/visualization-type-selector.module.css b/src/components/layout-panel/top-bar/visualization-type-selector/styles/visualization-type-selector.module.css index dec0a980..4053ef8b 100644 --- a/src/components/layout-panel/top-bar/visualization-type-selector/styles/visualization-type-selector.module.css +++ b/src/components/layout-panel/top-bar/visualization-type-selector/styles/visualization-type-selector.module.css @@ -7,7 +7,7 @@ display: flex; align-items: center; gap: var(--spacers-dp4); - padding: 6px 8px; + padding: 6px 4px 6px 8px; block-size: 100%; border: none; border-radius: 5px; diff --git a/src/components/sidebar/__tests__/sidebar-disabling.spec.tsx b/src/components/sidebar/__tests__/sidebar-disabling.spec.tsx index 68218ea8..cac45958 100644 --- a/src/components/sidebar/__tests__/sidebar-disabling.spec.tsx +++ b/src/components/sidebar/__tests__/sidebar-disabling.spec.tsx @@ -293,7 +293,7 @@ describe('useCardDisabledNoticeText', () => { }) ) expect(result.current.enrollment).toBeUndefined() - expect(result.current.pi).toMatch(/Program indicators/) + expect(result.current.pi).toMatch(/Cannot be used with Pivot table/) }) it('returns undefined for non-sidebar card ids (metadata, other)', async () => { @@ -405,7 +405,7 @@ describe('useCardDisabledNoticeText', () => { }, }) ) - expect(result.current).toMatch(/Program indicators/) + expect(result.current).toMatch(/Cannot be used with Pivot table/) }) }) diff --git a/src/components/sidebar/data-source-select/data-source-select-combobox.tsx b/src/components/sidebar/data-source-select/data-source-select-combobox.tsx index 47117430..4fc06be1 100644 --- a/src/components/sidebar/data-source-select/data-source-select-combobox.tsx +++ b/src/components/sidebar/data-source-select/data-source-select-combobox.tsx @@ -2,7 +2,6 @@ import i18n from '@dhis2/d2-i18n' import { IconChevronDown16, IconChevronUp16, - IconDimensionEventDataItem16, IconErrorFilled24, theme, } from '@dhis2/ui' @@ -57,7 +56,6 @@ export const DataSourceSelectCombobox: FC = ({ [classes.empty]: !dataSourceMetadata, })} > - {!!dataSourceMetadata && } {dataSourceMetadata?.name ?? i18n.t('Choose a data source')} diff --git a/src/components/sidebar/data-source-select/styles/data-source-select-combobox.module.css b/src/components/sidebar/data-source-select/styles/data-source-select-combobox.module.css index 43d67b4a..fa94f7b7 100644 --- a/src/components/sidebar/data-source-select/styles/data-source-select-combobox.module.css +++ b/src/components/sidebar/data-source-select/styles/data-source-select-combobox.module.css @@ -13,7 +13,7 @@ align-items: center; border: 1px solid transparent; border-radius: 3px; - padding-block: 2px; + padding-block: 3px 1px; padding-inline: 8px 4px; min-block-size: 28px; justify-content: space-between; diff --git a/src/components/sidebar/dimension-card/card-disabled-notice.tsx b/src/components/sidebar/dimension-card/card-disabled-notice.tsx index 32d652da..3572e82d 100644 --- a/src/components/sidebar/dimension-card/card-disabled-notice.tsx +++ b/src/components/sidebar/dimension-card/card-disabled-notice.tsx @@ -1,5 +1,6 @@ -import { NoticeBox } from '@dhis2/ui' +import { IconInfo16 } from '@dhis2/ui' import type { FC } from 'react' +import classes from './styles/card-disabled-notice.module.css' type CardDisabledNoticeProps = { message: string @@ -8,9 +9,10 @@ type CardDisabledNoticeProps = { export const CardDisabledNotice: FC = ({ message, }) => ( -
- - {message} - +
+ + + + {message}
) diff --git a/src/components/sidebar/dimension-card/dimension-card-subsection.tsx b/src/components/sidebar/dimension-card/dimension-card-subsection.tsx index 832852c3..13226581 100644 --- a/src/components/sidebar/dimension-card/dimension-card-subsection.tsx +++ b/src/components/sidebar/dimension-card/dimension-card-subsection.tsx @@ -31,11 +31,10 @@ export const DimensionsCardSubsection: FC = ({ type="button" className={cx(classes.header, { [classes.collapsed]: isCollapsed, + [classes.disabled]: isDisabled, })} onClick={handleToggle} aria-expanded={!isCollapsed} - disabled={isDisabled} - tabIndex={isDisabled ? -1 : 0} data-test="dimension-card-subsection-header" > {} diff --git a/src/components/sidebar/dimension-card/dimension-card.tsx b/src/components/sidebar/dimension-card/dimension-card.tsx index 97c13918..42272ebb 100644 --- a/src/components/sidebar/dimension-card/dimension-card.tsx +++ b/src/components/sidebar/dimension-card/dimension-card.tsx @@ -68,10 +68,10 @@ export const DimensionCard: FC = ({ return ( <> - {noticeText && }
@@ -98,6 +98,7 @@ export const DimensionCard: FC = ({
+ {noticeText && } ) } diff --git a/src/components/sidebar/dimension-card/styles/card-disabled-notice.module.css b/src/components/sidebar/dimension-card/styles/card-disabled-notice.module.css new file mode 100644 index 00000000..72371e77 --- /dev/null +++ b/src/components/sidebar/dimension-card/styles/card-disabled-notice.module.css @@ -0,0 +1,30 @@ +.notice { + display: flex; + align-items: flex-start; + gap: 4px; + /* Cancels the card list's row gap so the notice sits flush against the + * bottom of the card it belongs to. */ + margin-block-start: -8px; + padding-block: 6px; + padding-inline: 8px; + background-color: transparent; + border: 1px solid var(--colors-grey300); + border-block-start: none; + border-end-start-radius: 5px; + border-end-end-radius: 5px; +} + +.icon { + flex-shrink: 0; + display: inline-flex; + margin-block-start: 0px; +} +.icon svg { + color: var(--colors-grey500); +} + +.message { + font-size: 12px; + line-height: 16px; + color: var(--colors-grey600); +} diff --git a/src/components/sidebar/dimension-card/styles/dimension-card-header.module.css b/src/components/sidebar/dimension-card/styles/dimension-card-header.module.css index 13707164..28319da4 100644 --- a/src/components/sidebar/dimension-card/styles/dimension-card-header.module.css +++ b/src/components/sidebar/dimension-card/styles/dimension-card-header.module.css @@ -4,7 +4,7 @@ min-block-size: 29px; display: flex; align-items: center; - background-color: var(--colors-white); + background-color: transparent; border-radius: 4px 4px 0 0; padding: 10px 6px 10px 4px; cursor: pointer; @@ -20,7 +20,7 @@ background-color: var(--colors-grey100); } .container.disabled:hover { - background-color: var(--colors-grey300); + background-color: var(--colors-grey100); } .container:focus { outline: 3px solid var(--theme-focus); @@ -38,7 +38,7 @@ font-size: 13px; line-height: 16px; font-weight: 500; - color: var(--colors-grey700); + color: var(--colors-grey800); } .count { diff --git a/src/components/sidebar/dimension-card/styles/dimension-card-subsection.module.css b/src/components/sidebar/dimension-card/styles/dimension-card-subsection.module.css index 2ca39c12..dde1b9a1 100644 --- a/src/components/sidebar/dimension-card/styles/dimension-card-subsection.module.css +++ b/src/components/sidebar/dimension-card/styles/dimension-card-subsection.module.css @@ -17,6 +17,9 @@ border-end-start-radius: 4px; border-end-end-radius: 4px; } +.header.disabled { + opacity: 0.5; +} .header:hover { background-color: var(--colors-grey100); } @@ -36,7 +39,7 @@ font-size: 13px; line-height: 16px; font-weight: 500; - color: var(--colors-grey900); + color: var(--colors-grey800); } .count { flex-grow: 0; diff --git a/src/components/sidebar/dimension-card/styles/dimension-card.module.css b/src/components/sidebar/dimension-card/styles/dimension-card.module.css index ef112ec0..cc286fe5 100644 --- a/src/components/sidebar/dimension-card/styles/dimension-card.module.css +++ b/src/components/sidebar/dimension-card/styles/dimension-card.module.css @@ -5,9 +5,17 @@ border: 1px solid var(--colors-grey400); border-radius: 5px; } +.container.hasNotice { + border-end-start-radius: 0; + border-end-end-radius: 0; +} .container.isDisabled { border-color: var(--colors-grey300); - background-color: var(--colors-grey050); + background-color: color-mix( + in srgb, + var(--colors-white) 50%, + transparent 50% + ); } .container:not(.isDisabled):hover { border-color: #b7c4d2; diff --git a/src/components/sidebar/dimension-card/styles/dimension-list.module.css b/src/components/sidebar/dimension-card/styles/dimension-list.module.css index 8a885df8..adcdf5ff 100644 --- a/src/components/sidebar/dimension-card/styles/dimension-list.module.css +++ b/src/components/sidebar/dimension-card/styles/dimension-list.module.css @@ -35,7 +35,7 @@ line-height: 16px; } .noResults { - padding: 0 8px; + padding: 0 8px 4px; color: var(--colors-grey600); text-align: start; font-size: 12px; diff --git a/src/components/sidebar/dimension-item/styles/dimension-item-container.module.css b/src/components/sidebar/dimension-item/styles/dimension-item-container.module.css index 9d35e0eb..619b3311 100644 --- a/src/components/sidebar/dimension-item/styles/dimension-item-container.module.css +++ b/src/components/sidebar/dimension-item/styles/dimension-item-container.module.css @@ -37,9 +37,10 @@ background-color: #bbe2e0; } -.container.dragging { +.container.dragging:active { background-color: var(--colors-grey100); border-color: var(--colors-grey400); + opacity: 0.9; } .container.multiSelected, @@ -63,6 +64,13 @@ padding-inline-end: 4px; } +/* The drag overlay tracks the cursor, so it's the element under the pointer + * for the whole drag. Override the inner item's cursor so it reads grabbing. */ +.container.dragOverlay, +.container.dragOverlay * { + cursor: grabbing; +} + .container.disabled { cursor: not-allowed; opacity: 0.5; diff --git a/src/components/sidebar/sidebar-disabling.ts b/src/components/sidebar/sidebar-disabling.ts index dcc3f928..2513977b 100644 --- a/src/components/sidebar/sidebar-disabling.ts +++ b/src/components/sidebar/sidebar-disabling.ts @@ -48,10 +48,9 @@ const differentTetMessage = (tetName: string): string => const programIndicatorsVisTypeMessage = ( visualizationType: VisualizationType ): string => - i18n.t( - 'Program indicators are not valid with {{visType}}. Switch to Line list to use them.', - { visType: visTypeDisplayNames[visualizationType] } - ) + i18n.t('Cannot be used with {{visType}}.', { + visType: visTypeDisplayNames[visualizationType], + }) /* The three data-source branches below directly express which cards a * particular data source's invalid-layout states disable and which card diff --git a/src/components/sidebar/sidebar.tsx b/src/components/sidebar/sidebar.tsx index 4bfce8d0..fbecd2da 100644 --- a/src/components/sidebar/sidebar.tsx +++ b/src/components/sidebar/sidebar.tsx @@ -58,6 +58,7 @@ export const Sidebar: FC = () => {
{isDataSourceProgramWithRegistration( dataSourceMetadataItem diff --git a/src/components/sidebar/styles/sidebar.module.css b/src/components/sidebar/styles/sidebar.module.css index c34f43f0..ad398b33 100644 --- a/src/components/sidebar/styles/sidebar.module.css +++ b/src/components/sidebar/styles/sidebar.module.css @@ -8,7 +8,6 @@ block-size: calc(100% - 8px); border: 1px solid var(--colors-grey400); border-radius: 5px; - padding-block-end: 4px; background-color: var(--colors-grey100); overflow: hidden; } @@ -61,6 +60,7 @@ gap: 8px; padding-block-start: 8px; padding-inline: 4px; + padding-block-end: 4px; margin-inline-end: 2px; transition: scrollbar-color 0.18s ease; } diff --git a/src/components/toolbar/title-bar/styles/title-bar.module.css b/src/components/toolbar/title-bar/styles/title-bar.module.css index ca1ab711..914cde13 100644 --- a/src/components/toolbar/title-bar/styles/title-bar.module.css +++ b/src/components/toolbar/title-bar/styles/title-bar.module.css @@ -14,7 +14,7 @@ .cell { display: flex; min-inline-size: 0; - align-items: center; + align-items: baseline; background-color: var(--colors-white); padding: 6px; border-radius: 5px; @@ -22,10 +22,11 @@ .title { font-size: 13px; - line-height: 1; + line-height: 1.2; font-weight: 500; color: var(--colors-grey800); min-inline-size: 0; + margin-block-start: 2px; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; @@ -43,13 +44,16 @@ } .suffix.edited { - color: var(--colors-grey800); - padding: 2px 4px; - border-radius: 3px; - background-color: var(--colors-grey200); - margin-block-start: -1px; + color: var(--colors-grey600); + margin-block-start: 2px; + margin-inline-start: 4px; font-size: 11px; letter-spacing: -0.2px; font-family: monospace; line-height: normal; } + +.suffix.edited::before { + content: '·'; + margin-inline-end: 4px; +}