aboutsummaryrefslogtreecommitdiffstats
path: root/apps/web/components/settings
diff options
context:
space:
mode:
authorMohamed Bassem <me@mbassem.com>2025-06-01 22:43:13 +0000
committerMohamed Bassem <me@mbassem.com>2025-06-01 22:43:13 +0000
commit1bae66f7785289818eba3249f651a320f497c6a4 (patch)
tree9c01aca6f2d8924d912f05d90e3e8e768479740d /apps/web/components/settings
parente59be245d5e3005b5b5dadf78ad7115cc800c663 (diff)
downloadkarakeep-1bae66f7785289818eba3249f651a320f497c6a4.tar.zst
feat: Maintain list structure when importing from netscape. Fixes #538
Diffstat (limited to 'apps/web/components/settings')
-rw-r--r--apps/web/components/settings/ImportExport.tsx131
1 files changed, 87 insertions, 44 deletions
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<string>();
+ // 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<string, string> = {};
+
+ // 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