From 30fa06feac15609fe99657e02d171e024e825322 Mon Sep 17 00:00:00 2001 From: sergio <33748103+colado@users.noreply.github.com> Date: Mon, 29 Dec 2025 23:45:36 +0200 Subject: feat(mobile): create new list edit screen (#2310) * feat(mobile): create new edit screen and path * refactor(mobile): use correct import for back navigation * refactor(mobile): remove set state for list type * feat(mobile): handle loading state * feat(mobile): add error handling * feat(mobile): add local validation for empty list name * refactor(mobile): use correct param name in edit path * feat(mobile): handle all pending state cases * refactor(mobile): remove unnecessary return * refactor(mobile): move type validation to top of the file * refactor(mobile): revert validation order * refactor(mobile): clean up submit values * fix(mobile): fix button views --- apps/mobile/app/dashboard/_layout.tsx | 9 ++ apps/mobile/app/dashboard/lists/[slug].tsx | 135 -------------------- apps/mobile/app/dashboard/lists/[slug]/edit.tsx | 152 +++++++++++++++++++++++ apps/mobile/app/dashboard/lists/[slug]/index.tsx | 150 ++++++++++++++++++++++ 4 files changed, 311 insertions(+), 135 deletions(-) delete mode 100644 apps/mobile/app/dashboard/lists/[slug].tsx create mode 100644 apps/mobile/app/dashboard/lists/[slug]/edit.tsx create mode 100644 apps/mobile/app/dashboard/lists/[slug]/index.tsx diff --git a/apps/mobile/app/dashboard/_layout.tsx b/apps/mobile/app/dashboard/_layout.tsx index 260071f0..60fbc4fc 100644 --- a/apps/mobile/app/dashboard/_layout.tsx +++ b/apps/mobile/app/dashboard/_layout.tsx @@ -109,6 +109,15 @@ export default function Dashboard() { presentation: "modal", }} /> + - ( - - ), - }} - /> - {error ? ( - refetch()} /> - ) : list ? ( - - - - ) : ( - - )} - - ); -} - -function ListActionsMenu({ - listId, - role, -}: { - listId: string; - role: ZBookmarkList["userRole"]; -}) { - const { mutate: deleteList } = api.lists.delete.useMutation({ - onSuccess: () => { - router.replace("/dashboard/lists"); - }, - }); - - const { mutate: leaveList } = api.lists.leaveList.useMutation({ - onSuccess: () => { - router.replace("/dashboard/lists"); - }, - }); - - const handleDelete = () => { - Alert.alert("Delete List", "Are you sure you want to delete this list?", [ - { text: "Cancel", style: "cancel" }, - { - text: "Delete", - onPress: () => { - deleteList({ listId }); - }, - style: "destructive", - }, - ]); - }; - - const handleLeave = () => { - Alert.alert("Leave List", "Are you sure you want to leave this list?", [ - { text: "Cancel", style: "cancel" }, - { - text: "Leave", - onPress: () => { - leaveList({ listId }); - }, - style: "destructive", - }, - ]); - }; - - return ( - { - if (nativeEvent.event === "delete") { - handleDelete(); - } - if (nativeEvent.event === "leave") { - handleLeave(); - } - }} - shouldOpenOnLongPress={false} - > - Haptics.selectionAsync()} color="gray" /> - - ); -} diff --git a/apps/mobile/app/dashboard/lists/[slug]/edit.tsx b/apps/mobile/app/dashboard/lists/[slug]/edit.tsx new file mode 100644 index 00000000..6ccc2f26 --- /dev/null +++ b/apps/mobile/app/dashboard/lists/[slug]/edit.tsx @@ -0,0 +1,152 @@ +import { useEffect, useState } from "react"; +import { View } from "react-native"; +import { router, useLocalSearchParams } from "expo-router"; +import { Button } from "@/components/ui/Button"; +import CustomSafeAreaView from "@/components/ui/CustomSafeAreaView"; +import FullPageSpinner from "@/components/ui/FullPageSpinner"; +import { Input } from "@/components/ui/Input"; +import { Text } from "@/components/ui/Text"; +import { useToast } from "@/components/ui/Toast"; +import { api } from "@/lib/trpc"; + +import { useEditBookmarkList } from "@karakeep/shared-react/hooks/lists"; + +const EditListPage = () => { + const { slug: listId } = useLocalSearchParams<{ slug?: string | string[] }>(); + const [text, setText] = useState(""); + const [query, setQuery] = useState(""); + const { toast } = useToast(); + const { mutate, isPending: editIsPending } = useEditBookmarkList({ + onSuccess: () => { + dismiss(); + }, + onError: (error) => { + // Extract error message from the error object + let errorMessage = "Something went wrong"; + if (error.data?.zodError) { + errorMessage = Object.values(error.data.zodError.fieldErrors) + .flat() + .join("\n"); + } else if (error.message) { + errorMessage = error.message; + } + toast({ + message: errorMessage, + variant: "destructive", + }); + }, + }); + + if (typeof listId !== "string") { + throw new Error("Unexpected param type"); + } + + const { data: list, isLoading: fetchIsPending } = api.lists.get.useQuery({ + listId, + }); + + const dismiss = () => { + router.back(); + }; + + useEffect(() => { + if (!list) return; + setText(list.name ?? ""); + setQuery(list.query ?? ""); + }, [list?.id, list?.query, list?.name]); + + const onSubmit = () => { + if (!text.trim()) { + toast({ message: "List name can't be empty", variant: "destructive" }); + return; + } + + if (list?.type === "smart" && !query.trim()) { + toast({ + message: "Smart lists must have a search query", + variant: "destructive", + }); + return; + } + + mutate({ + listId, + name: text.trim(), + query: list?.type === "smart" ? query.trim() : undefined, + }); + }; + + const isPending = fetchIsPending || editIsPending; + + return ( + + {isPending ? ( + + ) : ( + + {/* List Type Info - not editable */} + + List Type + + + + + + + + + + + {/* List Name */} + + {list?.icon || "🚀"} + + + + {/* Smart List Query Input */} + {list?.type === "smart" && ( + + + Search Query + + + + Smart lists automatically show bookmarks matching your search + query + + + )} + + + + )} + + ); +}; + +export default EditListPage; diff --git a/apps/mobile/app/dashboard/lists/[slug]/index.tsx b/apps/mobile/app/dashboard/lists/[slug]/index.tsx new file mode 100644 index 00000000..11379588 --- /dev/null +++ b/apps/mobile/app/dashboard/lists/[slug]/index.tsx @@ -0,0 +1,150 @@ +import { Alert, Platform, View } from "react-native"; +import * as Haptics from "expo-haptics"; +import { router, Stack, useLocalSearchParams } from "expo-router"; +import UpdatingBookmarkList from "@/components/bookmarks/UpdatingBookmarkList"; +import FullPageError from "@/components/FullPageError"; +import CustomSafeAreaView from "@/components/ui/CustomSafeAreaView"; +import FullPageSpinner from "@/components/ui/FullPageSpinner"; +import { api } from "@/lib/trpc"; +import { MenuView } from "@react-native-menu/menu"; +import { Ellipsis } from "lucide-react-native"; + +import { ZBookmarkList } from "@karakeep/shared/types/lists"; + +export default function ListView() { + const { slug } = useLocalSearchParams(); + if (typeof slug !== "string") { + throw new Error("Unexpected param type"); + } + const { + data: list, + error, + refetch, + } = api.lists.get.useQuery({ listId: slug }); + + return ( + + ( + + ), + }} + /> + {error ? ( + refetch()} /> + ) : list ? ( + + + + ) : ( + + )} + + ); +} + +function ListActionsMenu({ + listId, + role, +}: { + listId: string; + role: ZBookmarkList["userRole"]; +}) { + const { mutate: deleteList } = api.lists.delete.useMutation({ + onSuccess: () => { + router.replace("/dashboard/lists"); + }, + }); + + const { mutate: leaveList } = api.lists.leaveList.useMutation({ + onSuccess: () => { + router.replace("/dashboard/lists"); + }, + }); + + const handleDelete = () => { + Alert.alert("Delete List", "Are you sure you want to delete this list?", [ + { text: "Cancel", style: "cancel" }, + { + text: "Delete", + onPress: () => { + deleteList({ listId }); + }, + style: "destructive", + }, + ]); + }; + + const handleLeave = () => { + Alert.alert("Leave List", "Are you sure you want to leave this list?", [ + { text: "Cancel", style: "cancel" }, + { + text: "Leave", + onPress: () => { + leaveList({ listId }); + }, + style: "destructive", + }, + ]); + }; + + const handleEdit = () => { + router.push({ + pathname: "/dashboard/lists/[slug]/edit", + params: { slug: listId }, + }); + }; + + return ( + { + if (nativeEvent.event === "delete") { + handleDelete(); + } else if (nativeEvent.event === "leave") { + handleLeave(); + } else if (nativeEvent.event === "edit") { + handleEdit(); + } + }} + shouldOpenOnLongPress={false} + > + Haptics.selectionAsync()} color="gray" /> + + ); +} -- cgit v1.2.3-70-g09d2