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