Skip to content

feat(vue-mri): bulk selection and bulk actions on Data Exploration (#3127) - #3313

Open
khairul-syazwan wants to merge 5 commits into
khairul-syazwan/d2e-exploration-pagingfrom
khairul-syazwan/d2e-exploration-bulk
Open

feat(vue-mri): bulk selection and bulk actions on Data Exploration (#3127)#3313
khairul-syazwan wants to merge 5 commits into
khairul-syazwan/d2e-exploration-pagingfrom
khairul-syazwan/d2e-exploration-bulk

Conversation

@khairul-syazwan

Copy link
Copy Markdown
Collaborator

Closes #3127.

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

What this adds

Bulk selection on the Data Exploration page. Ticking one or more cards replaces the search toolbar with a bulk action bar: a select-all checkbox, a live count, and Compare and Delete.

Piece Where
Selection arithmetic, pure and unit-tested helpers/explorationSelection.ts
Store: count, hasSelection, setPageSelection, retain stores/explorations.ts
The bar, and the two actions ExplorationsPage.vue
The three-way delete branch, extracted helpers/deleteExploration.ts
The bulk delete loop helpers/bulkDeleteExplorations.ts
Selected-card border libs/d2e-ui/.../D2eExplorationCard.vue

Select-all is page-scoped, and the selection persists

The grid pages client-side at 12. Select-all ticks the current page; the selection survives a page change, so a user can collect cards from several pages, and the count is the total.

The design frame predates pagination, so this was a product decision, taken with the ticket owner. It drives one non-obvious rule in the code: the stale-selection cleanup watches the whole filtered set, never the rendered page. A watcher on the page would drop the selection every time the user turned it.

Compare and delete resolve selected ids against the filtered set too, so a card selected on another page is still actionable.

Bulk delete

There is no batch endpoint, and deletion branches three ways by record type. So the loop:

  • deletes one at a time — three services, no transaction, and a parallel run makes a partial failure impossible to attribute;
  • reloads once at the end rather than per record;
  • clears the selection, and clears the active bookmark if one of the deleted records was loaded in the builder;
  • reports the records that failed. The single-delete path only wrote to the console, so this notification is new.

deleteExploration() throws on an unrecognised record type. The original skipped all three branches and then reloaded, closed and emitted anyway — reporting success for a record it had not deleted.

Figma

Frame 1821:224152. The read corrected five values in the original plan: the bar is 60px not 44, padding 12px 16px not 8px 24px, gaps 16px and 8px not a single 12px, the count is primary navy not black, and the bar carries a neutral-lightest background with top and bottom borders that the plan omitted.

The frame holds a third button, Analyze, drawn at opacity: 0. A designer note reserves it for future work where several wizards run across cohorts. Compare and Delete are the whole scope, deliberately.

The frame paints Delete #A70300, which matches no token and comes from a Figma variable named Note that resolves to a blue elsewhere in the same file. Built with the existing danger token, which is what the confirmation dialog this button opens already uses. Three reds now exist for one meaning (#A3293D, #D53939, #A70300) and that needs a design decision.

The frame also settles the indeterminate select-all: it draws that state, and D2eCheckbox already paints it.

Review findings, fixed in this branch

A code review and a manual pass each found defects.

  • Compare could latch dead. CohortComparisonDialog watches the trigger for a false-to-true edge and emits closeEv only when it actually opens. Above its own ten-item cap it warns and stays shut, so the flag stayed true and every later click was a no-op. Page size is 12, so one select-all reaches this. Fixed by lowering the flag before raising it.
  • Failures were tracked by display name, and two never-materialised records can share one. A successful delete whose name matched a failed one was read as failed, leaving the active bookmark pointing at a deleted record. Now tracked by identity, with a test that fails against the old matching.
  • Two loaders during a delete. fireBookmarkQuery raises the page-wide loading flag on every call, a delete included, so the grid blanked behind the open dialog while that dialog showed its own spinner. The full-page spinner now only replaces the grid when there is nothing to show. This applied to single delete too.
  • The failure message was concatenated, baking in English word order. It now uses the locale string's {0}.
  • The count is a live region. It changes as cards are ticked and nothing else announced it.

Known issues, not fixed here

  • Per-record toasts still stack on a bulk delete. fireDeleteMaterializedCohortQuery and fireDeleteAtlasCohortDefinitionQuery toast unconditionally and accept only an id, so suppressing them means changing a shared store module and its other callers — wider than this ticket. Deleting several records shows a toast each, on top of the single failure summary. Worth its own change.
  • Focus is dropped when the bar unmounts. The bar and toolbar swap with v-if/v-else, so clearing the last selection destroys whatever held focus. Needs an explicit focus move, plus its own browser pass to verify.
  • ExplorationsPage.vue is 1398 lines, past the 800 the style guide allows. It was already 1159 before this branch. The bar should become ExplorationsBulkBar.vue, following ExplorationPagination.vue and ExplorationEmptyState.vue. Deferred rather than restructure a file straight after it was manually verified.
  • Compare gives no reason when disabled below two comparable records.
  • German and Chinese strings are machine-written and carry TODO(i18n) markers.

Validation

Reported honestly; this is not a claim of full end-to-end coverage.

Check Result
Unit tests, Node 20, the command CI runs 1055 passed, 3 skipped. 46 new
vite build pass
vite build --config vite.config.atlas.ts pass
vite build --config vite.config.atlas-app.ts pass
d2e-ui: tokens:check, test, build pass, 50 tests
prettier --check on new files pass
eslint not run. plugins/ui hoists eslint 7.32, which cannot read the app's flat config
Manual browser pass on trex done, seeded past one page
no-mistakes gate not run. It does not follow a treehouse worktree and targets develop, not this stack's parent — the same path every PR in this stack has taken

Manual verification covered the bar appearing and replacing the toolbar, the indeterminate and full select-all states, the selection surviving a page change, a cross-page delete, and the loader defect above, which it found.

Cross-page selection and the mixed-type delete were exercised with a seeded list, not with the demo environment's three records — every defect in this feature is invisible at three.

Add the selection arithmetic, a page-scoped select-all, a live count and the
bulk action bar on the Data Exploration page. Compare and Delete render as
stubs; the next subphase wires their behaviour.

Select-all acts on the current page, and the selection survives a page change
so the user can collect cards from more than one page. The retain watcher
therefore reads the whole filtered set, never the rendered page: a watcher on
the page ids would drop the selection every time the user turned the page.

Selection outranks hover on the card border. `--clickable:hover` is a class
plus a pseudo-class, so without an explicit rule it repaints a selected card
and the card stops looking selected while the pointer is over it.

Extract toCardId into the list helper so the card view model and the bulk
selection build the namespaced id the same way and cannot drift apart.
Wire the two bulk actions the action bar left as stubs.

Compare reuses CohortComparisonDialog whole. Only a record carrying a bookmark
can be compared, and it needs more than one, so two Atlas records leave the
button disabled instead of opening an empty comparison. The dialog keeps its
own ten-item cap and its own warning.

Bulk delete extracts the single-delete branch into deleteExploration() and
loops it. The loop deletes one record at a time, because the three paths hit
three services with no transaction and a parallel run makes a partial failure
impossible to attribute. It reloads once at the end rather than once per
record, clears the selection, clears the active bookmark when one of the
deleted records was loaded in the builder, and reports the names that failed.

Selected ids resolve against the whole filtered set, not the rendered page, so
a record selected on another page is still deletable.

Partial failure now reaches the user. The single-delete path only wrote to the
console, so there was no notification path to reuse.

deleteExploration() throws on an unrecognised bookmark type. The original
skipped all three branches and then reloaded, closed and emitted anyway,
reporting success for a record it had not deleted.
fireBookmarkQuery raises the page-wide loading flag for every call, a delete
included, not only for loadAll. The page keyed its full-page spinner off that
flag directly, so the grid blanked to a spinner behind whichever delete dialog
was open while that dialog showed its own busy spinner. Bulk delete held both
on screen for the whole sequential run.

The full-page spinner now replaces the grid only while there is nothing to
show. A refresh keeps the rows on screen and lets the dialog own the feedback.
The pagination bar stops disappearing on every refresh for the same reason.

This applies to single delete too, which had the same two spinners for a
shorter time.
Compare could latch dead. CohortComparisonDialog watches the trigger flag for
a false-to-true edge, and it emits closeEv only when it actually opened. Above
its own ten-item cap it raises a warning and stays shut, so the flag stayed
true, the next click produced no edge, and Compare did nothing for the rest of
the page's life. The page size is 12, so one select-all already exceeds the cap
and reaches this. Lower the flag before raising it, so every click is an edge.

Bulk delete tracked its failures by display name, and two never-materialised
records can share one. A successful delete whose name matched a failed one was
read as failed, so the active bookmark was left pointing at a record that had
already gone. Track the failed records by identity instead. A test covers the
shared-name case, and it fails against the old name matching.

The failure message was built by concatenation, which baked English word order
into the code one line after the count string correctly used a placeholder.
The names now go through the locale string's own {0}, in all three locales.

The selection count is a live region. It changes as the user ticks cards and
nothing else on screen announced it.
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