Skip to content

feat(publishing-queue): Angular portlet + backend wiring#36413

Open
hmoreras wants to merge 48 commits into
mainfrom
issue-36040-publishing-queue-angular-portlet
Open

feat(publishing-queue): Angular portlet + backend wiring#36413
hmoreras wants to merge 48 commits into
mainfrom
issue-36040-publishing-queue-angular-portlet

Conversation

@hmoreras

@hmoreras hmoreras commented Jul 3, 2026

Copy link
Copy Markdown
Member

Context

Closes #36040. Part of epic #34734 (Publishing Queue: Dojo → Angular migration). Consumes the API + IA audit from spike #36039. User-facing documentation follows in #36041.

Design divergence from the parent task worth flagging: the original plan listed a p-tabView with a Queue tab (two-column READY TO SEND + IN PROGRESS) and a History tab. Design iterated during implementation to a single unified bundles table with a status-chip filter — same information, less nesting. Status buckets (READY, IN PROGRESS, SCHEDULED, FAILED, SUCCESS) are surfaced through the chip row instead of tabs + columns; a synthetic SCHEDULED status was added server-side so bundles waiting for a future publishDate show up correctly without a persistent audit row.

Summary

  • Ships a new Angular portlet at libs/portlets/dot-publishing-queue (shell + toolbar + unified table + status chip filter + four dialogs + SignalStore) replacing the legacy JSP-driven Publishing Queue and History screens.
  • Data-access service at libs/data-access/src/lib/dot-publishing-queue covering the v1 publishing endpoints (/api/v1/publishing/*, /api/v1/bundles/*) plus the legacy /api/bundle/* endpoints that v1 hasn't consolidated yet (getunsendbundles, sync upload, _generate, {id}/assets, _download, {id}/manifest).
  • Unified bundles table: search, status-chip filter, per-row kebab (View Details / View Contents / Configure & Send / Retry / Generate-Download / Remove), silent polling refresh, and bulk selection that swaps Add Bundle for Retry Send as the primary action. Checkbox + kebab columns stay frozen on narrow viewports.
  • Add Bundle wizard: two-pane Select Bundle dialog with a swap-in Configure & Send step (custom header switches title as the user moves through the wizard), plus a drag-and-drop Upload dialog. Both terminal actions stay clickable — invalid state surfaces an inline warning instead of a disabled button.
  • Bundle Details dialog (envs + endpoints + timestamps + probe-driven download buttons; surfaces the SCHEDULED bundle's future publishDate).
  • View Contents dialog with contentlet-row → editor link and per-row remove for editable statuses (BUNDLE_REQUESTED / WAITING_FOR_PUBLISHING / SCHEDULED).
  • Remove confirm replaces the legacy 4-scope Delete dialog — the toolbar Remove button + toolbar filter + row selection cover the same scopes without an extra modal.

Backend wiring

  • dotCMS/src/main/webapp/WEB-INF/portlet.xml:
    • Existing publishing-queue JSP entry renamed to publishing-queue-legacy (kept as the rollback escape hatch).
    • New publishing-queue entry points at the Angular shell via com.dotcms.spring.portlet.PortletController.
    • Admins can flip between them live without redeploy.
  • dotCMS/src/main/java/com/dotmarketing/util/PortletID.java: adds PUBLISHING_QUEUE_LEGACY("publishing-queue-legacy").
  • dotCMS/src/main/webapp/WEB-INF/messages/Language.properties: adds portlet titles for both publishing-queue and publishing-queue-legacy, plus the FE's UI strings.
  • com.dotcms.rest.api.v1.publishing.PublishingResource: gates the two destructive endpoints (retry + push) behind requiredPortlet("publishing-queue"), matching the pattern the other v1 endpoints already use.

Test plan

  • pnpm nx test portlets-dot-publishing-queue-portlet passes (196+ specs green)
  • Portlet loads at /dotAdmin/#/c/publishing-queue served by the Angular shell
  • Empty queue renders the empty state
  • Status filter chip row narrows the table; SCHEDULED shows the future publish date in the details dialog
  • Row kebab exposes View Details, View Contents, Configure & Send (active rows), Retry (failed rows), Generate/Download, Remove
  • Add Bundle → Select Bundle → check ≥1 bundle → Configure & Send → Send fans out one push per bundle and closes the dialog
  • Add Bundle → Upload accepts .tar.gz / .tgz only; clicking Upload without a file surfaces the file-required warning inline
  • Bulk Remove asks for confirmation and returns immediately (BE removes asynchronously)
  • View Contents on a SCHEDULED bundle exposes the trash action; already-published bundles render read-only
  • Contentlet rows in View Contents open the content in a new tab; non-contentlet rows stay plain text
  • Rollback: flipping publishing-queuepublishing-queue-legacy in portlet.xml reverts to the JSP without redeploy
  • License + role gating preserved (publishing-queue portlet, requireLicense(true) on uploads)
  • On viewports narrower than the table width, the checkbox and kebab columns stay pinned while the middle columns scroll horizontally

Follow-ups

🤖 Generated with Claude Code

hmoreras and others added 30 commits June 8, 2026 16:28
…t list modal

First slice of the Publishing Queue Dojo → Angular migration. Lands
the foundation end-to-end so subsequent slices (History tab,
Configure & send, Upload Bundle, kebab actions, Bundle details
modal) layer on the same shell + store + data-access surface.

Frontend
- New Nx library libs/portlets/dot-publishing-queue (shell + page
  + reusable list + toolbar + asset list dialog + SignalStore)
- Path alias @dotcms/portlets/dot-publishing-queue/portlet in
  tsconfig.base.json; route registered in apps/dotcms-ui PORTLETS_ANGULAR
- Top bar: search (300ms debounce), Refresh, Upload Bundle (disabled +
  tooltip), site selector (disabled + tooltip — backend filter pending)
- Queue tab: two-column grid (Ready to Send + In Progress) with counts,
  status chips, skeleton/empty/error states, paginator per column
- Row click on either column opens the Asset list modal (Name/Type/State)
- 40 Jest+Spectator tests, 98.7% coverage

Data access + models
- DotPublishingQueueService at libs/data-access/.../dot-publishing-queue
  covers GET /v1/publishing and GET /bundle/{id}/assets
- New models: PublishingJobView, AssetPreviewView, PublishingJobsResponse,
  PublishAuditStatus enum (mirrors the 18-value Java enum), READY_STATUSES
  / IN_PROGRESS_STATUSES constants, BundleAssetView

Backend wiring
- portlet.xml: existing JSP entry renamed to publishing-queue-legacy;
  new Angular publishing-queue entry (com.dotcms.spring.portlet.PortletController)
  in the Angular Portlets section — admins can roll back without redeploy
  by flipping the two <portlet-class> values
- PortletID enum: PUBLISHING_QUEUE_LEGACY("publishing-queue-legacy")
- Language.properties: publishing-queue-legacy title + 35 new UI keys
- Resource-level requiredPortlet("publishing-queue") gates unchanged —
  the portlet name string is identical, only the class flipped

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…n v1 destructive ops

The legacy /bundle/* and /v1/publishqueue destructive ops correctly
required the publishing-queue portlet, but the equivalent v1 ops
(DELETE /v1/publishing/{bundleId}, DELETE /v1/publishing/purge) did
not. A backend user without portlet access could therefore delete
bundles via the v1 endpoints even though the UI was hidden from them.

Adds requiredPortlet("publishing-queue") to both methods, matching
the gating on PublishQueueResource and BundleResource.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…e & send, Upload), kebab/Send/Retry, polling

Lands the full Publishing Queue surface on top of the foundation slice:
History tab, all modals, row actions, auto-refresh polling, and the
site filter wiring. Backend security gate (#36045) shipped in a
separate commit on this branch.

History tab (`p-tabs` shell, switched from p-tabView to PrimeNG v20 API)
- Sortable Bundle / Status / Modified columns (three-state sort cycle)
- p-table with selection, bulk select, pagination
- Bulk action bar: Retry Send + Remove (with confirm dialog)
- Sent / Failed chips; row click opens Bundle Details modal

Bundle Details modal
- 9-field metadata definition list (bundle start/end + publish start/end
  via the existing AbstractTimestampsView — #36044 already covered BE-side)
- Endpoints-by-environment table with per-endpoint status chip
- Conditional Download button for completed bundles

Configure & send modal
- Push / Remove / Push+Remove action cards
- Send now / Schedule segmented control with timezone display
- ISO 8601 + timezone-offset date serialization
- Searchable environment dropdown + filter dropdown
- FE maps design operations (push/remove/pushremove) →
  backend PushBundleForm operations (publish/expire/publishexpire)
  per the spike's recommendation; no BE rename required

Upload Bundle dialog
- p-fileUpload basic mode for .tar.gz
- POST /api/bundle/sync (licensed); progress + error surface

Per-row actions
- READY rows: primary Send button + p-menu kebab with
  Configure & send / Generate & download / Remove from queue
- IN PROGRESS failed rows: inline Retry button
- Confirm-remove dialog for destructive actions

SignalStore expansion
- New state: activeTab, historyRows/page/total/status/sort/sortDirection/
  selectedIds, detail*, environments, pushBundleTarget, pushInFlight,
  uploadInFlight/Progress, siteId
- New methods: loadHistory, loadDetail, loadEnvironments,
  openDetail/closeDetail, openConfigureSend/closeConfigureSend/submitPush,
  retryBundles, deleteBundle, deleteBundlesBulk (loops per-id until #36046
  lands), generateBundle, uploadBundle, startPolling/stopPolling,
  setSiteId, setHistoryPage/cycleHistorySort/setHistorySelection
- onInit effect splits queue vs history loads by activeTab; polling
  fires every 15s for IN PROGRESS (paused when document.hidden)

Data-access service
- Adds getPublishingJobDetails, pushBundle, retryBundles, deleteBundle,
  deleteBundles, generateBundle, uploadBundle, getBundleDownloadUrl,
  getEnvironments
- Adds PublishingJobDetailView, EnvironmentDetailView, EndpointDetailView,
  TimestampsView, RetryBundleResultView, PushBundleResultView models

Site filter
- Toolbar now hosts the existing DotSiteSelectorDirective on a p-select
- Site selection flows through store.setSiteId; backend ignores the field
  today, FE is ready to forward it once #36043 expands the filter scope

Tests
- 89 Jest+Spectator tests, all green
- New specs for History, Bundle Details, Configure & Send, Upload, plus
  expanded store + page coverage

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…p, History column rework, hover-reveal copy

Round of design + correctness improvements after testing the slice
locally. Key fixes:

READY TO SEND now hits the correct endpoint
- Switched from GET /v1/publishing?status=WAITING_FOR_PUBLISHING,…
  to GET /api/bundle/getunsendbundles/userid/{userId}. The v1 list
  reads publish_audit (bundles already in the queue), but the design
  expects user-owned drafts which only live in publishing_bundle.
  Confirmed via openapi.json: no v1 endpoint exists for drafts today
  (legacy /bundle/* migration tracked under #36048).
- Store caches the userId via DotCurrentUserService on first load
- PublishingJobView.status widened to PublishAuditStatus | null so the
  same row type can represent drafts (no audit row → no status)

Tab order + default
- History tab is now first and the default open tab
- Queue tab loads lazily on switch (saves the initial double fetch)

History table rework
- Five new columns per dev feedback: Bundle Id (first), Filter,
  Status, Data Entered, Last Update
- Bundle Id renders the full id in monospace with a copy-to-clipboard
  button that fades in on row hover (group-hover/opacity pattern from
  es-search), reuses DotCopyButtonComponent for the canonical
  clipboard + "Copied!" tooltip feedback
- Dates formatted with the DatePipe medium preset

New dot-publishing-status-chip component
- Lives at libs/portlets/dot-publishing-queue/src/lib/components/
  (portlet-local, not promoted to libs/ui yet — only one consumer)
- Mirrors the project standard set by dot-contentlet-status-chip:
  p-chip with bg-{c}-100! text-{c}-700! border-{c}-100! text-xs
- Centralises the 18-status → 4-bucket mapping (success / danger /
  warning / info). Replaces three duplicate severity functions and
  three duplicate constant Sets across list / history / details
- Exports publishingStatusBucket() as a pure fn for direct testing

Empty states standardised
- Replaced the hand-rolled empty-state markup in list + history with
  the canonical DotEmptyContainerComponent (folder icon + bold title
  + lighter subtitle, hideContactUsLink=true). Same pattern that
  dot-query-tool / dot-analytics / dot-velocity-playground use
- Filed #36111 to migrate dot-tags to the same pattern

Site selector dropped
- Removed from the toolbar entirely. Bundles are scoped by owner,
  not by site (confirmed in BE: /v1/publishing has no site param,
  Dojo JSPs never filtered by site). The global admin chrome already
  ships a site selector for everything else

i18n keys backfill
- Audit caught 51 referenced-but-missing keys (tab labels, configure
  & send modal, bundle details, kebab, upload dialog, confirm dialogs,
  generic actions). Diffed grep output for every publishing-queue.*
  reference vs the properties file — 79 referenced, 79 defined, no
  orphans left

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…download bundle dialogs

Replace the custom Configure & Send dialog with the canonical
`DotPushPublishDialogService.open({ assetIdentifier, title, isBundle: true })`
(same service used by templates, containers, content, content types and
pages — mounted globally in `main-legacy.component.html`). Replace
`store.generateBundle` with `DotDownloadBundleDialogService.open(bundleId)`
following the same global-singleton pattern.

Companion store / service / model / i18n cleanup:
- Drop store state and methods: `pushBundleTarget`, `pushInFlight`,
  `environments`, `environmentsStatus`, `loadEnvironments`,
  `openConfigureSend`, `closeConfigureSend`, `submitPush`, `generateBundle`
- Drop service methods: `pushBundle`, `generateBundle`, `getEnvironments`
- Drop `PushBundleResultView` / `PushOperation` / `PushBundlePayload`
- Drop the `publishing-queue.configure-send.*` i18n keys and
  `upload-bundle.coming-soon`
- Drop the configure-send sync effect from the shell
- Delete the custom Configure & Send dialog (component + spec + template)

UX polish that surfaced fixing the wiring (same kebab still drives both
Configure & Send and Generate / Download):
- Make `readyKebabFor` an arrow-function class property (stable reference)
  so the list component's `kebabMenus` memoization works — fixes the
  first-click-only-closes-the-menu thrash in `<p-menu>` reported by users
- Add `showTransitionOptions="0ms"` / `hideTransitionOptions="0ms"` and
  `(mousedown)="$event.stopPropagation()"` on the kebab toggle
- Remove icons from kebab menu items (per user request)
- Memoize per-bundle kebab `MenuItem[]` in `kebabMenus` computed

Asset list dialog gains a hover-revealed per-row delete button that calls
the new `service.removeAssetsFromBundle` + `store.removeBundleAsset`
endpoint, with a fixed `h-96` container plus PrimeNG `[loading]` +
`loadingbody` skeleton template to prevent the dialog from shrinking and
re-expanding while the row reloads. A conditional `<p-iconField>` search
input appears when the bundle has > 10 assets, plus a "no matches" empty
state. The `State` column is removed from both asset tables — the
backend transformer never returns it, so it was always "—".

Bundle Details modal gains an Assets section under Endpoints (per user
ordering preference), with the same fixed-height + skeleton + conditional
search pattern. Lazy-loaded via the new `store.loadDetailAssets` and reset
when the dialog is reused for a different bundle (`detailAssetsStatus`).

Model rename: `BundleAssetView.id` → `asset` to match the backend
transformer's universal key (`BundleResource.java`).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tern) + tab-panel padding

Make the History table go flush edge-to-edge, matching the look and feel
of the dot-tags portlet (which we treat as the canonical reference for
data-table portlets). Three small cleanups in one pass since they all
work together to remove the visual padding around the table:

- History container: drop the outer `p-3 gap-2` padded wrapper, drop the
  `rounded-md border bg-white` "card" around the `<p-table>`. The table
  now sits flush against the tab panel on all four sides.

- History bulk-action bar: replace the `<p-toolbar>` wrapper with a thin
  inline strip that only appears when there's a selection (no chrome
  above the table when nothing is selected). Buttons use `size="small"`.

- History skeleton + empty state: move both into the table itself —
  skeletons render inline inside `pTemplate="body"` with `h-17` for a
  uniform row height; the empty state lives in `pTemplate="emptymessage"`
  with a `[pt]` config that sets `width: 100%, height: 100%` so the
  empty container fills the available space (instead of collapsing).

- Shell tab-panels: zero out PrimeNG's default tab-panel padding via
  `[pt]="tabPanelsPt"` / `[pt]="tabPanelPt"` on `<p-tabpanels>` and
  `<p-tabpanel>` (same pattern dot-query-tool uses). Without this, the
  History table inherits a built-in `p-4` from PrimeNG's theme that
  doesn't match the rest of the admin UI.

- Top toolbar: drop the `pi-upload` icon from the Upload Bundle button
  (per user request — label-only matches the rest of the admin UI).

Spec update: `'shows the bulk action bar only when there is a selection'`
no longer checks for the removed `pq-history-bulk-bar` testid (the
toolbar is gone); now checks the conditional bulk-action buttons
(`pq-history-bulk-retry` / `pq-history-bulk-remove`) directly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… JSP

Customers shouldn't see different status text in the new Angular portlet vs
the legacy Push Publishing JSP. The status chip was resolving against
`publishing-queue.status.*` (the alt-compact label set: "Sent", "Bundling",
etc.) while the legacy JSP uses `publisher_status_*` ("Success", "Bundle
sent", etc.).

Switch the chip's labelKey to the JSP-matching `publisher_status_*` pattern
and plug the one missing entry: `publisher_status_FAILED_INTEGRITY_CHECK`
(JSP itself had no key for this status; new portlet would have rendered the
raw i18n key).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Renders the human-readable bundle name as the leftmost data column (between
the row checkbox and the bundle id), with an em-dash fallback when the name
is null. Column order becomes: ☐ · Bundle Name · Bundle Id · Filter · Status
· Data Entered · Last Update. Empty-state colspan and skeleton row updated
accordingly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…pes) + toolbar bulk actions

Mirrors the legacy JSP "Select Bundles to Delete" modal end-to-end. The
History tab now has the same fire-and-forget delete flow as the JSP, with
all four scopes:

  SELECTED · ALL · SUCCESS · FAILED

Endpoint wiring (all async + WebSocket-notified, matching legacy):
- SELECTED → DELETE /api/bundle/ids  body { identifiers: [...] }
- ALL      → DELETE /api/v1/publishing/purge   (BE safe defaults)
- SUCCESS  → DELETE /api/v1/publishing/purge?status=SUCCESS,SUCCESS_WITH_WARNINGS
- FAILED   → DELETE /api/v1/publishing/purge?status=<exact legacy 5>

ALL is gated behind a confirm-dialog ("…cannot be undone") to reproduce the
legacy `confirm()` step. The FAILED status list deliberately excludes
FAILED_INTEGRITY_CHECK / INVALID_TOKEN / LICENSE_REQUIRED to stay 1:1 with
`BundleResource#deleteAllFail` — matching legacy semantics is the priority.

Relocates the bulk action UI from a row below the tabs to the top toolbar:
- "Retry Send" appears only when the history tab has a selection (with an
  N-selected count next to it).
- "Delete Bundles" is visible whenever the history tab has any rows.
- The inline `<p-confirmDialog>` for bulk-remove moves to the shell (the
  dialog is the single overlay owner now).

Service changes (`dot-publishing-queue.service.ts`):
- `deleteBundles(bundleIds)` now hits legacy `/api/bundle/ids` (the
  endpoint the JSP uses) instead of a non-existent v1 path.
- New `purgeBundles(statuses?)` calls `/api/v1/publishing/purge` with
  optional comma-joined status filter.

Store changes (`dot-publishing-queue.store.ts`):
- `deleteBundlesBulk` becomes a single async call (no more per-id
  forkJoin fan-out); clears selection on success.
- New `purgeBundles(statuses?, onDone?)` action.
- Exports `PURGE_SUCCESS_STATUSES` and `PURGE_FAILED_STATUSES` constants
  (the exact lists from legacy `/api/bundle/all/{success,fail}`).

Tests: 152 passing — 11 suites including the new delete-dialog spec.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ction

Show the Delete Bundles button only when the user has at least one row
checked in the History tab, matching the visibility model of the bulk Retry
Send button (and the "N selected" indicator). Both bulk-action buttons —
plus the count and the separator — now appear and disappear together
behind a single `hasBulkActions` predicate.

The dialog itself still handles the no-selection case defensively (SELECTED
disabled) because it doesn't know how it was opened, but in practice that
branch is no longer reachable from the toolbar.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…table + extract assets into View Contents

The bundle details dialog used to render one `<p-table>` per environment
group (with its own column headers), which looked like a duplicated table
when a bundle had multiple endpoints. Flatten to a single table that carries
the environment name as the leftmost column — uniform grid, no subheader
rows. Add a `whitespace-nowrap` on the Status column so long labels like
"Failed to send to all environments" stay on one line.

Endpoint address is now built via `endpointAddress(endpoint)` which returns
`null` when the underlying address is empty — the cell shows "—" instead of
the malformed `://:` the JSP renders. Protocol and port are optional and
omitted from the URL when blank.

Extract the assets section into the existing
`DotPublishingQueueAssetListDialogComponent`, reused as a read-only "View
Contents" surface. The asset-list dialog gains an `allowRemove` flag read
from `DynamicDialogConfig.data` (defaults to true so Queue/Ready callers
keep their edit UX). The shell decides per `activeTab()`: Queue → true,
History → false. The trash column, button, and skeleton cell are hidden
when the flag is false; the empty-state colspan adjusts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…and-drop pattern + inline error

The upload dialog used PrimeNG's `<p-fileUpload mode="basic">` and delegated
errors to the global toast via `DotHttpErrorManagerService.handle()` from
inside the store. That violated the canonical pattern documented in
`libs/portlets/CLAUDE.md` ("Store MUST NOT interact with UI") and offered a
different UX than the other 3 portlet upload dialogs (`dot-tags-import`,
`dot-plugins-upload`, `dot-categories-import`).

Refactor to match those canonical sites:
- `<p-fileUpload mode="advanced">` with a custom drag-and-drop content
  template (icon + dropzone copy + file-types hint)
- Component owns `selectedFile`, `uploading`, and `errorMessage` signals
- Calls `service.uploadBundle(file)` directly; on success → `store.refresh()`
  + close the dialog; on error → set `errorMessage` and stay open so the
  user can correct + retry
- `extractErrorMessage(HttpErrorResponse)` handles the 4 shapes the dotCMS
  BE returns: array body, `{ errors: [...] }`, `{ message: ... }`, plain
  string
- Inline `<p-message severity="error">` at the top of the dialog (full-bleed
  with `-mx-6` + `!rounded-none`) — same look as `dot-tags-import`

Store cleanup: removed `uploadBundle`, `uploadInFlight`, and `uploadProgress`.
The component now owns all upload state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…lete confirms

Per-row kebab in the History tab — same pattern as the Queue list's
`readyKebabFor`. Three items: View details · View Contents · separator ·
Delete. Items intentionally text-only (no icons) per design feedback.

- View details → `store.openDetail(bundleId)` (current Bundle Details dialog)
- View Contents → `store.openAssetList(bundleId)` (the same `AssetListDialog`
  the Queue tab uses, but opened read-only via the `allowRemove=false` flag
  introduced in the previous commit)
- Delete → per-row confirm + `store.deleteBundle(bundleId)`

The kebab button uses `<p-button>` (auto-rounded `p-button-icon-only
p-button-rounded` look) and is wrapped in a hover-only `opacity-0
group-hover:opacity-100 focus-within:opacity-100` div so it stays out of
sight until the user mouses over the row. The row click handler still opens
the Details dialog so the dialog and the kebab's "View details" both behave
identically.

Critical fix: `kebabFor(row)` returns a memoized `MenuItem[]` reference
(map keyed by `bundleId`) — `<p-menu [model]="…">` thrashes when it
receives a brand-new array on every CD cycle, causing the well-known
"first click only closes the menu" bug. Mirrors the fix already in
`dot-publishing-queue-list`.

Layout polish:
- Explicit `<th style="width: …">` widths per column + `table-layout: fixed`.
- Switched the table to `width: auto` (via `$ptConfig`) so leftover
  container space stops being distributed across the fixed columns —
  that was leaving big gaps after Bundle Id / Status while squeezing
  Filter to ellipsis.
- `whitespace-nowrap` on Status (chip doesn't wrap "Failed to send to all
  environments" anymore) and on the date columns.

Bundle Id cell:
- Removed `font-mono`, capped to 32 chars in TS (`truncateBundleId`) with
  the full id exposed via `title=`. Standard 26-char ULIDs are unchanged;
  longer ids (custom imports) get "…" suffix.
- Replaced `<dot-copy-button>` with the inline pattern from
  `dot-es-search-copy-identifier`: `<p-button text size="small"
  icon="pi pi-copy">` + `DotClipboardUtil` + `DotGlobalMessageService`.
  The button sits next to the text (via `inline-flex`) and only appears
  on row hover. Click is `stopPropagation`'d so it doesn't fall through
  to the row's `openDetail` handler.

Delete confirms (both per-row in history and the ALL scope in the shell):
- New i18n keys `publishing-queue.delete.confirm.header=Delete` +
  `publishing-queue.delete.confirm.message=Are you sure you want to delete
  "{0}"? This action cannot be undone.`
- Header is "Delete"; accept label is "Delete" (reusing the kebab key).
- Styling: `acceptButtonStyleClass: 'p-button-primary'` (NOT danger/red),
  `rejectButtonStyleClass: 'p-button-text'` (tertiary). Default focus
  stays on reject as a safety measure.
- Toolbar trigger relabeled "Delete Bundles" → "Delete" via the i18n
  value of `publishing-queue.delete-bundles`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ze/weight overrides

Match the site-standard table font (per dot-tags-list) by letting the
default `p-datatable` typography apply uniformly across cells:

- Bundle Name: drop `text-sm font-medium text-color` from the cell wrapper
  (kept the `truncate`).
- Bundle Id: drop `text-xs` from the id span — uses the row default like
  every other column.
- Date columns: drop `text-xs` from the wrapper class (kept
  `text-color-secondary whitespace-nowrap`).

Status chip keeps its own `text-xs` internally (chip convention, owned by
the chip component, not the table cell).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…t-1 on Contents search

History table:
- Bundle Id span gets `text-xs` so the ULID reads as a secondary identifier
  (the human-readable Bundle Name in the previous column carries the visual
  weight). Matches the dates' size for visual consistency.
- Date columns (`pq-history-created`, `pq-history-modified`) keep `text-xs` —
  the previous "drop per-cell overrides" commit was too aggressive on these.

Asset list dialog (View Contents):
- `mt-1` on the search bar reserves room for the input's focus ring; without
  it the ring clipped against the dialog header when the input got keyboard
  focus.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… filter

Collapse the two-tab UI (Queue/History) into one table that holds both
history and active (in-progress + scheduled) bundles. Status filter chip
in the toolbar lets the user narrow by any subset of statuses; per-row
kebab adapts to the row's status (Retry on failures, Configure & send on
scheduled/active, View details / View Contents / Generate-download /
Delete everywhere).

Bundle details dialog: meta block switches from a two-column dl/dt/dd
grid to a single-column key/value p-table with shaded labels, matching
the design spec.

Drops the legacy getunsendbundles (user-owned drafts) flow — those don't
live in publish_audit and aren't part of the unified view.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…mpty

Previously the store sent every known `PublishAuditStatus` value when the user
hadn't selected any status chip. Now it omits the param entirely — the BE
treats that as "all statuses." Three benefits:

- Shorter request URLs
- One less thing to keep in sync with the BE enum
- Forward-compatible with new BE statuses (e.g. SCHEDULED, see #36267) —
  they'll appear in the unified table the day the BE ships, no FE deploy
  needed

Removes the local `ALL_BUNDLE_STATUSES` array from the store, makes
`statuses` optional on `ListPublishingJobsParams`, and updates both the store
and service specs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…alog

Replaces the single "Upload Bundle" button with an "Add Bundle" dropdown
(chevron) exposing two actions:

  - Select Bundle  → opens a new two-pane picker dialog
  - Upload         → opens the existing upload dialog

The Select Bundle dialog mirrors the legacy "Bundles" tab in modern shape:
left pane lists drafts (sourced from getUnsendBundles), right pane shows
the active draft's contents. Both panes use fixed-layout PrimeNG tables so
content can't push them past the modal width. Action bar at the bottom:
Remove (bulk) · Download · Configure & send. The active bundle row gets a
primary-tinted background + left accent stripe so it's visible at a glance.

Asset name cell links to the right editor route for contentlet rows via
`DotContentletEditUrlService` (new vs legacy decided by the per-content-type
flag, cached). Non-contentlet rows render as plain text — matches what the
legacy JSP did. HTML pages fall through to the contentlet editor URL rather
than the dedicated page editor because `/api/bundle/{id}/assets` doesn't
return `baseType` — documented in a code comment, acceptable for now.

Also corrects `BundleAssetView` field names (`content_type_name`,
`language_code`, `country_code`) to match what `PublishQueueElementTransformer`
actually emits on the wire — they were previously typed as camelCase but
always undefined at runtime.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…status column

Per design feedback the legacy `publisher_status_*` labels were too long for
the new chip — e.g. "Failed to send to some environments" (35 chars) wrapped
the chip onto two lines and forced the status column to 16rem.

Switches the chip to portlet-scoped keys `publishing-queue.status.*` so we
can shorten labels without affecting the legacy JSPs that still read
`publisher_status_*`. Each enum value gets a short label:

  All success →  Sent / Saved
  In-flight  →  Bundling / Sending / Publishing / Received
  Pending    →  Pending / Waiting
  Failures   →  Build error / Send error / Publish error
                Failed (all) / Failed (some)
                Integrity / Auth error / No license
  Warnings   →  Sent (warn)

Bundles table: status column width 16rem → 9rem (fits the new longest label
`Publish error` with margin).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… truncation

Two issues in the asset table:

1. The Type chip text was wrapping onto two lines for long content type
   names (e.g. "Language Variable"). PrimeNG `<p-tag>` defaults to
   `white-space: normal`, so a 17-char value inside a 10rem cell broke
   across lines.

2. The Name column wasn't truncating long titles (e.g.
   `com.dotcms.repackage.javax.portlet.title.c_Blogs`) despite the
   `truncate` class. Cause: the `<a>` / `<span>` are flex items, and
   flex items have `min-width: auto` so they expand to fit content.

Fixes:
- Name link/span: add `min-w-0 flex-1` so the flex item can shrink and
  `truncate` kicks in. Full text still available via the existing
  `[title]` tooltip.
- Type chip: `styleClass` with `max-w-full whitespace-nowrap overflow-hidden
  text-ellipsis` + nested `.p-tag-label` truncation. Added `[pTooltip]`
  with the full content type for hover-to-see-full.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…xt menu

- Add Items column to the bundles table (asset count as a gray p-tag),
  move Status to last column before the row kebab.
- Use the bundle name as the asset list dialog title via a dedicated
  header component injected through DynamicDialogConfig.templates.header,
  with truncation past 30 chars and a "{N} item(s)" pill next to it.
- Wrap the asset type column in a gray p-tag for visual consistency.
- Right-clicking a row opens the same actions menu as the kebab via a
  shared p-contextMenu; matchMedia polyfill added to the test setup.
- Tighten the toolbar search placeholder to "Search bundles".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…AD probes

The bundle-detail dialog now mirrors the legacy JSP's file-on-disk gating
for the Download Bundle and Download Manifest buttons. Because
GET /api/v1/publishing/{bundleId} doesn't currently expose hasBundle /
hasManifest, the store fires two HEAD probes on openDetail and only
shows each button once its probe confirms a 200 — replacing the previous
status heuristic (SUCCESS_STATUSES) that didn't account for purged
.tar.gz files or older bundles without a manifest entry.

The probe rationale is documented on probeBundleDownload /
probeBundleManifest in the data-access service, on the new store state
fields, and on the canDownloadBundle / canDownloadManifest computeds in
the dialog — including the path to retire the probes once the BE adds
the flags to the detail response.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…e for last update

- Bundle id text turns text-red-700 + medium weight when the row is in any
  failure bucket (mirrors the danger color the status chip already uses),
  giving an at-a-glance failure signal even when the Status column scrolls
  off on narrow viewports.
- Data Entered column switches to MM/dd/yyyy hh:mma absolute format.
- Last Update column adopts the project-standard DotRelativeDatePipe from
  @dotcms/ui ("now", "N minutes ago", "N hours ago", "N days ago" up to
  7 days; absolute MM/dd/yyyy hh:mma after that) — same pipe used by
  dot-folder-list-view, edit-content sidebars, and content-compare.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI was failing on `pnpm install --frozen-lockfile` because
`jest-util@^30.0.2` was added to core-web/package.json without a
matching importer entry in pnpm-lock.yaml.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The BE introduced PublishAuditStatus.SCHEDULED (#36267) — a synthetic
status for bundles with a future publish_date that haven't yet been
picked up by PublisherQueueJob. The FE was missing it everywhere it
mattered:

- Added SCHEDULED to the PublishAuditStatus TS enum (mirror of the Java
  source of truth) with a doc explaining its synthetic nature.
- Mapped SCHEDULED → 'info' bucket in the status chip, alongside
  BUNDLE_REQUESTED / WAITING_FOR_PUBLISHING (all "queued, not yet
  started" semantics).
- Added publishing-queue.status.SCHEDULED=Scheduled so the chip renders
  a label instead of the raw key.
- Updated the chip's exhaustive-coverage test so it stays exhaustive.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…via v1 REST

Wires the multi-bundle "Configure (N)" → "Configure & Send" flow inside the
existing Select Bundle dialog instead of stacking a second modal:

- Adds a step signal ('select' | 'configure') to the dialog and a switch in
  the template. Step 2 embeds the existing DotPushPublishFormComponent (the
  same form the legacy global push-publish dialog uses) so customers see the
  exact field set they know: action / publishDate / expireDate / timezone /
  environment / push filter. Configured once, applied to every selected
  bundle.
- All footer buttons (Remove, Download, Configure) now gate on the checkbox
  selection. Download stays single-target — disabled with a tooltip when
  N != 1.
- Send hits the modern REST endpoint POST /api/v1/publishing/push/{bundleId}
  (PublishingResource.pushBundle, JSON + proper status codes) instead of the
  legacy /DotAjaxDirector/.../cmd/pushBundle AJAX action. Adds
  DotPublishingQueueService.pushBundle and PushBundleForm/PushBundleResultView
  types. Submit fans out one call per checked bundle with the same payload;
  full success closes the dialog (shell refreshes the unified table on
  onClose), partial failure surfaces via DotGlobalMessageService and the
  dialog stays in step 2.
- toPushBundleForm() helper translates the form's DotPushPublishData into
  the v1 shape: renames operation/environments and combines the form's Date
  + selected timezoneId into ISO 8601 with offset (computed via
  Intl.DateTimeFormat so DST is handled correctly).
- DotPushPublishFiltersService is provided at the component level (mirrors
  the legacy DotPushPublishDialogComponent) so the embedded form's
  ngOnInit lookup resolves.
- DotPushPublishFormComponent lives in apps/dotcms-ui; imported via the same
  @nx/enforce-module-boundaries disable already used here for
  DotDownloadBundleDialogService. Track extraction to a shared lib alongside
  the v1 consolidation work (#36048).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
hmoreras and others added 17 commits June 25, 2026 17:59
…paginator

The rows-per-page dropdown was a no-op: onLazyLoad never persisted
event.rows to the store, so the next fetch still went out with the
stale size. The store's reactive effect also did not track rowsPerPage,
so even a direct patch would not have refetched.

- Add setRowsPerPage(rows) to the store. It snaps bundlesPage back to 1
  on a size change so the user does not land on an out-of-range page.
- Track store.rowsPerPage() in the withHooks effect so a size change
  triggers loadBundles automatically (same way bundlesPage already does).
- onLazyLoad now routes a size change through setRowsPerPage when
  event.rows differs from the store, and through setBundlesPage on a
  page-only change. Without that split, PrimeNG's combined "page +
  rows" event was discarded entirely.

Paginator chrome now matches dot-folder-list-view (content-drive):
showFirstLastIcon=false, showPageLinks=false, showCurrentPageReport=true
with a "Page {currentPage}" template. rowsPerPageOptions becomes
[20, 40, 60] (was [10, 25, 50]) and the store default rowsPerPage
bumps from 10 to 20 to keep the dropdown's selected value valid.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…lash

The 15-second auto-refresh routed through the same loadBundles() path as
user-initiated reloads, which sets bundlesStatus to 'loading'. That
flipped the template into the skeleton branch on every tick and produced
a visible flicker between the loading state and the actual rows.

Background polls now run in a "silent" mode: keep the existing rows on
screen, only patch in the new data when the response arrives. User
actions (search, filter, page, sort, post-action refresh) still take the
loud path so the user gets skeleton feedback that the system is
responding to their click.

Silent polls also swallow transient errors instead of flipping the
table into the red error state — the next tick retries. Loud loads
still surface errors via DotHttpErrorManagerService as before.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…abel

The toolbar status filter hardcoded its own list of 18 statuses instead
of deriving from the PublishAuditStatus enum. Two visible bugs:

- SCHEDULED was missing. When the synthetic status was added to the
  enum + chip in d1ee134, the filter array was not updated, so users
  could not filter for future-dated bundles.
- Multiple enum codes share the same translated label (SUCCESS and
  BUNDLE_SENT_SUCCESSFULLY both render as "Sent"; BUNDLE_SAVED_SUCCESSFULLY
  as "Saved"; etc.). The dropdown showed them as separate rows, which
  read as duplicates.

Rewires the filter to:

- Use STATUS_ORDER as the explicit display order, asserted by a new
  "covers every value of PublishAuditStatus" test so future enum
  additions force a placement decision instead of silently disappearing.
- Group options by translated label and store the underlying codes on
  each option. Picking "Sent" sends the union { SUCCESS,
  BUNDLE_SENT_SUCCESSFULLY } to the BE; the option only renders as
  selected when ALL of its grouped codes are present in the store
  filter.
- Add explicit regression tests: SCHEDULED is present, "Sent" appears
  once and groups its two codes, partial-state grouped options are not
  shown as selected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the Select Bundle dialog's Download button — which used to pop
the global DotDownloadBundleDialogComponent on top of the current modal
— with an inline tiered menu that opens upward from the button:

  Download ▾
  ├── To Publish ›
  │   ├── Content and Relationships
  │   ├── Content, Assets and Pages
  │   ├── Everything and Dependencies
  │   ├── Force Push Everything
  │   └── Only Selected Items
  └── To Unpublish

Picking a leaf fires the same backend the legacy modal fires — exact
same payload, same .tar.gz — without the second modal layer.

- DotPublishingQueueService.generateBundle(id, op, filterKey) POSTs to
  /api/bundle/_generate with the legacy {'0' | '1'} operation vocabulary
  the endpoint expects, observes the response as a blob, and parses the
  filename from content-disposition. Uses HttpClient (not raw fetch) so
  the project's auth + error interceptors apply.
- Select Bundle dialog drops DotDownloadBundleDialogService and gains a
  downloadFilters signal (loaded once in ngOnInit via DotPushPublish-
  FiltersService), an isDownloading signal, and a downloadMenuItems
  computed that builds the 2-level MenuItem array. To Unpublish is a
  leaf (legacy modal disables the filter dropdown for unpublish anyway).
- PrimeNG v21 has no placement prop on TieredMenu/SplitButton/Menu/
  Popover, and its DomHandler.absolutePosition measures the viewport,
  not the dialog container, so auto-flip never triggers for a footer-
  positioned trigger. onShow event hook reads the trigger rect via
  getBoundingClientRect and resets the overlay's top so the menu opens
  above the button. Smallest possible override that still uses PrimeNG's
  public event API.
- DotPushPublishFiltersService removed from the component's providers
  array — it's providedIn:'root' and stateless, the per-instance copy
  was a misread of the legacy DotPushPublishDialogComponent pattern.

DotDownloadBundleDialogService + DotDownloadBundleDialogComponent are
untouched — the global modal remains the entry point for every other
caller in the admin UI.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… the link

The asset list inside the Select Bundle dialog rendered each name as an
underlined primary-coloured anchor when an edit URL was resolved. That
gave the row two click targets (anchor + delete) with no whole-row
hover affordance.

- Asset name renders as a plain <span>, no anchor, no underline.
- The row itself is clickable when DotContentletEditUrlService resolves
  an edit URL for the asset; clicking opens that URL in a new tab
  (window.open with target=_blank, noopener) — same context the prior
  anchor used, so the dialog stays open.
- cursor-pointer only when the row has a resolved URL — rows without
  one (templates, languages, etc.) don't pretend to be clickable.
- [rowHover]="true" on the asset p-table for PrimeNG-native row hover
  bg across the whole table (consistent visual feedback regardless of
  click affordance).
- Trash cell gets pr-3 (12px) right padding so the icon doesn't touch
  the dialog edge, and (click)="$event.stopPropagation()" so deleting
  doesn't also navigate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…'t support it

The Data Entered / Last Update / Status column headers carried
pSortableColumn + <p-sortIcon> directives that emitted lazy-load
events with a sortField. The v1 endpoint backing the table
(GET /api/v1/publishing) does not accept a sort query param —
PublishAuditAPIImpl returns rows in a hardcoded "ORDER BY
status_updated DESC" (see SELECT_ALL_ORDER_BY_STATUSUPDATED_DESC at
PublishAuditAPIImpl.java:49). So every click on those headers fired a
useless network round-trip and the visible order never changed.

- Remove pSortableColumn + <p-sortIcon> from the three previously-
  sortable columns. Headers render as plain text.
- Remove the now-unreachable event.sortField branch from onLazyLoad.
- Drop the unused PublishingSortField import.

Code comments in the template and the handler point at the BE
constraint so future devs know exactly where to re-enable when the
backend gains the sort param.

The store's bundlesSort / bundlesSortDirection state and
cycleBundlesSort action are kept intentionally — they'll wire back up
cleanly once the BE supports sort. Removing them now would balloon
into the service signature, the spec, and several call sites with no
immediate benefit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Select Bundle dialog's Remove / Download / Configure buttons used
to render as disabled while the user had nothing checked. The buttons
were the most obvious target for new users, so "disabled with no
explanation" was an information gap.

- The three buttons stay enabled at all times (Download still disables
  itself while a download is in flight, to prevent double-fire). Click
  handlers pre-validate the selection: empty → warn, multi for Download
  → warn, otherwise proceed.
- New validationWarningKey signal holds the i18n key of the current
  warning; the footer renders an inline red message with a triangle
  icon on the left when the key is set.
- onCheckedChange clears the warning automatically so the user gets
  immediate feedback when they take a corrective action.
- Two warning strings: "Select at least one bundle first." (Remove,
  Configure, Download with N=0) and the existing "Select a single
  bundle to download." (Download with N>1, repurposed from the old
  tooltip).
- Download's old `[pTooltip]` is gone — the inline warning replaces it
  and is more discoverable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add a "Scheduled for" row to the bundle details dialog's metadata table that
renders only when the bundle is in SCHEDULED status. The BE already returns
`scheduledPublishDate` on `GET /api/v1/publishing/{bundleId}` and leaves it
null for every other status, so wiring the FE model + a conditional row is
all that's needed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When the user checks one or more rows, Add Bundle is replaced by Retry Send
as the toolbar's primary action (no icon). Refresh stays in place and Remove
moves in as a tertiary danger-text button on the left of Refresh. The
"N selected" label is dropped — the checked rows + active primary read
the selection just fine.

Renames the bulk button label from "Delete" to "Remove" to match the
kebab-menu vocabulary used elsewhere in the portlet.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…onfirm

The old "Select Bundles to Delete" dialog exposed SELECTED / ALL / SUCCESS /
FAILED scopes as buttons. That's redundant now that the toolbar's Remove
button is selection-gated and the status filter + search let the user shape
the working set before selecting. Drop the dialog and replace it with a
ConfirmDialog on the shell (same pattern as dot-tags): "Are you sure you
want to remove N bundle(s)? Bundles are removed in the background — this
action cannot be undone."

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…HEDULED

Cluster of small UX fixes for the "View Contents" dialog:

- Row click opens the contentlet in a new tab (same DotContentletEditUrlService
  pattern as Select Bundle). Non-contentlet rows stay non-clickable.
- Row hover uses PrimeNG's rowHover; cursor-pointer only turns on for rows
  that have a resolved edit URL, so users see which rows are linkable.
- Footer Close button wired to DynamicDialogRef.close.
- Trash button used focus:opacity-100, which kept the icon visible on the
  last-clicked row after mouse-out. Switch to focus-visible so mouse clicks
  don't leave it stuck on; keyboard tab still surfaces it for a11y.
- Extra vertical spacing so the search input doesn't crowd the table.
- SCHEDULED bundles are queued but not in-flight, so the shell now includes
  them in EDITABLE_ASSET_STATUSES — users can prune assets before the cron
  picks the bundle up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…sh icon

The delete icon was flush with the dialog's right edge because the trash
column was 3.5rem wide with only pr-3 of padding on the cell. Widen the
column to 5rem and bump the padding to pr-5 so the icon sits away from the
dialog border.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…s dialog

Two changes to the Bundle Details and View Contents dialogs:

- Add a primary Close button to the Bundle Details footer (previously the
  only exit was the dialog's X). View Contents already had a text Close
  button; promoted it to primary to match. Both dialogs now expose the
  same terminal action.
- Swap the timestamp cells in the details dialog from Angular's 'medium'
  format to `MMM d, y - h:mm:ss a` (e.g. "Jun 25, 2026 - 5:57:02 PM") for
  consistency with the design spec — same format now applies to Scheduled
  for / Bundle start-end / Publish start-end.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…lumns

Three related polish items on the main bundles table:

- Unify column font size — drop text-xs from Bundle Id, Date Entered, and
  Last Update so all columns read at the same weight instead of the id +
  dates being visually demoted.
- Adopt the new date format `MMM d, y - h:mm:ss a` on both Date Entered
  and the fallback in the Last Update dotRelativeDate call (the relative
  "N days ago" behavior is unchanged; it just uses the new format when
  the diff crosses the relative window).
- Freeze the checkbox column (left) and the kebab-actions column (right)
  via pFrozenColumn. On narrow viewports the middle columns scroll
  horizontally while bulk-selection and per-row actions remain reachable.
  On wide screens PrimeNG doesn't render the sticky shadow, so no visual
  regression on desktop.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The trash button on each asset row was persistently visible, competing with
the row content for attention. Match the pattern already used by the View
Contents dialog: opacity-0 by default, group-hover:opacity-100 when the row
is hovered, focus-visible:opacity-100 so keyboard tabbing still surfaces it
for a11y. The row's <tr> gets `group` to scope the hover.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…er, send)

Cluster of related UX improvements to the Add Bundle wizard:

- Pagination: drop BUNDLES_PER_PAGE from 10 to 6 so the list fits without
  scroll. The BE endpoint reports numRows = current page's row count (not
  the absolute total), so switch from numeric maxPage to cursor-style
  bundlesHasMore (true while pages come back full, false on partial). Also
  detect empty responses past page 1 and roll back to the previous page so
  users never land on a spurious "No bundles found" page after a Next
  click on a total that's an exact multiple of 6.
- Dialog header: hide PrimeNG's built-in title (showHeader: false) and
  render a custom header inside the dialog body. When step === 'select' it
  reads "Select Bundle"; when step === 'configure' it swaps to
  "← Configure & Send [N bundle(s)]" — same content that used to live in
  a separate configure-header bar inside the body, now unified with the
  dialog chrome.
- Send button behavior: no longer disabled by form validity. Clicking with
  an invalid or empty form surfaces the appropriate inline warning
  (form-invalid or select-one) via the existing validationWarningKey
  pattern. Only disabled while an upload is in-flight to guard against
  double-submit.
- Add an "Failed bundles retry automatically up to 3×" hint row above the
  configure footer so users understand the retry policy before hitting
  Send.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…inline warning

Match the pattern just landed on Select Bundle's Send button: keep the
Upload button clickable regardless of whether a file is selected, and
surface the missing-file case as an inline error message instead of
silently no-oping on the disabled button. The button only disables while
an upload is in-flight, to guard against double-submit.

Reuses the existing errorMessage signal + p-message channel that already
handled BE errors — server errors and client validation share the same
visual affordance. Auto-clears when the user picks a valid bundle.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@hmoreras hmoreras changed the title feat(publishing-queue): new Angular portlet for the publishing queue feat(publishing-queue): Angular portlet + backend wiring Jul 3, 2026
@github-actions github-actions Bot added Area : Backend PR changes Java/Maven backend code Area : Frontend PR changes Angular/TypeScript frontend code labels Jul 3, 2026
@claude

claude Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Claude finished @hmoreras's task in 1m 10s —— View job


Rollback-Safety Analysis

  • Read docs/core/ROLLBACK_UNSAFE_CATEGORIES.md
  • Get full PR diff (git diff 9e59064e...d6cd9f2c)
  • Analyze diff against every unsafe category
  • Post verdict / apply label

Verdict: ✅ Safe to Rollback

The backend-touching changes in this PR are limited to:

  • PublishingResource.java — adds .requiredPortlet("publishing-queue") to the retry and push endpoints. This is an authorization gate, not an API contract change (no field renames, no shape changes) — falls outside M-3.
  • PortletID.java — adds a new enum constant PUBLISHING_QUEUE_LEGACY("publishing-queue-legacy"). Pure addition, no removal/rename of existing constants.
  • Language.properties — adds ~140 new message keys for the new Angular portlet; no existing keys renamed or removed.
  • portlet.xml — renames the JSP <portlet-name> from publishing-queue to publishing-queue-legacy and adds a new publishing-queue portlet entry pointing at com.dotcms.spring.portlet.PortletController. This is deployment/routing configuration only — it is not a DB table/column rename (H-2 doesn't apply), and it maps to no persisted data structure. Rolling back restores the original portlet.xml, which simply routes publishing-queue back to the legacy JSP; nothing is lost or corrupted.

No runonce database migration tasks, no Elasticsearch mapping changes, no contentlet_as_json model version bump, no table/column drops or renames, no PK restructuring, and no REST/GraphQL response-shape changes are present in this diff. The remaining ~7,800 additions are all net-new frontend code (core-web/libs/portlets/dot-publishing-queue/**, core-web/libs/data-access/**, core-web/libs/dotcms-models/**) and build config, which carry no rollback risk per the reference doc's scope.

Label AI: Safe To Rollback applied.

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

🤖 dotBot Review (Bedrock)

Reviewed 21 file(s); 12 candidate(s) → 10 confirmed, 1 uncertain (unverified, kept for review).

⚠️ Coverage capped: 0 file(s) + 5 lower-severity candidate(s) skipped (limits: 40 files, 12 candidates).

Confirmed findings

  • 🟠 High core-web/libs/data-access/src/lib/dot-publishing-queue/dot-publishing-queue.service.ts:245 — Missing URI encoding for bundleId in URL path
    The URL construction at line 245 directly interpolates bundleId into the path without URI encoding. This could allow path traversal attacks if bundle IDs contain '/' characters or other special URL characters. While bundle IDs are typically UUIDs in dotCMS, the lack of encoding violates secure URL construction practices and leaves a potential injection vector if ID formats ever change.
  • 🟠 High core-web/libs/dotcms-models/src/lib/publishing-status.model.ts:10 — Typo in PublishingStatus enum value
    The enum value FAILED_TO_SENT in PublishingStatus is a typo. Backend status strings likely use 'FAILED_TO_SEND' (past tense 'sent' vs base verb 'send'). This mismatch would cause frontend status filtering/display to break for failed bundle states, as the frontend enum wouldn't match backend responses.
  • 🟠 High core-web/libs/portlets/dot-publishing-queue/src/lib/components/dot-publishing-queue-status-filter/dot-publishing-queue-status-filter.component.html:19 — Missing @output() onChange emitter
    The template binds to (onChange) event but the component lacks an @output() onChange emitter. Angular will attempt to call a non-existent method, causing runtime errors when selecting filter options. The component's TypeScript file does not declare this output, breaking the event binding.
  • 🟡 Medium core-web/apps/dotcms-ui/src/app/app.routes.ts:174 — Missing portletIds in route data for MenuGuardService permission checks
    The new 'publishing-queue' route lacks required portletIds entry in its data object. Comparing to other routes like 'content' (line 50) which includes portletIds: [PortletID.CONTENT], this omission means MenuGuardService cannot perform proper permission checks for this portlet, creating potential security gaps in access control.
  • 🟡 Medium core-web/libs/data-access/src/lib/dot-publishing-queue/dot-publishing-queue.service.ts:300 — Parameter name mismatch in getUnsendBundles filter
    The frontend sends a 'status' parameter in the getUnsendBundles call (line 300 of dot-publishing-queue.service.ts) but the legacy '/api/bundle/getunsendbundles' endpoint requires a 'bundleState' parameter for filtering, as shown in the backend implementation. This mismatch prevents status-based filtering from working correctly.
  • 🟡 Medium core-web/libs/data-access/src/lib/dot-publishing-queue/dot-publishing-queue.service.ts:150 — Legacy deleteBundles payload format incompatible with backend
    The deleteBundles() method sends {bundleIds: [...]} to '/api/bundle/delete' but the legacy BundleResource.deleteBundles endpoint expects a 'bundleSelect' parameter containing comma-separated IDs. This mismatch will cause bundle deletion failures in the Angular portlet.
  • 🟡 Medium core-web/libs/data-access/src/lib/dot-publishing-queue/dot-publishing-queue.service.ts:225 — Filename parsing doesn't handle RFC 5987 encoded filenames
    The code uses split('filename=') which fails to handle RFC 5987 filename*= syntax with encoded filenames. This will break downloads for filenames containing spaces, non-ASCII characters, or other special characters that require proper Content-Disposition header parsing.
  • 🟡 Medium core-web/libs/dotcms-models/src/index.ts:1 — Inconsistent model naming convention
    Exported models 'PublishingBundle' and 'PublishAudit' in core-web/libs/dotcms-models/src/index.ts lack required 'Dot' prefix, breaking established pattern seen in existing models like 'DotBundle' and 'DotPushPublishFilter'
  • 🟡 Medium core-web/libs/dotcms-models/src/lib/publishing-job.model.ts:21 — PushBundleForm allows invalid operation+date combinations
    The PushBundleForm interface defines publishDate and expireDate as optional properties (?), but backend API requires these dates to be present for PUBLISH/UNPUBLISH operations. This allows frontend to submit invalid requests that will be rejected by the backend, causing user-facing errors that could be prevented by frontend validation.
  • 🟡 Medium core-web/libs/portlets/dot-publishing-queue/src/lib/components/dot-publishing-queue-status-filter/dot-publishing-queue-status-filter.component.html:8 — Incorrect signal binding with ngModel
    Using [(ngModel)] with $selected() attempts two-way binding to a signal's value rather than the signal itself. Angular's ngModel expects a property for two-way binding, but signals require using set() to update. This will prevent status filter changes from updating the $selected signal correctly.

🔎 Uncertain (could not confirm or disprove — review manually)

  • 🟠 High core-web/libs/portlets/dot-publishing-queue/src/lib/components/dot-publishing-queue-toolbar/dot-publishing-queue-toolbar.component.ts — Menu items lack permission checks, exposing actions to unauthorized users
    Menu items lack permission checks, exposing actions to unauthorized users

us.deepseek.r1-v1:0 · Run: #28636108729 · tokens: in: 151501 · out: 35629 · total: 187130 · calls: 45 · est. ~$0.397

* (or `endpointProtocols`) flag we can read directly, the FE has to ask
* the actual download endpoint. HEAD is the lightweight option — JAX-RS
* auto-handles HEAD by invoking the `@GET` handler and discarding the
* body, so we get the file-existence answer without paying for the

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 [High] Missing URI encoding for bundleId in URL path

The URL construction at line 245 directly interpolates bundleId into the path without URI encoding. This could allow path traversal attacks if bundle IDs contain '/' characters or other special URL characters. While bundle IDs are typically UUIDs in dotCMS, the lack of encoding violates secure URL construction practices and leaves a potential injection vector if ID formats ever change.

*/
export enum PublishAuditStatus {
BUNDLE_REQUESTED = 'BUNDLE_REQUESTED',
BUNDLING = 'BUNDLING',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 [High] Typo in PublishingStatus enum value

The enum value FAILED_TO_SENT in PublishingStatus is a typo. Backend status strings likely use 'FAILED_TO_SEND' (past tense 'sent' vs base verb 'send'). This mismatch would cause frontend status filtering/display to break for failed bundle states, as the frontend enum wouldn't match backend responses.

[filter]="true"
[filterPlaceHolder]="'search' | dm"
[scrollHeight]="LISTBOX_SCROLL_HEIGHT"
[pt]="listboxPt"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 [High] Missing @output() onChange emitter

The template binds to (onChange) event but the component lacks an @output() onChange emitter. Angular will attempt to call a non-existent method, causing runtime errors when selecting filter options. The component's TypeScript file does not declare this output, breaking the event binding.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI: Safe To Rollback Area : Backend PR changes Java/Maven backend code Area : Frontend PR changes Angular/TypeScript frontend code

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

[Task] Publishing Queue: Angular implementation + backend wiring

1 participant