diff --git a/components/ImageUploader.tsx b/components/ImageUploader.tsx index 6b3c4301..43a2188e 100644 --- a/components/ImageUploader.tsx +++ b/components/ImageUploader.tsx @@ -142,7 +142,7 @@ export default function ImageUploader({ target, id, buttonMsg, handleAvatarChang
diff --git a/components/team/EditTeam.tsx b/components/team/EditTeam.tsx new file mode 100644 index 00000000..78777682 --- /dev/null +++ b/components/team/EditTeam.tsx @@ -0,0 +1,298 @@ +import React, { useEffect, useState, useRef } from "react"; +import { ArrowLeftIcon, PlusIcon, TrashIcon } from "@heroicons/react/outline"; +import ErrorAlert from "@components/ui/alerts/Error"; +import { UsernameInput } from "@components/ui/UsernameInput"; +import MemberList from "./MemberList"; +import Avatar from "@components/Avatar"; +import ImageUploader from "@components/ImageUploader"; +import { Dialog, DialogTrigger } from "@components/Dialog"; +import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent"; +import Modal from "@components/Modal"; +import MemberInvitationModal from "@components/team/MemberInvitationModal"; +import Button from "@components/ui/Button"; +import { Member } from "@lib/member"; +import { Team } from "@lib/team"; + +export default function EditTeam(props: { team: Team | undefined | null; onCloseEdit: () => void }) { + const [members, setMembers] = useState([]); + + const nameRef = useRef() as React.MutableRefObject; + const teamUrlRef = useRef() as React.MutableRefObject; + const descriptionRef = useRef() as React.MutableRefObject; + const hideBrandingRef = useRef() as React.MutableRefObject; + const logoRef = useRef() as React.MutableRefObject; + const [hasErrors, setHasErrors] = useState(false); + const [successModalOpen, setSuccessModalOpen] = useState(false); + const [showMemberInvitationModal, setShowMemberInvitationModal] = useState(false); + const [inviteModalTeam, setInviteModalTeam] = useState(); + const [errorMessage, setErrorMessage] = useState(""); + const [imageSrc, setImageSrc] = useState(""); + + const loadMembers = () => + fetch("/api/teams/" + props.team?.id + "/membership") + .then((res) => res.json()) + .then((data) => setMembers(data.members)); + + useEffect(() => { + loadMembers(); + }, []); + + const deleteTeam = () => { + return fetch("/api/teams/" + props.team?.id, { + method: "DELETE", + }).then(props.onCloseEdit()); + }; + + const onRemoveMember = (member: Member) => { + return fetch("/api/teams/" + props.team?.id + "/membership", { + method: "DELETE", + body: JSON.stringify({ userId: member.id }), + headers: { + "Content-Type": "application/json", + }, + }).then(loadMembers); + }; + + const onInviteMember = (team: Team | null | undefined) => { + setShowMemberInvitationModal(true); + setInviteModalTeam(team); + }; + + const handleError = async (resp: Response) => { + if (!resp.ok) { + const error = await resp.json(); + throw new Error(error.message); + } + }; + + async function updateTeamHandler(event) { + event.preventDefault(); + + const enteredUsername = teamUrlRef?.current?.value.toLowerCase(); + const enteredName = nameRef?.current?.value; + const enteredDescription = descriptionRef?.current?.value; + const enteredLogo = logoRef?.current?.value; + const enteredHideBranding = hideBrandingRef?.current?.checked; + + // TODO: Add validation + + await fetch("/api/teams/" + props.team?.id + "/profile", { + method: "PATCH", + body: JSON.stringify({ + username: enteredUsername, + name: enteredName, + description: enteredDescription, + logo: enteredLogo, + hideBranding: enteredHideBranding, + }), + headers: { + "Content-Type": "application/json", + }, + }) + .then(handleError) + .then(() => { + setSuccessModalOpen(true); + setHasErrors(false); // dismiss any open errors + }) + .catch((err) => { + setHasErrors(true); + setErrorMessage(err.message); + }); + } + + const onMemberInvitationModalExit = () => { + loadMembers(); + setShowMemberInvitationModal(false); + }; + + const closeSuccessModal = () => { + setSuccessModalOpen(false); + }; + + const handleLogoChange = (newLogo: string) => { + logoRef.current.value = newLogo; + const nativeInputValueSetter = Object.getOwnPropertyDescriptor(HTMLInputElement?.prototype, "value").set; + nativeInputValueSetter?.call(logoRef.current, newLogo); + const ev2 = new Event("input", { bubbles: true }); + logoRef?.current?.dispatchEvent(ev2); + updateTeamHandler(ev2); + setImageSrc(newLogo); + }; + + return ( +
+
+
+ +
+
+
+

{props.team?.name}

+
+

Manage your team

+
+
+
+
+

Profile

+
+ {hasErrors && } +
+
+
+
+
+ +
+
+ + +
+
+
+ +
+ +

+ A few sentences about your team. This will appear on your team's URL page. +

+
+
+
+
+ + + +
+
+
+
+

Members

+
+ +
+
+
+ {!!members.length && ( + + )} +
+
+
+
+
+ +
+
+ +

Hide all Calendso branding from your public pages.

+
+
+
+
+

Danger Zone

+
+
+ + { + e.stopPropagation(); + }} + className="btn-sm btn-white"> + + Disband Team + + deleteTeam()}> + Are you sure you want to disband this team? Anyone who you've shared this team + link with will no longer be able to book using it. + + +
+
+
+
+
+
+ +
+
+ + + {showMemberInvitationModal && ( + + )} +
+
+ ); +} diff --git a/components/team/EditTeamModal.tsx b/components/team/EditTeamModal.tsx deleted file mode 100644 index 226b63ab..00000000 --- a/components/team/EditTeamModal.tsx +++ /dev/null @@ -1,131 +0,0 @@ -import { useEffect, useState } from "react"; -import { UserRemoveIcon, UsersIcon } from "@heroicons/react/outline"; -import { useSession } from "next-auth/client"; - -export default function EditTeamModal(props) { - const [session] = useSession(); - const [members, setMembers] = useState([]); - const [checkedDisbandTeam, setCheckedDisbandTeam] = useState(false); - - const loadMembers = () => - fetch("/api/teams/" + props.team.id + "/membership") - .then((res: any) => res.json()) - .then((data) => setMembers(data.members)); - - useEffect(() => { - loadMembers(); - }, []); - - const deleteTeam = (e) => { - e.preventDefault(); - return fetch("/api/teams/" + props.team.id, { - method: "DELETE", - }).then(props.onExit); - }; - - const removeMember = (member) => { - return fetch("/api/teams/" + props.team.id + "/membership", { - method: "DELETE", - body: JSON.stringify({ userId: member.id }), - headers: { - "Content-Type": "application/json", - }, - }).then(loadMembers); - }; - - return ( -
-
- - - - -
-
-
- -
-
- -
-

Manage and delete your team.

-
-
-
-
-
-
- {members.length > 0 && ( -
-
-

Members

-
- - - {members.map((member) => ( - - - - - - ))} - -
- {member.name} {member.name && "(" + member.email + ")"} - {!member.name && member.email} - {member.role.toLowerCase()} - {member.email !== session.user.email && ( - - )} -
-
- )} -
-
-

Tick the box to disband this team.

- -
-
-
- {/*!checkedDisbandTeam && */} - {checkedDisbandTeam && ( - - )} - -
-
-
-
-
- ); -} diff --git a/components/team/MemberInvitationModal.tsx b/components/team/MemberInvitationModal.tsx index fe2213d2..6f5ebfe9 100644 --- a/components/team/MemberInvitationModal.tsx +++ b/components/team/MemberInvitationModal.tsx @@ -1,10 +1,12 @@ import { UsersIcon } from "@heroicons/react/outline"; import { useState } from "react"; +import Button from "@components/ui/Button"; +import { Team } from "@lib/team"; -export default function MemberInvitationModal(props) { +export default function MemberInvitationModal(props: { team: Team | undefined | null; onExit: () => void }) { const [errorMessage, setErrorMessage] = useState(""); - const handleError = async (res) => { + const handleError = async (res: Response) => { const responseData = await res.json(); if (res.ok === false) { @@ -24,7 +26,7 @@ export default function MemberInvitationModal(props) { sendEmailInvitation: e.target.elements["sendInviteEmail"].checked, }; - return fetch("/api/teams/" + props.team.id + "/invite", { + return fetch("/api/teams/" + props?.team?.id + "/invite", { method: "POST", body: JSON.stringify(payload), headers: { @@ -40,26 +42,26 @@ export default function MemberInvitationModal(props) { return (
-
+
-
-
-
- +
+
+
+
-
@@ -79,16 +81,16 @@ export default function MemberInvitationModal(props) { id="inviteUser" placeholder="email@example.com" required - className="mt-1 block w-full border border-gray-300 rounded-md shadow-sm py-2 px-3 focus:outline-none focus:ring-black focus:border-black sm:text-sm" + className="block w-full px-3 py-2 mt-1 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-black focus:border-black sm:text-sm" />
-
@@ -111,18 +113,18 @@ export default function MemberInvitationModal(props) {
{errorMessage && ( -

+

Error: {errorMessage}

)}
- - + +
diff --git a/components/team/MemberList.tsx b/components/team/MemberList.tsx new file mode 100644 index 00000000..89536a28 --- /dev/null +++ b/components/team/MemberList.tsx @@ -0,0 +1,31 @@ +import MemberListItem from "./MemberListItem"; +import { Member } from "@lib/member"; + +export default function MemberList(props: { + members: Member[]; + onRemoveMember: (text: Member) => void; + onChange: (text: string) => void; +}) { + const selectAction = (action: string, member: Member) => { + switch (action) { + case "remove": + props.onRemoveMember(member); + break; + } + }; + + return ( +
+
    + {props.members.map((member) => ( + selectAction(action, member)} + /> + ))} +
+
+ ); +} diff --git a/components/team/MemberListItem.tsx b/components/team/MemberListItem.tsx new file mode 100644 index 00000000..0502b969 --- /dev/null +++ b/components/team/MemberListItem.tsx @@ -0,0 +1,94 @@ +import { DotsHorizontalIcon, UserRemoveIcon } from "@heroicons/react/outline"; +import Dropdown from "../ui/Dropdown"; +import { useState } from "react"; +import { Dialog, DialogTrigger } from "@components/Dialog"; +import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent"; +import Avatar from "@components/Avatar"; +import { Member } from "@lib/member"; +import Button from "@components/ui/Button"; + +export default function MemberListItem(props: { + member: Member; + onActionSelect: (text: string) => void; + onChange: (text: string) => void; +}) { + const [member] = useState(props.member); + + return ( + member && ( +
  • +
    +
    + +
    + {props.member.name} + {props.member.email} +
    +
    +
    + {props.member.role === "INVITEE" && ( + <> + + Pending + + + Member + + + )} + {props.member.role === "MEMBER" && ( + + Member + + )} + {props.member.role === "OWNER" && ( + + Owner + + )} + + +
      +
    • + + { + e.stopPropagation(); + }} + color="warn" + StartIcon={UserRemoveIcon} + className="w-full"> + Remove User + + props.onActionSelect("remove")}> + Are you sure you want to remove this member from the team? + + +
    • +
    +
    +
    +
    +
  • + ) + ); +} diff --git a/components/team/TeamList.tsx b/components/team/TeamList.tsx index abeaf733..b2e258a9 100644 --- a/components/team/TeamList.tsx +++ b/components/team/TeamList.tsx @@ -1,29 +1,32 @@ -import { useState } from "react"; import TeamListItem from "./TeamListItem"; -import EditTeamModal from "./EditTeamModal"; -import MemberInvitationModal from "./MemberInvitationModal"; +import { Team } from "@lib/team"; -export default function TeamList(props) { - const [showMemberInvitationModal, setShowMemberInvitationModal] = useState(false); - const [showEditTeamModal, setShowEditTeamModal] = useState(false); - const [team, setTeam] = useState(null); - - const selectAction = (action: string, team: any) => { - setTeam(team); +export default function TeamList(props: { + teams: Team[]; + onChange: () => void; + onEditTeam: (text: Team) => void; +}) { + const selectAction = (action: string, team: Team) => { switch (action) { case "edit": - setShowEditTeamModal(true); + props.onEditTeam(team); break; - case "invite": - setShowMemberInvitationModal(true); + case "disband": + deleteTeam(team); break; } }; + const deleteTeam = (team: Team) => { + return fetch("/api/teams/" + team.id, { + method: "DELETE", + }).then(props.onChange()); + }; + return (
    -
      - {props.teams.map((team: any) => ( +
        + {props.teams.map((team: Team) => ( selectAction(action, team)}> ))}
      - {showEditTeamModal && ( - { - props.onChange(); - setShowEditTeamModal(false); - }}> - )} - {showMemberInvitationModal && ( - setShowMemberInvitationModal(false)}> - )}
    ); } diff --git a/components/team/TeamListItem.tsx b/components/team/TeamListItem.tsx index ebfac5fe..4333b9bb 100644 --- a/components/team/TeamListItem.tsx +++ b/components/team/TeamListItem.tsx @@ -1,9 +1,38 @@ -import { CogIcon, TrashIcon, UsersIcon } from "@heroicons/react/outline"; +import { + TrashIcon, + DotsHorizontalIcon, + LinkIcon, + PencilAltIcon, + ExternalLinkIcon, +} from "@heroicons/react/outline"; import Dropdown from "../ui/Dropdown"; import { useState } from "react"; +import { Tooltip } from "@components/Tooltip"; +import Link from "next/link"; +import { Dialog, DialogTrigger } from "@components/Dialog"; +import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent"; +import Avatar from "@components/Avatar"; +import Button from "@components/ui/Button"; +import showToast from "@lib/notification"; -export default function TeamListItem(props) { - const [team, setTeam] = useState(props.team); +interface Team { + id: number; + name: string | null; + slug: string | null; + logo: string | null; + bio: string | null; + role: string | null; + hideBranding: boolean; + prevState: null; +} + +export default function TeamListItem(props: { + onChange: () => void; + key: number; + team: Team; + onActionSelect: (text: string) => void; +}) { + const [team, setTeam] = useState(props.team); const acceptInvite = () => invitationResponse(true); const declineInvite = () => invitationResponse(false); @@ -23,84 +52,117 @@ export default function TeamListItem(props) { return ( team && ( -
  • -
    -
    - -
    - {props.team.name} - - {props.team.role.toLowerCase()} +
  • +
    +
    + +
    + {props.team.name} + + {window.location.hostname}/{props.team.slug}
    {props.team.role === "INVITEE" && (
    - - + +
    )} {props.team.role === "MEMBER" && (
    - +
    )} {props.team.role === "OWNER" && ( -
    - - +
    + + Owner + + + + + +
      -
    • - + className="absolute right-0 z-10 origin-top-right bg-white rounded-sm shadow-lg top-10 w-44 ring-1 ring-black ring-opacity-5 focus:outline-none"> +
    • +
    • -
    • - +
    • + + + + + +
    • +
    • + + { + e.stopPropagation(); + }} + color="warn" + StartIcon={TrashIcon} + className="w-full"> + Disband Team + + props.onActionSelect("disband")}> + Are you sure you want to disband this team? Anyone who you've shared this team + link with will no longer be able to book using it. + +
    )}
    - {/*{props.team.userRole === 'Owner' && expanded &&
    - {props.team.members.length > 0 &&
    -

    Members

    - - - {props.team.members.map( (member) => - - - - )} - -
    Alex van Andel ({ member.email })Owner - -
    -
    } - - -
    }*/}
  • ) ); diff --git a/components/ui/Button.tsx b/components/ui/Button.tsx index 9c05316e..2ef72e9b 100644 --- a/components/ui/Button.tsx +++ b/components/ui/Button.tsx @@ -5,7 +5,7 @@ import React from "react"; type SVGComponent = React.FunctionComponent>; export type ButtonProps = { - color?: "primary" | "secondary" | "minimal"; + color?: "primary" | "secondary" | "minimal" | "warn"; size?: "base" | "sm" | "lg" | "fab"; loading?: boolean; disabled?: boolean; @@ -63,7 +63,10 @@ export const Button = function Button(props: ButtonProps) { (disabled ? "text-gray-400 bg-transparent" : "text-gray-700 bg-transparent hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-1 focus:bg-gray-100 focus:ring-neutral-500"), - + color === "warn" && + (disabled + ? "text-gray-400 bg-transparent" + : "text-red-700 bg-transparent hover:bg-red-100 focus:outline-none focus:ring-2 focus:ring-offset-1 focus:bg-red-50 focus:ring-red-500"), // set not-allowed cursor if disabled disabled && "cursor-not-allowed", props.className diff --git a/components/ui/UsernameInput.tsx b/components/ui/UsernameInput.tsx index 61aa858e..2b454e13 100644 --- a/components/ui/UsernameInput.tsx +++ b/components/ui/UsernameInput.tsx @@ -4,11 +4,11 @@ const UsernameInput = React.forwardRef((props, ref) => ( // todo, check if username is already taken here?
    -
    - - {typeof window !== "undefined" && window.location.hostname}/ +
    + + {typeof window !== "undefined" && window.location.hostname}/{props.label && "team/"} ( autoComplete="username" required {...props} - className="focus:ring-black focus:border-black flex-grow block w-full min-w-0 rounded-none rounded-r-sm sm:text-sm border-gray-300 lowercase" + className="flex-grow block w-full min-w-0 lowercase border-gray-300 rounded-none rounded-r-sm focus:ring-black focus:border-black sm:text-sm" />
    diff --git a/lib/member.ts b/lib/member.ts new file mode 100644 index 00000000..8b64c230 --- /dev/null +++ b/lib/member.ts @@ -0,0 +1,8 @@ +export interface Member { + id: number; + avatar: string | null; + role: string; + email: string; + name: string | null; + username: string | null; +} diff --git a/lib/team.ts b/lib/team.ts new file mode 100644 index 00000000..e6e290e0 --- /dev/null +++ b/lib/team.ts @@ -0,0 +1,10 @@ +export interface Team { + id: number; + name: string | null; + slug: string | null; + logo: string | null; + bio: string | null; + role: string | null; + hideBranding: boolean; + prevState: null; +} diff --git a/pages/api/teams/[team]/profile.ts b/pages/api/teams/[team]/profile.ts new file mode 100644 index 00000000..9084aef2 --- /dev/null +++ b/pages/api/teams/[team]/profile.ts @@ -0,0 +1,68 @@ +import type { NextApiRequest, NextApiResponse } from "next"; +import prisma from "@lib/prisma"; +import { getSession } from "next-auth/client"; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + const session = await getSession({ req: req }); + if (!session) { + return res.status(401).json({ message: "Not authenticated" }); + } + + const isTeamOwner = !!(await prisma.membership.findFirst({ + where: { + userId: session.user.id, + teamId: parseInt(req.query.team as string), + role: "OWNER", + }, + })); + + if (!isTeamOwner) { + res.status(403).json({ message: "You are not authorized to manage this team" }); + return; + } + + // PATCH /api/teams/profile/{team} + if (req.method === "PATCH") { + const team = await prisma.team.findFirst({ + where: { + id: parseInt(req.query.team as string), + }, + }); + + if (!team) { + return res.status(404).json({ message: "Invalid team" }); + } + + const username = req.body.username; + const userConflict = await prisma.team.findMany({ + where: { + slug: username, + }, + }); + const teamId = Number(req.query.team); + if (userConflict.some((team) => team.id !== teamId)) { + return res.status(409).json({ message: "Team username already taken" }); + } + + const name = req.body.name; + const slug = req.body.username; + const bio = req.body.description; + const logo = req.body.logo; + const hideBranding = req.body.hideBranding; + + await prisma.team.update({ + where: { + id: team.id, + }, + data: { + name, + slug, + logo, + bio, + hideBranding, + }, + }); + + return res.status(200).json({ message: "Team updated successfully" }); + } +} diff --git a/pages/settings/teams.tsx b/pages/settings/teams.tsx index ce807e7a..2847567e 100644 --- a/pages/settings/teams.tsx +++ b/pages/settings/teams.tsx @@ -1,7 +1,7 @@ import { GetServerSideProps } from "next"; import Shell from "@components/Shell"; import SettingsShell from "@components/Settings"; -import { useEffect, useState } from "react"; +import { useEffect, useState, useRef } from "react"; import type { Session } from "next-auth"; import { useSession } from "next-auth/client"; import { UsersIcon } from "@heroicons/react/outline"; @@ -9,14 +9,23 @@ import TeamList from "@components/team/TeamList"; import TeamListItem from "@components/team/TeamListItem"; import Loader from "@components/Loader"; import { getSession } from "@lib/auth"; +import EditTeam from "@components/team/EditTeam"; +import Button from "@components/ui/Button"; +import { Member } from "@lib/member"; +import { Team } from "@lib/team"; +import { PlusIcon } from "@heroicons/react/solid"; export default function Teams() { + const noop = () => undefined; const [, loading] = useSession(); const [teams, setTeams] = useState([]); const [invites, setInvites] = useState([]); const [showCreateTeamModal, setShowCreateTeamModal] = useState(false); + const [editTeamEnabled, setEditTeamEnabled] = useState(false); + const [teamToEdit, setTeamToEdit] = useState(); + const nameRef = useRef() as React.MutableRefObject; - const handleErrors = async (resp) => { + const handleErrors = async (resp: Response) => { if (!resp.ok) { const err = await resp.json(); throw new Error(err.message); @@ -28,8 +37,8 @@ export default function Teams() { fetch("/api/user/membership") .then(handleErrors) .then((data) => { - setTeams(data.membership.filter((m) => m.role !== "INVITEE")); - setInvites(data.membership.filter((m) => m.role === "INVITEE")); + setTeams(data.membership.filter((m: Member) => m.role !== "INVITEE")); + setInvites(data.membership.filter((m: Member) => m.role === "INVITEE")); }) .catch(console.log); }; @@ -42,12 +51,11 @@ export default function Teams() { return ; } - const createTeam = (e) => { + const createTeam = (e: React.FormEvent) => { e.preventDefault(); - return fetch("/api/teams", { method: "POST", - body: JSON.stringify({ name: e.target.elements["name"].value }), + body: JSON.stringify({ name: nameRef?.current?.value }), headers: { "Content-Type": "application/json", }, @@ -57,92 +65,93 @@ export default function Teams() { }); }; + const editTeam = (team: Team) => { + setEditTeamEnabled(true); + setTeamToEdit(team); + }; + + const onCloseEdit = () => { + loadData(); + setEditTeamEnabled(false); + }; + return ( -
    -
    -
    -
    - {!(invites.length || teams.length) && ( -
    -
    -

    - Create a team to get started -

    -
    -

    Create your first team and invite other users to work together with you.

    -
    -
    - + {!editTeamEnabled && ( +
    +
    +
    +
    + {!(invites.length || teams.length) && ( +
    +
    +

    + Create a team to get started +

    +
    +

    Create your first team and invite other users to work together with you.

    +
    + )} +
    +
    + +
    +
    +
    + {!!teams.length && ( + + )} + + {!!invites.length && ( +
    +

    Open Invitations

    +
      + {invites.map((team: Team) => ( + + ))} +
    )}
    - {!!(invites.length || teams.length) && ( -
    - -
    - )}
    -
    - {!!teams.length && } - - {!!invites.length && ( -
    -

    Open Invitations

    -
      - {invites.map((team) => ( - - ))} -
    -
    - )} -
    - {/*{teamsLoaded &&
    -
    -

    Transform account

    -

    - {membership.length !== 0 && "You cannot convert this account into a team until you leave all teams that you’re a member of."} - {membership.length === 0 && "A user account can be turned into a team, as a team ...."} -

    -
    -
    - -
    -
    }*/}
    -
    + )} + {!!editTeamEnabled && } {showCreateTeamModal && (
    -
    +
    -
    -
    -
    - +
    +
    +
    +
    -
    @@ -156,12 +165,13 @@ export default function Teams() { Name
    @@ -171,7 +181,7 @@ export default function Teams() {
    diff --git a/prisma/migrations/20210824054220_add_bio_branding_logo_to_team/migration.sql b/prisma/migrations/20210824054220_add_bio_branding_logo_to_team/migration.sql new file mode 100644 index 00000000..04450e4d --- /dev/null +++ b/prisma/migrations/20210824054220_add_bio_branding_logo_to_team/migration.sql @@ -0,0 +1,4 @@ +-- AlterTable +ALTER TABLE "Team" ADD COLUMN "bio" TEXT, +ADD COLUMN "hideBranding" BOOLEAN NOT NULL DEFAULT false, +ADD COLUMN "logo" TEXT; diff --git a/prisma/migrations/20210830064354_add_unique_to_team_slug/migration.sql b/prisma/migrations/20210830064354_add_unique_to_team_slug/migration.sql new file mode 100644 index 00000000..35b62a56 --- /dev/null +++ b/prisma/migrations/20210830064354_add_unique_to_team_slug/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - A unique constraint covering the columns `[slug]` on the table `Team` will be added. If there are existing duplicate values, this will fail. + +*/ +-- CreateIndex +CREATE UNIQUE INDEX "Team.slug_unique" ON "Team"("slug"); \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma index eb9ea280..339f8831 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -73,10 +73,13 @@ model User { } model Team { - id Int @id @default(autoincrement()) - name String? - slug String? - members Membership[] + id Int @default(autoincrement()) @id + name String? + slug String? @unique + logo String? + bio String? + hideBranding Boolean @default(false) + members Membership[] } enum MembershipRole {