@conduction/nextcloud-vue 2.61.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 (186) 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 +394 -27
  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/CnBannerWidget/CnBannerWidget.vue2.js +79 -2
  15. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js.map +1 -1
  16. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js +38 -25
  17. package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js.map +1 -1
  18. package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue2.js +21 -1
  19. package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue2.js.map +1 -1
  20. package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue3.js +4 -1
  21. package/dist/esm/components/CnBrandStripe/CnBrandStripe.vue3.js.map +1 -1
  22. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue2.js.map +1 -1
  23. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js +53 -37
  24. package/dist/esm/components/CnChoiceCards/CnChoiceCards.vue3.js.map +1 -1
  25. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +30 -6
  26. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  27. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +2 -2
  28. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  29. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue.js +10 -0
  30. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue.js.map +1 -0
  31. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue2.js +293 -0
  32. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue2.js.map +1 -0
  33. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue3.js +189 -0
  34. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue3.js.map +1 -0
  35. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +256 -23
  36. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  37. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +119 -51
  38. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  39. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
  40. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +2 -1
  41. package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
  42. package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue2.js +3 -1
  43. package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue2.js.map +1 -1
  44. package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue3.js +1 -1
  45. package/dist/esm/components/CnFieldHelper/CnFieldHelper.vue3.js.map +1 -1
  46. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +234 -9
  47. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
  48. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +96 -57
  49. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
  50. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +51 -2
  51. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  52. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +5 -1
  53. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  54. package/dist/esm/components/CnIndexPage/selfModeActions.js +3 -3
  55. package/dist/esm/components/CnIndexPage/selfModeActions.js.map +1 -1
  56. package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue2.js.map +1 -1
  57. package/dist/esm/components/CnLinkCardsPage/CnLinkCardsPage.vue3.js.map +1 -1
  58. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +36 -1
  59. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
  60. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js +4 -1
  61. package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
  62. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +53 -0
  63. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
  64. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +22 -5
  65. package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
  66. package/dist/esm/components/CnReportsPage/CnReportsPage.vue2.js.map +1 -1
  67. package/dist/esm/components/CnReportsPage/CnReportsPage.vue3.js.map +1 -1
  68. package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue2.js +121 -12
  69. package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue2.js.map +1 -1
  70. package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue3.js +3 -1
  71. package/dist/esm/components/CnResourceSelect/CnResourceSelect.vue3.js.map +1 -1
  72. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js +326 -1
  73. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js.map +1 -1
  74. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js +112 -26
  75. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js.map +1 -1
  76. package/dist/esm/components/CnStorePage/CnStorePage.vue2.js.map +1 -1
  77. package/dist/esm/components/CnStorePage/CnStorePage.vue3.js.map +1 -1
  78. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue.js +10 -0
  79. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue.js.map +1 -0
  80. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue2.js +337 -0
  81. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue2.js.map +1 -0
  82. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue3.js +102 -0
  83. package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue3.js.map +1 -0
  84. package/dist/esm/components/CnTimelineWidget/dashboardRegistration.js +34 -0
  85. package/dist/esm/components/CnTimelineWidget/dashboardRegistration.js.map +1 -0
  86. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js +131 -8
  87. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue2.js.map +1 -1
  88. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue3.js +40 -38
  89. package/dist/esm/components/CnWalkthrough/CnWalkthrough.vue3.js.map +1 -1
  90. package/dist/esm/components/CnWidgetGrid/builtInWidgets.js +8 -6
  91. package/dist/esm/components/CnWidgetGrid/builtInWidgets.js.map +1 -1
  92. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
  93. package/dist/esm/components/CnWidgetGrid/registerDashboardWidgets.js.map +1 -1
  94. package/dist/esm/components/CnWikiPage/CnWikiPage.vue2.js.map +1 -1
  95. package/dist/esm/components/CnWikiPage/CnWikiPage.vue3.js.map +1 -1
  96. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue2.js +19 -0
  97. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue2.js.map +1 -1
  98. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue3.js +1 -1
  99. package/dist/esm/components/CnWizardDialog/CnWizardDialog.vue3.js.map +1 -1
  100. package/dist/esm/composables/useDependencyCheck.js +65 -0
  101. package/dist/esm/composables/useDependencyCheck.js.map +1 -0
  102. package/dist/esm/composables/useWalkthrough.js +218 -6
  103. package/dist/esm/composables/useWalkthrough.js.map +1 -1
  104. package/dist/esm/index.js +2 -0
  105. package/dist/esm/index.js.map +1 -1
  106. package/dist/esm/l10n/en.json.js +48 -0
  107. package/dist/esm/l10n/en.json.js.map +1 -1
  108. package/dist/esm/l10n/nl.json.js +48 -0
  109. package/dist/esm/l10n/nl.json.js.map +1 -1
  110. package/dist/esm/nextcloud-vue.css +557 -9
  111. package/dist/esm/store/useObjectStore.js +8 -0
  112. package/dist/esm/store/useObjectStore.js.map +1 -1
  113. package/dist/esm/utils/columnFilters.js +311 -0
  114. package/dist/esm/utils/columnFilters.js.map +1 -0
  115. package/dist/esm/utils/groupAutocomplete.js +123 -0
  116. package/dist/esm/utils/groupAutocomplete.js.map +1 -0
  117. package/dist/esm/utils/libraryWidgetKeys.js +2 -0
  118. package/dist/esm/utils/libraryWidgetKeys.js.map +1 -1
  119. package/dist/esm/utils/pickerOptions.js +198 -0
  120. package/dist/esm/utils/pickerOptions.js.map +1 -0
  121. package/dist/esm/utils/schema.js +83 -7
  122. package/dist/esm/utils/schema.js.map +1 -1
  123. package/dist/esm/utils/timelineEvents.js +174 -0
  124. package/dist/esm/utils/timelineEvents.js.map +1 -0
  125. package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
  126. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  127. package/dist/esm/utils/walkthroughSignals.js +88 -0
  128. package/dist/esm/utils/walkthroughSignals.js.map +1 -0
  129. package/dist/nextcloud-vue.cjs.js +9779 -5236
  130. package/dist/nextcloud-vue.cjs.js.map +2 -2
  131. package/dist/nextcloud-vue.css +557 -9
  132. package/l10n/en.json +48 -0
  133. package/l10n/nl.json +48 -0
  134. package/package.json +3 -2
  135. package/scripts/manifest-schema-hash.js +41 -0
  136. package/scripts/record-manifest-schema-hash.js +52 -0
  137. package/src/components/CnAdminActionCard/CnAdminActionCard.vue +234 -0
  138. package/src/components/CnAdminActionCard/index.js +4 -0
  139. package/src/components/CnAppNav/CnAppNav.vue +394 -27
  140. package/src/components/CnAppRoot/CnAppRoot.vue +129 -15
  141. package/src/components/CnBannerWidget/CnBannerWidget.vue +113 -2
  142. package/src/components/CnBrandStripe/CnBrandStripe.md +16 -0
  143. package/src/components/CnBrandStripe/CnBrandStripe.vue +58 -5
  144. package/src/components/CnChoiceCards/CnChoiceCards.vue +56 -31
  145. package/src/components/CnDashboardPage/CnDashboardPage.vue +32 -8
  146. package/src/components/CnDataTable/CnColumnFilterPopover.vue +524 -0
  147. package/src/components/CnDataTable/CnDataTable.md +4 -1
  148. package/src/components/CnDataTable/CnDataTable.vue +302 -21
  149. package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +2 -2
  150. package/src/components/CnFieldHelper/CnFieldHelper.vue +4 -2
  151. package/src/components/CnFormDialog/CnFormDialog.vue +290 -31
  152. package/src/components/CnIndexPage/CnIndexPage.vue +55 -2
  153. package/src/components/CnIndexPage/selfModeActions.js +3 -3
  154. package/src/components/CnLinkCardsPage/CnLinkCardsPage.vue +35 -2
  155. package/src/components/CnObjectListWidget/CnObjectListWidget.vue +39 -1
  156. package/src/components/CnObjectSidebar/CnFilesTab.vue +65 -0
  157. package/src/components/CnReportsPage/CnReportsPage.vue +7 -0
  158. package/src/components/CnResourceSelect/CnResourceSelect.vue +123 -12
  159. package/src/components/CnSetupWizard/CnSetupWizard.vue +392 -3
  160. package/src/components/CnStorePage/CnStorePage.vue +7 -0
  161. package/src/components/CnTimelineWidget/CnTimelineWidget.vue +477 -0
  162. package/src/components/CnTimelineWidget/dashboardRegistration.js +32 -0
  163. package/src/components/CnTimelineWidget/index.js +4 -0
  164. package/src/components/CnWalkthrough/CnWalkthrough.vue +143 -10
  165. package/src/components/CnWidgetGrid/builtInWidgets.js +2 -0
  166. package/src/components/CnWidgetGrid/registerDashboardWidgets.js +1 -0
  167. package/src/components/CnWikiPage/CnWikiPage.vue +7 -0
  168. package/src/components/CnWizardDialog/CnWizardDialog.vue +20 -1
  169. package/src/components/index.js +2 -0
  170. package/src/composables/useDependencyCheck.js +61 -0
  171. package/src/composables/useWalkthrough.js +217 -5
  172. package/src/css/dashboard.css +49 -0
  173. package/src/css/detail-page.css +19 -0
  174. package/src/css/table.css +82 -2
  175. package/src/index.js +2 -0
  176. package/src/public/index.js +5 -0
  177. package/src/schemas/app-manifest-v2.schema.json +23 -1
  178. package/src/store/useObjectStore.js +8 -0
  179. package/src/utils/columnFilters.js +324 -0
  180. package/src/utils/groupAutocomplete.js +119 -0
  181. package/src/utils/libraryWidgetKeys.js +2 -0
  182. package/src/utils/pickerOptions.js +194 -0
  183. package/src/utils/schema.js +83 -7
  184. package/src/utils/timelineEvents.js +171 -0
  185. package/src/utils/validateManifestV2.compiled.js +2 -2
  186. package/src/utils/walkthroughSignals.js +94 -0
@@ -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,
@@ -80,3 +80,52 @@
80
80
  .cn-dashboard-list__item:hover {
81
81
  background: var(--color-background-hover);
82
82
  }
83
+
84
+ /* "Install <app>" placeholder in a widget whose app is missing.
85
+ *
86
+ * NcEmptyContent centres its content with `justify-content: center`. In a
87
+ * two-row dashboard tile (about 150px inside an `overflow: hidden` wrapper)
88
+ * the icon, name, description and button are taller than the tile, and plain
89
+ * `center` overflows on BOTH sides, so the top ran out of the widget and the
90
+ * name was cut off. `safe center` falls back to the start edge when the
91
+ * content does not fit, the spacing is compact, and what still does not fit
92
+ * scrolls instead of being clipped. Shared by CnDashboardPage and
93
+ * CnDetailWidgetHost (`cn-requires-app`). The class is doubled to outrank
94
+ * NcEmptyContent's own scoped rules (`.empty-content[data-v-…]`) whatever
95
+ * order the stylesheets load in. */
96
+ .cn-requires-app.cn-requires-app.empty-content,
97
+ .cn-requires-app.cn-requires-app .empty-content {
98
+ justify-content: safe center;
99
+ min-height: 0;
100
+ max-height: 100%;
101
+ overflow: auto;
102
+ margin: 0;
103
+ padding: calc(2 * var(--default-grid-baseline, 4px));
104
+ gap: var(--default-grid-baseline, 4px);
105
+ box-sizing: border-box;
106
+ }
107
+
108
+ .cn-requires-app.cn-requires-app .empty-content__icon {
109
+ width: auto;
110
+ height: auto;
111
+ min-height: 0;
112
+ margin: 0;
113
+ opacity: 0.7;
114
+ }
115
+
116
+ .cn-requires-app.cn-requires-app .empty-content__name {
117
+ margin: 0;
118
+ font-size: var(--default-font-size, 15px);
119
+ font-weight: 600;
120
+ text-align: center;
121
+ }
122
+
123
+ .cn-requires-app.cn-requires-app .empty-content__description {
124
+ margin: 0;
125
+ text-align: center;
126
+ font-size: 0.9em;
127
+ }
128
+
129
+ .cn-requires-app.cn-requires-app .empty-content__action {
130
+ margin-top: var(--default-grid-baseline, 4px);
131
+ }
@@ -106,6 +106,25 @@
106
106
  flex-shrink: 0;
107
107
  }
108
108
 
109
+ /* Every control in the header sits on one line at one height. The lifecycle
110
+ bar carries `margin: 0 0 8px` for its place above a page body; in the
111
+ header that margin pushed its buttons up and made the row 8px taller, so
112
+ the Edit button beside it read as the bigger one. A consumer's actions
113
+ component can bring its own margins too, so they are reset here as well. */
114
+ .cn-detail-page .cn-detail-page__header-actions .cn-lifecycle-actions {
115
+ margin: 0;
116
+ flex-wrap: nowrap;
117
+ }
118
+
119
+ .cn-detail-page .cn-detail-page__header-actions > * {
120
+ margin-block: 0;
121
+ }
122
+
123
+ .cn-detail-page .cn-detail-page__header-actions .button-vue {
124
+ min-height: var(--default-clickable-area);
125
+ block-size: var(--default-clickable-area);
126
+ }
127
+
109
128
  /* The sentence a refused lock acquire left behind, under the locked banner. */
110
129
  .cn-detail-page__lock-refusal {
111
130
  margin: 0 0 calc(3 * var(--default-grid-baseline));
package/src/css/table.css CHANGED
@@ -295,8 +295,88 @@
295
295
  outline-offset: -2px;
296
296
  }
297
297
 
298
- /* Multi-column sort priority badge (1, 2, 3) — only rendered once more than
299
- one sort key is active; a single active sort shows the arrow alone. */
298
+ /* Header filter button (CnDataTable `filterable`). 24px hit area with the
299
+ 16px icon; the filled icon plus the primary colour marks an active filter,
300
+ so the state never rests on colour alone. */
301
+ .cn-table-header__filter {
302
+ display: inline-flex;
303
+ align-items: center;
304
+ justify-content: center;
305
+ width: 24px;
306
+ min-width: 24px;
307
+ height: 24px;
308
+ min-height: 24px;
309
+ margin: 0 0 0 4px;
310
+ padding: 0;
311
+ border: none;
312
+ border-radius: var(--border-radius, 4px);
313
+ background: transparent;
314
+ color: var(--color-text-maxcontrast);
315
+ vertical-align: middle;
316
+ cursor: pointer;
317
+ }
318
+
319
+ .cn-table-header__filter:hover {
320
+ background-color: var(--color-background-hover);
321
+ color: var(--color-main-text);
322
+ }
323
+
324
+ .cn-table-header__filter:focus-visible {
325
+ outline: 2px solid var(--color-main-text);
326
+ outline-offset: 0;
327
+ }
328
+
329
+ .cn-table-header__filter--active {
330
+ color: var(--color-primary-element);
331
+ }
332
+
333
+ /* Active header filters above the table, one removable chip each. */
334
+ .cn-data-table__filter-chips {
335
+ display: flex;
336
+ flex-wrap: wrap;
337
+ gap: 6px;
338
+ margin: 0 0 8px;
339
+ padding: 0;
340
+ list-style: none;
341
+ }
342
+
343
+ .cn-data-table__filter-chip {
344
+ display: inline-flex;
345
+ align-items: center;
346
+ gap: 4px;
347
+ padding: 2px 4px 2px 10px;
348
+ border: 1px solid var(--color-border-dark);
349
+ border-radius: var(--border-radius-pill, 16px);
350
+ background-color: var(--color-background-dark);
351
+ color: var(--color-main-text);
352
+ }
353
+
354
+ .cn-data-table__filter-chip-remove {
355
+ display: inline-flex;
356
+ align-items: center;
357
+ justify-content: center;
358
+ width: 24px;
359
+ min-width: 24px;
360
+ height: 24px;
361
+ min-height: 24px;
362
+ margin: 0;
363
+ padding: 0;
364
+ border: none;
365
+ border-radius: 50%;
366
+ background: transparent;
367
+ color: var(--color-main-text);
368
+ cursor: pointer;
369
+ }
370
+
371
+ .cn-data-table__filter-chip-remove:hover {
372
+ background-color: var(--color-background-hover);
373
+ }
374
+
375
+ .cn-data-table__filter-chip-remove:focus-visible {
376
+ outline: 2px solid var(--color-main-text);
377
+ }
378
+
379
+ /* Multi-column sort priority badge (1, 2, 3), rendered only when two or more rendered, sortable columns are sorted. */
300
380
  .cn-table-sort-badge {
301
381
  display: inline-flex;
302
382
  align-items: center;
package/src/index.js CHANGED
@@ -35,6 +35,7 @@ export {
35
35
  CnActionsBar,
36
36
  CnActionsMenu,
37
37
  CnAddWidgetModal,
38
+ CnAdminActionCard,
38
39
  CnAdminSettingsShell,
39
40
  CnAdvancedFormDialog,
40
41
  CnAppLoading,
@@ -268,6 +269,7 @@ export {
268
269
  CnTileWidget,
269
270
  CnTimelineStages,
270
271
  CnTimelineView,
272
+ CnTimelineWidget,
271
273
  CnTranslatedBadge,
272
274
  CnTreeView,
273
275
  CnUserActionMenu,
@@ -40,6 +40,10 @@
40
40
  * claiming purity is worth nothing; the check is in CI.
41
41
  */
42
42
 
43
+ // The brand stripe imports nothing at all, so it is public-safe as it is: a
44
+ // portal draws the organisation's motif under its header with the same
45
+ // component, and the same tokens, as a Nextcloud page (brand-motif-token).
46
+ import CnBrandStripe from '../components/CnBrandStripe/CnBrandStripe.vue'
43
47
  import CnSiteCard from './components/CnSiteCard.vue'
44
48
  import CnSiteCardGrid from './components/CnSiteCardGrid.vue'
45
49
  import CnSiteEmptyState from './components/CnSiteEmptyState.vue'
@@ -50,6 +54,7 @@ import CnSiteSearch from './components/CnSiteSearch.vue'
50
54
  import CnSiteSection from './components/CnSiteSection.vue'
51
55
 
52
56
  export {
57
+ CnBrandStripe,
53
58
  CnSiteCard,
54
59
  CnSiteCardGrid,
55
60
  CnSiteEmptyState,
@@ -3,7 +3,7 @@
3
3
  "$id": "https://raw.githubusercontent.com/ConductionNL/nextcloud-vue/main/src/schemas/app-manifest-v2.schema.json",
4
4
  "title": "Conduction App Manifest v2",
5
5
  "description": "v2 schema for the JSON-driven page and navigation manifest consumed by @conduction/nextcloud-vue. Introduces a uniform widgets[] array on every page type with a per-slot grid coordinate system, a typed actions[] discriminator, and the required $schema field for version detection. v1 manifests continue to validate against app-manifest.schema.json (unchanged).",
6
- "version": "2.42.0",
6
+ "version": "2.45.0",
7
7
  "type": "object",
8
8
  "required": ["$schema", "version"],
9
9
  "allOf": [
@@ -97,6 +97,8 @@
97
97
  "suggestFrom": { "type": "string", "description": "choice: the configKey of an earlier choice this step pre-fills a suggestion from. When that value is set and the user has not yet picked here, the option matching suggestMap[parentValue] is auto-selected (still user-overridable)." },
98
98
  "suggestMap": { "type": "object", "description": "choice: map of suggestFrom value → suggested option value. Used to auto-suggest a default (e.g. RGS template suggested from organisation type).", "additionalProperties": { "type": "string" } },
99
99
  "multiple": { "type": "boolean", "default": false, "description": "choice: allow multi-select." },
100
+ "loadAction": { "type": "string", "minLength": 1, "description": "choice with display cards: an actionId each option card loads itself with. Every card except the one whose value is `none` gets its own Load button, which POSTs { dataset: <value> } to /api/setup/action/{loadAction}, spins while it runs and shows the result on that card. A successful load also selects the card. With loadAction the separate run-action step that loaded the picked dataset is no longer needed; manifests that keep it still work." },
101
+ "requires": { "type": "array", "items": { "type": "string", "minLength": 1 }, "uniqueItems": true, "description": "App ids this step needs. When any of them is not installed and enabled, the wizard skips the step and the summary shows it as skipped, naming the missing apps. Do not combine with required: true; setup status would keep reporting the skipped step as unmet." },
100
102
  "action": { "type": "string", "description": "run-action: actionId POSTed to /api/setup/action/{action}." },
101
103
  "onDemand": { "type": "boolean", "default": false, "description": "run-action: the step runs only when the user clicks Run, never automatically, and is never outstanding work (it does not open the wizard or decide where a resumed wizard starts). The summary ticks it only when its action succeeded in the current session and shows it as not run otherwise, whatever the server reports. For destructive steps an app reports as done on purpose so they never auto-run, such as removing example data." },
102
104
  "healthCheck": { "type": "boolean", "default": false, "description": "summary: render a health recap." },
@@ -1173,6 +1175,11 @@
1173
1175
  "type": "string",
1174
1176
  "description": "i18n translation key resolved by the consuming app's t() function at render time."
1175
1177
  },
1178
+ "translateLabel": {
1179
+ "type": "boolean",
1180
+ "default": true,
1181
+ "description": "Whether CnAppNav runs `label` through the translate function. Set false for a user-authored title (e.g. a catalog name) that must render as written even when it equals a translation key such as 'Search'. Rendered as text either way, never as HTML."
1182
+ },
1176
1183
  "icon": {
1177
1184
  "type": "string",
1178
1185
  "description": "CSS class for the icon (e.g. 'icon-checkmark') or MDI component name."
@@ -1186,6 +1193,11 @@
1186
1193
  "additionalProperties": { "type": ["string", "number", "boolean"] },
1187
1194
  "description": "Optional vue-router query parameters merged into the target route link (e.g. a filter like { caseType: '<uuid>' }). Deep-links a nav entry to a pre-filtered index page."
1188
1195
  },
1196
+ "params": {
1197
+ "type": "object",
1198
+ "additionalProperties": { "type": ["string", "number"], "minLength": 1 },
1199
+ "description": "Optional vue-router route params for a parameterised route (e.g. { catalogSlug: 'news' } for a page at '/catalogs/:catalogSlug'). String values must be non-empty. CnAppNav links to { name: route, params, query }. The entry is active only while every declared param equals the current route's param (compared as strings), so sibling entries on one route that differ in params are never marked together. On pages whose path sits below its page path with these params filled in, it is active too: by the same params check when those routes carry the same param names, otherwise as the one entry marked below its route."
1200
+ },
1189
1201
  "order": {
1190
1202
  "type": "integer",
1191
1203
  "description": "Display order in the menu. Items without an order render last."
@@ -1259,6 +1271,11 @@
1259
1271
  "properties": {
1260
1272
  "id": { "type": "string" },
1261
1273
  "label": { "type": "string" },
1274
+ "translateLabel": {
1275
+ "type": "boolean",
1276
+ "default": true,
1277
+ "description": "Same as menuItem.translateLabel: false renders `label` as written instead of translating it."
1278
+ },
1262
1279
  "icon": { "type": "string" },
1263
1280
  "route": { "type": "string" },
1264
1281
  "query": {
@@ -1266,6 +1283,11 @@
1266
1283
  "additionalProperties": { "type": ["string", "number", "boolean"] },
1267
1284
  "description": "Optional vue-router query parameters merged into the target route link (e.g. a filter like { caseType: '<uuid>' }). Deep-links a nav entry to a pre-filtered index page."
1268
1285
  },
1286
+ "params": {
1287
+ "type": "object",
1288
+ "additionalProperties": { "type": ["string", "number"], "minLength": 1 },
1289
+ "description": "Optional vue-router route params for a parameterised route; string values must be non-empty. Same contract as menuItem.params: part of the link, and the entry is active only while every declared param equals the current route's param."
1290
+ },
1269
1291
  "order": { "type": "integer" },
1270
1292
  "permission": { "type": "string" },
1271
1293
  "section": {
@@ -5,6 +5,7 @@ import { genericError, networkError, parseResponseError } from '../utils/errors.
5
5
  import { normalizeFacets } from '../utils/facets.js'
6
6
  import { buildHeaders, buildQueryString, capitalize, prefixUrl } from '../utils/headers.js'
7
7
  import { extractId } from '../utils/id.js'
8
+ import { dispatchObjectCreated } from '../utils/walkthroughSignals.js'
8
9
  import { mergePluginActions, mergePluginGetters, mergePluginState } from './pluginMerge.js'
9
10
  import { liveUpdatesPlugin } from './plugins/liveUpdates.js'
10
11
 
@@ -830,6 +831,13 @@ const baseActions = {
830
831
  [type]: { ...(this.objects[type] || {}), [savedId]: data },
831
832
  }
832
833
 
834
+ // Every create through the store lets an `advanceOn:
835
+ // object-created` walkthrough step advance, whichever form made it.
836
+ if (!isUpdate) {
837
+ const config = this.objectTypeRegistry[type] || {}
838
+ dispatchObjectCreated({ register: config.registerSlug || config.register, schema: config.schemaSlug || config.schema, object: data })
839
+ }
840
+
833
841
  return data
834
842
  } catch (error) {
835
843
  this.errors = {