diff --git a/apps/web/pages/apps/installed/[category].tsx b/apps/web/pages/apps/installed/[category].tsx index 64913c79c7..45a154ca0d 100644 --- a/apps/web/pages/apps/installed/[category].tsx +++ b/apps/web/pages/apps/installed/[category].tsx @@ -1,5 +1,5 @@ import { useRouter } from "next/router"; -import { useReducer, useState } from "react"; +import { useCallback, useReducer, useState } from "react"; import z from "zod"; import { AppSettings } from "@calcom/app-store/_components/AppSettings"; @@ -9,6 +9,7 @@ import { getEventLocationTypeFromApp } from "@calcom/app-store/locations"; import { InstalledAppVariants } from "@calcom/app-store/utils"; import { AppSetDefaultLinkDailog } from "@calcom/features/apps/components/AppSetDefaultLinkDialog"; import DisconnectIntegrationModal from "@calcom/features/apps/components/DisconnectIntegrationModal"; +import { BulkEditDefaultConferencingModal } from "@calcom/features/eventtypes/components/BulkEditDefaultConferencingModal"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import type { RouterOutputs } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react"; @@ -113,11 +114,16 @@ interface IntegrationsListProps { const IntegrationsList = ({ data, handleDisconnect, variant }: IntegrationsListProps) => { const { data: defaultConferencingApp } = trpc.viewer.getUsersDefaultConferencingApp.useQuery(); const utils = trpc.useContext(); - + const [bulkUpdateModal, setBulkUpdateModal] = useState(false); const [locationType, setLocationType] = useState<(EventLocationType & { slug: string }) | undefined>( undefined ); + const onSuccessCallback = useCallback(() => { + setBulkUpdateModal(true); + showToast("Default app updated successfully", "success"); + }, []); + const updateDefaultAppMutation = trpc.viewer.updateUserDefaultConferencingApp.useMutation({ onSuccess: () => { showToast("Default app updated successfully", "success"); @@ -172,6 +178,7 @@ const IntegrationsList = ({ data, handleDisconnect, variant }: IntegrationsListP updateDefaultAppMutation.mutate({ appSlug, }); + setBulkUpdateModal(true); } }}> {t("change_default_conferencing_app")} @@ -199,8 +206,13 @@ const IntegrationsList = ({ data, handleDisconnect, variant }: IntegrationsListP setLocationType(undefined)} + onSuccess={onSuccessCallback} /> )} + + {bulkUpdateModal && ( + + )} ); }; diff --git a/apps/web/pages/settings/my-account/conferencing.tsx b/apps/web/pages/settings/my-account/conferencing.tsx index 3e14dbef27..7c975827f8 100644 --- a/apps/web/pages/settings/my-account/conferencing.tsx +++ b/apps/web/pages/settings/my-account/conferencing.tsx @@ -1,8 +1,9 @@ -import { useState } from "react"; +import { useCallback, useState } from "react"; import type { EventLocationType } from "@calcom/app-store/locations"; import { getEventLocationTypeFromApp } from "@calcom/app-store/locations"; import { AppSetDefaultLinkDailog } from "@calcom/features/apps/components/AppSetDefaultLinkDialog"; +import { BulkEditDefaultConferencingModal } from "@calcom/features/eventtypes/components/BulkEditDefaultConferencingModal"; import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { trpc } from "@calcom/trpc/react"; @@ -62,17 +63,20 @@ const ConferencingLayout = () => { }, }); + const onSuccessCallback = useCallback(() => { + setBulkUpdateModal(true); + showToast("Default app updated successfully", "success"); + }, []); + const updateDefaultAppMutation = trpc.viewer.updateUserDefaultConferencingApp.useMutation({ - onSuccess: () => { - showToast("Default app updated successfully", "success"); - utils.viewer.getUsersDefaultConferencingApp.invalidate(); - }, + onSuccess: onSuccessCallback, onError: (error) => { showToast(`Error: ${error.message}`, "error"); }, }); const [deleteAppModal, setDeleteAppModal] = useState(false); + const [bulkUpdateModal, setBulkUpdateModal] = useState(false); const [locationType, setLocationType] = useState<(EventLocationType & { slug: string }) | undefined>( undefined ); @@ -167,7 +171,14 @@ const ConferencingLayout = () => { {locationType && ( - + + )} + {bulkUpdateModal && ( + )} ); diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 546c370113..5298332ce3 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -1634,5 +1634,9 @@ "no_responses_yet": "No responses yet", "this_will_be_the_placeholder": "This will be the placeholder", "verification_code": "Verification code", - "verify": "Verify" + "verify": "Verify", + "select_all":"Select All", + "default_conferncing_bulk_title":"Bulk update existing event types", + "default_conferncing_bulk_description":"Update the locations for the selected event types" + } diff --git a/packages/app-store/utils.ts b/packages/app-store/utils.ts index e0d113eba3..579c52991d 100644 --- a/packages/app-store/utils.ts +++ b/packages/app-store/utils.ts @@ -189,4 +189,8 @@ export function getAppFromSlug(slug: string | undefined): AppMeta | undefined { return ALL_APPS.find((app) => app.slug === slug); } +export function getAppFromLocationValue(type: string): AppMeta | undefined { + return ALL_APPS.find((app) => app?.appData?.location?.type === type); +} + export default getApps; diff --git a/packages/features/apps/components/AppSetDefaultLinkDialog.tsx b/packages/features/apps/components/AppSetDefaultLinkDialog.tsx index 4179cb5997..0fc389c46c 100644 --- a/packages/features/apps/components/AppSetDefaultLinkDialog.tsx +++ b/packages/features/apps/components/AppSetDefaultLinkDialog.tsx @@ -1,9 +1,10 @@ import { zodResolver } from "@hookform/resolvers/zod"; -import { Dispatch, SetStateAction } from "react"; +import type { Dispatch, SetStateAction } from "react"; import { useForm } from "react-hook-form"; import { z } from "zod"; -import { EventLocationType, getEventLocationType } from "@calcom/app-store/locations"; +import type { EventLocationType } from "@calcom/app-store/locations"; +import { getEventLocationType } from "@calcom/app-store/locations"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { trpc } from "@calcom/trpc/react"; import { @@ -26,9 +27,11 @@ type LocationTypeSetLinkDialogFormProps = { export function AppSetDefaultLinkDailog({ locationType, setLocationType, + onSuccess, }: { locationType: EventLocationType & { slug: string }; setLocationType: Dispatch>; + onSuccess: () => void; }) { const utils = trpc.useContext(); @@ -43,8 +46,7 @@ export function AppSetDefaultLinkDailog({ const updateDefaultAppMutation = trpc.viewer.updateUserDefaultConferencingApp.useMutation({ onSuccess: () => { - showToast("Default app updated successfully", "success"); - utils.viewer.getUsersDefaultConferencingApp.invalidate(); + onSuccess(); }, onError: () => { showToast(`Invalid App Link Format`, "error"); diff --git a/packages/features/eventtypes/components/BulkEditDefaultConferencingModal.tsx b/packages/features/eventtypes/components/BulkEditDefaultConferencingModal.tsx new file mode 100644 index 0000000000..290d4f51fc --- /dev/null +++ b/packages/features/eventtypes/components/BulkEditDefaultConferencingModal.tsx @@ -0,0 +1,103 @@ +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 } from "@calcom/ui"; + +export const BulkUpdateEventSchema = z.object({ + eventTypeIds: z.array(z.number()), +}); + +export function BulkEditDefaultConferencingModal(props: { open: boolean; setOpen: (open: boolean) => void }) { + const { t } = useLocale(); + const utils = trpc.useContext(); + const { data, isFetching } = trpc.viewer.eventTypes.bulkEventFetch.useQuery(); + const form = useForm({ + resolver: zodResolver(BulkUpdateEventSchema), + defaultValues: { + eventTypeIds: data?.eventTypes.map((e) => e.id) ?? [], + }, + }); + + const updateLocationsMutation = trpc.viewer.eventTypes.bulkUpdateToDefaultLocation.useMutation({ + onSuccess: () => { + utils.viewer.getUsersDefaultConferencingApp.invalidate(); + props.setOpen(false); + }, + }); + + const eventTypesSelected = form.watch("eventTypeIds"); + + if (isFetching || !open || !data?.eventTypes) return null; + + return ( + + +
{ + updateLocationsMutation.mutate(values); + }}> +
+ {data.eventTypes.length > 0 && ( +
+ +
+ )} + {data.eventTypes.map((eventType) => ( +
+ + +
+ # +
+
+ ))} +
+ + { + utils.viewer.getUsersDefaultConferencingApp.invalidate(); + }} + /> + + +
+
+
+ ); +} diff --git a/packages/trpc/server/routers/viewer/eventTypes.ts b/packages/trpc/server/routers/viewer/eventTypes.ts index 6c095bccbf..b1d6abbf64 100644 --- a/packages/trpc/server/routers/viewer/eventTypes.ts +++ b/packages/trpc/server/routers/viewer/eventTypes.ts @@ -1,20 +1,21 @@ import { MembershipRole, PeriodType, Prisma, SchedulingType } from "@prisma/client"; -import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library"; +import { PrismaClientKnownRequestError } from "@prisma/client/runtime"; // REVIEW: From lint error import _ from "lodash"; import { z } from "zod"; import getAppKeysFromSlug from "@calcom/app-store/_utils/getAppKeysFromSlug"; +import type { LocationObject } from "@calcom/app-store/locations"; import { DailyLocationType } from "@calcom/app-store/locations"; import { stripeDataSchema } from "@calcom/app-store/stripepayment/lib/server"; -import getApps from "@calcom/app-store/utils"; -import { updateEvent } from "@calcom/core/CalendarManager"; +import getApps, { getAppFromLocationValue, getAppFromSlug } from "@calcom/app-store/utils"; import { validateBookingLimitOrder } from "@calcom/lib"; import { CAL_URL } from "@calcom/lib/constants"; import getEventTypeById from "@calcom/lib/getEventTypeById"; import { baseEventTypeSelect, baseUserSelect } from "@calcom/prisma"; import { _DestinationCalendarModel, _EventTypeModel } from "@calcom/prisma/zod"; import type { CustomInputSchema } from "@calcom/prisma/zod-utils"; +import { eventTypeLocations as eventTypeLocationsSchema } from "@calcom/prisma/zod-utils"; import { customInputSchema, EventTypeMetaDataSchema, @@ -829,6 +830,70 @@ export const eventTypesRouter = router({ throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" }); } }), + bulkEventFetch: authedProcedure.query(async ({ ctx }) => { + const eventTypes = await ctx.prisma.eventType.findMany({ + where: { + userId: ctx.user.id, + team: null, + }, + select: { + id: true, + title: true, + locations: true, + }, + }); + + const eventTypesWithLogo = eventTypes.map((eventType) => { + const locationParsed = eventTypeLocationsSchema.parse(eventType.locations); + const app = getAppFromLocationValue(locationParsed[0].type); + return { + ...eventType, + logo: app?.logo, + }; + }); + + return { + eventTypes: eventTypesWithLogo, + }; + }), + bulkUpdateToDefaultLocation: authedProcedure + .input( + z.object({ + eventTypeIds: z.array(z.number()), + }) + ) + .mutation(async ({ ctx, input }) => { + const { eventTypeIds } = input; + const defaultApp = userMetadataSchema.parse(ctx.user.metadata)?.defaultConferencingApp; + + if (!defaultApp) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: "Default conferencing app not set", + }); + } + + const foundApp = getAppFromSlug(defaultApp.appSlug); + const appType = foundApp?.appData?.location?.type; + if (!appType) { + throw new TRPCError({ + code: "BAD_REQUEST", + message: `Default conferencing app '${defaultApp.appSlug}' doesnt exist.`, + }); + } + + return await ctx.prisma.eventType.updateMany({ + where: { + id: { + in: eventTypeIds, + }, + userId: ctx.user.id, + }, + data: { + locations: [{ type: appType, link: defaultApp.appLink }] as LocationObject[], + }, + }); + }), }); function ensureUniqueBookingFields(fields: z.infer["bookingFields"]) {