diff options
| author | Mohamed Bassem <me@mbassem.com> | 2025-10-04 13:40:24 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-10-04 13:40:24 +0100 |
| commit | 4a580d713621f99abb8baabc9b847ce039d44842 (patch) | |
| tree | a2aa6f3ae8045ad50a9316624e2a7028dd098c6b /packages/shared | |
| parent | 5e331a7d5b8d9666812170547574804d8b6da741 (diff) | |
| download | karakeep-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.ts | 12 | ||||
| -rw-r--r-- | packages/shared/import-export/importer.ts | 50 | ||||
| -rw-r--r-- | packages/shared/types/bookmarks.ts | 1 | ||||
| -rw-r--r-- | packages/shared/types/importSessions.ts | 76 |
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 +>; |
