diff --git a/src/assets/icons.py b/src/assets/icons.py
index d008fc0..81e390a 100644
--- a/src/assets/icons.py
+++ b/src/assets/icons.py
@@ -18,4 +18,7 @@ icon_add_regular = NotStr("""
-""")
\ No newline at end of file
+""")
+
+# Fluent ErrorCircle20Regular
+icon_error = NotStr("""""")
\ No newline at end of file
diff --git a/src/assets/main.js b/src/assets/main.js
index a0a320b..688b733 100644
--- a/src/assets/main.js
+++ b/src/assets/main.js
@@ -88,4 +88,125 @@ function enableTooltip() {
}
element.removeAttribute("mmt-no-tooltip");
-}
\ No newline at end of file
+}
+
+// Function to save form data to browser storage and track user input in real time
+function saveFormData(formId) {
+ const form = document.getElementById(formId);
+ if (!form) {
+ console.error(`Form with ID '${formId}' not found`);
+ return;
+ }
+
+ const storageKey = `formData_${formId}`;
+
+ // Function to save current form state
+ function saveCurrentState() {
+ const formData = {};
+
+ // Get all input elements
+ const inputs = form.querySelectorAll('input, select, textarea');
+
+ inputs.forEach(input => {
+ if (input.type === 'checkbox' || input.type === 'radio') {
+ formData[input.name || input.id] = input.checked;
+ } else {
+ formData[input.name || input.id] = input.value;
+ }
+ });
+
+ // Store in browser storage
+ const dataToStore = {
+ timestamp: new Date().toISOString(),
+ data: formData
+ };
+
+ try {
+ localStorage.setItem(storageKey, JSON.stringify(dataToStore));
+ } catch (error) {
+ console.error('Error saving form data:', error);
+ }
+ }
+
+ // Add event listeners for real-time tracking
+ const inputs = form.querySelectorAll('input, select, textarea');
+
+ inputs.forEach(input => {
+ // For text inputs, textareas, and selects
+ if (input.type === 'text' || input.type === 'email' || input.type === 'password' ||
+ input.type === 'number' || input.type === 'tel' || input.type === 'url' ||
+ input.tagName === 'TEXTAREA' || input.tagName === 'SELECT') {
+
+ // Use 'input' event for real-time tracking
+ input.addEventListener('input', saveCurrentState);
+ // Also use 'change' event as fallback
+ input.addEventListener('change', saveCurrentState);
+ }
+
+ // For checkboxes and radio buttons
+ if (input.type === 'checkbox' || input.type === 'radio') {
+ input.addEventListener('change', saveCurrentState);
+ }
+ });
+
+ // Save initial state
+ saveCurrentState();
+
+ console.debug(`Real-time form tracking enabled for form: ${formId}`);
+}
+
+// Function to restore form data from browser storage
+function restoreFormData(formId) {
+ const form = document.getElementById(formId);
+ if (!form) {
+ console.error(`Form with ID '${formId}' not found`);
+ return;
+ }
+
+ const storageKey = `formData_${formId}`;
+
+ try {
+ const storedData = localStorage.getItem(storageKey);
+
+ if (storedData) {
+ const parsedData = JSON.parse(storedData);
+ const formData = parsedData.data;
+
+ // Restore all input values
+ const inputs = form.querySelectorAll('input, select, textarea');
+
+ inputs.forEach(input => {
+ const key = input.name || input.id;
+ if (formData.hasOwnProperty(key)) {
+ if (input.type === 'checkbox' || input.type === 'radio') {
+ input.checked = formData[key];
+ } else {
+ input.value = formData[key];
+ }
+ }
+ });
+
+ }
+ } catch (error) {
+ console.error('Error restoring form data:', error);
+ }
+}
+
+
+function bindFormData(formId) {
+ console.debug("bindFormData on form " + (formId));
+ restoreFormData(formId);
+ saveFormData(formId);
+}
+
+// Function to clear saved form data
+function clearFormData(formId) {
+ const storageKey = `formData_${formId}`;
+
+ try {
+ localStorage.removeItem(storageKey);
+ console.log(`Cleared saved data for form: ${formId}`);
+ } catch (error) {
+ console.error('Error clearing form data:', error);
+ }
+}
diff --git a/src/components/workflows/Readme.md b/src/components/workflows/Readme.md
index 83af068..7f97e73 100644
--- a/src/components/workflows/Readme.md
+++ b/src/components/workflows/Readme.md
@@ -4,9 +4,10 @@
using `_id={WORKFLOW_DESIGNER_INSTANCE_ID}{session['user_id']}{get_unique_id()}`
-| Name | value |
-|------------|----------------|
-| Canvas | `c_{self._id}` |
-| Designer | `d_{self._id}` |
-| Properties | `p_{self._id}` |
-| Spliter | `s_{self._id}` |
+| Name | value |
+|---------------|------------------|
+| Canvas | `c_{self._id}` |
+| Designer | `d_{self._id}` |
+| Error Message | `err_{self._id}` |
+| Properties | `p_{self._id}` |
+| Spliter | `s_{self._id}` |
diff --git a/src/components/workflows/components/WorkflowDesigner.py b/src/components/workflows/components/WorkflowDesigner.py
index 9fb8bf7..ed05784 100644
--- a/src/components/workflows/components/WorkflowDesigner.py
+++ b/src/components/workflows/components/WorkflowDesigner.py
@@ -4,6 +4,7 @@ from fastcore.basics import NotStr
from fasthtml.components import *
from fasthtml.xtend import Script
+from assets.icons import icon_error
from components.BaseComponent import BaseComponent
from components.workflows.assets.icons import icon_play, icon_pause, icon_stop
from components.workflows.commands import WorkflowDesignerCommandManager
@@ -63,6 +64,7 @@ class WorkflowDesigner(BaseComponent):
self._state = self._db.load_state(key)
self._boundaries = boundaries
self.commands = WorkflowDesignerCommandManager(self)
+ self._error_message = None
def set_boundaries(self, boundaries: dict):
self._boundaries = boundaries
@@ -187,11 +189,13 @@ class WorkflowDesigner(BaseComponent):
player_settings=WorkflowsPlayerSettings(workflow_name,
list(self._state.components.values())),
boundaries=boundaries)
+ try:
+ player.run()
+ self.tabs_manager.add_tab(f"Workflow {workflow_name}", player, player.key)
+ return self.tabs_manager.refresh()
- player.run()
-
- self.tabs_manager.add_tab(f"Workflow {workflow_name}", player, player.key)
- return self.tabs_manager.refresh()
+ except Exception as e:
+ return self.error_message(str(e))
def on_processor_details_event(self, component_id: str, event_name: str, details: dict):
if component_id in self._state.components:
@@ -203,11 +207,19 @@ class WorkflowDesigner(BaseComponent):
return self.refresh_properties()
+ def error_message(self, message: str):
+ self._error_message = message
+ return self.tabs_manager.refresh()
+
def __ft__(self):
return Div(
H1(f"{self._designer_settings.workflow_name}", cls="text-xl font-bold"),
P("Drag components from the toolbox to the canvas to create your workflow.", cls="text-sm mb-6"),
- self._mk_media(),
+ Div(
+ self._mk_media(),
+ self._mk_error_message(),
+ cls="flex mb-2"
+ ),
self._mk_designer(),
Div(cls="wkf-splitter", id=f"s_{self._id}"),
self._mk_properties(),
@@ -292,6 +304,18 @@ class WorkflowDesigner(BaseComponent):
cls=f"media-controls flex m-2"
)
+ def _mk_error_message(self):
+ if not self._error_message:
+ return Div()
+
+ return Div(
+ mk_icon(icon_error),
+ Span(self._error_message, cls="text-sm"),
+ role="alert",
+ cls="alert alert-error alert-outline p-1!",
+ hx_swap_oob='true',
+ )
+
def _mk_processor_properties(self, component, processor_name):
if processor_name == "Jira":
return self._mk_jira_processor_details(component)
@@ -304,7 +328,7 @@ class WorkflowDesigner(BaseComponent):
return Div('Not defined yet !')
- def _mk_properties_details(self):
+ def _mk_properties_details(self, component_id, allow_component_selection=False):
def _mk_header():
return Div(
Div(
@@ -326,7 +350,7 @@ class WorkflowDesigner(BaseComponent):
**self.commands.select_processor(component_id)
)
- if self._state.selected_component_id is None or self._state.selected_component_id not in self._state.components:
+ if component_id is None or component_id not in self._state.components and not allow_component_selection:
return None
else:
component_id = self._state.selected_component_id
@@ -334,19 +358,23 @@ class WorkflowDesigner(BaseComponent):
selected_processor_name = component.properties["processor_name"]
icon = COMPONENT_TYPES[component.type]["icon"]
color = COMPONENT_TYPES[component.type]["color"]
- return Form(
- _mk_header(),
- _mk_select(),
- self._mk_processor_properties(component, selected_processor_name),
- mk_dialog_buttons(cls="mt-4",
- on_ok=self.commands.save_properties(component_id),
- on_cancel=self.commands.cancel_properties(component_id)),
- cls="font-mono text-sm",
+ return Div(
+ Form(
+ _mk_header(),
+ _mk_select(),
+ self._mk_processor_properties(component, selected_processor_name),
+ mk_dialog_buttons(cls="mt-4",
+ on_ok=self.commands.save_properties(component_id),
+ on_cancel=self.commands.cancel_properties(component_id)),
+ cls="font-mono text-sm",
+ id=f"f_{self._id}_{component_id}",
+ ),
+ Script(f"bindFormData('f_{self._id}_{component_id}');")
)
def _mk_properties(self):
return Div(
- self._mk_properties_details(),
+ self._mk_properties_details(self._state.selected_component_id),
cls="p-2 bg-base-100 rounded-lg border",
style=f"height:{self._get_properties_height()}px;",
id=f"p_{self._id}",