Compare commits
106 Commits
main
...
feat/org-u
Author | SHA1 | Date |
---|---|---|
Alex van Andel | 0fad273741 | |
Crowdin Bot | eeabeb355d | |
Ritesh Patil | b92303b59f | |
Leo Giovanetti | ce93355c16 | |
Pratik Kumar | 9778a30afb | |
Bhargav | 373ec296bf | |
Varun Thummar | 1e7209b81b | |
Rama Krishna Reddy | e7220c4bdd | |
Carina Wollendorfer | 682d4793f5 | |
Alex van Andel | 6e396bc65f | |
Rama Krishna Reddy | 7850131b0b | |
Crowdin Bot | 2ed3e09c20 | |
Shivam Kalra | c5dd667ef9 | |
Crowdin Bot | 990e6fa52d | |
Alex van Andel | c5dadb3d7d | |
Alex van Andel | fd880c80a6 | |
Crowdin Bot | fdb629be53 | |
sajanlamsal | 2eea0d2731 | |
Crowdin Bot | c5fccf357b | |
Shivam Kalra | 6688f8d89c | |
Crowdin Bot | f89ba37e0e | |
Hariom Balhara | bce96f7c87 | |
Hariom Balhara | c2815e52ce | |
mohammed hussam | b5e37c3175 | |
Crowdin Bot | e08fc09fee | |
Crowdin Bot | a7880a99a1 | |
Hariom Balhara | a185861a06 | |
Sean Brydon | 6529d32b49 | |
Sean Brydon | 587c788722 | |
Sean Brydon | 9214873272 | |
Sean Brydon | 35b4168eac | |
Sean Brydon | bf68100cdd | |
Sean Brydon | f7b8121c17 | |
Sean Brydon | 77f924e5a5 | |
Sean Brydon | 7a786c612d | |
Sean Brydon | d64e874dbe | |
Sean Brydon | 8dd61453b2 | |
Sean Brydon | 4f5c71e95b | |
Sean Brydon | 521110ed15 | |
Sean Brydon | 5625d1f347 | |
GitStart-Cal.com | 3840e40e68 | |
Neel Patel | 5e4f365e92 | |
sean-brydon | da23bd78a3 | |
Udit Takkar | d7800ac21e | |
Keith Williams | 4bd2ea85f9 | |
sean-brydon | 74815727ba | |
Udit Takkar | 7e8e9f5410 | |
Crowdin Bot | ac19bbe603 | |
Udit Takkar | c14356ecf9 | |
Shivam Kalra | 29b28bd803 | |
Danila | 84c0b6734a | |
GitStart-Cal.com | ce2df3feec | |
GitStart-Cal.com | b819f9ca12 | |
Crowdin Bot | 4c0bfc8312 | |
Peer Richelsen | 44c885aeab | |
nicktrn | bbcbadbc4b | |
Richard Poelderl | 6f24a79760 | |
Anik Dhabal Babu | 1c282a2846 | |
Cherish | b2949e7c1d | |
Alex van Andel | d4a0a0af85 | |
Alex van Andel | 47d1c78f76 | |
Crowdin Bot | d08fcc0eb6 | |
sajanlamsal | 801af91e53 | |
Crowdin Bot | df7fffd369 | |
GitStart-Cal.com | 13c12bbfb8 | |
GitStart-Cal.com | 7131d9f8ee | |
GitStart-Cal.com | 4d3e86330a | |
Shivam Kalra | bfcd193f6f | |
Peer Richelsen | 9a906f4d95 | |
Crowdin Bot | 714ecfb511 | |
Hariom Balhara | 8d0820be00 | |
Janakiram Yellapu | 6e587d32ad | |
sean-brydon | f978aef532 | |
Udit Takkar | b3fa34f01b | |
sydwardrae | 65c9b23a60 | |
Hariom Balhara | 1ace1f7d43 | |
Alex van Andel | 224f9dafed | |
Leo Giovanetti | f5b923968c | |
Hariom Balhara | 007a8d3da2 | |
Leo Giovanetti | 5ccea60719 | |
nicktrn | 645cce6c3a | |
Udit Takkar | 5a2fb9c506 | |
sean-brydon | b9f1bdf283 | |
Anik Dhabal Babu | 80d7973cfe | |
Crowdin Bot | ca1e831836 | |
sean-brydon | 39f61d1757 | |
Carina Wollendorfer | 73ec25b511 | |
mohammed hussam | 5766826efc | |
Richard Poelderl | 2d7bbb7951 | |
Udit Takkar | d7d2641cfc | |
Pradumn Kumar | 0aa7bec1b0 | |
Crowdin Bot | 2734a0b2bd | |
Leo Giovanetti | 1f3427d91a | |
Crowdin Bot | 5af733ee2c | |
Hariom Balhara | 55808b22be | |
Crowdin Bot | 07bd24ffb0 | |
Crowdin Bot | 22854ca8f0 | |
Keith Williams | b95447c351 | |
Hariom Balhara | daf3aa0a42 | |
Sean Brydon | 6fc3a9e1ca | |
Sean Brydon | 85c3354afd | |
Sean Brydon | 0540cf34d9 | |
Sean Brydon | abc5db02c4 | |
Sean Brydon | 9d39d20d37 | |
Sean Brydon | f4a2bd54b4 | |
Sean Brydon | 3a2f9d5eb6 |
|
@ -167,6 +167,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"
|
||||
},
|
||||
|
|
|
@ -1256,6 +1256,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",
|
||||
|
@ -1970,6 +1971,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",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
|
|
|
@ -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")],
|
||||
};
|
||||
|
|
|
@ -3,12 +3,28 @@
|
|||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"~/*": ["modules/*"],
|
||||
"@components/*": ["components/*"],
|
||||
"@lib/*": ["lib/*"],
|
||||
"@server/*": ["server/*"],
|
||||
"@prisma/client/*": ["@calcom/prisma/client/*"]
|
||||
}
|
||||
"~/*": [
|
||||
"modules/*"
|
||||
],
|
||||
"@components/*": [
|
||||
"components/*"
|
||||
],
|
||||
"@lib/*": [
|
||||
"lib/*"
|
||||
],
|
||||
"@server/*": [
|
||||
"server/*"
|
||||
],
|
||||
"@prisma/client/*": [
|
||||
"@calcom/prisma/client/*"
|
||||
]
|
||||
},
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"strictNullChecks": true
|
||||
},
|
||||
"include": [
|
||||
/* Find a way to not require this - App files don't belong here. */
|
||||
|
@ -17,7 +33,10 @@
|
|||
"../../packages/types/*.d.ts",
|
||||
"../../packages/types/next-auth.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx"
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts"
|
||||
],
|
||||
"exclude": ["node_modules"]
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ export function BookFormAsModal({ visible, onCancel }: { visible: boolean; onCan
|
|||
const selectedDuration = useBookerStore((state) => state.selectedDuration);
|
||||
const { data } = useEvent();
|
||||
const parsedSelectedTimeslot = dayjs(selectedTimeslot);
|
||||
|
||||
const { timeFormat, timezone } = useTimePreferences();
|
||||
|
||||
return (
|
||||
|
|
|
@ -0,0 +1,50 @@
|
|||
import type { Table } from "@tanstack/react-table";
|
||||
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 {
|
||||
table: Table<User>;
|
||||
}
|
||||
|
||||
export function DeleteBulkUsers({ table }: Props) {
|
||||
const { t } = useLocale();
|
||||
const selectedRows = table.getSelectedRowModel().flatRows.map((row) => row.original); // 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,133 @@
|
|||
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,
|
||||
CommandSeparator,
|
||||
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={() => {
|
||||
console.log("onSelect", isSelected);
|
||||
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>
|
||||
<>
|
||||
<CommandSeparator />
|
||||
<CommandGroup>
|
||||
<div className="mb-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>
|
||||
</CommandGroup>
|
||||
</>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</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,249 @@
|
|||
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 { useOrgBranding } from "@calcom/ee/organizations/context/provider";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetFooter,
|
||||
Avatar,
|
||||
Skeleton,
|
||||
Form,
|
||||
TextField,
|
||||
ToggleGroup,
|
||||
TextAreaField,
|
||||
TimezoneSelect,
|
||||
Label,
|
||||
showToast,
|
||||
Loader,
|
||||
} from "@calcom/ui";
|
||||
|
||||
import type { State, Action } from "../UserListTable";
|
||||
import { DisplayInfo } from "./DisplayInfo";
|
||||
import { SheetFooterControls } from "./SheetFooterControls";
|
||||
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>;
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
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,12 @@ 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 table={table} />,
|
||||
},
|
||||
]}
|
||||
tableContainerRef={tableContainerRef}
|
||||
|
@ -326,6 +335,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 };
|
||||
}
|
|
@ -2,13 +2,17 @@ import { ZVerifyCodeInputSchema } from "@calcom/prisma/zod-utils";
|
|||
|
||||
import authedProcedure, { authedAdminProcedure } from "../../../procedures/authedProcedure";
|
||||
import { 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 { ZGetUserInput } from "./getUser.schema";
|
||||
import { ZListMembersSchema } from "./listMembers.schema";
|
||||
import { ZSetPasswordSchema } from "./setPassword.schema";
|
||||
import { ZUpdateInputSchema } from "./update.schema";
|
||||
import { ZUpdateUserInputSchema } from "./updateUser.schema";
|
||||
|
||||
type OrganizationsRouterHandlerCache = {
|
||||
create?: typeof import("./create.handler").createHandler;
|
||||
|
@ -24,6 +28,11 @@ type OrganizationsRouterHandlerCache = {
|
|||
listMembers?: typeof import("./listMembers.handler").listMembersHandler;
|
||||
getBrand?: typeof import("./getBrand.handler").getBrandHandler;
|
||||
getMembers?: typeof import("./getMembers.handler").getMembersHandler;
|
||||
getUser?: typeof import("./getUser.handler").getUserHandler;
|
||||
updateUser?: typeof import("./updateUser.handler").updateUserHandler;
|
||||
getTeams?: typeof import("./getTeams.handler").getTeamsHandler;
|
||||
bulkAddToTeams?: typeof import("./addBulkTeams.handler").addBulkTeamsHandler;
|
||||
bulkDeleteUsers?: typeof import("./bulkDeleteUsers.handler").bulkDeleteUsersHandler;
|
||||
};
|
||||
|
||||
const UNSTABLE_HANDLER_CACHE: OrganizationsRouterHandlerCache = {};
|
||||
|
@ -227,4 +236,84 @@ export const viewerOrganizationsRouter = router({
|
|||
ctx,
|
||||
});
|
||||
}),
|
||||
getUser: authedProcedure.input(ZGetUserInput).query(async ({ ctx, input }) => {
|
||||
if (!UNSTABLE_HANDLER_CACHE.getUser) {
|
||||
UNSTABLE_HANDLER_CACHE.getUser = await import("./getUser.handler").then((mod) => mod.getUserHandler);
|
||||
}
|
||||
|
||||
// Unreachable code but required for type safety
|
||||
if (!UNSTABLE_HANDLER_CACHE.getUser) {
|
||||
throw new Error("Failed to load handler");
|
||||
}
|
||||
|
||||
return UNSTABLE_HANDLER_CACHE.getUser({
|
||||
ctx,
|
||||
input,
|
||||
});
|
||||
}),
|
||||
updateUser: authedProcedure.input(ZUpdateUserInputSchema).mutation(async ({ ctx, input }) => {
|
||||
if (!UNSTABLE_HANDLER_CACHE.updateUser) {
|
||||
UNSTABLE_HANDLER_CACHE.updateUser = await import("./updateUser.handler").then(
|
||||
(mod) => mod.updateUserHandler
|
||||
);
|
||||
}
|
||||
|
||||
// Unreachable code but required for type safety
|
||||
if (!UNSTABLE_HANDLER_CACHE.updateUser) {
|
||||
throw new Error("Failed to load handler");
|
||||
}
|
||||
|
||||
return UNSTABLE_HANDLER_CACHE.updateUser({
|
||||
ctx,
|
||||
input,
|
||||
});
|
||||
}),
|
||||
getTeams: authedProcedure.query(async ({ ctx }) => {
|
||||
if (!UNSTABLE_HANDLER_CACHE.getTeams) {
|
||||
UNSTABLE_HANDLER_CACHE.getTeams = await import("./getTeams.handler").then((mod) => mod.getTeamsHandler);
|
||||
}
|
||||
|
||||
// Unreachable code but required for type safety
|
||||
if (!UNSTABLE_HANDLER_CACHE.getTeams) {
|
||||
throw new Error("Failed to load handler");
|
||||
}
|
||||
|
||||
return UNSTABLE_HANDLER_CACHE.getTeams({
|
||||
ctx,
|
||||
});
|
||||
}),
|
||||
bulkAddToTeams: authedProcedure.input(ZAddBulkTeams).mutation(async ({ ctx, input }) => {
|
||||
if (!UNSTABLE_HANDLER_CACHE.bulkAddToTeams) {
|
||||
UNSTABLE_HANDLER_CACHE.bulkAddToTeams = await import("./addBulkTeams.handler").then(
|
||||
(mod) => mod.addBulkTeamsHandler
|
||||
);
|
||||
}
|
||||
|
||||
// Unreachable code but required for type safety
|
||||
if (!UNSTABLE_HANDLER_CACHE.bulkAddToTeams) {
|
||||
throw new Error("Failed to load handler");
|
||||
}
|
||||
|
||||
return UNSTABLE_HANDLER_CACHE.bulkAddToTeams({
|
||||
ctx,
|
||||
input,
|
||||
});
|
||||
}),
|
||||
bulkDeleteUsers: authedProcedure.input(ZBulkUsersDelete).mutation(async ({ ctx, input }) => {
|
||||
if (!UNSTABLE_HANDLER_CACHE.bulkDeleteUsers) {
|
||||
UNSTABLE_HANDLER_CACHE.bulkDeleteUsers = await import("./bulkDeleteUsers.handler").then(
|
||||
(mod) => mod.bulkDeleteUsersHandler
|
||||
);
|
||||
}
|
||||
|
||||
// Unreachable code but required for type safety
|
||||
if (!UNSTABLE_HANDLER_CACHE.bulkDeleteUsers) {
|
||||
throw new Error("Failed to load handler");
|
||||
}
|
||||
|
||||
return UNSTABLE_HANDLER_CACHE.bulkDeleteUsers({
|
||||
ctx,
|
||||
input,
|
||||
});
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
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: {
|
||||
user: {
|
||||
organizationId: currentUser.organizationId,
|
||||
id: {
|
||||
in: input.userIds,
|
||||
},
|
||||
},
|
||||
accepted: true,
|
||||
},
|
||||
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);
|
||||
});
|
||||
|
||||
// Loop over all users and add them to all teams in the array
|
||||
const membershipData = filteredUserIds.flatMap((userId) =>
|
||||
input.teamIds.map((teamId) => {
|
||||
return {
|
||||
userId,
|
||||
teamId,
|
||||
role: MembershipRole.MEMBER,
|
||||
accepted: true, // Auto accept cause we know at this point they are already in the organization
|
||||
} as Prisma.MembershipCreateManyInput;
|
||||
})
|
||||
);
|
||||
|
||||
await prisma.membership.createMany({
|
||||
data: membershipData,
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
invitedTotalUsers: input.userIds.length,
|
||||
};
|
||||
}
|
|
@ -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>;
|
|
@ -0,0 +1,60 @@
|
|||
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: {
|
||||
organizationId: null,
|
||||
},
|
||||
});
|
||||
// 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,
|
||||
};
|
||||
}
|
|
@ -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>;
|
|
@ -0,0 +1,34 @@
|
|||
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;
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
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;
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
import { z } from "zod";
|
||||
|
||||
export const ZGetUserInput = z.object({
|
||||
userId: z.number().optional(),
|
||||
});
|
||||
|
||||
export type TGetUserInput = z.infer<typeof ZGetUserInput>;
|
|
@ -0,0 +1,67 @@
|
|||
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,
|
||||
};
|
||||
};
|
|
@ -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>;
|
|
@ -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"],
|
||||
|
|
|
@ -36,7 +36,7 @@ const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTML
|
|||
({ className, ...props }, ref) => (
|
||||
<tr
|
||||
ref={ref}
|
||||
className={classNames("hover:bg-subtle data-[state=selected]:bg-subtle border-b", className)}
|
||||
className={classNames("hover:bg-subtle data-[state=selected]:bg-muted border-b", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
|
|
@ -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";
|
||||
|
|
98
yarn.lock
98
yarn.lock
|
@ -131,25 +131,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@auth/core@npm:^0.1.4":
|
||||
version: 0.1.4
|
||||
resolution: "@auth/core@npm:0.1.4"
|
||||
dependencies:
|
||||
"@panva/hkdf": 1.0.2
|
||||
cookie: 0.5.0
|
||||
jose: 4.11.1
|
||||
oauth4webapi: 2.0.5
|
||||
preact: 10.11.3
|
||||
preact-render-to-string: 5.2.3
|
||||
peerDependencies:
|
||||
nodemailer: 6.8.0
|
||||
peerDependenciesMeta:
|
||||
nodemailer:
|
||||
optional: true
|
||||
checksum: 64854404ea1883e0deb5535b34bed95cd43fc85094aeaf4f15a79e14045020eb944f844defe857edfc8528a0a024be89cbb2a3069dedef0e9217a74ca6c3eb79
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@aws-crypto/ie11-detection@npm:^3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "@aws-crypto/ie11-detection@npm:3.0.0"
|
||||
|
@ -3851,41 +3832,6 @@ __metadata:
|
|||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@calcom/auth@workspace:apps/auth":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@calcom/auth@workspace:apps/auth"
|
||||
dependencies:
|
||||
"@auth/core": ^0.1.4
|
||||
"@calcom/app-store": "*"
|
||||
"@calcom/app-store-cli": "*"
|
||||
"@calcom/config": "*"
|
||||
"@calcom/core": "*"
|
||||
"@calcom/dayjs": "*"
|
||||
"@calcom/embed-core": "workspace:*"
|
||||
"@calcom/embed-react": "workspace:*"
|
||||
"@calcom/embed-snippet": "workspace:*"
|
||||
"@calcom/features": "*"
|
||||
"@calcom/lib": "*"
|
||||
"@calcom/prisma": "*"
|
||||
"@calcom/trpc": "*"
|
||||
"@calcom/tsconfig": "*"
|
||||
"@calcom/types": "*"
|
||||
"@calcom/ui": "*"
|
||||
"@types/node": 16.9.1
|
||||
"@types/react": 18.0.26
|
||||
"@types/react-dom": ^18.0.9
|
||||
eslint: ^8.34.0
|
||||
eslint-config-next: ^13.2.1
|
||||
next: ^13.4.6
|
||||
next-auth: ^4.22.1
|
||||
postcss: ^8.4.18
|
||||
react: ^18.2.0
|
||||
react-dom: ^18.2.0
|
||||
tailwindcss: ^3.3.1
|
||||
typescript: ^4.9.4
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@calcom/caldavcalendar@workspace:packages/app-store/caldavcalendar":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@calcom/caldavcalendar@workspace:packages/app-store/caldavcalendar"
|
||||
|
@ -4880,6 +4826,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
|
||||
|
@ -7659,13 +7606,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@panva/hkdf@npm:1.0.2":
|
||||
version: 1.0.2
|
||||
resolution: "@panva/hkdf@npm:1.0.2"
|
||||
checksum: 75183b4d5ea816ef516dcea70985c610683579a9e2ac540c2d59b9a3ed27eedaff830a43a1c43c1683556a457c92ac66e09109ee995ab173090e4042c4c4bb03
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@panva/hkdf@npm:^1.0.2":
|
||||
version: 1.0.4
|
||||
resolution: "@panva/hkdf@npm:1.0.4"
|
||||
|
@ -23936,13 +23876,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"jose@npm:4.11.1":
|
||||
version: 4.11.1
|
||||
resolution: "jose@npm:4.11.1"
|
||||
checksum: cd15cba258d0fd20f6168631ce2e94fda8442df80e43c1033c523915cecdf390a1cc8efe0eab0c2d65935ca973d791c668aea80724d2aa9c2879d4e70f3081d7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"jose@npm:4.12.0":
|
||||
version: 4.12.0
|
||||
resolution: "jose@npm:4.12.0"
|
||||
|
@ -27522,13 +27455,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"oauth4webapi@npm:2.0.5":
|
||||
version: 2.0.5
|
||||
resolution: "oauth4webapi@npm:2.0.5"
|
||||
checksum: 32d0cb7b1cca42d51dfb88075ca2d69fe33172a807e8ea50e317d17cab3bc80588ab8ebcb7eb4600c371a70af4674595b4b341daf6f3a655f1efa1ab715bb6c9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"oauth@npm:^0.9.15":
|
||||
version: 0.9.15
|
||||
resolution: "oauth@npm:0.9.15"
|
||||
|
@ -29201,17 +29127,6 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"preact-render-to-string@npm:5.2.3":
|
||||
version: 5.2.3
|
||||
resolution: "preact-render-to-string@npm:5.2.3"
|
||||
dependencies:
|
||||
pretty-format: ^3.8.0
|
||||
peerDependencies:
|
||||
preact: ">=10"
|
||||
checksum: 6e46288d8956adde35b9fe3a21aecd9dea29751b40f0f155dea62f3896f27cb8614d457b32f48d33909d2da81135afcca6c55077520feacd7d15164d1371fb44
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"preact-render-to-string@npm:^5.1.19":
|
||||
version: 5.2.6
|
||||
resolution: "preact-render-to-string@npm:5.2.6"
|
||||
|
@ -29223,7 +29138,7 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"preact@npm:10.11.3, preact@npm:^10.6.3":
|
||||
"preact@npm:^10.6.3":
|
||||
version: 10.11.3
|
||||
resolution: "preact@npm:10.11.3"
|
||||
checksum: 9387115aa0581e8226309e6456e9856f17dfc0e3d3e63f774de80f3d462a882ba7c60914c05942cb51d51e23e120dcfe904b8d392d46f29ad15802941fe7a367
|
||||
|
@ -33974,6 +33889,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