From 66b3e735d87f62975cb0cd485a699e91b6e50fdf Mon Sep 17 00:00:00 2001 From: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com> Date: Tue, 14 Jan 2025 18:17:16 -0500 Subject: [PATCH] feat: Routing form submitted but no booking - Salesforce actions (#18616) * Add booking incomplete actions table * Add incomplete booking page tab * Add `getincompleteBookingSettings` trpc endpoints * Add incomplete booking page * Abstract enabled apps array * Handle no enabled credentials and no actions * Add enabled field to incomplete booking action db record * Add new write record entries * UI add separation between switch and inputs * Fix typo * clean up * Add saveIncompleteBookingSettings endpoint * Save incomplete booking settings * Fix language around when to write to field * Add `credentialId` to action record * Choose which credential to assign to action * Save credential to action * Revert "Save credential to action" This reverts commit ba6a1c808baed54f6d3d4c6155cf192c813d0696. * Revert "Choose which credential to assign to action" This reverts commit 968f6e5295d098c64abe95268afd1fa139a175ba. * Revert "Add `credentialId` to action record" This reverts commit 579f9ff4167b65aa987659e4e8f8c26f9e773317. * Add credentialId to action record - rewrite migration file * Revert "Add credentialId to action record - rewrite migration file" This reverts commit 2843a92c61820e7f7a1614a557d3f7ea96342107. * Revert "Add booking incomplete actions table" This reverts commit 7ec75bef4a0f0a9d07be0142da64c49d739439ea. * Revert "Add enabled field to incomplete booking action db record" This reverts commit d279a1da05819eafa8fc5d664e3be733e0687e8d. * Write migration in single commit * Rename table * Rename table - remove underscores * Remove credential relationship * Type fix - changing table name * Fix table name * Change writeToRecordObject to object * Salesforce add incomplete booking, write to record * Add incomplete booking actions to `triggerFormSubmittedNoEventWebhooks` * Remove console.log * Type fixes * Type fixes * Iterate if incompleteBookingActions * Choose which credential to assign to action * Save credential to action * Fix getServerSideProp changes * Type fix * Type fix * Type fix * Type fix --------- Co-authored-by: Alex van Andel --- apps/web/public/static/locales/en/common.json | 1 + .../components/RoutingNavBar.tsx | 4 + .../lib/enabledIncompleteBookingApps.ts | 1 + .../incompleteBooking/actionDataSchemas.ts | 10 + .../lib/incompleteBooking/actionFunctions.ts | 13 + .../pages/app-routing-server.config.ts | 1 + .../pages/app-routing.config.tsx | 2 + .../incomplete-booking/[...appPages].tsx | 329 ++++++++++++++++++ .../app-store/routing-forms/trpc/_router.ts | 22 ++ .../getIncompleteBookingSettings.handler.ts | 96 +++++ .../getIncompleteBookingSettings.schema.ts | 9 + .../saveIncompleteBookingSettings.handler.ts | 78 +++++ .../saveIncompleteBookingSettings.schema.ts | 15 + .../app-store/salesforce/lib/CrmService.ts | 61 +++- .../routingForm/incompleteBookingAction.ts | 33 ++ packages/app-store/salesforce/zod.ts | 17 +- .../triggerFormSubmittedNoEventWebhook.ts | 23 +- .../migration.sql | 17 + packages/prisma/schema.prisma | 45 ++- 19 files changed, 756 insertions(+), 21 deletions(-) create mode 100644 packages/app-store/routing-forms/lib/enabledIncompleteBookingApps.ts create mode 100644 packages/app-store/routing-forms/lib/incompleteBooking/actionDataSchemas.ts create mode 100644 packages/app-store/routing-forms/lib/incompleteBooking/actionFunctions.ts create mode 100644 packages/app-store/routing-forms/pages/incomplete-booking/[...appPages].tsx create mode 100644 packages/app-store/routing-forms/trpc/getIncompleteBookingSettings.handler.ts create mode 100644 packages/app-store/routing-forms/trpc/getIncompleteBookingSettings.schema.ts create mode 100644 packages/app-store/routing-forms/trpc/saveIncompleteBookingSettings.handler.ts create mode 100644 packages/app-store/routing-forms/trpc/saveIncompleteBookingSettings.schema.ts create mode 100644 packages/app-store/salesforce/lib/routingForm/incompleteBookingAction.ts create mode 100644 packages/prisma/migrations/20250113170733_add_routingforms_incompletebooking_action_table/migration.sql diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 8133ae2c6e..51958ad442 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -2926,5 +2926,6 @@ "managed_users": "Managed Users", "managed_users_description": "See all the managed users created by your OAuth client", "select_oAuth_client": "Select Oauth Client", + "on_every_instance": "On every instance", "ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑" } diff --git a/packages/app-store/routing-forms/components/RoutingNavBar.tsx b/packages/app-store/routing-forms/components/RoutingNavBar.tsx index 3c02afd411..57276c8fd0 100644 --- a/packages/app-store/routing-forms/components/RoutingNavBar.tsx +++ b/packages/app-store/routing-forms/components/RoutingNavBar.tsx @@ -38,6 +38,10 @@ export default function RoutingNavBar({ target: "_blank", href: `${appUrl}/reporting/${form?.id}`, }, + { + name: "Incomplete Booking", + href: `${appUrl}/incomplete-booking/${form?.id}`, + }, ]; return (
diff --git a/packages/app-store/routing-forms/lib/enabledIncompleteBookingApps.ts b/packages/app-store/routing-forms/lib/enabledIncompleteBookingApps.ts new file mode 100644 index 0000000000..5aee6237c1 --- /dev/null +++ b/packages/app-store/routing-forms/lib/enabledIncompleteBookingApps.ts @@ -0,0 +1 @@ +export const enabledIncompleteBookingApps = ["salesforce"]; diff --git a/packages/app-store/routing-forms/lib/incompleteBooking/actionDataSchemas.ts b/packages/app-store/routing-forms/lib/incompleteBooking/actionDataSchemas.ts new file mode 100644 index 0000000000..291e8e1d5f --- /dev/null +++ b/packages/app-store/routing-forms/lib/incompleteBooking/actionDataSchemas.ts @@ -0,0 +1,10 @@ +import type { z } from "zod"; + +import { routingFormIncompleteBookingDataSchema as salesforceRoutingFormIncompleteBookingDataSchema } from "@calcom/app-store/salesforce/zod"; +import { IncompleteBookingActionType } from "@calcom/prisma/enums"; + +const incompleteBookingActionDataSchemas: Record> = { + [IncompleteBookingActionType.SALESFORCE]: salesforceRoutingFormIncompleteBookingDataSchema, +}; + +export default incompleteBookingActionDataSchemas; diff --git a/packages/app-store/routing-forms/lib/incompleteBooking/actionFunctions.ts b/packages/app-store/routing-forms/lib/incompleteBooking/actionFunctions.ts new file mode 100644 index 0000000000..7758b68dba --- /dev/null +++ b/packages/app-store/routing-forms/lib/incompleteBooking/actionFunctions.ts @@ -0,0 +1,13 @@ +import type { App_RoutingForms_IncompleteBookingActions } from "@prisma/client"; + +import { incompleteBookingAction as salesforceIncompleteBookingAction } from "@calcom/app-store/salesforce/lib/routingForm/incompleteBookingAction"; +import { IncompleteBookingActionType } from "@calcom/prisma/enums"; + +const incompleteBookingActionFunctions: Record< + IncompleteBookingActionType, + (action: App_RoutingForms_IncompleteBookingActions, email: string) => void +> = { + [IncompleteBookingActionType.SALESFORCE]: salesforceIncompleteBookingAction, +}; + +export default incompleteBookingActionFunctions; diff --git a/packages/app-store/routing-forms/pages/app-routing-server.config.ts b/packages/app-store/routing-forms/pages/app-routing-server.config.ts index e0a85fdb75..a80ce55813 100644 --- a/packages/app-store/routing-forms/pages/app-routing-server.config.ts +++ b/packages/app-store/routing-forms/pages/app-routing-server.config.ts @@ -10,4 +10,5 @@ export const routingServerSidePropsConfig: Record "route-builder": getServerSidePropsSingleForm, "routing-link": getServerSidePropsRoutingLink, reporting: getServerSidePropsSingleForm, + "incomplete-booking": getServerSidePropsSingleForm, }; diff --git a/packages/app-store/routing-forms/pages/app-routing.config.tsx b/packages/app-store/routing-forms/pages/app-routing.config.tsx index 65749ef193..f54eb9d1be 100644 --- a/packages/app-store/routing-forms/pages/app-routing.config.tsx +++ b/packages/app-store/routing-forms/pages/app-routing.config.tsx @@ -1,6 +1,7 @@ //TODO: Generate this file automatically so that like in Next.js file based routing can work automatically import * as formEdit from "./form-edit/[...appPages]"; import * as forms from "./forms/[...appPages]"; +import * as IncompleteBooking from "./incomplete-booking/[...appPages]"; import * as LayoutHandler from "./layout-handler/[...appPages]"; import * as Reporting from "./reporting/[...appPages]"; import * as RouteBuilder from "./route-builder/[...appPages]"; @@ -13,6 +14,7 @@ const routingConfig = { "routing-link": RoutingLink, reporting: Reporting, layoutHandler: LayoutHandler, + "incomplete-booking": IncompleteBooking, }; export default routingConfig; diff --git a/packages/app-store/routing-forms/pages/incomplete-booking/[...appPages].tsx b/packages/app-store/routing-forms/pages/incomplete-booking/[...appPages].tsx new file mode 100644 index 0000000000..cda51e3a98 --- /dev/null +++ b/packages/app-store/routing-forms/pages/incomplete-booking/[...appPages].tsx @@ -0,0 +1,329 @@ +import { useState, useEffect } from "react"; +import type z from "zod"; + +import { WhenToWriteToRecord, SalesforceFieldType } from "@calcom/app-store/salesforce/lib/enums"; +import type { writeToRecordDataSchema as salesforceWriteToRecordDataSchema } from "@calcom/app-store/salesforce/zod"; +import { routingFormIncompleteBookingDataSchema as salesforceRoutingFormIncompleteBookingDataSchema } from "@calcom/app-store/salesforce/zod"; +import Shell from "@calcom/features/shell/Shell"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { IncompleteBookingActionType } from "@calcom/prisma/enums"; +import { trpc } from "@calcom/trpc/react"; +import type { inferSSRProps } from "@calcom/types/inferSSRProps"; +import { Switch, InputField, Button, Select, showToast } from "@calcom/ui"; + +import SingleForm, { + getServerSidePropsForSingleFormView as getServerSideProps, +} from "../../components/SingleForm"; +import type { RoutingFormWithResponseCount } from "../../components/SingleForm"; +import { enabledIncompleteBookingApps } from "../../lib/enabledIncompleteBookingApps"; + +function Page({ form }: { form: RoutingFormWithResponseCount }) { + const { t } = useLocale(); + const { data, isLoading } = trpc.viewer.appRoutingForms.getIncompleteBookingSettings.useQuery({ + formId: form.id, + }); + + const mutation = trpc.viewer.appRoutingForms.saveIncompleteBookingSettings.useMutation({ + onSuccess: () => { + showToast(t("success"), "success"); + }, + onError: (error) => { + showToast(t(`error: ${error.message}`), "error"); + }, + }); + + const [salesforceWriteToRecordObject, setSalesforceWriteToRecordObject] = useState< + z.infer + >({}); + + // Handle just Salesforce for now but need to expand this to other apps + const [salesforceActionEnabled, setSalesforceActionEnabled] = useState(false); + + const fieldTypeOptions = [{ label: t("text"), value: SalesforceFieldType.TEXT }]; + + const [selectedFieldType, setSelectedFieldType] = useState(fieldTypeOptions[0]); + + const whenToWriteToRecordOptions = [ + { label: t("on_every_instance"), value: WhenToWriteToRecord.EVERY_BOOKING }, + { label: t("only_if_field_is_empty"), value: WhenToWriteToRecord.FIELD_EMPTY }, + ]; + + const [selectedWhenToWrite, setSelectedWhenToWrite] = useState(whenToWriteToRecordOptions[0]); + + const [newSalesforceAction, setNewSalesforceAction] = useState({ + field: "", + fieldType: selectedFieldType.value, + value: "", + whenToWrite: WhenToWriteToRecord.FIELD_EMPTY, + }); + + const credentialOptions = data?.credentials.map((credential) => ({ + label: credential.team?.name, + value: credential.id, + })); + + const [selectedCredential, setSelectedCredential] = useState( + Array.isArray(credentialOptions) ? credentialOptions[0] : null + ); + + useEffect(() => { + const salesforceAction = data?.incompleteBookingActions.find( + (action) => action.actionType === IncompleteBookingActionType.SALESFORCE + ); + + if (salesforceAction) { + setSalesforceActionEnabled(salesforceAction.enabled); + + const parsedSalesforceActionData = salesforceRoutingFormIncompleteBookingDataSchema.safeParse( + salesforceAction.data + ); + if (parsedSalesforceActionData.success) { + setSalesforceWriteToRecordObject(parsedSalesforceActionData.data?.writeToRecordObject ?? {}); + } + + setSelectedCredential( + credentialOptions + ? credentialOptions.find((option) => option.value === salesforceAction?.credentialId) ?? + selectedCredential + : selectedCredential + ); + } + }, [data]); + + if (isLoading) { + return
Loading...
; + } + + // Check to see if the user has any compatible credentials + if ( + !data?.credentials.some((credential) => enabledIncompleteBookingApps.includes(credential?.appId ?? "")) + ) { + return
No apps installed that support this feature
; + } + + return ( + <> +
+
+ { + setSalesforceActionEnabled(checked); + }} + /> +
+ + {salesforceActionEnabled ? ( + <> +
+ + {form.team && ( + <> +
+

Credential to use

+ option.value === action.fieldType)} + isDisabled={true} + /> +
+
+ +
+
+ { + if (e) { + setSelectedFieldType(e); + setNewSalesforceAction({ + ...newSalesforceAction, + fieldType: e.value, + }); + } + }} + /> +
+
+ + setNewSalesforceAction({ + ...newSalesforceAction, + value: e.target.value, + }) + } + /> +
+
+