fix: Orgs/create child teams CAL-1986 (#9631)
* Re-applied valid changes * Update packages/trpc/server/middlewares/sessionMiddleware.ts * Type fix * Type fix --------- Co-authored-by: zomars <zomars@me.com>
This commit is contained in:
parent
936432a730
commit
75f76c130a
|
@ -1,41 +1,47 @@
|
|||
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
|
||||
import { TeamsListing } from "@calcom/features/ee/teams/components";
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Button } from "@calcom/ui";
|
||||
import { Plus } from "@calcom/ui/components/icon";
|
||||
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
import { ssrInit } from "@server/lib/ssr";
|
||||
|
||||
function Teams() {
|
||||
const { t } = useLocale();
|
||||
const [user] = trpc.viewer.me.useSuspenseQuery();
|
||||
|
||||
return (
|
||||
<Shell
|
||||
heading={t("teams")}
|
||||
hideHeadingOnMobile
|
||||
subtitle={t("create_manage_teams_collaborative")}
|
||||
CTA={
|
||||
<Button
|
||||
variant="fab"
|
||||
StartIcon={Plus}
|
||||
type="button"
|
||||
href={`${WEBAPP_URL}/settings/teams/new?returnTo=${WEBAPP_URL}/teams`}>
|
||||
{t("new")}
|
||||
</Button>
|
||||
(!user.organizationId || user.organization.isOrgAdmin) && (
|
||||
<Button
|
||||
variant="fab"
|
||||
StartIcon={Plus}
|
||||
type="button"
|
||||
href={`${WEBAPP_URL}/settings/teams/new?returnTo=${WEBAPP_URL}/teams`}>
|
||||
{t("new")}
|
||||
</Button>
|
||||
)
|
||||
}>
|
||||
<TeamsListing />
|
||||
</Shell>
|
||||
);
|
||||
}
|
||||
|
||||
export const getStaticProps = async () => {
|
||||
return {
|
||||
props: {
|
||||
...(await serverSideTranslations("en", ["common"])),
|
||||
},
|
||||
};
|
||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||
const ssr = await ssrInit(context);
|
||||
await ssr.viewer.me.prefetch();
|
||||
|
||||
return { props: { trpcState: ssr.dehydrate() } };
|
||||
};
|
||||
|
||||
Teams.requiresLicense = false;
|
||||
|
|
|
@ -1923,6 +1923,7 @@
|
|||
"crm": "CRM",
|
||||
"messaging": "Messaging",
|
||||
"sender_id_info": "Name or number shown as the sender of an SMS (some countries do not allow alphanumeric sender IDs)",
|
||||
"org_admins_can_create_new_teams": "Only the admin of your organization can create new teams",
|
||||
"google_new_spam_policy": "Google’s new spam policy could prevent you from receiving any email and calendar notifications about this booking.",
|
||||
"resolve": "Resolve",
|
||||
"no_organization_slug": "There was an error creating teams for this organization. Missing URL slug.",
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { useRouter } from "next/router";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { WEBAPP_URL, APP_NAME } from "@calcom/lib/constants";
|
||||
import { APP_NAME, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Alert, Button, ButtonGroup, Label, showToast } from "@calcom/ui";
|
||||
import { Alert, Label, showToast, ButtonGroup, Button } from "@calcom/ui";
|
||||
import { EyeOff, Mail, RefreshCcw, UserPlus, Users, Video } from "@calcom/ui/components/icon";
|
||||
|
||||
import { UpgradeTip } from "../../../tips";
|
||||
|
@ -26,6 +26,8 @@ export function TeamsListing() {
|
|||
},
|
||||
});
|
||||
|
||||
const { data: user } = trpc.viewer.me.useQuery();
|
||||
|
||||
const { mutate: inviteMemberByToken } = trpc.viewer.teams.inviteMemberByToken.useMutation({
|
||||
onSuccess: (teamName) => {
|
||||
trpcContext.viewer.teams.list.invalidate();
|
||||
|
@ -102,16 +104,20 @@ export function TeamsListing() {
|
|||
features={features}
|
||||
background="/tips/teams"
|
||||
buttons={
|
||||
<div className="space-y-2 rtl:space-x-reverse sm:space-x-2">
|
||||
<ButtonGroup>
|
||||
<Button color="primary" href={`${WEBAPP_URL}/settings/teams/new`}>
|
||||
{t("create_team")}
|
||||
</Button>
|
||||
<Button color="minimal" href="https://go.cal.com/teams-video" target="_blank">
|
||||
{t("learn_more")}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
!user?.organizationId || user?.organization.isOrgAdmin ? (
|
||||
<div className="space-y-2 rtl:space-x-reverse sm:space-x-2">
|
||||
<ButtonGroup>
|
||||
<Button color="primary" href={`${WEBAPP_URL}/settings/teams/new`}>
|
||||
{t("create_team")}
|
||||
</Button>
|
||||
<Button color="minimal" href="https://go.cal.com/teams-video" target="_blank">
|
||||
{t("learn_more")}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
) : (
|
||||
<p>{t("org_admins_can_create_new_teams")}</p>
|
||||
)
|
||||
}>
|
||||
{teams.length > 0 ? <TeamList teams={teams} /> : <></>}
|
||||
</UpgradeTip>
|
||||
|
|
|
@ -284,6 +284,9 @@ const SettingsSidebarContainer = ({
|
|||
{teams &&
|
||||
teamMenuState &&
|
||||
teams.map((team, index: number) => {
|
||||
if (!teamMenuState[index]) {
|
||||
return null;
|
||||
}
|
||||
if (teamMenuState.some((teamState) => teamState.teamId === team.id))
|
||||
return (
|
||||
<Collapsible
|
||||
|
|
|
@ -2,6 +2,7 @@ import type { Session } from "next-auth";
|
|||
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { defaultAvatarSrc } from "@calcom/lib/defaultAvatarImage";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
import { teamMetadataSchema, userMetadata } from "@calcom/prisma/zod-utils";
|
||||
|
||||
import type { Maybe } from "@trpc/server";
|
||||
|
@ -77,6 +78,13 @@ export async function getUserFromSession(ctx: TRPCContextInner, session: Maybe<S
|
|||
id: true,
|
||||
slug: true,
|
||||
metadata: true,
|
||||
members: {
|
||||
select: { userId: true },
|
||||
where: {
|
||||
userId: session.user.id,
|
||||
OR: [{ role: MembershipRole.ADMIN }, { role: MembershipRole.OWNER }],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -98,10 +106,17 @@ export async function getUserFromSession(ctx: TRPCContextInner, session: Maybe<S
|
|||
// This helps to prevent reaching the 4MB payload limit by avoiding base64 and instead passing the avatar url
|
||||
user.avatar = rawAvatar ? `${WEBAPP_URL}/${user.username}/avatar.png` : defaultAvatarSrc({ email });
|
||||
const locale = user?.locale || ctx.locale;
|
||||
|
||||
const isOrgAdmin = !!user.organization?.members.length;
|
||||
// Want to reduce the amount of data being sent
|
||||
if (isOrgAdmin && user.organization?.members) {
|
||||
user.organization.members = [];
|
||||
}
|
||||
return {
|
||||
...user,
|
||||
organization: {
|
||||
...user.organization,
|
||||
isOrgAdmin,
|
||||
metadata: orgMetadata,
|
||||
},
|
||||
id,
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
import { IS_TEAM_BILLING_ENABLED } from "@calcom/lib/constants";
|
||||
import { isOrganisationAdmin } from "@calcom/lib/server/queries/organisations";
|
||||
import { closeComUpsertTeamUser } from "@calcom/lib/sync/SyncServiceManager";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
|
@ -17,21 +15,36 @@ type CreateOptions = {
|
|||
};
|
||||
|
||||
export const createHandler = async ({ ctx, input }: CreateOptions) => {
|
||||
const { user } = ctx;
|
||||
const { slug, name, logo } = input;
|
||||
const currentOrgId = ctx.user.organization?.id;
|
||||
const isOrgChildTeam = !!user.organizationId;
|
||||
|
||||
// For orgs we want to create teams under the org
|
||||
if (user.organizationId && !user.organization.isOrgAdmin) {
|
||||
throw new TRPCError({ code: "FORBIDDEN", message: "org_admins_can_create_new_teams" });
|
||||
}
|
||||
|
||||
const slugCollisions = await prisma.team.findFirst({
|
||||
where: {
|
||||
slug: slug,
|
||||
parentId: currentOrgId
|
||||
? {
|
||||
equals: currentOrgId,
|
||||
}
|
||||
: null,
|
||||
// If this is under an org, check that the team doesn't already exist
|
||||
...(isOrgChildTeam && { parentId: user.organizationId }),
|
||||
},
|
||||
});
|
||||
|
||||
if (slugCollisions) throw new TRPCError({ code: "BAD_REQUEST", message: "team_url_taken" });
|
||||
|
||||
if (user.organizationId) {
|
||||
const nameCollisions = await prisma.user.findFirst({
|
||||
where: {
|
||||
organizationId: user.organization.id,
|
||||
username: slug,
|
||||
},
|
||||
});
|
||||
|
||||
if (nameCollisions) throw new TRPCError({ code: "BAD_REQUEST", message: "team_slug_exists_as_user" });
|
||||
}
|
||||
|
||||
// Ensure that the user is not duplicating a requested team
|
||||
const duplicatedRequest = await prisma.team.findFirst({
|
||||
where: {
|
||||
|
@ -51,25 +64,6 @@ export const createHandler = async ({ ctx, input }: CreateOptions) => {
|
|||
return duplicatedRequest;
|
||||
}
|
||||
|
||||
let parentId: number | null = null;
|
||||
// If the user in session is part of an org. check permissions
|
||||
if (ctx.user.organization?.id) {
|
||||
if (!isOrganisationAdmin(ctx.user.id, ctx.user.organization.id)) {
|
||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
}
|
||||
|
||||
const nameCollisions = await prisma.user.findFirst({
|
||||
where: {
|
||||
organizationId: ctx.user.organization.id,
|
||||
username: slug,
|
||||
},
|
||||
});
|
||||
|
||||
if (nameCollisions) throw new TRPCError({ code: "BAD_REQUEST", message: "team_slug_exists_as_user" });
|
||||
|
||||
parentId = ctx.user.organization.id;
|
||||
}
|
||||
|
||||
const createTeam = await prisma.team.create({
|
||||
data: {
|
||||
name,
|
||||
|
@ -84,10 +78,7 @@ export const createHandler = async ({ ctx, input }: CreateOptions) => {
|
|||
metadata: {
|
||||
requestedSlug: slug,
|
||||
},
|
||||
...(!IS_TEAM_BILLING_ENABLED && { slug }),
|
||||
parent: {
|
||||
connect: parentId ? { id: parentId } : undefined,
|
||||
},
|
||||
...(isOrgChildTeam && { parentId: user.organizationId }),
|
||||
},
|
||||
});
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user