Skip to content

37207 migrate openbrowsermodal to the new assetpicker and delete the legacy browser selector - #37273

Open
nicobytes wants to merge 11 commits into
mainfrom
nicobytes/37207-migrate-openbrowsermodal-to-the-new-assetpicker-and-delete-the-legacy-browser-selector
Open

37207 migrate openbrowsermodal to the new assetpicker and delete the legacy browser selector#37273
nicobytes wants to merge 11 commits into
mainfrom
nicobytes/37207-migrate-openbrowsermodal-to-the-new-assetpicker-and-delete-the-legacy-browser-selector

Conversation

@nicobytes

@nicobytes nicobytes commented Aug 28, 2026

Copy link
Copy Markdown
Member

This pull request introduces support for browsing and selecting menu links (in addition to files, folders, and pages) in the asset picker modal used by custom field templates. It refactors the Angular form bridge to use the new asset picker, adds a mechanism for resolving the current site, and updates the relevant models and APIs to handle the new "link" type. The Dojo form bridge remains a stub for the browser modal, but now emits a warning for unsupported usage. The most important changes are grouped below.

Asset Picker and Browsing Enhancements

  • Added a new DotContentDriveLink interface and updated DotContentDriveItem to include links, allowing the asset picker to display and select menu links alongside other item types. (core-web/libs/dotcms-models/src/lib/dot-content-drive.model.ts)
  • Extended the search request and response models to support paginated retrieval of links (showLinks, linkCursor, hasMoreLinks, nextLinkCursor). (core-web/libs/dotcms-models/src/lib/dot-content-drive.model.ts) [1] [2]

Angular Form Bridge Refactor

  • Refactored AngularFormBridge.openBrowserModal() to use the new DotAssetPickerComponent, supporting selection of pages, folders, and links, and returning a consistent handle with a promise-based API. (core-web/libs/edit-content-bridge/src/lib/bridges/angular-form-bridge.ts)
  • Added a resolveSite mechanism to allow the bridge to obtain the current site context asynchronously, ensuring the picker always browses the correct site. (core-web/libs/edit-content-bridge/src/lib/bridges/angular-form-bridge.ts) [1] [2] [3] [4]

Legacy Editor (Dojo) Handling

  • Updated the Dojo form bridge to emit a warning and return a well-formed, always-null handle when openBrowserModal is called, making unsupported usage more transparent. (core-web/libs/edit-content-bridge/src/lib/bridges/dojo-form-bridge.ts, core-web/libs/edit-content-bridge/src/lib/bridges/dojo-form-bridge.spec.ts) [1] [2]

Feature Directory Update

  • Changed the feature directory in .specify/feature.json to reflect the new asset picker modal for menu links. (.specify/feature.json)

This PR fixes: #37207

I will check the main configuration directory to see if there are any custom settings, READMEs, or specific project files that provide more context for the sandbox environment.
I will ask you what type of sandbox environment you would like to set up in the scratch directory.
I will create a sandbox project directory and add a basic Python environment structure with a `README.md` and a starting script (`main.py`) to serve as a playground.
I will write the `README.md` file without the `ArtifactMetadata` parameter to create it directly in the project sandbox directory.
I will create the main entry point script `main.py` in the sandbox directory.
I will run the `main.py` script using the terminal to verify the environment works correctly.
- Replace the legacy BrowserSelector interface with DotBrowserOptions/
  DotBrowserHandle, backed by a site-aware asset picker instead of the
  old browser selector dialog.
- Add a resolveSite hook to createFormBridge so the Angular host can
  supply the current site, since the bridge has no injector of its own.
- Wire resolveSite from DotSiteService in the iframe and native custom
  field components.
- Stub openBrowserModal in the Dojo bridge to warn and resolve null,
  since the legacy editor never had this dialog.
- The bridge's openBrowserModal previously piped `extensions` through to the
  browse config even though the browse endpoint has no such parameter, so the
  option was silently accepted and ignored; that test now documents the gap
  instead of asserting dead behavior.
- Introduces `mimeTypes` on `DotAssetPickerEntryOptions`, restricted to
  `browse` mode (media modes still derive theirs from
  `ASSET_PICKER_MIME_TYPES` and must stay non-overridable).
- Adds coverage for the browse endpoint dropping `links` whenever a mimetype
  filter is set, surfacing a console warning rather than silently returning
  fewer kinds than requested.
…the-new-assetpicker-and-delete-the-legacy-browser-selector
@github-actions github-actions Bot added the Area : Frontend PR changes Angular/TypeScript frontend code label Aug 28, 2026
@claude

claude Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Claude finished @nicobytes's task in 1m 7s —— View job


SDK Compatibility Analysis — Complete

  • Read docs/core/SDK_BREAKING_CHANGE_CATEGORIES.md
  • Get full PR diff (d5ab3fd3...9410c5d0)
  • Analyze diff against every category (G-1/2/3, R-1, U-1/2, H-1)
  • Finish

Result: No SDK breaking change detected.

All 31 changed files are confined to:

  • core-web/libs/edit-content-bridge/*AngularFormBridge/DojoFormBridge openBrowserModal() rewrite (admin editor's custom-field host API, not an @dotcms/* published SDK surface)
  • core-web/libs/ui/src/lib/components/dot-asset-picker/* and dot-folder-list-view — admin-UI Asset Picker component/store
  • core-web/libs/dotcms-models/src/lib/dot-content-drive.model.ts — adds DotContentDriveLink and link-paging fields (showLinks, linkCursor, hasMoreLinks, nextLinkCursor); confirmed via grep that nothing under core-web/libs/sdk/** imports dotcms-models/content-drive types — this model backs the admin Content Drive/Asset Picker feature only
  • .vtl custom-field templates and one Language.properties key
  • specs/** documentation

There are no Java/backend changes at all in this diff (git diff --stat shows zero files under dotCMS/src/main/java), so:

  • G-1/G-2/G-3 (GraphQL page/content schema, args, error codes) — not applicable, no GraphQL resolver or schema changes
  • R-1 (/api/v1/nav, /api/v1/content, /api/v1/page/* response shape) — not applicable, no REST controller changes
  • U-1/U-2 (UVE postMessage protocol — __DOTCMS_UVE_EVENT__ / DotCMSUVEAction) — not applicable, no editor/UVE files touched
  • H-1 (SdkVersionWebInterceptor, X-DotCMS-Version/X-DotCMS-Min-SDK, compareVersions()) — not applicable, sdk-compatibility.ts and the interceptor are untouched

This PR migrates the legacy DotBrowserSelectorComponent to the new Asset Picker for VTL custom-field templates (file_browser_field_render_new.vtl, redirect_custom_field_new.vtl) entirely within the admin UI. No action taken (no comment posted, no label applied), per instructions when no breaking change is found.

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 migrates DotCustomFieldApi.openBrowserModal() (Angular host) from the legacy browser selector to the new Asset Picker, expanding the picker’s domain to include menu links (and related browse-mode configuration) while making the legacy Dojo bridge explicitly unsupported via a warning + always-null handle.

Changes:

  • Introduces a redesigned openBrowserModal public contract (DotBrowserOptions / DotBrowserSelection / DotBrowserHandle) and wires the Angular form bridge to open DotAssetPickerComponent.
  • Extends Asset Picker configuration to add a browse mode and opt-in browse flags (folders/links/version state/sort direction) while keeping existing entry points gated.
  • Extends Content Drive frontend models to represent menu links (DotContentDriveLink) and link paging fields.

Reviewed changes

Copilot reviewed 25 out of 25 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
specs/37207-openbrowsermodal-assetpicker/spec.md Adds the feature specification and acceptance criteria for the migration and browse-mode behavior.
specs/37207-openbrowsermodal-assetpicker/data-model.md Documents the data model changes across API, picker config/state, and drive models.
specs/37207-openbrowsermodal-assetpicker/contracts/README.md Summarizes the three contract boundaries and their compatibility rules.
specs/37207-openbrowsermodal-assetpicker/contracts/openbrowsermodal-public-api.md Defines the redesigned openBrowserModal contract for VTL templates.
specs/37207-openbrowsermodal-assetpicker/contracts/drive-search-delta.md Specifies the (planned) additive extensions request field for drive search.
specs/37207-openbrowsermodal-assetpicker/contracts/asset-picker-config.md Specifies the additive picker config changes for browse mode + nested browse options.
dotCMS/src/main/webapp/WEB-INF/messages/Language.properties Adds the browse-mode Asset Picker header translation key.
core-web/libs/ui/src/lib/components/dot-asset-picker/store/models.ts Adds DotAssetPickerBrowseOptions, extends paging/selection state types for mixed item kinds.
core-web/libs/ui/src/lib/components/dot-asset-picker/store/features/with-asset-browse.feature.ts Starts wiring browse-mode version-state flags into the drive search request.
core-web/libs/ui/src/lib/components/dot-asset-picker/store/dot-asset-picker.store.ts Seeds initial sort order based on browse options.
core-web/libs/ui/src/lib/components/dot-asset-picker/store/constants.ts Extends default page bookmark to include folder/link cursors and hasMore* flags.
core-web/libs/ui/src/lib/components/dot-asset-picker/asset-picker-config.ts Adds browse mode, browse-only options (allowedBaseTypes, browse, mimeTypes) and gating.
core-web/libs/ui/src/lib/components/dot-asset-picker/asset-picker-config.spec.ts Adds unit coverage for browse mode + opt-in guarantees.
core-web/libs/edit-content/src/lib/fields/dot-edit-content-custom-field/components/native-field/native-field.component.ts Injects DotSiteService and passes resolveSite() into createFormBridge.
core-web/libs/edit-content/src/lib/fields/dot-edit-content-custom-field/components/iframe-field/iframe-field.component.ts Injects DotSiteService and passes resolveSite() into createFormBridge.
core-web/libs/edit-content-bridge/src/lib/interfaces/form-bridge.interface.ts Updates the bridge API to the redesigned openBrowserModal types and re-exports.
core-web/libs/edit-content-bridge/src/lib/interfaces/browser-selector.interface.ts Removes the legacy browser-selector interface contract types.
core-web/libs/edit-content-bridge/src/lib/interfaces/asset-browser.interface.ts Adds the redesigned openBrowserModal types (options/selection/handle).
core-web/libs/edit-content-bridge/src/lib/factories/form-bridge.factory.ts Adds resolveSite plumbing into AngularFormBridge creation.
core-web/libs/edit-content-bridge/src/lib/bridges/dojo-form-bridge.ts Makes openBrowserModal explicitly unsupported in Dojo (warn + always-null handle).
core-web/libs/edit-content-bridge/src/lib/bridges/dojo-form-bridge.spec.ts Adds tests for the Dojo openBrowserModal stub behavior.
core-web/libs/edit-content-bridge/src/lib/bridges/angular-form-bridge.ts Refactors openBrowserModal to open the new Asset Picker and return a promise-based handle.
core-web/libs/edit-content-bridge/src/lib/bridges/angular-form-bridge.spec.ts Updates tests to cover opening, option mapping, result mapping, and cancellation behaviors.
core-web/libs/dotcms-models/src/lib/dot-content-drive.model.ts Adds DotContentDriveLink and link paging fields to drive request/response models.
.specify/feature.json Points Spec-Kit to the new feature directory.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines 142 to 145
...(pickerConfig?.browse?.showWorking === false ? { live: true } : {}),
// Invariant, not a default: the picker's list is for assets. Folders are
// navigated through the sidebar tree.
showFolders: false
Comment on lines +42 to +46
/** Include working (unpublished) versions. Omit for live-only. */
showWorking?: boolean;

/** Include archived content. Omit to exclude it, which is the picker's standing behaviour. */
showArchived?: boolean;
Comment on lines +115 to +116
* Browse capabilities beyond plain asset picking — folders, menu links, version state, sorting
* and extension narrowing.
…te-openbrowsermodal-to-the-new-assetpicker-and-delete-the-legacy-browser-selector
- The bridge's openBrowserModal previously piped `extensions` through to the
  browse config even though the browse endpoint has no such parameter, so the
  option was silently accepted and ignored; that test now documents the gap
  instead of asserting dead behavior.
- Introduces `mimeTypes` on `DotAssetPickerEntryOptions`, restricted to
  `browse` mode (media modes still derive theirs from
  `ASSET_PICKER_MIME_TYPES` and must stay non-overridable).
- Adds coverage for the browse endpoint dropping `links` whenever a mimetype
  filter is set, surfacing a console warning rather than silently returning
  fewer kinds than requested.
…etpicker-and-delete-the-legacy-browser-selector' of github.com:dotCMS/core into nicobytes/37207-migrate-openbrowsermodal-to-the-new-assetpicker-and-delete-the-legacy-browser-selector
…grate-openbrowsermodal-to-the-new-assetpicker-and-delete-the-legacy-browser-selector
@github-actions github-actions Bot added Area : Backend PR changes Java/Maven backend code and removed AI: Not Safe To Rollback labels Aug 28, 2026
… the code

Addresses the review feedback on #37273 that is still live. All three describe
behaviour the browse work replaced:

- `showWorking` claimed "Omit for live-only", but omitting it applies the
  endpoint's default, which *includes* working versions. `false` is the only
  value that narrows to live-only. The doc said the opposite of what the
  request builder does.
- `DotAssetPickerConfig.browse` still advertised "extension narrowing".
  `extensions` was removed when the endpoint work was split into its own
  issue, so the option no longer exists.
- The `withAssetBrowse` header still called `showFolders: false` an
  "invariant". It stopped being one when folders became opt-in — and the same
  file already says so 100 lines further down, so it contradicted itself.

Comments only; no behaviour change. Verified with `nx build dotcms-ui`, lint
across 5 projects, and the picker plus Content Drive suites (1377 tests).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* (workflow filters, user-searchable fields) and plus the two things it needs: a silent mimetype
* restriction and `showFolders: false` as an invariant.
* restriction, and folders and menu links switched off unless the caller opts in through
* `DotAssetPickerBrowseOptions`.

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 comment is about $totalRecords at lines 74-83 (unchanged by this PR, so anchoring here at the nearest changed line).

That computed still inspects only hasMoreContent:

return bookmark?.hasMoreContent
    ? limit * (page + 1)
    : limit * (page - 1) + items().length;

Now that folders and links are independently paginated streams, is this still sufficient? Walking a browse-mode case with limit=20 — if page 1 comes back hasMoreContent: false but hasMoreFolders: true, hasMoreLinks: true with 15 rows rendered, this returns 20*0 + 15 = 15. The paginator then sees first + rows >= totalRecords and disables Next, so the remaining folders and links look unreachable even though the server still has them.

The converse (folders/links exhaust first, content continues) looks fine, since hasMoreContent keeps it growing.

Would something like this be right?

const hasMore = bookmark
    ? bookmark.hasMoreContent || bookmark.hasMoreFolders || bookmark.hasMoreLinks
    : true;

return hasMore ? limit * (page + 1) : limit * (page - 1) + items().length;

The docstring just above ("answers is there anything past what is on screen") would want updating too, since it now speaks for three streams rather than one.

// Three states, expressed as the two flags the picker already understands.
...(options.status ? { showWorking: options.status !== 'live' } : {}),
...(options.status ? { showArchived: options.status === 'archived' } : {}),
...(options.sort ? { sortByDesc: options.sort.direction === 'desc' } : {})

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.

Only sort.direction is read here — is sort.field intentionally dropped?

DotAssetPickerBrowseOptions has no sort-field property, so there is currently no way to pass it through even if a caller sets it. That makes the drop structural rather than an oversight at this line.

The public JSDoc on openBrowserModal advertises the full shape:

sort: { field: 'modDate', direction: 'desc' },

so a template author passing sort: { field: 'title', direction: 'desc' } would reasonably expect title ordering and instead get modDate from DEFAULT_ASSET_PICKER_SORT. Both shipped VTL templates happen to pass modDate, which is already the default, so this stays invisible today.

extensions right above carries an explicit "not yet, ships with the endpoint support" note. Would it be worth giving sort.field the same treatment — either wiring it through to the store's sort state, or documenting it as direction-only for now?

// Only a `showLinks` response carries these. Defaulting the
// cursor to the one we sent keeps it from rewinding to 0 on a
// page that did not query links.
linkCursor: response.nextLinkCursor ?? 0,

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 comment and the code seem to disagree here. The comment says the fallback keeps the cursor "from rewinding to 0" by defaulting to "the one we sent", but the code is ?? 0 — which is the rewind.

Preserving the last-sent cursor would be ?? bookmark?.linkCursor ?? 0.

Does this matter in practice? Both hasMoreLinks and nextLinkCursor are optional on the response type, so nothing enforces that they arrive together. If a response ever reports hasMoreLinks: true without a nextLinkCursor, this stores linkCursor: 0 while hasMoreLinks stays true — the next request then re-requests links from the start while content and folder cursors keep advancing, which would surface as duplicate link rows on a later page.

I could not confirm the backend ever does that, so this may be purely a comment fix. But the one change satisfies both readings:

linkCursor: response.hasMoreLinks
    ? (response.nextLinkCursor ?? bookmark?.linkCursor ?? 0)
    : 0,

(For contrast, folderCursor on the line above needs no fallback — nextFolderCursor and hasMoreFolders are both non-optional, so that asymmetry looks deliberate and correct.)

},
// Nothing to browse and nothing useful to say beyond that — same as the File field,
// which simply does not open when no site resolves.
error: () => finish(null)

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.

Would it be worth logging here before resolving null?

getCurrentSite() is a plain HttpClient.get() with no catchError upstream, so this branch is what runs on a 5xx, a network failure, or an expired session. As written, the template author's onClose(null) fires and that is indistinguishable from the user pressing Cancel — with nothing in the console and no signal for whoever is debugging a "the browse button does nothing" report.

The comment says there is "nothing useful to say beyond that", which is fair for the user-facing side, but the developer-facing side currently gets nothing at all:

error: (err) => {
    console.error('DotCustomFieldApi.openBrowserModal: failed to resolve the current site', err);
    finish(null);
}

The !site branch above has the same property, though that one is at least a expected-shape outcome rather than a failure.

return { siteId: site.identifier, hostname: site.hostname, path: item.path };
}

const url = (item as DotCMSContentlet).url;

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.

Is this cast still accurate? After isFolder(item) returns false on the line above, the narrowed type is DotCMSContentlet | DotContentDriveLink — and a link genuinely reaches here, via the !isContentlet(asset) early-return in confirm() which calls this function with link rows.

Both union members declare url: string, so the cast also looks unnecessary — item.url should compile directly on the union:

const url = item.url;

Dropping it would mean a future browse-item variant without .url fails at compile time rather than silently reading undefined here.

showWorking: false,
showArchived: false,
sortByDesc: true,
extensions: ['jpg']

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.

extensions is not a field on DotAssetPickerBrowseOptions — should it be in this fixture?

The interface defines five fields (showFolders, showLinks, showWorking, showArchived, sortByDesc), and this PR's own spec.md documents extensions as deliberately not exposed yet because the endpoint has no such parameter. The bridge-side comment makes the same point ("No extensions here on purpose").

The assertion still passes because excess-property checking does not apply to a variable (only to an inline object literal), and toEqual compares the same object against itself. So the test would keep passing even if browse pass-through broke for the real fields.

Worth dropping extensions: ['jpg'] so the fixture matches the contract as shipped? Otherwise the next reader could reasonably conclude it is a supported browse option today.


return {
...base,
name: row['name'] ?? row['fileName'],

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.

These four values are typed unknown (from Record<string, unknown>), and the trailing as DotBrowserSelection is what makes the object literal type-check rather than any actual narrowing.

The base fields above all go through String(...), so they are guaranteed strings. These do not, yet DotBrowserSelection declares them as string | undefined — so a malformed row handing back a number or an object for baseType would flow through to consumers that read .baseType as a string with full confidence.

Would a small narrowing helper be worth it here?

const asString = (v: unknown): string | undefined =>
    typeof v === 'string' ? v : undefined;

then name: asString(row['name']) ?? asString(row['fileName']), and likewise for the other three — which would also let the as DotBrowserSelection cast go away.

foldersStatus: signal(ComponentStatus.LOADED),
selectedNode: signal<{ data: unknown } | undefined>(undefined),
selectedAsset: signal<DotCMSContentlet | null>(null),
selectedAsset: signal<DotContentDriveItem | null>(null),

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.

Should this mock match the widened store contract? The real DotAssetPickerSelectionState.selectedAsset became DotContentDriveBrowseItem | null in this PR, which includes DotContentDriveLink; the mock is still DotContentDriveItem | null, which does not.

That is what forces the as unknown as DotContentDriveItem double-cast on SELECTED_FOLDER and SELECTED_LINK above — and a double-cast means the type-checker can no longer catch a drift between these fixtures and what the store actually accepts.

Typing the signal as signal<DotContentDriveBrowseItem | null>(null) should let both fixtures use a single direct cast (or none, for the link, which already matches the shape).

// exactly what it always was.
archived: browse?.showArchived ?? false,
// `live: true` means published-only. The endpoint's own default is `false`
// (working included), which is what every entry point but `browse` wants, so

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.

Small wording nit: should this say openBrowserModal rather than browse?

Every sibling comment in this file draws the same contrast that way — line 118 ("every entry point but openBrowserModal uses") and line 143 ("every entry point but openBrowserModal"). Here it reads "every entry point but browse", and browse is the name of the options object (pickerConfig?.browse) rather than an entry point, so it briefly scans as a different thing.

…the-new-assetpicker-and-delete-the-legacy-browser-selector
… mode

All nine findings from @oidacra's review of #37273. Two change behaviour, the
rest tighten types and diagnostics.

Behavioural:
- `$totalRecords` only inspected `hasMoreContent`, so a page whose content
  stream ended while folders or links kept going reported the rows already on
  screen as the grand total. PrimeNG then disabled Next and those rows became
  unreachable. It now accounts for all three streams. Covered by three new
  store tests.
- `sort.field` was read for its direction only and silently dropped, so a
  caller asking for `title` got the default `modDate`. Both shipped templates
  pass `modDate`, which is why it stayed invisible. `DotAssetPickerBrowseOptions`
  gains `sortField`, wired through to the store's initial sort.

Correctness and diagnostics:
- The link-cursor fallback was `?? 0` while its own comment claimed it
  preserved the last-sent cursor. `nextLinkCursor` and `hasMoreLinks` are both
  optional, so a response reporting more links without a cursor would rewind
  the stream and re-serve links already shown.
- A failed `getCurrentSite()` resolved null with nothing logged, making a 5xx
  or expired session indistinguishable from the user pressing Cancel.
- `toSelection` cast four `unknown` values straight into `string` fields; they
  now go through a narrowing helper, which also removes the blanket cast.
- Dropped an unnecessary `as DotCMSContentlet` in `#resolveAssetLocation` —
  both union members declare `url`, so reading it directly means a future
  variant without one fails to compile.
- Test fixtures: removed `extensions` from a browse-options fixture (not part
  of the contract), and typed the component-spec selection mock as
  `DotContentDriveBrowseItem` so its fixtures no longer need double casts.
- Comment wording: `openBrowserModal` rather than `browse`, matching siblings.

Verified with `nx build dotcms-ui`, lint across 5 projects, and the picker,
bridge and Content Drive suites (1377 Content Drive tests unchanged).

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

claude Bot commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Pull Request Unsafe to Rollback!!!

  • Category: H-8 — VTL Viewtool Contract Change (adapted: this is a client-side JS API contract — DotCustomFieldApi.openBrowserModal() — invoked from stored/rendered VTL custom-field templates, the same class of risk H-8 describes for server-side Velocity viewtools: a contract change that breaks a persistent template consumer that survives a rollback)

  • Risk Level: 🟠 HIGH (mitigated — see note below)

  • Why it's unsafe: openBrowserModal()'s option shape and callback payload were redesigned in a breaking, non-backward-compatible way, and the two shipped VTL templates that call it were migrated to the new shape in this same PR:

    • header (string) → title (string)
    • params: { showFiles, showPages, showFolders, showDotAssets, showWorking, showArchived, sortByDesc }kinds: DotBrowserItemKind[], status: 'live'|'working'|'archived', sort: { field, direction }
    • onClose: (result) => result.urlonClose: (selection) => selection.url
    • The old interface (BrowserSelectorOptions/BrowserSelectorController) was deleted outright, not deprecated.

    If this release (N) is rolled back to N-1, the JS bridge (angular-form-bridge.ts / dojo-form-bridge.ts) reverts to only understanding the old shape. Any stored custom-field VTL that was migrated to (or newly written against) the new shape — including customer-authored custom fields that copied/customized these starter templates while running on N — will call openBrowserModal with title/kinds/status/sort and read selection.url, none of which the reverted N-1 bridge understands. The picker fails to open correctly or the field never receives a value, at render time, with no compile-time or startup-time signal.

    Mitigating factor: both call sites are gated behind $structures.isNewEditModeEnabled() in file_browser_field_render.vtl / redirect_custom_field.vtl, which checks a per-Content-Type metadata flag (Constants.CONTENT_EDITOR2_ENABLED) that is not enabled by default — this is not a shared/global template (layout, header, footer), so exposure is narrower than the worst case the reference doc warns about. Per the doc's own guidance ("provably confined to a single scope-limited template" → downgrade to MEDIUM), this could arguably be MEDIUM, but since DotCustomFieldApi.openBrowserModal is a public documented API that any custom-field author (not just these two shipped templates) could target, we're flagging HIGH per the "absent that proof, assume HIGH" default.

  • Code that makes it unsafe:

    • dotCMS/src/main/webapp/WEB-INF/velocity/static/content/file_browser_field_render_new.vtl (the DotCustomFieldApi.openBrowserModal({...}) block — header/params/onClose(result)title/kinds/status/sort/onClose(selection))
    • dotCMS/src/main/webapp/WEB-INF/velocity/static/htmlpage_assets/redirect_custom_field_new.vtl (same pattern)
    • core-web/libs/edit-content-bridge/src/lib/interfaces/form-bridge.interface.tsopenBrowserModal(options: BrowserSelectorOptions): BrowserSelectorControlleropenBrowserModal(options?: DotBrowserOptions): DotBrowserController
    • core-web/libs/edit-content-bridge/src/lib/interfaces/asset-browser.interface.ts (new) replacing the deleted core-web/libs/edit-content-bridge/src/lib/interfaces/browser-selector.interface.ts
    • core-web/libs/edit-content-bridge/src/lib/bridges/angular-form-bridge.ts and dojo-form-bridge.ts (implementations updated to only understand the new contract)
  • Alternative (if possible): Apply H-8's two-phase contract pattern — for one release, have the bridge implementation accept both shapes (detect legacy header/params/old-style onClose(result) and translate internally to the new model), and only remove the old-shape handling in N+1 once N-1 is outside the rollback window. This also protects any customer who forked these starter templates before this PR.

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

Labels

AI: Not Safe To Rollback Area : Backend PR changes Java/Maven backend code Area : Frontend PR changes Angular/TypeScript frontend code

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

Migrate openBrowserModal to the new AssetPicker and delete the legacy Browser Selector

4 participants