fix: Autodetection of time zone only updated default time zone (#15392)

* fix: Autodetection of time zone only updated default time zone

* fix: description text

---------

Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
This commit is contained in:
Amit Sharma
2024-06-28 10:12:33 +01:00
committed by GitHub
co-authored by sean-brydon
parent c458b297ae
commit 285d1ec4c9
8 changed files with 148 additions and 139 deletions
+14 -3
View File
@@ -5,7 +5,7 @@ import { InstallAppButton } from "@calcom/app-store/components";
import { getEventLocationTypeFromApp, type EventLocationType } from "@calcom/app-store/locations";
import type { CredentialOwner } from "@calcom/app-store/types";
import { AppSetDefaultLinkDialog } from "@calcom/features/apps/components/AppSetDefaultLinkDialog";
import { BulkEditDefaultForEventsModal } from "@calcom/features/eventtypes/components/BulkEditDefaultForEventsModal";
import { BulkEditDefaultModal } from "@calcom/features/eventtypes/components/BulkEditDefaultModal";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { AppCategories } from "@calcom/prisma/enums";
import { trpc, type RouterOutputs } from "@calcom/trpc";
@@ -153,6 +153,8 @@ export const AppList = ({ data, handleDisconnect, variant, listClassName }: AppL
});
const { t } = useLocale();
const [eventTypeIds, setEventTypeIds] = useState<number[]>([]);
const { data: bulkEventTypes } = trpc.viewer.eventTypes.bulkEventFetch.useQuery();
const updateLocationsMutation = trpc.viewer.eventTypes.bulkUpdateToDefaultLocation.useMutation({
onSuccess: () => {
utils.viewer.getUsersDefaultConferencingApp.invalidate();
@@ -178,11 +180,20 @@ export const AppList = ({ data, handleDisconnect, variant, listClassName }: AppL
)}
{bulkUpdateModal && (
<BulkEditDefaultForEventsModal
bulkUpdateFunction={updateLocationsMutation.mutate}
<BulkEditDefaultModal
handleSubmit={() => {
updateLocationsMutation.mutate({
eventTypeIds,
});
}}
open={bulkUpdateModal}
setOpen={setBulkUpdateModal}
isPending={updateLocationsMutation.isPending}
title={t("default_conferencing_bulk_title")}
description={t("default_conferencing_bulk_description")}
data={bulkEventTypes?.eventTypes}
ids={eventTypeIds}
setIds={setEventTypeIds}
/>
)}
</>
+14 -3
View File
@@ -3,7 +3,7 @@ import Link from "next/link";
import { useRouter, usePathname } from "next/navigation";
import { useCallback, useState } from "react";
import { BulkEditDefaultForEventsModal } from "@calcom/features/eventtypes/components/BulkEditDefaultForEventsModal";
import { BulkEditDefaultModal } from "@calcom/features/eventtypes/components/BulkEditDefaultModal";
import { NewScheduleButton, ScheduleListItem } from "@calcom/features/schedules";
import Shell from "@calcom/features/shell/Shell";
import { AvailabilitySliderTable } from "@calcom/features/timezone-buddy/components/AvailabilitySliderTable";
@@ -24,12 +24,14 @@ import SkeletonLoader from "@components/availability/SkeletonLoader";
export function AvailabilityList({ schedules }: RouterOutputs["viewer"]["availability"]["list"]) {
const { t } = useLocale();
const [bulkUpdateModal, setBulkUpdateModal] = useState(false);
const [eventTypeIds, setEventTypeIds] = useState<number[]>([]);
const utils = trpc.useUtils();
const meQuery = trpc.viewer.me.useQuery();
const router = useRouter();
const { data } = trpc.viewer.eventTypes.bulkEventFetch.useQuery();
const deleteMutation = trpc.viewer.availability.schedule.delete.useMutation({
onMutate: async ({ scheduleId }) => {
await utils.viewer.availability.list.cancel();
@@ -143,11 +145,20 @@ export function AvailabilityList({ schedules }: RouterOutputs["viewer"]["availab
</Link>
</div>
{bulkUpdateModal && (
<BulkEditDefaultForEventsModal
<BulkEditDefaultModal
isPending={bulkUpdateDefaultAvailabilityMutation.isPending}
open={bulkUpdateModal}
setOpen={setBulkUpdateModal}
bulkUpdateFunction={bulkUpdateDefaultAvailabilityMutation.mutate}
title={t("default_conferencing_bulk_title")}
description={t("default_conferencing_bulk_description")}
ids={eventTypeIds}
setIds={setEventTypeIds}
data={data?.eventTypes}
handleSubmit={() => {
bulkUpdateDefaultAvailabilityMutation.mutate({
eventTypeIds,
});
}}
/>
)}
</>
@@ -1916,12 +1916,14 @@
"invalid_event_name_variables": "There is an invalid variable in your event name",
"select_all": "Select All",
"default_conferencing_bulk_title": "Bulk update existing event types",
"default_timezone_bulk_title": "Bulk update existing availabilities",
"members_default_schedule": "Member's default schedule",
"set_by_admin": "Set by team admin",
"members_default_location": "Member's default location",
"members_default_schedule_description": "We will use each members default availability schedule. They will be able to edit or change it.",
"requires_at_least_one_schedule": "You are required to have at least one schedule",
"default_conferencing_bulk_description": "Update the locations for the selected event types",
"default_timezone_bulk_description": "Update the timezone for selected availabilities.",
"locked_for_members": "Locked for members",
"unlocked_for_members": "Unlocked for members",
"apps_locked_for_members_description": "Members will be able to see the active apps but will not be able to edit any app settings",
@@ -1,90 +0,0 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { Dialog, DialogContent, Form, DialogFooter, DialogClose, Button, CheckboxField } from "@calcom/ui";
export const BulkUpdateEventSchema = z.object({
eventTypeIds: z.array(z.number()),
});
export function BulkEditDefaultForEventsModal(props: {
open: boolean;
setOpen: (open: boolean) => void;
bulkUpdateFunction: ({ eventTypeIds }: { eventTypeIds: number[] }) => void;
isPending: boolean;
}) {
const { t } = useLocale();
const utils = trpc.useUtils();
const { data, isFetching } = trpc.viewer.eventTypes.bulkEventFetch.useQuery();
const form = useForm({
resolver: zodResolver(BulkUpdateEventSchema),
defaultValues: {
eventTypeIds: data?.eventTypes.map((e) => e.id) ?? [],
},
});
const eventTypesSelected = form.watch("eventTypeIds");
if (isFetching || !open || !data?.eventTypes) return null;
return (
<Dialog name="Bulk Default Location Update" open={props.open} onOpenChange={props.setOpen}>
<DialogContent
type="creation"
title={t("default_conferencing_bulk_title")}
description={t("default_conferencing_bulk_description")}
enableOverflow>
<Form
form={form}
handleSubmit={(values) => {
props.bulkUpdateFunction(values);
}}>
<div className="flex flex-col space-y-2">
{data.eventTypes.length > 0 && (
<div className="flex items-center space-x-2 rounded-md px-3 pb-2.5 pt-1">
<CheckboxField
description={t("select_all")}
descriptionAsLabel
onChange={(e) => {
form.setValue("eventTypeIds", e.target.checked ? data.eventTypes.map((e) => e.id) : []);
}}
checked={eventTypesSelected.length === data.eventTypes.length}
/>
</div>
)}
{data.eventTypes.map((eventType) => (
<div key={eventType.id} className="bg-muted flex items-center space-x-2 rounded-md px-3 py-2.5">
<CheckboxField
description={eventType.title}
descriptionAsLabel
checked={eventTypesSelected.includes(eventType.id)}
onChange={(e) => {
form.setValue(
"eventTypeIds",
e.target.checked
? [...eventTypesSelected, eventType.id]
: eventTypesSelected.filter((id) => id !== eventType.id)
);
}}
/>
</div>
))}
</div>
<DialogFooter showDivider className="mt-10">
<DialogClose
onClick={() => {
utils.viewer.getUsersDefaultConferencingApp.invalidate();
}}
/>
<Button type="submit" color="primary" loading={props.isPending}>
{t("update")}
</Button>
</DialogFooter>
</Form>
</DialogContent>
</Dialog>
);
}
@@ -0,0 +1,63 @@
import type { Dispatch, SetStateAction } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Dialog, DialogContent, DialogFooter, DialogClose, Button, CheckboxField } from "@calcom/ui";
export function BulkEditDefaultModal(props: {
open: boolean;
setOpen: (open: boolean) => void;
ids: number[];
setIds: Dispatch<SetStateAction<number[]>>;
isPending: boolean;
data?: {
id: number;
title: string;
default?: boolean;
}[];
title: string;
description: string;
handleSubmit: () => void;
}) {
const { title, description, handleSubmit, data, ids, setIds, open } = props;
const { t } = useLocale();
if (!open || !data || !ids) return null;
return (
<Dialog name="Bulk Default Update" open={props.open} onOpenChange={props.setOpen}>
<DialogContent type="creation" title={title} description={description} enableOverflow>
<div className="flex flex-col space-y-2">
{data.length > 0 && (
<div className="flex items-center space-x-2 rounded-md px-3 pb-2.5 pt-1">
<CheckboxField
description={t("select_all")}
descriptionAsLabel
onChange={(e) => {
setIds(e.target.checked ? data.map((e) => e.id) : []);
}}
checked={ids.length === data.length}
/>
</div>
)}
{data.map((item) => (
<div key={item.id} className="bg-muted flex items-center space-x-2 rounded-md px-3 py-2.5">
<CheckboxField
description={item.title}
descriptionAsLabel
checked={ids.includes(item.id)}
onChange={(e) => {
setIds(e.target.checked ? [...ids, item.id] : ids.filter((id) => id !== item.id));
}}
/>
</div>
))}
</div>
<DialogFooter showDivider className="mt-10">
<DialogClose />
<Button type="submit" color="primary" loading={props.isPending} onClick={handleSubmit}>
{t("update")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
@@ -4,6 +4,7 @@ import { useSession } from "next-auth/react";
import { useEffect, useState } from "react";
import dayjs from "@calcom/dayjs";
import { BulkEditDefaultModal } from "@calcom/features/eventtypes/components/BulkEditDefaultModal";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { Dialog, DialogClose, DialogContent, DialogFooter, showToast } from "@calcom/ui";
@@ -16,7 +17,6 @@ const TimezoneChangeDialogContent = ({
onAction: (action?: "update" | "cancel") => void;
}) => {
const { t } = useLocale();
const utils = trpc.useUtils();
const formattedCurrentTz = browserTimezone.replace("_", " ");
// save cookie to not show again
@@ -28,28 +28,6 @@ const TimezoneChangeDialogContent = ({
toast && showToast(t("we_wont_show_again"), "success");
}
const onSuccessMutation = async () => {
showToast(t("updated_timezone_to", { formattedCurrentTz }), "success");
await utils.viewer.me.invalidate();
};
const onErrorMutation = () => {
showToast(t("couldnt_update_timezone"), "error");
};
// update timezone in db
const mutation = trpc.viewer.updateProfile.useMutation({
onSuccess: onSuccessMutation,
onError: onErrorMutation,
});
function updateTimezone() {
onAction("update");
mutation.mutate({
timeZone: browserTimezone,
});
}
return (
<DialogContent
title={t("update_timezone_question")}
@@ -64,7 +42,7 @@ const TimezoneChangeDialogContent = ({
<DialogClose onClick={() => onCancel([3, "months"], true)} color="secondary">
{t("dont_update")}
</DialogClose>
<DialogClose onClick={() => updateTimezone()} color="primary">
<DialogClose onClick={() => onAction("update")} color="primary">
{t("update_timezone")}
</DialogClose>
</DialogFooter>
@@ -104,9 +82,56 @@ export function useOpenTimezoneDialog() {
export default function TimezoneChangeDialog() {
const { open, setOpen, browserTimezone } = useOpenTimezoneDialog();
const { t } = useLocale();
const utils = trpc.useUtils();
const [selectAvailability, setSelectAvailability] = useState(false);
const [ids, setIds] = useState<number[]>([]);
const { data: availability, isPending } = trpc.viewer.availability.list.useQuery();
const formattedCurrentTz = browserTimezone.replace("_", " ");
const data = availability?.schedules?.map((a) => ({
id: a.id,
title: a.name,
default: a.isDefault,
}));
const mutation = trpc.viewer.updateProfile.useMutation({
onSuccess: async () => {
showToast(t("updated_timezone_to", { formattedCurrentTz }), "success");
await utils.viewer.me.invalidate();
setSelectAvailability(false);
},
onError: () => {
showToast(t("couldnt_update_timezone"), "error");
},
});
return (
<Dialog open={open} onOpenChange={setOpen}>
<TimezoneChangeDialogContent browserTimezone={browserTimezone} onAction={() => setOpen(false)} />
<TimezoneChangeDialogContent
browserTimezone={browserTimezone}
onAction={() => {
setOpen(false);
setSelectAvailability(true);
}}
/>
<BulkEditDefaultModal
title={t("default_timezone_bulk_title")}
description={t("default_timezone_bulk_description")}
ids={ids}
isPending={isPending}
open={selectAvailability}
setOpen={setSelectAvailability}
setIds={setIds}
data={data}
handleSubmit={() => {
mutation.mutate({
timeZone: browserTimezone,
availabilityIds: ids,
});
}}
/>
</Dialog>
);
}
@@ -24,7 +24,6 @@ import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
import { TRPCError } from "@trpc/server";
import { getDefaultScheduleId } from "../viewer/availability/util";
import { updateUserMetadataAllowedKeys, type TUpdateProfileInputSchema } from "./updateProfile.schema";
const log = logger.getSubLogger({ prefix: ["updateProfile"] });
@@ -42,7 +41,7 @@ export const updateProfileHandler = async ({ ctx, input }: UpdateProfileOptions)
const locale = input.locale || user.locale;
const emailVerification = await getFeatureFlag(prisma, "email-verification");
const { travelSchedules, ...rest } = input;
const { travelSchedules, availabilityIds, ...rest } = input;
const secondaryEmails = input?.secondaryEmails || [];
delete input.secondaryEmails;
@@ -257,24 +256,11 @@ export const updateProfileHandler = async ({ ctx, input }: UpdateProfileOptions)
}
if (user.timeZone !== data.timeZone && updatedUser.schedules.length > 0) {
// on timezone change update timezone of default schedule
const defaultScheduleId = await getDefaultScheduleId(user.id, prisma);
if (!user.defaultScheduleId) {
// set default schedule if not already set
await prisma.user.update({
where: {
id: user.id,
},
data: {
defaultScheduleId,
},
});
}
await prisma.schedule.updateMany({
where: {
id: defaultScheduleId,
id: {
in: input.availabilityIds,
},
},
data: {
timeZone: data.timeZone,
@@ -15,6 +15,7 @@ export const ZUpdateProfileInputSchema = z.object({
bio: z.string().optional(),
avatarUrl: z.string().nullable().optional(),
timeZone: z.string().optional(),
availabilityIds: z.array(z.number()).optional(),
weekStart: z.string().optional(),
hideBranding: z.boolean().optional(),
allowDynamicBooking: z.boolean().optional(),