✨ feat: enhance project setup and architecture documentation
- Updated `tibi-project-setup` skill to clarify project initialization goals and steps. - Improved `tibi-ssr-caching` skill to detail SSR architecture, responsibilities, and caching mechanisms. - Introduced `website-solution-architecture` skill for translating website requirements into coherent solutions. - Refined `AGENTS.md` to provide a structured roadmap for project development phases. - Added `ADMIN_ASSET_VERSION` to `api/config.yml.env` for asset versioning. - Updated SSR request flow and cache invalidation logic in `api/hooks/ssr/AGENTS.md`. - Removed obsolete `esbuild.config.admin.js` and integrated asset versioning into the main `esbuild.config.js`. - Adjusted `api/collections/content.yml` to utilize asset versioning for admin scripts.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: admin-ui-config
|
||||
description: Configure the admin UI for collections — meta labels, views (table/list/cards), field widgets, inputProps, fieldLists, sidebar layout, choices, foreign references, and image handling. Use when setting up or customizing collection admin views.
|
||||
description: Configure the admin UI for collections — meta labels, preview/viewHint, field widgets, inputProps, sidebar layout, choices, foreign references, and image handling. Use when setting up or customizing collection admin views.
|
||||
---
|
||||
|
||||
# admin-ui-config
|
||||
@@ -10,7 +10,7 @@ description: Configure the admin UI for collections — meta labels, views (tabl
|
||||
Use this skill when:
|
||||
|
||||
- Configuring how a collection appears in the tibi-admin UI
|
||||
- Setting up table/list/card views for a collection
|
||||
- Configuring collection preview and default list presentation
|
||||
- Configuring field widgets (dropdowns, media pickers, richtext, etc.)
|
||||
- Organizing fields into sidebar groups or sections
|
||||
- Setting up foreign key references between collections
|
||||
@@ -18,13 +18,15 @@ Use this skill when:
|
||||
|
||||
## Reference source
|
||||
|
||||
The canonical type definitions are in `tibi-admin-nova/types/admin.d.ts` (1296 lines). Always consult this file for the full API. This skill provides a practical summary.
|
||||
The canonical type definitions are in `tibi-admin-nova/types/admin.d.ts`. Always consult this file for the full API. This skill provides a practical summary.
|
||||
|
||||
Treat this skill as Nova-first. Use current Nova concepts such as `preview`, `singleton: { enabled }`, `drillDown`, `dependsOn`, `containerProps.layout`, `pagebuilder`, `viewHint`, `subNavigation`, and AI media assist.
|
||||
|
||||
---
|
||||
|
||||
## Collection meta configuration
|
||||
|
||||
The `meta` key in a collection YAML controls how the collection appears in the admin sidebar and list views.
|
||||
The `meta` key in a collection YAML controls how the collection appears in the admin sidebar and collection/list UI.
|
||||
|
||||
```yaml
|
||||
name: mycollection
|
||||
@@ -32,71 +34,55 @@ meta:
|
||||
label: { de: "Produkte", en: "Products" } # Sidebar label (i18n)
|
||||
muiIcon: shopping_cart # Material UI icon name
|
||||
group: shop # Group in admin sidebar
|
||||
singleton: false # true = only one entry allowed
|
||||
hideInNavigation: false # true = don't show in sidebar
|
||||
defaultSort: "-insertTime" # Default sort (prefix - = descending)
|
||||
rowIdentTpl: { twig: "{{ name }} ({{ price }})" } # Row display template
|
||||
singleton:
|
||||
enabled: false
|
||||
hide: false # Set to true to hide the collection for non-admin users
|
||||
preview:
|
||||
label: name
|
||||
secondary: price
|
||||
```
|
||||
|
||||
### Row identification
|
||||
### Preview
|
||||
|
||||
`rowIdentTpl` uses Twig syntax with field names. Used in admin list to identify entries:
|
||||
Use `meta.preview` as the universal entry representation for Nova lists, breadcrumbs, foreign-key widgets, and search result previews:
|
||||
|
||||
```yaml
|
||||
rowIdentTpl: { twig: "{{ name }}" } # Simple
|
||||
rowIdentTpl: { twig: "{{ type }} — {{ language }}" } # Combined
|
||||
preview: name
|
||||
|
||||
preview:
|
||||
label: name
|
||||
secondary: slug
|
||||
badge: status
|
||||
|
||||
preview:
|
||||
eval: "`${$this.firstName} ${$this.lastName}`"
|
||||
```
|
||||
|
||||
---
|
||||
## List presentation
|
||||
|
||||
## Views: table, simpleList, cardList
|
||||
|
||||
The `views` array defines how entries are displayed in the admin list. Multiple views can coexist (e.g. table for desktop, simpleList for mobile).
|
||||
|
||||
### Table view
|
||||
For current Nova, use `meta.viewHint` plus `meta.preview` for collection/list presentation.
|
||||
|
||||
```yaml
|
||||
meta:
|
||||
views:
|
||||
- type: table
|
||||
columns:
|
||||
- name # Simple: field name as column
|
||||
- source: lang # With filter
|
||||
filter: true
|
||||
- source: active # Boolean column with filter
|
||||
filter: true
|
||||
- source: price # Custom label
|
||||
label: { de: "Preis", en: "Price" }
|
||||
- source: insertTime # Date field
|
||||
width: 160
|
||||
viewHint: table
|
||||
preview:
|
||||
label: name
|
||||
secondary: slug
|
||||
badge: status
|
||||
table:
|
||||
- name
|
||||
- source: status
|
||||
label: Status
|
||||
- source: author.name
|
||||
label: Author
|
||||
select:
|
||||
- author.name
|
||||
```
|
||||
|
||||
### Simple list view (mobile)
|
||||
|
||||
```yaml
|
||||
meta:
|
||||
views:
|
||||
- type: simpleList
|
||||
mediaQuery: "(max-width: 600px)" # Show only on small screens
|
||||
primaryText: name
|
||||
secondaryText: lang
|
||||
tertiaryText: path
|
||||
image: thumbnail # Optional: show image thumbnail
|
||||
```
|
||||
|
||||
### Card list view
|
||||
|
||||
```yaml
|
||||
meta:
|
||||
views:
|
||||
- type: cardList
|
||||
fields:
|
||||
- source: name
|
||||
label: Name
|
||||
- source: price
|
||||
label: Preis
|
||||
widget: currency
|
||||
```
|
||||
- `meta.viewHint` controls the preferred collection presentation (`table`, `cards`, `media`, or `navigation` object where supported).
|
||||
- `preview.table` defines explicit list columns for Nova.
|
||||
- `preview.select` can reduce lookup work for preview table columns.
|
||||
- `meta.subNavigation` defines filtered entry tabs in the sidebar.
|
||||
|
||||
---
|
||||
|
||||
@@ -171,18 +157,25 @@ Override the default widget with `meta.widget`:
|
||||
meta:
|
||||
widget: richtext # Rich text editor (HTML)
|
||||
|
||||
- name: color
|
||||
type: string
|
||||
- name: heroImage
|
||||
type: file
|
||||
meta:
|
||||
widget: color # Color picker
|
||||
widget: image # Image-focused file widget
|
||||
|
||||
- name: image
|
||||
type: string
|
||||
- name: relatedPages
|
||||
type: string[]
|
||||
meta:
|
||||
widget: medialib # Media library picker
|
||||
widget: foreignKeyChipArray
|
||||
```
|
||||
|
||||
Common widget types: `text` (default), `richtext`, `color`, `medialib`, `code`, `markdown`, `password`, `hidden`.
|
||||
Common widget types: `text`, `checkbox`, `select`, `chipArray`, `checkboxArray`, `date`, `datetime`, `file`, `image`, `richtext`, `json`, `foreignKey`, `foreignKeyChipArray`, `pagebuilder`, `containerLessObject`, `containerLessObjectArray`.
|
||||
|
||||
Important current widgets/features to consider when designing a real website backoffice:
|
||||
|
||||
- `pagebuilder` for CMS-driven block/page authoring
|
||||
- `foreignKeyChipArray` for many-reference editing
|
||||
- `image` plus `imageEditor` / `downscale` for image-heavy workflows
|
||||
- `drillDown` editing for complex nested arrays
|
||||
|
||||
### Choices — dropdowns/selects
|
||||
|
||||
@@ -211,7 +204,7 @@ Dynamic choices from API:
|
||||
choices:
|
||||
endpoint: categories # Collection name
|
||||
mapping:
|
||||
id: _id
|
||||
id: id
|
||||
name: name
|
||||
```
|
||||
|
||||
@@ -226,22 +219,25 @@ Link to entries in another collection:
|
||||
label: { de: "Autor", en: "Author" }
|
||||
foreign:
|
||||
collection: users
|
||||
id: _id
|
||||
id: id
|
||||
sort: name
|
||||
projection: name,email
|
||||
render: { twig: "{{ name }} <{{ email }}>" }
|
||||
autoFill: # Auto-fill other fields on selection
|
||||
- source: email
|
||||
target: authorEmail
|
||||
render:
|
||||
label: name
|
||||
secondary: email
|
||||
createDefaults:
|
||||
role: author
|
||||
```
|
||||
|
||||
Use `foreign.id: id` for the outward FK field identity. Only Mongo-style filters/query conditions use `_id`. Use `foreign.render` or target-collection `meta.preview` so references stay readable. Bare IDs are not acceptable authoring UX for a serious website project.
|
||||
|
||||
### Image fields
|
||||
|
||||
```yaml
|
||||
- name: image
|
||||
type: file
|
||||
meta:
|
||||
widget: medialib
|
||||
widget: image
|
||||
downscale: # Auto-resize on upload
|
||||
maxWidth: 1920
|
||||
maxHeight: 1080
|
||||
@@ -264,7 +260,7 @@ Link to entries in another collection:
|
||||
- name: publishDate
|
||||
type: date
|
||||
meta:
|
||||
position: "sidebar:Veröffentlichung" # Sidebar with group header
|
||||
position: "sidebar:publishing" # Sidebar with group key
|
||||
```
|
||||
|
||||
### Sidebar groups (ordered)
|
||||
@@ -274,9 +270,12 @@ Define sidebar group order in collection meta:
|
||||
```yaml
|
||||
meta:
|
||||
sidebar:
|
||||
- Veröffentlichung
|
||||
- SEO
|
||||
- Einstellungen
|
||||
- group: publishing
|
||||
label: { de: "Veröffentlichung", en: "Publishing" }
|
||||
- group: seo
|
||||
label: { de: "SEO", en: "SEO" }
|
||||
- group: settings
|
||||
label: { de: "Einstellungen", en: "Settings" }
|
||||
```
|
||||
|
||||
### Sections in main area
|
||||
@@ -313,6 +312,15 @@ Use `containerProps` for multi-column layout:
|
||||
size: col-6
|
||||
```
|
||||
|
||||
`containerProps.layout` is one of the most important Nova ergonomics features. Use it aggressively to avoid long, single-column forms.
|
||||
|
||||
Recommended pattern for real projects:
|
||||
|
||||
- sidebar for publication, SEO, flags, relations, admin-only metadata
|
||||
- main area for editorial content
|
||||
- 2-column or 3-column layout for short related fields
|
||||
- section headings for repeated conceptual groups
|
||||
|
||||
---
|
||||
|
||||
## Nested objects and arrays
|
||||
@@ -340,7 +348,9 @@ Use `containerProps` for multi-column layout:
|
||||
type: object[]
|
||||
meta:
|
||||
label: { de: "Inhaltsblöcke", en: "Content Blocks" }
|
||||
preview: { eval: "item.type + ': ' + (item.headline || '')" }
|
||||
widget: pagebuilder
|
||||
preview: { eval: "`${$this.type}: ${$this.headline || ''}`" }
|
||||
drillDown: true
|
||||
subFields:
|
||||
- name: type
|
||||
type: string
|
||||
@@ -358,6 +368,74 @@ Use `containerProps` for multi-column layout:
|
||||
|
||||
The `preview` eval determines what's shown in the collapsed state of each array item.
|
||||
|
||||
### Drill-down arrays
|
||||
|
||||
For complex `object[]` data, prefer `drillDown: true` over dense inline editing. This is especially important for:
|
||||
|
||||
- nested content blocks
|
||||
- FAQs / accordions
|
||||
- team members with nested metadata
|
||||
- pricing tables / feature matrices
|
||||
|
||||
### Pagebuilder fields
|
||||
|
||||
Nova supports pagebuilder configuration at both collection and field level.
|
||||
|
||||
Typical pattern:
|
||||
|
||||
```yaml
|
||||
meta:
|
||||
pagebuilder:
|
||||
blockTypeField: type
|
||||
defaultViewport: desktop
|
||||
blockRegistry:
|
||||
file: /_/assets/dist/admin.mjs
|
||||
|
||||
fields:
|
||||
- name: blocks
|
||||
type: object[]
|
||||
meta:
|
||||
widget: pagebuilder
|
||||
pagebuilder:
|
||||
blockTypeField: type
|
||||
```
|
||||
|
||||
Use pagebuilder when editors work with heterogeneous content blocks. Use plain `object[]` only when the structure is uniform and simple.
|
||||
|
||||
### dependsOn
|
||||
|
||||
Use `dependsOn` to show only fields relevant to the selected block or mode:
|
||||
|
||||
```yaml
|
||||
- name: image
|
||||
type: file
|
||||
meta:
|
||||
dependsOn:
|
||||
eval: $parent.type == 'hero'
|
||||
```
|
||||
|
||||
This is critical for keeping pagebuilder schemas usable.
|
||||
|
||||
### AI-aware media and admin features
|
||||
|
||||
Current Nova types support AI-related admin capabilities, especially around media workflows. When appropriate for a project:
|
||||
|
||||
- use AI-assisted alt/caption generation for image-heavy collections
|
||||
- prefer explicit target fields for generated metadata
|
||||
- keep AI assist opt-in and editorially reviewable
|
||||
|
||||
Use AI only where it improves authoring quality; do not force it into every collection.
|
||||
|
||||
## Field-level permissions and authoring safety
|
||||
|
||||
Current tibi-server supports `readonlyFields`, `hiddenFields`, and eval-based field visibility/readonly rules.
|
||||
|
||||
Reflect these server rules in admin design:
|
||||
|
||||
- do not put critical computed fields front-and-center if editors may not be allowed to modify them
|
||||
- use `dependsOn`, `hidden`, and readonly semantics deliberately
|
||||
- remember that server-side permissions are authoritative even if the UI looks editable
|
||||
|
||||
### Drill-down
|
||||
|
||||
For complex nested objects, use `drillDown` to render them as a sub-page:
|
||||
@@ -405,28 +483,26 @@ meta:
|
||||
label: { de: "Produkte", en: "Products" }
|
||||
muiIcon: inventory_2
|
||||
group: shop
|
||||
defaultSort: "-insertTime"
|
||||
rowIdentTpl: { twig: "{{ name }} ({{ sku }})" }
|
||||
viewHint: table
|
||||
defaultSort:
|
||||
field: insertTime
|
||||
order: DESC
|
||||
preview:
|
||||
label: name
|
||||
secondary: sku
|
||||
badge: active
|
||||
table:
|
||||
- name
|
||||
- sku
|
||||
- source: price
|
||||
label: { de: "Preis", en: "Price" }
|
||||
- source: category
|
||||
label: { de: "Kategorie", en: "Category" }
|
||||
sidebar:
|
||||
- Veröffentlichung
|
||||
- SEO
|
||||
|
||||
views:
|
||||
- type: simpleList
|
||||
mediaQuery: "(max-width: 600px)"
|
||||
primaryText: name
|
||||
secondaryText: sku
|
||||
image: image
|
||||
- type: table
|
||||
columns:
|
||||
- name
|
||||
- sku
|
||||
- source: price
|
||||
label: { de: "Preis", en: "Price" }
|
||||
- source: active
|
||||
filter: true
|
||||
- source: category
|
||||
filter: true
|
||||
- group: publishing
|
||||
label: { de: "Veröffentlichung", en: "Publishing" }
|
||||
- group: seo
|
||||
label: { de: "SEO", en: "SEO" }
|
||||
|
||||
permissions:
|
||||
public:
|
||||
@@ -439,18 +515,12 @@ permissions:
|
||||
put: true
|
||||
delete: true
|
||||
|
||||
hooks:
|
||||
beforeRead: |
|
||||
!include hooks/filter_public.js
|
||||
afterWrite: |
|
||||
!include hooks/clear_cache.js
|
||||
|
||||
fields:
|
||||
- name: active
|
||||
type: boolean
|
||||
meta:
|
||||
label: { de: "Aktiv", en: "Active" }
|
||||
position: "sidebar:Veröffentlichung"
|
||||
position: "sidebar:publishing"
|
||||
- name: name
|
||||
type: string
|
||||
meta:
|
||||
@@ -492,7 +562,7 @@ fields:
|
||||
type: file
|
||||
meta:
|
||||
label: { de: "Produktbild", en: "Product Image" }
|
||||
widget: medialib
|
||||
widget: image
|
||||
downscale:
|
||||
maxWidth: 1200
|
||||
quality: 0.85
|
||||
@@ -500,12 +570,12 @@ fields:
|
||||
type: string
|
||||
meta:
|
||||
label: { de: "SEO Titel", en: "SEO Title" }
|
||||
position: "sidebar:SEO"
|
||||
position: "sidebar:seo"
|
||||
- name: seoDescription
|
||||
type: string
|
||||
meta:
|
||||
label: { de: "SEO Beschreibung", en: "SEO Description" }
|
||||
position: "sidebar:SEO"
|
||||
position: "sidebar:seo"
|
||||
inputProps:
|
||||
multiline: true
|
||||
rows: 3
|
||||
@@ -515,10 +585,8 @@ fields:
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **`meta.label` is i18n** — Always provide `{ de: "...", en: "..." }` objects, not plain strings.
|
||||
- **`views` order matters** — First matching view (by `mediaQuery`) is shown. Put mobile views (with `mediaQuery`) before desktop views (without).
|
||||
- **`meta.label` supports both strings and i18n objects** — Use i18n objects only when the collection or field label must be localized.
|
||||
- **`choices.id` must match stored value** — The `id` in choices is what gets saved to the database.
|
||||
- **`inputProps` depends on widget** — Not all props work with all widgets. Check tibi-admin-nova source if unsure.
|
||||
- **`position: sidebar` without group** — Fields go to an ungrouped area. Use `position: "sidebar:GroupName"` for grouping.
|
||||
- **`type: object[]` needs `subFields`** — Forgetting `subFields` renders an empty repeater.
|
||||
- **hooks path** — Hook includes are relative to `api/` directory: `!include hooks/myfile.js`.
|
||||
|
||||
Reference in New Issue
Block a user