@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.
- 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/CnDashboardPage/CnDashboardPage.vue2.js +99 -2
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +438 -383
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue2.js +9 -3
- package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue2.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnColumnFilterPopover.vue3.js +1 -1
- package/dist/esm/components/CnDataTable/CnColumnFilterPopover.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/CnDetailPage/CnDetailPage.vue2.js +78 -21
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +129 -96
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnFkResolveCell/CnFkResolveCell.vue2.js +3 -1
- package/dist/esm/components/CnFkResolveCell/CnFkResolveCell.vue2.js.map +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +144 -4
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
- package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js +63 -7
- package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js +2 -1
- package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.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/CnSegmentedControl/CnSegmentedControl.vue2.js +11 -0
- package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue2.js.map +1 -1
- package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.vue3.js +2 -1
- package/dist/esm/components/CnSegmentedControl/CnSegmentedControl.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 +7 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +7 -1
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/mixins/pageViews.js +400 -0
- package/dist/esm/mixins/pageViews.js.map +1 -0
- package/dist/esm/nextcloud-vue.css +38 -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/actionsDispatcher.js +11 -2
- package/dist/esm/utils/actionsDispatcher.js.map +1 -1
- package/dist/esm/utils/columnFilters.js +27 -3
- package/dist/esm/utils/columnFilters.js.map +1 -1
- package/dist/esm/utils/schema.js +40 -2
- package/dist/esm/utils/schema.js.map +1 -1
- package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +2377 -1247
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +38 -2
- package/l10n/en.json +7 -1
- package/l10n/nl.json +7 -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/CnDashboardPage/CnDashboardPage.vue +165 -8
- package/src/components/CnDataTable/CnColumnFilterPopover.vue +11 -5
- package/src/components/CnDataTable/CnDataTable.md +1 -1
- package/src/components/CnDataTable/CnDataTable.vue +4 -4
- package/src/components/CnDetailPage/CnDetailPage.vue +94 -5
- package/src/components/CnFkResolveCell/CnFkResolveCell.vue +3 -1
- package/src/components/CnFormDialog/CnFormDialog.vue +144 -4
- package/src/components/CnHeaderWidget/CnHeaderWidget.vue +67 -8
- package/src/components/CnIndexPage/CnIndexPage.md +2 -1
- package/src/components/CnIndexPage/CnIndexPage.vue +47 -1
- package/src/components/CnSegmentedControl/CnSegmentedControl.md +1 -1
- package/src/components/CnSegmentedControl/CnSegmentedControl.vue +12 -0
- 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/css/detail-page.css +5 -0
- package/src/mixins/pageViews.js +396 -0
- package/src/schemas/app-manifest-v2.schema.json +31 -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/actionsDispatcher.js +11 -2
- package/src/utils/columnFilters.js +29 -2
- package/src/utils/schema.js +40 -2
- package/src/utils/validateManifestV2.compiled.js +2 -2
|
@@ -145,6 +145,18 @@
|
|
|
145
145
|
</div>
|
|
146
146
|
</slot>
|
|
147
147
|
<div class="cn-detail-page__header-actions">
|
|
148
|
+
<!-- The page's view switch (`views`), unless a greeting header
|
|
149
|
+
widget on the page draws it. Its options control the view
|
|
150
|
+
region in the body. -->
|
|
151
|
+
<CnSegmentedControl
|
|
152
|
+
v-if="showsPageViewSwitch"
|
|
153
|
+
class="cn-page-view-switch"
|
|
154
|
+
:options="viewSwitchOptions"
|
|
155
|
+
:modelValue="activeViewId"
|
|
156
|
+
:ariaLabel="viewSwitchLabel"
|
|
157
|
+
:controls="viewRegionId"
|
|
158
|
+
data-testid="cn-detail-page-view-switch"
|
|
159
|
+
@update:modelValue="selectView" />
|
|
148
160
|
<!-- Next and previous inside the list this record was opened
|
|
149
161
|
from. Rendered only when the address names that list: a
|
|
150
162
|
record reached by a bare link offers neither, rather than
|
|
@@ -609,15 +621,19 @@
|
|
|
609
621
|
subscribed to the layout/widgets arrays — in-place pushes
|
|
610
622
|
(Add widget) keep the same array identity and can't re-render
|
|
611
623
|
it. One remount per edit flip re-subscribes it. -->
|
|
624
|
+
<!-- One grid per section: the body grid, then the chosen view
|
|
625
|
+
(`views`) in a region the view switch controls. A view grid is
|
|
626
|
+
keyed on its view, so switching mounts the other view's grid. -->
|
|
612
627
|
<CnDashboardGrid
|
|
613
|
-
v-
|
|
614
|
-
:key="`cn-detail-grid-${editingBody ? 'editing' : 'live'}`"
|
|
615
|
-
|
|
628
|
+
v-for="section in bodyGridSections"
|
|
629
|
+
:key="`cn-detail-grid-${section.key}-${editingBody ? 'editing' : 'live'}`"
|
|
630
|
+
v-bind="section.attrs"
|
|
631
|
+
:layout="section.layout"
|
|
616
632
|
:editable="editingBody"
|
|
617
633
|
:columns="12"
|
|
618
634
|
:columnOpts="columnOpts"
|
|
619
635
|
class="cn-detail-page__grid"
|
|
620
|
-
@layoutChange="onBodyLayoutChange">
|
|
636
|
+
@layoutChange="section.view ? onViewLayoutChange($event) : onBodyLayoutChange($event)">
|
|
621
637
|
<template #widget="{ item }">
|
|
622
638
|
<div
|
|
623
639
|
class="cn-detail-page__grid-item"
|
|
@@ -727,6 +743,22 @@
|
|
|
727
743
|
</template>
|
|
728
744
|
</CnDashboardGrid>
|
|
729
745
|
|
|
746
|
+
<!-- The chosen view has nothing to draw: say so in the region,
|
|
747
|
+
never leave a blank area under the switch. -->
|
|
748
|
+
<div
|
|
749
|
+
v-if="viewIsEmpty"
|
|
750
|
+
:id="viewRegionId"
|
|
751
|
+
role="region"
|
|
752
|
+
:aria-label="activeView ? activeView.label : null"
|
|
753
|
+
class="cn-page-view-region cn-page-view-region--empty"
|
|
754
|
+
data-testid="cn-detail-page-view-empty">
|
|
755
|
+
<NcEmptyContent :description="viewEmptyText">
|
|
756
|
+
<template #icon>
|
|
757
|
+
<ViewDashboardOutline :size="48" />
|
|
758
|
+
</template>
|
|
759
|
+
</NcEmptyContent>
|
|
760
|
+
</div>
|
|
761
|
+
|
|
730
762
|
<!-- Statistics table -->
|
|
731
763
|
<div v-if="hasStats" class="cn-detail-page__stats">
|
|
732
764
|
<!--
|
|
@@ -1036,6 +1068,7 @@ import InformationOutline from 'vue-material-design-icons/InformationOutline.vue
|
|
|
1036
1068
|
import Pencil from 'vue-material-design-icons/Pencil.vue'
|
|
1037
1069
|
import Plus from 'vue-material-design-icons/Plus.vue'
|
|
1038
1070
|
import Refresh from 'vue-material-design-icons/Refresh.vue'
|
|
1071
|
+
import ViewDashboardOutline from 'vue-material-design-icons/ViewDashboardOutline.vue'
|
|
1039
1072
|
import CnRelationLinkModal from '../../dialogs/CnRelationLinkModal.vue'
|
|
1040
1073
|
import CnWidgetStyleEditorModal from '../../dialogs/CnWidgetStyleEditorModal.vue'
|
|
1041
1074
|
import CnActionsMenu from '../CnActionsMenu/CnActionsMenu.vue'
|
|
@@ -1048,6 +1081,7 @@ import CnLifecycleActions from '../CnLifecycleActions/CnLifecycleActions.vue'
|
|
|
1048
1081
|
import CnLockedBanner from '../CnLockedBanner/CnLockedBanner.vue'
|
|
1049
1082
|
import CnNextStepCard from '../CnNextStepCard/CnNextStepCard.vue'
|
|
1050
1083
|
import CnRelatedCollections from '../CnRelatedCollections/CnRelatedCollections.vue'
|
|
1084
|
+
import CnSegmentedControl from '../CnSegmentedControl/CnSegmentedControl.vue'
|
|
1051
1085
|
import CnSummaryAggregates from '../CnSummaryAggregates/CnSummaryAggregates.vue'
|
|
1052
1086
|
import CnTranslatedBadge from '../CnTranslatedBadge/CnTranslatedBadge.vue'
|
|
1053
1087
|
import { useIntegrationRegistry } from '../../composables/useIntegrationRegistry.js'
|
|
@@ -1055,6 +1089,7 @@ import { useObjectLock } from '../../composables/useObjectLock.js'
|
|
|
1055
1089
|
import { useObjectPresence } from '../../composables/useObjectPresence.js'
|
|
1056
1090
|
import { useObjectSubscription } from '../../composables/useObjectSubscription.js'
|
|
1057
1091
|
import { gridLayout } from '../../mixins/gridLayout.js'
|
|
1092
|
+
import { pageViews } from '../../mixins/pageViews.js'
|
|
1058
1093
|
import { useObjectStore } from '../../store/index.js'
|
|
1059
1094
|
import { isAppInstalled } from '../../utils/appInstalled.js'
|
|
1060
1095
|
import { compactLayoutRows } from '../../utils/dashboardPlacement.js'
|
|
@@ -1227,6 +1262,8 @@ export default {
|
|
|
1227
1262
|
CnDetailWidgetHost,
|
|
1228
1263
|
CnFormDialog,
|
|
1229
1264
|
CnDashboardGrid,
|
|
1265
|
+
CnSegmentedControl,
|
|
1266
|
+
ViewDashboardOutline,
|
|
1230
1267
|
CnLifecycleActions,
|
|
1231
1268
|
CnActionButtons,
|
|
1232
1269
|
CnBreadcrumbs,
|
|
@@ -1242,7 +1279,7 @@ export default {
|
|
|
1242
1279
|
Pencil,
|
|
1243
1280
|
},
|
|
1244
1281
|
|
|
1245
|
-
mixins: [gridLayout],
|
|
1282
|
+
mixins: [gridLayout, pageViews],
|
|
1246
1283
|
|
|
1247
1284
|
inject: {
|
|
1248
1285
|
/**
|
|
@@ -3462,6 +3499,7 @@ export default {
|
|
|
3462
3499
|
&& this.currentObject
|
|
3463
3500
|
&& !this.hasDefaultSlotContent
|
|
3464
3501
|
&& !this.hasGridLayout
|
|
3502
|
+
&& !this.hasViews
|
|
3465
3503
|
},
|
|
3466
3504
|
|
|
3467
3505
|
/**
|
|
@@ -3541,6 +3579,55 @@ export default {
|
|
|
3541
3579
|
return this.bodyGridLayout.length > 0
|
|
3542
3580
|
},
|
|
3543
3581
|
|
|
3582
|
+
/**
|
|
3583
|
+
* The chosen view's layout as the grid draws it: items that draw
|
|
3584
|
+
* nothing leave and their rows close, as for the body grid.
|
|
3585
|
+
*
|
|
3586
|
+
* @return {Array<object>}
|
|
3587
|
+
* @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-a-view-with-nothing-to-draw-says-so
|
|
3588
|
+
*/
|
|
3589
|
+
viewDisplayLayout() {
|
|
3590
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-expressions -- reading the flag IS the effect, as in bodyGridLayout
|
|
3591
|
+
this.editingBody
|
|
3592
|
+
return this.activeView ? this.closeDroppedRows(this.activeView.layout) : []
|
|
3593
|
+
},
|
|
3594
|
+
|
|
3595
|
+
/**
|
|
3596
|
+
* Whether the chosen view has nothing to draw.
|
|
3597
|
+
*
|
|
3598
|
+
* @return {boolean}
|
|
3599
|
+
* @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-a-view-with-nothing-to-draw-says-so
|
|
3600
|
+
*/
|
|
3601
|
+
viewIsEmpty() {
|
|
3602
|
+
return this.hasViews && this.viewDisplayLayout.length === 0
|
|
3603
|
+
},
|
|
3604
|
+
|
|
3605
|
+
/**
|
|
3606
|
+
* The grids the body draws: the body grid when it has items, then the
|
|
3607
|
+
* chosen view's grid in the region the view switch controls (left out
|
|
3608
|
+
* when the view is empty; the empty state takes the region then).
|
|
3609
|
+
*
|
|
3610
|
+
* @return {Array<{key: string, view: boolean, layout: Array<object>, attrs: object}>}
|
|
3611
|
+
* @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-a-page-declares-views-that-each-hold-a-widget-grid
|
|
3612
|
+
*/
|
|
3613
|
+
bodyGridSections() {
|
|
3614
|
+
const sections = this.hasBodyGrid ? [{ key: 'body', view: false, layout: this.bodyGridLayout, attrs: {} }] : []
|
|
3615
|
+
if (this.hasViews && !this.viewIsEmpty) {
|
|
3616
|
+
sections.push({
|
|
3617
|
+
key: `view-${this.activeViewId}`,
|
|
3618
|
+
view: true,
|
|
3619
|
+
layout: this.viewDisplayLayout,
|
|
3620
|
+
attrs: {
|
|
3621
|
+
id: this.viewRegionId,
|
|
3622
|
+
role: 'region',
|
|
3623
|
+
'aria-label': this.activeView.label,
|
|
3624
|
+
'data-testid': 'cn-detail-page-view-region',
|
|
3625
|
+
},
|
|
3626
|
+
})
|
|
3627
|
+
}
|
|
3628
|
+
return sections
|
|
3629
|
+
},
|
|
3630
|
+
|
|
3544
3631
|
/**
|
|
3545
3632
|
* Whether the sidebar is rendered externally (via objectSidebarState inject)
|
|
3546
3633
|
* rather than inline. When external, CnDetailPage only manages state —
|
|
@@ -4762,6 +4849,8 @@ export default {
|
|
|
4762
4849
|
*/
|
|
4763
4850
|
findWidget(item) {
|
|
4764
4851
|
return this.bodyGridWidgets.find((w) => w.id === item.widgetId)
|
|
4852
|
+
|| this.findViewWidget(item.widgetId)
|
|
4853
|
+
|| undefined
|
|
4765
4854
|
},
|
|
4766
4855
|
|
|
4767
4856
|
/**
|
|
@@ -135,7 +135,9 @@ export default {
|
|
|
135
135
|
if (!store) {
|
|
136
136
|
return
|
|
137
137
|
}
|
|
138
|
-
|
|
138
|
+
// `schema` is a slug the manifest author wrote, not a `$ref` title:
|
|
139
|
+
// use it exactly as given (a camelCase slug must not be kebab-cased).
|
|
140
|
+
const type = resolveObjectOpType(store, { register: this.register, schema: this.schema }, { exactSchema: true })
|
|
139
141
|
await Promise.all(this.ids.map(async (id) => {
|
|
140
142
|
if (this.labels[id]) {
|
|
141
143
|
return
|
|
@@ -513,6 +513,7 @@ import { draftKey, formDraftMixin, readDraft } from '../../composables/useFormDr
|
|
|
513
513
|
import { useIntegrationRegistry } from '../../composables/useIntegrationRegistry.js'
|
|
514
514
|
import { TENANT_CONTEXT_KEY } from '../../composables/useTenantContext.js'
|
|
515
515
|
import { useObjectStore } from '../../store/useObjectStore.js'
|
|
516
|
+
import { resolveCreateOverrideHandler } from '../../utils/actionsDispatcher.js'
|
|
516
517
|
import {
|
|
517
518
|
definitionQueryParams,
|
|
518
519
|
DYNAMIC_KEY_PREFIX,
|
|
@@ -757,6 +758,18 @@ export default {
|
|
|
757
758
|
* identity when used standalone (no CnAppRoot ancestor).
|
|
758
759
|
*/
|
|
759
760
|
cnTranslate: { default: () => (key) => key },
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* The app manifest, the v2 registry, the legacy customComponents map and
|
|
764
|
+
* the registry-modal opener, all provided by CnAppRoot. A select-or-create
|
|
765
|
+
* picker reads them to create an object the way the app's own index page
|
|
766
|
+
* does (`config.createOverride` / `config.createModal`). Standalone (no
|
|
767
|
+
* CnAppRoot) they are absent and the generic nested form is used.
|
|
768
|
+
*/
|
|
769
|
+
cnManifest: { default: null },
|
|
770
|
+
cnRegistry: { default: null },
|
|
771
|
+
cnCustomComponents: { default: null },
|
|
772
|
+
cnOpenModal: { default: null },
|
|
760
773
|
},
|
|
761
774
|
|
|
762
775
|
props: {
|
|
@@ -2649,6 +2662,16 @@ export default {
|
|
|
2649
2662
|
store.registerObjectType(slug, schemaRef, register)
|
|
2650
2663
|
}
|
|
2651
2664
|
const labelField = this.referenceLabelField(field)
|
|
2665
|
+
// The app may own how this schema is created (pipelinq's client and
|
|
2666
|
+
// contact need a contact-first endpoint; a plain save 400s). A
|
|
2667
|
+
// create override keeps this form and saves through the app's
|
|
2668
|
+
// handler; a create modal (only when no override exists) opens the
|
|
2669
|
+
// app's own dialog instead of the generic form.
|
|
2670
|
+
const appCreate = this.appCreateFor(register, schemaRef)
|
|
2671
|
+
if (!appCreate.override && appCreate.modal) {
|
|
2672
|
+
return this.openAppCreateModal(appCreate.modal, store, slug, labelField, term)
|
|
2673
|
+
}
|
|
2674
|
+
const override = appCreate.override
|
|
2652
2675
|
let schema = null
|
|
2653
2676
|
try {
|
|
2654
2677
|
schema = await store.fetchSchema(slug)
|
|
@@ -2656,14 +2679,113 @@ export default {
|
|
|
2656
2679
|
schema = null
|
|
2657
2680
|
}
|
|
2658
2681
|
if (!schema || !schema.properties) {
|
|
2659
|
-
|
|
2682
|
+
const payload = { [labelField]: term }
|
|
2683
|
+
return override
|
|
2684
|
+
? override(payload, { register, schema: schemaRef, objectType: slug, effectiveSchema: null })
|
|
2685
|
+
: store.saveObject(slug, payload)
|
|
2660
2686
|
}
|
|
2661
2687
|
const initialData = {}
|
|
2662
2688
|
if (term && schema.properties[labelField]) {
|
|
2663
2689
|
initialData[labelField] = term
|
|
2664
2690
|
}
|
|
2665
2691
|
return new Promise((resolve) => {
|
|
2666
|
-
this.nestedCreate = { field, schema, register, slug, initialData, resolve }
|
|
2692
|
+
this.nestedCreate = { field, schema, register, slug, schemaRef, initialData, override, resolve }
|
|
2693
|
+
})
|
|
2694
|
+
},
|
|
2695
|
+
|
|
2696
|
+
/**
|
|
2697
|
+
* How the app creates objects of a schema, read off the manifest page
|
|
2698
|
+
* that lists them: the index page whose `config.schema` (and, when it
|
|
2699
|
+
* names one, `config.register`) matches. Its `config.createOverride` is
|
|
2700
|
+
* resolved to the registered handler; its `config.createModal` is the
|
|
2701
|
+
* registry key of a `kind: 'modal'` dialog.
|
|
2702
|
+
*
|
|
2703
|
+
* @spec openspec/changes/review-round-two/specs/schema-utilities/spec.md
|
|
2704
|
+
* @param {string} register The reference's register.
|
|
2705
|
+
* @param {string} schemaRef The reference's schema slug or id.
|
|
2706
|
+
* @return {{override: (((formData: object, ctx: object) => Promise<object>)|null), modal: (string|null)}} What the app declared.
|
|
2707
|
+
*/
|
|
2708
|
+
appCreateFor(register, schemaRef) {
|
|
2709
|
+
const none = { override: null, modal: null }
|
|
2710
|
+
const pages = this.cnManifest && Array.isArray(this.cnManifest.pages) ? this.cnManifest.pages : []
|
|
2711
|
+
const wanted = String(schemaRef).toLowerCase()
|
|
2712
|
+
const page = pages.find((p) => {
|
|
2713
|
+
const config = p && p.config
|
|
2714
|
+
if (!config || config.schema === undefined || config.schema === null) {
|
|
2715
|
+
return false
|
|
2716
|
+
}
|
|
2717
|
+
if (String(config.schema).toLowerCase() !== wanted) {
|
|
2718
|
+
return false
|
|
2719
|
+
}
|
|
2720
|
+
return !config.register || String(config.register) === String(register)
|
|
2721
|
+
})
|
|
2722
|
+
if (!page) {
|
|
2723
|
+
return none
|
|
2724
|
+
}
|
|
2725
|
+
const config = page.config
|
|
2726
|
+
const override = typeof config.createOverride === 'function'
|
|
2727
|
+
? config.createOverride
|
|
2728
|
+
: resolveCreateOverrideHandler(config.createOverride, this.cnRegistry, this.cnCustomComponents)
|
|
2729
|
+
const modalEntry = typeof config.createModal === 'string' && config.createModal !== ''
|
|
2730
|
+
? (this.cnRegistry || {})[config.createModal]
|
|
2731
|
+
: null
|
|
2732
|
+
const modal = modalEntry && modalEntry.kind === 'modal' && typeof this.cnOpenModal === 'function'
|
|
2733
|
+
? config.createModal
|
|
2734
|
+
: null
|
|
2735
|
+
return { override: override || null, modal }
|
|
2736
|
+
},
|
|
2737
|
+
|
|
2738
|
+
/**
|
|
2739
|
+
* Open the app's own create dialog (a registry modal) for a picker and
|
|
2740
|
+
* wait for it. The dialog reports the new object (or its id) with a
|
|
2741
|
+
* `created` event; `close` without one resolves null so the field stays
|
|
2742
|
+
* as it was. A reported id is fetched so the picker can label it.
|
|
2743
|
+
*
|
|
2744
|
+
* @spec openspec/changes/review-round-two/specs/schema-utilities/spec.md
|
|
2745
|
+
* @param {string} key The registry key of the modal.
|
|
2746
|
+
* @param {object} store The object store.
|
|
2747
|
+
* @param {string} slug The object type slug.
|
|
2748
|
+
* @param {string} labelField The property the typed term belongs in.
|
|
2749
|
+
* @param {string} term The typed search term.
|
|
2750
|
+
* @return {Promise<object|null>} The created object, or null.
|
|
2751
|
+
*/
|
|
2752
|
+
openAppCreateModal(key, store, slug, labelField, term) {
|
|
2753
|
+
return new Promise((resolve) => {
|
|
2754
|
+
let settled = false
|
|
2755
|
+
let reported = false
|
|
2756
|
+
const settle = (value) => {
|
|
2757
|
+
if (!settled) {
|
|
2758
|
+
settled = true
|
|
2759
|
+
resolve(value)
|
|
2760
|
+
}
|
|
2761
|
+
}
|
|
2762
|
+
this.cnOpenModal(key, {
|
|
2763
|
+
initialData: term ? { [labelField]: term } : {},
|
|
2764
|
+
onCreated: async (created) => {
|
|
2765
|
+
if (created && typeof created === 'object') {
|
|
2766
|
+
settle(created)
|
|
2767
|
+
return
|
|
2768
|
+
}
|
|
2769
|
+
if (typeof created !== 'string' && typeof created !== 'number') {
|
|
2770
|
+
return
|
|
2771
|
+
}
|
|
2772
|
+
reported = true
|
|
2773
|
+
let obj
|
|
2774
|
+
try {
|
|
2775
|
+
obj = await store.fetchObject(slug, String(created))
|
|
2776
|
+
} catch {
|
|
2777
|
+
obj = null
|
|
2778
|
+
}
|
|
2779
|
+
settle(obj || { id: String(created), [labelField]: term })
|
|
2780
|
+
},
|
|
2781
|
+
onClose: () => {
|
|
2782
|
+
// A dialog often emits `close` right after `created`,
|
|
2783
|
+
// while the id is still being fetched: that fetch settles.
|
|
2784
|
+
if (!reported) {
|
|
2785
|
+
settle(null)
|
|
2786
|
+
}
|
|
2787
|
+
},
|
|
2788
|
+
})
|
|
2667
2789
|
})
|
|
2668
2790
|
},
|
|
2669
2791
|
|
|
@@ -2682,10 +2804,28 @@ export default {
|
|
|
2682
2804
|
return
|
|
2683
2805
|
}
|
|
2684
2806
|
const store = this.getObjectStore()
|
|
2685
|
-
|
|
2807
|
+
let created = null
|
|
2808
|
+
let thrown = null
|
|
2809
|
+
if (pending.override) {
|
|
2810
|
+
// The app's create override owns persistence (same contract as
|
|
2811
|
+
// CnIndexPage's `createOverride`).
|
|
2812
|
+
try {
|
|
2813
|
+
created = await pending.override(formData, {
|
|
2814
|
+
register: pending.register,
|
|
2815
|
+
schema: pending.schemaRef,
|
|
2816
|
+
objectType: pending.slug,
|
|
2817
|
+
effectiveSchema: pending.schema,
|
|
2818
|
+
})
|
|
2819
|
+
} catch (err) {
|
|
2820
|
+
thrown = (err && err.response && err.response.data && err.response.data.error) || (err && err.message) || null
|
|
2821
|
+
}
|
|
2822
|
+
} else {
|
|
2823
|
+
created = store ? await store.saveObject(pending.slug, formData) : null
|
|
2824
|
+
}
|
|
2686
2825
|
if (!created) {
|
|
2687
2826
|
const error = store && store.errors ? store.errors[pending.slug] : null
|
|
2688
|
-
const message =
|
|
2827
|
+
const message = thrown
|
|
2828
|
+
|| (!pending.override && error && error.message)
|
|
2689
2829
|
|| t('nextcloud-vue', 'The item could not be created.')
|
|
2690
2830
|
const dialog = this.$refs.nestedCreateDialog
|
|
2691
2831
|
if (dialog && typeof dialog.setValidationErrors === 'function') {
|
|
@@ -35,12 +35,15 @@
|
|
|
35
35
|
</a>
|
|
36
36
|
</div>
|
|
37
37
|
<!-- A view switch at the right of the heading (`content.views`): "My
|
|
38
|
-
work / My team" as a segmented control whose options are routes
|
|
38
|
+
work / My team" as a segmented control whose options are routes,
|
|
39
|
+
or views of the page the widget sits on (`option.view`); a view
|
|
40
|
+
option controls the page's view region. -->
|
|
39
41
|
<div v-if="viewOptions.length > 0" class="cn-header-widget__views">
|
|
40
42
|
<CnSegmentedControl
|
|
41
43
|
:options="viewOptions"
|
|
42
44
|
:modelValue="activeView"
|
|
43
45
|
:ariaLabel="viewsLabel"
|
|
46
|
+
:controls="viewsRegionId"
|
|
44
47
|
data-testid="cn-header-widget-views"
|
|
45
48
|
@update:modelValue="onViewChange" />
|
|
46
49
|
</div>
|
|
@@ -66,6 +69,9 @@ const ALLOWED_TEXT_ALIGN = ['left', 'center', 'right']
|
|
|
66
69
|
const ALLOWED_VERTICAL_ALIGN = ['top', 'middle', 'bottom']
|
|
67
70
|
const ALLOWED_CTA_STYLES = ['primary', 'secondary', 'ghost']
|
|
68
71
|
|
|
72
|
+
/** Marks a switch option that selects a page view rather than a route. */
|
|
73
|
+
const VIEW_OPTION_PREFIX = 'view:'
|
|
74
|
+
|
|
69
75
|
const HEIGHT_PIXELS = Object.freeze({
|
|
70
76
|
small: 120,
|
|
71
77
|
medium: 200,
|
|
@@ -121,6 +127,12 @@ export default {
|
|
|
121
127
|
* untranslated key renders as itself.
|
|
122
128
|
*/
|
|
123
129
|
cnTranslate: { default: () => (key) => key },
|
|
130
|
+
/**
|
|
131
|
+
* The views of the page this widget sits on, provided by
|
|
132
|
+
* CnDashboardPage / CnDetailPage (`views`). An option with a `view`
|
|
133
|
+
* selects one of them. Null outside such a page.
|
|
134
|
+
*/
|
|
135
|
+
cnPageViews: { default: null },
|
|
124
136
|
},
|
|
125
137
|
|
|
126
138
|
props: {
|
|
@@ -131,9 +143,12 @@ export default {
|
|
|
131
143
|
* greeting, showDate, plain, ground, views}`. All fields are optional except
|
|
132
144
|
* `title` (or `greeting`); unknown enum values collapse to documented
|
|
133
145
|
* defaults and the renderer never throws. `views` is
|
|
134
|
-
* `{ ariaLabel?, options: [{ label, route, params? }] }`:
|
|
135
|
-
* control at the right of the heading whose checked option
|
|
136
|
-
* current route; choosing another pushes its route.
|
|
146
|
+
* `{ ariaLabel?, options: [{ label, route, params? } | { label, view }] }`:
|
|
147
|
+
* a segmented control at the right of the heading whose checked option
|
|
148
|
+
* is the current route; choosing another pushes its route. An option
|
|
149
|
+
* with `view` selects that view of the page the widget sits on (the
|
|
150
|
+
* page's `views`) instead, and the page then draws no switch of its
|
|
151
|
+
* own. `ground: true`
|
|
137
152
|
* draws the greeting on the page ground: the plain look with no
|
|
138
153
|
* padding, a 32px heading and the date line 6px above it (a dashboard
|
|
139
154
|
* also drops the widget's card for it).
|
|
@@ -229,12 +244,45 @@ export default {
|
|
|
229
244
|
*/
|
|
230
245
|
viewOptions() {
|
|
231
246
|
const raw = this.content && this.content.views && this.content.views.options
|
|
232
|
-
if (!Array.isArray(raw)
|
|
247
|
+
if (!Array.isArray(raw)) {
|
|
233
248
|
return []
|
|
234
249
|
}
|
|
250
|
+
const pageViewIds = this.pageViewIds
|
|
235
251
|
return raw
|
|
236
|
-
.filter((o) => o && typeof o.label === 'string' && o.label !== ''
|
|
237
|
-
.map((o) =>
|
|
252
|
+
.filter((o) => o && typeof o.label === 'string' && o.label !== '')
|
|
253
|
+
.map((o) => {
|
|
254
|
+
if (typeof o.view === 'string' && o.view !== '') {
|
|
255
|
+
return pageViewIds.includes(o.view) ? { value: VIEW_OPTION_PREFIX + o.view, label: this.cnTranslate(o.label) } : null
|
|
256
|
+
}
|
|
257
|
+
if (typeof o.route === 'string' && o.route !== '' && this.$router) {
|
|
258
|
+
return { value: o.route, label: this.cnTranslate(o.label) }
|
|
259
|
+
}
|
|
260
|
+
return null
|
|
261
|
+
})
|
|
262
|
+
.filter(Boolean)
|
|
263
|
+
},
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* The ids of the page's views, empty outside a page with views.
|
|
267
|
+
*
|
|
268
|
+
* @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-a-greeting-header-can-switch-the-pages-views
|
|
269
|
+
* @return {string[]}
|
|
270
|
+
*/
|
|
271
|
+
pageViewIds() {
|
|
272
|
+
const views = this.cnPageViews && Array.isArray(this.cnPageViews.views) ? this.cnPageViews.views : []
|
|
273
|
+
return views.map((view) => view.id)
|
|
274
|
+
},
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* The id of the page's view region when an option selects a view,
|
|
278
|
+
* for the options' `aria-controls`; '' otherwise.
|
|
279
|
+
*
|
|
280
|
+
* @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-the-view-switch-is-accessible
|
|
281
|
+
* @return {string}
|
|
282
|
+
*/
|
|
283
|
+
viewsRegionId() {
|
|
284
|
+
const hasViewOption = this.viewOptions.some((o) => o.value.startsWith(VIEW_OPTION_PREFIX))
|
|
285
|
+
return hasViewOption && this.cnPageViews ? (this.cnPageViews.regionId || '') : ''
|
|
238
286
|
},
|
|
239
287
|
|
|
240
288
|
/**
|
|
@@ -249,6 +297,11 @@ export default {
|
|
|
249
297
|
if (options.length === 0) {
|
|
250
298
|
return null
|
|
251
299
|
}
|
|
300
|
+
const activeView = this.cnPageViews ? this.cnPageViews.activeId : null
|
|
301
|
+
const viewMatch = activeView ? options.find((o) => o.value === VIEW_OPTION_PREFIX + activeView) : null
|
|
302
|
+
if (viewMatch) {
|
|
303
|
+
return viewMatch.value
|
|
304
|
+
}
|
|
252
305
|
const current = this.$route && this.$route.name
|
|
253
306
|
const match = options.find((o) => o.value === current)
|
|
254
307
|
return match ? match.value : options[0].value
|
|
@@ -263,7 +316,7 @@ export default {
|
|
|
263
316
|
*/
|
|
264
317
|
viewsLabel() {
|
|
265
318
|
const label = this.content && this.content.views && this.content.views.ariaLabel
|
|
266
|
-
return label ? this.cnTranslate(label) : t('nextcloud-vue', '
|
|
319
|
+
return label ? this.cnTranslate(label) : t('nextcloud-vue', 'Views')
|
|
267
320
|
},
|
|
268
321
|
|
|
269
322
|
/**
|
|
@@ -697,6 +750,12 @@ export default {
|
|
|
697
750
|
* @return {void}
|
|
698
751
|
*/
|
|
699
752
|
onViewChange(route) {
|
|
753
|
+
if (typeof route === 'string' && route.startsWith(VIEW_OPTION_PREFIX)) {
|
|
754
|
+
if (this.cnPageViews && typeof this.cnPageViews.select === 'function') {
|
|
755
|
+
this.cnPageViews.select(route.slice(VIEW_OPTION_PREFIX.length))
|
|
756
|
+
}
|
|
757
|
+
return
|
|
758
|
+
}
|
|
700
759
|
if (!this.$router || !route || route === (this.$route && this.$route.name)) {
|
|
701
760
|
return
|
|
702
761
|
}
|
|
@@ -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
|
|
|
@@ -43,4 +43,4 @@ export default {
|
|
|
43
43
|
</script>
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
-
Props: `options` lists the choices, `modelValue` is the chosen value (use `v-model`), `aria-label` names the group (or point `aria-labelledby` at a visible heading), and `stretch` fills the width.
|
|
46
|
+
Props: `options` lists the choices, `modelValue` is the chosen value (use `v-model`), `aria-label` names the group (or point `aria-labelledby` at a visible heading), `controls` names the id of the element the options switch (set as `aria-controls` on every option), and `stretch` fills the width.
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
:aria-checked="isChecked(option) ? 'true' : 'false'"
|
|
21
21
|
:tabindex="index === focusableIndex ? 0 : -1"
|
|
22
22
|
:disabled="option.disabled || null"
|
|
23
|
+
:aria-controls="controls || null"
|
|
23
24
|
:data-value="option.key"
|
|
24
25
|
@click="select(option)">
|
|
25
26
|
<!-- @slot option Replaces the content of one option. -->
|
|
@@ -95,6 +96,17 @@ export default {
|
|
|
95
96
|
default: '',
|
|
96
97
|
},
|
|
97
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Id of the element the options switch, such as the region holding a
|
|
101
|
+
* page view's widgets. Set as `aria-controls` on every option.
|
|
102
|
+
*
|
|
103
|
+
* @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-the-view-switch-is-accessible
|
|
104
|
+
*/
|
|
105
|
+
controls: {
|
|
106
|
+
type: String,
|
|
107
|
+
default: '',
|
|
108
|
+
},
|
|
109
|
+
|
|
98
110
|
/** Stretch the control to the full width, with options of equal width. */
|
|
99
111
|
stretch: {
|
|
100
112
|
type: Boolean,
|