From 58a368021fffbf4d3657af068b8354d34c0f4cf9 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Thu, 4 Sep 2025 10:51:40 -0700 Subject: [PATCH] adjust workflow page header --- models/user/user.go | 5 +- models/user/user_list.go | 2 + models/user/user_system.go | 34 +++++++++++ routers/web/projects/workflows.go | 4 +- services/projects/workflow_notifier.go | 17 ++++-- templates/projects/workflows.tmpl | 3 - templates/repo/projects/workflows.tmpl | 8 +-- .../components/projects/ProjectWorkflow.vue | 60 +++++++++---------- 8 files changed, 86 insertions(+), 47 deletions(-) diff --git a/models/user/user.go b/models/user/user.go index ae500f3a1f2..57306c8700f 100644 --- a/models/user/user.go +++ b/models/user/user.go @@ -556,8 +556,9 @@ var globalVars = sync.OnceValue(func() *globalVarsStruct { emailRegexp: regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]*@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$"), systemUserNewFuncs: map[int64]func() *User{ - GhostUserID: NewGhostUser, - ActionsUserID: NewActionsUser, + GhostUserID: NewGhostUser, + ActionsUserID: NewActionsUser, + WorkflowsUserID: NewWorkflowsUser, }, } }) diff --git a/models/user/user_list.go b/models/user/user_list.go index 1b6a27dd862..2aecd5cac04 100644 --- a/models/user/user_list.go +++ b/models/user/user_list.go @@ -36,6 +36,8 @@ func GetPossibleUserFromMap(userID int64, usererMaps map[int64]*User) *User { return NewGhostUser() case ActionsUserID: return NewActionsUser() + case WorkflowsUserID: + return NewWorkflowsUser() case 0: return nil default: diff --git a/models/user/user_system.go b/models/user/user_system.go index e07274d291e..fc1bea41a70 100644 --- a/models/user/user_system.go +++ b/models/user/user_system.go @@ -66,6 +66,37 @@ func (u *User) IsGiteaActions() bool { return u != nil && u.ID == ActionsUserID } +const ( + WorkflowsUserID int64 = -3 + WorkflowsUserName = "gitea-workflows" + WorkflowsUserEmail = "workflows@gitea.io" +) + +func IsGiteaWorkflowsUserName(name string) bool { + return strings.EqualFold(name, WorkflowsUserName) +} + +// NewWorkflowsUser creates and returns a fake user for running the workflows. +func NewWorkflowsUser() *User { + return &User{ + ID: WorkflowsUserID, + Name: WorkflowsUserName, + LowerName: WorkflowsUserName, + IsActive: true, + FullName: "Gitea Workflows", + Email: WorkflowsUserEmail, + KeepEmailPrivate: true, + LoginName: WorkflowsUserName, + Type: UserTypeBot, + AllowCreateOrganization: true, + Visibility: structs.VisibleTypePublic, + } +} + +func (u *User) IsGiteaWorkflows() bool { + return u != nil && u.ID == WorkflowsUserID +} + func GetSystemUserByName(name string) *User { if IsGhostUserName(name) { return NewGhostUser() @@ -73,5 +104,8 @@ func GetSystemUserByName(name string) *User { if IsGiteaActionsUserName(name) { return NewActionsUser() } + if IsGiteaWorkflowsUserName(name) { + return NewWorkflowsUser() + } return nil } diff --git a/routers/web/projects/workflows.go b/routers/web/projects/workflows.go index fc47c64c4f6..346aa337fb6 100644 --- a/routers/web/projects/workflows.go +++ b/routers/web/projects/workflows.go @@ -310,9 +310,7 @@ func Workflows(ctx *context.Context) { } ctx.Data["Title"] = ctx.Tr("projects.workflows") - ctx.Data["PageIsWorkflows"] = true - ctx.Data["PageIsProjects"] = true - ctx.Data["PageIsProjectsWorkflows"] = true + ctx.Data["IsProjectsPage"] = true ctx.Data["Project"] = p workflows, err := project_model.FindWorkflowsByProjectID(ctx, projectID) diff --git a/services/projects/workflow_notifier.go b/services/projects/workflow_notifier.go index 96a9d1a7f75..ad5f9c395b9 100644 --- a/services/projects/workflow_notifier.go +++ b/services/projects/workflow_notifier.go @@ -13,6 +13,7 @@ import ( user_model "code.gitea.io/gitea/models/user" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/util" + issue_service "code.gitea.io/gitea/services/issue" notify_service "code.gitea.io/gitea/services/notify" ) @@ -41,6 +42,7 @@ func (m *workflowNotifier) NewIssue(ctx context.Context, issue *issues_model.Iss return } if issue.Project == nil { + // TODO: handle item opened return } @@ -95,7 +97,7 @@ func fireIssueWorkflow(ctx context.Context, workflow *project_model.Workflow, is return } default: - log.Error("NewIssue: Unsupported filter type: %s", filter.Type) + log.Error("Unsupported filter type: %s", filter.Type) return } } @@ -105,15 +107,22 @@ func fireIssueWorkflow(ctx context.Context, workflow *project_model.Workflow, is case project_model.WorkflowActionTypeColumn: column, err := project_model.GetColumnByProjectIDAndColumnName(ctx, issue.Project.ID, action.ActionValue) if err != nil { - log.Error("NewIssue: GetColumnByProjectIDAndColumnName: %v", err) + log.Error("GetColumnByProjectIDAndColumnName: %v", err) continue } if err := project_model.AddIssueToColumn(ctx, issue.ID, column); err != nil { - log.Error("NewIssue: AddIssueToColumn: %v", err) + log.Error("AddIssueToColumn: %v", err) + continue + } + case project_model.WorkflowActionTypeAddLabels: + case project_model.WorkflowActionTypeRemoveLabels: + case project_model.WorkflowActionTypeClose: + if err := issue_service.CloseIssue(ctx, issue, user_model.NewWorkflowsUser(), ""); err != nil { + log.Error("CloseIssue: %v", err) continue } default: - log.Error("NewIssue: Unsupported action type: %s", action.ActionType) + log.Error("Unsupported action type: %s", action.ActionType) } } } diff --git a/templates/projects/workflows.tmpl b/templates/projects/workflows.tmpl index 9973a82a788..e9222a667e4 100644 --- a/templates/projects/workflows.tmpl +++ b/templates/projects/workflows.tmpl @@ -1,7 +1,4 @@
-
-

{{.Project.Title}} - {{ctx.Locale.Tr "projects.workflows"}}

-
{{template "repo/header" .}}
-
- {{ctx.Locale.Tr "repo.labels"}} - {{ctx.Locale.Tr "repo.milestones"}} - {{ctx.Locale.Tr "repo.issues.new"}} -
+
+ {{svg "octicon-arrow-left"}} {{ctx.Locale.Tr "projects.workflows"}} {{.Project.Title}} +
{{template "projects/workflows" .}}
diff --git a/web_src/js/components/projects/ProjectWorkflow.vue b/web_src/js/components/projects/ProjectWorkflow.vue index f76b67a9d27..6f70f264910 100644 --- a/web_src/js/components/projects/ProjectWorkflow.vue +++ b/web_src/js/components/projects/ProjectWorkflow.vue @@ -18,12 +18,12 @@ const previousSelection = ref(null); // Helper to check if current workflow is in edit mode const isInEditMode = computed(() => { if (!store.selectedWorkflow) return false; - + // Unconfigured workflows (id === 0) are always in edit mode if (store.selectedWorkflow.id === 0) { return true; } - + // Configured workflows use the _isEditing flag return store.selectedWorkflow._isEditing || false; }); @@ -48,14 +48,14 @@ 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 => + const tempIndex = store.workflowEvents.findIndex(w => w.event_id === store.selectedWorkflow.event_id ); if (tempIndex >= 0) { store.workflowEvents.splice(tempIndex, 1); } } - + // Restore previous selection store.selectedItem = previousSelection.value.selectedItem; store.selectedWorkflow = previousSelection.value.selectedWorkflow; @@ -96,7 +96,7 @@ 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 => + const tempIndex = store.workflowEvents.findIndex(w => w.event_id === store.selectedWorkflow.event_id ); if (tempIndex >= 0) { @@ -133,7 +133,7 @@ const deleteWorkflow = async () => { const selectWorkflowEvent = async (event) => { // Prevent rapid successive clicks if (store.loading) return; - + // Toggle selection - if already selected, deselect if (store.selectedItem === event.event_id) { store.selectedItem = null; @@ -144,10 +144,10 @@ const selectWorkflowEvent = async (event) => { try { store.selectedItem = event.event_id; store.selectedWorkflow = event; - + // Wait for DOM update before proceeding await nextTick(); - + await store.loadWorkflowData(event.event_id); // Update URL without page reload @@ -164,7 +164,7 @@ const selectWorkflowEvent = async (event) => { const saveWorkflow = async () => { await store.saveWorkflow(); // The store.saveWorkflow already handles reloading events - + // Clear previous selection after successful save previousSelection.value = null; setEditMode(false); @@ -180,7 +180,7 @@ const getFilterDescription = (workflow) => { if (!workflow.filters || !Array.isArray(workflow.filters) || workflow.filters.length === 0) { return ''; } - + const descriptions = []; for (const filter of workflow.filters) { if (filter.type === 'issue_type' && filter.value) { @@ -192,7 +192,7 @@ const getFilterDescription = (workflow) => { } // Add more filter types here as needed } - + return descriptions.length > 0 ? ` (${descriptions.join(', ')})` : ''; }; @@ -212,7 +212,7 @@ const workflowList = computed(() => { if (!workflows || workflows.length === 0) { return []; } - + return workflows.map((workflow) => ({ ...workflow, isConfigured: isWorkflowConfigured(workflow), @@ -262,7 +262,7 @@ const cloneWorkflow = (sourceWorkflow) => { // Extract base name without filter descriptions const baseName = (sourceWorkflow.display_name || sourceWorkflow.workflow_event || sourceWorkflow.event_id) .replace(/\s*\([^)]*\)\s*/g, ''); // Remove any parenthetical descriptions - + const clonedWorkflow = { id: 0, event_id: tempId, @@ -290,7 +290,7 @@ const cloneWorkflow = (sourceWorkflow) => { // Load the source workflow's data into the form store.loadWorkflowData(sourceWorkflow.event_id); // Cloned workflows (id: 0) are always in edit mode by default - + // Update URL for cloned workflow const newUrl = `${props.projectLink}/workflows/${tempId}`; window.history.pushState({eventId: tempId}, '', newUrl); @@ -302,27 +302,27 @@ let selectTimeout = null; const selectWorkflowItem = async (item) => { // Prevent rapid successive clicks with debounce if (store.loading || selectTimeout) return; - + selectTimeout = setTimeout(() => { selectTimeout = null; }, 300); - + previousSelection.value = null; // Clear previous selection when manually selecting // Don't reset edit mode when switching - each workflow keeps its own state - + // Wait for DOM update to prevent conflicts await nextTick(); - + if (item.isConfigured) { // This is a configured workflow, select it 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 => - w.id === 0 && + const existingWorkflow = store.workflowEvents.find(w => + w.id === 0 && (w.base_event_type === item.base_event_type || w.workflow_event === item.base_event_type) ); - + if (existingWorkflow) { // We already have an unconfigured workflow for this event type, select it await selectWorkflowEvent(existingWorkflow); @@ -330,7 +330,7 @@ const selectWorkflowItem = async (item) => { // This is truly a new unconfigured event, create new workflow createNewWorkflow(item.base_event_type, item.capabilities, item.display_name); } - + // Update URL for workflow const newUrl = `${props.projectLink}/workflows/${item.base_event_type}`; window.history.pushState({eventId: item.base_event_type}, '', newUrl); @@ -353,18 +353,18 @@ const getStatusClass = (item) => { if (!item.isConfigured) { return 'status-inactive'; // Gray dot for unconfigured } - + // For configured workflows, check enabled status if (item.enabled === false) { return 'status-disabled'; // Red dot for disabled } - + return 'status-active'; // Green dot for enabled }; const isItemSelected = (item) => { if (!store.selectedItem) return false; - + if (item.isConfigured || item.id === 0) { // For configured workflows or temporary workflows (clones/new), match by event_id return store.selectedItem === item.event_id; @@ -409,7 +409,7 @@ onMounted(async () => { store.workflowEvents = await store.loadEvents(); await store.loadProjectColumns(); await store.loadProjectLabels(); - + // Add native event listener to prevent conflicts with Gitea await nextTick(); const workflowItemsContainer = elRoot.value.querySelector('.workflow-items'); @@ -445,7 +445,7 @@ onMounted(async () => { } else { // Check if eventID matches a base event type (unconfigured workflow) const items = workflowList.value; - const matchingUnconfigured = items.find((item) => + const matchingUnconfigured = items.find((item) => !item.isConfigured && (item.base_event_type === props.eventID || item.event_id === props.eventID) ); if (matchingUnconfigured) { @@ -495,7 +495,7 @@ const popstateHandler = (e) => { } else { // Check if it's a base event type const items = workflowList.value; - const matchingUnconfigured = items.find((item) => + const matchingUnconfigured = items.find((item) => !item.isConfigured && (item.base_event_type === e.state.eventId || item.event_id === e.state.eventId) ); if (matchingUnconfigured) { @@ -515,7 +515,7 @@ onUnmounted(() => { selectTimeout = null; } window.removeEventListener('popstate', popstateHandler); - + // Remove native click event listener const workflowItemsContainer = elRoot.value?.querySelector('.workflow-items'); if (workflowItemsContainer && workflowClickHandler) { @@ -529,7 +529,7 @@ onUnmounted(() => {
-

Project Workflows

+

Default Workflows