Fix remaining typescript issues, enable tsc (#32840)

Fixes 79 typescript errors. Discovered at least two bugs in
`notifications.ts`, and I'm pretty sure this feature was at least
partially broken and may still be, I don't really know how to test it.

After this, only like ~10 typescript errors remain in the codebase but
those are harder to solve.

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
silverwind 2024-12-15 22:02:32 +01:00 committed by GitHub
parent 74b06d4f5c
commit c8ea41b049
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
24 changed files with 152 additions and 134 deletions

View File

@ -377,12 +377,12 @@ lint-backend-fix: lint-go-fix lint-go-vet lint-editorconfig
.PHONY: lint-js .PHONY: lint-js
lint-js: node_modules lint-js: node_modules
npx eslint --color --max-warnings=0 --ext js,ts,vue $(ESLINT_FILES) npx eslint --color --max-warnings=0 --ext js,ts,vue $(ESLINT_FILES)
# npx vue-tsc npx vue-tsc
.PHONY: lint-js-fix .PHONY: lint-js-fix
lint-js-fix: node_modules lint-js-fix: node_modules
npx eslint --color --max-warnings=0 --ext js,ts,vue $(ESLINT_FILES) --fix npx eslint --color --max-warnings=0 --ext js,ts,vue $(ESLINT_FILES) --fix
# npx vue-tsc npx vue-tsc
.PHONY: lint-css .PHONY: lint-css
lint-css: node_modules lint-css: node_modules
@ -451,10 +451,6 @@ lint-templates: .venv node_modules
lint-yaml: .venv lint-yaml: .venv
@poetry run yamllint . @poetry run yamllint .
.PHONY: tsc
tsc:
npx vue-tsc
.PHONY: watch .PHONY: watch
watch: watch:
@bash tools/watch.sh @bash tools/watch.sh

62
package-lock.json generated
View File

@ -67,6 +67,7 @@
"devDependencies": { "devDependencies": {
"@eslint-community/eslint-plugin-eslint-comments": "4.4.1", "@eslint-community/eslint-plugin-eslint-comments": "4.4.1",
"@playwright/test": "1.49.0", "@playwright/test": "1.49.0",
"@silverwind/vue-tsc": "2.1.13",
"@stoplight/spectral-cli": "6.14.2", "@stoplight/spectral-cli": "6.14.2",
"@stylistic/eslint-plugin-js": "2.11.0", "@stylistic/eslint-plugin-js": "2.11.0",
"@stylistic/stylelint-plugin": "3.1.1", "@stylistic/stylelint-plugin": "3.1.1",
@ -111,8 +112,7 @@
"type-fest": "4.30.0", "type-fest": "4.30.0",
"updates": "16.4.0", "updates": "16.4.0",
"vite-string-plugin": "1.3.4", "vite-string-plugin": "1.3.4",
"vitest": "2.1.8", "vitest": "2.1.8"
"vue-tsc": "2.1.10"
}, },
"engines": { "engines": {
"node": ">= 18.0.0" "node": ">= 18.0.0"
@ -3833,6 +3833,24 @@
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/@silverwind/vue-tsc": {
"version": "2.1.13",
"resolved": "https://registry.npmjs.org/@silverwind/vue-tsc/-/vue-tsc-2.1.13.tgz",
"integrity": "sha512-ejFxz1KZiUGAESbC+eURnjqt0N95qkU9eZU7W15wgF9zV+v2FEu3ZLduuXTC7D/Sg6lL1R/QjPfUbxbAbBQOsw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@volar/typescript": "~2.4.11",
"@vue/language-core": "2.1.10",
"semver": "^7.5.4"
},
"bin": {
"vue-tsc": "bin/vue-tsc.js"
},
"peerDependencies": {
"typescript": ">=5.0.0"
}
},
"node_modules/@silverwind/vue3-calendar-heatmap": { "node_modules/@silverwind/vue3-calendar-heatmap": {
"version": "2.0.6", "version": "2.0.6",
"resolved": "https://registry.npmjs.org/@silverwind/vue3-calendar-heatmap/-/vue3-calendar-heatmap-2.0.6.tgz", "resolved": "https://registry.npmjs.org/@silverwind/vue3-calendar-heatmap/-/vue3-calendar-heatmap-2.0.6.tgz",
@ -5335,30 +5353,30 @@
} }
}, },
"node_modules/@volar/language-core": { "node_modules/@volar/language-core": {
"version": "2.4.10", "version": "2.4.11",
"resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.10.tgz", "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.11.tgz",
"integrity": "sha512-hG3Z13+nJmGaT+fnQzAkS0hjJRa2FCeqZt6Bd+oGNhUkQ+mTFsDETg5rqUTxyzIh5pSOGY7FHCWUS8G82AzLCA==", "integrity": "sha512-lN2C1+ByfW9/JRPpqScuZt/4OrUUse57GLI6TbLgTIqBVemdl1wNcZ1qYGEo2+Gw8coYLgCy7SuKqn6IrQcQgg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@volar/source-map": "2.4.10" "@volar/source-map": "2.4.11"
} }
}, },
"node_modules/@volar/source-map": { "node_modules/@volar/source-map": {
"version": "2.4.10", "version": "2.4.11",
"resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.10.tgz", "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.11.tgz",
"integrity": "sha512-OCV+b5ihV0RF3A7vEvNyHPi4G4kFa6ukPmyVocmqm5QzOd8r5yAtiNvaPEjl8dNvgC/lj4JPryeeHLdXd62rWA==", "integrity": "sha512-ZQpmafIGvaZMn/8iuvCFGrW3smeqkq/IIh9F1SdSx9aUl0J4Iurzd6/FhmjNO5g2ejF3rT45dKskgXWiofqlZQ==",
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@volar/typescript": { "node_modules/@volar/typescript": {
"version": "2.4.10", "version": "2.4.11",
"resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.10.tgz", "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.11.tgz",
"integrity": "sha512-F8ZtBMhSXyYKuBfGpYwqA5rsONnOwAVvjyE7KPYJ7wgZqo2roASqNWUnianOomJX5u1cxeRooHV59N0PhvEOgw==", "integrity": "sha512-2DT+Tdh88Spp5PyPbqhyoYavYCPDsqbHLFwcUI9K1NlY1YgUJvujGdrqUp0zWxnW7KWNTr3xSpMuv2WnaTKDAw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@volar/language-core": "2.4.10", "@volar/language-core": "2.4.11",
"path-browserify": "^1.0.1", "path-browserify": "^1.0.1",
"vscode-uri": "^3.0.8" "vscode-uri": "^3.0.8"
} }
@ -15780,24 +15798,6 @@
} }
} }
}, },
"node_modules/vue-tsc": {
"version": "2.1.10",
"resolved": "https://registry.npmjs.org/vue-tsc/-/vue-tsc-2.1.10.tgz",
"integrity": "sha512-RBNSfaaRHcN5uqVqJSZh++Gy/YUzryuv9u1aFWhsammDJXNtUiJMNoJ747lZcQ68wUQFx6E73y4FY3D8E7FGMA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@volar/typescript": "~2.4.8",
"@vue/language-core": "2.1.10",
"semver": "^7.5.4"
},
"bin": {
"vue-tsc": "bin/vue-tsc.js"
},
"peerDependencies": {
"typescript": ">=5.0.0"
}
},
"node_modules/watchpack": { "node_modules/watchpack": {
"version": "2.4.2", "version": "2.4.2",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz",

View File

@ -66,6 +66,7 @@
"devDependencies": { "devDependencies": {
"@eslint-community/eslint-plugin-eslint-comments": "4.4.1", "@eslint-community/eslint-plugin-eslint-comments": "4.4.1",
"@playwright/test": "1.49.0", "@playwright/test": "1.49.0",
"@silverwind/vue-tsc": "2.1.13",
"@stoplight/spectral-cli": "6.14.2", "@stoplight/spectral-cli": "6.14.2",
"@stylistic/eslint-plugin-js": "2.11.0", "@stylistic/eslint-plugin-js": "2.11.0",
"@stylistic/stylelint-plugin": "3.1.1", "@stylistic/stylelint-plugin": "3.1.1",
@ -110,8 +111,7 @@
"type-fest": "4.30.0", "type-fest": "4.30.0",
"updates": "16.4.0", "updates": "16.4.0",
"vite-string-plugin": "1.3.4", "vite-string-plugin": "1.3.4",
"vitest": "2.1.8", "vitest": "2.1.8"
"vue-tsc": "2.1.10"
}, },
"browserslist": [ "browserslist": [
"defaults" "defaults"

View File

@ -7,7 +7,8 @@
], ],
"compilerOptions": { "compilerOptions": {
"target": "es2020", "target": "es2020",
"module": "nodenext", "module": "esnext",
"moduleResolution": "bundler",
"lib": ["dom", "dom.iterable", "dom.asynciterable", "esnext"], "lib": ["dom", "dom.iterable", "dom.asynciterable", "esnext"],
"allowImportingTsExtensions": true, "allowImportingTsExtensions": true,
"allowJs": true, "allowJs": true,

View File

@ -7,7 +7,7 @@ const reIssueSharpIndex = /^#(\d+)$/; // eg: "#123"
const reIssueOwnerRepoIndex = /^([-.\w]+)\/([-.\w]+)#(\d+)$/; // eg: "{owner}/{repo}#{index}" const reIssueOwnerRepoIndex = /^([-.\w]+)\/([-.\w]+)#(\d+)$/; // eg: "{owner}/{repo}#{index}"
// if the searchText can be parsed to an "issue goto link", return the link, otherwise return empty string // if the searchText can be parsed to an "issue goto link", return the link, otherwise return empty string
export function parseIssueListQuickGotoLink(repoLink, searchText) { export function parseIssueListQuickGotoLink(repoLink: string, searchText: string) {
searchText = searchText.trim(); searchText = searchText.trim();
let targetUrl = ''; let targetUrl = '';
if (repoLink) { if (repoLink) {
@ -15,13 +15,12 @@ export function parseIssueListQuickGotoLink(repoLink, searchText) {
if (reIssueIndex.test(searchText)) { if (reIssueIndex.test(searchText)) {
targetUrl = `${repoLink}/issues/${searchText}`; targetUrl = `${repoLink}/issues/${searchText}`;
} else if (reIssueSharpIndex.test(searchText)) { } else if (reIssueSharpIndex.test(searchText)) {
targetUrl = `${repoLink}/issues/${searchText.substr(1)}`; targetUrl = `${repoLink}/issues/${searchText.substring(1)}`;
} }
} else { } else {
// try to parse it for a global search (eg: "owner/repo#123") // try to parse it for a global search (eg: "owner/repo#123")
const matchIssueOwnerRepoIndex = searchText.match(reIssueOwnerRepoIndex); const [_, owner, repo, index] = reIssueOwnerRepoIndex.exec(searchText) || [];
if (matchIssueOwnerRepoIndex) { if (owner) {
const [_, owner, repo, index] = matchIssueOwnerRepoIndex;
targetUrl = `${appSubUrl}/${owner}/${repo}/issues/${index}`; targetUrl = `${appSubUrl}/${owner}/${repo}/issues/${index}`;
} }
} }
@ -33,7 +32,7 @@ export function initCommonIssueListQuickGoto() {
if (!goto) return; if (!goto) return;
const form = goto.closest('form'); const form = goto.closest('form');
const input = form.querySelector('input[name=q]'); const input = form.querySelector<HTMLInputElement>('input[name=q]');
const repoLink = goto.getAttribute('data-repo-link'); const repoLink = goto.getAttribute('data-repo-link');
form.addEventListener('submit', (e) => { form.addEventListener('submit', (e) => {

View File

@ -283,8 +283,8 @@ export class ComboMarkdownEditor {
]; ];
} }
parseEasyMDEToolbar(EasyMDE, actions) { parseEasyMDEToolbar(easyMde: typeof EasyMDE, actions) {
this.easyMDEToolbarActions = this.easyMDEToolbarActions || easyMDEToolbarActions(EasyMDE, this); this.easyMDEToolbarActions = this.easyMDEToolbarActions || easyMDEToolbarActions(easyMde, this);
const processed = []; const processed = [];
for (const action of actions) { for (const action of actions) {
const actionButton = this.easyMDEToolbarActions[action]; const actionButton = this.easyMDEToolbarActions[action];

View File

@ -1,100 +1,102 @@
import {svg} from '../../svg.ts'; import {svg} from '../../svg.ts';
import type EasyMDE from 'easymde';
import type {ComboMarkdownEditor} from './ComboMarkdownEditor.ts';
export function easyMDEToolbarActions(EasyMDE, editor) { export function easyMDEToolbarActions(easyMde: typeof EasyMDE, editor: ComboMarkdownEditor): Record<string, Partial<EasyMDE.ToolbarIcon | string>> {
const actions = { const actions: Record<string, Partial<EasyMDE.ToolbarIcon> | string> = {
'|': '|', '|': '|',
'heading-1': { 'heading-1': {
action: EasyMDE.toggleHeading1, action: easyMde.toggleHeading1,
icon: svg('octicon-heading'), icon: svg('octicon-heading'),
title: 'Heading 1', title: 'Heading 1',
}, },
'heading-2': { 'heading-2': {
action: EasyMDE.toggleHeading2, action: easyMde.toggleHeading2,
icon: svg('octicon-heading'), icon: svg('octicon-heading'),
title: 'Heading 2', title: 'Heading 2',
}, },
'heading-3': { 'heading-3': {
action: EasyMDE.toggleHeading3, action: easyMde.toggleHeading3,
icon: svg('octicon-heading'), icon: svg('octicon-heading'),
title: 'Heading 3', title: 'Heading 3',
}, },
'heading-smaller': { 'heading-smaller': {
action: EasyMDE.toggleHeadingSmaller, action: easyMde.toggleHeadingSmaller,
icon: svg('octicon-heading'), icon: svg('octicon-heading'),
title: 'Decrease Heading', title: 'Decrease Heading',
}, },
'heading-bigger': { 'heading-bigger': {
action: EasyMDE.toggleHeadingBigger, action: easyMde.toggleHeadingBigger,
icon: svg('octicon-heading'), icon: svg('octicon-heading'),
title: 'Increase Heading', title: 'Increase Heading',
}, },
'bold': { 'bold': {
action: EasyMDE.toggleBold, action: easyMde.toggleBold,
icon: svg('octicon-bold'), icon: svg('octicon-bold'),
title: 'Bold', title: 'Bold',
}, },
'italic': { 'italic': {
action: EasyMDE.toggleItalic, action: easyMde.toggleItalic,
icon: svg('octicon-italic'), icon: svg('octicon-italic'),
title: 'Italic', title: 'Italic',
}, },
'strikethrough': { 'strikethrough': {
action: EasyMDE.toggleStrikethrough, action: easyMde.toggleStrikethrough,
icon: svg('octicon-strikethrough'), icon: svg('octicon-strikethrough'),
title: 'Strikethrough', title: 'Strikethrough',
}, },
'quote': { 'quote': {
action: EasyMDE.toggleBlockquote, action: easyMde.toggleBlockquote,
icon: svg('octicon-quote'), icon: svg('octicon-quote'),
title: 'Quote', title: 'Quote',
}, },
'code': { 'code': {
action: EasyMDE.toggleCodeBlock, action: easyMde.toggleCodeBlock,
icon: svg('octicon-code'), icon: svg('octicon-code'),
title: 'Code', title: 'Code',
}, },
'link': { 'link': {
action: EasyMDE.drawLink, action: easyMde.drawLink,
icon: svg('octicon-link'), icon: svg('octicon-link'),
title: 'Link', title: 'Link',
}, },
'unordered-list': { 'unordered-list': {
action: EasyMDE.toggleUnorderedList, action: easyMde.toggleUnorderedList,
icon: svg('octicon-list-unordered'), icon: svg('octicon-list-unordered'),
title: 'Unordered List', title: 'Unordered List',
}, },
'ordered-list': { 'ordered-list': {
action: EasyMDE.toggleOrderedList, action: easyMde.toggleOrderedList,
icon: svg('octicon-list-ordered'), icon: svg('octicon-list-ordered'),
title: 'Ordered List', title: 'Ordered List',
}, },
'image': { 'image': {
action: EasyMDE.drawImage, action: easyMde.drawImage,
icon: svg('octicon-image'), icon: svg('octicon-image'),
title: 'Image', title: 'Image',
}, },
'table': { 'table': {
action: EasyMDE.drawTable, action: easyMde.drawTable,
icon: svg('octicon-table'), icon: svg('octicon-table'),
title: 'Table', title: 'Table',
}, },
'horizontal-rule': { 'horizontal-rule': {
action: EasyMDE.drawHorizontalRule, action: easyMde.drawHorizontalRule,
icon: svg('octicon-horizontal-rule'), icon: svg('octicon-horizontal-rule'),
title: 'Horizontal Rule', title: 'Horizontal Rule',
}, },
'preview': { 'preview': {
action: EasyMDE.togglePreview, action: easyMde.togglePreview,
icon: svg('octicon-eye'), icon: svg('octicon-eye'),
title: 'Preview', title: 'Preview',
}, },
'fullscreen': { 'fullscreen': {
action: EasyMDE.toggleFullScreen, action: easyMde.toggleFullScreen,
icon: svg('octicon-screen-full'), icon: svg('octicon-screen-full'),
title: 'Fullscreen', title: 'Fullscreen',
}, },
'side-by-side': { 'side-by-side': {
action: EasyMDE.toggleSideBySide, action: easyMde.toggleSideBySide,
icon: svg('octicon-columns'), icon: svg('octicon-columns'),
title: 'Side by Side', title: 'Side by Side',
}, },

View File

@ -3,7 +3,7 @@ import {fomanticQuery} from '../../modules/fomantic/base.ts';
export function initCompReactionSelector(parent: ParentNode = document) { export function initCompReactionSelector(parent: ParentNode = document) {
for (const container of parent.querySelectorAll('.issue-content, .diff-file-body')) { for (const container of parent.querySelectorAll('.issue-content, .diff-file-body')) {
container.addEventListener('click', async (e) => { container.addEventListener('click', async (e: MouseEvent & {target: HTMLElement}) => {
// there are 2 places for the "reaction" buttons, one is the top-right reaction menu, one is the bottom of the comment // there are 2 places for the "reaction" buttons, one is the top-right reaction menu, one is the bottom of the comment
const target = e.target.closest('.comment-reaction-button'); const target = e.target.closest('.comment-reaction-button');
if (!target) return; if (!target) return;

View File

@ -23,7 +23,7 @@ export function initCompWebHookEditor() {
} }
// some webhooks (like Gitea) allow to set the request method (GET/POST), and it would toggle the "Content Type" field // some webhooks (like Gitea) allow to set the request method (GET/POST), and it would toggle the "Content Type" field
const httpMethodInput = document.querySelector('#http_method'); const httpMethodInput = document.querySelector<HTMLInputElement>('#http_method');
if (httpMethodInput) { if (httpMethodInput) {
const updateContentType = function () { const updateContentType = function () {
const visible = httpMethodInput.value === 'POST'; const visible = httpMethodInput.value === 'POST';

View File

@ -6,6 +6,7 @@ import {GET, POST} from '../modules/fetch.ts';
import {showErrorToast} from '../modules/toast.ts'; import {showErrorToast} from '../modules/toast.ts';
import {createElementFromHTML, createElementFromAttrs} from '../utils/dom.ts'; import {createElementFromHTML, createElementFromAttrs} from '../utils/dom.ts';
import {isImageFile, isVideoFile} from '../utils.ts'; import {isImageFile, isVideoFile} from '../utils.ts';
import type {DropzoneFile} from 'dropzone/index.js';
const {csrfToken, i18n} = window.config; const {csrfToken, i18n} = window.config;
@ -15,14 +16,14 @@ export const DropzoneCustomEventRemovedFile = 'dropzone-custom-removed-file';
export const DropzoneCustomEventUploadDone = 'dropzone-custom-upload-done'; export const DropzoneCustomEventUploadDone = 'dropzone-custom-upload-done';
async function createDropzone(el, opts) { async function createDropzone(el, opts) {
const [{Dropzone}] = await Promise.all([ const [{default: Dropzone}] = await Promise.all([
import(/* webpackChunkName: "dropzone" */'dropzone'), import(/* webpackChunkName: "dropzone" */'dropzone'),
import(/* webpackChunkName: "dropzone" */'dropzone/dist/dropzone.css'), import(/* webpackChunkName: "dropzone" */'dropzone/dist/dropzone.css'),
]); ]);
return new Dropzone(el, opts); return new Dropzone(el, opts);
} }
export function generateMarkdownLinkForAttachment(file, {width, dppx} = {}) { export function generateMarkdownLinkForAttachment(file, {width, dppx}: {width?: number, dppx?: number} = {}) {
let fileMarkdown = `[${file.name}](/attachments/${file.uuid})`; let fileMarkdown = `[${file.name}](/attachments/${file.uuid})`;
if (isImageFile(file)) { if (isImageFile(file)) {
fileMarkdown = `!${fileMarkdown}`; fileMarkdown = `!${fileMarkdown}`;
@ -60,14 +61,14 @@ function addCopyLink(file) {
/** /**
* @param {HTMLElement} dropzoneEl * @param {HTMLElement} dropzoneEl
*/ */
export async function initDropzone(dropzoneEl) { export async function initDropzone(dropzoneEl: HTMLElement) {
const listAttachmentsUrl = dropzoneEl.closest('[data-attachment-url]')?.getAttribute('data-attachment-url'); const listAttachmentsUrl = dropzoneEl.closest('[data-attachment-url]')?.getAttribute('data-attachment-url');
const removeAttachmentUrl = dropzoneEl.getAttribute('data-remove-url'); const removeAttachmentUrl = dropzoneEl.getAttribute('data-remove-url');
const attachmentBaseLinkUrl = dropzoneEl.getAttribute('data-link-url'); const attachmentBaseLinkUrl = dropzoneEl.getAttribute('data-link-url');
let disableRemovedfileEvent = false; // when resetting the dropzone (removeAllFiles), disable the "removedfile" event let disableRemovedfileEvent = false; // when resetting the dropzone (removeAllFiles), disable the "removedfile" event
let fileUuidDict = {}; // to record: if a comment has been saved, then the uploaded files won't be deleted from server when clicking the Remove in the dropzone let fileUuidDict = {}; // to record: if a comment has been saved, then the uploaded files won't be deleted from server when clicking the Remove in the dropzone
const opts = { const opts: Record<string, any> = {
url: dropzoneEl.getAttribute('data-upload-url'), url: dropzoneEl.getAttribute('data-upload-url'),
headers: {'X-Csrf-Token': csrfToken}, headers: {'X-Csrf-Token': csrfToken},
acceptedFiles: ['*/*', ''].includes(dropzoneEl.getAttribute('data-accepts')) ? null : dropzoneEl.getAttribute('data-accepts'), acceptedFiles: ['*/*', ''].includes(dropzoneEl.getAttribute('data-accepts')) ? null : dropzoneEl.getAttribute('data-accepts'),
@ -88,7 +89,7 @@ export async function initDropzone(dropzoneEl) {
// "http://localhost:3000/owner/repo/issues/[object%20Event]" // "http://localhost:3000/owner/repo/issues/[object%20Event]"
// the reason is that the preview "callback(dataURL)" is assign to "img.onerror" then "thumbnail" uses the error object as the dataURL and generates '<img src="[object Event]">' // the reason is that the preview "callback(dataURL)" is assign to "img.onerror" then "thumbnail" uses the error object as the dataURL and generates '<img src="[object Event]">'
const dzInst = await createDropzone(dropzoneEl, opts); const dzInst = await createDropzone(dropzoneEl, opts);
dzInst.on('success', (file, resp) => { dzInst.on('success', (file: DropzoneFile & {uuid: string}, resp: any) => {
file.uuid = resp.uuid; file.uuid = resp.uuid;
fileUuidDict[file.uuid] = {submitted: false}; fileUuidDict[file.uuid] = {submitted: false};
const input = createElementFromAttrs('input', {name: 'files', type: 'hidden', id: `dropzone-file-${resp.uuid}`, value: resp.uuid}); const input = createElementFromAttrs('input', {name: 'files', type: 'hidden', id: `dropzone-file-${resp.uuid}`, value: resp.uuid});
@ -97,7 +98,7 @@ export async function initDropzone(dropzoneEl) {
dzInst.emit(DropzoneCustomEventUploadDone, {file}); dzInst.emit(DropzoneCustomEventUploadDone, {file});
}); });
dzInst.on('removedfile', async (file) => { dzInst.on('removedfile', async (file: DropzoneFile & {uuid: string}) => {
if (disableRemovedfileEvent) return; if (disableRemovedfileEvent) return;
dzInst.emit(DropzoneCustomEventRemovedFile, {fileUuid: file.uuid}); dzInst.emit(DropzoneCustomEventRemovedFile, {fileUuid: file.uuid});

View File

@ -1,4 +1,4 @@
import emojis from '../../../assets/emoji.json'; import emojis from '../../../assets/emoji.json' with {type: 'json'};
const {assetUrlPrefix, customEmojis} = window.config; const {assetUrlPrefix, customEmojis} = window.config;

View File

@ -2,6 +2,11 @@ const sourcesByUrl = {};
const sourcesByPort = {}; const sourcesByPort = {};
class Source { class Source {
url: string;
eventSource: EventSource;
listening: Record<string, any>;
clients: Array<any>;
constructor(url) { constructor(url) {
this.url = url; this.url = url;
this.eventSource = new EventSource(url); this.eventSource = new EventSource(url);
@ -67,7 +72,7 @@ class Source {
} }
} }
self.addEventListener('connect', (e) => { self.addEventListener('connect', (e: Event & {ports: Array<any>}) => {
for (const port of e.ports) { for (const port of e.ports) {
port.addEventListener('message', (event) => { port.addEventListener('message', (event) => {
if (!self.EventSource) { if (!self.EventSource) {

View File

@ -21,8 +21,8 @@ export function initHeatmap() {
// last heatmap tooltip localization attempt https://github.com/go-gitea/gitea/pull/24131/commits/a83761cbbae3c2e3b4bced71e680f44432073ac8 // last heatmap tooltip localization attempt https://github.com/go-gitea/gitea/pull/24131/commits/a83761cbbae3c2e3b4bced71e680f44432073ac8
const locale = { const locale = {
heatMapLocale: { heatMapLocale: {
months: new Array(12).fill().map((_, idx) => translateMonth(idx)), months: new Array(12).fill(undefined).map((_, idx) => translateMonth(idx)),
days: new Array(7).fill().map((_, idx) => translateDay(idx)), days: new Array(7).fill(undefined).map((_, idx) => translateDay(idx)),
on: ' - ', // no correct locale support for it, because in many languages the sentence is not "something on someday" on: ' - ', // no correct locale support for it, because in many languages the sentence is not "something on someday"
more: el.getAttribute('data-locale-more'), more: el.getAttribute('data-locale-more'),
less: el.getAttribute('data-locale-less'), less: el.getAttribute('data-locale-less'),

View File

@ -22,9 +22,9 @@ function initPreInstall() {
mssql: '127.0.0.1:1433', mssql: '127.0.0.1:1433',
}; };
const dbHost = document.querySelector('#db_host'); const dbHost = document.querySelector<HTMLInputElement>('#db_host');
const dbUser = document.querySelector('#db_user'); const dbUser = document.querySelector<HTMLInputElement>('#db_user');
const dbName = document.querySelector('#db_name'); const dbName = document.querySelector<HTMLInputElement>('#db_name');
// Database type change detection. // Database type change detection.
document.querySelector('#db_type').addEventListener('change', function () { document.querySelector('#db_type').addEventListener('change', function () {
@ -48,12 +48,12 @@ function initPreInstall() {
}); });
document.querySelector('#db_type').dispatchEvent(new Event('change')); document.querySelector('#db_type').dispatchEvent(new Event('change'));
const appUrl = document.querySelector('#app_url'); const appUrl = document.querySelector<HTMLInputElement>('#app_url');
if (appUrl.value.includes('://localhost')) { if (appUrl.value.includes('://localhost')) {
appUrl.value = window.location.href; appUrl.value = window.location.href;
} }
const domain = document.querySelector('#domain'); const domain = document.querySelector<HTMLInputElement>('#domain');
if (domain.value.trim() === 'localhost') { if (domain.value.trim() === 'localhost') {
domain.value = window.location.hostname; domain.value = window.location.hostname;
} }
@ -61,43 +61,43 @@ function initPreInstall() {
// TODO: better handling of exclusive relations. // TODO: better handling of exclusive relations.
document.querySelector('#offline-mode input').addEventListener('change', function () { document.querySelector('#offline-mode input').addEventListener('change', function () {
if (this.checked) { if (this.checked) {
document.querySelector('#disable-gravatar input').checked = true; document.querySelector<HTMLInputElement>('#disable-gravatar input').checked = true;
document.querySelector('#federated-avatar-lookup input').checked = false; document.querySelector<HTMLInputElement>('#federated-avatar-lookup input').checked = false;
} }
}); });
document.querySelector('#disable-gravatar input').addEventListener('change', function () { document.querySelector('#disable-gravatar input').addEventListener('change', function () {
if (this.checked) { if (this.checked) {
document.querySelector('#federated-avatar-lookup input').checked = false; document.querySelector<HTMLInputElement>('#federated-avatar-lookup input').checked = false;
} else { } else {
document.querySelector('#offline-mode input').checked = false; document.querySelector<HTMLInputElement>('#offline-mode input').checked = false;
} }
}); });
document.querySelector('#federated-avatar-lookup input').addEventListener('change', function () { document.querySelector('#federated-avatar-lookup input').addEventListener('change', function () {
if (this.checked) { if (this.checked) {
document.querySelector('#disable-gravatar input').checked = false; document.querySelector<HTMLInputElement>('#disable-gravatar input').checked = false;
document.querySelector('#offline-mode input').checked = false; document.querySelector<HTMLInputElement>('#offline-mode input').checked = false;
} }
}); });
document.querySelector('#enable-openid-signin input').addEventListener('change', function () { document.querySelector('#enable-openid-signin input').addEventListener('change', function () {
if (this.checked) { if (this.checked) {
if (!document.querySelector('#disable-registration input').checked) { if (!document.querySelector<HTMLInputElement>('#disable-registration input').checked) {
document.querySelector('#enable-openid-signup input').checked = true; document.querySelector<HTMLInputElement>('#enable-openid-signup input').checked = true;
} }
} else { } else {
document.querySelector('#enable-openid-signup input').checked = false; document.querySelector<HTMLInputElement>('#enable-openid-signup input').checked = false;
} }
}); });
document.querySelector('#disable-registration input').addEventListener('change', function () { document.querySelector('#disable-registration input').addEventListener('change', function () {
if (this.checked) { if (this.checked) {
document.querySelector('#enable-captcha input').checked = false; document.querySelector<HTMLInputElement>('#enable-captcha input').checked = false;
document.querySelector('#enable-openid-signup input').checked = false; document.querySelector<HTMLInputElement>('#enable-openid-signup input').checked = false;
} else { } else {
document.querySelector('#enable-openid-signup input').checked = true; document.querySelector<HTMLInputElement>('#enable-openid-signup input').checked = true;
} }
}); });
document.querySelector('#enable-captcha input').addEventListener('change', function () { document.querySelector('#enable-captcha input').addEventListener('change', function () {
if (this.checked) { if (this.checked) {
document.querySelector('#disable-registration input').checked = false; document.querySelector<HTMLInputElement>('#disable-registration input').checked = false;
} }
}); });
} }

View File

@ -14,25 +14,25 @@ export function initNotificationsTable() {
window.addEventListener('pageshow', (e) => { window.addEventListener('pageshow', (e) => {
if (e.persisted) { // page was restored from bfcache if (e.persisted) { // page was restored from bfcache
const table = document.querySelector('#notification_table'); const table = document.querySelector('#notification_table');
const unreadCountEl = document.querySelector('.notifications-unread-count'); const unreadCountEl = document.querySelector<HTMLElement>('.notifications-unread-count');
let unreadCount = parseInt(unreadCountEl.textContent); let unreadCount = parseInt(unreadCountEl.textContent);
for (const item of table.querySelectorAll('.notifications-item[data-remove="true"]')) { for (const item of table.querySelectorAll('.notifications-item[data-remove="true"]')) {
item.remove(); item.remove();
unreadCount -= 1; unreadCount -= 1;
} }
unreadCountEl.textContent = unreadCount; unreadCountEl.textContent = String(unreadCount);
} }
}); });
// mark clicked unread links for deletion on bfcache restore // mark clicked unread links for deletion on bfcache restore
for (const link of table.querySelectorAll('.notifications-item[data-status="1"] .notifications-link')) { for (const link of table.querySelectorAll('.notifications-item[data-status="1"] .notifications-link')) {
link.addEventListener('click', (e) => { link.addEventListener('click', (e : MouseEvent & {target: HTMLElement}) => {
e.target.closest('.notifications-item').setAttribute('data-remove', 'true'); e.target.closest('.notifications-item').setAttribute('data-remove', 'true');
}); });
} }
} }
async function receiveUpdateCount(event) { async function receiveUpdateCount(event: MessageEvent) {
try { try {
const data = JSON.parse(event.data); const data = JSON.parse(event.data);
@ -50,7 +50,7 @@ export function initNotificationCount() {
if (!document.querySelector('.notification_count')) return; if (!document.querySelector('.notification_count')) return;
let usingPeriodicPoller = false; let usingPeriodicPoller = false;
const startPeriodicPoller = (timeout, lastCount) => { const startPeriodicPoller = (timeout: number, lastCount?: number) => {
if (timeout <= 0 || !Number.isFinite(timeout)) return; if (timeout <= 0 || !Number.isFinite(timeout)) return;
usingPeriodicPoller = true; usingPeriodicPoller = true;
lastCount = lastCount ?? getCurrentCount(); lastCount = lastCount ?? getCurrentCount();
@ -72,13 +72,13 @@ export function initNotificationCount() {
type: 'start', type: 'start',
url: `${window.location.origin}${appSubUrl}/user/events`, url: `${window.location.origin}${appSubUrl}/user/events`,
}); });
worker.port.addEventListener('message', (event) => { worker.port.addEventListener('message', (event: MessageEvent) => {
if (!event.data || !event.data.type) { if (!event.data || !event.data.type) {
console.error('unknown worker message event', event); console.error('unknown worker message event', event);
return; return;
} }
if (event.data.type === 'notification-count') { if (event.data.type === 'notification-count') {
const _promise = receiveUpdateCount(event.data); receiveUpdateCount(event); // no await
} else if (event.data.type === 'no-event-source') { } else if (event.data.type === 'no-event-source') {
// browser doesn't support EventSource, falling back to periodic poller // browser doesn't support EventSource, falling back to periodic poller
if (!usingPeriodicPoller) startPeriodicPoller(notificationSettings.MinTimeout); if (!usingPeriodicPoller) startPeriodicPoller(notificationSettings.MinTimeout);
@ -118,10 +118,10 @@ export function initNotificationCount() {
} }
function getCurrentCount() { function getCurrentCount() {
return document.querySelector('.notification_count').textContent; return Number(document.querySelector('.notification_count').textContent ?? '0');
} }
async function updateNotificationCountWithCallback(callback, timeout, lastCount) { async function updateNotificationCountWithCallback(callback: (timeout: number, newCount: number) => void, timeout: number, lastCount: number) {
const currentCount = getCurrentCount(); const currentCount = getCurrentCount();
if (lastCount !== currentCount) { if (lastCount !== currentCount) {
callback(notificationSettings.MinTimeout, currentCount); callback(notificationSettings.MinTimeout, currentCount);
@ -149,10 +149,9 @@ async function updateNotificationTable() {
if (notificationDiv) { if (notificationDiv) {
try { try {
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
params.set('div-only', true); params.set('div-only', String(true));
params.set('sequence-number', ++notificationSequenceNumber); params.set('sequence-number', String(++notificationSequenceNumber));
const url = `${appSubUrl}/notifications?${params.toString()}`; const response = await GET(`${appSubUrl}/notifications?${params.toString()}`);
const response = await GET(url);
if (!response.ok) { if (!response.ok) {
throw new Error('Failed to fetch notification table'); throw new Error('Failed to fetch notification table');
@ -169,7 +168,7 @@ async function updateNotificationTable() {
} }
} }
async function updateNotificationCount() { async function updateNotificationCount(): Promise<number> {
try { try {
const response = await GET(`${appSubUrl}/notifications/new`); const response = await GET(`${appSubUrl}/notifications/new`);
@ -185,9 +184,9 @@ async function updateNotificationCount() {
el.textContent = `${data.new}`; el.textContent = `${data.new}`;
} }
return `${data.new}`; return data.new as number;
} catch (error) { } catch (error) {
console.error(error); console.error(error);
return '0'; return 0;
} }
} }

View File

@ -1,5 +1,7 @@
export function initOAuth2SettingsDisableCheckbox() { export function initOAuth2SettingsDisableCheckbox() {
for (const e of document.querySelectorAll('.disable-setting')) e.addEventListener('change', ({target}) => { for (const el of document.querySelectorAll('.disable-setting')) {
document.querySelector(e.getAttribute('data-target')).classList.toggle('disabled', target.checked); el.addEventListener('change', (e: Event & {target: HTMLInputElement}) => {
}); document.querySelector(e.target.getAttribute('data-target')).classList.toggle('disabled', e.target.checked);
});
}
} }

View File

@ -34,7 +34,7 @@ export function countAndUpdateViewedFiles() {
export function initViewedCheckboxListenerFor() { export function initViewedCheckboxListenerFor() {
for (const form of document.querySelectorAll(`${viewedCheckboxSelector}:not([data-has-viewed-checkbox-listener="true"])`)) { for (const form of document.querySelectorAll(`${viewedCheckboxSelector}:not([data-has-viewed-checkbox-listener="true"])`)) {
// To prevent double addition of listeners // To prevent double addition of listeners
form.setAttribute('data-has-viewed-checkbox-listener', true); form.setAttribute('data-has-viewed-checkbox-listener', String(true));
// The checkbox consists of a div containing the real checkbox with its label and the CSRF token, // The checkbox consists of a div containing the real checkbox with its label and the CSRF token,
// hence the actual checkbox first has to be found // hence the actual checkbox first has to be found
@ -67,7 +67,7 @@ export function initViewedCheckboxListenerFor() {
// Unfortunately, actual forms cause too many problems, hence another approach is needed // Unfortunately, actual forms cause too many problems, hence another approach is needed
const files = {}; const files = {};
files[fileName] = this.checked; files[fileName] = this.checked;
const data = {files}; const data: Record<string, any> = {files};
const headCommitSHA = form.getAttribute('data-headcommit'); const headCommitSHA = form.getAttribute('data-headcommit');
if (headCommitSHA) data.headCommitSHA = headCommitSHA; if (headCommitSHA) data.headCommitSHA = headCommitSHA;
POST(form.getAttribute('data-link'), {data}); POST(form.getAttribute('data-link'), {data});

View File

@ -35,7 +35,7 @@ function initEditPreviewTab(elForm: HTMLFormElement) {
} }
export function initRepoEditor() { export function initRepoEditor() {
const dropzoneUpload = document.querySelector('.page-content.repository.editor.upload .dropzone'); const dropzoneUpload = document.querySelector<HTMLElement>('.page-content.repository.editor.upload .dropzone');
if (dropzoneUpload) initDropzone(dropzoneUpload); if (dropzoneUpload) initDropzone(dropzoneUpload);
const editArea = document.querySelector<HTMLTextAreaElement>('.page-content.repository.editor textarea#edit_area'); const editArea = document.querySelector<HTMLTextAreaElement>('.page-content.repository.editor textarea#edit_area');

View File

@ -5,9 +5,10 @@ export function initRepositorySearch() {
repositorySearchForm.addEventListener('change', (e: Event & {target: HTMLFormElement}) => { repositorySearchForm.addEventListener('change', (e: Event & {target: HTMLFormElement}) => {
e.preventDefault(); e.preventDefault();
const formData = new FormData(repositorySearchForm); const params = new URLSearchParams();
const params = new URLSearchParams(formData); for (const [key, value] of new FormData(repositorySearchForm).entries()) {
params.set(key, value.toString());
}
if (e.target.name === 'clear-filter') { if (e.target.name === 'clear-filter') {
params.delete('archived'); params.delete('archived');
params.delete('fork'); params.delete('fork');

View File

@ -2,6 +2,7 @@ import {beforeEach, describe, expect, test, vi} from 'vitest';
import {initRepoSettingsBranchesDrag} from './repo-settings-branches.ts'; import {initRepoSettingsBranchesDrag} from './repo-settings-branches.ts';
import {POST} from '../modules/fetch.ts'; import {POST} from '../modules/fetch.ts';
import {createSortable} from '../modules/sortable.ts'; import {createSortable} from '../modules/sortable.ts';
import type {SortableEvent} from 'sortablejs';
vi.mock('../modules/fetch.ts', () => ({ vi.mock('../modules/fetch.ts', () => ({
POST: vi.fn(), POST: vi.fn(),
@ -54,8 +55,8 @@ describe('Repository Branch Settings', () => {
vi.mocked(POST).mockResolvedValue({ok: true} as Response); vi.mocked(POST).mockResolvedValue({ok: true} as Response);
// Mock createSortable to capture and execute the onEnd callback // Mock createSortable to capture and execute the onEnd callback
vi.mocked(createSortable).mockImplementation((_el, options) => { vi.mocked(createSortable).mockImplementation(async (_el: Element, options) => {
options.onEnd(); options.onEnd(new Event('SortableEvent') as SortableEvent);
return {destroy: vi.fn()}; return {destroy: vi.fn()};
}); });

View File

@ -51,6 +51,7 @@ function makeCollections({mentions, emoji}) {
export async function attachTribute(element, {mentions, emoji}) { export async function attachTribute(element, {mentions, emoji}) {
const {default: Tribute} = await import(/* webpackChunkName: "tribute" */'tributejs'); const {default: Tribute} = await import(/* webpackChunkName: "tribute" */'tributejs');
const collections = makeCollections({mentions, emoji}); const collections = makeCollections({mentions, emoji});
// @ts-expect-error TS2351: This expression is not constructable (strange, why)
const tribute = new Tribute({collection: collections, noMatchTemplate: ''}); const tribute = new Tribute({collection: collections, noMatchTemplate: ''});
tribute.attach(element); tribute.attach(element);
return tribute; return tribute;

View File

@ -8,6 +8,17 @@ declare module '*.css' {
export default value; export default value;
} }
declare module '*.vue' {
import type {DefineComponent} from 'vue';
const component: DefineComponent<unknown, unknown, any>;
export default component;
// List of named exports from vue components, used to make `tsc` output clean.
// To actually lint .vue files, `vue-tsc` is used because `tsc` can not parse them.
export function initRepoBranchTagSelector(selector: string): void;
export function initDashboardRepoList(): void;
export function initRepositoryActionView(): void;
}
declare let __webpack_public_path__: string; declare let __webpack_public_path__: string;
declare module 'htmx.org/dist/htmx.esm.js' { declare module 'htmx.org/dist/htmx.esm.js' {
@ -16,8 +27,8 @@ declare module 'htmx.org/dist/htmx.esm.js' {
} }
declare module 'uint8-to-base64' { declare module 'uint8-to-base64' {
export function encode(arrayBuffer: ArrayBuffer): string; export function encode(arrayBuffer: Uint8Array): string;
export function decode(base64str: string): ArrayBuffer; export function decode(base64str: string): Uint8Array;
} }
declare module 'swagger-ui-dist/swagger-ui-es-bundle.js' { declare module 'swagger-ui-dist/swagger-ui-es-bundle.js' {

View File

@ -16,7 +16,6 @@ export function createTippy(target: Element, opts: TippyOpts = {}): Instance {
// because we should use our own wrapper functions to handle them, do not let the user override them // 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 {onHide, onShow, onDestroy, role, theme, arrow, ...other} = opts;
// @ts-expect-error: wrong type derived by typescript
const instance: Instance = tippy(target, { const instance: Instance = tippy(target, {
appendTo: document.body, appendTo: document.body,
animation: false, animation: false,

View File

@ -134,16 +134,16 @@ export function toAbsoluteUrl(url: string): string {
return `${window.location.origin}${url}`; return `${window.location.origin}${url}`;
} }
// Encode an ArrayBuffer into a URLEncoded base64 string. // Encode an Uint8Array into a URLEncoded base64 string.
export function encodeURLEncodedBase64(arrayBuffer: ArrayBuffer): string { export function encodeURLEncodedBase64(uint8Array: Uint8Array): string {
return encode(arrayBuffer) return encode(uint8Array)
.replace(/\+/g, '-') .replace(/\+/g, '-')
.replace(/\//g, '_') .replace(/\//g, '_')
.replace(/=/g, ''); .replace(/=/g, '');
} }
// Decode a URLEncoded base64 to an ArrayBuffer. // Decode a URLEncoded base64 to an Uint8Array.
export function decodeURLEncodedBase64(base64url: string): ArrayBuffer { export function decodeURLEncodedBase64(base64url: string): Uint8Array {
return decode(base64url return decode(base64url
.replace(/_/g, '/') .replace(/_/g, '/')
.replace(/-/g, '+')); .replace(/-/g, '+'));