@conduction/nextcloud-vue 2.115.0 → 2.116.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 (117) hide show
  1. package/dist/esm/components/CnActionButtons/CnActionButtons.vue.js +1 -1
  2. package/dist/esm/components/CnActionButtons/CnActionButtons.vue2.js +92 -0
  3. package/dist/esm/components/CnActionButtons/CnActionButtons.vue2.js.map +1 -1
  4. package/dist/esm/components/CnActionButtons/CnActionButtons.vue3.js +2 -2
  5. package/dist/esm/components/CnActionButtons/CnActionButtons.vue3.js.map +1 -1
  6. package/dist/esm/components/CnChecklistWidget/CnChecklistWidget.vue.js +9 -0
  7. package/dist/esm/components/CnChecklistWidget/CnChecklistWidget.vue.js.map +1 -0
  8. package/dist/esm/components/CnChecklistWidget/CnChecklistWidget.vue2.js +145 -0
  9. package/dist/esm/components/CnChecklistWidget/CnChecklistWidget.vue2.js.map +1 -0
  10. package/dist/esm/components/CnChecklistWidget/CnChecklistWidget.vue3.js +58 -0
  11. package/dist/esm/components/CnChecklistWidget/CnChecklistWidget.vue3.js.map +1 -0
  12. package/dist/esm/components/CnChecklistWidget/index.js +28 -0
  13. package/dist/esm/components/CnChecklistWidget/index.js.map +1 -0
  14. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
  15. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +25 -0
  16. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  17. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +1 -1
  18. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  19. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +376 -51
  20. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  21. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +364 -285
  22. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  23. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue.js +1 -1
  24. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +11 -0
  25. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  26. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +6 -4
  27. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
  28. package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue.js +1 -1
  29. package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue2.js +48 -1
  30. package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue2.js.map +1 -1
  31. package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue3.js +11 -2
  32. package/dist/esm/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue3.js.map +1 -1
  33. package/dist/esm/components/CnMacrosWidget/CnMacrosWidget.vue.js +1 -1
  34. package/dist/esm/components/CnMacrosWidget/CnMacrosWidget.vue2.js +11 -3
  35. package/dist/esm/components/CnMacrosWidget/CnMacrosWidget.vue2.js.map +1 -1
  36. package/dist/esm/components/CnMacrosWidget/CnMacrosWidget.vue3.js +8 -3
  37. package/dist/esm/components/CnMacrosWidget/CnMacrosWidget.vue3.js.map +1 -1
  38. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue.js +1 -1
  39. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +55 -2
  40. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
  41. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
  42. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
  43. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +94 -4
  44. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  45. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +248 -239
  46. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  47. package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue.js +9 -0
  48. package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue.js.map +1 -0
  49. package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue2.js +282 -0
  50. package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue2.js.map +1 -0
  51. package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue3.js +202 -0
  52. package/dist/esm/components/CnRecordListWidget/CnRecordNoteRows.vue3.js.map +1 -0
  53. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
  54. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +10 -5
  55. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  56. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  57. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
  58. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
  59. package/dist/esm/composables/indexSources.js +64 -0
  60. package/dist/esm/composables/indexSources.js.map +1 -1
  61. package/dist/esm/composables/useTaskInboxStore.js +85 -0
  62. package/dist/esm/composables/useTaskInboxStore.js.map +1 -1
  63. package/dist/esm/dialogs/CnAccessPurposeDialog.vue.js +9 -0
  64. package/dist/esm/dialogs/CnAccessPurposeDialog.vue.js.map +1 -0
  65. package/dist/esm/dialogs/CnAccessPurposeDialog.vue2.js +107 -0
  66. package/dist/esm/dialogs/CnAccessPurposeDialog.vue2.js.map +1 -0
  67. package/dist/esm/dialogs/CnAccessPurposeDialog.vue3.js +110 -0
  68. package/dist/esm/dialogs/CnAccessPurposeDialog.vue3.js.map +1 -0
  69. package/dist/esm/index.js +1 -0
  70. package/dist/esm/index.js.map +1 -1
  71. package/dist/esm/l10n/en.json.js +19 -1
  72. package/dist/esm/l10n/en.json.js.map +1 -1
  73. package/dist/esm/l10n/nl.json.js +23 -1
  74. package/dist/esm/l10n/nl.json.js.map +1 -1
  75. package/dist/esm/library-style.js +1 -1
  76. package/dist/esm/nextcloud-vue.css +1054 -911
  77. package/dist/esm/utils/actionsDispatcher.js +10 -5
  78. package/dist/esm/utils/actionsDispatcher.js.map +1 -1
  79. package/dist/esm/utils/detailActionModel.js +6 -2
  80. package/dist/esm/utils/detailActionModel.js.map +1 -1
  81. package/dist/esm/utils/libraryWidgetKeys.js +1 -0
  82. package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
  83. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  84. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  85. package/dist/esm/utils/widgetDispatch.js +1 -1
  86. package/dist/esm/utils/widgetDispatch.js.map +1 -1
  87. package/dist/nextcloud-vue.cjs.js +17078 -15224
  88. package/dist/nextcloud-vue.cjs.js.map +2 -2
  89. package/dist/nextcloud-vue.css +1054 -911
  90. package/l10n/en.json +19 -1
  91. package/l10n/nl.json +23 -1
  92. package/package.json +1 -1
  93. package/src/components/CnActionButtons/CnActionButtons.vue +93 -1
  94. package/src/components/CnChecklistWidget/CnChecklistWidget.vue +206 -0
  95. package/src/components/CnChecklistWidget/index.js +27 -0
  96. package/src/components/CnDashboardPage/CnDashboardPage.vue +26 -1
  97. package/src/components/CnDetailPage/CnDetailPage.md +2 -0
  98. package/src/components/CnDetailPage/CnDetailPage.vue +377 -10
  99. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +15 -2
  100. package/src/components/CnGroupedCardsWidget/CnGroupedCardsWidget.vue +59 -1
  101. package/src/components/CnMacrosWidget/CnMacrosWidget.vue +16 -6
  102. package/src/components/CnObjectListWidget/CnObjectListWidget.vue +55 -2
  103. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +101 -3
  104. package/src/components/CnRecordListWidget/CnRecordNoteRows.vue +508 -0
  105. package/src/components/CnTabsWidget/CnTabsWidget.vue +10 -5
  106. package/src/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
  107. package/src/components/index.js +1 -0
  108. package/src/composables/indexSources.js +64 -0
  109. package/src/composables/useTaskInboxStore.js +85 -0
  110. package/src/dialogs/CnAccessPurposeDialog.vue +183 -0
  111. package/src/index.js +1 -0
  112. package/src/schemas/app-manifest-v2.schema.json +49 -3
  113. package/src/utils/actionsDispatcher.js +10 -5
  114. package/src/utils/detailActionModel.js +6 -2
  115. package/src/utils/libraryWidgetKeys.js +1 -0
  116. package/src/utils/validateManifestV2.compiled.js +3 -3
  117. package/src/utils/widgetDispatch.js +1 -1
@@ -43,6 +43,13 @@
43
43
  <p v-else class="cn-grouped-cards__group-empty">
44
44
  {{ groupEmptyLabel }}
45
45
  </p>
46
+ <!-- `groupAction`: one action per group ("Add a task" on a phase),
47
+ its `@group.<path>` tokens filled from the group. -->
48
+ <CnActionButtons
49
+ v-if="group.action"
50
+ class="cn-grouped-cards__group-action"
51
+ :actions="[group.action]"
52
+ data-testid="cn-grouped-cards-group-action" />
46
53
  </section>
47
54
  <section
48
55
  v-if="extra"
@@ -71,6 +78,7 @@ import { fillTemplate } from '../../utils/fillTemplate.js'
71
78
  import { unwrapAppConfig } from '../../utils/formatMetric.js'
72
79
  import { languageMapText } from '../../utils/languageMapText.js'
73
80
  import { readPath } from '../../utils/readPath.js'
81
+ import { CnActionButtons } from '../CnActionButtons/index.js'
74
82
 
75
83
  let uid = 0
76
84
 
@@ -121,7 +129,7 @@ function readable(row) {
121
129
  export default {
122
130
  name: 'CnGroupedCardsWidget',
123
131
 
124
- components: { NcLoadingIcon },
132
+ components: { CnActionButtons, NcLoadingIcon },
125
133
 
126
134
  inject: {
127
135
  /** The host app's translate function. */
@@ -147,6 +155,11 @@ export default {
147
155
  * - `extraGroup` (`{ title, path, itemText, join? }`) closes the grid with a
148
156
  * dashed box whose entries, read at `path` of the payload, are one line.
149
157
  * - `columns` (default 2) is the number of groups side by side.
158
+ * - `groupAction` is an action drawn at the foot of every group (as
159
+ * CnActionButtons draws it), with `@group.<path>` in its strings read
160
+ * from that group: `{ type: "open-form", endpoint: { url:
161
+ * "/apps/x/api/phases/@group.id/steps", method: "POST" }, ... }`
162
+ * adds a card to the group.
150
163
  *
151
164
  * A language map (`{ nl, en }`) reads in the user's language, and a list
152
165
  * of plain values reads joined with ", ". Texts go through the app's
@@ -231,6 +244,7 @@ export default {
231
244
  title: this.fill(c.groupTitle || '{name}', group),
232
245
  count: this.countOf(items.length),
233
246
  items,
247
+ action: this.groupActionFor(raw, index),
234
248
  }
235
249
  })
236
250
  },
@@ -285,6 +299,46 @@ export default {
285
299
  },
286
300
 
287
301
  methods: {
302
+ /**
303
+ * The `groupAction` for one group, with every `@group.<path>` token in
304
+ * its strings read from that group, and its id made unique per group.
305
+ * Null without a `groupAction`, or for a group missing a value a token
306
+ * names (the action would address no group).
307
+ *
308
+ * @param {object} raw The group as read.
309
+ * @param {number} index The group's position.
310
+ * @return {object|null} The action.
311
+ * @spec openspec/changes/grouped-cards-group-action/specs/grouped-cards-group-action/spec.md#requirement-every-group-can-carry-an-action
312
+ */
313
+ groupActionFor(raw, index) {
314
+ const action = this.content?.groupAction
315
+ if (!action || typeof action !== 'object') {
316
+ return null
317
+ }
318
+ let missing = false
319
+ const fill = (value) => {
320
+ if (typeof value === 'string') {
321
+ return value.replace(/@group\.([\w.]+)/g, (token, path) => {
322
+ const found = readPath(raw, path)
323
+ if (found === undefined || found === null || found === '') {
324
+ missing = true
325
+ return ''
326
+ }
327
+ return encodeURIComponent(String(found))
328
+ })
329
+ }
330
+ if (Array.isArray(value)) {
331
+ return value.map(fill)
332
+ }
333
+ if (value && typeof value === 'object') {
334
+ return Object.fromEntries(Object.entries(value).map(([key, inner]) => [key, fill(inner)]))
335
+ }
336
+ return value
337
+ }
338
+ const filled = fill(action)
339
+ return missing ? null : { ...filled, id: `${action.id || 'group-action'}-${index}` }
340
+ },
341
+
288
342
  /**
289
343
  * Translate a text through the host app's catalogue.
290
344
  *
@@ -382,6 +436,10 @@ export default {
382
436
  color: var(--color-main-text);
383
437
  }
384
438
 
439
+ .cn-grouped-cards__group-action {
440
+ margin-top: 8px;
441
+ }
442
+
385
443
  .cn-grouped-cards__count {
386
444
  padding: 2px 8px;
387
445
  border-radius: var(--border-radius-pill, 12px);
@@ -33,8 +33,8 @@
33
33
  class="cn-macros-widget__row"
34
34
  :data-testid="`cn-macros-widget-row-${macro.key}`">
35
35
  <div class="cn-macros-widget__text">
36
- <span class="cn-macros-widget__name">{{ macro.name }}</span>
37
- <span v-if="macro.description" class="cn-macros-widget__hint">{{ macro.description }}</span>
36
+ <span class="cn-macros-widget__name">{{ tr(macro.name) }}</span>
37
+ <span v-if="macro.description" class="cn-macros-widget__hint">{{ tr(macro.description) }}</span>
38
38
  </div>
39
39
  <NcButton
40
40
  :disabled="running !== ''"
@@ -54,7 +54,9 @@
54
54
  v-else-if="lastRun"
55
55
  type="success"
56
56
  data-testid="cn-macros-widget-last-run">
57
- {{ t('nextcloud-vue', 'Last run: {name}, {date}', { name: lastRun.name, date: lastRun.date }) }}
57
+ {{ t('nextcloud-vue', 'Last run: {name}, {date}', { name: lastRun.name, date: lastRun.date }) }}<template v-if="lastRun.changes.length">
58
+ {{ n('nextcloud-vue', '{count} change: {list}', '{count} changes: {list}', lastRun.changes.length, { count: lastRun.changes.length, list: lastRun.changes.join(', ') }) }}
59
+ </template>
58
60
  </NcNoteCard>
59
61
  </div>
60
62
  </template>
@@ -62,7 +64,7 @@
62
64
  <script>
63
65
  import axios from '@nextcloud/axios'
64
66
  import { emit } from '@nextcloud/event-bus'
65
- import { translate as t } from '@nextcloud/l10n'
67
+ import { translatePlural as n, translate as t } from '@nextcloud/l10n'
66
68
  import { generateUrl } from '@nextcloud/router'
67
69
  import { NcButton, NcNoteCard } from '@nextcloud/vue'
68
70
  import { declaredMacros } from '../../utils/declaredMacros.js'
@@ -135,6 +137,7 @@ export default {
135
137
 
136
138
  methods: {
137
139
  t,
140
+ n,
138
141
 
139
142
  /**
140
143
  * Translate an app-supplied text through the host app when it offers it.
@@ -188,8 +191,11 @@ export default {
188
191
  const entry = (res?.data?.results || [])[0]
189
192
  const key = entry?.changed?.declaredAction
190
193
  const macro = this.macros.find((candidate) => candidate.key === key)
194
+ const changes = Array.isArray(entry?.changed?.changes)
195
+ ? entry.changed.changes.map((change) => (change && typeof change.step === 'string' ? change.step : '')).filter(Boolean)
196
+ : []
191
197
  this.lastRun = macro && entry.created
192
- ? { name: macro.name, date: new Date(entry.created).toLocaleString() }
198
+ ? { name: this.tr(macro.name), date: new Date(entry.created).toLocaleString(), changes }
193
199
  : null
194
200
  } catch {
195
201
  this.lastRun = null
@@ -208,8 +214,12 @@ export default {
208
214
  this.error = ''
209
215
  try {
210
216
  await axios.post(generateUrl('/apps/openregister/api/objects/{register}/{schema}/{id}/actions/{action}', { register, schema, id, action: macro.key }))
211
- this.lastRun = { name: macro.name, date: new Date().toLocaleString() }
217
+ this.lastRun = { name: this.tr(macro.name), date: new Date().toLocaleString(), changes: [] }
212
218
  emit('cn:page:refresh', { source: 'cn-macros-widget' })
219
+ // The audit entry of this run says what it changed.
220
+ const local = this.lastRun
221
+ await this.loadLastRun()
222
+ this.lastRun = this.lastRun || local
213
223
  } catch (e) {
214
224
  this.error = t('nextcloud-vue', 'The macro did not run: {error}', { error: e?.response?.data?.error || e?.message || '' })
215
225
  } finally {
@@ -315,6 +315,7 @@ import CnPagination from '../CnPagination/CnPagination.vue'
315
315
  import CnWidgetEmptyState from '../CnWidgetEmptyState/CnWidgetEmptyState.vue'
316
316
  import { useWriteFeedback } from '../../composables/useWriteFeedback.js'
317
317
  import { actionLink, dispatchAction, hasActionTargetTokens } from '../../utils/actionsDispatcher.js'
318
+ import { buildExportUrl } from '../../utils/indexExportHelpers.js'
318
319
  import { followLinkClick, openRowTarget, resolveHref } from '../../utils/linkNavigation.js'
319
320
  import { objectDisplayName, objectFieldValue } from '../../utils/objectName.js'
320
321
  import { moveIndex, renumberRows, rowIdOf } from '../../utils/reorderRows.js'
@@ -322,6 +323,7 @@ import { dropOptionalUnresolved, hasUnresolvedTokens, resolveFilterTokens } from
322
323
  import { markNewTabHandled } from '../../utils/rowAuxClick.js'
323
324
  import { changedKeys } from '../../utils/saveEdited.js'
324
325
  import { dispatchObjectCreated } from '../../utils/walkthroughSignals.js'
326
+ import { getCurrentUserGroups } from '../../utils/widgetVisibility.js'
325
327
  import { CnRowActions } from '../CnRowActions/index.js'
326
328
 
327
329
  /**
@@ -450,8 +452,11 @@ export default {
450
452
  *
451
453
  * `footnote` is a line under the table, through the host translate.
452
454
  *
455
+ * `export` (`{ label?, format?, group? }`) offers the filtered list as a
456
+ * download in the card's header; `group` shows it to one group only.
457
+ *
453
458
  * @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-small-opt-ins-from-the-decidiq-and-learniq-lanes
454
- * @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}}
459
+ * @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, export?: {label?: string, format?: ('csv'|'excel'), group?: string}}}
455
460
  */
456
461
  content: {
457
462
  type: Object,
@@ -459,7 +464,7 @@ export default {
459
464
  },
460
465
  },
461
466
 
462
- emits: ['created', 'updated', 'row-click', 'row-aux-click', 'view-all', 'files-dropped'],
467
+ emits: ['created', 'updated', 'row-click', 'row-aux-click', 'view-all', 'files-dropped', 'header'],
463
468
 
464
469
  data() {
465
470
  return {
@@ -1268,6 +1273,7 @@ export default {
1268
1273
  mounted() {
1269
1274
  this.fetchRows()
1270
1275
  this.loadHeadingsIfNeeded()
1276
+ this.offerExport()
1271
1277
  // A table wider than the widget fits its columns to the width instead of
1272
1278
  // scrolling sideways (no inner scrollbars).
1273
1279
  if (typeof ResizeObserver !== 'undefined') {
@@ -1318,6 +1324,53 @@ export default {
1318
1324
  },
1319
1325
 
1320
1326
  methods: {
1327
+ /**
1328
+ * Offer `content.export` as a button in the card's header: the list as
1329
+ * it is filtered here, through OpenRegister's export (which applies its
1330
+ * own read rights to every row). `group` keeps the button to the members
1331
+ * of one Nextcloud group, a role such as AI oversight; it hides the
1332
+ * button, and the export endpoint still decides what anyone may read.
1333
+ *
1334
+ * @return {Promise<void>}
1335
+ * @spec openspec/changes/widget-subtitle-and-list-export/specs/object-list-widget/spec.md#requirement-a-list-can-offer-its-rows-as-an-export
1336
+ */
1337
+ /**
1338
+ * The export URL for this list as it is filtered here.
1339
+ *
1340
+ * @param {'csv'|'excel'} format The export format.
1341
+ * @return {string} The URL.
1342
+ */
1343
+ exportUrl(format) {
1344
+ return buildExportUrl(this.content.register, this.content.schema, this.resolvedFilter, format)
1345
+ },
1346
+
1347
+ async offerExport() {
1348
+ const config = this.content && this.content.export
1349
+ if (!config || typeof config !== 'object' || !this.content.register || !this.content.schema) {
1350
+ return
1351
+ }
1352
+ if (typeof config.group === 'string' && config.group !== '') {
1353
+ const groups = await getCurrentUserGroups().catch(() => [])
1354
+ if (!groups.includes(config.group)) {
1355
+ return
1356
+ }
1357
+ }
1358
+ const format = config.format === 'excel' ? 'excel' : 'csv'
1359
+ const label = typeof config.label === 'string' && config.label !== '' ? (typeof this.cnTranslate === 'function' ? this.cnTranslate(config.label) : config.label) : t('nextcloud-vue', 'Download')
1360
+ /**
1361
+ * @event header The card header's Download action for the list as
1362
+ * filtered here (`content.export`), handed to the host card.
1363
+ * @type {{action: {label: string, icon: string, run: Function}}}
1364
+ */
1365
+ this.$emit('header', {
1366
+ action: {
1367
+ label,
1368
+ icon: 'Download',
1369
+ run: () => window.location.assign(this.exportUrl(format)),
1370
+ },
1371
+ })
1372
+ },
1373
+
1321
1374
  /**
1322
1375
  * Move a row one place up or down (the row actions).
1323
1376
  *
@@ -58,6 +58,14 @@
58
58
  </tr>
59
59
  </tbody>
60
60
  </table>
61
+ <!-- notes: a field per row for what happened, recorded on the row's
62
+ own notes endpoint (DqDagAfsluiten "Nog open van vandaag"). -->
63
+ <CnRecordNoteRows
64
+ v-else-if="presentation === 'notes' && noteAction"
65
+ :items="items"
66
+ :action="noteAction"
67
+ :recordAll="noteRecordAll"
68
+ :leaf="content.rowLeaf || null" />
61
69
  <ul v-else class="cn-record-list__list">
62
70
  <li
63
71
  v-for="item in items"
@@ -277,6 +285,7 @@ import { inject, ref } from 'vue'
277
285
  import ChevronRight from 'vue-material-design-icons/ChevronRight.vue'
278
286
  import CnIcon from '../CnIcon/CnIcon.vue'
279
287
  import CnStatusBadge from '../CnStatusBadge/CnStatusBadge.vue'
288
+ import CnRecordNoteRows from './CnRecordNoteRows.vue'
280
289
  import { useEndpointSource } from '../../composables/useEndpointSource.js'
281
290
  import { actionLink, dispatchAction, interpolateActionTarget } from '../../utils/actionsDispatcher.js'
282
291
  import { isBadgeVariant } from '../../utils/badgeVariants.js'
@@ -295,7 +304,7 @@ const PAGE_REFRESH_CHANNEL = 'cn:page:refresh'
295
304
 
296
305
  /** Gives each list's footer reason its own element id. */
297
306
  let footerSeq = 0
298
- const PRESENTATIONS = ['rows', 'feed', 'inline', 'cards', 'notice', 'table']
307
+ const PRESENTATIONS = ['rows', 'feed', 'inline', 'cards', 'notice', 'table', 'notes']
299
308
 
300
309
  /** The tones a `notice` list takes. */
301
310
  const NOTICE_VARIANTS = ['info', 'warning', 'error']
@@ -348,7 +357,7 @@ function unwrap(value) {
348
357
  export default {
349
358
  name: 'CnRecordListWidget',
350
359
 
351
- components: { ChevronRight, CnIcon, CnStatusBadge, NcButton, NcLoadingIcon },
360
+ components: { ChevronRight, CnIcon, CnRecordNoteRows, CnStatusBadge, NcButton, NcLoadingIcon },
352
361
 
353
362
  inject: {
354
363
  /** Detail-page object context (`{ objectId, object, register, schema }`). */
@@ -446,6 +455,8 @@ export default {
446
455
  /** Whether `footerAction.disabledWhen` holds. */
447
456
  footerDisabled: false,
448
457
  footerReasonId: `cn-record-list-footer-reason-${++footerSeq}`,
458
+ /** The first `emptyTexts` entry whose condition holds, translated; '' for none. */
459
+ conditionalEmptyText: '',
449
460
  }
450
461
  },
451
462
 
@@ -731,10 +742,58 @@ export default {
731
742
  },
732
743
 
733
744
  /** @return {string} The empty line. */
745
+ /**
746
+ * `noteAction` with its labels through the host translate, or null
747
+ * (`presentation: "notes"`).
748
+ *
749
+ * @spec openspec/changes/record-list-notes/specs/record-list-notes/spec.md#requirement-a-record-list-can-collect-and-record-a-note-per-row
750
+ * @return {object|null}
751
+ */
752
+ noteAction() {
753
+ const action = this.content?.noteAction
754
+ if (!action || typeof action !== 'object' || (!action.urlField && !action.url)) {
755
+ return null
756
+ }
757
+ const tr = (text) => (typeof text === 'string' && text !== '' ? this.translate(text) : '')
758
+ return {
759
+ ...action,
760
+ label: tr(action.label),
761
+ fieldLabel: tr(action.fieldLabel),
762
+ placeholder: tr(action.placeholder),
763
+ readyLabel: tr(action.readyLabel),
764
+ errorText: tr(action.errorText),
765
+ }
766
+ },
767
+
768
+ /**
769
+ * `noteAction.recordAll` translated, its cancel link as a route, or null.
770
+ *
771
+ * @return {object|null}
772
+ */
773
+ noteRecordAll() {
774
+ const all = this.content?.noteAction?.recordAll
775
+ if (!all || typeof all !== 'object' || !all.label) {
776
+ return null
777
+ }
778
+ const cancel = all.cancel && all.cancel.label && all.cancel.route
779
+ ? { label: this.translate(all.cancel.label), to: typeof all.cancel.route === 'string' ? { name: all.cancel.route } : all.cancel.route }
780
+ : null
781
+ return {
782
+ label: this.translate(all.label),
783
+ title: all.title ? this.translate(all.title) : '',
784
+ text: all.text ? this.translate(all.text) : '',
785
+ icon: all.icon || '',
786
+ cancel,
787
+ }
788
+ },
789
+
734
790
  emptyLabel() {
735
791
  if (this.lensReasonText !== '') {
736
792
  return this.lensReasonText
737
793
  }
794
+ if (this.conditionalEmptyText !== '') {
795
+ return this.conditionalEmptyText
796
+ }
738
797
  return this.content?.emptyText ? this.translate(this.content.emptyText) : t('nextcloud-vue', 'Nothing here yet')
739
798
  },
740
799
 
@@ -754,6 +813,10 @@ export default {
754
813
  this.fetchRows()
755
814
  },
756
815
 
816
+ rows() {
817
+ this.evaluateEmptyTexts()
818
+ },
819
+
757
820
  footerDisabledKey: {
758
821
  immediate: true,
759
822
  handler() {
@@ -839,6 +902,38 @@ export default {
839
902
  * @spec openspec/changes/record-list-footer-action/specs/record-list/spec.md#requirement-a-record-list-can-end-with-an-action-that-says-why-it-is-disabled
840
903
  * @return {Promise<void>}
841
904
  */
905
+ /**
906
+ * Pick the empty text (`emptyTexts`: `[{ when, text }]`, the first whose
907
+ * visibleWhen condition holds), for a list that is empty for more than
908
+ * one reason ("Everything you opened today has a note" or "You have not
909
+ * opened anything today"). `emptyText` stays the fallback.
910
+ *
911
+ * @spec openspec/changes/record-list-notes/specs/record-list-notes/spec.md#requirement-a-record-list-can-say-why-it-is-empty
912
+ * @return {Promise<void>}
913
+ */
914
+ async evaluateEmptyTexts() {
915
+ const options = Array.isArray(this.content?.emptyTexts) ? this.content.emptyTexts : []
916
+ if (options.length === 0 || this.rows.length > 0) {
917
+ this.conditionalEmptyText = ''
918
+ return
919
+ }
920
+ const run = this.emptyRun = (this.emptyRun || 0) + 1
921
+ for (const option of options) {
922
+ if (!option || typeof option.text !== 'string' || !option.when) {
923
+ continue
924
+ }
925
+ const holds = await evaluateVisibleWhen(option.when, this.tokenCtx)
926
+ if (run !== this.emptyRun) {
927
+ return
928
+ }
929
+ if (holds) {
930
+ this.conditionalEmptyText = this.translate(option.text)
931
+ return
932
+ }
933
+ }
934
+ this.conditionalEmptyText = ''
935
+ },
936
+
842
937
  async evaluateFooterDisabled() {
843
938
  const cond = this.content?.footerAction?.disabledWhen
844
939
  if (!cond) {
@@ -1103,7 +1198,10 @@ export default {
1103
1198
  },
1104
1199
 
1105
1200
  rowLink(row) {
1106
- const route = this.content?.rowRoute
1201
+ // `rowRouteField`: the route name read from the row, for a list
1202
+ // whose rows open different pages (a case and a task).
1203
+ const byField = this.content?.rowRouteField ? readPath(row, this.content.rowRouteField) : null
1204
+ const route = (typeof byField === 'string' && byField !== '') ? byField : this.content?.rowRoute
1107
1205
  if (!route || !this.$router || !row) {
1108
1206
  return null
1109
1207
  }