dotCMS / dotCMS/core

Workflow Center: select all results across pages, with per-row exclusion

Open
#36,981 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

dotCMS : Workflow Team : Scout Type : Task
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:338loadItems() 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:

  1. checkUncheckAll() (:1836) — header checkbox selects the visible page only, then calls selectAllContentsMessage().
  2. selectAllContentsMessage() (:1860) — shows "all N content item(s) on this Page are selected" and, only when perPage < totalContents, appends a Select all <totalContents> content-s link.
  3. selectAllContents() (:1876) — sets fullCommand = "true", shows the total and a Clear Selection link.
  4. updateUnCheckedList() (:93) — while fullCommand is true, unchecking a row accumulates into unCheckedInodes, an exclusion list layered on the query.
  5. 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:

  1. No total count. The response (DotContentDriveSearchResponse) carries folderCount / contentCount, which are per-page counts — confirmed in BrowserAPIImpl.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 results link cannot be rendered from a fabricated number.

  2. No Lucene query for the client to send. /api/v1/drive/search takes a structured DriveRequestForm (assetPath, contentTypes, baseTypes, language, workflow, userSearchable, cursors), and BrowserAPIImpl resolves it SQL-first / hybrid DB+ES, not as one Lucene query. buildPureESQuery(BrowserQuery) (BrowserAPIImpl.java:596) is close and doPureESQuery even computes an exact total via contentletAPI.indexCount(...) (:573) — but it is private, and PURE_ES explicitly 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 excludeFolders in action-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. userSearchable field 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() — the query branch at :451-452 has no return/else, so a request carrying both query and contentletIds fires the query path and then the ids path, double-firing. Fixed, with a test asserting a single execution.

Frontend — selection state

  • selectedItems is replaced by (or wrapped in) a selection descriptor supporting both items and query modes; store.setSelectedItems keeps working for the items path.
  • 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 items selection and hides the bar.
  • Unchecking an individual row while in query mode adds its inode to excludedInodes, decrements the displayed count, and keeps the selection in query mode.
  • Re-checking a previously excluded row removes it from excludedInodes and restores the count.
  • Unchecking the header checkbox while in query mode 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 actionsgetBulkActions and _bulkfire send { query } (with exclusions negated into it) instead of contentletIds when in query mode. Only one of the two is ever sent; WorkflowResource:1288-1297 warns that supplying both to _bulkfire runs 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 fireBulkActionsNoReturn double-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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.