From b191ded690aa572ecf9bdd50a1d19ac9e9243331 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Wed, 22 Oct 2025 22:40:49 -0700 Subject: [PATCH] Fix --- models/project/column.go | 26 ++- models/project/workflows.go | 8 +- models/user/user.go | 6 +- models/user/user_list.go | 4 +- models/user/user_system.go | 30 +-- routers/web/projects/workflows.go | 44 ++-- routers/web/repo/projects.go | 3 +- routers/web/repo/pull.go | 1 + services/issue/project.go | 31 +++ services/notify/notifier.go | 2 + services/notify/notify.go | 8 + services/notify/null.go | 4 + services/projects/issue.go | 27 +++ services/projects/workflow_notifier.go | 142 +++++++++++- .../components/projects/ProjectWorkflow.vue | 210 ++++++++---------- .../js/components/projects/WorkflowStore.ts | 51 +++-- 16 files changed, 382 insertions(+), 215 deletions(-) create mode 100644 services/issue/project.go diff --git a/models/project/column.go b/models/project/column.go index 2c91935478..814794fac7 100644 --- a/models/project/column.go +++ b/models/project/column.go @@ -46,6 +46,9 @@ type Column struct { Color string `xorm:"VARCHAR(7)"` ProjectID int64 `xorm:"INDEX NOT NULL"` + + Project *Project `xorm:"-"` + CreatorID int64 `xorm:"NOT NULL"` NumIssues int64 `xorm:"-"` @@ -59,6 +62,19 @@ func (Column) TableName() string { return "project_board" // TODO: the legacy table name should be project_column } +func (c *Column) LoadProject(ctx context.Context) error { + if c.Project != nil { + return nil + } + + project, err := GetProjectByID(ctx, c.ProjectID) + if err != nil { + return err + } + c.Project = project + return nil +} + func (c *Column) GetIssues(ctx context.Context) ([]*ProjectIssue, error) { issues := make([]*ProjectIssue, 0, 5) if err := db.GetEngine(ctx).Where("project_id=?", c.ProjectID). @@ -213,16 +229,16 @@ func GetColumn(ctx context.Context, columnID int64) (*Column, error) { return column, nil } -func GetColumnByProjectIDAndColumnName(ctx context.Context, projectID int64, columnName string) (*Column, error) { - board := new(Column) - has, err := db.GetEngine(ctx).Where("project_id=? AND title=?", projectID, columnName).Get(board) +func GetColumnByProjectIDAndColumnID(ctx context.Context, projectID, columnID int64) (*Column, error) { + column := new(Column) + has, err := db.GetEngine(ctx).Where("project_id=? AND id=?", projectID, columnID).Get(column) if err != nil { return nil, err } else if !has { - return nil, ErrProjectColumnNotExist{ProjectID: projectID, Name: columnName} + return nil, ErrProjectColumnNotExist{ProjectID: projectID, ColumnID: columnID} } - return board, nil + return column, nil } // UpdateColumn updates a project column diff --git a/models/project/workflows.go b/models/project/workflows.go index 35d923afdf..1e92f434b3 100644 --- a/models/project/workflows.go +++ b/models/project/workflows.go @@ -94,8 +94,8 @@ const ( ) type WorkflowFilter struct { - Type WorkflowFilterType - Value string // e.g., "issue", "pull_request", etc. depends on the filter type definition + Type WorkflowFilterType `json:"type"` + Value string `json:"value"` } type WorkflowActionType string @@ -108,8 +108,8 @@ const ( ) type WorkflowAction struct { - ActionType WorkflowActionType - ActionValue string + Type WorkflowActionType `json:"type"` + Value string `json:"value"` } // WorkflowEventCapabilities defines what filters and actions are available for each event diff --git a/models/user/user.go b/models/user/user.go index 78b30203fa..4d27abeb1e 100644 --- a/models/user/user.go +++ b/models/user/user.go @@ -561,9 +561,9 @@ var globalVars = sync.OnceValue(func() *globalVarsStruct { emailRegexp: regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]*@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$"), systemUserNewFuncs: map[int64]func() *User{ - GhostUserID: NewGhostUser, - ActionsUserID: NewActionsUser, - WorkflowsUserID: NewWorkflowsUser, + GhostUserID: NewGhostUser, + ActionsUserID: NewActionsUser, + ProjectWorkflowsUserID: NewProjectWorkflowsUser, }, } }) diff --git a/models/user/user_list.go b/models/user/user_list.go index 2aecd5cac0..0114db8dbd 100644 --- a/models/user/user_list.go +++ b/models/user/user_list.go @@ -36,8 +36,8 @@ func GetPossibleUserFromMap(userID int64, usererMaps map[int64]*User) *User { return NewGhostUser() case ActionsUserID: return NewActionsUser() - case WorkflowsUserID: - return NewWorkflowsUser() + case ProjectWorkflowsUserID: + return NewProjectWorkflowsUser() case 0: return nil default: diff --git a/models/user/user_system.go b/models/user/user_system.go index dd786b7cc7..3b06e6b7ec 100644 --- a/models/user/user_system.go +++ b/models/user/user_system.go @@ -66,34 +66,34 @@ func (u *User) IsGiteaActions() bool { } const ( - WorkflowsUserID int64 = -3 - WorkflowsUserName = "gitea-workflows" - WorkflowsUserEmail = "workflows@gitea.io" + ProjectWorkflowsUserID int64 = -3 + ProjectWorkflowsUserName = "project-workflows" + ProjectWorkflowsUserEmail = "workflows@gitea.io" ) func IsGiteaWorkflowsUserName(name string) bool { - return strings.EqualFold(name, WorkflowsUserName) + return strings.EqualFold(name, ProjectWorkflowsUserName) } -// NewWorkflowsUser creates and returns a fake user for running the workflows. -func NewWorkflowsUser() *User { +// NewProjectWorkflowsUser creates and returns a fake user for running the project workflows. +func NewProjectWorkflowsUser() *User { return &User{ - ID: WorkflowsUserID, - Name: WorkflowsUserName, - LowerName: WorkflowsUserName, + ID: ProjectWorkflowsUserID, + Name: ProjectWorkflowsUserName, + LowerName: ProjectWorkflowsUserName, IsActive: true, - FullName: "Gitea Workflows", - Email: WorkflowsUserEmail, + FullName: "Project Workflows", + Email: ProjectWorkflowsUserEmail, KeepEmailPrivate: true, - LoginName: WorkflowsUserName, + LoginName: ProjectWorkflowsUserName, Type: UserTypeBot, AllowCreateOrganization: true, Visibility: structs.VisibleTypePublic, } } -func (u *User) IsGiteaWorkflows() bool { - return u != nil && u.ID == WorkflowsUserID +func (u *User) IsProjectWorkflows() bool { + return u != nil && u.ID == ProjectWorkflowsUserID } func GetSystemUserByName(name string) *User { @@ -104,7 +104,7 @@ func GetSystemUserByName(name string) *User { return NewActionsUser() } if IsGiteaWorkflowsUserName(name) { - return NewWorkflowsUser() + return NewProjectWorkflowsUser() } return nil } diff --git a/routers/web/projects/workflows.go b/routers/web/projects/workflows.go index 6e7f5b9a55..f54fc83fdd 100644 --- a/routers/web/projects/workflows.go +++ b/routers/web/projects/workflows.go @@ -58,14 +58,6 @@ func convertFormToFilters(formFilters map[string]string) []project_model.Workflo return filters } -func convertFiltersToMap(filters []project_model.WorkflowFilter) map[string]string { - filterMap := make(map[string]string) - for _, filter := range filters { - filterMap[string(filter.Type)] = filter.Value - } - return filterMap -} - // convertFormToActions converts form actions to WorkflowAction objects func convertFormToActions(formActions map[string]any) []project_model.WorkflowAction { actions := make([]project_model.WorkflowAction, 0) @@ -73,12 +65,12 @@ func convertFormToActions(formActions map[string]any) []project_model.WorkflowAc for key, value := range formActions { switch key { case "column": - if floatValue, ok := value.(float64); ok { - floatValueInt := int64(floatValue) + if floatValue, ok := value.(string); ok { + floatValueInt, _ := strconv.ParseInt(floatValue, 10, 64) if floatValueInt > 0 { actions = append(actions, project_model.WorkflowAction{ - ActionType: project_model.WorkflowActionTypeColumn, - ActionValue: strconv.FormatInt(floatValueInt, 10), + Type: project_model.WorkflowActionTypeColumn, + Value: strconv.FormatInt(floatValueInt, 10), }) } } @@ -87,8 +79,8 @@ func convertFormToActions(formActions map[string]any) []project_model.WorkflowAc for _, label := range labels { if label != "" { actions = append(actions, project_model.WorkflowAction{ - ActionType: project_model.WorkflowActionTypeAddLabels, - ActionValue: label, + Type: project_model.WorkflowActionTypeAddLabels, + Value: label, }) } } @@ -98,8 +90,8 @@ func convertFormToActions(formActions map[string]any) []project_model.WorkflowAc for _, label := range labels { if label != "" { actions = append(actions, project_model.WorkflowAction{ - ActionType: project_model.WorkflowActionTypeRemoveLabels, - ActionValue: label, + Type: project_model.WorkflowActionTypeRemoveLabels, + Value: label, }) } } @@ -107,8 +99,8 @@ func convertFormToActions(formActions map[string]any) []project_model.WorkflowAc case "closeIssue": if boolValue, ok := value.(bool); ok && boolValue { actions = append(actions, project_model.WorkflowAction{ - ActionType: project_model.WorkflowActionTypeClose, - ActionValue: "true", + Type: project_model.WorkflowActionTypeClose, + Value: "true", }) } } @@ -117,14 +109,6 @@ func convertFormToActions(formActions map[string]any) []project_model.WorkflowAc return actions } -func convertActionsToMap(actions []project_model.WorkflowAction) map[string]any { - actionMap := make(map[string]any) - for _, action := range actions { - actionMap[string(action.ActionType)] = action.ActionValue - } - return actionMap -} - func WorkflowsEvents(ctx *context.Context) { projectID := ctx.PathParamInt64("id") p, err := project_model.GetProjectByID(ctx, projectID) @@ -156,8 +140,8 @@ func WorkflowsEvents(ctx *context.Context) { EventID string `json:"event_id"` DisplayName string `json:"display_name"` Capabilities project_model.WorkflowEventCapabilities `json:"capabilities"` - Filters map[string]string `json:"filters"` - Actions map[string]any `json:"actions"` + 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"` } @@ -183,8 +167,8 @@ func WorkflowsEvents(ctx *context.Context) { EventID: strconv.FormatInt(wf.ID, 10), DisplayName: string(ctx.Tr(wf.WorkflowEvent.LangKey())) + filterSummary, Capabilities: capabilities[event], - Filters: convertFiltersToMap(wf.WorkflowFilters), - Actions: convertActionsToMap(wf.WorkflowActions), + Filters: wf.WorkflowFilters, + Actions: wf.WorkflowActions, FilterSummary: filterSummary, Enabled: wf.Enabled, }) diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go index a57976b4ca..eb1af2d1a2 100644 --- a/routers/web/repo/projects.go +++ b/routers/web/repo/projects.go @@ -27,6 +27,7 @@ import ( shared_user "code.gitea.io/gitea/routers/web/shared/user" "code.gitea.io/gitea/services/context" "code.gitea.io/gitea/services/forms" + issues_servie "code.gitea.io/gitea/services/issue" project_service "code.gitea.io/gitea/services/projects" ) @@ -446,7 +447,7 @@ func UpdateIssueProject(ctx *context.Context) { if issue.Project != nil && issue.Project.ID == projectID { continue } - if err := issues_model.IssueAssignOrRemoveProject(ctx, issue, ctx.Doer, projectID, 0); err != nil { + if err := issues_servie.IssueAssignOrRemoveProject(ctx, issue, ctx.Doer, projectID, 0); err != nil { if errors.Is(err, util.ErrPermissionDenied) { continue } diff --git a/routers/web/repo/pull.go b/routers/web/repo/pull.go index 1d7e5be31f..56d9093eaf 100644 --- a/routers/web/repo/pull.go +++ b/routers/web/repo/pull.go @@ -1421,6 +1421,7 @@ func CompareAndPullRequestPost(ctx *context.Context) { return } + // FIXME: this should be moved in the function NewPullRequest if projectID > 0 && ctx.Repo.CanWrite(unit.TypeProjects) { if err := issues_model.IssueAssignOrRemoveProject(ctx, pullIssue, ctx.Doer, projectID, 0); err != nil { if !errors.Is(err, util.ErrPermissionDenied) { diff --git a/services/issue/project.go b/services/issue/project.go new file mode 100644 index 0000000000..f818a869b0 --- /dev/null +++ b/services/issue/project.go @@ -0,0 +1,31 @@ +// Copyright 2025 The Gitea Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package issue + +import ( + "context" + + issues_model "code.gitea.io/gitea/models/issues" + project_model "code.gitea.io/gitea/models/project" + user_model "code.gitea.io/gitea/models/user" + "code.gitea.io/gitea/services/notify" +) + +func IssueAssignOrRemoveProject(ctx context.Context, issue *issues_model.Issue, doer *user_model.User, projectID int64, position int) error { + if err := issues_model.IssueAssignOrRemoveProject(ctx, issue, doer, projectID, 0); err != nil { + return err + } + + var newProject *project_model.Project + var err error + if projectID > 0 { + newProject, err = project_model.GetProjectByID(ctx, projectID) + if err != nil { + return err + } + } + + notify.IssueChangeProjects(ctx, doer, issue, newProject) + return nil +} diff --git a/services/notify/notifier.go b/services/notify/notifier.go index 875a70e564..a599f10f6f 100644 --- a/services/notify/notifier.go +++ b/services/notify/notifier.go @@ -10,6 +10,7 @@ import ( git_model "code.gitea.io/gitea/models/git" issues_model "code.gitea.io/gitea/models/issues" packages_model "code.gitea.io/gitea/models/packages" + project_model "code.gitea.io/gitea/models/project" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" @@ -41,6 +42,7 @@ type Notifier interface { IssueChangeRef(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, oldRef string) IssueChangeLabels(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, addedLabels, removedLabels []*issues_model.Label) + IssueChangeProjects(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, newProject *project_model.Project) NewPullRequest(ctx context.Context, pr *issues_model.PullRequest, mentions []*user_model.User) MergePullRequest(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest) diff --git a/services/notify/notify.go b/services/notify/notify.go index 2416cbd2e0..5bff7ea193 100644 --- a/services/notify/notify.go +++ b/services/notify/notify.go @@ -10,6 +10,7 @@ import ( git_model "code.gitea.io/gitea/models/git" issues_model "code.gitea.io/gitea/models/issues" packages_model "code.gitea.io/gitea/models/packages" + project_model "code.gitea.io/gitea/models/project" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" @@ -274,6 +275,13 @@ func IssueChangeLabels(ctx context.Context, doer *user_model.User, issue *issues } } +// IssueChangeProjects notifies change projects to notifiers +func IssueChangeProjects(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, newProject *project_model.Project) { + for _, notifier := range notifiers { + notifier.IssueChangeProjects(ctx, doer, issue, newProject) + } +} + // CreateRepository notifies create repository to notifiers func CreateRepository(ctx context.Context, doer, u *user_model.User, repo *repo_model.Repository) { for _, notifier := range notifiers { diff --git a/services/notify/null.go b/services/notify/null.go index c3085d7c9e..1f612033fe 100644 --- a/services/notify/null.go +++ b/services/notify/null.go @@ -10,6 +10,7 @@ import ( git_model "code.gitea.io/gitea/models/git" issues_model "code.gitea.io/gitea/models/issues" packages_model "code.gitea.io/gitea/models/packages" + project_model "code.gitea.io/gitea/models/project" repo_model "code.gitea.io/gitea/models/repo" user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/git" @@ -143,6 +144,9 @@ func (*NullNotifier) IssueChangeLabels(ctx context.Context, doer *user_model.Use addedLabels, removedLabels []*issues_model.Label) { } +func (*NullNotifier) IssueChangeProjects(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, newProject *project_model.Project) { +} + // CreateRepository places a place holder function func (*NullNotifier) CreateRepository(ctx context.Context, doer, u *user_model.User, repo *repo_model.Repository) { } diff --git a/services/projects/issue.go b/services/projects/issue.go index 0c8cd610ad..25f6400c52 100644 --- a/services/projects/issue.go +++ b/services/projects/issue.go @@ -205,3 +205,30 @@ func LoadIssueNumbersForProject(ctx context.Context, project *project_model.Proj return nil } + +func MoveIssueToAnotherColumn(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, column *project_model.Column) error { + return db.WithTx(ctx, func(ctx context.Context) error { + if err := project_model.MoveIssueToAnotherColumn(ctx, issue.ID, column); err != nil { + return err + } + + if err := column.LoadProject(ctx); err != nil { + return err + } + + // add timeline to issue + if _, err := issues_model.CreateComment(ctx, &issues_model.CreateCommentOptions{ + Type: issues_model.CommentTypeProjectColumn, + Doer: doer, + Repo: issue.Repo, + Issue: issue, + ProjectID: column.ProjectID, + ProjectTitle: column.Project.Title, + ProjectColumnID: column.ID, + ProjectColumnTitle: column.Title, + }); err != nil { + return err + } + return nil + }) +} diff --git a/services/projects/workflow_notifier.go b/services/projects/workflow_notifier.go index ad5f9c395b..2cbf857634 100644 --- a/services/projects/workflow_notifier.go +++ b/services/projects/workflow_notifier.go @@ -6,6 +6,7 @@ package projects import ( "context" "slices" + "strconv" "strings" issues_model "code.gitea.io/gitea/models/issues" @@ -52,14 +53,23 @@ func (m *workflowNotifier) NewIssue(ctx context.Context, issue *issues_model.Iss return } - // Find workflows for the ItemAddedToProject event + // Find workflows for the ItemOpened event for _, workflow := range workflows { - if workflow.WorkflowEvent == project_model.WorkflowEventItemAddedToProject { + if workflow.WorkflowEvent == project_model.WorkflowEventItemOpened { fireIssueWorkflow(ctx, workflow, issue) } } } +func (m *workflowNotifier) NewPullRequest(ctx context.Context, pr *issues_model.PullRequest, mentions []*user_model.User) { + if err := pr.LoadIssue(ctx); err != nil { + log.Error("NewIssue: LoadIssue: %v", err) + return + } + issue := pr.Issue + m.NewIssue(ctx, issue, mentions) +} + func (m *workflowNotifier) IssueChangeStatus(ctx context.Context, doer *user_model.User, commitID string, issue *issues_model.Issue, actionComment *issues_model.Comment, isClosed bool) { if err := issue.LoadRepo(ctx); err != nil { log.Error("IssueChangeStatus: LoadRepo: %v", err) @@ -88,6 +98,111 @@ func (m *workflowNotifier) IssueChangeStatus(ctx context.Context, doer *user_mod } } +func (*workflowNotifier) IssueChangeProjects(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, newProject *project_model.Project) { + if newProject == nil { + return + } + + if err := issue.LoadRepo(ctx); err != nil { + log.Error("IssueChangeStatus: LoadRepo: %v", err) + return + } + + if err := issue.LoadProject(ctx); err != nil { + log.Error("NewIssue: LoadProject: %v", err) + return + } + if issue.Project == nil || issue.Project.ID != newProject.ID { + return + } + + workflows, err := project_model.FindWorkflowsByProjectID(ctx, issue.Project.ID) + if err != nil { + log.Error("IssueChangeStatus: FindWorkflowsByProjectID: %v", err) + return + } + + // Find workflows for the ItemOpened event + for _, workflow := range workflows { + if workflow.WorkflowEvent == project_model.WorkflowEventItemAddedToProject { + fireIssueWorkflow(ctx, workflow, issue) + } + } +} + +func (*workflowNotifier) MergePullRequest(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest) { + if err := pr.LoadIssue(ctx); err != nil { + log.Error("NewIssue: LoadIssue: %v", err) + return + } + issue := pr.Issue + + if err := issue.LoadRepo(ctx); err != nil { + log.Error("IssueChangeStatus: LoadRepo: %v", err) + return + } + + if err := issue.LoadProject(ctx); err != nil { + log.Error("NewIssue: LoadProject: %v", err) + return + } + if issue.Project == nil { + return + } + + workflows, err := project_model.FindWorkflowsByProjectID(ctx, issue.Project.ID) + if err != nil { + log.Error("IssueChangeStatus: FindWorkflowsByProjectID: %v", err) + return + } + + // Find workflows for the PullRequestMerged event + for _, workflow := range workflows { + if workflow.WorkflowEvent == project_model.WorkflowEventPullRequestMerged { + fireIssueWorkflow(ctx, workflow, issue) + } + } +} + +func (m *workflowNotifier) AutoMergePullRequest(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest) { + m.MergePullRequest(ctx, doer, pr) +} + +func (*workflowNotifier) PullRequestReview(ctx context.Context, pr *issues_model.PullRequest, review *issues_model.Review, comment *issues_model.Comment, mentions []*user_model.User) { + if err := pr.LoadIssue(ctx); err != nil { + log.Error("NewIssue: LoadIssue: %v", err) + return + } + issue := pr.Issue + + if err := issue.LoadRepo(ctx); err != nil { + log.Error("IssueChangeStatus: LoadRepo: %v", err) + return + } + + if err := issue.LoadProject(ctx); err != nil { + log.Error("NewIssue: LoadProject: %v", err) + return + } + if issue.Project == nil { + return + } + + workflows, err := project_model.FindWorkflowsByProjectID(ctx, issue.Project.ID) + if err != nil { + log.Error("IssueChangeStatus: FindWorkflowsByProjectID: %v", err) + return + } + + // Find workflows for the PullRequestMerged event + for _, workflow := range workflows { + if (workflow.WorkflowEvent == project_model.WorkflowEventCodeChangesRequested && review.Type == issues_model.ReviewTypeReject) || + (workflow.WorkflowEvent == project_model.WorkflowEventCodeReviewApproved && review.Type == issues_model.ReviewTypeApprove) { + fireIssueWorkflow(ctx, workflow, issue) + } + } +} + func fireIssueWorkflow(ctx context.Context, workflow *project_model.Workflow, issue *issues_model.Issue) { for _, filter := range workflow.WorkflowFilters { switch filter.Type { @@ -103,26 +218,33 @@ func fireIssueWorkflow(ctx context.Context, workflow *project_model.Workflow, is } for _, action := range workflow.WorkflowActions { - switch action.ActionType { + switch action.Type { case project_model.WorkflowActionTypeColumn: - column, err := project_model.GetColumnByProjectIDAndColumnName(ctx, issue.Project.ID, action.ActionValue) - if err != nil { - log.Error("GetColumnByProjectIDAndColumnName: %v", err) + columnID, _ := strconv.ParseInt(action.Value, 10, 64) + if columnID == 0 { + log.Error("Invalid column ID: %s", action.Value) continue } - if err := project_model.AddIssueToColumn(ctx, issue.ID, column); err != nil { - log.Error("AddIssueToColumn: %v", err) + column, err := project_model.GetColumnByProjectIDAndColumnID(ctx, issue.Project.ID, columnID) + if err != nil { + log.Error("GetColumnByProjectIDAndColumnID: %v", err) + continue + } + if err := MoveIssueToAnotherColumn(ctx, user_model.NewProjectWorkflowsUser(), issue, column); err != nil { + log.Error("MoveIssueToAnotherColumn: %v", err) continue } case project_model.WorkflowActionTypeAddLabels: + // TODO: implement adding labels case project_model.WorkflowActionTypeRemoveLabels: + // TODO: implement removing labels case project_model.WorkflowActionTypeClose: - if err := issue_service.CloseIssue(ctx, issue, user_model.NewWorkflowsUser(), ""); err != nil { + if err := issue_service.CloseIssue(ctx, issue, user_model.NewProjectWorkflowsUser(), ""); err != nil { log.Error("CloseIssue: %v", err) continue } default: - log.Error("Unsupported action type: %s", action.ActionType) + log.Error("Unsupported action type: %s", action.Type) } } } diff --git a/web_src/js/components/projects/ProjectWorkflow.vue b/web_src/js/components/projects/ProjectWorkflow.vue index 4d3dde0388..b1952c43d9 100644 --- a/web_src/js/components/projects/ProjectWorkflow.vue +++ b/web_src/js/components/projects/ProjectWorkflow.vue @@ -373,36 +373,6 @@ const isItemSelected = (item) => { return store.selectedItem === item.base_event_type; }; -const _getActionsSummary = (workflow) => { - if (!workflow.actions || workflow.actions.length === 0) { - return ''; - } - - const actions = []; - for (const action of workflow.actions) { - if (action.action_type === 'column') { - const column = store.projectColumns.find((c) => c.id === action.action_value); - if (column) { - actions.push(`Move to "${column.title}"`); - } - } else if (action.action_type === 'add_labels') { - const label = store.projectLabels.find((l) => l.id === action.action_value); - if (label) { - actions.push(`Add label "${label.name}"`); - } - } else if (action.action_type === 'remove_labels') { - const label = store.projectLabels.find((l) => l.id === action.action_value); - if (label) { - actions.push(`Remove label "${label.name}"`); - } - } else if (action.action_type === 'close') { - actions.push('Close issue'); - } - } - - return actions.join(', '); -}; - onMounted(async () => { // Load all necessary data store.workflowEvents = await store.loadEvents(); @@ -626,113 +596,111 @@ onUnmounted(() => { - -
-
-
- -
-
- This workflow will run when: {{ store.selectedWorkflow.display_name }} +
+
+
+ +
+
+ This workflow will run when: {{ store.selectedWorkflow.display_name }} +
+
+
+ + +
+ +
+
+ + +
+ {{ store.workflowFilters.issue_type === 'issue' ? 'Issues' : + store.workflowFilters.issue_type === 'pull_request' ? 'Pull requests' : + 'Issues And Pull Requests' }}
+
- -
- -
-
- - -
- {{ store.workflowFilters.issue_type === 'issue' ? 'Issues' : - store.workflowFilters.issue_type === 'pull_request' ? 'Pull requests' : - 'Issues And Pull Requests' }} -
+ +
+ +
+
+ + +
+ {{ store.projectColumns.find(c => String(c.id) === store.workflowActions.column)?.title || 'None' }}
-
- -
- -
-
- - -
- {{ store.projectColumns.find(c => c.id === store.workflowActions.column)?.title || 'None' }} -
+
+ + +
+ {{ store.workflowActions.add_labels?.map(id => + store.projectLabels.find(l => String(l.id) === id)?.name).join(', ') || 'None' }}
+
-
- - -
- {{ store.workflowActions.add_labels?.map(id => - store.projectLabels.find(l => l.id === id)?.name).join(', ') || 'None' }} -
+
+
+ +
- -
-
- - -
-
- -
{{ store.workflowActions.closeIssue ? 'Yes' : 'No' }}
-
+
+ +
{{ store.workflowActions.closeIssue ? 'Yes' : 'No' }}
+
- -
- - -
- + +
+ + +
diff --git a/web_src/js/components/projects/WorkflowStore.ts b/web_src/js/components/projects/WorkflowStore.ts index 32689d01d8..195f7de186 100644 --- a/web_src/js/components/projects/WorkflowStore.ts +++ b/web_src/js/components/projects/WorkflowStore.ts @@ -33,6 +33,10 @@ export function createWorkflowStore(props: { projectLink: string, eventID: strin try { const response = await GET(`${props.projectLink}/workflows/columns`); store.projectColumns = await response.json(); + console.log('[WorkflowStore] Loaded columns:', store.projectColumns); + if (store.projectColumns.length > 0) { + console.log('[WorkflowStore] First column.id type:', typeof store.projectColumns[0].id, 'value:', store.projectColumns[0].id); + } } catch (error) { console.error('Failed to load project columns:', error); store.projectColumns = []; @@ -48,38 +52,37 @@ export function createWorkflowStore(props: { projectLink: string, eventID: strin // Find the workflow from existing workflowEvents const workflow = store.workflowEvents.find((e) => e.event_id === eventId); - if (workflow && workflow.filters && workflow.actions) { + console.log('[WorkflowStore] loadWorkflowData - eventId:', eventId); + console.log('[WorkflowStore] loadWorkflowData - found workflow:', workflow); + // Load existing configuration from the workflow data // Convert backend filter format to frontend format - const frontendFilters = {issue_type: ''}; - if (workflow.filters && Array.isArray(workflow.filters)) { - for (const filter of workflow.filters) { - if (filter.type === 'issue_type') { - frontendFilters.issue_type = filter.value; - } + const frontendFilters = {issue_type: ''}; + // Convert backend action format to frontend format + const frontendActions = {column: '', add_labels: [], closeIssue: false}; + + if (workflow) { + for (const filter of workflow.filters) { + if (filter.type === 'issue_type') { + frontendFilters.issue_type = filter.value; } } - // Convert backend action format to frontend format - const frontendActions = {column: '', add_labels: [], closeIssue: false}; - if (workflow.actions && Array.isArray(workflow.actions)) { - for (const action of workflow.actions) { - if (action.action_type === 'column') { - frontendActions.column = action.action_value; - } else if (action.action_type === 'add_labels') { - frontendActions.add_labels.push(action.action_value); - } else if (action.action_type === 'close') { - frontendActions.closeIssue = action.action_value === 'true'; - } + for (const action of workflow.actions) { + if (action.type === 'column') { + // Backend returns string, keep as string to match column.id type + frontendActions.column = action.value; + } else if (action.type === 'add_labels') { + // Backend returns string, keep as string to match label.id type + frontendActions.add_labels.push(action.value); + } else if (action.type === 'close') { + frontendActions.closeIssue = action.value === 'true'; } } - - store.workflowFilters = frontendFilters; - store.workflowActions = frontendActions; - } else { - // Reset to defaults for new workflow - store.resetWorkflowData(); } + + store.workflowFilters = frontendFilters; + store.workflowActions = frontendActions; } finally { store.loading = false; }