--- 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. --- # admin-ui-config ## When to use this skill Use this skill when: - Configuring how a collection appears in the tibi-admin UI - Setting up table/list/card views for a collection - 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` (1296 lines). Always consult this file for the full API. This skill provides a practical summary. --- ## Collection meta configuration The `meta` key in a collection YAML controls how the collection appears in the admin sidebar and list views. ```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: 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 ``` ### Row identification `rowIdentTpl` uses Twig syntax with field names. Used in admin list to identify entries: ```yaml rowIdentTpl: { twig: "{{ name }}" } # Simple rowIdentTpl: { twig: "{{ type }} — {{ language }}" } # Combined ``` --- ## 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 ```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 ``` ### 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 ``` --- ## 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: color type: string meta: widget: color # Color picker - name: image type: string meta: widget: medialib # Media library picker ``` Common widget types: `text` (default), `richtext`, `color`, `medialib`, `code`, `markdown`, `password`, `hidden`. ### 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: { twig: "{{ name }} <{{ email }}>" } autoFill: # Auto-fill other fields on selection - source: email target: authorEmail ``` ### Image fields ```yaml - name: image type: file meta: widget: medialib 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:Veröffentlichung" # Sidebar with group header ``` ### Sidebar groups (ordered) Define sidebar group order in collection meta: ```yaml meta: sidebar: - Veröffentlichung - SEO - Einstellungen ``` ### 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 ``` --- ## 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" } preview: { eval: "item.type + ': ' + (item.headline || '')" } 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 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:admin`. The output 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 defaultSort: "-insertTime" rowIdentTpl: { twig: "{{ name }} ({{ sku }})" } 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 permissions: public: methods: get: true user: methods: get: true post: true 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" - 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: medialib 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` 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). - **`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`.