forked from enviPath/enviPy
Initial bayer app Show Pack Classification Adjusted docker compose to bayer specifics Adjusted Dockerfile for Bayer Adding secret flags to group, add secret pools to packages Adjusted View for Package creation Prep configs, added Package Create Modal wip More on PES wip wip Wip minor PW interactions API PES wip Make Select Widget reflect required make required generallay available Update UI if pathway mode is set to build Added ais circle adjustments Initial Zoom, fix AD Creation wip
504 lines
12 KiB
JavaScript
504 lines
12 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,
|
||
mode,
|
||
debugErrors,
|
||
context = null // NEW: context for error namespacing
|
||
) => ({
|
||
fieldName,
|
||
data,
|
||
schema,
|
||
uiSchema,
|
||
mode,
|
||
debugErrors,
|
||
context, // Store context for use in templates
|
||
|
||
// 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;
|
||
},
|
||
|
||
// Mode checks
|
||
get isViewMode() {
|
||
return this.mode === "view";
|
||
},
|
||
get isEditMode() {
|
||
return this.mode === "edit";
|
||
},
|
||
get isRequired() {
|
||
return (this.schema.required || []).indexOf(this.fieldName) > -1
|
||
}
|
||
});
|
||
|
||
// Text widget
|
||
Alpine.data(
|
||
"textWidget",
|
||
(fieldName, data, schema, uiSchema, mode, debugErrors, context = null) => ({
|
||
...baseWidget(
|
||
fieldName,
|
||
data,
|
||
schema,
|
||
uiSchema,
|
||
mode,
|
||
debugErrors,
|
||
context,
|
||
),
|
||
|
||
get value() {
|
||
return this.data[this.fieldName] || "";
|
||
},
|
||
set value(v) {
|
||
this.data[this.fieldName] = v;
|
||
},
|
||
}),
|
||
);
|
||
|
||
// Textarea widget
|
||
Alpine.data(
|
||
"textareaWidget",
|
||
(fieldName, data, schema, uiSchema, mode, debugErrors, context = null) => ({
|
||
...baseWidget(
|
||
fieldName,
|
||
data,
|
||
schema,
|
||
uiSchema,
|
||
mode,
|
||
debugErrors,
|
||
context,
|
||
),
|
||
|
||
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, mode, debugErrors, context = null) => ({
|
||
...baseWidget(
|
||
fieldName,
|
||
data,
|
||
schema,
|
||
uiSchema,
|
||
mode,
|
||
debugErrors,
|
||
context,
|
||
),
|
||
|
||
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, mode, debugErrors, context = null) => ({
|
||
...baseWidget(
|
||
fieldName,
|
||
data,
|
||
schema,
|
||
uiSchema,
|
||
mode,
|
||
debugErrors,
|
||
context,
|
||
),
|
||
|
||
get value() {
|
||
return this.data[this.fieldName] || "";
|
||
},
|
||
set value(v) {
|
||
this.data[this.fieldName] = v;
|
||
},
|
||
get multiple() {
|
||
return !!(this.fieldSchema.items && this.fieldSchema.items.enum);
|
||
|
||
},
|
||
get options() {
|
||
if (this.fieldSchema.enum) {
|
||
return this.fieldSchema.enum;
|
||
} else if (this.fieldSchema.items && this.fieldSchema.items.enum) {
|
||
return this.fieldSchema.items.enum;
|
||
} else {
|
||
return [];
|
||
}
|
||
},
|
||
}),
|
||
);
|
||
|
||
// Checkbox widget
|
||
Alpine.data(
|
||
"checkboxWidget",
|
||
(fieldName, data, schema, uiSchema, mode, debugErrors, context = null) => ({
|
||
...baseWidget(
|
||
fieldName,
|
||
data,
|
||
schema,
|
||
uiSchema,
|
||
mode,
|
||
debugErrors,
|
||
context,
|
||
),
|
||
|
||
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, mode, debugErrors, context = null) => ({
|
||
...baseWidget(
|
||
fieldName,
|
||
data,
|
||
schema,
|
||
uiSchema,
|
||
mode,
|
||
debugErrors,
|
||
context,
|
||
),
|
||
|
||
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 (client-side)
|
||
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;
|
||
},
|
||
}),
|
||
);
|
||
|
||
// PubMed link widget
|
||
Alpine.data(
|
||
"pubmedWidget",
|
||
(fieldName, data, schema, uiSchema, mode, debugErrors, context = null) => ({
|
||
...baseWidget(
|
||
fieldName,
|
||
data,
|
||
schema,
|
||
uiSchema,
|
||
mode,
|
||
debugErrors,
|
||
context,
|
||
),
|
||
|
||
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;
|
||
},
|
||
}),
|
||
);
|
||
|
||
// PubMed link widget
|
||
Alpine.data(
|
||
"doiWidget",
|
||
(fieldName, data, schema, uiSchema, mode, debugErrors, context = null) => ({
|
||
...baseWidget(
|
||
fieldName,
|
||
data,
|
||
schema,
|
||
uiSchema,
|
||
mode,
|
||
debugErrors,
|
||
context,
|
||
),
|
||
|
||
get value() {
|
||
return this.data[this.fieldName] || "";
|
||
},
|
||
set value(v) {
|
||
this.data[this.fieldName] = v;
|
||
},
|
||
get doiUrl() {
|
||
return this.value
|
||
? `https://dx.doi.org/${this.value}`
|
||
: null;
|
||
},
|
||
}),
|
||
);
|
||
|
||
// Compound link widget
|
||
Alpine.data(
|
||
"compoundWidget",
|
||
(fieldName, data, schema, uiSchema, mode, debugErrors, context = null) => ({
|
||
...baseWidget(
|
||
fieldName,
|
||
data,
|
||
schema,
|
||
uiSchema,
|
||
mode,
|
||
debugErrors,
|
||
context,
|
||
),
|
||
|
||
get value() {
|
||
return this.data[this.fieldName] || "";
|
||
},
|
||
set value(v) {
|
||
this.data[this.fieldName] = v;
|
||
},
|
||
}),
|
||
);
|
||
|
||
// TimeSeries table widget
|
||
Alpine.data(
|
||
"timeseriesTableWidget",
|
||
(fieldName, data, schema, uiSchema, mode, debugErrors, context = null) => ({
|
||
...baseWidget(
|
||
fieldName,
|
||
data,
|
||
schema,
|
||
uiSchema,
|
||
mode,
|
||
debugErrors,
|
||
context,
|
||
),
|
||
|
||
chartInstance: null,
|
||
|
||
// Getter/setter for measurements array
|
||
get measurements() {
|
||
return this.data[this.fieldName] || [];
|
||
},
|
||
set measurements(v) {
|
||
this.data[this.fieldName] = v;
|
||
},
|
||
|
||
// Get description from sibling field
|
||
get description() {
|
||
return this.data?.description || "";
|
||
},
|
||
|
||
// Get method from sibling field
|
||
get method() {
|
||
return this.data?.method || "";
|
||
},
|
||
|
||
// Computed property for chart options
|
||
get chartOptions() {
|
||
return {
|
||
measurements: this.measurements,
|
||
xAxisLabel: this.data?.x_axis_label || "Time",
|
||
yAxisLabel: this.data?.y_axis_label || "Value",
|
||
xAxisUnit: this.data?.x_axis_unit || "",
|
||
yAxisUnit: this.data?.y_axis_unit || "",
|
||
};
|
||
},
|
||
|
||
// Add new measurement
|
||
addMeasurement() {
|
||
if (!this.data[this.fieldName]) {
|
||
this.data[this.fieldName] = [];
|
||
}
|
||
this.data[this.fieldName].push({
|
||
timestamp: null,
|
||
value: null,
|
||
error: null,
|
||
note: "",
|
||
});
|
||
},
|
||
|
||
// Remove measurement by index
|
||
removeMeasurement(index) {
|
||
if (
|
||
this.data[this.fieldName] &&
|
||
Array.isArray(this.data[this.fieldName])
|
||
) {
|
||
this.data[this.fieldName].splice(index, 1);
|
||
}
|
||
},
|
||
|
||
// Update specific measurement field
|
||
updateMeasurement(index, field, value) {
|
||
if (this.data[this.fieldName] && this.data[this.fieldName][index]) {
|
||
if (field === "timestamp" || field === "value" || field === "error") {
|
||
// Parse all numeric fields (timestamp is days as float)
|
||
this.data[this.fieldName][index][field] =
|
||
value === "" || value === null ? null : parseFloat(value);
|
||
} else {
|
||
// Store other fields as-is
|
||
this.data[this.fieldName][index][field] = value;
|
||
}
|
||
}
|
||
},
|
||
|
||
// Format timestamp for display (timestamp is numeric days as float)
|
||
formatTimestamp(timestamp) {
|
||
return timestamp ?? "";
|
||
},
|
||
|
||
// Sort by timestamp (numeric days)
|
||
sortByTimestamp() {
|
||
if (
|
||
this.data[this.fieldName] &&
|
||
Array.isArray(this.data[this.fieldName])
|
||
) {
|
||
this.data[this.fieldName].sort((a, b) => {
|
||
const tsA = a.timestamp ?? Infinity;
|
||
const tsB = b.timestamp ?? Infinity;
|
||
return tsA - tsB;
|
||
});
|
||
}
|
||
},
|
||
|
||
// Chart lifecycle methods (delegates to TimeSeriesChart utility)
|
||
initChart() {
|
||
if (!this.isViewMode || !window.Chart || !window.TimeSeriesChart)
|
||
return;
|
||
|
||
const canvas = this.$refs?.chartCanvas;
|
||
if (!canvas) return;
|
||
|
||
this.destroyChart();
|
||
|
||
if (this.measurements.length === 0) return;
|
||
|
||
this.chartInstance = window.TimeSeriesChart.create(
|
||
canvas,
|
||
this.chartOptions,
|
||
);
|
||
},
|
||
|
||
updateChart() {
|
||
if (!this.chartInstance || !this.isViewMode) return;
|
||
window.TimeSeriesChart.update(
|
||
this.chartInstance,
|
||
this.measurements,
|
||
this.chartOptions,
|
||
);
|
||
},
|
||
|
||
destroyChart() {
|
||
if (this.chartInstance) {
|
||
window.TimeSeriesChart.destroy(this.chartInstance);
|
||
this.chartInstance = null;
|
||
}
|
||
},
|
||
|
||
// Alpine lifecycle hooks
|
||
init() {
|
||
if (this.isViewMode && window.Chart) {
|
||
// Use $nextTick to ensure DOM is ready
|
||
this.$nextTick(() => {
|
||
this.initChart();
|
||
});
|
||
|
||
// Watch measurements array for changes and update chart
|
||
this.$watch("data." + this.fieldName, () => {
|
||
if (this.chartInstance) {
|
||
this.updateChart();
|
||
}
|
||
});
|
||
}
|
||
},
|
||
}),
|
||
);
|
||
});
|