Added user input tracking + Started error management for in the Designer
This commit is contained in:
@@ -18,4 +18,7 @@ icon_add_regular = NotStr("""<svg name="add" xmlns="http://www.w3.org/2000/svg"
|
|||||||
</path>
|
</path>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
""")
|
""")
|
||||||
|
|
||||||
|
# Fluent ErrorCircle20Regular
|
||||||
|
icon_error = NotStr("""<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 20 20"><g fill="none"><path d="M10 2a8 8 0 1 1 0 16a8 8 0 0 1 0-16zm0 1a7 7 0 1 0 0 14a7 7 0 0 0 0-14zm0 9.5a.75.75 0 1 1 0 1.5a.75.75 0 0 1 0-1.5zM10 6a.5.5 0 0 1 .492.41l.008.09V11a.5.5 0 0 1-.992.09L9.5 11V6.5A.5.5 0 0 1 10 6z" fill="currentColor"></path></g></svg>""")
|
||||||
@@ -88,4 +88,125 @@ function enableTooltip() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
element.removeAttribute("mmt-no-tooltip");
|
element.removeAttribute("mmt-no-tooltip");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,9 +4,10 @@
|
|||||||
|
|
||||||
using `_id={WORKFLOW_DESIGNER_INSTANCE_ID}{session['user_id']}{get_unique_id()}`
|
using `_id={WORKFLOW_DESIGNER_INSTANCE_ID}{session['user_id']}{get_unique_id()}`
|
||||||
|
|
||||||
| Name | value |
|
| Name | value |
|
||||||
|------------|----------------|
|
|---------------|------------------|
|
||||||
| Canvas | `c_{self._id}` |
|
| Canvas | `c_{self._id}` |
|
||||||
| Designer | `d_{self._id}` |
|
| Designer | `d_{self._id}` |
|
||||||
| Properties | `p_{self._id}` |
|
| Error Message | `err_{self._id}` |
|
||||||
| Spliter | `s_{self._id}` |
|
| Properties | `p_{self._id}` |
|
||||||
|
| Spliter | `s_{self._id}` |
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from fastcore.basics import NotStr
|
|||||||
from fasthtml.components import *
|
from fasthtml.components import *
|
||||||
from fasthtml.xtend import Script
|
from fasthtml.xtend import Script
|
||||||
|
|
||||||
|
from assets.icons import icon_error
|
||||||
from components.BaseComponent import BaseComponent
|
from components.BaseComponent import BaseComponent
|
||||||
from components.workflows.assets.icons import icon_play, icon_pause, icon_stop
|
from components.workflows.assets.icons import icon_play, icon_pause, icon_stop
|
||||||
from components.workflows.commands import WorkflowDesignerCommandManager
|
from components.workflows.commands import WorkflowDesignerCommandManager
|
||||||
@@ -63,6 +64,7 @@ class WorkflowDesigner(BaseComponent):
|
|||||||
self._state = self._db.load_state(key)
|
self._state = self._db.load_state(key)
|
||||||
self._boundaries = boundaries
|
self._boundaries = boundaries
|
||||||
self.commands = WorkflowDesignerCommandManager(self)
|
self.commands = WorkflowDesignerCommandManager(self)
|
||||||
|
self._error_message = None
|
||||||
|
|
||||||
def set_boundaries(self, boundaries: dict):
|
def set_boundaries(self, boundaries: dict):
|
||||||
self._boundaries = boundaries
|
self._boundaries = boundaries
|
||||||
@@ -187,11 +189,13 @@ class WorkflowDesigner(BaseComponent):
|
|||||||
player_settings=WorkflowsPlayerSettings(workflow_name,
|
player_settings=WorkflowsPlayerSettings(workflow_name,
|
||||||
list(self._state.components.values())),
|
list(self._state.components.values())),
|
||||||
boundaries=boundaries)
|
boundaries=boundaries)
|
||||||
|
try:
|
||||||
|
player.run()
|
||||||
|
self.tabs_manager.add_tab(f"Workflow {workflow_name}", player, player.key)
|
||||||
|
return self.tabs_manager.refresh()
|
||||||
|
|
||||||
player.run()
|
except Exception as e:
|
||||||
|
return self.error_message(str(e))
|
||||||
self.tabs_manager.add_tab(f"Workflow {workflow_name}", player, player.key)
|
|
||||||
return self.tabs_manager.refresh()
|
|
||||||
|
|
||||||
def on_processor_details_event(self, component_id: str, event_name: str, details: dict):
|
def on_processor_details_event(self, component_id: str, event_name: str, details: dict):
|
||||||
if component_id in self._state.components:
|
if component_id in self._state.components:
|
||||||
@@ -203,11 +207,19 @@ class WorkflowDesigner(BaseComponent):
|
|||||||
|
|
||||||
return self.refresh_properties()
|
return self.refresh_properties()
|
||||||
|
|
||||||
|
def error_message(self, message: str):
|
||||||
|
self._error_message = message
|
||||||
|
return self.tabs_manager.refresh()
|
||||||
|
|
||||||
def __ft__(self):
|
def __ft__(self):
|
||||||
return Div(
|
return Div(
|
||||||
H1(f"{self._designer_settings.workflow_name}", cls="text-xl font-bold"),
|
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"),
|
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(),
|
self._mk_designer(),
|
||||||
Div(cls="wkf-splitter", id=f"s_{self._id}"),
|
Div(cls="wkf-splitter", id=f"s_{self._id}"),
|
||||||
self._mk_properties(),
|
self._mk_properties(),
|
||||||
@@ -292,6 +304,18 @@ class WorkflowDesigner(BaseComponent):
|
|||||||
cls=f"media-controls flex m-2"
|
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):
|
def _mk_processor_properties(self, component, processor_name):
|
||||||
if processor_name == "Jira":
|
if processor_name == "Jira":
|
||||||
return self._mk_jira_processor_details(component)
|
return self._mk_jira_processor_details(component)
|
||||||
@@ -304,7 +328,7 @@ class WorkflowDesigner(BaseComponent):
|
|||||||
|
|
||||||
return Div('Not defined yet !')
|
return Div('Not defined yet !')
|
||||||
|
|
||||||
def _mk_properties_details(self):
|
def _mk_properties_details(self, component_id, allow_component_selection=False):
|
||||||
def _mk_header():
|
def _mk_header():
|
||||||
return Div(
|
return Div(
|
||||||
Div(
|
Div(
|
||||||
@@ -326,7 +350,7 @@ class WorkflowDesigner(BaseComponent):
|
|||||||
**self.commands.select_processor(component_id)
|
**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
|
return None
|
||||||
else:
|
else:
|
||||||
component_id = self._state.selected_component_id
|
component_id = self._state.selected_component_id
|
||||||
@@ -334,19 +358,23 @@ class WorkflowDesigner(BaseComponent):
|
|||||||
selected_processor_name = component.properties["processor_name"]
|
selected_processor_name = component.properties["processor_name"]
|
||||||
icon = COMPONENT_TYPES[component.type]["icon"]
|
icon = COMPONENT_TYPES[component.type]["icon"]
|
||||||
color = COMPONENT_TYPES[component.type]["color"]
|
color = COMPONENT_TYPES[component.type]["color"]
|
||||||
return Form(
|
return Div(
|
||||||
_mk_header(),
|
Form(
|
||||||
_mk_select(),
|
_mk_header(),
|
||||||
self._mk_processor_properties(component, selected_processor_name),
|
_mk_select(),
|
||||||
mk_dialog_buttons(cls="mt-4",
|
self._mk_processor_properties(component, selected_processor_name),
|
||||||
on_ok=self.commands.save_properties(component_id),
|
mk_dialog_buttons(cls="mt-4",
|
||||||
on_cancel=self.commands.cancel_properties(component_id)),
|
on_ok=self.commands.save_properties(component_id),
|
||||||
cls="font-mono text-sm",
|
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):
|
def _mk_properties(self):
|
||||||
return Div(
|
return Div(
|
||||||
self._mk_properties_details(),
|
self._mk_properties_details(self._state.selected_component_id),
|
||||||
cls="p-2 bg-base-100 rounded-lg border",
|
cls="p-2 bg-base-100 rounded-lg border",
|
||||||
style=f"height:{self._get_properties_height()}px;",
|
style=f"height:{self._get_properties_height()}px;",
|
||||||
id=f"p_{self._id}",
|
id=f"p_{self._id}",
|
||||||
|
|||||||
Reference in New Issue
Block a user