0
0
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:
Lunny Xiao 2025-10-23 11:06:44 -07:00
parent 74fc30ff71
commit 822387aa38
No known key found for this signature in database
GPG Key ID: C3B7C91B632F738A
5 changed files with 194 additions and 31 deletions

View File

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

View File

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

View File

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

View File

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

View File

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