Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Routing form submitted but no booking - Salesforce actions #18616

Merged
merged 46 commits into from
Jan 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
7ec75be
Add booking incomplete actions table
joeauyeung Dec 25, 2024
360b669
Add incomplete booking page tab
joeauyeung Dec 25, 2024
8903e99
Add `getincompleteBookingSettings` trpc endpoints
joeauyeung Dec 25, 2024
b611c03
Add incomplete booking page
joeauyeung Dec 25, 2024
4787745
Abstract enabled apps array
joeauyeung Dec 25, 2024
6281df0
Handle no enabled credentials and no actions
joeauyeung Dec 25, 2024
d279a1d
Add enabled field to incomplete booking action db record
joeauyeung Dec 26, 2024
9065a4b
Add new write record entries
joeauyeung Dec 26, 2024
b3d4cff
UI add separation between switch and inputs
joeauyeung Dec 26, 2024
e00204e
Fix typo
joeauyeung Dec 26, 2024
04d5112
clean up
joeauyeung Dec 26, 2024
d4ae1dc
Add saveIncompleteBookingSettings endpoint
joeauyeung Dec 26, 2024
a5c4877
Save incomplete booking settings
joeauyeung Dec 26, 2024
80f66cf
Fix language around when to write to field
joeauyeung Dec 26, 2024
4dd4071
Add `credentialId` to action record
joeauyeung Dec 26, 2024
f77d6dd
Choose which credential to assign to action
joeauyeung Dec 26, 2024
ba6a1c8
Save credential to action
joeauyeung Dec 26, 2024
3903e6b
Revert "Save credential to action"
joeauyeung Jan 13, 2025
a0a0927
Revert "Choose which credential to assign to action"
joeauyeung Jan 13, 2025
316a366
Revert "Add `credentialId` to action record"
joeauyeung Jan 13, 2025
2843a92
Add credentialId to action record - rewrite migration file
joeauyeung Jan 13, 2025
6319927
Revert "Add credentialId to action record - rewrite migration file"
joeauyeung Jan 13, 2025
f490b51
Revert "Add booking incomplete actions table"
joeauyeung Jan 13, 2025
b884958
Revert "Add enabled field to incomplete booking action db record"
joeauyeung Jan 13, 2025
b145f55
Write migration in single commit
joeauyeung Jan 13, 2025
1ad2f51
Rename table
joeauyeung Jan 13, 2025
d73089d
Rename table - remove underscores
joeauyeung Jan 13, 2025
7b78f53
Remove credential relationship
joeauyeung Jan 13, 2025
de62a1a
Type fix - changing table name
joeauyeung Jan 13, 2025
b4e7a02
Fix table name
joeauyeung Jan 13, 2025
4231bd2
Change writeToRecordObject to object
joeauyeung Dec 27, 2024
ec5b702
Salesforce add incomplete booking, write to record
joeauyeung Dec 27, 2024
b8940bd
Add incomplete booking actions to `triggerFormSubmittedNoEventWebhooks`
joeauyeung Dec 27, 2024
53b8cbe
Remove console.log
joeauyeung Dec 27, 2024
fd092fd
Type fixes
joeauyeung Jan 13, 2025
09e7f60
Type fixes
joeauyeung Jan 13, 2025
3fe38e1
Iterate if incompleteBookingActions
joeauyeung Jan 14, 2025
e595e2f
Choose which credential to assign to action
joeauyeung Dec 26, 2024
e247e3b
Save credential to action
joeauyeung Dec 26, 2024
7598738
Fix getServerSideProp changes
joeauyeung Jan 14, 2025
0eb085e
Type fix
joeauyeung Jan 14, 2025
49f3e51
Type fix
joeauyeung Jan 14, 2025
f3b07d4
Type fix
joeauyeung Jan 14, 2025
71a4245
Type fix
joeauyeung Jan 14, 2025
1048934
Merge branch 'main' into form-submission-no-booking-options-2
emrysal Jan 14, 2025
59ac451
Merge branch 'main' into form-submission-no-booking-options-2
emrysal Jan 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/web/public/static/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -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 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
4 changes: 4 additions & 0 deletions packages/app-store/routing-forms/components/RoutingNavBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div className="mb-4">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const enabledIncompleteBookingApps = ["salesforce"];
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;
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;
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ export const routingServerSidePropsConfig: Record<string, AppGetServerSideProps>
"route-builder": getServerSidePropsSingleForm,
"routing-link": getServerSidePropsRoutingLink,
reporting: getServerSidePropsSingleForm,
"incomplete-booking": getServerSidePropsSingleForm,
};
2 changes: 2 additions & 0 deletions packages/app-store/routing-forms/pages/app-routing.config.tsx
Original file line number Diff line number Diff line change
@@ -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]";
Expand All @@ -13,6 +14,7 @@ const routingConfig = {
"routing-link": RoutingLink,
reporting: Reporting,
layoutHandler: LayoutHandler,
"incomplete-booking": IncompleteBooking,
};

export default routingConfig;
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 };
Loading
Loading