dzql 0.1.0-alpha.1
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/package.json +65 -0
- package/src/client/ui-configs/sample-2.js +207 -0
- package/src/client/ui-loader.js +618 -0
- package/src/client/ui.js +990 -0
- package/src/client/ws.js +352 -0
- package/src/client.js +9 -0
- package/src/database/migrations/001_schema.sql +59 -0
- package/src/database/migrations/002_functions.sql +742 -0
- package/src/database/migrations/003_operations.sql +725 -0
- package/src/database/migrations/004_search.sql +505 -0
- package/src/database/migrations/005_entities.sql +511 -0
- package/src/database/migrations/006_auth.sql +83 -0
- package/src/database/migrations/007_events.sql +136 -0
- package/src/database/migrations/008_hello.sql +18 -0
- package/src/database/migrations/008a_meta.sql +165 -0
- package/src/index.js +19 -0
- package/src/server/api.js +9 -0
- package/src/server/db.js +261 -0
- package/src/server/index.js +141 -0
- package/src/server/logger.js +246 -0
- package/src/server/mcp.js +594 -0
- package/src/server/meta-route.js +251 -0
- package/src/server/ws.js +464 -0
|
@@ -0,0 +1,618 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DZQL UI Configuration Loader
|
|
3
|
+
*
|
|
4
|
+
* Loads UI configurations from JSON files or API endpoints
|
|
5
|
+
* and renders them using the declarative UI framework.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { mount, state, Component, registerComponent } from './ui.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* UI Configuration Cache
|
|
12
|
+
*/
|
|
13
|
+
const configCache = new Map();
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Load a UI configuration from a URL or object
|
|
17
|
+
*/
|
|
18
|
+
export async function loadUI(source, container, ws) {
|
|
19
|
+
let config;
|
|
20
|
+
|
|
21
|
+
if (typeof source === 'string') {
|
|
22
|
+
// Load from URL
|
|
23
|
+
config = await fetchConfig(source);
|
|
24
|
+
} else if (typeof source === 'object') {
|
|
25
|
+
// Direct configuration object
|
|
26
|
+
config = source;
|
|
27
|
+
} else {
|
|
28
|
+
throw new Error('Invalid UI source: must be a URL string or configuration object');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Validate configuration
|
|
32
|
+
validateConfig(config);
|
|
33
|
+
|
|
34
|
+
// Register any custom components defined in the config
|
|
35
|
+
if (config.components) {
|
|
36
|
+
registerCustomComponents(config.components);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Set initial state if provided
|
|
40
|
+
if (config.initialState) {
|
|
41
|
+
initializeState(config.initialState);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Mount the UI
|
|
45
|
+
const instance = mount(config.ui || config, container, ws);
|
|
46
|
+
|
|
47
|
+
// Set up data fetching if configured
|
|
48
|
+
if (config.onMount) {
|
|
49
|
+
await executeLifecycleHook(config.onMount, ws);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Set up periodic updates if configured
|
|
53
|
+
if (config.refreshInterval) {
|
|
54
|
+
setupRefresh(config, instance, ws);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return instance;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Fetch configuration from URL
|
|
62
|
+
*/
|
|
63
|
+
async function fetchConfig(url) {
|
|
64
|
+
// Check cache first
|
|
65
|
+
if (configCache.has(url)) {
|
|
66
|
+
return configCache.get(url);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const response = await fetch(url);
|
|
71
|
+
if (!response.ok) {
|
|
72
|
+
throw new Error(`Failed to load UI config: ${response.statusText}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const config = await response.json();
|
|
76
|
+
|
|
77
|
+
// Cache the configuration
|
|
78
|
+
configCache.set(url, config);
|
|
79
|
+
|
|
80
|
+
return config;
|
|
81
|
+
} catch (error) {
|
|
82
|
+
console.error('Error loading UI configuration:', error);
|
|
83
|
+
throw error;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Validate UI configuration
|
|
89
|
+
*/
|
|
90
|
+
function validateConfig(config) {
|
|
91
|
+
if (!config) {
|
|
92
|
+
throw new Error('UI configuration is required');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Check for required fields based on config type
|
|
96
|
+
if (config.version && config.version !== '1.0') {
|
|
97
|
+
console.warn(`Unknown UI config version: ${config.version}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Validate component structure if it's a direct component config
|
|
101
|
+
if (config.type && !config.ui) {
|
|
102
|
+
validateComponent(config);
|
|
103
|
+
} else if (config.ui) {
|
|
104
|
+
validateComponent(config.ui);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Validate component structure
|
|
110
|
+
*/
|
|
111
|
+
function validateComponent(component) {
|
|
112
|
+
if (!component.type) {
|
|
113
|
+
throw new Error('Component must have a type');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Recursively validate children
|
|
117
|
+
if (component.children && Array.isArray(component.children)) {
|
|
118
|
+
component.children.forEach(validateComponent);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Initialize state from configuration
|
|
124
|
+
*/
|
|
125
|
+
function initializeState(initialState) {
|
|
126
|
+
for (const [key, value] of Object.entries(initialState)) {
|
|
127
|
+
if (typeof value === 'function') {
|
|
128
|
+
// Skip functions in initial state
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
state.set(key, value);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Register custom components from configuration
|
|
137
|
+
*/
|
|
138
|
+
function registerCustomComponents(components) {
|
|
139
|
+
for (const [name, componentDef] of Object.entries(components)) {
|
|
140
|
+
// Create a component class from the definition
|
|
141
|
+
class CustomComponent extends Component {
|
|
142
|
+
render() {
|
|
143
|
+
// Use the template defined in the component
|
|
144
|
+
const template = typeof componentDef.template === 'function'
|
|
145
|
+
? componentDef.template(this.config.props || {})
|
|
146
|
+
: componentDef.template;
|
|
147
|
+
|
|
148
|
+
// Merge props into the template
|
|
149
|
+
const mergedConfig = {
|
|
150
|
+
...template,
|
|
151
|
+
...this.config,
|
|
152
|
+
type: template.type
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
// Render using the base component
|
|
156
|
+
const element = document.createElement('div');
|
|
157
|
+
this.element = element;
|
|
158
|
+
|
|
159
|
+
// Render the template
|
|
160
|
+
const child = renderComponent(mergedConfig, this.ws);
|
|
161
|
+
if (child) {
|
|
162
|
+
this.children.push(child);
|
|
163
|
+
element.appendChild(child.element);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return element;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
registerComponent(name, CustomComponent);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Execute lifecycle hook
|
|
176
|
+
*/
|
|
177
|
+
async function executeLifecycleHook(hook, ws) {
|
|
178
|
+
if (!hook) return;
|
|
179
|
+
|
|
180
|
+
if (Array.isArray(hook)) {
|
|
181
|
+
// Array of actions
|
|
182
|
+
for (const action of hook) {
|
|
183
|
+
await executeAction(action, ws);
|
|
184
|
+
}
|
|
185
|
+
} else if (typeof hook === 'object') {
|
|
186
|
+
// Single action
|
|
187
|
+
await executeAction(hook, ws);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Execute an action
|
|
193
|
+
*/
|
|
194
|
+
async function executeAction(action, ws) {
|
|
195
|
+
switch (action.type) {
|
|
196
|
+
case 'fetch':
|
|
197
|
+
await fetchData(action, ws);
|
|
198
|
+
break;
|
|
199
|
+
case 'setState':
|
|
200
|
+
state.set(action.path, action.value);
|
|
201
|
+
break;
|
|
202
|
+
case 'call':
|
|
203
|
+
await callAPI(action, ws);
|
|
204
|
+
break;
|
|
205
|
+
default:
|
|
206
|
+
console.warn(`Unknown action type: ${action.type}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Fetch data using DZQL
|
|
212
|
+
*/
|
|
213
|
+
async function fetchData(action, ws) {
|
|
214
|
+
try {
|
|
215
|
+
const { entity, operation, params, resultPath } = action;
|
|
216
|
+
|
|
217
|
+
let result;
|
|
218
|
+
if (operation && entity) {
|
|
219
|
+
result = await ws.api[operation][entity](params || {});
|
|
220
|
+
} else if (action.method) {
|
|
221
|
+
result = await ws.call(action.method, params || {});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (resultPath) {
|
|
225
|
+
state.set(resultPath, result);
|
|
226
|
+
}
|
|
227
|
+
} catch (error) {
|
|
228
|
+
console.error('Error fetching data:', error);
|
|
229
|
+
if (action.errorPath) {
|
|
230
|
+
state.set(action.errorPath, error.message);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Call API endpoint
|
|
237
|
+
*/
|
|
238
|
+
async function callAPI(action, ws) {
|
|
239
|
+
try {
|
|
240
|
+
const result = await ws.call(action.method, action.params || {});
|
|
241
|
+
if (action.resultPath) {
|
|
242
|
+
state.set(action.resultPath, result);
|
|
243
|
+
}
|
|
244
|
+
} catch (error) {
|
|
245
|
+
console.error('API call error:', error);
|
|
246
|
+
if (action.errorPath) {
|
|
247
|
+
state.set(action.errorPath, error.message);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Set up automatic refresh
|
|
254
|
+
*/
|
|
255
|
+
function setupRefresh(config, instance, ws) {
|
|
256
|
+
const interval = setInterval(async () => {
|
|
257
|
+
if (config.onRefresh) {
|
|
258
|
+
await executeLifecycleHook(config.onRefresh, ws);
|
|
259
|
+
}
|
|
260
|
+
}, config.refreshInterval);
|
|
261
|
+
|
|
262
|
+
// Store interval ID for cleanup
|
|
263
|
+
instance.refreshInterval = interval;
|
|
264
|
+
|
|
265
|
+
// Override destroy to clear interval
|
|
266
|
+
const originalDestroy = instance.destroy;
|
|
267
|
+
instance.destroy = () => {
|
|
268
|
+
clearInterval(interval);
|
|
269
|
+
originalDestroy?.();
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Load UI from entity metadata
|
|
275
|
+
*/
|
|
276
|
+
export async function loadEntityUI(entityName, viewType, container, ws) {
|
|
277
|
+
try {
|
|
278
|
+
// Fetch entity metadata from DZQL
|
|
279
|
+
const metadata = await ws.call('dzql.get_entity_metadata', { entity: entityName });
|
|
280
|
+
|
|
281
|
+
// Generate UI based on metadata and view type
|
|
282
|
+
const config = generateEntityUI(metadata, viewType);
|
|
283
|
+
|
|
284
|
+
// Load the generated UI
|
|
285
|
+
return await loadUI(config, container, ws);
|
|
286
|
+
} catch (error) {
|
|
287
|
+
console.error(`Error loading entity UI for ${entityName}:`, error);
|
|
288
|
+
throw error;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Generate UI configuration from entity metadata
|
|
294
|
+
*/
|
|
295
|
+
function generateEntityUI(metadata, viewType = 'list') {
|
|
296
|
+
const { table_name, columns, label_field, searchable_fields } = metadata;
|
|
297
|
+
|
|
298
|
+
switch (viewType) {
|
|
299
|
+
case 'list':
|
|
300
|
+
return generateListView(table_name, columns, searchable_fields);
|
|
301
|
+
case 'detail':
|
|
302
|
+
return generateDetailView(table_name, columns);
|
|
303
|
+
case 'form':
|
|
304
|
+
return generateFormView(table_name, columns);
|
|
305
|
+
case 'search':
|
|
306
|
+
return generateSearchView(table_name, columns, searchable_fields);
|
|
307
|
+
default:
|
|
308
|
+
throw new Error(`Unknown view type: ${viewType}`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Generate list view configuration
|
|
314
|
+
*/
|
|
315
|
+
function generateListView(tableName, columns, searchableFields) {
|
|
316
|
+
return {
|
|
317
|
+
type: 'container',
|
|
318
|
+
class: 'entity-list',
|
|
319
|
+
children: [
|
|
320
|
+
{
|
|
321
|
+
type: 'h2',
|
|
322
|
+
text: `${tableName} List`
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
type: 'div',
|
|
326
|
+
class: 'controls',
|
|
327
|
+
children: [
|
|
328
|
+
{
|
|
329
|
+
type: 'input',
|
|
330
|
+
bind: `\${state.${tableName}.searchText}`,
|
|
331
|
+
attributes: {
|
|
332
|
+
placeholder: `Search ${tableName}...`
|
|
333
|
+
}
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
type: 'button',
|
|
337
|
+
text: 'Search',
|
|
338
|
+
onClick: {
|
|
339
|
+
actions: [
|
|
340
|
+
{
|
|
341
|
+
type: 'call',
|
|
342
|
+
operation: 'search',
|
|
343
|
+
entity: tableName,
|
|
344
|
+
params: {
|
|
345
|
+
filters: {
|
|
346
|
+
_search: `\${state.${tableName}.searchText}`
|
|
347
|
+
},
|
|
348
|
+
limit: 25
|
|
349
|
+
},
|
|
350
|
+
resultPath: `${tableName}.results`
|
|
351
|
+
}
|
|
352
|
+
]
|
|
353
|
+
}
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
type: 'button',
|
|
357
|
+
text: 'New',
|
|
358
|
+
onClick: {
|
|
359
|
+
actions: [
|
|
360
|
+
{
|
|
361
|
+
type: 'setState',
|
|
362
|
+
path: `${tableName}.showForm`,
|
|
363
|
+
value: true
|
|
364
|
+
}
|
|
365
|
+
]
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
]
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
type: 'table',
|
|
372
|
+
data: `\${state.${tableName}.results.data}`,
|
|
373
|
+
columns: columns.map(col => ({
|
|
374
|
+
field: col.name,
|
|
375
|
+
label: col.name.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())
|
|
376
|
+
})),
|
|
377
|
+
onRowClick: {
|
|
378
|
+
type: 'setState',
|
|
379
|
+
path: `${tableName}.selected`,
|
|
380
|
+
value: '\${row}'
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
]
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Generate detail view configuration
|
|
389
|
+
*/
|
|
390
|
+
function generateDetailView(tableName, columns) {
|
|
391
|
+
return {
|
|
392
|
+
type: 'container',
|
|
393
|
+
class: 'entity-detail',
|
|
394
|
+
children: [
|
|
395
|
+
{
|
|
396
|
+
type: 'h2',
|
|
397
|
+
text: `${tableName} Details`
|
|
398
|
+
},
|
|
399
|
+
{
|
|
400
|
+
type: 'if',
|
|
401
|
+
condition: `\${state.${tableName}.selected}`,
|
|
402
|
+
then: {
|
|
403
|
+
type: 'div',
|
|
404
|
+
class: 'detail-grid',
|
|
405
|
+
children: columns.map(col => ({
|
|
406
|
+
type: 'div',
|
|
407
|
+
class: 'detail-field',
|
|
408
|
+
children: [
|
|
409
|
+
{
|
|
410
|
+
type: 'label',
|
|
411
|
+
text: col.name.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
type: 'div',
|
|
415
|
+
class: 'detail-value',
|
|
416
|
+
text: `\${state.${tableName}.selected.${col.name}}`
|
|
417
|
+
}
|
|
418
|
+
]
|
|
419
|
+
}))
|
|
420
|
+
},
|
|
421
|
+
else: {
|
|
422
|
+
type: 'p',
|
|
423
|
+
text: 'No item selected'
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
]
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Generate form view configuration
|
|
432
|
+
*/
|
|
433
|
+
function generateFormView(tableName, columns) {
|
|
434
|
+
// Filter out auto-generated columns
|
|
435
|
+
const editableColumns = columns.filter(col =>
|
|
436
|
+
!col.is_primary && !col.is_generated && col.name !== 'created_at' && col.name !== 'updated_at'
|
|
437
|
+
);
|
|
438
|
+
|
|
439
|
+
return {
|
|
440
|
+
type: 'form',
|
|
441
|
+
dataPath: `${tableName}.formData`,
|
|
442
|
+
fields: editableColumns.map(col => ({
|
|
443
|
+
name: col.name,
|
|
444
|
+
label: col.name.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase()),
|
|
445
|
+
inputType: getInputType(col.type),
|
|
446
|
+
bind: `\${state.${tableName}.formData.${col.name}}`,
|
|
447
|
+
required: !col.is_nullable
|
|
448
|
+
})),
|
|
449
|
+
submitButton: {
|
|
450
|
+
text: 'Save',
|
|
451
|
+
onClick: {
|
|
452
|
+
actions: [
|
|
453
|
+
{
|
|
454
|
+
type: 'call',
|
|
455
|
+
operation: 'save',
|
|
456
|
+
entity: tableName,
|
|
457
|
+
params: `\${state.${tableName}.formData}`,
|
|
458
|
+
resultPath: `${tableName}.saveResult`,
|
|
459
|
+
onSuccess: [
|
|
460
|
+
{
|
|
461
|
+
type: 'setState',
|
|
462
|
+
path: `${tableName}.showForm`,
|
|
463
|
+
value: false
|
|
464
|
+
},
|
|
465
|
+
{
|
|
466
|
+
type: 'setState',
|
|
467
|
+
path: `${tableName}.formData`,
|
|
468
|
+
value: {}
|
|
469
|
+
}
|
|
470
|
+
]
|
|
471
|
+
}
|
|
472
|
+
]
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Generate search view configuration
|
|
480
|
+
*/
|
|
481
|
+
function generateSearchView(tableName, columns, searchableFields) {
|
|
482
|
+
return {
|
|
483
|
+
type: 'container',
|
|
484
|
+
class: 'entity-search',
|
|
485
|
+
children: [
|
|
486
|
+
{
|
|
487
|
+
type: 'h2',
|
|
488
|
+
text: `Search ${tableName}`
|
|
489
|
+
},
|
|
490
|
+
{
|
|
491
|
+
type: 'div',
|
|
492
|
+
class: 'search-filters',
|
|
493
|
+
children: [
|
|
494
|
+
// Text search across searchable fields
|
|
495
|
+
{
|
|
496
|
+
type: 'div',
|
|
497
|
+
class: 'filter-group',
|
|
498
|
+
children: [
|
|
499
|
+
{
|
|
500
|
+
type: 'label',
|
|
501
|
+
text: 'Search Text'
|
|
502
|
+
},
|
|
503
|
+
{
|
|
504
|
+
type: 'input',
|
|
505
|
+
bind: `\${state.${tableName}.search._search}`,
|
|
506
|
+
attributes: {
|
|
507
|
+
placeholder: `Search in ${searchableFields.join(', ')}`
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
]
|
|
511
|
+
},
|
|
512
|
+
// Generate filter inputs for key columns
|
|
513
|
+
...columns.filter(col => !col.is_generated).slice(0, 5).map(col => ({
|
|
514
|
+
type: 'div',
|
|
515
|
+
class: 'filter-group',
|
|
516
|
+
children: [
|
|
517
|
+
{
|
|
518
|
+
type: 'label',
|
|
519
|
+
text: col.name.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())
|
|
520
|
+
},
|
|
521
|
+
{
|
|
522
|
+
type: 'input',
|
|
523
|
+
inputType: getInputType(col.type),
|
|
524
|
+
bind: `\${state.${tableName}.search.${col.name}}`
|
|
525
|
+
}
|
|
526
|
+
]
|
|
527
|
+
}))
|
|
528
|
+
]
|
|
529
|
+
},
|
|
530
|
+
{
|
|
531
|
+
type: 'button',
|
|
532
|
+
text: 'Search',
|
|
533
|
+
onClick: {
|
|
534
|
+
actions: [
|
|
535
|
+
{
|
|
536
|
+
type: 'call',
|
|
537
|
+
operation: 'search',
|
|
538
|
+
entity: tableName,
|
|
539
|
+
params: {
|
|
540
|
+
filters: `\${state.${tableName}.search}`,
|
|
541
|
+
limit: 50
|
|
542
|
+
},
|
|
543
|
+
resultPath: `${tableName}.searchResults`
|
|
544
|
+
}
|
|
545
|
+
]
|
|
546
|
+
}
|
|
547
|
+
},
|
|
548
|
+
{
|
|
549
|
+
type: 'if',
|
|
550
|
+
condition: `\${state.${tableName}.searchResults}`,
|
|
551
|
+
then: {
|
|
552
|
+
type: 'div',
|
|
553
|
+
children: [
|
|
554
|
+
{
|
|
555
|
+
type: 'p',
|
|
556
|
+
text: `Found \${state.${tableName}.searchResults.total} results`
|
|
557
|
+
},
|
|
558
|
+
{
|
|
559
|
+
type: 'table',
|
|
560
|
+
data: `\${state.${tableName}.searchResults.data}`,
|
|
561
|
+
columns: columns.slice(0, 6).map(col => ({
|
|
562
|
+
field: col.name,
|
|
563
|
+
label: col.name.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())
|
|
564
|
+
}))
|
|
565
|
+
}
|
|
566
|
+
]
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
]
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Get appropriate input type for column type
|
|
575
|
+
*/
|
|
576
|
+
function getInputType(columnType) {
|
|
577
|
+
const type = columnType.toLowerCase();
|
|
578
|
+
if (type.includes('int') || type.includes('numeric') || type.includes('decimal')) {
|
|
579
|
+
return 'number';
|
|
580
|
+
} else if (type.includes('date') && !type.includes('time')) {
|
|
581
|
+
return 'date';
|
|
582
|
+
} else if (type.includes('datetime') || type.includes('timestamp')) {
|
|
583
|
+
return 'datetime-local';
|
|
584
|
+
} else if (type.includes('time')) {
|
|
585
|
+
return 'time';
|
|
586
|
+
} else if (type.includes('bool')) {
|
|
587
|
+
return 'checkbox';
|
|
588
|
+
} else if (type.includes('email')) {
|
|
589
|
+
return 'email';
|
|
590
|
+
} else if (type.includes('url')) {
|
|
591
|
+
return 'url';
|
|
592
|
+
} else if (type.includes('text') || type.includes('json')) {
|
|
593
|
+
return 'textarea';
|
|
594
|
+
}
|
|
595
|
+
return 'text';
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Clear configuration cache
|
|
600
|
+
*/
|
|
601
|
+
export function clearConfigCache(url = null) {
|
|
602
|
+
if (url) {
|
|
603
|
+
configCache.delete(url);
|
|
604
|
+
} else {
|
|
605
|
+
configCache.clear();
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Preload configurations
|
|
611
|
+
*/
|
|
612
|
+
export async function preloadConfigs(urls) {
|
|
613
|
+
const promises = urls.map(url => fetchConfig(url));
|
|
614
|
+
return await Promise.all(promises);
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// Export for use in other modules
|
|
618
|
+
export { state };
|