Bulk edit locations when default conferencing app is set (#7520)

* Bulk edit default locations

* Update [category]

* Open modal on none link events

---------

Co-authored-by: Alex van Andel <me@alexvanandel.com>
This commit is contained in:
sean-brydon 2023-03-06 18:53:35 +08:00 committed by GitHub
parent c8956680ad
commit 8a9b985760
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 217 additions and 16 deletions

View File

@ -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
<AppSetDefaultLinkDailog
locationType={locationType}
setLocationType={() => setLocationType(undefined)}
onSuccess={onSuccessCallback}
/>
)}
{bulkUpdateModal && (
<BulkEditDefaultConferencingModal open={bulkUpdateModal} setOpen={setBulkUpdateModal} />
)}
</>
);
};

View File

@ -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 = () => {
</Dialog>
{locationType && (
<AppSetDefaultLinkDailog locationType={locationType} setLocationType={setLocationType} />
<AppSetDefaultLinkDailog
locationType={locationType}
setLocationType={setLocationType}
onSuccess={onSuccessCallback}
/>
)}
{bulkUpdateModal && (
<BulkEditDefaultConferencingModal open={bulkUpdateModal} setOpen={setBulkUpdateModal} />
)}
</div>
);

View File

@ -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"
}

View File

@ -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;

View File

@ -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<SetStateAction<(EventLocationType & { slug: string }) | undefined>>;
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");

View File

@ -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 (
<Dialog name="Bulk Default Location Update" open={props.open} onOpenChange={props.setOpen}>
<DialogContent
type="creation"
title={t("default_conferncing_bulk_title")}
description={t("default_conferncing_bulk_description")}>
<Form
form={form}
handleSubmit={(values) => {
updateLocationsMutation.mutate(values);
}}>
<div className="flex flex-col space-y-2">
{data.eventTypes.length > 0 && (
<div className="flex items-center space-x-2 rounded-md py-2.5 px-3">
<label className="w-full text-sm font-medium leading-none text-gray-900">
<input
type="checkbox"
className="text-primary-600 focus:ring-primary-500 h-4 w-4 rounded border-gray-300 checked:bg-gray-800 hover:bg-gray-100 ltr:mr-2 rtl:ml-2"
onChange={(e) => {
form.setValue("eventTypeIds", e.target.checked ? data.eventTypes.map((e) => e.id) : []);
}}
checked={eventTypesSelected.length === data.eventTypes.length}
/>
{t("select_all")}
</label>
</div>
)}
{data.eventTypes.map((eventType) => (
<div
key={eventType.id}
className="flex items-center space-x-2 rounded-md bg-gray-50 py-2.5 px-3">
<label className="w-full text-sm font-medium leading-none text-gray-900">
<input
type="checkbox"
checked={eventTypesSelected.includes(eventType.id)}
className="text-primary-600 focus:ring-primary-500 h-4 w-4 rounded border-gray-300 checked:bg-gray-800 hover:bg-gray-100 ltr:mr-2 rtl:ml-2"
onChange={(e) => {
form.setValue(
"eventTypeIds",
e.target.checked
? [...eventTypesSelected, eventType.id]
: eventTypesSelected.filter((id) => id !== eventType.id)
);
}}
/>
{eventType.title}
</label>
<div className="ml-auto flex h-4 w-4 items-center">
<img src={eventType.logo} alt="#" />
</div>
</div>
))}
</div>
<DialogFooter>
<DialogClose
onClick={() => {
utils.viewer.getUsersDefaultConferencingApp.invalidate();
}}
/>
<Button type="submit" color="primary" loading={updateLocationsMutation.isLoading}>
{t("update")}
</Button>
</DialogFooter>
</Form>
</DialogContent>
</Dialog>
);
}

View File

@ -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<typeof EventTypeUpdateInput>["bookingFields"]) {