@conduction/nextcloud-vue 0.1.0-beta.3 → 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 (152) hide show
  1. package/README.md +226 -226
  2. package/dist/nextcloud-vue.cjs.js +60455 -8755
  3. package/dist/nextcloud-vue.cjs.js.map +1 -1
  4. package/dist/nextcloud-vue.css +2062 -528
  5. package/dist/nextcloud-vue.esm.js +60411 -8731
  6. package/dist/nextcloud-vue.esm.js.map +1 -1
  7. package/package.json +75 -62
  8. package/src/components/CnActionsBar/CnActionsBar.vue +235 -225
  9. package/src/components/CnActionsBar/index.js +1 -1
  10. package/src/components/CnAdvancedFormDialog/CnAdvancedFormDialog.vue +579 -0
  11. package/src/components/CnAdvancedFormDialog/CnDataTab.vue +217 -0
  12. package/src/components/CnAdvancedFormDialog/CnMetadataTab.vue +121 -0
  13. package/src/components/CnAdvancedFormDialog/CnPropertiesTab.vue +418 -0
  14. package/src/components/CnAdvancedFormDialog/CnPropertyValueCell.vue +247 -0
  15. package/src/components/CnAdvancedFormDialog/index.js +1 -0
  16. package/src/components/CnCardGrid/CnCardGrid.vue +152 -152
  17. package/src/components/CnCardGrid/index.js +1 -1
  18. package/src/components/CnCellRenderer/CnCellRenderer.vue +132 -132
  19. package/src/components/CnCellRenderer/index.js +1 -1
  20. package/src/components/CnChartWidget/CnChartWidget.vue +320 -0
  21. package/src/components/CnChartWidget/index.js +1 -0
  22. package/src/components/CnConfigurationCard/CnConfigurationCard.vue +77 -77
  23. package/src/components/CnConfigurationCard/index.js +1 -1
  24. package/src/components/CnDashboardGrid/CnDashboardGrid.vue +225 -0
  25. package/src/components/CnDashboardGrid/index.js +1 -0
  26. package/src/components/CnDashboardPage/CnDashboardPage.vue +390 -0
  27. package/src/components/CnDashboardPage/index.js +1 -0
  28. package/src/components/CnDataTable/CnDataTable.vue +349 -349
  29. package/src/components/CnDataTable/index.js +1 -1
  30. package/src/components/CnDetailCard/CnDetailCard.vue +214 -0
  31. package/src/components/CnDetailCard/index.js +1 -0
  32. package/src/components/CnDetailPage/CnDetailPage.vue +281 -0
  33. package/src/components/CnDetailPage/index.js +1 -0
  34. package/src/components/CnFacetSidebar/CnFacetSidebar.vue +231 -223
  35. package/src/components/CnFacetSidebar/index.js +1 -1
  36. package/src/components/CnFilterBar/CnFilterBar.vue +152 -152
  37. package/src/components/CnFilterBar/index.js +1 -1
  38. package/src/components/CnIcon/CnIcon.vue +89 -89
  39. package/src/components/CnIcon/index.js +1 -1
  40. package/src/components/CnIndexPage/CnIndexPage.vue +874 -816
  41. package/src/components/CnIndexPage/index.js +1 -1
  42. package/src/components/CnIndexSidebar/CnIndexSidebar.vue +503 -484
  43. package/src/components/CnIndexSidebar/index.js +1 -1
  44. package/src/components/CnItemCard/CnItemCard.vue +132 -0
  45. package/src/components/CnItemCard/index.js +1 -0
  46. package/src/components/CnKpiGrid/CnKpiGrid.vue +89 -89
  47. package/src/components/CnKpiGrid/index.js +1 -1
  48. package/src/components/CnMassActionBar/CnMassActionBar.vue +160 -160
  49. package/src/components/CnMassActionBar/index.js +1 -1
  50. package/src/components/CnMassCopyDialog/CnMassCopyDialog.vue +320 -320
  51. package/src/components/CnMassCopyDialog/index.js +1 -1
  52. package/src/components/CnMassDeleteDialog/CnMassDeleteDialog.vue +238 -238
  53. package/src/components/CnMassDeleteDialog/index.js +1 -1
  54. package/src/components/CnMassExportDialog/CnMassExportDialog.vue +190 -190
  55. package/src/components/CnMassExportDialog/index.js +1 -1
  56. package/src/components/CnMassImportDialog/CnMassImportDialog.vue +491 -491
  57. package/src/components/CnMassImportDialog/index.js +1 -1
  58. package/src/components/CnNoteCard/CnNoteCard.vue +149 -0
  59. package/src/components/CnNoteCard/index.js +1 -0
  60. package/src/components/CnNotesCard/CnNotesCard.vue +413 -0
  61. package/src/components/CnNotesCard/index.js +1 -0
  62. package/src/components/CnObjectCard/CnObjectCard.vue +292 -292
  63. package/src/components/CnObjectCard/index.js +1 -1
  64. package/src/components/CnObjectSidebar/CnObjectSidebar.vue +876 -0
  65. package/src/components/CnObjectSidebar/index.js +1 -0
  66. package/src/components/CnPageHeader/CnPageHeader.vue +57 -57
  67. package/src/components/CnPageHeader/index.js +1 -1
  68. package/src/components/CnPagination/CnPagination.vue +252 -252
  69. package/src/components/CnPagination/index.js +1 -1
  70. package/src/components/CnRowActions/CnRowActions.vue +73 -73
  71. package/src/components/CnRowActions/index.js +1 -1
  72. package/src/components/CnSchemaFormDialog/CnSchemaConfigurationTab.vue +226 -0
  73. package/src/components/CnSchemaFormDialog/CnSchemaFormDialog.vue +787 -0
  74. package/src/components/CnSchemaFormDialog/CnSchemaPropertiesTab.vue +305 -0
  75. package/src/components/CnSchemaFormDialog/CnSchemaPropertyActions.vue +1398 -0
  76. package/src/components/CnSchemaFormDialog/CnSchemaSecurityTab.vue +236 -0
  77. package/src/components/CnSchemaFormDialog/index.js +1 -0
  78. package/src/components/CnSettingsCard/CnSettingsCard.vue +92 -92
  79. package/src/components/CnSettingsCard/index.js +1 -1
  80. package/src/components/CnSettingsSection/CnSettingsSection.vue +266 -266
  81. package/src/components/CnSettingsSection/index.js +1 -1
  82. package/src/components/CnStatsBlock/CnStatsBlock.vue +420 -366
  83. package/src/components/CnStatsBlock/index.js +1 -1
  84. package/src/components/CnStatusBadge/CnStatusBadge.vue +77 -77
  85. package/src/components/CnStatusBadge/index.js +1 -1
  86. package/src/components/CnTabbedFormDialog/CnTabbedFormDialog.vue +540 -0
  87. package/src/components/CnTabbedFormDialog/index.js +1 -0
  88. package/src/components/CnTasksCard/CnTasksCard.vue +373 -0
  89. package/src/components/CnTasksCard/index.js +1 -0
  90. package/src/components/CnTileWidget/CnTileWidget.vue +159 -0
  91. package/src/components/CnTileWidget/index.js +1 -0
  92. package/src/components/CnTimelineStages/CnTimelineStages.vue +292 -0
  93. package/src/components/CnTimelineStages/index.js +1 -0
  94. package/src/components/CnUserActionMenu/CnUserActionMenu.vue +435 -0
  95. package/src/components/CnUserActionMenu/index.js +1 -0
  96. package/src/components/CnVersionInfoCard/CnVersionInfoCard.vue +312 -312
  97. package/src/components/CnVersionInfoCard/index.js +1 -1
  98. package/src/components/CnWidgetRenderer/CnWidgetRenderer.vue +180 -0
  99. package/src/components/CnWidgetRenderer/index.js +1 -0
  100. package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +211 -0
  101. package/src/components/CnWidgetWrapper/index.js +1 -0
  102. package/src/components/index.js +43 -29
  103. package/src/composables/index.js +4 -3
  104. package/src/composables/useDashboardView.js +240 -0
  105. package/src/composables/useDetailView.js +289 -132
  106. package/src/composables/useListView.js +363 -362
  107. package/src/composables/useSubResource.js +142 -142
  108. package/src/constants/metadata.js +30 -30
  109. package/src/css/CnSchemaFormDialog.css +546 -0
  110. package/src/css/__sample_nextcloud_tokens.css +110 -0
  111. package/src/css/actions-bar.css +48 -48
  112. package/src/css/badge.css +51 -51
  113. package/src/css/card.css +128 -128
  114. package/src/css/dashboard.css +70 -0
  115. package/src/css/detail-page.css +168 -0
  116. package/src/css/detail.css +68 -68
  117. package/src/css/index-page.css +44 -32
  118. package/src/css/index-sidebar.css +193 -187
  119. package/src/css/index.css +16 -12
  120. package/src/css/layout.css +90 -90
  121. package/src/css/page-header.css +33 -33
  122. package/src/css/pagination.css +72 -72
  123. package/src/css/table.css +142 -142
  124. package/src/css/timeline-stages.css +218 -0
  125. package/src/css/utilities.css +46 -46
  126. package/src/index.js +72 -53
  127. package/src/store/createSubResourcePlugin.js +135 -135
  128. package/src/store/index.js +3 -3
  129. package/src/store/plugins/auditTrails.js +17 -17
  130. package/src/store/plugins/files.js +250 -186
  131. package/src/store/plugins/index.js +7 -5
  132. package/src/store/plugins/lifecycle.js +180 -180
  133. package/src/store/plugins/relations.js +68 -68
  134. package/src/store/plugins/search.js +372 -0
  135. package/src/store/plugins/selection.js +104 -0
  136. package/src/store/useObjectStore.js +829 -686
  137. package/src/types/auditTrail.d.ts +32 -32
  138. package/src/types/file.d.ts +23 -23
  139. package/src/types/index.d.ts +35 -35
  140. package/src/types/notification.d.ts +36 -36
  141. package/src/types/object.d.ts +40 -40
  142. package/src/types/organisation.d.ts +41 -41
  143. package/src/types/register.d.ts +25 -25
  144. package/src/types/schema.d.ts +39 -39
  145. package/src/types/shared.d.ts +79 -79
  146. package/src/types/source.d.ts +14 -14
  147. package/src/types/task.d.ts +31 -31
  148. package/src/utils/errors.js +96 -96
  149. package/src/utils/headers.js +68 -50
  150. package/src/utils/id.js +13 -0
  151. package/src/utils/index.js +3 -3
  152. 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
+ }