Files
superwork/vscode/webview-ui/src/components/PanelHeader.tsx
T
cruldra 9e255f517b ✨ feat(vscode): 看板顶部加筛选框,按标题 / 工单号 / PR 号过滤卡片
工单多了以后靠肉眼在四列里翻太慢。顶部输入框按空白分词逐词 AND 匹配
标题、工单号、PR 号,`#427` 与 `427` 等价,纯数字按包含匹配,记不全
编号也能搜到。筛选只收窄渲染,拖拽与依赖锁定仍按全量列表算;列头角标
在筛选时显示「可见 / 总数」。
2026-09-20 17:24:54 +08:00

266 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
)
}