Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
12 changes: 4 additions & 8 deletions i18n/en.pot
Original file line number Diff line number Diff line change
Expand Up @@ -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}}"
Expand Down Expand Up @@ -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 "
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,13 @@ const DragOverlayItem: FC<
chipClasses.dragging,
classes.overlay,
{
[chipClasses.chipEmpty]:
!!data.overlayItemProps.itemsText,
[chipClasses.chipEmpty]: data.overlayItemProps.isEmpty,
}
)}
>
<ChipBase {...data.overlayItemProps} isDragging />
<div className={chipClasses.content}>
<ChipBase {...data.overlayItemProps} isDragging />
</div>
</div>
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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),
}
Comment thread
HendrikThePendric marked this conversation as resolved.

export const DndContextProvider: FC<PropsWithChildren> = ({ children }) => {
// Wait 15px movement before starting drag, so that click event isn't overridden
const sensor = useSensor(PointerSensor, activateAt15pixels)
Expand All @@ -18,6 +30,7 @@ export const DndContextProvider: FC<PropsWithChildren> = ({ children }) => {

return (
<DndContext
autoScroll={autoScrollOptions}
collisionDetection={collisionDetector}
sensors={sensors}
onDragEnd={onDragEnd}
Expand Down
36 changes: 36 additions & 0 deletions src/components/layout-panel/axis/__tests__/chip.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,42 @@ describe('<Chip />', () => {
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(
<MockAppWrapper {...appWrapperProps}>
<Chip dimension={dimension} axisId="filters" />
</MockAppWrapper>
)

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'
Expand Down
12 changes: 10 additions & 2 deletions src/components/layout-panel/axis/chip-base.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export interface ChipBaseProps {
dimensionName: string
itemsText: string
suffix?: string
isEmpty?: boolean
isDragging?: boolean
onClick: () => void
}
Expand All @@ -20,12 +21,16 @@ export const ChipBase: React.FC<ChipBaseProps> = ({
dimensionName,
itemsText,
suffix,
isEmpty,
isDragging,
onClick,
}) => (
<button
type="button"
className={cx(classes.chipBase, { [classes.dragging]: isDragging })}
className={cx(classes.chipBase, {
[classes.empty]: isEmpty,
[classes.dragging]: isDragging,
})}
onClick={onClick}
>
{dimensionType && (
Expand All @@ -36,7 +41,10 @@ export const ChipBase: React.FC<ChipBaseProps> = ({
<span className={classes.label}>
<span className={classes.primary}>{dimensionName}</span>
{suffix && (
<span className={classes.secondary}>{`· ${suffix}`}</span>
<span
className={classes.secondary}
data-test="chip-suffix"
>{`· ${suffix}`}</span>
)}
</span>
<span className={classes.items} data-test="chip-items">
Expand Down
9 changes: 4 additions & 5 deletions src/components/layout-panel/axis/chip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export const Chip: FC<ChipProps> = ({ dimension, axisId }) => {
dimension,
formatValueOptions: { digitGroupSeparator },
})
const isEmpty = axisId === 'filters' && items.length === 0 && !hasConditions
const chipItemsText = useMemo(
() =>
getChipItemsText({
Expand All @@ -85,9 +86,10 @@ export const Chip: FC<ChipProps> = ({ dimension, axisId }) => {
dimensionName: dimension.name,
suffix: dimension.suffix,
itemsText: chipItemsText,
isEmpty,
onClick: openDimensionModal,
}),
[dimension, chipItemsText, openDimensionModal]
[dimension, chipItemsText, isEmpty, openDimensionModal]
)

const {
Expand All @@ -111,10 +113,7 @@ export const Chip: FC<ChipProps> = ({ dimension, axisId }) => {
>
<div
className={cx(classes.chip, {
[classes.chipEmpty]:
axisId === 'filters' &&
items.length === 0 &&
!hasConditions,
[classes.chipEmpty]: isEmpty,
[classes.active]: isDragging,
[classes.showBlank]: !dimension.name,
})}
Expand Down
18 changes: 16 additions & 2 deletions src/components/layout-panel/axis/styles/chip-base.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@
background: #cdeae8;
}

.chipBase.empty {
background-color: var(--colors-grey100);
color: var(--colors-grey900);
}

.chipBase.empty:hover {
background-color: var(--colors-grey200);
}

.label {
white-space: nowrap;
display: flex;
Expand All @@ -41,6 +50,10 @@
white-space: nowrap;
}

.empty .secondary {
color: var(--colors-grey600);
}

.items {
white-space: nowrap;
flex-shrink: 0;
Expand All @@ -54,8 +67,9 @@
padding: 2px 2px 1px 2px;
}

.dragging .items {
background-color: var(--colors-grey300);
.empty .items {
background: #eff1f3;
color: var(--colors-grey900);
}

.items:empty {
Expand Down
19 changes: 7 additions & 12 deletions src/components/layout-panel/axis/styles/chip.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -38,24 +38,19 @@
border-color: var(--colors-grey400);
}

.chipEmpty .chipBase {
color: var(--colors-grey900);
}

.chipEmpty .secondary {
color: var(--colors-grey600);
}

.chipEmpty .suffix {
background: #eff1f3;
color: var(--colors-grey900);
}
.dragging {
margin: 0;
align-items: center;
min-block-size: 20px;
opacity: 0.9;
}

/* The drag overlay tracks the cursor, so it's the element under the pointer
* for the whole drag. Override the inner cursor so it reads grabbing. */
.chip.dragging,
.chip.dragging * {
cursor: grabbing;
}
.showBlank .content {
visibility: hidden;
min-inline-size: 100px;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ import {
type VisUiConfigState,
type CustomValueObject,
} from '@store/vis-ui-config-slice'
import { renderHookWithAppWrapper } from '@test-utils/app-wrapper'
import {
renderHookWithAppWrapper,
type MockOptions,
} from '@test-utils/app-wrapper'
import { createDeferredQuery } from '@test-utils/deferred-query'
import { waitFor } from '@testing-library/react'
import type { RootState } from '@types'
import deepmerge from 'deepmerge'
Expand Down Expand Up @@ -146,6 +150,42 @@ describe('useCustomValueDataElements', () => {
expect(result.current.filteredByStageName).toBeUndefined()
})

it('sorts data elements alphabetically by name regardless of API order', async () => {
const outOfOrderResponse = {
dimensions: [
{
id: 's2.de2',
name: 'DE 2',
aggregationType: 'AVERAGE',
dimensionType: 'DATA_ELEMENT',
},
{
id: 's1.de1',
name: 'DE 1',
aggregationType: 'SUM',
dimensionType: 'DATA_ELEMENT',
},
],
}
const { result } = await renderHookWithAppWrapper(
() => useCustomValueDataElements(),
{
...buildMockOptions({ columns: ['p1.enrollmentDate'] }),
queryData: {
[ANALYTICS_RESOURCE]: outOfOrderResponse,
},
}
)

await waitFor(() => {
expect(result.current.dataElements).toBeDefined()
})

expect(
result.current.dataElements?.map((dataElement) => dataElement.name)
).toEqual(['DE 1', 'DE 2'])
})

it('omits stageName when the layout has no program stage and the program has only one stage', async () => {
const singleStage = {
id: 'sX',
Expand Down Expand Up @@ -298,12 +338,29 @@ describe('useCustomValueDataElements', () => {
})

it('returns undefined dataElements while loading', async () => {
/* Hold the dimensions request in flight so the loading assertion is
* deterministic. Without this, the query can resolve during the
* wrapper's internal store wait, flipping isLoading to false before
* the assertion under full-suite load. */
const deferredDimensions = createDeferredQuery()
const { result } = await renderHookWithAppWrapper(
() => useCustomValueDataElements(),
buildMockOptions({ columns: ['p1.enrollmentDate'] })
{
...buildMockOptions({ columns: ['p1.enrollmentDate'] }),
queryData: {
[ANALYTICS_RESOURCE]: deferredDimensions.defer(
() => analyticsResponse
),
} as MockOptions['queryData'],
}
)

expect(result.current.isLoading).toBe(true)
expect(result.current.dataElements).toBeUndefined()

await deferredDimensions.releaseAll()
await waitFor(() => {
expect(result.current.dataElements).toBeDefined()
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,6 @@ export const CustomValueOption: FC<CustomValueOptionProps> = ({
)}
</svg>
<span className={classes.label}>{label}</span>
{stageName && <span className={classes.stageChip}>{stageName}</span>}
{stageName && <span className={classes.stageLabel}>{stageName}</span>}
</button>
)
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
}
Loading
Loading