Skip to content

feat(edit-content): restructure command bar & side panel around Actions tab (#35892)#36021

Open
oidacra wants to merge 28 commits into
mainfrom
issue-35892-restructure-command-bar-sidebar-actions-tab
Open

feat(edit-content): restructure command bar & side panel around Actions tab (#35892)#36021
oidacra wants to merge 28 commits into
mainfrom
issue-35892-restructure-command-bar-sidebar-actions-tab

Conversation

@oidacra
Copy link
Copy Markdown
Member

@oidacra oidacra commented Jun 5, 2026

Proposed Changes

Restructures the new Edit Contentlet command bar and side panel around a single Actions tab (#35892).

Command bar (dot-edit-content-form)

  • Slimmed to [status tag] | Preview … (overflow menu) ▢ (sidebar toggle) and made sticky on scroll.
  • Status now renders as a local p-tag; the lock toggle and inline workflow actions were removed from the bar.
  • New dot-edit-content-command-bar-actions overflow () menu — Permissions, Rules (pages only), View references — which owns the dialog-open logic relocated from the deleted sidebar wrappers.
  • Preview now resolves HTML pages via a new generatePageEditUrl helper in addition to URL-mapped content.

Side panel (dot-edit-content-sidebar)

  • The Info tab becomes Actions (lightning-bolt icon); the Settings tab is removed.
  • The lock control is relocated as a distinct outlined button with a lock icon, separated from the workflow actions by a divider.
  • Workflow actions render stacked full-width, one above another (new stacked mode on the shared dot-workflow-actions), first action primary. Firing stays in the form — the sidebar emits workflowActionFired and the layout delegates to DotEditContentFormComponent.fireWorkflowAction via its viewChild, so all validation/wizard/push-publish logic is untouched.
  • The Locales, Workflow and Details sections are independently collapsible and persist their open/closed state via DotLocalstorageService; headers use the design's uppercase treatment.
  • The information card drops the status chip and the References card (both now live in the command bar).

Removed the now-orphaned dot-edit-content-sidebar-permissions / dot-edit-content-sidebar-rules wrapper components (their dialogs are reused by the new overflow menu).

Out of scope (by design): Locales content is untouched (tracked in #35889) — only its section becomes collapsible. The lock control is relocated only; the richer "Release Lock"/steal-lock flow is deferred. Workflow/Details card visual restyle was intentionally left for a later pass.

Checklist

  • yarn nx lint edit-content / yarn nx lint ui — pass (pre-existing warnings only)
  • yarn nx test edit-content / yarn nx test ui — pass
  • yarn nx build dotcms-ui (AOT template type-check) — pass
  • yarn nx format:write

How to test

  1. Open an existing contentlet.
  2. Command bar shows [status tag] | Preview … ▢, sticks on scroll; no lock toggle / inline workflow actions.
  3. Actions tab: lock button (outlined + icon, divider below); workflow actions stacked full-width with the first action primary; firing Save/Publish still validates the form (scroll-to-error) and opens the wizard for push-publish / action-input.
  4. Locales / Workflow / Details collapse independently and persist across reload.
  5. menu opens Permissions, Rules (pages only) and View references; Settings tab is gone.
  6. Preview shown for a page and for URL-mapped content (existing only); hidden for new content. New contentlet → menu hidden.

…ns tab (#35892)

Command bar (dot-edit-content-form):
- Slim to [status tag] | Preview ... (overflow menu) [sidebar toggle], sticky on scroll.
- Status rendered as a local p-tag; lock toggle and inline workflow actions removed.
- New dot-edit-content-command-bar-actions overflow menu (Permissions, Rules for pages,
  View references) that owns the relocated dialog logic.
- Preview now supports HTML pages via generatePageEditUrl and URL-mapped content.

Side panel (dot-edit-content-sidebar):
- Info tab becomes Actions (bolt icon); Settings tab removed.
- Lock control relocated as a distinct outlined button with a lock icon, separated from the
  workflow actions by a divider.
- Workflow actions render stacked full-width (new `stacked` mode on dot-workflow-actions),
  one above another, first action primary; firing stays in the form via layout wiring
  (workflowActionFired output -> DotEditContentLayoutComponent.onWorkflowActionFired).
- Sections (Locales, Workflow, Details) are independently collapsible and persist their
  state via DotLocalstorageService; headers restyled to the design's uppercase treatment.
- Information card drops the status chip and References card (moved to the command bar).

Removed the now-orphaned sidebar permissions/rules wrapper components.
@github-actions github-actions Bot added Area : Backend PR changes Java/Maven backend code Area : Frontend PR changes Angular/TypeScript frontend code labels Jun 5, 2026
@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Jun 5, 2026

Claude finished @oidacra's task in 3m 43s —— View job


Code Review

  • Gather context
  • Review command-bar-actions component (new)
  • Review store changes (lock, workflow, content features)
  • Review sidebar restructuring
  • Review utility functions (generatePageEditUrl)
  • Review dot-workflow-actions stacked mode
  • Final findings summary

Findings

1. $hasReferences races with sidebar load — may disable References in the overflow menu prematurely

$hasReferences in dot-edit-content-form.component.ts (line 204–208) reads information.relatedContent() from the store, which is initialised to '0' and only populated after getReferencePages fires inside DotEditContentSidebarComponent.#informationEffect. That effect is gated on isSidebarOpen (line 135). So if the sidebar is closed when the user opens an existing contentlet, relatedContent stays '0', $hasReferences returns false, and the View References menu item is permanently disabled for that session — even when references exist. Consider always fetching the reference count regardless of sidebar state, or initialising relatedContent to null and treating it as "unknown" rather than "zero" until the API responds.

2. Stale DynamicDialogRef prevents re-opening a dialog after the component is destroyed and recreated

In dot-edit-content-command-bar-actions.component.ts, the three #xxxDialogRef fields are cleared in onClose (lines 136–138, 166–167, 202–204). However, the onClose subscription uses takeUntilDestroyed(this.#destroyRef), which unsubscribes on component destroy before the dialog's own onClose fires. If the dialog is still open when the component is destroyed (e.g. during navigation), #permissionsDialogRef is closed (the onDestroy hook, line 100–104 handles this) but the undefined assignment in the subscription will never run. The next time the component is constructed the ref fields start as undefined, so this is fine. The issue is narrower: if the component is destroyed mid-dialog, the dialog is closed but the ref field in the new instance won't exist (it's a fresh constructor), so no real bug — just document this clearly or simplify with finally on the observable instead of a separate subscription.

3. generatePageEditUrl adds ?host_id= to the page URL directly, potentially creating a double query-string

functions.util.ts line 455:

params.set('url', `${contentlet.url}?host_id=${contentlet.host}`);

If contentlet.url already contains a query string (e.g. /about-us?param=1), this produces /about-us?param=1?host_id=… — the second ? is not valid. generatePreviewUrl (line 430) has the same pattern with URL_MAP_FOR_CONTENT. Both should use new URLSearchParams or a URL object to append host_id safely. Fix this →

4. onLockAction in DotEditContentSidebarComponent: lockedByName() can return null, but escapeHtml is called without a null guard

dot-edit-content-sidebar.component.ts line 183:

lockedBy ? ` (${escapeHtml(lockedBy)})` : ''

lockedBy is string | null so the ternary handles null — but only at runtime. The type annotation for lockedByName() (computed in lock.feature.ts line 94) already returns string | null, so this is fine. But notice that lockedBy is assigned from this.$store.lockedByName() which could return null; the escapeHtml(lockedBy) call then narrowly avoids accepting null via the ternary. However, the code reads lockedBy ? (${escapeHtml(lockedBy)}) : '' which would pass the empty-string case on '', not just null. This is a minor correctness issue only for the empty-string case, but lockedByName already returns null not '' when there's no name, so it's safe in practice.

5. Hardcoded 'Actions' string in the tab tooltip (not i18n)

dot-edit-content-sidebar.component.html line 19:

<p-tab [value]="0" [pTooltip]="'Actions' | dm" tooltipPosition="bottom">

The string 'Actions' is passed to dm (the DotMessage pipe), which looks up a message key. The key Actions does exist in Language.properties (value: "Actions"), so this works. It's worth keeping aligned with the naming convention used by other tabs ('edit.content.sidebar.tab.history', 'edit.content.sidebar.tab.comments'), which use namespaced keys.

6. Lock button inline styles are a workaround for a PrimeNG directive limitation

dot-edit-content-sidebar.component.html lines 62–64:

[class.p-button-outlined]="$store.isContentLocked()"
[style.border-color]="$store.isContentLocked() ? null : 'var(--p-surface-200)'"

This is correctly called out in the comment above it — the pButton directive doesn't reactively remove p-button-outlined. The workaround is correct. No action needed, but consider tracking a PrimeNG issue for upstream fix.

7. $collapsed linkedSignal re-seeds from localStorage on every key change, not on sidebar close/reopen

dot-edit-content-sidebar-section.component.ts lines 62–68: the comment correctly documents this; the note "If DotLocalstorageService ever becomes signal-backed, this would snap back…" is an active risk. Currently safe, but the defensive comment should stay.

8. dot-workflow-actions: stacked and groupActions interaction is not guarded

dot-workflow-actions.component.ts: the docstring says "Takes precedence over the flat overflow layout; ignored when groupActions is true." But there's no assertion or guard in the code — if a caller sets both stacked=true and groupActions=true, $groupedActions would still be computed and the template would render both split-buttons and stacked buttons. Low risk (callers in this PR are consistent), but could be worth a console.warn in dev mode.

9. checkCanLock fires on every contentlet change (including lock/unlock updates)

lock.feature.ts lines 273–289: the effect fires checkCanLock whenever contentlet() changes. A lock/unlock update patches the contentlet in the store (patchState(store, { contentlet: { ...current, locked: ... } })), which triggers this effect again, resulting in a redundant canLock API call immediately after every lock or unlock. The API response would likely be the same, but the extra round-trip is unnecessary. Consider keying the effect on contentlet?.inode only so it skips pure lock-state updates.

10. Missing i18n key for the new "View References" command-bar action label (minor)

Language.properties line 6444: edit.content.sidebar.command-bar.references=View References. The key name mixes two conventions — sidebar (which is being removed as the home of this action) and command-bar (the new home). Not a bug, just a naming smell that will read oddly if another engineer looks at the properties file without context.


Overall: The restructuring is well-reasoned and the code is clean. Issues #3 (double ? in URL) and #1 (references count race with closed sidebar) are the only ones that could surface as actual bugs for users. The rest are low-severity design notes.

Override the Tag `success` color scheme in CustomLaraPreset so it uses a tinted
background with dark text ({green.100}/{green.700}) instead of Lara's default solid
fill + white text. Applies globally to every p-tag success, including the Edit
Content command-bar status; the primitives map 1:1 to the Claude Design green tones.
… fluid buttons (#35892)

- Make the status p-tag a fully-rounded pill globally via CustomLaraPreset (tag.root:
  9999px radius, 4px 12px padding, 600 weight) instead of a per-instance class, so a
  forgotten class can never make a tag look different.
- Replace the statusSeverity template method with memoized computeds ($contentStatus
  via DotContentletStatusPipe + $statusSeverity), backed by a pure exported
  contentStatusSeverity() so the mapping stays unit-testable. Avoids the per-CD method call.
- Use the native PrimeNG [fluid] input for full-width stacked workflow actions and the
  lock button instead of styleClass (class does not reach p-button's inner element).
…35892)

Move the tag's hardcoded shape/typography values into a css block driven by Tailwind
variables — same mechanism as the chip preset — instead of magic numbers: padding uses
var(--spacing), font-weight uses var(--font-weight-semibold), and the full pill radius
uses calc(infinity * 1px) (what Tailwind's rounded-full emits; there is no --radius-full).
…with the design (#35892)

- Invert sidebar backgrounds to match the design: the panel is now white and the
  Workflow/Details sections sit in surface-50 cards (was a gray panel with white cards).
- Sidebar width 360px (was 21.875rem ≈ 306px at the 14px root) and the left drop shadow
  is removed — only the thin border remains, per the design.
- Accordion headers: chevron moved to the right, points up when expanded, and gains a
  surface-50 hover background.
- Rename the "General" section to "Details" (new edit.content.sidebar.details.title key).
- Details card redesigned to the design spec: a single surface-50 card with a key/value
  grid (Content type link, Modified by + initials avatar, Modified) and a footer holding the
  Copy Identifier button and a "View as JSON" link. The copy button moved out of the section
  header; Created/Published columns were dropped to match the design.
- Tabs use an underline indicator (active border on the bottom, not the top) with no static
  tab background, via the global Tabs preset.
…er accordion titles (#35892)

- Lock button, Actions tab tooltip and the Details section title now reuse existing,
  already-translated message keys (Unlock / Make-Editable, Actions, details) via the dm
  pipe instead of brand-new keys. New keys only resolve after a backend rebuild, so they
  rendered as raw uppercase keys in a running instance; the existing keys translate
  immediately. Removed the now-unused new keys.
- The lock wording is action-based: "Lock for Editing" when unlocked, "Unlock" when locked.
- Vertically center the collapsible section titles: the section had asymmetric padding
  (pt-0 pb-3) which pushed the header toward the top divider; it is now symmetric (py-3),
  and the title uses leading-none so the uppercase text sits on the true vertical center.
oidacra added 2 commits June 8, 2026 13:31
Drop the font-size override on the top-bar messages. It was converted assuming a
16px root (0.84375rem) but the app root is 14px, so it rendered at 11.8px — too
small. The fixed 54px height already handles alignment, so the text keeps its
default 14px scale.
Set the top-bar message font to 13.5px (in px, since the app root is 14px and a
rem value would not land on 13.5) and align the icon-to-text gap across all top-bar
messages to gap-3 (12px) so the info and lock banners match. Previously the info
banner used gap-1 (4px) while the lock banner used gap-3.
…35892)

Mirror the sidebar tabs: give the form tablist a defined height and make the tabs
and tab-list fill it (min-h-[53px], [&_.p-tab]:h-full, [&_.p-tablist-tab-list]:h-full).
The active-tab underline now sits flush on the bottom border instead of floating
~3px above it, matching the sidebar tabs.
Resolve the actionable findings from the automated PR review:

- Dialogs: set closeOnEscape: true on the permissions and rules dialogs (CLAUDE.md
  requires all dialogs to be ESC-closable); update the matching spec expectation.
- Permissions dialog: drop the invalid transitionOptions: null option.
- form: convert the $appendContext getter to a computed so its object reference is
  memoized instead of recreated on every change-detection cycle.
- Move isPage into the store as a single computed; the form now reads $store.isPage
  and the sidebar's duplicate (unused) computed is removed.
- references dialog: drop the dead error handler on the onClose subscription (the
  Subject never errors) — match the other two dialogs.
- Document the intentional reset-workflow validation bypass in the sidebar with a TODO.
@alwaysmeticulous
Copy link
Copy Markdown

🤖 No test run has been triggered as your Meticulous project has been deactivated (since you haven't viewed any test results in a while). Click here to reactivate.

Last updated for commit a352347. This comment will update as new commits are pushed.

…user (#35892)

When content is locked by a different user and the current user can release it, the
sidebar lock button now reads "Release Lock" and asks for confirmation before stealing
the lock — instead of releasing it silently.

- store/lock: add isLockedByAnotherUser and lockedByName computeds.
- sidebar lock button: label is "Release Lock" (locked by another), "Unlock" (own lock)
  or "Lock for Editing" (unlocked); the click is routed through onLockAction().
- onLockAction opens a confirmation dialog (existing p-confirmDialog/ConfirmationService)
  before releasing another user's lock; own-lock unlock and locking act directly.
- i18n: add edit.content.release.lock.confirmation.header/message.
…log (#35892)

- Constrain the shared confirm dialog to 32rem so the long release-lock message wraps
  instead of stretching across the viewport.
- Make "Release Lock" the primary action and "Cancel" an outlined/secondary button.
…35892)

Map the "New" status to the `secondary` severity instead of `warn`, so a brand-new
unsaved contentlet shows a soft gray pill (Lara secondary = surface.100/surface.600)
rather than a warning-orange one. Reads as informational and matches the soft-pill
style of the other statuses without colliding with Revision (info/blue).
- showPreview(): early-return when the contentlet is null so a concurrent state change
  between render and click can't trigger a null dereference in the URL generators.
- lock button: pass `undefined` (not `null`) to the PrimeNG `severity` input.
- sidebar: rename fireWorkflowAction → fireResetWorkflowAction and tighten its doc so the
  validation-bypassing direct-fire path is clearly reset-only (other actions go through the
  workflowActionFired output).
- test: add a coupling test asserting every DotContentletStatusPipe output maps to its
  intended status-chip severity, so a future pipe label change fails loudly.
…review (#35892)

- Escape the API-provided locker name before it is interpolated into the [innerHTML]
  banner and the release-lock confirm message (new escapeHtml util). Also drop the
  redundant <b> wrap — the i18n keys already bold {0}, so the name was double-bolded.
- Extract the duplicated lockedBy-resolution into a single resolveLocker() util (shared
  by lockWarningMessage, lockedByName and isLockedByAnotherUser).
- command-bar: guard openReferencesDialog() with !hasReferences() so a direct call can't
  open an empty dialog.
- layout: guard onWorkflowActionFired() against an undefined inode before firing.
- dot-workflow-actions: getVariant() returns undefined (not null) for cleaner typing.
- Document the section linkedSignal's non-reactive localStorage dependency and the
  stacked-mode SEPARATOR drop.
- Tests for escapeHtml and resolveLocker; specs updated for the above.
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.

Restructure Command Bar and Side Panel around new Actions tab (Edit Contentlet)

2 participants