@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/dist/nextcloud-vue.cjs.js +178 -33
- package/dist/nextcloud-vue.cjs.js.map +1 -1
- package/dist/nextcloud-vue.esm.js +178 -33
- package/dist/nextcloud-vue.esm.js.map +1 -1
- package/package.json +1 -1
- package/src/components/CnIndexPage/CnIndexPage.md +1 -0
- package/src/components/CnIndexPage/CnIndexPage.vue +153 -23
- package/src/schemas/app-manifest.schema.json +9 -0
- package/src/utils/validateManifest.js +25 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@conduction/nextcloud-vue",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
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="
|
|
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="
|
|
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="
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|