feat: enhance deleteLink functionality with optimistic rendering and improved error handling

This commit is contained in:
daniel31x13
2026-02-19 17:30:01 -05:00
parent c99f9edd9a
commit c9fd573b31
6 changed files with 131 additions and 55 deletions

View File

@@ -121,7 +121,7 @@ const RootComponent = ({
}) => {
const { colorScheme } = useColorScheme();
const updateLink = useUpdateLink(auth);
const deleteLink = useDeleteLink(auth);
const deleteLink = useDeleteLink({ auth, Alert });
const { tmp } = useTmpStore();
@@ -282,18 +282,15 @@ const RootComponent = ({
{
text: "Delete",
style: "destructive",
onPress: () => {
onPress: async () => {
deleteLink.mutate(
tmp.link?.id as number,
{
onSuccess: async () => {
await deleteLinkCache(
tmp.link?.id as number
);
},
}
tmp.link?.id as number
);
// go back
await deleteLinkCache(
tmp.link?.id as number
);
router.back();
},
},

View File

@@ -45,7 +45,7 @@ const LinkListing = ({ link, dashboard }: Props) => {
const { colorScheme } = useColorScheme();
const { data } = useDataStore();
const deleteLink = useDeleteLink(auth);
const deleteLink = useDeleteLink({ auth, Alert });
const [url, setUrl] = useState("");
@@ -319,12 +319,10 @@ const LinkListing = ({ link, dashboard }: Props) => {
{
text: "Delete",
style: "destructive",
onPress: () => {
deleteLink.mutate(link.id as number, {
onSuccess: async () => {
await deleteLinkCache(link.id as number);
},
});
onPress: async () => {
deleteLink.mutate(link.id as number);
await deleteLinkCache(link.id as number);
},
},
]

View File

@@ -54,7 +54,7 @@ export default function LinkActions({
const [refreshPreservationsModal, setRefreshPreservationsModal] =
useState(false);
const deleteLink = useDeleteLink();
const deleteLink = useDeleteLink({ toast, t });
const updateArchive = async () => {
const load = toast.loading(t("sending_request"));
@@ -131,13 +131,7 @@ export default function LinkActions({
onClick={async (e) => {
if (e.shiftKey) {
const load = toast.loading(t("deleting"));
await deleteLink.mutateAsync(link.id as number, {
onSettled: (data, error) => {
toast.dismiss(load);
if (error) toast.error(error.message);
else toast.success(t("deleted"));
},
});
await deleteLink.mutateAsync(link.id as number);
} else {
setDeleteLinkModal(true);
}

View File

@@ -18,7 +18,7 @@ export default function DeleteLinkModal({ onClose, activeLink }: Props) {
const [link, setLink] =
useState<LinkIncludingShortenedCollectionAndTags>(activeLink);
const deleteLink = useDeleteLink();
const deleteLink = useDeleteLink({ toast, t });
const router = useRouter();
useEffect(() => {
@@ -26,26 +26,15 @@ export default function DeleteLinkModal({ onClose, activeLink }: Props) {
}, []);
const submit = async () => {
const load = toast.loading(t("deleting"));
deleteLink.mutateAsync(link.id as number);
await deleteLink.mutateAsync(link.id as number, {
onSettled: (data, error) => {
toast.dismiss(load);
if (error) {
toast.error(error.message);
} else {
if (
router.pathname.startsWith("/links/[id]") ||
router.pathname.startsWith("/preserved/[id]")
) {
router.push("/dashboard");
}
toast.success(t("deleted"));
onClose();
}
},
});
if (
router.pathname.startsWith("/links/[id]") ||
router.pathname.startsWith("/preserved/[id]")
) {
router.push("/dashboard");
}
onClose();
};
return (

View File

@@ -43,7 +43,7 @@ export default function LinkModal({
const isPublicRoute = router.pathname.startsWith("/public") ? true : false;
const deleteLink = useDeleteLink();
const deleteLink = useDeleteLink({ toast, t });
const [mode, setMode] = useState<"view" | "edit">(activeMode || "view");
@@ -51,13 +51,8 @@ export default function LinkModal({
setTimeout(() => (document.body.style.pointerEvents = ""), 0);
if (e.shiftKey && link.id) {
const loading = toast.loading(t("deleting"));
await deleteLink.mutateAsync(link.id, {
onSettled: (data, error) => {
toast.dismiss(loading);
error ? toast.error(error.message) : toast.success(t("deleted"));
},
});
deleteLink.mutateAsync(link.id);
onClose();
} else {
onDelete();

View File

@@ -193,6 +193,48 @@ const upsertLinkInDashboardData = (
};
};
const removeLinkFromInfiniteData = (oldData: any, linkId: number) => {
if (!oldData?.pages?.length) return oldData;
return {
...oldData,
pages: oldData.pages.map((page: any) => ({
...page,
links: (page.links ?? []).filter((item: any) => item.id !== linkId),
})),
};
};
const removeLinkFromDashboardData = (oldData: any, linkId: number) => {
if (!oldData) return oldData;
const removedLink = (oldData.links ?? []).find(
(item: any) => item.id === linkId
);
const numberOfPinnedLinks = removedLink?.pinnedBy?.length
? Math.max(0, (oldData.numberOfPinnedLinks ?? 0) - 1)
: oldData.numberOfPinnedLinks;
const hasCollectionLinks = oldData.collectionLinks != null;
const collectionLinks = hasCollectionLinks
? { ...oldData.collectionLinks }
: oldData.collectionLinks;
if (hasCollectionLinks) {
for (const [collectionId, links] of Object.entries(collectionLinks)) {
collectionLinks[Number(collectionId)] = (links as any[]).filter(
(item: any) => item.id !== linkId
);
}
}
return {
...oldData,
links: (oldData.links ?? []).filter((item: any) => item.id !== linkId),
collectionLinks,
numberOfPinnedLinks,
};
};
const useAddLink = ({
auth,
Alert,
@@ -393,7 +435,17 @@ const useUpdateLink = (auth?: MobileAuth) => {
});
};
const useDeleteLink = (auth?: MobileAuth) => {
const useDeleteLink = ({
auth,
Alert,
toast,
t,
}: {
auth?: MobileAuth;
Alert?: typeof Alert_;
toast?: typeof toaster;
t?: TFunction;
}) => {
const queryClient = useQueryClient();
return useMutation({
@@ -416,6 +468,57 @@ const useDeleteLink = (auth?: MobileAuth) => {
return data.response;
},
onMutate: async (id) => {
await queryClient.cancelQueries({ queryKey: ["links"] });
await queryClient.cancelQueries({ queryKey: ["dashboardData"] });
await queryClient.cancelQueries({ queryKey: ["publicLinks"] });
const previousLinks = queryClient.getQueriesData({
queryKey: ["links"],
});
const previousPublicLinks = queryClient.getQueriesData({
queryKey: ["publicLinks"],
});
const previousDashboard = queryClient.getQueryData(["dashboardData"]);
queryClient.setQueriesData({ queryKey: ["links"] }, (oldData: any) =>
removeLinkFromInfiniteData(oldData, id)
);
queryClient.setQueriesData(
{ queryKey: ["publicLinks"] },
(oldData: any) => removeLinkFromInfiniteData(oldData, id)
);
queryClient.setQueryData(["dashboardData"], (oldData: any) =>
removeLinkFromDashboardData(oldData, id)
);
return {
previousLinks,
previousPublicLinks,
previousDashboard,
};
},
onError: (error, _variables, context) => {
if (toast && t) toast.error(t(error.message));
else if (Alert)
Alert.alert("Error", "There was an error adding the link.");
if (!context) return;
context.previousLinks?.forEach(([queryKey, data]: [unknown, unknown]) => {
queryClient.setQueryData(queryKey as QueryKey, data);
});
context.previousPublicLinks?.forEach(
([queryKey, data]: [unknown, unknown]) => {
queryClient.setQueryData(queryKey as QueryKey, data);
}
);
queryClient.setQueryData(["dashboardData"], context.previousDashboard);
},
onSuccess: (data) => {
queryClient.setQueriesData({ queryKey: ["links"] }, (oldData: any) => {
if (!oldData?.pages?.[0]) return undefined;