Skip to content

feat(content-drive): replace Content Search and Site Browser for Content Drive users (#37759) - #37832

Merged
dario-daza merged 38 commits into
mainfrom
37759-content-drive-open-legacy-editor-content-in-the-side-panel
Oct 7, 2026
Merged

dario-daza merged 38 commits into
mainfrom
37759-content-drive-open-legacy-editor-content-in-the-side-panel

Conversation

@dario-daza

@dario-daza dario-daza commented Sep 30, 2026 •

Copy link
Copy Markdown
Member

The one PR for #37759: Content Drive replaces Content Search and Site Browser for users who only have Content Drive in their menu. The issue has two parts, and they were first split into stacked PRs. We closed the stack (#37829) to keep things simple, so both parts land here.

This PR now carries both parts, implemented:

  1. Part 1, route redirects. @zJaaal's commits from fix(dotcms-ui): redirect Content Search and Site Browser routes to Content Drive (#37759) #37829 (750e33d645, 0936c76c1f, 7cefdd247d, bbd5a22c10). Part 2 later removed the CD_ restoration and the loop check from them (see below).
  2. Part 2, legacy-editor content in the Content Drive side panel. The approved spec (specs/37759-content-drive-legacy-side-panel/spec.md), its data model and contracts, and the implementation.

Please review the Part 2 code. The spec was reviewed in the first revision.

Why

When a user's menu has Content Drive but not Content Search or Site Browser, every /c/content… and /c/site-browser… URL hits MenuGuardService. The guard finds the portlet missing and sends the user to their first portlet with no explanation. That breaks every link from other screens. Inside Content Drive, it breaks editing and creating any content whose type still uses the legacy editor. The server does not block the legacy editor (UVE already embeds it); only the Angular route does.


Part 1: route redirects

When the portlet is missing and Content Drive is in the menu, MenuGuardService returns the Content Drive equivalent instead of going to the first portlet. Every /c/:id route goes through this guard, so internal navigations, routerLinks, and full-page links from JSPs, emails and bookmarks are all covered.

Requested URL Redirects to
/c/content /content-drive
/c/content?filter=<variable> /content-drive?filters=contentType:<variable>
/c/content/<inode> /content-drive?editContent=<identifier>&editContentLang=<languageId> (looked up with GET /api/v1/content/<inode>)
/c/content/new/<type> /content-drive?createContent=<type>
/c/content/new /content-drive
/c/site-browser /content-drive
/c/site-browser?path=//<host>/<folder>/ /content-drive?path=/<folder>/, switching to <host> first when it is not the current site
  • Other params: params with no Content Drive equivalent (variantName, the legacy folder inode) are dropped.
  • Containers and templates stored as files open on their folder: those callers now also pass the folder to DotRouterService.goToSiteBrowser(path), and the guard maps it with the host stripped. The legacy Site Browser still reads the session, so users who have it see no change.
  • When the inode lookup fails: the standard HTTP error handler reports it and the user lands on Content Drive. If the handler already navigated away (a 401 goes to login), the guard stops.
  • Content Types chip: a link that names only a content type (filters=contentType:Blog) now shows it in the chip. DotContentTypeFilterComponent (libs/ui, shared with the asset picker) fills in the implied base type and drops content types the server doesn't return. The Content Drive store's init effect no longer re-runs when the default language arrives, which used to overwrite filters changed in between.
  • Unchanged: users who still have Content Search or Site Browser in their menu, and users without Content Drive.

Part 2: legacy editor in the side panel

Content Drive opens legacy-editor content in its own side panel. The panel copies the new-editor panel and hosts the legacy editor in an iframe. The links Part 1 builds now open in the editor each content type chose. Content Drive stops reading the side-panel flag: both editors always open in a panel.

Path With Part 1 alone Now
Edit legacy content from Content Drive Lands on the user's first portlet Legacy editor in the side panel (US1)
Create legacy content from Content Drive Redirected back to Content Drive with nothing open Legacy create form in the panel, in the current folder (US2)
?editContent= link to legacy content (Query Tool, Publishing Queue, Block Editor, emails) Opens in the new editor, ignoring the type's setting Opens in the legacy panel (US5)
?createContent=<type> link (starter onboarding cards) Nothing reads it Opens the create form for that type (US2)
Side-panel flag (FEATURE_FLAG_EDIT_CONTENT_SIDE_PANEL) turned off Content Drive opens new-editor content full screen Content Drive doesn't read the flag: both editors always open in a panel (FR-006). UVE, Query Tool and the relationship field keep reading it
URL while a panel is open (either editor) Edit: editContent + editContentLang. Create: editContent=new, without the type Edit unchanged, and editContentLang follows a language switch in either editor's panel, or a save of a new translation. Create: createContent=<type>, switching to the edit params after the first save. All removed on close (US7)
URL while a folder dialog is open (New Folder, Folder Settings, Edit Permissions) No param, so a refresh closes it createFolder=true, editFolder=<identifier>, folderPermissions=<identifier>, removed on close (US8)
URL with params that can't hold together Not handled Opens nothing and removes them (FR-023)
CD_ params from #33726 Produced on every legacy hand-off and restored by the guard Removed, together with the guard's loop check (FR-026, FR-027)
"Switch to the old editor" / load error in the new-editor side panel Leaves the drive for c/content Stays in Content Drive. The switch reopens the same content and language, or the same create, in the legacy panel. A load error shows the standard error and closes the panel. Other openers keep main's behavior (US6)
Compare versions from the legacy History tab Not handled in Content Drive Compare dialog over Content Drive, and Bring Back restores the version in the panel (US6)
Breadcrumb when Content Drive is reached by a link (Content Types, Query Tool, a pasted URL) Shows the previous portlet's trail Names Content Drive

The panel also handles:

  • workflow actions, including the wizard and push publish;
  • delete;
  • the same unsaved-changes prompt as the new-editor panel, including on browser Back;
  • a quiet list refresh (no loading placeholder) on every save and every close, keeping folder, filters and page.

Every create starts in the language the list is filtered by, or in Content Drive's default language when there's no language filter (FR-003). Saving a new page in the legacy panel closes it and opens the page editor, as Content Search does (FR-008). The legacy editor's links to the page editor are plain links and keep working.

Where the code is:

  • Content Drive (libs/portlets/dot-content-drive):
    • DotLegacyEditorSidePanelComponent is the new panel. It hosts the iframe and handles the legacy ng-events.
    • DotContentDriveNavigationService decides which panel opens. It keeps what the panel shows separate from what the URL says, so the URL can follow a save or a language switch without remounting the editor.
    • The shell owns the URL: it writes the params, handles Back, and reads links once on load (resolveContentDriveUrlIntent).
  • Edit Content (libs/edit-content):
    • An optional EDIT_CONTENT_NAVIGATION_OVERRIDE token. content.feature.ts keeps main's router.navigate calls for "switch to the old editor" and a load error, and asks the override first. Only Content Drive provides it.
    • reloadContent carries the language of a locale switch. The layout reports it once the reload runs, and the side panel emits it through a languageChanged output.
  • data-access:
    • DotActionUrlService moved out of UVE so Content Drive can use it.
    • New: DotFolderService.getFolderById and DotPermissionsService.getUserAccess, for the folder-dialog links.
  • dotcms-ui: the CD_ producer, its readers in the contentlet wrapper and create screens, and the guard's CD_ restoration and loop check are removed.
  • global-store: a contentDrive breadcrumb handler. docs/frontend/BREADCRUMBS.md is updated.

Decisions worth a look:

  • Each content type keeps its own editor. Always opening the new editor in the panel was rejected because it silently switches editors for types that opted out (FR-007). The new-editor setting (CONTENT_EDITOR2_ENABLED plus each type's opt-out) is unchanged and is the only thing that decides which panel opens.

  • Content Drive stops reading the side-panel flag (changed on review). Both editors always open in a panel there, and the full-screen fallback for new-editor content goes away. Content Drive never hands off to the full-page legacy editor, so the CD_ round trip and the guard's loop check are removed (FR-006, FR-026, FR-027). The flag itself isn't removed or deprecated.

  • The legacy panel lives in the Content Drive lib, its only consumer. Putting it in libs/ui would have risked a dependency cycle with edit-content. Dropping the legacy editor later never touches the new-editor panel, but it is more than that folder: it also removes the panel's models (shared/legacy-editor.models.ts), the 'legacy' case of DotContentDrivePanelRequest, the navigation service's legacy branches and its EditContentNavigationOverride implementation, the shell's legacy block and handlers, and the override token below. Each piece says "Remove with the legacy editor", so one grep finds them all.

  • The panel listens only to the legacy events it needs: close, save, data changed, workflow wizard, push publish and compare versions (FR-008). We're moving away from the legacy editor, so the spec doesn't commit to its whole event contract.

  • Only Content Drive changes "switch to the old editor" and a load error (changed on review). main's router.navigate cannot work inside Content Drive:

    • Users with Content Search would leave the drive.
    • Content Drive-only users would be redirected to the route they are already on, which reads its link params only once.

    Instead, Content Drive provides an optional EDIT_CONTENT_NAVIGATION_OVERRIDE on its shell. The editor asks it first and otherwise navigates exactly as on main. Every other opener (Edit Content portlet, Query Tool, UVE, relationship field, asset picker) is unchanged. Content Drive answers only for the panel it opened, matched by what the editor was opened with, which stays the same across a language switch. Nested editors inherit the provider (a related content opened inside the panel), so they are declined and navigate as on main. The token, its provider and the two branches carry the same "Remove with the legacy editor" marker. main's crash when switching a create that was never saved is unchanged outside Content Drive.

  • The URL follows the language of the new-editor panel too. A language switch reloads the editor in place; once the reload runs, the panel reports the language and Content Drive keeps editContentLang in step (replaceState), as it already did for the legacy panel.

  • Refresh on every close. The panel closed, so the list refreshes; a delete isn't told apart from any other close. The cost is one extra list request on a close with no changes.

  • Links are untrusted input:

    • editContent, editFolder and folderPermissions are used only when shaped like a dotCMS identifier, and createContent only when shaped like a type variable or id.
    • The REST paths they reach are encoded.
    • The legacy create screen is resolved before the panel opens, so a type the server won't serve shows the standard error and writes no URL or history entry.
  • Folder-dialog links follow the context menu's rules: Folder Settings needs edit permission and Edit Permissions needs permission to edit permissions. A New Folder link where New Folder isn't offered shows the standard 403. A folder that is gone or in another site shows the standard 404. In every case nothing opens and the param is removed.

  • A type the user can read but not create: the form opens and the editor refuses the save with its own message. Content Drive adds no permission check of its own.

Out of scope: any change to the side-panel flag outside Content Drive; related content opened from a Block Editor field inside either panel (the bubble menu navigates the whole window); server-rendered JSP/Java p_p_id=content|site-browser links.


Checklist

  • Part 2 spec approved by another dev
  • Tests (Part 1): menu-guard.service.spec.ts covers each redirect row, lookup failure and the already-redirected case, folder paths on the current site, another site and an unknown host, and the "portlet still in menu" and "no Content Drive" cases. dot-content-type-filter.component.spec.ts covers the base-type fill-in and the unknown-type drop. dot-content-drive.store.spec.ts covers a filter changed before the default language lands. The router service, container list and template list specs assert the folder is passed.
  • Tests (Part 2, Vitest + Spectator, per FR-034):
    • The routing decision: new vs legacy editor, and edit, create, editContent link or createContent link, with no dependency on the flag.
    • Each legacy editor event, the list refresh after save and after every kind of close, and the unsaved-changes prompt including Back.
    • The URL params for each panel and folder-dialog state, the language switch, Bring Back, conflicting params, and malformed or unreachable links.
    • The page editor after a new page's first save, the CD_ removal, and the switch and load-error paths of the new-editor panel.
  • Translations: none added. The panel reuses the new-editor panel's unsaved-changes wording and the existing folder-dialog and permissions labels.
  • Security Implications Contemplated:
    • Link params are shape-checked and encoded before they reach a query or a REST path.
    • Only same-origin relative URLs load in the legacy iframe.
    • The inode lookup and the folder and permission lookups run with the user's own permissions.
    • Redirects only target the internal /content-drive route.
    • The legacy editor enforces content permissions itself.

Additional Info

  • No e2e tests: the flows need a user whose menu has Content Drive but not Content Search, and the e2e setup has no helper for that role and menu layout. The spec's manual scenarios (specs/37759-content-drive-legacy-side-panel/quickstart.md) were run by hand against a local instance with that menu. The e2e helpers that build /c/content?filter= URLs stay as they are: they run as an admin who still has Content Search, so the guard never redirects them.

This PR fixes: #37759

🤖 Generated with Claude Code

This PR fixes: #37759

zJaaal and others added 5 commits September 30, 2026 13:16
…ntent Drive (#37827)

When a user's menu has Content Drive but not Content Search or Site
Browser, MenuGuardService now sends their URLs to the Content Drive
equivalent instead of the first portlet:

- /c/content and /c/site-browser open Content Drive
- /c/content?filter=<variable> opens it filtered by that content type
- /c/content/<inode> looks the content up and opens it by identifier
  and language (?editContent=&editContentLang=)
- /c/content/new/<type> opens ?createContent=<type>

Edit and create links restore the CD_ params Content Drive adds when it
hands off to the legacy editor. Edit and create links that come from
Content Drive itself are not redirected, since Content Drive still sends
legacy-editor content to those URLs and redirecting would loop.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… double navigation (#37759)

- Containers and templates stored as files now pass their folder to
  goToSiteBrowser(path). The menu guard turns //<host>/<folder>/ into
  Content Drive's ?path=, switching to that site first when it is not
  the current one. The legacy Site Browser still reads the folder from
  the session, so users who have it see no change.
- When the inode lookup fails and the error handler has already
  navigated (a 401 goes to login), the guard now stops instead of also
  going to the first portlet. Rejections with no Content Drive
  equivalent are told apart with null.
- The variant id is cleared on the Content Drive redirect too, not only
  when going to the first portlet.
- The loop protection now covers edit links only. Nothing reads
  createContent yet, so a legacy create from Content Drive stays in
  Content Drive instead of jumping to the first portlet.
- The Content Search filter goes over as contentType only, with no
  lookup: the Content Types chip now fills in the base type.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#37759)

The chip lists content types under their selected base type, so a link
that names only a content type (filters=contentType:Blog) filtered the
grid but left the chip empty. The chip now adds the base types of the
host's content types, resolved from the content types it already loads,
so no extra request. Content types the server does not return are
dropped from the host's selection after a successful load, so a deleted
type or a typo in an old link does not keep filtering with nothing to
clear.

Content Drive's init effect no longer tracks the default language: it
re-ran when the language landed and re-read the page's original URL
over any filter changed in between, including that cleanup.
loadDefaultLanguage already seeds the language into the current
filters.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- menu-guard spec: the guard result starts undefined until the guard emits.
- container list spec: narrow the file container found in the mock list.
- Content Drive store: pass SYSTEM_HOST while the site has not loaded,
  as initContentDrive already falls back to.

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

claude Bot commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Claude encountered an error after 0s —— View job


I'll analyze this and get back to you.

@dario-daza dario-daza changed the title docs(content-drive): spec for opening legacy-editor content in the si… docs(content-drive): spec for opening legacy-editor content in the side panel (#37759) Sep 30, 2026
…ith the stack (#37759)

- Follow the stack's numbering: route redirects (#37829) are Part 1, the
  legacy-editor side panel is Part 2.
- Renumber the functional requirements so they increase in document order.
- State the create location once: the folder Content Drive is showing, the
  `path` of a `createContent` link, or the site root.
- Record that a workflow wizard action reports a save like a plain save
  (checked in edit_contentlet_js_inc.jsp), and add the rejected-save case.
- Align with what #37829 implemented: the redirect keeps folder and filters,
  CD_ params are added on every legacy hand-off until this work, and page
  creates follow the same create flow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dario-daza
dario-daza marked this pull request as ready for review September 30, 2026 19:44
Comment thread specs/37759-content-drive-legacy-side-panel/spec.md Outdated
Comment thread specs/37759-content-drive-legacy-side-panel/spec.md Outdated
Comment thread specs/37759-content-drive-legacy-side-panel/spec.md Outdated
Comment thread specs/37759-content-drive-legacy-side-panel/spec.md
@dario-daza
dario-daza removed this pull request from stack #37833 September 30, 2026 19:59
@dario-daza
dario-daza changed the base branch from issue-37827-content-drive-route-redirect to main September 30, 2026 20:03
…l spec

The stack with #37829 was closed and its commits brought into this branch,
so the spec now says Part 1 is already on the branch and Part 2's
implementation follows in the same PR.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions github-actions Bot added the Area : Frontend PR changes Angular/TypeScript frontend code label Sep 30, 2026
…7759)

- Open the Scope with what this work delivers.
- Legacy-editor content always opens in the panel, whatever the side-panel
  flag says, so the CD_ round trip from #33726 and the guard's loop check
  are removed (FR-006, FR-026, FR-027).
- The URL follows the open panel for both editors: editContent +
  editContentLang for edits, createContent=<type> for creates, switching to
  the edit params after the first save, all removed on close (US7,
  FR-020, FR-024, FR-025).
- Record that push publish reaches the legacy editor through workflow
  actions and that it has no Add to bundle action of its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…de-panel spec (#37759)

New Folder, Folder Settings and Edit Permissions write createFolder=true,
editFolder=<identifier> and folderPermissions=<identifier> while open, reopen
from the URL and remove the param on close (US8, FR-030 to FR-033). Tests
move to FR-034.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dario-daza

Copy link
Copy Markdown
Member Author

Technical notes for #37759

These are the implementation notes that lived in the #37759 description. The issue now describes only the business rules, for QA, so the notes move here to stay at hand for /speckit-plan and the Part 2 implementation. The spec (specs/37759-content-drive-legacy-side-panel/spec.md) stays the source of truth for what Part 2 does; these notes are about how.

Part 2 (not implemented yet)

  • Refresh on every close, not only on detected deletes. Deleting or destroying non-page content makes the JSP send a bare close with no data. Only HTML pages send deleted-page (edit_contentlet_js_inc.jsp, save callback, around lines 704-720). A close after a delete looks exactly like a Cancel, so the panel should not try to tell them apart. Reloading on every close also covers workflow actions fired from the wizard (see the next note), moves, language switches and anything else the JSP changes without an event Content Drive handles. The cost is one extra list request when the author closes without changing anything.
    • Use reloadContentDrive({ quiet: true }) for the close reload. The plain reloadContentDrive() shows the loading skeleton, which would flash on every Cancel.
    • Keep the reload on save-page too, so the list updates while the panel is still open when the author saves and keeps editing.
    • The panel then only needs two outputs: saved on save-page, and closed on any close. The shell treats both as "reload quietly".
  • Two more iframe events are needed beyond the six the full-page wrapper handles (close, save-page, deleted-page, edit-contentlet-data-updated, edit-page, edit-contentlet-loaded, in DotContentletWrapperComponent). The legacy editor also sends workflow-wizard (workflow actions with comments, assignment or push publish inputs) and push-publish. In apps/dotcms-ui these go to DotCustomEventHandlerService, which a lib cannot import. Both can be handled from a lib: DotWorkflowEventHandlerService (@dotcms/data-access) and DotPushPublishDialogService (@dotcms/dotcms-js). The wizard calls back into the JSP through DotIframeService.run, so the panel's iframe must subscribe to DotIframeService.ran() and call the named function on its window, the way IframeComponent does. Confirmed in edit_contentlet_js_inc.jsp: the wizard calls back saveAssignCallBackAngular, which runs the editor's normal saveContent, so a wizard action ends in save-page like a plain save (or close / deleted-page when the content is gone). The reload on close stays as a safety net.
  • Less rework in the shell. Keep $editPanelRequest in DotContentDriveNavigationService in its current shape and add a separate signal saying which editor is open (new or legacy), both derived from one source signal. The shell's URL sync, browser Back handling and reload hold all key off $editPanelRequest, so they work for both panels. The URL sync changes in two places, for both editors: a create panel writes createContent=<type> instead of the editContent=new marker (NEW_CONTENT_MARKER), and after the first save it switches to editContent / editContentLang, using the contentlet the new-editor panel's saved output emits or the legacy save-page payload. The legacy panel needs the same public requestClose() so Back can route through its unsaved-changes guard.
  • Deep link. openEditByIdentifier always opens the new editor today. It needs a getContentType lookup on the resolved contentlet to send legacy content to the legacy panel, whatever the side-panel flag says. The existing deep-link tests will then need getContentType mocked.
  • Tests that will need to change. The navigation service tests that expect legacy content to route to c/content/... are replaced: that routing is removed, with the side-panel flag on and off. The shell tests that assert the editContent=new marker change to createContent.
  • Removing the CD_ round trip. mapQueryParamsToCDParams (the producer, in DotContentDriveNavigationService), mapParamsFromEditContentlet (read in DotContentletWrapperComponent.onClose, DotCreateContentletComponent.onClose and MenuGuardService) and CD_PARAM_PREFIX have no other users, so they go with it. DotCreateContentletComponent.onClose keeps its goToContent() fallback for users who reach it from Content Search.
  • Unsaved-changes prompt. To match the new editor, use PrimeNG's ConfirmationService with the edit.content.unsaved.changes.* message keys (see DotEditContentLayoutComponent, private confirmIfDirty), not the legacy wrapper's editcontentlet.lose.dialog.* prompt. Dirty state comes from the edit-contentlet-data-updated event and clears on save-page.
  • Create URL. The legacy create form URL comes from GET /api/v1/portlet/_actionurl/<contentType> (entity), with folder=<inode> appended so the Host/Folder field pre-selects the current folder. This mirrors DotCreateContentletResolver.
  • Two new-editor paths also leave the drive. Both come from content.feature.ts, which the panel shares with the full-page editor through DotEditContentLayoutComponent: "switch to the old editor" navigates to c/content/<inode>, and the load error navigates to c/content. Both call the router directly instead of going through the EditContentHost port. Adding them to the port (the full-page RouterEditContentHost keeps today's navigation, and OverlayEditContentHost hands them to the panel) keeps the full-page editor unchanged.
  • Folder dialogs in the URL. New Folder and Folder Settings open through the store's dialog state (setDialog, from the toolbar and dot-folder-list-context-menu.component.ts), so the shell's URL sync can follow them. Edit Permissions opens directly through DialogService from the context menu, so it has to go through the store (or the shell) for the URL to see it. It only needs the folder identifier. Folder Settings needs the folder resolved on load (GET /api/v1/folder/{folderId}) and mapped to DotContentDriveActionableFolder, since today it receives the listing row.
  • One more iframe event. The legacy editor's History tab sends compare-contentlet (edit_contentlet_js_inc.jsp, emmitCompareEvent). In apps/dotcms-ui, DotCustomEventHandlerService forwards it to DotEventsService, and DotContentCompareDialogComponent (libs/portlets/edit-ema/ui) listens for it, so a lib can handle it too.

Code pointers

  • Part 2 code paths: libs/portlets/dot-content-drive/portlet/src/lib/shared/services/dot-content-drive-navigation.service.ts (#editContentlet, createContent, openEditByIdentifier), and the shell's side-panel host in dot-content-drive-shell.component.html, which reloads through onEditPanelSaved
  • dot-iframe, DotContentletWrapperComponent and DotContentletEditorService live in apps/dotcms-ui, which a lib cannot import. The iframe and event handling will need a lib-level home (for example libs/ui or libs/edit-content)
Part 1 (implemented in this PR): notes and the inventory of links it fixes
  • Redirect in the guard, not in each caller. MenuGuardService is the single place every c/:id URL passes through. When the requested portlet is not in the menu, getContentDriveRedirect returns a UrlTree to the Content Drive equivalent if the user has content-drive, and falls back to goToFirstPortlet() otherwise. That covers internal router.navigate calls, routerLinks, and full-page links from JSPs, emails and bookmarks alike. Call sites only changed where the URL alone doesn't carry enough information (templates and containers stored as files, below).
  • Inode to identifier. Old editor links carry an inode, and Content Drive's link takes an identifier plus a language (dot-content-drive-shell.component.ts reads editContent and editContentLang once on load). The guard looks the inode up with DotContentletService.getContentletByInode (GET /api/v1/content/<inode>, which returns both). If the lookup fails, DotHttpErrorManagerService reports it and the user lands on /content-drive. If the handler already navigated away itself (a 401 goes to login), the guard stops without a second navigation.
  • Loop check, by origin rather than by flag. An edit link is not redirected when the navigation starts from Content Drive (router.url starts with /content-drive), whatever the side-panel flag says. Content Drive still sends legacy-editor content to /c/content/<inode> until Part 2 lands, and opening editContent would send it straight back, so that user still goes to their first portlet. Create links are always redirected, because nothing reads createContent yet. Part 2 removes the check: once Content Drive opens legacy-editor content in its own panel, whatever the side-panel flag says, it never links to /c/content/..., so there is no loop to prevent (Part 2 spec, FR-027).
  • Create links. /c/content/new/<type> redirects to /content-drive?createContent=<type>, and /c/content/new with no type to /content-drive. The Part 2 spec keeps the name createContent (FR-024). Until Part 2 reads it, a create link opens Content Drive with nothing selected. Part 2 opens the create form in the editor the type chose, in the folder named by path, or the site root, and writes the same param for its own open create panel. htmlpageasset needs no special case: Content Drive's own create action already creates pages through the same create flow.
  • Folders for templates and containers. Those three callers still call DotSiteBrowserService.setSelectedFolder(path), which saves the folder in the server session for the legacy Site Browser. They now also pass it to DotRouterService.goToSiteBrowser(path) as a path query param. The guard strips the host from //<host>/<folder>/, since Content Drive's path is a folder inside the current site, and switches site first (GlobalStore.switchCurrentSite) when the host is not the current site. If the host can't be found, the user lands on Content Drive at the root. Users who still have Site Browser see no change.
  • Query params carried over. Edit and create redirects restore the CD_-prefixed params with mapParamsFromEditContentlet, so a hand-off from Content Drive's own legacy path lands back on its folder and filters. Part 2 removes this restoration together with the CD_ params (Part 2 spec, FR-026). Other params have no Content Drive equivalent and are dropped: variantName (appended by lucene_search.jsp) and the legacy folder inode.
  • Content Types chip. A link that names only a content type (filters=contentType:Blog) filtered the list but left the chip empty, because the chip lists content types under their base type. DotContentTypeFilterComponent (libs/ui, shared with the asset picker) now fills in the implied base type and drops content types the server doesn't return (deleted, or a typo in an old link). For that to stick, the Content Drive store's init effect no longer re-runs when the default language arrives, which used to re-read the original URL over filters changed in between.
  • Block Editor "back" link. The bubble menu stores a return URL built on the /c/content/<inode> pattern (dot-bubble-menu.component.ts, generateBackUrl). After the redirect it still works, but it lands in Content Drive instead of the editor the user started from.
  • e2e helpers left as they are. The helpers that build /c/content?filter= URLs (contentListingNavigation.ts, listingContent.page.ts) run as an admin who still has Content Search, so the guard never redirects them. There are no e2e tests for the redirects: the e2e setup has no helper to build a user whose menu has Content Drive but not Content Search, so the flows were checked by hand.

Content Drive URL format. Content Drive's filters param is key:value;key:value, and contentType takes the content type variable (libs/portlets/dot-content-drive/portlet/src/lib/utils/functions.ts, decodeByFilterKey). Its path param is a folder path inside the current site (dot-content-drive.store.ts).

Links this fixes

Legacy editor links (/c/content/<inode>, /c/content/new/<type>)

  • Starter onboarding cards: /c/content/new/webPageContent and /c/content/new/htmlpageasset (apps/dotcms-ui/.../onboarding-author.component.html)
  • "Switch to the old editor" in the full-page new editor (libs/edit-content/.../content.feature.ts, disableNewContentEditor). The side-panel case is covered by Part 2
  • Block Editor bubble menu, editing related content (libs/block-editor/.../dot-bubble-menu.component.ts)
  • Edit links from DotContentletEditUrlService (libs/data-access), used by the Publishing Queue dialogs and the Query Tool, and its copy in libs/new-block-editor (ContentletEditUrlService)
  • Lucene query screen (WEB-INF/jsp/lucene/lucene_search.jsp), dotAI (html/portlet/ext/dotai/dotai.js), and the "View in dotCMS" link in form-entry emails (VelocityUtil.java, linkToContent). These are plain /dotAdmin/#/c/content/<inode> links, so the Angular redirect covers them with no backend change

Content Search list links (/c/content, ?filter=)

  • Content Types list, "View (N)" (apps/dotcms-ui/.../dot-listing-data-table.component.html)
  • Full-page new editor: back button (dot-edit-content-form.component.ts, goBack via CONTENT_SEARCH_ROUTE), leaving deleted content (router-edit-content-host.ts, leaveDeletedContent), and the load-error fallback (content.feature.ts)
  • DotRouterService.goToContent(), used when the legacy create dialog closes (dot-create-contentlet.component.ts)
  • /c/content/new with no type (dot-contentlet-editor.routes.ts)
  • UVE "Add device" (libs/portlets/edit-ema/ui/.../dot-device-selector-seo.component.ts). Should also filter to the device content type

Site Browser links (/c/site-browser, all via DotRouterService.goToSiteBrowser())

  • Templates stored as files (dot-template-list.component.ts, editTemplate and goToFolder) and containers stored as files (dot-container-list.store.ts, editContainer)
  • Page editor fallbacks: page failed to load (dot-edit-page-resolver.service.ts), 403/404 on the page (dot-page-state.service.ts), and the site switching while editing a page (dot-app-lifecycle.effect.ts)

Not covered: server-rendered JSP and Java links that use p_p_id=content or p_p_id=site-browser portlet URLs (dashboard JSPs, link checker, publishing JSPs, ContentTypeUtil action URLs, the page editor's "reorder menu" in VelocityEditMode). They never go through the Angular routes.

@zJaaal zJaaal left a comment

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.

Second pass on the spec only, now that the earlier threads are addressed. Six things to tighten: language sync in the legacy panel, compare plus Bring Back, the rationale for refreshing on close, clearing incompatible URL params (and the default language for createContent), listening only to the legacy events we need, and a wording slip in SC-001.

Written by Claude Code on behalf of @zJaaal.

Comment thread specs/37759-content-drive-legacy-side-panel/spec.md Outdated
Comment thread specs/37759-content-drive-legacy-side-panel/spec.md
Comment thread specs/37759-content-drive-legacy-side-panel/spec.md Outdated
Comment thread specs/37759-content-drive-legacy-side-panel/spec.md Outdated
Comment thread specs/37759-content-drive-legacy-side-panel/spec.md
Comment thread specs/37759-content-drive-legacy-side-panel/spec.md Outdated
Comment thread specs/37759-content-drive-legacy-side-panel/spec.md
…e panel (#37759)

- editContentLang follows a language switch inside the legacy panel.
- Bring Back in the compare dialog restores the version, closes the
  dialog and refreshes the list.
- The panel listens only to the legacy editor events its requirements
  need; page-editor links stay plain links.
- A URL with params that can't hold together opens nothing.
- Every create starts in Content Drive's default language.
- State the refresh on close as a plain rule and fix SC-001's scope.

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

@rjvelazco rjvelazco left a comment

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.

@zJaaal zJaaal left a comment

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.

Narrowing the spec down after a discussion about the side-panel flag. Two decisions: the legacy panel copies the new-editor panel's p-drawer pattern with the legacy editor in an iframe, and Content Drive stops reading FEATURE_FLAG_EDIT_CONTENT_SIDE_PANEL (both editors always open in a panel). UVE, Query Tool and the relationship field are untouched, and the flag itself stays as it is. The wizard, push publish and event handling stay as the spec already has them.

Written by Claude Code on behalf of @zJaaal.

Comment thread specs/37759-content-drive-legacy-side-panel/spec.md
Comment thread specs/37759-content-drive-legacy-side-panel/spec.md Outdated
Comment thread specs/37759-content-drive-legacy-side-panel/spec.md Outdated
Comment thread specs/37759-content-drive-legacy-side-panel/spec.md Outdated
…he spec (#37759)

- Content Drive stops reading FEATURE_FLAG_EDIT_CONTENT_SIDE_PANEL: both
  editors always open in a panel, chosen only by the type's editor
  setting (FR-005 to FR-007, FR-022). The flag stays for UVE, Query Tool
  and the relationship field.
- Remove every flag-off and flag-unresolved case.
- The legacy panel copies the new-editor panel's drawer, with the legacy
  editor in an iframe (FR-012).
- Record that the legacy panel stays separate so it is easy to remove.

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

@rjvelazco rjvelazco left a comment

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.

You have some alerts, can you check them?

…l work (#37759)

The PR's strict typecheck gate reported 31 violations on lines this branch
wrote, plus one TypeScript only surfaced once those were gone.

- withDialog: declare dialog/dialogDrillDown as `X | undefined` instead of
  optional, so the store's `dialog` signal itself is never optional.
- content.feature: assert contentType in switchToLegacyEditor; the update
  call has already read its id by then.
- Specs: bracket access on deep-link query params, non-null Drawer query,
  a typed vi.fn() for defaultLanguageId instead of mocking a Signal, and
  drop the extra argument to SpectatorService.inject.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dario-daza
dario-daza requested a review from rjvelazco October 6, 2026 15:05
rjvelazco
rjvelazco previously approved these changes Oct 6, 2026
@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

⚠️ AI review failed

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

Run: #37484299695

@zJaaal zJaaal left a comment

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.

Re-review after the override token. This is in good shape: edit-content is back to main except the token, the override branch in content.feature.ts and the language work, and every opener other than Content Drive keeps main's navigation. Having the override return a boolean and match on what the editor was opened with is a nice touch: it also handles nested editors without the relationship field passing anything down.

Two small things inline, neither blocking.

Written by Claude Code on behalf of @zJaaal.

@zJaaal zJaaal left a comment

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.

Two structural points worth fixing in this PR. A follow-up suggestion, not for this PR: the shell grew from 2310 to 2734 lines with the URL-driven state (pending create/folder effects, folder-link resolution, Back handling, URL writes), and the navigation service doubled. A dedicated URL-state service or store feature would be a better home later.

Written by Claude Code on behalf of @zJaaal.

Comment thread core-web/libs/portlets/dot-content-drive/portlet/src/lib/shared/models.ts Outdated
… side panel (#37759)

- The Folder Settings and Edit Permissions links share one opener: the folder,
  the user's access and the site are resolved the same way, a folder in another
  site is refused, and only the permission checked and the dialog opened differ.
- The legacy panel's models move from the component folder to
  `shared/legacy-editor.models.ts`, so shared code no longer imports from a
  component.
- Every piece that goes with the legacy editor says "Remove with the legacy
  editor", so removing it is one grep: the panel, its models, the `'legacy'`
  panel request, the navigation service's legacy branches and its
  `EditContentNavigationOverride` implementation, the shell's block and
  handlers, and the override token with its two branches in `content.feature.ts`.
- The folder-dialog URL tests assert with `toHaveBeenLastCalledWith` instead of
  reading `createUrlTree.mock.calls`.

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

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

⚠️ AI review failed

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

Run: #37505199805

@zJaaal zJaaal left a comment

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.

One non-blocking cleanup inline.

Written by Claude Code on behalf of @zJaaal.

dario-daza and others added 2 commits October 6, 2026 14:51
- The legacy panel covers its iframe with a spinner until the editor has
  loaded, and again while the editor reloads itself (a save, a language switch,
  a restored version), detected through `pagehide` on the editor's window.
- The new editor showed nothing on its first load, because the layout renders
  only once there is content. It now shows the same spinner, over the whole
  editor. This is the layout every host uses, so the full-page editor, the
  Query Tool, UVE and the relationship field get it too.

Both use the spinner the new editor already shows while it reloads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… in (#37759)

Both callers, the shell's pending create and the content type picker, passed
`currentFolder()`, so the parameter carried nothing the navigation service
didn't already know. `createContent` now reads it itself, before the type
lookup, so the content lands in the folder the author was looking at when they
asked even if they browse while the request is in flight. `currentFolder()` is
private now. Its tests go through `createContent`, setting the folder in the
store, and one more covers browsing during the lookup.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@dario-daza
dario-daza requested review from rjvelazco and zJaaal October 6, 2026 20:13
@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

⚠️ AI review failed

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

Run: #37524675261

@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

⚠️ AI review failed

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

Run: #37524962067

zJaaal
zJaaal previously approved these changes Oct 6, 2026
@dario-daza
dario-daza enabled auto-merge October 6, 2026 20:48
…37759)

- Layout spec: patch the editor store through unprotected(), the
  @ngrx/signals/testing helper, instead of writing a protected store.
- Navigation spec: give the mocked selected folder the id, path and
  hostname its node type requires.

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

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

⚠️ AI review failed

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

Run: #37532977887

@dario-daza
dario-daza requested a review from zJaaal October 7, 2026 11:13
@dario-daza
dario-daza added this pull request to the merge queue Oct 7, 2026
Merged via the queue into main with commit 8fb8103 Oct 7, 2026
48 of 49 checks passed
@dario-daza
dario-daza deleted the 37759-content-drive-open-legacy-editor-content-in-the-side-panel branch October 7, 2026 13:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area : Documentation PR changes documentation files Area : Frontend PR changes Angular/TypeScript frontend code PR: docker image Build & push a per-PR test image to dotcms/dotcms-test

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

Content Drive: replace Content Search and Site Browser (legacy editor in the side panel, route redirects)

3 participants