Compare commits

...

20 Commits

Author SHA1 Message Date
Alan dea991a80c merge with remote main 2023-10-01 17:21:17 -07:00
Alan 1166948016 merge with main 2023-09-18 16:42:39 -07:00
Alan 472b955ef4 improve seeder with users within new teams, fixed host-users avatars on collective 2023-09-11 18:51:23 -07:00
Alan 49944c223f Fix types and temporarily hiding pagination 2023-09-11 00:10:42 -07:00
Alan 198c7174e4 Add seeder for event types and teams 2023-09-10 23:37:54 -07:00
Alan 4df1064f4c Merge branch 'main' of github.com:calcom/cal.com into refactor-event-types-type-id-10419-cal-2264-cal-2296 2023-09-10 22:43:36 -07:00
Alan 1c2211e2bf merge with remote main 2023-09-07 16:48:17 -07:00
Alan 2a2cb14f6c Handle children on event types, managed events, readonly and team count 2023-09-06 17:25:17 -07:00
Alan 9d1ebbebd1 merge with main 2023-09-06 12:53:00 -07:00
Alan 3cdd210ee4 fix mobile tabs event types 2023-09-05 10:40:13 -07:00
Alan 5776d8b495 working mobile and desktop team event load 2023-09-04 22:40:46 -07:00
Alan 247df0c69e clean up 2023-09-04 13:14:36 -07:00
Peer Richelsen b48934b126
Merge branch 'main' into refactor-event-types-type-id-10419-cal-2264-cal-2296 2023-09-04 15:42:24 +02:00
Alan 932723abe6 WIP 2023-09-04 06:29:55 -07:00
Alan 4652b3ee01 merge with remote main 2023-09-03 10:59:15 -07:00
Alan 74fd513e44 WIP 2023-08-31 09:21:22 -07:00
Alan 35502c0784 Merge branch 'main' of github.com:calcom/cal.com into refactor-event-types-type-id-10419-cal-2264-cal-2296 2023-08-30 23:56:48 -07:00
Alan 0ae4604a6b merge with main 2023-08-30 20:05:23 -07:00
Alan 40d3c605e2 Merge branch 'main' of github.com:calcom/cal.com into refactor-event-types-type-id-10419-cal-2264-cal-2296 2023-08-30 13:32:21 -07:00
Alan d632d7881a WIP and merge with main 2023-08-28 20:07:11 -07:00
9 changed files with 1340 additions and 340 deletions

View File

@ -1,5 +1,6 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
import type { User } from "@prisma/client";
import type { TFunction } from "next-i18next";
import { Trans } from "next-i18next";
import Link from "next/link";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
@ -11,8 +12,10 @@ import { getLayout } from "@calcom/features/MainLayout";
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
import useIntercom from "@calcom/features/ee/support/lib/intercom/useIntercom";
import { EventTypeEmbedButton, EventTypeEmbedDialog } from "@calcom/features/embed/EventTypeEmbed";
import { EventTypeDescriptionLazy as EventTypeDescription } from "@calcom/features/eventtypes/components";
import CreateEventTypeDialog from "@calcom/features/eventtypes/components/CreateEventTypeDialog";
import {
CreateEventTypeDialog,
EventTypeDescriptionLazy as EventTypeDescription,
} from "@calcom/features/eventtypes/components";
import { DuplicateDialog } from "@calcom/features/eventtypes/components/DuplicateDialog";
import { TeamsFilter } from "@calcom/features/filters/components/TeamsFilter";
import { getTeamsFiltersFromQuery } from "@calcom/features/filters/lib/getTeamsFiltersFromQuery";
@ -24,6 +27,7 @@ import useMediaQuery from "@calcom/lib/hooks/useMediaQuery";
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
import { HttpError } from "@calcom/lib/http-error";
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import { SchedulingType } from "@calcom/prisma/enums";
import type { RouterOutputs } from "@calcom/trpc/react";
import { trpc, TRPCClientError } from "@calcom/trpc/react";
@ -35,7 +39,6 @@ import {
Button,
ButtonGroup,
ConfirmationDialogContent,
CreateButton,
Dialog,
Dropdown,
DropdownItem,
@ -53,6 +56,7 @@ import {
Switch,
Tooltip,
ArrowButton,
CreateButtonWithTeamsList,
} from "@calcom/ui";
import {
Clipboard,
@ -74,85 +78,99 @@ import useMeQuery from "@lib/hooks/useMeQuery";
import PageWrapper from "@components/PageWrapper";
import SkeletonLoader from "@components/eventtype/SkeletonLoader";
type EventTypeGroups = RouterOutputs["viewer"]["eventTypes"]["getByViewer"]["eventTypeGroups"];
type EventTypeGroupProfile = EventTypeGroups[number]["profile"];
type GetByViewerResponse = RouterOutputs["viewer"]["eventTypes"]["getByViewer"] | undefined;
interface EventTypeListHeadingProps {
profile: EventTypeGroupProfile;
teamSlugOrUsername: string;
teamNameOrUserName: string;
membershipCount: number;
teamId?: number | null;
orgSlug?: string;
}
type EventTypeGroup = EventTypeGroups[number];
type EventType = EventTypeGroup["eventTypes"][number];
type EventTypeList = RouterOutputs["viewer"]["eventTypes"]["paginate"];
type EventType = EventTypeList[number];
interface EventTypeListProps {
group: EventTypeGroup;
groupIndex: number;
readOnly: boolean;
types: EventType[];
data: EventTypeList;
readonly?: boolean;
}
interface MobileTeamsTabProps {
eventTypeGroups: EventTypeGroups;
teamEventTypes: EventTypeList[];
readonly?: boolean;
}
const querySchema = z.object({
teamId: z.nullable(z.coerce.number()).optional().default(null),
});
const MobileTeamsTab: FC<MobileTeamsTabProps> = (props) => {
const { eventTypeGroups } = props;
const MobileTeamsTab: FC<MobileTeamsTabProps> = (props: MobileTeamsTabProps) => {
const { teamEventTypes, readonly } = props;
const orgBranding = useOrgBranding();
const tabs = eventTypeGroups.map((item) => ({
name: item.profile.name ?? "",
href: item.teamId ? `/event-types?teamId=${item.teamId}` : "/event-types?noTeam",
avatar: orgBranding
? `${orgBranding.fullDomain}${item.teamId ? "/team" : ""}/${item.profile.slug}/avatar.png`
: item.profile.image ?? `${WEBAPP_URL + (item.teamId && "/team")}/${item.profile.slug}/avatar.png`,
}));
const tabs = teamEventTypes
.filter((item) => item !== undefined)
.map((item) => {
const [firstElement] = item;
const [mainUser] = firstElement?.users ?? [];
const teamSlugOrUsername = firstElement?.team?.slug || mainUser?.username || "";
const teamNameOrUserName = firstElement?.team?.name || mainUser?.name || "";
const teamId = firstElement?.team?.id;
return {
name: teamNameOrUserName,
href: teamId ? `/event-types?teamId=${teamId}` : "/event-types",
avatar: teamId
? `${orgBranding?.fullDomain ?? WEBAPP_URL}/${teamSlugOrUsername}/avatar.png`
: `${WEBAPP_URL}/${teamSlugOrUsername}/avatar.png`,
};
});
const { data } = useTypedQuery(querySchema);
const events = eventTypeGroups.filter((item) => item.teamId === data.teamId);
const eventsIndex = teamEventTypes.findIndex((item) => item[0]?.team?.id === data?.teamId);
const events = teamEventTypes[eventsIndex > -1 ? eventsIndex : 0];
return (
<div>
<HorizontalTabs tabs={tabs} />
{events.length > 0 ? (
<EventTypeList
types={events[0].eventTypes}
group={events[0]}
groupIndex={0}
readOnly={events[0].metadata.readOnly}
/>
{events && events.length > 0 ? (
<EventTypeList data={events} readonly={readonly} />
) : (
<CreateFirstEventTypeView slug={eventTypeGroups[0].profile.slug ?? ""} />
// @TODO: fix later when we have the context provider
<CreateFirstEventTypeView slug="" />
)}
</div>
);
};
const Item = ({ type, group, readOnly }: { type: EventType; group: EventTypeGroup; readOnly: boolean }) => {
const getEventTypeSlug = (eventType: EventType, t: TFunction) => {
const { team, schedulingType, users } = eventType;
if (team?.slug) {
return `/${team.slug}/${eventType.slug}`;
} else if (schedulingType === SchedulingType.MANAGED) {
return t("username_placeholder");
} else {
return `/${users[0].username}/${eventType.slug}`;
}
};
const Item = ({ eventType, readonly }: { eventType: EventType; readonly?: boolean }) => {
const { t } = useLocale();
const content = () => (
<div>
<span
className="text-default font-semibold ltr:mr-1 rtl:ml-1"
data-testid={"event-type-title-" + type.id}>
{type.title}
data-testid={"event-type-title-" + eventType.id}>
{eventType.title}
</span>
{group.profile.slug ? (
<small
className="text-subtle hidden font-normal leading-4 sm:inline"
data-testid={"event-type-slug-" + type.id}>
{`/${
type.schedulingType !== SchedulingType.MANAGED ? group.profile.slug : t("username_placeholder")
}/${type.slug}`}
data-testid={"event-type-slug-" + eventType.id}>
{getEventTypeSlug(eventType, t)}
</small>
) : null}
{readOnly && (
{readonly && (
<Badge variant="gray" className="ml-2">
{t("readonly")}
</Badge>
@ -160,34 +178,34 @@ const Item = ({ type, group, readOnly }: { type: EventType; group: EventTypeGrou
</div>
);
return readOnly ? (
return readonly ? (
<div className="flex-1 overflow-hidden pr-4 text-sm">
{content()}
<EventTypeDescription
// @ts-expect-error FIXME: We have a type mismatch here @hariombalhara @sean-brydon
eventType={type}
eventType={eventType}
shortenDescription
/>
</div>
) : (
<Link
href={`/event-types/${type.id}?tabName=setup`}
href={`/event-types/${eventType?.id}?tabName=setup`}
className="flex-1 overflow-hidden pr-4 text-sm"
title={type.title}>
title={eventType?.title}>
<div>
<span
className="text-default font-semibold ltr:mr-1 rtl:ml-1"
data-testid={"event-type-title-" + type.id}>
{type.title}
data-testid={"event-type-title-" + eventType?.id}>
{eventType?.title}
</span>
{group.profile.slug ? (
<small
className="text-subtle hidden font-normal leading-4 sm:inline"
data-testid={"event-type-slug-" + type.id}>
{`/${group.profile.slug}/${type.slug}`}
data-testid={"event-type-slug-" + eventType?.id}>
{getEventTypeSlug(eventType, t)}
</small>
) : null}
{readOnly && (
{readonly && (
<Badge variant="gray" className="ml-2">
{t("readonly")}
</Badge>
@ -195,7 +213,10 @@ const Item = ({ type, group, readOnly }: { type: EventType; group: EventTypeGrou
</div>
<EventTypeDescription
// @ts-expect-error FIXME: We have a type mismatch here @hariombalhara @sean-brydon
eventType={{ ...type, descriptionAsSafeHTML: type.safeDescription }}
eventType={{
...eventType,
descriptionAsSafeHTML: eventType?.description ? markdownToSafeHTML(eventType?.description) : "",
}}
shortenDescription
/>
</Link>
@ -204,7 +225,7 @@ const Item = ({ type, group, readOnly }: { type: EventType; group: EventTypeGrou
const MemoizedItem = memo(Item);
export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeListProps): JSX.Element => {
export const EventTypeList = ({ data, readonly }: EventTypeListProps): JSX.Element => {
const { t } = useLocale();
const router = useRouter();
const pathname = usePathname();
@ -235,18 +256,18 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
await utils.viewer.eventTypes.getByViewer.cancel();
const previousValue = utils.viewer.eventTypes.getByViewer.getData();
if (previousValue) {
const newList = [...types];
const newList = [...data];
const itemIndex = newList.findIndex((item) => item.id === id);
if (itemIndex !== -1 && newList[itemIndex]) {
newList[itemIndex].hidden = !newList[itemIndex].hidden;
}
utils.viewer.eventTypes.getByViewer.setData(undefined, {
...previousValue,
eventTypeGroups: [
...previousValue.eventTypeGroups.slice(0, groupIndex),
{ ...group, eventTypes: newList },
...previousValue.eventTypeGroups.slice(groupIndex + 1),
],
// eventTypeGroups: [
// ...previousValue.eventTypeGroups.slice(0, groupIndex),
// { ...group, eventTypes: newList },
// ...previousValue.eventTypeGroups.slice(groupIndex + 1),
// ],
});
}
return { previousValue };
@ -264,10 +285,10 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
});
async function moveEventType(index: number, increment: 1 | -1) {
const newList = [...types];
const newList = [...data];
const type = types[index];
const tmp = types[index + increment];
const type = data[index];
const tmp = data[index + increment];
if (tmp) {
newList[index] = tmp;
newList[index + increment] = type;
@ -279,11 +300,11 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
if (previousValue) {
utils.viewer.eventTypes.getByViewer.setData(undefined, {
...previousValue,
eventTypeGroups: [
...previousValue.eventTypeGroups.slice(0, groupIndex),
{ ...group, eventTypes: newList },
...previousValue.eventTypeGroups.slice(groupIndex + 1),
],
// eventTypeGroups: [
// ...previousValue.eventTypeGroups.slice(0, groupIndex),
// { ...group, eventTypes: newList },
// ...previousValue.eventTypeGroups.slice(groupIndex + 1),
// ],
});
}
@ -298,7 +319,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
}
// inject selection data into url for correct router history
const openDuplicateModal = (eventType: EventType, group: EventTypeGroup) => {
const openDuplicateModal = (eventType: EventType) => {
const newSearchParams = new URLSearchParams(searchParams);
function setParamsIfDefined(key: string, value: string | number | boolean | null | undefined) {
if (value) newSearchParams.set(key, value.toString());
@ -310,7 +331,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
setParamsIfDefined("slug", eventType.slug);
setParamsIfDefined("id", eventType.id);
setParamsIfDefined("length", eventType.length);
setParamsIfDefined("pageSlug", group.profile.slug);
setParamsIfDefined("pageSlug", getEventTypeSlug(eventType, t));
router.push(`${pathname}?${newSearchParams.toString()}`);
};
@ -323,15 +344,15 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
await utils.viewer.eventTypes.getByViewer.cancel();
const previousValue = utils.viewer.eventTypes.getByViewer.getData();
if (previousValue) {
const newList = types.filter((item) => item.id !== id);
// const newList = data.filter((item) => item.id !== id);
utils.viewer.eventTypes.getByViewer.setData(undefined, {
...previousValue,
eventTypeGroups: [
...previousValue.eventTypeGroups.slice(0, groupIndex),
{ ...group, eventTypes: newList },
...previousValue.eventTypeGroups.slice(groupIndex + 1),
],
// eventTypeGroups: [
// ...previousValue.eventTypeGroups.slice(0, groupIndex),
// { ...group, eventTypes: newList },
// ...previousValue.eventTypeGroups.slice(groupIndex + 1),
// ],
});
}
return { previousValue };
@ -362,88 +383,91 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
}
}, []);
if (!types.length) {
return group.teamId ? (
<EmptyEventTypeList group={group} />
) : (
<CreateFirstEventTypeView slug={group.profile.slug ?? ""} />
);
}
// @TODO: Fix later after we have the context provider
// if (!data.length) {
// return data[0].teamId ? (
// <EmptyEventTypeList teamId={data[0].teamId} teamSlugOrUsername={data[0].team.slug || ""} />
// ) : (
// <CreateFirstEventTypeView slug={data[0].team.slug || []} />
// );
// }
const firstItem = types[0];
const lastItem = types[types.length - 1];
const firstItem = data[0];
const lastItem = data[data.length - 1];
const isManagedEventPrefix = () => {
return deleteDialogTypeSchedulingType === SchedulingType.MANAGED ? "_managed" : "";
};
return (
<div className="bg-default border-subtle mb-16 flex overflow-hidden rounded-md border">
<ul ref={parent} className="divide-subtle !static w-full divide-y" data-testid="event-types">
{types.map((type, index) => {
const embedLink = `${group.profile.slug}/${type.slug}`;
{data.map((eventType, index) => {
const embedLink = `${eventType?.team?.slug || eventType.users[0].username}/${eventType.slug}`;
const calLink = `${orgBranding?.fullDomain ?? CAL_URL}/${embedLink}`;
const isManagedEventType = type.schedulingType === SchedulingType.MANAGED;
const isChildrenManagedEventType =
type.metadata?.managedEventConfig !== undefined && type.schedulingType !== SchedulingType.MANAGED;
const isManagedEventType = eventType.schedulingType === SchedulingType.MANAGED;
const isChildrenManagedEventType = !!eventType.parentId;
const isReadOnly = readonly || isChildrenManagedEventType;
const hosts = !!eventType?.hosts?.length
? eventType?.hosts.map((user) => user.user)
: eventType.users;
return (
<li key={type.id}>
<li key={eventType.id}>
<div className="hover:bg-muted flex w-full items-center justify-between">
<div className="group flex w-full max-w-full items-center justify-between overflow-hidden px-4 py-4 sm:px-6">
{!(firstItem && firstItem.id === type.id) && (
{!(firstItem && firstItem.id === eventType.id) && (
<ArrowButton onClick={() => moveEventType(index, -1)} arrowDirection="up" />
)}
{!(lastItem && lastItem.id === type.id) && (
{!(lastItem && lastItem.id === eventType.id) && (
<ArrowButton onClick={() => moveEventType(index, 1)} arrowDirection="down" />
)}
<MemoizedItem type={type} group={group} readOnly={readOnly} />
<MemoizedItem eventType={eventType} readonly={isReadOnly} />
<div className="mt-4 hidden sm:mt-0 sm:flex">
<div className="flex justify-between space-x-2 rtl:space-x-reverse">
{type.team && !isManagedEventType && (
{eventType.team && !isManagedEventType && (
<AvatarGroup
className="relative right-3 top-1"
size="sm"
truncateAfter={4}
items={
type?.users
? type.users.map(
(organizer: { name: string | null; username: string | null }) => ({
items={hosts.map((organizer: { name: string | null; username: string | null }) => ({
alt: organizer.name || "",
image: `${orgBranding?.fullDomain ?? WEBAPP_URL}/${
organizer.username
}/avatar.png`,
title: organizer.name || "",
})
)
: []
}
}))}
/>
)}
{isManagedEventType && type?.children && type.children?.length > 0 && (
{isManagedEventType && eventType.children?.length > 0 && (
<AvatarGroup
className="relative right-3 top-1"
size="sm"
truncateAfter={4}
items={type?.children
.flatMap((ch) => ch.users)
.map((user: Pick<User, "name" | "username">) => ({
alt: user.name || "",
image: `${orgBranding?.fullDomain ?? WEBAPP_URL}/${user.username}/avatar.png`,
title: user.name || "",
}))}
items={eventType.children.map(
({ users }: { users: Pick<User, "name" | "username">[] }) => ({
alt: users[0].name || "",
image: `${orgBranding?.fullDomain ?? WEBAPP_URL}/${
users[0].username
}/avatar.png`,
title: users[0].name || "",
})
)}
/>
)}
<div className="flex items-center justify-between space-x-2 rtl:space-x-reverse">
{!isManagedEventType && (
<>
{type.hidden && <Badge variant="gray">{t("hidden")}</Badge>}
{eventType.hidden && <Badge variant="gray">{t("hidden")}</Badge>}
<Tooltip
content={type.hidden ? t("show_eventtype_on_profile") : t("hide_from_profile")}>
content={
eventType.hidden ? t("show_eventtype_on_profile") : t("hide_from_profile")
}>
<div className="self-center rounded-md p-2">
<Switch
name="Hidden"
checked={!type.hidden}
checked={!eventType.hidden}
onCheckedChange={() => {
setHiddenMutation.mutate({ id: type.id, hidden: !type.hidden });
setHiddenMutation.mutate({ id: eventType.id, hidden: !eventType.hidden });
}}
/>
</div>
@ -478,8 +502,8 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
</Tooltip>
</>
)}
<Dropdown modal={false}>
<DropdownMenuTrigger asChild data-testid={"event-type-options-" + type.id}>
<Dropdown modal={isReadOnly}>
<DropdownMenuTrigger asChild data-testid={"event-type-options-" + eventType.id}>
<Button
type="button"
variant="icon"
@ -489,13 +513,13 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
/>
</DropdownMenuTrigger>
<DropdownMenuContent>
{!readOnly && (
{!isReadOnly && (
<DropdownMenuItem>
<DropdownItem
type="button"
data-testid={"event-type-edit-" + type.id}
data-testid={"event-type-edit-" + eventType.id}
StartIcon={Edit2}
onClick={() => router.push("/event-types/" + type.id)}>
onClick={() => router.push("/event-types/" + eventType.id)}>
{t("edit")}
</DropdownItem>
</DropdownMenuItem>
@ -505,9 +529,9 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
<DropdownMenuItem className="outline-none">
<DropdownItem
type="button"
data-testid={"event-type-duplicate-" + type.id}
data-testid={"event-type-duplicate-" + eventType.id}
StartIcon={Copy}
onClick={() => openDuplicateModal(type, group)}>
onClick={() => openDuplicateModal(eventType)}>
{t("duplicate")}
</DropdownItem>
</DropdownMenuItem>
@ -521,14 +545,13 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
StartIcon={Code}
className="w-full rounded-none"
embedUrl={encodeURIComponent(embedLink)}
eventId={type.id}>
eventId={eventType.id}>
{t("embed")}
</EventTypeEmbedButton>
</DropdownMenuItem>
)}
{/* readonly is only set when we are on a team - if we are on a user event type null will be the value. */}
{(group.metadata?.readOnly === false || group.metadata.readOnly === null) &&
!isChildrenManagedEventType && (
{isReadOnly && !isChildrenManagedEventType && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem>
@ -536,8 +559,8 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
color="destructive"
onClick={() => {
setDeleteDialogOpen(true);
setDeleteDialogTypeId(type.id);
setDeleteDialogSchedulingType(type.schedulingType);
setDeleteDialogTypeId(eventType.id);
setDeleteDialogSchedulingType(eventType.schedulingType);
}}
StartIcon={Trash}
className="w-full rounded-none">
@ -555,7 +578,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
</div>
<div className="min-w-9 mx-5 flex sm:hidden">
<Dropdown>
<DropdownMenuTrigger asChild data-testid={"event-type-options-" + type.id}>
<DropdownMenuTrigger asChild data-testid={"event-type-options-" + eventType.id}>
<Button type="button" variant="icon" color="secondary" StartIcon={MoreHorizontal} />
</DropdownMenuTrigger>
<DropdownMenuPortal>
@ -573,7 +596,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
</DropdownMenuItem>
<DropdownMenuItem className="outline-none">
<DropdownItem
data-testid={"event-type-duplicate-" + type.id}
data-testid={"event-type-duplicate-" + eventType.id}
onClick={() => {
navigator.clipboard.writeText(calLink);
showToast(t("link_copied"), "success");
@ -588,7 +611,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
{isNativeShare ? (
<DropdownMenuItem className="outline-none">
<DropdownItem
data-testid={"event-type-duplicate-" + type.id}
data-testid={"event-type-duplicate-" + eventType.id}
onClick={() => {
navigator
.share({
@ -605,10 +628,10 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
</DropdownItem>
</DropdownMenuItem>
) : null}
{!readOnly && (
{!isReadOnly && (
<DropdownMenuItem className="outline-none">
<DropdownItem
onClick={() => router.push("/event-types/" + type.id)}
onClick={() => router.push("/event-types/" + eventType.id)}
StartIcon={Edit}
className="w-full rounded-none">
{t("edit")}
@ -618,24 +641,23 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
{!isManagedEventType && !isChildrenManagedEventType && (
<DropdownMenuItem className="outline-none">
<DropdownItem
onClick={() => openDuplicateModal(type, group)}
onClick={() => openDuplicateModal(eventType)}
StartIcon={Copy}
data-testid={"event-type-duplicate-" + type.id}>
data-testid={"event-type-duplicate-" + eventType.id}>
{t("duplicate")}
</DropdownItem>
</DropdownMenuItem>
)}
{/* readonly is only set when we are on a team - if we are on a user event type null will be the value. */}
{(group.metadata?.readOnly === false || group.metadata.readOnly === null) &&
!isChildrenManagedEventType && (
{isReadOnly && !isChildrenManagedEventType && (
<>
<DropdownMenuItem className="outline-none">
<DropdownItem
color="destructive"
onClick={() => {
setDeleteDialogOpen(true);
setDeleteDialogTypeId(type.id);
setDeleteDialogSchedulingType(type.schedulingType);
setDeleteDialogTypeId(eventType.id);
setDeleteDialogSchedulingType(eventType.schedulingType);
}}
StartIcon={Trash}
className="w-full rounded-none">
@ -651,14 +673,14 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
as={Label}
htmlFor="hiddenSwitch"
className="mt-2 inline cursor-pointer self-center pr-2 ">
{type.hidden ? t("show_eventtype_on_profile") : t("hide_from_profile")}
{eventType.hidden ? t("show_eventtype_on_profile") : t("hide_from_profile")}
</Skeleton>
<Switch
id="hiddenSwitch"
name="Hidden"
checked={!type.hidden}
checked={!eventType.hidden}
onCheckedChange={() => {
setHiddenMutation.mutate({ id: type.id, hidden: !type.hidden });
setHiddenMutation.mutate({ id: eventType.id, hidden: !eventType.hidden });
}}
/>
</div>
@ -702,7 +724,8 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
};
const EventTypeListHeading = ({
profile,
teamSlugOrUsername,
teamNameOrUserName,
membershipCount,
teamId,
}: EventTypeListHeadingProps): JSX.Element => {
@ -711,7 +734,7 @@ const EventTypeListHeading = ({
const orgBranding = useOrgBranding();
const publishTeamMutation = trpc.viewer.teams.publish.useMutation({
onSuccess(data) {
onSuccess(data: { url: string }) {
router.push(data.url);
},
onError: (error) => {
@ -719,17 +742,13 @@ const EventTypeListHeading = ({
},
});
const bookerUrl = useBookerUrl();
const slug = teamSlugOrUsername;
return (
<div className="mb-4 flex items-center space-x-2">
<Avatar
alt={profile?.name || ""}
alt={teamNameOrUserName}
href={teamId ? `/settings/teams/${teamId}/profile` : "/settings/my-account/profile"}
imageSrc={
orgBranding?.fullDomain
? `${orgBranding.fullDomain}${teamId ? "/team" : ""}/${profile.slug}/avatar.png`
: profile.image
}
imageSrc={`${orgBranding?.fullDomain ?? WEBAPP_URL}/${slug}/avatar.png` || undefined}
size="md"
className="mt-1 inline-flex justify-center"
/>
@ -737,9 +756,9 @@ const EventTypeListHeading = ({
<Link
href={teamId ? `/settings/teams/${teamId}/profile` : "/settings/my-account/profile"}
className="text-emphasis font-bold">
{profile?.name || ""}
{teamNameOrUserName}
</Link>
{membershipCount && teamId && (
{membershipCount >= 0 && teamId && (
<span className="text-subtle relative -top-px me-2 ms-2 text-xs">
<Link href={`/settings/teams/${teamId}/members`}>
<Badge variant="gray">
@ -749,15 +768,15 @@ const EventTypeListHeading = ({
</Link>
</span>
)}
{profile?.slug && (
{slug && (
<Link
href={`${orgBranding ? orgBranding.fullDomain : CAL_URL}/${profile.slug}`}
href={`${orgBranding ? orgBranding.fullDomain : CAL_URL}/${slug}`}
className="text-subtle block text-xs">
{`${bookerUrl.replace("https://", "").replace("http://", "")}/${profile.slug}`}
{`${bookerUrl.replace("https://", "").replace("http://", "")}/${slug}`}
</Link>
)}
</div>
{!profile?.slug && !!teamId && (
{!slug && !!teamId && (
<button onClick={() => publishTeamMutation.mutate({ teamId })}>
<Badge variant="gray" className="-ml-2 mb-1">
{t("upgrade")}
@ -786,30 +805,18 @@ const CreateFirstEventTypeView = ({ slug }: { slug: string }) => {
);
};
const CTA = ({ data }: { data: GetByViewerResponse }) => {
const CTA = () => {
const { t } = useLocale();
if (!data) return null;
const profileOptions = data.profiles
.filter((profile) => !profile.readOnly)
.map((profile) => {
return {
teamId: profile.teamId,
label: profile.name || profile.slug,
image: profile.image,
membershipRole: profile.membershipRole,
slug: profile.slug,
};
});
return (
<CreateButton
<>
<CreateButtonWithTeamsList
data-testid="new-event-type"
subtitle={t("create_event_on").toUpperCase()}
options={profileOptions}
createDialog={() => <CreateEventTypeDialog profileOptions={profileOptions} />}
createDialog={() => <CreateEventTypeDialog profileOptions={[]} />}
/>
<CreateEventTypeDialog profileOptions={[]} />
</>
);
};
@ -849,15 +856,24 @@ const SetupProfileBanner = ({ closeAction }: { closeAction: () => void }) => {
);
};
const EmptyEventTypeList = ({ group }: { group: EventTypeGroup }) => {
const EmptyEventTypeList = ({
teamSlugOrUsername,
teamId,
}: {
teamSlugOrUsername: string | null;
teamId?: number | null;
}) => {
const { t } = useLocale();
if (!teamSlugOrUsername && !teamId) {
return null;
}
return (
<>
<EmptyScreen
headline={t("team_no_event_types")}
buttonRaw={
<Button
href={`?dialog=new&eventPage=${group.profile.slug}&teamId=${group.teamId}`}
href={`?dialog=new&eventPage=${teamSlugOrUsername}${teamId ? `&teamId=${teamId}` : ""}`}
variant="button"
className="mt-5">
{t("create")}
@ -868,78 +884,163 @@ const EmptyEventTypeList = ({ group }: { group: EventTypeGroup }) => {
);
};
const Main = ({
status,
errorMessage,
data,
filters,
}: {
status: string;
data: GetByViewerResponse;
errorMessage?: string;
filters: ReturnType<typeof getTeamsFiltersFromQuery>;
}) => {
const Main = ({ filters }: { filters: ReturnType<typeof getTeamsFiltersFromQuery> }) => {
const isMobile = useMediaQuery("(max-width: 768px)");
const searchParams = useSearchParams();
const orgBranding = useOrgBranding();
if (!data || status === "loading") {
// const isFilteredByOnlyOneItem =
// (filters?.teamIds?.length === 1 || filters?.userIds?.length === 1) && data?.eventTypeGroups.length === 1;
const isFilteredByOnlyOneItem = false;
// We first load user event types and then team event types
const { data, status, error } = trpc.viewer.eventTypes.paginate.useQuery(
{
page: 1,
pageSize: 10,
},
{
trpc: {
context: { skipBatch: true },
},
}
);
// Then we load teams
const { data: teams } = trpc.viewer.teams.list.useQuery(undefined, {
// Teams don't change that frequently
refetchOnWindowFocus: false,
trpc: {
context: { skipBatch: true },
},
});
// After teams are fetched we then load event types for each team
const eventTypePaginate = trpc.useQueries(
(t) =>
teams?.map((team) => t.viewer.eventTypes.paginate({ page: 1, pageSize: 10, teamIds: [team.id] }), {
enabled: teams.length > 0,
}) || []
);
if (status === "error") {
return <Alert severity="error" title="Something went wrong" message={error.message} />;
}
if (status === "loading") {
return <SkeletonLoader />;
}
if (status === "error") {
return <Alert severity="error" title="Something went wrong" message={errorMessage} />;
}
if ((!!data && data.length > 1) || isFilteredByOnlyOneItem) {
const [firstElementPersonalEventTypes] = data;
const [mainUser] = firstElementPersonalEventTypes?.users || [];
const isFilteredByOnlyOneItem =
(filters?.teamIds?.length === 1 || filters?.userIds?.length === 1) && data.eventTypeGroups.length === 1;
const teamEventTypesForTabs = eventTypePaginate
.map((trpcFetch) => {
const { data } = trpcFetch;
return data || [];
})
.filter((item) => item && item.length > 0);
return (
<>
{data.eventTypeGroups.length > 1 || isFilteredByOnlyOneItem ? (
<>
{isMobile ? (
<MobileTeamsTab eventTypeGroups={data.eventTypeGroups} />
<MobileTeamsTab teamEventTypes={[data, ...teamEventTypesForTabs]} />
) : (
data.eventTypeGroups.map((group: EventTypeGroup, index: number) => (
<div className="mt-4 flex flex-col" key={group.profile.slug}>
<div className="mt-4 flex flex-col">
<EventTypeListHeading
profile={group.profile}
membershipCount={group.metadata.membershipCount}
teamId={group.teamId}
teamSlugOrUsername={mainUser.username || ""}
teamNameOrUserName={mainUser.name || ""}
// Single event types have no teamMembershipCount
membershipCount={0}
teamId={firstElementPersonalEventTypes.teamId}
orgSlug={orgBranding?.slug}
/>
{group.eventTypes.length ? (
<EventTypeList
types={group.eventTypes}
group={group}
groupIndex={index}
readOnly={group.metadata.readOnly}
/>
) : group.teamId ? (
<EmptyEventTypeList group={group} />
{data.length > 0 ? (
<EventTypeList data={data} />
) : (
<CreateFirstEventTypeView slug={data.profiles[0].slug ?? ""} />
<EmptyEventTypeList teamSlugOrUsername={mainUser.username ?? ""} />
)}
</div>
))
{/* Then we list team event types */}
{eventTypePaginate.map((trpcFetch, index) => {
const { data: teamEventTypes } = trpcFetch;
const [firstElementTeamEventTypes] = teamEventTypes || [];
if (!teamEventTypes || teamEventTypes.length === 0 || !firstElementTeamEventTypes.team) {
return null;
}
const teamDataMatchWithEventType = teams && teams.length > 0 ? teams[index] : null;
const membershipCount = teamDataMatchWithEventType
? teamDataMatchWithEventType?.membershipCount
: 0;
return (
<>
<EventTypeListHeading
key={index}
teamSlugOrUsername={firstElementTeamEventTypes.team.slug || ""}
teamNameOrUserName={firstElementTeamEventTypes.team.name || ""}
membershipCount={membershipCount}
teamId={firstElementTeamEventTypes.teamId}
orgSlug={orgBranding?.slug}
/>
{teamEventTypes.length > 0 ? (
<EventTypeList
data={teamEventTypes}
key={index}
readonly={
teamDataMatchWithEventType ? "MEMBER" === teamDataMatchWithEventType.role : false
}
/>
) : (
<EmptyEventTypeList
teamId={firstElementTeamEventTypes.teamId}
teamSlugOrUsername={firstElementTeamEventTypes.team.slug || ""}
key={index}
/>
)}
</>
) : (
data.eventTypeGroups.length === 1 && (
<EventTypeList
types={data.eventTypeGroups[0].eventTypes}
group={data.eventTypeGroups[0]}
groupIndex={0}
readOnly={data.eventTypeGroups[0].metadata.readOnly}
/>
)
);
})}
</div>
)}
{data.eventTypeGroups.length === 0 && <CreateFirstEventTypeView slug={data.profiles[0].slug ?? ""} />}
<EventTypeEmbedDialog />
{searchParams?.get("dialog") === "duplicate" && <DuplicateDialog />}
</>
);
} else if (!!data && data.length === 1) {
return <EventTypeList data={data} />;
} else if (!!data && data.length === 0) {
// @TODO:
return <CreateFirstEventTypeView slug="fixlater" />;
} else {
return <></>;
}
// ) : group.teamId ? (
// <EmptyEventTypeList group={group} />
// ) : (
// <CreateFirstEventTypeView slug={data.profiles[0].slug ?? ""} />
// )}
// </div>
// ))
// )}
// </>
// ) : (
// data.eventTypeGroups.length === 1 && (
// <EventTypeList
// types={data.eventTypeGroups[0].eventTypes}
// group={data.eventTypeGroups[0]}
// groupIndex={0}
// readOnly={data.eventTypeGroups[0].metadata.readOnly}
// />
// )
// )}
// {data.eventTypeGroups.length === 0 && <CreateFirstEventTypeView slug={data.profiles[0].slug ?? ""} />}
// <EventTypeEmbedDialog />
// {searchParams?.get("dialog") === "duplicate" && <DuplicateDialog />}
// </>
// );
};
const EventTypesPage = () => {
@ -952,13 +1053,6 @@ const EventTypesPage = () => {
const routerQuery = useRouterQuery();
const filters = getTeamsFiltersFromQuery(routerQuery);
// TODO: Maybe useSuspenseQuery to focus on success case only? Remember that it would crash the page when there is an error in query. Also, it won't support skeleton
const { data, status, error } = trpc.viewer.eventTypes.getByViewer.useQuery(filters && { filters }, {
refetchOnWindowFocus: false,
cacheTime: 1 * 60 * 60 * 1000,
staleTime: 1 * 60 * 60 * 1000,
});
function closeBanner() {
setShowProfileBanner(false);
document.cookie = `calcom-profile-banner=1;max-age=${60 * 60 * 24 * 90}`; // 3 months
@ -986,12 +1080,12 @@ const EventTypesPage = () => {
subtitle={t("event_types_page_subtitle")}
afterHeading={showProfileBanner && <SetupProfileBanner closeAction={closeBanner} />}
beforeCTAactions={<Actions />}
CTA={<CTA data={data} />}>
CTA={<CTA />}>
<HeadSeo
title="Event Types"
description="Create events to share for people to book on your calendar."
/>
<Main data={data} status={status} errorMessage={error?.message} filters={filters} />
<Main filters={filters} />
</ShellMain>
);
};

View File

@ -0,0 +1,12 @@
-- View: public.TeamMemberCount
-- DROP VIEW public."TeamMemberCount";
CREATE OR REPLACE VIEW public."TeamMemberCount"
AS
SELECT t.id,
count(*) AS count
FROM "Membership" m
LEFT JOIN "Team" t ON t.id = m."teamId"
WHERE m.accepted = true
GROUP BY t.id;

View File

@ -938,6 +938,19 @@ enum AccessScope {
READ_PROFILE
}
model CalendarCache {
// The key would be the unique URL that is requested by the user
key String
value Json
expiresAt DateTime
credentialId Int
credential Credential? @relation(fields: [credentialId], references: [id], onDelete: Cascade)
@@id([credentialId, key])
@@unique([credentialId, key])
}
// Let's keep views at the bottom of the schema
view BookingTimeStatus {
id Int @unique
uid String?
@ -958,14 +971,7 @@ view BookingTimeStatus {
eventParentId Int?
}
model CalendarCache {
// The key would be the unique URL that is requested by the user
key String
value Json
expiresAt DateTime
credentialId Int
credential Credential? @relation(fields: [credentialId], references: [id], onDelete: Cascade)
@@id([credentialId, key])
@@unique([credentialId, key])
view TeamMemberCount {
id Int @unique
count Int
}

View File

@ -0,0 +1,221 @@
/**
* This script can be used to seed the database with a lot of data for performance testing.
* TODO: Make it more structured and configurable from CLI
* Run it as `npx ts-node --transpile-only ./seed-event-types-and-teams.ts`
*/
import type { Prisma } from "@prisma/client";
import prisma from ".";
async function createEventTypesForUserAndForTeams() {
const selectedUser = await prisma.user.findFirst({
where: {
username: "teampro",
},
});
if (!selectedUser) {
throw new Error("No user found");
}
// Create randomized event types for the user
await createManyEventTypes({ userId: selectedUser.id });
// Create randomized event types for the teams
await createManyTeams(selectedUser.id);
}
async function createManyEventTypes({ userId, teamId }: { userId?: number; teamId?: number }) {
// If teamId is provided then we fetch membership users from that team
const membershipUsers: number[] = [];
if (teamId) {
const memberships = await prisma.membership.findMany({ where: { teamId } });
for (const membership of memberships) {
if (!membership.userId) {
// Do nothing
} else {
membershipUsers.push(membership.userId);
}
}
}
const eventTypesPromises: Promise<
Prisma.EventTypeGetPayload<{
select: {
id: true;
schedulingType: true;
teamId: true;
};
}>
>[] = [];
for (let i = 0; i < 20; i++) {
const schedulingType = randomSchedulingType();
eventTypesPromises.push(
prisma.eventType.create({
data: {
title: `${schedulingType.toLowerCase() ?? ""} Event Type ${i + 1} - ${
teamId ? `Team ${teamId}` : ""
}`,
slug: `event-type-${teamId ? `team-${teamId}` : ""}${i + 1}`,
userId: teamId ? null : userId,
teamId: teamId ? teamId : null,
length: 30,
description: "This is a description",
...(teamId ? { schedulingType: schedulingType } : {}),
},
})
);
}
const resultEventTypes = await Promise.all(eventTypesPromises);
const updateRelationships: Promise<any>[] = [];
for (const eventType of resultEventTypes) {
const schedulingType = eventType.schedulingType;
updateRelationships.push(
prisma.eventType.update({
where: {
id: eventType.id,
},
data: {
users: {
connect: {
id: userId,
},
},
...(schedulingType === "COLLECTIVE" || schedulingType === "ROUND_ROBIN"
? {
hosts: {
create: membershipUsers.map((userId) => ({
userId,
isFixed: schedulingType === "COLLECTIVE" ? true : false,
})),
},
}
: {}),
},
})
);
}
await Promise.all(updateRelationships);
}
async function createManyTeams(userId: number) {
const teamsPromises: Promise<
Prisma.TeamGetPayload<{
select: {
id: true;
};
}>
>[] = [];
for (let i = 0; i < 20; i++) {
teamsPromises.push(
prisma.team.create({
data: {
name: `Team ${i + 1}`,
slug: `team-${i + 1}`,
},
})
);
}
const result = await Promise.all(teamsPromises);
if (result.length > 0) {
console.log(`👥 Created ${result.length} teams`);
}
const teamIds = result.map((team) => team.id);
const usersForTeam = await createManyUsers();
if (usersForTeam.length > 0) {
console.log(`👥 Created ${usersForTeam.length} users for teams`);
}
// Create memberships for the user
const memberships: Prisma.MembershipUncheckedCreateInput[] = [];
for (const teamId of teamIds) {
for (const userId of usersForTeam) {
memberships.push({
userId: userId,
teamId: teamId,
role: randomTeamRole(),
accepted: true,
});
}
// Add main user to all teams as Owner
memberships.push({
userId: userId,
teamId: teamId,
role: "OWNER",
accepted: true,
});
}
await prisma.membership.createMany({
data: memberships,
});
// Create event Types for those teams
// Load memberships
const membershipsFound = await prisma.membership.findMany({
where: {
userId,
},
select: {
teamId: true,
},
});
if (!membershipsFound.length) {
throw new Error("No memberships found");
}
for (const membership of membershipsFound) {
await createManyEventTypes({ userId, teamId: membership.teamId });
}
}
async function createManyUsers() {
const userPromises: Promise<
Prisma.UserGetPayload<{
select: { id: true };
}>
>[] = [];
for (let i = 0; i < 20; i++) {
userPromises.push(
prisma.user.create({
data: {
username: `user-${i + 1}`,
email: `user-${i + 1}`,
password: "password",
name: `User ${i + 1}`,
role: randomUserRole(),
completedOnboarding: true,
},
})
);
}
const result = await Promise.all(userPromises);
return result.map((user) => user.id);
}
createEventTypesForUserAndForTeams();
function randomUserRole(): any {
const roles = ["USER", "ADMIN"];
return roles[Math.floor(Math.random() * roles.length)];
}
function randomTeamRole(): any {
const roles = ["ADMIN", "OWNER", "MEMBER"];
return roles[Math.floor(Math.random() * roles.length)];
}
function randomSchedulingType(): any {
const roles = ["MANAGED", "COLLECTIVE", "ROUND_ROBIN"];
return roles[Math.floor(Math.random() * roles.length)];
}

View File

@ -9,6 +9,7 @@ import { ZDeleteInputSchema } from "./delete.schema";
import { ZDuplicateInputSchema } from "./duplicate.schema";
import { ZGetInputSchema } from "./get.schema";
import { ZEventTypeInputSchema } from "./getByViewer.schema";
import { ZInfiniteEventsTypeInputSchema } from "./infiniteEventTypes.schema";
import { ZUpdateInputSchema } from "./update.schema";
import { eventOwnerProcedure } from "./util";
@ -23,6 +24,7 @@ type BookingsRouterHandlerCache = {
duplicate?: typeof import("./duplicate.handler").duplicateHandler;
bulkEventFetch?: typeof import("./bulkEventFetch.handler").bulkEventFetchHandler;
bulkUpdateToDefaultLocation?: typeof import("./bulkUpdateToDefaultLocation.handler").bulkUpdateToDefaultLocationHandler;
paginate?: typeof import("./infiniteEventTypes.handler").paginateHandler;
};
const UNSTABLE_HANDLER_CACHE: BookingsRouterHandlerCache = {};
@ -207,4 +209,22 @@ export const eventTypesRouter = router({
input,
});
}),
paginate: authedProcedure.input(ZInfiniteEventsTypeInputSchema).query(async ({ ctx, input }) => {
if (!UNSTABLE_HANDLER_CACHE.paginate) {
UNSTABLE_HANDLER_CACHE.paginate = await import("./infiniteEventTypes.handler").then(
(mod) => mod.paginateHandler
);
}
// Unreachable code but required for type safety
if (!UNSTABLE_HANDLER_CACHE.paginate) {
throw new Error("Failed to load handler");
}
return UNSTABLE_HANDLER_CACHE.paginate({
ctx,
input,
});
}),
});

View File

@ -0,0 +1,103 @@
import { checkRateLimitAndThrowError } from "@calcom/lib/checkRateLimitAndThrowError";
import { prisma } from "@calcom/prisma";
import type { TrpcSessionUser } from "../../../trpc";
import type { TInfiniteEventTypesInputSchema } from "./infiniteEventTypes.schema";
import type { Prisma } from ".prisma/client";
interface EventTypesPaginateProps {
ctx: {
user: NonNullable<TrpcSessionUser>;
};
input: TInfiniteEventTypesInputSchema;
}
export const paginateHandler = async ({ ctx, input }: EventTypesPaginateProps) => {
await checkRateLimitAndThrowError({
identifier: `eventTypes:paginate:${ctx.user.id}`,
rateLimitingType: "common",
});
const userId = ctx.user.id;
const { teamIds, pageSize = 20, page: pageFromInput = 1 } = input;
let page = 1;
if (pageFromInput < 1) {
page = 1;
}
let teamConditional: Prisma.EventTypeWhereInput = {};
const whereConditional: Prisma.EventTypeWhereInput = {
userId,
};
if (teamIds && teamIds.length === 1) {
whereConditional.userId = null;
teamConditional = { teamId: teamIds[0] };
} else if (teamIds && teamIds.length > 1) {
teamConditional = { teamId: { in: teamIds } };
}
// const skip = (page - 1) * pageSize;
const result = await prisma.eventType.findMany({
where: {
...whereConditional,
...teamConditional,
},
select: {
id: true,
title: true,
description: true,
length: true,
schedulingType: true,
slug: true,
hidden: true,
metadata: true,
teamId: true,
parentId: true,
users: {
select: {
id: true,
username: true,
name: true,
},
},
hosts: {
select: {
user: {
select: {
id: true,
username: true,
name: true,
},
},
},
},
children: {
select: {
id: true,
users: {
select: {
id: true,
username: true,
name: true,
},
},
},
},
team: {
select: {
id: true,
slug: true,
name: true,
},
},
},
// Temporarily disabled until we can figure out how to do this properly
// skip,
// take: pageSize,
});
return result;
};

View File

@ -0,0 +1,9 @@
import { z } from "zod";
export const ZInfiniteEventsTypeInputSchema = z.object({
teamIds: z.number().array().optional(),
page: z.number().optional(),
pageSize: z.number().optional(),
});
export type TInfiniteEventTypesInputSchema = z.infer<typeof ZInfiniteEventsTypeInputSchema>;

View File

@ -35,6 +35,15 @@ export const listHandler = async ({ ctx }: ListOptions) => {
const isOrgAdmin = !!(await isOrganisationAdmin(ctx.user.id, ctx.user.organization.id)); // Org id exists here as we're inside a conditional TS complaining for some reason
// This can be optimized by using a custom view between membership and team and teamMemberCount
const membershipCount = await prisma.teamMemberCount.findMany({
where: {
id: {
in: membershipsWithoutParent.map((m) => m.teamId),
},
},
});
return membershipsWithoutParent.map(({ team: { inviteTokens, ..._team }, ...membership }) => ({
role: membership.role,
accepted: membership.accepted,
@ -42,6 +51,7 @@ export const listHandler = async ({ ctx }: ListOptions) => {
..._team,
/** To prevent breaking we only return non-email attached token here, if we have one */
inviteToken: inviteTokens.find((token) => token.identifier === "invite-link-for-teamId-" + _team.id),
membershipCount: membershipCount.find((m) => m.id === _team.id)?.count || 0,
}));
}
@ -59,6 +69,15 @@ export const listHandler = async ({ ctx }: ListOptions) => {
orderBy: { role: "desc" },
});
// This can be optimized by using a custom view between membership and team and teamMemberCount
const membershipCount = await prisma.teamMemberCount.findMany({
where: {
id: {
in: memberships.map((m) => m.teamId),
},
},
});
return memberships
.filter((mmship) => {
const metadata = teamMetadataSchema.parse(mmship.team.metadata);
@ -70,5 +89,6 @@ export const listHandler = async ({ ctx }: ListOptions) => {
..._team,
/** To prevent breaking we only return non-email attached token here, if we have one */
inviteToken: inviteTokens.find((token) => token.identifier === "invite-link-for-teamId-" + _team.id),
membershipCount: membershipCount.find((m) => m.id === _team.id)?.count || 0,
}));
};

589
yarn.lock
View File

@ -80,14 +80,10 @@ __metadata:
languageName: node
linkType: hard
"@algora/sdk@npm:^0.1.2":
version: 0.1.2
resolution: "@algora/sdk@npm:0.1.2"
dependencies:
"@trpc/client": ^10.0.0
"@trpc/server": ^10.0.0
superjson: ^1.9.1
checksum: 0ef5c0ac7fa09c57f685a61859e263f4b5c2d69e6ee72b395f88490106fa6213a0308ca11e5673368e5b33dc4dff38411c0d226f9c65856ef91aa58bc2e0e61c
"@alloc/quick-lru@npm:^5.2.0":
version: 5.2.0
resolution: "@alloc/quick-lru@npm:5.2.0"
checksum: bdc35758b552bcf045733ac047fb7f9a07c4678b944c641adfbd41f798b4b91fffd0fdc0df2578d9b0afc7b4d636aa6e110ead5d6281a2adc1ab90efd7f057f8
languageName: node
linkType: hard
@ -3535,15 +3531,13 @@ __metadata:
"@calcom/ui": "*"
"@types/node": 16.9.1
"@types/react": 18.0.26
"@types/react-dom": ^18.0.9
"@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
next: ^13.2.1
next-auth: ^4.20.1
react: ^18.2.0
react-dom: ^18.2.0
tailwindcss: ^3.3.1
typescript: ^4.9.4
languageName: unknown
linkType: soft
@ -3645,9 +3639,9 @@ __metadata:
chart.js: ^3.7.1
client-only: ^0.0.1
eslint: ^8.34.0
next: ^13.4.6
next-auth: ^4.22.1
next-i18next: ^13.2.2
next: ^13.2.1
next-auth: ^4.20.1
next-i18next: ^11.3.0
postcss: ^8.4.18
prisma: ^5.0.0
prisma-field-encryption: ^1.4.0
@ -3655,9 +3649,9 @@ __metadata:
react-chartjs-2: ^4.0.1
react-dom: ^18.2.0
react-hook-form: ^7.43.3
react-live-chat-loader: ^2.8.1
react-live-chat-loader: ^2.7.3
swr: ^1.2.2
tailwindcss: ^3.3.1
tailwindcss: ^3.2.1
typescript: ^4.9.4
zod: ^3.22.2
languageName: unknown
@ -4621,7 +4615,6 @@ __metadata:
version: 0.0.0-use.local
resolution: "@calcom/website@workspace:apps/website"
dependencies:
"@algora/sdk": ^0.1.2
"@calcom/app-store": "*"
"@calcom/config": "*"
"@calcom/dayjs": "*"
@ -4660,13 +4653,11 @@ __metadata:
"@types/node": 16.9.1
"@types/react": 18.0.26
"@types/react-gtm-module": ^2.0.1
"@types/xml2js": ^0.4.11
"@vercel/analytics": ^0.1.6
"@vercel/edge-functions-ui": ^0.2.1
"@vercel/og": ^0.5.0
autoprefixer: ^10.4.12
bcryptjs: ^2.4.3
clsx: ^1.2.1
cobe: ^0.4.1
concurrently: ^7.6.0
cross-env: ^7.0.3
@ -4683,10 +4674,9 @@ __metadata:
graphql-request: ^6.1.0
gray-matter: ^4.0.3
gsap: ^3.11.0
i18n-unused: ^0.13.0
iframe-resizer-react: ^1.1.0
keen-slider: ^6.8.0
lucide-react: ^0.171.0
lucide-react: ^0.125.0
micro: ^10.0.1
next: ^13.4.6
next-auth: ^4.22.1
@ -4709,7 +4699,6 @@ __metadata:
react-merge-refs: 1.1.0
react-twemoji: ^0.3.0
react-use-measure: ^2.1.1
react-wrap-balancer: ^1.0.0
remark: ^14.0.2
remark-html: ^14.0.1
remeda: ^1.24.1
@ -7761,6 +7750,20 @@ __metadata:
languageName: node
linkType: hard
"@next/env@npm:13.2.4":
version: 13.2.4
resolution: "@next/env@npm:13.2.4"
checksum: 4123e08a79e66d6144006972027a9ceb8f3fdd782c4a869df1eb3b91b59ad9f4a44082d3f8e421f4df5214c6bc7190b52b94881369452d65eb4580485f33b9e6
languageName: node
linkType: hard
"@next/env@npm:13.4.16":
version: 13.4.16
resolution: "@next/env@npm:13.4.16"
checksum: 2fc506a0c2e4b0596297fc488b9f589cd0d7757b87a1876e77dd83f52b1240aa8932cbd81fb88e1dc9eacec7489d9d2d7dcb063e3b2bcd8099136d6700bcc2f8
languageName: node
linkType: hard
"@next/env@npm:13.4.6":
version: 13.4.6
resolution: "@next/env@npm:13.4.6"
@ -7777,6 +7780,34 @@ __metadata:
languageName: node
linkType: hard
"@next/swc-android-arm-eabi@npm:13.2.4":
version: 13.2.4
resolution: "@next/swc-android-arm-eabi@npm:13.2.4"
conditions: os=android & cpu=arm
languageName: node
linkType: hard
"@next/swc-android-arm64@npm:13.2.4":
version: 13.2.4
resolution: "@next/swc-android-arm64@npm:13.2.4"
conditions: os=android & cpu=arm64
languageName: node
linkType: hard
"@next/swc-darwin-arm64@npm:13.2.4":
version: 13.2.4
resolution: "@next/swc-darwin-arm64@npm:13.2.4"
conditions: os=darwin & cpu=arm64
languageName: node
linkType: hard
"@next/swc-darwin-arm64@npm:13.4.16":
version: 13.4.16
resolution: "@next/swc-darwin-arm64@npm:13.4.16"
conditions: os=darwin & cpu=arm64
languageName: node
linkType: hard
"@next/swc-darwin-arm64@npm:13.4.6":
version: 13.4.6
resolution: "@next/swc-darwin-arm64@npm:13.4.6"
@ -7784,6 +7815,20 @@ __metadata:
languageName: node
linkType: hard
"@next/swc-darwin-x64@npm:13.2.4":
version: 13.2.4
resolution: "@next/swc-darwin-x64@npm:13.2.4"
conditions: os=darwin & cpu=x64
languageName: node
linkType: hard
"@next/swc-darwin-x64@npm:13.4.16":
version: 13.4.16
resolution: "@next/swc-darwin-x64@npm:13.4.16"
conditions: os=darwin & cpu=x64
languageName: node
linkType: hard
"@next/swc-darwin-x64@npm:13.4.6":
version: 13.4.6
resolution: "@next/swc-darwin-x64@npm:13.4.6"
@ -7791,6 +7836,34 @@ __metadata:
languageName: node
linkType: hard
"@next/swc-freebsd-x64@npm:13.2.4":
version: 13.2.4
resolution: "@next/swc-freebsd-x64@npm:13.2.4"
conditions: os=freebsd & cpu=x64
languageName: node
linkType: hard
"@next/swc-linux-arm-gnueabihf@npm:13.2.4":
version: 13.2.4
resolution: "@next/swc-linux-arm-gnueabihf@npm:13.2.4"
conditions: os=linux & cpu=arm
languageName: node
linkType: hard
"@next/swc-linux-arm64-gnu@npm:13.2.4":
version: 13.2.4
resolution: "@next/swc-linux-arm64-gnu@npm:13.2.4"
conditions: os=linux & cpu=arm64 & libc=glibc
languageName: node
linkType: hard
"@next/swc-linux-arm64-gnu@npm:13.4.16":
version: 13.4.16
resolution: "@next/swc-linux-arm64-gnu@npm:13.4.16"
conditions: os=linux & cpu=arm64 & libc=glibc
languageName: node
linkType: hard
"@next/swc-linux-arm64-gnu@npm:13.4.6":
version: 13.4.6
resolution: "@next/swc-linux-arm64-gnu@npm:13.4.6"
@ -7798,6 +7871,20 @@ __metadata:
languageName: node
linkType: hard
"@next/swc-linux-arm64-musl@npm:13.2.4":
version: 13.2.4
resolution: "@next/swc-linux-arm64-musl@npm:13.2.4"
conditions: os=linux & cpu=arm64 & libc=musl
languageName: node
linkType: hard
"@next/swc-linux-arm64-musl@npm:13.4.16":
version: 13.4.16
resolution: "@next/swc-linux-arm64-musl@npm:13.4.16"
conditions: os=linux & cpu=arm64 & libc=musl
languageName: node
linkType: hard
"@next/swc-linux-arm64-musl@npm:13.4.6":
version: 13.4.6
resolution: "@next/swc-linux-arm64-musl@npm:13.4.6"
@ -7805,6 +7892,20 @@ __metadata:
languageName: node
linkType: hard
"@next/swc-linux-x64-gnu@npm:13.2.4":
version: 13.2.4
resolution: "@next/swc-linux-x64-gnu@npm:13.2.4"
conditions: os=linux & cpu=x64 & libc=glibc
languageName: node
linkType: hard
"@next/swc-linux-x64-gnu@npm:13.4.16":
version: 13.4.16
resolution: "@next/swc-linux-x64-gnu@npm:13.4.16"
conditions: os=linux & cpu=x64 & libc=glibc
languageName: node
linkType: hard
"@next/swc-linux-x64-gnu@npm:13.4.6":
version: 13.4.6
resolution: "@next/swc-linux-x64-gnu@npm:13.4.6"
@ -7812,6 +7913,20 @@ __metadata:
languageName: node
linkType: hard
"@next/swc-linux-x64-musl@npm:13.2.4":
version: 13.2.4
resolution: "@next/swc-linux-x64-musl@npm:13.2.4"
conditions: os=linux & cpu=x64 & libc=musl
languageName: node
linkType: hard
"@next/swc-linux-x64-musl@npm:13.4.16":
version: 13.4.16
resolution: "@next/swc-linux-x64-musl@npm:13.4.16"
conditions: os=linux & cpu=x64 & libc=musl
languageName: node
linkType: hard
"@next/swc-linux-x64-musl@npm:13.4.6":
version: 13.4.6
resolution: "@next/swc-linux-x64-musl@npm:13.4.6"
@ -7819,6 +7934,20 @@ __metadata:
languageName: node
linkType: hard
"@next/swc-win32-arm64-msvc@npm:13.2.4":
version: 13.2.4
resolution: "@next/swc-win32-arm64-msvc@npm:13.2.4"
conditions: os=win32 & cpu=arm64
languageName: node
linkType: hard
"@next/swc-win32-arm64-msvc@npm:13.4.16":
version: 13.4.16
resolution: "@next/swc-win32-arm64-msvc@npm:13.4.16"
conditions: os=win32 & cpu=arm64
languageName: node
linkType: hard
"@next/swc-win32-arm64-msvc@npm:13.4.6":
version: 13.4.6
resolution: "@next/swc-win32-arm64-msvc@npm:13.4.6"
@ -7826,6 +7955,20 @@ __metadata:
languageName: node
linkType: hard
"@next/swc-win32-ia32-msvc@npm:13.2.4":
version: 13.2.4
resolution: "@next/swc-win32-ia32-msvc@npm:13.2.4"
conditions: os=win32 & cpu=ia32
languageName: node
linkType: hard
"@next/swc-win32-ia32-msvc@npm:13.4.16":
version: 13.4.16
resolution: "@next/swc-win32-ia32-msvc@npm:13.4.16"
conditions: os=win32 & cpu=ia32
languageName: node
linkType: hard
"@next/swc-win32-ia32-msvc@npm:13.4.6":
version: 13.4.6
resolution: "@next/swc-win32-ia32-msvc@npm:13.4.6"
@ -7833,6 +7976,20 @@ __metadata:
languageName: node
linkType: hard
"@next/swc-win32-x64-msvc@npm:13.2.4":
version: 13.2.4
resolution: "@next/swc-win32-x64-msvc@npm:13.2.4"
conditions: os=win32 & cpu=x64
languageName: node
linkType: hard
"@next/swc-win32-x64-msvc@npm:13.4.16":
version: 13.4.16
resolution: "@next/swc-win32-x64-msvc@npm:13.4.16"
conditions: os=win32 & cpu=x64
languageName: node
linkType: hard
"@next/swc-win32-x64-msvc@npm:13.4.6":
version: 13.4.6
resolution: "@next/swc-win32-x64-msvc@npm:13.4.6"
@ -12040,6 +12197,15 @@ __metadata:
languageName: node
linkType: hard
"@swc/helpers@npm:0.4.14":
version: 0.4.14
resolution: "@swc/helpers@npm:0.4.14"
dependencies:
tslib: ^2.4.0
checksum: 273fd3f3fc461a92f3790cc551ea054745c6d6959afbe1232e6d7aa1c722bbc114d308aab96bef5c78fc0303c85c7b472ef00e2253251cc89737f3b1af56e5a5
languageName: node
linkType: hard
"@swc/helpers@npm:0.5.1":
version: 0.5.1
resolution: "@swc/helpers@npm:0.5.1"
@ -17449,7 +17615,7 @@ __metadata:
languageName: node
linkType: hard
"clsx@npm:^1.0.4, clsx@npm:^1.2.1":
"clsx@npm:^1.0.4":
version: 1.2.1
resolution: "clsx@npm:1.2.1"
checksum: 30befca8019b2eb7dbad38cff6266cf543091dae2825c856a62a8ccf2c3ab9c2907c4d12b288b73101196767f66812365400a227581484a05f968b0307cfaf12
@ -23885,6 +24051,13 @@ __metadata:
languageName: node
linkType: hard
"i18next-fs-backend@npm:^1.1.4":
version: 1.2.0
resolution: "i18next-fs-backend@npm:1.2.0"
checksum: da74d20f2b007f8e34eaf442fa91ad12aaff3b9891e066c6addd6d111b37e370c62370dfbc656730ab2f8afd988f2e7ea1c48301ebb19ccb716fb5965600eddf
languageName: node
linkType: hard
"i18next-fs-backend@npm:^2.1.1":
version: 2.1.3
resolution: "i18next-fs-backend@npm:2.1.3"
@ -23892,6 +24065,15 @@ __metadata:
languageName: node
linkType: hard
"i18next@npm:^21.8.13":
version: 21.10.0
resolution: "i18next@npm:21.10.0"
dependencies:
"@babel/runtime": ^7.17.2
checksum: f997985e2d4d15a62a0936a82ff6420b97f3f971e776fe685bdd50b4de0cb4dc2198bc75efe6b152844794ebd5040d8060d6d152506a687affad534834836d81
languageName: node
linkType: hard
"i18next@npm:^23.2.3":
version: 23.2.3
resolution: "i18next@npm:23.2.3"
@ -24564,7 +24746,7 @@ __metadata:
languageName: node
linkType: hard
"is-core-module@npm:^2.11.0":
"is-core-module@npm:^2.13.0":
version: 2.13.0
resolution: "is-core-module@npm:2.13.0"
dependencies:
@ -25535,6 +25717,15 @@ __metadata:
languageName: node
linkType: hard
"jiti@npm:^1.18.2":
version: 1.19.1
resolution: "jiti@npm:1.19.1"
bin:
jiti: bin/jiti.js
checksum: fdf55e315f9e81c04ae902416642062851d92c6cdcc17a59d5d1d35e1a0842e4e79be38da86613c5776fa18c579954542a441b93d1c347a50137dee2e558cbd0
languageName: node
linkType: hard
"joi@npm:^17.7.0":
version: 17.10.2
resolution: "joi@npm:17.10.2"
@ -26699,6 +26890,13 @@ __metadata:
languageName: node
linkType: hard
"lilconfig@npm:^2.1.0":
version: 2.1.0
resolution: "lilconfig@npm:2.1.0"
checksum: 8549bb352b8192375fed4a74694cd61ad293904eee33f9d4866c2192865c44c4eb35d10782966242634e0cbc1e91fe62b1247f148dc5514918e3a966da7ea117
languageName: node
linkType: hard
"limiter@npm:^1.1.5":
version: 1.1.5
resolution: "limiter@npm:1.1.5"
@ -27372,6 +27570,15 @@ __metadata:
languageName: node
linkType: hard
"lucide-react@npm:^0.125.0":
version: 0.125.0
resolution: "lucide-react@npm:0.125.0"
peerDependencies:
react: ^16.5.1 || ^17.0.0 || ^18.0.0
checksum: 6d86330fd4316a42624d537cc07adc3c51c18a699a02ef3abbd221c3140ae5ac5685396e1f5cb20f5f5ba80fa100523a5a6811c22a51bd13bbfdf65546cfffdf
languageName: node
linkType: hard
"lucide-react@npm:^0.171.0":
version: 0.171.0
resolution: "lucide-react@npm:0.171.0"
@ -29120,6 +29327,31 @@ __metadata:
languageName: node
linkType: hard
"next-auth@npm:^4.20.1":
version: 4.23.1
resolution: "next-auth@npm:4.23.1"
dependencies:
"@babel/runtime": ^7.20.13
"@panva/hkdf": ^1.0.2
cookie: ^0.5.0
jose: ^4.11.4
oauth: ^0.9.15
openid-client: ^5.4.0
preact: ^10.6.3
preact-render-to-string: ^5.1.19
uuid: ^8.3.2
peerDependencies:
next: ^12.2.5 || ^13
nodemailer: ^6.6.5
react: ^17.0.2 || ^18
react-dom: ^17.0.2 || ^18
peerDependenciesMeta:
nodemailer:
optional: true
checksum: 995114797c257ccf71a82d19fb6316fb7709b552aaaf66444591c505a4b8e00b0cae3f4db4316b63a8cc439076044cc391ab171c4f6ee2e086709c5b3bbfed24
languageName: node
linkType: hard
"next-auth@npm:^4.22.1":
version: 4.22.1
resolution: "next-auth@npm:4.22.1"
@ -29169,6 +29401,24 @@ __metadata:
languageName: node
linkType: hard
"next-i18next@npm:^11.3.0":
version: 11.3.0
resolution: "next-i18next@npm:11.3.0"
dependencies:
"@babel/runtime": ^7.18.6
"@types/hoist-non-react-statics": ^3.3.1
core-js: ^3
hoist-non-react-statics: ^3.3.2
i18next: ^21.8.13
i18next-fs-backend: ^1.1.4
react-i18next: ^11.18.0
peerDependencies:
next: ">= 10.0.0"
react: ">= 16.8.0"
checksum: fbce97a4fbf9ad846c08652471a833c7f173c3e7ddc7cafa1423625b4a684715bb85f76ae06fe9cbed3e70f12b8e78e2459e5bc1a3c3f5c517743f17648f8939
languageName: node
linkType: hard
"next-i18next@npm:^13.2.2":
version: 13.3.0
resolution: "next-i18next@npm:13.3.0"
@ -29250,6 +29500,62 @@ __metadata:
languageName: node
linkType: hard
"next@npm:^13.2.1":
version: 13.4.16
resolution: "next@npm:13.4.16"
dependencies:
"@next/env": 13.4.16
"@next/swc-darwin-arm64": 13.4.16
"@next/swc-darwin-x64": 13.4.16
"@next/swc-linux-arm64-gnu": 13.4.16
"@next/swc-linux-arm64-musl": 13.4.16
"@next/swc-linux-x64-gnu": 13.4.16
"@next/swc-linux-x64-musl": 13.4.16
"@next/swc-win32-arm64-msvc": 13.4.16
"@next/swc-win32-ia32-msvc": 13.4.16
"@next/swc-win32-x64-msvc": 13.4.16
"@swc/helpers": 0.5.1
busboy: 1.6.0
caniuse-lite: ^1.0.30001406
postcss: 8.4.14
styled-jsx: 5.1.1
watchpack: 2.4.0
zod: 3.21.4
peerDependencies:
"@opentelemetry/api": ^1.1.0
react: ^18.2.0
react-dom: ^18.2.0
sass: ^1.3.0
dependenciesMeta:
"@next/swc-darwin-arm64":
optional: true
"@next/swc-darwin-x64":
optional: true
"@next/swc-linux-arm64-gnu":
optional: true
"@next/swc-linux-arm64-musl":
optional: true
"@next/swc-linux-x64-gnu":
optional: true
"@next/swc-linux-x64-musl":
optional: true
"@next/swc-win32-arm64-msvc":
optional: true
"@next/swc-win32-ia32-msvc":
optional: true
"@next/swc-win32-x64-msvc":
optional: true
peerDependenciesMeta:
"@opentelemetry/api":
optional: true
sass:
optional: true
bin:
next: dist/bin/next
checksum: b9e346061969531401a1b3a81819adad0018fd33e52954032cc96a3d703821ee1c39a5807dcd73fab8df197424b409d51f837910df225341c1f83735614a811f
languageName: node
linkType: hard
"next@npm:^13.4.6":
version: 13.4.6
resolution: "next@npm:13.4.6"
@ -29309,6 +29615,77 @@ __metadata:
languageName: node
linkType: hard
"next@npm:~13.2.1":
version: 13.2.4
resolution: "next@npm:13.2.4"
dependencies:
"@next/env": 13.2.4
"@next/swc-android-arm-eabi": 13.2.4
"@next/swc-android-arm64": 13.2.4
"@next/swc-darwin-arm64": 13.2.4
"@next/swc-darwin-x64": 13.2.4
"@next/swc-freebsd-x64": 13.2.4
"@next/swc-linux-arm-gnueabihf": 13.2.4
"@next/swc-linux-arm64-gnu": 13.2.4
"@next/swc-linux-arm64-musl": 13.2.4
"@next/swc-linux-x64-gnu": 13.2.4
"@next/swc-linux-x64-musl": 13.2.4
"@next/swc-win32-arm64-msvc": 13.2.4
"@next/swc-win32-ia32-msvc": 13.2.4
"@next/swc-win32-x64-msvc": 13.2.4
"@swc/helpers": 0.4.14
caniuse-lite: ^1.0.30001406
postcss: 8.4.14
styled-jsx: 5.1.1
peerDependencies:
"@opentelemetry/api": ^1.4.0
fibers: ">= 3.1.0"
node-sass: ^6.0.0 || ^7.0.0
react: ^18.2.0
react-dom: ^18.2.0
sass: ^1.3.0
dependenciesMeta:
"@next/swc-android-arm-eabi":
optional: true
"@next/swc-android-arm64":
optional: true
"@next/swc-darwin-arm64":
optional: true
"@next/swc-darwin-x64":
optional: true
"@next/swc-freebsd-x64":
optional: true
"@next/swc-linux-arm-gnueabihf":
optional: true
"@next/swc-linux-arm64-gnu":
optional: true
"@next/swc-linux-arm64-musl":
optional: true
"@next/swc-linux-x64-gnu":
optional: true
"@next/swc-linux-x64-musl":
optional: true
"@next/swc-win32-arm64-msvc":
optional: true
"@next/swc-win32-ia32-msvc":
optional: true
"@next/swc-win32-x64-msvc":
optional: true
peerDependenciesMeta:
"@opentelemetry/api":
optional: true
fibers:
optional: true
node-sass:
optional: true
sass:
optional: true
bin:
next: dist/bin/next
checksum: 8531dee41b60181b582f5ee80858907b102f083ef8808ff9352d589dd39e6b3a96f7a11b3776a03eef3a28430cff768336fa2e3ff2c6f8fcd699fbc891749051
languageName: node
linkType: hard
"nice-try@npm:^1.0.4":
version: 1.0.5
resolution: "nice-try@npm:1.0.5"
@ -31271,6 +31648,19 @@ __metadata:
languageName: node
linkType: hard
"postcss-import@npm:^15.1.0":
version: 15.1.0
resolution: "postcss-import@npm:15.1.0"
dependencies:
postcss-value-parser: ^4.0.0
read-cache: ^1.0.0
resolve: ^1.1.7
peerDependencies:
postcss: ^8.0.0
checksum: 7bd04bd8f0235429009d0022cbf00faebc885de1d017f6d12ccb1b021265882efc9302006ba700af6cab24c46bfa2f3bc590be3f9aee89d064944f171b04e2a3
languageName: node
linkType: hard
"postcss-js@npm:^4.0.0":
version: 4.0.0
resolution: "postcss-js@npm:4.0.0"
@ -31282,6 +31672,17 @@ __metadata:
languageName: node
linkType: hard
"postcss-js@npm:^4.0.1":
version: 4.0.1
resolution: "postcss-js@npm:4.0.1"
dependencies:
camelcase-css: ^2.0.1
peerDependencies:
postcss: ^8.4.21
checksum: 5c1e83efeabeb5a42676193f4357aa9c88f4dc1b3c4a0332c132fe88932b33ea58848186db117cf473049fc233a980356f67db490bd0a7832ccba9d0b3fd3491
languageName: node
linkType: hard
"postcss-load-config@npm:^3.1.4":
version: 3.1.4
resolution: "postcss-load-config@npm:3.1.4"
@ -31300,6 +31701,24 @@ __metadata:
languageName: node
linkType: hard
"postcss-load-config@npm:^4.0.1":
version: 4.0.1
resolution: "postcss-load-config@npm:4.0.1"
dependencies:
lilconfig: ^2.0.5
yaml: ^2.1.1
peerDependencies:
postcss: ">=8.0.9"
ts-node: ">=9.0.0"
peerDependenciesMeta:
postcss:
optional: true
ts-node:
optional: true
checksum: b61f890499ed7dcda1e36c20a9582b17d745bad5e2b2c7bc96942465e406bc43ae03f270c08e60d1e29dab1ee50cb26970b5eb20c9aae30e066e20bd607ae4e4
languageName: node
linkType: hard
"postcss-loader@npm:^4.2.0":
version: 4.3.0
resolution: "postcss-loader@npm:4.3.0"
@ -31426,6 +31845,17 @@ __metadata:
languageName: node
linkType: hard
"postcss-nested@npm:^6.0.1":
version: 6.0.1
resolution: "postcss-nested@npm:6.0.1"
dependencies:
postcss-selector-parser: ^6.0.11
peerDependencies:
postcss: ^8.2.14
checksum: 7ddb0364cd797de01e38f644879189e0caeb7ea3f78628c933d91cc24f327c56d31269384454fc02ecaf503b44bfa8e08870a7c4cc56b23bc15640e1894523fa
languageName: node
linkType: hard
"postcss-pseudo-companion-classes@npm:^0.1.1":
version: 0.1.1
resolution: "postcss-pseudo-companion-classes@npm:0.1.1"
@ -32717,6 +33147,24 @@ __metadata:
languageName: node
linkType: hard
"react-i18next@npm:^11.18.0":
version: 11.18.6
resolution: "react-i18next@npm:11.18.6"
dependencies:
"@babel/runtime": ^7.14.5
html-parse-stringify: ^3.0.1
peerDependencies:
i18next: ">= 19.0.0"
react: ">= 16.8.0"
peerDependenciesMeta:
react-dom:
optional: true
react-native:
optional: true
checksum: 624c0a0313fac4e0d18560b83c99a8bd0a83abc02e5db8d01984e0643ac409d178668aa3a4720d01f7a0d9520d38598dcbff801d6f69a970bae67461de6cd852
languageName: node
linkType: hard
"react-i18next@npm:^12.2.0":
version: 12.3.1
resolution: "react-i18next@npm:12.3.1"
@ -32840,7 +33288,7 @@ __metadata:
languageName: node
linkType: hard
"react-live-chat-loader@npm:^2.8.1":
"react-live-chat-loader@npm:^2.7.3, react-live-chat-loader@npm:^2.8.1":
version: 2.8.1
resolution: "react-live-chat-loader@npm:2.8.1"
peerDependencies:
@ -34092,6 +34540,19 @@ __metadata:
languageName: node
linkType: hard
"resolve@npm:^1.22.2":
version: 1.22.4
resolution: "resolve@npm:1.22.4"
dependencies:
is-core-module: ^2.13.0
path-parse: ^1.0.7
supports-preserve-symlinks-flag: ^1.0.0
bin:
resolve: bin/resolve
checksum: 23f25174c2736ce24c6d918910e0d1f89b6b38fefa07a995dff864acd7863d59a7f049e691f93b4b2ee29696303390d921552b6d1b841ed4a8101f517e1d0124
languageName: node
linkType: hard
"resolve@npm:^2.0.0-next.3":
version: 2.0.0-next.3
resolution: "resolve@npm:2.0.0-next.3"
@ -34141,6 +34602,19 @@ __metadata:
languageName: node
linkType: hard
"resolve@patch:resolve@^1.22.2#~builtin<compat/resolve>":
version: 1.22.4
resolution: "resolve@patch:resolve@npm%3A1.22.4#~builtin<compat/resolve>::version=1.22.4&hash=c3c19d"
dependencies:
is-core-module: ^2.13.0
path-parse: ^1.0.7
supports-preserve-symlinks-flag: ^1.0.0
bin:
resolve: bin/resolve
checksum: c45f2545fdc4d21883861b032789e20aa67a2f2692f68da320cc84d5724cd02f2923766c5354b3210897e88f1a7b3d6d2c7c22faeead8eed7078e4c783a444bc
languageName: node
linkType: hard
"resolve@patch:resolve@^2.0.0-next.3#~builtin<compat/resolve>":
version: 2.0.0-next.3
resolution: "resolve@patch:resolve@npm%3A2.0.0-next.3#~builtin<compat/resolve>::version=2.0.0-next.3&hash=c3c19d"
@ -36259,6 +36733,24 @@ __metadata:
languageName: node
linkType: hard
"sucrase@npm:^3.32.0":
version: 3.34.0
resolution: "sucrase@npm:3.34.0"
dependencies:
"@jridgewell/gen-mapping": ^0.3.2
commander: ^4.0.0
glob: 7.1.6
lines-and-columns: ^1.1.6
mz: ^2.7.0
pirates: ^4.0.1
ts-interface-checker: ^0.1.9
bin:
sucrase: bin/sucrase
sucrase-node: bin/sucrase-node
checksum: 61860063bdf6103413698e13247a3074d25843e91170825a9752e4af7668ffadd331b6e99e92fc32ee5b3c484ee134936f926fa9039d5711fafff29d017a2110
languageName: node
linkType: hard
"superagent@npm:^5.1.1":
version: 5.3.1
resolution: "superagent@npm:5.3.1"
@ -36620,6 +37112,39 @@ __metadata:
languageName: node
linkType: hard
"tailwindcss@npm:^3.2.1":
version: 3.3.3
resolution: "tailwindcss@npm:3.3.3"
dependencies:
"@alloc/quick-lru": ^5.2.0
arg: ^5.0.2
chokidar: ^3.5.3
didyoumean: ^1.2.2
dlv: ^1.1.3
fast-glob: ^3.2.12
glob-parent: ^6.0.2
is-glob: ^4.0.3
jiti: ^1.18.2
lilconfig: ^2.1.0
micromatch: ^4.0.5
normalize-path: ^3.0.0
object-hash: ^3.0.0
picocolors: ^1.0.0
postcss: ^8.4.23
postcss-import: ^15.1.0
postcss-js: ^4.0.1
postcss-load-config: ^4.0.1
postcss-nested: ^6.0.1
postcss-selector-parser: ^6.0.11
resolve: ^1.22.2
sucrase: ^3.32.0
bin:
tailwind: lib/cli.js
tailwindcss: lib/cli.js
checksum: 0195c7a3ebb0de5e391d2a883d777c78a4749f0c532d204ee8aea9129f2ed8e701d8c0c276aa5f7338d07176a3c2a7682c1d0ab9c8a6c2abe6d9325c2954eb50
languageName: node
linkType: hard
"tailwindcss@npm:^3.3.1":
version: 3.3.1
resolution: "tailwindcss@npm:3.3.1"
@ -40360,16 +40885,6 @@ __metadata:
languageName: node
linkType: hard
"xml2js@npm:^0.6.0":
version: 0.6.2
resolution: "xml2js@npm:0.6.2"
dependencies:
sax: ">=0.6.0"
xmlbuilder: ~11.0.0
checksum: 458a83806193008edff44562c0bdb982801d61ee7867ae58fd35fab781e69e17f40dfeb8fc05391a4648c9c54012066d3955fe5d993ffbe4dc63399023f32ac2
languageName: node
linkType: hard
"xml@npm:=1.0.1":
version: 1.0.1
resolution: "xml@npm:1.0.1"