[Task] Tools: v1 endpoints for section management (`/v1/layouts`)
@hmoreras is already working on this.
Since Sep 2, 2026.
- 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_LAYOUTSthroughSystemEventsAPI.pushAsync, as theRoleAjaxlayout methods do. Without it, open admin sessions keep a stale navigation until they reload. LayoutAPI.saveLayoutthrowsLayoutNameAlreadyExistsExceptionon 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 byfindAllLayoutslike 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_rolesrows.
Gating
PortletID.TOOLSis added by #37574; this resource gates on it.- Reads (
GET /v1/layouts) initialise throughWebResource.InitBuilderwithrequiredBackendUser(true),rejectWhenNoUser(true)andrequiredPortlet("tools", "tools-beta"). - Writes (
POST, bothPUTs,DELETE,_reorder) require the same portlet gate and the CMS Administrator role, checked afterinit()exactly asRoleResourcedoes forPOST/DELETE /v1/roles/layouts: a non-admin is logged throughSecurityLogger("unauthorized attempt … by user … from …") and rejected withDotSecurityException. Decision (2026-09-17): sections are the permission carrier —LayoutAPI.doesUserHaveAccessToPortletgrants 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.doesUserHaveAccessToPortletchecks exact membership of the id in the user's layouts, and the portlet admins add during Beta istools-beta, so gating ontoolsalone would let only CMS Admins through.requiredPortletis varargs and passes on any match. A backend user with neither gets a 401 (WebResource.checkPortletPermissionsthrowsSecurityExceptionwithUNAUTHORIZED, as every gated resource does). GET /v1/roles/layoutsis already gated on the roles portlet (#37259, PR #37323). Nothing to do here;GET /v1/layoutsis 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 RESTdotCMS/src/main/java/com/dotmarketing/business/LayoutAPI.java,com/dotmarketing/business/portal/PortletAPI.javadotCMS/src/main/java/com/dotcms/rest/api/v1/portlet/ToolGroupResource.javafor the resource and gating conventionsdotCMS/src/main/java/com/dotmarketing/business/LayoutFactoryImpl.java—setPortletsToLayout,populatePortlets,removeLayout- Frontend contract:
libs/portlets/dot-tools/src/lib/models/dot-tools.models.tson 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/layoutsreturns every section with its icon, position, ordered tool ids and localized tool titles, and the order matches the admin navigation. -
POST /v1/layoutscreates 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/_reorderrewrites the navigation order; reloading the admin reflects it. -
PUT /v1/layouts/{id}/portletsadds, removes and reorders the tools in one section, and a subsequentGETreturns 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
toolsnortools-betain their layouts. - A non-admin user with only
tools-betacan callGET /v1/layoutsand is rejected on every write; a CMS Administrator withtools-betapasses every write; a CMS Administrator withouttools-betain any layout also passes (admin fallback). -
PUT /v1/layouts/{id}/portletsreturns 400 for an unknown id, a non-placeable id and a duplicated id, and leaves the section unchanged. -
PUT /v1/layouts/_reorderreturns 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.
-
@Schemaon each response matches the actual return type, and the regeneratedopenapi.yamlis committed with the change.
Definition of Done
- Every endpoint above is implemented, gated, and documented through
@Operation/@Parameterannotations. openapi.yamlis regenerated with./mvnw compile -pl :dotcms-core --am -DskipTestsand 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
MainSuiteso CI runs them (PortletResourcehas no integration tests today). - The DWR methods and the Dojo screen still work unchanged.
Refs #37351
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.