Skip to content

feat(vue-mri): filter the exploration list by author, status and date (#3150) - #3263

Open
khairul-syazwan wants to merge 6 commits into
khairul-syazwan/d2e-exploration-pagefrom
khairul-syazwan/d2e-exploration-filters
Open

feat(vue-mri): filter the exploration list by author, status and date (#3150)#3263
khairul-syazwan wants to merge 6 commits into
khairul-syazwan/d2e-exploration-pagefrom
khairul-syazwan/d2e-exploration-filters

Conversation

@khairul-syazwan

Copy link
Copy Markdown
Collaborator

Closes #3150.

Fifth in the Data Exploration redesign stack. Based on khairul-syazwan/d2e-exploration-page (#3262), not develop — review that one first.

What this adds

A Filters button in the exploration toolbar, right of the search field, opening a 420px panel anchored to its right edge. The panel filters the list by author, materialisation status, and three From / To date ranges.

Filtering is client-side: the list endpoint accepts no filter parameters and the whole list is already in memory. The order is filter, then search, then sort.

Piece Where
D2eDateField — a single date input, ISO YYYY-MM-DD in and out libs/d2e-ui
D2eMenu default slot, so an activator can anchor arbitrary content libs/d2e-ui
D2eSelect searchable prop, rendering a VAutocomplete libs/d2e-ui
Filter shape, option list and predicate, as pure functions apps/vue-mri-ui-lib
ExplorationFiltersPanel.vue, the Filters button, i18n apps/vue-mri-ui-lib

Decisions a diff cannot show

The CREATED range is disabled on purpose. A Bookmark carries only dateModified; CohortDefinition.createdOn is the materialisation instant, and the Atlas createdOn covers Atlas records only. Filtering dateModified and labelling it "created" would give wrong results that look right. The control renders with a note saying why, and matchesFilters honours the range already, so it starts working with no logic change once a creation timestamp is surfaced. The audit columns exist on the user_artifact row but formatUserArtifactData does not expose them — no ticket for that yet.

Two materialisation statuses, not three. Derived from the presence of a cohort definition, which is the same test getBookmarkType() makes. Stale is not derivable from any field today and belongs to #3117.

LAST UPDATED does not reuse lastUpdatedMs(). That helper falls through to cohortDefinition.createdOn so sorting has a total order, but the card's own "Last updated" row stops at the bookmark and Atlas fields. A materialised record with neither reads - on the card, so matching it on its materialisation instant would drop or keep it for a reason nothing on screen explains — and would make LAST UPDATED and LAST MATERIALIZED the same filter for those records. The filter reads what the card reads; sorting keeps the three-field chain.

D2eDateField composes VTextField + VMenu + VDatePicker rather than using VDateInput. VDateInput does exist in the pinned Vuetify 3.12.0. Vuetify exposes labs only as vuetify/labs/<Name>, and this repo's vite alias list rewrites vuetify/components and vuetify/directives and nothing else, so a bare labs import from libs/d2e-ui resolves by plain node lookup and breaks the ATLAS isolated install (npm install --workspaces=false puts vuetify below the library, not above it). Adding a fourth alias to three vite configs for one labs component that can change between minors was the worse trade.

The author option list reads the unfiltered list. Deriving it from the visible cards would remove every other option as soon as one author is selected, and the filter could never be widened again.

Two bugs fixed in shared components

The outline notch struck through the floating Data source label. D2eSelect restated Vuetify's notch borders and gave both ::before and ::after a top border. Vuetify draws the top rule on ::before and the bottom rule on ::after, and only ::before takes opacity: 0 once the label floats — so the reworked ::after survived and drew a line across the label. The restatement is deleted; Vuetify's own rules already read --v-field-border-width.

Field text was not vertically centred, from two separate causes. D2eSelect's <input> never inherited a font, so the UA's 13.33px Arial made it 17px tall while its placeholder painted at 16px. And Vuetify takes a VSelect's input out of flex flow (position: absolute with align-self: flex-start), so align-items: center could not reach it. D2eDateField's input took its intrinsic 34px and sat flush against the top of the 40px box. All three fields now measure zero drift against the field centre.

A trap worth knowing

A teleported Vuetify overlay lands in .v-overlay-container, a direct child of <body>, outside .mri-app-vue-container. Every rule scoped to that container stops applying — including style.scss's compensation for $reset: false, which is the only thing removing the browser's default 2px inset border from an <input>. Without it the border returns and draws a box around every placeholder. The panel carries its own unscoped block to replace it, the way ExplorationsPage.vue already does for tooltips. This applies to any Vuetify overlay, so the dialogs in #3259 are worth a look.

Validation evidence — agent reported

Run in the working checkout, not in CI. No end-to-end suite was run; do not read this as full E2E coverage.

  • libs/d2e-ui: bun run tokens:check, bun run test (50 tests), bun run build, bun run verify:dist — all pass.
  • apps/vue-mri-ui-lib: npx vitest run — 947 pass, 1 fail. The failure is app-segmented-button, a known local-environment artefact unrelated to this change; it passes in CI.
  • Filter helper tests pass under TZ=Pacific/Auckland, America/Los_Angeles, UTC and Asia/Singapore. The date comparison is on the local calendar day and never uses toISOString(), which shifts the day east of UTC.
  • All three app builds pass: vite build, --config vite.config.atlas.ts, --config vite.config.atlas-app.ts.
  • Histoire explorer builds: 11 stories, 54 variants.
  • Lint not run. plugins/ui/node_modules hoists eslint 7.32, which cannot read the app's flat eslint.config.js.

Browser checks, hot-deployed and driven with Playwright against a local runtime, 19 assertions:

  • Materialized-only empties the grid and shows the empty state; Not-materialized-only keeps all three records. All three are Not run yet, so that is the real complement.
  • A LAST UPDATED To of 2025-07-01 empties the grid; Clear all selections restores it.
  • Typing adm in Author narrows to the one match; zzz shows no data.
  • Setting From disables the ten earlier days in the To picker.
  • The panel survives picking a date; Escape and click-outside both close it; Enter on the last field opens that field's picker instead of closing the panel.
  • Search still composes with the filters. No page errors.

Measured geometry against the design frame: panel 420 wide with its right edge on the button's, selects 388x40, date fields 186x40.

Reviewer notes

  • The panel is 453px tall against the design's 432. The difference is the explanatory note under the disabled CREATED pair, which the frame does not draw.
  • Placeholders render italic, from an app-global rule that also affects the shipped search field. Left alone; changing it is a page-wide typographic change.
  • The date fields show the raw ISO value while the cards show 3 Sept 2026. Locale-aware formatting is deferred to design; the fix belongs in the library component, which must not import the app's DateUtils.
  • Filtering to nothing shows "No explorations yet.", which reads as "you have none" rather than "none match". Pre-existing and shared with search.
  • de and zh strings need a native-speaker review.

@khairul-syazwan
khairul-syazwan marked this pull request as ready for review September 7, 2026 02:10
D2eDateField is a single date input with a leading calendar icon, for
#3150's filter panel, which uses six of them in three From/To pairs.
There is no range picker in the design.

It composes VTextField, VMenu and VDatePicker rather than using the
labs VDateInput. VDateInput exists in the pinned Vuetify 3.12.0, but
vuetify's export map only exposes labs as vuetify/labs/<Name>, and the
app aliases vuetify/components and vuetify/directives and nothing else.
A bare labs import from libs/d2e-ui therefore resolves by plain node
lookup and breaks the ATLAS isolated install, which puts vuetify under
the app rather than above the library.

The model value is an ISO YYYY-MM-DD string or null, in and out. The
two conversion helpers are a plain .ts file so the test does not mount
anything, and they read a Date's local parts instead of calling
toISOString(), which returns the previous day east of UTC.

D2eMenu gains a default slot so an activator can anchor arbitrary
content. With no default slot the items path is unchanged.
Closes the Filters button the exploration toolbar left space for. It
opens a 420px panel below it, anchored to its right edge, holding an
author multi-select, a materialization-status multi-select and three
From/To date pairs.

Filtering is client-side; the list endpoint takes no filter parameters
and the whole list is already in memory. The order is filter, then
search, then sort. The author option list reads the unfiltered list, or
selecting one author would remove every other option and the filter
could not be widened again.

Materialization status is derived from the presence of a cohort
definition, the same test getBookmarkType() makes. It yields two
options; stale is not derivable today and belongs to #3117.

The CREATED pair renders disabled with a note. A bookmark carries only
dateModified, so there is nothing to compare against; the predicate
honours the range anyway, so the filter starts working once the backend
surfaces a creation timestamp. Filtering dateModified and labelling it
created would give wrong results that look right.

de and zh strings need native review.
The other range tests use lastMaterialized, which reads a date field
directly. lastUpdated goes through lastUpdatedMs(), which returns
milliseconds and falls back across three fields, so that path had no
coverage.
D2eDateField stopped inheriting attrs. Its root is a VMenu, which does
not stop inheritance either, so a fallthrough attr travelled on to
VOverlay and was merged onto the teleported .v-overlay div: the story's
width sized the popup instead of the field, and a data-testid existed on
two nodes once the picker had opened. forwardAttrs already puts every
attr on the field.

D2eDateField gained an opt-in clearable, and the panel turns it on.
VDatePickerMonth in single mode always assigns the clicked day and never
deselects, so a picked date could not be dropped short of Clear all
selections, which also wipes the author and status filters.

EMPTY_FILTERS is frozen through its nested ranges, not only at the top
level. The comment promised a loud failure; a shallow freeze left
EMPTY_FILTERS.created writable, which is the exact object the bug it
describes corrupts.

Enter on Clear all selections now clears. D2eMenu sets
close-on-content-click false, and Vuetify's VMenu keydown handler then
calls preventDefault() on Enter, cancelling the browser's
Enter-activates-a-button default.
…d typeahead

Three defects reported from manual verification.

The Author filter is now searchable, so a growing author list can be
narrowed by typing. D2eSelect gained a searchable prop that renders a
VAutocomplete in place of a VSelect; everything else about the component
is unchanged, and the prop is off by default because a fixed two-item
list is faster to click than to type.

Field text was not vertically centred. Two separate causes. In
D2eSelect the <input> never inherited a font, so the UA's 13.33px Arial
made the element 17px tall while its placeholder painted at 16px, and
the two disagreed about the baseline; the input now takes the field's
font and the input row fills the box with no vertical padding. In
D2eDateField the field's own <input> took its intrinsic 34px and sat
flush against the top edge of the 40px box, leaving 6px below it. Both
now measure zero drift against the field's centre.

The outline notch drew a line through the floating 'Data source' label.
D2eSelect restated Vuetify's notch borders and gave BOTH ::before and
::after a top border. Only ::before takes Vuetify's opacity: 0 when the
label floats, so the reworked ::after survived and struck the label
through. Vuetify's own rules already read --v-field-border-width, so
the restatement is removed rather than corrected. D2eDateField carried
a copy of the same rule and is cleaned up with it.
VSelect no longer sits 9px above centre. Vuetify takes a VSelect's
<input> out of flex flow (position: absolute with align-self:
flex-start), so align-items: center on the row cannot reach it and it
hangs from the top of the content box; removing the vertical padding
moved that anchor from 16px to 0 and made it worse, not better. An
absolutely positioned flex child takes its static position from
align-self, so that is what now centres it. Measured zero drift on the
select, the autocomplete and the date field.

The LAST UPDATED range filtered on a date the card never shows.
lastUpdatedMs() falls through to cohortDefinition.createdOn so that
sorting has a total order, but the card's Last updated row stops at the
bookmark and Atlas fields. A materialised record with no bookmark reads
'-' there and was still matched on its materialisation instant, which
also made LAST UPDATED and LAST MATERIALIZED the same filter for those
records. The filter now reads what the card reads.

Enter works in the date fields. A VMenu with close-on-content-click
false treats Enter on its content as move-to-next-focusable-then-close,
so Enter on the last field closed the whole panel, and its
preventDefault meant Enter never opened a picker. D2eDateField now
handles the key itself and keeps it from the enclosing menu.

The CREATED note claimed no creation date exists. Atlas cohort
definitions do carry one. Reworded in all three locales to say the
range is disabled because it cannot apply to every card.

menuProps.contentClass replaced Vuetify's own content class rather than
merging with it, because mergeProps special-cases only class, style and
on*. Vuetify's class is carried through by hand. This PR is the first
time a D2eSelect dropdown ever opens in the app.

D2eDateField merges the activator props instead of spreading them, so a
consumer's own click handler cannot silently replace the one that opens
the picker.

Date tests now cover a UTC timestamp, which is the format the store
actually produces.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant