mirror of
https://github.com/go-gitea/gitea.git
synced 2026-05-18 22:57:44 +02:00
Support add labels and remove labels for issue's project column changed event
This commit is contained in:
parent
836bf98507
commit
74fc30ff71
@ -91,6 +91,7 @@ type WorkflowFilterType string
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
WorkflowFilterTypeIssueType WorkflowFilterType = "issue_type" // issue, pull_request, etc.
|
WorkflowFilterTypeIssueType WorkflowFilterType = "issue_type" // issue, pull_request, etc.
|
||||||
|
WorkflowFilterTypeColumn WorkflowFilterType = "column" // target column for item_column_changed event
|
||||||
)
|
)
|
||||||
|
|
||||||
type WorkflowFilter struct {
|
type WorkflowFilter struct {
|
||||||
@ -138,8 +139,8 @@ func GetWorkflowEventCapabilities() map[WorkflowEvent]WorkflowEventCapabilities
|
|||||||
AvailableActions: []WorkflowActionType{WorkflowActionTypeColumn, WorkflowActionTypeAddLabels, WorkflowActionTypeRemoveLabels},
|
AvailableActions: []WorkflowActionType{WorkflowActionTypeColumn, WorkflowActionTypeAddLabels, WorkflowActionTypeRemoveLabels},
|
||||||
},
|
},
|
||||||
WorkflowEventItemColumnChanged: {
|
WorkflowEventItemColumnChanged: {
|
||||||
AvailableFilters: []WorkflowFilterType{WorkflowFilterTypeIssueType},
|
AvailableFilters: []WorkflowFilterType{WorkflowFilterTypeIssueType, WorkflowFilterTypeColumn},
|
||||||
AvailableActions: []WorkflowActionType{WorkflowActionTypeColumn, WorkflowActionTypeAddLabels, WorkflowActionTypeRemoveLabels, WorkflowActionTypeClose},
|
AvailableActions: []WorkflowActionType{WorkflowActionTypeAddLabels, WorkflowActionTypeRemoveLabels, WorkflowActionTypeClose},
|
||||||
},
|
},
|
||||||
WorkflowEventCodeChangesRequested: {
|
WorkflowEventCodeChangesRequested: {
|
||||||
AvailableFilters: []WorkflowFilterType{}, // only applies to pull requests
|
AvailableFilters: []WorkflowFilterType{}, // only applies to pull requests
|
||||||
|
|||||||
@ -75,6 +75,7 @@ func convertFormToActions(formActions map[string]any) []project_model.WorkflowAc
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
case "add_labels":
|
case "add_labels":
|
||||||
|
// Handle both []string and []interface{} from JSON unmarshaling
|
||||||
if labels, ok := value.([]string); ok && len(labels) > 0 {
|
if labels, ok := value.([]string); ok && len(labels) > 0 {
|
||||||
for _, label := range labels {
|
for _, label := range labels {
|
||||||
if label != "" {
|
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":
|
case "remove_labels":
|
||||||
|
// Handle both []string and []interface{} from JSON unmarshaling
|
||||||
if labels, ok := value.([]string); ok && len(labels) > 0 {
|
if labels, ok := value.([]string); ok && len(labels) > 0 {
|
||||||
for _, label := range labels {
|
for _, label := range labels {
|
||||||
if label != "" {
|
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":
|
case "closeIssue":
|
||||||
if boolValue, ok := value.(bool); ok && boolValue {
|
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,
|
IssueChangeLabels(ctx context.Context, doer *user_model.User, issue *issues_model.Issue,
|
||||||
addedLabels, removedLabels []*issues_model.Label)
|
addedLabels, removedLabels []*issues_model.Label)
|
||||||
IssueChangeProjects(ctx context.Context, doer *user_model.User, issue *issues_model.Issue, newProject *project_model.Project)
|
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)
|
NewPullRequest(ctx context.Context, pr *issues_model.PullRequest, mentions []*user_model.User)
|
||||||
MergePullRequest(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest)
|
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
|
// CreateRepository notifies create repository to notifiers
|
||||||
func CreateRepository(ctx context.Context, doer, u *user_model.User, repo *repo_model.Repository) {
|
func CreateRepository(ctx context.Context, doer, u *user_model.User, repo *repo_model.Repository) {
|
||||||
for _, notifier := range notifiers {
|
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) 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
|
// CreateRepository places a place holder function
|
||||||
func (*NullNotifier) CreateRepository(ctx context.Context, doer, u *user_model.User, repo *repo_model.Repository) {
|
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"
|
project_model "code.gitea.io/gitea/models/project"
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/optional"
|
"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
|
// 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 {
|
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))
|
||||||
issueIDs := make([]int64, 0, len(sortedIssueIDs))
|
for _, issueID := range sortedIssueIDs {
|
||||||
for _, issueID := range sortedIssueIDs {
|
issueIDs = append(issueIDs, issueID)
|
||||||
issueIDs = append(issueIDs, issueID)
|
}
|
||||||
}
|
count, err := db.GetEngine(ctx).
|
||||||
count, err := db.GetEngine(ctx).
|
Where("project_id=?", column.ProjectID).
|
||||||
Where("project_id=?", column.ProjectID).
|
In("issue_id", issueIDs).
|
||||||
In("issue_id", issueIDs).
|
Count(new(project_model.ProjectIssue))
|
||||||
Count(new(project_model.ProjectIssue))
|
if err != nil {
|
||||||
if err != nil {
|
return err
|
||||||
return err
|
}
|
||||||
}
|
if int(count) != len(sortedIssueIDs) {
|
||||||
if int(count) != len(sortedIssueIDs) {
|
return errors.New("all issues have to be added to a project first")
|
||||||
return errors.New("all issues have to be added to a project first")
|
}
|
||||||
}
|
|
||||||
|
|
||||||
issues, err := issues_model.GetIssuesByIDs(ctx, issueIDs)
|
issues, err := issues_model.GetIssuesByIDs(ctx, issueIDs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := db.WithTx(ctx, func(ctx context.Context) error {
|
||||||
if _, err := issues.LoadRepositories(ctx); err != nil {
|
if _, err := issues.LoadRepositories(ctx); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -83,7 +85,15 @@ func MoveIssuesOnProjectColumn(ctx context.Context, doer *user_model.User, colum
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
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
|
// 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 {
|
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 {
|
if err := project_model.MoveIssueToAnotherColumn(ctx, issue.ID, column); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -230,5 +240,10 @@ func MoveIssueToAnotherColumn(ctx context.Context, doer *user_model.User, issue
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
notify.IssueChangeProjectColumn(ctx, doer, issue, column.ID)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@ package projects
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"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) {
|
func (*workflowNotifier) MergePullRequest(ctx context.Context, doer *user_model.User, pr *issues_model.PullRequest) {
|
||||||
if err := pr.LoadIssue(ctx); err != nil {
|
if err := pr.LoadIssue(ctx); err != nil {
|
||||||
log.Error("NewIssue: LoadIssue: %v", err)
|
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) {
|
if !(slices.Contains(values, "issue") && !issue.IsPull) || (slices.Contains(values, "pull") && issue.IsPull) {
|
||||||
return
|
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:
|
default:
|
||||||
log.Error("Unsupported filter type: %s", filter.Type)
|
log.Error("Unsupported filter type: %s", filter.Type)
|
||||||
return
|
return
|
||||||
@ -235,9 +284,37 @@ func fireIssueWorkflow(ctx context.Context, workflow *project_model.Workflow, is
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
case project_model.WorkflowActionTypeAddLabels:
|
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:
|
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:
|
case project_model.WorkflowActionTypeClose:
|
||||||
if err := issue_service.CloseIssue(ctx, issue, user_model.NewProjectWorkflowsUser(), ""); err != nil {
|
if err := issue_service.CloseIssue(ctx, issue, user_model.NewProjectWorkflowsUser(), ""); err != nil {
|
||||||
log.Error("CloseIssue: %v", err)
|
log.Error("CloseIssue: %v", err)
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
<script lang="ts" setup>
|
<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 {createWorkflowStore} from './WorkflowStore.ts';
|
||||||
import {svg} from '../../svg.ts';
|
import {svg} from '../../svg.ts';
|
||||||
import {confirmModal} from '../../features/comp/ConfirmModal.ts';
|
import {confirmModal} from '../../features/comp/ConfirmModal.ts';
|
||||||
|
import {fomanticQuery} from '../../modules/fomantic/base.ts';
|
||||||
|
|
||||||
const elRoot = useTemplateRef('elRoot');
|
const elRoot = useTemplateRef('elRoot');
|
||||||
|
|
||||||
@ -48,7 +49,7 @@ const toggleEditMode = () => {
|
|||||||
if (previousSelection.value) {
|
if (previousSelection.value) {
|
||||||
// If there was a previous selection, return to it
|
// If there was a previous selection, return to it
|
||||||
if (store.selectedWorkflow && store.selectedWorkflow.id === 0) {
|
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) =>
|
const tempIndex = store.workflowEvents.findIndex((w) =>
|
||||||
w.event_id === store.selectedWorkflow.event_id,
|
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)
|
const currentDisplayName = (store.selectedWorkflow.display_name || store.selectedWorkflow.workflow_event || store.selectedWorkflow.event_id)
|
||||||
.replace(/\s*\([^)]*\)\s*/g, '');
|
.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) {
|
if (store.selectedWorkflow.id === 0) {
|
||||||
const tempIndex = store.workflowEvents.findIndex((w) =>
|
const tempIndex = store.workflowEvents.findIndex((w) =>
|
||||||
w.event_id === store.selectedWorkflow.event_id,
|
w.event_id === store.selectedWorkflow.event_id,
|
||||||
@ -331,6 +332,53 @@ const isItemSelected = (item) => {
|
|||||||
return store.selectedItem === item.base_event_type;
|
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 () => {
|
onMounted(async () => {
|
||||||
// Load all necessary data
|
// Load all necessary data
|
||||||
store.workflowEvents = await store.loadEvents();
|
store.workflowEvents = await store.loadEvents();
|
||||||
@ -518,28 +566,60 @@ onUnmounted(() => {
|
|||||||
<p v-else>View workflow configuration</p>
|
<p v-else>View workflow configuration</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="editor-actions-header">
|
<div class="editor-actions-header">
|
||||||
<!-- Edit/Cancel Button (only for configured workflows) -->
|
<!-- Edit Mode Buttons -->
|
||||||
<button
|
<template v-if="isInEditMode">
|
||||||
v-if="store.selectedWorkflow && store.selectedWorkflow.id > 0"
|
<!-- Save Button -->
|
||||||
class="btn"
|
<button
|
||||||
:class="isInEditMode ? 'btn-outline-secondary' : 'btn-primary'"
|
class="btn btn-primary"
|
||||||
@click="toggleEditMode"
|
@click="saveWorkflow"
|
||||||
>
|
:disabled="store.saving"
|
||||||
<i :class="isInEditMode ? 'times icon' : 'edit icon'"/>
|
>
|
||||||
{{ isInEditMode ? 'Cancel' : 'Edit' }}
|
<i class="save icon"/>
|
||||||
</button>
|
Save
|
||||||
|
</button>
|
||||||
|
|
||||||
<!-- Enable/Disable Button (only for configured workflows) -->
|
<!-- Cancel Button -->
|
||||||
<button
|
<button
|
||||||
v-if="store.selectedWorkflow && store.selectedWorkflow.id > 0 && !isInEditMode"
|
class="btn btn-outline-secondary"
|
||||||
class="btn"
|
@click="toggleEditMode"
|
||||||
:class="store.selectedWorkflow.enabled ? 'btn-outline-danger' : 'btn-success'"
|
>
|
||||||
@click="toggleWorkflowStatus"
|
<i class="times icon"/>
|
||||||
:title="store.selectedWorkflow.enabled ? 'Disable workflow' : 'Enable workflow'"
|
Cancel
|
||||||
>
|
</button>
|
||||||
<i :class="store.selectedWorkflow.enabled ? 'pause icon' : 'play icon'"/>
|
|
||||||
{{ store.selectedWorkflow.enabled ? 'Disable' : 'Enable' }}
|
<!-- Delete Button (only for configured workflows) -->
|
||||||
</button>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -575,6 +655,23 @@ onUnmounted(() => {
|
|||||||
'Issues And Pull Requests' }}
|
'Issues And Pull Requests' }}
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -599,22 +696,75 @@ onUnmounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field" v-if="hasAction('label')">
|
<div class="field" v-if="hasAction('add_labels')">
|
||||||
<label>Add labels</label>
|
<label>Add labels</label>
|
||||||
<select
|
<div v-if="isInEditMode" class="ui fluid multiple search selection dropdown label-dropdown">
|
||||||
v-if="isInEditMode"
|
<input type="hidden" :value="store.workflowActions.add_labels.join(',')">
|
||||||
class="form-select"
|
<i class="dropdown icon"></i>
|
||||||
v-model="store.workflowActions.add_labels"
|
<div class="text" :class="{ default: !store.workflowActions.add_labels?.length }">
|
||||||
multiple
|
<span v-if="!store.workflowActions.add_labels?.length">Select labels...</span>
|
||||||
>
|
<template v-else>
|
||||||
<option value="">Select labels...</option>
|
<span v-for="labelId in store.workflowActions.add_labels" :key="labelId"
|
||||||
<option v-for="label in store.projectLabels" :key="label.id" :value="String(label.id)">
|
class="ui label"
|
||||||
{{ label.name }}
|
:style="`background-color: ${store.projectLabels.find(l => String(l.id) === labelId)?.color}; color: ${getLabelTextColor(store.projectLabels.find(l => String(l.id) === labelId)?.color)}`">
|
||||||
</option>
|
{{ store.projectLabels.find(l => String(l.id) === labelId)?.name }}
|
||||||
</select>
|
</span>
|
||||||
<div v-else class="readonly-value">
|
</template>
|
||||||
{{ store.workflowActions.add_labels?.map(id =>
|
</div>
|
||||||
store.projectLabels.find(l => String(l.id) === id)?.name).join(', ') || 'None' }}
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -633,21 +783,6 @@ onUnmounted(() => {
|
|||||||
</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>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -861,14 +996,6 @@ onUnmounted(() => {
|
|||||||
font-size: 0.9rem;
|
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 */
|
/* Responsive */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
@ -893,10 +1020,6 @@ onUnmounted(() => {
|
|||||||
.editor-content {
|
.editor-content {
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.editor-actions {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
@ -910,8 +1033,13 @@ onUnmounted(() => {
|
|||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.editor-actions button {
|
.editor-actions-header {
|
||||||
width: 100%;
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-actions-header button {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-width: 80px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1125,4 +1253,34 @@ onUnmounted(() => {
|
|||||||
background-color: #c82333;
|
background-color: #c82333;
|
||||||
border-color: #bd2130;
|
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>
|
</style>
|
||||||
|
|||||||
@ -16,11 +16,13 @@ export function createWorkflowStore(props: { projectLink: string, eventID: strin
|
|||||||
|
|
||||||
workflowFilters: {
|
workflowFilters: {
|
||||||
issue_type: '', // 'issue', 'pull_request', or ''
|
issue_type: '', // 'issue', 'pull_request', or ''
|
||||||
|
column: '', // target column ID for item_column_changed event
|
||||||
},
|
},
|
||||||
|
|
||||||
workflowActions: {
|
workflowActions: {
|
||||||
column: '', // column ID to move to
|
column: '', // column ID to move to
|
||||||
add_labels: [], // selected label IDs
|
add_labels: [], // selected label IDs
|
||||||
|
remove_labels: [], // selected label IDs to remove
|
||||||
closeIssue: false,
|
closeIssue: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -58,14 +60,16 @@ export function createWorkflowStore(props: { projectLink: string, eventID: strin
|
|||||||
|
|
||||||
// Load existing configuration from the workflow data
|
// Load existing configuration from the workflow data
|
||||||
// Convert backend filter format to frontend format
|
// Convert backend filter format to frontend format
|
||||||
const frontendFilters = {issue_type: ''};
|
const frontendFilters = {issue_type: '', column: ''};
|
||||||
// Convert backend action format to frontend format
|
// 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) {
|
if (workflow) {
|
||||||
for (const filter of workflow.filters) {
|
for (const filter of workflow.filters) {
|
||||||
if (filter.type === 'issue_type') {
|
if (filter.type === 'issue_type') {
|
||||||
frontendFilters.issue_type = filter.value;
|
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') {
|
} else if (action.type === 'add_labels') {
|
||||||
// Backend returns string, keep as string to match label.id type
|
// Backend returns string, keep as string to match label.id type
|
||||||
frontendActions.add_labels.push(action.value);
|
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') {
|
} else if (action.type === 'close') {
|
||||||
frontendActions.closeIssue = action.value === 'true';
|
frontendActions.closeIssue = action.value === 'true';
|
||||||
}
|
}
|
||||||
@ -100,8 +107,8 @@ export function createWorkflowStore(props: { projectLink: string, eventID: strin
|
|||||||
},
|
},
|
||||||
|
|
||||||
resetWorkflowData() {
|
resetWorkflowData() {
|
||||||
store.workflowFilters = {issue_type: ''};
|
store.workflowFilters = {issue_type: '', column: ''};
|
||||||
store.workflowActions = {column: '', add_labels: [], closeIssue: false};
|
store.workflowActions = {column: '', add_labels: [], remove_labels: [], closeIssue: false};
|
||||||
},
|
},
|
||||||
|
|
||||||
async saveWorkflow() {
|
async saveWorkflow() {
|
||||||
@ -163,13 +170,15 @@ export function createWorkflowStore(props: { projectLink: string, eventID: strin
|
|||||||
|
|
||||||
// Convert backend data to frontend format and update form
|
// Convert backend data to frontend format and update form
|
||||||
// Use the selectedWorkflow which now points to the reloaded workflow with complete data
|
// Use the selectedWorkflow which now points to the reloaded workflow with complete data
|
||||||
const frontendFilters = {issue_type: ''};
|
const frontendFilters = {issue_type: '', column: ''};
|
||||||
const frontendActions = {column: '', add_labels: [], closeIssue: false};
|
const frontendActions = {column: '', add_labels: [], remove_labels: [], closeIssue: false};
|
||||||
|
|
||||||
if (store.selectedWorkflow.filters && Array.isArray(store.selectedWorkflow.filters)) {
|
if (store.selectedWorkflow.filters && Array.isArray(store.selectedWorkflow.filters)) {
|
||||||
for (const filter of store.selectedWorkflow.filters) {
|
for (const filter of store.selectedWorkflow.filters) {
|
||||||
if (filter.type === 'issue_type') {
|
if (filter.type === 'issue_type') {
|
||||||
frontendFilters.issue_type = filter.value;
|
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;
|
frontendActions.column = action.value;
|
||||||
} else if (action.type === 'add_labels') {
|
} else if (action.type === 'add_labels') {
|
||||||
frontendActions.add_labels.push(action.value);
|
frontendActions.add_labels.push(action.value);
|
||||||
|
} else if (action.type === 'remove_labels') {
|
||||||
|
frontendActions.remove_labels.push(action.value);
|
||||||
} else if (action.type === 'close') {
|
} else if (action.type === 'close') {
|
||||||
frontendActions.closeIssue = action.value === 'true';
|
frontendActions.closeIssue = action.value === 'true';
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user