mirror of
https://github.com/go-gitea/gitea.git
synced 2026-03-18 05:07:57 +01:00
Add valid when saving project workflow event
This commit is contained in:
parent
df50690df7
commit
0b41bfa135
@ -3977,6 +3977,8 @@ workflows.reopen_issue = Reopen issue
|
||||
workflows.save_workflow_failed = Failed to save workflow
|
||||
workflows.update_workflow_failed = Failed to update workflow status
|
||||
workflows.delete_workflow_failed = Failed to delete workflow
|
||||
workflows.at_least_one_action_required = At least one action must be configured
|
||||
workflows.error.at_least_one_action = At least one action must be configured
|
||||
|
||||
[git.filemode]
|
||||
changed_filemode = %[1]s → %[2]s
|
||||
|
||||
@ -442,6 +442,15 @@ func WorkflowsPost(ctx *context.Context) {
|
||||
filters := convertFormToFilters(form.Filters)
|
||||
actions := convertFormToActions(form.Actions)
|
||||
|
||||
// Validate: at least one action must be configured
|
||||
if len(actions) == 0 {
|
||||
ctx.JSON(http.StatusBadRequest, map[string]any{
|
||||
"error": "NoActions",
|
||||
"message": ctx.Tr("projects.workflows.error.at_least_one_action"),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
eventID, _ := strconv.ParseInt(form.EventID, 10, 64)
|
||||
if eventID == 0 {
|
||||
// check if workflow event is valid
|
||||
|
||||
@ -39,6 +39,7 @@
|
||||
data-locale-save-workflow-failed="{{ctx.Locale.Tr "projects.workflows.save_workflow_failed"}}"
|
||||
data-locale-update-workflow-failed="{{ctx.Locale.Tr "projects.workflows.update_workflow_failed"}}"
|
||||
data-locale-delete-workflow-failed="{{ctx.Locale.Tr "projects.workflows.delete_workflow_failed"}}"
|
||||
data-locale-at-least-one-action-required="{{ctx.Locale.Tr "projects.workflows.at_least_one_action_required"}}"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -444,3 +444,114 @@ func TestProjectWorkflowPermissions(t *testing.T) {
|
||||
fmt.Sprintf("/%s/%s/projects/%d/workflows/%d/delete?_csrf=%s", user.Name, repo.Name, project.ID, workflow.ID, GetUserCSRFToken(t, session2)))
|
||||
session2.MakeRequest(t, req, http.StatusNotFound) // we use 404 to avoid leaking existence
|
||||
}
|
||||
|
||||
func TestProjectWorkflowValidation(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
user := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2})
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
|
||||
// Create a project
|
||||
project := &project_model.Project{
|
||||
Title: "Test Project for Workflow Validation",
|
||||
RepoID: repo.ID,
|
||||
Type: project_model.TypeRepository,
|
||||
TemplateType: project_model.TemplateTypeNone,
|
||||
}
|
||||
err := project_model.NewProject(t.Context(), project)
|
||||
assert.NoError(t, err)
|
||||
|
||||
session := loginUser(t, user.Name)
|
||||
|
||||
// Test 1: Try to create a workflow without any actions (should fail)
|
||||
t.Run("Create workflow without actions should fail", func(t *testing.T) {
|
||||
workflowData := map[string]any{
|
||||
"event_id": string(project_model.WorkflowEventItemOpened),
|
||||
"filters": map[string]any{
|
||||
string(project_model.WorkflowFilterTypeIssueType): "issue",
|
||||
},
|
||||
"actions": map[string]any{
|
||||
// No actions provided - this should trigger validation error
|
||||
},
|
||||
}
|
||||
|
||||
body, err := json.Marshal(workflowData)
|
||||
assert.NoError(t, err)
|
||||
|
||||
req := NewRequestWithBody(t, "POST",
|
||||
fmt.Sprintf("/%s/%s/projects/%d/workflows/item_opened?_csrf=%s", user.Name, repo.Name, project.ID, GetUserCSRFToken(t, session)),
|
||||
strings.NewReader(string(body)))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp := session.MakeRequest(t, req, http.StatusBadRequest)
|
||||
|
||||
// Parse response
|
||||
var result map[string]any
|
||||
err = json.Unmarshal(resp.Body.Bytes(), &result)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "NoActions", result["error"], "Error should be NoActions")
|
||||
assert.NotEmpty(t, result["message"], "Error message should be provided")
|
||||
})
|
||||
|
||||
// Test 2: Try to update a workflow to have no actions (should fail)
|
||||
t.Run("Update workflow to remove all actions should fail", func(t *testing.T) {
|
||||
// First create a valid workflow
|
||||
column := &project_model.Column{
|
||||
Title: "Test Column",
|
||||
ProjectID: project.ID,
|
||||
}
|
||||
err := project_model.NewColumn(t.Context(), column)
|
||||
assert.NoError(t, err)
|
||||
|
||||
workflow := &project_model.Workflow{
|
||||
ProjectID: project.ID,
|
||||
WorkflowEvent: project_model.WorkflowEventItemOpened,
|
||||
WorkflowFilters: []project_model.WorkflowFilter{
|
||||
{
|
||||
Type: project_model.WorkflowFilterTypeIssueType,
|
||||
Value: "issue",
|
||||
},
|
||||
},
|
||||
WorkflowActions: []project_model.WorkflowAction{
|
||||
{
|
||||
Type: project_model.WorkflowActionTypeColumn,
|
||||
Value: strconv.FormatInt(column.ID, 10),
|
||||
},
|
||||
},
|
||||
Enabled: true,
|
||||
}
|
||||
err = project_model.CreateWorkflow(t.Context(), workflow)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Try to update it to have no actions
|
||||
updateData := map[string]any{
|
||||
"event_id": strconv.FormatInt(workflow.ID, 10),
|
||||
"filters": map[string]any{
|
||||
string(project_model.WorkflowFilterTypeIssueType): "issue",
|
||||
},
|
||||
"actions": map[string]any{
|
||||
// No actions - should fail
|
||||
},
|
||||
}
|
||||
|
||||
body, err := json.Marshal(updateData)
|
||||
assert.NoError(t, err)
|
||||
|
||||
req := NewRequestWithBody(t, "POST",
|
||||
fmt.Sprintf("/%s/%s/projects/%d/workflows/%d?_csrf=%s", user.Name, repo.Name, project.ID, workflow.ID, GetUserCSRFToken(t, session)),
|
||||
strings.NewReader(string(body)))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp := session.MakeRequest(t, req, http.StatusBadRequest)
|
||||
|
||||
// Parse response
|
||||
var result map[string]any
|
||||
err = json.Unmarshal(resp.Body.Bytes(), &result)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "NoActions", result["error"], "Error should be NoActions")
|
||||
assert.NotEmpty(t, result["message"], "Error message should be provided")
|
||||
|
||||
// Verify the workflow was not changed
|
||||
unchangedWorkflow, err := project_model.GetWorkflowByID(t.Context(), workflow.ID)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, unchangedWorkflow.WorkflowActions, 1, "Workflow should still have the original action")
|
||||
})
|
||||
}
|
||||
|
||||
@ -49,6 +49,7 @@ const props = defineProps<{
|
||||
saveWorkflowFailed: string;
|
||||
updateWorkflowFailed: string;
|
||||
deleteWorkflowFailed: string;
|
||||
atLeastOneActionRequired: string;
|
||||
},
|
||||
}>();
|
||||
|
||||
|
||||
@ -176,6 +176,19 @@ export function createWorkflowStore(props: any) {
|
||||
async saveWorkflow() {
|
||||
if (!store.selectedWorkflow) return;
|
||||
|
||||
// Validate: at least one action must be configured
|
||||
const hasAtLeastOneAction = !!(
|
||||
store.workflowActions.column ||
|
||||
store.workflowActions.add_labels.length > 0 ||
|
||||
store.workflowActions.remove_labels.length > 0 ||
|
||||
store.workflowActions.issue_state
|
||||
);
|
||||
|
||||
if (!hasAtLeastOneAction) {
|
||||
showErrorToast(props.locale.atLeastOneActionRequired || 'At least one action must be configured');
|
||||
return;
|
||||
}
|
||||
|
||||
store.saving = true;
|
||||
try {
|
||||
// For new workflows, use the base event type
|
||||
@ -196,9 +209,20 @@ export function createWorkflowStore(props: any) {
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error('Response error:', errorText);
|
||||
showErrorToast(`${props.locale.failedToSaveWorkflow}: ${response.status} ${response.statusText}\n${errorText}`);
|
||||
let errorMessage = `${props.locale.failedToSaveWorkflow}: ${response.status} ${response.statusText}`;
|
||||
try {
|
||||
const errorData = await response.json();
|
||||
if (errorData.message) {
|
||||
errorMessage = errorData.message;
|
||||
} else if (errorData.error === 'NoActions') {
|
||||
errorMessage = props.locale.atLeastOneActionRequired || 'At least one action must be configured';
|
||||
}
|
||||
} catch {
|
||||
const errorText = await response.text();
|
||||
console.error('Response error:', errorText);
|
||||
errorMessage += `\n${errorText}`;
|
||||
}
|
||||
showErrorToast(errorMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@ -44,6 +44,7 @@ export async function initProjectWorkflow() {
|
||||
saveWorkflowFailed: workflowDiv.getAttribute('data-locale-save-workflow-failed'),
|
||||
updateWorkflowFailed: workflowDiv.getAttribute('data-locale-update-workflow-failed'),
|
||||
deleteWorkflowFailed: workflowDiv.getAttribute('data-locale-delete-workflow-failed'),
|
||||
atLeastOneActionRequired: workflowDiv.getAttribute('data-locale-at-least-one-action-required'),
|
||||
};
|
||||
|
||||
const View = createApp(ProjectWorkflow, {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user