🐛 fix(vscode): 刷新按钮加载态可见,图标随重拉转圈
刷新按钮此前只发消息重拉数据,看板已有列表时界面毫无变化,用户 无法判断点击是否生效。消息协议里的 issues/loading 从未被发送。 loadAndPush 现在在开跑前推 issues/loading,并在 finally 里推新增的 issues/loaded,保证成功、报错、缺 token 提前返回都能收尾;webhook 触发的自动重拉同样走这条链路。webview 据此维护 refreshing 状态, 刷新图标转圈并在期间禁用按钮。 Claude-Session: https://claude.ai/code/session_01REdhpbaNyqWASF2HWsnTFv
This commit is contained in:
1 parent
9167b86708
commit
ef0750b4ef
7 files changed
+30
-5
No files matched your search
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
"publisher": "clurdra",
|
"publisher": "clurdra",
|
||||||
"name": "superpowers-vscode-clurdra",
|
"name": "superpowers-vscode-clurdra",
|
||||||
"displayName": "Superpowers-clurdra",
|
"displayName": "Superpowers-clurdra",
|
||||||
"version": "0.2.125",
|
"version": "0.2.126",
|
||||||
"packageManager": "pnpm@10.27.0",
|
"packageManager": "pnpm@10.27.0",
|
||||||
"description": "Superpowers specs and plans Kanban explorer",
|
"description": "Superpowers specs and plans Kanban explorer",
|
||||||
"author": "clurdra",
|
"author": "clurdra",
|
||||||
|
|||||||
@@ -912,8 +912,12 @@ export class KanbanWebviewPanel {
|
|||||||
async loadAndPush(): Promise<void> {
|
async loadAndPush(): Promise<void> {
|
||||||
if (this.loadInFlight)
|
if (this.loadInFlight)
|
||||||
return this.loadInFlight
|
return this.loadInFlight
|
||||||
|
// 每次真正开跑都成对推 loading/loaded,webview 据此转刷新图标;
|
||||||
|
// 无论成功、报错还是缺 token 中途返回,finally 都会收尾。
|
||||||
|
this.postMessage({ type: 'issues/loading' })
|
||||||
this.loadInFlight = this.loadAndPushInner().finally(() => {
|
this.loadInFlight = this.loadAndPushInner().finally(() => {
|
||||||
this.loadInFlight = undefined
|
this.loadInFlight = undefined
|
||||||
|
this.postMessage({ type: 'issues/loaded' })
|
||||||
})
|
})
|
||||||
return this.loadInFlight
|
return this.loadInFlight
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ export type ToastLevel = 'info' | 'success' | 'error'
|
|||||||
|
|
||||||
export type ExtensionToWebview
|
export type ExtensionToWebview
|
||||||
= | { type: 'issues/loading' }
|
= | { type: 'issues/loading' }
|
||||||
|
| { type: 'issues/loaded' }
|
||||||
| { type: 'issues/update', issues: Issue[], scope: 'mine' | 'all', globalAutoReview: boolean, youtrackConfigured: boolean, me?: string, avatars?: Record<string, string> }
|
| { type: 'issues/update', issues: Issue[], scope: 'mine' | 'all', globalAutoReview: boolean, youtrackConfigured: boolean, me?: string, avatars?: Record<string, string> }
|
||||||
| { type: 'issues/error', message: string }
|
| { type: 'issues/error', message: string }
|
||||||
| { type: 'issue/patch', issueNumber: number, patch: { autoReview?: boolean, specFile?: string, planFile?: string, prDiffFile?: string | null, sessionId?: string | null, implementSessionId?: string | null, reviewSessionId?: string | null, reviewSessionFileExists?: boolean, testSessionId?: string | null, pr?: string | null, implementStatus?: 'running' | 'done' | 'failed' | null, column?: IssueColumn, worktreePath?: string | null, prMerged?: boolean, prMergedAt?: string | null, branch?: string | null, color?: string, worktreeExists?: boolean, brainstormTabOpen?: boolean, implementTabOpen?: boolean, reviewTabOpen?: boolean, testTabOpen?: boolean, profilePath?: string | null, brainstormProfilePath?: string | null, testProfilePath?: string | null, assignees?: string[], handoffAttachmentId?: string | null, handoffFrom?: string | null } }
|
| { type: 'issue/patch', issueNumber: number, patch: { autoReview?: boolean, specFile?: string, planFile?: string, prDiffFile?: string | null, sessionId?: string | null, implementSessionId?: string | null, reviewSessionId?: string | null, reviewSessionFileExists?: boolean, testSessionId?: string | null, pr?: string | null, implementStatus?: 'running' | 'done' | 'failed' | null, column?: IssueColumn, worktreePath?: string | null, prMerged?: boolean, prMergedAt?: string | null, branch?: string | null, color?: string, worktreeExists?: boolean, brainstormTabOpen?: boolean, implementTabOpen?: boolean, reviewTabOpen?: boolean, testTabOpen?: boolean, profilePath?: string | null, brainstormProfilePath?: string | null, testProfilePath?: string | null, assignees?: string[], handoffAttachmentId?: string | null, handoffFrom?: string | null } }
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export function App() {
|
|||||||
avatars,
|
avatars,
|
||||||
setIssues,
|
setIssues,
|
||||||
refresh,
|
refresh,
|
||||||
|
refreshing,
|
||||||
saveSettings,
|
saveSettings,
|
||||||
listYouTrackProjects,
|
listYouTrackProjects,
|
||||||
youtrackProjects,
|
youtrackProjects,
|
||||||
@@ -305,6 +306,7 @@ export function App() {
|
|||||||
<>
|
<>
|
||||||
<PanelHeader
|
<PanelHeader
|
||||||
onRefresh={refresh}
|
onRefresh={refresh}
|
||||||
|
refreshing={refreshing}
|
||||||
scope={scope}
|
scope={scope}
|
||||||
onToggleScope={toggleScope}
|
onToggleScope={toggleScope}
|
||||||
onEditAuth={requestEditAuth}
|
onEditAuth={requestEditAuth}
|
||||||
@@ -331,6 +333,7 @@ export function App() {
|
|||||||
<>
|
<>
|
||||||
<PanelHeader
|
<PanelHeader
|
||||||
onRefresh={refresh}
|
onRefresh={refresh}
|
||||||
|
refreshing={refreshing}
|
||||||
scope={scope}
|
scope={scope}
|
||||||
onToggleScope={toggleScope}
|
onToggleScope={toggleScope}
|
||||||
onEditAuth={requestEditAuth}
|
onEditAuth={requestEditAuth}
|
||||||
@@ -429,6 +432,7 @@ export function App() {
|
|||||||
<>
|
<>
|
||||||
<PanelHeader
|
<PanelHeader
|
||||||
onRefresh={refresh}
|
onRefresh={refresh}
|
||||||
|
refreshing={refreshing}
|
||||||
scope={scope}
|
scope={scope}
|
||||||
onToggleScope={toggleScope}
|
onToggleScope={toggleScope}
|
||||||
onEditAuth={requestEditAuth}
|
onEditAuth={requestEditAuth}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { useEffect, useRef, useState } from 'react'
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onRefresh: () => void
|
onRefresh: () => void
|
||||||
|
/** host 正在重拉看板;为 true 时刷新图标转起来。 */
|
||||||
|
refreshing: boolean
|
||||||
/** 看板范围:'mine' 只看我的(assigned/created),'all' 团队全部工单。 */
|
/** 看板范围:'mine' 只看我的(assigned/created),'all' 团队全部工单。 */
|
||||||
scope: 'mine' | 'all'
|
scope: 'mine' | 'all'
|
||||||
onToggleScope: () => void
|
onToggleScope: () => void
|
||||||
@@ -65,6 +67,7 @@ function releaseTitle(release: ReleaseStatus | undefined): string {
|
|||||||
|
|
||||||
export function PanelHeader({
|
export function PanelHeader({
|
||||||
onRefresh,
|
onRefresh,
|
||||||
|
refreshing,
|
||||||
scope,
|
scope,
|
||||||
onToggleScope,
|
onToggleScope,
|
||||||
onEditAuth,
|
onEditAuth,
|
||||||
@@ -207,11 +210,13 @@ export function PanelHeader({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onRefresh}
|
onClick={onRefresh}
|
||||||
title="刷新"
|
disabled={refreshing}
|
||||||
|
title={refreshing ? '正在刷新…' : '刷新'}
|
||||||
aria-label="刷新"
|
aria-label="刷新"
|
||||||
className="flex h-6 w-6 items-center justify-center rounded hover:bg-[var(--vscode-toolbar-hoverBackground)]"
|
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} />
|
<RefreshCw size={14} className={refreshing ? 'animate-spin' : undefined} />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -159,6 +159,11 @@ export interface UseIssuesResult {
|
|||||||
profiles: ClaudeProfile[]
|
profiles: ClaudeProfile[]
|
||||||
setIssues: (issues: Issue[]) => void
|
setIssues: (issues: Issue[]) => void
|
||||||
refresh: () => void
|
refresh: () => void
|
||||||
|
/**
|
||||||
|
* True while the host is reloading the board (手动刷新、切换范围、webhook
|
||||||
|
* 触发都算)。看板已有列表时不会退回 loading 空屏,靠它转刷新图标给反馈。
|
||||||
|
*/
|
||||||
|
refreshing: boolean
|
||||||
saveSettings: (values: SettingsValues) => void
|
saveSettings: (values: SettingsValues) => void
|
||||||
/**
|
/**
|
||||||
* Fetch the YouTrack project list for the settings dropdown using the
|
* Fetch the YouTrack project list for the settings dropdown using the
|
||||||
@@ -344,6 +349,7 @@ export function useIssues(): UseIssuesResult {
|
|||||||
// 默认团队全部,与扩展侧 DEFAULT_KANBAN_SCOPE 一致(两个包不共享源码)。
|
// 默认团队全部,与扩展侧 DEFAULT_KANBAN_SCOPE 一致(两个包不共享源码)。
|
||||||
const [scope, setScope] = useState<'mine' | 'all'>(injected?.scope ?? 'all')
|
const [scope, setScope] = useState<'mine' | 'all'>(injected?.scope ?? 'all')
|
||||||
const [youtrackConfigured, setYoutrackConfigured] = useState<boolean>(injected?.youtrackConfigured ?? false)
|
const [youtrackConfigured, setYoutrackConfigured] = useState<boolean>(injected?.youtrackConfigured ?? false)
|
||||||
|
const [refreshing, setRefreshing] = useState<boolean>(false)
|
||||||
const [toasts, setToasts] = useState<ToastItem[]>([])
|
const [toasts, setToasts] = useState<ToastItem[]>([])
|
||||||
const [profiles, setProfiles] = useState<ClaudeProfile[]>([])
|
const [profiles, setProfiles] = useState<ClaudeProfile[]>([])
|
||||||
const [logs, setLogs] = useState<LogEntry[]>([])
|
const [logs, setLogs] = useState<LogEntry[]>([])
|
||||||
@@ -700,8 +706,12 @@ export function useIssues(): UseIssuesResult {
|
|||||||
const cleanup = onMessage((msg) => {
|
const cleanup = onMessage((msg) => {
|
||||||
switch (msg.type) {
|
switch (msg.type) {
|
||||||
case 'issues/loading':
|
case 'issues/loading':
|
||||||
|
setRefreshing(true)
|
||||||
setState(prev => prev.status === 'ready' ? prev : { status: 'loading' })
|
setState(prev => prev.status === 'ready' ? prev : { status: 'loading' })
|
||||||
break
|
break
|
||||||
|
case 'issues/loaded':
|
||||||
|
setRefreshing(false)
|
||||||
|
break
|
||||||
case 'issues/update':
|
case 'issues/update':
|
||||||
setState({ status: 'ready', issues: msg.issues })
|
setState({ status: 'ready', issues: msg.issues })
|
||||||
setScope(msg.scope)
|
setScope(msg.scope)
|
||||||
@@ -908,5 +918,5 @@ export function useIssues(): UseIssuesResult {
|
|||||||
return cleanup
|
return cleanup
|
||||||
}, [clearPrDiffSummaryRunning, markHandoffRunning])
|
}, [clearPrDiffSummaryRunning, markHandoffRunning])
|
||||||
|
|
||||||
return { state, settings, avatars, globalAutoReview, youtrackConfigured, importYouTrack, toasts, profiles, setIssues, refresh, scope, toggleScope, saveSettings, listYouTrackProjects, youtrackProjects, dismissSettings, requestEditAuth, createIssue, dismissToast, openUrl, resumeSession, resumeReviewSession, startTestSession, resumeTestSession, focusSession, openFile, implement, generatePrDiffSummary, isPrDiffSummaryRunning, openPr, openWorktree, deleteWorktree, mergeBranch, deleteIssue, closeIssue, closeSessionTab, continueBrainstorm, changeColumn, setDependency, clearDependency, updateIssueAutoReview, updateIssueProfilePath, updateIssueBrainstormProfilePath, updateIssueTestProfilePath, logs, fetchLogs, clearLogs, pendingSelectId, clearPendingSelect, commitRunning, runCommit, hasChanges, release, runRelease, cherryPick, dismissCherryPick, confirmCherryPick, envLocked, envFileCount, envLockRunning, envLockTitle, toggleEnvLock, me, handoffUsers, requestHandoffUsers, startHandoff, acceptHandoff, isHandoffRunning }
|
return { state, settings, avatars, globalAutoReview, youtrackConfigured, importYouTrack, toasts, profiles, setIssues, refresh, refreshing, scope, toggleScope, saveSettings, listYouTrackProjects, youtrackProjects, dismissSettings, requestEditAuth, createIssue, dismissToast, openUrl, resumeSession, resumeReviewSession, startTestSession, resumeTestSession, focusSession, openFile, implement, generatePrDiffSummary, isPrDiffSummaryRunning, openPr, openWorktree, deleteWorktree, mergeBranch, deleteIssue, closeIssue, closeSessionTab, continueBrainstorm, changeColumn, setDependency, clearDependency, updateIssueAutoReview, updateIssueProfilePath, updateIssueBrainstormProfilePath, updateIssueTestProfilePath, logs, fetchLogs, clearLogs, pendingSelectId, clearPendingSelect, commitRunning, runCommit, hasChanges, release, runRelease, cherryPick, dismissCherryPick, confirmCherryPick, envLocked, envFileCount, envLockRunning, envLockTitle, toggleEnvLock, me, handoffUsers, requestHandoffUsers, startHandoff, acceptHandoff, isHandoffRunning }
|
||||||
}
|
}
|
||||||
@@ -80,6 +80,7 @@ export interface ProfilesData {
|
|||||||
|
|
||||||
export type ExtensionToWebview
|
export type ExtensionToWebview
|
||||||
= | { type: 'issues/loading' }
|
= | { type: 'issues/loading' }
|
||||||
|
| { type: 'issues/loaded' }
|
||||||
| { type: 'issues/update', issues: Issue[], scope: 'mine' | 'all', globalAutoReview: boolean, youtrackConfigured: boolean, me?: string, avatars?: Record<string, string> }
|
| { type: 'issues/update', issues: Issue[], scope: 'mine' | 'all', globalAutoReview: boolean, youtrackConfigured: boolean, me?: string, avatars?: Record<string, string> }
|
||||||
| { type: 'issues/error', message: string }
|
| { type: 'issues/error', message: string }
|
||||||
| { type: 'issue/patch', issueNumber: number, patch: { autoReview?: boolean, specFile?: string, planFile?: string, prDiffFile?: string | null, sessionId?: string | null, implementSessionId?: string | null, reviewSessionId?: string | null, reviewSessionFileExists?: boolean, testSessionId?: string | null, pr?: string | null, implementStatus?: 'running' | 'done' | 'failed' | null, column?: IssueColumn, worktreePath?: string | null, prMerged?: boolean, prMergedAt?: string | null, branch?: string | null, color?: string, worktreeExists?: boolean, brainstormTabOpen?: boolean, implementTabOpen?: boolean, reviewTabOpen?: boolean, testTabOpen?: boolean, profilePath?: string | null, brainstormProfilePath?: string | null, testProfilePath?: string | null, assignees?: string[], handoffAttachmentId?: string | null, handoffFrom?: string | null } }
|
| { type: 'issue/patch', issueNumber: number, patch: { autoReview?: boolean, specFile?: string, planFile?: string, prDiffFile?: string | null, sessionId?: string | null, implementSessionId?: string | null, reviewSessionId?: string | null, reviewSessionFileExists?: boolean, testSessionId?: string | null, pr?: string | null, implementStatus?: 'running' | 'done' | 'failed' | null, column?: IssueColumn, worktreePath?: string | null, prMerged?: boolean, prMergedAt?: string | null, branch?: string | null, color?: string, worktreeExists?: boolean, brainstormTabOpen?: boolean, implementTabOpen?: boolean, reviewTabOpen?: boolean, testTabOpen?: boolean, profilePath?: string | null, brainstormProfilePath?: string | null, testProfilePath?: string | null, assignees?: string[], handoffAttachmentId?: string | null, handoffFrom?: string | null } }
|
||||||
|
|||||||
Reference in new issue
Block a user