@conduction/nextcloud-vue 0.1.0-beta.1 → 0.1.0-beta.3

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 (56) hide show
  1. package/README.md +226 -0
  2. package/css/index.css +5 -0
  3. package/dist/nextcloud-vue.cjs.js +7039 -2409
  4. package/dist/nextcloud-vue.cjs.js.map +1 -1
  5. package/dist/nextcloud-vue.css +237 -52
  6. package/dist/nextcloud-vue.esm.js +7012 -2386
  7. package/dist/nextcloud-vue.esm.js.map +1 -1
  8. package/package.json +4 -5
  9. package/src/components/CnActionsBar/CnActionsBar.vue +225 -0
  10. package/src/components/CnActionsBar/index.js +1 -0
  11. package/src/components/CnCopyDialog/CnCopyDialog.vue +250 -0
  12. package/src/components/CnCopyDialog/index.js +1 -0
  13. package/src/components/CnDataTable/CnDataTable.vue +0 -5
  14. package/src/components/CnDeleteDialog/CnDeleteDialog.vue +170 -0
  15. package/src/components/CnDeleteDialog/index.js +1 -0
  16. package/src/components/CnFormDialog/CnFormDialog.vue +629 -0
  17. package/src/components/CnFormDialog/index.js +1 -0
  18. package/src/components/CnIcon/CnIcon.vue +89 -0
  19. package/src/components/CnIcon/index.js +1 -0
  20. package/src/components/CnIndexPage/CnIndexPage.vue +434 -300
  21. package/src/components/CnIndexSidebar/CnIndexSidebar.vue +484 -0
  22. package/src/components/CnIndexSidebar/index.js +1 -0
  23. package/src/components/CnPageHeader/CnPageHeader.vue +57 -0
  24. package/src/components/CnPageHeader/index.js +1 -0
  25. package/src/components/CnRegisterMapping/CnRegisterMapping.vue +792 -0
  26. package/src/components/CnRegisterMapping/index.js +1 -0
  27. package/src/components/index.js +8 -4
  28. package/src/composables/useListView.js +254 -45
  29. package/src/constants/metadata.js +30 -0
  30. package/src/css/actions-bar.css +48 -0
  31. package/src/css/badge.css +4 -4
  32. package/src/css/card.css +23 -23
  33. package/src/css/detail.css +13 -13
  34. package/src/css/index-page.css +32 -0
  35. package/src/css/index-sidebar.css +187 -0
  36. package/src/css/index.css +4 -0
  37. package/src/css/layout.css +14 -14
  38. package/src/css/page-header.css +33 -0
  39. package/src/css/pagination.css +12 -12
  40. package/src/css/table.css +21 -22
  41. package/src/css/utilities.css +2 -2
  42. package/src/index.js +11 -8
  43. package/src/store/plugins/index.js +1 -0
  44. package/src/store/plugins/registerMapping.js +185 -0
  45. package/src/store/useObjectStore.js +122 -61
  46. package/src/utils/headers.js +7 -1
  47. package/src/utils/index.js +1 -1
  48. package/src/utils/schema.js +133 -1
  49. package/src/components/CnDetailViewLayout/CnDetailViewLayout.vue +0 -88
  50. package/src/components/CnDetailViewLayout/index.js +0 -1
  51. package/src/components/CnEmptyState/CnEmptyState.vue +0 -78
  52. package/src/components/CnEmptyState/index.js +0 -1
  53. package/src/components/CnListViewLayout/CnListViewLayout.vue +0 -80
  54. package/src/components/CnListViewLayout/index.js +0 -1
  55. package/src/components/CnViewModeToggle/CnViewModeToggle.vue +0 -77
  56. package/src/components/CnViewModeToggle/index.js +0 -1
@@ -0,0 +1,185 @@
1
+ import { buildHeaders } from '../../utils/headers.js'
2
+
3
+ /**
4
+ * Register mapping plugin for the object store.
5
+ *
6
+ * Adds state and actions for fetching OpenRegister registers and their
7
+ * schemas, used by CnRegisterMapping to populate dropdowns.
8
+ *
9
+ * State: registers, registerSchemas, registersLoading, registersError
10
+ * Actions: fetchRegisters, fetchSchemasForRegister, clearRegisterMapping
11
+ * Getters: getRegisters, registerOptions, schemaOptions, isRegistersLoading, getRegistersError
12
+ *
13
+ * @return {Function} Plugin factory
14
+ *
15
+ * @example
16
+ * const useStore = createObjectStore('object', {
17
+ * plugins: [registerMappingPlugin()],
18
+ * })
19
+ * const store = useStore()
20
+ * await store.fetchRegisters()
21
+ * const options = store.registerOptions // [{ label, value }]
22
+ * const schemas = store.schemaOptions('5') // [{ label, value }]
23
+ */
24
+ export function registerMappingPlugin() {
25
+ return {
26
+ name: 'RegisterMapping',
27
+
28
+ state: () => ({
29
+ /** @type {Array} All available registers from OpenRegister */
30
+ registers: [],
31
+ /** @type {Object<string, Array>} Schemas keyed by register ID */
32
+ registerSchemas: {},
33
+ /** @type {boolean} Whether registers are being fetched */
34
+ registersLoading: false,
35
+ /** @type {string|null} Error message from last fetch */
36
+ registersError: null,
37
+ }),
38
+
39
+ getters: {
40
+ /** @return {Array} Raw register list */
41
+ getRegisters: (state) => state.registers,
42
+
43
+ /** @return {boolean} Whether registers are loading */
44
+ isRegistersLoading: (state) => state.registersLoading,
45
+
46
+ /** @return {string|null} Last error */
47
+ getRegistersError: (state) => state.registersError,
48
+
49
+ /**
50
+ * Registers as NcSelect-compatible options.
51
+ *
52
+ * @return {Array<{label: string, value: string}>}
53
+ */
54
+ registerOptions: (state) => state.registers.map((r) => ({
55
+ label: r.title || r.slug || `Register ${r.id}`,
56
+ value: String(r.id),
57
+ })),
58
+ },
59
+
60
+ actions: {
61
+ /**
62
+ * Get schemas for a register as NcSelect options.
63
+ *
64
+ * @param {string|number} registerId Register ID
65
+ * @return {Array<{label: string, value: string}>}
66
+ */
67
+ schemaOptions(registerId) {
68
+ const id = String(registerId)
69
+ return (this.registerSchemas[id] || []).map((s) => ({
70
+ label: s.title || s.slug || `Schema ${s.id}`,
71
+ value: String(s.id),
72
+ }))
73
+ },
74
+
75
+ /**
76
+ * Fetch all registers from OpenRegister with expanded schemas.
77
+ *
78
+ * @param {boolean} [withSchemas=true] Include schemas in response
79
+ * @return {Promise<Array>} Fetched registers
80
+ */
81
+ async fetchRegisters(withSchemas = true) {
82
+ this.registersLoading = true
83
+ this.registersError = null
84
+
85
+ try {
86
+ let url = '/apps/openregister/api/registers'
87
+ if (withSchemas) {
88
+ url += '?_extend[]=schemas'
89
+ }
90
+
91
+ const response = await fetch(url, {
92
+ method: 'GET',
93
+ headers: buildHeaders(),
94
+ })
95
+
96
+ if (!response.ok) {
97
+ this.registersError = `Failed to fetch registers: ${response.statusText}`
98
+ return []
99
+ }
100
+
101
+ const data = await response.json()
102
+ const results = data.results || data
103
+ this.registers = Array.isArray(results) ? results : []
104
+
105
+ // Cache expanded schemas by register ID
106
+ if (withSchemas) {
107
+ for (const reg of this.registers) {
108
+ if (Array.isArray(reg.schemas) && reg.schemas.length > 0) {
109
+ this.registerSchemas = {
110
+ ...this.registerSchemas,
111
+ [String(reg.id)]: reg.schemas.filter(
112
+ (s) => s && typeof s === 'object' && s.id,
113
+ ),
114
+ }
115
+ }
116
+ }
117
+ }
118
+
119
+ return this.registers
120
+ } catch (error) {
121
+ this.registersError = error.message || 'Network error fetching registers'
122
+ return []
123
+ } finally {
124
+ this.registersLoading = false
125
+ }
126
+ },
127
+
128
+ /**
129
+ * Fetch schemas for a specific register.
130
+ * Returns cached data if already fetched via expanded registers.
131
+ *
132
+ * @param {string|number} registerId Register ID
133
+ * @return {Promise<Array>} Schemas for the register
134
+ */
135
+ async fetchSchemasForRegister(registerId) {
136
+ const id = String(registerId)
137
+
138
+ // Return cached if available and non-empty
139
+ if (this.registerSchemas[id]?.length > 0) {
140
+ return this.registerSchemas[id]
141
+ }
142
+
143
+ // Check registers array for expanded schemas
144
+ const register = this.registers.find((r) => String(r.id) === id)
145
+ if (register?.schemas?.length > 0) {
146
+ const schemas = register.schemas.filter(
147
+ (s) => s && typeof s === 'object' && s.id,
148
+ )
149
+ if (schemas.length > 0) {
150
+ this.registerSchemas = { ...this.registerSchemas, [id]: schemas }
151
+ return schemas
152
+ }
153
+ }
154
+
155
+ // Fetch from API as fallback
156
+ try {
157
+ const response = await fetch(
158
+ `/apps/openregister/api/registers/${id}?_extend[]=schemas`,
159
+ { method: 'GET', headers: buildHeaders() },
160
+ )
161
+ if (!response.ok) return []
162
+
163
+ const data = await response.json()
164
+ const schemas = (data.schemas || []).filter(
165
+ (s) => s && typeof s === 'object' && s.id,
166
+ )
167
+ this.registerSchemas = { ...this.registerSchemas, [id]: schemas }
168
+ return schemas
169
+ } catch {
170
+ return []
171
+ }
172
+ },
173
+
174
+ /**
175
+ * Clear all register mapping state.
176
+ */
177
+ clearRegisterMapping() {
178
+ this.registers = []
179
+ this.registerSchemas = {}
180
+ this.registersLoading = false
181
+ this.registersError = null
182
+ },
183
+ },
184
+ }
185
+ }
@@ -103,6 +103,8 @@ function baseState() {
103
103
  searchTerms: {},
104
104
  /** @type {Object<string, object|null>} */
105
105
  schemas: {},
106
+ /** @type {Object<string, object>} Facet data per type for CnIndexSidebar: { fieldName: { values: [{value, count}] } } */
107
+ facets: {},
106
108
  /** @type {{baseUrl: string}} */
107
109
  _options: {
108
110
  baseUrl: DEFAULT_BASE_URL,
@@ -167,6 +169,12 @@ const baseGetters = {
167
169
  * @return {Function} (type: string) => object|null
168
170
  */
169
171
  getSchema: (state) => (type) => state.schemas[type] || null,
172
+
173
+ /**
174
+ * Get facet data for a type (CnIndexSidebar-compatible format).
175
+ * @return {Function} (type: string) => object
176
+ */
177
+ getFacets: (state) => (type) => state.facets[type] || {},
170
178
  }
171
179
 
172
180
  // ── Base actions ────────────────────────────────────────────────────────
@@ -191,14 +199,17 @@ const baseActions = {
191
199
  * @param {string} registerId OpenRegister register ID
192
200
  */
193
201
  registerObjectType(slug, schemaId, registerId) {
194
- this.objectTypeRegistry[slug] = { schema: schemaId, register: registerId }
195
- this.collections[slug] = []
196
- this.objects[slug] = {}
197
- this.loading[slug] = false
198
- this.errors[slug] = null
199
- this.pagination[slug] = { total: 0, page: 1, pages: 1, limit: 20 }
200
- this.searchTerms[slug] = ''
201
- this.schemas[slug] = null
202
+ // Replace entire objects so Vue 2 reactivity detects the change
203
+ // (Vue 2 cannot track new properties added to existing reactive objects)
204
+ this.objectTypeRegistry = { ...this.objectTypeRegistry, [slug]: { schema: schemaId, register: registerId } }
205
+ this.collections = { ...this.collections, [slug]: [] }
206
+ this.objects = { ...this.objects, [slug]: {} }
207
+ this.loading = { ...this.loading, [slug]: false }
208
+ this.errors = { ...this.errors, [slug]: null }
209
+ this.pagination = { ...this.pagination, [slug]: { total: 0, page: 1, pages: 1, limit: 20 } }
210
+ this.searchTerms = { ...this.searchTerms, [slug]: '' }
211
+ this.schemas = { ...this.schemas, [slug]: null }
212
+ this.facets = { ...this.facets, [slug]: {} }
202
213
  },
203
214
 
204
215
  /**
@@ -215,6 +226,7 @@ const baseActions = {
215
226
  delete this.pagination[slug]
216
227
  delete this.searchTerms[slug]
217
228
  delete this.schemas[slug]
229
+ delete this.facets[slug]
218
230
  },
219
231
 
220
232
  /**
@@ -254,7 +266,7 @@ const baseActions = {
254
266
  * @param {string} type The type slug
255
267
  */
256
268
  clearError(type) {
257
- this.errors[type] = null
269
+ this.errors = { ...this.errors, [type]: null }
258
270
  },
259
271
 
260
272
  /**
@@ -264,7 +276,7 @@ const baseActions = {
264
276
  * @param {string} term The search term
265
277
  */
266
278
  setSearchTerm(type, term) {
267
- this.searchTerms[type] = term
279
+ this.searchTerms = { ...this.searchTerms, [type]: term }
268
280
  },
269
281
 
270
282
  /**
@@ -273,7 +285,7 @@ const baseActions = {
273
285
  * @param {string} type The type slug
274
286
  */
275
287
  clearSearchTerm(type) {
276
- this.searchTerms[type] = ''
288
+ this.searchTerms = { ...this.searchTerms, [type]: '' }
277
289
  },
278
290
 
279
291
  /**
@@ -299,7 +311,7 @@ const baseActions = {
299
311
  if (!response.ok) return null
300
312
 
301
313
  const schema = await response.json()
302
- this.schemas[type] = schema
314
+ this.schemas = { ...this.schemas, [type]: schema }
303
315
  return schema
304
316
  } catch {
305
317
  return null
@@ -314,11 +326,23 @@ const baseActions = {
314
326
  * @return {Promise<Array>} The fetched collection (also stored in state)
315
327
  */
316
328
  async fetchCollection(type, params = {}) {
317
- this.loading[type] = true
318
- this.errors[type] = null
329
+ this.loading = { ...this.loading, [type]: true }
330
+ this.errors = { ...this.errors, [type]: null }
319
331
 
320
332
  try {
321
- const url = this._buildUrl(type) + buildQueryString(params)
333
+ // Auto-include _facets=extend when schema has facetable properties
334
+ const fetchParams = { ...params }
335
+ if (!fetchParams._facets) {
336
+ const schema = this.schemas[type]
337
+ const hasFacetable = schema
338
+ && schema.properties
339
+ && Object.values(schema.properties).some((p) => p.facetable)
340
+ if (hasFacetable) {
341
+ fetchParams._facets = 'extend'
342
+ }
343
+ }
344
+
345
+ const url = this._buildUrl(type) + buildQueryString(fetchParams)
322
346
 
323
347
  const response = await fetch(url, {
324
348
  method: 'GET',
@@ -326,30 +350,54 @@ const baseActions = {
326
350
  })
327
351
 
328
352
  if (!response.ok) {
329
- this.errors[type] = await parseResponseError(response, type)
353
+ this.errors = { ...this.errors, [type]: await parseResponseError(response, type) }
330
354
  console.error(`Error fetching ${type} collection:`, this.errors[type])
331
355
  return []
332
356
  }
333
357
 
334
358
  const data = await response.json()
359
+ const results = data.results || data
360
+
361
+ this.collections = { ...this.collections, [type]: results }
362
+ this.pagination = {
363
+ ...this.pagination,
364
+ [type]: {
365
+ total: data.total || results.length,
366
+ page: data.page || 1,
367
+ pages: data.pages || 1,
368
+ limit: params._limit || 20,
369
+ },
370
+ }
335
371
 
336
- this.collections[type] = data.results || data
337
- this.pagination[type] = {
338
- total: data.total || (data.results || data).length,
339
- page: data.page || 1,
340
- pages: data.pages || 1,
341
- limit: params._limit || 20,
372
+ // Parse facet data from API response and transform to CnIndexSidebar format
373
+ if (data.facets) {
374
+ const transformed = {}
375
+ for (const [key, facet] of Object.entries(data.facets)) {
376
+ if (facet.buckets || facet.data?.buckets) {
377
+ const buckets = facet.buckets || facet.data.buckets
378
+ transformed[key] = {
379
+ values: buckets.map((b) => ({
380
+ value: b.key ?? b.value,
381
+ count: b.count || 0,
382
+ })),
383
+ }
384
+ }
385
+ }
386
+ this.facets = { ...this.facets, [type]: transformed }
342
387
  }
343
388
 
344
- return this.collections[type]
389
+ return results
345
390
  } catch (error) {
346
- this.errors[type] = error.name === 'TypeError'
347
- ? networkError(error)
348
- : genericError(error)
391
+ this.errors = {
392
+ ...this.errors,
393
+ [type]: error.name === 'TypeError'
394
+ ? networkError(error)
395
+ : genericError(error),
396
+ }
349
397
  console.error(`Error fetching ${type} collection:`, error)
350
398
  return []
351
399
  } finally {
352
- this.loading[type] = false
400
+ this.loading = { ...this.loading, [type]: false }
353
401
  }
354
402
  },
355
403
 
@@ -361,8 +409,8 @@ const baseActions = {
361
409
  * @return {Promise<object|null>} The fetched object (also cached in state)
362
410
  */
363
411
  async fetchObject(type, id) {
364
- this.loading[type] = true
365
- this.errors[type] = null
412
+ this.loading = { ...this.loading, [type]: true }
413
+ this.errors = { ...this.errors, [type]: null }
366
414
 
367
415
  try {
368
416
  const url = this._buildUrl(type, id)
@@ -373,27 +421,30 @@ const baseActions = {
373
421
  })
374
422
 
375
423
  if (!response.ok) {
376
- this.errors[type] = await parseResponseError(response, type)
424
+ this.errors = { ...this.errors, [type]: await parseResponseError(response, type) }
377
425
  console.error(`Error fetching ${type}/${id}:`, this.errors[type])
378
426
  return null
379
427
  }
380
428
 
381
429
  const data = await response.json()
382
430
 
383
- if (!this.objects[type]) {
384
- this.objects[type] = {}
431
+ this.objects = {
432
+ ...this.objects,
433
+ [type]: { ...(this.objects[type] || {}), [id]: data },
385
434
  }
386
- this.objects[type][id] = data
387
435
 
388
436
  return data
389
437
  } catch (error) {
390
- this.errors[type] = error.name === 'TypeError'
391
- ? networkError(error)
392
- : genericError(error)
438
+ this.errors = {
439
+ ...this.errors,
440
+ [type]: error.name === 'TypeError'
441
+ ? networkError(error)
442
+ : genericError(error),
443
+ }
393
444
  console.error(`Error fetching ${type}/${id}:`, error)
394
445
  return null
395
446
  } finally {
396
- this.loading[type] = false
447
+ this.loading = { ...this.loading, [type]: false }
397
448
  }
398
449
  },
399
450
 
@@ -405,8 +456,8 @@ const baseActions = {
405
456
  * @return {Promise<object|null>} The saved object or null on error
406
457
  */
407
458
  async saveObject(type, objectData) {
408
- this.loading[type] = true
409
- this.errors[type] = null
459
+ this.loading = { ...this.loading, [type]: true }
460
+ this.errors = { ...this.errors, [type]: null }
410
461
 
411
462
  try {
412
463
  const isUpdate = !!objectData.id
@@ -422,28 +473,31 @@ const baseActions = {
422
473
  })
423
474
 
424
475
  if (!response.ok) {
425
- this.errors[type] = await parseResponseError(response, type)
476
+ this.errors = { ...this.errors, [type]: await parseResponseError(response, type) }
426
477
  console.error(`Error saving ${type}:`, this.errors[type])
427
478
  return null
428
479
  }
429
480
 
430
481
  const data = await response.json()
482
+ const savedId = data.id || objectData.id
431
483
 
432
- if (!this.objects[type]) {
433
- this.objects[type] = {}
484
+ this.objects = {
485
+ ...this.objects,
486
+ [type]: { ...(this.objects[type] || {}), [savedId]: data },
434
487
  }
435
- const savedId = data.id || objectData.id
436
- this.objects[type][savedId] = data
437
488
 
438
489
  return data
439
490
  } catch (error) {
440
- this.errors[type] = error.name === 'TypeError'
441
- ? networkError(error)
442
- : genericError(error)
491
+ this.errors = {
492
+ ...this.errors,
493
+ [type]: error.name === 'TypeError'
494
+ ? networkError(error)
495
+ : genericError(error),
496
+ }
443
497
  console.error(`Error saving ${type}:`, error)
444
498
  return null
445
499
  } finally {
446
- this.loading[type] = false
500
+ this.loading = { ...this.loading, [type]: false }
447
501
  }
448
502
  },
449
503
 
@@ -455,8 +509,8 @@ const baseActions = {
455
509
  * @return {Promise<boolean>} True if deleted successfully
456
510
  */
457
511
  async deleteObject(type, id) {
458
- this.loading[type] = true
459
- this.errors[type] = null
512
+ this.loading = { ...this.loading, [type]: true }
513
+ this.errors = { ...this.errors, [type]: null }
460
514
 
461
515
  try {
462
516
  const url = this._buildUrl(type, id)
@@ -467,29 +521,34 @@ const baseActions = {
467
521
  })
468
522
 
469
523
  if (!response.ok) {
470
- this.errors[type] = await parseResponseError(response, type)
524
+ this.errors = { ...this.errors, [type]: await parseResponseError(response, type) }
471
525
  console.error(`Error deleting ${type}/${id}:`, this.errors[type])
472
526
  return false
473
527
  }
474
528
 
475
529
  if (this.objects[type]) {
476
- delete this.objects[type][id]
530
+ const { [id]: _, ...remaining } = this.objects[type]
531
+ this.objects = { ...this.objects, [type]: remaining }
477
532
  }
478
533
  if (this.collections[type]) {
479
- this.collections[type] = this.collections[type].filter(
480
- (obj) => obj.id !== id,
481
- )
534
+ this.collections = {
535
+ ...this.collections,
536
+ [type]: this.collections[type].filter((obj) => obj.id !== id),
537
+ }
482
538
  }
483
539
 
484
540
  return true
485
541
  } catch (error) {
486
- this.errors[type] = error.name === 'TypeError'
487
- ? networkError(error)
488
- : genericError(error)
542
+ this.errors = {
543
+ ...this.errors,
544
+ [type]: error.name === 'TypeError'
545
+ ? networkError(error)
546
+ : genericError(error),
547
+ }
489
548
  console.error(`Error deleting ${type}/${id}:`, error)
490
549
  return false
491
550
  } finally {
492
- this.loading[type] = false
551
+ this.loading = { ...this.loading, [type]: false }
493
552
  }
494
553
  },
495
554
 
@@ -527,8 +586,10 @@ const baseActions = {
527
586
  })
528
587
  if (response.ok) {
529
588
  const data = await response.json()
530
- if (!this.objects[type]) this.objects[type] = {}
531
- this.objects[type][id] = data
589
+ this.objects = {
590
+ ...this.objects,
591
+ [type]: { ...(this.objects[type] || {}), [id]: data },
592
+ }
532
593
  result[id] = data
533
594
  }
534
595
  } catch {
@@ -32,7 +32,13 @@ export function buildQueryString(params = {}) {
32
32
 
33
33
  for (const [key, value] of Object.entries(params)) {
34
34
  if (value === undefined || value === null || value === '') continue
35
- if (key === '_order' && typeof value === 'object') {
35
+ if (Array.isArray(value)) {
36
+ for (const item of value) {
37
+ if (item !== undefined && item !== null && item !== '') {
38
+ queryParams.append(key, String(item))
39
+ }
40
+ }
41
+ } else if (key === '_order' && typeof value === 'object') {
36
42
  queryParams.set(key, JSON.stringify(value))
37
43
  } else {
38
44
  queryParams.set(key, String(value))
@@ -1,3 +1,3 @@
1
1
  export { buildHeaders, buildQueryString } from './headers.js'
2
2
  export { parseResponseError, networkError, genericError } from './errors.js'
3
- export { columnsFromSchema, formatValue, filtersFromSchema } from './schema.js'
3
+ export { columnsFromSchema, formatValue, filtersFromSchema, fieldsFromSchema } from './schema.js'