From f5604224272ead209d0bb9ddccb7c943832b5b7e Mon Sep 17 00:00:00 2001 From: Isaac Wise Date: Sun, 18 Feb 2024 03:32:31 -0600 Subject: [PATCH 1/2] Allow collections with the same name --- .../InputSelect/CollectionSelection.tsx | 61 ++++++++++++++++--- .../controllers/collections/getCollections.ts | 6 ++ .../controllers/collections/postCollection.ts | 29 ++------- lib/api/controllers/links/postLink.ts | 7 +-- .../migration.sql | 2 + prisma/schema.prisma | 2 - 6 files changed, 67 insertions(+), 40 deletions(-) create mode 100644 prisma/migrations/20240218080348_allow_duplicate_collection_names/migration.sql diff --git a/components/InputSelect/CollectionSelection.tsx b/components/InputSelect/CollectionSelection.tsx index ec586ea..5e36ff8 100644 --- a/components/InputSelect/CollectionSelection.tsx +++ b/components/InputSelect/CollectionSelection.tsx @@ -10,11 +10,11 @@ type Props = { onChange: any; showDefaultValue?: boolean; defaultValue?: - | { - label: string; - value?: number; - } - | undefined; + | { + label: string; + value?: number; + } + | undefined; creatable?: boolean; }; @@ -44,12 +44,54 @@ export default function CollectionSelection({ useEffect(() => { const formatedCollections = collections.map((e) => { - return { value: e.id, label: e.name, ownerId: e.ownerId }; + return { value: e.id, label: e.name, ownerId: e.ownerId, count: e._count, parentId: e.parentId }; }); setOptions(formatedCollections); }, [collections]); + const getParentNames = (parentId: number): string[] => { + const parentNames = []; + const parent = collections.find((e) => e.id === parentId); + + if (parent) { + parentNames.push(parent.name); + if (parent.parentId) { + parentNames.push(...getParentNames(parent.parentId)); + } + } + + // Have the top level parent at beginning + return parentNames.reverse(); + } + + const customOption = ({ data, innerProps }: any) => { + return ( +
+
+ + {data.label} + + + {data.count?.links} + +
+
+ {getParentNames(data?.parentId).length > 0 ? ( + <> + {getParentNames(data.parentId).join(' > ')} {'>'} {data.label} + + ) : ( + data.label + )} +
+
+ ); + }; + if (creatable) { return ( ); } else { @@ -73,7 +118,7 @@ export default function CollectionSelection({ options={options} styles={styles} defaultValue={showDefaultValue ? defaultValue : null} - // menuPosition="fixed" + // menuPosition="fixed" /> ); } diff --git a/lib/api/controllers/collections/getCollections.ts b/lib/api/controllers/collections/getCollections.ts index 3742163..fa13798 100644 --- a/lib/api/controllers/collections/getCollections.ts +++ b/lib/api/controllers/collections/getCollections.ts @@ -12,6 +12,12 @@ export default async function getCollection(userId: number) { _count: { select: { links: true }, }, + parent: { + select: { + id: true, + name: true, + }, + }, members: { include: { user: { diff --git a/lib/api/controllers/collections/postCollection.ts b/lib/api/controllers/collections/postCollection.ts index 245f642..94f4763 100644 --- a/lib/api/controllers/collections/postCollection.ts +++ b/lib/api/controllers/collections/postCollection.ts @@ -32,27 +32,6 @@ export default async function postCollection( }; } - const findCollection = await prisma.user.findUnique({ - where: { - id: userId, - }, - select: { - collections: { - where: { - name: collection.name, - }, - }, - }, - }); - - const checkIfCollectionExists = findCollection?.collections[0]; - - if (checkIfCollectionExists) - return { - response: "Oops! There's already a Collection with that name.", - status: 400, - }; - const newCollection = await prisma.collection.create({ data: { owner: { @@ -65,10 +44,10 @@ export default async function postCollection( color: collection.color, parent: collection.parentId ? { - connect: { - id: collection.parentId, - }, - } + connect: { + id: collection.parentId, + }, + } : undefined, }, include: { diff --git a/lib/api/controllers/links/postLink.ts b/lib/api/controllers/links/postLink.ts index 1c82fc2..de2d80c 100644 --- a/lib/api/controllers/links/postLink.ts +++ b/lib/api/controllers/links/postLink.ts @@ -88,14 +88,11 @@ export default async function postLink( collection: { connectOrCreate: { where: { - name_ownerId: { - ownerId: link.collection.ownerId, - name: link.collection.name, - }, + id: link.collection.id ?? 0, }, create: { name: link.collection.name.trim(), - ownerId: userId, + ownerId: userId }, }, }, diff --git a/prisma/migrations/20240218080348_allow_duplicate_collection_names/migration.sql b/prisma/migrations/20240218080348_allow_duplicate_collection_names/migration.sql new file mode 100644 index 0000000..d73171b --- /dev/null +++ b/prisma/migrations/20240218080348_allow_duplicate_collection_names/migration.sql @@ -0,0 +1,2 @@ +-- DropIndex +DROP INDEX "Collection_name_ownerId_key"; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 8cfa7b3..f081fc5 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -91,8 +91,6 @@ model Collection { links Link[] createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt - - @@unique([name, ownerId]) } model UsersAndCollections { From bbc2e4c457fdfce47e6b2860d34b08861b3ac824 Mon Sep 17 00:00:00 2001 From: daniel31x13 Date: Mon, 19 Feb 2024 14:37:07 -0500 Subject: [PATCH 2/2] final touch --- ...ionSelection.tsx => CollectionListing.tsx} | 0 .../InputSelect/CollectionSelection.tsx | 39 ++++---- components/Sidebar.tsx | 4 +- lib/api/controllers/links/postLink.ts | 89 +++++++++++++------ prisma/schema.prisma | 1 - 5 files changed, 88 insertions(+), 45 deletions(-) rename components/{CollectionSelection.tsx => CollectionListing.tsx} (100%) diff --git a/components/CollectionSelection.tsx b/components/CollectionListing.tsx similarity index 100% rename from components/CollectionSelection.tsx rename to components/CollectionListing.tsx diff --git a/components/InputSelect/CollectionSelection.tsx b/components/InputSelect/CollectionSelection.tsx index 5e36ff8..99b999e 100644 --- a/components/InputSelect/CollectionSelection.tsx +++ b/components/InputSelect/CollectionSelection.tsx @@ -10,11 +10,11 @@ type Props = { onChange: any; showDefaultValue?: boolean; defaultValue?: - | { - label: string; - value?: number; - } - | undefined; + | { + label: string; + value?: number; + } + | undefined; creatable?: boolean; }; @@ -44,7 +44,13 @@ export default function CollectionSelection({ useEffect(() => { const formatedCollections = collections.map((e) => { - return { value: e.id, label: e.name, ownerId: e.ownerId, count: e._count, parentId: e.parentId }; + return { + value: e.id, + label: e.name, + ownerId: e.ownerId, + count: e._count, + parentId: e.parentId, + }; }); setOptions(formatedCollections); @@ -63,26 +69,22 @@ export default function CollectionSelection({ // Have the top level parent at beginning return parentNames.reverse(); - } + }; const customOption = ({ data, innerProps }: any) => { return (
- - {data.label} - - - {data.count?.links} - + {data.label} + {data.count?.links}
{getParentNames(data?.parentId).length > 0 ? ( <> - {getParentNames(data.parentId).join(' > ')} {'>'} {data.label} + {getParentNames(data.parentId).join(" > ")} {">"} {data.label} ) : ( data.label @@ -105,7 +107,7 @@ export default function CollectionSelection({ components={{ Option: customOption, }} - // menuPosition="fixed" + // menuPosition="fixed" /> ); } else { @@ -118,7 +120,10 @@ export default function CollectionSelection({ options={options} styles={styles} defaultValue={showDefaultValue ? defaultValue : null} - // menuPosition="fixed" + components={{ + Option: customOption, + }} + // menuPosition="fixed" /> ); } diff --git a/components/Sidebar.tsx b/components/Sidebar.tsx index 66a8d23..3f3e97f 100644 --- a/components/Sidebar.tsx +++ b/components/Sidebar.tsx @@ -5,7 +5,7 @@ import { useRouter } from "next/router"; import { useEffect, useState } from "react"; import { Disclosure, Transition } from "@headlessui/react"; import SidebarHighlightLink from "@/components/SidebarHighlightLink"; -import CollectionSelection from "@/components/CollectionSelection"; +import CollectionListing from "@/components/CollectionListing"; export default function Sidebar({ className }: { className?: string }) { const [tagDisclosure, setTagDisclosure] = useState(() => { @@ -99,7 +99,7 @@ export default function Sidebar({ className }: { className?: string }) { leaveTo="transform opacity-0 -translate-y-3" > - + diff --git a/lib/api/controllers/links/postLink.ts b/lib/api/controllers/links/postLink.ts index de2d80c..eb82949 100644 --- a/lib/api/controllers/links/postLink.ts +++ b/lib/api/controllers/links/postLink.ts @@ -22,8 +22,69 @@ export default async function postLink( }; } - if (!link.collection.name) { + if (!link.collection.id && link.collection.name) { + link.collection.name = link.collection.name.trim(); + + // find the collection with the name and the user's id + const findCollection = await prisma.collection.findFirst({ + where: { + name: link.collection.name, + ownerId: userId, + parentId: link.collection.parentId, + }, + }); + + if (findCollection) { + const collectionIsAccessible = await getPermission({ + userId, + collectionId: findCollection.id, + }); + + const memberHasAccess = collectionIsAccessible?.members.some( + (e: UsersAndCollections) => e.userId === userId && e.canCreate + ); + + if (!(collectionIsAccessible?.ownerId === userId || memberHasAccess)) + return { response: "Collection is not accessible.", status: 401 }; + + link.collection.id = findCollection.id; + } else { + const collection = await prisma.collection.create({ + data: { + name: link.collection.name, + ownerId: userId, + }, + }); + + link.collection.id = collection.id; + } + } else if (link.collection.id) { + const collectionIsAccessible = await getPermission({ + userId, + collectionId: link.collection.id, + }); + + const memberHasAccess = collectionIsAccessible?.members.some( + (e: UsersAndCollections) => e.userId === userId && e.canCreate + ); + + if (!(collectionIsAccessible?.ownerId === userId || memberHasAccess)) + return { response: "Collection is not accessible.", status: 401 }; + } else if (!link.collection.id) { link.collection.name = "Unorganized"; + link.collection.parentId = null; + + // find the collection with the name "Unorganized" and the user's id + const unorganizedCollection = await prisma.collection.findFirst({ + where: { + name: "Unorganized", + ownerId: userId, + }, + }); + + link.collection.id = unorganizedCollection?.id; + } else { + return { response: "Uncaught error.", status: 500 }; } const numberOfLinksTheUserHas = await prisma.link.count({ @@ -42,22 +103,6 @@ export default async function postLink( link.collection.name = link.collection.name.trim(); - if (link.collection.id) { - const collectionIsAccessible = await getPermission({ - userId, - collectionId: link.collection.id, - }); - - const memberHasAccess = collectionIsAccessible?.members.some( - (e: UsersAndCollections) => e.userId === userId && e.canCreate - ); - - if (!(collectionIsAccessible?.ownerId === userId || memberHasAccess)) - return { response: "Collection is not accessible.", status: 401 }; - } else { - link.collection.ownerId = userId; - } - const description = link.description && link.description !== "" ? link.description @@ -86,14 +131,8 @@ export default async function postLink( description, type: linkType, collection: { - connectOrCreate: { - where: { - id: link.collection.id ?? 0, - }, - create: { - name: link.collection.name.trim(), - ownerId: userId - }, + connect: { + id: link.collection.id, }, }, tags: { diff --git a/prisma/schema.prisma b/prisma/schema.prisma index f081fc5..4d9ccec 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -163,4 +163,3 @@ model AccessToken { createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt } -