@conduction/nextcloud-vue 2.64.0 → 2.66.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/CnAppNav/CnAppNav.vue2.js +99 -3
- package/dist/esm/components/CnAppNav/CnAppNav.vue2.js.map +1 -1
- package/dist/esm/components/CnAppNav/CnAppNav.vue3.js +23 -7
- package/dist/esm/components/CnAppNav/CnAppNav.vue3.js.map +1 -1
- package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue2.js +13 -0
- package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue2.js.map +1 -1
- package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue3.js +2 -1
- package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue3.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +125 -4
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +439 -381
- 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/CnDetailPage/CnDetailPage.vue2.js +142 -26
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +195 -124
- 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 +84 -11
- package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js +3 -2
- package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.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/CnStagesWidget/CnStagesWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStagesWidget/CnStagesWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js +22 -3
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js +10 -3
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
- package/dist/esm/l10n/en.json.js +3 -0
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +3 -0
- 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 +182 -0
- package/dist/esm/store/useObjectStore.js +118 -33
- package/dist/esm/store/useObjectStore.js.map +1 -1
- package/dist/esm/utils/actionsDispatcher.js +25 -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 +2001 -684
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +182 -0
- package/l10n/en.json +3 -0
- package/l10n/nl.json +3 -0
- package/package.json +1 -1
- package/src/components/CnAppNav/CnAppNav.vue +148 -7
- package/src/components/CnBreadcrumbs/CnBreadcrumbs.vue +14 -0
- package/src/components/CnDashboardPage/CnDashboardPage.vue +194 -10
- package/src/components/CnDataTable/CnColumnFilterPopover.vue +11 -5
- package/src/components/CnDetailPage/CnDetailPage.md +5 -1
- package/src/components/CnDetailPage/CnDetailPage.vue +208 -11
- package/src/components/CnFkResolveCell/CnFkResolveCell.vue +3 -1
- package/src/components/CnFormDialog/CnFormDialog.vue +144 -4
- package/src/components/CnHeaderWidget/CnHeaderWidget.vue +100 -13
- package/src/components/CnSegmentedControl/CnSegmentedControl.md +1 -1
- package/src/components/CnSegmentedControl/CnSegmentedControl.vue +12 -0
- package/src/components/CnStagesWidget/CnStagesWidget.vue +9 -0
- package/src/components/CnStatWidget/CnStatWidget.vue +33 -5
- package/src/css/detail-page.css +27 -0
- package/src/css/kpi-card.css +47 -0
- package/src/mixins/pageViews.js +396 -0
- package/src/schemas/app-manifest-v2.schema.json +51 -2
- package/src/store/useObjectStore.js +118 -33
- package/src/utils/actionsDispatcher.js +25 -2
- package/src/utils/columnFilters.js +29 -2
- package/src/utils/schema.js +40 -2
- package/src/utils/validateManifestV2.compiled.js +2 -2
|
@@ -47,9 +47,14 @@
|
|
|
47
47
|
v-if="breadcrumbCrumbs.length > 0 && !objectNotFound"
|
|
48
48
|
class="cn-detail-page__breadcrumbs"
|
|
49
49
|
:crumbs="breadcrumbCrumbs"
|
|
50
|
+
:rootText="breadcrumbCrumbs[0].icon === undefined"
|
|
50
51
|
data-testid="cn-detail-page-breadcrumbs" />
|
|
51
52
|
<!-- Header -->
|
|
52
|
-
<div
|
|
53
|
+
<div
|
|
54
|
+
v-if="!objectNotFound"
|
|
55
|
+
class="cn-detail-page__header"
|
|
56
|
+
:class="{ 'cn-detail-page__header--card': headerCard, 'cn-detail-page__header--with-widget': headerWidgetDef !== null }"
|
|
57
|
+
data-testid="cn-detail-page-header">
|
|
53
58
|
<!-- Header (left block) — overridable via #header slot. Default
|
|
54
59
|
renders the icon + title + description. The right-hand
|
|
55
60
|
#actions slot remains separate so headerComponent and
|
|
@@ -140,6 +145,18 @@
|
|
|
140
145
|
</div>
|
|
141
146
|
</slot>
|
|
142
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" />
|
|
143
160
|
<!-- Next and previous inside the list this record was opened
|
|
144
161
|
from. Rendered only when the address names that list: a
|
|
145
162
|
record reached by a bare link offers neither, rather than
|
|
@@ -370,6 +387,50 @@
|
|
|
370
387
|
</template>
|
|
371
388
|
</CnActionsMenu>
|
|
372
389
|
</div>
|
|
390
|
+
<!-- A widget inside the header (manifest `config.headerWidget`), on
|
|
391
|
+
a row of its own under the title and the actions: the stages
|
|
392
|
+
bars of the board's case card. Drawn without a card of its own,
|
|
393
|
+
and taken out of the body grid. Nothing renders without the
|
|
394
|
+
key. -->
|
|
395
|
+
<div
|
|
396
|
+
v-if="headerWidgetDef !== null"
|
|
397
|
+
class="cn-detail-page__header-widget"
|
|
398
|
+
data-testid="cn-detail-page-header-widget">
|
|
399
|
+
<!--
|
|
400
|
+
@slot `widget-${headerWidgetDef.id}`
|
|
401
|
+
@description The same per-widget slot the body grid and the side
|
|
402
|
+
column offer, for the widget placed in the header
|
|
403
|
+
(`headerWidget`). Same name and same bindings.
|
|
404
|
+
-->
|
|
405
|
+
<slot
|
|
406
|
+
:name="`widget-${headerWidgetDef.id}`"
|
|
407
|
+
:item="{ id: headerWidgetDef.id, widgetId: headerWidgetDef.id }"
|
|
408
|
+
:widget="headerWidgetDef"
|
|
409
|
+
:objectId="objectId"
|
|
410
|
+
:object="resolvedObject"
|
|
411
|
+
:objectData="resolvedObject"
|
|
412
|
+
:objectType="resolvedObjectType"
|
|
413
|
+
:register="register"
|
|
414
|
+
:schema="schema">
|
|
415
|
+
<CnDetailWidgetHost
|
|
416
|
+
:widget="headerWidgetDef"
|
|
417
|
+
chrome="bare"
|
|
418
|
+
:objectId="objectId"
|
|
419
|
+
:object="currentObject"
|
|
420
|
+
:objectType="resolvedObjectType"
|
|
421
|
+
:schemaObject="currentSchema"
|
|
422
|
+
:register="register"
|
|
423
|
+
:schema="schema"
|
|
424
|
+
:store="effectiveObjectStore"
|
|
425
|
+
:surface="surface"
|
|
426
|
+
:integrationContext="effectiveIntegrationContext"
|
|
427
|
+
:hideEmpty="hideEmpty"
|
|
428
|
+
:cnRegistry="cnRegistry"
|
|
429
|
+
:availableWidgets="widgets"
|
|
430
|
+
@geoSaved="onGeoSaved"
|
|
431
|
+
@openIntegration="onAutoBodyOpenIntegration" />
|
|
432
|
+
</slot>
|
|
433
|
+
</div>
|
|
373
434
|
</div>
|
|
374
435
|
|
|
375
436
|
<!-- The locked card, under the title, for ANY active lock — not only a
|
|
@@ -560,15 +621,19 @@
|
|
|
560
621
|
subscribed to the layout/widgets arrays — in-place pushes
|
|
561
622
|
(Add widget) keep the same array identity and can't re-render
|
|
562
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. -->
|
|
563
627
|
<CnDashboardGrid
|
|
564
|
-
v-
|
|
565
|
-
:key="`cn-detail-grid-${editingBody ? 'editing' : 'live'}`"
|
|
566
|
-
|
|
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"
|
|
567
632
|
:editable="editingBody"
|
|
568
633
|
:columns="12"
|
|
569
634
|
:columnOpts="columnOpts"
|
|
570
635
|
class="cn-detail-page__grid"
|
|
571
|
-
@layoutChange="onBodyLayoutChange">
|
|
636
|
+
@layoutChange="section.view ? onViewLayoutChange($event) : onBodyLayoutChange($event)">
|
|
572
637
|
<template #widget="{ item }">
|
|
573
638
|
<div
|
|
574
639
|
class="cn-detail-page__grid-item"
|
|
@@ -678,6 +743,22 @@
|
|
|
678
743
|
</template>
|
|
679
744
|
</CnDashboardGrid>
|
|
680
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
|
+
|
|
681
762
|
<!-- Statistics table -->
|
|
682
763
|
<div v-if="hasStats" class="cn-detail-page__stats">
|
|
683
764
|
<!--
|
|
@@ -987,6 +1068,7 @@ import InformationOutline from 'vue-material-design-icons/InformationOutline.vue
|
|
|
987
1068
|
import Pencil from 'vue-material-design-icons/Pencil.vue'
|
|
988
1069
|
import Plus from 'vue-material-design-icons/Plus.vue'
|
|
989
1070
|
import Refresh from 'vue-material-design-icons/Refresh.vue'
|
|
1071
|
+
import ViewDashboardOutline from 'vue-material-design-icons/ViewDashboardOutline.vue'
|
|
990
1072
|
import CnRelationLinkModal from '../../dialogs/CnRelationLinkModal.vue'
|
|
991
1073
|
import CnWidgetStyleEditorModal from '../../dialogs/CnWidgetStyleEditorModal.vue'
|
|
992
1074
|
import CnActionsMenu from '../CnActionsMenu/CnActionsMenu.vue'
|
|
@@ -999,6 +1081,7 @@ import CnLifecycleActions from '../CnLifecycleActions/CnLifecycleActions.vue'
|
|
|
999
1081
|
import CnLockedBanner from '../CnLockedBanner/CnLockedBanner.vue'
|
|
1000
1082
|
import CnNextStepCard from '../CnNextStepCard/CnNextStepCard.vue'
|
|
1001
1083
|
import CnRelatedCollections from '../CnRelatedCollections/CnRelatedCollections.vue'
|
|
1084
|
+
import CnSegmentedControl from '../CnSegmentedControl/CnSegmentedControl.vue'
|
|
1002
1085
|
import CnSummaryAggregates from '../CnSummaryAggregates/CnSummaryAggregates.vue'
|
|
1003
1086
|
import CnTranslatedBadge from '../CnTranslatedBadge/CnTranslatedBadge.vue'
|
|
1004
1087
|
import { useIntegrationRegistry } from '../../composables/useIntegrationRegistry.js'
|
|
@@ -1006,6 +1089,7 @@ import { useObjectLock } from '../../composables/useObjectLock.js'
|
|
|
1006
1089
|
import { useObjectPresence } from '../../composables/useObjectPresence.js'
|
|
1007
1090
|
import { useObjectSubscription } from '../../composables/useObjectSubscription.js'
|
|
1008
1091
|
import { gridLayout } from '../../mixins/gridLayout.js'
|
|
1092
|
+
import { pageViews } from '../../mixins/pageViews.js'
|
|
1009
1093
|
import { useObjectStore } from '../../store/index.js'
|
|
1010
1094
|
import { isAppInstalled } from '../../utils/appInstalled.js'
|
|
1011
1095
|
import { compactLayoutRows } from '../../utils/dashboardPlacement.js'
|
|
@@ -1178,6 +1262,8 @@ export default {
|
|
|
1178
1262
|
CnDetailWidgetHost,
|
|
1179
1263
|
CnFormDialog,
|
|
1180
1264
|
CnDashboardGrid,
|
|
1265
|
+
CnSegmentedControl,
|
|
1266
|
+
ViewDashboardOutline,
|
|
1181
1267
|
CnLifecycleActions,
|
|
1182
1268
|
CnActionButtons,
|
|
1183
1269
|
CnBreadcrumbs,
|
|
@@ -1193,7 +1279,7 @@ export default {
|
|
|
1193
1279
|
Pencil,
|
|
1194
1280
|
},
|
|
1195
1281
|
|
|
1196
|
-
mixins: [gridLayout],
|
|
1282
|
+
mixins: [gridLayout, pageViews],
|
|
1197
1283
|
|
|
1198
1284
|
inject: {
|
|
1199
1285
|
/**
|
|
@@ -2104,11 +2190,14 @@ export default {
|
|
|
2104
2190
|
* A breadcrumb line above the header (manifest `config.breadcrumb`):
|
|
2105
2191
|
* `{ label, route?, params?, href? }` names the list the record
|
|
2106
2192
|
* belongs to; the record's display name follows as the current
|
|
2107
|
-
* crumb. `label` goes through the host translate function
|
|
2108
|
-
*
|
|
2193
|
+
* crumb. `label` goes through the host translate function and shows
|
|
2194
|
+
* as text; `icon` (an MDI name, e.g. `Home`) draws that icon instead,
|
|
2195
|
+
* with the label as its accessible name. Null (the default) draws no
|
|
2196
|
+
* trail.
|
|
2109
2197
|
*
|
|
2110
2198
|
* @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-detail-page-drops-its-eyebrow-and-shows-a-breadcrumb
|
|
2111
|
-
* @
|
|
2199
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps-2/specs/zuiddrecht-pixel-gaps-2/spec.md#requirement-a-declared-breadcrumb-label-shows-as-text
|
|
2200
|
+
* @type {{label: string, route?: string, params?: object, href?: string, icon?: string}|null}
|
|
2112
2201
|
*/
|
|
2113
2202
|
breadcrumb: {
|
|
2114
2203
|
type: Object,
|
|
@@ -2130,6 +2219,33 @@ export default {
|
|
|
2130
2219
|
default: () => [],
|
|
2131
2220
|
},
|
|
2132
2221
|
|
|
2222
|
+
/**
|
|
2223
|
+
* Draw the header as a card (manifest `config.headerCard: true`): the
|
|
2224
|
+
* pills, the title and the actions on the page's surface colour inside
|
|
2225
|
+
* a bordered, rounded box, as the board's case header. Off by default,
|
|
2226
|
+
* which keeps the header on the page ground.
|
|
2227
|
+
*
|
|
2228
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps-2/specs/zuiddrecht-pixel-gaps-2/spec.md#requirement-a-detail-header-can-be-a-card-that-holds-a-widget
|
|
2229
|
+
*/
|
|
2230
|
+
headerCard: {
|
|
2231
|
+
type: Boolean,
|
|
2232
|
+
default: false,
|
|
2233
|
+
},
|
|
2234
|
+
|
|
2235
|
+
/**
|
|
2236
|
+
* The id of a widget declared in `widgets` to render inside the header,
|
|
2237
|
+
* on its own row under the title and the actions (manifest
|
|
2238
|
+
* `config.headerWidget`), e.g. the stages bars. It renders without a
|
|
2239
|
+
* card of its own and leaves the body grid. Empty (the default), or an
|
|
2240
|
+
* id that names no widget, renders nothing extra.
|
|
2241
|
+
*
|
|
2242
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps-2/specs/zuiddrecht-pixel-gaps-2/spec.md#requirement-a-detail-header-can-be-a-card-that-holds-a-widget
|
|
2243
|
+
*/
|
|
2244
|
+
headerWidget: {
|
|
2245
|
+
type: String,
|
|
2246
|
+
default: '',
|
|
2247
|
+
},
|
|
2248
|
+
|
|
2133
2249
|
/**
|
|
2134
2250
|
* Whether the current user administers this instance. Shows the
|
|
2135
2251
|
* `adminOnly` header actions. `null` (the default) reads it from
|
|
@@ -2669,6 +2785,21 @@ export default {
|
|
|
2669
2785
|
.filter(Boolean)
|
|
2670
2786
|
},
|
|
2671
2787
|
|
|
2788
|
+
/**
|
|
2789
|
+
* The widget the header holds (`headerWidget`): the definition in
|
|
2790
|
+
* `widgets` with that id, or null without the key or a match.
|
|
2791
|
+
*
|
|
2792
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps-2/specs/zuiddrecht-pixel-gaps-2/spec.md#requirement-a-detail-header-can-be-a-card-that-holds-a-widget
|
|
2793
|
+
* @return {object|null} The widget definition, or null.
|
|
2794
|
+
*/
|
|
2795
|
+
headerWidgetDef() {
|
|
2796
|
+
if (!this.headerWidget) {
|
|
2797
|
+
return null
|
|
2798
|
+
}
|
|
2799
|
+
const declared = Array.isArray(this.widgets) ? this.widgets : []
|
|
2800
|
+
return declared.find((widget) => widget && widget.id === this.headerWidget) || null
|
|
2801
|
+
},
|
|
2802
|
+
|
|
2672
2803
|
/**
|
|
2673
2804
|
* The side column's widget definitions. A string names a widget
|
|
2674
2805
|
* declared in `widgets`; an object is a definition of its own.
|
|
@@ -3241,10 +3372,12 @@ export default {
|
|
|
3241
3372
|
/**
|
|
3242
3373
|
* The breadcrumb trail: the declared crumb (a router target from
|
|
3243
3374
|
* `route` + `params`, or an `href`), then the record's display name
|
|
3244
|
-
* as the current crumb. Empty without a `breadcrumb` label.
|
|
3375
|
+
* as the current crumb. Empty without a `breadcrumb` label. The first
|
|
3376
|
+
* crumb carries `icon` only when the manifest declares one.
|
|
3245
3377
|
*
|
|
3246
3378
|
* @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-detail-page-drops-its-eyebrow-and-shows-a-breadcrumb
|
|
3247
|
-
* @
|
|
3379
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps-2/specs/zuiddrecht-pixel-gaps-2/spec.md#requirement-a-declared-breadcrumb-label-shows-as-text
|
|
3380
|
+
* @return {Array<{label: string, to?: object, href?: string, icon?: string}>}
|
|
3248
3381
|
*/
|
|
3249
3382
|
breadcrumbCrumbs() {
|
|
3250
3383
|
const crumb = this.breadcrumb
|
|
@@ -3252,6 +3385,9 @@ export default {
|
|
|
3252
3385
|
return []
|
|
3253
3386
|
}
|
|
3254
3387
|
const first = { label: this.effectiveTranslate(crumb.label) }
|
|
3388
|
+
if (typeof crumb.icon === 'string' && crumb.icon !== '') {
|
|
3389
|
+
first.icon = crumb.icon
|
|
3390
|
+
}
|
|
3255
3391
|
if (crumb.route) {
|
|
3256
3392
|
first.to = { name: crumb.route, ...(crumb.params ? { params: crumb.params } : {}) }
|
|
3257
3393
|
} else if (crumb.href) {
|
|
@@ -3363,6 +3499,7 @@ export default {
|
|
|
3363
3499
|
&& this.currentObject
|
|
3364
3500
|
&& !this.hasDefaultSlotContent
|
|
3365
3501
|
&& !this.hasGridLayout
|
|
3502
|
+
&& !this.hasViews
|
|
3366
3503
|
},
|
|
3367
3504
|
|
|
3368
3505
|
/**
|
|
@@ -3442,6 +3579,55 @@ export default {
|
|
|
3442
3579
|
return this.bodyGridLayout.length > 0
|
|
3443
3580
|
},
|
|
3444
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
|
+
|
|
3445
3631
|
/**
|
|
3446
3632
|
* Whether the sidebar is rendered externally (via objectSidebarState inject)
|
|
3447
3633
|
* rather than inline. When external, CnDetailPage only manages state —
|
|
@@ -4663,6 +4849,8 @@ export default {
|
|
|
4663
4849
|
*/
|
|
4664
4850
|
findWidget(item) {
|
|
4665
4851
|
return this.bodyGridWidgets.find((w) => w.id === item.widgetId)
|
|
4852
|
+
|| this.findViewWidget(item.widgetId)
|
|
4853
|
+
|| undefined
|
|
4666
4854
|
},
|
|
4667
4855
|
|
|
4668
4856
|
/**
|
|
@@ -4711,6 +4899,10 @@ export default {
|
|
|
4711
4899
|
* it — CnPageRenderer's split pane forwards no slots, which is exactly
|
|
4712
4900
|
* that fault and wants fixing, not concealing.
|
|
4713
4901
|
*
|
|
4902
|
+
* The widget the header holds (`headerWidget`) draws nothing here
|
|
4903
|
+
* either: it is already drawn in the header.
|
|
4904
|
+
*
|
|
4905
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps-2/specs/zuiddrecht-pixel-gaps-2/spec.md#requirement-a-detail-header-can-be-a-card-that-holds-a-widget
|
|
4714
4906
|
* @param {object} item A layout item.
|
|
4715
4907
|
* @return {boolean} False only when the item provably draws nothing.
|
|
4716
4908
|
*/
|
|
@@ -4721,6 +4913,11 @@ export default {
|
|
|
4721
4913
|
if (this.editingBody) {
|
|
4722
4914
|
return true
|
|
4723
4915
|
}
|
|
4916
|
+
// The widget the header holds (`headerWidget`) is drawn there, so
|
|
4917
|
+
// its grid cell would be the same widget twice.
|
|
4918
|
+
if (this.headerWidgetDef !== null && item.widgetId === this.headerWidgetDef.id) {
|
|
4919
|
+
return false
|
|
4920
|
+
}
|
|
4724
4921
|
const widget = this.findWidget(item)
|
|
4725
4922
|
if (!widget) {
|
|
4726
4923
|
return true
|
|
@@ -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') {
|