diff --git a/routers/web/projects/workflows.go b/routers/web/projects/workflows.go
index 80cf363384..1ec8f2940c 100644
--- a/routers/web/projects/workflows.go
+++ b/routers/web/projects/workflows.go
@@ -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)
diff --git a/services/projects/workflow_notifier.go b/services/projects/workflow_notifier.go
index 614ea87c0a..812c9f5bd2 100644
--- a/services/projects/workflow_notifier.go
+++ b/services/projects/workflow_notifier.go
@@ -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)
diff --git a/web_src/js/components/projects/ProjectWorkflow.vue b/web_src/js/components/projects/ProjectWorkflow.vue
index 9f056b00c1..36b623c726 100644
--- a/web_src/js/components/projects/ProjectWorkflow.vue
+++ b/web_src/js/components/projects/ProjectWorkflow.vue
@@ -978,13 +978,20 @@ onUnmounted(() => {
-
-
-
-
+
+
-
-
{{ store.workflowActions.closeIssue ? 'Yes' : 'No' }}
+ {{ store.workflowActions.issueState === 'close' ? 'Close issue' :
+ store.workflowActions.issueState === 'reopen' ? 'Reopen issue' : 'No change' }}
@@ -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 {
diff --git a/web_src/js/components/projects/WorkflowStore.ts b/web_src/js/components/projects/WorkflowStore.ts
index 2cc129cd3c..f7009a8cca 100644
--- a/web_src/js/components/projects/WorkflowStore.ts
+++ b/web_src/js/components/projects/WorkflowStore.ts
@@ -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';
+ }
}
}
}