Skip to content

Commit 0b41bfa

Browse files
committed
Add valid when saving project workflow event
1 parent df50690 commit 0b41bfa

File tree

7 files changed

+152
-3
lines changed

7 files changed

+152
-3
lines changed

options/locale/locale_en-US.ini

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3977,6 +3977,8 @@ workflows.reopen_issue = Reopen issue
39773977
workflows.save_workflow_failed = Failed to save workflow
39783978
workflows.update_workflow_failed = Failed to update workflow status
39793979
workflows.delete_workflow_failed = Failed to delete workflow
3980+
workflows.at_least_one_action_required = At least one action must be configured
3981+
workflows.error.at_least_one_action = At least one action must be configured
39803982

39813983
[git.filemode]
39823984
changed_filemode = %[1]s → %[2]s

routers/web/projects/workflows.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,15 @@ func WorkflowsPost(ctx *context.Context) {
442442
filters := convertFormToFilters(form.Filters)
443443
actions := convertFormToActions(form.Actions)
444444

445+
// Validate: at least one action must be configured
446+
if len(actions) == 0 {
447+
ctx.JSON(http.StatusBadRequest, map[string]any{
448+
"error": "NoActions",
449+
"message": ctx.Tr("projects.workflows.error.at_least_one_action"),
450+
})
451+
return
452+
}
453+
445454
eventID, _ := strconv.ParseInt(form.EventID, 10, 64)
446455
if eventID == 0 {
447456
// check if workflow event is valid

templates/projects/workflows.tmpl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
data-locale-save-workflow-failed="{{ctx.Locale.Tr "projects.workflows.save_workflow_failed"}}"
4040
data-locale-update-workflow-failed="{{ctx.Locale.Tr "projects.workflows.update_workflow_failed"}}"
4141
data-locale-delete-workflow-failed="{{ctx.Locale.Tr "projects.workflows.delete_workflow_failed"}}"
42+
data-locale-at-least-one-action-required="{{ctx.Locale.Tr "projects.workflows.at_least_one_action_required"}}"
4243
>
4344
</div>
4445
</div>

tests/integration/project_workflow_test.go

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -444,3 +444,114 @@ func TestProjectWorkflowPermissions(t *testing.T) {
444444
fmt.Sprintf("/%s/%s/projects/%d/workflows/%d/delete?_csrf=%s", user.Name, repo.Name, project.ID, workflow.ID, GetUserCSRFToken(t, session2)))
445445
session2.MakeRequest(t, req, http.StatusNotFound) // we use 404 to avoid leaking existence
446446
}
447+
448+
func TestProjectWorkflowValidation(t *testing.T) {
449+
defer tests.PrepareTestEnv(t)()
450+
451+
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
452+
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
453+
454+
// Create a project
455+
project := &project_model.Project{
456+
Title: "Test Project for Workflow Validation",
457+
RepoID: repo.ID,
458+
Type: project_model.TypeRepository,
459+
TemplateType: project_model.TemplateTypeNone,
460+
}
461+
err := project_model.NewProject(t.Context(), project)
462+
assert.NoError(t, err)
463+
464+
session := loginUser(t, user.Name)
465+
466+
// Test 1: Try to create a workflow without any actions (should fail)
467+
t.Run("Create workflow without actions should fail", func(t *testing.T) {
468+
workflowData := map[string]any{
469+
"event_id": string(project_model.WorkflowEventItemOpened),
470+
"filters": map[string]any{
471+
string(project_model.WorkflowFilterTypeIssueType): "issue",
472+
},
473+
"actions": map[string]any{
474+
// No actions provided - this should trigger validation error
475+
},
476+
}
477+
478+
body, err := json.Marshal(workflowData)
479+
assert.NoError(t, err)
480+
481+
req := NewRequestWithBody(t, "POST",
482+
fmt.Sprintf("/%s/%s/projects/%d/workflows/item_opened?_csrf=%s", user.Name, repo.Name, project.ID, GetUserCSRFToken(t, session)),
483+
strings.NewReader(string(body)))
484+
req.Header.Set("Content-Type", "application/json")
485+
resp := session.MakeRequest(t, req, http.StatusBadRequest)
486+
487+
// Parse response
488+
var result map[string]any
489+
err = json.Unmarshal(resp.Body.Bytes(), &result)
490+
assert.NoError(t, err)
491+
assert.Equal(t, "NoActions", result["error"], "Error should be NoActions")
492+
assert.NotEmpty(t, result["message"], "Error message should be provided")
493+
})
494+
495+
// Test 2: Try to update a workflow to have no actions (should fail)
496+
t.Run("Update workflow to remove all actions should fail", func(t *testing.T) {
497+
// First create a valid workflow
498+
column := &project_model.Column{
499+
Title: "Test Column",
500+
ProjectID: project.ID,
501+
}
502+
err := project_model.NewColumn(t.Context(), column)
503+
assert.NoError(t, err)
504+
505+
workflow := &project_model.Workflow{
506+
ProjectID: project.ID,
507+
WorkflowEvent: project_model.WorkflowEventItemOpened,
508+
WorkflowFilters: []project_model.WorkflowFilter{
509+
{
510+
Type: project_model.WorkflowFilterTypeIssueType,
511+
Value: "issue",
512+
},
513+
},
514+
WorkflowActions: []project_model.WorkflowAction{
515+
{
516+
Type: project_model.WorkflowActionTypeColumn,
517+
Value: strconv.FormatInt(column.ID, 10),
518+
},
519+
},
520+
Enabled: true,
521+
}
522+
err = project_model.CreateWorkflow(t.Context(), workflow)
523+
assert.NoError(t, err)
524+
525+
// Try to update it to have no actions
526+
updateData := map[string]any{
527+
"event_id": strconv.FormatInt(workflow.ID, 10),
528+
"filters": map[string]any{
529+
string(project_model.WorkflowFilterTypeIssueType): "issue",
530+
},
531+
"actions": map[string]any{
532+
// No actions - should fail
533+
},
534+
}
535+
536+
body, err := json.Marshal(updateData)
537+
assert.NoError(t, err)
538+
539+
req := NewRequestWithBody(t, "POST",
540+
fmt.Sprintf("/%s/%s/projects/%d/workflows/%d?_csrf=%s", user.Name, repo.Name, project.ID, workflow.ID, GetUserCSRFToken(t, session)),
541+
strings.NewReader(string(body)))
542+
req.Header.Set("Content-Type", "application/json")
543+
resp := session.MakeRequest(t, req, http.StatusBadRequest)
544+
545+
// Parse response
546+
var result map[string]any
547+
err = json.Unmarshal(resp.Body.Bytes(), &result)
548+
assert.NoError(t, err)
549+
assert.Equal(t, "NoActions", result["error"], "Error should be NoActions")
550+
assert.NotEmpty(t, result["message"], "Error message should be provided")
551+
552+
// Verify the workflow was not changed
553+
unchangedWorkflow, err := project_model.GetWorkflowByID(t.Context(), workflow.ID)
554+
assert.NoError(t, err)
555+
assert.Len(t, unchangedWorkflow.WorkflowActions, 1, "Workflow should still have the original action")
556+
})
557+
}

web_src/js/components/projects/ProjectWorkflow.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ const props = defineProps<{
4949
saveWorkflowFailed: string;
5050
updateWorkflowFailed: string;
5151
deleteWorkflowFailed: string;
52+
atLeastOneActionRequired: string;
5253
},
5354
}>();
5455

web_src/js/components/projects/WorkflowStore.ts

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,19 @@ export function createWorkflowStore(props: any) {
176176
async saveWorkflow() {
177177
if (!store.selectedWorkflow) return;
178178

179+
// Validate: at least one action must be configured
180+
const hasAtLeastOneAction = !!(
181+
store.workflowActions.column ||
182+
store.workflowActions.add_labels.length > 0 ||
183+
store.workflowActions.remove_labels.length > 0 ||
184+
store.workflowActions.issue_state
185+
);
186+
187+
if (!hasAtLeastOneAction) {
188+
showErrorToast(props.locale.atLeastOneActionRequired || 'At least one action must be configured');
189+
return;
190+
}
191+
179192
store.saving = true;
180193
try {
181194
// For new workflows, use the base event type
@@ -196,9 +209,20 @@ export function createWorkflowStore(props: any) {
196209
});
197210

198211
if (!response.ok) {
199-
const errorText = await response.text();
200-
console.error('Response error:', errorText);
201-
showErrorToast(`${props.locale.failedToSaveWorkflow}: ${response.status} ${response.statusText}\n${errorText}`);
212+
let errorMessage = `${props.locale.failedToSaveWorkflow}: ${response.status} ${response.statusText}`;
213+
try {
214+
const errorData = await response.json();
215+
if (errorData.message) {
216+
errorMessage = errorData.message;
217+
} else if (errorData.error === 'NoActions') {
218+
errorMessage = props.locale.atLeastOneActionRequired || 'At least one action must be configured';
219+
}
220+
} catch {
221+
const errorText = await response.text();
222+
console.error('Response error:', errorText);
223+
errorMessage += `\n${errorText}`;
224+
}
225+
showErrorToast(errorMessage);
202226
return;
203227
}
204228

web_src/js/features/projects/workflow.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ export async function initProjectWorkflow() {
4444
saveWorkflowFailed: workflowDiv.getAttribute('data-locale-save-workflow-failed'),
4545
updateWorkflowFailed: workflowDiv.getAttribute('data-locale-update-workflow-failed'),
4646
deleteWorkflowFailed: workflowDiv.getAttribute('data-locale-delete-workflow-failed'),
47+
atLeastOneActionRequired: workflowDiv.getAttribute('data-locale-at-least-one-action-required'),
4748
};
4849

4950
const View = createApp(ProjectWorkflow, {

0 commit comments

Comments
 (0)