diff --git a/apps/web/components/apps/AppList.tsx b/apps/web/components/apps/AppList.tsx index 55206c8f04..16567fb815 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 { 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([]); + 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 && ( - { + 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} /> )} diff --git a/apps/web/pages/availability/index.tsx b/apps/web/pages/availability/index.tsx index 569b47fbcf..faf236e29f 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 { 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([]); 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 {bulkUpdateModal && ( - { + bulkUpdateDefaultAvailabilityMutation.mutate({ + eventTypeIds, + }); + }} /> )} diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index b18fbaa399..57f3e50ea0 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -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", diff --git a/packages/features/eventtypes/components/BulkEditDefaultForEventsModal.tsx b/packages/features/eventtypes/components/BulkEditDefaultForEventsModal.tsx deleted file mode 100644 index 76b18b985c..0000000000 --- a/packages/features/eventtypes/components/BulkEditDefaultForEventsModal.tsx +++ /dev/null @@ -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 ( - - -
{ - 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 new file mode 100644 index 0000000000..6d4223d4d7 --- /dev/null +++ b/packages/features/eventtypes/components/BulkEditDefaultModal.tsx @@ -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>; + 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 853f85b9a1..985ba8bc3f 100644 --- a/packages/features/settings/TimezoneChangeDialog.tsx +++ b/packages/features/settings/TimezoneChangeDialog.tsx @@ -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 ( onCancel([3, "months"], true)} color="secondary"> {t("dont_update")} - updateTimezone()} color="primary"> + onAction("update")} color="primary"> {t("update_timezone")} @@ -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([]); + 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)} /> + { + setOpen(false); + setSelectAvailability(true); + }} + /> + { + mutation.mutate({ + timeZone: browserTimezone, + availabilityIds: ids, + }); + }} + /> ); } diff --git a/packages/trpc/server/routers/loggedInViewer/updateProfile.handler.ts b/packages/trpc/server/routers/loggedInViewer/updateProfile.handler.ts index 20351a9a75..2950629ded 100644 --- a/packages/trpc/server/routers/loggedInViewer/updateProfile.handler.ts +++ b/packages/trpc/server/routers/loggedInViewer/updateProfile.handler.ts @@ -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, diff --git a/packages/trpc/server/routers/loggedInViewer/updateProfile.schema.ts b/packages/trpc/server/routers/loggedInViewer/updateProfile.schema.ts index ab9a4a7a20..e1e873e842 100644 --- a/packages/trpc/server/routers/loggedInViewer/updateProfile.schema.ts +++ b/packages/trpc/server/routers/loggedInViewer/updateProfile.schema.ts @@ -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(),