pull/30205/head
Lunny Xiao 2025-10-22 22:40:49 +07:00
parent f7b8f6ed99
commit b191ded690
No known key found for this signature in database
GPG Key ID: C3B7C91B632F738A
16 changed files with 380 additions and 213 deletions

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -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)
columnID, _ := strconv.ParseInt(action.Value, 10, 64)
if columnID == 0 {
log.Error("Invalid column ID: %s", action.Value)
continue
}
column, err := project_model.GetColumnByProjectIDAndColumnID(ctx, issue.Project.ID, columnID)
if err != nil {
log.Error("GetColumnByProjectIDAndColumnName: %v", err)
log.Error("GetColumnByProjectIDAndColumnID: %v", err)
continue
}
if err := project_model.AddIssueToColumn(ctx, issue.ID, column); err != nil {
log.Error("AddIssueToColumn: %v", err)
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)
}
}
}

@ -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(() => {
</div>
</div>
<!--<form class="ui form form-fetch-action" :action="props.projectLink+'/workflows/'+store.selectedWorkflow.id" method="post">-->
<div class="editor-content">
<div class="form" :class="{ 'readonly': !isInEditMode }">
<div class="field">
<label>When</label>
<div class="segment">
<div class="description">
This workflow will run when: <strong>{{ store.selectedWorkflow.display_name }}</strong>
</div>
<div class="editor-content">
<div class="form" :class="{ 'readonly': !isInEditMode }">
<div class="field">
<label>When</label>
<div class="segment">
<div class="description">
This workflow will run when: <strong>{{ store.selectedWorkflow.display_name }}</strong>
</div>
</div>
</div>
<!-- Filters Section -->
<div class="field" v-if="hasAvailableFilters">
<label>Filters</label>
<div class="segment">
<div class="field" v-if="hasFilter('issue_type')">
<label>Apply to</label>
<select
v-if="isInEditMode"
class="form-select"
v-model="store.workflowFilters.issue_type"
>
<option value="">Issues And Pull Requests</option>
<option value="issue">Issues</option>
<option value="pull_request">Pull requests</option>
</select>
<div v-else class="readonly-value">
{{ store.workflowFilters.issue_type === 'issue' ? 'Issues' :
store.workflowFilters.issue_type === 'pull_request' ? 'Pull requests' :
'Issues And Pull Requests' }}
</div>
<!-- Filters Section -->
<div class="field" v-if="hasAvailableFilters">
<label>Filters</label>
<div class="segment">
<div class="field" v-if="hasFilter('issue_type')">
<label>Apply to</label>
<select
v-if="isInEditMode"
class="form-select"
v-model="store.workflowFilters.issue_type"
>
<option value="">Issues And Pull Requests</option>
<option value="issue">Issues</option>
<option value="pull_request">Pull requests</option>
</select>
<div v-else class="readonly-value">
{{ store.workflowFilters.issue_type === 'issue' ? 'Issues' :
store.workflowFilters.issue_type === 'pull_request' ? 'Pull requests' :
'Issues And Pull Requests' }}
</div>
</div>
</div>
</div>
<!-- Actions Section -->
<div class="field">
<label>Actions</label>
<div class="segment">
<div class="field" v-if="hasAction('column')">
<label>Move to column</label>
<select
v-if="isInEditMode"
class="form-select"
v-model="store.workflowActions.column"
>
<option value="">Select column...</option>
<option v-for="column in store.projectColumns" :key="column.id" :value="column.id">
{{ column.title }}
</option>
</select>
<div v-else class="readonly-value">
{{ store.projectColumns.find(c => c.id === store.workflowActions.column)?.title || 'None' }}
</div>
<!-- Actions Section -->
<div class="field">
<label>Actions</label>
<div class="segment">
<div class="field" v-if="hasAction('column')">
<label>Move to column</label>
<select
v-if="isInEditMode"
class="form-select"
v-model="store.workflowActions.column"
>
<option value="">Select column...</option>
<option v-for="column in store.projectColumns" :key="column.id" :value="String(column.id)">
{{ column.title }}
</option>
</select>
<div v-else class="readonly-value">
{{ store.projectColumns.find(c => String(c.id) === store.workflowActions.column)?.title || 'None' }}
</div>
</div>
<div class="field" v-if="hasAction('label')">
<label>Add labels</label>
<select
v-if="isInEditMode"
class="form-select"
v-model="store.workflowActions.add_labels"
multiple
>
<option value="">Select labels...</option>
<option v-for="label in store.projectLabels" :key="label.id" :value="label.id">
{{ label.name }}
</option>
</select>
<div v-else class="readonly-value">
{{ store.workflowActions.add_labels?.map(id =>
store.projectLabels.find(l => l.id === id)?.name).join(', ') || 'None' }}
</div>
<div class="field" v-if="hasAction('label')">
<label>Add labels</label>
<select
v-if="isInEditMode"
class="form-select"
v-model="store.workflowActions.add_labels"
multiple
>
<option value="">Select labels...</option>
<option v-for="label in store.projectLabels" :key="label.id" :value="String(label.id)">
{{ label.name }}
</option>
</select>
<div v-else class="readonly-value">
{{ store.workflowActions.add_labels?.map(id =>
store.projectLabels.find(l => String(l.id) === id)?.name).join(', ') || 'None' }}
</div>
</div>
<div class="field" v-if="hasAction('close')">
<div v-if="isInEditMode" class="form-check">
<input type="checkbox" v-model="store.workflowActions.closeIssue" id="close-issue">
<label for="close-issue">Close issue</label>
</div>
<div v-else class="readonly-value">
<label>Close issue</label>
<div>{{ store.workflowActions.closeIssue ? 'Yes' : 'No' }}</div>
</div>
<div class="field" v-if="hasAction('close')">
<div v-if="isInEditMode" class="form-check">
<input type="checkbox" v-model="store.workflowActions.closeIssue" id="close-issue">
<label for="close-issue">Close issue</label>
</div>
<div v-else class="readonly-value">
<label>Close issue</label>
<div>{{ store.workflowActions.closeIssue ? 'Yes' : 'No' }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Fixed bottom actions (only show in edit mode) -->
<div v-if="isInEditMode" class="editor-actions">
<button class="btn btn-primary" @click="saveWorkflow" :disabled="store.saving">
<i class="save icon"/>
Save Workflow
</button>
<button
v-if="store.selectedWorkflow && store.selectedWorkflow.id > 0"
class="btn btn-danger"
@click="deleteWorkflow"
>
<i class="trash icon"/>
Delete
</button>
</div>
<!--</form>-->
<!-- Fixed bottom actions (only show in edit mode) -->
<div v-if="isInEditMode" class="editor-actions">
<button class="btn btn-primary" @click="saveWorkflow" :disabled="store.saving">
<i class="save icon"/>
Save Workflow
</button>
<button
v-if="store.selectedWorkflow && store.selectedWorkflow.id > 0"
class="btn btn-danger"
@click="deleteWorkflow"
>
<i class="trash icon"/>
Delete
</button>
</div>
</div>
</div>
</div>

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