@conduction/nextcloud-vue 2.70.0 → 2.72.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 (85) hide show
  1. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +151 -13
  2. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
  3. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js +3 -1
  4. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
  5. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  6. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +38 -42
  7. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  8. package/dist/esm/components/CnFlowDetail/CnRunDetailSidebar.vue2.js +2 -0
  9. package/dist/esm/components/CnFlowDetail/CnRunDetailSidebar.vue2.js.map +1 -1
  10. package/dist/esm/components/CnFlowDetail/CnRunDetailSidebar.vue3.js +7 -0
  11. package/dist/esm/components/CnFlowDetail/CnRunDetailSidebar.vue3.js.map +1 -1
  12. package/dist/esm/components/CnFlowStepOutcomes/CnFlowStepOutcomes.vue.js +10 -0
  13. package/dist/esm/components/CnFlowStepOutcomes/CnFlowStepOutcomes.vue.js.map +1 -0
  14. package/dist/esm/components/CnFlowStepOutcomes/CnFlowStepOutcomes.vue2.js +150 -0
  15. package/dist/esm/components/CnFlowStepOutcomes/CnFlowStepOutcomes.vue2.js.map +1 -0
  16. package/dist/esm/components/CnFlowStepOutcomes/CnFlowStepOutcomes.vue3.js +54 -0
  17. package/dist/esm/components/CnFlowStepOutcomes/CnFlowStepOutcomes.vue3.js.map +1 -0
  18. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +29 -2
  19. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
  20. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +71 -34
  21. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
  22. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +22 -0
  23. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  24. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +1 -1
  25. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  26. package/dist/esm/components/CnObjectSidebar/CnNotesTab.vue2.js +1 -1
  27. package/dist/esm/components/CnObjectSidebar/CnNotesTab.vue2.js.map +1 -1
  28. package/dist/esm/components/CnObjectSidebar/CnNotesTab.vue3.js.map +1 -1
  29. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js.map +1 -1
  30. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js +46 -32
  31. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js.map +1 -1
  32. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js +30 -9
  33. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js.map +1 -1
  34. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue3.js.map +1 -1
  35. package/dist/esm/composables/useWalkthrough.js +11 -3
  36. package/dist/esm/composables/useWalkthrough.js.map +1 -1
  37. package/dist/esm/dialogs/CnFlowNodeEditModal.vue2.js +7 -10
  38. package/dist/esm/dialogs/CnFlowNodeEditModal.vue2.js.map +1 -1
  39. package/dist/esm/dialogs/CnFlowNodeEditModal.vue3.js +23 -13
  40. package/dist/esm/dialogs/CnFlowNodeEditModal.vue3.js.map +1 -1
  41. package/dist/esm/dialogs/CnRunNodeDialog.vue2.js +8 -6
  42. package/dist/esm/dialogs/CnRunNodeDialog.vue2.js.map +1 -1
  43. package/dist/esm/dialogs/CnRunNodeDialog.vue3.js.map +1 -1
  44. package/dist/esm/index.js +1 -0
  45. package/dist/esm/index.js.map +1 -1
  46. package/dist/esm/l10n/en.json.js +15 -1
  47. package/dist/esm/l10n/en.json.js.map +1 -1
  48. package/dist/esm/l10n/nl.json.js +15 -1
  49. package/dist/esm/l10n/nl.json.js.map +1 -1
  50. package/dist/esm/nextcloud-vue.css +117 -0
  51. package/dist/esm/utils/columnFilters.js +54 -4
  52. package/dist/esm/utils/columnFilters.js.map +1 -1
  53. package/dist/esm/utils/selectOptions.js +57 -0
  54. package/dist/esm/utils/selectOptions.js.map +1 -0
  55. package/dist/esm/utils/userAutocomplete.js +69 -3
  56. package/dist/esm/utils/userAutocomplete.js.map +1 -1
  57. package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
  58. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  59. package/dist/nextcloud-vue.cjs.js +6560 -5880
  60. package/dist/nextcloud-vue.cjs.js.map +2 -2
  61. package/dist/nextcloud-vue.css +117 -0
  62. package/l10n/en.json +15 -1
  63. package/l10n/nl.json +15 -1
  64. package/package.json +1 -1
  65. package/src/components/CnAppRoot/CnAppRoot.vue +153 -13
  66. package/src/components/CnDashboardPage/CnDashboardPage.vue +20 -18
  67. package/src/components/CnFlowDetail/CnRunDetailSidebar.vue +6 -0
  68. package/src/components/CnFlowStepOutcomes/CnFlowStepOutcomes.vue +250 -0
  69. package/src/components/CnFlowStepOutcomes/index.js +8 -0
  70. package/src/components/CnFormDialog/CnFormDialog.vue +62 -2
  71. package/src/components/CnIndexPage/CnIndexPage.vue +23 -1
  72. package/src/components/CnObjectSidebar/CnNotesTab.vue +1 -1
  73. package/src/components/CnSetupWizard/CnSetupWizard.vue +7 -0
  74. package/src/components/CnWalkthrough/CnWalkthrough.vue +30 -9
  75. package/src/components/index.js +1 -0
  76. package/src/composables/useWalkthrough.js +11 -3
  77. package/src/css/dashboard.css +44 -0
  78. package/src/dialogs/CnFlowNodeEditModal.vue +41 -20
  79. package/src/dialogs/CnRunNodeDialog.vue +8 -6
  80. package/src/index.js +1 -0
  81. package/src/schemas/app-manifest-v2.schema.json +2 -1
  82. package/src/utils/columnFilters.js +54 -4
  83. package/src/utils/selectOptions.js +54 -0
  84. package/src/utils/userAutocomplete.js +68 -2
  85. package/src/utils/validateManifestV2.compiled.js +2 -2
@@ -0,0 +1,250 @@
1
+ <!--
2
+ CnFlowStepOutcomes: who a send step reached, and who it did not, by name.
3
+
4
+ A messaging step (send email, send notification, post to Talk) writes its
5
+ per-recipient outcome onto its run-log entry as `report.messaging`: one
6
+ bucket per outcome, each `{count, sample}`. Nothing rendered it, so the run
7
+ log could not show that a person was skipped because they opted out, or
8
+ because the opt-out check did not answer.
9
+
10
+ Only buckets with someone in them are listed. A bucket this component does
11
+ not know is still shown, under its own key made readable, so a new outcome
12
+ on the server is never hidden by an older library.
13
+
14
+ SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>
15
+ SPDX-License-Identifier: EUPL-1.2
16
+ -->
17
+ <template>
18
+ <div v-if="rows.length" class="cn-flow-step-outcomes" data-testid="flow-step-outcomes">
19
+ <dl class="cn-flow-step-outcomes__list" :aria-label="t('nextcloud-vue', 'Who this step reached')">
20
+ <div v-for="row in rows"
21
+ :key="row.key"
22
+ class="cn-flow-step-outcomes__row"
23
+ :class="`cn-flow-step-outcomes__row--${row.tone}`"
24
+ :data-testid="`flow-step-outcome-${row.key}`">
25
+ <dt class="cn-flow-step-outcomes__name">
26
+ {{ row.label }}
27
+ </dt>
28
+ <dd class="cn-flow-step-outcomes__value">
29
+ <span class="cn-flow-step-outcomes__count">{{ row.count }}</span>
30
+ <span v-if="row.sample.length" class="cn-flow-step-outcomes__sample"> · {{ row.sample.join(', ') }}</span>
31
+ </dd>
32
+ </div>
33
+ </dl>
34
+ <p v-if="report && report.truncated"
35
+ class="cn-flow-step-outcomes__truncated"
36
+ data-testid="flow-step-outcomes-truncated">
37
+ {{ t('nextcloud-vue', 'Each list shows its first entries only.') }}
38
+ </p>
39
+ </div>
40
+ </template>
41
+
42
+ <script>
43
+ import { translate as t } from '@nextcloud/l10n'
44
+
45
+ /**
46
+ * The buckets a messaging report carries, in reading order: who got it first,
47
+ * then each reason someone did not. `tone` marks the row for styling only.
48
+ */
49
+ const KNOWN_BUCKETS = [
50
+ { key: 'delivered', tone: 'success' },
51
+ { key: 'optedOut', tone: 'neutral' },
52
+ { key: 'authorityUnavailable', tone: 'warning' },
53
+ { key: 'skippedByPreference', tone: 'neutral' },
54
+ { key: 'skippedByKillSwitch', tone: 'neutral' },
55
+ { key: 'rateLimited', tone: 'warning' },
56
+ { key: 'refusedRecipients', tone: 'warning' },
57
+ { key: 'unknownRecipients', tone: 'warning' },
58
+ { key: 'failed', tone: 'error' },
59
+ ]
60
+
61
+ /** Report keys that are facts about the send, not outcome buckets. */
62
+ const NOT_BUCKETS = ['channel', 'actor', 'recipients', 'truncated']
63
+
64
+ export default {
65
+ name: 'CnFlowStepOutcomes',
66
+
67
+ props: {
68
+ /**
69
+ * A step's messaging report: the run-log entry's `report.messaging`.
70
+ * Each outcome bucket is `{count, sample}`; a `refusedRecipients`
71
+ * sample entry is `{recipient, reason}`. `null` renders nothing.
72
+ */
73
+ report: {
74
+ type: Object,
75
+ default: null,
76
+ },
77
+
78
+ /**
79
+ * Names to use instead of the built-in ones, keyed by bucket
80
+ * (`{ optedOut: 'Unsubscribed' }`). Pass already-translated text.
81
+ */
82
+ labels: {
83
+ type: Object,
84
+ default: () => ({}),
85
+ },
86
+ },
87
+
88
+ computed: {
89
+ /**
90
+ * The non-empty buckets, known ones first in reading order, then any
91
+ * bucket the server added that this component does not know.
92
+ *
93
+ * @return {Array<object>} One row per bucket: key, label, count, sample, tone.
94
+ */
95
+ rows() {
96
+ const report = this.report
97
+ if (!report || typeof report !== 'object') {
98
+ return []
99
+ }
100
+
101
+ const known = KNOWN_BUCKETS.map((bucket) => bucket.key)
102
+ const extra = Object.keys(report).filter((key) => !known.includes(key) && !NOT_BUCKETS.includes(key))
103
+ const ordered = [
104
+ ...KNOWN_BUCKETS,
105
+ ...extra.map((key) => ({ key, tone: 'neutral' })),
106
+ ]
107
+
108
+ return ordered
109
+ .map(({ key, tone }) => ({ key, tone, bucket: report[key] }))
110
+ .filter(({ bucket }) => bucket && typeof bucket === 'object' && Number(bucket.count) > 0)
111
+ .map(({ key, tone, bucket }) => ({
112
+ key,
113
+ tone,
114
+ label: this.labelFor(key),
115
+ count: Number(bucket.count),
116
+ sample: (Array.isArray(bucket.sample) ? bucket.sample : []).map((entry) => this.describeEntry(entry)),
117
+ }))
118
+ },
119
+ },
120
+
121
+ methods: {
122
+ t,
123
+
124
+ /**
125
+ * The readable name of a bucket: the host's, then the built-in one,
126
+ * then the key itself (`heldForReview` reads "Held for review").
127
+ *
128
+ * @param {string} key The bucket key.
129
+ * @return {string} The name.
130
+ */
131
+ labelFor(key) {
132
+ if (this.labels && this.labels[key]) {
133
+ return String(this.labels[key])
134
+ }
135
+
136
+ const builtIn = {
137
+ delivered: t('nextcloud-vue', 'Delivered'),
138
+ optedOut: t('nextcloud-vue', 'Opted out'),
139
+ authorityUnavailable: t('nextcloud-vue', 'Not sent, the opt-out check did not answer'),
140
+ skippedByPreference: t('nextcloud-vue', 'Skipped by their notification settings'),
141
+ skippedByKillSwitch: t('nextcloud-vue', 'Skipped, sending is switched off'),
142
+ rateLimited: t('nextcloud-vue', 'Held back by the send limit'),
143
+ refusedRecipients: t('nextcloud-vue', 'Refused by the step’s address rule'),
144
+ unknownRecipients: t('nextcloud-vue', 'Not a known user or group'),
145
+ failed: t('nextcloud-vue', 'Failed'),
146
+ }
147
+ if (builtIn[key]) {
148
+ return builtIn[key]
149
+ }
150
+
151
+ const words = String(key).replace(/([a-z0-9])([A-Z])/g, '$1 $2').replace(/[-_]+/g, ' ').trim().toLowerCase()
152
+
153
+ return words.charAt(0).toUpperCase() + words.slice(1)
154
+ },
155
+
156
+ /**
157
+ * One sample entry as text. A refused address carries its reason.
158
+ *
159
+ * @param {string|object} entry The sample entry.
160
+ * @return {string} The text.
161
+ */
162
+ describeEntry(entry) {
163
+ if (entry && typeof entry === 'object') {
164
+ const who = String(entry.recipient ?? entry.id ?? '')
165
+ const reason = this.reasonFor(entry.reason)
166
+
167
+ return reason ? `${who} (${reason})` : who
168
+ }
169
+
170
+ return String(entry)
171
+ },
172
+
173
+ /**
174
+ * Why the step's address rule refused an address, in words.
175
+ *
176
+ * @param {string} [code] The refusal code.
177
+ * @return {string} The reason, or the code when it is not known.
178
+ */
179
+ reasonFor(code) {
180
+ const reasons = {
181
+ 'external-recipients-off': t('nextcloud-vue', 'external recipients are off'),
182
+ 'not-on-item': t('nextcloud-vue', 'not on the item'),
183
+ 'invalid-address': t('nextcloud-vue', 'not a valid address'),
184
+ }
185
+
186
+ return code ? (reasons[code] || String(code)) : ''
187
+ },
188
+ },
189
+ }
190
+ </script>
191
+
192
+ <style scoped>
193
+ .cn-flow-step-outcomes {
194
+ margin: 4px 0 0;
195
+ font-size: 0.9em;
196
+ }
197
+
198
+ .cn-flow-step-outcomes__list {
199
+ margin: 0;
200
+ padding: 0;
201
+ }
202
+
203
+ /* Stacked, name over value: the run sidebar is narrow, and a side-by-side
204
+ row wrapped mid-list. The host's dt/dd margins and alignment are reset. */
205
+ .cn-flow-step-outcomes__row {
206
+ display: block;
207
+ margin-block: 4px;
208
+ padding-block: 2px;
209
+ padding-inline-start: 8px;
210
+ border-inline-start: 3px solid var(--color-border);
211
+ text-align: start;
212
+ }
213
+
214
+ .cn-flow-step-outcomes__row--success {
215
+ border-inline-start-color: var(--color-success);
216
+ }
217
+
218
+ .cn-flow-step-outcomes__row--warning {
219
+ border-inline-start-color: var(--color-warning);
220
+ }
221
+
222
+ .cn-flow-step-outcomes__row--error {
223
+ border-inline-start-color: var(--color-error);
224
+ }
225
+
226
+ .cn-flow-step-outcomes__name {
227
+ display: block;
228
+ white-space: normal;
229
+ overflow-wrap: anywhere;
230
+ margin: 0;
231
+ padding: 0;
232
+ font-weight: bold;
233
+ text-align: start;
234
+ }
235
+
236
+ .cn-flow-step-outcomes__value {
237
+ display: block;
238
+ white-space: normal;
239
+ margin: 0;
240
+ padding: 0;
241
+ text-align: start;
242
+ color: var(--color-main-text);
243
+ overflow-wrap: anywhere;
244
+ }
245
+
246
+ .cn-flow-step-outcomes__truncated {
247
+ margin: 4px 0 0;
248
+ color: var(--color-text-maxcontrast);
249
+ }
250
+ </style>
@@ -0,0 +1,8 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: 2026 Conduction B.V. <info@conduction.nl>
3
+ * SPDX-License-Identifier: EUPL-1.2
4
+ */
5
+ import CnFlowStepOutcomes from './CnFlowStepOutcomes.vue'
6
+
7
+ export default CnFlowStepOutcomes
8
+ export { CnFlowStepOutcomes }
@@ -250,6 +250,17 @@
250
250
  <!-- @binding {object} option The option being rendered (`{ id, label }`). -->
251
251
  <slot :name="'field-' + field.key + '-option'" v-bind="optionProps" />
252
252
  </template>
253
+ <!-- NcSelect's own option splits a label in two spans
254
+ (NcEllipsisedOption, keeping its last characters
255
+ visible), and in a narrow dropdown the halves broke
256
+ onto two lines mid-word: "Agricu lture". A plain
257
+ label wraps at spaces only. NcSelectUsers keeps its
258
+ avatar rendering. -->
259
+ <template
260
+ v-else-if="!isUserField(field)"
261
+ #option="optionProps">
262
+ <span class="cn-form-dialog__option">{{ optionText(optionProps) }}</span>
263
+ </template>
253
264
  <template
254
265
  v-if="$slots['field-' + field.key + '-selected-option']"
255
266
  #selected-option="optionProps">
@@ -284,11 +295,17 @@
284
295
  #option="optionProps">
285
296
  <slot :name="'field-' + field.key + '-option'" v-bind="optionProps" />
286
297
  </template>
298
+ <template v-else #option="optionProps">
299
+ <span class="cn-form-dialog__option">{{ optionText(optionProps) }}</span>
300
+ </template>
287
301
  <template
288
302
  v-if="$slots['field-' + field.key + '-selected-option']"
289
303
  #selected-option="optionProps">
290
304
  <slot :name="'field-' + field.key + '-selected-option'" v-bind="optionProps" />
291
305
  </template>
306
+ <template v-else #selected-option="optionProps">
307
+ <span class="cn-form-dialog__option">{{ optionText(optionProps) }}</span>
308
+ </template>
292
309
  </NcSelect>
293
310
  <CnFieldHelper
294
311
  :text="field.description"
@@ -318,11 +335,17 @@
318
335
  #option="optionProps">
319
336
  <slot :name="'field-' + field.key + '-option'" v-bind="optionProps" />
320
337
  </template>
338
+ <template v-else #option="optionProps">
339
+ <span class="cn-form-dialog__option">{{ optionText(optionProps) }}</span>
340
+ </template>
321
341
  <template
322
342
  v-if="$slots['field-' + field.key + '-selected-option']"
323
343
  #selected-option="optionProps">
324
344
  <slot :name="'field-' + field.key + '-selected-option'" v-bind="optionProps" />
325
345
  </template>
346
+ <template v-else #selected-option="optionProps">
347
+ <span class="cn-form-dialog__option">{{ optionText(optionProps) }}</span>
348
+ </template>
326
349
  </NcSelect>
327
350
  <CnFieldHelper
328
351
  :text="field.description"
@@ -2452,6 +2475,26 @@ export default {
2452
2475
  * @param {object} field The field definition
2453
2476
  * @return {boolean}
2454
2477
  */
2478
+ /**
2479
+ * The text a select option shows: its label, else a display name, a
2480
+ * name or the id. A plain string option (a tag) is its own text.
2481
+ *
2482
+ * @spec openspec/changes/audit-round-lib-fixes/specs/schema-utilities/spec.md
2483
+ * @param {object|string} option The option (slot props).
2484
+ * @return {string} The text.
2485
+ */
2486
+ optionText(option) {
2487
+ if (option === null || option === undefined) {
2488
+ return ''
2489
+ }
2490
+ if (typeof option !== 'object') {
2491
+ return String(option)
2492
+ }
2493
+ const text = [option.label, option.displayName, option.name, option.id]
2494
+ .find((v) => v !== undefined && v !== null && v !== '')
2495
+ return text === undefined ? '' : String(text)
2496
+ },
2497
+
2455
2498
  isUserField(field) {
2456
2499
  return !!(field && field.userPicker && !field.userPicker.multiple)
2457
2500
  },
@@ -3519,8 +3562,15 @@ export default {
3519
3562
  if (this.isIdPickerField(field)) {
3520
3563
  // Reference / user fields store the chosen id (UUID / UID),
3521
3564
  // not the full option. Cache its label so the selection displays.
3565
+ // A user option can carry only `displayName` (NcSelectUsers' own
3566
+ // shape), and without that fallback the chip showed the uid.
3522
3567
  if (option && option.id) {
3523
- this.referenceLabels = { ...this.referenceLabels, [option.id]: option.label || String(option.id) }
3568
+ const name = option.label || option.displayName || this.referenceLabels[option.id]
3569
+ if (name) {
3570
+ this.referenceLabels = { ...this.referenceLabels, [option.id]: name }
3571
+ } else if (this.isUserField(field)) {
3572
+ this.resolveUserLabel(option.id)
3573
+ }
3524
3574
  }
3525
3575
  this.updateField(field.key, option ? option.id : null)
3526
3576
  // Reference options are label-only ({id,label}) — pass the id so
@@ -3587,7 +3637,7 @@ export default {
3587
3637
  const labels = {}
3588
3638
  for (const o of list) {
3589
3639
  if (o && o.id) {
3590
- labels[o.id] = o.label || String(o.id)
3640
+ labels[o.id] = o.label || o.displayName || this.referenceLabels[o.id] || String(o.id)
3591
3641
  }
3592
3642
  }
3593
3643
  if (Object.keys(labels).length > 0) {
@@ -3861,6 +3911,16 @@ export default {
3861
3911
  </script>
3862
3912
 
3863
3913
  <style scoped>
3914
+ /* A select option breaks between words only, never inside one. */
3915
+ .cn-form-dialog__option {
3916
+ display: block;
3917
+ min-width: 0;
3918
+ white-space: normal;
3919
+ overflow-wrap: normal;
3920
+ word-break: normal;
3921
+ hyphens: manual;
3922
+ }
3923
+
3864
3924
  .cn-form-dialog__draft-actions {
3865
3925
  display: flex;
3866
3926
  gap: 8px;
@@ -488,7 +488,7 @@
488
488
  :includeColumns="includeColumns"
489
489
  :columnOverrides="columnOverrides"
490
490
  :rowClass="rowClass"
491
- :filterable="headerFilters && !!effectiveSchema"
491
+ :filterable="tableHeaderFilters"
492
492
  :activeFilters="effectiveActiveFilters"
493
493
  :filterRegister="typeof register === 'string' ? register : ''"
494
494
  @columnFilter="onColumnFilterEvent"
@@ -3873,6 +3873,28 @@ export default {
3873
3873
  },
3874
3874
 
3875
3875
  /** Resolved schema OBJECT (for column generation / icons / labels). */
3876
+ /**
3877
+ * Whether the table shows its header filters. A self-fetching page
3878
+ * applies them itself, with or without a resolved schema (the columns
3879
+ * then say how each one filters, else a text filter on contains). A
3880
+ * host-fed table shows them when a schema came along, as before, or
3881
+ * when the host listens for `filter-change`; a filter nobody applies
3882
+ * would be a control that does nothing.
3883
+ *
3884
+ * @spec openspec/changes/audit-round-lib-fixes/specs/cn-data-table/spec.md
3885
+ * @return {boolean} True when header filters show.
3886
+ */
3887
+ tableHeaderFilters() {
3888
+ if (!this.headerFilters) {
3889
+ return false
3890
+ }
3891
+ if (this.effectiveSchema || this.isSelfFetchMode) {
3892
+ return true
3893
+ }
3894
+ const vnodeProps = (this.$ && this.$.vnode && this.$.vnode.props) || {}
3895
+ return typeof vnodeProps.onFilterChange === 'function' || Array.isArray(vnodeProps.onFilterChange)
3896
+ },
3897
+
3876
3898
  effectiveSchema() {
3877
3899
  if (this.isSelfFetchMode) {
3878
3900
  return this.list.schema.value
@@ -279,7 +279,7 @@ export default {
279
279
  * @param {(suggestions: Array<object>) => void} callback Receives the suggestion array.
280
280
  */
281
281
  async fetchMentionSuggestions(search, callback) {
282
- const users = await searchNextcloudUsers(search)
282
+ const users = await searchNextcloudUsers(search, { includeCurrentUser: false })
283
283
  callback(users.map((user) => ({
284
284
  id: user.id,
285
285
  label: user.label,
@@ -123,6 +123,13 @@
123
123
 
124
124
  <!-- config-fields -->
125
125
  <template v-else-if="step.type === 'config-fields'">
126
+ <!-- The step's intro, drawn like choice and run-action steps.
127
+ It was left out here, so pipelinq's organisation step
128
+ ("Your organisation details…") showed fields with no
129
+ word about what they were for. -->
130
+ <NcNoteCard v-if="step.body" type="info" data-testid="cn-setup-step-intro">
131
+ {{ stepBody(step) }}
132
+ </NcNoteCard>
126
133
  <div
127
134
  v-for="field in fieldsFor(step)"
128
135
  :key="field.key"
@@ -172,6 +172,12 @@ export default {
172
172
  tourId: { type: String, default: '' },
173
173
  /** Optional resume token `{ tourId, stepId }` (refresh / cross-app hand-off). */
174
174
  resume: { type: Object, default: null },
175
+ /**
176
+ * Whether a first-visit tour may start on its own. The host turns it
177
+ * off while the user's tour is paused, so a paused tour stays hidden
178
+ * across page loads until the user picks "Continue".
179
+ */
180
+ autoStart: { type: Boolean, default: true },
175
181
  /** Stacking order of the overlay. */
176
182
  zIndex: { type: Number, default: 10000 },
177
183
  /** Next button label. */
@@ -571,6 +577,9 @@ export default {
571
577
  this.wt.start(this.tourId)
572
578
  return
573
579
  }
580
+ if (!this.autoStart) {
581
+ return
582
+ }
574
583
  const auto = this.wt.autoStartTour.value
575
584
  if (!auto) {
576
585
  return
@@ -1171,31 +1180,43 @@ export default {
1171
1180
  },
1172
1181
 
1173
1182
  /**
1174
- * End the tour for good from the corner close button: mark it complete so
1175
- * the seen-version is persisted and it does not auto-show again.
1183
+ * The corner close button PAUSES the tour, like ESC and the dim. It
1184
+ * used to complete it, and the host then wiped the saved step, so a
1185
+ * restart began at step 1 again. Only Finish on the last step (and a
1186
+ * host-supplied Skip) completes the tour; "Start over" in the user
1187
+ * settings stays the way back to step 1.
1176
1188
  *
1189
+ * @spec openspec/changes/audit-round-lib-fixes/specs/cn-walkthrough/spec.md
1177
1190
  * @return {void}
1178
1191
  */
1179
1192
  close() {
1180
- this.wt.complete()
1181
- this.$emit('complete')
1193
+ this.onBackdrop()
1182
1194
  },
1183
1195
 
1184
1196
  /**
1185
- * Pause the tour from a backdrop click or ESC. The tour hides and
1186
- * keeps its step; only Skip (the close button) and Finish end it.
1197
+ * Pause the tour from the corner X, a backdrop click or ESC. The tour
1198
+ * hides and keeps its step; only Finish (or a host Skip) ends it.
1187
1199
  * Pressing ESC used to dismiss it, and the host recorded that as
1188
- * "seen", so the tour never came back.
1200
+ * "seen", so the tour never came back. The `pause` event carries
1201
+ * where the user was, so the host can keep a paused tour hidden on
1202
+ * the next page load.
1189
1203
  *
1190
1204
  * @spec openspec/changes/walkthrough-advance-pause-resume/specs/cn-walkthrough/spec.md
1205
+ * @spec openspec/changes/audit-round-lib-fixes/specs/cn-walkthrough/spec.md
1191
1206
  * @return {void}
1192
1207
  */
1193
1208
  onBackdrop() {
1209
+ const where = {
1210
+ tourId: (this.wt.activeTour.value && this.wt.activeTour.value.id) || '',
1211
+ stepId: (this.step && this.step.id) || '',
1212
+ index: this.index,
1213
+ }
1194
1214
  this.wt.pause()
1195
1215
  /**
1196
- * @event pause Emitted when the user hides the tour (backdrop / ESC). The tour can continue later.
1216
+ * @event pause Emitted when the user hides the tour (X, backdrop, ESC). The tour can continue later.
1217
+ * @type {{ tourId: string, stepId: string, index: number }}
1197
1218
  */
1198
- this.$emit('pause')
1219
+ this.$emit('pause', where)
1199
1220
  },
1200
1221
  },
1201
1222
  }
@@ -191,6 +191,7 @@ export { CnDeltaWidgetForm } from './CnDeltaWidgetForm/index.js'
191
191
  export { CnGaugeWidget } from './CnGaugeWidget/index.js'
192
192
  export { CnGaugeWidgetForm } from './CnGaugeWidgetForm/index.js'
193
193
  export { CnFlowRunsWidget } from './CnFlowRunsWidget/index.js'
194
+ export { CnFlowStepOutcomes } from './CnFlowStepOutcomes/index.js'
194
195
  export { CnFlowRunsWidgetForm } from './CnFlowRunsWidgetForm/index.js'
195
196
  export { CnTasksWidget } from './CnTasksWidget/index.js'
196
197
  export { CnTasksWidgetForm } from './CnTasksWidgetForm/index.js'
@@ -216,11 +216,13 @@ export function walkthroughProgressKey(configKey) {
216
216
 
217
217
  /**
218
218
  * Normalise stored progress (an object, or the JSON string a preference
219
- * holds) into `{ tourId, stepId, index, version }`, or null.
219
+ * holds) into `{ tourId, stepId, index, version }`, plus `paused: true`
220
+ * when the user paused the tour, or null.
220
221
  *
221
222
  * @spec openspec/changes/walkthrough-advance-pause-resume/specs/cn-walkthrough/spec.md
223
+ * @spec openspec/changes/audit-round-lib-fixes/specs/cn-walkthrough/spec.md
222
224
  * @param {unknown} value The stored value.
223
- * @return {{tourId: string, stepId: string, index: number, version: string}|null} The progress.
225
+ * @return {{tourId: string, stepId: string, index: number, version: string, paused: (boolean|undefined)}|null} The progress.
224
226
  */
225
227
  export function normaliseWalkthroughProgress(value) {
226
228
  let data = value
@@ -237,12 +239,18 @@ export function normaliseWalkthroughProgress(value) {
237
239
  if (!isPlainObject(data) || typeof data.tourId !== 'string' || data.tourId === '') {
238
240
  return null
239
241
  }
240
- return {
242
+ const progress = {
241
243
  tourId: data.tourId,
242
244
  stepId: typeof data.stepId === 'string' ? data.stepId : '',
243
245
  index: Number.isInteger(data.index) && data.index >= 0 ? data.index : 0,
244
246
  version: typeof data.version === 'string' ? data.version : '',
245
247
  }
248
+ // A paused tour stays hidden on the next visit until the user picks
249
+ // "Continue". Only written when true, so older values read unchanged.
250
+ if (data.paused === true) {
251
+ progress.paused = true
252
+ }
253
+ return progress
246
254
  }
247
255
 
248
256
  /**
@@ -129,3 +129,47 @@
129
129
  .cn-requires-app.cn-requires-app .empty-content__action {
130
130
  margin-top: var(--default-grid-baseline, 4px);
131
131
  }
132
+
133
+ /* NcEmptyContent forces its icon's svg to 64px with !important while the
134
+ * rule above lets the icon box shrink, so the svg spilled out of its box and
135
+ * over the name. Hold the svg to the size the caller asked for. */
136
+ .cn-requires-app.cn-requires-app .empty-content__icon svg {
137
+ width: 32px !important;
138
+ height: 32px !important;
139
+ max-width: 32px !important;
140
+ max-height: 32px !important;
141
+ }
142
+
143
+ /* The dashboard's compact "Install <app>" placeholder: two lines of text and
144
+ * the button, nothing else, so it fits a two-row tile even under a title row.
145
+ * `safe center` keeps the top in view when a tile is smaller still, and the
146
+ * body scrolls rather than clipping the button. */
147
+ .cn-requires-app--compact {
148
+ display: flex;
149
+ flex-direction: column;
150
+ align-items: center;
151
+ justify-content: safe center;
152
+ gap: calc(2 * var(--default-grid-baseline, 4px));
153
+ box-sizing: border-box;
154
+ height: 100%;
155
+ min-height: 0;
156
+ overflow: auto;
157
+ padding: calc(2 * var(--default-grid-baseline, 4px));
158
+ text-align: center;
159
+ }
160
+
161
+ .cn-requires-app--compact .cn-requires-app__description {
162
+ display: -webkit-box;
163
+ margin: 0;
164
+ overflow: hidden;
165
+ color: var(--color-text-maxcontrast);
166
+ font-size: 0.9em;
167
+ line-height: 1.4;
168
+ -webkit-box-orient: vertical;
169
+ -webkit-line-clamp: 2;
170
+ line-clamp: 2;
171
+ }
172
+
173
+ .cn-requires-app--compact .button-vue {
174
+ flex-shrink: 0;
175
+ }