fix: revalidation replacing availability form values (#14114)
This commit is contained in:
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user