refactor: improve types in the frontend, misc fixes (#39142)

This commit is contained in:
silverwind
2026-08-31 04:12:25 -07:00
committed by GitHub
parent 535fc29ae8
commit 7c93ead628
46 changed files with 392 additions and 217 deletions
+12 -1
View File
@@ -25,6 +25,17 @@ function isLogElementInViewport(el: Element, {extraViewPortHeight}={extraViewPor
return 0 <= rect.bottom && rect.bottom <= window.innerHeight + extraViewPortHeight;
}
export type ActionRunJobViewLocale = {
status: Record<ActionsStatus, string>,
showTimeStamps: string,
showLogSeconds: string,
showFullScreen: string,
logsAlwaysAutoScroll: string,
logsAlwaysExpandRunning: string,
downloadLogs: string,
copyOutput: string,
};
type Step = {
summary: string,
duration: string,
@@ -84,7 +95,7 @@ const props = defineProps<{
store: ActionRunViewStore,
jobId: number;
actionsViewUrl: string;
locale: Record<string, any>;
locale: ActionRunJobViewLocale;
}>();
const store = props.store;
const {currentRun: run} = toRefs(store.viewData);
+12 -2
View File
@@ -1,16 +1,26 @@
<script setup lang="ts">
import WorkflowGraph from './WorkflowGraph.vue';
import WorkflowGraph, {type WorkflowGraphLocale} from './WorkflowGraph.vue';
import type {ActionRunViewStore} from './ActionRunView.ts';
import {computed, onBeforeUnmount, onMounted, toRefs} from 'vue';
import {trString} from '../modules/i18n.ts';
import type {ActionsStatus} from '../modules/gitea-actions.ts';
defineOptions({
name: 'ActionRunSummaryView',
});
export type ActionRunSummaryViewLocale = WorkflowGraphLocale & {
status: Record<ActionsStatus, string>,
statusLabel: string,
totalDuration: string,
artifactsTitle: string,
triggeredVia: string,
rerunTriggered: string,
};
const props = defineProps<{
store: ActionRunViewStore;
locale: Record<string, any>;
locale: ActionRunSummaryViewLocale;
artifactCount: number;
}>();
+3 -1
View File
@@ -4,6 +4,8 @@ import {getIssueColorClass, getIssueIcon} from '../features/issue.ts';
import {computed} from 'vue';
import type {Issue} from '../types.ts';
const {appSubUrl} = window.config;
const props = defineProps<{
issue?: Issue | null,
renderedLabels?: string,
@@ -26,7 +28,7 @@ const body = computed(() => {
<div class="tw-p-4">
<div v-if="issue" class="tw-flex tw-flex-col tw-gap-2">
<div class="tw-text-12">
<a :href="issue.repository.html_url" class="muted">{{ issue.repository.full_name }}</a>
<a :href="`${appSubUrl}/${issue.repository.full_name}`" class="muted">{{ issue.repository.full_name }}</a>
on {{ createdAt }}
</div>
<div class="flex-text-block">
+11 -2
View File
@@ -24,6 +24,15 @@ type DashboardRepo = {
type CommitStatus = 'pending' | 'success' | 'error' | 'failure' | 'warning' | 'skipped';
type WebSearchRepo = {
repository: DashboardRepo,
latest_commit_status: {
State: CommitStatus,
TargetURL: string,
} | null,
locale_latest_commit_status: string,
};
type CommitStatusMap = {
[status in CommitStatus]: {
name: SvgName,
@@ -263,7 +272,7 @@ async function searchRepos() {
const searchedURL = searchURL.value;
const searchedQuery = searchQuery.value;
let response: Response, json: any;
let response: Response, json: {data: WebSearchRepo[]};
try {
const firstLoad = reposTotalCount.value === null;
// independent of the search, so both requests go out together
@@ -293,7 +302,7 @@ async function searchRepos() {
}
if (searchedURL === searchURL.value) {
repos.value = json.data.map((webSearchRepo: any) => {
repos.value = json.data.map((webSearchRepo) => {
return {
...webSearchRepo.repository,
latest_commit_status_state: webSearchRepo.latest_commit_status?.State, // if latest_commit_status is null, it means there is no commit status
+43 -15
View File
@@ -3,27 +3,55 @@ import {computed, onMounted, onUnmounted, shallowRef, watch} from 'vue';
import SvgIcon from './SvgIcon.vue';
import {toggleElem} from '../utils/dom.ts';
type MergeStyle = {
name: string,
allowed: boolean,
textDoMerge: string,
mergeTitleFieldText?: string,
mergeMessageFieldText?: string,
hideMergeMessageTexts?: boolean,
hideAutoMerge: boolean,
};
type MergeForm = {
allOverridableChecksOk: boolean,
baseLink: string,
canMergeNow: boolean,
defaultDeleteBranchAfterMerge: boolean,
defaultMergeMessage: string,
defaultMergeStyle: string,
emptyCommit: boolean,
hasPendingPullRequestMerge: boolean,
hasPendingPullRequestMergeTip: string,
isPullBranchDeletable: boolean,
mergeMessageFieldPlaceHolder: string,
mergeStyles: MergeStyle[],
pullHeadCommitID: string,
textAutoMergeButtonWhenSucceed: string,
textAutoMergeCancelSchedule: string,
textAutoMergeWhenSucceed: string,
textCancel: string,
textClearMergeMessage: string,
textClearMergeMessageHint: string,
textDeleteBranch: string,
textMergeCommitId: string,
};
const props = defineProps<{
mergeFormProps: any, // TODO: this is a huge object, need to be refactored in the future
mergeFormProps: MergeForm,
}>();
const mergeStyleManuallyMerged = 'manually-merged';
const mergeForm = props.mergeFormProps;
const mergeTitleFieldValue = shallowRef('');
const mergeMessageFieldValue = shallowRef('');
const mergeTitleFieldValue = shallowRef<string | undefined>('');
const mergeMessageFieldValue = shallowRef<string | undefined>('');
const deleteBranchAfterMerge = shallowRef(false);
const autoMergeWhenSucceed = shallowRef(false);
const mergeStyle = shallowRef('');
const mergeStyleDetail = shallowRef({
hideMergeMessageTexts: false,
textDoMerge: '',
mergeTitleFieldText: '',
mergeMessageFieldText: '',
hideAutoMerge: false,
});
const mergeStyleDetail = shallowRef<MergeStyle>({name: '', allowed: false, textDoMerge: '', hideAutoMerge: false});
const mergeStyleAllowedCount = shallowRef(0);
@@ -48,18 +76,18 @@ const forceMerge = computed(() => {
});
watch(mergeStyle, (val) => {
mergeStyleDetail.value = mergeForm.mergeStyles.find((e: any) => e.name === val);
mergeStyleDetail.value = mergeForm.mergeStyles.find((e) => e.name === val)!;
for (const elem of document.querySelectorAll('[data-pull-merge-style]')) {
toggleElem(elem, elem.getAttribute('data-pull-merge-style') === val);
}
});
onMounted(() => {
mergeStyleAllowedCount.value = mergeForm.mergeStyles.reduce((v: any, msd: any) => v + (msd.allowed ? 1 : 0), 0);
mergeStyleAllowedCount.value = mergeForm.mergeStyles.reduce((v, msd) => v + (msd.allowed ? 1 : 0), 0);
let mergeStyle = mergeForm.mergeStyles.find((e: any) => e.allowed && e.name === mergeForm.defaultMergeStyle)?.name;
if (!mergeStyle) mergeStyle = mergeForm.mergeStyles.find((e: any) => e.allowed)?.name;
switchMergeStyle(mergeStyle, !mergeForm.canMergeNow);
let mergeStyle = mergeForm.mergeStyles.find((e) => e.allowed && e.name === mergeForm.defaultMergeStyle)?.name;
if (!mergeStyle) mergeStyle = mergeForm.mergeStyles.find((e) => e.allowed)?.name;
if (mergeStyle) switchMergeStyle(mergeStyle, !mergeForm.canMergeNow);
document.addEventListener('mouseup', hideMergeStyleMenu);
});
+28 -3
View File
@@ -4,8 +4,8 @@ import ActionStatusIcon from './ActionStatusIcon.vue';
import {computed, onBeforeUnmount, ref, toRefs, watch} from 'vue';
import {resetActionFavicon, syncActionRunFavicon} from '../modules/favicon-status.ts';
import {POST, DELETE} from '../modules/fetch.ts';
import ActionRunSummaryView from './ActionRunSummaryView.vue';
import ActionRunJobView from './ActionRunJobView.vue';
import ActionRunSummaryView, {type ActionRunSummaryViewLocale} from './ActionRunSummaryView.vue';
import ActionRunJobView, {type ActionRunJobViewLocale} from './ActionRunJobView.vue';
import type {ActionsJob, ActionsRunAttempt} from '../modules/gitea-actions.ts';
import {buildJobsByParentJobID, createActionRunViewStore} from './ActionRunView.ts';
import {buildArtifactTooltipHtml} from './ActionRunArtifacts.ts';
@@ -15,10 +15,35 @@ defineOptions({
name: 'RepoActionView',
});
type RepoActionViewLocale = ActionRunSummaryViewLocale & ActionRunJobViewLocale & {
approve: string,
cancel: string,
rerun: string,
rerun_all: string,
rerun_failed: string,
latest: string,
latestAttempt: string,
attempt: string,
summary: string,
allJobs: string,
jobSummaries: string,
expandCallerJobs: string,
collapseCallerJobs: string,
backToPullRequest: string,
backToWorkflow: string,
artifactExpired: string,
artifactExpiresAt: string,
artifactExpiredAt: string,
confirmDeleteArtifact: string,
workflowFile: string,
workflowFileNoPermission: string,
runDetails: string,
};
const props = defineProps<{
jobId: number;
actionsViewUrl: string;
locale: Record<string, any>;
locale: RepoActionViewLocale;
}>();
const locale = props.locale;
@@ -15,15 +15,7 @@ const colors = shallowRef({
textAltColor: 'white',
});
type ActivityAuthorData = {
avatar_link: string;
commits: number;
home_link: string;
login: string;
name: string;
}
const activityTopAuthors: Array<ActivityAuthorData> = window.config.pageData.repoActivityTopAuthors || [];
const activityTopAuthors = window.config.pageData.repoActivityTopAuthors || [];
const graphWidth = activityTopAuthors.length * barSlotWidth;
const maxCommits = Math.max(...activityTopAuthors.map((author) => author.commits));
+52 -33
View File
@@ -20,6 +20,8 @@ import {
startDaysBetween,
firstStartDateAfterDate,
fillEmptyStartDaysWithZeroes,
type DayData,
type DayDataObject,
} from '../utils/time.ts';
import {errorMessage} from '../modules/errors.ts';
import {sleep} from '../utils.ts';
@@ -67,12 +69,22 @@ function roundUpMax(maxValue: number) {
return Math.ceil(coefficient) * 10 ** exp;
}
type ContributorsData = {
total: {
weeks: Record<string, any>,
},
[other: string]: Record<string, Record<string, any>>,
}
type ContributorInfo = {
name: string,
avatar_link: string,
home_link: string,
};
type ContributorStats = ContributorInfo & {weeks: DayData[]};
type Contributor = ContributorStats & {
email: string,
total_commits: number,
total_additions: number,
total_deletions: number,
max_contribution_type: number,
};
type ContributorsData = Record<string, ContributorInfo & {weeks: DayDataObject}>;
const props = defineProps<{
locale: {
@@ -89,10 +101,10 @@ const props = defineProps<{
const isLoading = shallowRef(false);
const errorText = shallowRef('');
const totalStats = shallowRef<Record<string, any>>({});
const sortedContributors = shallowRef<Array<Record<string, any>>>([]);
const totalStats = shallowRef<DayData[]>([]);
const sortedContributors = shallowRef<Contributor[]>([]);
const type = shallowRef<ContributionType>('commits');
let contributorsStats: Record<string, any> = {};
let contributorsStats: Record<string, ContributorStats> = {};
// plain values, so the main chart options do not follow the zoomed range
let xAxisStart: number | null = null;
let xAxisEnd: number | null = null;
@@ -113,7 +125,7 @@ onMounted(() => {
});
function sortContributors() {
const criteria = `total_${type.value}`;
const criteria = `total_${type.value}` as const;
sortedContributors.value = filterContributorWeeksByDateRange()
.filter((contributor) => contributor[criteria] !== 0)
.sort((a, b) => b[criteria] - a[criteria])
@@ -142,23 +154,22 @@ async function fetchGraphData() {
}
} while (response.status === 202);
if (response.ok) {
const data = await response.json() as ContributorsData;
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
total.weeks = Object.fromEntries(Object.entries(total.weeks).sort());
const totalWeeks = Object.fromEntries(Object.entries(total.weeks).sort());
const weekValues = Object.values(total.weeks);
const weekValues = Object.values(totalWeeks);
xAxisStart = weekValues[0].week;
xAxisEnd = firstStartDateAfterDate(new Date());
const startDays = startDaysBetween(xAxisStart, xAxisEnd);
total.weeks = fillEmptyStartDaysWithZeroes(startDays, total.weeks);
xAxisMin.value = xAxisStart;
xAxisMax.value = xAxisEnd;
contributorsStats = Object.fromEntries(Object.entries(other).map(([email, user]) => {
return [email, {...user, weeks: fillEmptyStartDaysWithZeroes(startDays, user.weeks)}];
}));
sortContributors();
totalStats.value = total;
totalStats.value = fillEmptyStartDaysWithZeroes(startDays, totalWeeks);
errorText.value = '';
} else {
errorText.value = response.statusText;
@@ -171,22 +182,22 @@ async function fetchGraphData() {
}
function filterContributorWeeksByDateRange() {
const filteredData: Array<Record<string, any>> = [];
const filteredData: Contributor[] = [];
const minTime = xAxisMin.value! - oneWeek;
const maxTime = xAxisMax.value! + oneWeek;
const contributionType = type.value;
for (const [key, user] of Object.entries(contributorsStats)) {
user.total_commits = 0;
user.total_additions = 0;
user.total_deletions = 0;
user.max_contribution_type = 0;
const filteredWeeks = user.weeks.filter((week: Record<string, number>) => {
let totalCommits = 0;
let totalAdditions = 0;
let totalDeletions = 0;
let maxContributionType = 0;
const filteredWeeks = user.weeks.filter((week) => {
if (week.week >= minTime && week.week <= maxTime) {
user.total_commits += week.commits;
user.total_additions += week.additions;
user.total_deletions += week.deletions;
if (week[contributionType] > user.max_contribution_type) {
user.max_contribution_type = week[contributionType];
totalCommits += week.commits;
totalAdditions += week.additions;
totalDeletions += week.deletions;
if (week[contributionType] > maxContributionType) {
maxContributionType = week[contributionType];
}
return true;
}
@@ -194,24 +205,32 @@ function filterContributorWeeksByDateRange() {
});
// this line is required. See https://github.com/sahinakkaya/gitea/pull/3#discussion_r1396495722
// for details.
user.max_contribution_type += 1;
maxContributionType += 1;
filteredData.push({...user, weeks: filteredWeeks, email: key});
filteredData.push({
...user,
weeks: filteredWeeks,
total_commits: totalCommits,
total_additions: totalAdditions,
total_deletions: totalDeletions,
max_contribution_type: maxContributionType,
email: key,
});
}
return filteredData;
}
const maxMainGraph = computed(() => {
return roundUpMax(Math.max(...totalStats.value.weeks.map((o: Record<string, any>) => o[type.value])));
return roundUpMax(Math.max(...totalStats.value.map((o) => o[type.value])));
});
// one shared maximum, otherwise the contributor graphs cannot be compared
const maxContributorGraph = computed(() => {
return roundUpMax(Math.max(...sortedContributors.value.map((c: Record<string, any>) => c.max_contribution_type)));
return roundUpMax(Math.max(...sortedContributors.value.map((c) => c.max_contribution_type)));
});
function toGraphData(data: Array<Record<string, any>>): ChartData<'line'> {
function toGraphData(data: DayData[]): ChartData<'line'> {
const contributionType = type.value;
return {
datasets: [
@@ -319,7 +338,7 @@ function getOptions(chartType: ChartType): LineOptions {
}
const mainChart = computed(() => ({
graphData: toGraphData(totalStats.value.weeks),
graphData: toGraphData(totalStats.value),
chartOptions: getOptions('main'),
}));
@@ -390,7 +409,7 @@ const contributorCharts = computed(() => sortedContributors.value.map((contribut
</div>
</div>
<ChartCanvas
v-if="Object.keys(totalStats).length !== 0"
v-if="totalStats.length"
type="line" :data="mainChart.graphData" :options="mainChart.chartOptions"
/>
</div>
+13 -1
View File
@@ -18,6 +18,18 @@ import {
type RoutedEdge,
} from './WorkflowGraph.utils.ts';
export type WorkflowGraphLocale = {
graphJobsCount1: string,
graphJobsCountN: string,
graphDependenciesCount1: string,
graphDependenciesCountN: string,
graphSuccessRate: string,
graphZoomIn: string,
graphZoomMax: string,
graphZoomOut: string,
graphResetView: string,
};
interface StoredState {
scale: number;
translateX: number;
@@ -32,7 +44,7 @@ const props = defineProps<{
workflowId: string;
workflowLink?: string;
triggerEvent?: string;
locale: Record<string, string>;
locale: WorkflowGraphLocale;
}>();
const settingKeyStates = 'actions-graph-states';