0
0
mirror of https://github.com/go-gitea/gitea.git synced 2026-04-03 21:12:09 +02:00

Refactor column picker to use generic combo list rerender

Address review feedback from bircni and wxiaoguang:

- Add rerender(items, selectedValue) method to IssueSidebarComboList
  so any picker can dynamically rebuild its menu. This replaces the
  one-off renderColumnPicker function with a framework-level capability.

- Convert the column picker template to use the standard
  issue-sidebar-combo structure, so it is initialized by the existing
  queryElems loop alongside milestone/label/assignee pickers.

- Move projectColumnInfo struct to package level (bircni)
- Handle ProjectColumnID and MustDefaultColumn errors (bircni)
- Remove custom Fomantic dropdown setup and DOM manipulation

The rerender method is designed to support future multi-project use:
each project's column picker can independently rerender when its
parent project selection changes.
This commit is contained in:
Myers Carpenter 2026-03-29 23:38:06 +00:00
parent 537174c104
commit 0b1b962ab0
4 changed files with 80 additions and 99 deletions

View File

@ -36,6 +36,11 @@ const (
tplProjectsView templates.TplName = "repo/projects/view"
)
type projectColumnInfo struct {
ID int64 `json:"id"`
Title string `json:"title"`
}
// MustEnableRepoProjects check if repo projects are enabled in settings
func MustEnableRepoProjects(ctx *context.Context) {
if unit.TypeProjects.UnitGlobalDisabled() {
@ -461,12 +466,6 @@ 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)
@ -479,25 +478,31 @@ func UpdateIssueProject(ctx *context.Context) {
ctx.ServerError("GetProjectColumns", err)
return
}
cols := make([]columnInfo, 0, len(columns))
cols := make([]projectColumnInfo, 0, len(columns))
for _, c := range columns {
cols = append(cols, columnInfo{ID: c.ID, Title: c.Title})
cols = append(cols, projectColumnInfo{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)
selectedColumnID, err = issues[0].ProjectColumnID(ctx)
if err != nil {
ctx.ServerError("ProjectColumnID", err)
return
}
if selectedColumnID == 0 {
defaultColumn, err := project.MustDefaultColumn(ctx)
if err == nil {
selectedColumnID = defaultColumn.ID
if err != nil {
ctx.ServerError("MustDefaultColumn", err)
return
}
selectedColumnID = defaultColumn.ID
}
}
result["columns"] = cols
result["selected_column_id"] = selectedColumnID
} else {
result["columns"] = []columnInfo{}
result["columns"] = []projectColumnInfo{}
result["selected_column_id"] = 0
}
ctx.JSON(http.StatusOK, result)

View File

@ -1,29 +1,35 @@
{{$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"}}
{{if and $pageMeta.Issue $pageMeta.Issue.Project $data.ProjectColumns (gt (len $data.ProjectColumns) 1) $pageMeta.CanModifyIssueOrPull}}
<div class="issue-sidebar-combo" id="sidebar-project-column"
data-selection-mode="single" data-update-algo="all"
data-update-url="{{$pageMeta.RepoLink}}/issues/projects/column?issue_id={{$pageMeta.Issue.ID}}">
<input class="combo-value" name="column_id" type="hidden" value="{{$data.SelectedColumnID}}">
<div class="ui dropdown full-width">
<a class="fixed-text muted">
<strong>{{svg "octicon-columns" 16}} Column</strong>
</a>
<div class="menu">
{{range $data.ProjectColumns}}
<div class="item {{if eq .ID $data.SelectedColumnID}}active selected{{end}}" data-value="{{.ID}}">{{.Title}}</div>
<div class="item {{if eq .ID $data.SelectedColumnID}}checked{{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>
<div class="ui list muted-links">
<span class="item empty-list tw-hidden">No column</span>
{{range $data.ProjectColumns}}
{{if eq .ID $data.SelectedColumnID}}
<span class="item">{{svg "octicon-columns" 16 "tw-mr-1"}}{{.Title}}</span>
{{end}}
{{end}}
{{end}}
</div>
</div>
{{else if and $pageMeta.Issue $pageMeta.Issue.Project $data.ProjectColumns (gt (len $data.ProjectColumns) 1)}}
{{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}}
</div>
{{end}}

View File

@ -137,6 +137,29 @@ export class IssueSidebarComboList {
if (this.selectionMode === 'multiple') this.doUpdate();
}
rerender(items: {value: string; text: string}[], selectedValue: string) {
const menu = this.elDropdown.querySelector('.menu')!;
menu.innerHTML = '';
for (const item of items) {
const el = document.createElement('div');
el.className = `item${item.value === selectedValue ? ' checked' : ''}`;
el.setAttribute('data-value', item.value);
el.textContent = item.text;
menu.append(el);
}
this.elComboValue.value = selectedValue;
this.initialValues = selectedValue ? [selectedValue] : [];
this.updateUiList(this.initialValues);
addDelegatedEventListener(this.elDropdown, 'click', '.item', (el, e) => this.onItemClick(el, e));
fomanticQuery(this.elDropdown).dropdown('destroy');
fomanticQuery(this.elDropdown).dropdown({
action: 'nothing',
fullTextSearch: 'exact',
hideDividers: 'empty',
onHide: () => this.onHide(),
});
}
init() {
(this.container as any)._comboList = this;
// init the checked items from initial value

View File

@ -1,7 +1,4 @@
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;
@ -9,17 +6,18 @@ type ColumnInfo = {
};
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;
const columnComboEl = document.querySelector<HTMLElement>('#sidebar-project-column');
if (!columnComboEl) return;
const columnComboList = (columnComboEl as any)._comboList as IssueSidebarComboList | undefined;
if (!columnComboList) return;
comboList.onAfterUpdate = async (response: Response, _changedValues: string[]): Promise<boolean> => {
const data = await response.json();
const columns: ColumnInfo[] = data.columns || [];
@ -27,66 +25,15 @@ export function initProjectColumnPicker() {
comboList.updateUiList(comboList.collectCheckedValues());
const updateUrl = columnSection.getAttribute('data-update-url') || '';
renderColumnPicker(columnSection, columns, selectedColumnID, updateUrl);
if (columns.length > 1) {
columnComboList.rerender(
columns.map((c) => ({value: String(c.id), text: c.title})),
String(selectedColumnID),
);
columnComboEl.classList.remove('tw-hidden');
} else {
columnComboEl.classList.add('tw-hidden');
}
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);
}