From 737ffca4bc897fbb3a1b337e2bb275d8f886cdb5 Mon Sep 17 00:00:00 2001 From: Izan Gil <66965250+SrIzan10@users.noreply.github.com> Date: Sun, 22 Dec 2024 23:43:27 +0100 Subject: [PATCH] feat: initial collaboration implementation --- package.json | 1 + .../migration.sql | 24 +++++++ .../migration.sql | 40 +++++++++++ .../migration.sql | 5 ++ prisma/migrations/migration_lock.toml | 2 +- prisma/schema.prisma | 36 +++++++--- src/app/(protected)/dashboard/page.tsx | 19 +++-- src/app/(protected)/join/action.ts | 36 ++++++++++ src/app/(protected)/join/page.tsx | 61 ++++++++++++++++ src/app/(protected)/project/[id]/page.tsx | 8 +-- .../project/[id]/settings/page.tsx | 8 ++- .../app/InviteCodeViewer/InviteCodeViewer.tsx | 24 +++++++ src/components/app/NavBar/NavBar.tsx | 1 + .../app/ProjectSettings/ProjectSettings.tsx | 47 +++++++++++- .../app/ProjectTeamUsers/ProjectTeamUsers.tsx | 71 +++++++++++++++++++ src/components/app/ProjectTeamUsers/delete.ts | 24 +++++++ src/components/ui/input-otp.tsx | 71 +++++++++++++++++++ src/lib/auth/index.ts | 2 +- src/lib/forms/actions.ts | 29 ++++++-- tailwind.config.ts | 5 ++ yarn.lock | 5 ++ 21 files changed, 489 insertions(+), 30 deletions(-) create mode 100644 prisma/migrations/20241222102322_add_multiple_users/migration.sql create mode 100644 prisma/migrations/20241222201815_add_project_invite_code/migration.sql create mode 100644 prisma/migrations/20241222223410_user_is_owner/migration.sql create mode 100644 src/app/(protected)/join/action.ts create mode 100644 src/app/(protected)/join/page.tsx create mode 100644 src/components/app/InviteCodeViewer/InviteCodeViewer.tsx create mode 100644 src/components/app/ProjectTeamUsers/ProjectTeamUsers.tsx create mode 100644 src/components/app/ProjectTeamUsers/delete.ts create mode 100644 src/components/ui/input-otp.tsx diff --git a/package.json b/package.json index 2335834..2eb9952 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.0", "cmdk": "1.0.0", + "input-otp": "^1.4.1", "ioredis": "^5.4.1", "lucia": "^3.1.1", "lucide-react": "^0.368.0", diff --git a/prisma/migrations/20241222102322_add_multiple_users/migration.sql b/prisma/migrations/20241222102322_add_multiple_users/migration.sql new file mode 100644 index 0000000..c145698 --- /dev/null +++ b/prisma/migrations/20241222102322_add_multiple_users/migration.sql @@ -0,0 +1,24 @@ +-- thanks ai + +-- Step 1: Create the new UserProject table +CREATE TABLE "UserProject" ( + "userId" TEXT NOT NULL, + "projectId" TEXT NOT NULL, + + CONSTRAINT "UserProject_pkey" PRIMARY KEY ("userId","projectId") +); + +-- Step 2: Insert existing userId and projectId pairs into the UserProject table +INSERT INTO "UserProject" ("userId", "projectId") +SELECT "userId", "id" FROM "Project" WHERE "userId" IS NOT NULL; + +-- Step 3: Drop the foreign key constraint and the userId column from the Project table +ALTER TABLE "Project" DROP CONSTRAINT "Project_userId_fkey"; +ALTER TABLE "Project" DROP COLUMN "userId"; + +-- Step 4: Add foreign key constraints to the UserProject table +ALTER TABLE "UserProject" ADD CONSTRAINT "UserProject_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; +ALTER TABLE "UserProject" ADD CONSTRAINT "UserProject_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- Step 5: Drop the unnecessary _UserProjects table and its constraints +DROP TABLE IF EXISTS "_UserProjects"; \ No newline at end of file diff --git a/prisma/migrations/20241222201815_add_project_invite_code/migration.sql b/prisma/migrations/20241222201815_add_project_invite_code/migration.sql new file mode 100644 index 0000000..8e188c2 --- /dev/null +++ b/prisma/migrations/20241222201815_add_project_invite_code/migration.sql @@ -0,0 +1,40 @@ +/* + Warnings: + + - A unique constraint covering the columns `[inviteCode]` on the table `Project` will be added. If there are existing duplicate values, this will fail. + +*/ +-- AlterTable +ALTER TABLE "Project" ADD COLUMN "inviteCode" TEXT NOT NULL DEFAULT floor(random() * 90000000 + 10000000)::text; + +-- CreateTable +CREATE TABLE "_UserProjects" ( + "A" TEXT NOT NULL, + "B" TEXT NOT NULL, + + CONSTRAINT "_UserProjects_AB_pkey" PRIMARY KEY ("A","B") +); + +-- CreateIndex +CREATE INDEX "_UserProjects_B_index" ON "_UserProjects"("B"); + +-- CreateIndex +CREATE INDEX "Feedback_projectId_idx" ON "Feedback"("projectId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Project_inviteCode_key" ON "Project"("inviteCode"); + +-- CreateIndex +CREATE INDEX "Session_userId_idx" ON "Session"("userId"); + +-- CreateIndex +CREATE INDEX "UserProject_userId_idx" ON "UserProject"("userId"); + +-- CreateIndex +CREATE INDEX "UserProject_projectId_idx" ON "UserProject"("projectId"); + +-- AddForeignKey +ALTER TABLE "_UserProjects" ADD CONSTRAINT "_UserProjects_A_fkey" FOREIGN KEY ("A") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_UserProjects" ADD CONSTRAINT "_UserProjects_B_fkey" FOREIGN KEY ("B") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20241222223410_user_is_owner/migration.sql b/prisma/migrations/20241222223410_user_is_owner/migration.sql new file mode 100644 index 0000000..be282fd --- /dev/null +++ b/prisma/migrations/20241222223410_user_is_owner/migration.sql @@ -0,0 +1,5 @@ +-- AlterTable +ALTER TABLE "Project" ALTER COLUMN "inviteCode" SET DEFAULT floor(random() * 90000000 + 10000000)::text; + +-- AlterTable +ALTER TABLE "UserProject" ADD COLUMN "isOwner" BOOLEAN NOT NULL DEFAULT false; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml index fbffa92..648c57f 100644 --- a/prisma/migrations/migration_lock.toml +++ b/prisma/migrations/migration_lock.toml @@ -1,3 +1,3 @@ # Please do not edit this file manually -# It should be added in your version-control system (i.e. Git) +# It should be added in your version-control system (e.g., Git) provider = "postgresql" \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 786a5d0..2aa17ac 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -14,12 +14,13 @@ datasource db { } model User { - id String @id @default(cuid()) - githubId String @unique + id String @id @default(cuid()) + githubId String @unique username String installations String[] - projects Project[] + projects Project[] @relation("UserProjects") sessions Session[] + UserProject UserProject[] } model Session { @@ -27,6 +28,8 @@ model Session { userId String expiresAt DateTime user User @relation(references: [id], fields: [userId], onDelete: Cascade) + + @@index([userId]) } model Project { @@ -37,17 +40,32 @@ model Project { customData String[] rateLimitReq Int @default(5) rateLimitTime Int @default(60) + // 8 digit random number + inviteCode String @unique @default(dbgenerated("floor(random() * 90000000 + 10000000)::text")) - userId String - user User @relation(fields: [userId], references: [id]) - feedback Feedback[] + users User[] @relation("UserProjects") + feedback Feedback[] + UserProject UserProject[] +} + +model UserProject { + userId String + projectId String + isOwner Boolean @default(false) + user User @relation(fields: [userId], references: [id]) + project Project @relation(fields: [projectId], references: [id]) + + @@id([userId, projectId]) + @@index([userId]) + @@index([projectId]) } model Feedback { - id Int @id @default(autoincrement()) + id Int @id @default(autoincrement()) message String customData String + projectId String + project Project @relation(fields: [projectId], references: [id]) - projectId String - project Project @relation(fields: [projectId], references: [id]) + @@index([projectId]) } diff --git a/src/app/(protected)/dashboard/page.tsx b/src/app/(protected)/dashboard/page.tsx index 36c2ab0..9bbeead 100644 --- a/src/app/(protected)/dashboard/page.tsx +++ b/src/app/(protected)/dashboard/page.tsx @@ -8,7 +8,11 @@ export default async function Page() { const { user } = await validateRequest(); const db = await prisma.project.findMany({ where: { - userId: user!.id, + UserProject: { + some: { + userId: user!.id, + }, + }, }, }); if (db.length === 0) { @@ -16,9 +20,16 @@ export default async function Page() {

No projects found

Create a project to get started

- - - +
+ + + + + + +
); } diff --git a/src/app/(protected)/join/action.ts b/src/app/(protected)/join/action.ts new file mode 100644 index 0000000..f4ec184 --- /dev/null +++ b/src/app/(protected)/join/action.ts @@ -0,0 +1,36 @@ +'use server'; + +import { validateRequest } from "@/lib/auth"; +import prisma from "@/lib/db"; + +export async function join(code: string) { + const { user } = await validateRequest(); + if (!user) { + return { success: false, message: "Unauthorized" }; + } + const numCode = parseInt(code); + if (isNaN(numCode) || code.length !== 8) { + return { success: false, message: "Invalid code" }; + } + + const project = await prisma.project.findFirst({ + where: { inviteCode: code }, + include: { + UserProject: { + where: { userId: user.id }, + } + } + }); + if (!project) { + return { success: false, message: "Project not found" }; + } + if (project.UserProject.length > 0) { + return { success: false, message: "Already joined project" }; + } + + await prisma.project.update({ + where: { id: project.id }, + data: { UserProject: { create: { userId: user.id } } }, + }); + return { success: true, message: "Joined project", id: project.id }; +} \ No newline at end of file diff --git a/src/app/(protected)/join/page.tsx b/src/app/(protected)/join/page.tsx new file mode 100644 index 0000000..69a7142 --- /dev/null +++ b/src/app/(protected)/join/page.tsx @@ -0,0 +1,61 @@ +'use client'; +import { Button } from '@/components/ui/button'; +import { InputOTP, InputOTPGroup, InputOTPSlot } from '@/components/ui/input-otp'; +import { REGEXP_ONLY_DIGITS } from 'input-otp'; +import React from 'react'; +import { join } from './action'; +import { toast } from 'sonner'; +import { useRouter } from 'next/navigation'; + +export default function Page() { + const router = useRouter(); + const [value, setValue] = React.useState(''); + const [loading, setLoading] = React.useState(false); + + return ( +
+

Join another user's project!

+ setValue(val)} + value={value} + disabled={loading} + > + + {/* THIS DOESNT WORK: + {Array.from({ length: 8 }).map((_, i) => ( + + ))} + SO HERE'S BAD CODE YOU ARE WELCOME + */} + + + + + + + + + + + +
+ ); +} diff --git a/src/app/(protected)/project/[id]/page.tsx b/src/app/(protected)/project/[id]/page.tsx index e4979cc..661626f 100644 --- a/src/app/(protected)/project/[id]/page.tsx +++ b/src/app/(protected)/project/[id]/page.tsx @@ -27,7 +27,7 @@ export default async function Page({ params }: { params: Promise<{ id: string }> const { id } = await params; const { user } = await validateRequest(); const project = await prisma.project.findFirst({ - where: { id, userId: user!.id }, + where: { id, UserProject: { some: { userId: user!.id } } }, include: { feedback: true }, }); @@ -97,11 +97,9 @@ export default async function Page({ params }: { params: Promise<{ id: string }> {Object.entries(JSON.parse(feedback.customData)).map(([key, value]) => ( {value as string} ))} - + - {project.github && ( - - )} + {project.github && } ))} diff --git a/src/app/(protected)/project/[id]/settings/page.tsx b/src/app/(protected)/project/[id]/settings/page.tsx index 537af81..fa6d7c6 100644 --- a/src/app/(protected)/project/[id]/settings/page.tsx +++ b/src/app/(protected)/project/[id]/settings/page.tsx @@ -6,7 +6,13 @@ export default async function Page({ params }: { params: Promise<{ id: string }> const { id } = await params; const { user } = await validateRequest(); const project = await prisma.project.findFirst({ - where: { id, userId: user!.id }, + where: { id, UserProject: { some: { userId: user!.id } } }, + include: { + UserProject: { + where: { projectId: id }, + include: { user: true }, + }, + }, }); if (!project) { return

Project not found

; diff --git a/src/components/app/InviteCodeViewer/InviteCodeViewer.tsx b/src/components/app/InviteCodeViewer/InviteCodeViewer.tsx new file mode 100644 index 0000000..c898382 --- /dev/null +++ b/src/components/app/InviteCodeViewer/InviteCodeViewer.tsx @@ -0,0 +1,24 @@ +'use client'; + +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { useState } from 'react'; + +export default function InviteCodeViewer({ code }: { code: string }) { + const [hover, setHover] = useState(false); + return ( +
+ + {}} + onMouseOver={() => setHover(true)} + onMouseOut={() => setHover(false)} + value={code} + /> +
+ ); +} diff --git a/src/components/app/NavBar/NavBar.tsx b/src/components/app/NavBar/NavBar.tsx index 26c1f88..34a896d 100644 --- a/src/components/app/NavBar/NavBar.tsx +++ b/src/components/app/NavBar/NavBar.tsx @@ -18,6 +18,7 @@ import { ThemeSwitcher } from "../ThemeSwitcher/ThemeSwitcher" export const links = [ { href: '/dashboard', name: 'Dashboard' }, { href: '/create', name: 'Create' }, + { href: '/join', name: 'Join' }, ] function NavbarLinks() { diff --git a/src/components/app/ProjectSettings/ProjectSettings.tsx b/src/components/app/ProjectSettings/ProjectSettings.tsx index ecf7960..7519129 100644 --- a/src/components/app/ProjectSettings/ProjectSettings.tsx +++ b/src/components/app/ProjectSettings/ProjectSettings.tsx @@ -2,7 +2,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; -import type { Project } from '@prisma/client'; +import type { Project, User, UserProject } from '@prisma/client'; import { UniversalForm } from '../UniversalForm/UniversalForm'; import { customData, @@ -23,8 +23,10 @@ import { import GithubRepoChooser from '../GithubRepoChooser/GithubRepoChooser'; import React from 'react'; import Link from 'next/link'; +import InviteCodeViewer from '../InviteCodeViewer/InviteCodeViewer'; +import ProjectTeamUsers from '../ProjectTeamUsers/ProjectTeamUsers'; -export default function ProjectSettings(project: Project) { +export default function ProjectSettings(project: ProjectWithUsers) { const [ghRepo, setGhRepo] = React.useState(''); const [hasSubmitted, setHasSubmitted] = React.useState(false); const apiUrl = `https://${window.location.hostname}/api/feedback/${project.id}`; @@ -121,6 +123,41 @@ export default function ProjectSettings(project: Project) { /> + + + Your team + Invite people to join the project! + + + + + + + {/* + + Project Deletion + Permanently delete your project + + +

+ This action is irreversible. All feedback and settings will be lost. +

+ +
+
*/} @@ -297,3 +334,9 @@ function stripIndents(strings: TemplateStringsArray, ...values: any[]) { .join('\n') .trim(); } + +interface ProjectWithUsers extends Project { + UserProject: (UserProject & { + user: User; + })[]; +} \ No newline at end of file diff --git a/src/components/app/ProjectTeamUsers/ProjectTeamUsers.tsx b/src/components/app/ProjectTeamUsers/ProjectTeamUsers.tsx new file mode 100644 index 0000000..8c02a00 --- /dev/null +++ b/src/components/app/ProjectTeamUsers/ProjectTeamUsers.tsx @@ -0,0 +1,71 @@ +'use client'; + +import { useState } from 'react'; +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; +import { Button } from '@/components/ui/button'; +import { Trash2 } from 'lucide-react'; +import type { User, UserProject } from '@prisma/client'; +import { useSession } from '@/lib/providers/SessionProvider'; +import { deleteProjectTeamUser } from './delete'; +import { toast } from 'sonner'; + +export default function ProjectTeamUsers(userProject: ProjectTeamUsersProps) { + const [users, setUsers] = useState< + (UserProject & { + user: User; + })[] + >(userProject.UserProject); + const [loading, isLoading] = useState([]); + const { user: currentUser } = useSession(); + + const handleDelete = (userId: string) => { + isLoading([...loading, userId]); + deleteProjectTeamUser(userId, userProject.UserProject[0].projectId).then((res) => { + if (res.success) { + toast.success(res.message); + setUsers(users.filter((user) => user.userId !== userId)); + } else { + toast.error(res.message); + } + isLoading(loading.filter((id) => id !== userId)); + }); + }; + + return ( + + ); +} + +interface ProjectTeamUsersProps { + UserProject: (UserProject & { + user: User; + })[]; +} diff --git a/src/components/app/ProjectTeamUsers/delete.ts b/src/components/app/ProjectTeamUsers/delete.ts new file mode 100644 index 0000000..e448c14 --- /dev/null +++ b/src/components/app/ProjectTeamUsers/delete.ts @@ -0,0 +1,24 @@ +'use server'; + +import { validateRequest } from "@/lib/auth"; +import prisma from "@/lib/db"; + +export async function deleteProjectTeamUser(userId: string, projectId: string) { + const { user } = await validateRequest(); + if (!user) { + return { success: false, message: 'Unauthorized' }; + } + + const userProject = await prisma.userProject.findFirst({ + where: { userId, projectId }, + include: { user: true }, + }); + if (!userProject) { + return { success: false, message: 'User not found' }; + } + + await prisma.userProject.deleteMany({ + where: { userId, projectId }, + }); + return { success: true, message: `User ${userProject.user.username} removed.` }; +} \ No newline at end of file diff --git a/src/components/ui/input-otp.tsx b/src/components/ui/input-otp.tsx new file mode 100644 index 0000000..f66fcfa --- /dev/null +++ b/src/components/ui/input-otp.tsx @@ -0,0 +1,71 @@ +"use client" + +import * as React from "react" +import { OTPInput, OTPInputContext } from "input-otp" +import { Dot } from "lucide-react" + +import { cn } from "@/lib/utils" + +const InputOTP = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, containerClassName, ...props }, ref) => ( + +)) +InputOTP.displayName = "InputOTP" + +const InputOTPGroup = React.forwardRef< + React.ElementRef<"div">, + React.ComponentPropsWithoutRef<"div"> +>(({ className, ...props }, ref) => ( +
+)) +InputOTPGroup.displayName = "InputOTPGroup" + +const InputOTPSlot = React.forwardRef< + React.ElementRef<"div">, + React.ComponentPropsWithoutRef<"div"> & { index: number } +>(({ index, className, ...props }, ref) => { + const inputOTPContext = React.useContext(OTPInputContext) + const { char, hasFakeCaret, isActive } = inputOTPContext.slots[index] + + return ( +
+ {char} + {hasFakeCaret && ( +
+
+
+ )} +
+ ) +}) +InputOTPSlot.displayName = "InputOTPSlot" + +const InputOTPSeparator = React.forwardRef< + React.ElementRef<"div">, + React.ComponentPropsWithoutRef<"div"> +>(({ ...props }, ref) => ( +
+ +
+)) +InputOTPSeparator.displayName = "InputOTPSeparator" + +export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator } diff --git a/src/lib/auth/index.ts b/src/lib/auth/index.ts index 55f01ec..86864fe 100644 --- a/src/lib/auth/index.ts +++ b/src/lib/auth/index.ts @@ -1,5 +1,5 @@ import { PrismaAdapter } from '@lucia-auth/adapter-prisma'; -import { Lucia, Session, User } from 'lucia'; +import { Lucia } from 'lucia'; import prisma from '../db'; import { cache } from 'react'; import { cookies } from 'next/headers'; diff --git a/src/lib/forms/actions.ts b/src/lib/forms/actions.ts index 5564043..0b669da 100644 --- a/src/lib/forms/actions.ts +++ b/src/lib/forms/actions.ts @@ -27,9 +27,10 @@ export async function create(prev: any, formData: FormData) { const dbCreate = await prisma.project.create({ data: { ...zod.data, - user: { - connect: { - id: user.id, + UserProject: { + create: { + userId: user.id, + isOwner: true, }, }, }, @@ -141,7 +142,14 @@ export async function githubTestIssue(prev: any, formData: FormData) { id: zod.data.id, }, include: { - user: true, + UserProject: { + where: { + userId: user.id, + }, + include: { + user: true, + }, + } }, }); if (!project) { @@ -152,7 +160,7 @@ export async function githubTestIssue(prev: any, formData: FormData) { const [owner, repo] = project.github!.split('/').slice(-2); let issueCreated = false; - for (const installationId of project.user.installations) { + for (const installationId of project.UserProject[0].user.installations) { if (issueCreated) break; const installation = await octokitApp.getInstallationOctokit(Number(installationId)); @@ -199,7 +207,14 @@ export async function githubCreateIssue(prev: any, formData: FormData) { id: zod.data.project, }, include: { - user: true, + UserProject: { + where: { + userId: user.id, + }, + include: { + user: true, + } + }, }, }); if (!project) { @@ -209,7 +224,7 @@ export async function githubCreateIssue(prev: any, formData: FormData) { try { const [owner, repo] = project.github!.split('/').slice(-2); - for (const installationId of project.user.installations) { + for (const installationId of project.UserProject[0].user.installations) { const installation = await octokitApp.getInstallationOctokit(Number(installationId)); const getRepo = await installation .request('GET /repos/{owner}/{repo}', { diff --git a/tailwind.config.ts b/tailwind.config.ts index 84287e8..73bd5d9 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -67,10 +67,15 @@ const config = { from: { height: "var(--radix-accordion-content-height)" }, to: { height: "0" }, }, + "caret-blink": { + "0%,70%,100%": { opacity: "1" }, + "20%,50%": { opacity: "0" }, + }, }, animation: { "accordion-down": "accordion-down 0.2s ease-out", "accordion-up": "accordion-up 0.2s ease-out", + "caret-blink": "caret-blink 1.25s ease-out infinite", }, }, }, diff --git a/yarn.lock b/yarn.lock index 455daa9..8cb32fe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3579,6 +3579,11 @@ inherits@2, inherits@^2.0.3, inherits@^2.0.4: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== +input-otp@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/input-otp/-/input-otp-1.4.1.tgz#bc22e68b14b1667219d54adf74243e37ea79cf84" + integrity sha512-+yvpmKYKHi9jIGngxagY9oWiiblPB7+nEO75F2l2o4vs+6vpPZZmUl4tBNYuTCvQjhvEIbdNeJu70bhfYP2nbw== + internal-slot@^1.0.7: version "1.0.7" resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.7.tgz#c06dcca3ed874249881007b0a5523b172a190802"