@conduction/nextcloud-vue 1.0.0-beta.18 → 1.0.0-beta.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@conduction/nextcloud-vue",
3
- "version": "1.0.0-beta.18",
3
+ "version": "1.0.0-beta.19",
4
4
  "description": "Shared Vue component library for Conduction Nextcloud apps — complements @nextcloud/vue with higher-level components, OpenRegister integration, and NL Design System support",
5
5
  "license": "EUPL-1.2",
6
6
  "author": "Conduction B.V. <info@conduction.nl>",
@@ -371,6 +371,7 @@ export default {
371
371
  | `excludeFields` | Array | `[]` | Field keys to exclude from the form dialog |
372
372
  | `includeFields` | Array | `null` | Field keys to include in the form dialog (whitelist) |
373
373
  | `fieldOverrides` | Object | `{}` | Per-field config overrides passed to `CnFormDialog` |
374
+ | `customComponents` | Object | `null` | Custom-component / handler registry. When set, takes precedence over the injected `cnCustomComponents` from CnAppRoot. Used to resolve `actions[].handler` registry names declared in the manifest (manifest-actions-dispatch). |
374
375
  | `showViewToggle` | Boolean | `true` | Whether to show the Cards/Table view toggle |
375
376
  | `refreshing` | Boolean | `false` | Whether a refresh is currently in progress |
376
377
  | `refreshDisabled` | Boolean | `false` | Whether the refresh button is disabled |
@@ -215,7 +215,7 @@
215
215
  <CnRowActions
216
216
  :actions="mergedActions"
217
217
  :row="row"
218
- @action="$emit('action', $event)" />
218
+ @action="onRowAction" />
219
219
  </slot>
220
220
  </template>
221
221
  </CnDataTable>
@@ -257,7 +257,7 @@
257
257
  <CnRowActions
258
258
  :actions="mergedActions"
259
259
  :row="object"
260
- @action="$emit('action', $event)" />
260
+ @action="onRowAction" />
261
261
  </slot>
262
262
  </template>
263
263
  </CnCardGrid>
@@ -267,7 +267,7 @@
267
267
  :open.sync="contextMenuOpen"
268
268
  :actions="mergedActions"
269
269
  :target-item="contextMenuRow"
270
- @action="$emit('action', $event)"
270
+ @action="onRowAction"
271
271
  @close="closeContextMenu" />
272
272
 
273
273
  <!-- Pagination -->
@@ -433,14 +433,20 @@ export default {
433
433
  CnIndexSidebar,
434
434
  },
435
435
 
436
+ /**
437
+ * Inject the customComponents registry from a CnAppRoot ancestor.
438
+ * Used by:
439
+ * - REQ-MAD-3 / REQ-MAD-8 (manifest-actions-dispatch): resolves
440
+ * `actions[].handler` registry names to functions called on
441
+ * row-action click.
442
+ * - The cardComponent + form-dialog override paths: when set, the
443
+ * prop-level `customComponents` wins, but the inject is the
444
+ * default. See `effectiveCustomComponents`.
445
+ *
446
+ * Falls back to an empty object so `CnIndexPage` works standalone
447
+ * (unit tests, isolated mount) without `CnAppRoot`.
448
+ */
436
449
  inject: {
437
- /**
438
- * Custom-component registry provided by `CnAppRoot`. Falls back
439
- * to an empty object so `CnIndexPage` works when mounted
440
- * without `CnAppRoot` (e.g. in unit tests). The explicit
441
- * `customComponents` prop wins when set — see
442
- * `effectiveCustomComponents`.
443
- */
444
450
  cnCustomComponents: { default: () => ({}) },
445
451
  },
446
452
 
@@ -783,6 +789,11 @@ export default {
783
789
  * `cnCustomComponents`. Provided primarily so unit tests can
784
790
  * pass a registry without mounting `CnAppRoot`.
785
791
  *
792
+ * Used by:
793
+ * - `cardComponent` resolution (REQ-MCI from manifest-card-index)
794
+ * - `actions[].handler` registry name resolution (REQ-MAD-3 from
795
+ * manifest-actions-dispatch — handler funcs called on row-action click)
796
+ *
786
797
  * @type {object|null}
787
798
  */
788
799
  customComponents: {
@@ -887,9 +898,56 @@ export default {
887
898
  return builtIn
888
899
  },
889
900
 
890
- /** Merged actions: app-provided first, then built-in defaults */
901
+ /**
902
+ * Effective customComponents registry — explicit prop wins over
903
+ * the injected ancestor registry. Used to:
904
+ * - Resolve `actions[].handler` registry names (REQ-MAD-3,
905
+ * manifest-actions-dispatch).
906
+ * - Resolve the `cardComponent` name for card-grid view (REQ-MCI,
907
+ * manifest-card-index).
908
+ *
909
+ * @return {object}
910
+ */
911
+ effectiveCustomComponents() {
912
+ return this.customComponents ?? this.cnCustomComponents ?? {}
913
+ },
914
+
915
+ /**
916
+ * Merged actions: app-provided first, then built-in defaults.
917
+ *
918
+ * REQ-MAD-3 / REQ-MAD-4 / REQ-MAD-5 / REQ-MAD-6 / REQ-MAD-7
919
+ * (manifest-actions-dispatch) — for any action whose `handler`
920
+ * is a string, resolve it through `resolveHandler()` so
921
+ * `CnRowActions` sees the same `{ handler: fn }` shape it does
922
+ * for built-in defaults. Function-typed handlers (the existing
923
+ * runtime path) pass through untouched.
924
+ */
891
925
  mergedActions() {
892
- return [...this.actions, ...this.defaultActions]
926
+ const dispatched = this.actions.map((action) => {
927
+ if (typeof action.handler === 'function') {
928
+ // Back-compat: programmatic function handler — keep as-is.
929
+ return action
930
+ }
931
+ if (typeof action.handler !== 'string' || action.handler.length === 0) {
932
+ // No handler → emit-only path (existing default).
933
+ return action
934
+ }
935
+ const isNone = action.handler === 'none'
936
+ const resolved = this.resolveHandler(action)
937
+ if (resolved) {
938
+ // `none` returns a sentinel no-op handler AND must
939
+ // suppress the `@action` emit; flag it so onRowAction
940
+ // can drop the bubbled event.
941
+ return isNone
942
+ ? { ...action, handler: resolved, _dispatchSuppress: true }
943
+ : { ...action, handler: resolved }
944
+ }
945
+ // Either reserved keyword "emit" / unknown name / non-function
946
+ // registry entry → page emits @action only; no handler call.
947
+ const { handler, ...rest } = action
948
+ return rest
949
+ })
950
+ return [...dispatched, ...this.defaultActions]
893
951
  },
894
952
 
895
953
  hasRowActions() {
@@ -937,17 +995,6 @@ export default {
937
995
  return (this.sidebar && this.sidebar.search) || {}
938
996
  },
939
997
 
940
- /**
941
- * Effective customComponents registry used to resolve the
942
- * `cardComponent` name. Explicit prop wins, inject falls back,
943
- * empty object is the last resort.
944
- *
945
- * @return {object}
946
- */
947
- effectiveCustomComponents() {
948
- return this.customComponents ?? this.cnCustomComponents ?? {}
949
- },
950
-
951
998
  /**
952
999
  * Resolved card component for card-grid view mode. Returns
953
1000
  * `null` when `cardComponent` is empty OR when the name is not
@@ -984,6 +1031,89 @@ export default {
984
1031
  },
985
1032
 
986
1033
  methods: {
1034
+ /**
1035
+ * REQ-MAD-3 / REQ-MAD-4 / REQ-MAD-5 / REQ-MAD-6 / REQ-MAD-7
1036
+ * (manifest-actions-dispatch) — Resolve a manifest-declared
1037
+ * action's `handler` string into a `(row) => void` invocation
1038
+ * function. Returns null when the action should fall back to
1039
+ * the page's `@action`-event-only path.
1040
+ *
1041
+ * - Reserved keyword `"navigate"` → push the configured route
1042
+ * with `params: { id: row[rowKey] }`.
1043
+ * - Reserved keyword `"emit"` → null (page still bubbles
1044
+ * `@action`; explicit no-op).
1045
+ * - Reserved keyword `"none"` → returns a no-op function that
1046
+ * suppresses both the handler and the `@action` emit. The
1047
+ * suppression happens via the special `_dispatchSuppress`
1048
+ * flag on the cloned action; see mergedActions for the
1049
+ * detail.
1050
+ * - Registry name → look up in `effectiveCustomComponents`;
1051
+ * when it's a function, wrap as
1052
+ * `(row) => fn({ actionId: action.id, item: row })`. When
1053
+ * it's a non-function, console.warn and return null.
1054
+ * - Unknown registry name → silent fall-through (null).
1055
+ *
1056
+ * @param {object} action The manifest-shaped action object.
1057
+ * @return {Function|null}
1058
+ */
1059
+ resolveHandler(action) {
1060
+ const name = action.handler
1061
+ if (typeof name !== 'string' || name.length === 0) return null
1062
+ if (name === 'navigate') {
1063
+ const route = action.route
1064
+ if (typeof route !== 'string' || route.length === 0) {
1065
+ // eslint-disable-next-line no-console
1066
+ console.warn(
1067
+ `[CnIndexPage] action "${action.id}" declares handler:"navigate" `
1068
+ + 'but route is missing; falling back to @action-only.',
1069
+ )
1070
+ return null
1071
+ }
1072
+ return (row) => {
1073
+ this.$router.push({
1074
+ name: route,
1075
+ params: { id: row[this.rowKey] },
1076
+ })
1077
+ }
1078
+ }
1079
+ if (name === 'emit') return null
1080
+ if (name === 'none') {
1081
+ // Returns a sentinel that CnRowActions will treat as a
1082
+ // no-op; we additionally short-circuit @action emit in
1083
+ // `onRowAction` via the action's id.
1084
+ return () => {}
1085
+ }
1086
+ const fn = this.effectiveCustomComponents[name]
1087
+ if (typeof fn === 'function') {
1088
+ return (row) => fn({ actionId: action.id, item: row })
1089
+ }
1090
+ if (fn !== undefined) {
1091
+ // eslint-disable-next-line no-console
1092
+ console.warn(
1093
+ `[CnIndexPage] action.handler "${name}" resolved to a non-function in `
1094
+ + 'customComponents — components belong to slot overrides; falling '
1095
+ + 'back to @action-only.',
1096
+ )
1097
+ }
1098
+ return null
1099
+ },
1100
+
1101
+ /**
1102
+ * REQ-MAD-6 (manifest-actions-dispatch) — `handler: "none"`
1103
+ * blocks the `@action` emit entirely. CnRowActions emits
1104
+ * `@action` with `{ action: action.label, row }` and the page
1105
+ * forwards via `@action="$emit('action', $event)"`. This handler
1106
+ * intercepts so the `none`-flagged action is dropped before
1107
+ * re-emit.
1108
+ *
1109
+ * @param {{action: string, row: object}} payload The CnRowActions emit.
1110
+ */
1111
+ onRowAction(payload) {
1112
+ const matched = this.mergedActions.find((a) => a.label === payload.action)
1113
+ if (matched && matched._dispatchSuppress) return
1114
+ this.$emit('action', payload)
1115
+ },
1116
+
987
1117
  /**
988
1118
  * Handle row click — emits row-click event for the parent to handle navigation.
989
1119
  * @param {object} row The clicked row object
@@ -315,6 +315,15 @@
315
315
  "type": "boolean",
316
316
  "default": false,
317
317
  "description": "When true the consumer SHOULD show a confirmation dialog before invoking the action. Useful for destructive actions; defaults to false."
318
+ },
319
+ "handler": {
320
+ "type": "string",
321
+ "description": "Optional dispatch target for the action. Either (a) one of the reserved keywords \"navigate\" / \"emit\" / \"none\", or (b) a registry name resolving to a function in the customComponents map passed to CnAppRoot. When the registry name resolves to a function, CnIndexPage / CnDetailPage call it with `{ actionId, item }` on row-action click. The reserved keywords short-circuit the registry lookup: \"navigate\" calls `$router.push({ name: action.route, params: { id: row[rowKey] } })`; \"emit\" emits `@action` only (semantic-explicit no-op); \"none\" disables the click entirely. When unset (the default), the action only emits `@action` and the page-level listener decides the side-effect — preserves v1.2 behaviour. Added in schema 1.3.0.",
322
+ "pattern": "^(navigate|emit|none|[A-Za-z][A-Za-z0-9_]*)$"
323
+ },
324
+ "route": {
325
+ "type": "string",
326
+ "description": "Vue-router route name dispatched when `handler === \"navigate\"`. Required for the navigate keyword; ignored for other handler values. CnIndexPage uses it as `$router.push({ name: action.route, params: { id: row[rowKey] } })`. Added in schema 1.3.0."
318
327
  }
319
328
  }
320
329
  },
@@ -563,9 +563,34 @@ function validateActionsArray(cfg, pathSlash, pathBracket, errors) {
563
563
  if (typeof action.label !== 'string' || action.label.length === 0) {
564
564
  errors.push(`${actionPath}/label: must be a non-empty string`)
565
565
  }
566
+ // REQ-MAD-1 / REQ-MAD-2 — `handler` (string, registry name OR
567
+ // reserved keyword `navigate`/`emit`/`none`) and the matching
568
+ // `navigate` requirement on `route`. Schema 1.3.0+.
569
+ if (action.handler !== undefined) {
570
+ if (typeof action.handler !== 'string') {
571
+ errors.push(`${actionPath}/handler: must be a string when set`)
572
+ } else if (!HANDLER_PATTERN.test(action.handler)) {
573
+ errors.push(
574
+ `${actionPath}/handler: "${action.handler}" must match `
575
+ + '"navigate" | "emit" | "none" | [A-Za-z][A-Za-z0-9_]*',
576
+ )
577
+ }
578
+ if (action.handler === 'navigate'
579
+ && (typeof action.route !== 'string' || action.route.length === 0)) {
580
+ errors.push(`${actionPath}/route: required when handler is "navigate"`)
581
+ }
582
+ }
566
583
  })
567
584
  }
568
585
 
586
+ /**
587
+ * REQ-MAD-1 — Allowed shapes for `actions[].handler`. Either a
588
+ * reserved keyword (`navigate` | `emit` | `none`) or a JS-identifier
589
+ * registry name (alphanumeric + underscore, leading letter). Mirrors
590
+ * the schema's `pattern` on the `handler` property.
591
+ */
592
+ const HANDLER_PATTERN = /^(navigate|emit|none|[A-Za-z][A-Za-z0-9_]*)$/
593
+
569
594
  /**
570
595
  * Validate `config.widgets[]` for dashboard page type
571
596
  * (`manifest-config-refs` REQ-MCR). Each entry MUST be an object with