diff --git a/routers/web/repo/issue_page_meta.go b/routers/web/repo/issue_page_meta.go index 719b485bc5..548134c092 100644 --- a/routers/web/repo/issue_page_meta.go +++ b/routers/web/repo/issue_page_meta.go @@ -37,6 +37,8 @@ type issueSidebarProjectsData struct { SelectedProjectID int64 OpenProjects []*project_model.Project ClosedProjects []*project_model.Project + ProjectColumns []*project_model.Column + SelectedColumnID int64 } type IssuePageMetaData struct { @@ -97,6 +99,12 @@ func retrieveRepoIssueMetaData(ctx *context.Context, repo *repo_model.Repository // A reader(creator) could update some meta (eg: target branch), but can't change assignees anymore. // For non-creator users, only writers could update some meta (eg: assignees, milestone, project) // Need to clarify the logic and add some tests in the future + // Load project column data for all users (read-only display for non-writers) + data.retrieveProjectColumnsData(ctx) + if ctx.Written() { + return data + } + data.CanModifyIssueOrPull = ctx.Repo.CanWriteIssuesOrPulls(isPull) && !ctx.Repo.Repository.IsArchived if !data.CanModifyIssueOrPull { return data @@ -158,6 +166,33 @@ func (d *IssuePageMetaData) retrieveAssigneesData(ctx *context.Context) { ctx.Data["Assignees"] = d.AssigneesData.CandidateAssignees } +func (d *IssuePageMetaData) retrieveProjectColumnsData(ctx *context.Context) { + if d.Issue == nil || d.Issue.Project == nil { + return + } + d.ProjectsData.SelectedProjectID = d.Issue.Project.ID + columns, err := d.Issue.Project.GetColumns(ctx) + if err != nil { + ctx.ServerError("GetProjectColumns", err) + return + } + d.ProjectsData.ProjectColumns = columns + columnID, err := d.Issue.ProjectColumnID(ctx) + if err != nil { + ctx.ServerError("ProjectColumnID", err) + return + } + if columnID == 0 { + defaultColumn, err := d.Issue.Project.MustDefaultColumn(ctx) + if err != nil { + ctx.ServerError("MustDefaultColumn", err) + return + } + columnID = defaultColumn.ID + } + d.ProjectsData.SelectedColumnID = columnID +} + func (d *IssuePageMetaData) retrieveProjectsDataForIssueWriter(ctx *context.Context) { if d.Issue != nil && d.Issue.Project != nil { d.ProjectsData.SelectedProjectID = d.Issue.Project.ID diff --git a/routers/web/repo/projects.go b/routers/web/repo/projects.go index c9bdc5be76..079cc9521d 100644 --- a/routers/web/repo/projects.go +++ b/routers/web/repo/projects.go @@ -461,6 +461,95 @@ func UpdateIssueProject(ctx *context.Context) { } } + // Return columns for the new project so the sidebar column picker + // can update without a page reload. + type columnInfo struct { + ID int64 `json:"id"` + Title string `json:"title"` + } + result := map[string]any{"ok": true} + if projectID > 0 { + project, err := project_model.GetProjectByID(ctx, projectID) + if err != nil { + ctx.ServerError("GetProjectByID", err) + return + } + columns, err := project.GetColumns(ctx) + if err != nil { + ctx.ServerError("GetProjectColumns", err) + return + } + cols := make([]columnInfo, 0, len(columns)) + for _, c := range columns { + cols = append(cols, columnInfo{ID: c.ID, Title: c.Title}) + } + // The issue was assigned to the default column + var selectedColumnID int64 + if len(issues) > 0 { + selectedColumnID, _ = issues[0].ProjectColumnID(ctx) + if selectedColumnID == 0 { + defaultColumn, err := project.MustDefaultColumn(ctx) + if err == nil { + selectedColumnID = defaultColumn.ID + } + } + } + result["columns"] = cols + result["selected_column_id"] = selectedColumnID + } else { + result["columns"] = []columnInfo{} + result["selected_column_id"] = 0 + } + ctx.JSON(http.StatusOK, result) +} + +// UpdateIssueProjectColumn moves an issue to a different column within its current project +func UpdateIssueProjectColumn(ctx *context.Context) { + issueID := ctx.FormInt64("issue_id") + columnID := ctx.FormInt64("id") + + issue, err := issues_model.GetIssueByID(ctx, issueID) + if err != nil { + if issues_model.IsErrIssueNotExist(err) { + ctx.NotFound(nil) + return + } + ctx.ServerError("GetIssueByID", err) + return + } + if issue.RepoID != ctx.Repo.Repository.ID { + ctx.NotFound(nil) + return + } + + if err := issue.LoadProject(ctx); err != nil { + ctx.ServerError("LoadProject", err) + return + } + if issue.Project == nil { + ctx.NotFound(nil) + return + } + + column, err := project_model.GetColumn(ctx, columnID) + if err != nil { + if project_model.IsErrProjectColumnNotExist(err) { + ctx.NotFound(nil) + return + } + ctx.ServerError("GetColumn", err) + return + } + if column.ProjectID != issue.Project.ID { + ctx.NotFound(nil) + return + } + + if err := project_service.MoveIssuesOnProjectColumn(ctx, ctx.Doer, column, map[int64]int64{0: issue.ID}); err != nil { + ctx.ServerError("MoveIssuesOnProjectColumn", err) + return + } + ctx.JSONOK() } diff --git a/routers/web/web.go b/routers/web/web.go index 72d2c27eaf..89635bfa71 100644 --- a/routers/web/web.go +++ b/routers/web/web.go @@ -1354,6 +1354,7 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) { m.Post("/labels", reqRepoIssuesOrPullsWriter, repo.UpdateIssueLabel) m.Post("/milestone", reqRepoIssuesOrPullsWriter, repo.UpdateIssueMilestone) m.Post("/projects", reqRepoIssuesOrPullsWriter, reqRepoProjectsReader, repo.UpdateIssueProject) + m.Post("/projects/column", reqRepoIssuesOrPullsWriter, reqRepoProjectsReader, repo.UpdateIssueProjectColumn) m.Post("/assignee", reqRepoIssuesOrPullsWriter, repo.UpdateIssueAssignee) m.Post("/status", reqRepoIssuesOrPullsWriter, repo.UpdateIssueStatus) m.Post("/delete", reqRepoAdmin, repo.BatchDeleteIssues) diff --git a/templates/repo/issue/sidebar/project_column.tmpl b/templates/repo/issue/sidebar/project_column.tmpl new file mode 100644 index 0000000000..fc9f294c9a --- /dev/null +++ b/templates/repo/issue/sidebar/project_column.tmpl @@ -0,0 +1,29 @@ +{{$pageMeta := .}} +{{$data := .ProjectsData}} +
diff --git a/templates/repo/issue/sidebar/project_list.tmpl b/templates/repo/issue/sidebar/project_list.tmpl index be8097fbbc..e6aa16a8a6 100644 --- a/templates/repo/issue/sidebar/project_list.tmpl +++ b/templates/repo/issue/sidebar/project_list.tmpl @@ -49,3 +49,4 @@ {{end}} +{{template "repo/issue/sidebar/project_column" $pageMeta}} diff --git a/web_src/js/features/repo-issue-sidebar-combolist.ts b/web_src/js/features/repo-issue-sidebar-combolist.ts index aaff3804c7..43e26ab862 100644 --- a/web_src/js/features/repo-issue-sidebar-combolist.ts +++ b/web_src/js/features/repo-issue-sidebar-combolist.ts @@ -31,6 +31,9 @@ export class IssueSidebarComboList { elComboValue: HTMLInputElement; initialValues: string[]; container: HTMLElement; + // Optional callback invoked after the backend update completes. + // If it returns true, the page reload is skipped. + onAfterUpdate?: (response: Response, changedValues: string[]) => Promise