Skip to content

Commit 2803b50

Browse files
emrysalzomars
andauthored
fix: Remove the right date override on removal (#13988)
Co-authored-by: zomars <zomars@me.com>
1 parent 502a324 commit 2803b50

File tree

11 files changed

+470
-201
lines changed

11 files changed

+470
-201
lines changed

apps/web/pages/availability/[schedule].tsx

+165-112
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import { useRouter } from "next/navigation";
2-
import { useState } from "react";
3-
import { Controller, useFieldArray, useForm } from "react-hook-form";
2+
import { useMemo, useState } from "react";
3+
import { Controller, useFieldArray, useForm, useWatch } from "react-hook-form";
44

55
import dayjs from "@calcom/dayjs";
66
import { DateOverrideInputDialog, DateOverrideList } from "@calcom/features/schedules";
77
import Schedule from "@calcom/features/schedules/components/Schedule";
88
import Shell from "@calcom/features/shell/Shell";
99
import { classNames } from "@calcom/lib";
1010
import { availabilityAsString } from "@calcom/lib/availability";
11+
import { withErrorFromUnknown } from "@calcom/lib/getClientErrorFromUnknown";
1112
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
1213
import { useLocale } from "@calcom/lib/hooks/useLocale";
1314
import { HttpError } from "@calcom/lib/http-error";
@@ -29,25 +30,43 @@ import {
2930
Tooltip,
3031
VerticalDivider,
3132
} from "@calcom/ui";
32-
import { Info, MoreVertical, ArrowLeft, Plus, Trash } from "@calcom/ui/components/icon";
33+
import { ArrowLeft, Info, MoreVertical, Plus, Trash } from "@calcom/ui/components/icon";
3334

3435
import PageWrapper from "@components/PageWrapper";
3536
import { SelectSkeletonLoader } from "@components/availability/SkeletonLoader";
3637
import EditableHeading from "@components/ui/EditableHeading";
3738

38-
type AvailabilityFormValues = {
39+
export type AvailabilityFormValues = {
3940
name: string;
4041
schedule: ScheduleType;
4142
dateOverrides: { ranges: TimeRange[] }[];
4243
timeZone: string;
4344
isDefault: boolean;
4445
};
4546

47+
const useExcludedDates = () => {
48+
const watchValues = useWatch<AvailabilityFormValues>({ name: "dateOverrides" }) as {
49+
ranges: TimeRange[];
50+
}[];
51+
return useMemo(() => {
52+
return watchValues?.map((field) => dayjs(field.ranges[0].start).utc().format("YYYY-MM-DD"));
53+
}, [watchValues]);
54+
};
55+
56+
const useSettings = () => {
57+
const { data } = useMeQuery();
58+
return {
59+
hour12: data?.timeFormat === 12,
60+
timeZone: data?.timeZone,
61+
};
62+
};
63+
4664
const DateOverride = ({ workingHours }: { workingHours: WorkingHours[] }) => {
47-
const { remove, append, replace, fields } = useFieldArray<AvailabilityFormValues, "dateOverrides">({
65+
const { hour12 } = useSettings();
66+
const { append, replace, fields } = useFieldArray<AvailabilityFormValues, "dateOverrides">({
4867
name: "dateOverrides",
4968
});
50-
const excludedDates = fields.map((field) => dayjs(field.ranges[0].start).utc().format("YYYY-MM-DD"));
69+
const excludedDates = useExcludedDates();
5170
const { t } = useLocale();
5271
return (
5372
<div className="p-6">
@@ -62,10 +81,10 @@ const DateOverride = ({ workingHours }: { workingHours: WorkingHours[] }) => {
6281
<p className="text-subtle mb-4 text-sm">{t("date_overrides_subtitle")}</p>
6382
<div className="space-y-2">
6483
<DateOverrideList
65-
excludedDates={excludedDates}
66-
remove={remove}
84+
hour12={hour12}
6785
replace={replace}
68-
items={fields}
86+
fields={fields}
87+
excludedDates={excludedDates}
6988
workingHours={workingHours}
7089
/>
7190
<DateOverrideInputDialog
@@ -83,11 +102,85 @@ const DateOverride = ({ workingHours }: { workingHours: WorkingHours[] }) => {
83102
);
84103
};
85104

105+
const DeleteDialogButton = ({
106+
disabled,
107+
scheduleId,
108+
buttonClassName,
109+
onDeleteConfirmed,
110+
}: {
111+
disabled?: boolean;
112+
onDeleteConfirmed?: () => void;
113+
buttonClassName: string;
114+
scheduleId: number;
115+
}) => {
116+
const { t } = useLocale();
117+
const router = useRouter();
118+
const utils = trpc.useUtils();
119+
const { isPending, mutate } = trpc.viewer.availability.schedule.delete.useMutation({
120+
onError: withErrorFromUnknown((err) => {
121+
showToast(err.message, "error");
122+
}),
123+
onSettled: () => {
124+
utils.viewer.availability.list.invalidate();
125+
},
126+
onSuccess: () => {
127+
showToast(t("schedule_deleted_successfully"), "success");
128+
router.push("/availability");
129+
},
130+
});
131+
132+
return (
133+
<Dialog>
134+
<DialogTrigger asChild>
135+
<Button
136+
StartIcon={Trash}
137+
variant="icon"
138+
color="destructive"
139+
aria-label={t("delete")}
140+
className={buttonClassName}
141+
disabled={disabled}
142+
tooltip={disabled ? t("requires_at_least_one_schedule") : t("delete")}
143+
/>
144+
</DialogTrigger>
145+
<ConfirmationDialogContent
146+
isPending={isPending}
147+
variety="danger"
148+
title={t("delete_schedule")}
149+
confirmBtnText={t("delete")}
150+
loadingText={t("delete")}
151+
onConfirm={() => {
152+
scheduleId && mutate({ scheduleId });
153+
onDeleteConfirmed?.();
154+
}}>
155+
{t("delete_schedule_description")}
156+
</ConfirmationDialogContent>
157+
</Dialog>
158+
);
159+
};
160+
161+
// Simplify logic by assuming this will never be opened on a large screen
162+
const SmallScreenSideBar = ({ open, children }: { open: boolean; children: JSX.Element }) => {
163+
return (
164+
<div
165+
className={classNames(
166+
open
167+
? "fadeIn fixed inset-0 z-50 bg-neutral-800 bg-opacity-70 transition-opacity dark:bg-opacity-70 sm:hidden"
168+
: ""
169+
)}>
170+
<div
171+
className={classNames(
172+
"bg-default fixed right-0 z-20 flex h-screen w-80 flex-col space-y-2 overflow-x-hidden rounded-md px-2 pb-3 transition-transform",
173+
open ? "translate-x-0 opacity-100" : "translate-x-full opacity-0"
174+
)}>
175+
{open ? children : null}
176+
</div>
177+
</div>
178+
);
179+
};
86180
export default function Availability() {
87181
const searchParams = useCompatSearchParams();
88182
const { t, i18n } = useLocale();
89-
const router = useRouter();
90-
const utils = trpc.useContext();
183+
const utils = trpc.useUtils();
91184
const me = useMeQuery();
92185
const scheduleId = searchParams?.get("schedule") ? Number(searchParams.get("schedule")) : -1;
93186
const fromEventType = searchParams?.get("fromEventType");
@@ -133,22 +226,6 @@ export default function Availability() {
133226
},
134227
});
135228

136-
const deleteMutation = trpc.viewer.availability.schedule.delete.useMutation({
137-
onError: (err) => {
138-
if (err instanceof HttpError) {
139-
const message = `${err.statusCode}: ${err.message}`;
140-
showToast(message, "error");
141-
}
142-
},
143-
onSettled: () => {
144-
utils.viewer.availability.list.invalidate();
145-
},
146-
onSuccess: () => {
147-
showToast(t("schedule_deleted_successfully"), "success");
148-
router.push("/availability");
149-
},
150-
});
151-
152229
return (
153230
<Shell
154231
backPath={fromEventType ? true : "/availability"}
@@ -179,88 +256,58 @@ export default function Availability() {
179256
CTA={
180257
<div className="flex items-center justify-end">
181258
<div className="sm:hover:bg-muted hidden items-center rounded-md px-2 sm:flex">
182-
<Skeleton
183-
as={Label}
184-
htmlFor="hiddenSwitch"
185-
className="mt-2 cursor-pointer self-center pe-2"
186-
loadingClassName="me-4">
187-
{t("set_to_default")}
188-
</Skeleton>
189-
<Switch
190-
id="hiddenSwitch"
191-
disabled={isPending || schedule?.isDefault}
192-
checked={form.watch("isDefault")}
193-
onCheckedChange={(e) => {
194-
form.setValue("isDefault", e);
195-
}}
196-
/>
259+
{!openSidebar ? (
260+
<>
261+
<Skeleton
262+
as={Label}
263+
htmlFor="hiddenSwitch"
264+
className="mt-2 cursor-pointer self-center pe-2"
265+
loadingClassName="me-4">
266+
{t("set_to_default")}
267+
</Skeleton>
268+
<Controller
269+
control={form.control}
270+
name="isDefault"
271+
render={({ field: { value, onChange } }) => (
272+
<Switch
273+
id="hiddenSwitch"
274+
disabled={isPending || schedule?.isDefault}
275+
checked={value}
276+
onCheckedChange={onChange}
277+
/>
278+
)}
279+
/>
280+
</>
281+
) : null}
197282
</div>
198283

199284
<VerticalDivider className="hidden sm:inline" />
200-
<Dialog>
201-
<DialogTrigger asChild>
202-
<Button
203-
StartIcon={Trash}
204-
variant="icon"
205-
color="destructive"
206-
aria-label={t("delete")}
207-
className="hidden sm:inline"
208-
disabled={schedule?.isLastSchedule}
209-
tooltip={schedule?.isLastSchedule ? t("requires_at_least_one_schedule") : t("delete")}
210-
/>
211-
</DialogTrigger>
212-
<ConfirmationDialogContent
213-
isPending={deleteMutation.isPending}
214-
variety="danger"
215-
title={t("delete_schedule")}
216-
confirmBtnText={t("delete")}
217-
loadingText={t("delete")}
218-
onConfirm={() => {
219-
scheduleId && deleteMutation.mutate({ scheduleId });
220-
}}>
221-
{t("delete_schedule_description")}
222-
</ConfirmationDialogContent>
223-
</Dialog>
285+
<DeleteDialogButton
286+
buttonClassName="hidden sm:inline"
287+
scheduleId={scheduleId}
288+
disabled={schedule?.isLastSchedule}
289+
/>
224290
<VerticalDivider className="hidden sm:inline" />
225-
<div
226-
className={classNames(
227-
openSidebar
228-
? "fadeIn fixed inset-0 z-50 bg-neutral-800 bg-opacity-70 transition-opacity dark:bg-opacity-70 sm:hidden"
229-
: ""
230-
)}>
231-
<div
232-
className={classNames(
233-
"bg-default fixed right-0 z-20 flex h-screen w-80 flex-col space-y-2 overflow-x-hidden rounded-md px-2 pb-3 transition-transform",
234-
openSidebar ? "translate-x-0 opacity-100" : "translate-x-full opacity-0"
235-
)}>
291+
292+
<SmallScreenSideBar open={openSidebar}>
293+
<>
236294
<div className="flex flex-row items-center pt-5">
237-
<Button StartIcon={ArrowLeft} color="minimal" onClick={() => setOpenSidebar(false)} />
295+
<Button
296+
StartIcon={ArrowLeft}
297+
color="minimal"
298+
onClick={() => {
299+
setOpenSidebar(false);
300+
}}
301+
/>
238302
<p className="-ml-2">{t("availability_settings")}</p>
239-
<Dialog>
240-
<DialogTrigger asChild>
241-
<Button
242-
StartIcon={Trash}
243-
variant="icon"
244-
color="destructive"
245-
aria-label={t("delete")}
246-
className="ml-16 inline"
247-
disabled={schedule?.isLastSchedule}
248-
tooltip={schedule?.isLastSchedule ? t("requires_at_least_one_schedule") : t("delete")}
249-
/>
250-
</DialogTrigger>
251-
<ConfirmationDialogContent
252-
isPending={deleteMutation.isPending}
253-
variety="danger"
254-
title={t("delete_schedule")}
255-
confirmBtnText={t("delete")}
256-
loadingText={t("delete")}
257-
onConfirm={() => {
258-
scheduleId && deleteMutation.mutate({ scheduleId });
259-
setOpenSidebar(false);
260-
}}>
261-
{t("delete_schedule_description")}
262-
</ConfirmationDialogContent>
263-
</Dialog>
303+
<DeleteDialogButton
304+
buttonClassName="ml-16 inline"
305+
scheduleId={scheduleId}
306+
disabled={schedule?.isLastSchedule}
307+
onDeleteConfirmed={() => {
308+
setOpenSidebar(false);
309+
}}
310+
/>
264311
</div>
265312
<div className="flex flex-col px-2 py-2">
266313
<Skeleton as={Label}>{t("name")}</Skeleton>
@@ -275,20 +322,25 @@ export default function Availability() {
275322
)}
276323
/>
277324
</div>
325+
278326
<div className="flex h-9 flex-row-reverse items-center justify-end gap-3 px-2">
279327
<Skeleton
280328
as={Label}
281329
htmlFor="hiddenSwitch"
282330
className="mt-2 cursor-pointer self-center pr-2 sm:inline">
283331
{t("set_to_default")}
284332
</Skeleton>
285-
<Switch
286-
id="hiddenSwitch"
287-
disabled={isPending || schedule?.isDefault}
288-
checked={form.watch("isDefault")}
289-
onCheckedChange={(e) => {
290-
form.setValue("isDefault", e);
291-
}}
333+
<Controller
334+
control={form.control}
335+
name="isDefault"
336+
render={({ field: { value, onChange } }) => (
337+
<Switch
338+
id="hiddenSwitch"
339+
disabled={isPending || value}
340+
checked={value}
341+
onCheckedChange={onChange}
342+
/>
343+
)}
292344
/>
293345
</div>
294346

@@ -331,8 +383,9 @@ export default function Availability() {
331383
</div>
332384
</div>
333385
</div>
334-
</div>
335-
</div>
386+
</>
387+
</SmallScreenSideBar>
388+
336389
<div className="border-default border-l-2" />
337390
<Button
338391
className="ml-4 lg:ml-0"

0 commit comments

Comments
 (0)