forked from cms/tibi-svelte-starter
✨ feat: enhance admin UI configuration and SSR handling
- Add support for number chip arrays and JSON editor in admin UI config. - Introduce pagebuilder block registry for Svelte components in admin previews. - Implement custom role names and a 3-layer cascade model for field-level permissions. - Add CORS configuration hierarchy for better API security. - Update project setup instructions for admin token and config management. - Improve SSR 404 signaling with proper context handling in NotFound component. - Refactor routing structure to separate NotFound page into its own route.
This commit is contained in:
@@ -108,6 +108,7 @@ fields:
|
||||
| ----------- | ---------------------- | --------------------------------------------- |
|
||||
| `string` | Text input | Use `inputProps.multiline: true` for textarea |
|
||||
| `number` | Number input | |
|
||||
| `number[]` | Number chip array | Multiple numeric values |
|
||||
| `boolean` | Toggle/checkbox | |
|
||||
| `date` | Date picker | |
|
||||
| `object` | Nested field group | Requires `subFields` |
|
||||
@@ -115,6 +116,7 @@ fields:
|
||||
| `string[]` | Tag input | |
|
||||
| `file` | File upload | |
|
||||
| `file[]` | Multi-file upload | |
|
||||
| `any` | JSON editor | For mixed/arbitrary data |
|
||||
|
||||
### inputProps — widget customization
|
||||
|
||||
@@ -451,7 +453,79 @@ For complex nested objects, use `drillDown` to render them as a sub-page:
|
||||
|
||||
## 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.
|
||||
The `admin.ts` file exports the **pagebuilder block registry** and optional custom Svelte components for the tibi-admin UI. This is how the admin preview renders your Svelte blocks.
|
||||
|
||||
### Pagebuilder block registry
|
||||
|
||||
The current starter uses `createContentBlockDefinition()` to register each block type. This mounts real Svelte block components into Shadow DOM for admin previews:
|
||||
|
||||
```typescript
|
||||
import { mount, unmount, type Component, type SvelteComponent } from "svelte"
|
||||
import BlockRenderer from "./blocks/BlockRenderer.svelte"
|
||||
|
||||
// Creates a block definition that renders the same Svelte component
|
||||
// used in the public frontend. The block is mounted inside Shadow DOM
|
||||
// for style isolation.
|
||||
function createContentBlockDefinition(presentation: {
|
||||
label: string
|
||||
icon: string
|
||||
color: string
|
||||
}) {
|
||||
return {
|
||||
css: [previewCssUrl], // CSS files to inject into Shadow DOM
|
||||
label: presentation.label,
|
||||
icon: presentation.icon,
|
||||
color: presentation.color,
|
||||
previewStyles: {
|
||||
"background-color": "white",
|
||||
},
|
||||
render(container, row, context) {
|
||||
// Mount the Svelte component inside the admin preview
|
||||
const target = document.createElement("div")
|
||||
container.appendChild(target)
|
||||
|
||||
let mountedComponent = mount(BlockRenderer as Component<any>, {
|
||||
target,
|
||||
props: { blocks: [row], isAdminPreview: true },
|
||||
})
|
||||
|
||||
return {
|
||||
update(nextRow) {
|
||||
unmount(mountedComponent)
|
||||
target.innerHTML = ""
|
||||
mountedComponent = mount(BlockRenderer as Component<any>, {
|
||||
target,
|
||||
props: { blocks: [nextRow], isAdminPreview: true },
|
||||
})
|
||||
},
|
||||
destroy() {
|
||||
unmount(mountedComponent)
|
||||
target.remove()
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const blockRegistry = {
|
||||
hero: createContentBlockDefinition({ label: "Hero", icon: "image", color: "#1d4ed8" }),
|
||||
richtext: createContentBlockDefinition({ label: "Richtext", icon: "article", color: "#7c3aed" }),
|
||||
// ... add new blocks here
|
||||
}
|
||||
|
||||
export { blockRegistry }
|
||||
```
|
||||
|
||||
**Key points:**
|
||||
- Each registry entry wraps the Svelte `BlockRenderer` to render the block in the admin preview.
|
||||
- The `row` object is the block data (same shape as `ContentBlockEntry`).
|
||||
- Preview data may contain hydrated `_lookup.<fieldPath>` foreign key data and absolute file URLs — do not prepend `apiBase` or attempt re-fetching.
|
||||
- The `previewCssUrl` loads the project's `index.css` into Shadow DOM so block styles apply.
|
||||
- After adding blocks to the registry, run `yarn build` so `frontend/dist/admin.mjs` is regenerated.
|
||||
|
||||
### Custom Svelte components (advanced)
|
||||
|
||||
For custom dashboard widgets, preview components, or field widgets that require Svelte rendering inside the admin UI, use `getRenderedElement()`:
|
||||
|
||||
```typescript
|
||||
import type { SvelteComponent } from "svelte"
|
||||
@@ -462,16 +536,14 @@ function getRenderedElement(
|
||||
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.
|
||||
### Build
|
||||
|
||||
**Use case:** Custom dashboard widgets, preview components, or field widgets that require Svelte rendering inside the admin UI.
|
||||
Run `yarn build`. The admin module (`frontend/src/admin.ts`) is compiled into `frontend/dist/admin.mjs` as part of the esbuild build pipeline (the same build produces both `index.mjs` for the SPA and `admin.mjs` for the admin module). tibi-admin-nova loads this module from the project's asset path (`/_/assets/dist/admin.mjs`). The `ADMIN_ASSET_VERSION` from `config.yml.env` is appended as a query parameter for cache busting: `admin.mjs?v=${ADMIN_ASSET_VERSION}`.
|
||||
|
||||
---
|
||||
|
||||
@@ -513,7 +585,7 @@ permissions:
|
||||
get: true
|
||||
post: true
|
||||
put: true
|
||||
delete: true
|
||||
delete: false # usually false for real editorial workflows
|
||||
|
||||
fields:
|
||||
- name: active
|
||||
@@ -583,6 +655,33 @@ fields:
|
||||
|
||||
---
|
||||
|
||||
## Indexes and search
|
||||
|
||||
For production collections with many entries, consider adding indexes in the YAML:
|
||||
|
||||
```yaml
|
||||
name: products
|
||||
indexes:
|
||||
- name: price_sort
|
||||
key: [price]
|
||||
- name: category_active
|
||||
key: [category, -active] # -prefix for descending
|
||||
- name: slug_unique
|
||||
key: [slug]
|
||||
unique: true
|
||||
```
|
||||
|
||||
Search configurations can be added for advanced text/vector search:
|
||||
|
||||
```yaml
|
||||
search:
|
||||
- name: default
|
||||
mode: text
|
||||
fields: [name, description]
|
||||
```
|
||||
|
||||
See `tibi-server/docs/04-collections.md` (sections on indexes and search config) for full reference.
|
||||
|
||||
## Common pitfalls
|
||||
|
||||
- **`meta.label` supports both strings and i18n objects** — Use i18n objects only when the collection or field label must be localized.
|
||||
|
||||
Reference in New Issue
Block a user