@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.
- package/README.md +226 -226
- package/css/index.css +5 -0
- package/dist/nextcloud-vue.cjs.js +60455 -8755
- package/dist/nextcloud-vue.cjs.js.map +1 -1
- package/dist/nextcloud-vue.css +2062 -528
- package/dist/nextcloud-vue.esm.js +60411 -8731
- package/dist/nextcloud-vue.esm.js.map +1 -1
- package/package.json +75 -61
- package/src/components/CnActionsBar/CnActionsBar.vue +235 -225
- package/src/components/CnActionsBar/index.js +1 -1
- package/src/components/CnAdvancedFormDialog/CnAdvancedFormDialog.vue +579 -0
- package/src/components/CnAdvancedFormDialog/CnDataTab.vue +217 -0
- package/src/components/CnAdvancedFormDialog/CnMetadataTab.vue +121 -0
- package/src/components/CnAdvancedFormDialog/CnPropertiesTab.vue +418 -0
- package/src/components/CnAdvancedFormDialog/CnPropertyValueCell.vue +247 -0
- package/src/components/CnAdvancedFormDialog/index.js +1 -0
- package/src/components/CnCardGrid/CnCardGrid.vue +152 -152
- package/src/components/CnCardGrid/index.js +1 -1
- package/src/components/CnCellRenderer/CnCellRenderer.vue +132 -132
- package/src/components/CnCellRenderer/index.js +1 -1
- package/src/components/CnChartWidget/CnChartWidget.vue +320 -0
- package/src/components/CnChartWidget/index.js +1 -0
- package/src/components/CnConfigurationCard/CnConfigurationCard.vue +77 -77
- package/src/components/CnConfigurationCard/index.js +1 -1
- package/src/components/CnDashboardGrid/CnDashboardGrid.vue +225 -0
- package/src/components/CnDashboardGrid/index.js +1 -0
- package/src/components/CnDashboardPage/CnDashboardPage.vue +390 -0
- package/src/components/CnDashboardPage/index.js +1 -0
- package/src/components/CnDataTable/CnDataTable.vue +349 -349
- package/src/components/CnDataTable/index.js +1 -1
- package/src/components/CnDetailCard/CnDetailCard.vue +214 -0
- package/src/components/CnDetailCard/index.js +1 -0
- package/src/components/CnDetailPage/CnDetailPage.vue +281 -0
- package/src/components/CnDetailPage/index.js +1 -0
- package/src/components/CnFacetSidebar/CnFacetSidebar.vue +231 -223
- package/src/components/CnFacetSidebar/index.js +1 -1
- package/src/components/CnFilterBar/CnFilterBar.vue +152 -152
- package/src/components/CnFilterBar/index.js +1 -1
- package/src/components/CnIcon/CnIcon.vue +89 -89
- package/src/components/CnIcon/index.js +1 -1
- package/src/components/CnIndexPage/CnIndexPage.vue +874 -816
- package/src/components/CnIndexPage/index.js +1 -1
- package/src/components/CnIndexSidebar/CnIndexSidebar.vue +503 -484
- package/src/components/CnIndexSidebar/index.js +1 -1
- package/src/components/CnItemCard/CnItemCard.vue +132 -0
- package/src/components/CnItemCard/index.js +1 -0
- package/src/components/CnKpiGrid/CnKpiGrid.vue +89 -89
- package/src/components/CnKpiGrid/index.js +1 -1
- package/src/components/CnMassActionBar/CnMassActionBar.vue +160 -160
- package/src/components/CnMassActionBar/index.js +1 -1
- package/src/components/CnMassCopyDialog/CnMassCopyDialog.vue +320 -320
- package/src/components/CnMassCopyDialog/index.js +1 -1
- package/src/components/CnMassDeleteDialog/CnMassDeleteDialog.vue +238 -238
- package/src/components/CnMassDeleteDialog/index.js +1 -1
- package/src/components/CnMassExportDialog/CnMassExportDialog.vue +190 -190
- package/src/components/CnMassExportDialog/index.js +1 -1
- package/src/components/CnMassImportDialog/CnMassImportDialog.vue +491 -491
- package/src/components/CnMassImportDialog/index.js +1 -1
- package/src/components/CnNoteCard/CnNoteCard.vue +149 -0
- package/src/components/CnNoteCard/index.js +1 -0
- package/src/components/CnNotesCard/CnNotesCard.vue +413 -0
- package/src/components/CnNotesCard/index.js +1 -0
- package/src/components/CnObjectCard/CnObjectCard.vue +292 -292
- package/src/components/CnObjectCard/index.js +1 -1
- package/src/components/CnObjectSidebar/CnObjectSidebar.vue +876 -0
- package/src/components/CnObjectSidebar/index.js +1 -0
- package/src/components/CnPageHeader/CnPageHeader.vue +57 -57
- package/src/components/CnPageHeader/index.js +1 -1
- package/src/components/CnPagination/CnPagination.vue +252 -252
- package/src/components/CnPagination/index.js +1 -1
- package/src/components/CnRowActions/CnRowActions.vue +73 -73
- package/src/components/CnRowActions/index.js +1 -1
- package/src/components/CnSchemaFormDialog/CnSchemaConfigurationTab.vue +226 -0
- package/src/components/CnSchemaFormDialog/CnSchemaFormDialog.vue +787 -0
- package/src/components/CnSchemaFormDialog/CnSchemaPropertiesTab.vue +305 -0
- package/src/components/CnSchemaFormDialog/CnSchemaPropertyActions.vue +1398 -0
- package/src/components/CnSchemaFormDialog/CnSchemaSecurityTab.vue +236 -0
- package/src/components/CnSchemaFormDialog/index.js +1 -0
- package/src/components/CnSettingsCard/CnSettingsCard.vue +92 -92
- package/src/components/CnSettingsCard/index.js +1 -1
- package/src/components/CnSettingsSection/CnSettingsSection.vue +266 -266
- package/src/components/CnSettingsSection/index.js +1 -1
- package/src/components/CnStatsBlock/CnStatsBlock.vue +420 -366
- package/src/components/CnStatsBlock/index.js +1 -1
- package/src/components/CnStatusBadge/CnStatusBadge.vue +77 -77
- package/src/components/CnStatusBadge/index.js +1 -1
- package/src/components/CnTabbedFormDialog/CnTabbedFormDialog.vue +540 -0
- package/src/components/CnTabbedFormDialog/index.js +1 -0
- package/src/components/CnTasksCard/CnTasksCard.vue +373 -0
- package/src/components/CnTasksCard/index.js +1 -0
- package/src/components/CnTileWidget/CnTileWidget.vue +159 -0
- package/src/components/CnTileWidget/index.js +1 -0
- package/src/components/CnTimelineStages/CnTimelineStages.vue +292 -0
- package/src/components/CnTimelineStages/index.js +1 -0
- package/src/components/CnUserActionMenu/CnUserActionMenu.vue +435 -0
- package/src/components/CnUserActionMenu/index.js +1 -0
- package/src/components/CnVersionInfoCard/CnVersionInfoCard.vue +312 -312
- package/src/components/CnVersionInfoCard/index.js +1 -1
- package/src/components/CnWidgetRenderer/CnWidgetRenderer.vue +180 -0
- package/src/components/CnWidgetRenderer/index.js +1 -0
- package/src/components/CnWidgetWrapper/CnWidgetWrapper.vue +211 -0
- package/src/components/CnWidgetWrapper/index.js +1 -0
- package/src/components/index.js +43 -29
- package/src/composables/index.js +4 -3
- package/src/composables/useDashboardView.js +240 -0
- package/src/composables/useDetailView.js +289 -132
- package/src/composables/useListView.js +363 -153
- package/src/composables/useSubResource.js +142 -142
- package/src/constants/metadata.js +30 -30
- package/src/css/CnSchemaFormDialog.css +546 -0
- package/src/css/__sample_nextcloud_tokens.css +110 -0
- package/src/css/actions-bar.css +48 -48
- package/src/css/badge.css +51 -51
- package/src/css/card.css +128 -128
- package/src/css/dashboard.css +70 -0
- package/src/css/detail-page.css +168 -0
- package/src/css/detail.css +68 -68
- package/src/css/index-page.css +44 -32
- package/src/css/index-sidebar.css +193 -187
- package/src/css/index.css +16 -12
- package/src/css/layout.css +90 -90
- package/src/css/page-header.css +33 -33
- package/src/css/pagination.css +72 -72
- package/src/css/table.css +142 -142
- package/src/css/timeline-stages.css +218 -0
- package/src/css/utilities.css +46 -46
- package/src/index.js +72 -53
- package/src/store/createSubResourcePlugin.js +135 -135
- package/src/store/index.js +3 -3
- package/src/store/plugins/auditTrails.js +17 -17
- package/src/store/plugins/files.js +250 -186
- package/src/store/plugins/index.js +7 -5
- package/src/store/plugins/lifecycle.js +180 -180
- package/src/store/plugins/relations.js +68 -68
- package/src/store/plugins/search.js +372 -0
- package/src/store/plugins/selection.js +104 -0
- package/src/store/useObjectStore.js +829 -686
- package/src/types/auditTrail.d.ts +32 -32
- package/src/types/file.d.ts +23 -23
- package/src/types/index.d.ts +35 -35
- package/src/types/notification.d.ts +36 -36
- package/src/types/object.d.ts +40 -40
- package/src/types/organisation.d.ts +41 -41
- package/src/types/register.d.ts +25 -25
- package/src/types/schema.d.ts +39 -39
- package/src/types/shared.d.ts +79 -79
- package/src/types/source.d.ts +14 -14
- package/src/types/task.d.ts +31 -31
- package/src/utils/errors.js +96 -96
- package/src/utils/headers.js +68 -50
- package/src/utils/id.js +13 -0
- package/src/utils/index.js +3 -3
- package/src/utils/schema.js +422 -419
package/src/utils/schema.js
CHANGED
|
@@ -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]) =>
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
filter.
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
|
|
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
|
+
}
|