diff --git a/apps/web/components/apps/AppList.tsx b/apps/web/components/apps/AppList.tsx index 16567fb815..55206c8f04 100644 --- a/apps/web/components/apps/AppList.tsx +++ b/apps/web/components/apps/AppList.tsx @@ -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 { BulkEditDefaultModal } from "@calcom/features/eventtypes/components/BulkEditDefaultModal"; +import { BulkEditDefaultForEventsModal } from "@calcom/features/eventtypes/components/BulkEditDefaultForEventsModal"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import type { AppCategories } from "@calcom/prisma/enums"; import { trpc, type RouterOutputs } from "@calcom/trpc"; @@ -153,8 +153,6 @@ export const AppList = ({ data, handleDisconnect, variant, listClassName }: AppL }); const { t } = useLocale(); - const [eventTypeIds, setEventTypeIds] = useState([]); - const { data: bulkEventTypes } = trpc.viewer.eventTypes.bulkEventFetch.useQuery(); const updateLocationsMutation = trpc.viewer.eventTypes.bulkUpdateToDefaultLocation.useMutation({ onSuccess: () => { utils.viewer.getUsersDefaultConferencingApp.invalidate(); @@ -180,20 +178,11 @@ export const AppList = ({ data, handleDisconnect, variant, listClassName }: AppL )} {bulkUpdateModal && ( - { - updateLocationsMutation.mutate({ - eventTypeIds, - }); - }} + )} diff --git a/apps/web/pages/availability/index.tsx b/apps/web/pages/availability/index.tsx index faf236e29f..569b47fbcf 100644 --- a/apps/web/pages/availability/index.tsx +++ b/apps/web/pages/availability/index.tsx @@ -3,7 +3,7 @@ import Link from "next/link"; import { useRouter, usePathname } from "next/navigation"; import { useCallback, useState } from "react"; -import { BulkEditDefaultModal } from "@calcom/features/eventtypes/components/BulkEditDefaultModal"; +import { BulkEditDefaultForEventsModal } from "@calcom/features/eventtypes/components/BulkEditDefaultForEventsModal"; import { NewScheduleButton, ScheduleListItem } from "@calcom/features/schedules"; import Shell from "@calcom/features/shell/Shell"; import { AvailabilitySliderTable } from "@calcom/features/timezone-buddy/components/AvailabilitySliderTable"; @@ -24,14 +24,12 @@ 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([]); 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(); @@ -145,20 +143,11 @@ export function AvailabilityList({ schedules }: RouterOutputs["viewer"]["availab {bulkUpdateModal && ( - { - bulkUpdateDefaultAvailabilityMutation.mutate({ - eventTypeIds, - }); - }} + bulkUpdateFunction={bulkUpdateDefaultAvailabilityMutation.mutate} /> )} diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 1921edaf93..14fa65d5e0 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -1928,14 +1928,12 @@ "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", diff --git a/packages/features/eventtypes/components/BulkEditDefaultForEventsModal.tsx b/packages/features/eventtypes/components/BulkEditDefaultForEventsModal.tsx new file mode 100644 index 0000000000..76b18b985c --- /dev/null +++ b/packages/features/eventtypes/components/BulkEditDefaultForEventsModal.tsx @@ -0,0 +1,90 @@ +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 ( + + +
{ + props.bulkUpdateFunction(values); + }}> +
+ {data.eventTypes.length > 0 && ( +
+ { + form.setValue("eventTypeIds", e.target.checked ? data.eventTypes.map((e) => e.id) : []); + }} + checked={eventTypesSelected.length === data.eventTypes.length} + /> +
+ )} + {data.eventTypes.map((eventType) => ( +
+ { + form.setValue( + "eventTypeIds", + e.target.checked + ? [...eventTypesSelected, eventType.id] + : eventTypesSelected.filter((id) => id !== eventType.id) + ); + }} + /> +
+ ))} +
+ + { + utils.viewer.getUsersDefaultConferencingApp.invalidate(); + }} + /> + + +
+
+
+ ); +} diff --git a/packages/features/eventtypes/components/BulkEditDefaultModal.tsx b/packages/features/eventtypes/components/BulkEditDefaultModal.tsx deleted file mode 100644 index 6d4223d4d7..0000000000 --- a/packages/features/eventtypes/components/BulkEditDefaultModal.tsx +++ /dev/null @@ -1,63 +0,0 @@ -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>; - 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 ( - - -
- {data.length > 0 && ( -
- { - setIds(e.target.checked ? data.map((e) => e.id) : []); - }} - checked={ids.length === data.length} - /> -
- )} - {data.map((item) => ( -
- { - setIds(e.target.checked ? [...ids, item.id] : ids.filter((id) => id !== item.id)); - }} - /> -
- ))} -
- - - - -
-
- ); -} diff --git a/packages/features/settings/TimezoneChangeDialog.tsx b/packages/features/settings/TimezoneChangeDialog.tsx index 985ba8bc3f..853f85b9a1 100644 --- a/packages/features/settings/TimezoneChangeDialog.tsx +++ b/packages/features/settings/TimezoneChangeDialog.tsx @@ -4,7 +4,6 @@ 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"; @@ -17,6 +16,7 @@ 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,6 +28,28 @@ 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 ( onCancel([3, "months"], true)} color="secondary"> {t("dont_update")} - onAction("update")} color="primary"> + updateTimezone()} color="primary"> {t("update_timezone")} @@ -82,56 +104,9 @@ 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([]); - 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 ( - { - setOpen(false); - setSelectAvailability(true); - }} - /> - { - mutation.mutate({ - timeZone: browserTimezone, - availabilityIds: ids, - }); - }} - /> + setOpen(false)} /> ); } diff --git a/packages/trpc/server/routers/loggedInViewer/updateProfile.handler.ts b/packages/trpc/server/routers/loggedInViewer/updateProfile.handler.ts index 2950629ded..20351a9a75 100644 --- a/packages/trpc/server/routers/loggedInViewer/updateProfile.handler.ts +++ b/packages/trpc/server/routers/loggedInViewer/updateProfile.handler.ts @@ -24,6 +24,7 @@ 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"] }); @@ -41,7 +42,7 @@ export const updateProfileHandler = async ({ ctx, input }: UpdateProfileOptions) const locale = input.locale || user.locale; const emailVerification = await getFeatureFlag(prisma, "email-verification"); - const { travelSchedules, availabilityIds, ...rest } = input; + const { travelSchedules, ...rest } = input; const secondaryEmails = input?.secondaryEmails || []; delete input.secondaryEmails; @@ -256,11 +257,24 @@ 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: { - in: input.availabilityIds, - }, + id: defaultScheduleId, }, data: { timeZone: data.timeZone, diff --git a/packages/trpc/server/routers/loggedInViewer/updateProfile.schema.ts b/packages/trpc/server/routers/loggedInViewer/updateProfile.schema.ts index e1e873e842..ab9a4a7a20 100644 --- a/packages/trpc/server/routers/loggedInViewer/updateProfile.schema.ts +++ b/packages/trpc/server/routers/loggedInViewer/updateProfile.schema.ts @@ -15,7 +15,6 @@ 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(),