@conduction/nextcloud-vue 2.141.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 (140) 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/CnCellRenderer/CnCellRenderer.vue.js +1 -1
  23. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +13 -0
  24. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
  25. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +3 -3
  26. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
  27. package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue.js +1 -1
  28. package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue2.js.map +1 -1
  29. package/dist/esm/components/CnChecklistRunWidget/CnChecklistRunWidget.vue3.js.map +1 -1
  30. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue.js +1 -1
  31. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js.map +1 -1
  32. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js.map +1 -1
  33. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +21 -0
  34. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  35. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +1 -1
  36. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  37. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue.js +1 -1
  38. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  39. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +2 -1
  40. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
  41. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue.js +1 -1
  42. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +34 -3
  43. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
  44. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
  45. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue.js +1 -1
  46. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +70 -5
  47. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
  48. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +5 -3
  49. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
  50. package/dist/esm/components/CnFilesBrowser/filesBrowser.js +17 -4
  51. package/dist/esm/components/CnFilesBrowser/filesBrowser.js.map +1 -1
  52. package/dist/esm/components/CnFilesBrowser/filesBrowserColumns.js +21 -1
  53. package/dist/esm/components/CnFilesBrowser/filesBrowserColumns.js.map +1 -1
  54. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +36 -2
  55. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  56. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +2 -1
  57. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  58. package/dist/esm/components/CnIndexPage/useSelfFetchList.js +14 -0
  59. package/dist/esm/components/CnIndexPage/useSelfFetchList.js.map +1 -1
  60. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue.js +1 -1
  61. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +3 -1
  62. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
  63. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +2 -1
  64. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
  65. package/dist/esm/components/CnPageHeader/CnHeaderTrail.vue2.js +6 -1
  66. package/dist/esm/components/CnPageHeader/CnHeaderTrail.vue2.js.map +1 -1
  67. package/dist/esm/components/CnPageHeader/CnHeaderTrail.vue3.js.map +1 -1
  68. package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue.js +1 -1
  69. package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue2.js +31 -1
  70. package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue2.js.map +1 -1
  71. package/dist/esm/components/CnPartyPicker/CnPartyPicker.vue3.js.map +1 -1
  72. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
  73. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +12 -1
  74. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  75. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +2 -2
  76. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  77. package/dist/esm/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue.js +1 -1
  78. package/dist/esm/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue2.js.map +1 -1
  79. package/dist/esm/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue3.js.map +1 -1
  80. package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue.js +1 -1
  81. package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue2.js +4 -0
  82. package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue2.js.map +1 -1
  83. package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue3.js +18 -8
  84. package/dist/esm/components/CnTimelineWidget/CnTimelineManage.vue3.js.map +1 -1
  85. package/dist/esm/integrations/builtin/contacts/CnContactsTab.vue.js +1 -1
  86. package/dist/esm/integrations/builtin/contacts/CnContactsTab.vue2.js +52 -1
  87. package/dist/esm/integrations/builtin/contacts/CnContactsTab.vue2.js.map +1 -1
  88. package/dist/esm/integrations/builtin/contacts/CnContactsTab.vue3.js +119 -65
  89. package/dist/esm/integrations/builtin/contacts/CnContactsTab.vue3.js.map +1 -1
  90. package/dist/esm/integrations/builtin/deck/CnDeckCard.vue.js +1 -1
  91. package/dist/esm/integrations/builtin/deck/CnDeckCard.vue2.js +86 -4
  92. package/dist/esm/integrations/builtin/deck/CnDeckCard.vue2.js.map +1 -1
  93. package/dist/esm/integrations/builtin/deck/CnDeckCard.vue3.js +67 -5
  94. package/dist/esm/integrations/builtin/deck/CnDeckCard.vue3.js.map +1 -1
  95. package/dist/esm/l10n/en.json.js +10 -1
  96. package/dist/esm/l10n/en.json.js.map +1 -1
  97. package/dist/esm/l10n/nl.json.js +12 -3
  98. package/dist/esm/l10n/nl.json.js.map +1 -1
  99. package/dist/esm/library-style.js +1 -1
  100. package/dist/esm/nextcloud-vue.css +976 -592
  101. package/dist/esm/utils/buildViewTree.js +23 -8
  102. package/dist/esm/utils/buildViewTree.js.map +1 -1
  103. package/dist/nextcloud-vue.cjs.js +1363 -734
  104. package/dist/nextcloud-vue.cjs.js.map +2 -2
  105. package/dist/nextcloud-vue.css +976 -592
  106. package/l10n/en.json +10 -1
  107. package/l10n/nl.json +12 -3
  108. package/package.json +1 -1
  109. package/src/components/CnAdminSettingsShell/CnAdminSettingsShell.vue +28 -1
  110. package/src/components/CnAiCompanion/CnAiChatPanel.vue +12 -0
  111. package/src/components/CnAiCompanion/CnAiInput.vue +32 -1
  112. package/src/components/CnAuditTrailCard/CnAuditTrailCard.vue +34 -4
  113. package/src/components/CnAuditTrailWidget/CnAuditTrailWidget.vue +1 -0
  114. package/src/components/CnCellRenderer/CnCellRenderer.vue +15 -1
  115. package/src/components/CnChecklistRunWidget/CnChecklistRunWidget.vue +8 -0
  116. package/src/components/CnChoiceCards/CnChoiceCards.vue +1 -0
  117. package/src/components/CnDataTable/CnDataTable.md +1 -0
  118. package/src/components/CnDataTable/CnDataTable.vue +22 -1
  119. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +1 -0
  120. package/src/components/CnFactsWidget/CnFactsWidget.vue +34 -3
  121. package/src/components/CnFilesBrowser/CnFilesBrowser.vue +72 -7
  122. package/src/components/CnFilesBrowser/filesBrowser.js +17 -4
  123. package/src/components/CnFilesBrowser/filesBrowserColumns.js +20 -1
  124. package/src/components/CnIndexPage/CnIndexPage.vue +37 -2
  125. package/src/components/CnIndexPage/useSelfFetchList.js +14 -0
  126. package/src/components/CnObjectSidebar/CnFilesTab.vue +4 -1
  127. package/src/components/CnPageHeader/CnHeaderTrail.vue +6 -1
  128. package/src/components/CnPartyPicker/CnPartyPicker.vue +31 -1
  129. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +21 -1
  130. package/src/components/CnSavedViewAlertWidget/CnSavedViewAlertWidget.vue +6 -0
  131. package/src/components/CnTimelineWidget/CnTimelineManage.vue +10 -1
  132. package/src/css/dialog.css +18 -0
  133. package/src/css/look-board-detail.css +126 -0
  134. package/src/css/look-board-index.css +95 -1
  135. package/src/css/look-board.css +90 -1
  136. package/src/integrations/builtin/contacts/CnContactsTab.vue +101 -26
  137. package/src/integrations/builtin/contacts/__tests__/CnContactsTabBoardRows.spec.js +67 -0
  138. package/src/integrations/builtin/deck/CnDeckCard.vue +121 -2
  139. package/src/integrations/builtin/deck/__tests__/CnDeckCardHeaderActions.spec.js +68 -0
  140. package/src/utils/buildViewTree.js +23 -8
@@ -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
@@ -875,6 +875,7 @@
875
875
  :columns="renderedColumns"
876
876
  :pinnedCount="pinnedColumnCount"
877
877
  :rowIcon="rowIcon"
878
+ :rowIconTone="rowIconTone"
878
879
  :rowIndicators="rowIndicators"
879
880
  :rowIndicatorCap="rowIndicatorCap"
880
881
  :rows="displayObjects"
@@ -3185,6 +3186,17 @@ export default {
3185
3186
  default: null,
3186
3187
  },
3187
3188
 
3189
+ /**
3190
+ * The leading icon's tile tone in the board look (`primary`, `teal`,
3191
+ * `neutral`), from `pages[].config.rowIconTone`. Forwarded to CnDataTable.
3192
+ *
3193
+ * @type {string}
3194
+ */
3195
+ rowIconTone: {
3196
+ type: String,
3197
+ default: 'primary',
3198
+ },
3199
+
3188
3200
  /** Columns to exclude in schema mode */
3189
3201
  excludeColumns: {
3190
3202
  type: Array,
@@ -6422,6 +6434,7 @@ export default {
6422
6434
  r: this.register,
6423
6435
  s: this.exportSchemaSlug,
6424
6436
  f: this.filter || null,
6437
+ a: this.effectiveActiveFilters || null,
6425
6438
  e: this.countEntries,
6426
6439
  // The store's total, not the error override in effectivePagination,
6427
6440
  // so a failed fetch and its recovery do not refetch the counts.
@@ -8504,7 +8517,9 @@ export default {
8504
8517
  return [
8505
8518
  ...this.searchTermChips,
8506
8519
  ...this.quickFilterActiveChips,
8507
- ...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)),
8508
8523
  ]
8509
8524
  },
8510
8525
 
@@ -9690,6 +9705,18 @@ export default {
9690
9705
  * @return {boolean}
9691
9706
  * @spec openspec/changes/index-scope-presentation/specs/index-page/spec.md#requirement-the-query-that-chose-a-preset-is-not-a-removable-filter
9692
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
+
9693
9720
  isFixedFilter(key, value) {
9694
9721
  const fixed = this.fixedFilters
9695
9722
  if (!fixed || !Object.hasOwn(fixed, key)) {
@@ -10012,7 +10039,15 @@ export default {
10012
10039
  const counts = await fetchFilterCounts({
10013
10040
  register: this.register,
10014
10041
  schema,
10015
- entries: this.countEntries.map((entry) => ({ key: entry.key, filter: resolveFilterMap(entry.filter, params, ctx) })),
10042
+ // A quick filter counts within the list as filtered (the
10043
+ // active filters, its own field its own); a saved view counts
10044
+ // its own query.
10045
+ entries: this.countEntries.map((entry) => ({
10046
+ key: entry.key,
10047
+ filter: entry.key.startsWith('tab:')
10048
+ ? { ...(this.effectiveActiveFilters || {}), ...resolveFilterMap(entry.filter, params, ctx) }
10049
+ : resolveFilterMap(entry.filter, params, ctx),
10050
+ })),
10016
10051
  baseFilter: resolveFilterMap(this.filter, params, ctx),
10017
10052
  ctx,
10018
10053
  })
@@ -5,7 +5,9 @@ import { useObjectStore } from '../../store/index.js'
5
5
  // Both filter resolvers live in `utils/routeFilters.js` so CnLogsPage applies
6
6
  // the same two grammars (route-param interpolation + `?key=value` deep links)
7
7
  // without pulling in this composable's index-only sidebar/subscription wiring.
8
+ import { loadMyTeam } from '../../utils/myTeam.js'
8
9
  import { withPersonalLenses } from '../../utils/personalLenses.js'
10
+ import { dropOptionalUnresolved, MY_TEAM_TOKEN, resolveFilterTokens } from '../../utils/resolveFilterTokens.js'
9
11
  import { parseSortKeysFromQuery, resolveFilterMap, resolveQueryFilters } from '../../utils/routeFilters.js'
10
12
  import { orderToSortKeys } from '../../utils/sortOrder.js'
11
13
 
@@ -392,6 +394,18 @@ export function useSelfFetchList(props, instance, inject, extras = {}) {
392
394
  // a reactive getter so a runtime flip attaches/detaches accordingly.
393
395
  // Stores without live-updates support (no `subscribe` action) are a
394
396
  // silent no-op inside the composable, keeping this fully inert.
397
+ // An address carrying `@myTeam` (a shared link) waits for the team, as a
398
+ // saved view on it does: once read, the token becomes the team, or goes
399
+ // for a reader in none.
400
+ const carriesMyTeam = Object.values(initialActiveFilters || {})
401
+ .some((value) => value === MY_TEAM_TOKEN || (Array.isArray(value) && value.includes(MY_TEAM_TOKEN)))
402
+ if (carriesMyTeam) {
403
+ loadMyTeam().then(() => {
404
+ list.activeFilters.value = dropOptionalUnresolved(resolveFilterTokens(list.activeFilters.value, tokenCtx()))
405
+ list.refresh(1)
406
+ })
407
+ }
408
+
395
409
  useObjectSubscription(objectStore, () => objectType.value, null, {
396
410
  enabled: () => props.subscribe !== false,
397
411
  })
@@ -16,6 +16,7 @@
16
16
  :dropOverlay="dropOverlay"
17
17
  :dropLabel="dropLabel"
18
18
  :dropDetail="dropDetail"
19
+ :dropDetailValues="dropDetailValues"
19
20
  :dropHint="dropHint"
20
21
  :rowActions="rowActions"
21
22
  :newActions="newActions"
@@ -360,7 +361,9 @@ export default {
360
361
  /** Forwarded to CnFilesBrowser: the drop state's line ("Laat los om toe te voegen"); the library's own without it. */
361
362
  dropLabel: { type: String, default: undefined },
362
363
  /** Forwarded to CnFilesBrowser: the drop state's second line, `{count}` the files dragged; it also makes the overlay fill the panel. */
363
- dropDetail: { type: String, default: '' },
364
+ dropDetail: { type: [String, Array], default: '' },
365
+ /** Forwarded to CnFilesBrowser: named values filled into `dropDetail`. */
366
+ dropDetailValues: { type: Object, default: null },
364
367
  /** Forwarded to CnFilesBrowser: "Or drag files onto this list." under the list. */
365
368
  dropHint: { type: Boolean, default: false },
366
369
  /**
@@ -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
@@ -52,7 +52,9 @@ const SEARCH_DELAY = 250
52
52
  * Organisatie · KvK …"). Built on CnSearchSelect.
53
53
  *
54
54
  * `sources`: each `{ id, register, schema, label?, title?, secondary?,
55
- * limit?, icon? }` searches OpenRegister with `_search`; `title` and
55
+ * limit?, icon?, lookup? }` searches OpenRegister with `_search`; `lookup`
56
+ * (`{ url, when }`) sends a term matching `when` to the app's exact lookup
57
+ * instead (a POST of `{ term }` answering `{ results }`); `title` and
56
58
  * `secondary` are templates filled from the row (default `{name}` / none),
57
59
  * `label` names the kind ("Resident") and starts the second line; `label`
58
60
  * and `secondary` go through the host translate, so a manifest can write
@@ -284,6 +286,10 @@ export default {
284
286
  const { data } = await axios.post(generateUrl('/contactsmenu/contacts'), { filter: term })
285
287
  return (data && Array.isArray(data.contacts) ? data.contacts : []).map((c) => this.contactOptionOf(source, c))
286
288
  }
289
+ if (this.isExactLookup(source, term) === true) {
290
+ const { data } = await axios.post(generateUrl(source.lookup.url), { term })
291
+ return (data && Array.isArray(data.results) ? data.results : []).map((row) => this.optionOf(source, row))
292
+ }
287
293
  if (!source.register || !source.schema) {
288
294
  return []
289
295
  }
@@ -311,6 +317,30 @@ export default {
311
317
  this.searching = false
312
318
  },
313
319
 
320
+ /**
321
+ * Whether a typed term goes to the source's exact lookup instead of the
322
+ * free-text search: `source.lookup` is `{ url, when }`, `when` a regular
323
+ * expression the whole term must match ("^[0-9]{9}$" for a BSN). The
324
+ * lookup is a POST, so the term never sits in a URL or a server log line,
325
+ * and the app behind it may check and record who looked whom up.
326
+ *
327
+ * @param {object} source The source.
328
+ * @param {string} term The typed term.
329
+ * @return {boolean}
330
+ * @spec openspec/changes/party-picker-exact-lookup/specs/party-picker/spec.md#requirement-a-party-source-can-look-a-term-up-exactly
331
+ */
332
+ isExactLookup(source, term) {
333
+ const lookup = source && source.lookup
334
+ if (!lookup || typeof lookup.url !== 'string' || lookup.url === '' || typeof lookup.when !== 'string') {
335
+ return false
336
+ }
337
+ try {
338
+ return new RegExp(lookup.when).test(term)
339
+ } catch {
340
+ return false
341
+ }
342
+ },
343
+
314
344
  /**
315
345
  * Name a value the list does not hold: the first OpenRegister source
316
346
  * that has an object with that id.
@@ -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. */
@@ -842,6 +842,29 @@
842
842
  display: flex;
843
843
  }
844
844
 
845
+ /* The assistant in the side column (DqZaakAssistent "Assistent") is a panel,
846
+ not a label card: a 17px title over a hairline, 16px 18px in, its pill at
847
+ the row's end, the chat flush under it. */
848
+ .cn-look-board .cn-detail-page__side .cn-widget-wrapper:has(.cn-ai-chat-widget) > .cn-widget-wrapper__header {
849
+ padding: 16px 18px;
850
+ border-bottom: 1px solid var(--color-border);
851
+ }
852
+
853
+ .cn-look-board .cn-detail-page__side .cn-widget-wrapper:has(.cn-ai-chat-widget) .cn-widget-wrapper__title {
854
+ color: var(--color-main-text) !important;
855
+ font-size: var(--cn-board-heading-size, 17px);
856
+ font-weight: 700;
857
+ line-height: 1.2;
858
+ }
859
+
860
+ .cn-look-board .cn-detail-page__side .cn-widget-wrapper:has(.cn-ai-chat-widget) .cn-widget-wrapper__actions:has(.cn-detail-widget-header-items) {
861
+ display: flex;
862
+ }
863
+
864
+ .cn-look-board .cn-detail-page__side .cn-widget-wrapper:has(.cn-ai-chat-widget) > .cn-widget-wrapper__content {
865
+ padding: 0;
866
+ }
867
+
845
868
  /* A widget-reported card header action in the main column is a neutral 40px
846
869
  control: the main background, the control border, the main text at
847
870
  14px/600, a 16px icon 8px before the label (DqZaakBesluiten "Document
@@ -1431,6 +1454,14 @@
1431
1454
  display: none;
1432
1455
  }
1433
1456
 
1457
+ /* ---- A file list's trail at its root (CnFilesBrowser) ------------------- */
1458
+ /* At the browser's root the boards draw the bar's buttons only, no trail of
1459
+ one crumb naming the case (DqZaakDocumenten); in a subfolder the trail shows.
1460
+ @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 */
1461
+ .cn-look-board .cn-files-browser__crumbs--at-root:not(.cn-look-nextcloud *) {
1462
+ display: none;
1463
+ }
1464
+
1434
1465
  /* ---- A file list (CnFilesBrowser) --------------------------------------- */
1435
1466
  /* The board's compact card table (DqDocumentenBeheer): a near-white header at
1436
1467
  14px/600 in the muted ink, 10px 12px; 14px cells, 11px 12px, a hairline
@@ -1585,6 +1616,49 @@
1585
1616
  line-height: 1.2;
1586
1617
  }
1587
1618
 
1619
+ /* The contacts tab's rows as DqZaakBetrokkenen "Partijen" draws them: the
1620
+ role heading 15px bold ink in sentence case, each person on a row 12px
1621
+ high-padded under a hairline, the details and the period 13px muted.
1622
+ @spec openspec/changes/board-contacts-rows/specs/contacts/spec.md */
1623
+ .cn-look-board .cn-contacts-tab .cn-contacts-tab__group-title {
1624
+ margin: 6px 0 0;
1625
+ font-size: 15px;
1626
+ font-weight: 700;
1627
+ letter-spacing: 0;
1628
+ text-transform: none;
1629
+ color: var(--color-main-text);
1630
+ }
1631
+
1632
+ .cn-look-board .cn-contacts-tab .cn-contacts-tab__item {
1633
+ align-items: center;
1634
+ padding: 12px 0;
1635
+ border-top: 1px solid var(--cn-board-hairline);
1636
+ border-radius: 0;
1637
+ }
1638
+
1639
+ .cn-look-board .cn-contacts-tab .cn-contacts-tab__item:first-child {
1640
+ margin-top: 8px;
1641
+ }
1642
+
1643
+ .cn-look-board .cn-contacts-tab .cn-contacts-tab__name {
1644
+ font-size: 15px;
1645
+ font-weight: 600;
1646
+ }
1647
+
1648
+ .cn-look-board .cn-contacts-tab .cn-contacts-tab__meta,
1649
+ .cn-look-board .cn-contacts-tab .cn-contacts-tab__period {
1650
+ font-size: 13px;
1651
+ color: var(--color-text-maxcontrast);
1652
+ }
1653
+
1654
+ .cn-look-board .cn-contacts-tab .cn-contacts-tab__period {
1655
+ flex: none;
1656
+ }
1657
+
1658
+ .cn-look-board .cn-contacts-tab .cn-contacts-tab__group {
1659
+ margin-bottom: 8px;
1660
+ }
1661
+
1588
1662
  /* The contacts tab inside a detail card (DqZaakBetrokkenen "Partijen") takes
1589
1663
  the card's padding instead of its own, and its buttons start at the card's
1590
1664
  edge under the subtitle.
@@ -1596,3 +1670,55 @@
1596
1670
  .cn-look-board .cn-detail-page__leaf-card .cn-contacts-tab .cn-contacts-tab__header {
1597
1671
  justify-content: flex-start;
1598
1672
  }
1673
+
1674
+ /* A body card's subtitle as the Dq boards draw it: 14px muted at line
1675
+ height 1.2, 3px under the title, and the content 12px under it.
1676
+ @spec openspec/changes/board-card-subtitle-gaps/specs/board-card/spec.md */
1677
+ .cn-look-board .cn-detail-page .cn-widget-wrapper:not(.cn-detail-page__side *) .cn-widget-wrapper__title-stack:has(> .cn-widget-wrapper__subtitle) {
1678
+ gap: 3px;
1679
+ }
1680
+
1681
+ .cn-look-board .cn-detail-page .cn-widget-wrapper:not(.cn-detail-page__side *) .cn-widget-wrapper__subtitle {
1682
+ font-size: 14px;
1683
+ line-height: 1.2;
1684
+ }
1685
+
1686
+ .cn-look-board .cn-detail-page .cn-widget-wrapper:not(.cn-detail-page__side *):has(> .cn-widget-wrapper__header .cn-widget-wrapper__subtitle) > .cn-widget-wrapper__content {
1687
+ padding-top: 12px;
1688
+ }
1689
+
1690
+ /* A checklist (DqTaak): each item an 18-20px box at the card's own inset, the
1691
+ label 10px after it in weight 600, 12px between items, the list 2px below
1692
+ the title block. Nextcloud's checkbox pads its box 7px in and makes every row
1693
+ 34px tall, which put the list 7px right and 2px apart too far per item.
1694
+ @spec openspec/changes/board-checklist-rows/specs/checklist-widget/spec.md#requirement-a-board-checklist-sits-at-the-card-inset */
1695
+ .cn-look-board .cn-checklist-widget__items {
1696
+ display: flex;
1697
+ flex-direction: column;
1698
+ gap: 12px;
1699
+ margin-top: 2px;
1700
+ }
1701
+
1702
+ .cn-look-board .cn-checklist-widget__item .checkbox-content {
1703
+ padding: 0;
1704
+ gap: 10px;
1705
+ min-height: 20px;
1706
+ }
1707
+
1708
+ .cn-look-board .cn-checklist-widget__item .checkbox-content.checkbox-content .checkbox-content__icon {
1709
+ margin: 0;
1710
+ }
1711
+
1712
+ .cn-look-board .cn-checklist-widget__item .checkbox-radio-switch__input {
1713
+ min-height: 20px;
1714
+ }
1715
+
1716
+ .cn-look-board .cn-checklist-widget__item .checkbox-content__wrapper,
1717
+ .cn-look-board .cn-checklist-widget__item .checkbox-content__text {
1718
+ line-height: 18px;
1719
+ min-height: 0;
1720
+ }
1721
+
1722
+ .cn-look-board .cn-checklist-widget__item .checkbox-content__text {
1723
+ font-weight: 600;
1724
+ }