* feat: add ListEventTypes atom (#25047) * feat: add ListEventTypes atom for Platform (#25047) - Add EventTypeListItem component in @calcom/features - Add ListEventTypesPlatformWrapper with delete functionality - Implement useAtomGetAllEventTypes hook - Update backend to include slug, description, length - Add 11 unit tests (65% coverage) - Add 3 i18n keys No breaking changes. Backward compatible. * refactor(eventtypes): improve EventTypeListItem readability - Extract EventTypeContent and EventTypeActions sub-components - Add formatEventTypeDuration helper (90min → '1h 30m') - Use Badge for duration display (consistency with EventTypeDescription) - Use polymorphic Wrapper component (DRY) - Add comprehensive tests - Reduce main component size Addresses @volnei feedback * refactor: replace window.confirm with ConfirmationDialog - Replace native window.confirm with ConfirmationDialogContent - Update test mocks for Dialog components - Fix HTML nesting (div instead of p for Badge container) - Follows Cal.com pattern from AvailabilitySettings Addresses @ThyMinimalDev feedback * fix: add aria-label to options button for accessibility * test: update getBulkEventTypes test to include new fields (description, slug, length) * refactor: move EventTypeListItem to atoms package --------- Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
This commit is contained in:
@@ -4063,6 +4063,9 @@
|
||||
"no_available_users_found_input": "No available users found",
|
||||
"repeats_num_times": "Repeats {{count}} times",
|
||||
"view_booking_details": "View Booking Details",
|
||||
"cannot_delete_event_type": "This event type cannot be deleted",
|
||||
"delete_event_type_confirmation": "Are you sure you want to delete this event type?",
|
||||
"error_loading_event_types": "Error loading event types",
|
||||
"recurring_booking_description": "Recurring {{frequency}} event with {{count}} occurrences",
|
||||
"team_bio": "Team Bio",
|
||||
"team_bio_placeholder": "Tell us about your team...",
|
||||
|
||||
@@ -265,6 +265,9 @@ describe("getBulkUserEventTypes", () => {
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
description: true,
|
||||
length: true,
|
||||
slug: true,
|
||||
locations: true,
|
||||
metadata: true,
|
||||
parentId: true,
|
||||
|
||||
@@ -40,6 +40,9 @@ export const getBulkUserEventTypes = async (userId: number) => {
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
slug: true,
|
||||
description: true,
|
||||
length: true,
|
||||
locations: true,
|
||||
metadata: true,
|
||||
parentId: true,
|
||||
@@ -65,6 +68,9 @@ export const getBulkTeamEventTypes = async (teamId: number) => {
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
slug: true,
|
||||
description: true,
|
||||
length: true,
|
||||
locations: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { vi } from "vitest";
|
||||
|
||||
import type { AtomEventTypeListItem } from "../types";
|
||||
import { EventTypeListItem } from "../components/EventTypeListItem";
|
||||
|
||||
// Mock auto-animate
|
||||
vi.mock("@formkit/auto-animate/react", () => ({
|
||||
useAutoAnimate: () => [null],
|
||||
}));
|
||||
|
||||
// Mock Next.js Link
|
||||
vi.mock("next/link", () => ({
|
||||
__esModule: true,
|
||||
default: ({ children, href }: { children: React.ReactNode; href: string }) => <a href={href}>{children}</a>,
|
||||
}));
|
||||
|
||||
// Mock useLocale
|
||||
vi.mock("@calcom/lib/hooks/useLocale", () => ({
|
||||
useLocale: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}));
|
||||
|
||||
// Mock toast
|
||||
vi.mock("@calcom/ui/components/toast", () => ({
|
||||
showToast: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock dialog components
|
||||
vi.mock("@calcom/ui/components/dialog", () => ({
|
||||
Dialog: ({ children, open }: { children: React.ReactNode; open: boolean }) =>
|
||||
open ? <div data-testid="dialog">{children}</div> : null,
|
||||
ConfirmationDialogContent: ({
|
||||
children,
|
||||
onConfirm,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
onConfirm: () => void;
|
||||
}) => (
|
||||
<div data-testid="confirmation-dialog">
|
||||
{children}
|
||||
<button onClick={onConfirm} data-testid="confirm-delete">
|
||||
Confirm
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
describe("EventTypeListItem", () => {
|
||||
const mockEventType: AtomEventTypeListItem = {
|
||||
id: 1,
|
||||
title: "30 Min Meeting",
|
||||
slug: "30min",
|
||||
description: "Quick meeting",
|
||||
length: 30,
|
||||
locations: null,
|
||||
logo: "/test-logo.svg",
|
||||
};
|
||||
|
||||
const mockDeleteFunction = vi.fn();
|
||||
|
||||
const renderComponent = ({
|
||||
eventType = mockEventType,
|
||||
deleteFunction = mockDeleteFunction,
|
||||
isDeletable = true,
|
||||
getEventTypeUrl,
|
||||
}: {
|
||||
eventType?: typeof mockEventType;
|
||||
deleteFunction?: typeof mockDeleteFunction;
|
||||
isDeletable?: boolean;
|
||||
getEventTypeUrl?: (id: number) => string;
|
||||
} = {}) => {
|
||||
return render(
|
||||
<EventTypeListItem
|
||||
eventType={eventType}
|
||||
deleteFunction={deleteFunction}
|
||||
isDeletable={isDeletable}
|
||||
getEventTypeUrl={getEventTypeUrl}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
it("should render event type details correctly", () => {
|
||||
renderComponent();
|
||||
|
||||
expect(screen.getByText("30 Min Meeting")).toBeInTheDocument();
|
||||
expect(screen.getByText("Quick meeting")).toBeInTheDocument();
|
||||
expect(screen.getByText(/30m/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render as link when getEventTypeUrl is provided", () => {
|
||||
const getEventTypeUrl = (id: number) => `/event-types/${id}`;
|
||||
renderComponent({ getEventTypeUrl });
|
||||
|
||||
const link = screen.getByRole("link", { name: /30 Min Meeting/i });
|
||||
expect(link).toHaveAttribute("href", "/event-types/1");
|
||||
});
|
||||
|
||||
it("should render without link when getEventTypeUrl is not provided", () => {
|
||||
renderComponent();
|
||||
|
||||
const links = screen.queryAllByRole("link");
|
||||
expect(links.length).toBe(0);
|
||||
});
|
||||
|
||||
it("should display event type without description when description is null", () => {
|
||||
renderComponent({
|
||||
eventType: { ...mockEventType, description: null },
|
||||
});
|
||||
|
||||
expect(screen.getByText("30 Min Meeting")).toBeInTheDocument();
|
||||
expect(screen.queryByText("Quick meeting")).not.toBeInTheDocument();
|
||||
expect(screen.getByText(/30m/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render dropdown menu trigger", () => {
|
||||
renderComponent();
|
||||
|
||||
const dropdownTrigger = screen.getByTestId("event-type-options-1");
|
||||
expect(dropdownTrigger).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render with unique data-testid based on event type ID", () => {
|
||||
const customEventType = {
|
||||
...mockEventType,
|
||||
id: 999,
|
||||
};
|
||||
|
||||
renderComponent({ eventType: customEventType });
|
||||
|
||||
const dropdownTrigger = screen.getByTestId("event-type-options-999");
|
||||
expect(dropdownTrigger).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should display different event durations correctly", () => {
|
||||
const shortEvent = { ...mockEventType, length: 15 };
|
||||
const { rerender } = renderComponent({ eventType: shortEvent });
|
||||
|
||||
expect(screen.getByText(/15m/)).toBeInTheDocument();
|
||||
|
||||
const longEvent = { ...mockEventType, length: 120 };
|
||||
rerender(
|
||||
<EventTypeListItem eventType={longEvent} deleteFunction={mockDeleteFunction} isDeletable={true} />
|
||||
);
|
||||
|
||||
expect(screen.getByText(/2h/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render event type with long title without breaking layout", () => {
|
||||
const longTitleEvent = {
|
||||
...mockEventType,
|
||||
title: "This is a very long event type title that should be truncated properly in the UI",
|
||||
};
|
||||
|
||||
renderComponent({ eventType: longTitleEvent });
|
||||
|
||||
expect(
|
||||
screen.getByText("This is a very long event type title that should be truncated properly in the UI")
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText("Quick meeting")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render with locations as null", () => {
|
||||
const eventType: AtomEventTypeListItem = {
|
||||
...mockEventType,
|
||||
locations: null,
|
||||
};
|
||||
|
||||
renderComponent({ eventType });
|
||||
expect(screen.getByText("30 Min Meeting")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render without logo", () => {
|
||||
const eventType: AtomEventTypeListItem = {
|
||||
...mockEventType,
|
||||
logo: undefined,
|
||||
};
|
||||
|
||||
renderComponent({ eventType });
|
||||
expect(screen.getByText("30 Min Meeting")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("should render with locations object", () => {
|
||||
const eventType: AtomEventTypeListItem = {
|
||||
...mockEventType,
|
||||
locations: [{ type: "integrations:zoom" }],
|
||||
logo: "/app-store/zoomvideo/icon.svg",
|
||||
};
|
||||
|
||||
renderComponent({ eventType });
|
||||
expect(screen.getByText("30 Min Meeting")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { formatEventTypeDuration } from "../lib/formatEventTypeDuration";
|
||||
|
||||
describe("formatEventTypeDuration", () => {
|
||||
it("should format single digit minutes", () => {
|
||||
expect(formatEventTypeDuration(5)).toBe("5m");
|
||||
});
|
||||
|
||||
it("should format double digit minutes", () => {
|
||||
expect(formatEventTypeDuration(15)).toBe("15m");
|
||||
expect(formatEventTypeDuration(30)).toBe("30m");
|
||||
expect(formatEventTypeDuration(45)).toBe("45m");
|
||||
});
|
||||
|
||||
it("should format edge case at 59 minutes", () => {
|
||||
expect(formatEventTypeDuration(59)).toBe("59m");
|
||||
});
|
||||
|
||||
it("should handle edge case at 61 minutes", () => {
|
||||
expect(formatEventTypeDuration(61)).toBe("1h 1m");
|
||||
});
|
||||
|
||||
it("should format single hour", () => {
|
||||
expect(formatEventTypeDuration(60)).toBe("1h");
|
||||
});
|
||||
|
||||
it("should format multiple hours", () => {
|
||||
expect(formatEventTypeDuration(120)).toBe("2h");
|
||||
expect(formatEventTypeDuration(180)).toBe("3h");
|
||||
expect(formatEventTypeDuration(240)).toBe("4h");
|
||||
});
|
||||
|
||||
it("should format hours with minutes", () => {
|
||||
expect(formatEventTypeDuration(75)).toBe("1h 15m");
|
||||
expect(formatEventTypeDuration(150)).toBe("2h 30m");
|
||||
expect(formatEventTypeDuration(195)).toBe("3h 15m");
|
||||
});
|
||||
|
||||
it("should handle 1 minute", () => {
|
||||
expect(formatEventTypeDuration(1)).toBe("1m");
|
||||
});
|
||||
|
||||
it("should handle zero minutes", () => {
|
||||
expect(formatEventTypeDuration(0)).toBe("0m");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,151 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useState } from "react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Badge } from "@calcom/ui/components/badge";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { Dialog, ConfirmationDialogContent } from "@calcom/ui/components/dialog";
|
||||
import {
|
||||
Dropdown,
|
||||
DropdownItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@calcom/ui/components/dropdown";
|
||||
import { showToast } from "@calcom/ui/components/toast";
|
||||
|
||||
import type { AtomEventTypeListItem } from "../types";
|
||||
import { formatEventTypeDuration } from "../lib/formatEventTypeDuration";
|
||||
|
||||
const EventTypeContent = ({ eventType }: { eventType: AtomEventTypeListItem }) => {
|
||||
return (
|
||||
<div>
|
||||
<div className="space-x-2 rtl:space-x-reverse">
|
||||
<span className="text-emphasis truncate font-medium">{eventType.title}</span>
|
||||
</div>
|
||||
<div className="text-subtle mt-1">
|
||||
{eventType.description && <span className="block">{eventType.description}</span>}
|
||||
<Badge variant="gray" className="text-xs">
|
||||
{formatEventTypeDuration(eventType.length)}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type EventTypeActionsProps = {
|
||||
eventType: AtomEventTypeListItem;
|
||||
isDeletable: boolean;
|
||||
getEventTypeUrl?: (eventTypeId: number) => string;
|
||||
deleteFunction: ({ eventTypeId }: { eventTypeId: number }) => void;
|
||||
};
|
||||
|
||||
const EventTypeActions = ({
|
||||
eventType,
|
||||
isDeletable,
|
||||
getEventTypeUrl,
|
||||
deleteFunction,
|
||||
}: EventTypeActionsProps) => {
|
||||
const { t } = useLocale();
|
||||
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dropdown>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
data-testid={`event-type-options-${eventType.id}`}
|
||||
type="button"
|
||||
variant="icon"
|
||||
color="secondary"
|
||||
StartIcon="ellipsis"
|
||||
aria-label={t("options")}
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
{getEventTypeUrl && (
|
||||
<DropdownMenuItem className="min-w-40 focus:ring-muted">
|
||||
<DropdownItem type="button" StartIcon="pencil" href={getEventTypeUrl(eventType.id)}>
|
||||
{t("edit")}
|
||||
</DropdownItem>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem className="min-w-40 focus:ring-muted">
|
||||
<DropdownItem
|
||||
type="button"
|
||||
color="destructive"
|
||||
StartIcon="trash"
|
||||
data-testid={`delete-event-type-${eventType.id}`}
|
||||
className="rounded-t-none"
|
||||
onClick={() => {
|
||||
if (!isDeletable) {
|
||||
showToast(t("cannot_delete_event_type"), "error");
|
||||
return;
|
||||
}
|
||||
setIsDeleteDialogOpen(true);
|
||||
}}>
|
||||
{t("delete")}
|
||||
</DropdownItem>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</Dropdown>
|
||||
|
||||
<Dialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
|
||||
<ConfirmationDialogContent
|
||||
variety="danger"
|
||||
title={t("delete_event_type")}
|
||||
confirmBtnText={t("delete")}
|
||||
onConfirm={() => {
|
||||
deleteFunction({ eventTypeId: eventType.id });
|
||||
setIsDeleteDialogOpen(false);
|
||||
}}>
|
||||
{t("delete_event_type_confirmation")}
|
||||
</ConfirmationDialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
type EventTypeListItemProps = {
|
||||
eventType: AtomEventTypeListItem;
|
||||
deleteFunction: ({ eventTypeId }: { eventTypeId: number }) => void;
|
||||
isDeletable?: boolean;
|
||||
getEventTypeUrl?: (eventTypeId: number) => string;
|
||||
};
|
||||
|
||||
export function EventTypeListItem({
|
||||
eventType,
|
||||
deleteFunction,
|
||||
isDeletable = true,
|
||||
getEventTypeUrl,
|
||||
}: EventTypeListItemProps) {
|
||||
const Wrapper: React.ElementType = getEventTypeUrl ? Link : "div";
|
||||
const wrapperProps = getEventTypeUrl
|
||||
? {
|
||||
href: getEventTypeUrl(eventType.id),
|
||||
title: eventType.title,
|
||||
}
|
||||
: {};
|
||||
|
||||
return (
|
||||
<li key={eventType.id}>
|
||||
<div className="hover:bg-muted flex items-center justify-between px-3 py-5 transition sm:px-4">
|
||||
<div className="group flex w-full items-center justify-between">
|
||||
<Wrapper className="flex-grow truncate text-sm" {...wrapperProps}>
|
||||
<EventTypeContent eventType={eventType} />
|
||||
</Wrapper>
|
||||
</div>
|
||||
<EventTypeActions
|
||||
eventType={eventType}
|
||||
isDeletable={isDeletable}
|
||||
getEventTypeUrl={getEventTypeUrl}
|
||||
deleteFunction={deleteFunction}
|
||||
/>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import { V2_ENDPOINTS, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import type { ApiResponse } from "@calcom/platform-types";
|
||||
|
||||
import { useAtomsContext } from "../../hooks/useAtomsContext";
|
||||
import http from "../../lib/http";
|
||||
import type { AtomEventTypeListItem, AtomEventTypesResponse } from "../types";
|
||||
|
||||
export const QUERY_KEY = "use-atom-all-event-types";
|
||||
|
||||
export const useAtomGetAllEventTypes = () => {
|
||||
const pathname = `/atoms/${V2_ENDPOINTS.eventTypes}`;
|
||||
const { isInit, accessToken } = useAtomsContext();
|
||||
|
||||
const { isLoading, error, data, refetch } = useQuery<AtomEventTypeListItem[]>({
|
||||
queryKey: [QUERY_KEY],
|
||||
queryFn: () => {
|
||||
return http.get<ApiResponse<AtomEventTypesResponse>>(pathname).then((res) => {
|
||||
if (res.data.status === SUCCESS_STATUS) {
|
||||
return res.data.data.eventTypes;
|
||||
}
|
||||
throw new Error(res.data.error?.message);
|
||||
});
|
||||
},
|
||||
enabled: isInit && !!accessToken,
|
||||
});
|
||||
|
||||
return { isLoading, error, data, refetch };
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { ListEventTypesPlatformWrapper } from "./wrappers/ListEventTypesPlatformWrapper";
|
||||
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* Formats event type duration in a human-readable format.
|
||||
*
|
||||
* @param minutes - Duration in minutes (always an integer for event types)
|
||||
* @returns Formatted duration string (e.g., "30m", "1h", "1h 30m")
|
||||
*
|
||||
* @example
|
||||
* formatEventTypeDuration(30) // "30m"
|
||||
* formatEventTypeDuration(60) // "1h"
|
||||
* formatEventTypeDuration(90) // "1h 30m"
|
||||
*/
|
||||
export function formatEventTypeDuration(minutes: number): string {
|
||||
if (minutes < 60) {
|
||||
return `${minutes}m`;
|
||||
}
|
||||
|
||||
const hours = Math.floor(minutes / 60);
|
||||
const remainingMinutes = minutes % 60;
|
||||
|
||||
if (remainingMinutes === 0) {
|
||||
return `${hours}h`;
|
||||
}
|
||||
|
||||
return `${hours}h ${remainingMinutes}m`;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* Event type item as returned by the atoms API
|
||||
*/
|
||||
export type AtomEventTypeListItem = {
|
||||
id: number;
|
||||
title: string;
|
||||
slug: string;
|
||||
description: string | null;
|
||||
length: number;
|
||||
locations: unknown | null;
|
||||
logo?: string;
|
||||
};
|
||||
|
||||
export type AtomEventTypesResponse = {
|
||||
eventTypes: AtomEventTypeListItem[];
|
||||
};
|
||||
@@ -0,0 +1,87 @@
|
||||
import { AtomsWrapper } from "@/components/atoms-wrapper";
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
|
||||
import { EventTypeListItem } from "../components/EventTypeListItem";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { EmptyScreen } from "@calcom/ui/components/empty-screen";
|
||||
|
||||
import { useDeleteEventTypeById } from "../../hooks/event-types/private/useDeleteEventTypeById";
|
||||
import { useToast } from "../../src/components/ui/use-toast";
|
||||
import { useAtomGetAllEventTypes } from "../hooks/useAtomGetAllEventTypes";
|
||||
|
||||
interface ListEventTypesPlatformWrapperProps {
|
||||
getEventTypeUrl?: (eventTypeId: number) => string;
|
||||
}
|
||||
|
||||
export const ListEventTypesPlatformWrapper = ({
|
||||
getEventTypeUrl,
|
||||
}: ListEventTypesPlatformWrapperProps = {}) => {
|
||||
const [animationParentRef] = useAutoAnimate<HTMLUListElement>();
|
||||
const { toast } = useToast();
|
||||
const { t } = useLocale();
|
||||
|
||||
const {
|
||||
data: eventTypes,
|
||||
isLoading: isLoadingEventTypes,
|
||||
error,
|
||||
refetch: refetchEventTypes,
|
||||
} = useAtomGetAllEventTypes();
|
||||
|
||||
const { mutate: deleteEventType } = useDeleteEventTypeById({
|
||||
onSuccess: () => {
|
||||
toast({
|
||||
description: t("event_type_deleted_successfully"),
|
||||
});
|
||||
refetchEventTypes();
|
||||
},
|
||||
onError: (err) => {
|
||||
toast({
|
||||
description: err.message,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
if (isLoadingEventTypes) {
|
||||
return <>{t("loading")}</>;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<EmptyScreen
|
||||
Icon="alert-circle"
|
||||
headline={t("error_loading_event_types")}
|
||||
description={error.message}
|
||||
className="w-full"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isLoadingEventTypes && (!eventTypes || eventTypes.length === 0)) {
|
||||
return (
|
||||
<EmptyScreen
|
||||
Icon="calendar"
|
||||
headline={t("no_event_types")}
|
||||
description={t("no_event_types_have_been_setup")}
|
||||
className="w-full"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AtomsWrapper>
|
||||
<div className="border-subtle bg-default overflow-hidden rounded-md border">
|
||||
<ul className="divide-subtle divide-y" data-testid="event-types" ref={animationParentRef}>
|
||||
{eventTypes?.map((eventType) => (
|
||||
<EventTypeListItem
|
||||
key={eventType.id}
|
||||
eventType={eventType}
|
||||
deleteFunction={({ eventTypeId }) => deleteEventType(eventTypeId)}
|
||||
isDeletable={true}
|
||||
getEventTypeUrl={getEventTypeUrl}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</AtomsWrapper>
|
||||
);
|
||||
};
|
||||
@@ -49,4 +49,6 @@ export { CreateScheduleForm } from "./create-schedule/CreateScheduleForm";
|
||||
|
||||
export { ListSchedulesPlatformWrapper as ListSchedules } from "./list-schedules/index";
|
||||
|
||||
export { ListEventTypesPlatformWrapper as ListEventTypes } from "./event-types/index";
|
||||
|
||||
export { useAvailableSlots } from "./hooks/useAvailableSlots";
|
||||
|
||||
Reference in New Issue
Block a user