mirror of https://github.com/go-gitea/gitea.git
Support add labels and remove labels for issue's project column changed event
parent
836bf98507
commit
74fc30ff71
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue