Skip to content

feat(content-drive): browse scopes for All, site root and System Host (#37426) - #37487

Open
zJaaal wants to merge 54 commits into
mainfrom
37426-content-drive-browse-scopes
Open

zJaaal wants to merge 54 commits into
mainfrom
37426-content-drive-browse-scopes

Conversation

@zJaaal

@zJaaal zJaaal commented Sep 9, 2026

Copy link
Copy Markdown
Member

Proposed Changes

Content Drive can express one browse scope today: the whole current site, at every depth, with System Host mixed in. Browsing "the site" and browsing "the site root" are the same request, and System Host cannot be reached on its own. This PR splits that into three browse scopes picked from the sidebar. Spelled in full throughout: Content Drive is separately gaining a search scope (#37479) that says which fields a search reads, and bare "scope" would name either.

  • All, above the hierarchy — the whole site at any depth, content only, and read-only: nothing can be uploaded, created, or dropped there.
  • The site row, in the hierarchy — now means the site root: what sits at the top of the site, plus its top-level folders. Folders below it are unchanged.
  • System Host, below the hierarchy — shared content on its own, and a drop target, so content can be moved there by dragging.
  • The Show Shared Assets chip becomes Show System Host, and is offered only while All is selected.
  • One URL value says where the drive is browsing, rather than a scope beside a path that could disagree with it: absent means All, / means the site root, a deeper path means that folder, and a reserved word means System Host. Links made before this change keep meaning what they meant.
  • Backend: the listing gains a browse scope, and a request that carries none behaves exactly as it does today. That is what leaves the Asset Picker and the other consumers of the shared listing untouched.

The spec lands first, in specs/37426-content-drive-browse-scopes/spec.md; the implementation follows in this same PR. Draft until both are in.

The contract delta is worth reading on its own if you own a caller of POST /v1/drive/search: contracts/drive-search-browse-scope.md. Short version: omitting the new field returns what it returns today, byte for byte, and the field is deliberately not defaulted, because defaulting it would silently turn the Asset Picker's folder requests recursive.

Merged with main, including the upload destination this PR warned about

The bulk file upload work (#37166) landed first, and this branch has merged it. The warning that
stood here was right: the conflict covered the import block but not the destination expression,
which merged clean and would have shipped currentSite()?.identifier — the current-site fallback
this PR exists to remove, silently sending System Host uploads to whatever site the switcher shows.

The batch call is kept, the destination is kept ($newContentHostId()), and a test now pins it:
it fails against main's line with siteId: "site-001" where "SYSTEM_HOST" belongs.

Three other things the merge needed, all in the same commit: the listing SQL moved the folder
predicate into a materialized candidates CTE that still read the forceSystemHost boolean this
branch replaced with a three-state; signalStore accepts at most sixteen features and the merged
store asked for seventeen, which Vitest cannot catch because it transpiles without typechecking;
and the host-clause guard asserted the site id was the first bound value, which the CTE changed
by binding the folder path ahead of it. The statement and its parameter list still line up, so that
assertion is re-stated without the index rather than relaxed.

A run in flight is now a toast, not part of the filter row

The toolbar drew the in-flight indicator itself, at the end of the filter row. That row is where the
user works, so filter chips coming and going beside a status that appears and disappears moved the
controls under the pointer. dot-status-toast is a new outlet in libs/ui rather than a variant of
dot-toast: that one is a report, several lines wide because it has to be, while this states an
outcome in a couple of words, so it is sized to its text and drops detail entirely. It claims a
key, because a portlet provides one MessageService and a keyless outlet renders every message on
it. Colour comes from Lara through the dotCMS preset; the prototype's dark pill is deliberately not
reproduced.

Two things fixed in the same pass: uploads borrowed the workflow sentence and read "Applying Upload
to demo.dotcms.com", a phrasing for an action applied to content rather than files going into a
place, so they now carry their own wording; and a backgrounded upload announced itself twice, as the
wide advisory and as the status, so the advisory is gone.

System Host and the site root no longer fight over the sidebar selection

Opening System Host showed it selected and the site root selected, and the location then snapped
back to the site root on its own. The sidebar treated every location as a folder path, but only one
of the three kinds is: all site content is the absence of a location, System Host is a host rather
than a folder. So System Host was resolved as /SYSTEM_HOST/, a folder nobody has, and the empty
result fell back to selecting the site row — which the shell then read as the location, writing it
back to the site root. Which effect ran first decided whether it happened, so it came and went.

The mirror image of the same confusion was in the sync that keeps the tree in step with the
location: the tree marks its site row with an empty path while the site root as a location is
/. Looking / up literally matched no node, so the sync cleared the selection every time the user
stood on the site root.

An upload to System Host now refreshes the grid it landed in

A finished run reloads only when the folders it changed include the one on screen, and both sides of
that comparison were built the same wrong way: the switcher's hostname glued to the location. That
holds while the location is a path on that site, and System Host is not one. The listing described
itself as //demo.dotcms.comsystem_host, the batch called itself //demo.dotcms.com, and neither
named where the files actually went. browsedFolderRef now answers "which folder is on screen" for
all three kinds of location, and an accepted batch with no folder of its own records that same
reference rather than rebuilding one from the switcher — the destination the upload already resolves
through $newContentHostId(), so the run and the listing cannot disagree.

Unknown fields on the drive request form are ignored, not refused

Raised by the automatic review and confirmed before acting on it. AbstractDriveRequestForm carried
no @JsonIgnoreProperties(ignoreUnknown = true), and DotObjectMapperProvider.createDefaultMapper
disables only WRAP_EXCEPTIONS, so FAIL_ON_UNKNOWN_PROPERTIES sat at Jackson's enabled default. A
binary rolled back to a build predating browseScope would therefore refuse every request carrying
it — and since toRequestLocation sends a scope for all site content, the site root and System
Host, that is the view the drive opens on, not an edge case. Browsing would stay broken until a hard
refresh nothing prompts the user to make.

The trade is real: a caller who misspells a field now gets silence instead of an error naming their
typo. A11yAgentStopForm, PageScanCheckForm and FileUploadDetail make the same trade, and the
first has the identical Immutables shape, which is what shows the annotation survives the generation.

No new folder on System Host, and a path that resolves

"New Folder" there opened a dialog previewing //demo.dotcms.comSYSTEM_HOST/. The dialog anchors a
new folder on the location the drive is open on and pastes it after the hostname, but only one of
the three locations is a folder path — all site content is the absence of one, and System Host is a
reserved word, recognisable as not-a-path precisely because it does not begin with /. It now
anchors only on a location that is a path.

The entry should not be there at all, though: System Host lists no folders (listsFolders answers
false for that scope, and its sidebar row opens no tree), so a folder created there could never be
shown again by this portlet. The "New" menu drops it there and keeps the content types. All site
content is untouched — its location is absent, so a new folder lands at the site root, which is
where that entry points for uploads too.

Not verified against a running instance: whether dotCMS itself permits a folder on SYSTEM_HOST is
unanswered, because the QA environment was unreachable. This is a decision about Content Drive, not
about the platform.

Checklist

  • Tests
  • Translations
  • Security Implications Contemplated (add notes if applicable)

Additional Info

Reviewers: the spec is the thing to read now. The code will be written against it, so a disagreement is much cheaper to settle here than after the implementation.

Fixes #37426

🤖 Generated with Claude Code

… and System Host

Content Drive has one browse scope today: the whole current site at any depth,
with System Host mixed in. Selecting the site and browsing the site root are the
same request, and System Host cannot be reached on its own.

Specifies three scopes picked from the sidebar. All (the whole site, content
only, read-only) sits above the site hierarchy, whose site row now means the
site root. System Host sits below it, listing shared content on its own and
accepting content dropped onto it. The "Show Shared Assets" chip becomes "Show
System Host" and applies only to All.

Callers that ask for no scope keep today's behavior, which is what leaves the
Asset Picker and the other consumers of the shared listing untouched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@claude

claude Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Claude finished @zJaaal's task in 4m 55s —— View job


Content Drive browse scopes — code review

Review checklist

  • Read the diff against origin/main
  • Review backend (BrowserAPIImpl, BrowserQuery, SystemHostMode, BrowseScope, AbstractDriveRequestForm, ContentDriveHelper)
  • Review frontend (store, withSidebar, sidebar, toolbar, shell, functions.ts, dot-status-toast)
  • Recheck the three prior review findings
  • Post review

I reviewed the full diff for bugs introduced by this PR. The change is coherent end to end: the location→(assetPath, browseScope) mapping in toRequestLocation, the three-state SystemHostMode, and the ContentDriveHelper translation all line up with the BrowseScope enum, and the frontend DotContentDriveBrowseScope string union matches the backend enum values exactly. Tests are present and the integration test is wired into a suite, so nothing is silently skipped.

New Issues

None found (no 🔴 / 🟠 / 🟡).

Notable correctness points I specifically verified and consider sound:

  • BrowserAPIImpl.buildPureESQuery now derives the host clause from systemHostMode instead of forceSystemHost || folder.isSystemFolder(), so the PURE_ES path matches the SQL path (appendSiteQuery) for the same request — the prior divergence at every site root is closed (BrowserAPIImpl.java:729, :2427).
  • AbstractDriveRequestForm.checkBrowseScopeIsAtTheSiteRoot() rejects a scope sent with a non-root path via pathWithinSite(), which splits the string without a DB lookup; //site/ and //site both pass, deeper paths 400 (AbstractDriveRequestForm.java:160).
  • ContentDriveHelper.systemHostModeFor/skipsFolderConstraint keep an omitted scope byte-identical to today's behavior and never make a folder request recursive (ContentDriveHelper.java:263).
  • withSidebar selectionSync and the shell setPathEffect guard each other with !== comparisons + untracked, so the /'' (site-root vs tree site-row) translation does not loop.

Existing

None flagged (only reviewing PR-introduced changes).

Resolved

  • P1 — withSidebar cleared/bounced the site-root selection. selectionSync now translates the / location to the tree's empty-path site row (findNodeByPath(folders, path === ROOT_PATH ? '' : path)), and loadFolders only preselects a node for a real folder path (withSidebar.ts:262, :88).
  • P2 — AbstractDriveRequestForm would 400 on unknown fields (rollback safety). @JsonIgnoreProperties(ignoreUnknown = true) is now on the interface (AbstractDriveRequestForm.java:79), so an older node ignores the new browseScope field rather than failing deserialization.
  • P3 — folder-dialog easing (afterNextRender/#easePanelHeight) never attached. The easing feature was reverted (commit d46e0243); that code no longer exists in dot-content-drive-dialog-folder.component.ts. (The remaining afterNextRender in dot-content-drive-sidebar.component.ts:249 is the unrelated, pre-existing reveal-node scroll.)

All is the default only on a cold open. A URL that names a scope opens on that
scope; one that names none opens the folder it points at, or All when it points
nowhere. That keeps links made before this feature meaning what they meant, and
it makes the scope behave like every other filter rather than like a preference
the URL cannot override.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ihoffmann-dot

Copy link
Copy Markdown
Member

The spec looks solid overall — two acceptance-criteria gaps I'd flag before signing off:

1. Query builder parity has no dedicated test

The driving issue notes buildPureESQuery and the SQL path disagree on how System Host is handled, and today that only bites under the PURE_ES heuristic (not the default). FR-012 and SC-004 cover this only implicitly, through a general "same results with/without search" criterion.

Given this is a real, already-identified bug, I'd want an explicit acceptance scenario that exercises both query heuristics (HYBRID_SINGLE_CHUNKED_QUERY_ES and PURE_ES) for the ROOT and SYSTEM_HOST scopes specifically, so it can't silently regress.

2. No-scope backward compatibility isn't broken down by consumer

SC-007 says "surfaces other than Content Drive keep listing exactly what they list today," but there are 6 named consumers of the shared listing:

  • Asset Picker
  • Assets API
  • The old file browser
  • The deprecated tree endpoint
  • The legacy admin browser
  • The Velocity viewtool
  • 2 internal callers

One aggregate success criterion doesn't tell us which of those get an actual regression test vs. which are assumed safe by inspection. Could the plan/tasks phase enumerate this explicitly?

One URL value says where the drive is browsing, rather than a scope beside a
path that could disagree with it: absent means All, `/` means the site root, a
deeper path means that folder, and a reserved word means System Host. Real
folder paths all begin with `/`, so a reserved word can never collide with one,
and links made before this feature keep meaning what they meant.

The search rule was worded backwards. It said a selection returns the same
items with or without a search, which reads as "search does nothing" and
contradicts the success criterion that already allows for what a search
excludes. It now says filtering may only remove items from the scope, never add
items from outside it, whichever internal search path serves the request, and
both paths are exercised for the site root and System Host rather than only the
one that runs by default.

Saying All and System Host have none of the behaviors of folders was too broad,
since System Host does take content. Narrowed to structure.

Backward compatibility is no longer one blanket claim over eight consumers: the
shared seam they all reach gets a test, and each consumer is recorded as covered
by it or checked by inspection.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@zJaaal

zJaaal commented Sep 11, 2026

Copy link
Copy Markdown
Member Author

Both added in 6b40cd126a. Thanks, the second one was a blanket claim that deserved to be broken up.

Query-path parity. You are right that it was only implied. Two things changed. The requirement it hung off was actually worded backwards: it said a selection returns the same items with or without a search, which reads as "search does nothing" and contradicts the success criterion that already allows for what a search legitimately excludes. It now says a search or field filter may only remove items from the selected scope and never add items from outside it, whichever internal search path serves the request. On top of that there are now explicit acceptance scenarios for the site-root and System Host scopes stating both paths return the same items, and the Legacy section spells out that only one of the two runs by default, so proving they agree means deliberately exercising each rather than waiting for the non-default one to turn up. Concretely that is flipping BROWSE_API_HEURISTIC_TYPE between HYBRID_SINGLE_CHUNKED_QUERY_ES and PURE_ES in the test, no production change.

Per-consumer backward compatibility. Added as a requirement rather than left to the plan, since it is the promise and not just how we check it: the claim must be demonstrated rather than assumed, and every consumer must be recorded as covered by a test or checked by inspection, with none unaccounted for. The success criterion now says the same.

Worth knowing how small that turns out to be in practice. All eight of those callers reach the listing through one builder, and none of them sets the System Host flag or the folder-skip flag today. So the entire risk collapses to a single question: does the new three-state default to the same host clause the boolean produces today? One test pinning the default at that shared seam covers every consumer at once, and the rest of the list is inspection. The plan will carry the enumeration so it is visible rather than assumed.

Reply written by Claude on @zJaaal's behalf.

@zJaaal

zJaaal commented Sep 11, 2026

Copy link
Copy Markdown
Member Author

All three addressed in 6b40cd126a.

The search requirement was worded backwards. Correct, and it was the one worth catching. It now says a search or field filter may only remove items from the selected scope and never add items from outside it, which is the invariant that was meant, and it adds that the answer must not depend on which internal search path served the request.

"None of the behaviors that belong to folders" was too absolute. Also correct. Narrowed to structure: no expansion control, no children, nothing beneath them to navigate into, with an explicit note that this does not stop System Host accepting content.

The site-row scope had no URL representation. This was the real gap, and the fix went further than the suggestion. Rather than adding a scope parameter beside the existing location, the spec now requires a single value to say where the drive is browsing, because two values can disagree and then neither is the answer. Absent means All, / means the site root, a deeper path means that folder, and a reserved word means System Host. Every real folder path begins with / and no reserved word does, so the two can never collide. This also drops the earlier precedence rule for a URL that named a scope and carried a folder, which was only needed because the two could contradict each other.

Reply written by Claude on @zJaaal's behalf.

ihoffmann-dot
ihoffmann-dot previously approved these changes Sep 11, 2026
zJaaal and others added 2 commits September 11, 2026 16:48
Content Drive is separately gaining a search scope (#37479), which says which
fields a search reads. This one says where you are browsing. Two different
things, and "scope" on its own would name either, so every mention here is now
spelled in full and the Key Entities entry says what the other one is.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

⚠️ AI review failed

Claude did not produce a review — the backend call errored before generating any output (provider: anthropic-bedrock, model: global.anthropic.claude-sonnet-5). This usually means the model has no Bedrock access grant in the target account, or the model ID is invalid — not a problem with this PR.

Run: #34640908222

@github-actions

Copy link
Copy Markdown
Contributor

⚠️ AI review failed

Claude did not produce a review — the backend call errored before generating any output (provider: anthropic-bedrock, model: global.anthropic.claude-sonnet-5). This usually means the model has no Bedrock access grant in the target account, or the model ID is invalid — not a problem with this PR.

Run: #34640908195

zJaaal and others added 6 commits September 11, 2026 17:15
Two callers share POST /v1/drive/search, and one of them is owned elsewhere, so
what the new field means when it is absent is the whole promise. Written down
rather than left to the implementation: omitting the browse scope returns what
it returns today, byte for byte, and the field is deliberately NOT defaulted to
ALL, because ALL as unconditional folder-skipping would silently turn the Asset
Picker's folder requests recursive.

An explicit scope is valid only at the site root. A folder path with any scope
is refused rather than resolved by precedence, following the bulk upload form:
picking one of two contradictory statements puts content somewhere the caller
did not choose.

plan.md, research.md and quickstart.md stay local per .gitignore:228.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…place

"All" said nothing about what it was the all of, and Content Drive is separately
gaining a search scope whose own option reads "All Content". It is now All Site
Content, which also explains why it sits above a section headed SITE HIERARCHY:
the first row is the site seen flat, the section below is the same site seen by
folder.

System Host is fixed at the bottom while the hierarchy scrolls beneath it. The
tree loads lazily and grows as folders open, so its bottom edge moves, and an
entry placed after it would get steadily harder to reach. The top row needs no
such treatment, since scrolling back up is a cheap, known gesture.

The Show System Host toggle stays visible everywhere and is disabled outside All
Site Content, saying why. Hiding it reflowed the filter row and moved the
neighbouring controls under the pointer on every selection change.

Switching sites while System Host is selected keeps the selection, re-renders
the hierarchy for the new site so the switch visibly does something, and keeps
the highlight on System Host rather than letting it drift onto the new root.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A flat view over a whole site cannot tell two files of the same name in
different folders apart, because the listing has no path column. The gap is
inherited rather than introduced: today's site view is already that flat view.
Naming it and making it the default raises the cost, so it is written down as
consciously accepted with the fix named, rather than left for a reviewer or QA
to rediscover as a defect in this work.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…for #37426

Content Drive needs a System-Host-only clause, which means replacing the
forceSystemHost boolean with a three-state. Seven callers reach the same builder
and none of them sets that flag: the assets API, the older file browser and its
deprecated tree endpoint, the legacy admin browser, a Velocity viewtool, and two
internal callers. If the new default emits anything but "this site alone", every
one of them silently changes what it returns, and nothing in the suite notices.

This passes today, deliberately. A regression guard that failed first would be
guarding something else. It asserts the emitted SQL rather than how the builder
is invoked, so the refactor should not need to touch this file; if it does, that
is the signal to look harder.

Verified green before the refactor: 2 tests, 0 failures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions github-actions Bot added the Area : Backend PR changes Java/Maven backend code label Sep 14, 2026
…ed (#37426)

The three browse scopes are things you can only be in at the top of a site, so a
scope named alongside a folder path is two contradictory statements. This pins
that it must be refused rather than resolved by precedence, following the bulk
upload form: picking one of two contradictory statements puts content somewhere
the caller did not choose.

Deliberately not pinned to an exception type — where the refusal is raised is an
implementation choice, and naming the type would decide it from the test. What
is asserted is that the request does not succeed and that the message names both
halves, which is also what keeps the broad type honest: an incidental NPE has no
message and fails rather than passing as a refusal that never happened.

The enum and the field are here because the test cannot compile without them.
The behavior under test is the refusal, and it is absent: 1 failure, "expected
RuntimeException to be thrown, but nothing was thrown". The other two cases pass
already — every scope at the site root, and a folder path with no scope, which
is the Asset Picker's shape and the reason the field carries no default.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@dotCMS-Machine-User dotCMS-Machine-User left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

✅ dotbot review: all reviewer models (meta/muse-spark-1.3, deepseek/deepseek-v4.1-flash) agree — patch is correct.

approved automatically by dotbot

…olves (#37426)

"New Folder" on System Host opened a dialog previewing the path
`//demo.dotcms.comSYSTEM_HOST/`.

Two faults, one cause. The dialog anchors a new folder on the location the drive
is open on and pastes it after the hostname, but only one of the three locations
is a folder path: all site content is the absence of one, and System Host is a
reserved word, recognisable as not-a-path precisely because it does not begin
with `/`. Concatenated anyway it produced a path resolving to nothing, which the
dialog then showed the author as where their folder would land. It now anchors
only on a location that is a path, which is the same test the rest of the feature
applies.

The deeper answer is that the entry should not be there at all. System Host lists
no folders -- `listsFolders` answers false for that scope, and its sidebar row
opens no tree -- so a folder created there could never be shown again by this
portlet. Writing something the UI can never read back is a trap regardless of
what the server would accept. The "New" menu drops the folder entry there and
keeps the content types, because System Host holds content and adding some is why
the scope accepts new items at all.

All site content is deliberately untouched: its location is absent, so the
builder already yields `//hostname/name` and a new folder lands at the site root,
which is where that entry points for uploads too.

Not verified against a running instance: whether dotCMS itself permits a folder
on SYSTEM_HOST is unanswered, because the QA environment was unreachable. This is
a decision about Content Drive, not about the platform, and the path builder is
right either way.

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

@dotCMS-Machine-User dotCMS-Machine-User left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

✅ dotbot review: all reviewer models (meta/muse-spark-1.3, deepseek/deepseek-v4.1-flash) agree — patch is correct.

approved automatically by dotbot

…ble run copy (#37426)

The bulk-upload e2e failed on every attempt waiting for
`action-execution-indicator`, a testid deleted when the status toast replaced the
toolbar's in-flight indicator. Nothing swept the e2e for it, so the page object
kept waiting for markup that no longer exists. `expectHandedToBackground` now
asserts the status toast carries the backgrounded wording, which is the single
surface saying both halves since the duplicate advisory was removed.

The "Applying X to Y" fallback is deleted, and the two message keys with it.
Nothing could reach it: only an unmarked run arrives at this surface
(`toolbarRun` filters to `targets.length === 0`, because a run whose rows are
marked in the grid already shows the author where it is), and every unmarked run
is an upload, which names itself. Its one real effect was on uploads before they
had their own words, where it produced "Applying Upload to demo.dotcms.com" -- a
sentence for an action performed ON content rather than for files going INTO a
place. A run arriving with no copy now says nothing, and the effect raises no
toast rather than an empty pill.

Nine fixtures in the toolbar spec built runs as `{ actionName: 'Publish', total:
3 }`: no label key, no targets. No such run exists, and they are why the dead
branch read as live. They are uploads now, which is the only thing that reaches
that surface. The escaping test moves to the surviving path, where a target label
is still interpolated into a message bound with `[innerHTML]`.

Five e2e tests for this session's work: the hierarchy stays unselected on System
Host across a reload and the location does not bounce back to the site root; no
New Folder there while content types remain; the folder dialog names a path that
resolves; an upload landing on System Host refreshes the listing with no reload
of the test's own; and a run in flight reports and then stops. The last matters
because the toast is raised sticky, which makes ending it the store's job rather
than the toast library's.

`expectNothingSelected` is scoped to `hierarchy-scroll`, not to the sidebar: the
two standalone entries reuse `p-tree-node-selected` so they match the rows they
sit around, and counting from the panel would read a selected System Host button
as a selected tree row.

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

@dotCMS-Machine-User dotCMS-Machine-User left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

✅ dotbot review: all reviewer models (meta/muse-spark-1.3, deepseek/deepseek-v4.1-flash) agree — patch is correct.

approved automatically by dotbot

…napping (#37426)

The three tabs hold different amounts, so moving between them resized the dialog
in a single frame and read as a bounce.

The panels now sit in a viewport whose height the component writes from their own
measurement. That number has to come from script rather than a stylesheet: CSS
cannot ease to or from the "auto" keyword, so there is nothing for a transition
to interpolate between while the height is content-derived.

Driven by a ResizeObserver on the panels rather than by the tab selection,
because two different things change that height and both deserve the same
treatment: switching tabs, and a tab growing in place when a file-extension chip
is added or a validation message appears. Watching the result covers both and
cannot fall out of step with how many tabs there happen to be. There is no
feedback loop, because the observed element sizes itself to its content while
what is written back is its parent's height, which constrains nothing.

Clipped only while it moves. A panel shrinking into a shorter box would spill
over the footer on the way down, so `overflow` goes on with the height change and
comes off at `transitionend` -- nothing is clipped at rest. Safe here because the
two overlays in the Upload Behavior tab already escape via `appendTo="body"`
(#35578), so the box cannot re-break what that fix addressed.

`prefers-reduced-motion` gets the old instant resize rather than a slower version
of the thing it asked to turn off.

No unit test: this is a transition and a measured height, and asserting on either
pins the implementation rather than the behaviour. It wants eyes in a browser,
and the duration is one value in the component's styles.

One trap worth recording. The inline stylesheet is a template literal, and the
CSS comments first written here quoted `auto` in backticks. Escaping them is
valid JavaScript, but Angular's JIT lifts the CSS into its own virtual module
where the escape does not survive, so the extracted stylesheet was parsed as
script and died on the identifier. Two spec files then failed to load and 294
tests silently did not run while every test that did run passed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
constructor() {
// After the first render, because there is nothing to measure until the form exists: the
// whole tab set sits behind `@if ($formReady())`.
afterNextRender(() => this.#easePanelHeight(), { injector: this.#injector });

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.

🟡 [P2] dot-content-drive-dialog-folder.component.ts:150 height easing never attaches when form loads async

Current code:
typescript
constructor() {
afterNextRender(() => this.#easePanelHeight(), { injector: this.#injector });
}

Problem: #panelsViewport lives inside @if ($formReady()), and $formReady is !!$fileAssetTypes() from an async HTTP call. The one-shot afterNextRender fires after the first render (loading spinner, no viewport), hits the if (!viewport || !panels) return; guard, and never observes — so the easing this commit adds does nothing in the normal flow.

Fix:
typescript
constructor() {
afterRenderEffect(() => {
if (!this.$formReady()) {
return;
}
this.#easePanelHeight();
}, { injector: this.#injector });
}

Or register from an effect that waits on $formReady().

Assumption: afterNextRender is one-shot and runs after the initial render only. What to verify: that the viewport element does not exist on that first render in production (tests mock getContentTypes with of(...), so they exercise the synchronous path and cannot catch this).

@dotCMS-Machine-User dotCMS-Machine-User left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

✅ dotbot review: all reviewer models (meta/muse-spark-1.3, deepseek/deepseek-v4.1-flash) agree — patch is correct.

approved automatically by dotbot

…ipe (#37426)

Two unrelated bits of polish, both verified in the browser rather than reasoned
about in the markup.

The tree's chevron sat out of line with the icons on the rows above and below it.
What has to match is the middle of each mark, not the left of its box: the
chevron is a 10.5px glyph centred in a 24.5px button, while those rows carry a
16px icon, so lining up the boxes still leaves the chevron looking about 4px to
the right of everything else. Measured, the icons centre at 85 and the chevron
centred at 89.25. The tree's left inset now leaves the three in one column.

It goes on the sidebar's own `.p-tree` rule rather than in the shared tree, which
knows nothing about the rows it happens to sit between -- the offset is a fact
about this layout, not about trees.

Lara draws a 6px severity-coloured stripe down the left edge of every toast
(`borderWidth: '0 0 0 6px'`). The preset now sets it to zero, so a toast is a
plain rectangle app-wide. Severity is already carried by the background, the icon
and the text colour; the stripe was a second and louder way of saying the same
thing. Done in the preset because it is a decision about every toast in the
product, not about the one this branch added.

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

@dotCMS-Machine-User dotCMS-Machine-User left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

✅ dotbot review: all reviewer models (meta/muse-spark-1.3, deepseek/deepseek-v4.1-flash) agree — patch is correct.

approved automatically by dotbot

…osable (#37426)

The in-flight line was 39 characters, and 57 once a batch went to the background
-- nearly three times the length of the other thing this surface says ("2
operations running"). It now reads "Uploading 3 files" and "Uploading 3 files in
the background": 18 and 36.

What went is the destination. Developer's call, made knowing the trade: the
destination was added because the site switcher still names a site while System
Host is browsed, so an upload there no longer says where it landed. The drive
does show where the author is, and the sentence had grown long enough that the
part they could read at a glance was getting lost behind the part they could not.

Dropping the "file(s)" hedge means the count has to pick the noun, and only the
caller knows the count, so the shell chooses the key. Four messages rather than
two, which is what spelling a word out costs.

The status is no longer closable. A status is not a message the reader has to
deal with: it reports work already under way and clears when that work settles,
so a close button asks them to tidy up after something they did not start and
cannot affect.

That needed fixing in two places, because they fail differently. PrimeNG reads
`closable` off the MESSAGE (`message?.closable !== false`) and has no input on
the component, so `[closable]="false"` on `p-toast` is a no-op -- written, and
caught in the browser rather than by a test. The toolbar now raises the message
with `closable: false`, which keeps the button out of the DOM entirely, and the
outlet hides `.p-toast-close-button` in its own stylesheet so it stays safe when
a future caller forgets. `display: none` also takes it out of the tab order,
rather than leaving a control that can be reached but not seen.

The tests split along the same line: the toolbar spec pins that the message
carries `closable: false`, and the outlet spec claims only that it renders no
close control of its own. Assertions on the label key are matched by prefix where
the test is about behaviour -- that one file is still announced as backgrounded
-- so they no longer fail over which noun the sentence uses.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

dotbot code review:

  • Reviewer: meta/muse-spark-1.3 (medium)
  • Overall: patch is correct
  • New findings this run: 0
  • Prior unresolved dotbot findings still relevant: 0
  • Active findings total: 0

Incremental delta is intentional UX polish for status toast and upload wording with matching tests and translations; no correctness or security defect found.

Tip: comment with "/dotbot address comments" to attempt automated fixes for unresolved review threads.

reviewed by dotbot · meta/muse-spark-1.3 · medium

@github-actions

Copy link
Copy Markdown
Contributor

dotbot code review:

  • Reviewer: deepseek/deepseek-v4.1-flash (medium)
  • Overall: patch is correct
  • New findings this run: 0
  • Prior unresolved dotbot findings still relevant: 0
  • Active findings total: 0

The delta removes the status toast's close control (via message-level closable: false plus a scoped display: none fallback) and adds singular/plural upload label keys selected by count via uploadIndicatorKey. The new keys exist in Language.properties, the messages still consume {1} as the count argument the caller passes, no stale references to the removed status-toast-close/onClose remain, and the tests were updated consistently. No functional defect is introduced.

Tip: comment with "/dotbot address comments" to attempt automated fixes for unresolved review threads.

reviewed by dotbot · deepseek/deepseek-v4.1-flash · medium

@dotCMS-Machine-User dotCMS-Machine-User left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

✅ dotbot review: all reviewer models (meta/muse-spark-1.3, deepseek/deepseek-v4.1-flash) agree — patch is correct.

approved automatically by dotbot

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 : Documentation PR changes documentation files Area : Frontend PR changes Angular/TypeScript frontend code PR: docker image Build & push a per-PR test image to dotcms/dotcms-test PR : dotbot review Trigger dotbot AI code review on this PR

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

[TASK] Content Drive: browse scopes for All, site root and System Host

3 participants