"use client"; import React, { useCallback, useRef, useState, useEffect } from "react"; import type { BuilderProps, Config, ImmutableTree, JsonLogicResult, JsonTree, } from "react-awesome-query-builder"; import { Builder, Query, Utils as QbUtils } from "react-awesome-query-builder"; import { classNames } from "@calcom/lib"; import { downloadAsCsv, sanitizeValue } from "@calcom/lib/csvUtils"; import { useInViewObserver } from "@calcom/lib/hooks/useInViewObserver"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { trpc } from "@calcom/trpc/react"; import type { inferSSRProps } from "@calcom/types/inferSSRProps"; import { Button, showToast } from "@calcom/ui"; import SingleForm, { getServerSidePropsForSingleFormView as getServerSideProps, } from "../../components/SingleForm"; import { withRaqbSettingsAndWidgets, ConfigFor, } from "../../components/react-awesome-query-builder/config/uiConfig"; import type { JsonLogicQuery } from "../../jsonLogicToPrisma"; import { getQueryBuilderConfigForFormFields, type FormFieldsQueryBuilderConfigWithRaqbFields, } from "../../lib/getQueryBuilderConfig"; export { getServerSideProps }; const Result = ({ formName, formId, jsonLogicQuery, }: { formName: string; formId: string; jsonLogicQuery: JsonLogicQuery | null; }) => { const { t } = useLocale(); const [isDownloading, setIsDownloading] = useState(false); const { isPending, status, data, isFetching, error, isFetchingNextPage, hasNextPage, fetchNextPage } = trpc.viewer.appRoutingForms.report.useInfiniteQuery( { formId: formId, // Send jsonLogicQuery only if it's a valid logic, otherwise send a logic with no query. jsonLogicQuery: jsonLogicQuery?.logic ? jsonLogicQuery : { logic: {}, }, }, { getNextPageParam: (lastPage) => lastPage.nextCursor, } ); const exportQuery = trpc.viewer.appRoutingForms.report.useInfiniteQuery( { limit: 100, // 100 is max formId: formId, // Send jsonLogicQuery only if it's a valid logic, otherwise send a logic with no query. jsonLogicQuery: jsonLogicQuery?.logic ? jsonLogicQuery : { logic: {}, }, }, { getNextPageParam: (lastPage) => lastPage.nextCursor, enabled: false, } ); const buttonInView = useInViewObserver(() => { if (!isFetching && hasNextPage && status === "success") { fetchNextPage(); } }); const headers = useRef(null); if (!isPending && !data) { return
Error loading report {error?.message}
; } headers.current = (data?.pages && data?.pages[0]?.headers) || headers.current; const numberOfRows = data?.pages.reduce((total, page) => total + (page.responses?.length || 0), 0); const downloadButtonDisabled = !numberOfRows || !data || !headers.current || isDownloading; const handleDownload = async () => { try { setIsDownloading(true); if (downloadButtonDisabled) { return; } const result = await exportQuery.refetch(); if (!result.data) { throw new Error("There are no routing forms found."); } const allRows = result.data.pages.flatMap((page) => page.responses.map((response) => `${response.map((value) => sanitizeValue(value)).join(",")}\n`) ); let lastPage = result.data.pages[result.data.pages.length - 1]; while (lastPage.nextCursor) { const nextPage = await exportQuery.fetchNextPage(); if (!nextPage.data) { break; } const latestPageItems = nextPage.data.pages[nextPage.data.pages.length - 1].responses ?? []; allRows.push( ...latestPageItems.map((response) => `${response.map((value) => sanitizeValue(value)).join(",")}\n`) ); lastPage = nextPage.data.pages[nextPage.data.pages.length - 1]; } const header = `${(headers.current ?? []).map((value) => sanitizeValue(value)).join(",")}\n`; const csvRaw = header + allRows.join(""); const filename = `${formName}_${new Date().toISOString().split("T")[0]}.csv`; // e.g., ${FormName}_2024-10-30.csv downloadAsCsv(csvRaw, filename); } catch (error) { showToast(`Error: ${error}`, "error"); } finally { setIsDownloading(false); } }; return (
{!isPending && (
{`${numberOfRows} ${numberOfRows === 1 ? t("row") : t("rows")}`}
)} {headers.current?.map((header, index) => ( ))} {!isPending && data?.pages.map((page) => { return page.responses?.map((responses, rowIndex) => { const isLastRow = page.responses.length - 1 === rowIndex; return ( {responses.map((r, columnIndex) => { const isLastColumn = columnIndex === responses.length - 1; return ( ); })} ); }); })}
{header}
{r}
{isPending ?
{t("loading")}
: ""} {hasNextPage && ( )}
); }; const getInitialQuery = (config: ReturnType) => { const uuid = QbUtils.uuid(); const queryValue: JsonTree = { id: uuid, type: "group" } as JsonTree; const tree = QbUtils.checkTree(QbUtils.loadTree(queryValue), config as unknown as Config); return { state: { tree, config }, queryValue, }; }; const Reporter = ({ form }: { form: inferSSRProps["form"] }) => { const config = getQueryBuilderConfigForFormFields(form, true); const [query, setQuery] = useState(getInitialQuery(config)); const [jsonLogicQuery, setJsonLogicQuery] = useState(null); const onChange = (immutableTree: ImmutableTree, config: FormFieldsQueryBuilderConfigWithRaqbFields) => { const jsonTree = QbUtils.getTree(immutableTree); setQuery(() => { const newValue = { state: { tree: immutableTree, config: config }, queryValue: jsonTree, }; setJsonLogicQuery(QbUtils.jsonLogicFormat(newValue.state.tree, config as unknown as Config)); return newValue; }); }; const renderBuilder = useCallback( (props: BuilderProps) => (
), [] ); return (
{ onChange(immutableTree, config as unknown as FormFieldsQueryBuilderConfigWithRaqbFields); }} renderBuilder={renderBuilder} />
); }; export default function ReporterWrapper({ ...props }: inferSSRProps & { appUrl: string }) { const [isClient, setIsClient] = useState(false); useEffect(() => { // It isn't possible to render Reporter without hydration errors if it is rendered on the server. // This is because the RAQB generates some dynamic ids on elements which change b/w client and server. // This is a workaround to render the Reporter on the client only. setIsClient(true); }, []); return ( (
{isClient && }
)} /> ); }