@conduction/nextcloud-vue 2.75.0 → 2.77.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/components/CnAppNav/CnAppNav.vue2.js +18 -3
- package/dist/esm/components/CnAppNav/CnAppNav.vue2.js.map +1 -1
- package/dist/esm/components/CnAppNav/CnAppNav.vue3.js +49 -3
- package/dist/esm/components/CnAppNav/CnAppNav.vue3.js.map +1 -1
- package/dist/esm/components/CnCardGrid/CnCardGrid.vue2.js +6 -0
- package/dist/esm/components/CnCardGrid/CnCardGrid.vue2.js.map +1 -1
- package/dist/esm/components/CnCardGrid/CnCardGrid.vue3.js +3 -2
- package/dist/esm/components/CnCardGrid/CnCardGrid.vue3.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js +42 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue2.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js +228 -203
- package/dist/esm/components/CnCellRenderer/CnCellRenderer.vue3.js.map +1 -1
- package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue.js +9 -0
- package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue.js.map +1 -0
- package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue2.js +50 -0
- package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue2.js.map +1 -0
- package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue3.js +13 -0
- package/dist/esm/components/CnCellRenderer/CnGroupNameCell.vue3.js.map +1 -0
- package/dist/esm/components/CnCredentials/CnCredentials.vue2.js +11 -10
- package/dist/esm/components/CnCredentials/CnCredentials.vue2.js.map +1 -1
- package/dist/esm/components/CnCredentials/CnCredentials.vue3.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js +41 -4
- package/dist/esm/components/CnDataTable/CnDataTable.vue2.js.map +1 -1
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js +90 -51
- package/dist/esm/components/CnDataTable/CnDataTable.vue3.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js +24 -33
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue2.js.map +1 -1
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js +12 -24
- package/dist/esm/components/CnDetailPage/CnDetailPage.vue3.js.map +1 -1
- package/dist/esm/components/CnFavouriteToggle/CnFavouriteToggle.vue2.js +7 -2
- package/dist/esm/components/CnFavouriteToggle/CnFavouriteToggle.vue2.js.map +1 -1
- package/dist/esm/components/CnFavouriteToggle/CnFavouriteToggle.vue3.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js +114 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue2.js.map +1 -1
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js +115 -62
- package/dist/esm/components/CnFilesBrowser/CnFilesBrowser.vue3.js.map +1 -1
- package/dist/esm/components/CnFollowToggle/CnFollowToggle.vue2.js +89 -7
- package/dist/esm/components/CnFollowToggle/CnFollowToggle.vue2.js.map +1 -1
- package/dist/esm/components/CnFollowToggle/CnFollowToggle.vue3.js +62 -30
- package/dist/esm/components/CnFollowToggle/CnFollowToggle.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js +242 -19
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue2.js.map +1 -1
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js +25 -20
- package/dist/esm/components/CnIndexPage/CnIndexPage.vue3.js.map +1 -1
- package/dist/esm/components/CnIndexPage/selfModeActions.js +90 -14
- package/dist/esm/components/CnIndexPage/selfModeActions.js.map +1 -1
- package/dist/esm/components/CnIndexPage/useSelfFetchList.js +15 -3
- package/dist/esm/components/CnIndexPage/useSelfFetchList.js.map +1 -1
- package/dist/esm/components/CnObjectCard/CnObjectCard.vue2.js +26 -0
- package/dist/esm/components/CnObjectCard/CnObjectCard.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectCard/CnObjectCard.vue3.js +26 -9
- package/dist/esm/components/CnObjectCard/CnObjectCard.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js +199 -7
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js +155 -119
- package/dist/esm/components/CnObjectDataWidget/CnObjectDataWidget.vue3.js.map +1 -1
- package/dist/esm/components/CnObjectListWidget/CnObjectListWidget.vue2.js +19 -3
- 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 +6 -0
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue2.js.map +1 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js +4 -1
- package/dist/esm/components/CnObjectSidebar/CnFilesTab.vue3.js.map +1 -1
- package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue2.js +153 -38
- package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue2.js.map +1 -1
- package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue3.js +74 -56
- package/dist/esm/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.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/useListView.js +4 -0
- package/dist/esm/composables/useListView.js.map +1 -1
- package/dist/esm/composables/useObjectLock.js +41 -10
- package/dist/esm/composables/useObjectLock.js.map +1 -1
- package/dist/esm/index.js +3 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/l10n/en.json.js +31 -1
- package/dist/esm/l10n/en.json.js.map +1 -1
- package/dist/esm/l10n/nl.json.js +32 -2
- package/dist/esm/l10n/nl.json.js.map +1 -1
- package/dist/esm/nextcloud-vue.css +376 -75
- package/dist/esm/store/plugins/interactions.js +20 -10
- package/dist/esm/store/plugins/interactions.js.map +1 -1
- package/dist/esm/store/useObjectStore.js +54 -9
- package/dist/esm/store/useObjectStore.js.map +1 -1
- package/dist/esm/utils/builtInFormatters.js +15 -7
- package/dist/esm/utils/builtInFormatters.js.map +1 -1
- package/dist/esm/utils/groupAutocomplete.js +63 -1
- package/dist/esm/utils/groupAutocomplete.js.map +1 -1
- package/dist/esm/utils/lensAvailability.js +90 -0
- package/dist/esm/utils/lensAvailability.js.map +1 -0
- package/dist/esm/utils/objectSignals.js +44 -0
- package/dist/esm/utils/objectSignals.js.map +1 -0
- package/dist/esm/utils/personalLenses.js +13 -6
- package/dist/esm/utils/personalLenses.js.map +1 -1
- package/dist/esm/utils/recordInteractions.js +31 -13
- package/dist/esm/utils/recordInteractions.js.map +1 -1
- package/dist/esm/utils/resolveFilterTokens.js +46 -3
- package/dist/esm/utils/resolveFilterTokens.js.map +1 -1
- package/dist/esm/utils/schema.js +1 -1
- package/dist/esm/utils/schema.js.map +1 -1
- package/dist/esm/utils/sentinelTokens.js +4 -4
- package/dist/esm/utils/sentinelTokens.js.map +1 -1
- package/dist/esm/utils/validateManifestV2.compiled.js +2 -2
- package/dist/esm/utils/validateManifestV2.compiled.js.map +1 -1
- package/dist/esm/utils/widgetVisibility.js +46 -1
- package/dist/esm/utils/widgetVisibility.js.map +1 -1
- package/dist/nextcloud-vue.cjs.js +4732 -2990
- package/dist/nextcloud-vue.cjs.js.map +2 -2
- package/dist/nextcloud-vue.css +376 -75
- package/l10n/en.json +31 -1
- package/l10n/nl.json +32 -2
- package/package.json +1 -1
- package/src/components/CnAppNav/CnAppNav.vue +44 -5
- package/src/components/CnCardGrid/CnCardGrid.md +1 -0
- package/src/components/CnCardGrid/CnCardGrid.vue +7 -0
- package/src/components/CnCellRenderer/CnCellRenderer.vue +56 -1
- package/src/components/CnCellRenderer/CnGroupNameCell.vue +61 -0
- package/src/components/CnCredentials/CnCredentials.vue +17 -16
- package/src/components/CnDataTable/CnDataTable.md +9 -0
- package/src/components/CnDataTable/CnDataTable.vue +80 -20
- package/src/components/CnDetailPage/CnDetailPage.vue +18 -32
- package/src/components/CnFavouriteToggle/CnFavouriteToggle.vue +7 -2
- package/src/components/CnFilesBrowser/CnFilesBrowser.vue +204 -5
- package/src/components/CnFollowToggle/CnFollowToggle.vue +118 -13
- package/src/components/CnIndexPage/CnIndexPage.md +51 -0
- package/src/components/CnIndexPage/CnIndexPage.vue +261 -35
- package/src/components/CnIndexPage/selfModeActions.js +89 -13
- package/src/components/CnIndexPage/useSelfFetchList.js +15 -3
- package/src/components/CnObjectCard/CnObjectCard.md +1 -0
- package/src/components/CnObjectCard/CnObjectCard.vue +89 -1
- package/src/components/CnObjectDataWidget/CnObjectDataWidget.vue +226 -2
- package/src/components/CnObjectListWidget/CnObjectListWidget.vue +19 -3
- package/src/components/CnObjectSidebar/CnFilesTab.vue +9 -0
- package/src/components/CnRelatedObjectsWidget/CnRelatedObjectsWidget.vue +181 -38
- package/src/components/CnWidgetObjectTable/CnWidgetObjectTable.vue +13 -0
- package/src/composables/__tests__/useObjectLock.endpoints.spec.js +83 -0
- package/src/composables/useListView.js +4 -0
- package/src/composables/useObjectLock.js +41 -10
- package/src/css/look-board-detail.css +40 -2
- package/src/css/look-board-index.css +35 -1
- package/src/css/table.css +82 -17
- package/src/index.js +3 -1
- package/src/schemas/app-manifest-v2.schema.json +11 -7
- package/src/store/plugins/interactions.js +20 -10
- package/src/store/useObjectStore.js +54 -9
- package/src/utils/builtInFormatters.js +15 -7
- package/src/utils/groupAutocomplete.js +75 -0
- package/src/utils/index.js +2 -1
- package/src/utils/lensAvailability.js +86 -0
- package/src/utils/objectSignals.js +41 -0
- package/src/utils/personalLenses.js +13 -6
- package/src/utils/recordInteractions.js +31 -13
- package/src/utils/resolveFilterTokens.js +45 -2
- package/src/utils/schema.js +1 -1
- package/src/utils/sentinelTokens.js +4 -4
- package/src/utils/validateManifestV2.compiled.js +2 -2
- package/src/utils/widgetVisibility.js +45 -0
|
@@ -13,6 +13,58 @@ function resolveNameField(ctx) {
|
|
|
13
13
|
|
|
14
14
|
function findSource(ctx, id) {
|
|
15
15
|
return ctx.effectiveObjects().find((o) => o.id === id || o['@self']?.id === id)
|
|
16
|
+
|| (typeof ctx.knownRow === 'function' ? ctx.knownRow(id) : undefined)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The register/schema of a row that belongs to another pair than the page's,
|
|
21
|
+
* in a list fetched from a `collectionUrl`. Registers a store type for that
|
|
22
|
+
* pair on first use. Returns null for rows of the page's own pair, and always
|
|
23
|
+
* without a `collectionUrl`.
|
|
24
|
+
*
|
|
25
|
+
* @param {object} opts Page context.
|
|
26
|
+
* @param {string} opts.collectionUrl The page's collection endpoint, or ''.
|
|
27
|
+
* @param {string} opts.register The page's register.
|
|
28
|
+
* @param {string} opts.schema The page's schema.
|
|
29
|
+
* @param {object|null} opts.primarySchema The page's resolved schema.
|
|
30
|
+
* @param {object|null} [opts.primaryRegister] The page's resolved register, which matches a slug `register` to `@self.register`.
|
|
31
|
+
* @param {object|null} opts.store The object store.
|
|
32
|
+
* @param {object} row The row.
|
|
33
|
+
* @return {{register: string, schema: string, type: string}|null} The row's own pair.
|
|
34
|
+
*/
|
|
35
|
+
export function resolveRowTarget({ collectionUrl, register, schema, primarySchema, primaryRegister, store }, row) {
|
|
36
|
+
if (!collectionUrl || !row || !store) {
|
|
37
|
+
return null
|
|
38
|
+
}
|
|
39
|
+
const self = row['@self'] || {}
|
|
40
|
+
const reg = self.register !== undefined && self.register !== null ? String(self.register) : ''
|
|
41
|
+
const sch = self.schema !== undefined && self.schema !== null ? String(self.schema) : ''
|
|
42
|
+
if (!reg || !sch) {
|
|
43
|
+
return null
|
|
44
|
+
}
|
|
45
|
+
const idsOf = (...values) => values.filter((v) => v !== undefined && v !== null && v !== '').map(String)
|
|
46
|
+
const schemaIds = idsOf(schema, primarySchema && primarySchema.id, primarySchema && primarySchema.slug)
|
|
47
|
+
const registerIds = idsOf(register, primaryRegister && primaryRegister.id, primaryRegister && primaryRegister.slug)
|
|
48
|
+
if (schemaIds.includes(sch) && registerIds.includes(reg)) {
|
|
49
|
+
return null
|
|
50
|
+
}
|
|
51
|
+
const type = `${reg}-${sch}`
|
|
52
|
+
if (!store.objectTypeRegistry?.[type]) {
|
|
53
|
+
store.registerObjectType(type, sch, reg, { registerSlug: reg, schemaSlug: sch })
|
|
54
|
+
}
|
|
55
|
+
return { register: reg, schema: sch, type }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The store type a row is written through: its own pair's, or the page's.
|
|
60
|
+
*
|
|
61
|
+
* @param {object} ctx Accessor closures.
|
|
62
|
+
* @param {object} [row] The row.
|
|
63
|
+
* @return {string} The type slug.
|
|
64
|
+
*/
|
|
65
|
+
function typeFor(ctx, row) {
|
|
66
|
+
const target = row && typeof ctx.rowTarget === 'function' ? ctx.rowTarget(row) : null
|
|
67
|
+
return target ? target.type : ctx.selfObjectType()
|
|
16
68
|
}
|
|
17
69
|
|
|
18
70
|
/**
|
|
@@ -25,6 +77,10 @@ function findSource(ctx, id) {
|
|
|
25
77
|
*/
|
|
26
78
|
function copyAddress(ctx, source) {
|
|
27
79
|
const self = source['@self'] || {}
|
|
80
|
+
const target = typeof ctx.rowTarget === 'function' ? ctx.rowTarget(source) : null
|
|
81
|
+
if (target) {
|
|
82
|
+
return { register: target.register, schema: target.schema, id: String(source.id || self.id) }
|
|
83
|
+
}
|
|
28
84
|
const reg = ctx.register()
|
|
29
85
|
const sch = ctx.schema()
|
|
30
86
|
const register = (typeof reg === 'string' && reg) || self.register
|
|
@@ -54,12 +110,12 @@ function refreshList(ctx) {
|
|
|
54
110
|
}
|
|
55
111
|
}
|
|
56
112
|
|
|
57
|
-
function storeError(ctx) {
|
|
58
|
-
return ctx.selfObjectStore()?.getError?.(
|
|
113
|
+
function storeError(ctx, type = ctx.selfObjectType()) {
|
|
114
|
+
return ctx.selfObjectStore()?.getError?.(type)
|
|
59
115
|
}
|
|
60
116
|
|
|
61
|
-
function storeErrorMessage(ctx, fallback) {
|
|
62
|
-
const err = storeError(ctx)
|
|
117
|
+
function storeErrorMessage(ctx, fallback, type) {
|
|
118
|
+
const err = storeError(ctx, type)
|
|
63
119
|
return (err && err.message) || fallback
|
|
64
120
|
}
|
|
65
121
|
|
|
@@ -76,13 +132,14 @@ export function createSelfModeActions(ctx) {
|
|
|
76
132
|
return false
|
|
77
133
|
}
|
|
78
134
|
try {
|
|
79
|
-
const
|
|
135
|
+
const type = typeFor(ctx, findSource(ctx, id))
|
|
136
|
+
const ok = await ctx.selfObjectStore().deleteObject(type, id)
|
|
80
137
|
if (ok) {
|
|
81
138
|
ctx.setResults.singleDelete({ success: true })
|
|
82
139
|
ctx.emit('delete', id)
|
|
83
140
|
refreshList(ctx)
|
|
84
141
|
} else {
|
|
85
|
-
ctx.setResults.singleDelete({ error: storeErrorMessage(ctx, 'Delete failed') })
|
|
142
|
+
ctx.setResults.singleDelete({ error: storeErrorMessage(ctx, 'Delete failed', type) })
|
|
86
143
|
}
|
|
87
144
|
} catch (err) {
|
|
88
145
|
ctx.setResults.singleDelete({ error: (err && err.message) || 'Delete failed' })
|
|
@@ -95,12 +152,28 @@ export function createSelfModeActions(ctx) {
|
|
|
95
152
|
return false
|
|
96
153
|
}
|
|
97
154
|
try {
|
|
98
|
-
|
|
155
|
+
// One request per pair: a mixed list holds rows of several types.
|
|
156
|
+
const idsByType = new Map()
|
|
157
|
+
for (const id of ids) {
|
|
158
|
+
const type = typeFor(ctx, findSource(ctx, id))
|
|
159
|
+
idsByType.set(type, [...(idsByType.get(type) || []), id])
|
|
160
|
+
}
|
|
161
|
+
const successfulIds = []
|
|
162
|
+
const failedIds = []
|
|
163
|
+
let failedType = null
|
|
164
|
+
for (const [type, typeIds] of idsByType) {
|
|
165
|
+
const outcome = await ctx.selfObjectStore().deleteObjects(type, typeIds)
|
|
166
|
+
successfulIds.push(...outcome.successfulIds)
|
|
167
|
+
failedIds.push(...outcome.failedIds)
|
|
168
|
+
if (outcome.failedIds.length > 0 && failedType === null) {
|
|
169
|
+
failedType = type
|
|
170
|
+
}
|
|
171
|
+
}
|
|
99
172
|
if (failedIds.length === 0) {
|
|
100
173
|
ctx.setResults.massDelete({ success: true, successfulIds })
|
|
101
174
|
} else {
|
|
102
175
|
ctx.setResults.massDelete({
|
|
103
|
-
error: storeErrorMessage(ctx, `Failed to delete ${failedIds.length} item(s)
|
|
176
|
+
error: storeErrorMessage(ctx, `Failed to delete ${failedIds.length} item(s)`, failedType),
|
|
104
177
|
successfulIds,
|
|
105
178
|
failedIds,
|
|
106
179
|
})
|
|
@@ -142,13 +215,14 @@ export function createSelfModeActions(ctx) {
|
|
|
142
215
|
}
|
|
143
216
|
}
|
|
144
217
|
const clone = cloneObjectForCopy(source, newName, resolveNameField(ctx))
|
|
145
|
-
const
|
|
218
|
+
const type = typeFor(ctx, source)
|
|
219
|
+
const saved = await ctx.selfObjectStore().saveObject(type, clone)
|
|
146
220
|
if (saved) {
|
|
147
221
|
ctx.setResults.singleCopy({ success: true })
|
|
148
222
|
ctx.emit('copy', payload)
|
|
149
223
|
refreshList(ctx)
|
|
150
224
|
} else {
|
|
151
|
-
ctx.setResults.singleCopy({ error: storeErrorMessage(ctx, 'Copy failed') })
|
|
225
|
+
ctx.setResults.singleCopy({ error: storeErrorMessage(ctx, 'Copy failed', type) })
|
|
152
226
|
}
|
|
153
227
|
} catch (err) {
|
|
154
228
|
ctx.setResults.singleCopy({ error: (err && err.message) || 'Copy failed' })
|
|
@@ -191,7 +265,7 @@ export function createSelfModeActions(ctx) {
|
|
|
191
265
|
}
|
|
192
266
|
const clone = cloneObjectForCopy(source, getName(source), nameField)
|
|
193
267
|
try {
|
|
194
|
-
const saved = await ctx.selfObjectStore().saveObject(ctx
|
|
268
|
+
const saved = await ctx.selfObjectStore().saveObject(typeFor(ctx, source), clone)
|
|
195
269
|
if (saved) {
|
|
196
270
|
successfulIds.push(id)
|
|
197
271
|
} else {
|
|
@@ -261,7 +335,9 @@ export function createSelfModeActions(ctx) {
|
|
|
261
335
|
return false
|
|
262
336
|
}
|
|
263
337
|
try {
|
|
264
|
-
const
|
|
338
|
+
const item = ctx.editItem()
|
|
339
|
+
const type = item ? typeFor(ctx, item) : ctx.selfObjectType()
|
|
340
|
+
const saved = await ctx.selfObjectStore().saveObject(type, formData)
|
|
265
341
|
if (saved) {
|
|
266
342
|
ctx.setResults.form({ success: true })
|
|
267
343
|
const isCreate = !ctx.editItem()
|
|
@@ -277,7 +353,7 @@ export function createSelfModeActions(ctx) {
|
|
|
277
353
|
ctx.afterCreateSuccess(saved)
|
|
278
354
|
}
|
|
279
355
|
} else {
|
|
280
|
-
const err = storeError(ctx)
|
|
356
|
+
const err = storeError(ctx, type)
|
|
281
357
|
if (err && err.isValidation) {
|
|
282
358
|
// Keep the form visible so the user can fix the invalid data.
|
|
283
359
|
ctx.setResults.formValidation(err.fields, err.message || 'Validation failed')
|
|
@@ -106,7 +106,11 @@ export function useSelfFetchList(props, instance, inject, extras = {}) {
|
|
|
106
106
|
const register = folder.register || props.register
|
|
107
107
|
return { register, schema: folder.schema, type: `${register}-${folder.schema}` }
|
|
108
108
|
}
|
|
109
|
-
|
|
109
|
+
// A collection endpoint gets its own type key, so a plain registration of
|
|
110
|
+
// the same pair elsewhere never resets it.
|
|
111
|
+
const collectionUrl = props.collectionUrl || ''
|
|
112
|
+
const type = `${props.register}-${props.schema}` + (collectionUrl ? `@${collectionUrl}` : '')
|
|
113
|
+
return { register: props.register, schema: props.schema, type, collectionUrl }
|
|
110
114
|
})
|
|
111
115
|
const objectType = computed(() => resolvedTarget.value.type)
|
|
112
116
|
const sidebarState = inject('sidebarState', null) ?? inject('objectSidebarState', null)
|
|
@@ -155,9 +159,17 @@ export function useSelfFetchList(props, instance, inject, extras = {}) {
|
|
|
155
159
|
// second arg) — that previously made fetch URLs go to `/api/objects/undefined/[object Object]`.
|
|
156
160
|
// Runs (sync, ahead of the list's own watcher) again whenever the type switches.
|
|
157
161
|
function registerTarget() {
|
|
158
|
-
const { register, schema, type } = resolvedTarget.value
|
|
162
|
+
const { register, schema, type, collectionUrl } = resolvedTarget.value
|
|
159
163
|
if (typeof objectStore.registerObjectType === 'function') {
|
|
160
|
-
objectStore.registerObjectType(type, schema, register, {
|
|
164
|
+
objectStore.registerObjectType(type, schema, register, {
|
|
165
|
+
registerSlug: register,
|
|
166
|
+
schemaSlug: schema,
|
|
167
|
+
...(collectionUrl ? { collectionUrl } : {}),
|
|
168
|
+
})
|
|
169
|
+
}
|
|
170
|
+
// A mixed list tells its own rows apart by `@self.register`, an id, so a slug register needs resolving.
|
|
171
|
+
if (collectionUrl && !/^\d+$/.test(String(register)) && typeof objectStore.fetchRegister === 'function') {
|
|
172
|
+
objectStore.fetchRegister(type)
|
|
161
173
|
}
|
|
162
174
|
}
|
|
163
175
|
registerTarget()
|
|
@@ -137,6 +137,7 @@ export default {
|
|
|
137
137
|
| Prop | Type | Default | Description |
|
|
138
138
|
|---|---|---|---|
|
|
139
139
|
| `maxMetadata` | `Number` | `4` | Maximum number of schema properties to show in the metadata section. Properties are sorted by their `order` value before slicing |
|
|
140
|
+
| `accent` | `Object` | `null` | Status accent `{ variant, icon?, label? }`: a colored start border and, with `icon`, a colored icon before the title. `variant` is `success`, `warning`, `error`, `info` or `primary`; `label` is the icon's tooltip and accessible name |
|
|
140
141
|
|
|
141
142
|
## Slots
|
|
142
143
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div
|
|
3
3
|
class="cn-object-card"
|
|
4
|
-
:class="{ 'cn-object-card--selected': selected, 'cn-object-card--board': isBoardLook }"
|
|
4
|
+
:class="[{ 'cn-object-card--selected': selected, 'cn-object-card--board': isBoardLook }, accentVariant && ['cn-object-card--accent', `cn-object-card--accent-${accentVariant}`]]"
|
|
5
5
|
@mousedown="onCardMouseDown"
|
|
6
6
|
@click="onCardClick($event)"
|
|
7
7
|
@auxclick="onCardAuxClick($event)">
|
|
@@ -42,6 +42,16 @@
|
|
|
42
42
|
to report. A card whose consumer passes no badges
|
|
43
43
|
would otherwise show no padlock at all. -->
|
|
44
44
|
<CnLockIndicator :object="object" :size="16" />
|
|
45
|
+
<span
|
|
46
|
+
v-if="accentVariant && accent.icon"
|
|
47
|
+
class="cn-object-card__accent-icon"
|
|
48
|
+
data-testid="cn-object-card-accent-icon"
|
|
49
|
+
:role="accent.label ? 'img' : null"
|
|
50
|
+
:aria-label="accent.label || null"
|
|
51
|
+
:aria-hidden="accent.label ? null : 'true'"
|
|
52
|
+
:title="accent.label || null">
|
|
53
|
+
<CnIcon :name="accent.icon" :size="16" />
|
|
54
|
+
</span>
|
|
45
55
|
{{ title }}
|
|
46
56
|
</h3>
|
|
47
57
|
<p v-if="description" class="cn-object-card__description">
|
|
@@ -128,6 +138,8 @@ import { CnIcon } from '../CnIcon/index.js'
|
|
|
128
138
|
import { CnLockIndicator } from '../CnLockIndicator/index.js'
|
|
129
139
|
import { CnStatusBadge } from '../CnStatusBadge/index.js'
|
|
130
140
|
|
|
141
|
+
const ACCENT_VARIANTS = ['success', 'warning', 'error', 'info', 'primary']
|
|
142
|
+
|
|
131
143
|
/**
|
|
132
144
|
* CnObjectCard — Schema-configuration-driven card for object display.
|
|
133
145
|
*
|
|
@@ -244,6 +256,20 @@ export default {
|
|
|
244
256
|
default: null,
|
|
245
257
|
},
|
|
246
258
|
|
|
259
|
+
/**
|
|
260
|
+
* A status accent: a colored border at the start of the card and,
|
|
261
|
+
* with `icon`, a colored icon before the title. `variant` is one of
|
|
262
|
+
* `success`, `warning`, `error`, `info` or `primary`; `icon` is a
|
|
263
|
+
* name registered with CnIcon; `label` names the status for screen
|
|
264
|
+
* readers and as the icon's tooltip.
|
|
265
|
+
*
|
|
266
|
+
* @type {{variant: string, icon?: string, label?: string}}
|
|
267
|
+
*/
|
|
268
|
+
accent: {
|
|
269
|
+
type: Object,
|
|
270
|
+
default: null,
|
|
271
|
+
},
|
|
272
|
+
|
|
247
273
|
/**
|
|
248
274
|
* The property keys the facts list shows, in order (manifest key
|
|
249
275
|
* `config.cardFields`). Without it the card shows the first
|
|
@@ -307,6 +333,16 @@ export default {
|
|
|
307
333
|
return null
|
|
308
334
|
},
|
|
309
335
|
|
|
336
|
+
/**
|
|
337
|
+
* The accent variant when it is a known one, else ''.
|
|
338
|
+
*
|
|
339
|
+
* @return {string}
|
|
340
|
+
*/
|
|
341
|
+
accentVariant() {
|
|
342
|
+
const variant = this.accent?.variant
|
|
343
|
+
return ACCENT_VARIANTS.includes(variant) ? variant : ''
|
|
344
|
+
},
|
|
345
|
+
|
|
310
346
|
config() {
|
|
311
347
|
return this.schema?.configuration || {}
|
|
312
348
|
},
|
|
@@ -502,6 +538,58 @@ export default {
|
|
|
502
538
|
background: var(--color-primary-element-light);
|
|
503
539
|
}
|
|
504
540
|
|
|
541
|
+
/* The 4px start border takes 3px of padding, so the content stays put. */
|
|
542
|
+
.cn-object-card.cn-object-card--accent {
|
|
543
|
+
border-inline-start-width: 4px;
|
|
544
|
+
padding-inline-start: 13px;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
.cn-object-card.cn-object-card--accent-success {
|
|
548
|
+
border-inline-start-color: var(--color-element-success);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
.cn-object-card.cn-object-card--accent-warning {
|
|
552
|
+
border-inline-start-color: var(--color-element-warning);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
.cn-object-card.cn-object-card--accent-error {
|
|
556
|
+
border-inline-start-color: var(--color-element-error);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
.cn-object-card.cn-object-card--accent-info {
|
|
560
|
+
border-inline-start-color: var(--color-element-info);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
.cn-object-card.cn-object-card--accent-primary {
|
|
564
|
+
border-inline-start-color: var(--color-primary-element);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
.cn-object-card__accent-icon {
|
|
568
|
+
display: inline-flex;
|
|
569
|
+
vertical-align: -2px;
|
|
570
|
+
margin-inline-end: 4px;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
.cn-object-card--accent-success .cn-object-card__accent-icon {
|
|
574
|
+
color: var(--color-text-success);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
.cn-object-card--accent-warning .cn-object-card__accent-icon {
|
|
578
|
+
color: var(--color-text-warning);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
.cn-object-card--accent-error .cn-object-card__accent-icon {
|
|
582
|
+
color: var(--color-text-error);
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
.cn-object-card--accent-info .cn-object-card__accent-icon {
|
|
586
|
+
color: var(--color-element-info);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
.cn-object-card--accent-primary .cn-object-card__accent-icon {
|
|
590
|
+
color: var(--color-primary-element);
|
|
591
|
+
}
|
|
592
|
+
|
|
505
593
|
.cn-object-card__checkbox {
|
|
506
594
|
flex-shrink: 0;
|
|
507
595
|
padding-top: 2px;
|
|
@@ -149,6 +149,28 @@
|
|
|
149
149
|
@update:modelValue="onRelationChange(field, $event)"
|
|
150
150
|
@close="commitEdit" />
|
|
151
151
|
|
|
152
|
+
<!-- Nextcloud group or user (`referenceType: nextcloud-group`
|
|
153
|
+
/ `nextcloud-user`, one or a list): pick by display name,
|
|
154
|
+
store the group id or uid. The same search the create and
|
|
155
|
+
edit dialogs use. -->
|
|
156
|
+
<component
|
|
157
|
+
:is="isUserPicker(field) ? 'NcSelectUsers' : 'NcSelect'"
|
|
158
|
+
v-else-if="isPersonPicker(field)"
|
|
159
|
+
ref="activeEditor"
|
|
160
|
+
:inputLabel="field.label"
|
|
161
|
+
:labelOutside="true"
|
|
162
|
+
:options="personOptions[field.key] || []"
|
|
163
|
+
:modelValue="personSelected(field)"
|
|
164
|
+
:multiple="isPersonMultiple(field)"
|
|
165
|
+
:keepOpen="isPersonMultiple(field)"
|
|
166
|
+
:loading="personLoading === field.key"
|
|
167
|
+
:filterable="false"
|
|
168
|
+
:clearable="!field.required"
|
|
169
|
+
label="label"
|
|
170
|
+
@search="(q) => searchPeople(field, q)"
|
|
171
|
+
@update:modelValue="onPersonChange(field, $event)"
|
|
172
|
+
@close="isPersonMultiple(field) ? undefined : commitEdit()" />
|
|
173
|
+
|
|
152
174
|
<!-- Select -->
|
|
153
175
|
<NcSelect
|
|
154
176
|
v-else-if="field.widget === 'select'"
|
|
@@ -259,7 +281,17 @@
|
|
|
259
281
|
:value="displayValues[field.key]"
|
|
260
282
|
:raw="(objectData || {})[field.key]" />
|
|
261
283
|
<template v-else>
|
|
262
|
-
|
|
284
|
+
<!-- A Nextcloud group (or list): its display name, cached
|
|
285
|
+
per id; the id while the name loads or when unknown. -->
|
|
286
|
+
<span
|
|
287
|
+
v-if="isGroupPicker(field) && groupIdsOf(field).length"
|
|
288
|
+
class="cn-object-data-widget__groups">
|
|
289
|
+
<template v-for="(gid, gi) in groupIdsOf(field)" :key="gid">
|
|
290
|
+
<span v-if="gi > 0">, </span>
|
|
291
|
+
<CnGroupNameCell :gid="gid" />
|
|
292
|
+
</template>
|
|
293
|
+
</span>
|
|
294
|
+
<img v-else-if="isImageField(field) && rawOf(field)"
|
|
263
295
|
:src="rawOf(field)"
|
|
264
296
|
:alt="field.label"
|
|
265
297
|
class="cn-object-data-widget__image">
|
|
@@ -370,19 +402,22 @@
|
|
|
370
402
|
import axios from '@nextcloud/axios'
|
|
371
403
|
import { translate as t } from '@nextcloud/l10n'
|
|
372
404
|
import { generateUrl } from '@nextcloud/router'
|
|
373
|
-
import { NcActionButton, NcButton, NcCheckboxRadioSwitch, NcLoadingIcon, NcSelect, NcTextField } from '@nextcloud/vue'
|
|
405
|
+
import { NcActionButton, NcButton, NcCheckboxRadioSwitch, NcLoadingIcon, NcSelect, NcSelectUsers, NcTextField } from '@nextcloud/vue'
|
|
374
406
|
import Check from 'vue-material-design-icons/Check.vue'
|
|
375
407
|
import Close from 'vue-material-design-icons/Close.vue'
|
|
376
408
|
import ContentSaveOutline from 'vue-material-design-icons/ContentSaveOutline.vue'
|
|
377
409
|
import InformationOutline from 'vue-material-design-icons/InformationOutline.vue'
|
|
378
410
|
import Pencil from 'vue-material-design-icons/Pencil.vue'
|
|
411
|
+
import CnGroupNameCell from '../CnCellRenderer/CnGroupNameCell.vue'
|
|
379
412
|
import CnFormDialog from '../CnFormDialog/CnFormDialog.vue'
|
|
380
413
|
import { normalizeLook } from '../../composables/useLook.js'
|
|
381
414
|
import { useObjectStore } from '../../store/index.js'
|
|
415
|
+
import { groupDisplayName, loadGroupDisplayName, searchNextcloudGroups } from '../../utils/groupAutocomplete.js'
|
|
382
416
|
import { PANEL_ACTION_SINK } from '../../utils/panelActions.js'
|
|
383
417
|
import { resolveFilterTokens } from '../../utils/resolveFilterTokens.js'
|
|
384
418
|
import { fieldsFromSchema, formatValue } from '../../utils/schema.js'
|
|
385
419
|
import { schemaRefSlug } from '../../utils/schemaRefSlug.js'
|
|
420
|
+
import { resolveNextcloudUser, searchNextcloudUsers } from '../../utils/userAutocomplete.js'
|
|
386
421
|
import { CnIcon } from '../CnIcon/index.js'
|
|
387
422
|
import { CnObjectMetadataModal } from '../CnObjectMetadataModal/index.js'
|
|
388
423
|
import { CnWidgetWrapper } from '../CnWidgetWrapper/index.js'
|
|
@@ -427,6 +462,8 @@ export default {
|
|
|
427
462
|
NcLoadingIcon,
|
|
428
463
|
NcTextField,
|
|
429
464
|
NcSelect,
|
|
465
|
+
NcSelectUsers,
|
|
466
|
+
CnGroupNameCell,
|
|
430
467
|
NcCheckboxRadioSwitch,
|
|
431
468
|
NcActionButton,
|
|
432
469
|
CnWidgetWrapper,
|
|
@@ -806,6 +843,12 @@ export default {
|
|
|
806
843
|
relationOptions: {},
|
|
807
844
|
/** Whether relation picker options are being fetched. */
|
|
808
845
|
relationOptionsLoading: false,
|
|
846
|
+
/** Group and user picker options per field key ({ id, label }[]). */
|
|
847
|
+
personOptions: {},
|
|
848
|
+
/** Key of the group or user field whose options are loading, or null. */
|
|
849
|
+
personLoading: null,
|
|
850
|
+
/** Display names of users seen in a user picker, by uid. */
|
|
851
|
+
userLabels: {},
|
|
809
852
|
/** Whether the user expanded the widget to see every field. */
|
|
810
853
|
expanded: false,
|
|
811
854
|
/** Fields that fit the cell while collapsed; null when all fit. */
|
|
@@ -1719,6 +1762,11 @@ export default {
|
|
|
1719
1762
|
if (this.isSingleRelationField(field.key)) {
|
|
1720
1763
|
this.loadRelationOptions(field.key)
|
|
1721
1764
|
}
|
|
1765
|
+
// Group and user fields pick from Nextcloud's own directory.
|
|
1766
|
+
if (this.isPersonPicker(field)) {
|
|
1767
|
+
this.searchPeople(field, '')
|
|
1768
|
+
this.resolvePersonLabels(field, currentValue)
|
|
1769
|
+
}
|
|
1722
1770
|
|
|
1723
1771
|
this.$nextTick(() => {
|
|
1724
1772
|
// Focus the editor
|
|
@@ -1944,6 +1992,182 @@ export default {
|
|
|
1944
1992
|
return options.find((opt) => opt.id === val) || { id: val, label: String(val) }
|
|
1945
1993
|
},
|
|
1946
1994
|
|
|
1995
|
+
/**
|
|
1996
|
+
* Whether a field picks a Nextcloud group (widget `group` or
|
|
1997
|
+
* `group-multiselect`, from `referenceType: nextcloud-group`).
|
|
1998
|
+
*
|
|
1999
|
+
* @spec openspec/changes/nextcloud-group-surfaces/specs/data-display/spec.md#requirement-the-inline-editor-picks-a-nextcloud-group-or-user
|
|
2000
|
+
* @param {object} field Field descriptor.
|
|
2001
|
+
* @return {boolean}
|
|
2002
|
+
*/
|
|
2003
|
+
isGroupPicker(field) {
|
|
2004
|
+
return !!field && (field.widget === 'group' || field.widget === 'group-multiselect')
|
|
2005
|
+
},
|
|
2006
|
+
|
|
2007
|
+
/**
|
|
2008
|
+
* Whether a field picks a Nextcloud user (widget `user` or
|
|
2009
|
+
* `user-multiselect`, from `referenceType: nextcloud-user`).
|
|
2010
|
+
*
|
|
2011
|
+
* @spec openspec/changes/nextcloud-group-surfaces/specs/data-display/spec.md#requirement-the-inline-editor-picks-a-nextcloud-group-or-user
|
|
2012
|
+
* @param {object} field Field descriptor.
|
|
2013
|
+
* @return {boolean}
|
|
2014
|
+
*/
|
|
2015
|
+
isUserPicker(field) {
|
|
2016
|
+
return !!field && (field.widget === 'user' || field.widget === 'user-multiselect')
|
|
2017
|
+
},
|
|
2018
|
+
|
|
2019
|
+
/**
|
|
2020
|
+
* Whether a field picks a group or a user.
|
|
2021
|
+
*
|
|
2022
|
+
* @spec openspec/changes/nextcloud-group-surfaces/specs/data-display/spec.md#requirement-the-inline-editor-picks-a-nextcloud-group-or-user
|
|
2023
|
+
* @param {object} field Field descriptor.
|
|
2024
|
+
* @return {boolean}
|
|
2025
|
+
*/
|
|
2026
|
+
isPersonPicker(field) {
|
|
2027
|
+
return this.isGroupPicker(field) || this.isUserPicker(field)
|
|
2028
|
+
},
|
|
2029
|
+
|
|
2030
|
+
/**
|
|
2031
|
+
* Whether a group or user field holds a list.
|
|
2032
|
+
*
|
|
2033
|
+
* @spec openspec/changes/nextcloud-group-surfaces/specs/data-display/spec.md#requirement-the-inline-editor-picks-a-nextcloud-group-or-user
|
|
2034
|
+
* @param {object} field Field descriptor.
|
|
2035
|
+
* @return {boolean}
|
|
2036
|
+
*/
|
|
2037
|
+
isPersonMultiple(field) {
|
|
2038
|
+
return !!field && (field.widget === 'group-multiselect' || field.widget === 'user-multiselect')
|
|
2039
|
+
},
|
|
2040
|
+
|
|
2041
|
+
/**
|
|
2042
|
+
* The group ids a group field holds, for the display mode.
|
|
2043
|
+
*
|
|
2044
|
+
* @spec openspec/changes/nextcloud-group-surfaces/specs/data-display/spec.md#requirement-the-inline-editor-picks-a-nextcloud-group-or-user
|
|
2045
|
+
* @param {object} field Field descriptor.
|
|
2046
|
+
* @return {string[]}
|
|
2047
|
+
*/
|
|
2048
|
+
groupIdsOf(field) {
|
|
2049
|
+
const raw = this.rawOf(field)
|
|
2050
|
+
const list = Array.isArray(raw) ? raw : [raw]
|
|
2051
|
+
return list.filter((v) => typeof v === 'string' && v !== '')
|
|
2052
|
+
},
|
|
2053
|
+
|
|
2054
|
+
/**
|
|
2055
|
+
* The display name of a group id or uid, falling back to the id.
|
|
2056
|
+
*
|
|
2057
|
+
* @spec openspec/changes/nextcloud-group-surfaces/specs/data-display/spec.md#requirement-the-inline-editor-picks-a-nextcloud-group-or-user
|
|
2058
|
+
* @param {object} field Field descriptor.
|
|
2059
|
+
* @param {string} id The group id or uid.
|
|
2060
|
+
* @return {string}
|
|
2061
|
+
*/
|
|
2062
|
+
personLabel(field, id) {
|
|
2063
|
+
if (this.isGroupPicker(field)) {
|
|
2064
|
+
return groupDisplayName(id) || String(id)
|
|
2065
|
+
}
|
|
2066
|
+
return this.userLabels[id] || String(id)
|
|
2067
|
+
},
|
|
2068
|
+
|
|
2069
|
+
/**
|
|
2070
|
+
* Search Nextcloud groups or users for a picker. Fails soft (the
|
|
2071
|
+
* helpers return `[]`), so the picker stays usable with what it has.
|
|
2072
|
+
*
|
|
2073
|
+
* @spec openspec/changes/nextcloud-group-surfaces/specs/data-display/spec.md#requirement-the-inline-editor-picks-a-nextcloud-group-or-user
|
|
2074
|
+
* @param {object} field Field descriptor.
|
|
2075
|
+
* @param {string} query The search term.
|
|
2076
|
+
* @return {Promise<void>}
|
|
2077
|
+
*/
|
|
2078
|
+
async searchPeople(field, query) {
|
|
2079
|
+
this.personLoading = field.key
|
|
2080
|
+
try {
|
|
2081
|
+
const options = this.isGroupPicker(field)
|
|
2082
|
+
? await searchNextcloudGroups(query || '')
|
|
2083
|
+
: await searchNextcloudUsers(query || '')
|
|
2084
|
+
const list = Array.isArray(options) ? options : []
|
|
2085
|
+
if (this.isUserPicker(field)) {
|
|
2086
|
+
const labels = { ...this.userLabels }
|
|
2087
|
+
for (const opt of list) {
|
|
2088
|
+
labels[opt.id] = opt.label
|
|
2089
|
+
}
|
|
2090
|
+
this.userLabels = labels
|
|
2091
|
+
}
|
|
2092
|
+
this.personOptions = { ...this.personOptions, [field.key]: list }
|
|
2093
|
+
} finally {
|
|
2094
|
+
if (this.personLoading === field.key) {
|
|
2095
|
+
this.personLoading = null
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
},
|
|
2099
|
+
|
|
2100
|
+
/**
|
|
2101
|
+
* Look up the display names of the value a group or user field holds,
|
|
2102
|
+
* so the open editor shows names rather than ids.
|
|
2103
|
+
*
|
|
2104
|
+
* @spec openspec/changes/nextcloud-group-surfaces/specs/data-display/spec.md#requirement-the-inline-editor-picks-a-nextcloud-group-or-user
|
|
2105
|
+
* @param {object} field Field descriptor.
|
|
2106
|
+
* @param {unknown} value The held value (an id or a list of ids).
|
|
2107
|
+
* @return {Promise<void>}
|
|
2108
|
+
*/
|
|
2109
|
+
async resolvePersonLabels(field, value) {
|
|
2110
|
+
const ids = (Array.isArray(value) ? value : [value])
|
|
2111
|
+
.filter((v) => typeof v === 'string' && v !== '')
|
|
2112
|
+
if (this.isGroupPicker(field)) {
|
|
2113
|
+
await Promise.all(ids.map((gid) => loadGroupDisplayName(gid)))
|
|
2114
|
+
return
|
|
2115
|
+
}
|
|
2116
|
+
const unknown = ids.filter((uid) => !this.userLabels[uid])
|
|
2117
|
+
const options = await Promise.all(unknown.map((uid) => resolveNextcloudUser(uid)))
|
|
2118
|
+
if (options.length > 0) {
|
|
2119
|
+
const labels = { ...this.userLabels }
|
|
2120
|
+
for (const opt of options) {
|
|
2121
|
+
if (opt && opt.id) {
|
|
2122
|
+
labels[opt.id] = opt.label || opt.id
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
this.userLabels = labels
|
|
2126
|
+
}
|
|
2127
|
+
},
|
|
2128
|
+
|
|
2129
|
+
/**
|
|
2130
|
+
* The selected option(s) of a group or user picker.
|
|
2131
|
+
*
|
|
2132
|
+
* @spec openspec/changes/nextcloud-group-surfaces/specs/data-display/spec.md#requirement-the-inline-editor-picks-a-nextcloud-group-or-user
|
|
2133
|
+
* @param {object} field Field descriptor.
|
|
2134
|
+
* @return {object|object[]|null}
|
|
2135
|
+
*/
|
|
2136
|
+
personSelected(field) {
|
|
2137
|
+
const toOption = (id) => {
|
|
2138
|
+
const label = this.personLabel(field, id)
|
|
2139
|
+
return { id, label, displayName: label }
|
|
2140
|
+
}
|
|
2141
|
+
const v = this.editData[field.key]
|
|
2142
|
+
if (this.isPersonMultiple(field)) {
|
|
2143
|
+
return (Array.isArray(v) ? v : [])
|
|
2144
|
+
.filter((id) => typeof id === 'string' && id !== '')
|
|
2145
|
+
.map(toOption)
|
|
2146
|
+
}
|
|
2147
|
+
return (typeof v === 'string' && v !== '') ? toOption(v) : null
|
|
2148
|
+
},
|
|
2149
|
+
|
|
2150
|
+
/**
|
|
2151
|
+
* Store a group or user choice as the id (or list of ids).
|
|
2152
|
+
*
|
|
2153
|
+
* @spec openspec/changes/nextcloud-group-surfaces/specs/data-display/spec.md#requirement-the-inline-editor-picks-a-nextcloud-group-or-user
|
|
2154
|
+
* @param {object} field Field descriptor.
|
|
2155
|
+
* @param {object|object[]|null} selected The chosen option(s).
|
|
2156
|
+
*/
|
|
2157
|
+
onPersonChange(field, selected) {
|
|
2158
|
+
if (this.isPersonMultiple(field)) {
|
|
2159
|
+
const ids = (Array.isArray(selected) ? selected : [])
|
|
2160
|
+
.map((opt) => (opt && typeof opt === 'object' ? opt.id : opt))
|
|
2161
|
+
.filter((id) => typeof id === 'string' && id !== '')
|
|
2162
|
+
this.updateField(field.key, ids)
|
|
2163
|
+
return
|
|
2164
|
+
}
|
|
2165
|
+
if (selected && this.isUserPicker(field) && selected.id) {
|
|
2166
|
+
this.userLabels = { ...this.userLabels, [selected.id]: selected.label || selected.id }
|
|
2167
|
+
}
|
|
2168
|
+
this.updateField(field.key, selected && selected.id ? selected.id : null)
|
|
2169
|
+
},
|
|
2170
|
+
|
|
1947
2171
|
onSelectChange(field, option) {
|
|
1948
2172
|
this.updateField(field.key, option ? option.id : null)
|
|
1949
2173
|
},
|
|
@@ -357,7 +357,8 @@ export default {
|
|
|
357
357
|
* Host translate function provided by CnAppRoot as
|
|
358
358
|
* `cnTranslate: this.translate` (bound to the host app's id). The
|
|
359
359
|
* manifest-authored `content.emptyText` is run through it for this
|
|
360
|
-
* component's OWN empty state, and so
|
|
360
|
+
* component's OWN empty state, and so are `content.prompt` and
|
|
361
|
+
* `content.addLabel`. Defaults to an identity function so
|
|
361
362
|
* an untranslated key renders as itself.
|
|
362
363
|
*/
|
|
363
364
|
cnTranslate: { default: () => (key) => key },
|
|
@@ -1092,9 +1093,24 @@ export default {
|
|
|
1092
1093
|
return (c.formFieldOverrides && typeof c.formFieldOverrides === 'object') ? c.formFieldOverrides : {}
|
|
1093
1094
|
},
|
|
1094
1095
|
|
|
1095
|
-
/**
|
|
1096
|
+
/**
|
|
1097
|
+
* The Add label: `content.addLabel` through the app's translate
|
|
1098
|
+
* function, else the library's "Add".
|
|
1099
|
+
*
|
|
1100
|
+
* @spec openspec/changes/r4-object-lock-url-and-credentials-copy/specs/object-list-add-label/spec.md
|
|
1101
|
+
* @return {string}
|
|
1102
|
+
*/
|
|
1096
1103
|
addLabel() {
|
|
1097
|
-
|
|
1104
|
+
// `content.addLabel` is manifest copy in the APP's catalogue, so it
|
|
1105
|
+
// goes through the host translate function like `emptyText` and
|
|
1106
|
+
// `prompt`. It used to print raw, so a Dutch user read the English
|
|
1107
|
+
// manifest string. A label an app already translated has no key
|
|
1108
|
+
// of its own in the catalogue and comes back unchanged.
|
|
1109
|
+
if (this.content.addLabel) {
|
|
1110
|
+
const fn = typeof this.cnTranslate === 'function' ? this.cnTranslate : (k) => k
|
|
1111
|
+
return fn(this.content.addLabel)
|
|
1112
|
+
}
|
|
1113
|
+
return t('nextcloud-vue', 'Add')
|
|
1098
1114
|
},
|
|
1099
1115
|
|
|
1100
1116
|
/** Stable signature of the query so the watcher only refetches on real change. */
|