dotCMS / dotCMS/core

[Task] Tools: v1 endpoints for section management (`/v1/layouts`)

Open
#37,353 1 comment 0 reactions 1 assignee View on GitHub

@hmoreras is already working on this.

Since Sep 2, 2026.

dotCMS : Admin Tools Team : Modernization Type : Task
Dominant language
Java
Stars
970
Forks
486
Avg merge
3d 33m
Merged PRs (30d)
170

Description

Goal

Add the /v1 endpoints the Tools portlet needs to manage sections and the tools inside them. Section create, update, delete, reorder, the ordered set of tools in a section, and the available-tools catalog exist today only as RoleAjax DWR methods, which an Angular portlet cannot call.

This issue carries the section side: a new LayoutResource under /v1/layouts. The portlet side — PortletID.TOOLS, the tools catalog, the custom-tool read and the custom-tool gate change — is #37574, which this issue depends on for the TOOLS portlet id. The two touch disjoint files and can be reviewed in parallel.

Endpoints

The exact request and response shapes are pinned by the spike. The set below is what the portlet consumes.

Sections

Verb Path Purpose
GET /v1/layouts Every section in navigation order, each with its id, name, icon, position, its ordered tool ids, and localized tool titles. Every write below returns this same list (or the single saved section for POST / PUT /{layoutId}) so the client can replace its state from the response instead of patching locally
POST /v1/layouts Create a section from a name and an icon; the server assigns the last position
PUT /v1/layouts/{layoutId} Rename a section and change its icon
DELETE /v1/layouts/{layoutId} Delete a section
PUT /v1/layouts/_reorder Rewrite the position of every section from a full ordered list of ids. The list must contain every existing section id exactly once; an unknown, missing or duplicated id is a 400 and nothing is written
PUT /v1/layouts/{layoutId}/portlets Replace the ordered set of tools in one section from a full ordered list of tool ids — this covers adding, removing and reordering in a single call. Every id must name an existing portlet that PortletAPI.canAddPortletToLayout accepts (the same rule the legacy _addtolayout endpoint enforces) and may appear once; otherwise 400 and nothing is written

Field mapping

Wire field Backing column
icon Layout.description
tabOrder Layout.tabOrder
ordered tool ids row order of cms_layouts_portlets, written through LayoutAPI.setPortletIdsToLayout

LayoutAPI.setPortletIdsToLayout persists the tool order: LayoutFactoryImpl.setPortletsToLayout writes portlet_order 1..n and populatePortlets reads order by portlet_order, so the read path returns the order it was given. Cover it with an integration test, since the tool order inside a section is a new user-facing capability that depends on it.

Behaviour to preserve

  • Every write pushes SystemEventType.UPDATE_PORTLET_LAYOUTS through SystemEventsAPI.pushAsync, as the RoleAjax layout methods do. Without it, open admin sessions keep a stale navigation until they reload.
  • LayoutAPI.saveLayout throws LayoutNameAlreadyExistsException on a duplicate name. Map it to a 400 with a message the UI can display, rather than letting it surface as a 500.
  • Getting Started (LayoutAPI.GETTING_STARTED_LAYOUT_ID, tab order -320000) is returned by findAllLayouts like any other section, so it will appear in the portlet. Rename, icon change and reorder are allowed; DELETE /v1/layouts/{layoutId} returns 400 for that id.
  • Deleting a section detaches it from the roles and users that hold it. The spike settles what that means for their menus; the endpoint must not leave dangling layouts_cms_roles rows.

Gating

  • PortletID.TOOLS is added by #37574; this resource gates on it.
  • Reads (GET /v1/layouts) initialise through WebResource.InitBuilder with requiredBackendUser(true), rejectWhenNoUser(true) and requiredPortlet("tools", "tools-beta").
  • Writes (POST, both PUTs, DELETE, _reorder) require the same portlet gate and the CMS Administrator role, checked after init() exactly as RoleResource does for POST / DELETE /v1/roles/layouts: a non-admin is logged through SecurityLogger ("unauthorized attempt … by user … from …") and rejected with DotSecurityException. Decision (2026-09-17): sections are the permission carrier — LayoutAPI.doesUserHaveAccessToPortlet grants a portlet to whoever holds a section containing it — so changing a section changes what roles can reach. The modern REST precedent for that class of write (granting sections to roles or users) is already admin-only, and Tools follows it. Non-admin Tools holders get a read-only view of the navigation.
  • Every successful write logs the acting user id, the operation and the section id through SecurityLogger.logInfo, so navigation changes are auditable. Both ids are needed: LayoutAPI.doesUserHaveAccessToPortlet checks exact membership of the id in the user's layouts, and the portlet admins add during Beta is tools-beta, so gating on tools alone would let only CMS Admins through. requiredPortlet is varargs and passes on any match. A backend user with neither gets a 401 (WebResource.checkPortletPermissions throws SecurityException with UNAUTHORIZED, as every gated resource does).
  • GET /v1/roles/layouts is already gated on the roles portlet (#37259, PR #37323). Nothing to do here; GET /v1/layouts is its tools-gated replacement for this portlet.
  • The custom-tool endpoints and their gate change are #37574.

Untouched files

  • com.dotmarketing.business.ajax.RoleAjax — the DWR methods stay so the Dojo screen keeps working.
  • dotCMS/src/main/webapp/html/portlet/ext/roleadmin/* — no JSP changes.
  • PortletResource — the catalog, custom-tool read and gate change are #37574.

Inputs

  • The spike's endpoint contract and destructive-action findings
  • dotCMS/src/main/java/com/dotmarketing/business/ajax/RoleAjax.java — the behaviour being lifted onto REST
  • dotCMS/src/main/java/com/dotmarketing/business/LayoutAPI.java, com/dotmarketing/business/portal/PortletAPI.java
  • dotCMS/src/main/java/com/dotcms/rest/api/v1/portlet/ToolGroupResource.java for the resource and gating conventions
  • dotCMS/src/main/java/com/dotmarketing/business/LayoutFactoryImpl.javasetPortletsToLayout, populatePortlets, removeLayout
  • Frontend contract: libs/portlets/dot-tools/src/lib/models/dot-tools.models.ts on PR #37481 — DotToolsSection { id, name, icon, tabOrder, portletIds }
  • dotCMS/src/main/java/com/dotcms/rest/api/v1/system/role/RoleResource.java — the existing layout endpoints

QA

  • GET /v1/layouts returns every section with its icon, position, ordered tool ids and localized tool titles, and the order matches the admin navigation.
  • POST /v1/layouts creates a section that appears last in the navigation; a duplicate name returns a 400 with a readable message.
  • PUT /v1/layouts/{id} changes name and icon and nothing else.
  • PUT /v1/layouts/_reorder rewrites the navigation order; reloading the admin reflects it.
  • PUT /v1/layouts/{id}/portlets adds, removes and reorders the tools in one section, and a subsequent GET returns them in the order sent.
  • DELETE /v1/layouts/{id} removes the section, detaches it from every role and user that held it, and leaves no orphan rows.
  • Every endpoint returns 401 for an unauthenticated caller and for a backend user with neither tools nor tools-beta in their layouts.
  • A non-admin user with only tools-beta can call GET /v1/layouts and is rejected on every write; a CMS Administrator with tools-beta passes every write; a CMS Administrator without tools-beta in any layout also passes (admin fallback).
  • PUT /v1/layouts/{id}/portlets returns 400 for an unknown id, a non-placeable id and a duplicated id, and leaves the section unchanged.
  • PUT /v1/layouts/_reorder returns 400 when an id is unknown, missing or duplicated, and leaves every position unchanged.
  • Every rejected write and every successful write appears in the security log with the acting user.
  • DELETE /v1/layouts/{id} on the Getting Started layout returns 400 and leaves it intact.
  • A write from one session updates the navigation of another open session without a hard reload.
  • @Schema on each response matches the actual return type, and the regenerated openapi.yaml is committed with the change.

Definition of Done

  • Every endpoint above is implemented, gated, and documented through @Operation / @Parameter annotations.
  • openapi.yaml is regenerated with ./mvnw compile -pl :dotcms-core --am -DskipTests and committed alongside the Java changes.
  • Integration tests cover the happy path, the duplicate-name rejection, the ordering guarantees, the delete detach behaviour, and the gating on each endpoint, and the new test classes are registered in a MainSuite so CI runs them (PortletResource has no integration tests today).
  • The DWR methods and the Dojo screen still work unchanged.

Refs #37351

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.