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:
parent
c8956680ad
commit
8a9b985760
|
@ -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} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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"
|
||||
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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"]) {
|
||||
|
|
Loading…
Reference in New Issue
Block a user