Conversation
… 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 finished @zJaaal's task in 4m 55s —— View job Content Drive browse scopes — code reviewReview checklist
I reviewed the full diff for bugs introduced by this PR. The change is coherent end to end: the location→(assetPath, browseScope) mapping in New IssuesNone found (no 🔴 / 🟠 / 🟡). Notable correctness points I specifically verified and consider sound:
ExistingNone flagged (only reviewing PR-introduced changes). Resolved
|
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>
|
The spec looks solid overall — two acceptance-criteria gaps I'd flag before signing off: 1. Query builder parity has no dedicated testThe driving issue notes Given this is a real, already-identified bug, I'd want an explicit acceptance scenario that exercises both query heuristics ( 2. No-scope backward compatibility isn't broken down by consumerSC-007 says "surfaces other than Content Drive keep listing exactly what they list today," but there are 6 named consumers of the shared listing:
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>
|
Both added in 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 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. |
|
All three addressed in 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, Reply written by Claude on @zJaaal's behalf. |
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>
…s' into 37426-content-drive-browse-scopes
|
|
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>
…s' into 37426-content-drive-browse-scopes
…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>
…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
left a comment
There was a problem hiding this comment.
✅ 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
left a comment
There was a problem hiding this comment.
✅ 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
left a comment
There was a problem hiding this comment.
✅ 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 }); |
There was a problem hiding this comment.
🟡 [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
left a comment
There was a problem hiding this comment.
✅ 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
left a comment
There was a problem hiding this comment.
✅ 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>
|
dotbot code review:
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 |
|
dotbot code review:
The delta removes the status toast's close control (via message-level 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
left a comment
There was a problem hiding this comment.
✅ dotbot review: all reviewer models (meta/muse-spark-1.3, deepseek/deepseek-v4.1-flash) agree — patch is correct.
approved automatically by dotbot
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.
/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.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 fallbackthis 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
candidatesCTE that still read theforceSystemHostboolean thisbranch replaced with a three-state;
signalStoreaccepts at most sixteen features and the mergedstore 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-toastis a new outlet inlibs/uirather than a variant ofdot-toast: that one is a report, several lines wide because it has to be, while this states anoutcome in a couple of words, so it is sized to its text and drops
detailentirely. It claims akey, because a portlet provides one
MessageServiceand a keyless outlet renders every message onit. 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 emptyresult 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 userstood 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 neithernamed where the files actually went.
browsedFolderRefnow answers "which folder is on screen" forall 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.
AbstractDriveRequestFormcarriedno
@JsonIgnoreProperties(ignoreUnknown = true), andDotObjectMapperProvider.createDefaultMapperdisables only
WRAP_EXCEPTIONS, soFAIL_ON_UNKNOWN_PROPERTIESsat at Jackson's enabled default. Abinary rolled back to a build predating
browseScopewould therefore refuse every request carryingit — and since
toRequestLocationsends a scope for all site content, the site root and SystemHost, 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,PageScanCheckFormandFileUploadDetailmake the same trade, and thefirst 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 anew 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 nowanchors only on a location that is a path.
The entry should not be there at all, though: System Host lists no folders (
listsFoldersanswersfalse 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_HOSTisunanswered, because the QA environment was unreachable. This is a decision about Content Drive, not
about the platform.
Checklist
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