@conduction/nextcloud-vue 2.67.0 → 2.69.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/CnContextMenu/CnContextMenu.vue2.js +37 -26
- package/dist/esm/components/CnContextMenu/CnContextMenu.vue2.js.map +1 -1
- package/dist/esm/components/CnContextMenu/CnContextMenu.vue3.js +3 -3
- package/dist/esm/components/CnContextMenu/CnContextMenu.vue3.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +21 -35
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +17 -0
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +40 -3
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +58 -16
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +87 -18
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js +6 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +58 -48
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +2 -2
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/defaultActions.js +6 -7
- package/dist/esm/components/CnIndexPage/defaultActions.js.map +1 -1
- package/dist/esm/components/CnRowActions/CnRowActions.vue2.js +33 -34
- package/dist/esm/components/CnRowActions/CnRowActions.vue2.js.map +1 -1
- package/dist/esm/components/CnRowActions/CnRowActions.vue3.js +3 -3
- package/dist/esm/components/CnRowActions/CnRowActions.vue3.js.map +1 -1
- package/dist/esm/components/CnSetupWizard/CnSetupWizard.vue2.js +8 -2
- 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/components/CnStatWidget/CnStatWidget.vue2.js +2 -2
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js +70 -1
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js +23 -3
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue2.js +3 -2
- package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnTimelineWidget/CnTimelineWidget.vue3.js.map +1 -1
- package/dist/esm/composables/useContextMenu.js +4 -2
- package/dist/esm/composables/useContextMenu.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/store/plugins/dashboardLayouts.js +35 -1
- package/dist/esm/store/plugins/dashboardLayouts.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/resolveRowActions.js +130 -0
- package/dist/esm/utils/resolveRowActions.js.map +1 -0
- package/dist/esm/utils/rowActionItem.js +104 -0
- package/dist/esm/utils/rowActionItem.js.map +1 -0
- package/dist/esm/utils/timelineEvents.js +44 -1
- package/dist/esm/utils/timelineEvents.js.map +1 -1
- package/dist/esm/utils/validateManifest.js +59 -3
- package/dist/esm/utils/validateManifest.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 +1251 -232
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/l10n/en.json +3 -0
- package/l10n/nl.json +3 -0
- package/package.json +1 -1
- package/scripts/.jsdoc-baselines.json +2 -2
- package/src/components/CnContextMenu/CnContextMenu.vue +40 -29
- package/src/components/CnDashboardPage/CnDashboardPage.md +2 -0
- package/src/components/CnDashboardPage/CnDashboardPage.vue +34 -35
- package/src/components/CnDetailPage/CnDetailPage.md +6 -0
- package/src/components/CnDetailPage/CnDetailPage.vue +67 -5
- package/src/components/CnFormDialog/CnFormDialog.vue +92 -18
- package/src/components/CnIndexPage/CnIndexPage.md +21 -0
- package/src/components/CnIndexPage/CnIndexPage.vue +61 -51
- package/src/components/CnIndexPage/defaultActions.js +6 -7
- package/src/components/CnRowActions/CnRowActions.vue +36 -37
- package/src/components/CnSetupWizard/CnSetupWizard.vue +8 -2
- package/src/components/CnStatWidget/CnStatWidget.vue +2 -2
- package/src/components/CnStatsBlockWidget/CnStatsBlockWidget.vue +82 -3
- package/src/components/CnTimelineWidget/CnTimelineWidget.vue +3 -2
- package/src/composables/useContextMenu.js +4 -2
- package/src/mixins/pageViews.js +277 -10
- package/src/schemas/app-manifest-v2.schema.json +73 -9
- package/src/store/plugins/dashboardLayouts.js +34 -0
- package/src/utils/formatMetric.js +143 -15
- package/src/utils/resolveRowActions.js +127 -0
- package/src/utils/rowActionItem.js +100 -0
- package/src/utils/timelineEvents.js +43 -0
- package/src/utils/validateManifest.js +60 -3
- package/src/utils/validateManifestV2.compiled.js +2 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
[diffend] Oversized file quarantined before diffing.
|
|
2
2
|
name: package/dist/nextcloud-vue.cjs.js.map
|
|
3
|
-
size:
|
|
4
|
-
sha256:
|
|
3
|
+
size: 35499103 bytes
|
|
4
|
+
sha256: fe1cf925335eec45a51c59903eb32d313c16bdcbdaf047c4b80fad05712db22a
|
package/l10n/en.json
CHANGED
|
@@ -1092,6 +1092,9 @@
|
|
|
1092
1092
|
"An attention card needs a title": "An attention card needs a title",
|
|
1093
1093
|
"Good morning, {name}": "Good morning, {name}",
|
|
1094
1094
|
"This view has no widgets yet.": "This view has no widgets yet.",
|
|
1095
|
+
"Arrange view": "Arrange view",
|
|
1096
|
+
"Reset view": "Reset view",
|
|
1097
|
+
"Reset layout": "Reset layout",
|
|
1095
1098
|
"Good morning": "Good morning",
|
|
1096
1099
|
"Good afternoon, {name}": "Good afternoon, {name}",
|
|
1097
1100
|
"Good afternoon": "Good afternoon",
|
package/l10n/nl.json
CHANGED
|
@@ -1136,6 +1136,9 @@
|
|
|
1136
1136
|
"An attention card needs a title": "Een aandachtskaart heeft een titel nodig",
|
|
1137
1137
|
"Good morning, {name}": "Goedemorgen, {name}",
|
|
1138
1138
|
"This view has no widgets yet.": "Deze weergave heeft nog geen widgets.",
|
|
1139
|
+
"Arrange view": "Weergave indelen",
|
|
1140
|
+
"Reset view": "Weergave herstellen",
|
|
1141
|
+
"Reset layout": "Indeling herstellen",
|
|
1139
1142
|
"Good morning": "Goedemorgen",
|
|
1140
1143
|
"Good afternoon, {name}": "Goedemiddag, {name}",
|
|
1141
1144
|
"Good afternoon": "Goedemiddag",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@conduction/nextcloud-vue",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.69.0",
|
|
4
4
|
"description": "Shared Vue component library for Conduction Nextcloud apps — complements @nextcloud/vue with higher-level components, OpenRegister integration, and NL Design System support",
|
|
5
5
|
"homepage": "https://github.com/ConductionNL/nextcloud-vue#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -111,7 +111,7 @@
|
|
|
111
111
|
"CnIconPicker": 1,
|
|
112
112
|
"CnImageWidget": 1,
|
|
113
113
|
"CnImageWidgetForm": 1,
|
|
114
|
-
"CnIndexPage": 0.
|
|
114
|
+
"CnIndexPage": 0.79,
|
|
115
115
|
"CnIndexSidebar": 0.69,
|
|
116
116
|
"CnInfoWidget": 1,
|
|
117
117
|
"CnIntegrationCard": 1,
|
|
@@ -202,7 +202,7 @@
|
|
|
202
202
|
"CnRichSubmitDialog": 1,
|
|
203
203
|
"CnRoadmapItem": 1,
|
|
204
204
|
"CnRoadmapTab": 1,
|
|
205
|
-
"CnRowActions":
|
|
205
|
+
"CnRowActions": 1,
|
|
206
206
|
"CnSavedViewsControl": 1,
|
|
207
207
|
"CnSaveViewDialog": 1,
|
|
208
208
|
"CnSchemaFormDialog": 0.79,
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
@close="onClose"
|
|
13
13
|
@closed="onClosed">
|
|
14
14
|
<!-- Dynamic actions from array prop -->
|
|
15
|
-
<template v-for="{ action, link } in renderedActions" :key="action
|
|
15
|
+
<template v-for="{ action, link } in renderedActions" :key="actionKey(action)">
|
|
16
16
|
<!-- A navigate-only action is a real link, so it can be middle-clicked, opened in a new tab or copied. -->
|
|
17
17
|
<NcActionLink
|
|
18
18
|
v-if="link"
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
:target="link.target"
|
|
21
21
|
:title="resolveTitle(action)"
|
|
22
22
|
:class="{ 'cn-row-action--destructive': action.destructive }"
|
|
23
|
-
:data-testid="
|
|
23
|
+
:data-testid="actionTestId(action)"
|
|
24
24
|
closeAfterClick
|
|
25
25
|
@click="onLinkAction(action, link, $event)">
|
|
26
26
|
<template v-if="action.icon" #icon>
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
:title="resolveTitle(action)"
|
|
35
35
|
:disabled="resolveDisabled(action)"
|
|
36
36
|
:class="{ 'cn-row-action--destructive': action.destructive }"
|
|
37
|
-
:data-testid="
|
|
37
|
+
:data-testid="actionTestId(action)"
|
|
38
38
|
closeAfterClick
|
|
39
39
|
@click="onAction(action)">
|
|
40
40
|
<template v-if="action.icon" #icon>
|
|
@@ -95,6 +95,7 @@ import { NcActionButton, NcActionLink, NcActions } from '@nextcloud/vue'
|
|
|
95
95
|
import { CTX_MENU_DATA_ATTR, CTX_MENU_POPPER_ATTR } from '../../composables/useContextMenu.js'
|
|
96
96
|
import { followItemActionLink, resolveItemActionLink } from '../../utils/actionLink.js'
|
|
97
97
|
import { isModifiedClick } from '../../utils/linkNavigation.js'
|
|
98
|
+
import { isRowActionVisible, rowActionKey, rowActionPayload, rowActionTestId, slugifyActionLabel } from '../../utils/rowActionItem.js'
|
|
98
99
|
import { CnIcon } from '../CnIcon/index.js'
|
|
99
100
|
|
|
100
101
|
/**
|
|
@@ -109,12 +110,13 @@ import { CnIcon } from '../CnIcon/index.js'
|
|
|
109
110
|
* cursor positioning). The composable handles the DOM attributes; this component
|
|
110
111
|
* handles the NcActions template boilerplate.
|
|
111
112
|
*
|
|
112
|
-
* Dynamic actions (CnIndexPage pattern)
|
|
113
|
+
* Dynamic actions (CnIndexPage pattern).
|
|
114
|
+
* CnIndexPage passes the same per-row list its row actions menu renders, so the two menus always match.
|
|
113
115
|
* ```vue
|
|
114
116
|
* <CnContextMenu
|
|
115
117
|
* v-model:open="contextMenuOpen"
|
|
116
|
-
* :actions="
|
|
117
|
-
* :target-item="
|
|
118
|
+
* :actions="rowActionsFor(contextMenuShownRow)"
|
|
119
|
+
* :target-item="contextMenuShownRow"
|
|
118
120
|
* @action="$emit('action', $event)"
|
|
119
121
|
* @close="closeContextMenu" />
|
|
120
122
|
* ```
|
|
@@ -161,13 +163,15 @@ export default {
|
|
|
161
163
|
* `visible` (boolean | (targetItem) => boolean) hides the entry when falsy
|
|
162
164
|
* (default: shown). `title` (string | (targetItem) => string) renders as
|
|
163
165
|
* a native tooltip — useful for explaining why an entry is disabled.
|
|
166
|
+
* A locally decidable `visibleWhen` that is false for the target hides the entry too, as in CnRowActions.
|
|
167
|
+
* `id` is passed in the `action` payload; `builtin` marks a CnIndexPage built-in, whose testid is `cn-action-item-<id>` in every locale.
|
|
164
168
|
* `href` (URL) or `to` (vue-router location), each a value or a
|
|
165
169
|
* `(targetItem) => …` function, render the entry as a real link, with
|
|
166
170
|
* `linkTarget` as its `target`; a link emits `action` but does not call
|
|
167
171
|
* `handler`. When the entire array is empty (or all entries are filtered
|
|
168
172
|
* out), only the default slot content is rendered.
|
|
169
173
|
*
|
|
170
|
-
* @type {Array<{label: string, icon: object | string, handler: (targetItem: object) => void, disabled: boolean | ((targetItem: object) => boolean), visible: boolean | ((targetItem: object) => boolean), title: string | ((targetItem: object) => string), destructive: boolean, href: string | ((targetItem: object) => string), to: string | object | ((targetItem: object) => string | object), linkTarget: string}>}
|
|
174
|
+
* @type {Array<{label: string, id: string, builtin: boolean, icon: object | string, handler: (targetItem: object) => void, disabled: boolean | ((targetItem: object) => boolean), visible: boolean | ((targetItem: object) => boolean), visibleWhen: object, title: string | ((targetItem: object) => string), destructive: boolean, href: string | ((targetItem: object) => string), to: string | object | ((targetItem: object) => string | object), linkTarget: string}>}
|
|
171
175
|
*/
|
|
172
176
|
actions: {
|
|
173
177
|
type: Array,
|
|
@@ -209,21 +213,13 @@ export default {
|
|
|
209
213
|
|
|
210
214
|
computed: {
|
|
211
215
|
/**
|
|
212
|
-
* Filter actions by their `visible`
|
|
213
|
-
*
|
|
216
|
+
* Filter actions by their `visibleWhen` and `visible` gates, with the same rule CnRowActions uses.
|
|
217
|
+
* Entries without either always show.
|
|
214
218
|
*
|
|
215
219
|
* @return {Array} Visible actions for the current targetItem.
|
|
216
220
|
*/
|
|
217
221
|
visibleActions() {
|
|
218
|
-
return this.actions.filter((action) =>
|
|
219
|
-
if (action.visible === undefined) {
|
|
220
|
-
return true
|
|
221
|
-
}
|
|
222
|
-
if (typeof action.visible === 'function') {
|
|
223
|
-
return !!action.visible(this.targetItem)
|
|
224
|
-
}
|
|
225
|
-
return !!action.visible
|
|
226
|
-
})
|
|
222
|
+
return this.actions.filter((action) => isRowActionVisible(action, this.targetItem))
|
|
227
223
|
},
|
|
228
224
|
|
|
229
225
|
/**
|
|
@@ -466,18 +462,33 @@ export default {
|
|
|
466
462
|
|
|
467
463
|
/**
|
|
468
464
|
* Slugify an action label for use in stable `data-testid` selectors.
|
|
469
|
-
* Lowercase, kebab-case, strip non-alphanumeric.
|
|
470
|
-
* `:data-testid` binding on NcActionButton — does not affect runtime
|
|
471
|
-
* behaviour or rendered text.
|
|
465
|
+
* Lowercase, kebab-case, strip non-alphanumeric.
|
|
472
466
|
*
|
|
473
467
|
* @param {string} label The action's display label.
|
|
474
468
|
* @return {string} kebab-case slug suitable for a testid suffix.
|
|
475
469
|
*/
|
|
476
470
|
slugifyLabel(label) {
|
|
477
|
-
return
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
471
|
+
return slugifyActionLabel(label)
|
|
472
|
+
},
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* The entry's `data-testid`: `cn-action-item-<id>` for a built-in, the label slug otherwise.
|
|
476
|
+
*
|
|
477
|
+
* @param {object} action The action descriptor.
|
|
478
|
+
* @return {string} The testid.
|
|
479
|
+
*/
|
|
480
|
+
actionTestId(action) {
|
|
481
|
+
return rowActionTestId(action)
|
|
482
|
+
},
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* The entry's render key: `builtin:<id>` for a built-in, the label otherwise.
|
|
486
|
+
*
|
|
487
|
+
* @param {object} action The action descriptor.
|
|
488
|
+
* @return {string} The key.
|
|
489
|
+
*/
|
|
490
|
+
actionKey(action) {
|
|
491
|
+
return rowActionKey(action)
|
|
481
492
|
},
|
|
482
493
|
|
|
483
494
|
onAction(action) {
|
|
@@ -485,10 +496,10 @@ export default {
|
|
|
485
496
|
action.handler(this.targetItem)
|
|
486
497
|
}
|
|
487
498
|
/**
|
|
488
|
-
* @event action User picked an entry from the menu. The action's own `handler(targetItem)` (when present) ran synchronously before this event fires; the event lets parents observe / log the choice.
|
|
489
|
-
* @type {{ action: string, row: object|null }}
|
|
499
|
+
* @event action User picked an entry from the menu. The action's own `handler(targetItem)` (when present) ran synchronously before this event fires; the event lets parents observe / log the choice. Payload: `action` (the label), the target as `row`, the action's `id` when it has one, and `builtin: true` for a CnIndexPage built-in.
|
|
500
|
+
* @type {{ action: string, row: object|null, id?: string, builtin?: boolean }}
|
|
490
501
|
*/
|
|
491
|
-
this.$emit('action',
|
|
502
|
+
this.$emit('action', rowActionPayload(action, this.targetItem))
|
|
492
503
|
},
|
|
493
504
|
|
|
494
505
|
/**
|
|
@@ -506,7 +517,7 @@ export default {
|
|
|
506
517
|
return
|
|
507
518
|
}
|
|
508
519
|
followItemActionLink(event, link, this.$router)
|
|
509
|
-
this.$emit('action',
|
|
520
|
+
this.$emit('action', rowActionPayload(action, this.targetItem))
|
|
510
521
|
},
|
|
511
522
|
|
|
512
523
|
onClose() {
|
|
@@ -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. -->
|
|
@@ -754,6 +767,7 @@ import Check from 'vue-material-design-icons/Check.vue'
|
|
|
754
767
|
import Cog from 'vue-material-design-icons/Cog.vue'
|
|
755
768
|
import Download from 'vue-material-design-icons/Download.vue'
|
|
756
769
|
import Pencil from 'vue-material-design-icons/Pencil.vue'
|
|
770
|
+
import Restore from 'vue-material-design-icons/Restore.vue'
|
|
757
771
|
import ViewDashboardOutline from 'vue-material-design-icons/ViewDashboardOutline.vue'
|
|
758
772
|
import CnWidgetStyleEditorModal from '../../dialogs/CnWidgetStyleEditorModal.vue'
|
|
759
773
|
import CnBodySections from '../CnBodySections/CnBodySections.vue'
|
|
@@ -768,9 +782,8 @@ import CnWidgetRefItem from '../CnWidgetRefItem/CnWidgetRefItem.vue'
|
|
|
768
782
|
import CnWidgetRenderer from '../CnWidgetRenderer/CnWidgetRenderer.vue'
|
|
769
783
|
import CnWidgetWrapper from '../CnWidgetWrapper/CnWidgetWrapper.vue'
|
|
770
784
|
import { useIntegrationRegistry } from '../../composables/useIntegrationRegistry.js'
|
|
771
|
-
import { readUserPreference, writeUserPreference } from '../../composables/useUserPreferences.js'
|
|
772
785
|
import { pageViews } from '../../mixins/pageViews.js'
|
|
773
|
-
import {
|
|
786
|
+
import { mergeUserLayout, resolveUserLayoutApi } from '../../store/plugins/dashboardLayouts.js'
|
|
774
787
|
import { isAppInstalled } from '../../utils/appInstalled.js'
|
|
775
788
|
import { compareVisibleWhen, readVisibleWhenValue } from '../../utils/visibleWhen.js'
|
|
776
789
|
import { canonicalWidgetType } from '../../utils/widgetTypeAliases.js'
|
|
@@ -905,6 +918,7 @@ export default {
|
|
|
905
918
|
NcLoadingIcon,
|
|
906
919
|
NcSelect,
|
|
907
920
|
Pencil,
|
|
921
|
+
Restore,
|
|
908
922
|
Check,
|
|
909
923
|
Cog,
|
|
910
924
|
CalendarRange,
|
|
@@ -1958,7 +1972,7 @@ export default {
|
|
|
1958
1972
|
* @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-a-view-with-nothing-to-draw-says-so
|
|
1959
1973
|
*/
|
|
1960
1974
|
viewDisplayLayout() {
|
|
1961
|
-
const items = this.
|
|
1975
|
+
const items = this.activeViewLayout
|
|
1962
1976
|
if (this.gridEditable) {
|
|
1963
1977
|
return items
|
|
1964
1978
|
}
|
|
@@ -2000,8 +2014,9 @@ export default {
|
|
|
2000
2014
|
key: `view-${this.activeViewId}`,
|
|
2001
2015
|
view: true,
|
|
2002
2016
|
layout: this.viewDisplayLayout,
|
|
2003
|
-
//
|
|
2004
|
-
|
|
2017
|
+
// With `userLayout` the user arranges the view too; the
|
|
2018
|
+
// arrangement is kept per view (pageViews mixin).
|
|
2019
|
+
editable: this.gridEditable,
|
|
2005
2020
|
attrs: {
|
|
2006
2021
|
id: this.viewRegionId,
|
|
2007
2022
|
role: 'region',
|
|
@@ -2721,6 +2736,7 @@ export default {
|
|
|
2721
2736
|
// the user's arrangement is finished when they say it is.
|
|
2722
2737
|
if (leaving) {
|
|
2723
2738
|
this.saveUserLayout()
|
|
2739
|
+
this.saveViewUserLayouts()
|
|
2724
2740
|
}
|
|
2725
2741
|
|
|
2726
2742
|
/**
|
|
@@ -2780,6 +2796,17 @@ export default {
|
|
|
2780
2796
|
}
|
|
2781
2797
|
},
|
|
2782
2798
|
|
|
2799
|
+
/**
|
|
2800
|
+
* The edit-mode reset: the page's own grid and the chosen view return
|
|
2801
|
+
* to the manifest layout. Other views keep this user's arrangement.
|
|
2802
|
+
*
|
|
2803
|
+
* @return {Promise<void>}
|
|
2804
|
+
* @spec openspec/changes/page-view-user-layouts/specs/view-switch-containers/spec.md#requirement-resetting-a-view-restores-its-manifest-layout
|
|
2805
|
+
*/
|
|
2806
|
+
async resetLayout() {
|
|
2807
|
+
await Promise.all([this.resetUserLayout(), this.resetViewUserLayout()])
|
|
2808
|
+
},
|
|
2809
|
+
|
|
2783
2810
|
/**
|
|
2784
2811
|
* Drop this user's arrangement and return to the manifest.
|
|
2785
2812
|
*
|
|
@@ -2832,35 +2859,7 @@ export default {
|
|
|
2832
2859
|
* @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
2860
|
*/
|
|
2834
2861
|
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
|
-
}
|
|
2862
|
+
return resolveUserLayoutApi(this.userLayoutStore)
|
|
2864
2863
|
},
|
|
2865
2864
|
|
|
2866
2865
|
onLayoutChange(updated) {
|
|
@@ -3906,7 +3905,7 @@ export default {
|
|
|
3906
3905
|
// library's own defaults, so a manifest need not set them — but a
|
|
3907
3906
|
// manifest that DOES set them must reach the component, or the
|
|
3908
3907
|
// declaration is a silent no-op that reads like configuration.
|
|
3909
|
-
for (const key of ['countLabel', 'variant', 'showZeroCount', 'horizontal', 'vertical', 'filled', 'route', 'iconClass']) {
|
|
3908
|
+
for (const key of ['countLabel', 'variant', 'showZeroCount', 'horizontal', 'vertical', 'filled', 'route', 'iconClass', 'format']) {
|
|
3910
3909
|
if (props[key] !== undefined) {
|
|
3911
3910
|
out[key] = props[key]
|
|
3912
3911
|
}
|
|
@@ -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 |
|
|
@@ -157,6 +157,31 @@
|
|
|
157
157
|
:controls="viewRegionId"
|
|
158
158
|
data-testid="cn-detail-page-view-switch"
|
|
159
159
|
@update:modelValue="selectView" />
|
|
160
|
+
<!-- With `userLayout`, the user arranges the chosen view for
|
|
161
|
+
themselves: Arrange view makes its grid draggable, Done
|
|
162
|
+
stores the arrangement, Reset view returns the view to the
|
|
163
|
+
layout the app ships. -->
|
|
164
|
+
<NcButton
|
|
165
|
+
v-if="userLayout && viewUserLayoutOn && !viewIsEmpty"
|
|
166
|
+
:variant="arrangingView ? 'primary' : 'secondary'"
|
|
167
|
+
data-testid="cn-detail-page-arrange-view"
|
|
168
|
+
@click="toggleArrangeView">
|
|
169
|
+
<template #icon>
|
|
170
|
+
<Check v-if="arrangingView" :size="20" />
|
|
171
|
+
<Pencil v-else :size="20" />
|
|
172
|
+
</template>
|
|
173
|
+
{{ arrangingView ? t('nextcloud-vue', 'Done') : t('nextcloud-vue', 'Arrange view') }}
|
|
174
|
+
</NcButton>
|
|
175
|
+
<NcButton
|
|
176
|
+
v-if="viewUserLayoutOn && arrangingView"
|
|
177
|
+
variant="tertiary"
|
|
178
|
+
data-testid="cn-detail-page-reset-view"
|
|
179
|
+
@click="resetViewUserLayout()">
|
|
180
|
+
<template #icon>
|
|
181
|
+
<Restore :size="20" />
|
|
182
|
+
</template>
|
|
183
|
+
{{ t('nextcloud-vue', 'Reset view') }}
|
|
184
|
+
</NcButton>
|
|
160
185
|
<!-- Next and previous inside the list this record was opened
|
|
161
186
|
from. Rendered only when the address names that list: a
|
|
162
187
|
record reached by a bare link offers neither, rather than
|
|
@@ -626,10 +651,10 @@
|
|
|
626
651
|
keyed on its view, so switching mounts the other view's grid. -->
|
|
627
652
|
<CnDashboardGrid
|
|
628
653
|
v-for="section in bodyGridSections"
|
|
629
|
-
:key="`cn-detail-grid-${section.key}-${
|
|
654
|
+
:key="`cn-detail-grid-${section.key}-${section.editable ? 'editing' : 'live'}`"
|
|
630
655
|
v-bind="section.attrs"
|
|
631
656
|
:layout="section.layout"
|
|
632
|
-
:editable="
|
|
657
|
+
:editable="section.editable"
|
|
633
658
|
:columns="12"
|
|
634
659
|
:columnOpts="columnOpts"
|
|
635
660
|
class="cn-detail-page__grid"
|
|
@@ -1060,6 +1085,7 @@ import { NcActionButton, NcActionCaption, NcActionLink, NcActionSeparator, NcBut
|
|
|
1060
1085
|
import { provide, ref, watch } from 'vue'
|
|
1061
1086
|
import AlertCircleOutline from 'vue-material-design-icons/AlertCircleOutline.vue'
|
|
1062
1087
|
import ArrowLeft from 'vue-material-design-icons/ArrowLeft.vue'
|
|
1088
|
+
import Check from 'vue-material-design-icons/Check.vue'
|
|
1063
1089
|
import ChevronLeft from 'vue-material-design-icons/ChevronLeft.vue'
|
|
1064
1090
|
import ChevronRight from 'vue-material-design-icons/ChevronRight.vue'
|
|
1065
1091
|
import Cog from 'vue-material-design-icons/Cog.vue'
|
|
@@ -1068,6 +1094,7 @@ import InformationOutline from 'vue-material-design-icons/InformationOutline.vue
|
|
|
1068
1094
|
import Pencil from 'vue-material-design-icons/Pencil.vue'
|
|
1069
1095
|
import Plus from 'vue-material-design-icons/Plus.vue'
|
|
1070
1096
|
import Refresh from 'vue-material-design-icons/Refresh.vue'
|
|
1097
|
+
import Restore from 'vue-material-design-icons/Restore.vue'
|
|
1071
1098
|
import ViewDashboardOutline from 'vue-material-design-icons/ViewDashboardOutline.vue'
|
|
1072
1099
|
import CnRelationLinkModal from '../../dialogs/CnRelationLinkModal.vue'
|
|
1073
1100
|
import CnWidgetStyleEditorModal from '../../dialogs/CnWidgetStyleEditorModal.vue'
|
|
@@ -1274,9 +1301,11 @@ export default {
|
|
|
1274
1301
|
CnTranslatedBadge,
|
|
1275
1302
|
CnWidgetStyleEditorModal,
|
|
1276
1303
|
CnRelationLinkModal,
|
|
1304
|
+
Check,
|
|
1277
1305
|
Cog,
|
|
1278
1306
|
Plus,
|
|
1279
1307
|
Pencil,
|
|
1308
|
+
Restore,
|
|
1280
1309
|
},
|
|
1281
1310
|
|
|
1282
1311
|
mixins: [gridLayout, pageViews],
|
|
@@ -1769,6 +1798,20 @@ export default {
|
|
|
1769
1798
|
default: '',
|
|
1770
1799
|
},
|
|
1771
1800
|
|
|
1801
|
+
/**
|
|
1802
|
+
* Whether the user arranges each view (`views`) for themselves. On,
|
|
1803
|
+
* the header offers Arrange view next to the view switch; the
|
|
1804
|
+
* arrangement is stored per view and per user (the server, mirrored
|
|
1805
|
+
* in the browser) and a reset returns the view to its manifest
|
|
1806
|
+
* layout. Off (the default), a view renders as the manifest says.
|
|
1807
|
+
*
|
|
1808
|
+
* @type {boolean}
|
|
1809
|
+
*/
|
|
1810
|
+
userLayout: {
|
|
1811
|
+
type: Boolean,
|
|
1812
|
+
default: false,
|
|
1813
|
+
},
|
|
1814
|
+
|
|
1772
1815
|
/**
|
|
1773
1816
|
* Whether the Refresh entry renders in the page-header menu.
|
|
1774
1817
|
* Tri-state:
|
|
@@ -2419,6 +2462,8 @@ export default {
|
|
|
2419
2462
|
|
|
2420
2463
|
data() {
|
|
2421
2464
|
return {
|
|
2465
|
+
/** Whether the user is arranging the chosen view (`userLayout`). */
|
|
2466
|
+
arrangingView: false,
|
|
2422
2467
|
/** Whether the record edit form is open. */
|
|
2423
2468
|
editFormOpen: false,
|
|
2424
2469
|
/**
|
|
@@ -3589,7 +3634,7 @@ export default {
|
|
|
3589
3634
|
viewDisplayLayout() {
|
|
3590
3635
|
// eslint-disable-next-line @typescript-eslint/no-unused-expressions -- reading the flag IS the effect, as in bodyGridLayout
|
|
3591
3636
|
this.editingBody
|
|
3592
|
-
return this.activeView ? this.closeDroppedRows(this.
|
|
3637
|
+
return this.activeView ? this.closeDroppedRows(this.activeViewLayout) : []
|
|
3593
3638
|
},
|
|
3594
3639
|
|
|
3595
3640
|
/**
|
|
@@ -3607,16 +3652,18 @@ export default {
|
|
|
3607
3652
|
* chosen view's grid in the region the view switch controls (left out
|
|
3608
3653
|
* when the view is empty; the empty state takes the region then).
|
|
3609
3654
|
*
|
|
3610
|
-
* @return {Array<{key: string, view: boolean, layout: Array<object>, attrs: object}>}
|
|
3655
|
+
* @return {Array<{key: string, view: boolean, layout: Array<object>, editable: boolean, attrs: object}>}
|
|
3611
3656
|
* @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-a-page-declares-views-that-each-hold-a-widget-grid
|
|
3612
3657
|
*/
|
|
3613
3658
|
bodyGridSections() {
|
|
3614
|
-
const sections = this.hasBodyGrid ? [{ key: 'body', view: false, layout: this.bodyGridLayout, attrs: {} }] : []
|
|
3659
|
+
const sections = this.hasBodyGrid ? [{ key: 'body', view: false, layout: this.bodyGridLayout, editable: this.editingBody, attrs: {} }] : []
|
|
3615
3660
|
if (this.hasViews && !this.viewIsEmpty) {
|
|
3616
3661
|
sections.push({
|
|
3617
3662
|
key: `view-${this.activeViewId}`,
|
|
3618
3663
|
view: true,
|
|
3619
3664
|
layout: this.viewDisplayLayout,
|
|
3665
|
+
// The user arranges the view while Arrange view is on.
|
|
3666
|
+
editable: this.editingBody || this.arrangingView,
|
|
3620
3667
|
attrs: {
|
|
3621
3668
|
id: this.viewRegionId,
|
|
3622
3669
|
role: 'region',
|
|
@@ -3941,6 +3988,21 @@ export default {
|
|
|
3941
3988
|
},
|
|
3942
3989
|
|
|
3943
3990
|
methods: {
|
|
3991
|
+
/**
|
|
3992
|
+
* Start or finish arranging the chosen view. Finishing stores the
|
|
3993
|
+
* arrangement of every view the user moved, once.
|
|
3994
|
+
*
|
|
3995
|
+
* @return {void}
|
|
3996
|
+
* @spec openspec/changes/page-view-user-layouts/specs/view-switch-containers/spec.md#requirement-a-user-arranges-each-view-for-themselves
|
|
3997
|
+
*/
|
|
3998
|
+
toggleArrangeView() {
|
|
3999
|
+
const leaving = this.arrangingView
|
|
4000
|
+
this.arrangingView = !this.arrangingView
|
|
4001
|
+
if (leaving) {
|
|
4002
|
+
this.saveViewUserLayouts()
|
|
4003
|
+
}
|
|
4004
|
+
},
|
|
4005
|
+
|
|
3944
4006
|
// Expose the shared grid helpers to the template (grid mode + auto-body).
|
|
3945
4007
|
cnGridCellStyle,
|
|
3946
4008
|
hasGridRow,
|