@conduction/nextcloud-vue 2.56.0-beta.30 → 2.56.0-beta.31

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 (173) hide show
  1. package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js.map +1 -1
  2. package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js +3 -3
  3. package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js.map +1 -1
  4. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +80 -4
  5. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
  6. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js +95 -91
  7. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
  8. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js +85 -6
  9. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js.map +1 -1
  10. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js +1 -1
  11. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js.map +1 -1
  12. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +4 -1
  13. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
  14. package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
  15. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js +1 -1
  16. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js.map +1 -1
  17. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue3.js.map +1 -1
  18. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js +2 -2
  19. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js.map +1 -1
  20. package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js.map +1 -1
  21. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +40 -3
  22. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  23. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  24. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +19 -11
  25. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  26. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  27. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +43 -0
  28. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  29. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +1 -1
  30. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  31. package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue2.js +1 -1
  32. package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue2.js.map +1 -1
  33. package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue3.js.map +1 -1
  34. package/dist/esm/components/CnFacetSidebar/CnFacetSidebar.vue2.js +7 -7
  35. package/dist/esm/components/CnFacetSidebar/CnFacetSidebar.vue2.js.map +1 -1
  36. package/dist/esm/components/CnFacetSidebar/CnFacetSidebar.vue3.js.map +1 -1
  37. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +125 -0
  38. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
  39. package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
  40. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js +3 -2
  41. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js.map +1 -1
  42. package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js.map +1 -1
  43. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +302 -4
  44. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
  45. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +417 -244
  46. package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
  47. package/dist/esm/components/CnFilesBrowser/filesBrowser.js +101 -1
  48. package/dist/esm/components/CnFilesBrowser/filesBrowser.js.map +1 -1
  49. package/dist/esm/components/CnFilesBrowser/filesBrowserColumns.js +141 -1
  50. package/dist/esm/components/CnFilesBrowser/filesBrowserColumns.js.map +1 -1
  51. package/dist/esm/components/CnFormPage/CnFormPage.vue2.js +60 -0
  52. package/dist/esm/components/CnFormPage/CnFormPage.vue2.js.map +1 -1
  53. package/dist/esm/components/CnFormPage/CnFormPage.vue3.js +74 -49
  54. package/dist/esm/components/CnFormPage/CnFormPage.vue3.js.map +1 -1
  55. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +29 -1
  56. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  57. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +48 -4
  58. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  59. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue2.js +7 -7
  60. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue2.js.map +1 -1
  61. package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue3.js.map +1 -1
  62. package/dist/esm/components/CnNextStepCard/CnNextStepWidget.vue2.js +1 -1
  63. package/dist/esm/components/CnNextStepCard/CnNextStepWidget.vue2.js.map +1 -1
  64. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +1 -1
  65. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
  66. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
  67. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +63 -1
  68. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
  69. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +8 -1
  70. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
  71. package/dist/esm/components/CnObjectSidebar/CnNotesTab.vue2.js +14 -0
  72. package/dist/esm/components/CnObjectSidebar/CnNotesTab.vue2.js.map +1 -1
  73. package/dist/esm/components/CnObjectSidebar/CnNotesTab.vue3.js +5 -1
  74. package/dist/esm/components/CnObjectSidebar/CnNotesTab.vue3.js.map +1 -1
  75. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +13 -6
  76. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  77. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  78. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js +84 -1
  79. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js.map +1 -1
  80. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js +85 -41
  81. package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js.map +1 -1
  82. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js +7 -2
  83. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js.map +1 -1
  84. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue3.js.map +1 -1
  85. package/dist/esm/components/CnWidgetObjectTable/dashboardRegistration.js +5 -0
  86. package/dist/esm/components/CnWidgetObjectTable/dashboardRegistration.js.map +1 -1
  87. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue2.js +197 -6
  88. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue2.js.map +1 -1
  89. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue3.js +94 -50
  90. package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue3.js.map +1 -1
  91. package/dist/esm/composables/useListView.js +4 -4
  92. package/dist/esm/composables/useListView.js.map +1 -1
  93. package/dist/esm/index.js +10 -1
  94. package/dist/esm/index.js.map +1 -1
  95. package/dist/esm/l10n/en.json.js +8 -1
  96. package/dist/esm/l10n/en.json.js.map +1 -1
  97. package/dist/esm/l10n/nl.json.js +54 -1
  98. package/dist/esm/l10n/nl.json.js.map +1 -1
  99. package/dist/esm/nextcloud-vue.css +198 -8
  100. package/dist/esm/utils/facets.js +75 -3
  101. package/dist/esm/utils/facets.js.map +1 -1
  102. package/dist/esm/utils/fillTemplate.js +7 -1
  103. package/dist/esm/utils/fillTemplate.js.map +1 -1
  104. package/dist/esm/utils/pageControls.js +81 -0
  105. package/dist/esm/utils/pageControls.js.map +1 -0
  106. package/dist/esm/utils/personalLenses.js +40 -3
  107. package/dist/esm/utils/personalLenses.js.map +1 -1
  108. package/dist/esm/utils/resolveFilterTokens.js +1 -1
  109. package/dist/esm/utils/resolveFilterTokens.js.map +1 -1
  110. package/dist/esm/utils/resolveManifestTokens.js +2 -0
  111. package/dist/esm/utils/resolveManifestTokens.js.map +1 -1
  112. package/dist/esm/utils/sentinelTokens.js +19 -1
  113. package/dist/esm/utils/sentinelTokens.js.map +1 -1
  114. package/dist/esm/utils/userGroupsPeek.js +91 -0
  115. package/dist/esm/utils/userGroupsPeek.js.map +1 -0
  116. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  117. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  118. package/dist/esm/utils/visibleWhen.js +57 -5
  119. package/dist/esm/utils/visibleWhen.js.map +1 -1
  120. package/dist/esm/utils/widgetVisibility.js +4 -57
  121. package/dist/esm/utils/widgetVisibility.js.map +1 -1
  122. package/dist/nextcloud-vue.cjs.js +4309 -2233
  123. package/dist/nextcloud-vue.cjs.js.map +2 -2
  124. package/dist/nextcloud-vue.css +198 -8
  125. package/l10n/en.json +8 -1
  126. package/l10n/nl.json +54 -1
  127. package/package.json +2 -1
  128. package/scripts/check-docs-mdx.mjs +108 -0
  129. package/src/components/CnActionsBar/CnActionsBar.vue +6 -1
  130. package/src/components/CnAppRoot/CnAppRoot.md +4 -2
  131. package/src/components/CnAppRoot/CnAppRoot.vue +151 -59
  132. package/src/components/CnBannerWidget/CnBannerWidget.vue +112 -7
  133. package/src/components/CnCellRenderer/CnCellRenderer.vue +4 -1
  134. package/src/components/CnConversationThread/CnConversationWidget.vue +1 -1
  135. package/src/components/CnDashboardGrid/CnDashboardGrid.vue +2 -2
  136. package/src/components/CnDashboardPage/CnDashboardPage.vue +40 -3
  137. package/src/components/CnDataTable/CnDataTable.vue +19 -11
  138. package/src/components/CnDetailPage/CnDetailPage.vue +44 -1
  139. package/src/components/CnDocumentReviewList/CnDocumentReviewWidget.vue +1 -1
  140. package/src/components/CnFacetSidebar/CnFacetSidebar.vue +7 -7
  141. package/src/components/CnFactsWidget/CnFactsWidget.vue +125 -0
  142. package/src/components/CnFieldListWidget/CnFieldListWidget.vue +3 -2
  143. package/src/components/CnFilesBrowser/CnFilesBrowser.vue +588 -117
  144. package/src/components/CnFilesBrowser/filesBrowser.js +100 -0
  145. package/src/components/CnFilesBrowser/filesBrowserColumns.js +140 -0
  146. package/src/components/CnFormPage/CnFormPage.vue +81 -0
  147. package/src/components/CnIndexPage/CnIndexPage.vue +56 -3
  148. package/src/components/CnIndexSidebar/CnIndexSidebar.vue +7 -7
  149. package/src/components/CnNextStepCard/CnNextStepWidget.vue +1 -1
  150. package/src/components/CnObjectListWidget/CnObjectListWidget.vue +1 -1
  151. package/src/components/CnObjectSidebar/CnFilesTab.vue +70 -1
  152. package/src/components/CnObjectSidebar/CnNotesTab.vue +16 -0
  153. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +13 -6
  154. package/src/components/CnStatsBlockWidget/CnStatsBlockWidget.vue +180 -3
  155. package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +7 -2
  156. package/src/components/CnWidgetObjectTable/dashboardRegistration.js +5 -0
  157. package/src/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue +281 -10
  158. package/src/components/index.js +8 -0
  159. package/src/composables/useListView.js +4 -4
  160. package/src/css/look-board-index.css +16 -8
  161. package/src/index.js +8 -0
  162. package/src/schemas/app-manifest-v2.schema.json +93 -9
  163. package/src/utils/facets.js +74 -2
  164. package/src/utils/fillTemplate.js +7 -1
  165. package/src/utils/pageControls.js +78 -0
  166. package/src/utils/personalLenses.js +39 -2
  167. package/src/utils/resolveFilterTokens.js +3 -1
  168. package/src/utils/resolveManifestTokens.js +2 -0
  169. package/src/utils/sentinelTokens.js +19 -1
  170. package/src/utils/userGroupsPeek.js +87 -0
  171. package/src/utils/validateManifestV2.compiled.js +3 -3
  172. package/src/utils/visibleWhen.js +56 -4
  173. package/src/utils/widgetVisibility.js +7 -56
@@ -6,7 +6,13 @@
6
6
  <template>
7
7
  <div
8
8
  class="cn-workspace-filter-widget"
9
- :class="{ 'cn-workspace-filter-widget--start': isStart }"
9
+ :class="{
10
+ 'cn-workspace-filter-widget--start': isStart,
11
+ 'cn-workspace-filter-widget--chips': style === 'chips',
12
+ 'cn-workspace-filter-widget--bar': isBar,
13
+ 'cn-workspace-filter-widget--inline': content.inline === true,
14
+ 'cn-workspace-filter-widget--end': content.align === 'end',
15
+ }"
10
16
  data-testid="cn-workspace-filter-widget">
11
17
  <!-- The start card (`layout: "start"`): a title and one line of
12
18
  guidance beside the control, as the "Contact starten" card. -->
@@ -19,14 +25,24 @@
19
25
  </p>
20
26
  </div>
21
27
 
22
- <p v-if="label" class="cn-workspace-filter-widget__label">
28
+ <!-- A bar (`layout: "bar"`): its `controls` side by side in one row,
29
+ each a workspace filter of its own (chips, a switch, a select). -->
30
+ <template v-if="isBar">
31
+ <CnWorkspaceFilterWidget
32
+ v-for="(control, index) in barControls"
33
+ :key="index"
34
+ :content="control"
35
+ @change="$emit('change', $event)" />
36
+ </template>
37
+
38
+ <p v-if="!isBar && label && style !== 'chips' && style !== 'toggle'" class="cn-workspace-filter-widget__label">
23
39
  {{ label }}
24
40
  </p>
25
41
 
26
42
  <!-- An object picker (`objectSource`): searches OpenRegister on the
27
43
  server, so it reaches every object, not a fetched page. -->
28
44
  <CnResourceSelect
29
- v-if="objectSource"
45
+ v-if="!isBar && objectSource"
30
46
  class="cn-workspace-filter-widget__control"
31
47
  :register="objectSource.register"
32
48
  :schema="objectSource.schema"
@@ -44,6 +60,30 @@
44
60
  {{ loadingLabel }}
45
61
  </div>
46
62
 
63
+ <!-- Chips style (screens-page-lenses): one compact segmented group,
64
+ a count after each label, arrow keys move the choice. -->
65
+ <CnSegmentedControl
66
+ v-else-if="style === 'chips'"
67
+ class="cn-workspace-filter-widget__chips"
68
+ size="compact"
69
+ :options="chipOptions"
70
+ :modelValue="selected === undefined || selected === null ? null : String(selected)"
71
+ :ariaLabel="label || writeKey"
72
+ data-testid="cn-workspace-filter-chips"
73
+ @update:modelValue="applySelection" />
74
+
75
+ <!-- Toggle style (screens-page-controls): a switch that writes
76
+ `onValue` / `offValue` (true / false) to the key. -->
77
+ <NcCheckboxRadioSwitch
78
+ v-else-if="style === 'toggle'"
79
+ class="cn-workspace-filter-widget__toggle"
80
+ type="switch"
81
+ :modelValue="toggleOn"
82
+ data-testid="cn-workspace-filter-toggle"
83
+ @update:modelValue="onToggle">
84
+ {{ tr(label) }}
85
+ </NcCheckboxRadioSwitch>
86
+
47
87
  <!-- Select style -->
48
88
  <NcSelect
49
89
  v-else-if="style === 'select'"
@@ -56,7 +96,7 @@
56
96
  @update:modelValue="onSelect" />
57
97
 
58
98
  <!-- Radio-list style (default) -->
59
- <ul v-else
99
+ <ul v-else-if="!isBar"
60
100
  class="cn-workspace-filter-widget__list"
61
101
  role="radiogroup"
62
102
  :aria-label="label || writeKey">
@@ -92,10 +132,13 @@
92
132
  </template>
93
133
 
94
134
  <script>
135
+ import { subscribe, unsubscribe } from '@nextcloud/event-bus'
95
136
  import { translate as t } from '@nextcloud/l10n'
96
137
  import { NcCheckboxRadioSwitch, NcSelect } from '@nextcloud/vue'
97
138
  import CnResourceSelect from '../CnResourceSelect/CnResourceSelect.vue'
139
+ import CnSegmentedControl from '../CnSegmentedControl/CnSegmentedControl.vue'
98
140
  import { fetchEndpointSource } from '../../composables/useEndpointSource.js'
141
+ import { fetchListTotal } from '../../utils/fetchFilterCounts.js'
99
142
  import { resolveFilterTokens } from '../../utils/resolveFilterTokens.js'
100
143
 
101
144
  /**
@@ -126,7 +169,7 @@ import { resolveFilterTokens } from '../../utils/resolveFilterTokens.js'
126
169
  * content: {
127
170
  * label: 'Queue',
128
171
  * writes: '@workspace.queue',
129
- * style: 'radio', // 'radio' (default) | 'select'
172
+ * style: 'radio', // 'radio' (default) | 'select' | 'chips'
130
173
  * allLabel: 'All',
131
174
  * showCounts: true,
132
175
  * source: { register: 'pipelinq', schema: 'werkitem', groupBy: 'queue' },
@@ -136,7 +179,7 @@ import { resolveFilterTokens } from '../../utils/resolveFilterTokens.js'
136
179
  export default {
137
180
  name: 'CnWorkspaceFilterWidget',
138
181
 
139
- components: { CnResourceSelect, NcSelect, NcCheckboxRadioSwitch },
182
+ components: { CnResourceSelect, CnSegmentedControl, NcSelect, NcCheckboxRadioSwitch },
140
183
 
141
184
  inject: {
142
185
  /**
@@ -162,7 +205,19 @@ export default {
162
205
  * server and writes the picked object's id; without a selection and
163
206
  * without `allLabel` it selects the first object.
164
207
  *
165
- * @type {{label?: string, writes?: string, style?: string, allLabel?: string, showCounts?: boolean, options?: Array, source?: object, endpointSource?: object, objectSource?: object, default?: (string|number), layout?: string, title?: string, description?: string, link?: object}}
208
+ * Chips (screens-page-lenses): `style: "chips"` draws the options as one
209
+ * compact segmented group with a count after each label. An option's
210
+ * `countFrom` (`{ register, schema, filter? }`, or a list of them that
211
+ * are added up) counts that list; a static `count` is used otherwise.
212
+ * `urlKey` keeps the choice in the address (`?<urlKey>=<value>`), and
213
+ * the address wins over `default` on load.
214
+ *
215
+ * Page controls (screens-page-controls): `style: "toggle"` is a switch
216
+ * that writes `onValue` / `offValue` (true / false); `layout: "bar"`
217
+ * with `controls` draws several controls side by side in one row, and
218
+ * `align: "end"` pushes a control to the right edge.
219
+ *
220
+ * @type {{label?: string, controls?: Array<object>, onValue?: unknown, offValue?: unknown, align?: string, urlKey?: string, writes?: string, style?: string, allLabel?: string, showCounts?: boolean, options?: Array, source?: object, endpointSource?: object, objectSource?: object, default?: (string|number), layout?: string, title?: string, description?: string, link?: object}}
166
221
  */
167
222
  content: {
168
223
  type: Object,
@@ -180,6 +235,8 @@ export default {
180
235
  loading: false,
181
236
  /** The currently-selected value (mirrors the workspace key). */
182
237
  selected: undefined,
238
+ /** Counted totals of the options that ask (`countFrom`), by option position. */
239
+ optionCounts: {},
183
240
  }
184
241
  },
185
242
 
@@ -233,9 +290,73 @@ export default {
233
290
  return src && typeof src === 'object' && src.register && src.schema ? src : null
234
291
  },
235
292
 
236
- /** Choice style: 'select' or 'radio' (default). */
293
+ /** Choice style: 'chips', 'select' or 'radio' (default). */
237
294
  style() {
238
- return this.content.style === 'select' ? 'select' : 'radio'
295
+ return ['select', 'chips', 'toggle'].includes(this.content.style) ? this.content.style : 'radio'
296
+ },
297
+
298
+ /**
299
+ * The chips: each option with its label translated and its count,
300
+ * the static one or the counted one.
301
+ *
302
+ * @return {Array<{value: string, label: string, count?: number}>}
303
+ * @spec openspec/changes/screens-page-lenses/specs/page-lenses/spec.md#requirement-a-workspace-filter-can-draw-chips-with-counts
304
+ */
305
+ chipOptions() {
306
+ return this.displayOptions.map((option, index) => {
307
+ const counted = this.optionCounts[index]
308
+ const count = counted !== undefined && counted !== null ? counted : option.count
309
+ const chip = { value: String(option.value), label: this.tr(option.label) }
310
+ if (this.showCounts && Number.isFinite(count)) {
311
+ chip.count = count
312
+ }
313
+ return chip
314
+ })
315
+ },
316
+
317
+ /**
318
+ * Whether the widget is a bar of controls (`layout: "bar"`).
319
+ *
320
+ * @return {boolean}
321
+ * @spec openspec/changes/screens-page-controls/specs/page-controls/spec.md#requirement-a-bar-holds-the-pages-controls-in-one-row
322
+ */
323
+ isBar() {
324
+ return this.content.layout === 'bar' && Array.isArray(this.content.controls)
325
+ },
326
+
327
+ /**
328
+ * The bar's controls, each an inline workspace filter of its own.
329
+ *
330
+ * @return {Array<object>}
331
+ */
332
+ barControls() {
333
+ return this.content.controls
334
+ .filter((control) => control && typeof control === 'object')
335
+ .map((control) => ({ ...control, inline: true }))
336
+ },
337
+
338
+ /** @return {unknown} The value a switch writes when on. */
339
+ onValue() {
340
+ return this.content.onValue !== undefined ? this.content.onValue : true
341
+ },
342
+
343
+ /** @return {unknown} The value a switch writes when off. */
344
+ offValue() {
345
+ return this.content.offValue !== undefined ? this.content.offValue : false
346
+ },
347
+
348
+ /** @return {boolean} Whether the switch is on. */
349
+ toggleOn() {
350
+ return this.selected !== undefined && String(this.selected) === String(this.onValue)
351
+ },
352
+
353
+ /**
354
+ * The address key that carries the choice (`urlKey`), or ''.
355
+ *
356
+ * @return {string}
357
+ */
358
+ urlKey() {
359
+ return typeof this.content.urlKey === 'string' ? this.content.urlKey : ''
239
360
  },
240
361
 
241
362
  /** Whether to render per-option counts. */
@@ -316,17 +437,115 @@ export default {
316
437
  // Seed the selection: existing workspace value → content.default → the
317
438
  // "All" clear option (empty) when present, else the first option once
318
439
  // loaded (handled in loadOptions).
440
+ const fromAddress = this.readAddress()
319
441
  const existing = this.readWorkspace()
320
- if (existing !== undefined) {
442
+ if (fromAddress !== undefined) {
443
+ this.selected = fromAddress
444
+ this.writeWorkspace(fromAddress)
445
+ } else if (existing !== undefined) {
321
446
  this.selected = existing
322
447
  } else if (this.content.default !== undefined) {
323
448
  this.selected = this.content.default
324
449
  this.writeWorkspace(this.content.default)
450
+ } else if (this.style === 'toggle') {
451
+ // A switch starts off, and says so, so a condition on its key holds.
452
+ this.selected = this.offValue
453
+ this.writeWorkspace(this.offValue)
325
454
  }
326
455
  this.loadOptions()
327
456
  },
328
457
 
458
+ mounted() {
459
+ this.onPageRefresh = () => this.loadCounts()
460
+ subscribe('cn:page:refresh', this.onPageRefresh)
461
+ this.loadCounts()
462
+ },
463
+
464
+ beforeUnmount() {
465
+ unsubscribe('cn:page:refresh', this.onPageRefresh)
466
+ },
467
+
329
468
  methods: {
469
+ /**
470
+ * The choice named in the address (`?<urlKey>=…`), when it is one of
471
+ * the options; undefined otherwise.
472
+ *
473
+ * @return {string|undefined}
474
+ * @spec openspec/changes/screens-page-lenses/specs/page-lenses/spec.md#requirement-the-choice-is-in-the-address
475
+ */
476
+ readAddress() {
477
+ if (!this.urlKey || !this.$route || !this.$route.query) {
478
+ return undefined
479
+ }
480
+ const raw = this.$route.query[this.urlKey]
481
+ if (this.style === 'toggle') {
482
+ if (raw === undefined) {
483
+ return undefined
484
+ }
485
+ return [this.onValue, this.offValue].find((value) => String(value) === String(raw))
486
+ }
487
+ const known = this.displayOptions.some((option) => String(option.value) === String(raw))
488
+ return known ? String(raw) : undefined
489
+ },
490
+
491
+ /**
492
+ * Write the choice to the address, replacing the history entry and
493
+ * keeping the other query keys.
494
+ *
495
+ * @param {unknown} value The chosen value.
496
+ * @return {void}
497
+ * @spec openspec/changes/screens-page-lenses/specs/page-lenses/spec.md#requirement-the-choice-is-in-the-address
498
+ */
499
+ writeAddress(value) {
500
+ if (!this.urlKey || !this.$router || !this.$route) {
501
+ return
502
+ }
503
+ const query = this.$route.query || {}
504
+ if (String(query[this.urlKey]) === String(value)) {
505
+ return
506
+ }
507
+ const result = this.$router.replace({ query: { ...query, [this.urlKey]: String(value) } })
508
+ if (result && typeof result.catch === 'function') {
509
+ result.catch(() => {})
510
+ }
511
+ },
512
+
513
+ /**
514
+ * Count the lists behind the options that ask (`countFrom`: a list
515
+ * `{ register, schema, filter? }`, or several that are added up). One
516
+ * list request each; an option whose count failed shows none.
517
+ *
518
+ * @return {Promise<void>}
519
+ * @spec openspec/changes/screens-page-lenses/specs/page-lenses/spec.md#requirement-a-workspace-filter-can-draw-chips-with-counts
520
+ */
521
+ async loadCounts() {
522
+ const options = this.displayOptions
523
+ const seq = (this.countSeq = (this.countSeq || 0) + 1)
524
+ const ctx = { workspace: this.readWorkspaceBag() }
525
+ await Promise.all(options.map(async (option, index) => {
526
+ const raw = (Array.isArray(this.content.options) ? this.content.options : [])
527
+ .find((candidate) => candidate && typeof candidate === 'object' && String(candidate.value ?? candidate.id) === String(option.value))
528
+ const from = raw ? raw.countFrom : null
529
+ const lists = (Array.isArray(from) ? from : [from]).filter((list) => list && typeof list === 'object' && list.register && list.schema)
530
+ if (lists.length === 0) {
531
+ return
532
+ }
533
+ let total
534
+ try {
535
+ const parts = await Promise.all(lists.map((list) => fetchListTotal(list.register, list.schema, list.filter || {}, ctx)))
536
+ if (parts.some((part) => part === null)) {
537
+ throw new Error('no total')
538
+ }
539
+ total = parts.reduce((sum, part) => sum + part, 0)
540
+ } catch {
541
+ total = null
542
+ }
543
+ if (seq === this.countSeq) {
544
+ this.optionCounts = { ...this.optionCounts, [index]: total }
545
+ }
546
+ }))
547
+ },
548
+
330
549
  /**
331
550
  * Normalise a raw option into `{ value, label, count? }`. Accepts
332
551
  * `{ value, label, count }`, `{ id, name }`, `{ key, count }` (OR
@@ -503,6 +722,17 @@ export default {
503
722
  this.applySelection(option.value)
504
723
  },
505
724
 
725
+ /**
726
+ * Switch handler: write `onValue` or `offValue`.
727
+ *
728
+ * @param {boolean} on Whether the switch is on.
729
+ * @return {void}
730
+ * @spec openspec/changes/screens-page-controls/specs/page-controls/spec.md#requirement-a-toggle-and-a-select-are-page-controls
731
+ */
732
+ onToggle(on) {
733
+ this.applySelection(on ? this.onValue : this.offValue)
734
+ },
735
+
506
736
  /**
507
737
  * NcSelect input handler.
508
738
  *
@@ -523,6 +753,7 @@ export default {
523
753
  applySelection(value) {
524
754
  this.selected = value
525
755
  this.writeWorkspace(value)
756
+ this.writeAddress(value)
526
757
  /**
527
758
  * @event change Emitted when the selection changes. Payload: `{ key, value }`.
528
759
  */
@@ -610,6 +841,46 @@ export default {
610
841
  gap: 2px;
611
842
  }
612
843
 
844
+ /* The chips: the compact segmented group, left, in the card the grid draws. */
845
+ .cn-workspace-filter-widget--chips {
846
+ flex-direction: row;
847
+ flex-wrap: wrap;
848
+ align-items: center;
849
+ }
850
+
851
+ /* A bar: its controls in one row, an `align: "end"` control pushed right. */
852
+ .cn-workspace-filter-widget--bar {
853
+ flex-direction: row;
854
+ flex-wrap: wrap;
855
+ align-items: center;
856
+ gap: 12px 16px;
857
+ }
858
+
859
+ /* An inline control sits in a bar: label left of the control, width to its content. */
860
+ .cn-workspace-filter-widget--inline {
861
+ flex-direction: row;
862
+ align-items: center;
863
+ gap: 8px;
864
+ width: auto;
865
+ }
866
+
867
+ .cn-workspace-filter-widget--inline .cn-workspace-filter-widget__label {
868
+ font-weight: 400;
869
+ color: var(--color-text-maxcontrast);
870
+ }
871
+
872
+ .cn-workspace-filter-widget--end {
873
+ margin-inline-start: auto;
874
+ }
875
+
876
+ .cn-workspace-filter-widget--end ~ .cn-workspace-filter-widget--end {
877
+ margin-inline-start: 0;
878
+ }
879
+
880
+ .cn-workspace-filter-widget__toggle {
881
+ font-weight: 600;
882
+ }
883
+
613
884
  /* The start card: text left, control (and link) right, as PqDashboard's "Contact starten". */
614
885
  .cn-workspace-filter-widget--start {
615
886
  flex-direction: row;
@@ -220,6 +220,14 @@ export { CnSectionsWidget } from './CnSectionsWidget/index.js'
220
220
  export { CnFactsWidget } from './CnFactsWidget/index.js'
221
221
  export { CnFieldListWidget } from './CnFieldListWidget/index.js'
222
222
  export { CnRecordListWidget } from './CnRecordListWidget/index.js'
223
+ export { CnBannerWidget } from './CnBannerWidget/index.js'
224
+ export { default as CnConversationWidget } from './CnConversationThread/CnConversationWidget.vue'
225
+ export { default as CnDocumentReviewWidget } from './CnDocumentReviewList/CnDocumentReviewWidget.vue'
226
+ export { default as CnNextStepWidget } from './CnNextStepCard/CnNextStepWidget.vue'
227
+ export { default as CnAuditTrailWidget } from './CnAuditTrailWidget/CnAuditTrailWidget.vue'
228
+ export { CnKbSearchWidget } from './CnKbSearchWidget/index.js'
229
+ export { CnInteractionFormWidget } from './CnInteractionFormWidget/index.js'
230
+ export { CnWorkspaceFilterWidget } from './CnWorkspaceFilterWidget/index.js'
223
231
  export { CnFilePreview } from './CnFilePreview/index.js'
224
232
  export { CnTaskFormDialog } from './CnTaskFormDialog/index.js'
225
233
  export { CnChildRecordsField } from './CnChildRecordsField/index.js'
@@ -1,5 +1,6 @@
1
1
  import { computed, isRef, onBeforeUnmount, onMounted, ref, watch } from 'vue'
2
2
  import { useObjectStore } from '../store/index.js'
3
+ import { facetFilterParams } from '../utils/facets.js'
3
4
 
4
5
  /**
5
6
  * Composable for managing list view state with full objectStore integration.
@@ -163,10 +164,9 @@ export function useListView(objectTypeOrOptions, options) {
163
164
  }
164
165
 
165
166
  for (const [key, values] of Object.entries(activeFilters.value)) {
166
- if (values && values.length > 0) {
167
- // Single-value arrays are unwrapped to scalar params
168
- params[key] = values.length === 1 ? values[0] : values
169
- }
167
+ // Single-value arrays are unwrapped to scalar params; the
168
+ // missing-value option becomes `<key>_isnull=true`.
169
+ Object.assign(params, facetFilterParams(key, values))
170
170
  }
171
171
 
172
172
  // Fixed filters (e.g. a route-param-scoped `pages[].config.filter`) are
@@ -249,7 +249,8 @@
249
249
  }
250
250
 
251
251
  /* Filter: a labelled secondary button, 36px, radius 8, with the active count. */
252
- .cn-look-board .cn-actions-bar__filter-button.button-vue {
252
+ .cn-look-board .cn-actions-bar__filter-button.button-vue,
253
+ .cn-look-board .cn-actions-bar__filter-menu .button-vue {
253
254
  min-height: 36px;
254
255
  height: 36px;
255
256
  border-radius: 8px;
@@ -499,11 +500,16 @@
499
500
  max-width: calc(100% - 26px);
500
501
  }
501
502
 
502
- /* The selection column: 28px, 18px checkboxes. */
503
+ /* The selection column: 18px checkboxes, 28px of content after 16px of padding.
504
+ The box model is set with `!important` and the widths include the padding:
505
+ Nextcloud's server.css sets `box-sizing: border-box` on everything under
506
+ `#content[class*="app-"]`, an id selector no class rule outranks, so a plain
507
+ `content-box` here was ignored and the cell left 12px for the checkbox (the
508
+ header's checkbox showed as a stray bracket). */
503
509
  .cn-look-board .cn-table-container--board .cn-data-table .cn-table-col--checkbox {
504
- box-sizing: content-box;
505
- width: 28px;
506
- min-width: 28px;
510
+ box-sizing: border-box !important;
511
+ width: 44px;
512
+ min-width: 44px;
507
513
  padding-inline: 16px 0;
508
514
  text-align: start;
509
515
  }
@@ -561,10 +567,12 @@
561
567
  line-height: 1.4;
562
568
  }
563
569
 
564
- /* The row menu column holds one 34px button. */
570
+ /* The row menu column holds one 34px button between 16px of padding, so 66px
571
+ border to border (see the selection column for why the box model is forced). */
565
572
  .cn-look-board .cn-table-container--board .cn-data-table .cn-table-col--actions {
566
- width: 34px;
567
- min-width: 0;
573
+ box-sizing: border-box !important;
574
+ width: 66px;
575
+ min-width: 66px;
568
576
  padding-inline: 16px;
569
577
  text-align: end;
570
578
  }
package/src/index.js CHANGED
@@ -43,6 +43,8 @@ export {
43
43
  CnAppNav,
44
44
  CnAppRoot,
45
45
  CnAuditTrailCard,
46
+ CnAuditTrailWidget,
47
+ CnBannerWidget,
46
48
  CnBoardView,
47
49
  CnBodySections,
48
50
  CnBreadcrumbs,
@@ -70,6 +72,7 @@ export {
70
72
  CnContainerWidgetForm,
71
73
  CnContextMenu,
72
74
  CnConversationThread,
75
+ CnConversationWidget,
73
76
  CnCopyDialog,
74
77
  CnCountdownWidget,
75
78
  CnCountdownWidgetForm,
@@ -97,6 +100,7 @@ export {
97
100
  CnDividerWidget,
98
101
  CnDividerWidgetForm,
99
102
  CnDocumentReviewList,
103
+ CnDocumentReviewWidget,
100
104
  CnDurationField,
101
105
  CnEditActionsModal,
102
106
  CnEditDataModal,
@@ -164,11 +168,13 @@ export {
164
168
  CnIntegrationTab,
165
169
  CnIntegrationWidget,
166
170
  CnIntegrationWidgetGrid,
171
+ CnInteractionFormWidget,
167
172
  CnItemCard,
168
173
  CnJourney,
169
174
  CnJourneyDialog,
170
175
  CnJourneyReviewList,
171
176
  CnJsonViewer,
177
+ CnKbSearchWidget,
172
178
  CnKpiGrid,
173
179
  CnLabelWidget,
174
180
  CnLabelWidgetForm,
@@ -200,6 +206,7 @@ export {
200
206
  CnNcWidgetGridPicker,
201
207
  CnNcWidgetWidget,
202
208
  CnNextStepCard,
209
+ CnNextStepWidget,
203
210
  CnNoteBody,
204
211
  CnNoteCard,
205
212
  CnNoteComposer,
@@ -324,6 +331,7 @@ export {
324
331
  CnWidgetVisibilityRulesModal,
325
332
  CnWidgetWrapper,
326
333
  CnWizardDialog,
334
+ CnWorkspaceFilterWidget,
327
335
  DEFAULT_DATE_RANGE_PRESETS,
328
336
  defaultPageTypes,
329
337
  fetchWebdavFolderTree,