diff options
| author | MohamedBassem <me@mbassem.com> | 2024-04-15 18:39:59 +0100 |
|---|---|---|
| committer | MohamedBassem <me@mbassem.com> | 2024-04-15 18:55:34 +0100 |
| commit | 81e0b2849d837649da9adbc5d077b8c819fe7bee (patch) | |
| tree | 003bb21413372825dc19c07a87bdbe6692e384a9 | |
| parent | 5c9acb1cb3bfe341378b91bbed344dd7202a00d7 (diff) | |
| download | karakeep-81e0b2849d837649da9adbc5d077b8c819fe7bee.tar.zst | |
feature: Add title to bookmarks and allow editing them. Fixes #27
| -rw-r--r-- | apps/mobile/components/bookmarks/BookmarkCard.tsx | 9 | ||||
| -rw-r--r-- | apps/web/components/dashboard/bookmarks/AssetCard.tsx | 2 | ||||
| -rw-r--r-- | apps/web/components/dashboard/bookmarks/LinkCard.tsx | 2 | ||||
| -rw-r--r-- | apps/web/components/dashboard/bookmarks/TextCard.tsx | 1 | ||||
| -rw-r--r-- | apps/web/components/dashboard/preview/BookmarkPreview.tsx | 51 | ||||
| -rw-r--r-- | apps/web/components/dashboard/preview/EditableTitle.tsx | 165 | ||||
| -rw-r--r-- | apps/web/components/ui/action-button.tsx | 40 | ||||
| -rw-r--r-- | apps/web/components/ui/button.tsx | 27 | ||||
| -rw-r--r-- | apps/workers/searchWorker.ts | 3 | ||||
| -rw-r--r-- | packages/db/drizzle/0018_bright_infant_terrible.sql | 1 | ||||
| -rw-r--r-- | packages/db/drizzle/meta/0018_snapshot.json | 974 | ||||
| -rw-r--r-- | packages/db/drizzle/meta/_journal.json | 7 | ||||
| -rw-r--r-- | packages/db/schema.ts | 1 | ||||
| -rw-r--r-- | packages/shared/search.ts | 1 | ||||
| -rw-r--r-- | packages/trpc/routers/bookmarks.ts | 1 | ||||
| -rw-r--r-- | packages/trpc/types/bookmarks.ts | 4 | ||||
| -rw-r--r-- | tooling/eslint/base.js | 5 |
17 files changed, 1240 insertions, 54 deletions
diff --git a/apps/mobile/components/bookmarks/BookmarkCard.tsx b/apps/mobile/components/bookmarks/BookmarkCard.tsx index d4fbcb58..76a05aef 100644 --- a/apps/mobile/components/bookmarks/BookmarkCard.tsx +++ b/apps/mobile/components/bookmarks/BookmarkCard.tsx @@ -201,7 +201,7 @@ function LinkCard({ bookmark }: { bookmark: ZBookmark }) { className="line-clamp-2 text-xl font-bold" onPress={() => WebBrowser.openBrowserAsync(url)} > - {bookmark.content.title ?? parsedUrl.host} + {bookmark.title ?? bookmark.content.title ?? parsedUrl.host} </Text> <TagList bookmark={bookmark} /> <Divider orientation="vertical" className="mt-2 h-0.5 w-full" /> @@ -220,6 +220,9 @@ function TextCard({ bookmark }: { bookmark: ZBookmark }) { } return ( <View className="flex max-h-96 gap-2 p-2"> + {bookmark.title && ( + <Text className="line-clamp-2 text-xl font-bold">{bookmark.title}</Text> + )} <View className="max-h-56 overflow-hidden p-2"> <Markdown>{bookmark.content.text}</Markdown> </View> @@ -238,6 +241,7 @@ function AssetCard({ bookmark }: { bookmark: ZBookmark }) { if (bookmark.content.type !== "asset") { throw new Error("Wrong content type rendered"); } + const title = bookmark.title ?? bookmark.content.fileName; return ( <View className="flex gap-2"> @@ -251,6 +255,9 @@ function AssetCard({ bookmark }: { bookmark: ZBookmark }) { className="h-56 min-h-56 w-full object-cover" /> <View className="flex gap-2 p-2"> + {title && ( + <Text className="line-clamp-2 text-xl font-bold">{title}</Text> + )} <TagList bookmark={bookmark} /> <Divider orientation="vertical" className="mt-2 h-0.5 w-full" /> <View className="mt-2 flex flex-row justify-between px-2 pb-2"> diff --git a/apps/web/components/dashboard/bookmarks/AssetCard.tsx b/apps/web/components/dashboard/bookmarks/AssetCard.tsx index 3bda1ee8..ea0317aa 100644 --- a/apps/web/components/dashboard/bookmarks/AssetCard.tsx +++ b/apps/web/components/dashboard/bookmarks/AssetCard.tsx @@ -80,7 +80,7 @@ export default function AssetCard({ return ( <BookmarkLayoutAdaptingCard - title={bookmarkedAsset.content.fileName} + title={bookmarkedAsset.title ?? bookmarkedAsset.content.fileName} footer={null} bookmark={bookmarkedAsset} className={className} diff --git a/apps/web/components/dashboard/bookmarks/LinkCard.tsx b/apps/web/components/dashboard/bookmarks/LinkCard.tsx index 5c329424..6d51695d 100644 --- a/apps/web/components/dashboard/bookmarks/LinkCard.tsx +++ b/apps/web/components/dashboard/bookmarks/LinkCard.tsx @@ -16,7 +16,7 @@ function LinkTitle({ bookmark }: { bookmark: ZBookmarkTypeLink }) { const parsedUrl = new URL(link.url); return ( <Link className="line-clamp-2" href={link.url} target="_blank"> - {link?.title ?? parsedUrl.host} + {bookmark.title ?? link?.title ?? parsedUrl.host} </Link> ); } diff --git a/apps/web/components/dashboard/bookmarks/TextCard.tsx b/apps/web/components/dashboard/bookmarks/TextCard.tsx index c715c8ab..e24108d2 100644 --- a/apps/web/components/dashboard/bookmarks/TextCard.tsx +++ b/apps/web/components/dashboard/bookmarks/TextCard.tsx @@ -51,6 +51,7 @@ export default function TextCard({ setOpen={setPreviewModalOpen} /> <BookmarkLayoutAdaptingCard + title={bookmark.title} content={ <Markdown className="prose dark:prose-invert"> {bookmarkedText.text} diff --git a/apps/web/components/dashboard/preview/BookmarkPreview.tsx b/apps/web/components/dashboard/preview/BookmarkPreview.tsx index 73e49376..93f14c64 100644 --- a/apps/web/components/dashboard/preview/BookmarkPreview.tsx +++ b/apps/web/components/dashboard/preview/BookmarkPreview.tsx @@ -24,6 +24,7 @@ import type { ZBookmark } from "@hoarder/trpc/types/bookmarks"; import ActionBar from "./ActionBar"; import { AssetContentSection } from "./AssetContentSection"; +import { EditableTitle } from "./EditableTitle"; import { NoteEditor } from "./NoteEditor"; import { TextContentSection } from "./TextContentSection"; @@ -62,37 +63,6 @@ function CreationTime({ createdAt }: { createdAt: Date }) { ); } -function LinkHeader({ bookmark }: { bookmark: ZBookmark }) { - if (bookmark.content.type !== "link") { - throw new Error("Unexpected content type"); - } - - const title = bookmark.content.title ?? bookmark.content.url; - - return ( - <div className="flex w-full flex-col items-center justify-center space-y-3"> - <Tooltip> - <TooltipTrigger asChild> - <p className="line-clamp-2 text-center text-lg">{title}</p> - </TooltipTrigger> - <TooltipPortal> - <TooltipContent side="bottom" className="w-96"> - {title} - </TooltipContent> - </TooltipPortal> - </Tooltip> - <Link - href={bookmark.content.url} - className="mx-auto flex gap-2 text-gray-400" - > - <span className="my-auto">View Original</span> - <ExternalLink /> - </Link> - <Separator /> - </div> - ); -} - export default function BookmarkPreview({ initialData, }: { @@ -131,17 +101,26 @@ export default function BookmarkPreview({ } } - const linkHeader = bookmark.content.type == "link" && ( - <LinkHeader bookmark={bookmark} /> - ); - return ( <div className="grid h-full grid-rows-3 gap-2 overflow-hidden bg-background lg:grid-cols-3 lg:grid-rows-none"> <div className="row-span-2 h-full w-full overflow-auto p-2 md:col-span-2 lg:row-auto"> {isBookmarkStillCrawling(bookmark) ? <ContentLoading /> : content} </div> <div className="lg:col-span1 row-span-1 flex flex-col gap-4 overflow-auto bg-accent p-4 lg:row-auto"> - {linkHeader} + <div className="flex w-full flex-col items-center justify-center gap-y-2"> + <EditableTitle bookmark={bookmark} /> + {bookmark.content.type == "link" && ( + <Link + href={bookmark.content.url} + className="flex items-center gap-2 text-gray-400" + > + <span>View Original</span> + <ExternalLink /> + </Link> + )} + <Separator /> + </div> + <CreationTime createdAt={bookmark.createdAt} /> <div className="flex gap-4"> <p className="text-sm text-gray-400">Tags</p> diff --git a/apps/web/components/dashboard/preview/EditableTitle.tsx b/apps/web/components/dashboard/preview/EditableTitle.tsx new file mode 100644 index 00000000..1500212d --- /dev/null +++ b/apps/web/components/dashboard/preview/EditableTitle.tsx @@ -0,0 +1,165 @@ +import { useEffect, useRef, useState } from "react"; +import { ActionButtonWithTooltip } from "@/components/ui/action-button"; +import { ButtonWithTooltip } from "@/components/ui/button"; +import { + Tooltip, + TooltipContent, + TooltipPortal, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { toast } from "@/components/ui/use-toast"; +import { Check, Pencil, X } from "lucide-react"; + +import { useUpdateBookmark } from "@hoarder/shared-react/hooks/bookmarks"; +import { ZBookmark } from "@hoarder/trpc/types/bookmarks"; + +interface Props { + bookmarkId: string; + originalTitle: string | null; + setEditable: (editable: boolean) => void; +} + +function EditMode({ bookmarkId, originalTitle, setEditable }: Props) { + const ref = useRef<HTMLDivElement>(null); + + const { mutate: updateBookmark, isPending } = useUpdateBookmark({ + onSuccess: () => { + toast({ + description: "Title updated!", + }); + }, + }); + + useEffect(() => { + if (ref.current) { + ref.current.focus(); + ref.current.textContent = originalTitle; + } + }, [ref]); + + const onSave = () => { + let toSave: string | null = ref.current?.textContent ?? null; + if (originalTitle == toSave) { + // Nothing to do here + return; + } + if (toSave == "") { + toSave = null; + } + updateBookmark({ + bookmarkId, + title: toSave, + }); + setEditable(false); + }; + + return ( + <div className="flex gap-3"> + <div + ref={ref} + role="presentation" + className="p-2 text-center text-lg" + contentEditable={true} + onKeyDown={(e) => { + if (e.key === "Enter") { + e.preventDefault(); + } + }} + /> + <ActionButtonWithTooltip + tooltip="Save" + delayDuration={500} + size="none" + variant="ghost" + className="align-middle text-gray-400" + loading={isPending} + onClick={() => onSave()} + > + <Check className="size-4" /> + </ActionButtonWithTooltip> + <ButtonWithTooltip + tooltip="Cancel" + delayDuration={500} + size="none" + variant="ghost" + className="align-middle text-gray-400" + onClick={() => { + setEditable(false); + }} + > + <X className="size-4" /> + </ButtonWithTooltip> + </div> + ); +} + +function ViewMode({ originalTitle, setEditable }: Props) { + return ( + <Tooltip delayDuration={500}> + <div className="flex items-center gap-3 text-center"> + <TooltipTrigger asChild> + {originalTitle ? ( + <p className="line-clamp-2 text-lg">{originalTitle}</p> + ) : ( + <p className="text-lg italic text-gray-600">Untitled</p> + )} + </TooltipTrigger> + <ButtonWithTooltip + delayDuration={500} + tooltip="Edit title" + size="none" + variant="ghost" + className="align-middle text-gray-400" + onClick={() => { + setEditable(true); + }} + > + <Pencil className="size-4" /> + </ButtonWithTooltip> + </div> + <TooltipPortal> + {originalTitle && ( + <TooltipContent side="bottom" className="max-w-[40ch]"> + {originalTitle} + </TooltipContent> + )} + </TooltipPortal> + </Tooltip> + ); +} + +export function EditableTitle({ bookmark }: { bookmark: ZBookmark }) { + const [editable, setEditable] = useState(false); + + let title: string | null = null; + switch (bookmark.content.type) { + case "link": + title = bookmark.content.title ?? bookmark.content.url; + break; + case "text": + title = null; + break; + case "asset": + title = bookmark.content.fileName ?? null; + break; + } + + title = bookmark.title ?? title; + if (title == "") { + title = null; + } + + return editable ? ( + <EditMode + bookmarkId={bookmark.id} + originalTitle={title} + setEditable={setEditable} + /> + ) : ( + <ViewMode + bookmarkId={bookmark.id} + originalTitle={title} + setEditable={setEditable} + /> + ); +} diff --git a/apps/web/components/ui/action-button.tsx b/apps/web/components/ui/action-button.tsx index e9cdc3c9..2ac361f5 100644 --- a/apps/web/components/ui/action-button.tsx +++ b/apps/web/components/ui/action-button.tsx @@ -4,15 +4,20 @@ import { useClientConfig } from "@/lib/clientConfig"; import type { ButtonProps } from "./button"; import { Button } from "./button"; import LoadingSpinner from "./spinner"; +import { + Tooltip, + TooltipContent, + TooltipPortal, + TooltipTrigger, +} from "./tooltip"; -const ActionButton = React.forwardRef< - HTMLButtonElement, - ButtonProps & { - loading: boolean; - spinner?: React.ReactNode; - ignoreDemoMode?: boolean; - } ->( +interface ActionButtonProps extends ButtonProps { + loading: boolean; + spinner?: React.ReactNode; + ignoreDemoMode?: boolean; +} + +const ActionButton = React.forwardRef<HTMLButtonElement, ActionButtonProps>( ( { children, loading, spinner, disabled, ignoreDemoMode = false, ...props }, ref, @@ -35,4 +40,21 @@ const ActionButton = React.forwardRef< ); ActionButton.displayName = "ActionButton"; -export { ActionButton }; +const ActionButtonWithTooltip = React.forwardRef< + HTMLButtonElement, + ActionButtonProps & { tooltip: string; delayDuration?: number } +>(({ tooltip, delayDuration, ...props }, ref) => { + return ( + <Tooltip delayDuration={delayDuration}> + <TooltipTrigger> + <ActionButton ref={ref} {...props} /> + </TooltipTrigger> + <TooltipPortal> + <TooltipContent>{tooltip}</TooltipContent> + </TooltipPortal> + </Tooltip> + ); +}); +ActionButtonWithTooltip.displayName = "ActionButtonWithTooltip"; + +export { ActionButton, ActionButtonWithTooltip }; diff --git a/apps/web/components/ui/button.tsx b/apps/web/components/ui/button.tsx index 40794eb2..2d6dee6b 100644 --- a/apps/web/components/ui/button.tsx +++ b/apps/web/components/ui/button.tsx @@ -4,6 +4,13 @@ import { cn } from "@/lib/utils"; import { Slot } from "@radix-ui/react-slot"; import { cva } from "class-variance-authority"; +import { + Tooltip, + TooltipContent, + TooltipPortal, + TooltipTrigger, +} from "./tooltip"; + const buttonVariants = cva( "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", { @@ -24,6 +31,7 @@ const buttonVariants = cva( link: "text-primary underline-offset-4 hover:underline", }, size: { + none: "", default: "h-10 px-4 py-2", sm: "h-9 rounded-md px-3", lg: "h-11 rounded-md px-8", @@ -57,4 +65,21 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>( ); Button.displayName = "Button"; -export { Button, buttonVariants }; +const ButtonWithTooltip = React.forwardRef< + HTMLButtonElement, + ButtonProps & { tooltip: string; delayDuration?: number } +>(({ tooltip, delayDuration, ...props }, ref) => { + return ( + <Tooltip delayDuration={delayDuration}> + <TooltipTrigger> + <Button ref={ref} {...props} /> + </TooltipTrigger> + <TooltipPortal> + <TooltipContent>{tooltip}</TooltipContent> + </TooltipPortal> + </Tooltip> + ); +}); +ButtonWithTooltip.displayName = "ButtonWithTooltip"; + +export { Button, buttonVariants, ButtonWithTooltip }; diff --git a/apps/workers/searchWorker.ts b/apps/workers/searchWorker.ts index 42a28bc1..a2dff49c 100644 --- a/apps/workers/searchWorker.ts +++ b/apps/workers/searchWorker.ts @@ -68,7 +68,7 @@ async function runIndex( ...(bookmark.link ? { url: bookmark.link.url, - title: bookmark.link.title, + linkTitle: bookmark.link.title, description: bookmark.link.description, content: bookmark.link.content, } @@ -81,6 +81,7 @@ async function runIndex( : undefined), ...(bookmark.text ? { content: bookmark.text.text } : undefined), note: bookmark.note, + title: bookmark.title, createdAt: bookmark.createdAt.toISOString(), tags: bookmark.tagsOnBookmarks.map((t) => t.tag.name), }, diff --git a/packages/db/drizzle/0018_bright_infant_terrible.sql b/packages/db/drizzle/0018_bright_infant_terrible.sql new file mode 100644 index 00000000..bbe5e5b0 --- /dev/null +++ b/packages/db/drizzle/0018_bright_infant_terrible.sql @@ -0,0 +1 @@ +ALTER TABLE bookmarks ADD `title` text;
\ No newline at end of file diff --git a/packages/db/drizzle/meta/0018_snapshot.json b/packages/db/drizzle/meta/0018_snapshot.json new file mode 100644 index 00000000..331b2773 --- /dev/null +++ b/packages/db/drizzle/meta/0018_snapshot.json @@ -0,0 +1,974 @@ +{ + "version": "5", + "dialect": "sqlite", + "id": "ca4ff2bd-aed3-474d-92a1-68e3a8349e01", + "prevId": "58699146-ac61-4f68-94d3-645566b26b52", + "tables": { + "account": { + "name": "account", + "columns": { + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "providerAccountId": { + "name": "providerAccountId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "token_type": { + "name": "token_type", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "session_state": { + "name": "session_state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "account_userId_user_id_fk": { + "name": "account_userId_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "account_provider_providerAccountId_pk": { + "columns": [ + "provider", + "providerAccountId" + ], + "name": "account_provider_providerAccountId_pk" + } + }, + "uniqueConstraints": {} + }, + "apiKey": { + "name": "apiKey", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "keyId": { + "name": "keyId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "keyHash": { + "name": "keyHash", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "apiKey_keyId_unique": { + "name": "apiKey_keyId_unique", + "columns": [ + "keyId" + ], + "isUnique": true + }, + "apiKey_name_userId_unique": { + "name": "apiKey_name_userId_unique", + "columns": [ + "name", + "userId" + ], + "isUnique": true + } + }, + "foreignKeys": { + "apiKey_userId_user_id_fk": { + "name": "apiKey_userId_user_id_fk", + "tableFrom": "apiKey", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "bookmarkAssets": { + "name": "bookmarkAssets", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "assetType": { + "name": "assetType", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "assetId": { + "name": "assetId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "metadata": { + "name": "metadata", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "fileName": { + "name": "fileName", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "bookmarkAssets_id_bookmarks_id_fk": { + "name": "bookmarkAssets_id_bookmarks_id_fk", + "tableFrom": "bookmarkAssets", + "tableTo": "bookmarks", + "columnsFrom": [ + "id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "bookmarkLinks": { + "name": "bookmarkLinks", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "imageUrl": { + "name": "imageUrl", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "favicon": { + "name": "favicon", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "htmlContent": { + "name": "htmlContent", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "crawledAt": { + "name": "crawledAt", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "crawlStatus": { + "name": "crawlStatus", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'pending'" + } + }, + "indexes": {}, + "foreignKeys": { + "bookmarkLinks_id_bookmarks_id_fk": { + "name": "bookmarkLinks_id_bookmarks_id_fk", + "tableFrom": "bookmarkLinks", + "tableTo": "bookmarks", + "columnsFrom": [ + "id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "bookmarkLists": { + "name": "bookmarkLists", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "bookmarkLists_userId_idx": { + "name": "bookmarkLists_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + }, + "bookmarkLists_name_userId_unique": { + "name": "bookmarkLists_name_userId_unique", + "columns": [ + "name", + "userId" + ], + "isUnique": true + } + }, + "foreignKeys": { + "bookmarkLists_userId_user_id_fk": { + "name": "bookmarkLists_userId_user_id_fk", + "tableFrom": "bookmarkLists", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "bookmarkTags": { + "name": "bookmarkTags", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "bookmarkTags_name_idx": { + "name": "bookmarkTags_name_idx", + "columns": [ + "name" + ], + "isUnique": false + }, + "bookmarkTags_userId_idx": { + "name": "bookmarkTags_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + }, + "bookmarkTags_userId_name_unique": { + "name": "bookmarkTags_userId_name_unique", + "columns": [ + "userId", + "name" + ], + "isUnique": true + } + }, + "foreignKeys": { + "bookmarkTags_userId_user_id_fk": { + "name": "bookmarkTags_userId_user_id_fk", + "tableFrom": "bookmarkTags", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "bookmarkTexts": { + "name": "bookmarkTexts", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "text": { + "name": "text", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "bookmarkTexts_id_bookmarks_id_fk": { + "name": "bookmarkTexts_id_bookmarks_id_fk", + "tableFrom": "bookmarkTexts", + "tableTo": "bookmarks", + "columnsFrom": [ + "id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "bookmarks": { + "name": "bookmarks", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "createdAt": { + "name": "createdAt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "archived": { + "name": "archived", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "favourited": { + "name": "favourited", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "taggingStatus": { + "name": "taggingStatus", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'pending'" + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "bookmarks_userId_idx": { + "name": "bookmarks_userId_idx", + "columns": [ + "userId" + ], + "isUnique": false + }, + "bookmarks_archived_idx": { + "name": "bookmarks_archived_idx", + "columns": [ + "archived" + ], + "isUnique": false + }, + "bookmarks_favourited_idx": { + "name": "bookmarks_favourited_idx", + "columns": [ + "favourited" + ], + "isUnique": false + }, + "bookmarks_createdAt_idx": { + "name": "bookmarks_createdAt_idx", + "columns": [ + "createdAt" + ], + "isUnique": false + } + }, + "foreignKeys": { + "bookmarks_userId_user_id_fk": { + "name": "bookmarks_userId_user_id_fk", + "tableFrom": "bookmarks", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "bookmarksInLists": { + "name": "bookmarksInLists", + "columns": { + "bookmarkId": { + "name": "bookmarkId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "listId": { + "name": "listId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "addedAt": { + "name": "addedAt", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": { + "bookmarksInLists_bookmarkId_idx": { + "name": "bookmarksInLists_bookmarkId_idx", + "columns": [ + "bookmarkId" + ], + "isUnique": false + }, + "bookmarksInLists_listId_idx": { + "name": "bookmarksInLists_listId_idx", + "columns": [ + "listId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "bookmarksInLists_bookmarkId_bookmarks_id_fk": { + "name": "bookmarksInLists_bookmarkId_bookmarks_id_fk", + "tableFrom": "bookmarksInLists", + "tableTo": "bookmarks", + "columnsFrom": [ + "bookmarkId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "bookmarksInLists_listId_bookmarkLists_id_fk": { + "name": "bookmarksInLists_listId_bookmarkLists_id_fk", + "tableFrom": "bookmarksInLists", + "tableTo": "bookmarkLists", + "columnsFrom": [ + "listId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "bookmarksInLists_bookmarkId_listId_pk": { + "columns": [ + "bookmarkId", + "listId" + ], + "name": "bookmarksInLists_bookmarkId_listId_pk" + } + }, + "uniqueConstraints": {} + }, + "session": { + "name": "session", + "columns": { + "sessionToken": { + "name": "sessionToken", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires": { + "name": "expires", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_userId_user_id_fk": { + "name": "session_userId_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "tagsOnBookmarks": { + "name": "tagsOnBookmarks", + "columns": { + "bookmarkId": { + "name": "bookmarkId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "tagId": { + "name": "tagId", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "attachedAt": { + "name": "attachedAt", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "attachedBy": { + "name": "attachedBy", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": { + "tagsOnBookmarks_tagId_idx": { + "name": "tagsOnBookmarks_tagId_idx", + "columns": [ + "bookmarkId" + ], + "isUnique": false + }, + "tagsOnBookmarks_bookmarkId_idx": { + "name": "tagsOnBookmarks_bookmarkId_idx", + "columns": [ + "bookmarkId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "tagsOnBookmarks_bookmarkId_bookmarks_id_fk": { + "name": "tagsOnBookmarks_bookmarkId_bookmarks_id_fk", + "tableFrom": "tagsOnBookmarks", + "tableTo": "bookmarks", + "columnsFrom": [ + "bookmarkId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "tagsOnBookmarks_tagId_bookmarkTags_id_fk": { + "name": "tagsOnBookmarks_tagId_bookmarkTags_id_fk", + "tableFrom": "tagsOnBookmarks", + "tableTo": "bookmarkTags", + "columnsFrom": [ + "tagId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "tagsOnBookmarks_bookmarkId_tagId_pk": { + "columns": [ + "bookmarkId", + "tagId" + ], + "name": "tagsOnBookmarks_bookmarkId_tagId_pk" + } + }, + "uniqueConstraints": {} + }, + "user": { + "name": "user", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true, + "autoincrement": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "emailVerified": { + "name": "emailVerified", + "type": "integer", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false, + "default": "'user'" + } + }, + "indexes": { + "user_email_unique": { + "name": "user_email_unique", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "verificationToken": { + "name": "verificationToken", + "columns": { + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "expires": { + "name": "expires", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "verificationToken_identifier_token_pk": { + "columns": [ + "identifier", + "token" + ], + "name": "verificationToken_identifier_token_pk" + } + }, + "uniqueConstraints": {} + } + }, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + } +}
\ No newline at end of file diff --git a/packages/db/drizzle/meta/_journal.json b/packages/db/drizzle/meta/_journal.json index 0cd67266..89dc6610 100644 --- a/packages/db/drizzle/meta/_journal.json +++ b/packages/db/drizzle/meta/_journal.json @@ -127,6 +127,13 @@ "when": 1712837113359, "tag": "0017_slippery_senator_kelly", "breakpoints": true + }, + { + "idx": 18, + "version": "5", + "when": 1713183014188, + "tag": "0018_bright_infant_terrible", + "breakpoints": true } ] }
\ No newline at end of file diff --git a/packages/db/schema.ts b/packages/db/schema.ts index 6a541a0a..6a7128c0 100644 --- a/packages/db/schema.ts +++ b/packages/db/schema.ts @@ -104,6 +104,7 @@ export const bookmarks = sqliteTable( .primaryKey() .$defaultFn(() => createId()), createdAt: createdAtField(), + title: text("title"), archived: integer("archived", { mode: "boolean" }).notNull().default(false), favourited: integer("favourited", { mode: "boolean" }) .notNull() diff --git a/packages/shared/search.ts b/packages/shared/search.ts index 4795d34d..555f0cdb 100644 --- a/packages/shared/search.ts +++ b/packages/shared/search.ts @@ -9,6 +9,7 @@ export const zBookmarkIdxSchema = z.object({ userId: z.string(), url: z.string().nullish(), title: z.string().nullish(), + linkTitle: z.string().nullish(), description: z.string().nullish(), content: z.string().nullish(), metadata: z.string().nullish(), diff --git a/packages/trpc/routers/bookmarks.ts b/packages/trpc/routers/bookmarks.ts index 65037f6b..a447235b 100644 --- a/packages/trpc/routers/bookmarks.ts +++ b/packages/trpc/routers/bookmarks.ts @@ -231,6 +231,7 @@ export const bookmarksAppRouter = router({ const res = await ctx.db .update(bookmarks) .set({ + title: input.title, archived: input.archived, favourited: input.favourited, note: input.note, diff --git a/packages/trpc/types/bookmarks.ts b/packages/trpc/types/bookmarks.ts index cf9c2ddf..2cf8152b 100644 --- a/packages/trpc/types/bookmarks.ts +++ b/packages/trpc/types/bookmarks.ts @@ -2,6 +2,8 @@ import { z } from "zod"; import { zBookmarkTagSchema } from "./tags"; +const MAX_TITLE_LENGTH = 100; + export const zBookmarkedLinkSchema = z.object({ type: z.literal("link"), url: z.string().url(), @@ -39,6 +41,7 @@ export type ZBookmarkContent = z.infer<typeof zBookmarkContentSchema>; export const zBareBookmarkSchema = z.object({ id: z.string(), createdAt: z.date(), + title: z.string().max(MAX_TITLE_LENGTH).nullish(), archived: z.boolean(), favourited: z.boolean(), taggingStatus: z.enum(["success", "failure", "pending"]).nullable(), @@ -108,6 +111,7 @@ export const zUpdateBookmarksRequestSchema = z.object({ archived: z.boolean().optional(), favourited: z.boolean().optional(), note: z.string().optional(), + title: z.string().max(MAX_TITLE_LENGTH).nullish(), }); export type ZUpdateBookmarksRequest = z.infer< typeof zUpdateBookmarksRequestSchema diff --git a/tooling/eslint/base.js b/tooling/eslint/base.js index 285993ef..123b25fb 100644 --- a/tooling/eslint/base.js +++ b/tooling/eslint/base.js @@ -19,10 +19,7 @@ const config = { "error", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }, ], - "@typescript-eslint/consistent-type-imports": [ - "warn", - { prefer: "type-imports", fixStyle: "separate-type-imports" }, - ], + "@typescript-eslint/consistent-type-imports": "off", "@typescript-eslint/no-misused-promises": [ 2, { checksVoidReturn: { attributes: false } }, |
