# Field types

Source: https://docs.externa.qiick.io/docs/field-types

Each collection field has a `type` from `App\Enums\FieldTypeEnum`. The type drives validation, normalization, EAV storage shape, and which React input renders in the item form.

> **Sources of truth**
> Enum: `app/Enums/FieldTypeEnum.php`. Settings validation: `ValidatesCollectionFieldSettings`. UI catalog: `resources/js/lib/collection-field-types.ts`. Item inputs: `dynamic-item-fields.tsx`.

> Changing a field's type after it has stored values can lead to data loss or validation errors. See [Schema changes](/docs/schema-changes) for current behaviour, safe migration paths, and planned fixes.

## Common settings

Available on (almost) every field via `collections_fields.settings` JSON:

| Key                     | Shape                                       | Effect                                                                                                                                       |
| ----------------------- | ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| `required`              | flag (`true` / `1` / `'true'` / …)          | Value required unless overridden by validation operators                                                                                     |
| `readonly`              | flag                                        | Not editable in the item form                                                                                                                |
| `hidden_in_form`        | flag                                        | Hidden in form; skipped by the rule builder                                                                                                  |
| `display_name`          | locale map (enabled content locales)        | Localized label override                                                                                                                     |
| `note`                  | locale map                                  | Localized helper text                                                                                                                        |
| `default_value`         | mixed                                       | Applied on create when empty (not for hash / files / relations / blocks / m2a in UI)                                                         |
| `validation_rules`      | `[{ operator, value? }]`                    | Extra operators: `required`, `unique`, `min_length`, `max_length`, `min`, `max`, `regex`, `contains`, `not_contains`, `equals`, `not_equals` |
| `validation_message`    | locale map                                  | Localized message for custom rules                                                                                                           |
| `layout_width`          | `half` \| `full` \| `fill` (default `full`) | Form column width                                                                                                                            |
| `layout_starts_new_row` | flag                                        | Force new row in the field layout UI                                                                                                         |
| `placeholder`           | locale map                                  | Where the input supports it                                                                                                                  |
| `conditions`            | see below                                   | Dynamic hide / readonly / required                                                                                                           |

Flags are normalized with a truthy check (boolean / `1` / `'1'` / `'true'` / `'on'`). Localized string maps are validated against the project’s **enabled content locales** (not hard-coded `en`/`it`) — see [Project settings](/docs/project-settings).

## Field packs

Deterministic field bundles you can apply in one shot onto an **existing** collection from the **Fields** UI (**Add field pack…**) or via AI (`ManageCollections` `apply_field_pack`). Existing field names are **skipped**; missing ones are created through the same settings pipeline as HTTP create field.

| Pack key     | Label        | Fields                                                                                                                                                                                                                                                                                                                                                                              |
| ------------ | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `seo_inline` | SEO (inline) | `seo_title` (string, translatable), `seo_description` (textarea, translatable), `seo_keywords` (string, translatable), `seo_alternate` (string, translatable), `seo_canonical` (string), `seo_robots` (select), `seo_noindex` (boolean), `seo_og_image` / `seo_facebook_image` / `seo_twitter_image` (image). Prefer the **SEO collection pack** + M2O for Articles/Pages/Products. |
| `publishing` | Publishing   | `status` (select draft/published/archived), `published_at` (date), `featured` (boolean)                                                                                                                                                                                                                                                                                             |
| `contact`    | Contact      | `email`, `phone`, `address`, `city`, `postal_code`, `country`                                                                                                                                                                                                                                                                                                                       |
| `social`     | Social       | `facebook_url`, `instagram_url`, `linkedin_url`, `twitter_url`, `youtube_url`                                                                                                                                                                                                                                                                                                       |

HTTP: `POST /collections/{collection}/field-packs/{pack}` (`can-edit-collections`). Source: `app/Support/Collections/FieldPacks/`.

### How to add a field pack

1. Add a `*FieldPack` class with a static `definition()` (see `SeoInlineFieldPack`).
2. Register it in `FieldPackRegistry::all()`.
3. Ship docs + a Pest apply/skip test.

## Collection packs

Starter collections. Prefer these for SEO entity / Articles / Pages / Products / Categories. Apply from the collections list (**Create from pack…**) or AI (`apply_collection_pack`). Missing dependencies are **auto-created**. Existing collection slugs are **reused**; missing fields/relations are added.

| Pack key     | Creates                                                                                                 | Requires            | Relations                 |
| ------------ | ------------------------------------------------------------------------------------------------------- | ------------------- | ------------------------- |
| `seo`        | `seo` — short names (`title`, `description`, …) not `seo_*`                                             | —                   | —                         |
| `categories` | `categories` — name (tr), slug, description (tr)                                                        | —                   | —                         |
| `pages`      | `pages` — title, slug, status, published_at, `body` (blocks: `section`, `rich_text`)                    | `seo`               | M2O `seo` → seo           |
| `articles`   | `articles` — title, slug, excerpt, `body` (blocks: `section`, `rich_text`), status, published_at, cover | `seo`, `categories` | M2O `category`, M2O `seo` |
| `products`   | `products` — title, slug, description, price, images, status                                            | `seo`, `categories` | M2O `category`, M2O `seo` |

HTTP: `POST /collections/packs/{pack}` (`can-create-collections`), optional body `name` / `slug`. Source: `app/Support/Collections/CollectionPacks/`.

### Pack block schema (articles, pages)

Both `articles.body` and `pages.body` use `MinimalBlocksBody` (`app/Support/Collections/CollectionPacks/MinimalBlocksBody.php`) with two block types:

| Block type  | Fields                           | Notes                 |
| ----------- | -------------------------------- | --------------------- |
| `section`   | `heading` (string, translatable) | Section heading       |
| `rich_text` | `body` (wysiwyg, translatable)   | TipTap WYSIWYG editor |

Example Public CMS API response (`articles.body` with `?locale=it`):

```json
[
  {
    "id": "...",
    "type": "section",
    "data": { "heading": { "en": "Intro", "it": "Introduzione" } }
  },
  {
    "id": "...",
    "type": "rich_text",
    "data": { "body": { "en": "<p>Hello</p>", "it": "<p>Ciao</p>" } }
  }
]
```

**Note:** The `?locale=` query parameter and `include_all_translations` only apply to **top-level** translatable fields. Nested block fields always return their per-locale maps (`{ "en": "...", "it": "..." }`). Frontends should pick the locale from each block's field data using the same fallback order as the server (see [Locale fallback](/docs/configuration#locale-fallback), or [Live Preview — Locale resolution](/docs/live-preview#locale-resolution) for active-locale resolution).

### How to add a collection pack

1. Add a `*CollectionPack` class with `definition()` (`key`, `label`, `collection`, `fields`, `requires`, `relations`).
2. Register it in `CollectionPackRegistry::all()`.
3. Ship docs + Pest (deps + relations + idempotent apply).

**Model note:** SEO is a **collection**; Articles/Pages/Products link via **many_to_one**. Use `seo_inline` only when you need denormalized `seo_*` fields on an existing collection.

## Field conditions

Optional per-field rules evaluated against other field values on the item form (client) and for **required** / **readonly** on the server. Full operator table + examples: [Field conditions](/docs/field-conditions).

```json
{
  "logic": "or",
  "rules": [
    { "field": "kind", "operator": "equals", "value": "custom" },
    { "field": "status", "operator": "in", "value": "draft, review" }
  ],
  "hidden": false,
  "readonly": false,
  "required": true
}
```

Operators: `equals`, `not_equals`, `contains`, `empty`, `not_empty`, `gt` / `gte` / `lt` / `lte`, `in` / `not_in`. Logic is flat **AND** or **OR** (no nested groups). When rules match, any of `hidden` / `readonly` / `required` present in the object override the base settings for that request.

## Collection form layout

Presentation metadata on `collections.form_layout` (JSON), **not** a field type. Fields stay leaves; layout groups them into optional tabs + collapsible sections:

```json
{
  "version": 1,
  "tabs": [{ "id": "main", "label": { "en": "Main" } }],
  "sections": [
    {
      "id": "details",
      "tab_id": "main",
      "label": { "en": "Details" },
      "collapsible": true,
      "collapsed": false,
      "field_ids": [1, 2]
    }
  ]
}
```

`layout_width` / `layout_starts_new_row` still apply inside each section. Unplaced fields render in an “Other” group. Endpoint: `PUT /collections/{collection}/form-layout`.

## Storage helpers on the enum

| Method                     | Meaning                                                                                               |
| -------------------------- | ----------------------------------------------------------------------------------------------------- |
| `isArrayStorage()`         | Value is stored as multiple EAV rows (`position` 0..n-1) or as a JSON array assembled from those rows |
| `isRelationType()`         | Points at another collection’s items                                                                  |
| `isMultipleRelationType()` | Relation that stores **many** related item IDs                                                        |
| `supportsTranslatable()`   | Whether the field may be marked per-locale (`collections_fields.translatable`)                        |

> **Translatable is not universal**
> `hash`, `blocks`, and all relation types (`many_to_one`, `many_to_many`, `one_to_many`, `m2a`, `relation_tree`, legacy `relation` / `relation_many`) do **not** support `translatable`. The admin UI hides the toggle; create/update (and the AI tool) force `translatable=false` for those types. For `blocks`, localization lives inside nested block fields instead. Text, media, selects, boolean/number/date/color/map/slider, and similar scalars still can.

> `image` is **not** in `FieldTypeEnum::isArrayStorage()`, but `CollectionField::usesArrayStorage()` returns true when `settings.allow_multiple` is enabled. Treat image-with-multiple as array storage at runtime.

## Array storage types

These return `true` from `isArrayStorage()`:

`multiselect`, `checkbox_group`, `checkbox_group_tree`, `tag`, `files`, `one_to_many`, `many_to_many`, `blocks`, `m2a`, `relation_many`.

## Relation types

These return `true` from `isRelationType()`:

`relation`, `many_to_one`, `one_to_many`, `many_to_many`, `relation_tree`, `relation_many`.

Multiple relation IDs (`isMultipleRelationType()`): `one_to_many`, `many_to_many`, `relation_many`.

> **M2A exception**
> `m2a` uses array storage of `{ related_collection_id, related_item_id }` blocks but is **not** classified as `isRelationType()` in the enum.

## Every field type

### Text and scalars

| Type             | Value              | Purpose                                                          | Array? | Relation? | Notable settings                                                                                                                                                   |
| ---------------- | ------------------ | ---------------------------------------------------------------- | ------ | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| String           | `string`           | Single-line text                                                 | no     | no        | `input_type` (`string`/`text`/`integer`/`bigInteger`/`float`/`decimal`/`uuid`), `max_length`, `placeholder`, `icon_left`/`icon_right`, `trim`, `slugify`, `masked` |
| Autocomplete     | `autocomplete`     | Combobox from static options                                     | no     | no        | `options[{value,label}]`, `allow_other`, `placeholder`                                                                                                             |
| API autocomplete | `api_autocomplete` | Remote suggestions                                               | no     | no        | `url` (may include `{{value}}`), `results_path`, `text_path`, `value_path`, `trigger` (`debounce`\|`throttle`), `rate`, `placeholder`, icons                       |
| Number           | `number`           | Numeric input                                                    | no     | no        | `min`, `max`, `step`, `placeholder`                                                                                                                                |
| Boolean          | `boolean`          | Toggle                                                           | no     | no        | `label_on`, `label_off` (translated)                                                                                                                               |
| Textarea         | `textarea`         | Multi-line plain text                                            | no     | no        | `placeholder`, `rows` (default 6), `max_length`                                                                                                                    |
| WYSIWYG          | `wysiwyg`          | Rich HTML (TipTap editor)                                        | no     | no        | `rows`, `max_length`, `placeholder`; server strips scripts/unsafe tags on save                                                                                     |
| Markdown         | `markdown`         | Markdown source                                                  | no     | no        | same textarea-like options                                                                                                                                         |
| Code             | `code`             | Code editor                                                      | no     | no        | `language` (default `javascript`), `line_numbers`, `line_wrapping`, `template`                                                                                     |
| Hash             | `hash`             | Auto fingerprint ID (`sha256` of UUID) — **not** a password hash | no     | no        | `masked`; always nullable in rules                                                                                                                                 |
| Slider           | `slider`           | Numeric slider                                                   | no     | no        | `min` (0), `max` (100), `step`, `default_value`, `show_value`                                                                                                      |

### Choice

| Type                | Value                 | Purpose               | Array?  | Relation? | Notable settings                                                                |
| ------------------- | --------------------- | --------------------- | ------- | --------- | ------------------------------------------------------------------------------- |
| Select              | `select`              | Single choice         | no      | no        | `options`, `allow_none`, `allow_other` (UI + validation)                        |
| Multiselect         | `multiselect`         | Multi choice dropdown | **yes** | no        | `options`, `allow_other`                                                        |
| Checkbox group      | `checkbox_group`      | Multi checkboxes      | **yes** | no        | `options`, `allow_other`                                                        |
| Checkbox group tree | `checkbox_group_tree` | Nested multi select   | **yes** | no        | Tree `options` (+ `children`), `value_combining` (`all`\|`leaf`)                |
| Radio group         | `radio_group`         | Single radio          | no      | no        | `options`, `allow_other`                                                        |
| Tag                 | `tag`                 | Tag list              | **yes** | no        | `presets`, `separator` (default `,`), `allow_other`, `lowercase`, `alphabetize` |

### Date, map, color

| Type  | Value   | Purpose                     | Array? | Relation? | Notable settings / storage                                                                                                                                                                                                                                                                                                  |
| ----- | ------- | --------------------------- | ------ | --------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Date  | `date`  | Date / time / datetime      | no     | no        | `date_mode` (`date`\|`time`\|`datetime`, default `datetime`), `include_seconds`. Dead `use_24h` removed (native inputs).                                                                                                                                                                                                    |
| Map   | `map`   | GeoJSON Point or MultiPoint | no     | no        | Stored as GeoJSON (`Point` / `MultiPoint`, coordinates `[lng, lat]`); legacy `{ lat, lng }` accepted on write and converted. Settings: `geometry_mode` (`point`\|`multipoint`, default `point`), `default_lat`, `default_lng`, `default_zoom`. Leaflet OSM picker in admin; respects `readonly`. No LineString/Polygon yet. |
| Color | `color` | Color picker                | no     | no        | `opacity`, `preset_colors`                                                                                                                                                                                                                                                                                                  |

### Files and media

| Type  | Value   | Purpose                   | Array?        | Relation? | Notable settings                                                                         |
| ----- | ------- | ------------------------- | ------------- | --------- | ---------------------------------------------------------------------------------------- |
| Image | `image` | File-manager image picker | conditional\* | no        | `allow_multiple` → list of file IDs; else single id; `allowed_mime_types`, `crop_to_fit` |
| File  | `file`  | Single file (legacy)      | no            | no        | `allowed_mime_types`                                                                     |
| Files | `files` | Multiple files            | **yes**       | no        | `allowed_mime_types`                                                                     |

\* See callout above for `allow_multiple`.

### Relations

Shared relation settings (where applicable):

- `related_collection_id` — target collection
- `display_field` — field name used as label
- `display_template` — optional label template
- `filter` — option list filter
- `allow_duplicates` — whether the same related id may appear more than once
- `junction_fields` — **many_to_many only**: `[{ "name": "sort", "type": "number" }]` mini-schema for per-link `meta` (`string` \| `number` \| `boolean`)

| Type          | Value           | Purpose                                                               | Array?  | Relation? | Extra                                                                                                |
| ------------- | --------------- | --------------------------------------------------------------------- | ------- | --------- | ---------------------------------------------------------------------------------------------------- |
| Relation      | `relation`      | Legacy many-to-one alias (hidden from type picker)                    | no      | **yes**   | Shared relation settings                                                                             |
| Many to one   | `many_to_one`   | Link one related item                                                 | no      | **yes**   | Shared relation settings                                                                             |
| One to many   | `one_to_many`   | Many related IDs                                                      | **yes** | **yes**   | + `layout` (`list`\|`table`)                                                                         |
| Many to many  | `many_to_many`  | Junction objects `{ related_item_id, meta }` (ints accepted on write) | **yes** | **yes**   | Shared relation settings + optional `junction_fields`                                                |
| Relation tree | `relation_tree` | **Alias of M2O** — no tree UI yet (hidden from picker; TODO)          | no      | **yes**   | Shared relation settings                                                                             |
| Relation many | `relation_many` | Legacy multi-relation (hidden from picker)                            | **yes** | **yes**   | Shared relation settings                                                                             |
| Blocks        | `blocks`        | Inline page-builder blocks                                            | **yes** | no        | `block_types[]`; value `[{ id, type, data }]`                                                        |
| M2A           | `m2a`           | Many-to-any builder                                                   | **yes** | no†       | `allowed_collection_ids[]`, `allow_duplicates`; value `[{ related_collection_id, related_item_id }]` |

† Not in `isRelationType()`.

## Normalized storage shapes

| Shape                         | Types                                                                                                       |
| ----------------------------- | ----------------------------------------------------------------------------------------------------------- |
| string / null                 | string, autocomplete, api_autocomplete, textarea, wysiwyg, markdown, code, select, radio_group, date, color |
| number / null                 | number, slider                                                                                              |
| bool / null                   | boolean                                                                                                     |
| string[]                      | multiselect, checkbox_group, checkbox_group_tree, tag                                                       |
| GeoJSON Point / MultiPoint    | map (legacy `{ lat, lng }` still accepted on write)                                                         |
| int (file id)                 | file; image (single)                                                                                        |
| int[] (file ids)              | files; image (multiple)                                                                                     |
| int (item id)                 | relation, many_to_one, relation_tree                                                                        |
| int[] (item ids)              | one_to_many, relation_many                                                                                  |
| `{ related_item_id, meta }[]` | many_to_many (breaking vs bare ints; ints still accepted on write)                                          |
| block[] `{ id, type, data }`  | blocks                                                                                                      |
| M2A block[]                   | m2a                                                                                                         |
| string (hash)                 | hash                                                                                                        |

## Blocks field

`blocks` defines its schema inline on the field itself instead of pointing to another collection like `m2a`.

### Settings

```json
{
  "max_blocks_depth": 3,
  "block_types": [
    {
      "key": "rich_text",
      "label": "Rich text",
      "fields": [
        {
          "name": "title",
          "type": "string",
          "translatable": true,
          "settings": {}
        },
        {
          "name": "body",
          "type": "wysiwyg",
          "translatable": true,
          "settings": {}
        }
      ]
    },
    {
      "key": "media",
      "label": "Media",
      "fields": [
        { "name": "image", "type": "image", "settings": {} },
        {
          "name": "caption",
          "type": "string",
          "translatable": true,
          "settings": {}
        }
      ]
    }
  ]
}
```

`max_blocks_depth` is per field (default **3**, absolute ceiling **5**). Nested `blocks` fields inherit the root field’s max; deeper `blocks` types are stripped on save.

### Value shape

```json
[
  {
    "id": "11111111-1111-1111-1111-111111111111",
    "type": "rich_text",
    "data": {
      "title": { "en": "Hello", "it": "Ciao" },
      "body": { "en": "<p>Body</p>" }
    }
  }
]
```

### Notes

- `wysiwyg` uses TipTap in the admin UI (toolbar: bold/italic/strike/headings/lists/blockquote/link/undo/redo). Paste from Word/HTML is accepted; `WysiwygHtmlSanitizer` strips scripts and non-allowlisted tags on normalize/save.
- The `blocks` field itself is not translatable; nested fields may be.
- **Nested translatable fields are not flattened by `?locale=` or `include_all_translations`** — they always return per-locale maps in API responses. Frontends must pick the locale per block themselves (see [Pack block schema](#pack-block-schema-articles-pages) above or [Locale fallback](/docs/configuration#locale-fallback) for the resolution order).
- Nested file fields (`image`, `file`, `files`) expand on the public API just like top-level file fields (including inside nested blocks).
- Nested relations (`m2a`, `many_to_many`, `one_to_many`) are **JSON-embedded** inside `block.data` — same link shapes as top-level (`m2a` `{ related_collection_id, related_item_id }`, m2m `{ related_item_id, meta }`). There is **no** junction table for nested paths; SQL filter/query on nested m2m is out of scope.
- Nested field `settings.conditions` use the same AND / equals / empty operators as collection fields, but evaluate against **sibling keys in `block.data`**, not top-level item fields.
- Builder UX: drag-and-drop, duplicate, delete, collapse/expand, text summary in the header, move up/down. No live frontend/site preview.
- `blocks` is distinct from top-level `m2a`: `blocks` stores inline typed fragments; top-level `m2a` links out to items. You can also put an `m2a` field _inside_ a block type.
- Golden path: an **Articles** collection with a body `blocks` field — for example, a richer custom design beyond the pack default might have `section` → nested `blocks` → leaf (`rich_text` / `media`); optional `related_modules` (`m2a`) and `related_articles` (`many_to_many`) block types; conditional nested fields (e.g. show `video_url` when `media_kind === video`).

### Nested blocks example

```json
{
  "max_blocks_depth": 3,
  "block_types": [
    {
      "key": "rich_text",
      "label": "Rich text",
      "fields": [
        {
          "name": "title",
          "type": "string",
          "translatable": true,
          "settings": {}
        },
        {
          "name": "body",
          "type": "wysiwyg",
          "translatable": true,
          "settings": {}
        }
      ]
    },
    {
      "key": "section",
      "label": "Section",
      "fields": [
        {
          "name": "heading",
          "type": "string",
          "translatable": true,
          "settings": {}
        },
        {
          "name": "blocks",
          "type": "blocks",
          "translatable": false,
          "settings": {
            "block_types": [
              {
                "key": "rich_text",
                "label": "Rich text",
                "fields": [
                  {
                    "name": "body",
                    "type": "wysiwyg",
                    "translatable": true,
                    "settings": {}
                  }
                ]
              },
              {
                "key": "media",
                "label": "Media",
                "fields": [{ "name": "image", "type": "image", "settings": {} }]
              }
            ]
          }
        }
      ]
    },
    {
      "key": "related_modules",
      "label": "Related modules",
      "fields": [
        {
          "name": "modules",
          "type": "m2a",
          "settings": { "allowed_collection_ids": [2] }
        }
      ]
    },
    {
      "key": "related_articles",
      "label": "Related articles",
      "fields": [
        {
          "name": "articles",
          "type": "many_to_many",
          "settings": { "related_collection_id": 1, "display_field": "title" }
        }
      ]
    }
  ]
}
```

### Nested conditions example

Inside a block type, conditions reference sibling field names:

```json
{
  "name": "video_url",
  "type": "string",
  "settings": {
    "conditions": {
      "logic": "and",
      "rules": [
        { "field": "media_kind", "operator": "equals", "value": "video" }
      ],
      "required": true
    }
  }
}
```

## Related pages

- [Collections data model](/docs/collections-data-model)
- [Collection items](/docs/collection-items)
- [AI assistant model](/docs/ai-assistant) — tools that create/edit fields and items
