@conduction/nextcloud-vue 2.102.0 → 2.104.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 (147) hide show
  1. package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js +23 -0
  2. package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js.map +1 -1
  3. package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js +94 -72
  4. package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js.map +1 -1
  5. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +18 -1
  6. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
  7. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js +3 -3
  8. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
  9. package/dist/esm/components/CnConversationThread/CnConversationThread.vue.js +1 -1
  10. package/dist/esm/components/CnConversationThread/CnConversationThread.vue2.js +73 -0
  11. package/dist/esm/components/CnConversationThread/CnConversationThread.vue2.js.map +1 -1
  12. package/dist/esm/components/CnConversationThread/CnConversationThread.vue3.js +99 -64
  13. package/dist/esm/components/CnConversationThread/CnConversationThread.vue3.js.map +1 -1
  14. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue.js +1 -1
  15. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js +46 -0
  16. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js.map +1 -1
  17. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue3.js +37 -2
  18. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue3.js.map +1 -1
  19. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
  20. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +29 -6
  21. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  22. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  23. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +39 -2
  24. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  25. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +14 -0
  26. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  27. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +40 -2
  28. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  29. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  30. package/dist/esm/components/CnFormDialog/CnFormDialog.vue.js +1 -1
  31. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +113 -20
  32. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
  33. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +387 -359
  34. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
  35. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +173 -3
  36. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  37. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +107 -87
  38. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  39. package/dist/esm/components/CnIndexPage/useSelfFetchList.js +7 -2
  40. package/dist/esm/components/CnIndexPage/useSelfFetchList.js.map +1 -1
  41. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue.js +1 -1
  42. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +10 -1
  43. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
  44. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
  45. package/dist/esm/components/CnObjectRow/CnObjectRow.vue.js +1 -1
  46. package/dist/esm/components/CnObjectRow/CnObjectRow.vue2.js +57 -5
  47. package/dist/esm/components/CnObjectRow/CnObjectRow.vue2.js.map +1 -1
  48. package/dist/esm/components/CnObjectRow/CnObjectRow.vue3.js +37 -4
  49. package/dist/esm/components/CnObjectRow/CnObjectRow.vue3.js.map +1 -1
  50. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
  51. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +97 -3
  52. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  53. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +46 -2
  54. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  55. package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue.js +9 -0
  56. package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue.js.map +1 -0
  57. package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue2.js +165 -0
  58. package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue2.js.map +1 -0
  59. package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue3.js +85 -0
  60. package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue3.js.map +1 -0
  61. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue.js +1 -1
  62. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js +54 -6
  63. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js.map +1 -1
  64. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js +3 -1
  65. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js.map +1 -1
  66. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
  67. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +8 -1
  68. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  69. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  70. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue.js +1 -1
  71. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue2.js.map +1 -1
  72. package/dist/esm/components/CnUserSettingsDialog/CnUserSettingsDialog.vue3.js.map +1 -1
  73. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue.js +1 -1
  74. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js +18 -2
  75. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js.map +1 -1
  76. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue3.js.map +1 -1
  77. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue.js +1 -1
  78. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue2.js +19 -0
  79. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue2.js.map +1 -1
  80. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue3.js +1 -1
  81. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue3.js.map +1 -1
  82. package/dist/esm/composables/indexSources.js +7 -7
  83. package/dist/esm/composables/indexSources.js.map +1 -1
  84. package/dist/esm/index.js +1 -0
  85. package/dist/esm/index.js.map +1 -1
  86. package/dist/esm/l10n/en.json.js +4 -0
  87. package/dist/esm/l10n/en.json.js.map +1 -1
  88. package/dist/esm/l10n/nl.json.js +4 -0
  89. package/dist/esm/l10n/nl.json.js.map +1 -1
  90. package/dist/esm/library-style.js +1 -1
  91. package/dist/esm/mixins/pageViews.js +8 -1
  92. package/dist/esm/mixins/pageViews.js.map +1 -1
  93. package/dist/esm/nextcloud-vue.css +586 -416
  94. package/dist/esm/utils/nextcloudDirectory.js +18 -1
  95. package/dist/esm/utils/nextcloudDirectory.js.map +1 -1
  96. package/dist/esm/utils/sortOrder.js +61 -0
  97. package/dist/esm/utils/sortOrder.js.map +1 -0
  98. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  99. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  100. package/dist/esm/utils/visibleWhen.js +13 -2
  101. package/dist/esm/utils/visibleWhen.js.map +1 -1
  102. package/dist/nextcloud-vue.cjs.js +12270 -10936
  103. package/dist/nextcloud-vue.cjs.js.map +2 -2
  104. package/dist/nextcloud-vue.css +586 -416
  105. package/l10n/en.json +4 -0
  106. package/l10n/nl.json +4 -0
  107. package/package.json +1 -1
  108. package/src/components/CnActionsBar/CnActionsBar.md +1 -0
  109. package/src/components/CnActionsBar/CnActionsBar.vue +39 -2
  110. package/src/components/CnAppRoot/CnAppRoot.md +1 -1
  111. package/src/components/CnAppRoot/CnAppRoot.vue +21 -4
  112. package/src/components/CnConversationThread/CnConversationThread.md +17 -0
  113. package/src/components/CnConversationThread/CnConversationThread.vue +113 -8
  114. package/src/components/CnConversationThread/CnConversationWidget.vue +60 -1
  115. package/src/components/CnDashboardPage/CnDashboardPage.md +4 -1
  116. package/src/components/CnDashboardPage/CnDashboardPage.vue +29 -6
  117. package/src/components/CnDataTable/CnDataTable.md +1 -0
  118. package/src/components/CnDataTable/CnDataTable.vue +49 -0
  119. package/src/components/CnDetailPage/CnDetailPage.md +2 -1
  120. package/src/components/CnDetailPage/CnDetailPage.vue +40 -2
  121. package/src/components/CnFormDialog/CnFormDialog.md +1 -1
  122. package/src/components/CnFormDialog/CnFormDialog.vue +127 -0
  123. package/src/components/CnIndexPage/CnIndexPage.md +14 -2
  124. package/src/components/CnIndexPage/CnIndexPage.vue +192 -11
  125. package/src/components/CnIndexPage/useSelfFetchList.js +6 -1
  126. package/src/components/CnObjectListWidget/CnObjectListWidget.vue +10 -1
  127. package/src/components/CnObjectRow/CnObjectRow.vue +95 -1
  128. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +134 -2
  129. package/src/components/CnSearchSelect/CnSearchSelect.vue +283 -0
  130. package/src/components/CnSearchSelect/index.js +12 -0
  131. package/src/components/CnSetupWizard/CnSetupWizard.vue +56 -6
  132. package/src/components/CnTabsWidget/CnTabsWidget.vue +8 -1
  133. package/src/components/CnUserSettingsDialog/CnUserSettingsDialog.vue +7 -2
  134. package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +18 -2
  135. package/src/components/CnWizardDialog/CnWizardDialog.vue +20 -1
  136. package/src/components/index.js +1 -0
  137. package/src/composables/indexSources.js +7 -7
  138. package/src/css/dialog.css +5 -3
  139. package/src/css/index-page.css +19 -0
  140. package/src/css/look-board-index.css +48 -0
  141. package/src/index.js +1 -0
  142. package/src/mixins/pageViews.js +8 -1
  143. package/src/schemas/app-manifest-v2.schema.json +75 -6
  144. package/src/utils/nextcloudDirectory.js +17 -0
  145. package/src/utils/sortOrder.js +58 -0
  146. package/src/utils/validateManifestV2.compiled.js +3 -3
  147. package/src/utils/visibleWhen.js +13 -2
package/l10n/en.json CHANGED
@@ -1727,6 +1727,10 @@
1727
1727
  "On": "On"
1728
1728
  },
1729
1729
  "plurals": {
1730
+ "{count} member": [
1731
+ "{count} member",
1732
+ "{count} members"
1733
+ ],
1730
1734
  "{count} selected": [
1731
1735
  "{count} selected",
1732
1736
  "{count} selected"
package/l10n/nl.json CHANGED
@@ -1940,6 +1940,10 @@
1940
1940
  "On": "Aan"
1941
1941
  },
1942
1942
  "plurals": {
1943
+ "{count} member": [
1944
+ "{count} lid",
1945
+ "{count} leden"
1946
+ ],
1943
1947
  "{count} selected": [
1944
1948
  "{count} geselecteerd",
1945
1949
  "{count} geselecteerd"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@conduction/nextcloud-vue",
3
- "version": "2.102.0",
3
+ "version": "2.104.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": {
@@ -153,6 +153,7 @@ Controlling the inline action button count — `inlineActionCount` sets how many
153
153
  | `mapLabel` | `String` | `''` | Label for the map view-toggle option (defaults to "Map"). Only shown when `showMap` |
154
154
  | `mapIcon` | `String` | `''` | MDI icon name for the map option (defaults to the built-in map-marker icon). Resolved via `CnIcon` |
155
155
  | `showSearch` | `Boolean` | `false` | Whether to show the inline search field on the left of the bar. The field takes the place of the "Showing X of Y" counter unless `showCountWithSearch` is set |
156
+ | `searchPlacement` | `String` | `''` | Board look only: `'toolbar'` draws the search field on row 1, before Filter and the view switch, instead of at the start of row 2 |
156
157
  | `searchValue` | `String` | `''` | Current value of the inline search field (controlled) |
157
158
  | `searchPlaceholder` | `String` | `''` | Placeholder / accessible label for the inline search field |
158
159
  | `showCountWithSearch` | `Boolean` | `false` | Keep the "Showing X of Y" counter visible beside the inline search field, after the search and any `#after-search` controls. No effect without `showSearch` |
@@ -18,6 +18,20 @@
18
18
  @description Under the board look: saved views as chips and the Save view button, in row 1 after the quick-filter chips.
19
19
  -->
20
20
  <slot name="actions-end" />
21
+ <!-- `searchPlacement: "toolbar"`: the search field on row 1, before
22
+ Filter and the view switch, instead of opening row 2. -->
23
+ <div v-if="showSearch && searchInToolbar"
24
+ class="cn-actions-bar__search cn-actions-bar__search--toolbar"
25
+ data-testid="cn-actions-bar-search-toolbar">
26
+ <Magnify :size="18" class="cn-actions-bar__search-icon" />
27
+ <input
28
+ type="search"
29
+ class="cn-actions-bar__search-input"
30
+ :placeholder="searchPlaceholder || t('nextcloud-vue', 'Search…')"
31
+ :value="searchValue"
32
+ :aria-label="searchPlaceholder || t('nextcloud-vue', 'Search')"
33
+ @input="onSearchInput">
34
+ </div>
21
35
  <!--
22
36
  Filter and the view switch are one unit: when row 1 is full they
23
37
  wrap together to the start of the next line (the DqZaken board),
@@ -373,7 +387,7 @@
373
387
 
374
388
  <!-- Board look, row 2: the search field, then the active filters. -->
375
389
  <div v-if="isBoardLayout" class="cn-actions-bar__row cn-actions-bar__row--search" data-testid="cn-actions-bar-row-search">
376
- <div v-if="showSearch" class="cn-actions-bar__search">
390
+ <div v-if="showSearch && !searchInToolbar" class="cn-actions-bar__search">
377
391
  <Magnify :size="18" class="cn-actions-bar__search-icon" />
378
392
  <input
379
393
  type="search"
@@ -383,7 +397,7 @@
383
397
  :aria-label="searchPlaceholder || t('nextcloud-vue', 'Search')"
384
398
  @input="onSearchInput">
385
399
  </div>
386
- <span v-else-if="showCount && hasTotal" class="cn-actions-bar__count">
400
+ <span v-else-if="showCount && hasTotal && !searchInToolbar" class="cn-actions-bar__count">
387
401
  {{ countText }}
388
402
  </span>
389
403
  <slot name="after-search" />
@@ -943,6 +957,19 @@ export default {
943
957
  default: false,
944
958
  },
945
959
 
960
+ /**
961
+ * Where the board look draws the search field: `''` (the default) at
962
+ * the start of row 2, `'toolbar'` on row 1 before Filter and the view
963
+ * switch (DqTeamwachtrij). No effect outside the board look.
964
+ *
965
+ * @type {''|'toolbar'}
966
+ */
967
+ searchPlacement: {
968
+ type: String,
969
+ default: '',
970
+ validator: (v) => ['', 'toolbar'].includes(v),
971
+ },
972
+
946
973
  /** Current value of the inline search field (controlled) */
947
974
  searchValue: {
948
975
  type: String,
@@ -1101,6 +1128,16 @@ export default {
1101
1128
  ],
1102
1129
 
1103
1130
  computed: {
1131
+ /**
1132
+ * Whether the search field sits on the board's row 1.
1133
+ *
1134
+ * @spec openspec/changes/index-search-on-toolbar/specs/index-page/spec.md#requirement-the-search-field-can-sit-on-the-toolbar-row
1135
+ * @return {boolean}
1136
+ */
1137
+ searchInToolbar() {
1138
+ return this.isBoardLayout && this.searchPlacement === 'toolbar'
1139
+ },
1140
+
1104
1141
  /**
1105
1142
  * Whether the board toolbar is drawn: the `layout` prop, else the look
1106
1143
  * the app provides.
@@ -67,7 +67,7 @@ With loading state, custom components, translate, permissions, and custom page t
67
67
  |------|------|----------|---------|-------------|
68
68
  | `manifest` | Object | Yes | — | Reactive manifest object from `useAppManifest`. Provides `manifest.dependencies`, `manifest.menu`, and is propagated via provide/inject |
69
69
  | `appId` | String | Yes | — | Nextcloud app id. Forwarded to `NcContent` and `CnDependencyMissing` |
70
- | `appDisplayName` | String | No | `''` | Human-readable name shown in the Nextcloud top bar. When set it overrides the technical `appId` so a virtual app shows its own name (e.g. "Pet Store") instead of the host app id. |
70
+ | `appDisplayName` | String | No | `''` | Human-readable name shown in the Nextcloud top bar. When set it overrides the technical `appId` so a virtual app shows its own name (e.g. "Pet Store") instead of the host app id. The user settings dialog, the setup wizard and the credentials section show it through the app's `translate`; without it they show the name Nextcloud's app menu gives (localised), else the id with a capital, never the bare id. The top bar's own name, which also keys the stored list-pane width (`pane-list-size-<name>`), stays untranslated so a reader's pane width survives a language switch. |
71
71
  | `persistManifestDelta` | Function | No | `null` | In-app editing (ADR-041) persistence hook, called with the minimal manifest delta on save. When Buildiq is reachable for the user, CnAppRoot provides a shared `cnManifestEditor` + `cnOpenBuildAvailable` (the inject key deliberately keeps its old spelling — see the rename note in CHANGELOG); the page action rows (`CnDashboardPage`/`CnDetailPage`/`CnIndexPage`) then render an icon-only `CnBuildiqEditButton` inline with their normal buttons, and the body grid becomes drag/resizable in edit mode. Omit to keep edits in-memory only. |
72
72
  | `dataSources` | Object \| null | No | `null` | App registers/schemas for the in-app pages editor (ADR-041). Lets the Edit-pages modal offer Register / Schema / Columns dropdowns for `index`/`detail` pages instead of free-text slug inputs, so a created page actually renders a table. Shape: `{ registers: [{ value, label, schemas: [{ value, label, columns: string[] }] }] }`. Provided to descendants as `cnDataSources`; when omitted the editor falls back to free-text register/schema fields. **A snapshot** — captured once, so it cannot show anything created after boot. Prefer `dataSourcesLoader`. |
73
73
  | `dataSourcesLoader` | Function \| null | No | `null` | Async loader for the same data, re-invoked every time a pages-editor modal opens: `async () => ({ registers: [...] })`. Because `provide()` runs once, the static `dataSources` snapshot can never change — so a register or schema created after the app booted (in another tab, the OpenRegister UI, or via the API) would never appear in the dropdowns. A loader fixes that with no page reload, and moves the fetch off the app-boot path onto the far rarer editor-open path. When both props are given, `dataSources` seeds the initial list and the loader's result replaces it on the first refresh. Provided to descendants as the `cnDataSourcesState` holder plus a `cnRefreshDataSources()` action. |
@@ -180,7 +180,7 @@
180
180
  <div class="cn-app-root__setup">
181
181
  <CnSetupWizard
182
182
  :appId="appId"
183
- :appName="appDisplayName || (manifest && manifest.name) || appId"
183
+ :appName="shownAppName"
184
184
  :steps="manifest.setup.steps"
185
185
  :cancellable="false"
186
186
  :completedStepIds="setupCompletedStepIds"
@@ -441,7 +441,7 @@
441
441
  <div v-if="setupWizardOpen" class="cn-app-root__setup-optional">
442
442
  <CnSetupWizard
443
443
  :appId="appId"
444
- :appName="appDisplayName || (manifest && manifest.name) || appId"
444
+ :appName="shownAppName"
445
445
  :steps="manifest.setup.steps"
446
446
  :cancellable="true"
447
447
  :completedStepIds="setupCompletedStepIds"
@@ -580,7 +580,7 @@
580
580
  <CnCredentials
581
581
  scope="personal"
582
582
  :appId="appId"
583
- :appName="appDisplayName || (manifest && manifest.name) || appId"
583
+ :appName="shownAppName"
584
584
  :appCredentials="(manifest && manifest.credentials) || []" />
585
585
  </NcAppSettingsSection>
586
586
  <!--
@@ -747,6 +747,7 @@ import {
747
747
  import { RegistryKindError } from '../../errors/RegistryKindError.js'
748
748
  import { useObjectStore } from '../../store/index.js'
749
749
  import { isAppInstalled } from '../../utils/appInstalled.js'
750
+ import { appShownName } from '../../utils/appShownName.js'
750
751
  import { BUILT_IN_FORMATTERS } from '../../utils/builtInFormatters.js'
751
752
  import { addDiagnosticsListener, clearReportedDiagnostics, reportDiagnostic } from '../../utils/diagnostics.js'
752
753
  import { DEFAULT_FORGE, resolveForge } from '../../utils/forge.js'
@@ -3134,6 +3135,22 @@ export default {
3134
3135
  return this.userSettingsTitle || this.libraryCopy('User settings')
3135
3136
  },
3136
3137
 
3138
+ /**
3139
+ * The app's name as the reader reads it: `appDisplayName` or the
3140
+ * manifest's `name` through the app's translate, else the name
3141
+ * Nextcloud's app menu shows (localised), else the id with a capital.
3142
+ * Used for the user settings dialog, the setup wizard and the
3143
+ * credentials section; never for NcContent's `appName`, which also
3144
+ * keys the stored list-pane width (`pane-list-size-<appName>`).
3145
+ *
3146
+ * @spec openspec/changes/app-shown-name-in-chrome/specs/app-root/spec.md#requirement-the-app-chrome-shows-the-apps-localised-name
3147
+ * @return {string}
3148
+ */
3149
+ shownAppName() {
3150
+ const given = this.appDisplayName || (this.manifest && this.manifest.name) || ''
3151
+ return appShownName(this.appId, given ? this.translate(given) : '')
3152
+ },
3153
+
3137
3154
  /**
3138
3155
  * The props of the user-settings dialog: `NcAppSettingsDialog`'s, or
3139
3156
  * under the board look CnUserSettingsDialog's (eyebrow, subtitle and
@@ -3149,7 +3166,7 @@ export default {
3149
3166
  const props = {
3150
3167
  open: this.userSettingsOpen,
3151
3168
  title: this.resolvedUserSettingsTitle,
3152
- appName: this.appDisplayName || (this.manifest && this.manifest.name) || this.appId || '',
3169
+ appName: this.shownAppName,
3153
3170
  }
3154
3171
  if (this.userSettingsEyebrow) {
3155
3172
  props.eyebrow = this.translate(this.userSettingsEyebrow)
@@ -23,6 +23,23 @@ Read-only, for a closed record:
23
23
  ]" />
24
24
  ```
25
25
 
26
+ Channel chips and a header action. `channel-filter` draws All plus a chip per channel with its count, `channels` lists labels that always get a chip, and the `header-actions` slot puts buttons beside the title:
27
+
28
+ ```vue
29
+ <CnConversationThread
30
+ title="Contact"
31
+ channel-filter
32
+ :channels="['E-mail', 'Letter']"
33
+ :messages="[
34
+ { id: 1, author: 'Sanne de Vries', side: 'them', channel: 'E-mail', text: 'Could I see the documents?' },
35
+ { id: 2, side: 'us', channel: 'E-mail', text: 'They are on their way.' },
36
+ ]">
37
+ <template #header-actions>
38
+ <NcButton>Log contact</NcButton>
39
+ </template>
40
+ </CnConversationThread>
41
+ ```
42
+
26
43
  No messages yet:
27
44
 
28
45
  ```vue
@@ -9,13 +9,27 @@
9
9
  data-testid="cn-conversation-thread"
10
10
  :aria-labelledby="title ? titleId : null"
11
11
  :aria-label="title ? null : fallbackLabel">
12
- <component
13
- :is="titleTag"
14
- v-if="title"
15
- :id="titleId"
16
- class="cn-conversation-thread__title">
17
- {{ title }}
18
- </component>
12
+ <div v-if="title || $slots['header-actions']" class="cn-conversation-thread__header">
13
+ <component
14
+ :is="titleTag"
15
+ v-if="title"
16
+ :id="titleId"
17
+ class="cn-conversation-thread__title">
18
+ {{ title }}
19
+ </component>
20
+ <div v-if="$slots['header-actions']" class="cn-conversation-thread__header-actions">
21
+ <!-- @slot header-actions Buttons beside the title, for example "Log contact". -->
22
+ <slot name="header-actions" />
23
+ </div>
24
+ </div>
25
+
26
+ <CnQuickFilterBar
27
+ v-if="channelChips.length > 0"
28
+ inline
29
+ class="cn-conversation-thread__channels"
30
+ :tabs="channelChips"
31
+ :activeIndex="activeChannelIndex"
32
+ @update:activeIndex="onChannelChip" />
19
33
 
20
34
  <p v-if="loading && rows.length === 0"
21
35
  class="cn-conversation-thread__empty"
@@ -27,9 +41,13 @@
27
41
  {{ emptyText }}
28
42
  </p>
29
43
 
44
+ <p v-else-if="shownRows.length === 0" class="cn-conversation-thread__empty" data-testid="cn-conversation-channel-empty">
45
+ {{ emptyText }}
46
+ </p>
47
+
30
48
  <ol v-else class="cn-conversation-thread__messages">
31
49
  <li
32
- v-for="row in rows"
50
+ v-for="row in shownRows"
33
51
  :key="row.key"
34
52
  class="cn-conversation-thread__message"
35
53
  :class="`cn-conversation-thread__message--${row.side}`"
@@ -93,6 +111,7 @@
93
111
  import axios from '@nextcloud/axios'
94
112
  import { translate as t } from '@nextcloud/l10n'
95
113
  import { NcButton } from '@nextcloud/vue'
114
+ import CnQuickFilterBar from '../CnQuickFilterBar/CnQuickFilterBar.vue'
96
115
  import { prefixUrl } from '../../utils/headers.js'
97
116
  import { userLocale } from '../../utils/userLocale.js'
98
117
 
@@ -126,6 +145,7 @@ export default {
126
145
  name: 'CnConversationThread',
127
146
 
128
147
  components: {
148
+ CnQuickFilterBar,
129
149
  NcButton,
130
150
  },
131
151
 
@@ -204,6 +224,24 @@ export default {
204
224
  default: false,
205
225
  },
206
226
 
227
+ /**
228
+ * Draw channel chips above the messages: All, then one per channel
229
+ * with its count. Choosing one shows only that channel's messages.
230
+ */
231
+ channelFilter: {
232
+ type: Boolean,
233
+ default: false,
234
+ },
235
+
236
+ /**
237
+ * Channel labels that always get a chip, in this order, also when no
238
+ * message uses them yet. Channels the messages use follow.
239
+ */
240
+ channels: {
241
+ type: Array,
242
+ default: () => [],
243
+ },
244
+
207
245
  /** Shown when there are no messages yet. */
208
246
  emptyText: {
209
247
  type: String,
@@ -224,6 +262,8 @@ export default {
224
262
  conversationThreadUid += 1
225
263
  return {
226
264
  draft: '',
265
+ /** The channel the chips narrow the thread to; '' is All. */
266
+ activeChannel: '',
227
267
  sending: false,
228
268
  errorMessage: '',
229
269
  titleId: `cn-conversation-title-${conversationThreadUid}`,
@@ -262,6 +302,47 @@ export default {
262
302
  })
263
303
  },
264
304
 
305
+ /**
306
+ * The channel labels with a chip: the declared ones, then the ones
307
+ * the messages use.
308
+ *
309
+ * @return {Array<string>} The labels, each once.
310
+ */
311
+ channelLabels() {
312
+ const declared = (Array.isArray(this.channels) ? this.channels : []).filter((label) => typeof label === 'string' && label !== '')
313
+ return [...new Set([...declared, ...this.rows.map((row) => row.channel).filter(Boolean)])]
314
+ },
315
+
316
+ /**
317
+ * The chips: All, then each channel, each with its count. Empty
318
+ * without `channelFilter` or without channels.
319
+ *
320
+ * @return {Array<{label: string, count: number}>} The chips.
321
+ */
322
+ channelChips() {
323
+ if (!this.channelFilter || this.channelLabels.length === 0) {
324
+ return []
325
+ }
326
+ return [
327
+ { label: t('nextcloud-vue', 'All'), count: this.rows.length },
328
+ ...this.channelLabels.map((label) => ({ label, count: this.rows.filter((row) => row.channel === label).length })),
329
+ ]
330
+ },
331
+
332
+ /** @return {number} The pressed chip: 0 for All. */
333
+ activeChannelIndex() {
334
+ const index = this.channelLabels.indexOf(this.activeChannel)
335
+ return index < 0 ? 0 : index + 1
336
+ },
337
+
338
+ /** @return {Array<object>} The rows on the chosen channel, or all of them. */
339
+ shownRows() {
340
+ if (this.channelChips.length === 0 || this.activeChannelIndex === 0) {
341
+ return this.rows
342
+ }
343
+ return this.rows.filter((row) => row.channel === this.activeChannel)
344
+ },
345
+
265
346
  /**
266
347
  * Whether there is something to send and nothing in flight.
267
348
  *
@@ -289,6 +370,16 @@ export default {
289
370
  },
290
371
 
291
372
  methods: {
373
+ /**
374
+ * Narrow the thread to the chip's channel; the All chip (0) clears it.
375
+ *
376
+ * @param {number|null} index The chip index.
377
+ * @return {void}
378
+ */
379
+ onChannelChip(index) {
380
+ this.activeChannel = index > 0 ? (this.channelLabels[index - 1] || '') : ''
381
+ },
382
+
292
383
  /**
293
384
  * Up to two initials of a name, for the avatar.
294
385
  *
@@ -370,6 +461,20 @@ export default {
370
461
  gap: calc(3.5 * var(--default-grid-baseline));
371
462
  }
372
463
 
464
+ .cn-conversation-thread__header {
465
+ align-items: flex-start;
466
+ display: flex;
467
+ flex-wrap: wrap;
468
+ gap: calc(3 * var(--default-grid-baseline));
469
+ justify-content: space-between;
470
+ }
471
+
472
+ .cn-conversation-thread__header-actions {
473
+ display: flex;
474
+ flex-wrap: wrap;
475
+ gap: calc(2 * var(--default-grid-baseline));
476
+ }
477
+
373
478
  .cn-conversation-thread__title {
374
479
  font-size: 1.1em;
375
480
  font-weight: 700;
@@ -15,12 +15,27 @@
15
15
  :endpoint="replyAction ? '' : endpoint"
16
16
  :loading="loading"
17
17
  :emptyText="content.emptyText ? tr(content.emptyText) : undefined"
18
+ :channelFilter="content.channelChips === true"
19
+ :channels="channelLabels"
18
20
  @send="onSend"
19
- @sent="onSent" />
21
+ @sent="onSent">
22
+ <template v-if="headerAction" #header-actions>
23
+ <NcButton
24
+ data-testid="cn-conversation-header-action"
25
+ @click="onHeaderAction">
26
+ <template v-if="headerAction.icon" #icon>
27
+ <CnIcon :name="headerAction.icon" :size="20" />
28
+ </template>
29
+ {{ tr(headerAction.label) }}
30
+ </NcButton>
31
+ </template>
32
+ </CnConversationThread>
20
33
  </div>
21
34
  </template>
22
35
 
23
36
  <script>
37
+ import { NcButton } from '@nextcloud/vue'
38
+ import CnIcon from '../CnIcon/CnIcon.vue'
24
39
  import CnConversationThread from './CnConversationThread.vue'
25
40
  import { dispatchAction } from '../../utils/actionsDispatcher.js'
26
41
  import { fetchReferenceObjects } from '../../utils/boardReferenceColumns.js'
@@ -45,12 +60,20 @@ import { hasUnresolvedDeepTokens, resolveDeepTokens } from '../../utils/resolveF
45
60
  * `replyAction`, an action descriptor the app decides the meaning of: it is
46
61
  * dispatched with `args: [{ text, objectId }]` and `props.text`. After a send
47
62
  * the source is read again.
63
+ *
64
+ * `channelChips: true` draws a chip per channel above the thread (every
65
+ * `channelMap` label, in map order, then any other channel a message uses).
66
+ * `headerAction` is a button beside the title (`{ label, icon?, ...action }`,
67
+ * DqZaakContact's "Contact vastleggen"), dispatched like `replyAction` with
68
+ * `args: [{ objectId }]`; after it the source is read again.
48
69
  */
49
70
  export default {
50
71
  name: 'CnConversationWidget',
51
72
 
52
73
  components: {
53
74
  CnConversationThread,
75
+ CnIcon,
76
+ NcButton,
54
77
  },
55
78
 
56
79
  inject: {
@@ -102,6 +125,18 @@ export default {
102
125
  return source && typeof source === 'object' && source.register && source.schema ? source : null
103
126
  },
104
127
 
128
+ /** @return {object|null} The header action, or null without a label. */
129
+ headerAction() {
130
+ const action = this.content?.headerAction
131
+ return action && typeof action === 'object' && typeof action.label === 'string' && action.label !== '' ? action : null
132
+ },
133
+
134
+ /** @return {Array<string>} The `channelMap` labels, translated, each once. */
135
+ channelLabels() {
136
+ const map = this.content?.channelMap && typeof this.content.channelMap === 'object' ? this.content.channelMap : {}
137
+ return [...new Set(Object.values(map).filter((label) => typeof label === 'string' && label !== '').map((label) => this.tr(label)))]
138
+ },
139
+
105
140
  /** @return {object|null} The reply action, or null. */
106
141
  replyAction() {
107
142
  const action = this.content?.replyAction
@@ -254,6 +289,30 @@ export default {
254
289
  this.onSent({ text })
255
290
  },
256
291
 
292
+ /**
293
+ * Run `headerAction` with the record id, then read the source again.
294
+ *
295
+ * @return {Promise<void>}
296
+ */
297
+ async onHeaderAction() {
298
+ if (!this.headerAction) {
299
+ return
300
+ }
301
+ const { label, icon, ...rest } = resolveDeepTokens(this.headerAction, this.tokenCtx)
302
+ const action = { ...rest, args: [{ objectId: this.objectId }] }
303
+ try {
304
+ const outcome = await (typeof this.cnDispatchAction === 'function'
305
+ ? this.cnDispatchAction(action)
306
+ : dispatchAction(action, { router: this.$router || null }))
307
+ if (outcome === false) {
308
+ return
309
+ }
310
+ } catch {
311
+ return
312
+ }
313
+ await this.loadSource()
314
+ },
315
+
257
316
  /**
258
317
  * Show an accepted reply at once, without waiting for the record.
259
318
  *
@@ -126,12 +126,15 @@ predicate's field value into their text wherever it says `{value}` (e.g.
126
126
  | `gridMargin` | Number | `12` | Grid margin (gutter) in pixels between widgets |
127
127
  | `emptyLabel` | String | `'No widgets configured'` | Text shown in the empty state when `layout` is empty |
128
128
  | `unavailableLabel` | String | `'Widget not available'` | Text shown for unknown or unavailable widgets |
129
- | `userLayout` | Boolean | `false` | Keep a per-user arrangement of this page. Off by default: a page without it makes **no** layout request and renders exactly as before. |
129
+ | `userLayout` | Boolean | `false` | **Deprecated** (layout belongs to Buildiq; removed after this release). Keep a per-user arrangement of this page. Off by default: a page without it makes **no** layout request and renders exactly as before. |
130
130
  | `appId` | String | `''` | The Nextcloud app the user layout is stored under. Required for `userLayout` to do anything. |
131
131
  | `userLayoutStore` | Object | `null` | A store exposing `loadDashboardLayout` / `saveDashboardLayout` / `resetDashboardLayout`, usually one carrying [`dashboardLayoutsPlugin`](../../../docs/store/plugins/dashboard-layouts.md). Left unset, the page reads and writes user preferences directly. |
132
132
 
133
133
  ## Per-user layout
134
134
 
135
+ > **Deprecated: layout belongs to Buildiq.** Arranging a page is Buildiq's job (Ruben, 18 Oct 2026): its **Indeling aanpassen** edits the layout everybody sees. `userLayout` still works for this release and will be removed after it. While Buildiq edits the page (`cnEditingBody`), the grid shows the manifest layout, and a drag or resize goes into the manifest Buildiq saves, also on a page with `userLayout`. A user arrangement made outside an in-page edit session is saved at once, because no **Done** follows to save it.
136
+
137
+
135
138
  With `userLayout: true` and an `appId`, this page loads the current user's arrangement before the first grid render and merges it over the manifest layout. The rules are [`mergeUserLayout`](../../../docs/utilities/merge-user-layout.md)'s: **the manifest decides which widgets exist, the user decides where they sit.**
136
139
 
137
140
  ```vue
@@ -1839,6 +1839,9 @@ export default {
1839
1839
  * and renders exactly as it did before this existed, which is the
1840
1840
  * property that makes shipping it safe on every dashboard at once.
1841
1841
  *
1842
+ * Deprecated: layout belongs to Buildiq. Kept working for one release.
1843
+ *
1844
+ * @deprecated
1842
1845
  * @type {boolean}
1843
1846
  */
1844
1847
  userLayout: {
@@ -2216,9 +2219,25 @@ export default {
2216
2219
  * @return {boolean}
2217
2220
  */
2218
2221
  gridEditable() {
2222
+ return this.isEditing || this.editingBody
2223
+ },
2224
+
2225
+ /**
2226
+ * Whether Buildiq is editing the page (the injected `cnEditingBody`
2227
+ * ref, or a plain boolean). Buildiq is the layout editor: while it
2228
+ * edits, the grid shows and changes the manifest layout, also on a
2229
+ * page with `userLayout`.
2230
+ *
2231
+ * @return {boolean}
2232
+ */
2233
+ editingBody() {
2219
2234
  const e = this.cnEditingBody
2220
- const buildiqEditing = Boolean(e && typeof e === 'object' && 'value' in e ? e.value : e)
2221
- return this.isEditing || buildiqEditing
2235
+ return Boolean(e && typeof e === 'object' && 'value' in e ? e.value : e)
2236
+ },
2237
+
2238
+ /** @return {boolean} Whether the grid shows this user's arrangement. */
2239
+ showsUserLayout() {
2240
+ return this.userLayout && !this.editingBody && Array.isArray(this.userLayoutItems)
2222
2241
  },
2223
2242
 
2224
2243
  /**
@@ -2279,9 +2298,7 @@ export default {
2279
2298
  * @return {Array<object>} The layout.
2280
2299
  */
2281
2300
  renderedLayout() {
2282
- const base = (this.userLayout && Array.isArray(this.userLayoutItems))
2283
- ? this.userLayoutItems
2284
- : this.layout
2301
+ const base = this.showsUserLayout ? this.userLayoutItems : this.layout
2285
2302
  // The KPI row is not part of the grid.
2286
2303
  if (Array.isArray(this.kpiRow) && this.kpiRow.length > 0 && Array.isArray(base)) {
2287
2304
  return base.filter((item) => !this.kpiRow.includes(item.widgetId))
@@ -3577,7 +3594,8 @@ export default {
3577
3594
  // write below is for the admin editor, which edits the page for
3578
3595
  // everybody; doing it for a user rearranging their own dashboard
3579
3596
  // would rewrite the page for everyone who opens it after them.
3580
- if (this.userLayout && Array.isArray(this.userLayoutItems)) {
3597
+ // While Buildiq edits, the drag is the admin's and goes below.
3598
+ if (this.showsUserLayout) {
3581
3599
  this.userLayoutDirty = true
3582
3600
  this.userLayoutItems = this.userLayoutItems.map((item) => {
3583
3601
  const u = (Array.isArray(updated) ? updated : []).find((candidate) => String(candidate.id) === String(item.id)
@@ -3586,6 +3604,11 @@ export default {
3586
3604
  return (u ? { ...item, ...this.geometryFrom(u) } : item)
3587
3605
  })
3588
3606
  this.$emit('layout-change', this.userLayoutItems)
3607
+ // Outside an in-page edit session no Done will come to save
3608
+ // the arrangement, so it is saved now.
3609
+ if (!this.isEditing) {
3610
+ this.saveUserLayout()
3611
+ }
3589
3612
 
3590
3613
  return
3591
3614
  }
@@ -229,6 +229,7 @@ features are folded in here as opt-in props (bare-table usage is unchanged):
229
229
  - `borderless` — drop the container's card chrome so the table sits flush inside
230
230
  a parent card (e.g. a `CnWidgetWrapper` dashboard slot).
231
231
  - `limit` — show only the first N rows; with `viewAllRoute` a "View all" footer appears.
232
+ - `pageSize` — rows per page: one page of the rows the table holds, with the board's pager under them ("4 of 7", the page buttons, Next) once there is more than one page (DqCatalogus). 0 (the default) shows every row, or the first `limit`.
232
233
  - `viewAllRoute` / `viewAllLabel` — the footer control's route and label. It renders
233
234
  as a real `<a :href>` when the router resolves the route, and as a
234
235
  `<button type="button">` (emitting `view-all`) otherwise, so it always has a