From fabcf2055be5da83f45cf480a2ea82d3c0d21ac3 Mon Sep 17 00:00:00 2001 From: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com> Date: Thu, 5 Sep 2024 14:35:45 +0530 Subject: [PATCH] feat: Ability to Hyperlink text in Checkbox on Additional Questions (#15194) * feat: Ability to Hyperlink text in Checkbox on Additional Questions * update * fix and update * fix type error * fix and update * dpdate * fix type error * fix unit test * Update * fix tests * revert test * remove yarn.lock * remove schema.prisma * update * remove log * revert --------- Co-authored-by: Keith Williams Co-authored-by: Peer Richelsen Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com> --- ...ookings-single-view.getServerSideProps.tsx | 8 +++ .../bookings/views/bookings-single-view.tsx | 9 ++- .../manage-booking-questions.e2e.ts | 8 ++- packages/emails/src/components/Info.tsx | 35 +++++----- .../src/components/UserFieldsResponses.tsx | 1 + .../features/form-builder/FormBuilder.tsx | 66 ++++++++++++++----- packages/features/form-builder/testUtils.ts | 16 ++++- packages/ui/components/dialog/Dialog.tsx | 12 +++- packages/ui/components/editor/Editor.tsx | 1 + 9 files changed, 118 insertions(+), 38 deletions(-) diff --git a/apps/web/modules/bookings/views/bookings-single-view.getServerSideProps.tsx b/apps/web/modules/bookings/views/bookings-single-view.getServerSideProps.tsx index 0524350b04..d602ea2692 100644 --- a/apps/web/modules/bookings/views/bookings-single-view.getServerSideProps.tsx +++ b/apps/web/modules/bookings/views/bookings-single-view.getServerSideProps.tsx @@ -6,6 +6,7 @@ import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; import getBookingInfo from "@calcom/features/bookings/lib/getBookingInfo"; import { parseRecurringEvent } from "@calcom/lib"; import { getDefaultEvent } from "@calcom/lib/defaultEvents"; +import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML"; import { maybeGetBookingUidFromSeat } from "@calcom/lib/server/maybeGetBookingUidFromSeat"; import { BookingRepository } from "@calcom/lib/server/repository/booking"; import prisma from "@calcom/prisma"; @@ -120,6 +121,13 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { metadata: EventTypeMetaDataSchema.parse(eventTypeRaw.metadata), recurringEvent: parseRecurringEvent(eventTypeRaw.recurringEvent), customInputs: customInputSchema.array().parse(eventTypeRaw.customInputs), + bookingFields: eventTypeRaw.bookingFields.map((field) => { + return { + ...field, + label: markdownToSafeHTML(field.label || ""), + defaultLabel: markdownToSafeHTML(field.defaultLabel || ""), + }; + }), }; const profile = { diff --git a/apps/web/modules/bookings/views/bookings-single-view.tsx b/apps/web/modules/bookings/views/bookings-single-view.tsx index d0f2c42acd..289ac1b072 100644 --- a/apps/web/modules/bookings/views/bookings-single-view.tsx +++ b/apps/web/modules/bookings/views/bookings-single-view.tsx @@ -643,11 +643,16 @@ export default function Success(props: PageProps) { ) return null; - const label = field.label || t(field.defaultLabel || ""); + const label = field.label || t(field.defaultLabel); return ( <> -
{label}
+

{ if (!props.description || props.description === "") return null; - const descriptionCSS = "color: '#101010'; font-weight: 400; line-height: 24px; margin: 0;"; - const safeDescription = markdownToSafeHTML(props.description.toString()) || ""; + const safeLabel = markdownToSafeHTML(props.label.toString()); + + const StyledHtmlContent = ({ htmlContent }: { htmlContent: string }) => { + const css = "color: '#101010'; font-weight: 400; line-height: 24px; margin: 0;"; + return ( +

", `

`) + .replaceAll("

  • ", `
  • `), + }} + /> + ); + }; return ( <> {props.withSpacer && }
    -

    {props.label}

    +

    + {props.isLabelHTML ? : props.label} +

    - {props.formatted ? ( -

    ", `

    `) - .replaceAll("

  • ", `
  • `), - }} - /> - ) : ( - props.description - )} + {props.formatted ? : props.description}

    {props.extraInfo}
  • diff --git a/packages/emails/src/components/UserFieldsResponses.tsx b/packages/emails/src/components/UserFieldsResponses.tsx index c2a018936a..0df70b467b 100644 --- a/packages/emails/src/components/UserFieldsResponses.tsx +++ b/packages/emails/src/components/UserFieldsResponses.tsx @@ -25,6 +25,7 @@ export function UserFieldsResponses(props: { calEvent: CalendarEvent; t: TFuncti : `${labelValueMap[key] ? labelValueMap[key] : ""}` } withSpacer + isLabelHTML /> ) : null )} diff --git a/packages/features/form-builder/FormBuilder.tsx b/packages/features/form-builder/FormBuilder.tsx index 88ee25f90f..0bc8f52b20 100644 --- a/packages/features/form-builder/FormBuilder.tsx +++ b/packages/features/form-builder/FormBuilder.tsx @@ -6,7 +6,9 @@ import type { z } from "zod"; import { classNames } from "@calcom/lib"; import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { md } from "@calcom/lib/markdownIt"; import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML"; +import turndown from "@calcom/lib/turndownService"; import { Badge, BooleanToggleGroupField, @@ -24,6 +26,7 @@ import { Label, SelectField, showToast, + Editor, Switch, } from "@calcom/ui"; @@ -421,6 +424,27 @@ function Options({ ); } +const CheckboxFieldLabel = ({ fieldForm }: { fieldForm: UseFormReturn }) => { + const { t } = useLocale(); + const [firstRender, setFirstRender] = useState(true); + return ( +
    + + md.render(fieldForm.getValues("label") || "")} + setText={(value: string) => { + fieldForm.setValue("label", turndown(value), { shouldDirty: true }); + }} + excludedToolbarItems={["blockType", "bold", "italic"]} + disableLists + firstRender={firstRender} + setFirstRender={setFirstRender} + placeholder={t(fieldForm.getValues("defaultLabel") || "")} + /> +
    + ); +}; + function FieldEditDialog({ dialog, onOpenChange, @@ -437,19 +461,22 @@ function FieldEditDialog({ defaultValues: dialog.data || {}, // resolver: zodResolver(fieldSchema), }); + const formFieldType = fieldForm.getValues("type"); + useEffect(() => { - if (!fieldForm.getValues("type")) { + if (!formFieldType) { return; } const variantsConfig = getVariantsConfig({ - type: fieldForm.getValues("type"), + type: formFieldType, variantsConfig: fieldForm.getValues("variantsConfig"), }); // We need to set the variantsConfig in the RHF instead of using a derived value because RHF won't have the variantConfig for the variant that's not rendered yet. fieldForm.setValue("variantsConfig", variantsConfig); }, [fieldForm]); + const isFieldEditMode = !!dialog.data; const fieldType = getCurrentFieldType(fieldForm); @@ -458,8 +485,11 @@ function FieldEditDialog({ const fieldTypes = Object.values(fieldTypesConfigMap); return ( - - + +
    @@ -478,7 +508,7 @@ function FieldEditDialog({ } fieldForm.setValue("type", value, { shouldDirty: true }); }} - value={fieldTypesConfigMap[fieldForm.getValues("type")]} + value={fieldTypesConfigMap[formFieldType]} options={fieldTypes.filter((f) => !f.systemOnly)} label={t("input_type")} /> @@ -505,16 +535,22 @@ function FieldEditDialog({ description={t("disable_input_if_prefilled")} {...fieldForm.register("disableOnPrefill", { setValueAs: Boolean })} /> - +
    + {formFieldType === "boolean" ? ( + + ) : ( + + )} +
    {fieldType?.isTextType ? ( { fireEvent.change(dialog.getAllByRole("textbox")[0], { target: { value: identifier } }); }, - fillInFieldLabel: ({ dialog, label }: { dialog: TestingLibraryElement; label: string }) => { - fireEvent.change(dialog.getAllByRole("textbox")[1], { target: { value: label } }); + fillInFieldLabel: ({ + dialog, + label, + fieldType, + }: { + dialog: TestingLibraryElement; + label: string; + fieldType: string; + }) => { + if (fieldType !== "boolean") { + fireEvent.change(dialog.getAllByRole("textbox")[1], { target: { value: label } }); + } }, close: ({ dialog }: { dialog: TestingLibraryElement }) => { fireEvent.click(dialog.getByTestId("dialog-rejection")); @@ -183,7 +193,7 @@ export const verifier = { const dialog = pageObject.openAddFieldDialog(); pageObject.dialog.selectFieldType({ dialog, fieldType: props.fieldType }); pageObject.dialog.fillInFieldIdentifier({ dialog, identifier: props.identifier }); - pageObject.dialog.fillInFieldLabel({ dialog, label: props.label }); + pageObject.dialog.fillInFieldLabel({ dialog, label: props.label, fieldType: props.fieldType }); pageObject.dialog.saveField({ dialog: getEditDialogForm() }); await waitFor(() => { diff --git a/packages/ui/components/dialog/Dialog.tsx b/packages/ui/components/dialog/Dialog.tsx index 2d88f9648a..c1b09b158d 100644 --- a/packages/ui/components/dialog/Dialog.tsx +++ b/packages/ui/components/dialog/Dialog.tsx @@ -89,11 +89,15 @@ type DialogContentProps = React.ComponentProps<(typeof DialogPrimitive)["Content actionDisabled?: boolean; Icon?: IconName; enableOverflow?: boolean; + forceOverlayWhenNoModal?: boolean; }; // enableOverflow:- use this prop whenever content inside DialogContent could overflow and require scrollbar export const DialogContent = React.forwardRef( - ({ children, title, Icon: icon, enableOverflow, type = "creation", ...props }, forwardedRef) => { + ( + { children, title, Icon: icon, enableOverflow, forceOverlayWhenNoModal, type = "creation", ...props }, + forwardedRef + ) => { const isPlatform = useIsPlatform(); const [Portal, Overlay, Content] = useMemo( () => @@ -108,7 +112,11 @@ export const DialogContent = React.forwardRef - + {forceOverlayWhenNoModal ? ( +
    + ) : ( + + )} { readOnly={!editable} style={{ height: props.height }} className="editor-input" + data-testid="editor-input" /> } placeholder={