Skip to content
Clone
main
145 lines 5.4 KB
import type { ChangedFile, MergeMethod, PullState, ReviewerState, ReviewState, UserDto } from '@app/domains/pulls/model'
import type { DiffFile, DiffFileStatus } from '@app/domains/repo/model'
import { type MessageKey, t } from '@app/i18n'
/** UI-only filter: the backend understands open/closed/merged and no filter. */
export type PullFilter = PullState | 'all' | 'draft'
const PULL_STATE_KEYS: Record<PullState, MessageKey> = {
open: 'pull.state.open',
closed: 'pull.state.closed',
merged: 'pull.state.merged',
}
/** Lazy so badges follow the active locale. */
export const PULL_STATE_LABEL = new Proxy({} as Record<PullState, string>, {
get: (_target, key: PullState) => t(PULL_STATE_KEYS[key]),
})
/** tailwind-ish tone classes shared across badges (mirrors repo-history colors). */
export const TONE = {
green: 'text-[#1a7f37] bg-[rgba(35,165,90,0.12)]',
red: 'text-[#b42318] bg-[rgba(242,63,67,0.10)]',
amber: 'text-[#8a6100] bg-[rgba(240,178,50,0.16)]',
violet: 'text-[#6937c3] bg-[rgba(120,82,196,0.12)]',
gray: 'text-shell-ink-muted bg-shell-active',
} as const
const REVIEW_STATE_KEYS: Record<ReviewState, MessageKey> = {
approved: 'pull.reviewState.approved',
changes_requested: 'pull.reviewState.changesRequested',
commented: 'pull.reviewState.commented',
pending: 'pull.reviewState.pending',
dismissed: 'pull.reviewState.dismissed',
}
export const REVIEW_STATE_LABEL = new Proxy({} as Record<ReviewState, string>, {
get: (_target, key: ReviewState) => t(REVIEW_STATE_KEYS[key]),
})
export const REVIEW_STATE_TONE: Record<ReviewState, keyof typeof TONE> = {
approved: 'green',
changes_requested: 'red',
commented: 'gray',
pending: 'amber',
dismissed: 'gray',
}
const REVIEWER_STATE_KEYS: Record<ReviewerState, MessageKey> = {
requested: 'pull.reviewerState.requested',
approved: 'pull.reviewerState.approved',
changes_requested: 'pull.reviewerState.changesRequested',
commented: 'pull.reviewerState.commented',
dismissed: 'pull.reviewerState.dismissed',
}
export const REVIEWER_STATE_LABEL = new Proxy({} as Record<ReviewerState, string>, {
get: (_target, key: ReviewerState) => t(REVIEWER_STATE_KEYS[key]),
})
const MERGE_METHOD_KEYS: Record<MergeMethod, MessageKey> = {
merge: 'pull.mergeMethod.merge',
squash: 'pull.mergeMethod.squash',
rebase: 'pull.mergeMethod.rebase',
}
/** 合并方式的可选顺序(供 <select> 迭代;Proxy 标签对象本身没有可枚举键)。 */
export const MERGE_METHODS: MergeMethod[] = ['merge', 'squash', 'rebase']
export const MERGE_METHOD_LABEL = new Proxy({} as Record<MergeMethod, string>, {
get: (_target, key: MergeMethod) => t(MERGE_METHOD_KEYS[key]),
})
/** The backend formats status with `{:?}` (Debug names) — normalize loosely. */
export function normalizeFileStatus(raw: string): DiffFileStatus {
const s = raw.toLowerCase()
if (s.startsWith('ad') || s.startsWith('new') || s.startsWith('creat') || s === 'a') return 'added'
if (s.startsWith('del') || s.startsWith('rem') || s === 'd') return 'deleted'
if (s.startsWith('ren') || s.startsWith('mov') || s === 'r') return 'renamed'
if (s.startsWith('cop') || s === 'c') return 'copied'
if (s.startsWith('bin')) return 'binary'
return 'modified'
}
const EVENT_KEYS: Record<string, MessageKey> = {
created: 'pull.event.created',
updated: 'pull.event.updated',
title_changed: 'pull.event.titleChanged',
body_edited: 'pull.event.bodyEdited',
base_changed: 'pull.event.baseChanged',
marked_ready: 'pull.event.markedReady',
marked_draft: 'pull.event.markedDraft',
closed: 'pull.event.closed',
reopened: 'pull.event.reopened',
merged: 'pull.event.merged',
review_requested: 'pull.event.reviewRequested',
review_request_removed: 'pull.event.reviewRequestRemoved',
review_submitted: 'pull.event.reviewSubmitted',
approved: 'pull.event.approved',
changes_requested: 'pull.event.changesRequested',
comment: 'pull.event.comment',
labeled: 'pull.event.labeled',
unlabeled: 'pull.event.unlabeled',
assigned: 'pull.event.assigned',
unassigned: 'pull.event.unassigned',
locked: 'pull.event.locked',
unlocked: 'pull.event.unlocked',
force_push: 'pull.event.forcePush',
branch_updated: 'pull.event.branchUpdated',
head_ref_updated: 'pull.event.headRefUpdated',
auto_merge_enabled: 'pull.event.autoMergeEnabled',
auto_merge_disabled: 'pull.event.autoMergeDisabled',
}
/** Unknown backend event types keep rendering as their raw snake_case name. */
export const eventLabel = (eventType: string): string =>
EVENT_KEYS[eventType] ? t(EVENT_KEYS[eventType]) : eventType.replaceAll('_', ' ')
export const userName = (user?: UserDto | null): string =>
user?.display_name || user?.username || t('common.state.unknownUser')
export interface InlineAnchor {
path: string
line: number
side: 'left' | 'right'
}
export const anchorKey = (path: string, line: number, side: 'left' | 'right') => `${path}#${side}${line}`
/** A pinned inline comment as rendered under its diff line. */
export interface AnchoredComment {
id: string
body: string
}
export function changedFileToDiff(file: ChangedFile): DiffFile {
const status = file.binary ? 'binary' : normalizeFileStatus(file.path.status)
return {
path: file.path.new_path,
oldPath: file.path.old_path && file.path.old_path !== file.path.new_path ? file.path.old_path : null,
status,
raw: file.patch ?? '',
additions: file.lines_added,
deletions: file.lines_removed,
}
}