2024-03-02 08:07:33 -06:00
|
|
|
import React, { useEffect, useMemo, useState } from "react";
|
2024-02-28 23:05:38 -06:00
|
|
|
import Tree, {
|
|
|
|
mutateTree,
|
|
|
|
moveItemOnTree,
|
|
|
|
RenderItemParams,
|
|
|
|
TreeItem,
|
|
|
|
TreeData,
|
|
|
|
ItemId,
|
|
|
|
TreeSourcePosition,
|
|
|
|
TreeDestinationPosition,
|
|
|
|
} from "@atlaskit/tree";
|
2024-03-01 05:59:14 -06:00
|
|
|
import { Collection } from "@prisma/client";
|
|
|
|
import Link from "next/link";
|
2024-03-01 08:33:58 -06:00
|
|
|
import { CollectionIncludingMembersAndLinkCount } from "@/types/global";
|
2024-03-01 13:02:55 -06:00
|
|
|
import { useRouter } from "next/router";
|
2024-03-04 07:56:16 -06:00
|
|
|
import toast from "react-hot-toast";
|
2024-06-09 08:27:16 -05:00
|
|
|
import { useTranslation } from "next-i18next";
|
2024-07-30 13:57:09 -05:00
|
|
|
import { useCollections, useUpdateCollection } from "@/hooks/store/collections";
|
2024-07-31 13:15:50 -05:00
|
|
|
import { useUpdateUser, useUser } from "@/hooks/store/user";
|
2024-02-05 01:42:54 -06:00
|
|
|
|
2024-03-01 05:59:14 -06:00
|
|
|
interface ExtendedTreeItem extends TreeItem {
|
|
|
|
data: Collection;
|
|
|
|
}
|
2024-02-05 01:42:54 -06:00
|
|
|
|
2024-03-01 13:02:55 -06:00
|
|
|
const CollectionListing = () => {
|
2024-06-09 08:27:16 -05:00
|
|
|
const { t } = useTranslation();
|
2024-07-30 13:57:09 -05:00
|
|
|
const updateCollection = useUpdateCollection();
|
2024-08-01 17:40:08 -05:00
|
|
|
const { data: collections } = useCollections();
|
2024-07-30 22:19:29 -05:00
|
|
|
|
2024-08-01 17:40:08 -05:00
|
|
|
const { data: user } = useUser();
|
2024-07-30 22:19:29 -05:00
|
|
|
const updateUser = useUpdateUser();
|
2024-02-28 23:05:38 -06:00
|
|
|
|
2024-03-01 13:02:55 -06:00
|
|
|
const router = useRouter();
|
|
|
|
const currentPath = router.asPath;
|
|
|
|
|
2024-07-30 22:19:29 -05:00
|
|
|
const [tree, setTree] = useState<TreeData | undefined>();
|
|
|
|
|
2024-03-02 08:07:33 -06:00
|
|
|
const initialTree = useMemo(() => {
|
2024-07-30 22:19:29 -05:00
|
|
|
if (
|
|
|
|
// !tree &&
|
|
|
|
collections.length > 0
|
|
|
|
) {
|
2024-03-04 07:56:16 -06:00
|
|
|
return buildTreeFromCollections(
|
|
|
|
collections,
|
|
|
|
router,
|
2024-07-30 22:19:29 -05:00
|
|
|
user.collectionOrder
|
2024-03-04 07:56:16 -06:00
|
|
|
);
|
2024-07-30 22:19:29 -05:00
|
|
|
} else return undefined;
|
|
|
|
}, [collections, user, router]);
|
2024-03-02 08:07:33 -06:00
|
|
|
|
2024-02-28 23:05:38 -06:00
|
|
|
useEffect(() => {
|
2024-07-30 22:19:29 -05:00
|
|
|
// if (!tree)
|
2024-03-02 08:07:33 -06:00
|
|
|
setTree(initialTree);
|
|
|
|
}, [initialTree]);
|
2024-02-28 23:05:38 -06:00
|
|
|
|
2024-03-04 07:56:16 -06:00
|
|
|
useEffect(() => {
|
2024-07-30 22:19:29 -05:00
|
|
|
if (user.username) {
|
2024-03-10 05:08:28 -05:00
|
|
|
if (
|
2024-07-30 22:19:29 -05:00
|
|
|
(!user.collectionOrder || user.collectionOrder.length === 0) &&
|
2024-03-10 05:08:28 -05:00
|
|
|
collections.length > 0
|
|
|
|
)
|
2024-07-30 22:19:29 -05:00
|
|
|
updateUser.mutate({
|
|
|
|
...user,
|
2024-03-04 07:56:16 -06:00
|
|
|
collectionOrder: collections
|
2024-03-05 07:50:47 -06:00
|
|
|
.filter(
|
|
|
|
(e) =>
|
|
|
|
e.parentId === null ||
|
|
|
|
!collections.find((i) => i.id === e.parentId)
|
|
|
|
) // Filter out collections with non-null parentId
|
2024-07-30 22:19:29 -05:00
|
|
|
.map((e) => e.id as number),
|
2024-03-04 07:56:16 -06:00
|
|
|
});
|
|
|
|
else {
|
2024-07-30 22:19:29 -05:00
|
|
|
const newCollectionOrder: number[] = [...(user.collectionOrder || [])];
|
2024-03-05 07:50:47 -06:00
|
|
|
|
|
|
|
// Start with collections that are in both account.collectionOrder and collections
|
|
|
|
const existingCollectionIds = collections.map((c) => c.id as number);
|
2024-07-30 22:19:29 -05:00
|
|
|
const filteredCollectionOrder = user.collectionOrder.filter((id: any) =>
|
2024-03-05 07:50:47 -06:00
|
|
|
existingCollectionIds.includes(id)
|
|
|
|
);
|
|
|
|
|
|
|
|
// Add new collections that are not in account.collectionOrder and meet the specific conditions
|
|
|
|
collections.forEach((collection) => {
|
|
|
|
if (
|
|
|
|
!filteredCollectionOrder.includes(collection.id as number) &&
|
2024-07-30 22:19:29 -05:00
|
|
|
(!collection.parentId || collection.ownerId === user.id)
|
2024-03-05 07:50:47 -06:00
|
|
|
) {
|
|
|
|
filteredCollectionOrder.push(collection.id as number);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
// check if the newCollectionOrder is the same as the old one
|
|
|
|
if (
|
|
|
|
JSON.stringify(newCollectionOrder) !==
|
2024-07-30 22:19:29 -05:00
|
|
|
JSON.stringify(user.collectionOrder)
|
2024-03-05 07:50:47 -06:00
|
|
|
) {
|
2024-07-30 22:19:29 -05:00
|
|
|
updateUser.mutateAsync({
|
|
|
|
...user,
|
2024-03-05 07:50:47 -06:00
|
|
|
collectionOrder: newCollectionOrder,
|
|
|
|
});
|
|
|
|
}
|
2024-03-04 07:56:16 -06:00
|
|
|
}
|
|
|
|
}
|
2024-03-05 07:50:47 -06:00
|
|
|
}, [collections]);
|
2024-03-04 07:56:16 -06:00
|
|
|
|
|
|
|
const onExpand = (movedCollectionId: ItemId) => {
|
2024-03-02 08:07:33 -06:00
|
|
|
setTree((currentTree) =>
|
2024-03-04 07:56:16 -06:00
|
|
|
mutateTree(currentTree!, movedCollectionId, { isExpanded: true })
|
2024-03-02 08:07:33 -06:00
|
|
|
);
|
2024-03-01 05:59:14 -06:00
|
|
|
};
|
2024-02-22 01:51:51 -06:00
|
|
|
|
2024-03-04 07:56:16 -06:00
|
|
|
const onCollapse = (movedCollectionId: ItemId) => {
|
2024-03-02 08:07:33 -06:00
|
|
|
setTree((currentTree) =>
|
2024-03-04 07:56:16 -06:00
|
|
|
mutateTree(currentTree as TreeData, movedCollectionId, {
|
|
|
|
isExpanded: false,
|
|
|
|
})
|
2024-03-02 08:07:33 -06:00
|
|
|
);
|
2024-03-01 05:59:14 -06:00
|
|
|
};
|
|
|
|
|
2024-03-04 07:56:16 -06:00
|
|
|
const onDragEnd = async (
|
2024-03-01 05:59:14 -06:00
|
|
|
source: TreeSourcePosition,
|
|
|
|
destination: TreeDestinationPosition | undefined
|
|
|
|
) => {
|
|
|
|
if (!destination || !tree) {
|
|
|
|
return;
|
|
|
|
}
|
2024-03-04 07:56:16 -06:00
|
|
|
|
|
|
|
if (
|
|
|
|
source.index === destination.index &&
|
|
|
|
source.parentId === destination.parentId
|
|
|
|
) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const movedCollectionId = Number(
|
|
|
|
tree.items[source.parentId].children[source.index]
|
|
|
|
);
|
|
|
|
|
|
|
|
const movedCollection = collections.find((c) => c.id === movedCollectionId);
|
|
|
|
|
|
|
|
const destinationCollection = collections.find(
|
|
|
|
(c) => c.id === Number(destination.parentId)
|
|
|
|
);
|
|
|
|
|
|
|
|
if (
|
2024-07-30 22:19:29 -05:00
|
|
|
(movedCollection?.ownerId !== user.id &&
|
2024-03-04 07:56:16 -06:00
|
|
|
destination.parentId !== source.parentId) ||
|
2024-07-30 22:19:29 -05:00
|
|
|
(destinationCollection?.ownerId !== user.id &&
|
2024-03-04 07:56:16 -06:00
|
|
|
destination.parentId !== "root")
|
|
|
|
) {
|
2024-06-09 08:27:16 -05:00
|
|
|
return toast.error(t("cant_change_collection_you_dont_own"));
|
2024-03-04 07:56:16 -06:00
|
|
|
}
|
|
|
|
|
2024-03-01 05:59:14 -06:00
|
|
|
setTree((currentTree) => moveItemOnTree(currentTree!, source, destination));
|
2024-03-04 07:56:16 -06:00
|
|
|
|
2024-07-30 22:19:29 -05:00
|
|
|
const updatedCollectionOrder = [...user.collectionOrder];
|
2024-03-04 07:56:16 -06:00
|
|
|
|
|
|
|
if (source.parentId !== destination.parentId) {
|
2024-07-30 13:57:09 -05:00
|
|
|
await updateCollection.mutateAsync({
|
2024-03-04 07:56:16 -06:00
|
|
|
...movedCollection,
|
|
|
|
parentId:
|
|
|
|
destination.parentId && destination.parentId !== "root"
|
|
|
|
? Number(destination.parentId)
|
|
|
|
: destination.parentId === "root"
|
|
|
|
? "root"
|
|
|
|
: null,
|
|
|
|
} as any);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (
|
|
|
|
destination.index !== undefined &&
|
|
|
|
destination.parentId === source.parentId &&
|
|
|
|
source.parentId === "root"
|
|
|
|
) {
|
2024-03-05 07:50:47 -06:00
|
|
|
updatedCollectionOrder.includes(movedCollectionId) &&
|
|
|
|
updatedCollectionOrder.splice(source.index, 1);
|
2024-03-04 07:56:16 -06:00
|
|
|
|
|
|
|
updatedCollectionOrder.splice(destination.index, 0, movedCollectionId);
|
|
|
|
|
2024-07-30 22:19:29 -05:00
|
|
|
await updateUser.mutateAsync({
|
|
|
|
...user,
|
2024-03-04 07:56:16 -06:00
|
|
|
collectionOrder: updatedCollectionOrder,
|
|
|
|
});
|
|
|
|
} else if (
|
|
|
|
destination.index !== undefined &&
|
|
|
|
destination.parentId === "root"
|
|
|
|
) {
|
|
|
|
updatedCollectionOrder.splice(destination.index, 0, movedCollectionId);
|
|
|
|
|
2024-07-30 22:19:29 -05:00
|
|
|
updateUser.mutate({
|
|
|
|
...user,
|
2024-03-04 07:56:16 -06:00
|
|
|
collectionOrder: updatedCollectionOrder,
|
|
|
|
});
|
|
|
|
} else if (
|
|
|
|
source.parentId === "root" &&
|
|
|
|
destination.parentId &&
|
|
|
|
destination.parentId !== "root"
|
|
|
|
) {
|
|
|
|
updatedCollectionOrder.splice(source.index, 1);
|
|
|
|
|
2024-07-30 22:19:29 -05:00
|
|
|
await updateUser.mutateAsync({
|
|
|
|
...user,
|
2024-03-04 07:56:16 -06:00
|
|
|
collectionOrder: updatedCollectionOrder,
|
|
|
|
});
|
|
|
|
}
|
2024-03-01 05:59:14 -06:00
|
|
|
};
|
2024-02-22 02:04:01 -06:00
|
|
|
|
2024-02-28 23:05:38 -06:00
|
|
|
if (!tree) {
|
2024-05-07 15:59:00 -05:00
|
|
|
return (
|
|
|
|
<p className="text-neutral text-xs font-semibold truncate w-full px-2 mt-5 mb-8">
|
2024-06-09 08:27:16 -05:00
|
|
|
{t("you_have_no_collections")}
|
2024-05-07 15:59:00 -05:00
|
|
|
</p>
|
|
|
|
);
|
2024-03-01 13:02:55 -06:00
|
|
|
} else
|
|
|
|
return (
|
|
|
|
<Tree
|
|
|
|
tree={tree}
|
|
|
|
renderItem={(itemProps) => renderItem({ ...itemProps }, currentPath)}
|
|
|
|
onExpand={onExpand}
|
|
|
|
onCollapse={onCollapse}
|
|
|
|
onDragEnd={onDragEnd}
|
|
|
|
isDragEnabled
|
|
|
|
isNestingEnabled
|
|
|
|
/>
|
|
|
|
);
|
2024-02-05 01:42:54 -06:00
|
|
|
};
|
|
|
|
|
2024-03-01 13:02:55 -06:00
|
|
|
export default CollectionListing;
|
2024-03-01 07:37:20 -06:00
|
|
|
|
2024-03-01 13:02:55 -06:00
|
|
|
const renderItem = (
|
|
|
|
{ item, onExpand, onCollapse, provided }: RenderItemParams,
|
|
|
|
currentPath: string
|
|
|
|
) => {
|
2024-03-01 07:37:20 -06:00
|
|
|
const collection = item.data;
|
|
|
|
|
|
|
|
return (
|
2024-03-01 13:02:55 -06:00
|
|
|
<div ref={provided.innerRef} {...provided.draggableProps} className="mb-1">
|
|
|
|
<div
|
2024-03-05 11:11:40 -06:00
|
|
|
className={`${
|
|
|
|
currentPath === `/collections/${collection.id}`
|
2024-03-01 13:02:55 -06:00
|
|
|
? "bg-primary/20 is-active"
|
|
|
|
: "hover:bg-neutral/20"
|
2024-03-05 11:11:40 -06:00
|
|
|
} duration-100 flex gap-1 items-center pr-2 pl-1 rounded-md`}
|
2024-03-01 07:37:20 -06:00
|
|
|
>
|
2024-03-01 13:02:55 -06:00
|
|
|
{Icon(item as ExtendedTreeItem, onExpand, onCollapse)}
|
2024-03-01 07:37:20 -06:00
|
|
|
|
2024-03-01 13:02:55 -06:00
|
|
|
<Link
|
|
|
|
href={`/collections/${collection.id}`}
|
|
|
|
className="w-full"
|
|
|
|
{...provided.dragHandleProps}
|
|
|
|
>
|
|
|
|
<div
|
|
|
|
className={`py-1 cursor-pointer flex items-center gap-2 w-full rounded-md h-8 capitalize`}
|
|
|
|
>
|
2024-03-01 07:37:20 -06:00
|
|
|
<i
|
2024-03-01 13:02:55 -06:00
|
|
|
className="bi-folder-fill text-2xl drop-shadow"
|
|
|
|
style={{ color: collection.color }}
|
2024-03-01 07:37:20 -06:00
|
|
|
></i>
|
2024-03-01 13:02:55 -06:00
|
|
|
<p className="truncate w-full">{collection.name}</p>
|
|
|
|
|
|
|
|
{collection.isPublic ? (
|
|
|
|
<i
|
|
|
|
className="bi-globe2 text-sm text-black/50 dark:text-white/50 drop-shadow"
|
|
|
|
title="This collection is being shared publicly."
|
|
|
|
></i>
|
|
|
|
) : undefined}
|
|
|
|
<div className="drop-shadow text-neutral text-xs">
|
|
|
|
{collection._count?.links}
|
|
|
|
</div>
|
2024-03-01 07:37:20 -06:00
|
|
|
</div>
|
2024-03-01 13:02:55 -06:00
|
|
|
</Link>
|
|
|
|
</div>
|
2024-03-01 07:37:20 -06:00
|
|
|
</div>
|
|
|
|
);
|
|
|
|
};
|
|
|
|
|
|
|
|
const Icon = (
|
|
|
|
item: ExtendedTreeItem,
|
|
|
|
onExpand: (id: ItemId) => void,
|
|
|
|
onCollapse: (id: ItemId) => void
|
|
|
|
) => {
|
|
|
|
if (item.children && item.children.length > 0) {
|
|
|
|
return item.isExpanded ? (
|
|
|
|
<button onClick={() => onCollapse(item.id)}>
|
2024-03-01 13:02:55 -06:00
|
|
|
<div className="bi-caret-down-fill opacity-50 hover:opacity-100 duration-200"></div>
|
2024-03-01 07:37:20 -06:00
|
|
|
</button>
|
|
|
|
) : (
|
|
|
|
<button onClick={() => onExpand(item.id)}>
|
2024-03-01 13:02:55 -06:00
|
|
|
<div className="bi-caret-right-fill opacity-40 hover:opacity-100 duration-200"></div>
|
2024-03-01 07:37:20 -06:00
|
|
|
</button>
|
|
|
|
);
|
|
|
|
}
|
|
|
|
// return <span>•</span>;
|
2024-03-05 07:50:47 -06:00
|
|
|
return <div></div>;
|
2024-03-01 07:37:20 -06:00
|
|
|
};
|
2024-03-01 08:33:58 -06:00
|
|
|
|
|
|
|
const buildTreeFromCollections = (
|
2024-03-01 13:02:55 -06:00
|
|
|
collections: CollectionIncludingMembersAndLinkCount[],
|
2024-03-04 07:56:16 -06:00
|
|
|
router: ReturnType<typeof useRouter>,
|
|
|
|
order?: number[]
|
2024-03-01 08:33:58 -06:00
|
|
|
): TreeData => {
|
2024-03-04 07:56:16 -06:00
|
|
|
if (order) {
|
|
|
|
collections.sort((a: any, b: any) => {
|
|
|
|
return order.indexOf(a.id) - order.indexOf(b.id);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2024-03-01 08:33:58 -06:00
|
|
|
const items: { [key: string]: ExtendedTreeItem } = collections.reduce(
|
|
|
|
(acc: any, collection) => {
|
|
|
|
acc[collection.id as number] = {
|
|
|
|
id: collection.id,
|
|
|
|
children: [],
|
|
|
|
hasChildren: false,
|
|
|
|
isExpanded: false,
|
|
|
|
data: {
|
|
|
|
id: collection.id,
|
2024-03-01 13:02:55 -06:00
|
|
|
parentId: collection.parentId,
|
2024-03-01 08:33:58 -06:00
|
|
|
name: collection.name,
|
|
|
|
description: collection.description,
|
|
|
|
color: collection.color,
|
|
|
|
isPublic: collection.isPublic,
|
|
|
|
ownerId: collection.ownerId,
|
|
|
|
createdAt: collection.createdAt,
|
|
|
|
updatedAt: collection.updatedAt,
|
|
|
|
_count: {
|
|
|
|
links: collection._count?.links,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
};
|
|
|
|
return acc;
|
|
|
|
},
|
|
|
|
{}
|
|
|
|
);
|
|
|
|
|
2024-03-01 13:02:55 -06:00
|
|
|
const activeCollectionId = Number(router.asPath.split("/collections/")[1]);
|
|
|
|
|
|
|
|
if (activeCollectionId) {
|
|
|
|
for (const item in items) {
|
|
|
|
const collection = items[item];
|
|
|
|
if (Number(item) === activeCollectionId && collection.data.parentId) {
|
|
|
|
// get all the parents of the active collection recursively until root and set isExpanded to true
|
|
|
|
let parentId = collection.data.parentId || null;
|
2024-03-05 07:50:47 -06:00
|
|
|
while (parentId && items[parentId]) {
|
2024-03-01 13:02:55 -06:00
|
|
|
items[parentId].isExpanded = true;
|
|
|
|
parentId = items[parentId].data.parentId;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-03-01 08:33:58 -06:00
|
|
|
collections.forEach((collection) => {
|
|
|
|
const parentId = collection.parentId;
|
|
|
|
if (parentId && items[parentId] && collection.id) {
|
|
|
|
items[parentId].children.push(collection.id);
|
|
|
|
items[parentId].hasChildren = true;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
const rootId = "root";
|
|
|
|
items[rootId] = {
|
|
|
|
id: rootId,
|
|
|
|
children: (collections
|
2024-03-05 07:50:47 -06:00
|
|
|
.filter(
|
|
|
|
(c) =>
|
|
|
|
c.parentId === null || !collections.find((i) => i.id === c.parentId)
|
|
|
|
)
|
2024-03-01 08:33:58 -06:00
|
|
|
.map((c) => c.id) || "") as unknown as string[],
|
|
|
|
hasChildren: true,
|
|
|
|
isExpanded: true,
|
|
|
|
data: { name: "Root" } as Collection,
|
|
|
|
};
|
|
|
|
|
|
|
|
return { rootId, items };
|
|
|
|
};
|