@conduction/nextcloud-vue 2.77.0 → 2.79.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/bundled-packages.json +0 -2
- package/dist/esm/bundled-packages.json +0 -2
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue2.js.map +1 -1
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js +113 -104
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js.map +1 -1
- package/dist/esm/components/CnAiCompanion/CnAiCompanion.vue2.js +23 -0
- package/dist/esm/components/CnAiCompanion/CnAiCompanion.vue2.js.map +1 -1
- package/dist/esm/components/CnAiCompanion/CnAiCompanion.vue3.js.map +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +12 -2
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
- package/dist/esm/components/CnBoardView/CnBoardView.vue2.js +2 -1
- package/dist/esm/components/CnBoardView/CnBoardView.vue2.js.map +1 -1
- package/dist/esm/components/CnBoardView/CnBoardView.vue3.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +117 -0
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +122 -97
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
- package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js +37 -0
- package/dist/esm/components/CnChartWidget/CnChartWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js +2 -2
- package/dist/esm/components/CnChartWidget/CnChartWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +168 -5
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js +140 -92
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +84 -2
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +4 -2
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
- package/dist/esm/components/CnDateRangePicker/CnDateRangePicker.vue.js +1 -1
- package/dist/esm/components/CnDateRangePicker/CnDateRangePicker.vue2.js +45 -2
- package/dist/esm/components/CnDateRangePicker/CnDateRangePicker.vue2.js.map +1 -1
- package/dist/esm/components/CnDateRangePicker/CnDateRangePicker.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js +61 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailWidgetHost/CnDetailWidgetHost.vue3.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +173 -5
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +181 -99
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/filesBrowser.js +29 -11
- package/dist/esm/components/CnFilesBrowser/filesBrowser.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/openRegisterSource.js +252 -0
- package/dist/esm/components/CnFilesBrowser/openRegisterSource.js.map +1 -0
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js +8 -2
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue2.js.map +1 -1
- package/dist/esm/components/CnFormDialog/CnFormDialog.vue3.js.map +1 -1
- package/dist/esm/components/CnFormPage/CnFormPage.vue2.js +4 -1
- package/dist/esm/components/CnFormPage/CnFormPage.vue2.js.map +1 -1
- package/dist/esm/components/CnFormPage/CnFormPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +31 -2
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +3 -2
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js +100 -3
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +4 -2
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
- package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue2.js +3 -2
- package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue2.js.map +1 -1
- package/dist/esm/components/CnQuickFilterBar/CnQuickFilterBar.vue3.js.map +1 -1
- package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue2.js.map +1 -1
- package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue3.js +1 -1
- package/dist/esm/components/CnSavedViewsControl/CnSavedViewsControl.vue3.js.map +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js +36 -0
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js +35 -23
- package/dist/esm/components/CnStatWidget/CnStatWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnStatusBadge/CnStatusBadge.vue2.js +6 -2
- package/dist/esm/components/CnStatusBadge/CnStatusBadge.vue2.js.map +1 -1
- package/dist/esm/components/CnStatusBadge/CnStatusBadge.vue3.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js +30 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnTabsWidget/CnTabsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js +13 -0
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js.map +1 -1
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue3.js.map +1 -1
- package/dist/esm/composables/useAppManifest.js +8 -3
- package/dist/esm/composables/useAppManifest.js.map +1 -1
- package/dist/esm/composables/useFormDraft.js +11 -1
- package/dist/esm/composables/useFormDraft.js.map +1 -1
- package/dist/esm/composables/useObjectPresence.js +15 -4
- package/dist/esm/composables/useObjectPresence.js.map +1 -1
- package/dist/esm/index.js +1 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/l10n/en.json.js +68 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +113 -2
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/nextcloud-vue.css +526 -16
- package/dist/esm/utils/badgeVariants.js +32 -0
- package/dist/esm/utils/badgeVariants.js.map +1 -0
- package/dist/esm/utils/boardDate.js +193 -0
- package/dist/esm/utils/boardDate.js.map +1 -0
- package/dist/esm/utils/dynamicProperties.js +5 -1
- package/dist/esm/utils/dynamicProperties.js.map +1 -1
- package/dist/esm/utils/validateManifestV2.compiled.js +57 -6
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +2569 -811
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +526 -16
- package/l10n/en.json +68 -1
- package/l10n/nl.json +113 -2
- package/package.json +1 -1
- package/scripts/build-validators.js +26 -0
- package/src/components/CnActionsBar/CnActionsBar.vue +48 -39
- package/src/components/CnAiCompanion/CnAiCompanion.vue +23 -0
- package/src/components/CnAppRoot/CnAppRoot.vue +12 -2
- package/src/components/CnBoardView/CnBoardView.vue +2 -1
- package/src/components/CnCellRenderer/CnCellRenderer.md +1 -0
- package/src/components/CnCellRenderer/CnCellRenderer.vue +169 -10
- package/src/components/CnChartWidget/CnChartWidget.vue +39 -2
- package/src/components/CnDashboardPage/CnDashboardPage.vue +248 -8
- package/src/components/CnDataTable/CnDataTable.md +2 -0
- package/src/components/CnDataTable/CnDataTable.vue +88 -4
- package/src/components/CnDateRangePicker/CnDateRangePicker.vue +44 -1
- package/src/components/CnDetailWidgetHost/CnDetailWidgetHost.vue +61 -1
- package/src/components/CnFilesBrowser/CnFilesBrowser.vue +232 -11
- package/src/components/CnFilesBrowser/filesBrowser.js +43 -2
- package/src/components/CnFilesBrowser/index.js +1 -0
- package/src/components/CnFilesBrowser/openRegisterSource.js +248 -0
- package/src/components/CnFormDialog/CnFormDialog.vue +8 -2
- package/src/components/CnFormPage/CnFormPage.vue +4 -1
- package/src/components/CnIndexPage/CnIndexPage.vue +33 -3
- package/src/components/CnObjectSidebar/CnFilesTab.vue +103 -4
- package/src/components/CnQuickFilterBar/CnQuickFilterBar.vue +3 -2
- package/src/components/CnSavedViewsControl/CnSavedViewsControl.vue +1 -1
- package/src/components/CnStatWidget/CnStatWidget.vue +52 -2
- package/src/components/CnStatusBadge/CnStatusBadge.vue +6 -2
- package/src/components/CnTabsWidget/CnTabsWidget.vue +30 -1
- package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +13 -0
- package/src/components/index.js +1 -1
- package/src/composables/__tests__/useObjectPresence.spec.js +24 -0
- package/src/composables/useAppManifest.js +8 -3
- package/src/composables/useFormDraft.js +11 -1
- package/src/composables/useObjectPresence.js +15 -4
- package/src/css/badge.css +31 -0
- package/src/css/kpi-card.css +51 -0
- package/src/css/look-board-detail.css +10 -0
- package/src/css/look-board-index.css +128 -14
- package/src/css/look-board.css +85 -0
- package/src/css/table.css +37 -0
- package/src/index.js +4 -0
- package/src/schemas/app-manifest-v2.schema.json +50 -5
- package/src/utils/badgeVariants.js +29 -0
- package/src/utils/boardDate.js +189 -0
- package/src/utils/dynamicProperties.js +5 -1
- package/src/utils/validateManifestV2.compiled.js +57 -2
- package/dist/esm/_virtual/equal.js +0 -4
- package/dist/esm/_virtual/equal.js.map +0 -1
- package/dist/esm/_virtual/ucs2length.js +0 -4
- package/dist/esm/_virtual/ucs2length.js.map +0 -1
- package/dist/esm/node_modules/ajv/dist/runtime/equal.js +0 -9
- package/dist/esm/node_modules/ajv/dist/runtime/equal.js.map +0 -1
- package/dist/esm/node_modules/ajv/dist/runtime/ucs2length.js +0 -25
- package/dist/esm/node_modules/ajv/dist/runtime/ucs2length.js.map +0 -1
- package/dist/esm/node_modules/fast-deep-equal/index.js +0 -47
- package/dist/esm/node_modules/fast-deep-equal/index.js.map +0 -1
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SPDX-FileCopyrightText: 2026 Conduction B.V.
|
|
3
|
+
* SPDX-License-Identifier: EUPL-1.2
|
|
4
|
+
*
|
|
5
|
+
* A data source for CnFilesBrowser that reads and writes one OpenRegister
|
|
6
|
+
* object's folder through OpenRegister's own files API, not WebDAV.
|
|
7
|
+
*
|
|
8
|
+
* Why: an object's files live in the `openregister` account's home, outside
|
|
9
|
+
* every person's Files app, so a reader has no WebDAV path to them. Every call
|
|
10
|
+
* here goes through the object's access rule on the server, as the signed-in
|
|
11
|
+
* person: a listing needs read on the object, a change needs update. Nothing
|
|
12
|
+
* is shared to make the folder visible.
|
|
13
|
+
*
|
|
14
|
+
* Paths are relative to the object folder and written with a leading slash
|
|
15
|
+
* (`/`, `/Bijlagen`), the same shape the WebDAV browser uses for its own
|
|
16
|
+
* paths, so crumbs and navigation work unchanged.
|
|
17
|
+
*
|
|
18
|
+
* @spec openspec/changes/files-browser-openregister-source/specs/files-browser/spec.md
|
|
19
|
+
*/
|
|
20
|
+
import axios from '@nextcloud/axios'
|
|
21
|
+
import { FileType } from '@nextcloud/files'
|
|
22
|
+
import { generateUrl } from '@nextcloud/router'
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A browser path (`/a/b`) as the API's relative path (`a/b`).
|
|
26
|
+
*
|
|
27
|
+
* @param {string} path The browser path.
|
|
28
|
+
* @return {string} The relative path, empty for the root.
|
|
29
|
+
*/
|
|
30
|
+
export function toApiPath(path) {
|
|
31
|
+
return String(path || '').replace(/^\/+|\/+$/g, '')
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* One API listing entry as a row the browser can render and sort.
|
|
36
|
+
*
|
|
37
|
+
* The shape follows what the browser reads off an `@nextcloud/files` node
|
|
38
|
+
* (basename, type, mime, size, mtime, fileid, path, source, attributes), so
|
|
39
|
+
* the table, the sorter and the icons need no second code path.
|
|
40
|
+
*
|
|
41
|
+
* @param {object} entry The API entry: `{ id, name, type, mimetype, size, mtime, path }`.
|
|
42
|
+
* @return {object} The row.
|
|
43
|
+
*/
|
|
44
|
+
export function entryToNode(entry) {
|
|
45
|
+
const isFolder = entry.type === 'folder'
|
|
46
|
+
return {
|
|
47
|
+
source: `openregister:${entry.id}`,
|
|
48
|
+
fileid: entry.id,
|
|
49
|
+
basename: entry.name,
|
|
50
|
+
displayname: entry.name,
|
|
51
|
+
path: `/${toApiPath(entry.path)}`,
|
|
52
|
+
type: isFolder ? FileType.Folder : FileType.File,
|
|
53
|
+
mime: isFolder ? 'httpd/unix-directory' : (entry.mimetype || ''),
|
|
54
|
+
size: typeof entry.size === 'number' ? entry.size : undefined,
|
|
55
|
+
mtime: typeof entry.mtime === 'number' ? new Date(entry.mtime * 1000) : undefined,
|
|
56
|
+
attributes: {},
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The status of a failed request, or null.
|
|
62
|
+
*
|
|
63
|
+
* @param {Error} err The error.
|
|
64
|
+
* @return {number|null} The HTTP status.
|
|
65
|
+
*/
|
|
66
|
+
function statusOf(err) {
|
|
67
|
+
return err?.response?.status ?? err?.status ?? null
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The server's own message on a failed request, or null.
|
|
72
|
+
*
|
|
73
|
+
* @param {Error} err The error.
|
|
74
|
+
* @return {string|null} The message.
|
|
75
|
+
*/
|
|
76
|
+
function messageOf(err) {
|
|
77
|
+
const data = err?.response?.data
|
|
78
|
+
return (data && typeof data.error === 'string' && data.error !== '') ? data.error : null
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Build the OpenRegister source for one object.
|
|
83
|
+
*
|
|
84
|
+
* @param {object} options The object.
|
|
85
|
+
* @param {string} [options.apiBase] OpenRegister's API base, `/apps/openregister/api`.
|
|
86
|
+
* @param {string} options.register The register slug or id.
|
|
87
|
+
* @param {string} options.schema The schema slug or id.
|
|
88
|
+
* @param {string} options.objectId The object's uuid.
|
|
89
|
+
* @return {object} The source CnFilesBrowser takes as `source`.
|
|
90
|
+
*/
|
|
91
|
+
export function createOpenRegisterSource({ apiBase = '/apps/openregister/api', register, schema, objectId }) {
|
|
92
|
+
const objectUrl = (suffix) => generateUrl(`${apiBase}/objects/{register}/{schema}/{objectId}${suffix}`, { register, schema, objectId })
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Re-throw a failed request with its status and the server's message.
|
|
96
|
+
*
|
|
97
|
+
* @param {Error} err The error.
|
|
98
|
+
* @return {never}
|
|
99
|
+
*/
|
|
100
|
+
const fail = (err) => {
|
|
101
|
+
const error = new Error(messageOf(err) || err?.message || 'Request failed')
|
|
102
|
+
error.status = statusOf(err)
|
|
103
|
+
throw error
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
kind: 'openregister',
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* List a folder.
|
|
111
|
+
*
|
|
112
|
+
* @param {string} path The browser path.
|
|
113
|
+
* @return {Promise<{folder: object, nodes: Array<object>}>} The folder (with `canChange`) and its rows.
|
|
114
|
+
*/
|
|
115
|
+
async list(path) {
|
|
116
|
+
try {
|
|
117
|
+
const { data } = await axios.get(objectUrl('/folder'), { params: { path: toApiPath(path) } })
|
|
118
|
+
if (!Array.isArray(data?.entries)) {
|
|
119
|
+
// Not a folder listing: an OpenRegister without these endpoints.
|
|
120
|
+
const error = new Error('This OpenRegister has no folder endpoint')
|
|
121
|
+
error.status = 501
|
|
122
|
+
throw error
|
|
123
|
+
}
|
|
124
|
+
const relative = toApiPath(data?.path ?? path)
|
|
125
|
+
return {
|
|
126
|
+
folder: {
|
|
127
|
+
fileid: data?.folderId ?? null,
|
|
128
|
+
path: `/${relative}`,
|
|
129
|
+
basename: relative.split('/').pop() || '',
|
|
130
|
+
type: FileType.Folder,
|
|
131
|
+
canChange: data?.canChange === true,
|
|
132
|
+
},
|
|
133
|
+
nodes: data.entries.map(entryToNode),
|
|
134
|
+
}
|
|
135
|
+
} catch (err) {
|
|
136
|
+
if (err?.status && !err.response) {
|
|
137
|
+
throw err
|
|
138
|
+
}
|
|
139
|
+
return fail(err)
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Create a folder.
|
|
145
|
+
*
|
|
146
|
+
* @param {string} path Where, as a browser path.
|
|
147
|
+
* @param {string} name The new folder's name.
|
|
148
|
+
* @return {Promise<object>} The new folder's row.
|
|
149
|
+
*/
|
|
150
|
+
async createFolder(path, name) {
|
|
151
|
+
try {
|
|
152
|
+
const { data } = await axios.post(objectUrl('/folder'), { path: toApiPath(path), name })
|
|
153
|
+
return entryToNode(data)
|
|
154
|
+
} catch (err) {
|
|
155
|
+
return fail(err)
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Upload one file into a folder.
|
|
161
|
+
*
|
|
162
|
+
* @param {string} path The target folder, as a browser path.
|
|
163
|
+
* @param {File} file The file.
|
|
164
|
+
* @param {(percent: number) => void} [onProgress] Called with 0..100.
|
|
165
|
+
* @return {Promise<object>} The stored file's row.
|
|
166
|
+
*/
|
|
167
|
+
async upload(path, file, onProgress) {
|
|
168
|
+
const form = new FormData()
|
|
169
|
+
form.append('path', toApiPath(path))
|
|
170
|
+
form.append('files[]', file, file.name)
|
|
171
|
+
try {
|
|
172
|
+
const { data } = await axios.post(objectUrl('/folder/upload'), form, {
|
|
173
|
+
onUploadProgress: (progress) => {
|
|
174
|
+
if (progress?.total && typeof onProgress === 'function') {
|
|
175
|
+
onProgress(Math.round((progress.loaded / progress.total) * 100))
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
})
|
|
179
|
+
const stored = Array.isArray(data?.stored) ? data.stored : []
|
|
180
|
+
if (stored.length === 0) {
|
|
181
|
+
const error = new Error(data?.rejected?.[0]?.error || 'The upload failed')
|
|
182
|
+
error.status = 400
|
|
183
|
+
throw error
|
|
184
|
+
}
|
|
185
|
+
return entryToNode(stored[0])
|
|
186
|
+
} catch (err) {
|
|
187
|
+
if (err?.status && !err.response) {
|
|
188
|
+
throw err
|
|
189
|
+
}
|
|
190
|
+
return fail(err)
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Rename a file or folder.
|
|
196
|
+
*
|
|
197
|
+
* @param {object} node The row.
|
|
198
|
+
* @param {string} name The new name.
|
|
199
|
+
* @return {Promise<object>} The renamed row.
|
|
200
|
+
*/
|
|
201
|
+
async rename(node, name) {
|
|
202
|
+
try {
|
|
203
|
+
const { data } = await axios.put(objectUrl(`/folder/${encodeURIComponent(node.fileid)}`), { name })
|
|
204
|
+
return entryToNode(data)
|
|
205
|
+
} catch (err) {
|
|
206
|
+
return fail(err)
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Delete a file or folder.
|
|
212
|
+
*
|
|
213
|
+
* @param {object} node The row.
|
|
214
|
+
* @return {Promise<void>}
|
|
215
|
+
*/
|
|
216
|
+
async remove(node) {
|
|
217
|
+
try {
|
|
218
|
+
await axios.delete(objectUrl(`/folder/${encodeURIComponent(node.fileid)}`))
|
|
219
|
+
} catch (err) {
|
|
220
|
+
fail(err)
|
|
221
|
+
}
|
|
222
|
+
},
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Where a file downloads from: the object's own file endpoint, which
|
|
226
|
+
* checks read on the object.
|
|
227
|
+
*
|
|
228
|
+
* @param {object} node The row.
|
|
229
|
+
* @return {string|null} The url, or null for a folder.
|
|
230
|
+
*/
|
|
231
|
+
downloadUrl(node) {
|
|
232
|
+
if (!node?.fileid || node.type === FileType.Folder) {
|
|
233
|
+
return null
|
|
234
|
+
}
|
|
235
|
+
return objectUrl(`/files/${encodeURIComponent(node.fileid)}`)
|
|
236
|
+
},
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* A small preview of an image, through the object's preview endpoint.
|
|
240
|
+
*
|
|
241
|
+
* @param {object} node The row.
|
|
242
|
+
* @return {string} The url.
|
|
243
|
+
*/
|
|
244
|
+
previewUrl(node) {
|
|
245
|
+
return objectUrl(`/files/${encodeURIComponent(node.fileid)}/preview?width=64&height=64`)
|
|
246
|
+
},
|
|
247
|
+
}
|
|
248
|
+
}
|
|
@@ -1466,11 +1466,17 @@ export default {
|
|
|
1466
1466
|
|
|
1467
1467
|
/**
|
|
1468
1468
|
* What the draft indicator announces, or '' when it says nothing.
|
|
1469
|
+
* While the dialog shows a failed save it never says Saved: it says
|
|
1470
|
+
* the draft is kept on this device, so it cannot be read as the
|
|
1471
|
+
* server save that just failed.
|
|
1469
1472
|
*
|
|
1470
1473
|
* @return {string} The text.
|
|
1474
|
+
* @spec openspec/changes/cell-labels-and-draft-indicator/specs/cell-labels-and-draft-indicator/spec.md#requirement-the-draft-indicator-never-reads-as-a-server-save-after-a-failed-save
|
|
1471
1475
|
*/
|
|
1472
1476
|
draftIndicatorLabel() {
|
|
1473
|
-
|
|
1477
|
+
const saveFailed = this.result !== null && typeof this.result === 'object'
|
|
1478
|
+
&& (Boolean(this.result.error) || this.result.success === false)
|
|
1479
|
+
return draftIndicatorText(this.draftState, this.draftSavedAt, Date.now(), { saveFailed })
|
|
1474
1480
|
},
|
|
1475
1481
|
|
|
1476
1482
|
isCreateMode() {
|
|
@@ -3885,7 +3891,7 @@ export default {
|
|
|
3885
3891
|
const mappedProps = propertiesFromDefinitions(
|
|
3886
3892
|
Array.isArray(records) ? records : [],
|
|
3887
3893
|
config,
|
|
3888
|
-
{ orderFrom: 1000 + Object.keys(properties).length },
|
|
3894
|
+
{ orderFrom: 1000 + Object.keys(properties).length, create: this.isCreateMode },
|
|
3889
3895
|
)
|
|
3890
3896
|
Object.assign(properties, mappedProps.properties)
|
|
3891
3897
|
required.push(...mappedProps.required)
|
|
@@ -954,11 +954,14 @@ export default {
|
|
|
954
954
|
|
|
955
955
|
/**
|
|
956
956
|
* What the draft indicator announces: Saving, then Saved with a relative time.
|
|
957
|
+
* After a failed submit it says the draft is kept on this device instead,
|
|
958
|
+
* so it cannot be read as the server save that just failed.
|
|
957
959
|
*
|
|
958
960
|
* @return {string} The text, or '' before anything is typed.
|
|
961
|
+
* @spec openspec/changes/cell-labels-and-draft-indicator/specs/cell-labels-and-draft-indicator/spec.md#requirement-the-draft-indicator-never-reads-as-a-server-save-after-a-failed-save
|
|
959
962
|
*/
|
|
960
963
|
draftIndicatorLabel() {
|
|
961
|
-
return draftIndicatorText(this.draftState, this.draftSavedAt)
|
|
964
|
+
return draftIndicatorText(this.draftState, this.draftSavedAt, Date.now(), { saveFailed: Boolean(this.lastError) })
|
|
962
965
|
},
|
|
963
966
|
},
|
|
964
967
|
|
|
@@ -111,7 +111,7 @@
|
|
|
111
111
|
:sortValue="sortSelectValue"
|
|
112
112
|
:showSearch="inlineSearch || isBoardLook"
|
|
113
113
|
:searchValue="effectiveSearchValue"
|
|
114
|
-
:searchPlaceholder="searchPlaceholder"
|
|
114
|
+
:searchPlaceholder="searchPlaceholder ? cnTranslate(searchPlaceholder) : searchPlaceholder"
|
|
115
115
|
:showCountWithSearch="showCountWithSearch"
|
|
116
116
|
:refreshing="effectiveRefreshing"
|
|
117
117
|
:refreshDisabled="refreshDisabled"
|
|
@@ -565,6 +565,7 @@
|
|
|
565
565
|
:includeColumns="includeColumns"
|
|
566
566
|
:columnOverrides="columnOverrides"
|
|
567
567
|
:rowClass="rowClass"
|
|
568
|
+
:rowTitle="rowTitle"
|
|
568
569
|
:filterable="tableHeaderFilters"
|
|
569
570
|
:activeFilters="effectiveActiveFilters"
|
|
570
571
|
:filterRegister="typeof register === 'string' ? register : ''"
|
|
@@ -1533,6 +1534,21 @@ export default {
|
|
|
1533
1534
|
default: () => [],
|
|
1534
1535
|
},
|
|
1535
1536
|
|
|
1537
|
+
/**
|
|
1538
|
+
* How the board look draws the table's title column (manifest
|
|
1539
|
+
* `config.rowTitle`): `link` (the default) underlines the title,
|
|
1540
|
+
* `plain` draws it as bold text without an underline over a 13px muted
|
|
1541
|
+
* secondary line. The row stays clickable. The Nextcloud look ignores
|
|
1542
|
+
* it. Maps to CnDataTable `rowTitle`.
|
|
1543
|
+
*
|
|
1544
|
+
* @spec openspec/changes/screens-table-rows-parity/specs/index-list-board-look/spec.md#requirement-a-row-title-can-be-plain-text
|
|
1545
|
+
*/
|
|
1546
|
+
rowTitle: {
|
|
1547
|
+
type: String,
|
|
1548
|
+
default: 'link',
|
|
1549
|
+
validator: (value) => ['link', 'plain'].includes(value),
|
|
1550
|
+
},
|
|
1551
|
+
|
|
1536
1552
|
/** Optional MDI icon name. Defaults to schema.icon when a schema is provided. */
|
|
1537
1553
|
icon: {
|
|
1538
1554
|
type: String,
|
|
@@ -5156,6 +5172,7 @@ export default {
|
|
|
5156
5172
|
* stable key and a variant.
|
|
5157
5173
|
*
|
|
5158
5174
|
* @spec openspec/changes/zuiddrecht-pixel-gaps-3/specs/zuiddrecht-pixel-gaps-3/spec.md#requirement-an-index-page-can-take-the-board-header
|
|
5175
|
+
* @spec openspec/changes/screens-index-header-buttons-parity/specs/index-list-board-look/spec.md#requirement-the-add-header-button-carries-a-plus
|
|
5159
5176
|
* @return {Array<{key: string, label: string, action: string, variant: string, icon: string, format: string}>}
|
|
5160
5177
|
*/
|
|
5161
5178
|
resolvedHeaderButtons() {
|
|
@@ -5168,6 +5185,11 @@ export default {
|
|
|
5168
5185
|
if (label === '' && button.action === 'add') {
|
|
5169
5186
|
label = this.resolvedAddLabel
|
|
5170
5187
|
}
|
|
5188
|
+
// The board's primary button reads "+ New request": the Add
|
|
5189
|
+
// button takes the plus unless the manifest names an icon.
|
|
5190
|
+
if (this.isBoardLook && button.action === 'add' && icon === '') {
|
|
5191
|
+
icon = 'Plus'
|
|
5192
|
+
}
|
|
5171
5193
|
// The board's header: Export reads "Download" with its icon and
|
|
5172
5194
|
// the Actions menu reads "Actions", without the manifest saying so.
|
|
5173
5195
|
if (this.isBoardLook && label === '' && button.action === 'export') {
|
|
@@ -5338,6 +5360,7 @@ export default {
|
|
|
5338
5360
|
* filled in when one is known, else `description`.
|
|
5339
5361
|
*
|
|
5340
5362
|
* @spec openspec/changes/zuiddrecht-pixel-gaps/specs/zuiddrecht-pixel-gaps/spec.md#requirement-an-index-page-title-is-a-manifest-key
|
|
5363
|
+
* @spec openspec/changes/screens-table-footer-and-system-dates-parity/specs/index-list-board-look/spec.md#requirement-the-count-reads-in-the-users-language
|
|
5341
5364
|
* @return {string}
|
|
5342
5365
|
*/
|
|
5343
5366
|
headerDescription() {
|
|
@@ -5345,9 +5368,16 @@ export default {
|
|
|
5345
5368
|
// Board look: the count line is a template over the rows on this page
|
|
5346
5369
|
// and all rows, "{shown} of {total}" unless the page says otherwise.
|
|
5347
5370
|
if (this.isBoardLook && typeof total === 'number' && total >= 0) {
|
|
5348
|
-
const
|
|
5371
|
+
const shown = this.effectiveObjects.length
|
|
5372
|
+
// The default count line is the library's own string, so it
|
|
5373
|
+
// reads "14 van 14" in Dutch; a page's own template goes
|
|
5374
|
+
// through the app's label lookup.
|
|
5375
|
+
if (!this.countText && !this.countSubtitle) {
|
|
5376
|
+
return t('nextcloud-vue', '{shown} of {total}', { shown, total })
|
|
5377
|
+
}
|
|
5378
|
+
const template = this.countText || this.countSubtitle
|
|
5349
5379
|
return this.cnTranslate(template)
|
|
5350
|
-
.replace('{shown}', String(
|
|
5380
|
+
.replace('{shown}', String(shown))
|
|
5351
5381
|
.replace('{total}', String(total))
|
|
5352
5382
|
}
|
|
5353
5383
|
if (this.countSubtitle && typeof total === 'number' && total >= 0) {
|
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
<CnFilesBrowser
|
|
8
8
|
v-if="browserRoot !== null"
|
|
9
9
|
:rootPath="browserRoot"
|
|
10
|
-
:rootLabel="
|
|
10
|
+
:rootLabel="effectiveBrowserRootLabel"
|
|
11
|
+
:source="browserSource"
|
|
12
|
+
:deleteLabel="deleteLabel"
|
|
11
13
|
:uploadButton="uploadButton"
|
|
12
14
|
:dropOverlay="dropOverlay"
|
|
13
15
|
:dropHint="dropHint"
|
|
@@ -238,7 +240,8 @@ import CnFilePreview from '../CnFilePreview/CnFilePreview.vue'
|
|
|
238
240
|
import CnFilesBrowser from '../CnFilesBrowser/CnFilesBrowser.vue'
|
|
239
241
|
import { useFileOpener } from '../../composables/useFileOpener.js'
|
|
240
242
|
import { buildHeaders, prefixUrl } from '../../utils/index.js'
|
|
241
|
-
import {
|
|
243
|
+
import { objectDisplayName, resolveObjectFolderInfo } from '../CnFilesBrowser/filesBrowser.js'
|
|
244
|
+
import { createOpenRegisterSource } from '../CnFilesBrowser/openRegisterSource.js'
|
|
242
245
|
|
|
243
246
|
/**
|
|
244
247
|
* The mime types Nextcloud Office opens by default for the current person.
|
|
@@ -308,8 +311,22 @@ export default {
|
|
|
308
311
|
openInOfficeLabel: { type: String, default: () => t('nextcloud-vue', 'Open in Office') },
|
|
309
312
|
/** Label for the delete action */
|
|
310
313
|
deleteLabel: { type: String, default: () => t('nextcloud-vue', 'Delete') },
|
|
311
|
-
/** What the files browser's root crumb reads; null shows the
|
|
314
|
+
/** What the files browser's root crumb reads; null shows the object's name, else the folder's own name. */
|
|
312
315
|
browserRootLabel: { type: String, default: null },
|
|
316
|
+
/**
|
|
317
|
+
* Where the files browser reads the object's folder from.
|
|
318
|
+
* `openregister` (the default) goes through OpenRegister's files API under
|
|
319
|
+
* the object's access rule, so anyone who may read the object sees its
|
|
320
|
+
* files, without a share. `webdav` is the earlier behaviour: the folder is
|
|
321
|
+
* looked up in the person's own Files over WebDAV and the browser shows
|
|
322
|
+
* only when it is shared with them.
|
|
323
|
+
*/
|
|
324
|
+
source: {
|
|
325
|
+
type: String,
|
|
326
|
+
default: 'openregister',
|
|
327
|
+
validator: (value) => ['openregister', 'webdav'].includes(value),
|
|
328
|
+
},
|
|
329
|
+
|
|
313
330
|
/** Forwarded to CnFilesBrowser: a primary "Add files" button beside the New menu. */
|
|
314
331
|
uploadButton: { type: Boolean, default: false },
|
|
315
332
|
/** Forwarded to CnFilesBrowser: the drop state drawn over the list ("Drop to add") and announced. */
|
|
@@ -385,6 +402,9 @@ export default {
|
|
|
385
402
|
share: false,
|
|
386
403
|
/** The object's folder as a user-relative path, or null while unresolved or absent. */
|
|
387
404
|
browserRoot: null,
|
|
405
|
+
/** The browser's data source for `source: 'openregister'`, or null for WebDAV. */
|
|
406
|
+
browserSource: null,
|
|
407
|
+
browserObjectName: null,
|
|
388
408
|
linkedItems: [],
|
|
389
409
|
/** File ids whose preview request failed, so the row falls back to the mime icon. */
|
|
390
410
|
previewFailed: {},
|
|
@@ -403,6 +423,19 @@ export default {
|
|
|
403
423
|
}
|
|
404
424
|
},
|
|
405
425
|
|
|
426
|
+
computed: {
|
|
427
|
+
/**
|
|
428
|
+
* The root crumb: the host's label, else the object's name (its folder
|
|
429
|
+
* on disk is called after its uuid), else the folder's own name.
|
|
430
|
+
*
|
|
431
|
+
* @return {string|null}
|
|
432
|
+
* @spec openspec/changes/dutch-library-strings-and-files-crumb/specs/dutch-library-strings-and-files-crumb/spec.md#requirement-the-files-browser-names-the-object-in-its-root-crumb
|
|
433
|
+
*/
|
|
434
|
+
effectiveBrowserRootLabel() {
|
|
435
|
+
return this.browserRootLabel || this.browserObjectName || null
|
|
436
|
+
},
|
|
437
|
+
},
|
|
438
|
+
|
|
406
439
|
watch: {
|
|
407
440
|
// Match every other CnObjectSidebar tab — a single immediate
|
|
408
441
|
// `objectId` watcher drives all initial loads. Folding the
|
|
@@ -423,6 +456,10 @@ export default {
|
|
|
423
456
|
linkedItemsUrl() {
|
|
424
457
|
this.fetchLinkedItems()
|
|
425
458
|
},
|
|
459
|
+
|
|
460
|
+
source() {
|
|
461
|
+
this.resolveBrowserRoot()
|
|
462
|
+
},
|
|
426
463
|
},
|
|
427
464
|
|
|
428
465
|
methods: {
|
|
@@ -483,16 +520,22 @@ export default {
|
|
|
483
520
|
|
|
484
521
|
async resolveBrowserRoot() {
|
|
485
522
|
this.browserRoot = null
|
|
523
|
+
this.browserSource = null
|
|
524
|
+
this.browserObjectName = null
|
|
486
525
|
if (!this.objectId || !this.register || !this.schema) {
|
|
487
526
|
return
|
|
488
527
|
}
|
|
528
|
+
if (this.source === 'openregister') {
|
|
529
|
+
await this.resolveOpenRegisterSource()
|
|
530
|
+
return
|
|
531
|
+
}
|
|
489
532
|
let remoteUrl
|
|
490
533
|
try {
|
|
491
534
|
remoteUrl = generateRemoteUrl('dav')
|
|
492
535
|
} catch {
|
|
493
536
|
return
|
|
494
537
|
}
|
|
495
|
-
|
|
538
|
+
const info = await resolveObjectFolderInfo({
|
|
496
539
|
apiBase: this.apiBase,
|
|
497
540
|
register: this.register,
|
|
498
541
|
schema: this.schema,
|
|
@@ -500,6 +543,62 @@ export default {
|
|
|
500
543
|
uid: getCurrentUser()?.uid || '',
|
|
501
544
|
remoteUrl,
|
|
502
545
|
})
|
|
546
|
+
this.browserRoot = info === null ? null : info.path
|
|
547
|
+
this.browserObjectName = info === null ? null : info.name
|
|
548
|
+
},
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Use OpenRegister's files API for the browser, when this person may read
|
|
552
|
+
* the object's folder through it. A listing that fails (no read on the
|
|
553
|
+
* object, or an OpenRegister without the folder endpoints) leaves the
|
|
554
|
+
* legacy list, as an unresolved WebDAV folder did.
|
|
555
|
+
*
|
|
556
|
+
* @return {Promise<void>}
|
|
557
|
+
* @spec openspec/changes/files-browser-openregister-source/specs/files-browser/spec.md
|
|
558
|
+
*/
|
|
559
|
+
async resolveOpenRegisterSource() {
|
|
560
|
+
const objectId = this.objectId
|
|
561
|
+
const source = createOpenRegisterSource({
|
|
562
|
+
apiBase: this.apiBase,
|
|
563
|
+
register: this.register,
|
|
564
|
+
schema: this.schema,
|
|
565
|
+
objectId,
|
|
566
|
+
})
|
|
567
|
+
try {
|
|
568
|
+
await source.list('/')
|
|
569
|
+
} catch {
|
|
570
|
+
return
|
|
571
|
+
}
|
|
572
|
+
if (objectId !== this.objectId) {
|
|
573
|
+
return
|
|
574
|
+
}
|
|
575
|
+
this.browserSource = source
|
|
576
|
+
this.browserRoot = '/'
|
|
577
|
+
this.fetchObjectName()
|
|
578
|
+
},
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Read the object's name for the root crumb, by the same rule the WebDAV
|
|
582
|
+
* path uses (`objectDisplayName`: never the uuid). The OpenRegister path
|
|
583
|
+
* has no folder lookup to piggyback on, so this is its one object read.
|
|
584
|
+
* A failure keeps the crumb on its default.
|
|
585
|
+
*
|
|
586
|
+
* @return {Promise<void>}
|
|
587
|
+
*/
|
|
588
|
+
async fetchObjectName() {
|
|
589
|
+
const objectId = this.objectId
|
|
590
|
+
try {
|
|
591
|
+
const { data } = await axios.get(generateUrl(`${this.apiBase}/objects/{register}/{schema}/{objectId}`, {
|
|
592
|
+
register: this.register,
|
|
593
|
+
schema: this.schema,
|
|
594
|
+
objectId,
|
|
595
|
+
}))
|
|
596
|
+
if (objectId === this.objectId) {
|
|
597
|
+
this.browserObjectName = objectDisplayName(data, objectId)
|
|
598
|
+
}
|
|
599
|
+
} catch {
|
|
600
|
+
// The crumb keeps its default.
|
|
601
|
+
}
|
|
503
602
|
},
|
|
504
603
|
|
|
505
604
|
async fetchFiles(append = false) {
|
|
@@ -115,7 +115,7 @@
|
|
|
115
115
|
</template>
|
|
116
116
|
|
|
117
117
|
<script>
|
|
118
|
-
import { translate as t } from '@nextcloud/l10n'
|
|
118
|
+
import { translatePlural as n, translate as t } from '@nextcloud/l10n'
|
|
119
119
|
import { NcPopover, NcSelect } from '@nextcloud/vue'
|
|
120
120
|
import DotsHorizontal from 'vue-material-design-icons/DotsHorizontal.vue'
|
|
121
121
|
import CnIcon from '../CnIcon/CnIcon.vue'
|
|
@@ -354,7 +354,8 @@ export default {
|
|
|
354
354
|
if (this.overflowLabel) {
|
|
355
355
|
return t('nextcloud-vue', '{label} — show the other filters', { label: this.overflowLabel })
|
|
356
356
|
}
|
|
357
|
-
|
|
357
|
+
const count = this.overflowEntries.length
|
|
358
|
+
return n('nextcloud-vue', '{count} more filter', '{count} more filters', count, { count })
|
|
358
359
|
},
|
|
359
360
|
|
|
360
361
|
/** @return {string} Heading above the hidden chips in the panel. */
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
data-testid="cn-saved-views-control"
|
|
42
42
|
:aria-label="isBoardLook ? t('nextcloud-vue', 'Save view') : menuLabel">
|
|
43
43
|
<template #icon>
|
|
44
|
-
<ContentSaveOutline v-if="isBoardLook" :size="
|
|
44
|
+
<ContentSaveOutline v-if="isBoardLook" :size="14" />
|
|
45
45
|
<BookmarkOutline v-else :size="20" />
|
|
46
46
|
</template>
|
|
47
47
|
|
|
@@ -33,11 +33,14 @@
|
|
|
33
33
|
</div>
|
|
34
34
|
|
|
35
35
|
<div class="cn-kpi-card__body cn-stat-widget__body">
|
|
36
|
-
<div
|
|
36
|
+
<div
|
|
37
|
+
v-if="content.label || rangePresets.length"
|
|
38
|
+
class="cn-kpi-card__title cn-stat-widget__label"
|
|
39
|
+
:class="{ 'cn-kpi-card__title--badge-end': hasEndBadge }">
|
|
37
40
|
<!-- Board look: the icon is an 18px glyph before the label, and
|
|
38
41
|
only when the tile links to a filtered list. -->
|
|
39
42
|
<span
|
|
40
|
-
v-if="isBoardLook && isLinked && resolvedIcon"
|
|
43
|
+
v-if="isBoardLook && isLinked && resolvedIcon && !hasEndBadge"
|
|
41
44
|
class="cn-kpi-card__glyph cn-stat-widget__glyph"
|
|
42
45
|
data-testid="cn-stat-widget-glyph"
|
|
43
46
|
aria-hidden="true">
|
|
@@ -66,6 +69,17 @@
|
|
|
66
69
|
{{ effectiveTranslate(preset.label || preset.id) }}
|
|
67
70
|
</option>
|
|
68
71
|
</select>
|
|
72
|
+
<!-- Board look, `content.iconPlacement: "end"`: the icon sits in a
|
|
73
|
+
32px circle at the right of the label row, tinted by the
|
|
74
|
+
tile's state (DqDashboard). -->
|
|
75
|
+
<span
|
|
76
|
+
v-if="hasEndBadge"
|
|
77
|
+
class="cn-kpi-card__badge cn-stat-widget__badge-icon"
|
|
78
|
+
:class="'cn-kpi-card__badge--' + stateTone"
|
|
79
|
+
data-testid="cn-stat-widget-icon-badge"
|
|
80
|
+
aria-hidden="true">
|
|
81
|
+
<CnWidgetIcon :name="resolvedIcon" :size="18" />
|
|
82
|
+
</span>
|
|
69
83
|
</div>
|
|
70
84
|
|
|
71
85
|
<div class="cn-kpi-card__value-row cn-stat-widget__value-row">
|
|
@@ -1330,9 +1344,45 @@ export default {
|
|
|
1330
1344
|
if (layout === 'vertical' || layout === 'stacked') {
|
|
1331
1345
|
return layout
|
|
1332
1346
|
}
|
|
1347
|
+
// Board look: every KPI tile on the screens is stacked, so a tile
|
|
1348
|
+
// that names no layout takes it. An explicit `horizontal` stays.
|
|
1349
|
+
if (this.isBoardLook && layout === undefined) {
|
|
1350
|
+
return 'stacked'
|
|
1351
|
+
}
|
|
1333
1352
|
return 'horizontal'
|
|
1334
1353
|
},
|
|
1335
1354
|
|
|
1355
|
+
/**
|
|
1356
|
+
* Whether the board look draws the icon in a tinted circle at the
|
|
1357
|
+
* right of the label row (`content.iconPlacement: "end"`), as the
|
|
1358
|
+
* DqDashboard tiles. Off without the board look and without an icon.
|
|
1359
|
+
*
|
|
1360
|
+
* @spec openspec/changes/screens-stat-tile-parity/specs/dashboard-page/spec.md#requirement-the-board-kpi-tile-can-carry-its-icon-at-the-end
|
|
1361
|
+
* @return {boolean}
|
|
1362
|
+
*/
|
|
1363
|
+
hasEndBadge() {
|
|
1364
|
+
return this.isBoardLook
|
|
1365
|
+
&& (this.content || {}).iconPlacement === 'end'
|
|
1366
|
+
&& Boolean(this.resolvedIcon)
|
|
1367
|
+
},
|
|
1368
|
+
|
|
1369
|
+
/**
|
|
1370
|
+
* The tile's state as a tone name for the end badge: the variant the
|
|
1371
|
+
* value colour comes from (`success`, `warning`, `error`), else
|
|
1372
|
+
* `primary`. A custom `valueColor` has no tone and reads as primary.
|
|
1373
|
+
*
|
|
1374
|
+
* @spec openspec/changes/screens-stat-tile-parity/specs/dashboard-page/spec.md#requirement-the-board-kpi-tile-can-carry-its-icon-at-the-end
|
|
1375
|
+
* @return {'primary'|'success'|'warning'|'error'}
|
|
1376
|
+
*/
|
|
1377
|
+
stateTone() {
|
|
1378
|
+
const color = this.variantColor
|
|
1379
|
+
if (!color) {
|
|
1380
|
+
return 'primary'
|
|
1381
|
+
}
|
|
1382
|
+
const tone = ['success', 'warning', 'error'].find((name) => VARIANT_COLORS[name] === color)
|
|
1383
|
+
return tone || 'primary'
|
|
1384
|
+
},
|
|
1385
|
+
|
|
1336
1386
|
/**
|
|
1337
1387
|
* Whether the tile takes the stacked board look (`content.layout:
|
|
1338
1388
|
* "stacked"`): a plain muted label, the number at 34px in the text
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
</template>
|
|
11
11
|
|
|
12
12
|
<script>
|
|
13
|
+
import { isBadgeVariant } from '../../utils/badgeVariants.js'
|
|
14
|
+
|
|
13
15
|
/**
|
|
14
16
|
* CnStatusBadge — Color-coded pill badge for status, priority, or category display.
|
|
15
17
|
*
|
|
@@ -39,12 +41,14 @@ export default {
|
|
|
39
41
|
},
|
|
40
42
|
|
|
41
43
|
/**
|
|
42
|
-
* Color variant: 'default', 'primary', 'success', 'warning', 'error',
|
|
44
|
+
* Color variant: 'default', 'primary', 'success', 'warning', 'error',
|
|
45
|
+
* 'info', or one of the two board tones without a Nextcloud role,
|
|
46
|
+
* 'purple' (work in progress) and 'teal' (handed on).
|
|
43
47
|
*/
|
|
44
48
|
variant: {
|
|
45
49
|
type: String,
|
|
46
50
|
default: 'default',
|
|
47
|
-
validator: (v) =>
|
|
51
|
+
validator: (v) => isBadgeVariant(v),
|
|
48
52
|
},
|
|
49
53
|
|
|
50
54
|
/** Size: 'small' or 'medium' */
|