From 9633f336c87947dc7d2a5e76077a10699ba5e50d Mon Sep 17 00:00:00 2001 From: a1012112796 <1012112796@qq.com> Date: Sat, 10 Aug 2024 09:09:34 +0800 Subject: [PATCH 1/5] Add warning message in merge instructions when `AutodetectManualMerge` was not enabled (#31805) not enabled quick-f-i-x https://github.com/go-gitea/gitea/issues/31433 ? , maybe need more disscusion about better solutions. example view: ![image](https://github.com/user-attachments/assets/2af7e1e8-42b9-4473-89c7-12d4a9205d3f) adtion notes about how to enable `AutodetectManualMerge` ![image](https://github.com/user-attachments/assets/28f84317-367a-40d8-b50d-a19ef7c664d4) Signed-off-by: a1012112796 <1012112796@qq.com> --- options/locale/locale_en-US.ini | 1 + routers/web/repo/issue.go | 2 ++ .../repo/issue/view_content/pull_merge_instruction.tmpl | 8 +++++++- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index cca068a3a2..28b3df6c49 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -1891,6 +1891,7 @@ pulls.cmd_instruction_checkout_title = Checkout pulls.cmd_instruction_checkout_desc = From your project repository, check out a new branch and test the changes. pulls.cmd_instruction_merge_title = Merge pulls.cmd_instruction_merge_desc = Merge the changes and update on Gitea. +pulls.cmd_instruction_merge_warning = Warning: This operation can not merge pull request because "autodetect manual merge" was not enable pulls.clear_merge_message = Clear merge message pulls.clear_merge_message_hint = Clearing the merge message will only remove the commit message content and keep generated git trailers such as "Co-Authored-By …". diff --git a/routers/web/repo/issue.go b/routers/web/repo/issue.go index 4773cc9adc..691de94290 100644 --- a/routers/web/repo/issue.go +++ b/routers/web/repo/issue.go @@ -1869,6 +1869,8 @@ func ViewIssue(ctx *context.Context) { } prConfig := prUnit.PullRequestsConfig() + ctx.Data["AutodetectManualMerge"] = prConfig.AutodetectManualMerge + var mergeStyle repo_model.MergeStyle // Check correct values and select default if ms, ok := ctx.Data["MergeStyle"].(repo_model.MergeStyle); !ok || diff --git a/templates/repo/issue/view_content/pull_merge_instruction.tmpl b/templates/repo/issue/view_content/pull_merge_instruction.tmpl index bb59b49719..9a3e2cb7d7 100644 --- a/templates/repo/issue/view_content/pull_merge_instruction.tmpl +++ b/templates/repo/issue/view_content/pull_merge_instruction.tmpl @@ -15,7 +15,13 @@
git checkout {{$localBranch}}
{{if .ShowMergeInstructions}} -

{{ctx.Locale.Tr "repo.pulls.cmd_instruction_merge_title"}}

{{ctx.Locale.Tr "repo.pulls.cmd_instruction_merge_desc"}}
+
+

{{ctx.Locale.Tr "repo.pulls.cmd_instruction_merge_title"}}

+ {{ctx.Locale.Tr "repo.pulls.cmd_instruction_merge_desc"}} + {{if not .AutodetectManualMerge}} +
{{ctx.Locale.Tr "repo.pulls.cmd_instruction_merge_warning"}}
+ {{end}} +
git checkout {{.PullRequest.BaseBranch}}
From 32075d28803344230e6366e2a683b8d3f39b2433 Mon Sep 17 00:00:00 2001 From: silverwind Date: Sat, 10 Aug 2024 11:46:48 +0200 Subject: [PATCH 2/5] Add types to various low-level functions (#31781) Adds types to various low-level modules. All changes are type-only, no runtime changes. `tsc` now reports 38 less errors. One problem was that `@types/sortablejs` does not accept promise return in its functions which triggered the linter, so I disabled the rules on those line. --- package-lock.json | 22 ++++++-- package.json | 1 + types.d.ts | 9 ++++ web_src/js/features/dropzone.ts | 2 +- web_src/js/features/repo-issue-list.ts | 2 +- web_src/js/features/repo-projects.ts | 6 +-- web_src/js/features/stopwatch.ts | 2 +- web_src/js/modules/dirauto.ts | 14 +++-- web_src/js/modules/sortable.ts | 6 ++- web_src/js/modules/stores.ts | 3 +- web_src/js/modules/tippy.ts | 47 ++++++++-------- web_src/js/utils/color.ts | 11 ++-- web_src/js/utils/dom.ts | 75 +++++++++++++++----------- 13 files changed, 123 insertions(+), 77 deletions(-) diff --git a/package-lock.json b/package-lock.json index 846cf6f838..5c56531ec0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -108,6 +108,7 @@ "stylelint-declaration-strict-value": "1.10.6", "stylelint-value-no-unknown-custom-properties": "6.0.1", "svgo": "3.3.2", + "type-fest": "4.23.0", "updates": "16.3.7", "vite-string-plugin": "1.3.4", "vitest": "2.0.5" @@ -7439,6 +7440,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/globals/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/globby": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", @@ -12287,13 +12301,13 @@ } }, "node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.23.0.tgz", + "integrity": "sha512-ZiBujro2ohr5+Z/hZWHESLz3g08BBdrdLMieYFULJO+tWc437sn8kQsWLJoZErY8alNhxre9K4p3GURAG11n+w==", "dev": true, "license": "(MIT OR CC0-1.0)", "engines": { - "node": ">=10" + "node": ">=16" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" diff --git a/package.json b/package.json index 730c47f90d..d1a624f116 100644 --- a/package.json +++ b/package.json @@ -107,6 +107,7 @@ "stylelint-declaration-strict-value": "1.10.6", "stylelint-value-no-unknown-custom-properties": "6.0.1", "svgo": "3.3.2", + "type-fest": "4.23.0", "updates": "16.3.7", "vite-string-plugin": "1.3.4", "vitest": "2.0.5" diff --git a/types.d.ts b/types.d.ts index 3da7cbe050..a8dc09e064 100644 --- a/types.d.ts +++ b/types.d.ts @@ -3,6 +3,11 @@ declare module '*.svg' { export default value; } +declare module '*.css' { + const value: string; + export default value; +} + declare let __webpack_public_path__: string; interface Window { @@ -20,3 +25,7 @@ declare module 'htmx.org/dist/htmx.esm.js' { const value = await import('htmx.org'); export default value; } + +interface Element { + _tippy: import('tippy.js').Instance; +} diff --git a/web_src/js/features/dropzone.ts b/web_src/js/features/dropzone.ts index 392bc1db66..f652af0456 100644 --- a/web_src/js/features/dropzone.ts +++ b/web_src/js/features/dropzone.ts @@ -52,7 +52,7 @@ function addCopyLink(file) { copyLinkEl.addEventListener('click', async (e) => { e.preventDefault(); const success = await clippie(generateMarkdownLinkForAttachment(file)); - showTemporaryTooltip(e.target, success ? i18n.copy_success : i18n.copy_error); + showTemporaryTooltip(e.target as Element, success ? i18n.copy_success : i18n.copy_error); }); file.previewTemplate.append(copyLinkEl); } diff --git a/web_src/js/features/repo-issue-list.ts b/web_src/js/features/repo-issue-list.ts index 1e4a880f2e..134304617b 100644 --- a/web_src/js/features/repo-issue-list.ts +++ b/web_src/js/features/repo-issue-list.ts @@ -196,7 +196,7 @@ async function initIssuePinSort() { createSortable(pinDiv, { group: 'shared', - onEnd: pinMoveEnd, + onEnd: pinMoveEnd, // eslint-disable-line @typescript-eslint/no-misused-promises }); } diff --git a/web_src/js/features/repo-projects.ts b/web_src/js/features/repo-projects.ts index 950d78fec7..bc2bb69a33 100644 --- a/web_src/js/features/repo-projects.ts +++ b/web_src/js/features/repo-projects.ts @@ -60,7 +60,7 @@ async function initRepoProjectSortable() { handle: '.project-column-header', delayOnTouchOnly: true, delay: 500, - onSort: async () => { + onSort: async () => { // eslint-disable-line @typescript-eslint/no-misused-promises boardColumns = mainBoard.querySelectorAll('.project-column'); const columnSorting = { @@ -84,8 +84,8 @@ async function initRepoProjectSortable() { const boardCardList = boardColumn.querySelectorAll('.cards')[0]; createSortable(boardCardList, { group: 'shared', - onAdd: moveIssue, - onUpdate: moveIssue, + onAdd: moveIssue, // eslint-disable-line @typescript-eslint/no-misused-promises + onUpdate: moveIssue, // eslint-disable-line @typescript-eslint/no-misused-promises delayOnTouchOnly: true, delay: 500, }); diff --git a/web_src/js/features/stopwatch.ts b/web_src/js/features/stopwatch.ts index d89aa4bfac..af52be4e24 100644 --- a/web_src/js/features/stopwatch.ts +++ b/web_src/js/features/stopwatch.ts @@ -27,7 +27,7 @@ export function initStopwatch() { stopwatchEl.removeAttribute('href'); // intended for noscript mode only createTippy(stopwatchEl, { - content: stopwatchPopup.cloneNode(true), + content: stopwatchPopup.cloneNode(true) as Element, placement: 'bottom-end', trigger: 'click', maxWidth: 'none', diff --git a/web_src/js/modules/dirauto.ts b/web_src/js/modules/dirauto.ts index 855bae1ca8..db45a9cd17 100644 --- a/web_src/js/modules/dirauto.ts +++ b/web_src/js/modules/dirauto.ts @@ -1,7 +1,9 @@ import {isDocumentFragmentOrElementNode} from '../utils/dom.ts'; +type DirElement = HTMLInputElement | HTMLTextAreaElement; + // for performance considerations, it only uses performant syntax -function attachDirAuto(el) { +function attachDirAuto(el: DirElement) { if (el.type !== 'hidden' && el.type !== 'checkbox' && el.type !== 'radio' && @@ -18,10 +20,12 @@ export function initDirAuto() { const mutation = mutationList[i]; const len = mutation.addedNodes.length; for (let i = 0; i < len; i++) { - const addedNode = mutation.addedNodes[i]; + const addedNode = mutation.addedNodes[i] as HTMLElement; if (!isDocumentFragmentOrElementNode(addedNode)) continue; - if (addedNode.nodeName === 'INPUT' || addedNode.nodeName === 'TEXTAREA') attachDirAuto(addedNode); - const children = addedNode.querySelectorAll('input, textarea'); + if (addedNode.nodeName === 'INPUT' || addedNode.nodeName === 'TEXTAREA') { + attachDirAuto(addedNode as DirElement); + } + const children = addedNode.querySelectorAll('input, textarea'); const len = children.length; for (let childIdx = 0; childIdx < len; childIdx++) { attachDirAuto(children[childIdx]); @@ -30,7 +34,7 @@ export function initDirAuto() { } }); - const docNodes = document.querySelectorAll('input, textarea'); + const docNodes = document.querySelectorAll('input, textarea'); const len = docNodes.length; for (let i = 0; i < len; i++) { attachDirAuto(docNodes[i]); diff --git a/web_src/js/modules/sortable.ts b/web_src/js/modules/sortable.ts index 1c9adb6d72..460f4c6d91 100644 --- a/web_src/js/modules/sortable.ts +++ b/web_src/js/modules/sortable.ts @@ -1,4 +1,6 @@ -export async function createSortable(el, opts = {}) { +import type {SortableOptions} from 'sortablejs'; + +export async function createSortable(el, opts: {handle?: string} & SortableOptions = {}) { const {Sortable} = await import(/* webpackChunkName: "sortablejs" */'sortablejs'); return new Sortable(el, { @@ -15,5 +17,5 @@ export async function createSortable(el, opts = {}) { opts.onUnchoose?.(e); }, ...opts, - }); + } satisfies SortableOptions); } diff --git a/web_src/js/modules/stores.ts b/web_src/js/modules/stores.ts index 1a0ed7eda1..942a7bc508 100644 --- a/web_src/js/modules/stores.ts +++ b/web_src/js/modules/stores.ts @@ -1,6 +1,7 @@ import {reactive} from 'vue'; +import type {Reactive} from 'vue'; -let diffTreeStoreReactive; +let diffTreeStoreReactive: Reactive>; export function diffTreeStore() { if (!diffTreeStoreReactive) { diffTreeStoreReactive = reactive(window.config.pageData.diffFileInfo); diff --git a/web_src/js/modules/tippy.ts b/web_src/js/modules/tippy.ts index a18bad5db7..375d816c6b 100644 --- a/web_src/js/modules/tippy.ts +++ b/web_src/js/modules/tippy.ts @@ -1,16 +1,22 @@ import tippy, {followCursor} from 'tippy.js'; import {isDocumentFragmentOrElementNode} from '../utils/dom.ts'; import {formatDatetime} from '../utils/time.ts'; +import type {Content, Instance, Props} from 'tippy.js'; -const visibleInstances = new Set(); +type TippyOpts = { + role?: string, + theme?: 'default' | 'tooltip' | 'menu' | 'box-with-header' | 'bare', +} & Partial; + +const visibleInstances = new Set(); const arrowSvg = ``; -export function createTippy(target, opts = {}) { +export function createTippy(target: Element, opts: TippyOpts = {}) { // the callback functions should be destructured from opts, // because we should use our own wrapper functions to handle them, do not let the user override them const {onHide, onShow, onDestroy, role, theme, arrow, ...other} = opts; - const instance = tippy(target, { + const instance: Instance = tippy(target, { appendTo: document.body, animation: false, allowHTML: false, @@ -18,15 +24,15 @@ export function createTippy(target, opts = {}) { interactiveBorder: 20, ignoreAttributes: true, maxWidth: 500, // increase over default 350px - onHide: (instance) => { + onHide: (instance: Instance) => { visibleInstances.delete(instance); return onHide?.(instance); }, - onDestroy: (instance) => { + onDestroy: (instance: Instance) => { visibleInstances.delete(instance); return onDestroy?.(instance); }, - onShow: (instance) => { + onShow: (instance: Instance) => { // hide other tooltip instances so only one tooltip shows at a time for (const visibleInstance of visibleInstances) { if (visibleInstance.props.role === 'tooltip') { @@ -43,7 +49,7 @@ export function createTippy(target, opts = {}) { theme: theme || role || 'default', plugins: [followCursor], ...other, - }); + } satisfies Partial); if (role === 'menu') { target.setAttribute('aria-haspopup', 'true'); @@ -58,12 +64,8 @@ export function createTippy(target, opts = {}) { * If the target element has no content, then no tooltip will be attached, and it returns null. * * Note: "tooltip" doesn't equal to "tippy". "tooltip" means a auto-popup content, it just uses tippy as the implementation. - * - * @param target {HTMLElement} - * @param content {null|string} - * @returns {null|tippy} */ -function attachTooltip(target, content = null) { +function attachTooltip(target: Element, content: Content = null) { switchTitleToTooltip(target); content = content ?? target.getAttribute('data-tooltip-content'); @@ -84,7 +86,7 @@ function attachTooltip(target, content = null) { placement: target.getAttribute('data-tooltip-placement') || 'top-start', followCursor: target.getAttribute('data-tooltip-follow-cursor') || false, ...(target.getAttribute('data-tooltip-interactive') === 'true' ? {interactive: true, aria: {content: 'describedby', expanded: false}} : {}), - }; + } as TippyOpts; if (!target._tippy) { createTippy(target, props); @@ -94,7 +96,7 @@ function attachTooltip(target, content = null) { return target._tippy; } -function switchTitleToTooltip(target) { +function switchTitleToTooltip(target: Element) { let title = target.getAttribute('title'); if (title) { // apply custom formatting to relative-time's tooltips @@ -118,16 +120,15 @@ function switchTitleToTooltip(target) { * According to https://www.w3.org/TR/DOM-Level-3-Events/#events-mouseevent-event-order , mouseover event is fired before mouseenter event * Some browsers like PaleMoon don't support "addEventListener('mouseenter', capture)" * The tippy by default uses "mouseenter" event to show, so we use "mouseover" event to switch to tippy - * @param e {Event} */ -function lazyTooltipOnMouseHover(e) { +function lazyTooltipOnMouseHover(e: MouseEvent) { e.target.removeEventListener('mouseover', lazyTooltipOnMouseHover, true); attachTooltip(this); } // Activate the tooltip for current element. // If the element has no aria-label, use the tooltip content as aria-label. -function attachLazyTooltip(el) { +function attachLazyTooltip(el: Element) { el.addEventListener('mouseover', lazyTooltipOnMouseHover, {capture: true}); // meanwhile, if the element has no aria-label, use the tooltip content as aria-label @@ -140,15 +141,15 @@ function attachLazyTooltip(el) { } // Activate the tooltip for all children elements. -function attachChildrenLazyTooltip(target) { - for (const el of target.querySelectorAll('[data-tooltip-content]')) { +function attachChildrenLazyTooltip(target: Element) { + for (const el of target.querySelectorAll('[data-tooltip-content]')) { attachLazyTooltip(el); } } export function initGlobalTooltips() { // use MutationObserver to detect new "data-tooltip-content" elements added to the DOM, or attributes changed - const observerConnect = (observer) => observer.observe(document, { + const observerConnect = (observer: MutationObserver) => observer.observe(document, { subtree: true, childList: true, attributeFilter: ['data-tooltip-content', 'title'], @@ -159,7 +160,7 @@ export function initGlobalTooltips() { for (const mutation of [...mutationList, ...pending]) { if (mutation.type === 'childList') { // mainly for Vue components and AJAX rendered elements - for (const el of mutation.addedNodes) { + for (const el of mutation.addedNodes as NodeListOf) { if (!isDocumentFragmentOrElementNode(el)) continue; attachChildrenLazyTooltip(el); if (el.hasAttribute('data-tooltip-content')) { @@ -167,7 +168,7 @@ export function initGlobalTooltips() { } } } else if (mutation.type === 'attributes') { - attachTooltip(mutation.target); + attachTooltip(mutation.target as Element); } } observerConnect(observer); @@ -177,7 +178,7 @@ export function initGlobalTooltips() { attachChildrenLazyTooltip(document.documentElement); } -export function showTemporaryTooltip(target, content) { +export function showTemporaryTooltip(target: Element, content: Content) { // if the target is inside a dropdown, don't show the tooltip because when the dropdown // closes, the tippy would be pushed unsightly to the top-left of the screen like seen // on the issue comment menu. diff --git a/web_src/js/utils/color.ts b/web_src/js/utils/color.ts index 198f97c454..3ee32395fb 100644 --- a/web_src/js/utils/color.ts +++ b/web_src/js/utils/color.ts @@ -1,26 +1,27 @@ import tinycolor from 'tinycolor2'; +import type {ColorInput} from 'tinycolor2'; // Returns relative luminance for a SRGB color - https://en.wikipedia.org/wiki/Relative_luminance // Keep this in sync with modules/util/color.go -function getRelativeLuminance(color) { +function getRelativeLuminance(color: ColorInput) { const {r, g, b} = tinycolor(color).toRgb(); return (0.2126729 * r + 0.7151522 * g + 0.072175 * b) / 255; } -function useLightText(backgroundColor) { +function useLightText(backgroundColor: ColorInput) { return getRelativeLuminance(backgroundColor) < 0.453; } // Given a background color, returns a black or white foreground color that the highest // contrast ratio. In the future, the APCA contrast function, or CSS `contrast-color` will be better. // https://github.com/color-js/color.js/blob/eb7b53f7a13bb716ec8b28c7a56f052cd599acd9/src/contrast/APCA.js#L42 -export function contrastColor(backgroundColor) { +export function contrastColor(backgroundColor: ColorInput) { return useLightText(backgroundColor) ? '#fff' : '#000'; } -function resolveColors(obj) { +function resolveColors(obj: Record) { const styles = window.getComputedStyle(document.documentElement); - const getColor = (name) => styles.getPropertyValue(name).trim(); + const getColor = (name: string) => styles.getPropertyValue(name).trim(); return Object.fromEntries(Object.entries(obj).map(([key, value]) => [key, getColor(value)])); } diff --git a/web_src/js/utils/dom.ts b/web_src/js/utils/dom.ts index 82e7b755a5..5fc2183194 100644 --- a/web_src/js/utils/dom.ts +++ b/web_src/js/utils/dom.ts @@ -1,14 +1,21 @@ import {debounce} from 'throttle-debounce'; +import type {Promisable} from 'type-fest'; +import type $ from 'jquery'; -function elementsCall(el, func, ...args) { +type ElementArg = Element | string | NodeListOf | Array | ReturnType; +type ElementsCallback = (el: Element) => Promisable; +type ElementsCallbackWithArgs = (el: Element, ...args: any[]) => Promisable; +type IterableElements = NodeListOf | Array; + +function elementsCall(el: ElementArg, func: ElementsCallbackWithArgs, ...args: any[]) { if (typeof el === 'string' || el instanceof String) { - el = document.querySelectorAll(el); + el = document.querySelectorAll(el as string); } if (el instanceof Node) { func(el, ...args); } else if (el.length !== undefined) { // this works for: NodeList, HTMLCollection, Array, jQuery - for (const e of el) { + for (const e of (el as IterableElements)) { func(e, ...args); } } else { @@ -17,10 +24,10 @@ function elementsCall(el, func, ...args) { } /** - * @param el string (selector), Node, NodeList, HTMLCollection, Array or jQuery + * @param el Element * @param force force=true to show or force=false to hide, undefined to toggle */ -function toggleShown(el, force) { +function toggleShown(el: Element, force: boolean) { if (force === true) { el.classList.remove('tw-hidden'); } else if (force === false) { @@ -32,26 +39,26 @@ function toggleShown(el, force) { } } -export function showElem(el) { +export function showElem(el: ElementArg) { elementsCall(el, toggleShown, true); } -export function hideElem(el) { +export function hideElem(el: ElementArg) { elementsCall(el, toggleShown, false); } -export function toggleElem(el, force) { +export function toggleElem(el: ElementArg, force?: boolean) { elementsCall(el, toggleShown, force); } -export function isElemHidden(el) { - const res = []; +export function isElemHidden(el: ElementArg) { + const res: boolean[] = []; elementsCall(el, (e) => res.push(e.classList.contains('tw-hidden'))); if (res.length > 1) throw new Error(`isElemHidden doesn't work for multiple elements`); return res[0]; } -function applyElemsCallback(elems, fn) { +function applyElemsCallback(elems: IterableElements, fn?: ElementsCallback) { if (fn) { for (const el of elems) { fn(el); @@ -60,20 +67,22 @@ function applyElemsCallback(elems, fn) { return elems; } -export function queryElemSiblings(el, selector = '*', fn) { - return applyElemsCallback(Array.from(el.parentNode.children).filter((child) => child !== el && child.matches(selector)), fn); +export function queryElemSiblings(el: Element, selector = '*', fn?: ElementsCallback) { + return applyElemsCallback(Array.from(el.parentNode.children).filter((child: Element) => { + return child !== el && child.matches(selector); + }), fn); } // it works like jQuery.children: only the direct children are selected -export function queryElemChildren(parent, selector = '*', fn) { +export function queryElemChildren(parent: Element | ParentNode, selector = '*', fn?: ElementsCallback) { return applyElemsCallback(parent.querySelectorAll(`:scope > ${selector}`), fn); } -export function queryElems(selector, fn) { +export function queryElems(selector: string, fn?: ElementsCallback) { return applyElemsCallback(document.querySelectorAll(selector), fn); } -export function onDomReady(cb) { +export function onDomReady(cb: () => Promisable) { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', cb); } else { @@ -83,7 +92,7 @@ export function onDomReady(cb) { // checks whether an element is owned by the current document, and whether it is a document fragment or element node // if it is, it means it is a "normal" element managed by us, which can be modified safely. -export function isDocumentFragmentOrElementNode(el) { +export function isDocumentFragmentOrElementNode(el: Element) { try { return el.ownerDocument === document && el.nodeType === Node.ELEMENT_NODE || el.nodeType === Node.DOCUMENT_FRAGMENT_NODE; } catch { @@ -108,12 +117,15 @@ export function isDocumentFragmentOrElementNode(el) { // The above copyright notice and this permission notice shall be // included in all copies or substantial portions of the Software. // --------------------------------------------------------------------- -export function autosize(textarea, {viewportMarginBottom = 0} = {}) { +export function autosize(textarea: HTMLTextAreaElement, {viewportMarginBottom = 0}: {viewportMarginBottom?: number} = {}) { let isUserResized = false; // lastStyleHeight and initialStyleHeight are CSS values like '100px' - let lastMouseX, lastMouseY, lastStyleHeight, initialStyleHeight; + let lastMouseX: number; + let lastMouseY: number; + let lastStyleHeight: string; + let initialStyleHeight: string; - function onUserResize(event) { + function onUserResize(event: MouseEvent) { if (isUserResized) return; if (lastMouseX !== event.clientX || lastMouseY !== event.clientY) { const newStyleHeight = textarea.style.height; @@ -133,7 +145,7 @@ export function autosize(textarea, {viewportMarginBottom = 0} = {}) { while (el !== document.body && el !== null) { offsetTop += el.offsetTop || 0; - el = el.offsetParent; + el = el.offsetParent as HTMLTextAreaElement; } const top = offsetTop - document.defaultView.scrollY; @@ -213,14 +225,15 @@ export function autosize(textarea, {viewportMarginBottom = 0} = {}) { }; } -export function onInputDebounce(fn) { +export function onInputDebounce(fn: () => Promisable) { return debounce(300, fn); } +type LoadableElement = HTMLEmbedElement | HTMLIFrameElement | HTMLImageElement | HTMLScriptElement | HTMLTrackElement; + // Set the `src` attribute on an element and returns a promise that resolves once the element -// has loaded or errored. Suitable for all elements mention in: -// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/load_event -export function loadElem(el, src) { +// has loaded or errored. +export function loadElem(el: LoadableElement, src: string) { return new Promise((resolve) => { el.addEventListener('load', () => resolve(true), {once: true}); el.addEventListener('error', () => resolve(false), {once: true}); @@ -256,14 +269,14 @@ export function initSubmitEventPolyfill() { * @param {HTMLElement} element The element to check. * @returns {boolean} True if the element is visible. */ -export function isElemVisible(element) { +export function isElemVisible(element: HTMLElement) { if (!element) return false; return Boolean(element.offsetWidth || element.offsetHeight || element.getClientRects().length); } // replace selected text in a textarea while preserving editor history, e.g. CTRL-Z works after this -export function replaceTextareaSelection(textarea, text) { +export function replaceTextareaSelection(textarea: HTMLTextAreaElement, text: string) { const before = textarea.value.slice(0, textarea.selectionStart ?? undefined); const after = textarea.value.slice(textarea.selectionEnd ?? undefined); let success = true; @@ -287,13 +300,13 @@ export function replaceTextareaSelection(textarea, text) { } // Warning: Do not enter any unsanitized variables here -export function createElementFromHTML(htmlString) { +export function createElementFromHTML(htmlString: string) { const div = document.createElement('div'); div.innerHTML = htmlString.trim(); - return div.firstChild; + return div.firstChild as Element; } -export function createElementFromAttrs(tagName, attrs) { +export function createElementFromAttrs(tagName: string, attrs: Record) { const el = document.createElement(tagName); for (const [key, value] of Object.entries(attrs)) { if (value === undefined || value === null) continue; @@ -307,7 +320,7 @@ export function createElementFromAttrs(tagName, attrs) { return el; } -export function animateOnce(el, animationClassName) { +export function animateOnce(el: Element, animationClassName: string): Promise { return new Promise((resolve) => { el.addEventListener('animationend', function onAnimationEnd() { el.classList.remove(animationClassName); From ff1779d7cf6d4adb04d9692f63948e83bc7a9022 Mon Sep 17 00:00:00 2001 From: GiteaBot Date: Sun, 11 Aug 2024 00:31:26 +0000 Subject: [PATCH 3/5] [skip ci] Updated translations via Crowdin --- options/locale/locale_pt-PT.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/options/locale/locale_pt-PT.ini b/options/locale/locale_pt-PT.ini index 89cb776b69..5526a00fc3 100644 --- a/options/locale/locale_pt-PT.ini +++ b/options/locale/locale_pt-PT.ini @@ -1475,6 +1475,7 @@ issues.remove_labels=removeu os rótulos %s %s issues.add_remove_labels=adicionou o(s) rótulo(s) %s e removeu %s %s issues.add_milestone_at=`adicionou esta questão à etapa %s %s` issues.add_project_at=`adicionou esta questão ao planeamento %s %s` +issues.move_to_column_of_project=`isto foi movido para %s dentro de %s em %s` issues.change_milestone_at=`modificou a etapa de %s para %s %s` issues.change_project_at=`modificou o planeamento de %s para %s %s` issues.remove_milestone_at=`removeu esta questão da etapa %s %s` From e45a4c98292bf7c53700ff2f6f8e4dc7ba2e3e68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A1bio=20Barkoski?= <65479069+fabiobarkoski@users.noreply.github.com> Date: Sun, 11 Aug 2024 01:50:54 -0300 Subject: [PATCH 4/5] Move repository visibility to danger zone in the settings area (#31126) Moved repository visibility to the danger zone in the settings area. To change the visibility, it is necessary to go to the danger zone, click on the private/public button, and accept the change in the modal. Resolves: #23826 --- ## Screenshots
Before Private repo: ![Private repo](https://github.com/go-gitea/gitea/assets/65479069/4313492a-4854-48bc-9f47-974e3539d791) Public repo: ![Public repo](https://github.com/go-gitea/gitea/assets/65479069/1c45f6e4-ee93-4799-9331-e9d4a7e0f16a)
After Make private: ![Screenshot from 2024-05-28 21-35-38](https://github.com/go-gitea/gitea/assets/65479069/4887e28a-0514-4990-aa69-bf3ddc7e6c7d) Make private modal ![Screenshot from 2024-06-13 23-55-55](https://github.com/go-gitea/gitea/assets/65479069/9f5a7604-069b-41a2-973b-ee2d58e85953) ![Screenshot from 2024-06-13 23-53-09](https://github.com/go-gitea/gitea/assets/65479069/06c22726-eab2-4bce-8df7-62849dcce974) Make public: ![Screenshot from 2024-05-28 21-34-27](https://github.com/go-gitea/gitea/assets/65479069/6d388f99-0356-48a0-9d85-320cdba55179) Make public modal ![Screenshot from 2024-06-13 23-53-37](https://github.com/go-gitea/gitea/assets/65479069/8944972e-f2d4-4aea-ba96-b892febb5ced)
--------- Co-authored-by: Kemal Zebari <60799661+kemzeb@users.noreply.github.com> --- options/locale/locale_en-US.ini | 12 ++++++ routers/web/repo/setting/setting.go | 43 +++++++++++++++---- services/repository/repository.go | 25 +++++++++++ templates/repo/settings/options.tmpl | 63 +++++++++++++++++++++------- 4 files changed, 120 insertions(+), 23 deletions(-) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index 28b3df6c49..b30504edd7 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -2466,6 +2466,18 @@ settings.thread_id = Thread ID settings.matrix.homeserver_url = Homeserver URL settings.matrix.room_id = Room ID settings.matrix.message_type = Message Type +settings.visibility.private.button = Make Private +settings.visibility.private.text = Changing the visibility to private will not only make the repo visible to only allowed members but may remove the relation between it and forks, watchers, and stars. +settings.visibility.private.bullet_title = Changing the visibility to private will: +settings.visibility.private.bullet_one = Make the repo visible to only allowed members. +settings.visibility.private.bullet_two = May remove the relation between it and forks, watchers, and stars. +settings.visibility.public.button = Make Public +settings.visibility.public.text = Changing the visibility to public will make the repo visible to anyone. +settings.visibility.public.bullet_title= Changing the visibility to public will: +settings.visibility.public.bullet_one = Make the repo visible to anyone. +settings.visibility.success = Repository visibility changed. +settings.visibility.error = An error occurred while trying to change the repo visibility. +settings.visibility.fork_error = Can't change the visibility of a forked repo. settings.archive.button = Archive Repo settings.archive.header = Archive This Repo settings.archive.text = Archiving the repo will make it entirely read-only. It will be hidden from the dashboard. Nobody (not even you!) will be able to make new commits, or open any issues or pull requests. diff --git a/routers/web/repo/setting/setting.go b/routers/web/repo/setting/setting.go index 1e0349cdee..3f9140857a 100644 --- a/routers/web/repo/setting/setting.go +++ b/routers/web/repo/setting/setting.go @@ -170,15 +170,7 @@ func SettingsPost(ctx *context.Context) { form.Private = repo.BaseRepo.IsPrivate || repo.BaseRepo.Owner.Visibility == structs.VisibleTypePrivate } - visibilityChanged := repo.IsPrivate != form.Private - // when ForcePrivate enabled, you could change public repo to private, but only admin users can change private to public - if visibilityChanged && setting.Repository.ForcePrivate && !form.Private && !ctx.Doer.IsAdmin { - ctx.RenderWithErr(ctx.Tr("form.repository_force_private"), tplSettingsOptions, form) - return - } - - repo.IsPrivate = form.Private - if err := repo_service.UpdateRepository(ctx, repo, visibilityChanged); err != nil { + if err := repo_service.UpdateRepository(ctx, repo, false); err != nil { ctx.ServerError("UpdateRepository", err) return } @@ -940,6 +932,39 @@ func SettingsPost(ctx *context.Context) { log.Trace("Repository was un-archived: %s/%s", ctx.Repo.Owner.Name, repo.Name) ctx.Redirect(ctx.Repo.RepoLink + "/settings") + case "visibility": + if repo.IsFork { + ctx.Flash.Error(ctx.Tr("repo.settings.visibility.fork_error")) + ctx.Redirect(ctx.Repo.RepoLink + "/settings") + return + } + + var err error + + // when ForcePrivate enabled, you could change public repo to private, but only admin users can change private to public + if setting.Repository.ForcePrivate && repo.IsPrivate && !ctx.Doer.IsAdmin { + ctx.RenderWithErr(ctx.Tr("form.repository_force_private"), tplSettingsOptions, form) + return + } + + if repo.IsPrivate { + err = repo_service.MakeRepoPublic(ctx, repo) + } else { + err = repo_service.MakeRepoPrivate(ctx, repo) + } + + if err != nil { + log.Error("Tried to change the visibility of the repo: %s", err) + ctx.Flash.Error(ctx.Tr("repo.settings.visibility.error")) + ctx.Redirect(ctx.Repo.RepoLink + "/settings") + return + } + + ctx.Flash.Success(ctx.Tr("repo.settings.visibility.success")) + + log.Trace("Repository visibility changed: %s/%s", ctx.Repo.Owner.Name, repo.Name) + ctx.Redirect(ctx.Repo.RepoLink + "/settings") + default: ctx.NotFound("", nil) } diff --git a/services/repository/repository.go b/services/repository/repository.go index b7aac3cfe0..5306e7d45c 100644 --- a/services/repository/repository.go +++ b/services/repository/repository.go @@ -122,6 +122,31 @@ func UpdateRepository(ctx context.Context, repo *repo_model.Repository, visibili return committer.Commit() } +func UpdateRepositoryVisibility(ctx context.Context, repo *repo_model.Repository, isPrivate bool) (err error) { + ctx, committer, err := db.TxContext(ctx) + if err != nil { + return err + } + + defer committer.Close() + + repo.IsPrivate = isPrivate + + if err = repo_module.UpdateRepository(ctx, repo, true); err != nil { + return fmt.Errorf("UpdateRepositoryVisibility: %w", err) + } + + return committer.Commit() +} + +func MakeRepoPublic(ctx context.Context, repo *repo_model.Repository) (err error) { + return UpdateRepositoryVisibility(ctx, repo, false) +} + +func MakeRepoPrivate(ctx context.Context, repo *repo_model.Repository) (err error) { + return UpdateRepositoryVisibility(ctx, repo, true) +} + // LinkedRepository returns the linked repo if any func LinkedRepository(ctx context.Context, a *repo_model.Attachment) (*repo_model.Repository, unit.Type, error) { if a.IssueID != 0 { diff --git a/templates/repo/settings/options.tmpl b/templates/repo/settings/options.tmpl index 4f98133df3..f12bbbdf4a 100644 --- a/templates/repo/settings/options.tmpl +++ b/templates/repo/settings/options.tmpl @@ -23,20 +23,6 @@
- {{if not .Repository.IsFork}} -
- -
- {{if .IsAdmin}} - - {{else}} - - {{if and .Repository.IsPrivate $.ForcePrivate}}{{end}} - {{end}} - -
-
- {{end}}
@@ -786,6 +772,27 @@
+ {{if not .Repository.IsFork}} +
+
+
{{ctx.Locale.Tr "repo.visibility"}}
+ {{if .Repository.IsPrivate}} +
{{ctx.Locale.Tr "repo.settings.visibility.public.text"}}
+ {{else}} +
{{ctx.Locale.Tr "repo.settings.visibility.private.text"}}
+ {{end}} +
+
+ +
+
+ {{end}} {{if .Repository.IsMirror}}
@@ -1012,6 +1019,34 @@
+ {{if not .Repository.IsFork}} + + {{end}} + {{if .Repository.UnitEnabled $.Context ctx.Consts.RepoUnitTypeWiki}}