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' }}
+
+
+