feat(content-drive): preview a workflow action's items before executing - #36884
feat(content-drive): preview a workflow action's items before executing#36884rjvelazco wants to merge 31 commits into
Conversation
Adds the client side of POST /api/v1/workflow/contentlet/actions/bulk, which returns the workflow actions available for a set of contentlets grouped by scheme and step, each with the number of selected contentlets it applies to. The models capture two properties of the response that matter to callers: - An action's `count` is already summed across every step of its scheme, so flattening steps into one list per scheme does not double-count. - `conditionPresent` means the count is an upper bound. The backend does not evaluate the action's Velocity condition while aggregating, since there is no per-contentlet permissionable at that point. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Draft of the Action Center: a dialog for acting on a multi-item selection, opened from the toolbar once more than one contentlet is selected. Built with PrimeNG (accordion, radiobutton, badge, message, skeleton) and Tailwind for layout only. Quick Actions fires system actions over the whole eligible selection in one request. Counts are derived client-side from row state, and an action that applies to nothing is omitted rather than shown as "(0)". Workflow Actions renders one collapsible panel per scheme from the bulk actions endpoint, with real per-action eligibility counts. Steps are flattened into a single list per scheme, since the step grouping is a backend detail the dialog does not need to surface. Scope limits, all deliberate: - One action per execute. No endpoint fires several different actions in one call, and firing one moves contentlets to a new step, which invalidates the other counts. The legacy JSP dialog works the same way. - Actions needing extra input (push publish, move path, assign/comment) are disabled with a tooltip rather than reimplementing the params dialog. Conditional counts render as "<= N". - Lock/Unlock and Add to Bundle are omitted: neither has a bulk REST endpoint. Legacy drives unlock through a Struts command that loops server-side, and add-to-bundle through a legacy AJAX servlet. - Fires synchronously. Legacy uses the SSE endpoint for live progress, which is the better path for large batches but needs an SSE shim. - Folders are excluded from every payload, matching the endpoints. System Workflow is intentionally left visible: hiding it would remove bulk Copy entirely (it has no system-action mapping) and would empty the section on Community, where it is the only scheme that can exist. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Addresses design feedback on the dialog shell. The Action Center now owns its own p-dialog instead of rendering inside the shell's shared one. That shared dialog gives a title and a scrolling content area; this dialog needs a custom header, a custom footer, and a body that is the only scrollable region. Two constraints forced the split: - PrimeNG queries `#header` / `#footer` with `descendants: false`, so the templates must be direct children of `p-dialog` and cannot sit inside the shell's `@switch` over dialog types. - Providing a `#footer` template on the shared dialog would render an empty footer for the folder and content-type dialogs. The shell routes ACTION_CENTER out of the shared dialog's content switch and mounts the component as a sibling instead, keeping the store as the single source of open/close state. Layout changes: - Header carries the title and the selected-contentlet count. - Footer carries the "one at a time" hint and Done, pinned. - Content area is `p-0 overflow-hidden` via `pt`; an inner div owns `max-h-[60vh] overflow-y-auto`, so only the body scrolls. - Quick action rows are plain buttons styled with Tailwind to match the design: a Material Symbols icon chip, left-aligned label, count and chevron trailing, red chip and label for destructive actions. They were p-buttons before, which centered their content and could not express the chip. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Restyles the dialog against the prototype in remix_-content-drive/components/ActionCenterDialog.tsx. The prototype's raw palette is mapped onto the theme tokens that tailwindcss-primeui exposes — `surface-*`, `primary-*`, `border-surface` — rather than copying its `slate-*` and hardcoded `#1D1B4B` / `#1D4ED8` values, so the dialog follows the active PrimeNG theme. - Fixed-height flex column (`80vh`, `42rem` wide) with the content area flexing and scrolling. Replaces the `max-h-[60vh]` guess on an inner div; this is how the prototype does it and it keeps header and footer pinned without a second scroll container. - Card treatment: sections sit on `surface-50` inside a rounded-xl `surface-100` border, and rows lift to `surface-0` on hover. This was inverted before (white card, grey hover). - Row metrics from the prototype: `gap-4`, `py-3.5`, 20px icon glyphs, `size-9` chips with `shadow-sm`, 10px bold section labels. - Scheme panels are now single-expand, matching the prototype: opening one collapses the rest, and the expanded scheme's name takes the primary colour. Collapsing a panel clears its pending action so Execute cannot stay armed for a hidden panel. Two deliberate deviations from the prototype, both carried over from the endpoint analysis: - Steps use radio semantics, not checkboxes. The prototype multi-selects steps, but no endpoint fires several actions in one call and firing one moves contentlets to a new step, invalidating the other counts. - No per-step icon chips. The prototype hand-picks a glyph per step; the API returns dotCMS icon names that do not map to Material Symbols, so there is nothing to render faithfully yet. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A collapsed scheme panel in the Action Center still took up the height of
its expanded content, leaving a large blank gap under the header.
PrimeNG 21 collapses accordion content by animating the motion wrapper's
`grid-template-rows` to `0fr`, but that wrapper is configured with
`hideStrategy: 'visibility'` and `unmountOnLeave: false`, so it stays
mounted and keeps its layout box. Without `overflow: hidden` the content
simply overflows the zero-height grid row and the panel keeps its full
height. PrimeNG's own stylesheet only ever sets `grid-template-rows: 1fr`
on `.p-accordioncontent .p-motion`, so nothing clips it by default.
Fixed with the same `pt` override already used by
dot-page-scanner-a11y-report:
{ motion: { root: { style: { overflow: 'hidden' } } } }
Also adopts that component's `dt` content-padding reset so the rows own
their padding, which restores the design's full-bleed dividers, and makes
`[multiple]="false"` explicit rather than relying on the default.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The scheme panels rendered as borderless grey cards. A `border` utility on
the panel had no effect: the theme ships `accordion.panel.border.width` as
`0 0 1px 0` — inter-panel dividers, which suit one stacked accordion — and
its runtime-injected CSS wins over a utility class.
Overriding the design token instead gives each panel a real border, which
is what the design shows: the schemes are separate cards separated by a
gap, not a stack.
panel: { borderWidth: '1px', borderColor: '{surface.200}' }
The content section also gets a top border so an expanded header is
separated from its rows, and its background moves to the token rather than
a utility on an inner div, keeping one source of truth per surface.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Gives the scheme panels the same radius and background treatment as the Quick Actions list, and fixes the reason they did not match. `p-accordion-panel` exposes only a `value` input — it has no `styleClass` — so the `rounded-xl bg-surface-50 overflow-hidden` attribute on it was inert. It has been doing nothing since the panels were introduced; the grey came from the theme's header background, not from that class. Panel styling now goes through the `panel` pass-through section, where `overflow: hidden` is also what clips the header and content corners to the panel radius. This also corrects the reasoning in 2a4ebf1: the panel border did not lose a specificity fight with the theme, the class was never applied at all. Overriding `accordion.panel.border.*` remains the right fix, since the theme's `0 0 1px 0` default is meant for a stacked accordion. Header backgrounds now come from tokens and mirror the Quick Actions rows: `surface-50` at rest, white on hover, and white while expanded so the open scheme reads as the active card. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…hrough Applies the scheme panel's radius via `p-accordion-panel`'s `pt.root` instead of the parent accordion's `pt.panel`, keeping the override on the component it targets. Also drops a stray unused `PanelClasses` import. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…tion Center The Action Center button was replacing the flat workflow action buttons once more than one contentlet was selected, so the quick per-item actions disappeared exactly when a multi-selection was made. They are now offered together: the workflow action buttons stay visible for any selection and keep their place first in the toolbar row, with the Action Center button appended after them. The dialog adds what the flat buttons cannot express — per-action eligibility counts and the workflow actions grouped by scheme — rather than standing in for them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…tlet Drops the Action Center button's threshold from more than one contentlet to one or more, so it is available alongside the flat action buttons for a single selection. This widens the original scope, which specced the button as appearing only for a multi-selection. It holds up: the dialog's value is the per-action eligibility counts and the workflow actions grouped by scheme, and neither depends on the selection having more than one item. The bulk endpoints take an inode list, so a single inode needs no special handling. Folders still do not count — a folder-only selection offers no Action Center, since every bulk endpoint ignores folders. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ck actions visible Two pieces of design feedback. **Renamed to "Workflow Center"** and dropped the toolbar button's icon. The button and the dialog header share one message key, so the label change covers both and they stay consistent. **Quick actions are no longer filtered out when they apply to nothing.** Every action is always listed; a count of 0 now means "does not apply to this selection" and the row renders non-selectable, with a tooltip saying so. Previously such rows were dropped, which made the list shift as the selection changed — rows appearing and disappearing under the pointer, and no indication that an action existed but was unavailable. An empty result still means there are no contentlets at all, where nothing could apply. Note this makes the count honest rather than decorative: a disabled row is the one place the UI admits an action is unavailable for the whole selection, which the previous behaviour hid entirely. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…x the order Adds Add to Bundle to the quick actions, always non-selectable with a hint explaining why, and pins the display order to Publish, Unpublish, Archive, Delete, Add to Bundle. Rows hold that position whether or not they are selectable, so the list never reshuffles as the selection changes. Add to Bundle is not blocked on an endpoint: `POST /api/v1/bundles/assets` takes a list of asset **identifiers** (note: identifiers, unlike the workflow bulk endpoints, which take inodes). What it needs is a target bundle, so a picker step — `DotAddToBundleComponent` accepts a single identifier and still posts to the legacy AJAX servlet — plus an enterprise-license gate. That is a shared-component change and is tracked separately, hence the disabled row rather than a half-wired one. A `pendingHint` on the action models "no working implementation yet", distinct from a zero count meaning "does not apply to this selection". The hint takes precedence, so the row explains itself rather than blaming the selection, and the execute handler guards on it as well as on the disabled attribute. This drops **Unarchive**, which is not in the design's quick-action set. It is a valid SystemAction and was working, so re-adding it is a one-line change if that set is revisited. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Puts Unarchive back, positioned after Delete so the two archived-only actions sit together and Add to Bundle stays last. Without it, archiving was a one-way trip: Archive shows for items that are not archived and Delete for items that are, but nothing in the dialog could un-archive. Unarchive is a valid SystemAction the multi-contentlet fire endpoint accepts, so it needs no extra plumbing. Order is now Publish, Unpublish, Archive, Delete, Unarchive, Add to Bundle, asserted by the fixed-order test. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… owns
Removes Publish, Unpublish, Archive, Unarchive and Delete from the
toolbar's flat action buttons. They are offered by the Workflow Center's
Quick Actions, which shows how many of the selected items each one applies
to — something a flat button cannot express. Keeping both meant the same
action appeared twice, reached by two different code paths.
The toolbar keeps what the dialog does not cover: Edit Content, Edit Page,
Rename, Save as Draft and Download.
Two consequences handled rather than shipped:
- **Quick action labels no longer borrow from the toolbar list.** They were
looked up in `DEFAULT_WORKFLOW_ACTIONS` by id, so trimming that array
would have rendered raw ids ("PUBLISH"). Each quick action now carries its
own `nameKey`, reusing the same `Default-Action-*` message keys.
- **Bulk Delete keeps its confirmation.** The toolbar's Delete prompted
before firing; moving it would have left the only delete path unguarded.
The Action Center now confirms any quick action carrying a
`confirmMessage`, which is Delete alone, matching the previous behaviour.
Test changes: the toolbar spec used Publish and Archive as stand-ins for
"a workflow action" in its execute, toast and error tests — those are
repointed to Save as Draft, which fires through the same path. The two tests
whose subject was specifically a removed action are gone, and Delete's
confirm-then-fire flow is now covered in the Action Center spec, including
the dismissal case. The toolbar's confirm branch is left in place as the
mechanism a future confirmable action would use.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…w-fire path Save as Draft is no longer needed in the toolbar. With it gone, every remaining toolbar action is navigation, rename or download — none of them fires a workflow action — so the machinery behind that path goes too: `DotWorkflowActionsFireService`, the confirmation service and dialog, the `confirmationMessage` field on `ContentDriveWorkflowAction`, and the `executeWorkflowAction` / `performWorkflowAction` / `beforeExecuteWorkflowAction` chain. The component is now purely presentational dispatch: edit, rename, download. Lifecycle actions live in the Workflow Center, which owns their confirmation and error handling. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Addresses the PR review. **Quick actions fired over the whole selection while the row showed only the eligible count.** `onExecuteQuickAction` built its payload from `toContentletInodes($selectedItems())`, but the badge came from `filter(eligibleWhen).length` — two independent derivations. A Publish reading "(1)" would publish two items, and Delete would be attempted on contentlets that are not archived. The eligible inodes are now derived alongside the count in a single filter pass and carried on the action as `eligibleInodes`, which is exactly what gets fired. One source, so the number shown and the items touched cannot drift apart. The success toast reports the same set. Two regression tests lock this down: one asserts Publish sends only the non-live inode, another parses the count out of the rendered row and asserts the fired payload has that many inodes. The previous spec asserted the buggy payload, which is why this passed review-less. Also from the review: - Errors now go through `DotHttpErrorManagerService.handle()` instead of a bespoke toast plus `console.error`. Beyond the documented anti-pattern, it matters here: per-item permission failures are an expected outcome of these endpoints, and the manager distinguishes 401/403 rather than flattening everything into one message. - `getQuickActions` is fed `$contentlets()` rather than re-deriving folder exclusion from the raw selection. New specs for the three untested seams: `getBulkActions` (inode and query variants, plus the null-entity fallback), the toolbar's Workflow Center gating and dialog wiring, and the shell's ACTION_CENTER routing including that the shared dialog stays hidden while it is active. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…-workflow-actions-with-one-action-per-execution
Addresses review feedback (zJaaal) on both the dialog template and the shell: reuse the existing content-drive dialog instead of a second `p-dialog`, so there is no duplicate dialog logic. The Action Center is a case in the shared dialog's content switch again. `$sharedDialogVisible` / `$actionCenterVisible` / `$isActionCenter` are gone along with the sibling mount, and `onVisibleChange` no longer needs its guard — there is one dialog and one open/close path. The pinned-chrome layout survives without PrimeNG's `#header` / `#footer` slots, which was the reason I reached for a second dialog in the first place. Those slots are `ContentChild` queries with `descendants: false`, so they cannot be provided from inside the shell's `@switch`, and adding a footer slot would render an empty footer for the folder and content-type dialogs. Making the *content box* a flex column achieves the same thing: - The shell gives this type `flex flex-col overflow-hidden p-0!` plus a fixed height (the column needs something to flex against, or the body never scrolls), and drops the header's bottom rule so the title and the "N items selected" line read as one block. - This component supplies the three rows: a pinned summary, a `flex-1 overflow-y-auto` body, and a pinned footer. Other dialog types are untouched — they get no style overrides and no footer. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… into the header
Three related symptoms, one cause.
The content box was scrolling as a whole — footer included — because the
theme sets `overflow-y: auto` on `.p-dialog-content` and its
runtime-injected CSS outranks a Tailwind `overflow-hidden` utility. The flex
column never took effect, so the footer read as part of the content and the
scrollbar spanned it.
Fixed by driving the content box through `[contentStyle]` instead. Inline
styles beat the theme's class rules without `!` overrides:
display: flex; flex-direction: column; flex: 1;
min-height: 0; overflow: hidden; padding: 0
`min-height: 0` is the load-bearing part — a flex item will not shrink below
its content height without it, so the body could never scroll and the footer
was pushed out of view. The body is now the only scroll container and the
footer is pinned as its sibling.
The "N items selected" line moves into a real custom header via PrimeNG's
`#header` slot, which is where it belongs — previously it sat at the top of
the content and scrolled away, which is why it was missing from the report.
The slot is a `ContentChild` with `descendants: false`, so it lives as a
single direct child of `p-dialog` with the dialog type switched inside it;
providing it suppresses PrimeNG's own title span, hence the default branch
renders the title itself. The header's bottom rule is dropped for this type
so title and sub-line read as one block.
The footer stays a flex sibling rather than using the `#footer` slot, so the
Done button can remain disabled while an action is in flight — that state
lives in the dialog component, not the shell.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ove query parameter handling
…item-drill-down-with-execution-results
…mproved readability
…dupe its header Three fixes to the workflow-action preview: - The dialog showed two headers: the shell's "Workflow Center" title and count, plus the preview's own action title and count. The preview now publishes a drill-down header through the store (`dialogDrillDown`), so the shell's single header retitles to the action name with a count that tracks the checked rows. Only the back control stays in the body, where it can disable itself while an action is in flight. - The preview listed every selected contentlet, including content types the action's scheme is not assigned to — a Blog action showing a VtlInclude row the server was always going to skip. The bulk endpoint reports counts without saying which contentlets match, but workflow schemes are assigned per content type, so the lookup now runs once per content type in the selection and each action records the types that can run it. Execute sends only those inodes. This is accurate at scheme level, not step level (contentlets of one type can sit on different steps), so the partial-match warning stays and now compares against the filtered rows. - A long title widened the table past the dialog and pushed the Type column behind a horizontal scrollbar. Fixed table layout plus `min-w-0` on the title span so it truncates, with the full text kept as the hover title. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ts for improved readability" This reverts commit 0304def.
|
Claude finished @rjvelazco's task in 4m 30s —— View job Code Review — workflow action previewReviewed the full diff (frontend only): New IssuesNo bugs found. The changes are internally consistent and the edge cases I checked hold up:
Notes (non-blocking, already documented)
Test coverageThe suite is thorough. The scheme-switch deselection ( Verdict: No blocking issues. Frontend-only change; backend • |
…p the redundant Execute label - The dialog body scrolled, not the table, so the column headers scrolled out of view and the paginator ended up at the bottom of a scrolled page where it was easy to miss. The table is now `scrollable` with `scrollHeight="flex"` and goes edge to edge (no body padding), so it fills the dialog, keeps its headers pinned, and leaves the paginator outside the scroll region. The host is a shrinkable flex column so `flex` has a bounded height to resolve against. - The Execute button repeated the action name, which the dialog header already shows since the drill-down header landed. It now reads just "Execute" with the count badge, and no longer grows with the action name. Reuses the existing `Execute` key, so `content-drive.action-center.preview.execute` is removed rather than left dangling. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…item-drill-down-with-execution-results
…down-with-execution-results' of https://github.com/dotCMS/core into issue-36819-content-drive-action-center-per-item-drill-down-with-execution-results
|
@claude review this PR and check what unit test are valid, what can be removed and propuse new unit tests |
|
Claude finished @rjvelazco's task in 6m 43s —— View job Unit test review —
|
…tch deselection Closes the coverage gaps from the PR's unit-test review. All of them sat on behavior this PR added that was only exercised indirectly: - `withDialog`: `setDialogDrillDown` / `clearDialogDrillDown` had no tests at all, including the two behaviors that keep a stale header from leaking — `setDialog` and `closeDialog` both clearing it. - Shell: `dialogDrillDown` was wired into the store mock but never set, so "publishing a drill-down retitles the one dialog header" was never verified at the layer that renders it. Now asserts the retitle and the fallback on clear. - `onOpenSchemeChange`: the deselection branch was entirely untested — no spec called it. Covers switching scheme, collapsing every panel, reopening the same scheme, and the array shape PrimeNG's accordion can emit but JSDOM clicking never produces. - `onContinueToPreview`: the early return when an action resolves to no eligible contentlets (selection changed after the lookup) had no test. Each new test was checked against a deliberately mutated implementation to confirm it actually fails when the behavior is removed. That caught a weak one: asserting the array branch with a single-element array passes either way, since `['x'].toString() === 'x'` — it now uses two entries and asserts `$openSchemeId`. Tests only; no production code changed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Partially addresses #36819.
Adds the drill-down half of that issue: picking a workflow action now opens a preview of the contentlets it is about to run on, with a checkbox per row, so the payload can be reviewed and trimmed before firing. The execution-results half (per-item spinner / success / error with retry) is not in this PR — see Additional Info.
Proposed Changes
skippedCount. They now send only the checked inodes, which also makes them consistent with quick actions (already sending just their eligible subset).VtlIncluderow the server was always going to skip. Since workflow schemes are assigned per content type, the lookup now runs once per content type in the selection and each action records the types that can run it. Cost is bounded by distinct content types (typically 1–2), not by contentlet count, and the requests run in parallel.dialogDrillDown), and the shell's single header retitles to the action name with a count that tracks the checked rows. Only the back control stays in the body, where it can disable itself while an action is in flight.min-w-0on the title span; full text kept as the hover title.Reuses existing building blocks rather than adding new ones —
DotContentletStatusBadgeComponentandDotContentThumbnailComponentfrom@dotcms/ui, PrimeNG table checkboxes, and content-drive's existing 20-rows-per-page convention.Checklist
Additional Info
Out of scope here — the other half of #36819. No per-item execution results. Execute still fires, toasts, refreshes the grid and closes the dialog. Adding per-row status/retry needs a decision on how per-item outcomes are reported (the synchronous
bulkFirereturns aggregate counts; the SSE_bulkfireendpoint streams progress but needs a shim, since nativeEventSourcecannot POST a body). Worth splitting into its own issue.Known limitation: scheme-level, not step-level. Content-type grouping makes eligibility exact at scheme level, but two contentlets of the same type can sit on different workflow steps and only one step may expose the action. The endpoint still won't say which. The action's
countremains the authority, so the preview keeps a partial-match warning that fires only on that genuine step-level gap. Making rows individually accurate needs a backend change (returning matching contentlet ids per action).Rows are keyed on
inode, notidentifieras the main grid does. Language variants of one contentlet share an identifier but have distinct inodes, and inodes are what gets fired — keying on identifier would collapse two variants into one selection entry. Pinned by a test.Reviewers: new i18n keys need a dotCMS restart.
Language.propertiesis read by the backend at startup, so the five newcontent-drive.action-center.*keys render as raw keys against an already-running instance until it restarts (or the language cache is flushed). Not a frontend bug.One commit is a revert.
bd8560ffbcreverts0304def518, a Vue-template reformatting commit that had reformatted 29 unrelatedlibs/sdk/vuefiles against the repo's Prettier config (prettier --checkflagged all 29; clean after the revert). The two cancel out, so they contribute nothing to this PR's diff. Root cause worth noting separately:lint-staged.config.mjsglobs coverts,js,mjs,cjs,tsx,jsxandjson,html,css,scss,md,yaml,yml—.vueis in neither, so the pre-commit hook never formats Vue files while an unscopednx format:writedoes.Branch history is noisy. It carries the pre-squash commits of #36821 plus a merge, all redundant with main's squashed
d595cd8e3c. The diff is unaffected (15 files, all listed above); only the commit list is long.Verification
Also exercised manually against a local backend on Angular 22 / TypeScript 6.0: mixed-type selection, unchecking rows, >20-item pagination, back navigation, and that quick actions still fire on a single click.
Videos
video.mov
video-2.mov