diff options
| author | MohamedBassem <me@mbassem.com> | 2025-04-07 01:03:26 +0100 |
|---|---|---|
| committer | MohamedBassem <me@mbassem.com> | 2025-04-08 03:48:12 -0700 |
| commit | 3207264fc13c275d6dcfbd2628cc6b3974ceeaed (patch) | |
| tree | d426ffe0fe6bc3b9e692d96af94aa8d5d2a51162 /apps/web/components/dashboard/bookmarks/EditBookmarkDialog.tsx | |
| parent | 817eb58832a3e715e21892417b7624f4b1cf0d46 (diff) | |
| download | karakeep-3207264fc13c275d6dcfbd2628cc6b3974ceeaed.tar.zst | |
feat: Allow editing bookmark details
Diffstat (limited to 'apps/web/components/dashboard/bookmarks/EditBookmarkDialog.tsx')
| -rw-r--r-- | apps/web/components/dashboard/bookmarks/EditBookmarkDialog.tsx | 371 |
1 files changed, 371 insertions, 0 deletions
diff --git a/apps/web/components/dashboard/bookmarks/EditBookmarkDialog.tsx b/apps/web/components/dashboard/bookmarks/EditBookmarkDialog.tsx new file mode 100644 index 00000000..2d47102b --- /dev/null +++ b/apps/web/components/dashboard/bookmarks/EditBookmarkDialog.tsx @@ -0,0 +1,371 @@ +import * as React from "react"; +import { ActionButton } from "@/components/ui/action-button"; +import { Button } from "@/components/ui/button"; +import { Calendar } from "@/components/ui/calendar"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "@/components/ui/popover"; +import { Textarea } from "@/components/ui/textarea"; +import { toast } from "@/components/ui/use-toast"; +import { useTranslation } from "@/lib/i18n/client"; +import { cn } from "@/lib/utils"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { format } from "date-fns"; +import { CalendarIcon } from "lucide-react"; +import { useForm } from "react-hook-form"; + +import { useUpdateBookmark } from "@hoarder/shared-react/hooks/bookmarks"; +import { + BookmarkTypes, + ZBookmark, + ZUpdateBookmarksRequest, + zUpdateBookmarksRequestSchema, +} from "@hoarder/shared/types/bookmarks"; + +import { BookmarkTagsEditor } from "./BookmarkTagsEditor"; + +const formSchema = zUpdateBookmarksRequestSchema; + +export function EditBookmarkDialog({ + open, + setOpen, + bookmark, + children, +}: { + bookmark: ZBookmark; + children?: React.ReactNode; + open: boolean; + setOpen: (v: boolean) => void; +}) { + const { t } = useTranslation(); + const bookmarkToDefault = (bookmark: ZBookmark) => ({ + bookmarkId: bookmark.id, + summary: bookmark.summary, + title: bookmark.title + ? bookmark.title + : bookmark.content.type === BookmarkTypes.LINK + ? bookmark.content.title + : undefined, + createdAt: bookmark.createdAt ?? new Date(), + // Link specific defaults (only if bookmark is a link) + url: + bookmark.content.type === BookmarkTypes.LINK + ? bookmark.content.url + : undefined, + description: + bookmark.content.type === BookmarkTypes.LINK + ? (bookmark.content.description ?? "") + : undefined, + author: + bookmark.content.type === BookmarkTypes.LINK + ? (bookmark.content.author ?? "") + : undefined, + publisher: + bookmark.content.type === BookmarkTypes.LINK + ? (bookmark.content.publisher ?? "") + : undefined, + datePublished: + bookmark.content.type === BookmarkTypes.LINK + ? bookmark.content.datePublished + : undefined, + }); + + const form = useForm<ZUpdateBookmarksRequest>({ + resolver: zodResolver(formSchema), + defaultValues: bookmarkToDefault(bookmark), + }); + + const { mutate: updateBookmarkMutate, isPending: isUpdatingBookmark } = + useUpdateBookmark({ + onSuccess: (updatedBookmark) => { + toast({ description: "Bookmark details updated successfully!" }); + // Close the dialog after successful detail update + setOpen(false); + // Reset form with potentially updated data + form.reset(bookmarkToDefault(updatedBookmark)); + }, + onError: (error) => { + toast({ + variant: "destructive", + title: "Failed to update bookmark", + description: error.message, + }); + }, + }); + + function onSubmit(values: ZUpdateBookmarksRequest) { + // Ensure optional fields that are empty strings are sent as null/undefined if appropriate + const payload = { + ...values, + title: values.title ?? null, + }; + updateBookmarkMutate(payload); + } + + // Reset form when bookmark data changes externally or dialog reopens + React.useEffect(() => { + if (open) { + form.reset(bookmarkToDefault(bookmark)); + } + }, [bookmark, form, open]); + + const isLink = bookmark.content.type === BookmarkTypes.LINK; + + return ( + <Dialog open={open} onOpenChange={setOpen}> + {children && <DialogTrigger asChild>{children}</DialogTrigger>} + <DialogContent className="max-h-[90vh] overflow-y-auto sm:max-w-xl"> + <DialogHeader> + <DialogTitle>{t("bookmark_editor.title")}</DialogTitle> + <DialogDescription>{t("bookmark_editor.subtitle")}</DialogDescription> + </DialogHeader> + <Form {...form}> + <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4"> + <FormField + control={form.control} + name="title" + render={({ field }) => ( + <FormItem> + <FormLabel>{t("common.title")}</FormLabel> + <FormControl> + <Input + placeholder="Bookmark title" + {...field} + value={field.value ?? ""} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + + {isLink && ( + <FormField + control={form.control} + name="url" + render={({ field }) => ( + <FormItem> + <FormLabel>{t("common.url")}</FormLabel> + <FormControl> + <Input placeholder="https://example.com" {...field} /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + )} + + {isLink && ( + <FormField + control={form.control} + name="description" + render={({ field }) => ( + <FormItem> + <FormLabel>{t("common.description")}</FormLabel> + <FormControl> + <Textarea + placeholder="Bookmark description" + {...field} + value={field.value ?? ""} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + )} + + {isLink && ( + <FormField + control={form.control} + name="summary" + render={({ field }) => ( + <FormItem> + <FormLabel>{t("common.summary")}</FormLabel> + <FormControl> + <Textarea + placeholder="Bookmark summary" + {...field} + value={field.value ?? ""} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + )} + + {isLink && ( + <div className="grid grid-cols-1 gap-4 md:grid-cols-2"> + <FormField + control={form.control} + name="author" + render={({ field }) => ( + <FormItem> + <FormLabel>{t("bookmark_editor.author")}</FormLabel> + <FormControl> + <Input + placeholder="Author name" + {...field} + value={field.value ?? ""} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + <FormField + control={form.control} + name="publisher" + render={({ field }) => ( + <FormItem> + <FormLabel>{t("bookmark_editor.publisher")}</FormLabel> + <FormControl> + <Input + placeholder="Publisher name" + {...field} + value={field.value ?? ""} + /> + </FormControl> + <FormMessage /> + </FormItem> + )} + /> + </div> + )} + + <div className="grid grid-cols-1 gap-4 md:grid-cols-2"> + <FormField + control={form.control} + name="createdAt" + render={({ field }) => ( + <FormItem className="flex flex-col"> + <FormLabel>{t("common.created_at")}</FormLabel> + <Popover> + <PopoverTrigger asChild> + <FormControl> + <Button + variant={"outline"} + className={cn( + "pl-3 text-left font-normal", + !field.value && "text-muted-foreground", + )} + > + {field.value ? ( + format(field.value, "PPP") + ) : ( + <span>{t("bookmark_editor.pick_a_date")}</span> + )} + <CalendarIcon className="ml-auto h-4 w-4 opacity-50" /> + </Button> + </FormControl> + </PopoverTrigger> + <PopoverContent className="w-auto p-0" align="start"> + <Calendar + mode="single" + selected={field.value} + onSelect={field.onChange} + disabled={(date) => + date > new Date() || date < new Date("1900-01-01") + } + /> + </PopoverContent> + </Popover> + <FormMessage /> + </FormItem> + )} + /> + + {isLink && ( + <FormField + control={form.control} + name="datePublished" + render={({ field }) => ( + <FormItem className="flex flex-col"> + <FormLabel> + {t("bookmark_editor.date_published")} + </FormLabel> + <Popover> + <PopoverTrigger asChild> + <FormControl> + <Button + variant={"outline"} + className={cn( + "pl-3 text-left font-normal", + !field.value && "text-muted-foreground", + )} + > + {field.value ? ( + format(field.value, "PPP") + ) : ( + <span>{t("bookmark_editor.pick_a_date")}</span> + )} + <CalendarIcon className="ml-auto h-4 w-4 opacity-50" /> + </Button> + </FormControl> + </PopoverTrigger> + <PopoverContent className="w-auto p-0" align="start"> + <Calendar + mode="single" + selected={field.value ?? undefined} // Calendar expects Date | undefined + onSelect={(date) => field.onChange(date ?? null)} // Handle undefined -> null + disabled={(date) => + date > new Date() || date < new Date("1900-01-01") + } + /> + </PopoverContent> + </Popover> + <FormMessage /> + </FormItem> + )} + /> + )} + </div> + + <FormItem> + <FormLabel>{t("common.tags")}</FormLabel> + <FormControl> + <BookmarkTagsEditor bookmark={bookmark} /> + </FormControl> + <FormMessage /> + </FormItem> + + <DialogFooter> + <Button + type="button" + variant="outline" + onClick={() => setOpen(false)} + disabled={isUpdatingBookmark} + > + {t("actions.cancel")} + </Button> + <ActionButton type="submit" loading={isUpdatingBookmark}> + {t("bookmark_editor.save_changes")} + </ActionButton> + </DialogFooter> + </form> + </Form> + </DialogContent> + </Dialog> + ); +} |
