feat: from org team invite (#9569)
* Initial commit * Adding feature flag * Desktop first banner, mobile pending * Removing dead code and img * AppInstallButtonBase * WIP * Adds Email verification template+translations for organizations (#9202) * feat: Orgs Schema Changing `scopedMembers` to `orgUsers` (#9209) * Change scopedMembers to orgMembers * Change to orgUsers * First step done * Merge branch 'feat/organizations-onboarding' of github.com:calcom/cal.com into feat/organizations-onboarding * Session logic to show org label * Step 2 done, avatar not working * List orgs and list teams specific if orgs exist * Conditionally show org - fix settings layout - add labels for all pages * Profile Page + update * Org specific team creation * appearance page * Ensure members cant of org cant update settings in UI * Fix update handler imports * hide billing on sub teams * Update profile slug page * Letting duplicate slugs for teams to support orgs * Add slug coliisions for org * Covering null on unique clauses * Covering null on unique clauses * Extract to utils * Update settings to use subdomain path in team url , team + org * Supporting having the orgId in the session cookie * Onboarding admins step * Last step to create teams * Update handler comments * Upgrade ORG banner - disabled team banner for child teams * Handle publishing ORGS * Fix licenese issue * Update packages/trpc/server/routers/viewer/teams/create.handler.ts * Split into function calls to make this file more explisit * Update parents stripe sub not teamID * Moving change password handler, improving verifying code flow * Clearing error before submitting * Reverting email testing api changes * Reverting having the banner for now * Consistent exported components * Remove unneeded files from banner * Removing uneeded code * Fixing avatar selector * Using meta component for head/descr * Missing i18n strings * Create org membership also - billing portal page * A11ly * Hide create team if no valid permisisons * Get Org members router * Handle updating subscription if orgId * Fix double upgrade banner * Update constants * Feedback * Copy change * Making an org avatar (temp) * Add slug colission detection for user and team name * Fix Import * Remove update password func * Fix module import over relative * feat: organization event type filter (#9253) Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in> * Missing changes to support orgs schema changes * Fix import again * Throw no team found before auth error * Check if invited found user is already in differnt org * feat: organization settings general page Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in> * feat: add members page Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in> * chore: remove Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in> * fix: use invalidate Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in> * Move to for of loop to throw errors in usenamelist * fix: delete mutation Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in> * fix: remove organization id Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in> * chore Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in> * Remove app install button sa its in 9337 * Remove i18n key not being used * feat: Onboarding process to create an organization (#9184) * Desktop first banner, mobile pending * Removing dead code and img * WIP * Adds Email verification template+translations for organizations (#9202) * First step done * Merge branch 'feat/organizations-onboarding' of github.com:calcom/cal.com into feat/organizations-onboarding * Step 2 done, avatar not working * Covering null on unique clauses * Onboarding admins step * Last step to create teams * Moving change password handler, improving verifying code flow * Clearing error before submitting * Reverting email testing api changes * Reverting having the banner for now * Consistent exported components * Remove unneeded files from banner * Removing uneeded code * Fixing avatar selector * Using meta component for head/descr * Missing i18n strings * Feedback * Making an org avatar (temp) * Check for subteams slug clashes with usernames * Fixing create teams onsuccess * feedback * Making sure we check requestedSlug now --------- Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com> * feat: [CAL-1816] Organization subdomain support (#9345) * Desktop first banner, mobile pending * Removing dead code and img * WIP * Adds Email verification template+translations for organizations (#9202) * First step done * Merge branch 'feat/organizations-onboarding' of github.com:calcom/cal.com into feat/organizations-onboarding * Step 2 done, avatar not working * Covering null on unique clauses * Onboarding admins step * Last step to create teams * Moving change password handler, improving verifying code flow * Clearing error before submitting * Reverting email testing api changes * Reverting having the banner for now * Consistent exported components * Remove unneeded files from banner * Removing uneeded code * Fixing avatar selector * Using meta component for head/descr * Missing i18n strings * Feedback * Making an org avatar (temp) * Check for subteams slug clashes with usernames * Fixing create teams onsuccess * Covering users and subteams, excluding non-org users * Unpublished teams shows correctly * Create subdomain in Vercel * feedback * Renaming Vercel env vars * Vercel domain check before creation * Supporting cal-staging.com * Change to have vercel detect it * vercel domain check data message error * Remove check domain * Making sure we check requestedSlug now * Feedback and unneeded code * Reverting unneeded changes * Unneeded changes --------- Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com> * Vercel subdomain creation in PROD only * Fix router * fix: use zod schema Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in> * feat: organization settings general and members page (#9266) * feat: organization settings general page Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in> * feat: add members page Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in> * chore: remove Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in> * fix: use invalidate Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in> * fix: delete mutation Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in> * fix: remove organization id Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in> * chore Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in> * fix: use zod schema Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in> --------- Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in> * Type fixes * Use org Stripe product when upgrading * Removed unused code * UI completed * Reverting changes * Update UsernameTextfield.tsx * More reverts * Update next-auth-options.ts * Update common.json * Type fixes * Include invite token for orgs * Update org schema * Make token settings optional as it isnt used in orgs yet * Reverts * Add correct array logic and move to controlled component * Fix toggle group default | Update toggle group bg * Darkmode toggle group * Distinct user * Hide modal if no org members * Extract toggle logic * Update packages/features/ee/organizations/components/TeamInviteFromOrg.tsx * remove yarn.lock from commit * Fix types * Add getMember router back * As a query lol * Fix types * Fix accepted param defaulting to true as we want both * Fix list not pulling back people who have not joined the org yet * Fix tests to handle invite a existing org user to a team * Fix test * Fix an error sometimes when existing org user is invited * Updates radix & fixes bug --------- Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in> Co-authored-by: Leo Giovanetti <hello@leog.me> Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com> Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com> Co-authored-by: Udit Takkar <udit.07814802719@cse.mait.ac.in> Co-authored-by: zomars <zomars@me.com> Co-authored-by: Joe Au-Yeung <j.auyeung419@gmail.com> Co-authored-by: Hariom Balhara <hariombalhara@gmail.com> Co-authored-by: Shivam Kalra <shivamkalra98@gmail.com>pull/10041/head
parent
7c4b7209c2
commit
650d0082a3
|
@ -12,7 +12,7 @@
|
|||
"@calcom/ui": "*",
|
||||
"@radix-ui/react-avatar": "^1.0.0",
|
||||
"@radix-ui/react-collapsible": "^1.0.0",
|
||||
"@radix-ui/react-dialog": "^1.0.0",
|
||||
"@radix-ui/react-dialog": "^1.0.4",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.5",
|
||||
"@radix-ui/react-id": "^1.0.0",
|
||||
"@radix-ui/react-popover": "^1.0.2",
|
||||
|
|
|
@ -47,7 +47,7 @@
|
|||
"@next/bundle-analyzer": "^13.1.6",
|
||||
"@radix-ui/react-avatar": "^1.0.0",
|
||||
"@radix-ui/react-collapsible": "^1.0.0",
|
||||
"@radix-ui/react-dialog": "^1.0.0",
|
||||
"@radix-ui/react-dialog": "^1.0.4",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.5",
|
||||
"@radix-ui/react-id": "^1.0.0",
|
||||
"@radix-ui/react-popover": "^1.0.2",
|
||||
|
|
|
@ -0,0 +1,103 @@
|
|||
import type { PropsWithChildren } from "react";
|
||||
import { useState } from "react";
|
||||
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import type { RouterOutputs } from "@calcom/trpc";
|
||||
import { Avatar, TextField } from "@calcom/ui";
|
||||
|
||||
type TeamInviteFromOrgProps = PropsWithChildren<{
|
||||
selectedEmails?: string | string[];
|
||||
handleOnChecked: (usersEmail: string) => void;
|
||||
orgMembers?: RouterOutputs["viewer"]["organizations"]["getMembers"];
|
||||
}>;
|
||||
|
||||
const keysToCheck = ["name", "email", "username"] as const; // array of keys to check
|
||||
|
||||
export default function TeamInviteFromOrg({
|
||||
handleOnChecked,
|
||||
selectedEmails,
|
||||
orgMembers,
|
||||
}: TeamInviteFromOrgProps) {
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
|
||||
const filteredMembers = orgMembers?.filter((member) => {
|
||||
if (!searchQuery) {
|
||||
return true; // return all members if searchQuery is empty
|
||||
}
|
||||
const { user } = member ?? {}; // destructuring with default value in case member is undefined
|
||||
return keysToCheck.some((key) => user?.[key]?.toLowerCase().includes(searchQuery.toLowerCase()));
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="bg-muted border-subtle flex flex-col rounded-md border p-4">
|
||||
<div className="-my-1">
|
||||
<TextField placeholder="Search..." onChange={(e) => setSearchQuery(e.target.value)} />
|
||||
</div>
|
||||
<hr className="border-subtle -mx-4 mt-2" />
|
||||
<div className="scrollbar min-h-48 flex max-h-48 flex-col space-y-0.5 overflow-y-scroll pt-2">
|
||||
<>
|
||||
{filteredMembers &&
|
||||
filteredMembers.map((member) => {
|
||||
const isSelected = Array.isArray(selectedEmails)
|
||||
? selectedEmails.includes(member.user.email)
|
||||
: selectedEmails === member.user.email;
|
||||
return (
|
||||
<UserToInviteItem
|
||||
key={member.user.id}
|
||||
member={member}
|
||||
isSelected={isSelected}
|
||||
onChange={() => handleOnChecked(member.user.email)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function UserToInviteItem({
|
||||
member,
|
||||
isSelected,
|
||||
onChange,
|
||||
}: {
|
||||
member: RouterOutputs["viewer"]["organizations"]["getMembers"][number];
|
||||
isSelected: boolean;
|
||||
onChange: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
key={member.userId}
|
||||
onClick={() => onChange()} // We handle this on click on the div also - for a11y we handle it with label and checkbox below
|
||||
className={classNames(
|
||||
"flex cursor-pointer items-center rounded-md py-1 px-2",
|
||||
isSelected ? "bg-emphasis" : "hover:bg-subtle "
|
||||
)}>
|
||||
<div className="flex items-center space-x-2 rtl:space-x-reverse">
|
||||
<Avatar
|
||||
size="sm"
|
||||
alt="Users avatar"
|
||||
asChild
|
||||
imageSrc={`/api/user/${member.user.username}`}
|
||||
gravatarFallbackMd5="hash"
|
||||
/>
|
||||
<label
|
||||
htmlFor={`${member.user.id}`}
|
||||
className="text-emphasis cursor-pointer text-sm font-medium leading-none">
|
||||
{member.user.name || member.user.email || "Nameless User"}
|
||||
</label>
|
||||
</div>
|
||||
<div className="ml-auto">
|
||||
<input
|
||||
id={`${member.user.id}`}
|
||||
checked={isSelected}
|
||||
type="checkbox"
|
||||
className="text-primary-600 focus:ring-primary-500 border-default hover:bg-subtle inline-flex h-4 w-4 place-self-center justify-self-end rounded checked:bg-gray-800"
|
||||
onChange={() => {
|
||||
onChange();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -4,6 +4,7 @@ import { useMemo, useState } from "react";
|
|||
import type { FormEvent } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
|
||||
import TeamInviteFromOrg from "@calcom/ee/organizations/components/TeamInviteFromOrg";
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
|
@ -52,17 +53,24 @@ export interface NewMemberForm {
|
|||
sendInviteEmail: boolean;
|
||||
}
|
||||
|
||||
type ModalMode = "INDIVIDUAL" | "BULK";
|
||||
type ModalMode = "INDIVIDUAL" | "BULK" | "ORGANIZATION";
|
||||
|
||||
interface FileEvent<T = Element> extends FormEvent<T> {
|
||||
target: EventTarget & T;
|
||||
}
|
||||
|
||||
function toggleElementInArray(value: string[] | string | undefined, element: string): string[] {
|
||||
const array = value ? (Array.isArray(value) ? value : [value]) : [];
|
||||
return array.includes(element) ? array.filter((item) => item !== element) : [...array, element];
|
||||
}
|
||||
|
||||
export default function MemberInvitationModal(props: MemberInvitationModalProps) {
|
||||
const { t } = useLocale();
|
||||
const trpcContext = trpc.useContext();
|
||||
|
||||
const [modalImportMode, setModalInputMode] = useState<ModalMode>("INDIVIDUAL");
|
||||
const [modalImportMode, setModalInputMode] = useState<ModalMode>(
|
||||
props?.orgMembers && props.orgMembers?.length > 0 ? "ORGANIZATION" : "INDIVIDUAL"
|
||||
);
|
||||
|
||||
const createInviteMutation = trpc.viewer.teams.createInvite.useMutation({
|
||||
onSuccess(token) {
|
||||
|
@ -98,7 +106,7 @@ export default function MemberInvitationModal(props: MemberInvitationModalProps)
|
|||
},
|
||||
{ value: "BULK", label: t("invite_team_bulk_segment"), iconLeft: <Users /> },
|
||||
];
|
||||
if (props.orgMembers) {
|
||||
if (props?.orgMembers && props.orgMembers?.length > 0) {
|
||||
array.unshift({
|
||||
value: "ORGANIZATION",
|
||||
label: t("organization"),
|
||||
|
@ -170,7 +178,7 @@ export default function MemberInvitationModal(props: MemberInvitationModalProps)
|
|||
<ToggleGroup
|
||||
isFullWidth={true}
|
||||
onValueChange={(val) => setModalInputMode(val as ModalMode)}
|
||||
defaultValue="INDIVIDUAL"
|
||||
defaultValue={modalImportMode}
|
||||
options={toggleGroupOptions}
|
||||
/>
|
||||
</div>
|
||||
|
@ -260,6 +268,29 @@ export default function MemberInvitationModal(props: MemberInvitationModalProps)
|
|||
</Button>
|
||||
</div>
|
||||
)}
|
||||
{modalImportMode === "ORGANIZATION" && (
|
||||
<Controller
|
||||
name="emailOrUsername"
|
||||
control={newMemberFormMethods.control}
|
||||
rules={{
|
||||
required: t("enter_email_or_username"),
|
||||
}}
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<>
|
||||
<TeamInviteFromOrg
|
||||
selectedEmails={value}
|
||||
handleOnChecked={(userEmail) => {
|
||||
// If 'value' is not an array, create a new array with 'userEmail' to allow future updates to the array.
|
||||
// If 'value' is an array, update the array by either adding or removing 'userEmail'.
|
||||
const newValue = toggleElementInArray(value, userEmail);
|
||||
onChange(newValue);
|
||||
}}
|
||||
orgMembers={props.orgMembers}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
<Controller
|
||||
name="role"
|
||||
control={newMemberFormMethods.control}
|
||||
|
|
|
@ -266,7 +266,7 @@ export default function MemberListItem(props: Props) {
|
|||
</div>
|
||||
|
||||
{editMode && (
|
||||
<Dialog open={showDeleteModal} onOpenChange={() => setShowDeleteModal(false)}>
|
||||
<Dialog open={showDeleteModal} onOpenChange={() => setShowDeleteModal((prev) => !prev)}>
|
||||
<ConfirmationDialogContent
|
||||
variety="danger"
|
||||
title={t("remove_member")}
|
||||
|
|
|
@ -70,13 +70,25 @@ const MembersView = () => {
|
|||
const session = useSession();
|
||||
|
||||
const utils = trpc.useContext();
|
||||
|
||||
const teamId = Number(router.query.id);
|
||||
|
||||
const showDialog = router.query.inviteModal === "true";
|
||||
const [showMemberInvitationModal, setShowMemberInvitationModal] = useState(showDialog);
|
||||
const [showInviteLinkSettingsModal, setInviteLinkSettingsModal] = useState(false);
|
||||
|
||||
const { data: team, isLoading } = trpc.viewer.teams.get.useQuery(
|
||||
const { data: orgMembersNotInThisTeam, isLoading: isOrgListLoading } =
|
||||
trpc.viewer.organizations.getMembers.useQuery(
|
||||
{
|
||||
teamIdToExclude: teamId,
|
||||
distinctUser: true,
|
||||
},
|
||||
{
|
||||
enabled: router.isReady,
|
||||
}
|
||||
);
|
||||
|
||||
const { data: team, isLoading: isTeamsLoading } = trpc.viewer.teams.get.useQuery(
|
||||
{ teamId },
|
||||
{
|
||||
onError: () => {
|
||||
|
@ -85,6 +97,8 @@ const MembersView = () => {
|
|||
}
|
||||
);
|
||||
|
||||
const isLoading = isOrgListLoading || isTeamsLoading;
|
||||
|
||||
const inviteMemberMutation = trpc.viewer.teams.inviteMember.useMutation();
|
||||
|
||||
const isInviteOpen = !team?.membership.accepted;
|
||||
|
@ -161,6 +175,7 @@ const MembersView = () => {
|
|||
<MemberInvitationModal
|
||||
isLoading={inviteMemberMutation.isLoading}
|
||||
isOpen={showMemberInvitationModal}
|
||||
orgMembers={orgMembersNotInThisTeam}
|
||||
members={team.members}
|
||||
teamId={team.id}
|
||||
token={team.inviteToken?.token}
|
||||
|
|
|
@ -19,9 +19,9 @@ type OrganizationsRouterHandlerCache = {
|
|||
setPassword?: typeof import("./setPassword.handler").setPasswordHandler;
|
||||
adminGetUnverified?: typeof import("./adminGetUnverified.handler").adminGetUnverifiedHandler;
|
||||
adminVerify?: typeof import("./adminVerify.handler").adminVerifyHandler;
|
||||
getMembers?: typeof import("./getMembers.handler").getMembersHandler;
|
||||
listMembers?: typeof import("./listMembers.handler").listMembersHandler;
|
||||
getBrand?: typeof import("./getBrand.handler").getBrandHandler;
|
||||
getMembers?: typeof import("./getMembers.handler").getMembersHandler;
|
||||
};
|
||||
|
||||
const UNSTABLE_HANDLER_CACHE: OrganizationsRouterHandlerCache = {};
|
||||
|
@ -146,6 +146,23 @@ export const viewerOrganizationsRouter = router({
|
|||
input,
|
||||
});
|
||||
}),
|
||||
getMembers: authedProcedure.input(ZGetMembersInput).query(async ({ ctx, input }) => {
|
||||
if (!UNSTABLE_HANDLER_CACHE.getMembers) {
|
||||
UNSTABLE_HANDLER_CACHE.getMembers = await import("./getMembers.handler").then(
|
||||
(mod) => mod.getMembersHandler
|
||||
);
|
||||
}
|
||||
|
||||
// Unreachable code but required for type safety
|
||||
if (!UNSTABLE_HANDLER_CACHE.getMembers) {
|
||||
throw new Error("Failed to load handler");
|
||||
}
|
||||
|
||||
return UNSTABLE_HANDLER_CACHE.getMembers({
|
||||
ctx,
|
||||
input,
|
||||
});
|
||||
}),
|
||||
adminGetUnverified: authedAdminProcedure.query(async ({ ctx }) => {
|
||||
if (!UNSTABLE_HANDLER_CACHE.adminGetUnverified) {
|
||||
UNSTABLE_HANDLER_CACHE.adminGetUnverified = await import("./adminGetUnverified.handler").then(
|
||||
|
@ -207,21 +224,4 @@ export const viewerOrganizationsRouter = router({
|
|||
ctx,
|
||||
});
|
||||
}),
|
||||
getMembers: authedProcedure.input(ZGetMembersInput).query(async ({ ctx, input }) => {
|
||||
if (!UNSTABLE_HANDLER_CACHE.getMembers) {
|
||||
UNSTABLE_HANDLER_CACHE.getMembers = await import("./getMembers.handler").then(
|
||||
(mod) => mod.getMembersHandler
|
||||
);
|
||||
}
|
||||
|
||||
// Unreachable code but required for type safety
|
||||
if (!UNSTABLE_HANDLER_CACHE.getMembers) {
|
||||
throw new Error("Failed to load handler");
|
||||
}
|
||||
|
||||
return UNSTABLE_HANDLER_CACHE.getMembers({
|
||||
ctx,
|
||||
input,
|
||||
});
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -11,32 +11,44 @@ type CreateOptions = {
|
|||
};
|
||||
|
||||
export const getMembersHandler = async ({ input, ctx }: CreateOptions) => {
|
||||
const { teamIdToExclude } = input;
|
||||
const { teamIdToExclude, accepted, distinctUser } = input;
|
||||
|
||||
if (!ctx.user.organizationId) return null;
|
||||
if (!ctx.user.organizationId) return [];
|
||||
|
||||
const users = await prisma.membership.findMany({
|
||||
const teamQuery = await prisma.team.findUnique({
|
||||
where: {
|
||||
user: {
|
||||
organizationId: ctx.user.organizationId,
|
||||
id: ctx.user.organizationId,
|
||||
},
|
||||
...(teamIdToExclude && {
|
||||
select: {
|
||||
members: {
|
||||
where: {
|
||||
teamId: {
|
||||
not: teamIdToExclude,
|
||||
},
|
||||
}),
|
||||
accepted,
|
||||
},
|
||||
include: {
|
||||
select: {
|
||||
accepted: true,
|
||||
disableImpersonation: true,
|
||||
id: true,
|
||||
teamId: true,
|
||||
role: true,
|
||||
userId: true,
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
email: true,
|
||||
completedOnboarding: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
...(distinctUser && {
|
||||
distinct: ["userId"],
|
||||
}),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return users;
|
||||
return teamQuery?.members || [];
|
||||
};
|
||||
|
|
|
@ -2,6 +2,8 @@ import { z } from "zod";
|
|||
|
||||
export const ZGetMembersInput = z.object({
|
||||
teamIdToExclude: z.number().optional(),
|
||||
accepted: z.boolean().optional(),
|
||||
distinctUser: z.boolean().optional().default(false),
|
||||
});
|
||||
|
||||
export type TGetMembersInputSchema = z.infer<typeof ZGetMembersInput>;
|
||||
|
|
|
@ -40,7 +40,8 @@ const mockedTeam: TeamWithParent = {
|
|||
timeFormat: null,
|
||||
metadata: null,
|
||||
parentId: null,
|
||||
parent: null
|
||||
parent: null,
|
||||
isPrivate:false
|
||||
};
|
||||
|
||||
const mockUser: User = {
|
||||
|
@ -263,7 +264,8 @@ describe("Invite Member Utils", () => {
|
|||
};
|
||||
const isOrg = false;
|
||||
|
||||
it("should throw a TRPCError with code FORBIDDEN if the invitee is already a member of another organization", () => {
|
||||
|
||||
it("should not throw when inviting an existing user to the same organization", () => {
|
||||
const inviteeWithOrg: User = {
|
||||
...invitee,
|
||||
organizationId: 2,
|
||||
|
@ -272,6 +274,19 @@ describe("Invite Member Utils", () => {
|
|||
...mockedTeam,
|
||||
parentId: 2,
|
||||
}
|
||||
expect(() =>
|
||||
throwIfInviteIsToOrgAndUserExists(inviteeWithOrg, teamWithOrg, isOrg)
|
||||
).not.toThrow();
|
||||
});
|
||||
it("should throw a TRPCError with code FORBIDDEN if the invitee is already a member of another organization", () => {
|
||||
const inviteeWithOrg: User = {
|
||||
...invitee,
|
||||
organizationId: 2,
|
||||
};
|
||||
const teamWithOrg = {
|
||||
...mockedTeam,
|
||||
parentId: 3,
|
||||
}
|
||||
expect(() =>
|
||||
throwIfInviteIsToOrgAndUserExists(inviteeWithOrg, teamWithOrg, isOrg)
|
||||
).toThrow(TRPCError);
|
||||
|
|
|
@ -120,7 +120,6 @@ export function getOrgConnectionInfo({
|
|||
if (usersEmail.split("@")[1] == orgAutoAcceptDomain) {
|
||||
autoAccept = orgVerified ?? true;
|
||||
} else {
|
||||
// No longer throw error - not needed we just dont auto accept them
|
||||
orgId = undefined;
|
||||
autoAccept = false;
|
||||
}
|
||||
|
@ -279,12 +278,17 @@ export async function sendVerificationEmail({
|
|||
}
|
||||
|
||||
export function throwIfInviteIsToOrgAndUserExists(invitee: User, team: TeamWithParent, isOrg: boolean) {
|
||||
if (invitee.organizationId && invitee.organizationId === team.parentId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (invitee.organizationId && invitee.organizationId !== team.parentId) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
message: `User ${invitee.username} is already a member of another organization.`,
|
||||
});
|
||||
}
|
||||
|
||||
if ((invitee && isOrg) || (team.parentId && invitee)) {
|
||||
throw new TRPCError({
|
||||
code: "FORBIDDEN",
|
||||
|
|
|
@ -40,7 +40,7 @@ export const ToggleGroup = ({ options, onValueChange, isFullWidth, ...props }: T
|
|||
{...props}
|
||||
onValueChange={onValueChange}
|
||||
className={classNames(
|
||||
"min-h-9 border-default relative inline-flex gap-0.5 rounded-md border p-1",
|
||||
"min-h-9 border-default bg-default relative inline-flex gap-0.5 rounded-md border p-1",
|
||||
props.className,
|
||||
isFullWidth && "w-full"
|
||||
)}>
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
"@calcom/lib": "*",
|
||||
"@calcom/trpc": "*",
|
||||
"@formkit/auto-animate": "^1.0.0-beta.5",
|
||||
"@radix-ui/react-dialog": "^1.0.0",
|
||||
"@radix-ui/react-dialog": "^1.0.4",
|
||||
"@radix-ui/react-popover": "^1.0.2",
|
||||
"@radix-ui/react-portal": "^1.0.0",
|
||||
"@radix-ui/react-select": "^0.1.1",
|
||||
|
|
Loading…
Reference in New Issue