@conduction/nextcloud-vue 2.62.0 → 2.63.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 (154) hide show
  1. package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue.js +10 -0
  2. package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue.js.map +1 -0
  3. package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue2.js +189 -0
  4. package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue2.js.map +1 -0
  5. package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue3.js +72 -0
  6. package/dist/esm/components/CnAdminActionCard/CnAdminActionCard.vue3.js.map +1 -0
  7. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js +271 -32
  8. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js.map +1 -1
  9. package/dist/esm/components/CnAppNav/CnAppNav.vue3.js.map +1 -1
  10. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +99 -9
  11. package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
  12. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js +39 -18
  13. package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
  14. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js.map +1 -1
  15. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js +53 -37
  16. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js.map +1 -1
  17. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  18. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +2 -2
  19. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  20. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue.js +10 -0
  21. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue.js.map +1 -0
  22. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue2.js +293 -0
  23. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue2.js.map +1 -0
  24. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue3.js +189 -0
  25. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue3.js.map +1 -0
  26. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +216 -8
  27. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  28. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +118 -50
  29. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  30. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  31. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +2 -1
  32. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
  33. package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue2.js +3 -1
  34. package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue2.js.map +1 -1
  35. package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue3.js +1 -1
  36. package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue3.js.map +1 -1
  37. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +234 -9
  38. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
  39. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +96 -57
  40. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
  41. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +51 -2
  42. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  43. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +5 -1
  44. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  45. package/dist/esm/components/CnIndexPage/selfModeActions.js +3 -3
  46. package/dist/esm/components/CnIndexPage/selfModeActions.js.map +1 -1
  47. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +36 -1
  48. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
  49. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js +4 -1
  50. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
  51. package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue2.js +121 -12
  52. package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue2.js.map +1 -1
  53. package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue3.js +3 -1
  54. package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue3.js.map +1 -1
  55. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js +326 -1
  56. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js.map +1 -1
  57. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js +112 -26
  58. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js.map +1 -1
  59. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue.js +10 -0
  60. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue.js.map +1 -0
  61. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue2.js +337 -0
  62. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue2.js.map +1 -0
  63. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue3.js +102 -0
  64. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue3.js.map +1 -0
  65. package/dist/esm/components/CnTimelineWidget/dashboardRegistration.js +34 -0
  66. package/dist/esm/components/CnTimelineWidget/dashboardRegistration.js.map +1 -0
  67. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js +131 -8
  68. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js.map +1 -1
  69. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue3.js +40 -38
  70. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue3.js.map +1 -1
  71. package/dist/esm/components/CnWidgetGrid/builtInWidgets.js +8 -6
  72. package/dist/esm/components/CnWidgetGrid/builtInWidgets.js.map +1 -1
  73. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
  74. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
  75. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue2.js +19 -0
  76. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue2.js.map +1 -1
  77. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue3.js +1 -1
  78. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue3.js.map +1 -1
  79. package/dist/esm/composables/useDependencyCheck.js +65 -0
  80. package/dist/esm/composables/useDependencyCheck.js.map +1 -0
  81. package/dist/esm/composables/useWalkthrough.js +218 -6
  82. package/dist/esm/composables/useWalkthrough.js.map +1 -1
  83. package/dist/esm/index.js +2 -0
  84. package/dist/esm/index.js.map +1 -1
  85. package/dist/esm/l10n/en.json.js +43 -0
  86. package/dist/esm/l10n/en.json.js.map +1 -1
  87. package/dist/esm/l10n/nl.json.js +43 -0
  88. package/dist/esm/l10n/nl.json.js.map +1 -1
  89. package/dist/esm/nextcloud-vue.css +449 -0
  90. package/dist/esm/store/useObjectStore.js +8 -0
  91. package/dist/esm/store/useObjectStore.js.map +1 -1
  92. package/dist/esm/utils/columnFilters.js +311 -0
  93. package/dist/esm/utils/columnFilters.js.map +1 -0
  94. package/dist/esm/utils/groupAutocomplete.js +123 -0
  95. package/dist/esm/utils/groupAutocomplete.js.map +1 -0
  96. package/dist/esm/utils/libraryWidgetKeys.js +2 -0
  97. package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
  98. package/dist/esm/utils/pickerOptions.js +198 -0
  99. package/dist/esm/utils/pickerOptions.js.map +1 -0
  100. package/dist/esm/utils/schema.js +83 -7
  101. package/dist/esm/utils/schema.js.map +1 -1
  102. package/dist/esm/utils/timelineEvents.js +174 -0
  103. package/dist/esm/utils/timelineEvents.js.map +1 -0
  104. package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
  105. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  106. package/dist/esm/utils/walkthroughSignals.js +88 -0
  107. package/dist/esm/utils/walkthroughSignals.js.map +1 -0
  108. package/dist/nextcloud-vue.cjs.js +9369 -5193
  109. package/dist/nextcloud-vue.cjs.js.map +2 -2
  110. package/dist/nextcloud-vue.css +449 -0
  111. package/l10n/en.json +43 -0
  112. package/l10n/nl.json +43 -0
  113. package/package.json +1 -1
  114. package/src/components/CnAdminActionCard/CnAdminActionCard.vue +234 -0
  115. package/src/components/CnAdminActionCard/index.js +4 -0
  116. package/src/components/CnAppNav/CnAppNav.vue +271 -32
  117. package/src/components/CnAppRoot/CnAppRoot.vue +129 -15
  118. package/src/components/CnChoiceCards/CnChoiceCards.vue +56 -31
  119. package/src/components/CnDashboardPage/CnDashboardPage.vue +2 -2
  120. package/src/components/CnDataTable/CnColumnFilterPopover.vue +524 -0
  121. package/src/components/CnDataTable/CnDataTable.md +3 -0
  122. package/src/components/CnDataTable/CnDataTable.vue +260 -4
  123. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +2 -2
  124. package/src/components/CnFieldHelper/CnFieldHelper.vue +4 -2
  125. package/src/components/CnFormDialog/CnFormDialog.vue +290 -31
  126. package/src/components/CnIndexPage/CnIndexPage.vue +55 -2
  127. package/src/components/CnIndexPage/selfModeActions.js +3 -3
  128. package/src/components/CnObjectListWidget/CnObjectListWidget.vue +39 -1
  129. package/src/components/CnResourceSelect/CnResourceSelect.vue +123 -12
  130. package/src/components/CnSetupWizard/CnSetupWizard.vue +392 -3
  131. package/src/components/CnTimelineWidget/CnTimelineWidget.vue +477 -0
  132. package/src/components/CnTimelineWidget/dashboardRegistration.js +32 -0
  133. package/src/components/CnTimelineWidget/index.js +4 -0
  134. package/src/components/CnWalkthrough/CnWalkthrough.vue +143 -10
  135. package/src/components/CnWidgetGrid/builtInWidgets.js +2 -0
  136. package/src/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
  137. package/src/components/CnWizardDialog/CnWizardDialog.vue +20 -1
  138. package/src/components/index.js +2 -0
  139. package/src/composables/useDependencyCheck.js +61 -0
  140. package/src/composables/useWalkthrough.js +217 -5
  141. package/src/css/dashboard.css +49 -0
  142. package/src/css/detail-page.css +19 -0
  143. package/src/css/table.css +81 -0
  144. package/src/index.js +2 -0
  145. package/src/schemas/app-manifest-v2.schema.json +23 -1
  146. package/src/store/useObjectStore.js +8 -0
  147. package/src/utils/columnFilters.js +324 -0
  148. package/src/utils/groupAutocomplete.js +119 -0
  149. package/src/utils/libraryWidgetKeys.js +2 -0
  150. package/src/utils/pickerOptions.js +194 -0
  151. package/src/utils/schema.js +83 -7
  152. package/src/utils/timelineEvents.js +171 -0
  153. package/src/utils/validateManifestV2.compiled.js +2 -2
  154. package/src/utils/walkthroughSignals.js +94 -0
@@ -25,7 +25,12 @@
25
25
  the greyed-out area reaches the app, which is the point. A centered
26
26
  step spotlights nothing, so its backdrop stays interactive and
27
27
  dismisses the tour. -->
28
- <template v-if="isCentered || !rect">
28
+ <!-- Quiet: an app dialog is open, or the step's target belongs to
29
+ another page. No dim and no cutout, so the overlay never sits on
30
+ top of the dialog and never invites a click on the wrong page.
31
+ Only the docked coachmark stays. -->
32
+ <template v-if="quiet" />
33
+ <template v-else-if="isCentered || !rect">
29
34
  <div v-if="isCentered"
30
35
  class="cn-walkthrough__dim cn-walkthrough__dim--full"
31
36
  @click.self="onBackdrop" />
@@ -50,7 +55,7 @@
50
55
  <!-- Coachmark card -->
51
56
  <div ref="card"
52
57
  class="cn-walkthrough__card"
53
- :class="`cn-walkthrough__card--${cardPlacement}`"
58
+ :class="[`cn-walkthrough__card--${cardPlacement}`, { 'cn-walkthrough__card--docked': quiet }]"
54
59
  :style="cardStyle">
55
60
  <NcButton class="cn-walkthrough__close"
56
61
  variant="tertiary"
@@ -183,7 +188,7 @@ export default {
183
188
  translate: { type: Function, default: null },
184
189
  },
185
190
 
186
- emits: ['complete', 'dismiss', 'step-change', 'advance', 'handoff'],
191
+ emits: ['complete', 'dismiss', 'pause', 'progress', 'step-change', 'advance', 'handoff'],
187
192
 
188
193
  setup(props) {
189
194
  const wt = useWalkthrough(props.appId, props.manifest, {
@@ -200,6 +205,10 @@ export default {
200
205
  cardPos: { top: 0, left: 0 },
201
206
  cardPlacement: 'bottom',
202
207
  targetEl: null,
208
+ // An app dialog (NcModal / NcDialog) is open over the page.
209
+ appModalOpen: false,
210
+ // The step's target was found on another page than the current one.
211
+ offPage: false,
203
212
  }
204
213
  },
205
214
 
@@ -228,6 +237,18 @@ export default {
228
237
  return this.wt.isLast.value
229
238
  },
230
239
 
240
+ /**
241
+ * Whether the overlay steps back: no dim, no cutout, a docked card.
242
+ * True while an app dialog is open or the step's target is on
243
+ * another page.
244
+ *
245
+ * @spec openspec/changes/walkthrough-advance-pause-resume/specs/cn-walkthrough/spec.md
246
+ * @return {boolean}
247
+ */
248
+ quiet() {
249
+ return this.appModalOpen || this.offPage
250
+ },
251
+
231
252
  isCentered() {
232
253
  return this.step && this.step.placement === 'center'
233
254
  },
@@ -321,6 +342,9 @@ export default {
321
342
  },
322
343
 
323
344
  cardStyle() {
345
+ if (this.quiet) {
346
+ return {}
347
+ }
324
348
  if (this.isCentered || !this.rect) {
325
349
  return { top: '50%', left: '50%', transform: 'translate(-50%, -50%)' }
326
350
  }
@@ -339,6 +363,18 @@ export default {
339
363
  * @type {{ stepId: string, index: number }}
340
364
  */
341
365
  this.$emit('step-change', { stepId: newStep.id, index: this.index })
366
+ /**
367
+ * @event progress Emitted on every step change so the host can remember where the user is.
368
+ * @type {{ tourId: string, stepId: string, index: number }}
369
+ */
370
+ this.$emit('progress', {
371
+ tourId: (this.wt.activeTour.value && this.wt.activeTour.value.id) || '',
372
+ stepId: newStep.id,
373
+ index: this.index,
374
+ })
375
+ // A new step finds its own page again.
376
+ this._homeRoute = null
377
+ this.offPage = false
342
378
  this.$nextTick(() => this.locateTarget())
343
379
  }
344
380
  },
@@ -377,6 +413,9 @@ export default {
377
413
  // A qualifying auto-start tour whose first-step page is NOT the current
378
414
  // route — held here until the user navigates to that page (see hookRouter).
379
415
  this._pendingAutoTour = null
416
+ // The route the active step's target was first found on (see locateTarget).
417
+ this._homeRoute = null
418
+ this._modalObs = null
380
419
  },
381
420
 
382
421
  mounted() {
@@ -386,12 +425,27 @@ export default {
386
425
  window.addEventListener('scroll', this._onScroll, true)
387
426
  window.addEventListener('resize', this._onScroll)
388
427
  this._onKey = (e) => {
389
- if (e.key === 'Escape') {
390
- this.onBackdrop()
428
+ if (e.key !== 'Escape' || !this.active) {
429
+ return
391
430
  }
431
+ // ESC while an app dialog is open belongs to that dialog. It used
432
+ // to dismiss the tour too, so closing a create form ended it.
433
+ if (this.isAppModalOpen()) {
434
+ return
435
+ }
436
+ this.onBackdrop()
392
437
  }
393
438
  window.addEventListener('keydown', this._onKey)
394
- this._onObjectCreated = (e) => this.wt.notify({ kind: 'object-created', object: (e && e.detail) || {} })
439
+ this._onObjectCreated = (e) => {
440
+ const d = (e && e.detail) || {}
441
+ this.wt.notify({
442
+ kind: 'object-created',
443
+ register: d.register,
444
+ schema: d.schema,
445
+ object: d.object && typeof d.object === 'object' ? d.object : d,
446
+ })
447
+ }
448
+ this.watchAppModals()
395
449
  window.addEventListener('cn-walkthrough:object-created', this._onObjectCreated)
396
450
  this.hookRouter()
397
451
  if (this.active) {
@@ -404,6 +458,10 @@ export default {
404
458
  window.removeEventListener('resize', this._onScroll)
405
459
  window.removeEventListener('keydown', this._onKey)
406
460
  window.removeEventListener('cn-walkthrough:object-created', this._onObjectCreated)
461
+ if (this._modalObs) {
462
+ this._modalObs.disconnect()
463
+ this._modalObs = null
464
+ }
407
465
  if (typeof this._routeUnhook === 'function') {
408
466
  this._routeUnhook()
409
467
  }
@@ -411,6 +469,51 @@ export default {
411
469
  },
412
470
 
413
471
  methods: {
472
+ /**
473
+ * Whether an app dialog (NcModal / NcDialog, or any other modal
474
+ * dialog that is not this overlay) is open.
475
+ *
476
+ * @spec openspec/changes/walkthrough-advance-pause-resume/specs/cn-walkthrough/spec.md
477
+ * @return {boolean}
478
+ */
479
+ isAppModalOpen() {
480
+ if (typeof document === 'undefined') {
481
+ return false
482
+ }
483
+ const own = this.$el
484
+ const nodes = document.querySelectorAll('.modal-mask, [role="dialog"][aria-modal="true"]')
485
+ for (const node of nodes) {
486
+ if (own && typeof own.contains === 'function' && (own === node || own.contains(node))) {
487
+ continue
488
+ }
489
+ return true
490
+ }
491
+ return false
492
+ },
493
+
494
+ /**
495
+ * Follow app dialogs opening and closing, so the overlay steps back
496
+ * while one is open and comes back after.
497
+ *
498
+ * @return {void}
499
+ */
500
+ watchAppModals() {
501
+ const update = () => {
502
+ const open = this.isAppModalOpen()
503
+ if (open !== this.appModalOpen) {
504
+ this.appModalOpen = open
505
+ if (!open && this.active) {
506
+ this.$nextTick(() => this.locateTarget())
507
+ }
508
+ }
509
+ }
510
+ update()
511
+ if (typeof window !== 'undefined' && window.MutationObserver) {
512
+ this._modalObs = new MutationObserver(update)
513
+ this._modalObs.observe(document.body, { childList: true, subtree: true })
514
+ }
515
+ },
516
+
414
517
  /**
415
518
  * Translate an i18n key/text via the `translate` prop when given.
416
519
  *
@@ -598,6 +701,23 @@ export default {
598
701
  return
599
702
  }
600
703
  const el = this.resolveTarget(this.step)
704
+ // A step's target belongs to the page it was first found on. The
705
+ // same `index-add` button exists on every list page, and a step
706
+ // left half-done on Products used to spotlight the Clients page's
707
+ // button too, inviting a click that belonged to another step.
708
+ const kind = (this.step.target && this.step.target.kind) || ''
709
+ const routeName = this.$route ? this.$route.name : null
710
+ if (el && kind !== 'page' && kind !== 'nav-item' && routeName) {
711
+ if (this._homeRoute === null) {
712
+ this._homeRoute = routeName
713
+ } else if (this._homeRoute !== routeName) {
714
+ this.offPage = true
715
+ this.targetEl = null
716
+ this.rect = null
717
+ return
718
+ }
719
+ }
720
+ this.offPage = false
601
721
  this.targetEl = el
602
722
  if (!el) {
603
723
  // Optional step whose target is absent → skip; else wait for it.
@@ -862,6 +982,10 @@ export default {
862
982
  * @return {void}
863
983
  */
864
984
  focusCard() {
985
+ // Never take focus from an open app dialog.
986
+ if (this.appModalOpen) {
987
+ return
988
+ }
865
989
  this.$nextTick(() => {
866
990
  const btn = this.$refs.firstBtn && this.$refs.firstBtn.$el
867
991
  if (btn && typeof btn.focus === 'function') {
@@ -1058,16 +1182,20 @@ export default {
1058
1182
  },
1059
1183
 
1060
1184
  /**
1061
- * Dismiss the tour from a backdrop click or ESC.
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.
1187
+ * Pressing ESC used to dismiss it, and the host recorded that as
1188
+ * "seen", so the tour never came back.
1062
1189
  *
1190
+ * @spec openspec/changes/walkthrough-advance-pause-resume/specs/cn-walkthrough/spec.md
1063
1191
  * @return {void}
1064
1192
  */
1065
1193
  onBackdrop() {
1066
- this.wt.dismiss()
1194
+ this.wt.pause()
1067
1195
  /**
1068
- * @event dismiss Emitted when the user dismisses the tour (backdrop / ESC).
1196
+ * @event pause Emitted when the user hides the tour (backdrop / ESC). The tour can continue later.
1069
1197
  */
1070
- this.$emit('dismiss')
1198
+ this.$emit('pause')
1071
1199
  },
1072
1200
  },
1073
1201
  }
@@ -1126,6 +1254,11 @@ export default {
1126
1254
  pointer-events: auto;
1127
1255
  }
1128
1256
 
1257
+ .cn-walkthrough__card--docked {
1258
+ inset-block-end: 16px;
1259
+ inset-inline-start: 16px;
1260
+ }
1261
+
1129
1262
  .cn-walkthrough__close {
1130
1263
  /* !important beats NcButton's own `position: relative`, which would otherwise
1131
1264
  leave the close button in-flow at the card's top-left. */
@@ -25,6 +25,7 @@ import CnObjectGeoWidget from '../CnObjectGeoWidget/CnObjectGeoWidget.vue'
25
25
  import CnObjectMetadataWidget from '../CnObjectMetadataWidget/CnObjectMetadataWidget.vue'
26
26
  import CnRelatedObjectsWidget from '../CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue'
27
27
  import CnTextWidget from '../CnTextWidget/CnTextWidget.vue'
28
+ import CnTimelineWidget from '../CnTimelineWidget/CnTimelineWidget.vue'
28
29
  import CnWidgetCardGrid from '../CnWidgetCardGrid/CnWidgetCardGrid.vue'
29
30
  import CnWidgetFormRenderer from '../CnWidgetFormRenderer/CnWidgetFormRenderer.vue'
30
31
  import CnWidgetMapViewer from '../CnWidgetMapViewer/CnWidgetMapViewer.vue'
@@ -84,6 +85,7 @@ export const BUILT_IN_WIDGETS = {
84
85
  integration: CnIntegrationWidget,
85
86
  banner: CnBannerWidget,
86
87
  'audit-trail': CnAuditTrailWidget,
88
+ timeline: CnTimelineWidget,
87
89
  header: CnHeaderWidget,
88
90
  text: CnTextWidget,
89
91
  divider: CnDividerWidget,
@@ -72,6 +72,7 @@ import '../CnObjectDataWidget/dashboardRegistration.js'
72
72
  import '../CnObjectGeoWidget/dashboardRegistration.js'
73
73
  import '../CnWidgetObjectTable/dashboardRegistration.js'
74
74
  import '../CnAuditTrailWidget/dashboardRegistration.js'
75
+ import '../CnTimelineWidget/dashboardRegistration.js'
75
76
  import '../CnKbSearchWidget/index.js'
76
77
  import '../CnInteractionFormWidget/index.js'
77
78
  import '../CnBannerWidget/index.js'
@@ -105,7 +105,7 @@
105
105
  </NcButton>
106
106
  <NcButton variant="primary"
107
107
  data-testid="cn-wizard-next"
108
- :disabled="loading"
108
+ :disabled="loading || nextDisabled"
109
109
  @click="isLast ? submit() : next()">
110
110
  <template #icon>
111
111
  <NcLoadingIcon v-if="loading" :size="20" />
@@ -280,6 +280,19 @@ export default {
280
280
  type: Boolean,
281
281
  default: true,
282
282
  },
283
+
284
+ /**
285
+ * Disable the Next / Submit button, whatever the step's `validate`
286
+ * would say. For a host that knows the wizard cannot go on yet, such as
287
+ * `CnSetupWizard` while a required app is missing.
288
+ *
289
+ * @type {boolean}
290
+ * @spec openspec/changes/setup-wizard-card-load-and-dependency-gate/specs/cn-setup-wizard/spec.md#requirement-the-wizard-checks-dependencies-before-any-step
291
+ */
292
+ nextDisabled: {
293
+ type: Boolean,
294
+ default: false,
295
+ },
283
296
  },
284
297
 
285
298
  emits: ['close', 'step-change', 'submit'],
@@ -396,6 +409,9 @@ export default {
396
409
  * @return {Promise<void>}
397
410
  */
398
411
  async next() {
412
+ if (this.nextDisabled) {
413
+ return
414
+ }
399
415
  if (this.isLast) {
400
416
  return this.submit()
401
417
  }
@@ -467,6 +483,9 @@ export default {
467
483
  * @return {Promise<void>}
468
484
  */
469
485
  async submit() {
486
+ if (this.nextDisabled) {
487
+ return
488
+ }
470
489
  const ok = await this.runValidation()
471
490
  if (!ok) {
472
491
  return
@@ -17,6 +17,7 @@ export { CnLeafDependencySettings } from './CnLeafDependencySettings/index.js'
17
17
  export { CnStatsBlock } from './CnStatsBlock/index.js'
18
18
  export { CnStructuredDocReview } from './CnStructuredDocReview/index.js'
19
19
  export { CnConfigurationCard } from './CnConfigurationCard/index.js'
20
+ export { CnAdminActionCard } from './CnAdminActionCard/index.js'
20
21
  export { CnVersionInfoCard } from './CnVersionInfoCard/index.js'
21
22
  export { CnAdminSettingsShell } from './CnAdminSettingsShell/index.js'
22
23
  export { CnCellRenderer } from './CnCellRenderer/index.js'
@@ -103,6 +104,7 @@ export { CnSearchPage } from './CnSearchPage/index.js'
103
104
  export { CnCommandPalette } from './CnCommandPalette/index.js'
104
105
  export { CnTabbedFormDialog } from './CnTabbedFormDialog/index.js'
105
106
  export { CnTimelineStages } from './CnTimelineStages/index.js'
107
+ export { CnTimelineWidget } from './CnTimelineWidget/index.js'
106
108
  export { CnTreeView } from './CnTreeView/index.js'
107
109
  export { CnUserActionMenu } from './CnUserActionMenu/index.js'
108
110
  export { CnNotesCard } from './CnNotesCard/index.js'
@@ -0,0 +1,61 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
3
+ * SPDX-License-Identifier: EUPL-1.2
4
+ */
5
+
6
+ import { useAppStatus } from './useAppStatus.js'
7
+
8
+ /**
9
+ * Resolve `manifest.dependencies` entries to their live status.
10
+ *
11
+ * Accepts the manifest's own shape: an app-id string (a REQUIRED dependency)
12
+ * or `{ id, name?, required? }` where `required` defaults to true. Each entry
13
+ * becomes `{ id, name, required, installed, enabled }`.
14
+ *
15
+ * `serverStatuses` is the optional `dependency_statuses` initial state an app
16
+ * injects from PHP, keyed by app id: `{ installed, enabled }`. It wins over
17
+ * the browser heuristic, because only the server can tell "installed but
18
+ * disabled" from "not installed". Without it, `useAppStatus` answers, which
19
+ * is the same resolution `CnAppRoot` uses for its dependency-missing phase.
20
+ *
21
+ * @param {Array<string|{id: string, name?: string, required?: boolean}>} entries The declared dependencies.
22
+ * @param {object} [serverStatuses] Server-reported statuses keyed by app id.
23
+ * @return {Array<{id: string, name: string, required: boolean, installed: boolean, enabled: boolean}>} One row per valid entry.
24
+ *
25
+ * @spec openspec/changes/setup-wizard-card-load-and-dependency-gate/specs/cn-setup-wizard/spec.md#requirement-the-wizard-checks-dependencies-before-any-step
26
+ *
27
+ * @example
28
+ * const missing = checkDependencies(manifest.dependencies)
29
+ * .filter((d) => d.required && !(d.installed && d.enabled))
30
+ */
31
+ export function checkDependencies(entries, serverStatuses = {}) {
32
+ const list = Array.isArray(entries) ? entries : []
33
+ const server = (serverStatuses && typeof serverStatuses === 'object') ? serverStatuses : {}
34
+ return list
35
+ .map((entry) => {
36
+ const isObject = entry !== null && typeof entry === 'object'
37
+ const id = isObject ? entry.id : entry
38
+ if (typeof id !== 'string' || id === '') {
39
+ return null
40
+ }
41
+ const required = isObject ? entry.required !== false : true
42
+ const name = (isObject && entry.name) || id
43
+ const reported = server[id]
44
+ if (reported !== undefined && reported !== null) {
45
+ return { id, name, required, installed: !!reported.installed, enabled: !!reported.enabled }
46
+ }
47
+ const status = useAppStatus(id)
48
+ return { id, name, required, installed: !!status.installed.value, enabled: !!status.enabled.value }
49
+ })
50
+ .filter((row) => row !== null)
51
+ }
52
+
53
+ /**
54
+ * Whether a resolved dependency row is usable: installed AND enabled.
55
+ *
56
+ * @param {{installed: boolean, enabled: boolean}} row A row from `checkDependencies`.
57
+ * @return {boolean} True when the app can be used.
58
+ */
59
+ export function isDependencyResolved(row) {
60
+ return !!row && row.installed === true && row.enabled === true
61
+ }
@@ -199,6 +199,162 @@ export function persistWalkthroughSeenVersion(appId, configKey, version, options
199
199
  }
200
200
  }
201
201
 
202
+ /**
203
+ * localStorage key prefix for the per-browser mirror of tour progress.
204
+ */
205
+ export const WALKTHROUGH_PROGRESS_STORAGE_PREFIX = 'cn-walkthrough-progress:'
206
+
207
+ /**
208
+ * The preference key that holds tour progress, next to the completion key.
209
+ *
210
+ * @param {string} configKey `manifest.walkthrough.completionConfigKey` (may be empty).
211
+ * @return {string} `<configKey>-progress`, or '' when no key is declared.
212
+ */
213
+ export function walkthroughProgressKey(configKey) {
214
+ return configKey ? configKey + '-progress' : ''
215
+ }
216
+
217
+ /**
218
+ * Normalise stored progress (an object, or the JSON string a preference
219
+ * holds) into `{ tourId, stepId, index, version }`, or null.
220
+ *
221
+ * @spec openspec/changes/walkthrough-advance-pause-resume/specs/cn-walkthrough/spec.md
222
+ * @param {unknown} value The stored value.
223
+ * @return {{tourId: string, stepId: string, index: number, version: string}|null} The progress.
224
+ */
225
+ export function normaliseWalkthroughProgress(value) {
226
+ let data = value
227
+ if (typeof data === 'string') {
228
+ if (data === '') {
229
+ return null
230
+ }
231
+ try {
232
+ data = JSON.parse(data)
233
+ } catch {
234
+ return null
235
+ }
236
+ }
237
+ if (!isPlainObject(data) || typeof data.tourId !== 'string' || data.tourId === '') {
238
+ return null
239
+ }
240
+ return {
241
+ tourId: data.tourId,
242
+ stepId: typeof data.stepId === 'string' ? data.stepId : '',
243
+ index: Number.isInteger(data.index) && data.index >= 0 ? data.index : 0,
244
+ version: typeof data.version === 'string' ? data.version : '',
245
+ }
246
+ }
247
+
248
+ /**
249
+ * Read the per-browser mirror of the tour progress. Synchronous.
250
+ *
251
+ * @spec openspec/changes/walkthrough-advance-pause-resume/specs/cn-walkthrough/spec.md
252
+ * @param {string} appId The Nextcloud app id.
253
+ * @param {Storage} [storage] Injectable storage backend.
254
+ * @return {object|null} The progress, or null.
255
+ */
256
+ export function readLocalWalkthroughProgress(appId, storage) {
257
+ const s = resolveStorage(storage)
258
+ if (!s) {
259
+ return null
260
+ }
261
+ try {
262
+ return normaliseWalkthroughProgress(s.getItem(WALKTHROUGH_PROGRESS_STORAGE_PREFIX + appId))
263
+ } catch {
264
+ return null
265
+ }
266
+ }
267
+
268
+ /**
269
+ * Load the user's tour progress: the per-user preference
270
+ * `<completionConfigKey>-progress` when declared, else the local mirror.
271
+ *
272
+ * @spec openspec/changes/walkthrough-advance-pause-resume/specs/cn-walkthrough/spec.md
273
+ * @param {string} appId The Nextcloud app id.
274
+ * @param {string} configKey `manifest.walkthrough.completionConfigKey` (may be empty).
275
+ * @param {object} [options] `{ http, storage }` injection points for tests.
276
+ * @return {Promise<object|null>} The progress, or null.
277
+ */
278
+ export async function loadWalkthroughProgress(appId, configKey, options = {}) {
279
+ const local = readLocalWalkthroughProgress(appId, options.storage)
280
+ const key = walkthroughProgressKey(configKey)
281
+ if (!key) {
282
+ return local
283
+ }
284
+ const http = options.http || axios
285
+ try {
286
+ const { data } = await http.get(walkthroughPreferenceUrl(appId, key))
287
+ if (!isPlainObject(data) || !('value' in data)) {
288
+ return local
289
+ }
290
+ return normaliseWalkthroughProgress(data.value) || local
291
+ } catch {
292
+ return local
293
+ }
294
+ }
295
+
296
+ /**
297
+ * Persist the user's tour progress, or clear it with `null`. Writes the
298
+ * local mirror and, with a completion key, the per-user preference. Never
299
+ * throws.
300
+ *
301
+ * @spec openspec/changes/walkthrough-advance-pause-resume/specs/cn-walkthrough/spec.md
302
+ * @param {string} appId The Nextcloud app id.
303
+ * @param {string} configKey `manifest.walkthrough.completionConfigKey` (may be empty).
304
+ * @param {object|null} progress `{ tourId, stepId, index, version }`, or null to clear.
305
+ * @param {object} [options] `{ http, storage }` injection points for tests.
306
+ * @return {Promise<boolean>} True when the server preference was written.
307
+ */
308
+ export function persistWalkthroughProgress(appId, configKey, progress, options = {}) {
309
+ const value = progress ? normaliseWalkthroughProgress(progress) : null
310
+ const raw = value ? JSON.stringify(value) : ''
311
+ const s = resolveStorage(options.storage)
312
+ if (s) {
313
+ try {
314
+ if (raw) {
315
+ s.setItem(WALKTHROUGH_PROGRESS_STORAGE_PREFIX + appId, raw)
316
+ } else {
317
+ s.removeItem(WALKTHROUGH_PROGRESS_STORAGE_PREFIX + appId)
318
+ }
319
+ } catch {
320
+ /* quota / private mode: best-effort */
321
+ }
322
+ }
323
+ const key = walkthroughProgressKey(configKey)
324
+ if (!key) {
325
+ return Promise.resolve(false)
326
+ }
327
+ const http = options.http || axios
328
+ try {
329
+ return Promise.resolve(http.put(walkthroughPreferenceUrl(appId, key), { value: raw }))
330
+ .then(() => true)
331
+ .catch(() => false)
332
+ } catch {
333
+ return Promise.resolve(false)
334
+ }
335
+ }
336
+
337
+ /**
338
+ * Whether an `object-created` signal satisfies a step's `advanceOn`.
339
+ *
340
+ * The signal's own `register` / `schema` (the slugs the creating surface
341
+ * knows) are compared first; an object's `@self` values are numeric ids on
342
+ * OpenRegister and only match a step that also uses ids.
343
+ *
344
+ * @param {object} advanceOn The step's `advanceOn`.
345
+ * @param {object} signal `{ register?, schema?, object? }`.
346
+ * @return {boolean} True when it matches.
347
+ */
348
+ function objectCreatedMatches(advanceOn, signal) {
349
+ const obj = signal.object || {}
350
+ const self = obj['@self'] || {}
351
+ const same = (want, candidates) => !want || candidates
352
+ .filter((c) => c !== null && c !== undefined && c !== '')
353
+ .some((c) => String(c).toLowerCase() === String(want).toLowerCase())
354
+ return same(advanceOn.register, [signal.register, obj.register, self.register])
355
+ && same(advanceOn.schema, [signal.schema, obj.schema, self.schema])
356
+ }
357
+
202
358
  /**
203
359
  * Compare two semver-ish strings. Missing / unparseable parts sort as 0.
204
360
  *
@@ -276,6 +432,9 @@ export function useWalkthrough(appId, manifest, options = {}) {
276
432
  const currentIndex = ref(0)
277
433
  const context = ref({})
278
434
  const running = ref(false)
435
+ // True while the tour is hidden by ESC or a backdrop click. The tour
436
+ // and its step stay, so it continues where it was.
437
+ const paused = ref(false)
279
438
  // True while a user-initiated replay is active (the "Restart tutorial"
280
439
  // entry). A replay shows the FULL tour regardless of the persisted
281
440
  // seen-version — otherwise a returning user, whose seenVersion already
@@ -479,6 +638,7 @@ export function useWalkthrough(appId, manifest, options = {}) {
479
638
  */
480
639
  function start(tourId, startIndex = 0) {
481
640
  replaying.value = false
641
+ paused.value = false
482
642
  activeTourId.value = tourId
483
643
  currentIndex.value = startIndex
484
644
  context.value = {}
@@ -503,9 +663,59 @@ export function useWalkthrough(appId, manifest, options = {}) {
503
663
  */
504
664
  function dismiss() {
505
665
  running.value = false
666
+ paused.value = false
506
667
  activeTourId.value = null
507
668
  replaying.value = false
508
669
  }
670
+ /**
671
+ * Hide the active tour without ending it (ESC, a click on the dim).
672
+ * The tour and step are kept, so `resumePaused()` continues there.
673
+ *
674
+ * @spec openspec/changes/walkthrough-advance-pause-resume/specs/cn-walkthrough/spec.md
675
+ * @return {void}
676
+ */
677
+ function pause() {
678
+ if (!activeTourId.value) {
679
+ return
680
+ }
681
+ running.value = false
682
+ paused.value = true
683
+ }
684
+ /**
685
+ * Show a paused tour again at the step it was on.
686
+ *
687
+ * @spec openspec/changes/walkthrough-advance-pause-resume/specs/cn-walkthrough/spec.md
688
+ * @return {boolean} True when there was a paused tour to show.
689
+ */
690
+ function resumePaused() {
691
+ if (!paused.value || !activeTourId.value) {
692
+ return false
693
+ }
694
+ paused.value = false
695
+ running.value = true
696
+ return true
697
+ }
698
+ /**
699
+ * Start a tour at a remembered step ("continue where you left off").
700
+ * Runs in replay mode so a returning user sees the full tour, and
701
+ * falls back to the first step when the step no longer exists.
702
+ *
703
+ * @spec openspec/changes/walkthrough-advance-pause-resume/specs/cn-walkthrough/spec.md
704
+ * @param {string} tourId The tour id.
705
+ * @param {string} [stepId] The step to continue at.
706
+ * @return {boolean} True when the tour exists and started.
707
+ */
708
+ function resumeAt(tourId, stepId) {
709
+ const tour = tours.find((t) => t.id === tourId)
710
+ if (!tour) {
711
+ return false
712
+ }
713
+ replaying.value = true
714
+ const idx = composeSteps(tour).findIndex((s) => s.id === stepId)
715
+ start(tourId, Math.max(0, idx))
716
+ replaying.value = true
717
+ return true
718
+ }
509
719
  /**
510
720
  * Mark the active tour complete: stop running and invoke the
511
721
  * `onComplete(appVersion)` persistence callback so the seen version is
@@ -515,6 +725,7 @@ export function useWalkthrough(appId, manifest, options = {}) {
515
725
  */
516
726
  function complete() {
517
727
  running.value = false
728
+ paused.value = false
518
729
  activeTourId.value = null
519
730
  replaying.value = false
520
731
  if (typeof options.onComplete === 'function') {
@@ -542,12 +753,9 @@ export function useWalkthrough(appId, manifest, options = {}) {
542
753
  runCapture(step, { params: signal.params || {} })
543
754
  }
544
755
  } else if (signal.kind === 'object-created' && a.type === 'object-created') {
545
- const obj = signal.object || {}
546
- const reg = obj.register ?? obj['@self']?.register
547
- const sch = obj.schema ?? obj['@self']?.schema
548
- match = (!a.register || reg === a.register) && (!a.schema || sch === a.schema)
756
+ match = objectCreatedMatches(a, signal)
549
757
  if (match) {
550
- runCapture(step, { object: obj })
758
+ runCapture(step, { object: signal.object || {} })
551
759
  }
552
760
  } else if (signal.kind === 'element' && a.type === 'element-appears') {
553
761
  match = true
@@ -575,9 +783,13 @@ export function useWalkthrough(appId, manifest, options = {}) {
575
783
  isLast,
576
784
  context,
577
785
  running,
786
+ paused,
578
787
  replaying,
579
788
  start,
580
789
  restart,
790
+ pause,
791
+ resumePaused,
792
+ resumeAt,
581
793
  next,
582
794
  back,
583
795
  skip,