From ab5fa408ef75ada3a4758c430caeea1d1ab181e9 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Thu, 23 Oct 2025 11:15:49 -0700 Subject: [PATCH] fix clone --- routers/web/projects/workflows.go | 25 +++++--- .../components/projects/ProjectWorkflow.vue | 63 ++++++++++++++++++- 2 files changed, 78 insertions(+), 10 deletions(-) diff --git a/routers/web/projects/workflows.go b/routers/web/projects/workflows.go index 894d882287..80cf363384 100644 --- a/routers/web/projects/workflows.go +++ b/routers/web/projects/workflows.go @@ -172,14 +172,17 @@ func WorkflowsEvents(ctx *context.Context) { } type WorkflowConfig struct { - ID int64 `json:"id"` - EventID string `json:"event_id"` - DisplayName string `json:"display_name"` - Capabilities project_model.WorkflowEventCapabilities `json:"capabilities"` - Filters []project_model.WorkflowFilter `json:"filters"` - Actions []project_model.WorkflowAction `json:"actions"` - FilterSummary string `json:"filter_summary"` // Human readable filter description - Enabled bool `json:"enabled"` + ID int64 `json:"id"` + EventID string `json:"event_id"` + DisplayName string `json:"display_name"` + BaseEventType string `json:"base_event_type"` // Base event type for grouping + WorkflowEvent string `json:"workflow_event"` // The actual workflow event + Capabilities project_model.WorkflowEventCapabilities `json:"capabilities"` + Filters []project_model.WorkflowFilter `json:"filters"` + Actions []project_model.WorkflowAction `json:"actions"` + FilterSummary string `json:"filter_summary"` // Human readable filter description + Enabled bool `json:"enabled"` + IsConfigured bool `json:"isConfigured"` // Whether this workflow is configured/saved } outputWorkflows := make([]*WorkflowConfig, 0) @@ -202,11 +205,14 @@ func WorkflowsEvents(ctx *context.Context) { ID: wf.ID, EventID: strconv.FormatInt(wf.ID, 10), DisplayName: string(ctx.Tr(wf.WorkflowEvent.LangKey())) + filterSummary, + BaseEventType: string(wf.WorkflowEvent), + WorkflowEvent: string(wf.WorkflowEvent), Capabilities: capabilities[event], Filters: wf.WorkflowFilters, Actions: wf.WorkflowActions, FilterSummary: filterSummary, Enabled: wf.Enabled, + IsConfigured: true, }) } } else { @@ -215,9 +221,12 @@ func WorkflowsEvents(ctx *context.Context) { ID: 0, EventID: event.UUID(), DisplayName: string(ctx.Tr(event.LangKey())), + BaseEventType: string(event), + WorkflowEvent: string(event), Capabilities: capabilities[event], FilterSummary: "", Enabled: true, // Default to enabled for new workflows + IsConfigured: false, }) } } diff --git a/web_src/js/components/projects/ProjectWorkflow.vue b/web_src/js/components/projects/ProjectWorkflow.vue index de00ba2d84..72a0111451 100644 --- a/web_src/js/components/projects/ProjectWorkflow.vue +++ b/web_src/js/components/projects/ProjectWorkflow.vue @@ -49,7 +49,7 @@ const toggleEditMode = () => { if (previousSelection.value) { // If there was a previous selection, return to it if (store.selectedWorkflow && store.selectedWorkflow.id === 0) { - // Remove temporary unsaved workflow from list + // Remove temporary unsaved workflow (new or cloned) from list const tempIndex = store.workflowEvents.findIndex((w) => w.event_id === store.selectedWorkflow.event_id, ); @@ -98,7 +98,7 @@ const deleteWorkflow = async () => { const currentDisplayName = (store.selectedWorkflow.display_name || store.selectedWorkflow.workflow_event || store.selectedWorkflow.event_id) .replace(/\s*\([^)]*\)\s*/g, ''); - // If deleting a temporary workflow (unsaved), just remove from list + // If deleting a temporary workflow (new or cloned, unsaved), just remove from list if (store.selectedWorkflow.id === 0) { const tempIndex = store.workflowEvents.findIndex((w) => w.event_id === store.selectedWorkflow.event_id, @@ -134,6 +134,55 @@ const deleteWorkflow = async () => { setEditMode(false); }; +const cloneWorkflow = (sourceWorkflow) => { + if (!sourceWorkflow) return; + + // Generate a unique temporary ID for the cloned workflow + const tempId = `clone-${sourceWorkflow.base_event_type || sourceWorkflow.workflow_event}-${Date.now()}`; + + // Extract base name without any parenthetical descriptions + const baseName = (sourceWorkflow.display_name || sourceWorkflow.workflow_event || sourceWorkflow.event_id) + .replace(/\s*\([^)]*\)\s*/g, ''); + + // Create a new workflow object based on the source + const clonedWorkflow = { + id: 0, // New workflow + event_id: tempId, + display_name: `${baseName} (Copy)`, + base_event_type: sourceWorkflow.base_event_type || sourceWorkflow.workflow_event || sourceWorkflow.event_id, + workflow_event: sourceWorkflow.workflow_event || sourceWorkflow.base_event_type, + capabilities: sourceWorkflow.capabilities, + filters: JSON.parse(JSON.stringify(sourceWorkflow.filters || [])), // Deep clone + actions: JSON.parse(JSON.stringify(sourceWorkflow.actions || [])), // Deep clone + enabled: false, // Cloned workflows start disabled + isConfigured: false, // Mark as new/unsaved + }; + + // Insert cloned workflow right after the source workflow (keep same type together) + const sourceIndex = store.workflowEvents.findIndex(w => w.event_id === sourceWorkflow.event_id); + if (sourceIndex >= 0) { + store.workflowEvents.splice(sourceIndex + 1, 0, clonedWorkflow); + } else { + // Fallback: add to end if source not found + store.workflowEvents.push(clonedWorkflow); + } + + // Select the cloned workflow and enter edit mode + store.selectedItem = tempId; + store.selectedWorkflow = clonedWorkflow; + + // Load the workflow data into the form + store.loadWorkflowData(tempId); + + // Enter edit mode + previousSelection.value = null; // No previous selection for cloned workflow + setEditMode(true); + + // Update URL + const newUrl = `${props.projectLink}/workflows/${tempId}`; + window.history.pushState({eventId: tempId}, '', newUrl); +}; + const selectWorkflowEvent = async (event) => { // Prevent rapid successive clicks if (store.loading) return; @@ -624,6 +673,16 @@ onUnmounted(() => { {{ store.selectedWorkflow.enabled ? 'Disable' : 'Enable' }} + + +