fix: revalidation replacing availability form values (#14114)

This commit is contained in:
Omar López
2024-03-21 23:07:32 +00:00
committed by GitHub
parent 74e5f4bf37
commit d08c3fdc71
3 changed files with 65 additions and 64 deletions
@@ -1,14 +1,16 @@
import { useForm, useFieldArray } from "react-hook-form";
import { useFieldArray, useForm } from "react-hook-form";
import dayjs from "@calcom/dayjs";
import { DateOverrideInputDialog, DateOverrideList } from "@calcom/features/schedules";
import Schedule from "@calcom/features/schedules/components/Schedule";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { HttpError } from "@calcom/lib/http-error";
import type { RouterOutputs } from "@calcom/trpc/react";
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 {
Alert,
Button,
Form,
Label,
@@ -16,9 +18,8 @@ import {
SheetContent,
SheetHeader,
SheetTitle,
TimezoneSelect,
showToast,
Alert,
TimezoneSelect,
} from "@calcom/ui";
import { Plus } from "@calcom/ui/components/icon";
@@ -82,6 +83,25 @@ const DateOverride = ({ workingHours, disabled }: { workingHours: WorkingHours[]
};
export function AvailabilityEditSheet(props: Props) {
// This sheet will not be rendered without a selected user
const userId = props.selectedUser?.id as number;
const { data, isPending } = trpc.viewer.availability.schedule.getScheduleByUserId.useQuery({
userId: userId,
});
// TODO: reimplement Skeletons for this page in here
if (isPending) return null;
if (!data) return null;
// We wait for the schedule to be loaded before rendering the form since `defaultValues`
// cannot be redeclared after first render. And using `values` will trigger a form reset
// when revalidating.
return <AvailabilityEditSheetForm {...props} data={data} isPending={isPending} />;
}
type Data = RouterOutputs["viewer"]["availability"]["schedule"]["getScheduleByUserId"];
export function AvailabilityEditSheetForm(props: Props & { data: Data; isPending: boolean }) {
// This sheet will not be rendered without a selected user
const userId = props.selectedUser?.id as number;
const { t } = useLocale();
@@ -92,13 +112,11 @@ export function AvailabilityEditSheet(props: Props) {
memberId: userId,
});
const { data, isPending } = trpc.viewer.availability.schedule.getScheduleByUserId.useQuery({
userId: userId,
});
const { data, isPending } = props;
const updateMutation = trpc.viewer.availability.schedule.update.useMutation({
onSuccess: async () => {
utils.viewer.availability.listTeam.invalidate();
await utils.viewer.availability.listTeam.invalidate();
showToast(t("success"), "success");
props.onOpenChange(false);
},
@@ -111,10 +129,10 @@ export function AvailabilityEditSheet(props: Props) {
});
const form = useForm<AvailabilityFormValues>({
values: data && {
defaultValues: {
...data,
timeZone: data?.timeZone || Intl.DateTimeFormat().resolvedOptions().timeZone,
schedule: data?.availability || [],
timeZone: data.timeZone || Intl.DateTimeFormat().resolvedOptions().timeZone,
schedule: data.availability || [],
},
});
@@ -128,12 +146,11 @@ export function AvailabilityEditSheet(props: Props) {
handleSubmit={async ({ dateOverrides, ...values }) => {
// Just blocking on a UI side -> Backend will also do the validation
if (!hasEditPermission) return;
data &&
updateMutation.mutate({
scheduleId: data?.id,
dateOverrides: dateOverrides.flatMap((override) => override.ranges),
...values,
});
updateMutation.mutate({
scheduleId: data.id,
dateOverrides: dateOverrides.flatMap((override) => override.ranges),
...values,
});
}}>
<SheetContent
bottomActions={
@@ -142,7 +159,7 @@ export function AvailabilityEditSheet(props: Props) {
{t("cancel")}
</Button>
<Button
disabled={!hasEditPermission || !data?.hasDefaultSchedule}
disabled={!hasEditPermission || !data.hasDefaultSchedule}
className="w-full justify-center"
type="submit"
loading={updateMutation.isPending}
@@ -158,7 +175,7 @@ export function AvailabilityEditSheet(props: Props) {
})}
</SheetTitle>
</SheetHeader>
{!data?.hasDefaultSchedule && !isPending && hasEditPermission && (
{!data.hasDefaultSchedule && !isPending && hasEditPermission && (
<div className="my-2">
<Alert severity="warning" title={t("view_only_edit_availability_not_onboarded")} />
</div>
@@ -176,7 +193,7 @@ export function AvailabilityEditSheet(props: Props) {
</Label>
<TimezoneSelect
id="timezone"
isDisabled={!hasEditPermission || !data?.hasDefaultSchedule}
isDisabled={!hasEditPermission || !data.hasDefaultSchedule}
value={watchTimezone ?? "Europe/London"}
onChange={(event) => {
if (event) form.setValue("timeZone", event.value, { shouldDirty: true });
@@ -191,12 +208,12 @@ export function AvailabilityEditSheet(props: Props) {
control={form.control}
name="schedule"
weekStart={0}
disabled={!hasEditPermission || !data?.hasDefaultSchedule}
disabled={!hasEditPermission || !data.hasDefaultSchedule}
/>
</div>
</div>
<div className="mt-4">
{data?.workingHours && (
{data.workingHours && (
<DateOverride
workingHours={data.workingHours}
disabled={!hasEditPermission || !data.hasDefaultSchedule}
@@ -1,5 +1,5 @@
import { useMemo, useState } from "react";
import { useForm, Controller, useFieldArray, useWatch } from "react-hook-form";
import { Controller, useFieldArray, useForm, useWatch } from "react-hook-form";
import dayjs from "@calcom/dayjs";
import { DateOverrideInputDialog, DateOverrideList } from "@calcom/features/schedules";
@@ -10,25 +10,24 @@ import WebShell from "@calcom/features/shell/Shell";
import { availabilityAsString } from "@calcom/lib/availability";
import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { WorkingHours } from "@calcom/types/schedule";
import type { TimeRange } from "@calcom/types/schedule";
import type { TimeRange, WorkingHours } from "@calcom/types/schedule";
import {
VerticalDivider,
Button,
Form,
SkeletonText,
ConfirmationDialogContent as WebConfirmationDialogContent,
Dialog as WebDialog,
DialogTrigger as WebDialogTrigger,
EditableHeading,
Form,
Label,
SelectSkeletonLoader,
Skeleton,
SkeletonText,
Switch,
TimezoneSelect as WebTimezoneSelect,
SelectSkeletonLoader,
Tooltip,
EditableHeading,
VerticalDivider,
} from "@calcom/ui";
import { MoreVertical, ArrowLeft, Trash, Info, Plus } from "@calcom/ui/components/icon";
import { ArrowLeft, Info, MoreVertical, Plus, Trash } from "@calcom/ui/components/icon";
import { ConfirmationDialogContent as PlatformConfirmationDialogContent } from "../src/components/ui/confirmation-dialog-content";
import {
@@ -69,7 +68,7 @@ export type CustomClassNames = {
type AvailabilitySettingsProps = {
skeletonLabel?: string;
schedule?: {
schedule: {
name: string;
id: number;
availability: TimeRange[][];
@@ -78,7 +77,7 @@ type AvailabilitySettingsProps = {
workingHours: WorkingHours[];
dateOverrides: { ranges: TimeRange[] }[];
timeZone: string;
schedule: Schedule[] | [];
schedule: Schedule[];
};
handleDelete: () => void;
isDeleting: boolean;
@@ -239,9 +238,9 @@ export function AvailabilitySettings({
const { t, i18n } = useLocale();
const form = useForm<AvailabilityFormValues>({
values: schedule && {
defaultValues: {
...schedule,
schedule: schedule?.availability || [],
schedule: schedule.availability || [],
},
});
@@ -255,7 +254,7 @@ export function AvailabilitySettings({
<Shell
headerClassName={cn(customClassNames?.containerClassName)}
backPath={backPath}
title={schedule?.name ? `${schedule.name} | t("availability")}` : t("availability")}
title={schedule.name ? `${schedule.name} | t("availability")}` : t("availability")}
heading={
<Controller
control={form.control}
@@ -303,7 +302,7 @@ export function AvailabilitySettings({
render={({ field: { value, onChange } }) => (
<Switch
id="hiddenSwitch"
disabled={isSaving || schedule?.isDefault}
disabled={isSaving || schedule.isDefault}
checked={value}
onCheckedChange={onChange}
/>
@@ -316,7 +315,7 @@ export function AvailabilitySettings({
<VerticalDivider className="hidden sm:inline" />
<DeleteDialogButton
buttonClassName="hidden sm:inline"
disabled={schedule?.isLastSchedule}
disabled={schedule.isLastSchedule}
isPending={isDeleting}
handleDelete={handleDelete}
isPlatform={isPlatform}
@@ -340,7 +339,7 @@ export function AvailabilitySettings({
<p className="-ml-2">{t("availability_settings")}</p>
<DeleteDialogButton
buttonClassName="ml-16 inline"
disabled={schedule?.isLastSchedule}
disabled={schedule.isLastSchedule}
isPending={isDeleting}
handleDelete={handleDelete}
isPlatform={isPlatform}
@@ -492,7 +491,7 @@ export function AvailabilitySettings({
</div>
{!isPlatform ? (
<div className="border-subtle my-6 rounded-md border">
{schedule?.workingHours && (
{schedule.workingHours && (
<DateOverride workingHours={schedule.workingHours} userTimeFormat={timeFormat} />
)}
</div>
@@ -1,5 +1,4 @@
import { useRouter } from "next/navigation";
import { useForm } from "react-hook-form";
import { withErrorFromUnknown } from "@calcom/lib/getClientErrorFromUnknown";
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
@@ -10,13 +9,12 @@ import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
import { showToast } from "@calcom/ui";
import { AvailabilitySettings } from "../AvailabilitySettings";
import type { AvailabilityFormValues } from "../types";
export const WebAvailabilitySettingsWrapper = () => {
const searchParams = useCompatSearchParams();
const { t } = 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");
@@ -27,13 +25,6 @@ export const WebAvailabilitySettingsWrapper = () => {
enabled: !!scheduleId,
}
);
const form = useForm<AvailabilityFormValues>({
values: schedule && {
...schedule,
schedule: schedule?.availability || [],
},
});
const updateMutation = trpc.viewer.availability.schedule.update.useMutation({
onSuccess: async ({ prevDefaultId, currentDefaultId, ...data }) => {
if (prevDefaultId && currentDefaultId) {
@@ -74,23 +65,17 @@ export const WebAvailabilitySettingsWrapper = () => {
},
});
// TODO: reimplement Skeletons for this page in here
if (isPending) return null;
// We wait for the schedule to be loaded before rendering the form inside AvailabilitySettings
// since `defaultValues` cannot be redeclared after first render and using `values` will
// trigger a form reset when revalidating. Introducing flaky behavior.
if (!schedule) return null;
return (
<AvailabilitySettings
schedule={
schedule
? {
name: schedule.name,
id: schedule.id,
isLastSchedule: schedule.isLastSchedule,
isDefault: schedule.isDefault,
workingHours: schedule.workingHours,
dateOverrides: schedule.dateOverrides,
timeZone: schedule.timeZone,
availability: schedule.availability || [],
schedule: schedule.schedule || [],
}
: undefined
}
schedule={schedule}
isDeleting={deleteMutation.isPending}
isLoading={isPending}
isSaving={updateMutation.isPending}