mirror of
https://github.com/go-gitea/gitea.git
synced 2026-02-24 19:07:24 +01:00
new layout
This commit is contained in:
parent
5481800fe8
commit
6af504562b
@ -117,11 +117,59 @@ type WorkflowAction struct {
|
||||
ActionValue string
|
||||
}
|
||||
|
||||
// WorkflowEventCapabilities defines what filters and actions are available for each event
|
||||
type WorkflowEventCapabilities struct {
|
||||
AvailableFilters []string `json:"available_filters"`
|
||||
AvailableActions []WorkflowActionType `json:"available_actions"`
|
||||
}
|
||||
|
||||
// GetWorkflowEventCapabilities returns the capabilities for each workflow event
|
||||
func GetWorkflowEventCapabilities() map[WorkflowEvent]WorkflowEventCapabilities {
|
||||
return map[WorkflowEvent]WorkflowEventCapabilities{
|
||||
WorkflowEventItemAddedToProject: {
|
||||
AvailableFilters: []string{"scope"}, // issue, pull_request
|
||||
AvailableActions: []WorkflowActionType{WorkflowActionTypeColumn, WorkflowActionTypeLabel},
|
||||
},
|
||||
WorkflowEventItemReopened: {
|
||||
AvailableFilters: []string{"scope"},
|
||||
AvailableActions: []WorkflowActionType{WorkflowActionTypeColumn, WorkflowActionTypeLabel},
|
||||
},
|
||||
WorkflowEventItemClosed: {
|
||||
AvailableFilters: []string{"scope"},
|
||||
AvailableActions: []WorkflowActionType{WorkflowActionTypeColumn, WorkflowActionTypeLabel},
|
||||
},
|
||||
WorkflowEventCodeChangesRequested: {
|
||||
AvailableFilters: []string{}, // only applies to pull requests
|
||||
AvailableActions: []WorkflowActionType{WorkflowActionTypeColumn, WorkflowActionTypeLabel},
|
||||
},
|
||||
WorkflowEventCodeReviewApproved: {
|
||||
AvailableFilters: []string{}, // only applies to pull requests
|
||||
AvailableActions: []WorkflowActionType{WorkflowActionTypeColumn, WorkflowActionTypeLabel},
|
||||
},
|
||||
WorkflowEventPullRequestMerged: {
|
||||
AvailableFilters: []string{}, // only applies to pull requests
|
||||
AvailableActions: []WorkflowActionType{WorkflowActionTypeColumn, WorkflowActionTypeLabel, WorkflowActionTypeClose},
|
||||
},
|
||||
WorkflowEventAutoArchiveItems: {
|
||||
AvailableFilters: []string{"scope"},
|
||||
AvailableActions: []WorkflowActionType{WorkflowActionTypeColumn},
|
||||
},
|
||||
WorkflowEventAutoAddToProject: {
|
||||
AvailableFilters: []string{"scope"},
|
||||
AvailableActions: []WorkflowActionType{WorkflowActionTypeColumn, WorkflowActionTypeLabel},
|
||||
},
|
||||
WorkflowEventAutoCloseIssue: {
|
||||
AvailableFilters: []string{}, // only applies to issues
|
||||
AvailableActions: []WorkflowActionType{WorkflowActionTypeClose, WorkflowActionTypeLabel},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type Workflow struct {
|
||||
ID int64
|
||||
ProjectID int64 `xorm:"unique(s)"`
|
||||
ProjectID int64 `xorm:"INDEX"`
|
||||
Project *Project `xorm:"-"`
|
||||
WorkflowEvent WorkflowEvent `xorm:"unique(s)"`
|
||||
WorkflowEvent WorkflowEvent `xorm:"INDEX"`
|
||||
WorkflowFilters []WorkflowFilter `xorm:"TEXT json"`
|
||||
WorkflowActions []WorkflowAction `xorm:"TEXT json"`
|
||||
CreatedUnix timeutil.TimeStamp `xorm:"created"`
|
||||
@ -157,16 +205,12 @@ func init() {
|
||||
db.RegisterModel(new(Workflow))
|
||||
}
|
||||
|
||||
func FindWorkflowEvents(ctx context.Context, projectID int64) (map[WorkflowEvent]*Workflow, error) {
|
||||
events := make(map[WorkflowEvent]*Workflow)
|
||||
if err := db.GetEngine(ctx).Where("project_id=?", projectID).Find(&events); err != nil {
|
||||
func FindWorkflowsByProjectID(ctx context.Context, projectID int64) ([]*Workflow, error) {
|
||||
workflows := make([]*Workflow, 0)
|
||||
if err := db.GetEngine(ctx).Where("project_id=?", projectID).Find(&workflows); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res := make(map[WorkflowEvent]*Workflow, len(events))
|
||||
for _, event := range events {
|
||||
res[event.WorkflowEvent] = event
|
||||
}
|
||||
return res, nil
|
||||
return workflows, nil
|
||||
}
|
||||
|
||||
func GetWorkflowByID(ctx context.Context, id int64) (*Workflow, error) {
|
||||
|
||||
@ -4,11 +4,11 @@
|
||||
package projects
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"code.gitea.io/gitea/models/project"
|
||||
"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/templates"
|
||||
"code.gitea.io/gitea/modules/web"
|
||||
@ -20,6 +20,78 @@ var (
|
||||
tmplOrgWorkflows = templates.TplName("org/projects/workflows")
|
||||
)
|
||||
|
||||
// getFilterSummary returns a human-readable summary of the filters
|
||||
func getFilterSummary(filters []project_model.WorkflowFilter) string {
|
||||
if len(filters) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
for _, filter := range filters {
|
||||
if filter.Type == "scope" {
|
||||
switch filter.Value {
|
||||
case "issue":
|
||||
return " (Issues only)"
|
||||
case "pull_request":
|
||||
return " (Pull requests only)"
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// convertFormToFilters converts form filters to WorkflowFilter objects
|
||||
func convertFormToFilters(formFilters map[string]string) []project_model.WorkflowFilter {
|
||||
filters := make([]project_model.WorkflowFilter, 0)
|
||||
|
||||
for key, value := range formFilters {
|
||||
if value != "" {
|
||||
filters = append(filters, project_model.WorkflowFilter{
|
||||
Type: project_model.WorkflowFilterType(key),
|
||||
Value: value,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return filters
|
||||
}
|
||||
|
||||
// convertFormToActions converts form actions to WorkflowAction objects
|
||||
func convertFormToActions(formActions map[string]any) []project_model.WorkflowAction {
|
||||
actions := make([]project_model.WorkflowAction, 0)
|
||||
|
||||
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,
|
||||
})
|
||||
}
|
||||
case "labels":
|
||||
if labels, ok := value.([]string); ok && len(labels) > 0 {
|
||||
for _, label := range labels {
|
||||
if label != "" {
|
||||
actions = append(actions, project_model.WorkflowAction{
|
||||
ActionType: project_model.WorkflowActionTypeLabel,
|
||||
ActionValue: label,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
case "closeIssue":
|
||||
if boolValue, ok := value.(bool); ok && boolValue {
|
||||
actions = append(actions, project_model.WorkflowAction{
|
||||
ActionType: project_model.WorkflowActionTypeClose,
|
||||
ActionValue: "true",
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return actions
|
||||
}
|
||||
|
||||
func WorkflowsEvents(ctx *context.Context) {
|
||||
projectID := ctx.PathParamInt64("id")
|
||||
p, err := project_model.GetProjectByID(ctx, projectID)
|
||||
@ -40,35 +112,60 @@ func WorkflowsEvents(ctx *context.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
workflows, err := project_model.FindWorkflowEvents(ctx, projectID)
|
||||
workflows, err := project_model.FindWorkflowsByProjectID(ctx, projectID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetWorkflows", err)
|
||||
ctx.ServerError("FindWorkflowsByProjectID", err)
|
||||
return
|
||||
}
|
||||
type WorkflowEvent struct {
|
||||
EventID string `json:"event_id"`
|
||||
DisplayName string `json:"display_name"`
|
||||
|
||||
type WorkflowConfig struct {
|
||||
ID int64 `json:"id"`
|
||||
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"`
|
||||
FilterSummary string `json:"filter_summary"` // Human readable filter description
|
||||
}
|
||||
outputWorkflows := make([]*WorkflowEvent, 0, len(workflows))
|
||||
|
||||
outputWorkflows := make([]*WorkflowConfig, 0)
|
||||
events := project_model.GetWorkflowEvents()
|
||||
capabilities := project_model.GetWorkflowEventCapabilities()
|
||||
|
||||
// Create a map for quick lookup of existing workflows
|
||||
workflowMap := make(map[project_model.WorkflowEvent][]*project_model.Workflow)
|
||||
for _, wf := range workflows {
|
||||
workflowMap[wf.WorkflowEvent] = append(workflowMap[wf.WorkflowEvent], wf)
|
||||
}
|
||||
|
||||
for _, event := range events {
|
||||
var workflow *WorkflowEvent
|
||||
for _, wf := range workflows {
|
||||
if wf.WorkflowEvent == event {
|
||||
workflow = &WorkflowEvent{
|
||||
EventID: fmt.Sprintf("%d", wf.ID),
|
||||
DisplayName: string(ctx.Tr(wf.WorkflowEvent.LangKey())),
|
||||
}
|
||||
break
|
||||
existingWorkflows := workflowMap[event]
|
||||
if len(existingWorkflows) > 0 {
|
||||
// Add all existing workflows for this event
|
||||
for _, wf := range existingWorkflows {
|
||||
filterSummary := getFilterSummary(wf.WorkflowFilters)
|
||||
outputWorkflows = append(outputWorkflows, &WorkflowConfig{
|
||||
ID: wf.ID,
|
||||
EventID: strconv.FormatInt(wf.ID, 10),
|
||||
DisplayName: string(ctx.Tr(wf.WorkflowEvent.LangKey())) + filterSummary,
|
||||
Capabilities: capabilities[event],
|
||||
Filters: wf.WorkflowFilters,
|
||||
Actions: wf.WorkflowActions,
|
||||
FilterSummary: filterSummary,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// Add placeholder for creating new workflow
|
||||
outputWorkflows = append(outputWorkflows, &WorkflowConfig{
|
||||
ID: 0,
|
||||
EventID: event.UUID(),
|
||||
DisplayName: string(ctx.Tr(event.LangKey())),
|
||||
Capabilities: capabilities[event],
|
||||
Filters: []project_model.WorkflowFilter{},
|
||||
Actions: []project_model.WorkflowAction{},
|
||||
FilterSummary: "",
|
||||
})
|
||||
}
|
||||
if workflow == nil {
|
||||
workflow = &WorkflowEvent{
|
||||
EventID: event.UUID(),
|
||||
DisplayName: string(ctx.Tr(event.LangKey())),
|
||||
}
|
||||
}
|
||||
outputWorkflows = append(outputWorkflows, workflow)
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, outputWorkflows)
|
||||
@ -115,6 +212,53 @@ func WorkflowsColumns(ctx *context.Context) {
|
||||
ctx.JSON(http.StatusOK, outputColumns)
|
||||
}
|
||||
|
||||
func WorkflowsLabels(ctx *context.Context) {
|
||||
projectID := ctx.PathParamInt64("id")
|
||||
p, err := project_model.GetProjectByID(ctx, projectID)
|
||||
if err != nil {
|
||||
if project_model.IsErrProjectNotExist(err) {
|
||||
ctx.NotFound(nil)
|
||||
} else {
|
||||
ctx.ServerError("GetProjectByID", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Only repository projects have access to labels
|
||||
if p.Type != project_model.TypeRepository {
|
||||
ctx.JSON(http.StatusOK, []any{})
|
||||
return
|
||||
}
|
||||
|
||||
if p.RepoID != ctx.Repo.Repository.ID {
|
||||
ctx.NotFound(nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Get repository labels
|
||||
labels, err := issues_model.GetLabelsByRepoID(ctx, p.RepoID, "", db.ListOptions{})
|
||||
if err != nil {
|
||||
ctx.ServerError("GetLabelsByRepoID", err)
|
||||
return
|
||||
}
|
||||
|
||||
type Label struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Color string `json:"color"`
|
||||
}
|
||||
outputLabels := make([]*Label, 0, len(labels))
|
||||
for _, label := range labels {
|
||||
outputLabels = append(outputLabels, &Label{
|
||||
ID: label.ID,
|
||||
Name: label.Name,
|
||||
Color: label.Color,
|
||||
})
|
||||
}
|
||||
|
||||
ctx.JSON(http.StatusOK, outputLabels)
|
||||
}
|
||||
|
||||
func Workflows(ctx *context.Context) {
|
||||
workflowIDStr := ctx.PathParam("workflow_id")
|
||||
if workflowIDStr == "events" {
|
||||
@ -125,6 +269,10 @@ func Workflows(ctx *context.Context) {
|
||||
WorkflowsColumns(ctx)
|
||||
return
|
||||
}
|
||||
if workflowIDStr == "labels" {
|
||||
WorkflowsLabels(ctx)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Data["WorkflowEvents"] = project_model.GetWorkflowEvents()
|
||||
|
||||
@ -153,9 +301,9 @@ func Workflows(ctx *context.Context) {
|
||||
ctx.Data["PageIsProjectsWorkflows"] = true
|
||||
ctx.Data["Project"] = p
|
||||
|
||||
workflows, err := project_model.FindWorkflowEvents(ctx, projectID)
|
||||
workflows, err := project_model.FindWorkflowsByProjectID(ctx, projectID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetWorkflows", err)
|
||||
ctx.ServerError("FindWorkflowsByProjectID", err)
|
||||
return
|
||||
}
|
||||
for _, wf := range workflows {
|
||||
@ -184,7 +332,7 @@ func Workflows(ctx *context.Context) {
|
||||
}
|
||||
}
|
||||
ctx.Data["CurWorkflow"] = curWorkflow
|
||||
ctx.Data["ProjectLink"] = project.ProjectLinkForRepo(ctx.Repo.Repository, projectID)
|
||||
ctx.Data["ProjectLink"] = project_model.ProjectLinkForRepo(ctx.Repo.Repository, projectID)
|
||||
|
||||
if p.Type == project_model.TypeRepository {
|
||||
ctx.HTML(200, tmplRepoWorkflows)
|
||||
@ -220,19 +368,38 @@ func WorkflowsPost(ctx *context.Context) {
|
||||
}
|
||||
|
||||
form := web.GetForm(ctx).(*WorkflowsPostForm)
|
||||
|
||||
// Convert form data to filters and actions
|
||||
filters := convertFormToFilters(form.Filters)
|
||||
actions := convertFormToActions(form.Actions)
|
||||
|
||||
eventID, _ := strconv.ParseInt(form.EventID, 10, 64)
|
||||
if eventID == 0 {
|
||||
// Create a new workflow
|
||||
// Create a new workflow for the given event
|
||||
wf := &project_model.Workflow{
|
||||
ProjectID: projectID,
|
||||
WorkflowEvent: project_model.WorkflowEvent(form.EventID),
|
||||
WorkflowFilters: []project_model.WorkflowFilter{},
|
||||
WorkflowActions: []project_model.WorkflowAction{},
|
||||
WorkflowFilters: filters,
|
||||
WorkflowActions: actions,
|
||||
}
|
||||
if err := project_model.CreateWorkflow(ctx, wf); err != nil {
|
||||
ctx.ServerError("CreateWorkflow", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Return the newly created workflow with filter summary
|
||||
filterSummary := getFilterSummary(wf.WorkflowFilters)
|
||||
ctx.JSON(http.StatusOK, map[string]any{
|
||||
"success": true,
|
||||
"workflow": map[string]any{
|
||||
"id": wf.ID,
|
||||
"event_id": strconv.FormatInt(wf.ID, 10),
|
||||
"display_name": string(ctx.Tr(wf.WorkflowEvent.LangKey())) + filterSummary,
|
||||
"filters": wf.WorkflowFilters,
|
||||
"actions": wf.WorkflowActions,
|
||||
"filter_summary": filterSummary,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
// Update an existing workflow
|
||||
wf, err := project_model.GetWorkflowByID(ctx, eventID)
|
||||
@ -240,11 +407,30 @@ func WorkflowsPost(ctx *context.Context) {
|
||||
ctx.ServerError("GetWorkflowByID", err)
|
||||
return
|
||||
}
|
||||
wf.WorkflowFilters = []project_model.WorkflowFilter{}
|
||||
wf.WorkflowActions = []project_model.WorkflowAction{}
|
||||
if wf.ProjectID != projectID {
|
||||
ctx.NotFound(nil)
|
||||
return
|
||||
}
|
||||
|
||||
wf.WorkflowFilters = filters
|
||||
wf.WorkflowActions = actions
|
||||
if err := project_model.UpdateWorkflow(ctx, wf); err != nil {
|
||||
ctx.ServerError("UpdateWorkflow", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Return the updated workflow with filter summary
|
||||
filterSummary := getFilterSummary(wf.WorkflowFilters)
|
||||
ctx.JSON(http.StatusOK, map[string]any{
|
||||
"success": true,
|
||||
"workflow": map[string]any{
|
||||
"id": wf.ID,
|
||||
"event_id": strconv.FormatInt(wf.ID, 10),
|
||||
"display_name": string(ctx.Tr(wf.WorkflowEvent.LangKey())) + filterSummary,
|
||||
"filters": wf.WorkflowFilters,
|
||||
"actions": wf.WorkflowActions,
|
||||
"filter_summary": filterSummary,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -1433,6 +1433,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.Group("", func() { //nolint:dupl // duplicates lines 1034-1054
|
||||
m.Get("/new", repo.RenderNewProject)
|
||||
|
||||
@ -44,18 +44,18 @@ func (m *workflowNotifier) NewIssue(ctx context.Context, issue *issues_model.Iss
|
||||
return
|
||||
}
|
||||
|
||||
eventWorkflows, err := project_model.FindWorkflowEvents(ctx, issue.Project.ID)
|
||||
workflows, err := project_model.FindWorkflowsByProjectID(ctx, issue.Project.ID)
|
||||
if err != nil {
|
||||
log.Error("NewIssue: FindWorkflowEvents: %v", err)
|
||||
log.Error("NewIssue: FindWorkflowsByProjectID: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
workflow := eventWorkflows[project_model.WorkflowEventItemAddedToProject]
|
||||
if workflow == nil {
|
||||
return
|
||||
// Find workflows for the ItemAddedToProject event
|
||||
for _, workflow := range workflows {
|
||||
if workflow.WorkflowEvent == project_model.WorkflowEventItemAddedToProject {
|
||||
fireIssueWorkflow(ctx, workflow, issue)
|
||||
}
|
||||
}
|
||||
|
||||
fireIssueWorkflow(ctx, workflow, issue)
|
||||
}
|
||||
|
||||
func (m *workflowNotifier) IssueChangeStatus(ctx context.Context, doer *user_model.User, commitID string, issue *issues_model.Issue, actionComment *issues_model.Comment, isClosed bool) {
|
||||
@ -71,19 +71,19 @@ func (m *workflowNotifier) IssueChangeStatus(ctx context.Context, doer *user_mod
|
||||
return
|
||||
}
|
||||
|
||||
eventWorkflows, err := project_model.FindWorkflowEvents(ctx, issue.Project.ID)
|
||||
workflows, err := project_model.FindWorkflowsByProjectID(ctx, issue.Project.ID)
|
||||
if err != nil {
|
||||
log.Error("NewIssue: FindWorkflowEvents: %v", err)
|
||||
log.Error("IssueChangeStatus: FindWorkflowsByProjectID: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
workflowEvent := util.Iif(isClosed, project_model.WorkflowEventItemClosed, project_model.WorkflowEventItemReopened)
|
||||
workflow := eventWorkflows[workflowEvent]
|
||||
if workflow == nil {
|
||||
return
|
||||
// Find workflows for the specific event
|
||||
for _, workflow := range workflows {
|
||||
if workflow.WorkflowEvent == workflowEvent {
|
||||
fireIssueWorkflow(ctx, workflow, issue)
|
||||
}
|
||||
}
|
||||
|
||||
fireIssueWorkflow(ctx, workflow, issue)
|
||||
}
|
||||
|
||||
func fireIssueWorkflow(ctx context.Context, workflow *project_model.Workflow, issue *issues_model.Issue) {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<script lang="ts" setup>
|
||||
import {onMounted, useTemplateRef} from 'vue';
|
||||
import {onMounted, useTemplateRef, computed, ref} from 'vue';
|
||||
import {createWorkflowStore} from './WorkflowStore.ts';
|
||||
import {svg} from '../../svg.ts';
|
||||
|
||||
@ -13,6 +13,13 @@ const props = defineProps({
|
||||
const store = createWorkflowStore(props);
|
||||
|
||||
const selectWorkflowEvent = (event) => {
|
||||
// Toggle selection - if already selected, deselect
|
||||
if (store.selectedItem === event.event_id) {
|
||||
store.selectedItem = null;
|
||||
store.selectedWorkflow = null;
|
||||
return;
|
||||
}
|
||||
|
||||
store.selectedItem = event.event_id;
|
||||
store.selectedWorkflow = event;
|
||||
store.loadWorkflowData(event.event_id);
|
||||
@ -24,28 +31,139 @@ const selectWorkflowEvent = (event) => {
|
||||
|
||||
const saveWorkflow = async () => {
|
||||
await store.saveWorkflow();
|
||||
};
|
||||
|
||||
const resetWorkflow = () => {
|
||||
store.resetWorkflowData();
|
||||
// After saving, refresh the list to show the new workflow
|
||||
store.workflowEvents = await store.loadEvents();
|
||||
};
|
||||
|
||||
const isWorkflowConfigured = (event) => {
|
||||
// Check if the event_id is a number (saved workflow ID) vs UUID (unconfigured)
|
||||
// If it's a number, it means the workflow has been saved to database
|
||||
return !isNaN(parseInt(event.event_id));
|
||||
return !Number.isNaN(parseInt(event.event_id));
|
||||
};
|
||||
|
||||
// Get flat list of all workflows - directly use backend data
|
||||
const workflowList = computed(() => {
|
||||
return store.workflowEvents.map(workflow => ({
|
||||
...workflow,
|
||||
isConfigured: isWorkflowConfigured(workflow),
|
||||
base_event_type: workflow.event_id.includes('-') ? workflow.event_id : workflow.event_id
|
||||
}));
|
||||
});
|
||||
|
||||
const createNewWorkflow = (baseEventType, capabilities, displayName) => {
|
||||
const tempId = `new-${baseEventType}-${Date.now()}`;
|
||||
const newWorkflow = {
|
||||
id: 0,
|
||||
event_id: tempId,
|
||||
display_name: displayName,
|
||||
capabilities: capabilities,
|
||||
filters: [],
|
||||
actions: [],
|
||||
filter_summary: '',
|
||||
base_event_type: baseEventType,
|
||||
};
|
||||
|
||||
store.selectedWorkflow = newWorkflow;
|
||||
store.selectedItem = tempId;
|
||||
store.resetWorkflowData();
|
||||
};
|
||||
|
||||
const cloneWorkflow = (sourceWorkflow) => {
|
||||
const tempId = `clone-${sourceWorkflow.base_event_type || sourceWorkflow.workflow_event}-${Date.now()}`;
|
||||
const clonedWorkflow = {
|
||||
id: 0,
|
||||
event_id: tempId,
|
||||
display_name: sourceWorkflow.display_name.split(' (')[0], // Remove filter suffix
|
||||
capabilities: sourceWorkflow.capabilities,
|
||||
filters: [...(sourceWorkflow.filters || [])],
|
||||
actions: [...(sourceWorkflow.actions || [])],
|
||||
filter_summary: '',
|
||||
base_event_type: sourceWorkflow.base_event_type || sourceWorkflow.workflow_event,
|
||||
};
|
||||
|
||||
store.selectedWorkflow = clonedWorkflow;
|
||||
store.selectedItem = tempId;
|
||||
|
||||
// Load the source workflow's data into the form
|
||||
store.loadWorkflowData(sourceWorkflow.event_id);
|
||||
};
|
||||
|
||||
const selectWorkflowItem = (item) => {
|
||||
if (item.isConfigured) {
|
||||
// This is a configured workflow, select it
|
||||
selectWorkflowEvent(item);
|
||||
} else {
|
||||
// This is an unconfigured event, create new workflow
|
||||
createNewWorkflow(item.base_event_type, item.capabilities, item.display_name);
|
||||
}
|
||||
};
|
||||
|
||||
const hasAvailableFilters = computed(() => {
|
||||
return store.selectedWorkflow?.capabilities?.available_filters?.length > 0;
|
||||
});
|
||||
|
||||
const hasFilter = (filterType) => {
|
||||
return store.selectedWorkflow?.capabilities?.available_filters?.includes(filterType);
|
||||
};
|
||||
|
||||
const hasAction = (actionType) => {
|
||||
return store.selectedWorkflow?.capabilities?.available_actions?.includes(actionType);
|
||||
};
|
||||
|
||||
const getActionsSummary = (workflow) => {
|
||||
if (!workflow.actions || workflow.actions.length === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const actions = [];
|
||||
for (const action of workflow.actions) {
|
||||
if (action.action_type === 'column') {
|
||||
const column = store.projectColumns.find((c) => c.id === action.action_value);
|
||||
if (column) {
|
||||
actions.push(`Move to "${column.title}"`);
|
||||
}
|
||||
} else if (action.action_type === 'label') {
|
||||
const label = store.projectLabels.find((l) => l.id === action.action_value);
|
||||
if (label) {
|
||||
actions.push(`Add label "${label.name}"`);
|
||||
}
|
||||
} else if (action.action_type === 'close') {
|
||||
actions.push('Close issue');
|
||||
}
|
||||
}
|
||||
|
||||
return actions.join(', ');
|
||||
};
|
||||
|
||||
onMounted(async () => {
|
||||
// Load all necessary data
|
||||
store.workflowEvents = await store.loadEvents();
|
||||
await store.loadProjectColumns();
|
||||
await store.loadProjectLabels();
|
||||
|
||||
// Set initial selected workflow if eventID is provided
|
||||
// Auto-select logic
|
||||
if (props.eventID) {
|
||||
// If eventID is provided in URL, select that specific workflow
|
||||
const selectedEvent = store.workflowEvents.find((e) => e.event_id === props.eventID);
|
||||
if (selectedEvent) {
|
||||
store.selectedItem = props.eventID;
|
||||
store.selectedWorkflow = selectedEvent;
|
||||
await store.loadWorkflowData(props.eventID);
|
||||
|
||||
}
|
||||
} else {
|
||||
// Auto-select first configured workflow, or first item if none configured
|
||||
const items = workflowList.value;
|
||||
if (items.length > 0) {
|
||||
// Find first configured workflow
|
||||
let firstConfigured = items.find(item => item.isConfigured);
|
||||
|
||||
if (firstConfigured) {
|
||||
// Select first configured workflow
|
||||
selectWorkflowItem(firstConfigured);
|
||||
} else {
|
||||
// No configured workflows, select first item
|
||||
selectWorkflowItem(items[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -64,87 +182,45 @@ onMounted(async () => {
|
||||
|
||||
<template>
|
||||
<div ref="elRoot" class="workflow-container">
|
||||
<!-- Left Sidebar - Workflow List -->
|
||||
<div class="workflow-sidebar">
|
||||
<div class="ui fluid vertical menu">
|
||||
<a
|
||||
v-for="event in store.workflowEvents"
|
||||
:key="event.event_id"
|
||||
class="item"
|
||||
:class="{ active: store.selectedItem === event.event_id }"
|
||||
:href="`${props.projectLink}/workflows/${event.event_id}`"
|
||||
@click.prevent="selectWorkflowEvent(event)"
|
||||
>
|
||||
<span class="workflow-status" :class="{ configured: isWorkflowConfigured(event) }">
|
||||
<span v-if="isWorkflowConfigured(event)" v-html="svg('octicon-dot-fill')" class="status-icon configured"></span>
|
||||
<span v-else class="status-icon unconfigured"></span>
|
||||
</span>
|
||||
{{ event.display_name }}
|
||||
</a>
|
||||
<div class="sidebar-header">
|
||||
<h3>Project Workflows</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="workflow-main">
|
||||
<div class="workflow-content">
|
||||
<div v-if="!store.selectedWorkflow" class="ui placeholder segment">
|
||||
<div class="ui icon header">
|
||||
<i class="settings icon"/>
|
||||
Select a workflow event to configure
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="workflow-editor">
|
||||
<div class="ui header">
|
||||
<i class="settings icon"/>
|
||||
{{ store.selectedWorkflow.display_name }}
|
||||
</div>
|
||||
<div class="workflow-form">
|
||||
<div class="ui form">
|
||||
<div class="field">
|
||||
<label>When</label>
|
||||
<div class="ui segment">
|
||||
<div class="description">
|
||||
This workflow will run when: <strong>{{ store.selectedWorkflow.display_name }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-content">
|
||||
<!-- Flat Workflow List -->
|
||||
<div class="workflow-items">
|
||||
<div
|
||||
v-for="item in workflowList"
|
||||
:key="item.event_id"
|
||||
class="workflow-item"
|
||||
:class="{ active: store.selectedItem === item.event_id }"
|
||||
@click="selectWorkflowItem(item)"
|
||||
>
|
||||
<div class="workflow-content">
|
||||
<div class="workflow-info">
|
||||
<span class="status-indicator">
|
||||
<span v-html="svg('octicon-dot-fill')"
|
||||
:class="item.isConfigured ? 'status-active' : 'status-inactive'"/>
|
||||
</span>
|
||||
<div class="workflow-title">{{ item.display_name }}</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Filters</label>
|
||||
<div class="ui segment">
|
||||
<div class="field">
|
||||
<label>Apply to</label>
|
||||
<select class="ui dropdown" v-model="store.workflowFilters.scope">
|
||||
<option value="">Issues And Pull Requests</option>
|
||||
<option value="issue">Issues</option>
|
||||
<option value="pull_request">Pull requests</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Actions</label>
|
||||
<div class="ui segment">
|
||||
<div class="field">
|
||||
<label>Move to column</label>
|
||||
<select class="ui dropdown" v-model="store.workflowActions.column">
|
||||
<option value="">Select column...</option>
|
||||
<option v-for="column in store.projectColumns" :key="column.id" :value="column.id">
|
||||
{{ column.title }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" v-model="store.workflowActions.closeIssue" id="close-issue">
|
||||
<label for="close-issue">Close issue</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<button class="ui primary button" @click="saveWorkflow" :loading="store.saving">
|
||||
Save workflow
|
||||
<div class="workflow-actions">
|
||||
<button
|
||||
class="ui tiny basic button"
|
||||
@click.stop="createNewWorkflow(item.base_event_type, item.capabilities, item.display_name.split('(')[0])"
|
||||
:title="item.isConfigured ? 'Create another workflow' : 'Create workflow'"
|
||||
>
|
||||
<i class="plus icon"/>
|
||||
</button>
|
||||
<button
|
||||
v-if="item.isConfigured"
|
||||
class="ui tiny basic button clone-btn"
|
||||
@click.stop="cloneWorkflow(item)"
|
||||
title="Clone this workflow"
|
||||
>
|
||||
<i class="copy icon"/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -152,107 +228,386 @@ onMounted(async () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Main Content - Editor -->
|
||||
<div class="workflow-main">
|
||||
<!-- Default State -->
|
||||
<div v-if="!store.selectedWorkflow" class="workflow-placeholder">
|
||||
<div class="placeholder-content">
|
||||
<div class="placeholder-icon">
|
||||
<i class="huge settings icon"/>
|
||||
</div>
|
||||
<h3>Select a workflow to configure</h3>
|
||||
<p>Choose an event from the left sidebar to create or configure workflows.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Workflow Editor -->
|
||||
<div v-else class="workflow-editor">
|
||||
<div class="editor-header">
|
||||
<div class="editor-title">
|
||||
<h2>
|
||||
<i class="settings icon"/>
|
||||
{{ store.selectedWorkflow.display_name }}
|
||||
</h2>
|
||||
<p>Configure automated actions for this workflow</p>
|
||||
</div>
|
||||
<div class="editor-actions-header">
|
||||
<button class="ui basic button" @click="store.selectedWorkflow = null; store.selectedItem = null;">
|
||||
<i class="times icon"/>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="editor-content">
|
||||
<div class="ui form">
|
||||
<div class="field">
|
||||
<label>When</label>
|
||||
<div class="ui segment">
|
||||
<div class="description">
|
||||
This workflow will run when: <strong>{{ store.selectedWorkflow.display_name }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters Section -->
|
||||
<div class="field" v-if="hasAvailableFilters">
|
||||
<label>Filters</label>
|
||||
<div class="ui segment">
|
||||
<div class="field" v-if="hasFilter('scope')">
|
||||
<label>Apply to</label>
|
||||
<select class="ui dropdown" v-model="store.workflowFilters.scope">
|
||||
<option value="">Issues And Pull Requests</option>
|
||||
<option value="issue">Issues</option>
|
||||
<option value="pull_request">Pull requests</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions Section -->
|
||||
<div class="field">
|
||||
<label>Actions</label>
|
||||
<div class="ui segment">
|
||||
<div class="field" v-if="hasAction('column')">
|
||||
<label>Move to column</label>
|
||||
<select class="ui dropdown" v-model="store.workflowActions.column">
|
||||
<option value="">Select column...</option>
|
||||
<option v-for="column in store.projectColumns" :key="column.id" :value="column.id">
|
||||
{{ column.title }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="field" v-if="hasAction('label')">
|
||||
<label>Add labels</label>
|
||||
<select class="ui multiple dropdown" v-model="store.workflowActions.labels">
|
||||
<option value="">Select labels...</option>
|
||||
<option v-for="label in store.projectLabels" :key="label.id" :value="label.id">
|
||||
{{ label.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="field" v-if="hasAction('close')">
|
||||
<div class="ui checkbox">
|
||||
<input type="checkbox" v-model="store.workflowActions.closeIssue" id="close-issue">
|
||||
<label for="close-issue">Close issue</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="editor-actions">
|
||||
<button class="ui primary button" @click="saveWorkflow" :class="{ loading: store.saving }">
|
||||
<i class="save icon"/>
|
||||
Save Workflow
|
||||
</button>
|
||||
<button class="ui basic button" @click="store.selectedWorkflow = null; store.selectedItem = null;">
|
||||
<i class="times icon"/>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* Main Layout */
|
||||
.workflow-container {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
width: 100%;
|
||||
min-height: 500px;
|
||||
height: calc(100vh - 200px);
|
||||
min-height: 600px;
|
||||
border: 1px solid #e1e4e8;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.workflow-sidebar {
|
||||
width: 300px;
|
||||
width: 350px;
|
||||
flex-shrink: 0;
|
||||
background: #f6f8fa;
|
||||
border-right: 1px solid #e1e4e8;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.workflow-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
background: white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Sidebar */
|
||||
.sidebar-header {
|
||||
padding: 1rem 1.25rem;
|
||||
border-bottom: 1px solid #e1e4e8;
|
||||
background: #f6f8fa;
|
||||
}
|
||||
|
||||
.sidebar-header h3 {
|
||||
margin: 0;
|
||||
color: #24292e;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.sidebar-content {
|
||||
flex: 1;
|
||||
padding: 1rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Workflow Items */
|
||||
.workflow-items {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.workflow-item {
|
||||
padding: 0.75rem 1rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.workflow-item:hover {
|
||||
background: #f6f8fa;
|
||||
}
|
||||
|
||||
.workflow-item.active {
|
||||
background: #f1f8ff;
|
||||
border-left: 3px solid #0366d6;
|
||||
}
|
||||
|
||||
.workflow-content {
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.workflow-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.workflow-title {
|
||||
font-weight: 500;
|
||||
color: #24292e;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.workflow-actions {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.clone-btn {
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
flex-shrink: 0;
|
||||
padding: 0.25rem 0.5rem !important;
|
||||
font-size: 0.75rem;
|
||||
border: none !important;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.workflow-item:hover .clone-btn {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.status-indicator .status-active {
|
||||
color: #28a745;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.status-indicator .status-inactive {
|
||||
color: #d1d5da;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
|
||||
/* Main Content Area */
|
||||
.workflow-placeholder {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.placeholder-content {
|
||||
text-align: center;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.placeholder-icon {
|
||||
margin-bottom: 1.5rem;
|
||||
color: #d1d5da;
|
||||
}
|
||||
|
||||
.placeholder-content h3 {
|
||||
color: #24292e;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.placeholder-content p {
|
||||
color: #586069;
|
||||
margin-bottom: 2rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* Editor */
|
||||
.workflow-editor {
|
||||
margin-top: 1rem;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.workflow-form .field {
|
||||
.editor-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
padding: 1.5rem;
|
||||
border-bottom: 1px solid #e1e4e8;
|
||||
background: #fafbfc;
|
||||
}
|
||||
|
||||
.editor-title h2 {
|
||||
margin: 0 0 0.25rem 0;
|
||||
color: #24292e;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 600;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.editor-title p {
|
||||
margin: 0;
|
||||
color: #586069;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.editor-actions-header {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.editor-content {
|
||||
flex: 1;
|
||||
padding: 1.5rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.editor-content .field {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.workflow-form .field label {
|
||||
font-weight: bold;
|
||||
.editor-content .field label {
|
||||
font-weight: 600;
|
||||
color: #24292e;
|
||||
margin-bottom: 0.5rem;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.workflow-form .ui.segment {
|
||||
.editor-content .ui.segment {
|
||||
background: #fafbfc;
|
||||
border: 1px solid #e1e4e8;
|
||||
padding: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.workflow-form .description {
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
.editor-content .description {
|
||||
color: #586069;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.workflow-form .actions {
|
||||
.editor-actions {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-top: 2rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid #ddd;
|
||||
padding-top: 1.5rem;
|
||||
border-top: 1px solid #e1e4e8;
|
||||
}
|
||||
|
||||
.ui.placeholder.segment {
|
||||
text-align: center;
|
||||
padding: 3rem;
|
||||
}
|
||||
|
||||
.ui.vertical.menu .item.active {
|
||||
background-color: #f0f0f0;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.workflow-status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.status-icon {
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
.status-icon.configured {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.status-icon.configured svg {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.status-icon.unconfigured {
|
||||
border: 1px solid #6c757d;
|
||||
border-radius: 50%;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
.workflow-container {
|
||||
flex-direction: column;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.workflow-sidebar {
|
||||
width: 100%;
|
||||
max-height: 40vh;
|
||||
border-right: none;
|
||||
border-bottom: 1px solid #e1e4e8;
|
||||
}
|
||||
|
||||
.editor-header {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.editor-content {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.editor-actions {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.sidebar-header {
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.workflow-item {
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.editor-actions button {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@ -7,7 +7,10 @@ export function createWorkflowStore(props: { projectLink: string, eventID: strin
|
||||
selectedItem: props.eventID,
|
||||
selectedWorkflow: null,
|
||||
projectColumns: [],
|
||||
projectLabels: [], // Add labels data
|
||||
saving: false,
|
||||
showCreateDialog: false, // For create workflow dialog
|
||||
selectedEventType: null, // For workflow creation
|
||||
|
||||
workflowFilters: {
|
||||
scope: '', // 'issue', 'pull_request', or ''
|
||||
@ -15,6 +18,7 @@ export function createWorkflowStore(props: { projectLink: string, eventID: strin
|
||||
|
||||
workflowActions: {
|
||||
column: '', // column ID to move to
|
||||
labels: [], // selected label IDs
|
||||
closeIssue: false,
|
||||
},
|
||||
|
||||
@ -35,24 +39,59 @@ export function createWorkflowStore(props: { projectLink: string, eventID: strin
|
||||
},
|
||||
|
||||
async loadWorkflowData(eventId: string) {
|
||||
// Load project columns for the dropdown
|
||||
// Load project columns and labels for the dropdowns
|
||||
await store.loadProjectColumns();
|
||||
await store.loadProjectLabels();
|
||||
|
||||
// Find the workflow from existing workflowEvents
|
||||
const workflow = store.workflowEvents.find((e) => e.event_id === eventId);
|
||||
if (workflow && workflow.filters && workflow.actions) {
|
||||
// Load existing configuration from the workflow data
|
||||
store.workflowFilters = workflow.filters || {scope: ''};
|
||||
store.workflowActions = workflow.actions || {column: '', closeIssue: false};
|
||||
// Convert backend filter format to frontend format
|
||||
const frontendFilters = {scope: ''};
|
||||
if (workflow.filters && Array.isArray(workflow.filters)) {
|
||||
for (const filter of workflow.filters) {
|
||||
if (filter.type === 'scope') {
|
||||
frontendFilters.scope = filter.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Convert backend action format to frontend format
|
||||
const frontendActions = {column: '', labels: [], closeIssue: false};
|
||||
if (workflow.actions && Array.isArray(workflow.actions)) {
|
||||
for (const action of workflow.actions) {
|
||||
if (action.action_type === 'column') {
|
||||
frontendActions.column = action.action_value;
|
||||
} else if (action.action_type === 'label') {
|
||||
frontendActions.labels.push(action.action_value);
|
||||
} else if (action.action_type === 'close') {
|
||||
frontendActions.closeIssue = action.action_value === 'true';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
store.workflowFilters = frontendFilters;
|
||||
store.workflowActions = frontendActions;
|
||||
} else {
|
||||
// Reset to defaults for new workflow
|
||||
store.resetWorkflowData();
|
||||
}
|
||||
},
|
||||
|
||||
async loadProjectLabels() {
|
||||
try {
|
||||
const response = await GET(`${props.projectLink}/workflows/labels`);
|
||||
store.projectLabels = await response.json();
|
||||
} catch (error) {
|
||||
console.error('Failed to load project labels:', error);
|
||||
store.projectLabels = [];
|
||||
}
|
||||
},
|
||||
|
||||
resetWorkflowData() {
|
||||
store.workflowFilters = {scope: ''};
|
||||
store.workflowActions = {column: '', closeIssue: false};
|
||||
store.workflowActions = {column: '', labels: [], closeIssue: false};
|
||||
},
|
||||
|
||||
async saveWorkflow() {
|
||||
@ -60,21 +99,92 @@ export function createWorkflowStore(props: { projectLink: string, eventID: strin
|
||||
|
||||
store.saving = true;
|
||||
try {
|
||||
const workflowData = {
|
||||
event_id: store.selectedWorkflow.event_id,
|
||||
filters: store.workflowFilters,
|
||||
actions: store.workflowActions,
|
||||
};
|
||||
// For new workflows, use the base event type
|
||||
const eventId = store.selectedWorkflow.base_event_type || store.selectedWorkflow.event_id;
|
||||
|
||||
const response = await POST(`${props.projectLink}/workflows/${store.selectedWorkflow.event_id}`, {
|
||||
data: workflowData,
|
||||
// Convert frontend data format to backend form format
|
||||
const formData = new FormData();
|
||||
formData.append('event_id', eventId);
|
||||
|
||||
// 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}`);
|
||||
}
|
||||
|
||||
const response = await POST(`${props.projectLink}/workflows/${eventId}`, {
|
||||
data: formData,
|
||||
});
|
||||
|
||||
console.log('Response status:', response.status);
|
||||
console.log('Response headers:', response.headers);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to save workflow');
|
||||
const errorText = await response.text();
|
||||
console.error('Response error:', errorText);
|
||||
alert(`Failed to save workflow: ${response.status} ${response.statusText}\n${errorText}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('Response result:', result);
|
||||
|
||||
if (result.success && result.workflow) {
|
||||
// For new workflows, add to the store
|
||||
if (store.selectedWorkflow.id === 0 || store.selectedWorkflow.event_id.startsWith('new-')) {
|
||||
store.workflowEvents.push(result.workflow);
|
||||
|
||||
// Update URL to use the new workflow ID
|
||||
const newUrl = `${props.projectLink}/workflows/${result.workflow.event_id}`;
|
||||
window.history.replaceState({eventId: result.workflow.event_id}, '', newUrl);
|
||||
} else {
|
||||
// Update existing workflow
|
||||
const existingIndex = store.workflowEvents.findIndex((e) => e.event_id === store.selectedWorkflow.event_id);
|
||||
if (existingIndex >= 0) {
|
||||
store.workflowEvents[existingIndex] = {
|
||||
...store.workflowEvents[existingIndex],
|
||||
...result.workflow,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Update selected workflow and selectedItem
|
||||
store.selectedWorkflow = result.workflow;
|
||||
store.selectedItem = result.workflow.event_id;
|
||||
alert('Workflow saved successfully!');
|
||||
} else {
|
||||
console.error('Unexpected response format:', result);
|
||||
alert('Failed to save workflow: Unexpected response format');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error saving workflow:', error);
|
||||
alert(`Error saving workflow: ${error.message}`);
|
||||
} finally {
|
||||
store.saving = false;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user