From af5ba854d9792a018e72915460f35340f5e007ef Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Wed, 3 Sep 2025 21:59:46 -0700 Subject: [PATCH] improvements --- models/migrations/migrations.go | 1 + models/migrations/v1_25/v322.go | 25 +++++ models/project/workflows.go | 6 ++ routers/web/projects/workflows.go | 96 +++++++++++++++++++ routers/web/web.go | 4 + .../components/projects/ProjectWorkflow.vue | 50 ++++++++-- .../js/components/projects/WorkflowStore.ts | 30 +++--- 7 files changed, 189 insertions(+), 23 deletions(-) create mode 100644 models/migrations/v1_25/v322.go diff --git a/models/migrations/migrations.go b/models/migrations/migrations.go index 88967a8b87..6a223237c7 100644 --- a/models/migrations/migrations.go +++ b/models/migrations/migrations.go @@ -393,6 +393,7 @@ func prepareMigrationTasks() []*migration { // Gitea 1.24.0 ends at database version 321 newMigration(321, "Use LONGTEXT for some columns and fix review_state.updated_files column", v1_25.UseLongTextInSomeColumnsAndFixBugs), + newMigration(322, "Add new table project_workflow", v1_25.AddEnabledToProjectWorkflow), } return preparedMigrations } diff --git a/models/migrations/v1_25/v322.go b/models/migrations/v1_25/v322.go new file mode 100644 index 0000000000..b03479f6df --- /dev/null +++ b/models/migrations/v1_25/v322.go @@ -0,0 +1,25 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package v1_25 + +import ( + "code.gitea.io/gitea/modules/timeutil" + + "xorm.io/xorm" +) + +func AddEnabledToProjectWorkflow(x *xorm.Engine) error { + type ProjectWorkflow struct { + ID int64 + ProjectID int64 `xorm:"INDEX"` + WorkflowEvent int `xorm:"INDEX"` + WorkflowFilters string `xorm:"TEXT json"` + WorkflowActions string `xorm:"TEXT json"` + Enabled bool `xorm:"DEFAULT true"` + CreatedUnix timeutil.TimeStamp `xorm:"created"` + UpdatedUnix timeutil.TimeStamp `xorm:"updated"` + } + + return x.Sync(&ProjectWorkflow{}) +} diff --git a/models/project/workflows.go b/models/project/workflows.go index 4bfba3a61d..dd9d77f233 100644 --- a/models/project/workflows.go +++ b/models/project/workflows.go @@ -163,6 +163,7 @@ type Workflow struct { WorkflowEvent WorkflowEvent `xorm:"INDEX"` WorkflowFilters []WorkflowFilter `xorm:"TEXT json"` WorkflowActions []WorkflowAction `xorm:"TEXT json"` + Enabled bool `xorm:"DEFAULT true"` CreatedUnix timeutil.TimeStamp `xorm:"created"` UpdatedUnix timeutil.TimeStamp `xorm:"updated"` } @@ -223,3 +224,8 @@ func UpdateWorkflow(ctx context.Context, wf *Workflow) error { _, err := db.GetEngine(ctx).ID(wf.ID).Update(wf) return err } + +func DeleteWorkflow(ctx context.Context, id int64) error { + _, err := db.GetEngine(ctx).ID(id).Delete(&Workflow{}) + return err +} diff --git a/routers/web/projects/workflows.go b/routers/web/projects/workflows.go index 3906ba72b3..db78a9d6c3 100644 --- a/routers/web/projects/workflows.go +++ b/routers/web/projects/workflows.go @@ -137,6 +137,7 @@ func WorkflowsEvents(ctx *context.Context) { 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"` } outputWorkflows := make([]*WorkflowConfig, 0) @@ -163,6 +164,7 @@ func WorkflowsEvents(ctx *context.Context) { Filters: wf.WorkflowFilters, Actions: wf.WorkflowActions, FilterSummary: filterSummary, + Enabled: wf.Enabled, }) } } else { @@ -175,6 +177,7 @@ func WorkflowsEvents(ctx *context.Context) { Filters: []project_model.WorkflowFilter{}, Actions: []project_model.WorkflowAction{}, FilterSummary: "", + Enabled: true, // Default to enabled for new workflows }) } } @@ -392,6 +395,7 @@ func WorkflowsPost(ctx *context.Context) { WorkflowEvent: project_model.WorkflowEvent(form.EventID), WorkflowFilters: filters, WorkflowActions: actions, + Enabled: true, // New workflows are enabled by default } if err := project_model.CreateWorkflow(ctx, wf); err != nil { ctx.ServerError("CreateWorkflow", err) @@ -409,6 +413,7 @@ func WorkflowsPost(ctx *context.Context) { "filters": wf.WorkflowFilters, "actions": wf.WorkflowActions, "filter_summary": filterSummary, + "enabled": wf.Enabled, }, }) } else { @@ -441,7 +446,98 @@ func WorkflowsPost(ctx *context.Context) { "filters": wf.WorkflowFilters, "actions": wf.WorkflowActions, "filter_summary": filterSummary, + "enabled": wf.Enabled, }, }) } } + +func WorkflowsStatus(ctx *context.Context) { + projectID := ctx.PathParamInt64("id") + workflowID, _ := strconv.ParseInt(ctx.PathParam("workflow_id"), 10, 64) + + p, err := project_model.GetProjectByID(ctx, projectID) + if err != nil { + if project_model.IsErrProjectNotExist(err) { + ctx.NotFound(nil) + } else { + ctx.ServerError("GetProjectByID", err) + } + return + } + if p.Type == project_model.TypeRepository && p.RepoID != ctx.Repo.Repository.ID { + ctx.NotFound(nil) + return + } + if (p.Type == project_model.TypeOrganization || p.Type == project_model.TypeIndividual) && p.OwnerID != ctx.ContextUser.ID { + ctx.NotFound(nil) + return + } + + wf, err := project_model.GetWorkflowByID(ctx, workflowID) + if err != nil { + ctx.ServerError("GetWorkflowByID", err) + return + } + if wf.ProjectID != projectID { + ctx.NotFound(nil) + return + } + + // Get enabled status from form + enabledStr := ctx.Req.FormValue("enabled") + enabled := enabledStr == "true" + + wf.Enabled = enabled + if err := project_model.UpdateWorkflow(ctx, wf); err != nil { + ctx.ServerError("UpdateWorkflow", err) + return + } + + ctx.JSON(http.StatusOK, map[string]any{ + "success": true, + "enabled": wf.Enabled, + }) +} + +func WorkflowsDelete(ctx *context.Context) { + projectID := ctx.PathParamInt64("id") + workflowID, _ := strconv.ParseInt(ctx.PathParam("workflow_id"), 10, 64) + + p, err := project_model.GetProjectByID(ctx, projectID) + if err != nil { + if project_model.IsErrProjectNotExist(err) { + ctx.NotFound(nil) + } else { + ctx.ServerError("GetProjectByID", err) + } + return + } + if p.Type == project_model.TypeRepository && p.RepoID != ctx.Repo.Repository.ID { + ctx.NotFound(nil) + return + } + if (p.Type == project_model.TypeOrganization || p.Type == project_model.TypeIndividual) && p.OwnerID != ctx.ContextUser.ID { + ctx.NotFound(nil) + return + } + + wf, err := project_model.GetWorkflowByID(ctx, workflowID) + if err != nil { + ctx.ServerError("GetWorkflowByID", err) + return + } + if wf.ProjectID != projectID { + ctx.NotFound(nil) + return + } + + if err := project_model.DeleteWorkflow(ctx, workflowID); err != nil { + ctx.ServerError("DeleteWorkflow", err) + return + } + + ctx.JSON(http.StatusOK, map[string]any{ + "success": true, + }) +} diff --git a/routers/web/web.go b/routers/web/web.go index 64364aec55..c3f6879ca5 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -1042,6 +1042,8 @@ func registerWebRoutes(m *web.Router) { m.Get("", projects.Workflows) m.Get("/{workflow_id}", projects.Workflows) m.Post("/{workflow_id}", web.Bind(projects.WorkflowsPostForm{}), projects.WorkflowsPost) + m.Post("/{workflow_id}/status", projects.WorkflowsStatus) + m.Post("/{workflow_id}/delete", projects.WorkflowsDelete) }) m.Group("", func() { //nolint:dupl // duplicates lines 1421-1441 m.Get("/new", org.RenderNewProject) @@ -1434,6 +1436,8 @@ func registerWebRoutes(m *web.Router) { m.Get("", projects.Workflows) m.Get("/{workflow_id}", projects.Workflows) m.Post("/{workflow_id}", web.Bind(projects.WorkflowsPostForm{}), projects.WorkflowsPost) + m.Post("/{workflow_id}/status", projects.WorkflowsStatus) + m.Post("/{workflow_id}/delete", projects.WorkflowsDelete) }) m.Group("", func() { //nolint:dupl // duplicates lines 1034-1054 m.Get("/new", repo.RenderNewProject) diff --git a/web_src/js/components/projects/ProjectWorkflow.vue b/web_src/js/components/projects/ProjectWorkflow.vue index f8e0fefa28..58fe11b24c 100644 --- a/web_src/js/components/projects/ProjectWorkflow.vue +++ b/web_src/js/components/projects/ProjectWorkflow.vue @@ -62,9 +62,11 @@ const deleteWorkflow = async () => { return; } - const currentBaseEventType = store.selectedWorkflow.base_event_type; + const currentBaseEventType = store.selectedWorkflow.base_event_type || store.selectedWorkflow.workflow_event || store.selectedWorkflow.event_id; const currentCapabilities = store.selectedWorkflow.capabilities; - const currentDisplayName = store.selectedWorkflow.display_name.split(' (')[0]; // Remove filter suffix + // Extract base name without any parenthetical descriptions + const currentDisplayName = (store.selectedWorkflow.display_name || store.selectedWorkflow.workflow_event || store.selectedWorkflow.event_id) + .replace(/\s*\([^)]*\)\s*/g, ''); // If deleting a temporary workflow (clone/new), just remove from list if (store.selectedWorkflow.id === 0) { @@ -135,8 +137,7 @@ const selectWorkflowEvent = async (event) => { const saveWorkflow = async () => { await store.saveWorkflow(); - // After saving, refresh the list to show the new workflow - store.workflowEvents = await store.loadEvents(); + // The store.saveWorkflow already handles reloading events // Clear previous selection after successful save previousSelection.value = null; @@ -148,6 +149,36 @@ const isWorkflowConfigured = (event) => { return !Number.isNaN(parseInt(event.event_id)) || (event.id !== undefined && event.id > 0); }; +// Generate filter description for display name +const getFilterDescription = (workflow) => { + if (!workflow.filters || !Array.isArray(workflow.filters) || workflow.filters.length === 0) { + return ''; + } + + const descriptions = []; + for (const filter of workflow.filters) { + if (filter.type === 'issue_type' && filter.value) { + if (filter.value === 'issue') { + descriptions.push('Issues'); + } else if (filter.value === 'pull_request') { + descriptions.push('Pull Requests'); + } + } + // Add more filter types here as needed + } + + return descriptions.length > 0 ? ` (${descriptions.join(', ')})` : ''; +}; + +// Get display name with filters +const getWorkflowDisplayName = (workflow) => { + const baseName = workflow.display_name || workflow.workflow_event || workflow.event_id; + if (isWorkflowConfigured(workflow)) { + return baseName + getFilterDescription(workflow); + } + return baseName; +}; + // Get flat list of all workflows - use cached data to prevent frequent recomputation const workflowList = computed(() => { // Use a stable reference to prevent unnecessary DOM updates @@ -159,7 +190,8 @@ const workflowList = computed(() => { return workflows.map((workflow) => ({ ...workflow, isConfigured: isWorkflowConfigured(workflow), - base_event_type: workflow.event_id, + base_event_type: workflow.base_event_type || workflow.workflow_event || workflow.event_id, + display_name: getWorkflowDisplayName(workflow), })); }); @@ -200,15 +232,19 @@ const cloneWorkflow = (sourceWorkflow) => { }; const tempId = `clone-${sourceWorkflow.base_event_type || sourceWorkflow.workflow_event}-${Date.now()}`; + // Extract base name without filter descriptions + const baseName = (sourceWorkflow.display_name || sourceWorkflow.workflow_event || sourceWorkflow.event_id) + .replace(/\s*\([^)]*\)\s*/g, ''); // Remove any parenthetical descriptions + const clonedWorkflow = { id: 0, event_id: tempId, - display_name: `${sourceWorkflow.display_name.split(' (')[0]} (Copy)`, // Add copy suffix + display_name: `${baseName} (Copy)`, // Add copy suffix capabilities: sourceWorkflow.capabilities, filters: Array.from(sourceWorkflow.filters || []), actions: Array.from(sourceWorkflow.actions || []), filter_summary: '', - base_event_type: sourceWorkflow.base_event_type || sourceWorkflow.workflow_event, + base_event_type: sourceWorkflow.base_event_type || sourceWorkflow.workflow_event || sourceWorkflow.event_id, enabled: true, }; diff --git a/web_src/js/components/projects/WorkflowStore.ts b/web_src/js/components/projects/WorkflowStore.ts index 35886570b6..20c37fba0d 100644 --- a/web_src/js/components/projects/WorkflowStore.ts +++ b/web_src/js/components/projects/WorkflowStore.ts @@ -161,27 +161,25 @@ export function createWorkflowStore(props: { projectLink: string, eventID: strin const result = await response.json(); console.log('Response result:', result); if (result.success && result.workflow) { - // For new workflows, add to the store - if (store.selectedWorkflow.id === 0 || store.selectedWorkflow.event_id.startsWith('new-')) { - store.workflowEvents.push(result.workflow); + // Always reload the events list to get the updated structure + // This ensures we have both the base event and the new filtered event + const wasNewWorkflow = store.selectedWorkflow.id === 0 || + store.selectedWorkflow.event_id.startsWith('new-') || + store.selectedWorkflow.event_id.startsWith('clone-'); - // Update URL to use the new workflow ID - const newUrl = `${props.projectLink}/workflows/${result.workflow.event_id}`; - window.history.replaceState({eventId: result.workflow.event_id}, '', newUrl); - } else { - // Update existing workflow - const existingIndex = store.workflowEvents.findIndex((e) => e.event_id === store.selectedWorkflow.event_id); - if (existingIndex >= 0) { - store.workflowEvents[existingIndex] = { - ...store.workflowEvents[existingIndex], - ...result.workflow, - }; - } - } + // Reload events from server to get the correct event structure + await store.loadEvents(); // Update selected workflow and selectedItem store.selectedWorkflow = result.workflow; store.selectedItem = result.workflow.event_id; + + // Update URL to use the new workflow ID + if (wasNewWorkflow) { + const newUrl = `${props.projectLink}/workflows/${result.workflow.event_id}`; + window.history.replaceState({eventId: result.workflow.event_id}, '', newUrl); + } + alert('Workflow saved successfully!'); } else { console.error('Unexpected response format:', result);