mirror of
https://github.com/linkwarden/linkwarden.git
synced 2026-04-18 12:09:44 +00:00
283 lines
8.0 KiB
TypeScript
283 lines
8.0 KiB
TypeScript
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
import {
|
|
CollectionIncludingMembersAndLinkCount,
|
|
MobileAuth,
|
|
} from "@linkwarden/types/global";
|
|
import { useSession } from "next-auth/react";
|
|
import type toaster from "react-hot-toast";
|
|
import { TFunction } from "next-i18next";
|
|
|
|
const getCollectionSubtreeIds = (collections: any[] = [], rootId: number) => {
|
|
const byParent = new Map<number, number[]>();
|
|
|
|
for (const collection of collections) {
|
|
if (collection?.id == null || collection?.parentId == null) continue;
|
|
const siblings = byParent.get(collection.parentId) ?? [];
|
|
siblings.push(collection.id);
|
|
byParent.set(collection.parentId, siblings);
|
|
}
|
|
|
|
const visited = new Set<number>([rootId]);
|
|
const stack = [rootId];
|
|
|
|
while (stack.length) {
|
|
const currentId = stack.pop() as number;
|
|
const children = byParent.get(currentId) ?? [];
|
|
|
|
for (const childId of children) {
|
|
if (visited.has(childId)) continue;
|
|
visited.add(childId);
|
|
stack.push(childId);
|
|
}
|
|
}
|
|
|
|
return Array.from(visited);
|
|
};
|
|
|
|
const removeCollectionsFromDashboard = (
|
|
oldData: any,
|
|
collectionIds: number[]
|
|
) => {
|
|
if (!oldData) return oldData;
|
|
|
|
const deletedCollectionIds = new Set(collectionIds);
|
|
const nextLinks =
|
|
oldData.links?.filter(
|
|
(link: any) => !deletedCollectionIds.has(link.collectionId)
|
|
) || [];
|
|
|
|
const nextCollectionLinks = oldData.collectionLinks
|
|
? Object.fromEntries(
|
|
Object.entries(oldData.collectionLinks)
|
|
.filter(
|
|
([collectionId]) => !deletedCollectionIds.has(Number(collectionId))
|
|
)
|
|
.map(([collectionId, links]) => [
|
|
collectionId,
|
|
(links as any[]).filter(
|
|
(link) => !deletedCollectionIds.has(link.collectionId)
|
|
),
|
|
])
|
|
)
|
|
: oldData.collectionLinks;
|
|
|
|
return {
|
|
...oldData,
|
|
links: nextLinks,
|
|
collectionLinks: nextCollectionLinks,
|
|
numberOfPinnedLinks:
|
|
nextLinks.filter(
|
|
(link: any) =>
|
|
link.isPinned || (link.pinnedBy && link.pinnedBy.length > 0)
|
|
).length || 0,
|
|
};
|
|
};
|
|
|
|
const useCollections = (auth?: MobileAuth) => {
|
|
let status: "loading" | "authenticated" | "unauthenticated";
|
|
|
|
if (!auth) {
|
|
const session = useSession();
|
|
status = session.status;
|
|
} else {
|
|
status = auth?.status;
|
|
}
|
|
|
|
return useQuery({
|
|
queryKey: ["collections"],
|
|
queryFn: async (): Promise<CollectionIncludingMembersAndLinkCount[]> => {
|
|
const response = await fetch(
|
|
(auth?.instance ? auth?.instance : "") + "/api/v1/collections",
|
|
auth?.session
|
|
? {
|
|
headers: {
|
|
Authorization: `Bearer ${auth.session}`,
|
|
},
|
|
}
|
|
: undefined
|
|
);
|
|
const data = await response.json();
|
|
|
|
if (Array.isArray(data.response)) return data.response;
|
|
else return [];
|
|
},
|
|
enabled: status === "authenticated",
|
|
});
|
|
};
|
|
|
|
const useCreateCollection = (auth?: MobileAuth) => {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: async (body: any) => {
|
|
const response = await fetch(
|
|
(auth?.instance ? auth?.instance : "") + "/api/v1/collections",
|
|
{
|
|
body: JSON.stringify(body),
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
...(auth?.session
|
|
? { Authorization: `Bearer ${auth.session}` }
|
|
: {}),
|
|
},
|
|
method: "POST",
|
|
}
|
|
);
|
|
|
|
const data = await response.json();
|
|
|
|
if (!response.ok) throw new Error(data.response);
|
|
|
|
return data.response;
|
|
},
|
|
onSuccess: (data) => {
|
|
return queryClient.setQueryData(["collections"], (oldData: any) => {
|
|
return [...oldData, data];
|
|
});
|
|
},
|
|
});
|
|
};
|
|
|
|
const useUpdateCollection = () => {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: async (body: any) => {
|
|
const response = await fetch(`/api/v1/collections/${body.id}`, {
|
|
method: "PUT",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify(body),
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (!response.ok) throw new Error(data.response);
|
|
|
|
return data.response;
|
|
},
|
|
onSuccess: (data) => {
|
|
{
|
|
return queryClient.setQueryData(["collections"], (oldData: any) => {
|
|
return oldData.map((collection: any) =>
|
|
collection.id === data.id ? data : collection
|
|
);
|
|
});
|
|
}
|
|
},
|
|
// onMutate: async (data) => {
|
|
// await queryClient.cancelQueries({ queryKey: ["collections"] });
|
|
// queryClient.setQueryData(["collections"], (oldData: any) => {
|
|
// return oldData.map((collection: any) =>
|
|
// collection.id === data.id ? data : collection
|
|
// )
|
|
// });
|
|
// },
|
|
});
|
|
};
|
|
|
|
const useDeleteCollection = ({
|
|
auth,
|
|
Alert,
|
|
toast,
|
|
t,
|
|
}: {
|
|
auth?: MobileAuth;
|
|
Alert?: any;
|
|
toast?: typeof toaster;
|
|
t?: TFunction;
|
|
}) => {
|
|
const queryClient = useQueryClient();
|
|
|
|
return useMutation({
|
|
mutationFn: async (id: number) => {
|
|
const response = await fetch(
|
|
(auth?.instance ? auth?.instance : "") + `/api/v1/collections/${id}`,
|
|
{
|
|
method: "DELETE",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
...(auth?.session
|
|
? { Authorization: `Bearer ${auth.session}` }
|
|
: {}),
|
|
},
|
|
}
|
|
);
|
|
|
|
const data = await response.json();
|
|
|
|
if (!response.ok) throw new Error(data.response);
|
|
|
|
return data.response;
|
|
},
|
|
onMutate: async (id) => {
|
|
await queryClient.cancelQueries({ queryKey: ["collections"] });
|
|
await queryClient.cancelQueries({ queryKey: ["dashboardData"] });
|
|
|
|
const previousCollections =
|
|
(queryClient.getQueryData(["collections"]) as any[]) ?? [];
|
|
const previousDashboard = queryClient.getQueryData(["dashboardData"]);
|
|
const user = queryClient.getQueryData(["user"]) as any;
|
|
const targetCollection = previousCollections.find(
|
|
(collection: any) => collection.id === id
|
|
);
|
|
const isOwner = Boolean(
|
|
targetCollection?.ownerId != null &&
|
|
user?.id != null &&
|
|
targetCollection.ownerId === user.id
|
|
);
|
|
const collectionIdsToRemove = isOwner
|
|
? getCollectionSubtreeIds(previousCollections, id)
|
|
: [id];
|
|
const deletedCollectionIds = new Set(collectionIdsToRemove);
|
|
|
|
queryClient.setQueryData(["collections"], (oldData: any) => {
|
|
return oldData?.filter(
|
|
(collection: any) => !deletedCollectionIds.has(collection.id)
|
|
);
|
|
});
|
|
|
|
queryClient.setQueryData(["dashboardData"], (oldData: any) => {
|
|
return removeCollectionsFromDashboard(oldData, collectionIdsToRemove);
|
|
});
|
|
|
|
return { previousCollections, previousDashboard, collectionIdsToRemove };
|
|
},
|
|
onError: (error, _variables, context) => {
|
|
if (toast && t) toast.error(t(error.message));
|
|
else if (Alert)
|
|
Alert.alert("Error", "There was an error deleting the collection.");
|
|
|
|
if (!context) return;
|
|
|
|
queryClient.setQueryData(["collections"], context.previousCollections);
|
|
queryClient.setQueryData(["dashboardData"], context.previousDashboard);
|
|
},
|
|
onSuccess: (data, id, context) => {
|
|
const collectionIdsToRemove = context?.collectionIdsToRemove ?? [id];
|
|
const deletedCollectionIds = new Set(collectionIdsToRemove);
|
|
|
|
queryClient.setQueryData(["collections"], (oldData: any) => {
|
|
return oldData?.filter(
|
|
(collection: any) => !deletedCollectionIds.has(collection.id)
|
|
);
|
|
});
|
|
|
|
queryClient.setQueryData(["dashboardData"], (oldData: any) => {
|
|
return removeCollectionsFromDashboard(oldData, collectionIdsToRemove);
|
|
});
|
|
|
|
queryClient.invalidateQueries({ queryKey: ["tags"] });
|
|
queryClient.invalidateQueries({ queryKey: ["links"] });
|
|
},
|
|
});
|
|
};
|
|
|
|
export {
|
|
useCollections,
|
|
useCreateCollection,
|
|
useUpdateCollection,
|
|
useDeleteCollection,
|
|
};
|