Skip to content

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

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

feat(publishing-queue): Angular portlet + backend wiring#36413
hmoreras wants to merge 61 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>
…queue Angular portlet

The new Angular publishing-queue portlet raised the total from 54 to 55.
Also assert both publishing-queue and publishing-queue-legacy are present,
mirroring the categories/categories-legacy coverage pattern.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
BUNDLE_REQUESTED = 'BUNDLE_REQUESTED',
BUNDLING = 'BUNDLING',
SENDING_TO_ENDPOINTS = 'SENDING_TO_ENDPOINTS',
FAILED_TO_SEND_TO_ALL_GROUPS = 'FAILED_TO_SEND_TO_ALL_GROUPS',

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.

🔴 [Critical] Enum value 'FAILED_TO_SENT' may mismatch Java backend

The TypeScript enum uses 'FAILED_TO_SENT' (past tense) while Java backend likely expects 'FAILED_TO_SEND' (infinitive). This would cause deserialization failures and incorrect status handling for failed bundle pushes. Grep of backend code shows PublishingAPI uses BundleStatus.FAILED_TO_SEND in BundleStatus.java:45.

*/
SCHEDULED = 'SCHEDULED'
}

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] Incorrect status categorization for READY_STATUSES

The Angular model's READY_STATUSES includes WAITING_FOR_PUBLISHING (line 35), but backend Java code shows this status represents server-side queued bundles, not sender-ready ones. PublishingResource.java lines 415/543 document WAITING_FOR_PUBLISHING as 'Already queued' state, and integration tests (e.g., PublishingResourceIntegrationTest:1327-1333) treat it as a queued status requiring cancellation. This mismatch causes UI to show queued bundles as 'ready to send', enabling duplicate transmission attempts.


private buildOptions(): StatusOption[] {
// Group consecutive-or-not enum values by their translated label, in
// STATUS_ORDER. First occurrence determines display position; later

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] Status filter options not reactive to language changes

The buildOptions() method initializes $options once using DotMessageService.get(), which fetches static translations. Since DotMessageService relies on observable streams for language changes, the filter labels won't update when the language changes without a page reload. This violates i18n expectations in the Angular portlet.

data-testid="pq-refresh-btn" />
@if (hasBulkActions()) {
<p-button
[label]="'publishing-queue.retry-send' | dm"

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 @ViewChild for p-menu reference

The template uses #addBundleMenu in click handler but lacks corresponding @ViewChild('addBundleMenu') declaration in component class. This will cause runtime errors when attempting to toggle undefined menu.

export class DotPublishingQueueToolbarComponent {
readonly store = inject(DotPublishingQueueStore);
readonly uploadClick = output<void>();
readonly selectBundleClick = output<void>();

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] deleteClick output not emitted

The deleteClick event emitter is declared but not emitted in the onRemove() method, preventing parent components from receiving delete action notifications. The toolbar's Remove button click handler triggers confirmation but doesn't propagate the event, breaking bulk delete functionality.

DotGlobalMessageService only surfaces messages when a portlet embeds
<dot-global-message> in its own template — this portlet does not, so retry
outcomes (including 200 + success:false "already in queue" cases) were silent.
Switch to DotMessageDisplayService, which is hosted app-wide by
<dot-message-display> in main-legacy and renders a PrimeNG p-toast.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
*/
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.

🔴 [Critical] Enum value FAILED_TO_SENT is a typo for FAILED_TO_SEND

The frontend enum value 'FAILED_TO_SENT' at line 10 does not match the backend's expected 'FAILED_TO_SEND' status. This mismatch will cause failed bundle statuses from the backend to be unrecognized/misrepresented in the frontend, breaking status filtering and error handling.


describe('source-of-truth invariant', () => {
it('STATUS_ORDER covers every value of PublishAuditStatus (catches future enum drift)', () => {
const ordered = new Set<string>(DotPublishingQueueStatusFilterComponent.STATUS_ORDER);

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.

🔴 [Critical] Numeric enum handling in test causes false validation

The test uses Object.keys(Status) which returns both enum names and numeric reverse mappings for numeric enums (e.g. '0', '1' etc). This results in 10 keys (5 valid names + 5 numeric strings) instead of 5 actual status values, making the test validate against double the expected status count and potentially miss missing status implementations.

@@ -0,0 +1 @@
export * from './lib/lib.routes';

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 route authorization guard

The route configuration in lib.routes.ts does not include a canActivate guard to enforce portlet access permissions, creating a frontend authorization gap despite backend checks. This could allow unauthorized users to access the publishing queue UI components even if API calls fail, violating defense-in-depth security principles.

data-testid="pq-status-filter-chip" />

<p-popover #popover [pt]="popoverPt" (onHide)="listbox.resetFilter()">
<p-listbox

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] Signal and ngModel two-way binding incompatibility

The code uses [(ngModel)]="$selected()" which attempts two-way binding to a signal's value. Angular's ngModel two-way binding requires a property reference, not a signal value accessor. This prevents proper updates to the signal when the UI changes, breaking the filter functionality. Evidence: Angular signals require explicit .set() calls for updates, which [(ngModel)] syntax cannot perform when bound to $selected() directly.

`jest-util` was never imported directly; Jest already provides it
transitively. Removing it and regenerating the lockfile keeps the
resolved version identical (30.3.0) while shrinking package.json.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@switch (bucket()) {
@case ('success') {
<p-chip
class="border-green-100! bg-green-100! text-xs text-green-700!"

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] Incorrect Tailwind important modifier syntax

The Tailwind class 'border-green-100!' uses incorrect syntax for important modifier. Evidence: Tailwind requires '!border-green-100' (exclamation prefix) to apply important flag. Current syntax places '!' after class name which is invalid, breaking border styling for success status chips.

*
* Dates must be ISO 8601 with timezone offset (e.g. `2025-03-15T14:30:00-05:00`).
* `publishDate` is required for `publish` / `publishexpire`; `expireDate` is
* required for `expire` / `publishexpire` — the BE enforces this.

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] PushBundleForm incorrectly marks publishDate/expireDate as optional

The frontend's PushBundleForm interface in publishing-job.model.ts marks publishDate and expireDate as optional (with '?'), but the backend's PublishingResource.pushBundle method requires these fields. This mismatch can lead to API errors when submitting the form without these dates, as the backend enforces their presence.

data-testid="pq-status-filter-chip" />

<p-popover #popover [pt]="popoverPt" (onHide)="listbox.resetFilter()">
<p-listbox

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] Incorrect signal binding in two-way data binding

The code uses [(ngModel)]="$selected" with a signal. Angular signals require using [ngModel]="$selected()" and (ngModelChange)="$selected.set($event)" instead of two-way binding, as signals are accessed via function calls and updated via .set(). This mismatch can cause runtime errors or failed updates to the signal's value.

* filter — picking "Success" only counts when SUCCESS *and*
* BUNDLE_SENT_SUCCESSFULLY are both in the filter. */
protected readonly $selected = linkedSignal<string[]>(() => {
const filter = new Set(this.store.statusFilter());

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] Incorrect status filter selection logic using every() instead of some()

The $selected signal calculation uses every() to check if all status codes in an option are present in the filter. This causes multi-code status options (like 'Success') to only appear selected when ALL their status codes are active. The correct behavior requires some() to match ANY code in the option.

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