feat: Org user table - bulk actions (#10504)
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com> Co-authored-by: Hariom Balhara <hariombalhara@gmail.com> Co-authored-by: Leo Giovanetti <hello@leog.me> Co-authored-by: Alex van Andel <me@alexvanandel.com> Co-authored-by: CarinaWolli <wollencarina@gmail.com> Co-authored-by: zomars <zomars@me.com> Co-authored-by: Peer Richelsen <peeroke@gmail.com> Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com> Co-authored-by: Udit Takkar <udit.07814802719@cse.mait.ac.in> Co-authored-by: Keith Williams <keithwillcode@gmail.com> Co-authored-by: Peer Richelsen <peer@cal.com> Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com> Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com> Co-authored-by: Shivam Kalra <shivamkalra98@gmail.com> Co-authored-by: cherish2003 <saicherissh90@gmail.com> Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com> Co-authored-by: rkreddy99 <rreddy@e2clouds.com> Co-authored-by: varun thummar <Varun> Co-authored-by: Crowdin Bot <support+bot@crowdin.com> Co-authored-by: Pradumn Kumar <47187878+Pradumn27@users.noreply.github.com> Co-authored-by: Richard Poelderl <richard.poelderl@gmail.com> Co-authored-by: mohammed hussam <hussamkhatib20@gmail.com> Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com> Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com> Co-authored-by: nicktrn <55853254+nicktrn@users.noreply.github.com> Co-authored-by: sydwardrae <94979838+sydwardrae@users.noreply.github.com> Co-authored-by: Janakiram Yellapu <jyellapu@vmware.com> Co-authored-by: GitStart-Cal.com <121884634+gitstart-calcom@users.noreply.github.com> Co-authored-by: sajanlamsal <saznlamsal@gmail.com> Co-authored-by: Cherish <88829894+cherish2003@users.noreply.github.com> Co-authored-by: Danila <daniil.demidovich@gmail.com> Co-authored-by: Neel Patel <29038590+N-NeelPatel@users.noreply.github.com> Co-authored-by: Rama Krishna Reddy <49095575+rkreddy99@users.noreply.github.com> Co-authored-by: Varun Thummar <110765105+VARUN949@users.noreply.github.com> Co-authored-by: Bhargav <bhargavtenali@gmail.com> Co-authored-by: Pratik Kumar <kpratik1929@gmail.com> Co-authored-by: Ritesh Patil <riteshsp2000@gmail.com>pull/10791/head^2
parent
20c45b5d88
commit
10ffd9bacd
|
@ -166,6 +166,7 @@
|
|||
"msw": "^0.42.3",
|
||||
"postcss": "^8.4.18",
|
||||
"tailwindcss": "^3.3.1",
|
||||
"tailwindcss-animate": "^1.0.6",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^4.9.4"
|
||||
},
|
||||
|
|
|
@ -1265,6 +1265,7 @@
|
|||
"error_updating_settings": "Error updating settings",
|
||||
"personal_cal_url": "My personal {{appName}} URL",
|
||||
"bio_hint": "A few sentences about yourself. this will appear on your personal url page.",
|
||||
"user_has_no_bio": "This user has not added a bio yet.",
|
||||
"delete_account_modal_title": "Delete Account",
|
||||
"confirm_delete_account_modal": "Are you sure you want to delete your {{appName}} account?",
|
||||
"delete_my_account": "Delete my account",
|
||||
|
@ -1982,6 +1983,11 @@
|
|||
"org_team_names_example_5": "e.g. Data Analytics Team",
|
||||
"org_max_team_warnings": "You will be able to add more teams later on.",
|
||||
"what_is_this_meeting_about": "What is this meeting about?",
|
||||
"add_to_team":"Add to team",
|
||||
"remove_users_from_org": "Remove users from organization",
|
||||
"remove_users_from_org_confirm":"Are you sure you want to remove {{userCount}} users from this organization?",
|
||||
"user_has_no_schedules":"This user has not setup any schedules yet",
|
||||
"user_isnt_in_any_teams":"This user is not in any teams",
|
||||
"requires_booker_email_verification": "Requires booker email verification",
|
||||
"description_requires_booker_email_verification": "To ensure booker's email verification before scheduling events",
|
||||
"requires_confirmation_mandatory": "Text messages can only be sent to attendees when event type requires confirmation.",
|
||||
|
|
|
@ -3,4 +3,5 @@ const base = require("@calcom/config/tailwind-preset");
|
|||
module.exports = {
|
||||
...base,
|
||||
content: [...base.content, "../../node_modules/@tremor/**/*.{js,ts,jsx,tsx}"],
|
||||
plugins: [...base.plugins, require("tailwindcss-animate")],
|
||||
};
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
import { BanIcon } from "lucide-react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Dialog, DialogTrigger, ConfirmationDialogContent, Button, showToast } from "@calcom/ui";
|
||||
|
||||
import type { User } from "../UserListTable";
|
||||
|
||||
interface Props {
|
||||
users: User[];
|
||||
}
|
||||
|
||||
export function DeleteBulkUsers({ users }: Props) {
|
||||
const { t } = useLocale();
|
||||
const selectedRows = users; // Get selected rows from table
|
||||
const utils = trpc.useContext();
|
||||
const deleteMutation = trpc.viewer.organizations.bulkDeleteUsers.useMutation({
|
||||
onSuccess: () => {
|
||||
utils.viewer.organizations.listMembers.invalidate();
|
||||
showToast("Deleted Users", "success");
|
||||
},
|
||||
onError: (error) => {
|
||||
showToast(error.message, "error");
|
||||
},
|
||||
});
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button StartIcon={BanIcon}>{t("Delete")}</Button>
|
||||
</DialogTrigger>
|
||||
<ConfirmationDialogContent
|
||||
variety="danger"
|
||||
title={t("remove_users_from_org")}
|
||||
confirmBtnText={t("remove")}
|
||||
isLoading={deleteMutation.isLoading}
|
||||
onConfirm={() => {
|
||||
deleteMutation.mutateAsync({
|
||||
userIds: selectedRows.map((user) => user.id),
|
||||
});
|
||||
}}>
|
||||
<p className="mt-5">
|
||||
{t("remove_users_from_org_confirm", {
|
||||
userCount: selectedRows.length,
|
||||
})}
|
||||
</p>
|
||||
</ConfirmationDialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,126 @@
|
|||
import type { Table } from "@tanstack/react-table";
|
||||
import { Users, Check } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc";
|
||||
import {
|
||||
Command,
|
||||
CommandInput,
|
||||
CommandList,
|
||||
CommandItem,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
Button,
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
showToast,
|
||||
} from "@calcom/ui";
|
||||
|
||||
import type { User } from "../UserListTable";
|
||||
|
||||
interface Props {
|
||||
table: Table<User>;
|
||||
}
|
||||
|
||||
export function TeamListBulkAction({ table }: Props) {
|
||||
const { data: teams } = trpc.viewer.organizations.getTeams.useQuery();
|
||||
const [selectedValues, setSelectedValues] = useState<Set<number>>(new Set());
|
||||
const utils = trpc.useContext();
|
||||
const mutation = trpc.viewer.organizations.bulkAddToTeams.useMutation({
|
||||
onError: (error) => {
|
||||
showToast(error.message, "error");
|
||||
},
|
||||
onSuccess: (res) => {
|
||||
showToast(
|
||||
`${res.invitedTotalUsers} Users invited to ${Array.from(selectedValues).length} teams`,
|
||||
"success"
|
||||
);
|
||||
// Optimistically update the data from query trpc cache listMembers
|
||||
// We may need to set this data instread of invalidating. Will see how performance handles it
|
||||
utils.viewer.organizations.listMembers.invalidate();
|
||||
|
||||
// Clear the selected values
|
||||
setSelectedValues(new Set());
|
||||
table.toggleAllRowsSelected(false);
|
||||
},
|
||||
});
|
||||
|
||||
const { t } = useLocale();
|
||||
|
||||
// Add a value to the set
|
||||
const addValue = (value: number) => {
|
||||
const updatedSet = new Set(selectedValues);
|
||||
updatedSet.add(value);
|
||||
setSelectedValues(updatedSet);
|
||||
};
|
||||
|
||||
// Remove a value from the set
|
||||
const removeValue = (value: number) => {
|
||||
const updatedSet = new Set(selectedValues);
|
||||
updatedSet.delete(value);
|
||||
setSelectedValues(updatedSet);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button StartIcon={Users}>{t("add_to_team")}</Button>
|
||||
</PopoverTrigger>
|
||||
{/* We dont really use shadows much - but its needed here */}
|
||||
<PopoverContent className="w-[200px] p-0 shadow-md" align="start" sideOffset={12}>
|
||||
<Command>
|
||||
<CommandInput placeholder={t("search")} />
|
||||
<CommandList>
|
||||
<CommandEmpty>No results found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{teams &&
|
||||
teams.map((option) => {
|
||||
const isSelected = selectedValues.has(option.id);
|
||||
return (
|
||||
<CommandItem
|
||||
key={option.id}
|
||||
onSelect={() => {
|
||||
if (!isSelected) {
|
||||
addValue(option.id);
|
||||
} else {
|
||||
removeValue(option.id);
|
||||
}
|
||||
}}>
|
||||
<span>{option.name}</span>
|
||||
<div
|
||||
className={classNames(
|
||||
"border-subtle ml-auto flex h-4 w-4 items-center justify-center rounded-sm border",
|
||||
isSelected ? "text-emphasis" : "opacity-50 [&_svg]:invisible"
|
||||
)}>
|
||||
<Check className={classNames("h-4 w-4")} />
|
||||
</div>
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
<div className="my-1.5 flex w-full">
|
||||
<Button
|
||||
loading={mutation.isLoading}
|
||||
className="ml-auto mr-1.5 rounded-md"
|
||||
size="sm"
|
||||
onClick={async () => {
|
||||
const selectedRows = table.getSelectedRowModel().flatRows.map((row) => row.original);
|
||||
mutation.mutateAsync({
|
||||
userIds: selectedRows.map((row) => row.id),
|
||||
teamIds: Array.from(selectedValues),
|
||||
});
|
||||
}}>
|
||||
{t("apply")}
|
||||
</Button>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
import { classNames } from "@calcom/lib";
|
||||
import { useCopy } from "@calcom/lib/hooks/useCopy";
|
||||
import type { BadgeProps } from "@calcom/ui";
|
||||
import { Badge, Button, Label } from "@calcom/ui";
|
||||
import { ClipboardCheck, Clipboard } from "@calcom/ui/components/icon";
|
||||
|
||||
type DisplayInfoType<T extends boolean> = {
|
||||
label: string;
|
||||
value: T extends true ? string[] : string;
|
||||
asBadge?: boolean;
|
||||
isArray?: T;
|
||||
displayCopy?: boolean;
|
||||
badgeColor?: BadgeProps["variant"];
|
||||
} & (T extends false
|
||||
? { displayCopy?: boolean; displayCount?: never }
|
||||
: { displayCopy?: never; displayCount?: number }); // Only show displayCopy if its not an array is false
|
||||
|
||||
export function DisplayInfo<T extends boolean>({
|
||||
label,
|
||||
value,
|
||||
asBadge,
|
||||
isArray,
|
||||
displayCopy,
|
||||
displayCount,
|
||||
badgeColor,
|
||||
}: DisplayInfoType<T>) {
|
||||
const { copyToClipboard, isCopied } = useCopy();
|
||||
const values = (isArray ? value : [value]) as string[];
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<Label className="text-subtle mb-1 text-xs font-semibold uppercase leading-none">
|
||||
{label} {displayCount && `(${displayCount})`}
|
||||
</Label>
|
||||
<div className={classNames(asBadge ? "mt-0.5 flex space-x-2" : "flex flex-col")}>
|
||||
<>
|
||||
{values.map((v) => {
|
||||
const content = (
|
||||
<span
|
||||
className={classNames(
|
||||
"text-emphasis inline-flex items-center gap-1 font-normal leading-5",
|
||||
asBadge ? "text-xs" : "text-sm"
|
||||
)}>
|
||||
{v}
|
||||
{displayCopy && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="icon"
|
||||
onClick={() => copyToClipboard(v)}
|
||||
color="minimal"
|
||||
className="text-subtle rounded-md"
|
||||
StartIcon={isCopied ? ClipboardCheck : Clipboard}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
|
||||
return asBadge ? (
|
||||
<Badge variant={badgeColor} size="sm">
|
||||
{content}
|
||||
</Badge>
|
||||
) : (
|
||||
content
|
||||
);
|
||||
})}
|
||||
</>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,142 @@
|
|||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import type { Dispatch } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import shallow from "zustand/shallow";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc, type RouterOutputs } from "@calcom/trpc/react";
|
||||
import {
|
||||
Form,
|
||||
TextField,
|
||||
ToggleGroup,
|
||||
TextAreaField,
|
||||
TimezoneSelect,
|
||||
Label,
|
||||
showToast,
|
||||
Avatar,
|
||||
} from "@calcom/ui";
|
||||
|
||||
import type { Action } from "../UserListTable";
|
||||
import { useEditMode } from "./store";
|
||||
|
||||
const editSchema = z.object({
|
||||
name: z.string(),
|
||||
email: z.string().email(),
|
||||
bio: z.string(),
|
||||
role: z.enum(["ADMIN", "MEMBER"]),
|
||||
timeZone: z.string(),
|
||||
// schedules: z.array(z.string()),
|
||||
// teams: z.array(z.string()),
|
||||
});
|
||||
|
||||
type EditSchema = z.infer<typeof editSchema>;
|
||||
|
||||
export function EditForm({
|
||||
selectedUser,
|
||||
avatarUrl,
|
||||
domainUrl,
|
||||
dispatch,
|
||||
}: {
|
||||
selectedUser: RouterOutputs["viewer"]["organizations"]["getUser"];
|
||||
avatarUrl: string;
|
||||
domainUrl: string;
|
||||
dispatch: Dispatch<Action>;
|
||||
}) {
|
||||
const [setMutationLoading] = useEditMode((state) => [state.setMutationloading], shallow);
|
||||
const { t } = useLocale();
|
||||
const utils = trpc.useContext();
|
||||
const form = useForm({
|
||||
resolver: zodResolver(editSchema),
|
||||
defaultValues: {
|
||||
name: selectedUser?.name ?? "",
|
||||
email: selectedUser?.email ?? "",
|
||||
bio: selectedUser?.bio ?? "",
|
||||
role: selectedUser?.role ?? "",
|
||||
timeZone: selectedUser?.timeZone ?? "",
|
||||
},
|
||||
});
|
||||
|
||||
const mutation = trpc.viewer.organizations.updateUser.useMutation({
|
||||
onSuccess: () => {
|
||||
dispatch({ type: "CLOSE_MODAL" });
|
||||
utils.viewer.organizations.listMembers.invalidate();
|
||||
showToast(t("profile_updated_successfully"), "success");
|
||||
},
|
||||
onError: (error) => {
|
||||
showToast(error.message, "error");
|
||||
},
|
||||
onSettled: () => {
|
||||
/**
|
||||
* /We need to do this as the submit button lives out side
|
||||
* the form for some complicated reason so we can't relay on mutationState
|
||||
*/
|
||||
setMutationLoading(false);
|
||||
},
|
||||
});
|
||||
|
||||
const watchTimezone = form.watch("timeZone");
|
||||
|
||||
return (
|
||||
<Form
|
||||
form={form}
|
||||
id="edit-user-form"
|
||||
handleSubmit={(values) => {
|
||||
setMutationLoading(true);
|
||||
mutation.mutate({
|
||||
userId: selectedUser?.id ?? "",
|
||||
role: values.role as "ADMIN" | "MEMBER", // Cast needed as we dont provide an option for owner
|
||||
name: values.name,
|
||||
email: values.email,
|
||||
bio: values.bio,
|
||||
timeZone: values.timeZone,
|
||||
});
|
||||
}}>
|
||||
<div className="mt-4 flex items-center gap-2">
|
||||
<Avatar
|
||||
size="lg"
|
||||
alt={`${selectedUser?.name} avatar`}
|
||||
imageSrc={avatarUrl}
|
||||
gravatarFallbackMd5="fallback"
|
||||
/>
|
||||
<div className="space-between flex flex-col leading-none">
|
||||
<span className="text-emphasis text-lg font-semibold">{selectedUser?.name ?? "Nameless User"}</span>
|
||||
<p className="subtle text-sm font-normal">
|
||||
{domainUrl}/{selectedUser?.username}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6 flex flex-col space-y-3">
|
||||
<TextField label={t("name")} {...form.register("name")} />
|
||||
<TextField label={t("email")} {...form.register("email")} />
|
||||
|
||||
<TextAreaField label={t("bio")} {...form.register("bio")} className="min-h-52" />
|
||||
<div>
|
||||
<Label>{t("role")}</Label>
|
||||
<ToggleGroup
|
||||
isFullWidth
|
||||
defaultValue={selectedUser?.role ?? "MEMBER"}
|
||||
value={form.watch("role")}
|
||||
options={[
|
||||
{
|
||||
value: "MEMBER",
|
||||
label: t("member"),
|
||||
},
|
||||
{
|
||||
value: "ADMIN",
|
||||
label: t("admin"),
|
||||
},
|
||||
]}
|
||||
onValueChange={(value: EditSchema["role"]) => {
|
||||
form.setValue("role", value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>{t("timezone")}</Label>
|
||||
<TimezoneSelect value={watchTimezone ?? "America/Los_Angeles"} />
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
import type { Dispatch } from "react";
|
||||
import { shallow } from "zustand/shallow";
|
||||
|
||||
import { useOrgBranding } from "@calcom/ee/organizations/context/provider";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Sheet, SheetContent, SheetFooter, Avatar, Skeleton, Loader } from "@calcom/ui";
|
||||
|
||||
import type { State, Action } from "../UserListTable";
|
||||
import { DisplayInfo } from "./DisplayInfo";
|
||||
import { EditForm } from "./EditUserForm";
|
||||
import { SheetFooterControls } from "./SheetFooterControls";
|
||||
import { useEditMode } from "./store";
|
||||
|
||||
export function EditUserSheet({ state, dispatch }: { state: State; dispatch: Dispatch<Action> }) {
|
||||
const { t } = useLocale();
|
||||
const { user: selectedUser } = state.editSheet;
|
||||
const orgBranding = useOrgBranding();
|
||||
const [editMode, setEditMode] = useEditMode((state) => [state.editMode, state.setEditMode], shallow);
|
||||
const { data: loadedUser, isLoading } = trpc.viewer.organizations.getUser.useQuery({
|
||||
userId: selectedUser?.id,
|
||||
});
|
||||
|
||||
const avatarURL = `${orgBranding?.fullDomain ?? WEBAPP_URL}/${loadedUser?.username}/avatar.png`;
|
||||
|
||||
const schedulesNames = loadedUser?.schedules && loadedUser?.schedules.map((s) => s.name);
|
||||
const teamNames =
|
||||
loadedUser?.teams && loadedUser?.teams.map((t) => `${t.name} ${!t.accepted ? "(pending)" : ""}`);
|
||||
|
||||
return (
|
||||
<Sheet
|
||||
open={true}
|
||||
onOpenChange={() => {
|
||||
setEditMode(false);
|
||||
dispatch({ type: "CLOSE_MODAL" });
|
||||
}}>
|
||||
<SheetContent position="right" size="default">
|
||||
{!isLoading && loadedUser ? (
|
||||
<div className="flex h-full flex-col">
|
||||
{!editMode ? (
|
||||
<div className="flex-grow">
|
||||
<div className="mt-4 flex items-center gap-2">
|
||||
<Avatar
|
||||
asChild
|
||||
className="h-[36px] w-[36px]"
|
||||
alt={`${loadedUser?.name} avatar`}
|
||||
imageSrc={avatarURL}
|
||||
gravatarFallbackMd5="fallback"
|
||||
/>
|
||||
<div className="space-between flex flex-col leading-none">
|
||||
<Skeleton loading={isLoading} as="p" waitForTranslation={false}>
|
||||
<span className="text-emphasis text-lg font-semibold">
|
||||
{loadedUser?.name ?? "Nameless User"}
|
||||
</span>
|
||||
</Skeleton>
|
||||
<Skeleton loading={isLoading} as="p" waitForTranslation={false}>
|
||||
<p className="subtle text-sm font-normal">
|
||||
{orgBranding?.fullDomain ?? WEBAPP_URL}/{loadedUser?.username}
|
||||
</p>
|
||||
</Skeleton>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-6 flex flex-col space-y-5">
|
||||
<DisplayInfo label={t("email")} value={loadedUser?.email ?? ""} displayCopy />
|
||||
<DisplayInfo
|
||||
label={t("bio")}
|
||||
badgeColor="gray"
|
||||
value={loadedUser?.bio ? loadedUser?.bio : t("user_has_no_bio")}
|
||||
/>
|
||||
<DisplayInfo label={t("role")} value={loadedUser?.role ?? ""} asBadge badgeColor="blue" />
|
||||
<DisplayInfo label={t("timezone")} value={loadedUser?.timeZone ?? ""} />
|
||||
<DisplayInfo
|
||||
label={t("availability_schedules")}
|
||||
value={
|
||||
schedulesNames && schedulesNames?.length === 0
|
||||
? [t("user_has_no_schedules")]
|
||||
: schedulesNames ?? "" // TS wtf
|
||||
}
|
||||
/>
|
||||
<DisplayInfo
|
||||
label={t("teams")}
|
||||
displayCount={teamNames?.length ?? 0}
|
||||
value={
|
||||
teamNames && teamNames?.length === 0 ? [t("user_isnt_in_any_teams")] : teamNames ?? "" // TS wtf
|
||||
}
|
||||
asBadge={teamNames && teamNames?.length > 0}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex-grow">
|
||||
<EditForm
|
||||
selectedUser={loadedUser}
|
||||
avatarUrl={avatarURL}
|
||||
domainUrl={orgBranding?.fullDomain ?? WEBAPP_URL}
|
||||
dispatch={dispatch}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<SheetFooter className="mt-auto">
|
||||
<SheetFooterControls />
|
||||
</SheetFooter>
|
||||
</div>
|
||||
) : (
|
||||
<Loader />
|
||||
)}
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,60 @@
|
|||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { SheetClose, Button } from "@calcom/ui";
|
||||
import { Pencil } from "@calcom/ui/components/icon";
|
||||
|
||||
import { useEditMode } from "./store";
|
||||
|
||||
function EditModeFooter() {
|
||||
const { t } = useLocale();
|
||||
const setEditMode = useEditMode((state) => state.setEditMode);
|
||||
const isLoading = useEditMode((state) => state.mutationLoading);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
color="secondary"
|
||||
type="button"
|
||||
className="justify-center md:w-1/5"
|
||||
onClick={() => {
|
||||
setEditMode(false);
|
||||
}}>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
|
||||
<Button type="submit" className="w-full justify-center" form="edit-user-form" loading={isLoading}>
|
||||
{t("update")}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function MoreInfoFooter() {
|
||||
const { t } = useLocale();
|
||||
const setEditMode = useEditMode((state) => state.setEditMode);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SheetClose asChild>
|
||||
<Button color="secondary" type="button" className="justify-center md:w-1/5">
|
||||
{t("close")}
|
||||
</Button>
|
||||
</SheetClose>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setEditMode(true);
|
||||
}}
|
||||
className="w-full justify-center gap-2"
|
||||
variant="icon"
|
||||
key="EDIT_BUTTON"
|
||||
StartIcon={Pencil}>
|
||||
{t("edit")}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function SheetFooterControls() {
|
||||
const editMode = useEditMode((state) => state.editMode);
|
||||
return <>{editMode ? <EditModeFooter /> : <MoreInfoFooter />}</>;
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
import { create } from "zustand";
|
||||
|
||||
interface EditModeState {
|
||||
editMode: boolean;
|
||||
setEditMode: (editMode: boolean) => void;
|
||||
mutationLoading: boolean;
|
||||
setMutationloading: (loading: boolean) => void;
|
||||
}
|
||||
|
||||
export const useEditMode = create<EditModeState>((set) => ({
|
||||
editMode: false,
|
||||
setEditMode: (editMode) => set({ editMode }),
|
||||
mutationLoading: false,
|
||||
setMutationloading: (loading) => set({ mutationLoading: loading }),
|
||||
}));
|
|
@ -1,5 +1,5 @@
|
|||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
import { Plus, StopCircle, Users } from "lucide-react";
|
||||
import { Plus } from "lucide-react";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useMemo, useRef, useCallback, useEffect, useReducer } from "react";
|
||||
|
||||
|
@ -7,11 +7,14 @@ import { WEBAPP_URL } from "@calcom/lib/constants";
|
|||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { MembershipRole } from "@calcom/prisma/enums";
|
||||
import { trpc } from "@calcom/trpc";
|
||||
import { Avatar, Badge, Button, DataTable } from "@calcom/ui";
|
||||
import { Avatar, Badge, Button, DataTable, Checkbox } from "@calcom/ui";
|
||||
|
||||
import { useOrgBranding } from "../../../ee/organizations/context/provider";
|
||||
import { DeleteBulkUsers } from "./BulkActions/DeleteBulkUsers";
|
||||
import { TeamListBulkAction } from "./BulkActions/TeamList";
|
||||
import { ChangeUserRoleModal } from "./ChangeUserRoleModal";
|
||||
import { DeleteMemberModal } from "./DeleteMemberModal";
|
||||
import { EditUserSheet } from "./EditSheet/EditUserSheet";
|
||||
import { ImpersonationMemberModal } from "./ImpersonationMemberModal";
|
||||
import { InviteMemberModal } from "./InviteMemberModal";
|
||||
import { TableActions } from "./UserTableActions";
|
||||
|
@ -42,11 +45,17 @@ export type State = {
|
|||
deleteMember: Payload;
|
||||
impersonateMember: Payload;
|
||||
inviteMember: Payload;
|
||||
editSheet: Payload;
|
||||
};
|
||||
|
||||
export type Action =
|
||||
| {
|
||||
type: "SET_CHANGE_MEMBER_ROLE_ID" | "SET_DELETE_ID" | "SET_IMPERSONATE_ID" | "INVITE_MEMBER";
|
||||
type:
|
||||
| "SET_CHANGE_MEMBER_ROLE_ID"
|
||||
| "SET_DELETE_ID"
|
||||
| "SET_IMPERSONATE_ID"
|
||||
| "INVITE_MEMBER"
|
||||
| "EDIT_USER_SHEET";
|
||||
payload: Payload;
|
||||
}
|
||||
| {
|
||||
|
@ -66,6 +75,9 @@ const initialState: State = {
|
|||
inviteMember: {
|
||||
showModal: false,
|
||||
},
|
||||
editSheet: {
|
||||
showModal: false,
|
||||
},
|
||||
};
|
||||
|
||||
function reducer(state: State, action: Action): State {
|
||||
|
@ -78,6 +90,8 @@ function reducer(state: State, action: Action): State {
|
|||
return { ...state, impersonateMember: action.payload };
|
||||
case "INVITE_MEMBER":
|
||||
return { ...state, inviteMember: action.payload };
|
||||
case "EDIT_USER_SHEET":
|
||||
return { ...state, editSheet: action.payload };
|
||||
case "CLOSE_MODAL":
|
||||
return {
|
||||
...state,
|
||||
|
@ -85,6 +99,7 @@ function reducer(state: State, action: Action): State {
|
|||
deleteMember: { showModal: false },
|
||||
impersonateMember: { showModal: false },
|
||||
inviteMember: { showModal: false },
|
||||
editSheet: { showModal: false },
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
|
@ -121,25 +136,25 @@ export function UserListTable() {
|
|||
};
|
||||
const cols: ColumnDef<User>[] = [
|
||||
// Disabling select for this PR: Will work on actions etc in a follow up
|
||||
// {
|
||||
// id: "select",
|
||||
// header: ({ table }) => (
|
||||
// <Checkbox
|
||||
// checked={table.getIsAllPageRowsSelected()}
|
||||
// onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
||||
// aria-label="Select all"
|
||||
// className="translate-y-[2px]"
|
||||
// />
|
||||
// ),
|
||||
// cell: ({ row }) => (
|
||||
// <Checkbox
|
||||
// checked={row.getIsSelected()}
|
||||
// onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||
// aria-label="Select row"
|
||||
// className="translate-y-[2px]"
|
||||
// />
|
||||
// ),
|
||||
// },
|
||||
{
|
||||
id: "select",
|
||||
header: ({ table }) => (
|
||||
<Checkbox
|
||||
checked={table.getIsAllPageRowsSelected()}
|
||||
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
||||
aria-label="Select all"
|
||||
className="translate-y-[2px]"
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<Checkbox
|
||||
checked={row.getIsSelected()}
|
||||
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||
aria-label="Select row"
|
||||
className="translate-y-[2px]"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "member",
|
||||
accessorFn: (data) => data.email,
|
||||
|
@ -269,18 +284,14 @@ export function UserListTable() {
|
|||
searchKey="member"
|
||||
selectionOptions={[
|
||||
{
|
||||
label: "Add To Team",
|
||||
onClick: () => {
|
||||
console.log("Add To Team");
|
||||
},
|
||||
icon: Users,
|
||||
type: "render",
|
||||
render: (table) => <TeamListBulkAction table={table} />,
|
||||
},
|
||||
{
|
||||
label: "Delete",
|
||||
onClick: () => {
|
||||
console.log("Delete");
|
||||
},
|
||||
icon: StopCircle,
|
||||
type: "render",
|
||||
render: (table) => (
|
||||
<DeleteBulkUsers users={table.getSelectedRowModel().flatRows.map((row) => row.original)} />
|
||||
),
|
||||
},
|
||||
]}
|
||||
tableContainerRef={tableContainerRef}
|
||||
|
@ -326,6 +337,7 @@ export function UserListTable() {
|
|||
{state.inviteMember.showModal && <InviteMemberModal dispatch={dispatch} />}
|
||||
{state.impersonateMember.showModal && <ImpersonationMemberModal dispatch={dispatch} state={state} />}
|
||||
{state.changeMemberRole.showModal && <ChangeUserRoleModal dispatch={dispatch} state={state} />}
|
||||
{state.editSheet.showModal && <EditUserSheet dispatch={dispatch} state={state} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -66,7 +66,7 @@ export function TableActions({
|
|||
type="button"
|
||||
onClick={() =>
|
||||
dispatch({
|
||||
type: "SET_CHANGE_MEMBER_ROLE_ID",
|
||||
type: "EDIT_USER_SHEET",
|
||||
payload: {
|
||||
user,
|
||||
showModal: true,
|
||||
|
@ -140,7 +140,7 @@ export function TableActions({
|
|||
type="button"
|
||||
onClick={() =>
|
||||
dispatch({
|
||||
type: "SET_IMPERSONATE_ID",
|
||||
type: "EDIT_USER_SHEET",
|
||||
payload: {
|
||||
user,
|
||||
showModal: true,
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
import { useState, useEffect } from "react";
|
||||
|
||||
export function useCopy() {
|
||||
const [isCopied, setIsCopied] = useState(false);
|
||||
|
||||
const copyToClipboard = (text: string) => {
|
||||
if (typeof navigator !== "undefined" && navigator.clipboard) {
|
||||
navigator.clipboard
|
||||
.writeText(text)
|
||||
.then(() => setIsCopied(true))
|
||||
.catch((error) => console.error("Copy to clipboard failed:", error));
|
||||
}
|
||||
};
|
||||
|
||||
const resetCopyStatus = () => {
|
||||
setIsCopied(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isCopied) {
|
||||
const timer = setTimeout(resetCopyStatus, 3000); // Reset copy status after 3 seconds
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [isCopied]);
|
||||
|
||||
return { isCopied, copyToClipboard, resetCopyStatus };
|
||||
}
|
|
@ -4,287 +4,123 @@ import authedProcedure, {
|
|||
authedAdminProcedure,
|
||||
authedOrgAdminProcedure,
|
||||
} from "../../../procedures/authedProcedure";
|
||||
import { router } from "../../../trpc";
|
||||
import { importHandler, router } from "../../../trpc";
|
||||
import { ZAddBulkTeams } from "./addBulkTeams.schema";
|
||||
import { ZAdminVerifyInput } from "./adminVerify.schema";
|
||||
import { ZBulkUsersDelete } from "./bulkDeleteUsers.schema.";
|
||||
import { ZCreateInputSchema } from "./create.schema";
|
||||
import { ZCreateTeamsSchema } from "./createTeams.schema";
|
||||
import { ZGetMembersInput } from "./getMembers.schema";
|
||||
import { ZGetOtherTeamInputSchema } from "./getOtherTeam.handler";
|
||||
import { ZGetUserInput } from "./getUser.schema";
|
||||
import { ZListMembersSchema } from "./listMembers.schema";
|
||||
import { ZListOtherTeamMembersSchema } from "./listOtherTeamMembers.handler";
|
||||
import { ZSetPasswordSchema } from "./setPassword.schema";
|
||||
import { ZUpdateInputSchema } from "./update.schema";
|
||||
import { ZUpdateUserInputSchema } from "./updateUser.schema";
|
||||
|
||||
type OrganizationsRouterHandlerCache = {
|
||||
create?: typeof import("./create.handler").createHandler;
|
||||
listCurrent?: typeof import("./list.handler").listHandler;
|
||||
publish?: typeof import("./publish.handler").publishHandler;
|
||||
checkIfOrgNeedsUpgrade?: typeof import("./checkIfOrgNeedsUpgrade.handler").checkIfOrgNeedsUpgradeHandler;
|
||||
update?: typeof import("./update.handler").updateHandler;
|
||||
verifyCode?: typeof import("./verifyCode.handler").verifyCodeHandler;
|
||||
createTeams?: typeof import("./createTeams.handler").createTeamsHandler;
|
||||
setPassword?: typeof import("./setPassword.handler").setPasswordHandler;
|
||||
adminGetUnverified?: typeof import("./adminGetUnverified.handler").adminGetUnverifiedHandler;
|
||||
adminVerify?: typeof import("./adminVerify.handler").adminVerifyHandler;
|
||||
listMembers?: typeof import("./listMembers.handler").listMembersHandler;
|
||||
getBrand?: typeof import("./getBrand.handler").getBrandHandler;
|
||||
getMembers?: typeof import("./getMembers.handler").getMembersHandler;
|
||||
listOtherTeams?: typeof import("./listOtherTeams.handler").listOtherTeamHandler;
|
||||
getOtherTeam?: typeof import("./getOtherTeam.handler").getOtherTeamHandler;
|
||||
listOtherTeamMembers?: typeof import("./listOtherTeamMembers.handler").listOtherTeamMembers;
|
||||
};
|
||||
const NAMESPACE = "organizations";
|
||||
|
||||
const UNSTABLE_HANDLER_CACHE: OrganizationsRouterHandlerCache = {};
|
||||
const namespaced = (s: string) => `${NAMESPACE}.${s}`;
|
||||
|
||||
export const viewerOrganizationsRouter = router({
|
||||
create: authedProcedure.input(ZCreateInputSchema).mutation(async ({ ctx, input }) => {
|
||||
if (!UNSTABLE_HANDLER_CACHE.create) {
|
||||
UNSTABLE_HANDLER_CACHE.create = await import("./create.handler").then((mod) => mod.createHandler);
|
||||
}
|
||||
|
||||
// Unreachable code but required for type safety
|
||||
if (!UNSTABLE_HANDLER_CACHE.create) {
|
||||
throw new Error("Failed to load handler");
|
||||
}
|
||||
|
||||
return UNSTABLE_HANDLER_CACHE.create({
|
||||
ctx,
|
||||
input,
|
||||
});
|
||||
create: authedProcedure.input(ZCreateInputSchema).mutation(async (opts) => {
|
||||
const handler = await importHandler(namespaced("create"), () => import("./create.handler"));
|
||||
return handler(opts);
|
||||
}),
|
||||
update: authedProcedure.input(ZUpdateInputSchema).mutation(async ({ ctx, input }) => {
|
||||
if (!UNSTABLE_HANDLER_CACHE.update) {
|
||||
UNSTABLE_HANDLER_CACHE.update = await import("./update.handler").then((mod) => mod.updateHandler);
|
||||
}
|
||||
|
||||
// Unreachable code but required for type safety
|
||||
if (!UNSTABLE_HANDLER_CACHE.update) {
|
||||
throw new Error("Failed to load handler");
|
||||
}
|
||||
|
||||
return UNSTABLE_HANDLER_CACHE.update({
|
||||
ctx,
|
||||
input,
|
||||
});
|
||||
update: authedProcedure.input(ZUpdateInputSchema).mutation(async (opts) => {
|
||||
const handler = await importHandler(namespaced("update"), () => import("./update.handler"));
|
||||
return handler(opts);
|
||||
}),
|
||||
verifyCode: authedProcedure.input(ZVerifyCodeInputSchema).mutation(async ({ ctx, input }) => {
|
||||
if (!UNSTABLE_HANDLER_CACHE.verifyCode) {
|
||||
UNSTABLE_HANDLER_CACHE.verifyCode = await import("./verifyCode.handler").then(
|
||||
(mod) => mod.verifyCodeHandler
|
||||
);
|
||||
}
|
||||
|
||||
// Unreachable code but required for type safety
|
||||
if (!UNSTABLE_HANDLER_CACHE.verifyCode) {
|
||||
throw new Error("Failed to load handler");
|
||||
}
|
||||
|
||||
return UNSTABLE_HANDLER_CACHE.verifyCode({
|
||||
ctx,
|
||||
input,
|
||||
});
|
||||
verifyCode: authedProcedure.input(ZVerifyCodeInputSchema).mutation(async (opts) => {
|
||||
const handler = await importHandler(namespaced("verifyCode"), () => import("./verifyCode.handler"));
|
||||
return handler(opts);
|
||||
}),
|
||||
createTeams: authedProcedure.input(ZCreateTeamsSchema).mutation(async ({ ctx, input }) => {
|
||||
if (!UNSTABLE_HANDLER_CACHE.createTeams) {
|
||||
UNSTABLE_HANDLER_CACHE.createTeams = await import("./createTeams.handler").then(
|
||||
(mod) => mod.createTeamsHandler
|
||||
);
|
||||
}
|
||||
|
||||
// Unreachable code but required for type safety
|
||||
if (!UNSTABLE_HANDLER_CACHE.createTeams) {
|
||||
throw new Error("Failed to load handler");
|
||||
}
|
||||
|
||||
return UNSTABLE_HANDLER_CACHE.createTeams({
|
||||
ctx,
|
||||
input,
|
||||
});
|
||||
createTeams: authedProcedure.input(ZCreateTeamsSchema).mutation(async (opts) => {
|
||||
const handler = await importHandler(namespaced("createTeams"), () => import("./createTeams.handler"));
|
||||
return handler(opts);
|
||||
}),
|
||||
listCurrent: authedProcedure.query(async ({ ctx }) => {
|
||||
if (!UNSTABLE_HANDLER_CACHE.listCurrent) {
|
||||
UNSTABLE_HANDLER_CACHE.listCurrent = await import("./list.handler").then((mod) => mod.listHandler);
|
||||
}
|
||||
|
||||
// Unreachable code but required for type safety
|
||||
if (!UNSTABLE_HANDLER_CACHE.listCurrent) {
|
||||
throw new Error("Failed to load handler");
|
||||
}
|
||||
|
||||
return UNSTABLE_HANDLER_CACHE.listCurrent({
|
||||
ctx,
|
||||
});
|
||||
listCurrent: authedProcedure.query(async (opts) => {
|
||||
const handler = await importHandler(namespaced("listCurrent"), () => import("./list.handler"));
|
||||
return handler(opts);
|
||||
}),
|
||||
checkIfOrgNeedsUpgrade: authedProcedure.query(async ({ ctx }) => {
|
||||
if (!UNSTABLE_HANDLER_CACHE.checkIfOrgNeedsUpgrade) {
|
||||
UNSTABLE_HANDLER_CACHE.checkIfOrgNeedsUpgrade = await import("./checkIfOrgNeedsUpgrade.handler").then(
|
||||
(mod) => mod.checkIfOrgNeedsUpgradeHandler
|
||||
);
|
||||
}
|
||||
|
||||
if (!UNSTABLE_HANDLER_CACHE.checkIfOrgNeedsUpgrade) {
|
||||
throw new Error("Failed to load handler");
|
||||
}
|
||||
|
||||
return UNSTABLE_HANDLER_CACHE.checkIfOrgNeedsUpgrade({ ctx });
|
||||
checkIfOrgNeedsUpgrade: authedProcedure.query(async (opts) => {
|
||||
const handler = await importHandler(
|
||||
namespaced("checkIfOrgNeedsUpgrade"),
|
||||
() => import("./checkIfOrgNeedsUpgrade.handler")
|
||||
);
|
||||
return handler(opts);
|
||||
}),
|
||||
publish: authedProcedure.mutation(async ({ ctx }) => {
|
||||
if (!UNSTABLE_HANDLER_CACHE.publish) {
|
||||
UNSTABLE_HANDLER_CACHE.publish = await import("./publish.handler").then((mod) => mod.publishHandler);
|
||||
}
|
||||
|
||||
if (!UNSTABLE_HANDLER_CACHE.publish) {
|
||||
throw new Error("Failed to load handler");
|
||||
}
|
||||
|
||||
return UNSTABLE_HANDLER_CACHE.publish({ ctx });
|
||||
publish: authedProcedure.mutation(async (opts) => {
|
||||
const handler = await importHandler(namespaced("publish"), () => import("./publish.handler"));
|
||||
return handler(opts);
|
||||
}),
|
||||
setPassword: authedProcedure.input(ZSetPasswordSchema).mutation(async ({ ctx, input }) => {
|
||||
if (!UNSTABLE_HANDLER_CACHE.setPassword) {
|
||||
UNSTABLE_HANDLER_CACHE.setPassword = await import("./setPassword.handler").then(
|
||||
(mod) => mod.setPasswordHandler
|
||||
);
|
||||
}
|
||||
|
||||
// Unreachable code but required for type safety
|
||||
if (!UNSTABLE_HANDLER_CACHE.setPassword) {
|
||||
throw new Error("Failed to load handler");
|
||||
}
|
||||
|
||||
return UNSTABLE_HANDLER_CACHE.setPassword({
|
||||
ctx,
|
||||
input,
|
||||
});
|
||||
setPassword: authedProcedure.input(ZSetPasswordSchema).mutation(async (opts) => {
|
||||
const handler = await importHandler(namespaced("setPassword"), () => import("./setPassword.handler"));
|
||||
return handler(opts);
|
||||
}),
|
||||
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,
|
||||
});
|
||||
getMembers: authedProcedure.input(ZGetMembersInput).query(async (opts) => {
|
||||
const handler = await importHandler(namespaced("getMembers"), () => import("./getMembers.handler"));
|
||||
return handler(opts);
|
||||
}),
|
||||
adminGetUnverified: authedAdminProcedure.query(async ({ ctx }) => {
|
||||
if (!UNSTABLE_HANDLER_CACHE.adminGetUnverified) {
|
||||
UNSTABLE_HANDLER_CACHE.adminGetUnverified = await import("./adminGetUnverified.handler").then(
|
||||
(mod) => mod.adminGetUnverifiedHandler
|
||||
);
|
||||
}
|
||||
if (!UNSTABLE_HANDLER_CACHE.adminGetUnverified) {
|
||||
throw new Error("Failed to load handler");
|
||||
}
|
||||
|
||||
return UNSTABLE_HANDLER_CACHE.adminGetUnverified({
|
||||
ctx,
|
||||
});
|
||||
adminGetUnverified: authedAdminProcedure.query(async (opts) => {
|
||||
const handler = await importHandler(
|
||||
namespaced("adminGetUnverified"),
|
||||
() => import("./adminGetUnverified.handler")
|
||||
);
|
||||
return handler(opts);
|
||||
}),
|
||||
adminVerify: authedAdminProcedure.input(ZAdminVerifyInput).mutation(async ({ input, ctx }) => {
|
||||
if (!UNSTABLE_HANDLER_CACHE.adminVerify) {
|
||||
UNSTABLE_HANDLER_CACHE.adminVerify = await import("./adminVerify.handler").then(
|
||||
(mod) => mod.adminVerifyHandler
|
||||
);
|
||||
}
|
||||
|
||||
// Unreachable code but required for type safety
|
||||
if (!UNSTABLE_HANDLER_CACHE.adminVerify) {
|
||||
throw new Error("Failed to load handler");
|
||||
}
|
||||
|
||||
return UNSTABLE_HANDLER_CACHE.adminVerify({
|
||||
ctx,
|
||||
input,
|
||||
});
|
||||
adminVerify: authedAdminProcedure.input(ZAdminVerifyInput).mutation(async (opts) => {
|
||||
const handler = await importHandler(namespaced("adminVerify"), () => import("./adminVerify.handler"));
|
||||
return handler(opts);
|
||||
}),
|
||||
listMembers: authedProcedure.input(ZListMembersSchema).query(async ({ ctx, input }) => {
|
||||
if (!UNSTABLE_HANDLER_CACHE.listMembers) {
|
||||
UNSTABLE_HANDLER_CACHE.listMembers = await import("./listMembers.handler").then(
|
||||
(mod) => mod.listMembersHandler
|
||||
);
|
||||
}
|
||||
|
||||
// Unreachable code but required for type safety
|
||||
if (!UNSTABLE_HANDLER_CACHE.listMembers) {
|
||||
throw new Error("Failed to load handler");
|
||||
}
|
||||
|
||||
return UNSTABLE_HANDLER_CACHE.listMembers({
|
||||
ctx,
|
||||
input,
|
||||
});
|
||||
listMembers: authedProcedure.input(ZListMembersSchema).query(async (opts) => {
|
||||
const handler = await importHandler(namespaced("listMembers"), () => import("./listMembers.handler"));
|
||||
return handler(opts);
|
||||
}),
|
||||
getBrand: authedProcedure.query(async ({ ctx }) => {
|
||||
if (!UNSTABLE_HANDLER_CACHE.getBrand) {
|
||||
UNSTABLE_HANDLER_CACHE.getBrand = await import("./getBrand.handler").then((mod) => mod.getBrandHandler);
|
||||
}
|
||||
|
||||
// Unreachable code but required for type safety
|
||||
if (!UNSTABLE_HANDLER_CACHE.getBrand) {
|
||||
throw new Error("Failed to load handler");
|
||||
}
|
||||
|
||||
return UNSTABLE_HANDLER_CACHE.getBrand({
|
||||
ctx,
|
||||
});
|
||||
getBrand: authedProcedure.query(async (opts) => {
|
||||
const handler = await importHandler(namespaced("getBrand"), () => import("./getBrand.handler"));
|
||||
return handler(opts);
|
||||
}),
|
||||
listOtherTeams: authedOrgAdminProcedure.query(async ({ ctx }) => {
|
||||
if (!UNSTABLE_HANDLER_CACHE.listOtherTeams) {
|
||||
UNSTABLE_HANDLER_CACHE.listOtherTeams = await import("./listOtherTeams.handler").then(
|
||||
(mod) => mod.listOtherTeamHandler
|
||||
);
|
||||
}
|
||||
|
||||
// Unreachable code but required for type safety
|
||||
if (!UNSTABLE_HANDLER_CACHE.listOtherTeams) {
|
||||
throw new Error("Failed to load handler");
|
||||
}
|
||||
|
||||
return UNSTABLE_HANDLER_CACHE.listOtherTeams({
|
||||
ctx,
|
||||
});
|
||||
getUser: authedProcedure.input(ZGetUserInput).query(async (opts) => {
|
||||
const handler = await importHandler(namespaced("getUser"), () => import("./getUser.handler"));
|
||||
return handler(opts);
|
||||
}),
|
||||
getOtherTeam: authedOrgAdminProcedure.input(ZGetOtherTeamInputSchema).query(async ({ ctx, input }) => {
|
||||
if (!UNSTABLE_HANDLER_CACHE.getOtherTeam) {
|
||||
UNSTABLE_HANDLER_CACHE.getOtherTeam = await import("./getOtherTeam.handler").then(
|
||||
(mod) => mod.getOtherTeamHandler
|
||||
);
|
||||
}
|
||||
|
||||
// Unreachable code but required for type safety
|
||||
if (!UNSTABLE_HANDLER_CACHE.getOtherTeam) {
|
||||
throw new Error("Failed to load handler");
|
||||
}
|
||||
|
||||
return UNSTABLE_HANDLER_CACHE.getOtherTeam({
|
||||
ctx,
|
||||
input,
|
||||
});
|
||||
updateUser: authedProcedure.input(ZUpdateUserInputSchema).mutation(async (opts) => {
|
||||
const handler = await importHandler(namespaced("updateUser"), () => import("./updateUser.handler"));
|
||||
return handler(opts);
|
||||
}),
|
||||
getTeams: authedProcedure.query(async (opts) => {
|
||||
const handler = await importHandler(namespaced("getTeams"), () => import("./getTeams.handler"));
|
||||
return handler(opts);
|
||||
}),
|
||||
bulkAddToTeams: authedProcedure.input(ZAddBulkTeams).mutation(async (opts) => {
|
||||
const handler = await importHandler(namespaced("addBulkTeams"), () => import("./addBulkTeams.handler"));
|
||||
return handler(opts);
|
||||
}),
|
||||
bulkDeleteUsers: authedProcedure.input(ZBulkUsersDelete).mutation(async (opts) => {
|
||||
const handler = await importHandler(
|
||||
namespaced("bulkDeleteUsers"),
|
||||
() => import("./bulkDeleteUsers.handler")
|
||||
);
|
||||
return handler(opts);
|
||||
}),
|
||||
listOtherTeamMembers: authedOrgAdminProcedure.input(ZListOtherTeamMembersSchema).query(async (opts) => {
|
||||
const handler = await importHandler(
|
||||
namespaced("listOtherTeamMembers"),
|
||||
() => import("./listOtherTeamMembers.handler")
|
||||
);
|
||||
return handler(opts);
|
||||
}),
|
||||
getOtherTeam: authedOrgAdminProcedure.input(ZGetOtherTeamInputSchema).query(async (opts) => {
|
||||
const handler = await importHandler(namespaced("getOtherTeam"), () => import("./getOtherTeam.handler"));
|
||||
return handler(opts);
|
||||
}),
|
||||
listOtherTeams: authedOrgAdminProcedure.query(async (opts) => {
|
||||
const handler = await importHandler(
|
||||
namespaced("listOtherTeams"),
|
||||
() => import("./listOtherTeams.handler")
|
||||
);
|
||||
return handler(opts);
|
||||
}),
|
||||
listOtherTeamMembers: authedOrgAdminProcedure
|
||||
.input(ZListOtherTeamMembersSchema)
|
||||
.query(async ({ ctx, input }) => {
|
||||
if (!UNSTABLE_HANDLER_CACHE.listOtherTeamMembers) {
|
||||
UNSTABLE_HANDLER_CACHE.listOtherTeamMembers = await import("./listOtherTeamMembers.handler").then(
|
||||
(mod) => mod.listOtherTeamMembers
|
||||
);
|
||||
}
|
||||
|
||||
// Unreachable code but required for type safety
|
||||
if (!UNSTABLE_HANDLER_CACHE.listOtherTeamMembers) {
|
||||
throw new Error("Failed to load handler");
|
||||
}
|
||||
|
||||
return UNSTABLE_HANDLER_CACHE.listOtherTeamMembers({
|
||||
ctx,
|
||||
input,
|
||||
});
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
import { isOrganisationAdmin } from "@calcom/lib/server/queries/organisations";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import type { Prisma } from "@calcom/prisma/client";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
import type { TrpcSessionUser } from "../../../trpc";
|
||||
import type { TAddBulkTeams } from "./addBulkTeams.schema";
|
||||
|
||||
type AddBulkTeamsHandler = {
|
||||
ctx: {
|
||||
user: NonNullable<TrpcSessionUser>;
|
||||
};
|
||||
input: TAddBulkTeams;
|
||||
};
|
||||
|
||||
export async function addBulkTeamsHandler({ ctx, input }: AddBulkTeamsHandler) {
|
||||
const currentUser = ctx.user;
|
||||
|
||||
if (!currentUser.organizationId) throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
|
||||
// check if user is admin of organization
|
||||
if (!(await isOrganisationAdmin(currentUser?.id, currentUser.organizationId)))
|
||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
|
||||
// Loop over all users and check if they are already in the organization
|
||||
const usersInOrganization = await prisma.membership.findMany({
|
||||
where: {
|
||||
teamId: currentUser.organizationId,
|
||||
user: {
|
||||
id: {
|
||||
in: input.userIds,
|
||||
},
|
||||
},
|
||||
},
|
||||
distinct: ["userId"],
|
||||
});
|
||||
|
||||
// Throw error if any of the users are not in the organization. They should be invited to the organization via the onboaring flow first.
|
||||
if (usersInOrganization.length !== input.userIds.length) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "One or more users are not in the organization",
|
||||
});
|
||||
}
|
||||
|
||||
// loop over all users and check if they are already in team they are being invited to
|
||||
const usersInTeams = await prisma.membership.findMany({
|
||||
where: {
|
||||
userId: {
|
||||
in: input.userIds,
|
||||
},
|
||||
teamId: {
|
||||
in: input.teamIds,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Filter out users who are already in teams they are being invited to
|
||||
const filteredUserIds = input.userIds.filter((userId) => {
|
||||
return !usersInTeams.some((membership) => membership.userId === userId);
|
||||
});
|
||||
|
||||
// TODO: might need to come back to this is people are doing ALOT of invites with bulk actions.
|
||||
// Loop over all users and add them to all teams in the array
|
||||
const membershipData = filteredUserIds.flatMap((userId) =>
|
||||
input.teamIds.map((teamId) => {
|
||||
const userMembership = usersInOrganization.find((membership) => membership.userId === userId);
|
||||
const accepted = userMembership && userMembership.accepted;
|
||||
return {
|
||||
userId,
|
||||
teamId,
|
||||
role: MembershipRole.MEMBER,
|
||||
accepted: accepted || false,
|
||||
} as Prisma.MembershipCreateManyInput;
|
||||
})
|
||||
);
|
||||
|
||||
await prisma.membership.createMany({
|
||||
data: membershipData,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
invitedTotalUsers: input.userIds.length,
|
||||
};
|
||||
}
|
||||
|
||||
export default addBulkTeamsHandler;
|
|
@ -0,0 +1,8 @@
|
|||
import { z } from "zod";
|
||||
|
||||
export const ZAddBulkTeams = z.object({
|
||||
userIds: z.array(z.number()),
|
||||
teamIds: z.array(z.number()),
|
||||
});
|
||||
|
||||
export type TAddBulkTeams = z.infer<typeof ZAddBulkTeams>;
|
|
@ -49,3 +49,5 @@ export const adminGetUnverifiedHandler = async ({ ctx }: AdminGetUnverifiedOptio
|
|||
|
||||
return unVerifiedTeams;
|
||||
};
|
||||
|
||||
export default adminGetUnverifiedHandler;
|
||||
|
|
|
@ -114,3 +114,5 @@ export const adminVerifyHandler = async ({ input }: AdminVerifyOptions) => {
|
|||
message: `Verified Organization - Auto accepted all members ending in ${acceptedEmailDomain}`,
|
||||
};
|
||||
};
|
||||
|
||||
export default adminVerifyHandler;
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
import { isOrganisationAdmin } from "@calcom/lib/server/queries/organisations";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
import type { TrpcSessionUser } from "../../../trpc";
|
||||
import type { TBulkUsersDelete } from "./bulkDeleteUsers.schema.";
|
||||
|
||||
type BulkDeleteUsersHandler = {
|
||||
ctx: {
|
||||
user: NonNullable<TrpcSessionUser>;
|
||||
};
|
||||
input: TBulkUsersDelete;
|
||||
};
|
||||
|
||||
export async function bulkDeleteUsersHandler({ ctx, input }: BulkDeleteUsersHandler) {
|
||||
const currentUser = ctx.user;
|
||||
|
||||
if (!currentUser.organizationId) throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
|
||||
// check if user is admin of organization
|
||||
if (!(await isOrganisationAdmin(currentUser?.id, currentUser.organizationId)))
|
||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
|
||||
// Loop over all users in input.userIds and remove all memberships for the organization including child teams
|
||||
const deleteMany = prisma.membership.deleteMany({
|
||||
where: {
|
||||
userId: {
|
||||
in: input.userIds,
|
||||
},
|
||||
team: {
|
||||
OR: [
|
||||
{
|
||||
parentId: currentUser.organizationId,
|
||||
},
|
||||
{ id: currentUser.organizationId },
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const removeOrgrelation = prisma.user.updateMany({
|
||||
where: {
|
||||
id: {
|
||||
in: input.userIds,
|
||||
},
|
||||
},
|
||||
data: {
|
||||
// Remove organization relation
|
||||
organizationId: null,
|
||||
// Set username to null - to make sure there is no conflicts
|
||||
username: null,
|
||||
// Set completedOnboarding to false - to make sure the user has to complete onboarding again -> Setup a new username
|
||||
completedOnboarding: false,
|
||||
},
|
||||
});
|
||||
// We do this in a transaction to make sure that all memberships are removed before we remove the organization relation from the user
|
||||
// We also do this to make sure that if one of the queries fail, the whole transaction fails
|
||||
await prisma.$transaction([deleteMany, removeOrgrelation]);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
usersDeleted: input.userIds.length,
|
||||
};
|
||||
}
|
||||
|
||||
export default bulkDeleteUsersHandler;
|
|
@ -0,0 +1,7 @@
|
|||
import { z } from "zod";
|
||||
|
||||
export const ZBulkUsersDelete = z.object({
|
||||
userIds: z.array(z.number()),
|
||||
});
|
||||
|
||||
export type TBulkUsersDelete = z.infer<typeof ZBulkUsersDelete>;
|
|
@ -40,3 +40,5 @@ export async function checkIfOrgNeedsUpgradeHandler({ ctx }: GetUpgradeableOptio
|
|||
|
||||
return teams;
|
||||
}
|
||||
|
||||
export default checkIfOrgNeedsUpgradeHandler;
|
||||
|
|
|
@ -160,3 +160,5 @@ export const createHandler = async ({ input, ctx }: CreateOptions) => {
|
|||
|
||||
return { checked: true };
|
||||
};
|
||||
|
||||
export default createHandler;
|
||||
|
|
|
@ -135,3 +135,5 @@ class NoOrganizationSlugError extends TRPCError {
|
|||
super({ code: "BAD_REQUEST", message: "no_organization_slug" });
|
||||
}
|
||||
}
|
||||
|
||||
export default createTeamsHandler;
|
||||
|
|
|
@ -14,3 +14,5 @@ export const getBrandHandler = async ({ ctx }: VerifyCodeOptions) => {
|
|||
|
||||
return await getBrand(user.organizationId);
|
||||
};
|
||||
|
||||
export default getBrandHandler;
|
||||
|
|
|
@ -52,3 +52,5 @@ export const getMembersHandler = async ({ input, ctx }: CreateOptions) => {
|
|||
});
|
||||
return teamQuery?.members || [];
|
||||
};
|
||||
|
||||
export default getMembersHandler;
|
||||
|
|
|
@ -52,3 +52,5 @@ export const getOtherTeamHandler = async ({ input }: GetOptions) => {
|
|||
safeBio: markdownToSafeHTML(team.bio),
|
||||
};
|
||||
};
|
||||
|
||||
export default getOtherTeamHandler;
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
import { isOrganisationAdmin } from "@calcom/lib/server/queries/organisations";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
import type { TrpcSessionUser } from "../../../trpc";
|
||||
|
||||
type GetTeamsHandler = {
|
||||
ctx: {
|
||||
user: NonNullable<TrpcSessionUser>;
|
||||
};
|
||||
};
|
||||
|
||||
export async function getTeamsHandler({ ctx }: GetTeamsHandler) {
|
||||
const currentUser = ctx.user;
|
||||
|
||||
if (!currentUser.organizationId) throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
|
||||
// check if user is admin of organization
|
||||
if (!(await isOrganisationAdmin(currentUser?.id, currentUser.organizationId)))
|
||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
|
||||
const allOrgTeams = await prisma.team.findMany({
|
||||
where: {
|
||||
parentId: currentUser.organizationId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
});
|
||||
|
||||
return allOrgTeams;
|
||||
}
|
||||
|
||||
export default getTeamsHandler;
|
|
@ -0,0 +1,89 @@
|
|||
import { isOrganisationAdmin } from "@calcom/lib/server/queries/organisations";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
import type { TrpcSessionUser } from "../../../trpc";
|
||||
import type { TGetUserInput } from "./getUser.schema";
|
||||
|
||||
type AdminVerifyOptions = {
|
||||
ctx: {
|
||||
user: NonNullable<TrpcSessionUser>;
|
||||
};
|
||||
input: TGetUserInput;
|
||||
};
|
||||
|
||||
export async function getUserHandler({ input, ctx }: AdminVerifyOptions) {
|
||||
const currentUser = ctx.user;
|
||||
|
||||
if (!currentUser.organizationId) throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
|
||||
// check if user is admin of organization
|
||||
if (!(await isOrganisationAdmin(currentUser?.id, currentUser.organizationId)))
|
||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
|
||||
// get requested user from database and ensure they are in the same organization
|
||||
const [requestedUser, membership, teams] = await prisma.$transaction([
|
||||
prisma.user.findFirst({
|
||||
where: { id: input.userId, organizationId: currentUser.organizationId },
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
username: true,
|
||||
name: true,
|
||||
bio: true,
|
||||
timeZone: true,
|
||||
schedules: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
// Query on accepted as we don't want the user to be able to get this much info on a user that hasn't accepted the invite
|
||||
prisma.membership.findFirst({
|
||||
where: {
|
||||
userId: input.userId,
|
||||
teamId: currentUser.organizationId,
|
||||
accepted: true,
|
||||
},
|
||||
select: {
|
||||
role: true,
|
||||
},
|
||||
}),
|
||||
prisma.membership.findMany({
|
||||
where: {
|
||||
userId: input.userId,
|
||||
team: {
|
||||
parentId: currentUser.organizationId,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
accepted: true,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
if (!requestedUser || !membership)
|
||||
throw new TRPCError({ code: "UNAUTHORIZED", message: "user_not_exist_or_not_in_org" });
|
||||
|
||||
const foundUser = {
|
||||
...requestedUser,
|
||||
teams: teams.map((team) => ({
|
||||
...team.team,
|
||||
accepted: team.accepted,
|
||||
})),
|
||||
role: membership.role,
|
||||
};
|
||||
|
||||
return foundUser;
|
||||
}
|
||||
|
||||
export default getUserHandler;
|
|
@ -0,0 +1,7 @@
|
|||
import { z } from "zod";
|
||||
|
||||
export const ZGetUserInput = z.object({
|
||||
userId: z.number().optional(),
|
||||
});
|
||||
|
||||
export type TGetUserInput = z.infer<typeof ZGetUserInput>;
|
|
@ -40,3 +40,5 @@ export const listHandler = async ({ ctx }: ListHandlerInput) => {
|
|||
metadata,
|
||||
};
|
||||
};
|
||||
|
||||
export default listHandler;
|
||||
|
|
|
@ -102,3 +102,5 @@ export const listMembersHandler = async ({ ctx, input }: GetOptions) => {
|
|||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default listMembersHandler;
|
||||
|
|
|
@ -79,3 +79,5 @@ export const listOtherTeamMembers = async ({ ctx, input }: ListOptions) => {
|
|||
|
||||
return members;
|
||||
};
|
||||
|
||||
export default listOtherTeamMembers;
|
||||
|
|
|
@ -41,3 +41,5 @@ export const listOtherTeamHandler = async ({ ctx }: ListOptions) => {
|
|||
...team,
|
||||
}));
|
||||
};
|
||||
|
||||
export default listOtherTeamHandler;
|
||||
|
|
|
@ -82,3 +82,5 @@ export const publishHandler = async ({ ctx }: PublishOptions) => {
|
|||
message: "Team published successfully",
|
||||
};
|
||||
};
|
||||
|
||||
export default publishHandler;
|
||||
|
|
|
@ -55,3 +55,5 @@ export const setPasswordHandler = async ({ ctx, input }: UpdateOptions) => {
|
|||
|
||||
return { update: true };
|
||||
};
|
||||
|
||||
export default setPasswordHandler;
|
||||
|
|
|
@ -100,3 +100,5 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
|
|||
|
||||
return { update: true, userId: ctx.user.id };
|
||||
};
|
||||
|
||||
export default updateHandler;
|
||||
|
|
|
@ -0,0 +1,69 @@
|
|||
import { isOrganisationAdmin } from "@calcom/lib/server/queries/organisations";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
|
||||
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
import type { TUpdateUserInputSchema } from "./updateUser.schema";
|
||||
|
||||
type UpdateUserOptions = {
|
||||
ctx: {
|
||||
user: NonNullable<TrpcSessionUser>;
|
||||
};
|
||||
input: TUpdateUserInputSchema;
|
||||
};
|
||||
|
||||
export const updateUserHandler = async ({ ctx, input }: UpdateUserOptions) => {
|
||||
const { user } = ctx;
|
||||
const { id: userId, organizationId } = user;
|
||||
if (!organizationId)
|
||||
throw new TRPCError({ code: "UNAUTHORIZED", message: "You must be a memeber of an organizaiton" });
|
||||
|
||||
if (!(await isOrganisationAdmin(userId, organizationId))) throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||
|
||||
// Is requested user a member of the organization?
|
||||
const requestedMember = await prisma.membership.findFirst({
|
||||
where: {
|
||||
userId: input.userId,
|
||||
teamId: organizationId,
|
||||
accepted: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!requestedMember)
|
||||
throw new TRPCError({ code: "UNAUTHORIZED", message: "User does not belong to your organization" });
|
||||
|
||||
// Update user
|
||||
await prisma.$transaction([
|
||||
prisma.user.update({
|
||||
where: {
|
||||
id: input.userId,
|
||||
},
|
||||
data: {
|
||||
bio: input.bio,
|
||||
email: input.email,
|
||||
name: input.name,
|
||||
timeZone: input.timeZone,
|
||||
},
|
||||
}),
|
||||
prisma.membership.update({
|
||||
where: {
|
||||
userId_teamId: {
|
||||
userId: input.userId,
|
||||
teamId: organizationId,
|
||||
},
|
||||
},
|
||||
data: {
|
||||
role: input.role,
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
// TODO: audit log this
|
||||
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
};
|
||||
|
||||
export default updateUserHandler;
|
|
@ -0,0 +1,12 @@
|
|||
import { z } from "zod";
|
||||
|
||||
export const ZUpdateUserInputSchema = z.object({
|
||||
userId: z.number(),
|
||||
bio: z.string().optional(),
|
||||
name: z.string().optional(),
|
||||
email: z.string().optional(),
|
||||
role: z.enum(["ADMIN", "MEMBER"]),
|
||||
timeZone: z.string(),
|
||||
});
|
||||
|
||||
export type TUpdateUserInputSchema = z.infer<typeof ZUpdateUserInputSchema>;
|
|
@ -38,3 +38,5 @@ export const verifyCodeHandler = async ({ ctx, input }: VerifyCodeOptions) => {
|
|||
|
||||
return isValidToken;
|
||||
};
|
||||
|
||||
export default verifyCodeHandler;
|
||||
|
|
|
@ -15,3 +15,41 @@ export const middleware = tRPCContext.middleware;
|
|||
export const procedure = tRPCContext.procedure;
|
||||
|
||||
export type TrpcSessionUser = UserFromSession;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
const UNSTABLE_HANDLER_CACHE: Record<string, Function> = {};
|
||||
|
||||
/**
|
||||
* This function will import the module defined in importer just once and then cache the default export of that module.
|
||||
*
|
||||
* It gives you the default export of the module.
|
||||
*
|
||||
* **Note: It is your job to ensure that the name provided is unique across all routes.**
|
||||
* @example
|
||||
* ```ts
|
||||
const handler = await importHandler("myUniqueNameSpace", () => import("./getUser.handler"));
|
||||
return handler({ ctx, input });
|
||||
* ```
|
||||
*/
|
||||
export const importHandler = async <
|
||||
T extends {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
default: Function;
|
||||
}
|
||||
>(
|
||||
/**
|
||||
* The name of the handler in cache. It has to be unique across all routes
|
||||
*/
|
||||
name: string,
|
||||
importer: () => Promise<T>
|
||||
) => {
|
||||
const nameInCache = name as keyof typeof UNSTABLE_HANDLER_CACHE;
|
||||
|
||||
if (!UNSTABLE_HANDLER_CACHE[nameInCache]) {
|
||||
const importedModule = await importer();
|
||||
UNSTABLE_HANDLER_CACHE[nameInCache] = importedModule.default;
|
||||
return importedModule.default as T["default"];
|
||||
}
|
||||
|
||||
return UNSTABLE_HANDLER_CACHE[nameInCache] as unknown as T["default"];
|
||||
};
|
||||
|
|
|
@ -12,7 +12,7 @@ import { Tooltip } from "../tooltip";
|
|||
|
||||
export type AvatarProps = {
|
||||
className?: string;
|
||||
size: "xxs" | "xs" | "xsm" | "sm" | "md" | "mdLg" | "lg" | "xl";
|
||||
size?: "xxs" | "xs" | "xsm" | "sm" | "md" | "mdLg" | "lg" | "xl";
|
||||
imageSrc?: Maybe<string>;
|
||||
title?: string;
|
||||
alt: string;
|
||||
|
@ -35,7 +35,7 @@ const sizesPropsBySize = {
|
|||
} as const;
|
||||
|
||||
export function Avatar(props: AvatarProps) {
|
||||
const { imageSrc, gravatarFallbackMd5, size, alt, title, href } = props;
|
||||
const { imageSrc, gravatarFallbackMd5, size = "md", alt, title, href } = props;
|
||||
const rootClass = classNames("aspect-square rounded-full", sizesPropsBySize[size]);
|
||||
let avatar = (
|
||||
<AvatarPrimitive.Root
|
||||
|
|
|
@ -95,7 +95,7 @@ const CommandSeparator = React.forwardRef<
|
|||
>(({ className, ...props }, ref) => (
|
||||
<CommandPrimitive.Separator
|
||||
ref={ref}
|
||||
className={classNames("bg-border -mx-1 h-px", className)}
|
||||
className={classNames("bg-subtle -mx-1 mb-2 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
|
|
|
@ -1,16 +1,25 @@
|
|||
import type { Table } from "@tanstack/react-table";
|
||||
import { Fragment } from "react";
|
||||
|
||||
import type { SVGComponent } from "@calcom/types/SVGComponent";
|
||||
|
||||
import { Button } from "../button";
|
||||
|
||||
export type ActionItem<TData> =
|
||||
| {
|
||||
type: "action";
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
icon?: SVGComponent;
|
||||
}
|
||||
| {
|
||||
type: "render";
|
||||
render: (table: Table<TData>) => React.ReactNode;
|
||||
};
|
||||
|
||||
interface DataTableSelectionBarProps<TData> {
|
||||
table: Table<TData>;
|
||||
actions?: {
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
icon?: SVGComponent;
|
||||
}[];
|
||||
actions?: ActionItem<TData>[];
|
||||
}
|
||||
|
||||
export function DataTableSelectionBar<TData>({ table, actions }: DataTableSelectionBarProps<TData>) {
|
||||
|
@ -21,10 +30,16 @@ export function DataTableSelectionBar<TData>({ table, actions }: DataTableSelect
|
|||
return (
|
||||
<div className="bg-brand-default text-brand item-center absolute bottom-0 left-1/2 flex -translate-x-1/2 gap-4 rounded-lg p-2">
|
||||
<div className="text-brand-subtle my-auto px-2">{numberOfSelectedRows} selected</div>
|
||||
{actions?.map((action) => (
|
||||
<Button aria-label={action.label} onClick={action.onClick} StartIcon={action.icon} key={action.label}>
|
||||
{action.label}
|
||||
</Button>
|
||||
{actions?.map((action, index) => (
|
||||
<Fragment key={index}>
|
||||
{action.type === "action" ? (
|
||||
<Button aria-label={action.label} onClick={action.onClick} StartIcon={action.icon}>
|
||||
{action.label}
|
||||
</Button>
|
||||
) : action.type === "render" ? (
|
||||
action.render(table)
|
||||
) : null}
|
||||
</Fragment>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -17,9 +17,8 @@ import {
|
|||
import { useState } from "react";
|
||||
import { useVirtual } from "react-virtual";
|
||||
|
||||
import type { SVGComponent } from "@calcom/types/SVGComponent";
|
||||
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../table/TableNew";
|
||||
import type { ActionItem } from "./DataTableSelectionBar";
|
||||
import { DataTableSelectionBar } from "./DataTableSelectionBar";
|
||||
import type { FilterableItems } from "./DataTableToolbar";
|
||||
import { DataTableToolbar } from "./DataTableToolbar";
|
||||
|
@ -30,11 +29,7 @@ export interface DataTableProps<TData, TValue> {
|
|||
data: TData[];
|
||||
searchKey?: string;
|
||||
filterableItems?: FilterableItems;
|
||||
selectionOptions?: {
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
icon?: SVGComponent;
|
||||
}[];
|
||||
selectionOptions?: ActionItem<TData>[];
|
||||
tableCTA?: React.ReactNode;
|
||||
isLoading?: boolean;
|
||||
onScroll?: (e: React.UIEvent<HTMLDivElement, UIEvent>) => void;
|
||||
|
|
|
@ -52,10 +52,10 @@ const sheetVariants = cva(
|
|||
{
|
||||
variants: {
|
||||
position: {
|
||||
top: "animate-in slide-in-from-top w-full duration-300",
|
||||
bottom: "animate-in slide-in-from-bottom w-full duration-300",
|
||||
left: "animate-in slide-in-from-left h-full duration-300",
|
||||
right: "animate-in slide-in-from-right h-full duration-300",
|
||||
top: "animate-in slide-in-from-top w-full duration-200",
|
||||
bottom: "animate-in slide-in-from-bottom w-full duration-200",
|
||||
left: "animate-in slide-in-from-left h-full duration-200",
|
||||
right: "animate-in slide-in-from-right h-full duration-200",
|
||||
},
|
||||
size: {
|
||||
content: "",
|
||||
|
@ -105,12 +105,12 @@ const sheetVariants = cva(
|
|||
{
|
||||
position: ["right", "left"],
|
||||
size: "default",
|
||||
class: "w-1/3 h-[calc(100vh-2rem)]",
|
||||
class: "w-1/3 max-h-[calc(100vh-2rem)]",
|
||||
},
|
||||
{
|
||||
position: ["right", "left"],
|
||||
size: "sm",
|
||||
class: "w-1/4 h-[calc(100vh-2rem)]",
|
||||
class: "w-1/4 max-h-[calc(100vh-2rem)]",
|
||||
},
|
||||
{
|
||||
position: ["right", "left"],
|
||||
|
|
|
@ -138,3 +138,26 @@ export { useCalcomTheme } from "./styles/useCalcomTheme";
|
|||
export { ScrollableArea } from "./components/scrollable/ScrollableArea";
|
||||
export { WizardLayout } from "./layouts/WizardLayout";
|
||||
export { DataTable } from "./components/data-table";
|
||||
export {
|
||||
Sheet,
|
||||
SheetClose,
|
||||
SheetContent,
|
||||
SheetDescription,
|
||||
SheetFooter,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
} from "./components/sheet/sheet";
|
||||
export {
|
||||
Command,
|
||||
CommandDialog,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
CommandInput,
|
||||
CommandSeparator,
|
||||
CommandShortcut,
|
||||
} from "./components/command";
|
||||
|
||||
export { Popover, PopoverContent, PopoverTrigger } from "./components/popover";
|
||||
|
|
46
yarn.lock
46
yarn.lock
|
@ -4925,6 +4925,7 @@ __metadata:
|
|||
stripe: ^9.16.0
|
||||
superjson: 1.9.1
|
||||
tailwindcss: ^3.3.1
|
||||
tailwindcss-animate: ^1.0.6
|
||||
tailwindcss-radix: ^2.6.0
|
||||
ts-node: ^10.9.1
|
||||
turndown: ^7.1.1
|
||||
|
@ -5011,7 +5012,6 @@ __metadata:
|
|||
next-collect: ^0.2.1
|
||||
next-i18next: ^13.2.2
|
||||
next-seo: ^6.0.0
|
||||
playwright: ^1.31.2
|
||||
postcss: ^8.4.18
|
||||
prism-react-renderer: ^1.3.5
|
||||
react: ^18.2.0
|
||||
|
@ -15758,6 +15758,7 @@ __metadata:
|
|||
jsdom: ^22.0.0
|
||||
lint-staged: ^12.5.0
|
||||
lucide-react: ^0.171.0
|
||||
mailhog: ^4.16.0
|
||||
prettier: ^2.8.6
|
||||
tsc-absolute: ^1.0.0
|
||||
turbo: ^1.10.1
|
||||
|
@ -22519,7 +22520,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"iconv-lite@npm:0.6.3, iconv-lite@npm:^0.6.2, iconv-lite@npm:^0.6.3":
|
||||
"iconv-lite@npm:0.6.3, iconv-lite@npm:^0.6, iconv-lite@npm:^0.6.2, iconv-lite@npm:^0.6.3":
|
||||
version: 0.6.3
|
||||
resolution: "iconv-lite@npm:0.6.3"
|
||||
dependencies:
|
||||
|
@ -25491,6 +25492,18 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mailhog@npm:^4.16.0":
|
||||
version: 4.16.0
|
||||
resolution: "mailhog@npm:4.16.0"
|
||||
dependencies:
|
||||
iconv-lite: ^0.6
|
||||
dependenciesMeta:
|
||||
iconv-lite:
|
||||
optional: true
|
||||
checksum: 3fe666bd0cb4cd6998da77e4b362ad1f34e4b8e8fc06724dba72712fe8f091dbc4e6d15e315c88d83bff6b9d3e682536cab51f376752ae340504265bf0bb3dd8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"make-dir@npm:^2.0.0, make-dir@npm:^2.1.0":
|
||||
version: 2.1.0
|
||||
resolution: "make-dir@npm:2.1.0"
|
||||
|
@ -28916,26 +28929,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"playwright-core@npm:1.36.2":
|
||||
version: 1.36.2
|
||||
resolution: "playwright-core@npm:1.36.2"
|
||||
bin:
|
||||
playwright-core: cli.js
|
||||
checksum: 2193ce802ef93c28b9b5e11a0b1d7b60778c686015659978d1cbf0eb9cda2cdc85ec5575b887c1346e9d161cc2805bf27638d76a2f7f857dffeae968e6ceffcd
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"playwright@npm:^1.31.2":
|
||||
version: 1.36.2
|
||||
resolution: "playwright@npm:1.36.2"
|
||||
dependencies:
|
||||
playwright-core: 1.36.2
|
||||
bin:
|
||||
playwright: cli.js
|
||||
checksum: 5876b65a0f1303e45f99c7d120706af0ab808efd5d89c514741584ff1060408b62148ae2790c2e6527642f2b8f49db682710b87d3df7b3ba510e8e847e6041ef
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"pngjs@npm:^3.0.0, pngjs@npm:^3.3.3":
|
||||
version: 3.4.0
|
||||
resolution: "pngjs@npm:3.4.0"
|
||||
|
@ -34086,6 +34079,15 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tailwindcss-animate@npm:^1.0.6":
|
||||
version: 1.0.6
|
||||
resolution: "tailwindcss-animate@npm:1.0.6"
|
||||
peerDependencies:
|
||||
tailwindcss: "*"
|
||||
checksum: 01471cb5e64d0936b3dc90bdb1d4b379e8b73b3d285553337d8576e5d458e868ddff99c6db0ae454c1be69ce8f88dc0eed44ea62e4d7bca10d7d2479fc4c8ee0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tailwindcss-radix@npm:^2.6.0":
|
||||
version: 2.6.0
|
||||
resolution: "tailwindcss-radix@npm:2.6.0"
|
||||
|
|
Loading…
Reference in New Issue