restructure folders
parent
d4156f5dc7
commit
fa796f07d5
|
@ -1,120 +0,0 @@
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
DropdownMenu,
|
|
||||||
DropdownMenuTrigger,
|
|
||||||
DropdownMenuContent,
|
|
||||||
DropdownMenuItem,
|
|
||||||
} from "@/components/ui/dropdown-menu";
|
|
||||||
import { Toaster } from "@/components/ui/toaster";
|
|
||||||
import { useToast } from "@/components/ui/use-toast";
|
|
||||||
import type { Schedule } from "availabilitylist/AvailabilityList";
|
|
||||||
import { Globe, MoreHorizontal, Star, Copy, Trash } from "lucide-react";
|
|
||||||
import { Fragment } from "react";
|
|
||||||
|
|
||||||
import { availabilityAsString } from "@calcom/lib/availability";
|
|
||||||
|
|
||||||
type AvailabilityProps = {
|
|
||||||
schedule: Schedule;
|
|
||||||
isDeletable: boolean;
|
|
||||||
updateDefault: ({ scheduleId, isDefault }: { scheduleId: number; isDefault: boolean }) => void;
|
|
||||||
duplicateFunction: ({ scheduleId }: { scheduleId: number }) => void;
|
|
||||||
deleteFunction: ({ scheduleId }: { scheduleId: number }) => void;
|
|
||||||
displayOptions?: {
|
|
||||||
timeZone?: string;
|
|
||||||
hour12?: boolean;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export function Availability({
|
|
||||||
schedule,
|
|
||||||
isDeletable,
|
|
||||||
displayOptions,
|
|
||||||
updateDefault,
|
|
||||||
duplicateFunction,
|
|
||||||
deleteFunction,
|
|
||||||
}: AvailabilityProps) {
|
|
||||||
const { toast } = useToast();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<li key={schedule.id}>
|
|
||||||
<div className="hover:bg-muted flex items-center justify-between py-5 ltr:pl-4 rtl:pr-4 sm:ltr:pl-0 sm:rtl:pr-0">
|
|
||||||
<div className="group flex w-full items-center justify-between sm:px-6">
|
|
||||||
<a className="flex-grow truncate text-sm" href={`/availability/${schedule.id}`}>
|
|
||||||
<h1>{schedule.name}</h1>
|
|
||||||
<div className="space-x-2 rtl:space-x-reverse">
|
|
||||||
{schedule.isDefault && <Badge className="bg-success text-success text-xs">Default</Badge>}
|
|
||||||
</div>
|
|
||||||
<p className="text-subtle mt-1">
|
|
||||||
{schedule.availability
|
|
||||||
.filter((availability) => !!availability.days.length)
|
|
||||||
.map((availability) => (
|
|
||||||
<Fragment key={availability.id}>
|
|
||||||
{availabilityAsString(availability, {
|
|
||||||
hour12: displayOptions?.hour12,
|
|
||||||
})}
|
|
||||||
<br />
|
|
||||||
</Fragment>
|
|
||||||
))}
|
|
||||||
{(schedule.timeZone || displayOptions?.timeZone) && (
|
|
||||||
<p className="my-1 flex items-center first-letter:text-xs">
|
|
||||||
<Globe className="h-3.5 w-3.5" />
|
|
||||||
{schedule.timeZone ?? displayOptions?.timeZone}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger>
|
|
||||||
<Button type="button" className="mx-5" color="secondary">
|
|
||||||
<MoreHorizontal />
|
|
||||||
</Button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent>
|
|
||||||
{!schedule.isDefault && (
|
|
||||||
<DropdownMenuItem
|
|
||||||
onClick={() => {
|
|
||||||
updateDefault({
|
|
||||||
scheduleId: schedule.id,
|
|
||||||
isDefault: true,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
className="min-w-40 focus:ring-mute min-w-40 focus:ring-muted">
|
|
||||||
<Star />
|
|
||||||
Set as default
|
|
||||||
</DropdownMenuItem>
|
|
||||||
)}
|
|
||||||
<DropdownMenuItem
|
|
||||||
className="outline-none"
|
|
||||||
onClick={() => {
|
|
||||||
duplicateFunction({
|
|
||||||
scheduleId: schedule.id,
|
|
||||||
});
|
|
||||||
}}>
|
|
||||||
<Copy />
|
|
||||||
Duplicate
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem
|
|
||||||
className="min-w-40 focus:ring-muted"
|
|
||||||
onClick={() => {
|
|
||||||
if (!isDeletable) {
|
|
||||||
toast({
|
|
||||||
description: "You are required to have at least one schedule",
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
deleteFunction({
|
|
||||||
scheduleId: schedule.id,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}>
|
|
||||||
<Trash />
|
|
||||||
Delete
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
<Toaster />
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,74 +0,0 @@
|
||||||
import { NewScheduleButton } from "availabilitylist/NewScheduleButton";
|
|
||||||
|
|
||||||
import type { HttpError } from "@calcom/lib/http-error";
|
|
||||||
import { Clock } from "@calcom/ui/components/icon";
|
|
||||||
|
|
||||||
import { Availability } from "./Availability";
|
|
||||||
import { EmptyScreen } from "./EmptyScreen";
|
|
||||||
|
|
||||||
export type Schedule = {
|
|
||||||
isDefault: boolean;
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
availability: {
|
|
||||||
id: number;
|
|
||||||
startTime: Date;
|
|
||||||
endTime: Date;
|
|
||||||
userId?: number;
|
|
||||||
eventTypeId?: number;
|
|
||||||
date?: Date;
|
|
||||||
days: number[];
|
|
||||||
scheduleId?: number;
|
|
||||||
}[];
|
|
||||||
timezone?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
type AvailabilityListProps = {
|
|
||||||
schedules: Schedule[] | [];
|
|
||||||
onCreateMutation: (values: {
|
|
||||||
onSucess: (schedule: Schedule) => void;
|
|
||||||
onError: (err: HttpError) => void;
|
|
||||||
}) => void;
|
|
||||||
updateMutation: ({ scheduleId, isDefault }: { scheduleId: number; isDefault: boolean }) => void;
|
|
||||||
duplicateMutation: ({ scheduleId }: { scheduleId: number }) => void;
|
|
||||||
deleteMutation: ({ scheduleId }: { scheduleId: number }) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function AvailabilityList({
|
|
||||||
schedules,
|
|
||||||
onCreateMutation,
|
|
||||||
updateMutation,
|
|
||||||
duplicateMutation,
|
|
||||||
deleteMutation,
|
|
||||||
}: AvailabilityListProps) {
|
|
||||||
if (schedules.length === 0) {
|
|
||||||
return (
|
|
||||||
<div className="flex justify-center">
|
|
||||||
<EmptyScreen
|
|
||||||
Icon={Clock}
|
|
||||||
headline="Create an availability schedule"
|
|
||||||
subtitle="Creating availability schedules allows you to manage availability across event types. They can be applied to one or more event types."
|
|
||||||
className="w-full"
|
|
||||||
buttonRaw={<NewScheduleButton createMutation={onCreateMutation} />}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="border-subtle bg-default mb-16 overflow-hidden rounded-md border">
|
|
||||||
<ul className="divide-subtle divide-y" data-testid="schedules">
|
|
||||||
{schedules.map((schedule) => (
|
|
||||||
<Availability
|
|
||||||
key={schedule.id}
|
|
||||||
schedule={schedule}
|
|
||||||
isDeletable={schedules.length !== 1}
|
|
||||||
updateDefault={updateMutation}
|
|
||||||
deleteFunction={deleteMutation}
|
|
||||||
duplicateFunction={duplicateMutation}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,70 +0,0 @@
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import type { LucideIcon as IconType } from "lucide-react";
|
|
||||||
import type { ReactNode } from "react";
|
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
import { classNames } from "@calcom/lib";
|
|
||||||
import type { SVGComponent } from "@calcom/types/SVGComponent";
|
|
||||||
|
|
||||||
type EmptyScreenProps = {
|
|
||||||
Icon?: SVGComponent | IconType;
|
|
||||||
avatar?: React.ReactElement;
|
|
||||||
headline: string | React.ReactElement;
|
|
||||||
description?: string | React.ReactElement;
|
|
||||||
buttonText?: string;
|
|
||||||
buttonOnClick?: (event: React.MouseEvent<HTMLElement, MouseEvent>) => void;
|
|
||||||
buttonRaw?: ReactNode; // Used incase you want to provide your own button.
|
|
||||||
border?: boolean;
|
|
||||||
dashedBorder?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function EmptyScreen({
|
|
||||||
Icon,
|
|
||||||
avatar,
|
|
||||||
headline,
|
|
||||||
description,
|
|
||||||
buttonText,
|
|
||||||
buttonOnClick,
|
|
||||||
buttonRaw,
|
|
||||||
border = true,
|
|
||||||
dashedBorder = true,
|
|
||||||
className,
|
|
||||||
}: EmptyScreenProps & React.HTMLAttributes<HTMLDivElement>) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div
|
|
||||||
data-testid="empty-screen"
|
|
||||||
className={classNames(
|
|
||||||
"flex w-full select-none flex-col items-center justify-center rounded-lg p-7 lg:p-20",
|
|
||||||
border && "border-subtle border",
|
|
||||||
dashedBorder && "border-dashed",
|
|
||||||
className
|
|
||||||
)}>
|
|
||||||
{!avatar ? null : (
|
|
||||||
<div className="flex h-[72px] w-[72px] items-center justify-center rounded-full">{avatar}</div>
|
|
||||||
)}
|
|
||||||
{!Icon ? null : (
|
|
||||||
<div className="bg-emphasis flex h-[72px] w-[72px] items-center justify-center rounded-full ">
|
|
||||||
<Icon className="text-default inline-block h-10 w-10 stroke-[1.3px]" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="flex max-w-[420px] flex-col items-center">
|
|
||||||
<h2
|
|
||||||
className={classNames(
|
|
||||||
"text-semibold font-cal text-emphasis text-center text-xl",
|
|
||||||
Icon && "mt-6"
|
|
||||||
)}>
|
|
||||||
{headline}
|
|
||||||
</h2>
|
|
||||||
{description && (
|
|
||||||
<div className="text-default mb-8 mt-3 text-center text-sm font-normal leading-6">
|
|
||||||
{description}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{buttonOnClick && buttonText && <Button onClick={(e) => buttonOnClick(e)}>{buttonText}</Button>}
|
|
||||||
{buttonRaw}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,39 +0,0 @@
|
||||||
import type { ReactElement, Ref } from "react";
|
|
||||||
import React, { forwardRef } from "react";
|
|
||||||
import type { FieldValues, SubmitHandler, UseFormReturn } from "react-hook-form";
|
|
||||||
import { FormProvider } from "react-hook-form";
|
|
||||||
|
|
||||||
type FormProps<T extends object> = { form: UseFormReturn<T>; handleSubmit: SubmitHandler<T> } & Omit<
|
|
||||||
JSX.IntrinsicElements["form"],
|
|
||||||
"onSubmit"
|
|
||||||
>;
|
|
||||||
|
|
||||||
const PlainForm = <T extends FieldValues>(props: FormProps<T>, ref: Ref<HTMLFormElement>) => {
|
|
||||||
const { form, handleSubmit, ...passThrough } = props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<FormProvider {...form}>
|
|
||||||
<form
|
|
||||||
ref={ref}
|
|
||||||
onSubmit={(event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
event.stopPropagation();
|
|
||||||
|
|
||||||
form
|
|
||||||
.handleSubmit(handleSubmit)(event)
|
|
||||||
.catch((err) => {
|
|
||||||
// FIXME: Booking Pages don't have toast, so this error is never shown
|
|
||||||
// showToast(`${getErrorFromUnknown(err).message}`, "error");
|
|
||||||
console.error(`${getErrorFromUnknown(err).message}`, "error");
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
{...passThrough}>
|
|
||||||
{props.children}
|
|
||||||
</form>
|
|
||||||
</FormProvider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Form = forwardRef(PlainForm) as <T extends FieldValues>(
|
|
||||||
p: FormProps<T> & { ref?: Ref<HTMLFormElement> }
|
|
||||||
) => ReactElement;
|
|
|
@ -1,68 +0,0 @@
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogTrigger,
|
|
||||||
DialogContent,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from "@/components/ui/dialog";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Label } from "@/components/ui/label";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
|
|
||||||
import type { HttpError } from "@calcom/lib/http-error";
|
|
||||||
import { Plus } from "@calcom/ui/components/icon";
|
|
||||||
|
|
||||||
import { Button } from "../src/components/ui/button";
|
|
||||||
import { Form } from "./Form";
|
|
||||||
import type { Schedule } from ".prisma/client";
|
|
||||||
|
|
||||||
// create mutation handler to be handled outside the component
|
|
||||||
// then passed in as a prop
|
|
||||||
// TODO: translations can be taken care of later
|
|
||||||
|
|
||||||
type NewScheduleButtonProps = {
|
|
||||||
name?: string;
|
|
||||||
createMutation: (values: {
|
|
||||||
onSucess: (schedule: Schedule) => void;
|
|
||||||
onError: (err: HttpError) => void;
|
|
||||||
}) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function NewScheduleButton({ name = "new-schedule", createMutation }: NewScheduleButtonProps) {
|
|
||||||
const form = useForm<{
|
|
||||||
name: string;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<Dialog>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<Button type="button" data-testid={name}>
|
|
||||||
{Plus}
|
|
||||||
New
|
|
||||||
</Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Add new schedule</DialogTitle>
|
|
||||||
<Form
|
|
||||||
form={form}
|
|
||||||
handleSubmit={(values) => {
|
|
||||||
createMutation(values);
|
|
||||||
}}>
|
|
||||||
<Label htmlFor="working-hours">Name</Label>
|
|
||||||
<Input id="working-hours" placeholder="Working Hours" />
|
|
||||||
<DialogFooter>
|
|
||||||
<Button type="button" variant="outline" className="mr-2 border-none">
|
|
||||||
Close
|
|
||||||
</Button>
|
|
||||||
<Button type="submit">Continue</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</Form>
|
|
||||||
</DialogHeader>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
export { AvailabilityList } from "./AvailabilityList";
|
|
||||||
export { Availability } from "./Availability";
|
|
||||||
export * from "../types";
|
|
Loading…
Reference in New Issue