@conduction/nextcloud-vue 2.68.0 → 2.70.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 +25 -0
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js.map +1 -1
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js +155 -152
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js.map +1 -1
- package/dist/esm/components/CnAppNav/CnAppNav.vue2.js +77 -0
- package/dist/esm/components/CnAppNav/CnAppNav.vue2.js.map +1 -1
- package/dist/esm/components/CnAppNav/CnAppNav.vue3.js +8 -8
- package/dist/esm/components/CnAppNav/CnAppNav.vue3.js.map +1 -1
- package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue.js +1 -0
- package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue.js.map +1 -1
- package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue2.js +28 -0
- package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue2.js.map +1 -1
- package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue3.js +4 -2
- package/dist/esm/components/CnBreadcrumbs/CnBreadcrumbs.vue3.js.map +1 -1
- package/dist/esm/components/CnCalendarWidget/CnCalendarWidget.vue2.js +25 -1
- package/dist/esm/components/CnCalendarWidget/CnCalendarWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnCalendarWidget/CnCalendarWidget.vue3.js +32 -17
- package/dist/esm/components/CnCalendarWidget/CnCalendarWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnCapabilityTable/CnCapabilityTable.vue2.js +14 -1
- package/dist/esm/components/CnCapabilityTable/CnCapabilityTable.vue2.js.map +1 -1
- package/dist/esm/components/CnCapabilityTable/CnCapabilityTable.vue3.js.map +1 -1
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js +43 -2
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js +3 -3
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +45 -35
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +19 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +35 -1
- 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 +94 -6
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +64 -19
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +28 -0
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js +3 -2
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
- package/dist/esm/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue2.js +12 -0
- package/dist/esm/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue2.js.map +1 -1
- package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue2.js +9 -0
- package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue2.js.map +1 -1
- package/dist/esm/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue3.js.map +1 -1
- package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js +54 -7
- package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js +26 -8
- package/dist/esm/components/CnHeaderWidget/CnHeaderWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +143 -0
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +68 -25
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnNextStepCard/CnNextStepCard.vue2.js.map +1 -1
- package/dist/esm/components/CnNextStepCard/CnNextStepCard.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +4 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue2.js +17 -3
- package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.vue3.js +25 -24
- package/dist/esm/components/CnStackedBarWidget/CnStackedBarWidget.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 +29 -2
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js +7 -0
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnStatsBlock/CnStatsBlock.vue2.js +24 -1
- package/dist/esm/components/CnStatsBlock/CnStatsBlock.vue2.js.map +1 -1
- package/dist/esm/components/CnStatsBlock/CnStatsBlock.vue3.js +1 -1
- package/dist/esm/components/CnStatsBlock/CnStatsBlock.vue3.js.map +1 -1
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js +81 -1
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js +25 -3
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue2.js +15 -2
- package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.vue3.js +31 -32
- package/dist/esm/components/CnWeekStripWidget/CnWeekStripWidget.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 +278 -11
- package/dist/esm/mixins/pageViews.js.map +1 -1
- package/dist/esm/nextcloud-vue.css +145 -0
- package/dist/esm/store/plugins/dashboardLayouts.js +35 -1
- package/dist/esm/store/plugins/dashboardLayouts.js.map +1 -1
- package/dist/esm/utils/capabilityComparison.js +29 -1
- package/dist/esm/utils/capabilityComparison.js.map +1 -1
- package/dist/esm/utils/formatMetric.js +137 -16
- package/dist/esm/utils/formatMetric.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 +2840 -1477
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +145 -0
- package/l10n/en.json +3 -0
- package/l10n/nl.json +3 -0
- package/package.json +1 -1
- package/src/components/CnActionsBar/CnActionsBar.md +2 -0
- package/src/components/CnActionsBar/CnActionsBar.vue +28 -2
- package/src/components/CnAppNav/CnAppNav.vue +85 -8
- package/src/components/CnBreadcrumbs/CnBreadcrumbs.vue +45 -0
- package/src/components/CnCalendarWidget/CnCalendarWidget.vue +32 -4
- package/src/components/CnCapabilityTable/CnCapabilityTable.vue +14 -0
- package/src/components/CnDashboardGrid/CnDashboardGrid.md +2 -0
- package/src/components/CnDashboardGrid/CnDashboardGrid.vue +54 -2
- package/src/components/CnDashboardPage/CnDashboardPage.md +2 -0
- package/src/components/CnDashboardPage/CnDashboardPage.vue +59 -35
- package/src/components/CnDataTable/CnDataTable.vue +35 -1
- package/src/components/CnDetailPage/CnDetailPage.md +8 -0
- package/src/components/CnDetailPage/CnDetailPage.vue +124 -8
- package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +30 -1
- package/src/components/CnFeaturesAndRoadmapPage/CnFeaturesAndRoadmapPage.vue +12 -0
- package/src/components/CnFeaturesAndRoadmapView/CnFeaturesAndRoadmapView.vue +9 -0
- package/src/components/CnHeaderWidget/CnHeaderWidget.vue +95 -8
- package/src/components/CnIndexPage/CnIndexPage.vue +171 -6
- package/src/components/CnNextStepCard/CnNextStepCard.vue +11 -0
- package/src/components/CnObjectListWidget/CnObjectListWidget.vue +4 -1
- package/src/components/CnStackedBarWidget/CnStackedBarWidget.vue +34 -7
- package/src/components/CnStagesWidget/CnStagesWidget.vue +19 -0
- package/src/components/CnStatWidget/CnStatWidget.vue +38 -2
- package/src/components/CnStatsBlock/CnStatsBlock.md +2 -0
- package/src/components/CnStatsBlock/CnStatsBlock.vue +25 -2
- package/src/components/CnStatsBlockWidget/CnStatsBlockWidget.vue +95 -3
- package/src/components/CnWeekStripWidget/CnWeekStripWidget.vue +24 -3
- package/src/css/detail-page.css +10 -0
- package/src/css/index-page.css +12 -0
- package/src/css/kpi-card.css +15 -0
- package/src/mixins/pageViews.js +277 -10
- package/src/schemas/app-manifest-v2.schema.json +73 -6
- package/src/store/plugins/dashboardLayouts.js +34 -0
- package/src/utils/capabilityComparison.js +28 -0
- package/src/utils/formatMetric.js +143 -15
- package/src/utils/validateManifestV2.compiled.js +2 -2
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
v-for="(item, index) in layout"
|
|
32
32
|
:key="resolveItemKey(item)"
|
|
33
33
|
class="grid-stack-item"
|
|
34
|
+
:class="{ 'cn-dashboard-grid__item--fit': sizeToContentAttr(item) !== undefined }"
|
|
34
35
|
role="group"
|
|
35
36
|
:aria-label="resolveItemLabel(item, index)"
|
|
36
37
|
:aria-describedby="itemDescribedBy"
|
|
@@ -255,6 +256,19 @@ export default {
|
|
|
255
256
|
type: Boolean,
|
|
256
257
|
default: true,
|
|
257
258
|
},
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* GridStack's `float`. True (the default) keeps every item on the row
|
|
262
|
+
* it was placed on, with empty rows above it if need be. False packs
|
|
263
|
+
* items upward, so a size-to-content cell that shrinks, or a widget
|
|
264
|
+
* that is not shown, closes its gap instead of leaving a band.
|
|
265
|
+
*
|
|
266
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-a-dashboard-closes-the-gap-a-shrinking-or-hidden-widget-leaves
|
|
267
|
+
*/
|
|
268
|
+
float: {
|
|
269
|
+
type: Boolean,
|
|
270
|
+
default: true,
|
|
271
|
+
},
|
|
258
272
|
},
|
|
259
273
|
|
|
260
274
|
emits: [
|
|
@@ -278,6 +292,8 @@ export default {
|
|
|
278
292
|
return {
|
|
279
293
|
grid: null,
|
|
280
294
|
contentObserver: null,
|
|
295
|
+
/** True while the grid moves an item to where the layout put it. */
|
|
296
|
+
applyingLayout: false,
|
|
281
297
|
domId: `cn-dashboard-grid-${++domIdCounter}`,
|
|
282
298
|
/** Text currently held in the polite live region. */
|
|
283
299
|
announcement: '',
|
|
@@ -485,7 +501,7 @@ export default {
|
|
|
485
501
|
column: this.columns,
|
|
486
502
|
cellHeight: this.cellHeight,
|
|
487
503
|
margin: this.margin,
|
|
488
|
-
float:
|
|
504
|
+
float: this.float,
|
|
489
505
|
animate: true,
|
|
490
506
|
disableDrag: !this.editable,
|
|
491
507
|
disableResize: !this.editable,
|
|
@@ -558,6 +574,10 @@ export default {
|
|
|
558
574
|
// switched off as the grid narrows, and back on as it widens.
|
|
559
575
|
this.syncInteractivity()
|
|
560
576
|
|
|
577
|
+
if (this.applyingLayout) {
|
|
578
|
+
return
|
|
579
|
+
}
|
|
580
|
+
|
|
561
581
|
// GridStack fires `change` for its own responsive rescale too, and at a
|
|
562
582
|
// 12 → 1 breakpoint every item reads `gridX: 0, gridWidth: 1`. Emitting
|
|
563
583
|
// that has the host persist the reflow AS the authored layout, which
|
|
@@ -957,7 +977,29 @@ export default {
|
|
|
957
977
|
const node = this.grid.engine.nodes.find((n) => String(n.id) === String(item.id))
|
|
958
978
|
if (!node) {
|
|
959
979
|
this.grid.makeWidget(el)
|
|
960
|
-
} else if (node.el
|
|
980
|
+
} else if (node.el === el) {
|
|
981
|
+
// The layout MOVED an item the grid already tracks: the
|
|
982
|
+
// page re-compacts its display layout when a widget's
|
|
983
|
+
// condition hides it, and the item below was given a new
|
|
984
|
+
// gridY that never reached GridStack, so a band stayed
|
|
985
|
+
// where the hidden widget had been. A drag reports the
|
|
986
|
+
// grid's own position back, which matches and does
|
|
987
|
+
// nothing here.
|
|
988
|
+
const x = item.gridX ?? node.x
|
|
989
|
+
const y = item.gridY ?? node.y
|
|
990
|
+
if (x !== node.x || y !== node.y) {
|
|
991
|
+
// The layout said where it goes; GridStack's `change`
|
|
992
|
+
// for it is not a user's arrangement and is not
|
|
993
|
+
// reported back (it would write the display-only
|
|
994
|
+
// compaction into the authored layout).
|
|
995
|
+
this.applyingLayout = true
|
|
996
|
+
try {
|
|
997
|
+
this.grid.update(el, { x, y })
|
|
998
|
+
} finally {
|
|
999
|
+
this.applyingLayout = false
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
} else {
|
|
961
1003
|
// Drop the stale node without touching its detached DOM,
|
|
962
1004
|
// then adopt the new element so GridStack sizes/places it.
|
|
963
1005
|
this.grid.removeWidget(node.el, false, false)
|
|
@@ -1016,6 +1058,16 @@ export default {
|
|
|
1016
1058
|
border-radius: var(--border-radius-container-large, 16px);
|
|
1017
1059
|
}
|
|
1018
1060
|
|
|
1061
|
+
/* A size-to-content cell (`sizeToContent`) is measured by GridStack from the
|
|
1062
|
+
content's first child, and a widget wrapper there filled the cell
|
|
1063
|
+
(`height: 100%`), so the measure returned the cell's own height and the
|
|
1064
|
+
cell never shrank: "Waiting for me" stayed a tall card around one row. In
|
|
1065
|
+
such a cell the child takes its own height. A cell without the key keeps
|
|
1066
|
+
the filling child. */
|
|
1067
|
+
.cn-dashboard-grid__item--fit > :deep(.grid-stack-item-content > *) {
|
|
1068
|
+
height: auto;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1019
1071
|
:deep(.grid-stack-item-content) {
|
|
1020
1072
|
background: var(--color-main-background);
|
|
1021
1073
|
border: none;
|
|
@@ -153,6 +153,8 @@ Three behaviours are worth knowing because each is load-bearing:
|
|
|
153
153
|
|
|
154
154
|
Call `resetUserLayout()` to drop the arrangement; the page emits `user-layout-reset` and returns to the manifest.
|
|
155
155
|
|
|
156
|
+
The same `userLayout` covers the page's [views](../../../docs/components/cn-dashboard-page.md#each-view-arranged-per-user-userlayout): each view's grid is draggable in edit mode and is stored per view, under the page id with `.view.<view id>` appended. Edit mode shows **Reset layout**, which returns the page's own grid and the chosen view to the manifest (`user-layout-reset`, `view-layout-reset`). `resetViewUserLayout(viewId)` resets one view.
|
|
157
|
+
|
|
156
158
|
## Slots
|
|
157
159
|
|
|
158
160
|
| Slot | Scope | Description |
|
|
@@ -67,6 +67,19 @@
|
|
|
67
67
|
</template>
|
|
68
68
|
{{ isEditing ? doneLabel : editLabel }}
|
|
69
69
|
</NcButton>
|
|
70
|
+
<!-- With `userLayout`, edit mode offers a way back to the
|
|
71
|
+
manifest: the page's own grid and the chosen view return
|
|
72
|
+
to the layout the app ships. Other views keep theirs. -->
|
|
73
|
+
<NcButton
|
|
74
|
+
v-if="userLayout && isEditing"
|
|
75
|
+
variant="tertiary"
|
|
76
|
+
data-testid="cn-dashboard-page-reset-layout"
|
|
77
|
+
@click="resetLayout">
|
|
78
|
+
<template #icon>
|
|
79
|
+
<Restore :size="20" />
|
|
80
|
+
</template>
|
|
81
|
+
{{ t('nextcloud-vue', 'Reset layout') }}
|
|
82
|
+
</NcButton>
|
|
70
83
|
<!-- In-app edit button (ADR-041). Renders only when Buildiq is
|
|
71
84
|
reachable; self-wires from the cnManifestEditor / cnOpenBuildAvailable
|
|
72
85
|
provided by CnAppRoot. Sits inline with the page's action buttons. -->
|
|
@@ -292,6 +305,7 @@
|
|
|
292
305
|
:columns="columns"
|
|
293
306
|
:cellHeight="cellHeight"
|
|
294
307
|
:margin="gridMargin"
|
|
308
|
+
:float="gridFloat"
|
|
295
309
|
@layoutChange="section.view ? onViewLayoutChange($event) : onLayoutChange($event)">
|
|
296
310
|
<template #widget="{ item }">
|
|
297
311
|
<!-- In-app edit overlay (ADR-041): a single launchpad-style
|
|
@@ -754,6 +768,7 @@ import Check from 'vue-material-design-icons/Check.vue'
|
|
|
754
768
|
import Cog from 'vue-material-design-icons/Cog.vue'
|
|
755
769
|
import Download from 'vue-material-design-icons/Download.vue'
|
|
756
770
|
import Pencil from 'vue-material-design-icons/Pencil.vue'
|
|
771
|
+
import Restore from 'vue-material-design-icons/Restore.vue'
|
|
757
772
|
import ViewDashboardOutline from 'vue-material-design-icons/ViewDashboardOutline.vue'
|
|
758
773
|
import CnWidgetStyleEditorModal from '../../dialogs/CnWidgetStyleEditorModal.vue'
|
|
759
774
|
import CnBodySections from '../CnBodySections/CnBodySections.vue'
|
|
@@ -768,9 +783,8 @@ import CnWidgetRefItem from '../CnWidgetRefItem/CnWidgetRefItem.vue'
|
|
|
768
783
|
import CnWidgetRenderer from '../CnWidgetRenderer/CnWidgetRenderer.vue'
|
|
769
784
|
import CnWidgetWrapper from '../CnWidgetWrapper/CnWidgetWrapper.vue'
|
|
770
785
|
import { useIntegrationRegistry } from '../../composables/useIntegrationRegistry.js'
|
|
771
|
-
import { readUserPreference, writeUserPreference } from '../../composables/useUserPreferences.js'
|
|
772
786
|
import { pageViews } from '../../mixins/pageViews.js'
|
|
773
|
-
import {
|
|
787
|
+
import { mergeUserLayout, resolveUserLayoutApi } from '../../store/plugins/dashboardLayouts.js'
|
|
774
788
|
import { isAppInstalled } from '../../utils/appInstalled.js'
|
|
775
789
|
import { compareVisibleWhen, readVisibleWhenValue } from '../../utils/visibleWhen.js'
|
|
776
790
|
import { canonicalWidgetType } from '../../utils/widgetTypeAliases.js'
|
|
@@ -905,6 +919,7 @@ export default {
|
|
|
905
919
|
NcLoadingIcon,
|
|
906
920
|
NcSelect,
|
|
907
921
|
Pencil,
|
|
922
|
+
Restore,
|
|
908
923
|
Check,
|
|
909
924
|
Cog,
|
|
910
925
|
CalendarRange,
|
|
@@ -1016,6 +1031,19 @@ export default {
|
|
|
1016
1031
|
default: true,
|
|
1017
1032
|
},
|
|
1018
1033
|
|
|
1034
|
+
/**
|
|
1035
|
+
* GridStack's `float` for this dashboard (manifest
|
|
1036
|
+
* `config.gridFloat`). True (the default) keeps every widget on its
|
|
1037
|
+
* row. False packs widgets upward, so a size-to-content widget that
|
|
1038
|
+
* shrinks closes the gap under it instead of leaving an empty band.
|
|
1039
|
+
*
|
|
1040
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-a-dashboard-closes-the-gap-a-shrinking-or-hidden-widget-leaves
|
|
1041
|
+
*/
|
|
1042
|
+
gridFloat: {
|
|
1043
|
+
type: Boolean,
|
|
1044
|
+
default: true,
|
|
1045
|
+
},
|
|
1046
|
+
|
|
1019
1047
|
/**
|
|
1020
1048
|
* Widget definitions array. Each widget defines metadata for rendering.
|
|
1021
1049
|
*
|
|
@@ -1958,7 +1986,7 @@ export default {
|
|
|
1958
1986
|
* @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-a-view-with-nothing-to-draw-says-so
|
|
1959
1987
|
*/
|
|
1960
1988
|
viewDisplayLayout() {
|
|
1961
|
-
const items = this.
|
|
1989
|
+
const items = this.activeViewLayout
|
|
1962
1990
|
if (this.gridEditable) {
|
|
1963
1991
|
return items
|
|
1964
1992
|
}
|
|
@@ -2000,8 +2028,9 @@ export default {
|
|
|
2000
2028
|
key: `view-${this.activeViewId}`,
|
|
2001
2029
|
view: true,
|
|
2002
2030
|
layout: this.viewDisplayLayout,
|
|
2003
|
-
//
|
|
2004
|
-
|
|
2031
|
+
// With `userLayout` the user arranges the view too; the
|
|
2032
|
+
// arrangement is kept per view (pageViews mixin).
|
|
2033
|
+
editable: this.gridEditable,
|
|
2005
2034
|
attrs: {
|
|
2006
2035
|
id: this.viewRegionId,
|
|
2007
2036
|
role: 'region',
|
|
@@ -2721,6 +2750,7 @@ export default {
|
|
|
2721
2750
|
// the user's arrangement is finished when they say it is.
|
|
2722
2751
|
if (leaving) {
|
|
2723
2752
|
this.saveUserLayout()
|
|
2753
|
+
this.saveViewUserLayouts()
|
|
2724
2754
|
}
|
|
2725
2755
|
|
|
2726
2756
|
/**
|
|
@@ -2780,6 +2810,17 @@ export default {
|
|
|
2780
2810
|
}
|
|
2781
2811
|
},
|
|
2782
2812
|
|
|
2813
|
+
/**
|
|
2814
|
+
* The edit-mode reset: the page's own grid and the chosen view return
|
|
2815
|
+
* to the manifest layout. Other views keep this user's arrangement.
|
|
2816
|
+
*
|
|
2817
|
+
* @return {Promise<void>}
|
|
2818
|
+
* @spec openspec/changes/page-view-user-layouts/specs/view-switch-containers/spec.md#requirement-resetting-a-view-restores-its-manifest-layout
|
|
2819
|
+
*/
|
|
2820
|
+
async resetLayout() {
|
|
2821
|
+
await Promise.all([this.resetUserLayout(), this.resetViewUserLayout()])
|
|
2822
|
+
},
|
|
2823
|
+
|
|
2783
2824
|
/**
|
|
2784
2825
|
* Drop this user's arrangement and return to the manifest.
|
|
2785
2826
|
*
|
|
@@ -2832,35 +2873,7 @@ export default {
|
|
|
2832
2873
|
* @return {{ load: (appId: string, pageId: string) => Promise<object|null>, save: (appId: string, pageId: string, layout: Array<object>) => Promise<boolean>, reset: (appId: string, pageId: string) => Promise<boolean> }} The api.
|
|
2833
2874
|
*/
|
|
2834
2875
|
userLayoutApi() {
|
|
2835
|
-
|
|
2836
|
-
if (store && typeof store.loadDashboardLayout === 'function') {
|
|
2837
|
-
return {
|
|
2838
|
-
load: (...a) => store.loadDashboardLayout(...a),
|
|
2839
|
-
save: (...a) => store.saveDashboardLayout(...a),
|
|
2840
|
-
reset: (...a) => store.resetDashboardLayout(...a),
|
|
2841
|
-
}
|
|
2842
|
-
}
|
|
2843
|
-
|
|
2844
|
-
return {
|
|
2845
|
-
load: (appId, pageId) => readUserPreference(appId, dashboardLayoutKey(pageId), null),
|
|
2846
|
-
save: (appId, pageId, layout) => writeUserPreference(
|
|
2847
|
-
appId,
|
|
2848
|
-
dashboardLayoutKey(pageId),
|
|
2849
|
-
{ items: (layout || []).map((i) => ({
|
|
2850
|
-
widgetId: i.widgetId,
|
|
2851
|
-
gridX: i.gridX,
|
|
2852
|
-
gridY: i.gridY,
|
|
2853
|
-
gridWidth: i.gridWidth,
|
|
2854
|
-
gridHeight: i.gridHeight,
|
|
2855
|
-
})) },
|
|
2856
|
-
),
|
|
2857
|
-
|
|
2858
|
-
reset: (appId, pageId) => writeUserPreference(
|
|
2859
|
-
appId,
|
|
2860
|
-
dashboardLayoutKey(pageId),
|
|
2861
|
-
{ items: [] },
|
|
2862
|
-
),
|
|
2863
|
-
}
|
|
2876
|
+
return resolveUserLayoutApi(this.userLayoutStore)
|
|
2864
2877
|
},
|
|
2865
2878
|
|
|
2866
2879
|
onLayoutChange(updated) {
|
|
@@ -3603,6 +3616,12 @@ export default {
|
|
|
3603
3616
|
|
|
3604
3617
|
getWidgetButtons(item) {
|
|
3605
3618
|
const def = this.getWidgetDef(item.widgetId)
|
|
3619
|
+
// `showButtons: false` on the placement or the definition drops
|
|
3620
|
+
// the footer links ("More events"), for a placement whose card
|
|
3621
|
+
// says enough without them (zuiddrecht-pixel-gaps-3).
|
|
3622
|
+
if (item?.showButtons === false || def?.showButtons === false) {
|
|
3623
|
+
return []
|
|
3624
|
+
}
|
|
3606
3625
|
return def?.buttons || []
|
|
3607
3626
|
},
|
|
3608
3627
|
|
|
@@ -3906,11 +3925,16 @@ export default {
|
|
|
3906
3925
|
// library's own defaults, so a manifest need not set them — but a
|
|
3907
3926
|
// manifest that DOES set them must reach the component, or the
|
|
3908
3927
|
// declaration is a silent no-op that reads like configuration.
|
|
3909
|
-
for (const key of ['countLabel', 'variant', 'showZeroCount', 'horizontal', 'vertical', 'filled', 'route', 'iconClass']) {
|
|
3928
|
+
for (const key of ['countLabel', 'variant', 'showZeroCount', 'horizontal', 'vertical', 'filled', 'route', 'iconClass', 'format', 'layout']) {
|
|
3910
3929
|
if (props[key] !== undefined) {
|
|
3911
3930
|
out[key] = props[key]
|
|
3912
3931
|
}
|
|
3913
3932
|
}
|
|
3933
|
+
// `content.layout: "stacked"`, the key a stat tile takes, works on
|
|
3934
|
+
// a stats block too (zuiddrecht-pixel-gaps-3).
|
|
3935
|
+
if (out.layout === undefined && typeof content.layout === 'string') {
|
|
3936
|
+
out.layout = content.layout
|
|
3937
|
+
}
|
|
3914
3938
|
// `countLabel` is the unit beside the number ("0 cases", "0 tasks").
|
|
3915
3939
|
// It is manifest-authored prose and was being forwarded raw, so a
|
|
3916
3940
|
// Dutch account read "0 cases" beside a translated widget title.
|
|
@@ -361,6 +361,40 @@ import { CnLockIndicator } from '../CnLockIndicator/index.js'
|
|
|
361
361
|
// consuming app does not pull in the library's global css/index.css.
|
|
362
362
|
import '../../css/table.css'
|
|
363
363
|
|
|
364
|
+
/**
|
|
365
|
+
* Fill a `secondary` template such as `"{identifier} · {requester}"`.
|
|
366
|
+
*
|
|
367
|
+
* A field without a value takes its separator with it: the text between two
|
|
368
|
+
* fields belongs to the field after it, so an empty requester leaves
|
|
369
|
+
* "2026-0002", not "2026-0002 ·", and an empty first field does not leave a
|
|
370
|
+
* leading separator either. Text before the first field and after the last
|
|
371
|
+
* one is kept as written. A template whose fields all have values fills
|
|
372
|
+
* exactly as before.
|
|
373
|
+
*
|
|
374
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-an-empty-field-takes-its-separator-with-it
|
|
375
|
+
* @param {string} template The template, with `{field}` placeholders.
|
|
376
|
+
* @param {(key: string) => string} valueOf The text of a field ('' when empty).
|
|
377
|
+
* @return {string} The filled template.
|
|
378
|
+
*/
|
|
379
|
+
function fillSecondaryTemplate(template, valueOf) {
|
|
380
|
+
// split() with a capture group alternates literal, field, literal, …
|
|
381
|
+
const parts = template.split(/\{([^}]+)\}/)
|
|
382
|
+
const lead = parts[0]
|
|
383
|
+
const tail = parts[parts.length - 1]
|
|
384
|
+
let body = ''
|
|
385
|
+
for (let i = 1; i < parts.length; i += 2) {
|
|
386
|
+
const value = valueOf(parts[i].trim())
|
|
387
|
+
if (value === '') {
|
|
388
|
+
continue
|
|
389
|
+
}
|
|
390
|
+
// The literal before this field: the separator, unless nothing has
|
|
391
|
+
// been written yet (then there is nothing to separate from).
|
|
392
|
+
const separator = i === 1 ? '' : parts[i - 1]
|
|
393
|
+
body += (body === '' ? '' : separator) + value
|
|
394
|
+
}
|
|
395
|
+
return lead + body + tail
|
|
396
|
+
}
|
|
397
|
+
|
|
364
398
|
/**
|
|
365
399
|
* CnDataTable — Generic sortable data table for list views.
|
|
366
400
|
*
|
|
@@ -1451,7 +1485,7 @@ export default {
|
|
|
1451
1485
|
}
|
|
1452
1486
|
const asText = (v) => (v === null || v === undefined ? '' : String(v))
|
|
1453
1487
|
if (spec.includes('{')) {
|
|
1454
|
-
const filled = spec
|
|
1488
|
+
const filled = fillSecondaryTemplate(spec, (key) => asText(this.getCellValue(row, key)))
|
|
1455
1489
|
// A template whose every field is empty leaves only separators:
|
|
1456
1490
|
// nothing worth a line.
|
|
1457
1491
|
return /[\p{L}\p{N}]/u.test(filled) ? filled.trim() : ''
|
|
@@ -211,6 +211,12 @@ export default {
|
|
|
211
211
|
| `excludeFields` (`exclude-fields`) | Array | `[]` | Fields the create and edit forms never ask for. Applied after `includeFields`. |
|
|
212
212
|
| `fieldOverrides` (`field-overrides`) | Object | `{}` | Per-field widget / label overrides for the create and edit forms, keyed by field name. Forwarded to `CnFormDialog`. |
|
|
213
213
|
|
|
214
|
+
## Each view arranged per user
|
|
215
|
+
|
|
216
|
+
| Prop | Type | Default | Description |
|
|
217
|
+
|------|------|---------|-------------|
|
|
218
|
+
| `userLayout` | Boolean | `false` | Let the user arrange each of the page's `views` for themselves. The header shows **Arrange view** next to the view switch; **Done** stores the arrangement per view and user (a user preference, mirrored in the browser), **Reset view** returns the chosen view to its manifest layout. The manifest `layout` is never changed. See [the guide](../../../docs/components/cn-detail-page.md#each-view-arranged-per-user-userlayout). |
|
|
219
|
+
|
|
214
220
|
## Slots
|
|
215
221
|
|
|
216
222
|
| Slot | Description |
|
|
@@ -438,3 +444,5 @@ When the schema-driven fetch answers 404 the page shows a not-found state whose
|
|
|
438
444
|
### Header card and header widget
|
|
439
445
|
|
|
440
446
|
`headerCard` (default `false`) draws the header as a bordered card. `headerWidget` (default `''`) names a widget in `widgets` to render inside the header under the title, without a card of its own and out of the body grid: the stages bars of the board's case card.
|
|
447
|
+
|
|
448
|
+
`showWidgetActions` (Boolean, default `true`): `false` (manifest `config.showWidgetActions: false`) drops the overflow Actions menu from the cards of the body grid and the side column, unless a widget definition sets `showActions: true`.
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
class="cn-detail-page__breadcrumbs"
|
|
49
49
|
:crumbs="breadcrumbCrumbs"
|
|
50
50
|
:rootText="breadcrumbCrumbs[0].icon === undefined"
|
|
51
|
+
:separator="breadcrumbSeparator"
|
|
51
52
|
data-testid="cn-detail-page-breadcrumbs" />
|
|
52
53
|
<!-- Header -->
|
|
53
54
|
<div
|
|
@@ -157,6 +158,31 @@
|
|
|
157
158
|
:controls="viewRegionId"
|
|
158
159
|
data-testid="cn-detail-page-view-switch"
|
|
159
160
|
@update:modelValue="selectView" />
|
|
161
|
+
<!-- With `userLayout`, the user arranges the chosen view for
|
|
162
|
+
themselves: Arrange view makes its grid draggable, Done
|
|
163
|
+
stores the arrangement, Reset view returns the view to the
|
|
164
|
+
layout the app ships. -->
|
|
165
|
+
<NcButton
|
|
166
|
+
v-if="userLayout && viewUserLayoutOn && !viewIsEmpty"
|
|
167
|
+
:variant="arrangingView ? 'primary' : 'secondary'"
|
|
168
|
+
data-testid="cn-detail-page-arrange-view"
|
|
169
|
+
@click="toggleArrangeView">
|
|
170
|
+
<template #icon>
|
|
171
|
+
<Check v-if="arrangingView" :size="20" />
|
|
172
|
+
<Pencil v-else :size="20" />
|
|
173
|
+
</template>
|
|
174
|
+
{{ arrangingView ? t('nextcloud-vue', 'Done') : t('nextcloud-vue', 'Arrange view') }}
|
|
175
|
+
</NcButton>
|
|
176
|
+
<NcButton
|
|
177
|
+
v-if="viewUserLayoutOn && arrangingView"
|
|
178
|
+
variant="tertiary"
|
|
179
|
+
data-testid="cn-detail-page-reset-view"
|
|
180
|
+
@click="resetViewUserLayout()">
|
|
181
|
+
<template #icon>
|
|
182
|
+
<Restore :size="20" />
|
|
183
|
+
</template>
|
|
184
|
+
{{ t('nextcloud-vue', 'Reset view') }}
|
|
185
|
+
</NcButton>
|
|
160
186
|
<!-- Next and previous inside the list this record was opened
|
|
161
187
|
from. Rendered only when the address names that list: a
|
|
162
188
|
record reached by a bare link offers neither, rather than
|
|
@@ -626,10 +652,10 @@
|
|
|
626
652
|
keyed on its view, so switching mounts the other view's grid. -->
|
|
627
653
|
<CnDashboardGrid
|
|
628
654
|
v-for="section in bodyGridSections"
|
|
629
|
-
:key="`cn-detail-grid-${section.key}-${
|
|
655
|
+
:key="`cn-detail-grid-${section.key}-${section.editable ? 'editing' : 'live'}`"
|
|
630
656
|
v-bind="section.attrs"
|
|
631
657
|
:layout="section.layout"
|
|
632
|
-
:editable="
|
|
658
|
+
:editable="section.editable"
|
|
633
659
|
:columns="12"
|
|
634
660
|
:columnOpts="columnOpts"
|
|
635
661
|
class="cn-detail-page__grid"
|
|
@@ -724,6 +750,7 @@
|
|
|
724
750
|
:widget="findWidget(item)"
|
|
725
751
|
chrome="card"
|
|
726
752
|
:showCardTitle="showCardTitle(item)"
|
|
753
|
+
:showActions="showWidgetActions"
|
|
727
754
|
:objectId="objectId"
|
|
728
755
|
:object="currentObject"
|
|
729
756
|
:objectType="resolvedObjectType"
|
|
@@ -934,6 +961,7 @@
|
|
|
934
961
|
:widget="widget"
|
|
935
962
|
chrome="card"
|
|
936
963
|
:showCardTitle="true"
|
|
964
|
+
:showActions="showWidgetActions"
|
|
937
965
|
:objectId="objectId"
|
|
938
966
|
:object="currentObject"
|
|
939
967
|
:objectType="resolvedObjectType"
|
|
@@ -1060,6 +1088,7 @@ import { NcActionButton, NcActionCaption, NcActionLink, NcActionSeparator, NcBut
|
|
|
1060
1088
|
import { provide, ref, watch } from 'vue'
|
|
1061
1089
|
import AlertCircleOutline from 'vue-material-design-icons/AlertCircleOutline.vue'
|
|
1062
1090
|
import ArrowLeft from 'vue-material-design-icons/ArrowLeft.vue'
|
|
1091
|
+
import Check from 'vue-material-design-icons/Check.vue'
|
|
1063
1092
|
import ChevronLeft from 'vue-material-design-icons/ChevronLeft.vue'
|
|
1064
1093
|
import ChevronRight from 'vue-material-design-icons/ChevronRight.vue'
|
|
1065
1094
|
import Cog from 'vue-material-design-icons/Cog.vue'
|
|
@@ -1068,6 +1097,7 @@ import InformationOutline from 'vue-material-design-icons/InformationOutline.vue
|
|
|
1068
1097
|
import Pencil from 'vue-material-design-icons/Pencil.vue'
|
|
1069
1098
|
import Plus from 'vue-material-design-icons/Plus.vue'
|
|
1070
1099
|
import Refresh from 'vue-material-design-icons/Refresh.vue'
|
|
1100
|
+
import Restore from 'vue-material-design-icons/Restore.vue'
|
|
1071
1101
|
import ViewDashboardOutline from 'vue-material-design-icons/ViewDashboardOutline.vue'
|
|
1072
1102
|
import CnRelationLinkModal from '../../dialogs/CnRelationLinkModal.vue'
|
|
1073
1103
|
import CnWidgetStyleEditorModal from '../../dialogs/CnWidgetStyleEditorModal.vue'
|
|
@@ -1274,9 +1304,11 @@ export default {
|
|
|
1274
1304
|
CnTranslatedBadge,
|
|
1275
1305
|
CnWidgetStyleEditorModal,
|
|
1276
1306
|
CnRelationLinkModal,
|
|
1307
|
+
Check,
|
|
1277
1308
|
Cog,
|
|
1278
1309
|
Plus,
|
|
1279
1310
|
Pencil,
|
|
1311
|
+
Restore,
|
|
1280
1312
|
},
|
|
1281
1313
|
|
|
1282
1314
|
mixins: [gridLayout, pageViews],
|
|
@@ -1769,6 +1801,20 @@ export default {
|
|
|
1769
1801
|
default: '',
|
|
1770
1802
|
},
|
|
1771
1803
|
|
|
1804
|
+
/**
|
|
1805
|
+
* Whether the user arranges each view (`views`) for themselves. On,
|
|
1806
|
+
* the header offers Arrange view next to the view switch; the
|
|
1807
|
+
* arrangement is stored per view and per user (the server, mirrored
|
|
1808
|
+
* in the browser) and a reset returns the view to its manifest
|
|
1809
|
+
* layout. Off (the default), a view renders as the manifest says.
|
|
1810
|
+
*
|
|
1811
|
+
* @type {boolean}
|
|
1812
|
+
*/
|
|
1813
|
+
userLayout: {
|
|
1814
|
+
type: Boolean,
|
|
1815
|
+
default: false,
|
|
1816
|
+
},
|
|
1817
|
+
|
|
1772
1818
|
/**
|
|
1773
1819
|
* Whether the Refresh entry renders in the page-header menu.
|
|
1774
1820
|
* Tri-state:
|
|
@@ -2192,18 +2238,38 @@ export default {
|
|
|
2192
2238
|
* belongs to; the record's display name follows as the current
|
|
2193
2239
|
* crumb. `label` goes through the host translate function and shows
|
|
2194
2240
|
* as text; `icon` (an MDI name, e.g. `Home`) draws that icon instead,
|
|
2195
|
-
* with the label as its accessible name.
|
|
2241
|
+
* with the label as its accessible name. `currentField` names a field
|
|
2242
|
+
* of the record (a dotted path, e.g. `identifier`) whose value is the
|
|
2243
|
+
* current crumb instead of the display name; an empty value falls back
|
|
2244
|
+
* to the display name. `separator` draws that text (e.g. `/`) between
|
|
2245
|
+
* the crumbs instead of the chevron. Null (the default) draws no
|
|
2196
2246
|
* trail.
|
|
2197
2247
|
*
|
|
2198
2248
|
* @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-a-detail-page-drops-its-eyebrow-and-shows-a-breadcrumb
|
|
2199
2249
|
* @spec openspec/changes/zuiddrecht-pixel-gaps-2/specs/zuiddrecht-pixel-gaps-2/spec.md#requirement-a-declared-breadcrumb-label-shows-as-text
|
|
2200
|
-
* @
|
|
2250
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-a-breadcrumb-can-name-the-record-by-a-field-and-use-a-text-separator
|
|
2251
|
+
* @type {{label: string, route?: string, params?: object, href?: string, icon?: string, currentField?: string, separator?: string}|null}
|
|
2201
2252
|
*/
|
|
2202
2253
|
breadcrumb: {
|
|
2203
2254
|
type: Object,
|
|
2204
2255
|
default: null,
|
|
2205
2256
|
},
|
|
2206
2257
|
|
|
2258
|
+
/**
|
|
2259
|
+
* Whether the record's widget cards (the body grid and the side
|
|
2260
|
+
* column) keep their overflow Actions menu when their definition does
|
|
2261
|
+
* not say (`showActions`). `false` (manifest
|
|
2262
|
+
* `config.showWidgetActions: false`) drops it, as on a dashboard; a
|
|
2263
|
+
* widget with `showActions: true` keeps its menu. A card that offers
|
|
2264
|
+
* an Add action keeps the menu that holds it. True by default.
|
|
2265
|
+
*
|
|
2266
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-a-detail-page-can-drop-the-widget-actions-menu
|
|
2267
|
+
*/
|
|
2268
|
+
showWidgetActions: {
|
|
2269
|
+
type: Boolean,
|
|
2270
|
+
default: true,
|
|
2271
|
+
},
|
|
2272
|
+
|
|
2207
2273
|
/**
|
|
2208
2274
|
* A column of cards beside the body (manifest `config.sideColumn`),
|
|
2209
2275
|
* for the record's facts: deadline, requester, handler, links. Each
|
|
@@ -2419,6 +2485,8 @@ export default {
|
|
|
2419
2485
|
|
|
2420
2486
|
data() {
|
|
2421
2487
|
return {
|
|
2488
|
+
/** Whether the user is arranging the chosen view (`userLayout`). */
|
|
2489
|
+
arrangingView: false,
|
|
2422
2490
|
/** Whether the record edit form is open. */
|
|
2423
2491
|
editFormOpen: false,
|
|
2424
2492
|
/**
|
|
@@ -3393,7 +3461,38 @@ export default {
|
|
|
3393
3461
|
} else if (crumb.href) {
|
|
3394
3462
|
first.href = crumb.href
|
|
3395
3463
|
}
|
|
3396
|
-
return [first, { label: this.
|
|
3464
|
+
return [first, { label: this.breadcrumbCurrentLabel }]
|
|
3465
|
+
},
|
|
3466
|
+
|
|
3467
|
+
/**
|
|
3468
|
+
* The current crumb: the record's `breadcrumb.currentField` value
|
|
3469
|
+
* (the case number on a case page) when declared and filled, else the
|
|
3470
|
+
* display name.
|
|
3471
|
+
*
|
|
3472
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-a-breadcrumb-can-name-the-record-by-a-field-and-use-a-text-separator
|
|
3473
|
+
* @return {string}
|
|
3474
|
+
*/
|
|
3475
|
+
breadcrumbCurrentLabel() {
|
|
3476
|
+
const field = this.breadcrumb && this.breadcrumb.currentField
|
|
3477
|
+
if (typeof field === 'string' && field !== '') {
|
|
3478
|
+
const value = stageOf(this.resolvedObject, field)
|
|
3479
|
+
if (value !== '') {
|
|
3480
|
+
return value
|
|
3481
|
+
}
|
|
3482
|
+
}
|
|
3483
|
+
return this.displayTitle
|
|
3484
|
+
},
|
|
3485
|
+
|
|
3486
|
+
/**
|
|
3487
|
+
* The text drawn between the crumbs (`breadcrumb.separator`), or ''
|
|
3488
|
+
* for the chevron.
|
|
3489
|
+
*
|
|
3490
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-a-breadcrumb-can-name-the-record-by-a-field-and-use-a-text-separator
|
|
3491
|
+
* @return {string}
|
|
3492
|
+
*/
|
|
3493
|
+
breadcrumbSeparator() {
|
|
3494
|
+
const separator = this.breadcrumb && this.breadcrumb.separator
|
|
3495
|
+
return typeof separator === 'string' ? separator : ''
|
|
3397
3496
|
},
|
|
3398
3497
|
|
|
3399
3498
|
/**
|
|
@@ -3589,7 +3688,7 @@ export default {
|
|
|
3589
3688
|
viewDisplayLayout() {
|
|
3590
3689
|
// eslint-disable-next-line @typescript-eslint/no-unused-expressions -- reading the flag IS the effect, as in bodyGridLayout
|
|
3591
3690
|
this.editingBody
|
|
3592
|
-
return this.activeView ? this.closeDroppedRows(this.
|
|
3691
|
+
return this.activeView ? this.closeDroppedRows(this.activeViewLayout) : []
|
|
3593
3692
|
},
|
|
3594
3693
|
|
|
3595
3694
|
/**
|
|
@@ -3607,16 +3706,18 @@ export default {
|
|
|
3607
3706
|
* chosen view's grid in the region the view switch controls (left out
|
|
3608
3707
|
* when the view is empty; the empty state takes the region then).
|
|
3609
3708
|
*
|
|
3610
|
-
* @return {Array<{key: string, view: boolean, layout: Array<object>, attrs: object}>}
|
|
3709
|
+
* @return {Array<{key: string, view: boolean, layout: Array<object>, editable: boolean, attrs: object}>}
|
|
3611
3710
|
* @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-a-page-declares-views-that-each-hold-a-widget-grid
|
|
3612
3711
|
*/
|
|
3613
3712
|
bodyGridSections() {
|
|
3614
|
-
const sections = this.hasBodyGrid ? [{ key: 'body', view: false, layout: this.bodyGridLayout, attrs: {} }] : []
|
|
3713
|
+
const sections = this.hasBodyGrid ? [{ key: 'body', view: false, layout: this.bodyGridLayout, editable: this.editingBody, attrs: {} }] : []
|
|
3615
3714
|
if (this.hasViews && !this.viewIsEmpty) {
|
|
3616
3715
|
sections.push({
|
|
3617
3716
|
key: `view-${this.activeViewId}`,
|
|
3618
3717
|
view: true,
|
|
3619
3718
|
layout: this.viewDisplayLayout,
|
|
3719
|
+
// The user arranges the view while Arrange view is on.
|
|
3720
|
+
editable: this.editingBody || this.arrangingView,
|
|
3620
3721
|
attrs: {
|
|
3621
3722
|
id: this.viewRegionId,
|
|
3622
3723
|
role: 'region',
|
|
@@ -3941,6 +4042,21 @@ export default {
|
|
|
3941
4042
|
},
|
|
3942
4043
|
|
|
3943
4044
|
methods: {
|
|
4045
|
+
/**
|
|
4046
|
+
* Start or finish arranging the chosen view. Finishing stores the
|
|
4047
|
+
* arrangement of every view the user moved, once.
|
|
4048
|
+
*
|
|
4049
|
+
* @return {void}
|
|
4050
|
+
* @spec openspec/changes/page-view-user-layouts/specs/view-switch-containers/spec.md#requirement-a-user-arranges-each-view-for-themselves
|
|
4051
|
+
*/
|
|
4052
|
+
toggleArrangeView() {
|
|
4053
|
+
const leaving = this.arrangingView
|
|
4054
|
+
this.arrangingView = !this.arrangingView
|
|
4055
|
+
if (leaving) {
|
|
4056
|
+
this.saveViewUserLayouts()
|
|
4057
|
+
}
|
|
4058
|
+
},
|
|
4059
|
+
|
|
3944
4060
|
// Expose the shared grid helpers to the template (grid mode + auto-body).
|
|
3945
4061
|
cnGridCellStyle,
|
|
3946
4062
|
hasGridRow,
|