✨ feat(vscode): 头脑风暴会话一键续聊,终端开着直接打进去、关了带提示词恢复
详情面板「头脑风暴会话id」行的 ▶ 改为续聊:规划终端还开着就把续聊提示词 打进去;终端关了但有会话 id 就 `claude --resume <id>` 带提示词恢复;没会话 才新开。属性行右侧按钮改为数组,续聊与关闭 tab 可并存。 Claude-Session: https://claude.ai/code/session_01KDcq9HcrVLs2c9tkRxasub
This commit is contained in:
1 parent
90600cf070
commit
ae875756a9
15 files changed
+307
-297
No files matched your search
@@ -44,7 +44,7 @@ interface BottomTabsProps {
|
||||
onCloseIssue: (issueNumber: number) => void
|
||||
onResetToTodo: (issueNumber: number) => void
|
||||
onCloseSessionTab: (issueNumber: number, kind: 'brainstorm' | 'implement' | 'review' | 'test') => void
|
||||
onStartBrainstormSession: (issueNumber: number) => void
|
||||
onContinueBrainstorm: (issueNumber: number) => void
|
||||
onUpdateAutoReview: (issueNumber: number, value: boolean) => void
|
||||
onUpdateProfilePath: (issueNumber: number, profilePath: string) => void
|
||||
onUpdateBrainstormProfilePath: (issueNumber: number, brainstormProfilePath: string) => void
|
||||
@@ -139,7 +139,7 @@ export function BottomTabs(props: BottomTabsProps) {
|
||||
onCloseIssue={props.onCloseIssue}
|
||||
onResetToTodo={props.onResetToTodo}
|
||||
onCloseSessionTab={props.onCloseSessionTab}
|
||||
onStartBrainstormSession={props.onStartBrainstormSession}
|
||||
onContinueBrainstorm={props.onContinueBrainstorm}
|
||||
onUpdateAutoReview={props.onUpdateAutoReview}
|
||||
onUpdateProfilePath={props.onUpdateProfilePath}
|
||||
onUpdateBrainstormProfilePath={props.onUpdateBrainstormProfilePath}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import type { ClaudeProfile } from '../hooks/useIssues'
|
||||
import type { Issue, IssueColumn } from '../types'
|
||||
import type { PropertyGroup } from './property-grid'
|
||||
import type { PropertyGroup, RowAction } from './property-grid'
|
||||
import { ArrowRightLeft, CircleSlash, Download, ExternalLink, GitMerge, Loader2, Play, RotateCcw, Terminal, Trash2, X } from 'lucide-react'
|
||||
import { useMemo, useRef } from 'react'
|
||||
import { isIssueLocked } from '../lib/dependencies'
|
||||
@@ -87,11 +86,10 @@ interface IssueDetailPanelProps {
|
||||
*/
|
||||
onCloseSessionTab: (issueNumber: number, kind: 'brainstorm' | 'implement' | 'review' | 'test') => void
|
||||
/**
|
||||
* Spawn a fresh "规划" cc tab for an issue whose sessionId is still empty
|
||||
* (created out-of-band via spx without webhook linking). Bound to the Play
|
||||
* button next to the "头脑风暴会话 id" row.
|
||||
* 「▶ 续聊」:由插件把续聊提示词送进头脑风暴会话——规划 tab 开着就直接
|
||||
* 打进去,关了就 `claude --resume <id>` 带着提示词恢复,没会话就新开一个。
|
||||
*/
|
||||
onStartBrainstormSession: (issueNumber: number) => void
|
||||
onContinueBrainstorm: (issueNumber: number) => void
|
||||
/** Persist a per-issue `autoReview` override into the state JSON. */
|
||||
onUpdateAutoReview: (issueNumber: number, value: boolean) => void
|
||||
/** 已知 Claude profile 列表,用于「配置文件」下拉选项。 */
|
||||
@@ -140,7 +138,7 @@ export function IssueDetailPanel({
|
||||
onCloseIssue,
|
||||
onResetToTodo,
|
||||
onCloseSessionTab,
|
||||
onStartBrainstormSession,
|
||||
onContinueBrainstorm,
|
||||
onUpdateAutoReview,
|
||||
profiles,
|
||||
onUpdateProfilePath,
|
||||
@@ -206,39 +204,39 @@ export function IssueDetailPanel({
|
||||
})),
|
||||
},
|
||||
(() => {
|
||||
// 头脑风暴会话 id 行有三种状态:
|
||||
// 1. sessionId 已存在 + brainstormTabOpen=true → 显示 id + X 关闭 tab
|
||||
// 2. sessionId 已存在 + brainstormTabOpen=false → 显示 id(无右侧按钮)
|
||||
// 3. sessionId 为空 + column !== 'done' → 显示 Play 按钮(启动一个新规划 cc tab)
|
||||
// 4. sessionId 为空 + column === 'done' → 占位 —,不显示按钮
|
||||
// 头脑风暴会话 id 行:
|
||||
// - 工单未完成 → 一直有 ▶ 续聊。插件把续聊提示词送进对应会话:
|
||||
// 规划终端开着就直接打进去,关了就 --resume 带提示词,没会话就新开。
|
||||
// - brainstormTabOpen → 再加一个 X 关闭 tab
|
||||
const hasSession = !!(issue?.sessionId && issue.sessionId.length > 0)
|
||||
const canStart = !hasSession && issue?.column !== 'done'
|
||||
let secondaryActionIcon: ReactNode | undefined
|
||||
let secondaryActionTitle: string | undefined
|
||||
let onSecondaryAction: (() => void) | undefined
|
||||
if (hasSession && issue?.brainstormTabOpen) {
|
||||
secondaryActionIcon = <X className="size-3.5" />
|
||||
secondaryActionTitle = '关闭头脑风暴 tab'
|
||||
onSecondaryAction = () => {
|
||||
if (issue)
|
||||
onCloseSessionTab(issue.number, 'brainstorm')
|
||||
}
|
||||
const secondaryActions: RowAction[] = []
|
||||
if (issue?.column !== 'done') {
|
||||
secondaryActions.push({
|
||||
icon: <Play className="size-3.5" />,
|
||||
title: '续聊:把续聊提示词发给头脑风暴会话',
|
||||
onClick: () => {
|
||||
if (issue)
|
||||
onContinueBrainstorm(issue.number)
|
||||
},
|
||||
})
|
||||
}
|
||||
else if (canStart) {
|
||||
secondaryActionIcon = <Play className="size-3.5" />
|
||||
secondaryActionTitle = '启动头脑风暴会话'
|
||||
onSecondaryAction = () => {
|
||||
if (issue)
|
||||
onStartBrainstormSession(issue.number)
|
||||
}
|
||||
if (hasSession && issue?.brainstormTabOpen) {
|
||||
secondaryActions.push({
|
||||
icon: <X className="size-3.5" />,
|
||||
title: '关闭头脑风暴 tab',
|
||||
onClick: () => {
|
||||
if (issue)
|
||||
onCloseSessionTab(issue.number, 'brainstorm')
|
||||
},
|
||||
})
|
||||
}
|
||||
return {
|
||||
key: 'sessionId',
|
||||
label: '头脑风暴会话id',
|
||||
type: 'action' as const,
|
||||
description: hasSession
|
||||
? '双击在新终端运行 claude --resume <id> 恢复对话'
|
||||
: '工单尚未关联 cc 会话;点击右侧 ▶ 启动一个新的规划 cc tab',
|
||||
? '双击在新终端运行 claude --resume <id> 恢复对话;点 ▶ 续聊'
|
||||
: '工单尚未关联 cc 会话;点 ▶ 新开一个规划会话并续聊',
|
||||
actionIcon: <Terminal className="size-3.5" />,
|
||||
onAction: (v: unknown) => {
|
||||
if (typeof v !== 'string' || v.length === 0)
|
||||
@@ -247,9 +245,7 @@ export function IssueDetailPanel({
|
||||
return
|
||||
onResumeSession(v, issue?.brainstormProfilePath, undefined, issue?.number)
|
||||
},
|
||||
secondaryActionIcon,
|
||||
secondaryActionTitle,
|
||||
onSecondaryAction,
|
||||
secondaryActions,
|
||||
}
|
||||
})(),
|
||||
// 实施会话即使 worktree 已被清掉也保留可双击的 resume 入口:
|
||||
@@ -271,14 +267,13 @@ export function IssueDetailPanel({
|
||||
return
|
||||
onResumeSession(v, issue?.profilePath, issue?.worktreePath, issue?.number)
|
||||
},
|
||||
secondaryActionIcon: issue?.implementTabOpen
|
||||
? <X className="size-3.5" />
|
||||
: undefined,
|
||||
secondaryActionTitle: '关闭实施 tab',
|
||||
onSecondaryAction: () => {
|
||||
if (issue)
|
||||
onCloseSessionTab(issue.number, 'implement')
|
||||
},
|
||||
secondaryActions: issue?.implementTabOpen
|
||||
? [{
|
||||
icon: <X className="size-3.5" />,
|
||||
title: '关闭实施 tab',
|
||||
onClick: () => onCloseSessionTab(issue.number, 'implement'),
|
||||
}]
|
||||
: [],
|
||||
},
|
||||
issue?.reviewSessionFileExists
|
||||
? {
|
||||
@@ -294,14 +289,13 @@ export function IssueDetailPanel({
|
||||
return
|
||||
onResumeReviewSession(v, issue.number, issue?.worktreePath)
|
||||
},
|
||||
secondaryActionIcon: issue?.reviewTabOpen
|
||||
? <X className="size-3.5" />
|
||||
: undefined,
|
||||
secondaryActionTitle: '关闭审查 tab',
|
||||
onSecondaryAction: () => {
|
||||
if (issue)
|
||||
onCloseSessionTab(issue.number, 'review')
|
||||
},
|
||||
secondaryActions: issue.reviewTabOpen
|
||||
? [{
|
||||
icon: <X className="size-3.5" />,
|
||||
title: '关闭审查 tab',
|
||||
onClick: () => onCloseSessionTab(issue.number, 'review'),
|
||||
}]
|
||||
: [],
|
||||
}
|
||||
: {
|
||||
key: 'reviewSessionId',
|
||||
@@ -318,24 +312,26 @@ export function IssueDetailPanel({
|
||||
// (prompt 依赖已合并的 PR 号,没有 PR 不显示启动按钮)
|
||||
const hasSession = !!(issue?.testSessionId && issue.testSessionId.length > 0)
|
||||
const canStart = !hasSession && !!issue?.pr
|
||||
let secondaryActionIcon: ReactNode | undefined
|
||||
let secondaryActionTitle: string | undefined
|
||||
let onSecondaryAction: (() => void) | undefined
|
||||
const secondaryActions: RowAction[] = []
|
||||
if (hasSession && issue?.testTabOpen) {
|
||||
secondaryActionIcon = <X className="size-3.5" />
|
||||
secondaryActionTitle = '关闭测试 tab'
|
||||
onSecondaryAction = () => {
|
||||
if (issue)
|
||||
onCloseSessionTab(issue.number, 'test')
|
||||
}
|
||||
secondaryActions.push({
|
||||
icon: <X className="size-3.5" />,
|
||||
title: '关闭测试 tab',
|
||||
onClick: () => {
|
||||
if (issue)
|
||||
onCloseSessionTab(issue.number, 'test')
|
||||
},
|
||||
})
|
||||
}
|
||||
else if (canStart) {
|
||||
secondaryActionIcon = <Play className="size-3.5" />
|
||||
secondaryActionTitle = '启动测试会话'
|
||||
onSecondaryAction = () => {
|
||||
if (issue)
|
||||
onStartTestSession(issue.number)
|
||||
}
|
||||
secondaryActions.push({
|
||||
icon: <Play className="size-3.5" />,
|
||||
title: '启动测试会话',
|
||||
onClick: () => {
|
||||
if (issue)
|
||||
onStartTestSession(issue.number)
|
||||
},
|
||||
})
|
||||
}
|
||||
return {
|
||||
key: 'testSessionId',
|
||||
@@ -352,9 +348,7 @@ export function IssueDetailPanel({
|
||||
return
|
||||
onResumeTestSession(v, issue.number)
|
||||
},
|
||||
secondaryActionIcon,
|
||||
secondaryActionTitle,
|
||||
onSecondaryAction,
|
||||
secondaryActions,
|
||||
}
|
||||
})(),
|
||||
{
|
||||
@@ -427,26 +421,27 @@ export function IssueDetailPanel({
|
||||
description: '双击文件名在编辑器打开;点击实施按钮启动实施流程;cc 通过 issue body 的 <!-- spx:plan=路径 --> 注释实时同步',
|
||||
onOpen: (p: string) => onOpenFile(p),
|
||||
// Hide the "实施" button entirely when there's no plan file.
|
||||
secondaryActionIcon: issue?.planFile
|
||||
? <Play className="size-3.5" />
|
||||
: undefined,
|
||||
secondaryActionTitle: locked ? lockTitle : '实施此计划',
|
||||
// Disable while running or already done. The user can still click
|
||||
// when status === 'failed' or never been run. Re-clicking while
|
||||
// 'running' is blocked at the UI to avoid registering duplicate
|
||||
// webhooks. To retry after a perceived failure with status still
|
||||
// 'running', edit the state-JSON comment manually for now.
|
||||
// 锁定态(前置工单未完成)同样禁止启动实施。
|
||||
secondaryDisabled:
|
||||
!issue?.planFile
|
||||
|| issue?.implementStatus === 'running'
|
||||
|| issue?.implementStatus === 'done'
|
||||
|| locked,
|
||||
onSecondaryAction: () => {
|
||||
if (!issue?.planFile)
|
||||
return
|
||||
onImplement(issue.number, issue.planFile, issue.profilePath, issue.sessionId)
|
||||
},
|
||||
secondaryActions: issue?.planFile
|
||||
? [{
|
||||
icon: <Play className="size-3.5" />,
|
||||
title: locked ? lockTitle : '实施此计划',
|
||||
// Disable while running or already done. The user can still click
|
||||
// when status === 'failed' or never been run. Re-clicking while
|
||||
// 'running' is blocked at the UI to avoid registering duplicate
|
||||
// webhooks. To retry after a perceived failure with status still
|
||||
// 'running', edit the state-JSON comment manually for now.
|
||||
// 锁定态(前置工单未完成)同样禁止启动实施。
|
||||
disabled:
|
||||
issue.implementStatus === 'running'
|
||||
|| issue.implementStatus === 'done'
|
||||
|| locked,
|
||||
onClick: () => {
|
||||
if (!issue?.planFile)
|
||||
return
|
||||
onImplement(issue.number, issue.planFile, issue.profilePath, issue.sessionId)
|
||||
},
|
||||
}]
|
||||
: [],
|
||||
},
|
||||
{
|
||||
key: 'pr',
|
||||
@@ -461,17 +456,19 @@ export function IssueDetailPanel({
|
||||
if (pr.length > 0)
|
||||
onOpenPr(pr)
|
||||
},
|
||||
secondaryActionIcon: issue?.pr && !issue?.prDiffFile
|
||||
? <Play className="size-3.5" />
|
||||
: undefined,
|
||||
secondaryActionTitle: prDiffSummaryRunning
|
||||
? 'PR 变更摘要生成中'
|
||||
: '生成 PR 变更摘要',
|
||||
secondaryDisabled: prDiffSummaryRunning,
|
||||
onSecondaryAction: () => {
|
||||
if (issue?.pr && !issue.prDiffFile && !prDiffSummaryRunning)
|
||||
onGeneratePrDiffSummary(issue.number)
|
||||
},
|
||||
secondaryActions: issue?.pr && !issue?.prDiffFile
|
||||
? [{
|
||||
icon: <Play className="size-3.5" />,
|
||||
title: prDiffSummaryRunning
|
||||
? 'PR 变更摘要生成中'
|
||||
: '生成 PR 变更摘要',
|
||||
disabled: prDiffSummaryRunning,
|
||||
onClick: () => {
|
||||
if (issue?.pr && !issue.prDiffFile && !prDiffSummaryRunning)
|
||||
onGeneratePrDiffSummary(issue.number)
|
||||
},
|
||||
}]
|
||||
: [],
|
||||
},
|
||||
{
|
||||
key: 'prDiffFile',
|
||||
@@ -486,12 +483,16 @@ export function IssueDetailPanel({
|
||||
type: 'string',
|
||||
readOnly: true,
|
||||
description: '实施流程创建的分支名',
|
||||
secondaryActionIcon: issue?.branch ? <GitMerge className="size-3.5" /> : undefined,
|
||||
secondaryActionTitle: '本地预合并 (git merge --no-commit --no-ff)',
|
||||
onSecondaryAction: () => {
|
||||
if (issue?.branch)
|
||||
onMergeBranch(issue.number, issue.branch)
|
||||
},
|
||||
secondaryActions: issue?.branch
|
||||
? [{
|
||||
icon: <GitMerge className="size-3.5" />,
|
||||
title: '本地预合并 (git merge --no-commit --no-ff)',
|
||||
onClick: () => {
|
||||
if (issue?.branch)
|
||||
onMergeBranch(issue.number, issue.branch)
|
||||
},
|
||||
}]
|
||||
: [],
|
||||
},
|
||||
issue?.worktreeExists
|
||||
? {
|
||||
@@ -500,9 +501,11 @@ export function IssueDetailPanel({
|
||||
type: 'file-link',
|
||||
description: '双击文件名在新 VS Code 窗口打开;点击垃圾桶删除 worktree',
|
||||
onOpen: (p: string) => onOpenWorktree(p),
|
||||
onSecondaryAction: () => issue && onDeleteWorktree(issue.number, issue.worktreePath ?? ''),
|
||||
secondaryActionIcon: <Trash2 className="size-3.5" />,
|
||||
secondaryActionTitle: '删除 worktree',
|
||||
secondaryActions: [{
|
||||
icon: <Trash2 className="size-3.5" />,
|
||||
title: '删除 worktree',
|
||||
onClick: () => onDeleteWorktree(issue.number, issue.worktreePath ?? ''),
|
||||
}],
|
||||
}
|
||||
: {
|
||||
key: 'worktreePath',
|
||||
@@ -514,7 +517,7 @@ export function IssueDetailPanel({
|
||||
],
|
||||
},
|
||||
],
|
||||
[onResumeSession, onResumeReviewSession, onResumeTestSession, onStartTestSession, onOpenFile, onImplement, onOpenPr, onGeneratePrDiffSummary, onOpenWorktree, onDeleteWorktree, onMergeBranch, onCloseSessionTab, onStartBrainstormSession, onOpenLogs, issue, locked, lockTitle, globalAutoReview, prDiffSummaryRunning, profiles],
|
||||
[onResumeSession, onResumeReviewSession, onResumeTestSession, onStartTestSession, onOpenFile, onImplement, onOpenPr, onGeneratePrDiffSummary, onOpenWorktree, onDeleteWorktree, onMergeBranch, onCloseSessionTab, onContinueBrainstorm, onOpenLogs, issue, locked, lockTitle, globalAutoReview, prDiffSummaryRunning, profiles],
|
||||
)
|
||||
|
||||
const data = useMemo<Record<string, unknown> | null>(() => {
|
||||
|
||||
@@ -66,14 +66,41 @@ export interface PropertyDef {
|
||||
onReload?: () => void
|
||||
/** type='file-link' 专用:重新加载按钮的图标,默认为 RefreshCw */
|
||||
reloadIcon?: ReactNode
|
||||
/** type='file-link' / 'action' 通用:第二个右侧按钮的图标,缺省则不渲染该按钮 */
|
||||
secondaryActionIcon?: ReactNode
|
||||
/** type='file-link' / 'action' 通用:第二个右侧按钮的 tooltip */
|
||||
secondaryActionTitle?: string
|
||||
/** type='file-link' / 'action' 通用:第二个右侧按钮的点击回调 */
|
||||
onSecondaryAction?: () => void
|
||||
/** type='file-link' / 'action' 通用:第二个右侧按钮是否禁用 */
|
||||
secondaryDisabled?: boolean
|
||||
/** type='file-link' / 'action' / 'pr-link' / 只读 string 通用:值右侧的图标按钮,按数组顺序渲染 */
|
||||
secondaryActions?: RowAction[]
|
||||
}
|
||||
|
||||
/** 属性行右侧的图标按钮 */
|
||||
export interface RowAction {
|
||||
icon: ReactNode
|
||||
/** tooltip,同时作为 aria-label */
|
||||
title?: string
|
||||
onClick: () => void
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
function RowActionButtons({ actions, className }: { actions: RowAction[] | undefined, className?: string }): ReactNode {
|
||||
if (!actions || actions.length === 0)
|
||||
return null
|
||||
return actions.map((action, i) => (
|
||||
<button
|
||||
key={i}
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (action.disabled)
|
||||
return
|
||||
action.onClick()
|
||||
}}
|
||||
disabled={action.disabled}
|
||||
title={action.title}
|
||||
aria-label={action.title ?? '次要操作'}
|
||||
className={`grid size-5 shrink-0 place-items-center ${className ?? ''} ${
|
||||
action.disabled ? 'cursor-not-allowed opacity-30' : 'opacity-60 hover:opacity-100'
|
||||
}`}
|
||||
>
|
||||
{action.icon}
|
||||
</button>
|
||||
))
|
||||
}
|
||||
|
||||
/** 属性分组定义 */
|
||||
@@ -294,8 +321,7 @@ function PropertyRow({
|
||||
)
|
||||
|
||||
case 'action': {
|
||||
const hasSecondaryAction = propDef.secondaryActionIcon != null
|
||||
const secondaryDisabledAction = !!propDef.secondaryDisabled
|
||||
const hasSecondaryAction = (propDef.secondaryActions?.length ?? 0) > 0
|
||||
const hasValue = value != null && value !== ''
|
||||
// Empty-value branch: still render the secondary button when defined
|
||||
// (e.g. "Play" to start a brainstorm session for an issue that has
|
||||
@@ -329,26 +355,7 @@ function PropertyRow({
|
||||
: (
|
||||
<span className="min-w-0 flex-1 truncate px-2 py-1.5 text-xs opacity-50">—</span>
|
||||
)}
|
||||
{hasSecondaryAction && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (secondaryDisabledAction)
|
||||
return
|
||||
propDef.onSecondaryAction?.()
|
||||
}}
|
||||
disabled={secondaryDisabledAction}
|
||||
title={propDef.secondaryActionTitle}
|
||||
aria-label={propDef.secondaryActionTitle ?? '次要操作'}
|
||||
className={`mr-2 grid size-5 shrink-0 place-items-center ${
|
||||
secondaryDisabledAction
|
||||
? 'cursor-not-allowed opacity-30'
|
||||
: 'opacity-60 hover:opacity-100'
|
||||
}`}
|
||||
>
|
||||
{propDef.secondaryActionIcon}
|
||||
</button>
|
||||
)}
|
||||
<RowActionButtons actions={propDef.secondaryActions} className="mr-2" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -356,8 +363,6 @@ function PropertyRow({
|
||||
case 'file-link': {
|
||||
const hasValue = typeof value === 'string' && value.length > 0
|
||||
const reloadIcon = propDef.reloadIcon ?? <RefreshCw className="size-3.5" />
|
||||
const hasSecondary = propDef.secondaryActionIcon != null
|
||||
const secondaryDisabled = !!propDef.secondaryDisabled
|
||||
return (
|
||||
<div className="flex w-full items-center gap-1.5 px-2 py-1.5">
|
||||
{hasValue
|
||||
@@ -389,34 +394,14 @@ function PropertyRow({
|
||||
{reloadIcon}
|
||||
</button>
|
||||
)}
|
||||
{hasSecondary && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (secondaryDisabled)
|
||||
return
|
||||
propDef.onSecondaryAction?.()
|
||||
}}
|
||||
disabled={secondaryDisabled}
|
||||
title={propDef.secondaryActionTitle}
|
||||
aria-label={propDef.secondaryActionTitle ?? '次要操作'}
|
||||
className={`grid size-5 shrink-0 place-items-center ${
|
||||
secondaryDisabled
|
||||
? 'cursor-not-allowed opacity-30'
|
||||
: 'opacity-60 hover:opacity-100'
|
||||
}`}
|
||||
>
|
||||
{propDef.secondaryActionIcon}
|
||||
</button>
|
||||
)}
|
||||
<RowActionButtons actions={propDef.secondaryActions} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
case 'pr-link': {
|
||||
const hasValue = typeof value === 'string' && value.length > 0
|
||||
const hasSecondary = propDef.secondaryActionIcon != null
|
||||
const secondaryDisabled = !!propDef.secondaryDisabled
|
||||
const hasSecondary = (propDef.secondaryActions?.length ?? 0) > 0
|
||||
if (!hasValue && !hasSecondary) {
|
||||
return (
|
||||
<span className="block truncate px-2 py-1.5 text-xs opacity-50">未关联 PR</span>
|
||||
@@ -442,26 +427,7 @@ function PropertyRow({
|
||||
: (
|
||||
<span className="min-w-0 flex-1 truncate text-xs opacity-50">未关联 PR</span>
|
||||
)}
|
||||
{hasSecondary && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (secondaryDisabled)
|
||||
return
|
||||
propDef.onSecondaryAction?.()
|
||||
}}
|
||||
disabled={secondaryDisabled}
|
||||
title={propDef.secondaryActionTitle}
|
||||
aria-label={propDef.secondaryActionTitle ?? '次要操作'}
|
||||
className={`grid size-5 shrink-0 place-items-center ${
|
||||
secondaryDisabled
|
||||
? 'cursor-not-allowed opacity-30'
|
||||
: 'opacity-60 hover:opacity-100'
|
||||
}`}
|
||||
>
|
||||
{propDef.secondaryActionIcon}
|
||||
</button>
|
||||
)}
|
||||
<RowActionButtons actions={propDef.secondaryActions} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -469,30 +435,11 @@ function PropertyRow({
|
||||
case 'string':
|
||||
default:
|
||||
if (ro) {
|
||||
const hasSecondary = propDef.secondaryActionIcon != null
|
||||
const secondaryDisabled = !!propDef.secondaryDisabled
|
||||
if (hasSecondary) {
|
||||
if ((propDef.secondaryActions?.length ?? 0) > 0) {
|
||||
return (
|
||||
<div className="flex w-full items-center gap-1.5 px-2 py-1.5">
|
||||
<span className="min-w-0 flex-1 truncate text-xs">{String(value ?? '')}</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (secondaryDisabled)
|
||||
return
|
||||
propDef.onSecondaryAction?.()
|
||||
}}
|
||||
disabled={secondaryDisabled}
|
||||
title={propDef.secondaryActionTitle}
|
||||
aria-label={propDef.secondaryActionTitle ?? '次要操作'}
|
||||
className={`grid size-5 shrink-0 place-items-center ${
|
||||
secondaryDisabled
|
||||
? 'cursor-not-allowed opacity-30'
|
||||
: 'opacity-60 hover:opacity-100'
|
||||
}`}
|
||||
>
|
||||
{propDef.secondaryActionIcon}
|
||||
</button>
|
||||
<RowActionButtons actions={propDef.secondaryActions} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user