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 <keithwillcode@gmail.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com>
This commit is contained in:
Anik Dhabal Babu
2024-09-05 09:05:45 +00:00
committed by GitHub
co-authored by Keith Williams Peer Richelsen Amit Sharma
parent 633697a812
commit fabcf2055b
9 changed files with 118 additions and 38 deletions
@@ -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 = {
@@ -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 (
<>
<div className="text-emphasis mt-4 font-medium">{label}</div>
<div
className="text-emphasis mt-4 font-medium"
dangerouslySetInnerHTML={{
__html: label,
}}
/>
<p
className="text-default break-words"
data-testid="field-response"
@@ -4,6 +4,7 @@ import type { createUsersFixture } from "playwright/fixtures/users";
import { uuid } from "short-uuid";
import { fieldTypesConfigMap } from "@calcom/features/form-builder/fieldTypes";
import { md } from "@calcom/lib/markdownIt";
import prisma from "@calcom/prisma";
import { WebhookTriggerEvents } from "@calcom/prisma/enums";
import type { CalendarEvent } from "@calcom/types/Calendar";
@@ -549,7 +550,12 @@ async function addQuestionAndSave({
}
if (question.label !== undefined) {
await page.fill('[name="label"]', question.label);
if (question.type === "Checkbox") {
const editorInput = page.locator('[data-testid="editor-input"]');
await editorInput.fill(md.render(question.label));
} else {
await page.fill('[name="label"]', question.label);
}
}
if (question.placeholder !== undefined) {
+20 -15
View File
@@ -9,18 +9,34 @@ export const Info = (props: {
withSpacer?: boolean;
lineThrough?: boolean;
formatted?: boolean;
isLabelHTML?: boolean;
}) => {
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 (
<p
className="dark:text-darkgray-600 mt-2 text-sm text-gray-500 [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
dangerouslySetInnerHTML={{
__html: htmlContent
.replaceAll("<p>", `<p style="${css}">`)
.replaceAll("<li>", `<li style="${css}">`),
}}
/>
);
};
return (
<>
{props.withSpacer && <Spacer />}
<div>
<p style={{ color: "#101010" }}>{props.label}</p>
<p style={{ color: "#101010" }}>
{props.isLabelHTML ? <StyledHtmlContent htmlContent={safeLabel} /> : props.label}
</p>
<p
style={{
color: "#101010",
@@ -29,18 +45,7 @@ export const Info = (props: {
whiteSpace: "pre-wrap",
textDecoration: props.lineThrough ? "line-through" : undefined,
}}>
{props.formatted ? (
<p
className="dark:text-darkgray-600 mt-2 text-sm text-gray-500 [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
dangerouslySetInnerHTML={{
__html: safeDescription
.replaceAll("<p>", `<p style="${descriptionCSS}">`)
.replaceAll("<li>", `<li style="${descriptionCSS}">`),
}}
/>
) : (
props.description
)}
{props.formatted ? <StyledHtmlContent htmlContent={safeDescription} /> : props.description}
</p>
{props.extraInfo}
</div>
@@ -25,6 +25,7 @@ export function UserFieldsResponses(props: { calEvent: CalendarEvent; t: TFuncti
: `${labelValueMap[key] ? labelValueMap[key] : ""}`
}
withSpacer
isLabelHTML
/>
) : null
)}
+51 -15
View File
@@ -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<RhfFormField> }) => {
const { t } = useLocale();
const [firstRender, setFirstRender] = useState(true);
return (
<div className="mt-6">
<Label>{t("label")}</Label>
<Editor
getText={() => 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") || "")}
/>
</div>
);
};
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 (
<Dialog open={dialog.isOpen} onOpenChange={onOpenChange}>
<DialogContent className="max-h-none p-0" data-testid="edit-field-dialog">
<Dialog open={dialog.isOpen} onOpenChange={onOpenChange} modal={false}>
<DialogContent
className="max-h-none p-0"
data-testid="edit-field-dialog"
forceOverlayWhenNoModal={true}>
<Form id="form-builder" form={fieldForm} handleSubmit={handleSubmit}>
<div className="h-auto max-h-[85vh] overflow-auto px-8 pb-7 pt-8">
<DialogHeader title={t("add_a_booking_question")} subtitle={t("booking_questions_description")} />
@@ -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 })}
/>
<InputField
{...fieldForm.register("label")}
// System fields have a defaultLabel, so there a label is not required
required={
!["system", "system-but-optional"].includes(fieldForm.getValues("editable") || "")
}
placeholder={t(fieldForm.getValues("defaultLabel") || "")}
containerClassName="mt-6"
label={t("label")}
/>
<div>
{formFieldType === "boolean" ? (
<CheckboxFieldLabel fieldForm={fieldForm} />
) : (
<InputField
{...fieldForm.register("label")}
// System fields have a defaultLabel, so there a label is not required
required={
!["system", "system-but-optional"].includes(fieldForm.getValues("editable") || "")
}
placeholder={t(fieldForm.getValues("defaultLabel") || "")}
containerClassName="mt-6"
label={t("label")}
/>
)}
</div>
{fieldType?.isTextType ? (
<InputField
+13 -3
View File
@@ -145,8 +145,18 @@ export const pageObject = {
}) => {
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(() => {
+10 -2
View File
@@ -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<HTMLDivElement, DialogContentProps>(
({ 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<HTMLDivElement, DialogContentProps
);
return (
<Portal>
<Overlay className="fadeIn fixed inset-0 z-50 bg-neutral-800 bg-opacity-70 transition-opacity dark:bg-opacity-70 " />
{forceOverlayWhenNoModal ? (
<div className="fadeIn fixed inset-0 z-50 bg-neutral-800 bg-opacity-70 transition-opacity dark:bg-opacity-70 " />
) : (
<Overlay className="fadeIn fixed inset-0 z-50 bg-neutral-800 bg-opacity-70 transition-opacity dark:bg-opacity-70 " />
)}
<Content
{...props}
className={classNames(
+1
View File
@@ -93,6 +93,7 @@ export const Editor = (props: TextEditorProps) => {
readOnly={!editable}
style={{ height: props.height }}
className="editor-input"
data-testid="editor-input"
/>
}
placeholder={