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

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 (54) hide show
  1. package/README.md +226 -0
  2. package/dist/nextcloud-vue.cjs.js +7039 -2409
  3. package/dist/nextcloud-vue.cjs.js.map +1 -1
  4. package/dist/nextcloud-vue.css +237 -52
  5. package/dist/nextcloud-vue.esm.js +7012 -2386
  6. package/dist/nextcloud-vue.esm.js.map +1 -1
  7. package/package.json +2 -4
  8. package/src/components/CnActionsBar/CnActionsBar.vue +225 -0
  9. package/src/components/CnActionsBar/index.js +1 -0
  10. package/src/components/CnCopyDialog/CnCopyDialog.vue +250 -0
  11. package/src/components/CnCopyDialog/index.js +1 -0
  12. package/src/components/CnDataTable/CnDataTable.vue +0 -5
  13. package/src/components/CnDeleteDialog/CnDeleteDialog.vue +170 -0
  14. package/src/components/CnDeleteDialog/index.js +1 -0
  15. package/src/components/CnFormDialog/CnFormDialog.vue +629 -0
  16. package/src/components/CnFormDialog/index.js +1 -0
  17. package/src/components/CnIcon/CnIcon.vue +89 -0
  18. package/src/components/CnIcon/index.js +1 -0
  19. package/src/components/CnIndexPage/CnIndexPage.vue +434 -300
  20. package/src/components/CnIndexSidebar/CnIndexSidebar.vue +484 -0
  21. package/src/components/CnIndexSidebar/index.js +1 -0
  22. package/src/components/CnPageHeader/CnPageHeader.vue +57 -0
  23. package/src/components/CnPageHeader/index.js +1 -0
  24. package/src/components/CnRegisterMapping/CnRegisterMapping.vue +792 -0
  25. package/src/components/CnRegisterMapping/index.js +1 -0
  26. package/src/components/index.js +8 -4
  27. package/src/constants/metadata.js +30 -0
  28. package/src/css/actions-bar.css +48 -0
  29. package/src/css/badge.css +4 -4
  30. package/src/css/card.css +23 -23
  31. package/src/css/detail.css +13 -13
  32. package/src/css/index-page.css +32 -0
  33. package/src/css/index-sidebar.css +187 -0
  34. package/src/css/index.css +4 -0
  35. package/src/css/layout.css +14 -14
  36. package/src/css/page-header.css +33 -0
  37. package/src/css/pagination.css +12 -12
  38. package/src/css/table.css +21 -22
  39. package/src/css/utilities.css +2 -2
  40. package/src/index.js +11 -8
  41. package/src/store/plugins/index.js +1 -0
  42. package/src/store/plugins/registerMapping.js +185 -0
  43. package/src/store/useObjectStore.js +122 -61
  44. package/src/utils/headers.js +7 -1
  45. package/src/utils/index.js +1 -1
  46. package/src/utils/schema.js +133 -1
  47. package/src/components/CnDetailViewLayout/CnDetailViewLayout.vue +0 -88
  48. package/src/components/CnDetailViewLayout/index.js +0 -1
  49. package/src/components/CnEmptyState/CnEmptyState.vue +0 -78
  50. package/src/components/CnEmptyState/index.js +0 -1
  51. package/src/components/CnListViewLayout/CnListViewLayout.vue +0 -80
  52. package/src/components/CnListViewLayout/index.js +0 -1
  53. package/src/components/CnViewModeToggle/CnViewModeToggle.vue +0 -77
  54. package/src/components/CnViewModeToggle/index.js +0 -1
@@ -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'
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Schema utility functions for auto-generating table columns, cell formatting,
3
- * and faceted filter definitions from OpenRegister schema property definitions.
3
+ * form field definitions, and faceted filter definitions from OpenRegister
4
+ * schema property definitions.
4
5
  *
5
6
  * @module utils/schema
6
7
  */
@@ -236,6 +237,136 @@ function truncateString(str, maxLength) {
236
237
  return str.substring(0, maxLength) + '...'
237
238
  }
238
239
 
240
+ /**
241
+ * Resolve the form widget type for a JSON Schema property.
242
+ *
243
+ * Resolution priority (first match wins):
244
+ * 1. Explicit `prop.widget` — pass-through custom widget name
245
+ * 2. `prop.enum` → `'select'`
246
+ * 3. Type-based: `boolean` → `'checkbox'`, `integer`/`number` → `'number'`,
247
+ * `array` + `items.enum` → `'multiselect'`, `array` → `'tags'`
248
+ * 4. Format-based: `date-time` → `'datetime'`, `date` → `'date'`,
249
+ * `email` → `'email'`, `uri`/`url` → `'url'`,
250
+ * `markdown`/`textarea` → `'textarea'`
251
+ * 5. Long text: `maxLength > 255` → `'textarea'`
252
+ * 6. Fallback → `'text'`
253
+ *
254
+ * @param {object} prop The schema property definition (type, format, enum, widget, items, maxLength)
255
+ * @return {string} Widget identifier: 'text'|'email'|'url'|'number'|'checkbox'|'select'|'multiselect'|'tags'|'textarea'|'date'|'datetime' or a custom string
256
+ */
257
+ function resolveWidget(prop) {
258
+ // Explicit widget hint takes priority
259
+ if (prop.widget) return prop.widget
260
+
261
+ // Enum → select
262
+ if (prop.enum) return 'select'
263
+
264
+ const type = prop.type || 'string'
265
+ const format = prop.format || ''
266
+
267
+ // Boolean → switch/checkbox
268
+ if (type === 'boolean') return 'checkbox'
269
+
270
+ // Number types
271
+ if (type === 'integer' || type === 'number') return 'number'
272
+
273
+ // Array types
274
+ if (type === 'array') {
275
+ if (prop.items && prop.items.enum) return 'multiselect'
276
+ return 'tags'
277
+ }
278
+
279
+ // Format-based widgets
280
+ if (format === 'date-time') return 'datetime'
281
+ if (format === 'date') return 'date'
282
+ if (format === 'email') return 'email'
283
+ if (format === 'uri' || format === 'url') return 'url'
284
+ if (format === 'markdown' || format === 'textarea') return 'textarea'
285
+
286
+ // Long text → textarea
287
+ if (prop.maxLength && prop.maxLength > 255) return 'textarea'
288
+
289
+ return 'text'
290
+ }
291
+
292
+ /**
293
+ * Generate form field definitions from a schema's properties.
294
+ *
295
+ * Reads `schema.properties` and creates field descriptor objects suitable
296
+ * for auto-generating form UIs. Follows the same pattern as
297
+ * `columnsFromSchema()` — filters, sorts, and supports overrides.
298
+ *
299
+ * @param {object} schema The schema object with a `properties` field
300
+ * @param {object} [options] Configuration options
301
+ * @param {string[]} [options.exclude] Property keys to exclude
302
+ * @param {string[]} [options.include] Property keys to include (whitelist mode)
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
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
+ */
307
+ export function fieldsFromSchema(schema, options = {}) {
308
+ const { exclude = [], include = null, overrides = {}, includeReadOnly = false } = options
309
+
310
+ if (!schema || !schema.properties) {
311
+ return []
312
+ }
313
+
314
+ const requiredKeys = Array.isArray(schema.required) ? schema.required : []
315
+
316
+ const entries = Object.entries(schema.properties)
317
+ .filter(([key, prop]) => {
318
+ // Skip properties marked as not visible
319
+ if (prop.visible === false) return false
320
+ // Skip readOnly properties by default
321
+ if (prop.readOnly === true && !includeReadOnly) return false
322
+ // Apply exclude list
323
+ if (exclude.includes(key)) return false
324
+ // Apply include whitelist
325
+ if (include && !include.includes(key)) return false
326
+ // Skip complex object types (not supported in auto-form)
327
+ if (prop.type === 'object') return false
328
+ return true
329
+ })
330
+ .sort(([keyA, propA], [keyB, propB]) => {
331
+ // Sort by order hint first, then alphabetically
332
+ const orderA = typeof propA.order === 'number' ? propA.order : Infinity
333
+ const orderB = typeof propB.order === 'number' ? propB.order : Infinity
334
+ if (orderA !== orderB) return orderA - orderB
335
+ return keyA.localeCompare(keyB)
336
+ })
337
+
338
+ return entries.map(([key, prop]) => {
339
+ const field = {
340
+ key,
341
+ label: prop.title || key,
342
+ description: prop.description || '',
343
+ type: prop.type || 'string',
344
+ format: prop.format || null,
345
+ widget: resolveWidget(prop),
346
+ required: requiredKeys.includes(key),
347
+ readOnly: prop.readOnly || false,
348
+ default: prop.default !== undefined ? prop.default : null,
349
+ enum: prop.enum || null,
350
+ items: prop.items || null,
351
+ validation: {
352
+ minLength: prop.minLength,
353
+ maxLength: prop.maxLength,
354
+ minimum: prop.minimum,
355
+ maximum: prop.maximum,
356
+ pattern: prop.pattern,
357
+ },
358
+ order: typeof prop.order === 'number' ? prop.order : Infinity,
359
+ }
360
+
361
+ // Apply per-field overrides
362
+ if (overrides[key]) {
363
+ Object.assign(field, overrides[key])
364
+ }
365
+
366
+ return field
367
+ })
368
+ }
369
+
239
370
  /**
240
371
  * Generate faceted filter definitions from a schema's facetable properties.
241
372
  *
@@ -263,6 +394,7 @@ export function filtersFromSchema(schema) {
263
394
  const filter = {
264
395
  key,
265
396
  label: prop.title || key,
397
+ description: prop.description || '',
266
398
  propertyType: prop.type || 'string',
267
399
  options: [],
268
400
  value: null,
@@ -1,88 +0,0 @@
1
- <template>
2
- <div class="cn-detail-layout">
3
- <!-- Header -->
4
- <div class="cn-detail-layout__header">
5
- <NcButton @click="$emit('back')">
6
- <template #icon>
7
- <ArrowLeft :size="20" />
8
- </template>
9
- {{ backLabel }}
10
- </NcButton>
11
-
12
- <h2 class="cn-detail-layout__title">
13
- <slot name="title">{{ title }}</slot>
14
- </h2>
15
-
16
- <div class="cn-detail-layout__actions">
17
- <slot name="actions" />
18
- </div>
19
- </div>
20
-
21
- <!-- Loading state -->
22
- <div v-if="loading" class="cn-loading-container">
23
- <NcLoadingIcon :size="32" />
24
- </div>
25
-
26
- <!-- Main content -->
27
- <div v-else class="cn-detail-layout__content">
28
- <slot />
29
- </div>
30
-
31
- <!-- Delete confirmation dialog -->
32
- <slot name="dialogs" />
33
- </div>
34
- </template>
35
-
36
- <script>
37
- import { NcButton, NcLoadingIcon } from '@nextcloud/vue'
38
- import ArrowLeft from 'vue-material-design-icons/ArrowLeft.vue'
39
-
40
- /**
41
- * CnDetailViewLayout — Detail page layout with back button, title, actions, and content.
42
- *
43
- * Provides the standard structure for detail/edit views: back navigation,
44
- * page title, action buttons, and a content area. Supports loading state
45
- * and a dialogs slot for modals.
46
- *
47
- * @example
48
- * <CnDetailViewLayout
49
- * title="Client: Acme Corp"
50
- * :loading="isLoading"
51
- * @back="goBack">
52
- * <template #actions>
53
- * <NcButton @click="edit">Edit</NcButton>
54
- * <NcButton type="error" @click="confirmDelete">Delete</NcButton>
55
- * </template>
56
- * <div class="cn-detail-grid">
57
- * <div class="cn-detail-item">...</div>
58
- * </div>
59
- * </CnDetailViewLayout>
60
- */
61
- export default {
62
- name: 'CnDetailViewLayout',
63
-
64
- components: {
65
- NcButton,
66
- NcLoadingIcon,
67
- ArrowLeft,
68
- },
69
-
70
- props: {
71
- /** Page title */
72
- title: {
73
- type: String,
74
- default: '',
75
- },
76
- /** Whether data is loading */
77
- loading: {
78
- type: Boolean,
79
- default: false,
80
- },
81
- /** Back button label */
82
- backLabel: {
83
- type: String,
84
- default: 'Back',
85
- },
86
- },
87
- }
88
- </script>
@@ -1 +0,0 @@
1
- export { default as CnDetailViewLayout } from './CnDetailViewLayout.vue'