@conduction/nextcloud-vue 2.65.0 → 2.66.0

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.
Files changed (61) hide show
  1. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +99 -2
  2. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  3. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +438 -383
  4. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  5. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue2.js +9 -3
  6. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue2.js.map +1 -1
  7. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue3.js +1 -1
  8. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue3.js.map +1 -1
  9. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +78 -21
  10. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  11. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +129 -96
  12. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  13. package/dist/esm/components/CnFkResolveCell/CnFkResolveCell.vue2.js +3 -1
  14. package/dist/esm/components/CnFkResolveCell/CnFkResolveCell.vue2.js.map +1 -1
  15. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +144 -4
  16. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
  17. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
  18. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js +63 -7
  19. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js.map +1 -1
  20. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js +2 -1
  21. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js.map +1 -1
  22. package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue2.js +11 -0
  23. package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue2.js.map +1 -1
  24. package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue3.js +2 -1
  25. package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue3.js.map +1 -1
  26. package/dist/esm/l10n/en.json.js +3 -0
  27. package/dist/esm/l10n/en.json.js.map +1 -1
  28. package/dist/esm/l10n/nl.json.js +3 -0
  29. package/dist/esm/l10n/nl.json.js.map +1 -1
  30. package/dist/esm/mixins/pageViews.js +400 -0
  31. package/dist/esm/mixins/pageViews.js.map +1 -0
  32. package/dist/esm/nextcloud-vue.css +18 -0
  33. package/dist/esm/utils/actionsDispatcher.js +11 -2
  34. package/dist/esm/utils/actionsDispatcher.js.map +1 -1
  35. package/dist/esm/utils/columnFilters.js +27 -3
  36. package/dist/esm/utils/columnFilters.js.map +1 -1
  37. package/dist/esm/utils/schema.js +40 -2
  38. package/dist/esm/utils/schema.js.map +1 -1
  39. package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
  40. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  41. package/dist/nextcloud-vue.cjs.js +1521 -596
  42. package/dist/nextcloud-vue.cjs.js.map +2 -2
  43. package/dist/nextcloud-vue.css +18 -0
  44. package/l10n/en.json +3 -0
  45. package/l10n/nl.json +3 -0
  46. package/package.json +1 -1
  47. package/src/components/CnDashboardPage/CnDashboardPage.vue +165 -8
  48. package/src/components/CnDataTable/CnColumnFilterPopover.vue +11 -5
  49. package/src/components/CnDetailPage/CnDetailPage.vue +94 -5
  50. package/src/components/CnFkResolveCell/CnFkResolveCell.vue +3 -1
  51. package/src/components/CnFormDialog/CnFormDialog.vue +144 -4
  52. package/src/components/CnHeaderWidget/CnHeaderWidget.vue +67 -8
  53. package/src/components/CnSegmentedControl/CnSegmentedControl.md +1 -1
  54. package/src/components/CnSegmentedControl/CnSegmentedControl.vue +12 -0
  55. package/src/css/detail-page.css +5 -0
  56. package/src/mixins/pageViews.js +396 -0
  57. package/src/schemas/app-manifest-v2.schema.json +29 -1
  58. package/src/utils/actionsDispatcher.js +11 -2
  59. package/src/utils/columnFilters.js +29 -2
  60. package/src/utils/schema.js +40 -2
  61. package/src/utils/validateManifestV2.compiled.js +2 -2
@@ -125,7 +125,9 @@ var script = {
125
125
  if (!store) {
126
126
  return
127
127
  }
128
- const type = resolveObjectOpType(store, { register: this.register, schema: this.schema });
128
+ // `schema` is a slug the manifest author wrote, not a `$ref` title:
129
+ // use it exactly as given (a camelCase slug must not be kebab-cased).
130
+ const type = resolveObjectOpType(store, { register: this.register, schema: this.schema }, { exactSchema: true });
129
131
  await Promise.all(this.ids.map(async (id) => {
130
132
  if (this.labels[id]) {
131
133
  return
@@ -1 +1 @@
1
- {"version":3,"file":"CnFkResolveCell.vue2.js","sources":["../../../../src/components/CnFkResolveCell/CnFkResolveCell.vue"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<span class=\"cn-fk-resolve-cell\" :title=\"ids.join(', ')\">{{ display }}</span>\n</template>\n\n<script>\nimport { useObjectStore } from '../../store/useObjectStore.js'\nimport { resolveObjectOpType } from '../../utils/actionsDispatcher.js'\n\n/**\n * CnFkResolveCell — built-in `fkResolve` cell widget: resolves a reference\n * uuid (or an array of uuids) to the related object's display label.\n *\n * Fetches through the shared object store (`useObjectStore`), which gives\n * per-schema caching for free: objects are cached under the\n * `<register>/<schema>` type slug and concurrent fetches for the same id are\n * de-duplicated, so a column of 50 rows pointing at the same client resolves\n * with a single request. Unresolvable ids (fetch failure, missing label\n * field, no Pinia store) degrade to the raw id — the cell never blanks out\n * or throws.\n *\n * Declared on a table column via the cell-widget mechanism:\n * ```js\n * {\n * key: 'client',\n * label: 'Client',\n * widget: 'fkResolve',\n * widgetProps: { register: 'crm', schema: 'client', labelField: 'name' },\n * }\n * ```\n */\nexport default {\n\tname: 'CnFkResolveCell',\n\n\tprops: {\n\t\t/**\n\t\t * The reference value: a single object uuid/id, or an array of them\n\t\t * (multi-reference properties render comma-joined labels).\n\t\t *\n\t\t * @type {string|number|Array<string|number>|null}\n\t\t */\n\t\tvalue: {\n\t\t\ttype: [String, Number, Array],\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** OpenRegister register slug (or id) the reference points into. */\n\t\tregister: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** OpenRegister schema slug (or id) the reference points into. */\n\t\tschema: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Property on the referenced object used as the display label.\n\t\t * Falls back to `title`, then `@self.name`, then the raw id.\n\t\t */\n\t\tlabelField: {\n\t\t\ttype: String,\n\t\t\tdefault: 'name',\n\t\t},\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\t/**\n\t\t\t * Resolved labels keyed by String(id). Filled from the store cache\n\t\t\t * synchronously where possible, otherwise after the fetch resolves.\n\t\t\t *\n\t\t\t * @type {Record<string, string>}\n\t\t\t */\n\t\t\tlabels: {},\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The reference ids as a normalized string array (empty when the\n\t\t * value is unset).\n\t\t *\n\t\t * @return {string[]}\n\t\t */\n\t\tids() {\n\t\t\tif (this.value === null || this.value === undefined || this.value === '') {\n\t\t\t\treturn []\n\t\t\t}\n\t\t\tconst list = Array.isArray(this.value) ? this.value : [this.value]\n\t\t\treturn list\n\t\t\t\t.filter((id) => id !== null && id !== undefined && id !== '')\n\t\t\t\t.map((id) => String(id))\n\t\t},\n\n\t\t/**\n\t\t * The rendered text: each id's resolved label (raw id while\n\t\t * resolving / when unresolvable), comma-joined for multi-refs.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tdisplay() {\n\t\t\treturn this.ids.map((id) => this.labels[id] || id).join(', ')\n\t\t},\n\t},\n\n\twatch: {\n\t\tids: {\n\t\t\timmediate: true,\n\t\t\thandler() {\n\t\t\t\tthis.resolveAll()\n\t\t\t},\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Resolve every id via the shared object store: cache hit renders\n\t\t * synchronously, misses fetch (store-level in-flight dedup applies).\n\t\t * A failed / label-less fetch leaves the raw id in place.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync resolveAll() {\n\t\t\tif (!this.register || !this.schema || this.ids.length === 0) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst store = this.getObjectStore()\n\t\t\tif (!store) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst type = resolveObjectOpType(store, { register: this.register, schema: this.schema })\n\t\t\tawait Promise.all(this.ids.map(async (id) => {\n\t\t\t\tif (this.labels[id]) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tconst cached = store.objects && store.objects[type] && store.objects[type][id]\n\t\t\t\tconst obj = cached || await this.fetchOne(store, type, id)\n\t\t\t\tconst label = this.pickLabel(obj)\n\t\t\t\tif (label) {\n\t\t\t\t\tthis.labels[id] = label\n\t\t\t\t}\n\t\t\t}))\n\t\t},\n\n\t\t/**\n\t\t * Fetch one referenced object; null on any failure so the raw id\n\t\t * stays visible.\n\t\t *\n\t\t * @param {object} store The object store instance.\n\t\t * @param {string} type The `<register>/<schema>` type slug.\n\t\t * @param {string} id The object id.\n\t\t * @return {Promise<object|null>}\n\t\t */\n\t\tasync fetchOne(store, type, id) {\n\t\t\ttry {\n\t\t\t\treturn await store.fetchObject(type, id)\n\t\t\t} catch {\n\t\t\t\treturn null\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Extract the display label off a resolved object: `labelField`\n\t\t * first, then `title`, then `@self.name`. Translatable per-language\n\t\t * maps collapse to their first value.\n\t\t *\n\t\t * @param {object|null} obj The referenced object.\n\t\t * @return {string} The label, or '' when none is usable.\n\t\t */\n\t\tpickLabel(obj) {\n\t\t\tif (!obj || typeof obj !== 'object') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst candidates = [obj[this.labelField], obj.title, obj['@self'] && obj['@self'].name]\n\t\t\tfor (const raw of candidates) {\n\t\t\t\tif (typeof raw === 'string' && raw !== '') {\n\t\t\t\t\treturn raw\n\t\t\t\t}\n\t\t\t\tif (typeof raw === 'number') {\n\t\t\t\t\treturn String(raw)\n\t\t\t\t}\n\t\t\t\tif (raw && typeof raw === 'object' && !Array.isArray(raw)) {\n\t\t\t\t\tconst first = Object.values(raw).find((v) => typeof v === 'string' && v !== '')\n\t\t\t\t\tif (first) {\n\t\t\t\t\t\treturn first\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn ''\n\t\t},\n\n\t\t/**\n\t\t * The shared object store, or null when no Pinia is active (the cell\n\t\t * then renders the raw id).\n\t\t *\n\t\t * @return {object|null}\n\t\t */\n\t\tgetObjectStore() {\n\t\t\ttry {\n\t\t\t\treturn useObjectStore()\n\t\t\t} catch {\n\t\t\t\treturn null\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-fk-resolve-cell {\n\twhite-space: nowrap;\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n}\n</style>\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"CnFkResolveCell.vue2.js","sources":["../../../../src/components/CnFkResolveCell/CnFkResolveCell.vue"],"sourcesContent":["<!--\n - SPDX-FileCopyrightText: 2026 Conduction B.V.\n - SPDX-License-Identifier: EUPL-1.2\n-->\n\n<template>\n\t<span class=\"cn-fk-resolve-cell\" :title=\"ids.join(', ')\">{{ display }}</span>\n</template>\n\n<script>\nimport { useObjectStore } from '../../store/useObjectStore.js'\nimport { resolveObjectOpType } from '../../utils/actionsDispatcher.js'\n\n/**\n * CnFkResolveCell — built-in `fkResolve` cell widget: resolves a reference\n * uuid (or an array of uuids) to the related object's display label.\n *\n * Fetches through the shared object store (`useObjectStore`), which gives\n * per-schema caching for free: objects are cached under the\n * `<register>/<schema>` type slug and concurrent fetches for the same id are\n * de-duplicated, so a column of 50 rows pointing at the same client resolves\n * with a single request. Unresolvable ids (fetch failure, missing label\n * field, no Pinia store) degrade to the raw id — the cell never blanks out\n * or throws.\n *\n * Declared on a table column via the cell-widget mechanism:\n * ```js\n * {\n * key: 'client',\n * label: 'Client',\n * widget: 'fkResolve',\n * widgetProps: { register: 'crm', schema: 'client', labelField: 'name' },\n * }\n * ```\n */\nexport default {\n\tname: 'CnFkResolveCell',\n\n\tprops: {\n\t\t/**\n\t\t * The reference value: a single object uuid/id, or an array of them\n\t\t * (multi-reference properties render comma-joined labels).\n\t\t *\n\t\t * @type {string|number|Array<string|number>|null}\n\t\t */\n\t\tvalue: {\n\t\t\ttype: [String, Number, Array],\n\t\t\tdefault: null,\n\t\t},\n\n\t\t/** OpenRegister register slug (or id) the reference points into. */\n\t\tregister: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/** OpenRegister schema slug (or id) the reference points into. */\n\t\tschema: {\n\t\t\ttype: String,\n\t\t\tdefault: '',\n\t\t},\n\n\t\t/**\n\t\t * Property on the referenced object used as the display label.\n\t\t * Falls back to `title`, then `@self.name`, then the raw id.\n\t\t */\n\t\tlabelField: {\n\t\t\ttype: String,\n\t\t\tdefault: 'name',\n\t\t},\n\t},\n\n\tdata() {\n\t\treturn {\n\t\t\t/**\n\t\t\t * Resolved labels keyed by String(id). Filled from the store cache\n\t\t\t * synchronously where possible, otherwise after the fetch resolves.\n\t\t\t *\n\t\t\t * @type {Record<string, string>}\n\t\t\t */\n\t\t\tlabels: {},\n\t\t}\n\t},\n\n\tcomputed: {\n\t\t/**\n\t\t * The reference ids as a normalized string array (empty when the\n\t\t * value is unset).\n\t\t *\n\t\t * @return {string[]}\n\t\t */\n\t\tids() {\n\t\t\tif (this.value === null || this.value === undefined || this.value === '') {\n\t\t\t\treturn []\n\t\t\t}\n\t\t\tconst list = Array.isArray(this.value) ? this.value : [this.value]\n\t\t\treturn list\n\t\t\t\t.filter((id) => id !== null && id !== undefined && id !== '')\n\t\t\t\t.map((id) => String(id))\n\t\t},\n\n\t\t/**\n\t\t * The rendered text: each id's resolved label (raw id while\n\t\t * resolving / when unresolvable), comma-joined for multi-refs.\n\t\t *\n\t\t * @return {string}\n\t\t */\n\t\tdisplay() {\n\t\t\treturn this.ids.map((id) => this.labels[id] || id).join(', ')\n\t\t},\n\t},\n\n\twatch: {\n\t\tids: {\n\t\t\timmediate: true,\n\t\t\thandler() {\n\t\t\t\tthis.resolveAll()\n\t\t\t},\n\t\t},\n\t},\n\n\tmethods: {\n\t\t/**\n\t\t * Resolve every id via the shared object store: cache hit renders\n\t\t * synchronously, misses fetch (store-level in-flight dedup applies).\n\t\t * A failed / label-less fetch leaves the raw id in place.\n\t\t *\n\t\t * @return {Promise<void>}\n\t\t */\n\t\tasync resolveAll() {\n\t\t\tif (!this.register || !this.schema || this.ids.length === 0) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tconst store = this.getObjectStore()\n\t\t\tif (!store) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\t// `schema` is a slug the manifest author wrote, not a `$ref` title:\n\t\t\t// use it exactly as given (a camelCase slug must not be kebab-cased).\n\t\t\tconst type = resolveObjectOpType(store, { register: this.register, schema: this.schema }, { exactSchema: true })\n\t\t\tawait Promise.all(this.ids.map(async (id) => {\n\t\t\t\tif (this.labels[id]) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tconst cached = store.objects && store.objects[type] && store.objects[type][id]\n\t\t\t\tconst obj = cached || await this.fetchOne(store, type, id)\n\t\t\t\tconst label = this.pickLabel(obj)\n\t\t\t\tif (label) {\n\t\t\t\t\tthis.labels[id] = label\n\t\t\t\t}\n\t\t\t}))\n\t\t},\n\n\t\t/**\n\t\t * Fetch one referenced object; null on any failure so the raw id\n\t\t * stays visible.\n\t\t *\n\t\t * @param {object} store The object store instance.\n\t\t * @param {string} type The `<register>/<schema>` type slug.\n\t\t * @param {string} id The object id.\n\t\t * @return {Promise<object|null>}\n\t\t */\n\t\tasync fetchOne(store, type, id) {\n\t\t\ttry {\n\t\t\t\treturn await store.fetchObject(type, id)\n\t\t\t} catch {\n\t\t\t\treturn null\n\t\t\t}\n\t\t},\n\n\t\t/**\n\t\t * Extract the display label off a resolved object: `labelField`\n\t\t * first, then `title`, then `@self.name`. Translatable per-language\n\t\t * maps collapse to their first value.\n\t\t *\n\t\t * @param {object|null} obj The referenced object.\n\t\t * @return {string} The label, or '' when none is usable.\n\t\t */\n\t\tpickLabel(obj) {\n\t\t\tif (!obj || typeof obj !== 'object') {\n\t\t\t\treturn ''\n\t\t\t}\n\t\t\tconst candidates = [obj[this.labelField], obj.title, obj['@self'] && obj['@self'].name]\n\t\t\tfor (const raw of candidates) {\n\t\t\t\tif (typeof raw === 'string' && raw !== '') {\n\t\t\t\t\treturn raw\n\t\t\t\t}\n\t\t\t\tif (typeof raw === 'number') {\n\t\t\t\t\treturn String(raw)\n\t\t\t\t}\n\t\t\t\tif (raw && typeof raw === 'object' && !Array.isArray(raw)) {\n\t\t\t\t\tconst first = Object.values(raw).find((v) => typeof v === 'string' && v !== '')\n\t\t\t\t\tif (first) {\n\t\t\t\t\t\treturn first\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn ''\n\t\t},\n\n\t\t/**\n\t\t * The shared object store, or null when no Pinia is active (the cell\n\t\t * then renders the raw id).\n\t\t *\n\t\t * @return {object|null}\n\t\t */\n\t\tgetObjectStore() {\n\t\t\ttry {\n\t\t\t\treturn useObjectStore()\n\t\t\t} catch {\n\t\t\t\treturn null\n\t\t\t}\n\t\t},\n\t},\n}\n</script>\n\n<style scoped>\n.cn-fk-resolve-cell {\n\twhite-space: nowrap;\n\toverflow: hidden;\n\ttext-overflow: ellipsis;\n}\n</style>\n"],"names":[],"mappings":"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"}
@@ -10,6 +10,7 @@ import { formDraftMixin, draftKey, readDraft } from '../../composables/useFormDr
10
10
  import { useIntegrationRegistry } from '../../composables/useIntegrationRegistry.js';
11
11
  import { TENANT_CONTEXT_KEY } from '../../composables/useTenantContext.js';
12
12
  import { useObjectStore } from '../../store/useObjectStore.js';
13
+ import { resolveCreateOverrideHandler } from '../../utils/actionsDispatcher.js';
13
14
  import { extendsFormDeclarations, prefillDeclarations, definitionQueryParams, propertiesFromDefinitions, prefillValues, splitDynamicFormData, DYNAMIC_KEY_PREFIX } from '../../utils/dynamicProperties.js';
14
15
  import { shouldShow } from '../../utils/fieldCondition.js';
15
16
  import { resolveNextcloudGroup, searchNextcloudGroups } from '../../utils/groupAutocomplete.js';
@@ -246,6 +247,18 @@ var script = {
246
247
  * identity when used standalone (no CnAppRoot ancestor).
247
248
  */
248
249
  cnTranslate: { default: () => (key) => key },
250
+
251
+ /**
252
+ * The app manifest, the v2 registry, the legacy customComponents map and
253
+ * the registry-modal opener, all provided by CnAppRoot. A select-or-create
254
+ * picker reads them to create an object the way the app's own index page
255
+ * does (`config.createOverride` / `config.createModal`). Standalone (no
256
+ * CnAppRoot) they are absent and the generic nested form is used.
257
+ */
258
+ cnManifest: { default: null },
259
+ cnRegistry: { default: null },
260
+ cnCustomComponents: { default: null },
261
+ cnOpenModal: { default: null },
249
262
  },
250
263
 
251
264
  props: {
@@ -2138,6 +2151,16 @@ var script = {
2138
2151
  store.registerObjectType(slug, schemaRef, register);
2139
2152
  }
2140
2153
  const labelField = this.referenceLabelField(field);
2154
+ // The app may own how this schema is created (pipelinq's client and
2155
+ // contact need a contact-first endpoint; a plain save 400s). A
2156
+ // create override keeps this form and saves through the app's
2157
+ // handler; a create modal (only when no override exists) opens the
2158
+ // app's own dialog instead of the generic form.
2159
+ const appCreate = this.appCreateFor(register, schemaRef);
2160
+ if (!appCreate.override && appCreate.modal) {
2161
+ return this.openAppCreateModal(appCreate.modal, store, slug, labelField, term)
2162
+ }
2163
+ const override = appCreate.override;
2141
2164
  let schema = null;
2142
2165
  try {
2143
2166
  schema = await store.fetchSchema(slug);
@@ -2145,14 +2168,113 @@ var script = {
2145
2168
  schema = null;
2146
2169
  }
2147
2170
  if (!schema || !schema.properties) {
2148
- return store.saveObject(slug, { [labelField]: term })
2171
+ const payload = { [labelField]: term };
2172
+ return override
2173
+ ? override(payload, { register, schema: schemaRef, objectType: slug, effectiveSchema: null })
2174
+ : store.saveObject(slug, payload)
2149
2175
  }
2150
2176
  const initialData = {};
2151
2177
  if (term && schema.properties[labelField]) {
2152
2178
  initialData[labelField] = term;
2153
2179
  }
2154
2180
  return new Promise((resolve) => {
2155
- this.nestedCreate = { field, schema, register, slug, initialData, resolve };
2181
+ this.nestedCreate = { field, schema, register, slug, schemaRef, initialData, override, resolve };
2182
+ })
2183
+ },
2184
+
2185
+ /**
2186
+ * How the app creates objects of a schema, read off the manifest page
2187
+ * that lists them: the index page whose `config.schema` (and, when it
2188
+ * names one, `config.register`) matches. Its `config.createOverride` is
2189
+ * resolved to the registered handler; its `config.createModal` is the
2190
+ * registry key of a `kind: 'modal'` dialog.
2191
+ *
2192
+ * @spec openspec/changes/review-round-two/specs/schema-utilities/spec.md
2193
+ * @param {string} register The reference's register.
2194
+ * @param {string} schemaRef The reference's schema slug or id.
2195
+ * @return {{override: (((formData: object, ctx: object) => Promise<object>)|null), modal: (string|null)}} What the app declared.
2196
+ */
2197
+ appCreateFor(register, schemaRef) {
2198
+ const none = { override: null, modal: null };
2199
+ const pages = this.cnManifest && Array.isArray(this.cnManifest.pages) ? this.cnManifest.pages : [];
2200
+ const wanted = String(schemaRef).toLowerCase();
2201
+ const page = pages.find((p) => {
2202
+ const config = p && p.config;
2203
+ if (!config || config.schema === undefined || config.schema === null) {
2204
+ return false
2205
+ }
2206
+ if (String(config.schema).toLowerCase() !== wanted) {
2207
+ return false
2208
+ }
2209
+ return !config.register || String(config.register) === String(register)
2210
+ });
2211
+ if (!page) {
2212
+ return none
2213
+ }
2214
+ const config = page.config;
2215
+ const override = typeof config.createOverride === 'function'
2216
+ ? config.createOverride
2217
+ : resolveCreateOverrideHandler(config.createOverride, this.cnRegistry, this.cnCustomComponents);
2218
+ const modalEntry = typeof config.createModal === 'string' && config.createModal !== ''
2219
+ ? (this.cnRegistry || {})[config.createModal]
2220
+ : null;
2221
+ const modal = modalEntry && modalEntry.kind === 'modal' && typeof this.cnOpenModal === 'function'
2222
+ ? config.createModal
2223
+ : null;
2224
+ return { override: override || null, modal }
2225
+ },
2226
+
2227
+ /**
2228
+ * Open the app's own create dialog (a registry modal) for a picker and
2229
+ * wait for it. The dialog reports the new object (or its id) with a
2230
+ * `created` event; `close` without one resolves null so the field stays
2231
+ * as it was. A reported id is fetched so the picker can label it.
2232
+ *
2233
+ * @spec openspec/changes/review-round-two/specs/schema-utilities/spec.md
2234
+ * @param {string} key The registry key of the modal.
2235
+ * @param {object} store The object store.
2236
+ * @param {string} slug The object type slug.
2237
+ * @param {string} labelField The property the typed term belongs in.
2238
+ * @param {string} term The typed search term.
2239
+ * @return {Promise<object|null>} The created object, or null.
2240
+ */
2241
+ openAppCreateModal(key, store, slug, labelField, term) {
2242
+ return new Promise((resolve) => {
2243
+ let settled = false;
2244
+ let reported = false;
2245
+ const settle = (value) => {
2246
+ if (!settled) {
2247
+ settled = true;
2248
+ resolve(value);
2249
+ }
2250
+ };
2251
+ this.cnOpenModal(key, {
2252
+ initialData: term ? { [labelField]: term } : {},
2253
+ onCreated: async (created) => {
2254
+ if (created && typeof created === 'object') {
2255
+ settle(created);
2256
+ return
2257
+ }
2258
+ if (typeof created !== 'string' && typeof created !== 'number') {
2259
+ return
2260
+ }
2261
+ reported = true;
2262
+ let obj;
2263
+ try {
2264
+ obj = await store.fetchObject(slug, String(created));
2265
+ } catch {
2266
+ obj = null;
2267
+ }
2268
+ settle(obj || { id: String(created), [labelField]: term });
2269
+ },
2270
+ onClose: () => {
2271
+ // A dialog often emits `close` right after `created`,
2272
+ // while the id is still being fetched: that fetch settles.
2273
+ if (!reported) {
2274
+ settle(null);
2275
+ }
2276
+ },
2277
+ });
2156
2278
  })
2157
2279
  },
2158
2280
 
@@ -2171,10 +2293,28 @@ var script = {
2171
2293
  return
2172
2294
  }
2173
2295
  const store = this.getObjectStore();
2174
- const created = store ? await store.saveObject(pending.slug, formData) : null;
2296
+ let created = null;
2297
+ let thrown = null;
2298
+ if (pending.override) {
2299
+ // The app's create override owns persistence (same contract as
2300
+ // CnIndexPage's `createOverride`).
2301
+ try {
2302
+ created = await pending.override(formData, {
2303
+ register: pending.register,
2304
+ schema: pending.schemaRef,
2305
+ objectType: pending.slug,
2306
+ effectiveSchema: pending.schema,
2307
+ });
2308
+ } catch (err) {
2309
+ thrown = (err && err.response && err.response.data && err.response.data.error) || (err && err.message) || null;
2310
+ }
2311
+ } else {
2312
+ created = store ? await store.saveObject(pending.slug, formData) : null;
2313
+ }
2175
2314
  if (!created) {
2176
2315
  const error = store && store.errors ? store.errors[pending.slug] : null;
2177
- const message = (error && error.message)
2316
+ const message = thrown
2317
+ || (!pending.override && error && error.message)
2178
2318
  || translate('nextcloud-vue', 'The item could not be created.');
2179
2319
  const dialog = this.$refs.nestedCreateDialog;
2180
2320
  if (dialog && typeof dialog.setValidationErrors === 'function') {