# Collection items

Source: https://docs.qiick.io/externa/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`.

<!-- screenshot omitted -->

<!-- screenshot omitted -->

> **Related**
> Schema & cascade: [Collections](https://docs.qiick.io/externa/collections). Field type catalog: [Field types](https://docs.qiick.io/externa/field-types). Model internals: [Collections data model](https://docs.qiick.io/externa/collections-data-model). HTTP table: [Collections API](https://docs.qiick.io/externa/collections-api). Item discussion threads: [Chat (hub & items)](https://docs.qiick.io/externa/chat). Collaborative presence / soft lock: [Collaborative editing](https://docs.qiick.io/externa/collaborative-editing). Locales & translation: [Translation workspace](https://docs.qiick.io/externa/translation-workspace). Kanban/calendar: [Collection layouts](https://docs.qiick.io/externa/collection-layouts).

> **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](https://docs.qiick.io/externa/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](https://docs.qiick.io/externa/ai-chat#ask-ai) |
| Item chat       | Drawer on item form                                           | Shared item thread — [Chat (hub & items)](https://docs.qiick.io/externa/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](https://docs.qiick.io/externa/roles-and-permissions#collection-access-rules).

## Advanced list filters

The items index toolbar has:

1. **Title search** — debounced contains on `title` (`filter[title][_contains]=…`)
2. **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 (UI: Field filters popover → Add rule → Apply). Service: `CollectionItemQueryService`. **Clear all** resets the URL filters (and leaves sort/direction alone).

Pest coverage: `tests/Feature/Collections/AdvancedFiltersTest.php` (operators + multi-rule) and `FieldCreateMatrixTest.php` (thin field-type create matrix).

## 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 `return` when it is a same-origin path under `/collections/…`.
- Invalid or off-site `return` values 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_revisions` on every `CollectionItemValuesWriter::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](https://docs.qiick.io/externa/roles-and-permissions) (role form Discard), [Project settings](https://docs.qiick.io/externa/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](https://docs.qiick.io/externa/roles-and-permissions#preview-as-role) and [Public CMS API — `access: denied`](https://docs.qiick.io/externa/public-cms-api#public-vs-private-files).

## 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:

1. Create the singleton item if missing.
2. 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):

```json
[{ "name": "sort", "type": "number" }]
```

Allowed types: `string`, `number`, `boolean`. The item form shows meta inputs per linked item. API consumers should send:

```json
{ "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/*`                      |
