@conduction/nextcloud-vue 2.65.0 → 2.67.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 (103) hide show
  1. package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js +19 -0
  2. package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js.map +1 -1
  3. package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js +10 -3
  4. package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js.map +1 -1
  5. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js +2 -2
  6. package/dist/esm/components/CnAppNav/CnAppNav.vue2.js.map +1 -1
  7. package/dist/esm/components/CnAppNav/CnAppNav.vue3.js.map +1 -1
  8. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +99 -2
  9. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
  10. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +438 -383
  11. package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
  12. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue2.js +9 -3
  13. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue2.js.map +1 -1
  14. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue3.js +1 -1
  15. package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue3.js.map +1 -1
  16. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +4 -4
  17. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  18. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  19. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +78 -21
  20. package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
  21. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +129 -96
  22. package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
  23. package/dist/esm/components/CnFkResolveCell/CnFkResolveCell.vue2.js +3 -1
  24. package/dist/esm/components/CnFkResolveCell/CnFkResolveCell.vue2.js.map +1 -1
  25. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +144 -4
  26. package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
  27. package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
  28. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js +63 -7
  29. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js.map +1 -1
  30. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js +2 -1
  31. package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js.map +1 -1
  32. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +31 -1
  33. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  34. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +335 -313
  35. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  36. package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue2.js +11 -0
  37. package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue2.js.map +1 -1
  38. package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue3.js +2 -1
  39. package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue3.js.map +1 -1
  40. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js +5 -11
  41. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js.map +1 -1
  42. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js.map +1 -1
  43. package/dist/esm/composables/useDependencyCheck.js +44 -1
  44. package/dist/esm/composables/useDependencyCheck.js.map +1 -1
  45. package/dist/esm/composables/useListView.js +43 -1
  46. package/dist/esm/composables/useListView.js.map +1 -1
  47. package/dist/esm/composables/useSetupStatus.js +26 -3
  48. package/dist/esm/composables/useSetupStatus.js.map +1 -1
  49. package/dist/esm/l10n/en.json.js +7 -1
  50. package/dist/esm/l10n/en.json.js.map +1 -1
  51. package/dist/esm/l10n/nl.json.js +7 -1
  52. package/dist/esm/l10n/nl.json.js.map +1 -1
  53. package/dist/esm/mixins/pageViews.js +400 -0
  54. package/dist/esm/mixins/pageViews.js.map +1 -0
  55. package/dist/esm/nextcloud-vue.css +38 -2
  56. package/dist/esm/store/plugins/liveUpdates.js +19 -9
  57. package/dist/esm/store/plugins/liveUpdates.js.map +1 -1
  58. package/dist/esm/store/useObjectStore.js +20 -9
  59. package/dist/esm/store/useObjectStore.js.map +1 -1
  60. package/dist/esm/utils/actionsDispatcher.js +11 -2
  61. package/dist/esm/utils/actionsDispatcher.js.map +1 -1
  62. package/dist/esm/utils/columnFilters.js +27 -3
  63. package/dist/esm/utils/columnFilters.js.map +1 -1
  64. package/dist/esm/utils/schema.js +40 -2
  65. package/dist/esm/utils/schema.js.map +1 -1
  66. package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
  67. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  68. package/dist/nextcloud-vue.cjs.js +2377 -1247
  69. package/dist/nextcloud-vue.cjs.js.map +2 -2
  70. package/dist/nextcloud-vue.css +38 -2
  71. package/l10n/en.json +7 -1
  72. package/l10n/nl.json +7 -1
  73. package/package.json +1 -1
  74. package/src/components/CnActionsBar/CnActionsBar.md +2 -1
  75. package/src/components/CnActionsBar/CnActionsBar.vue +25 -1
  76. package/src/components/CnAppNav/CnAppNav.vue +2 -2
  77. package/src/components/CnDashboardPage/CnDashboardPage.vue +165 -8
  78. package/src/components/CnDataTable/CnColumnFilterPopover.vue +11 -5
  79. package/src/components/CnDataTable/CnDataTable.md +1 -1
  80. package/src/components/CnDataTable/CnDataTable.vue +4 -4
  81. package/src/components/CnDetailPage/CnDetailPage.vue +94 -5
  82. package/src/components/CnFkResolveCell/CnFkResolveCell.vue +3 -1
  83. package/src/components/CnFormDialog/CnFormDialog.vue +144 -4
  84. package/src/components/CnHeaderWidget/CnHeaderWidget.vue +67 -8
  85. package/src/components/CnIndexPage/CnIndexPage.md +2 -1
  86. package/src/components/CnIndexPage/CnIndexPage.vue +47 -1
  87. package/src/components/CnSegmentedControl/CnSegmentedControl.md +1 -1
  88. package/src/components/CnSegmentedControl/CnSegmentedControl.vue +12 -0
  89. package/src/components/CnSetupWizard/CnSetupWizard.vue +5 -11
  90. package/src/composables/useDependencyCheck.js +43 -0
  91. package/src/composables/useListView.js +43 -1
  92. package/src/composables/useSetupStatus.js +26 -3
  93. package/src/css/actions-bar.css +20 -2
  94. package/src/css/detail-page.css +5 -0
  95. package/src/mixins/pageViews.js +396 -0
  96. package/src/schemas/app-manifest-v2.schema.json +31 -3
  97. package/src/schemas/app-manifest.schema.json +1 -1
  98. package/src/store/plugins/liveUpdates.js +19 -9
  99. package/src/store/useObjectStore.js +19 -8
  100. package/src/utils/actionsDispatcher.js +11 -2
  101. package/src/utils/columnFilters.js +29 -2
  102. package/src/utils/schema.js +40 -2
  103. package/src/utils/validateManifestV2.compiled.js +2 -2
@@ -1,4 +1,5 @@
1
1
  import { ref, computed } from 'vue';
2
+ import { readServerAppStatuses, missingRequiredApps } from './useDependencyCheck.js';
2
3
 
3
4
  /**
4
5
  * SPDX-FileCopyrightText: 2026 Conduction B.V.
@@ -28,6 +29,14 @@ const cache = new Map();
28
29
  * every non-admin user of an app with a `setup` block met a wizard they had no
29
30
  * permission to complete instead of the app itself.
30
31
  *
32
+ * A step that declares `requires: [appId, ...]` is **not applicable** while any
33
+ * of those apps is not installed and enabled: it is neither met nor unmet, so
34
+ * it never lands in `requiredUnmet` or `optionalUnmet`. `CnSetupWizard` skips
35
+ * such a step, and counting it as unmet kept the wizard reopening (optional
36
+ * step) or gating the app (required step) for a step it would never show. App
37
+ * status is resolved exactly as the wizard does it: the `dependency_statuses`
38
+ * initial state first, then `useAppStatus`.
39
+ *
31
40
  * @param {string} appId Nextcloud app id (e.g. `"procest"`).
32
41
  * @param {object} manifest The app manifest (reads `manifest.setup`).
33
42
  * @return {{
@@ -36,6 +45,7 @@ const cache = new Map();
36
45
  * requiredUnmet: import('vue').ComputedRef<Array<object>>,
37
46
  * optionalUnmet: import('vue').ComputedRef<Array<object>>,
38
47
  * optionalUnmetReported: import('vue').ComputedRef<Array<object>>,
48
+ * notApplicable: import('vue').ComputedRef<Array<object>>,
39
49
  * completed: import('vue').ComputedRef<boolean>,
40
50
  * enabled: boolean,
41
51
  * loading: import('vue').Ref<boolean>,
@@ -46,6 +56,7 @@ const cache = new Map();
46
56
  *
47
57
  * @example
48
58
  * const { requiredUnmet, completed, loading } = useSetupStatus('procest', manifest)
59
+ * @spec openspec/changes/optional-step-requires/specs/cn-setup-wizard/spec.md#requirement-a-step-whose-required-apps-are-absent-is-not-applicable
49
60
  */
50
61
  function useSetupStatus(appId, manifest) {
51
62
  const setup = (manifest && manifest.setup) || {};
@@ -69,6 +80,9 @@ function useSetupStatus(appId, manifest) {
69
80
  cache.set(appId, entry);
70
81
  }
71
82
  const { status, loading, error, forbidden } = entry;
83
+ // Same source the wizard's dependency gate reads. Initial state is fixed
84
+ // for the page lifetime, so one read per call is enough.
85
+ const serverAppStatuses = readServerAppStatuses(appId);
72
86
 
73
87
  const steps = computed(() => stepDefs.map((s) => {
74
88
  const bag = status.value.steps || {};
@@ -82,7 +96,12 @@ function useSetupStatus(appId, manifest) {
82
96
  // that act on an unmet step — CnAppRoot auto-opening the non-gating
83
97
  // wizard — must act only on steps the server actually reported, or an
84
98
  // unknowable step parks the wizard over the app for ever.
85
- return { ...s, done: st.done === true, reported, detail: st.detail }
99
+ //
100
+ // `applicable` is false when the step `requires` an app that is not
101
+ // installed and enabled. The wizard skips such a step, so it can never
102
+ // be done here; it is not applicable rather than unmet.
103
+ const missingApps = missingRequiredApps(s.requires, serverAppStatuses).map((row) => row.id);
104
+ return { ...s, done: st.done === true, reported, detail: st.detail, applicable: missingApps.length === 0, missingApps }
86
105
  }));
87
106
  // Presentational step types carry no work, so the server never reports a
88
107
  // `done` flag for them. Counting them as "unmet" made `optionalUnmet`
@@ -90,7 +109,9 @@ function useSetupStatus(appId, manifest) {
90
109
  // which kept CnAppRoot's non-gating setup wizard auto-opening over the
91
110
  // app on every fresh browser profile no matter how complete setup was.
92
111
  // Only actionable steps can be unmet.
93
- const isActionable = (s) => s.type !== 'info' && s.type !== 'summary';
112
+ // A step whose required apps are absent is skipped by the wizard, so it
113
+ // is neither met nor unmet (see `applicable` above).
114
+ const isActionable = (s) => s.type !== 'info' && s.type !== 'summary' && s.applicable !== false;
94
115
  // `forbidden` empties BOTH lists, and that is what actually suppresses the
95
116
  // wizard: CnAppRoot gates on `requiredUnmet.length > 0` (blocking) and on
96
117
  // `requiredUnmet.length === 0 && optionalUnmet.length > 0` (auto-open) —
@@ -111,6 +132,8 @@ function useSetupStatus(appId, manifest) {
111
132
  // suppressing it is what left dossiq's `seed` step (and with it the app's
112
133
  // only demo-data affordance) permanently unreachable.
113
134
  const optionalUnmetReported = computed(() => optionalUnmet.value.filter((s) => s.reported === true));
135
+ // Steps the wizard skips because an app in their `requires` is absent.
136
+ const notApplicable = computed(() => steps.value.filter((s) => s.applicable === false));
114
137
  const completed = computed(() => {
115
138
  if (!enabled) {
116
139
  return true
@@ -171,7 +194,7 @@ function useSetupStatus(appId, manifest) {
171
194
  refresh();
172
195
  }
173
196
 
174
- return { steps, status, requiredUnmet, optionalUnmet, optionalUnmetReported, completed, enabled, loading, error, forbidden, refresh }
197
+ return { steps, status, requiredUnmet, optionalUnmet, optionalUnmetReported, notApplicable, completed, enabled, loading, error, forbidden, refresh }
175
198
  }
176
199
 
177
200
  export { useSetupStatus };
@@ -1 +1 @@
1
- {"version":3,"file":"useSetupStatus.js","sources":["../../../src/composables/useSetupStatus.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V.\n * SPDX-License-Identifier: EUPL-1.2\n */\n\nimport { computed, ref } from 'vue'\n\n/**\n * Per-`appId` cache of setup-status results. The Vue refs are stored so all\n * consumers of the same app share one fetch + reactive state for the page\n * lifetime (mirrors useAppStatus).\n */\nconst cache = new Map()\n\n/**\n * Composable that reports an app's first-time-setup status (ADR-042).\n *\n * Fetches `GET /apps/{appId}/api/setup/status` → `{ version, completed, steps:\n * { <id>: { done, detail } } }`, then cross-references the manifest's\n * `setup.steps[].required` flags so `CnAppRoot` can gate on required-unmet and\n * the wizard can drive itself. On error it falls back to \"nothing done\" so a\n * failed lookup never crashes the shell — the wizard simply shows the steps.\n *\n * One error is treated as an answer rather than a failure: a **401/403** means\n * the caller may not read setup state (setup endpoints are admin-only), so\n * `completed` reports `true` and the wizard stays out of the way. Without that,\n * every non-admin user of an app with a `setup` block met a wizard they had no\n * permission to complete instead of the app itself.\n *\n * @param {string} appId Nextcloud app id (e.g. `\"procest\"`).\n * @param {object} manifest The app manifest (reads `manifest.setup`).\n * @return {{\n * steps: import('vue').ComputedRef<Array<object>>,\n * status: import('vue').Ref<object>,\n * requiredUnmet: import('vue').ComputedRef<Array<object>>,\n * optionalUnmet: import('vue').ComputedRef<Array<object>>,\n * optionalUnmetReported: import('vue').ComputedRef<Array<object>>,\n * completed: import('vue').ComputedRef<boolean>,\n * enabled: boolean,\n * loading: import('vue').Ref<boolean>,\n * error: import('vue').Ref<Error|null>,\n * forbidden: import('vue').Ref<boolean>,\n * refresh: () => Promise<void>\n * }}\n *\n * @example\n * const { requiredUnmet, completed, loading } = useSetupStatus('procest', manifest)\n */\nexport function useSetupStatus(appId, manifest) {\n\tconst setup = (manifest && manifest.setup) || {}\n\tconst enabled = setup.enabled !== false && Array.isArray(setup.steps) && setup.steps.length > 0\n\tconst stepDefs = Array.isArray(setup.steps) ? setup.steps : []\n\tconst requiredById = Object.create(null)\n\tfor (const s of stepDefs) {\n\t\trequiredById[s.id] = s.required === true\n\t}\n\n\tlet entry = cache.get(appId)\n\tif (!entry) {\n\t\tentry = {\n\t\t\tstatus: ref({ version: null, completed: false, steps: {} }),\n\t\t\tloading: ref(enabled),\n\t\t\terror: ref(null),\n\t\t\t// Set when the server answers 401/403 — see `completed` below.\n\t\t\tforbidden: ref(false),\n\t\t\tfetched: false,\n\t\t}\n\t\tcache.set(appId, entry)\n\t}\n\tconst { status, loading, error, forbidden } = entry\n\n\tconst steps = computed(() => stepDefs.map((s) => {\n\t\tconst bag = status.value.steps || {}\n\t\tconst reported = Object.hasOwn(bag, s.id)\n\t\tconst st = bag[s.id] || {}\n\t\t// `reported` separates \"the server says this step is NOT done\" from \"the\n\t\t// server never mentioned this step at all\". Both used to flatten into\n\t\t// `done: false`, and that conflation is load-bearing: a manifest may\n\t\t// declare an optional step its SetupController has no status key for\n\t\t// (pipelinq's `demo-data`), which is UNKNOWN, not outstanding. Callers\n\t\t// that act on an unmet step — CnAppRoot auto-opening the non-gating\n\t\t// wizard — must act only on steps the server actually reported, or an\n\t\t// unknowable step parks the wizard over the app for ever.\n\t\treturn { ...s, done: st.done === true, reported, detail: st.detail }\n\t}))\n\t// Presentational step types carry no work, so the server never reports a\n\t// `done` flag for them. Counting them as \"unmet\" made `optionalUnmet`\n\t// permanently non-empty for any manifest with a welcome/summary step,\n\t// which kept CnAppRoot's non-gating setup wizard auto-opening over the\n\t// app on every fresh browser profile no matter how complete setup was.\n\t// Only actionable steps can be unmet.\n\tconst isActionable = (s) => s.type !== 'info' && s.type !== 'summary'\n\t// `forbidden` empties BOTH lists, and that is what actually suppresses the\n\t// wizard: CnAppRoot gates on `requiredUnmet.length > 0` (blocking) and on\n\t// `requiredUnmet.length === 0 && optionalUnmet.length > 0` (auto-open) —\n\t// it never reads `completed`. A caller who may not READ setup state has no\n\t// unmet setup work *of their own*, so reporting steps as unmet to them\n\t// described work they could neither see nor do.\n\tconst requiredUnmet = computed(() => (forbidden.value === true\n\t\t? []\n\t\t: steps.value.filter((s) => isActionable(s) && requiredById[s.id] && !s.done)))\n\tconst optionalUnmet = computed(() => (forbidden.value === true\n\t\t? []\n\t\t: steps.value.filter((s) => isActionable(s) && !requiredById[s.id] && !s.done)))\n\t// The subset of `optionalUnmet` the server positively reported as not done.\n\t// This — not `optionalUnmet` — is what may auto-open the non-gating wizard:\n\t// a step with no status key is unknown, and prompting on unknown is what\n\t// parked the wizard over pipelinq on every fresh browser profile. A step\n\t// the server DID report as `{done: false}` is genuinely outstanding, and\n\t// suppressing it is what left dossiq's `seed` step (and with it the app's\n\t// only demo-data affordance) permanently unreachable.\n\tconst optionalUnmetReported = computed(() => optionalUnmet.value.filter((s) => s.reported === true))\n\tconst completed = computed(() => {\n\t\tif (!enabled) {\n\t\t\treturn true\n\t\t}\n\t\t// A 401/403 means the caller may not READ setup state — first-time setup\n\t\t// is admin-only. That is not \"setup is unfinished\"; it is \"not this\n\t\t// user's concern\". Reporting incomplete here put every non-admin in\n\t\t// front of a setup wizard they cannot complete, INSTEAD of the app:\n\t\t// `/api/setup/status` answers 200 {completed:true} to an admin and 403\n\t\t// to everyone else, and the generic error path below then left status at\n\t\t// its \"nothing done\" default. Measured on openbuild, where it made the\n\t\t// app unusable for every non-admin as soon as they could see it at all.\n\t\tif (forbidden.value === true) {\n\t\t\treturn true\n\t\t}\n\t\t// Authoritative server flag, but never report complete while a required\n\t\t// step is still unmet (defends against a stale completion flag).\n\t\treturn status.value.completed === true && requiredUnmet.value.length === 0\n\t})\n\n\t/**\n\t * (Re)fetch the setup status from the server.\n\t *\n\t * @return {Promise<void>}\n\t */\n\tasync function refresh() {\n\t\tif (!enabled) {\n\t\t\tloading.value = false\n\t\t\tentry.fetched = true\n\t\t\treturn\n\t\t}\n\t\tloading.value = true\n\t\terror.value = null\n\t\ttry {\n\t\t\tconst [{ default: axios }, { generateUrl }] = await Promise.all([\n\t\t\t\timport('@nextcloud/axios'),\n\t\t\t\timport('@nextcloud/router'),\n\t\t\t])\n\t\t\tconst { data } = await axios.get(generateUrl(`/apps/${appId}/api/setup/status`))\n\t\t\tstatus.value = (data && typeof data === 'object') ? data : { steps: {} }\n\t\t\tforbidden.value = false\n\t\t} catch (err) {\n\t\t\terror.value = err\n\t\t\t// 401/403 is a DIFFERENT outcome from a failed lookup: the server\n\t\t\t// answered, and its answer was \"you may not see this\". Setup is\n\t\t\t// admin-only, so for everyone else it is settled, not unknown.\n\t\t\t// Anything else (network error, 500) stays unknown and falls back to\n\t\t\t// \"nothing done\" so the wizard is still reachable for an admin.\n\t\t\tconst httpStatus = err && err.response && err.response.status\n\t\t\tforbidden.value = (httpStatus === 401 || httpStatus === 403)\n\t\t} finally {\n\t\t\tloading.value = false\n\t\t\tentry.fetched = true\n\t\t}\n\t}\n\n\tif (!entry.fetched) {\n\t\trefresh()\n\t}\n\n\treturn { steps, status, requiredUnmet, optionalUnmet, optionalUnmetReported, completed, enabled, loading, error, forbidden, refresh }\n}\n\n/**\n * Test-only helper to reset the per-`appId` cache.\n *\n * @internal\n */\nexport function __resetSetupStatusCacheForTests() {\n\tcache.clear()\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"useSetupStatus.js","sources":["../../../src/composables/useSetupStatus.js"],"sourcesContent":["/**\n * SPDX-FileCopyrightText: 2026 Conduction B.V.\n * SPDX-License-Identifier: EUPL-1.2\n */\n\nimport { computed, ref } from 'vue'\nimport { missingRequiredApps, readServerAppStatuses } from './useDependencyCheck.js'\n\n/**\n * Per-`appId` cache of setup-status results. The Vue refs are stored so all\n * consumers of the same app share one fetch + reactive state for the page\n * lifetime (mirrors useAppStatus).\n */\nconst cache = new Map()\n\n/**\n * Composable that reports an app's first-time-setup status (ADR-042).\n *\n * Fetches `GET /apps/{appId}/api/setup/status` → `{ version, completed, steps:\n * { <id>: { done, detail } } }`, then cross-references the manifest's\n * `setup.steps[].required` flags so `CnAppRoot` can gate on required-unmet and\n * the wizard can drive itself. On error it falls back to \"nothing done\" so a\n * failed lookup never crashes the shell — the wizard simply shows the steps.\n *\n * One error is treated as an answer rather than a failure: a **401/403** means\n * the caller may not read setup state (setup endpoints are admin-only), so\n * `completed` reports `true` and the wizard stays out of the way. Without that,\n * every non-admin user of an app with a `setup` block met a wizard they had no\n * permission to complete instead of the app itself.\n *\n * A step that declares `requires: [appId, ...]` is **not applicable** while any\n * of those apps is not installed and enabled: it is neither met nor unmet, so\n * it never lands in `requiredUnmet` or `optionalUnmet`. `CnSetupWizard` skips\n * such a step, and counting it as unmet kept the wizard reopening (optional\n * step) or gating the app (required step) for a step it would never show. App\n * status is resolved exactly as the wizard does it: the `dependency_statuses`\n * initial state first, then `useAppStatus`.\n *\n * @param {string} appId Nextcloud app id (e.g. `\"procest\"`).\n * @param {object} manifest The app manifest (reads `manifest.setup`).\n * @return {{\n * steps: import('vue').ComputedRef<Array<object>>,\n * status: import('vue').Ref<object>,\n * requiredUnmet: import('vue').ComputedRef<Array<object>>,\n * optionalUnmet: import('vue').ComputedRef<Array<object>>,\n * optionalUnmetReported: import('vue').ComputedRef<Array<object>>,\n * notApplicable: import('vue').ComputedRef<Array<object>>,\n * completed: import('vue').ComputedRef<boolean>,\n * enabled: boolean,\n * loading: import('vue').Ref<boolean>,\n * error: import('vue').Ref<Error|null>,\n * forbidden: import('vue').Ref<boolean>,\n * refresh: () => Promise<void>\n * }}\n *\n * @example\n * const { requiredUnmet, completed, loading } = useSetupStatus('procest', manifest)\n * @spec openspec/changes/optional-step-requires/specs/cn-setup-wizard/spec.md#requirement-a-step-whose-required-apps-are-absent-is-not-applicable\n */\nexport function useSetupStatus(appId, manifest) {\n\tconst setup = (manifest && manifest.setup) || {}\n\tconst enabled = setup.enabled !== false && Array.isArray(setup.steps) && setup.steps.length > 0\n\tconst stepDefs = Array.isArray(setup.steps) ? setup.steps : []\n\tconst requiredById = Object.create(null)\n\tfor (const s of stepDefs) {\n\t\trequiredById[s.id] = s.required === true\n\t}\n\n\tlet entry = cache.get(appId)\n\tif (!entry) {\n\t\tentry = {\n\t\t\tstatus: ref({ version: null, completed: false, steps: {} }),\n\t\t\tloading: ref(enabled),\n\t\t\terror: ref(null),\n\t\t\t// Set when the server answers 401/403 — see `completed` below.\n\t\t\tforbidden: ref(false),\n\t\t\tfetched: false,\n\t\t}\n\t\tcache.set(appId, entry)\n\t}\n\tconst { status, loading, error, forbidden } = entry\n\t// Same source the wizard's dependency gate reads. Initial state is fixed\n\t// for the page lifetime, so one read per call is enough.\n\tconst serverAppStatuses = readServerAppStatuses(appId)\n\n\tconst steps = computed(() => stepDefs.map((s) => {\n\t\tconst bag = status.value.steps || {}\n\t\tconst reported = Object.hasOwn(bag, s.id)\n\t\tconst st = bag[s.id] || {}\n\t\t// `reported` separates \"the server says this step is NOT done\" from \"the\n\t\t// server never mentioned this step at all\". Both used to flatten into\n\t\t// `done: false`, and that conflation is load-bearing: a manifest may\n\t\t// declare an optional step its SetupController has no status key for\n\t\t// (pipelinq's `demo-data`), which is UNKNOWN, not outstanding. Callers\n\t\t// that act on an unmet step — CnAppRoot auto-opening the non-gating\n\t\t// wizard — must act only on steps the server actually reported, or an\n\t\t// unknowable step parks the wizard over the app for ever.\n\t\t//\n\t\t// `applicable` is false when the step `requires` an app that is not\n\t\t// installed and enabled. The wizard skips such a step, so it can never\n\t\t// be done here; it is not applicable rather than unmet.\n\t\tconst missingApps = missingRequiredApps(s.requires, serverAppStatuses).map((row) => row.id)\n\t\treturn { ...s, done: st.done === true, reported, detail: st.detail, applicable: missingApps.length === 0, missingApps }\n\t}))\n\t// Presentational step types carry no work, so the server never reports a\n\t// `done` flag for them. Counting them as \"unmet\" made `optionalUnmet`\n\t// permanently non-empty for any manifest with a welcome/summary step,\n\t// which kept CnAppRoot's non-gating setup wizard auto-opening over the\n\t// app on every fresh browser profile no matter how complete setup was.\n\t// Only actionable steps can be unmet.\n\t// A step whose required apps are absent is skipped by the wizard, so it\n\t// is neither met nor unmet (see `applicable` above).\n\tconst isActionable = (s) => s.type !== 'info' && s.type !== 'summary' && s.applicable !== false\n\t// `forbidden` empties BOTH lists, and that is what actually suppresses the\n\t// wizard: CnAppRoot gates on `requiredUnmet.length > 0` (blocking) and on\n\t// `requiredUnmet.length === 0 && optionalUnmet.length > 0` (auto-open) —\n\t// it never reads `completed`. A caller who may not READ setup state has no\n\t// unmet setup work *of their own*, so reporting steps as unmet to them\n\t// described work they could neither see nor do.\n\tconst requiredUnmet = computed(() => (forbidden.value === true\n\t\t? []\n\t\t: steps.value.filter((s) => isActionable(s) && requiredById[s.id] && !s.done)))\n\tconst optionalUnmet = computed(() => (forbidden.value === true\n\t\t? []\n\t\t: steps.value.filter((s) => isActionable(s) && !requiredById[s.id] && !s.done)))\n\t// The subset of `optionalUnmet` the server positively reported as not done.\n\t// This — not `optionalUnmet` — is what may auto-open the non-gating wizard:\n\t// a step with no status key is unknown, and prompting on unknown is what\n\t// parked the wizard over pipelinq on every fresh browser profile. A step\n\t// the server DID report as `{done: false}` is genuinely outstanding, and\n\t// suppressing it is what left dossiq's `seed` step (and with it the app's\n\t// only demo-data affordance) permanently unreachable.\n\tconst optionalUnmetReported = computed(() => optionalUnmet.value.filter((s) => s.reported === true))\n\t// Steps the wizard skips because an app in their `requires` is absent.\n\tconst notApplicable = computed(() => steps.value.filter((s) => s.applicable === false))\n\tconst completed = computed(() => {\n\t\tif (!enabled) {\n\t\t\treturn true\n\t\t}\n\t\t// A 401/403 means the caller may not READ setup state — first-time setup\n\t\t// is admin-only. That is not \"setup is unfinished\"; it is \"not this\n\t\t// user's concern\". Reporting incomplete here put every non-admin in\n\t\t// front of a setup wizard they cannot complete, INSTEAD of the app:\n\t\t// `/api/setup/status` answers 200 {completed:true} to an admin and 403\n\t\t// to everyone else, and the generic error path below then left status at\n\t\t// its \"nothing done\" default. Measured on openbuild, where it made the\n\t\t// app unusable for every non-admin as soon as they could see it at all.\n\t\tif (forbidden.value === true) {\n\t\t\treturn true\n\t\t}\n\t\t// Authoritative server flag, but never report complete while a required\n\t\t// step is still unmet (defends against a stale completion flag).\n\t\treturn status.value.completed === true && requiredUnmet.value.length === 0\n\t})\n\n\t/**\n\t * (Re)fetch the setup status from the server.\n\t *\n\t * @return {Promise<void>}\n\t */\n\tasync function refresh() {\n\t\tif (!enabled) {\n\t\t\tloading.value = false\n\t\t\tentry.fetched = true\n\t\t\treturn\n\t\t}\n\t\tloading.value = true\n\t\terror.value = null\n\t\ttry {\n\t\t\tconst [{ default: axios }, { generateUrl }] = await Promise.all([\n\t\t\t\timport('@nextcloud/axios'),\n\t\t\t\timport('@nextcloud/router'),\n\t\t\t])\n\t\t\tconst { data } = await axios.get(generateUrl(`/apps/${appId}/api/setup/status`))\n\t\t\tstatus.value = (data && typeof data === 'object') ? data : { steps: {} }\n\t\t\tforbidden.value = false\n\t\t} catch (err) {\n\t\t\terror.value = err\n\t\t\t// 401/403 is a DIFFERENT outcome from a failed lookup: the server\n\t\t\t// answered, and its answer was \"you may not see this\". Setup is\n\t\t\t// admin-only, so for everyone else it is settled, not unknown.\n\t\t\t// Anything else (network error, 500) stays unknown and falls back to\n\t\t\t// \"nothing done\" so the wizard is still reachable for an admin.\n\t\t\tconst httpStatus = err && err.response && err.response.status\n\t\t\tforbidden.value = (httpStatus === 401 || httpStatus === 403)\n\t\t} finally {\n\t\t\tloading.value = false\n\t\t\tentry.fetched = true\n\t\t}\n\t}\n\n\tif (!entry.fetched) {\n\t\trefresh()\n\t}\n\n\treturn { steps, status, requiredUnmet, optionalUnmet, optionalUnmetReported, notApplicable, completed, enabled, loading, error, forbidden, refresh }\n}\n\n/**\n * Test-only helper to reset the per-`appId` cache.\n *\n * @internal\n */\nexport function __resetSetupStatusCacheForTests() {\n\tcache.clear()\n}\n"],"names":[],"mappings":"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"}
@@ -1,4 +1,5 @@
1
1
  var translations = {
2
+ Advanced: "Advanced",
2
3
  "The item could not be created.": "The item could not be created.",
3
4
  "Start over": "Start over",
4
5
  "Continue where you left off": "Continue where you left off",
@@ -48,6 +49,7 @@ var translations = {
48
49
  "This integration is not available.": "This integration is not available.",
49
50
  Integrations: "Integrations",
50
51
  View: "View",
52
+ Views: "Views",
51
53
  "current step": "current step",
52
54
  "{app} not available": "{app} not available",
53
55
  "{app} not configured": "{app} not configured",
@@ -404,6 +406,7 @@ var translations = {
404
406
  Any: "Any",
405
407
  To: "To",
406
408
  Equals: "Equals",
409
+ Contains: "Contains",
407
410
  Clear: "Clear",
408
411
  Apply: "Apply",
409
412
  "This column cannot list its objects.": "This column cannot list its objects.",
@@ -1072,6 +1075,7 @@ var translations = {
1072
1075
  "The actions of an attention card are declared in the manifest.": "The actions of an attention card are declared in the manifest.",
1073
1076
  "An attention card needs a title": "An attention card needs a title",
1074
1077
  "Good morning, {name}": "Good morning, {name}",
1078
+ "This view has no widgets yet.": "This view has no widgets yet.",
1075
1079
  "Good morning": "Good morning",
1076
1080
  "Good afternoon, {name}": "Good afternoon, {name}",
1077
1081
  "Good afternoon": "Good afternoon",
@@ -1081,7 +1085,9 @@ var translations = {
1081
1085
  "Show the date of today above the heading": "Show the date of today above the heading",
1082
1086
  "Plain header without a coloured background": "Plain header without a coloured background",
1083
1087
  "Loading…": "Loading…",
1084
- "A register and schema are required": "A register and schema are required"
1088
+ "A register and schema are required": "A register and schema are required",
1089
+ "Change the search or try again.": "Change the search or try again.",
1090
+ "Try again later.": "Try again later."
1085
1091
  };
1086
1092
  var plurals = {
1087
1093
  "%n year": [
@@ -1 +1 @@
1
- {"version":3,"file":"en.json.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"en.json.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -1,4 +1,5 @@
1
1
  var translations = {
2
+ Advanced: "Geavanceerd",
2
3
  "The item could not be created.": "Het item kon niet worden aangemaakt.",
3
4
  "Start over": "Opnieuw beginnen",
4
5
  "Continue where you left off": "Ga verder waar je was",
@@ -48,6 +49,7 @@ var translations = {
48
49
  "This integration is not available.": "Deze integratie is niet beschikbaar.",
49
50
  Integrations: "Integraties",
50
51
  View: "Bekijken",
52
+ Views: "Weergaven",
51
53
  "current step": "huidige stap",
52
54
  "{app} not available": "{app} niet beschikbaar",
53
55
  "{app} not configured": "{app} niet geconfigureerd",
@@ -404,6 +406,7 @@ var translations = {
404
406
  Any: "Alle",
405
407
  To: "Tot",
406
408
  Equals: "Is gelijk aan",
409
+ Contains: "Bevat",
407
410
  Clear: "Wissen",
408
411
  Apply: "Toepassen",
409
412
  "This column cannot list its objects.": "Deze kolom kan zijn objecten niet tonen.",
@@ -1124,6 +1127,7 @@ var translations = {
1124
1127
  "The actions of an attention card are declared in the manifest.": "De acties van een aandachtskaart staan in het manifest.",
1125
1128
  "An attention card needs a title": "Een aandachtskaart heeft een titel nodig",
1126
1129
  "Good morning, {name}": "Goedemorgen, {name}",
1130
+ "This view has no widgets yet.": "Deze weergave heeft nog geen widgets.",
1127
1131
  "Good morning": "Goedemorgen",
1128
1132
  "Good afternoon, {name}": "Goedemiddag, {name}",
1129
1133
  "Good afternoon": "Goedemiddag",
@@ -1133,7 +1137,9 @@ var translations = {
1133
1137
  "Show the date of today above the heading": "Toon de datum van vandaag boven de kop",
1134
1138
  "Plain header without a coloured background": "Eenvoudige kop zonder gekleurde achtergrond",
1135
1139
  "Loading…": "Laden…",
1136
- "A register and schema are required": "Een register en schema zijn verplicht"
1140
+ "A register and schema are required": "Een register en schema zijn verplicht",
1141
+ "Change the search or try again.": "Pas de zoekopdracht aan of probeer het opnieuw.",
1142
+ "Try again later.": "Probeer het later opnieuw."
1137
1143
  };
1138
1144
  var plurals = {
1139
1145
  "%n year": [
@@ -1 +1 @@
1
- {"version":3,"file":"nl.json.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"nl.json.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
@@ -0,0 +1,400 @@
1
+ import { translate } from '@nextcloud/l10n';
2
+ import { readUserPreference, writeUserPreference, USER_PREFERENCE_STORAGE_PREFIX } from '../composables/useUserPreferences.js';
3
+
4
+ /**
5
+ * SPDX-FileCopyrightText: 2026 Conduction B.V.
6
+ * SPDX-License-Identifier: EUPL-1.2
7
+ *
8
+ * Page views: a few views of one page, each its own widget grid, behind one
9
+ * segmented control ("My work | My team").
10
+ *
11
+ * A page that mixes this in accepts `views` (each `{ id, label, icon?,
12
+ * widgets, layout, emptyText? }`), `defaultView` and `viewsLabel`. It keeps
13
+ * the chosen view, writes it to the address (`?view=<id>`) and to the
14
+ * user's preferences (the server, mirrored in the browser), and provides `cnPageViews` so a greeting header widget on
15
+ * the page can draw the switch instead of the page.
16
+ *
17
+ * Which view opens: the address, then the stored view, then `defaultView`,
18
+ * then the first view. An id that names no view is ignored at every step.
19
+ *
20
+ * Used by CnDashboardPage and CnDetailPage. Each renders the chosen view's
21
+ * `layout` with its own grid, so a view looks exactly like the page's own
22
+ * grid.
23
+ *
24
+ * @mixin pageViews
25
+ * @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-a-page-declares-views-that-each-hold-a-widget-grid
26
+ */
27
+
28
+ /** The query parameter that carries the chosen view. */
29
+ const PAGE_VIEW_QUERY_KEY = 'view';
30
+
31
+ /** Prefix of the user-preference key; the page id follows it. */
32
+ const PAGE_VIEW_PREFERENCE_PREFIX = 'cn_page_view:';
33
+
34
+ let regionSeq = 0;
35
+
36
+ /**
37
+ * Read the browser mirror of a user preference synchronously, so the stored
38
+ * view is on screen from the first frame. Errors read as nothing stored.
39
+ *
40
+ * @param {string} appId The app id.
41
+ * @param {string} key The preference key.
42
+ * @return {string|null} The stored id, or null.
43
+ * @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-the-chosen-view-is-linkable-and-remembered
44
+ */
45
+ function readStoredViewMirror(appId, key) {
46
+ try {
47
+ if (typeof localStorage === 'undefined') {
48
+ return null
49
+ }
50
+ const raw = localStorage.getItem(`${USER_PREFERENCE_STORAGE_PREFIX}${appId}:${key}`);
51
+ if (raw === null || raw === undefined) {
52
+ return null
53
+ }
54
+ const value = JSON.parse(raw);
55
+ return typeof value === 'string' && value !== '' ? value : null
56
+ } catch {
57
+ return null
58
+ }
59
+ }
60
+
61
+ const pageViews = {
62
+ inject: {
63
+ /** The host app id, provided by CnAppRoot; keys the stored view. */
64
+ cnPageViewsAppId: { from: 'cnAppId', default: '' },
65
+ },
66
+
67
+ props: {
68
+ /**
69
+ * Views of this page, each its own widget grid behind one segmented
70
+ * control. Each is `{ id, label, icon?, widgets, layout, emptyText? }`:
71
+ * `widgets` and `layout` have the same shape as the page's own. The
72
+ * chosen view renders in a region below the page's own grid. Empty
73
+ * (the default) draws no switch and no region.
74
+ *
75
+ * @type {Array<{id: string, label: string, icon?: string, widgets?: Array<object>, layout?: Array<object>, emptyText?: string}>}
76
+ */
77
+ views: {
78
+ type: Array,
79
+ default: () => [],
80
+ },
81
+
82
+ /**
83
+ * The id of the view chosen when neither the address nor the browser
84
+ * store names one. Empty chooses the first view.
85
+ *
86
+ * @type {string}
87
+ */
88
+ defaultView: {
89
+ type: String,
90
+ default: '',
91
+ },
92
+
93
+ /**
94
+ * Accessible name of the view switch (through the host translate
95
+ * function). Empty reads "View".
96
+ *
97
+ * @type {string}
98
+ */
99
+ viewsLabel: {
100
+ type: String,
101
+ default: '',
102
+ },
103
+ },
104
+
105
+ data() {
106
+ regionSeq += 1;
107
+ return {
108
+ /** The view the user chose in this session, or the stored one. */
109
+ selectedViewId: null,
110
+ /** Whether the user chose a view in this session (a late server read then leaves it alone). */
111
+ viewChosen: false,
112
+ /** Id of the view region, the target of the options' `aria-controls`. */
113
+ viewRegionId: `cn-page-view-region-${regionSeq}`,
114
+ }
115
+ },
116
+
117
+ /**
118
+ * Provide the views to descendants, so a greeting header widget in the
119
+ * page's grid can draw the switch. Getters keep it reactive.
120
+ *
121
+ * @return {object} The provided `cnPageViews`.
122
+ */
123
+ provide() {
124
+ const vm = this;
125
+ return {
126
+ cnPageViews: {
127
+ get views() { return vm.normalizedViews },
128
+ get activeId() { return vm.activeViewId },
129
+ get regionId() { return vm.viewRegionId },
130
+ select: (id) => vm.selectView(id),
131
+ },
132
+ }
133
+ },
134
+
135
+ computed: {
136
+ /**
137
+ * The declared views in one shape, labels translated. Entries without
138
+ * an id or a label are dropped, as are repeated ids.
139
+ *
140
+ * @return {Array<{id: string, label: string, icon: string, widgets: Array<object>, layout: Array<object>, emptyText: string}>}
141
+ * @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-a-page-declares-views-that-each-hold-a-widget-grid
142
+ */
143
+ normalizedViews() {
144
+ const translate = typeof this.cnTranslate === 'function' ? this.cnTranslate : (key) => key;
145
+ const seen = new Set();
146
+ return (Array.isArray(this.views) ? this.views : [])
147
+ .filter((view) => {
148
+ if (!view || typeof view !== 'object' || view.id === undefined || view.id === null || view.id === '') {
149
+ return false
150
+ }
151
+ if (typeof view.label !== 'string' || view.label === '' || seen.has(String(view.id))) {
152
+ return false
153
+ }
154
+ seen.add(String(view.id));
155
+ return true
156
+ })
157
+ .map((view) => ({
158
+ id: String(view.id),
159
+ label: translate(view.label),
160
+ icon: typeof view.icon === 'string' ? view.icon : '',
161
+ widgets: Array.isArray(view.widgets) ? view.widgets : [],
162
+ layout: Array.isArray(view.layout) ? view.layout : [],
163
+ emptyText: typeof view.emptyText === 'string' && view.emptyText !== '' ? translate(view.emptyText) : '',
164
+ }))
165
+ },
166
+
167
+ /**
168
+ * Whether this page has views.
169
+ *
170
+ * @return {boolean}
171
+ */
172
+ hasViews() {
173
+ return this.normalizedViews.length > 0
174
+ },
175
+
176
+ /**
177
+ * The chosen view's id: the address, then this session's or the
178
+ * stored choice, then `defaultView`, then the first view.
179
+ *
180
+ * @return {string|null} The id, or null without views.
181
+ * @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-the-chosen-view-is-linkable-and-remembered
182
+ */
183
+ activeViewId() {
184
+ const views = this.normalizedViews;
185
+ if (views.length === 0) {
186
+ return null
187
+ }
188
+ const known = (id) => id !== null && id !== undefined && views.some((view) => view.id === String(id));
189
+ const fromAddress = this.$route && this.$route.query ? this.$route.query[PAGE_VIEW_QUERY_KEY] : undefined;
190
+ if (known(fromAddress)) {
191
+ return String(fromAddress)
192
+ }
193
+ if (known(this.selectedViewId)) {
194
+ return String(this.selectedViewId)
195
+ }
196
+ if (known(this.defaultView)) {
197
+ return this.defaultView
198
+ }
199
+ return views[0].id
200
+ },
201
+
202
+ /**
203
+ * The chosen view, or null without views.
204
+ *
205
+ * @return {object|null}
206
+ */
207
+ activeView() {
208
+ return this.normalizedViews.find((view) => view.id === this.activeViewId) || null
209
+ },
210
+
211
+ /**
212
+ * The switch's options, for CnSegmentedControl.
213
+ *
214
+ * @return {Array<{value: string, label: string}>}
215
+ */
216
+ viewSwitchOptions() {
217
+ return this.normalizedViews.map((view) => ({ value: view.id, label: view.label }))
218
+ },
219
+
220
+ /**
221
+ * The switch's accessible name: `viewsLabel` translated, else "View".
222
+ *
223
+ * @return {string}
224
+ * @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-the-view-switch-is-accessible
225
+ */
226
+ viewSwitchLabel() {
227
+ const translate$1 = typeof this.cnTranslate === 'function' ? this.cnTranslate : (key) => key;
228
+ return this.viewsLabel ? translate$1(this.viewsLabel) : translate('nextcloud-vue', 'Views')
229
+ },
230
+
231
+ /**
232
+ * Whether a greeting header in the page's own widgets draws the
233
+ * switch (an option with a `view`), so the page draws none.
234
+ *
235
+ * @return {boolean}
236
+ * @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-a-greeting-header-can-switch-the-pages-views
237
+ */
238
+ viewSwitchInHeaderWidget() {
239
+ const widgets = Array.isArray(this.widgets) ? this.widgets : [];
240
+ return widgets.some((def) => {
241
+ const options = def && def.content && def.content.views && def.content.views.options;
242
+ return Array.isArray(options) && options.some((option) => option && typeof option.view === 'string' && option.view !== '')
243
+ })
244
+ },
245
+
246
+ /**
247
+ * Whether the page draws the switch itself.
248
+ *
249
+ * @return {boolean}
250
+ */
251
+ showsPageViewSwitch() {
252
+ return this.hasViews && !this.viewSwitchInHeaderWidget
253
+ },
254
+
255
+ /**
256
+ * The user-preference key for this page's chosen view.
257
+ *
258
+ * @return {string}
259
+ */
260
+ viewPreferenceKey() {
261
+ // The page id, else the route name: a detail page's fallback id is
262
+ // its record's title, which would remember a view per record.
263
+ const id = this.pageId || (this.$route && this.$route.name) || this.resolvedPageId || 'page';
264
+ return `${PAGE_VIEW_PREFERENCE_PREFIX}${id}`
265
+ },
266
+
267
+ /**
268
+ * The app id the stored view is kept under: the page's `appId` prop
269
+ * when it has one, else the app root's.
270
+ *
271
+ * @return {string}
272
+ */
273
+ viewPreferenceAppId() {
274
+ const own = typeof this.appId === 'string' ? this.appId : '';
275
+ return own || (typeof this.cnPageViewsAppId === 'string' ? this.cnPageViewsAppId : '')
276
+ },
277
+
278
+ /**
279
+ * The message of the chosen view's empty state.
280
+ *
281
+ * @return {string}
282
+ * @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-a-view-with-nothing-to-draw-says-so
283
+ */
284
+ viewEmptyText() {
285
+ if (this.activeView && this.activeView.emptyText) {
286
+ return this.activeView.emptyText
287
+ }
288
+ return translate('nextcloud-vue', 'This view has no widgets yet.')
289
+ },
290
+ },
291
+
292
+ created() {
293
+ this.loadStoredView();
294
+ },
295
+
296
+ methods: {
297
+ /**
298
+ * Read the stored view: the browser mirror at once, then the user
299
+ * preference on the server, which wins unless the user already chose.
300
+ *
301
+ * @return {Promise<void>}
302
+ * @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-the-chosen-view-is-linkable-and-remembered
303
+ */
304
+ async loadStoredView() {
305
+ if (!this.hasViews) {
306
+ return
307
+ }
308
+ const appId = this.viewPreferenceAppId;
309
+ const key = this.viewPreferenceKey;
310
+ this.selectedViewId = readStoredViewMirror(appId, key);
311
+ if (!appId) {
312
+ return
313
+ }
314
+ const stored = await readUserPreference(appId, key, null);
315
+ if (!this.viewChosen && typeof stored === 'string' && stored !== '') {
316
+ this.selectedViewId = stored;
317
+ }
318
+ },
319
+
320
+ /**
321
+ * Choose a view: keep it, store it, and write it to the address
322
+ * (replacing the history entry, keeping the other query keys).
323
+ *
324
+ * @param {string} id The view id.
325
+ * @return {void}
326
+ * @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-the-chosen-view-is-linkable-and-remembered
327
+ */
328
+ selectView(id) {
329
+ const view = this.normalizedViews.find((candidate) => candidate.id === String(id));
330
+ if (!view) {
331
+ return
332
+ }
333
+ this.selectedViewId = view.id;
334
+ this.viewChosen = true;
335
+ writeUserPreference(this.viewPreferenceAppId, this.viewPreferenceKey, view.id);
336
+ if (!this.$router || !this.$route) {
337
+ return
338
+ }
339
+ const query = this.$route.query || {};
340
+ if (query[PAGE_VIEW_QUERY_KEY] === view.id) {
341
+ return
342
+ }
343
+ const result = this.$router.replace({ query: { ...query, [PAGE_VIEW_QUERY_KEY]: view.id } });
344
+ if (result && typeof result.catch === 'function') {
345
+ result.catch(() => {});
346
+ }
347
+ },
348
+
349
+ /**
350
+ * Look a widget definition up in the views, for a layout item of the
351
+ * chosen view.
352
+ *
353
+ * @param {string} widgetId The widget id.
354
+ * @return {object|null} The definition, or null.
355
+ */
356
+ findViewWidget(widgetId) {
357
+ const view = this.activeView;
358
+ if (!view) {
359
+ return null
360
+ }
361
+ return view.widgets.find((def) => def && def.id === widgetId) || null
362
+ },
363
+
364
+ /**
365
+ * Write a drag or resize in the chosen view's grid back onto its
366
+ * layout items in place, as the page does for its own layout, so the
367
+ * in-app manifest editor sees it.
368
+ *
369
+ * @param {Array<object>} updated The layout from the grid.
370
+ * @return {void}
371
+ */
372
+ onViewLayoutChange(updated) {
373
+ const view = this.activeView;
374
+ if (!view || !Array.isArray(updated)) {
375
+ return
376
+ }
377
+ for (const u of updated) {
378
+ const item = view.layout.find((l) => String(l.id) === String(u.id));
379
+ if (!item) {
380
+ continue
381
+ }
382
+ for (const key of ['gridX', 'gridY', 'gridWidth', 'gridHeight']) {
383
+ if (u[key] !== undefined) {
384
+ item[key] = u[key];
385
+ }
386
+ }
387
+ }
388
+ /**
389
+ * Emitted when a widget in a view's grid is dragged or resized.
390
+ *
391
+ * @event view-layout-change
392
+ * @type {{view: string, layout: Array<object>}}
393
+ */
394
+ this.$emit('view-layout-change', { view: view.id, layout: view.layout });
395
+ },
396
+ },
397
+ };
398
+
399
+ export { PAGE_VIEW_PREFERENCE_PREFIX, PAGE_VIEW_QUERY_KEY, pageViews };
400
+ //# sourceMappingURL=pageViews.js.map