Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
2b3690e
docs(api): enhance ContentType and NavResource descriptions for clarity
rjvelazco Apr 20, 2026
955e90a
docs(api): enhance workflow action descriptions with detailed usage g…
rjvelazco Apr 20, 2026
4f2e5b5
docs(api): feedback v1 — fix PR review findings in OpenAPI descriptions
rjvelazco Apr 20, 2026
556df46
Merge branch 'main' into issue-35381-improve-openapi-endpoint-descrip…
rjvelazco Apr 20, 2026
380286e
doc(api): update v1
rjvelazco Apr 21, 2026
d6be0da
Merge branch 'main' into issue-35381-improve-openapi-endpoint-descrip…
rjvelazco Apr 21, 2026
f49da37
docs(api): enhance OpenAPI descriptions for content type endpoints
rjvelazco Apr 21, 2026
027bd31
Merge branch 'main' into issue-35381-improve-openapi-endpoint-descrip…
rjvelazco Apr 21, 2026
084df9a
docs(claude): update CLAUDE.md with project structure, environment pr…
rjvelazco Apr 21, 2026
2ee8e4b
Merge branch 'main' into issue-35381-improve-openapi-endpoint-descrip…
rjvelazco Apr 21, 2026
6b161ee
Merge branch 'main' into issue-35381-improve-openapi-endpoint-descrip…
rjvelazco Apr 22, 2026
fe606f2
Merge branch 'main' into issue-35381-improve-openapi-endpoint-descrip…
rjvelazco May 12, 2026
ad45813
Enhance OpenAPI documentation for content type and workflow actions
rjvelazco May 12, 2026
a912b13
Merge branch 'main' into issue-35381-improve-openapi-endpoint-descrip…
rjvelazco May 12, 2026
23c33f5
fix(MultiPartUtils): prevent NPE by handling null file parts in multi…
rjvelazco May 12, 2026
8244b87
Merge branch 'issue-35381-improve-openapi-endpoint-descriptions-based…
rjvelazco May 12, 2026
fed017f
Merge branch 'main' into issue-35381-improve-openapi-endpoint-descrip…
rjvelazco May 12, 2026
1f2ded7
Refactor OpenAPI documentation for temporary file responses and workf…
rjvelazco May 12, 2026
c41d636
Merge branch 'issue-35381-improve-openapi-endpoint-descriptions-based…
rjvelazco May 12, 2026
b58bc62
Merge branch 'main' into issue-35381-improve-openapi-endpoint-descrip…
rjvelazco May 12, 2026
2d978eb
Merge branch 'main' into issue-35381-improve-openapi-endpoint-descrip…
rjvelazco May 12, 2026
e515415
Enhance TempFilesView to ensure immutability and handle null input gr…
rjvelazco May 12, 2026
133f59a
Merge branch 'issue-35381-improve-openapi-endpoint-descriptions-based…
rjvelazco May 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 27 additions & 7 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
# dotCMS Development Guide
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Structure

```
core/
├── dotCMS/ # Main backend Java code
│ └── src/main/java/com/ # Java source files
│ └── src/main/java/com/
│ ├── dotcms/ # Modern domain-driven packages (prefer these)
│ └── dotmarketing/ # Legacy packages (15+ yr old code, still active)
├── core-web/ # Frontend (Angular/Nx monorepo) → see core-web/CLAUDE.md
├── dotcms-integration/ # Integration tests
├── dotcms-postman/ # Postman API tests
Expand All @@ -14,13 +18,21 @@ core/
└── .github/workflows/ # CI/CD pipelines
```

## Environment Prerequisites

```bash
sdk env install # Java 21 via SDKMAN (.sdkmanrc) — build fails with wrong version
nvm use # Node 22.15+ via nvm (.nvmrc) — frontend build fails with wrong version
```

## Build & Test Commands

```bash
# Build (choose based on scope)
./mvnw install -pl :dotcms-core -DskipTests # Simple core changes (~2-3 min)
./mvnw install -pl :dotcms-core --am -DskipTests # Core + dependencies (~3-5 min)
./mvnw clean install -DskipTests # Full rebuild (~8-15 min)
./mvnw install -pl :dotcms-core --am -DskipTests # Core + in-project deps (~2-3 min) ✅
./mvnw install -pl :dotcms-core -DskipTests # ⚠️ Can fail: missing in-project deps
./mvnw clean install -DskipTests # Full rebuild (~8-15 min)
./mvnw clean install -DskipTests -Ddocker.skip # Full rebuild, skip Docker image

# Test (⚠️ NEVER run full integration suite — 60+ min)
./mvnw verify -pl :dotcms-integration -Dcoreit.test.skip=false -Dit.test=MyTestClass # Specific class
Expand All @@ -32,8 +44,8 @@ just test-integration-ide # Start PostgreSQL + Elasticsearch + dotCMS
just test-integration-stop # Stop services when done

# Run
just dev-run # Start dotCMS in Docker with Glowroot
cd core-web && nx run dotcms-ui:serve # Frontend dev server only
just dev-run # Start dotCMS in Docker with Glowroot
cd core-web && yarn nx serve dotcms-ui # Frontend dev server only (use yarn nx, not nx)
```

> All test modules need explicit `skip=false` flags or tests are silently skipped.
Expand All @@ -56,6 +68,14 @@ UserAPI userAPI = APILocator.getUserAPI(); // Service access pattern
- **REST @Schema**: Must match actual return type — see [REST API Guide](dotCMS/src/main/java/com/dotcms/rest/CLAUDE.md)
- **Frontend**: See [core-web/CLAUDE.md](core-web/CLAUDE.md) for Angular/TypeScript standards

### OpenAPI / Swagger

`openapi.yaml` is **auto-generated** by `swagger-maven-plugin` at compile phase — it writes directly to `src/main/webapp/WEB-INF/openapi/openapi.yaml`. The CI verifies the committed file matches what the build produces.

- All description changes must go in Java `@Operation` / `@Parameter` annotations, not in the yaml directly
- Regenerate after annotation changes: `./mvnw compile -pl :dotcms-core -DskipTests` (no Docker needed)
- Commit the regenerated yaml alongside the Java changes

### Progressive Enhancement

When editing ANY code, improve incrementally:
Expand Down
7 changes: 6 additions & 1 deletion dotCMS/src/main/java/com/dotcms/rest/api/MultiPartUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,12 @@ public List<File> getBinariesFromMultipart(final FormDataMultiPart multipart) th

final List<File> binaries = new ArrayList<>();

for (final FormDataBodyPart part : multipart.getFields(FILE)) {
final List<FormDataBodyPart> fileParts = multipart.getFields(FILE);
if (fileParts == null) {
return binaries;
}

for (final FormDataBodyPart part : fileParts) {

final File tmpFolder = new File(
this.fileAssetAPI.getRealAssetPathTmpBinary() + UUIDUtil.uuid());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -256,11 +256,13 @@ public final Response copyType(@Context final HttpServletRequest req,
"| Property | Type | Description |\n" +
"|----------|--------|-------------|\n" +
"| `name` | String | **Required.** Name of new content type |\n" +
"| `variable` | String | System variable of new content type |\n" +
"| `variable` | String | Velocity variable name of the new content type |\n" +
"| `folder` | String | Folder in which new content type will live |\n" +
"| `host` | String | Site or host to which the new content type will belong |\n" +
"| `icon` | String | System icon to represent content type |\n\n" +
"Values not specified default to values of the original content type.",
"The copy preserves: `description`, `host`, `folder`, full `fields[]` (with new field IDs), " +
"layout (Row/Column markers), `metadata`, and workflow assignments. " +
"Unspecified values default to those of the original content type.",
required = true,
content = @Content(
schema = @Schema(implementation = CopyContentTypeForm.class),
Expand Down Expand Up @@ -484,16 +486,42 @@ private ImmutableMap<Object, Object> copyContentTypeAndDependencies(final Conten
public final Response createType(@Context final HttpServletRequest req,
@Context final HttpServletResponse res,
@RequestBody(
description = "Payload may consist of a single content type JSON object, or a list " +
"containing multiple content type objects.\n\n" +
"Objects require `clazz` and `name` properties at minimum.\n\n" +
"May optionally include the following special properties:\n\n" +
"| Property | Value | Description |\n" +
"|-|-|-|\n" +
"| `systemActionMappings` | JSON Object | Maps " +
"[Default Workflow Actions](https://www.dotcms.com/docs/latest/managing-workflows#DefaultActions) (as keys) " +
"to workflow action identifiers (as values) for this content type.|\n" +
"| `workflow` | List of Strings | A list of identifiers of workflow schemes to be associated with the content type.",
description = "Accepts either a single content-type object or an array. " +
"The body is the content-type object directly (not wrapped in a 'contentType' envelope).\n\n" +
"**Required properties:**\n" +
"- `clazz` *(string)* — fully-qualified class name. One of: " +
"`com.dotcms.contenttype.model.type.ImmutableSimpleContentType`, " +
"`com.dotcms.contenttype.model.type.ImmutableWidgetContentType`, " +
"`com.dotcms.contenttype.model.type.ImmutableFormContentType`, " +
"`com.dotcms.contenttype.model.type.ImmutableFileAssetContentType`, " +
"`com.dotcms.contenttype.model.type.ImmutablePageContentType`, " +
"`com.dotcms.contenttype.model.type.ImmutablePersonaContentType`, " +
"`com.dotcms.contenttype.model.type.ImmutableVanityUrlContentType`, " +
"`com.dotcms.contenttype.model.type.ImmutableKeyValueContentType`, " +
"`com.dotcms.contenttype.model.type.ImmutableDotAssetContentType`\n" +
"- `name` *(string)* — display name\n\n" +
"**Common optional properties:**\n" +
"- `variable` *(string)* — Velocity variable name (unique, alphanumeric, starts with a letter; auto-generated if omitted)\n" +
"- `host` *(string)* — site identifier UUID or the literal `SYSTEM_HOST` (defaults to the default site)\n" +
"- `folder` *(string)* — folder identifier UUID or the literal `SYSTEM_FOLDER` (defaults to `SYSTEM_FOLDER`)\n" +
"- `description` *(string)*\n" +
"- `workflow` *(array of workflow scheme UUIDs)* — e.g. `[\"d61a59e1-a49c-46f2-a929-db2b4bfa88b2\"]` for System Workflow. " +
"⚠️ **Note:** this is `workflow` (singular) in the request. GET responses return `workflows` (plural, array of objects) — " +
"clients round-tripping an object must rename this key.\n" +
"- `fields` *(array of field objects)* — see field schema below\n" +
"- `metadata` *(object)* — known keys: `CONTENT_EDITOR2_ENABLED` (boolean), `DOT_STYLE_EDITOR_SCHEMA` (JSON string)\n" +
"- `systemActionMappings` *(object)* — maps system actions (`NEW`, `EDIT`, `PUBLISH`, `UNPUBLISH`, `ARCHIVE`, `UNARCHIVE`, `DELETE`, `DESTROY`) to workflow action UUIDs\n\n" +
"**Field object schema** (each item in `fields[]`):\n" +
"- `clazz` *(string, required)* — e.g. `com.dotcms.contenttype.model.field.ImmutableTextField`, `ImmutableTextAreaField`, " +
"`ImmutableStoryBlockField`, `ImmutableBinaryField`, `ImmutableTagField`, `ImmutableRadioField`, `ImmutableSelectField`, " +
"`ImmutableDateField`, `ImmutableDateTimeField`, `ImmutableRowField` *(layout marker)*, `ImmutableColumnField` *(layout marker)*\n" +
"- `name`, `variable`, `dataType` (one of `TEXT`, `LONG_TEXT`, `SYSTEM`, `BOOL`, `INTEGER`, `FLOAT`, `DATE`), " +
"`required`, `indexed`, `listed`, `sortOrder` *(integer, position in the fields array)*\n" +
"- `values` *(string)* — for Radio/Select/Checkbox: newline-separated `Display|value` pairs. " +
"For a boolean field use `ImmutableRadioField` + `dataType: BOOL` + `values: 'True|true\\r\\nFalse|false'` — there is no dedicated Boolean field class.\n\n" +
"**Layout encoding:** Rows and columns are regular field entries placed in `fields[]`. " +
"`ImmutableRowField` begins a new row; `ImmutableColumnField` begins a new column inside that row; " +
"following content fields belong to the most-recent column until the next marker.",
required = true,
content = @Content(
schema = @Schema(implementation = ContentTypeForm.class),
Expand Down Expand Up @@ -627,10 +655,19 @@ public final Response createType(@Context final HttpServletRequest req,
operationId = "putContentTypeUpdate",
summary = "Updates a content type",
description = "Updates the content type based on the given ID or Velocity variable name.\n\n" +
"Returns a copy of the updated content type object.\n\n" +
"> **Caution:** When updating a content type, any editable fields omitted from the request body " +
"will be removed from the content type. To update selected properties without deleting others," +
"submit the full JSON entity with the desired items edited.",
"⚠️ **Destructive semantics.** This endpoint treats the request body as the full desired state. " +
"Any editable property (including items in `fields[]` and `metadata` keys) absent from the body will be removed.\n\n" +
"**Recommended update pattern:**\n" +
"1. `GET /api/v1/contenttype/id/{idOrVar}` to fetch the current object.\n" +
"2. Mutate the returned object in place. Rename `workflows` (array of objects) → `workflow` (array of UUIDs) before sending.\n" +
"3. PUT the entire mutated object back.\n\n" +
"This is also the only supported way to add, remove, or modify individual fields — " +
"the `/api/v1/contenttype/{typeId}/fields/**` family is deprecated in favor of this full-CT PUT.\n\n" +
"⚠️ **`systemActionMappings` is validated on write.** Each entry's `workflowAction.id` must still exist; " +
"if a mapping points to a deleted workflow action, the entire PUT fails with " +
"`\"The workflow action with the id <uuid> does not exists\"`. When round-tripping the object, prune " +
"`systemActionMappings` entries you do not intend to update — or omit the `systemActionMappings` property " +
"entirely if no mapping changes are needed.",
tags = {"Content Type"},
responses = {
@ApiResponse(responseCode = "200", description = "Content type updated successfully",
Expand Down Expand Up @@ -989,7 +1026,9 @@ private void handleUpdateFieldVariables(
operationId = "deleteContentType",
summary = "Deletes a content type",
description = "Deletes the content type based on the provided ID or Velocity variable name.\n\n" +
"Returns JSON string containing the identifier of the deleted content type.",
"⚠️ **Note:** The `entity` field in the response is a **JSON-encoded string**, not an object. " +
"Typed clients must call `JSON.parse(entity)` before use to get `{ \"deleted\": \"<typeId>\" }`. " +
"This differs from other endpoints in the same family where `entity` is a direct object.",
tags = {"Content Type"},
responses = {
@ApiResponse(responseCode = "200", description = "Content type deleted successfully",
Expand Down Expand Up @@ -1056,7 +1095,13 @@ public Response deleteType(@PathParam("idOrVar") @Parameter(
@Operation(
operationId = "getContentTypeIdVar",
summary = "Retrieves a single content type",
description = "Returns one content type based on the provided ID or Velocity variable name.",
description = "Returns one content type based on the provided ID or Velocity variable name.\n\n" +
"The response includes a `fields[]` array describing every field on the type. " +
"Each field has a `clazz` property (e.g. `ImmutableBinaryField`, `ImmutableImageField`, `ImmutableTextField`) " +
"that determines what values the field accepts. This is particularly important for file-like fields: " +
"`ImmutableBinaryField` only accepts a `temp_<id>` from `POST /api/v1/temp`; " +
"`ImmutableImageField` accepts a `temp_<id>` or a dotAsset `identifier`. " +
"Always inspect `fields[].clazz` before attempting to set binary or image field values via the workflow fire endpoint.",
tags = {"Content Type"},
responses = {
@ApiResponse(responseCode = "200", description = "Content type retrieved successfully",
Expand Down Expand Up @@ -1320,7 +1365,9 @@ private Response retrieveContentType(final HttpServletRequest httpRequest,
@Operation(
operationId = "postContentTypeFilter",
summary = "Filters content types",
description = "Returns the list of content type objects that match the specified filter, with optional pagination criteria.",
description = "Returns the list of content type objects that match the specified filter, with optional pagination criteria.\n\n" +
"For simple substring filtering without pagination control, `GET /api/v1/contenttype?filter=<string>` " +
"is equivalent and simpler. Use `_filter` when you need pagination, ordering, and direction together.",
tags = {"Content Type"},
responses = {
@ApiResponse(responseCode = "200", description = "Content types filtered successfully",
Expand Down
21 changes: 15 additions & 6 deletions dotCMS/src/main/java/com/dotcms/rest/api/v1/page/NavResource.java
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,10 @@ protected NavResource(final WebResource webResource) {
@Operation(
operationId = "getNavigationTree",
summary = "Get site navigation tree",
description = "Returns navigation metadata in JSON format for objects marked as 'show on menu'. "
+ "Builds a tree structure starting from the given URI path, up to the specified depth. "
+ "Example: /api/v1/nav/about-us?depth=2&languageId=1 returns the navigation tree under /about-us, 2 levels deep."
description = "Returns the dotCMS site navigation tree starting at the given **folder** URI, up to the specified depth. "
+ "Only objects marked as 'show on menu' are included. "
+ "The URI must resolve to a folder — page URIs (e.g., '/index', '/about-us/team') will return 404. "
+ "Use '/' to fetch the navigation tree for the site root."
)
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "Navigation tree retrieved successfully",
Expand All @@ -107,11 +108,19 @@ protected NavResource(final WebResource webResource) {
@Produces({MediaType.APPLICATION_JSON, "application/javascript"})
@Path("/{uri: .*}")
public final Response loadJson(@Context final HttpServletRequest request, @Context final HttpServletResponse response,
@Parameter(description = "Starting URI path for the navigation tree (e.g., 'about-us')", required = true)
@Parameter(description = "Starting folder URI for the navigation tree (e.g., '/about-us', '/blog', or '/' for the site root). "
+ "Must resolve to a folder — page URIs return 404.", required = true)
@PathParam("uri") final String uri,
@Parameter(description = "Number of levels deep to include in the tree (default: 1)")
@Parameter(description = "Total number of levels to include, counting the starting node as level 1. "
+ "depth=1 returns only the starting node with no children; depth=2 returns the node plus its direct children. "
+ "Values exceeding the actual tree depth return the full subtree. Values less than 1 are treated as 1. (default: 1)",
schema = @Schema(type = "integer", format = "int32"))
@QueryParam("depth") final String depth,
@Parameter(description = "Language ID for the navigation content (defaults to the request language)")
@Parameter(description = "Tags each returned node with this language ID. "
+ "Note: folder names are language-neutral in dotCMS and are not translated — "
+ "this parameter only affects the 'languageId' attribute on each node, not the visible 'title'. "
+ "Defaults to the language of the current request.",
schema = @Schema(type = "integer", format = "int64"))
@QueryParam("languageId") final String languageId) {

final InitDataObject auth = webResource.init(request, response, true);
Expand Down
Loading
Loading