feat(publishing-queue): Angular portlet + backend wiring#36413
feat(publishing-queue): Angular portlet + backend wiring#36413hmoreras wants to merge 61 commits into
Conversation
…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', |
There was a problem hiding this comment.
🔴 [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' | ||
| } | ||
|
|
There was a problem hiding this comment.
🟠 [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 |
There was a problem hiding this comment.
🟠 [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" |
There was a problem hiding this comment.
🟠 [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>(); |
There was a problem hiding this comment.
🟠 [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>
…//github.com/dotCMS/core into issue-36040-publishing-queue-angular-portlet
| */ | ||
| export enum PublishAuditStatus { | ||
| BUNDLE_REQUESTED = 'BUNDLE_REQUESTED', | ||
| BUNDLING = 'BUNDLING', |
There was a problem hiding this comment.
🔴 [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); |
There was a problem hiding this comment.
🔴 [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'; | |||
There was a problem hiding this comment.
🟠 [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 |
There was a problem hiding this comment.
🟠 [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!" |
There was a problem hiding this comment.
🟠 [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. |
There was a problem hiding this comment.
🟠 [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 |
There was a problem hiding this comment.
🟠 [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()); |
There was a problem hiding this comment.
🟠 [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.
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-tabViewwith aQueuetab (two-columnREADY TO SEND+IN PROGRESS) and aHistorytab. 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 syntheticSCHEDULEDstatus was added server-side so bundles waiting for a futurepublishDateshow up correctly without a persistent audit row.Summary
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.libs/data-access/src/lib/dot-publishing-queuecovering the v1 publishing endpoints (/api/v1/publishing/*,/api/v1/bundles/*) plus the legacy/api/bundle/*endpoints that v1 hasn't consolidated yet (getunsendbundles,syncupload,_generate,{id}/assets,_download,{id}/manifest).Add BundleforRetry Sendas the primary action. Checkbox + kebab columns stay frozen on narrow viewports.Backend wiring
dotCMS/src/main/webapp/WEB-INF/portlet.xml:publishing-queueJSP entry renamed topublishing-queue-legacy(kept as the rollback escape hatch).publishing-queueentry points at the Angular shell viacom.dotcms.spring.portlet.PortletController.dotCMS/src/main/java/com/dotmarketing/util/PortletID.java: addsPUBLISHING_QUEUE_LEGACY("publishing-queue-legacy").dotCMS/src/main/webapp/WEB-INF/messages/Language.properties: adds portlet titles for bothpublishing-queueandpublishing-queue-legacy, plus the FE's UI strings.com.dotcms.rest.api.v1.publishing.PublishingResource: gates the two destructive endpoints (retry+push) behindrequiredPortlet("publishing-queue"), matching the pattern the other v1 endpoints already use.Test plan
pnpm nx test portlets-dot-publishing-queue-portletpasses (196+ specs green)/dotAdmin/#/c/publishing-queueserved by the Angular shell.tar.gz/.tgzonly; clicking Upload without a file surfaces the file-required warning inlinepublishing-queue↔publishing-queue-legacyinportlet.xmlreverts to the JSP without redeploypublishing-queueportlet,requireLicense(true)on uploads)Follow-ups
/api/bundle/*endpoints into/api/v1/publishing/*is out of scope for this PR — filed as separate tasks per the spike's backend hardening backlog on [Spike] Publishing Queue: consolidated audit — current state, backend API, target UI mapping #36039.🤖 Generated with Claude Code