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
lint-js: node_modules
npx eslint --color --max-warnings=0 --ext js,ts,vue $(ESLINT_FILES)
# npx vue-tsc
npx vue-tsc
.PHONY: lint-js-fix
lint-js-fix: node_modules
npx eslint --color --max-warnings=0 --ext js,ts,vue $(ESLINT_FILES) --fix
# npx vue-tsc
npx vue-tsc
.PHONY: lint-css
lint-css: node_modules
@ -451,10 +451,6 @@ lint-templates: .venv node_modules
lint-yaml: .venv
@poetry run yamllint .
.PHONY: tsc
tsc:
npx vue-tsc
.PHONY: watch
watch:
@bash tools/watch.sh

62
package-lock.json generated
View File

@ -67,6 +67,7 @@
"devDependencies": {
"@eslint-community/eslint-plugin-eslint-comments": "4.4.1",
"@playwright/test": "1.49.0",
"@silverwind/vue-tsc": "2.1.13",
"@stoplight/spectral-cli": "6.14.2",
"@stylistic/eslint-plugin-js": "2.11.0",
"@stylistic/stylelint-plugin": "3.1.1",
@ -111,8 +112,7 @@
"type-fest": "4.30.0",
"updates": "16.4.0",
"vite-string-plugin": "1.3.4",
"vitest": "2.1.8",
"vue-tsc": "2.1.10"
"vitest": "2.1.8"
},
"engines": {
"node": ">= 18.0.0"
@ -3833,6 +3833,24 @@
"hasInstallScript": true,
"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": {
"version": "2.0.6",
"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": {
"version": "2.4.10",
"resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.10.tgz",
"integrity": "sha512-hG3Z13+nJmGaT+fnQzAkS0hjJRa2FCeqZt6Bd+oGNhUkQ+mTFsDETg5rqUTxyzIh5pSOGY7FHCWUS8G82AzLCA==",
"version": "2.4.11",
"resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.11.tgz",
"integrity": "sha512-lN2C1+ByfW9/JRPpqScuZt/4OrUUse57GLI6TbLgTIqBVemdl1wNcZ1qYGEo2+Gw8coYLgCy7SuKqn6IrQcQgg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@volar/source-map": "2.4.10"
"@volar/source-map": "2.4.11"
}
},
"node_modules/@volar/source-map": {
"version": "2.4.10",
"resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.10.tgz",
"integrity": "sha512-OCV+b5ihV0RF3A7vEvNyHPi4G4kFa6ukPmyVocmqm5QzOd8r5yAtiNvaPEjl8dNvgC/lj4JPryeeHLdXd62rWA==",
"version": "2.4.11",
"resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-2.4.11.tgz",
"integrity": "sha512-ZQpmafIGvaZMn/8iuvCFGrW3smeqkq/IIh9F1SdSx9aUl0J4Iurzd6/FhmjNO5g2ejF3rT45dKskgXWiofqlZQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@volar/typescript": {
"version": "2.4.10",
"resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.10.tgz",
"integrity": "sha512-F8ZtBMhSXyYKuBfGpYwqA5rsONnOwAVvjyE7KPYJ7wgZqo2roASqNWUnianOomJX5u1cxeRooHV59N0PhvEOgw==",
"version": "2.4.11",
"resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-2.4.11.tgz",
"integrity": "sha512-2DT+Tdh88Spp5PyPbqhyoYavYCPDsqbHLFwcUI9K1NlY1YgUJvujGdrqUp0zWxnW7KWNTr3xSpMuv2WnaTKDAw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@volar/language-core": "2.4.10",
"@volar/language-core": "2.4.11",
"path-browserify": "^1.0.1",
"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": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz",

View File

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

View File

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

View File

@ -7,7 +7,7 @@ const reIssueSharpIndex = /^#(\d+)$/; // eg: "#123"
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
export function parseIssueListQuickGotoLink(repoLink, searchText) {
export function parseIssueListQuickGotoLink(repoLink: string, searchText: string) {
searchText = searchText.trim();
let targetUrl = '';
if (repoLink) {
@ -15,13 +15,12 @@ export function parseIssueListQuickGotoLink(repoLink, searchText) {
if (reIssueIndex.test(searchText)) {
targetUrl = `${repoLink}/issues/${searchText}`;
} else if (reIssueSharpIndex.test(searchText)) {
targetUrl = `${repoLink}/issues/${searchText.substr(1)}`;
targetUrl = `${repoLink}/issues/${searchText.substring(1)}`;
}
} else {
// try to parse it for a global search (eg: "owner/repo#123")
const matchIssueOwnerRepoIndex = searchText.match(reIssueOwnerRepoIndex);
if (matchIssueOwnerRepoIndex) {
const [_, owner, repo, index] = matchIssueOwnerRepoIndex;
const [_, owner, repo, index] = reIssueOwnerRepoIndex.exec(searchText) || [];
if (owner) {
targetUrl = `${appSubUrl}/${owner}/${repo}/issues/${index}`;
}
}
@ -33,7 +32,7 @@ export function initCommonIssueListQuickGoto() {
if (!goto) return;
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');
form.addEventListener('submit', (e) => {

View File

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

View File

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

View File

@ -3,7 +3,7 @@ import {fomanticQuery} from '../../modules/fomantic/base.ts';
export function initCompReactionSelector(parent: ParentNode = document) {
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
const target = e.target.closest('.comment-reaction-button');
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
const httpMethodInput = document.querySelector('#http_method');
const httpMethodInput = document.querySelector<HTMLInputElement>('#http_method');
if (httpMethodInput) {
const updateContentType = function () {
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 {createElementFromHTML, createElementFromAttrs} from '../utils/dom.ts';
import {isImageFile, isVideoFile} from '../utils.ts';
import type {DropzoneFile} from 'dropzone/index.js';
const {csrfToken, i18n} = window.config;
@ -15,14 +16,14 @@ export const DropzoneCustomEventRemovedFile = 'dropzone-custom-removed-file';
export const DropzoneCustomEventUploadDone = 'dropzone-custom-upload-done';
async function createDropzone(el, opts) {
const [{Dropzone}] = await Promise.all([
const [{default: Dropzone}] = await Promise.all([
import(/* webpackChunkName: "dropzone" */'dropzone'),
import(/* webpackChunkName: "dropzone" */'dropzone/dist/dropzone.css'),
]);
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})`;
if (isImageFile(file)) {
fileMarkdown = `!${fileMarkdown}`;
@ -60,14 +61,14 @@ function addCopyLink(file) {
/**
* @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 removeAttachmentUrl = dropzoneEl.getAttribute('data-remove-url');
const attachmentBaseLinkUrl = dropzoneEl.getAttribute('data-link-url');
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
const opts = {
const opts: Record<string, any> = {
url: dropzoneEl.getAttribute('data-upload-url'),
headers: {'X-Csrf-Token': csrfToken},
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]"
// 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);
dzInst.on('success', (file, resp) => {
dzInst.on('success', (file: DropzoneFile & {uuid: string}, resp: any) => {
file.uuid = resp.uuid;
fileUuidDict[file.uuid] = {submitted: false};
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.on('removedfile', async (file) => {
dzInst.on('removedfile', async (file: DropzoneFile & {uuid: string}) => {
if (disableRemovedfileEvent) return;
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;

View File

@ -2,6 +2,11 @@ const sourcesByUrl = {};
const sourcesByPort = {};
class Source {
url: string;
eventSource: EventSource;
listening: Record<string, any>;
clients: Array<any>;
constructor(url) {
this.url = 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) {
port.addEventListener('message', (event) => {
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
const locale = {
heatMapLocale: {
months: new Array(12).fill().map((_, idx) => translateMonth(idx)),
days: new Array(7).fill().map((_, idx) => translateDay(idx)),
months: new Array(12).fill(undefined).map((_, idx) => translateMonth(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"
more: el.getAttribute('data-locale-more'),
less: el.getAttribute('data-locale-less'),

View File

@ -22,9 +22,9 @@ function initPreInstall() {
mssql: '127.0.0.1:1433',
};
const dbHost = document.querySelector('#db_host');
const dbUser = document.querySelector('#db_user');
const dbName = document.querySelector('#db_name');
const dbHost = document.querySelector<HTMLInputElement>('#db_host');
const dbUser = document.querySelector<HTMLInputElement>('#db_user');
const dbName = document.querySelector<HTMLInputElement>('#db_name');
// Database type change detection.
document.querySelector('#db_type').addEventListener('change', function () {
@ -48,12 +48,12 @@ function initPreInstall() {
});
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')) {
appUrl.value = window.location.href;
}
const domain = document.querySelector('#domain');
const domain = document.querySelector<HTMLInputElement>('#domain');
if (domain.value.trim() === 'localhost') {
domain.value = window.location.hostname;
}
@ -61,43 +61,43 @@ function initPreInstall() {
// TODO: better handling of exclusive relations.
document.querySelector('#offline-mode input').addEventListener('change', function () {
if (this.checked) {
document.querySelector('#disable-gravatar input').checked = true;
document.querySelector('#federated-avatar-lookup input').checked = false;
document.querySelector<HTMLInputElement>('#disable-gravatar input').checked = true;
document.querySelector<HTMLInputElement>('#federated-avatar-lookup input').checked = false;
}
});
document.querySelector('#disable-gravatar input').addEventListener('change', function () {
if (this.checked) {
document.querySelector('#federated-avatar-lookup input').checked = false;
document.querySelector<HTMLInputElement>('#federated-avatar-lookup input').checked = false;
} 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 () {
if (this.checked) {
document.querySelector('#disable-gravatar input').checked = false;
document.querySelector('#offline-mode input').checked = false;
document.querySelector<HTMLInputElement>('#disable-gravatar input').checked = false;
document.querySelector<HTMLInputElement>('#offline-mode input').checked = false;
}
});
document.querySelector('#enable-openid-signin input').addEventListener('change', function () {
if (this.checked) {
if (!document.querySelector('#disable-registration input').checked) {
document.querySelector('#enable-openid-signup input').checked = true;
if (!document.querySelector<HTMLInputElement>('#disable-registration input').checked) {
document.querySelector<HTMLInputElement>('#enable-openid-signup input').checked = true;
}
} 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 () {
if (this.checked) {
document.querySelector('#enable-captcha input').checked = false;
document.querySelector('#enable-openid-signup input').checked = false;
document.querySelector<HTMLInputElement>('#enable-captcha input').checked = false;
document.querySelector<HTMLInputElement>('#enable-openid-signup input').checked = false;
} 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 () {
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) => {
if (e.persisted) { // page was restored from bfcache
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);
for (const item of table.querySelectorAll('.notifications-item[data-remove="true"]')) {
item.remove();
unreadCount -= 1;
}
unreadCountEl.textContent = unreadCount;
unreadCountEl.textContent = String(unreadCount);
}
});
// mark clicked unread links for deletion on bfcache restore
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');
});
}
}
async function receiveUpdateCount(event) {
async function receiveUpdateCount(event: MessageEvent) {
try {
const data = JSON.parse(event.data);
@ -50,7 +50,7 @@ export function initNotificationCount() {
if (!document.querySelector('.notification_count')) return;
let usingPeriodicPoller = false;
const startPeriodicPoller = (timeout, lastCount) => {
const startPeriodicPoller = (timeout: number, lastCount?: number) => {
if (timeout <= 0 || !Number.isFinite(timeout)) return;
usingPeriodicPoller = true;
lastCount = lastCount ?? getCurrentCount();
@ -72,13 +72,13 @@ export function initNotificationCount() {
type: 'start',
url: `${window.location.origin}${appSubUrl}/user/events`,
});
worker.port.addEventListener('message', (event) => {
worker.port.addEventListener('message', (event: MessageEvent) => {
if (!event.data || !event.data.type) {
console.error('unknown worker message event', event);
return;
}
if (event.data.type === 'notification-count') {
const _promise = receiveUpdateCount(event.data);
receiveUpdateCount(event); // no await
} else if (event.data.type === 'no-event-source') {
// browser doesn't support EventSource, falling back to periodic poller
if (!usingPeriodicPoller) startPeriodicPoller(notificationSettings.MinTimeout);
@ -118,10 +118,10 @@ export function initNotificationCount() {
}
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();
if (lastCount !== currentCount) {
callback(notificationSettings.MinTimeout, currentCount);
@ -149,10 +149,9 @@ async function updateNotificationTable() {
if (notificationDiv) {
try {
const params = new URLSearchParams(window.location.search);
params.set('div-only', true);
params.set('sequence-number', ++notificationSequenceNumber);
const url = `${appSubUrl}/notifications?${params.toString()}`;
const response = await GET(url);
params.set('div-only', String(true));
params.set('sequence-number', String(++notificationSequenceNumber));
const response = await GET(`${appSubUrl}/notifications?${params.toString()}`);
if (!response.ok) {
throw new Error('Failed to fetch notification table');
@ -169,7 +168,7 @@ async function updateNotificationTable() {
}
}
async function updateNotificationCount() {
async function updateNotificationCount(): Promise<number> {
try {
const response = await GET(`${appSubUrl}/notifications/new`);
@ -185,9 +184,9 @@ async function updateNotificationCount() {
el.textContent = `${data.new}`;
}
return `${data.new}`;
return data.new as number;
} catch (error) {
console.error(error);
return '0';
return 0;
}
}

View File

@ -1,5 +1,7 @@
export function initOAuth2SettingsDisableCheckbox() {
for (const e of document.querySelectorAll('.disable-setting')) e.addEventListener('change', ({target}) => {
document.querySelector(e.getAttribute('data-target')).classList.toggle('disabled', target.checked);
});
for (const el of document.querySelectorAll('.disable-setting')) {
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() {
for (const form of document.querySelectorAll(`${viewedCheckboxSelector}:not([data-has-viewed-checkbox-listener="true"])`)) {
// 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,
// 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
const files = {};
files[fileName] = this.checked;
const data = {files};
const data: Record<string, any> = {files};
const headCommitSHA = form.getAttribute('data-headcommit');
if (headCommitSHA) data.headCommitSHA = headCommitSHA;
POST(form.getAttribute('data-link'), {data});

View File

@ -35,7 +35,7 @@ function initEditPreviewTab(elForm: HTMLFormElement) {
}
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);
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}) => {
e.preventDefault();
const formData = new FormData(repositorySearchForm);
const params = new URLSearchParams(formData);
const params = new URLSearchParams();
for (const [key, value] of new FormData(repositorySearchForm).entries()) {
params.set(key, value.toString());
}
if (e.target.name === 'clear-filter') {
params.delete('archived');
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 {POST} from '../modules/fetch.ts';
import {createSortable} from '../modules/sortable.ts';
import type {SortableEvent} from 'sortablejs';
vi.mock('../modules/fetch.ts', () => ({
POST: vi.fn(),
@ -54,8 +55,8 @@ describe('Repository Branch Settings', () => {
vi.mocked(POST).mockResolvedValue({ok: true} as Response);
// Mock createSortable to capture and execute the onEnd callback
vi.mocked(createSortable).mockImplementation((_el, options) => {
options.onEnd();
vi.mocked(createSortable).mockImplementation(async (_el: Element, options) => {
options.onEnd(new Event('SortableEvent') as SortableEvent);
return {destroy: vi.fn()};
});

View File

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

View File

@ -8,6 +8,17 @@ declare module '*.css' {
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 module 'htmx.org/dist/htmx.esm.js' {
@ -16,8 +27,8 @@ declare module 'htmx.org/dist/htmx.esm.js' {
}
declare module 'uint8-to-base64' {
export function encode(arrayBuffer: ArrayBuffer): string;
export function decode(base64str: string): ArrayBuffer;
export function encode(arrayBuffer: Uint8Array): string;
export function decode(base64str: string): Uint8Array;
}
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
const {onHide, onShow, onDestroy, role, theme, arrow, ...other} = opts;
// @ts-expect-error: wrong type derived by typescript
const instance: Instance = tippy(target, {
appendTo: document.body,
animation: false,

View File

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