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" "io"
"net/http" "net/http"
"strconv" "strconv"
"strings"
"code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/db"
issues_model "code.gitea.io/gitea/models/issues" 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": case "closeIssue":
if boolValue, ok := value.(bool); ok && boolValue { if boolValue, ok := value.(bool); ok && boolValue {
actions = append(actions, project_model.WorkflowAction{ actions = append(actions, project_model.WorkflowAction{
Type: project_model.WorkflowActionTypeClose, Type: project_model.WorkflowActionTypeClose,
Value: "true", Value: "close",
}) })
} }
} }

View File

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

View File

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

View File

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