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: Salesforce RR skip based on a user on a lookup field on an account #17526

Merged
merged 24 commits into from
Nov 12, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
feb63fd
move types to types file
joeauyeung Nov 7, 2024
90ada06
Create salesforce routing form components
joeauyeung Nov 7, 2024
0e407eb
Save salesforce data to routing form
joeauyeung Nov 7, 2024
03583d4
Fixes
joeauyeung Nov 7, 2024
dd859b0
Add event type service
joeauyeung Nov 8, 2024
071cf74
Change commenting
joeauyeung Nov 8, 2024
89191c6
Pass data from routing form to CRM
joeauyeung Nov 8, 2024
20fcf6c
Init Salesforce routing form booking form handler
joeauyeung Nov 8, 2024
df3f4d5
Merge branch 'main' into salesforce-route-to-user-lookup-field
joeauyeung Nov 8, 2024
b604055
Salesforce find user associated with lookup field
joeauyeung Nov 8, 2024
ebcc41d
Add looking up the contact owner
joeauyeung Nov 8, 2024
93e358b
If salesforce is disabled then don't change the Salesforce option
joeauyeung Nov 8, 2024
868f302
Small refactor
joeauyeung Nov 8, 2024
2076d23
Refactor getting event type app metadata
joeauyeung Nov 8, 2024
968d0b3
Refactor eventType service
joeauyeung Nov 8, 2024
778382b
Type fix
joeauyeung Nov 8, 2024
54a724a
Clean up
joeauyeung Nov 8, 2024
cd03f79
Merge branch 'main' into salesforce-route-to-user-lookup-field
joeauyeung Nov 11, 2024
74d72e9
Add translations
joeauyeung Nov 11, 2024
7218477
Bump lockfile
emrysal Nov 11, 2024
f058483
Move appBookingFormHandler to inside of getServerSideProps call
keithwillcode Nov 11, 2024
8fbc654
Fix e2e test, outdated i18n string
emrysal Nov 11, 2024
def526d
Merge branch 'salesforce-route-to-user-lookup-field' of https://githu…
emrysal Nov 11, 2024
2c72ee8
Remove click switch
joeauyeung Nov 12, 2024
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
5 changes: 5 additions & 0 deletions packages/app-store/routing-forms/appComponents.ts
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same with the components that are rendered on the routing form route builder.

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import dynamic from "next/dynamic";

export const routingFormAppComponents = {
salesforce: dynamic(() => import("../salesforce/components/RoutingFormOptions")),
};
5 changes: 5 additions & 0 deletions packages/app-store/routing-forms/appDataSchemas.ts
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also any metadata associated with any apps enabled for routing forms.

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { routingFormOptions as salesforce_routing_form_schema } from "../salesforce/zod";

export const routingFormAppDataSchemas = {
salesforce: salesforce_routing_form_schema,
};
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This component is used to dynamically render app specific options on the route builder.

Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { Route, AttributeRoutingConfig } from "../types/types";

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export default function DynamicAppComponent<T extends Record<string, React.ComponentType<any>>>(props: {
componentMap: T;
slug: string;
appData: any;
route: Route;
setAttributeRoutingConfig: (id: string, attributeRoutingConfig: Partial<AttributeRoutingConfig>) => void;
wrapperClassName?: string;
}) {
const { componentMap, slug, wrapperClassName, appData, route, setAttributeRoutingConfig, ...rest } = props;

// There can be apps with no matching component
if (!componentMap[slug]) return null;

const Component = componentMap[slug];

return (
<div className={wrapperClassName || ""}>
<Component
appData={appData}
route={route}
setAttributeRoutingConfig={setAttributeRoutingConfig}
{...rest}
/>
</div>
);
}
2 changes: 2 additions & 0 deletions packages/app-store/routing-forms/lib/enabledApps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// This is a list of apps which have functionality in routing forms
export const enabledAppSlugs = ["salesforce"];
102 changes: 66 additions & 36 deletions packages/app-store/routing-forms/pages/route-builder/[...appPages].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { areTheySiblingEntitites } from "@calcom/lib/entityPermissionUtils";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { App_RoutingForms_Form } from "@calcom/prisma/client";
import { SchedulingType } from "@calcom/prisma/client";
import { EventTypeAppMetadataSchema } from "@calcom/prisma/zod-utils";
import type { RouterOutputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react";
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
Expand All @@ -26,13 +27,16 @@ import {
Switch,
} from "@calcom/ui";

import { routingFormAppComponents } from "../../appComponents";
import DynamicAppComponent from "../../components/DynamicAppComponent";
import type { RoutingFormWithResponseCount } from "../../components/SingleForm";
import SingleForm, {
getServerSidePropsForSingleFormView as getServerSideProps,
} from "../../components/SingleForm";
import "../../components/react-awesome-query-builder/styles.css";
import { RoutingPages } from "../../lib/RoutingPages";
import { createFallbackRoute } from "../../lib/createFallbackRoute";
import { enabledAppSlugs } from "../../lib/enabledApps";
import {
getQueryBuilderConfigForFormFields,
getQueryBuilderConfigForAttributes,
Expand All @@ -41,31 +45,22 @@ import {
} from "../../lib/getQueryBuilderConfig";
import isRouter from "../../lib/isRouter";
import type { SerializableForm } from "../../types/types";
import type { GlobalRoute, LocalRoute, SerializableRoute, Attribute } from "../../types/types";
import type {
GlobalRoute,
LocalRoute,
SerializableRoute,
Attribute,
EditFormRoute,
LocalRouteWithRaqbStates,
AttributeRoutingConfig,
} from "../../types/types";
import { RouteActionType } from "../../zod";

type FormFieldsQueryBuilderState = {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move types to the types file so they can be used in other components

tree: ImmutableTree;
config: FormFieldsQueryBuilderConfigWithRaqbFields;
};

type AttributesQueryBuilderState = {
tree: ImmutableTree;
config: AttributesQueryBuilderConfigWithRaqbFields;
};

type LocalRouteWithRaqbStates = LocalRoute & {
formFieldsQueryBuilderState: FormFieldsQueryBuilderState;
attributesQueryBuilderState: AttributesQueryBuilderState | null;
fallbackAttributesQueryBuilderState: AttributesQueryBuilderState | null;
};

type EventTypesByGroup = RouterOutputs["viewer"]["eventTypes"]["getByViewer"];

type Form = inferSSRProps<typeof getServerSideProps>["form"];

type Route = LocalRouteWithRaqbStates | GlobalRoute;
type SetRoute = (id: string, route: Partial<Route>) => void;
type SetRoute = (id: string, route: Partial<EditFormRoute>) => void;

const RoundRobinContactOwnerOverrideSwitch = ({
route,
Expand Down Expand Up @@ -96,7 +91,6 @@ const RoundRobinContactOwnerOverrideSwitch = ({

type AttributesQueryValue = NonNullable<LocalRoute["attributesQueryValue"]>;
type FormFieldsQueryValue = LocalRoute["queryValue"];
type AttributeRoutingConfig = NonNullable<LocalRoute["attributeRoutingConfig"]>;

/**
* We need eventTypeId in every redirect url action now for Rerouting to work smoothly.
Expand All @@ -107,7 +101,7 @@ function useEnsureEventTypeIdInRedirectUrlAction({
eventOptions,
setRoute,
}: {
route: Route;
route: EditFormRoute;
eventOptions: { label: string; value: string; eventTypeId: number }[];
setRoute: SetRoute;
}) {
Expand All @@ -134,7 +128,7 @@ function useEnsureEventTypeIdInRedirectUrlAction({
}, [eventOptions, setRoute, route.id, (route as unknown as any).action?.value]);
}

const hasRules = (route: Route) => {
const hasRules = (route: EditFormRoute) => {
if (isRouter(route)) return false;
route.queryValue.children1 && Object.keys(route.queryValue.children1).length;
};
Expand Down Expand Up @@ -169,9 +163,14 @@ const buildEventsData = ({
}: {
eventTypesByGroup: EventTypesByGroup | undefined;
form: Form;
route: Route;
route: EditFormRoute;
}) => {
const eventOptions: { label: string; value: string; eventTypeId: number }[] = [];
const eventOptions: {
label: string;
value: string;
eventTypeId: number;
eventTypeAppMetadata: Record<string, any>;
}[] = [];
const eventTypesMap = new Map<
number,
{
Expand All @@ -196,16 +195,34 @@ const buildEventsData = ({
const isRouteAlreadyInUse = isRouter(route) ? false : uniqueSlug === route.action.value;

// If Event is already in use, we let it be so as to not break the existing setup

// Pass app data that works with routing forms
const eventTypeAppMetadataParse = EventTypeAppMetadataSchema.safeParse(eventType.metadata?.apps);
const eventTypeAppMetadata: Record<string, any> = {};
if (eventTypeAppMetadataParse.success) {
const appMetadata = eventTypeAppMetadataParse.data;

if (appMetadata) {
for (const appSlug of Object.keys(appMetadata)) {
if (enabledAppSlugs.includes(appSlug)) {
eventTypeAppMetadata[appSlug] = appMetadata[appSlug as keyof typeof appMetadata];
}
}
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know it is in draft but still leaving code feedback.

Let's abstract it out


if (!isRouteAlreadyInUse && !eventTypeValidInContext) {
return;
}
eventTypesMap.set(eventType.id, {
eventTypeAppMetadata,
schedulingType: eventType.schedulingType,
});
eventOptions.push({
label: uniqueSlug,
value: uniqueSlug,
eventTypeId: eventType.id,
eventTypeAppMetadata,
});
});
});
Expand All @@ -230,13 +247,13 @@ const Route = ({
eventTypesByGroup,
}: {
form: Form;
route: Route;
routes: Route[];
route: EditFormRoute;
routes: EditFormRoute[];
setRoute: SetRoute;
setAttributeRoutingConfig: (id: string, attributeRoutingConfig: Partial<AttributeRoutingConfig>) => void;
formFieldsQueryBuilderConfig: FormFieldsQueryBuilderConfigWithRaqbFields;
attributesQueryBuilderConfig: AttributesQueryBuilderConfigWithRaqbFields | null;
setRoutes: React.Dispatch<React.SetStateAction<Route[]>>;
setRoutes: React.Dispatch<React.SetStateAction<EditFormRoute[]>>;
fieldIdentifiers: string[];
moveUp?: { fn: () => void; check: () => boolean } | null;
moveDown?: { fn: () => void; check: () => boolean } | null;
Expand Down Expand Up @@ -271,7 +288,7 @@ const Route = ({
});

const onChangeFormFieldsQuery = (
route: Route,
route: EditFormRoute,
immutableTree: ImmutableTree,
config: FormFieldsQueryBuilderConfigWithRaqbFields
) => {
Expand All @@ -283,7 +300,7 @@ const Route = ({
};

const onChangeTeamMembersQuery = (
route: Route,
route: EditFormRoute,
immutableTree: ImmutableTree,
config: AttributesQueryBuilderConfigWithRaqbFields
) => {
Expand All @@ -295,7 +312,7 @@ const Route = ({
};

const onChangeFallbackTeamMembersQuery = (
route: Route,
route: EditFormRoute,
immutableTree: ImmutableTree,
config: AttributesQueryBuilderConfigWithRaqbFields
) => {
Expand Down Expand Up @@ -404,12 +421,25 @@ const Route = ({
and use only the Team Members that match the following criteria (matches all by default)
</span>

{isRoundRobinEventSelectedForRedirect ? (
{eventTypeRedirectUrlSelectedOption?.eventTypeAppMetadata &&
"salesforce" in eventTypeRedirectUrlSelectedOption.eventTypeAppMetadata ? (
<div className="mt-4 px-2.5">
<DynamicAppComponent
componentMap={routingFormAppComponents}
slug="salesforce"
appData={eventTypeRedirectUrlSelectedOption?.eventTypeAppMetadata["salesforce"]}
route={route}
setAttributeRoutingConfig={setAttributeRoutingConfig}
/>
</div>
) : null}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO: Make this more dynamic and not hard code "salesforce" here


{/* {isRoundRobinEventSelectedForRedirect ? (
<RoundRobinContactOwnerOverrideSwitch
route={route}
setAttributeRoutingConfig={setAttributeRoutingConfig}
/>
) : null}
) : null} */}

<div className="mt-2">
{route.attributesQueryBuilderState && attributesQueryBuilderConfig && (
Expand Down Expand Up @@ -625,7 +655,7 @@ const deserializeRoute = ({
route: Exclude<SerializableRoute, GlobalRoute>;
formFieldsQueryBuilderConfig: FormFieldsQueryBuilderConfigWithRaqbFields;
attributesQueryBuilderConfig: AttributesQueryBuilderConfigWithRaqbFields | null;
}): Route => {
}): EditFormRoute => {
const attributesQueryBuilderState =
route.attributesQueryValue && attributesQueryBuilderConfig
? buildState({
Expand Down Expand Up @@ -707,7 +737,7 @@ function useRoutes({
return newRoutes;
});

function getRoutesToSave(routes: Route[]) {
function getRoutesToSave(routes: EditFormRoute[]) {
return routes.map((route) => {
if (isRouter(route)) {
return route;
Expand Down Expand Up @@ -868,7 +898,7 @@ const Routes = ({
});
}

const setRoute = (id: string, route: Partial<Route>) => {
const setRoute = (id: string, route: Partial<EditFormRoute>) => {
const index = routes.findIndex((route) => route.id === id);
const existingRoute = routes[index];
const newRoutes = [...routes];
Expand Down Expand Up @@ -990,7 +1020,7 @@ const Routes = ({
id: routerId,
name: option.name,
description: option.description,
} as Route,
} as EditFormRoute,
]);
}
}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,12 @@ export function getUrlSearchParamsToForward({
}
}

const attributeRoutingConfigParams: Record<string, any> = {};

if (attributeRoutingConfig?.skipContactOwner) {
attributeRoutingConfigParams["cal.skipContactOwner"] = "true";
}

const allQueryParams: Params = {
...paramsFromCurrentUrl,
// In case of conflict b/w paramsFromResponse and paramsFromCurrentUrl, paramsFromResponse should win as the booker probably improved upon the prefilled value.
Expand All @@ -88,7 +94,8 @@ export function getUrlSearchParamsToForward({
? { ["cal.routedTeamMemberIds"]: teamMembersMatchingAttributeLogic.join(",") }
: null),
["cal.routingFormResponseId"]: String(formResponseId),
...(attributeRoutingConfig?.skipContactOwner ? { ["cal.skipContactOwner"]: "true" } : {}),
// ...(attributeRoutingConfig?.skipContactOwner ? { ["cal.skipContactOwner"]: "true" } : {}),
...attributeRoutingConfigParams,
...(reroutingFormResponses
? { ["cal.reroutingFormResponses"]: JSON.stringify(reroutingFormResponses) }
: null),
Expand Down
25 changes: 25 additions & 0 deletions packages/app-store/routing-forms/types/types.d.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import type { App_RoutingForms_Form } from "@prisma/client";
import type { ImmutableTree, Config } from "react-awesome-query-builder";
import type z from "zod";

import type { AttributeType } from "@calcom/prisma/client";
import type { RoutingFormSettings } from "@calcom/prisma/zod-utils";

import type QueryBuilderInitialConfig from "../components/react-awesome-query-builder/config/config";
import type {
FormFieldsQueryBuilderConfigWithRaqbFields,
AttributesQueryBuilderConfigWithRaqbFields,
} from "../lib/getQueryBuilderConfig";
import type { zodRouterRouteView, zodNonRouterRoute, zodFieldsView, zodRoutesView } from "../zod";

export type RoutingForm = SerializableForm<App_RoutingForms_Form>;
Expand Down Expand Up @@ -74,3 +79,23 @@ export type Attribute = {
export type AttributesQueryValue = NonNullable<LocalRoute["attributesQueryValue"]>;
export type FormFieldsQueryValue = LocalRoute["queryValue"];
export type SerializableField = NonNullable<SerializableForm<App_RoutingForms_Form>["fields"]>[number];

export type AttributeRoutingConfig = NonNullable<LocalRoute["attributeRoutingConfig"]>;

export type FormFieldsQueryBuilderState = {
tree: ImmutableTree;
config: FormFieldsQueryBuilderConfigWithRaqbFields;
};

export type AttributesQueryBuilderState = {
tree: ImmutableTree;
config: AttributesQueryBuilderConfigWithRaqbFields;
};

export type LocalRouteWithRaqbStates = LocalRoute & {
formFieldsQueryBuilderState: FormFieldsQueryBuilderState;
attributesQueryBuilderState: AttributesQueryBuilderState | null;
fallbackAttributesQueryBuilderState: AttributesQueryBuilderState | null;
};

export type EditFormRoute = LocalRouteWithRaqbStates | GlobalRoute;
3 changes: 3 additions & 0 deletions packages/app-store/routing-forms/zod.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { z } from "zod";

import { routingFormAppDataSchemas } from "./appDataSchemas";

export const zodNonRouterField = z.object({
id: z.string(),
label: z.string(),
Expand Down Expand Up @@ -133,6 +135,7 @@ export const zodNonRouterRoute = z.object({
attributeRoutingConfig: z
.object({
skipContactOwner: z.boolean().optional(),
salesforce: routingFormAppDataSchemas["salesforce"],
})
.nullish(),

Expand Down
Loading
Loading