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": "*",
|
"@calcom/ui": "*",
|
||||||
"@radix-ui/react-avatar": "^1.0.0",
|
"@radix-ui/react-avatar": "^1.0.0",
|
||||||
"@radix-ui/react-collapsible": "^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-dropdown-menu": "^2.0.5",
|
||||||
"@radix-ui/react-id": "^1.0.0",
|
"@radix-ui/react-id": "^1.0.0",
|
||||||
"@radix-ui/react-popover": "^1.0.2",
|
"@radix-ui/react-popover": "^1.0.2",
|
||||||
|
|
|
@ -47,7 +47,7 @@
|
||||||
"@next/bundle-analyzer": "^13.1.6",
|
"@next/bundle-analyzer": "^13.1.6",
|
||||||
"@radix-ui/react-avatar": "^1.0.0",
|
"@radix-ui/react-avatar": "^1.0.0",
|
||||||
"@radix-ui/react-collapsible": "^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-dropdown-menu": "^2.0.5",
|
||||||
"@radix-ui/react-id": "^1.0.0",
|
"@radix-ui/react-id": "^1.0.0",
|
||||||
"@radix-ui/react-popover": "^1.0.2",
|
"@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 type { FormEvent } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
|
||||||
|
import TeamInviteFromOrg from "@calcom/ee/organizations/components/TeamInviteFromOrg";
|
||||||
import { classNames } from "@calcom/lib";
|
import { classNames } from "@calcom/lib";
|
||||||
import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants";
|
import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
@ -52,17 +53,24 @@ export interface NewMemberForm {
|
||||||
sendInviteEmail: boolean;
|
sendInviteEmail: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
type ModalMode = "INDIVIDUAL" | "BULK";
|
type ModalMode = "INDIVIDUAL" | "BULK" | "ORGANIZATION";
|
||||||
|
|
||||||
interface FileEvent<T = Element> extends FormEvent<T> {
|
interface FileEvent<T = Element> extends FormEvent<T> {
|
||||||
target: EventTarget & 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) {
|
export default function MemberInvitationModal(props: MemberInvitationModalProps) {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const trpcContext = trpc.useContext();
|
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({
|
const createInviteMutation = trpc.viewer.teams.createInvite.useMutation({
|
||||||
onSuccess(token) {
|
onSuccess(token) {
|
||||||
|
@ -98,7 +106,7 @@ export default function MemberInvitationModal(props: MemberInvitationModalProps)
|
||||||
},
|
},
|
||||||
{ value: "BULK", label: t("invite_team_bulk_segment"), iconLeft: <Users /> },
|
{ value: "BULK", label: t("invite_team_bulk_segment"), iconLeft: <Users /> },
|
||||||
];
|
];
|
||||||
if (props.orgMembers) {
|
if (props?.orgMembers && props.orgMembers?.length > 0) {
|
||||||
array.unshift({
|
array.unshift({
|
||||||
value: "ORGANIZATION",
|
value: "ORGANIZATION",
|
||||||
label: t("organization"),
|
label: t("organization"),
|
||||||
|
@ -170,7 +178,7 @@ export default function MemberInvitationModal(props: MemberInvitationModalProps)
|
||||||
<ToggleGroup
|
<ToggleGroup
|
||||||
isFullWidth={true}
|
isFullWidth={true}
|
||||||
onValueChange={(val) => setModalInputMode(val as ModalMode)}
|
onValueChange={(val) => setModalInputMode(val as ModalMode)}
|
||||||
defaultValue="INDIVIDUAL"
|
defaultValue={modalImportMode}
|
||||||
options={toggleGroupOptions}
|
options={toggleGroupOptions}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -260,6 +268,29 @@ export default function MemberInvitationModal(props: MemberInvitationModalProps)
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</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
|
<Controller
|
||||||
name="role"
|
name="role"
|
||||||
control={newMemberFormMethods.control}
|
control={newMemberFormMethods.control}
|
||||||
|
|
|
@ -266,7 +266,7 @@ export default function MemberListItem(props: Props) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{editMode && (
|
{editMode && (
|
||||||
<Dialog open={showDeleteModal} onOpenChange={() => setShowDeleteModal(false)}>
|
<Dialog open={showDeleteModal} onOpenChange={() => setShowDeleteModal((prev) => !prev)}>
|
||||||
<ConfirmationDialogContent
|
<ConfirmationDialogContent
|
||||||
variety="danger"
|
variety="danger"
|
||||||
title={t("remove_member")}
|
title={t("remove_member")}
|
||||||
|
|
|
@ -70,13 +70,25 @@ const MembersView = () => {
|
||||||
const session = useSession();
|
const session = useSession();
|
||||||
|
|
||||||
const utils = trpc.useContext();
|
const utils = trpc.useContext();
|
||||||
|
|
||||||
const teamId = Number(router.query.id);
|
const teamId = Number(router.query.id);
|
||||||
|
|
||||||
const showDialog = router.query.inviteModal === "true";
|
const showDialog = router.query.inviteModal === "true";
|
||||||
const [showMemberInvitationModal, setShowMemberInvitationModal] = useState(showDialog);
|
const [showMemberInvitationModal, setShowMemberInvitationModal] = useState(showDialog);
|
||||||
const [showInviteLinkSettingsModal, setInviteLinkSettingsModal] = useState(false);
|
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 },
|
{ teamId },
|
||||||
{
|
{
|
||||||
onError: () => {
|
onError: () => {
|
||||||
|
@ -85,6 +97,8 @@ const MembersView = () => {
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isLoading = isOrgListLoading || isTeamsLoading;
|
||||||
|
|
||||||
const inviteMemberMutation = trpc.viewer.teams.inviteMember.useMutation();
|
const inviteMemberMutation = trpc.viewer.teams.inviteMember.useMutation();
|
||||||
|
|
||||||
const isInviteOpen = !team?.membership.accepted;
|
const isInviteOpen = !team?.membership.accepted;
|
||||||
|
@ -161,6 +175,7 @@ const MembersView = () => {
|
||||||
<MemberInvitationModal
|
<MemberInvitationModal
|
||||||
isLoading={inviteMemberMutation.isLoading}
|
isLoading={inviteMemberMutation.isLoading}
|
||||||
isOpen={showMemberInvitationModal}
|
isOpen={showMemberInvitationModal}
|
||||||
|
orgMembers={orgMembersNotInThisTeam}
|
||||||
members={team.members}
|
members={team.members}
|
||||||
teamId={team.id}
|
teamId={team.id}
|
||||||
token={team.inviteToken?.token}
|
token={team.inviteToken?.token}
|
||||||
|
|
|
@ -19,9 +19,9 @@ type OrganizationsRouterHandlerCache = {
|
||||||
setPassword?: typeof import("./setPassword.handler").setPasswordHandler;
|
setPassword?: typeof import("./setPassword.handler").setPasswordHandler;
|
||||||
adminGetUnverified?: typeof import("./adminGetUnverified.handler").adminGetUnverifiedHandler;
|
adminGetUnverified?: typeof import("./adminGetUnverified.handler").adminGetUnverifiedHandler;
|
||||||
adminVerify?: typeof import("./adminVerify.handler").adminVerifyHandler;
|
adminVerify?: typeof import("./adminVerify.handler").adminVerifyHandler;
|
||||||
getMembers?: typeof import("./getMembers.handler").getMembersHandler;
|
|
||||||
listMembers?: typeof import("./listMembers.handler").listMembersHandler;
|
listMembers?: typeof import("./listMembers.handler").listMembersHandler;
|
||||||
getBrand?: typeof import("./getBrand.handler").getBrandHandler;
|
getBrand?: typeof import("./getBrand.handler").getBrandHandler;
|
||||||
|
getMembers?: typeof import("./getMembers.handler").getMembersHandler;
|
||||||
};
|
};
|
||||||
|
|
||||||
const UNSTABLE_HANDLER_CACHE: OrganizationsRouterHandlerCache = {};
|
const UNSTABLE_HANDLER_CACHE: OrganizationsRouterHandlerCache = {};
|
||||||
|
@ -146,6 +146,23 @@ export const viewerOrganizationsRouter = router({
|
||||||
input,
|
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 }) => {
|
adminGetUnverified: authedAdminProcedure.query(async ({ ctx }) => {
|
||||||
if (!UNSTABLE_HANDLER_CACHE.adminGetUnverified) {
|
if (!UNSTABLE_HANDLER_CACHE.adminGetUnverified) {
|
||||||
UNSTABLE_HANDLER_CACHE.adminGetUnverified = await import("./adminGetUnverified.handler").then(
|
UNSTABLE_HANDLER_CACHE.adminGetUnverified = await import("./adminGetUnverified.handler").then(
|
||||||
|
@ -207,21 +224,4 @@ export const viewerOrganizationsRouter = router({
|
||||||
ctx,
|
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) => {
|
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: {
|
where: {
|
||||||
user: {
|
id: ctx.user.organizationId,
|
||||||
organizationId: ctx.user.organizationId,
|
|
||||||
},
|
},
|
||||||
...(teamIdToExclude && {
|
select: {
|
||||||
|
members: {
|
||||||
|
where: {
|
||||||
teamId: {
|
teamId: {
|
||||||
not: teamIdToExclude,
|
not: teamIdToExclude,
|
||||||
},
|
},
|
||||||
}),
|
accepted,
|
||||||
},
|
},
|
||||||
include: {
|
select: {
|
||||||
|
accepted: true,
|
||||||
|
disableImpersonation: true,
|
||||||
|
id: true,
|
||||||
|
teamId: true,
|
||||||
|
role: true,
|
||||||
|
userId: true,
|
||||||
user: {
|
user: {
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
username: true,
|
username: true,
|
||||||
email: true,
|
email: true,
|
||||||
completedOnboarding: true,
|
completedOnboarding: true,
|
||||||
|
name: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
...(distinctUser && {
|
||||||
|
distinct: ["userId"],
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
return teamQuery?.members || [];
|
||||||
return users;
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,6 +2,8 @@ import { z } from "zod";
|
||||||
|
|
||||||
export const ZGetMembersInput = z.object({
|
export const ZGetMembersInput = z.object({
|
||||||
teamIdToExclude: z.number().optional(),
|
teamIdToExclude: z.number().optional(),
|
||||||
|
accepted: z.boolean().optional(),
|
||||||
|
distinctUser: z.boolean().optional().default(false),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TGetMembersInputSchema = z.infer<typeof ZGetMembersInput>;
|
export type TGetMembersInputSchema = z.infer<typeof ZGetMembersInput>;
|
||||||
|
|
|
@ -40,7 +40,8 @@ const mockedTeam: TeamWithParent = {
|
||||||
timeFormat: null,
|
timeFormat: null,
|
||||||
metadata: null,
|
metadata: null,
|
||||||
parentId: null,
|
parentId: null,
|
||||||
parent: null
|
parent: null,
|
||||||
|
isPrivate:false
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockUser: User = {
|
const mockUser: User = {
|
||||||
|
@ -263,7 +264,8 @@ describe("Invite Member Utils", () => {
|
||||||
};
|
};
|
||||||
const isOrg = false;
|
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 = {
|
const inviteeWithOrg: User = {
|
||||||
...invitee,
|
...invitee,
|
||||||
organizationId: 2,
|
organizationId: 2,
|
||||||
|
@ -272,6 +274,19 @@ describe("Invite Member Utils", () => {
|
||||||
...mockedTeam,
|
...mockedTeam,
|
||||||
parentId: 2,
|
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(() =>
|
expect(() =>
|
||||||
throwIfInviteIsToOrgAndUserExists(inviteeWithOrg, teamWithOrg, isOrg)
|
throwIfInviteIsToOrgAndUserExists(inviteeWithOrg, teamWithOrg, isOrg)
|
||||||
).toThrow(TRPCError);
|
).toThrow(TRPCError);
|
||||||
|
|
|
@ -120,7 +120,6 @@ export function getOrgConnectionInfo({
|
||||||
if (usersEmail.split("@")[1] == orgAutoAcceptDomain) {
|
if (usersEmail.split("@")[1] == orgAutoAcceptDomain) {
|
||||||
autoAccept = orgVerified ?? true;
|
autoAccept = orgVerified ?? true;
|
||||||
} else {
|
} else {
|
||||||
// No longer throw error - not needed we just dont auto accept them
|
|
||||||
orgId = undefined;
|
orgId = undefined;
|
||||||
autoAccept = false;
|
autoAccept = false;
|
||||||
}
|
}
|
||||||
|
@ -279,12 +278,17 @@ export async function sendVerificationEmail({
|
||||||
}
|
}
|
||||||
|
|
||||||
export function throwIfInviteIsToOrgAndUserExists(invitee: User, team: TeamWithParent, isOrg: boolean) {
|
export function throwIfInviteIsToOrgAndUserExists(invitee: User, team: TeamWithParent, isOrg: boolean) {
|
||||||
|
if (invitee.organizationId && invitee.organizationId === team.parentId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (invitee.organizationId && invitee.organizationId !== team.parentId) {
|
if (invitee.organizationId && invitee.organizationId !== team.parentId) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "FORBIDDEN",
|
code: "FORBIDDEN",
|
||||||
message: `User ${invitee.username} is already a member of another organization.`,
|
message: `User ${invitee.username} is already a member of another organization.`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((invitee && isOrg) || (team.parentId && invitee)) {
|
if ((invitee && isOrg) || (team.parentId && invitee)) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "FORBIDDEN",
|
code: "FORBIDDEN",
|
||||||
|
|
|
@ -40,7 +40,7 @@ export const ToggleGroup = ({ options, onValueChange, isFullWidth, ...props }: T
|
||||||
{...props}
|
{...props}
|
||||||
onValueChange={onValueChange}
|
onValueChange={onValueChange}
|
||||||
className={classNames(
|
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,
|
props.className,
|
||||||
isFullWidth && "w-full"
|
isFullWidth && "w-full"
|
||||||
)}>
|
)}>
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
"@calcom/lib": "*",
|
"@calcom/lib": "*",
|
||||||
"@calcom/trpc": "*",
|
"@calcom/trpc": "*",
|
||||||
"@formkit/auto-animate": "^1.0.0-beta.5",
|
"@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-popover": "^1.0.2",
|
||||||
"@radix-ui/react-portal": "^1.0.0",
|
"@radix-ui/react-portal": "^1.0.0",
|
||||||
"@radix-ui/react-select": "^0.1.1",
|
"@radix-ui/react-select": "^0.1.1",
|
||||||
|
|
Loading…
Reference in New Issue