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

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 (153) hide show
  1. package/README.md +226 -226
  2. package/css/index.css +5 -0
  3. package/dist/nextcloud-vue.cjs.js +60455 -8755
  4. package/dist/nextcloud-vue.cjs.js.map +1 -1
  5. package/dist/nextcloud-vue.css +2062 -528
  6. package/dist/nextcloud-vue.esm.js +60411 -8731
  7. package/dist/nextcloud-vue.esm.js.map +1 -1
  8. package/package.json +75 -61
  9. package/src/components/CnActionsBar/CnActionsBar.vue +235 -225
  10. package/src/components/CnActionsBar/index.js +1 -1
  11. package/src/components/CnAdvancedFormDialog/CnAdvancedFormDialog.vue +579 -0
  12. package/src/components/CnAdvancedFormDialog/CnDataTab.vue +217 -0
  13. package/src/components/CnAdvancedFormDialog/CnMetadataTab.vue +121 -0
  14. package/src/components/CnAdvancedFormDialog/CnPropertiesTab.vue +418 -0
  15. package/src/components/CnAdvancedFormDialog/CnPropertyValueCell.vue +247 -0
  16. package/src/components/CnAdvancedFormDialog/index.js +1 -0
  17. package/src/components/CnCardGrid/CnCardGrid.vue +152 -152
  18. package/src/components/CnCardGrid/index.js +1 -1
  19. package/src/components/CnCellRenderer/CnCellRenderer.vue +132 -132
  20. package/src/components/CnCellRenderer/index.js +1 -1
  21. package/src/components/CnChartWidget/CnChartWidget.vue +320 -0
  22. package/src/components/CnChartWidget/index.js +1 -0
  23. package/src/components/CnConfigurationCard/CnConfigurationCard.vue +77 -77
  24. package/src/components/CnConfigurationCard/index.js +1 -1
  25. package/src/components/CnDashboardGrid/CnDashboardGrid.vue +225 -0
  26. package/src/components/CnDashboardGrid/index.js +1 -0
  27. package/src/components/CnDashboardPage/CnDashboardPage.vue +390 -0
  28. package/src/components/CnDashboardPage/index.js +1 -0
  29. package/src/components/CnDataTable/CnDataTable.vue +349 -349
  30. package/src/components/CnDataTable/index.js +1 -1
  31. package/src/components/CnDetailCard/CnDetailCard.vue +214 -0
  32. package/src/components/CnDetailCard/index.js +1 -0
  33. package/src/components/CnDetailPage/CnDetailPage.vue +281 -0
  34. package/src/components/CnDetailPage/index.js +1 -0
  35. package/src/components/CnFacetSidebar/CnFacetSidebar.vue +231 -223
  36. package/src/components/CnFacetSidebar/index.js +1 -1
  37. package/src/components/CnFilterBar/CnFilterBar.vue +152 -152
  38. package/src/components/CnFilterBar/index.js +1 -1
  39. package/src/components/CnIcon/CnIcon.vue +89 -89
  40. package/src/components/CnIcon/index.js +1 -1
  41. package/src/components/CnIndexPage/CnIndexPage.vue +874 -816
  42. package/src/components/CnIndexPage/index.js +1 -1
  43. package/src/components/CnIndexSidebar/CnIndexSidebar.vue +503 -484
  44. package/src/components/CnIndexSidebar/index.js +1 -1
  45. package/src/components/CnItemCard/CnItemCard.vue +132 -0
  46. package/src/components/CnItemCard/index.js +1 -0
  47. package/src/components/CnKpiGrid/CnKpiGrid.vue +89 -89
  48. package/src/components/CnKpiGrid/index.js +1 -1
  49. package/src/components/CnMassActionBar/CnMassActionBar.vue +160 -160
  50. package/src/components/CnMassActionBar/index.js +1 -1
  51. package/src/components/CnMassCopyDialog/CnMassCopyDialog.vue +320 -320
  52. package/src/components/CnMassCopyDialog/index.js +1 -1
  53. package/src/components/CnMassDeleteDialog/CnMassDeleteDialog.vue +238 -238
  54. package/src/components/CnMassDeleteDialog/index.js +1 -1
  55. package/src/components/CnMassExportDialog/CnMassExportDialog.vue +190 -190
  56. package/src/components/CnMassExportDialog/index.js +1 -1
  57. package/src/components/CnMassImportDialog/CnMassImportDialog.vue +491 -491
  58. package/src/components/CnMassImportDialog/index.js +1 -1
  59. package/src/components/CnNoteCard/CnNoteCard.vue +149 -0
  60. package/src/components/CnNoteCard/index.js +1 -0
  61. package/src/components/CnNotesCard/CnNotesCard.vue +413 -0
  62. package/src/components/CnNotesCard/index.js +1 -0
  63. package/src/components/CnObjectCard/CnObjectCard.vue +292 -292
  64. package/src/components/CnObjectCard/index.js +1 -1
  65. package/src/components/CnObjectSidebar/CnObjectSidebar.vue +876 -0
  66. package/src/components/CnObjectSidebar/index.js +1 -0
  67. package/src/components/CnPageHeader/CnPageHeader.vue +57 -57
  68. package/src/components/CnPageHeader/index.js +1 -1
  69. package/src/components/CnPagination/CnPagination.vue +252 -252
  70. package/src/components/CnPagination/index.js +1 -1
  71. package/src/components/CnRowActions/CnRowActions.vue +73 -73
  72. package/src/components/CnRowActions/index.js +1 -1
  73. package/src/components/CnSchemaFormDialog/CnSchemaConfigurationTab.vue +226 -0
  74. package/src/components/CnSchemaFormDialog/CnSchemaFormDialog.vue +787 -0
  75. package/src/components/CnSchemaFormDialog/CnSchemaPropertiesTab.vue +305 -0
  76. package/src/components/CnSchemaFormDialog/CnSchemaPropertyActions.vue +1398 -0
  77. package/src/components/CnSchemaFormDialog/CnSchemaSecurityTab.vue +236 -0
  78. package/src/components/CnSchemaFormDialog/index.js +1 -0
  79. package/src/components/CnSettingsCard/CnSettingsCard.vue +92 -92
  80. package/src/components/CnSettingsCard/index.js +1 -1
  81. package/src/components/CnSettingsSection/CnSettingsSection.vue +266 -266
  82. package/src/components/CnSettingsSection/index.js +1 -1
  83. package/src/components/CnStatsBlock/CnStatsBlock.vue +420 -366
  84. package/src/components/CnStatsBlock/index.js +1 -1
  85. package/src/components/CnStatusBadge/CnStatusBadge.vue +77 -77
  86. package/src/components/CnStatusBadge/index.js +1 -1
  87. package/src/components/CnTabbedFormDialog/CnTabbedFormDialog.vue +540 -0
  88. package/src/components/CnTabbedFormDialog/index.js +1 -0
  89. package/src/components/CnTasksCard/CnTasksCard.vue +373 -0
  90. package/src/components/CnTasksCard/index.js +1 -0
  91. package/src/components/CnTileWidget/CnTileWidget.vue +159 -0
  92. package/src/components/CnTileWidget/index.js +1 -0
  93. package/src/components/CnTimelineStages/CnTimelineStages.vue +292 -0
  94. package/src/components/CnTimelineStages/index.js +1 -0
  95. package/src/components/CnUserActionMenu/CnUserActionMenu.vue +435 -0
  96. package/src/components/CnUserActionMenu/index.js +1 -0
  97. package/src/components/CnVersionInfoCard/CnVersionInfoCard.vue +312 -312
  98. package/src/components/CnVersionInfoCard/index.js +1 -1
  99. package/src/components/CnWidgetRenderer/CnWidgetRenderer.vue +180 -0
  100. package/src/components/CnWidgetRenderer/index.js +1 -0
  101. package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +211 -0
  102. package/src/components/CnWidgetWrapper/index.js +1 -0
  103. package/src/components/index.js +43 -29
  104. package/src/composables/index.js +4 -3
  105. package/src/composables/useDashboardView.js +240 -0
  106. package/src/composables/useDetailView.js +289 -132
  107. package/src/composables/useListView.js +363 -153
  108. package/src/composables/useSubResource.js +142 -142
  109. package/src/constants/metadata.js +30 -30
  110. package/src/css/CnSchemaFormDialog.css +546 -0
  111. package/src/css/__sample_nextcloud_tokens.css +110 -0
  112. package/src/css/actions-bar.css +48 -48
  113. package/src/css/badge.css +51 -51
  114. package/src/css/card.css +128 -128
  115. package/src/css/dashboard.css +70 -0
  116. package/src/css/detail-page.css +168 -0
  117. package/src/css/detail.css +68 -68
  118. package/src/css/index-page.css +44 -32
  119. package/src/css/index-sidebar.css +193 -187
  120. package/src/css/index.css +16 -12
  121. package/src/css/layout.css +90 -90
  122. package/src/css/page-header.css +33 -33
  123. package/src/css/pagination.css +72 -72
  124. package/src/css/table.css +142 -142
  125. package/src/css/timeline-stages.css +218 -0
  126. package/src/css/utilities.css +46 -46
  127. package/src/index.js +72 -53
  128. package/src/store/createSubResourcePlugin.js +135 -135
  129. package/src/store/index.js +3 -3
  130. package/src/store/plugins/auditTrails.js +17 -17
  131. package/src/store/plugins/files.js +250 -186
  132. package/src/store/plugins/index.js +7 -5
  133. package/src/store/plugins/lifecycle.js +180 -180
  134. package/src/store/plugins/relations.js +68 -68
  135. package/src/store/plugins/search.js +372 -0
  136. package/src/store/plugins/selection.js +104 -0
  137. package/src/store/useObjectStore.js +829 -686
  138. package/src/types/auditTrail.d.ts +32 -32
  139. package/src/types/file.d.ts +23 -23
  140. package/src/types/index.d.ts +35 -35
  141. package/src/types/notification.d.ts +36 -36
  142. package/src/types/object.d.ts +40 -40
  143. package/src/types/organisation.d.ts +41 -41
  144. package/src/types/register.d.ts +25 -25
  145. package/src/types/schema.d.ts +39 -39
  146. package/src/types/shared.d.ts +79 -79
  147. package/src/types/source.d.ts +14 -14
  148. package/src/types/task.d.ts +31 -31
  149. package/src/utils/errors.js +96 -96
  150. package/src/utils/headers.js +68 -50
  151. package/src/utils/id.js +13 -0
  152. package/src/utils/index.js +3 -3
  153. package/src/utils/schema.js +422 -419
@@ -1,419 +1,422 @@
1
- /**
2
- * Schema utility functions for auto-generating table columns, cell formatting,
3
- * form field definitions, and faceted filter definitions from OpenRegister
4
- * schema property definitions.
5
- *
6
- * @module utils/schema
7
- */
8
-
9
- /**
10
- * Default column widths per property type/format.
11
- */
12
- const DEFAULT_WIDTHS = {
13
- boolean: '80px',
14
- integer: '100px',
15
- number: '100px',
16
- 'string:uuid': '140px',
17
- 'string:date-time': '180px',
18
- 'string:email': '200px',
19
- }
20
-
21
- /**
22
- * Get default width for a property type + format combination.
23
- *
24
- * @param {string} type Property type
25
- * @param {string} [format] Property format
26
- * @return {string|undefined} CSS width or undefined
27
- */
28
- function getDefaultWidth(type, format) {
29
- if (format) {
30
- return DEFAULT_WIDTHS[`${type}:${format}`]
31
- }
32
- return DEFAULT_WIDTHS[type]
33
- }
34
-
35
- /**
36
- * Generate CnDataTable column definitions from a schema's properties.
37
- *
38
- * Reads `schema.properties` and creates column objects sorted by the `order`
39
- * hint (if present) then alphabetically. Filters out properties marked
40
- * `visible: false`. Supports include/exclude lists and per-column overrides.
41
- *
42
- * @param {object} schema The schema object with a `properties` field
43
- * @param {object} [options] Configuration options
44
- * @param {string[]} [options.exclude] Property keys to exclude
45
- * @param {string[]} [options.include] Property keys to include (whitelist mode)
46
- * @param {object} [options.overrides] Per-key column overrides, e.g. `{ status: { width: '200px' } }`
47
- * @return {Array<{key: string, label: string, sortable: boolean, type: string, format: string, width: string}>}
48
- */
49
- export function columnsFromSchema(schema, options = {}) {
50
- const { exclude = [], include = null, overrides = {} } = options
51
-
52
- if (!schema || !schema.properties) {
53
- return []
54
- }
55
-
56
- const entries = Object.entries(schema.properties)
57
- .filter(([key, prop]) => {
58
- // Skip properties marked as not visible
59
- if (prop.visible === false) return false
60
- // Apply exclude list
61
- if (exclude.includes(key)) return false
62
- // Apply include whitelist
63
- if (include && !include.includes(key)) return false
64
- // Skip complex object types by default (they don't render well in tables)
65
- if (prop.type === 'object') return false
66
- return true
67
- })
68
- .sort(([keyA, propA], [keyB, propB]) => {
69
- // Sort by order hint first, then alphabetically
70
- const orderA = typeof propA.order === 'number' ? propA.order : Infinity
71
- const orderB = typeof propB.order === 'number' ? propB.order : Infinity
72
- if (orderA !== orderB) return orderA - orderB
73
- return keyA.localeCompare(keyB)
74
- })
75
-
76
- return entries.map(([key, prop]) => {
77
- const column = {
78
- key,
79
- label: prop.title || key,
80
- sortable: true,
81
- type: prop.type || 'string',
82
- format: prop.format || null,
83
- }
84
-
85
- // Apply default width
86
- const defaultWidth = getDefaultWidth(column.type, column.format)
87
- if (defaultWidth) {
88
- column.width = defaultWidth
89
- }
90
-
91
- // Store enum values for cell renderer
92
- if (prop.enum) {
93
- column.enum = prop.enum
94
- }
95
-
96
- // Store items type for arrays
97
- if (prop.items) {
98
- column.items = prop.items
99
- }
100
-
101
- // Apply per-column overrides
102
- if (overrides[key]) {
103
- Object.assign(column, overrides[key])
104
- }
105
-
106
- return column
107
- })
108
- }
109
-
110
- /**
111
- * Format a cell value based on its schema property definition.
112
- *
113
- * Handles dates, booleans, arrays, numbers, UUIDs, emails, and markdown.
114
- * Returns a plain string suitable for display in a table cell.
115
- *
116
- * @param {*} value The raw value
117
- * @param {object} [property] The schema property definition `{ type, format, enum, items }`
118
- * @param {object} [options] Formatting options
119
- * @param {number} [options.truncate=100] Maximum string length before truncation
120
- * @return {string} Formatted display string
121
- */
122
- export function formatValue(value, property = {}, options = {}) {
123
- const { truncate = 100 } = options
124
-
125
- // Null/undefined/empty
126
- if (value === null || value === undefined || value === '') {
127
- return '—'
128
- }
129
-
130
- const { type = 'string', format } = property
131
-
132
- // Boolean
133
- if (type === 'boolean' || typeof value === 'boolean') {
134
- return value ? '✓' : '—'
135
- }
136
-
137
- // Number/Integer
138
- if (type === 'integer' || type === 'number') {
139
- const num = Number(value)
140
- if (Number.isNaN(num)) return String(value)
141
- return num.toLocaleString()
142
- }
143
-
144
- // Array
145
- if (type === 'array' || Array.isArray(value)) {
146
- if (!Array.isArray(value)) return String(value)
147
- if (value.length === 0) return '—'
148
- // For short arrays, join values
149
- if (value.length <= 3) {
150
- return value.join(', ')
151
- }
152
- return `${value.slice(0, 3).join(', ')} +${value.length - 3}`
153
- }
154
-
155
- // Object (shouldn't normally appear in tables)
156
- if (type === 'object' || (typeof value === 'object' && value !== null)) {
157
- return '[Object]'
158
- }
159
-
160
- // String types
161
- const str = String(value)
162
-
163
- // Date-time
164
- if (format === 'date-time' || format === 'date') {
165
- try {
166
- const date = new Date(str)
167
- if (Number.isNaN(date.getTime())) return str
168
- if (format === 'date') {
169
- return date.toLocaleDateString(undefined, {
170
- day: '2-digit',
171
- month: '2-digit',
172
- year: 'numeric',
173
- })
174
- }
175
- return date.toLocaleDateString(undefined, {
176
- day: '2-digit',
177
- month: '2-digit',
178
- year: 'numeric',
179
- }) + ', ' + date.toLocaleTimeString(undefined, {
180
- hour: '2-digit',
181
- minute: '2-digit',
182
- second: '2-digit',
183
- })
184
- } catch {
185
- return str
186
- }
187
- }
188
-
189
- // UUID — truncate to first 8 chars
190
- if (format === 'uuid') {
191
- if (str.length > 8) {
192
- return str.substring(0, 8) + '...'
193
- }
194
- return str
195
- }
196
-
197
- // URI — show truncated
198
- if (format === 'uri' || format === 'url') {
199
- try {
200
- const url = new URL(str)
201
- return url.hostname + url.pathname.substring(0, 20)
202
- } catch {
203
- return truncateString(str, truncate)
204
- }
205
- }
206
-
207
- // Markdown — strip formatting
208
- if (format === 'markdown') {
209
- const stripped = str
210
- .replace(/#{1,6}\s+/g, '') // headings
211
- .replace(/[*_]{1,3}([^*_]+)[*_]{1,3}/g, '$1') // bold/italic
212
- .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') // links
213
- .replace(/`{1,3}[^`]*`{1,3}/g, '') // code
214
- .replace(/\n+/g, ' ') // newlines
215
- .trim()
216
- return truncateString(stripped, truncate)
217
- }
218
-
219
- // Email — display as-is (no truncation)
220
- if (format === 'email') {
221
- return str
222
- }
223
-
224
- // Plain string — truncate if needed
225
- return truncateString(str, truncate)
226
- }
227
-
228
- /**
229
- * Truncate a string to the given length, adding ellipsis if needed.
230
- *
231
- * @param {string} str The string to truncate
232
- * @param {number} maxLength Maximum length
233
- * @return {string} Truncated string
234
- */
235
- function truncateString(str, maxLength) {
236
- if (str.length <= maxLength) return str
237
- return str.substring(0, maxLength) + '...'
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
-
370
- /**
371
- * Generate faceted filter definitions from a schema's facetable properties.
372
- *
373
- * Reads `schema.properties` and creates filter definitions for properties
374
- * marked with `facetable: true`. Maps property types to appropriate filter
375
- * widget types (select, checkbox, text).
376
- *
377
- * @param {object} schema The schema object with a `properties` field
378
- * @return {Array<{key: string, label: string, type: string, propertyType: string, options: Array}>}
379
- */
380
- export function filtersFromSchema(schema) {
381
- if (!schema || !schema.properties) {
382
- return []
383
- }
384
-
385
- return Object.entries(schema.properties)
386
- .filter(([, prop]) => prop.facetable === true)
387
- .sort(([keyA, propA], [keyB, propB]) => {
388
- const orderA = typeof propA.order === 'number' ? propA.order : Infinity
389
- const orderB = typeof propB.order === 'number' ? propB.order : Infinity
390
- if (orderA !== orderB) return orderA - orderB
391
- return keyA.localeCompare(keyB)
392
- })
393
- .map(([key, prop]) => {
394
- const filter = {
395
- key,
396
- label: prop.title || key,
397
- description: prop.description || '',
398
- propertyType: prop.type || 'string',
399
- options: [],
400
- value: null,
401
- }
402
-
403
- // Map property type to filter widget type
404
- if (prop.type === 'boolean') {
405
- filter.type = 'checkbox'
406
- } else if (prop.enum) {
407
- filter.type = 'select'
408
- filter.options = prop.enum.map((val) => ({
409
- id: val,
410
- label: val,
411
- }))
412
- } else {
413
- // Default to select — options loaded dynamically from facet API
414
- filter.type = 'select'
415
- }
416
-
417
- return filter
418
- })
419
- }
1
+ /**
2
+ * Schema utility functions for auto-generating table columns, cell formatting,
3
+ * form field definitions, and faceted filter definitions from OpenRegister
4
+ * schema property definitions.
5
+ *
6
+ * @module utils/schema
7
+ */
8
+
9
+ /**
10
+ * Default column widths per property type/format.
11
+ */
12
+ const DEFAULT_WIDTHS = {
13
+ boolean: '80px',
14
+ integer: '100px',
15
+ number: '100px',
16
+ 'string:uuid': '140px',
17
+ 'string:date-time': '180px',
18
+ 'string:email': '200px',
19
+ }
20
+
21
+ /**
22
+ * Get default width for a property type + format combination.
23
+ *
24
+ * @param {string} type Property type
25
+ * @param {string} [format] Property format
26
+ * @return {string|undefined} CSS width or undefined
27
+ */
28
+ function getDefaultWidth(type, format) {
29
+ if (format) {
30
+ return DEFAULT_WIDTHS[`${type}:${format}`]
31
+ }
32
+ return DEFAULT_WIDTHS[type]
33
+ }
34
+
35
+ /**
36
+ * Generate CnDataTable column definitions from a schema's properties.
37
+ *
38
+ * Reads `schema.properties` and creates column objects sorted by the `order`
39
+ * hint (if present) then alphabetically. Filters out properties marked
40
+ * `visible: false`. Supports include/exclude lists and per-column overrides.
41
+ *
42
+ * @param {object} schema The schema object with a `properties` field
43
+ * @param {object} [options] Configuration options
44
+ * @param {string[]} [options.exclude] Property keys to exclude
45
+ * @param {string[]} [options.include] Property keys to include (whitelist mode)
46
+ * @param {object} [options.overrides] Per-key column overrides, e.g. `{ status: { width: '200px' } }`
47
+ * @return {Array<{key: string, label: string, sortable: boolean, type: string, format: string, width: string}>}
48
+ */
49
+ export function columnsFromSchema(schema, options = {}) {
50
+ const { exclude = [], include = null, overrides = {} } = options
51
+
52
+ if (!schema || !schema.properties) {
53
+ return []
54
+ }
55
+
56
+ const entries = Object.entries(schema.properties)
57
+ .filter(([key, prop]) => {
58
+ // Skip properties marked as not visible
59
+ if (prop.visible === false) return false
60
+ // Apply exclude list
61
+ if (exclude.includes(key)) return false
62
+ // Apply include whitelist
63
+ if (include && !include.includes(key)) return false
64
+ // Skip complex object types by default (they don't render well in tables)
65
+ if (prop.type === 'object') return false
66
+ return true
67
+ })
68
+ .sort(([keyA, propA], [keyB, propB]) => {
69
+ // Sort by order hint first, then alphabetically
70
+ const orderA = typeof propA.order === 'number' ? propA.order : Infinity
71
+ const orderB = typeof propB.order === 'number' ? propB.order : Infinity
72
+ if (orderA !== orderB) return orderA - orderB
73
+ return keyA.localeCompare(keyB)
74
+ })
75
+
76
+ return entries.map(([key, prop]) => {
77
+ const column = {
78
+ key,
79
+ label: prop.title || key,
80
+ sortable: true,
81
+ type: prop.type || 'string',
82
+ format: prop.format || null,
83
+ }
84
+
85
+ // Apply default width
86
+ const defaultWidth = getDefaultWidth(column.type, column.format)
87
+ if (defaultWidth) {
88
+ column.width = defaultWidth
89
+ }
90
+
91
+ // Store enum values for cell renderer
92
+ if (prop.enum) {
93
+ column.enum = prop.enum
94
+ }
95
+
96
+ // Store items type for arrays
97
+ if (prop.items) {
98
+ column.items = prop.items
99
+ }
100
+
101
+ // Apply per-column overrides
102
+ if (overrides[key]) {
103
+ Object.assign(column, overrides[key])
104
+ }
105
+
106
+ return column
107
+ })
108
+ }
109
+
110
+ /**
111
+ * Format a cell value based on its schema property definition.
112
+ *
113
+ * Handles dates, booleans, arrays, numbers, UUIDs, emails, and markdown.
114
+ * Returns a plain string suitable for display in a table cell.
115
+ *
116
+ * @param {*} value The raw value
117
+ * @param {object} [property] The schema property definition `{ type, format, enum, items }`
118
+ * @param {object} [options] Formatting options
119
+ * @param {number} [options.truncate=100] Maximum string length before truncation
120
+ * @return {string} Formatted display string
121
+ */
122
+ export function formatValue(value, property = {}, options = {}) {
123
+ const { truncate = 100 } = options
124
+
125
+ // Null/undefined/empty
126
+ if (value === null || value === undefined || value === '') {
127
+ return '—'
128
+ }
129
+
130
+ const { type = 'string', format } = property
131
+
132
+ // Boolean
133
+ if (type === 'boolean' || typeof value === 'boolean') {
134
+ return value ? '✓' : '—'
135
+ }
136
+
137
+ // Number/Integer
138
+ if (type === 'integer' || type === 'number') {
139
+ const num = Number(value)
140
+ if (Number.isNaN(num)) return String(value)
141
+ return num.toLocaleString()
142
+ }
143
+
144
+ // Array
145
+ if (type === 'array' || Array.isArray(value)) {
146
+ if (!Array.isArray(value)) return String(value)
147
+ if (value.length === 0) return '—'
148
+ // For short arrays, join values
149
+ if (value.length <= 3) {
150
+ return value.join(', ')
151
+ }
152
+ return `${value.slice(0, 3).join(', ')} +${value.length - 3}`
153
+ }
154
+
155
+ // Object (shouldn't normally appear in tables)
156
+ if (type === 'object' || (typeof value === 'object' && value !== null)) {
157
+ return '[Object]'
158
+ }
159
+
160
+ // String types
161
+ const str = String(value)
162
+
163
+ // Date-time
164
+ if (format === 'date-time' || format === 'date') {
165
+ try {
166
+ const date = new Date(str)
167
+ if (Number.isNaN(date.getTime())) return str
168
+ if (format === 'date') {
169
+ return date.toLocaleDateString(undefined, {
170
+ day: '2-digit',
171
+ month: '2-digit',
172
+ year: 'numeric',
173
+ })
174
+ }
175
+ return date.toLocaleDateString(undefined, {
176
+ day: '2-digit',
177
+ month: '2-digit',
178
+ year: 'numeric',
179
+ }) + ', ' + date.toLocaleTimeString(undefined, {
180
+ hour: '2-digit',
181
+ minute: '2-digit',
182
+ second: '2-digit',
183
+ })
184
+ } catch {
185
+ return str
186
+ }
187
+ }
188
+
189
+ // UUID — truncate to first 8 chars
190
+ if (format === 'uuid') {
191
+ if (str.length > 8) {
192
+ return str.substring(0, 8) + '...'
193
+ }
194
+ return str
195
+ }
196
+
197
+ // URI — show truncated
198
+ if (format === 'uri' || format === 'url') {
199
+ try {
200
+ const url = new URL(str)
201
+ return url.hostname + url.pathname.substring(0, 20)
202
+ } catch {
203
+ return truncateString(str, truncate)
204
+ }
205
+ }
206
+
207
+ // Markdown — strip formatting
208
+ if (format === 'markdown') {
209
+ const stripped = str
210
+ .replace(/#{1,6}\s+/g, '') // headings
211
+ .replace(/[*_]{1,3}([^*_]+)[*_]{1,3}/g, '$1') // bold/italic
212
+ .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') // links
213
+ .replace(/`{1,3}[^`]*`{1,3}/g, '') // code
214
+ .replace(/\n+/g, ' ') // newlines
215
+ .trim()
216
+ return truncateString(stripped, truncate)
217
+ }
218
+
219
+ // Email — display as-is (no truncation)
220
+ if (format === 'email') {
221
+ return str
222
+ }
223
+
224
+ // Plain string — truncate if needed
225
+ return truncateString(str, truncate)
226
+ }
227
+
228
+ /**
229
+ * Truncate a string to the given length, adding ellipsis if needed.
230
+ *
231
+ * @param {string} str The string to truncate
232
+ * @param {number} maxLength Maximum length
233
+ * @return {string} Truncated string
234
+ */
235
+ function truncateString(str, maxLength) {
236
+ if (str.length <= maxLength) return str
237
+ return str.substring(0, maxLength) + '...'
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
+
370
+ /**
371
+ * Generate faceted filter definitions from a schema's facetable properties.
372
+ *
373
+ * Reads `schema.properties` and creates filter definitions for properties
374
+ * marked with `facetable: true`. Maps property types to appropriate filter
375
+ * widget types (select, checkbox, text).
376
+ *
377
+ * @param {object} schema The schema object with a `properties` field
378
+ * @return {Array<{key: string, label: string, type: string, propertyType: string, options: Array}>}
379
+ */
380
+ export function filtersFromSchema(schema) {
381
+ if (!schema || !schema.properties) {
382
+ return []
383
+ }
384
+
385
+ return Object.entries(schema.properties)
386
+ .filter(([key, prop]) => {
387
+ if (prop.facetable !== true) return false
388
+ return true
389
+ })
390
+ .sort(([keyA, propA], [keyB, propB]) => {
391
+ const orderA = typeof propA.order === 'number' ? propA.order : Infinity
392
+ const orderB = typeof propB.order === 'number' ? propB.order : Infinity
393
+ if (orderA !== orderB) return orderA - orderB
394
+ return keyA.localeCompare(keyB)
395
+ })
396
+ .map(([key, prop]) => {
397
+ const filter = {
398
+ key,
399
+ label: prop.title || key,
400
+ description: prop.description || '',
401
+ propertyType: prop.type || 'string',
402
+ options: [],
403
+ value: null,
404
+ }
405
+
406
+ // Map property type to filter widget type
407
+ if (prop.type === 'boolean') {
408
+ filter.type = 'checkbox'
409
+ } else if (prop.enum) {
410
+ filter.type = 'select'
411
+ filter.options = prop.enum.map((val) => ({
412
+ id: val,
413
+ label: val,
414
+ }))
415
+ } else {
416
+ // Default to select — options loaded dynamically from facet API
417
+ filter.type = 'select'
418
+ }
419
+
420
+ return filter
421
+ })
422
+ }