@conduction/nextcloud-vue 2.144.0 → 2.146.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 (133) hide show
  1. package/dist/esm/components/CnActionDialog/CnActionDialog.vue.js +1 -1
  2. package/dist/esm/components/CnActionDialog/CnActionDialog.vue2.js.map +1 -1
  3. package/dist/esm/components/CnActionDialog/CnActionDialog.vue3.js +1 -0
  4. package/dist/esm/components/CnActionDialog/CnActionDialog.vue3.js.map +1 -1
  5. package/dist/esm/components/CnAiCompanion/CnAiChatPanel.vue2.js.map +1 -1
  6. package/dist/esm/components/CnAiCompanion/CnAiChatPanel.vue3.js.map +1 -1
  7. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue.js +1 -1
  8. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js +55 -2
  9. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js.map +1 -1
  10. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue3.js.map +1 -1
  11. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue.js +1 -1
  12. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue2.js +16 -0
  13. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue2.js.map +1 -1
  14. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue3.js +3 -1
  15. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue3.js.map +1 -1
  16. package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue.js +1 -1
  17. package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue2.js.map +1 -1
  18. package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue3.js.map +1 -1
  19. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +12 -2
  20. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  21. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +42 -30
  22. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  23. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +22 -0
  24. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  25. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +2 -1
  26. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  27. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue.js +1 -1
  28. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue2.js +1 -1
  29. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue2.js.map +1 -1
  30. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue3.js +3 -3
  31. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue3.js.map +1 -1
  32. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
  33. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +46 -8
  34. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
  35. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js +3 -3
  36. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
  37. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue.js +1 -1
  38. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js +2 -2
  39. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js.map +1 -1
  40. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js.map +1 -1
  41. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +6 -3
  42. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  43. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  44. package/dist/esm/components/CnMailTab/CnMailTab.vue.js +1 -1
  45. package/dist/esm/components/CnMailTab/CnMailTab.vue2.js.map +1 -1
  46. package/dist/esm/components/CnMailTab/CnMailTab.vue3.js +2 -2
  47. package/dist/esm/components/CnMailTab/CnMailTab.vue3.js.map +1 -1
  48. package/dist/esm/components/CnNotesCard/CnNotesCard.vue.js +1 -1
  49. package/dist/esm/components/CnNotesCard/CnNotesCard.vue2.js.map +1 -1
  50. package/dist/esm/components/CnNotesCard/CnNotesCard.vue3.js +1 -0
  51. package/dist/esm/components/CnNotesCard/CnNotesCard.vue3.js.map +1 -1
  52. package/dist/esm/components/CnNotificationMatrix/CnNotificationMatrix.vue.js +1 -1
  53. package/dist/esm/components/CnNotificationMatrix/CnNotificationMatrix.vue2.js +21 -2
  54. package/dist/esm/components/CnNotificationMatrix/CnNotificationMatrix.vue2.js.map +1 -1
  55. package/dist/esm/components/CnNotificationMatrix/CnNotificationMatrix.vue3.js.map +1 -1
  56. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
  57. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +73 -12
  58. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  59. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +165 -126
  60. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  61. package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue.js +1 -1
  62. package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue2.js.map +1 -1
  63. package/dist/esm/components/CnRetentionWidget/CnRetentionWidget.vue3.js.map +1 -1
  64. package/dist/esm/components/CnSettingsField/CnSettingsField.vue.js +1 -1
  65. package/dist/esm/components/CnSettingsField/CnSettingsField.vue2.js +14 -0
  66. package/dist/esm/components/CnSettingsField/CnSettingsField.vue2.js.map +1 -1
  67. package/dist/esm/components/CnSettingsField/CnSettingsField.vue3.js +2 -1
  68. package/dist/esm/components/CnSettingsField/CnSettingsField.vue3.js.map +1 -1
  69. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
  70. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  71. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  72. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue.js +1 -1
  73. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js +13 -8
  74. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js.map +1 -1
  75. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue3.js.map +1 -1
  76. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue.js +1 -1
  77. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue2.js +10 -2
  78. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue2.js.map +1 -1
  79. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue3.js.map +1 -1
  80. package/dist/esm/composables/indexSources.js +35 -9
  81. package/dist/esm/composables/indexSources.js.map +1 -1
  82. package/dist/esm/l10n/nl.json.js +2 -0
  83. package/dist/esm/l10n/nl.json.js.map +1 -1
  84. package/dist/esm/library-style.js +1 -1
  85. package/dist/esm/nextcloud-vue.css +1204 -467
  86. package/dist/esm/utils/builtInFormatters.js +18 -1
  87. package/dist/esm/utils/builtInFormatters.js.map +1 -1
  88. package/dist/esm/utils/detailObjectContext.js +7 -2
  89. package/dist/esm/utils/detailObjectContext.js.map +1 -1
  90. package/dist/esm/utils/localHeaderRows.js +95 -0
  91. package/dist/esm/utils/localHeaderRows.js.map +1 -0
  92. package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
  93. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  94. package/dist/esm/utils/visibleWhen.js +6 -0
  95. package/dist/esm/utils/visibleWhen.js.map +1 -1
  96. package/dist/nextcloud-vue.cjs.js +686 -239
  97. package/dist/nextcloud-vue.cjs.js.map +2 -2
  98. package/dist/nextcloud-vue.css +1204 -467
  99. package/l10n/nl.json +2 -0
  100. package/package.json +1 -1
  101. package/src/components/CnActionDialog/CnActionDialog.vue +8 -1
  102. package/src/components/CnAiCompanion/CnAiChatPanel.vue +3 -0
  103. package/src/components/CnAuditTrailCard/CnAuditTrailCard.vue +55 -2
  104. package/src/components/CnAuditTrailWidget/CnAuditTrailWidget.vue +18 -0
  105. package/src/components/CnChecklistRunWidget/CnChecklistRunWidget.vue +4 -2
  106. package/src/components/CnDataTable/CnDataTable.vue +19 -2
  107. package/src/components/CnDetailPage/CnDetailPage.md +1 -0
  108. package/src/components/CnDetailPage/CnDetailPage.vue +23 -0
  109. package/src/components/CnDetailWidgetHost/CnDetailWidgetHeaderItems.vue +3 -1
  110. package/src/components/CnFactsWidget/CnFactsWidget.vue +74 -9
  111. package/src/components/CnFieldListWidget/CnFieldListWidget.vue +2 -2
  112. package/src/components/CnIndexPage/CnIndexPage.vue +6 -3
  113. package/src/components/CnMailTab/CnMailTab.vue +6 -4
  114. package/src/components/CnNotesCard/CnNotesCard.vue +7 -2
  115. package/src/components/CnNotificationMatrix/CnNotificationMatrix.vue +25 -3
  116. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +132 -20
  117. package/src/components/CnRetentionWidget/CnRetentionWidget.vue +2 -0
  118. package/src/components/CnSettingsField/CnSettingsField.vue +15 -0
  119. package/src/components/CnTabsWidget/CnTabsWidget.vue +6 -1
  120. package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +13 -8
  121. package/src/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue +10 -2
  122. package/src/composables/indexSources.js +35 -9
  123. package/src/css/dialog.css +25 -9
  124. package/src/css/look-board-detail.css +370 -30
  125. package/src/css/look-board-index.css +116 -4
  126. package/src/css/look-board.css +168 -8
  127. package/src/css/table.css +17 -1
  128. package/src/schemas/app-manifest-v2.schema.json +2 -2
  129. package/src/utils/builtInFormatters.js +17 -0
  130. package/src/utils/detailObjectContext.js +7 -2
  131. package/src/utils/localHeaderRows.js +91 -0
  132. package/src/utils/validateManifestV2.compiled.js +2 -2
  133. package/src/utils/visibleWhen.js +6 -0
@@ -331,6 +331,23 @@ import {
331
331
  * @event {object} digest-change — A channel's digest was set. Payload: `{ channelId, mode, timeOfDay }`.
332
332
  * @event {object} test-send — A test was asked for. Payload: `{ channelId }`.
333
333
  */
334
+ /**
335
+ * An event's shipped default in one channel: one boolean for every channel,
336
+ * or the channel's own entry when the default differs per channel.
337
+ *
338
+ * @param {object} event The event.
339
+ * @param {object} channel The channel.
340
+ * @return {boolean} The default.
341
+ * @spec openspec/changes/notification-matrix-default-per-channel/specs/notification-matrix/spec.md
342
+ */
343
+ function appDefaultFor(event, channel) {
344
+ const value = event?.appDefault
345
+ if (value !== null && typeof value === 'object') {
346
+ return value[channel?.id] === true
347
+ }
348
+ return value === true
349
+ }
350
+
334
351
  export default {
335
352
  name: 'CnNotificationMatrix',
336
353
 
@@ -342,7 +359,9 @@ export default {
342
359
  props: {
343
360
  /**
344
361
  * The events, each `{ id, label, group, groupLabel, immediate,
345
- * appDefault }`. From the host's catalogue.
362
+ * appDefault }`. From the host's catalogue. `appDefault` is one
363
+ * boolean for every channel, or `{ [channelId]: boolean }` when the
364
+ * default differs per channel (an opt-in e-mail is off by default).
346
365
  */
347
366
  events: {
348
367
  type: Array,
@@ -607,7 +626,7 @@ export default {
607
626
  })
608
627
 
609
628
  return resolvePreference({
610
- appDefault: event?.appDefault === true,
629
+ appDefault: appDefaultFor(event, channel),
611
630
  groupValue: this.at(this.groupValues, event, channel),
612
631
  personalValue: narrow.value === null
613
632
  ? this.at(this.personalValues, event, channel)
@@ -1135,7 +1154,10 @@ export default {
1135
1154
  min-inline-size: 20px;
1136
1155
  block-size: 20px;
1137
1156
  min-block-size: 20px;
1138
- margin: 0;
1157
+ /* A block box, so the line's baseline adds no height: the row is
1158
+ 42px as on the board (10px, the 1px-low checkbox, 10px, the rule). */
1159
+ display: block;
1160
+ margin: 1px auto 0;
1139
1161
  }
1140
1162
 
1141
1163
  .cn-notification-matrix__group {
@@ -53,15 +53,37 @@
53
53
  scope="col"
54
54
  :aria-sort="col.sortable ? sortStateOf(col.key) : undefined"
55
55
  :class="{ 'cn-record-list__cell--end': col.align === 'end' }">
56
- <button
57
- v-if="col.sortable"
58
- type="button"
59
- class="cn-record-list__sort"
60
- :data-testid="`cn-record-list-sort-${col.key}`"
61
- @click="toggleTableSort(col.key)">
62
- {{ col.label }}
63
- <span class="cn-record-list__sort-mark" aria-hidden="true">{{ sortMarkOf(col.key) }}</span>
64
- </button>
56
+ <span v-if="col.sortable || tableFilterable" class="cn-record-list__head">
57
+ <button
58
+ v-if="col.sortable"
59
+ type="button"
60
+ class="cn-record-list__sort"
61
+ :data-testid="`cn-record-list-sort-${col.key}`"
62
+ @click="toggleTableSort(col.key)">
63
+ {{ col.label }}
64
+ <span class="cn-record-list__sort-mark" aria-hidden="true">{{ sortMarkOf(col.key) }}</span>
65
+ </button>
66
+ <template v-else>
67
+ {{ col.label }}
68
+ </template>
69
+ <!-- `content.filterable`: the head offers the values the column holds and keeps the rows with a chosen one. -->
70
+ <NcActions
71
+ v-if="tableFilterable"
72
+ class="cn-record-list__head-filter"
73
+ :aria-label="filterLabelOf(col.label)"
74
+ :data-testid="`cn-record-list-filter-${col.key}`">
75
+ <template #icon>
76
+ <FilterOutline :size="14" />
77
+ </template>
78
+ <NcActionCheckbox
79
+ v-for="option in tableFilterOptions(col.key)"
80
+ :key="option"
81
+ :modelValue="(tableFilters[col.key] || []).includes(option)"
82
+ @update:modelValue="toggleTableFilter(col.key, option)">
83
+ {{ option }}
84
+ </NcActionCheckbox>
85
+ </NcActions>
86
+ </span>
65
87
  <template v-else>
66
88
  {{ col.label }}
67
89
  </template>
@@ -550,10 +572,11 @@
550
572
  import { subscribe, unsubscribe } from '@nextcloud/event-bus'
551
573
  import { translate as t } from '@nextcloud/l10n'
552
574
  import { generateUrl } from '@nextcloud/router'
553
- import { NcActionButton, NcActionLink, NcActions, NcButton, NcLoadingIcon } from '@nextcloud/vue'
575
+ import { NcActionButton, NcActionCheckbox, NcActionLink, NcActions, NcButton, NcLoadingIcon } from '@nextcloud/vue'
554
576
  import { inject, ref } from 'vue'
555
577
  import Check from 'vue-material-design-icons/Check.vue'
556
578
  import ChevronRight from 'vue-material-design-icons/ChevronRight.vue'
579
+ import FilterOutline from 'vue-material-design-icons/FilterOutline.vue'
557
580
  import CnIcon from '../CnIcon/CnIcon.vue'
558
581
  import CnStatusBadge from '../CnStatusBadge/CnStatusBadge.vue'
559
582
  import CnRecordNoteRows from './CnRecordNoteRows.vue'
@@ -649,7 +672,7 @@ function unwrap(value) {
649
672
  export default {
650
673
  name: 'CnRecordListWidget',
651
674
 
652
- components: { Check, ChevronRight, CnIcon, CnRecordNoteRows, CnStatusBadge, NcActionButton, NcActionLink, NcActions, NcButton, NcLoadingIcon },
675
+ components: { Check, ChevronRight, CnIcon, CnRecordNoteRows, CnStatusBadge, FilterOutline, NcActionButton, NcActionCheckbox, NcActionLink, NcActions, NcButton, NcLoadingIcon },
653
676
 
654
677
  inject: {
655
678
  /** Detail-page object context (`{ objectId, object, register, schema }`). */
@@ -714,6 +737,8 @@ export default {
714
737
  * without a value.
715
738
  * `density: "compact"` draws the rows 10px above and below, at least
716
739
  * 56px tall, with the title at 500 (DqProcesanalyse).
740
+ * On `cards` it draws the small framed card: 12px 14px in, the title
741
+ * 15px/700 with the pill at its end (DqInspectie).
717
742
  * `density: "stacked"` stands the rows 10px apart with no rule and no
718
743
  * row padding (DqDagAfsluiten "Morgen eerst").
719
744
  * In `rows`, `text` is a line under the meta line ("12 dagen stil"),
@@ -809,6 +834,8 @@ export default {
809
834
  return {
810
835
  /** The reader's sort of a `table`: the column key and `ascending` or `descending`. */
811
836
  tableSort: { key: '', dir: '' },
837
+ /** The reader's head filters of a `table`: `{ [columnKey]: cellTexts[] }`. */
838
+ tableFilters: {},
812
839
  rows: [],
813
840
  /** The `aggregates` figures per row id: `{ [rowId]: { [key]: value } }`. */
814
841
  aggregateValues: {},
@@ -1183,7 +1210,16 @@ export default {
1183
1210
  const isTemplate = (value) => typeof value === 'string' || (Array.isArray(value) && value.length > 0 && value.every((entry) => typeof entry === 'string'))
1184
1211
  return columns
1185
1212
  .filter((col) => col && typeof col === 'object' && isTemplate(col.value))
1186
- .map((col, index) => ({ ...col, key: `col-${index}`, label: typeof col.label === 'string' ? this.translate(col.label) : '' }))
1213
+ .map((col, index) => ({ ...col, key: `col-${index}`, label: typeof col.label === 'string' ? this.translate(col.label) : '', sortable: col.sortable === true || this.content?.sortable === true }))
1214
+ },
1215
+
1216
+ /**
1217
+ * Whether the `table` heads filter (`content.filterable`).
1218
+ *
1219
+ * @return {boolean}
1220
+ */
1221
+ tableFilterable() {
1222
+ return this.content?.filterable === true
1187
1223
  },
1188
1224
 
1189
1225
  /**
@@ -1193,8 +1229,27 @@ export default {
1193
1229
  * @spec openspec/changes/record-list-table/specs/record-list-table/spec.md#requirement-a-record-list-can-draw-its-rows-as-a-small-table
1194
1230
  */
1195
1231
  tableRows() {
1232
+ const rows = this.tableAllRows.filter((row) => Object.entries(this.tableFilters).every(([key, values]) => {
1233
+ const index = this.tableColumns.findIndex((col) => col.key === key)
1234
+ return values.length === 0 || index < 0 || values.includes(row.cells[index].text)
1235
+ }))
1236
+ const { key, dir } = this.tableSort
1237
+ const index = this.tableColumns.findIndex((col) => col.key === key)
1238
+ if (index < 0 || !dir) {
1239
+ return rows
1240
+ }
1241
+ const sign = dir === 'descending' ? -1 : 1
1242
+ return [...rows].sort((a, b) => sign * compareCells(a.cells[index].text, b.cells[index].text))
1243
+ },
1244
+
1245
+ /**
1246
+ * Every `table` row, before the reader's head filters and sort.
1247
+ *
1248
+ * @return {Array<object>} `{ key, cells: [{ key, text, align, alert }] }`.
1249
+ */
1250
+ tableAllRows() {
1196
1251
  const indentPath = this.content?.indentPath
1197
- const rows = this.items.map((item) => ({
1252
+ return this.items.map((item) => ({
1198
1253
  key: item.key,
1199
1254
  row: item.row,
1200
1255
  rowActions: this.rowActions.length > 0 ? this.rowActionsOf(item.row) : [],
@@ -1215,13 +1270,6 @@ export default {
1215
1270
  }
1216
1271
  }),
1217
1272
  }))
1218
- const { key, dir } = this.tableSort
1219
- const index = this.tableColumns.findIndex((col) => col.key === key)
1220
- if (index < 0 || !dir) {
1221
- return rows
1222
- }
1223
- const sign = dir === 'descending' ? -1 : 1
1224
- return [...rows].sort((a, b) => sign * compareCells(a.cells[index].text, b.cells[index].text))
1225
1273
  },
1226
1274
 
1227
1275
  /**
@@ -2246,6 +2294,41 @@ export default {
2246
2294
  this.tableSort = { key, dir }
2247
2295
  },
2248
2296
 
2297
+ /**
2298
+ * The values a `table` column's head filter offers: each text the column holds, once.
2299
+ *
2300
+ * @param {string} key The column key.
2301
+ * @return {Array<string>} The texts, in row order.
2302
+ */
2303
+ tableFilterOptions(key) {
2304
+ const index = this.tableColumns.findIndex((col) => col.key === key)
2305
+ if (index < 0) {
2306
+ return []
2307
+ }
2308
+ return [...new Set(this.tableAllRows.map((row) => row.cells[index].text).filter((text) => text !== ''))]
2309
+ },
2310
+
2311
+ /**
2312
+ * Keep or drop one value of a `table` column's head filter.
2313
+ *
2314
+ * @param {string} key The column key.
2315
+ * @param {string} value The cell text.
2316
+ */
2317
+ toggleTableFilter(key, value) {
2318
+ const current = this.tableFilters[key] || []
2319
+ this.tableFilters = { ...this.tableFilters, [key]: current.includes(value) ? current.filter((v) => v !== value) : [...current, value] }
2320
+ },
2321
+
2322
+ /**
2323
+ * The accessible name of a column's head filter.
2324
+ *
2325
+ * @param {string} label The column label.
2326
+ * @return {string}
2327
+ */
2328
+ filterLabelOf(label) {
2329
+ return t('nextcloud-vue', 'Filter by {column}', { column: label })
2330
+ },
2331
+
2249
2332
  /**
2250
2333
  * The rows of a read with `flattenPath` set: each entry of the list at
2251
2334
  * that path on a record becomes a row, with the record at `@parent`.
@@ -3232,6 +3315,16 @@ export default {
3232
3315
  font-size: 10px;
3233
3316
  }
3234
3317
 
3318
+ .cn-record-list__head {
3319
+ display: inline-flex;
3320
+ gap: 4px;
3321
+ align-items: center;
3322
+ }
3323
+
3324
+ .cn-record-list__head-filter {
3325
+ --button-size: 22px;
3326
+ }
3327
+
3235
3328
  .cn-record-list__table .cn-record-list__cell--end {
3236
3329
  padding-inline-start: 12px;
3237
3330
  text-align: end;
@@ -3320,6 +3413,7 @@ export default {
3320
3413
  font-weight: 600;
3321
3414
  height: 34px;
3322
3415
  justify-content: center;
3416
+ margin: 0;
3323
3417
  padding: 0 14px;
3324
3418
  text-decoration: none;
3325
3419
  }
@@ -3383,6 +3477,24 @@ export default {
3383
3477
  text-decoration: none;
3384
3478
  }
3385
3479
 
3480
+ /* `density: "compact"` on cards: the boards' small framed rows (DqInspectie
3481
+ "Molenweg 4" and the outbox): 12px 14px in, the title 15px/700 on an
3482
+ 18px line with the pill at its end, the meta 6px under it. */
3483
+ .cn-record-list--compact .cn-record-list__card {
3484
+ gap: 6px;
3485
+ padding: 12px 14px;
3486
+ }
3487
+
3488
+ .cn-record-list--compact .cn-record-list__card-head {
3489
+ align-items: flex-start;
3490
+ }
3491
+
3492
+ .cn-record-list--compact .cn-record-list__card .cn-record-list__title {
3493
+ font-size: 15px;
3494
+ font-weight: 700;
3495
+ line-height: 1.2;
3496
+ }
3497
+
3386
3498
  .cn-record-list__card--link:hover,
3387
3499
  .cn-record-list__card--link:focus-visible {
3388
3500
  border-color: var(--color-border-dark, var(--color-border));
@@ -604,6 +604,7 @@ export default {
604
604
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
605
605
  gap: 12px 24px;
606
606
  margin: 0;
607
+ padding: 0;
607
608
  }
608
609
 
609
610
  /* The server draws every dt as a 130px right-aligned inline block and pads
@@ -621,6 +622,7 @@ export default {
621
622
  padding: 0;
622
623
  color: var(--color-text-maxcontrast);
623
624
  font-size: 13px;
625
+ line-height: 1.2;
624
626
  }
625
627
 
626
628
  .cn-retention__fact dd {
@@ -275,6 +275,7 @@
275
275
  type="number"
276
276
  :min="field.min"
277
277
  :max="field.max"
278
+ :step="numberStep"
278
279
  :modelValue="stringValue"
279
280
  :readonly="field.readOnly"
280
281
  :error="!!error"
@@ -424,6 +425,20 @@ export default {
424
425
  return INPUT_TYPES.includes(this.kind) ? this.kind : 'text'
425
426
  },
426
427
 
428
+ /**
429
+ * The number input's step: the field's own, 1 for a whole number, else
430
+ * any, so a decimal value (0.8) is not invalid to the browser.
431
+ *
432
+ * @return {string}
433
+ * @spec openspec/changes/settings-number-decimal-step/specs/settings-fields/spec.md#requirement-a-number-setting-steps-as-it-is-declared
434
+ */
435
+ numberStep() {
436
+ if (typeof this.field.step === 'number' || (typeof this.field.step === 'string' && this.field.step !== '')) {
437
+ return String(this.field.step)
438
+ }
439
+ return this.field.integer ? '1' : 'any'
440
+ },
441
+
427
442
  /** @return {string} The label. */
428
443
  label() {
429
444
  return String(this.field.label || this.field.key || '')
@@ -1566,12 +1566,17 @@ export default {
1566
1566
 
1567
1567
  .cn-tabs-widget__panel-heading-text {
1568
1568
  display: flex;
1569
- flex: 1 1 260px;
1570
1569
  flex-direction: column;
1571
1570
  gap: 4px;
1572
1571
  min-width: 0;
1573
1572
  }
1574
1573
 
1574
+ /* The 260px basis is a width in the actions row; in the column heading it
1575
+ would be a height. */
1576
+ .cn-tabs-widget__panel-heading--actions > .cn-tabs-widget__panel-heading-text {
1577
+ flex: 1 1 260px;
1578
+ }
1579
+
1575
1580
  .cn-tabs-widget__panel-actions {
1576
1581
  display: flex;
1577
1582
  justify-content: flex-end;
@@ -118,9 +118,10 @@ import CnConfirmDialog from '../../dialogs/CnConfirmDialog.vue'
118
118
  import CnDataTable from '../CnDataTable/CnDataTable.vue'
119
119
  import { getByPath, useEndpointSource } from '../../composables/useEndpointSource.js'
120
120
  import { useObjectStore } from '../../store/useObjectStore.js'
121
- import { dispatchAction, resolveObjectOpType } from '../../utils/actionsDispatcher.js'
121
+ import { dispatchAction, interpolateActionTarget, resolveObjectOpType } from '../../utils/actionsDispatcher.js'
122
122
  import { resolveObjectTokenContext } from '../../utils/detailObjectContext.js'
123
123
  import { headerFilterParams, nextHeaderFilters, withSortableColumns } from '../../utils/headerFilterState.js'
124
+ import { localHeaderRows } from '../../utils/localHeaderRows.js'
124
125
  import { pageFilter, pageSort } from '../../utils/pageControls.js'
125
126
  import { readPath } from '../../utils/readPath.js'
126
127
  import { dropOptionalUnresolved, hasUnresolvedTokens, resolveFilterTokens } from '../../utils/resolveFilterTokens.js'
@@ -869,9 +870,9 @@ export default {
869
870
  if (this.endpointActive && this.columns.some((col) => col && typeof col === 'object' && col.labelPath)) {
870
871
  inner.columns = this.labelledColumns(inner.columns || this.columns)
871
872
  }
872
- // Header sort and filter (`sortable`, `filterable`) only where the
873
- // widget fetches its own rows: the server sorts and filters.
874
- if (this.selfFetchActive && (this.sortable !== false || this.filterable)) {
873
+ // Header sort and filter (`sortable`, `filterable`): the server sorts
874
+ // and filters a self-fetch; endpoint rows are sorted and filtered here.
875
+ if ((this.selfFetchActive || this.endpointActive) && (this.sortable !== false || this.filterable)) {
875
876
  inner.columns = withSortableColumns(inner.columns || this.columns, this.sortable)
876
877
  inner.filterable = this.filterable
877
878
  inner.activeFilters = this.headerFilters
@@ -889,7 +890,7 @@ export default {
889
890
  }
890
891
  }
891
892
  if (this.endpointActive) {
892
- inner.rows = this.endpointRows
893
+ inner.rows = localHeaderRows(this.endpointRows, this.headerSort, this.headerFilters)
893
894
  inner.loading = this.epLoading || this.loading
894
895
  }
895
896
  if (this.rowRoute) {
@@ -1159,9 +1160,13 @@ export default {
1159
1160
  return { ok: false, error: '' }
1160
1161
  }
1161
1162
  if (action.type !== 'object-op') {
1162
- const wrapped = (!action.type || action.type === 'handler')
1163
- ? { ...action, args: [...(action.args || []), row] }
1164
- : action
1163
+ let wrapped = action
1164
+ if (!action.type || action.type === 'handler') {
1165
+ wrapped = { ...action, args: [...(action.args || []), row] }
1166
+ } else if (action.type === 'navigate' && row) {
1167
+ // A row's navigate reads that row: `{objectId}` / `@objectId` is its id, `@object.<field>` its fields.
1168
+ wrapped = { ...action, target: interpolateActionTarget(action.target, { objectId: row.id, object: row }) }
1169
+ }
1165
1170
  if (typeof this.cnDispatchAction === 'function') {
1166
1171
  this.cnDispatchAction(wrapped)
1167
1172
  } else {
@@ -880,15 +880,23 @@ export default {
880
880
  */
881
881
  /**
882
882
  * The value an input starts with: `content.default`, else the current
883
- * quarter or year for those types, else empty.
883
+ * quarter or year for those types, else empty. For a quarter or year
884
+ * input, `default: "previous"` is the last complete one (DqTermijnen
885
+ * opens on the last complete quarter).
884
886
  *
885
887
  * @return {string}
886
888
  */
887
889
  inputDefault() {
890
+ const today = new Date()
891
+ if (this.content.default === 'previous' && this.inputType === 'quarter') {
892
+ return quarterKey(new Date(today.getFullYear(), today.getMonth() - 3, 1))
893
+ }
894
+ if (this.content.default === 'previous' && this.inputType === 'year') {
895
+ return String(today.getFullYear() - 1)
896
+ }
888
897
  if (this.content.default !== undefined && this.content.default !== null) {
889
898
  return String(this.content.default)
890
899
  }
891
- const today = new Date()
892
900
  if (this.inputType === 'quarter') {
893
901
  return quarterKey(today)
894
902
  }
@@ -1109,7 +1109,9 @@ function fillRowTemplate(template, row) {
1109
1109
  * template, "{case.title}") or `linkedField` (a path) fills the "Linked"
1110
1110
  * column, drawn when a kind declares one; `sourceConfig.linkedLabel` names it.
1111
1111
  * `subtitleTemplate` ("Case {identifier}", translated) fills the line under a
1112
- * result, else `subtitleField`.
1112
+ * result, else `subtitleField`; `subtitleResolve` (`{ field, schema, labelField,
1113
+ * as }`) names a referenced object for it as `{as}`. `sourceConfig.titleWrap`
1114
+ * wraps a long result title instead of cutting it.
1113
1115
  * `linkedUser: true` reads that value as a user id and shows the user's
1114
1116
  * display name. `sourceConfig.columnWidths` sets a width per column key.
1115
1117
  * The page's title and count line can name the term as `{term}`, and the
@@ -1226,9 +1228,32 @@ function searchSource(config = {}, context = {}) {
1226
1228
  return ''
1227
1229
  }
1228
1230
 
1229
- const subtitleOf = (kind, object) => {
1231
+ // `subtitleResolve`: `{ field, schema, register?, labelField?, as }` names
1232
+ // the object a reference points at, for the subtitle template as `{as}`.
1233
+ const subtitleLabels = async (kind) => {
1234
+ const resolve = kind.subtitleResolve
1235
+ if (!resolve || !resolve.schema || !resolve.field) {
1236
+ return null
1237
+ }
1238
+ const objects = await fetchReferenceObjects({ register: resolve.register || kind.register, schema: resolve.schema }).catch(() => [])
1239
+ const labels = new Map()
1240
+ for (const object of objects) {
1241
+ const id = object.id || object.uuid || (object['@self'] && object['@self'].id)
1242
+ const label = readPath(object, resolve.labelField || 'title') ?? object.name
1243
+ if (id !== undefined && label !== undefined && label !== null) {
1244
+ labels.set(String(id), String(label))
1245
+ }
1246
+ }
1247
+ return labels
1248
+ }
1249
+
1250
+ const subtitleOf = (kind, object, labels = null) => {
1230
1251
  if (kind.subtitleTemplate) {
1231
- return fillTemplate(translate(kind.subtitleTemplate), object)
1252
+ const resolve = kind.subtitleResolve
1253
+ const data = labels && resolve
1254
+ ? { ...object, [resolve.as || resolve.field]: labels.get(String(readPath(object, resolve.field) ?? '')) ?? '' }
1255
+ : object
1256
+ return fillTemplate(translate(kind.subtitleTemplate), data)
1232
1257
  }
1233
1258
  return kind.subtitleField ? String(readPath(object, kind.subtitleField) ?? '') : ''
1234
1259
  }
@@ -1247,6 +1272,7 @@ function searchSource(config = {}, context = {}) {
1247
1272
  const results = Array.isArray(body.results) ? body.results : (Array.isArray(body) ? body : [])
1248
1273
  const labels = kind.statusResolve && kind.statusField ? await statusLabels(kind) : null
1249
1274
  const userNames = kind.linkedUser === true ? await linkedUserNames(kind, results) : null
1275
+ const subtitleNames = await subtitleLabels(kind)
1250
1276
  return {
1251
1277
  total: typeof body.total === 'number' ? body.total : results.length,
1252
1278
  rows: results.map((object) => {
@@ -1261,7 +1287,7 @@ function searchSource(config = {}, context = {}) {
1261
1287
  kindLabel: kind.label,
1262
1288
  kindText: kindTextOf(kind, object),
1263
1289
  title: String(readPath(object, kind.titleField || 'title') ?? object.name ?? id ?? ''),
1264
- subtitle: subtitleOf(kind, object),
1290
+ subtitle: subtitleOf(kind, object, subtitleNames),
1265
1291
  status,
1266
1292
  linked: userNames ? (userNames.get(linkedOf(kind, object)) || linkedOf(kind, object)) : linkedOf(kind, object),
1267
1293
  href: kind.href ? generateUrl(fillRowTemplate(kind.href, { ...object, id })) : '',
@@ -1372,7 +1398,7 @@ function searchSource(config = {}, context = {}) {
1372
1398
  loading: () => state.loading,
1373
1399
 
1374
1400
  columns: withColumnWidths([
1375
- { key: 'title', label: t('nextcloud-vue', 'Result'), secondary: 'subtitle', sortable: true },
1401
+ { key: 'title', label: t('nextcloud-vue', 'Result'), secondary: 'subtitle', sortable: true, ...(config && config.titleWrap === true ? { cellClass: 'cn-cell--wrap' } : {}) },
1376
1402
  // `kindAsText`: what one row is, as plain text ("Zaak", "Lantaarnpaal",
1377
1403
  // DqZoekresultaten), instead of the plural kind as a pill.
1378
1404
  config && config.kindAsText === true
@@ -1619,10 +1645,10 @@ function usersSource() {
1619
1645
  loading: () => state.loading,
1620
1646
 
1621
1647
  columns: [
1622
- { key: 'displayname', label: t('nextcloud-vue', 'Employee'), secondary: 'function' },
1623
- { key: 'roles', label: t('nextcloud-vue', 'Roles') },
1624
- { key: 'email', label: t('nextcloud-vue', 'E-mail') },
1625
- { key: 'today', label: t('nextcloud-vue', 'Today'), widget: 'badge', widgetProps: { colorMap: presenceColors, variant: 'warning' } },
1648
+ { key: 'displayname', label: t('nextcloud-vue', 'Employee'), secondary: 'function', sortable: true, cellClass: 'cn-cell--lead' },
1649
+ { key: 'roles', label: t('nextcloud-vue', 'Roles'), sortable: true },
1650
+ { key: 'email', label: t('nextcloud-vue', 'E-mail'), sortable: true },
1651
+ { key: 'today', label: t('nextcloud-vue', 'Today'), widget: 'badge', widgetProps: { colorMap: presenceColors, variant: 'warning' }, sortable: true },
1626
1652
  ],
1627
1653
 
1628
1654
  searchFields: {
@@ -177,11 +177,14 @@ body:has(.cn-look-board):not(:has(.cn-look-nextcloud)) .modal-mask {
177
177
  color: var(--color-text-maxcontrast);
178
178
  }
179
179
 
180
- /* The saved-view edit and share forms start 22px under the subtitle, as
181
- the Dq dialog boards draw a first field: the body's 16px gap and 6px. */
180
+ /* A body starts 22px under the subtitle, as the Dq dialog boards draw a
181
+ first field: the header's foot and the body gap alone, so the saved-view
182
+ edit and share forms and the confirm sentence pad nothing on top
183
+ (r29 on :8080: 28, 28 and 26px with their own padding). */
182
184
  .cn-dialog-board .dialog__content .cn-edit-view,
183
- .cn-dialog-board .dialog__content .cn-share-view {
184
- padding-top: 6px;
185
+ .cn-dialog-board .dialog__content .cn-share-view,
186
+ .cn-dialog-board .dialog__content .cn-confirm__confirm {
187
+ padding-top: 0;
185
188
  }
186
189
 
187
190
  /* The close button is a 36px circle. NcButton sizes an icon-only button by
@@ -218,7 +221,9 @@ body:has(.cn-look-board):not(:has(.cn-look-nextcloud)) .modal-mask {
218
221
  10px 14px padding, the checkbox row at the 24px minimum target instead of
219
222
  NcCheckboxRadioSwitch's 34px, the note at 1.2 line height; the card's
220
223
  corners 8px and the label on an 18px line, as the board draws them. */
221
- .cn-dialog-board .cn-assign-dialog__toggle {
224
+ /* `.dialog` outranks the component's scoped rule (class + attribute), which
225
+ loads after this sheet and kept 12px 14px and 12px corners on :8080 (r29). */
226
+ .cn-dialog-board .dialog .cn-assign-dialog__toggle {
222
227
  padding: 10px 14px;
223
228
  border-radius: 8px;
224
229
  }
@@ -272,6 +277,13 @@ body:has(.cn-look-board):not(:has(.cn-look-nextcloud)) .modal-mask {
272
277
  border-top: 1px solid var(--color-border);
273
278
  }
274
279
 
280
+ /* NcDialog's scoped `.dialog__actions[data-v-…]` margin (6px 0 12px) has the
281
+ rule above's specificity and, loaded later, put the footer 6px low under the
282
+ body (DqVolgendeStap). This one outranks it. */
283
+ .cn-dialog-board .dialog .dialog__actions {
284
+ margin: 0;
285
+ }
286
+
275
287
  .cn-dialog-board .dialog__actions .button-vue,
276
288
  .cn-dialog-board .dialog__actions button {
277
289
  min-height: var(--cn-board-control-height, 40px);
@@ -347,10 +359,14 @@ body:has(.cn-look-board):not(:has(.cn-look-nextcloud)) .modal-mask {
347
359
  border and radius 8 (PlanItemDialog, DqWerkvoorraad). NcInputField draws
348
360
  its border as a 65%-transparent shadow, which on the white dialog read as
349
361
  an empty band, and floats its label inside the border. A field with
350
- `labelOutside` keeps the app's own label and takes the same control. */
362
+ `labelOutside` keeps the app's own label and takes the same control. The
363
+ label's box starts where the field starts and the control 23px under it,
364
+ as on every dialog board (72 fields on 11 boards): the label at the font's
365
+ normal line, 21px of room plus the control's 2px focus inset, the label
366
+ 4px above that room's end. */
351
367
  .cn-dialog-board .dialog__content .input-field:has(.input-field__label),
352
368
  .cn-dialog-board .dialog__content .textarea:has(.textarea__label) {
353
- margin-block-start: 26px;
369
+ margin-block-start: 21px;
354
370
  }
355
371
 
356
372
  /* NcSelect's search is an NcInputField inside the select, and the select's
@@ -359,7 +375,7 @@ body:has(.cn-look-board):not(:has(.cn-look-nextcloud)) .modal-mask {
359
375
  select makes the label's room instead.
360
376
  @spec openspec/changes/board-dialog-select-label-room/specs/board-dialog-select-label-room/spec.md#requirement-a-board-dialog-select-makes-room-for-its-raised-label */
361
377
  .cn-dialog-board .dialog__content .v-select:has(.input-field__label) {
362
- margin-block-start: 26px;
378
+ margin-block-start: 21px;
363
379
  }
364
380
 
365
381
  /* A select in a form dialog sits under its label and over its help line
@@ -420,7 +436,7 @@ body:has(.cn-look-board):not(:has(.cn-look-nextcloud)) .modal-mask {
420
436
  color: var(--color-main-text);
421
437
  font-size: var(--cn-field-label-size, 14px);
422
438
  font-weight: 600;
423
- line-height: 20px;
439
+ line-height: normal;
424
440
  pointer-events: auto;
425
441
  transition: none;
426
442
  }