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:
parent
537174c104
commit
0b1b962ab0
@ -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)
|
||||
|
||||
@ -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}}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user