@conduction/nextcloud-vue 2.61.0 → 2.63.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 (186) hide show
  1. package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue.js +10 -0
  2. package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue.js.map +1 -0
  3. package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue2.js +189 -0
  4. package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue2.js.map +1 -0
  5. package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue3.js +72 -0
  6. package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue3.js.map +1 -0
  7. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js +394 -27
  8. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js.map +1 -1
  9. package/dist/esm/components/CnAppNav/CnAppNav.vue3.js.map +1 -1
  10. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +99 -9
  11. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
  12. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js +39 -18
  13. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
  14. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js +79 -2
  15. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js.map +1 -1
  16. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js +38 -25
  17. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js.map +1 -1
  18. package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue2.js +21 -1
  19. package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue2.js.map +1 -1
  20. package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue3.js +4 -1
  21. package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue3.js.map +1 -1
  22. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js.map +1 -1
  23. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js +53 -37
  24. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js.map +1 -1
  25. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +30 -6
  26. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  27. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +2 -2
  28. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  29. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue.js +10 -0
  30. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue.js.map +1 -0
  31. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue2.js +293 -0
  32. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue2.js.map +1 -0
  33. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue3.js +189 -0
  34. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue3.js.map +1 -0
  35. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +256 -23
  36. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  37. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +119 -51
  38. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  39. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  40. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +2 -1
  41. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
  42. package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue2.js +3 -1
  43. package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue2.js.map +1 -1
  44. package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue3.js +1 -1
  45. package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue3.js.map +1 -1
  46. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +234 -9
  47. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
  48. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +96 -57
  49. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
  50. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +51 -2
  51. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  52. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +5 -1
  53. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  54. package/dist/esm/components/CnIndexPage/selfModeActions.js +3 -3
  55. package/dist/esm/components/CnIndexPage/selfModeActions.js.map +1 -1
  56. package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue2.js.map +1 -1
  57. package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue3.js.map +1 -1
  58. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +36 -1
  59. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
  60. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js +4 -1
  61. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
  62. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +53 -0
  63. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
  64. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +22 -5
  65. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
  66. package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js.map +1 -1
  67. package/dist/esm/components/CnReportsPage/CnReportsPage.vue3.js.map +1 -1
  68. package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue2.js +121 -12
  69. package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue2.js.map +1 -1
  70. package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue3.js +3 -1
  71. package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue3.js.map +1 -1
  72. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js +326 -1
  73. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js.map +1 -1
  74. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js +112 -26
  75. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js.map +1 -1
  76. package/dist/esm/components/CnStorePage/CnStorePage.vue2.js.map +1 -1
  77. package/dist/esm/components/CnStorePage/CnStorePage.vue3.js.map +1 -1
  78. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue.js +10 -0
  79. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue.js.map +1 -0
  80. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue2.js +337 -0
  81. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue2.js.map +1 -0
  82. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue3.js +102 -0
  83. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue3.js.map +1 -0
  84. package/dist/esm/components/CnTimelineWidget/dashboardRegistration.js +34 -0
  85. package/dist/esm/components/CnTimelineWidget/dashboardRegistration.js.map +1 -0
  86. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js +131 -8
  87. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js.map +1 -1
  88. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue3.js +40 -38
  89. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue3.js.map +1 -1
  90. package/dist/esm/components/CnWidgetGrid/builtInWidgets.js +8 -6
  91. package/dist/esm/components/CnWidgetGrid/builtInWidgets.js.map +1 -1
  92. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
  93. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
  94. package/dist/esm/components/CnWikiPage/CnWikiPage.vue2.js.map +1 -1
  95. package/dist/esm/components/CnWikiPage/CnWikiPage.vue3.js.map +1 -1
  96. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue2.js +19 -0
  97. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue2.js.map +1 -1
  98. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue3.js +1 -1
  99. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue3.js.map +1 -1
  100. package/dist/esm/composables/useDependencyCheck.js +65 -0
  101. package/dist/esm/composables/useDependencyCheck.js.map +1 -0
  102. package/dist/esm/composables/useWalkthrough.js +218 -6
  103. package/dist/esm/composables/useWalkthrough.js.map +1 -1
  104. package/dist/esm/index.js +2 -0
  105. package/dist/esm/index.js.map +1 -1
  106. package/dist/esm/l10n/en.json.js +48 -0
  107. package/dist/esm/l10n/en.json.js.map +1 -1
  108. package/dist/esm/l10n/nl.json.js +48 -0
  109. package/dist/esm/l10n/nl.json.js.map +1 -1
  110. package/dist/esm/nextcloud-vue.css +557 -9
  111. package/dist/esm/store/useObjectStore.js +8 -0
  112. package/dist/esm/store/useObjectStore.js.map +1 -1
  113. package/dist/esm/utils/columnFilters.js +311 -0
  114. package/dist/esm/utils/columnFilters.js.map +1 -0
  115. package/dist/esm/utils/groupAutocomplete.js +123 -0
  116. package/dist/esm/utils/groupAutocomplete.js.map +1 -0
  117. package/dist/esm/utils/libraryWidgetKeys.js +2 -0
  118. package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
  119. package/dist/esm/utils/pickerOptions.js +198 -0
  120. package/dist/esm/utils/pickerOptions.js.map +1 -0
  121. package/dist/esm/utils/schema.js +83 -7
  122. package/dist/esm/utils/schema.js.map +1 -1
  123. package/dist/esm/utils/timelineEvents.js +174 -0
  124. package/dist/esm/utils/timelineEvents.js.map +1 -0
  125. package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
  126. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  127. package/dist/esm/utils/walkthroughSignals.js +88 -0
  128. package/dist/esm/utils/walkthroughSignals.js.map +1 -0
  129. package/dist/nextcloud-vue.cjs.js +9779 -5236
  130. package/dist/nextcloud-vue.cjs.js.map +2 -2
  131. package/dist/nextcloud-vue.css +557 -9
  132. package/l10n/en.json +48 -0
  133. package/l10n/nl.json +48 -0
  134. package/package.json +3 -2
  135. package/scripts/manifest-schema-hash.js +41 -0
  136. package/scripts/record-manifest-schema-hash.js +52 -0
  137. package/src/components/CnAdminActionCard/CnAdminActionCard.vue +234 -0
  138. package/src/components/CnAdminActionCard/index.js +4 -0
  139. package/src/components/CnAppNav/CnAppNav.vue +394 -27
  140. package/src/components/CnAppRoot/CnAppRoot.vue +129 -15
  141. package/src/components/CnBannerWidget/CnBannerWidget.vue +113 -2
  142. package/src/components/CnBrandStripe/CnBrandStripe.md +16 -0
  143. package/src/components/CnBrandStripe/CnBrandStripe.vue +58 -5
  144. package/src/components/CnChoiceCards/CnChoiceCards.vue +56 -31
  145. package/src/components/CnDashboardPage/CnDashboardPage.vue +32 -8
  146. package/src/components/CnDataTable/CnColumnFilterPopover.vue +524 -0
  147. package/src/components/CnDataTable/CnDataTable.md +4 -1
  148. package/src/components/CnDataTable/CnDataTable.vue +302 -21
  149. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +2 -2
  150. package/src/components/CnFieldHelper/CnFieldHelper.vue +4 -2
  151. package/src/components/CnFormDialog/CnFormDialog.vue +290 -31
  152. package/src/components/CnIndexPage/CnIndexPage.vue +55 -2
  153. package/src/components/CnIndexPage/selfModeActions.js +3 -3
  154. package/src/components/CnLinkCardsPage/CnLinkCardsPage.vue +35 -2
  155. package/src/components/CnObjectListWidget/CnObjectListWidget.vue +39 -1
  156. package/src/components/CnObjectSidebar/CnFilesTab.vue +65 -0
  157. package/src/components/CnReportsPage/CnReportsPage.vue +7 -0
  158. package/src/components/CnResourceSelect/CnResourceSelect.vue +123 -12
  159. package/src/components/CnSetupWizard/CnSetupWizard.vue +392 -3
  160. package/src/components/CnStorePage/CnStorePage.vue +7 -0
  161. package/src/components/CnTimelineWidget/CnTimelineWidget.vue +477 -0
  162. package/src/components/CnTimelineWidget/dashboardRegistration.js +32 -0
  163. package/src/components/CnTimelineWidget/index.js +4 -0
  164. package/src/components/CnWalkthrough/CnWalkthrough.vue +143 -10
  165. package/src/components/CnWidgetGrid/builtInWidgets.js +2 -0
  166. package/src/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
  167. package/src/components/CnWikiPage/CnWikiPage.vue +7 -0
  168. package/src/components/CnWizardDialog/CnWizardDialog.vue +20 -1
  169. package/src/components/index.js +2 -0
  170. package/src/composables/useDependencyCheck.js +61 -0
  171. package/src/composables/useWalkthrough.js +217 -5
  172. package/src/css/dashboard.css +49 -0
  173. package/src/css/detail-page.css +19 -0
  174. package/src/css/table.css +82 -2
  175. package/src/index.js +2 -0
  176. package/src/public/index.js +5 -0
  177. package/src/schemas/app-manifest-v2.schema.json +23 -1
  178. package/src/store/useObjectStore.js +8 -0
  179. package/src/utils/columnFilters.js +324 -0
  180. package/src/utils/groupAutocomplete.js +119 -0
  181. package/src/utils/libraryWidgetKeys.js +2 -0
  182. package/src/utils/pickerOptions.js +194 -0
  183. package/src/utils/schema.js +83 -7
  184. package/src/utils/timelineEvents.js +171 -0
  185. package/src/utils/validateManifestV2.compiled.js +2 -2
  186. package/src/utils/walkthroughSignals.js +94 -0
@@ -297,10 +297,13 @@ function truncateString(str, maxLength) {
297
297
  * surface (CnFormDialog) resolves the reference to a searchable dropdown
298
298
  * of the referenced objects (label = human name, value = UUID).
299
299
  * 4. Nextcloud user reference: `referenceType: 'nextcloud-user'` (or
300
- * `format: 'user'`/`'username'`) → `'user'`; an array of such
300
+ * `format: 'user'`/`'username'`/`'nc-user'`) → `'user'`; an array of such
301
301
  * properties → `'user-multiselect'`. CnFormDialog resolves these to a
302
302
  * searchable dropdown of real Nextcloud users (label = display name,
303
- * value = UID).
303
+ * value = UID). A Nextcloud group (`referenceType: 'nextcloud-group'` or
304
+ * `format: 'nc-group'`) → `'group'`, an array of groups →
305
+ * `'group-multiselect'`. `format: 'language'` → `'language'` and
306
+ * `format: 'timezone'` → `'timezone'` (both searchable code lists).
304
307
  * 5. Type-based: `boolean` → `'checkbox'`, `integer`/`number` → `'number'`,
305
308
  * `array` + `items.enum` → `'multiselect'`, `array` → `'tags'`
306
309
  * 6. Format-based: `date-time` → `'datetime'`, `date` → `'date'`,
@@ -368,9 +371,32 @@ function isUserProp(prop) {
368
371
  return true
369
372
  }
370
373
  const format = prop.format || ''
371
- return format === 'user' || format === 'username'
374
+ return format === 'user' || format === 'username' || format === 'nc-user'
372
375
  }
373
376
 
377
+ /**
378
+ * Whether a (single-value) schema property represents a Nextcloud group.
379
+ *
380
+ * A property is a group field when it declares `referenceType: 'nextcloud-group'`
381
+ * or `format: 'nc-group'`. CnFormDialog renders it as a searchable dropdown of
382
+ * Nextcloud groups (label = display name, value = group id).
383
+ *
384
+ * @spec openspec/changes/form-pickers-from-schema/specs/schema-utilities/spec.md
385
+ * @param {object} prop A schema property definition (or `items` for an array).
386
+ * @return {boolean} True when the property marks a Nextcloud group.
387
+ */
388
+ function isGroupProp(prop) {
389
+ if (!prop || typeof prop !== 'object') {
390
+ return false
391
+ }
392
+ return prop.referenceType === 'nextcloud-group' || prop.format === 'nc-group'
393
+ }
394
+
395
+ /**
396
+ * @spec openspec/changes/form-pickers-from-schema/specs/schema-utilities/spec.md
397
+ * @param {object} prop The schema property definition.
398
+ * @return {string} The widget identifier (see the block above).
399
+ */
374
400
  function resolveWidget(prop) {
375
401
  // Explicit widget hint takes priority
376
402
  if (prop.widget) {
@@ -409,6 +435,24 @@ function resolveWidget(prop) {
409
435
  return 'user-multiselect'
410
436
  }
411
437
 
438
+ // Nextcloud group (`format: 'nc-group'` / `referenceType: 'nextcloud-group'`):
439
+ // a searchable dropdown of groups storing the group id.
440
+ if (isGroupProp(prop)) {
441
+ return 'group'
442
+ }
443
+ if (type === 'array' && isGroupProp(prop.items)) {
444
+ return 'group-multiselect'
445
+ }
446
+
447
+ // Language and time zone codes: a searchable list instead of a free-text
448
+ // box, so a person picks "Dutch" and the record stores `nl`.
449
+ if (type === 'string' && format === 'language') {
450
+ return 'language'
451
+ }
452
+ if (type === 'string' && format === 'timezone') {
453
+ return 'timezone'
454
+ }
455
+
412
456
  // Boolean → switch/checkbox
413
457
  if (type === 'boolean') {
414
458
  return 'checkbox'
@@ -587,7 +631,8 @@ export function isTenantProperty(key, prop) {
587
631
  * @param {boolean} [options.includeReadOnly] Whether to include readOnly properties
588
632
  * @param {boolean} [options.hideTenant] Drop properties that hold the record's tenant (see `isTenantProperty`). Off by default, so a detail page still shows the tenant; CnFormDialog turns it on because nobody should be asked for it. `overrides[key].hidden === false` keeps one visible.
589
633
  * @param {(text: string) => string} [options.translate] Optional display-layer translation function applied to each field's `label` and `description`. Schema property titles/descriptions are authored in English as the canonical source; consumers pass their bound `t()` (via the injected `cnTranslate`) so the rendered field label follows the user's language. When omitted, label/description are the English source strings unchanged (pure, backward-compatible).
590
- * @return {Array<{key: string, label: string, description: string, descriptionLong: string, type: string, format: string|null, widget: string, required: boolean, readOnly: boolean, default: unknown, enum: Array|null, enumLabels: object|null, items: object|null, referenceType: string|null, referenceSemanticType: string|null, referenceSemanticApp: string|null, reference: {schema: string|number, multiple: boolean}|null, userPicker: {multiple: boolean}|null, fillFrom: object|null, validation: object, order: number}>} `description` is the inline helper text (see `splitDescription`); `descriptionLong` carries the full text when it was too long to render inline, else ''. `enumLabels` maps each raw enum value to its English display label (from the property's `x-enum-labels`), or null.
634
+ * @spec openspec/changes/form-pickers-from-schema/specs/schema-utilities/spec.md
635
+ * @return {Array<{key: string, label: string, description: string, descriptionLong: string, type: string, format: string|null, widget: string, required: boolean, readOnly: boolean, default: unknown, enum: Array|null, enumLabels: object|null, items: object|null, referenceType: string|null, referenceSemanticType: string|null, referenceSemanticApp: string|null, reference: {schema: string|number, multiple: boolean, register?: string, labelField?: string}|null, allowCreate: boolean, userPicker: {multiple: boolean}|null, groupPicker: {multiple: boolean}|null, defaultToken: string|null, fillFrom: object|null, validation: object, order: number}>} `description` is the inline helper text (see `splitDescription`); `descriptionLong` carries the property's `x-help` text when it declares one, else the full description when it was too long to render inline, else ''. `enumLabels` maps each raw enum value to its English display label (from the property's `x-enum-labels`), or null.
591
636
  */
592
637
  export function fieldsFromSchema(schema, options = {}) {
593
638
  const { exclude = [], include = null, overrides = {}, includeReadOnly = false, hideTenant = false, translate } = options
@@ -663,11 +708,19 @@ export function fieldsFromSchema(schema, options = {}) {
663
708
 
664
709
  return entries.map(([key, prop]) => {
665
710
  const description = splitDescription(prop.description ? tr(prop.description) : '')
711
+ // `x-help` is the long explanation behind the (i) popover. It always
712
+ // shows the popover, while `description` stays the short helper line.
713
+ const help = typeof prop['x-help'] === 'string' && prop['x-help'].trim() !== ''
714
+ ? tr(prop['x-help'].trim())
715
+ : ''
716
+ const labelField = prop['x-label-field']
717
+ || (prop.items && prop.items['x-label-field'])
718
+ || null
666
719
  const field = {
667
720
  key,
668
721
  label: tr(prop.title || key),
669
722
  description: description.short,
670
- descriptionLong: description.long,
723
+ descriptionLong: help || description.long,
671
724
  type: prop.type || 'string',
672
725
  format: prop.format || null,
673
726
  widget: resolveWidget(prop),
@@ -685,6 +738,10 @@ export function fieldsFromSchema(schema, options = {}) {
685
738
  required: requiredKeys.includes(key),
686
739
  readOnly: prop.readOnly || false,
687
740
  default: prop.default !== undefined ? prop.default : null,
741
+ // A default that depends on who opens the form (`x-default:
742
+ // 'current-language'` / `'current-timezone'`). Resolved by the
743
+ // consuming form on a NEW object only; never on edit.
744
+ defaultToken: typeof prop['x-default'] === 'string' ? prop['x-default'] : null,
688
745
  enum: prop.enum || null,
689
746
  // Display labels for the enum CODES, keyed by raw value. An enum
690
747
  // value is a stored contract value, not display text — several are
@@ -732,11 +789,23 @@ export function fieldsFromSchema(schema, options = {}) {
732
789
  // form's own; absent, the reference stays same-register (the form's
733
790
  // `register` prop is used). Consolidates the ad-hoc caseReference /
734
791
  // approvalDecisionId / x-mirror-of variants onto one convention.
792
+ //
793
+ // `x-label-field` names the referenced object's property that labels
794
+ // an option and that an inline create writes the typed term to
795
+ // (`reference.labelField`, default `name` at the consumer).
735
796
  reference: (normalizeRef(prop.$ref) !== null)
736
- ? { schema: normalizeRef(prop.$ref), multiple: false, ...(prop['x-external-register'] ? { register: prop['x-external-register'] } : {}) }
797
+ ? { schema: normalizeRef(prop.$ref), multiple: false, ...(prop['x-external-register'] ? { register: prop['x-external-register'] } : {}), ...(labelField ? { labelField } : {}) }
737
798
  : (prop.type === 'array' && prop.items && normalizeRef(prop.items.$ref) !== null)
738
- ? { schema: normalizeRef(prop.items.$ref), multiple: true, ...((prop.items['x-external-register'] || prop['x-external-register']) ? { register: prop.items['x-external-register'] || prop['x-external-register'] } : {}) }
799
+ ? { schema: normalizeRef(prop.items.$ref), multiple: true, ...((prop.items['x-external-register'] || prop['x-external-register']) ? { register: prop.items['x-external-register'] || prop['x-external-register'] } : {}), ...(labelField ? { labelField } : {}) }
739
800
  : null,
801
+ // Select OR create (`x-allow-create: true`, on the property or on
802
+ // its `items` for an array): the picker offers "Create" next to the
803
+ // existing objects. Only meaningful on a reference.
804
+ allowCreate: prop['x-allow-create'] === true
805
+ || (prop.type === 'array' && !!prop.items && prop.items['x-allow-create'] === true),
806
+ // Template copy (`x-fill-from: { formKey: sourceKey }`): choosing a
807
+ // referenced object copies those of its values into this form.
808
+ fillFrom: (prop['x-fill-from'] && typeof prop['x-fill-from'] === 'object') ? prop['x-fill-from'] : null,
740
809
  // Nextcloud user reference: when a property marks a NC user
741
810
  // (`referenceType: 'nextcloud-user'`, or `format: 'user'`/
742
811
  // `'username'`), tag it so CnFormDialog renders a searchable
@@ -749,6 +818,13 @@ export function fieldsFromSchema(schema, options = {}) {
749
818
  : (prop.type === 'array' && isUserProp(prop.items))
750
819
  ? { multiple: true }
751
820
  : null,
821
+ // Nextcloud group reference (`format: 'nc-group'` or
822
+ // `referenceType: 'nextcloud-group'`): the form stores the group id.
823
+ groupPicker: isGroupProp(prop)
824
+ ? { multiple: false }
825
+ : (prop.type === 'array' && isGroupProp(prop.items))
826
+ ? { multiple: true }
827
+ : null,
752
828
  // Conditional immutability (AD: x-openregister-readonly-when): a
753
829
  // property can declare it becomes read-only when another field on the
754
830
  // same object holds a given value — e.g. a hybrid app's identity
@@ -0,0 +1,171 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ * SPDX-License-Identifier: EUPL-1.2
4
+ */
5
+
6
+ /**
7
+ * Turn an object's dated facts into timeline events.
8
+ *
9
+ * Pure helpers behind `CnTimelineWidget`. An event is
10
+ * `{ id, at, dateOnly, label, detail, source, upcoming }`, where `at` is a
11
+ * `Date`, `dateOnly` says the source value carried no time of day, and
12
+ * `upcoming` says the moment lies after `now`.
13
+ *
14
+ * @module utils/timelineEvents
15
+ * @spec openspec/changes/timeline-widget/specs/timeline-widget/spec.md
16
+ */
17
+
18
+ /**
19
+ * Read a dotted path off an object (`@self.created`, `payment.clearedAt`).
20
+ *
21
+ * @param {object} obj The object.
22
+ * @param {string} path The dotted path.
23
+ * @return {unknown} The value, or undefined.
24
+ */
25
+ export function readPath(obj, path) {
26
+ if (!obj || typeof path !== 'string' || path === '') {
27
+ return undefined
28
+ }
29
+ if (Object.hasOwn(obj, path)) {
30
+ return obj[path]
31
+ }
32
+ return path.split('.').reduce((acc, part) => ((acc === null || acc === undefined) ? undefined : acc[part]), obj)
33
+ }
34
+
35
+ /**
36
+ * Parse a stored date value. A bare `YYYY-MM-DD` is a calendar day and is
37
+ * read as local midnight, not UTC, so it never shows as the day before.
38
+ *
39
+ * @param {unknown} raw The stored value.
40
+ * @return {{at: Date, dateOnly: boolean}|null} The moment, or null when it is not a date.
41
+ */
42
+ export function parseMoment(raw) {
43
+ if (raw === undefined || raw === null || raw === '') {
44
+ return null
45
+ }
46
+ if (typeof raw === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(raw)) {
47
+ const [y, m, d] = raw.split('-').map(Number)
48
+ return { at: new Date(y, m - 1, d), dateOnly: true }
49
+ }
50
+ const at = new Date(raw)
51
+ return Number.isNaN(at.getTime()) ? null : { at, dateOnly: false }
52
+ }
53
+
54
+ /**
55
+ * Build one event, or null when the value is not a date.
56
+ *
57
+ * @param {object} spec `{ id, raw, label, detail, source }`.
58
+ * @param {Date} now The reference moment for `upcoming`.
59
+ * @return {object|null} The event.
60
+ */
61
+ function makeEvent(spec, now) {
62
+ const moment = parseMoment(spec.raw)
63
+ if (!moment) {
64
+ return null
65
+ }
66
+ return {
67
+ id: spec.id,
68
+ at: moment.at,
69
+ dateOnly: moment.dateOnly,
70
+ label: spec.label || '',
71
+ detail: spec.detail || '',
72
+ source: spec.source,
73
+ upcoming: moment.at.getTime() > now.getTime(),
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Events from the object's own date properties.
79
+ *
80
+ * @param {object} object The object.
81
+ * @param {Array<{field: string, label?: string}>} fields The configured date fields.
82
+ * @param {Date} [now] The reference moment.
83
+ * @return {Array<object>} One event per field that holds a date.
84
+ */
85
+ export function fieldEvents(object, fields, now = new Date()) {
86
+ return (Array.isArray(fields) ? fields : [])
87
+ .filter((f) => f && typeof f.field === 'string')
88
+ .map((f) => makeEvent({ id: `field:${f.field}`, raw: readPath(object, f.field), label: f.label || f.field, source: 'field' }, now))
89
+ .filter(Boolean)
90
+ }
91
+
92
+ /**
93
+ * Events from related objects: one per row that holds a date in `dateField`.
94
+ *
95
+ * @param {Array<object>} rows The related objects.
96
+ * @param {{label?: string, dateField?: string, titleField?: string, schema?: string}} config The related-source config.
97
+ * @param {Date} [now] The reference moment.
98
+ * @return {Array<object>} The events.
99
+ */
100
+ export function relatedEvents(rows, config, now = new Date()) {
101
+ const cfg = config || {}
102
+ const dateField = cfg.dateField || '@self.created'
103
+ return (Array.isArray(rows) ? rows : [])
104
+ .map((row, i) => {
105
+ const self = (row && row['@self']) || {}
106
+ const id = String(self.id || row.id || i)
107
+ const title = cfg.titleField ? readPath(row, cfg.titleField) : (row && (row.title || row.name)) || self.name
108
+ return makeEvent({
109
+ id: `related:${cfg.schema || ''}:${id}`,
110
+ raw: readPath(row, dateField),
111
+ label: cfg.label || '',
112
+ detail: title === undefined || title === null ? '' : String(title),
113
+ source: 'related',
114
+ }, now)
115
+ })
116
+ .filter(Boolean)
117
+ }
118
+
119
+ /**
120
+ * Events from audit-trail entries.
121
+ *
122
+ * @param {Array<object>} entries The audit-trail rows.
123
+ * @param {(action: string, actor: string) => string} describe Builds the label from action and actor.
124
+ * @param {Date} [now] The reference moment.
125
+ * @return {Array<object>} The events.
126
+ */
127
+ export function auditEvents(entries, describe, now = new Date()) {
128
+ return (Array.isArray(entries) ? entries : [])
129
+ .map((e, i) => makeEvent({
130
+ id: `audit:${e && (e.id || e.uuid) ? (e.id || e.uuid) : i}`,
131
+ raw: e && (e.created || e.creationDateTime || e.timestamp),
132
+ label: describe(String((e && (e.action || e.event)) || ''), String((e && (e.actorDisplayName || e.userName || e.user || e.actor)) || '')),
133
+ source: 'audit',
134
+ }, now))
135
+ .filter(Boolean)
136
+ }
137
+
138
+ /**
139
+ * Events from OpenRegister's timeline (notes, calls, messages).
140
+ *
141
+ * @param {Array<object>} entries The timeline rows.
142
+ * @param {string} noteLabel The label for an entry without a kind.
143
+ * @param {Date} [now] The reference moment.
144
+ * @return {Array<object>} The events.
145
+ */
146
+ export function timelineEntryEvents(entries, noteLabel, now = new Date()) {
147
+ return (Array.isArray(entries) ? entries : [])
148
+ .map((e, i) => makeEvent({
149
+ id: `timeline:${e && e.id ? e.id : i}`,
150
+ raw: e && e.created,
151
+ label: (e && (e.kindTitle || e.kind)) || noteLabel,
152
+ detail: [e && e.author, e && e.message].filter(Boolean).join(': '),
153
+ source: 'timeline',
154
+ }, now))
155
+ .filter(Boolean)
156
+ }
157
+
158
+ /**
159
+ * Sort events by time; ties keep their input order.
160
+ *
161
+ * @param {Array<object>} events The events.
162
+ * @param {'asc'|'desc'} [order] Oldest first (default) or newest first.
163
+ * @return {Array<object>} A new, sorted array.
164
+ */
165
+ export function sortEvents(events, order = 'asc') {
166
+ const dir = order === 'desc' ? -1 : 1
167
+ return (events || [])
168
+ .map((e, i) => ({ e, i }))
169
+ .sort((a, b) => ((a.e.at.getTime() - b.e.at.getTime()) * dir) || (a.i - b.i))
170
+ .map(({ e }) => e)
171
+ }