工单多了以后靠肉眼在四列里翻太慢。顶部输入框按空白分词逐词 AND 匹配 标题、工单号、PR 号,`#427` 与 `427` 等价,纯数字按包含匹配,记不全 编号也能搜到。筛选只收窄渲染,拖拽与依赖锁定仍按全量列表算;列头角标 在筛选时显示「可见 / 总数」。
266 lines
10 KiB
TypeScript
266 lines
10 KiB
TypeScript
import type { ReleaseAction, ReleaseStatus } from '../hooks/useIssues'
|
||
import { GitCommit, ListPlus, Loader2, Lock, RefreshCw, Rocket, Search, Settings, Unlock, User, Users, X } from 'lucide-react'
|
||
import { useEffect, useRef, useState } from 'react'
|
||
|
||
interface Props {
|
||
/** 顶部筛选框当前内容;按标题 / 工单号 / PR 号过滤看板卡片。 */
|
||
filterQuery: string
|
||
onFilterChange: (query: string) => void
|
||
onRefresh: () => void
|
||
/** host 正在重拉看板;为 true 时刷新图标转起来。 */
|
||
refreshing: boolean
|
||
/** 看板范围:'mine' 只看我的(assigned/created),'all' 团队全部工单。 */
|
||
scope: 'mine' | 'all'
|
||
onToggleScope: () => void
|
||
onEditAuth: () => void
|
||
commitRunning: boolean
|
||
/**
|
||
* Whether the workspace git working tree has uncommitted changes. The
|
||
* commit button is only rendered when this is true (or `commitRunning` is
|
||
* true — we keep it visible mid-run so the spinner stays on screen even
|
||
* if `cc` clears the tree before finishing).
|
||
*/
|
||
hasChanges: boolean
|
||
onCommit: () => void
|
||
/** 最近一次发布状态;首次消息到达前为 undefined,按钮显示「正在检查」并禁用。 */
|
||
release: ReleaseStatus | undefined
|
||
onRelease: (action: ReleaseAction) => void
|
||
/**
|
||
* Whether the workspace's `.env*` files are currently chmod-locked (444).
|
||
* Icon flips between Lock and Unlock based on this.
|
||
*/
|
||
envLocked: boolean
|
||
/**
|
||
* Number of `.env*` files discovered in the workspace at the last scan.
|
||
* The button is disabled when 0 — nothing to lock.
|
||
*/
|
||
envFileCount: number
|
||
/**
|
||
* True while a chmod batch is in flight; disables the button to avoid
|
||
* duplicate toggles.
|
||
*/
|
||
envLockRunning: boolean
|
||
/** Hover tooltip, already includes file count + current lock state. */
|
||
envLockTitle: string
|
||
onToggleEnvLock: () => void
|
||
/**
|
||
* Whether YouTrack is configured. The「导入 YouTrack 工单」button is only
|
||
* rendered when true.
|
||
*/
|
||
youtrackConfigured: boolean
|
||
/**
|
||
* Open the native multi-select dialog to pick which YouTrack issues to
|
||
* mirror onto the board.
|
||
*/
|
||
onImportYouTrack: () => void
|
||
}
|
||
|
||
const MENU_ITEM_CLASS = 'w-full px-3 py-1 text-left hover:bg-[var(--vscode-menu-selectionBackground)] hover:text-[var(--vscode-menu-selectionForeground)] disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:bg-transparent disabled:hover:text-inherit'
|
||
|
||
function releaseTitle(release: ReleaseStatus | undefined): string {
|
||
if (!release)
|
||
return '正在检查发布状态…'
|
||
if (release.unavailable)
|
||
return release.unavailable
|
||
if (!release.current)
|
||
return '尚无发布点 · 点击切第一个发布分支'
|
||
const lead = release.unreleased > 0 ? `${release.devBranch} 领先 ${release.unreleased} 个提交` : '已全部发布'
|
||
return `正式服发布点 ${release.current} · ${lead}`
|
||
}
|
||
|
||
export function PanelHeader({
|
||
filterQuery,
|
||
onFilterChange,
|
||
onRefresh,
|
||
refreshing,
|
||
scope,
|
||
onToggleScope,
|
||
onEditAuth,
|
||
commitRunning,
|
||
hasChanges,
|
||
onCommit,
|
||
release,
|
||
onRelease,
|
||
envLocked,
|
||
envFileCount,
|
||
envLockRunning,
|
||
envLockTitle,
|
||
onToggleEnvLock,
|
||
youtrackConfigured,
|
||
onImportYouTrack,
|
||
}: Props) {
|
||
const showCommit = hasChanges || commitRunning
|
||
// Env-lock button is always visible; disabled while a chmod batch is in
|
||
// flight or when the workspace has no .env files to act on.
|
||
const envLockDisabled = envLockRunning || envFileCount === 0
|
||
|
||
const [releaseMenuOpen, setReleaseMenuOpen] = useState(false)
|
||
const menuRef = useRef<HTMLDivElement>(null)
|
||
useEffect(() => {
|
||
if (!releaseMenuOpen)
|
||
return
|
||
const onDown = (e: MouseEvent): void => {
|
||
if (!menuRef.current?.contains(e.target as Node))
|
||
setReleaseMenuOpen(false)
|
||
}
|
||
document.addEventListener('mousedown', onDown)
|
||
return () => document.removeEventListener('mousedown', onDown)
|
||
}, [releaseMenuOpen])
|
||
|
||
const title = releaseTitle(release)
|
||
// 发布按钮常驻,让用户随时看到发布点状态;结构性不可用或动作进行中禁用。
|
||
const releaseDisabled = !release || !!release.unavailable || release.running
|
||
const unreleased = release?.unreleased ?? 0
|
||
|
||
function pickRelease(action: ReleaseAction): void {
|
||
setReleaseMenuOpen(false)
|
||
onRelease(action)
|
||
}
|
||
|
||
return (
|
||
<div className="flex h-8 shrink-0 items-center gap-1 border-b border-[var(--vscode-panel-border)] px-2">
|
||
<div className="relative mr-auto flex items-center">
|
||
<Search size={12} className="pointer-events-none absolute left-2 opacity-60" />
|
||
<input
|
||
type="text"
|
||
value={filterQuery}
|
||
onChange={e => onFilterChange(e.target.value)}
|
||
onKeyDown={(e) => {
|
||
if (e.key === 'Escape') {
|
||
e.stopPropagation()
|
||
onFilterChange('')
|
||
}
|
||
}}
|
||
placeholder="筛选:标题 / 工单号 / PR 号"
|
||
aria-label="筛选工单"
|
||
className="h-6 w-56 rounded border border-[var(--vscode-input-border,transparent)] bg-[var(--vscode-input-background)] pl-6 pr-6 text-xs text-[var(--vscode-input-foreground)] outline-none placeholder:text-[var(--vscode-input-placeholderForeground)] focus:border-[var(--vscode-focusBorder)]"
|
||
/>
|
||
{filterQuery !== '' && (
|
||
<button
|
||
type="button"
|
||
onClick={() => onFilterChange('')}
|
||
title="清空筛选"
|
||
aria-label="清空筛选"
|
||
className="absolute right-1 flex h-4 w-4 items-center justify-center rounded hover:bg-[var(--vscode-toolbar-hoverBackground)]"
|
||
>
|
||
<X size={11} />
|
||
</button>
|
||
)}
|
||
</div>
|
||
<div ref={menuRef} className="relative">
|
||
<button
|
||
type="button"
|
||
onClick={() => setReleaseMenuOpen(open => !open)}
|
||
disabled={releaseDisabled}
|
||
title={title}
|
||
aria-label={title}
|
||
aria-haspopup="menu"
|
||
aria-expanded={releaseMenuOpen}
|
||
className="relative flex h-6 w-6 items-center justify-center rounded hover:bg-[var(--vscode-toolbar-hoverBackground)] disabled:cursor-not-allowed disabled:opacity-50"
|
||
>
|
||
{release?.running
|
||
? <Loader2 size={14} className="animate-spin" />
|
||
: <Rocket size={14} />}
|
||
{unreleased > 0 && (
|
||
<span className="absolute -right-1 -top-1 rounded-full bg-[var(--vscode-badge-background)] px-1 text-[9px] leading-3 text-[var(--vscode-badge-foreground)]">
|
||
{unreleased > 99 ? '99+' : unreleased}
|
||
</span>
|
||
)}
|
||
</button>
|
||
{releaseMenuOpen && release && (
|
||
<div
|
||
role="menu"
|
||
className="absolute right-0 top-7 z-20 min-w-44 rounded border border-[var(--vscode-menu-border)] bg-[var(--vscode-menu-background)] py-1 text-xs text-[var(--vscode-menu-foreground)] shadow"
|
||
>
|
||
<button type="button" role="menuitem" className={MENU_ITEM_CLASS} onClick={() => pickRelease('cut')}>
|
||
{release.current === release.nextBranch ? `更新今日发布(${release.nextBranch})` : `切新发布(${release.nextBranch})`}
|
||
</button>
|
||
<button
|
||
type="button"
|
||
role="menuitem"
|
||
className={MENU_ITEM_CLASS}
|
||
disabled={!release.current}
|
||
onClick={() => pickRelease('cherry-pick')}
|
||
>
|
||
{release.current ? `摘修复到 ${release.current}` : '摘修复(尚无发布分支)'}
|
||
</button>
|
||
{release.jenkinsConfigured && (
|
||
<button type="button" role="menuitem" className={MENU_ITEM_CLASS} onClick={() => pickRelease('deploy')}>
|
||
部署到正式服…
|
||
</button>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
<button
|
||
type="button"
|
||
onClick={onToggleEnvLock}
|
||
disabled={envLockDisabled}
|
||
title={envLockTitle}
|
||
aria-label={envLockTitle}
|
||
className="flex h-6 w-6 items-center justify-center rounded hover:bg-[var(--vscode-toolbar-hoverBackground)] disabled:cursor-not-allowed disabled:opacity-50"
|
||
>
|
||
{envLockRunning
|
||
? <Loader2 size={14} className="animate-spin" />
|
||
: envLocked
|
||
? <Lock size={14} />
|
||
: <Unlock size={14} />}
|
||
</button>
|
||
{showCommit && (
|
||
<button
|
||
type="button"
|
||
onClick={onCommit}
|
||
disabled={commitRunning}
|
||
title="提交代码"
|
||
aria-label="提交代码"
|
||
className="flex h-6 w-6 items-center justify-center rounded hover:bg-[var(--vscode-toolbar-hoverBackground)] disabled:cursor-not-allowed disabled:opacity-50"
|
||
>
|
||
{commitRunning
|
||
? <Loader2 size={14} className="animate-spin" />
|
||
: <GitCommit size={14} />}
|
||
</button>
|
||
)}
|
||
{youtrackConfigured && (
|
||
<button
|
||
type="button"
|
||
onClick={onImportYouTrack}
|
||
title="导入 YouTrack 工单"
|
||
aria-label="导入 YouTrack 工单"
|
||
className="flex h-6 w-6 items-center justify-center rounded hover:bg-[var(--vscode-toolbar-hoverBackground)]"
|
||
>
|
||
<ListPlus size={14} />
|
||
</button>
|
||
)}
|
||
<button
|
||
type="button"
|
||
onClick={onToggleScope}
|
||
title={scope === 'mine' ? '当前:只看我的工单,点击切换为全部' : '当前:团队全部工单,点击切换为只看我的'}
|
||
aria-label={scope === 'mine' ? '切换为全部工单' : '切换为只看我的'}
|
||
className="flex h-6 w-6 items-center justify-center rounded hover:bg-[var(--vscode-toolbar-hoverBackground)]"
|
||
>
|
||
{scope === 'mine' ? <User size={14} /> : <Users size={14} />}
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={onRefresh}
|
||
disabled={refreshing}
|
||
title={refreshing ? '正在刷新…' : '刷新'}
|
||
aria-label="刷新"
|
||
aria-busy={refreshing}
|
||
className="flex h-6 w-6 items-center justify-center rounded hover:bg-[var(--vscode-toolbar-hoverBackground)] disabled:cursor-not-allowed"
|
||
>
|
||
<RefreshCw size={14} className={refreshing ? 'animate-spin' : undefined} />
|
||
</button>
|
||
<button
|
||
type="button"
|
||
onClick={onEditAuth}
|
||
title="重新配置 Gitea Token"
|
||
aria-label="重新配置 Gitea Token"
|
||
className="flex h-6 w-6 items-center justify-center rounded hover:bg-[var(--vscode-toolbar-hoverBackground)]"
|
||
>
|
||
<Settings size={14} />
|
||
</button>
|
||
</div>
|
||
)
|
||
}
|