diff --git a/components/CollectionListing.tsx b/components/CollectionListing.tsx index 72dc5d40..c6119ed0 100644 --- a/components/CollectionListing.tsx +++ b/components/CollectionListing.tsx @@ -1,257 +1,359 @@ -import useAccountStore from "@/store/account"; -import useCollectionStore from "@/store/collections"; +import React, { Component, useCallback, useEffect, useState } from "react"; +import Tree, { + mutateTree, + moveItemOnTree, + RenderItemParams, + TreeItem, + TreeData, + ItemId, + TreeSourcePosition, + TreeDestinationPosition, +} from "@atlaskit/tree"; import { CollectionIncludingMembersAndLinkCount } from "@/types/global"; -import Link from "next/link"; -import { useRouter } from "next/router"; -import React, { useEffect, useState } from "react"; -import { - DragDropContext, - Draggable, - DraggableProvided, -} from "react-beautiful-dnd"; -import { StrictModeDroppable as Droppable } from "./StrictModeDroppable"; +import useCollectionStore from "@/store/collections"; -type Props = { - links: boolean; -}; +const collections = [ + { + id: 262, + name: "dasd", + description: "", + color: "#0ea5e9", + parentId: null, + isPublic: false, + ownerId: 1, + createdAt: "2024-02-18T23:40:44.043Z", + updatedAt: "2024-02-19T19:16:14.873Z", + parent: null, + members: [ + { + userId: 17, + collectionId: 262, + canCreate: true, + canUpdate: false, + canDelete: false, + createdAt: "2024-02-19T19:16:14.873Z", + updatedAt: "2024-02-19T19:16:14.873Z", + user: { username: "test", name: "ben", image: "" }, + }, + ], + _count: { links: 0 }, + }, + { + id: 268, + name: "ab", + description: "", + color: "#0ea5e9", + parentId: 267, + isPublic: false, + ownerId: 17, + createdAt: "2024-02-19T21:06:52.545Z", + updatedAt: "2024-02-19T21:06:52.545Z", + parent: { id: 267, name: "a" }, + members: [], + _count: { links: 0 }, + }, + { + id: 269, + name: "abc", + description: "", + color: "#0ea5e9", + parentId: 268, + isPublic: false, + ownerId: 17, + createdAt: "2024-02-19T21:07:08.565Z", + updatedAt: "2024-02-19T21:07:08.565Z", + parent: { id: 268, name: "ab" }, + members: [], + _count: { links: 0 }, + }, + { + id: 267, + name: "a", + description: "", + color: "#0ea5e9", + parentId: null, + isPublic: false, + ownerId: 17, + createdAt: "2024-02-19T21:06:45.402Z", + updatedAt: "2024-02-26T16:59:20.312Z", + parent: null, + members: [], + _count: { links: 0 }, + }, + { + id: 80, + name: "abc", + description: "s", + color: "#0ea5e9", + parentId: 79, + isPublic: false, + ownerId: 1, + createdAt: "2024-02-05T07:00:46.881Z", + updatedAt: "2024-02-27T06:11:46.358Z", + parent: { id: 79, name: "ab" }, + members: [ + { + userId: 17, + collectionId: 80, + canCreate: false, + canUpdate: false, + canDelete: false, + createdAt: "2024-02-27T06:11:46.358Z", + updatedAt: "2024-02-27T06:11:46.358Z", + user: { username: "test", name: "ben", image: "" }, + }, + { + userId: 2, + collectionId: 80, + canCreate: false, + canUpdate: false, + canDelete: false, + createdAt: "2024-02-27T06:11:46.358Z", + updatedAt: "2024-02-27T06:11:46.358Z", + user: { + username: "sarah_connor", + name: "Sarah Smith", + image: "uploads/avatar/2.jpg", + }, + }, + ], + _count: { links: 0 }, + }, +]; -const CollectionListing = ({ links }: Props) => { - const { collections } = useCollectionStore(); - const { account, updateAccount } = useAccountStore(); - // Use local state to store the collection order so we don't have to wait for a response from the API to update the UI - const [localCollectionOrder, setLocalCollectionOrder] = useState( - [] - ); - const [active, setActive] = useState(""); - const router = useRouter(); +const DragDropWithNestingTree = () => { + const buildTreeFromCollections = (collections) => { + // Step 1: Map Collections to TreeItems + const items = collections.reduce((acc, collection) => { + acc[collection.id] = { + id: collection.id.toString(), + children: [], + hasChildren: false, // Initially assume no children, adjust in Step 2 + isExpanded: false, + data: { + title: collection.name, + description: collection.description, + color: collection.color, + isPublic: collection.isPublic, + ownerId: collection.ownerId, + createdAt: collection.createdAt, + updatedAt: collection.updatedAt, + }, + }; + return acc; + }, {}); - useEffect(() => { - setActive(router.asPath); - setLocalCollectionOrder(account.collectionOrder || []); - - // if a collection wasn't in the collectionOrder, add it to the end - if (account.username) { - if (!account.collectionOrder || account.collectionOrder.length === 0) - updateAccount({ - ...account, - collectionOrder: collections - .filter((e) => e.parentId === null) // Filter out collections with non-null parentId - .map((e) => e.id as number), // Use "as number" to assert that e.id is a number - }); - else { - const newCollectionOrder: number[] = [ - ...(account.collectionOrder || []), - ]; - - collections?.forEach((collection) => { - if ( - account.username && - !newCollectionOrder.includes(collection.id as number) && - (!collection.parentId || collection.ownerId !== account.id) - ) { - newCollectionOrder.push(collection.id as number); - } - }); - - if (newCollectionOrder.length > account.collectionOrder.length) { - updateAccount({ - ...account, - collectionOrder: newCollectionOrder, - }); - } + // Step 2: Build Hierarchy + collections.forEach((collection) => { + const parentId = collection.parentId; + if (parentId !== null && items[parentId]) { + items[parentId].children.push(collection.id.toString()); + items[parentId].hasChildren = true; } - } - }, [router, collections, account]); + }); - return ( - { - if (!result.destination) { - return; // Dragged outside the droppable area, do nothing - } - - const updatedCollectionOrder = [...account.collectionOrder]; - const [movedCollectionId] = updatedCollectionOrder.splice( - result.source.index, - 1 - ); - updatedCollectionOrder.splice( - result.destination.index, - 0, - movedCollectionId - ); - - // Update local state with the new collection order - setLocalCollectionOrder(updatedCollectionOrder); - - // Update account with the new collection order - updateAccount({ - ...account, - collectionOrder: updatedCollectionOrder, - }); - }} - > - - {(provided) => ( -
- {localCollectionOrder?.map((collectionId, index) => { - const collection = collections.find((c) => c.id === collectionId); - - if (collection) { - return ( - - {(provided) => ( - - )} - - ); - } - })} - {provided.placeholder} -
- )} -
-
- ); -}; - -export default CollectionListing; - -const CollectionItem = ({ - collection, - active, - collections, - innerRef, - ...props -}: { - collection: CollectionIncludingMembersAndLinkCount; - active: string; - collections: CollectionIncludingMembersAndLinkCount[]; - innerRef?: DraggableProvided["innerRef"]; -}) => { - const hasChildren = collections.some((e) => e.parentId === collection.id); - - // Check if the current collection or any of its subcollections is active - const isActiveOrParentOfActive = React.useMemo(() => { - const isActive = active === `/collections/${collection.id}`; - if (isActive) return true; - - const checkIfParentOfActive = (parentId: number): boolean => { - return collections.some((e) => { - if (e.id === parseInt(active.split("/collections/")[1])) { - if (e.parentId === parentId) return true; - if (e.parentId) return checkIfParentOfActive(e.parentId); - } - return false; - }); + // Define a root item to act as the top-level node of your tree if needed + const rootId = "root"; + items[rootId] = { + id: rootId, + children: collections + .filter((c) => c.parentId === null) + .map((c) => c.id.toString()), + hasChildren: true, + isExpanded: true, + data: { title: "Root" }, }; - return checkIfParentOfActive(collection.id as number); - }, [active, collection.id, collections]); + return { rootId, items }; + }; - const [isOpen, setIsOpen] = useState(isActiveOrParentOfActive); + const [tree, setTree] = useState(); + + const { collections } = useCollectionStore(); useEffect(() => { - setIsOpen(isActiveOrParentOfActive); - }, [isActiveOrParentOfActive]); + const initialTree = buildTreeFromCollections(collections); + collections[0] && setTree(initialTree); + }, [collections]); - return hasChildren ? ( - <> -
- - + ) : ( + + ); + } + return ; + }, []); + + const renderItem = useCallback( + ({ item, onExpand, onCollapse, provided }: RenderItemParams) => { + return ( +
- -

{collection.name}

- - {collection.isPublic ? ( - - ) : undefined} -
- {collection._count?.links} -
-
- -
- {isOpen && hasChildren && ( -
- {collections - .filter((e) => e.parentId === collection.id) - .map((subCollection) => ( - - ))} -
- )} - - ) : ( -
- -
- -

{collection.name}

- - {collection.isPublic ? ( - - ) : undefined} -
- {collection._count?.links} + {getIcon(item, onExpand, onCollapse)} + {item.data ? item.data.title : ""}
- -
+ ); + }, + [getIcon] + ); + + const onExpand = useCallback( + (itemId: ItemId) => { + if (tree) { + setTree((currentTree) => + mutateTree(currentTree, itemId, { isExpanded: true }) + ); + } + }, + [tree] + ); + + const onCollapse = useCallback( + (itemId: ItemId) => { + if (tree) { + setTree((currentTree) => + mutateTree(currentTree, itemId, { isExpanded: false }) + ); + } + }, + [tree] + ); + + const onDragEnd = useCallback( + (source: TreeSourcePosition, destination?: TreeDestinationPosition) => { + if (!destination || !tree) { + return; + } + + setTree((currentTree) => + moveItemOnTree(currentTree, source, destination) + ); + }, + [tree] + ); + + if (!tree) { + return
Loading...
; // or any other loading state representation + } + + return ( + ); }; + +export default DragDropWithNestingTree; + +class TreeBuilder { + rootId: ItemId; + + items: Record; + + constructor(rootId: ItemId) { + const rootItem = this._createItem(`${rootId}`); + this.rootId = rootItem.id; + this.items = { + [rootItem.id]: rootItem, + }; + } + + withLeaf(id: number) { + const leafItem = this._createItem(`${this.rootId}-${id}`); + this._addItemToRoot(leafItem.id); + this.items[leafItem.id] = leafItem; + return this; + } + + withSubTree(tree: TreeBuilder) { + const subTree = tree.build(); + this._addItemToRoot(`${this.rootId}-${subTree.rootId}`); + + Object.keys(subTree.items).forEach((itemId) => { + const finalId = `${this.rootId}-${itemId}`; + this.items[finalId] = { + ...subTree.items[itemId], + id: finalId, + children: subTree.items[itemId].children.map( + (i) => `${this.rootId}-${i}` + ), + }; + }); + + return this; + } + + build() { + return { + rootId: this.rootId, + items: this.items, + }; + } + + _addItemToRoot(id: string) { + const rootItem = this.items[this.rootId]; + rootItem.children.push(id); + rootItem.isExpanded = true; + rootItem.hasChildren = true; + } + + _createItem = (id: string) => { + return { + id: `${id}`, + children: [], + hasChildren: false, + isExpanded: false, + isChildrenLoading: false, + data: { + title: `Title ${id}`, + }, + }; + }; +} + +const complexTree: TreeData = new TreeBuilder(1) + .withLeaf(0) // 0 + .withLeaf(1) // 1 + .withSubTree( + new TreeBuilder(2) // 2 + .withLeaf(0) // 3 + .withLeaf(1) // 4 + .withLeaf(2) // 5 + .withLeaf(3) // 6 + ) + .withLeaf(3) // 7 + .withLeaf(4) // 8 + .withLeaf(5) // 9 + .withSubTree( + new TreeBuilder(6) // 10 + .withLeaf(0) // 11 + .withLeaf(1) // 12 + .withSubTree( + new TreeBuilder(2) // 13 + .withLeaf(0) // 14 + .withLeaf(1) // 15 + .withLeaf(2) // 16 + ) + .withLeaf(3) // 17 + .withLeaf(4) // 18 + ) + .withLeaf(7) // 19 + .withLeaf(8) // 20 + .build(); diff --git a/package.json b/package.json index 81056fda..9193b5bf 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "format": "prettier --write \"**/*.{ts,tsx,js,json,md}\"" }, "dependencies": { + "@atlaskit/tree": "^8.8.7", "@auth/prisma-adapter": "^1.0.1", "@aws-sdk/client-s3": "^3.379.1", "@headlessui/react": "^1.7.15", @@ -84,4 +85,4 @@ "ts-node": "^10.9.2", "typescript": "4.9.4" } -} \ No newline at end of file +} diff --git a/types/nav.ts b/types/nav.ts new file mode 100644 index 00000000..5fa87044 --- /dev/null +++ b/types/nav.ts @@ -0,0 +1 @@ +declare module "@atlaskit/navigation"; diff --git a/yarn.lock b/yarn.lock index 0ef136b2..1f6899a0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12,6 +12,15 @@ resolved "https://registry.yarnpkg.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz#7bf68b20c0a350f936915fcae06f58e32007ce30" integrity sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw== +"@atlaskit/tree@^8.8.7": + version "8.8.7" + resolved "https://registry.yarnpkg.com/@atlaskit/tree/-/tree-8.8.7.tgz#f895137b063f676a490abb0b5deb939a96f51fd7" + integrity sha512-ftbFCzZoa5tZh35EdwMEP9lPuBfw19vtB1CcBmDDMP0AnyEXLjUVfVo8kIls6oI4wivYfIWkZgrUlgN+Jk1b0Q== + dependencies: + "@babel/runtime" "^7.0.0" + css-box-model "^1.2.0" + react-beautiful-dnd-next "11.0.5" + "@auth/core@0.9.0": version "0.9.0" resolved "https://registry.yarnpkg.com/@auth/core/-/core-0.9.0.tgz#7a5d66eea0bc059cef072734698547ae2a0c86a6" @@ -614,6 +623,21 @@ chalk "^2.0.0" js-tokens "^4.0.0" +"@babel/runtime-corejs2@^7.4.5": + version "7.24.0" + resolved "https://registry.yarnpkg.com/@babel/runtime-corejs2/-/runtime-corejs2-7.24.0.tgz#23c12d76ac8a7a0ec223c4b0c3b937f9c203fa33" + integrity sha512-RZVGq1it0GA1K8rb+z7v7NzecP6VYCMedN7yHsCCIQUMmRXFCPJD8GISdf6uIGj7NDDihg7ieQEzpdpQbUL75Q== + dependencies: + core-js "^2.6.12" + regenerator-runtime "^0.14.0" + +"@babel/runtime@^7.0.0": + version "7.24.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.0.tgz#584c450063ffda59697021430cb47101b085951e" + integrity sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw== + dependencies: + regenerator-runtime "^0.14.0" + "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.5", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.13", "@babel/runtime@^7.20.7", "@babel/runtime@^7.5.5", "@babel/runtime@^7.8.7": version "7.21.5" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.21.5.tgz#8492dddda9644ae3bda3b45eabe87382caee7200" @@ -2640,6 +2664,11 @@ cookie@0.5.0, cookie@^0.5.0: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b" integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== +core-js@^2.6.12: + version "2.6.12" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec" + integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ== + core-util-is@1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" @@ -2675,7 +2704,7 @@ crypto-js@^4.2.0: resolved "https://registry.yarnpkg.com/crypto-js/-/crypto-js-4.2.0.tgz#4d931639ecdfd12ff80e8186dba6af2c2e856631" integrity sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q== -css-box-model@^1.2.0: +css-box-model@^1.1.2, css-box-model@^1.2.0: version "1.2.1" resolved "https://registry.yarnpkg.com/css-box-model/-/css-box-model-1.2.1.tgz#59951d3b81fd6b2074a62d49444415b0d2b4d7c1" integrity sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw== @@ -4351,7 +4380,7 @@ make-error@^1.1.1: resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== -memoize-one@^5.1.1: +memoize-one@^5.0.4, memoize-one@^5.1.1: version "5.2.1" resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-5.2.1.tgz#8337aa3c4335581839ec01c3d594090cebe8f00e" integrity sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q== @@ -5079,7 +5108,7 @@ queue-microtask@^1.2.2: resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== -raf-schd@^4.0.2: +raf-schd@^4.0.0, raf-schd@^4.0.2: version "4.0.3" resolved "https://registry.yarnpkg.com/raf-schd/-/raf-schd-4.0.3.tgz#5d6c34ef46f8b2a0e880a8fcdb743efc5bfdbc1a" integrity sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ== @@ -5094,6 +5123,20 @@ raw-body@2.4.1: iconv-lite "0.4.24" unpipe "1.0.0" +react-beautiful-dnd-next@11.0.5: + version "11.0.5" + resolved "https://registry.yarnpkg.com/react-beautiful-dnd-next/-/react-beautiful-dnd-next-11.0.5.tgz#41e693733bbdeb6269b9e4b923a36de2e99ed761" + integrity sha512-kM5Mob41HkA3ShS9uXqeMkW51L5bVsfttxfrwwHucu7I6SdnRKCyN78t6QiLH/UJQQ8T4ukI6NeQAQQpGwolkg== + dependencies: + "@babel/runtime-corejs2" "^7.4.5" + css-box-model "^1.1.2" + memoize-one "^5.0.4" + raf-schd "^4.0.0" + react-redux "^7.0.3" + redux "^4.0.1" + tiny-invariant "^1.0.4" + use-memo-one "^1.1.0" + react-beautiful-dnd@^13.1.1: version "13.1.1" resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-13.1.1.tgz#b0f3087a5840920abf8bb2325f1ffa46d8c4d0a2" @@ -5142,7 +5185,7 @@ react-is@^17.0.2: resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== -react-redux@^7.2.0: +react-redux@^7.0.3, react-redux@^7.2.0: version "7.2.9" resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.2.9.tgz#09488fbb9416a4efe3735b7235055442b042481d" integrity sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ== @@ -5244,7 +5287,7 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" -redux@^4.0.0, redux@^4.0.4: +redux@^4.0.0, redux@^4.0.1, redux@^4.0.4: version "4.2.1" resolved "https://registry.yarnpkg.com/redux/-/redux-4.2.1.tgz#c08f4306826c49b5e9dc901dee0452ea8fce6197" integrity sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w== @@ -5774,6 +5817,11 @@ tiny-glob@^0.2.9: globalyzer "0.1.0" globrex "^0.1.2" +tiny-invariant@^1.0.4: + version "1.3.3" + resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz#46680b7a873a0d5d10005995eb90a70d74d60127" + integrity sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg== + tiny-invariant@^1.0.6: version "1.3.1" resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.1.tgz#8560808c916ef02ecfd55e66090df23a4b7aa642" @@ -6015,7 +6063,7 @@ use-isomorphic-layout-effect@^1.1.2: resolved "https://registry.yarnpkg.com/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz#497cefb13d863d687b08477d9e5a164ad8c1a6fb" integrity sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA== -use-memo-one@^1.1.1: +use-memo-one@^1.1.0, use-memo-one@^1.1.1: version "1.1.3" resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-1.1.3.tgz#2fd2e43a2169eabc7496960ace8c79efef975e99" integrity sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==