@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.
Files changed (82) hide show
  1. package/dist/nextcloud-vue.cjs.js +13575 -2374
  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 +13517 -2336
  5. package/dist/nextcloud-vue.esm.js.map +1 -1
  6. package/package.json +11 -7
  7. package/src/components/CnActionsBar/CnActionsBar.vue +20 -2
  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 +63 -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 +13 -3
  78. package/src/utils/index.js +1 -0
  79. package/src/utils/schema.js +3 -3
  80. package/src/utils/widgetVisibility.js +162 -0
  81. package/src/components/CnObjectCard/eslint-setup.md +0 -235
  82. package/src/components/CnObjectCard/package.json-or.json +0 -132
@@ -0,0 +1,360 @@
1
+ import { defineStore } from 'pinia'
2
+ import { buildHeaders, prefixUrl } from '../utils/headers.js'
3
+ import { parseResponseError } from '../utils/errors.js'
4
+
5
+ /**
6
+ * Default fields stripped from items before POST/PUT.
7
+ * @type {string[]}
8
+ */
9
+ const DEFAULT_CLEAN_FIELDS = ['id', 'uuid', 'created', 'updated']
10
+
11
+ /**
12
+ * Default base URL for the API.
13
+ * @type {string}
14
+ */
15
+ const DEFAULT_BASE_URL = '/apps/openregister/api'
16
+
17
+ /**
18
+ * Default list-response parser — extracts the `results` array.
19
+ *
20
+ * Called with the store as `this`, so custom implementations can
21
+ * perform side-effects (e.g. update extra state from the response).
22
+ *
23
+ * @param {object} json Parsed response body
24
+ * @return {Array} The items array for setList
25
+ */
26
+ function defaultParseListResponse(json) {
27
+ return json.results
28
+ }
29
+
30
+ /**
31
+ * Create a Pinia store with standard CRUD operations.
32
+ *
33
+ * Generates a store with list/item state, pagination, filters, and
34
+ * async actions for fetching, creating, updating, and deleting items.
35
+ * Domain-specific state, getters, and actions can be added via `extend`.
36
+ *
37
+ * @example
38
+ * // Minimal — pure CRUD
39
+ * import { createCrudStore } from '@conduction/nextcloud-vue'
40
+ * import { Source } from '../../entities/index.js'
41
+ *
42
+ * export const useSourceStore = createCrudStore('source', {
43
+ * endpoint: 'sources',
44
+ * entity: Source,
45
+ * })
46
+ *
47
+ * @example
48
+ * // With features and extensions
49
+ * import { createCrudStore } from '@conduction/nextcloud-vue'
50
+ * import { Agent } from '../../entities/index.js'
51
+ *
52
+ * export const useAgentStore = createCrudStore('agent', {
53
+ * endpoint: 'agents',
54
+ * entity: Agent,
55
+ * features: { loading: true, viewMode: true },
56
+ * extend: {
57
+ * actions: {
58
+ * async getStats() {
59
+ * const response = await fetch(this._options.baseApiUrl + '/stats')
60
+ * if (!response.ok) throw new Error('HTTP ' + response.status)
61
+ * return response.json()
62
+ * },
63
+ * },
64
+ * },
65
+ * })
66
+ *
67
+ * @param {string} name Pinia store ID (e.g. 'source', 'agent')
68
+ * @param {object} config Store configuration
69
+ * @param {string} config.endpoint API resource path segment (e.g. 'sources')
70
+ * @param {string} [config.baseUrl] API base URL (before endpoint)
71
+ * @param {Function|null} [config.entity] Entity class constructor for wrapping items, or null for raw data
72
+ * @param {string[]} [config.cleanFields] Fields to strip in cleanForSave
73
+ * @param {object} [config.features] Feature flags to enable optional state/getters/actions
74
+ * @param {boolean} [config.features.loading] Add loading/error state and isLoading/getError getters
75
+ * @param {boolean} [config.features.viewMode] Add viewMode state, getViewMode getter, setViewMode action
76
+ * @param {Function} [config.parseListResponse] Custom response parser for refreshList.
77
+ * Receives the parsed JSON body with the store instance as `this`.
78
+ * Must return an array of items. Default: `(json) => json.results`
79
+ * @param {object} [config.extend] Extra state/getters/actions to merge into the store
80
+ * @param {Function} [config.extend.state] State factory returning extra state properties
81
+ * @param {object} [config.extend.getters] Extra getters (or overrides of base getters)
82
+ * @param {object} [config.extend.actions] Extra actions (or overrides of base actions)
83
+ * @return {Function} Pinia store composable (useXxxStore)
84
+ */
85
+ export function createCrudStore(name, config = {}) {
86
+ const {
87
+ endpoint,
88
+ baseUrl = DEFAULT_BASE_URL,
89
+ entity: Entity = null,
90
+ cleanFields = DEFAULT_CLEAN_FIELDS,
91
+ features = {},
92
+ parseListResponse = defaultParseListResponse,
93
+ extend = {},
94
+ } = config
95
+
96
+ if (!endpoint) {
97
+ throw new Error(`createCrudStore("${name}"): config.endpoint is required`)
98
+ }
99
+
100
+ const baseApiUrl = prefixUrl(`${baseUrl}/${endpoint}`)
101
+
102
+ return defineStore(name, {
103
+ state: () => ({
104
+ // ── Core state ──
105
+ item: null,
106
+ list: [],
107
+ filters: {},
108
+ pagination: { page: 1, limit: 20 },
109
+
110
+ // ── Optional feature state ──
111
+ ...(features.loading ? { loading: false, error: null } : {}),
112
+ ...(features.viewMode ? { viewMode: 'cards' } : {}),
113
+
114
+ // ── Internal config (available to extend actions) ──
115
+ _options: { endpoint, cleanFields, baseApiUrl },
116
+
117
+ // ── Domain-specific state ──
118
+ ...(typeof extend.state === 'function' ? extend.state() : {}),
119
+ }),
120
+
121
+ getters: {
122
+ // ── Optional feature getters ──
123
+ ...(features.viewMode ? { getViewMode: (state) => state.viewMode } : {}),
124
+ ...(features.loading
125
+ ? {
126
+ isLoading: (state) => state.loading,
127
+ getError: (state) => state.error,
128
+ }
129
+ : {}),
130
+
131
+ // ── Domain-specific getters ──
132
+ ...(extend.getters ?? {}),
133
+ },
134
+
135
+ actions: {
136
+ // ── Setters ──
137
+
138
+ /**
139
+ * Set the active item. Wraps in Entity class if configured.
140
+ * @param {object|null} data Raw item data or null to clear
141
+ */
142
+ setItem(data) {
143
+ this.item = data
144
+ ? (Entity ? new Entity(data) : data)
145
+ : null
146
+ },
147
+
148
+ /**
149
+ * Set the item list. Wraps each item in Entity class if configured.
150
+ * @param {Array} data Array of raw item objects
151
+ */
152
+ setList(data) {
153
+ this.list = Entity
154
+ ? data.map((item) => new Entity(item))
155
+ : [...data]
156
+ },
157
+
158
+ /**
159
+ * Set pagination parameters.
160
+ * @param {number} page Current page number
161
+ * @param {number} [limit] Items per page
162
+ */
163
+ setPagination(page, limit = 20) {
164
+ this.pagination = { page, limit }
165
+ },
166
+
167
+ /**
168
+ * Merge filter criteria into the current filters.
169
+ * @param {object} filters Key-value filter pairs to merge
170
+ */
171
+ setFilters(filters) {
172
+ this.filters = { ...this.filters, ...filters }
173
+ },
174
+
175
+ // ── Optional feature actions ──
176
+ ...(features.viewMode
177
+ ? {
178
+ /**
179
+ * Set the view mode (e.g. 'cards', 'table').
180
+ * @param {string} mode View mode identifier
181
+ */
182
+ setViewMode(mode) {
183
+ this.viewMode = mode
184
+ },
185
+ }
186
+ : {}),
187
+
188
+ // ── CRUD actions ──
189
+
190
+ /**
191
+ * Fetch the item list from the API.
192
+ * @param {string|null} [search] Optional search query
193
+ * @param {boolean} [soft] If true, don't toggle loading state
194
+ * @return {Promise<{response: Response, data: Array}>}
195
+ */
196
+ async refreshList(search = null, soft = false) {
197
+ if (features.loading && !soft) {
198
+ this.loading = true
199
+ this.error = null
200
+ }
201
+ try {
202
+ let url = this._options.baseApiUrl
203
+ if (search) {
204
+ url += '?_search=' + encodeURIComponent(search)
205
+ }
206
+ const response = await fetch(url, {
207
+ method: 'GET',
208
+ headers: buildHeaders(),
209
+ })
210
+ if (!response.ok) {
211
+ throw await parseResponseError(response, name)
212
+ }
213
+ const json = await response.json()
214
+ const data = parseListResponse.call(this, json)
215
+ this.setList(data)
216
+ return { response, data }
217
+ } catch (error) {
218
+ if (features.loading) {
219
+ this.error = error.message ?? error.toString()
220
+ }
221
+ throw error
222
+ } finally {
223
+ if (features.loading && !soft) {
224
+ this.loading = false
225
+ }
226
+ }
227
+ },
228
+
229
+ /**
230
+ * Fetch a single item by ID and set it as the active item.
231
+ * @param {string|number} id Item ID or UUID
232
+ * @return {Promise<object>} The fetched item data
233
+ */
234
+ async getOne(id) {
235
+ if (features.loading) {
236
+ this.loading = true
237
+ }
238
+ try {
239
+ const response = await fetch(`${this._options.baseApiUrl}/${id}`, {
240
+ method: 'GET',
241
+ headers: buildHeaders(),
242
+ })
243
+ if (!response.ok) {
244
+ throw await parseResponseError(response, name)
245
+ }
246
+ const data = await response.json()
247
+ this.setItem(data)
248
+ return data
249
+ } catch (error) {
250
+ if (features.loading) {
251
+ this.error = error.message ?? error.toString()
252
+ }
253
+ throw error
254
+ } finally {
255
+ if (features.loading) {
256
+ this.loading = false
257
+ }
258
+ }
259
+ },
260
+
261
+ /**
262
+ * Delete an item by ID. Refreshes the list and clears the active item.
263
+ * @param {object} item Item object (must have .id)
264
+ * @return {Promise<{response: Response}>}
265
+ */
266
+ async deleteOne(item) {
267
+ if (!item.id) {
268
+ throw new Error(`No ${name} to delete`)
269
+ }
270
+ if (features.loading) {
271
+ this.loading = true
272
+ }
273
+ try {
274
+ const response = await fetch(`${this._options.baseApiUrl}/${item.id}`, {
275
+ method: 'DELETE',
276
+ headers: buildHeaders(),
277
+ })
278
+ if (!response.ok) {
279
+ throw await parseResponseError(response, name)
280
+ }
281
+ await this.refreshList()
282
+ this.setItem(null)
283
+ return { response }
284
+ } catch (error) {
285
+ if (features.loading) {
286
+ this.error = error.message ?? error.toString()
287
+ }
288
+ throw error
289
+ } finally {
290
+ if (features.loading) {
291
+ this.loading = false
292
+ }
293
+ }
294
+ },
295
+
296
+ /**
297
+ * Strip read-only fields from an item before saving.
298
+ * Uses the `cleanFields` config array. Override in `extend.actions`
299
+ * for custom cleaning (the configured fields are in `this._options.cleanFields`).
300
+ * @param {object} item Raw item data
301
+ * @return {object} Cleaned copy safe for POST/PUT
302
+ */
303
+ cleanForSave(item) {
304
+ const cleaned = { ...item }
305
+ for (const field of this._options.cleanFields) {
306
+ delete cleaned[field]
307
+ }
308
+ return cleaned
309
+ },
310
+
311
+ /**
312
+ * Create or update an item. Determines method from presence of `.id`.
313
+ * @param {object} item Item data (without .id = create, with .id = update)
314
+ * @return {Promise<{response: Response, data: object}>}
315
+ */
316
+ async save(item) {
317
+ if (!item) {
318
+ throw new Error(`No ${name} to save`)
319
+ }
320
+ if (features.loading) {
321
+ this.loading = true
322
+ }
323
+ const isNew = !item.id
324
+ const url = isNew
325
+ ? this._options.baseApiUrl
326
+ : `${this._options.baseApiUrl}/${item.id}`
327
+ const method = isNew ? 'POST' : 'PUT'
328
+ const body = this.cleanForSave(item)
329
+
330
+ try {
331
+ const response = await fetch(url, {
332
+ method,
333
+ headers: buildHeaders(),
334
+ body: JSON.stringify(body),
335
+ })
336
+ if (!response.ok) {
337
+ throw await parseResponseError(response, name)
338
+ }
339
+ const responseData = await response.json()
340
+ const data = Entity ? new Entity(responseData) : responseData
341
+ this.setItem(data)
342
+ await this.refreshList()
343
+ return { response, data }
344
+ } catch (error) {
345
+ if (features.loading) {
346
+ this.error = error.message ?? error.toString()
347
+ }
348
+ throw error
349
+ } finally {
350
+ if (features.loading) {
351
+ this.loading = false
352
+ }
353
+ }
354
+ },
355
+
356
+ // ── Domain-specific actions (may override base actions) ──
357
+ ...(extend.actions ?? {}),
358
+ },
359
+ })
360
+ }
@@ -1,26 +1,16 @@
1
- import { buildHeaders, buildQueryString } from '../utils/headers.js'
1
+ import { buildHeaders, buildQueryString, capitalize } from '../utils/headers.js'
2
2
  import { parseResponseError, networkError } from '../utils/errors.js'
3
3
 
4
4
  /**
5
5
  * Standard empty paginated response shape used by all sub-resource plugins.
6
6
  *
7
- * @param {number} [limit=20] Default page size
7
+ * @param {number} [limit] Default page size
8
8
  * @return {object} Empty paginated state
9
9
  */
10
10
  export function emptyPaginated(limit = 20) {
11
11
  return { results: [], total: 0, page: 1, pages: 0, limit, offset: 0 }
12
12
  }
13
13
 
14
- /**
15
- * Capitalize the first letter of a string.
16
- *
17
- * @param {string} str Input string
18
- * @return {string} Capitalized string
19
- */
20
- function capitalize(str) {
21
- return str.charAt(0).toUpperCase() + str.slice(1)
22
- }
23
-
24
14
  /**
25
15
  * Create a sub-resource plugin for the object store.
26
16
  *
@@ -30,8 +20,8 @@ function capitalize(str) {
30
20
  *
31
21
  * @param {string} name Camel-case name for the sub-resource (e.g. 'auditTrails')
32
22
  * @param {string} endpoint URL path segment appended to the object URL (e.g. 'audit-trails')
33
- * @param {object} [options={}] Plugin options
34
- * @param {number} [options.limit=20] Default page size
23
+ * @param {object} [options] Plugin options
24
+ * @param {number} [options.limit] Default page size
35
25
  * @return {Function} Plugin factory that returns the plugin definition
36
26
  *
37
27
  * @example
@@ -76,7 +66,7 @@ export function createSubResourcePlugin(name, endpoint, options = {}) {
76
66
  *
77
67
  * @param {string} type The registered object type slug
78
68
  * @param {string} objectId The parent object ID
79
- * @param {object} [params={}] Query parameters (_search, _limit, _page)
69
+ * @param {object} [params] Query parameters (_search, _limit, _page)
80
70
  * @return {Promise<Array>} The fetched results
81
71
  */
82
72
  async [`fetch${cap}`](type, objectId, params = {}) {
@@ -1,3 +1,4 @@
1
1
  export { useObjectStore, createObjectStore } from './useObjectStore.js'
2
+ export { createCrudStore } from './createCrudStore.js'
2
3
  export { createSubResourcePlugin, emptyPaginated } from './createSubResourcePlugin.js'
3
4
  export { auditTrailsPlugin, relationsPlugin, filesPlugin, lifecyclePlugin, selectionPlugin, searchPlugin, SEARCH_TYPE, getRegisterApiUrl, getSchemaApiUrl } from './plugins/index.js'