491f495c66
- 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.
593 lines
15 KiB
Markdown
593 lines
15 KiB
Markdown
---
|
|
name: admin-ui-config
|
|
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
|
|
|
|
## When to use this skill
|
|
|
|
Use this skill when:
|
|
|
|
- Configuring how a collection appears in the tibi-admin UI
|
|
- 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
|
|
- Customizing the admin module (`frontend/src/admin.ts`)
|
|
|
|
## Reference source
|
|
|
|
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 collection/list UI.
|
|
|
|
```yaml
|
|
name: mycollection
|
|
meta:
|
|
label: { de: "Produkte", en: "Products" } # Sidebar label (i18n)
|
|
muiIcon: shopping_cart # Material UI icon name
|
|
group: shop # Group in admin sidebar
|
|
singleton:
|
|
enabled: false
|
|
hide: false # Set to true to hide the collection for non-admin users
|
|
preview:
|
|
label: name
|
|
secondary: price
|
|
```
|
|
|
|
### Preview
|
|
|
|
Use `meta.preview` as the universal entry representation for Nova lists, breadcrumbs, foreign-key widgets, and search result previews:
|
|
|
|
```yaml
|
|
preview: name
|
|
|
|
preview:
|
|
label: name
|
|
secondary: slug
|
|
badge: status
|
|
|
|
preview:
|
|
eval: "`${$this.firstName} ${$this.lastName}`"
|
|
```
|
|
|
|
## List presentation
|
|
|
|
For current Nova, use `meta.viewHint` plus `meta.preview` for collection/list presentation.
|
|
|
|
```yaml
|
|
meta:
|
|
viewHint: table
|
|
preview:
|
|
label: name
|
|
secondary: slug
|
|
badge: status
|
|
table:
|
|
- name
|
|
- source: status
|
|
label: Status
|
|
- source: author.name
|
|
label: Author
|
|
select:
|
|
- author.name
|
|
```
|
|
|
|
- `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.
|
|
|
|
---
|
|
|
|
## Field configuration
|
|
|
|
Each field in the `fields` array can have a `meta` section controlling its admin UI behavior.
|
|
|
|
### Basic field with meta
|
|
|
|
```yaml
|
|
fields:
|
|
- name: name
|
|
type: string
|
|
meta:
|
|
label: { de: "Name", en: "Name" }
|
|
helperText: { de: "Anzeigename", en: "Display name" }
|
|
position: main # "main" (default) or "sidebar"
|
|
```
|
|
|
|
### Field types
|
|
|
|
| YAML `type` | Admin widget (default) | Notes |
|
|
| ----------- | ---------------------- | --------------------------------------------- |
|
|
| `string` | Text input | Use `inputProps.multiline: true` for textarea |
|
|
| `number` | Number input | |
|
|
| `boolean` | Toggle/checkbox | |
|
|
| `date` | Date picker | |
|
|
| `object` | Nested field group | Requires `subFields` |
|
|
| `object[]` | Repeatable group | Requires `subFields`, drag-to-reorder |
|
|
| `string[]` | Tag input | |
|
|
| `file` | File upload | |
|
|
| `file[]` | Multi-file upload | |
|
|
|
|
### inputProps — widget customization
|
|
|
|
`inputProps` passes props directly to the field widget:
|
|
|
|
```yaml
|
|
# Multiline text (textarea)
|
|
- name: description
|
|
type: string
|
|
meta:
|
|
label: { de: "Beschreibung", en: "Description" }
|
|
inputProps:
|
|
multiline: true
|
|
rows: 5
|
|
|
|
# Number with min/max
|
|
- name: price
|
|
type: number
|
|
meta:
|
|
inputProps:
|
|
min: 0
|
|
max: 99999
|
|
step: 0.01
|
|
|
|
# Placeholder text
|
|
- name: email
|
|
type: string
|
|
meta:
|
|
inputProps:
|
|
placeholder: "name@example.com"
|
|
```
|
|
|
|
### Widget override
|
|
|
|
Override the default widget with `meta.widget`:
|
|
|
|
```yaml
|
|
- name: content
|
|
type: string
|
|
meta:
|
|
widget: richtext # Rich text editor (HTML)
|
|
|
|
- name: heroImage
|
|
type: file
|
|
meta:
|
|
widget: image # Image-focused file widget
|
|
|
|
- name: relatedPages
|
|
type: string[]
|
|
meta:
|
|
widget: foreignKeyChipArray
|
|
```
|
|
|
|
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
|
|
|
|
Static choices:
|
|
|
|
```yaml
|
|
- name: type
|
|
type: string
|
|
meta:
|
|
label: { de: "Typ", en: "Type" }
|
|
choices:
|
|
- id: page
|
|
name: { de: "Seite", en: "Page" }
|
|
- id: blog
|
|
name: { de: "Blog", en: "Blog" }
|
|
- id: product
|
|
name: { de: "Produkt", en: "Product" }
|
|
```
|
|
|
|
Dynamic choices from API:
|
|
|
|
```yaml
|
|
- name: category
|
|
type: string
|
|
meta:
|
|
choices:
|
|
endpoint: categories # Collection name
|
|
mapping:
|
|
id: id
|
|
name: name
|
|
```
|
|
|
|
### Foreign references
|
|
|
|
Link to entries in another collection:
|
|
|
|
```yaml
|
|
- name: author
|
|
type: string
|
|
meta:
|
|
label: { de: "Autor", en: "Author" }
|
|
foreign:
|
|
collection: users
|
|
id: id
|
|
sort: name
|
|
projection: name,email
|
|
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: image
|
|
downscale: # Auto-resize on upload
|
|
maxWidth: 1920
|
|
maxHeight: 1080
|
|
quality: 0.85
|
|
imageEditor: true # Enable crop/rotate editor
|
|
```
|
|
|
|
---
|
|
|
|
## Layout: position, sections, sidebar
|
|
|
|
### Sidebar placement
|
|
|
|
```yaml
|
|
- name: active
|
|
type: boolean
|
|
meta:
|
|
position: sidebar # Moves field to sidebar
|
|
|
|
- name: publishDate
|
|
type: date
|
|
meta:
|
|
position: "sidebar:publishing" # Sidebar with group key
|
|
```
|
|
|
|
### Sidebar groups (ordered)
|
|
|
|
Define sidebar group order in collection meta:
|
|
|
|
```yaml
|
|
meta:
|
|
sidebar:
|
|
- 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
|
|
|
|
```yaml
|
|
- name: seoTitle
|
|
type: string
|
|
meta:
|
|
section: SEO # Groups fields under a section header
|
|
|
|
- name: seoDescription
|
|
type: string
|
|
meta:
|
|
section: SEO
|
|
```
|
|
|
|
### Grid layout (columns)
|
|
|
|
Use `containerProps` for multi-column layout:
|
|
|
|
```yaml
|
|
- name: firstName
|
|
type: string
|
|
meta:
|
|
containerProps:
|
|
layout:
|
|
size: col-6 # Half width (12-column grid)
|
|
|
|
- name: lastName
|
|
type: string
|
|
meta:
|
|
containerProps:
|
|
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
|
|
|
|
### Object (nested group)
|
|
|
|
```yaml
|
|
- name: address
|
|
type: object
|
|
meta:
|
|
label: { de: "Adresse", en: "Address" }
|
|
subFields:
|
|
- name: street
|
|
type: string
|
|
- name: city
|
|
type: string
|
|
- name: zip
|
|
type: string
|
|
```
|
|
|
|
### Object array (repeatable blocks)
|
|
|
|
```yaml
|
|
- name: blocks
|
|
type: object[]
|
|
meta:
|
|
label: { de: "Inhaltsblöcke", en: "Content Blocks" }
|
|
widget: pagebuilder
|
|
preview: { eval: "`${$this.type}: ${$this.headline || ''}`" }
|
|
drillDown: true
|
|
subFields:
|
|
- name: type
|
|
type: string
|
|
meta:
|
|
choices:
|
|
- id: hero
|
|
name: Hero
|
|
- id: richtext
|
|
name: Richtext
|
|
- name: headline
|
|
type: string
|
|
- name: hide
|
|
type: boolean
|
|
```
|
|
|
|
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:
|
|
|
|
```yaml
|
|
- name: variants
|
|
type: object[]
|
|
meta:
|
|
drillDown: true # Opens as sub-page instead of inline
|
|
```
|
|
|
|
---
|
|
|
|
## Admin module (frontend/src/admin.ts)
|
|
|
|
The `admin.ts` file exports custom Svelte components for injection into the tibi-admin UI. Components are rendered inside Shadow DOM to isolate styles.
|
|
|
|
```typescript
|
|
import type { SvelteComponent } from "svelte"
|
|
|
|
function getRenderedElement(
|
|
component: typeof SvelteComponent,
|
|
options?: { props: { [key: string]: any }; addCss?: string[] },
|
|
nestedElements?: { tagName: string; className?: string }[]
|
|
) {
|
|
// Creates a Shadow DOM container, mounts the Svelte component inside
|
|
// addCss: CSS files to inject into Shadow DOM
|
|
// nestedElements: wrapper elements inside Shadow DOM
|
|
}
|
|
|
|
export { getRenderedElement }
|
|
```
|
|
|
|
Build with `yarn build`. The output includes the admin module and is loaded by tibi-admin-nova as a custom module.
|
|
|
|
**Use case:** Custom dashboard widgets, preview components, or field widgets that require Svelte rendering inside the admin UI.
|
|
|
|
---
|
|
|
|
## Complete collection example
|
|
|
|
```yaml
|
|
name: products
|
|
meta:
|
|
label: { de: "Produkte", en: "Products" }
|
|
muiIcon: inventory_2
|
|
group: shop
|
|
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:
|
|
- group: publishing
|
|
label: { de: "Veröffentlichung", en: "Publishing" }
|
|
- group: seo
|
|
label: { de: "SEO", en: "SEO" }
|
|
|
|
permissions:
|
|
public:
|
|
methods:
|
|
get: true
|
|
user:
|
|
methods:
|
|
get: true
|
|
post: true
|
|
put: true
|
|
delete: true
|
|
|
|
fields:
|
|
- name: active
|
|
type: boolean
|
|
meta:
|
|
label: { de: "Aktiv", en: "Active" }
|
|
position: "sidebar:publishing"
|
|
- name: name
|
|
type: string
|
|
meta:
|
|
label: { de: "Name", en: "Name" }
|
|
- name: sku
|
|
type: string
|
|
meta:
|
|
label: { de: "Artikelnummer", en: "SKU" }
|
|
containerProps:
|
|
layout:
|
|
size: col-6
|
|
- name: price
|
|
type: number
|
|
meta:
|
|
label: { de: "Preis", en: "Price" }
|
|
inputProps:
|
|
min: 0
|
|
step: 0.01
|
|
containerProps:
|
|
layout:
|
|
size: col-6
|
|
- name: category
|
|
type: string
|
|
meta:
|
|
label: { de: "Kategorie", en: "Category" }
|
|
choices:
|
|
- id: electronics
|
|
name: { de: "Elektronik", en: "Electronics" }
|
|
- id: clothing
|
|
name: { de: "Kleidung", en: "Clothing" }
|
|
- name: description
|
|
type: string
|
|
meta:
|
|
label: { de: "Beschreibung", en: "Description" }
|
|
inputProps:
|
|
multiline: true
|
|
rows: 4
|
|
- name: image
|
|
type: file
|
|
meta:
|
|
label: { de: "Produktbild", en: "Product Image" }
|
|
widget: image
|
|
downscale:
|
|
maxWidth: 1200
|
|
quality: 0.85
|
|
- name: seoTitle
|
|
type: string
|
|
meta:
|
|
label: { de: "SEO Titel", en: "SEO Title" }
|
|
position: "sidebar:seo"
|
|
- name: seoDescription
|
|
type: string
|
|
meta:
|
|
label: { de: "SEO Beschreibung", en: "SEO Description" }
|
|
position: "sidebar:seo"
|
|
inputProps:
|
|
multiline: true
|
|
rows: 3
|
|
```
|
|
|
|
---
|
|
|
|
## Common pitfalls
|
|
|
|
- **`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.
|