Skip to content

Issue 36702 asset picker - #36944

Open
nicobytes wants to merge 30 commits into
mainfrom
issue-36702-asset-picker
Open

Issue 36702 asset picker#36944
nicobytes wants to merge 30 commits into
mainfrom
issue-36702-asset-picker

Conversation

@nicobytes

@nicobytes nicobytes commented Aug 7, 2026

Copy link
Copy Markdown
Member

This pull request introduces a set of improvements and refactorings to the folder tree utilities and related data-access APIs, aimed at enhancing code reuse, maintainability, and consistency across the Content Drive and Host Folder Field features. The changes include moving folder tree logic into shared utilities, updating service providers, and aligning data models.

Core refactoring and utility extraction:

  • Introduced new shared utilities folder-tree.utils.ts and folder-tree-load.utils.ts in @dotcms/data-access, centralizing the logic for building, loading, and paginating folder trees. This includes new functions such as generateAllParentPaths, createTreeNode, and buildTreeFolderNodes, as well as utilities for paginated loading and "Load more" node handling. [1] [2]

API and provider updates:

  • Updated DotContentDriveService to use Angular's providedIn: 'root' for global availability, and removed it from route-level providers. This ensures the service can be used from dialogs and other contexts without explicit injection in every route. [1] [2]

Imports and dependency cleanup:

  • Updated imports in various portlet files to use the new shared utilities from @dotcms/data-access instead of local utility definitions, and cleaned up duplicate or outdated imports for components such as DotFolderListViewComponent. [1] [2] [3] [4]

Data model alignment:

  • Extended the TreeNodeContentData model to include additional metadata fields (inode, defaultBaseType, fromTable) for improved folder selection and upload behavior in the Content Drive and AssetPicker.

Constants and type re-exports:

  • Refactored and re-exported shared folder tree page size constants from the new utilities, ensuring consistent pagination limits across features, and deprecated local type definitions in favor of those from @dotcms/data-access. [1] [2] [3] [4]

These changes improve code maintainability, reduce duplication, and ensure that folder tree behaviors remain consistent across all consuming features.

This PR fixes: #36702

AssetPicker series — all seven sub-issues are resolved in this PR:

Fixes #36831 — AssetPicker 1/7: Migrate DotFolderListView to @dotcms/ui
Fixes #36832 — AssetPicker 2/7: Lift folder-tree helpers and harden drive data-access
Fixes #36833 — AssetPicker 3/7: Decouple Content Drive toolbar chrome for AssetPicker reuse
Fixes #36834 — AssetPicker 4/7: Headless browse store (no URL) for AssetPicker
Fixes #36835 — AssetPicker 5/7: Build DotAssetPicker shell in @dotcms/ui
Fixes #36836 — AssetPicker 6/7: File/Image entry configs, silent mime, CT limits, global last path
Fixes #36837 — AssetPicker 7/7: Wire AssetPicker into Edit Content File/Image fields

- Extracted DotFolderListView component and related models/constants/mocks into the new `@dotcms/ui` library.
- Updated imports in the Content Drive shell to reference the new location.
- Introduced `selectionMode` input to support both single and multiple selection.
- Re-exported necessary symbols from `@dotcms/ui` in the Content Drive UI for compatibility.
- Added tests for the new component and selection mode functionality.

This migration aims to share the folder/asset list component between Content Drive and future AssetPicker implementations, enhancing code reuse and maintainability.
Copilot AI lite review requested due to automatic review settings August 7, 2026 14:45
@github-actions github-actions Bot added the Area : Frontend PR changes Angular/TypeScript frontend code label Aug 7, 2026
@claude

claude Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Claude finished @nicobytes's task in 2m 40s —— View job


Code Review — AssetPicker PR

I focused on the AssetPicker store/features, the lifted folder-tree utilities, and the File/Image field wiring, and rechecked the earlier Copilot findings against the current diff.

New Issues

  • 🟡 Medium: core-web/libs/ui/src/lib/components/dot-asset-picker/dot-asset-picker.component.ts:176confirm() subscribes to getContentletByInodeWithContent(asset.inode) with a next-only handler and no error callback. If that hydration request fails (network error, deleted/permission-changed inode between listing and confirm), nothing happens: the dialog stays open, no toast fires, and writeLastAssetLocation/close() never run — the user clicks Confirm and sees no response. Every other subscribe in this component (#uploadByBaseType) handles the error branch; this one should too (show an error toast, keep the dialog open). Fix this →

Existing

  • 🟡 Medium: core-web/libs/data-access/src/lib/dot-folder/folder-tree.utils.ts:96const isLeaf = (levelIndex) => folderHierarchyLevels.length >= levelIndex + 1; is true for every valid levelIndex (0 … length−1), so every node on the target path is marked leaf even when child levels are attached below it, hiding the PrimeNG expand toggle. It should only be true for the final level (levelIndex === folderHierarchyLevels.length - 1). Note: this is carried over verbatim from origin/main's tree-folder.utils.ts — not a regression introduced here — but this PR promotes it into a shared @dotcms/data-access util, so its blast radius now widens to every consumer. Worth fixing while it's being lifted. Fix this →

Resolved

  • dot-asset-picker.store.ts:149setSearch now resets path: undefined and calls store.selectRootNode(), so the tree highlight and $targetFolder follow the site-wide scope instead of staying pinned to the old folder.
  • ui/src/index.ts:11 (AssetPicker not wired) — dot-file-field.component.ts:830 showSelectExistingFileDialog() now opens DotAssetPickerComponent with buildAssetPickerConfig({ mode, site, title, languageId }); the legacy DotBrowserSelectorComponent path for File/Image is gone.
  • dot-asset-picker-toolbar.component.ts:58$allowedBaseTypes now reads config.allowedBaseTypes (never baseTypes), and buildAssetPickerConfig sets allowedBaseTypes to both asset base types in both modes, so a File field restricts the selector to DOTASSET/FILEASSET while starting with no pre-selection.
  • with-asset-browse.feature.ts:120 — a new request now clears selectedAsset: null in the LOADING tap, so Confirm can't return a stale asset that scrolled out of the list.
  • with-asset-folder-tree.feature.ts:83/97 — success work is in tap before catchError, and the error path returns EMPTY and sets ERROR; a failed folder load is no longer masked as a successfully-loaded empty tree.
  • dot-asset-picker.component.html:22 (double-click) — routed through onSelect([$event]) (select-only) intentionally, documented as "confirming stays an explicit action" because DotFolderListView.doubleClick also fires on single title/thumbnail clicks. Reasonable by design.

Nice work overall — the store composition ordering, cursor-invalidation on sort/page-size change, and the error-before-success ordering in the folder-tree feature are all handled carefully. The one blocking-ish item is the missing error branch on confirm(); the isLeaf predicate is worth fixing while the util is being moved but is pre-existing.

· branch issue-36702-asset-picker

Copilot AI left a comment

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.

Pull request overview

This PR refactors the existing Folder List View used by Content Drive into a reusable presentational component in @dotcms/ui, adding a single-selection mode intended for the upcoming AssetPicker (while preserving current Content Drive behavior via the default multiple selection mode).

Changes:

  • Moved Folder List View domain-agnostic types/constants into @dotcms/ui and re-exported them from the Content Drive UI package for compatibility.
  • Added selectionMode: 'single' | 'multiple' support to the table (checkboxes in multiple mode, radios in single mode) and normalized emitted selections to an array.
  • Updated Content Drive shell imports and updated/extended unit tests accordingly.

Reviewed changes

Copilot reviewed 12 out of 13 changed files in this pull request and generated no comments.

Show a summary per file
File Description
core-web/libs/ui/src/lib/components/dot-folder-list-view/models.ts Introduces shared column typing and selectionMode model for the Folder List View.
core-web/libs/ui/src/lib/components/dot-folder-list-view/constants.ts Defines header column config and drag MIME type local to the component folder.
core-web/libs/ui/src/lib/components/dot-folder-list-view/mocks.ts Moves test mocks alongside the component.
core-web/libs/ui/src/lib/components/dot-folder-list-view/dot-folder-list-view.component.ts Adds selectionMode input, normalizes selection output, and updates internal imports to local UI sources.
core-web/libs/ui/src/lib/components/dot-folder-list-view/dot-folder-list-view.component.html Switches checkbox vs radio rendering based on selectionMode and updates selection binding.
core-web/libs/ui/src/lib/components/dot-folder-list-view/dot-folder-list-view.component.scss Fixes relative SCSS imports to match the libs layout.
core-web/libs/ui/src/lib/components/dot-folder-list-view/dot-folder-list-view.component.spec.ts Updates tests for the new selection model and adds coverage for single-selection behavior.
core-web/libs/ui/src/index.ts Exposes Folder List View component + related models/constants from @dotcms/ui.
core-web/libs/portlets/dot-content-drive/ui/src/lib/shared/models.ts Removes Folder List View column typing now owned by @dotcms/ui.
core-web/libs/portlets/dot-content-drive/ui/src/lib/shared/constants.ts Removes list-view constants now owned by @dotcms/ui.
core-web/libs/portlets/dot-content-drive/ui/src/index.ts Re-exports the Folder List View API surface from @dotcms/ui for Content Drive consumers.
core-web/libs/portlets/dot-content-drive/portlet/src/lib/dot-content-drive-shell/dot-content-drive-shell.component.ts Updates imports to use @dotcms/ui for the presentational list component/types.
core-web/libs/portlets/dot-content-drive/portlet/src/lib/dot-content-drive-shell/dot-content-drive-shell.component.spec.ts Aligns test imports with the updated component export location.

- Introduced new utility functions for managing folder hierarchies, including `getFolderHierarchyByPath` and `getFolderNodesByPath`, to improve folder navigation and loading in the content drive.
- Added `folder-tree-load.utils.ts` and `folder-tree.utils.ts` files to encapsulate the new logic.
- Implemented comprehensive unit tests for the new utilities to ensure functionality and reliability.
- Updated existing services to utilize the new utilities, enhancing code organization and maintainability.

These changes aim to streamline folder management and improve the user experience in the content drive interface.
Extracts the content-type, language, and search filter components (plus the
chip-filter/list-item primitives and upload button) out of the content-drive
portlet into @dotcms/ui so they can be shared with the AssetPicker. Store-
specific logic stays behind thin adapter components in the portlet.
- serve target lacked a dependsOn, so dotcms-webcomponents could be
  stale or missing when dotcms-ui starts serving
- webcomponents build target was missing outputs, preventing Nx from
  caching/detecting its build artifacts correctly
- Introduces DotAssetPickerStore in @dotcms/ui to power the upcoming
  AssetPicker dialog with a search request builder mirroring Content
  Drive's, but with no router/URL coupling so it can run inside a
  dialog over Edit Contentlet without corrupting host navigation.
- Relocates ALL_FOLDER/SYSTEM_HOST_ID out of the Content Drive UI
  library into shared dot-folder-tree constants so both Content
  Drive and the new picker consume a single source.
@nicobytes
nicobytes requested a lite review from Copilot August 7, 2026 17:55

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 76 out of 78 changed files in this pull request and generated no new comments.

Suppressed comments (1)

core-web/libs/ui/src/lib/components/dot-asset-picker/store/features/with-asset-folder-tree.feature.ts:97

  • loadFolders sets foldersStatus to ERROR in catchError, but then the subscribe block unconditionally patches it back to LOADED (because catchError returns an empty array). This masks folder-tree failures and makes the UI indistinguishable from a successful empty tree.

The AssetPicker (browse/pick a single asset) now composes the dropzone, upload-type selector, and folder sidebar/toolbar that Content Drive already had, so both features share one implementation instead of duplicating upload flow logic.

- Move `dot-content-drive-dropzone` and the upload-type-selector dialog out of the Content Drive portlet into `@dotcms/ui` as `DotUploadDropzoneComponent` and `DotUploadTypeSelectorComponent`, decoupled from `DotContentDriveStore` (folder/drag-state now passed via inputs/outputs)
- Add `DotAssetPickerComponent` with sidebar/toolbar subcomponents, wiring the shared dropzone, upload selector, and folder tree to a new `DotAssetPickerStore`
- Update Content Drive shell to consume the relocated shared components and derive drag/target-folder state locally
Adds a global last-used-path store, a config builder that translates
Edit Content field type (File/Image) into picker filters, and
server-side base-type narrowing for the content type filter so
restricted hosts don't page through mostly-discarded results.

Copilot AI left a comment

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.

Pull request overview

Copilot reviewed 97 out of 101 changed files in this pull request and generated 2 comments.

Suppressed comments (5)

core-web/libs/ui/src/lib/components/dot-asset-picker/components/dot-asset-picker-toolbar/dot-asset-picker-toolbar.component.ts:58

  • This returns null for File mode because that config intentionally has no preselected base types, but null makes the selector offer every base type, including Content and Widget. The acceptance criteria restrict the selector in both File and Image modes; selection and allowed options must be separate so File starts with no selection while still only offering DOTASSET/FILEASSET.
    core-web/libs/ui/src/lib/components/dot-asset-picker/dot-asset-picker.component.html:22
  • Double-click only selects the row, whereas the AssetPicker acceptance criteria require it to select and confirm. Note that DotFolderListViewComponent.doubleClick is also emitted by single clicks on the title/thumbnail, so directly calling confirm() here would close on a single click; introduce a distinct actual-double-click confirmation path without restoring editor navigation.
    core-web/libs/data-access/src/lib/dot-folder/folder-tree.utils.ts:96
  • This predicate is true for every valid level (length >= index + 1), so every node on the target path is marked as a leaf even when child levels are attached below it. PrimeNG then hides the expansion control, making expanded ancestors impossible to reopen after collapse. Only the final hierarchy level should be marked as a leaf.
    const isLeaf = (levelIndex: number) => folderHierarchyLevels.length >= levelIndex + 1;

core-web/libs/ui/src/lib/components/dot-asset-picker/store/features/with-asset-browse.feature.ts:120

  • Starting a new request leaves selectedAsset untouched. Since the list clears its own PrimeNG selection silently when items changes, changing folder/filter/page/sort can leave Confirm enabled for an asset no longer visible and return that stale asset. Clear the picker selection whenever a new browse request starts (or when its result replaces items).
    core-web/libs/ui/src/lib/components/dot-asset-picker/store/features/with-asset-folder-tree.feature.ts:83
  • The error status is immediately overwritten: catchError emits an empty hierarchy, so the subscription runs and patches foldersStatus to LOADED. A failed folder request therefore looks like a successfully loaded empty tree. Stop the success callback from running for the error fallback.

Comment thread core-web/libs/ui/src/index.ts
- The dotAsset/File Asset picker now uses the Content Drive-backed
  AssetPicker instead of DotBrowserSelectorComponent, restricting by
  base type/mime for image fields and resolving site/locale via
  GlobalStore and DotEditContentStore.
- Guards against opening the picker before a site has resolved.
- Updates specs to mock GlobalStore's siteDetails and cover the new
  picker config, locale fallback, and open/close guards.
- Add `allowedBaseTypes` to the picker config so both File and Image
  modes restrict the content-type selector to DOTASSET/FILEASSET,
  independent of `baseTypes` (which only controls pre-selection). The
  File field previously fell back to "no restriction" and listed
  Widget/Content.
- Reorder `withAssetSelection` before `withAssetBrowse` so a new
  browse result can clear a stale selection instead of leaving
  Confirm enabled for a row no longer in the list.
- Fix folder-tree hierarchy loading to only patch LOADED state on
  success (in `tap`, before `catchError`), so a failed request stays
  in ERROR instead of looking like an empty tree.
- Reset the tree highlight to the site root when a search widens the
  list back to site-wide, since `$targetFolder` reads the highlight
  to pick the upload destination.
Adds a `{ state: type<DotAssetPickerState>() }` constraint to the signal store feature so it correctly composes with `withAssetBrowse`, which now runs after it and depends on `selectedAsset`. Also fixes a Prettier import formatting issue in the file field spec.
- Replace the single-site folder tree with a sites-and-folders tree so
  users can pick assets from any site, not just the one being edited;
  adds sidebar search, cross-site "last used" location memory, and a
  custom header that drives PrimeNG's maximize state for full screen.
- Extract the full-screen dialog styling shared with the image editor
  into `@dotcms/ui` so both consumers stay in sync.
- Introduce DotAssetPickerHeaderComponent with title, full-screen toggle,
  and close button wired to the picker store
- Add DotAssetPickerSidebarComponent tests covering multi-site tree
  rendering, node selection/expansion, and load-more pagination
- Extract site-tree.utils.ts with shared helpers for multi-site folder
  trees (pagination, load-more sentinels, site/folder lookups) used by
  both the AssetPicker and the legacy Browser Selector
- Add fullscreen-dialog.ts constants for driving dialog full-screen state
  via PrimeNG's maximized class instead of inline style overrides
Widen height and narrow width for better content fit before reaching full screen.
Lazy-load tap/catchError handlers mutated the tree node captured before
the request fired, but publish() replaces the whole folders array with
a clone on every update, orphaning that reference — so the loading
spinner and expanded state landed on a node the tree no longer rendered.
mutateNode()/findNodeByKey() re-locate the target by key against the
current state before each mutation, and publish() re-derives
selectedNode by key so the highlight survives across a load. Also adds
a showActions input to DotFolderListViewComponent to hide the kebab
menu in the picker's single-selection context.
Switch the width/height caps from fixed pixels (1120px/58rem) to
rem-based caps (114rem/68rem) so the dialog scales with content
sizing and only clamps on large external monitors, while the
viewport-relative min() still governs on typical laptop screens.
Previously, clearing the base-type chip (or starting with none, as File
fields do) dropped the restriction entirely and let the picker list
Pages and other non-asset content types. Now allowedBaseTypes acts as
a floor that always constrains the request, while a user-selected
baseType filter can narrow further.
- Add createDotAsset helper to seed dotAsset contentlets via a single
  multipart workflow call, so picker tests can seed real assets without
  going through the /api/v1/temp fingerprinting endpoint
- Introduce AssetPickerDialog page object and wire it into file/image
  field specs to cover selecting, confirming, and cancelling an
  existing asset, plus mimetype filtering for image fields
- Add data-testid="asset-picker" to the picker root so the new tests
  have a stable selector
const root = site ? findSiteRoot(roots, site.identifier) : undefined;

if (!site || !root) {
return of({ folders: roots });

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The site being browsed can disappear from the sidebar entirely. sitesPage() filters the sites list with the same tree search term (filter: activeSearchTerm() || '*'). If the term matches a folder name but not the site's own name/hostname, the site gets filtered out of roots, findSiteRoot returns undefined, and we fall into of({ folders: roots }) — no search runs and the current site vanishes from the tree.

Repro: while browsing "democms.com", type a folder name like "logos" that isn't part of the hostname → the site disappears and no results are shown at all, even though matching folders exist.

folders,
selectedNode,
foldersStatus: ComponentStatus.LOADED
})

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

TreeLoadResult documents that an absent selectedNode means "leave the highlight alone" (distinct from null). But destructuring ({ folders, selectedNode }) and passing it to patchState(store, { folders, selectedNode, ... }) turns that absent key into a present key with value undefined — and patchState merges via spread ({...current, ...partial}), so it overwrites the signal to undefined anyway. The highlight gets wiped in the exact same case as the comment above, contradicting the contract documented on TreeLoadResult.

Suggestion: only include selectedNode in the patch when it's actually present in the result (same conditional-spread pattern already used in publish() further down this file).

// Persist on confirm, not on selection: a row the user highlights and then cancels
// must not move a value shared by every picker in the system.
writeLastAssetLocation(this.#resolveAssetLocation(hydrated));
this.#dialogRef.close(hydrated);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This subscribe has no error handler, unlike #uploadByBaseType elsewhere in this file, which shows a toast on failure. If getContentletByInodeWithContent errors (network blip, 404, permissions changed since selection), the error is silently swallowed, the dialog never closes, and the user gets no feedback — Confirm just looks like it did nothing.

Suggestion: add an error: handler (toast + keep dialog open), matching the pattern in #uploadByBaseType.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area : Frontend PR changes Angular/TypeScript frontend code

Projects

Status: No status

3 participants