From 14e4fed321634dc014ad2f15cafef3ed0123855e Mon Sep 17 00:00:00 2001 From: Mohamed Bassem Date: Sun, 23 Feb 2025 22:50:12 +0000 Subject: feat: Add a setting page to manage assets. Fixes #730 --- .../[bookmarkId]/assets/[assetId]/route.ts | 4 +- .../api/v1/bookmarks/[bookmarkId]/assets/route.ts | 2 +- apps/web/app/settings/assets/page.tsx | 182 ++++++++++++++++++ apps/web/app/settings/layout.tsx | 6 + .../components/dashboard/preview/AttachmentBox.tsx | 35 +--- apps/web/lib/attachments.tsx | 14 ++ apps/web/lib/i18n/locales/en/translation.json | 11 ++ apps/web/lib/utils.ts | 12 ++ packages/shared-react/hooks/assets.ts | 49 +++++ packages/shared-react/hooks/bookmarks.ts | 45 ----- packages/trpc/lib/attachments.ts | 2 +- packages/trpc/routers/_app.ts | 2 + packages/trpc/routers/assets.test.ts | 128 +++++++++++++ packages/trpc/routers/assets.ts | 213 +++++++++++++++++++++ packages/trpc/routers/bookmarks.test.ts | 119 +----------- packages/trpc/routers/bookmarks.ts | 153 +-------------- 16 files changed, 630 insertions(+), 347 deletions(-) create mode 100644 apps/web/app/settings/assets/page.tsx create mode 100644 apps/web/lib/attachments.tsx create mode 100644 packages/shared-react/hooks/assets.ts create mode 100644 packages/trpc/routers/assets.test.ts create mode 100644 packages/trpc/routers/assets.ts diff --git a/apps/web/app/api/v1/bookmarks/[bookmarkId]/assets/[assetId]/route.ts b/apps/web/app/api/v1/bookmarks/[bookmarkId]/assets/[assetId]/route.ts index 3fc50801..88e203de 100644 --- a/apps/web/app/api/v1/bookmarks/[bookmarkId]/assets/[assetId]/route.ts +++ b/apps/web/app/api/v1/bookmarks/[bookmarkId]/assets/[assetId]/route.ts @@ -12,7 +12,7 @@ export const PUT = ( req, bodySchema: z.object({ assetId: z.string() }), handler: async ({ api, body }) => { - await api.bookmarks.replaceAsset({ + await api.assets.replaceAsset({ bookmarkId: params.params.bookmarkId, oldAssetId: params.params.assetId, newAssetId: body!.assetId, @@ -28,7 +28,7 @@ export const DELETE = ( buildHandler({ req, handler: async ({ api }) => { - await api.bookmarks.detachAsset({ + await api.assets.detachAsset({ bookmarkId: params.params.bookmarkId, assetId: params.params.assetId, }); diff --git a/apps/web/app/api/v1/bookmarks/[bookmarkId]/assets/route.ts b/apps/web/app/api/v1/bookmarks/[bookmarkId]/assets/route.ts index e5284a39..156876b6 100644 --- a/apps/web/app/api/v1/bookmarks/[bookmarkId]/assets/route.ts +++ b/apps/web/app/api/v1/bookmarks/[bookmarkId]/assets/route.ts @@ -27,7 +27,7 @@ export const POST = ( req, bodySchema: zAssetSchema, handler: async ({ api, body }) => { - const asset = await api.bookmarks.attachAsset({ + const asset = await api.assets.attachAsset({ bookmarkId: params.params.bookmarkId, asset: body!, }); diff --git a/apps/web/app/settings/assets/page.tsx b/apps/web/app/settings/assets/page.tsx new file mode 100644 index 00000000..a6c13525 --- /dev/null +++ b/apps/web/app/settings/assets/page.tsx @@ -0,0 +1,182 @@ +"use client"; + +import Link from "next/link"; +import { ActionButton } from "@/components/ui/action-button"; +import ActionConfirmingDialog from "@/components/ui/action-confirming-dialog"; +import { Button } from "@/components/ui/button"; +import { FullPageSpinner } from "@/components/ui/full-page-spinner"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { toast } from "@/components/ui/use-toast"; +import { ASSET_TYPE_TO_ICON } from "@/lib/attachments"; +import { useTranslation } from "@/lib/i18n/client"; +import { api } from "@/lib/trpc"; +import { formatBytes } from "@/lib/utils"; +import { ExternalLink, Trash2 } from "lucide-react"; + +import { useDetachBookmarkAsset } from "@hoarder/shared-react/hooks/assets"; +import { getAssetUrl } from "@hoarder/shared-react/utils/assetUtils"; +import { + humanFriendlyNameForAssertType, + isAllowedToDetachAsset, +} from "@hoarder/trpc/lib/attachments"; + +export default function AssetsSettingsPage() { + const { t } = useTranslation(); + const { mutate: detachAsset, isPending: isDetaching } = + useDetachBookmarkAsset({ + onSuccess: () => { + toast({ + description: "Asset has been deleted!", + }); + }, + onError: (e) => { + toast({ + description: e.message, + variant: "destructive", + }); + }, + }); + const { + data, + isLoading: isAssetsLoading, + fetchNextPage, + hasNextPage, + isFetchingNextPage, + } = api.assets.list.useInfiniteQuery( + { + limit: 20, + }, + { + getNextPageParam: (lastPage) => lastPage.nextCursor, + }, + ); + + const assets = data?.pages.flatMap((page) => page.assets) ?? []; + + if (isAssetsLoading) { + return ; + } + + return ( +
+
+
+ {t("settings.manage_assets.manage_assets")} +
+ {assets.length === 0 && ( +

+ {t("settings.manage_assets.no_assets")} +

+ )} + {assets.length > 0 && ( + + + + {t("settings.manage_assets.asset_type")} + {t("common.size")} + {t("settings.manage_assets.asset_link")} + + {t("settings.manage_assets.bookmark_link")} + + {t("common.actions")} + + + + {assets.map((asset) => ( + + + {ASSET_TYPE_TO_ICON[asset.assetType]} + + {humanFriendlyNameForAssertType(asset.assetType)} + + + {formatBytes(asset.size)} + + + + View Asset + + + + {asset.bookmarkId ? ( + + + View Bookmark + + ) : ( + No bookmark + )} + + + {isAllowedToDetachAsset(asset.assetType) && + asset.bookmarkId && ( + ( + + detachAsset( + { + bookmarkId: asset.bookmarkId!, + assetId: asset.id, + }, + { onSettled: () => setDialogOpen(false) }, + ) + } + > + + {t("actions.delete")} + + )} + > + + + )} + + + ))} + +
+ )} + {hasNextPage && ( +
+ fetchNextPage()} + loading={isFetchingNextPage} + ignoreDemoMode={true} + > + Load More + +
+ )} +
+
+ ); +} diff --git a/apps/web/app/settings/layout.tsx b/apps/web/app/settings/layout.tsx index 909dfd9c..62ac041c 100644 --- a/apps/web/app/settings/layout.tsx +++ b/apps/web/app/settings/layout.tsx @@ -5,6 +5,7 @@ import { TFunction } from "i18next"; import { ArrowLeft, Download, + Image, KeyRound, Link, Rss, @@ -60,6 +61,11 @@ const settingsSidebarItems = ( icon: , path: "/settings/webhooks", }, + { + name: t("settings.manage_assets.manage_assets"), + icon: , + path: "/settings/assets", + }, ]; export default async function SettingsLayout({ diff --git a/apps/web/components/dashboard/preview/AttachmentBox.tsx b/apps/web/components/dashboard/preview/AttachmentBox.tsx index 32939cb0..19622f8d 100644 --- a/apps/web/components/dashboard/preview/AttachmentBox.tsx +++ b/apps/web/components/dashboard/preview/AttachmentBox.tsx @@ -9,32 +9,18 @@ import { } from "@/components/ui/collapsible"; import FilePickerButton from "@/components/ui/file-picker-button"; import { toast } from "@/components/ui/use-toast"; +import { ASSET_TYPE_TO_ICON } from "@/lib/attachments"; import useUpload from "@/lib/hooks/upload-file"; import { useTranslation } from "@/lib/i18n/client"; -import { - Archive, - Camera, - ChevronsDownUp, - Download, - Image, - Paperclip, - Pencil, - Plus, - Trash2, - Video, -} from "lucide-react"; +import { ChevronsDownUp, Download, Pencil, Plus, Trash2 } from "lucide-react"; import { useAttachBookmarkAsset, useDetachBookmarkAsset, useReplaceBookmarkAsset, -} from "@hoarder/shared-react/hooks/bookmarks"; +} from "@hoarder/shared-react/hooks/assets"; import { getAssetUrl } from "@hoarder/shared-react/utils/assetUtils"; -import { - BookmarkTypes, - ZAssetType, - ZBookmark, -} from "@hoarder/shared/types/bookmarks"; +import { BookmarkTypes, ZBookmark } from "@hoarder/shared/types/bookmarks"; import { humanFriendlyNameForAssertType, isAllowedToAttachAsset, @@ -43,17 +29,6 @@ import { export default function AttachmentBox({ bookmark }: { bookmark: ZBookmark }) { const { t } = useTranslation(); - const typeToIcon: Record = { - screenshot: , - assetScreenshot: , - fullPageArchive: , - precrawledArchive: , - bannerImage: , - video: