forked from enviPath/enviPy
This implements a version of #274, relying on Pydantics built in JSON schema and JSON rendering. Requires additional UI tagging in the ai model repo but will remove HTML tags. Example scenario with filled information: 5882df9c-dae1-4d80-a40e-db4724271456/scenario/3a4d395a-6a6d-4154-8ce3-ced667fceec0 Reviewed-on: enviPath/enviPy#282 Co-authored-by: Tobias O <tobias.olenyi@envipath.com> Co-committed-by: Tobias O <tobias.olenyi@envipath.com>
187 lines
6.2 KiB
JavaScript
187 lines
6.2 KiB
JavaScript
/**
|
||
* Alpine.js Widget Components for Schema Forms
|
||
*
|
||
* Centralized widget component definitions for dynamic form rendering.
|
||
* Each widget receives explicit parameters instead of context object for better traceability.
|
||
*/
|
||
document.addEventListener('alpine:init', () => {
|
||
// Base widget factory with common functionality
|
||
const baseWidget = (fieldName, data, schema, uiSchema, fieldErrors, mode) => ({
|
||
fieldName,
|
||
data,
|
||
schema,
|
||
uiSchema,
|
||
fieldErrors,
|
||
mode,
|
||
|
||
// Field schema access
|
||
get fieldSchema() {
|
||
return this.schema?.properties?.[this.fieldName] || {};
|
||
},
|
||
|
||
// Common metadata
|
||
get label() {
|
||
// Check uiSchema first (RJSF format)
|
||
if (this.uiSchema?.[this.fieldName]?.['ui:label']) {
|
||
return this.uiSchema[this.fieldName]['ui:label'];
|
||
}
|
||
// Fall back to schema title
|
||
if (this.fieldSchema.title) {
|
||
return this.fieldSchema.title;
|
||
}
|
||
// Default: format field name
|
||
return this.fieldName.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
|
||
},
|
||
get helpText() {
|
||
return this.fieldSchema.description || '';
|
||
},
|
||
|
||
// Field-level unit extraction from uiSchema (RJSF format)
|
||
get unit() {
|
||
return this.uiSchema?.[this.fieldName]?.['ui:unit'] || null;
|
||
},
|
||
|
||
// Error handling
|
||
get hasError() {
|
||
return !!this.fieldErrors?.[this.fieldName];
|
||
},
|
||
get errors() {
|
||
return this.fieldErrors?.[this.fieldName] || [];
|
||
},
|
||
|
||
// Mode checks
|
||
get isViewMode() { return this.mode === 'view'; },
|
||
get isEditMode() { return this.mode === 'edit'; },
|
||
});
|
||
|
||
// Text widget
|
||
Alpine.data('textWidget', (fieldName, data, schema, uiSchema, fieldErrors, mode) => ({
|
||
...baseWidget(fieldName, data, schema, uiSchema, fieldErrors, mode),
|
||
|
||
get value() { return this.data[this.fieldName] || ''; },
|
||
set value(v) { this.data[this.fieldName] = v; },
|
||
}));
|
||
|
||
// Textarea widget
|
||
Alpine.data('textareaWidget', (fieldName, data, schema, uiSchema, fieldErrors, mode) => ({
|
||
...baseWidget(fieldName, data, schema, uiSchema, fieldErrors, mode),
|
||
|
||
get value() { return this.data[this.fieldName] || ''; },
|
||
set value(v) { this.data[this.fieldName] = v; },
|
||
}));
|
||
|
||
// Number widget with unit support
|
||
Alpine.data('numberWidget', (fieldName, data, schema, uiSchema, fieldErrors, mode) => ({
|
||
...baseWidget(fieldName, data, schema, uiSchema, fieldErrors, mode),
|
||
|
||
get value() { return this.data[this.fieldName]; },
|
||
set value(v) {
|
||
this.data[this.fieldName] = v === '' || v === null ? null : parseFloat(v);
|
||
},
|
||
get hasValue() {
|
||
return this.value !== null && this.value !== undefined && this.value !== '';
|
||
},
|
||
// Format value with unit for view mode
|
||
get displayValue() {
|
||
if (!this.hasValue) return '—';
|
||
return this.unit ? `${this.value} ${this.unit}` : String(this.value);
|
||
},
|
||
}));
|
||
|
||
// Select widget
|
||
Alpine.data('selectWidget', (fieldName, data, schema, uiSchema, fieldErrors, mode) => ({
|
||
...baseWidget(fieldName, data, schema, uiSchema, fieldErrors, mode),
|
||
|
||
get value() { return this.data[this.fieldName] || ''; },
|
||
set value(v) { this.data[this.fieldName] = v; },
|
||
get options() { return this.fieldSchema.enum || []; },
|
||
}));
|
||
|
||
// Checkbox widget
|
||
Alpine.data('checkboxWidget', (fieldName, data, schema, uiSchema, fieldErrors, mode) => ({
|
||
...baseWidget(fieldName, data, schema, uiSchema, fieldErrors, mode),
|
||
|
||
get checked() { return !!this.data[this.fieldName]; },
|
||
set checked(v) { this.data[this.fieldName] = v; },
|
||
}));
|
||
|
||
// Interval widget with unit support
|
||
Alpine.data('intervalWidget', (fieldName, data, schema, uiSchema, fieldErrors, mode) => ({
|
||
...baseWidget(fieldName, data, schema, uiSchema, fieldErrors, mode),
|
||
|
||
get start() {
|
||
return this.data[this.fieldName]?.start ?? null;
|
||
},
|
||
set start(v) {
|
||
if (!this.data[this.fieldName]) this.data[this.fieldName] = {};
|
||
this.data[this.fieldName].start = v === '' || v === null ? null : parseFloat(v);
|
||
},
|
||
get end() {
|
||
return this.data[this.fieldName]?.end ?? null;
|
||
},
|
||
set end(v) {
|
||
if (!this.data[this.fieldName]) this.data[this.fieldName] = {};
|
||
this.data[this.fieldName].end = v === '' || v === null ? null : parseFloat(v);
|
||
},
|
||
// Format interval with unit for view mode
|
||
get displayValue() {
|
||
const s = this.start, e = this.end;
|
||
const unitStr = this.unit ? ` ${this.unit}` : '';
|
||
|
||
if (s !== null && e !== null) return `${s} – ${e}${unitStr}`;
|
||
if (s !== null) return `≥ ${s}${unitStr}`;
|
||
if (e !== null) return `≤ ${e}${unitStr}`;
|
||
return '—';
|
||
},
|
||
|
||
get isSameValue() {
|
||
return this.start !== null && this.start === this.end;
|
||
},
|
||
|
||
// Validation: start must be <= end
|
||
get hasValidationError() {
|
||
if (this.isViewMode) return false;
|
||
const s = this.start;
|
||
const e = this.end;
|
||
// Only validate if both values are provided
|
||
if (s !== null && e !== null && typeof s === 'number' && typeof e === 'number') {
|
||
return s > e;
|
||
}
|
||
return false;
|
||
},
|
||
|
||
// Override hasError to include validation error
|
||
get hasError() {
|
||
return this.hasValidationError || !!this.fieldErrors?.[this.fieldName];
|
||
},
|
||
|
||
// Override errors to include validation error message
|
||
get errors() {
|
||
const serverErrors = this.fieldErrors?.[this.fieldName] || [];
|
||
const validationErrors = this.hasValidationError
|
||
? ['Start value must be less than or equal to end value']
|
||
: [];
|
||
return [...validationErrors, ...serverErrors];
|
||
},
|
||
}));
|
||
|
||
// PubMed link widget
|
||
Alpine.data('pubmedWidget', (fieldName, data, schema, uiSchema, fieldErrors, mode) => ({
|
||
...baseWidget(fieldName, data, schema, uiSchema, fieldErrors, mode),
|
||
|
||
get value() { return this.data[this.fieldName] || ''; },
|
||
set value(v) { this.data[this.fieldName] = v; },
|
||
get pubmedUrl() {
|
||
return this.value ? `https://pubmed.ncbi.nlm.nih.gov/${this.value}` : null;
|
||
},
|
||
}));
|
||
|
||
// Compound link widget
|
||
Alpine.data('compoundWidget', (fieldName, data, schema, uiSchema, fieldErrors, mode) => ({
|
||
...baseWidget(fieldName, data, schema, uiSchema, fieldErrors, mode),
|
||
|
||
get value() { return this.data[this.fieldName] || ''; },
|
||
set value(v) { this.data[this.fieldName] = v; },
|
||
}));
|
||
});
|