Files
calendar/packages/app-store/routing-forms/trpc/report.handler.ts
T
cf76cf7aae feat: Multiple Options Paste and Routing Form improvements related to Select fields (#15706)
* Add handlePaste function and integrate it with options fields

* Prevents function running if user does not paste a list, allows list items to be pasted without deleting following items

* Subs useState for rhf useWatch to fix issues with multiple form fields

* Sets options array as optional property in zod schema

* Assigns default value to options array to fix possibly undefined type errors

* Sets placedholder property as optional

* maps selectText to options on mount

* Removes comma & semicolon delimiting for pasted lists (new line only) & defines regex as a constant

* Sets default value of placeholder to empty string, provides default fallback for component text field if placeholder is omitted.

* Fix type error on placeholder

* Extracts handlePaste to service function & tests thoroughly

* Some improvements and tests arent needed

* More fixes

* Fix reporting as well

* TS fixes

* Do transforms to and fro from label

* Add unit tests

* Unit test reporting

* Test Improvements

* Self review feedback addressed

---------

Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
2024-08-22 13:38:46 +00:00

144 lines
4.2 KiB
TypeScript

import type { z } from "zod";
import logger from "@calcom/lib/logger";
import type { PrismaClient } from "@calcom/prisma";
import type { App_RoutingForms_FormResponse } from "@calcom/prisma/client";
import { TRPCError } from "@calcom/trpc/server";
import { jsonLogicToPrisma } from "../jsonLogicToPrisma";
import { getSerializableForm } from "../lib/getSerializableForm";
import type { FormResponse } from "../types/types";
import type { zodNonRouterField, zodFieldView } from "../zod";
import type { TReportInputSchema } from "./report.schema";
interface ReportHandlerOptions {
ctx: {
prisma: PrismaClient;
};
input: TReportInputSchema;
}
type Field = z.infer<typeof zodNonRouterField>;
function ensureStringOrStringArray(value: string | number | (string | number)[]): string | string[] {
if (typeof value === "string") {
return value;
} else if (value instanceof Array) {
return value.map((v) => v.toString());
}
return [value.toString()];
}
function getLabelsFromOptionIds({
options,
optionIds,
}: {
options: NonNullable<Field["options"]>;
optionIds: string | string[];
}) {
if (optionIds instanceof Array) {
const labels = optionIds.map((optionId) => {
const foundOption = options.find((option) => option.id === optionId);
// It would mean that the optionId is actually a label which is why it isn't matching any option id.
// Fallback to optionId(i.e. label) which was the case with legacy options
if (!foundOption) {
return optionId;
}
return foundOption.label;
});
return labels;
} else {
const foundOption = options.find((option) => option.id === optionIds);
if (!foundOption) {
return [optionIds];
}
return [foundOption.label];
}
}
export const reportHandler = async ({ ctx: { prisma }, input }: ReportHandlerOptions) => {
// Can be any prisma `where` clause
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const prismaWhere: Record<string, any> = input.jsonLogicQuery
? jsonLogicToPrisma(input.jsonLogicQuery)
: {};
const skip = input.cursor ?? 0;
const take = 50;
logger.debug(
`Built Prisma where ${JSON.stringify(prismaWhere)} from jsonLogicQuery ${JSON.stringify(
input.jsonLogicQuery
)}`
);
const form = await prisma.app_RoutingForms_Form.findUnique({
where: {
id: input.formId,
},
});
if (!form) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Form not found",
});
}
// TODO: Second argument is required to return deleted operators.
const serializedForm = await getSerializableForm({ form, withDeletedFields: true });
const rows = await prisma.app_RoutingForms_FormResponse.findMany({
where: {
formId: input.formId,
...prismaWhere,
},
take,
skip,
});
const fields = serializedForm?.fields || [];
const { responses, headers } = buildResponsesForReporting({
responsesFromDb: rows.map((r) => r.response),
fields,
});
const areThereNoResultsOrLessThanAskedFor = !rows.length || rows.length < take;
return {
headers,
responses,
nextCursor: areThereNoResultsOrLessThanAskedFor ? null : skip + rows.length,
};
};
export default reportHandler;
export function buildResponsesForReporting({
responsesFromDb,
fields,
}: {
responsesFromDb: App_RoutingForms_FormResponse["response"][];
fields: Pick<z.infer<typeof zodFieldView>, "id" | "options" | "label" | "deleted">[];
}) {
const headers = fields.map((f) => f.label + (f.deleted ? "(Deleted)" : ""));
const responses: string[][] = [];
responsesFromDb.forEach((r) => {
const rowResponses: string[] = [];
responses.push(rowResponses);
fields.forEach((field) => {
if (!r) {
return;
}
const response = r as FormResponse;
const value = response[field.id]?.value || "";
if (field.options) {
const optionIds = ensureStringOrStringArray(value);
const labels = getLabelsFromOptionIds({ options: field.options, optionIds });
rowResponses.push(labels.join(", "));
} else {
const arrayOfValues = value instanceof Array ? value : [value];
const transformedValue = arrayOfValues.join(", ");
rowResponses.push(transformedValue);
}
});
});
return { responses, headers };
}