From 4a580d713621f99abb8baabc9b847ce039d44842 Mon Sep 17 00:00:00 2001 From: Mohamed Bassem Date: Sat, 4 Oct 2025 13:40:24 +0100 Subject: feat: Revamp import experience (#2001) * WIP: import v2 * remove new session button * don't redirect after import * store and lint to root list * models + tests * redesign the progress * simplify the import session for ow * drop status from session schema * split the import session page * i18n * fix test * remove pagination * fix some colors in darkmode * one last fix * add privacy filter * privacy check * fix interactivity of import progress * fix test --- apps/web/components/settings/ImportExport.tsx | 21 +- apps/web/components/settings/ImportSessionCard.tsx | 257 +++++++++++++++++++++ .../components/settings/ImportSessionsSection.tsx | 79 +++++++ 3 files changed, 352 insertions(+), 5 deletions(-) create mode 100644 apps/web/components/settings/ImportSessionCard.tsx create mode 100644 apps/web/components/settings/ImportSessionsSection.tsx (limited to 'apps/web/components') diff --git a/apps/web/components/settings/ImportExport.tsx b/apps/web/components/settings/ImportExport.tsx index c644af27..ee220342 100644 --- a/apps/web/components/settings/ImportExport.tsx +++ b/apps/web/components/settings/ImportExport.tsx @@ -19,6 +19,7 @@ import { Download, Loader2, Upload } from "lucide-react"; import { Card, CardContent } from "../ui/card"; import { toast } from "../ui/use-toast"; +import { ImportSessionsSection } from "./ImportSessionsSection"; function ImportCard({ text, @@ -266,11 +267,21 @@ export function ImportExportRow() { export default function ImportExport() { const { t } = useTranslation(); return ( -
-

- {t("settings.import.import_export_bookmarks")} -

- +
+
+
+
+

+ {t("settings.import.import_export_bookmarks")} +

+ +
+
+
+ +
+ +
); } diff --git a/apps/web/components/settings/ImportSessionCard.tsx b/apps/web/components/settings/ImportSessionCard.tsx new file mode 100644 index 00000000..690caaa5 --- /dev/null +++ b/apps/web/components/settings/ImportSessionCard.tsx @@ -0,0 +1,257 @@ +"use client"; + +import Link from "next/link"; +import ActionConfirmingDialog from "@/components/ui/action-confirming-dialog"; +import { Badge } from "@/components/ui/badge"; +import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader } from "@/components/ui/card"; +import { Progress } from "@/components/ui/progress"; +import { + useDeleteImportSession, + useImportSessionStats, +} from "@/lib/hooks/useImportSessions"; +import { useTranslation } from "@/lib/i18n/client"; +import { formatDistanceToNow } from "date-fns"; +import { + AlertCircle, + CheckCircle2, + ClipboardList, + Clock, + ExternalLink, + Loader2, + Trash2, +} from "lucide-react"; + +import type { ZImportSessionWithStats } from "@karakeep/shared/types/importSessions"; + +interface ImportSessionCardProps { + session: ZImportSessionWithStats; +} + +function getStatusColor(status: string) { + switch (status) { + case "pending": + return "bg-muted text-muted-foreground"; + case "in_progress": + return "bg-blue-500/10 text-blue-700 dark:text-blue-400"; + case "completed": + return "bg-green-500/10 text-green-700 dark:text-green-400"; + case "failed": + return "bg-destructive/10 text-destructive"; + default: + return "bg-muted text-muted-foreground"; + } +} + +function getStatusIcon(status: string) { + switch (status) { + case "pending": + return ; + case "in_progress": + return ; + case "completed": + return ; + case "failed": + return ; + default: + return ; + } +} + +export function ImportSessionCard({ session }: ImportSessionCardProps) { + const { t } = useTranslation(); + const { data: liveStats } = useImportSessionStats(session.id); + const deleteSession = useDeleteImportSession(); + + const statusLabels: Record = { + pending: t("settings.import_sessions.status.pending"), + in_progress: t("settings.import_sessions.status.in_progress"), + completed: t("settings.import_sessions.status.completed"), + failed: t("settings.import_sessions.status.failed"), + }; + + // Use live stats if available, otherwise fallback to session stats + const stats = liveStats || session; + const progress = + stats.totalBookmarks > 0 + ? ((stats.completedBookmarks + stats.failedBookmarks) / + stats.totalBookmarks) * + 100 + : 0; + + const canDelete = stats.status !== "in_progress"; + + return ( + + +
+
+

{session.name}

+

+ {t("settings.import_sessions.created_at", { + time: formatDistanceToNow(session.createdAt, { + addSuffix: true, + }), + })} +

+
+
+ + {getStatusIcon(stats.status)} + + {statusLabels[stats.status] ?? stats.status.replace("_", " ")} + + +
+
+
+ + +
+ {/* Progress Section */} +
+
+

+ {t("settings.import_sessions.progress")} +

+
+ + {stats.completedBookmarks + stats.failedBookmarks} /{" "} + {stats.totalBookmarks} + + + {Math.round(progress)}% + +
+
+ {stats.totalBookmarks > 0 && ( + + )} +
+ + {/* Stats Breakdown */} + {stats.totalBookmarks > 0 && ( +
+
+ {stats.pendingBookmarks > 0 && ( + + + {t("settings.import_sessions.badges.pending", { + count: stats.pendingBookmarks, + })} + + )} + {stats.processingBookmarks > 0 && ( + + + {t("settings.import_sessions.badges.processing", { + count: stats.processingBookmarks, + })} + + )} + {stats.completedBookmarks > 0 && ( + + + {t("settings.import_sessions.badges.completed", { + count: stats.completedBookmarks, + })} + + )} + {stats.failedBookmarks > 0 && ( + + + {t("settings.import_sessions.badges.failed", { + count: stats.failedBookmarks, + })} + + )} +
+
+ )} + + {/* Root List Link */} + {session.rootListId && ( +
+
+ + + {t("settings.import_sessions.imported_to")} + + + {t("settings.import_sessions.view_list")} + + +
+
+ )} + + {/* Message */} + {stats.message && ( +
+ {stats.message} +
+ )} + + {/* Actions */} +
+
+ {canDelete && ( + + {t("settings.import_sessions.delete_dialog_description", { + name: session.name, + })} +
+ } + actionButton={(setDialogOpen) => ( + + )} + > + + + )} +
+
+
+ + + ); +} diff --git a/apps/web/components/settings/ImportSessionsSection.tsx b/apps/web/components/settings/ImportSessionsSection.tsx new file mode 100644 index 00000000..38f248d2 --- /dev/null +++ b/apps/web/components/settings/ImportSessionsSection.tsx @@ -0,0 +1,79 @@ +"use client"; + +import { Card, CardContent } from "@/components/ui/card"; +import { useListImportSessions } from "@/lib/hooks/useImportSessions"; +import { useTranslation } from "@/lib/i18n/client"; +import { Package } from "lucide-react"; + +import { FullPageSpinner } from "../ui/full-page-spinner"; +import { ImportSessionCard } from "./ImportSessionCard"; + +export function ImportSessionsSection() { + const { t } = useTranslation(); + const { data: sessions, isLoading, error } = useListImportSessions(); + + if (isLoading) { + return ( +
+
+

+ {t("settings.import_sessions.title")} +

+
+ +
+ ); + } + + if (error) { + return ( +
+
+

+ {t("settings.import_sessions.title")} +

+
+ + +

+ {t("settings.import_sessions.load_error")} +

+
+
+
+ ); + } + + return ( +
+
+

+ {t("settings.import_sessions.title")} +

+

+ {t("settings.import_sessions.description")} +

+
+ + {sessions && sessions.length > 0 ? ( +
+ {sessions.map((session) => ( + + ))} +
+ ) : ( + + + +

+ {t("settings.import_sessions.no_sessions")} +

+

+ {t("settings.import_sessions.no_sessions_detail")} +

+
+
+ )} +
+ ); +} -- cgit v1.2.3-70-g09d2