@conduction/nextcloud-vue 2.101.0 → 2.103.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/CnActionsBar/CnActionsBar.vue2.js +22 -0
  2. package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js.map +1 -1
  3. package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js +8 -6
  4. package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js.map +1 -1
  5. package/dist/esm/components/CnChartWidget/CnChartWidget.vue.js +1 -1
  6. package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js +29 -4
  7. package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js.map +1 -1
  8. package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js +9 -1
  9. package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js.map +1 -1
  10. package/dist/esm/components/CnConversationThread/CnConversationThread.vue.js +1 -1
  11. package/dist/esm/components/CnConversationThread/CnConversationThread.vue2.js +73 -0
  12. package/dist/esm/components/CnConversationThread/CnConversationThread.vue2.js.map +1 -1
  13. package/dist/esm/components/CnConversationThread/CnConversationThread.vue3.js +99 -64
  14. package/dist/esm/components/CnConversationThread/CnConversationThread.vue3.js.map +1 -1
  15. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue.js +1 -1
  16. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js +46 -0
  17. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js.map +1 -1
  18. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue3.js +37 -2
  19. package/dist/esm/components/CnConversationThread/CnConversationWidget.vue3.js.map +1 -1
  20. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue.js +1 -1
  21. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +49 -6
  22. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  23. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +1 -1
  24. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  25. package/dist/esm/components/CnFormDialog/CnFormDialog.vue.js +1 -1
  26. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +113 -20
  27. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
  28. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +387 -359
  29. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
  30. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +84 -4
  31. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  32. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +8 -6
  33. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  34. package/dist/esm/components/CnIndexPage/useNamedSource.js +26 -2
  35. package/dist/esm/components/CnIndexPage/useNamedSource.js.map +1 -1
  36. package/dist/esm/components/CnMenuWidget/CnMenuWidget.vue.js +1 -1
  37. package/dist/esm/components/CnMenuWidget/CnMenuWidget.vue2.js +25 -1
  38. package/dist/esm/components/CnMenuWidget/CnMenuWidget.vue2.js.map +1 -1
  39. package/dist/esm/components/CnMenuWidget/CnMenuWidget.vue3.js.map +1 -1
  40. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue.js +1 -1
  41. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +10 -1
  42. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
  43. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
  44. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue.js +1 -1
  45. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue2.js +106 -4
  46. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue2.js.map +1 -1
  47. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue3.js +70 -25
  48. package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue3.js.map +1 -1
  49. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue.js +1 -1
  50. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +97 -3
  51. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
  52. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js +46 -2
  53. package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
  54. package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue.js +9 -0
  55. package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue.js.map +1 -0
  56. package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue2.js +165 -0
  57. package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue2.js.map +1 -0
  58. package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue3.js +85 -0
  59. package/dist/esm/components/CnSearchSelect/CnSearchSelect.vue3.js.map +1 -0
  60. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue.js +1 -1
  61. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js +54 -6
  62. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js.map +1 -1
  63. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js +3 -1
  64. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js.map +1 -1
  65. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue.js +1 -1
  66. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +8 -1
  67. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
  68. package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
  69. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue.js +1 -1
  70. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js +2 -23
  71. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js.map +1 -1
  72. package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue3.js.map +1 -1
  73. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue.js +1 -1
  74. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue2.js +19 -0
  75. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue2.js.map +1 -1
  76. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue3.js +1 -1
  77. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue3.js.map +1 -1
  78. package/dist/esm/composables/indexSources.js +9 -0
  79. package/dist/esm/composables/indexSources.js.map +1 -1
  80. package/dist/esm/index.js +1 -0
  81. package/dist/esm/index.js.map +1 -1
  82. package/dist/esm/l10n/en.json.js +5 -1
  83. package/dist/esm/l10n/en.json.js.map +1 -1
  84. package/dist/esm/l10n/nl.json.js +5 -1
  85. package/dist/esm/l10n/nl.json.js.map +1 -1
  86. package/dist/esm/library-style.js +1 -1
  87. package/dist/esm/mixins/pageViews.js +8 -1
  88. package/dist/esm/mixins/pageViews.js.map +1 -1
  89. package/dist/esm/nextcloud-vue.css +593 -459
  90. package/dist/esm/utils/builtInFormatters.js +24 -4
  91. package/dist/esm/utils/builtInFormatters.js.map +1 -1
  92. package/dist/esm/utils/rowClassRules.js +43 -0
  93. package/dist/esm/utils/rowClassRules.js.map +1 -0
  94. package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
  95. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  96. package/dist/esm/utils/visibleWhen.js +32 -4
  97. package/dist/esm/utils/visibleWhen.js.map +1 -1
  98. package/dist/nextcloud-vue.cjs.js +10093 -8858
  99. package/dist/nextcloud-vue.cjs.js.map +2 -2
  100. package/dist/nextcloud-vue.css +593 -459
  101. package/l10n/en.json +5 -1
  102. package/l10n/nl.json +5 -1
  103. package/package.json +1 -1
  104. package/src/components/CnActionsBar/CnActionsBar.md +1 -0
  105. package/src/components/CnActionsBar/CnActionsBar.vue +31 -4
  106. package/src/components/CnChartWidget/CnChartWidget.md +3 -2
  107. package/src/components/CnChartWidget/CnChartWidget.vue +42 -4
  108. package/src/components/CnConversationThread/CnConversationThread.md +17 -0
  109. package/src/components/CnConversationThread/CnConversationThread.vue +113 -8
  110. package/src/components/CnConversationThread/CnConversationWidget.vue +60 -1
  111. package/src/components/CnDashboardPage/CnDashboardPage.md +4 -1
  112. package/src/components/CnDashboardPage/CnDashboardPage.vue +50 -7
  113. package/src/components/CnDetailPage/CnDetailPage.md +1 -1
  114. package/src/components/CnFormDialog/CnFormDialog.md +1 -1
  115. package/src/components/CnFormDialog/CnFormDialog.vue +127 -0
  116. package/src/components/CnIndexPage/CnIndexPage.md +15 -1
  117. package/src/components/CnIndexPage/CnIndexPage.vue +92 -9
  118. package/src/components/CnIndexPage/useNamedSource.js +26 -2
  119. package/src/components/CnMenuWidget/CnMenuWidget.vue +25 -1
  120. package/src/components/CnObjectListWidget/CnObjectListWidget.vue +10 -1
  121. package/src/components/CnQuickFilterBar/CnQuickFilterBar.md +1 -0
  122. package/src/components/CnQuickFilterBar/CnQuickFilterBar.vue +159 -5
  123. package/src/components/CnRecordListWidget/CnRecordListWidget.vue +134 -2
  124. package/src/components/CnSearchSelect/CnSearchSelect.vue +283 -0
  125. package/src/components/CnSearchSelect/index.js +12 -0
  126. package/src/components/CnSetupWizard/CnSetupWizard.vue +56 -6
  127. package/src/components/CnTabsWidget/CnTabsWidget.vue +8 -1
  128. package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +2 -23
  129. package/src/components/CnWizardDialog/CnWizardDialog.vue +20 -1
  130. package/src/components/index.js +1 -0
  131. package/src/composables/indexSources.js +9 -0
  132. package/src/css/dialog.css +5 -3
  133. package/src/css/look-board-index.css +10 -0
  134. package/src/index.js +1 -0
  135. package/src/mixins/pageViews.js +8 -1
  136. package/src/schemas/app-manifest-v2.schema.json +107 -8
  137. package/src/utils/builtInFormatters.js +24 -4
  138. package/src/utils/rowClassRules.js +39 -0
  139. package/src/utils/validateManifestV2.compiled.js +3 -3
  140. package/src/utils/visibleWhen.js +32 -4
@@ -0,0 +1,283 @@
1
+ <!--
2
+ - SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ - SPDX-License-Identifier: EUPL-1.2
4
+ -->
5
+ <template>
6
+ <div class="cn-search-select" :class="{ 'cn-search-select--label-outside': labelOutside }" data-testid="cn-search-select">
7
+ <NcSelect
8
+ :inputId="inputId"
9
+ :inputLabel="inputLabel"
10
+ :labelOutside="labelOutside"
11
+ :modelValue="selectedOption"
12
+ :options="options"
13
+ :loading="loading"
14
+ :clearable="clearable"
15
+ :disabled="disabled"
16
+ :placeholder="resolvedPlaceholder"
17
+ :filterable="filterable"
18
+ label="label"
19
+ @search="onSearch"
20
+ @update:modelValue="onInput">
21
+ <template #option="option">
22
+ <span class="cn-search-select__row" data-testid="cn-search-select-row">
23
+ <span
24
+ v-if="option.avatar || option.icon"
25
+ class="cn-search-select__lead"
26
+ :class="{ 'cn-search-select__lead--avatar': option.avatar && !option.icon }"
27
+ aria-hidden="true">
28
+ <CnIcon v-if="option.icon" :name="option.icon" :size="18" />
29
+ <template v-else>{{ initialsOf(option) }}</template>
30
+ </span>
31
+ <span class="cn-search-select__text">
32
+ <span class="cn-search-select__name">{{ option.label }}</span>
33
+ <span v-if="option.secondary" class="cn-search-select__secondary">{{ option.secondary }}</span>
34
+ </span>
35
+ <Check
36
+ v-if="isSelected(option)"
37
+ class="cn-search-select__check"
38
+ :size="18"
39
+ data-testid="cn-search-select-check" />
40
+ </span>
41
+ </template>
42
+ </NcSelect>
43
+ <Magnify class="cn-search-select__magnifier" :size="18" aria-hidden="true" />
44
+ </div>
45
+ </template>
46
+
47
+ <script>
48
+ import { translate as t } from '@nextcloud/l10n'
49
+ import { NcSelect } from '@nextcloud/vue'
50
+ import Check from 'vue-material-design-icons/Check.vue'
51
+ import Magnify from 'vue-material-design-icons/Magnify.vue'
52
+ import CnIcon from '../CnIcon/CnIcon.vue'
53
+ import { initialsOf } from '../../utils/initials.js'
54
+
55
+ /**
56
+ * CnSearchSelect — a single picker drawn as a search field: a magnifier
57
+ * instead of the chevron, and results as rows with an icon or initials, a
58
+ * name, a secondary line and a check on the chosen one (boards DqOverdragen
59
+ * "Nieuw team", DqNieuweZaak "Aanvrager").
60
+ *
61
+ * Each option is `{ id, label, secondary?, icon?, avatar? }`: `icon` names a
62
+ * CnIcon, `avatar: true` draws the label's initials in a circle. The value is
63
+ * the option's `id` (or `value`); `update:modelValue` emits that id, or null
64
+ * when cleared. Searching is the host's: listen to `search` and pass new
65
+ * `options`, with `filterable: false` when the host already filtered them.
66
+ *
67
+ * ```vue
68
+ * <CnSearchSelect
69
+ * v-model="team"
70
+ * inputLabel="Nieuw team"
71
+ * :options="[{ id: 'legal', label: 'Juridische zaken', secondary: '6 leden · bezwaar, beroep en Woo-advies', avatar: true }]" />
72
+ * ```
73
+ */
74
+ export default {
75
+ name: 'CnSearchSelect',
76
+
77
+ components: { Check, CnIcon, Magnify, NcSelect },
78
+
79
+ props: {
80
+ /** The chosen option's id, or the option itself. */
81
+ modelValue: {
82
+ type: [String, Number, Object],
83
+ default: null,
84
+ },
85
+
86
+ /**
87
+ * The options: `{ id, label, secondary?, icon?, avatar? }`.
88
+ *
89
+ * @type {Array<{id: (string|number), label: string, secondary?: string, icon?: string, avatar?: boolean}>}
90
+ */
91
+ options: {
92
+ type: Array,
93
+ default: () => [],
94
+ },
95
+
96
+ /** The field's label. */
97
+ inputLabel: {
98
+ type: String,
99
+ default: '',
100
+ },
101
+
102
+ /** Draw the label above the field instead of inside it. */
103
+ labelOutside: {
104
+ type: Boolean,
105
+ default: false,
106
+ },
107
+
108
+ /** The input's id. */
109
+ inputId: {
110
+ type: String,
111
+ default: undefined,
112
+ },
113
+
114
+ /** Placeholder text; defaults to "Search…". */
115
+ placeholder: {
116
+ type: String,
117
+ default: '',
118
+ },
119
+
120
+ /** Show the loading spinner. */
121
+ loading: {
122
+ type: Boolean,
123
+ default: false,
124
+ },
125
+
126
+ /** Offer a clear button. */
127
+ clearable: {
128
+ type: Boolean,
129
+ default: true,
130
+ },
131
+
132
+ /** Disable the field. */
133
+ disabled: {
134
+ type: Boolean,
135
+ default: false,
136
+ },
137
+
138
+ /** Filter the options on the typed text; false when the host searches. */
139
+ filterable: {
140
+ type: Boolean,
141
+ default: true,
142
+ },
143
+ },
144
+
145
+ emits: ['update:modelValue', 'search'],
146
+
147
+ computed: {
148
+ /**
149
+ * The option matching `modelValue`, or a bare one carrying it.
150
+ *
151
+ * @return {object|null}
152
+ */
153
+ selectedOption() {
154
+ const value = this.modelValue
155
+ if (value === null || value === undefined || value === '') {
156
+ return null
157
+ }
158
+ if (typeof value === 'object') {
159
+ return value
160
+ }
161
+ return this.options.find((o) => this.idOf(o) === String(value)) || { id: value, label: String(value) }
162
+ },
163
+
164
+ /** @return {string} The placeholder. */
165
+ resolvedPlaceholder() {
166
+ return this.placeholder || t('nextcloud-vue', 'Search…')
167
+ },
168
+ },
169
+
170
+ methods: {
171
+ initialsOf(option) {
172
+ return initialsOf(option.label || '')
173
+ },
174
+
175
+ /**
176
+ * @param {object} option An option.
177
+ * @return {string} Its id as a string, '' without one.
178
+ */
179
+ idOf(option) {
180
+ const id = option && (option.id ?? option.value)
181
+ return id === null || id === undefined ? '' : String(id)
182
+ },
183
+
184
+ /**
185
+ * @param {object} option An option.
186
+ * @return {boolean} Whether it is the chosen one.
187
+ */
188
+ isSelected(option) {
189
+ return this.selectedOption !== null && this.idOf(option) !== '' && this.idOf(option) === this.idOf(this.selectedOption)
190
+ },
191
+
192
+ /**
193
+ * @param {object|null} option The picked option.
194
+ */
195
+ onInput(option) {
196
+ const id = option ? (option.id ?? option.value ?? null) : null
197
+ /** @event update:modelValue The picked option's id, or null when cleared. */
198
+ this.$emit('update:modelValue', id)
199
+ },
200
+
201
+ /**
202
+ * @param {string} query The typed text.
203
+ */
204
+ onSearch(query) {
205
+ /** @event search The typed text, for a host that searches itself. */
206
+ this.$emit('search', query)
207
+ },
208
+ },
209
+ }
210
+ </script>
211
+
212
+ <style scoped>
213
+ .cn-search-select {
214
+ position: relative;
215
+ }
216
+
217
+ /* The magnifier sits where the chevron was: in the field, at its end. */
218
+ .cn-search-select__magnifier {
219
+ position: absolute;
220
+ inset-inline-end: 12px;
221
+ bottom: 0;
222
+ height: var(--default-clickable-area, 34px);
223
+ display: flex;
224
+ align-items: center;
225
+ color: var(--color-text-maxcontrast);
226
+ pointer-events: none;
227
+ }
228
+
229
+ .cn-search-select :deep(.vs__open-indicator),
230
+ .cn-search-select :deep(.vs__open-indicator-button) {
231
+ display: none;
232
+ }
233
+
234
+ .cn-search-select :deep(.vs__actions) {
235
+ padding-inline-end: 36px;
236
+ }
237
+
238
+ .cn-search-select__row {
239
+ display: flex;
240
+ align-items: center;
241
+ gap: 10px;
242
+ width: 100%;
243
+ min-width: 0;
244
+ padding: 4px 0;
245
+ }
246
+
247
+ .cn-search-select__lead {
248
+ flex: 0 0 32px;
249
+ width: 32px;
250
+ height: 32px;
251
+ border-radius: 50%;
252
+ display: inline-flex;
253
+ align-items: center;
254
+ justify-content: center;
255
+ background: var(--color-primary-element-light);
256
+ color: var(--color-primary-element-light-text);
257
+ font-size: 13px;
258
+ font-weight: 600;
259
+ }
260
+
261
+ .cn-search-select__text {
262
+ display: flex;
263
+ flex-direction: column;
264
+ flex: 1 1 auto;
265
+ min-width: 0;
266
+ }
267
+
268
+ .cn-search-select__name {
269
+ font-weight: 600;
270
+ overflow-wrap: anywhere;
271
+ }
272
+
273
+ .cn-search-select__secondary {
274
+ font-size: 13px;
275
+ color: var(--color-text-maxcontrast);
276
+ overflow-wrap: anywhere;
277
+ }
278
+
279
+ .cn-search-select__check {
280
+ flex: 0 0 auto;
281
+ color: var(--color-primary-element);
282
+ }
283
+ </style>
@@ -0,0 +1,12 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ * SPDX-License-Identifier: EUPL-1.2
4
+ *
5
+ * CnSearchSelect — a single picker drawn as a search field with rows that
6
+ * carry an icon, a name, a secondary line and a check on the chosen one.
7
+ */
8
+
9
+ import CnSearchSelect from './CnSearchSelect.vue'
10
+
11
+ export { CnSearchSelect }
12
+ export default CnSearchSelect
@@ -5,6 +5,7 @@
5
5
  ref="wizard"
6
6
  :steps="wizardSteps"
7
7
  :dialogTitle="resolvedDialogTitle"
8
+ stepHeading
8
9
  :submitLabel="submitLabel"
9
10
  :cancelLabel="cancelLabel"
10
11
  :nextLabel="nextLabel"
@@ -79,6 +80,7 @@
79
80
  </NcNoteCard>
80
81
  <CnChoiceCards
81
82
  v-if="isCardChoice(step)"
83
+ :layout="step.display === 'rows' ? 'rows' : 'cards'"
82
84
  :label="stepTitle(step)"
83
85
  :options="optionsFor(step)"
84
86
  :multiple="step.multiple === true"
@@ -464,14 +466,16 @@ export default {
464
466
  computed: {
465
467
  /**
466
468
  * The title actually rendered by CnWizardDialog: "Set up {appName}"
467
- * when a display name was passed, otherwise whatever `dialogTitle`
468
- * resolves to (a caller override, or its own generic default).
469
+ * when a display name was passed (the name through the host's
470
+ * translate), otherwise whatever `dialogTitle` resolves to (a caller
471
+ * override, or its own generic default). In the board look it is the
472
+ * eyebrow's context and the current step's title is the heading.
469
473
  *
470
474
  * @return {string}
471
475
  */
472
476
  resolvedDialogTitle() {
473
477
  return this.appName
474
- ? t('nextcloud-vue', 'Set up {app}', { app: this.appName })
478
+ ? t('nextcloud-vue', 'Set up {app}', { app: this.cnTranslate(this.appName) })
475
479
  : this.dialogTitle
476
480
  },
477
481
 
@@ -727,6 +731,12 @@ export default {
727
731
  },
728
732
  },
729
733
 
734
+ created() {
735
+ for (const step of this.setupSteps) {
736
+ this.applyDefault(step)
737
+ }
738
+ },
739
+
730
740
  mounted() {
731
741
  // Covers a RESUMED wizard opening straight onto its run-action step
732
742
  // (`initialStepId`) — `onStepChange` only fires on later navigation,
@@ -944,13 +954,14 @@ export default {
944
954
  },
945
955
 
946
956
  /**
947
- * Whether a choice step renders as a card grid rather than a dropdown.
957
+ * Whether a choice step renders as CnChoiceCards (a card grid, or radio
958
+ * rows with `display: "rows"`) rather than a dropdown.
948
959
  *
949
960
  * @param {object} step The choice step.
950
- * @return {boolean} True for `display: "cards"`.
961
+ * @return {boolean} True for `display: "cards"` and `display: "rows"`.
951
962
  */
952
963
  isCardChoice(step) {
953
- return step.display === 'cards'
964
+ return step.display === 'cards' || step.display === 'rows'
954
965
  },
955
966
 
956
967
  /**
@@ -1054,6 +1065,36 @@ export default {
1054
1065
  }
1055
1066
  },
1056
1067
 
1068
+ /**
1069
+ * Preselect a `choice` step's `default` option. Skipped once the user
1070
+ * picked here, when a value is already chosen, and for a step the
1071
+ * server already completed (its stored value must not be overwritten
1072
+ * by a Next). A card step holds the plain value; a dropdown holds the
1073
+ * option object, or the value while the options are still loading.
1074
+ *
1075
+ * @param {object} step The choice step.
1076
+ * @return {void}
1077
+ */
1078
+ applyDefault(step) {
1079
+ if (!step || step.type !== 'choice' || step.default === undefined || step.default === null) {
1080
+ return
1081
+ }
1082
+ if (this.userTouched[step.id] === true || this.hasChoice(step) || this.isServerDone(step.id)) {
1083
+ return
1084
+ }
1085
+ const wanted = step.multiple === true ? [].concat(step.default) : step.default
1086
+ if (this.isCardChoice(step)) {
1087
+ this.choiceModel[step.id] = wanted
1088
+ } else {
1089
+ const options = this.optionsFor(step)
1090
+ const pick = (value) => options.find((o) => o.value === value) || value
1091
+ this.choiceModel[step.id] = Array.isArray(wanted) ? wanted.map(pick) : pick(wanted)
1092
+ }
1093
+ if (step.configKey) {
1094
+ this.choiceValues[step.configKey] = this.scalarChoice(step)
1095
+ }
1096
+ },
1097
+
1057
1098
  /**
1058
1099
  * Auto-fill a `choice` step's suggested default from an earlier choice
1059
1100
  * (its `suggestFrom` configKey → `suggestMap[parentValue]`). No-op once
@@ -1360,6 +1401,15 @@ export default {
1360
1401
  if (step && step.suggestFrom) {
1361
1402
  this.applySuggestion(step)
1362
1403
  }
1404
+ // A default seeded before a dropdown's options arrived is a bare
1405
+ // value; swap in its option object now the step opens.
1406
+ if (step && step.default !== undefined && this.userTouched[step.id] !== true && !this.isCardChoice(step)) {
1407
+ const held = this.choiceModel[step.id]
1408
+ const option = this.optionsFor(step).find((o) => o.value === held)
1409
+ if (option && held !== null && typeof held !== 'object') {
1410
+ this.choiceModel[step.id] = option
1411
+ }
1412
+ }
1363
1413
  this.maybeAutoRunStep(payload.stepId)
1364
1414
  /**
1365
1415
  * @event step-change Emitted when the active step changes.
@@ -624,6 +624,7 @@ export default {
624
624
  // is already going under "More" does not use one up.
625
625
  let placed = pinActivity ? 1 : 0
626
626
  const resolved = []
627
+ const usedKeys = new Set()
627
628
  tabs.forEach((tab, position) => {
628
629
  const index = originalIndex[position]
629
630
  // A tab whose condition is false is absent, not empty.
@@ -654,8 +655,14 @@ export default {
654
655
  placed += 1
655
656
  }
656
657
  }
658
+ // Keyed on the tab's own id, else its place in `content.tabs`:
659
+ // never on the widget the panel starts with, which changes when
660
+ // a stack's first card turns visible and would remount the panel.
661
+ const ownId = tab && typeof tab === 'object' && typeof tab.id === 'string' ? tab.id : ''
662
+ const key = ownId !== '' && !usedKeys.has(ownId) ? ownId : `tab-${index}`
663
+ usedKeys.add(key)
657
664
  resolved.push({
658
- key: `${widgetId || 'tab'}-${index}`,
665
+ key,
659
666
  id: (tab && typeof tab === 'object' && tab.id) || widgetId || '',
660
667
  widgetId,
661
668
  label: this.tabLabel(tab, widget, widgetId),
@@ -88,8 +88,8 @@ import { dispatchAction, resolveObjectOpType } from '../../utils/actionsDispatch
88
88
  import { resolveObjectTokenContext } from '../../utils/detailObjectContext.js'
89
89
  import { pageFilter, pageSort } from '../../utils/pageControls.js'
90
90
  import { dropOptionalUnresolved, hasUnresolvedTokens, resolveFilterTokens } from '../../utils/resolveFilterTokens.js'
91
+ import { compileRowClass } from '../../utils/rowClassRules.js'
91
92
  import { fieldsFromSchema } from '../../utils/schema.js'
92
- import { compareVisibleWhen, readVisibleWhenPath } from '../../utils/visibleWhen.js'
93
93
  import { CnIcon } from '../CnIcon/index.js'
94
94
  import { CnRowActions } from '../CnRowActions/index.js'
95
95
  import { CnWidgetWrapper } from '../CnWidgetWrapper/index.js'
@@ -762,28 +762,7 @@ export default {
762
762
  * @return {?(row: object) => string} The row-class function, or null.
763
763
  */
764
764
  compiledRowClass() {
765
- const rc = this.rowClass
766
- if (typeof rc === 'function') {
767
- return rc
768
- }
769
- if (!Array.isArray(rc) || rc.length === 0) {
770
- return null
771
- }
772
- const rules = rc
773
- return (row) => {
774
- const classes = []
775
- for (const rule of rules) {
776
- if (!rule || !rule.class) {
777
- continue
778
- }
779
- const when = rule.when || {}
780
- const actual = readVisibleWhenPath(row, when.field)
781
- if (compareVisibleWhen(actual, when.op || 'eq', when.value)) {
782
- classes.push(rule.class)
783
- }
784
- }
785
- return classes.join(' ')
786
- }
765
+ return compileRowClass(this.rowClass)
787
766
  },
788
767
 
789
768
  /**
@@ -5,7 +5,7 @@
5
5
  defaultWidth="wizard"
6
6
  :eyebrow="boardEyebrow"
7
7
  :subtitle="subtitle"
8
- :name="dialogTitle"
8
+ :name="shownTitle"
9
9
  size="large"
10
10
  :open="dialogOpen"
11
11
  :noClose="loading || !cancellable"
@@ -265,6 +265,12 @@ export default {
265
265
  * Empty uses the dialog title.
266
266
  */
267
267
  eyebrowContext: { type: String, default: '' },
268
+ /**
269
+ * Board look: head the dialog with the current step's label, the
270
+ * dialog title moving to the eyebrow's context. The result phase and
271
+ * the Nextcloud look keep `dialogTitle`.
272
+ */
273
+ stepHeading: { type: Boolean, default: false },
268
274
  /**
269
275
  * The eyebrow sentence, with `{context}`, `{step}` and `{total}`
270
276
  * placeholders, so it can be translated.
@@ -356,6 +362,19 @@ export default {
356
362
  .replace('{total}', String(this.steps.length))
357
363
  },
358
364
 
365
+ /**
366
+ * The dialog's heading: the current step's label with `stepHeading`
367
+ * in the board look, otherwise `dialogTitle`.
368
+ *
369
+ * @return {string} The heading.
370
+ */
371
+ shownTitle() {
372
+ if (this.stepHeading && this.isBoardLook && this.result === null && this.currentStep && this.currentStep.label) {
373
+ return this.currentStep.label
374
+ }
375
+ return this.dialogTitle
376
+ },
377
+
359
378
  /** @return {object} The check icon, the default finishing icon. */
360
379
  Check() {
361
380
  return Check
@@ -130,6 +130,7 @@ export { default as CnContactsTab } from '../integrations/builtin/contacts/CnCon
130
130
  export { CnContactPicker } from './CnContactPicker/index.js'
131
131
  export { CnContactCreate } from './CnContactCreate/index.js'
132
132
  export { CnResourceSelect } from './CnResourceSelect/index.js'
133
+ export { CnSearchSelect } from './CnSearchSelect/index.js'
133
134
  export { CnPropertySourceField } from './CnPropertySourceField/index.js'
134
135
  export { CnDurationField } from './CnDurationField/index.js'
135
136
  export { CnFavouriteToggle } from './CnFavouriteToggle/index.js'
@@ -530,6 +530,15 @@ function tasksSource() {
530
530
  */
531
531
  loading: () => !!store.loading,
532
532
 
533
+ // The inbox pages: the page sends `limit` and `offset` and draws the
534
+ // pager from the datastore total.
535
+ pages: true,
536
+
537
+ /**
538
+ * @return {number} The datastore total, independent of the page size.
539
+ */
540
+ total: () => Number(store.total) || 0,
541
+
533
542
  /**
534
543
  * Count the inbox for a config without touching the rows on screen,
535
544
  * for a quick-filter tab with `showCount`.
@@ -9,14 +9,16 @@
9
9
  * Theme hooks (all optional, defaults below):
10
10
  * --cn-dialog-danger fill of a destructive primary
11
11
  * --cn-dialog-eyebrow-color eyebrow colour
12
- * --cn-dialog-eyebrow-transform uppercase (default) or none
12
+ * --cn-dialog-eyebrow-transform none (default, sentence case) or uppercase
13
+ * --cn-dialog-eyebrow-tracking letter spacing; 0 by default, 0.06em suits capitals
13
14
  */
14
15
 
15
16
  .cn-dialog-board {
16
17
  --cn-dialog-width: 640px;
17
18
  --cn-dialog-danger: var(--color-error);
18
19
  --cn-dialog-eyebrow-color: var(--color-primary-element-text, var(--color-main-text));
19
- --cn-dialog-eyebrow-transform: uppercase;
20
+ --cn-dialog-eyebrow-transform: none;
21
+ --cn-dialog-eyebrow-tracking: 0;
20
22
  }
21
23
 
22
24
  /* Dimmed page behind the dialog: text colour at 45%. */
@@ -73,7 +75,7 @@
73
75
  margin-bottom: 4px;
74
76
  font-size: 13px;
75
77
  font-weight: 700;
76
- letter-spacing: 0.06em;
78
+ letter-spacing: var(--cn-dialog-eyebrow-tracking, 0.06em);
77
79
  text-transform: var(--cn-dialog-eyebrow-transform, uppercase);
78
80
  color: var(--cn-dialog-eyebrow-color, var(--color-main-text));
79
81
  }
@@ -128,6 +128,16 @@
128
128
  gap: 8px;
129
129
  }
130
130
 
131
+ /* `quickFilterPlacement: "toolbar"` (DqTeamwachtrij): the chips take the room
132
+ the Filter button and the view switch leave on row 1, so the three stay on
133
+ one row in the column beside a side panel; the chips that do not fit move
134
+ into the overflow chip (CnQuickFilterBar `fit`) instead of pushing the
135
+ buttons onto a row of their own. */
136
+ .cn-look-board.cn-index-page--quick-filters-toolbar .cn-actions-bar--board > .cn-actions-bar__row--views > .cn-quick-filter-bar {
137
+ flex: 1 1 0;
138
+ min-width: 0;
139
+ }
140
+
131
141
  /* Row 2: the search field and the active filters on a line of their own. */
132
142
  .cn-look-board .cn-actions-bar--board > .cn-actions-bar__row--search {
133
143
  display: flex;
package/src/index.js CHANGED
@@ -279,6 +279,7 @@ export {
279
279
  CnSaveViewDialog,
280
280
  CnSchemaFormDialog,
281
281
  CnSearchPage,
282
+ CnSearchSelect,
282
283
  CnSectionsWidget,
283
284
  CnSettingsCard,
284
285
  CnSettingsField,
@@ -270,12 +270,14 @@ export const pageViews = {
270
270
 
271
271
  /**
272
272
  * Whether a user arranges the views for themselves (`userLayout`).
273
+ * Not while Buildiq edits the page (`editingBody`): Buildiq is the
274
+ * layout editor, and it shows and changes the manifest layout.
273
275
  *
274
276
  * @return {boolean}
275
277
  * @spec openspec/changes/page-view-user-layouts/specs/view-switch-containers/spec.md#requirement-a-user-arranges-each-view-for-themselves
276
278
  */
277
279
  viewUserLayoutOn() {
278
- return this.userLayout === true && this.hasViews
280
+ return this.userLayout === true && this.hasViews && this.editingBody !== true
279
281
  },
280
282
 
281
283
  /**
@@ -640,6 +642,11 @@ export const pageViews = {
640
642
  this.viewUserLayouts = { ...this.viewUserLayouts, [view.id]: arranged }
641
643
  this.viewUserLayoutsDirty = { ...this.viewUserLayoutsDirty, [view.id]: true }
642
644
  this.$emit('view-layout-change', { view: view.id, layout: arranged })
645
+ // Outside an in-page edit session (the dashboard's edit mode, the
646
+ // detail page's Arrange) no Done will come to save it.
647
+ if (this.isEditing !== true && this.arrangingView !== true) {
648
+ this.saveViewUserLayouts()
649
+ }
643
650
  return
644
651
  }
645
652
  for (const u of updated) {