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}",