forked from enviPath/enviPy
[Feature] Dynamic additional information rendering in frontend (#282)
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>
This commit is contained in:
@ -3,10 +3,99 @@
|
||||
<dialog
|
||||
id="update_scenario_additional_information_modal"
|
||||
class="modal"
|
||||
x-data="modalForm()"
|
||||
x-data="{
|
||||
isSubmitting: false,
|
||||
items: [],
|
||||
schemas: {},
|
||||
loading: false,
|
||||
error: null,
|
||||
originalItems: [], // Store original data to detect changes
|
||||
modifiedUuids: new Set(), // Track which items were modified
|
||||
|
||||
async init() {
|
||||
try {
|
||||
this.loading = true;
|
||||
const scenarioUuid = '{{ scenario.uuid }}';
|
||||
const { items, schemas } =
|
||||
await window.AdditionalInformationApi.loadSchemasAndItems(scenarioUuid);
|
||||
this.items = items;
|
||||
this.schemas = schemas;
|
||||
// Store deep copy of original items for comparison
|
||||
this.originalItems = JSON.parse(JSON.stringify(items));
|
||||
} catch (err) {
|
||||
this.error = err.message;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
reset() {
|
||||
this.isSubmitting = false;
|
||||
this.error = null;
|
||||
this.modifiedUuids.clear();
|
||||
},
|
||||
|
||||
updateItemData(uuid, data) {
|
||||
// Update the item's data in the items array
|
||||
const item = this.items.find(i => i.uuid === uuid);
|
||||
if (item) {
|
||||
item.data = data;
|
||||
// Mark this item as modified
|
||||
this.modifiedUuids.add(uuid);
|
||||
}
|
||||
},
|
||||
|
||||
async submit() {
|
||||
if (this.items.length === 0) {
|
||||
this.error = 'No data to update';
|
||||
return;
|
||||
}
|
||||
|
||||
// Filter to only items that were actually modified
|
||||
const modifiedItems = this.items.filter(item => this.modifiedUuids.has(item.uuid));
|
||||
|
||||
if (modifiedItems.length === 0) {
|
||||
this.error = 'No changes to save';
|
||||
return;
|
||||
}
|
||||
|
||||
this.isSubmitting = true;
|
||||
this.error = null;
|
||||
|
||||
try {
|
||||
const scenarioUuid = '{{ scenario.uuid }}';
|
||||
|
||||
// Use the unified API client for sequential, safe updates - only modified items
|
||||
await window.AdditionalInformationApi.updateItems(scenarioUuid, modifiedItems);
|
||||
|
||||
// Close modal and reload page
|
||||
document.getElementById('update_scenario_additional_information_modal').close();
|
||||
window.location.reload();
|
||||
} catch (err) {
|
||||
// Handle validation errors with field-level details
|
||||
if (err.isValidationError && err.fieldErrors) {
|
||||
this.error = err.message;
|
||||
// Dispatch event to set field errors in the specific form
|
||||
if (err.itemUuid) {
|
||||
window.dispatchEvent(new CustomEvent('set-field-errors-for-item', {
|
||||
detail: {
|
||||
uuid: err.itemUuid,
|
||||
fieldErrors: err.fieldErrors
|
||||
}
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
this.error = err.message;
|
||||
}
|
||||
} finally {
|
||||
this.isSubmitting = false;
|
||||
}
|
||||
}
|
||||
}"
|
||||
@close="reset()"
|
||||
@update-item-data.window="updateItemData($event.detail.uuid, $event.detail.data)"
|
||||
>
|
||||
<div class="modal-box">
|
||||
<div class="modal-box max-h-[90vh] max-w-4xl overflow-y-auto">
|
||||
<!-- Header -->
|
||||
<h3 class="text-lg font-bold">Update Additional Information</h3>
|
||||
|
||||
@ -22,18 +111,39 @@
|
||||
|
||||
<!-- Body -->
|
||||
<div class="py-4">
|
||||
<form
|
||||
id="edit-scenario-additional-information-modal-form"
|
||||
accept-charset="UTF-8"
|
||||
action=""
|
||||
method="post"
|
||||
>
|
||||
{% csrf_token %}
|
||||
{% for widget in update_widgets %}
|
||||
{{ widget|safe }}
|
||||
{% endfor %}
|
||||
<input type="hidden" name="hidden" value="set-additional-information" />
|
||||
</form>
|
||||
<!-- Loading state -->
|
||||
{% include "components/modals/loading_state.html" with loading_var="loading" %}
|
||||
|
||||
<!-- Error state -->
|
||||
{% include "components/modals/error_state.html" with error_var="error" %}
|
||||
|
||||
<!-- Items list -->
|
||||
<template x-if="!loading">
|
||||
<div class="space-y-4">
|
||||
<template x-if="items.length === 0">
|
||||
<p class="text-base-content/60">
|
||||
No additional information to update.
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<template x-for="(item, index) in items" :key="item.uuid">
|
||||
<div class="card bg-base-200 shadow-sm">
|
||||
<div class="card-body p-4">
|
||||
<div
|
||||
x-data="schemaRenderer({
|
||||
rjsf: schemas[item.type.toLowerCase()],
|
||||
data: item.data,
|
||||
mode: 'edit'
|
||||
})"
|
||||
x-init="await init(); $watch('data', (value) => { $dispatch('update-item-data', { uuid: item.uuid, data: value }) }, { deep: true })"
|
||||
>
|
||||
{% include "components/schema_form.html" %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
@ -49,10 +159,17 @@
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary"
|
||||
@click="submit('edit-scenario-additional-information-modal-form')"
|
||||
:disabled="isSubmitting"
|
||||
@click="submit()"
|
||||
:disabled="isSubmitting || loading || items.length === 0 || modifiedUuids.size === 0"
|
||||
>
|
||||
<span x-show="!isSubmitting">Update</span>
|
||||
<span x-show="!isSubmitting">
|
||||
<template x-if="modifiedUuids.size > 0">
|
||||
<span x-text="`Update (${modifiedUuids.size})`"></span>
|
||||
</template>
|
||||
<template x-if="modifiedUuids.size === 0">
|
||||
<span>No Changes</span>
|
||||
</template>
|
||||
</span>
|
||||
<span
|
||||
x-show="isSubmitting"
|
||||
class="loading loading-spinner loading-sm"
|
||||
|
||||
Reference in New Issue
Block a user