@conduction/nextcloud-vue 2.56.0-beta.13 → 2.56.0-beta.14

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 (57) 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/CnDataTable/CnDataTable.vue2.js +4 -4
  9. package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
  10. package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
  11. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +31 -1
  12. package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
  13. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +335 -313
  14. package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
  15. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js +5 -11
  16. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js.map +1 -1
  17. package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js.map +1 -1
  18. package/dist/esm/composables/useDependencyCheck.js +44 -1
  19. package/dist/esm/composables/useDependencyCheck.js.map +1 -1
  20. package/dist/esm/composables/useListView.js +43 -1
  21. package/dist/esm/composables/useListView.js.map +1 -1
  22. package/dist/esm/composables/useSetupStatus.js +26 -3
  23. package/dist/esm/composables/useSetupStatus.js.map +1 -1
  24. package/dist/esm/l10n/en.json.js +4 -1
  25. package/dist/esm/l10n/en.json.js.map +1 -1
  26. package/dist/esm/l10n/nl.json.js +4 -1
  27. package/dist/esm/l10n/nl.json.js.map +1 -1
  28. package/dist/esm/nextcloud-vue.css +20 -2
  29. package/dist/esm/store/plugins/liveUpdates.js +19 -9
  30. package/dist/esm/store/plugins/liveUpdates.js.map +1 -1
  31. package/dist/esm/store/useObjectStore.js +20 -9
  32. package/dist/esm/store/useObjectStore.js.map +1 -1
  33. package/dist/esm/utils/validateManifestV2.compiled.js +1 -1
  34. package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
  35. package/dist/nextcloud-vue.cjs.js +857 -652
  36. package/dist/nextcloud-vue.cjs.js.map +2 -2
  37. package/dist/nextcloud-vue.css +20 -2
  38. package/l10n/en.json +4 -1
  39. package/l10n/nl.json +4 -1
  40. package/package.json +1 -1
  41. package/src/components/CnActionsBar/CnActionsBar.md +2 -1
  42. package/src/components/CnActionsBar/CnActionsBar.vue +25 -1
  43. package/src/components/CnAppNav/CnAppNav.vue +2 -2
  44. package/src/components/CnDataTable/CnDataTable.md +1 -1
  45. package/src/components/CnDataTable/CnDataTable.vue +4 -4
  46. package/src/components/CnIndexPage/CnIndexPage.md +2 -1
  47. package/src/components/CnIndexPage/CnIndexPage.vue +47 -1
  48. package/src/components/CnSetupWizard/CnSetupWizard.vue +5 -11
  49. package/src/composables/useDependencyCheck.js +43 -0
  50. package/src/composables/useListView.js +43 -1
  51. package/src/composables/useSetupStatus.js +26 -3
  52. package/src/css/actions-bar.css +20 -2
  53. package/src/schemas/app-manifest-v2.schema.json +3 -3
  54. package/src/schemas/app-manifest.schema.json +1 -1
  55. package/src/store/plugins/liveUpdates.js +19 -9
  56. package/src/store/useObjectStore.js +19 -8
  57. package/src/utils/validateManifestV2.compiled.js +1 -1
@@ -1,4 +1,4 @@
1
1
  [diffend] Oversized file quarantined before diffing.
2
2
  name: package/dist/nextcloud-vue.cjs.js.map
3
- size: 35355767 bytes
4
- sha256: e048d3627c1eba03f90228c9f300e9341baf3086530119fe40b4af2106f5b7b2
3
+ size: 35372297 bytes
4
+ sha256: 7065df25e721d338e4612aac2ee75c58c87fb5e17477c811e7bcf66cee41783d
@@ -1940,6 +1940,23 @@ a.cn-kpi-card--stacked:focus-visible {
1940
1940
  color: var(--color-text-maxcontrast);
1941
1941
  }
1942
1942
 
1943
+ /* Beside the inline search (`showCountWithSearch`): the search field shrinks
1944
+ first and the counter stays on one line. */
1945
+ .cn-actions-bar__count--beside-search {
1946
+ flex-shrink: 0;
1947
+ white-space: nowrap;
1948
+ }
1949
+
1950
+ /* Taken out of the flex row so it adds no gap, but kept in the accessibility
1951
+ tree so the live region exists before a total appears. */
1952
+ .cn-actions-bar__count--empty {
1953
+ position: absolute;
1954
+ width: 1px;
1955
+ height: 1px;
1956
+ overflow: hidden;
1957
+ clip-path: inset(50%);
1958
+ }
1959
+
1943
1960
  /* Inline search field */
1944
1961
  .cn-actions-bar__search {
1945
1962
  position: relative;
@@ -2199,8 +2216,9 @@ a.cn-kpi-card--stacked:focus-visible {
2199
2216
  display: contents;
2200
2217
  }
2201
2218
 
2202
- /* #after-search filter buttons + the view toggle ride beside the
2203
- search field (DOM order — search first via its own order). */
2219
+ /* #after-search filter buttons, the beside-search counter and the view
2220
+ toggle ride beside the search field (DOM order — search first via its
2221
+ own order). */
2204
2222
  .cn-actions-bar__info > * {
2205
2223
  order: 1;
2206
2224
  }
package/l10n/en.json CHANGED
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "translations": {
3
+ "Advanced": "Advanced",
3
4
  "The item could not be created.": "The item could not be created.",
4
5
  "Start over": "Start over",
5
6
  "Continue where you left off": "Continue where you left off",
@@ -1100,7 +1101,9 @@
1100
1101
  "Show the date of today above the heading": "Show the date of today above the heading",
1101
1102
  "Plain header without a coloured background": "Plain header without a coloured background",
1102
1103
  "Loading…": "Loading…",
1103
- "A register and schema are required": "A register and schema are required"
1104
+ "A register and schema are required": "A register and schema are required",
1105
+ "Change the search or try again.": "Change the search or try again.",
1106
+ "Try again later.": "Try again later."
1104
1107
  },
1105
1108
  "plurals": {
1106
1109
  "%n year": ["%n year", "%n years"],
package/l10n/nl.json CHANGED
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "translations": {
3
+ "Advanced": "Geavanceerd",
3
4
  "The item could not be created.": "Het item kon niet worden aangemaakt.",
4
5
  "Start over": "Opnieuw beginnen",
5
6
  "Continue where you left off": "Ga verder waar je was",
@@ -1144,7 +1145,9 @@
1144
1145
  "Show the date of today above the heading": "Toon de datum van vandaag boven de kop",
1145
1146
  "Plain header without a coloured background": "Eenvoudige kop zonder gekleurde achtergrond",
1146
1147
  "Loading…": "Laden…",
1147
- "A register and schema are required": "Een register en schema zijn verplicht"
1148
+ "A register and schema are required": "Een register en schema zijn verplicht",
1149
+ "Change the search or try again.": "Pas de zoekopdracht aan of probeer het opnieuw.",
1150
+ "Try again later.": "Probeer het later opnieuw."
1148
1151
  },
1149
1152
  "plurals": {
1150
1153
  "%n year": ["%n jaar", "%n jaar"],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@conduction/nextcloud-vue",
3
- "version": "2.56.0-beta.13",
3
+ "version": "2.56.0-beta.14",
4
4
  "description": "Shared Vue component library for Conduction Nextcloud apps — complements @nextcloud/vue with higher-level components, OpenRegister integration, and NL Design System support",
5
5
  "homepage": "https://github.com/ConductionNL/nextcloud-vue#readme",
6
6
  "bugs": {
@@ -152,9 +152,10 @@ Controlling the inline action button count — `inlineActionCount` sets how many
152
152
  | `showMap` | `Boolean` | `false` | Whether to render the third "Map" view-toggle segment. Off by default so existing two-segment consumers are unchanged |
153
153
  | `mapLabel` | `String` | `''` | Label for the map view-toggle option (defaults to "Map"). Only shown when `showMap` |
154
154
  | `mapIcon` | `String` | `''` | MDI icon name for the map option (defaults to the built-in map-marker icon). Resolved via `CnIcon` |
155
- | `showSearch` | `Boolean` | `false` | Whether to show the inline search field on the left of the bar |
155
+ | `showSearch` | `Boolean` | `false` | Whether to show the inline search field on the left of the bar. The field takes the place of the "Showing X of Y" counter unless `showCountWithSearch` is set |
156
156
  | `searchValue` | `String` | `''` | Current value of the inline search field (controlled) |
157
157
  | `searchPlaceholder` | `String` | `''` | Placeholder / accessible label for the inline search field |
158
+ | `showCountWithSearch` | `Boolean` | `false` | Keep the "Showing X of Y" counter visible beside the inline search field, after the search and any `#after-search` controls. No effect without `showSearch` |
158
159
  | `headerActions` | `Array` | `[]` | Manifest-declared page-level actions rendered inside the overflow dropdown between the built-in Refresh and the `#action-items` slot. Each entry is `{ id, label, icon?, disabled? }` — the bar emits `@header-action({ action: id, id })` on click and the parent (e.g. `CnIndexPage`) dispatches the resolved handler. The `icon` field accepts EITHER an MDI Vue component name (e.g. `'History'`) — rendered via `CnIcon` — OR a Nextcloud core CSS icon class (e.g. `'icon-history'`) — rendered as a `<span>` carrying that class. An entry with `href` (URL) or `to` (vue-router location) renders as a real link, `linkTarget` as its `target`; it still emits `@header-action`, so the parent must not navigate for it again. |
159
160
 
160
161
  ## Manifest header actions example
@@ -12,11 +12,16 @@
12
12
  :aria-label="searchPlaceholder || t('nextcloud-vue', 'Search')"
13
13
  @input="onSearchInput">
14
14
  </div>
15
- <span v-else-if="pagination && pagination.total > 0" class="cn-actions-bar__count">
15
+ <span v-else-if="hasTotal" class="cn-actions-bar__count">
16
16
  {{ countText }}
17
17
  </span>
18
18
  <!-- @slot after-search Refinement controls rendered beside the search field on the LEFT side of the bar (e.g. a filter menu button). Convention: the left side groups the VISUAL controls — search, filters, view toggle — while the right cluster holds the ACT controls (add, overflow); the standalone sort select is a display control too but keeps its legacy right-side placement. -->
19
19
  <slot name="after-search" />
20
+ <!-- Counter beside the search (`showCountWithSearch`): after the search and its #after-search controls. The live region stays mounted while there is no total, so results coming back after none are announced too. -->
21
+ <span v-if="showSearch && showCountWithSearch"
22
+ class="cn-actions-bar__count cn-actions-bar__count--beside-search"
23
+ :class="{ 'cn-actions-bar__count--empty': !hasTotal }"
24
+ aria-live="polite">{{ hasTotal ? countText : '' }}</span>
20
25
 
21
26
  <!-- View mode toggle (Cards / Table / List) — segmented control with
22
27
  a sliding thumb that animates between the N segments. Lives in
@@ -644,6 +649,16 @@ export default {
644
649
  default: '',
645
650
  },
646
651
 
652
+ /**
653
+ * Keep the "Showing X of Y" counter visible beside the inline search field,
654
+ * after any `#after-search` controls. By default the search field takes the
655
+ * counter's place. No effect without `showSearch`.
656
+ */
657
+ showCountWithSearch: {
658
+ type: Boolean,
659
+ default: false,
660
+ },
661
+
647
662
  /** Whether the refresh action is currently in progress */
648
663
  refreshing: {
649
664
  type: Boolean,
@@ -811,6 +826,15 @@ export default {
811
826
  }))
812
827
  },
813
828
 
829
+ /**
830
+ * Whether there is a non-empty total for the "Showing X of Y" counter.
831
+ *
832
+ * @return {boolean}
833
+ */
834
+ hasTotal() {
835
+ return Boolean(this.pagination && this.pagination.total > 0)
836
+ },
837
+
814
838
  countText() {
815
839
  if (!this.pagination) {
816
840
  return ''
@@ -1113,7 +1113,7 @@ export default {
1113
1113
 
1114
1114
  /**
1115
1115
  * Label for the foldout's gear button. Manifest override:
1116
- * `nav.settingsLabel`; defaults to "Settings".
1116
+ * `nav.settingsLabel`; defaults to "Advanced".
1117
1117
  *
1118
1118
  * @return {string}
1119
1119
  */
@@ -1122,7 +1122,7 @@ export default {
1122
1122
  if (typeof custom === 'string' && custom.length > 0) {
1123
1123
  return this.effectiveTranslate(custom)
1124
1124
  }
1125
- return t('nextcloud-vue', 'Settings')
1125
+ return t('nextcloud-vue', 'Advanced')
1126
1126
  },
1127
1127
 
1128
1128
  /**
@@ -186,7 +186,7 @@ export default {
186
186
  | `includeColumns` | Array | `null` | Column keys to include (whitelist) in schema mode |
187
187
  | `sortKey` | String | `null` | Currently active sort column key (controlled) |
188
188
  | `sortOrder` | String | `'asc'` | Current sort direction: `'asc'` or `'desc'` |
189
- | `sortKeys` | Array | `[]` | Ordered multi-column ("shift+click") sort key list, `[{ key, order }, …]` (0–3 entries); takes precedence over `sortKey`/`sortOrder` when non-empty. Priority badges show only when two or more rendered, sortable columns are sorted; a key whose column is not rendered or not sortable (such as a `_uuid` tie-break) still sorts, but is not counted or numbered |
189
+ | `sortKeys` | Array | `[]` | Ordered multi-column ("shift+click") sort key list, `[{ key, order }, …]` (0–3 entries); takes precedence over `sortKey`/`sortOrder` when non-empty. Priority badges show only when two or more rendered, sortable columns are sorted; a key whose column is not rendered or not sortable (such as a `_uuid` tie-break) still sorts, but is not counted or numbered, and `aria-sort` goes to the first sort key whose column is rendered and sortable |
190
190
  | `selectedIds` | Array | `[]` | Array of selected row IDs (controlled) |
191
191
  | `rowClass` | Function | `null` | `(row) => string` — CSS class(es) applied to each `<tr>` |
192
192
  | `rowClickToView` | Boolean | `false` | Emit `row-click` on a row-body click even while `selectable` (selection then via the checkbox column only) |
@@ -371,7 +371,7 @@ import '../../css/table.css'
371
371
  *
372
372
  * Sorting: a plain click on a sortable header is single-sort (cycle asc → desc → cleared), unchanged from before.
373
373
  * Shift+click (or Shift+Enter on a focused header) appends the column as a secondary/tertiary sort key, capped at 3, with numbered priority badges (1, 2, 3) once two or more rendered, sortable columns are sorted.
374
- * A sort key whose column is not rendered, or not sortable, still sorts, but is not counted or numbered.
374
+ * A sort key whose column is not rendered, or not sortable, still sorts, but is not counted or numbered, and `aria-sort` goes to the first sort key whose column is rendered and sortable.
375
375
  * Pass `sortKeys: [{key, order}, ...]` for multi-sort (falls back to the legacy `sortKey`/`sortOrder` props when empty).
376
376
  * The `sort` event payload is extended, not replaced: `{key, order}` still mirrors the primary key exactly as before; a new `keys` field carries the full ordered list.
377
377
  * See `src/utils/multiColumnSort.js` for the state machine.
@@ -1722,8 +1722,8 @@ export default {
1722
1722
  },
1723
1723
 
1724
1724
  /**
1725
- * `aria-sort` value for a column header: `'ascending'`/`'descending'` for the PRIMARY (index 0) active sort key only, per WCAG guidance that `aria-sort` describes single-column sort state.
1726
- * `null` omits the attribute entirely (unsorted, not sortable, or not the primary key).
1725
+ * `aria-sort` value for a column header: `'ascending'`/`'descending'` for the first rendered sort key only, per WCAG guidance that `aria-sort` describes single-column sort state.
1726
+ * `null` omits the attribute entirely (unsorted, not sortable, or not the first rendered sort key).
1727
1727
  *
1728
1728
  * @param {object} col Column definition.
1729
1729
  * @return {string|null}
@@ -1732,7 +1732,7 @@ export default {
1732
1732
  if (!col.sortable) {
1733
1733
  return null
1734
1734
  }
1735
- const primary = this.effectiveSortKeys[0]
1735
+ const primary = this.renderedSortKeys[0]
1736
1736
  if (!primary || primary.key !== col.key) {
1737
1737
  return null
1738
1738
  }
@@ -386,7 +386,8 @@ export default {
386
386
  | `fieldOverrides` | Object | `{}` | Per-field config overrides passed to `CnFormDialog` |
387
387
  | `customComponents` | Object | `null` | Custom-component / handler registry. When set, takes precedence over the injected `cnCustomComponents` from CnAppRoot. Used to resolve `actions[].handler` registry names declared in the manifest (manifest-actions-dispatch). Named handlers resolve out of the v2 `registry` (a `kind: "handler"` entry) FIRST and fall back to this map. |
388
388
  | `showViewToggle` | Boolean | `true` | Whether to show the Cards/Table view toggle |
389
- | `inlineSearch` | Boolean | `false` | Show an inline search field in the actions bar (in addition to / instead of the sidebar search). Fed from the manifest as `pages[].config.inlineSearch`. |
389
+ | `inlineSearch` | Boolean | `false` | Show an inline search field in the actions bar (in addition to / instead of the sidebar search). Fed from the manifest as `pages[].config.inlineSearch`. The field takes the place of the "Showing X of Y" counter unless `showCountWithSearch` is set. |
390
+ | `showCountWithSearch` | Boolean | `false` | Keep the "Showing X of Y" counter visible beside the inline search field, after the search and any `#after-search` controls (only relevant with `inlineSearch`, which otherwise takes the counter's place). Fed from the manifest as `pages[].config.showCountWithSearch`. |
390
391
  | `searchPlaceholder` | String | `''` | Placeholder for the inline search field (manifest `config.searchPlaceholder`). |
391
392
  | `cardsLabel` | String | `''` | Label for the cards view-toggle option (manifest `config.cardsLabel`, e.g. "Tiles"). |
392
393
  | `tableLabel` | String | `''` | Label for the table view-toggle option (manifest `config.tableLabel`, e.g. "List"). |
@@ -59,6 +59,7 @@
59
59
  :showSearch="inlineSearch"
60
60
  :searchValue="effectiveSearchValue"
61
61
  :searchPlaceholder="searchPlaceholder"
62
+ :showCountWithSearch="showCountWithSearch"
62
63
  :refreshing="effectiveRefreshing"
63
64
  :refreshDisabled="refreshDisabled"
64
65
  :addDisabled="addDisabled"
@@ -417,6 +418,21 @@
417
418
  <NcLoadingIcon :size="32" :name="loadingText" />
418
419
  </div>
419
420
 
421
+ <!-- Error state: the latest self-fetch failed (e.g. a search term the server rejects) -->
422
+ <div v-else-if="selfFetchFailed"
423
+ class="cn-index-page__empty"
424
+ role="status"
425
+ data-testid="cn-index-page-fetch-error">
426
+ <NcEmptyContent :name="t('nextcloud-vue', 'An error occurred')"
427
+ :description="effectiveSearchValue
428
+ ? t('nextcloud-vue', 'Change the search or try again.')
429
+ : t('nextcloud-vue', 'Try again later.')">
430
+ <template #icon>
431
+ <AlertCircleOutline :size="64" />
432
+ </template>
433
+ </NcEmptyContent>
434
+ </div>
435
+
420
436
  <!-- Empty state -->
421
437
  <div v-else-if="effectiveObjects.length === 0" class="cn-index-page__empty">
422
438
  <slot name="empty">
@@ -774,6 +790,7 @@ import { getCurrentUser } from '@nextcloud/auth'
774
790
  import { translate as t } from '@nextcloud/l10n'
775
791
  import { NcActionButton, NcActionCaption, NcActionCheckbox, NcActions, NcButton, NcEmptyContent, NcLoadingIcon } from '@nextcloud/vue'
776
792
  import { getCurrentInstance, inject, markRaw, ref } from 'vue'
793
+ import AlertCircleOutline from 'vue-material-design-icons/AlertCircleOutline.vue'
777
794
  import Close from 'vue-material-design-icons/Close.vue'
778
795
  import Cog from 'vue-material-design-icons/Cog.vue'
779
796
  import DatabaseSearch from 'vue-material-design-icons/DatabaseSearch.vue'
@@ -1054,6 +1071,7 @@ export default {
1054
1071
  NcActionCaption,
1055
1072
  NcActionCheckbox,
1056
1073
  NcButton,
1074
+ AlertCircleOutline,
1057
1075
  Close,
1058
1076
  Cog,
1059
1077
  DatabaseSearch,
@@ -2034,6 +2052,17 @@ export default {
2034
2052
  default: false,
2035
2053
  },
2036
2054
 
2055
+ /**
2056
+ * Keep the "Showing X of Y" counter visible beside the inline search field,
2057
+ * after any `#after-search` controls (only relevant with `inlineSearch`,
2058
+ * which otherwise takes the counter's place). Fed from the manifest as
2059
+ * `pages[].config.showCountWithSearch`.
2060
+ */
2061
+ showCountWithSearch: {
2062
+ type: Boolean,
2063
+ default: false,
2064
+ },
2065
+
2037
2066
  /** Placeholder for the inline search field (manifest `config.searchPlaceholder`) */
2038
2067
  searchPlaceholder: {
2039
2068
  type: String,
@@ -2934,11 +2963,23 @@ export default {
2934
2963
  return this.isSelfFetch && !!this.list
2935
2964
  },
2936
2965
 
2966
+ /**
2967
+ * Whether the latest self-fetch failed. The store keeps the previous
2968
+ * rows and pagination on a failed fetch, so while this holds the page
2969
+ * shows its error state instead of results for an earlier query.
2970
+ */
2971
+ selfFetchFailed() {
2972
+ return this.isSelfFetchMode && !!this.list.error?.value
2973
+ },
2974
+
2937
2975
  /** Rows: store collection in self-fetch mode, else the `objects` prop. */
2938
2976
  effectiveObjects() {
2939
2977
  if (this.isNamedSource) {
2940
2978
  return this.namedRows
2941
2979
  }
2980
+ if (this.selfFetchFailed) {
2981
+ return []
2982
+ }
2942
2983
  return this.isSelfFetchMode ? (this.list.objects.value || []) : this.objects
2943
2984
  },
2944
2985
 
@@ -3342,7 +3383,9 @@ export default {
3342
3383
  s: this.exportSchemaSlug,
3343
3384
  f: this.filter || null,
3344
3385
  e: this.countEntries,
3345
- t: Number(this.effectivePagination?.total ?? 0),
3386
+ // The store's total, not the error override in effectivePagination,
3387
+ // so a failed fetch and its recovery do not refetch the counts.
3388
+ t: Number((this.isSelfFetchMode ? this.list.pagination.value : this.effectivePagination)?.total ?? 0),
3346
3389
  })
3347
3390
  },
3348
3391
 
@@ -3748,6 +3791,9 @@ export default {
3748
3791
 
3749
3792
  /** Pagination: store pagination in self-fetch mode, else the `pagination` prop. */
3750
3793
  effectivePagination() {
3794
+ if (this.selfFetchFailed) {
3795
+ return { ...this.list.pagination.value, total: 0, page: 1, pages: 1 }
3796
+ }
3751
3797
  return this.isSelfFetchMode ? this.list.pagination.value : this.pagination
3752
3798
  },
3753
3799
 
@@ -199,13 +199,12 @@
199
199
 
200
200
  <script>
201
201
  import { getCurrentUser } from '@nextcloud/auth'
202
- import { loadState } from '@nextcloud/initial-state'
203
202
  import { translate as t } from '@nextcloud/l10n'
204
203
  import { NcButton, NcCheckboxRadioSwitch, NcLoadingIcon, NcNoteCard, NcSelect, NcTextField } from '@nextcloud/vue'
205
204
  import CnChoiceCards from '../CnChoiceCards/CnChoiceCards.vue'
206
205
  import CnLeafDependencySettings from '../CnLeafDependencySettings/CnLeafDependencySettings.vue'
207
206
  import CnWizardDialog from '../CnWizardDialog/CnWizardDialog.vue'
208
- import { checkDependencies, isDependencyResolved } from '../../composables/useDependencyCheck.js'
207
+ import { checkDependencies, isDependencyResolved, missingRequiredApps, readServerAppStatuses } from '../../composables/useDependencyCheck.js'
209
208
  import { useSetupStatus } from '../../composables/useSetupStatus.js'
210
209
  import { fieldsFromSchema } from '../../utils/schema.js'
211
210
 
@@ -474,16 +473,15 @@ export default {
474
473
  *
475
474
  * @return {Array<{ step: object, missing: Array<string> }>}
476
475
  * @spec openspec/changes/setup-wizard-card-load-and-dependency-gate/specs/cn-setup-wizard/spec.md#requirement-the-wizard-checks-dependencies-before-any-step
476
+ * @spec openspec/changes/optional-step-requires/specs/cn-setup-wizard/spec.md#requirement-a-step-whose-required-apps-are-absent-is-not-applicable
477
477
  */
478
478
  skippedSteps() {
479
479
  return this.declaredSteps
480
480
  .filter((s) => Array.isArray(s.requires) && s.requires.length > 0)
481
481
  .map((step) => {
482
482
  const known = Object.fromEntries(this.dependencyRows.map((d) => [d.id, d]))
483
- const missing = checkDependencies(step.requires, this.serverAppStatuses)
484
- .map((row) => known[row.id] || row)
485
- .filter((row) => !isDependencyResolved(row))
486
- .map((row) => row.name || row.id)
483
+ const missing = missingRequiredApps(step.requires, this.serverAppStatuses)
484
+ .map((row) => (known[row.id] || row).name || row.id)
487
485
  return { step, missing }
488
486
  })
489
487
  .filter((entry) => entry.missing.length > 0)
@@ -523,11 +521,7 @@ export default {
523
521
  * @return {object}
524
522
  */
525
523
  serverAppStatuses() {
526
- try {
527
- return loadState(this.appId, 'dependency_statuses', {}) || {}
528
- } catch {
529
- return {}
530
- }
524
+ return readServerAppStatuses(this.appId)
531
525
  },
532
526
 
533
527
  /**
@@ -3,6 +3,7 @@
3
3
  * SPDX-License-Identifier: EUPL-1.2
4
4
  */
5
5
 
6
+ import { loadState } from '@nextcloud/initial-state'
6
7
  import { useAppStatus } from './useAppStatus.js'
7
8
 
8
9
  /**
@@ -59,3 +60,45 @@ export function checkDependencies(entries, serverStatuses = {}) {
59
60
  export function isDependencyResolved(row) {
60
61
  return !!row && row.installed === true && row.enabled === true
61
62
  }
63
+
64
+ /**
65
+ * Read the `dependency_statuses` initial state an app may inject from PHP.
66
+ *
67
+ * Keyed by app id: `{ installed, enabled }`. Returns `{}` when the app injects
68
+ * none or the state cannot be read, so callers fall back to `useAppStatus`.
69
+ *
70
+ * @param {string} appId The app whose initial state to read.
71
+ * @return {object} Server-reported statuses keyed by app id.
72
+ *
73
+ * @spec openspec/changes/optional-step-requires/specs/cn-setup-wizard/spec.md#requirement-a-step-whose-required-apps-are-absent-is-not-applicable
74
+ */
75
+ export function readServerAppStatuses(appId) {
76
+ try {
77
+ const statuses = loadState(appId, 'dependency_statuses', {})
78
+ return (statuses && typeof statuses === 'object') ? statuses : {}
79
+ } catch {
80
+ return {}
81
+ }
82
+ }
83
+
84
+ /**
85
+ * The apps in a setup step's `requires` list that are not installed and
86
+ * enabled.
87
+ *
88
+ * This is the one resolution every setup surface uses: `CnSetupWizard` skips a
89
+ * step when it is non-empty, and `useSetupStatus` then counts that step as not
90
+ * applicable rather than unmet. Keeping both on one function is what stops the
91
+ * wizard and the status from disagreeing about the same step.
92
+ *
93
+ * @param {Array<string>} requires The step's `requires` app ids.
94
+ * @param {object} [serverStatuses] Server-reported statuses keyed by app id.
95
+ * @return {Array<{id: string, name: string, required: boolean, installed: boolean, enabled: boolean}>} The missing apps.
96
+ *
97
+ * @spec openspec/changes/optional-step-requires/specs/cn-setup-wizard/spec.md#requirement-a-step-whose-required-apps-are-absent-is-not-applicable
98
+ */
99
+ export function missingRequiredApps(requires, serverStatuses = {}) {
100
+ if (!Array.isArray(requires) || requires.length === 0) {
101
+ return []
102
+ }
103
+ return checkDependencies(requires, serverStatuses).filter((row) => !isDependencyResolved(row))
104
+ }
@@ -94,6 +94,13 @@ export function useListView(objectTypeOrOptions, options) {
94
94
  // field (a folder pane grouping by it, say) reads them here rather than
95
95
  // deriving them from `objects`, which is only the current page.
96
96
  const facets = computed(() => objectStore.facets[objectType] || {})
97
+ // Outcome of the latest `refresh()`: the store's error for it, or null once
98
+ // it succeeded or a later fetch stored new rows. The store keeps the previous
99
+ // rows on a failed fetch, so this is how a consumer tells stale rows from
100
+ // current ones.
101
+ const error = ref(null)
102
+ let refreshSeq = 0
103
+ let pendingRefreshes = 0
97
104
 
98
105
  let searchTimeout = null
99
106
 
@@ -178,14 +185,48 @@ export function useListView(objectTypeOrOptions, options) {
178
185
 
179
186
  /**
180
187
  * Fetch the collection using current state params and update sidebar facet data.
188
+ * Only the most recently started refresh sets `error`, so an older response
189
+ * settling late cannot override a newer one's outcome.
181
190
  *
182
191
  * @param {number} [page] Page to fetch
183
192
  * @return {Promise<void>}
184
193
  */
185
194
  async function refresh(page = 1) {
186
- await objectStore.fetchCollection(objectType, buildParams(page))
195
+ const seq = ++refreshSeq
196
+ // This call's own result. `errors[objectType]` is shared by every call
197
+ // for the type and is not cleared on success, so it cannot say which
198
+ // request failed; it is only the fallback for a store that ignores
199
+ // `options.outcome`.
200
+ const outcome = {}
201
+ pendingRefreshes++
202
+ try {
203
+ await objectStore.fetchCollection(objectType, buildParams(page), { outcome })
204
+ } catch (e) {
205
+ if (seq === refreshSeq) {
206
+ error.value = e
207
+ }
208
+ throw e
209
+ } finally {
210
+ pendingRefreshes--
211
+ }
212
+ if (seq === refreshSeq) {
213
+ error.value = ('error' in outcome) ? outcome.error : (objectStore.errors?.[objectType] || null)
214
+ }
187
215
  }
188
216
 
217
+ // A fetch outside `refresh()` (e.g. the live-updates refetch) that stores
218
+ // new rows recovers the list from an earlier failure. Rows written while
219
+ // a refresh is pending are left to that refresh, which decides `error`.
220
+ watch(
221
+ () => objectStore.collections[objectType],
222
+ () => {
223
+ if (pendingRefreshes === 0) {
224
+ error.value = null
225
+ }
226
+ },
227
+ { flush: 'sync' },
228
+ )
229
+
189
230
  // ── Event handlers ───────────────────────────────────────────────────
190
231
 
191
232
  /**
@@ -323,6 +364,7 @@ export function useListView(objectTypeOrOptions, options) {
323
364
  loading,
324
365
  pagination,
325
366
  facets,
367
+ error,
326
368
  // Local state
327
369
  searchTerm,
328
370
  sortKey,
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  import { computed, ref } from 'vue'
7
+ import { missingRequiredApps, readServerAppStatuses } from './useDependencyCheck.js'
7
8
 
8
9
  /**
9
10
  * Per-`appId` cache of setup-status results. The Vue refs are stored so all
@@ -27,6 +28,14 @@ const cache = new Map()
27
28
  * every non-admin user of an app with a `setup` block met a wizard they had no
28
29
  * permission to complete instead of the app itself.
29
30
  *
31
+ * A step that declares `requires: [appId, ...]` is **not applicable** while any
32
+ * of those apps is not installed and enabled: it is neither met nor unmet, so
33
+ * it never lands in `requiredUnmet` or `optionalUnmet`. `CnSetupWizard` skips
34
+ * such a step, and counting it as unmet kept the wizard reopening (optional
35
+ * step) or gating the app (required step) for a step it would never show. App
36
+ * status is resolved exactly as the wizard does it: the `dependency_statuses`
37
+ * initial state first, then `useAppStatus`.
38
+ *
30
39
  * @param {string} appId Nextcloud app id (e.g. `"procest"`).
31
40
  * @param {object} manifest The app manifest (reads `manifest.setup`).
32
41
  * @return {{
@@ -35,6 +44,7 @@ const cache = new Map()
35
44
  * requiredUnmet: import('vue').ComputedRef<Array<object>>,
36
45
  * optionalUnmet: import('vue').ComputedRef<Array<object>>,
37
46
  * optionalUnmetReported: import('vue').ComputedRef<Array<object>>,
47
+ * notApplicable: import('vue').ComputedRef<Array<object>>,
38
48
  * completed: import('vue').ComputedRef<boolean>,
39
49
  * enabled: boolean,
40
50
  * loading: import('vue').Ref<boolean>,
@@ -45,6 +55,7 @@ const cache = new Map()
45
55
  *
46
56
  * @example
47
57
  * const { requiredUnmet, completed, loading } = useSetupStatus('procest', manifest)
58
+ * @spec openspec/changes/optional-step-requires/specs/cn-setup-wizard/spec.md#requirement-a-step-whose-required-apps-are-absent-is-not-applicable
48
59
  */
49
60
  export function useSetupStatus(appId, manifest) {
50
61
  const setup = (manifest && manifest.setup) || {}
@@ -68,6 +79,9 @@ export function useSetupStatus(appId, manifest) {
68
79
  cache.set(appId, entry)
69
80
  }
70
81
  const { status, loading, error, forbidden } = entry
82
+ // Same source the wizard's dependency gate reads. Initial state is fixed
83
+ // for the page lifetime, so one read per call is enough.
84
+ const serverAppStatuses = readServerAppStatuses(appId)
71
85
 
72
86
  const steps = computed(() => stepDefs.map((s) => {
73
87
  const bag = status.value.steps || {}
@@ -81,7 +95,12 @@ export function useSetupStatus(appId, manifest) {
81
95
  // that act on an unmet step — CnAppRoot auto-opening the non-gating
82
96
  // wizard — must act only on steps the server actually reported, or an
83
97
  // unknowable step parks the wizard over the app for ever.
84
- return { ...s, done: st.done === true, reported, detail: st.detail }
98
+ //
99
+ // `applicable` is false when the step `requires` an app that is not
100
+ // installed and enabled. The wizard skips such a step, so it can never
101
+ // be done here; it is not applicable rather than unmet.
102
+ const missingApps = missingRequiredApps(s.requires, serverAppStatuses).map((row) => row.id)
103
+ return { ...s, done: st.done === true, reported, detail: st.detail, applicable: missingApps.length === 0, missingApps }
85
104
  }))
86
105
  // Presentational step types carry no work, so the server never reports a
87
106
  // `done` flag for them. Counting them as "unmet" made `optionalUnmet`
@@ -89,7 +108,9 @@ export function useSetupStatus(appId, manifest) {
89
108
  // which kept CnAppRoot's non-gating setup wizard auto-opening over the
90
109
  // app on every fresh browser profile no matter how complete setup was.
91
110
  // Only actionable steps can be unmet.
92
- const isActionable = (s) => s.type !== 'info' && s.type !== 'summary'
111
+ // A step whose required apps are absent is skipped by the wizard, so it
112
+ // is neither met nor unmet (see `applicable` above).
113
+ const isActionable = (s) => s.type !== 'info' && s.type !== 'summary' && s.applicable !== false
93
114
  // `forbidden` empties BOTH lists, and that is what actually suppresses the
94
115
  // wizard: CnAppRoot gates on `requiredUnmet.length > 0` (blocking) and on
95
116
  // `requiredUnmet.length === 0 && optionalUnmet.length > 0` (auto-open) —
@@ -110,6 +131,8 @@ export function useSetupStatus(appId, manifest) {
110
131
  // suppressing it is what left dossiq's `seed` step (and with it the app's
111
132
  // only demo-data affordance) permanently unreachable.
112
133
  const optionalUnmetReported = computed(() => optionalUnmet.value.filter((s) => s.reported === true))
134
+ // Steps the wizard skips because an app in their `requires` is absent.
135
+ const notApplicable = computed(() => steps.value.filter((s) => s.applicable === false))
113
136
  const completed = computed(() => {
114
137
  if (!enabled) {
115
138
  return true
@@ -170,7 +193,7 @@ export function useSetupStatus(appId, manifest) {
170
193
  refresh()
171
194
  }
172
195
 
173
- return { steps, status, requiredUnmet, optionalUnmet, optionalUnmetReported, completed, enabled, loading, error, forbidden, refresh }
196
+ return { steps, status, requiredUnmet, optionalUnmet, optionalUnmetReported, notApplicable, completed, enabled, loading, error, forbidden, refresh }
174
197
  }
175
198
 
176
199
  /**
@@ -35,6 +35,23 @@
35
35
  color: var(--color-text-maxcontrast);
36
36
  }
37
37
 
38
+ /* Beside the inline search (`showCountWithSearch`): the search field shrinks
39
+ first and the counter stays on one line. */
40
+ .cn-actions-bar__count--beside-search {
41
+ flex-shrink: 0;
42
+ white-space: nowrap;
43
+ }
44
+
45
+ /* Taken out of the flex row so it adds no gap, but kept in the accessibility
46
+ tree so the live region exists before a total appears. */
47
+ .cn-actions-bar__count--empty {
48
+ position: absolute;
49
+ width: 1px;
50
+ height: 1px;
51
+ overflow: hidden;
52
+ clip-path: inset(50%);
53
+ }
54
+
38
55
  /* Inline search field */
39
56
  .cn-actions-bar__search {
40
57
  position: relative;
@@ -294,8 +311,9 @@
294
311
  display: contents;
295
312
  }
296
313
 
297
- /* #after-search filter buttons + the view toggle ride beside the
298
- search field (DOM order — search first via its own order). */
314
+ /* #after-search filter buttons, the beside-search counter and the view
315
+ toggle ride beside the search field (DOM order — search first via its
316
+ own order). */
299
317
  .cn-actions-bar__info > * {
300
318
  order: 1;
301
319
  }