Support add labels and remove labels for issue's project column changed event

pull/30205/head
Lunny Xiao 2025-10-23 00:47:34 -07:00
parent 836bf98507
commit 74fc30ff71
No known key found for this signature in database
GPG Key ID: C3B7C91B632F738A
9 changed files with 392 additions and 100 deletions

View File

@ -91,6 +91,7 @@ type WorkflowFilterType string
const (
WorkflowFilterTypeIssueType WorkflowFilterType = "issue_type" // issue, pull_request, etc.
WorkflowFilterTypeColumn WorkflowFilterType = "column" // target column for item_column_changed event
)
type WorkflowFilter struct {
@ -138,8 +139,8 @@ func GetWorkflowEventCapabilities() map[WorkflowEvent]WorkflowEventCapabilities
AvailableActions: []WorkflowActionType{WorkflowActionTypeColumn, WorkflowActionTypeAddLabels, WorkflowActionTypeRemoveLabels},
},
WorkflowEventItemColumnChanged: {
AvailableFilters: []WorkflowFilterType{WorkflowFilterTypeIssueType},
AvailableActions: []WorkflowActionType{WorkflowActionTypeColumn, WorkflowActionTypeAddLabels, WorkflowActionTypeRemoveLabels, WorkflowActionTypeClose},
AvailableFilters: []WorkflowFilterType{WorkflowFilterTypeIssueType, WorkflowFilterTypeColumn},
AvailableActions: []WorkflowActionType{WorkflowActionTypeAddLabels, WorkflowActionTypeRemoveLabels, WorkflowActionTypeClose},
},
WorkflowEventCodeChangesRequested: {
AvailableFilters: []WorkflowFilterType{}, // only applies to pull requests

View File

@ -75,6 +75,7 @@ func convertFormToActions(formActions map[string]any) []project_model.WorkflowAc
}
}
case "add_labels":
// Handle both []string and []interface{} from JSON unmarshaling
if labels, ok := value.([]string); ok && len(labels) > 0 {
for _, label := range labels {
if label != "" {
@ -84,8 +85,18 @@ func convertFormToActions(formActions map[string]any) []project_model.WorkflowAc
})
}
}
} else if labelInterfaces, ok := value.([]interface{}); ok && len(labelInterfaces) > 0 {
for _, labelInterface := range labelInterfaces {
if label, ok := labelInterface.(string); ok && label != "" {
actions = append(actions, project_model.WorkflowAction{
Type: project_model.WorkflowActionTypeAddLabels,
Value: label,
})
}
}
}
case "remove_labels":
// Handle both []string and []interface{} from JSON unmarshaling
if labels, ok := value.([]string); ok && len(labels) > 0 {
for _, label := range labels {
if label != "" {
@ -95,6 +106,15 @@ func convertFormToActions(formActions map[string]any) []project_model.WorkflowAc
})
}
}
} else if labelInterfaces, ok := value.([]interface{}); ok && len(labelInterfaces) > 0 {
for _, labelInterface := range labelInterfaces {
if label, ok := labelInterface.(string); ok && label != "" {
actions = append(actions, project_model.WorkflowAction{
Type: project_model.WorkflowActionTypeRemoveLabels,
Value: label,
})
}
}
}
case "closeIssue":
if boolValue, ok := value.(bool); ok && boolValue {

View File

@ -43,6 +43,7 @@ type Notifier interface {
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)
IssueChangeProjectColumn(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, newColumnID int64)
NewPullRequest(ctx context.Context, pr *issues_model.PullRequest, mentions []*user_model.User)
MergePullRequest(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest)

View File

@ -282,6 +282,12 @@ func IssueChangeProjects(ctx context.Context, doer *user_model.User, issue *issu
}
}
func IssueChangeProjectColumn(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, newColumnID int64) {
for _, notifier := range notifiers {
notifier.IssueChangeProjectColumn(ctx, doer, issue, newColumnID)
}
}
// CreateRepository notifies create repository to notifiers
func CreateRepository(ctx context.Context, doer, u *user_model.User, repo *repo_model.Repository) {
for _, notifier := range notifiers {

View File

@ -147,6 +147,9 @@ func (*NullNotifier) IssueChangeLabels(ctx context.Context, doer *user_model.Use
func (*NullNotifier) IssueChangeProjects(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, newProject *project_model.Project) {
}
func (*NullNotifier) IssueChangeProjectColumn(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, newColumnID int64) {
}
// CreateRepository places a place holder function
func (*NullNotifier) CreateRepository(ctx context.Context, doer, u *user_model.User, repo *repo_model.Repository) {
}

View File

@ -12,30 +12,32 @@ import (
project_model "code.gitea.io/gitea/models/project"
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/optional"
"code.gitea.io/gitea/services/notify"
)
// MoveIssuesOnProjectColumn moves or keeps issues in a column and sorts them inside that column
func MoveIssuesOnProjectColumn(ctx context.Context, doer *user_model.User, column *project_model.Column, sortedIssueIDs map[int64]int64) error {
return db.WithTx(ctx, func(ctx context.Context) error {
issueIDs := make([]int64, 0, len(sortedIssueIDs))
for _, issueID := range sortedIssueIDs {
issueIDs = append(issueIDs, issueID)
}
count, err := db.GetEngine(ctx).
Where("project_id=?", column.ProjectID).
In("issue_id", issueIDs).
Count(new(project_model.ProjectIssue))
if err != nil {
return err
}
if int(count) != len(sortedIssueIDs) {
return errors.New("all issues have to be added to a project first")
}
issueIDs := make([]int64, 0, len(sortedIssueIDs))
for _, issueID := range sortedIssueIDs {
issueIDs = append(issueIDs, issueID)
}
count, err := db.GetEngine(ctx).
Where("project_id=?", column.ProjectID).
In("issue_id", issueIDs).
Count(new(project_model.ProjectIssue))
if err != nil {
return err
}
if int(count) != len(sortedIssueIDs) {
return errors.New("all issues have to be added to a project first")
}
issues, err := issues_model.GetIssuesByIDs(ctx, issueIDs)
if err != nil {
return err
}
issues, err := issues_model.GetIssuesByIDs(ctx, issueIDs)
if err != nil {
return err
}
if err := db.WithTx(ctx, func(ctx context.Context) error {
if _, err := issues.LoadRepositories(ctx); err != nil {
return err
}
@ -83,7 +85,15 @@ func MoveIssuesOnProjectColumn(ctx context.Context, doer *user_model.User, colum
}
}
return nil
})
}); err != nil {
return err
}
for _, issue := range issues {
notify.IssueChangeProjectColumn(ctx, doer, issue, column.ID)
}
return nil
}
// LoadIssuesFromProject load issues assigned to each project column inside the given project
@ -207,7 +217,7 @@ func LoadIssueNumbersForProject(ctx context.Context, project *project_model.Proj
}
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 := db.WithTx(ctx, func(ctx context.Context) error {
if err := project_model.MoveIssueToAnotherColumn(ctx, issue.ID, column); err != nil {
return err
}
@ -230,5 +240,10 @@ func MoveIssueToAnotherColumn(ctx context.Context, doer *user_model.User, issue
return err
}
return nil
})
}); err != nil {
return err
}
notify.IssueChangeProjectColumn(ctx, doer, issue, column.ID)
return nil
}

View File

@ -5,6 +5,7 @@ package projects
import (
"context"
"errors"
"slices"
"strconv"
"strings"
@ -130,6 +131,40 @@ func (*workflowNotifier) IssueChangeProjects(ctx context.Context, doer *user_mod
}
}
func (*workflowNotifier) IssueChangeProjectColumn(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, newColumnID int64) {
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
}
newColumn, err := project_model.GetColumn(ctx, newColumnID)
if err != nil {
log.Error("IssueChangeProjectColumn: GetColumn: %v", err)
return
}
if issue.Project == nil || issue.Project.ID != newColumn.ProjectID {
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.WorkflowEventItemColumnChanged {
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)
@ -211,6 +246,20 @@ func fireIssueWorkflow(ctx context.Context, workflow *project_model.Workflow, is
if !(slices.Contains(values, "issue") && !issue.IsPull) || (slices.Contains(values, "pull") && issue.IsPull) {
return
}
case project_model.WorkflowFilterTypeColumn:
columnID, _ := strconv.ParseInt(filter.Value, 10, 64)
if columnID == 0 {
log.Error("Invalid column ID: %s", filter.Value)
return
}
issueProjectColumnID, err := issue.ProjectColumnID(ctx)
if err != nil {
log.Error("Issue.ProjectColumnID: %v", err)
return
}
if issueProjectColumnID != columnID {
return
}
default:
log.Error("Unsupported filter type: %s", filter.Type)
return
@ -235,9 +284,37 @@ func fireIssueWorkflow(ctx context.Context, workflow *project_model.Workflow, is
continue
}
case project_model.WorkflowActionTypeAddLabels:
// TODO: implement adding labels
labelID, _ := strconv.ParseInt(action.Value, 10, 64)
if labelID == 0 {
log.Error("Invalid label ID: %s", action.Value)
continue
}
label, err := issues_model.GetLabelByID(ctx, labelID)
if err != nil {
log.Error("GetLabelByID: %v", err)
continue
}
if err := issue_service.AddLabel(ctx, issue, user_model.NewProjectWorkflowsUser(), label); err != nil {
log.Error("AddLabels: %v", err)
continue
}
case project_model.WorkflowActionTypeRemoveLabels:
// TODO: implement removing labels
labelID, _ := strconv.ParseInt(action.Value, 10, 64)
if labelID == 0 {
log.Error("Invalid label ID: %s", action.Value)
continue
}
label, err := issues_model.GetLabelByID(ctx, labelID)
if err != nil {
log.Error("GetLabelByID: %v", err)
continue
}
if err := issue_service.RemoveLabel(ctx, issue, user_model.NewProjectWorkflowsUser(), label); err != nil {
if !issues_model.IsErrRepoLabelNotExist(err) {
log.Error("RemoveLabels: %v", err)
}
continue
}
case project_model.WorkflowActionTypeClose:
if err := issue_service.CloseIssue(ctx, issue, user_model.NewProjectWorkflowsUser(), ""); err != nil {
log.Error("CloseIssue: %v", err)

View File

@ -1,8 +1,9 @@
<script lang="ts" setup>
import {onMounted, onUnmounted, useTemplateRef, computed, ref, nextTick} from 'vue';
import {onMounted, onUnmounted, useTemplateRef, computed, ref, nextTick, watch} from 'vue';
import {createWorkflowStore} from './WorkflowStore.ts';
import {svg} from '../../svg.ts';
import {confirmModal} from '../../features/comp/ConfirmModal.ts';
import {fomanticQuery} from '../../modules/fomantic/base.ts';
const elRoot = useTemplateRef('elRoot');
@ -48,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 cloned workflow from list
// Remove temporary unsaved workflow from list
const tempIndex = store.workflowEvents.findIndex((w) =>
w.event_id === store.selectedWorkflow.event_id,
);
@ -97,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 (new), just remove from list
// If deleting a temporary workflow (unsaved), just remove from list
if (store.selectedWorkflow.id === 0) {
const tempIndex = store.workflowEvents.findIndex((w) =>
w.event_id === store.selectedWorkflow.event_id,
@ -331,6 +332,53 @@ const isItemSelected = (item) => {
return store.selectedItem === item.base_event_type;
};
// Toggle label selection for add_labels or remove_labels
const toggleLabel = (actionType, labelId) => {
const labels = store.workflowActions[actionType];
const index = labels.indexOf(labelId);
if (index > -1) {
labels.splice(index, 1);
} else {
labels.push(labelId);
}
};
// Calculate text color based on background color for better contrast
const getLabelTextColor = (hexColor) => {
if (!hexColor) return '#000';
// Remove # if present
const color = hexColor.replace('#', '');
// Convert to RGB
const r = parseInt(color.substring(0, 2), 16);
const g = parseInt(color.substring(2, 4), 16);
const b = parseInt(color.substring(4, 6), 16);
// Calculate relative luminance
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
// Return black for light backgrounds, white for dark backgrounds
return luminance > 0.5 ? '#000' : '#fff';
};
// Initialize Fomantic UI dropdowns for label selection
const initLabelDropdowns = async () => {
await nextTick();
const dropdowns = elRoot.value?.querySelectorAll('.ui.dropdown');
if (dropdowns) {
dropdowns.forEach((dropdown) => {
fomanticQuery(dropdown).dropdown({
action: 'nothing', // Don't hide on selection for multiple selection
fullTextSearch: true,
});
});
}
};
// Watch for edit mode changes to initialize dropdowns
watch(isInEditMode, async (newVal) => {
if (newVal) {
await initLabelDropdowns();
}
});
onMounted(async () => {
// Load all necessary data
store.workflowEvents = await store.loadEvents();
@ -518,28 +566,60 @@ onUnmounted(() => {
<p v-else>View workflow configuration</p>
</div>
<div class="editor-actions-header">
<!-- Edit/Cancel Button (only for configured workflows) -->
<button
v-if="store.selectedWorkflow && store.selectedWorkflow.id > 0"
class="btn"
:class="isInEditMode ? 'btn-outline-secondary' : 'btn-primary'"
@click="toggleEditMode"
>
<i :class="isInEditMode ? 'times icon' : 'edit icon'"/>
{{ isInEditMode ? 'Cancel' : 'Edit' }}
</button>
<!-- Edit Mode Buttons -->
<template v-if="isInEditMode">
<!-- Save Button -->
<button
class="btn btn-primary"
@click="saveWorkflow"
:disabled="store.saving"
>
<i class="save icon"/>
Save
</button>
<!-- Enable/Disable Button (only for configured workflows) -->
<button
v-if="store.selectedWorkflow && store.selectedWorkflow.id > 0 && !isInEditMode"
class="btn"
:class="store.selectedWorkflow.enabled ? 'btn-outline-danger' : 'btn-success'"
@click="toggleWorkflowStatus"
:title="store.selectedWorkflow.enabled ? 'Disable workflow' : 'Enable workflow'"
>
<i :class="store.selectedWorkflow.enabled ? 'pause icon' : 'play icon'"/>
{{ store.selectedWorkflow.enabled ? 'Disable' : 'Enable' }}
</button>
<!-- Cancel Button -->
<button
class="btn btn-outline-secondary"
@click="toggleEditMode"
>
<i class="times icon"/>
Cancel
</button>
<!-- Delete Button (only for configured workflows) -->
<button
v-if="store.selectedWorkflow && store.selectedWorkflow.id > 0"
class="btn btn-danger"
@click="deleteWorkflow"
>
<i class="trash icon"/>
Delete
</button>
</template>
<!-- View Mode Buttons (only for configured workflows) -->
<template v-else-if="store.selectedWorkflow && store.selectedWorkflow.id > 0">
<!-- Edit Button -->
<button
class="btn btn-primary"
@click="toggleEditMode"
>
<i class="edit icon"/>
Edit
</button>
<!-- Enable/Disable Button -->
<button
class="btn"
:class="store.selectedWorkflow.enabled ? 'btn-outline-danger' : 'btn-success'"
@click="toggleWorkflowStatus"
:title="store.selectedWorkflow.enabled ? 'Disable workflow' : 'Enable workflow'"
>
<i :class="store.selectedWorkflow.enabled ? 'pause icon' : 'play icon'"/>
{{ store.selectedWorkflow.enabled ? 'Disable' : 'Enable' }}
</button>
</template>
</div>
</div>
@ -575,6 +655,23 @@ onUnmounted(() => {
'Issues And Pull Requests' }}
</div>
</div>
<div class="field" v-if="hasFilter('column')">
<label>When moved to column</label>
<select
v-if="isInEditMode"
class="form-select"
v-model="store.workflowFilters.column"
>
<option value="">Any 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.workflowFilters.column)?.title || 'Any column' }}
</div>
</div>
</div>
</div>
@ -599,22 +696,75 @@ onUnmounted(() => {
</div>
</div>
<div class="field" v-if="hasAction('label')">
<div class="field" v-if="hasAction('add_labels')">
<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 v-if="isInEditMode" class="ui fluid multiple search selection dropdown label-dropdown">
<input type="hidden" :value="store.workflowActions.add_labels.join(',')">
<i class="dropdown icon"></i>
<div class="text" :class="{ default: !store.workflowActions.add_labels?.length }">
<span v-if="!store.workflowActions.add_labels?.length">Select labels...</span>
<template v-else>
<span v-for="labelId in store.workflowActions.add_labels" :key="labelId"
class="ui label"
:style="`background-color: ${store.projectLabels.find(l => String(l.id) === labelId)?.color}; color: ${getLabelTextColor(store.projectLabels.find(l => String(l.id) === labelId)?.color)}`">
{{ store.projectLabels.find(l => String(l.id) === labelId)?.name }}
</span>
</template>
</div>
<div class="menu">
<div class="item" v-for="label in store.projectLabels" :key="label.id"
:data-value="String(label.id)"
@click.prevent="toggleLabel('add_labels', String(label.id))"
:class="{ active: store.workflowActions.add_labels.includes(String(label.id)), selected: store.workflowActions.add_labels.includes(String(label.id)) }">
<span class="ui label" :style="`background-color: ${label.color}; color: ${getLabelTextColor(label.color)}`">
{{ label.name }}
</span>
</div>
</div>
</div>
<div v-else class="ui labels">
<span v-if="!store.workflowActions.add_labels?.length" class="text-muted">None</span>
<span v-for="labelId in store.workflowActions.add_labels" :key="labelId"
class="ui label"
:style="`background-color: ${store.projectLabels.find(l => String(l.id) === labelId)?.color}; color: ${getLabelTextColor(store.projectLabels.find(l => String(l.id) === labelId)?.color)}`">
{{ store.projectLabels.find(l => String(l.id) === labelId)?.name }}
</span>
</div>
</div>
<div class="field" v-if="hasAction('remove_labels')">
<label>Remove labels</label>
<div v-if="isInEditMode" class="ui fluid multiple search selection dropdown label-dropdown">
<input type="hidden" :value="store.workflowActions.remove_labels.join(',')">
<i class="dropdown icon"></i>
<div class="text" :class="{ default: !store.workflowActions.remove_labels?.length }">
<span v-if="!store.workflowActions.remove_labels?.length">Select labels...</span>
<template v-else>
<span v-for="labelId in store.workflowActions.remove_labels" :key="labelId"
class="ui label"
:style="`background-color: ${store.projectLabels.find(l => String(l.id) === labelId)?.color}; color: ${getLabelTextColor(store.projectLabels.find(l => String(l.id) === labelId)?.color)}`">
{{ store.projectLabels.find(l => String(l.id) === labelId)?.name }}
</span>
</template>
</div>
<div class="menu">
<div class="item" v-for="label in store.projectLabels" :key="label.id"
:data-value="String(label.id)"
@click.prevent="toggleLabel('remove_labels', String(label.id))"
:class="{ active: store.workflowActions.remove_labels.includes(String(label.id)), selected: store.workflowActions.remove_labels.includes(String(label.id)) }">
<span class="ui label" :style="`background-color: ${label.color}; color: ${getLabelTextColor(label.color)}`">
{{ label.name }}
</span>
</div>
</div>
</div>
<div v-else class="ui labels">
<span v-if="!store.workflowActions.remove_labels?.length" class="text-muted">None</span>
<span v-for="labelId in store.workflowActions.remove_labels" :key="labelId"
class="ui label"
:style="`background-color: ${store.projectLabels.find(l => String(l.id) === labelId)?.color}; color: ${getLabelTextColor(store.projectLabels.find(l => String(l.id) === labelId)?.color)}`">
{{ store.projectLabels.find(l => String(l.id) === labelId)?.name }}
</span>
</div>
</div>
@ -633,21 +783,6 @@ onUnmounted(() => {
</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>
</div>
</div>
</div>
@ -861,14 +996,6 @@ onUnmounted(() => {
font-size: 0.9rem;
}
.editor-actions {
display: flex;
gap: 0.5rem;
padding: 1.5rem;
border-top: 1px solid #e1e4e8;
background: white;
flex-shrink: 0;
}
/* Responsive */
@media (max-width: 768px) {
@ -893,10 +1020,6 @@ onUnmounted(() => {
.editor-content {
padding: 1rem;
}
.editor-actions {
flex-direction: column;
}
}
@media (max-width: 480px) {
@ -910,8 +1033,13 @@ onUnmounted(() => {
padding: 0.75rem;
}
.editor-actions button {
width: 100%;
.editor-actions-header {
flex-wrap: wrap;
}
.editor-actions-header button {
flex: 1 1 auto;
min-width: 80px;
}
}
@ -1125,4 +1253,34 @@ onUnmounted(() => {
background-color: #c82333;
border-color: #bd2130;
}
/* Label selector styles */
.label-dropdown.ui.dropdown .menu > .item.active,
.label-dropdown.ui.dropdown .menu > .item.selected {
background: rgba(0, 0, 0, 0.05);
font-weight: normal;
}
.label-dropdown.ui.dropdown .menu > .item .ui.label {
margin: 0;
}
.label-dropdown.ui.dropdown > .text > .ui.label {
margin: 0.125rem;
}
.ui.labels {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
align-items: center;
}
.ui.labels .ui.label {
margin: 0;
}
.text-muted {
color: #6c757d;
}
</style>

View File

@ -16,11 +16,13 @@ export function createWorkflowStore(props: { projectLink: string, eventID: strin
workflowFilters: {
issue_type: '', // 'issue', 'pull_request', or ''
column: '', // target column ID for item_column_changed event
},
workflowActions: {
column: '', // column ID to move to
add_labels: [], // selected label IDs
remove_labels: [], // selected label IDs to remove
closeIssue: false,
},
@ -58,14 +60,16 @@ export function createWorkflowStore(props: { projectLink: string, eventID: strin
// Load existing configuration from the workflow data
// Convert backend filter format to frontend format
const frontendFilters = {issue_type: ''};
const frontendFilters = {issue_type: '', column: ''};
// Convert backend action format to frontend format
const frontendActions = {column: '', add_labels: [], closeIssue: false};
const frontendActions = {column: '', add_labels: [], remove_labels: [], closeIssue: false};
if (workflow) {
for (const filter of workflow.filters) {
if (filter.type === 'issue_type') {
frontendFilters.issue_type = filter.value;
} else if (filter.type === 'column') {
frontendFilters.column = filter.value;
}
}
@ -76,6 +80,9 @@ export function createWorkflowStore(props: { projectLink: string, eventID: strin
} 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 === 'remove_labels') {
// Backend returns string, keep as string to match label.id type
frontendActions.remove_labels.push(action.value);
} else if (action.type === 'close') {
frontendActions.closeIssue = action.value === 'true';
}
@ -100,8 +107,8 @@ export function createWorkflowStore(props: { projectLink: string, eventID: strin
},
resetWorkflowData() {
store.workflowFilters = {issue_type: ''};
store.workflowActions = {column: '', add_labels: [], closeIssue: false};
store.workflowFilters = {issue_type: '', column: ''};
store.workflowActions = {column: '', add_labels: [], remove_labels: [], closeIssue: false};
},
async saveWorkflow() {
@ -163,13 +170,15 @@ export function createWorkflowStore(props: { projectLink: string, eventID: strin
// Convert backend data to frontend format and update form
// Use the selectedWorkflow which now points to the reloaded workflow with complete data
const frontendFilters = {issue_type: ''};
const frontendActions = {column: '', add_labels: [], closeIssue: false};
const frontendFilters = {issue_type: '', column: ''};
const frontendActions = {column: '', add_labels: [], remove_labels: [], closeIssue: false};
if (store.selectedWorkflow.filters && Array.isArray(store.selectedWorkflow.filters)) {
for (const filter of store.selectedWorkflow.filters) {
if (filter.type === 'issue_type') {
frontendFilters.issue_type = filter.value;
} else if (filter.type === 'column') {
frontendFilters.column = filter.value;
}
}
}
@ -180,6 +189,8 @@ export function createWorkflowStore(props: { projectLink: string, eventID: strin
frontendActions.column = action.value;
} else if (action.type === 'add_labels') {
frontendActions.add_labels.push(action.value);
} else if (action.type === 'remove_labels') {
frontendActions.remove_labels.push(action.value);
} else if (action.type === 'close') {
frontendActions.closeIssue = action.value === 'true';
}