🐛 fix(vscode): 申请移交改到工单详情标题栏
This commit is contained in:
1 parent
b53b989bbe
commit
d15c6ceedd
6 files changed
+86
-95
No files matched your search
@@ -90,7 +90,7 @@ codex/YYYY/MM/DD/rollout-*.jsonl 审查会话(存在才有)
|
|||||||
|
|
||||||
进度 toast 从核对分支开始,同一条提示随步骤改文案(核对分支、检查会话大小、压缩检查、关闭终端、收集会话、打包、上传、写入状态、指派、清理本机)。移交结束前不自动消失。
|
进度 toast 从核对分支开始,同一条提示随步骤改文案(核对分支、检查会话大小、压缩检查、关闭终端、收集会话、打包、上传、写入状态、指派、清理本机)。移交结束前不自动消失。
|
||||||
|
|
||||||
审查列里,负责人不是自己、也没有待接管包的 Gitea 工单,卡片上可以「申请移交」。同一张卡同时只有一条申请。同事收到提示,卡片上可以同意或拒绝。同意走上面的移交,对象就是申请人;申请人仍自己点接管。拒绝后申请消失,申请人看到被拒绝。申请人可以在回应前取消。同意后若移交失败,申请留着,两边都看到原因,修好可以再点同意。工单离开审查列时清掉未处理的申请。共享字段 `handoffRequestFrom` / `handoffRequestError`(拒绝时 error 为 `rejected`)。
|
审查列里,负责人不是自己、也没有待接管包的 Gitea 工单,工单详情标题栏可以「申请移交」。同一张工单同时只有一条申请。同事收到提示,打开工单详情可以同意或拒绝。同意走上面的移交,对象就是申请人;申请人仍自己点接管。拒绝后申请消失,申请人看到被拒绝。申请人可以在回应前取消。同意后若移交失败,申请留着,两边都看到原因,修好可以再点同意。工单离开审查列时清掉未处理的申请。共享字段 `handoffRequestFrom` / `handoffRequestError`(拒绝时 error 为 `rejected`)。
|
||||||
|
|
||||||
回滚:大小检查失败 → 什么都不改;打包/上传失败 → toast 终止(终端已关,无其他改动);写状态失败 → 删附件;指派失败 → 清回两个共享字段、删附件。结束 `toast/dismiss`。
|
回滚:大小检查失败 → 什么都不改;打包/上传失败 → toast 终止(终端已关,无其他改动);写状态失败 → 删附件;指派失败 → 清回两个共享字段、删附件。结束 `toast/dismiss`。
|
||||||
|
|
||||||
|
|||||||
@@ -393,10 +393,6 @@ export function App() {
|
|||||||
onDependencyClear={clearDependency}
|
onDependencyClear={clearDependency}
|
||||||
me={me}
|
me={me}
|
||||||
avatars={avatars}
|
avatars={avatars}
|
||||||
onAskHandoff={askHandoff}
|
|
||||||
onCancelHandoffRequest={cancelHandoffRequest}
|
|
||||||
onAcceptHandoffRequest={acceptHandoffRequest}
|
|
||||||
onRejectHandoffRequest={rejectHandoffRequest}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="h-[40vh] shrink-0">
|
<div className="h-[40vh] shrink-0">
|
||||||
@@ -441,6 +437,10 @@ export function App() {
|
|||||||
requestHandoffUsers(n)
|
requestHandoffUsers(n)
|
||||||
}}
|
}}
|
||||||
onAcceptHandoff={acceptHandoff}
|
onAcceptHandoff={acceptHandoff}
|
||||||
|
onAskHandoff={askHandoff}
|
||||||
|
onCancelHandoffRequest={cancelHandoffRequest}
|
||||||
|
onAcceptHandoffRequest={acceptHandoffRequest}
|
||||||
|
onRejectHandoffRequest={rejectHandoffRequest}
|
||||||
isHandoffRunning={isHandoffRunning}
|
isHandoffRunning={isHandoffRunning}
|
||||||
profileData={profileData}
|
profileData={profileData}
|
||||||
onProfileSave={saveProfiles}
|
onProfileSave={saveProfiles}
|
||||||
|
|||||||
@@ -62,6 +62,10 @@ interface BottomTabsProps {
|
|||||||
avatars?: Record<string, string>
|
avatars?: Record<string, string>
|
||||||
onStartHandoff: (issueNumber: number) => void
|
onStartHandoff: (issueNumber: number) => void
|
||||||
onAcceptHandoff: (issueNumber: number) => void
|
onAcceptHandoff: (issueNumber: number) => void
|
||||||
|
onAskHandoff: (issueNumber: number) => void
|
||||||
|
onCancelHandoffRequest: (issueNumber: number) => void
|
||||||
|
onAcceptHandoffRequest: (issueNumber: number) => void
|
||||||
|
onRejectHandoffRequest: (issueNumber: number) => void
|
||||||
isHandoffRunning: (issueNumber: number) => boolean
|
isHandoffRunning: (issueNumber: number) => boolean
|
||||||
|
|
||||||
// Profile tab props
|
// Profile tab props
|
||||||
@@ -166,6 +170,10 @@ export function BottomTabs(props: BottomTabsProps) {
|
|||||||
avatars={props.avatars}
|
avatars={props.avatars}
|
||||||
onStartHandoff={props.onStartHandoff}
|
onStartHandoff={props.onStartHandoff}
|
||||||
onAcceptHandoff={props.onAcceptHandoff}
|
onAcceptHandoff={props.onAcceptHandoff}
|
||||||
|
onAskHandoff={props.onAskHandoff}
|
||||||
|
onCancelHandoffRequest={props.onCancelHandoffRequest}
|
||||||
|
onAcceptHandoffRequest={props.onAcceptHandoffRequest}
|
||||||
|
onRejectHandoffRequest={props.onRejectHandoffRequest}
|
||||||
isHandoffRunning={props.isHandoffRunning}
|
isHandoffRunning={props.isHandoffRunning}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import type { ReactNode, Ref } from 'react'
|
import type { Ref } from 'react'
|
||||||
import type { Issue } from '../types'
|
import type { Issue } from '../types'
|
||||||
import { useSortable } from '@dnd-kit/sortable'
|
import { useSortable } from '@dnd-kit/sortable'
|
||||||
import { CSS } from '@dnd-kit/utilities'
|
import { CSS } from '@dnd-kit/utilities'
|
||||||
import { Lock } from 'lucide-react'
|
import { Lock } from 'lucide-react'
|
||||||
import { forwardRef } from 'react'
|
import { forwardRef } from 'react'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
import { canAnswerHandoffRequest, canAskHandoff, canCancelHandoffRequest } from '../../../src/panel/handlers/handoffRequest'
|
|
||||||
import { Card } from './ui/card'
|
import { Card } from './ui/card'
|
||||||
|
|
||||||
interface IssueCardProps {
|
interface IssueCardProps {
|
||||||
@@ -27,11 +26,6 @@ interface IssueCardProps {
|
|||||||
ownerLogin?: string
|
ownerLogin?: string
|
||||||
/** 归属人头像 data URI;缺失时回退首字母圆点。 */
|
/** 归属人头像 data URI;缺失时回退首字母圆点。 */
|
||||||
ownerAvatar?: string
|
ownerAvatar?: string
|
||||||
me?: string
|
|
||||||
onAskHandoff?: (issueNumber: number) => void
|
|
||||||
onCancelHandoffRequest?: (issueNumber: number) => void
|
|
||||||
onAcceptHandoffRequest?: (issueNumber: number) => void
|
|
||||||
onRejectHandoffRequest?: (issueNumber: number) => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 同事工单的归属人标记:有头像用头像,没有就用 login 首字母。 */
|
/** 同事工单的归属人标记:有头像用头像,没有就用 login 首字母。 */
|
||||||
@@ -57,69 +51,8 @@ function OwnerBadge({ login, avatar }: { login: string, avatar?: string }) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function HandoffRequestActions({
|
|
||||||
issue,
|
|
||||||
me,
|
|
||||||
onAsk,
|
|
||||||
onCancel,
|
|
||||||
onAccept,
|
|
||||||
onReject,
|
|
||||||
}: {
|
|
||||||
issue: Issue
|
|
||||||
me?: string
|
|
||||||
onAsk?: (issueNumber: number) => void
|
|
||||||
onCancel?: (issueNumber: number) => void
|
|
||||||
onAccept?: (issueNumber: number) => void
|
|
||||||
onReject?: (issueNumber: number) => void
|
|
||||||
}) {
|
|
||||||
const button = 'rounded border border-[var(--vscode-contrastBorder)] px-1.5 py-0.5 text-[10px] hover:bg-[var(--vscode-toolbar-hoverBackground)]'
|
|
||||||
let body: ReactNode = null
|
|
||||||
if (canAnswerHandoffRequest(issue, me)) {
|
|
||||||
body = (
|
|
||||||
<>
|
|
||||||
<span className="text-[10px] opacity-80">
|
|
||||||
{issue.handoffRequestFrom}
|
|
||||||
{' '}
|
|
||||||
想要
|
|
||||||
</span>
|
|
||||||
{issue.handoffRequestError && issue.handoffRequestError !== 'rejected'
|
|
||||||
? <span className="text-[10px] text-[var(--vscode-errorForeground)]">{issue.handoffRequestError}</span>
|
|
||||||
: null}
|
|
||||||
<button type="button" className={button} onClick={() => onAccept?.(issue.number)}>同意</button>
|
|
||||||
<button type="button" className={button} onClick={() => onReject?.(issue.number)}>拒绝</button>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
else if (canCancelHandoffRequest(issue, me)) {
|
|
||||||
body = (
|
|
||||||
<>
|
|
||||||
<span className="text-[10px] opacity-80">
|
|
||||||
{issue.handoffRequestError && issue.handoffRequestError !== 'rejected' ? issue.handoffRequestError : '已申请'}
|
|
||||||
</span>
|
|
||||||
<button type="button" className={button} onClick={() => onCancel?.(issue.number)}>取消</button>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
else if (canAskHandoff(issue, me)) {
|
|
||||||
body = <button type="button" className={button} onClick={() => onAsk?.(issue.number)}>申请移交</button>
|
|
||||||
}
|
|
||||||
else if (issue.column === 'review' && issue.handoffRequestFrom && issue.source !== 'youtrack' && !issue.handoffAttachmentId) {
|
|
||||||
body = <span className="text-[10px] opacity-60">申请中</span>
|
|
||||||
}
|
|
||||||
if (!body)
|
|
||||||
return null
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="mb-1 flex flex-wrap items-center gap-1"
|
|
||||||
onPointerDown={event => event.stopPropagation()}
|
|
||||||
>
|
|
||||||
{body}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const IssueCard = forwardRef<HTMLDivElement, IssueCardProps>(
|
export const IssueCard = forwardRef<HTMLDivElement, IssueCardProps>(
|
||||||
({ issue, selected, onSelect, depth = 0, locked, lockedPrerequisite, readOnly, ownerLogin, ownerAvatar, me, onAskHandoff, onCancelHandoffRequest, onAcceptHandoffRequest, onRejectHandoffRequest }, ref) => {
|
({ issue, selected, onSelect, depth = 0, locked, lockedPrerequisite, readOnly, ownerLogin, ownerAvatar }, ref) => {
|
||||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging }
|
const { attributes, listeners, setNodeRef, transform, transition, isDragging }
|
||||||
= useSortable({ id: issue.id, data: { column: issue.column }, disabled: readOnly })
|
= useSortable({ id: issue.id, data: { column: issue.column }, disabled: readOnly })
|
||||||
|
|
||||||
@@ -178,14 +111,6 @@ export const IssueCard = forwardRef<HTMLDivElement, IssueCardProps>(
|
|||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
: null}
|
: null}
|
||||||
<HandoffRequestActions
|
|
||||||
issue={issue}
|
|
||||||
me={me}
|
|
||||||
onAsk={onAskHandoff}
|
|
||||||
onCancel={onCancelHandoffRequest}
|
|
||||||
onAccept={onAcceptHandoffRequest}
|
|
||||||
onReject={onRejectHandoffRequest}
|
|
||||||
/>
|
|
||||||
<div className={cn('truncate', (locked || (readOnly && ownerLogin)) && 'pr-5', locked && readOnly && ownerLogin && 'pr-10')}>
|
<div className={cn('truncate', (locked || (readOnly && ownerLogin)) && 'pr-5', locked && readOnly && ownerLogin && 'pr-10')}>
|
||||||
<span className="font-mono opacity-60">
|
<span className="font-mono opacity-60">
|
||||||
{issue.source === 'youtrack' ? issue.externalId : `#${issue.number}`}
|
{issue.source === 'youtrack' ? issue.externalId : `#${issue.number}`}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { Issue, IssueColumn } from '../types'
|
|||||||
import type { PropertyGroup, RowAction } from './property-grid'
|
import type { PropertyGroup, RowAction } from './property-grid'
|
||||||
import { ArrowRightLeft, CircleSlash, Download, Eraser, ExternalLink, GitMerge, Loader2, Play, RotateCcw, Terminal, Trash2, X } from 'lucide-react'
|
import { ArrowRightLeft, CircleSlash, Download, Eraser, ExternalLink, GitMerge, Loader2, Play, RotateCcw, Terminal, Trash2, X } from 'lucide-react'
|
||||||
import { useMemo, useRef } from 'react'
|
import { useMemo, useRef } from 'react'
|
||||||
|
import { canAnswerHandoffRequest, canAskHandoff, canCancelHandoffRequest } from '../../../src/panel/handlers/handoffRequest'
|
||||||
import { isIssueLocked } from '../lib/dependencies'
|
import { isIssueLocked } from '../lib/dependencies'
|
||||||
import { isIssueMine, issueOwnerLogin } from '../lib/issueOwnership'
|
import { isIssueMine, issueOwnerLogin } from '../lib/issueOwnership'
|
||||||
import { isSessionMissing } from '../lib/missingSession'
|
import { isSessionMissing } from '../lib/missingSession'
|
||||||
@@ -134,6 +135,10 @@ interface IssueDetailPanelProps {
|
|||||||
me?: string
|
me?: string
|
||||||
onStartHandoff: (issueNumber: number) => void
|
onStartHandoff: (issueNumber: number) => void
|
||||||
onAcceptHandoff: (issueNumber: number) => void
|
onAcceptHandoff: (issueNumber: number) => void
|
||||||
|
onAskHandoff: (issueNumber: number) => void
|
||||||
|
onCancelHandoffRequest: (issueNumber: number) => void
|
||||||
|
onAcceptHandoffRequest: (issueNumber: number) => void
|
||||||
|
onRejectHandoffRequest: (issueNumber: number) => void
|
||||||
isHandoffRunning: (issueNumber: number) => boolean
|
isHandoffRunning: (issueNumber: number) => boolean
|
||||||
/** Gitea login → 头像 data URI;只读工单在标题旁贴归属人头像。 */
|
/** Gitea login → 头像 data URI;只读工单在标题旁贴归属人头像。 */
|
||||||
avatars?: Record<string, string>
|
avatars?: Record<string, string>
|
||||||
@@ -185,6 +190,10 @@ export function IssueDetailPanel({
|
|||||||
isSessionTabPending,
|
isSessionTabPending,
|
||||||
onStartHandoff,
|
onStartHandoff,
|
||||||
onAcceptHandoff,
|
onAcceptHandoff,
|
||||||
|
onAskHandoff,
|
||||||
|
onCancelHandoffRequest,
|
||||||
|
onAcceptHandoffRequest,
|
||||||
|
onRejectHandoffRequest,
|
||||||
isHandoffRunning,
|
isHandoffRunning,
|
||||||
avatars,
|
avatars,
|
||||||
}: IssueDetailPanelProps) {
|
}: IssueDetailPanelProps) {
|
||||||
@@ -847,6 +856,68 @@ export function IssueDetailPanel({
|
|||||||
<ExternalLink className="size-3.5" />
|
<ExternalLink className="size-3.5" />
|
||||||
{issue.source === 'youtrack' ? '在 YouTrack 打开' : '在 Gitea 打开'}
|
{issue.source === 'youtrack' ? '在 YouTrack 打开' : '在 Gitea 打开'}
|
||||||
</button>
|
</button>
|
||||||
|
{canAskHandoff(issue, me) && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
data-tooltip="让负责人把这个工单移交给你"
|
||||||
|
onClick={() => onAskHandoff(issue.number)}
|
||||||
|
className="inline-flex shrink-0 items-center gap-1 rounded border border-[var(--vscode-panel-border)] px-2 py-1 text-xs hover:bg-black/10 dark:hover:bg-white/10"
|
||||||
|
>
|
||||||
|
申请移交
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{canCancelHandoffRequest(issue, me) && (
|
||||||
|
<>
|
||||||
|
<span
|
||||||
|
className="max-w-[16rem] truncate text-xs opacity-70"
|
||||||
|
data-tooltip={issue.handoffRequestError && issue.handoffRequestError !== 'rejected' ? issue.handoffRequestError : '等对方同意'}
|
||||||
|
>
|
||||||
|
{issue.handoffRequestError && issue.handoffRequestError !== 'rejected' ? issue.handoffRequestError : '已申请'}
|
||||||
|
</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => onCancelHandoffRequest(issue.number)}
|
||||||
|
className="inline-flex shrink-0 items-center gap-1 rounded border border-[var(--vscode-panel-border)] px-2 py-1 text-xs hover:bg-black/10 dark:hover:bg-white/10"
|
||||||
|
>
|
||||||
|
取消申请
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{canAnswerHandoffRequest(issue, me) && (
|
||||||
|
<>
|
||||||
|
<span className="shrink-0 text-xs opacity-80">
|
||||||
|
{issue.handoffRequestFrom}
|
||||||
|
{' '}
|
||||||
|
想要这个工单
|
||||||
|
</span>
|
||||||
|
{issue.handoffRequestError && issue.handoffRequestError !== 'rejected'
|
||||||
|
? (
|
||||||
|
<span
|
||||||
|
className="max-w-[16rem] truncate text-xs text-[var(--vscode-errorForeground)]"
|
||||||
|
data-tooltip={issue.handoffRequestError}
|
||||||
|
>
|
||||||
|
{issue.handoffRequestError}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
: null}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={isHandoffRunning(issue.number)}
|
||||||
|
onClick={() => onAcceptHandoffRequest(issue.number)}
|
||||||
|
className="inline-flex shrink-0 items-center gap-1 rounded border border-green-500/60 bg-green-500/10 px-2 py-1 text-xs text-green-500 hover:bg-green-500/20 disabled:opacity-50"
|
||||||
|
>
|
||||||
|
{isHandoffRunning(issue.number) ? <Loader2 className="size-3.5 animate-spin" /> : null}
|
||||||
|
同意
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => onRejectHandoffRequest(issue.number)}
|
||||||
|
className="inline-flex shrink-0 items-center gap-1 rounded border border-[var(--vscode-panel-border)] px-2 py-1 text-xs hover:bg-black/10 dark:hover:bg-white/10"
|
||||||
|
>
|
||||||
|
拒绝
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
{/* Close/delete write to gitea by number — hide for youtrack cards to
|
{/* Close/delete write to gitea by number — hide for youtrack cards to
|
||||||
avoid acting on a gitea issue of the same number. */}
|
avoid acting on a gitea issue of the same number. */}
|
||||||
{issue.source !== 'youtrack' && !readOnly && (
|
{issue.source !== 'youtrack' && !readOnly && (
|
||||||
|
|||||||
@@ -45,10 +45,6 @@ interface KanbanBoardProps {
|
|||||||
me?: string
|
me?: string
|
||||||
/** Gitea login → 头像 data URI。 */
|
/** Gitea login → 头像 data URI。 */
|
||||||
avatars?: Record<string, string>
|
avatars?: Record<string, string>
|
||||||
onAskHandoff?: (issueNumber: number) => void
|
|
||||||
onCancelHandoffRequest?: (issueNumber: number) => void
|
|
||||||
onAcceptHandoffRequest?: (issueNumber: number) => void
|
|
||||||
onRejectHandoffRequest?: (issueNumber: number) => void
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const kanbanCollisionDetection: CollisionDetection = (args) => {
|
const kanbanCollisionDetection: CollisionDetection = (args) => {
|
||||||
@@ -123,10 +119,6 @@ export function KanbanBoard({
|
|||||||
onDependencyClear,
|
onDependencyClear,
|
||||||
me,
|
me,
|
||||||
avatars,
|
avatars,
|
||||||
onAskHandoff,
|
|
||||||
onCancelHandoffRequest,
|
|
||||||
onAcceptHandoffRequest,
|
|
||||||
onRejectHandoffRequest,
|
|
||||||
}: KanbanBoardProps) {
|
}: KanbanBoardProps) {
|
||||||
const [activeId, setActiveId] = useState<string | null>(null)
|
const [activeId, setActiveId] = useState<string | null>(null)
|
||||||
|
|
||||||
@@ -289,11 +281,6 @@ export function KanbanBoard({
|
|||||||
readOnly={readOnly}
|
readOnly={readOnly}
|
||||||
ownerLogin={ownerLogin}
|
ownerLogin={ownerLogin}
|
||||||
ownerAvatar={ownerLogin ? avatars?.[ownerLogin] : undefined}
|
ownerAvatar={ownerLogin ? avatars?.[ownerLogin] : undefined}
|
||||||
me={me}
|
|
||||||
onAskHandoff={onAskHandoff}
|
|
||||||
onCancelHandoffRequest={onCancelHandoffRequest}
|
|
||||||
onAcceptHandoffRequest={onAcceptHandoffRequest}
|
|
||||||
onRejectHandoffRequest={onRejectHandoffRequest}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|||||||
Reference in new issue
Block a user