pull/30205/head
Lunny Xiao 2025-10-23 15:19:02 -07:00
parent 60bf918934
commit 6c4160dba0
No known key found for this signature in database
GPG Key ID: C3B7C91B632F738A
4 changed files with 83 additions and 29 deletions

View File

@ -8,6 +8,7 @@ import (
"io"
"net/http"
"strconv"
"strings"
"code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues"
@ -132,11 +133,26 @@ func convertFormToActions(formActions map[string]any) []project_model.WorkflowAc
}
}
}
case "issueState":
if strValue, ok := value.(string); ok {
switch strings.ToLower(strValue) {
case "close", "closed", "true":
actions = append(actions, project_model.WorkflowAction{
Type: project_model.WorkflowActionTypeClose,
Value: "close",
})
case "reopen", "open", "false":
actions = append(actions, project_model.WorkflowAction{
Type: project_model.WorkflowActionTypeClose,
Value: "reopen",
})
}
}
case "closeIssue":
if boolValue, ok := value.(bool); ok && boolValue {
actions = append(actions, project_model.WorkflowAction{
Type: project_model.WorkflowActionTypeClose,
Value: "true",
Value: "close",
})
}
}
@ -172,17 +188,17 @@ func WorkflowsEvents(ctx *context.Context) {
}
type WorkflowConfig struct {
ID int64 `json:"id"`
EventID string `json:"event_id"`
DisplayName string `json:"display_name"`
BaseEventType string `json:"base_event_type"` // Base event type for grouping
WorkflowEvent string `json:"workflow_event"` // The actual workflow event
Capabilities project_model.WorkflowEventCapabilities `json:"capabilities"`
Filters []project_model.WorkflowFilter `json:"filters"`
Actions []project_model.WorkflowAction `json:"actions"`
FilterSummary string `json:"filter_summary"` // Human readable filter description
Enabled bool `json:"enabled"`
IsConfigured bool `json:"isConfigured"` // Whether this workflow is configured/saved
ID int64 `json:"id"`
EventID string `json:"event_id"`
DisplayName string `json:"display_name"`
BaseEventType string `json:"base_event_type"` // Base event type for grouping
WorkflowEvent string `json:"workflow_event"` // The actual workflow event
Capabilities project_model.WorkflowEventCapabilities `json:"capabilities"`
Filters []project_model.WorkflowFilter `json:"filters"`
Actions []project_model.WorkflowAction `json:"actions"`
FilterSummary string `json:"filter_summary"` // Human readable filter description
Enabled bool `json:"enabled"`
IsConfigured bool `json:"isConfigured"` // Whether this workflow is configured/saved
}
outputWorkflows := make([]*WorkflowConfig, 0)

View File

@ -6,6 +6,7 @@ package projects
import (
"context"
"strconv"
"strings"
issues_model "code.gitea.io/gitea/models/issues"
project_model "code.gitea.io/gitea/models/project"
@ -424,9 +425,22 @@ func executeWorkflowActions(ctx context.Context, workflow *project_model.Workflo
continue
}
case project_model.WorkflowActionTypeClose:
if err := issue_service.CloseIssue(ctx, issue, user_model.NewProjectWorkflowsUser(), ""); err != nil {
log.Error("CloseIssue: %v", err)
continue
if strings.EqualFold(action.Value, "reopen") || strings.EqualFold(action.Value, "false") {
if issue.IsClosed {
if err := issue_service.ReopenIssue(ctx, issue, user_model.NewProjectWorkflowsUser(), ""); err != nil {
log.Error("ReopenIssue: %v", err)
continue
}
issue.IsClosed = false
}
} else {
if !issue.IsClosed {
if err := issue_service.CloseIssue(ctx, issue, user_model.NewProjectWorkflowsUser(), ""); err != nil {
log.Error("CloseIssue: %v", err)
continue
}
issue.IsClosed = true
}
}
default:
log.Error("Unsupported action type: %s", action.Type)

View File

@ -978,13 +978,20 @@ onUnmounted(() => {
</div>
<div class="field" v-if="hasAction('close')">
<div v-if="isInEditMode" class="form-check">
<input type="checkbox" v-model="store.workflowActions.closeIssue" id="close-issue">
<label for="close-issue">Close issue</label>
</div>
<label for="issue-state-action">Issue state</label>
<select
v-if="isInEditMode"
id="issue-state-action"
class="form-select"
v-model="store.workflowActions.issueState"
>
<option value="">No change</option>
<option value="close">Close issue</option>
<option value="reopen">Reopen issue</option>
</select>
<div v-else class="readonly-value">
<label>Close issue</label>
<div>{{ store.workflowActions.closeIssue ? 'Yes' : 'No' }}</div>
{{ store.workflowActions.issueState === 'close' ? 'Close issue' :
store.workflowActions.issueState === 'reopen' ? 'Reopen issue' : 'No change' }}
</div>
</div>
</div>
@ -1024,6 +1031,7 @@ onUnmounted(() => {
background: white;
display: flex;
flex-direction: column;
min-height: 0;
}
/* Sidebar */
@ -1164,6 +1172,7 @@ onUnmounted(() => {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
}
.editor-header {
@ -1202,6 +1211,7 @@ onUnmounted(() => {
flex: 1;
padding: 1.5rem;
overflow-y: auto;
min-height: 0;
}
.editor-content .field {

View File

@ -8,11 +8,13 @@ type WorkflowFiltersState = {
labels: string[];
};
type WorkflowIssueStateAction = '' | 'close' | 'reopen';
type WorkflowActionsState = {
column: string;
add_labels: string[];
remove_labels: string[];
closeIssue: boolean;
issueState: WorkflowIssueStateAction;
};
type WorkflowDraftState = {
@ -21,7 +23,7 @@ type WorkflowDraftState = {
};
const createDefaultFilters = (): WorkflowFiltersState => ({issue_type: '', column: '', labels: []});
const createDefaultActions = (): WorkflowActionsState => ({column: '', add_labels: [], remove_labels: [], closeIssue: false});
const createDefaultActions = (): WorkflowActionsState => ({column: '', add_labels: [], remove_labels: [], issueState: ''});
const cloneFilters = (filters: WorkflowFiltersState): WorkflowFiltersState => ({
issue_type: filters.issue_type,
@ -33,7 +35,7 @@ const cloneActions = (actions: WorkflowActionsState): WorkflowActionsState => ({
column: actions.column,
add_labels: Array.from(actions.add_labels),
remove_labels: Array.from(actions.remove_labels),
closeIssue: actions.closeIssue,
issueState: actions.issueState,
});
export function createWorkflowStore(props: {projectLink: string, eventID: string}) {
@ -112,7 +114,7 @@ export function createWorkflowStore(props: {projectLink: string, eventID: string
// Convert backend filter format to frontend format
const frontendFilters = {issue_type: '', column: '', labels: []};
// Convert backend action format to frontend format
const frontendActions = {column: '', add_labels: [], remove_labels: [], closeIssue: false};
const frontendActions: WorkflowActionsState = {column: '', add_labels: [], remove_labels: [], issueState: ''};
if (workflow?.filters && Array.isArray(workflow.filters)) {
for (const filter of workflow.filters) {
@ -137,7 +139,11 @@ export function createWorkflowStore(props: {projectLink: string, eventID: string
// 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';
if (action.value === 'reopen' || action.value === 'false') {
frontendActions.issueState = 'reopen';
} else if (action.value === 'true' || action.value === 'close') {
frontendActions.issueState = 'close';
}
}
}
}
@ -153,7 +159,11 @@ export function createWorkflowStore(props: {projectLink: string, eventID: string
// 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';
if (action.value === 'reopen' || action.value === 'false') {
frontendActions.issueState = 'reopen';
} else if (action.value === 'true' || action.value === 'close') {
frontendActions.issueState = 'close';
}
}
}
}
@ -252,7 +262,7 @@ export function createWorkflowStore(props: {projectLink: string, eventID: string
// 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: '', labels: []};
const frontendActions = {column: '', add_labels: [], remove_labels: [], closeIssue: false};
const frontendActions: WorkflowActionsState = {column: '', add_labels: [], remove_labels: [], issueState: ''};
if (store.selectedWorkflow.filters && Array.isArray(store.selectedWorkflow.filters)) {
for (const filter of store.selectedWorkflow.filters) {
@ -275,7 +285,11 @@ export function createWorkflowStore(props: {projectLink: string, eventID: string
} else if (action.type === 'remove_labels') {
frontendActions.remove_labels.push(action.value);
} else if (action.type === 'close') {
frontendActions.closeIssue = action.value === 'true';
if (action.value === 'reopen' || action.value === 'false') {
frontendActions.issueState = 'reopen';
} else if (action.value === 'true' || action.value === 'close') {
frontendActions.issueState = 'close';
}
}
}
}