-
Notifications
You must be signed in to change notification settings - Fork 8.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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 ba6a1c8. * Revert "Choose which credential to assign to action" This reverts commit 968f6e5. * Revert "Add `credentialId` to action record" This reverts commit 579f9ff. * Add credentialId to action record - rewrite migration file * Revert "Add credentialId to action record - rewrite migration file" This reverts commit 2843a92. * Revert "Add booking incomplete actions table" This reverts commit 7ec75be. * Revert "Add enabled field to incomplete booking action db record" This reverts commit d279a1d. * 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 <me@alexvanandel.com>
- Loading branch information
1 parent
7a8b9ed
commit 66b3e73
Showing
19 changed files
with
756 additions
and
21 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
1 change: 1 addition & 0 deletions
1
packages/app-store/routing-forms/lib/enabledIncompleteBookingApps.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export const enabledIncompleteBookingApps = ["salesforce"]; |
10 changes: 10 additions & 0 deletions
10
packages/app-store/routing-forms/lib/incompleteBooking/actionDataSchemas.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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, z.ZodType<any>> = { | ||
[IncompleteBookingActionType.SALESFORCE]: salesforceRoutingFormIncompleteBookingDataSchema, | ||
}; | ||
|
||
export default incompleteBookingActionDataSchemas; |
13 changes: 13 additions & 0 deletions
13
packages/app-store/routing-forms/lib/incompleteBooking/actionFunctions.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
329 changes: 329 additions & 0 deletions
329
packages/app-store/routing-forms/pages/incomplete-booking/[...appPages].tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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<typeof salesforceWriteToRecordDataSchema> | ||
>({}); | ||
|
||
// Handle just Salesforce for now but need to expand this to other apps | ||
const [salesforceActionEnabled, setSalesforceActionEnabled] = useState<boolean>(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 <div>Loading...</div>; | ||
} | ||
|
||
// Check to see if the user has any compatible credentials | ||
if ( | ||
!data?.credentials.some((credential) => enabledIncompleteBookingApps.includes(credential?.appId ?? "")) | ||
) { | ||
return <div>No apps installed that support this feature</div>; | ||
} | ||
|
||
return ( | ||
<> | ||
<div className="bg-default border-subtle rounded-md border p-8"> | ||
<div> | ||
<Switch | ||
labelOnLeading | ||
label="Write to Salesforce contact/lead record" | ||
checked={salesforceActionEnabled} | ||
onCheckedChange={(checked) => { | ||
setSalesforceActionEnabled(checked); | ||
}} | ||
/> | ||
</div> | ||
|
||
{salesforceActionEnabled ? ( | ||
<> | ||
<hr className="mt-4 border" /> | ||
|
||
{form.team && ( | ||
<> | ||
<div className="mt-2"> | ||
<p>Credential to use</p> | ||
<Select | ||
options={credentialOptions} | ||
value={selectedCredential} | ||
onChange={(option) => { | ||
if (!option) { | ||
return; | ||
} | ||
setSelectedCredential(option); | ||
}} | ||
/> | ||
</div> | ||
|
||
<hr className="mt-4 border" /> | ||
</> | ||
)} | ||
|
||
<div className="mt-2"> | ||
<div className="grid grid-cols-5 gap-4"> | ||
<div>{t("field_name")}</div> | ||
<div>{t("field_type")}</div> | ||
<div>{t("value")}</div> | ||
<div>{t("when_to_write")}</div> | ||
</div> | ||
<div> | ||
{Object.keys(salesforceWriteToRecordObject).map((key) => { | ||
const action = | ||
salesforceWriteToRecordObject[key as keyof typeof salesforceWriteToRecordObject]; | ||
return ( | ||
<div className="mt-2 grid grid-cols-5 gap-4" key={key}> | ||
<div> | ||
<InputField value={key} readOnly /> | ||
</div> | ||
<div> | ||
<Select | ||
value={fieldTypeOptions.find((option) => option.value === action.fieldType)} | ||
isDisabled={true} | ||
/> | ||
</div> | ||
<div> | ||
<InputField value={action.value} readOnly /> | ||
</div> | ||
<div> | ||
<Select | ||
value={whenToWriteToRecordOptions.find( | ||
(option) => option.value === action.whenToWrite | ||
)} | ||
isDisabled={true} | ||
/> | ||
</div> | ||
<div> | ||
<Button | ||
StartIcon="trash" | ||
variant="icon" | ||
color="destructive" | ||
onClick={() => { | ||
const newActions = { ...salesforceWriteToRecordObject }; | ||
delete newActions[key]; | ||
setSalesforceWriteToRecordObject(newActions); | ||
}} | ||
/> | ||
</div> | ||
</div> | ||
); | ||
})} | ||
<div className="mt-2 grid grid-cols-5 gap-4"> | ||
<div> | ||
<InputField | ||
value={newSalesforceAction.field} | ||
onChange={(e) => | ||
setNewSalesforceAction({ | ||
...newSalesforceAction, | ||
field: e.target.value, | ||
}) | ||
} | ||
/> | ||
</div> | ||
<div> | ||
<Select | ||
options={fieldTypeOptions} | ||
value={selectedFieldType} | ||
onChange={(e) => { | ||
if (e) { | ||
setSelectedFieldType(e); | ||
setNewSalesforceAction({ | ||
...newSalesforceAction, | ||
fieldType: e.value, | ||
}); | ||
} | ||
}} | ||
/> | ||
</div> | ||
<div> | ||
<InputField | ||
value={newSalesforceAction.value} | ||
onChange={(e) => | ||
setNewSalesforceAction({ | ||
...newSalesforceAction, | ||
value: e.target.value, | ||
}) | ||
} | ||
/> | ||
</div> | ||
<div> | ||
<Select | ||
options={whenToWriteToRecordOptions} | ||
value={selectedWhenToWrite} | ||
onChange={(e) => { | ||
if (e) { | ||
setSelectedWhenToWrite(e); | ||
setNewSalesforceAction({ | ||
...newSalesforceAction, | ||
whenToWrite: e.value, | ||
}); | ||
} | ||
}} | ||
/> | ||
</div> | ||
</div> | ||
</div> | ||
<Button | ||
className="mt-2" | ||
size="sm" | ||
disabled={ | ||
!( | ||
newSalesforceAction.field && | ||
newSalesforceAction.fieldType && | ||
newSalesforceAction.value && | ||
newSalesforceAction.whenToWrite | ||
) | ||
} | ||
onClick={() => { | ||
if (Object.keys(salesforceWriteToRecordObject).includes(newSalesforceAction.field.trim())) { | ||
showToast("Field already exists", "error"); | ||
return; | ||
} | ||
|
||
setSalesforceWriteToRecordObject({ | ||
...salesforceWriteToRecordObject, | ||
[newSalesforceAction.field]: { | ||
fieldType: newSalesforceAction.fieldType, | ||
value: newSalesforceAction.value, | ||
whenToWrite: newSalesforceAction.whenToWrite, | ||
}, | ||
}); | ||
|
||
setNewSalesforceAction({ | ||
field: "", | ||
fieldType: selectedFieldType.value, | ||
value: "", | ||
whenToWrite: WhenToWriteToRecord.FIELD_EMPTY, | ||
}); | ||
}}> | ||
{t("add_new_field")} | ||
</Button> | ||
</div> | ||
</> | ||
) : null} | ||
</div> | ||
<div className="mt-2 flex justify-end"> | ||
<Button | ||
size="sm" | ||
disabled={mutation.isPending} | ||
onClick={() => { | ||
mutation.mutate({ | ||
formId: form.id, | ||
data: { | ||
writeToRecordObject: salesforceWriteToRecordObject, | ||
}, | ||
actionType: IncompleteBookingActionType.SALESFORCE, | ||
enabled: salesforceActionEnabled, | ||
credentialId: selectedCredential?.value ?? data.credentials[0].id, | ||
}); | ||
}}> | ||
{t("save")} | ||
</Button> | ||
</div> | ||
</> | ||
); | ||
} | ||
|
||
export default function IncompleteBookingPage({ | ||
form, | ||
appUrl, | ||
enrichedWithUserProfileForm, | ||
}: inferSSRProps<typeof getServerSideProps> & { appUrl: string }) { | ||
return ( | ||
<SingleForm | ||
form={form} | ||
appUrl={appUrl} | ||
enrichedWithUserProfileForm={enrichedWithUserProfileForm} | ||
Page={Page} | ||
/> | ||
); | ||
} | ||
|
||
IncompleteBookingPage.getLayout = (page: React.ReactElement) => { | ||
return ( | ||
<Shell backPath="/apps/routing-forms/forms" withoutMain={true}> | ||
{page} | ||
</Shell> | ||
); | ||
}; | ||
|
||
export { getServerSideProps }; |
Oops, something went wrong.