@conduction/nextcloud-vue 2.66.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.
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js +19 -0
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js.map +1 -1
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js +10 -3
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js.map +1 -1
- package/dist/esm/components/CnAppNav/CnAppNav.vue2.js +2 -2
- package/dist/esm/components/CnAppNav/CnAppNav.vue2.js.map +1 -1
- package/dist/esm/components/CnAppNav/CnAppNav.vue3.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +4 -4
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +31 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +335 -313
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js +5 -11
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js.map +1 -1
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue3.js.map +1 -1
- package/dist/esm/composables/useDependencyCheck.js +44 -1
- package/dist/esm/composables/useDependencyCheck.js.map +1 -1
- package/dist/esm/composables/useListView.js +43 -1
- package/dist/esm/composables/useListView.js.map +1 -1
- package/dist/esm/composables/useSetupStatus.js +26 -3
- package/dist/esm/composables/useSetupStatus.js.map +1 -1
- package/dist/esm/l10n/en.json.js +4 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +4 -1
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/nextcloud-vue.css +20 -2
- package/dist/esm/store/plugins/liveUpdates.js +19 -9
- package/dist/esm/store/plugins/liveUpdates.js.map +1 -1
- package/dist/esm/store/useObjectStore.js +20 -9
- package/dist/esm/store/useObjectStore.js.map +1 -1
- package/dist/esm/utils/validateManifestV2.compiled.js +1 -1
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +857 -652
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +20 -2
- package/l10n/en.json +4 -1
- package/l10n/nl.json +4 -1
- package/package.json +1 -1
- package/src/components/CnActionsBar/CnActionsBar.md +2 -1
- package/src/components/CnActionsBar/CnActionsBar.vue +25 -1
- package/src/components/CnAppNav/CnAppNav.vue +2 -2
- package/src/components/CnDataTable/CnDataTable.md +1 -1
- package/src/components/CnDataTable/CnDataTable.vue +4 -4
- package/src/components/CnIndexPage/CnIndexPage.md +2 -1
- package/src/components/CnIndexPage/CnIndexPage.vue +47 -1
- package/src/components/CnSetupWizard/CnSetupWizard.vue +5 -11
- package/src/composables/useDependencyCheck.js +43 -0
- package/src/composables/useListView.js +43 -1
- package/src/composables/useSetupStatus.js +26 -3
- package/src/css/actions-bar.css +20 -2
- package/src/schemas/app-manifest-v2.schema.json +3 -3
- package/src/schemas/app-manifest.schema.json +1 -1
- package/src/store/plugins/liveUpdates.js +19 -9
- package/src/store/useObjectStore.js +19 -8
- 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:
|
|
4
|
-
sha256:
|
|
3
|
+
size: 35372297 bytes
|
|
4
|
+
sha256: 7065df25e721d338e4612aac2ee75c58c87fb5e17477c811e7bcf66cee41783d
|
package/dist/nextcloud-vue.css
CHANGED
|
@@ -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
|
|
2203
|
-
search field (DOM order — search first via its
|
|
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.
|
|
3
|
+
"version": "2.67.0",
|
|
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="
|
|
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 "
|
|
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', '
|
|
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
|
|
1726
|
-
* `null` omits the attribute entirely (unsorted, not sortable, or not the
|
|
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.
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
/**
|
package/src/css/actions-bar.css
CHANGED
|
@@ -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
|
|
298
|
-
search field (DOM order — search first via its
|
|
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
|
}
|