From 1bae66f7785289818eba3249f651a320f497c6a4 Mon Sep 17 00:00:00 2001 From: Mohamed Bassem Date: Sun, 1 Jun 2025 22:43:13 +0000 Subject: feat: Maintain list structure when importing from netscape. Fixes #538 --- apps/web/components/settings/ImportExport.tsx | 131 +++++++++++++++++--------- 1 file changed, 87 insertions(+), 44 deletions(-) (limited to 'apps/web/components/settings/ImportExport.tsx') diff --git a/apps/web/components/settings/ImportExport.tsx b/apps/web/components/settings/ImportExport.tsx index a20bd554..35c2b88f 100644 --- a/apps/web/components/settings/ImportExport.tsx +++ b/apps/web/components/settings/ImportExport.tsx @@ -27,7 +27,6 @@ import { } from "@/lib/importBookmarkParser"; import { cn } from "@/lib/utils"; import { useMutation } from "@tanstack/react-query"; -import { TRPCClientError } from "@trpc/client"; import { Download, Upload } from "lucide-react"; import { @@ -125,7 +124,7 @@ export function ImportExportRow() { const { mutateAsync: parseAndCreateBookmark } = useMutation({ mutationFn: async (toImport: { bookmark: ParsedBookmark; - listId: string; + listIds: string[]; }) => { const bookmark = toImport.bookmark; if (bookmark.content === undefined) { @@ -151,20 +150,14 @@ export function ImportExportRow() { await Promise.all([ // Add to import list - addToList({ - bookmarkId: created.id, - listId: toImport.listId, - }).catch((e) => { - if ( - e instanceof TRPCClientError && - e.message.includes("already in the list") - ) { - /* empty */ - } else { - throw e; - } - }), - + ...[ + toImport.listIds.map((listId) => + addToList({ + bookmarkId: created.id, + listId, + }), + ), + ], // Update tags bookmark.tags.length > 0 ? updateTags({ @@ -214,7 +207,7 @@ export function ImportExportRow() { return; } - const importList = await createList({ + const rootList = await createList({ name: t("settings.import.imported_bookmarks"), icon: "⬆️", }); @@ -223,33 +216,83 @@ export function ImportExportRow() { setImportProgress({ done: 0, total: finalBookmarksToImport.length }); + // Precreate folder lists + const allRequiredPaths = new Set(); + // collect the paths of all bookmarks that have non-empty paths + for (const bookmark of finalBookmarksToImport) { + for (const path of bookmark.paths) { + if (path && path.length > 0) { + // We need every prefix of the path for the hierarchy + for (let i = 1; i <= path.length; i++) { + const subPath = path.slice(0, i); + const pathKey = subPath.join("/"); + allRequiredPaths.add(pathKey); + } + } + } + } + + // Convert to array and sort by depth (so that parent paths come first) + const allRequiredPathsArray = Array.from(allRequiredPaths).sort( + (a, b) => a.split("/").length - b.split("/").length, + ); + + const pathMap: Record = {}; + + // Root list is the parent for top-level folders + // Represent root as empty string + pathMap[""] = rootList.id; + + for (const pathKey of allRequiredPathsArray) { + const parts = pathKey.split("/"); + const parentKey = parts.slice(0, -1).join("/"); + const parentId = pathMap[parentKey] || rootList.id; + + const folderName = parts[parts.length - 1]; + // Create the list + const folderList = await createList({ + name: folderName, + parentId: parentId, + icon: "📁", + }); + pathMap[pathKey] = folderList.id; + } + const importPromises = finalBookmarksToImport.map( - (bookmark) => () => - parseAndCreateBookmark({ - bookmark: bookmark, - listId: importList.id, - }).then( - (value) => { - setImportProgress((prev) => { - const newDone = (prev?.done ?? 0) + 1; - return { - done: newDone, - total: finalBookmarksToImport.length, - }; - }); - return { status: "fulfilled" as const, value }; - }, - () => { - setImportProgress((prev) => { - const newDone = (prev?.done ?? 0) + 1; - return { - done: newDone, - total: finalBookmarksToImport.length, - }; - }); - return { status: "rejected" as const }; - }, - ), + (bookmark) => async () => { + // Determine the target list ids + const listIds = bookmark.paths.map( + (path) => pathMap[path.join("/")] || rootList.id, + ); + if (listIds.length === 0) { + listIds.push(rootList.id); + } + + try { + const created = await parseAndCreateBookmark({ + bookmark: bookmark, + listIds, + }); + + setImportProgress((prev) => { + const newDone = (prev?.done ?? 0) + 1; + return { + done: newDone, + total: finalBookmarksToImport.length, + }; + }); + return { status: "fulfilled" as const, value: created }; + } catch (e) { + setImportProgress((prev) => { + const newDone = (prev?.done ?? 0) + 1; + return { + done: newDone, + total: finalBookmarksToImport.length, + }; + }); + return { status: "rejected" as const }; + } + }, ); const CONCURRENCY_LIMIT = 20; @@ -290,7 +333,7 @@ export function ImportExportRow() { }); } - router.push(`/dashboard/lists/${importList.id}`); + router.push(`/dashboard/lists/${rootList.id}`); }, onError: (error) => { setImportProgress(null); // Clear progress on initial parsing error -- cgit v1.2.3-70-g09d2