@conduction/nextcloud-vue 2.102.0 → 2.104.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 (147) hide show
  1. package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js +23 -0
  2. package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js.map +1 -1
  3. package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js +94 -72
  4. package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js.map +1 -1
  5. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +18 -1
  6. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
  7. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js +3 -3
  8. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
  9. package/dist/esm/components/CnConversationThread/CnConversationThread.vue.js +1 -1
  10. package/dist/esm/components/CnConversationThread/CnConversationThread.vue2.js +73 -0
  11. package/dist/esm/components/CnConversationThread/CnConversationThread.vue2.js.map +1 -1
  12. package/dist/esm/components/CnConversationThread/CnConversationThread.vue3.js +99 -64
  13. package/dist/esm/components/CnConversationThread/CnConversationThread.vue3.js.map +1 -1
  14. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue.js +1 -1
  15. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js +46 -0
  16. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js.map +1 -1
  17. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue3.js +37 -2
  18. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue3.js.map +1 -1
  19. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
  20. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +29 -6
  21. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  22. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  23. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +39 -2
  24. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  25. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +14 -0
  26. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  27. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +40 -2
  28. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  29. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  30. package/dist/esm/components/CnFormDialog/CnFormDialog.vue.js +1 -1
  31. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +113 -20
  32. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
  33. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +387 -359
  34. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
  35. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +173 -3
  36. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  37. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +107 -87
  38. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  39. package/dist/esm/components/CnIndexPage/useSelfFetchList.js +7 -2
  40. package/dist/esm/components/CnIndexPage/useSelfFetchList.js.map +1 -1
  41. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue.js +1 -1
  42. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +10 -1
  43. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
  44. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
  45. package/dist/esm/components/CnObjectRow/CnObjectRow.vue.js +1 -1
  46. package/dist/esm/components/CnObjectRow/CnObjectRow.vue2.js +57 -5
  47. package/dist/esm/components/CnObjectRow/CnObjectRow.vue2.js.map +1 -1
  48. package/dist/esm/components/CnObjectRow/CnObjectRow.vue3.js +37 -4
  49. package/dist/esm/components/CnObjectRow/CnObjectRow.vue3.js.map +1 -1
  50. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
  51. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +97 -3
  52. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  53. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +46 -2
  54. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  55. package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue.js +9 -0
  56. package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue.js.map +1 -0
  57. package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue2.js +165 -0
  58. package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue2.js.map +1 -0
  59. package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue3.js +85 -0
  60. package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue3.js.map +1 -0
  61. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue.js +1 -1
  62. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js +54 -6
  63. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js.map +1 -1
  64. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js +3 -1
  65. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js.map +1 -1
  66. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
  67. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +8 -1
  68. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  69. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  70. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue.js +1 -1
  71. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue2.js.map +1 -1
  72. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue3.js.map +1 -1
  73. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue.js +1 -1
  74. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js +18 -2
  75. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js.map +1 -1
  76. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue3.js.map +1 -1
  77. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue.js +1 -1
  78. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue2.js +19 -0
  79. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue2.js.map +1 -1
  80. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue3.js +1 -1
  81. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue3.js.map +1 -1
  82. package/dist/esm/composables/indexSources.js +7 -7
  83. package/dist/esm/composables/indexSources.js.map +1 -1
  84. package/dist/esm/index.js +1 -0
  85. package/dist/esm/index.js.map +1 -1
  86. package/dist/esm/l10n/en.json.js +4 -0
  87. package/dist/esm/l10n/en.json.js.map +1 -1
  88. package/dist/esm/l10n/nl.json.js +4 -0
  89. package/dist/esm/l10n/nl.json.js.map +1 -1
  90. package/dist/esm/library-style.js +1 -1
  91. package/dist/esm/mixins/pageViews.js +8 -1
  92. package/dist/esm/mixins/pageViews.js.map +1 -1
  93. package/dist/esm/nextcloud-vue.css +586 -416
  94. package/dist/esm/utils/nextcloudDirectory.js +18 -1
  95. package/dist/esm/utils/nextcloudDirectory.js.map +1 -1
  96. package/dist/esm/utils/sortOrder.js +61 -0
  97. package/dist/esm/utils/sortOrder.js.map +1 -0
  98. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  99. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  100. package/dist/esm/utils/visibleWhen.js +13 -2
  101. package/dist/esm/utils/visibleWhen.js.map +1 -1
  102. package/dist/nextcloud-vue.cjs.js +12270 -10936
  103. package/dist/nextcloud-vue.cjs.js.map +2 -2
  104. package/dist/nextcloud-vue.css +586 -416
  105. package/l10n/en.json +4 -0
  106. package/l10n/nl.json +4 -0
  107. package/package.json +1 -1
  108. package/src/components/CnActionsBar/CnActionsBar.md +1 -0
  109. package/src/components/CnActionsBar/CnActionsBar.vue +39 -2
  110. package/src/components/CnAppRoot/CnAppRoot.md +1 -1
  111. package/src/components/CnAppRoot/CnAppRoot.vue +21 -4
  112. package/src/components/CnConversationThread/CnConversationThread.md +17 -0
  113. package/src/components/CnConversationThread/CnConversationThread.vue +113 -8
  114. package/src/components/CnConversationThread/CnConversationWidget.vue +60 -1
  115. package/src/components/CnDashboardPage/CnDashboardPage.md +4 -1
  116. package/src/components/CnDashboardPage/CnDashboardPage.vue +29 -6
  117. package/src/components/CnDataTable/CnDataTable.md +1 -0
  118. package/src/components/CnDataTable/CnDataTable.vue +49 -0
  119. package/src/components/CnDetailPage/CnDetailPage.md +2 -1
  120. package/src/components/CnDetailPage/CnDetailPage.vue +40 -2
  121. package/src/components/CnFormDialog/CnFormDialog.md +1 -1
  122. package/src/components/CnFormDialog/CnFormDialog.vue +127 -0
  123. package/src/components/CnIndexPage/CnIndexPage.md +14 -2
  124. package/src/components/CnIndexPage/CnIndexPage.vue +192 -11
  125. package/src/components/CnIndexPage/useSelfFetchList.js +6 -1
  126. package/src/components/CnObjectListWidget/CnObjectListWidget.vue +10 -1
  127. package/src/components/CnObjectRow/CnObjectRow.vue +95 -1
  128. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +134 -2
  129. package/src/components/CnSearchSelect/CnSearchSelect.vue +283 -0
  130. package/src/components/CnSearchSelect/index.js +12 -0
  131. package/src/components/CnSetupWizard/CnSetupWizard.vue +56 -6
  132. package/src/components/CnTabsWidget/CnTabsWidget.vue +8 -1
  133. package/src/components/CnUserSettingsDialog/CnUserSettingsDialog.vue +7 -2
  134. package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +18 -2
  135. package/src/components/CnWizardDialog/CnWizardDialog.vue +20 -1
  136. package/src/components/index.js +1 -0
  137. package/src/composables/indexSources.js +7 -7
  138. package/src/css/dialog.css +5 -3
  139. package/src/css/index-page.css +19 -0
  140. package/src/css/look-board-index.css +48 -0
  141. package/src/index.js +1 -0
  142. package/src/mixins/pageViews.js +8 -1
  143. package/src/schemas/app-manifest-v2.schema.json +75 -6
  144. package/src/utils/nextcloudDirectory.js +17 -0
  145. package/src/utils/sortOrder.js +58 -0
  146. package/src/utils/validateManifestV2.compiled.js +3 -3
  147. package/src/utils/visibleWhen.js +13 -2
@@ -389,6 +389,18 @@
389
389
  </button>
390
390
  </slot>
391
391
  </div>
392
+ <!-- `pageSize`: the board's pager under the rows, "4 of 7" and the
393
+ page buttons (DqCatalogus). -->
394
+ <CnPagination
395
+ v-if="pageSize > 0 && totalRowCount > pageSize"
396
+ variant="board"
397
+ class="cn-data-table__pager"
398
+ :currentPage="currentPage"
399
+ :totalPages="pageCount"
400
+ :totalItems="totalRowCount"
401
+ :currentPageSize="pageSize"
402
+ data-testid="cn-data-table-pager"
403
+ @pageChanged="currentPage = $event" />
392
404
  </div>
393
405
  </template>
394
406
 
@@ -400,6 +412,7 @@ import { NcCheckboxRadioSwitch, NcLoadingIcon } from '@nextcloud/vue'
400
412
  import ChevronDown from 'vue-material-design-icons/ChevronDown.vue'
401
413
  import ChevronUp from 'vue-material-design-icons/ChevronUp.vue'
402
414
  import FilterOutline from 'vue-material-design-icons/FilterOutline.vue'
415
+ import CnPagination from '../CnPagination/CnPagination.vue'
403
416
  import CnColumnFilterPopover from './CnColumnFilterPopover.vue'
404
417
  import { useClickDragGuard } from '../../composables/useClickDragGuard.js'
405
418
  import { normalizeLook } from '../../composables/useLook.js'
@@ -491,6 +504,7 @@ export default {
491
504
  CnColumnFilterPopover,
492
505
  CnIcon,
493
506
  CnLockIndicator,
507
+ CnPagination,
494
508
  CnUnreadMarker,
495
509
  ChevronDown,
496
510
  ChevronUp,
@@ -858,6 +872,19 @@ export default {
858
872
  default: 0,
859
873
  },
860
874
 
875
+ /**
876
+ * Rows per page. Above 0 the table shows one page of its rows and the
877
+ * board's pager under them ("4 of 7", page buttons, Next) once there is
878
+ * more than one page. Pages the rows the table holds; 0 (the default)
879
+ * shows them all, or the first `limit`.
880
+ *
881
+ * @spec openspec/changes/object-table-pager/specs/object-table/spec.md#requirement-an-object-table-can-page-its-rows
882
+ */
883
+ pageSize: {
884
+ type: Number,
885
+ default: 0,
886
+ },
887
+
861
888
  /**
862
889
  * vue-router route object for the "View all" footer link. The footer only
863
890
  * shows when set AND the rows are a `limit`-ed subset. Folded in from CnTableWidget.
@@ -1010,6 +1037,8 @@ export default {
1010
1037
  aggregateRequestId: 0,
1011
1038
  /** Rows fetched in self-fetch mode (register + schemaId). */
1012
1039
  fetchedRows: [],
1040
+ /** The page shown under `pageSize`. */
1041
+ currentPage: 1,
1013
1042
  /** Personal-lens reports (`@self.lenses`) of the latest self-fetch response. */
1014
1043
  fetchedLenses: {},
1015
1044
  /** True while a self-fetch request is in flight. */
@@ -1125,9 +1154,23 @@ export default {
1125
1154
  * @return {Array<object>}
1126
1155
  */
1127
1156
  effectiveRows() {
1157
+ if (this.pageSize > 0) {
1158
+ const page = Math.min(this.currentPage, this.pageCount)
1159
+ return this.sourceRows.slice((page - 1) * this.pageSize, page * this.pageSize)
1160
+ }
1128
1161
  return this.limit > 0 ? this.sourceRows.slice(0, this.limit) : this.sourceRows
1129
1162
  },
1130
1163
 
1164
+ /**
1165
+ * The number of pages under `pageSize` (at least 1).
1166
+ *
1167
+ * @spec openspec/changes/object-table-pager/specs/object-table/spec.md#requirement-an-object-table-can-page-its-rows
1168
+ * @return {number}
1169
+ */
1170
+ pageCount() {
1171
+ return this.pageSize > 0 ? Math.max(1, Math.ceil(this.sourceRows.length / this.pageSize)) : 1
1172
+ },
1173
+
1131
1174
  /**
1132
1175
  * Total row count before the `limit` cap (drives the count badge + the
1133
1176
  * "View all" footer condition).
@@ -1339,6 +1382,12 @@ export default {
1339
1382
  },
1340
1383
  },
1341
1384
 
1385
+ pageCount(count) {
1386
+ if (this.currentPage > count) {
1387
+ this.currentPage = count
1388
+ }
1389
+ },
1390
+
1342
1391
  effectiveColumns: {
1343
1392
  handler() {
1344
1393
  this.loadAggregates()
@@ -215,7 +215,7 @@ export default {
215
215
 
216
216
  | Prop | Type | Default | Description |
217
217
  |------|------|---------|-------------|
218
- | `userLayout` | Boolean | `false` | Let the user arrange each of the page's `views` for themselves. The header shows **Arrange view** next to the view switch; **Done** stores the arrangement per view and user (a user preference, mirrored in the browser), **Reset view** returns the chosen view to its manifest layout. The manifest `layout` is never changed. See [the guide](../../../docs/components/cn-detail-page.md#each-view-arranged-per-user-userlayout). |
218
+ | `userLayout` | Boolean | `false` | **Deprecated** (layout belongs to Buildiq; removed after this release). Let the user arrange each of the page's `views` for themselves. The header shows **Arrange view** next to the view switch; **Done** stores the arrangement per view and user (a user preference, mirrored in the browser), **Reset view** returns the chosen view to its manifest layout. The manifest `layout` is never changed. See [the guide](../../../docs/components/cn-detail-page.md#each-view-arranged-per-user-userlayout). |
219
219
 
220
220
  ## Slots
221
221
 
@@ -426,6 +426,7 @@ Four levels of action and the case surfaces around them. All opt in. See [the re
426
426
  | `statusPill` | Object \| null | `null` | A second pill, same shape. |
427
427
  | `headerFields` | Array | `[]` | Fields shown as chips under the title (manifest `config.headerFields`). Each entry is a property key, or `{ key, format, labelField, colorField, warnWhenPast }`; `format` is `text` (default), `mono`, `badge`, `user` or `date`. A `$ref` value shows `labelField` of the referenced object and falls back to the raw id in mono; a badge maps `colorField` (`success`, `warning`, `error`, `info`, `neutral`) to a variant; `warnWhenPast` gives a past date the error variant. An empty value shows no chip, a page without the key shows no row. Each chip reads "Title: value" to a screen reader, and the row prints as plain text. |
428
428
  | `sideColumn` | Array | `[]` | Widget definitions or widget ids, rendered as a column of cards beside the body. |
429
+ | `sideColumnFrom` | String | `''` | A field path, read like a `visibleWhen` field (e.g. `@context.caseType.sideCards`), whose list of ids picks the `sideColumn` entries this record shows and their order. Any other value keeps `sideColumn` as declared. |
429
430
  | `isAdmin` | Boolean \| null | `null` | Shows `adminOnly` header actions. `null` reads it from Nextcloud. |
430
431
 
431
432
  ## The record as a place
@@ -1267,7 +1267,7 @@ import { isActivityWidget, resolveHeaderMeta } from '../../utils/headerMeta.js'
1267
1267
  import { patchStoredSelf } from '../../utils/patchStoredSelf.js'
1268
1268
  import { setReadState } from '../../utils/recordInteractions.js'
1269
1269
  import { slotRenders } from '../../utils/slotContent.js'
1270
- import { evaluateVisibleWhen, isLocallyDecidableVisibleWhen } from '../../utils/visibleWhen.js'
1270
+ import { evaluateVisibleWhen, isLocallyDecidableVisibleWhen, readVisibleWhenPath } from '../../utils/visibleWhen.js'
1271
1271
  import {
1272
1272
  isCardWidgetDef,
1273
1273
  isContentOnlyWidgetDef,
@@ -2582,6 +2582,22 @@ export default {
2582
2582
  default: () => [],
2583
2583
  },
2584
2584
 
2585
+ /**
2586
+ * A field path whose value picks the side column and its order for
2587
+ * this record (manifest `config.sideColumnFrom`), read like a
2588
+ * `visibleWhen` field: `@context.caseType.sideCards` reads the list a
2589
+ * case type keeps. A non-empty list of ids keeps those `sideColumn`
2590
+ * entries, in that order, and leaves the rest out; each still follows
2591
+ * its own `visibleWhen`. Any other value keeps `sideColumn` as
2592
+ * declared, so a record whose list is empty changes nothing.
2593
+ *
2594
+ * @spec openspec/changes/detail-side-column-from/specs/detail-page/spec.md#requirement-a-record-can-pick-the-side-column-and-its-order
2595
+ */
2596
+ sideColumnFrom: {
2597
+ type: String,
2598
+ default: '',
2599
+ },
2600
+
2585
2601
  /**
2586
2602
  * Draw the header as a card (manifest `config.headerCard: true`): the
2587
2603
  * pills, the title and the actions on the page's surface colour inside
@@ -3492,7 +3508,7 @@ export default {
3492
3508
  * @spec openspec/changes/detail-side-visible-when/specs/detail-page/spec.md#requirement-a-side-column-widget-shows-only-while-its-visiblewhen-holds
3493
3509
  */
3494
3510
  sideColumnWidgets() {
3495
- const resolved = this.declaredSideWidgets.filter((widget) => this.sideWidgetVisible(widget))
3511
+ const resolved = this.pickedSideWidgets.filter((widget) => this.sideWidgetVisible(widget))
3496
3512
  if (!this.isBoard) {
3497
3513
  return resolved
3498
3514
  }
@@ -3504,6 +3520,28 @@ export default {
3504
3520
  .map(({ widget }) => widget)
3505
3521
  },
3506
3522
 
3523
+ /**
3524
+ * The side column's definitions as the record picks them
3525
+ * (`sideColumnFrom`): the ids it lists, in its order, or every
3526
+ * declared entry when it lists none. Edit mode shows them all.
3527
+ *
3528
+ * @return {Array<object>}
3529
+ * @spec openspec/changes/detail-side-column-from/specs/detail-page/spec.md#requirement-a-record-can-pick-the-side-column-and-its-order
3530
+ */
3531
+ pickedSideWidgets() {
3532
+ const declared = this.declaredSideWidgets
3533
+ if (!this.sideColumnFrom || this.editingBody || !this.widgetVisibilityData) {
3534
+ return declared
3535
+ }
3536
+ const ids = readVisibleWhenPath(this.widgetVisibilityData, this.sideColumnFrom)
3537
+ if (!Array.isArray(ids) || ids.length === 0) {
3538
+ return declared
3539
+ }
3540
+ return ids
3541
+ .map((id) => declared.find((widget) => widget.id === id) || null)
3542
+ .filter(Boolean)
3543
+ },
3544
+
3507
3545
  /**
3508
3546
  * The side column's entries resolved to definitions, before any
3509
3547
  * `visibleWhen` is read: a string names a widget declared in
@@ -100,7 +100,7 @@ export default {
100
100
  | `lockedFields` | `[]` | Field keys rendered read-only (disabled) and immutable — typically the parent reference seeded via `initialData` so the user can't repoint a child away from the record it was created under. |
101
101
  | `excludeFields` | `[]` | Array of field keys to exclude from the auto-generated form. |
102
102
  | `includeFields` | `null` | Array of field keys to include (whitelist mode). Null means all fields. |
103
- | `fieldOverrides` | `{}` | Per-field override objects passed to `fieldsFromSchema`. |
103
+ | `fieldOverrides` | `{}` | Per-field override objects passed to `fieldsFromSchema`. A `widget` that names a registered component (v2 registry, then `customComponents`) mounts it as the field's control, with `value`/`modelValue`, `field`, `error` and `disabled`, writing back on `input` or `update:modelValue`. |
104
104
  | `nameField` | `'title'` | Which field is the "name" of the item (used in result messages). |
105
105
  | `size` | `'normal'` | NcDialog size — `'small'`, `'normal'`, or `'large'`. |
106
106
  | `dynamicLoadingLabel` | `'Loading the fields this choice adds.'` | Text shown while the fields a chosen value brings with it are being fetched. See *Fields the data decides* below. |
@@ -131,6 +131,22 @@
131
131
  :error="errors[field.key]"
132
132
  :updateField="updateField" />
133
133
 
134
+ <!-- A `widget` naming a registered component (v2 registry or
135
+ customComponents) mounts that component as the control.
136
+ It gets `value`/`modelValue`, `field`, `error` and
137
+ `disabled`, and writes back with `input` or
138
+ `update:modelValue`. -->
139
+ <component
140
+ :is="resolveFieldComponent(field)"
141
+ v-else-if="resolveFieldComponent(field)"
142
+ :value="formData[field.key]"
143
+ :modelValue="formData[field.key]"
144
+ :field="field"
145
+ :error="errors[field.key] || ''"
146
+ :disabled="!!field.readOnly"
147
+ @input="value => updateField(field.key, value)"
148
+ @update:modelValue="value => updateField(field.key, value)" />
149
+
134
150
  <!-- referenceType (AD-18): render the integration's
135
151
  single-entity widget instead of a plain input. -->
136
152
  <component
@@ -314,8 +330,26 @@
314
330
  plain select. NcSelectUsers wraps NcSelect and merges the
315
331
  rest of these bindings through, so the async load, search
316
332
  and select machinery is unchanged either way. -->
333
+ <!-- `look: "search"` (a `fieldOverrides` key): a search field
334
+ with rows of icon or initials, name, a secondary line from
335
+ `optionSecondary` and a check on the chosen one
336
+ (DqOverdragen "Nieuw team"). -->
337
+ <CnSearchSelect
338
+ v-if="field.look === 'search' && !isUserField(field)"
339
+ :inputId="'cn-form-' + field.key"
340
+ :inputLabel="fieldLabelText(field)"
341
+ :labelOutside="isBoardLook"
342
+ :options="searchSelectOptions(field)"
343
+ :modelValue="getEffectiveSelectedOption(field)"
344
+ :clearable="!field.required"
345
+ :disabled="field.readOnly"
346
+ :loading="isFieldLoading(field)"
347
+ :filterable="!isAsyncEnum(field)"
348
+ @update:modelValue="onSearchSelectChange(field, $event)"
349
+ @search="isAsyncEnum(field) ? onAsyncSearch(field, $event) : undefined" />
317
350
  <component
318
351
  :is="isUserField(field) ? 'NcSelectUsers' : 'NcSelect'"
352
+ v-else
319
353
  v-cn-select-aria="fieldAria(field)"
320
354
  :inputId="'cn-form-' + field.key"
321
355
  :inputLabel="fieldLabelText(field)"
@@ -799,6 +833,7 @@ import CnIconBrowser from '../CnIconBrowser/CnIconBrowser.vue'
799
833
  import CnJsonViewer from '../CnJsonViewer/CnJsonViewer.vue'
800
834
  import CnPropertySourceField from '../CnPropertySourceField/CnPropertySourceField.vue'
801
835
  import CnResourceSelect from '../CnResourceSelect/CnResourceSelect.vue'
836
+ import CnSearchSelect from '../CnSearchSelect/CnSearchSelect.vue'
802
837
  import CnSubObjectsField from '../CnSubObjectsField/CnSubObjectsField.vue'
803
838
  import { describeRowProblem, useChildRecords } from '../../composables/useChildRecords.js'
804
839
  import { clearAnonymousDrafts, draftIndicatorText, draftKey, formDraftMixin, hasDraftChanges, readDraft, resolveDraftUserId } from '../../composables/useFormDraft.js'
@@ -821,6 +856,7 @@ import {
821
856
  splitDynamicFormData,
822
857
  } from '../../utils/dynamicProperties.js'
823
858
  import { shouldShow } from '../../utils/fieldCondition.js'
859
+ import { fillTemplate } from '../../utils/fillTemplate.js'
824
860
  import { computeAssignments } from '../../utils/formAssign.js'
825
861
  import { resolveNextcloudGroup, searchNextcloudGroups } from '../../utils/groupAutocomplete.js'
826
862
  import { durationSeconds } from '../../utils/isoDuration.js'
@@ -844,6 +880,36 @@ import { resolveNextcloudUser, searchNextcloudUsers } from '../../utils/userAuto
844
880
  */
845
881
  const WIDE_WIDGETS = ['textarea', 'json', 'code']
846
882
 
883
+ // The widget names the form draws itself; a registered component cannot take one over.
884
+ const BUILT_IN_WIDGETS = new Set([
885
+ 'text',
886
+ 'email',
887
+ 'url',
888
+ 'number',
889
+ 'textarea',
890
+ 'switch',
891
+ 'select',
892
+ 'user',
893
+ 'group',
894
+ 'multiselect',
895
+ 'user-multiselect',
896
+ 'group-multiselect',
897
+ 'tags',
898
+ 'checkbox',
899
+ 'date',
900
+ 'datetime',
901
+ 'json',
902
+ 'code',
903
+ 'file',
904
+ 'duration',
905
+ 'child-records',
906
+ 'sub-objects',
907
+ 'property-source',
908
+ 'icon',
909
+ 'language',
910
+ 'timezone',
911
+ ])
912
+
847
913
  /** Largest file carried inline in the saved payload; bigger ones upload after save. */
848
914
  const INLINE_FILE_CAP = 1024 * 1024
849
915
 
@@ -1033,6 +1099,7 @@ export default {
1033
1099
  CnFileField,
1034
1100
  CnDurationField,
1035
1101
  CnPropertySourceField,
1102
+ CnSearchSelect,
1036
1103
  CnReplaceValuesDialog,
1037
1104
  CnSubObjectsField,
1038
1105
  CnChildRecordsField,
@@ -2029,6 +2096,28 @@ export default {
2029
2096
  },
2030
2097
 
2031
2098
  methods: {
2099
+ /**
2100
+ * Resolve the registered component a field's `widget` names: the v2
2101
+ * registry (any entry with a `component`) first, then the legacy
2102
+ * customComponents map. Built-in widget names never resolve here.
2103
+ *
2104
+ * @param {object} field A resolved field descriptor.
2105
+ * @return {object|null} Vue component, or null.
2106
+ */
2107
+ resolveFieldComponent(field) {
2108
+ const name = field && field.widget
2109
+ if (typeof name !== 'string' || name === '' || BUILT_IN_WIDGETS.has(name)) {
2110
+ return null
2111
+ }
2112
+ const entry = this.cnRegistry && this.cnRegistry[name]
2113
+ if (entry && entry.component) {
2114
+ return entry.component
2115
+ }
2116
+ const legacy = this.cnCustomComponents && this.cnCustomComponents[name]
2117
+ // A bare function in the legacy map is a handler, not a component.
2118
+ return legacy && typeof legacy === 'object' ? legacy : null
2119
+ },
2120
+
2032
2121
  /**
2033
2122
  * Resolve a field's reference integration widget, if any.
2034
2123
  * Returns the integration's single-entity widget component
@@ -4337,6 +4426,44 @@ export default {
4337
4426
  * @param {object} field The field definition
4338
4427
  * @return {Array}
4339
4428
  */
4429
+ /**
4430
+ * The options of a `look: "search"` field as CnSearchSelect rows: each
4431
+ * option's own `secondary` / `icon` / `avatar`, else the field's
4432
+ * `optionSecondary` template (filled from the option), `optionIcon` and
4433
+ * `optionAvatar`.
4434
+ *
4435
+ * @spec openspec/changes/search-select-look/specs/search-select/spec.md#requirement-a-single-picker-can-take-the-search-look
4436
+ * @param {object} field The field definition.
4437
+ * @return {Array<object>}
4438
+ */
4439
+ searchSelectOptions(field) {
4440
+ const template = typeof field.optionSecondary === 'string' ? field.optionSecondary : ''
4441
+ return this.getEffectiveOptions(field).map((option) => ({
4442
+ ...option,
4443
+ secondary: option.secondary || (template ? fillTemplate(template, option).trim() : ''),
4444
+ icon: option.icon || field.optionIcon || '',
4445
+ avatar: option.avatar ?? (field.optionAvatar === true),
4446
+ }))
4447
+ },
4448
+
4449
+ /**
4450
+ * Pick the option a CnSearchSelect field emitted by its id, and hand
4451
+ * it to the select change handler.
4452
+ *
4453
+ * @spec openspec/changes/search-select-look/specs/search-select/spec.md#requirement-a-single-picker-can-take-the-search-look
4454
+ * @param {object} field The field definition.
4455
+ * @param {string|number|null} id The chosen option's id.
4456
+ */
4457
+ onSearchSelectChange(field, id) {
4458
+ if (id === null || id === undefined) {
4459
+ this.onEffectiveSelectChange(field, null)
4460
+ return
4461
+ }
4462
+ const option = this.searchSelectOptions(field).find((o) => String(o.id ?? o.value) === String(id)) || { id, label: String(id) }
4463
+ const { secondary, icon, avatar, ...plain } = option
4464
+ this.onEffectiveSelectChange(field, plain)
4465
+ },
4466
+
4340
4467
  getEffectiveOptions(field) {
4341
4468
  if (this.isAsyncEnum(field)) {
4342
4469
  // TODO: restore to `this.asyncState[field.key]?.options || []` once on Vue 3 (buble doesn't support optional chaining)
@@ -389,6 +389,7 @@ export default {
389
389
  | `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). Named handlers resolve out of the v2 `registry` (a `kind: "handler"` entry) FIRST and fall back to this map. |
390
390
  | `showViewToggle` | Boolean | `true` | Whether to show the Cards/Table view toggle |
391
391
  | `inlineSearch` | Boolean | `false` | Show an inline search field in the actions bar (in addition to / instead of the sidebar search). Fed from the manifest as `pages[].config.inlineSearch`. The field takes the place of the "Showing X of Y" counter unless `showCountWithSearch` is set. |
392
+ | `search` | Boolean | `true` | `false` draws no inline search in the actions bar, also in the board look, which otherwise draws one on every index page (manifest: `config.search`). Wins over `inlineSearch`. |
392
393
  | `showCountWithSearch` | Boolean | `false` | Keep the "Showing X of Y" counter visible beside the inline search field, after the search and any `#after-search` controls (only relevant with `inlineSearch`, which otherwise takes the counter's place). Fed from the manifest as `pages[].config.showCountWithSearch`. |
393
394
  | `searchInFiles` | Boolean | `false` | "Also search inside files" switch beside the search box (manifest `config.searchInFiles`). On, a search with a term sends `_content_search=true`; rows found through a file show "Found in {file}" (`@self.matchedFile`). State kept in the route as `contentSearch=1`. Emits `content-search`. |
394
395
  | `searchPlaceholder` | String | `''` | Placeholder for the inline search field (manifest `config.searchPlaceholder`). |
@@ -878,8 +879,9 @@ The list view (`view-mode="list"`) and standalone sort dropdown add these props:
878
879
  | `listConfig` | Object | `{}` | Field mapping for the default list rows (`CnObjectRow`). |
879
880
  | `listComponent` | String | `''` | Custom row component, resolved against the v2 `registry` (any kind carrying a `component`) and then the legacy `customComponents` map. |
880
881
  | `showSortSelect` | Boolean | `false` | Show a standalone sort dropdown in the actions bar. |
881
- | `sortSelectOptions` | Array | `[]` | Options `{ value, label }` for the sort dropdown. |
882
- | `sortSelectValue` | String | `''` | Selected sort dropdown value (controlled). |
882
+ | `sortSelectOptions` | Array | `[]` | Options `{ value, label, order? }` for the sort dropdown. On a self-fetch page a choice orders the list as a header click does (`_order`, kept in the address): by the option's `order`, else its `value` read as an order (`"-createdAt"`, `"priority:desc,title"`). |
883
+ | `sortSelectValue` | String | `''` | Selected sort dropdown value. Empty lets a self-fetch page show the option matching its current order, so a header click that matches an option shows it. |
884
+ | `defaultOrder` | String, Object or Array | `''` | The order a self-fetch page opens on when neither the address nor `sortKeys`/`sortKey` gives one (manifest `config.defaultOrder`): `"-createdAt"`, `"priority:desc,title"`, an `_order` map `{ "priority": "desc" }` or a `[{ key, order }]` list. |
883
885
 
884
886
  The `#list-item`, `#row-icon`, `#row-badges`, and `#row-actions` slots override the list rows (see [CnObjectList](./cn-object-list.md)). Emits `@sort-change` with the chosen sort value.
885
887
 
@@ -1094,6 +1096,16 @@ Sources: `register` (fetch the folder list from an OpenRegister `register`/`sche
1094
1096
  }
1095
1097
  ```
1096
1098
 
1099
+ `pane: false` draws no pane. The folder still comes from the deep-link filter (`/cases?caseType=<uuid>`) and its scope look (title, count line, columns, quick filters, `afterList` from the folder or its row's `x-index`) still applies, so a list narrowed to one case type looks like that case type's list without a folder column (boards DqWooVerzoeken, DqKlachten):
1100
+
1101
+ ```json
1102
+ "folderSidebar": {
1103
+ "source": "register", "register": "dossiq", "schema": "caseType",
1104
+ "idField": "@self.uuid", "nameField": "title",
1105
+ "filterField": "caseType", "pane": false
1106
+ }
1107
+ ```
1108
+
1097
1109
  ### A column set per folder
1098
1110
 
1099
1111
  A folder is also a scope. Give it `columns`, `defaultSort` or `searchFields`