@conduction/nextcloud-vue 2.68.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/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/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/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/validateManifestV2.compiled.js +2 -2
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +678 -83
- 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/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/CnStatWidget/CnStatWidget.vue +2 -2
- package/src/components/CnStatsBlockWidget/CnStatsBlockWidget.vue +82 -3
- package/src/mixins/pageViews.js +277 -10
- package/src/schemas/app-manifest-v2.schema.json +28 -2
- package/src/store/plugins/dashboardLayouts.js +34 -0
- package/src/utils/formatMetric.js +143 -15
- 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": {
|
|
@@ -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,
|
|
@@ -1312,7 +1312,7 @@ export default {
|
|
|
1312
1312
|
* @return {string}
|
|
1313
1313
|
*/
|
|
1314
1314
|
formattedValue() {
|
|
1315
|
-
return formatMetricValue(this.displayValue, this.content.format, this.configCtx)
|
|
1315
|
+
return formatMetricValue(this.displayValue, this.content.format, this.configCtx, this.objectCtx?.object || null)
|
|
1316
1316
|
},
|
|
1317
1317
|
|
|
1318
1318
|
/**
|
|
@@ -1346,7 +1346,7 @@ export default {
|
|
|
1346
1346
|
return ''
|
|
1347
1347
|
}
|
|
1348
1348
|
const { prefix, suffix, ...rest } = (this.content.format || {})
|
|
1349
|
-
return formatMetricValue(this.limitValue, rest, this.configCtx)
|
|
1349
|
+
return formatMetricValue(this.limitValue, rest, this.configCtx, this.objectCtx?.object || null)
|
|
1350
1350
|
},
|
|
1351
1351
|
|
|
1352
1352
|
/**
|
|
@@ -34,7 +34,13 @@
|
|
|
34
34
|
:vertical="vertical"
|
|
35
35
|
:filled="filled"
|
|
36
36
|
:clickable="!!view.route"
|
|
37
|
-
:route="view.route || null"
|
|
37
|
+
:route="view.route || null">
|
|
38
|
+
<!-- An entry with a `format` (or `currency` / `currencyField`)
|
|
39
|
+
shows its value formatted, money in its own currency. -->
|
|
40
|
+
<template v-if="view.formatted !== null" #value>
|
|
41
|
+
{{ view.formatted }}
|
|
42
|
+
</template>
|
|
43
|
+
</CnStatsBlock>
|
|
38
44
|
</template>
|
|
39
45
|
|
|
40
46
|
<!-- Single-source mode (pre-existing interface, unchanged). -->
|
|
@@ -50,7 +56,11 @@
|
|
|
50
56
|
:vertical="vertical"
|
|
51
57
|
:filled="filled"
|
|
52
58
|
:clickable="!!route"
|
|
53
|
-
:route="route"
|
|
59
|
+
:route="route">
|
|
60
|
+
<template v-if="formattedSingle !== null" #value>
|
|
61
|
+
{{ formattedSingle }}
|
|
62
|
+
</template>
|
|
63
|
+
</CnStatsBlock>
|
|
54
64
|
</div>
|
|
55
65
|
</template>
|
|
56
66
|
|
|
@@ -58,6 +68,7 @@
|
|
|
58
68
|
import { subscribe, unsubscribe } from '@nextcloud/event-bus'
|
|
59
69
|
import CnStatsBlock from '../CnStatsBlock/CnStatsBlock.vue'
|
|
60
70
|
import { useDataSource } from '../../composables/useDataSource.js'
|
|
71
|
+
import { formatMetricValue, normalizeMetricFormat, unwrapAppConfig } from '../../utils/formatMetric.js'
|
|
61
72
|
import { dropOptionalUnresolved, hasUnresolvedTokens, resolveFilterTokens } from '../../utils/resolveFilterTokens.js'
|
|
62
73
|
|
|
63
74
|
// The canonical KPI scale (`--cn-kpi-*`) lives in one stylesheet. Imported
|
|
@@ -85,6 +96,15 @@ const PAGE_REFRESH_BUS_CHANNEL = 'cn:page:refresh'
|
|
|
85
96
|
* `field`, token-resolved `filter`) plus per-entry presentation
|
|
86
97
|
* (`title`, `variant`, `countLabel`), an optional `route` deep link, and
|
|
87
98
|
* `hideWhenZero` (the entry is omitted when its resolved count is 0).
|
|
99
|
+
*
|
|
100
|
+
* An entry formats its value with `format` (a style name such as
|
|
101
|
+
* `"currency"`, or `{ style, currency, currencyField, decimals, prefix,
|
|
102
|
+
* suffix }`), as a `type:"stat"` widget's `content.format` does. A money
|
|
103
|
+
* value takes the currency from `currencyField` (a field of the detail
|
|
104
|
+
* page's object, e.g. a contract's own `currency`), then `currency` (a code
|
|
105
|
+
* or `@config.currency`), then the app's reporting currency, then EUR, and
|
|
106
|
+
* is formatted in the user's locale. `currency` / `currencyField` may sit on
|
|
107
|
+
* the entry itself and imply `format: "currency"`.
|
|
88
108
|
* Exactly one of `dataSource` / `entries` must be provided — when
|
|
89
109
|
* `entries` is absent, the single-`dataSource` path renders exactly as
|
|
90
110
|
* before.
|
|
@@ -140,6 +160,11 @@ export default {
|
|
|
140
160
|
* `filter`. Null on pages that don't provide it.
|
|
141
161
|
*/
|
|
142
162
|
cnWorkspaceContext: { default: null },
|
|
163
|
+
/**
|
|
164
|
+
* Page-level app config provided by the page — the reporting
|
|
165
|
+
* currency (`currency`) and `@config.<key>` tokens in `format`.
|
|
166
|
+
*/
|
|
167
|
+
cnAppConfig: { default: () => ({}) },
|
|
143
168
|
},
|
|
144
169
|
|
|
145
170
|
props: {
|
|
@@ -179,6 +204,20 @@ export default {
|
|
|
179
204
|
default: () => [],
|
|
180
205
|
},
|
|
181
206
|
|
|
207
|
+
/**
|
|
208
|
+
* How the single-source value is shown: a style name (`'currency'`,
|
|
209
|
+
* `'number'`, `'percent'`, `'decimal'`, `'duration-hours'`) or
|
|
210
|
+
* `{ style, currency, currencyField, decimals, prefix, suffix }`.
|
|
211
|
+
* Empty (the default) shows the plain localized count. Entries carry
|
|
212
|
+
* their own `format`.
|
|
213
|
+
*
|
|
214
|
+
* @type {string|object|null}
|
|
215
|
+
*/
|
|
216
|
+
format: {
|
|
217
|
+
type: [String, Object],
|
|
218
|
+
default: null,
|
|
219
|
+
},
|
|
220
|
+
|
|
182
221
|
/** Block title (manifest `widgetDef.title`). */
|
|
183
222
|
title: {
|
|
184
223
|
type: String,
|
|
@@ -329,6 +368,41 @@ export default {
|
|
|
329
368
|
return (typeof c === 'object' && 'value' in c) ? c.value : c
|
|
330
369
|
},
|
|
331
370
|
|
|
371
|
+
/**
|
|
372
|
+
* The page-level app config map (reporting currency, `@config` keys).
|
|
373
|
+
*
|
|
374
|
+
* @return {object}
|
|
375
|
+
*/
|
|
376
|
+
configCtx() {
|
|
377
|
+
return unwrapAppConfig(this.cnAppConfig)
|
|
378
|
+
},
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* The detail page's object, or null on a dashboard. Its fields feed
|
|
382
|
+
* an entry's `currencyField`.
|
|
383
|
+
*
|
|
384
|
+
* @return {object|null}
|
|
385
|
+
*/
|
|
386
|
+
pageObject() {
|
|
387
|
+
const object = this.objectCtx && this.objectCtx.object
|
|
388
|
+
return object && typeof object === 'object' ? object : null
|
|
389
|
+
},
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* The single-source value formatted per `format`, or null to keep
|
|
393
|
+
* the plain count.
|
|
394
|
+
*
|
|
395
|
+
* @return {string|null}
|
|
396
|
+
* @spec openspec/changes/stat-currency-from-object/specs/dashboard-page/spec.md#requirement-a-stats-block-entry-formats-its-value
|
|
397
|
+
*/
|
|
398
|
+
formattedSingle() {
|
|
399
|
+
const fmt = normalizeMetricFormat({ format: this.format })
|
|
400
|
+
if (!fmt || this.loading) {
|
|
401
|
+
return null
|
|
402
|
+
}
|
|
403
|
+
return formatMetricValue(this.resolvedCount, fmt, this.configCtx, this.pageObject)
|
|
404
|
+
},
|
|
405
|
+
|
|
332
406
|
/**
|
|
333
407
|
* Token-resolution context for entry filters, merged from the
|
|
334
408
|
* detail-page object context and the page-level workspace bag.
|
|
@@ -346,14 +420,19 @@ export default {
|
|
|
346
420
|
* its fetched count and loading flag, with `hideWhenZero` entries
|
|
347
421
|
* whose resolved count is 0 omitted.
|
|
348
422
|
*
|
|
349
|
-
* @return {Array<{entry: object, key: string, count: number, loading: boolean}>}
|
|
423
|
+
* @return {Array<{entry: object, key: string, count: number, loading: boolean, formatted: (string|null)}>}
|
|
424
|
+
* @spec openspec/changes/stat-currency-from-object/specs/dashboard-page/spec.md#requirement-a-stats-block-entry-formats-its-value
|
|
350
425
|
*/
|
|
351
426
|
entryViews() {
|
|
352
427
|
return (this.entries || [])
|
|
353
428
|
.map((entry, i) => {
|
|
354
429
|
const raw = this.entryCounts[i]
|
|
355
430
|
const resolved = typeof raw === 'number'
|
|
431
|
+
const fmt = normalizeMetricFormat(entry)
|
|
356
432
|
return {
|
|
433
|
+
// The value as the entry's `format` says, money in the
|
|
434
|
+
// currency the entry names; null keeps the plain count.
|
|
435
|
+
formatted: fmt && resolved ? formatMetricValue(raw, fmt, this.configCtx, this.pageObject) : null,
|
|
357
436
|
entry: entry || {},
|
|
358
437
|
key: `${(entry && entry.register) || ''}/${(entry && entry.schema) || ''}/${i}`,
|
|
359
438
|
count: resolved ? raw : 0,
|