From d332edd26589989b1ed8ee2bd4d42699137ee8ed Mon Sep 17 00:00:00 2001 From: Remko Date: Mon, 4 May 2026 10:43:30 +0200 Subject: [PATCH 1/4] Updated docs check to check for gaps in documentation --- CLAUDE.md | 1 + scripts/check-docs.js | 207 +++++++++++++++++++++++++++++++++++++++--- 2 files changed, 195 insertions(+), 13 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 913e537..279c1d4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -224,6 +224,7 @@ See `examples/manifest-demo/manifest.json` for a fuller reference and `docs/migr 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. **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 diff --git a/scripts/check-docs.js b/scripts/check-docs.js index f049210..2175090 100644 --- a/scripts/check-docs.js +++ b/scripts/check-docs.js @@ -19,8 +19,12 @@ * * Exempt exports are allow-listed in EXEMPT (e.g. registerIcons). * - * Exits 1 when any export lacks coverage. All categories are reported in - * a single run so CI surfaces every gap at once. + * Phase 1 — existence: exits 1 when any export lacks a doc file. + * Phase 2 — accuracy: for every Component export, verifies that each prop + * name and each static named-slot from the SFC appears somewhere in the + * component's doc file. Exits 1 when any are missing. + * + * All categories are reported in a single run so CI surfaces every gap at once. */ const fs = require('fs') @@ -83,6 +87,137 @@ const STORE_MENTION_ONLY = { getSchemaApiUrl: 'plugins.md', } +// --------------------------------------------------------------------------- +// SFC parsing helpers (used by Phase 2 accuracy check) +// --------------------------------------------------------------------------- + +/** + * Extract prop names from a Vue SFC's script-block `props:` definition. + * Handles both object form (`props: { name: { ... } }`) and array form + * (`props: ['name1', 'name2']`). Uses brace-depth tracking so prop option + * keys (type, default, required, validator) are not mistaken for prop names. + * @param {string} sfcPath absolute path to the .vue file + * @return {string[]} camelCase prop names, or empty array when none found + */ +function extractSfcProps(sfcPath) { + if (!fs.existsSync(sfcPath)) return [] + const source = fs.readFileSync(sfcPath, 'utf8') + const scriptMatch = source.match(/]*>([\s\S]*?)<\/script>/m) + if (!scriptMatch) return [] + const script = scriptMatch[1] + + const propsIdx = script.search(/\bprops\s*:/) + if (propsIdx === -1) return [] + const afterProps = script.slice(propsIdx) + + // Array form: props: ['a', 'b'] + const arrMatch = afterProps.match(/^props\s*:\s*\[([^\]]*)\]/) + if (arrMatch) { + const names = [] + const re = /['"]([^'"]+)['"]/g + let m + while ((m = re.exec(arrMatch[1])) !== null) names.push(m[1]) + return names + } + + // Object form: walk characters tracking brace depth. + // Keys are captured at depth 1 (direct children of the props object): + // - Right before a `{` opens a sub-object (e.g. `title: {`) + // - At end of line for flat props (e.g. `name: String,`) + const braceIdx = afterProps.indexOf('{') + if (braceIdx === -1) return [] + + const propNames = [] + let depth = 0 + let lineText = '' + + for (let i = braceIdx; i < afterProps.length; i++) { + const ch = afterProps[i] + if (ch === '{') { + // Capture key immediately before its options object opens + if (depth === 1) { + const key = lineText.match(/^\s+([a-zA-Z][a-zA-Z0-9]*)\s*:/) + if (key) propNames.push(key[1]) + } + depth++ + } else if (ch === '}') { + depth-- + if (depth === 0) break + } else if (ch === '\n') { + // Capture flat prop (no sub-object) at end of its line + if (depth === 1) { + const key = lineText.match(/^\s+([a-zA-Z][a-zA-Z0-9]*)\s*:/) + if (key) propNames.push(key[1]) + } + lineText = '' + continue + } + lineText += ch + } + + return [...new Set(propNames)] +} + +/** + * Extract names of all statically-named `` elements from a Vue SFC + * template. Dynamic `:name="..."` bindings are intentionally skipped since + * the slot name is only known at runtime and cannot be literally checked. + * @param {string} sfcPath absolute path to the .vue file + * @return {string[]} static slot names (the implicit default slot is excluded) + */ +function extractSfcNamedSlots(sfcPath) { + if (!fs.existsSync(sfcPath)) return [] + const source = fs.readFileSync(sfcPath, 'utf8') + const templateMatch = source.match(/]*>([\s\S]*?)<\/template>/m) + if (!templateMatch) return [] + + const slots = new Set() + const slotRe = /]*?)(?:\s*\/?>)/g + let m + while ((m = slotRe.exec(templateMatch[1])) !== null) { + const attrs = m[1] + if (attrs.includes(':name=')) continue // skip dynamic slot names + const nameM = attrs.match(/\bname="([^"]+)"/) + if (nameM) slots.add(nameM[1]) + } + return [...slots] +} + +/** + * Accuracy check for one Component export: verifies that every prop name and + * every static named slot defined in the SFC is mentioned at least once in + * the component's doc file (by camelCase or kebab-case name). + * @param {string} componentName PascalCase name (e.g. 'CnWidgetWrapper') + * @param {string} docPath absolute path to the component's .md file + * @return {string[]} plain-English issue strings (empty when all covered) + */ +function checkComponentDetail(componentName, docPath) { + const sfcPath = path.join(ROOT, 'src', 'components', componentName, `${componentName}.vue`) + if (!fs.existsSync(sfcPath) || !fs.existsSync(docPath)) return [] + + const docContent = fs.readFileSync(docPath, 'utf8') + const issues = [] + + for (const prop of extractSfcProps(sfcPath)) { + const kebab = toKebab(prop) + if (!docContent.includes(prop) && !docContent.includes(kebab)) { + issues.push(`prop \`${prop}\` (${kebab}) not mentioned in doc`) + } + } + + for (const slot of extractSfcNamedSlots(sfcPath)) { + if (!docContent.includes(slot)) { + issues.push(`slot \`${slot}\` not mentioned in doc`) + } + } + + return issues +} + +// --------------------------------------------------------------------------- +// Shared utilities +// --------------------------------------------------------------------------- + /** * Convert PascalCase or camelCase to kebab-case. * @param {string} name identifier to convert @@ -228,14 +363,23 @@ function isCovered(info) { return stems.has(info.expectedStem) } +// --------------------------------------------------------------------------- +// Phase 1: existence check +// --------------------------------------------------------------------------- + const exports_ = parsePublicExports(INDEX_FILE) const categories = new Map() +const componentExports = [] // collected for Phase 2 + for (const name of exports_) { const info = classify(name) if (info.category === 'Exempt') { continue } + if (info.category === 'Components') { + componentExports.push(name) + } if (!categories.has(info.category)) { categories.set(info.category, { checked: 0, missing: [] }) } @@ -261,21 +405,58 @@ for (const [category, { checked, missing }] of ordered) { console.log(` ${mark} ${category}: ${covered}/${checked}`) } -if (totalMissing === 0) { - console.log(`\n✓ All ${totalChecked} public exports are documented.`) - process.exit(0) +if (totalMissing > 0) { + console.error(`\n✗ ${totalMissing} export(s) are missing documentation:\n`) + for (const [category, { missing }] of ordered) { + if (missing.length === 0) { + continue + } + console.error(`${category}:`) + for (const { name, expectedPath } of missing) { + console.error(` - ${name} → ${expectedPath}`) + } + console.error('') + } + console.error('Create the missing files (or add the missing mention) so every public export is documented.') + process.exit(1) } -console.error(`\n✗ ${totalMissing} export(s) are missing documentation:\n`) -for (const [category, { missing }] of ordered) { - if (missing.length === 0) { - continue +console.log(`\n✓ All ${totalChecked} public exports are documented.`) + +// --------------------------------------------------------------------------- +// Phase 2: accuracy check — every SFC prop and named slot must appear in doc +// --------------------------------------------------------------------------- + +console.log('\nChecking component doc accuracy (props and slots):\n') + +const detailFailures = [] + +for (const name of componentExports) { + const kebab = toKebab(name) + const docPath = path.join(DOCS_DIR, 'components', `${kebab}.md`) + const issues = checkComponentDetail(name, docPath) + if (issues.length > 0) { + detailFailures.push({ name, issues }) } - console.error(`${category}:`) - for (const { name, expectedPath } of missing) { - console.error(` - ${name} → ${expectedPath}`) +} + +const detailChecked = componentExports.length +const detailFailed = detailFailures.length + +if (detailFailed === 0) { + console.log(` ✓ All ${detailChecked} component docs cover their props and slots.`) + process.exit(0) +} + +console.log(` ✓ ${detailChecked - detailFailed}/${detailChecked} component docs cover their props and slots.`) +console.error(`\n✗ ${detailFailed} component doc(s) are missing prop or slot coverage:\n`) +for (const { name, issues } of detailFailures) { + const kebab = toKebab(name) + console.error(`${name} (docs/components/${kebab}.md):`) + for (const issue of issues) { + console.error(` - ${issue}`) } console.error('') } -console.error('Create the missing files (or add the missing mention) so every public export is documented.') +console.error('Add the missing props/slots to the component doc so every SFC interface item is covered.') process.exit(1) From 5af4ae7c972cc3cdfa494d3aed4235da0fb16d2a Mon Sep 17 00:00:00 2001 From: Remko Date: Mon, 4 May 2026 10:43:58 +0200 Subject: [PATCH 2/4] Updated docs based on recently changed workflow --- docs/components/cn-configuration-card.md | 14 ++---- docs/components/cn-detail-page.md | 5 ++ docs/components/cn-facet-sidebar.md | 1 + docs/components/cn-index-page.md | 2 + docs/components/cn-index-sidebar.md | 1 + docs/components/cn-mass-copy-dialog.md | 6 ++- docs/components/cn-mass-delete-dialog.md | 8 +++- docs/components/cn-mass-export-dialog.md | 12 +++-- docs/components/cn-mass-import-dialog.md | 24 ++++++++-- docs/components/cn-object-sidebar.md | 1 + docs/components/cn-register-mapping.md | 61 ++++++++++++++++-------- docs/components/cn-schema-form-dialog.md | 1 + docs/components/cn-settings-card.md | 6 +-- docs/components/cn-settings-section.md | 38 ++++++++------- docs/components/cn-stats-block.md | 1 + docs/components/cn-status-badge.md | 1 + docs/components/cn-tabbed-form-dialog.md | 1 + docs/components/cn-version-info-card.md | 43 +++++++++++------ 18 files changed, 147 insertions(+), 79 deletions(-) diff --git a/docs/components/cn-configuration-card.md b/docs/components/cn-configuration-card.md index 50d471a..7f2063d 100644 --- a/docs/components/cn-configuration-card.md +++ b/docs/components/cn-configuration-card.md @@ -14,24 +14,16 @@ Configuration card with title, actions, and status indicator. Used for feature t | Prop | Type | Default | Description | |------|------|---------|-------------| | `title` | String | `''` | Card title | -| `description` | String | `''` | Card description | -| `status` | String | `null` | `'active'`, `'inactive'`, `'error'`, `'warning'` | -| `statusLabel` | String | `''` | Override status display text | -| `loading` | Boolean | `false` | Loading state | - -## Events - -| Event | Payload | Description | -|-------|---------|-------------| -| `action` | `actionName` | Action button clicked | ## Slots | Slot | Description | |------|-------------| | `default` | Card content | -| `#actions` | Action buttons | +| `#icon` | Icon displayed in the card header | +| `#actions` | Action buttons in the card header | | `#status` | Custom status indicator | +| `#footer` | Footer content below the card body | ## Usage diff --git a/docs/components/cn-detail-page.md b/docs/components/cn-detail-page.md index 77d8259..74b838b 100644 --- a/docs/components/cn-detail-page.md +++ b/docs/components/cn-detail-page.md @@ -18,6 +18,11 @@ A generic detail/overview page component. The simpler counterpart to CnIndexPage | `iconSize` | Number | `28` | Icon size in pixels | | `loading` | Boolean | `false` | Loading state | | `loadingLabel` | String | `'Loading...'` | Message shown during loading | +| `sidebar` | Boolean | `false` | Whether to activate the external `CnObjectSidebar` via the `objectSidebarState` inject | +| `sidebarOpen` | Boolean | `true` | Whether the sidebar starts open (only relevant when `sidebar` is `true`) | +| `objectType` | String | `''` | Object type slug passed to the sidebar (e.g. `'pipelinq_lead'`) | +| `objectId` | String\|Number | `''` | Object ID passed to the sidebar | +| `sidebarProps` | Object | `{}` | Extra sidebar configuration forwarded to `CnObjectSidebar` (`register`, `schema`, `hiddenTabs`, `title`, `subtitle`) | | `error` | Boolean | `false` | Error state | | `errorMessage` | String | `'An error occurred'` | Message shown in error state | | `onRetry` | Function | `null` | Callback for retry button in error state. If null, no retry button shown. | diff --git a/docs/components/cn-facet-sidebar.md b/docs/components/cn-facet-sidebar.md index 44586ff..5cf6f37 100644 --- a/docs/components/cn-facet-sidebar.md +++ b/docs/components/cn-facet-sidebar.md @@ -20,6 +20,7 @@ Auto-generated faceted search sidebar from schema. Renders filter controls for p | `loading` | Boolean | `false` | Loading state | | `title` | String | `'Filters'` | Sidebar title | | `clearLabel` | String | `'Clear all'` | Clear all button label | +| `userIsAdmin` | Boolean | `true` | Whether the current user is an admin. When `false`, schema properties with `adminOnly: true` are hidden from the filter list. | ## Events diff --git a/docs/components/cn-index-page.md b/docs/components/cn-index-page.md index 55ba00c..93784c5 100644 --- a/docs/components/cn-index-page.md +++ b/docs/components/cn-index-page.md @@ -18,6 +18,8 @@ The main list page component. Combines a data table (or card grid), filter bar, |------|------|---------|-------------| | `title` | String | *(required)* | Page title | | `description` | String | `''` | Optional subtitle | +| `showTitle` | Boolean | `false` | Show the page header (icon, title, description) inline above the table. When `false` (default), the title is shown in the sidebar header instead. | +| `icon` | String | `''` | MDI icon name for the page header. Defaults to `schema.icon` when a schema is provided. | | `schema` | Object | `null` | OpenRegister schema for auto-generating columns, filters, and form fields | | `objects` | Array | `[]` | Row data | | `pagination` | Object | `null` | Pagination state (`\{ currentPage, totalPages, totalItems, pageSize \}`) | diff --git a/docs/components/cn-index-sidebar.md b/docs/components/cn-index-sidebar.md index 3312a83..e0f3b13 100644 --- a/docs/components/cn-index-sidebar.md +++ b/docs/components/cn-index-sidebar.md @@ -86,6 +86,7 @@ When using `CnIndexPage`, the sidebar is managed internally — you do not need | `filtersLabel` | String | `'Filters'` | Heading above the filter controls | | `columnsHeading` | String | `'Column Visibility'` | Heading inside the Columns tab | | `columnsDescription` | String | `'Select which columns to display in the table'` | Subtitle inside the Columns tab | +| `userIsAdmin` | Boolean | `true` | Whether the current user is an admin. When `false`, schema properties with `adminOnly: true` are hidden from the filter list in the Search tab. | ### Events diff --git a/docs/components/cn-mass-copy-dialog.md b/docs/components/cn-mass-copy-dialog.md index 6caa6cf..5ca33a3 100644 --- a/docs/components/cn-mass-copy-dialog.md +++ b/docs/components/cn-mass-copy-dialog.md @@ -20,8 +20,12 @@ Two-phase mass copy dialog with naming pattern. Allows users to define a naming | `dialogTitle` | String | `'Copy items'` | | | `patternLabel` | String | `'Naming pattern'` | | | `patternPlaceholder` | String | `'\{name\} (copy)'` | | -| `confirmLabel` | String | `'Copy'` | | +| `emptyText` | String | `'No items selected for copying.'` | Message shown when all items have been removed from the list | +| `successText` | String | `'Items successfully copied.'` | Message shown in the result phase on success | | `cancelLabel` | String | `'Cancel'` | | +| `closeLabel` | String | `'Close'` | Label for the close button shown in the result phase | +| `confirmLabel` | String | `'Copy'` | | +| `removeLabel` | String | `'Remove from list'` | Tooltip/label for the per-item remove button | ## Events diff --git a/docs/components/cn-mass-delete-dialog.md b/docs/components/cn-mass-delete-dialog.md index a9d7790..e3022cd 100644 --- a/docs/components/cn-mass-delete-dialog.md +++ b/docs/components/cn-mass-delete-dialog.md @@ -18,9 +18,13 @@ Two-phase mass delete confirmation dialog. Shows list of items to delete, requir | `nameField` | String | `'title'` | Field to display as item name | | `nameFormatter` | Function | `null` | Optional function `(item) => string` to format item names. Overrides `nameField` when provided. | | `dialogTitle` | String | `'Delete items'` | | -| `warningText` | String | `''` | Warning message | -| `confirmLabel` | String | `'Delete'` | | +| `warningText` | String | `''` | Warning text shown above the item list | +| `emptyText` | String | `'No items selected for deletion.'` | Message shown when all items have been removed from the list | +| `successText` | String | `'Items successfully deleted.'` | Message shown in the result phase on success | | `cancelLabel` | String | `'Cancel'` | | +| `closeLabel` | String | `'Close'` | Label for the close button shown in the result phase | +| `confirmLabel` | String | `'Delete'` | | +| `removeLabel` | String | `'Remove from list'` | Tooltip/label for the per-item remove button | ## Events diff --git a/docs/components/cn-mass-export-dialog.md b/docs/components/cn-mass-export-dialog.md index e07ae91..a530e06 100644 --- a/docs/components/cn-mass-export-dialog.md +++ b/docs/components/cn-mass-export-dialog.md @@ -14,11 +14,15 @@ Export format selection dialog. Lets users pick a format and triggers export for | Prop | Type | Default | Description | |------|------|---------|-------------| -| `items` | Array | `[]` | Items to export | -| `formats` | Array | `['json', 'csv']` | Available export formats | -| `dialogTitle` | String | `'Export items'` | | -| `confirmLabel` | String | `'Export'` | | +| `dialogTitle` | String | `'Export objects'` | | +| `description` | String | `''` | Optional description text shown above the format selector | +| `formats` | Array | `[{ id: 'excel', label: 'Excel (.xlsx)' }, { id: 'csv', label: 'CSV (.csv)' }]` | Available export formats as `[{ id, label }]` objects | +| `defaultFormat` | String | `'excel'` | ID of the format selected by default | +| `successText` | String | `'Export completed successfully.'` | Message shown in the result phase on success | +| `formatLabel` | String | `'Export format'` | Label above the format selector | | `cancelLabel` | String | `'Cancel'` | | +| `closeLabel` | String | `'Close'` | Label for the close button shown in the result phase | +| `confirmLabel` | String | `'Export'` | | ## Events diff --git a/docs/components/cn-mass-import-dialog.md b/docs/components/cn-mass-import-dialog.md index 8f5426b..f752392 100644 --- a/docs/components/cn-mass-import-dialog.md +++ b/docs/components/cn-mass-import-dialog.md @@ -14,12 +14,26 @@ File upload dialog with options and results summary. Supports importing data fro | Prop | Type | Default | Description | |------|------|---------|-------------| -| `dialogTitle` | String | `'Import items'` | | -| `acceptedTypes` | String | `'.json,.csv'` | Accepted file types | -| `options` | Array | `[]` | Additional import options | -| `confirmLabel` | String | `'Import'` | | +| `dialogTitle` | String | `'Import data'` | | +| `acceptedTypes` | String | `'.json,.xlsx,.xls,.csv'` | Accepted file types (input `accept` attribute) | +| `options` | Array | `[]` | Additional import option definitions | +| `fileTypeHelp` | Array | `[{ label, description }]` | Help entries shown in the "Supported file types" list; defaults to JSON, Excel, and CSV entries | +| `canSubmit` | Boolean | `true` | Whether the form is ready to submit. The parent can set this to `false` (via a slot) while required options are incomplete. | +| `successText` | String | `'Import completed successfully!'` | Message shown in the result phase when all rows imported without errors | +| `partialSuccessText` | String | `'Import completed with errors. Check the details below.'` | Message shown when the import finished but with some errors | +| `loadingText` | String | `'Importing data — this may take a moment for large files...'` | Text shown while the import is running | +| `summaryTitle` | String | `'Import summary'` | Heading above the result summary table | +| `supportedFormatsLabel` | String | `'Supported file types:'` | Label above the file type help list | +| `selectFileLabel` | String | `'Select file'` | Label for the file-picker button | | `cancelLabel` | String | `'Cancel'` | | -| `maxFileSize` | Number | `10485760` | Max file size in bytes (10MB default) | +| `closeLabel` | String | `'Close'` | Label for the close button shown in the result phase | +| `confirmLabel` | String | `'Import'` | | +| `sheetLabel` | String | `'Sheet'` | Column header for the sheet name in the summary table | +| `foundLabel` | String | `'Found'` | Column header for the found-rows count | +| `createdLabel` | String | `'Created'` | Column header for the created-rows count | +| `updatedLabel` | String | `'Updated'` | Column header for the updated-rows count | +| `unchangedLabel` | String | `'Unchanged'` | Column header for the unchanged-rows count | +| `errorsLabel` | String | `'Errors'` | Column header for the error count | ## Events 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-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-schema-form-dialog.md b/docs/components/cn-schema-form-dialog.md index 67244d5..1f66c3d 100644 --- a/docs/components/cn-schema-form-dialog.md +++ b/docs/components/cn-schema-form-dialog.md @@ -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 diff --git a/docs/components/cn-settings-card.md b/docs/components/cn-settings-card.md index 60df04d..04212c3 100644 --- a/docs/components/cn-settings-card.md +++ b/docs/components/cn-settings-card.md @@ -13,9 +13,9 @@ Collapsible card for organizing settings into sections. Used in admin settings p | Prop | Type | Default | Description | |------|------|---------|-------------| | `title` | String | `''` | Card title | -| `description` | String | `''` | Card description | -| `collapsed` | Boolean | `false` | Initial collapsed state | -| `collapsible` | Boolean | `true` | Allow collapsing | +| `icon` | String | `''` | Emoji or text icon displayed before the title | +| `collapsible` | Boolean | `false` | Allow collapsing the card | +| `defaultCollapsed` | Boolean | `false` | Whether the card starts in a collapsed state (only relevant when `collapsible` is `true`) | ## Events diff --git a/docs/components/cn-settings-section.md b/docs/components/cn-settings-section.md index e51fa85..a1073cd 100644 --- a/docs/components/cn-settings-section.md +++ b/docs/components/cn-settings-section.md @@ -16,19 +16,18 @@ Admin settings section with loading and error states. Wraps NcSettingsSection wi | Prop | Type | Default | Description | |------|------|---------|-------------| -| `title` | String | `''` | Section title | -| `description` | String | `''` | Section description | -| `loading` | Boolean | `false` | Loading state | -| `error` | String | `null` | Error message to display | -| `saveLabel` | String | `'Save'` | Save button label | -| `saving` | Boolean | `false` | Save in progress | - -## Events - -| Event | Payload | Description | -|-------|---------|-------------| -| `save` | — | Save button clicked | -| `retry` | — | Retry button clicked (on error) | +| `name` | String | *(required)* | Section title (passed to `NcSettingsSection`) | +| `description` | String | `''` | Brief section description shown under the title | +| `detailedDescription` | String | `''` | Longer description rendered in a separate block below the title | +| `docUrl` | String | `''` | Documentation URL — shows an info icon link next to the title | +| `loading` | Boolean | `false` | Loading state — shows a loading indicator instead of content | +| `loadingMessage` | String | `'Loading...'` | Message shown during loading | +| `error` | Boolean | `false` | Error state — shows an error card with optional retry button | +| `errorMessage` | String | `'An error occurred'` | Message shown in error state | +| `onRetry` | Function | `null` | Callback for the retry button. When `null`, no retry button is shown. | +| `retryButtonText` | String | `'Retry'` | Label for the retry button | +| `empty` | Boolean | `false` | Empty state — shows an empty message instead of content | +| `emptyMessage` | String | `'No data available'` | Message shown when the section has no data | ## Slots @@ -36,17 +35,20 @@ Admin settings section with loading and error states. Wraps NcSettingsSection wi |------|-------------| | `default` | Section content | | `#actions` | Extra action buttons | +| `#description` | Custom description content (replaces `description` prop) | +| `#footer` | Footer content rendered below the section body | +| `#empty` | Custom empty state (replaces the default empty message) | ## Usage ```vue + :error="hasError" + error-message="Could not load settings" + :on-retry="fetchSettings"> diff --git a/docs/components/cn-stats-block.md b/docs/components/cn-stats-block.md index a10abbc..0009248 100644 --- a/docs/components/cn-stats-block.md +++ b/docs/components/cn-stats-block.md @@ -27,6 +27,7 @@ Statistics display card with icon, count, and optional breakdown. Used inside Cn | `horizontal` | Boolean | `false` | Icon-left layout | | `clickable` | Boolean | `false` | Enable click interaction | | `showZeroCount` | Boolean | `false` | Display 0 as a count value instead of the empty label | +| `route` | Object | `null` | Vue Router location object (`{ name, path, query, ... }`). When set, the card renders as a `` and clickable styles are applied automatically. | ## Events diff --git a/docs/components/cn-status-badge.md b/docs/components/cn-status-badge.md index 5a522e8..bf070ea 100644 --- a/docs/components/cn-status-badge.md +++ b/docs/components/cn-status-badge.md @@ -23,6 +23,7 @@ Color-coded pill badge for status, priority, or category display. Supports autom | Slot | Description | |------|-------------| | default | Custom label content | +| `#icon` | Icon element displayed before the label text | ## Usage diff --git a/docs/components/cn-tabbed-form-dialog.md b/docs/components/cn-tabbed-form-dialog.md index bcdc5df..b5e0dab 100644 --- a/docs/components/cn-tabbed-form-dialog.md +++ b/docs/components/cn-tabbed-form-dialog.md @@ -19,6 +19,7 @@ A generic tabbed dialog for create/edit forms. Provides the standard dialog shel | `size` | `String` | `'large'` | NcDialog size | | `showCreateAnother` | `Boolean` | `false` | Show "Create Another" checkbox in create mode | | `disableSave` | `Boolean` | `false` | Disable primary button (parent controls validation) | +| `disableSaveTooltip` | `String` | `''` | Tooltip shown on the save button when it is disabled — explains the blocked state to the user (also used as `aria-label` for accessibility). | | `successText` | `String` | `''` | Custom success message | | `cancelLabel` | `String` | `'Cancel'` | Cancel button text | | `closeLabel` | `String` | `'Close'` | Close button text (result phase) | diff --git a/docs/components/cn-version-info-card.md b/docs/components/cn-version-info-card.md index 9c3fba0..fe68993 100644 --- a/docs/components/cn-version-info-card.md +++ b/docs/components/cn-version-info-card.md @@ -4,7 +4,7 @@ sidebar_position: 26 # CnVersionInfoCard -Displays app version information in admin settings. Shows current version, Nextcloud compatibility, and optional update notice. +Displays application version information in admin settings pages. Shows the app name, installed version, optional configured version, and an update status indicator with an optional update button. ## Props @@ -12,29 +12,44 @@ Displays app version information in admin settings. Shows current version, Nextc | Prop | Type | Default | Description | |------|------|---------|-------------| -| `appName` | String | `''` | Application name | -| `version` | String | `''` | Current version string | -| `ncVersion` | String | `''` | Nextcloud version compatibility | -| `phpVersion` | String | `''` | PHP version compatibility | -| `databaseVersion` | String | `''` | Database version info | -| `updateAvailable` | Boolean | `false` | Show update notice | -| `latestVersion` | String | `''` | Latest available version | +| `appName` | String | *(required)* | Application name displayed in the version row | +| `appVersion` | String | *(required)* | Installed application version string | +| `title` | String | `'Version information'` | Section title | +| `description` | String | `'Information about the current application installation'` | Section description | +| `docUrl` | String | `''` | Documentation URL — shows an info icon link next to the title | +| `cardTitle` | String | `'Application information'` | Heading shown inside the card | +| `configuredVersion` | String | `''` | Configured version string (for apps that track configuration versions separately). When non-empty, an extra row is shown. | +| `isUpToDate` | Boolean | `true` | Whether the configured version matches the installed version. Controls the status indicator color. | +| `showUpdateButton` | Boolean | `false` | Whether to show an "Update" action button | +| `updating` | Boolean | `false` | Whether an update is currently in progress (shows a loading indicator on the button) | +| `additionalItems` | Array | `[]` | Extra key-value items: `[{ label, value, statusClass? }]` | | `loading` | Boolean | `false` | Loading state | +| `labels` | Object | `{ appName: 'Application Name', version: 'Version', configuredVersion: 'Configured Version' }` | Override the standard row labels | + +## Events + +| Event | Payload | Description | +|-------|---------|-------------| +| `update` | — | Emitted when the Update button is clicked | ## Slots | Slot | Description | |------|-------------| -| `#extra-info` | Additional version info rows | +| `#actions` | Extra action buttons in the card header | +| `#additional-items` | Custom additional items (replaces `additionalItems` prop rendering) | +| `#footer` | Footer content below the card | +| `#extra-cards` | Additional cards rendered after the version info card | ## Usage ```vue + app-version="1.5.0" + configured-version="1.4.0" + :is-up-to-date="false" + :show-update-button="true" + :updating="updating" + @update="runUpdate" /> ``` From 796e4eadf7f6af16efea957027a2a5a07591161e Mon Sep 17 00:00:00 2001 From: Remko Date: Mon, 4 May 2026 11:00:22 +0200 Subject: [PATCH 3/4] Updated check-docs to check more then just components --- scripts/check-docs.js | 210 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 196 insertions(+), 14 deletions(-) diff --git a/scripts/check-docs.js b/scripts/check-docs.js index 2175090..296b0c9 100644 --- a/scripts/check-docs.js +++ b/scripts/check-docs.js @@ -88,7 +88,137 @@ const STORE_MENTION_ONLY = { } // --------------------------------------------------------------------------- -// SFC parsing helpers (used by Phase 2 accuracy check) +// JS source helpers (used by Phase 2 accuracy check — non-component exports) +// --------------------------------------------------------------------------- + +/** + * @param names that should be skipped during the accuracy check because they + * are either generic containers ('options', 'params') or too short to carry + * meaningful signal ('e', 'err'). The check would produce too many false + * positives if it required every doc to spell out these placeholder names. + */ +const SKIP_PARAMS = new Set(['options', 'params', 'e', 'err', 'error', 'cb', 'fn', 'response']) + +/** + * Find the JS source file that contains the definition of a given export. + * Returns null when the file cannot be determined (e.g. store constants that + * live in the same file as a plugin — those are covered by mention-only checks + * in Phase 1 and don't need an accuracy check). + * @param {string} exportName identifier from src/index.js + * @param {string} category classification returned by classify() + * @return {string|null} absolute path to the source file, or null + */ +function findSourceFile(exportName, category) { + if (category === 'Composables') { + return path.join(ROOT, 'src', 'composables', `${exportName}.js`) + } + if (category === 'Store plugins') { + // e.g. 'auditTrailsPlugin' → 'auditTrails.js' + const stem = exportName.slice(0, -'Plugin'.length) + return path.join(ROOT, 'src', 'store', 'plugins', `${stem}.js`) + } + if (category === 'Store factories') { + const map = { + useObjectStore: path.join(ROOT, 'src', 'store', 'index.js'), + createObjectStore: path.join(ROOT, 'src', 'store', 'index.js'), + createCrudStore: path.join(ROOT, 'src', 'store', 'createCrudStore.js'), + createSubResourcePlugin: path.join(ROOT, 'src', 'store', 'createSubResourcePlugin.js'), + } + return map[exportName] || null + } + if (category === 'Utilities') { + // Search every utils/*.js file (excluding the barrel) for this export + const utilDir = path.join(ROOT, 'src', 'utils') + for (const file of fs.readdirSync(utilDir)) { + if (!file.endsWith('.js') || file === 'index.js') continue + const filePath = path.join(utilDir, file) + const content = fs.readFileSync(filePath, 'utf8') + const funcRe = new RegExp(`export\\s+(?:async\\s+)?function\\s+${exportName}\\b`) + const constRe = new RegExp(`export\\s+const\\s+${exportName}\\b`) + if (funcRe.test(content) || constRe.test(content)) return filePath + } + return null + } + return null +} + +/** + * Extract @param names from the JSDoc block immediately preceding the named + * export declaration in a JS file. Only looks at the JSDoc for that specific + * function so params from sibling exports don't pollute results. + * @param {string} filePath absolute path to the .js source file + * @param {string} exportName identifier to look up (function or const name) + * @return {string[]} @param identifiers (may include 'options.subKey' dotted forms) + */ +function extractFunctionParams(filePath, exportName) { + if (!filePath || !fs.existsSync(filePath)) return [] + const source = fs.readFileSync(filePath, 'utf8') + + // Locate the export declaration for this specific function + const funcRe = new RegExp( + `export\\s+(?:async\\s+)?function\\s+${exportName}\\b|export\\s+const\\s+${exportName}\\s*=`, + ) + const funcMatch = funcRe.exec(source) + if (!funcMatch) return [] + + // Find the last /** ... */ JSDoc block that sits before this declaration + const before = source.slice(0, funcMatch.index) + const jsdocRe = /\/\*\*[\s\S]*?\*\//g + let lastJsdoc = null + let jsdocMatch + while ((jsdocMatch = jsdocRe.exec(before)) !== null) { + lastJsdoc = jsdocMatch[0] + } + if (!lastJsdoc) return [] + + // Pull out every @param identifier (handles both plain and [optional] forms) + const paramRe = /@param\s+\{[^}]+\}\s+\[?([a-zA-Z][a-zA-Z0-9._]*)\]?/g + const params = new Set() + let m + while ((m = paramRe.exec(lastJsdoc)) !== null) { + params.add(m[1]) + } + return [...params] +} + +/** + * Accuracy check for a non-component JS export: verifies that every + * meaningful @param name from the JSDoc appears in the export's doc file + * (by exact match or, for `options.subKey` dotted paths, by the leaf name). + * @param {string} exportName identifier from src/index.js + * @param {string|null} srcPath absolute path to the source file + * @param {string} docPath absolute path to the expected .md doc file + * @return {string[]} plain-English issue strings (empty when all covered) + */ +function checkJsAccuracy(exportName, srcPath, docPath) { + if (!srcPath || !fs.existsSync(srcPath) || !fs.existsSync(docPath)) return [] + + const params = extractFunctionParams(srcPath, exportName) + if (params.length === 0) return [] + + const docContent = fs.readFileSync(docPath, 'utf8') + const issues = [] + + for (const param of params) { + const parts = param.split('.') + const leaf = parts[parts.length - 1] + + // Skip generic placeholder names + if (SKIP_PARAMS.has(leaf) || SKIP_PARAMS.has(param)) continue + // Skip single-character params and dual-form implementation names + if (leaf.length <= 1 || param.endsWith('OrOptions') || param.endsWith('OrString')) continue + + // Check: the exact param name OR (for dotted paths) just the leaf must appear in the doc + if (!docContent.includes(param) && !docContent.includes(leaf)) { + issues.push(`param \`${param}\` not mentioned in doc`) + } + } + + return issues +} + +// --------------------------------------------------------------------------- +// SFC parsing helpers (used by Phase 2 accuracy check — Component exports) // --------------------------------------------------------------------------- /** @@ -370,7 +500,12 @@ function isCovered(info) { const exports_ = parsePublicExports(INDEX_FILE) const categories = new Map() -const componentExports = [] // collected for Phase 2 +const componentExports = [] // collected for Phase 2a (SFC prop/slot check) + +// Categories that get a Phase 2b @param accuracy check (must have their own +// doc file — Store constants use mention-only and are skipped). +const JS_ACCURACY_CATEGORIES = new Set(['Composables', 'Utilities', 'Store factories', 'Store plugins']) +const jsExports = [] // { name, category, docPath } collected for Phase 2b for (const name of exports_) { const info = classify(name) @@ -380,6 +515,11 @@ for (const name of exports_) { if (info.category === 'Components') { componentExports.push(name) } + // Collect JS exports that have a dedicated doc file for the @param check + if (JS_ACCURACY_CATEGORIES.has(info.category) && !info.mentionFile && info.searchDir && info.expectedStem) { + const docPath = path.join(info.searchDir, `${info.expectedStem}.md`) + jsExports.push({ name, category: info.category, docPath }) + } if (!categories.has(info.category)) { categories.set(info.category, { checked: 0, missing: [] }) } @@ -424,7 +564,7 @@ if (totalMissing > 0) { console.log(`\n✓ All ${totalChecked} public exports are documented.`) // --------------------------------------------------------------------------- -// Phase 2: accuracy check — every SFC prop and named slot must appear in doc +// Phase 2a: accuracy check — every SFC prop and named slot must appear in doc // --------------------------------------------------------------------------- console.log('\nChecking component doc accuracy (props and slots):\n') @@ -445,18 +585,60 @@ const detailFailed = detailFailures.length if (detailFailed === 0) { console.log(` ✓ All ${detailChecked} component docs cover their props and slots.`) - process.exit(0) +} else { + console.log(` ✓ ${detailChecked - detailFailed}/${detailChecked} component docs cover their props and slots.`) + console.error(`\n✗ ${detailFailed} component doc(s) are missing prop or slot coverage:\n`) + for (const { name, issues } of detailFailures) { + const kebab = toKebab(name) + console.error(`${name} (docs/components/${kebab}.md):`) + for (const issue of issues) { + console.error(` - ${issue}`) + } + console.error('') + } } -console.log(` ✓ ${detailChecked - detailFailed}/${detailChecked} component docs cover their props and slots.`) -console.error(`\n✗ ${detailFailed} component doc(s) are missing prop or slot coverage:\n`) -for (const { name, issues } of detailFailures) { - const kebab = toKebab(name) - console.error(`${name} (docs/components/${kebab}.md):`) - for (const issue of issues) { - console.error(` - ${issue}`) +// --------------------------------------------------------------------------- +// Phase 2b: accuracy check — every JSDoc @param must appear in the doc +// --------------------------------------------------------------------------- + +console.log('\nChecking JS export doc accuracy (JSDoc @param names):\n') + +const jsDetailFailures = [] + +for (const { name, category, docPath } of jsExports) { + const srcPath = findSourceFile(name, category) + const issues = checkJsAccuracy(name, srcPath, docPath) + if (issues.length > 0) { + jsDetailFailures.push({ name, category, issues, docPath }) + } +} + +const jsDetailChecked = jsExports.length +const jsDetailFailed = jsDetailFailures.length + +if (jsDetailFailed === 0) { + console.log(` ✓ All ${jsDetailChecked} JS export docs cover their @param names.`) +} else { + console.log(` ✓ ${jsDetailChecked - jsDetailFailed}/${jsDetailChecked} JS export docs cover their @param names.`) + console.error(`\n✗ ${jsDetailFailed} JS export doc(s) are missing @param coverage:\n`) + for (const { name, category, issues, docPath } of jsDetailFailures) { + const rel = path.relative(ROOT, docPath).replace(/\\/g, '/') + console.error(`${name} [${category}] (${rel}):`) + for (const issue of issues) { + console.error(` - ${issue}`) + } + console.error('') } - console.error('') + console.error('Add the missing @param names to the doc so every JSDoc parameter is covered.') +} + +// --------------------------------------------------------------------------- +// Final exit +// --------------------------------------------------------------------------- + +if (detailFailed > 0 || jsDetailFailed > 0) { + process.exit(1) } -console.error('Add the missing props/slots to the component doc so every SFC interface item is covered.') -process.exit(1) +console.log('\n✓ All accuracy checks passed.') +process.exit(0) From 123d8988a7a071808d60900610a18635dd40c505 Mon Sep 17 00:00:00 2001 From: Remko Date: Mon, 4 May 2026 11:03:12 +0200 Subject: [PATCH 4/4] Updated details --- scripts/check-docs.js | 42 +++++++++++++++++++++++++++++------------- 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/scripts/check-docs.js b/scripts/check-docs.js index 296b0c9..27aaf86 100644 --- a/scripts/check-docs.js +++ b/scripts/check-docs.js @@ -604,29 +604,45 @@ if (detailFailed === 0) { console.log('\nChecking JS export doc accuracy (JSDoc @param names):\n') -const jsDetailFailures = [] +// Accumulate results per category so we can print a Phase-1-style summary +const jsDetailByCategory = new Map() +for (const cat of JS_ACCURACY_CATEGORIES) { + jsDetailByCategory.set(cat, { checked: 0, failures: [] }) +} for (const { name, category, docPath } of jsExports) { + const bucket = jsDetailByCategory.get(category) + bucket.checked += 1 const srcPath = findSourceFile(name, category) const issues = checkJsAccuracy(name, srcPath, docPath) if (issues.length > 0) { - jsDetailFailures.push({ name, category, issues, docPath }) + bucket.failures.push({ name, issues, docPath }) } } -const jsDetailChecked = jsExports.length -const jsDetailFailed = jsDetailFailures.length +const JS_ORDER = ['Composables', 'Store factories', 'Store plugins', 'Utilities'] +let jsDetailFailed = 0 -if (jsDetailFailed === 0) { - console.log(` ✓ All ${jsDetailChecked} JS export docs cover their @param names.`) -} else { - console.log(` ✓ ${jsDetailChecked - jsDetailFailed}/${jsDetailChecked} JS export docs cover their @param names.`) +for (const cat of JS_ORDER) { + const { checked, failures } = jsDetailByCategory.get(cat) + const covered = checked - failures.length + const mark = failures.length === 0 ? '✓' : '✗' + console.log(` ${mark} ${cat}: ${covered}/${checked}`) + jsDetailFailed += failures.length +} + +if (jsDetailFailed > 0) { console.error(`\n✗ ${jsDetailFailed} JS export doc(s) are missing @param coverage:\n`) - for (const { name, category, issues, docPath } of jsDetailFailures) { - const rel = path.relative(ROOT, docPath).replace(/\\/g, '/') - console.error(`${name} [${category}] (${rel}):`) - for (const issue of issues) { - console.error(` - ${issue}`) + for (const cat of JS_ORDER) { + const { failures } = jsDetailByCategory.get(cat) + if (failures.length === 0) continue + console.error(`${cat}:`) + for (const { name, issues, docPath } of failures) { + const rel = path.relative(ROOT, docPath).replace(/\\/g, '/') + console.error(` ${name} (${rel}):`) + for (const issue of issues) { + console.error(` - ${issue}`) + } } console.error('') }