diff --git a/docs/Fields.md b/docs/Fields.md index 9f8bfa3..3dd6392 100644 --- a/docs/Fields.md +++ b/docs/Fields.md @@ -5,374 +5,213 @@ include_toc: true # Fields -Fields are similar to a table's columns in a relational databases. +Fields define the columns of a schema. Each field has a `ui` type that controls both storage and the admin UI component rendered. -## Available fields for Collections and Files +## Common Optional Properties + +Most fields share these optional properties: + +| Property | Description | +|---|---| +| `required` | Whether the field must have a value to save as `published` | +| `nullable` | Allow saving as `null` | +| `help` | Help text shown below the input | +| `default` | Default value when creating a new record | +| `readonly` | Prevent editing from the UI | +| `group` | Tab group this field belongs to | + + +## Field Types ### text -One-line text input -required +One-line text input. -- **name**: The id of the field -- **label**: The friendly name of the field +**Required:** `name`, `label` -optional +| Property | Description | +|---|---| +| `min` | Minimum character count | +| `max` | Maximum character count | +| `selectOptions` | Array of options. Strings or `[{value, label}]` objects | +| `optionsFrom` | Schema name to load options from | +| `optionsField` | Field from `optionsFrom` to use as the value | +| `optionsSuggest` | Allow typing new values not in the options list | -- **required**: Is the field required to save the record -- **nullable**: Can the field be saved as null -- **min**: Minimum characters -- **max**: Maximum characters -- **help**: Help text -- **default**: Default value when creating new record -- **readonly**: Cannot edit this value from the UI -- **optionsFrom**: Schema to choose options from -- **optionsField**: Field's value to insert -- **optionsSuggest**: Allow to insert new values -- **selectOptions**: Array of options to select from. Or array of objects `[{value,label}]` -- **group**: The group that this field belongs to +--- ### textarea -textarea input -required +Multi-line text input. -- **name**: The id of the field -- **label**: The friendly name of the field +**Required:** `name`, `label` -optional +| Property | Description | +|---|---| +| `min` | Minimum character count | +| `max` | Maximum character count | -- **required**: Is the field required to save the record -- **nullable**: Can the field be saved as null -- **min**: Minimum characters -- **max**: Maximum characters -- **help**: Help text -- **default**: Default value when creating new record -- **readonly**: Cannot edit this value from the UI -- **group**: The group that this field belongs to +--- ### slug -Slug input. Generates automatically if left empty -required +Slug input. Auto-generates from a source field if left empty. -- **name**: The id of the field -- **label**: The friendly name of the field -- **source**: The source field from which it generates +**Required:** `name`, `label`, `source` -optional +| Property | Description | +|---|---| +| `source` | Field name to generate the slug from | +| `min` | Minimum character count | +| `max` | Maximum character count | -- **required**: Is the field required to save the record -- **nullable**: Can the field be saved as null -- **min**: Minimum characters -- **max**: Maximum characters -- **help**: Help text -- **default**: Default value when creating new record -- **readonly**: Cannot edit this value from the UI -- **group**: The group that this field belongs to +--- ### rich -WYSIWYG editor -required +WYSIWYG rich text editor. -- **name**: The id of the field -- **label**: The friendly name of the field +**Required:** `name`, `label` -optional +| Property | Description | +|---|---| +| `min` | Minimum character count | +| `max` | Maximum character count | -- **required**: Is the field required to save the record -- **nullable**: Can the field be saved as null -- **min**: Minimum characters -- **max**: Maximum characters -- **help**: Help text -- **default**: Default value when creating new record -- **readonly**: Cannot edit this value from the UI -- **group**: The group that this field belongs to +--- + +### markdown + +Markdown editor. + +**Required:** `name`, `label` + +| Property | Description | +|---|---| +| `min` | Minimum character count | +| `max` | Maximum character count | + +--- ### number -Any numeric value -required +Numeric input. -- **name**: The id of the field -- **label**: The friendly name of the field +**Required:** `name`, `label` -optional +| Property | Description | +|---|---| +| `decimals` | Number of decimal places. Default: `0` | +| `min` | Minimum value | +| `max` | Maximum value | +| `optionsFrom` | Schema name to load options from | +| `optionsField` | Field from `optionsFrom` to use as the value | +| `optionsSuggest` | Allow typing new values not in the options list | -- **decimals**: default is 0 -- **required**: Is the field required to save the record -- **nullable**: Can the field be saved as null -- **min**: Minimum characters -- **max**: Maximum characters -- **help**: Help text -- **default**: Default value when creating new record -- **readonly**: Cannot edit this value from the UI -- **optionsFrom**: Schema to choose options from -- **optionsField**: Field's value to insert -- **optionsSuggest**: Allow to insert new values -- **group**: The group that this field belongs to +--- ### checkbox -True or false -required +Boolean true/false toggle. -- **name**: The id of the field -- **label**: The friendly name of the field +**Required:** `name`, `label` -optional - -- **required**: Is the field required to save the record -- **nullable**: Can the field be saved as null -- **help**: Help text -- **default**: Default value when creating new record -- **readonly**: Cannot edit this value from the UI -- **group**: The group that this field belongs to +--- ### color -Color picker -required +Color picker. -- **name**: The id of the field -- **label**: The friendly name of the field +**Required:** `name`, `label` -optional +| Property | Description | +|---|---| +| `selectOptions` | Restrict to a predefined palette | +| `optionsFrom` | Schema name to load options from | +| `optionsField` | Field from `optionsFrom` to use as the value | +| `optionsSuggest` | Allow typing new values not in the options list | -- **required**: Is the field required to save the record -- **nullable**: Can the field be saved as null -- **help**: Help text -- **default**: Default value when creating new record -- **readonly**: Cannot edit this value from the UI -- **optionsFrom**: Schema to choose options from -- **optionsField**: Field's value to insert -- **optionsSuggest**: Allow to insert new values -- **selectOptions**: Array of options to select from. Or array of objects `[{value,label}]` -- **group**: The group that this field belongs to +--- ### date -Date select -required +Date selector. Stores as a date string. -- **name**: The id of the field -- **label**: The friendly name of the field +**Required:** `name`, `label` -optional +| Property | Description | +|---|---| +| `min` | Minimum date | +| `max` | Maximum date | +| `selectOptions` | Predefined date options | +| `optionsFrom` | Schema name to load options from | +| `optionsField` | Field from `optionsFrom` to use as the value | -- **required**: Is the field required to save the record -- **nullable**: Can the field be saved as null -- **min**: Minimum date -- **max**: Maximum date -- **help**: Help text -- **default**: Default value when creating new record -- **readonly**: Cannot edit this value from the UI -- **optionsFrom**: Schema to choose options from -- **optionsField**: Field's value to insert -- **optionsSuggest**: Allow to insert new values -- **selectOptions**: Array of options to select from. Or array of objects `[{value,label}]` -- **group**: The group that this field belongs to +--- ### datetime -Date and time selector -required +Date and time selector. Stores as an ISO 8601 string. -- **name**: The id of the field -- **label**: The friendly name of the field +**Required:** `name`, `label` -optional +| Property | Description | +|---|---| +| `min` | Minimum datetime | +| `max` | Maximum datetime | +| `selectOptions` | Predefined datetime options | +| `optionsFrom` | Schema name to load options from | +| `optionsField` | Field from `optionsFrom` to use as the value | -- **required**: Is the field required to save the record -- **nullable**: Can the field be saved as null -- **min**: Minimum date -- **max**: Maximum date -- **help**: Help text -- **default**: Default value when creating new record -- **readonly**: Cannot edit this value from the UI -- **optionsFrom**: Schema to choose options from -- **optionsField**: Field's value to insert -- **optionsSuggest**: Allow to insert new values -- **selectOptions**: Array of options to select from. Or array of objects `[{value,label}]` -- **group**: The group that this field belongs to +--- ### json -Json data -required +Raw JSON data field. -- **name**: The id of the field -- **label**: The friendly name of the field +**Required:** `name`, `label` -optional - -- **required**: Is the field required to save the record -- **nullable**: Can the field be saved as null -- **help**: Help text -- **default**: Default value when creating new record -- **readonly**: Cannot edit this value from the UI -- **group**: The group that this field belongs to - -### markdown -Markdown editor - -required - -- **name**: The id of the field -- **label**: The friendly name of the field - -optional - -- **required**: Is the field required to save the record -- **nullable**: Can the field be saved as null -- **min**: Minimum characters -- **max**: Maximum characters -- **help**: Help text -- **default**: Default value when creating new record -- **readonly**: Cannot edit this value from the UI -- **group**: The group that this field belongs to +--- ### file -Upload or select files -required +Upload or select files from a files schema. -- **name**: The id of the field -- **label**: The friendly name of the field -- **collections**: File collections to choose from +**Required:** `name`, `label`, `collections` -optional - -- **mime**: The mime types allowed to select -- **nullable**: Can the field be saved as null -- **min**: Minimum files -- **max**: Maximum files -- **help**: Help text -- **group**: The group that this field belongs to +| Property | Description | +|---|---| +| `collections` | Array of file schema names to choose from | +| `mime` | Allowed MIME types e.g. `["image/*"]` | +| `min` | Minimum number of files | +| `max` | Maximum number of files | +--- ### reference -Reference other records -required +Reference records from another collection. -- **name**: The id of the field -- **label**: The friendly name of the field -- **collections**: Collections to choose from +**Required:** `name`, `label`, `collections` -optional +| Property | Description | +|---|---| +| `collections` | Array of collection schema names to reference | +| `min` | Minimum number of references | +| `max` | Maximum number of references | -- **nullable**: Can the field be saved as null -- **min**: Minimum files -- **max**: Maximum files -- **help**: Help text -- **group**: The group that this field belongs to +--- +## Example Field -### block -The block editor - -required - -- **name**: The id of the field -- **label**: The friendly name of the field -- **schema**: The block schema name - -optional - -- **required**: Is the field required to save the record -- **nullable**: Can the field be saved as null -- **help**: Help text -- **default**: Default value when creating new record -- **readonly**: Cannot edit this value from the UI -- **group**: The group that this field belongs to - - ) -## Available fields for the Block Editor - -### heading -Single-line text - -required - -- **name**: The id of the field -- **label**: The friendly name of the field - -optional - -- **min**: Minimum date -- **max**: Maximum date -- **default**: Default value when creating new record - -### textarea -Multiline text - -required - -- **name**: The id of the field -- **label**: The friendly name of the field - -optional - -- **min**: Minimum date -- **max**: Maximum date -- **default**: Default value when creating new record - -### rich -WYSIWYG editor - -required - -- **name**: The id of the field -- **label**: The friendly name of the field - -optional - -- **min**: Minimum date -- **max**: Maximum date -- **default**: Default value when creating new record - - -### markdown -Markdown editor - -required - -- **name**: The id of the field -- **label**: The friendly name of the field - -optional - -- **min**: Minimum date -- **max**: Maximum date -- **default**: Default value when creating new record - -### file -Choose files - -required - -- **name**: The id of the field -- **label**: The friendly name of the field -- **collections**: File collections to choose from - -optional -- **mime**: The mime types allowed to select -- **min**: Minimum date -- **max**: Maximum date -- **default**: Default value when creating new record - -### reference -Choose files - -required - -- **name**: The id of the field -- **label**: The friendly name of the field -- **collections**: Collections to choose from - -optional -- **min**: Minimum date -- **max**: Maximum date -- **default**: Default value when creating new record \ No newline at end of file +```json +{ + "ui": "text", + "name": "title", + "label": "Title", + "required": true, + "min": 3, + "max": 200, + "help": "The main title of the post" +} +``` diff --git a/docs/Queries.md b/docs/Queries.md index 07d5db8..f667cb9 100644 --- a/docs/Queries.md +++ b/docs/Queries.md @@ -5,88 +5,169 @@ include_toc: true # Queries -## Graph or Tree - -Queries can return results in 2 formats. A graph or a tree. - -Graphs results are a collection of records (nodes) and a collection of edges. This format is more useful for network visualization. - -The tree format is more straightforward as it returns a collection of records. Each record has a **_children** and a **_parents** field and the tree can continue upwards or downwards according to the depth you have requested. - -For example to request records with their children and their children's children: +The `Query` class is the main way to fetch records. Inject it via the Laravel container. ```php -$query->childrenDepth(2); +public function __construct(private Query $query) {} ``` -Maybe you only want to get a specific type of relationship: -```php -$query->childrenDepth(2)->childrenFields(["categories"]); -``` +## Return Formats + +Queries return results in two formats: + +**Graph** — via `->run()`. Returns a `Graph` object with `records` and `edges` collections. Useful for network-style data. + +**Tree** — via `->tree()`. Returns a flat `Collection` of records where each record has `_children` and `_parents` arrays populated based on the requested depth. This is the most common format. + +## Chaining Methods + +All methods return `$this` and can be chained: + +| Method | Description | +|---|---| +| `->filter(array)` | Add an AND filter | +| `->orFilter(array)` | Add an OR filter (grouped) | +| `->limit(int)` | Max number of root records to return | +| `->skip(int)` | Offset for pagination | +| `->sort(string)` | Sort by field. Prefix with `-` for descending e.g. `"-_sys.updatedAt"` | +| `->status(array)` | Filter by status array e.g. `["published", "draft"]` | +| `->onlyPublished()` | Shorthand for `->status(["published"])` | +| `->childrenDepth(int)` | How many levels of children to load | +| `->childrenLimit(int)` | Max children per record | +| `->childrenFields(array)` | Only follow specific relationship fields | +| `->parentsDepth(int)` | How many levels of parents to load | +| `->parentsLimit(int)` | Max parents per record | +| `->parentFields(array)` | Only follow specific relationship fields | +| `->notLinked(string)` | Return only records with no parents | +| `->run()` | Execute and return a `Graph` | +| `->tree()` | Execute and return a `Collection` (tree format) | +| `->runWithCount()` | Execute and return a `Graph` with a `total` count | + ## Filters -You can filter your query with the following format: +Filter keys use the format `field_operator`. When no operator suffix is given, `eq` is assumed. ```php $query->filter(["field_operator" => "value"]); -// example: +// No operator = eq +$query->filter(["schema" => "blogPosts"]); -$query->filter(["date_lt" => "2020-09-15"]); +// With operator +$query->filter(["_sys.updatedAt_gte" => "2024-01-01"]); ``` -Or filters are also available: +### Operator Reference + +| Operator | Description | +|---|---| +| _(none)_ or `eq` | Equals (string) | +| `ne` | Not equals (string) | +| `eqnum` | Equals (numeric) | +| `neqnum` | Not equals (numeric) | +| `eqtrue` | Equals `true` | +| `eqfalse` | Equals `false` | +| `netrue` | Not equals `true` | +| `nefalse` | Not equals `false` | +| `regex` | Regular expression match | +| `in` | Value is in array (strings) | +| `nin` | Value is not in array (strings) | +| `innum` | Value is in array (numeric) | +| `ninnum` | Value is not in array (numeric) | +| `lt` | Less than | +| `lte` | Less than or equal | +| `gt` | Greater than | +| `gte` | Greater than or equal | +| `null` | Field is null | +| `nnull` | Field is not null | +| `exists` | Field key exists | +| `nexists` | Field key does not exist | +| `filter` | Raw filter passthrough | + +### OR Filters ```php $query - ->filter(["price_eqn" => 10]) - ->orFilter(["title_regex" => "search", "slug_regex" => "search"]) - ; + ->filter(["schema" => "blogPosts"]) + ->orFilter(["title_regex" => "search", "slug_regex" => "search"]); ``` -## Operator List -- regex -- eq -- ne -- eqnum -- neqnum -- filter -- eqtrue -- eqfalse -- netrue -- nefalse -- in: -- innum -- nin -- ninnum -- lt -- lte -- gt -- gte -- null -- nnull -- exists -- nexists +Each `orFilter` call groups its arguments with OR between them. Multiple `orFilter` / `filter` calls are AND-ed together. -## Example +### Cross-Schema (Children) Filters -Get 10 posts from the sports category as a tree +You can filter records by properties of their related records using the `children.` prefix: +```php +$query->filter([ + "schema" => "blogPosts", + "children.categories.data.slug" => "sports", +]); +``` + +This returns `blogPosts` that have a child in `categories` where `slug` equals `sports`. + + +## Nested Data Fields + +Use dot notation to filter on JSON data fields and system fields: + +```php +// Filter on a data field +$query->filter(["data.status_eq" => "draft"]); + +// Filter on a system field +$query->filter(["_sys.updatedAt_gte" => "2024-01-01"]); +``` + + +## Examples + +**Get published blog posts, newest first:** + +```php +$posts = $query + ->filter(["schema" => "blogPosts"]) + ->onlyPublished() + ->sort("-_sys.updatedAt") + ->limit(10) + ->tree(); +``` + +**Get posts from a specific category with children loaded:** ```php $posts = $query ->filter([ - "schema" => "posts", + "schema" => "blogPosts", "children.categories.data.slug" => "sports", - ] - ) + ]) ->childrenDepth(1) + ->childrenFields(["categories"]) ->limit(10) ->tree(); - -$posts->map(...)->toArray(); - ``` +**Paginate records:** +```php +$graph = $query + ->filter(["schema" => "blogPosts"]) + ->limit(20) + ->skip(40) + ->runWithCount(); + +$posts = $graph->tree(); +$total = $graph->total; +``` + +**Search across fields:** + +```php +$results = $query + ->filter(["schema" => "blogPosts"]) + ->orFilter(["title_regex" => $term, "slug_regex" => $term]) + ->limit(10) + ->tree(); +``` diff --git a/docs/Schemas.md b/docs/Schemas.md index dfda485..a0684d2 100644 --- a/docs/Schemas.md +++ b/docs/Schemas.md @@ -5,80 +5,103 @@ include_toc: true # Schemas -Schemas define both the shape of your data and how the UI on the admin will behave. +Schemas define both the shape of your data and how the admin UI behaves. -There are 3 types of schemas +There are 2 types of schemas: -- Collections: Normal data -- Files: Images and files -- Block: Used in the block editor +- **collection** — Regular data records +- **files** — Images and file uploads ## Collection Reference -- **name**: The ID of the collection. Camelcase and plural is the recommended format ex. blogPosts -- **label**: The friendly name of the schema -- **type**: The type of the collection. Should be "collection" -- **visible**: An array of field id to show on the content browser _optional_ -- **groups**: A list if group ids to separate your field in different tabs _optional_ -- **fields**: The list of your fields. Look the field reference for more -- **isEntry**: If this schema is important, it will show be visible on the main the sidebar. Default: false _optional_ -- **sortBy**: The default sorting in the content browser _optional_ -- **cardTitle**: Mustache code to customize the preview field _optional_ -- **cardImage**: Field name of image you want to use as a preview image _optional_ -- **revisions**: How many revisions are going to be kept for each record _optional_ -- **read**: Array of user groups that have read permissions _optional_ -- **write**: Array of user groups that have write permissions _optional_ +| Field | Required | Description | +|---|---|---| +| `name` | yes | Unique ID. Use camelCase plural e.g. `blogPosts` | +| `label` | yes | Friendly display name | +| `type` | yes | Must be `"collection"` | +| `fields` | yes | Array of field definitions. See [Fields](Fields.md) | +| `visible` | no | Field IDs to show in the content browser list | +| `groups` | no | Group IDs to split fields into tabs | +| `isEntry` | no | Show in sidebar. Default: `false` | +| `sortBy` | no | Default sort in browser. Prefix with `-` for descending e.g. `-_sys.updatedAt` | +| `cardTitle` | no | Mustache template for the record preview title e.g. `{{name}} - {{slug}}` | +| `cardImage` | no | Field name to use as the preview image | +| `revisions` | no | Number of revisions to keep per record. Default: `0` (disabled) | +| `read` | no | Roles with read access. Empty means all roles can read | +| `write` | no | Roles with write access. Empty means all roles can write | ## Files Reference -- **name**: The ID of the collection. Camelcase and plural is the recommended format ex. blogPosts -- **label**: The friendly name of the schema -- **type**: The type of the collection. Should be "files" -- **path**: The relative directory that these files will be stored. -- **groups**: A list if group ids to separate your field in different tabs _optional_ -- **fields**: The list of your fields. Look the field reference for more -- **isEntry**: If this schema is important, it will show be visible on the main the sidebar _optional_ -- **sortBy**: The default sorting in the content browser _optional_ -- **cardTitle**: Mustache code to customize the preview field _optional_ -- **revisions**: How many revisions are going to be kept for each record _optional_ -- **read**: Array of user groups that have read permissions _optional_ -- **write**: Array of user groups that have write permissions _optional_ +| Field | Required | Description | +|---|---|---| +| `name` | yes | Unique ID. Use camelCase plural e.g. `heroImages` | +| `label` | yes | Friendly display name | +| `type` | yes | Must be `"files"` | +| `fields` | yes | Array of field definitions. See [Fields](Fields.md) | +| `disk` | no | Laravel disk name. Default: `"lucent"` | +| `path` | no | Subdirectory for uploads. Default: schema name | +| `groups` | no | Group IDs to split fields into tabs | +| `isEntry` | no | Show in sidebar. Default: `false` | +| `sortBy` | no | Default sort in browser | +| `cardTitle` | no | Mustache template for the record preview title | +| `cardImage` | no | Field name to use as the preview image | +| `revisions` | no | Number of revisions to keep per record | +| `read` | no | Roles with read access | +| `write` | no | Roles with write access | -A full Collection example without the fields: +## System Fields + +Every record automatically has these read-only system fields available in queries: + +| Field | Description | +|---|---| +| `_sys.createdAt` | ISO 8601 creation timestamp | +| `_sys.updatedAt` | ISO 8601 last update timestamp | +| `_sys.createdBy` | User ID who created the record | +| `_sys.updatedBy` | User ID who last updated the record | +| `_sys.version` | Revision version number | +| `status` | `draft` or `published` | + + +## Example ```json { - "label": "Departments", - "name": "departments", - "isEntry": true, - "type": "collection", - "visible": [ - "slug", - "cover", - "_sys.updatedAt", - "status" + "schemas": [ + { + "label": "Blog Posts", + "name": "blogPosts", + "isEntry": true, + "type": "collection", + "visible": [ + "title", + "slug", + "_sys.updatedAt", + "status" + ], + "groups": [ + "Content", + "SEO" + ], + "sortBy": "-_sys.createdAt", + "cardTitle": "{{title}}", + "cardImage": "cover", + "revisions": 15, + "read": [ + "admin", + "editors", + "reviewers" + ], + "write": [ + "admin", + "editors" + ], + "fields": [] + } ], - "groups": [ - "Extra Info", - "Metadata", - "SEO" - ], - "sortBy": "-_sys.createdAt", - "schemaTitle": "{{name}} {{slug}}", - "schemaImage": "cover", - "revisions": 15, - "read": [ - "admin", - "editors", - "reviewers" - ], - "write": [ - "admin", - "editors" - ], - "fields": [] + "roles": ["admin", "editors", "reviewers"] } -``` \ No newline at end of file +``` diff --git a/docs/Static Generator.md b/docs/Static Generator.md index 222a9df..98b34c7 100644 --- a/docs/Static Generator.md +++ b/docs/Static Generator.md @@ -47,7 +47,7 @@ In order to make it accessible you have to create symlink: php artisan lucent:livelink ``` -Now your static website is accessible in http://localhost:8000/live +Now your static website is accessible at `http://localhost:8000/live` But it would not be nice to have the **live** prefix everywhere. That's why you need to make the following tweak in the nginx vhost