Browse Source

隐藏图片输入节点未实现的按钮功能

TEST-s
weige 2 months ago
parent
commit
f7e83241e7
  1. 2
      src/app/page.tsx
  2. 23
      src/components/__tests__/ImageInputNode.test.tsx
  3. 71
      src/components/nodes/ImageInputNode.tsx
  4. 39
      src/store/__tests__/userStore.test.ts
  5. 8
      src/store/userStore.ts
  6. 5
      src/types/user.ts

2
src/app/page.tsx

@ -34,7 +34,7 @@ function CanvasPageContent() {
useEffect(() => {
if (token || getStoredLoginToken()) {
void fetchUserInfo(token);
void fetchUserInfo(token, { forceRefresh: true });
}
}, [fetchUserInfo, token]);

23
src/components/__tests__/ImageInputNode.test.tsx

@ -400,6 +400,27 @@ describe("ImageInputNode", () => {
expect(screen.getByLabelText("Remove image")).toBeInTheDocument();
});
it("should hide unavailable image editing and asset actions", () => {
render(
<TestWrapper>
<ImageInputNode {...propsWithImage} selected />
</TestWrapper>
);
expect(screen.queryByLabelText("Choose from assets")).not.toBeInTheDocument();
expect(screen.queryByLabelText("Crop")).not.toBeInTheDocument();
expect(screen.queryByLabelText("Draw")).not.toBeInTheDocument();
expect(screen.queryByLabelText("Multi-angle")).not.toBeInTheDocument();
expect(screen.queryByLabelText("Enhance")).not.toBeInTheDocument();
expect(screen.queryByLabelText("Lighting")).not.toBeInTheDocument();
expect(screen.queryByLabelText("Repaint")).not.toBeInTheDocument();
expect(screen.queryByLabelText("Grid")).not.toBeInTheDocument();
expect(screen.getAllByLabelText("Replace")).toHaveLength(2);
expect(screen.getByLabelText("Download image")).toBeInTheDocument();
expect(screen.getAllByLabelText("Open preview")).toHaveLength(2);
expect(screen.getByLabelText("Remove image")).toBeInTheDocument();
});
it("should call updateNodeData to clear image when remove button is clicked", () => {
render(
<TestWrapper>
@ -558,7 +579,7 @@ describe("ImageInputNode", () => {
const fileInput = document.querySelector('input[type="file"]') as HTMLInputElement;
const clickSpy = vi.spyOn(fileInput, "click");
fireEvent.click(screen.getByLabelText("Replace"));
fireEvent.click(screen.getAllByLabelText("Replace")[0]);
expect(clickSpy).toHaveBeenCalled();
});

71
src/components/nodes/ImageInputNode.tsx

@ -43,14 +43,6 @@ function EmptyImageIcon() {
}
type ImageToolbarIconKind =
| "crop"
| "draw"
| "multiAngle"
| "enhance"
| "lighting"
| "repaint"
| "grid"
| "folder"
| "download"
| "focus"
| "delete"
@ -66,22 +58,6 @@ function ImageToolbarIcon({ kind }: { kind: ImageToolbarIconKind }) {
};
switch (kind) {
case "crop":
return <svg {...commonProps}><path strokeLinecap="round" strokeLinejoin="round" d="M7 3v14h14M3 7h14v14M7 7h10v10" /></svg>;
case "draw":
return <svg {...commonProps}><path strokeLinecap="round" strokeLinejoin="round" d="m4 20 4.5-1 9.5-9.5-3.5-3.5L5 15.5 4 20zM13.5 7 17 3.5 20.5 7 17 10.5" /></svg>;
case "multiAngle":
return <svg {...commonProps}><path strokeLinecap="round" strokeLinejoin="round" d="M12 3 4 7v10l8 4 8-4V7l-8-4zM4 7l8 4 8-4M12 11v10" /></svg>;
case "enhance":
return <svg {...commonProps}><circle cx="12" cy="12" r="8" /><path strokeLinecap="round" d="M12 7v10M7 12h10M8.5 8.5l7 7M15.5 8.5l-7 7" /></svg>;
case "lighting":
return <svg {...commonProps}><path strokeLinecap="round" strokeLinejoin="round" d="M12 3v2M12 19v2M4.2 4.2l1.4 1.4M18.4 18.4l1.4 1.4M3 12h2M19 12h2M4.2 19.8l1.4-1.4M18.4 5.6l1.4-1.4" /><circle cx="12" cy="12" r="4" /></svg>;
case "repaint":
return <svg {...commonProps}><path strokeLinecap="round" strokeLinejoin="round" d="M4 20h5l10-10a3.5 3.5 0 0 0-5-5L4 15v5zM13 6l5 5M8 16h4" /></svg>;
case "grid":
return <svg {...commonProps}><path d="M4 4h6v6H4zM14 4h6v6h-6zM4 14h6v6H4zM14 14h6v6h-6z" /></svg>;
case "folder":
return <svg {...commonProps}><path strokeLinecap="round" strokeLinejoin="round" d="M3 7h6l2 2h10v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7z" /></svg>;
case "download":
return <svg {...commonProps}><path strokeLinecap="round" strokeLinejoin="round" d="M12 4v11m0 0 4-4m-4 4-4-4M4 17v2h16v-2" /></svg>;
case "focus":
@ -93,23 +69,6 @@ function ImageToolbarIcon({ kind }: { kind: ImageToolbarIconKind }) {
}
}
function ToolbarDivider() {
return <span className="h-9 w-px bg-neutral-600/70" aria-hidden="true" />;
}
function StaticImageToolbarItem({ label, icon }: { label: string; icon: ImageToolbarIconKind }) {
return (
<span
aria-label={label}
title={label}
className="flex h-10 items-center gap-3 rounded-md px-2 text-neutral-300"
>
<ImageToolbarIcon kind={icon} />
<span className="whitespace-nowrap">{label}</span>
</span>
);
}
export function ImageInputNode({ id, data, selected }: NodeProps<ImageInputNodeType>) {
const { t } = useI18n();
const nodeData = data;
@ -274,43 +233,23 @@ export function ImageInputNode({ id, data, selected }: NodeProps<ImageInputNodeT
<ImageToolbarIcon kind="upload" />
<span>{t("imageInput.localUpload")}</span>
</button>
<button
type="button"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}
aria-label={t("imageInput.fromAssets")}
title={t("imageInput.fromAssets")}
className="flex h-14 items-center gap-3 whitespace-nowrap rounded-xl border border-neutral-600/70 bg-neutral-800 px-5 text-xl text-neutral-200 shadow-xl shadow-black/30 transition-colors hover:border-neutral-500 hover:bg-neutral-700 hover:text-neutral-100"
>
<ImageToolbarIcon kind="folder" />
<span>{t("imageInput.fromAssets")}</span>
</button>
</div>
)}
{selected && hasImage && (
<div className="nodrag nopan absolute left-1/2 -top-[132px] z-[10002] flex -translate-x-1/2 items-center gap-4 rounded-xl border border-neutral-600/70 bg-neutral-800 px-5 py-3 text-xl text-neutral-300 shadow-2xl shadow-black/40">
<StaticImageToolbarItem label={t("imageInput.crop")} icon="crop" />
<StaticImageToolbarItem label={t("imageInput.draw")} icon="draw" />
<StaticImageToolbarItem label={t("imageInput.multiAngle")} icon="multiAngle" />
<StaticImageToolbarItem label={t("imageInput.enhance")} icon="enhance" />
<StaticImageToolbarItem label={t("imageInput.lighting")} icon="lighting" />
<StaticImageToolbarItem label={t("imageInput.repaint")} icon="repaint" />
<StaticImageToolbarItem label={t("imageInput.splitGrid")} icon="grid" />
<ToolbarDivider />
<div className="nodrag nopan absolute left-1/2 -top-[132px] z-[10002] flex -translate-x-1/2 items-center gap-2 rounded-xl border border-neutral-600/70 bg-neutral-800 px-4 py-3 text-xl text-neutral-300 shadow-2xl shadow-black/40">
<button
type="button"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleOpenFileDialog();
}}
aria-label={t("imageInput.fromAssets")}
title={t("imageInput.fromAssets")}
aria-label={t("imageInput.replace")}
title={t("imageInput.replace")}
className="flex h-10 items-center justify-center rounded-md px-2 text-neutral-300 transition-colors hover:bg-neutral-700 hover:text-neutral-100"
>
<ImageToolbarIcon kind="folder" />
<ImageToolbarIcon kind="upload" />
</button>
<button
type="button"

39
src/store/__tests__/userStore.test.ts

@ -259,4 +259,43 @@ describe("useUserStore.fetchUserInfo", () => {
expect(useUserStore.getState().gatewayApiKey).toBe("manual-gateway-key");
expect(openLoginModal).not.toHaveBeenCalled();
});
it("refreshes user info when forceRefresh is enabled", async () => {
const cachedUser = { ...user, allCoins: 10 };
const refreshedUser = { ...user, allCoins: 99 };
useUserStore.setState({
token: "stored-token",
userInfo: cachedUser,
gatewayApiKey: "stored-gateway-key",
});
const fetcher = vi.fn().mockResolvedValueOnce(
new Response(
JSON.stringify({
data: {
token: "stored-token",
user: refreshedUser,
},
message: "ok",
status: "0000",
}),
{ status: 200 }
)
);
vi.stubGlobal("fetch", fetcher);
await expect(
useUserStore.getState().fetchUserInfo("", { forceRefresh: true })
).resolves.toEqual(refreshedUser);
expect(fetcher).toHaveBeenCalledWith(USER_INFO_ENDPOINT, {
method: "GET",
headers: {
Accept: "application/json",
Authorization: "Bearer stored-token",
token: "stored-token",
},
});
expect(useUserStore.getState().userInfo?.allCoins).toBe(99);
});
});

8
src/store/userStore.ts

@ -174,7 +174,7 @@ export const useUserStore = create<UserStoreState>()(
isLoadingGatewayApiKey: false,
gatewayApiKeyError: null,
}),
fetchUserInfo: async (token) => {
fetchUserInfo: async (token, options) => {
const normalizedToken = resolveLoginToken(token, get().token);
if (!normalizedToken) {
set({
@ -188,7 +188,11 @@ export const useUserStore = create<UserStoreState>()(
}
const current = get();
if (current.token === normalizedToken && current.userInfo) {
if (
!options?.forceRefresh &&
current.token === normalizedToken &&
current.userInfo
) {
if (!current.gatewayApiKey) {
await get().fetchGatewayApiKey();
}

5
src/types/user.ts

@ -65,7 +65,10 @@ export interface UserStoreState {
gatewayApiKeyError: string | null;
setToken: (token: string | null) => void;
setGatewayApiKey: (apiKey: string | null) => void;
fetchUserInfo: (token: string) => Promise<UserInfo | null>;
fetchUserInfo: (
token: string,
options?: { forceRefresh?: boolean }
) => Promise<UserInfo | null>;
fetchGatewayApiKey: () => Promise<string | null>;
clearGatewayApiKey: () => void;
clearSession: () => void;

Loading…
Cancel
Save