Workflow Center: select all results across pages, with per-row exclusion
Nobody has claimed this yet.
- Dominant language
- Java
- Stars
- 970
- Forks
- 486
- Avg merge
- 3d 33m
- Merged PRs (30d)
- 170
Description
Description
Content Drive can only ever act on the rows visible on the current page. There is no way to act on a whole result set, which the legacy content-search portlet has supported for years. An editor who filters down to 3,400 items and wants to publish them has to paginate 170 times at 20 rows a page — and even that does not work, because the selection is discarded on every page change.
This is the missing capability behind #33338 (product Feature, with mockup). It is a prerequisite for the bulk workflow work in this epic being useful at real content volumes: #36817, #36844, #36845 and #36819 all operate on a selection that today cannot exceed one page.
Current state
| Capability | Status | Where |
|---|---|---|
| Per-row checkbox | ✅ Implemented | dot-folder-list-view.component.html:74 — <p-tableCheckbox> |
| Header checkbox — selects current page | ✅ Already works | dot-folder-list-view.component.html:32 — <p-tableHeaderCheckbox> |
| Selection survives page change | ❌ Wiped | dot-content-drive.store.ts:338 — loadItems() patches selectedItems: []; the table repeats it at dot-folder-list-view.component.ts:308-311 |
| "Select all N results" escalation | ❌ Missing | — |
| Bulk endpoints accept a query instead of inodes | ✅ Already supported | FireBulkActionsForm, BulkActionForm; WorkflowResource:937 / :1003 / :1094 |
Frontend models carry query |
✅ Already declared | dot-action-bulk-request-options.model.ts:9, dot-bulk-actions.model.ts:13 |
So the two halves of the contract already exist and were designed for this. There is even a TODO marking the exact insertion point — dot-content-drive-action-center.component.ts:576-577:
"Sends inodes rather than a Lucene query. If Content Drive later supports selecting beyond the current page, this is where the
{ query }variant goes — the endpoint accepts either."
What the legacy portlet did
view_contentlets_js_inc.jsp implements exactly the two-stage escalation this issue asks for, and is the reference behavior:
checkUncheckAll()(:1836) — header checkbox selects the visible page only, then callsselectAllContentsMessage().selectAllContentsMessage()(:1860) — shows "all N content item(s) on this Page are selected" and, only whenperPage < totalContents, appends aSelect all <totalContents> content-slink.selectAllContents()(:1876) — setsfullCommand = "true", shows the total and a Clear Selection link.updateUnCheckedList()(:93) — whilefullCommandis true, unchecking a row accumulates intounCheckedInodes, an exclusion list layered on the query.getSelectedInodesFromList()(:1344-1375) — sends"query_" + luceneQueryRaw + " -(inode:… inode:…)"instead of a list of inodes.
i18n keys already exist and should be reused: Select-all (Language.properties:4258), contents-on-this-page-are-selected (:841), Clear-Selection, content-s.
The two real gaps
Both are on POST /api/v1/drive/search, and both must be closed for this to work:
-
No total count. The response (
DotContentDriveSearchResponse) carriesfolderCount/contentCount, which are per-page counts — confirmed inBrowserAPIImpl.java:1611-1659. The UI therefore fabricates a total purely to keep the paginator's Next button alive (dot-content-drive-shell.component.ts:435-448):// The API uses cursor-based pagination and does not return a total count. return page?.hasMoreContent || page?.hasMoreFolders ? limit * (currentPage + 1) // fake "one page beyond" : limit * (currentPage - 1) + items.length;A
Select all 3,400 resultslink cannot be rendered from a fabricated number. -
No Lucene query for the client to send.
/api/v1/drive/searchtakes a structuredDriveRequestForm(assetPath, contentTypes, baseTypes, language, workflow, userSearchable, cursors), andBrowserAPIImplresolves it SQL-first / hybrid DB+ES, not as one Lucene query.buildPureESQuery(BrowserQuery)(BrowserAPIImpl.java:596) is close anddoPureESQueryeven computes an exact total viacontentletAPI.indexCount(...)(:573) — but it is private, andPURE_ESexplicitly cannot express Content Drive's field filters (BrowserAPIImpl.java:482-492):DotRuntimeException("Content Drive field filters (userSearchable) are not supported under the PURE_ES heuristic...")
Proposed design
Introduce a selection descriptor in the store so every consumer reads one shape instead of assuming an array of rows:
type DotContentDriveSelection =
| { mode: 'items'; items: DotContentDriveItem[] }
| { mode: 'query'; query: string; excludedInodes: string[]; total: number };
mode: 'items'is today's behavior, unchanged.mode: 'query'is entered only via the "Select all N" link, and carries the exclusion list.- Consumers that need a concrete inode list (quick actions, preview) resolve the descriptor through a paged resolve-identifiers call, so no new bulk endpoints are required for #36844 / #36845.
Assumptions made in writing these criteria — flag on review if any are wrong:
- The total shown in the link is the contentlet total; folders are excluded from select-all, consistent with
excludeFoldersinaction-center.ts:206. - Where the requested filter combination cannot be expressed as Lucene (the
userSearchable/ Tag cases above), the correct behavior is to not offer the select-all link rather than to offer a query that silently drops a filter. - The Action Center preview under a query selection shows per-content-type counts from
getBulkActions({ query })plus a sampled first page of rows, explicitly labeled as a sample.
Acceptance Criteria
Backend — /api/v1/drive/search
- The search response returns an exact total contentlet count for the current filter set, distinct from the existing per-page
contentCount. - The search response returns the Lucene/ES query equivalent of the current filter set, or an explicit indicator that no equivalent can be produced for this filter combination.
- When the filter set includes predicates that cannot be expressed in Lucene (e.g.
userSearchablefield filters, Tag predicates routed through SQL —BrowserAPIImpl.java:482-492), the response says so rather than returning a query that silently omits them. - A new paged endpoint resolves a query selection to contentlet identifiers, so consumers that require inodes can materialize the selection in batches.
- Computing the total does not regress search latency on the default (no-filter) Content Drive view; measured before/after.
-
WorkflowHelper.fireBulkActionsNoReturn()— thequerybranch at:451-452has noreturn/else, so a request carrying bothqueryandcontentletIdsfires the query path and then the ids path, double-firing. Fixed, with a test asserting a single execution.
Frontend — selection state
-
selectedItemsis replaced by (or wrapped in) a selection descriptor supporting bothitemsandquerymodes;store.setSelectedItemskeeps working for theitemspath. - A page-only selection persists across page navigation instead of being cleared by
loadItems()(dot-content-drive.store.ts:338) and the table effect (dot-folder-list-view.component.ts:308-311). - Changing the search query, filters, folder, or site clears the selection in both modes — a query selection must never outlive the filters that defined it.
- Changing sort order or rows-per-page does not clear the selection.
Frontend — the two-stage escalation
- Checking the header checkbox selects every contentlet on the current page (existing behavior) and now surfaces a selection bar reading "All N items on this page are selected."
- The bar shows a "Select all N results" link only when the header checkbox is fully checked and the total exceeds the current page size. It is absent when the result set fits on one page.
- The link is absent when the backend reports that no Lucene equivalent exists for the current filters, with a tooltip explaining why select-all is unavailable.
- Clicking the link switches to
mode: 'query', updates the bar to "All N results are selected", and shows a "Clear Selection" link. - "Clear Selection" returns to an empty
itemsselection and hides the bar. - Unchecking an individual row while in
querymode adds its inode toexcludedInodes, decrements the displayed count, and keeps the selection inquerymode. - Re-checking a previously excluded row removes it from
excludedInodesand restores the count. - Unchecking the header checkbox while in
querymode clears the whole selection (does not fall back to a page selection). - Folder rows are never included by select-all, and the count reflects contentlets only.
- The selection count in the Action Center header (
content-drive.action-center.items-selected) reflects the query total minus exclusions.
Consumers
- Workflow actions —
getBulkActionsand_bulkfiresend{ query }(with exclusions negated into it) instead ofcontentletIdswhen inquerymode. Only one of the two is ever sent;WorkflowResource:1288-1297warns that supplying both to_bulkfireruns the action on the union. - Quick actions (Lock/Unlock #36844, Refresh #36845) work under a query selection by resolving identifiers in batches, and report progress against the resolved total rather than a page-sized total (
withActionExecution.ts:114,:164). - Action Center preview (#36819) under a query selection shows per-content-type counts plus a labeled sample of rows, and does not claim to list every affected item.
- Any consumer that cannot honor a query selection is disabled with a visible reason, never silently applied to the visible page only.
Sad path and edge cases
- Select-all over a result set the user only partially has permission on fires only on permitted items and reports the skipped count — it does not fail the whole batch.
- A 422 from the bulk fire endpoint (entire batch skipped on scheme mismatch —
WorkflowResource:1003) surfaces as an explanatory message, not a generic error. - If the resolve-identifiers call fails partway, the user is told how many items were processed and that the operation was incomplete.
- Content added or removed between selecting and firing does not error; the fired total is reported as actual, and any divergence from the displayed count is surfaced.
- A select-all of zero results is impossible — the link is not rendered when the total is 0.
Tests
- Store unit tests: descriptor transitions (page → query → cleared), exclusion add/remove, and the clear-on-filter-change vs keep-on-sort-change rules.
- Component tests for the selection bar: link visibility thresholds, count rendering, Clear Selection.
- Backend tests for the exact total, the query-equivalent output, the not-expressible case, and the
fireBulkActionsNoReturndouble-fire fix. - E2E covering select-all → fire a workflow action → verify the result count, added under
apps/dotcms-ui-e2e/src/tests/content-drive/(which currently has no selection coverage).
Priority
Medium
Additional Context
Relationships
- Sub-issue of #33999 — [EPIC] CD: Workflow Actions
- Implements the product Feature #33338, which carries the intended UX and a mockup
- Unblocks bulk operation at scale for #36817, #36844, #36845, #36819
- Legacy precedent: #26317 (Content Search "Select All") — closed unimplemented
Key files
| Area | Path |
|---|---|
| Selection store | core-web/libs/portlets/dot-content-drive/portlet/src/lib/store/dot-content-drive.store.ts |
| State shape | .../portlet/src/lib/shared/models.ts:184-225 |
| Table + header checkbox | core-web/libs/portlets/dot-content-drive/ui/src/lib/dot-folder-list-view/ |
| Faked total | .../portlet/src/lib/dot-content-drive-shell/dot-content-drive-shell.component.ts:435-448 |
| Action Center + the TODO | .../portlet/src/lib/components/dialogs/dot-content-drive-action-center/dot-content-drive-action-center.component.ts:576 |
| Action execution | .../portlet/src/lib/store/features/action-execution/withActionExecution.ts |
| Selection helpers | .../portlet/src/lib/utils/action-center.ts |
| Search endpoint | dotCMS/src/main/java/com/dotcms/rest/api/v1/drive/ContentDriveResource.java:82 |
| Query building / totals | dotCMS/src/main/java/com/dotcms/browser/BrowserAPIImpl.java:470-596, :1611-1659 |
| Bulk forms | dotCMS/src/main/java/com/dotcms/workflow/form/FireBulkActionsForm.java, BulkActionForm.java |
| Bulk helper (+ the double-fire bug) | dotCMS/src/main/java/com/dotcms/workflow/helper/WorkflowHelper.java:159-200, :417-457 |
| Legacy reference | dotCMS/src/main/webapp/html/portlet/ext/contentlet/view_contentlets_js_inc.jsp:93, :1344-1375, :1836-1885 |
Open question for planning
doPureESQuery gets an exact total for free via contentletAPI.indexCount(...), but the default heuristic is HYBRID_SINGLE_CHUNKED_QUERY_ES (SQL page + per-chunk ES filter), which has no equivalent. Whether the total comes from a second count query, from a widened PaginatedContents, or only for filter sets that can route through pure ES, is a design decision for /speckit-plan — and it interacts directly with the "not expressible as Lucene" criterion above.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with dot-content-drive.store.ts, dot-folder-list-view.component.ts, and the POST /api/v1/drive/search flow in BrowserAPIImpl.java. Review the listed backend and store/component test requirements before changing selection state. Done means exact totals and query support, safe query-mode consumers, persistence and clearing rules, and passing backend, frontend, and E2E coverage.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- elasticsearch, java, typescript
- Domain
- backend-api-design, frontend, full-stack, testing-qa
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 25/100