diff options
Diffstat (limited to 'apps/web/components')
6 files changed, 175 insertions, 45 deletions
diff --git a/apps/web/components/dashboard/bookmarks/BookmarkOptions.tsx b/apps/web/components/dashboard/bookmarks/BookmarkOptions.tsx index e9e5834b..c37c6417 100644 --- a/apps/web/components/dashboard/bookmarks/BookmarkOptions.tsx +++ b/apps/web/components/dashboard/bookmarks/BookmarkOptions.tsx @@ -33,6 +33,7 @@ import { } from "@hoarder/shared-react/hooks//bookmarks"; import { useRemoveBookmarkFromList } from "@hoarder/shared-react/hooks//lists"; import { useBookmarkGridContext } from "@hoarder/shared-react/hooks/bookmark-grid-context"; +import { useBookmarkListContext } from "@hoarder/shared-react/hooks/bookmark-list-context"; import { BookmarkTypes } from "@hoarder/shared/types/bookmarks"; import { BookmarkedTextEditor } from "./BookmarkedTextEditor"; @@ -58,6 +59,7 @@ export default function BookmarkOptions({ bookmark }: { bookmark: ZBookmark }) { const [isTextEditorOpen, setTextEditorOpen] = useState(false); const { listId } = useBookmarkGridContext() ?? {}; + const withinListContext = useBookmarkListContext(); const onError = () => { toast({ @@ -210,20 +212,22 @@ export default function BookmarkOptions({ bookmark }: { bookmark: ZBookmark }) { <span>{t("actions.manage_lists")}</span> </DropdownMenuItem> - {listId && ( - <DropdownMenuItem - disabled={demoMode} - onClick={() => - removeFromListMutator.mutate({ - listId, - bookmarkId: bookmark.id, - }) - } - > - <ListX className="mr-2 size-4" /> - <span>{t("actions.remove_from_list")}</span> - </DropdownMenuItem> - )} + {listId && + withinListContext && + withinListContext.type === "manual" && ( + <DropdownMenuItem + disabled={demoMode} + onClick={() => + removeFromListMutator.mutate({ + listId, + bookmarkId: bookmark.id, + }) + } + > + <ListX className="mr-2 size-4" /> + <span>{t("actions.remove_from_list")}</span> + </DropdownMenuItem> + )} {bookmark.content.type === BookmarkTypes.LINK && ( <DropdownMenuItem diff --git a/apps/web/components/dashboard/lists/EditListModal.tsx b/apps/web/components/dashboard/lists/EditListModal.tsx index d66d7096..98bb19be 100644 --- a/apps/web/components/dashboard/lists/EditListModal.tsx +++ b/apps/web/components/dashboard/lists/EditListModal.tsx @@ -25,6 +25,13 @@ import { PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; import { toast } from "@/components/ui/use-toast"; import { useTranslation } from "@/lib/i18n/client"; import data from "@emoji-mart/data"; @@ -38,7 +45,10 @@ import { useCreateBookmarkList, useEditBookmarkList, } from "@hoarder/shared-react/hooks/lists"; -import { ZBookmarkList } from "@hoarder/shared/types/lists"; +import { + ZBookmarkList, + zNewBookmarkListSchema, +} from "@hoarder/shared/types/lists"; import { BookmarkListSelector } from "./BookmarkListSelector"; @@ -46,13 +56,13 @@ export function EditListModal({ open: userOpen, setOpen: userSetOpen, list, - parent, + prefill, children, }: { open?: boolean; setOpen?: (v: boolean) => void; list?: ZBookmarkList; - parent?: ZBookmarkList; + prefill?: Partial<Omit<ZBookmarkList, "id">>; children?: React.ReactNode; }) { const { t } = useTranslation(); @@ -64,17 +74,14 @@ export function EditListModal({ throw new Error("You must provide both open and setOpen or neither"); } const [customOpen, customSetOpen] = useState(false); - const formSchema = z.object({ - name: z.string(), - icon: z.string(), - parentId: z.string().nullish(), - }); - const form = useForm<z.infer<typeof formSchema>>({ - resolver: zodResolver(formSchema), + const form = useForm<z.infer<typeof zNewBookmarkListSchema>>({ + resolver: zodResolver(zNewBookmarkListSchema), defaultValues: { - name: list?.name ?? "", - icon: list?.icon ?? "🚀", - parentId: list?.parentId ?? parent?.id, + name: list?.name ?? prefill?.name ?? "", + icon: list?.icon ?? prefill?.icon ?? "🚀", + parentId: list?.parentId ?? prefill?.parentId, + type: list?.type ?? prefill?.type ?? "manual", + query: list?.query ?? prefill?.query ?? undefined, }, }); const [open, setOpen] = [ @@ -84,9 +91,11 @@ export function EditListModal({ useEffect(() => { form.reset({ - name: list?.name ?? "", - icon: list?.icon ?? "🚀", - parentId: list?.parentId ?? parent?.id, + name: list?.name ?? prefill?.name ?? "", + icon: list?.icon ?? prefill?.icon ?? "🚀", + parentId: list?.parentId ?? prefill?.parentId, + type: list?.type ?? prefill?.type ?? "manual", + query: list?.query ?? prefill?.query ?? undefined, }); }, [open]); @@ -154,14 +163,24 @@ export function EditListModal({ } }, }); + const listType = form.watch("type"); + + useEffect(() => { + if (listType !== "smart") { + form.resetField("query"); + } + }, [listType]); const isEdit = !!list; const isPending = isCreating || isEditing; - const onSubmit = form.handleSubmit((value: z.infer<typeof formSchema>) => { - value.parentId = value.parentId === "" ? null : value.parentId; - isEdit ? editList({ ...value, listId: list.id }) : createList(value); - }); + const onSubmit = form.handleSubmit( + (value: z.infer<typeof zNewBookmarkListSchema>) => { + value.parentId = value.parentId === "" ? null : value.parentId; + value.query = value.type === "smart" ? value.query : undefined; + isEdit ? editList({ ...value, listId: list.id }) : createList(value); + }, + ); return ( <Dialog @@ -176,7 +195,9 @@ export function EditListModal({ <Form {...form}> <form onSubmit={onSubmit}> <DialogHeader> - <DialogTitle>{isEdit ? "Edit" : "New"} List</DialogTitle> + <DialogTitle> + {isEdit ? t("lists.edit_list") : t("lists.new_list")} + </DialogTitle> </DialogHeader> <div className="flex w-full gap-2 py-4"> <FormField @@ -232,7 +253,7 @@ export function EditListModal({ render={({ field }) => { return ( <FormItem className="grow pb-4"> - <FormLabel>Parent</FormLabel> + <FormLabel>{t("lists.parent_list")}</FormLabel> <div className="flex items-center gap-1"> <FormControl> <BookmarkListSelector @@ -240,7 +261,7 @@ export function EditListModal({ hideSubtreeOf={list ? list.id : undefined} value={field.value} onChange={field.onChange} - placeholder={"No Parent"} + placeholder={t("lists.no_parent")} /> </FormControl> <Button @@ -258,6 +279,58 @@ export function EditListModal({ ); }} /> + <FormField + control={form.control} + name="type" + render={({ field }) => { + return ( + <FormItem className="grow pb-4"> + <FormLabel>{t("lists.list_type")}</FormLabel> + <FormControl> + <Select + disabled={isEdit} + onValueChange={field.onChange} + value={field.value} + > + <SelectTrigger className="w-full"> + <SelectValue /> + </SelectTrigger> + <SelectContent> + <SelectItem value="manual"> + {t("lists.manual_list")} + </SelectItem> + <SelectItem value="smart"> + {t("lists.smart_list")} + </SelectItem> + </SelectContent> + </Select> + </FormControl> + <FormMessage /> + </FormItem> + ); + }} + /> + {listType === "smart" && ( + <FormField + control={form.control} + name="query" + render={({ field }) => { + return ( + <FormItem className="grow pb-4"> + <FormLabel>{t("lists.search_query")}</FormLabel> + <FormControl> + <Input + value={field.value} + onChange={field.onChange} + placeholder={t("lists.search_query")} + /> + </FormControl> + <FormMessage /> + </FormItem> + ); + }} + /> + )} <DialogFooter className="sm:justify-end"> <DialogClose asChild> <Button type="button" variant="secondary"> diff --git a/apps/web/components/dashboard/lists/ListHeader.tsx b/apps/web/components/dashboard/lists/ListHeader.tsx index a6780e1e..b8bfb4ad 100644 --- a/apps/web/components/dashboard/lists/ListHeader.tsx +++ b/apps/web/components/dashboard/lists/ListHeader.tsx @@ -1,19 +1,24 @@ "use client"; +import { useMemo } from "react"; import { useRouter } from "next/navigation"; import { Button } from "@/components/ui/button"; -import { MoreHorizontal } from "lucide-react"; +import { useTranslation } from "@/lib/i18n/client"; +import { MoreHorizontal, SearchIcon } from "lucide-react"; import { api } from "@hoarder/shared-react/trpc"; +import { parseSearchQuery } from "@hoarder/shared/searchQueryParser"; import { ZBookmarkList } from "@hoarder/shared/types/lists"; +import QueryExplainerTooltip from "../search/QueryExplainerTooltip"; import { ListOptions } from "./ListOptions"; export default function ListHeader({ initialData, }: { - initialData: ZBookmarkList & { bookmarks: string[] }; + initialData: ZBookmarkList; }) { + const { t } = useTranslation(); const router = useRouter(); const { data: list, error } = api.lists.get.useQuery( { @@ -24,6 +29,13 @@ export default function ListHeader({ }, ); + const parsedQuery = useMemo(() => { + if (!list.query) { + return null; + } + return parseSearchQuery(list.query); + }, [list.query]); + if (error) { // This is usually exercised during list deletions. if (error.data?.code == "NOT_FOUND") { @@ -33,10 +45,24 @@ export default function ListHeader({ return ( <div className="flex items-center justify-between"> - <span className="text-2xl"> - {list.icon} {list.name} - </span> - <div className="flex"> + <div className="flex items-center gap-2"> + <span className="text-2xl"> + {list.icon} {list.name} + </span> + </div> + <div className="flex items-center"> + {parsedQuery && ( + <QueryExplainerTooltip + header={ + <div className="flex items-center justify-center gap-1"> + <SearchIcon className="size-3" /> + <span className="text-sm">{t("lists.smart_list")}</span> + </div> + } + parsedSearchQuery={parsedQuery} + className="size-6 stroke-foreground" + /> + )} <ListOptions list={list}> <Button variant="ghost"> <MoreHorizontal /> diff --git a/apps/web/components/dashboard/lists/ListOptions.tsx b/apps/web/components/dashboard/lists/ListOptions.tsx index e663a2e0..a7217954 100644 --- a/apps/web/components/dashboard/lists/ListOptions.tsx +++ b/apps/web/components/dashboard/lists/ListOptions.tsx @@ -33,7 +33,9 @@ export function ListOptions({ <EditListModal open={newNestedListModalOpen} setOpen={setNewNestedListModalOpen} - parent={list} + prefill={{ + parentId: list.id, + }} /> <EditListModal open={editModalOpen} diff --git a/apps/web/components/dashboard/search/QueryExplainerTooltip.tsx b/apps/web/components/dashboard/search/QueryExplainerTooltip.tsx index eb7282d0..13174fb2 100644 --- a/apps/web/components/dashboard/search/QueryExplainerTooltip.tsx +++ b/apps/web/components/dashboard/search/QueryExplainerTooltip.tsx @@ -6,8 +6,10 @@ import { Matcher } from "@hoarder/shared/types/search"; export default function QueryExplainerTooltip({ parsedSearchQuery, + header, className, }: { + header?: React.ReactNode; parsedSearchQuery: TextAndMatcher & { result: string }; className?: string; }) { @@ -98,6 +100,7 @@ export default function QueryExplainerTooltip({ return ( <InfoTooltip className={className}> + {header} <Table> <TableBody> {parsedSearchQuery.text && ( diff --git a/apps/web/components/dashboard/search/SearchInput.tsx b/apps/web/components/dashboard/search/SearchInput.tsx index 8ed2ea3c..ace3d785 100644 --- a/apps/web/components/dashboard/search/SearchInput.tsx +++ b/apps/web/components/dashboard/search/SearchInput.tsx @@ -1,11 +1,13 @@ "use client"; -import React, { useEffect, useImperativeHandle, useRef } from "react"; +import React, { useEffect, useImperativeHandle, useRef, useState } from "react"; +import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { useDoBookmarkSearch } from "@/lib/hooks/bookmark-search"; import { useTranslation } from "@/lib/i18n/client"; import { cn } from "@/lib/utils"; +import { EditListModal } from "../lists/EditListModal"; import QueryExplainerTooltip from "./QueryExplainerTooltip"; function useFocusSearchOnKeyPress( @@ -63,6 +65,7 @@ const SearchInput = React.forwardRef< useFocusSearchOnKeyPress(inputRef, onChange); useImperativeHandle(ref, () => inputRef.current!); + const [newNestedListModalOpen, setNewNestedListModalOpen] = useState(false); useEffect(() => { if (!isInSearchPage) { @@ -72,10 +75,29 @@ const SearchInput = React.forwardRef< return ( <div className={cn("relative flex-1", className)}> + <EditListModal + open={newNestedListModalOpen} + setOpen={setNewNestedListModalOpen} + prefill={{ + type: "smart", + query: value, + }} + /> <QueryExplainerTooltip className="-translate-1/2 absolute right-1.5 top-2 p-0.5" parsedSearchQuery={parsedSearchQuery} /> + {parsedSearchQuery.result === "full" && + parsedSearchQuery.text.length == 0 && ( + <Button + onClick={() => setNewNestedListModalOpen(true)} + size="none" + variant="secondary" + className="absolute right-9 top-2 z-50 px-2 py-1 text-xs" + > + {t("actions.save")} + </Button> + )} <Input ref={inputRef} value={value} |
