@conduction/nextcloud-vue 2.147.0 → 2.149.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 (130) hide show
  1. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue.js +1 -1
  2. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js +24 -7
  3. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js.map +1 -1
  4. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue3.js.map +1 -1
  5. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue.js +1 -1
  6. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue2.js +12 -1
  7. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue2.js.map +1 -1
  8. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue3.js +2 -1
  9. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue3.js.map +1 -1
  10. package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue.js +1 -1
  11. package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue2.js +23 -1
  12. package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue2.js.map +1 -1
  13. package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue3.js +1 -1
  14. package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue3.js.map +1 -1
  15. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +34 -1
  16. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  17. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  18. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +71 -8
  19. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  20. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +2 -2
  21. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  22. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue.js +1 -1
  23. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue2.js.map +1 -1
  24. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue3.js.map +1 -1
  25. package/dist/esm/components/CnDeviceFramesWidget/CnDeviceFramesWidget.vue.js +1 -1
  26. package/dist/esm/components/CnDeviceFramesWidget/CnDeviceFramesWidget.vue2.js.map +1 -1
  27. package/dist/esm/components/CnDeviceFramesWidget/CnDeviceFramesWidget.vue3.js.map +1 -1
  28. package/dist/esm/components/CnDuplicateWarning/CnDuplicateWarning.vue.js +1 -1
  29. package/dist/esm/components/CnDuplicateWarning/CnDuplicateWarning.vue2.js +64 -8
  30. package/dist/esm/components/CnDuplicateWarning/CnDuplicateWarning.vue2.js.map +1 -1
  31. package/dist/esm/components/CnDuplicateWarning/CnDuplicateWarning.vue3.js +14 -4
  32. package/dist/esm/components/CnDuplicateWarning/CnDuplicateWarning.vue3.js.map +1 -1
  33. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
  34. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +32 -4
  35. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
  36. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js +10 -2
  37. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
  38. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue.js +1 -1
  39. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js +84 -1
  40. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js.map +1 -1
  41. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js +61 -33
  42. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js.map +1 -1
  43. package/dist/esm/components/CnFlowRunsWidget/CnFlowRunsWidget.vue.js +1 -1
  44. package/dist/esm/components/CnFlowRunsWidget/CnFlowRunsWidget.vue2.js +49 -2
  45. package/dist/esm/components/CnFlowRunsWidget/CnFlowRunsWidget.vue2.js.map +1 -1
  46. package/dist/esm/components/CnFlowRunsWidget/CnFlowRunsWidget.vue3.js +90 -59
  47. package/dist/esm/components/CnFlowRunsWidget/CnFlowRunsWidget.vue3.js.map +1 -1
  48. package/dist/esm/components/CnFollowingWidgetHost/CnFollowingWidgetHost.vue2.js +3 -1
  49. package/dist/esm/components/CnFollowingWidgetHost/CnFollowingWidgetHost.vue2.js.map +1 -1
  50. package/dist/esm/components/CnFollowingWidgetHost/CnFollowingWidgetHost.vue3.js.map +1 -1
  51. package/dist/esm/components/CnNotesCard/CnNotesCard.vue.js +1 -1
  52. package/dist/esm/components/CnNotesCard/CnNotesCard.vue2.js +10 -0
  53. package/dist/esm/components/CnNotesCard/CnNotesCard.vue2.js.map +1 -1
  54. package/dist/esm/components/CnNotesCard/CnNotesCard.vue3.js.map +1 -1
  55. package/dist/esm/components/CnObjectCard/CnObjectCard.vue.js +1 -1
  56. package/dist/esm/components/CnObjectCard/CnObjectCard.vue2.js +11 -0
  57. package/dist/esm/components/CnObjectCard/CnObjectCard.vue2.js.map +1 -1
  58. package/dist/esm/components/CnObjectCard/CnObjectCard.vue3.js +28 -21
  59. package/dist/esm/components/CnObjectCard/CnObjectCard.vue3.js.map +1 -1
  60. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue.js +1 -1
  61. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js +8 -0
  62. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js.map +1 -1
  63. package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js.map +1 -1
  64. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue.js +1 -1
  65. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +1 -1
  66. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
  67. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
  68. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
  69. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +3 -15
  70. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  71. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +123 -115
  72. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  73. package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue.js +1 -1
  74. package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue2.js +4 -0
  75. package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue2.js.map +1 -1
  76. package/dist/esm/components/CnRegisterMapping/CnRegisterMapping.vue3.js.map +1 -1
  77. package/dist/esm/components/CnStepper/CnStepper.vue.js +1 -1
  78. package/dist/esm/components/CnStepper/CnStepper.vue2.js.map +1 -1
  79. package/dist/esm/components/CnStepper/CnStepper.vue3.js.map +1 -1
  80. package/dist/esm/dialogs/CnAccessPurposeDialog.vue.js +1 -1
  81. package/dist/esm/dialogs/CnAccessPurposeDialog.vue2.js +3 -1
  82. package/dist/esm/dialogs/CnAccessPurposeDialog.vue2.js.map +1 -1
  83. package/dist/esm/dialogs/CnAccessPurposeDialog.vue3.js +26 -15
  84. package/dist/esm/dialogs/CnAccessPurposeDialog.vue3.js.map +1 -1
  85. package/dist/esm/l10n/en.json.js +3 -1
  86. package/dist/esm/l10n/en.json.js.map +1 -1
  87. package/dist/esm/l10n/nl.json.js +3 -1
  88. package/dist/esm/l10n/nl.json.js.map +1 -1
  89. package/dist/esm/library-style.js +1 -1
  90. package/dist/esm/nextcloud-vue.css +785 -518
  91. package/dist/esm/utils/builtInFormatters.js +53 -1
  92. package/dist/esm/utils/builtInFormatters.js.map +1 -1
  93. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  94. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  95. package/dist/nextcloud-vue.cjs.js +2688 -2154
  96. package/dist/nextcloud-vue.cjs.js.map +2 -2
  97. package/dist/nextcloud-vue.css +785 -518
  98. package/l10n/en.json +3 -1
  99. package/l10n/nl.json +3 -1
  100. package/package.json +1 -1
  101. package/src/components/CnAuditTrailCard/CnAuditTrailCard.vue +24 -7
  102. package/src/components/CnAuditTrailWidget/CnAuditTrailWidget.vue +13 -1
  103. package/src/components/CnChecklistRunWidget/CnChecklistRunWidget.vue +29 -2
  104. package/src/components/CnDataTable/CnDataTable.vue +34 -1
  105. package/src/components/CnDetailPage/CnDetailPage.md +1 -1
  106. package/src/components/CnDetailPage/CnDetailPage.vue +73 -10
  107. package/src/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue +6 -1
  108. package/src/components/CnDeviceFramesWidget/CnDeviceFramesWidget.vue +1 -0
  109. package/src/components/CnDuplicateWarning/CnDuplicateWarning.vue +74 -10
  110. package/src/components/CnFactsWidget/CnFactsWidget.vue +41 -4
  111. package/src/components/CnFieldListWidget/CnFieldListWidget.vue +131 -3
  112. package/src/components/CnFlowRunsWidget/CnFlowRunsWidget.vue +103 -5
  113. package/src/components/CnFollowingWidgetHost/CnFollowingWidgetHost.vue +3 -1
  114. package/src/components/CnNotesCard/CnNotesCard.vue +10 -0
  115. package/src/components/CnObjectCard/CnObjectCard.vue +16 -0
  116. package/src/components/CnObjectDataWidget/CnObjectDataWidget.md +1 -1
  117. package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +8 -0
  118. package/src/components/CnObjectListWidget/CnObjectListWidget.vue +1 -1
  119. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +19 -19
  120. package/src/components/CnRegisterMapping/CnRegisterMapping.vue +4 -0
  121. package/src/components/CnStepper/CnStepper.vue +10 -2
  122. package/src/css/dialog.css +22 -2
  123. package/src/css/look-board-detail.css +52 -3
  124. package/src/css/look-board-index.css +51 -1
  125. package/src/css/look-board.css +48 -5
  126. package/src/css/table.css +5 -2
  127. package/src/dialogs/CnAccessPurposeDialog.vue +27 -12
  128. package/src/schemas/app-manifest-v2.schema.json +4 -2
  129. package/src/utils/builtInFormatters.js +52 -0
  130. package/src/utils/validateManifestV2.compiled.js +3 -3
@@ -227,6 +227,9 @@
227
227
  <CnIcon v-if="link.icon" :name="link.icon" :size="18" />
228
228
  {{ link.label }}
229
229
  </component>
230
+ <p v-if="link && link.note && record" class="cn-facts-widget__link-note" data-testid="cn-facts-link-note">
231
+ {{ link.note }}
232
+ </p>
230
233
  <button
231
234
  v-if="action && record"
232
235
  type="button"
@@ -262,6 +265,7 @@ import { useRefLabels } from '../../composables/useRefLabels.js'
262
265
  import { dispatchAction } from '../../utils/actionsDispatcher.js'
263
266
  import { isBadgeVariant } from '../../utils/badgeVariants.js'
264
267
  import { formatBoardDate } from '../../utils/boardDate.js'
268
+ import { formatTermDuration } from '../../utils/builtInFormatters.js'
265
269
  import { conditionalLine } from '../../utils/conditionalLine.js'
266
270
  import { parseDateValue } from '../../utils/dateVariant.js'
267
271
  import { fetchListTotal } from '../../utils/fetchFilterCounts.js'
@@ -485,7 +489,7 @@ export default {
485
489
  * - `headerPill` — a status pill in the card header (reported through the `header` event): `{ path?, text?, map?, variant?, variantMap?, colorMap? }`, read from the record like a `pill` entry (DqBezwaar "Advies ontvangen"); nothing shows while the value is empty.
486
490
  * - `notice` — `{ text, variant? }`, a note band inside the card under the facts (`info` by default; `success`, `warning` or `error`); the text is translated.
487
491
  * - `footnote` — a plain muted line under the facts, without a band (DqZaakBesluiten "Na het ondertekenen ..."); the text is translated.
488
- * `format` is `text` (default), `date` (board short date), `date-long` (always with the year), `longdate` (the month written out, the year only outside this year: "3 oktober"), `year` ("2026"), `list` (an array joined by ", ", each item read at `itemPath` and written in `itemFormat`), `group` (a Nextcloud group by its display name), `bytes` (a size, "1.5 MB"), `number` (in the reader's language, `decimals` fraction digits when set), `months` (a number of months in words, whole years as years: 240 is "20 jaar") or `currency` (a money amount in the reader's language, "€ 1.234,50": the code in `currency`, else the record's `currencyField`, else the app's reporting currency, else EUR; `decimals` defaults to 2).
492
+ * `format` is `text` (default), `date` (board short date), `date-long` (always with the year), `longdate` (the month written out, the year only outside this year: "3 oktober"), `duration` (an ISO period in words, "4 weken"; `extendBy` names the extension period on the record: "4 weken, te verdagen met 2"), `year` ("2026"), `list` (an array joined by ", ", each item read at `itemPath` and written in `itemFormat`), `group` (a Nextcloud group by its display name), `bytes` (a size, "1.5 MB"), `number` (in the reader's language, `decimals` fraction digits when set), `months` (a number of months in words, whole years as years: 240 is "20 jaar") or `currency` (a money amount in the reader's language, "€ 1.234,50": the code in `currency`, else the record's `currencyField`, else the app's reporting currency, else EUR; `decimals` defaults to 2).
489
493
  * `map` turns a raw value into a label (translated). `parts` (on a `line`) lists paths, or `{ path, label?, format? }`, joined by `separator` (default " · "); empty parts are dropped.
490
494
  * - `source` — `{ register, schema, id }` or `{ url }`; omitted = the current object. `{ register, schema, filter, sort? }` without `id` reads the first object that matches `filter` in `sort` order (`{ field, dir }`): the latest count of a shift. An entry with `from: "object"` reads the current object even then (a client card that also shows the ticket's channel).
491
495
  * - `endpointSource` — `{ url, method?, params?, responsePath? }`, the block a stat or chart takes: the facts read an app endpoint, shared and cached like those.
@@ -866,18 +870,19 @@ export default {
866
870
  if (!link || typeof link !== 'object' || !link.label) {
867
871
  return null
868
872
  }
873
+ const linkView = this.linkWords(link)
869
874
  if (typeof link.sidebarTab === 'string' && link.sidebarTab !== '') {
870
875
  return this.objectSidebarState
871
- ? { label: this.tr(link.label), sidebarTab: link.sidebarTab, icon: typeof link.icon === 'string' ? link.icon : '' }
876
+ ? { ...linkView, sidebarTab: link.sidebarTab, icon: typeof link.icon === 'string' ? link.icon : '' }
872
877
  : null
873
878
  }
874
879
  if (link.route) {
875
880
  const to = this.resolveRoute(link.route, this.record)
876
- return to ? { label: this.tr(link.label), to } : null
881
+ return to ? { ...linkView, to } : null
877
882
  }
878
883
  if (typeof link.href === 'string' && link.href !== '') {
879
884
  const href = this.linkHref(link, this.record)
880
- return href ? { label: this.tr(link.label), href, target: link.target === '_blank' ? '_blank' : '', icon: typeof link.icon === 'string' ? link.icon : '' } : null
885
+ return href ? { ...linkView, href, target: link.target === '_blank' ? '_blank' : '', icon: typeof link.icon === 'string' ? link.icon : '' } : null
881
886
  }
882
887
  return null
883
888
  },
@@ -1016,6 +1021,23 @@ export default {
1016
1021
  },
1017
1022
 
1018
1023
  methods: {
1024
+ /**
1025
+ * A link's label and the muted note under it, their `{path}` and
1026
+ * `{path:format}` placeholders filled from the record ("{page:last-segment}").
1027
+ *
1028
+ * @param {object} link The configured link.
1029
+ * @return {{label: string, note: string}}
1030
+ * @spec openspec/changes/facts-link-label-from-record/specs/facts-widget/spec.md#requirement-a-facts-link-can-take-its-words-from-the-record
1031
+ */
1032
+ linkWords(link) {
1033
+ const fill = (text) => (text.includes('{') && this.record ? this.templateValue(this.record, text) : this.tr(text))
1034
+ const label = fill(String(link.label))
1035
+ return {
1036
+ label: label || this.tr(String(link.label)),
1037
+ note: typeof link.note === 'string' && link.note !== '' ? fill(link.note) : '',
1038
+ }
1039
+ },
1040
+
1019
1041
  /**
1020
1042
  * Open the page's object sidebar on one of its tabs.
1021
1043
  *
@@ -1893,7 +1915,16 @@ export default {
1893
1915
  const date = parseDateValue(raw)
1894
1916
  return date === null ? '' : String(date.getFullYear())
1895
1917
  }
1918
+ // A term in words, with the extension at `extendBy` on the record: "4 weken, te verdagen met 2".
1919
+ if (format === 'duration') {
1920
+ return formatTermDuration(raw, typeof spec.extendBy === 'string' && record ? readPath(record, spec.extendBy) : null)
1921
+ }
1896
1922
  // The month written out, the year only outside this year: "3 oktober".
1923
+ // One segment of a path ("Team/Page"): its first or its last.
1924
+ if (format === 'first-segment' || format === 'last-segment') {
1925
+ const segments = String(raw).split('/').map((part) => part.trim()).filter((part) => part !== '')
1926
+ return (format === 'first-segment' ? segments[0] : segments[segments.length - 1]) || ''
1927
+ }
1897
1928
  if (format === 'longdate') {
1898
1929
  return formatLongDate(raw)
1899
1930
  }
@@ -2310,6 +2341,12 @@ export default {
2310
2341
  text-decoration: underline;
2311
2342
  }
2312
2343
 
2344
+ .cn-facts-widget__link-note {
2345
+ margin: 0;
2346
+ color: var(--color-text-maxcontrast);
2347
+ font-size: 13px;
2348
+ }
2349
+
2313
2350
  .cn-facts-widget__link--button {
2314
2351
  display: inline-flex;
2315
2352
  align-items: center;
@@ -165,13 +165,28 @@
165
165
  </NcButton>
166
166
  </div>
167
167
  </div>
168
- <ul v-else class="cn-field-list-widget__rows">
168
+ <ul v-else class="cn-field-list-widget__rows" :class="{ 'cn-field-list-widget__rows--options': isOptions }">
169
169
  <li
170
170
  v-for="row in rows"
171
171
  :key="row.key"
172
172
  class="cn-field-list-widget__row"
173
- :data-testid="`cn-field-list-row-${row.key}`">
173
+ :draggable="canReorder ? 'true' : undefined"
174
+ :data-testid="`cn-field-list-row-${row.key}`"
175
+ @dragstart="dragFrom = row.index"
176
+ @dragover.prevent
177
+ @drop.prevent="dropOn(row.index)">
174
178
  <div class="cn-field-list-widget__line">
179
+ <!-- `variant: "options"`: a grip that moves the option, by drag or with the arrow keys. -->
180
+ <button
181
+ v-if="canReorder"
182
+ type="button"
183
+ class="cn-field-list-widget__grip"
184
+ :aria-label="moveLabel(row)"
185
+ :data-testid="`cn-field-list-grip-${row.key}`"
186
+ @keydown.up.prevent="move(row.index, -1)"
187
+ @keydown.down.prevent="move(row.index, 1)">
188
+ <DragVertical :size="16" />
189
+ </button>
175
190
  <span v-if="row.initials" class="cn-field-list-widget__avatar" aria-hidden="true">{{ row.initials }}</span>
176
191
  <span class="cn-field-list-widget__main">
177
192
  <a
@@ -187,6 +202,7 @@
187
202
  :label="row.pill.label"
188
203
  :variant="row.pill.variant"
189
204
  size="small" />
205
+ <span v-if="row.code" class="cn-field-list-widget__code" :data-testid="`cn-field-list-code-${row.key}`">{{ row.code }}</span>
190
206
  <span v-if="row.aside" class="cn-field-list-widget__aside">{{ row.aside }}</span>
191
207
  <NcButton
192
208
  v-for="action in row.actions"
@@ -307,7 +323,9 @@
307
323
  </template>
308
324
 
309
325
  <script>
326
+ import { translate as t } from '@nextcloud/l10n'
310
327
  import { NcActionButton, NcActions, NcButton, NcCheckboxRadioSwitch, NcNoteCard, NcSelect, NcTextField } from '@nextcloud/vue'
328
+ import DragVertical from 'vue-material-design-icons/DragVertical.vue'
311
329
  import CnStatusBadge from '../CnStatusBadge/CnStatusBadge.vue'
312
330
  import { isBadgeVariant } from '../../utils/badgeVariants.js'
313
331
  import { formatBoardDate } from '../../utils/boardDate.js'
@@ -368,7 +386,7 @@ function rowHref(entry, spec) {
368
386
  export default {
369
387
  name: 'CnFieldListWidget',
370
388
 
371
- components: { CnStatusBadge, NcActionButton, NcActions, NcButton, NcCheckboxRadioSwitch, NcNoteCard, NcSelect, NcTextField },
389
+ components: { CnStatusBadge, DragVertical, NcActionButton, NcActions, NcButton, NcCheckboxRadioSwitch, NcNoteCard, NcSelect, NcTextField },
372
390
 
373
391
  inject: {
374
392
  cnTranslate: { default: () => (key) => key },
@@ -388,6 +406,7 @@ export default {
388
406
  * - `rowActions` — `[{ id, label, ariaLabel?, when?, placement?: 'band'|'inline', request: { method?, url, payload? } }]`, or `{ id, label, dialog, props }` to open a modal registered in the app registry with filled props (shown in `readOnly` too, since it writes nothing, and only when every token in `props` fills); tokens `@object.<field>`, `@objectId`, `@row.<field>`, `@index`. In the table variant `placement: 'inline'` draws the action as a button in the row instead of in the "..." menu; `ariaLabel` is a template filled from the row.
389
407
  * - `when` (row actions, `detail`) — `{ field: value }`, every field equal; or per field `{ eq?, neq?, set? }`, where a value `@row.<field>` (or `@<field>`) is another field of the same row: `{ deliveredRef: { set: true, neq: '@originalRef' } }`.
390
408
  * - `rowSource` — `{ url, path?, key }`: live per-row state merged by key.
409
+ * - `variant: 'options'` — the rows as framed options (a field's choice list): a grip that moves the option by drag or with the arrow keys and saves the order, and a code chip from `code` (`{ path }` on an object option, or `{ field }`, a map on the record from a plain option to its code).
391
410
  * - `variant: 'table'` + `columns` — `[{ header, path, map?, format?, strong?, pill?, variantMap?, labelsFrom? }]` (a list value reads as its items joined; `labelsFrom` names an add field whose option labels word the values): rows as a table; a per-row "..." menu holds the row actions and remove; `detail` lines become a warning band under the table, with the row actions that have `placement: 'band'`.
392
411
  * - `add.trigger: 'button'`, `add.placement: 'header' | 'title' | 'footer'` — the add form opens from a button; `title` reports the button for the card's title row on the `header` event.
393
412
  * - `readOnly`, `hint`, `emptyText`.
@@ -436,6 +455,8 @@ export default {
436
455
  return {
437
456
  /** The list as last saved by this widget; null = read it off objectData. */
438
457
  saved: null,
458
+ /** The place of the option being dragged, -1 while none is. */
459
+ dragFrom: -1,
439
460
  /** Live per-row state from `rowSource`, by key. */
440
461
  live: {},
441
462
  draft: {},
@@ -500,10 +521,21 @@ export default {
500
521
  copies: spec.detail && this.matches(entry, spec.detail.when) ? this.copiesOf(entry, spec.detail.copy) : [],
501
522
  actions: this.actionsFor(entry, index),
502
523
  cells: this.isTable ? this.cellsOf(entry) : [],
524
+ code: this.isOptions ? this.codeOf(item, entry) : '',
503
525
  }
504
526
  })
505
527
  },
506
528
 
529
+ /** Whether the rows are drawn as framed options (`variant: 'options'`): a grip, the name, a code chip. */
530
+ isOptions() {
531
+ return this.content.variant === 'options'
532
+ },
533
+
534
+ /** Whether the options can be moved: the options look on a list that saves. */
535
+ canReorder() {
536
+ return this.isOptions && !this.readOnly && Boolean(this.content.field)
537
+ },
538
+
507
539
  /** Whether the list is read-only. */
508
540
  readOnly() {
509
541
  return this.content.readOnly === true
@@ -1300,6 +1332,73 @@ export default {
1300
1332
  * @param {object} row The row.
1301
1333
  * @return {Promise<void>}
1302
1334
  */
1335
+ /**
1336
+ * An option's code chip: `content.code.path` on an object option, or the
1337
+ * value's entry in the record's map at `content.code.field` for a plain
1338
+ * one ("Woning" -> "WON"); '' for none.
1339
+ *
1340
+ * @param {unknown} item The stored option.
1341
+ * @param {object} entry The option as a row reads it.
1342
+ * @return {string}
1343
+ */
1344
+ codeOf(item, entry) {
1345
+ const spec = this.content.code
1346
+ if (!spec || typeof spec !== 'object') {
1347
+ return ''
1348
+ }
1349
+ if (item && typeof item === 'object' && typeof spec.path === 'string') {
1350
+ return String(readPath(item, spec.path) ?? '')
1351
+ }
1352
+ const map = typeof spec.field === 'string' ? readPath(this.objectData || {}, spec.field) : null
1353
+ const code = map && typeof map === 'object' ? map[String(entry.value ?? item)] : ''
1354
+ return typeof code === 'string' ? code : ''
1355
+ },
1356
+
1357
+ /**
1358
+ * The grip's accessible name.
1359
+ *
1360
+ * @param {object} row The row.
1361
+ * @return {string}
1362
+ */
1363
+ moveLabel(row) {
1364
+ return t('nextcloud-vue', 'Move {item}, use the arrow keys', { item: row.title })
1365
+ },
1366
+
1367
+ /**
1368
+ * Drop the dragged option on this one's place.
1369
+ *
1370
+ * @param {number} to The place it lands on.
1371
+ * @return {Promise<void>}
1372
+ */
1373
+ async dropOn(to) {
1374
+ const from = this.dragFrom
1375
+ this.dragFrom = -1
1376
+ if (from < 0 || from === to) {
1377
+ return
1378
+ }
1379
+ await this.move(from, to - from)
1380
+ },
1381
+
1382
+ /**
1383
+ * Move an option by a number of places and save the list.
1384
+ *
1385
+ * @param {number} from Its place.
1386
+ * @param {number} by How far, negative is up.
1387
+ * @return {Promise<void>}
1388
+ */
1389
+ async move(from, by) {
1390
+ const to = from + by
1391
+ if (!this.canReorder || to < 0 || to >= this.items.length) {
1392
+ return
1393
+ }
1394
+ await this.save((stored) => {
1395
+ const list = [...stored]
1396
+ const [moved] = list.splice(from, 1)
1397
+ list.splice(to, 0, moved)
1398
+ return list
1399
+ }, this.keyOf(this.items[from], from))
1400
+ },
1401
+
1303
1402
  async remove(row) {
1304
1403
  await this.save((stored) => stored.filter((item, index) => this.keyOf(item, index) !== row.key), row.key)
1305
1404
  },
@@ -1409,6 +1508,35 @@ export default {
1409
1508
  padding-top: 0;
1410
1509
  }
1411
1510
 
1511
+ /* `variant: "options"` (DqZaaktypeVelden Keuzelijst): each option a framed
1512
+ row, 6px apart, with a grip before its name and its code in a chip. */
1513
+ .cn-field-list-widget__rows--options {
1514
+ gap: 6px;
1515
+ }
1516
+
1517
+ .cn-field-list-widget__rows--options .cn-field-list-widget__row,
1518
+ .cn-field-list-widget__rows--options .cn-field-list-widget__row:first-child {
1519
+ padding: 6px 12px;
1520
+ border: 1px solid var(--color-border);
1521
+ border-radius: var(--border-radius-element, 8px);
1522
+ }
1523
+
1524
+ .cn-field-list-widget__grip {
1525
+ flex: none;
1526
+ display: inline-flex;
1527
+ padding: 0;
1528
+ border: 0;
1529
+ background: transparent;
1530
+ color: var(--color-text-maxcontrast);
1531
+ cursor: grab;
1532
+ }
1533
+
1534
+ .cn-field-list-widget__code {
1535
+ flex: none;
1536
+ color: var(--color-text-maxcontrast);
1537
+ font-size: 13px;
1538
+ }
1539
+
1412
1540
  .cn-field-list-widget__line {
1413
1541
  display: flex;
1414
1542
  align-items: center;
@@ -3,7 +3,7 @@
3
3
  - SPDX-License-Identifier: EUPL-1.2
4
4
  -->
5
5
  <template>
6
- <div class="cn-flow-runs-widget">
6
+ <div class="cn-flow-runs-widget" :class="{ 'cn-flow-runs-widget--pills': pills }">
7
7
  <!-- One quiet line on failure, never a leaked axios status string
8
8
  (ADR-062). The real error goes to the console. -->
9
9
  <p v-if="error" class="cn-flow-runs-widget__error">
@@ -26,7 +26,7 @@
26
26
  {{ emptyLabel }}
27
27
  </p>
28
28
  <template v-else>
29
- <p v-if="rows.length === 0" class="cn-flow-runs-widget__empty">
29
+ <p v-if="rows.length === 0 && content.quietLineWithHistory !== false" class="cn-flow-runs-widget__empty">
30
30
  {{ emptyLabel }}
31
31
  </p>
32
32
  <ul v-else class="cn-flow-runs-widget__list">
@@ -42,6 +42,7 @@
42
42
  :href="runHref(run) || undefined"
43
43
  @click="onRowClick($event, run)">
44
44
  <span
45
+ v-if="!pills"
45
46
  class="cn-flow-runs-widget__dot"
46
47
  :class="`cn-flow-runs-widget__dot--${run.status}`"
47
48
  :title="statusLabel(run.status)" />
@@ -49,6 +50,12 @@
49
50
  <span class="cn-flow-runs-widget__name">{{ run.flowName }}</span>
50
51
  <span class="cn-flow-runs-widget__meta">{{ metaLine(run) }}</span>
51
52
  </span>
53
+ <CnStatusBadge
54
+ v-if="pills"
55
+ class="cn-flow-runs-widget__pill"
56
+ :label="statusLabel(run.status)"
57
+ :variant="statusVariant(run.status)"
58
+ size="small" />
52
59
  <span class="cn-flow-runs-widget__age">{{ ageLabel(run) }}</span>
53
60
  </component>
54
61
  </li>
@@ -62,7 +69,7 @@
62
69
  labelled section with a hollow dot and a muted name, so live
63
70
  and finished never rely on colour alone to tell apart. -->
64
71
  <template v-if="showHistory">
65
- <p class="cn-flow-runs-widget__history-title">
72
+ <p v-if="content.historyTitle !== false" class="cn-flow-runs-widget__history-title">
66
73
  {{ tr('Earlier runs') }}
67
74
  </p>
68
75
  <p v-if="completedError" class="cn-flow-runs-widget__error">
@@ -81,6 +88,7 @@
81
88
  :href="runHref(run) || undefined"
82
89
  @click="onRowClick($event, run)">
83
90
  <span
91
+ v-if="!pills"
84
92
  class="cn-flow-runs-widget__dot cn-flow-runs-widget__dot--terminal"
85
93
  :class="`cn-flow-runs-widget__dot--${run.status}`"
86
94
  :title="statusLabel(run.status)" />
@@ -88,6 +96,12 @@
88
96
  <span class="cn-flow-runs-widget__name">{{ run.flowName }}</span>
89
97
  <span class="cn-flow-runs-widget__meta">{{ metaLine(run) }}</span>
90
98
  </span>
99
+ <CnStatusBadge
100
+ v-if="pills"
101
+ class="cn-flow-runs-widget__pill"
102
+ :label="statusLabel(run.status)"
103
+ :variant="statusVariant(run.status)"
104
+ size="small" />
91
105
  <span class="cn-flow-runs-widget__age">{{ ageLabel(run) }}</span>
92
106
  </component>
93
107
  </li>
@@ -106,9 +120,11 @@ import { NcLoadingIcon } from '@nextcloud/vue'
106
120
  import { computed, inject, ref } from 'vue'
107
121
  import { useEndpointSource } from '../../composables/useEndpointSource.js'
108
122
  import { dispatchAction } from '../../utils/actionsDispatcher.js'
123
+ import { formatBoardDate } from '../../utils/boardDate.js'
109
124
  import { resolveObjectTokenContext } from '../../utils/detailObjectContext.js'
110
125
  import { followLinkClick, resolveHref } from '../../utils/linkNavigation.js'
111
126
  import { resolveDeepTokens, resolveFilterValue } from '../../utils/resolveFilterTokens.js'
127
+ import { CnStatusBadge } from '../CnStatusBadge/index.js'
112
128
 
113
129
  /**
114
130
  * The OpenRegister endpoint that lists the caller's live flow runs.
@@ -197,6 +213,7 @@ export default {
197
213
  name: 'CnFlowRunsWidget',
198
214
 
199
215
  components: {
216
+ CnStatusBadge,
200
217
  NcLoadingIcon,
201
218
  },
202
219
 
@@ -231,6 +248,17 @@ export default {
231
248
  * subject's finished runs render below it. Absent = today's org-wide
232
249
  * widget, unchanged.
233
250
  * - `emptyText` — override for the "nothing running" line.
251
+ * - `quietLineWithHistory` — `false` leaves out the "nothing running"
252
+ * line while the subject has finished runs to list.
253
+ * - `historyTitle` — `false` leaves out the "Earlier runs" heading.
254
+ * - `statusPill` — `true` shows the status as a pill on each row
255
+ * (no dot, and the meta line leaves the status out), rows ruled.
256
+ * - `statusLabels` — words per status (`{ completed: 'Succeeded' }`),
257
+ * translated, over the built-in ones.
258
+ * - `triggerLabels` — the meta line per trigger in words
259
+ * (`{ 'object.created': 'Started when the case was created' }`), translated.
260
+ * - `age` — `'datetime'` shows when the run started ("3 okt, 14:32")
261
+ * instead of how long ago ("13d").
234
262
  * - `headerAction` — an action descriptor drawn in the card header
235
263
  * (`{ label, icon?, ...action }`, "Flow uitvoeren"), reported through
236
264
  * the `header` event and dispatched with the page's dispatcher; its
@@ -421,6 +449,11 @@ export default {
421
449
  return this.tr('+{count} more').replace('{count}', String(this.hiddenCount))
422
450
  },
423
451
 
452
+ /** Whether each row shows its status as a pill (`content.statusPill`). */
453
+ pills() {
454
+ return this.content.statusPill === true
455
+ },
456
+
424
457
  /** The nothing-running line. */
425
458
  emptyLabel() {
426
459
  const override = this.content.emptyText
@@ -678,10 +711,29 @@ export default {
678
711
  * @return {string} The label.
679
712
  */
680
713
  statusLabel(status) {
681
- const key = STATUS_LABELS[status]
714
+ const own = this.content.statusLabels && this.content.statusLabels[status]
715
+ const key = (typeof own === 'string' && own !== '') ? own : STATUS_LABELS[status]
682
716
  return key ? this.tr(key) : String(status || '')
683
717
  },
684
718
 
719
+ /**
720
+ * The pill tone of a status: finished well is success, failed is error,
721
+ * anything still going is info.
722
+ *
723
+ * @param {string} status The run status.
724
+ * @return {string} A CnStatusBadge variant.
725
+ * @spec openspec/changes/flow-runs-ruled-with-pills/specs/cn-flow-runs-widget/spec.md
726
+ */
727
+ statusVariant(status) {
728
+ if (status === 'completed') {
729
+ return 'success'
730
+ }
731
+ if (status === 'failed' || status === 'dead_letter') {
732
+ return 'error'
733
+ }
734
+ return status === 'stopped' ? 'default' : 'info'
735
+ },
736
+
685
737
  /**
686
738
  * The secondary line: status, the step the run sits on, and what
687
739
  * triggered it — whichever of those the run actually carries.
@@ -690,7 +742,11 @@ export default {
690
742
  * @return {string} The meta line.
691
743
  */
692
744
  metaLine(run) {
693
- const parts = [this.statusLabel(run.status)]
745
+ const words = this.content.triggerLabels && this.content.triggerLabels[run.trigger]
746
+ if (typeof words === 'string' && words !== '') {
747
+ return this.tr(words)
748
+ }
749
+ const parts = this.pills ? [] : [this.statusLabel(run.status)]
694
750
  if (run.step) {
695
751
  parts.push(String(run.step))
696
752
  }
@@ -714,6 +770,11 @@ export default {
714
770
  if (Number.isFinite(started) === false) {
715
771
  return ''
716
772
  }
773
+ if (this.content.age === 'datetime') {
774
+ const at = new Date(started)
775
+ const pad = (value) => String(value).padStart(2, '0')
776
+ return `${formatBoardDate(at)}, ${pad(at.getHours())}:${pad(at.getMinutes())}`
777
+ }
717
778
  const seconds = Math.max(0, Math.round((Date.now() - started) / 1000))
718
779
  if (seconds < 60) {
719
780
  return this.tr('now')
@@ -965,6 +1026,43 @@ a.cn-flow-runs-widget__row-content:hover .cn-flow-runs-widget__name {
965
1026
  letter-spacing: 0.04em;
966
1027
  }
967
1028
 
1029
+ /* Status pills: each row ruled on top, the name over its meta line, the pill
1030
+ and the start moment at the end, as the Dq case boards draw the Flows card. */
1031
+ .cn-flow-runs-widget--pills .cn-flow-runs-widget__row {
1032
+ border-top: 1px solid var(--color-border);
1033
+ border-bottom: none;
1034
+ }
1035
+
1036
+ .cn-flow-runs-widget--pills .cn-flow-runs-widget__row-content {
1037
+ flex-wrap: wrap;
1038
+ gap: 6px 16px;
1039
+ padding: 11px 0;
1040
+ }
1041
+
1042
+ .cn-flow-runs-widget--pills .cn-flow-runs-widget__body {
1043
+ flex: 1 1 260px;
1044
+ gap: 2px;
1045
+ }
1046
+
1047
+ .cn-flow-runs-widget--pills .cn-flow-runs-widget__name,
1048
+ .cn-flow-runs-widget--pills .cn-flow-runs-widget__row--terminal .cn-flow-runs-widget__name {
1049
+ font-size: 15px;
1050
+ font-weight: 500;
1051
+ color: var(--color-main-text);
1052
+ white-space: normal;
1053
+ }
1054
+
1055
+ .cn-flow-runs-widget--pills .cn-flow-runs-widget__meta {
1056
+ font-size: 13px;
1057
+ white-space: normal;
1058
+ }
1059
+
1060
+ .cn-flow-runs-widget--pills .cn-flow-runs-widget__age {
1061
+ flex: 0 0 104px;
1062
+ font-size: 13px;
1063
+ text-align: end;
1064
+ }
1065
+
968
1066
  @keyframes cn-flow-runs-pulse {
969
1067
  0%, 100% { opacity: 1; }
970
1068
  50% { opacity: 0.35; }
@@ -42,6 +42,8 @@ export default {
42
42
  cnWidgetSelection: { from: WIDGET_SELECTION, default: null },
43
43
  /** The host's translate (CnAppRoot provides `cnTranslate`), applied to the title before it is filled. */
44
44
  cnTranslate: { default: () => (key) => key },
45
+ /** The app's named formatters (CnAppRoot provides `cnFormatters`), for a `{field|formatter}` token in the title. */
46
+ cnFormatters: { default: () => ({}) },
45
47
  },
46
48
 
47
49
  inheritAttrs: false,
@@ -93,7 +95,7 @@ export default {
93
95
  return this.widget
94
96
  }
95
97
  const translated = this.cnTranslate(title)
96
- return { ...this.widget, title: fillTemplate(translated, this.row) || translated.replace(/\s*\{[^}]*\}/g, '') }
98
+ return { ...this.widget, title: fillTemplate(translated, this.row, new Date(), { formatters: this.cnFormatters }) || translated.replace(/\s*\{[^}]*\}/g, '') }
97
99
  },
98
100
  },
99
101
  }
@@ -176,6 +176,8 @@ import CnNoteComposer from '../CnNoteComposer/CnNoteComposer.vue'
176
176
  import CnUserActionMenu from '../CnUserActionMenu/CnUserActionMenu.vue'
177
177
  import CnVisibilityChip from '../CnVisibilityChip/CnVisibilityChip.vue'
178
178
  import { useFileComments } from '../../composables/useFileComments.js'
179
+ import { normalizeLook } from '../../composables/useLook.js'
180
+ import { formatBoardDateTime } from '../../utils/fillTemplate.js'
179
181
  import { buildHeaders, prefixUrl } from '../../utils/index.js'
180
182
  import { initialsOf } from '../../utils/initials.js'
181
183
  import { leafSourceUrl } from '../../utils/leafSourceUrl.js'
@@ -248,6 +250,10 @@ export default {
248
250
  Delete,
249
251
  },
250
252
 
253
+ inject: {
254
+ cnLook: { default: 'nextcloud' },
255
+ },
256
+
251
257
  props: {
252
258
  /** OpenRegister register ID */
253
259
  registerId: {
@@ -795,6 +801,10 @@ export default {
795
801
  if (!dateStr) {
796
802
  return ''
797
803
  }
804
+ // The board look writes a note's moment as the boards do: "6 okt 11:10", the year only outside this year.
805
+ if (normalizeLook(this.cnLook) === 'board') {
806
+ return formatBoardDateTime(dateStr) || dateStr
807
+ }
798
808
  try {
799
809
  return new Date(dateStr).toLocaleString(userLocale(), {
800
810
  year: 'numeric',
@@ -112,6 +112,11 @@
112
112
  :row="object" />
113
113
  <template v-else>{{ recordFootCell.text }}</template>
114
114
  </span>
115
+ <!-- An empty footerCell keeps its place as a dash, as an empty table cell. -->
116
+ <span
117
+ v-else-if="recordFootCellDeclared"
118
+ class="cn-object-card__record-foot-cell cn-object-card__record-foot-cell--empty"
119
+ data-testid="cn-object-card-foot-cell-empty">—</span>
115
120
  <span
116
121
  v-if="recordDate"
117
122
  class="cn-object-card__record-date"
@@ -589,6 +594,17 @@ export default {
589
594
  return { text: part.text, tone }
590
595
  },
591
596
 
597
+ /**
598
+ * Whether the record layout names a `footerCell`, so an empty one
599
+ * keeps its place.
600
+ *
601
+ * @spec openspec/changes/card-empty-footer-cell-dash/specs/card-empty-footer-cell-dash/spec.md#requirement-an-empty-footer-cell-reads-as-a-dash
602
+ * @return {boolean}
603
+ */
604
+ recordFootCellDeclared() {
605
+ return Boolean(this.recordLayout && this.recordLayout.footerCell)
606
+ },
607
+
592
608
  /**
593
609
  * The footer's `footerCell` part, read as a meta part is (through the
594
610
  * page column it names), or null while it is empty.
@@ -121,7 +121,7 @@ export default {
121
121
  | `icon` | Object\|Function | `null` | MDI icon component shown in the card header |
122
122
  | `objectType` | String | `''` | Registered object type slug used by `objectStore.saveObject()` |
123
123
  | `store` | Object | `null` | Explicit Pinia objectStore instance; auto-detected via Pinia when omitted |
124
- | `overrides` | Object | `{}` | Per-property config: `{ key: { order, gridColumn, gridRow, hidden, editable, label, widget, visibleWhen } }`. `visibleWhen` shows the field only when the shared visibility predicate holds for this object, in its local mode: `{ field, op, value }` (ops `eq`, `neq`, `in`, `empty`, ...), or `all` / `any` of those. It reads unsaved changes too. A rule that needs a request (`endpoint`, `source`) is ignored. Read grid only: the Edit form still offers the field. |
124
+ | `overrides` | Object | `{}` | Per-property config: `{ key: { order, gridColumn, gridRow, hidden, editable, label, widget, visibleWhen } }`. `visibleWhen` shows the field only when the shared visibility predicate holds for this object, in its local mode: `{ field, op, value }` (ops `eq`, `neq`, `in`, `empty`, ...), or `all` / `any` of those. It reads unsaved changes too. A rule that needs a request (`endpoint`, `source`) is ignored. Read grid only: the Edit form still offers the field. `format: 'duration'` reads an ISO period in words (`P28D` "4 weken"), and `extendBy` names the object's extension period, read as "4 weken, te verdagen met 2". |
125
125
  | `columns` | Number | `3` | Number of CSS grid columns in the widget |
126
126
  | `collapsedFields` (`collapsed-fields`) | Number | `null` | Number of fields shown while collapsed, with a "Show all N fields" button for the rest. `null` fits the cell: the whole rows that fit stay, and the button only appears when the cell overflows |
127
127
  | `hideEmpty` | Boolean | `false` | Hide fields with no value instead of rendering an em dash. For discriminated supertypes (one `ticket` schema holding request/complaint/contactmoment), so the grid is type-aware without the schema enumerating which fields belong to which variant. Read grid only — the field being edited, a field with unsaved changes, and the Edit form stay visible; `false`/`0` are values and are never hidden. |
@@ -438,10 +438,12 @@ import CnGroupNameCell from '../CnCellRenderer/CnGroupNameCell.vue'
438
438
  import CnFormDialog from '../CnFormDialog/CnFormDialog.vue'
439
439
  import { normalizeLook } from '../../composables/useLook.js'
440
440
  import { useObjectStore } from '../../store/index.js'
441
+ import { formatTermDuration } from '../../utils/builtInFormatters.js'
441
442
  import { foldDottedFields, isDottedKey, spreadDottedFields } from '../../utils/dottedFields.js'
442
443
  import { groupDisplayName, loadGroupDisplayName, searchNextcloudGroups } from '../../utils/groupAutocomplete.js'
443
444
  import { humaniseKey } from '../../utils/humaniseKey.js'
444
445
  import { PANEL_ACTION_SINK } from '../../utils/panelActions.js'
446
+ import { readPath } from '../../utils/readPath.js'
445
447
  import { resolveFilterTokens } from '../../utils/resolveFilterTokens.js'
446
448
  import { saveEdited } from '../../utils/saveEdited.js'
447
449
  import { fieldsFromSchema, formatValue } from '../../utils/schema.js'
@@ -1103,6 +1105,12 @@ export default {
1103
1105
  // label, beside a list that already read "Ingediend".
1104
1106
  const enumLabel = this.enumDisplayLabel(field, raw)
1105
1107
  const empty = this.emptyValueFor(field, raw)
1108
+ if (empty === null && field.format === 'duration' && raw !== null && raw !== undefined && raw !== '') {
1109
+ // A term in words, with the extension at `extendBy` when the object holds one.
1110
+ const extension = typeof field.extendBy === 'string' ? readPath(this.objectData || {}, field.extendBy) : null
1111
+ values[field.key] = formatTermDuration(raw, extension)
1112
+ continue
1113
+ }
1106
1114
  values[field.key] = empty !== null ? empty : (enumLabel !== null ? enumLabel : formatValue(raw, prop || {}))
1107
1115
  }
1108
1116
  return values
@@ -860,7 +860,7 @@ export default {
860
860
  // `secondary` (a second, muted line under the cell) rides
861
861
  // along too: the table draws it, and dropping it here took the
862
862
  // list rows' second line away (zuiddrecht-pixel-gaps-3).
863
- for (const k of ['format', 'widget', 'widgetProps', 'formatter', 'align', 'width', 'type', 'enum', 'enumLabels', 'sortable', 'secondary', 'colorMap', 'grow', 'cellClass']) {
863
+ for (const k of ['format', 'widget', 'widgetProps', 'formatter', 'align', 'width', 'type', 'enum', 'enumLabels', 'sortable', 'secondary', 'colorMap', 'grow', 'cellClass', 'emptyValue']) {
864
864
  if (c[k] !== undefined) {
865
865
  out[k] = c[k]
866
866
  }