diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..f5f4f07 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,36 @@ +# CODEOWNERS — auto-request reviewers based on path domain. +# Last-match-wins; broad rules first, specific overrides below. +# Created 2026-05-03 from the OR-abstraction audit follow-up. + +# Default: every named codeowner reviews unmatched paths. +* @rubenvdlinde @rjzondervan @Rem-Dam @remko48 @WilcoLouwerse @bbrands02 @SudoThijn + +# Backend (PHP) — services, controllers, mappers, db, migration, etc. +lib/ @bbrands02 @rjzondervan @WilcoLouwerse +appinfo/ @bbrands02 @rjzondervan @WilcoLouwerse +**/*.php @bbrands02 @rjzondervan @WilcoLouwerse +phpcs.xml @bbrands02 @rjzondervan @WilcoLouwerse +phpmd.xml @bbrands02 @rjzondervan @WilcoLouwerse +phpstan.neon @bbrands02 @rjzondervan @WilcoLouwerse +phpstan-baseline.neon @bbrands02 @rjzondervan @WilcoLouwerse +phpmd.baseline.xml @bbrands02 @rjzondervan @WilcoLouwerse +composer.json @bbrands02 @rjzondervan @WilcoLouwerse +composer.lock @bbrands02 @rjzondervan @WilcoLouwerse + +# Frontend (Vue / TS / JS) — components, stores, pages, build config. +src/ @SudoThijn @remko48 +**/*.vue @SudoThijn @remko48 +**/*.ts @SudoThijn @remko48 +**/*.js @SudoThijn @remko48 +package.json @SudoThijn @remko48 +package-lock.json @SudoThijn @remko48 +jest.config.js @SudoThijn @remko48 +playwright.config.ts @SudoThijn @remko48 +webpack.config.js @SudoThijn @remko48 +babel.config.js @SudoThijn @remko48 + +# Specs / docs / ADRs / openspec. +openspec/ @rubenvdlinde @Rem-Dam +docs/ @rubenvdlinde @Rem-Dam +**/*.md @rubenvdlinde @Rem-Dam +README.md @rubenvdlinde @Rem-Dam diff --git a/.github/workflows/branch-protection.yml b/.github/workflows/branch-protection.yml index a6d20bb..28065f9 100644 --- a/.github/workflows/branch-protection.yml +++ b/.github/workflows/branch-protection.yml @@ -5,5 +5,5 @@ on: branches: [main, beta] jobs: - check: + branch-protection: uses: ConductionNL/.github/.github/workflows/branch-protection.yml@main diff --git a/.github/workflows/documentation-beta.yml b/.github/workflows/documentation-beta.yml new file mode 100644 index 0000000..658d9ed --- /dev/null +++ b/.github/workflows/documentation-beta.yml @@ -0,0 +1,40 @@ +name: Documentation (Beta) + +on: + push: + branches: + - beta + +jobs: + deploy-beta: + name: Deploy Beta Styleguide + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node.js 20 + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install and build component styleguide + timeout-minutes: 10 + run: | + cd styleguide + npm ci + npm run build + + - name: Deploy styleguide to /beta/ on GitHub Pages + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./styleguide/build + publish_branch: gh-pages + destination_dir: beta/styleguide + user_name: 'github-actions[bot]' + user_email: 'github-actions[bot]@users.noreply.github.com' + keep_files: true diff --git a/.github/workflows/documentation-dev.yml b/.github/workflows/documentation-dev.yml new file mode 100644 index 0000000..8228662 --- /dev/null +++ b/.github/workflows/documentation-dev.yml @@ -0,0 +1,41 @@ +name: Documentation (Dev) + +on: + push: + branches: + - development + +jobs: + deploy-dev: + name: Deploy Dev Styleguide + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Setup Node.js 20 + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install and build component styleguide + timeout-minutes: 10 + run: | + cd styleguide + npm ci + npm run build + + - name: Deploy styleguide to /dev/ on GitHub Pages + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./styleguide/build + + publish_branch: gh-pages + destination_dir: dev/styleguide + user_name: 'github-actions[bot]' + user_email: 'github-actions[bot]@users.noreply.github.com' + keep_files: true diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 7a5e2c6..a659273 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -26,6 +26,13 @@ jobs: with: node-version: '20' + - name: Install and build component styleguide + timeout-minutes: 10 + run: | + cd styleguide + npm ci + npm run build + - name: Clear build cache and install dependencies timeout-minutes: 3 run: | @@ -43,6 +50,12 @@ jobs: exit 1 fi + - name: Copy styleguide into Docusaurus build + run: | + mkdir -p docusaurus/build/styleguide + cp -r styleguide/build/. docusaurus/build/styleguide/ + + - name: Create .nojekyll file run: | cd docusaurus/build @@ -58,7 +71,7 @@ jobs: user_email: 'github-actions[bot]@users.noreply.github.com' force_orphan: false allow_empty_commit: true - keep_files: false + keep_files: true - name: Verify deployment run: | diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 84474e2..2f8062d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,5 +1,21 @@ name: Release +# Auto-publish @conduction/nextcloud-vue to npm via semantic-release. +# +# Authentication: npm Trusted Publisher (OIDC). The runner mints a +# short-lived ID token via `id-token: write`, the npm plugin exchanges it +# at the registry for a one-shot publish credential, and provenance is +# attested via NPM_CONFIG_PROVENANCE=true. There is no NPM_TOKEN secret +# to rotate. +# +# Trust config on npmjs.com expects this workflow at the path +# `.github/workflows/release.yml` in `ConductionNL/nextcloud-vue`. If you +# rename or move this file, update the npm Trusted Publisher config. +# +# Node 24 is the floor: semantic-release@24 requires Node ^22.14 || ≥24.10 +# and the Trusted Publisher publish path uses npm CLI's own OIDC support, +# which lands in npm 11.x (bundled with Node 24). + on: push: branches: [main, beta] @@ -17,7 +33,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 24 cache: 'npm' - run: npm ci - run: npm run lint @@ -27,6 +43,13 @@ jobs: name: Release needs: quality runs-on: ubuntu-latest + permissions: + contents: write # @semantic-release/git pushes the chore(release) bump back to beta/main + issues: write + pull-requests: write + id-token: write # npm Trusted Publisher (OIDC) + provenance attestation + env: + NPM_CONFIG_PROVENANCE: 'true' steps: - uses: actions/checkout@v4 with: @@ -35,13 +58,13 @@ jobs: - uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 24 cache: 'npm' + registry-url: 'https://registry.npmjs.org' - run: npm ci - name: Release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} run: npx semantic-release diff --git a/.gitignore b/.gitignore index f79db30..3428abd 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,7 @@ dist/ # Docusaurus build output docusaurus/build .docusaurus + +# Styleguide build output +styleguide/build +.styleguide \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 372b5b3..f675313 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -28,4 +28,7 @@ "[php]": { "editor.defaultFormatter": "DEVSENSE.phptools-vscode" }, + "[markdown]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, } diff --git a/CLAUDE.md b/CLAUDE.md index c0ac082..279c1d4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -18,14 +18,23 @@ import { useObjectStore } from '@conduction/nextcloud-vue' import '@conduction/nextcloud-vue/src/css/index.css' ``` +Consumer apps MUST also call `registerTranslations()` once in `main.js` (alongside `registerIcons({})`) **before** `new Vue().$mount(...)` — without it, library-rendered strings stay in English even when the user's Nextcloud language is Dutch. See [docs/getting-started.md](docs/getting-started.md#register-library-translations-required). + ### Available Components **Layout & Pages** -- `CnIndexPage` — Top-level schema-driven index page (table/cards, pagination, mass actions, dialogs) -- `CnDetailPage` — Generic detail/overview page with stats table and flexible content slots (simpler alternative to CnIndexPage) +- `CnIndexPage` — Top-level schema-driven index page (table/cards, pagination, mass actions, dialogs). Overridable via `#header` and `#actions` slots. +- `CnDetailPage` — Generic detail/overview page with stats table and flexible content slots. Overridable via `#header` and `#actions` slots. - `CnPageHeader` — Page header with icon, title, description - `CnActionsBar` — Action bar with add button, mass actions, view toggle, search +**Manifest Renderer (JSON-driven app shell)** +- `CnAppRoot` — Top-level app wrapper. Orchestrates loading → dependency-check → shell phases. Provides `cnManifest`, `cnCustomComponents`, `cnTranslate` to descendants. Slots: `#loading`, `#dependency-missing`, `#menu`, `#header-actions`, `#sidebar`, `#footer` — each independently overridable. Use this when adopting the full manifest pattern; lower tiers (just `useAppManifest`, or `+ CnPageRenderer`, or `+ CnAppNav`) are also supported. +- `CnAppNav` — Manifest-driven `NcAppNavigation`. Reads `manifest.menu[]`; sorts by `order`; filters by `permission`; one level of `children[]`. Accepts `manifest`, `translate`, `permissions` as props (with inject fallback) for standalone use. +- `CnPageRenderer` — Type dispatcher mounted inside ``. Matches `$route.name === page.id`, dispatches by `page.type` (`index | detail | dashboard | custom`) using `defineAsyncComponent` for tree-shaking. Forwards `page.config` as props; resolves `page.headerComponent` / `page.actionsComponent` against the customComponents registry. +- `CnAppLoading` — Default loading screen (logo slot + NcLoadingIcon + message). Used by CnAppRoot's `#loading` phase. +- `CnDependencyMissing` — Default dependency-missing screen (lists missing apps with install/enable links). Used by CnAppRoot's `#dependency-missing` phase. + **Data Display** - `CnDetailGrid` — Data-driven label-value grid with grid and horizontal layout modes - `CnDataTable` — Sortable data table with selection, loading, empty states @@ -92,6 +101,7 @@ import '@conduction/nextcloud-vue/src/css/index.css' - `buildQueryString(params)` — Build URL query string from params object - `parseResponseError(response)` — Extract error message from API response - `networkError()` / `genericError()` — Standard error message helpers +- `validateManifest(manifest)` — Validate an app manifest against the JSON Schema. Returns `{ valid, errors }`. Use at build time or in test fixtures; the same validator runs at runtime inside `useAppManifest`. ### Available Store - `useObjectStore` — Generic Pinia store for OpenRegister objects (CRUD, pagination, search, caching) @@ -103,6 +113,8 @@ import '@conduction/nextcloud-vue/src/css/index.css' - `useFileSelection(options)` — File upload/drop handling - `useDashboardView(options)` — Dashboard state: widget defs, layout, NC widget loading, add/remove/persist - `useContextMenu()` — Right-click context menu positioning and state (cursor CSS vars, open/close, action helpers) +- `useAppManifest(appId, bundledManifest, options?)` — Load + validate the app manifest. Returns `{ manifest, isLoading, validationErrors }`. Synchronous bundled load + async backend-merge stub (silent fallback on 4xx / network errors); validates via `validateManifest`. Pass `options.endpoint` or `options.fetcher` to override the backend URL or inject a mock. +- `useAppStatus(appId)` — Check whether a Nextcloud app is installed and enabled via `@nextcloud/capabilities`. Returns `{ installed, enabled, loading }`. Cached per appId for the page lifetime. CnAppRoot calls this once per `manifest.dependencies` entry to drive the dependency-check phase. ### CnIndexPage Dialog Override System @@ -113,7 +125,7 @@ CnIndexPage has built-in single-object dialogs (Delete, Copy, Form) that are **o - `#copy-dialog="{ item, close }"` — Replace copy dialog - `#form-dialog="{ item, schema, close }"` — Replace create/edit dialog (use CnFormDialog or CnAdvancedFormDialog) 2. **Form content override** — `#form-fields` replaces the form inside the built-in CnFormDialog -3. **Per-field override** — `#field-{key}` inside CnFormDialog replaces a single field +3. **Per-field override** — `#field-{key}` inside CnFormDialog replaces a single field. For JSON / code-editor fields this slot is rarely needed: set `widget: 'json'` (structured value, parses on input) or `widget: 'code'` (raw string + `field.language` for highlighting) on the schema property and CnFormDialog renders `CnJsonViewer` automatically. 4. **Per-field option rendering** — `#field-{key}-option` and `#field-{key}-selected-option` customize dropdown option display for select/multiselect/tags fields Key events emitted by CnIndexPage: @@ -175,6 +187,34 @@ const DEFAULT_LAYOUT = [ ``` +### JSON Manifest Renderer + +Apps can declare their entire shell — routes, navigation, page configuration, dependencies — in a single `src/manifest.json`. The library reads it and renders the app. Adoption is incremental (four tiers from "just `useAppManifest`" to "full `CnAppRoot` shell"); a custom menu component can replace the default `CnAppNav` via the `#menu` slot. + +Minimal manifest: + +```json +{ + "$schema": "https://raw.githubusercontent.com/ConductionNL/nextcloud-vue/main/src/schemas/app-manifest.schema.json", + "version": "1.0.0", + "dependencies": ["openregister"], + "menu": [ + { "id": "decisions", "label": "myapp.menu.decisions", "icon": "icon-checkmark", "route": "decisions-index", "order": 10 } + ], + "pages": [ + { "id": "decisions-index", "route": "/decisions", "type": "index", "title": "myapp.decisions.title", + "config": { "register": "decisions", "schema": "decision", "columns": ["title", "status"] } }, + { "id": "decisions-detail", "route": "/decisions/:id", "type": "detail", "title": "myapp.decisions.detail", + "config": { "register": "decisions", "schema": "decision" } }, + { "id": "settings", "route": "/settings", "type": "custom", "title": "myapp.settings.title", "component": "SettingsPage" } + ] +} +``` + +`page.id` is also the vue-router route name; CnPageRenderer matches by `$route.name === page.id`. The `type` enum is closed (`index | detail | dashboard | custom`) — bespoke pages use `type: "custom"` with a registry component. + +See `examples/manifest-demo/manifest.json` for a fuller reference and `docs/migrating-to-manifest.md` for tier-by-tier adoption guidance. + ## Rules for Modifying Components 1. **NEVER break existing prop interfaces** — new props MUST have defaults @@ -184,7 +224,7 @@ const DEFAULT_LAYOUT = [ 5. **Run `npm test` before submitting changes** 6. **CSS class prefix**: All classes use `cn-` prefix to avoid collisions 7. **Theming**: Use Nextcloud CSS variables only (`var(--color-primary-element)`, `var(--color-border)`, etc.). Do NOT reference `--nldesign-*` variables — the nldesign app overrides Nextcloud's own variables, so theming works automatically. -8. **Translation**: Components accept pre-translated strings via props with English defaults. Never import `t()` from a specific app. +8. **Always update docs when you add, rename, or remove a prop, event, or slot** — edit `docs/components/cn-.md` in the same change. The CI `check:docs` step (run via `npm run check:docs`) verifies both that a doc file exists AND that every SFC prop and named slot appears in it. Run it locally before committing to catch gaps. ## Adding New Components @@ -268,3 +308,13 @@ scripts/ This library is used by: OpenRegister, OpenCatalogi, Procest, Pipelinq, MyDash. Changes here affect all of them. Test carefully. + +Every consumer's `main.js` must include: + +```js +import { registerIcons, registerTranslations } from '@conduction/nextcloud-vue' +registerIcons({ /* app-specific icons */ }) +registerTranslations() +``` + +`registerTranslations()` is a required bootstrap call — without it, the library falls back to English regardless of the user's Nextcloud language. diff --git a/docs/components/cn-advanced-form-dialog.md b/docs/components/cn-advanced-form-dialog.md index ad328de..1588352 100644 --- a/docs/components/cn-advanced-form-dialog.md +++ b/docs/components/cn-advanced-form-dialog.md @@ -13,7 +13,7 @@ Use **CnAdvancedFormDialog** when you need: - Raw JSON view and editing with validation - Metadata display (id, created, updated) in edit mode -Use **CnFormDialog** when you need a simpler form with auto-generated fields and standard widgets (text, select, checkbox, etc.) without the table/JSON tabs. +Use **CnFormDialog** when you need a simpler form with auto-generated fields and standard widgets (text, select, checkbox, etc.) without the table/JSON tabs. CnFormDialog also has native `json` and `code` widgets (backed by [`CnJsonViewer`](cn-json-viewer.md)) for single-field JSON/code editing — reach for those before CnAdvancedFormDialog when the richer properties table and metadata tab aren't needed. --- @@ -35,7 +35,7 @@ The dialog uses the same **two-phase pattern** as other CRUD dialogs: after the |------|------|---------|-------------| | `schema` | Object | `null` | JSON Schema used to derive property list, types, and labels (e.g. `schema.properties`, `schema.required`, `schema.title`) | | `item` | Object | `null` | Existing object for edit mode; `null` for create mode | -| `dialogTitle` | String | `''` | Override dialog title; default is "Create {schemaTitle}" or "Edit {schemaTitle}" | +| `dialogTitle` | String | `''` | Override dialog title; default is "Create \{schemaTitle\}" or "Edit \{schemaTitle\}" | | `nameField` | String | `'title'` | Property used as display name (e.g. in breadcrumbs or other UI) | | `successText` | String | `''` | Message shown in result phase on success; default "\{schemaTitle\} saved successfully." | | `cancelLabel` | String | `'Cancel'` | Label for cancel button in form phase | @@ -47,7 +47,7 @@ The dialog uses the same **two-phase pattern** as other CRUD dialogs: after the | `showPropertiesTable` | Boolean | `true` | Show the Properties tab | | `showJsonTab` | Boolean | `true` | Show the Data (JSON) tab | | `showMetadataTab` | Boolean | `null` | Force show/hide Metadata tab; `null` = show only when `item` is set (edit mode) | -| `editablePropertyTypes` | Array | `null` | Schema types that are editable in the table; default `['string','number','integer','boolean']` | +| `editablePropertyTypes` | Array | `null` | Schema types that are editable in the table; default `['string','number','integer','boolean','array','object']` | | `validationDisplay` | String | `'indicator'` | `'indicator'` = show validation state on rows (valid/invalid/new/warning); `'none'` = no indicator | | `jsonEditorDark` | Boolean | `false` | Use dark theme for the CodeMirror JSON editor | @@ -66,11 +66,11 @@ The dialog uses the same **two-phase pattern** as other CRUD dialogs: after the | Slot | Scope | Description | |------|-------|-------------| -| `#form` | formData, updateField, objectProperties, jsonData, updateJsonFromExternal, isValidJson | Replace the entire form content (all tabs). Use for fully custom UI while still using the dialog chrome and confirm/close flow. | +| `#form` | formData, updateField, objectProperties, jsonData, updateJson, isValidJson | Replace the entire form content (all tabs). Use for fully custom UI while still using the dialog chrome and confirm/close flow. | | `#register-schema-selection` | proceed | Optional step before the main tabs (e.g. choose register/schema). When this slot is provided, the main tabs are not shown until the consumer calls `proceed()`. | | `#tab-properties` | formData, updateField, objectProperties, selectedProperty, handleRowClick, getPropertyDisplayName, getPropertyValidationClass, isPropertyEditable, validationDisplay | Override the Properties tab content. Default is the built-in properties table. | | `#tab-metadata` | item, formData | Override the Metadata tab content. Default is a table with ID, Created, Updated. | -| `#tab-data` | jsonData, updateJsonFromExternal, isValid, formatJSON | Override the Data (JSON) tab content. Default is CodeMirror + "Format JSON" button. | +| `#tab-data` | jsonData, updateJson, isValid, formatJSON | Override the Data (JSON) tab content. Default is CodeMirror + "Format JSON" button. | | `#actions-left` | — | Content to the left of the Cancel/Close button in the dialog footer | | `#actions-right` | — | Content to the right of the primary Confirm button in the dialog footer | @@ -95,7 +95,7 @@ The dialog uses the same **two-phase pattern** as other CRUD dialogs: after the ## Properties table behavior -- **Editable types**: By default only `string`, `number`, `integer`, `boolean` are editable in the table; others are read-only and displayed as formatted values or JSON. Use `editablePropertyTypes` to restrict or extend. +- **Editable types**: By default `string`, `number`, `integer`, `boolean`, `array`, and `object` are editable in the table — strings get type-specific HTML5 inputs from their `format`, arrays get a comma-separated input, and objects get a CodeMirror JSON editor. Use `editablePropertyTypes` to restrict (e.g. lock arrays and objects to read-only). - **Row selection**: Clicking a row selects it and shows an inline input for editable properties; clicking outside or on another row commits the value. - **Exception: boolean properties** always render as a visible toggle switch — no row click is needed to activate editing. - **Validation**: Rows can get CSS classes for valid/invalid/new/warning when `validationDisplay === 'indicator'`. Properties with `const` or `immutable` (with existing value) are not editable and show a lock icon. @@ -129,11 +129,52 @@ This means the directive is self-contained — it works even if the consuming ap --- +## Live demo + +```vue + + +``` + ## Usage examples ### Standalone (emit confirm, parent saves) -```vue +```vue {static} + +``` + +## Usage + +```vue {static} +``` + +### Append domain-specific rows + +```vue + +``` + +### Replace defaults entirely + +```vue + +``` diff --git a/docs/components/cn-object-data-widget.md b/docs/components/cn-object-data-widget.md index 0c54e78..0c2c280 100644 --- a/docs/components/cn-object-data-widget.md +++ b/docs/components/cn-object-data-widget.md @@ -6,9 +6,10 @@ Schema-driven editable data grid widget. Displays object properties in a CSS gri ```vue + title="Character info" + :schema="schema" + :object-data="character" + object-type="characters" /> ``` ## Props @@ -16,13 +17,16 @@ Schema-driven editable data grid widget. Displays object properties in a CSS gri | Prop | Type | Default | Description | |------|------|---------|-------------| | `title` | `String` | `'Data'` | Widget title in the card header | -| `icon` | `Object\|Function` | `null` | Optional MDI icon component | -| `object-data` | `Object` | *required* | The object to display and edit | -| `schema` | `Object` | `null` | JSON Schema defining properties | -| `store-id` | `String` | `null` | Pinia store ID for save operations | -| `editable` | `Boolean` | `true` | Whether inline editing is enabled | -| `columns` | `Number` | `2` | Number of grid columns | -| `overrides` | `Object` | `{}` | Per-property overrides (see below) | +| `icon` | `Object\|Function` | `null` | Optional MDI icon component for the header | +| `object-data` | `Object` | *required* | The object to display and edit. Keys must match the schema property keys. | +| `schema` | `Object` | *required* | JSON Schema defining properties. Must have a `properties` field. | +| `object-type` | `String` | `''` | Registered object type slug in the objectStore. Required for saving via `objectStore.saveObject()`. | +| `store` | `Object` | `null` | Optional objectStore instance. When provided, used directly for saving instead of auto-detecting via Pinia. | +| `overrides` | `Object` | `{}` | Per-property configuration overrides (see below) | +| `columns` | `Number` | `3` | Number of grid columns | +| `editable` | `Boolean` | `true` | Whether inline editing is enabled globally | +| `exclude` | `Array` | `[]` | Property keys to hide from display | +| `include` | `Array` | `null` | Property keys to show (whitelist — all others hidden) | | `save-label` | `String` | `'Save'` | Label for the save button | | `discard-label` | `String` | `'Discard'` | Label for the discard button | | `empty-label` | `String` | `'No data available'` | Label when no properties are found | @@ -31,15 +35,18 @@ Schema-driven editable data grid widget. Displays object properties in a CSS gri | Slot | Scoped props | Description | |------|-------------|-------------| -| `#header-actions` | — | Extra buttons in the widget header (right side, next to save/discard) | -| `#field-{key}` | `{ field, value, update, cancel }` | Override the editor for a specific property | +| `#actions` | — | Extra buttons in the widget header (right side, next to save/discard) | +| `#field-{key}` | `{ field, value, update, cancel }` | Override the inline editor for a specific property | +| `#display-{key}` | `{ field, value, raw }` | Override the display (read-only) view for a specific property | ## Events | Event | Payload | Description | |-------|---------|-------------| -| `@save` | `{ data }` | Emitted after a successful save | -| `@error` | `{ error }` | Emitted when save fails | +| `@saved` | result object | Emitted after a successful objectStore save | +| `@save-error` | error message | Emitted when the objectStore save fails | +| `@save` | merged data object | Emitted when no `objectType` is set — lets the parent handle the save | +| `@discard` | — | Emitted when the user clicks the discard button | ## Property overrides @@ -48,12 +55,14 @@ The `overrides` prop accepts per-property configuration: ```js { propertyKey: { - order: 1, // Sort order - gridSpan: 2, // Number of grid columns to span - hidden: false, // Whether to hide this property - editable: true, // Whether this property can be edited - label: 'Custom', // Override the label - widget: 'textarea', // Override the widget type + order: 1, // Sort order (lower = first) + gridColumn: 2, // Number of grid columns to span + gridRow: 2, // Number of grid rows to span + hidden: false, // Whether to hide this property + editable: true, // Whether this property can be edited + label: 'Custom', // Override the display label + widget: 'textarea', // Override the widget type for editing + enum: [...], // Override enum values for select/multiselect } } ``` @@ -65,6 +74,8 @@ The widget auto-detects the editor based on the JSON Schema property type: | Widget | Schema type | Editor | |--------|-------------|--------| | `text` | `string` | Text input | +| `email` | `string` format `email` | Email input | +| `url` | `string` format `uri` | URL input | | `number` | `number`/`integer` | Number input | | `textarea` | `string` (long) | Textarea | | `select` | `string` with `enum` | Single select dropdown | @@ -73,20 +84,18 @@ The widget auto-detects the editor based on the JSON Schema property type: | `checkbox` | `boolean` | Toggle switch | | `date` | `string` format `date` | Date picker | | `datetime` | `string` format `date-time` | Datetime picker | -| `email` | `string` format `email` | Email input | -| `url` | `string` format `uri` | URL input | ## Example with overrides ```vue diff --git a/docs/components/cn-object-sidebar.md b/docs/components/cn-object-sidebar.md index 4009e1c..dd7c93b 100644 --- a/docs/components/cn-object-sidebar.md +++ b/docs/components/cn-object-sidebar.md @@ -60,6 +60,7 @@ Right sidebar for entity detail pages. Provides standardized tabs — Files, Not | `open` | Boolean | | `true` | Whether the sidebar is visible | | `title` | String | | `''` | Sidebar title (defaults to `objectType`) | | `subtitle` | String | | `''` | Sidebar subtitle | +| `subtitleProp` | String | | `''` | **Deprecated** — use `subtitle` instead | | `apiBase` | String | | `'/apps/openregister/api'` | Base URL for OpenRegister API calls | | `filesLabel` | String | | `'Files'` | Files tab label | | `notesLabel` | String | | `'Notes'` | Notes tab label | diff --git a/docs/components/cn-page-renderer.md b/docs/components/cn-page-renderer.md new file mode 100644 index 0000000..30e2ab0 --- /dev/null +++ b/docs/components/cn-page-renderer.md @@ -0,0 +1,114 @@ +# CnPageRenderer + +JSON-driven page dispatcher. Mounted inside ``, CnPageRenderer reads the manifest, finds the page definition whose `id` matches the current route name (`$route.name === page.id`), and renders the appropriate component by dispatching on `page.type`. + +Page types are resolved via the `pageTypes` registry. The library ships a built-in registry ([`defaultPageTypes`](../utilities/default-page-types.md) — `index`, `detail`, `dashboard`) and consumers extend it by passing a merged map. The `custom` type is special: it resolves `page.component` against the `customComponents` registry rather than `pageTypes`. + +Each entry in `pageTypes` is wrapped in `defineAsyncComponent`, so apps using only a subset pay no bundle cost for the others (notably the GridStack-backed `dashboard`). + +`manifest`, `customComponents`, `pageTypes`, and `translate` are injected from [`CnAppRoot`](./cn-app-root.md) by default; each can also be passed as props for standalone use. **Props always win over inject.** + +## Usage + +### Inside CnAppRoot via vue-router + +```js +// router.js +import { CnPageRenderer } from '@conduction/nextcloud-vue' +const routes = manifest.pages.map((page) => ({ + name: page.id, // CnPageRenderer matches by $route.name === page.id + path: page.route, + component: CnPageRenderer, +})) +``` + +```vue + + +``` + +### Standalone (props instead of inject) + +```vue + +``` + +### Manifest example + +```json +{ + "pages": [ + { + "id": "decisions-index", + "route": "/decisions", + "type": "index", + "title": "Decisions", + "config": { "register": "decisions", "schema": "decision", "columns": ["title", "status"] } + }, + { + "id": "decisions-detail", + "route": "/decisions/:id", + "type": "detail", + "title": "Decision", + "config": { "register": "decisions", "schema": "decision" }, + "headerComponent": "DecisionHeader", + "actionsComponent": "DecisionActions", + "slots": { "footer": "DecisionFooter" } + }, + { + "id": "settings", + "route": "/settings", + "type": "custom", + "title": "Settings", + "component": "SettingsPage" + } + ] +} +``` + +## Props + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `manifest` | `Object \| null` | `null` | Manifest. Falls back to injected `cnManifest`. | +| `customComponents` | `Object \| null` | `null` | Registry for `type: "custom"` pages and slot-override names. Falls back to injected `cnCustomComponents` (then `{}`). | +| `translate` | `Function \| null` | `null` | Falls back to injected `cnTranslate`. Reserved for future use; the renderer doesn't currently call it directly. | +| `pageTypes` | `Object \| null` | `null` | Map of `pages[].type` → Vue component. Falls back to injected `cnPageTypes`, then to the library's [`defaultPageTypes`](../utilities/default-page-types.md). | + +## Page resolution + +| Property | Behaviour | +|----------|-----------| +| `page.id` | Matched against `$route.name`. The instance's `$options.name` is set to `CnPageRenderer:` for cleaner Vue devtools / stack traces | +| `page.type` | Looked up in `pageTypes`. Special-case: `"custom"` resolves `page.component` in `customComponents` instead | +| `page.config` | Spread as props onto the resolved component | +| `page.slots` | `{ slotName: registryName }` map — each entry resolves a `customComponents` entry and mounts it inside the corresponding scoped slot | +| `page.headerComponent` | Sugar for `slots.header` (sugar wins when both are set) | +| `page.actionsComponent` | Sugar for `slots.actions` (sugar wins when both are set) | + +When `page.type` (or a registered `customComponents` name) is missing, the renderer logs `console.warn` once and mounts nothing rather than crashing. + +## Slot-override forwarding + +```js +// page in manifest +{ + "id": "decisions-detail", + "type": "detail", + "slots": { "header": "DecisionHeader", "footer": "DecisionFooter" } +} + +// customComponents registry (passed to CnAppRoot or directly to CnPageRenderer) +{ DecisionHeader, DecisionFooter } +``` + +The renderer mounts the resolved registry components inside the dispatched page component's scoped slots — so the page component receives them under its standard `#header` / `#footer` names with whatever scope it provides. + +## Related + +- [CnAppRoot](./cn-app-root.md) — Provides manifest / customComponents / pageTypes via inject. +- [defaultPageTypes](../utilities/default-page-types.md) — Built-in `index`, `detail`, `dashboard` types. +- [validateManifest](../utilities/validate-manifest.md) — The validator that enforces `page.id` uniqueness, required fields, and `component` for `type: "custom"` pages. diff --git a/docs/components/cn-pagination.md b/docs/components/cn-pagination.md index d5c3304..aa2176e 100644 --- a/docs/components/cn-pagination.md +++ b/docs/components/cn-pagination.md @@ -49,7 +49,7 @@ Full pagination bar with page navigation, ellipsis for large page counts, and a | `totalPages` | Number | `1` | Total number of pages | | `totalItems` | Number | `0` | Total number of items across all pages — used for the page info label | | `currentPageSize` | Number | `20` | Currently selected number of items per page | -| `pageSizeOptions` | Array | `[10, 20, 50, 100, 250, 500, 1000]` | Options shown in the "Items per page" dropdown | +| `pageSizeOptions` | Array | `[{ value: 10, label: '10' }, { value: 20, label: '20' }, …]` | Options shown in the "Items per page" dropdown. Each entry must be `{ value: number, label: string }` | | `minItemsToShow` | Number | `10` | Minimum item count before the pagination bar is rendered; hides it when all items fit on one page | | `firstLabel` | String | `'First'` | Accessible label for the First button | | `previousLabel` | String | `'Previous'` | Accessible label for the Previous button | diff --git a/docs/components/cn-properties-tab.md b/docs/components/cn-properties-tab.md new file mode 100644 index 0000000..af57550 --- /dev/null +++ b/docs/components/cn-properties-tab.md @@ -0,0 +1,175 @@ +--- +sidebar_position: 20 +--- + +# CnPropertiesTab + +Schema-driven properties table with click-to-edit cells, validation indicators, and per-row action slots. Used internally by [`CnAdvancedFormDialog`](cn-advanced-form-dialog.md), and exposed as a top-level component for consumers that want the same editing UX inside their own dialog/page chrome (e.g. when the surrounding shell, footer, or extra tabs don't fit `CnAdvancedFormDialog`'s assumptions). + +**Wraps**: `CnPropertyValueCell` (internal), `vue-material-design-icons` (Alert / AlertCircle / Plus / LockOutline) + +--- + +## When to use + +- You already have your own dialog or page layout, but want the schema-driven property table with validation indicators, lock icon, and click-to-edit behaviour. +- You need an extra "actions" column per row (e.g. a "drop / reset property" button). +- You need to override how individual cells render — e.g. plug in a markdown editor or a domain-specific multiselect. + +If the standard create/edit dialog with Properties / Metadata / Data tabs is enough, prefer [`CnAdvancedFormDialog`](cn-advanced-form-dialog.md). + +--- + +## Props + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `schema` | Object | `null` | JSON Schema. Property list is derived from `schema.properties`; `schema.required` drives validation. | +| `item` | Object | `null` | The current object whose values are displayed/edited. | +| `formData` | Object | `{}` | Edits-in-progress overlay. When `formData[key] !== undefined` it overrides `item[key]` and the row gets the "edited" highlight. `formData[key] === undefined` is treated as "marked for deletion" by the value cell. | +| `selectedProperty` | String | `null` | The currently selected (edit-mode) property key. Bind via `update:selected-property` for two-way control. | +| `editableTypes` | Array | `['string','number','integer','boolean','array','object']` | Schema types that render an editor when their row is selected. Pass a narrower list to restrict (e.g. `['string','number','integer','boolean']` to keep arrays and objects read-only). | +| `validationDisplay` | String | `'indicator'` | `'indicator'` shows error/warning/new icons + row tinting; `'none'` hides them. | +| `excludeFields` | Array | `[]` | Property keys to hide. `id` and `@self` are always hidden. | +| `includeFields` | Array | `null` | If set, only these keys are shown (whitelist). | +| `showConstantProperties` | Boolean | `true` | When `false`, properties whose schema entry has `const` set or `immutable: true` are filtered out. Use together with `hasConstantOrImmutableProperties` (see below) to drive a show/hide toggle in the parent. | +| `propertyOverrides` | Object | `{}` | Per-property cell-config overlay, keyed by property key. Each entry may contain: `widget` (`'text' \| 'number' \| 'boolean' \| 'datetime' \| 'textarea' \| 'array' \| 'select'`), `selectOptions`, `selectMultiple`, `textareaRows`. | + +### `propertyOverrides` example + +```js +{ + themes: { + widget: 'select', + selectOptions: [{ id: 1, label: 'Internal' }, { id: 2, label: 'Public' }], + selectMultiple: true, + }, + description: { widget: 'textarea', textareaRows: 6 }, +} +``` + +`textarea` and `array` widgets are also auto-detected from the schema (`format: 'text'` and `type: 'array'` respectively) — overrides only need to be supplied for cases the schema can't express, like a `select` whose options come from a runtime collection. + +--- + +## Events + +| Event | Payload | Description | +|-------|---------|-------------| +| `update:selected-property` | `key` | Emitted when a row is clicked and selection changes. | +| `update:property-value` | `{ key, value }` | Emitted when a cell editor commits a new value. The parent should patch `formData[key] = value` (or otherwise persist the edit) — this component does not mutate `formData` itself. | + +--- + +## Slots + +| Slot | Scope | Description | +|------|-------|-------------| +| `row-actions` | `{ propertyKey, value, resolvedValue, isEditable, isSchemaProperty }` | When provided, an extra column is appended to every row and rendered with this slot. Use it for inline action buttons (drop, reset, copy, …). When the slot is absent, no extra column is added. | +| `row-actions-header` | — | Optional header content for the actions column. Only relevant when `row-actions` is provided. | +| `value-cell` | `{ propertyKey, value, resolvedValue, isEditing, isEditable, displayName, schemaProp, editabilityWarning, onUpdate }` | Replaces the built-in `CnPropertyValueCell` renderer entirely. Use this for cells that need a renderer the library doesn't ship — e.g. a Toast UI markdown editor. Call `onUpdate(newValue)` to commit. | + +--- + +## Required-property indicator + +Properties listed in the schema's `required` array (or marked `required: true` on the property entry itself) get a red `*` next to their display name. The indicator is independent of validation state — it always shows for required fields, even when they have a valid value — so the user can tell at a glance which fields the schema treats as mandatory. + +The validation icon (red `AlertCircle`) still appears separately when a required field is empty (or otherwise invalid). + +--- + +## Public properties + +These are accessible via `$refs` and are useful for parents that want to drive UI based on the table state. + +| Name | Type | Description | +|------|------|-------------| +| `hasConstantOrImmutableProperties` | computed (boolean) | `true` when at least one property in the (unfiltered) list is `const` or `immutable`. Use it to render a "show/hide constant properties" toggle button only when relevant. | + +--- + +## Usage + +### Basic (read-only, no actions) + +```vue + +``` + +### With a "drop property" action column + +```vue + + + +``` + +### With a custom value-cell (markdown editor) + +```vue + + + +``` + +> The slot is "all or nothing" per row: when provided, the slot is responsible for both display and editing. The library does not ship a default fallback inside the slot — render your own per-property logic and only short-circuit for the keys that need it. + +### Show/hide constant properties toggle + +```vue + + {{ showConstantProperties ? 'Hide constants' : 'Show constants' }} + + + +``` diff --git a/docs/components/cn-property-value-cell.md b/docs/components/cn-property-value-cell.md new file mode 100644 index 0000000..2ccbbec --- /dev/null +++ b/docs/components/cn-property-value-cell.md @@ -0,0 +1,108 @@ +--- +sidebar_position: 22 +--- + +# CnPropertyValueCell + +The cell renderer used inside [`CnPropertiesTab`](cn-properties-tab.md) for a single property's value. Picks an appropriate input/display widget from the schema (with optional explicit override) and emits committed values back to the parent. + +Exposed publicly so consumers using a custom `value-cell` scoped slot on `CnPropertiesTab` can fall back to the default rendering for properties they don't want to handle themselves. + +**Wraps**: `NcTextField`, `NcTextArea`, `NcCheckboxRadioSwitch`, `NcDateTimePickerNative`, `NcSelect` + +--- + +## Widgets + +| Widget | When auto-detected | Notes | +|--------|--------------------|-------| +| `text` | string (default) | `NcTextField`. The `type` attribute follows the string `format` — see the format table below. | +| `number` | `type: 'number'` or `'integer'` | `NcTextField` (`type="number"` with `min`/`max`/`step` from schema). Emits parsed numbers (or `null` for empty). | +| `boolean` | `type: 'boolean'` | `NcCheckboxRadioSwitch` (always rendered as a switch — no row-click required to edit). | +| `datetime` | `type: 'string'` with `format` of `date`, `time`, or `date-time` | `NcDateTimePickerNative`. | +| `textarea` | `type: 'string'` with `format: 'text'` or `format: 'html'` | `NcTextArea` with configurable `rows`. | +| `array` | `type: 'array'` | `NcTextField` with comma-separated values. Emits an array on update (`split(/ *, */g)` + `filter(Boolean)`). | +| `object` | `type: 'object'` | `CnJsonViewer` (CodeMirror) editor for JSON. Emits the parsed object on commit; if the input is invalid JSON, the raw string is emitted unchanged so the user keeps their work and `CnJsonViewer` shows a parse error inline. Empty input emits `null`. | +| `select` | _(never auto-detected — must be set via `widget` prop)_ | `NcSelect` with `options`. Emits raw IDs (single value or array, depending on `selectMultiple`). | + +### String `format` → input `type` mapping + +For the `text` widget the cell maps the schema's `format` to a sensible HTML5 input type: + +| `format` value(s) | Input `type` | +|-------------------|--------------| +| `email`, `idn-email` | `email` | +| `url`, `uri`, `uri-reference`, `iri`, `iri-reference`, `uri-template`, `accessUrl`, `shareUrl`, `downloadUrl` | `url` | +| `password` | `password` | +| `telephone` | `tel` | +| `color`, `color-hex`, `color-hex-alpha` | `color` | +| `date` / `time` / `date-time` | (handled by the `datetime` widget — not a plain text input) | +| anything else (`hostname`, `ipv4`, `uuid`, `regex`, `bsn`, `kvk`, `semver`, …) | `text` | + +Use the `widget` prop to override the auto-detection — e.g. when a property's options come from a runtime collection (`select`) or when you want a textarea for a property whose schema doesn't carry `format: 'text'`. + +--- + +## Props + +| Prop | Type | Default | Description | +|------|------|---------|-------------| +| `propertyKey` | String | _required_ | The schema property key. | +| `schema` | Object | `null` | Full JSON schema. Used to derive the active widget when `widget` is not set. | +| `value` | * | `null` | The resolved current value (`formData[key] ?? item[key]`). | +| `isEditable` | Boolean | `true` | When `false`, the cell renders display-only. | +| `isEditing` | Boolean | `false` | When `true` (and `isEditable`), the cell renders an input — except for `boolean`, which always renders the switch. | +| `displayName` | String | `''` | Human-readable label used as placeholder/aria-label. | +| `editabilityWarning` | String | `null` | Tooltip/title on the display element when not editable. | +| `widget` | String | `null` | Explicit widget override. One of `text`, `number`, `boolean`, `datetime`, `textarea`, `array`, `select`, `object`. | +| `selectOptions` | Array | `null` | Options for the `select` widget. Each entry may be a primitive or `{ id, label }`. | +| `selectMultiple` | Boolean | `true` | Whether the `select` widget allows multiple values. | +| `textareaRows` | Number | `4` | Row count for the `textarea` widget. | +| `objectEditorHeight` | String | `'300px'` | CSS height passed to the `object` widget's CodeMirror editor. | + +--- + +## Events + +| Event | Payload | Description | +|-------|---------|-------------| +| `update:value` | `value` | The new committed value. For `array`/`select` the payload is already an array of primitives; for `number`/`integer` it is parsed (or `null`). | + +--- + +## Public methods + +| Method | Description | +|--------|-------------| +| `focus()` | Focus the underlying input/textarea (no-op for widgets without a focusable element). Called by `CnPropertiesTab` when a row is selected. | + +--- + +## Usage inside a custom `value-cell` slot + +```vue + + + +``` diff --git a/docs/components/cn-register-mapping.md b/docs/components/cn-register-mapping.md index 0cc327c..e4df8ea 100644 --- a/docs/components/cn-register-mapping.md +++ b/docs/components/cn-register-mapping.md @@ -4,45 +4,64 @@ sidebar_position: 27 # CnRegisterMapping -OpenRegister register/schema configuration panel. Allows admin users to map app object types to OpenRegister registers and schemas. +OpenRegister register/schema configuration panel. Lets admin users map app object types to OpenRegister registers and schemas. Groups related object types together, shows a register dropdown (with auto-schema-matching), and a save/reimport action area. -**Wraps**: NcSelect, NcNoteCard, NcButton, NcLoadingIcon +**Wraps**: NcSelect, NcNoteCard, NcButton, NcLoadingIcon (via CnSettingsSection) ![CnRegisterMapping showing register and schema dropdowns for each entity type](/img/screenshots/cn-register-mapping.png) -![CnRegisterMapping showing source, register, and schema dropdowns for each entity type](/img/screenshots/cn-register-mapping.png) - ## Props | Prop | Type | Default | Description | |------|------|---------|-------------| -| `objectTypes` | Object | `\{\}` | Map of `\{ typeSlug: \{ source, register, schema \} \}` | -| `registers` | Array | `[]` | Available registers | -| `schemas` | Array | `[]` | Available schemas | -| `sources` | Array | `[]` | Available sources | -| `loading` | Boolean | `false` | Loading state | -| `saving` | Boolean | `false` | Save in progress | +| `groups` | Array | *(required)* | Groups of object types that share a register. Each group: `{ name, description?, registerConfigKey?, types: [{ slug, label, description?, configKey? }] }` | +| `name` | String | `'Register configuration'` | Section title | +| `description` | String | `'Configure OpenRegister schema mappings for your object types'` | Section description | +| `docUrl` | String | `''` | Documentation URL — shows an info icon next to the title | +| `configuration` | Object | `{}` | Current configuration values keyed by config key: `{ register: '5', client_schema: '28', ... }` | +| `showSaveButton` | Boolean | `true` | Whether to show the Save button | +| `saving` | Boolean | `false` | Whether a save is in progress | +| `showReimportButton` | Boolean | `false` | Whether to show the Re-import button | +| `reimporting` | Boolean | `false` | Whether a reimport is in progress | +| `saveButtonText` | String | `'Save configuration'` | Save button label | +| `reimportButtonText` | String | `'Re-import configuration'` | Re-import button label | +| `autoMatch` | Boolean | `true` | When a register is selected, automatically match schema titles to object type slugs | +| `labels` | Object | `{ register, schema, configured, notConfigured, noSchemas, selectRegister, selectSchema, allConfigured, partiallyConfigured }` | Override UI strings (all default to translated English) | ## Events | Event | Payload | Description | |-------|---------|-------------| -| `save` | `mappings` | Save mappings object | -| `refresh` | — | Refresh registers/schemas list | -| `auto-detect` | — | Auto-detect schemas from register | +| `update:configuration` | `configuration` | Emitted when any register or schema selection changes (use with `.sync` or `v-model`) | +| `save` | — | Save button clicked | +| `reimport` | — | Re-import button clicked | + +## Slots + +| Slot | Description | +|------|-------------| +| `#actions` | Extra action buttons rendered alongside Save/Re-import | +| `#group-header` | Custom header content for each group (scoped: `{ group }`) | +| `#footer` | Footer content below the configuration section | ## Usage ```vue + @update:configuration="settings = $event" + @save="onSave" /> ``` -This component is typically used in the admin settings page of an app to let administrators configure which OpenRegister registers and schemas map to each entity type in the app. +This component is typically used in the admin settings page of an app to let administrators configure which OpenRegister registers and schemas map to each entity type. diff --git a/docs/components/cn-row-actions.md b/docs/components/cn-row-actions.md index c7a816b..55dfc3d 100644 --- a/docs/components/cn-row-actions.md +++ b/docs/components/cn-row-actions.md @@ -36,21 +36,22 @@ trigger button (last cell of every row) ```vue ``` -Listen for the `action` event to handle the selected action: +The `action` event can be used as an alternative to `handler` functions: ```js function onAction({ action, row }) { - if (action.handler === 'edit') openEditDialog(row) - if (action.handler === 'delete') confirmDelete(row) + // action is the label string of the clicked action + if (action === 'Edit') openEditDialog(row) + if (action === 'Delete') confirmDelete(row) } ``` @@ -67,11 +68,25 @@ function onAction({ action, row }) { | Field | Type | Required | Description | |-------|------|----------|-------------| -| `label` | String | ✓ | Display text for the action item | -| `icon` | String | — | MDI icon name (e.g., `'Pencil'`) resolved via the CnIcon registry | -| `handler` | String | ✓ | Identifier string emitted in the `action` event so the parent can switch on it | -| `disabled` | Boolean | — | When `true`, the action item is rendered but not clickable | -| `destructive` | Boolean | — | When `true`, renders the action in danger color and adds a divider above it | +| `label` | String | ✓ | Display text for the action item. Also used as the `action` key in the emitted `action` event. | +| `icon` | Object | — | Vue component to render as the icon (e.g., a vue-material-design-icons component) | +| `handler` | Function | — | Called with the `row` value when the action is clicked: `(row) => void` | +| `disabled` | Boolean\|Function | — | When `true`, or when a function returning `true` for the given row, the item is not clickable | +| `visible` | Boolean\|Function | — | Controls whether the item appears in the menu at all. Omit for "always shown". Pass `false` or a function returning `false` for the row to hide it. Useful for state-dependent actions (e.g. show *Publish* only when the row is unpublished). | +| `title` | String\|Function | — | Native tooltip shown on hover. Accepts a string or a function `(row) => string`. Useful for explaining *why* a `disabled` entry is disabled. | +| `destructive` | Boolean | — | When `true`, renders the action in danger color | + +#### Conditional visibility example + +```vue + +``` ### Events diff --git a/docs/components/cn-schema-form-dialog.md b/docs/components/cn-schema-form-dialog.md index 333fc96..11b604e 100644 --- a/docs/components/cn-schema-form-dialog.md +++ b/docs/components/cn-schema-form-dialog.md @@ -31,7 +31,7 @@ It follows the same **two-phase confirm → result pattern** as CnFormDialog and - **Properties tab**: Sortable table of schema properties with click-to-edit rows. Each property supports type selection, format, required flag, `$ref` references to other schemas, enum values, default values, validation constraints (`minLength`, `maxLength`, `minimum`, `maximum`, `pattern`, `minItems`, `maxItems`), and nested object/array configuration. Properties can be reordered, added, and deleted. - **Configuration tab**: Schema description, slug, version, allOf/oneOf/anyOf composition selectors, object field mappings (name, description, image, summary), file upload settings, allowed tags, hard validation toggle, and searchable toggle. -- **Security tab**: RBAC permissions table with per-group Create/Read/Update/Delete toggles. Includes built-in rows for `public` (anonymous), `user` (authenticated), and `admin` (always full access, disabled). Custom user groups are shown alphabetically between `user` and `admin`. Property-level permissions can also be configured per property via the action menu. +- **Security tab**: RBAC permissions table with per-group Create/Read/Update/Delete toggles. Includes built-in rows for `public` (anonymous), `authenticated` (all logged-in users), and `admin` (always full access, disabled). Custom user groups are shown alphabetically between `authenticated` and `admin`. An **Advanced** accordion (collapsed by default) exposes conditional access rules (property-value-based runtime rules) and the inherit-from-public toggle. Property-level permissions can also be configured per property via the action menu. - **Optional action buttons**: Extend Schema, Analyze Properties, Validate Objects, Delete Objects, Publish Objects, and Delete. Each is toggled via a boolean prop and emits an event — the parent handles the actual logic. - **External data via props**: All data dependencies (schemas, registers, user groups, tags) are passed as props. When a data source is unavailable (empty array), dependent fields are automatically disabled or show empty options. @@ -58,6 +58,7 @@ These props feed external data into the form. The component never fetches data o | `userGroups` | `Array` | `[]` | User groups for the Security (RBAC) tab. Each entry should have `{ id, displayname }`. The groups `admin` and `public` are automatically filtered out from this list since they have dedicated rows. Groups are sorted alphabetically by display name. When empty, only the built-in `public`, `user`, and `admin` rows are shown. | | `availableTags` | `Array` | `[]` | Tags available for the file property tag configuration. Array of strings (e.g. `['image', 'document', 'audio']`). Used in the property detail panel when a property has `type: 'file'` to configure allowed file tags. When empty, the tag selector shows no options. | | `loadingGroups` | `Boolean` | `false` | Whether user groups are still being loaded by the parent. When `true`, the Security tab shows a loading indicator instead of the RBAC table. Set this to `true` while fetching groups, then `false` once `userGroups` is populated. | +| `inheritedProperties` | `Object` | `{}` | Properties inherited from parent schemas (allOf). Displayed as locked, non-editable rows in the Properties tab so the user can see the full property surface including inherited fields. | | `objectCount` | `Number` | `0` | Number of objects currently attached to this schema. Used for two purposes: (1) the "Delete Objects" and "Publish Objects" buttons are disabled when `objectCount === 0` (nothing to act on), and (2) the "Delete" (schema) button is disabled when `objectCount > 0` (cannot delete a schema with attached objects). | ### Action button visibility props @@ -93,11 +94,11 @@ All labels accept pre-translated strings with English defaults. Use these to loc | `closeLabel` | `String` | `'Close'` | Label for the close button (shown during result phase). | | `confirmLabel` | `String` | `''` | Label for the primary action button. When empty, defaults to `'Create'` (create mode) or `'Save'` (edit mode). | | `successText` | `String` | `''` | Success message shown in the result phase. When empty, defaults to `'Schema saved successfully.'`. | -| `extendSchemaLabel` | `String` | `'Extend Schema'` | Label for the Extend Schema action button. | -| `analyzePropertiesLabel` | `String` | `'Analyze Properties'` | Label for the Analyze Properties action button. | -| `validateObjectsLabel` | `String` | `'Validate Objects'` | Label for the Validate Objects action button. | -| `deleteObjectsLabel` | `String` | `'Delete Objects'` | Label for the Delete Objects action button. | -| `publishObjectsLabel` | `String` | `'Publish Objects'` | Label for the Publish Objects action button. | +| `extendSchemaLabel` | `String` | `'Extend schema'` | Label for the Extend Schema action button. | +| `analyzePropertiesLabel` | `String` | `'Analyze properties'` | Label for the Analyze Properties action button. | +| `validateObjectsLabel` | `String` | `'Validate objects'` | Label for the Validate Objects action button. | +| `deleteObjectsLabel` | `String` | `'Delete objects'` | Label for the Delete Objects action button. | +| `publishObjectsLabel` | `String` | `'Publish objects'` | Label for the Publish Objects action button. | | `deleteLabel` | `String` | `'Delete'` | Label for the Delete (schema) action button. | | `deleteObjectsTooltip` | `String` | `'Delete all objects in this schema'` | Tooltip shown on the Delete Objects button when it is enabled. | | `publishObjectsTooltip` | `String` | `'Publish all objects in this schema'` | Tooltip shown on the Publish Objects button when it is enabled. | @@ -192,18 +193,37 @@ The Configuration tab provides schema-level settings: ### Security tab -The Security tab provides a Role-Based Access Control (RBAC) table: +The Security tab provides a Role-Based Access Control (RBAC) table and an advanced conditional access rules section. +**RBAC table:** - **Columns**: Group, Create, Read, Update, Delete - **Built-in rows** (always shown): - - `public` — anonymous/unauthenticated users (blue badge) - - `user` — all authenticated users (orange badge) - - `admin` — always has full access, all toggles disabled/checked (green badge) -- **Custom group rows** — populated from the `userGroups` prop, sorted alphabetically, shown between `user` and `admin` + - `public` — anonymous/unauthenticated users + - `authenticated` — all authenticated users + - `admin` — always has full access, all toggles disabled/checked +- **Custom group rows** — populated from the `userGroups` prop, sorted alphabetically, shown between `authenticated` and `admin` - **Loading state** — when `loadingGroups` is `true`, a spinner is shown instead of the table - **Summary cards** — below the table: - - "Open Access" (success) when no permissions are set - - "Restrictive Schema" (warning) when permissions restrict access to specific groups + - "Open access" (success) when no permissions are set + - "Restrictive schema" (warning) when permissions restrict access to specific groups + +**Advanced: Conditional access rules and inheritance** (accordion, collapsed by default): + +The accordion contains two subsections: + +**Conditional access rules** — Grant access based on object property values evaluated at runtime. Conditions are per-action (create/read/update/delete). Multiple rules per action are OR'd — any matching rule grants access. + +Each rule has: +- A **group** (public, authenticated, or a named user group) +- One or more **match conditions**: `property → operator → value` + +Supported operators: `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$in`, `$nin`, `$exists` + +Special value variables: `$now` (current date/time), `$userId` (current user ID), `$organisation` (current organisation) + +**Inheritance** — Controls whether authenticated users inherit the permissions of the `public` group. + +- **Authenticated users inherit `public` group rights** toggle (default: on). When enabled, logged-in users qualify for any rule that targets the `public` group. Disable to make authenticated access strictly gated by explicit group memberships — anonymous users are unaffected either way. This setting is placed at the bottom of the accordion because it rarely needs to be changed and changing it has significant RBAC implications. --- @@ -300,11 +320,39 @@ The component internally manages a `schemaItem` data object with this structure: --- +## Live demo + +```vue + + +``` + +--- + ## Usage examples ### Basic create/edit (standalone) -```vue +```vue {static}