diff --git a/eslint.config.ts b/eslint.config.ts index 64e46a53639..152069a3a50 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -17,6 +17,7 @@ import unescapedHtmlLiteral from './tools/eslint-rules/unescaped-html-literal.ts const jsExts = ['js', 'mjs', 'cjs'] as const; const tsExts = ['ts', 'mts', 'cts'] as const; +const vueExts = ['vue'] as const; const restrictedGlobals = [ {name: 'localStorage', message: 'Use `modules/user-settings.ts` instead.'}, @@ -36,7 +37,7 @@ export default defineConfig([ 'public/assets/js', ]), { - files: [`**/*.{${[...jsExts, ...tsExts].join(',')}}`], + files: [`**/*.{${[...jsExts, ...tsExts, ...vueExts].join(',')}}`], ignores: ['dist/*'], languageOptions: { ecmaVersion: 'latest', @@ -48,7 +49,9 @@ export default defineConfig([ ecmaFeatures: { impliedStrict: true, }, + parser: typescriptParser, project: true, + extraFileExtensions: vueExts.map((ext) => `.${ext}`), }, }, linterOptions: { @@ -66,8 +69,11 @@ export default defineConfig([ wc, }, settings: { - 'import-x/extensions': [...jsExts, ...tsExts].map((ext) => `.${ext}`), - 'import-x/parsers': {'@typescript-eslint/parser': [...jsExts, ...tsExts].map((ext) => `.${ext}`)}, + 'import-x/extensions': [...jsExts, ...tsExts, ...vueExts].map((ext) => `.${ext}`), + 'import-x/parsers': { + '@typescript-eslint/parser': [...jsExts, ...tsExts].map((ext) => `.${ext}`), + 'vue-eslint-parser': vueExts.map((ext) => `.${ext}`), + }, 'import-x/resolver': {'typescript': true}, }, rules: { @@ -958,7 +964,7 @@ export default defineConfig([ 'unicorn/prefer-iterable-in-constructor': [2], 'unicorn/prefer-iterator-concat': [0], // too opinionated 'unicorn/prefer-iterator-helpers': [0], - 'unicorn/prefer-iterator-to-array': [2], + 'unicorn/prefer-iterator-to-array': [0], // Iterator#toArray requires ES2025 'unicorn/prefer-iterator-to-array-at-end': [2], 'unicorn/prefer-iterator-zip': [0], 'unicorn/prefer-json-import': [0], @@ -1096,17 +1102,13 @@ export default defineConfig([ }, }, { - files: ['**/*.vue'], - languageOptions: { - parserOptions: { - parser: '@typescript-eslint/parser', - }, - }, + files: vueExts.map((ext) => `**/*.${ext}`), extends: [ vue.configs['flat/recommended'], vueScopedCss.configs.recommended, ], rules: { + '@typescript-eslint/no-redundant-type-constituents': [0], // types imported from .vue files resolve to any via typescript-eslint's *.vue shim 'vue/attributes-order': [0], 'vue/html-closing-bracket-spacing': [2, {startTag: 'never', endTag: 'never', selfClosingTag: 'never'}], 'vue/max-attributes-per-line': [0], diff --git a/package.json b/package.json index 7a531e0ca07..ee4a0416e24 100644 --- a/package.json +++ b/package.json @@ -109,6 +109,7 @@ "typescript": "6.0.3", "typescript-eslint": "8.70.1", "vitest": "5.0.1", + "vue-eslint-parser": "10.4.0", "vue-tsc": "3.3.11" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e4cbde93724..2021be74443 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -467,6 +467,9 @@ importers: vitest: specifier: 5.0.1 version: 5.0.1(@types/node@26.6.2)(@vitest/browser-playwright@5.0.1)(@vitest/ui@5.0.1)(happy-dom@20.11.2)(jsdom@20.0.3(supports-color@10.2.2))(vite@8.3.0(@types/node@26.6.2)(esbuild@0.28.2)(jiti@2.7.0)(yaml@2.9.0)) + vue-eslint-parser: + specifier: 10.4.0 + version: 10.4.0(eslint@10.11.0(jiti@2.7.0)(supports-color@10.2.2))(supports-color@10.2.2) vue-tsc: specifier: 3.3.11 version: 3.3.11(typescript@6.0.3) diff --git a/web_src/js/components/ActionRunJobView.vue b/web_src/js/components/ActionRunJobView.vue index 1d315654ea3..2c09ec5f2e2 100644 --- a/web_src/js/components/ActionRunJobView.vue +++ b/web_src/js/components/ActionRunJobView.vue @@ -13,18 +13,13 @@ import {toggleFullScreen} from '../utils.ts'; import {localUserSettings} from '../modules/user-settings.ts'; import type {ActionsArtifact, ActionsJob, ActionsRun, ActionsStatus} from '../modules/gitea-actions.ts'; import {AnsiLineRenderer} from '../render/ansi.ts'; -import { - type ActionRunViewStore, - createLogLineMessage, - type LogLine, - type LogLineCommand, - parseLogLineCommand, -} from './ActionRunView.ts'; +import {createLogLineMessage, parseLogLineCommand} from './ActionRunView.ts'; +import type {ActionRunViewStore, LogLine, LogLineCommand} from './ActionRunView.ts'; -function isLogElementInViewport(el: Element, {extraViewPortHeight}={extraViewPortHeight: 0}): boolean { +function isLogElementInViewport(el: Element, {extraViewPortHeight} = {extraViewPortHeight: 0}): boolean { const rect = el.getBoundingClientRect(); // only check whether bottom is in viewport, because the log element can be a log group which is usually tall - return 0 <= rect.bottom && rect.bottom <= window.innerHeight + extraViewPortHeight; + return rect.bottom >= 0 && rect.bottom <= window.innerHeight + extraViewPortHeight; } export type ActionRunJobViewLocale = { @@ -42,14 +37,14 @@ type Step = { summary: string, duration: string, status: ActionsStatus, -} +}; type JobStepState = { - cursor: string|null, + cursor: string | null, expanded: boolean, manuallyCollapsed: boolean, // whether the user manually collapsed the step, used to avoid auto-expanding it again firstLogTime?: number, // the step's first log line time, what "Show seconds" counts from -} +}; // one ANSI renderer per step, so an unterminated color carries between that step's lines only const stepAnsiRenderers: AnsiLineRenderer[] = []; @@ -59,7 +54,7 @@ type StepContainerElement = HTMLElement & { // then the following batches of logs should still use the same group (active logs container). // maybe it can be refactored to decouple from the HTML element in the future. _stepLogsActiveContainer?: HTMLElement; -} +}; type LocaleStorageOptions = { autoScroll: boolean; @@ -176,8 +171,8 @@ onMounted(async () => { }, 0); }); - intervalID = setInterval(() => void loadJob(), 1000); - void hashChangeListener(); + intervalID = setInterval(() => loadJob(), 1000); + hashChangeListener(); window.addEventListener('hashchange', hashChangeListener); }); @@ -256,7 +251,7 @@ async function copyStepOutput(event: MouseEvent, stepIndex: number) { function toggleStepLogs(idx: number) { currentJobStepsStates.value[idx].expanded = !currentJobStepsStates.value[idx].expanded; if (currentJobStepsStates.value[idx].expanded) { - void loadJobForce(); // try to load the data immediately instead of waiting for next timer interval + loadJobForce(); // try to load the data immediately instead of waiting for next timer interval } else if (currentJob.value.steps[idx].status === 'running') { currentJobStepsStates.value[idx].manuallyCollapsed = true; } @@ -293,19 +288,14 @@ function shouldAutoScroll(stepIndex: number): boolean { function appendLogs(stepIndex: number, startTime: number, logLines: LogLine[]) { for (const line of logLines) { const cmd = parseLogLineCommand(line); - switch (cmd?.name) { - case 'hidden': - continue; - case 'group': - beginLogGroup(stepIndex, startTime, line, cmd); - continue; - case 'endgroup': - endLogGroup(stepIndex); - continue; + if (cmd?.name === 'group') { + beginLogGroup(stepIndex, startTime, line, cmd); + } else if (cmd?.name === 'endgroup') { + endLogGroup(stepIndex); + } else if (cmd?.name !== 'hidden') { + // the active logs container may change during the loop, for example: entering and leaving a group + getActiveLogsContainer(stepIndex).append(createLogLine(stepIndex, startTime, line, cmd)); } - // the active logs container may change during the loop, for example: entering and leaving a group - const el = getActiveLogsContainer(stepIndex); - el.append(createLogLine(stepIndex, startTime, line, cmd)); } } diff --git a/web_src/js/components/DashboardRepoList.vue b/web_src/js/components/DashboardRepoList.vue index c620e89900b..ce80141e75a 100644 --- a/web_src/js/components/DashboardRepoList.vue +++ b/web_src/js/components/DashboardRepoList.vue @@ -148,8 +148,8 @@ const activeIndex = shallowRef(-1); // don't select anything at load, first curs const elSearch = useTemplateRef('elSearch') as Readonly>; const countsKey = computed(() => `${reposFilter.value}:${archivedFilter.value}:${privateFilter.value}`); -const showMoreReposLink = computed(() => repos.value.length > 0 && repos.value.length < repoTypeCount.value); const repoTypeCount = computed(() => counts.value[countsKey.value]); +const showMoreReposLink = computed(() => repos.value.length > 0 && repos.value.length < repoTypeCount.value); const checkboxArchivedFilterTitle = computed(() => textArchivedFilterTitles.get(archivedFilter.value)); const checkboxArchivedFilterProps = computed(() => ({checked: archivedFilter.value === 'archived', indeterminate: archivedFilter.value === 'both'})); const checkboxPrivateFilterTitle = computed(() => textPrivateFilterTitles.get(privateFilter.value)); @@ -218,7 +218,7 @@ function updateHistory() { if (page.value === 1) { params.delete('repo-search-page'); } else { - params.set('repo-search-page', `${page.value}`); + params.set('repo-search-page', String(page.value)); } const queryString = params.toString(); @@ -275,22 +275,23 @@ async function searchRepos() { let response: Response, json: {data: WebSearchRepo[]}; try { const firstLoad = reposTotalCount.value === null; + const loadTotalCount = async () => { + if (reposTotalCount.value) return; + const totalCountResponse = await GET(`${subUrl}/repo/search?count_only=1&uid=${uid}&team_id=${teamId}&q=&page=1&mode=`); + reposTotalCount.value = parseInt(totalCountResponse.headers.get('X-Total-Count') ?? '0'); + if (firstLoad && reposTotalCount.value) { + nextTick(() => { + // MDN: If there's no focused element, this is the Document.body or Document.documentElement. + if ((document.activeElement === document.body || document.activeElement === document.documentElement)) { + elSearch.value.focus({preventScroll: true}); + } + }); + } + }; // independent of the search, so both requests go out together - const totalCountSearchURL = `${subUrl}/repo/search?count_only=1&uid=${uid}&team_id=${teamId}&q=&page=1&mode=`; - const totalCountRequest = reposTotalCount.value ? null : GET(totalCountSearchURL); const searchRequest = GET(searchedURL); - searchRequest.catch(() => {}); // awaited below, marked handled in case the count throws first - if (totalCountRequest) { - reposTotalCount.value = parseInt((await totalCountRequest).headers.get('X-Total-Count') ?? '0'); - } - if (firstLoad && reposTotalCount.value) { - nextTick(() => { - // MDN: If there's no focused element, this is the Document.body or Document.documentElement. - if ((document.activeElement === document.body || document.activeElement === document.documentElement)) { - elSearch.value.focus({preventScroll: true}); - } - }); - } + const [totalCountResult] = await Promise.allSettled([loadTotalCount(), searchRequest]); // a failed search must not show the empty state before the count is known + if (totalCountResult.status === 'rejected') throw totalCountResult.reason; response = await searchRequest; json = await response.json(); } catch { diff --git a/web_src/js/components/DiffCommitSelector.vue b/web_src/js/components/DiffCommitSelector.vue index 47cb6df3de5..0471ad15f93 100644 --- a/web_src/js/components/DiffCommitSelector.vue +++ b/web_src/js/components/DiffCommitSelector.vue @@ -12,13 +12,13 @@ type Commit = { committer_or_author_name: string, time: string, short_sha: string, -} +}; type CommitListResult = { commits: Array, last_review_commit_sha: string, locale: Record, -} +}; const elRoot = useTemplateRef('elRoot') as Readonly>; const elExpandBtn = useTemplateRef('elExpandBtn') as Readonly>; @@ -154,7 +154,7 @@ async function fetchCommits() { commits.value.push(...results.commits); commits.value.reverse(); lastReviewCommitSha.value = results.last_review_commit_sha || null; - if (lastReviewCommitSha.value && !commits.value.some((x) => x.id === lastReviewCommitSha.value)) { + if (lastReviewCommitSha.value && commits.value.every((x) => x.id !== lastReviewCommitSha.value)) { // the lastReviewCommit is not available (probably due to a force push) // reset the last review commit sha lastReviewCommitSha.value = null; diff --git a/web_src/js/components/RepoBranchTagSelector.vue b/web_src/js/components/RepoBranchTagSelector.vue index d998175da11..ab4fb03dcb2 100644 --- a/web_src/js/components/RepoBranchTagSelector.vue +++ b/web_src/js/components/RepoBranchTagSelector.vue @@ -2,6 +2,7 @@ import {computed, nextTick, onBeforeUnmount, onMounted, shallowRef, useTemplateRef, watch, type ShallowRef} from 'vue'; import SvgIcon from './SvgIcon.vue'; import {showErrorToast} from '../modules/toast.ts'; +import {errorMessage} from '../modules/errors.ts'; import {GET} from '../modules/fetch.ts'; import {pathEscapeSegments} from '../utils/url.ts'; import {queryElemChildren} from '../utils/dom.ts'; @@ -17,7 +18,7 @@ type ListItem = { type SelectedTab = 'branches' | 'tags'; -type TabLoadingStates = Record +type TabLoadingStates = Record; const props = defineProps<{ elRoot: HTMLElement; @@ -65,6 +66,14 @@ const enableFeed = props.elRoot.getAttribute('data-enable-feed') === 'true'; const searchFieldPlaceholder = computed(() => selectedTab.value === 'branches' ? textFilterBranch : textFilterTag); +const showCreateNewRef = computed(() => { + if (!allowCreateNewRef || !searchTerm.value) { + return false; + } + // FIXME: not quite right here, it mixes "branch" and "tag" names + return allItems.value.every((item: ListItem) => item.refShortName !== searchTerm.value); +}); + const filteredItems = computed(() => { const searchTermLower = searchTerm.value.toLowerCase(); const items = allItems.value.filter((item: ListItem) => { @@ -74,8 +83,7 @@ const filteredItems = computed(() => { return item.refShortName.toLowerCase().includes(searchTermLower); }); - // TODO: fix this anti-pattern: side-effects-in-computed-properties - activeItemIndex.value = !items.length && showCreateNewRef.value ? 0 : -1; // eslint-disable-line vue/no-side-effects-in-computed-properties + activeItemIndex.value = !items.length && showCreateNewRef.value ? 0 : -1; // eslint-disable-line vue/no-side-effects-in-computed-properties -- TODO: move this side effect out of the computed return items; }); @@ -84,14 +92,6 @@ const showNoResults = computed(() => { return !filteredItems.value.length && !showCreateNewRef.value; }); -const showCreateNewRef = computed(() => { - if (!allowCreateNewRef || !searchTerm.value) { - return false; - } - // FIXME: not quite right here, it mixes "branch" and "tag" names - return !allItems.value.some((item: ListItem) => item.refShortName === searchTerm.value); -}); - const createNewRefFormActionUrl = computed(() => { return `${currentRepoLink}/branches/_new/${currentRefType.value}/${pathEscapeSegments(currentRefShortName.value)}`; }); @@ -127,15 +127,15 @@ function selectItem(item: ListItem) { currentRefType.value = item.refType; currentRefShortName.value = item.refShortName; elDropdown.value.closest('form')!.action = refFormActionTemplate - .replace('{RepoLink}', currentRepoLink) - .replace('{RefType}', pathEscapeSegments(item.refType)) - .replace('{RefShortName}', pathEscapeSegments(item.refShortName)); + .replace('{RepoLink}', () => currentRepoLink) + .replace('{RefType}', () => pathEscapeSegments(item.refType)) + .replace('{RefShortName}', () => pathEscapeSegments(item.refShortName)); } else { - window.location.href = refLinkTemplate - .replace('{RepoLink}', currentRepoLink) - .replace('{RefType}', pathEscapeSegments(item.refType)) - .replace('{RefShortName}', pathEscapeSegments(item.refShortName)) - .replace('{TreePath}', pathEscapeSegments(currentTreePath)); + window.location.assign(refLinkTemplate + .replace('{RepoLink}', () => currentRepoLink) + .replace('{RefType}', () => pathEscapeSegments(item.refType)) + .replace('{RefShortName}', () => pathEscapeSegments(item.refShortName)) + .replace('{TreePath}', () => pathEscapeSegments(currentTreePath))); } } @@ -174,7 +174,7 @@ function keydown(e: KeyboardEvent) { return; } activeItemIndex.value = nextIndex; - getActiveItem()!.scrollIntoView({block: 'nearest'}); + getActiveItem().scrollIntoView({block: 'nearest'}); } else if (e.key === 'Enter') { e.preventDefault(); getActiveItem()?.click(); @@ -208,7 +208,7 @@ async function loadTabItems() { tabLoadingStates.value = {...tabLoadingStates.value, [tab]: 'done'}; } catch (e) { tabLoadingStates.value = {...tabLoadingStates.value, [tab]: ''}; - showErrorToast(`Network error when fetching items for ${tab}, error: ${e}`); + showErrorToast(`Network error when fetching items for ${tab}, error: ${errorMessage(e)}`); console.error(e); } } diff --git a/web_src/js/components/RepoContributors.vue b/web_src/js/components/RepoContributors.vue index c94805125cc..cfb2a25dd49 100644 --- a/web_src/js/components/RepoContributors.vue +++ b/web_src/js/components/RepoContributors.vue @@ -46,13 +46,13 @@ const customEventListener: Plugin = { }; type LineOptions = ChartOptions<'line'> & { - plugins?: { - customEventListener?: { - chartType: ChartType; - onDoubleClick: (args: {chart: Chart}, reset: boolean) => void; - }; - }; -} + plugins?: { + customEventListener?: { + chartType: ChartType; + onDoubleClick: (args: {chart: Chart}, reset: boolean) => void; + }; + }; +}; Chart.register( Title, @@ -157,7 +157,7 @@ async function fetchGraphData() { const data: ContributorsData = await response.json(); const {total, ...other} = data; // below line might be deleted if we are sure go produces map always sorted by keys - const totalWeeks = Object.fromEntries(Object.entries(total.weeks).sort()); + const totalWeeks = Object.fromEntries(Object.entries(total.weeks).sort(([a], [b]) => Number(a) - Number(b))); const weekValues = Object.values(totalWeeks); xAxisStart = weekValues[0].week; diff --git a/web_src/js/components/RepoFileSearch.vue b/web_src/js/components/RepoFileSearch.vue index 0991c6bfe08..b2fac527a0f 100644 --- a/web_src/js/components/RepoFileSearch.vue +++ b/web_src/js/components/RepoFileSearch.vue @@ -2,18 +2,19 @@ import {ref, computed, watch, nextTick, useTemplateRef, onMounted, onUnmounted, type ShallowRef} from 'vue'; import {generateElemId} from '../utils/dom.ts'; import {GET} from '../modules/fetch.ts'; -import {filterRepoFilesWeighted} from '../features/repo-findfile.ts'; import {pathEscapeSegments} from '../utils/url.ts'; import SvgIcon from './SvgIcon.vue'; import {throttle} from '../utils/func.ts'; +import type {RepoFilesFilter} from '../features/repo-findfile.ts'; -const props = defineProps({ - repoLink: { type: String, required: true }, - currentRefNameSubURL: { type: String, required: true }, - treeListUrl: { type: String, required: true }, - noResultsText: { type: String, required: true }, - placeholder: { type: String, required: true }, -}); +const props = defineProps<{ + repoLink: string; + currentRefNameSubURL: string; + treeListUrl: string; + noResultsText: string; + placeholder: string; + filterFiles: RepoFilesFilter; +}>(); const refElemInput = useTemplateRef('searchInput') as Readonly>; const refElemPopup = useTemplateRef('searchPopup') as Readonly>; @@ -28,7 +29,7 @@ const showPopup = computed(() => searchQuery.value.length > 0); const filteredFiles = computed(() => { if (!searchQuery.value) return []; - return filterRepoFilesWeighted(allFiles.value, searchQuery.value); + return props.filterFiles(allFiles.value, searchQuery.value); }); const applySearchQuery = throttle(() => { @@ -52,16 +53,17 @@ const handleKeyDown = (e: KeyboardEvent) => { if (!searchQuery.value || filteredFiles.value.length === 0) return; const handleSelectedItem = (idx: number) => { - e.preventDefault(); selectedIndex.value = idx; - const el = refElemPopup.value.querySelector(`.file-search-results > :nth-child(${idx+1} of .item)`); - el?.scrollIntoView({ block: 'nearest', behavior: 'instant' }); + const el = refElemPopup.value.querySelector(`.file-search-results > :nth-child(${idx + 1} of .item)`); + el?.scrollIntoView({block: 'nearest', behavior: 'instant'}); }; if (e.key === 'ArrowDown') { + e.preventDefault(); handleSelectedItem(Math.min(selectedIndex.value + 1, filteredFiles.value.length - 1)); } else if (e.key === 'ArrowUp') { - handleSelectedItem(Math.max(selectedIndex.value - 1, 0)) + e.preventDefault(); + handleSelectedItem(Math.max(selectedIndex.value - 1, 0)); } else if (e.key === 'Enter') { e.preventDefault(); const selectedFile = filteredFiles.value[selectedIndex.value]; @@ -71,11 +73,10 @@ const handleKeyDown = (e: KeyboardEvent) => { } }; -const clearSearch = () => { +function clearSearch() { searchQuery.value = ''; refElemInput.value.value = ''; -}; - +} const handleClickOutside = (e: MouseEvent) => { if (!searchQuery.value) return; @@ -85,7 +86,7 @@ const handleClickOutside = (e: MouseEvent) => { if (!clickInside) clearSearch(); }; -const loadFileListForSearch = async () => { +async function loadFileListForSearch() { if (hasLoadedFileList.value || isLoadingFileList.value) return; isLoadingFileList.value = true; @@ -96,11 +97,11 @@ const loadFileListForSearch = async () => { } finally { isLoadingFileList.value = false; } -}; +} function handleSearchResultClick(filePath: string) { clearSearch(); - window.location.href = `${props.repoLink}/src/${pathEscapeSegments(props.currentRefNameSubURL)}/${pathEscapeSegments(filePath)}`; + window.location.assign(`${props.repoLink}/src/${pathEscapeSegments(props.currentRefNameSubURL)}/${pathEscapeSegments(filePath)}`); } const updatePosition = () => { @@ -121,17 +122,19 @@ const updatePosition = () => { } }; +const resizeObserver = new ResizeObserver(updatePosition); + onMounted(() => { const searchPopupId = generateElemId('file-search-popup-'); refElemPopup.value.setAttribute('id', searchPopupId); refElemInput.value.setAttribute('aria-controls', searchPopupId); document.addEventListener('click', handleClickOutside); - window.addEventListener('resize', updatePosition); + resizeObserver.observe(document.documentElement); }); onUnmounted(() => { document.removeEventListener('click', handleClickOutside); - window.removeEventListener('resize', updatePosition); + resizeObserver.disconnect(); }); // Position search results below the input diff --git a/web_src/js/components/ViewFileTree.vue b/web_src/js/components/ViewFileTree.vue index dc131383f62..c3344c91e2d 100644 --- a/web_src/js/components/ViewFileTree.vue +++ b/web_src/js/components/ViewFileTree.vue @@ -3,7 +3,7 @@ import ViewFileTreeItem from './ViewFileTreeItem.vue'; import {onMounted, useTemplateRef, type ShallowRef} from 'vue'; import {createViewFileTreeStore} from './ViewFileTreeStore.ts'; -const elRoot = useTemplateRef('elRoot') as Readonly>;; +const elRoot = useTemplateRef('elRoot') as Readonly>; const props = defineProps({ repoLink: {type: String, required: true}, diff --git a/web_src/js/components/WorkflowGraph.vue b/web_src/js/components/WorkflowGraph.vue index 6853dfd26fa..78a6acad162 100644 --- a/web_src/js/components/WorkflowGraph.vue +++ b/web_src/js/components/WorkflowGraph.vue @@ -30,12 +30,12 @@ export type WorkflowGraphLocale = { graphResetView: string, }; -interface StoredState { +type StoredState = { scale: number; translateX: number; translateY: number; timestamp: number; -} +}; const props = defineProps<{ store: ActionRunViewStore; @@ -233,14 +233,14 @@ function isJobLinked(job: ActionsJob) { } function onNodeClick(job: GraphNode | ActionsJob, event: MouseEvent) { - const target = 'jobs' in job ? job.jobs[0]! : job; + const target = 'jobs' in job ? job.jobs[0] : job; if (!isJobLinked(target)) return; const link = `${props.runLink}/jobs/${target.id}`; if (event.ctrlKey || event.metaKey) { window.open(link, '_blank'); return; } - window.location.href = link; + window.location.assign(link); } diff --git a/web_src/js/features/repo-findfile.ts b/web_src/js/features/repo-findfile.ts index 7fe184cc51b..61e3b34b0fb 100644 --- a/web_src/js/features/repo-findfile.ts +++ b/web_src/js/features/repo-findfile.ts @@ -67,6 +67,8 @@ export function filterRepoFilesWeighted(files: Array, filter: string) { return filterResult; } +export type RepoFilesFilter = typeof filterRepoFilesWeighted; + export function initRepoFileSearch() { registerGlobalInitFunc('initRepoFileSearch', async (el) => { const {default: RepoFileSearch} = await import('../components/RepoFileSearch.vue'); @@ -76,6 +78,7 @@ export function initRepoFileSearch() { treeListUrl: el.getAttribute('data-tree-list-url'), noResultsText: el.getAttribute('data-no-results-text'), placeholder: el.getAttribute('data-placeholder'), + filterFiles: filterRepoFilesWeighted, }).mount(el); }); }