fix: Remove the right date override on removal (#13988)
Co-authored-by: zomars <zomars@me.com>
This commit is contained in:
1 parent
502a3241e4
commit
2803b5072c
11 files changed
+470
-201
No files matched your search
@@ -1,6 +1,6 @@
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
||||
import { useMemo, useState } from "react";
|
||||
import { Controller, useFieldArray, useForm, useWatch } from "react-hook-form";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { DateOverrideInputDialog, DateOverrideList } from "@calcom/features/schedules";
|
||||
@@ -8,6 +8,7 @@ import Schedule from "@calcom/features/schedules/components/Schedule";
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { availabilityAsString } from "@calcom/lib/availability";
|
||||
import { withErrorFromUnknown } from "@calcom/lib/getClientErrorFromUnknown";
|
||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
@@ -29,13 +30,13 @@ import {
|
||||
Tooltip,
|
||||
VerticalDivider,
|
||||
} from "@calcom/ui";
|
||||
import { Info, MoreVertical, ArrowLeft, Plus, Trash } from "@calcom/ui/components/icon";
|
||||
import { ArrowLeft, Info, MoreVertical, Plus, Trash } from "@calcom/ui/components/icon";
|
||||
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
import { SelectSkeletonLoader } from "@components/availability/SkeletonLoader";
|
||||
import EditableHeading from "@components/ui/EditableHeading";
|
||||
|
||||
type AvailabilityFormValues = {
|
||||
export type AvailabilityFormValues = {
|
||||
name: string;
|
||||
schedule: ScheduleType;
|
||||
dateOverrides: { ranges: TimeRange[] }[];
|
||||
@@ -43,11 +44,29 @@ type AvailabilityFormValues = {
|
||||
isDefault: boolean;
|
||||
};
|
||||
|
||||
const useExcludedDates = () => {
|
||||
const watchValues = useWatch<AvailabilityFormValues>({ name: "dateOverrides" }) as {
|
||||
ranges: TimeRange[];
|
||||
}[];
|
||||
return useMemo(() => {
|
||||
return watchValues?.map((field) => dayjs(field.ranges[0].start).utc().format("YYYY-MM-DD"));
|
||||
}, [watchValues]);
|
||||
};
|
||||
|
||||
const useSettings = () => {
|
||||
const { data } = useMeQuery();
|
||||
return {
|
||||
hour12: data?.timeFormat === 12,
|
||||
timeZone: data?.timeZone,
|
||||
};
|
||||
};
|
||||
|
||||
const DateOverride = ({ workingHours }: { workingHours: WorkingHours[] }) => {
|
||||
const { remove, append, replace, fields } = useFieldArray<AvailabilityFormValues, "dateOverrides">({
|
||||
const { hour12 } = useSettings();
|
||||
const { append, replace, fields } = useFieldArray<AvailabilityFormValues, "dateOverrides">({
|
||||
name: "dateOverrides",
|
||||
});
|
||||
const excludedDates = fields.map((field) => dayjs(field.ranges[0].start).utc().format("YYYY-MM-DD"));
|
||||
const excludedDates = useExcludedDates();
|
||||
const { t } = useLocale();
|
||||
return (
|
||||
<div className="p-6">
|
||||
@@ -62,10 +81,10 @@ const DateOverride = ({ workingHours }: { workingHours: WorkingHours[] }) => {
|
||||
<p className="text-subtle mb-4 text-sm">{t("date_overrides_subtitle")}</p>
|
||||
<div className="space-y-2">
|
||||
<DateOverrideList
|
||||
excludedDates={excludedDates}
|
||||
remove={remove}
|
||||
hour12={hour12}
|
||||
replace={replace}
|
||||
items={fields}
|
||||
fields={fields}
|
||||
excludedDates={excludedDates}
|
||||
workingHours={workingHours}
|
||||
/>
|
||||
<DateOverrideInputDialog
|
||||
@@ -83,11 +102,85 @@ const DateOverride = ({ workingHours }: { workingHours: WorkingHours[] }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const DeleteDialogButton = ({
|
||||
disabled,
|
||||
scheduleId,
|
||||
buttonClassName,
|
||||
onDeleteConfirmed,
|
||||
}: {
|
||||
disabled?: boolean;
|
||||
onDeleteConfirmed?: () => void;
|
||||
buttonClassName: string;
|
||||
scheduleId: number;
|
||||
}) => {
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
const utils = trpc.useUtils();
|
||||
const { isPending, mutate } = trpc.viewer.availability.schedule.delete.useMutation({
|
||||
onError: withErrorFromUnknown((err) => {
|
||||
showToast(err.message, "error");
|
||||
}),
|
||||
onSettled: () => {
|
||||
utils.viewer.availability.list.invalidate();
|
||||
},
|
||||
onSuccess: () => {
|
||||
showToast(t("schedule_deleted_successfully"), "success");
|
||||
router.push("/availability");
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
StartIcon={Trash}
|
||||
variant="icon"
|
||||
color="destructive"
|
||||
aria-label={t("delete")}
|
||||
className={buttonClassName}
|
||||
disabled={disabled}
|
||||
tooltip={disabled ? t("requires_at_least_one_schedule") : t("delete")}
|
||||
/>
|
||||
</DialogTrigger>
|
||||
<ConfirmationDialogContent
|
||||
isPending={isPending}
|
||||
variety="danger"
|
||||
title={t("delete_schedule")}
|
||||
confirmBtnText={t("delete")}
|
||||
loadingText={t("delete")}
|
||||
onConfirm={() => {
|
||||
scheduleId && mutate({ scheduleId });
|
||||
onDeleteConfirmed?.();
|
||||
}}>
|
||||
{t("delete_schedule_description")}
|
||||
</ConfirmationDialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
// Simplify logic by assuming this will never be opened on a large screen
|
||||
const SmallScreenSideBar = ({ open, children }: { open: boolean; children: JSX.Element }) => {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
open
|
||||
? "fadeIn fixed inset-0 z-50 bg-neutral-800 bg-opacity-70 transition-opacity dark:bg-opacity-70 sm:hidden"
|
||||
: ""
|
||||
)}>
|
||||
<div
|
||||
className={classNames(
|
||||
"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",
|
||||
open ? "translate-x-0 opacity-100" : "translate-x-full opacity-0"
|
||||
)}>
|
||||
{open ? children : null}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default function Availability() {
|
||||
const searchParams = useCompatSearchParams();
|
||||
const { t, i18n } = useLocale();
|
||||
const router = useRouter();
|
||||
const utils = trpc.useContext();
|
||||
const utils = trpc.useUtils();
|
||||
const me = useMeQuery();
|
||||
const scheduleId = searchParams?.get("schedule") ? Number(searchParams.get("schedule")) : -1;
|
||||
const fromEventType = searchParams?.get("fromEventType");
|
||||
@@ -133,22 +226,6 @@ export default function Availability() {
|
||||
},
|
||||
});
|
||||
|
||||
const deleteMutation = trpc.viewer.availability.schedule.delete.useMutation({
|
||||
onError: (err) => {
|
||||
if (err instanceof HttpError) {
|
||||
const message = `${err.statusCode}: ${err.message}`;
|
||||
showToast(message, "error");
|
||||
}
|
||||
},
|
||||
onSettled: () => {
|
||||
utils.viewer.availability.list.invalidate();
|
||||
},
|
||||
onSuccess: () => {
|
||||
showToast(t("schedule_deleted_successfully"), "success");
|
||||
router.push("/availability");
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Shell
|
||||
backPath={fromEventType ? true : "/availability"}
|
||||
@@ -179,88 +256,58 @@ export default function Availability() {
|
||||
CTA={
|
||||
<div className="flex items-center justify-end">
|
||||
<div className="sm:hover:bg-muted hidden items-center rounded-md px-2 sm:flex">
|
||||
<Skeleton
|
||||
as={Label}
|
||||
htmlFor="hiddenSwitch"
|
||||
className="mt-2 cursor-pointer self-center pe-2"
|
||||
loadingClassName="me-4">
|
||||
{t("set_to_default")}
|
||||
</Skeleton>
|
||||
<Switch
|
||||
id="hiddenSwitch"
|
||||
disabled={isPending || schedule?.isDefault}
|
||||
checked={form.watch("isDefault")}
|
||||
onCheckedChange={(e) => {
|
||||
form.setValue("isDefault", e);
|
||||
}}
|
||||
/>
|
||||
{!openSidebar ? (
|
||||
<>
|
||||
<Skeleton
|
||||
as={Label}
|
||||
htmlFor="hiddenSwitch"
|
||||
className="mt-2 cursor-pointer self-center pe-2"
|
||||
loadingClassName="me-4">
|
||||
{t("set_to_default")}
|
||||
</Skeleton>
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="isDefault"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<Switch
|
||||
id="hiddenSwitch"
|
||||
disabled={isPending || schedule?.isDefault}
|
||||
checked={value}
|
||||
onCheckedChange={onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<VerticalDivider className="hidden sm:inline" />
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
StartIcon={Trash}
|
||||
variant="icon"
|
||||
color="destructive"
|
||||
aria-label={t("delete")}
|
||||
className="hidden sm:inline"
|
||||
disabled={schedule?.isLastSchedule}
|
||||
tooltip={schedule?.isLastSchedule ? t("requires_at_least_one_schedule") : t("delete")}
|
||||
/>
|
||||
</DialogTrigger>
|
||||
<ConfirmationDialogContent
|
||||
isPending={deleteMutation.isPending}
|
||||
variety="danger"
|
||||
title={t("delete_schedule")}
|
||||
confirmBtnText={t("delete")}
|
||||
loadingText={t("delete")}
|
||||
onConfirm={() => {
|
||||
scheduleId && deleteMutation.mutate({ scheduleId });
|
||||
}}>
|
||||
{t("delete_schedule_description")}
|
||||
</ConfirmationDialogContent>
|
||||
</Dialog>
|
||||
<DeleteDialogButton
|
||||
buttonClassName="hidden sm:inline"
|
||||
scheduleId={scheduleId}
|
||||
disabled={schedule?.isLastSchedule}
|
||||
/>
|
||||
<VerticalDivider className="hidden sm:inline" />
|
||||
<div
|
||||
className={classNames(
|
||||
openSidebar
|
||||
? "fadeIn fixed inset-0 z-50 bg-neutral-800 bg-opacity-70 transition-opacity dark:bg-opacity-70 sm:hidden"
|
||||
: ""
|
||||
)}>
|
||||
<div
|
||||
className={classNames(
|
||||
"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",
|
||||
openSidebar ? "translate-x-0 opacity-100" : "translate-x-full opacity-0"
|
||||
)}>
|
||||
|
||||
<SmallScreenSideBar open={openSidebar}>
|
||||
<>
|
||||
<div className="flex flex-row items-center pt-5">
|
||||
<Button StartIcon={ArrowLeft} color="minimal" onClick={() => setOpenSidebar(false)} />
|
||||
<Button
|
||||
StartIcon={ArrowLeft}
|
||||
color="minimal"
|
||||
onClick={() => {
|
||||
setOpenSidebar(false);
|
||||
}}
|
||||
/>
|
||||
<p className="-ml-2">{t("availability_settings")}</p>
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
StartIcon={Trash}
|
||||
variant="icon"
|
||||
color="destructive"
|
||||
aria-label={t("delete")}
|
||||
className="ml-16 inline"
|
||||
disabled={schedule?.isLastSchedule}
|
||||
tooltip={schedule?.isLastSchedule ? t("requires_at_least_one_schedule") : t("delete")}
|
||||
/>
|
||||
</DialogTrigger>
|
||||
<ConfirmationDialogContent
|
||||
isPending={deleteMutation.isPending}
|
||||
variety="danger"
|
||||
title={t("delete_schedule")}
|
||||
confirmBtnText={t("delete")}
|
||||
loadingText={t("delete")}
|
||||
onConfirm={() => {
|
||||
scheduleId && deleteMutation.mutate({ scheduleId });
|
||||
setOpenSidebar(false);
|
||||
}}>
|
||||
{t("delete_schedule_description")}
|
||||
</ConfirmationDialogContent>
|
||||
</Dialog>
|
||||
<DeleteDialogButton
|
||||
buttonClassName="ml-16 inline"
|
||||
scheduleId={scheduleId}
|
||||
disabled={schedule?.isLastSchedule}
|
||||
onDeleteConfirmed={() => {
|
||||
setOpenSidebar(false);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col px-2 py-2">
|
||||
<Skeleton as={Label}>{t("name")}</Skeleton>
|
||||
@@ -275,6 +322,7 @@ export default function Availability() {
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex h-9 flex-row-reverse items-center justify-end gap-3 px-2">
|
||||
<Skeleton
|
||||
as={Label}
|
||||
@@ -282,13 +330,17 @@ export default function Availability() {
|
||||
className="mt-2 cursor-pointer self-center pr-2 sm:inline">
|
||||
{t("set_to_default")}
|
||||
</Skeleton>
|
||||
<Switch
|
||||
id="hiddenSwitch"
|
||||
disabled={isPending || schedule?.isDefault}
|
||||
checked={form.watch("isDefault")}
|
||||
onCheckedChange={(e) => {
|
||||
form.setValue("isDefault", e);
|
||||
}}
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="isDefault"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<Switch
|
||||
id="hiddenSwitch"
|
||||
disabled={isPending || value}
|
||||
checked={value}
|
||||
onCheckedChange={onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -331,8 +383,9 @@ export default function Availability() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</SmallScreenSideBar>
|
||||
|
||||
<div className="border-default border-l-2" />
|
||||
<Button
|
||||
className="ml-4 lg:ml-0"
|
||||
|
||||
@@ -7,7 +7,7 @@ import { localize } from "./lib/testUtils";
|
||||
|
||||
test.describe.configure({ mode: "parallel" });
|
||||
|
||||
test.describe("Availablity tests", () => {
|
||||
test.describe("Availablity", () => {
|
||||
test.beforeEach(async ({ page, users }) => {
|
||||
const user = await users.create();
|
||||
await user.apiLogin();
|
||||
@@ -40,6 +40,51 @@ test.describe("Availablity tests", () => {
|
||||
await expect(page.locator('[data-testid="troubleshooter-busy-time"]')).toHaveCount(1);
|
||||
});
|
||||
|
||||
test("it can delete date overrides", async ({ page }) => {
|
||||
await page.getByTestId("schedules").first().click();
|
||||
await page.getByTestId("add-override").click();
|
||||
await page.locator('[id="modal-title"]').waitFor();
|
||||
// always go to the next month so there's enough slots regardless of current time.
|
||||
await page.getByTestId("incrementMonth").click();
|
||||
|
||||
await page.locator('[data-testid="day"][data-disabled="false"]').first().click();
|
||||
await page.locator('[data-testid="day"][data-disabled="false"]').nth(4).click();
|
||||
await page.locator('[data-testid="day"][data-disabled="false"]').nth(12).click();
|
||||
await page.getByTestId("date-override-mark-unavailable").click();
|
||||
await page.getByTestId("add-override-submit-btn").click();
|
||||
await page.getByTestId("dialog-rejection").click();
|
||||
await expect(page.locator('[data-testid="date-overrides-list"] > li')).toHaveCount(3);
|
||||
await page.locator('[form="availability-form"][type="submit"]').click();
|
||||
|
||||
await page.getByTestId("add-override").click();
|
||||
await page.locator('[id="modal-title"]').waitFor();
|
||||
|
||||
// always go to the next month so there's enough slots regardless of current time.
|
||||
await page.getByTestId("incrementMonth").click();
|
||||
|
||||
await page.locator('[data-testid="day"][data-disabled="false"]').nth(2).click();
|
||||
await page.getByTestId("date-override-mark-unavailable").click();
|
||||
await page.getByTestId("add-override-submit-btn").click();
|
||||
await page.getByTestId("dialog-rejection").click();
|
||||
|
||||
const dateOverrideList = page.locator('[data-testid="date-overrides-list"] > li');
|
||||
|
||||
await expect(dateOverrideList).toHaveCount(4);
|
||||
|
||||
await page.locator('[form="availability-form"][type="submit"]').click();
|
||||
|
||||
const deleteButton = dateOverrideList.nth(1).getByTestId("delete-button");
|
||||
// we cannot easily predict the title, as this changes throughout the year.
|
||||
const deleteButtonTitle = (await deleteButton.getAttribute("title")) as string;
|
||||
// press the delete button (should remove the .nth 1 element & trigger reorder)
|
||||
await deleteButton.click();
|
||||
|
||||
await page.locator('[form="availability-form"][type="submit"]').click();
|
||||
|
||||
await expect(dateOverrideList).toHaveCount(3);
|
||||
await expect(await page.getByTitle(deleteButtonTitle).isVisible()).toBe(false);
|
||||
});
|
||||
|
||||
test("Schedule listing", async ({ page }) => {
|
||||
await test.step("Can add a new schedule", async () => {
|
||||
await page.getByTestId("new-schedule").click();
|
||||
|
||||
@@ -1681,6 +1681,7 @@
|
||||
"meeting_url_variable": "Meeting url",
|
||||
"meeting_url_info": "The event meeting conference url",
|
||||
"date_overrides": "Date overrides",
|
||||
"date_overrides_delete_on_date": "Delete date overrides on {{date}}",
|
||||
"date_overrides_subtitle": "Add dates when your availability changes from your daily hours.",
|
||||
"date_overrides_info": "Date overrides are archived automatically after the date has passed",
|
||||
"date_overrides_dialog_which_hours": "Which hours are you free?",
|
||||
|
||||
@@ -93,9 +93,11 @@
|
||||
"jsdom": "^22.0.0",
|
||||
"lint-staged": "^12.5.0",
|
||||
"mailhog": "^4.16.0",
|
||||
"next-router-mock": "^0.9.12",
|
||||
"node-ical": "^0.16.1",
|
||||
"prettier": "^2.8.6",
|
||||
"prismock": "^1.21.1",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
"tsc-absolute": "^1.0.0",
|
||||
"typescript": "^4.9.4",
|
||||
"vitest": "^0.34.6",
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
import { TooltipProvider } from "@radix-ui/react-tooltip";
|
||||
import { render, renderHook, screen } from "@testing-library/react";
|
||||
import { useFieldArray, useForm, FormProvider } from "react-hook-form";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import type { TimeRange } from "@calcom/types/schedule";
|
||||
|
||||
import DateOverrideList from "./DateOverrideList";
|
||||
|
||||
type FormValues = {
|
||||
dateOverrides: { ranges: TimeRange[] }[];
|
||||
};
|
||||
|
||||
vi.mock("next/navigation", () => ({
|
||||
...require("next-router-mock"),
|
||||
useSearchParams: () => vi.fn(),
|
||||
usePathname: () => "",
|
||||
useParams: () => ({}),
|
||||
ReadonlyURLSearchParams: URLSearchParams,
|
||||
}));
|
||||
|
||||
describe("DateOverrideList", () => {
|
||||
it("renders DateOverrideList component in military time", () => {
|
||||
const { result } = renderHook(() =>
|
||||
useForm<FormValues>({
|
||||
values: {
|
||||
dateOverrides: [
|
||||
{
|
||||
ranges: [
|
||||
{
|
||||
start: dayjs.utc().startOf("day").add(540, "minute").toDate(),
|
||||
end: dayjs.utc().startOf("day").add(1020, "minute").toDate(),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
);
|
||||
const methods = result.current;
|
||||
const { control } = methods;
|
||||
|
||||
const { result: fieldArrayResult } = renderHook(() => useFieldArray({ control, name: "dateOverrides" }));
|
||||
render(
|
||||
<TooltipProvider>
|
||||
<FormProvider {...methods}>
|
||||
<DateOverrideList
|
||||
hour12={false}
|
||||
fields={fieldArrayResult.current.fields}
|
||||
replace={fieldArrayResult.current.replace}
|
||||
workingHours={[]}
|
||||
/>
|
||||
</FormProvider>
|
||||
</TooltipProvider>
|
||||
);
|
||||
|
||||
const dateString = new Intl.DateTimeFormat("en", {
|
||||
weekday: "long",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
timeZone: "UTC",
|
||||
}).format(new Date());
|
||||
|
||||
expect(screen.getByText(dateString)).toBeInTheDocument();
|
||||
expect(screen.getByText(`09:00 - 17:00`)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders DateOverrideList component in AM/PM", () => {
|
||||
const { result } = renderHook(() =>
|
||||
useForm<FormValues>({
|
||||
values: {
|
||||
dateOverrides: [
|
||||
{
|
||||
ranges: [
|
||||
{
|
||||
start: dayjs.utc().startOf("day").add(540, "minute").toDate(),
|
||||
end: dayjs.utc().startOf("day").add(1020, "minute").toDate(),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
);
|
||||
const methods = result.current;
|
||||
const { control } = methods;
|
||||
|
||||
const { result: fieldArrayResult } = renderHook(() => useFieldArray({ control, name: "dateOverrides" }));
|
||||
render(
|
||||
<TooltipProvider>
|
||||
<FormProvider {...methods}>
|
||||
<DateOverrideList
|
||||
hour12={true}
|
||||
fields={fieldArrayResult.current.fields}
|
||||
replace={fieldArrayResult.current.replace}
|
||||
workingHours={[]}
|
||||
/>
|
||||
</FormProvider>
|
||||
</TooltipProvider>
|
||||
);
|
||||
|
||||
const dateString = new Intl.DateTimeFormat("en", {
|
||||
weekday: "long",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
timeZone: "UTC",
|
||||
}).format(new Date());
|
||||
|
||||
expect(screen.getByText(dateString)).toBeInTheDocument();
|
||||
expect(screen.getByText(`9:00 AM - 5:00 PM`)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,4 @@
|
||||
import type { UseFieldArrayRemove } from "react-hook-form";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
|
||||
import type { TimeRange, WorkingHours } from "@calcom/types/schedule";
|
||||
import { Button, DialogTrigger, Tooltip } from "@calcom/ui";
|
||||
import { Edit2, Trash2 } from "@calcom/ui/components/icon";
|
||||
@@ -12,37 +9,29 @@ const sortByDate = (a: { ranges: TimeRange[]; id: string }, b: { ranges: TimeRan
|
||||
return a.ranges[0].start > b.ranges[0].start ? 1 : -1;
|
||||
};
|
||||
|
||||
const useSettings = () => {
|
||||
const { data } = useMeQuery();
|
||||
return {
|
||||
hour12: data?.timeFormat === 12,
|
||||
timeZone: data?.timeZone,
|
||||
};
|
||||
};
|
||||
|
||||
// I would like this to be decoupled, but RHF really doesn't support this.
|
||||
const DateOverrideList = ({
|
||||
items,
|
||||
remove,
|
||||
replace,
|
||||
workingHours,
|
||||
excludedDates = [],
|
||||
hour12,
|
||||
replace,
|
||||
fields,
|
||||
}: {
|
||||
remove: UseFieldArrayRemove;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
replace: any;
|
||||
items: { ranges: TimeRange[]; id: string }[];
|
||||
fields: { ranges: TimeRange[]; id: string }[];
|
||||
workingHours: WorkingHours[];
|
||||
excludedDates?: string[];
|
||||
hour12: boolean;
|
||||
}) => {
|
||||
const { t, i18n } = useLocale();
|
||||
const { hour12 } = useSettings();
|
||||
|
||||
const unsortedFieldArrayMap = items.reduce(
|
||||
const unsortedFieldArrayMap = fields.reduce(
|
||||
(map: { [id: string]: number }, { id }, index) => ({ ...map, [id]: index }),
|
||||
{}
|
||||
);
|
||||
|
||||
if (!items.length) {
|
||||
if (!fields.length) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
@@ -56,12 +45,12 @@ const DateOverrideList = ({
|
||||
|
||||
return (
|
||||
<ul className="border-subtle rounded border" data-testid="date-overrides-list">
|
||||
{items.sort(sortByDate).map((item) => (
|
||||
{fields.sort(sortByDate).map((item) => (
|
||||
<li key={item.id} className="border-subtle flex justify-between border-b px-5 py-4 last:border-b-0">
|
||||
<div>
|
||||
<h3 className="text-emphasis text-sm">
|
||||
{new Intl.DateTimeFormat("en-GB", {
|
||||
weekday: "short",
|
||||
{new Intl.DateTimeFormat(i18n.language, {
|
||||
weekday: "long",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
timeZone: "UTC",
|
||||
@@ -84,7 +73,7 @@ const DateOverrideList = ({
|
||||
value={item.ranges}
|
||||
onChange={(ranges) => {
|
||||
// update has very weird side-effects with sorting.
|
||||
replace([...items.filter((currentItem) => currentItem.id !== item.id), { ranges }]);
|
||||
replace([...fields.filter((currentItem) => currentItem.id !== item.id), { ranges }]);
|
||||
delete unsortedFieldArrayMap[item.id];
|
||||
}}
|
||||
Trigger={
|
||||
@@ -102,10 +91,21 @@ const DateOverrideList = ({
|
||||
<Tooltip content="Delete">
|
||||
<Button
|
||||
className="text-default"
|
||||
data-testid="delete-button"
|
||||
title={t("date_overrides_delete_on_date", {
|
||||
date: new Intl.DateTimeFormat(i18n.language, {
|
||||
weekday: "long",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
timeZone: "UTC",
|
||||
}).format(item.ranges[0].start),
|
||||
})}
|
||||
color="destructive"
|
||||
variant="icon"
|
||||
StartIcon={Trash2}
|
||||
onClick={() => remove(unsortedFieldArrayMap[item.id])}
|
||||
onClick={() => {
|
||||
replace([...fields.filter((currentItem) => currentItem.id !== item.id)]);
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,7 @@ import Schedule from "@calcom/features/schedules/components/Schedule";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
|
||||
import type { Schedule as ScheduleType, TimeRange, WorkingHours } from "@calcom/types/schedule";
|
||||
import {
|
||||
Button,
|
||||
@@ -37,8 +38,18 @@ type AvailabilityFormValues = {
|
||||
isDefault: boolean;
|
||||
};
|
||||
|
||||
const useSettings = () => {
|
||||
const { data } = useMeQuery();
|
||||
return {
|
||||
hour12: data?.timeFormat === 12,
|
||||
timeZone: data?.timeZone,
|
||||
};
|
||||
};
|
||||
|
||||
const DateOverride = ({ workingHours, disabled }: { workingHours: WorkingHours[]; disabled?: boolean }) => {
|
||||
const { remove, append, replace, fields } = useFieldArray<AvailabilityFormValues, "dateOverrides">({
|
||||
const { hour12 } = useSettings();
|
||||
|
||||
const { append, replace, fields } = useFieldArray<AvailabilityFormValues, "dateOverrides">({
|
||||
name: "dateOverrides",
|
||||
});
|
||||
const excludedDates = fields.map((field) => dayjs(field.ranges[0].start).utc().format("YYYY-MM-DD"));
|
||||
@@ -49,9 +60,9 @@ const DateOverride = ({ workingHours, disabled }: { workingHours: WorkingHours[]
|
||||
<div className="space-y-2">
|
||||
<DateOverrideList
|
||||
excludedDates={excludedDates}
|
||||
remove={remove}
|
||||
replace={replace}
|
||||
items={fields}
|
||||
fields={fields}
|
||||
hour12={hour12}
|
||||
workingHours={workingHours}
|
||||
/>
|
||||
<DateOverrideInputDialog
|
||||
@@ -73,7 +84,7 @@ export function AvailabilityEditSheet(props: Props) {
|
||||
// This sheet will not be rendered without a selected user
|
||||
const userId = props.selectedUser?.id as number;
|
||||
const { t } = useLocale();
|
||||
const utils = trpc.useContext();
|
||||
const utils = trpc.useUtils();
|
||||
|
||||
const { data: hasEditPermission, isPending: loadingPermissions } =
|
||||
trpc.viewer.teams.hasEditPermissionForUser.useQuery({
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { TRPCClientError } from "@trpc/client";
|
||||
|
||||
import { HttpError } from "./http-error";
|
||||
|
||||
export function getClientErrorFromUnknown(cause: unknown): Error {
|
||||
if (cause instanceof HttpError) {
|
||||
const message = `${cause.statusCode}: ${cause.message}`;
|
||||
return new Error(message);
|
||||
}
|
||||
if (cause instanceof TRPCClientError) {
|
||||
return new Error(cause.message);
|
||||
}
|
||||
if (cause instanceof Error) {
|
||||
return new Error(cause.message);
|
||||
}
|
||||
if (typeof cause === "string") {
|
||||
return new Error(cause);
|
||||
}
|
||||
|
||||
return new Error(`Unhandled error of type '${typeof cause}'. Please reach out for our customer support.`);
|
||||
}
|
||||
|
||||
export const withErrorFromUnknown = (a: (b: Error) => void) => (b: unknown) =>
|
||||
a(getClientErrorFromUnknown(b));
|
||||
+6
-1
@@ -1,7 +1,12 @@
|
||||
import { vi } from "vitest";
|
||||
import matchers from "@testing-library/jest-dom/matchers";
|
||||
import ResizeObserver from "resize-observer-polyfill";
|
||||
import { vi, expect } from "vitest";
|
||||
import createFetchMock from "vitest-fetch-mock";
|
||||
|
||||
global.ResizeObserver = ResizeObserver;
|
||||
const fetchMocker = createFetchMock(vi);
|
||||
|
||||
// sets globalThis.fetch and globalThis.fetchMock to our mocked version
|
||||
fetchMocker.enableMocks();
|
||||
|
||||
expect.extend(matchers);
|
||||
+7
-10
@@ -34,13 +34,10 @@ const workspaces = packagedEmbedTestsOnly
|
||||
: [
|
||||
{
|
||||
test: {
|
||||
include: ["packages/**/*.{test,spec}.{ts,js}", "apps/**/*.{test,spec}.{ts,js}"],
|
||||
exclude: [
|
||||
"**/node_modules/**/*",
|
||||
"**/.next/**/*",
|
||||
"packages/embeds/**/*",
|
||||
"packages/lib/hooks/**/*",
|
||||
],
|
||||
globals: true,
|
||||
name: "@calcom/features",
|
||||
include: ["packages/features/**/*.{test,spec}.tsx"],
|
||||
environment: "jsdom",
|
||||
setupFiles: ["setupVitest.ts"],
|
||||
},
|
||||
},
|
||||
@@ -56,8 +53,8 @@ const workspaces = packagedEmbedTestsOnly
|
||||
{
|
||||
test: {
|
||||
globals: true,
|
||||
name: "ui/components",
|
||||
include: ["packages/ui/components/**/*.{test,spec}.{ts,js,tsx}"],
|
||||
name: "@calcom/ui",
|
||||
include: ["packages/ui/components/**/*.{test,spec}.[jt]s?(x)"],
|
||||
environment: "jsdom",
|
||||
setupFiles: ["packages/ui/components/test-setup.ts"],
|
||||
},
|
||||
@@ -66,7 +63,7 @@ const workspaces = packagedEmbedTestsOnly
|
||||
test: {
|
||||
globals: true,
|
||||
name: "EventTypeAppCardInterface components",
|
||||
include: ["packages/app-store/_components/**/*.{test,spec}.{ts,js,tsx}"],
|
||||
include: ["packages/app-store/_components/**/*.{test,spec}.[jt]s?(x)"],
|
||||
environment: "jsdom",
|
||||
setupFiles: ["packages/app-store/test-setup.ts"],
|
||||
},
|
||||
|
||||
@@ -3546,7 +3546,7 @@ __metadata:
|
||||
"@types/react-dom": ^18.0.9
|
||||
eslint: ^8.34.0
|
||||
eslint-config-next: ^13.2.1
|
||||
next: ^14.1.0
|
||||
next: ^13.5.4
|
||||
next-auth: ^4.22.1
|
||||
postcss: ^8.4.18
|
||||
react: ^18.2.0
|
||||
@@ -8296,10 +8296,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@next/env@npm:14.1.1":
|
||||
version: 14.1.1
|
||||
resolution: "@next/env@npm:14.1.1"
|
||||
checksum: 9714e716ca26dd024b1680ca9c17be60adf4228a2bd7a1a9f71752cfd8989f005e805477453200cad0a5ed85439161563a248235fc897dc42f762cba742ec62a
|
||||
"@next/env@npm:14.1.3":
|
||||
version: 14.1.3
|
||||
resolution: "@next/env@npm:14.1.3"
|
||||
checksum: 1645d801acc5c642c57205c354f39d0b95d5641613a0d6e2aff1ab3e7bd34402953e10246484b405086961b91baf044baf0b7a510f020a68427b63f6093bca90
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -8319,9 +8319,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@next/swc-darwin-arm64@npm:14.1.1":
|
||||
version: 14.1.1
|
||||
resolution: "@next/swc-darwin-arm64@npm:14.1.1"
|
||||
"@next/swc-darwin-arm64@npm:14.1.3":
|
||||
version: 14.1.3
|
||||
resolution: "@next/swc-darwin-arm64@npm:14.1.3"
|
||||
conditions: os=darwin & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
@@ -8333,9 +8333,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@next/swc-darwin-x64@npm:14.1.1":
|
||||
version: 14.1.1
|
||||
resolution: "@next/swc-darwin-x64@npm:14.1.1"
|
||||
"@next/swc-darwin-x64@npm:14.1.3":
|
||||
version: 14.1.3
|
||||
resolution: "@next/swc-darwin-x64@npm:14.1.3"
|
||||
conditions: os=darwin & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
@@ -8347,9 +8347,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@next/swc-linux-arm64-gnu@npm:14.1.1":
|
||||
version: 14.1.1
|
||||
resolution: "@next/swc-linux-arm64-gnu@npm:14.1.1"
|
||||
"@next/swc-linux-arm64-gnu@npm:14.1.3":
|
||||
version: 14.1.3
|
||||
resolution: "@next/swc-linux-arm64-gnu@npm:14.1.3"
|
||||
conditions: os=linux & cpu=arm64 & libc=glibc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
@@ -8361,9 +8361,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@next/swc-linux-arm64-musl@npm:14.1.1":
|
||||
version: 14.1.1
|
||||
resolution: "@next/swc-linux-arm64-musl@npm:14.1.1"
|
||||
"@next/swc-linux-arm64-musl@npm:14.1.3":
|
||||
version: 14.1.3
|
||||
resolution: "@next/swc-linux-arm64-musl@npm:14.1.3"
|
||||
conditions: os=linux & cpu=arm64 & libc=musl
|
||||
languageName: node
|
||||
linkType: hard
|
||||
@@ -8375,9 +8375,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@next/swc-linux-x64-gnu@npm:14.1.1":
|
||||
version: 14.1.1
|
||||
resolution: "@next/swc-linux-x64-gnu@npm:14.1.1"
|
||||
"@next/swc-linux-x64-gnu@npm:14.1.3":
|
||||
version: 14.1.3
|
||||
resolution: "@next/swc-linux-x64-gnu@npm:14.1.3"
|
||||
conditions: os=linux & cpu=x64 & libc=glibc
|
||||
languageName: node
|
||||
linkType: hard
|
||||
@@ -8389,9 +8389,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@next/swc-linux-x64-musl@npm:14.1.1":
|
||||
version: 14.1.1
|
||||
resolution: "@next/swc-linux-x64-musl@npm:14.1.1"
|
||||
"@next/swc-linux-x64-musl@npm:14.1.3":
|
||||
version: 14.1.3
|
||||
resolution: "@next/swc-linux-x64-musl@npm:14.1.3"
|
||||
conditions: os=linux & cpu=x64 & libc=musl
|
||||
languageName: node
|
||||
linkType: hard
|
||||
@@ -8403,9 +8403,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@next/swc-win32-arm64-msvc@npm:14.1.1":
|
||||
version: 14.1.1
|
||||
resolution: "@next/swc-win32-arm64-msvc@npm:14.1.1"
|
||||
"@next/swc-win32-arm64-msvc@npm:14.1.3":
|
||||
version: 14.1.3
|
||||
resolution: "@next/swc-win32-arm64-msvc@npm:14.1.3"
|
||||
conditions: os=win32 & cpu=arm64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
@@ -8417,9 +8417,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@next/swc-win32-ia32-msvc@npm:14.1.1":
|
||||
version: 14.1.1
|
||||
resolution: "@next/swc-win32-ia32-msvc@npm:14.1.1"
|
||||
"@next/swc-win32-ia32-msvc@npm:14.1.3":
|
||||
version: 14.1.3
|
||||
resolution: "@next/swc-win32-ia32-msvc@npm:14.1.3"
|
||||
conditions: os=win32 & cpu=ia32
|
||||
languageName: node
|
||||
linkType: hard
|
||||
@@ -8431,9 +8431,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@next/swc-win32-x64-msvc@npm:14.1.1":
|
||||
version: 14.1.1
|
||||
resolution: "@next/swc-win32-x64-msvc@npm:14.1.1"
|
||||
"@next/swc-win32-x64-msvc@npm:14.1.3":
|
||||
version: 14.1.3
|
||||
resolution: "@next/swc-win32-x64-msvc@npm:14.1.3"
|
||||
conditions: os=win32 & cpu=x64
|
||||
languageName: node
|
||||
linkType: hard
|
||||
@@ -17356,9 +17356,11 @@ __metadata:
|
||||
lint-staged: ^12.5.0
|
||||
lucide-react: ^0.171.0
|
||||
mailhog: ^4.16.0
|
||||
next-router-mock: ^0.9.12
|
||||
node-ical: ^0.16.1
|
||||
prettier: ^2.8.6
|
||||
prismock: ^1.21.1
|
||||
resize-observer-polyfill: ^1.5.1
|
||||
tsc-absolute: ^1.0.0
|
||||
turbo: ^1.10.1
|
||||
typescript: ^4.9.4
|
||||
@@ -17515,9 +17517,9 @@ __metadata:
|
||||
linkType: hard
|
||||
|
||||
"caniuse-lite@npm:^1.0.30001579":
|
||||
version: 1.0.30001591
|
||||
resolution: "caniuse-lite@npm:1.0.30001591"
|
||||
checksum: e48f924cdefff86d29d38ee1bffe2cdb1ef55e179d08ae2f1f5546d9d563e030f13755a0096ea87a09498daffd18666d1fe0b2759aea8421bbf4c214b47d410d
|
||||
version: 1.0.30001597
|
||||
resolution: "caniuse-lite@npm:1.0.30001597"
|
||||
checksum: ec6a2cf0fd49f37d16732e6595939fc80a125dcd188a950bc936c61b4ad53becc0fe51bf2d9a625415de7b1cb23bd835f220e8b68d8ab951a940edeea65476fd
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -29894,6 +29896,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"next-router-mock@npm:^0.9.12":
|
||||
version: 0.9.12
|
||||
resolution: "next-router-mock@npm:0.9.12"
|
||||
peerDependencies:
|
||||
next: ">=10.0.0"
|
||||
react: ">=17.0.0"
|
||||
checksum: f630a4308686b2f6eeb1138d05ccb40dd2e6312216de1e94811403222967fd9d1ee723b2595d20bffbb1eaffce0d8e8da81983346528c6ac8334d4968338dd96
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"next-seo@npm:^6.0.0":
|
||||
version: 6.1.0
|
||||
resolution: "next-seo@npm:6.1.0"
|
||||
@@ -30005,20 +30017,20 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"next@npm:^14.1, next@npm:^14.1.0":
|
||||
version: 14.1.1
|
||||
resolution: "next@npm:14.1.1"
|
||||
"next@npm:^14.1":
|
||||
version: 14.1.3
|
||||
resolution: "next@npm:14.1.3"
|
||||
dependencies:
|
||||
"@next/env": 14.1.1
|
||||
"@next/swc-darwin-arm64": 14.1.1
|
||||
"@next/swc-darwin-x64": 14.1.1
|
||||
"@next/swc-linux-arm64-gnu": 14.1.1
|
||||
"@next/swc-linux-arm64-musl": 14.1.1
|
||||
"@next/swc-linux-x64-gnu": 14.1.1
|
||||
"@next/swc-linux-x64-musl": 14.1.1
|
||||
"@next/swc-win32-arm64-msvc": 14.1.1
|
||||
"@next/swc-win32-ia32-msvc": 14.1.1
|
||||
"@next/swc-win32-x64-msvc": 14.1.1
|
||||
"@next/env": 14.1.3
|
||||
"@next/swc-darwin-arm64": 14.1.3
|
||||
"@next/swc-darwin-x64": 14.1.3
|
||||
"@next/swc-linux-arm64-gnu": 14.1.3
|
||||
"@next/swc-linux-arm64-musl": 14.1.3
|
||||
"@next/swc-linux-x64-gnu": 14.1.3
|
||||
"@next/swc-linux-x64-musl": 14.1.3
|
||||
"@next/swc-win32-arm64-msvc": 14.1.3
|
||||
"@next/swc-win32-ia32-msvc": 14.1.3
|
||||
"@next/swc-win32-x64-msvc": 14.1.3
|
||||
"@swc/helpers": 0.5.2
|
||||
busboy: 1.6.0
|
||||
caniuse-lite: ^1.0.30001579
|
||||
@@ -30056,7 +30068,7 @@ __metadata:
|
||||
optional: true
|
||||
bin:
|
||||
next: dist/bin/next
|
||||
checksum: 58c17bf9520648cb79c3341a0a011ce19e8e5368f23be7415b4a35787b3562519cfd0dd8e008a1ce0b49dfe79e4ab89127f412cf039cb854b35f79a457a9be22
|
||||
checksum: 179a5dfd27a3ac900c0c229c523793cf28c3972cf87ec9ce7b68ead8fc2b8cd3b9c0538bba1b42d3351462ffe890b964c869e081da76c3bed29826e7eae4d7a4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -34749,6 +34761,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"resize-observer-polyfill@npm:^1.5.1":
|
||||
version: 1.5.1
|
||||
resolution: "resize-observer-polyfill@npm:1.5.1"
|
||||
checksum: 57e7f79489867b00ba43c9c051524a5c8f162a61d5547e99333549afc23e15c44fd43f2f318ea0261ea98c0eb3158cca261e6f48d66e1ed1cd1f340a43977094
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"resolve-alpn@npm:^1.0.0":
|
||||
version: 1.2.1
|
||||
resolution: "resolve-alpn@npm:1.2.1"
|
||||
|
||||
Reference in new issue
Block a user