Feature/date overrides (#5991)

* Initial incomplete (but mostly functional) push of date override functions

* Fixed date shifting on load

* Bring back minDate (automatically disable all dates before current date)

* Fix type error

* Supply working hours to render available dates

* Converted to SSR

* moving defaultValues to the backend

* Improv. as filter can be achieved within the reduce

Co-authored-by: Omar López <zomars@me.com>

* Double inversion -> single, as it is an early return

* uniq() exit - not needed anymore

* Typefixes

* It's overriding dates :D

* Fixed duplication DateOverrides in list

* Implemented changing the month

* Make dateOverrides an optional param

* Fixed test (which requires dateOverrides due to auto-typing)

* Prevent a full update on set as default from list view

* Added some extra keys to keep ts happy

* Only allow a single date override per date

* Disallow editing excludedDates to the same date

* Bring back duplicate key ?.?

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: zomars <zomars@me.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
This commit is contained in:
authored and GitHub committed 2022-12-14 17:30:55 +00:00
1 parent 8fd5d6b5b5
commit 2f2b72dd54
20 files changed
+707 -202

No files matched your search

+71 -42
View File
@@ -1,15 +1,16 @@
import { GetStaticPaths, GetStaticProps } from "next";
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { GetServerSidePropsContext } from "next";
import { Controller, useFieldArray, useForm } from "react-hook-form";
import { z } from "zod";
import { DateOverrideInputDialog, DateOverrideList } from "@calcom/features/schedules";
import Schedule from "@calcom/features/schedules/components/Schedule";
import { availabilityAsString } from "@calcom/lib/availability";
import { yyyymmdd } from "@calcom/lib/date-fns";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { stringOrNumber } from "@calcom/prisma/zod-utils";
import { trpc } from "@calcom/trpc/react";
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
import type { Schedule as ScheduleType } from "@calcom/types/schedule";
import type { Schedule as ScheduleType, TimeRange, WorkingHours } from "@calcom/types/schedule";
import {
Button,
Form,
@@ -21,6 +22,7 @@ import {
SkeletonText,
Switch,
TimezoneSelect,
Tooltip,
VerticalDivider,
} from "@calcom/ui";
@@ -29,7 +31,7 @@ import { HttpError } from "@lib/core/http/error";
import { SelectSkeletonLoader } from "@components/availability/SkeletonLoader";
import EditableHeading from "@components/ui/EditableHeading";
import { ssgInit } from "@server/lib/ssg";
import { ssrInit } from "@server/lib/ssr";
const querySchema = z.object({
schedule: stringOrNumber,
@@ -38,31 +40,59 @@ const querySchema = z.object({
type AvailabilityFormValues = {
name: string;
schedule: ScheduleType;
dateOverrides: { ranges: TimeRange[] }[];
timeZone: string;
isDefault: boolean;
};
const DateOverride = ({ workingHours }: { workingHours: WorkingHours[] }) => {
const { remove, append, update, fields } = useFieldArray<AvailabilityFormValues, "dateOverrides">({
name: "dateOverrides",
});
const { t } = useLocale();
return (
<div className="px-4 py-5 sm:p-6">
<h3 className="font-medium leading-6 text-gray-900">
{t("date_overrides")}{" "}
<Tooltip content={t("date_overrides_info")}>
<span className="inline-block">
<Icon.FiInfo />
</span>
</Tooltip>
</h3>
<p className="mb-4 text-sm text-neutral-500 ltr:mr-4 rtl:ml-4">{t("date_overrides_subtitle")}</p>
<div className="mt-1 space-y-2">
<DateOverrideList
excludedDates={fields.map((field) => yyyymmdd(field.ranges[0].start))}
remove={remove}
update={update}
items={fields}
workingHours={workingHours}
/>
<DateOverrideInputDialog
workingHours={workingHours}
excludedDates={fields.map((field) => yyyymmdd(field.ranges[0].start))}
onChange={(ranges) => append({ ranges })}
Trigger={
<Button color="secondary" StartIcon={Icon.FiPlus}>
Add an override
</Button>
}
/>
</div>
</div>
);
};
export default function Availability({ schedule }: { schedule: number }) {
const { t, i18n } = useLocale();
const utils = trpc.useContext();
const me = useMeQuery();
const { timeFormat } = me.data || { timeFormat: null };
const { data, isLoading } = trpc.viewer.availability.schedule.get.useQuery({ scheduleId: schedule });
const form = useForm<AvailabilityFormValues>();
const { control, reset } = form;
useEffect(() => {
if (!isLoading && data) {
reset({
name: data?.schedule?.name,
schedule: data.availability,
timeZone: data.timeZone,
isDefault: data.isDefault,
});
}
}, [data, isLoading, reset]);
const { data: defaultValues } = trpc.viewer.availability.defaultValues.useQuery({ scheduleId: schedule });
const form = useForm<AvailabilityFormValues>({ defaultValues });
const { control } = form;
const updateMutation = trpc.viewer.availability.schedule.update.useMutation({
onSuccess: async ({ prevDefaultId, currentDefaultId, ...data }) => {
if (prevDefaultId && currentDefaultId) {
@@ -73,7 +103,7 @@ export default function Availability({ schedule }: { schedule: number }) {
utils.viewer.availability.schedule.get.refetch({ scheduleId: prevDefaultId });
}
}
utils.viewer.availability.schedule.get.setData({ scheduleId: data.schedule.id }, data);
utils.viewer.availability.schedule.get.invalidate({ scheduleId: data.schedule.id });
utils.viewer.availability.list.invalidate();
showToast(
t("availability_updated_successfully", {
@@ -103,12 +133,14 @@ export default function Availability({ schedule }: { schedule: number }) {
}
subtitle={
data ? (
data.schedule.availability.map((availability) => (
<span key={availability.id}>
{availabilityAsString(availability, { locale: i18n.language, hour12: timeFormat === 12 })}
<br />
</span>
))
data.schedule.availability
.filter((availability) => !!availability.days.length)
.map((availability) => (
<span key={availability.id}>
{availabilityAsString(availability, { locale: i18n.language, hour12: timeFormat === 12 })}
<br />
</span>
))
) : (
<SkeletonText className="h-4 w-48" />
)
@@ -147,15 +179,16 @@ export default function Availability({ schedule }: { schedule: number }) {
<Form
form={form}
id="availability-form"
handleSubmit={async (values) => {
handleSubmit={async ({ dateOverrides, ...values }) => {
updateMutation.mutate({
scheduleId: schedule,
dateOverrides: dateOverrides.flatMap((override) => override.ranges),
...values,
});
}}
className="-mx-4 flex flex-col pb-16 sm:mx-0 xl:flex-row xl:space-x-6">
<div className="flex-1">
<div className="rounded-md border-gray-200 bg-white py-5 pr-4 sm:border sm:p-6">
<div className="flex-1 divide-y divide-neutral-200 rounded-md border">
<div className=" py-5 pr-4 sm:p-6">
<h3 className="mb-5 text-base font-medium leading-6 text-gray-900">
{t("change_start_end")}
</h3>
@@ -171,6 +204,7 @@ export default function Availability({ schedule }: { schedule: number }) {
/>
)}
</div>
{data?.workingHours && <DateOverride workingHours={data.workingHours} />}
</div>
<div className="min-w-40 col-span-3 space-y-2 lg:col-span-1">
<div className="xl:max-w-80 mt-4 w-full pr-4 sm:p-0">
@@ -211,24 +245,19 @@ export default function Availability({ schedule }: { schedule: number }) {
);
}
export const getStaticProps: GetStaticProps = async (ctx) => {
export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
const params = querySchema.safeParse(ctx.params);
const ssg = await ssgInit(ctx);
const ssr = await ssrInit(ctx);
if (!params.success) return { notFound: true };
const scheduleId = params.data.schedule;
await ssr.viewer.availability.schedule.get.fetch({ scheduleId });
await ssr.viewer.availability.defaultValues.fetch({ scheduleId });
return {
props: {
schedule: params.data.schedule,
trpcState: ssg.dehydrate(),
schedule: scheduleId,
trpcState: ssr.dehydrate(),
},
revalidate: 10, // seconds
};
};
export const getStaticPaths: GetStaticPaths = () => {
return {
paths: [],
fallback: "blocking",
};
};
@@ -1441,6 +1441,17 @@
"enter_option": "Enter Option {{index}}",
"add_an_option": "Add an option",
"radio": "Radio",
"date_overrides": "Date overrides",
"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?",
"date_overrides_dialog_which_hours_unavailable": "Which hours are you busy?",
"date_overrides_dialog_title": "Select the dates to override",
"date_overrides_unavailable": "Unavailable all day",
"date_overrides_mark_all_day_unavailable_one": "Mark unavailable (All day)",
"date_overrides_mark_all_day_unavailable_other": "Mark unavailable on selected dates",
"date_overrides_add_btn": "Add Override",
"date_overrides_update_btn": "Update Override",
"event_type_duplicate_copy_text": "{{slug}}-copy",
"set_as_default": "Set as default",
"hide_eventtype_details": "Hide EventType Details"
+5
View File
@@ -432,6 +432,11 @@ hr {
border: none !important;
}
.react-date-picker__inputGroup__input {
padding-top: 0;
padding-bottom: 0;
}
/* animations */
.slideInBottom {
animation-duration: 0.3s;
@@ -10,6 +10,7 @@ const HAWAII_AND_NEWYORK_TEAM = [
timeZone: "America/Detroit", // GMT -4 per 22th of Aug, 2022
workingHours: [{ days: [1, 2, 3, 4, 5], startTime: 780, endTime: 1260 }],
busy: [],
dateOverrides: [],
},
{
timeZone: "Pacific/Honolulu", // GMT -10 per 22th of Aug, 2022
@@ -20,6 +21,7 @@ const HAWAII_AND_NEWYORK_TEAM = [
{ days: [5], startTime: 780, endTime: 1439 },
],
busy: [],
dateOverrides: [],
},
];