@conduction/nextcloud-vue 0.1.0-beta.6 → 0.1.0-beta.8
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/nextcloud-vue.cjs.js +13575 -2374
- package/dist/nextcloud-vue.cjs.js.map +1 -1
- package/dist/nextcloud-vue.css +1238 -270
- package/dist/nextcloud-vue.esm.js +13517 -2336
- package/dist/nextcloud-vue.esm.js.map +1 -1
- package/package.json +11 -7
- package/src/components/CnActionsBar/CnActionsBar.vue +20 -2
- package/src/components/CnAdvancedFormDialog/CnAdvancedFormDialog.vue +1 -11
- package/src/components/CnAdvancedFormDialog/CnPropertiesTab.vue +5 -1
- package/src/components/CnAdvancedFormDialog/CnPropertyValueCell.vue +1 -1
- package/src/components/CnCard/CnCard.vue +415 -0
- package/src/components/CnCard/index.js +1 -0
- package/src/components/CnCardGrid/CnCardGrid.vue +20 -20
- package/src/components/CnChartWidget/CnChartWidget.vue +3 -1
- package/src/components/CnCopyDialog/CnCopyDialog.vue +7 -1
- package/src/components/CnDashboardGrid/CnDashboardGrid.vue +4 -0
- package/src/components/CnDashboardPage/CnDashboardPage.vue +2 -0
- package/src/components/CnDataTable/CnDataTable.vue +6 -2
- package/src/components/CnDeleteDialog/CnDeleteDialog.vue +7 -1
- package/src/components/CnDetailCard/CnDetailCard.vue +12 -1
- package/src/components/CnDetailGrid/CnDetailGrid.vue +254 -0
- package/src/components/CnDetailGrid/index.js +1 -0
- package/src/components/CnDetailPage/CnDetailPage.vue +157 -11
- package/src/components/CnFacetSidebar/CnFacetSidebar.vue +3 -1
- package/src/components/CnFormDialog/CnFormDialog.vue +934 -920
- package/src/components/CnIcon/CnIcon.vue +1 -1
- package/src/components/CnIndexPage/CnIndexPage.vue +63 -9
- package/src/components/CnIndexSidebar/CnIndexSidebar.vue +37 -9
- package/src/components/CnInfoWidget/CnInfoWidget.vue +219 -0
- package/src/components/CnInfoWidget/index.js +1 -0
- package/src/components/CnJsonViewer/CnJsonViewer.vue +283 -0
- package/src/components/CnJsonViewer/index.js +1 -0
- package/src/components/CnKpiGrid/CnKpiGrid.vue +5 -1
- package/src/components/CnMassCopyDialog/CnMassCopyDialog.vue +7 -1
- package/src/components/CnMassDeleteDialog/CnMassDeleteDialog.vue +7 -1
- package/src/components/CnMassExportDialog/CnMassExportDialog.vue +1 -1
- package/src/components/CnMassImportDialog/CnMassImportDialog.vue +1 -1
- package/src/components/CnObjectCard/CnObjectCard.vue +1 -1
- package/src/components/CnObjectSidebar/CnAuditTrailTab.vue +368 -0
- package/src/components/CnObjectSidebar/CnFilesTab.vue +286 -0
- package/src/components/CnObjectSidebar/CnNotesTab.vue +249 -0
- package/src/components/CnObjectSidebar/CnObjectSidebar.vue +45 -668
- package/src/components/CnObjectSidebar/CnTagsTab.vue +258 -0
- package/src/components/CnObjectSidebar/CnTasksTab.vue +482 -0
- package/src/components/CnObjectSidebar/index.js +5 -0
- package/src/components/CnProgressBar/CnProgressBar.vue +262 -0
- package/src/components/CnProgressBar/index.js +1 -0
- package/src/components/CnSchemaFormDialog/CnSchemaPropertiesTab.vue +1 -1
- package/src/components/CnStatsBlock/CnStatsBlock.vue +27 -11
- package/src/components/CnStatsPanel/CnStatsPanel.vue +320 -0
- package/src/components/CnStatsPanel/index.js +1 -0
- package/src/components/CnStatusBadge/CnStatusBadge.vue +15 -2
- package/src/components/CnTabbedFormDialog/CnTabbedFormDialog.vue +5 -1
- package/src/components/CnTableWidget/CnTableWidget.vue +332 -0
- package/src/components/CnTableWidget/index.js +1 -0
- package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +36 -1
- package/src/components/index.js +11 -0
- package/src/composables/useDashboardView.js +58 -12
- package/src/composables/useDetailView.js +3 -2
- package/src/composables/useListView.js +7 -6
- package/src/composables/useSubResource.js +3 -3
- package/src/css/badge.css +32 -0
- package/src/css/card.css +1 -0
- package/src/css/detail-page.css +74 -7
- package/src/index.js +16 -0
- package/src/mixins/gridLayout.js +118 -0
- package/src/store/createCrudStore.js +360 -0
- package/src/store/createSubResourcePlugin.js +5 -15
- package/src/store/index.js +1 -0
- package/src/store/plugins/auditTrails.js +346 -6
- package/src/store/plugins/lifecycle.js +4 -4
- package/src/store/plugins/registerMapping.js +18 -8
- package/src/store/plugins/relations.js +1 -1
- package/src/store/plugins/search.js +21 -8
- package/src/store/useObjectStore.js +30 -36
- package/src/utils/getTheme.js +9 -0
- package/src/utils/headers.js +13 -3
- package/src/utils/index.js +1 -0
- package/src/utils/schema.js +3 -3
- package/src/utils/widgetVisibility.js +162 -0
- package/src/components/CnObjectCard/eslint-setup.md +0 -235
- package/src/components/CnObjectCard/package.json-or.json +0 -132
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { defineStore } from 'pinia'
|
|
2
|
-
import { buildHeaders, buildQueryString, prefixUrl } from '../utils/headers.js'
|
|
2
|
+
import { buildHeaders, buildQueryString, prefixUrl, capitalize } from '../utils/headers.js'
|
|
3
3
|
import { parseResponseError, networkError, genericError } from '../utils/errors.js'
|
|
4
4
|
import { extractId } from '../utils/id.js'
|
|
5
5
|
|
|
@@ -26,16 +26,6 @@ import { extractId } from '../utils/id.js'
|
|
|
26
26
|
const DEFAULT_STORE_ID = 'conduction-objects'
|
|
27
27
|
const DEFAULT_BASE_URL = '/apps/openregister/api/objects'
|
|
28
28
|
|
|
29
|
-
/**
|
|
30
|
-
* Capitalize the first letter of a string.
|
|
31
|
-
*
|
|
32
|
-
* @param {string} str Input string
|
|
33
|
-
* @return {string} Capitalized string
|
|
34
|
-
*/
|
|
35
|
-
function capitalize(str) {
|
|
36
|
-
return str.charAt(0).toUpperCase() + str.slice(1)
|
|
37
|
-
}
|
|
38
|
-
|
|
39
29
|
/**
|
|
40
30
|
* Merge plugin state factories into a single state object.
|
|
41
31
|
*
|
|
@@ -123,49 +113,49 @@ function baseState(baseUrl = DEFAULT_BASE_URL) {
|
|
|
123
113
|
const baseGetters = {
|
|
124
114
|
/**
|
|
125
115
|
* Get all registered object type slugs.
|
|
126
|
-
* @param state
|
|
116
|
+
* @param {object} state Pinia state
|
|
127
117
|
* @return {string[]}
|
|
128
118
|
*/
|
|
129
119
|
objectTypes: (state) => Object.keys(state.objectTypeRegistry),
|
|
130
120
|
|
|
131
121
|
/**
|
|
132
122
|
* Get the collection array for a type.
|
|
133
|
-
* @param state
|
|
123
|
+
* @param {object} state Pinia state
|
|
134
124
|
* @return {Function} (type: string) => Array
|
|
135
125
|
*/
|
|
136
126
|
getCollection: (state) => (type) => state.collections[type] || [],
|
|
137
127
|
|
|
138
128
|
/**
|
|
139
129
|
* Get a single cached object by type and ID.
|
|
140
|
-
* @param state
|
|
130
|
+
* @param {object} state Pinia state
|
|
141
131
|
* @return {Function} (type: string, id: string) => object|null
|
|
142
132
|
*/
|
|
143
133
|
getObject: (state) => (type, id) => state.objects[type]?.[id] || null,
|
|
144
134
|
|
|
145
135
|
/**
|
|
146
136
|
* Alias for getObject — check cache without fetching.
|
|
147
|
-
* @param state
|
|
137
|
+
* @param {object} state Pinia state
|
|
148
138
|
* @return {Function} (type: string, id: string) => object|null
|
|
149
139
|
*/
|
|
150
140
|
getCachedObject: (state) => (type, id) => state.objects[type]?.[id] || null,
|
|
151
141
|
|
|
152
142
|
/**
|
|
153
143
|
* Check if a type is currently loading.
|
|
154
|
-
* @param state
|
|
144
|
+
* @param {object} state Pinia state
|
|
155
145
|
* @return {Function} (type: string) => boolean
|
|
156
146
|
*/
|
|
157
147
|
isLoading: (state) => (type) => state.loading[type] || false,
|
|
158
148
|
|
|
159
149
|
/**
|
|
160
150
|
* Get the current error for a type.
|
|
161
|
-
* @param state
|
|
151
|
+
* @param {object} state Pinia state
|
|
162
152
|
* @return {Function} (type: string) => ApiError|null
|
|
163
153
|
*/
|
|
164
154
|
getError: (state) => (type) => state.errors[type] || null,
|
|
165
155
|
|
|
166
156
|
/**
|
|
167
157
|
* Get pagination state for a type.
|
|
168
|
-
* @param state
|
|
158
|
+
* @param {object} state Pinia state
|
|
169
159
|
* @return {Function} (type: string) => {total, page, pages, limit}
|
|
170
160
|
*/
|
|
171
161
|
getPagination: (state) => (type) =>
|
|
@@ -173,28 +163,28 @@ const baseGetters = {
|
|
|
173
163
|
|
|
174
164
|
/**
|
|
175
165
|
* Get the current search term for a type.
|
|
176
|
-
* @param state
|
|
166
|
+
* @param {object} state Pinia state
|
|
177
167
|
* @return {Function} (type: string) => string
|
|
178
168
|
*/
|
|
179
169
|
getSearchTerm: (state) => (type) => state.searchTerms[type] || '',
|
|
180
170
|
|
|
181
171
|
/**
|
|
182
172
|
* Get a cached schema for a type.
|
|
183
|
-
* @param state
|
|
173
|
+
* @param {object} state Pinia state
|
|
184
174
|
* @return {Function} (type: string) => object|null
|
|
185
175
|
*/
|
|
186
176
|
getSchema: (state) => (type) => state.schemas[type] || null,
|
|
187
177
|
|
|
188
178
|
/**
|
|
189
179
|
* Get a cached register for a type.
|
|
190
|
-
* @param state
|
|
180
|
+
* @param {object} state Pinia state
|
|
191
181
|
* @return {Function} (type: string) => object|null
|
|
192
182
|
*/
|
|
193
183
|
getRegister: (state) => (type) => state.registers[type] || null,
|
|
194
184
|
|
|
195
185
|
/**
|
|
196
186
|
* Get facet data for a type (CnIndexSidebar-compatible format).
|
|
197
|
-
* @param state
|
|
187
|
+
* @param {object} state Pinia state
|
|
198
188
|
* @return {Function} (type: string) => object
|
|
199
189
|
*/
|
|
200
190
|
getFacets: (state) => (type) => state.facets[type] || {},
|
|
@@ -256,16 +246,20 @@ const baseActions = {
|
|
|
256
246
|
* @param {string} slug The type slug to unregister
|
|
257
247
|
*/
|
|
258
248
|
unregisterObjectType(slug) {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
249
|
+
const omit = (obj, key) => {
|
|
250
|
+
const { [key]: _, ...rest } = obj
|
|
251
|
+
return rest
|
|
252
|
+
}
|
|
253
|
+
this.objectTypeRegistry = omit(this.objectTypeRegistry, slug)
|
|
254
|
+
this.collections = omit(this.collections, slug)
|
|
255
|
+
this.objects = omit(this.objects, slug)
|
|
256
|
+
this.loading = omit(this.loading, slug)
|
|
257
|
+
this.errors = omit(this.errors, slug)
|
|
258
|
+
this.pagination = omit(this.pagination, slug)
|
|
259
|
+
this.searchTerms = omit(this.searchTerms, slug)
|
|
260
|
+
this.schemas = omit(this.schemas, slug)
|
|
261
|
+
this.registers = omit(this.registers, slug)
|
|
262
|
+
this.facets = omit(this.facets, slug)
|
|
269
263
|
},
|
|
270
264
|
|
|
271
265
|
/**
|
|
@@ -343,7 +337,7 @@ const baseActions = {
|
|
|
343
337
|
|
|
344
338
|
try {
|
|
345
339
|
const response = await fetch(
|
|
346
|
-
`/apps/openregister/api/schemas/${config.schema}
|
|
340
|
+
prefixUrl(`/apps/openregister/api/schemas/${config.schema}`),
|
|
347
341
|
{ method: 'GET', headers: buildHeaders() },
|
|
348
342
|
)
|
|
349
343
|
|
|
@@ -373,7 +367,7 @@ const baseActions = {
|
|
|
373
367
|
|
|
374
368
|
try {
|
|
375
369
|
const response = await fetch(
|
|
376
|
-
`/apps/openregister/api/registers/${config.register}
|
|
370
|
+
prefixUrl(`/apps/openregister/api/registers/${config.register}`),
|
|
377
371
|
{ method: 'GET', headers: buildHeaders() },
|
|
378
372
|
)
|
|
379
373
|
|
|
@@ -693,7 +687,7 @@ const baseActions = {
|
|
|
693
687
|
*
|
|
694
688
|
* @param {string} type The registered type slug
|
|
695
689
|
* @param {string[]} ids Array of object IDs to resolve
|
|
696
|
-
* @return {Promise<
|
|
690
|
+
* @return {Promise<{[key: string]: object}>} Map of id -> object
|
|
697
691
|
*/
|
|
698
692
|
async resolveReferences(type, ids) {
|
|
699
693
|
if (!ids || ids.length === 0) return {}
|
|
@@ -795,7 +789,7 @@ function defineObjectStore(storeId, plugins = [], baseUrl = DEFAULT_BASE_URL) {
|
|
|
795
789
|
* import { useObjectStore } from '@conduction/nextcloud-vue'
|
|
796
790
|
* const store = useObjectStore()
|
|
797
791
|
*/
|
|
798
|
-
export const useObjectStore = defineObjectStore(DEFAULT_STORE_ID)
|
|
792
|
+
export const useObjectStore = defineObjectStore(DEFAULT_STORE_ID, [], prefixUrl(DEFAULT_BASE_URL))
|
|
799
793
|
|
|
800
794
|
/**
|
|
801
795
|
* Factory function to create an object store with a custom Pinia store ID
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export const getTheme = () => {
|
|
2
|
+
if (document.body.hasAttribute('data-theme-light')) {
|
|
3
|
+
return 'light'
|
|
4
|
+
}
|
|
5
|
+
if (document.body.hasAttribute('data-theme-default')) {
|
|
6
|
+
return window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark'
|
|
7
|
+
}
|
|
8
|
+
return 'dark'
|
|
9
|
+
}
|
package/src/utils/headers.js
CHANGED
|
@@ -27,7 +27,7 @@ export function prefixUrl(path) {
|
|
|
27
27
|
*/
|
|
28
28
|
export function buildHeaders(contentType = 'application/json') {
|
|
29
29
|
const headers = {
|
|
30
|
-
requesttoken: OC.requestToken,
|
|
30
|
+
requesttoken: typeof OC !== 'undefined' ? OC.requestToken : '',
|
|
31
31
|
'OCS-APIREQUEST': 'true',
|
|
32
32
|
}
|
|
33
33
|
if (contentType) {
|
|
@@ -36,11 +36,21 @@ export function buildHeaders(contentType = 'application/json') {
|
|
|
36
36
|
return headers
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Capitalize the first letter of a string.
|
|
41
|
+
*
|
|
42
|
+
* @param {string} str Input string
|
|
43
|
+
* @return {string} Capitalized string
|
|
44
|
+
*/
|
|
45
|
+
export function capitalize(str) {
|
|
46
|
+
return str.charAt(0).toUpperCase() + str.slice(1)
|
|
47
|
+
}
|
|
48
|
+
|
|
39
49
|
/**
|
|
40
50
|
* Build a query string from a params object.
|
|
41
51
|
*
|
|
42
|
-
* Handles
|
|
43
|
-
*
|
|
52
|
+
* Handles _order serialization (JSON.stringify for objects) and skips
|
|
53
|
+
* null/undefined/empty values.
|
|
44
54
|
*
|
|
45
55
|
* @param {object} params Key-value pairs for query parameters
|
|
46
56
|
* @return {string} Query string including leading '?' or empty string
|
package/src/utils/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { buildHeaders, buildQueryString } from './headers.js'
|
|
2
2
|
export { parseResponseError, networkError, genericError } from './errors.js'
|
|
3
3
|
export { columnsFromSchema, formatValue, filtersFromSchema, fieldsFromSchema } from './schema.js'
|
|
4
|
+
export { filterWidgetsByVisibility, isWidgetVisible, getCurrentUserId, getCurrentUserGroups, resetVisibilityCache } from './widgetVisibility.js'
|
package/src/utils/schema.js
CHANGED
|
@@ -116,7 +116,7 @@ export function columnsFromSchema(schema, options = {}) {
|
|
|
116
116
|
* @param {*} value The raw value
|
|
117
117
|
* @param {object} [property] The schema property definition `{ type, format, enum, items }`
|
|
118
118
|
* @param {object} [options] Formatting options
|
|
119
|
-
* @param {number} [options.truncate
|
|
119
|
+
* @param {number} [options.truncate] Maximum string length before truncation
|
|
120
120
|
* @return {string} Formatted display string
|
|
121
121
|
*/
|
|
122
122
|
export function formatValue(value, property = {}, options = {}) {
|
|
@@ -301,7 +301,7 @@ function resolveWidget(prop) {
|
|
|
301
301
|
* @param {string[]} [options.exclude] Property keys to exclude
|
|
302
302
|
* @param {string[]} [options.include] Property keys to include (whitelist mode)
|
|
303
303
|
* @param {object} [options.overrides] Per-key field overrides, e.g. `{ status: { widget: 'select' } }`
|
|
304
|
-
* @param {boolean} [options.includeReadOnly
|
|
304
|
+
* @param {boolean} [options.includeReadOnly] Whether to include readOnly properties
|
|
305
305
|
* @return {Array<{key: string, label: string, description: string, type: string, format: string|null, widget: string, required: boolean, readOnly: boolean, default: *, enum: Array|null, items: object|null, validation: object, order: number}>}
|
|
306
306
|
*/
|
|
307
307
|
export function fieldsFromSchema(schema, options = {}) {
|
|
@@ -383,7 +383,7 @@ export function filtersFromSchema(schema) {
|
|
|
383
383
|
}
|
|
384
384
|
|
|
385
385
|
return Object.entries(schema.properties)
|
|
386
|
-
.filter(([
|
|
386
|
+
.filter(([, prop]) => {
|
|
387
387
|
if (prop.facetable !== true) return false
|
|
388
388
|
return true
|
|
389
389
|
})
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import axios from '@nextcloud/axios'
|
|
2
|
+
import { generateOcsUrl } from '@nextcloud/router'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Cached user groups — fetched once per session and reused.
|
|
6
|
+
* @type {{ userId: string|null, groups: string[]|null, promise: Promise|null }}
|
|
7
|
+
*/
|
|
8
|
+
const _cache = {
|
|
9
|
+
userId: null,
|
|
10
|
+
groups: null,
|
|
11
|
+
promise: null,
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Get the current Nextcloud user ID from OC.currentUser.
|
|
16
|
+
*
|
|
17
|
+
* @return {string|null} The current user ID, or null if not logged in
|
|
18
|
+
*/
|
|
19
|
+
export function getCurrentUserId() {
|
|
20
|
+
return window.OC?.currentUser?.uid
|
|
21
|
+
|| window.OC?.currentUser
|
|
22
|
+
|| null
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Fetch the current user's Nextcloud groups. Results are cached so the
|
|
27
|
+
* OCS API is only called once per page load.
|
|
28
|
+
*
|
|
29
|
+
* @return {Promise<string[]>} Array of group IDs the current user belongs to
|
|
30
|
+
*/
|
|
31
|
+
export async function getCurrentUserGroups() {
|
|
32
|
+
const userId = getCurrentUserId()
|
|
33
|
+
if (!userId) {
|
|
34
|
+
return []
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Return cached result if we already fetched for this user
|
|
38
|
+
if (_cache.userId === userId && _cache.groups !== null) {
|
|
39
|
+
return _cache.groups
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// If a fetch is already in progress for this user, await it
|
|
43
|
+
if (_cache.userId === userId && _cache.promise) {
|
|
44
|
+
return _cache.promise
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
_cache.userId = userId
|
|
48
|
+
_cache.promise = _fetchGroups(userId)
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
_cache.groups = await _cache.promise
|
|
52
|
+
} catch {
|
|
53
|
+
_cache.groups = []
|
|
54
|
+
} finally {
|
|
55
|
+
_cache.promise = null
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return _cache.groups
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Internal: fetch groups from OCS API.
|
|
63
|
+
*
|
|
64
|
+
* @param {string} userId The user ID to look up
|
|
65
|
+
* @return {Promise<string[]>} Array of group IDs
|
|
66
|
+
*/
|
|
67
|
+
async function _fetchGroups(userId) {
|
|
68
|
+
try {
|
|
69
|
+
const url = generateOcsUrl('/cloud/users/{userId}/groups', { userId })
|
|
70
|
+
const response = await axios.get(url)
|
|
71
|
+
const groups = response.data?.ocs?.data?.groups || []
|
|
72
|
+
return groups
|
|
73
|
+
} catch (error) {
|
|
74
|
+
console.error('[widgetVisibility] Failed to fetch user groups:', error)
|
|
75
|
+
return []
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Check whether a single widget is visible to the current user based on
|
|
81
|
+
* its `visibility` configuration.
|
|
82
|
+
*
|
|
83
|
+
* Visibility rules:
|
|
84
|
+
* - No `visibility` property -> visible to everyone
|
|
85
|
+
* - `visibility.users` contains the current user ID -> visible
|
|
86
|
+
* - `visibility.groups` overlaps with the user's groups -> visible
|
|
87
|
+
* - Both `users` and `groups` are specified -> either match grants access (OR logic)
|
|
88
|
+
* - Both are empty arrays -> visible to everyone
|
|
89
|
+
*
|
|
90
|
+
* @param {object} widget Widget definition object
|
|
91
|
+
* @param {object} [widget.visibility] Optional visibility configuration
|
|
92
|
+
* @param {string[]} [widget.visibility.users] User IDs who can see this widget
|
|
93
|
+
* @param {string[]} [widget.visibility.groups] Group names who can see this widget
|
|
94
|
+
* @param {string|null} userId Current user ID
|
|
95
|
+
* @param {string[]} userGroups Current user's group memberships
|
|
96
|
+
* @return {boolean} Whether the widget should be visible
|
|
97
|
+
*/
|
|
98
|
+
export function isWidgetVisible(widget, userId, userGroups) {
|
|
99
|
+
const visibility = widget?.visibility
|
|
100
|
+
if (!visibility) {
|
|
101
|
+
return true
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const allowedUsers = visibility.users || []
|
|
105
|
+
const allowedGroups = visibility.groups || []
|
|
106
|
+
|
|
107
|
+
// If both are empty, visible to everyone
|
|
108
|
+
if (allowedUsers.length === 0 && allowedGroups.length === 0) {
|
|
109
|
+
return true
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Check user match
|
|
113
|
+
if (allowedUsers.length > 0 && userId && allowedUsers.includes(userId)) {
|
|
114
|
+
return true
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Check group match
|
|
118
|
+
if (allowedGroups.length > 0 && userGroups.length > 0) {
|
|
119
|
+
const hasGroupMatch = allowedGroups.some(group => userGroups.includes(group))
|
|
120
|
+
if (hasGroupMatch) {
|
|
121
|
+
return true
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return false
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Filter an array of widget definitions by visibility for the current user.
|
|
130
|
+
*
|
|
131
|
+
* This is an async function because it may need to fetch the user's groups
|
|
132
|
+
* from the OCS API (cached after first call).
|
|
133
|
+
*
|
|
134
|
+
* @param {Array} widgets Array of widget definition objects
|
|
135
|
+
* @return {Promise<Array>} Filtered array of visible widgets
|
|
136
|
+
*/
|
|
137
|
+
export async function filterWidgetsByVisibility(widgets) {
|
|
138
|
+
if (!widgets || widgets.length === 0) {
|
|
139
|
+
return []
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Quick path: if no widgets have visibility config, return all
|
|
143
|
+
const hasVisibilityConfig = widgets.some(w => w.visibility)
|
|
144
|
+
if (!hasVisibilityConfig) {
|
|
145
|
+
return widgets
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const userId = getCurrentUserId()
|
|
149
|
+
const userGroups = await getCurrentUserGroups()
|
|
150
|
+
|
|
151
|
+
return widgets.filter(widget => isWidgetVisible(widget, userId, userGroups))
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Reset the internal groups cache. Useful for testing or when user
|
|
156
|
+
* context changes.
|
|
157
|
+
*/
|
|
158
|
+
export function resetVisibilityCache() {
|
|
159
|
+
_cache.userId = null
|
|
160
|
+
_cache.groups = null
|
|
161
|
+
_cache.promise = null
|
|
162
|
+
}
|
|
@@ -1,235 +0,0 @@
|
|
|
1
|
-
# ESLint Setup — How Red Lines and Auto-fix Work
|
|
2
|
-
|
|
3
|
-
Exact files, exact settings, no guessing. This is the working setup in OpenRegister.
|
|
4
|
-
|
|
5
|
-
---
|
|
6
|
-
|
|
7
|
-
## Required VSCode Extension
|
|
8
|
-
|
|
9
|
-
**`dbaeumer.vscode-eslint`** (version 3.0.24)
|
|
10
|
-
|
|
11
|
-
This is the only extension involved in linting and formatting `.vue` and `.js` files. It does two things:
|
|
12
|
-
1. Runs ESLint in the background and draws red/yellow underlines (diagnostics)
|
|
13
|
-
2. Acts as the formatter — fixes the code when you save
|
|
14
|
-
|
|
15
|
-
---
|
|
16
|
-
|
|
17
|
-
## The 6 Files That Matter
|
|
18
|
-
|
|
19
|
-
### 1. `.vscode/settings.json`
|
|
20
|
-
|
|
21
|
-
Tells VSCode to use ESLint as the formatter for all files (including `.vue`) and to run it on save.
|
|
22
|
-
|
|
23
|
-
```json
|
|
24
|
-
{
|
|
25
|
-
"editor.defaultFormatter": "dbaeumer.vscode-eslint",
|
|
26
|
-
"editor.formatOnSave": true,
|
|
27
|
-
"eslint.format.enable": true,
|
|
28
|
-
|
|
29
|
-
"[javascript]": {
|
|
30
|
-
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
|
|
31
|
-
},
|
|
32
|
-
"[css]": {
|
|
33
|
-
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
|
34
|
-
},
|
|
35
|
-
"[php]": {
|
|
36
|
-
"editor.defaultFormatter": "DEVSENSE.phptools-vscode"
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
- No `[vue]` block exists. Vue falls back to the top-level `editor.defaultFormatter` = ESLint.
|
|
42
|
-
- `eslint.format.enable: true` is what makes ESLint able to **fix** on save, not just show errors.
|
|
43
|
-
- `editor.formatOnSave: true` triggers that fix every Ctrl+S.
|
|
44
|
-
|
|
45
|
-
---
|
|
46
|
-
|
|
47
|
-
### 2. `eslint.config.js`
|
|
48
|
-
|
|
49
|
-
The ESLint flat config entry point. The extension finds this automatically at the project root.
|
|
50
|
-
|
|
51
|
-
```js
|
|
52
|
-
const { defineConfig } = require('@eslint/config-helpers')
|
|
53
|
-
const js = require('@eslint/js')
|
|
54
|
-
const { FlatCompat } = require('@eslint/eslintrc')
|
|
55
|
-
|
|
56
|
-
const compat = new FlatCompat({
|
|
57
|
-
baseDirectory: __dirname,
|
|
58
|
-
recommendedConfig: js.configs.recommended,
|
|
59
|
-
allConfig: js.configs.all,
|
|
60
|
-
})
|
|
61
|
-
|
|
62
|
-
module.exports = defineConfig([{
|
|
63
|
-
extends: compat.extends('@nextcloud'),
|
|
64
|
-
|
|
65
|
-
settings: {
|
|
66
|
-
'import/resolver': {
|
|
67
|
-
alias: {
|
|
68
|
-
map: [
|
|
69
|
-
['@', './src'],
|
|
70
|
-
['@floating-ui/dom-actual', './node_modules/@floating-ui/dom'],
|
|
71
|
-
['@conduction/nextcloud-vue', '../nextcloud-vue/src'],
|
|
72
|
-
],
|
|
73
|
-
extensions: ['.js', '.ts', '.vue', '.json', '.css'],
|
|
74
|
-
},
|
|
75
|
-
},
|
|
76
|
-
},
|
|
77
|
-
|
|
78
|
-
rules: {
|
|
79
|
-
'no-unused-vars': ['error', { varsIgnorePattern: '^(t|n)$', argsIgnorePattern: '^_' }],
|
|
80
|
-
'jsdoc/require-jsdoc': 'off',
|
|
81
|
-
'vue/first-attribute-linebreak': 'off',
|
|
82
|
-
'@typescript-eslint/no-explicit-any': 'off',
|
|
83
|
-
'n/no-missing-import': 'off',
|
|
84
|
-
'import/namespace': 'off',
|
|
85
|
-
'import/default': 'off',
|
|
86
|
-
'import/no-named-as-default': 'off',
|
|
87
|
-
'import/no-named-as-default-member': 'off',
|
|
88
|
-
},
|
|
89
|
-
}])
|
|
90
|
-
```
|
|
91
|
-
|
|
92
|
-
- Uses **flat config** format (ESLint 8.57.0).
|
|
93
|
-
- Uses `FlatCompat` to bridge `@nextcloud/eslint-config` which is still in legacy format.
|
|
94
|
-
- Works with `require()` because `package.json` has **no** `"type": "module"`.
|
|
95
|
-
|
|
96
|
-
---
|
|
97
|
-
|
|
98
|
-
### 3. `.babelrc`
|
|
99
|
-
|
|
100
|
-
The Babel config used by `@babel/eslint-parser` to parse `.js` and `.vue` `<script>` blocks. This is a **project-root** `.babelrc` file, not a `babel.config.js`.
|
|
101
|
-
|
|
102
|
-
```json
|
|
103
|
-
{
|
|
104
|
-
"presets": [
|
|
105
|
-
"@babel/preset-env",
|
|
106
|
-
"@babel/preset-typescript"
|
|
107
|
-
],
|
|
108
|
-
"plugins": [
|
|
109
|
-
"@babel/plugin-transform-typescript"
|
|
110
|
-
]
|
|
111
|
-
}
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
- `@babel/preset-env` — handles modern JS syntax without a `targets` restriction, so it supports ES module syntax (`export default`, `import`, etc.).
|
|
115
|
-
- `@babel/preset-typescript` + `@babel/plugin-transform-typescript` — allows TypeScript syntax in `.vue` `<script lang="ts">` blocks.
|
|
116
|
-
- No `"type": "module"` in `package.json` means this `.babelrc` is loaded as JSON without any module format issues.
|
|
117
|
-
|
|
118
|
-
---
|
|
119
|
-
|
|
120
|
-
### 4. `node_modules/@nextcloud/eslint-config/index.js` (v8.4.1)
|
|
121
|
-
|
|
122
|
-
Loaded by `compat.extends('@nextcloud')`. Combines three parts:
|
|
123
|
-
|
|
124
|
-
```js
|
|
125
|
-
module.exports = {
|
|
126
|
-
...base, // parts/base.js
|
|
127
|
-
overrides: [
|
|
128
|
-
{ ...typescriptOverrides }, // parts/typescript.js
|
|
129
|
-
{ ...vueOverrides }, // parts/vue.js
|
|
130
|
-
],
|
|
131
|
-
}
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
---
|
|
135
|
-
|
|
136
|
-
### 5. `node_modules/@nextcloud/eslint-config/parts/base.js`
|
|
137
|
-
|
|
138
|
-
Applies to all files. Contains the indent rules that cause **red lines in `<script>` blocks**.
|
|
139
|
-
|
|
140
|
-
```js
|
|
141
|
-
parser: '@babel/eslint-parser',
|
|
142
|
-
parserOptions: {
|
|
143
|
-
requireConfigFile: false, // won't crash if no babel config — but .babelrc IS present and IS used
|
|
144
|
-
},
|
|
145
|
-
|
|
146
|
-
rules: {
|
|
147
|
-
indent: ['error', 'tab'], // red line: spaces used instead of tabs
|
|
148
|
-
'no-tabs': ['error', { allowIndentationTabs: true }], // red line: tabs used outside indentation
|
|
149
|
-
'no-mixed-spaces-and-tabs': ['error', 'smart-tabs'], // red line: mixed
|
|
150
|
-
...
|
|
151
|
-
}
|
|
152
|
-
```
|
|
153
|
-
|
|
154
|
-
`'error'` = **red underline**. `'warn'` = yellow. Auto-fix on save corrects all of these.
|
|
155
|
-
|
|
156
|
-
---
|
|
157
|
-
|
|
158
|
-
### 6. `node_modules/@nextcloud/eslint-config/parts/vue.js`
|
|
159
|
-
|
|
160
|
-
Applies **only** to `**/*.vue` files. Contains the indent rule that causes **red lines in `<template>` blocks**.
|
|
161
|
-
|
|
162
|
-
```js
|
|
163
|
-
files: ['**/*.vue'],
|
|
164
|
-
parser: 'vue-eslint-parser', // parses the full .vue file structure
|
|
165
|
-
parserOptions: {
|
|
166
|
-
parser: '@babel/eslint-parser', // delegates <script> parsing to Babel (picks up .babelrc)
|
|
167
|
-
},
|
|
168
|
-
extends: ['plugin:vue/recommended'],
|
|
169
|
-
|
|
170
|
-
rules: {
|
|
171
|
-
'vue/html-indent': ['error', 'tab'], // red line: wrong indentation in <template>
|
|
172
|
-
'vue/component-name-in-template-casing': ['error', 'PascalCase'],
|
|
173
|
-
'vue/html-closing-bracket-spacing': 'error',
|
|
174
|
-
'vue/html-closing-bracket-newline': ['error', { multiline: 'never' }],
|
|
175
|
-
'vue/max-attributes-per-line': ['error', { singleline: 3, multiline: 1 }],
|
|
176
|
-
'vue/multi-word-component-names': ['off'],
|
|
177
|
-
...
|
|
178
|
-
}
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
- `vue-eslint-parser` is what lets ESLint understand `<template>`, `<script>`, `<style>` as separate blocks.
|
|
182
|
-
- `vue/html-indent: ['error', 'tab']` → red line on wrong indentation inside `<template>`.
|
|
183
|
-
- `indent: ['error', 'tab']` from base.js → red line on wrong indentation inside `<script>`.
|
|
184
|
-
|
|
185
|
-
---
|
|
186
|
-
|
|
187
|
-
## The Full Chain
|
|
188
|
-
|
|
189
|
-
```
|
|
190
|
-
You open a .vue file in VSCode
|
|
191
|
-
│
|
|
192
|
-
▼
|
|
193
|
-
dbaeumer.vscode-eslint extension starts
|
|
194
|
-
reads .vscode/settings.json
|
|
195
|
-
→ editor.defaultFormatter = ESLint for .vue (no [vue] override, falls back to global)
|
|
196
|
-
→ eslint.format.enable = true (ESLint can fix, not just report)
|
|
197
|
-
│
|
|
198
|
-
▼
|
|
199
|
-
ESLint loads eslint.config.js
|
|
200
|
-
→ FlatCompat bridges @nextcloud/eslint-config (legacy → flat)
|
|
201
|
-
→ index.js loads base.js + vue.js
|
|
202
|
-
│
|
|
203
|
-
▼
|
|
204
|
-
For <template> block:
|
|
205
|
-
vue-eslint-parser handles the HTML
|
|
206
|
-
Rule: vue/html-indent ['error', 'tab']
|
|
207
|
-
Wrong indent → RED UNDERLINE
|
|
208
|
-
|
|
209
|
-
For <script> block:
|
|
210
|
-
@babel/eslint-parser handles the JS
|
|
211
|
-
Picks up .babelrc (preset-env + preset-typescript)
|
|
212
|
-
Rule: indent ['error', 'tab']
|
|
213
|
-
Wrong indent → RED UNDERLINE
|
|
214
|
-
│
|
|
215
|
-
▼
|
|
216
|
-
You press Ctrl+S
|
|
217
|
-
editor.formatOnSave triggers ESLint formatter
|
|
218
|
-
ESLint runs --fix internally
|
|
219
|
-
→ replaces wrong indentation with tabs in both blocks
|
|
220
|
-
→ fixes bracket spacing, trailing commas, etc.
|
|
221
|
-
File saved with corrected formatting
|
|
222
|
-
```
|
|
223
|
-
|
|
224
|
-
---
|
|
225
|
-
|
|
226
|
-
## Key Reason This Works and nextcloud-vue Does Not
|
|
227
|
-
|
|
228
|
-
| | OpenRegister | nextcloud-vue |
|
|
229
|
-
|---|---|---|
|
|
230
|
-
| `package.json` `"type"` field | not set (defaults to CommonJS) | `"type": "module"` |
|
|
231
|
-
| `eslint.config.js` uses `require()` | works fine | **crashes** — `require` not defined in ES module scope |
|
|
232
|
-
| Babel config file | `.babelrc` (JSON, no module issues) | `babel.config.js` (treated as ESM, crashes Babel sync load) |
|
|
233
|
-
| ESLint server in VSCode | starts, lints, shows red lines | hangs on startup, times out, shows nothing |
|
|
234
|
-
|
|
235
|
-
The fix for nextcloud-vue is to rename both files to `.cjs` so Node treats them as CommonJS regardless of `"type": "module"`, and ensure `babel.config.cjs` uses `module.exports` (not `export default`) with `sourceType: 'unambiguous'` at the top level.
|