@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.
- package/README.md +226 -0
- package/dist/nextcloud-vue.cjs.js +7039 -2409
- package/dist/nextcloud-vue.cjs.js.map +1 -1
- package/dist/nextcloud-vue.css +237 -52
- package/dist/nextcloud-vue.esm.js +7012 -2386
- package/dist/nextcloud-vue.esm.js.map +1 -1
- package/package.json +2 -4
- package/src/components/CnActionsBar/CnActionsBar.vue +225 -0
- package/src/components/CnActionsBar/index.js +1 -0
- package/src/components/CnCopyDialog/CnCopyDialog.vue +250 -0
- package/src/components/CnCopyDialog/index.js +1 -0
- package/src/components/CnDataTable/CnDataTable.vue +0 -5
- package/src/components/CnDeleteDialog/CnDeleteDialog.vue +170 -0
- package/src/components/CnDeleteDialog/index.js +1 -0
- package/src/components/CnFormDialog/CnFormDialog.vue +629 -0
- package/src/components/CnFormDialog/index.js +1 -0
- package/src/components/CnIcon/CnIcon.vue +89 -0
- package/src/components/CnIcon/index.js +1 -0
- package/src/components/CnIndexPage/CnIndexPage.vue +434 -300
- package/src/components/CnIndexSidebar/CnIndexSidebar.vue +484 -0
- package/src/components/CnIndexSidebar/index.js +1 -0
- package/src/components/CnPageHeader/CnPageHeader.vue +57 -0
- package/src/components/CnPageHeader/index.js +1 -0
- package/src/components/CnRegisterMapping/CnRegisterMapping.vue +792 -0
- package/src/components/CnRegisterMapping/index.js +1 -0
- package/src/components/index.js +8 -4
- package/src/constants/metadata.js +30 -0
- package/src/css/actions-bar.css +48 -0
- package/src/css/badge.css +4 -4
- package/src/css/card.css +23 -23
- package/src/css/detail.css +13 -13
- package/src/css/index-page.css +32 -0
- package/src/css/index-sidebar.css +187 -0
- package/src/css/index.css +4 -0
- package/src/css/layout.css +14 -14
- package/src/css/page-header.css +33 -0
- package/src/css/pagination.css +12 -12
- package/src/css/table.css +21 -22
- package/src/css/utilities.css +2 -2
- package/src/index.js +11 -8
- package/src/store/plugins/index.js +1 -0
- package/src/store/plugins/registerMapping.js +185 -0
- package/src/store/useObjectStore.js +122 -61
- package/src/utils/headers.js +7 -1
- package/src/utils/index.js +1 -1
- package/src/utils/schema.js +133 -1
- package/src/components/CnDetailViewLayout/CnDetailViewLayout.vue +0 -88
- package/src/components/CnDetailViewLayout/index.js +0 -1
- package/src/components/CnEmptyState/CnEmptyState.vue +0 -78
- package/src/components/CnEmptyState/index.js +0 -1
- package/src/components/CnListViewLayout/CnListViewLayout.vue +0 -80
- package/src/components/CnListViewLayout/index.js +0 -1
- package/src/components/CnViewModeToggle/CnViewModeToggle.vue +0 -77
- 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
|
-
|
|
195
|
-
|
|
196
|
-
this.
|
|
197
|
-
this.
|
|
198
|
-
this.
|
|
199
|
-
this.
|
|
200
|
-
this.
|
|
201
|
-
this.
|
|
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]
|
|
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]
|
|
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]
|
|
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]
|
|
318
|
-
this.errors[type]
|
|
329
|
+
this.loading = { ...this.loading, [type]: true }
|
|
330
|
+
this.errors = { ...this.errors, [type]: null }
|
|
319
331
|
|
|
320
332
|
try {
|
|
321
|
-
|
|
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]
|
|
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
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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
|
|
389
|
+
return results
|
|
345
390
|
} catch (error) {
|
|
346
|
-
this.errors
|
|
347
|
-
|
|
348
|
-
:
|
|
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]
|
|
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]
|
|
365
|
-
this.errors[type]
|
|
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]
|
|
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
|
-
|
|
384
|
-
this.objects
|
|
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
|
|
391
|
-
|
|
392
|
-
:
|
|
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]
|
|
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]
|
|
409
|
-
this.errors[type]
|
|
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]
|
|
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
|
-
|
|
433
|
-
this.objects
|
|
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
|
|
441
|
-
|
|
442
|
-
:
|
|
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]
|
|
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]
|
|
459
|
-
this.errors[type]
|
|
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]
|
|
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
|
-
|
|
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
|
|
480
|
-
|
|
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
|
|
487
|
-
|
|
488
|
-
:
|
|
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]
|
|
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
|
-
|
|
531
|
-
|
|
589
|
+
this.objects = {
|
|
590
|
+
...this.objects,
|
|
591
|
+
[type]: { ...(this.objects[type] || {}), [id]: data },
|
|
592
|
+
}
|
|
532
593
|
result[id] = data
|
|
533
594
|
}
|
|
534
595
|
} catch {
|
package/src/utils/headers.js
CHANGED
|
@@ -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 (
|
|
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))
|
package/src/utils/index.js
CHANGED
|
@@ -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'
|
package/src/utils/schema.js
CHANGED
|
@@ -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
|
|
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'
|