mirror of
https://github.com/farion1231/cc-switch.git
synced 2026-10-26 01:34:51 +08:00
fix(prompts): delay the disabled look during quick reloads and writes
Refocusing the window reloads the prompt list, and reloadPending fed the buttons' disabled prop directly, so every focus flashed most of the column grey; toggling a prompt (also on Pi) did the same. Like the MCP and Skills panels, show the disabled state only after it lasts 300ms via useDelayedFlag. Handlers still check interactionBlocked, and the two Pi entry points that relied on disabled alone (toggle, new template) now check it too. Open forms and dialogs still disable immediately.
This commit is contained in:
1 parent
c55843ffad
commit
b074f83589
3 files changed
+54
-10
No files matched your search
@@ -5,6 +5,7 @@ import { Notice } from "@/components/ui/notice";
|
||||
import { SegmentedControl } from "@/components/ui/segmented-control";
|
||||
import { promptsApi, type AppId, type Prompt } from "@/lib/api";
|
||||
import { usePromptActions } from "@/hooks/usePromptActions";
|
||||
import { useDelayedFlag } from "@/hooks/useDelayedFlag";
|
||||
import { useTauriEvent } from "@/hooks/useTauriEvent";
|
||||
import PromptFormPanel from "./PromptFormPanel";
|
||||
import { PromptCopyDialog } from "./PromptCopyDialog";
|
||||
@@ -86,6 +87,10 @@ const PiPromptPanel: React.FC<PiPromptPanelProps> = ({
|
||||
const interactionBlocked =
|
||||
loading || writePending || isFormOpen || dialogOpen;
|
||||
const navigationBlocked = writePending || isFormOpen || dialogOpen;
|
||||
// 外观上的禁用晚 300ms 才出现:点一下启用这类很快的写入不让整列按钮闪一下变灰。
|
||||
// 拦截仍看 interactionBlocked;表单、对话框打开时照常立即禁用。
|
||||
const controlsDisabled =
|
||||
useDelayedFlag(loading || writePending) || isFormOpen || dialogOpen;
|
||||
|
||||
useEffect(() => {
|
||||
if (open) void reload();
|
||||
@@ -156,6 +161,7 @@ const PiPromptPanel: React.FC<PiPromptPanelProps> = ({
|
||||
const hasExternalPrompt = fileHasContent && activePrompt === undefined;
|
||||
|
||||
const handleToggle = (id: string, enabled: boolean) => {
|
||||
if (interactionBlocked) return;
|
||||
const before = prompts;
|
||||
const target = before[id];
|
||||
if (!target) return;
|
||||
@@ -385,8 +391,9 @@ const PiPromptPanel: React.FC<PiPromptPanelProps> = ({
|
||||
activeTab === "templates"
|
||||
? t("pi.prompts.newTemplate")
|
||||
: t("prompts.add"),
|
||||
disabled: interactionBlocked,
|
||||
disabled: controlsDisabled,
|
||||
onClick: () => {
|
||||
if (interactionBlocked) return;
|
||||
if (activeTab === "templates") {
|
||||
templatesRef.current?.openCreate();
|
||||
} else {
|
||||
@@ -442,7 +449,7 @@ const PiPromptPanel: React.FC<PiPromptPanelProps> = ({
|
||||
<Button
|
||||
variant="neutral"
|
||||
size="compact"
|
||||
disabled={interactionBlocked}
|
||||
disabled={controlsDisabled}
|
||||
onClick={handleImport}
|
||||
>
|
||||
{t("pi.prompts.saveToLibrary")}
|
||||
@@ -469,7 +476,7 @@ const PiPromptPanel: React.FC<PiPromptPanelProps> = ({
|
||||
app="pi"
|
||||
displayPath={displayPath}
|
||||
fileText={fileText}
|
||||
disabled={interactionBlocked}
|
||||
disabled={controlsDisabled}
|
||||
onImport={handleImport}
|
||||
onAdd={() => openGlobalPromptForm()}
|
||||
/>
|
||||
@@ -479,7 +486,7 @@ const PiPromptPanel: React.FC<PiPromptPanelProps> = ({
|
||||
prompts={prompts}
|
||||
searchQuery={searchQuery}
|
||||
listLabel={t("pi.prompts.libraryLabel")}
|
||||
disabled={interactionBlocked}
|
||||
disabled={controlsDisabled}
|
||||
onClearSearch={() => setSearchQuery("")}
|
||||
onToggle={handleToggle}
|
||||
onEdit={openGlobalPromptForm}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { toast } from "@/lib/toast";
|
||||
import { APP_DISPLAY_NAME } from "@/components/shell/AppGlyph";
|
||||
import { promptsApi, type AppId, type Prompt } from "@/lib/api";
|
||||
import { usePromptActions } from "@/hooks/usePromptActions";
|
||||
import { useDelayedFlag } from "@/hooks/useDelayedFlag";
|
||||
import { useTauriEvent } from "@/hooks/useTauriEvent";
|
||||
import PiPromptPanel from "./PiPromptPanel";
|
||||
import PromptFormPanel from "./PromptFormPanel";
|
||||
@@ -76,6 +77,12 @@ const StandardPromptPanel: React.FC<PromptPanelProps> = ({
|
||||
const interactionBlocked =
|
||||
loading || reloadPending || writePending || isFormOpen || dialogOpen;
|
||||
const navigationBlocked = writePending || isFormOpen || dialogOpen;
|
||||
// 外观上的禁用晚 300ms 才出现:切回窗口时的重读、点一下启用这类很快的操作,
|
||||
// 不让整列按钮闪一下变灰。拦截仍看 interactionBlocked 和写锁;表单、对话框打开时照常立即禁用。
|
||||
const controlsDisabled =
|
||||
useDelayedFlag(loading || reloadPending || writePending) ||
|
||||
isFormOpen ||
|
||||
dialogOpen;
|
||||
|
||||
useEffect(() => {
|
||||
onInteractionBlockedChange?.(interactionBlocked);
|
||||
@@ -493,7 +500,7 @@ const StandardPromptPanel: React.FC<PromptPanelProps> = ({
|
||||
primary={{
|
||||
label: t("prompts.add"),
|
||||
onClick: handleAdd,
|
||||
disabled: interactionBlocked,
|
||||
disabled: controlsDisabled,
|
||||
}}
|
||||
moreItems={moreItems(
|
||||
{
|
||||
@@ -531,7 +538,7 @@ const StandardPromptPanel: React.FC<PromptPanelProps> = ({
|
||||
app={appId}
|
||||
displayPath={displayPath}
|
||||
fileText={fileText}
|
||||
disabled={interactionBlocked}
|
||||
disabled={controlsDisabled}
|
||||
onImport={() => void handleImport()}
|
||||
onAdd={handleAdd}
|
||||
/>
|
||||
@@ -541,7 +548,7 @@ const StandardPromptPanel: React.FC<PromptPanelProps> = ({
|
||||
prompts={prompts}
|
||||
searchQuery={searchQuery}
|
||||
listLabel={t("prompts.listLabel", { app: APP_DISPLAY_NAME[appId] })}
|
||||
disabled={interactionBlocked}
|
||||
disabled={controlsDisabled}
|
||||
onClearSearch={() => setSearchQuery("")}
|
||||
onToggle={(id, enabled) => void handleToggle(id, enabled)}
|
||||
onEdit={handleEdit}
|
||||
|
||||
@@ -585,7 +585,8 @@ describe("PromptPanel", () => {
|
||||
await waitFor(() => {
|
||||
expect(onInteractionBlockedChange).toHaveBeenLastCalledWith(true);
|
||||
});
|
||||
expect(toggle).toBeDisabled();
|
||||
// 外观上的禁用晚 300ms 出现(useDelayedFlag),拦截本身是立即的
|
||||
await waitFor(() => expect(toggle).toBeDisabled());
|
||||
expect(editButton("Harbor Prompt")).toBeDisabled();
|
||||
expect(screen.getByRole("button", { name: "prompts.add" })).toBeDisabled();
|
||||
|
||||
@@ -609,7 +610,9 @@ describe("PromptPanel", () => {
|
||||
});
|
||||
expect(screen.getByText("prompts.loading")).toBeInTheDocument();
|
||||
const add = screen.getByRole("button", { name: "prompts.add" });
|
||||
expect(add).toBeDisabled();
|
||||
fireEvent.click(add);
|
||||
expect(screen.queryByTestId("prompt-form")).not.toBeInTheDocument();
|
||||
await waitFor(() => expect(add).toBeDisabled());
|
||||
fireEvent.click(add);
|
||||
expect(screen.queryByTestId("prompt-form")).not.toBeInTheDocument();
|
||||
expect(mocks.toggleEnabled).not.toHaveBeenCalled();
|
||||
@@ -710,6 +713,29 @@ describe("PromptPanel", () => {
|
||||
expect(mocks.reload).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps controls enabled through a quick focus reload but still blocks clicks", async () => {
|
||||
renderPanel();
|
||||
await waitForPanelReady();
|
||||
|
||||
let resolveReload!: () => void;
|
||||
mocks.reload.mockReturnValueOnce(
|
||||
new Promise<void>((resolve) => {
|
||||
resolveReload = resolve;
|
||||
}),
|
||||
);
|
||||
fireEvent(window, new Event("focus"));
|
||||
const add = screen.getByRole("button", { name: "prompts.add" });
|
||||
expect(add).not.toBeDisabled();
|
||||
fireEvent.click(add);
|
||||
expect(screen.queryByTestId("prompt-form")).not.toBeInTheDocument();
|
||||
|
||||
await act(async () => {
|
||||
resolveReload();
|
||||
await Promise.resolve();
|
||||
});
|
||||
expect(add).not.toBeDisabled();
|
||||
});
|
||||
|
||||
it.each(["focus", "prompt-imported"])(
|
||||
"queues %s reloads while the drawer is open",
|
||||
async (trigger) => {
|
||||
@@ -746,7 +772,11 @@ describe("PromptPanel", () => {
|
||||
|
||||
const view = renderPanel();
|
||||
await waitFor(() => expect(claudeReload).toHaveBeenCalledTimes(1));
|
||||
expect(screen.getByRole("button", { name: "prompts.add" })).toBeDisabled();
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
screen.getByRole("button", { name: "prompts.add" }),
|
||||
).toBeDisabled(),
|
||||
);
|
||||
|
||||
view.rerenderWith({ appId: "codex" });
|
||||
await waitFor(() => expect(codexReload).toHaveBeenCalledTimes(1));
|
||||
|
||||
Reference in new issue
Block a user