Skip to content

fix(content-drive): folder dialog overlays clipped and file extensions not persisting (#35578) - #37011

Merged
zJaaal merged 14 commits into
mainfrom
issue-35578-content-drive-folder-dialog-fe-fixes
Aug 12, 2026
Merged

fix(content-drive): folder dialog overlays clipped and file extensions not persisting (#35578)#37011
zJaaal merged 14 commits into
mainfrom
issue-35578-content-drive-folder-dialog-fe-fixes

Conversation

@ihoffmann-dot

@ihoffmann-dot ihoffmann-dot commented Aug 11, 2026

Copy link
Copy Markdown
Member

Fixes two front-end defects reported by QA on the Content Drive folder dialog (Create Folder / Folder Settings → Upload Behavior tab).

Proposed Changes

1. Default File Asset Type dropdown rendered inside the dialog

The p-select (and the p-autoComplete next to it) used PrimeNG's default appendTo="self", so their overlay panels were laid out inside the dialog's scrolling content: the panel was clipped at the dialog edge, and the wheel events over the options list scrolled the dialog instead of the list — which closed the panel.

  • appendTo="body" on both p-select and p-autoComplete, matching what the rest of Content Drive already does for overlays (toolbar menus, field filters, context menus).

2. Allowed File Extensions kept coming back to the previously saved value

Adding an extension wrote the whole array back through the form control (control.setValue([...current, value])). PrimeNG's AutoComplete.writeControlValue() re-derives its displayed model from the currently filtered suggestions, so every already-selected extension outside the active filter was dropped from the chips while staying in the form value. With *.jpg saved and *.png typed, the chips showed *.png but the payload still carried *.jpg — so the old extension reappeared on the next load.

The value is now added through the AutoComplete's own model, which keeps the control and the chips in sync.

3. A saved extension the suggested list does not carry was invisible and unremovable

AutoComplete.writeControlValue() rebuilds the chips from the current suggestions on every write to the control, keeping only the values it can find there — and loading a folder is exactly such a write. With *.jpg,*.svg saved, the field rendered only *.jpg: *.svg stayed in the form value but had no chip, so the user could neither see it nor remove it, and it was sent straight back on save. Same class of desync as defect 2, reached through the load path rather than the add path.

  • The suggestion list now starts empty rather than preloading SUGGESTED_ALLOWED_FILE_EXTENSIONS; onCompleteMethod fills it per keystroke. With nothing to match against, PrimeNG keeps the written value verbatim and the chips always mirror the control.
  • Nothing is lost visually: the panel only opens once a keystroke has produced suggestions (handleSuggestionsChange is gated on loading), and onArrowDownKey returns early unless the panel is already open — so the preloaded list was never reachable before typing.

4. Removing all extensions could not be persisted

The dialog omitted fileMasks when the list was empty, and WebAssetHelper guards the write with UtilMethods.isSet(meta.fileMasks()) — whose Collection overload is false for an empty list too. Absent and empty were both ignored, so the saved extensions came back on the next load and an explicit "clear" was impossible.

  • Editing a folder down to no extensions now sends a single blank mask, which String.join(",", …) turns into an empty string server-side. That is how a folder with no restrictions is already stored: FolderFactoryImpl.matchFilter treats a blank value as "allow everything", the upsert persists it verbatim, and the dialog reads it back as no extensions.
  • On create the field stays omitted, since there is nothing to clear.

This is a workaround for the payload contract, not the contract itself. An empty array is what should mean "clear", which needs the backend guard to distinguish null (absent, leave untouched) from an explicit empty list. fileMasks is @Nullable, so Immutables keeps absent as null and the two are distinguishable whenever that change is made. Raised for team discussion rather than filed.

5. Folder-tree toggle now follows the UVE pattern

Not part of the QA failure — picked up in the same pass because the toggle was drawing a bespoke icon and behaving unlike every other panel toggle in the product.

There were two buttons pretending to be one: the toolbar's copy collapsed to zero width once the tree expanded, and a second copy inside the sidebar header took over the same screen position. Both drew a custom tree-folder.svg sprite that never changed with state.

  • One button, always beside the search input, whether the tree is open or closed. $togglerStyles and the conditional grid gaps are gone, along with the orphaned $treeExpanded.
  • The sidebar's duplicate is removed and a site icon takes its slot, carrying the toggle button's own classes rather than a hardcoded box so the footprint tracks the theme and the site name keeps the indent that lines it up with the folder rows.
  • The glyphs are UVE's own left_panel_open / left_panel_close assets, not Material Symbols ligatures: that pair carries the primary blue, and the two files differ in how much of the panel is filled, which is the state cue a single font glyph cannot express.
  • The icon renders from isTreeVisuallyExpanded, not isTreeExpanded — the Edit Content side panel can force the tree collapsed on a narrow viewport without touching the user's stored preference, and the glyph has to follow what is on screen. Clicking still toggles the preference.
  • The unreferenced tree-folder.svg is deleted.

Checklist

  • Tests
  • Translations — not needed, no copy changes
  • Security Implications Contemplated — none, presentation-only changes

Additional Info

Unit tests in dot-content-drive-dialog-folder.component.spec.ts now drive the real PrimeNG input (typing + keydown) instead of synthetic handler calls, and assert that the rendered chips match the form value.

Enter is now dispatched with code: 'Enter', not just key. PrimeNG's key handler switches on code while the (keydown.enter) binding matches on key, so the earlier events exercised only our handler and skipped the real interleaving, where PrimeNG handles the same press first on the inner input.

The regression test for defect 3 binds folder at creation time because that is how the shell opens this dialog, and the defect depends on that ordering: the effect that loads the folder runs before PrimeNG's initial control write, so a fix applied inside the effect gets clobbered.

Out of scope / follow-up:

  • Committing the typed text on blur (click outside the field) is deliberately not part of this PR. It never worked before either — PrimeNG's addOnBlur only applies when typeahead is off, which is not the case here — so adding it would be new behavior rather than a fix for the reported defect. Extensions are added with Enter or by picking a suggestion, exactly as before; the bug being fixed is what happened after the extension was added.

main is merged in (through 4d49cfe).

Related to #35578

🤖 Generated with Claude Code

This PR fixes: #35578

This PR fixes: #35578

@claude

claude Bot commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Claude finished @zJaaal's task in 34s —— View job


SDK Compatibility Analysis

  • Read SDK breaking-change categories reference
  • Get full PR diff (origin/main range)
  • Analyze diff against each category
  • Post findings or conclude no breaking changes

Result: No SDK breaking change detected.

All 16 changed files are confined to:

  • core-web/libs/portlets/dot-content-drive/** — the Content Drive admin portlet (Angular UI in dotcms-ui): folder dialog template/component/spec, sidebar component/spec, tree-toggler component/spec, toolbar component/spec, and a deleted unused SVG asset (tree-folder.svg).
  • core-web/apps/dotcms-ui-e2e/src/pages/contentDrive.page.ts — e2e page-object test helper.
  • dotCMS/src/main/webapp/WEB-INF/messages/Language.properties — two new admin-UI copy strings.

None of this touches any surface tracked by SDK_BREAKING_CHANGE_CATEGORIES.md:

  • No GraphQL schema/query changes (graphql.page / graphql.content) — G-1/G-2/G-3 N/A.
  • No /api/v1/nav, /api/v1/content, or /api/v1/page/* REST response shape changes — R-1 N/A.
  • No UVE postMessage protocol changes (__DOTCMS_UVE_EVENT__ / DotCMSUVEAction) — U-1/U-2 N/A.
  • No changes to SdkVersionWebInterceptor, X-DotCMS-Version/X-DotCMS-Min-SDK headers, or compareVersions()/sdk-compatibility.ts — H-1 N/A.

This matches the doc's "Non-Breaking Examples" bucket directly: admin-UI (dotcms-ui)-only changes plus test-only changes, not consumed by any @dotcms/* SDK. No comment or label added per instructions.

…ns field

Committing the pending text on blur was never previous behavior and is not needed to fix the reported bug. Extensions are added with Enter or by picking a suggestion, as before.
zJaaal and others added 4 commits August 12, 2026 13:43
…not suggested

The allowed-file-extensions field starts its suggestion list empty instead of
preloading the suggested extensions. PrimeNG's AutoComplete rebuilds its chips
from `suggestions` on every write to the form control, keeping only the values it
can find there, and the folder is loaded through exactly such a write. A saved
extension the suggested list does not carry (`*.svg`) was therefore dropped from
the chips while staying in the form value: the user could not see it or remove
it, and it was sent straight back on save. With nothing to match against,
PrimeNG keeps the written value verbatim and the chips mirror the control.

Nothing is lost visually: the suggestion panel only opens once a keystroke has
produced suggestions (`handleSuggestionsChange` is gated on `loading`), and
ArrowDown returns early unless that panel is already open, so the preloaded list
was never reachable before typing.

Also dispatch Enter with `code` in the spec. PrimeNG's key handler switches on
`code` while our `(keydown.enter)` binding matches on `key`, so the previous
events only exercised our handler and skipped the real interleaving, where
PrimeNG handles the same press first on the inner input.

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

Removing all extensions from an existing folder could not be persisted: the
dialog omitted `fileMasks` when the list was empty, and `WebAssetHelper` guards
the write with `UtilMethods.isSet(meta.fileMasks())`, whose Collection overload
is false for an empty list too. Absent and empty were therefore both ignored and
the previously saved extensions came back on the next load.

Editing a folder down to no extensions now sends a single blank mask, which
`String.join(",", ...)` turns into an empty string server-side. That is how a
folder with no restrictions is already stored: `FolderFactoryImpl.matchFilter`
treats a blank value as "allow everything", the upsert persists it verbatim, and
the dialog reads it back as no extensions.

On create the field stays omitted, since there is nothing to clear.

This is a workaround for the payload contract, not the contract itself: an empty
array is what should mean "clear", which needs the backend guard to distinguish
null (absent, leave untouched) from an explicit empty list. `fileMasks` is
`@Nullable`, so Immutables keeps absent as null and the two are distinguishable
whenever that change is made.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The toggler drew a custom `tree-folder.svg` sprite. It now uses the same
`left_panel_open` glyph UVE uses to toggle the palette and the quick-edit panel,
rendered from the self-hosted Material Symbols font rather than UVE's exported
SVG, which keeps it consistent with the rest of Content Drive's icons.

UVE's asset hardcodes the primary blue in both panel states, so the color is set
on the glyph instead of inherited from the button: the old sprite drew with
`stroke="currentColor"`, which left the icon whatever tone the text button
happened to resolve to.

Template-only change; the open/close mechanism is untouched, and the icon keeps
its `tree-toggle-icon` test id.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… UVE's panel icons

The toggle now behaves the way UVE's palette and quick-edit toggles do: a single
button that stays beside the search input whether the tree is open or closed, and
whose icon reacts to that state.

Until now there were two buttons pretending to be one. The toolbar's copy
collapsed to zero width once the tree expanded, and a second copy inside the
sidebar header took over the same screen position. Both drew a custom
`tree-folder.svg` sprite that never changed with state.

- The toolbar's toggle is always visible, so `$togglerStyles` and the conditional
  grid gaps are gone, along with the now-orphaned `$treeExpanded`.
- The sidebar's duplicate is removed, and a site icon takes its slot. It carries
  the toggle button's own classes rather than a hardcoded box, so the footprint
  stays identical as the theme moves and the site name keeps the indent that
  lines it up with the folder rows underneath.
- The glyphs are UVE's own `left_panel_open`/`left_panel_close` assets rather than
  Material Symbols ligatures: the pair carries the primary blue and the two files
  differ in how much of the panel is filled, which is the state cue a single font
  glyph cannot express. Both render and toggle with `hidden`, so neither is
  fetched mid-interaction.

The icon renders from `isTreeVisuallyExpanded`, not `isTreeExpanded`: the Edit
Content side panel can force the tree collapsed on a narrow viewport without
touching the user's stored preference, and the glyph has to follow what is
actually on screen. Clicking still toggles the preference itself.

The unreferenced `tree-folder.svg` is deleted.

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

Two follow-ups from reviewing the toggler consolidation.

`contentDrive.page.ts#toggleTree` picked the sidebar's toggler whenever the tree
measured wider than 100px. That copy no longer exists, so the expanded branch
resolved to nothing and `content-drive-tree.spec.ts` ("toggles sidebar tree
collapsed and expanded", @critical) would have timed out clicking it. It now
always targets the toolbar's, which is the only one.

`$treeExpanded` on the toolbar was left behind when `$togglerStyles` and the
conditional grid gaps went: nothing in the template or the class referenced it,
and its doc comment had drifted onto `$showWorkflowActions`.

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

The toggle was a custom element wearing button classes, so it could never take
focus: the tree could not be opened or closed from the keyboard at all, and a
screen reader announced an unnamed control. It is now a real `p-button` with an
accessible name that states the action it performs, since both glyphs are
decorative. The name goes through PrimeNG's `[ariaLabel]` input rather than
`[attr.aria-label]`, which would land on the non-focusable `<p-button>` host and
leave the actual `<button>` unnamed.

The button also disables itself while the Edit Content side panel holds the tree
collapsed. `isTreeVisuallyExpanded` ands the stored preference with that override,
so clicking through a forced collapse flipped the preference and moved nothing —
the button looked live and did nothing. Honoring the override means refusing the
interaction until the panel releases it, rather than clearing a collapse the panel
owns.

`p-button-icon-only` is applied by hand: PrimeNG only derives it from the `icon`
input, and the glyphs are projected content here, so without it the button takes
label padding and renders as a padded oval rather than a circle.

Adds `content-drive.tree.collapse` / `content-drive.tree.expand`, and the new
signal to the store mocks of the specs that render this component.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
zJaaal and others added 4 commits August 12, 2026 16:31
…n chips

Loading a folder waits on the content-type request, so the patch that fills the
form can land after the user has typed into Allowed File Extensions. PrimeNG
rebuilds its chips from the current `suggestions` on every write to the control,
keeping only the values it can match there, so any saved extension outside the
active filter lost its chip while staying in the form value.

That was not merely cosmetic. Adding a chip goes through the AutoComplete's own
model, so the next add rebuilt the control from the pruned chips and deleted the
unseen extension for good: opening a folder saved as `*.jpg,*.svg`, typing `*.j`
while it loaded, then adding `*.png` sent `["*.jpg","*.png"]` and dropped `*.svg`
permanently, with nothing on screen to notice.

The loaded value is now restated straight into the AutoComplete's model after the
patch, which skips the matching entirely. This complements the empty starting
suggestion list: that covers the ordinary open, where the write happens before
the component can filter anything, and this covers the write that arrives after.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The dialog rendered an empty form immediately and patched the values in whenever
the file-asset-type request answered. Anything the user typed before that landed
was silently overwritten, on every field, not just the extensions one.

It now shows a spinner until those types arrive and renders the form already
populated, so nothing is ever patched under the user.

That also removes the reason for the previous commit's workaround: the extension
chips were being pruned because PrimeNG rebuilds them from the current
`suggestions` on each write to the control, and that write could land after the
user had typed and narrowed the list. With the form gated on its data, there is no
write to race, so the loaded value no longer has to be restated into the
AutoComplete's model by hand. The regression test now covers the guarantee that
replaces it: a spinner while loading, and a populated, in-sync field once loaded.

Drops the hardcoded `[loading]="false"` on the file-type select, which the gate
makes honest.

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

The loading test only checked the extensions field, but the guarantee is broader:
no input may exist before the values that belong in it, or the load patches over
whatever was typed. It now also asserts the name and title inputs are absent, plus
the save button, since submitting mid-load would persist an empty form over the
folder's real data.

Verified load-bearing: forcing the readiness gate open fails this test and nothing
else.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@zJaaal
zJaaal enabled auto-merge August 12, 2026 19:58
@zJaaal

zJaaal commented Aug 12, 2026

Copy link
Copy Markdown
Member

I just polished some accessibility issues and added a feedback of the icons open/close sidebar pattern.

Ivan made a great work! 🚀

@zJaaal
zJaaal added this pull request to the merge queue Aug 12, 2026
Merged via the queue into main with commit 85b3bfe Aug 12, 2026
53 of 54 checks passed
@zJaaal
zJaaal deleted the issue-35578-content-drive-folder-dialog-fe-fixes branch August 12, 2026 21:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI: Safe To Rollback Area : Frontend PR changes Angular/TypeScript frontend code

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

[TASK] Content Drive: Folder Upload Behavior tab — defaultBaseType preference + consolidate upload settings

2 participants