Features
Collection items
Collection items hold the EAV values for a collection. ItemController covers list/create/edit/soft-delete lifecycle; singletons upsert through PUT /collections/{id}/singleton-content. Validation is built by CollectionItemDataRuleBuilder.
Related
Schema & cascade: Collections. Field type catalog: Field types. Model internals: Collections data model. HTTP table: Collections API.
Authorization
All collection/item routes use Spatie permission: middleware (can-show-collections, can-create-collections, can-edit-collections, …). Super-admin bypasses via Gate::before. See Security checklist.
Controller & pages
| Item | Detail |
|---|---|
| Controller | App\Http\Controllers\Collections\ItemController |
| Revisions | App\Http\Controllers\Collections\ItemRevisionController |
| Singleton upsert | ContentCollectionController::upsertSingletonContent |
| Validation | App\Services\Collections\CollectionItemDataRuleBuilder |
| Query / filters | App\Services\Collections\CollectionItemQueryService |
| Pages | resources/js/pages/collections/items/index.tsx, form.tsx, revisions.tsx |
Item CRUD
| Action | Route | Permission |
|---|---|---|
| Index | GET /collections/{collection}/items | can-show-collections |
| Export CSV/JSON | GET /collections/{collection}/items/export?format=csv|json | can-show-collections |
| List columns | PUT /collections/{collection}/list-columns | can-edit-collections |
| Ask AI | Toolbar / row actions | Opens FAB with seeded prompt (AskAiButton, can-use-ai) — see AI chat |
| New form | GET /collections/{collection}/items/new | can-create-collections |
| Store | POST /collections/{collection}/items | can-create-collections |
| Show / edit | GET /collections/{collection}/items/{item} | can-show-collections |
| Update | PUT …/items/{item} | can-edit-collections (PUT only) |
| Bulk | POST …/items/bulk | FormRequest per action: delete | restore | force_delete |
| Soft-delete | DELETE …/items/{item} | can-delete-collections |
| Restore | POST …/items/{item}/restore | can-restore-collections |
| Force-delete | DELETE …/items/{item}/force | can-force-delete-collections |
Admin field-level rules / item_filter (from the role’s Collection access matrix) also apply when the user is not a super-admin — same dialect as the Public CMS API.
Field ACL in the item form
When the signed-in admin’s effective roles include collection field rules, ItemController (and singleton show) pass a fieldGrants map to the form:
| Grant | UI |
|---|---|
read: false (or field absent from the map) | Field hidden |
create: false on new item | Field read-only (No write access) |
update: false on edit | Field read-only |
| Super-admin / Spatie-only (no collection grant rules) | fieldGrants: null — all fields unrestricted in the form |
Server-side write checks (assertWritableFields) still apply on save. See Roles & permissions — Collection access rules.
Advanced list filters
The items index toolbar has:
- Title search — debounced contains on
title(filter[title][_contains]=…) - Filters popover — AND multi-field builder writing the same query dialect as the Public API / GraphQL
| Query | Meaning |
|---|---|
?filter[title]=hello | Contains (LIKE), backward compatible |
?filter[status][_eq]=published | Exact match |
?filter[status][_neq]=draft | Not equal |
?filter[title][_contains]=hello | Explicit contains |
?filter[status][_in]=a,b,c | In list |
?filter[count][_gte]=1 | Numeric / comparable ranges (_gt, _lt, _lte) |
?filter[body][_null]=1 | Empty / missing |
?filter[body][_nnull]=1 | Present |
Multiple fields are AND-combined. Service: CollectionItemQueryService. Clear all resets the URL filters.
Return to list (filters & page)
Edit / New item links from the items index append a return query param with the current list path + query (filters, sort, page).
- The form Items breadcrumb uses
returnwhen it is a same-origin path under/collections/…. - Invalid or off-site
returnvalues are ignored; the breadcrumb falls back to the plain items index. - Drawer-based admin screens (collections / users / groups) do not need
return— closing the drawer keeps the list URL.
Export CSV / JSON
From the items index header, Export downloads the current filtered list (same filter / sort / direction query params, plus role item_filter SQL pushdown) as CSV or JSON.
| Detail | Value |
|---|---|
| Route | GET /collections/{collection}/items/export?format=csv|json |
| Cap | 5 000 rows (CollectionItemExportService::MAX_ROWS) |
| Shared with | AI tool ExportCollection |
Response headers: X-Export-Rows, X-Export-Total-Matched, X-Export-Truncated.
Item History (revisions)
From the item form, History opens:
| Action | Route | Permission |
|---|---|---|
| List / compare | GET …/items/{item}/revisions | can-show-collections |
| Restore | POST …/items/{item}/revisions/{revision}/restore | can-edit-collections |
- Snapshots are stored in
collection_item_revisionson everyCollectionItemValuesWriter::sync(create / update / restore). - UI: select two revisions to diff field JSON; Restore writes the snapshot as a new update (and records another revision).
- Soft-deleted items keep history.
Activity log (/activity-logs) remains separate — actor/event audit, not version store.
Unsaved changes & destructive confirms
Item create/edit (and singleton show) register with the admin leave guard:
- Unsaved badge + Discard appear in the header toolbar while the form is dirty. Discard opens Keep editing / Discard changes, remounts the form with original values, and stays on the page.
- Navigating away via sidebar/links while dirty shows the same dialog; Discard then continues navigation.
- Delete on the item form (and History Restore) opens a confirm dialog — no bare submits or
window.confirm.
List views (items index) confirm soft-delete, bulk delete, and permanent delete with a count when multiple rows are selected.
See also: Roles & permissions (role form Discard), Project settings (settings Save bar Discard).
Browser draft autosave
While editing an item, dirty field values are debounced into localStorage (externa:item-draft:{collectionId}:{itemId|new}). Reloading the form shows Restored unsaved draft with Discard draft (clears storage and remounts server values). Successful Save/Create clears the draft. This is browser-only — not a server draft status and not shared across devices.
Preview as role / public
On an existing item, Preview as… opens a dialog to pick any role (including public). GET /collections/{collection}/items/{item}/preview-as-role (collections.items.preview-as-role) runs the same ACL + file expand pipeline as the public CMS API and returns read-only JSON (redacted fields omitted; public files without Files → Read appear as { id, access: "denied" }). If the role lacks collection Read or the item fails the item filter, readable is false.
See Roles & permissions and Public CMS API — access: denied.
Activity strip
Below the item form header: last updated by/at, links to Revisions and Activity (filtered /activity-logs?subject_type=App\Models\CollectionItem&subject_id={id}), plus the last few Spatie activity rows for the item subject (row-level events — field diffs live in revisions).
Singleton upsert
| Item | Detail |
|---|---|
| Route | PUT /collections/{collection}/singleton-content |
| Name | collections.singleton-content |
| Request | UpsertSingletonCollectionItemRequest |
| Guard | 404 unless $collection->is_singleton |
Behavior:
- Create the singleton item if missing.
- Otherwise merge arrays field-wise, strip readonly fields, normalize, sync values.
Field options endpoint
| Item | Detail |
|---|---|
| Route | GET /collections/{collection}/items/options |
| Name | collections.items.field-options |
| Service | CollectionItemOptionsService::paginateForField |
Query parameters: field_id (required), related_collection_id, search, page, per_page (1–100, default 20). Response pages of { id, label } for relation pickers.
Relational / M2M editing
| Field family | UI / payload |
|---|---|
| Relation / ManyToOne / RelationTree | Scalar related item id |
| RelationMany / OneToMany | Array of ids |
| ManyToMany | Array of { related_item_id, meta } (bare ints still accepted on write) |
| M2A | Array of { related_collection_id, related_item_id } |
Junction metadata (M2M)
Optional settings.junction_fields on the field (JSON mini-schema in field settings):
[{ "name": "sort", "type": "number" }]
Allowed types: string, number, boolean. The item form shows meta inputs per linked item. API consumers should send:
{ "related_item_id": 12, "meta": { "sort": 1 } }
Source map
| Concern | Location |
|---|---|
| Item controller | app/Http/Controllers/Collections/ItemController.php |
| Revisions | app/Http/Controllers/Collections/ItemRevisionController.php |
| Rule builder | app/Services/Collections/CollectionItemDataRuleBuilder.php |
| Query / filters | app/Services/Collections/CollectionItemQueryService.php |
| Routes | routes/collections.php |
| Pages | resources/js/pages/collections/items/* |