@conduction/nextcloud-vue 2.142.0 → 2.143.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 (87) hide show
  1. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue.js +1 -1
  2. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js +27 -0
  3. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue2.js.map +1 -1
  4. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js +1 -1
  5. package/dist/esm/components/CnAdminSettingsShell/CnAdminSettingsShell.vue3.js.map +1 -1
  6. package/dist/esm/components/CnAiCompanion/CnAiChatPanel.vue2.js.map +1 -1
  7. package/dist/esm/components/CnAiCompanion/CnAiChatPanel.vue3.js +2 -1
  8. package/dist/esm/components/CnAiCompanion/CnAiChatPanel.vue3.js.map +1 -1
  9. package/dist/esm/components/CnAiCompanion/CnAiInput.vue2.js +9 -0
  10. package/dist/esm/components/CnAiCompanion/CnAiInput.vue2.js.map +1 -1
  11. package/dist/esm/components/CnAiCompanion/CnAiInput.vue3.js +21 -22
  12. package/dist/esm/components/CnAiCompanion/CnAiInput.vue3.js.map +1 -1
  13. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue.js +1 -1
  14. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js +29 -1
  15. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue2.js.map +1 -1
  16. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue3.js +28 -13
  17. package/dist/esm/components/CnAuditTrailCard/CnAuditTrailCard.vue3.js.map +1 -1
  18. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue.js +1 -1
  19. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue2.js.map +1 -1
  20. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue3.js +2 -1
  21. package/dist/esm/components/CnAuditTrailWidget/CnAuditTrailWidget.vue3.js.map +1 -1
  22. package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue.js +1 -1
  23. package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue2.js.map +1 -1
  24. package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue3.js.map +1 -1
  25. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue.js +1 -1
  26. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js.map +1 -1
  27. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js.map +1 -1
  28. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue.js +1 -1
  29. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  30. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +2 -1
  31. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
  32. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue.js +1 -1
  33. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +27 -2
  34. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
  35. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +4 -2
  36. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
  37. package/dist/esm/components/CnFilesBrowser/filesBrowser.js +17 -4
  38. package/dist/esm/components/CnFilesBrowser/filesBrowser.js.map +1 -1
  39. package/dist/esm/components/CnFilesBrowser/filesBrowserColumns.js +21 -1
  40. package/dist/esm/components/CnFilesBrowser/filesBrowserColumns.js.map +1 -1
  41. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +15 -1
  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/CnPageHeader/CnHeaderTrail.vue2.js +6 -1
  45. package/dist/esm/components/CnPageHeader/CnHeaderTrail.vue2.js.map +1 -1
  46. package/dist/esm/components/CnPageHeader/CnHeaderTrail.vue3.js.map +1 -1
  47. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
  48. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +12 -1
  49. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  50. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +2 -2
  51. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  52. package/dist/esm/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue.js +1 -1
  53. package/dist/esm/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue2.js.map +1 -1
  54. package/dist/esm/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue3.js.map +1 -1
  55. package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue.js +1 -1
  56. package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue2.js +4 -0
  57. package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue2.js.map +1 -1
  58. package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue3.js +18 -8
  59. package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue3.js.map +1 -1
  60. package/dist/esm/l10n/nl.json.js +1 -1
  61. package/dist/esm/library-style.js +1 -1
  62. package/dist/esm/nextcloud-vue.css +537 -322
  63. package/dist/nextcloud-vue.cjs.js +300 -119
  64. package/dist/nextcloud-vue.cjs.js.map +2 -2
  65. package/dist/nextcloud-vue.css +537 -322
  66. package/l10n/nl.json +1 -1
  67. package/package.json +1 -1
  68. package/src/components/CnAdminSettingsShell/CnAdminSettingsShell.vue +28 -1
  69. package/src/components/CnAiCompanion/CnAiChatPanel.vue +12 -0
  70. package/src/components/CnAiCompanion/CnAiInput.vue +32 -1
  71. package/src/components/CnAuditTrailCard/CnAuditTrailCard.vue +34 -4
  72. package/src/components/CnAuditTrailWidget/CnAuditTrailWidget.vue +1 -0
  73. package/src/components/CnChecklistRunWidget/CnChecklistRunWidget.vue +8 -0
  74. package/src/components/CnChoiceCards/CnChoiceCards.vue +1 -0
  75. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +1 -0
  76. package/src/components/CnFilesBrowser/CnFilesBrowser.vue +28 -3
  77. package/src/components/CnFilesBrowser/filesBrowser.js +17 -4
  78. package/src/components/CnFilesBrowser/filesBrowserColumns.js +20 -1
  79. package/src/components/CnIndexPage/CnIndexPage.vue +15 -1
  80. package/src/components/CnPageHeader/CnHeaderTrail.vue +6 -1
  81. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +21 -1
  82. package/src/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue +6 -0
  83. package/src/components/CnTimelineWidget/CnTimelineManage.vue +10 -1
  84. package/src/css/dialog.css +18 -0
  85. package/src/css/look-board-detail.css +31 -0
  86. package/src/css/look-board-index.css +42 -1
  87. package/src/css/look-board.css +73 -1
package/l10n/nl.json CHANGED
@@ -1910,7 +1910,7 @@
1910
1910
  "Now": "Nu",
1911
1911
  "Tell me above a number": "Melden vanaf een aantal",
1912
1912
  "Threshold": "Grens",
1913
- "A message to you when there are {threshold} or more": "Bericht aan u als er {threshold} of meer zijn",
1913
+ "A message to you when there are {threshold} or more": "Bericht aan u als er {threshold} of meer open zijn",
1914
1914
  "Save this search or open a saved one to set an alert on it.": "Sla deze zoekopdracht op of open een opgeslagen zoekopdracht om er een melding op te zetten.",
1915
1915
  "The alert could not be saved.": "De melding kon niet worden opgeslagen.",
1916
1916
  "Your exports could not be loaded.": "Uw exports konden niet worden geladen.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@conduction/nextcloud-vue",
3
- "version": "2.142.0",
3
+ "version": "2.143.0",
4
4
  "description": "Shared Vue component library for Conduction Nextcloud apps — complements @nextcloud/vue with higher-level components, OpenRegister integration, and NL Design System support",
5
5
  "homepage": "https://github.com/ConductionNL/nextcloud-vue#readme",
6
6
  "bugs": {
@@ -2,7 +2,7 @@
2
2
  <div
3
3
  class="cn-admin-settings-shell"
4
4
  :class="[ownLookClass, sectionColumns ? `cn-admin-settings-shell--columns-${sectionColumns}` : '']"
5
- :style="versionTitleSize ? { '--cn-board-version-title-size': `${versionTitleSize}px` } : null">
5
+ :style="shellStyle">
6
6
  <!-- Names a development, test or acceptance environment; cannot be dismissed. Production shows nothing. -->
7
7
  <CnEnvironmentBanner v-if="environmentName" :environment="environmentName" />
8
8
 
@@ -780,6 +780,16 @@ export default {
780
780
  type: Number,
781
781
  default: 0,
782
782
  },
783
+
784
+ /**
785
+ * The board look's gap in px between a card's title and its
786
+ * description, when the app's board draws it other than 4px; the
787
+ * content follows 10px further (DqBeheerinstellingen 6, so 16).
788
+ */
789
+ descGap: {
790
+ type: Number,
791
+ default: 0,
792
+ },
783
793
  },
784
794
 
785
795
  emits: ['update', 'reimported', 'reimport-error', 'tab-change'],
@@ -818,6 +828,23 @@ export default {
818
828
  },
819
829
 
820
830
  computed: {
831
+ /**
832
+ * The board look sizes an app's board sets on the shell
833
+ * (`versionTitleSize`, `descGap`), as CSS custom properties.
834
+ *
835
+ * @return {object|null} The style, or null when none is set.
836
+ */
837
+ shellStyle() {
838
+ const style = {}
839
+ if (this.versionTitleSize) {
840
+ style['--cn-board-version-title-size'] = `${this.versionTitleSize}px`
841
+ }
842
+ if (this.descGap) {
843
+ style['--cn-board-settings-desc-gap'] = `${this.descGap}px`
844
+ }
845
+ return Object.keys(style).length > 0 ? style : null
846
+ },
847
+
821
848
  /**
822
849
  * The tabs that have an id and a label.
823
850
  *
@@ -310,6 +310,7 @@
310
310
  :disabled="streamState.isStreaming"
311
311
  :placeholder="inputPlaceholder"
312
312
  :sendLabel="sendLabel"
313
+ :stacked="titled"
313
314
  :chatAppId="chatAppId"
314
315
  :speechInputEngine="speechPolicy.inputEngine"
315
316
  :dictationSilenceTimeout="speechPolicy.silenceTimeout"
@@ -1599,10 +1600,21 @@ export default {
1599
1600
  }
1600
1601
 
1601
1602
  .cn-ai-chat-window--titled .cn-ai-chat-window__input {
1603
+ padding: 0;
1604
+ border-top: 0;
1602
1605
  border-radius: 0;
1603
1606
  background: transparent;
1604
1607
  }
1605
1608
 
1609
+ /* In a card the panel's body takes the card's 16px 18px inset. */
1610
+ .cn-ai-chat-window--titled .cn-ai-chat-window__messages {
1611
+ padding: 16px 18px;
1612
+ }
1613
+
1614
+ .cn-ai-chat-window--titled .cn-ai-chat-window__note {
1615
+ margin: 0 18px 16px;
1616
+ }
1617
+
1606
1618
  .cn-ai-chat-window__input {
1607
1619
  flex: 0 0 auto;
1608
1620
  padding: 8px;
@@ -23,7 +23,7 @@
23
23
  for the parent's async request to resolve before clearing either).
24
24
  -->
25
25
  <template>
26
- <div class="cn-ai-input" data-testid="cn-ai-input">
26
+ <div class="cn-ai-input" :class="{ 'cn-ai-input--stacked': stacked }" data-testid="cn-ai-input">
27
27
  <NcNoteCard
28
28
  v-if="uploadError"
29
29
  type="error"
@@ -298,6 +298,15 @@ export default {
298
298
  default: '',
299
299
  },
300
300
 
301
+ /**
302
+ * The narrow layout of a card: the text box and the send button on the
303
+ * first line, dictation and attach on a line under it.
304
+ */
305
+ stacked: {
306
+ type: Boolean,
307
+ default: false,
308
+ },
309
+
301
310
  /**
302
311
  * Backend app id the attach control uploads files to
303
312
  * (`POST /index.php/apps/{chatAppId}/api/chat/attachments`). See
@@ -1184,6 +1193,28 @@ export default {
1184
1193
  align-items: flex-end;
1185
1194
  }
1186
1195
 
1196
+ /* `stacked`: in a card the box keeps the line, dictation and attach wrap
1197
+ under it (DqZaakAssistent "Vraag iets over deze zaak" + "Vragen"). */
1198
+ .cn-ai-input--stacked {
1199
+ padding: 12px 18px;
1200
+ }
1201
+
1202
+ .cn-ai-input--stacked .cn-ai-input__row {
1203
+ flex-wrap: wrap;
1204
+ align-items: center;
1205
+ }
1206
+
1207
+ .cn-ai-input--stacked .cn-ai-input__textarea {
1208
+ order: -2;
1209
+ flex: 1 1 calc(100% - 104px);
1210
+ min-width: 0;
1211
+ min-height: 40px;
1212
+ }
1213
+
1214
+ .cn-ai-input--stacked .cn-ai-input__send-button {
1215
+ order: -1;
1216
+ }
1217
+
1187
1218
  .cn-ai-input__file-input {
1188
1219
  display: none;
1189
1220
  }
@@ -30,13 +30,15 @@
30
30
  <li
31
31
  v-for="entry in displayedEntries"
32
32
  :key="entry.id"
33
- class="cn-audit-card__row">
33
+ class="cn-audit-card__row"
34
+ :class="{ 'cn-audit-card__row--stacked': layout === 'stacked' }">
34
35
  <div class="cn-audit-card__row-head">
35
36
  <strong class="cn-audit-card__action">{{ formatAction(entry) }}</strong>
36
- <span class="cn-audit-card__when">{{ formatWhen(entry) }}</span>
37
+ <span v-if="layout !== 'stacked'" class="cn-audit-card__when">{{ formatWhen(entry) }}</span>
37
38
  </div>
38
39
  <div class="cn-audit-card__row-body">
39
- <span class="cn-audit-card__actor">{{ formatActor(entry) }}</span>
40
+ <span v-if="layout === 'stacked'" class="cn-audit-card__when" data-testid="cn-audit-card-when-below">{{ stackedLine(entry) }}</span>
41
+ <span v-else class="cn-audit-card__actor">{{ formatActor(entry) }}</span>
40
42
  <span
41
43
  v-if="entry.authorized === false"
42
44
  class="cn-audit-card__refused"
@@ -64,8 +66,10 @@ import { NcLoadingIcon } from '@nextcloud/vue'
64
66
  import { markRaw } from 'vue'
65
67
  import History from 'vue-material-design-icons/History.vue'
66
68
  import CnDetailCard from '../CnDetailCard/CnDetailCard.vue'
69
+ import { formatBoardDate, formatTimeOfDay } from '../../utils/boardDate.js'
67
70
  import { buildHeaders, prefixUrl } from '../../utils/index.js'
68
71
  import { leafSourceUrl } from '../../utils/leafSourceUrl.js'
72
+ import { userDisplayName } from '../../utils/userDisplayName.js'
69
73
  import { userLocale } from '../../utils/userLocale.js'
70
74
 
71
75
  /**
@@ -154,6 +158,12 @@ export default {
154
158
 
155
159
  /** Maximum rows to render. */
156
160
  maxDisplay: { type: Number, default: 5 },
161
+ /**
162
+ * `stacked` (DqTaak "Historie"): the action, then one muted line under
163
+ * it with the moment ("6 okt 09:05") and who, when the action does not
164
+ * already name them. Default: the moment beside the action, who under it.
165
+ */
166
+ layout: { type: String, default: '' },
157
167
  /**
158
168
  * Actions the card leaves out (case-insensitive), such as `['read']`
159
169
  * for a history of what changed. It then reads more entries than it
@@ -440,7 +450,27 @@ export default {
440
450
  * @return {string} Who did it, by name where the entry carries one.
441
451
  */
442
452
  actorName(entry) {
443
- return entry.actorDisplayName || entry.actor || entry.userId || entry.user || ''
453
+ if (entry.actorDisplayName) {
454
+ return entry.actorDisplayName
455
+ }
456
+ const id = entry.actor || entry.userId || entry.user || ''
457
+ // A person's user id reads as their display name once it is known.
458
+ return (id && (!entry.performerType || entry.performerType === 'user') && userDisplayName(String(id))) || id
459
+ },
460
+
461
+ /**
462
+ * The muted line of a stacked row: the moment in the board's short
463
+ * form ("6 okt 09:05") and who, unless the action names them already.
464
+ *
465
+ * @param {object} entry The entry.
466
+ * @return {string} The line.
467
+ * @spec openspec/changes/audit-card-stacked-rows/specs/audit-trail/spec.md#requirement-an-audit-row-can-put-its-moment-under-the-action
468
+ */
469
+ stackedLine(entry) {
470
+ const raw = entry.creationDateTime || entry.created || entry.timestamp
471
+ const date = raw ? new Date(raw) : null
472
+ const when = date && !Number.isNaN(date.getTime()) ? `${formatBoardDate(date)} ${formatTimeOfDay(date)}` : ''
473
+ return [when, this.formatActor(entry)].filter((part) => part !== '').join(' · ')
444
474
  },
445
475
 
446
476
  /**
@@ -16,6 +16,7 @@
16
16
  :scope="resolvedScope"
17
17
  :title="resolvedTitle"
18
18
  :maxDisplay="resolvedMaxDisplay"
19
+ :layout="content && typeof content.layout === 'string' ? content.layout : ''"
19
20
  :excludeActions="resolvedExcludeActions"
20
21
  :actionLabels="actionLabels"
21
22
  :valueLabels="valueLabels"
@@ -1195,18 +1195,25 @@ export default {
1195
1195
  }
1196
1196
 
1197
1197
  .cn-checklist-run__legend {
1198
+ font-size: 15px;
1198
1199
  font-weight: 600;
1200
+ line-height: 1.2;
1199
1201
  padding: 0 4px;
1200
1202
  }
1201
1203
 
1202
1204
  .cn-checklist-run__help {
1203
1205
  color: var(--color-text-maxcontrast);
1206
+ font-size: 14px;
1204
1207
  margin: 0;
1205
1208
  }
1206
1209
 
1210
+ /* An option reads as text, not as a field label (a theme makes every label
1211
+ bold); 15px as the board draws it (DqInspectie). */
1207
1212
  .cn-checklist-run__choice {
1208
1213
  align-items: center;
1209
1214
  display: flex;
1215
+ font-size: 15px;
1216
+ font-weight: 400;
1210
1217
  gap: 10px;
1211
1218
  min-height: 34px;
1212
1219
  }
@@ -1225,6 +1232,7 @@ export default {
1225
1232
  }
1226
1233
 
1227
1234
  .cn-checklist-run__group {
1235
+ font-size: 14px;
1228
1236
  font-weight: 600;
1229
1237
  margin: 0;
1230
1238
  }
@@ -622,6 +622,7 @@ export default {
622
622
  .cn-choice-cards__row-title {
623
623
  font-size: 15px;
624
624
  font-weight: 600;
625
+ line-height: 1.2;
625
626
  }
626
627
 
627
628
  /* Regular weight on the span itself: a theme that sets every label bold
@@ -86,6 +86,7 @@
86
86
  <CnObjectDataWidget
87
87
  v-else-if="isData && schemaObject"
88
88
  :title="resolvedTitle"
89
+ :subtitle="widgetSubtitle"
89
90
  :chromeless="isBare"
90
91
  :showActions="!isBare && actionsShown"
91
92
  :icon="widget.icon || null"
@@ -38,7 +38,7 @@
38
38
  Files app and its plugins registered for this folder (a new folder,
39
39
  a template, a file request) beside a plain upload. -->
40
40
  <div class="cn-files-browser__bar">
41
- <NcBreadcrumbs class="cn-files-browser__crumbs">
41
+ <NcBreadcrumbs class="cn-files-browser__crumbs" :class="{ 'cn-files-browser__crumbs--at-root': atRoot }">
42
42
  <!-- The whole trail from the user's files root, as the Files app
43
43
  draws it. A crumb above the browser's root links into the Files
44
44
  app (the browser never leaves its root); the root and everything
@@ -1471,6 +1471,19 @@ export default {
1471
1471
  return crumbsFor(this.rootPath, this.currentPath, this.rootLabel)
1472
1472
  },
1473
1473
 
1474
+ /**
1475
+ * Whether the open folder is the browser's root: the trail then ends on
1476
+ * the root itself (the board look draws no trail there).
1477
+ *
1478
+ * @return {boolean}
1479
+ * @spec openspec/changes/board-files-root-crumb/specs/board-files-root-crumb/spec.md#requirement-the-board-files-bar-draws-no-trail-at-the-root
1480
+ */
1481
+ atRoot() {
1482
+ const root = String(this.rootPath || '/').replace(/\/+$/, '') || '/'
1483
+ const last = this.crumbs[this.crumbs.length - 1]
1484
+ return Boolean(last) && last.path === root
1485
+ },
1486
+
1474
1487
  /**
1475
1488
  * The children in the chosen order, folders first the way the Files app
1476
1489
  * lists them.
@@ -2768,7 +2781,8 @@ export default {
2768
2781
  }
2769
2782
  })
2770
2783
  .filter((field) => field.value !== null && field.value !== undefined && field.value !== ''),
2771
- actions: (Array.isArray(card.actions) ? card.actions : []).filter((action) => action && action.id),
2784
+ actions: (Array.isArray(card.actions) ? card.actions : [])
2785
+ .filter((action) => action && action.id && hostActionApplies(action, node, this.rowValueOf(node))),
2772
2786
  }))
2773
2787
  },
2774
2788
 
@@ -3064,7 +3078,18 @@ export default {
3064
3078
  * @spec openspec/changes/files-browser-hosts-a-documents-list/specs/files-browser/spec.md
3065
3079
  */
3066
3080
  hostActionsFor(node) {
3067
- return this.rowActions.filter((action) => hostActionApplies(action, node))
3081
+ return this.rowActions.filter((action) => hostActionApplies(action, node, this.rowValueOf(node)))
3082
+ },
3083
+
3084
+ /**
3085
+ * A reader of the host's row fields for one file, for `visibleIf.row`.
3086
+ *
3087
+ * @param {object} node The file.
3088
+ * @return {(key: string) => unknown} `(key) => value` over the row the host gave for the file.
3089
+ * @spec openspec/changes/files-browser-action-visible-if-row/specs/files-browser/spec.md#requirement-a-host-action-can-be-offered-by-a-field-of-the-files-row
3090
+ */
3091
+ rowValueOf(node) {
3092
+ return (key) => fileColumnValue({ key, source: 'row' }, node, this.resolvedRowData)
3068
3093
  },
3069
3094
 
3070
3095
  /**
@@ -284,15 +284,19 @@ export function indexRowData(data, { path = '', key = 'fileId' } = {}) {
284
284
  *
285
285
  * `visibleIf.extension` lists file extensions (no case, a leading dot is
286
286
  * allowed); `visibleIf.mime` lists mime types, where one ending in `/` or
287
- * `/*` names the whole family. Every condition named must hold. An action
288
- * without `visibleIf` is offered on every file, as before. An action with
289
- * `when` is offered only while `when` is `true`.
287
+ * `/*` names the whole family; `visibleIf.row` maps fields of the host's row
288
+ * for the file to the value each must have (compared strictly, so `true` is
289
+ * not `"true"`). Every condition named must hold. An action without
290
+ * `visibleIf` is offered on every file, as before. An action with `when` is
291
+ * offered only while `when` is `true`.
290
292
  *
291
293
  * @param {object} action The declared action.
292
294
  * @param {object} node The file (`basename`, `mime`).
295
+ * @param {((key: string) => unknown)|null} rowValue Reads a field of the host's row for the file; without it a `row` condition fails.
293
296
  * @return {boolean} True when the action belongs on this file.
297
+ * @spec openspec/changes/files-browser-action-visible-if-row/specs/files-browser/spec.md#requirement-a-host-action-can-be-offered-by-a-field-of-the-files-row
294
298
  */
295
- export function hostActionApplies(action, node) {
299
+ export function hostActionApplies(action, node, rowValue = null) {
296
300
  // `when`: the action applies only while it is true (a case type flag).
297
301
  // @spec openspec/changes/files-review-row-when/specs/files-review-row-when/spec.md#requirement-a-files-browser-entry-can-apply-only-when-a-condition-holds
298
302
  if (action && typeof action === 'object' && 'when' in action && action.when !== true) {
@@ -324,5 +328,14 @@ export function hostActionApplies(action, node) {
324
328
  return false
325
329
  }
326
330
  }
331
+ if (condition.row && typeof condition.row === 'object') {
332
+ if (typeof rowValue !== 'function') {
333
+ return false
334
+ }
335
+ const holds = Object.entries(condition.row).every(([key, wanted]) => rowValue(key) === wanted)
336
+ if (!holds) {
337
+ return false
338
+ }
339
+ }
327
340
  return true
328
341
  }
@@ -3,6 +3,8 @@
3
3
  * SPDX-License-Identifier: AGPL-3.0-or-later
4
4
  */
5
5
 
6
+ import { isAppInstalled } from '../../utils/appInstalled.js'
7
+
6
8
  /**
7
9
  * The built-in columns a host may name by string, and what each one reads.
8
10
  *
@@ -53,12 +55,26 @@ export function isBuiltInFileColumn(name) {
53
55
  return typeof name === 'string' && Object.hasOwn(BUILT_IN_FILE_COLUMNS, name)
54
56
  }
55
57
 
58
+ /**
59
+ * Whether the app a column leans on is enabled: one app id, or a list of which
60
+ * any one will do. A column without `requiredApp` always applies.
61
+ *
62
+ * @param {(string|string[]|undefined)} requiredApp The app id(s).
63
+ * @return {boolean} True when the column applies.
64
+ * @spec openspec/changes/files-column-required-app/specs/files-column-required-app/spec.md#requirement-a-file-column-can-require-another-app
65
+ */
66
+ function requiredAppPresent(requiredApp) {
67
+ const ids = (Array.isArray(requiredApp) ? requiredApp : [requiredApp]).filter((id) => typeof id === 'string' && id !== '')
68
+ return ids.length === 0 || ids.some((id) => isAppInstalled(id))
69
+ }
70
+
56
71
  /**
57
72
  * Resolve a host's `columns` declaration into column objects.
58
73
  *
59
74
  * A string names a built-in. An object declares its own: `key`, `label`,
60
75
  * `source` (`node`, `attribute` or `row`), `attribute`, `formatter` and
61
- * `sortable`. An entry naming an unknown built-in is dropped here and refused
76
+ * `sortable`, and `requiredApp` (an app id, or a list of which any one will
77
+ * do; without it the column is left out). An entry naming an unknown built-in is dropped here and refused
62
78
  * by the manifest validator, so an author hears about it at authoring time
63
79
  * rather than finding a column missing at runtime.
64
80
  *
@@ -83,6 +99,9 @@ export function normaliseFileColumns(columns) {
83
99
  if ('when' in entry && entry.when !== true) {
84
100
  return
85
101
  }
102
+ if (!requiredAppPresent(entry.requiredApp)) {
103
+ return
104
+ }
86
105
  if (isBuiltInFileColumn(entry.key) && entry.source === undefined) {
87
106
  out.push({ ...BUILT_IN_FILE_COLUMNS[entry.key], ...entry, builtIn: true })
88
107
  return
@@ -8517,7 +8517,9 @@ export default {
8517
8517
  return [
8518
8518
  ...this.searchTermChips,
8519
8519
  ...this.quickFilterActiveChips,
8520
- ...this.filterChipsOf(this.effectiveActiveFilters, (key, value) => this.isFixedFilter(key, value) || ownToView(key, value)),
8520
+ // The folder pane's own filter is not a chip: the selected folder
8521
+ // shows in the pane and as the heading above the list (DqAfdelingen).
8522
+ ...this.filterChipsOf(this.effectiveActiveFilters, (key, value) => this.isFixedFilter(key, value) || ownToView(key, value) || this.isFolderFilter(key)),
8521
8523
  ]
8522
8524
  },
8523
8525
 
@@ -9703,6 +9705,18 @@ export default {
9703
9705
  * @return {boolean}
9704
9706
  * @spec openspec/changes/index-scope-presentation/specs/index-page/spec.md#requirement-the-query-that-chose-a-preset-is-not-a-removable-filter
9705
9707
  */
9708
+ /**
9709
+ * Whether a filter key is the folder pane's (`folderSidebar.filterField`).
9710
+ *
9711
+ * @param {string} key The filter key.
9712
+ * @return {boolean}
9713
+ * @spec openspec/changes/board-folder-scope-outside-card/specs/index-page/spec.md#requirement-the-selected-folder-heads-the-list-from-outside-its-card
9714
+ */
9715
+ isFolderFilter(key) {
9716
+ const field = this.folderSidebar && this.folderSidebar.filterField
9717
+ return typeof field === 'string' && field !== '' && key === field
9718
+ },
9719
+
9706
9720
  isFixedFilter(key, value) {
9707
9721
  const fixed = this.fixedFilters
9708
9722
  if (!fixed || !Object.hasOwn(fixed, key)) {
@@ -99,7 +99,8 @@ export default {
99
99
 
100
100
  /**
101
101
  * The crumbs for CnBreadcrumbs: each declared crumb as a link, then the
102
- * current page. Empty without a declared crumb.
102
+ * current page, once (a last crumb that names the page is dropped).
103
+ * Empty without a declared crumb.
103
104
  *
104
105
  * @spec openspec/changes/page-header-breadcrumb/specs/page-header/spec.md#requirement-a-list-dashboard-or-settings-page-can-show-a-breadcrumb-and-a-meta-note
105
106
  * @return {Array<object>}
@@ -121,6 +122,10 @@ export default {
121
122
  return entry
122
123
  })
123
124
  if (this.current) {
125
+ // A last crumb naming the page itself is the current one, drawn once.
126
+ if (out.length > 0 && out[out.length - 1].label === this.current) {
127
+ out.pop()
128
+ }
124
129
  out.push({ label: this.current })
125
130
  }
126
131
  return out
@@ -194,6 +194,7 @@
194
194
  <ul
195
195
  v-if="group.items.length > 0"
196
196
  class="cn-record-list__list"
197
+ :class="{ 'cn-record-list__list--avatar-small': avatarSmall }"
197
198
  :aria-labelledby="group.label ? group.headingId : null"
198
199
  :data-testid="isGrouped ? 'cn-record-list-group' : null">
199
200
  <li
@@ -635,7 +636,7 @@ function unwrap(value) {
635
636
  * `rowRoute`, `pageControls`), or `endpointSource` (an app endpoint whose
636
637
  * payload, after `responsePath`, is the array of rows, as on the object table), or `objectRows`
637
638
  * (`{ path, where?, valuePath? }`: rows the page record holds, such as one answer of a case); `avatar` puts a circle with the initials of a name
638
- * template before the title; what differs is the drawing: `presentation` is `rows`, `feed`
639
+ * template before the title (`{ text, size: 32 }` draws the 32px circle 12px from the title, as DqZaakBesluiten); what differs is the drawing: `presentation` is `rows`, `feed`
639
640
  * or `inline`, and each line is a template (`"{identifier} · {caseTypeName}"`,
640
641
  * `"Termijn {deadline|date}"`). `cards` draws a framed card per row in a
641
642
  * grid, with `trailing` as the large figure (board ShBank's accounts).
@@ -831,6 +832,17 @@ export default {
831
832
  },
832
833
 
833
834
  computed: {
835
+ /**
836
+ * Whether the avatar is the 32px circle (`avatar.size: 32`).
837
+ *
838
+ * @return {boolean}
839
+ * @spec openspec/changes/record-list-avatar-size/specs/record-list-avatar/spec.md#requirement-a-record-list-avatar-can-be-the-small-circle
840
+ */
841
+ avatarSmall() {
842
+ const avatar = this.content?.avatar
843
+ return Boolean(avatar && typeof avatar === 'object' && Number(avatar.size) === 32)
844
+ },
845
+
834
846
  /** @return {boolean} Whether a row's pill comes before its title (`pillAt: "start"`). */
835
847
  pillFirst() {
836
848
  return this.content?.pillAt === 'start'
@@ -3068,6 +3080,14 @@ export default {
3068
3080
  white-space: nowrap;
3069
3081
  }
3070
3082
 
3083
+ /* `avatar.size: 32`: the smaller circle, 12px from the title where the row
3084
+ keeps 16px between its other parts (DqZaakBesluiten Adviezen). */
3085
+ .cn-record-list__list--avatar-small .cn-record-list__avatar {
3086
+ width: 32px;
3087
+ height: 32px;
3088
+ margin-inline-end: -4px;
3089
+ }
3090
+
3071
3091
  .cn-record-list__avatar {
3072
3092
  align-items: center;
3073
3093
  background: var(--color-background-hover);
@@ -243,6 +243,7 @@ export default {
243
243
  }
244
244
 
245
245
  .cn-saved-view-alert__facts {
246
+ padding: 0;
246
247
  display: grid;
247
248
  grid-template-columns: max-content 1fr;
248
249
  gap: 6px 16px;
@@ -280,6 +281,11 @@ export default {
280
281
  font-weight: 600;
281
282
  }
282
283
 
284
+ /* The switch sits flush with the card edge, as the board draws it. */
285
+ .cn-saved-view-alert :deep(.checkbox-radio-switch__content) {
286
+ padding: 0;
287
+ }
288
+
283
289
  .cn-saved-view-alert__switch-text {
284
290
  display: flex;
285
291
  flex-direction: column;
@@ -199,13 +199,18 @@
199
199
  {{ t('nextcloud-vue', 'Goes on the timeline above, internal unless you make it visible') }}
200
200
  </p>
201
201
  </template>
202
+ <label v-if="textBlocks.length" class="cn-timeline-manage__draft-label" :for="blockId">
203
+ {{ t('nextcloud-vue', 'Template') }}
204
+ </label>
202
205
  <NcSelect v-if="textBlocks.length"
203
206
  v-model="chosenBlock"
204
207
  class="cn-timeline-manage__filter"
205
208
  :options="blockOptions"
206
209
  :clearable="true"
207
210
  label="label"
208
- :inputLabel="t('nextcloud-vue', 'Template')"
211
+ :inputId="blockId"
212
+ :labelOutside="true"
213
+ :ariaLabelCombobox="t('nextcloud-vue', 'Template')"
209
214
  :placeholder="t('nextcloud-vue', 'Write your own')"
210
215
  data-testid="cn-timeline-block"
211
216
  @update:modelValue="pickBlock" />
@@ -437,6 +442,10 @@ export default {
437
442
  return `cn-timeline-draft-${this.uid}`
438
443
  },
439
444
 
445
+ blockId() {
446
+ return `cn-timeline-block-${this.uid}`
447
+ },
448
+
440
449
  /**
441
450
  * The kind options, the plain note first. A note is an entry with NO
442
451
  * kind, so "Note" filters on its absence.
@@ -168,13 +168,22 @@ body:has(.cn-look-board):not(:has(.cn-look-nextcloud)) .modal-mask {
168
168
  goes to those tokens too (DqOverdragen, DqDocumentGenereren). */
169
169
  .cn-dialog-header__subtitle {
170
170
  --nldesign-component-paragraph-font-size: 14px;
171
+ --nldesign-component-paragraph-line-height: 1.5;
171
172
  --nldesign-component-paragraph-color: var(--color-text-maxcontrast);
172
173
  --nldesign-color-on-surface: var(--color-text-maxcontrast);
173
174
  margin: 4px 0 0;
174
175
  font-size: 14px;
176
+ line-height: 1.5;
175
177
  color: var(--color-text-maxcontrast);
176
178
  }
177
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. */
182
+ .cn-dialog-board .dialog__content .cn-edit-view,
183
+ .cn-dialog-board .dialog__content .cn-share-view {
184
+ padding-top: 6px;
185
+ }
186
+
178
187
  /* The close button is a 36px circle. NcButton sizes an icon-only button by
179
188
  --button-size with `!important`, so the size goes through that; the radius
180
189
  also goes to the nldesign theme's button radius token. */
@@ -539,6 +548,15 @@ body:has(.cn-look-board):not(:has(.cn-look-nextcloud)) .modal-mask {
539
548
  }
540
549
  }
541
550
 
551
+ /* A choice legend in a board dialog is 14px/600 on the normal line height, as
552
+ the dialog boards draw it (DqDocumentGenereren "Sjabloon").
553
+ @spec openspec/changes/board-choice-legend-type/specs/board-choice-legend-type/spec.md#requirement-a-board-dialogs-choice-legend-is-14px-600-on-the-normal-line-height */
554
+ .cn-dialog-board .cn-choice-cards__legend {
555
+ font-size: 14px;
556
+ font-weight: 600;
557
+ line-height: normal;
558
+ }
559
+
542
560
  /* Above a phone the boards top-align a dialog (DqNieuweZaak, DqUwWachtrij,
543
561
  DqVolgendeStap: the top edge sits about 90px down at 1440 wide) instead of
544
562
  centring it. `--cn-dialog-top` moves it. */