@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
@@ -12,7 +12,8 @@
12
12
  :successText="successText"
13
13
  :validate="validateStep"
14
14
  :cancellable="cancellable"
15
- :initialStep="initialStepId"
15
+ :initialStep="dependencyBlocked ? '' : initialStepId"
16
+ :nextDisabled="dependencyBlocked"
16
17
  @stepChange="onStepChange"
17
18
  @submit="onSubmit"
18
19
  @close="onClose">
@@ -21,9 +22,33 @@
21
22
  `<template>` (verified against the generated `createSlots` call)
22
23
  and honours it only on the child, so the rule's advice is inverted
23
24
  here — moving the key up would throw it away. -->
25
+ <!-- A required app is missing: this is the only step, and Next stays
26
+ disabled until the app is there. -->
27
+ <template v-if="dependencyBlocked" #[dependencyStepSlot]>
28
+ <div class="cn-setup-step" data-step-type="dependencies" data-testid="cn-setup-dependencies">
29
+ <NcNoteCard type="warning">
30
+ {{ dependencyBlockedText }}
31
+ </NcNoteCard>
32
+ <CnLeafDependencySettings
33
+ :appId="appId"
34
+ :dependencies="dependencyEntries"
35
+ :isAdmin="isAdmin"
36
+ :sectionName="dependencySectionName"
37
+ sectionDescription=""
38
+ @installed="onDependencyInstalled" />
39
+ </div>
40
+ </template>
24
41
  <!-- eslint-disable vue/no-v-for-template-key-on-child -->
25
42
  <template v-for="step in setupSteps" #[stepSlot(step)]="scope">
26
43
  <div :key="step.id" class="cn-setup-step" :data-step-type="step.type">
44
+ <!-- Optional apps that are missing never block, but the admin
45
+ reads about them once, on the first step. -->
46
+ <NcNoteCard
47
+ v-if="step.id === firstStepId && missingOptional.length > 0"
48
+ type="info"
49
+ data-testid="cn-setup-optional-dependencies">
50
+ {{ optionalMissingText }}
51
+ </NcNoteCard>
27
52
  <!-- @slot step-{id} Override a step's body (for `component` steps or
28
53
  any bespoke step). Scope: the CnWizardDialog step scope plus
29
54
  `{ step, runAction, saveConfig }`. -->
@@ -55,7 +80,33 @@
55
80
  :disabled="isChoiceDisabled(step)"
56
81
  :loading="isOptionsLoading(step)"
57
82
  :modelValue="cardModel(step)"
58
- @update:modelValue="(v) => onChoice(step, v)" />
83
+ @update:modelValue="(v) => onChoice(step, v)">
84
+ <!-- `loadAction`: every dataset card except "none" loads
85
+ itself, so no separate run-action step is needed. -->
86
+ <template v-if="step.loadAction" #option-actions="{ option }">
87
+ <template v-if="isLoadableOption(step, option)">
88
+ <NcButton
89
+ variant="secondary"
90
+ :disabled="isDatasetLoading(step, option)"
91
+ :aria-label="loadAriaLabel(option)"
92
+ data-testid="cn-setup-load-dataset"
93
+ @click="loadDataset(step, option)">
94
+ <template v-if="isDatasetLoading(step, option)" #icon>
95
+ <NcLoadingIcon :size="20" />
96
+ </template>
97
+ {{ loadButtonLabel(step, option) }}
98
+ </NcButton>
99
+ <p
100
+ v-if="datasetResultFor(step, option)"
101
+ role="status"
102
+ class="cn-setup-load-result"
103
+ :class="datasetResultFor(step, option).success ? 'cn-setup-load-result--success' : 'cn-setup-load-result--error'"
104
+ data-testid="cn-setup-load-result">
105
+ {{ datasetResultFor(step, option).message }}
106
+ </p>
107
+ </template>
108
+ </template>
109
+ </CnChoiceCards>
59
110
  <NcSelect
60
111
  v-else
61
112
  :inputLabel="stepTitle(step)"
@@ -147,13 +198,23 @@
147
198
  </template>
148
199
 
149
200
  <script>
201
+ import { getCurrentUser } from '@nextcloud/auth'
202
+ import { loadState } from '@nextcloud/initial-state'
150
203
  import { translate as t } from '@nextcloud/l10n'
151
204
  import { NcButton, NcCheckboxRadioSwitch, NcLoadingIcon, NcNoteCard, NcSelect, NcTextField } from '@nextcloud/vue'
152
205
  import CnChoiceCards from '../CnChoiceCards/CnChoiceCards.vue'
206
+ import CnLeafDependencySettings from '../CnLeafDependencySettings/CnLeafDependencySettings.vue'
153
207
  import CnWizardDialog from '../CnWizardDialog/CnWizardDialog.vue'
208
+ import { checkDependencies, isDependencyResolved } from '../../composables/useDependencyCheck.js'
154
209
  import { useSetupStatus } from '../../composables/useSetupStatus.js'
155
210
  import { fieldsFromSchema } from '../../utils/schema.js'
156
211
 
212
+ /** Id of the synthetic step shown while a required app is missing. */
213
+ const DEPENDENCY_STEP_ID = 'cn-setup-dependencies'
214
+
215
+ /** The dataset value that means "seed nothing"; it never gets a Load button. */
216
+ const NONE_DATASET = 'none'
217
+
157
218
  /**
158
219
  * CnSetupWizard — abstract, manifest-driven first-time setup wizard (ADR-042).
159
220
  *
@@ -183,6 +244,7 @@ export default {
183
244
  components: {
184
245
  CnWizardDialog,
185
246
  CnChoiceCards,
247
+ CnLeafDependencySettings,
186
248
  NcButton,
187
249
  NcNoteCard,
188
250
  NcSelect,
@@ -201,6 +263,11 @@ export default {
201
263
  * standalone (no CnAppRoot ancestor).
202
264
  */
203
265
  cnTranslate: { default: () => (key) => key },
266
+ /**
267
+ * The app manifest, provided by CnAppRoot. Read only for its
268
+ * `dependencies` when the `dependencies` prop is empty.
269
+ */
270
+ cnManifest: { default: null },
204
271
  },
205
272
 
206
273
  props: {
@@ -310,6 +377,33 @@ export default {
310
377
  type: Array,
311
378
  default: () => [],
312
379
  },
380
+
381
+ /**
382
+ * The app's dependencies, in the manifest's own shape (app-id strings
383
+ * or `{ id, name?, required? }`). Falls back to the injected manifest's
384
+ * `dependencies`. A missing REQUIRED one replaces every step with the
385
+ * dependency list and disables Next; a missing optional one is listed
386
+ * on the first step and does not block.
387
+ *
388
+ * @type {Array<string|object>}
389
+ * @spec openspec/changes/setup-wizard-card-load-and-dependency-gate/specs/cn-setup-wizard/spec.md#requirement-the-wizard-checks-dependencies-before-any-step
390
+ */
391
+ dependencies: {
392
+ type: Array,
393
+ default: () => [],
394
+ },
395
+
396
+ /** Load button label on a dataset card (`loadAction`). */
397
+ loadLabel: {
398
+ type: String,
399
+ default: () => t('nextcloud-vue', 'Load'),
400
+ },
401
+
402
+ /** Load button label once that dataset loaded in this session. */
403
+ reloadLabel: {
404
+ type: String,
405
+ default: () => t('nextcloud-vue', 'Load again'),
406
+ },
313
407
  },
314
408
 
315
409
  emits: ['complete', 'close', 'step-change', 'action-result'],
@@ -345,6 +439,9 @@ export default {
345
439
  actionResult: {},
346
440
  localDone: {},
347
441
  userTouched: {},
442
+ // Per-card load state for `loadAction` steps, keyed by `loadKey()`.
443
+ datasetRunning: {},
444
+ datasetResult: {},
348
445
  }
349
446
  },
350
447
 
@@ -362,11 +459,150 @@ export default {
362
459
  : this.dialogTitle
363
460
  },
364
461
 
365
- setupSteps() {
462
+ /**
463
+ * Every well-formed step, before `requires` is applied.
464
+ *
465
+ * @return {Array<object>}
466
+ */
467
+ declaredSteps() {
366
468
  return (this.steps || []).filter((s) => s && s.id && s.type)
367
469
  },
368
470
 
471
+ /**
472
+ * Steps that declare `requires` on an app that is not installed and
473
+ * enabled. They are not offered; the summary names them as skipped.
474
+ *
475
+ * @return {Array<{ step: object, missing: Array<string> }>}
476
+ * @spec openspec/changes/setup-wizard-card-load-and-dependency-gate/specs/cn-setup-wizard/spec.md#requirement-the-wizard-checks-dependencies-before-any-step
477
+ */
478
+ skippedSteps() {
479
+ return this.declaredSteps
480
+ .filter((s) => Array.isArray(s.requires) && s.requires.length > 0)
481
+ .map((step) => {
482
+ const known = Object.fromEntries(this.dependencyRows.map((d) => [d.id, d]))
483
+ const missing = checkDependencies(step.requires, this.serverAppStatuses)
484
+ .map((row) => known[row.id] || row)
485
+ .filter((row) => !isDependencyResolved(row))
486
+ .map((row) => row.name || row.id)
487
+ return { step, missing }
488
+ })
489
+ .filter((entry) => entry.missing.length > 0)
490
+ },
491
+
492
+ setupSteps() {
493
+ const skipped = new Set(this.skippedSteps.map((entry) => entry.step.id))
494
+ return this.declaredSteps.filter((s) => !skipped.has(s.id))
495
+ },
496
+
497
+ /**
498
+ * Id of the first offered step: where the optional-apps note shows.
499
+ *
500
+ * @return {string}
501
+ */
502
+ firstStepId() {
503
+ return this.setupSteps.length > 0 ? this.setupSteps[0].id : ''
504
+ },
505
+
506
+ /**
507
+ * The dependency declarations to check: the prop, else the manifest's.
508
+ *
509
+ * @return {Array<string|object>}
510
+ */
511
+ dependencyEntries() {
512
+ if (Array.isArray(this.dependencies) && this.dependencies.length > 0) {
513
+ return this.dependencies
514
+ }
515
+ const fromManifest = this.cnManifest && this.cnManifest.dependencies
516
+ return Array.isArray(fromManifest) ? fromManifest : []
517
+ },
518
+
519
+ /**
520
+ * The `dependency_statuses` initial state an app may inject from PHP.
521
+ * Same source CnAppRoot reads; `{}` when the app injects none.
522
+ *
523
+ * @return {object}
524
+ */
525
+ serverAppStatuses() {
526
+ try {
527
+ return loadState(this.appId, 'dependency_statuses', {}) || {}
528
+ } catch {
529
+ return {}
530
+ }
531
+ },
532
+
533
+ /**
534
+ * The declared dependencies with their live status.
535
+ *
536
+ * @return {Array<object>}
537
+ */
538
+ dependencyRows() {
539
+ return checkDependencies(this.dependencyEntries, this.serverAppStatuses)
540
+ },
541
+
542
+ /** @return {Array<object>} Required dependencies that are missing or disabled. */
543
+ missingRequired() {
544
+ return this.dependencyRows.filter((d) => d.required && !isDependencyResolved(d))
545
+ },
546
+
547
+ /** @return {Array<object>} Optional dependencies that are missing or disabled. */
548
+ missingOptional() {
549
+ return this.dependencyRows.filter((d) => !d.required && !isDependencyResolved(d))
550
+ },
551
+
552
+ /**
553
+ * Whether a required app is missing, so the wizard shows the
554
+ * dependency list instead of its steps and refuses to continue.
555
+ *
556
+ * @return {boolean}
557
+ * @spec openspec/changes/setup-wizard-card-load-and-dependency-gate/specs/cn-setup-wizard/spec.md#requirement-the-wizard-checks-dependencies-before-any-step
558
+ */
559
+ dependencyBlocked() {
560
+ return this.missingRequired.length > 0
561
+ },
562
+
563
+ dependencyStepSlot() {
564
+ return 'step-' + DEPENDENCY_STEP_ID
565
+ },
566
+
567
+ dependencySectionName() {
568
+ return t('nextcloud-vue', 'Apps this app needs')
569
+ },
570
+
571
+ dependencyBlockedText() {
572
+ return t('nextcloud-vue', 'Setup can start once these apps are installed and enabled: {apps}.', {
573
+ apps: this.missingRequired.map((d) => d.name).join(', '),
574
+ })
575
+ },
576
+
577
+ optionalMissingText() {
578
+ return t('nextcloud-vue', 'Optional, not installed: {apps}. You can add them later.', {
579
+ apps: this.missingOptional.map((d) => d.name).join(', '),
580
+ })
581
+ },
582
+
583
+ /**
584
+ * Whether the current user is a Nextcloud admin; only admins get the
585
+ * install and enable buttons.
586
+ *
587
+ * @return {boolean}
588
+ */
589
+ isAdmin() {
590
+ try {
591
+ const user = getCurrentUser()
592
+ return !!(user && user.isAdmin)
593
+ } catch {
594
+ return false
595
+ }
596
+ },
597
+
369
598
  wizardSteps() {
599
+ if (this.dependencyBlocked) {
600
+ return [{
601
+ id: DEPENDENCY_STEP_ID,
602
+ label: t('nextcloud-vue', 'Required apps'),
603
+ optional: false,
604
+ }]
605
+ }
370
606
  return this.setupSteps.map((s) => ({
371
607
  id: s.id,
372
608
  label: this.stepTitle(s),
@@ -390,6 +626,15 @@ export default {
390
626
  * @return {Array<{ id: string, title: string, value: string, done: boolean, notRun: boolean }>}
391
627
  */
392
628
  summaryItems() {
629
+ const skipped = this.skippedSteps
630
+ .filter((entry) => entry.step.type !== 'summary' && entry.step.type !== 'info')
631
+ .map((entry) => ({
632
+ id: entry.step.id,
633
+ title: this.stepTitle(entry.step),
634
+ value: t('nextcloud-vue', 'Skipped, needs {apps}', { apps: entry.missing.join(', ') }),
635
+ done: false,
636
+ notRun: true,
637
+ }))
393
638
  return this.setupSteps
394
639
  .filter((s) => s.type !== 'summary')
395
640
  .map((step) => {
@@ -426,6 +671,7 @@ export default {
426
671
  }
427
672
  return { id: step.id, title: this.stepTitle(step), value, done, notRun: false }
428
673
  })
674
+ .concat(skipped)
429
675
  },
430
676
 
431
677
  /**
@@ -486,6 +732,11 @@ export default {
486
732
  * @return {void}
487
733
  */
488
734
  maybeAutoRunStep(stepId) {
735
+ // Nothing runs while a required app is missing: every action
736
+ // would fail with an error that does not name the app.
737
+ if (this.dependencyBlocked) {
738
+ return
739
+ }
489
740
  const step = this.setupSteps.find((s) => s.id === stepId)
490
741
  if (step && step.type === 'run-action' && !this.isOnDemand(step) && !this.isStepDone(step.id) && !this.running[step.id]) {
491
742
  this.runAction(step)
@@ -868,6 +1119,131 @@ export default {
868
1119
  }
869
1120
  },
870
1121
 
1122
+ /**
1123
+ * Key for one card's load state.
1124
+ *
1125
+ * @param {object} step The choice step.
1126
+ * @param {object} option The card's option.
1127
+ * @return {string}
1128
+ */
1129
+ loadKey(step, option) {
1130
+ return step.id + '::' + String(option && option.value)
1131
+ },
1132
+
1133
+ /**
1134
+ * Whether a card gets a Load button: the step declares `loadAction`
1135
+ * and the option is not the "seed nothing" answer.
1136
+ *
1137
+ * @param {object} step The choice step.
1138
+ * @param {object} option The card's option.
1139
+ * @return {boolean}
1140
+ * @spec openspec/changes/setup-wizard-card-load-and-dependency-gate/specs/cn-setup-wizard/spec.md#requirement-a-dataset-card-loads-itself
1141
+ */
1142
+ isLoadableOption(step, option) {
1143
+ return !!(step && step.loadAction) && !!option && String(option.value) !== NONE_DATASET
1144
+ },
1145
+
1146
+ isDatasetLoading(step, option) {
1147
+ return this.datasetRunning[this.loadKey(step, option)] === true
1148
+ },
1149
+
1150
+ datasetResultFor(step, option) {
1151
+ return this.datasetResult[this.loadKey(step, option)] || null
1152
+ },
1153
+
1154
+ loadButtonLabel(step, option) {
1155
+ if (this.isDatasetLoading(step, option)) {
1156
+ return this.runningLabel
1157
+ }
1158
+ const result = this.datasetResultFor(step, option)
1159
+ return (result && result.success) ? this.reloadLabel : this.loadLabel
1160
+ },
1161
+
1162
+ loadAriaLabel(option) {
1163
+ return t('nextcloud-vue', 'Load {name}', { name: option.label })
1164
+ },
1165
+
1166
+ /**
1167
+ * Load one dataset from its card: POST `{ dataset }` to the step's
1168
+ * `loadAction`, spinner on the button while it runs, the result on the
1169
+ * card after. A successful load also selects the card, so the summary
1170
+ * and the stored choice name what was loaded.
1171
+ *
1172
+ * @param {object} step The choice step declaring `loadAction`.
1173
+ * @param {object} option The card's option.
1174
+ * @return {Promise<void>}
1175
+ * @spec openspec/changes/setup-wizard-card-load-and-dependency-gate/specs/cn-setup-wizard/spec.md#requirement-a-dataset-card-loads-itself
1176
+ */
1177
+ async loadDataset(step, option) {
1178
+ if (!this.isLoadableOption(step, option) || this.isDatasetLoading(step, option)) {
1179
+ return
1180
+ }
1181
+ const key = this.loadKey(step, option)
1182
+ this.datasetRunning[key] = true
1183
+ this.datasetResult[key] = null
1184
+ let result
1185
+ try {
1186
+ const [{ default: axios }, { generateUrl }] = await Promise.all([
1187
+ import('@nextcloud/axios'),
1188
+ import('@nextcloud/router'),
1189
+ ])
1190
+ const { data } = await axios.post(
1191
+ generateUrl(`/apps/${this.appId}/api/setup/action/${step.loadAction}`),
1192
+ { dataset: option.value },
1193
+ )
1194
+ result = {
1195
+ success: !!data && data.success !== false,
1196
+ message: (data && data.message) || t('nextcloud-vue', 'Loaded.'),
1197
+ }
1198
+ } catch (err) {
1199
+ result = { success: false, message: this.errorMessage(err) }
1200
+ } finally {
1201
+ this.datasetRunning[key] = false
1202
+ }
1203
+ this.datasetResult[key] = result
1204
+ if (result.success) {
1205
+ this.selectLoadedOption(step, option)
1206
+ this.localDone[step.id] = true
1207
+ }
1208
+ this.$emit('action-result', {
1209
+ stepId: step.id,
1210
+ action: step.loadAction,
1211
+ dataset: option.value,
1212
+ ...result,
1213
+ })
1214
+ },
1215
+
1216
+ /**
1217
+ * Select a card that just loaded, adding to (never replacing) the
1218
+ * selection of a `multiple` step.
1219
+ *
1220
+ * @param {object} step The choice step.
1221
+ * @param {object} option The loaded option.
1222
+ * @return {void}
1223
+ */
1224
+ selectLoadedOption(step, option) {
1225
+ if (step.multiple === true) {
1226
+ const current = this.cardModel(step)
1227
+ if (current.some((v) => String(v) === String(option.value))) {
1228
+ return
1229
+ }
1230
+ this.onChoice(step, current.concat([option.value]))
1231
+ return
1232
+ }
1233
+ this.onChoice(step, option.value)
1234
+ },
1235
+
1236
+ /**
1237
+ * A required app was installed from the dependency list. A new app's
1238
+ * scripts and its `OC.appswebroots` entry exist only after a full page
1239
+ * load, and `useAppStatus` caches per page, so reload.
1240
+ *
1241
+ * @return {void}
1242
+ */
1243
+ onDependencyInstalled() {
1244
+ window.location.reload()
1245
+ },
1246
+
871
1247
  /**
872
1248
  * Whether a step is marked on-demand (`onDemand: true`): it runs only
873
1249
  * when the user asks, never automatically, and the summary ticks it
@@ -971,6 +1347,19 @@ export default {
971
1347
  color: var(--color-text-maxcontrast);
972
1348
  }
973
1349
 
1350
+ .cn-setup-load-result {
1351
+ margin: 0;
1352
+ font-size: var(--font-size-small, 13px);
1353
+ }
1354
+
1355
+ .cn-setup-load-result--success {
1356
+ color: var(--color-success-text, var(--color-success));
1357
+ }
1358
+
1359
+ .cn-setup-load-result--error {
1360
+ color: var(--color-error-text, var(--color-error));
1361
+ }
1362
+
974
1363
  .cn-setup-field {
975
1364
  display: flex;
976
1365
  flex-direction: column;