Skip to content

Commit

Permalink
feat(import): restructure bookmark import flow and add edit capabilities
Browse files Browse the repository at this point in the history
Signed-off-by: Robert Goniszewski <[email protected]>
  • Loading branch information
goniszewski committed Nov 12, 2024
1 parent cf985ab commit dfbc421
Show file tree
Hide file tree
Showing 8 changed files with 261 additions and 205 deletions.
4 changes: 2 additions & 2 deletions src/lib/components/BulkList/BulkList.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { type Readable } from 'svelte/store';
import BulkListItem from '../BulkListItem/BulkListItem.svelte';
export let itemList: Readable<BulkListItem[]>;
export let isLoading = false;
export let isLoading: boolean;
const selectAllItems = ({ target }: Event) => {
if (target instanceof HTMLInputElement) {
Expand Down Expand Up @@ -39,7 +39,7 @@ const selectAllItems = ({ target }: Event) => {
title={item.title}
category={item.category}
selected={item.selected}
isLoading={item.isLoading}
isLoading={isLoading}
metadataFetched={!!item.contentHtml}
metadata={item} />
{/each}
Expand Down
1 change: 1 addition & 0 deletions src/lib/components/BulkListItem/BulkListItem.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ let urlObj = new URL(url);
const onEditItem = () => {
editBookmarkStore.set({
imported: true,
description: '',
note: '',
...metadata,
Expand Down
27 changes: 25 additions & 2 deletions src/lib/components/EditBookmarkForm/EditBookmarkForm.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { writable, type Writable } from 'svelte/store';
import { invalidate } from '$app/navigation';
import { editBookmarkStore } from '$lib/stores/edit-bookmark.store';
import { searchEngine } from '$lib/stores/search.store';
import type { Bookmark } from '$lib/types/Bookmark.type';
import type { Bookmark, BookmarkEdit } from '$lib/types/Bookmark.type';
import { updateBookmarkInSearchIndex } from '$lib/utils/search';
import { showToast } from '$lib/utils/show-toast';
Expand All @@ -19,7 +19,7 @@ export let closeModal: () => void;
let error = '';
const loading = writable(false);
const bookmark = writable<Partial<Bookmark>>({});
const bookmark = writable<Partial<Bookmark> | BookmarkEdit>();
$: $bookmark = { ...$editBookmarkStore };
Expand Down Expand Up @@ -92,6 +92,21 @@ function handleTagsChange() {
];
}
function handleImportedBookmarkEdit() {
const formData = new FormData(form);
let rawData = Object.fromEntries(formData as any);
console.log('rawData', rawData);
delete rawData.tags;
editBookmarkStore.set({
...$bookmark,
...rawData,
category: JSON.parse(rawData.category)?.label,
bookmarkTags: $bookmarkTags
});
console.log('$editBookmarkStore', $editBookmarkStore);
closeModal();
}
const onGetMetadata = _.debounce(
async (event: Event) => {
const validateUrlRegex =
Expand Down Expand Up @@ -376,6 +391,14 @@ const onGetMetadata = _.debounce(

<button
class="btn btn-primary mx-auto my-6 w-full max-w-xs"
on:click|preventDefault={() => {
console.log('$bookmark', $bookmark);
if ($bookmark.imported) {
handleImportedBookmarkEdit();
} else {
form.submit();
}
}}
disabled={$loading || !$bookmark.url || !$bookmark.title}>Save</button>
</div>
</div>
Expand Down
4 changes: 2 additions & 2 deletions src/lib/stores/edit-bookmark.store.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Bookmark } from '$lib/types/Bookmark.type';
import type { Bookmark, BookmarkEdit } from '$lib/types/Bookmark.type';
import { writable } from 'svelte/store';

export const editBookmarkStore = writable<Partial<Bookmark>>({});
export const editBookmarkStore = writable<Partial<Bookmark> | BookmarkEdit>({});
16 changes: 16 additions & 0 deletions src/lib/types/Bookmark.type.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Category } from './Category.type';
import type { Metadata } from './Metadata.type';
import type { Tag } from './Tag.type';
import type { User } from './User.type';

Expand Down Expand Up @@ -38,3 +39,18 @@ export type BookmarkForIndex = Omit<Bookmark, 'mainImage' | 'icon' | 'screenshot
tags: Tag[];
category: Omit<Category, 'parent' | 'owner'>;
};

export type BookmarkEdit = Partial<Metadata> & {
id: number;
icon: string | null;
url: string;
title: string;
category: string;
selected: boolean;
imported?: boolean;
bookmarkTags?: {
value: string;
label: string;
created?: boolean;
}[];
};
11 changes: 2 additions & 9 deletions src/lib/types/common/BulkList.type.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,3 @@
import type { Metadata } from '../Metadata.type';
import type { BookmarkEdit } from '../Bookmark.type';

export type BulkListItem = Partial<Metadata> & {
id: number;
icon: string | null;
url: string;
title: string;
category: string;
selected: boolean;
};
export type BulkListItem = BookmarkEdit;
198 changes: 8 additions & 190 deletions src/routes/import/+page.svelte
Original file line number Diff line number Diff line change
@@ -1,194 +1,12 @@
<script lang="ts">
import { page } from '$app/stores';
import BulkList from '$lib/components/BulkList/BulkList.svelte';
import Pagination from '$lib/components/Pagination/Pagination.svelte';
import Select from '$lib/components/Select/Select.svelte';
import { importBookmarkStore } from '$lib/stores/import-bookmarks.store';
import type { BulkListItem } from '$lib/types/common/BulkList.type';
import { importBookmarks } from '$lib/utils/import-bookmarks';
import { showToast } from '$lib/utils/show-toast';
import { derived, writable } from 'svelte/store';
const step = writable<number>(1);
const isFetchingMetadata = writable<boolean>(true);
const defaultCategory = '[No parent]';
const selectedCategory = writable<string>();
const processedItems = writable<number>(0);
const currentItems = derived([importBookmarkStore, page], ([$importBookmarkStore, $page]) => {
return $importBookmarkStore.slice(
($page.data.page - 1) * $page.data.limit,
$page.data.page * $page.data.limit
);
});
const { isAnySelected, length: itemsCount } = importBookmarkStore;
const processMetadataQueue = async (items: BulkListItem[]) => {
const CONCURRENT_REQUESTS = 2;
const queue = [...items.filter((item) => !item.contentHtml)];
const results: BulkListItem[] = [];
isFetchingMetadata.set(true);
while (queue.length > 0) {
const batch = queue.splice(0, CONCURRENT_REQUESTS);
const batchPromises = batch.map(async (item) => {
if (item.contentHtml) {
return item;
}
try {
const response = await fetch('/api/fetch-metadata', {
method: 'POST',
body: JSON.stringify({ url: item.url }),
headers: { 'Content-Type': 'application/json' }
});
const { metadata } = await response.json();
processedItems.update((count) => count + 1);
return {
...metadata,
...item,
icon: item.icon || metadata.iconUrl,
title: item.title || metadata.title
};
} catch (error) {
console.error(`Failed to fetch metadata for ${item.url}:`, error);
return item;
}
});
const batchResults = await Promise.all(batchPromises);
results.push(...batchResults);
importBookmarkStore.set(results.concat(queue));
}
const failedItemsCount = results.filter((item) => !item.contentHtml).length;
isFetchingMetadata.set(false);
showToast.success(
`Successfully imported ${results.length - failedItemsCount} bookmarks from ${
items.length
} items (${failedItemsCount} failed).`,
{
icon: failedItemsCount ? '⚠️' : '🎉',
duration: 3000
}
);
};
const categoriesOptions = writable<{ value: string; label: string }[]>([]);
$: {
$categoriesOptions = [
...[...new Set($importBookmarkStore.map((item) => item.category))].map((category) => ({
value: category,
label: category,
group: 'Imported'
})),
...$page.data.categories.map((c) => ({
value: `${c.id}`,
label: c.name,
group: 'Existing'
}))
];
}
const onFileSelected = async (event: Event) => {
const input = event.target as HTMLInputElement;
if (input.files && input.files.length > 0) {
const fileContent = await input.files[0].text();
const importedData = await importBookmarks(fileContent, 'netscape');
const updatedBookmarks = importedData.bookmarks.map((bookmark, i) => ({
...bookmark,
id: i + 1,
icon: bookmark.icon || null,
category: bookmark.categorySlug || defaultCategory,
description: bookmark.description || undefined,
selected: false
}));
importBookmarkStore.set(updatedBookmarks);
processMetadataQueue($importBookmarkStore);
step.set(2);
}
};
const onSelectCategory = (
e: CustomEvent<{
value: string;
label: string;
}>
) => {
$selectedCategory = e.detail.value;
};
const onSetSelectedCategory = () => {
importBookmarkStore.update((items) =>
items.map((item) => (item.selected ? { ...item, category: $selectedCategory } : item))
);
};
</script>

<div class="flex max-w-4xl flex-col">
{#if $step === 1}
<h1 class="mb-4 text-2xl font-bold">Import bookmarks from HTML file</h1>
<input
type="file"
title="Select backup file"
id="backup"
name="backup"
accept=".html,.htm"
multiple={false}
class="file-input file-input-bordered file-input-primary file-input-md w-full max-w-xs"
on:change={onFileSelected} />
{:else if $step === 2}
<div class="mb-4 flex w-full gap-2 pl-12">
<button
class="btn btn-primary btn-sm"
disabled={$isFetchingMetadata}
on:click={importBookmarkStore.removeSelected}>IMPORT</button>

{#if $isAnySelected && !$isFetchingMetadata}
<Select
name="category"
searchable
placeholder="Change category"
size="md"
items={$categoriesOptions}
onSelect={onSelectCategory} />
<button class="btn btn-primary btn-sm" on:click={onSetSelectedCategory}> SET </button>
{/if}

<div class="ml-auto flex gap-2">
<button
class="btn btn-primary btn-sm"
disabled={!$isAnySelected && !$selectedCategory}
on:click={importBookmarkStore.removeSelected}>DELETE</button>
</div>
</div>
<div class="flex min-h-6 flex-col items-center gap-2">
<div class="flex items-center justify-center">
{#if $isFetchingMetadata}
<span class="mr-2">Fetching metadata...</span>
<progress
class="progress progress-primary w-56"
value={$processedItems}
max={$importBookmarkStore.length}>
</progress>
{:else}
<span class="mr-2">
Done! {$importBookmarkStore.length === $processedItems
? 'All items processed.'
: `${$processedItems} of ${$importBookmarkStore.length} items processed 🪄`}
</span>
{/if}
</div>
</div>

<BulkList itemList={currentItems} isLoading={$isFetchingMetadata} />
<Pagination
page={$page.data.page}
limit={$page.data.limit}
items={$itemsCount}
position="right" />
{/if}
<div class="container mx-auto p-4">
<div class="flex flex-col gap-4">
<h1 class="text-2xl font-bold">Import Bookmarks</h1>
<p>Choose a type of import:</p>
</div>
<div class="rounded-smp-4 mx-auto mt-8 flex max-w-lg flex-wrap justify-center gap-4">
<a href="/import/html" class="btn btn-primary w-[calc(50%-0.5rem)]"> HTML bookmark file</a>
</div>
</div>
Loading

0 comments on commit dfbc421

Please sign in to comment.