+
+
@@ -107,9 +117,20 @@ export function AccountsPage() {
open={importDialog.open}
busy={importMutation.isPending}
error={getErrorMessageOrNull(importMutation.error)}
- onOpenChange={importDialog.onOpenChange}
- onImport={async (file) => {
- await importMutation.mutateAsync(file);
+ result={lastImportResult}
+ onOpenChange={(open) => {
+ if (!open) {
+ setLastImportResult(null);
+ }
+ importDialog.onOpenChange(open);
+ }}
+ onImport={async (files) => {
+ const result = await importMutation.mutateAsync(files);
+ setLastImportResult(result);
+ if (result.imported.length > 0) {
+ await accountsQuery.refetch();
+ }
+ return result;
}}
/>
diff --git a/frontend/src/features/accounts/components/import-dialog.test.tsx b/frontend/src/features/accounts/components/import-dialog.test.tsx
new file mode 100644
index 00000000..6ef421a1
--- /dev/null
+++ b/frontend/src/features/accounts/components/import-dialog.test.tsx
@@ -0,0 +1,75 @@
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { describe, expect, it, vi } from "vitest";
+
+import { ImportDialog } from "@/features/accounts/components/import-dialog";
+
+describe("ImportDialog", () => {
+ it("submits multiple files and closes when all imports succeed", async () => {
+ const user = userEvent.setup();
+ const onImport = vi.fn().mockResolvedValue({
+ imported: [
+ {
+ filename: "one.json",
+ accountId: "acc-1",
+ email: "one@example.com",
+ planType: "plus",
+ status: "active",
+ refreshedOnImport: false,
+ },
+ ],
+ failed: [],
+ });
+ const onOpenChange = vi.fn();
+
+ render(
+
,
+ );
+
+ const files = [
+ new File(["{}"], "one.json", { type: "application/json" }),
+ new File(["{}"], "two.json", { type: "application/json" }),
+ ];
+
+ await user.upload(screen.getByLabelText("Files"), files);
+ expect(screen.getByText("2 files selected")).toBeInTheDocument();
+
+ await user.click(screen.getByRole("button", { name: "Import" }));
+
+ expect(onImport).toHaveBeenCalledWith(files);
+ expect(onOpenChange).toHaveBeenCalledWith(false);
+ });
+
+ it("renders import failures returned by the batch endpoint", () => {
+ render(
+
{}}
+ onImport={vi.fn()}
+ />,
+ );
+
+ expect(screen.getByText("Imported 0 files, 1 failed.")).toBeInTheDocument();
+ expect(screen.getByText("broken.json:")).toBeInTheDocument();
+ expect(screen.getByText(/Invalid auth\.json payload/)).toBeInTheDocument();
+ });
+});
diff --git a/frontend/src/features/accounts/components/import-dialog.tsx b/frontend/src/features/accounts/components/import-dialog.tsx
index 8df3bd05..e4a83082 100644
--- a/frontend/src/features/accounts/components/import-dialog.tsx
+++ b/frontend/src/features/accounts/components/import-dialog.tsx
@@ -1,6 +1,7 @@
import { useState } from "react";
import type { FormEvent } from "react";
+import { AlertMessage } from "@/components/alert-message";
import { Button } from "@/components/ui/button";
import {
Dialog,
@@ -12,51 +13,69 @@ import {
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
+import type { AccountImportBatchResponse } from "@/features/accounts/schemas";
export type ImportDialogProps = {
open: boolean;
busy: boolean;
error: string | null;
+ result: AccountImportBatchResponse | null;
onOpenChange: (open: boolean) => void;
- onImport: (file: File) => Promise;
+ onImport: (files: File[]) => Promise;
};
export function ImportDialog({
open,
busy,
error,
+ result,
onOpenChange,
onImport,
}: ImportDialogProps) {
- const [file, setFile] = useState(null);
+ const [files, setFiles] = useState([]);
const handleSubmit = async (event: FormEvent) => {
event.preventDefault();
- if (!file) {
+ if (files.length === 0) {
return;
}
- await onImport(file);
- onOpenChange(false);
- setFile(null);
+ const importResult = await onImport(files);
+ setFiles([]);
+ if (importResult.failed.length === 0) {
+ onOpenChange(false);
+ }
};
return (
+
+