From 5ecdc36b7d60aa66b49e01e9fec8ba61ad537376 Mon Sep 17 00:00:00 2001
From: Mohamed Bassem <me@mbassem.com>
Date: Thu, 2 Jan 2025 13:00:58 +0200
Subject: feat: Add support for smart lists (#802)

* feat: Add support for smart lists

* i18n

* Fix update list endpoint

* Add a test for smart lists

* Add header to the query explainer

* Hide remove from lists in the smart context list

* Add proper validation to list form

---------

Co-authored-by: Deepak Kapoor <41769111+orthdron@users.noreply.github.com>
---
 .../components/dashboard/lists/EditListModal.tsx   | 119 +++++++++++++++++----
 1 file changed, 96 insertions(+), 23 deletions(-)

(limited to 'apps/web/components/dashboard/lists/EditListModal.tsx')

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">
-- 
cgit v1.2.3-70-g09d2

