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:
co-authored by
Keith Williams
Peer Richelsen
Amit Sharma
parent
633697a812
commit
fabcf2055b
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
)}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -93,6 +93,7 @@ export const Editor = (props: TextEditorProps) => {
|
||||
readOnly={!editable}
|
||||
style={{ height: props.height }}
|
||||
className="editor-input"
|
||||
data-testid="editor-input"
|
||||
/>
|
||||
}
|
||||
placeholder={
|
||||
|
||||
Reference in New Issue
Block a user