Even Better Teams (#1304)
- dropdown improvements - Improve performance of team availability - Fix default timezone - Allow team admins to edit event types - Change team availability slot input to dropdown select (15,30,60) - Prevent teams from access if not pro user
This commit is contained in:
parent
4ce879e5dc
commit
c21f0c2d49
16 changed files with 141 additions and 73 deletions
|
@ -39,7 +39,7 @@ import { useViewerI18n } from "./I18nLanguageHandler";
|
|||
import Logo from "./Logo";
|
||||
import Button from "./ui/Button";
|
||||
|
||||
function useMeQuery() {
|
||||
export function useMeQuery() {
|
||||
const meQuery = trpc.useQuery(["viewer.me"], {
|
||||
retry(failureCount) {
|
||||
return failureCount > 3;
|
||||
|
|
|
@ -17,7 +17,12 @@ import Avatar from "@components/ui/Avatar";
|
|||
import Button from "@components/ui/Button";
|
||||
import ModalContainer from "@components/ui/ModalContainer";
|
||||
|
||||
import Dropdown, { DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "../ui/Dropdown";
|
||||
import Dropdown, {
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "../ui/Dropdown";
|
||||
import MemberChangeRoleModal from "./MemberChangeRoleModal";
|
||||
import TeamRole from "./TeamRole";
|
||||
import { MembershipRole } from ".prisma/client";
|
||||
|
@ -91,13 +96,13 @@ export default function MemberListItem(props: Props) {
|
|||
<DropdownMenuItem>
|
||||
<Link href={"/" + props.member.username}>
|
||||
<a target="_blank">
|
||||
<Button color="minimal" StartIcon={ExternalLinkIcon} className="w-full">
|
||||
<Button color="minimal" StartIcon={ExternalLinkIcon} className="w-full font-normal">
|
||||
{t("view_public_page")}
|
||||
</Button>
|
||||
</a>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
|
||||
<DropdownMenuSeparator className="h-px bg-gray-200" />
|
||||
{(props.team.membership.role === MembershipRole.OWNER ||
|
||||
props.team.membership.role === MembershipRole.ADMIN) && (
|
||||
<>
|
||||
|
@ -106,10 +111,11 @@ export default function MemberListItem(props: Props) {
|
|||
onClick={() => setShowChangeMemberRoleModal(true)}
|
||||
color="minimal"
|
||||
StartIcon={PencilIcon}
|
||||
className="flex-shrink-0 w-full">
|
||||
className="flex-shrink-0 w-full font-normal">
|
||||
{t("edit_role")}
|
||||
</Button>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator className="h-px bg-gray-200" />
|
||||
<DropdownMenuItem>
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
|
@ -119,7 +125,7 @@ export default function MemberListItem(props: Props) {
|
|||
}}
|
||||
color="warn"
|
||||
StartIcon={UserRemoveIcon}
|
||||
className="w-full">
|
||||
className="w-full font-normal">
|
||||
{t("remove_member")}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
|
@ -151,9 +157,11 @@ export default function MemberListItem(props: Props) {
|
|||
<TeamAvailabilityModal team={props.team} member={props.member} />
|
||||
<div className="p-5 space-x-2 border-t">
|
||||
<Button onClick={() => setShowTeamAvailabilityModal(false)}>{t("done")}</Button>
|
||||
<Link href={`/settings/teams/${props.team.id}/availability`}>
|
||||
<Button color="secondary">{t("Open Team Availability")}</Button>
|
||||
</Link>
|
||||
{props.team.membership.role !== MembershipRole.MEMBER && (
|
||||
<Link href={`/settings/teams/${props.team.id}/availability`}>
|
||||
<Button color="secondary">{t("Open Team Availability")}</Button>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</ModalContainer>
|
||||
)}
|
||||
|
|
|
@ -17,6 +17,7 @@ import Dropdown, {
|
|||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuSeparator,
|
||||
} from "@components/ui/Dropdown";
|
||||
|
||||
import TeamRole from "./TeamRole";
|
||||
|
@ -123,23 +124,33 @@ export default function TeamListItem(props: Props) {
|
|||
<DropdownMenuItem>
|
||||
<Link href={"/settings/teams/" + team.id}>
|
||||
<a>
|
||||
<Button type="button" color="minimal" className="w-full" StartIcon={PencilIcon}>
|
||||
<Button
|
||||
type="button"
|
||||
color="minimal"
|
||||
className="w-full font-normal"
|
||||
StartIcon={PencilIcon}>
|
||||
{t("edit_team")}
|
||||
</Button>
|
||||
</a>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{isAdmin && <DropdownMenuSeparator className="h-px bg-gray-200" />}
|
||||
<DropdownMenuItem>
|
||||
<Link href={`${process.env.NEXT_PUBLIC_APP_URL}/team/${team.slug}`} passHref={true}>
|
||||
<a target="_blank">
|
||||
<Button type="button" color="minimal" className="w-full" StartIcon={ExternalLinkIcon}>
|
||||
<Button
|
||||
type="button"
|
||||
color="minimal"
|
||||
className="w-full font-normal"
|
||||
StartIcon={ExternalLinkIcon}>
|
||||
{" "}
|
||||
{t("preview_team")}
|
||||
</Button>
|
||||
</a>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator className="h-px bg-gray-200" />
|
||||
{isOwner && (
|
||||
<DropdownMenuItem>
|
||||
<Dialog>
|
||||
|
@ -150,7 +161,7 @@ export default function TeamListItem(props: Props) {
|
|||
}}
|
||||
color="warn"
|
||||
StartIcon={TrashIcon}
|
||||
className="w-full">
|
||||
className="w-full font-normal">
|
||||
{t("disband_team")}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
|
@ -164,6 +175,7 @@ export default function TeamListItem(props: Props) {
|
|||
</Dialog>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
|
||||
{!isOwner && (
|
||||
<DropdownMenuItem>
|
||||
<Dialog>
|
||||
|
|
|
@ -87,7 +87,7 @@ export default function TeamSettings(props: Props) {
|
|||
htmlFor="team-url"
|
||||
Input={
|
||||
<TextField
|
||||
name="team-url"
|
||||
name="" // typescript requires name but we don't want component to render name label
|
||||
id="team-url"
|
||||
addOnLeading={
|
||||
<span className="inline-flex items-center px-3 text-gray-500 border border-r-0 border-gray-300 rounded-l-sm bg-gray-50 sm:text-sm">
|
||||
|
|
|
@ -109,8 +109,7 @@ export default function TeamSettingsRightSidebar(props: { team: TeamWithMembers;
|
|||
</Dialog>
|
||||
)}
|
||||
</div>
|
||||
{/* TODO: Team availability */}
|
||||
{props.team?.id && (
|
||||
{props.team?.id && props.role !== MembershipRole.MEMBER && (
|
||||
<Link href={`/settings/teams/${props.team.id}/availability`}>
|
||||
<div className="mt-5 space-y-1">
|
||||
<LinkIconButton Icon={ClockIcon}>{"View Availability"}</LinkIconButton>
|
||||
|
|
|
@ -33,7 +33,7 @@ export function Alert(props: AlertProps) {
|
|||
<CheckCircleIcon className={classNames("h-5 w-5 text-gray-400")} aria-hidden="true" />
|
||||
)}
|
||||
</div>
|
||||
<div className="ml-3 flex-grow">
|
||||
<div className="flex-grow ml-3">
|
||||
<h3 className="text-sm font-medium">{props.title}</h3>
|
||||
<div className="text-sm">{props.message}</div>
|
||||
</div>
|
||||
|
|
|
@ -8,7 +8,7 @@ import { trpc, inferQueryOutput } from "@lib/trpc";
|
|||
|
||||
import Avatar from "@components/ui/Avatar";
|
||||
import { DatePicker } from "@components/ui/form/DatePicker";
|
||||
import MinutesField from "@components/ui/form/MinutesField";
|
||||
import Select from "@components/ui/form/Select";
|
||||
|
||||
import TeamAvailabilityTimes from "./TeamAvailabilityTimes";
|
||||
|
||||
|
@ -22,8 +22,10 @@ interface Props {
|
|||
export default function TeamAvailabilityModal(props: Props) {
|
||||
const utils = trpc.useContext();
|
||||
const [selectedDate, setSelectedDate] = useState(dayjs());
|
||||
const [selectedTimeZone, setSelectedTimeZone] = useState<ITimezone>(dayjs.tz.guess);
|
||||
const [frequency, setFrequency] = useState<number>(30);
|
||||
const [selectedTimeZone, setSelectedTimeZone] = useState<ITimezone>(
|
||||
localStorage.getItem("timeOption.preferredTimeZone") || dayjs.tz.guess()
|
||||
);
|
||||
const [frequency, setFrequency] = useState<15 | 30 | 60>(30);
|
||||
|
||||
useEffect(() => {
|
||||
utils.invalidateQueries(["viewer.teams.getMemberAvailability"]);
|
||||
|
@ -64,24 +66,25 @@ export default function TeamAvailabilityModal(props: Props) {
|
|||
</div>
|
||||
<div>
|
||||
<span className="font-bold text-gray-600">Slot Length</span>
|
||||
<MinutesField
|
||||
id="length"
|
||||
label=""
|
||||
required
|
||||
min="10"
|
||||
placeholder="15"
|
||||
defaultValue={frequency}
|
||||
onChange={(e) => {
|
||||
setFrequency(Number(e.target.value));
|
||||
}}
|
||||
<Select
|
||||
options={[
|
||||
{ value: 15, label: "15 minutes" },
|
||||
{ value: 30, label: "30 minutes" },
|
||||
{ value: 60, label: "60 minutes" },
|
||||
]}
|
||||
isSearchable={false}
|
||||
classNamePrefix="react-select"
|
||||
className="flex-1 block w-full min-w-0 border border-gray-300 rounded-sm react-select-container focus:ring-primary-500 focus:border-primary-500 sm:text-sm"
|
||||
value={{ value: frequency, label: `${frequency} minutes` }}
|
||||
onChange={(newFrequency) => setFrequency(newFrequency?.value ?? 30)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{props.team && props.member && (
|
||||
<TeamAvailabilityTimes
|
||||
className="overflow-scroll"
|
||||
team={props.team}
|
||||
member={props.member}
|
||||
teamId={props.team.id}
|
||||
memberId={props.member.id}
|
||||
frequency={frequency}
|
||||
selectedDate={selectedDate}
|
||||
selectedTimeZone={selectedTimeZone}
|
||||
|
|
|
@ -9,22 +9,21 @@ import { trpc, inferQueryOutput } from "@lib/trpc";
|
|||
|
||||
import Avatar from "@components/ui/Avatar";
|
||||
import { DatePicker } from "@components/ui/form/DatePicker";
|
||||
import MinutesField from "@components/ui/form/MinutesField";
|
||||
import Select from "@components/ui/form/Select";
|
||||
|
||||
import TeamAvailabilityTimes from "./TeamAvailabilityTimes";
|
||||
|
||||
interface Props {
|
||||
team?: inferQueryOutput<"viewer.teams.get">;
|
||||
members?: inferQueryOutput<"viewer.teams.get">["members"];
|
||||
}
|
||||
|
||||
// type Member = inferQueryOutput<"viewer.teams.get">["members"][number];
|
||||
|
||||
export default function TeamAvailabilityScreen(props: Props) {
|
||||
const utils = trpc.useContext();
|
||||
const [selectedDate, setSelectedDate] = useState(dayjs());
|
||||
const [selectedTimeZone, setSelectedTimeZone] = useState<ITimezone>(dayjs.tz.guess());
|
||||
const [frequency, setFrequency] = useState<number>(30);
|
||||
const [selectedTimeZone, setSelectedTimeZone] = useState<ITimezone>(
|
||||
localStorage.getItem("timeOption.preferredTimeZone") || dayjs.tz.guess()
|
||||
);
|
||||
const [frequency, setFrequency] = useState<15 | 30 | 60>(30);
|
||||
|
||||
useEffect(() => {
|
||||
utils.invalidateQueries(["viewer.teams.getMemberAvailability"]);
|
||||
|
@ -32,14 +31,14 @@ export default function TeamAvailabilityScreen(props: Props) {
|
|||
}, [selectedTimeZone, selectedDate]);
|
||||
|
||||
const Item = ({ index, style }: { index: number; style: CSSProperties }) => {
|
||||
const member = props.members?.[index];
|
||||
const member = props.team?.members?.[index];
|
||||
if (!member) return <></>;
|
||||
|
||||
return (
|
||||
<div key={member.id} style={style} className="flex pl-4 border-r border-gray-200 ">
|
||||
<TeamAvailabilityTimes
|
||||
team={props.team as inferQueryOutput<"viewer.teams.get">}
|
||||
member={member}
|
||||
teamId={props.team?.id as number}
|
||||
memberId={member.id}
|
||||
frequency={frequency}
|
||||
selectedDate={selectedDate}
|
||||
selectedTimeZone={selectedTimeZone}
|
||||
|
@ -86,17 +85,17 @@ export default function TeamAvailabilityScreen(props: Props) {
|
|||
</div>
|
||||
<div>
|
||||
<span className="font-bold text-gray-600">Slot Length</span>
|
||||
<MinutesField
|
||||
id="length"
|
||||
label=""
|
||||
required
|
||||
min="10"
|
||||
className="p-2.5"
|
||||
placeholder="15"
|
||||
defaultValue={frequency}
|
||||
onChange={(e) => {
|
||||
setFrequency(Number(e.target.value));
|
||||
}}
|
||||
<Select
|
||||
options={[
|
||||
{ value: 15, label: "15 minutes" },
|
||||
{ value: 30, label: "30 minutes" },
|
||||
{ value: 60, label: "60 minutes" },
|
||||
]}
|
||||
isSearchable={false}
|
||||
classNamePrefix="react-select"
|
||||
className="flex-1 block w-full min-w-0 border border-gray-300 rounded-sm react-select-container focus:ring-primary-500 focus:border-primary-500 sm:text-sm"
|
||||
value={{ value: frequency, label: `${frequency} minutes` }}
|
||||
onChange={(newFrequency) => setFrequency(newFrequency?.value ?? 30)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -105,10 +104,10 @@ export default function TeamAvailabilityScreen(props: Props) {
|
|||
{({ height, width }) => (
|
||||
<List
|
||||
itemSize={240}
|
||||
itemCount={props.members?.length || 0}
|
||||
itemCount={props.team?.members?.length ?? 0}
|
||||
className="List"
|
||||
height={height}
|
||||
direction="horizontal"
|
||||
layout="horizontal"
|
||||
width={width}>
|
||||
{Item}
|
||||
</List>
|
||||
|
|
|
@ -5,13 +5,13 @@ import React from "react";
|
|||
import { ITimezone } from "react-timezone-select";
|
||||
|
||||
import getSlots from "@lib/slots";
|
||||
import { inferQueryOutput, trpc } from "@lib/trpc";
|
||||
import { trpc } from "@lib/trpc";
|
||||
|
||||
import Loader from "@components/Loader";
|
||||
|
||||
interface Props {
|
||||
team: inferQueryOutput<"viewer.teams.get">;
|
||||
member: inferQueryOutput<"viewer.teams.get">["members"][number];
|
||||
teamId: number;
|
||||
memberId: number;
|
||||
selectedDate: Dayjs;
|
||||
selectedTimeZone: ITimezone;
|
||||
frequency: number;
|
||||
|
@ -26,8 +26,8 @@ export default function TeamAvailabilityTimes(props: Props) {
|
|||
[
|
||||
"viewer.teams.getMemberAvailability",
|
||||
{
|
||||
teamId: props.team.id,
|
||||
memberId: props.member.id,
|
||||
teamId: props.teamId,
|
||||
memberId: props.memberId,
|
||||
dateFrom: props.selectedDate.toString(),
|
||||
dateTo: props.selectedDate.add(1, "day").toString(),
|
||||
timezone: `${props.selectedTimeZone.toString()}`,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
import TeamAvailabilityScreen from "@ee/components/team/availability/TeamAvailabilityScreen";
|
||||
|
||||
|
@ -7,15 +7,18 @@ import { getPlaceholderAvatar } from "@lib/getPlaceholderAvatar";
|
|||
import { trpc } from "@lib/trpc";
|
||||
|
||||
import Loader from "@components/Loader";
|
||||
import Shell from "@components/Shell";
|
||||
import Shell, { useMeQuery } from "@components/Shell";
|
||||
import { Alert } from "@components/ui/Alert";
|
||||
import Avatar from "@components/ui/Avatar";
|
||||
|
||||
export function TeamSettingsPage() {
|
||||
export function TeamAvailabilityPage() {
|
||||
const router = useRouter();
|
||||
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
|
||||
const me = useMeQuery();
|
||||
const isFreeUser = me.data?.plan === "FREE";
|
||||
|
||||
const { data: team, isLoading } = trpc.useQuery(["viewer.teams.get", { teamId: Number(router.query.id) }], {
|
||||
refetchOnWindowFocus: false,
|
||||
onError: (e) => {
|
||||
|
@ -23,14 +26,20 @@ export function TeamSettingsPage() {
|
|||
},
|
||||
});
|
||||
|
||||
// prevent unnecessary re-renders due to shell queries
|
||||
const TeamAvailability = useMemo(() => {
|
||||
return <TeamAvailabilityScreen team={team} />;
|
||||
}, [team]);
|
||||
|
||||
return (
|
||||
<Shell
|
||||
showBackButton={!errorMessage}
|
||||
heading={team?.name}
|
||||
heading={!isFreeUser && team?.name}
|
||||
flexChildrenContainer
|
||||
subtitle={team && "Your team's availability at a glance"}
|
||||
subtitle={team && !isFreeUser && "Your team's availability at a glance"}
|
||||
HeadingLeftIcon={
|
||||
team && (
|
||||
team &&
|
||||
!isFreeUser && (
|
||||
<Avatar
|
||||
size={12}
|
||||
imageSrc={getPlaceholderAvatar(team?.logo, team?.name as string)}
|
||||
|
@ -41,9 +50,17 @@ export function TeamSettingsPage() {
|
|||
}>
|
||||
{!!errorMessage && <Alert className="-mt-24 border" severity="error" title={errorMessage} />}
|
||||
{isLoading && <Loader />}
|
||||
{team && <TeamAvailabilityScreen team={team} members={team.members} />}
|
||||
{isFreeUser ? (
|
||||
<Alert
|
||||
className="-mt-24 border"
|
||||
severity="warning"
|
||||
title="This is a pro feature. Upgrade to pro to see your team's availability."
|
||||
/>
|
||||
) : (
|
||||
TeamAvailability
|
||||
)}
|
||||
</Shell>
|
||||
);
|
||||
}
|
||||
|
||||
export default TeamSettingsPage;
|
||||
export default TeamAvailabilityPage;
|
||||
|
|
|
@ -86,7 +86,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||
const isAuthorized = (function () {
|
||||
if (event.team) {
|
||||
return event.team.members
|
||||
.filter((member) => member.role === MembershipRole.OWNER)
|
||||
.filter((member) => member.role === MembershipRole.OWNER || member.role === MembershipRole.ADMIN)
|
||||
.map((member) => member.userId)
|
||||
.includes(session.user.id);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { PlusIcon } from "@heroicons/react/solid";
|
||||
import classNames from "classnames";
|
||||
import { useSession } from "next-auth/client";
|
||||
import { useState } from "react";
|
||||
|
||||
|
@ -7,7 +8,7 @@ import { trpc } from "@lib/trpc";
|
|||
|
||||
import Loader from "@components/Loader";
|
||||
import SettingsShell from "@components/SettingsShell";
|
||||
import Shell from "@components/Shell";
|
||||
import Shell, { useMeQuery } from "@components/Shell";
|
||||
import TeamCreateModal from "@components/team/TeamCreateModal";
|
||||
import TeamList from "@components/team/TeamList";
|
||||
import { Alert } from "@components/ui/Alert";
|
||||
|
@ -19,6 +20,8 @@ export default function Teams() {
|
|||
const [showCreateTeamModal, setShowCreateTeamModal] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
|
||||
const me = useMeQuery();
|
||||
|
||||
const { data } = trpc.useQuery(["viewer.teams.list"], {
|
||||
onError: (e) => {
|
||||
setErrorMessage(e.message);
|
||||
|
@ -29,15 +32,34 @@ export default function Teams() {
|
|||
|
||||
const teams = data?.filter((m) => m.accepted) || [];
|
||||
const invites = data?.filter((m) => !m.accepted) || [];
|
||||
const isFreePlan = me.data?.plan === "FREE";
|
||||
|
||||
return (
|
||||
<Shell heading={t("teams")} subtitle={t("create_manage_teams_collaborative")}>
|
||||
<SettingsShell>
|
||||
{!!errorMessage && <Alert severity="error" title={errorMessage} />}
|
||||
|
||||
{isFreePlan && (
|
||||
<Alert
|
||||
severity="warning"
|
||||
title={<>{t("plan_upgrade_teams")}</>}
|
||||
message={
|
||||
<>
|
||||
{t("to_upgrade_go_to")}{" "}
|
||||
<a href={"https://cal.com/upgrade"} className="underline">
|
||||
{"https://cal.com/upgrade"}
|
||||
</a>
|
||||
</>
|
||||
}
|
||||
className="my-4"
|
||||
/>
|
||||
)}
|
||||
{showCreateTeamModal && <TeamCreateModal onClose={() => setShowCreateTeamModal(false)} />}
|
||||
<div className="flex justify-end my-4">
|
||||
<Button type="button" className="btn btn-white" onClick={() => setShowCreateTeamModal(true)}>
|
||||
<div className={classNames("flex justify-end my-4", isFreePlan && "opacity-50")}>
|
||||
<Button
|
||||
disabled={isFreePlan}
|
||||
type="button"
|
||||
className="btn btn-white"
|
||||
onClick={() => setShowCreateTeamModal(true)}>
|
||||
<PlusIcon className="group-hover:text-black text-gray-700 w-3.5 h-3.5 mr-2 inline-block" />
|
||||
{t("new_team")}
|
||||
</Button>
|
||||
|
|
|
@ -445,6 +445,7 @@
|
|||
"hidden": "Hidden",
|
||||
"readonly": "Readonly",
|
||||
"plan_upgrade": "You need to upgrade your plan to have more than one active event type.",
|
||||
"plan_upgrade_teams": "You need to upgrade your plan to create a team.",
|
||||
"plan_upgrade_instructions": "To upgrade, go to <a href=\"https://cal.com/upgrade\" className=\"underline\">https://cal.com/upgrade</a>",
|
||||
"event_types_page_title": "Event Types",
|
||||
"event_types_page_subtitle": "Create events to share for people to book on your calendar.",
|
||||
|
|
|
@ -44,6 +44,7 @@ async function getUserFromSession({
|
|||
avatar: true,
|
||||
twoFactorEnabled: true,
|
||||
brandColor: true,
|
||||
plan: true,
|
||||
credentials: {
|
||||
select: {
|
||||
id: true,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { BookingStatus, Prisma } from "@prisma/client";
|
||||
import { BookingStatus, MembershipRole, Prisma } from "@prisma/client";
|
||||
import _ from "lodash";
|
||||
import { z } from "zod";
|
||||
|
||||
|
@ -57,6 +57,7 @@ const loggedInViewerRouter = createProtectedRouter()
|
|||
completedOnboarding,
|
||||
twoFactorEnabled,
|
||||
brandColor,
|
||||
plan,
|
||||
} = ctx.user;
|
||||
const me = {
|
||||
id,
|
||||
|
@ -72,6 +73,7 @@ const loggedInViewerRouter = createProtectedRouter()
|
|||
completedOnboarding,
|
||||
twoFactorEnabled,
|
||||
brandColor,
|
||||
plan,
|
||||
};
|
||||
return me;
|
||||
},
|
||||
|
@ -231,7 +233,7 @@ const loggedInViewerRouter = createProtectedRouter()
|
|||
},
|
||||
metadata: {
|
||||
membershipCount: membership.team.members.length,
|
||||
readOnly: membership.role !== "OWNER",
|
||||
readOnly: membership.role === MembershipRole.MEMBER,
|
||||
},
|
||||
eventTypes: membership.team.eventTypes,
|
||||
}))
|
||||
|
|
|
@ -58,6 +58,10 @@ export const viewerTeamsRouter = createProtectedRouter()
|
|||
name: z.string(),
|
||||
}),
|
||||
async resolve({ ctx, input }) {
|
||||
if (ctx.user.plan === "FREE") {
|
||||
throw new TRPCError({ code: "UNAUTHORIZED", message: "You are not a pro user." });
|
||||
}
|
||||
|
||||
const slug = slugify(input.name);
|
||||
|
||||
const nameCollisions = await ctx.prisma.team.count({
|
||||
|
|
Loading…
Reference in a new issue