Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions routers/web/repo/issue_page_meta.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment and code was added to wrong place.


data.CanModifyIssueOrPull = ctx.Repo.CanWriteIssuesOrPulls(isPull) && !ctx.Repo.Repository.IsArchived
if !data.CanModifyIssueOrPull {
return data
Expand Down Expand Up @@ -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
Expand Down
89 changes: 89 additions & 0 deletions routers/web/repo/projects.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}

Expand Down
1 change: 1 addition & 0 deletions routers/web/web.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
29 changes: 29 additions & 0 deletions templates/repo/issue/sidebar/project_column.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{{$pageMeta := .}}
{{$data := .ProjectsData}}
<div id="sidebar-project-column"
data-issue-id="{{if $pageMeta.Issue}}{{$pageMeta.Issue.ID}}{{end}}"
data-update-url="{{if $pageMeta.Issue}}{{$pageMeta.RepoLink}}/issues/projects/column?issue_id={{$pageMeta.Issue.ID}}{{end}}">
{{if and $pageMeta.Issue $pageMeta.Issue.Project $data.ProjectColumns (gt (len $data.ProjectColumns) 1)}}
{{if $pageMeta.CanModifyIssueOrPull}}
<div class="ui dropdown selection fluid column-selector-dropdown"
data-update-url="{{$pageMeta.RepoLink}}/issues/projects/column?issue_id={{$pageMeta.Issue.ID}}">
<input type="hidden" name="column_id" value="{{$data.SelectedColumnID}}">
<div class="default text">{{range $data.ProjectColumns}}{{if eq .ID $data.SelectedColumnID}}{{.Title}}{{end}}{{end}}</div>
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
<div class="menu">
{{range $data.ProjectColumns}}
<div class="item {{if eq .ID $data.SelectedColumnID}}active selected{{end}}" data-value="{{.ID}}">{{.Title}}</div>
{{end}}
</div>
</div>
{{else}}
{{range $data.ProjectColumns}}
{{if eq .ID $data.SelectedColumnID}}
<div class="tw-mt-1">
<span class="muted">{{svg "octicon-columns" 16 "tw-mr-1"}}{{.Title}}</span>
</div>
{{end}}
{{end}}
{{end}}
{{end}}
</div>
1 change: 1 addition & 0 deletions templates/repo/issue/sidebar/project_list.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,4 @@
{{end}}
</div>
</div>
{{template "repo/issue/sidebar/project_column" $pageMeta}}
10 changes: 8 additions & 2 deletions web_src/js/features/repo-issue-sidebar-combolist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<boolean>;

constructor(container: HTMLElement) {
this.container = container;
Expand Down Expand Up @@ -63,6 +66,7 @@ export class IssueSidebarComboList {
}

async updateToBackend(changedValues: Array<string>) {
let resp: Response | undefined;
if (this.updateAlgo === 'diff') {
for (const value of this.initialValues) {
if (!changedValues.includes(value)) {
Expand All @@ -71,12 +75,13 @@ export class IssueSidebarComboList {
}
for (const value of changedValues) {
if (!this.initialValues.includes(value)) {
await POST(this.updateUrl, {data: new URLSearchParams({action: 'attach', id: value})});
resp = await POST(this.updateUrl, {data: new URLSearchParams({action: 'attach', id: value})});
}
}
} else {
await POST(this.updateUrl, {data: new URLSearchParams({id: changedValues.join(',')})});
resp = await POST(this.updateUrl, {data: new URLSearchParams({id: changedValues.join(',')})});
}
if (this.onAfterUpdate && resp && await this.onAfterUpdate(resp, changedValues)) return;
issueSidebarReloadConfirmDraftComment();
}

Expand Down Expand Up @@ -133,6 +138,7 @@ export class IssueSidebarComboList {
}

init() {
(this.container as any)._comboList = this;
// init the checked items from initial value
if (this.elComboValue.value && this.elComboValue.value !== '0' && !queryElems(this.elDropdown, `.menu > .item.checked`).length) {
const values = this.elComboValue.value.split(',');
Expand Down
92 changes: 92 additions & 0 deletions web_src/js/features/repo-issue-sidebar-project.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import {POST} from '../modules/fetch.ts';
import type {IssueSidebarComboList} from './repo-issue-sidebar-combolist.ts';
import {html, htmlRaw} from '../utils/html.ts';
import {createElementFromHTML} from '../utils/dom.ts';

type ColumnInfo = {
id: number;
title: string;
};

export function initProjectColumnPicker() {
const columnSection = document.querySelector<HTMLElement>('#sidebar-project-column');
if (!columnSection) return;

initColumnDropdown(columnSection);

const projectCombo = document.querySelector<HTMLElement>('.issue-sidebar-combo[data-update-url*="/issues/projects?"]');
if (!projectCombo) return;

const comboList = (projectCombo as any)._comboList as IssueSidebarComboList | undefined;
if (!comboList) return;

comboList.onAfterUpdate = async (response: Response, _changedValues: string[]): Promise<boolean> => {
const data = await response.json();
const columns: ColumnInfo[] = data.columns || [];
const selectedColumnID: number = data.selected_column_id || 0;

comboList.updateUiList(comboList.collectCheckedValues());

const updateUrl = columnSection.getAttribute('data-update-url') || '';
renderColumnPicker(columnSection, columns, selectedColumnID, updateUrl);
return true;
};
}

function initColumnDropdown(section: HTMLElement) {
const dropdown = section.querySelector<HTMLElement>('.column-selector-dropdown');
if (!dropdown) return;
setupFomanticDropdown(dropdown);
}

function setupFomanticDropdown(el: HTMLElement) {
const updateUrl = el.getAttribute('data-update-url');
if (!updateUrl) return;

$(el).dropdown({
onChange(value: string) {
POST(updateUrl, {data: new URLSearchParams({id: value})});
},
});
}

function renderColumnPicker(
section: HTMLElement,
columns: ColumnInfo[],
selectedColumnID: number,
baseUpdateUrl: string,
) {
section.innerHTML = '';

if (columns.length < 2) return;

const selectedCol = columns.find((c) => c.id === selectedColumnID);
const selectedTitle = selectedCol ? selectedCol.title : columns[0].title;

const svgTriangle = document.querySelector('.svg.octicon-triangle-down')?.cloneNode(true) as SVGElement | null;
const triangleHtml = svgTriangle ? (() => {
svgTriangle.setAttribute('width', '14');
svgTriangle.setAttribute('height', '14');
svgTriangle.classList.add('dropdown', 'icon');
return svgTriangle.outerHTML;
})() : '';

let menuItemsHtml = '';
for (const col of columns) {
const selectedClass = col.id === selectedColumnID ? ' active selected' : '';
menuItemsHtml += html`<div class="item${htmlRaw(selectedClass)}" data-value="${col.id}">${col.title}</div>`;
}

const dropdown = createElementFromHTML(html`
<div class="ui dropdown selection fluid column-selector-dropdown"
data-update-url="${baseUpdateUrl}">
<input type="hidden" name="column_id" value="${selectedColumnID}">
<div class="default text">${selectedTitle}</div>
${htmlRaw(triangleHtml)}
<div class="menu">${htmlRaw(menuItemsHtml)}</div>
</div>
`);

section.append(dropdown);
setupFomanticDropdown(dropdown);
}
4 changes: 4 additions & 0 deletions web_src/js/features/repo-issue-sidebar.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {POST} from '../modules/fetch.ts';
import {queryElems, toggleElem} from '../utils/dom.ts';
import {IssueSidebarComboList} from './repo-issue-sidebar-combolist.ts';
import {initProjectColumnPicker} from './repo-issue-sidebar-project.ts';

function initBranchSelector() {
// TODO: RemoveIssueRef: see "repo/issue/branch_selector_field.tmpl"
Expand Down Expand Up @@ -49,4 +50,7 @@ export function initRepoIssueSidebar() {

// init the combo list: a dropdown for selecting items, and a list for showing selected items and related actions
queryElems<HTMLElement>(document, '.issue-sidebar-combo', (el) => new IssueSidebarComboList(el).init());

// hook up the project column picker (must run after combo list init)
initProjectColumnPicker();
}