* mvp done * wip * fix ts errors and other code improvements * fix ts errors * ensure mobile layout support * Make skeleton responsive on screen resize * refactor * Add test for EmbedElement * make skeleton closer to pixel perfect * Address PR feedback * Router-preloading ## What does this PR do? <!-- Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. --> - Fixes #XXXX (GitHub issue number) - Fixes CAL-XXXX (Linear issue number - should be visible at the bottom of the GitHub issue description) ## Visual Demo (For contributors especially) A visual demonstration is strongly recommended, for both the original and new change **(video / image - any one)**. #### Video Demo (if applicable): - Show screen recordings of the issue or feature. - Demonstrate how to reproduce the issue, the behavior before and after the change. #### Image Demo (if applicable): - Add side-by-side screenshots of the original and updated change. - Highlight any significant change(s). ## Mandatory Tasks (DO NOT REMOVE) - [ ] I have self-reviewed the code (A decent size PR without self-review might be rejected). - [ ] I have updated the developer docs in /docs if this PR makes changes that would require a [documentation change](https://cal.com/docs). If N/A, write N/A here and check the checkbox. - [ ] I confirm automated tests are in place that prove my fix is effective or that my feature works. ## How should this be tested? <!-- Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration. Write details that help to start the tests --> - Are there environment variables that should be set? - What are the minimal test data to have? - What is expected (happy path) to have (input and output)? - Any other important info that could help to test that PR ## Checklist <!-- Remove bullet points below that don't apply to you --> - I haven't read the [contributing guide](https://github.com/calcom/cal.com/blob/main/CONTRIBUTING.md) - My code doesn't follow the style guidelines of this project - I haven't commented my code, particularly in hard-to-understand areas - I haven't checked if my changes generate no new warnings * wip\ * wip * fix mrge.io feedback * wip * Add README and lifecycle * Add README and lifecycle * Update routing form-seed and some other fixes * remove linkFailed fix from the branch * self-review * self-review-2 * self-review-3 * Handle soft connect\ * Update README and fix a bug with query parmas * Add one more case in routing-html playground --------- Co-authored-by: Benny Joo <sldisek783@gmail.com> Co-authored-by: amrit <iamamrit27@gmail.com>
224 lines
8.3 KiB
TypeScript
224 lines
8.3 KiB
TypeScript
// !IMPORTANT! changes to this file requires publishing new version of platform libraries in order for the changes to be applied to APIV2
|
|
import type { GetServerSidePropsContext } from "next";
|
|
import { stringify } from "querystring";
|
|
import { v4 as uuidv4 } from "uuid";
|
|
import z from "zod";
|
|
|
|
import { enrichFormWithMigrationData } from "@calcom/app-store/routing-forms/enrichFormWithMigrationData";
|
|
import { getAbsoluteEventTypeRedirectUrlWithEmbedSupport } from "@calcom/app-store/routing-forms/getEventTypeRedirectUrl";
|
|
import getFieldIdentifier from "@calcom/app-store/routing-forms/lib/getFieldIdentifier";
|
|
import { getSerializableForm } from "@calcom/app-store/routing-forms/lib/getSerializableForm";
|
|
import { getServerTimingHeader } from "@calcom/app-store/routing-forms/lib/getServerTimingHeader";
|
|
import { handleResponse } from "@calcom/app-store/routing-forms/lib/handleResponse";
|
|
import { findMatchingRoute } from "@calcom/app-store/routing-forms/lib/processRoute";
|
|
import { substituteVariables } from "@calcom/app-store/routing-forms/lib/substituteVariables";
|
|
import { getFieldResponseForJsonLogic } from "@calcom/app-store/routing-forms/lib/transformResponse";
|
|
import { getUrlSearchParamsToForward } from "@calcom/app-store/routing-forms/pages/routing-link/getUrlSearchParamsToForward";
|
|
import type { FormResponse } from "@calcom/app-store/routing-forms/types/types";
|
|
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
|
import { isAuthorizedToViewFormOnOrgDomain } from "@calcom/features/routing-forms/lib/isAuthorizedToViewForm";
|
|
import logger from "@calcom/lib/logger";
|
|
import monitorCallbackAsync from "@calcom/lib/sentryWrapper";
|
|
import { RoutingFormRepository } from "@calcom/lib/server/repository/routingForm";
|
|
import { UserRepository } from "@calcom/lib/server/repository/user";
|
|
|
|
import { TRPCError } from "@trpc/server";
|
|
|
|
const log = logger.getSubLogger({ prefix: ["[routing-forms]", "[router]"] });
|
|
const querySchema = z
|
|
.object({
|
|
form: z.string(),
|
|
})
|
|
.catchall(z.string().or(z.array(z.string())));
|
|
|
|
function hasEmbedPath(pathWithQuery: string) {
|
|
const onlyPath = pathWithQuery.split("?")[0];
|
|
return onlyPath.endsWith("/embed") || onlyPath.endsWith("/embed/");
|
|
}
|
|
|
|
export const getRoutedUrl = (context: Pick<GetServerSidePropsContext, "query" | "req">) => {
|
|
return monitorCallbackAsync(_getRoutedUrl, context);
|
|
};
|
|
|
|
const _getRoutedUrl = async (context: Pick<GetServerSidePropsContext, "query" | "req">) => {
|
|
const queryParsed = querySchema.safeParse(context.query);
|
|
const isEmbed = hasEmbedPath(context.req.url || "");
|
|
const pageProps = {
|
|
isEmbed,
|
|
};
|
|
|
|
if (!queryParsed.success) {
|
|
log.warn("Error parsing query", { issues: queryParsed.error.issues });
|
|
return {
|
|
notFound: true,
|
|
};
|
|
}
|
|
|
|
// TODO: Known params reserved by Cal.com are form, embed, layout and other cal. prefixed params. We should exclude all of them from fieldsResponses.
|
|
// But they must be present in `paramsToBeForwardedAsIs` as they could be needed by Booking Page as well.
|
|
const { form: formId, "cal.isBookingDryRun": isBookingDryRunParam, ...fieldsResponses } = queryParsed.data;
|
|
const isBookingDryRun = isBookingDryRunParam === "true";
|
|
const paramsToBeForwardedAsIs = {
|
|
...fieldsResponses,
|
|
// Must be forwarded if present to Booking Page. Setting it explicitly here as it is critical to be present in the URL.
|
|
...(isBookingDryRunParam ? { "cal.isBookingDryRun": isBookingDryRunParam } : null),
|
|
};
|
|
|
|
const { currentOrgDomain } = orgDomainConfig(context.req);
|
|
|
|
let timeTaken: Record<string, number | null> = {};
|
|
|
|
const formQueryStart = performance.now();
|
|
const form = await RoutingFormRepository.findFormByIdIncludeUserTeamAndOrg(formId);
|
|
timeTaken.formQuery = performance.now() - formQueryStart;
|
|
|
|
if (!form) {
|
|
return {
|
|
notFound: true,
|
|
};
|
|
}
|
|
|
|
const profileEnrichmentStart = performance.now();
|
|
const formWithUserProfile = {
|
|
...form,
|
|
user: await UserRepository.enrichUserWithItsProfile({ user: form.user }),
|
|
};
|
|
timeTaken.profileEnrichment = performance.now() - profileEnrichmentStart;
|
|
|
|
if (
|
|
!isAuthorizedToViewFormOnOrgDomain({ user: formWithUserProfile.user, currentOrgDomain, team: form.team })
|
|
) {
|
|
return {
|
|
notFound: true,
|
|
};
|
|
}
|
|
|
|
const getSerializableFormStart = performance.now();
|
|
const serializableForm = await getSerializableForm({
|
|
form: enrichFormWithMigrationData(formWithUserProfile),
|
|
});
|
|
timeTaken.getSerializableForm = performance.now() - getSerializableFormStart;
|
|
|
|
const response: FormResponse = {};
|
|
if (!serializableForm.fields) {
|
|
throw new Error("Form has no fields");
|
|
}
|
|
serializableForm.fields.forEach((field) => {
|
|
const fieldResponse = fieldsResponses[getFieldIdentifier(field)] || "";
|
|
|
|
response[field.id] = {
|
|
label: field.label,
|
|
value: getFieldResponseForJsonLogic({ field, value: fieldResponse }),
|
|
};
|
|
});
|
|
|
|
const matchingRoute = findMatchingRoute({ form: serializableForm, response });
|
|
|
|
if (!matchingRoute) {
|
|
throw new Error("No matching route could be found");
|
|
}
|
|
|
|
const decidedAction = matchingRoute.action;
|
|
|
|
let teamMembersMatchingAttributeLogic = null;
|
|
let formResponseId = null;
|
|
let attributeRoutingConfig = null;
|
|
let crmContactOwnerEmail: string | null = null;
|
|
let crmContactOwnerRecordType: string | null = null;
|
|
let crmAppSlug: string | null = null;
|
|
try {
|
|
const result = await handleResponse({
|
|
form: serializableForm,
|
|
formFillerId: uuidv4(),
|
|
response: response,
|
|
chosenRouteId: matchingRoute.id,
|
|
isPreview: isBookingDryRun,
|
|
});
|
|
teamMembersMatchingAttributeLogic = result.teamMembersMatchingAttributeLogic;
|
|
formResponseId = result.formResponse.id;
|
|
attributeRoutingConfig = result.attributeRoutingConfig;
|
|
timeTaken = {
|
|
...timeTaken,
|
|
...result.timeTaken,
|
|
};
|
|
crmContactOwnerEmail = result.crmContactOwnerEmail;
|
|
crmContactOwnerRecordType = result.crmContactOwnerRecordType;
|
|
crmAppSlug = result.crmAppSlug;
|
|
} catch (e) {
|
|
if (e instanceof TRPCError) {
|
|
return {
|
|
props: {
|
|
...pageProps,
|
|
form: serializableForm,
|
|
message: null,
|
|
errorMessage: e.message,
|
|
},
|
|
};
|
|
}
|
|
}
|
|
|
|
// TODO: To be done using sentry tracing
|
|
console.log("Server-Timing", getServerTimingHeader(timeTaken));
|
|
|
|
//TODO: Maybe take action after successful mutation
|
|
if (decidedAction.type === "customPageMessage") {
|
|
return {
|
|
props: {
|
|
...pageProps,
|
|
form: serializableForm,
|
|
message: decidedAction.value,
|
|
errorMessage: null,
|
|
},
|
|
};
|
|
} else if (decidedAction.type === "eventTypeRedirectUrl") {
|
|
const eventTypeUrlWithResolvedVariables = substituteVariables(
|
|
decidedAction.value,
|
|
response,
|
|
serializableForm.fields
|
|
);
|
|
return {
|
|
redirect: {
|
|
destination: getAbsoluteEventTypeRedirectUrlWithEmbedSupport({
|
|
eventTypeRedirectUrl: eventTypeUrlWithResolvedVariables,
|
|
form: serializableForm,
|
|
allURLSearchParams: getUrlSearchParamsToForward({
|
|
formResponse: response,
|
|
fields: serializableForm.fields,
|
|
searchParams: new URLSearchParams(
|
|
stringify({ ...paramsToBeForwardedAsIs, "cal.action": "eventTypeRedirectUrl" })
|
|
),
|
|
teamMembersMatchingAttributeLogic,
|
|
// formResponseId is guaranteed to be set because in catch block of trpc request we return from the function and otherwise it would have been set
|
|
formResponseId: formResponseId!,
|
|
attributeRoutingConfig: attributeRoutingConfig ?? null,
|
|
teamId: form?.teamId,
|
|
orgId: form.team?.parentId,
|
|
crmContactOwnerEmail,
|
|
crmContactOwnerRecordType,
|
|
crmAppSlug,
|
|
}),
|
|
isEmbed: pageProps.isEmbed,
|
|
}),
|
|
permanent: false,
|
|
},
|
|
};
|
|
} else if (decidedAction.type === "externalRedirectUrl") {
|
|
return {
|
|
redirect: {
|
|
destination: `${decidedAction.value}?${stringify(context.query)}&cal.action=externalRedirectUrl`,
|
|
permanent: false,
|
|
},
|
|
};
|
|
}
|
|
|
|
// TODO: Consider throwing error here as there is no value of decidedAction.type that would cause the flow to be here
|
|
return {
|
|
props: {
|
|
...pageProps,
|
|
form: serializableForm,
|
|
message: null,
|
|
errorMessage: "Unhandled type of action",
|
|
},
|
|
};
|
|
};
|