refactor: enable infinite scrolling for all (#16463)
* refactor: enable infinite scrolling for all * chore: remove unused code * fix: tests * chore: type err * chore: update email embed test * chore: update tests * chore: update tests * chore: fix horizontal tabitem * chore * fix: conflicts * chore: conflict * fix: E2E tests * chore: remove idle * fix test --------- Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com>
This commit is contained in:
co-authored by
Syed Ali Shahbaz
parent
c834b927b6
commit
ac300ff670
@@ -21,8 +21,5 @@ export const withAppDirSsr =
|
||||
...("trpcState" in props && {
|
||||
dehydratedState: props.trpcState,
|
||||
}),
|
||||
...("isInfiniteScrollEnabled" in props && {
|
||||
isInfiniteScrollEnabled: props.isInfiniteScrollEnabled,
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { ENABLE_INFINITE_EVENT_TYPES_FOR_ORG } from "@calcom/lib/constants";
|
||||
|
||||
import { ssrInit } from "@server/lib/ssr";
|
||||
|
||||
@@ -18,9 +17,5 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
};
|
||||
}
|
||||
|
||||
const isInfiniteScrollEnabled = session.user?.org?.slug
|
||||
? ENABLE_INFINITE_EVENT_TYPES_FOR_ORG.includes(session.user.org.slug)
|
||||
: false;
|
||||
|
||||
return { props: { trpcState: ssr.dehydrate(), isInfiniteScrollEnabled } };
|
||||
return { props: { trpcState: ssr.dehydrate() } };
|
||||
};
|
||||
|
||||
@@ -14,10 +14,7 @@ import { EventTypeEmbedButton, EventTypeEmbedDialog } from "@calcom/features/emb
|
||||
import { EventTypeDescription } from "@calcom/features/eventtypes/components";
|
||||
import CreateEventTypeDialog from "@calcom/features/eventtypes/components/CreateEventTypeDialog";
|
||||
import { DuplicateDialog } from "@calcom/features/eventtypes/components/DuplicateDialog";
|
||||
import SkeletonLoader, {
|
||||
InfiniteSkeletonLoader,
|
||||
} from "@calcom/features/eventtypes/components/SkeletonLoader";
|
||||
import { TeamsFilter } from "@calcom/features/filters/components/TeamsFilter";
|
||||
import { InfiniteSkeletonLoader } from "@calcom/features/eventtypes/components/SkeletonLoader";
|
||||
import { getTeamsFiltersFromQuery } from "@calcom/features/filters/lib/getTeamsFiltersFromQuery";
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
import { parseEventTypeColor } from "@calcom/lib";
|
||||
@@ -25,7 +22,6 @@ import { APP_NAME } from "@calcom/lib/constants";
|
||||
import { WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import useMediaQuery from "@calcom/lib/hooks/useMediaQuery";
|
||||
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
|
||||
import { useGetTheme } from "@calcom/lib/hooks/useTheme";
|
||||
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
|
||||
@@ -38,7 +34,6 @@ import { trpc, TRPCClientError } from "@calcom/trpc/react";
|
||||
import type { UserProfile } from "@calcom/types/UserProfile";
|
||||
import {
|
||||
Alert,
|
||||
Avatar,
|
||||
Badge,
|
||||
Button,
|
||||
ButtonGroup,
|
||||
@@ -62,7 +57,6 @@ import {
|
||||
Tooltip,
|
||||
ArrowButton,
|
||||
UserAvatarGroup,
|
||||
Icon,
|
||||
} from "@calcom/ui";
|
||||
|
||||
import type { AppProps } from "@lib/app-providers";
|
||||
@@ -76,8 +70,6 @@ type InfiniteEventTypeGroup = GetUserEventGroupsResponse["eventTypeGroups"][numb
|
||||
type InfiniteEventType = GetEventTypesFromGroupsResponse["eventTypes"][number];
|
||||
|
||||
type EventTypeGroups = RouterOutputs["viewer"]["eventTypes"]["getByViewer"]["eventTypeGroups"];
|
||||
type EventTypeGroupProfile = EventTypeGroups[number]["profile"];
|
||||
type GetByViewerResponse = RouterOutputs["viewer"]["eventTypes"]["getByViewer"] | undefined;
|
||||
|
||||
type EventTypeGroup = EventTypeGroups[number];
|
||||
type EventType = EventTypeGroup["eventTypes"][number];
|
||||
@@ -89,19 +81,8 @@ type DeNormalizedEventType = Omit<EventType, "userIds"> & {
|
||||
})[];
|
||||
};
|
||||
|
||||
type DeNormalizedEventTypeGroup = Omit<EventTypeGroup, "eventTypes"> & {
|
||||
eventTypes: DeNormalizedEventType[];
|
||||
};
|
||||
|
||||
const LIMIT = 10;
|
||||
|
||||
interface EventTypeListHeadingProps {
|
||||
profile: EventTypeGroupProfile;
|
||||
membershipCount: number;
|
||||
teamId?: number | null;
|
||||
bookerUrl: string;
|
||||
}
|
||||
|
||||
interface InfiniteEventTypeListProps {
|
||||
group: InfiniteEventTypeGroup;
|
||||
readOnly: boolean;
|
||||
@@ -111,56 +92,15 @@ interface InfiniteEventTypeListProps {
|
||||
isPending?: boolean;
|
||||
}
|
||||
|
||||
interface EventTypeListProps {
|
||||
group: DeNormalizedEventTypeGroup;
|
||||
groupIndex: number;
|
||||
readOnly: boolean;
|
||||
bookerUrl: string | null;
|
||||
types: DeNormalizedEventType[];
|
||||
lockedByOrg?: boolean;
|
||||
}
|
||||
|
||||
interface InfiniteMobileTeamsTabProps {
|
||||
interface InfiniteTeamsTabProps {
|
||||
activeEventTypeGroup: InfiniteEventTypeGroup;
|
||||
}
|
||||
|
||||
interface MobileTeamsTabProps {
|
||||
eventTypeGroups: DeNormalizedEventTypeGroup[];
|
||||
}
|
||||
|
||||
const querySchema = z.object({
|
||||
teamId: z.nullable(z.coerce.number()).optional().default(null),
|
||||
});
|
||||
|
||||
const MobileTeamsTab: FC<MobileTeamsTabProps> = (props) => {
|
||||
const { eventTypeGroups } = props;
|
||||
const tabs = eventTypeGroups.map((item) => ({
|
||||
name: item.profile.name ?? "",
|
||||
href: item.teamId ? `/event-types?teamId=${item.teamId}` : "/event-types?noTeam",
|
||||
avatar: item.profile.image,
|
||||
}));
|
||||
const { data } = useTypedQuery(querySchema);
|
||||
const events = eventTypeGroups.filter((item) => item.teamId === data.teamId);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<HorizontalTabs tabs={tabs} />
|
||||
{events.length > 0 ? (
|
||||
<EventTypeList
|
||||
types={events[0].eventTypes}
|
||||
group={events[0]}
|
||||
groupIndex={0}
|
||||
bookerUrl={events[0].bookerUrl}
|
||||
readOnly={events[0].metadata.readOnly}
|
||||
/>
|
||||
) : (
|
||||
<CreateFirstEventTypeView slug={eventTypeGroups[0].profile.slug ?? ""} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const InfiniteMobileTeamsTab: FC<InfiniteMobileTeamsTabProps> = (props) => {
|
||||
const InfiniteTeamsTab: FC<InfiniteTeamsTabProps> = (props) => {
|
||||
const { activeEventTypeGroup } = props;
|
||||
const { t } = useLocale();
|
||||
|
||||
@@ -292,510 +232,6 @@ const Item = ({
|
||||
|
||||
const MemoizedItem = memo(Item);
|
||||
|
||||
export const EventTypeList = ({
|
||||
group,
|
||||
groupIndex,
|
||||
readOnly,
|
||||
types,
|
||||
bookerUrl,
|
||||
lockedByOrg,
|
||||
}: EventTypeListProps): JSX.Element => {
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const searchParams = useCompatSearchParams();
|
||||
const [parent] = useAutoAnimate<HTMLUListElement>();
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
const [deleteDialogTypeId, setDeleteDialogTypeId] = useState(0);
|
||||
const [deleteDialogTypeSchedulingType, setDeleteDialogSchedulingType] = useState<SchedulingType | null>(
|
||||
null
|
||||
);
|
||||
const utils = trpc.useUtils();
|
||||
const mutation = trpc.viewer.eventTypeOrder.useMutation({
|
||||
onError: async (err) => {
|
||||
console.error(err.message);
|
||||
await utils.viewer.eventTypes.getByViewer.cancel();
|
||||
await utils.viewer.eventTypes.invalidate();
|
||||
},
|
||||
onSettled: () => {
|
||||
utils.viewer.eventTypes.invalidate();
|
||||
},
|
||||
});
|
||||
|
||||
const setHiddenMutation = trpc.viewer.eventTypes.update.useMutation({
|
||||
onMutate: async ({ id }) => {
|
||||
await utils.viewer.eventTypes.getByViewer.cancel();
|
||||
const previousValue = utils.viewer.eventTypes.getByViewer.getData();
|
||||
if (previousValue) {
|
||||
const newList = [...types.map(normalizeEventType)];
|
||||
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),
|
||||
],
|
||||
});
|
||||
}
|
||||
return { previousValue };
|
||||
},
|
||||
onError: async (err, _, context) => {
|
||||
if (context?.previousValue) {
|
||||
utils.viewer.eventTypes.getByViewer.setData(undefined, context.previousValue);
|
||||
}
|
||||
console.error(err.message);
|
||||
},
|
||||
onSettled: () => {
|
||||
// REVIEW: Should we invalidate the entire router or just the `getByViewer` query?
|
||||
utils.viewer.eventTypes.invalidate();
|
||||
},
|
||||
});
|
||||
|
||||
async function moveEventType(index: number, increment: 1 | -1) {
|
||||
const newList = [...types.map(normalizeEventType)];
|
||||
|
||||
const type = types[index];
|
||||
const tmp = types[index + increment];
|
||||
if (tmp) {
|
||||
newList[index] = normalizeEventType(tmp);
|
||||
newList[index + increment] = normalizeEventType(type);
|
||||
}
|
||||
|
||||
await utils.viewer.eventTypes.getByViewer.cancel();
|
||||
|
||||
const previousValue = utils.viewer.eventTypes.getByViewer.getData();
|
||||
if (previousValue) {
|
||||
utils.viewer.eventTypes.getByViewer.setData(undefined, {
|
||||
...previousValue,
|
||||
eventTypeGroups: [
|
||||
...previousValue.eventTypeGroups.slice(0, groupIndex),
|
||||
{ ...group, eventTypes: newList },
|
||||
...previousValue.eventTypeGroups.slice(groupIndex + 1),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
mutation.mutate({
|
||||
ids: newList.map((type) => type.id),
|
||||
});
|
||||
}
|
||||
|
||||
async function deleteEventTypeHandler(id: number) {
|
||||
const payload = { id };
|
||||
deleteMutation.mutate(payload);
|
||||
}
|
||||
|
||||
// inject selection data into url for correct router history
|
||||
const openDuplicateModal = (eventType: DeNormalizedEventType, group: DeNormalizedEventTypeGroup) => {
|
||||
const newSearchParams = new URLSearchParams(searchParams ?? undefined);
|
||||
function setParamsIfDefined(key: string, value: string | number | boolean | null | undefined) {
|
||||
if (value) newSearchParams.set(key, value.toString());
|
||||
if (value === null) newSearchParams.delete(key);
|
||||
}
|
||||
setParamsIfDefined("dialog", "duplicate");
|
||||
setParamsIfDefined("title", eventType.title);
|
||||
setParamsIfDefined("description", eventType.description);
|
||||
setParamsIfDefined("slug", eventType.slug);
|
||||
setParamsIfDefined("id", eventType.id);
|
||||
setParamsIfDefined("length", eventType.length);
|
||||
setParamsIfDefined("pageSlug", group.profile.slug);
|
||||
setParamsIfDefined("teamId", eventType.teamId);
|
||||
setParamsIfDefined("parentId", eventType.parentId);
|
||||
router.push(`${pathname}?${newSearchParams.toString()}`);
|
||||
};
|
||||
|
||||
const deleteMutation = trpc.viewer.eventTypes.delete.useMutation({
|
||||
onSuccess: () => {
|
||||
showToast(t("event_type_deleted_successfully"), "success");
|
||||
setDeleteDialogOpen(false);
|
||||
},
|
||||
onMutate: async ({ id }) => {
|
||||
await utils.viewer.eventTypes.getByViewer.cancel();
|
||||
const previousValue = utils.viewer.eventTypes.getByViewer.getData();
|
||||
if (previousValue) {
|
||||
const newList = types.filter((item) => item.id !== id).map(normalizeEventType);
|
||||
|
||||
utils.viewer.eventTypes.getByViewer.setData(undefined, {
|
||||
...previousValue,
|
||||
eventTypeGroups: [
|
||||
...previousValue.eventTypeGroups.slice(0, groupIndex),
|
||||
{ ...group, eventTypes: newList },
|
||||
...previousValue.eventTypeGroups.slice(groupIndex + 1),
|
||||
],
|
||||
});
|
||||
}
|
||||
return { previousValue };
|
||||
},
|
||||
onError: (err, _, context) => {
|
||||
if (context?.previousValue) {
|
||||
utils.viewer.eventTypes.getByViewer.setData(undefined, context.previousValue);
|
||||
}
|
||||
if (err instanceof HttpError) {
|
||||
const message = `${err.statusCode}: ${err.message}`;
|
||||
showToast(message, "error");
|
||||
setDeleteDialogOpen(false);
|
||||
} else if (err instanceof TRPCClientError) {
|
||||
showToast(err.message, "error");
|
||||
}
|
||||
},
|
||||
onSettled: () => {
|
||||
// REVIEW: Should we invalidate the entire router or just the `getByViewer` query?
|
||||
utils.viewer.eventTypes.invalidate();
|
||||
},
|
||||
});
|
||||
|
||||
const [isNativeShare, setNativeShare] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (!navigator.share) {
|
||||
setNativeShare(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
if (!types.length) {
|
||||
return group.teamId ? (
|
||||
<EmptyEventTypeList group={group} />
|
||||
) : !group.profile.eventTypesLockedByOrg ? (
|
||||
<CreateFirstEventTypeView slug={group.profile.slug ?? ""} />
|
||||
) : (
|
||||
<></>
|
||||
);
|
||||
}
|
||||
|
||||
const firstItem = types[0];
|
||||
const lastItem = types[types.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}`;
|
||||
const calLink = `${bookerUrl}/${embedLink}`;
|
||||
const isPrivateURLEnabled = type.hashedLink?.link;
|
||||
const placeholderHashedLink = `${WEBSITE_URL}/d/${type.hashedLink?.link}/${type.slug}`;
|
||||
const isManagedEventType = type.schedulingType === SchedulingType.MANAGED;
|
||||
const isChildrenManagedEventType =
|
||||
type.metadata?.managedEventConfig !== undefined && type.schedulingType !== SchedulingType.MANAGED;
|
||||
return (
|
||||
<li key={type.id}>
|
||||
<div className="hover:bg-muted flex w-full items-center justify-between transition">
|
||||
<div className="group flex w-full max-w-full items-center justify-between overflow-hidden py-4 pl-2 pr-4 sm:pl-3 sm:pr-6">
|
||||
{!(firstItem && firstItem.id === type.id) && (
|
||||
<ArrowButton onClick={() => moveEventType(index, -1)} arrowDirection="up" />
|
||||
)}
|
||||
|
||||
{!(lastItem && lastItem.id === type.id) && (
|
||||
<ArrowButton onClick={() => moveEventType(index, 1)} arrowDirection="down" />
|
||||
)}
|
||||
<MemoizedItem type={type} group={group} readOnly={readOnly} />
|
||||
<div className="mt-4 hidden sm:mt-0 sm:flex">
|
||||
<div className="flex justify-between space-x-2 rtl:space-x-reverse">
|
||||
{!!type.teamId && !isManagedEventType && (
|
||||
<UserAvatarGroup
|
||||
className="relative right-3"
|
||||
size="sm"
|
||||
truncateAfter={4}
|
||||
users={type?.users ?? []}
|
||||
/>
|
||||
)}
|
||||
{isManagedEventType && type?.children && type.children?.length > 0 && (
|
||||
<UserAvatarGroup
|
||||
className="relative right-3"
|
||||
size="sm"
|
||||
truncateAfter={4}
|
||||
users={type?.children.flatMap((ch) => ch.users) ?? []}
|
||||
/>
|
||||
)}
|
||||
<div className="flex items-center justify-between space-x-2 rtl:space-x-reverse">
|
||||
{!isManagedEventType && (
|
||||
<>
|
||||
{type.hidden && <Badge variant="gray">{t("hidden")}</Badge>}
|
||||
<Tooltip
|
||||
content={type.hidden ? t("show_eventtype_on_profile") : t("hide_from_profile")}>
|
||||
<div className="self-center rounded-md p-2">
|
||||
<Switch
|
||||
name="Hidden"
|
||||
disabled={lockedByOrg}
|
||||
checked={!type.hidden}
|
||||
onCheckedChange={() => {
|
||||
setHiddenMutation.mutate({ id: type.id, hidden: !type.hidden });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
|
||||
<ButtonGroup combined>
|
||||
{!isManagedEventType && (
|
||||
<>
|
||||
<Tooltip content={t("preview")}>
|
||||
<Button
|
||||
data-testid="preview-link-button"
|
||||
color="secondary"
|
||||
target="_blank"
|
||||
variant="icon"
|
||||
href={calLink}
|
||||
StartIcon="external-link"
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content={t("copy_link")}>
|
||||
<Button
|
||||
color="secondary"
|
||||
variant="icon"
|
||||
StartIcon="link"
|
||||
onClick={() => {
|
||||
showToast(t("link_copied"), "success");
|
||||
navigator.clipboard.writeText(calLink);
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
{isPrivateURLEnabled && (
|
||||
<Tooltip content={t("copy_private_link_to_event")}>
|
||||
<Button
|
||||
color="secondary"
|
||||
variant="icon"
|
||||
StartIcon="venetian-mask"
|
||||
onClick={() => {
|
||||
showToast(t("private_link_copied"), "success");
|
||||
navigator.clipboard.writeText(placeholderHashedLink);
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<Dropdown modal={false}>
|
||||
<DropdownMenuTrigger asChild data-testid={`event-type-options-${type.id}`}>
|
||||
<Button
|
||||
type="button"
|
||||
variant="icon"
|
||||
color="secondary"
|
||||
StartIcon="ellipsis"
|
||||
className="ltr:radix-state-open:rounded-r-md rtl:radix-state-open:rounded-l-md"
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
{!readOnly && (
|
||||
<DropdownMenuItem>
|
||||
<DropdownItem
|
||||
type="button"
|
||||
data-testid={`event-type-edit-${type.id}`}
|
||||
StartIcon="pencil"
|
||||
onClick={() => router.push(`/event-types/${type.id}`)}>
|
||||
{t("edit")}
|
||||
</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. */}
|
||||
{!readOnly && !isManagedEventType && !isChildrenManagedEventType && (
|
||||
<>
|
||||
<DropdownMenuItem className="outline-none">
|
||||
<DropdownItem
|
||||
type="button"
|
||||
data-testid={`event-type-duplicate-${type.id}`}
|
||||
StartIcon="copy"
|
||||
onClick={() => openDuplicateModal(type, group)}>
|
||||
{t("duplicate")}
|
||||
</DropdownItem>
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
)}
|
||||
{!isManagedEventType && (
|
||||
<DropdownMenuItem className="outline-none">
|
||||
<EventTypeEmbedButton
|
||||
namespace={type.slug}
|
||||
as={DropdownItem}
|
||||
type="button"
|
||||
StartIcon="code"
|
||||
className="w-full rounded-none"
|
||||
embedUrl={encodeURIComponent(embedLink)}
|
||||
eventId={type.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. */}
|
||||
{!readOnly && !isChildrenManagedEventType && (
|
||||
<>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>
|
||||
<DropdownItem
|
||||
color="destructive"
|
||||
onClick={() => {
|
||||
setDeleteDialogOpen(true);
|
||||
setDeleteDialogTypeId(type.id);
|
||||
setDeleteDialogSchedulingType(type.schedulingType);
|
||||
}}
|
||||
StartIcon="trash"
|
||||
className="w-full rounded-none">
|
||||
{t("delete")}
|
||||
</DropdownItem>
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</Dropdown>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="min-w-9 mx-5 flex sm:hidden">
|
||||
<Dropdown>
|
||||
<DropdownMenuTrigger asChild data-testid={`event-type-options-${type.id}`}>
|
||||
<Button type="button" variant="icon" color="secondary" StartIcon="ellipsis" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuContent>
|
||||
{!isManagedEventType && (
|
||||
<>
|
||||
<DropdownMenuItem className="outline-none">
|
||||
<DropdownItem
|
||||
href={calLink}
|
||||
target="_blank"
|
||||
StartIcon="external-link"
|
||||
className="w-full rounded-none">
|
||||
{t("preview")}
|
||||
</DropdownItem>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="outline-none">
|
||||
<DropdownItem
|
||||
data-testid={`event-type-duplicate-${type.id}`}
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(calLink);
|
||||
showToast(t("link_copied"), "success");
|
||||
}}
|
||||
StartIcon="clipboard"
|
||||
className="w-full rounded-none text-left">
|
||||
{t("copy_link")}
|
||||
</DropdownItem>
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
)}
|
||||
{isNativeShare ? (
|
||||
<DropdownMenuItem className="outline-none">
|
||||
<DropdownItem
|
||||
data-testid={`event-type-duplicate-${type.id}`}
|
||||
onClick={() => {
|
||||
navigator
|
||||
.share({
|
||||
title: t("share"),
|
||||
text: t("share_event", { appName: APP_NAME }),
|
||||
url: calLink,
|
||||
})
|
||||
.then(() => showToast(t("link_shared"), "success"))
|
||||
.catch(() => showToast(t("failed"), "error"));
|
||||
}}
|
||||
StartIcon="upload"
|
||||
className="w-full rounded-none">
|
||||
{t("share")}
|
||||
</DropdownItem>
|
||||
</DropdownMenuItem>
|
||||
) : null}
|
||||
{!readOnly && (
|
||||
<DropdownMenuItem className="outline-none">
|
||||
<DropdownItem
|
||||
onClick={() => router.push(`/event-types/${type.id}`)}
|
||||
StartIcon="pencil"
|
||||
className="w-full rounded-none">
|
||||
{t("edit")}
|
||||
</DropdownItem>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
{!readOnly && !isManagedEventType && !isChildrenManagedEventType && (
|
||||
<DropdownMenuItem className="outline-none">
|
||||
<DropdownItem
|
||||
onClick={() => openDuplicateModal(type, group)}
|
||||
StartIcon="copy"
|
||||
data-testid={`event-type-duplicate-${type.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. */}
|
||||
{!readOnly && !isChildrenManagedEventType && (
|
||||
<>
|
||||
<DropdownMenuItem className="outline-none">
|
||||
<DropdownItem
|
||||
color="destructive"
|
||||
onClick={() => {
|
||||
setDeleteDialogOpen(true);
|
||||
setDeleteDialogTypeId(type.id);
|
||||
setDeleteDialogSchedulingType(type.schedulingType);
|
||||
}}
|
||||
StartIcon="trash"
|
||||
className="w-full rounded-none">
|
||||
{t("delete")}
|
||||
</DropdownItem>
|
||||
</DropdownMenuItem>
|
||||
</>
|
||||
)}
|
||||
<DropdownMenuSeparator />
|
||||
{!isManagedEventType && (
|
||||
<div className="hover:bg-subtle flex h-9 cursor-pointer flex-row items-center justify-between px-4 py-2 transition">
|
||||
<Skeleton
|
||||
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")}
|
||||
</Skeleton>
|
||||
<Switch
|
||||
id="hiddenSwitch"
|
||||
name="Hidden"
|
||||
checked={!type.hidden}
|
||||
onCheckedChange={() => {
|
||||
setHiddenMutation.mutate({ id: type.id, hidden: !type.hidden });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenuPortal>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<ConfirmationDialogContent
|
||||
variety="danger"
|
||||
title={t(`delete${isManagedEventPrefix()}_event_type`)}
|
||||
confirmBtnText={t(`confirm_delete_event_type`)}
|
||||
loadingText={t(`confirm_delete_event_type`)}
|
||||
isPending={deleteMutation.isPending}
|
||||
onConfirm={(e) => {
|
||||
e.preventDefault();
|
||||
deleteEventTypeHandler(deleteDialogTypeId);
|
||||
}}>
|
||||
<p className="mt-5">
|
||||
<Trans
|
||||
i18nKey={`delete${isManagedEventPrefix()}_event_type_description`}
|
||||
components={{ li: <li />, ul: <ul className="ml-4 list-disc" /> }}>
|
||||
<ul>
|
||||
<li>Members assigned to this event type will also have their event types deleted.</li>
|
||||
<li>
|
||||
Anyone who they've shared their link with will no longer be able to book using it.
|
||||
</li>
|
||||
</ul>
|
||||
</Trans>
|
||||
</p>
|
||||
</ConfirmationDialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const InfiniteEventTypeList = ({
|
||||
group,
|
||||
readOnly,
|
||||
@@ -1339,66 +775,6 @@ export const InfiniteEventTypeList = ({
|
||||
);
|
||||
};
|
||||
|
||||
const EventTypeListHeading = ({
|
||||
profile,
|
||||
membershipCount,
|
||||
teamId,
|
||||
bookerUrl,
|
||||
}: EventTypeListHeadingProps): JSX.Element => {
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
|
||||
const publishTeamMutation = trpc.viewer.teams.publish.useMutation({
|
||||
onSuccess(data) {
|
||||
router.push(data.url);
|
||||
},
|
||||
onError: (error) => {
|
||||
showToast(error.message, "error");
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="mb-4 flex items-center space-x-2">
|
||||
<Avatar
|
||||
alt={profile.name || ""}
|
||||
href={teamId ? `/settings/teams/${teamId}/profile` : "/settings/my-account/profile"}
|
||||
imageSrc={profile.image}
|
||||
size="md"
|
||||
className="mt-1 inline-flex justify-center"
|
||||
/>
|
||||
<div>
|
||||
<Link
|
||||
href={teamId ? `/settings/teams/${teamId}/profile` : "/settings/my-account/profile"}
|
||||
className="text-emphasis font-bold">
|
||||
{profile.name || ""}
|
||||
</Link>
|
||||
{membershipCount && teamId && (
|
||||
<span className="text-subtle relative -top-px me-2 ms-2 text-xs">
|
||||
<Link href={`/settings/teams/${teamId}/members`}>
|
||||
<Badge variant="gray">
|
||||
<Icon name="users" className="-mt-px mr-1 inline h-3 w-3" />
|
||||
{membershipCount}
|
||||
</Badge>
|
||||
</Link>
|
||||
</span>
|
||||
)}
|
||||
{profile.slug && (
|
||||
<Link href={`${bookerUrl}/${profile.slug}`} className="text-subtle block text-xs">
|
||||
{`${bookerUrl.replace("https://", "").replace("http://", "")}/${profile.slug}`}
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
{!profile.slug && !!teamId && (
|
||||
<button onClick={() => publishTeamMutation.mutate({ teamId })}>
|
||||
<Badge variant="gray" className="-ml-2 mb-1">
|
||||
{t("upgrade")}
|
||||
</Badge>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const CreateFirstEventTypeView = ({ slug }: { slug: string }) => {
|
||||
const { t } = useLocale();
|
||||
|
||||
@@ -1420,7 +796,6 @@ const CreateFirstEventTypeView = ({ slug }: { slug: string }) => {
|
||||
const CTA = ({
|
||||
profileOptions,
|
||||
isOrganization,
|
||||
isInfiniteScrollEnabled,
|
||||
}: {
|
||||
profileOptions: {
|
||||
teamId: number | null | undefined;
|
||||
@@ -1430,7 +805,6 @@ const CTA = ({
|
||||
slug: string | null;
|
||||
}[];
|
||||
isOrganization: boolean;
|
||||
isInfiniteScrollEnabled: boolean;
|
||||
}) => {
|
||||
const { t } = useLocale();
|
||||
|
||||
@@ -1441,24 +815,11 @@ const CTA = ({
|
||||
data-testid="new-event-type"
|
||||
subtitle={t("create_event_on").toUpperCase()}
|
||||
options={profileOptions}
|
||||
createDialog={() => (
|
||||
<CreateEventTypeDialog
|
||||
isInfiniteScrollEnabled={isInfiniteScrollEnabled}
|
||||
profileOptions={profileOptions}
|
||||
/>
|
||||
)}
|
||||
createDialog={() => <CreateEventTypeDialog profileOptions={profileOptions} />}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const Actions = (props: { showDivider: boolean }) => {
|
||||
return (
|
||||
<div className="hidden items-center md:flex">
|
||||
<TeamsFilter useProfileFilter popoverTriggerClassNames="mb-0" showVerticalDivider={props.showDivider} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const EmptyEventTypeList = ({ group }: { group: EventTypeGroup | InfiniteEventTypeGroup }) => {
|
||||
const { t } = useLocale();
|
||||
return (
|
||||
@@ -1478,99 +839,6 @@ const EmptyEventTypeList = ({ group }: { group: EventTypeGroup | InfiniteEventTy
|
||||
);
|
||||
};
|
||||
|
||||
const Main = ({
|
||||
status,
|
||||
errorMessage,
|
||||
data: rawData,
|
||||
filters,
|
||||
}: {
|
||||
status: string;
|
||||
data: GetByViewerResponse;
|
||||
errorMessage?: string;
|
||||
filters: ReturnType<typeof getTeamsFiltersFromQuery>;
|
||||
}) => {
|
||||
const isMobile = useMediaQuery("(max-width: 768px)");
|
||||
const searchParams = useCompatSearchParams();
|
||||
|
||||
if (status === "error") {
|
||||
return <Alert severity="error" title="Something went wrong" message={errorMessage} />;
|
||||
}
|
||||
|
||||
if (!rawData || status === "pending") {
|
||||
return <SkeletonLoader />;
|
||||
}
|
||||
|
||||
const isFilteredByOnlyOneItem =
|
||||
(filters?.teamIds?.length === 1 || filters?.userIds?.length === 1) &&
|
||||
rawData.eventTypeGroups.length === 1;
|
||||
|
||||
const data = denormalizePayload(rawData);
|
||||
return (
|
||||
<>
|
||||
{data.eventTypeGroups.length > 1 || isFilteredByOnlyOneItem ? (
|
||||
<>
|
||||
{isMobile ? (
|
||||
<MobileTeamsTab eventTypeGroups={data.eventTypeGroups} />
|
||||
) : (
|
||||
data.eventTypeGroups.map((group, index: number) => {
|
||||
const eventsLockedByOrg = group.profile.eventTypesLockedByOrg;
|
||||
const userHasManagedOrHiddenEventTypes = group.eventTypes.find(
|
||||
(event) => event.metadata?.managedEventConfig || event.hidden
|
||||
);
|
||||
if (eventsLockedByOrg && !userHasManagedOrHiddenEventTypes) return null;
|
||||
return (
|
||||
<div
|
||||
className="mt-4 flex flex-col"
|
||||
data-testid={`slug-${group.profile.slug}`}
|
||||
key={group.profile.slug}>
|
||||
{/* If the group is readonly and empty don't leave a floating header when the user cant see the create box due
|
||||
to it being readonly for that user */}
|
||||
{group.eventTypes.length === 0 && group.metadata.readOnly ? null : (
|
||||
<EventTypeListHeading
|
||||
profile={group.profile}
|
||||
membershipCount={group.metadata.membershipCount}
|
||||
teamId={group.teamId}
|
||||
bookerUrl={group.bookerUrl}
|
||||
/>
|
||||
)}
|
||||
|
||||
{group.eventTypes.length ? (
|
||||
<EventTypeList
|
||||
types={group.eventTypes}
|
||||
group={group}
|
||||
bookerUrl={group.bookerUrl}
|
||||
groupIndex={index}
|
||||
readOnly={group.metadata.readOnly}
|
||||
lockedByOrg={eventsLockedByOrg}
|
||||
/>
|
||||
) : group.teamId && !group.metadata.readOnly ? (
|
||||
<EmptyEventTypeList group={group} />
|
||||
) : !group.metadata.readOnly ? (
|
||||
<CreateFirstEventTypeView slug={data.profiles[0].slug ?? ""} />
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
data.eventTypeGroups.length === 1 && (
|
||||
<EventTypeList
|
||||
types={data.eventTypeGroups[0].eventTypes}
|
||||
group={data.eventTypeGroups[0]}
|
||||
groupIndex={0}
|
||||
bookerUrl={data.eventTypeGroups[0].bookerUrl}
|
||||
readOnly={data.eventTypeGroups[0].metadata.readOnly}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
{data.eventTypeGroups.length === 0 && <CreateFirstEventTypeView slug={data.profiles[0].slug ?? ""} />}
|
||||
<EventTypeEmbedDialog />
|
||||
{searchParams?.get("dialog") === "duplicate" && <DuplicateDialog />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const InfiniteScrollMain = ({
|
||||
status,
|
||||
errorMessage,
|
||||
@@ -1607,20 +875,20 @@ const InfiniteScrollMain = ({
|
||||
{eventTypeGroups.length >= 1 && (
|
||||
<>
|
||||
<HorizontalTabs tabs={tabs} />
|
||||
<InfiniteMobileTeamsTab activeEventTypeGroup={activeEventTypeGroup[0]} />
|
||||
<InfiniteTeamsTab activeEventTypeGroup={activeEventTypeGroup[0]} />
|
||||
</>
|
||||
)}
|
||||
{eventTypeGroups.length === 0 && <CreateFirstEventTypeView slug={profiles[0].slug ?? ""} />}
|
||||
<EventTypeEmbedDialog />
|
||||
{searchParams?.get("dialog") === "duplicate" && <DuplicateDialog isInfiniteScrollEnabled />}
|
||||
{searchParams?.get("dialog") === "duplicate" && <DuplicateDialog />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const EventTypesPage: React.FC<{ isInfiniteScrollEnabled?: boolean }> & {
|
||||
const EventTypesPage: React.FC & {
|
||||
PageWrapper?: AppProps["Component"]["PageWrapper"];
|
||||
getLayout?: AppProps["Component"]["getLayout"];
|
||||
} = ({ isInfiniteScrollEnabled = false }) => {
|
||||
} = () => {
|
||||
const { t } = useLocale();
|
||||
const searchParams = useCompatSearchParams();
|
||||
const { open } = useIntercom();
|
||||
@@ -1632,13 +900,6 @@ const EventTypesPage: React.FC<{ isInfiniteScrollEnabled?: boolean }> & {
|
||||
const filters = getTeamsFiltersFromQuery(routerQuery);
|
||||
const router = useRouter();
|
||||
|
||||
const { data, status, error } = trpc.viewer.eventTypes.getByViewer.useQuery(filters && { filters }, {
|
||||
refetchOnWindowFocus: false,
|
||||
gcTime: 1 * 60 * 60 * 1000,
|
||||
staleTime: 1 * 60 * 60 * 1000,
|
||||
enabled: !isInfiniteScrollEnabled,
|
||||
});
|
||||
|
||||
// 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: getUserEventGroupsData,
|
||||
@@ -1648,7 +909,6 @@ const EventTypesPage: React.FC<{ isInfiniteScrollEnabled?: boolean }> & {
|
||||
refetchOnWindowFocus: false,
|
||||
gcTime: 1 * 60 * 60 * 1000,
|
||||
staleTime: 1 * 60 * 60 * 1000,
|
||||
enabled: isInfiniteScrollEnabled,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@@ -1672,31 +932,18 @@ const EventTypesPage: React.FC<{ isInfiniteScrollEnabled?: boolean }> & {
|
||||
}, [orgBranding, user]);
|
||||
|
||||
const profileOptions =
|
||||
(data?.profiles
|
||||
? data?.profiles
|
||||
.filter((profile) => !profile.readOnly)
|
||||
.filter((profile) => !profile.eventTypesLockedByOrg)
|
||||
.map((profile) => {
|
||||
return {
|
||||
teamId: profile.teamId,
|
||||
label: profile.name || profile.slug,
|
||||
image: profile.image,
|
||||
membershipRole: profile.membershipRole,
|
||||
slug: profile.slug,
|
||||
};
|
||||
})
|
||||
: getUserEventGroupsData?.profiles
|
||||
?.filter((profile) => !profile.readOnly)
|
||||
?.filter((profile) => !profile.eventTypesLockedByOrg)
|
||||
?.map((profile) => {
|
||||
return {
|
||||
teamId: profile.teamId,
|
||||
label: profile.name || profile.slug,
|
||||
image: profile.image,
|
||||
membershipRole: profile.membershipRole,
|
||||
slug: profile.slug,
|
||||
};
|
||||
})) ?? [];
|
||||
getUserEventGroupsData?.profiles
|
||||
?.filter((profile) => !profile.readOnly)
|
||||
?.filter((profile) => !profile.eventTypesLockedByOrg)
|
||||
?.map((profile) => {
|
||||
return {
|
||||
teamId: profile.teamId,
|
||||
label: profile.name || profile.slug,
|
||||
image: profile.image,
|
||||
membershipRole: profile.membershipRole,
|
||||
slug: profile.slug,
|
||||
};
|
||||
}) ?? [];
|
||||
|
||||
return (
|
||||
<Shell
|
||||
@@ -1707,63 +954,20 @@ const EventTypesPage: React.FC<{ isInfiniteScrollEnabled?: boolean }> & {
|
||||
heading={t("event_types_page_title")}
|
||||
hideHeadingOnMobile
|
||||
subtitle={t("event_types_page_subtitle")}
|
||||
beforeCTAactions={
|
||||
isInfiniteScrollEnabled ? undefined : <Actions showDivider={profileOptions.length > 0} />
|
||||
}
|
||||
CTA={
|
||||
<CTA
|
||||
isInfiniteScrollEnabled={isInfiniteScrollEnabled}
|
||||
profileOptions={profileOptions}
|
||||
isOrganization={!!user?.organizationId}
|
||||
/>
|
||||
}>
|
||||
CTA={<CTA profileOptions={profileOptions} isOrganization={!!user?.organizationId} />}>
|
||||
<HeadSeo
|
||||
title="Event Types"
|
||||
description="Create events to share for people to book on your calendar."
|
||||
/>
|
||||
{isInfiniteScrollEnabled ? (
|
||||
<InfiniteScrollMain
|
||||
profiles={getUserEventGroupsData?.profiles}
|
||||
eventTypeGroups={getUserEventGroupsData?.eventTypeGroups}
|
||||
status={getUserEventGroupsStatus}
|
||||
errorMessage={getUserEventGroupsStatusError?.message}
|
||||
/>
|
||||
) : (
|
||||
<Main data={data} status={status} errorMessage={error?.message} filters={filters} />
|
||||
)}
|
||||
|
||||
<InfiniteScrollMain
|
||||
profiles={getUserEventGroupsData?.profiles}
|
||||
eventTypeGroups={getUserEventGroupsData?.eventTypeGroups}
|
||||
status={getUserEventGroupsStatus}
|
||||
errorMessage={getUserEventGroupsStatusError?.message}
|
||||
/>
|
||||
</Shell>
|
||||
);
|
||||
};
|
||||
|
||||
export default EventTypesPage;
|
||||
|
||||
function normalizeEventType(eventType: DeNormalizedEventType): EventType {
|
||||
return {
|
||||
...eventType,
|
||||
userIds: eventType.users.map((user) => user.id),
|
||||
};
|
||||
}
|
||||
|
||||
function denormalizePayload(data: NonNullable<GetByViewerResponse>) {
|
||||
return {
|
||||
...data,
|
||||
eventTypeGroups: data.eventTypeGroups.map((eventTypeGroup) => {
|
||||
return {
|
||||
...eventTypeGroup,
|
||||
eventTypes: eventTypeGroup.eventTypes.map((eventType) => {
|
||||
const { userIds, ...rest } = eventType;
|
||||
return {
|
||||
...rest,
|
||||
users: userIds.map((userId) => {
|
||||
const user = data.allUsersAcrossAllEventTypes.get(userId);
|
||||
if (!user) {
|
||||
throw new Error(`User with id ${userId} not found in allUsersAcrossAllEventTypes`);
|
||||
}
|
||||
return user;
|
||||
}),
|
||||
};
|
||||
}),
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -42,7 +42,6 @@ export const CreateTeamEventType = () => {
|
||||
SubmitButton={SubmitButton}
|
||||
onSuccessMutation={onSuccessMutation}
|
||||
onErrorMutation={onErrorMutation}
|
||||
isInfiniteScrollEnabled={false}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -249,6 +249,7 @@ test.describe("Manage Booking Questions", () => {
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -256,7 +257,10 @@ test.describe("Manage Booking Questions", () => {
|
||||
const webhookReceiver = await addWebhook(undefined, teamId);
|
||||
|
||||
await test.step("Go to First Team Event", async () => {
|
||||
const $eventTypes = page.locator("[data-testid=event-types]").nth(1).locator("li a");
|
||||
await page.getByTestId(`horizontal-tab-${team?.name}`).click();
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
const $eventTypes = page.locator("[data-testid=event-types]").locator("li a");
|
||||
const firstEventTypeElement = $eventTypes.first();
|
||||
|
||||
await firstEventTypeElement.click();
|
||||
|
||||
@@ -38,7 +38,7 @@ async function setupManagedEvent({
|
||||
const memberUser = users.get().find((u) => u.name === teamMateName)!;
|
||||
const { team } = await adminUser.getFirstTeamMembership();
|
||||
const managedEvent = await adminUser.getFirstTeamEvent(team.id, SchedulingType.MANAGED);
|
||||
return { adminUser, memberUser, managedEvent, teamMateName, teamEventTitle };
|
||||
return { adminUser, memberUser, managedEvent, teamMateName, teamEventTitle, teamId: team.id };
|
||||
}
|
||||
|
||||
/** Short hand to get elements by translation key */
|
||||
@@ -129,7 +129,9 @@ test.describe("Managed Event Types", () => {
|
||||
test("Provides discrete field lock/unlock state for admin", async ({ page, users }) => {
|
||||
const { adminUser, teamEventTitle } = await setupManagedEvent({ users });
|
||||
await adminUser.apiLogin();
|
||||
await page.goto("/event-types");
|
||||
const teamMembership = await adminUser.getFirstTeamMembership();
|
||||
|
||||
await page.goto(`/event-types?teamId=${teamMembership.team.id}`);
|
||||
|
||||
await page.getByTestId("event-types").locator(`a[title="${teamEventTitle}"]`).click();
|
||||
await page.waitForURL("event-types/**");
|
||||
@@ -169,7 +171,7 @@ test.describe("Managed Event Types", () => {
|
||||
users,
|
||||
browser,
|
||||
}) => {
|
||||
const { adminUser, memberUser, teamEventTitle } = await setupManagedEvent({
|
||||
const { adminUser, memberUser, teamEventTitle, teamId } = await setupManagedEvent({
|
||||
users,
|
||||
unlockedFields: {
|
||||
title: true,
|
||||
@@ -185,7 +187,7 @@ test.describe("Managed Event Types", () => {
|
||||
|
||||
// We edit the managed event as original owner
|
||||
const [adminContext, adminPage] = await adminUser.apiLoginOnNewBrowser(browser);
|
||||
await adminPage.goto("/event-types");
|
||||
await adminPage.goto(`/event-types?teamId=${teamId}`);
|
||||
await adminPage.getByTestId("event-types").locator(`a[title="${teamEventTitle}"]`).click();
|
||||
await adminPage.waitForURL("event-types/**");
|
||||
await adminPage.locator('input[name="length"]').fill(`45`);
|
||||
|
||||
@@ -47,19 +47,28 @@ test.describe("user1NotMemberOfOrg1 is part of team1MemberOfOrg1", () => {
|
||||
|
||||
await user1NotMemberOfOrg1.apiLogin();
|
||||
await page.goto("/event-types");
|
||||
const userEventLinksSelector = `[data-testid=slug-${user1NotMemberOfOrg1.username}] [data-testid="preview-link-button"]`;
|
||||
await page.waitForSelector(userEventLinksSelector);
|
||||
|
||||
await page.waitForSelector(`[data-testid="event-types"] [data-testid="preview-link-button"]`, {
|
||||
timeout: 5000,
|
||||
});
|
||||
const userEventLinksLocators = await page
|
||||
.locator(`[data-testid="event-types"] [data-testid="preview-link-button"]`)
|
||||
.all();
|
||||
|
||||
// Get all the event links
|
||||
const userEventLinksLocators = await page.locator(userEventLinksSelector).all();
|
||||
expect(userEventLinksLocators.length).toBeGreaterThan(0);
|
||||
for (const userEventLinkLocator of userEventLinksLocators) {
|
||||
const href = await userEventLinkLocator.getAttribute("href");
|
||||
expect(href).toContain(WEBAPP_URL);
|
||||
}
|
||||
|
||||
await page.goto(`/event-types?teamId=${team1MemberOfOrg1.id}`);
|
||||
|
||||
await page.waitForSelector(`[data-testid="event-types"] [data-testid="preview-link-button"]`, {
|
||||
timeout: 5000,
|
||||
});
|
||||
const teamEventLinksLocators = await page
|
||||
.locator(`[data-testid=slug-${team1MemberOfOrg1.slug}] [data-testid="preview-link-button"]`)
|
||||
.locator(`[data-testid="event-types"] [data-testid="preview-link-button"]`)
|
||||
.all();
|
||||
|
||||
expect(teamEventLinksLocators.length).toBeGreaterThan(0);
|
||||
|
||||
@@ -263,7 +263,7 @@ testBothFutureAndLegacyRoutes.describe("Teams - NonOrg", (routeVariant) => {
|
||||
id: teamEventId,
|
||||
} = await owner.getFirstTeamEvent(team.id);
|
||||
|
||||
await page.goto("/event-types");
|
||||
await page.goto(`/event-types?teamId=${team.id}`);
|
||||
|
||||
await page.getByTestId(`event-type-options-${teamEventId}`).first().click();
|
||||
await page.getByTestId("embed").click();
|
||||
|
||||
@@ -17,7 +17,6 @@ type props = {
|
||||
SubmitButton: (isPending: boolean) => ReactNode;
|
||||
onSuccessMutation: (eventType: EventType) => void;
|
||||
onErrorMutation: (message: string) => void;
|
||||
isInfiniteScrollEnabled: boolean;
|
||||
};
|
||||
export const TeamEventTypeForm = ({
|
||||
isTeamAdminOrOwner,
|
||||
@@ -25,7 +24,6 @@ export const TeamEventTypeForm = ({
|
||||
SubmitButton,
|
||||
onSuccessMutation,
|
||||
onErrorMutation,
|
||||
isInfiniteScrollEnabled,
|
||||
}: props) => {
|
||||
const { t } = useLocale();
|
||||
const orgBranding = useOrgBranding();
|
||||
@@ -35,11 +33,7 @@ export const TeamEventTypeForm = ({
|
||||
);
|
||||
const urlPrefix = orgBranding?.fullDomain ?? process.env.NEXT_PUBLIC_WEBSITE_URL;
|
||||
|
||||
const { form, createMutation, isManagedEventType } = useCreateEventType(
|
||||
onSuccessMutation,
|
||||
onErrorMutation,
|
||||
isInfiniteScrollEnabled
|
||||
);
|
||||
const { form, createMutation, isManagedEventType } = useCreateEventType(onSuccessMutation, onErrorMutation);
|
||||
const { register, setValue, formState } = form;
|
||||
|
||||
return (
|
||||
|
||||
@@ -64,7 +64,6 @@ const querySchema = z.object({
|
||||
|
||||
export default function CreateEventTypeDialog({
|
||||
profileOptions,
|
||||
isInfiniteScrollEnabled,
|
||||
}: {
|
||||
profileOptions: {
|
||||
teamId: number | null | undefined;
|
||||
@@ -72,7 +71,6 @@ export default function CreateEventTypeDialog({
|
||||
image: string | undefined;
|
||||
membershipRole: MembershipRole | null | undefined;
|
||||
}[];
|
||||
isInfiniteScrollEnabled: boolean;
|
||||
}) {
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
@@ -115,11 +113,7 @@ export default function CreateEventTypeDialog({
|
||||
);
|
||||
};
|
||||
|
||||
const { form, createMutation, isManagedEventType } = useCreateEventType(
|
||||
onSuccessMutation,
|
||||
onErrorMutation,
|
||||
isInfiniteScrollEnabled
|
||||
);
|
||||
const { form, createMutation, isManagedEventType } = useCreateEventType(onSuccessMutation, onErrorMutation);
|
||||
|
||||
const { register } = form;
|
||||
const urlPrefix = orgBranding?.fullDomain ?? process.env.NEXT_PUBLIC_WEBSITE_URL;
|
||||
@@ -149,7 +143,6 @@ export default function CreateEventTypeDialog({
|
||||
SubmitButton={SubmitButton}
|
||||
onSuccessMutation={onSuccessMutation}
|
||||
onErrorMutation={onErrorMutation}
|
||||
isInfiniteScrollEnabled={isInfiniteScrollEnabled}
|
||||
/>
|
||||
) : (
|
||||
<Form
|
||||
|
||||
@@ -36,7 +36,7 @@ const querySchema = z.object({
|
||||
parentId: z.coerce.number().optional().nullable(),
|
||||
});
|
||||
|
||||
const DuplicateDialog = ({ isInfiniteScrollEnabled }: { isInfiniteScrollEnabled?: boolean }) => {
|
||||
const DuplicateDialog = () => {
|
||||
const utils = trpc.useUtils();
|
||||
|
||||
const searchParams = useCompatSearchParams();
|
||||
@@ -74,15 +74,11 @@ const DuplicateDialog = ({ isInfiniteScrollEnabled }: { isInfiniteScrollEnabled?
|
||||
onSuccess: async ({ eventType }) => {
|
||||
await router.replace(`/event-types/${eventType.id}`);
|
||||
|
||||
if (isInfiniteScrollEnabled) {
|
||||
await utils.viewer.eventTypes.getUserEventGroups.invalidate();
|
||||
await utils.viewer.eventTypes.getEventTypesFromGroup.invalidate({
|
||||
limit: 10,
|
||||
group: { teamId: eventType?.teamId, parentId: eventType?.parentId },
|
||||
});
|
||||
} else {
|
||||
await utils.viewer.eventTypes.getByViewer.invalidate();
|
||||
}
|
||||
await utils.viewer.eventTypes.getUserEventGroups.invalidate();
|
||||
await utils.viewer.eventTypes.getEventTypesFromGroup.invalidate({
|
||||
limit: 10,
|
||||
group: { teamId: eventType?.teamId, parentId: eventType?.parentId },
|
||||
});
|
||||
|
||||
showToast(
|
||||
t("event_type_created_successfully", {
|
||||
|
||||
@@ -115,10 +115,6 @@ export const ORGANIZATION_SELF_SERVE_PRICE = process.env.NEXT_PUBLIC_ORGANIZATIO
|
||||
|
||||
export const ORGANIZATION_MIN_SEATS = 30;
|
||||
|
||||
export const ENABLE_INFINITE_EVENT_TYPES_FOR_ORG = JSON.parse(
|
||||
`[${process.env.ENABLE_INFINITE_EVENT_TYPES_FOR_ORG || ""}]`
|
||||
) as string[];
|
||||
|
||||
// Needed for emails in E2E
|
||||
export const IS_MAILHOG_ENABLED = process.env.E2E_TEST_MAILHOG_ENABLED === "1";
|
||||
export const CALCOM_VERSION = process.env.NEXT_PUBLIC_CALCOM_VERSION as string;
|
||||
|
||||
@@ -13,8 +13,7 @@ import { trpc } from "@calcom/trpc/react";
|
||||
|
||||
export const useCreateEventType = (
|
||||
onSuccessMutation: (eventType: EventType) => void,
|
||||
onErrorMutation: (message: string) => void,
|
||||
isInfiniteScrollEnabled: boolean
|
||||
onErrorMutation: (message: string) => void
|
||||
) => {
|
||||
const utils = trpc.useUtils();
|
||||
const { t } = useLocale();
|
||||
@@ -40,14 +39,10 @@ export const useCreateEventType = (
|
||||
onSuccess: async ({ eventType }) => {
|
||||
onSuccessMutation(eventType);
|
||||
|
||||
if (isInfiniteScrollEnabled) {
|
||||
await utils.viewer.eventTypes.getEventTypesFromGroup.fetchInfinite({
|
||||
group: { teamId: eventType.teamId, parentId: eventType.parentId },
|
||||
limit: 10,
|
||||
});
|
||||
} else {
|
||||
await utils.viewer.eventTypes.getByViewer.invalidate();
|
||||
}
|
||||
await utils.viewer.eventTypes.getEventTypesFromGroup.fetchInfinite({
|
||||
group: { teamId: eventType.teamId, parentId: eventType.parentId },
|
||||
limit: 10,
|
||||
});
|
||||
|
||||
form.reset();
|
||||
},
|
||||
|
||||
@@ -315,7 +315,6 @@
|
||||
"EMAIL_SERVER_PORT",
|
||||
"EMAIL_SERVER_USER",
|
||||
"EMAIL_SERVER",
|
||||
"ENABLE_INFINITE_EVENT_TYPES_FOR_ORG",
|
||||
"EXCHANGE_DEFAULT_EWS_URL",
|
||||
"FORMBRICKS_FEEDBACK_SURVEY_ID",
|
||||
"AVATARAPI_USERNAME",
|
||||
|
||||
Reference in New Issue
Block a user