@conduction/nextcloud-vue 2.99.0 → 2.101.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 (180) hide show
  1. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue.js +1 -1
  2. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js +6 -1
  3. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js.map +1 -1
  4. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js +2 -1
  5. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js.map +1 -1
  6. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue.js +1 -1
  7. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js +22 -1
  8. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js.map +1 -1
  9. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js +26 -16
  10. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js.map +1 -1
  11. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
  12. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +51 -7
  13. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  14. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +104 -73
  15. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  16. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +25 -8
  17. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  18. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +5 -3
  19. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  20. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue.js +1 -1
  21. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +11 -8
  22. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  23. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +13 -4
  24. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
  25. package/dist/esm/components/CnExportsWidget/CnExportsWidget.vue.js +9 -0
  26. package/dist/esm/components/CnExportsWidget/CnExportsWidget.vue.js.map +1 -0
  27. package/dist/esm/components/CnExportsWidget/CnExportsWidget.vue2.js +139 -0
  28. package/dist/esm/components/CnExportsWidget/CnExportsWidget.vue2.js.map +1 -0
  29. package/dist/esm/components/CnExportsWidget/CnExportsWidget.vue3.js +76 -0
  30. package/dist/esm/components/CnExportsWidget/CnExportsWidget.vue3.js.map +1 -0
  31. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
  32. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +54 -3
  33. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
  34. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js +16 -2
  35. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
  36. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue.js +1 -1
  37. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +160 -2
  38. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
  39. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +206 -94
  40. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
  41. package/dist/esm/components/CnFilesBrowser/filesReview.js +61 -0
  42. package/dist/esm/components/CnFilesBrowser/filesReview.js.map +1 -0
  43. package/dist/esm/components/CnFlowsPage/CnFlowBoard.vue.js +1 -1
  44. package/dist/esm/components/CnFlowsPage/CnFlowBoard.vue2.js +42 -7
  45. package/dist/esm/components/CnFlowsPage/CnFlowBoard.vue2.js.map +1 -1
  46. package/dist/esm/components/CnFlowsPage/CnFlowBoard.vue3.js +121 -57
  47. package/dist/esm/components/CnFlowsPage/CnFlowBoard.vue3.js.map +1 -1
  48. package/dist/esm/components/CnFlowsPage/CnFlowEditorPage.vue2.js +10 -0
  49. package/dist/esm/components/CnFlowsPage/CnFlowEditorPage.vue2.js.map +1 -1
  50. package/dist/esm/components/CnFlowsPage/CnFlowEditorPage.vue3.js +2 -1
  51. package/dist/esm/components/CnFlowsPage/CnFlowEditorPage.vue3.js.map +1 -1
  52. package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue.js +9 -0
  53. package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue.js.map +1 -0
  54. package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue2.js +279 -0
  55. package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue2.js.map +1 -0
  56. package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue3.js +122 -0
  57. package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue3.js.map +1 -0
  58. package/dist/esm/components/CnGroupedCardsWidget/index.js +29 -0
  59. package/dist/esm/components/CnGroupedCardsWidget/index.js.map +1 -0
  60. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +387 -78
  61. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  62. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +317 -209
  63. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  64. package/dist/esm/components/CnIndexPage/useNamedSource.js +4 -1
  65. package/dist/esm/components/CnIndexPage/useNamedSource.js.map +1 -1
  66. package/dist/esm/components/CnMapWidget/CnMapWidget.vue.js +1 -1
  67. package/dist/esm/components/CnMapWidget/CnMapWidget.vue2.js +15 -0
  68. package/dist/esm/components/CnMapWidget/CnMapWidget.vue2.js.map +1 -1
  69. package/dist/esm/components/CnMapWidget/CnMapWidget.vue3.js.map +1 -1
  70. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue.js +1 -1
  71. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +14 -1
  72. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
  73. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js +14 -6
  74. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
  75. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue.js +1 -1
  76. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +10 -0
  77. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
  78. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +6 -1
  79. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
  80. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue.js +1 -1
  81. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue2.js +91 -4
  82. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue2.js.map +1 -1
  83. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue3.js +63 -38
  84. package/dist/esm/components/CnOrderedListField/CnOrderedListField.vue3.js.map +1 -1
  85. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
  86. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +68 -7
  87. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  88. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +256 -240
  89. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  90. package/dist/esm/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue.js +9 -0
  91. package/dist/esm/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue.js.map +1 -0
  92. package/dist/esm/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue2.js +175 -0
  93. package/dist/esm/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue2.js.map +1 -0
  94. package/dist/esm/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue3.js +77 -0
  95. package/dist/esm/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue3.js.map +1 -0
  96. package/dist/esm/components/CnSettingsFieldsSection/CnSettingsFieldsSection.vue.js +1 -1
  97. package/dist/esm/components/CnSettingsFieldsSection/CnSettingsFieldsSection.vue2.js +38 -9
  98. package/dist/esm/components/CnSettingsFieldsSection/CnSettingsFieldsSection.vue2.js.map +1 -1
  99. package/dist/esm/components/CnSettingsFieldsSection/CnSettingsFieldsSection.vue3.js.map +1 -1
  100. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue.js +1 -1
  101. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js +51 -4
  102. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js.map +1 -1
  103. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js +40 -25
  104. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js.map +1 -1
  105. package/dist/esm/components/CnWidgetGrid/builtInWidgets.js +4 -0
  106. package/dist/esm/components/CnWidgetGrid/builtInWidgets.js.map +1 -1
  107. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
  108. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
  109. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue.js +1 -1
  110. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue2.js +7 -7
  111. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue2.js.map +1 -1
  112. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue3.js.map +1 -1
  113. package/dist/esm/composables/indexSources.js +167 -20
  114. package/dist/esm/composables/indexSources.js.map +1 -1
  115. package/dist/esm/composables/useEndpointSource.js +28 -6
  116. package/dist/esm/composables/useEndpointSource.js.map +1 -1
  117. package/dist/esm/index.js +1 -0
  118. package/dist/esm/index.js.map +1 -1
  119. package/dist/esm/l10n/en.json.js +43 -1
  120. package/dist/esm/l10n/en.json.js.map +1 -1
  121. package/dist/esm/l10n/nl.json.js +41 -1
  122. package/dist/esm/l10n/nl.json.js.map +1 -1
  123. package/dist/esm/library-style.js +1 -1
  124. package/dist/esm/nextcloud-vue.css +1101 -396
  125. package/dist/esm/utils/languageMapText.js +50 -0
  126. package/dist/esm/utils/languageMapText.js.map +1 -0
  127. package/dist/esm/utils/libraryWidgetKeys.js +3 -0
  128. package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
  129. package/dist/esm/utils/mapList.js +71 -0
  130. package/dist/esm/utils/mapList.js.map +1 -0
  131. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  132. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  133. package/dist/nextcloud-vue.cjs.js +9996 -7378
  134. package/dist/nextcloud-vue.cjs.js.map +2 -2
  135. package/dist/nextcloud-vue.css +1101 -396
  136. package/l10n/en.json +43 -1
  137. package/l10n/nl.json +41 -1
  138. package/package.json +1 -1
  139. package/src/components/CnAdminSettingsShell/CnAdminSettingsShell.vue +7 -1
  140. package/src/components/CnChoiceCards/CnChoiceCards.md +39 -0
  141. package/src/components/CnChoiceCards/CnChoiceCards.vue +93 -16
  142. package/src/components/CnDashboardPage/CnDashboardPage.vue +89 -20
  143. package/src/components/CnDetailPage/CnDetailPage.vue +28 -9
  144. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +13 -4
  145. package/src/components/CnExportsWidget/CnExportsWidget.vue +233 -0
  146. package/src/components/CnExportsWidget/index.js +7 -0
  147. package/src/components/CnFactsWidget/CnFactsWidget.vue +65 -3
  148. package/src/components/CnFilesBrowser/CnFilesBrowser.vue +324 -0
  149. package/src/components/CnFilesBrowser/filesReview.js +57 -0
  150. package/src/components/CnFlowsPage/CnFlowBoard.vue +77 -2
  151. package/src/components/CnFlowsPage/CnFlowEditorPage.vue +11 -0
  152. package/src/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue +433 -0
  153. package/src/components/CnGroupedCardsWidget/index.js +28 -0
  154. package/src/components/CnIndexPage/CnIndexPage.vue +376 -11
  155. package/src/components/CnIndexPage/useNamedSource.js +4 -1
  156. package/src/components/CnMapWidget/CnMapWidget.vue +37 -0
  157. package/src/components/CnObjectListWidget/CnObjectListWidget.vue +29 -1
  158. package/src/components/CnObjectSidebar/CnFilesTab.vue +15 -0
  159. package/src/components/CnOrderedListField/CnOrderedListField.vue +151 -5
  160. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +114 -7
  161. package/src/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue +272 -0
  162. package/src/components/CnSavedViewAlertWidget/index.js +7 -0
  163. package/src/components/CnSettingsFieldsSection/CnSettingsFieldsSection.vue +38 -9
  164. package/src/components/CnSetupWizard/CnSetupWizard.vue +72 -5
  165. package/src/components/CnWidgetGrid/builtInWidgets.js +4 -0
  166. package/src/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
  167. package/src/components/CnWizardDialog/CnWizardDialog.vue +7 -7
  168. package/src/components/index.js +3 -0
  169. package/src/composables/indexSources.js +166 -19
  170. package/src/composables/useEndpointSource.js +28 -6
  171. package/src/css/detail-page.css +15 -0
  172. package/src/css/dialog.css +48 -0
  173. package/src/css/index-page.css +117 -0
  174. package/src/css/look-board.css +127 -1
  175. package/src/index.js +1 -0
  176. package/src/schemas/app-manifest-v2.schema.json +55 -1
  177. package/src/utils/languageMapText.js +46 -0
  178. package/src/utils/libraryWidgetKeys.js +3 -0
  179. package/src/utils/mapList.js +67 -0
  180. package/src/utils/validateManifestV2.compiled.js +3 -3
@@ -233,6 +233,15 @@
233
233
  {{ moreLabel }}
234
234
  </p>
235
235
  </template>
236
+ <!-- `content.footnote`: a line under the table ("Awb 4:17 en 4:18"),
237
+ through the host translate. Not while the list waits for its
238
+ context or failed to load. -->
239
+ <p
240
+ v-if="resolvedFootnote && !waitingForContext && !error"
241
+ class="cn-object-list-widget__footnote"
242
+ data-testid="cn-object-list-widget-footnote">
243
+ {{ resolvedFootnote }}
244
+ </p>
236
245
  <!-- Create affordance (ADR-062): every collection carries its Add at
237
246
  the bottom of the widget; the host card's Actions menu calls the
238
247
  same openCreate() through the public method. Suppressed while the
@@ -430,8 +439,10 @@ export default {
430
439
  * `hideHeader: true` drops the table's column header row (CnDataTable's
431
440
  * `hideHeader`), for a short list that reads as rows, not as a table.
432
441
  *
442
+ * `footnote` is a line under the table, through the host translate.
443
+ *
433
444
  * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-small-opt-ins-from-the-decidiq-and-learniq-lanes
434
- * @type {{register?: string, schema?: string, filter?: object, sort?: {field?: string, dir?: string}, limit?: number, extend?: Array<string>, columns?: Array, rowActions?: Array<object>, dropZone?: object, upload?: boolean, groupBy?: string, groupLabel?: string, groupLabelResolve?: {register: string, schema: string, labelField?: string}, selectable?: boolean, bulkActions?: Array<object>, sortable?: boolean, facet?: {field: string, label?: string}, rowRoute?: string, prompt?: string, emptyText?: string, viewAllRoute?: string, viewAllQuery?: object, hideHeader?: boolean}}
445
+ * @type {{register?: string, schema?: string, filter?: object, sort?: {field?: string, dir?: string}, limit?: number, extend?: Array<string>, columns?: Array, rowActions?: Array<object>, dropZone?: object, upload?: boolean, groupBy?: string, groupLabel?: string, groupLabelResolve?: {register: string, schema: string, labelField?: string}, selectable?: boolean, bulkActions?: Array<object>, sortable?: boolean, facet?: {field: string, label?: string}, rowRoute?: string, prompt?: string, emptyText?: string, footnote?: string, viewAllRoute?: string, viewAllQuery?: object, hideHeader?: boolean}}
435
446
  */
436
447
  content: {
437
448
  type: Object,
@@ -918,6 +929,17 @@ export default {
918
929
  return this.emptyText ? fn(this.emptyText) : this.emptyText
919
930
  },
920
931
 
932
+ /**
933
+ * `content.footnote` run through the host translate function, or ''.
934
+ *
935
+ * @return {string}
936
+ */
937
+ resolvedFootnote() {
938
+ const note = typeof this.content.footnote === 'string' ? this.content.footnote : ''
939
+ const fn = typeof this.cnTranslate === 'function' ? this.cnTranslate : (k) => k
940
+ return note ? fn(note) : ''
941
+ },
942
+
921
943
  /**
922
944
  * The rows actually rendered: capped to what fits the host grid cell
923
945
  * (ADR-062 — content adapts to the cell, never a nested scrollbar).
@@ -2027,6 +2049,12 @@ export default {
2027
2049
  padding: 4px;
2028
2050
  }
2029
2051
 
2052
+ .cn-object-list-widget__footnote {
2053
+ color: var(--color-text-maxcontrast);
2054
+ font-size: 13px;
2055
+ margin: 8px 0 0;
2056
+ }
2057
+
2030
2058
  /* Footer Add — same pattern as the integration leaves' footer action
2031
2059
  ("Open in Calendar"): full-width, centered, divider-topped, pinned to
2032
2060
  the card bottom. */
@@ -15,6 +15,11 @@
15
15
  :dropHint="dropHint"
16
16
  :rowActions="rowActions"
17
17
  :newActions="newActions"
18
+ :headerActions="headerActions"
19
+ :rowButton="rowButton"
20
+ :reviewSummary="reviewSummary"
21
+ :footnote="footnote"
22
+ :nameSecondary="nameSecondary"
18
23
  :linkedItems="linkedItems"
19
24
  :columns="columns"
20
25
  :rowData="rowDataSource"
@@ -420,6 +425,16 @@ export default {
420
425
  * @type {string[]}
421
426
  */
422
427
  facets: { type: Array, default: () => [] },
428
+ /** Forwarded to CnFilesBrowser: the host's own buttons in the bar. */
429
+ headerActions: { type: Array, default: () => [] },
430
+ /** Forwarded to CnFilesBrowser: one visible button on every file row. */
431
+ rowButton: { type: Object, default: null },
432
+ /** Forwarded to CnFilesBrowser: the review summary above the list. */
433
+ reviewSummary: { type: Object, default: null },
434
+ /** Forwarded to CnFilesBrowser: the host's own line under the list. */
435
+ footnote: { type: String, default: '' },
436
+ /** Forwarded to CnFilesBrowser: the second line under each file's name. */
437
+ nameSecondary: { type: String, default: '' },
423
438
  /** A dotted path to the list of records in the `rowDataUrl` answer; empty reads the answer itself. */
424
439
  rowDataPath: { type: String, default: '' },
425
440
  /** The record field that holds the Nextcloud file id. */
@@ -17,7 +17,30 @@
17
17
  v-for="(row, index) in rows"
18
18
  :key="row.value"
19
19
  class="cn-ordered-list-field__row"
20
- :data-testid="`cn-ordered-list-row-${row.value}`">
20
+ :class="{
21
+ 'cn-ordered-list-field__row--dragging': dragIndex === index,
22
+ 'cn-ordered-list-field__row--over': dropIndex === index && dragIndex !== index,
23
+ }"
24
+ :data-testid="`cn-ordered-list-row-${row.value}`"
25
+ @dragover.prevent="onDragOver(index)"
26
+ @drop.prevent="onDrop(index)">
27
+ <!-- A drag handle (DqPersoonlijkeInstellingen). It is a button: a
28
+ keyboard user moves the row with the arrow keys on it, and the
29
+ up and down buttons stay for anyone who neither drags nor uses
30
+ arrow keys. -->
31
+ <button
32
+ v-if="!disabled"
33
+ type="button"
34
+ class="cn-ordered-list-field__handle"
35
+ draggable="true"
36
+ :aria-label="t('nextcloud-vue', 'Reorder {item}: drag, or press the up and down arrow keys', { item: row.label })"
37
+ :data-testid="`cn-ordered-list-handle-${row.value}`"
38
+ @dragstart="onDragStart($event, index)"
39
+ @dragend="onDragEnd"
40
+ @keydown.up.prevent="move(index, index - 1, 'handle')"
41
+ @keydown.down.prevent="move(index, index + 1, 'handle')">
42
+ <DragVertical :size="20" />
43
+ </button>
21
44
  <span class="cn-ordered-list-field__position" aria-hidden="true">{{ index + 1 }}</span>
22
45
  <span v-if="row.icon" class="cn-ordered-list-field__tile" aria-hidden="true">
23
46
  <CnIcon :name="row.icon" :size="18" />
@@ -104,6 +127,7 @@ import { NcSelect } from '@nextcloud/vue'
104
127
  import ChevronDown from 'vue-material-design-icons/ChevronDown.vue'
105
128
  import ChevronUp from 'vue-material-design-icons/ChevronUp.vue'
106
129
  import Close from 'vue-material-design-icons/Close.vue'
130
+ import DragVertical from 'vue-material-design-icons/DragVertical.vue'
107
131
  import CnIcon from '../CnIcon/CnIcon.vue'
108
132
 
109
133
  let orderedListUid = 0
@@ -134,7 +158,7 @@ let orderedListUid = 0
134
158
  export default {
135
159
  name: 'CnOrderedListField',
136
160
 
137
- components: { ChevronDown, ChevronUp, Close, CnIcon, NcSelect },
161
+ components: { ChevronDown, ChevronUp, Close, CnIcon, DragVertical, NcSelect },
138
162
 
139
163
  props: {
140
164
  /** The values in their order. */
@@ -205,6 +229,12 @@ export default {
205
229
  return {
206
230
  labelId: `cn-ordered-list-label-${orderedListUid}`,
207
231
  announcement: '',
232
+ /** The value whose handle takes focus after the next render, or null. */
233
+ pendingHandleFocus: null,
234
+ /** The index of the row being dragged, or -1. */
235
+ dragIndex: -1,
236
+ /** The index of the row the drag is over, or -1. */
237
+ dropIndex: -1,
208
238
  }
209
239
  },
210
240
 
@@ -244,6 +274,20 @@ export default {
244
274
  },
245
275
  },
246
276
 
277
+ watch: {
278
+ // The parent's new order arrives as new rows; the moved row's handle
279
+ // lost focus when its row moved in the DOM, so it takes it back.
280
+ rows: {
281
+ flush: 'post',
282
+ handler() {
283
+ if (this.pendingHandleFocus !== null) {
284
+ this.focusPendingHandle()
285
+ this.pendingHandleFocus = null
286
+ }
287
+ },
288
+ },
289
+ },
290
+
247
291
  methods: {
248
292
  t,
249
293
 
@@ -270,13 +314,15 @@ export default {
270
314
  },
271
315
 
272
316
  /**
273
- * Move a row to another place and keep focus on the pressed button
274
- * where it still makes sense.
317
+ * Move a row to another place and keep focus on the pressed control
318
+ * where it still makes sense: the row's handle after an arrow key on
319
+ * it, else the up or down button.
275
320
  *
276
321
  * @param {number} from Its index.
277
322
  * @param {number} to The index it moves to.
323
+ * @param {string} [via] `handle` when the move came from the handle.
278
324
  */
279
- move(from, to) {
325
+ move(from, to, via = '') {
280
326
  if (to < 0 || to >= this.rows.length) {
281
327
  return
282
328
  }
@@ -286,7 +332,16 @@ export default {
286
332
  const row = this.rows[from]
287
333
  this.emitValue(values)
288
334
  this.announcement = t('nextcloud-vue', '{item} moved to position {position} of {total}', { item: row.label, position: to + 1, total: values.length })
335
+ if (via === 'handle') {
336
+ // Re-focused once the parent hands the new order back: moving the
337
+ // row in the DOM takes focus off its handle.
338
+ this.pendingHandleFocus = row.value
339
+ }
289
340
  this.$nextTick(() => {
341
+ if (via === 'handle') {
342
+ this.focusPendingHandle()
343
+ return
344
+ }
290
345
  const direction = to < from ? 'up' : 'down'
291
346
  const atEdge = (direction === 'up' && to === 0) || (direction === 'down' && to === values.length - 1)
292
347
  const target = this.$el.querySelector(`[data-testid="cn-ordered-list-${atEdge ? (direction === 'up' ? 'down' : 'up') : direction}-${row.value}"]`)
@@ -296,6 +351,61 @@ export default {
296
351
  })
297
352
  },
298
353
 
354
+ /** Put focus back on the handle of the row just moved by its handle. */
355
+ focusPendingHandle() {
356
+ if (this.pendingHandleFocus === null) {
357
+ return
358
+ }
359
+ const handle = this.$el.querySelector(`[data-testid="cn-ordered-list-handle-${this.pendingHandleFocus}"]`)
360
+ if (handle) {
361
+ handle.focus()
362
+ }
363
+ },
364
+
365
+ /**
366
+ * Start dragging a row by its handle.
367
+ *
368
+ * @param {DragEvent} event The drag event.
369
+ * @param {number} index The row's index.
370
+ */
371
+ onDragStart(event, index) {
372
+ this.dragIndex = index
373
+ if (event && event.dataTransfer) {
374
+ event.dataTransfer.effectAllowed = 'move'
375
+ event.dataTransfer.setData('text/plain', String(this.rows[index].value))
376
+ }
377
+ },
378
+
379
+ /**
380
+ * Mark the row a drag is over as where the row will land.
381
+ *
382
+ * @param {number} index The row's index.
383
+ */
384
+ onDragOver(index) {
385
+ if (this.dragIndex !== -1) {
386
+ this.dropIndex = index
387
+ }
388
+ },
389
+
390
+ /**
391
+ * Drop the dragged row in the place of the row it is over.
392
+ *
393
+ * @param {number} index The row's index.
394
+ */
395
+ onDrop(index) {
396
+ const from = this.dragIndex
397
+ this.onDragEnd()
398
+ if (from !== -1 && from !== index) {
399
+ this.move(from, index, 'handle')
400
+ }
401
+ },
402
+
403
+ /** End a drag, dropped or not. */
404
+ onDragEnd() {
405
+ this.dragIndex = -1
406
+ this.dropIndex = -1
407
+ },
408
+
299
409
  /**
300
410
  * Remove a row.
301
411
  *
@@ -371,6 +481,42 @@ export default {
371
481
  border-top: 1px solid var(--color-border);
372
482
  }
373
483
 
484
+ .cn-ordered-list-field__row--dragging {
485
+ opacity: 0.5;
486
+ }
487
+
488
+ /* Where a dragged row will land: a primary line on the row's top edge. */
489
+ .cn-ordered-list-field__row--over {
490
+ box-shadow: inset 0 2px 0 var(--color-primary-element);
491
+ }
492
+
493
+ .cn-ordered-list-field__handle {
494
+ display: inline-flex;
495
+ flex: none;
496
+ align-items: center;
497
+ justify-content: center;
498
+ width: 28px;
499
+ min-width: 28px;
500
+ height: 34px;
501
+ min-height: 34px;
502
+ margin: 0 -6px 0 -8px;
503
+ padding: 0;
504
+ border: 0;
505
+ border-radius: var(--border-radius-element, 8px);
506
+ background: transparent;
507
+ color: var(--color-text-maxcontrast);
508
+ cursor: grab;
509
+ }
510
+
511
+ .cn-ordered-list-field__handle:active {
512
+ cursor: grabbing;
513
+ }
514
+
515
+ .cn-ordered-list-field__handle:focus-visible {
516
+ outline: 2px solid var(--color-primary-element);
517
+ outline-offset: 1px;
518
+ }
519
+
374
520
  .cn-ordered-list-field__position {
375
521
  flex: none;
376
522
  min-width: 18px;
@@ -17,6 +17,22 @@
17
17
  <p v-else-if="rows.length === 0" class="cn-record-list__empty" data-testid="cn-record-list-empty">
18
18
  {{ emptyLabel }}
19
19
  </p>
20
+ <!-- notice: the rows as sentences in one toned box ("2 waarschuwingen: ..."). -->
21
+ <div
22
+ v-else-if="presentation === 'notice'"
23
+ class="cn-record-list__notice"
24
+ :class="`cn-record-list__notice--${noticeVariant}`"
25
+ role="note"
26
+ data-testid="cn-record-list-notice">
27
+ <CnIcon
28
+ :name="noticeIcon"
29
+ :size="18"
30
+ class="cn-record-list__notice-icon"
31
+ aria-hidden="true" />
32
+ <p class="cn-record-list__notice-text">
33
+ {{ noticeText }}
34
+ </p>
35
+ </div>
20
36
  <ul v-else class="cn-record-list__list">
21
37
  <li
22
38
  v-for="item in items"
@@ -210,7 +226,22 @@ import { readPath } from '../../utils/readPath.js'
210
226
  import { dropOptionalUnresolved, hasUnresolvedTokens, resolveFilterTokens } from '../../utils/resolveFilterTokens.js'
211
227
 
212
228
  const PAGE_REFRESH_CHANNEL = 'cn:page:refresh'
213
- const PRESENTATIONS = ['rows', 'feed', 'inline', 'cards']
229
+ const PRESENTATIONS = ['rows', 'feed', 'inline', 'cards', 'notice']
230
+
231
+ /** The tones a `notice` list takes. */
232
+ const NOTICE_VARIANTS = ['info', 'warning', 'error']
233
+
234
+ /**
235
+ * A row that is a plain string or number (an endpoint answering `["...", "..."]`)
236
+ * read as `{ value }`, so the templates reach it as `{value}` and a list
237
+ * without a `title` shows the value itself. An object row is returned as is.
238
+ *
239
+ * @param {(object|string|number)} row The row.
240
+ * @return {(object|string|number)} The row, or `{ value }` for a string or number.
241
+ */
242
+ function scalarRow(row) {
243
+ return typeof row === 'string' || typeof row === 'number' ? { value: String(row) } : row
244
+ }
214
245
 
215
246
  /**
216
247
  * Unwrap a value that may be a ref.
@@ -266,7 +297,7 @@ export default {
266
297
  props: {
267
298
  /**
268
299
  * The list config: `{ register, schema, filter?, sort?, limit?, extend?,
269
- * presentation?, title?, meta?, text?, label?, pill?, trailing?, icon?,
300
+ * presentation?, variant?, lead?, title?, meta?, text?, label?, pill?, trailing?, icon?,
270
301
  * rowRoute?, rowIdField?, headerLink?, headerNote?, emptyText?, errorText?,
271
302
  * valueLabels?, endpointSource?, bar? }`. `bar` (`{ valueField, maxField?,
272
303
  * overVariant? }`) draws a thin track under each row: filled to
@@ -275,7 +306,14 @@ export default {
275
306
  * largest row. `endpointSource` (`{ url, method?,
276
307
  * params?, responsePath? }`) reads the rows from an app endpoint instead
277
308
  * of `register` / `schema`. `valueLabels` maps a field's stored values to the labels
278
- * the templates show.
309
+ * the templates show. `headerNote` and `headerLink.label` fill `{total}`
310
+ * and `{count:<field>=<value>}`, the loaded rows with that value.
311
+ * A row that is a plain string or number (an endpoint
312
+ * answering `["…", "…"]`) is read as `{ value }`: templates reach it as
313
+ * `{value}`, and without `title` a row shows the value itself.
314
+ * `presentation: "notice"` draws the rows as sentences in one box toned by
315
+ * `variant` (`info`, `warning` or `error`), after an optional `lead`
316
+ * (`{total}` filled in).
279
317
  *
280
318
  * @type {object}
281
319
  */
@@ -334,6 +372,35 @@ export default {
334
372
 
335
373
  computed: {
336
374
  /** @return {string} `rows`, `feed`, `inline` or `cards`. */
375
+ /**
376
+ * The tone of a `notice` list (`content.variant`): info, warning or error.
377
+ *
378
+ * @return {string} The variant, `info` when unset or unknown.
379
+ * @spec openspec/changes/record-list-string-rows/specs/record-list-string-rows/spec.md#requirement-a-record-list-draws-rows-that-are-plain-strings
380
+ */
381
+ noticeVariant() {
382
+ const value = this.content?.variant
383
+ return NOTICE_VARIANTS.includes(value) ? value : 'info'
384
+ },
385
+
386
+ /** @return {string} The icon of the notice tone. */
387
+ noticeIcon() {
388
+ return this.noticeVariant === 'info' ? 'InformationOutline' : 'AlertOutline'
389
+ },
390
+
391
+ /**
392
+ * The rows of a `notice` list as one paragraph: each row's title, as
393
+ * sentences, after the optional `lead` with `{total}` filled in.
394
+ *
395
+ * @return {string} The text.
396
+ * @spec openspec/changes/record-list-string-rows/specs/record-list-string-rows/spec.md#requirement-a-record-list-draws-rows-that-are-plain-strings
397
+ */
398
+ noticeText() {
399
+ const parts = this.items.map((item) => item.title).filter((text) => text !== '')
400
+ const lead = typeof this.content?.lead === 'string' && this.content.lead !== '' ? this.withTotal(this.content.lead) : ''
401
+ return [lead, ...parts].filter(Boolean).join(' ')
402
+ },
403
+
337
404
  presentation() {
338
405
  const value = this.content?.presentation
339
406
  return PRESENTATIONS.includes(value) ? value : 'rows'
@@ -421,7 +488,8 @@ export default {
421
488
  items() {
422
489
  const c = this.content || {}
423
490
  const barScale = this.barScale
424
- return this.rows.slice(0, this.limit).map((row, index) => {
491
+ return this.rows.slice(0, this.limit).map((raw, index) => {
492
+ const row = scalarRow(raw)
425
493
  const link = this.rowLink(row)
426
494
  return {
427
495
  key: (row && (row.id || row['@self']?.id)) || `row-${index}`,
@@ -430,7 +498,7 @@ export default {
430
498
  tag: link ? 'router-link' : 'div',
431
499
  attrs: link ? { to: link } : {},
432
500
  initials: this.initialsFor(row),
433
- title: this.fill(c.title, row),
501
+ title: this.fill(c.title || (row !== raw ? '{value}' : ''), row),
434
502
  meta: this.metaOf(row),
435
503
  text: this.fill(c.text, row),
436
504
  label: this.fill(c.label, row),
@@ -638,13 +706,22 @@ export default {
638
706
  },
639
707
 
640
708
  /**
641
- * Replace `{total}` in a translated label.
709
+ * Fill a translated label's counts: `{total}` is the list's total, and
710
+ * `{count:<field>=<value>}` the loaded rows whose field reads that
711
+ * value ("{count:status=done} van {total} stappen klaar"). The count
712
+ * reads the rows the list loaded, so it is exact when `limit` covers
713
+ * the list.
642
714
  *
643
715
  * @param {string} label The label.
644
716
  * @return {string} The text.
645
717
  */
646
718
  withTotal(label) {
647
- return this.translate(label).replace(/\{total\}/g, String(this.total))
719
+ return this.translate(label)
720
+ .replace(/\{total\}/g, String(this.total))
721
+ .replace(/\{count:([^=}]+)=([^}]*)\}/g, (match, field, value) => {
722
+ const wanted = value.trim()
723
+ return String(this.rows.filter((row) => String(readPath(row, field.trim())) === wanted).length)
724
+ })
648
725
  },
649
726
 
650
727
  /**
@@ -1125,6 +1202,36 @@ export default {
1125
1202
  margin: 0;
1126
1203
  }
1127
1204
 
1205
+ .cn-record-list__notice {
1206
+ display: flex;
1207
+ gap: 10px;
1208
+ align-items: flex-start;
1209
+ padding: 12px 14px;
1210
+ border-radius: var(--border-radius-large, 10px);
1211
+ background: var(--color-primary-element-light, var(--color-background-dark));
1212
+ color: var(--color-main-text);
1213
+ }
1214
+
1215
+ .cn-record-list__notice--warning {
1216
+ background: var(--color-warning-light, rgba(255, 196, 0, 0.12));
1217
+ color: var(--color-warning-text, var(--color-main-text));
1218
+ }
1219
+
1220
+ .cn-record-list__notice--error {
1221
+ background: var(--color-error-light, rgba(219, 0, 0, 0.08));
1222
+ color: var(--color-error-text, var(--color-main-text));
1223
+ }
1224
+
1225
+ .cn-record-list__notice-icon {
1226
+ flex: 0 0 auto;
1227
+ margin-top: 1px;
1228
+ }
1229
+
1230
+ .cn-record-list__notice-text {
1231
+ margin: 0;
1232
+ line-height: 1.5;
1233
+ }
1234
+
1128
1235
  .cn-record-list__loading {
1129
1236
  display: flex;
1130
1237
  justify-content: center;