improvements

pull/30205/head
Lunny Xiao 2025-09-03 21:59:46 -07:00
parent 5cd2901d1e
commit af5ba854d9
No known key found for this signature in database
GPG Key ID: C3B7C91B632F738A
7 changed files with 189 additions and 23 deletions

View File

@ -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
}

View File

@ -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{})
}

View File

@ -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
}

View File

@ -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,
})
}

View File

@ -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)

View File

@ -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,
};

View File

@ -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);