feat(vue-mri): bulk selection and bulk actions on Data Exploration (#3127) - #3313
Open
khairul-syazwan wants to merge 5 commits into
Open
Conversation
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.
khairul-syazwan
requested review from
LSriragavan,
SantanM,
brandantck,
hengxian-jiang,
jerome-ng,
maggie-li-yd,
p-hoffmann and
suwarnoong
as code owners
September 9, 2026 00:59
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #3127.
Eighth in the Data Exploration redesign stack. Based on
khairul-syazwan/d2e-exploration-paging(#3292), notdevelop— 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.
helpers/explorationSelection.tshasSelection,setPageSelection,retainstores/explorations.tsExplorationsPage.vuehelpers/deleteExploration.tshelpers/bulkDeleteExplorations.tslibs/d2e-ui/.../D2eExplorationCard.vueSelect-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:
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, padding12px 16pxnot8px 24px, gaps 16px and 8px not a single 12px, the count is primary navy not black, and the bar carries aneutral-lightestbackground 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 namedNotethat resolves to a blue elsewhere in the same file. Built with the existingdangertoken, 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
D2eCheckboxalready paints it.Review findings, fixed in this branch
A code review and a manual pass each found defects.
CohortComparisonDialogwatches the trigger for a false-to-true edge and emitscloseEvonly 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.fireBookmarkQueryraises 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.{0}.Known issues, not fixed here
fireDeleteMaterializedCohortQueryandfireDeleteAtlasCohortDefinitionQuerytoast 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.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.vueis 1398 lines, past the 800 the style guide allows. It was already 1159 before this branch. The bar should becomeExplorationsBulkBar.vue, followingExplorationPagination.vueandExplorationEmptyState.vue. Deferred rather than restructure a file straight after it was manually verified.TODO(i18n)markers.Validation
Reported honestly; this is not a claim of full end-to-end coverage.
vite buildvite build --config vite.config.atlas.tsvite build --config vite.config.atlas-app.tsd2e-ui:tokens:check,test,buildprettier --checkon new filesplugins/uihoists eslint 7.32, which cannot read the app's flat configno-mistakesgatedevelop, not this stack's parent — the same path every PR in this stack has takenManual 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.