@conduction/nextcloud-vue 0.1.0-beta.5 → 0.1.0-beta.7

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.
Files changed (80) hide show
  1. package/dist/nextcloud-vue.cjs.js +13607 -1917
  2. package/dist/nextcloud-vue.cjs.js.map +1 -1
  3. package/dist/nextcloud-vue.css +1238 -270
  4. package/dist/nextcloud-vue.esm.js +13549 -1879
  5. package/dist/nextcloud-vue.esm.js.map +1 -1
  6. package/package.json +13 -6
  7. package/src/components/CnActionsBar/CnActionsBar.vue +6 -1
  8. package/src/components/CnAdvancedFormDialog/CnAdvancedFormDialog.vue +1 -11
  9. package/src/components/CnAdvancedFormDialog/CnPropertiesTab.vue +5 -1
  10. package/src/components/CnAdvancedFormDialog/CnPropertyValueCell.vue +1 -1
  11. package/src/components/CnCard/CnCard.vue +415 -0
  12. package/src/components/CnCard/index.js +1 -0
  13. package/src/components/CnCardGrid/CnCardGrid.vue +20 -20
  14. package/src/components/CnChartWidget/CnChartWidget.vue +3 -1
  15. package/src/components/CnCopyDialog/CnCopyDialog.vue +7 -1
  16. package/src/components/CnDashboardGrid/CnDashboardGrid.vue +4 -0
  17. package/src/components/CnDashboardPage/CnDashboardPage.vue +2 -0
  18. package/src/components/CnDataTable/CnDataTable.vue +6 -2
  19. package/src/components/CnDeleteDialog/CnDeleteDialog.vue +7 -1
  20. package/src/components/CnDetailCard/CnDetailCard.vue +12 -1
  21. package/src/components/CnDetailGrid/CnDetailGrid.vue +254 -0
  22. package/src/components/CnDetailGrid/index.js +1 -0
  23. package/src/components/CnDetailPage/CnDetailPage.vue +157 -11
  24. package/src/components/CnFacetSidebar/CnFacetSidebar.vue +3 -1
  25. package/src/components/CnFormDialog/CnFormDialog.vue +934 -920
  26. package/src/components/CnIcon/CnIcon.vue +1 -1
  27. package/src/components/CnIndexPage/CnIndexPage.vue +51 -9
  28. package/src/components/CnIndexSidebar/CnIndexSidebar.vue +37 -9
  29. package/src/components/CnInfoWidget/CnInfoWidget.vue +219 -0
  30. package/src/components/CnInfoWidget/index.js +1 -0
  31. package/src/components/CnJsonViewer/CnJsonViewer.vue +283 -0
  32. package/src/components/CnJsonViewer/index.js +1 -0
  33. package/src/components/CnKpiGrid/CnKpiGrid.vue +5 -1
  34. package/src/components/CnMassCopyDialog/CnMassCopyDialog.vue +7 -1
  35. package/src/components/CnMassDeleteDialog/CnMassDeleteDialog.vue +7 -1
  36. package/src/components/CnMassExportDialog/CnMassExportDialog.vue +1 -1
  37. package/src/components/CnMassImportDialog/CnMassImportDialog.vue +1 -1
  38. package/src/components/CnObjectCard/CnObjectCard.vue +1 -1
  39. package/src/components/CnObjectSidebar/CnAuditTrailTab.vue +368 -0
  40. package/src/components/CnObjectSidebar/CnFilesTab.vue +286 -0
  41. package/src/components/CnObjectSidebar/CnNotesTab.vue +249 -0
  42. package/src/components/CnObjectSidebar/CnObjectSidebar.vue +45 -668
  43. package/src/components/CnObjectSidebar/CnTagsTab.vue +258 -0
  44. package/src/components/CnObjectSidebar/CnTasksTab.vue +482 -0
  45. package/src/components/CnObjectSidebar/index.js +5 -0
  46. package/src/components/CnProgressBar/CnProgressBar.vue +262 -0
  47. package/src/components/CnProgressBar/index.js +1 -0
  48. package/src/components/CnSchemaFormDialog/CnSchemaPropertiesTab.vue +1 -1
  49. package/src/components/CnStatsBlock/CnStatsBlock.vue +27 -11
  50. package/src/components/CnStatsPanel/CnStatsPanel.vue +320 -0
  51. package/src/components/CnStatsPanel/index.js +1 -0
  52. package/src/components/CnStatusBadge/CnStatusBadge.vue +15 -2
  53. package/src/components/CnTabbedFormDialog/CnTabbedFormDialog.vue +5 -1
  54. package/src/components/CnTableWidget/CnTableWidget.vue +332 -0
  55. package/src/components/CnTableWidget/index.js +1 -0
  56. package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +36 -1
  57. package/src/components/index.js +11 -0
  58. package/src/composables/useDashboardView.js +58 -12
  59. package/src/composables/useDetailView.js +3 -2
  60. package/src/composables/useListView.js +7 -6
  61. package/src/composables/useSubResource.js +3 -3
  62. package/src/css/badge.css +32 -0
  63. package/src/css/card.css +1 -0
  64. package/src/css/detail-page.css +74 -7
  65. package/src/index.js +16 -0
  66. package/src/mixins/gridLayout.js +118 -0
  67. package/src/store/createCrudStore.js +360 -0
  68. package/src/store/createSubResourcePlugin.js +5 -15
  69. package/src/store/index.js +1 -0
  70. package/src/store/plugins/auditTrails.js +346 -6
  71. package/src/store/plugins/lifecycle.js +4 -4
  72. package/src/store/plugins/registerMapping.js +18 -8
  73. package/src/store/plugins/relations.js +1 -1
  74. package/src/store/plugins/search.js +21 -8
  75. package/src/store/useObjectStore.js +30 -36
  76. package/src/utils/getTheme.js +9 -0
  77. package/src/utils/headers.js +14 -2
  78. package/src/utils/index.js +1 -0
  79. package/src/utils/schema.js +3 -3
  80. package/src/utils/widgetVisibility.js +162 -0
@@ -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
- delete this.objectTypeRegistry[slug]
260
- delete this.collections[slug]
261
- delete this.objects[slug]
262
- delete this.loading[slug]
263
- delete this.errors[slug]
264
- delete this.pagination[slug]
265
- delete this.searchTerms[slug]
266
- delete this.schemas[slug]
267
- delete this.registers[slug]
268
- delete this.facets[slug]
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<Object<string, object>>} Map of id -> object
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
+ }
@@ -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,6 +36,16 @@ 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
  *
@@ -50,13 +60,15 @@ export function buildQueryString(params = {}) {
50
60
 
51
61
  for (const [key, value] of Object.entries(params)) {
52
62
  if (value === undefined || value === null || value === '') continue
63
+ if (Array.isArray(value) && value.length === 0) continue
64
+ if (typeof value === 'object' && !Array.isArray(value) && Object.keys(value).length === 0) continue
53
65
  if (Array.isArray(value)) {
54
66
  for (const item of value) {
55
67
  if (item !== undefined && item !== null && item !== '') {
56
68
  queryParams.append(key, String(item))
57
69
  }
58
70
  }
59
- } else if (key === '_order' && typeof value === 'object') {
71
+ } else if (typeof value === 'object') {
60
72
  queryParams.set(key, JSON.stringify(value))
61
73
  } else {
62
74
  queryParams.set(key, String(value))
@@ -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'
@@ -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=100] Maximum string length before truncation
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=false] Whether to include readOnly properties
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(([key, prop]) => {
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
+ }