mirror of
https://github.com/go-gitea/gitea.git
synced 2026-02-26 18:43:10 +01:00
support labels filter
This commit is contained in:
parent
74fc30ff71
commit
822387aa38
@ -92,6 +92,7 @@ type WorkflowFilterType string
|
||||
const (
|
||||
WorkflowFilterTypeIssueType WorkflowFilterType = "issue_type" // issue, pull_request, etc.
|
||||
WorkflowFilterTypeColumn WorkflowFilterType = "column" // target column for item_column_changed event
|
||||
WorkflowFilterTypeLabels WorkflowFilterType = "labels" // filter by issue/PR labels
|
||||
)
|
||||
|
||||
type WorkflowFilter struct {
|
||||
@ -123,35 +124,35 @@ type WorkflowEventCapabilities struct {
|
||||
func GetWorkflowEventCapabilities() map[WorkflowEvent]WorkflowEventCapabilities {
|
||||
return map[WorkflowEvent]WorkflowEventCapabilities{
|
||||
WorkflowEventItemOpened: {
|
||||
AvailableFilters: []WorkflowFilterType{WorkflowFilterTypeIssueType}, // issue, pull_request
|
||||
AvailableFilters: []WorkflowFilterType{WorkflowFilterTypeIssueType, WorkflowFilterTypeLabels},
|
||||
AvailableActions: []WorkflowActionType{WorkflowActionTypeColumn, WorkflowActionTypeAddLabels},
|
||||
},
|
||||
WorkflowEventItemAddedToProject: {
|
||||
AvailableFilters: []WorkflowFilterType{WorkflowFilterTypeIssueType}, // issue, pull_request
|
||||
AvailableFilters: []WorkflowFilterType{WorkflowFilterTypeIssueType, WorkflowFilterTypeLabels},
|
||||
AvailableActions: []WorkflowActionType{WorkflowActionTypeColumn, WorkflowActionTypeAddLabels, WorkflowActionTypeRemoveLabels},
|
||||
},
|
||||
WorkflowEventItemReopened: {
|
||||
AvailableFilters: []WorkflowFilterType{WorkflowFilterTypeIssueType},
|
||||
AvailableFilters: []WorkflowFilterType{WorkflowFilterTypeIssueType, WorkflowFilterTypeLabels},
|
||||
AvailableActions: []WorkflowActionType{WorkflowActionTypeColumn, WorkflowActionTypeAddLabels, WorkflowActionTypeRemoveLabels},
|
||||
},
|
||||
WorkflowEventItemClosed: {
|
||||
AvailableFilters: []WorkflowFilterType{WorkflowFilterTypeIssueType},
|
||||
AvailableFilters: []WorkflowFilterType{WorkflowFilterTypeIssueType, WorkflowFilterTypeLabels},
|
||||
AvailableActions: []WorkflowActionType{WorkflowActionTypeColumn, WorkflowActionTypeAddLabels, WorkflowActionTypeRemoveLabels},
|
||||
},
|
||||
WorkflowEventItemColumnChanged: {
|
||||
AvailableFilters: []WorkflowFilterType{WorkflowFilterTypeIssueType, WorkflowFilterTypeColumn},
|
||||
AvailableFilters: []WorkflowFilterType{WorkflowFilterTypeIssueType, WorkflowFilterTypeColumn, WorkflowFilterTypeLabels},
|
||||
AvailableActions: []WorkflowActionType{WorkflowActionTypeAddLabels, WorkflowActionTypeRemoveLabels, WorkflowActionTypeClose},
|
||||
},
|
||||
WorkflowEventCodeChangesRequested: {
|
||||
AvailableFilters: []WorkflowFilterType{}, // only applies to pull requests
|
||||
AvailableFilters: []WorkflowFilterType{WorkflowFilterTypeLabels}, // only applies to pull requests
|
||||
AvailableActions: []WorkflowActionType{WorkflowActionTypeColumn, WorkflowActionTypeAddLabels, WorkflowActionTypeRemoveLabels},
|
||||
},
|
||||
WorkflowEventCodeReviewApproved: {
|
||||
AvailableFilters: []WorkflowFilterType{}, // only applies to pull requests
|
||||
AvailableFilters: []WorkflowFilterType{WorkflowFilterTypeLabels}, // only applies to pull requests
|
||||
AvailableActions: []WorkflowActionType{WorkflowActionTypeColumn, WorkflowActionTypeAddLabels, WorkflowActionTypeRemoveLabels},
|
||||
},
|
||||
WorkflowEventPullRequestMerged: {
|
||||
AvailableFilters: []WorkflowFilterType{}, // only applies to pull requests
|
||||
AvailableFilters: []WorkflowFilterType{WorkflowFilterTypeLabels}, // only applies to pull requests
|
||||
AvailableActions: []WorkflowActionType{WorkflowActionTypeColumn, WorkflowActionTypeAddLabels, WorkflowActionTypeRemoveLabels},
|
||||
},
|
||||
}
|
||||
|
||||
@ -43,15 +43,31 @@ func getFilterSummary(filters []project_model.WorkflowFilter) string {
|
||||
}
|
||||
|
||||
// convertFormToFilters converts form filters to WorkflowFilter objects
|
||||
func convertFormToFilters(formFilters map[string]string) []project_model.WorkflowFilter {
|
||||
func convertFormToFilters(formFilters map[string]any) []project_model.WorkflowFilter {
|
||||
filters := make([]project_model.WorkflowFilter, 0)
|
||||
|
||||
for key, value := range formFilters {
|
||||
if value != "" {
|
||||
filters = append(filters, project_model.WorkflowFilter{
|
||||
Type: project_model.WorkflowFilterType(key),
|
||||
Value: value,
|
||||
})
|
||||
switch key {
|
||||
case "labels":
|
||||
// Handle labels array
|
||||
if labelInterfaces, ok := value.([]interface{}); ok && len(labelInterfaces) > 0 {
|
||||
for _, labelInterface := range labelInterfaces {
|
||||
if label, ok := labelInterface.(string); ok && label != "" {
|
||||
filters = append(filters, project_model.WorkflowFilter{
|
||||
Type: project_model.WorkflowFilterTypeLabels,
|
||||
Value: label,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
default:
|
||||
// Handle string values (issue_type, column)
|
||||
if strValue, ok := value.(string); ok && strValue != "" {
|
||||
filters = append(filters, project_model.WorkflowFilter{
|
||||
Type: project_model.WorkflowFilterType(key),
|
||||
Value: strValue,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -378,9 +394,9 @@ func Workflows(ctx *context.Context) {
|
||||
}
|
||||
|
||||
type WorkflowsPostForm struct {
|
||||
EventID string `json:"event_id"`
|
||||
Filters map[string]string `json:"filters"`
|
||||
Actions map[string]any `json:"actions"`
|
||||
EventID string `json:"event_id"`
|
||||
Filters map[string]any `json:"filters"`
|
||||
Actions map[string]any `json:"actions"`
|
||||
}
|
||||
|
||||
func WorkflowsPost(ctx *context.Context) {
|
||||
|
||||
@ -5,10 +5,7 @@ package projects
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
issues_model "code.gitea.io/gitea/models/issues"
|
||||
project_model "code.gitea.io/gitea/models/project"
|
||||
@ -157,10 +154,10 @@ func (*workflowNotifier) IssueChangeProjectColumn(ctx context.Context, doer *use
|
||||
return
|
||||
}
|
||||
|
||||
// Find workflows for the ItemOpened event
|
||||
// Find workflows for the ItemColumnChanged event
|
||||
for _, workflow := range workflows {
|
||||
if workflow.WorkflowEvent == project_model.WorkflowEventItemColumnChanged {
|
||||
fireIssueWorkflow(ctx, workflow, issue)
|
||||
fireIssueWorkflowWithColumn(ctx, workflow, issue, newColumnID)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -238,15 +235,96 @@ func (*workflowNotifier) PullRequestReview(ctx context.Context, pr *issues_model
|
||||
}
|
||||
}
|
||||
|
||||
func fireIssueWorkflow(ctx context.Context, workflow *project_model.Workflow, issue *issues_model.Issue) {
|
||||
// fireIssueWorkflowWithColumn fires a workflow for an issue with a specific column ID
|
||||
// This is used for ItemColumnChanged events where we need to check the target column
|
||||
func fireIssueWorkflowWithColumn(ctx context.Context, workflow *project_model.Workflow, issue *issues_model.Issue, columnID int64) {
|
||||
// Load issue labels for labels filter
|
||||
if err := issue.LoadLabels(ctx); err != nil {
|
||||
log.Error("LoadLabels: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, filter := range workflow.WorkflowFilters {
|
||||
switch filter.Type {
|
||||
case project_model.WorkflowFilterTypeIssueType:
|
||||
values := strings.Split(filter.Value, ",")
|
||||
if !(slices.Contains(values, "issue") && !issue.IsPull) || (slices.Contains(values, "pull") && issue.IsPull) {
|
||||
// If filter value is empty, match all types
|
||||
if filter.Value == "" {
|
||||
continue
|
||||
}
|
||||
// Filter value can be "issue" or "pull_request"
|
||||
if filter.Value == "issue" && issue.IsPull {
|
||||
return
|
||||
}
|
||||
if filter.Value == "pull_request" && !issue.IsPull {
|
||||
return
|
||||
}
|
||||
case project_model.WorkflowFilterTypeColumn:
|
||||
// If filter value is empty, match all columns
|
||||
if filter.Value == "" {
|
||||
continue
|
||||
}
|
||||
filterColumnID, _ := strconv.ParseInt(filter.Value, 10, 64)
|
||||
if filterColumnID == 0 {
|
||||
log.Error("Invalid column ID: %s", filter.Value)
|
||||
return
|
||||
}
|
||||
// For column changed event, check against the new column ID
|
||||
if columnID != filterColumnID {
|
||||
return
|
||||
}
|
||||
case project_model.WorkflowFilterTypeLabels:
|
||||
// Check if issue has the specified label
|
||||
labelID, _ := strconv.ParseInt(filter.Value, 10, 64)
|
||||
if labelID == 0 {
|
||||
log.Error("Invalid label ID: %s", filter.Value)
|
||||
return
|
||||
}
|
||||
// Check if issue has this label
|
||||
hasLabel := false
|
||||
for _, label := range issue.Labels {
|
||||
if label.ID == labelID {
|
||||
hasLabel = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasLabel {
|
||||
return
|
||||
}
|
||||
default:
|
||||
log.Error("Unsupported filter type: %s", filter.Type)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
executeWorkflowActions(ctx, workflow, issue)
|
||||
}
|
||||
|
||||
func fireIssueWorkflow(ctx context.Context, workflow *project_model.Workflow, issue *issues_model.Issue) {
|
||||
// Load issue labels for labels filter
|
||||
if err := issue.LoadLabels(ctx); err != nil {
|
||||
log.Error("LoadLabels: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, filter := range workflow.WorkflowFilters {
|
||||
switch filter.Type {
|
||||
case project_model.WorkflowFilterTypeIssueType:
|
||||
// If filter value is empty, match all types
|
||||
if filter.Value == "" {
|
||||
continue
|
||||
}
|
||||
// Filter value can be "issue" or "pull_request"
|
||||
if filter.Value == "issue" && issue.IsPull {
|
||||
return
|
||||
}
|
||||
if filter.Value == "pull_request" && !issue.IsPull {
|
||||
return
|
||||
}
|
||||
case project_model.WorkflowFilterTypeColumn:
|
||||
// If filter value is empty, match all columns
|
||||
if filter.Value == "" {
|
||||
continue
|
||||
}
|
||||
columnID, _ := strconv.ParseInt(filter.Value, 10, 64)
|
||||
if columnID == 0 {
|
||||
log.Error("Invalid column ID: %s", filter.Value)
|
||||
@ -260,12 +338,34 @@ func fireIssueWorkflow(ctx context.Context, workflow *project_model.Workflow, is
|
||||
if issueProjectColumnID != columnID {
|
||||
return
|
||||
}
|
||||
case project_model.WorkflowFilterTypeLabels:
|
||||
// Check if issue has the specified label
|
||||
labelID, _ := strconv.ParseInt(filter.Value, 10, 64)
|
||||
if labelID == 0 {
|
||||
log.Error("Invalid label ID: %s", filter.Value)
|
||||
return
|
||||
}
|
||||
// Check if issue has this label
|
||||
hasLabel := false
|
||||
for _, label := range issue.Labels {
|
||||
if label.ID == labelID {
|
||||
hasLabel = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasLabel {
|
||||
return
|
||||
}
|
||||
default:
|
||||
log.Error("Unsupported filter type: %s", filter.Type)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
executeWorkflowActions(ctx, workflow, issue)
|
||||
}
|
||||
|
||||
func executeWorkflowActions(ctx context.Context, workflow *project_model.Workflow, issue *issues_model.Issue) {
|
||||
for _, action := range workflow.WorkflowActions {
|
||||
switch action.Type {
|
||||
case project_model.WorkflowActionTypeColumn:
|
||||
|
||||
@ -332,9 +332,14 @@ 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];
|
||||
// Toggle label selection for add_labels, remove_labels, or filter_labels
|
||||
const toggleLabel = (type, labelId) => {
|
||||
let labels;
|
||||
if (type === 'filter_labels') {
|
||||
labels = store.workflowFilters.labels;
|
||||
} else {
|
||||
labels = store.workflowActions[type];
|
||||
}
|
||||
const index = labels.indexOf(labelId);
|
||||
if (index > -1) {
|
||||
labels.splice(index, 1);
|
||||
@ -672,6 +677,42 @@ onUnmounted(() => {
|
||||
{{ store.projectColumns.find(c => String(c.id) === store.workflowFilters.column)?.title || 'Any column' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field" v-if="hasFilter('labels')">
|
||||
<label>Only if has labels</label>
|
||||
<div v-if="isInEditMode" class="ui fluid multiple search selection dropdown label-dropdown">
|
||||
<input type="hidden" :value="store.workflowFilters.labels.join(',')">
|
||||
<i class="dropdown icon"></i>
|
||||
<div class="text" :class="{ default: !store.workflowFilters.labels?.length }">
|
||||
<span v-if="!store.workflowFilters.labels?.length">Any labels</span>
|
||||
<template v-else>
|
||||
<span v-for="labelId in store.workflowFilters.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('filter_labels', String(label.id))"
|
||||
:class="{ active: store.workflowFilters.labels.includes(String(label.id)), selected: store.workflowFilters.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.workflowFilters.labels?.length" class="text-muted">Any labels</span>
|
||||
<span v-for="labelId in store.workflowFilters.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>
|
||||
|
||||
|
||||
@ -17,6 +17,7 @@ export function createWorkflowStore(props: { projectLink: string, eventID: strin
|
||||
workflowFilters: {
|
||||
issue_type: '', // 'issue', 'pull_request', or ''
|
||||
column: '', // target column ID for item_column_changed event
|
||||
labels: [], // label IDs to filter by
|
||||
},
|
||||
|
||||
workflowActions: {
|
||||
@ -60,7 +61,7 @@ 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: '', column: ''};
|
||||
const frontendFilters = {issue_type: '', column: '', labels: []};
|
||||
// Convert backend action format to frontend format
|
||||
const frontendActions = {column: '', add_labels: [], remove_labels: [], closeIssue: false};
|
||||
|
||||
@ -70,6 +71,8 @@ export function createWorkflowStore(props: { projectLink: string, eventID: strin
|
||||
frontendFilters.issue_type = filter.value;
|
||||
} else if (filter.type === 'column') {
|
||||
frontendFilters.column = filter.value;
|
||||
} else if (filter.type === 'labels') {
|
||||
frontendFilters.labels.push(filter.value);
|
||||
}
|
||||
}
|
||||
|
||||
@ -107,7 +110,7 @@ export function createWorkflowStore(props: { projectLink: string, eventID: strin
|
||||
},
|
||||
|
||||
resetWorkflowData() {
|
||||
store.workflowFilters = {issue_type: '', column: ''};
|
||||
store.workflowFilters = {issue_type: '', column: '', labels: []};
|
||||
store.workflowActions = {column: '', add_labels: [], remove_labels: [], closeIssue: false};
|
||||
},
|
||||
|
||||
@ -170,7 +173,7 @@ 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: '', column: ''};
|
||||
const frontendFilters = {issue_type: '', column: '', labels: []};
|
||||
const frontendActions = {column: '', add_labels: [], remove_labels: [], closeIssue: false};
|
||||
|
||||
if (store.selectedWorkflow.filters && Array.isArray(store.selectedWorkflow.filters)) {
|
||||
@ -179,6 +182,8 @@ export function createWorkflowStore(props: { projectLink: string, eventID: strin
|
||||
frontendFilters.issue_type = filter.value;
|
||||
} else if (filter.type === 'column') {
|
||||
frontendFilters.column = filter.value;
|
||||
} else if (filter.type === 'labels') {
|
||||
frontendFilters.labels.push(filter.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user