diff --git a/CHANGELOG.md b/CHANGELOG.md index c4c9d61956..1d0abb20d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,38 @@ This changelog goes through the changes that have been made in each release without substantial changes to our git log; to see the highlights of what has been added to each release, please refer to the [blog](https://blog.gitea.com). +## [v1.26.1-moko.05.15.00] - 2026-05-31 + +* FEATURES + * feat(ui): add generic combo-multiselect component (#361) + * Reusable dropdown with search, checkable items, and selected-items display + * Template: `shared/combolist.tmpl` — accepts Items, Name, Title, SelectedValues + * Decoupled from issue sidebar — works in any form context + * feat(updates): extension metadata settings for update feed generation + * feat(licenses): platform enforcement, key deletion, expired key cleanup + * feat(licenses): store keys in plaintext, show full key with copy button +* TECH DEBT + * chore: full namespace migration from git.mokoconsulting.tech to code.mokoconsulting.tech (#336, #337, #344) + * Go module path, all imports, template URLs, workflow configs (2,276 files) + * fix(blame): set HasSourceRenderedToggle for renderable files (#344) + * fix(settings): translate team permission strings via data-locale attributes (#344) + * fix(dropzone): use relative path for non-image attachment markdown links (#344) + * fix(templates): add required validation to issue dropdown fields (#350) + * refactor(ts): remove redundant `handled` field from MarkdownHandleIndentionResult (#350) + * refactor(go): rename HasOrgOrUserVisible to IsOwnerVisibleToDoer (#350) + * refactor(go): replace ValuesRepository with maps.Values (Go 1.21+) (#357) + * refactor(go): remove CanEnableEditor wrapper, use CanContentChange directly (#357) + * fix(ts): parseIssueHref now uses URL pathname and trims appSubUrl (#360) + * fix(actions): enforce MaxJobNumPerRun (256) limit when creating jobs (#360) + * fix(css): use calc(infinity * 1px) for --border-radius-full (#361) + * fix(css): remove legacy .center class from 2015, replace with tw-text-center (#361) +* BUGFIXES + * fix(build): use slices.Collect for maps.Values (Go 1.23+ compat) + * fix(licenses): remove duplicate DeleteLicenseKey declaration + * fix(licenses): only show licenses tab when licensing is enabled + * fix(licenses): show feed URLs based on repo update platform setting + * fix(updates): correct dlid prefix and align XML with Joomla standard + ## [v1.26.1-moko.05.06.00] - 2026-05-30 * FEATURES diff --git a/templates/shared/combolist.tmpl b/templates/shared/combolist.tmpl new file mode 100644 index 0000000000..c120ca4ae4 --- /dev/null +++ b/templates/shared/combolist.tmpl @@ -0,0 +1,46 @@ +{{/* + Generic multiselect combo list component. + Provides a dropdown with search, checkable items, and a selected-items display list. + + Parameters: + Name - form input name (required) + Title - display label (required) + Items - slice of items, each must have .Value and .Label fields + SelectedValues - comma-separated string of selected values + Placeholder - search input placeholder (optional, defaults to "Filter...") + EmptyText - text when nothing is selected (optional) + Disabled - whether the control is disabled (optional) + Icon - gear icon shown next to title (optional, defaults to "octicon-gear") +*/}} +
+ + + +
+ {{or .EmptyText "None"}} +
+
diff --git a/web_src/css/index.css b/web_src/css/index.css index 06f7101af9..c09bfd1f7a 100644 --- a/web_src/css/index.css +++ b/web_src/css/index.css @@ -34,6 +34,7 @@ @import "./modules/codeeditor.css"; @import "./modules/chroma.css"; @import "./modules/charescape.css"; +@import "./modules/combo-multiselect.css"; @import "./shared/flex-list.css"; @import "./shared/milestone.css"; diff --git a/web_src/css/modules/combo-multiselect.css b/web_src/css/modules/combo-multiselect.css new file mode 100644 index 0000000000..b92868efa1 --- /dev/null +++ b/web_src/css/modules/combo-multiselect.css @@ -0,0 +1,27 @@ +/* Styles for the generic combo-multiselect component (shared/combolist.tmpl) */ + +.combo-multiselect > .ui.dropdown .item:not(.checked) .item-check-mark { + visibility: hidden; +} + +.combo-multiselect > .ui.dropdown .item .item-check-mark { + margin-right: 0.5em; +} + +.combo-multiselect > .combo-multiselect-list { + margin-top: 0.25em; +} + +.combo-multiselect > .combo-multiselect-list > .item { + display: inline-block; + padding: 2px 6px; + margin: 2px; + border-radius: var(--border-radius); + background: var(--color-label-bg); + font-size: 0.9em; +} + +.combo-multiselect > .combo-multiselect-list > .item.empty-list { + background: none; + color: var(--color-text-light-2); +} diff --git a/web_src/js/features/combo-multiselect.ts b/web_src/js/features/combo-multiselect.ts new file mode 100644 index 0000000000..a3931a22e3 --- /dev/null +++ b/web_src/js/features/combo-multiselect.ts @@ -0,0 +1,93 @@ +// Copyright 2026 Moko Consulting. All rights reserved. +// SPDX-License-Identifier: MIT + +import {fomanticQuery} from '../modules/fomantic/base.ts'; +import {addDelegatedEventListener, queryElemChildren, queryElems, toggleElem} from '../utils/dom.ts'; + +/** + * Generic multiselect combo list component. + * Works with the "shared/combolist" template to provide a reusable + * dropdown with search, checkable items, and a selected-items display list. + * + * Usage: add class="combo-multiselect" to the container element. + * The component is self-contained — no backend calls, just updates the hidden input. + */ +class ComboMultiselect { + container: HTMLElement; + elDropdown: HTMLElement; + elList: HTMLElement; + elComboValue: HTMLInputElement; + + constructor(container: HTMLElement) { + this.container = container; + this.elDropdown = container.querySelector(':scope > .ui.dropdown')!; + this.elList = container.querySelector(':scope > .combo-multiselect-list')!; + this.elComboValue = container.querySelector(':scope > .combo-value')!; + } + + collectCheckedValues(): string[] { + return Array.from( + this.elDropdown.querySelectorAll('.menu > .item.checked'), + (el) => el.getAttribute('data-value')!, + ); + } + + updateUiList() { + const checkedValues = this.collectCheckedValues(); + const elEmptyTip = this.elList.querySelector(':scope > .item.empty-list')!; + queryElemChildren(this.elList, '.item:not(.empty-list)', (el) => el.remove()); + + for (const value of checkedValues) { + const el = this.elDropdown.querySelector(`.menu > .item[data-value="${CSS.escape(value)}"]`); + if (!el) continue; + const labelText = el.querySelector('.item-label')?.textContent || value; + const listItem = document.createElement('span'); + listItem.classList.add('item'); + listItem.textContent = labelText; + this.elList.append(listItem); + } + + toggleElem(elEmptyTip, checkedValues.length === 0); + this.elComboValue.value = checkedValues.join(','); + this.elComboValue.dispatchEvent(new Event('change', {bubbles: true})); + } + + onItemClick(elItem: HTMLElement, e: Event) { + e.preventDefault(); + + if (elItem.matches('.clear-selection')) { + queryElems(this.elDropdown, '.menu > .item', (el) => el.classList.remove('checked')); + this.updateUiList(); + return; + } + + elItem.classList.toggle('checked'); + this.updateUiList(); + } + + init() { + // Restore checked state from initial value + const initialValues = this.elComboValue.value ? this.elComboValue.value.split(',') : []; + for (const value of initialValues) { + const elItem = this.elDropdown.querySelector(`.menu > .item[data-value="${CSS.escape(value)}"]`); + elItem?.classList.add('checked'); + } + this.updateUiList(); + + addDelegatedEventListener(this.elDropdown, 'click', '.item', (el, e) => this.onItemClick(el, e)); + + fomanticQuery(this.elDropdown).dropdown('setting', { + action: 'nothing', + fullTextSearch: 'exact', + hideDividers: 'empty', + }); + } +} + +export function initComboMultiselect() { + queryElems(document, '.combo-multiselect', (el) => { + if (el.hasAttribute('data-combo-inited')) return; + el.setAttribute('data-combo-inited', 'true'); + new ComboMultiselect(el).init(); + }); +} diff --git a/web_src/js/index.ts b/web_src/js/index.ts index cb2b56a5bd..32195e78e6 100644 --- a/web_src/js/index.ts +++ b/web_src/js/index.ts @@ -22,6 +22,7 @@ import {initUserExternalLogins, initUserCheckAppUrl} from './features/user-auth. import {initRepoPullRequestReview, initRepoIssueFilterItemLabel} from './features/repo-issue.ts'; import {initRepoEllipsisButton, initCommitStatuses} from './features/repo-commit.ts'; import {initRepoTopicBar} from './features/repo-home.ts'; +import {initComboMultiselect} from './features/combo-multiselect.ts'; import {initAdminCommon} from './features/admin/common.ts'; import {initRepoCodeView} from './features/repo-code.ts'; import {initSshKeyFormParser} from './features/sshkey-helper.ts'; @@ -105,6 +106,7 @@ const initPerformanceTracer = callInitFunctions([ initTableSort, initRepoFileSearch, initCopyContent, + initComboMultiselect, initAdminCommon, initAdminUserListSearchForm,