Folders & Rendering
A folder in DMART is deceptively simple — and quietly one of the most powerful concepts in the platform. It is at once a structural node (the thing that makes a subpath browsable) and a declarative control panel that tells the admin UI exactly how to list, sort, filter, paginate and edit everything inside it, and tells the backend exactly what is allowed to live there. All of that lives in one small JSON body validated against a schema called folder_rendering. This page dissects every property.
1. A folder is just an entry
There is no special Folder class with dozens of fields. A folder is an entry like any other, with resource_type = folder. It inherits the exact same Meta as a user or a blog post — uuid, shortname, ownership, tags, timestamps. It adds no fields of its own.
All of a folder's "rendering" power lives entirely in its payload body, a JSON object whose schema_shortname is folder_rendering. That body is the subject of this whole page.
graph TD
F["Folder entry<br/>(resource_type = folder)"] --> M["Meta<br/>uuid, shortname, owner, tags…"]
F --> P["Payload"]
P --> SS["schema_shortname:<br/>folder_rendering"]
P --> B["body = the rendering config<br/>index_attributes, allow_*, query, icons…"]
B --> UI["Admin UI reads it → renders the listing"]
B --> BE["Backend reads it → enforces what may be created here"]
The folder does not store its children. It stores the rules and the view for its children. The children are ordinary entries that happen to share its subpath.
2. Anatomy: where the config actually lives
At runtime a folder is a single row in PostgreSQL like any entry, but when a space is exported to disk (DMART's import / export & seed format) the split between a folder's meta and its body becomes physically visible. The meta sits inside the subpath's .dm directory; the body is a sibling JSON file named after the folder, one level up.
spaces/management/
├── schema.json ← the BODY (the folder_rendering config)
└── schema/ ← the /schema subpath
└── .dm/
└── meta.folder.json ← the META (points at ../schema.json as body)
The meta simply references the body file and names the schema:
// management/schema/.dm/meta.folder.json
{
"uuid": "b9b2b289-7f7c-4d40-a5a5-e896a9c146b4",
"shortname": "schema",
"is_active": true,
"owner_shortname": "dmart",
"payload": {
"content_type": "json",
"schema_shortname": "folder_rendering", // ← validated against THE schema
"body": "schema.json" // ← the sibling file with the config
}
}
3. The folder_rendering schema — centralized & exact
Every folder in every space is validated against a single canonical schema that lives in the management space at management/schema/folder_rendering. Two design decisions make it strict and safe:
Centralized The schema validator special-cases the shortname folder_rendering and always resolves it from the management space — so one definition governs all spaces, and you never copy it per space.
Exact additionalProperties: false at every level. An unknown or misspelled field is rejected, not silently ignored — you find the typo immediately instead of debugging a prop that "does nothing".
Minimal required The only required property is index_attributes. A folder with nothing but one index attribute is valid; everything else is optional and defaulted.
Because validation is exact, the props below are the complete vocabulary. There is no hidden columns, render_type or list_columns — those don't exist here. What you see is what a folder can declare.
4. Props — display & columns
These control what the listing (the "index page") looks like: which columns appear, their headers, the folder's icons, and the label for the shortname column.
| Property | Type | What it does |
|---|---|---|
index_attributes (required) |
[{key, name}] |
The star of the show. Each item is a column: key is the entry field/attribute to read, name is the header text shown. Order = column order. Must be unique. See §8. |
shortname_title |
string |
The header label used for the shortname column (e.g. "Permission Shortname", "API Name") — lets a technical shortname read as a friendly noun. |
search_columns |
[{key, name}] |
Which columns appear in the search/results view when searching within the folder. Same {key, name} shape as index_attributes. |
csv_columns |
[{key, name}] |
Which columns (and their headers) are written when the list is exported to CSV. |
icon |
string |
The folder's main icon, shown next to it in the tree/sidebar. |
icon_opened |
string |
Icon shown when the folder is expanded. |
icon_closed |
string |
Icon shown when the folder is collapsed. |
enable_pdf_schema_shortnames |
string[] |
For entries using one of these schemas, the UI shows a "download as PDF" icon. |
5. Props — what may live here (server-enforced)
These four are not just UI hints — the backend enforces them whenever a child is created, updated, or moved into the folder. An empty (or absent) array means "no restriction". See §9 for the enforcement details.
| Property | Type | What it restricts |
|---|---|---|
content_resource_types |
string[] (enum) |
A child's resource_type must be one of these. Values are drawn from the full resource-type enum (content, ticket, folder, media, …). |
content_schema_shortnames |
string[] |
A child that declares a payload.schema_shortname must use one of these. Also drives which form the UI loads (see §8). |
workflow_shortnames |
string[] |
A ticket created here that declares a workflow_shortname must use one of these. |
unique_fields |
string[][] |
A list of composite uniqueness constraints. Each inner list is a set of fields that together must be unique across all entries in the folder — e.g. [["email"], ["first_name","last_name"]]. |
6. Props — action toggles
Booleans that show or hide the corresponding buttons/affordances in the admin UI for this folder. They shape what an operator can do from the listing.
| Property | Default | Effect |
|---|---|---|
allow_view |
true |
Allow opening/viewing a resource inside the folder. |
allow_create |
true |
Show the "create resource" button (gates the create form). |
allow_create_category |
false |
Allow creating a sub-folder (a "category") inside this folder. |
allow_update |
true |
Allow editing existing resources. |
allow_delete |
false |
Allow deleting resources. |
allow_csv |
false |
Show the CSV download button (export the displayed list). |
allow_upload_csv |
false |
Show the CSV upload feature (batch-create from a CSV). |
use_media |
false |
Declares that entries here carry a media attachment (surfaces media affordances). |
These are UI gates for a smooth operator experience — they are not a security boundary. Real authorization is always enforced by RBAC + ACLs on the server regardless of what a folder's toggles say.
7. Props — query, sort, filter & behavior
These decide which entries the listing loads, in what order, and how it behaves — including the powerful ability to back a folder by a search instead of by its own subpath.
| Property | Type | What it does |
|---|---|---|
query |
{type, search, filter_types} |
type is "subpath" (list the folder's own contents) or "search" (list the results of a saved search query instead). search is the query string; filter_types narrows resource types. |
append_subpath |
string |
A string appended to the query subpath — lets a folder point its listing at a nested path below itself. |
sort_by |
string |
The field name used to order the listing (e.g. created_at, shortname). |
sort_type |
"ascending" \| "descending" |
The direction of the sort. |
filter |
object[] |
Additional filter options offered above the listing. |
disable_filter |
boolean |
Hides the search/filter icon entirely for this folder. |
expand_children |
boolean |
Whether the folder auto-expands its children in the tree. |
stream |
boolean |
Enables a folder-level websocket watch — the listing updates live as entries change. |
8. index_attributes in depth
This is the one required property and the heart of folder rendering. It is an ordered array of column descriptors, each a two-field object:
"index_attributes": [
{ "key": "shortname", "name": "API Name" }, // column 1
{ "key": "subpath", "name": "Sub path" }, // column 2
{ "key": "end_point", "name": "End point" }, // column 3 (a payload.body field)
{ "key": "verb", "name": "Verb" } // column 4 (a payload.body field)
]
key Which value to read from each entry. It can be a Meta field (shortname, created_at, owner_shortname) or a field inside the entry's payload.body (end_point, verb).
name The human-friendly column header rendered in the table. Purely presentational.
Fallback columns
If a folder somehow has no usable index attributes, the admin UI falls back to a sensible default set so the listing is never blank:
[
{ "key": "status", "name": "Status" },
{ "key": "created_at", "name": "Created At" },
{ "key": "updated_at", "name": "Updated At" },
{ "key": "author", "name": "Author" }
]
Editing columns live
Operators don't hand-edit JSON. The admin UI exposes a column-settings modal that adds, removes and reorders index_attributes visually, then writes them back into payload.body.index_attributes via a folder update request. The folder config is data you edit through the same CRUD API as everything else.
9. How the admin UI renders a folder
Putting the props together, here is the exact pipeline the admin SPA (CXB / Catalog) runs when you open a folder:
sequenceDiagram
participant UI as Admin UI
participant API as DMART API
UI->>API: GET folder entry (meta + payload.body)
API-->>UI: folder_rendering config
UI->>UI: applyFolderContentDefaults() fills missing props
UI->>UI: build columns from index_attributes (or fallback)
UI->>API: query (type=subpath/search, sort_by, sort_type, filter_types)
API-->>UI: page of entries
UI->>UI: render table — one column per index_attribute
UI->>UI: toolbar buttons gated by allow_create / allow_csv / allow_upload_csv …
Note over UI: click "Create" → load form from content_schema_shortnames
Load the folder entry and read
payload.body.Default every missing property (see §10) — crucially with
??, so an explicitfalsefrom the server is preserved, not overwritten by a truthy default.Columns from
index_attributes, or the fallback set.Fetch entries:
query.typedecides subpath-listing vs search;sort_by/sort_typeorder them (defaultcreated_at/descending).Toolbar: the create button appears only if
allow_create; CSV download only ifallow_csv; CSV upload only ifallow_upload_csv; and so on.Create form: driven by
content_schema_shortnames— zero schemas → a free-form entry; one → that schema's form is auto-loaded and rendered; many → the user picks which schema first.
10. Content policy is enforced on the server
The three content arrays plus uniqueness are not merely UI conveniences. On every create / update / move, the backend reads the parent folder's config and validates the child against it:
content_resource_types— the child's resource type must be allowed.content_schema_shortnames— a schema-declaring child must use an allowed schema.workflow_shortnames— a ticket's workflow must be allowed.
Semantics that matter in practice:
Empty = open An empty or absent array imposes no restriction. Restrictions are opt-in.
Fails open If the parent folder can't be read, enforcement is skipped rather than blocking the write.
Toggleable Global enforcement is controlled by ENFORCE_FOLDER_CONTENT_POLICY (default true).
11. Default values
When a property is absent, the admin UI applies these defaults (using ?? so a real false/empty value is never clobbered):
| Group | Default |
|---|---|
allow_view, allow_create, allow_update |
true |
allow_delete, allow_create_category, allow_csv, allow_upload_csv, use_media, stream, expand_children, disable_filter |
false |
index_attributes, content_schema_shortnames, content_resource_types, *_columns, workflow_shortnames |
[] |
query |
{ type: "", search: "", filter_types: [] } |
icon, icon_opened, icon_closed, shortname_title |
"" |
12. Worked examples (real seed folders)
a. Minimal, read-only listing — management/permissions
One column, everything locked down. A pure browse view.
{
"shortname_title": "Permission Shortname",
"content_schema_shortnames": [],
"index_attributes": [
{ "key": "shortname", "name": "Permission Shortname" }
],
"allow_create": false,
"allow_update": false,
"allow_delete": false,
"use_media": false,
"filter": [],
"allow_view": true
}
b. Rich, editable listing — applications/api
Four columns (two of them payload fields), full CRUD, CSV export, auto-expanding tree, constrained to one schema and the content type.
{
"shortname_title": "API Name",
"content_schema_shortnames": ["api"],
"content_resource_types": ["content"],
"index_attributes": [
{ "key": "shortname", "name": "name" },
{ "key": "subpath", "name": "Sub path" },
{ "key": "end_point", "name": "End point" },
{ "key": "verb", "name": "Verb" }
],
"expand_children": true,
"allow_create": true,
"allow_update": true,
"allow_delete": true,
"allow_csv": true,
"use_media": false,
"allow_view": true
}
c. Distinct search view — management/notifications
Two schemas allowed, and a bespoke search_columns set that differs from the index columns.
{
"shortname_title": "ID",
"content_schema_shortnames": ["system_notification_request", "admin_notification_request"],
"content_resource_types": ["content"],
"index_attributes": [
{ "key": "shortname", "name": "Shortname" },
{ "key": "on_subpath", "name": "On Subpath" },
{ "key": "on_action", "name": "On Action" },
{ "key": "on_state", "name": "On State" },
{ "key": "priority", "name": "Priority" }
],
"search_columns": [
{ "key": "shortname", "name": "username" },
{ "key": "on_subpath", "name": "On Subpath" }
],
"allow_create": true,
"allow_update": true,
"allow_delete": true,
"allow_view": false
}
d. Query-backed folder — management/health_check
Instead of listing its own subpath, this folder lists the results of a search query, sorted by creation time.
{
"index_attributes": [ { "key": "shortname", "name": "name" } ],
"sort_by": "created_at",
"sort_type": "ascending",
"query": { "type": "search", "search": "" },
"content_schema_shortnames": [],
"content_resource_types": [],
"workflow_shortnames": [],
"enable_pdf_schema_shortnames": [],
"allow_create": false,
"allow_view": false
}
13. Validation, strictness & the auto-fixer
Because the schema is exact, a folder body is rejected at write time (and at seed/startup) if it contains an unknown field or misses index_attributes. To repair legacy folders that predate the tightened schema, DMART ships a CLI:
dmart fix-folder-rendering [space] [--apply]
Strips fields that aren't in the canonical schema (kills typos and dead props).
Fills a required-but-missing
index_attributeswith[].Widens non-empty content-policy arrays so they still cover the folder's existing children (won't retroactively orphan data).
Reports invalid
content_resource_typesenum values without silently deleting them.
Run it without --apply for a dry-run report; add --apply to write the fixes. See the CLI reference.