aboutsummaryrefslogtreecommitdiffstats
path: root/packages/shared
diff options
context:
space:
mode:
authorMohamed Bassem <me@mbassem.com>2025-10-04 13:40:24 +0100
committerGitHub <noreply@github.com>2025-10-04 13:40:24 +0100
commit4a580d713621f99abb8baabc9b847ce039d44842 (patch)
treea2aa6f3ae8045ad50a9316624e2a7028dd098c6b /packages/shared
parent5e331a7d5b8d9666812170547574804d8b6da741 (diff)
downloadkarakeep-4a580d713621f99abb8baabc9b847ce039d44842.tar.zst
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
Diffstat (limited to 'packages/shared')
-rw-r--r--packages/shared/import-export/importer.test.ts12
-rw-r--r--packages/shared/import-export/importer.ts50
-rw-r--r--packages/shared/types/bookmarks.ts1
-rw-r--r--packages/shared/types/importSessions.ts76
4 files changed, 121 insertions, 18 deletions
diff --git a/packages/shared/import-export/importer.test.ts b/packages/shared/import-export/importer.test.ts
index 2ea63846..00f892a9 100644
--- a/packages/shared/import-export/importer.test.ts
+++ b/packages/shared/import-export/importer.test.ts
@@ -85,6 +85,8 @@ describe("importBookmarksFromFile", () => {
},
);
+ const createImportSession = vi.fn(async () => ({ id: "session-1" }));
+
const progress: number[] = [];
const res = await importBookmarksFromFile(
{
@@ -96,6 +98,7 @@ describe("importBookmarksFromFile", () => {
createBookmark,
addBookmarkToLists,
updateBookmarkTags,
+ createImportSession,
},
onProgress: (d, t) => progress.push(d / t),
},
@@ -167,6 +170,7 @@ describe("importBookmarksFromFile", () => {
createBookmark: vi.fn(),
addBookmarkToLists: vi.fn(),
updateBookmarkTags: vi.fn(),
+ createImportSession: vi.fn(async () => ({ id: "session-1" })),
},
},
{ parsers },
@@ -174,6 +178,7 @@ describe("importBookmarksFromFile", () => {
expect(res).toEqual({
counts: { successes: 0, failures: 0, alreadyExisted: 0, total: 0 },
rootListId: null,
+ importSessionId: null,
});
});
@@ -244,6 +249,8 @@ describe("importBookmarksFromFile", () => {
},
);
+ const createImportSession = vi.fn(async () => ({ id: "session-1" }));
+
const progress: number[] = [];
const res = await importBookmarksFromFile(
{
@@ -255,6 +262,7 @@ describe("importBookmarksFromFile", () => {
createBookmark,
addBookmarkToLists,
updateBookmarkTags,
+ createImportSession,
},
onProgress: (d, t) => progress.push(d / t),
},
@@ -353,6 +361,8 @@ describe("importBookmarksFromFile", () => {
},
);
+ const createImportSession = vi.fn(async () => ({ id: "session-1" }));
+
const progress: number[] = [];
const res = await importBookmarksFromFile(
{
@@ -364,6 +374,7 @@ describe("importBookmarksFromFile", () => {
createBookmark,
addBookmarkToLists,
updateBookmarkTags,
+ createImportSession,
},
onProgress: (d, t) => progress.push(d / t),
},
@@ -371,6 +382,7 @@ describe("importBookmarksFromFile", () => {
);
expect(res.rootListId).toBe("Imported");
+ expect(res.importSessionId).toBe("session-1");
// All bookmarks are created successfully, but 2 fail in post-processing
expect(res.counts).toEqual({
diff --git a/packages/shared/import-export/importer.ts b/packages/shared/import-export/importer.ts
index 88c0c3bc..b32c49c1 100644
--- a/packages/shared/import-export/importer.ts
+++ b/packages/shared/import-export/importer.ts
@@ -17,6 +17,7 @@ export interface ImportDeps {
}) => Promise<{ id: string }>;
createBookmark: (
bookmark: ParsedBookmark,
+ sessionId: string,
) => Promise<{ id: string; alreadyExists?: boolean }>;
addBookmarkToLists: (input: {
bookmarkId: string;
@@ -26,6 +27,10 @@ export interface ImportDeps {
bookmarkId: string;
tags: string[];
}) => Promise<void>;
+ createImportSession: (input: {
+ name: string;
+ rootListId: string;
+ }) => Promise<{ id: string }>;
}
export interface ImportOptions {
@@ -38,6 +43,7 @@ export interface ImportOptions {
export interface ImportResult {
counts: ImportCounts;
rootListId: string | null;
+ importSessionId: string | null;
}
export async function importBookmarksFromFile(
@@ -66,10 +72,15 @@ export async function importBookmarksFromFile(
return {
counts: { successes: 0, failures: 0, alreadyExisted: 0, total: 0 },
rootListId: null,
+ importSessionId: null,
};
}
const rootList = await deps.createList({ name: rootListName, icon: "⬆️" });
+ const session = await deps.createImportSession({
+ name: `${source.charAt(0).toUpperCase() + source.slice(1)} Import - ${new Date().toLocaleDateString()}`,
+ rootListId: rootList.id,
+ });
onProgress?.(0, parsedBookmarks.length);
@@ -109,22 +120,28 @@ export async function importBookmarksFromFile(
pathMap[pathKey] = folderList.id;
}
+ let done = 0;
const importPromises = parsedBookmarks.map((bookmark) => async () => {
- const listIds = bookmark.paths.map(
- (path) => pathMap[path.join(PATH_DELIMITER)] || rootList.id,
- );
- if (listIds.length === 0) listIds.push(rootList.id);
-
- const created = await deps.createBookmark(bookmark);
- await deps.addBookmarkToLists({ bookmarkId: created.id, listIds });
- if (bookmark.tags && bookmark.tags.length > 0) {
- await deps.updateBookmarkTags({
- bookmarkId: created.id,
- tags: bookmark.tags,
- });
- }
+ try {
+ const listIds = bookmark.paths.map(
+ (path) => pathMap[path.join(PATH_DELIMITER)] || rootList.id,
+ );
+ if (listIds.length === 0) listIds.push(rootList.id);
+
+ const created = await deps.createBookmark(bookmark, session.id);
+ await deps.addBookmarkToLists({ bookmarkId: created.id, listIds });
+ if (bookmark.tags && bookmark.tags.length > 0) {
+ await deps.updateBookmarkTags({
+ bookmarkId: created.id,
+ tags: bookmark.tags,
+ });
+ }
- return created;
+ return created;
+ } finally {
+ done += 1;
+ onProgress?.(done, parsedBookmarks.length);
+ }
});
const resultsPromises = limitConcurrency(importPromises, concurrencyLimit);
@@ -134,7 +151,6 @@ export async function importBookmarksFromFile(
let failures = 0;
let alreadyExisted = 0;
- let done = 0;
for (const r of results) {
if (r.status === "fulfilled") {
if (r.value.alreadyExists) alreadyExisted++;
@@ -142,10 +158,7 @@ export async function importBookmarksFromFile(
} else {
failures++;
}
- done += 1;
- onProgress?.(done, parsedBookmarks.length);
}
-
return {
counts: {
successes,
@@ -154,5 +167,6 @@ export async function importBookmarksFromFile(
total: parsedBookmarks.length,
},
rootListId: rootList.id,
+ importSessionId: session.id,
};
}
diff --git a/packages/shared/types/bookmarks.ts b/packages/shared/types/bookmarks.ts
index a22e7632..71cf1012 100644
--- a/packages/shared/types/bookmarks.ts
+++ b/packages/shared/types/bookmarks.ts
@@ -142,6 +142,7 @@ export const zNewBookmarkRequestSchema = z
// A mechanism to prioritize crawling of bookmarks depending on whether
// they were created by a user interaction or by a bulk import.
crawlPriority: z.enum(["low", "normal"]).optional(),
+ importSessionId: z.string().optional(),
})
.and(
z.discriminatedUnion("type", [
diff --git a/packages/shared/types/importSessions.ts b/packages/shared/types/importSessions.ts
new file mode 100644
index 00000000..0c1edd03
--- /dev/null
+++ b/packages/shared/types/importSessions.ts
@@ -0,0 +1,76 @@
+import { z } from "zod";
+
+export const zImportSessionStatusSchema = z.enum([
+ "pending",
+ "in_progress",
+ "completed",
+ "failed",
+]);
+export type ZImportSessionStatus = z.infer<typeof zImportSessionStatusSchema>;
+
+export const zImportSessionBookmarkStatusSchema = z.enum([
+ "pending",
+ "processing",
+ "completed",
+ "failed",
+]);
+export type ZImportSessionBookmarkStatus = z.infer<
+ typeof zImportSessionBookmarkStatusSchema
+>;
+
+export const zImportSessionSchema = z.object({
+ id: z.string(),
+ name: z.string(),
+ userId: z.string(),
+ message: z.string().nullable(),
+ rootListId: z.string().nullable(),
+ createdAt: z.date(),
+ modifiedAt: z.date().nullable(),
+});
+export type ZImportSession = z.infer<typeof zImportSessionSchema>;
+
+export const zImportSessionWithStatsSchema = zImportSessionSchema.extend({
+ status: z.enum(["pending", "in_progress", "completed", "failed"]),
+ totalBookmarks: z.number(),
+ completedBookmarks: z.number(),
+ failedBookmarks: z.number(),
+ pendingBookmarks: z.number(),
+ processingBookmarks: z.number(),
+});
+export type ZImportSessionWithStats = z.infer<
+ typeof zImportSessionWithStatsSchema
+>;
+
+export const zCreateImportSessionRequestSchema = z.object({
+ name: z.string().min(1).max(255),
+ rootListId: z.string().optional(),
+});
+export type ZCreateImportSessionRequest = z.infer<
+ typeof zCreateImportSessionRequestSchema
+>;
+
+export const zGetImportSessionStatsRequestSchema = z.object({
+ importSessionId: z.string(),
+});
+export type ZGetImportSessionStatsRequest = z.infer<
+ typeof zGetImportSessionStatsRequestSchema
+>;
+
+export const zListImportSessionsRequestSchema = z.object({});
+export type ZListImportSessionsRequest = z.infer<
+ typeof zListImportSessionsRequestSchema
+>;
+
+export const zListImportSessionsResponseSchema = z.object({
+ sessions: z.array(zImportSessionWithStatsSchema),
+});
+export type ZListImportSessionsResponse = z.infer<
+ typeof zListImportSessionsResponseSchema
+>;
+
+export const zDeleteImportSessionRequestSchema = z.object({
+ importSessionId: z.string(),
+});
+export type ZDeleteImportSessionRequest = z.infer<
+ typeof zDeleteImportSessionRequestSchema
+>;