@conduction/nextcloud-vue 2.78.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/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/CnCellRenderer/CnCellRenderer.vue2.js +80 -0
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +115 -91
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +139 -1
- 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 +77 -0
- 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/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 +1 -1
- 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/CnIndexPage/CnIndexPage.vue2.js +25 -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/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/index.js +1 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/l10n/en.json.js +61 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +106 -2
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/nextcloud-vue.css +314 -15
- 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 +2039 -581
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +314 -15
- package/l10n/en.json +61 -1
- package/l10n/nl.json +106 -2
- package/package.json +1 -1
- package/scripts/build-validators.js +26 -0
- package/src/components/CnActionsBar/CnActionsBar.vue +48 -39
- package/src/components/CnAppRoot/CnAppRoot.vue +12 -2
- package/src/components/CnCellRenderer/CnCellRenderer.vue +119 -2
- package/src/components/CnDashboardPage/CnDashboardPage.vue +219 -4
- package/src/components/CnDataTable/CnDataTable.md +2 -0
- package/src/components/CnDataTable/CnDataTable.vue +81 -2
- 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 +1 -1
- package/src/components/CnIndexPage/CnIndexPage.vue +27 -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/CnTabsWidget/CnTabsWidget.vue +30 -1
- package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +13 -0
- package/src/components/index.js +1 -1
- package/src/composables/useAppManifest.js +8 -3
- package/src/css/look-board-detail.css +10 -0
- package/src/css/look-board-index.css +92 -14
- package/src/css/look-board.css +77 -0
- package/src/css/table.css +37 -0
- package/src/index.js +4 -0
- package/src/schemas/app-manifest-v2.schema.json +46 -1
- 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
|
+
}
|
|
@@ -3891,7 +3891,7 @@ export default {
|
|
|
3891
3891
|
const mappedProps = propertiesFromDefinitions(
|
|
3892
3892
|
Array.isArray(records) ? records : [],
|
|
3893
3893
|
config,
|
|
3894
|
-
{ orderFrom: 1000 + Object.keys(properties).length },
|
|
3894
|
+
{ orderFrom: 1000 + Object.keys(properties).length, create: this.isCreateMode },
|
|
3895
3895
|
)
|
|
3896
3896
|
Object.assign(properties, mappedProps.properties)
|
|
3897
3897
|
required.push(...mappedProps.required)
|
|
@@ -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,
|
|
@@ -5344,6 +5360,7 @@ export default {
|
|
|
5344
5360
|
* filled in when one is known, else `description`.
|
|
5345
5361
|
*
|
|
5346
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
|
|
5347
5364
|
* @return {string}
|
|
5348
5365
|
*/
|
|
5349
5366
|
headerDescription() {
|
|
@@ -5351,9 +5368,16 @@ export default {
|
|
|
5351
5368
|
// Board look: the count line is a template over the rows on this page
|
|
5352
5369
|
// and all rows, "{shown} of {total}" unless the page says otherwise.
|
|
5353
5370
|
if (this.isBoardLook && typeof total === 'number' && total >= 0) {
|
|
5354
|
-
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
|
|
5355
5379
|
return this.cnTranslate(template)
|
|
5356
|
-
.replace('{shown}', String(
|
|
5380
|
+
.replace('{shown}', String(shown))
|
|
5357
5381
|
.replace('{total}', String(total))
|
|
5358
5382
|
}
|
|
5359
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
|
|
|
@@ -254,6 +254,11 @@ export default {
|
|
|
254
254
|
cnLook: { default: 'nextcloud' },
|
|
255
255
|
/** The tab list's name under the board look, provided by CnDetailPage (`config.tabsLabel`). */
|
|
256
256
|
cnDetailTabsLabel: { default: null },
|
|
257
|
+
/**
|
|
258
|
+
* The host's translate (CnAppRoot provides `cnTranslate`), so a tab
|
|
259
|
+
* label written in the manifest reads in the user's language.
|
|
260
|
+
*/
|
|
261
|
+
cnTranslate: { default: () => (key) => key },
|
|
257
262
|
},
|
|
258
263
|
|
|
259
264
|
props: {
|
|
@@ -471,7 +476,7 @@ export default {
|
|
|
471
476
|
key: `${widgetId || 'tab'}-${index}`,
|
|
472
477
|
id: (tab && typeof tab === 'object' && tab.id) || widgetId || '',
|
|
473
478
|
widgetId,
|
|
474
|
-
label:
|
|
479
|
+
label: this.tabLabel(tab, widget, widgetId),
|
|
475
480
|
icon: (tab && tab.icon) || widget?.icon || '',
|
|
476
481
|
widget,
|
|
477
482
|
count,
|
|
@@ -652,6 +657,30 @@ export default {
|
|
|
652
657
|
},
|
|
653
658
|
|
|
654
659
|
methods: {
|
|
660
|
+
/**
|
|
661
|
+
* The label a tab shows: the authored `label`, else (board look) History
|
|
662
|
+
* for the activity widget, else the child widget's title, else its id.
|
|
663
|
+
* A manifest label or title goes through the host's translate; the
|
|
664
|
+
* library's own History is translated by the library catalogue.
|
|
665
|
+
*
|
|
666
|
+
* @param {object|string} tab The `content.tabs[]` entry.
|
|
667
|
+
* @param {object|null} widget The widget definition it names.
|
|
668
|
+
* @param {string} widgetId The widget id.
|
|
669
|
+
* @return {string} The label.
|
|
670
|
+
* @spec openspec/changes/screens-detail-labels-parity/specs/detail-labels-board-look/spec.md#requirement-detail-labels-read-in-the-users-language
|
|
671
|
+
*/
|
|
672
|
+
tabLabel(tab, widget, widgetId) {
|
|
673
|
+
const translate = typeof this.cnTranslate === 'function' ? this.cnTranslate : (key) => key
|
|
674
|
+
if (tab && typeof tab.label === 'string' && tab.label !== '') {
|
|
675
|
+
return translate(tab.label)
|
|
676
|
+
}
|
|
677
|
+
if (this.isBoard && isActivityWidget(widget)) {
|
|
678
|
+
return t('nextcloud-vue', 'History')
|
|
679
|
+
}
|
|
680
|
+
const title = widgetTitleOf(widget)
|
|
681
|
+
return title ? translate(title) : (widgetId || '')
|
|
682
|
+
},
|
|
683
|
+
|
|
655
684
|
/**
|
|
656
685
|
* Whether a tab shows: `visible`, `hidden`, or `pending` while its
|
|
657
686
|
* condition cannot be decided yet (object not loaded, count not back).
|
|
@@ -323,6 +323,19 @@ export default {
|
|
|
323
323
|
default: false,
|
|
324
324
|
},
|
|
325
325
|
|
|
326
|
+
/**
|
|
327
|
+
* Fit the table to the tile: the title column (a column's `grow: true`,
|
|
328
|
+
* else `title` or `name`, else the first) shrinks and ends in an
|
|
329
|
+
* ellipsis so the trailing columns stay visible. On by default; pass
|
|
330
|
+
* `false` for the table's own auto layout. Forwarded to CnDataTable.
|
|
331
|
+
*
|
|
332
|
+
* @spec openspec/changes/narrow-object-table-widgets/specs/narrow-object-table-widgets/spec.md#requirement-a-narrow-object-table-keeps-its-trailing-columns-in-view
|
|
333
|
+
*/
|
|
334
|
+
fitWidth: {
|
|
335
|
+
type: Boolean,
|
|
336
|
+
default: true,
|
|
337
|
+
},
|
|
338
|
+
|
|
326
339
|
/**
|
|
327
340
|
* Route NAME for row-click navigation. Mapped onto CnDataTable's
|
|
328
341
|
* `rowClickRoute` as `{ name: rowRoute, params: { id } }` with the
|
package/src/components/index.js
CHANGED
|
@@ -109,7 +109,7 @@ export { CnTreeView } from './CnTreeView/index.js'
|
|
|
109
109
|
export { CnUserActionMenu } from './CnUserActionMenu/index.js'
|
|
110
110
|
export { CnNotesCard } from './CnNotesCard/index.js'
|
|
111
111
|
export { CnTasksCard } from './CnTasksCard/index.js'
|
|
112
|
-
export { CnFilesBrowser } from './CnFilesBrowser/index.js'
|
|
112
|
+
export { CnFilesBrowser, createOpenRegisterSource } from './CnFilesBrowser/index.js'
|
|
113
113
|
export { CnFilesCard } from './CnFilesCard/index.js'
|
|
114
114
|
export { CnFileManager } from './CnFileManager/index.js'
|
|
115
115
|
export { CnRelatedFiles } from './CnRelatedFiles/index.js'
|
|
@@ -205,6 +205,7 @@ function loadInMemory(input) {
|
|
|
205
205
|
* @param {object} options Configuration options (endpoint / fetcher /
|
|
206
206
|
* getAppConfigValue overrides).
|
|
207
207
|
* @return {{ manifest: import('vue').Ref<object>, isLoading: import('vue').Ref<boolean>, validationErrors: import('vue').Ref<string[]|null>, unresolvedSentinels: import('vue').Ref<string[]> }}
|
|
208
|
+
* @spec openspec/changes/manifest-validator-self-contained/specs/manifest-validator-self-contained/spec.md#requirement-a-backend-manifest-delta-reaches-the-navigation
|
|
208
209
|
*/
|
|
209
210
|
function loadFromBackend(appId, bundledManifest, options) {
|
|
210
211
|
// Shallow by default (manifest-shallow-reactivity-by-default) — see
|
|
@@ -284,9 +285,13 @@ function loadFromBackend(appId, bundledManifest, options) {
|
|
|
284
285
|
// The shallowRef reassignment is what re-renders consumers; the new
|
|
285
286
|
// object stays shallow (not deep-observed) unless CnAppRoot upgrades it.
|
|
286
287
|
manifest.value = resolved
|
|
287
|
-
} catch {
|
|
288
|
-
//
|
|
289
|
-
//
|
|
288
|
+
} catch (error) {
|
|
289
|
+
// Any unexpected error leaves the bundled manifest in place, so
|
|
290
|
+
// apps keep working. Say so: a silent catch here once hid a
|
|
291
|
+
// validator that threw on every manifest, and the backend delta
|
|
292
|
+
// never reached the navigation.
|
|
293
|
+
// eslint-disable-next-line no-console
|
|
294
|
+
console.warn('[useAppManifest] Could not resolve the manifest; keeping the bundled manifest.', error)
|
|
290
295
|
} finally {
|
|
291
296
|
isLoading.value = false
|
|
292
297
|
}
|
|
@@ -112,6 +112,16 @@
|
|
|
112
112
|
gap: 20px;
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
/* GridStack insets every cell by its 12px margin, the outer edge included, so
|
|
116
|
+
the tab strip started 12px below the top of the body column and stopped 12px
|
|
117
|
+
short of its end, while the side column started at the top. Pulling the grid
|
|
118
|
+
out by that margin puts its first card on the column's edges: the side column
|
|
119
|
+
is level with the strip, and the strip ends where the body column ends. */
|
|
120
|
+
.cn-look-board.cn-detail-page--with-side > .cn-detail-page__body > .cn-detail-page__grid {
|
|
121
|
+
margin: -12px;
|
|
122
|
+
width: auto;
|
|
123
|
+
}
|
|
124
|
+
|
|
115
125
|
/* ---- Folder tabs ------------------------------------------------------- */
|
|
116
126
|
|
|
117
127
|
/* The strip draws no line of its own (design-system pull request 147). The card under it
|