@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
package/src/mixins/pageViews.js
CHANGED
|
@@ -18,11 +18,19 @@
|
|
|
18
18
|
* `layout` with its own grid, so a view looks exactly like the page's own
|
|
19
19
|
* grid.
|
|
20
20
|
*
|
|
21
|
+
* With `userLayout` on, a user arranges each view's grid for themselves, as
|
|
22
|
+
* they arrange the dashboard's own grid. The arrangement is stored per view,
|
|
23
|
+
* per page and per user, through the same user-preference record the
|
|
24
|
+
* dashboard uses (the server, mirrored in the browser). The manifest stays
|
|
25
|
+
* the base: it decides which widgets a view has, the user decides where they
|
|
26
|
+
* sit, and a reset returns the view to the manifest.
|
|
27
|
+
*
|
|
21
28
|
* @mixin pageViews
|
|
22
29
|
* @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-a-page-declares-views-that-each-hold-a-widget-grid
|
|
23
30
|
*/
|
|
24
31
|
import { translate as t } from '@nextcloud/l10n'
|
|
25
32
|
import { readUserPreference, USER_PREFERENCE_STORAGE_PREFIX, writeUserPreference } from '../composables/useUserPreferences.js'
|
|
33
|
+
import { dashboardLayoutKey, LAYOUT_GEOMETRY_FIELDS, mergeUserLayout, resolveUserLayoutApi } from '../store/plugins/dashboardLayouts.js'
|
|
26
34
|
|
|
27
35
|
/** The query parameter that carries the chosen view. */
|
|
28
36
|
export const PAGE_VIEW_QUERY_KEY = 'view'
|
|
@@ -30,18 +38,32 @@ export const PAGE_VIEW_QUERY_KEY = 'view'
|
|
|
30
38
|
/** Prefix of the user-preference key; the page id follows it. */
|
|
31
39
|
export const PAGE_VIEW_PREFERENCE_PREFIX = 'cn_page_view:'
|
|
32
40
|
|
|
41
|
+
/**
|
|
42
|
+
* The page id one view's user layout is stored under: the page's own id,
|
|
43
|
+
* then `.view.`, then the view id. Spelled once, because a read and a write
|
|
44
|
+
* that disagree about the key read as an arrangement that never persists.
|
|
45
|
+
*
|
|
46
|
+
* @param {string} pageKey The page's id.
|
|
47
|
+
* @param {string} viewId The view id.
|
|
48
|
+
* @return {string} The page id handed to the layout store.
|
|
49
|
+
* @spec openspec/changes/page-view-user-layouts/specs/view-switch-containers/spec.md#requirement-a-user-arranges-each-view-for-themselves
|
|
50
|
+
*/
|
|
51
|
+
export function pageViewLayoutId(pageKey, viewId) {
|
|
52
|
+
return `${String(pageKey || 'page')}.view.${String(viewId)}`
|
|
53
|
+
}
|
|
54
|
+
|
|
33
55
|
let regionSeq = 0
|
|
34
56
|
|
|
35
57
|
/**
|
|
36
|
-
* Read the browser mirror of a user preference synchronously, so
|
|
37
|
-
*
|
|
58
|
+
* Read the browser mirror of a user preference synchronously, so a stored
|
|
59
|
+
* value is on screen from the first frame. Errors read as nothing stored.
|
|
38
60
|
*
|
|
39
61
|
* @param {string} appId The app id.
|
|
40
62
|
* @param {string} key The preference key.
|
|
41
|
-
* @return {string|null} The stored
|
|
42
|
-
* @spec openspec/changes/view-
|
|
63
|
+
* @return {object|string|number|boolean|Array|null} The stored value, or null.
|
|
64
|
+
* @spec openspec/changes/page-view-user-layouts/specs/view-switch-containers/spec.md#requirement-a-user-arranges-each-view-for-themselves
|
|
43
65
|
*/
|
|
44
|
-
function
|
|
66
|
+
function readPreferenceMirror(appId, key) {
|
|
45
67
|
try {
|
|
46
68
|
if (typeof localStorage === 'undefined') {
|
|
47
69
|
return null
|
|
@@ -50,19 +72,45 @@ function readStoredViewMirror(appId, key) {
|
|
|
50
72
|
if (raw === null || raw === undefined) {
|
|
51
73
|
return null
|
|
52
74
|
}
|
|
53
|
-
|
|
54
|
-
return typeof value === 'string' && value !== '' ? value : null
|
|
75
|
+
return JSON.parse(raw)
|
|
55
76
|
} catch {
|
|
56
77
|
return null
|
|
57
78
|
}
|
|
58
79
|
}
|
|
59
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Read the browser mirror of the stored view synchronously, so the stored
|
|
83
|
+
* view is on screen from the first frame.
|
|
84
|
+
*
|
|
85
|
+
* @param {string} appId The app id.
|
|
86
|
+
* @param {string} key The preference key.
|
|
87
|
+
* @return {string|null} The stored id, or null.
|
|
88
|
+
* @spec openspec/changes/view-switch-containers/specs/view-switch-containers/spec.md#requirement-the-chosen-view-is-linkable-and-remembered
|
|
89
|
+
*/
|
|
90
|
+
function readStoredViewMirror(appId, key) {
|
|
91
|
+
const value = readPreferenceMirror(appId, key)
|
|
92
|
+
return typeof value === 'string' && value !== '' ? value : null
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Whether a stored layout record holds an arrangement.
|
|
97
|
+
*
|
|
98
|
+
* @param {object|Array<object>|null} record The record, or its items.
|
|
99
|
+
* @return {boolean}
|
|
100
|
+
*/
|
|
101
|
+
function holdsArrangement(record) {
|
|
102
|
+
const items = Array.isArray(record) ? record : (record && Array.isArray(record.items) ? record.items : [])
|
|
103
|
+
return items.length > 0
|
|
104
|
+
}
|
|
105
|
+
|
|
60
106
|
export const pageViews = {
|
|
61
107
|
inject: {
|
|
62
108
|
/** The host app id, provided by CnAppRoot; keys the stored view. */
|
|
63
109
|
cnPageViewsAppId: { from: 'cnAppId', default: '' },
|
|
64
110
|
},
|
|
65
111
|
|
|
112
|
+
emits: ['view-layout-change', 'view-layout-reset'],
|
|
113
|
+
|
|
66
114
|
props: {
|
|
67
115
|
/**
|
|
68
116
|
* Views of this page, each its own widget grid behind one segmented
|
|
@@ -110,6 +158,17 @@ export const pageViews = {
|
|
|
110
158
|
viewChosen: false,
|
|
111
159
|
/** Id of the view region, the target of the options' `aria-controls`. */
|
|
112
160
|
viewRegionId: `cn-page-view-region-${regionSeq}`,
|
|
161
|
+
/**
|
|
162
|
+
* This user's arrangement of each view, by view id, when the page
|
|
163
|
+
* keeps a user layout. A separate copy, never the view's `layout`:
|
|
164
|
+
* that array is the manifest, and writing a user's drag into it
|
|
165
|
+
* would rearrange the view for everybody.
|
|
166
|
+
*
|
|
167
|
+
* @type {Record<string, Array<object>>}
|
|
168
|
+
*/
|
|
169
|
+
viewUserLayouts: {},
|
|
170
|
+
/** The views whose arrangement changed since the last save, by id. */
|
|
171
|
+
viewUserLayoutsDirty: {},
|
|
113
172
|
}
|
|
114
173
|
},
|
|
115
174
|
|
|
@@ -207,6 +266,32 @@ export const pageViews = {
|
|
|
207
266
|
return this.normalizedViews.find((view) => view.id === this.activeViewId) || null
|
|
208
267
|
},
|
|
209
268
|
|
|
269
|
+
/**
|
|
270
|
+
* Whether a user arranges the views for themselves (`userLayout`).
|
|
271
|
+
*
|
|
272
|
+
* @return {boolean}
|
|
273
|
+
* @spec openspec/changes/page-view-user-layouts/specs/view-switch-containers/spec.md#requirement-a-user-arranges-each-view-for-themselves
|
|
274
|
+
*/
|
|
275
|
+
viewUserLayoutOn() {
|
|
276
|
+
return this.userLayout === true && this.hasViews
|
|
277
|
+
},
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* The chosen view's layout: this user's arrangement when they have
|
|
281
|
+
* one, the manifest's otherwise.
|
|
282
|
+
*
|
|
283
|
+
* @return {Array<object>}
|
|
284
|
+
* @spec openspec/changes/page-view-user-layouts/specs/view-switch-containers/spec.md#requirement-a-user-arranges-each-view-for-themselves
|
|
285
|
+
*/
|
|
286
|
+
activeViewLayout() {
|
|
287
|
+
const view = this.activeView
|
|
288
|
+
if (!view) {
|
|
289
|
+
return []
|
|
290
|
+
}
|
|
291
|
+
const arranged = this.viewUserLayoutOn ? this.viewUserLayouts[view.id] : null
|
|
292
|
+
return Array.isArray(arranged) ? arranged : view.layout
|
|
293
|
+
},
|
|
294
|
+
|
|
210
295
|
/**
|
|
211
296
|
* The switch's options, for CnSegmentedControl.
|
|
212
297
|
*
|
|
@@ -288,8 +373,21 @@ export const pageViews = {
|
|
|
288
373
|
},
|
|
289
374
|
},
|
|
290
375
|
|
|
376
|
+
watch: {
|
|
377
|
+
/**
|
|
378
|
+
* Read the user's arrangement of a view the first time it is shown.
|
|
379
|
+
*
|
|
380
|
+
* @param {string|null} id The chosen view id.
|
|
381
|
+
* @return {void}
|
|
382
|
+
*/
|
|
383
|
+
activeViewId(id) {
|
|
384
|
+
this.loadViewUserLayout(id)
|
|
385
|
+
},
|
|
386
|
+
},
|
|
387
|
+
|
|
291
388
|
created() {
|
|
292
389
|
this.loadStoredView()
|
|
390
|
+
this.loadViewUserLayout(this.activeViewId)
|
|
293
391
|
},
|
|
294
392
|
|
|
295
393
|
methods: {
|
|
@@ -361,18 +459,187 @@ export const pageViews = {
|
|
|
361
459
|
},
|
|
362
460
|
|
|
363
461
|
/**
|
|
364
|
-
*
|
|
365
|
-
*
|
|
366
|
-
*
|
|
462
|
+
* The page id a view's arrangement is stored under.
|
|
463
|
+
*
|
|
464
|
+
* @param {string} viewId The view id.
|
|
465
|
+
* @return {string}
|
|
466
|
+
* @spec openspec/changes/page-view-user-layouts/specs/view-switch-containers/spec.md#requirement-a-user-arranges-each-view-for-themselves
|
|
467
|
+
*/
|
|
468
|
+
viewLayoutPageId(viewId) {
|
|
469
|
+
const pageKey = this.viewPreferenceKey.slice(PAGE_VIEW_PREFERENCE_PREFIX.length)
|
|
470
|
+
return pageViewLayoutId(pageKey, viewId)
|
|
471
|
+
},
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* The layout store calls: the page's own (`userLayoutApi`, so an
|
|
475
|
+
* injected `userLayoutStore` covers the views too), else the user
|
|
476
|
+
* preferences.
|
|
477
|
+
*
|
|
478
|
+
* @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>}}
|
|
479
|
+
*/
|
|
480
|
+
viewLayoutApi() {
|
|
481
|
+
if (typeof this.userLayoutApi === 'function') {
|
|
482
|
+
return this.userLayoutApi()
|
|
483
|
+
}
|
|
484
|
+
return resolveUserLayoutApi(null)
|
|
485
|
+
},
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Read this user's arrangement of one view, once: the browser mirror
|
|
489
|
+
* at once, then the server, which wins unless the user already moved
|
|
490
|
+
* something in that view. A page without `userLayout` reads nothing.
|
|
491
|
+
*
|
|
492
|
+
* @param {string|null} viewId The view id.
|
|
493
|
+
* @return {Promise<void>}
|
|
494
|
+
* @spec openspec/changes/page-view-user-layouts/specs/view-switch-containers/spec.md#requirement-a-user-arranges-each-view-for-themselves
|
|
495
|
+
*/
|
|
496
|
+
async loadViewUserLayout(viewId) {
|
|
497
|
+
if (!this.viewUserLayoutOn || viewId === null || viewId === undefined) {
|
|
498
|
+
return
|
|
499
|
+
}
|
|
500
|
+
const view = this.normalizedViews.find((candidate) => candidate.id === String(viewId))
|
|
501
|
+
if (!view || Array.isArray(this.viewUserLayouts[view.id])) {
|
|
502
|
+
return
|
|
503
|
+
}
|
|
504
|
+
const appId = this.viewPreferenceAppId
|
|
505
|
+
const pageId = this.viewLayoutPageId(view.id)
|
|
506
|
+
const mirror = readPreferenceMirror(appId, dashboardLayoutKey(pageId))
|
|
507
|
+
this.viewUserLayouts = { ...this.viewUserLayouts, [view.id]: mergeUserLayout(view.layout, holdsArrangement(mirror) ? mirror : null) }
|
|
508
|
+
if (!appId) {
|
|
509
|
+
return
|
|
510
|
+
}
|
|
511
|
+
let record
|
|
512
|
+
try {
|
|
513
|
+
record = await this.viewLayoutApi().load(appId, pageId)
|
|
514
|
+
} catch {
|
|
515
|
+
// The mirror, or the manifest, stands: a store that cannot
|
|
516
|
+
// answer leaves the view as the user last saw it.
|
|
517
|
+
return
|
|
518
|
+
}
|
|
519
|
+
if (this.viewUserLayoutsDirty[view.id] || !holdsArrangement(record)) {
|
|
520
|
+
return
|
|
521
|
+
}
|
|
522
|
+
this.viewUserLayouts = { ...this.viewUserLayouts, [view.id]: mergeUserLayout(view.layout, record) }
|
|
523
|
+
},
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Store the arrangement of every view the user changed since the last
|
|
527
|
+
* save. The page calls it when the user leaves edit mode, so a drag
|
|
528
|
+
* is not a write per pixel gesture.
|
|
529
|
+
*
|
|
530
|
+
* @return {Promise<void>}
|
|
531
|
+
* @spec openspec/changes/page-view-user-layouts/specs/view-switch-containers/spec.md#requirement-a-user-arranges-each-view-for-themselves
|
|
532
|
+
*/
|
|
533
|
+
async saveViewUserLayouts() {
|
|
534
|
+
if (!this.viewUserLayoutOn) {
|
|
535
|
+
return
|
|
536
|
+
}
|
|
537
|
+
const ids = Object.keys(this.viewUserLayoutsDirty).filter((id) => this.viewUserLayoutsDirty[id])
|
|
538
|
+
this.viewUserLayoutsDirty = {}
|
|
539
|
+
const appId = this.viewPreferenceAppId
|
|
540
|
+
for (const id of ids) {
|
|
541
|
+
const pageId = this.viewLayoutPageId(id)
|
|
542
|
+
const items = this.viewUserLayouts[id] || []
|
|
543
|
+
try {
|
|
544
|
+
if (appId) {
|
|
545
|
+
await this.viewLayoutApi().save(appId, pageId, items)
|
|
546
|
+
} else {
|
|
547
|
+
// No app to address: keep it in this browser only, as
|
|
548
|
+
// the chosen view is kept.
|
|
549
|
+
await writeUserPreference('', dashboardLayoutKey(pageId), { items: items.map((item) => this.viewGeometry(item, true)) })
|
|
550
|
+
}
|
|
551
|
+
} catch {
|
|
552
|
+
// The arrangement stands for this session. A toast on a
|
|
553
|
+
// failed layout write is noise on an instance without the
|
|
554
|
+
// preference route.
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
},
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Drop this user's arrangement of one view and return it to the
|
|
561
|
+
* manifest layout. Other views keep theirs.
|
|
562
|
+
*
|
|
563
|
+
* @param {string} [viewId] The view id; the chosen view when left out.
|
|
564
|
+
* @return {Promise<void>}
|
|
565
|
+
* @spec openspec/changes/page-view-user-layouts/specs/view-switch-containers/spec.md#requirement-resetting-a-view-restores-its-manifest-layout
|
|
566
|
+
*/
|
|
567
|
+
async resetViewUserLayout(viewId) {
|
|
568
|
+
const id = viewId === undefined || viewId === null ? this.activeViewId : String(viewId)
|
|
569
|
+
const view = this.normalizedViews.find((candidate) => candidate.id === id)
|
|
570
|
+
if (!this.viewUserLayoutOn || !view) {
|
|
571
|
+
return
|
|
572
|
+
}
|
|
573
|
+
const appId = this.viewPreferenceAppId
|
|
574
|
+
const pageId = this.viewLayoutPageId(view.id)
|
|
575
|
+
try {
|
|
576
|
+
if (appId) {
|
|
577
|
+
await this.viewLayoutApi().reset(appId, pageId)
|
|
578
|
+
} else {
|
|
579
|
+
await writeUserPreference('', dashboardLayoutKey(pageId), { items: [] })
|
|
580
|
+
}
|
|
581
|
+
} catch {
|
|
582
|
+
// The view returns to the manifest either way, which is what
|
|
583
|
+
// the user asked for.
|
|
584
|
+
}
|
|
585
|
+
const dirty = { ...this.viewUserLayoutsDirty }
|
|
586
|
+
delete dirty[view.id]
|
|
587
|
+
this.viewUserLayoutsDirty = dirty
|
|
588
|
+
this.viewUserLayouts = { ...this.viewUserLayouts, [view.id]: mergeUserLayout(view.layout, null) }
|
|
589
|
+
/**
|
|
590
|
+
* Emitted when the user drops their arrangement of a view and it
|
|
591
|
+
* returns to the manifest layout.
|
|
592
|
+
*
|
|
593
|
+
* @event view-layout-reset
|
|
594
|
+
* @type {{view: string}}
|
|
595
|
+
*/
|
|
596
|
+
this.$emit('view-layout-reset', { view: view.id })
|
|
597
|
+
},
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* The geometry fields of one layout item, and nothing else.
|
|
601
|
+
*
|
|
602
|
+
* @param {object} item A layout item, or a grid update.
|
|
603
|
+
* @param {boolean} [withWidgetId] Whether to keep the widget id too.
|
|
604
|
+
* @return {object}
|
|
605
|
+
*/
|
|
606
|
+
viewGeometry(item, withWidgetId = false) {
|
|
607
|
+
const geometry = withWidgetId ? { widgetId: String(item?.widgetId ?? '') } : {}
|
|
608
|
+
for (const field of LAYOUT_GEOMETRY_FIELDS) {
|
|
609
|
+
if (item?.[field] !== undefined) {
|
|
610
|
+
geometry[field] = item[field]
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
return geometry
|
|
614
|
+
},
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* A drag or resize in the chosen view's grid. With `userLayout` it
|
|
618
|
+
* moves this user's copy of the view and marks it for saving; the
|
|
619
|
+
* view's manifest `layout` is not touched. Without it the geometry is
|
|
620
|
+
* written back onto the layout items in place, as the page does for
|
|
621
|
+
* its own layout, so the in-app manifest editor sees it.
|
|
367
622
|
*
|
|
368
623
|
* @param {Array<object>} updated The layout from the grid.
|
|
369
624
|
* @return {void}
|
|
625
|
+
* @spec openspec/changes/page-view-user-layouts/specs/view-switch-containers/spec.md#requirement-a-user-arranges-each-view-for-themselves
|
|
370
626
|
*/
|
|
371
627
|
onViewLayoutChange(updated) {
|
|
372
628
|
const view = this.activeView
|
|
373
629
|
if (!view || !Array.isArray(updated)) {
|
|
374
630
|
return
|
|
375
631
|
}
|
|
632
|
+
if (this.viewUserLayoutOn) {
|
|
633
|
+
const current = Array.isArray(this.viewUserLayouts[view.id]) ? this.viewUserLayouts[view.id] : mergeUserLayout(view.layout, null)
|
|
634
|
+
const arranged = current.map((item) => {
|
|
635
|
+
const u = updated.find((candidate) => String(candidate.id) === String(item.id))
|
|
636
|
+
return u ? { ...item, ...this.viewGeometry(u) } : item
|
|
637
|
+
})
|
|
638
|
+
this.viewUserLayouts = { ...this.viewUserLayouts, [view.id]: arranged }
|
|
639
|
+
this.viewUserLayoutsDirty = { ...this.viewUserLayoutsDirty, [view.id]: true }
|
|
640
|
+
this.$emit('view-layout-change', { view: view.id, layout: arranged })
|
|
641
|
+
return
|
|
642
|
+
}
|
|
376
643
|
for (const u of updated) {
|
|
377
644
|
const item = view.layout.find((l) => String(l.id) === String(u.id))
|
|
378
645
|
if (!item) {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"$id": "https://raw.githubusercontent.com/ConductionNL/nextcloud-vue/main/src/schemas/app-manifest-v2.schema.json",
|
|
4
4
|
"title": "Conduction App Manifest v2",
|
|
5
5
|
"description": "v2 schema for the JSON-driven page and navigation manifest consumed by @conduction/nextcloud-vue. Introduces a uniform widgets[] array on every page type with a per-slot grid coordinate system, a typed actions[] discriminator, and the required $schema field for version detection. v1 manifests continue to validate against app-manifest.schema.json (unchanged).",
|
|
6
|
-
"version": "2.
|
|
6
|
+
"version": "2.52.0",
|
|
7
7
|
"type": "object",
|
|
8
8
|
"required": ["$schema", "version"],
|
|
9
9
|
"allOf": [
|
|
@@ -1569,6 +1569,11 @@
|
|
|
1569
1569
|
"href": { "type": "string" }
|
|
1570
1570
|
}
|
|
1571
1571
|
},
|
|
1572
|
+
"showButtons": {
|
|
1573
|
+
"type": "boolean",
|
|
1574
|
+
"default": true,
|
|
1575
|
+
"description": "`false` drops the widget's footer links (its definition's `buttons`, e.g. a calendar's \"More events\"), for a placement whose card says enough without them (zuiddrecht-pixel-gaps-3)."
|
|
1576
|
+
},
|
|
1572
1577
|
"sizeToContent": {
|
|
1573
1578
|
"type": ["boolean", "integer"],
|
|
1574
1579
|
"minimum": 1,
|
|
@@ -2195,7 +2200,8 @@
|
|
|
2195
2200
|
}
|
|
2196
2201
|
},
|
|
2197
2202
|
"emptyText": { "type": "string", "description": "Text of a day without items." },
|
|
2198
|
-
"weekOffset": { "type": "integer", "description": "Whole weeks from the current one (1 = next week)." }
|
|
2203
|
+
"weekOffset": { "type": "integer", "description": "Whole weeks from the current one (1 = next week)." },
|
|
2204
|
+
"inset": { "type": "boolean", "default": false, "description": "Draw the strip inside the board's inset (16px above, 24px at the sides, 22px below) instead of from card edge to card edge (zuiddrecht-pixel-gaps-3). Theme hook --cn-widget-board-inset." }
|
|
2199
2205
|
}
|
|
2200
2206
|
}
|
|
2201
2207
|
}
|
|
@@ -2246,7 +2252,8 @@
|
|
|
2246
2252
|
},
|
|
2247
2253
|
"order": { "type": "array", "items": { "type": "string" }, "description": "Group keys in the order they render. Unlisted groups follow; a listed key without records shows 0." },
|
|
2248
2254
|
"labels": { "type": "object", "additionalProperties": { "type": "string" }, "description": "The label of a group key." },
|
|
2249
|
-
"emptyText": { "type": "string", "description": "Shown when the total is 0." }
|
|
2255
|
+
"emptyText": { "type": "string", "description": "Shown when the total is 0." },
|
|
2256
|
+
"inset": { "type": "boolean", "default": false, "description": "Draw the bar and legend inside the board's inset (16px above, 24px at the sides, 22px below) instead of from card edge to card edge (zuiddrecht-pixel-gaps-3). Theme hook --cn-widget-board-inset." }
|
|
2250
2257
|
}
|
|
2251
2258
|
}
|
|
2252
2259
|
}
|
|
@@ -2903,7 +2910,7 @@
|
|
|
2903
2910
|
"type": "object",
|
|
2904
2911
|
"required": ["register", "schema"],
|
|
2905
2912
|
"additionalProperties": false,
|
|
2906
|
-
"description": "One KPI entry inside a multi-entry stats-block widget (CnStatsBlockWidget `entries[]`). Carries the same source contract as a type:'stat' widget: register + schema + metric + token-resolved filter (@today / @me / @workspace.* and ?-optional clauses), plus per-entry presentation (title, variant, countLabel), an optional route deep link, and hideWhenZero.",
|
|
2913
|
+
"description": "One KPI entry inside a multi-entry stats-block widget (CnStatsBlockWidget `entries[]`). Carries the same source contract as a type:'stat' widget: register + schema + metric + token-resolved filter (@today / @me / @workspace.* and ?-optional clauses), plus per-entry presentation (title, variant, countLabel, format), an optional route deep link, and hideWhenZero. A money value names its currency with currencyField (the page object's field) or currency.",
|
|
2907
2914
|
"properties": {
|
|
2908
2915
|
"title": {
|
|
2909
2916
|
"type": "string",
|
|
@@ -2952,6 +2959,32 @@
|
|
|
2952
2959
|
"type": "boolean",
|
|
2953
2960
|
"default": false,
|
|
2954
2961
|
"description": "When true, the entry is omitted from the card when its resolved count is 0."
|
|
2962
|
+
},
|
|
2963
|
+
"format": {
|
|
2964
|
+
"description": "How the value is shown, as a type:'stat' widget's content.format: a style name, or { style, currency, currencyField, decimals, prefix, suffix }. Omitted, the plain localized count is shown. Numbers are formatted with Intl in the user's locale.",
|
|
2965
|
+
"oneOf": [
|
|
2966
|
+
{ "type": "string", "enum": ["number", "currency", "percent", "decimal", "duration-hours"] },
|
|
2967
|
+
{
|
|
2968
|
+
"type": "object",
|
|
2969
|
+
"additionalProperties": false,
|
|
2970
|
+
"properties": {
|
|
2971
|
+
"style": { "type": "string", "enum": ["number", "currency", "percent", "decimal", "duration-hours"] },
|
|
2972
|
+
"currency": { "type": "string", "description": "ISO-4217 code, or a token such as @config.currency or @object.currency." },
|
|
2973
|
+
"currencyField": { "type": "string", "description": "Field of the detail page's object that holds the currency (dot-path allowed)." },
|
|
2974
|
+
"decimals": { "type": "integer", "minimum": 0, "description": "Fraction digits." },
|
|
2975
|
+
"prefix": { "type": "string" },
|
|
2976
|
+
"suffix": { "type": "string" }
|
|
2977
|
+
}
|
|
2978
|
+
}
|
|
2979
|
+
]
|
|
2980
|
+
},
|
|
2981
|
+
"currency": {
|
|
2982
|
+
"type": "string",
|
|
2983
|
+
"description": "Shorthand for format.currency: an ISO-4217 code, or a token such as @config.currency. Implies format 'currency'."
|
|
2984
|
+
},
|
|
2985
|
+
"currencyField": {
|
|
2986
|
+
"type": "string",
|
|
2987
|
+
"description": "Shorthand for format.currencyField: the field of the detail page's object that holds the currency, e.g. 'currency'. Implies format 'currency'. The currency is taken from the object, then currency, then the app's reporting currency (config currency), then EUR."
|
|
2955
2988
|
}
|
|
2956
2989
|
}
|
|
2957
2990
|
},
|
|
@@ -3655,7 +3688,12 @@
|
|
|
3655
3688
|
"showWidgetActions": {
|
|
3656
3689
|
"type": "boolean",
|
|
3657
3690
|
"default": true,
|
|
3658
|
-
"description": "type='dashboard': `false` drops the overflow Actions menu from every widget that does not set `showActions` itself, for a dashboard whose widget headers carry a header link and nothing else (zuiddrecht-pixel-gaps-2). Maps to CnDashboardPage `showWidgetActions`."
|
|
3691
|
+
"description": "type='dashboard' and type='detail': `false` drops the overflow Actions menu from every widget that does not set `showActions` itself, for a dashboard whose widget headers carry a header link and nothing else (zuiddrecht-pixel-gaps-2), or a record page whose data cards carry none (zuiddrecht-pixel-gaps-3; a card with an Add action keeps it). Maps to CnDashboardPage / CnDetailPage `showWidgetActions`."
|
|
3692
|
+
},
|
|
3693
|
+
"gridFloat": {
|
|
3694
|
+
"type": "boolean",
|
|
3695
|
+
"default": true,
|
|
3696
|
+
"description": "type='dashboard': `false` packs widgets upward (GridStack `float: false`), so a size-to-content widget that shrinks closes the gap under it instead of leaving an empty band (zuiddrecht-pixel-gaps-3). Maps to CnDashboardPage `gridFloat`."
|
|
3659
3697
|
},
|
|
3660
3698
|
"showTitle": {
|
|
3661
3699
|
"type": "boolean",
|
|
@@ -3666,6 +3704,33 @@
|
|
|
3666
3704
|
"type": "string",
|
|
3667
3705
|
"description": "type='index': an i18n key for the description line with `{total}` replaced by the collection's total, e.g. \"{total} open cases\" (zuiddrecht-pixel-gaps). Shown in place of `description` once a total is known. Maps to CnIndexPage `countSubtitle`."
|
|
3668
3706
|
},
|
|
3707
|
+
"showTitleIcon": {
|
|
3708
|
+
"type": "boolean",
|
|
3709
|
+
"default": true,
|
|
3710
|
+
"description": "type='index': `false` drops the icon before the page title, for a title that stands alone as the board draws it (zuiddrecht-pixel-gaps-3). Maps to CnIndexPage `showTitleIcon`."
|
|
3711
|
+
},
|
|
3712
|
+
"showCount": {
|
|
3713
|
+
"type": "boolean",
|
|
3714
|
+
"default": true,
|
|
3715
|
+
"description": "type='index': `false` drops the actions bar's \"Showing 20 of 258\" line, for a page whose title line already gives the total through `countSubtitle` (zuiddrecht-pixel-gaps-3). Maps to CnIndexPage `showCount`."
|
|
3716
|
+
},
|
|
3717
|
+
"headerButtons": {
|
|
3718
|
+
"type": "array",
|
|
3719
|
+
"description": "type='index': buttons beside the page title, as the board's \"Export\" and \"New case\" (zuiddrecht-pixel-gaps-3). `action` is `add` (the Add flow; the label defaults to the Add label), `export` (the export leaf in `format`, else the export dialog), `import`, `refresh`, or the id of a `headerActions` entry. Shown only with `showTitle`; when they show, the actions bar drops its Views and Actions menus, and the Add button and Export menu when a button takes their action. Maps to CnIndexPage `headerButtons`.",
|
|
3720
|
+
"items": {
|
|
3721
|
+
"type": "object",
|
|
3722
|
+
"additionalProperties": false,
|
|
3723
|
+
"required": ["action"],
|
|
3724
|
+
"properties": {
|
|
3725
|
+
"id": { "type": "string", "minLength": 1 },
|
|
3726
|
+
"label": { "type": "string", "minLength": 1, "description": "i18n key; optional for `add`." },
|
|
3727
|
+
"action": { "type": "string", "minLength": 1 },
|
|
3728
|
+
"variant": { "type": "string", "enum": ["primary", "secondary"], "default": "secondary" },
|
|
3729
|
+
"icon": { "type": "string", "minLength": 1, "description": "MDI icon name." },
|
|
3730
|
+
"format": { "type": "string", "enum": ["csv", "excel"], "default": "csv", "description": "For `export`." }
|
|
3731
|
+
}
|
|
3732
|
+
}
|
|
3733
|
+
},
|
|
3669
3734
|
"showTypeEyebrow": {
|
|
3670
3735
|
"type": "boolean",
|
|
3671
3736
|
"default": true,
|
|
@@ -3681,7 +3746,9 @@
|
|
|
3681
3746
|
"route": { "type": "string" },
|
|
3682
3747
|
"params": { "type": "object", "additionalProperties": true },
|
|
3683
3748
|
"href": { "type": "string" },
|
|
3684
|
-
"icon": { "type": "string", "minLength": 1, "description": "Optional MDI icon name (e.g. `Home`) drawn as the first crumb instead of the label, which then becomes its accessible name (zuiddrecht-pixel-gaps-2). Without it the label shows as text." }
|
|
3749
|
+
"icon": { "type": "string", "minLength": 1, "description": "Optional MDI icon name (e.g. `Home`) drawn as the first crumb instead of the label, which then becomes its accessible name (zuiddrecht-pixel-gaps-2). Without it the label shows as text." },
|
|
3750
|
+
"currentField": { "type": "string", "minLength": 1, "description": "A field of the record (dotted path, e.g. `identifier`) whose value is the current crumb instead of the display name; an empty value falls back to the display name (zuiddrecht-pixel-gaps-3)." },
|
|
3751
|
+
"separator": { "type": "string", "minLength": 1, "maxLength": 3, "description": "Text drawn between the crumbs (e.g. `/`) instead of the chevron (zuiddrecht-pixel-gaps-3)." }
|
|
3685
3752
|
}
|
|
3686
3753
|
},
|
|
3687
3754
|
"headerCard": {
|
|
@@ -156,6 +156,40 @@ export function mergeUserLayout(manifestLayout, record) {
|
|
|
156
156
|
return [...arranged, ...appended]
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
+
/**
|
|
160
|
+
* The three calls a user layout needs: from a store that carries the
|
|
161
|
+
* `dashboardLayoutsPlugin` actions, or straight over user preferences.
|
|
162
|
+
*
|
|
163
|
+
* Both paths write through `writeUserPreference`, so a record lands on the
|
|
164
|
+
* server and in the browser mirror whichever one a page uses. CnDashboardPage
|
|
165
|
+
* reads its own grid's record through this, and the page views mixin reads
|
|
166
|
+
* each view's record through it, so the two cannot disagree about where a
|
|
167
|
+
* user's arrangement lives.
|
|
168
|
+
*
|
|
169
|
+
* @param {object|null} [store] A store with `loadDashboardLayout`, `saveDashboardLayout` and `resetDashboardLayout`, or null.
|
|
170
|
+
* @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.
|
|
171
|
+
* @spec openspec/changes/page-view-user-layouts/specs/view-switch-containers/spec.md#requirement-a-user-arranges-each-view-for-themselves
|
|
172
|
+
*/
|
|
173
|
+
export function resolveUserLayoutApi(store = null) {
|
|
174
|
+
if (store && typeof store.loadDashboardLayout === 'function') {
|
|
175
|
+
return {
|
|
176
|
+
load: (...a) => store.loadDashboardLayout(...a),
|
|
177
|
+
save: (...a) => store.saveDashboardLayout(...a),
|
|
178
|
+
reset: (...a) => store.resetDashboardLayout(...a),
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
load: (appId, pageId) => readUserPreference(appId, dashboardLayoutKey(pageId), null),
|
|
184
|
+
save: (appId, pageId, layout) => writeUserPreference(
|
|
185
|
+
appId,
|
|
186
|
+
dashboardLayoutKey(pageId),
|
|
187
|
+
{ items: (Array.isArray(layout) ? layout : []).map(geometryOf).filter((entry) => entry.widgetId !== '') },
|
|
188
|
+
),
|
|
189
|
+
reset: (appId, pageId) => writeUserPreference(appId, dashboardLayoutKey(pageId), { items: [] }),
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
159
193
|
/**
|
|
160
194
|
* Plugin definition.
|
|
161
195
|
*
|
|
@@ -22,6 +22,34 @@
|
|
|
22
22
|
* @module utils/capabilityComparison
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
|
+
/*
|
|
26
|
+
* `process.env.NODE_ENV` is substituted by every consuming app's bundler at
|
|
27
|
+
* build time (webpack's DefinePlugin, rollup's replace); see NcSelectTags.js.
|
|
28
|
+
*/
|
|
29
|
+
/* global process */
|
|
30
|
+
|
|
31
|
+
let deprecationWarned = false
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Warn once per page load, in a development build only, that the in-app
|
|
35
|
+
* capability comparison is deprecated. Called by CnFeaturesAndRoadmapPage,
|
|
36
|
+
* CnFeaturesAndRoadmapView and CnCapabilityTable when a comparison is
|
|
37
|
+
* actually given, so an app that passes none hears nothing.
|
|
38
|
+
*
|
|
39
|
+
* @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-the-in-app-capability-comparison-is-deprecated
|
|
40
|
+
* @param {string} source What carried the comparison, for the message.
|
|
41
|
+
* @return {boolean} True when this call printed the warning.
|
|
42
|
+
*/
|
|
43
|
+
export function warnCapabilityComparisonDeprecated(source) {
|
|
44
|
+
if (deprecationWarned || process.env.NODE_ENV === 'production') {
|
|
45
|
+
return false
|
|
46
|
+
}
|
|
47
|
+
deprecationWarned = true
|
|
48
|
+
// eslint-disable-next-line no-console -- a deliberate deprecation notice to the developer integrating the component
|
|
49
|
+
console.warn(`[nextcloud-vue] ${source} is deprecated: the in-app capability comparison (capabilityComparison, the features_roadmap_capabilities initial state, CnCapabilityTable) will be removed in a future major version. Publish the comparison on the app's docs site and link to it instead.`)
|
|
50
|
+
return true
|
|
51
|
+
}
|
|
52
|
+
|
|
25
53
|
/**
|
|
26
54
|
* Ratings in the order they are counted and rendered.
|
|
27
55
|
*
|