From a18eba002688e4ba368f5361cde36f5252aa4c51 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Thu, 4 Sep 2025 20:23:37 -0700 Subject: [PATCH] fix --- routers/web/projects/workflows.go | 69 ++++-- routers/web/web.go | 6 +- .../components/projects/ProjectWorkflow.vue | 224 +++++++++--------- .../js/components/projects/WorkflowStore.ts | 60 ++--- 4 files changed, 185 insertions(+), 174 deletions(-) diff --git a/routers/web/projects/workflows.go b/routers/web/projects/workflows.go index 346aa337fb..6e7f5b9a55 100644 --- a/routers/web/projects/workflows.go +++ b/routers/web/projects/workflows.go @@ -4,14 +4,17 @@ package projects import ( + "errors" + "io" "net/http" "strconv" "code.gitea.io/gitea/models/db" issues_model "code.gitea.io/gitea/models/issues" project_model "code.gitea.io/gitea/models/project" + "code.gitea.io/gitea/modules/json" + "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/templates" - "code.gitea.io/gitea/modules/web" "code.gitea.io/gitea/services/context" ) @@ -55,6 +58,14 @@ func convertFormToFilters(formFilters map[string]string) []project_model.Workflo return filters } +func convertFiltersToMap(filters []project_model.WorkflowFilter) map[string]string { + filterMap := make(map[string]string) + for _, filter := range filters { + filterMap[string(filter.Type)] = filter.Value + } + return filterMap +} + // convertFormToActions converts form actions to WorkflowAction objects func convertFormToActions(formActions map[string]any) []project_model.WorkflowAction { actions := make([]project_model.WorkflowAction, 0) @@ -62,11 +73,14 @@ func convertFormToActions(formActions map[string]any) []project_model.WorkflowAc for key, value := range formActions { switch key { case "column": - if strValue, ok := value.(string); ok && strValue != "" { - actions = append(actions, project_model.WorkflowAction{ - ActionType: project_model.WorkflowActionTypeColumn, - ActionValue: strValue, - }) + if floatValue, ok := value.(float64); ok { + floatValueInt := int64(floatValue) + if floatValueInt > 0 { + actions = append(actions, project_model.WorkflowAction{ + ActionType: project_model.WorkflowActionTypeColumn, + ActionValue: strconv.FormatInt(floatValueInt, 10), + }) + } } case "add_labels": if labels, ok := value.([]string); ok && len(labels) > 0 { @@ -103,6 +117,14 @@ func convertFormToActions(formActions map[string]any) []project_model.WorkflowAc return actions } +func convertActionsToMap(actions []project_model.WorkflowAction) map[string]any { + actionMap := make(map[string]any) + for _, action := range actions { + actionMap[string(action.ActionType)] = action.ActionValue + } + return actionMap +} + func WorkflowsEvents(ctx *context.Context) { projectID := ctx.PathParamInt64("id") p, err := project_model.GetProjectByID(ctx, projectID) @@ -134,8 +156,8 @@ func WorkflowsEvents(ctx *context.Context) { EventID string `json:"event_id"` DisplayName string `json:"display_name"` Capabilities project_model.WorkflowEventCapabilities `json:"capabilities"` - Filters []project_model.WorkflowFilter `json:"filters"` - Actions []project_model.WorkflowAction `json:"actions"` + Filters map[string]string `json:"filters"` + Actions map[string]any `json:"actions"` FilterSummary string `json:"filter_summary"` // Human readable filter description Enabled bool `json:"enabled"` } @@ -161,8 +183,8 @@ func WorkflowsEvents(ctx *context.Context) { EventID: strconv.FormatInt(wf.ID, 10), DisplayName: string(ctx.Tr(wf.WorkflowEvent.LangKey())) + filterSummary, Capabilities: capabilities[event], - Filters: wf.WorkflowFilters, - Actions: wf.WorkflowActions, + Filters: convertFiltersToMap(wf.WorkflowFilters), + Actions: convertActionsToMap(wf.WorkflowActions), FilterSummary: filterSummary, Enabled: wf.Enabled, }) @@ -174,8 +196,6 @@ func WorkflowsEvents(ctx *context.Context) { EventID: event.UUID(), DisplayName: string(ctx.Tr(event.LangKey())), Capabilities: capabilities[event], - Filters: []project_model.WorkflowFilter{}, - Actions: []project_model.WorkflowAction{}, FilterSummary: "", Enabled: true, // Default to enabled for new workflows }) @@ -354,9 +374,9 @@ func Workflows(ctx *context.Context) { } type WorkflowsPostForm struct { - EventID string `form:"event_id" binding:"Required"` - Filters map[string]string `form:"filters"` - Actions map[string]any `form:"actions"` + EventID string `json:"event_id"` + Filters map[string]string `json:"filters"` + Actions map[string]any `json:"actions"` } func WorkflowsPost(ctx *context.Context) { @@ -379,7 +399,24 @@ func WorkflowsPost(ctx *context.Context) { return } - form := web.GetForm(ctx).(*WorkflowsPostForm) + // Handle both form data and JSON data + // Handle JSON data + form := &WorkflowsPostForm{} + content, err := io.ReadAll(ctx.Req.Body) + if err != nil { + ctx.ServerError("ReadRequestBody", err) + return + } + defer ctx.Req.Body.Close() + log.Trace("get " + string(content)) + if err := json.Unmarshal(content, &form); err != nil { + ctx.ServerError("DecodeWorkflowsPostForm", err) + return + } + if form.EventID == "" { + ctx.ServerError("InvalidEventID", errors.New("EventID is required")) + return + } // Convert form data to filters and actions filters := convertFormToFilters(form.Filters) diff --git a/routers/web/web.go b/routers/web/web.go index c3f6879ca5..06c1001b63 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -1041,10 +1041,10 @@ func registerWebRoutes(m *web.Router) { m.Group("/{id}/workflows", func() { m.Get("", projects.Workflows) m.Get("/{workflow_id}", projects.Workflows) - m.Post("/{workflow_id}", web.Bind(projects.WorkflowsPostForm{}), projects.WorkflowsPost) + m.Post("/{workflow_id}", projects.WorkflowsPost) m.Post("/{workflow_id}/status", projects.WorkflowsStatus) m.Post("/{workflow_id}/delete", projects.WorkflowsDelete) - }) + }, reqUnitAccess(unit.TypeProjects, perm.AccessModeRead, true)) m.Group("", func() { //nolint:dupl // duplicates lines 1421-1441 m.Get("/new", org.RenderNewProject) m.Post("/new", web.Bind(forms.CreateProjectForm{}), org.NewProjectPost) @@ -1435,7 +1435,7 @@ func registerWebRoutes(m *web.Router) { m.Group("/{id}/workflows", func() { m.Get("", projects.Workflows) m.Get("/{workflow_id}", projects.Workflows) - m.Post("/{workflow_id}", web.Bind(projects.WorkflowsPostForm{}), projects.WorkflowsPost) + m.Post("/{workflow_id}", projects.WorkflowsPost) m.Post("/{workflow_id}/status", projects.WorkflowsStatus) m.Post("/{workflow_id}/delete", projects.WorkflowsDelete) }) diff --git a/web_src/js/components/projects/ProjectWorkflow.vue b/web_src/js/components/projects/ProjectWorkflow.vue index 6f70f26491..4d3dde0388 100644 --- a/web_src/js/components/projects/ProjectWorkflow.vue +++ b/web_src/js/components/projects/ProjectWorkflow.vue @@ -48,8 +48,8 @@ const toggleEditMode = () => { // If there was a previous selection, return to it if (store.selectedWorkflow && store.selectedWorkflow.id === 0) { // Remove temporary cloned workflow from list - const tempIndex = store.workflowEvents.findIndex(w => - w.event_id === store.selectedWorkflow.event_id + const tempIndex = store.workflowEvents.findIndex((w) => + w.event_id === store.selectedWorkflow.event_id, ); if (tempIndex >= 0) { store.workflowEvents.splice(tempIndex, 1); @@ -69,7 +69,7 @@ const toggleEditMode = () => { // Entering edit mode - store current selection previousSelection.value = { selectedItem: store.selectedItem, - selectedWorkflow: store.selectedWorkflow ? {...store.selectedWorkflow} : null + selectedWorkflow: store.selectedWorkflow ? {...store.selectedWorkflow} : null, }; setEditMode(true); } @@ -84,7 +84,7 @@ const toggleWorkflowStatus = async () => { }; const deleteWorkflow = async () => { - if (!store.selectedWorkflow || !confirm('Are you sure you want to delete this workflow?')) { + if (!store.selectedWorkflow || !window.confirm('Are you sure you want to delete this workflow?')) { return; } @@ -96,8 +96,8 @@ const deleteWorkflow = async () => { // If deleting a temporary workflow (clone/new), just remove from list if (store.selectedWorkflow.id === 0) { - const tempIndex = store.workflowEvents.findIndex(w => - w.event_id === store.selectedWorkflow.event_id + const tempIndex = store.workflowEvents.findIndex((w) => + w.event_id === store.selectedWorkflow.event_id, ); if (tempIndex >= 0) { store.workflowEvents.splice(tempIndex, 1); @@ -110,9 +110,9 @@ const deleteWorkflow = async () => { } // Find workflows for the same base event type - const sameEventWorkflows = store.workflowEvents.filter(w => + const sameEventWorkflows = store.workflowEvents.filter((w) => w.base_event_type === currentBaseEventType || - w.workflow_event === currentBaseEventType + w.workflow_event === currentBaseEventType, ); if (sameEventWorkflows.length === 0) { @@ -226,7 +226,7 @@ const createNewWorkflow = (baseEventType, capabilities, displayName) => { if (!isInEditMode.value) { previousSelection.value = { selectedItem: store.selectedItem, - selectedWorkflow: store.selectedWorkflow ? {...store.selectedWorkflow} : null + selectedWorkflow: store.selectedWorkflow ? {...store.selectedWorkflow} : null, }; } @@ -255,7 +255,7 @@ const cloneWorkflow = (sourceWorkflow) => { // Store current selection before cloning previousSelection.value = { selectedItem: store.selectedItem, - selectedWorkflow: store.selectedWorkflow ? {...store.selectedWorkflow} : null + selectedWorkflow: store.selectedWorkflow ? {...store.selectedWorkflow} : null, }; const tempId = `clone-${sourceWorkflow.base_event_type || sourceWorkflow.workflow_event}-${Date.now()}`; @@ -276,7 +276,7 @@ const cloneWorkflow = (sourceWorkflow) => { }; // Find the position of source workflow and insert cloned workflow after it - const sourceIndex = store.workflowEvents.findIndex(w => w.event_id === sourceWorkflow.event_id); + const sourceIndex = store.workflowEvents.findIndex((w) => w.event_id === sourceWorkflow.event_id); if (sourceIndex >= 0) { store.workflowEvents.splice(sourceIndex + 1, 0, clonedWorkflow); } else { @@ -318,9 +318,9 @@ const selectWorkflowItem = async (item) => { await selectWorkflowEvent(item); } else { // This is an unconfigured event - check if we already have a workflow object for it - const existingWorkflow = store.workflowEvents.find(w => + const existingWorkflow = store.workflowEvents.find((w) => w.id === 0 && - (w.base_event_type === item.base_event_type || w.workflow_event === item.base_event_type) + (w.base_event_type === item.base_event_type || w.workflow_event === item.base_event_type), ); if (existingWorkflow) { @@ -368,10 +368,9 @@ const isItemSelected = (item) => { if (item.isConfigured || item.id === 0) { // For configured workflows or temporary workflows (clones/new), match by event_id return store.selectedItem === item.event_id; - } else { - // For unconfigured events, match by base_event_type - return store.selectedItem === item.base_event_type; } + // For unconfigured events, match by base_event_type + return store.selectedItem === item.base_event_type; }; const _getActionsSummary = (workflow) => { @@ -446,7 +445,7 @@ onMounted(async () => { // Check if eventID matches a base event type (unconfigured workflow) const items = workflowList.value; const matchingUnconfigured = items.find((item) => - !item.isConfigured && (item.base_event_type === props.eventID || item.event_id === props.eventID) + !item.isConfigured && (item.base_event_type === props.eventID || item.event_id === props.eventID), ); if (matchingUnconfigured) { // Create new workflow for this base event type @@ -496,7 +495,7 @@ const popstateHandler = (e) => { // Check if it's a base event type const items = workflowList.value; const matchingUnconfigured = items.find((item) => - !item.isConfigured && (item.base_event_type === e.state.eventId || item.event_id === e.state.eventId) + !item.isConfigured && (item.base_event_type === e.state.eventId || item.event_id === e.state.eventId), ); if (matchingUnconfigured) { createNewWorkflow(matchingUnconfigured.base_event_type, matchingUnconfigured.capabilities, matchingUnconfigured.display_name); @@ -578,9 +577,11 @@ onUnmounted(() => {

{{ store.selectedWorkflow.display_name }} - + {{ store.selectedWorkflow.enabled ? 'Enabled' : 'Disabled' }}

@@ -625,114 +626,115 @@ onUnmounted(() => { -
-
-
- -
-
- This workflow will run when: {{ store.selectedWorkflow.display_name }} -
-
-
- - -
- -
-
- - -
- {{ store.workflowFilters.issue_type === 'issue' ? 'Issues' : - store.workflowFilters.issue_type === 'pull_request' ? 'Pull requests' : - 'Issues And Pull Requests' }} + +
+
+
+ +
+
+ This workflow will run when: {{ store.selectedWorkflow.display_name }}
-
- -
- -
-
- - -
- {{ store.projectColumns.find(c => c.id === store.workflowActions.column)?.title || 'None' }} + +
+ +
+
+ + +
+ {{ store.workflowFilters.issue_type === 'issue' ? 'Issues' : + store.workflowFilters.issue_type === 'pull_request' ? 'Pull requests' : + 'Issues And Pull Requests' }} +
+
-
- - -
- {{ store.workflowActions.labels?.map(id => - store.projectLabels.find(l => l.id === id)?.name).join(', ') || 'None' }} + +
+ +
+
+ + +
+ {{ store.projectColumns.find(c => c.id === store.workflowActions.column)?.title || 'None' }} +
-
-
-
- - +
+ + +
+ {{ store.workflowActions.add_labels?.map(id => + store.projectLabels.find(l => l.id === id)?.name).join(', ') || 'None' }} +
-
- -
{{ store.workflowActions.closeIssue ? 'Yes' : 'No' }}
+ +
+
+ + +
+
+ +
{{ store.workflowActions.closeIssue ? 'Yes' : 'No' }}
+
-
- -
- - -
+ +
+ + +
+
-
diff --git a/web_src/js/components/projects/WorkflowStore.ts b/web_src/js/components/projects/WorkflowStore.ts index 1c9b6a793b..32689d01d8 100644 --- a/web_src/js/components/projects/WorkflowStore.ts +++ b/web_src/js/components/projects/WorkflowStore.ts @@ -19,7 +19,7 @@ export function createWorkflowStore(props: { projectLink: string, eventID: strin workflowActions: { column: '', // column ID to move to - labels: [], // selected label IDs + add_labels: [], // selected label IDs closeIssue: false, }, @@ -67,7 +67,7 @@ export function createWorkflowStore(props: { projectLink: string, eventID: strin if (action.action_type === 'column') { frontendActions.column = action.action_value; } else if (action.action_type === 'add_labels') { - frontendActions.labels.push(action.action_value); + frontendActions.add_labels.push(action.action_value); } else if (action.action_type === 'close') { frontendActions.closeIssue = action.action_value === 'true'; } @@ -108,49 +108,23 @@ export function createWorkflowStore(props: { projectLink: string, eventID: strin // For new workflows, use the base event type const eventId = store.selectedWorkflow.base_event_type || store.selectedWorkflow.event_id; - // Convert frontend data format to backend form format - const formData = new FormData(); - formData.append('event_id', eventId); + // Convert frontend data format to backend JSON format + const postData = { + event_id: eventId, + filters: store.workflowFilters, + actions: store.workflowActions, + }; - // Add filters as form fields - for (const [key, value] of Object.entries(store.workflowFilters)) { - if (value !== '') { - formData.append(`filters[${key}]`, value); - } - } - - // Add actions as form fields - for (const [key, value] of Object.entries(store.workflowActions)) { - if (key === 'labels' && Array.isArray(value)) { - // Handle label array - for (const labelId of value) { - if (labelId !== '') { - formData.append(`actions[labels][]`, labelId); - } - } - } else if (key === 'closeIssue') { - // Handle boolean - formData.append(`actions[${key}]`, value.toString()); - } else if (value !== '') { - // Handle string fields - formData.append(`actions[${key}]`, value); - } - } - - console.log('Saving workflow with FormData'); - console.log('URL:', `${props.projectLink}/workflows/${eventId}`); - // Log form data entries - for (const [key, value] of formData.entries()) { - console.log(`${key}: ${value}`); - } + // Send workflow data + console.info('Sending workflow data:', postData); const response = await POST(`${props.projectLink}/workflows/${eventId}`, { - data: formData, + data: postData, + headers: { + 'Content-Type': 'application/json', + }, }); - console.log('Response status:', response.status); - console.log('Response headers:', response.headers); - if (!response.ok) { const errorText = await response.text(); console.error('Response error:', errorText); @@ -163,8 +137,8 @@ export function createWorkflowStore(props: { projectLink: string, eventID: strin if (result.success && result.workflow) { // Always reload the events list to get the updated structure // This ensures we have both the base event and the new filtered event - const wasNewWorkflow = store.selectedWorkflow.id === 0 || - store.selectedWorkflow.event_id.startsWith('new-') || + const wasNewWorkflow = store.selectedWorkflow.id === 0 || + store.selectedWorkflow.event_id.startsWith('new-') || store.selectedWorkflow.event_id.startsWith('clone-'); // Reload events from server to get the correct event structure @@ -222,7 +196,6 @@ export function createWorkflowStore(props: { projectLink: string, eventID: strin if (existingIndex >= 0) { store.workflowEvents[existingIndex].enabled = store.selectedWorkflow.enabled; } - console.log(`Workflow status updated to: ${store.selectedWorkflow.enabled ? 'enabled' : 'disabled'}`); } else { // Revert the status change on failure store.selectedWorkflow.enabled = !store.selectedWorkflow.enabled; @@ -260,7 +233,6 @@ export function createWorkflowStore(props: { projectLink: string, eventID: strin if (existingIndex >= 0) { store.workflowEvents.splice(existingIndex, 1); } - console.log('Workflow deleted successfully'); } else { alert('Failed to delete workflow'); }