@conduction/nextcloud-vue 2.56.0-beta.30 → 2.56.0-beta.31
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.map +1 -1
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js +3 -3
- package/dist/esm/components/CnActionsBar/CnActionsBar.vue3.js.map +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js +80 -4
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue2.js.map +1 -1
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js +95 -91
- package/dist/esm/components/CnAppRoot/CnAppRoot.vue3.js.map +1 -1
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js +85 -6
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js +1 -1
- package/dist/esm/components/CnBannerWidget/CnBannerWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +4 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js +1 -1
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnConversationThread/CnConversationWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js +2 -2
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardGrid/CnDashboardGrid.vue3.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js +40 -3
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDashboardPage/CnDashboardPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +19 -11
- 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 +43 -0
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue2.js +1 -1
- package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnDocumentReviewList/CnDocumentReviewWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnFacetSidebar/CnFacetSidebar.vue2.js +7 -7
- package/dist/esm/components/CnFacetSidebar/CnFacetSidebar.vue2.js.map +1 -1
- package/dist/esm/components/CnFacetSidebar/CnFacetSidebar.vue3.js.map +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js +125 -0
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnFactsWidget/CnFactsWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js +3 -2
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnFieldListWidget/CnFieldListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +302 -4
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +417 -244
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/filesBrowser.js +101 -1
- package/dist/esm/components/CnFilesBrowser/filesBrowser.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/filesBrowserColumns.js +141 -1
- package/dist/esm/components/CnFilesBrowser/filesBrowserColumns.js.map +1 -1
- package/dist/esm/components/CnFormPage/CnFormPage.vue2.js +60 -0
- package/dist/esm/components/CnFormPage/CnFormPage.vue2.js.map +1 -1
- package/dist/esm/components/CnFormPage/CnFormPage.vue3.js +74 -49
- package/dist/esm/components/CnFormPage/CnFormPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +29 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +48 -4
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue2.js +7 -7
- package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexSidebar/CnIndexSidebar.vue3.js.map +1 -1
- package/dist/esm/components/CnNextStepCard/CnNextStepWidget.vue2.js +1 -1
- package/dist/esm/components/CnNextStepCard/CnNextStepWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +1 -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/CnObjectSidebar/CnFilesTab.vue2.js +63 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +8 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectSidebar/CnNotesTab.vue2.js +14 -0
- package/dist/esm/components/CnObjectSidebar/CnNotesTab.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectSidebar/CnNotesTab.vue3.js +5 -1
- package/dist/esm/components/CnObjectSidebar/CnNotesTab.vue3.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js +13 -6
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnRecordListWidget/CnRecordListWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js +84 -1
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js +85 -41
- package/dist/esm/components/CnStatsBlockWidget/CnStatsBlockWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnWidgetObjectTable/CnWidgetObjectTable.vue2.js +7 -2
- 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/components/CnWidgetObjectTable/dashboardRegistration.js +5 -0
- package/dist/esm/components/CnWidgetObjectTable/dashboardRegistration.js.map +1 -1
- package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue2.js +197 -6
- package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue3.js +94 -50
- package/dist/esm/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue3.js.map +1 -1
- package/dist/esm/composables/useListView.js +4 -4
- package/dist/esm/composables/useListView.js.map +1 -1
- package/dist/esm/index.js +10 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/l10n/en.json.js +8 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +54 -1
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/nextcloud-vue.css +198 -8
- package/dist/esm/utils/facets.js +75 -3
- package/dist/esm/utils/facets.js.map +1 -1
- package/dist/esm/utils/fillTemplate.js +7 -1
- package/dist/esm/utils/fillTemplate.js.map +1 -1
- package/dist/esm/utils/pageControls.js +81 -0
- package/dist/esm/utils/pageControls.js.map +1 -0
- package/dist/esm/utils/personalLenses.js +40 -3
- package/dist/esm/utils/personalLenses.js.map +1 -1
- package/dist/esm/utils/resolveFilterTokens.js +1 -1
- package/dist/esm/utils/resolveFilterTokens.js.map +1 -1
- package/dist/esm/utils/resolveManifestTokens.js +2 -0
- package/dist/esm/utils/resolveManifestTokens.js.map +1 -1
- package/dist/esm/utils/sentinelTokens.js +19 -1
- package/dist/esm/utils/sentinelTokens.js.map +1 -1
- package/dist/esm/utils/userGroupsPeek.js +91 -0
- package/dist/esm/utils/userGroupsPeek.js.map +1 -0
- package/dist/esm/utils/validateManifestV2.compiled.js +3 -3
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/esm/utils/visibleWhen.js +57 -5
- package/dist/esm/utils/visibleWhen.js.map +1 -1
- package/dist/esm/utils/widgetVisibility.js +4 -57
- package/dist/esm/utils/widgetVisibility.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +4309 -2233
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +198 -8
- package/l10n/en.json +8 -1
- package/l10n/nl.json +54 -1
- package/package.json +2 -1
- package/scripts/check-docs-mdx.mjs +108 -0
- package/src/components/CnActionsBar/CnActionsBar.vue +6 -1
- package/src/components/CnAppRoot/CnAppRoot.md +4 -2
- package/src/components/CnAppRoot/CnAppRoot.vue +151 -59
- package/src/components/CnBannerWidget/CnBannerWidget.vue +112 -7
- package/src/components/CnCellRenderer/CnCellRenderer.vue +4 -1
- package/src/components/CnConversationThread/CnConversationWidget.vue +1 -1
- package/src/components/CnDashboardGrid/CnDashboardGrid.vue +2 -2
- package/src/components/CnDashboardPage/CnDashboardPage.vue +40 -3
- package/src/components/CnDataTable/CnDataTable.vue +19 -11
- package/src/components/CnDetailPage/CnDetailPage.vue +44 -1
- package/src/components/CnDocumentReviewList/CnDocumentReviewWidget.vue +1 -1
- package/src/components/CnFacetSidebar/CnFacetSidebar.vue +7 -7
- package/src/components/CnFactsWidget/CnFactsWidget.vue +125 -0
- package/src/components/CnFieldListWidget/CnFieldListWidget.vue +3 -2
- package/src/components/CnFilesBrowser/CnFilesBrowser.vue +588 -117
- package/src/components/CnFilesBrowser/filesBrowser.js +100 -0
- package/src/components/CnFilesBrowser/filesBrowserColumns.js +140 -0
- package/src/components/CnFormPage/CnFormPage.vue +81 -0
- package/src/components/CnIndexPage/CnIndexPage.vue +56 -3
- package/src/components/CnIndexSidebar/CnIndexSidebar.vue +7 -7
- package/src/components/CnNextStepCard/CnNextStepWidget.vue +1 -1
- package/src/components/CnObjectListWidget/CnObjectListWidget.vue +1 -1
- package/src/components/CnObjectSidebar/CnFilesTab.vue +70 -1
- package/src/components/CnObjectSidebar/CnNotesTab.vue +16 -0
- package/src/components/CnRecordListWidget/CnRecordListWidget.vue +13 -6
- package/src/components/CnStatsBlockWidget/CnStatsBlockWidget.vue +180 -3
- package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +7 -2
- package/src/components/CnWidgetObjectTable/dashboardRegistration.js +5 -0
- package/src/components/CnWorkspaceFilterWidget/CnWorkspaceFilterWidget.vue +281 -10
- package/src/components/index.js +8 -0
- package/src/composables/useListView.js +4 -4
- package/src/css/look-board-index.css +16 -8
- package/src/index.js +8 -0
- package/src/schemas/app-manifest-v2.schema.json +93 -9
- package/src/utils/facets.js +74 -2
- package/src/utils/fillTemplate.js +7 -1
- package/src/utils/pageControls.js +78 -0
- package/src/utils/personalLenses.js +39 -2
- package/src/utils/resolveFilterTokens.js +3 -1
- package/src/utils/resolveManifestTokens.js +2 -0
- package/src/utils/sentinelTokens.js +19 -1
- package/src/utils/userGroupsPeek.js +87 -0
- package/src/utils/validateManifestV2.compiled.js +3 -3
- package/src/utils/visibleWhen.js +56 -4
- package/src/utils/widgetVisibility.js +7 -56
|
@@ -220,3 +220,103 @@ export async function resolveObjectFolderInfo({ apiBase, register, schema, objec
|
|
|
220
220
|
return null
|
|
221
221
|
}
|
|
222
222
|
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Read a dotted path off a value without throwing on a missing segment.
|
|
226
|
+
*
|
|
227
|
+
* @param {(Array|object|string|number|boolean|null|undefined)} value The value.
|
|
228
|
+
* @param {string} path A dotted path; empty answers the value itself.
|
|
229
|
+
* @return {(Array|object|string|number|boolean|null|undefined)} What sits at the path, or undefined.
|
|
230
|
+
*/
|
|
231
|
+
function readPath(value, path) {
|
|
232
|
+
if (typeof path !== 'string' || path === '') {
|
|
233
|
+
return value
|
|
234
|
+
}
|
|
235
|
+
return path.split('.').reduce((acc, key) => (acc === null || acc === undefined ? undefined : acc[key]), value)
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Turn a host endpoint's answer into the browser's `rowData`: the host's own
|
|
240
|
+
* record for each file, keyed by file id.
|
|
241
|
+
*
|
|
242
|
+
* The answer may be a list of records (directly, at `path`, or under `items`
|
|
243
|
+
* or `results`), each naming its file in `key` (`fileId` unless the host says
|
|
244
|
+
* otherwise); a record without one is skipped. An object that is not a list
|
|
245
|
+
* and holds neither is taken as already keyed by file id. Anything else is
|
|
246
|
+
* an empty map: the extra columns stay empty, the files still list.
|
|
247
|
+
*
|
|
248
|
+
* @param {(Array|object|string|number|boolean|null|undefined)} data The endpoint answer.
|
|
249
|
+
* @param {object} [options] Where to look.
|
|
250
|
+
* @param {string} [options.path] A dotted path to the list in the answer.
|
|
251
|
+
* @param {string} [options.key] The record field that holds the file id.
|
|
252
|
+
* @return {object} The records by file id.
|
|
253
|
+
*/
|
|
254
|
+
export function indexRowData(data, { path = '', key = 'fileId' } = {}) {
|
|
255
|
+
let picked = readPath(data, path)
|
|
256
|
+
if (picked && typeof picked === 'object' && !Array.isArray(picked)) {
|
|
257
|
+
if (Array.isArray(picked.items)) {
|
|
258
|
+
picked = picked.items
|
|
259
|
+
} else if (Array.isArray(picked.results)) {
|
|
260
|
+
picked = picked.results
|
|
261
|
+
} else {
|
|
262
|
+
return { ...picked }
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (!Array.isArray(picked)) {
|
|
266
|
+
return {}
|
|
267
|
+
}
|
|
268
|
+
const byId = {}
|
|
269
|
+
picked.forEach((record) => {
|
|
270
|
+
if (!record || typeof record !== 'object') {
|
|
271
|
+
return
|
|
272
|
+
}
|
|
273
|
+
const id = record[key]
|
|
274
|
+
if (id === null || id === undefined || id === '') {
|
|
275
|
+
return
|
|
276
|
+
}
|
|
277
|
+
byId[String(id)] = record
|
|
278
|
+
})
|
|
279
|
+
return byId
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Whether a host row action is offered on a file, by its `visibleIf`.
|
|
284
|
+
*
|
|
285
|
+
* `visibleIf.extension` lists file extensions (no case, a leading dot is
|
|
286
|
+
* allowed); `visibleIf.mime` lists mime types, where one ending in `/` or
|
|
287
|
+
* `/*` names the whole family. Every condition named must hold. An action
|
|
288
|
+
* without `visibleIf` is offered on every file, as before.
|
|
289
|
+
*
|
|
290
|
+
* @param {object} action The declared action.
|
|
291
|
+
* @param {object} node The file (`basename`, `mime`).
|
|
292
|
+
* @return {boolean} True when the action belongs on this file.
|
|
293
|
+
*/
|
|
294
|
+
export function hostActionApplies(action, node) {
|
|
295
|
+
const condition = action && action.visibleIf
|
|
296
|
+
if (!condition || typeof condition !== 'object') {
|
|
297
|
+
return true
|
|
298
|
+
}
|
|
299
|
+
if (Array.isArray(condition.extension)) {
|
|
300
|
+
const name = String((node && node.basename) || '')
|
|
301
|
+
const dot = name.lastIndexOf('.')
|
|
302
|
+
const extension = dot > 0 ? name.slice(dot + 1).toLowerCase() : ''
|
|
303
|
+
const wanted = condition.extension.map((entry) => String(entry).replace(/^\./, '').toLowerCase())
|
|
304
|
+
if (extension === '' || !wanted.includes(extension)) {
|
|
305
|
+
return false
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (Array.isArray(condition.mime)) {
|
|
309
|
+
const mime = String((node && node.mime) || '').toLowerCase()
|
|
310
|
+
const matches = condition.mime.some((entry) => {
|
|
311
|
+
const wanted = String(entry).toLowerCase()
|
|
312
|
+
if (wanted.endsWith('/*') || wanted.endsWith('/')) {
|
|
313
|
+
return mime.startsWith(wanted.replace(/\*$/, ''))
|
|
314
|
+
}
|
|
315
|
+
return mime === wanted
|
|
316
|
+
})
|
|
317
|
+
if (!matches) {
|
|
318
|
+
return false
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
return true
|
|
322
|
+
}
|
|
@@ -219,3 +219,143 @@ export function sortNodesByColumn(nodes, column, ascending, rowData = null, isFo
|
|
|
219
219
|
return String(left).localeCompare(String(right)) * direction
|
|
220
220
|
})
|
|
221
221
|
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Whether a cell value counts as filled in.
|
|
225
|
+
*
|
|
226
|
+
* @param {(Array|object|string|number|boolean|null|undefined)} value The value.
|
|
227
|
+
* @return {boolean} False for nothing, an empty string or an empty list.
|
|
228
|
+
*/
|
|
229
|
+
function isFilled(value) {
|
|
230
|
+
if (value === null || value === undefined || value === '') {
|
|
231
|
+
return false
|
|
232
|
+
}
|
|
233
|
+
return !(Array.isArray(value) && value.length === 0)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* The single value a node is grouped under, as a string key: a list groups
|
|
238
|
+
* under its first entry, an object under its label, title, name or id.
|
|
239
|
+
*
|
|
240
|
+
* @param {(Array|object|string|number|boolean|null|undefined)} value The cell value.
|
|
241
|
+
* @return {(string|null)} The group key, or null when nothing is filled in.
|
|
242
|
+
*/
|
|
243
|
+
function groupKeyOf(value) {
|
|
244
|
+
const first = Array.isArray(value) ? value[0] : value
|
|
245
|
+
if (!isFilled(first)) {
|
|
246
|
+
return null
|
|
247
|
+
}
|
|
248
|
+
if (typeof first === 'object') {
|
|
249
|
+
const label = first.label ?? first.title ?? first.name ?? first.id
|
|
250
|
+
return isFilled(label) ? String(label) : null
|
|
251
|
+
}
|
|
252
|
+
return String(first)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* The values a node carries in a column, as strings, for counting and
|
|
257
|
+
* filtering: every entry of a list, the one value otherwise.
|
|
258
|
+
*
|
|
259
|
+
* @param {object} column A resolved column.
|
|
260
|
+
* @param {object} node The node.
|
|
261
|
+
* @param {object} [rowData] The host's per-file data.
|
|
262
|
+
* @return {string[]} The values, deduplicated.
|
|
263
|
+
*/
|
|
264
|
+
function facetValuesOf(column, node, rowData) {
|
|
265
|
+
const value = fileColumnValue(column, node, rowData)
|
|
266
|
+
const entries = Array.isArray(value) ? value : [value]
|
|
267
|
+
const out = []
|
|
268
|
+
entries.forEach((entry) => {
|
|
269
|
+
const key = groupKeyOf(entry)
|
|
270
|
+
if (key !== null && !out.includes(key)) {
|
|
271
|
+
out.push(key)
|
|
272
|
+
}
|
|
273
|
+
})
|
|
274
|
+
return out
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Group nodes on a column's value, in the order each value is first met.
|
|
279
|
+
* Nodes with no value come last, in one group whose value is null.
|
|
280
|
+
*
|
|
281
|
+
* @param {Array<object>} nodes The nodes, already in display order.
|
|
282
|
+
* @param {object} column The resolved column.
|
|
283
|
+
* @param {object} [rowData] The host's per-file data.
|
|
284
|
+
* @return {Array<{value: (string|null), nodes: Array<object>}>} The groups.
|
|
285
|
+
*/
|
|
286
|
+
export function groupNodes(nodes, column, rowData = null) {
|
|
287
|
+
const groups = []
|
|
288
|
+
const byKey = new Map()
|
|
289
|
+
const empty = { value: null, nodes: [] }
|
|
290
|
+
;(Array.isArray(nodes) ? nodes : []).forEach((node) => {
|
|
291
|
+
const key = column ? groupKeyOf(fileColumnValue(column, node, rowData)) : null
|
|
292
|
+
if (key === null) {
|
|
293
|
+
empty.nodes.push(node)
|
|
294
|
+
return
|
|
295
|
+
}
|
|
296
|
+
if (!byKey.has(key)) {
|
|
297
|
+
const group = { value: key, nodes: [] }
|
|
298
|
+
byKey.set(key, group)
|
|
299
|
+
groups.push(group)
|
|
300
|
+
}
|
|
301
|
+
byKey.get(key).nodes.push(node)
|
|
302
|
+
})
|
|
303
|
+
if (empty.nodes.length > 0) {
|
|
304
|
+
groups.push(empty)
|
|
305
|
+
}
|
|
306
|
+
return groups
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* The values in use in a column across the nodes, with how many nodes carry
|
|
311
|
+
* each; a list counts once per entry. Most used first, then in first-seen
|
|
312
|
+
* order.
|
|
313
|
+
*
|
|
314
|
+
* @param {Array<object>} nodes The nodes.
|
|
315
|
+
* @param {object} column The resolved column.
|
|
316
|
+
* @param {object} [rowData] The host's per-file data.
|
|
317
|
+
* @return {Array<{value: string, count: number}>} The values and counts.
|
|
318
|
+
*/
|
|
319
|
+
export function facetCounts(nodes, column, rowData = null) {
|
|
320
|
+
const counts = []
|
|
321
|
+
const byValue = new Map()
|
|
322
|
+
;(Array.isArray(nodes) ? nodes : []).forEach((node) => {
|
|
323
|
+
facetValuesOf(column, node, rowData).forEach((value) => {
|
|
324
|
+
if (!byValue.has(value)) {
|
|
325
|
+
const entry = { value, count: 0 }
|
|
326
|
+
byValue.set(value, entry)
|
|
327
|
+
counts.push(entry)
|
|
328
|
+
}
|
|
329
|
+
byValue.get(value).count += 1
|
|
330
|
+
})
|
|
331
|
+
})
|
|
332
|
+
return counts
|
|
333
|
+
.map((entry, index) => ({ entry, index }))
|
|
334
|
+
.sort((a, b) => (b.entry.count - a.entry.count) || (a.index - b.index))
|
|
335
|
+
.map(({ entry }) => entry)
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Whether a node passes the chosen facet values: within one facet any chosen
|
|
340
|
+
* value will do, and every facet with a choice must pass. A facet with
|
|
341
|
+
* nothing chosen does not filter.
|
|
342
|
+
*
|
|
343
|
+
* @param {object} node The node.
|
|
344
|
+
* @param {object} selections Chosen values by column key.
|
|
345
|
+
* @param {object} columnsByKey Resolved columns by key.
|
|
346
|
+
* @param {object} [rowData] The host's per-file data.
|
|
347
|
+
* @return {boolean} True when the node stays in the list.
|
|
348
|
+
*/
|
|
349
|
+
export function nodeMatchesFacets(node, selections, columnsByKey, rowData = null) {
|
|
350
|
+
return Object.entries(selections || {}).every(([key, chosen]) => {
|
|
351
|
+
if (!Array.isArray(chosen) || chosen.length === 0) {
|
|
352
|
+
return true
|
|
353
|
+
}
|
|
354
|
+
const column = columnsByKey && columnsByKey[key]
|
|
355
|
+
if (!column) {
|
|
356
|
+
return true
|
|
357
|
+
}
|
|
358
|
+
const values = facetValuesOf(column, node, rowData)
|
|
359
|
+
return chosen.some((value) => values.includes(value))
|
|
360
|
+
})
|
|
361
|
+
}
|
|
@@ -116,6 +116,19 @@
|
|
|
116
116
|
v-if="!submitted || mode !== 'public'"
|
|
117
117
|
class="cn-form-page__form"
|
|
118
118
|
@submit.prevent="submit">
|
|
119
|
+
<!-- Spam protection: a field no person sees or reaches. A filled one
|
|
120
|
+
means a bot; the submit then sends nothing (see `honeypot`). -->
|
|
121
|
+
<div v-if="honeypot" class="cn-form-page__honeypot" aria-hidden="true">
|
|
122
|
+
<label :for="`cn-form-page-hp-${honeypot}`">{{ t('nextcloud-vue', 'Leave this field empty') }}</label>
|
|
123
|
+
<input
|
|
124
|
+
:id="`cn-form-page-hp-${honeypot}`"
|
|
125
|
+
v-model="honeypotValue"
|
|
126
|
+
:name="honeypot"
|
|
127
|
+
type="text"
|
|
128
|
+
tabindex="-1"
|
|
129
|
+
autocomplete="off"
|
|
130
|
+
data-testid="cn-form-page-honeypot">
|
|
131
|
+
</div>
|
|
119
132
|
<!-- Paste to fill: only where it may be used (see smartPasteAvailable). -->
|
|
120
133
|
<div v-if="smartPasteAvailable" class="cn-form-page__smart-paste">
|
|
121
134
|
<NcButton type="button" data-testid="cn-form-page-smart-paste" @click="smartPasteOpen = true">
|
|
@@ -610,6 +623,17 @@ export default {
|
|
|
610
623
|
default: '',
|
|
611
624
|
},
|
|
612
625
|
|
|
626
|
+
/**
|
|
627
|
+
* Name of a honeypot field for a form anonymous visitors fill in.
|
|
628
|
+
* The field is rendered out of sight and out of the tab order; when
|
|
629
|
+
* it is filled, submit sends nothing and shows the normal success,
|
|
630
|
+
* so a bot learns nothing. Empty (default): no honeypot.
|
|
631
|
+
*/
|
|
632
|
+
honeypot: {
|
|
633
|
+
type: String,
|
|
634
|
+
default: '',
|
|
635
|
+
},
|
|
636
|
+
|
|
613
637
|
/** HTTP method for endpoint mode. POST | PUT | PATCH. */
|
|
614
638
|
submitMethod: {
|
|
615
639
|
type: String,
|
|
@@ -748,6 +772,8 @@ export default {
|
|
|
748
772
|
summaryKeys: [],
|
|
749
773
|
/** Per-field validation error messages, keyed by field.key. */
|
|
750
774
|
fieldErrors: {},
|
|
775
|
+
/** What a bot typed into the honeypot field; stays empty for a person. */
|
|
776
|
+
honeypotValue: '',
|
|
751
777
|
/**
|
|
752
778
|
* Cache of resolved `endpoint`/`source` visibleWhen outcomes,
|
|
753
779
|
* keyed by field.key. Resolved ONCE in `mounted()` — see the
|
|
@@ -1783,6 +1809,11 @@ export default {
|
|
|
1783
1809
|
}
|
|
1784
1810
|
|
|
1785
1811
|
this.lastError = null
|
|
1812
|
+
if (this.honeypot && this.honeypotValue !== '') {
|
|
1813
|
+
// A bot filled the field no person sees: send nothing, look done.
|
|
1814
|
+
this.submitted = true
|
|
1815
|
+
return
|
|
1816
|
+
}
|
|
1786
1817
|
this.submitting = true
|
|
1787
1818
|
try {
|
|
1788
1819
|
if (this.submitEndpoint) {
|
|
@@ -1808,6 +1839,9 @@ export default {
|
|
|
1808
1839
|
// describes the transport ("Request failed with status code 400")
|
|
1809
1840
|
// and leaves the user to guess which field it meant.
|
|
1810
1841
|
this.lastError = serverErrorMessage(err)
|
|
1842
|
+
// A refusal that names its fields (`findings[]`, as OpenRegister's
|
|
1843
|
+
// form destination check answers 422) is marked on each field.
|
|
1844
|
+
this.showServerFindings(err)
|
|
1811
1845
|
/**
|
|
1812
1846
|
* Submit failure event. Payload is the thrown error / rejected reason.
|
|
1813
1847
|
*
|
|
@@ -1820,6 +1854,45 @@ export default {
|
|
|
1820
1854
|
}
|
|
1821
1855
|
},
|
|
1822
1856
|
|
|
1857
|
+
/**
|
|
1858
|
+
* Put each finding of a refused submit on its field.
|
|
1859
|
+
*
|
|
1860
|
+
* Reads `response.data.findings[]`, each `{ field?, property, message }`,
|
|
1861
|
+
* and marks the field whose key is the finding's `field`, or else its
|
|
1862
|
+
* `property`. Findings on no field of this form stay in the general
|
|
1863
|
+
* error above the form. Nothing happens for an answer without findings.
|
|
1864
|
+
*
|
|
1865
|
+
* @param {object} err The rejected request.
|
|
1866
|
+
* @return {void}
|
|
1867
|
+
*/
|
|
1868
|
+
showServerFindings(err) {
|
|
1869
|
+
const findings = err?.response?.data?.findings
|
|
1870
|
+
if (!Array.isArray(findings) || findings.length === 0) {
|
|
1871
|
+
return
|
|
1872
|
+
}
|
|
1873
|
+
const keys = new Set(this.fields.map((f) => f && f.key))
|
|
1874
|
+
const marked = []
|
|
1875
|
+
for (const finding of findings) {
|
|
1876
|
+
const key = finding && (finding.field || finding.property)
|
|
1877
|
+
if (!key || !keys.has(key) || this.fieldErrors[key]) {
|
|
1878
|
+
continue
|
|
1879
|
+
}
|
|
1880
|
+
this.fieldErrors[key] = String(finding.message || t('nextcloud-vue', 'This answer was refused.'))
|
|
1881
|
+
marked.push(key)
|
|
1882
|
+
}
|
|
1883
|
+
if (marked.length === 0) {
|
|
1884
|
+
return
|
|
1885
|
+
}
|
|
1886
|
+
this.summaryKeys = this.fields.map((f) => f.key).filter((key) => marked.includes(key))
|
|
1887
|
+
if (this.hasSteps) {
|
|
1888
|
+
const stepIndex = this.steps.findIndex((step) => this.stepFields(step).some((f) => f.key === this.summaryKeys[0]))
|
|
1889
|
+
if (stepIndex >= 0) {
|
|
1890
|
+
this.currentStepIndex = stepIndex
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
this.focusFirstError(this.summaryKeys[0])
|
|
1894
|
+
},
|
|
1895
|
+
|
|
1823
1896
|
async submitViaEndpoint() {
|
|
1824
1897
|
const method = (this.submitMethod || 'POST').toLowerCase()
|
|
1825
1898
|
const url = resolveParams(this.submitEndpoint, this.$route?.params || {})
|
|
@@ -1843,6 +1916,14 @@ export default {
|
|
|
1843
1916
|
</script>
|
|
1844
1917
|
|
|
1845
1918
|
<style>
|
|
1919
|
+
.cn-form-page__honeypot {
|
|
1920
|
+
position: absolute;
|
|
1921
|
+
left: -10000px;
|
|
1922
|
+
width: 1px;
|
|
1923
|
+
height: 1px;
|
|
1924
|
+
overflow: hidden;
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1846
1927
|
.cn-form-page {
|
|
1847
1928
|
display: flex;
|
|
1848
1929
|
flex-direction: column;
|
|
@@ -263,6 +263,31 @@
|
|
|
263
263
|
@update:activeIndex="onQuickFilterChange"
|
|
264
264
|
@update:selectedIndices="onQuickFilterMultiChange" />
|
|
265
265
|
</template>
|
|
266
|
+
<!-- Board look, `filterMenu` on a page with no filter sidebar: the
|
|
267
|
+
board's Filter button, a menu of each enum column's values as
|
|
268
|
+
toggleable filters. -->
|
|
269
|
+
<template v-if="boardFilterMenu" #filter-menu>
|
|
270
|
+
<NcActions
|
|
271
|
+
class="cn-actions-bar__filter-menu"
|
|
272
|
+
variant="secondary"
|
|
273
|
+
:forceMenu="true"
|
|
274
|
+
:menuName="t('nextcloud-vue', 'Filter')"
|
|
275
|
+
data-testid="cn-index-filter-menu">
|
|
276
|
+
<template #icon>
|
|
277
|
+
<Tune :size="16" />
|
|
278
|
+
</template>
|
|
279
|
+
<template v-for="field in filterableFields" :key="field.key">
|
|
280
|
+
<NcActionCaption :name="field.label" />
|
|
281
|
+
<NcActionCheckbox
|
|
282
|
+
v-for="val in field.values"
|
|
283
|
+
:key="`${field.key}-${val}`"
|
|
284
|
+
:modelValue="isFilterActive(field.key, val)"
|
|
285
|
+
@update:modelValue="toggleFilter(field.key, val)">
|
|
286
|
+
{{ val }}
|
|
287
|
+
</NcActionCheckbox>
|
|
288
|
+
</template>
|
|
289
|
+
</NcActions>
|
|
290
|
+
</template>
|
|
266
291
|
</CnActionsBar>
|
|
267
292
|
|
|
268
293
|
<!-- Quick edit: a few fields of one row, over the list, with the list
|
|
@@ -625,10 +650,10 @@
|
|
|
625
650
|
the column menu lists every governed column as a visibility
|
|
626
651
|
checkbox — compact, in-table alternatives to the sidebar. -->
|
|
627
652
|
<template
|
|
628
|
-
v-if="(
|
|
653
|
+
v-if="(showHeaderFilterMenu) || (columnMenu && governedColumns.length)"
|
|
629
654
|
#actions-header>
|
|
630
655
|
<NcActions
|
|
631
|
-
v-if="
|
|
656
|
+
v-if="showHeaderFilterMenu"
|
|
632
657
|
:forceMenu="true"
|
|
633
658
|
:aria-label="t('nextcloud-vue', 'Filter')">
|
|
634
659
|
<template #icon>
|
|
@@ -968,6 +993,7 @@ import DotsHorizontal from 'vue-material-design-icons/DotsHorizontal.vue'
|
|
|
968
993
|
import Export from 'vue-material-design-icons/Export.vue'
|
|
969
994
|
import Eye from 'vue-material-design-icons/Eye.vue'
|
|
970
995
|
import FilterOutline from 'vue-material-design-icons/FilterOutline.vue'
|
|
996
|
+
import Tune from 'vue-material-design-icons/Tune.vue'
|
|
971
997
|
import ViewColumnOutline from 'vue-material-design-icons/ViewColumnOutline.vue'
|
|
972
998
|
import CnConfirmDialog from '../../dialogs/CnConfirmDialog.vue'
|
|
973
999
|
import CnQuickEditDialog from '../../dialogs/CnQuickEditDialog.vue'
|
|
@@ -1275,6 +1301,7 @@ export default {
|
|
|
1275
1301
|
DatabaseSearch,
|
|
1276
1302
|
Export,
|
|
1277
1303
|
FilterOutline,
|
|
1304
|
+
Tune,
|
|
1278
1305
|
ViewColumnOutline,
|
|
1279
1306
|
NcCheckboxRadioSwitch,
|
|
1280
1307
|
CnPageHeader,
|
|
@@ -1607,7 +1634,10 @@ export default {
|
|
|
1607
1634
|
* unchanged. Under the board look a tab with `activeLabel` ("Pipeline:
|
|
1608
1635
|
* Sales") adds a removable chip with that text to the Active line and
|
|
1609
1636
|
* counts on the Filter badge while it is selected; removing the chip
|
|
1610
|
-
* goes back to the default tab.
|
|
1637
|
+
* goes back to the default tab. A tab can say `empty: "field"` (or a
|
|
1638
|
+
* list of fields) for the rows where the field has no value, and
|
|
1639
|
+
* `notEmpty` for the rows where it has one: it reads as the filter
|
|
1640
|
+
* `{ field: { isnull: true } }`.
|
|
1611
1641
|
*/
|
|
1612
1642
|
quickFilters: {
|
|
1613
1643
|
type: Array,
|
|
@@ -2892,6 +2922,8 @@ export default {
|
|
|
2892
2922
|
* Show a filter menu (funnel button) in the table header, above the
|
|
2893
2923
|
* row-actions column. Its menu lists every enum/badge column's values as
|
|
2894
2924
|
* toggleable facet filters — a compact alternative to the facet sidebar.
|
|
2925
|
+
* Under the board look, on a page with no filter sidebar, it is the
|
|
2926
|
+
* toolbar's labelled Filter button instead (row 1, before the view switch).
|
|
2895
2927
|
* Fed from the manifest as `pages[].config.filterMenu`.
|
|
2896
2928
|
*/
|
|
2897
2929
|
filterMenu: {
|
|
@@ -5606,6 +5638,27 @@ export default {
|
|
|
5606
5638
|
return !!this.resolvedSidebar.enabled && this.resolvedSidebar.show !== false
|
|
5607
5639
|
},
|
|
5608
5640
|
|
|
5641
|
+
/**
|
|
5642
|
+
* Whether the board toolbar draws the Filter button as a menu: the
|
|
5643
|
+
* board look, `filterMenu` on, something to filter by, and no filter
|
|
5644
|
+
* sidebar (whose toggle is that button otherwise).
|
|
5645
|
+
*
|
|
5646
|
+
* @return {boolean}
|
|
5647
|
+
*/
|
|
5648
|
+
boardFilterMenu() {
|
|
5649
|
+
return this.isBoardLook && this.filterMenu && !this.hasSidebar && this.filterableFields.length > 0
|
|
5650
|
+
},
|
|
5651
|
+
|
|
5652
|
+
/**
|
|
5653
|
+
* Whether the funnel menu sits in the table header: `filterMenu` with
|
|
5654
|
+
* values to offer, unless the toolbar's Filter button is that menu.
|
|
5655
|
+
*
|
|
5656
|
+
* @return {boolean}
|
|
5657
|
+
*/
|
|
5658
|
+
showHeaderFilterMenu() {
|
|
5659
|
+
return this.filterMenu && this.filterableFields.length > 0 && !this.boardFilterMenu
|
|
5660
|
+
},
|
|
5661
|
+
|
|
5609
5662
|
/** Search props forwarded to the embedded CnIndexSidebar (defaults applied per CnIndexSidebar). */
|
|
5610
5663
|
sidebarSearchProps() {
|
|
5611
5664
|
return (this.sidebar && this.sidebar.search) || {}
|
|
@@ -270,7 +270,7 @@ import Pin from 'vue-material-design-icons/Pin.vue'
|
|
|
270
270
|
import PinOutline from 'vue-material-design-icons/PinOutline.vue'
|
|
271
271
|
import ViewColumnOutline from 'vue-material-design-icons/ViewColumnOutline.vue'
|
|
272
272
|
import { METADATA_COLUMNS } from '../../constants/metadata.js'
|
|
273
|
-
import {
|
|
273
|
+
import { facetOptions } from '../../utils/facets.js'
|
|
274
274
|
import { labelLang } from '../../utils/manifestTranslate.js'
|
|
275
275
|
import { columnsFromSchema, filtersFromSchema } from '../../utils/schema.js'
|
|
276
276
|
import { CnDateRangePicker, DEFAULT_DATE_RANGE_PRESETS } from '../CnDateRangePicker/index.js'
|
|
@@ -867,12 +867,12 @@ export default {
|
|
|
867
867
|
* @param {object} filter Filter object
|
|
868
868
|
*/
|
|
869
869
|
getFilterOptions(filter) {
|
|
870
|
-
const
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
870
|
+
const live = facetOptions(
|
|
871
|
+
this.facetData[filter.key],
|
|
872
|
+
(count) => t('nextcloud-vue', 'No value ({count})', { count }),
|
|
873
|
+
)
|
|
874
|
+
if (live !== null) {
|
|
875
|
+
return live
|
|
876
876
|
}
|
|
877
877
|
// A reference picker's rows come from another list, and neither a
|
|
878
878
|
// schema enum nor a facet bucket can carry them.
|
|
@@ -21,7 +21,7 @@ import { resolveNextStep } from '../../utils/detailActionModel.js'
|
|
|
21
21
|
* Renders CnNextStepCard for the bound record's stage from
|
|
22
22
|
* `content: { field?, stages: { <stage>: { title?, checklist, after? } } }`.
|
|
23
23
|
* The checklist only: the page's own primary button stays where the page
|
|
24
|
-
* puts it. Resolved by its registry key,
|
|
24
|
+
* puts it. Resolved by its registry key, and exported for an app that renders it itself.
|
|
25
25
|
*/
|
|
26
26
|
export default {
|
|
27
27
|
name: 'CnNextStepWidget',
|
|
@@ -619,7 +619,7 @@ export default {
|
|
|
619
619
|
// `secondary` (a second, muted line under the cell) rides
|
|
620
620
|
// along too: the table draws it, and dropping it here took the
|
|
621
621
|
// list rows' second line away (zuiddrecht-pixel-gaps-3).
|
|
622
|
-
for (const k of ['format', 'widget', 'widgetProps', 'formatter', 'align', 'width', 'type', 'enum', 'enumLabels', 'sortable', 'secondary']) {
|
|
622
|
+
for (const k of ['format', 'widget', 'widgetProps', 'formatter', 'align', 'width', 'type', 'enum', 'enumLabels', 'sortable', 'secondary', 'colorMap']) {
|
|
623
623
|
if (c[k] !== undefined) {
|
|
624
624
|
out[k] = c[k]
|
|
625
625
|
}
|
|
@@ -16,6 +16,13 @@
|
|
|
16
16
|
:rowActions="rowActions"
|
|
17
17
|
:newActions="newActions"
|
|
18
18
|
:linkedItems="linkedItems"
|
|
19
|
+
:columns="columns"
|
|
20
|
+
:rowData="rowDataSource"
|
|
21
|
+
:preferenceApp="preferenceApp"
|
|
22
|
+
:preferenceKey="preferenceKey"
|
|
23
|
+
:bulkActions="bulkActions"
|
|
24
|
+
:groupBy="groupBy"
|
|
25
|
+
:facets="facets"
|
|
19
26
|
@changed="onBrowserChanged" />
|
|
20
27
|
<template v-else>
|
|
21
28
|
<!-- Upload error -->
|
|
@@ -240,7 +247,7 @@ import CnFilePreview from '../CnFilePreview/CnFilePreview.vue'
|
|
|
240
247
|
import CnFilesBrowser from '../CnFilesBrowser/CnFilesBrowser.vue'
|
|
241
248
|
import { useFileOpener } from '../../composables/useFileOpener.js'
|
|
242
249
|
import { buildHeaders, prefixUrl } from '../../utils/index.js'
|
|
243
|
-
import { objectDisplayName, resolveObjectFolderInfo } from '../CnFilesBrowser/filesBrowser.js'
|
|
250
|
+
import { indexRowData, objectDisplayName, resolveObjectFolderInfo } from '../CnFilesBrowser/filesBrowser.js'
|
|
244
251
|
import { createOpenRegisterSource } from '../CnFilesBrowser/openRegisterSource.js'
|
|
245
252
|
|
|
246
253
|
/**
|
|
@@ -359,6 +366,47 @@ export default {
|
|
|
359
366
|
* Null fetches nothing.
|
|
360
367
|
*/
|
|
361
368
|
linkedItemsUrl: { type: String, default: null },
|
|
369
|
+
/**
|
|
370
|
+
* Forwarded to CnFilesBrowser: the columns the browser shows, built-in
|
|
371
|
+
* names or declared columns (`{ key, label, source, formatter }`).
|
|
372
|
+
* A `source: 'row'` column reads the host's record for the file from
|
|
373
|
+
* `rowDataUrl`. None keeps name, size and modified.
|
|
374
|
+
*
|
|
375
|
+
* @type {Array<string|object>}
|
|
376
|
+
*/
|
|
377
|
+
columns: { type: Array, default: () => [] },
|
|
378
|
+
/** Forwarded to CnFilesBrowser: the app id the user's column choice is stored under; unset offers no Columns chooser. */
|
|
379
|
+
preferenceApp: { type: String, default: '' },
|
|
380
|
+
/** Forwarded to CnFilesBrowser: the key the column choice is stored under. */
|
|
381
|
+
preferenceKey: { type: String, default: 'files-browser-columns' },
|
|
382
|
+
/**
|
|
383
|
+
* An app endpoint that answers the host's own record per file, for the
|
|
384
|
+
* browser's `source: 'row'` columns. App-relative; `{objectId}` is
|
|
385
|
+
* replaced. Read once per listing. The answer is a list of records
|
|
386
|
+
* (or the list at `rowDataPath`, or under `items` or `results`), each
|
|
387
|
+
* naming its file in `rowDataKey`. Null reads nothing.
|
|
388
|
+
*/
|
|
389
|
+
rowDataUrl: { type: String, default: null },
|
|
390
|
+
/**
|
|
391
|
+
* Forwarded to CnFilesBrowser: the host's actions on a selection of
|
|
392
|
+
* files. With at least one, files get checkboxes and a bulk bar.
|
|
393
|
+
*
|
|
394
|
+
* @type {Array<{id: string, label: string, icon?: string, type?: string, target?: string, props?: object}>}
|
|
395
|
+
*/
|
|
396
|
+
bulkActions: { type: Array, default: () => [] },
|
|
397
|
+
/** Forwarded to CnFilesBrowser: the key of a declared column to group the files on. */
|
|
398
|
+
groupBy: { type: String, default: '' },
|
|
399
|
+
/**
|
|
400
|
+
* Forwarded to CnFilesBrowser: keys of declared columns to filter on,
|
|
401
|
+
* a chip per value in use.
|
|
402
|
+
*
|
|
403
|
+
* @type {string[]}
|
|
404
|
+
*/
|
|
405
|
+
facets: { type: Array, default: () => [] },
|
|
406
|
+
/** A dotted path to the list of records in the `rowDataUrl` answer; empty reads the answer itself. */
|
|
407
|
+
rowDataPath: { type: String, default: '' },
|
|
408
|
+
/** The record field that holds the Nextcloud file id. */
|
|
409
|
+
rowDataKey: { type: String, default: 'fileId' },
|
|
362
410
|
/** Label of the Download action. */
|
|
363
411
|
downloadLabel: { type: String, default: () => t('nextcloud-vue', 'Download') },
|
|
364
412
|
/** Label of the action that opens the Files sidebar on the file. */
|
|
@@ -424,6 +472,27 @@ export default {
|
|
|
424
472
|
},
|
|
425
473
|
|
|
426
474
|
computed: {
|
|
475
|
+
/**
|
|
476
|
+
* The browser's `rowData`: a function that reads the host's records
|
|
477
|
+
* from `rowDataUrl` and keys them by file id, or null without a url.
|
|
478
|
+
* The browser calls it once per listing.
|
|
479
|
+
*
|
|
480
|
+
* @return {(function(): Promise<object>)|null} The row data source.
|
|
481
|
+
* @spec openspec/changes/files-browser-hosts-a-documents-list/specs/files-browser/spec.md
|
|
482
|
+
*/
|
|
483
|
+
rowDataSource() {
|
|
484
|
+
if (!this.rowDataUrl || !this.objectId) {
|
|
485
|
+
return null
|
|
486
|
+
}
|
|
487
|
+
const url = generateUrl(this.rowDataUrl.replace('{objectId}', encodeURIComponent(this.objectId)))
|
|
488
|
+
const path = this.rowDataPath
|
|
489
|
+
const key = this.rowDataKey
|
|
490
|
+
return async () => {
|
|
491
|
+
const { data } = await axios.get(url)
|
|
492
|
+
return indexRowData(data, { path, key })
|
|
493
|
+
}
|
|
494
|
+
},
|
|
495
|
+
|
|
427
496
|
/**
|
|
428
497
|
* The root crumb: the host's label, else the object's name (its folder
|
|
429
498
|
* on disk is called after its uuid), else the folder's own name.
|
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
{{ cancelLabel }}
|
|
19
19
|
</NcButton>
|
|
20
20
|
</p>
|
|
21
|
+
<!-- @slot composer-before An app's own control above the composer, e.g. a template picker. Scope: `{ setText, text }`; `setText(text)` fills the composer, `text` is what it holds now. -->
|
|
22
|
+
<slot name="composer-before" :setText="setComposerText" :text="newNoteText" />
|
|
21
23
|
<CnNoteComposer
|
|
22
24
|
class="cn-sidebar-tab__composer"
|
|
23
25
|
:modelValue="newNoteText"
|
|
@@ -481,6 +483,20 @@ export default {
|
|
|
481
483
|
}
|
|
482
484
|
},
|
|
483
485
|
|
|
486
|
+
/**
|
|
487
|
+
* Fill the composer from outside, for the `composer-before` slot.
|
|
488
|
+
*
|
|
489
|
+
* The app decides what goes in (a template body, a quoted line); the
|
|
490
|
+
* composer stays the one place the text lives, so sending, editing and
|
|
491
|
+
* the mention picker all see it exactly as if it had been typed.
|
|
492
|
+
*
|
|
493
|
+
* @param {string} text The text the composer should hold.
|
|
494
|
+
* @return {void}
|
|
495
|
+
*/
|
|
496
|
+
setComposerText(text) {
|
|
497
|
+
this.newNoteText = (text === null || text === undefined) ? '' : String(text)
|
|
498
|
+
},
|
|
499
|
+
|
|
484
500
|
startEdit(note) {
|
|
485
501
|
this.replyTo = null
|
|
486
502
|
this.editingNoteId = note.id
|