Editor for event type description (#7450)
* add editor for event type description * enable same things for markdownIt * show links in blue * fix placeholder design * format description for event list * limit event descript ot 4 lines in list * shorten event-type description whenever needed * add editor to create event type dialog * fix link title in event type list * Fix overwriting users column when saving event types (#7445) * Only overwrite user column when present * Clean up * Merge branch 'main' into feat/editor-event-type-description * Merge branch 'main' into feat/editor-event-type-description # Conflicts: # apps/web/pages/settings/my-account/profile.tsx * Linting --------- Co-authored-by: CarinaWolli <wollencarina@gmail.com> Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com> Co-authored-by: zomars <zomars@me.com>
This commit is contained in:
parent
e12b21a73c
commit
cfb625e934
|
@ -102,7 +102,7 @@ const BookingDescription: FC<Props> = (props) => {
|
|||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="max-w-[calc(100%_-_2rem)] flex-shrink break-words">
|
||||
<div className="max-w-[calc(100%_-_2rem)] flex-shrink break-words [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600">
|
||||
<EventTypeDescriptionSafeHTML eventType={eventType} />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { isValidPhoneNumber } from "libphonenumber-js";
|
||||
import MarkdownIt from "markdown-it";
|
||||
import { Trans } from "next-i18next";
|
||||
import Link from "next/link";
|
||||
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
||||
|
@ -14,13 +15,16 @@ import { getEventLocationType, MeetLocationType, LocationType } from "@calcom/ap
|
|||
import { CAL_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { slugify } from "@calcom/lib/slugify";
|
||||
import { Button, Label, Select, SettingsToggle, Skeleton, TextField } from "@calcom/ui";
|
||||
import turndown from "@calcom/lib/turndownService";
|
||||
import { Button, Editor, Label, Select, SettingsToggle, Skeleton, TextField } from "@calcom/ui";
|
||||
import { FiEdit2, FiCheck, FiX, FiPlus } from "@calcom/ui/components/icon";
|
||||
|
||||
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
|
||||
import type { SingleValueLocationOption, LocationOption } from "@components/ui/form/LocationSelect";
|
||||
import LocationSelect from "@components/ui/form/LocationSelect";
|
||||
|
||||
const md = new MarkdownIt("default", { html: true, breaks: true, linkify: true });
|
||||
|
||||
const getLocationFromType = (
|
||||
type: EventLocationType["type"],
|
||||
locationOptions: Pick<EventTypeSetupProps, "locationOptions">["locationOptions"]
|
||||
|
@ -272,12 +276,15 @@ export const EventSetupTab = (
|
|||
defaultValue={eventType.title}
|
||||
{...formMethods.register("title")}
|
||||
/>
|
||||
<TextField
|
||||
label={t("description")}
|
||||
placeholder={t("quick_video_meeting")}
|
||||
defaultValue={eventType.description ?? ""}
|
||||
{...formMethods.register("description")}
|
||||
/>
|
||||
<div>
|
||||
<Label>{t("description")}</Label>
|
||||
<Editor
|
||||
getText={() => md.render(formMethods.getValues("description") || eventType.description || "")}
|
||||
setText={(value: string) => formMethods.setValue("description", turndown(value))}
|
||||
excludedToolbarItems={["blockType"]}
|
||||
placeholder={t("quick_video_meeting")}
|
||||
/>
|
||||
</div>
|
||||
<TextField
|
||||
required
|
||||
label={t("URL")}
|
||||
|
|
|
@ -14,7 +14,7 @@ import { Avatar } from "@calcom/ui";
|
|||
|
||||
import type { IOnboardingPageProps } from "../../../pages/getting-started/[[...step]]";
|
||||
|
||||
const md = new MarkdownIt("default", { html: true, breaks: true });
|
||||
const md = new MarkdownIt("default", { html: true, breaks: true, linkify: true });
|
||||
|
||||
type FormData = {
|
||||
bio: string;
|
||||
|
|
|
@ -124,13 +124,7 @@ async function getUserPageProps(context: GetStaticPropsContext) {
|
|||
},
|
||||
});
|
||||
|
||||
const md = new MarkdownIt("zero").enable([
|
||||
//
|
||||
"emphasis",
|
||||
"list",
|
||||
"newline",
|
||||
"strikethrough",
|
||||
]);
|
||||
const md = new MarkdownIt("default", { html: true, breaks: true, linkify: true });
|
||||
|
||||
if (!user || !user.eventTypes.length) return { notFound: true };
|
||||
|
||||
|
|
|
@ -119,7 +119,7 @@ const Item = ({ type, group, readOnly }: { type: EventType; group: EventTypeGrou
|
|||
<Link
|
||||
href={`/event-types/${type.id}?tabName=setup`}
|
||||
className="flex-1 overflow-hidden pr-4 text-sm"
|
||||
title={`${type.title} ${type.description ? `– ${type.description}` : ""}`}>
|
||||
title={type.title}>
|
||||
<div>
|
||||
<span
|
||||
className="font-semibold text-gray-700 ltr:mr-1 rtl:ml-1"
|
||||
|
@ -142,6 +142,7 @@ const Item = ({ type, group, readOnly }: { type: EventType; group: EventTypeGrou
|
|||
<EventTypeDescription
|
||||
// @ts-expect-error FIXME: We have a type mismatch here @hariombalhara @sean-brydon
|
||||
eventType={type}
|
||||
shortenDescription
|
||||
/>
|
||||
</Link>
|
||||
);
|
||||
|
|
|
@ -41,7 +41,7 @@ import { FiAlertTriangle, FiTrash2 } from "@calcom/ui/components/icon";
|
|||
import TwoFactor from "@components/auth/TwoFactor";
|
||||
import { UsernameAvailabilityField } from "@components/ui/UsernameAvailability";
|
||||
|
||||
const md = new MarkdownIt("default", { html: true, breaks: true });
|
||||
const md = new MarkdownIt("default", { html: true, breaks: true, linkify: true });
|
||||
|
||||
const SkeletonLoader = ({ title, description }: { title: string; description: string }) => {
|
||||
return (
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { SchedulingType } from "@prisma/client";
|
||||
import { isValidPhoneNumber } from "libphonenumber-js";
|
||||
import MarkdownIt from "markdown-it";
|
||||
import { useRouter } from "next/router";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
|
@ -9,6 +10,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|||
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
import turndown from "@calcom/lib/turndownService";
|
||||
import { createEventTypeInput } from "@calcom/prisma/zod/custom/eventtype";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import {
|
||||
|
@ -20,10 +22,12 @@ import {
|
|||
Form,
|
||||
RadioGroup as RadioArea,
|
||||
showToast,
|
||||
TextAreaField,
|
||||
TextField,
|
||||
Editor,
|
||||
} from "@calcom/ui";
|
||||
|
||||
const md = new MarkdownIt("default", { html: true, breaks: true, linkify: true });
|
||||
|
||||
// this describes the uniform data needed to create a new event type on Profile or Team
|
||||
export interface EventTypeParent {
|
||||
teamId: number | null | undefined; // if undefined, then it's a profile
|
||||
|
@ -167,10 +171,11 @@ export default function CreateEventTypeDialog() {
|
|||
/>
|
||||
)}
|
||||
|
||||
<TextAreaField
|
||||
label={t("description")}
|
||||
<Editor
|
||||
getText={() => md.render(form.getValues("description") || "")}
|
||||
setText={(value: string) => form.setValue("description", turndown(value))}
|
||||
excludedToolbarItems={["blockType", "link"]}
|
||||
placeholder={t("quick_video_meeting")}
|
||||
{...register("description")}
|
||||
/>
|
||||
|
||||
<div className="relative">
|
||||
|
@ -198,26 +203,26 @@ export default function CreateEventTypeDialog() {
|
|||
message={form.formState.errors.schedulingType.message}
|
||||
/>
|
||||
)}
|
||||
<RadioArea.Group className="mt-1 flex space-x-4">
|
||||
<RadioArea.Group className="flex mt-1 space-x-4">
|
||||
<RadioArea.Item
|
||||
{...register("schedulingType")}
|
||||
value={SchedulingType.COLLECTIVE}
|
||||
className="w-1/2 text-sm">
|
||||
<strong className="mb-1 block">{t("collective")}</strong>
|
||||
<strong className="block mb-1">{t("collective")}</strong>
|
||||
<p>{t("collective_description")}</p>
|
||||
</RadioArea.Item>
|
||||
<RadioArea.Item
|
||||
{...register("schedulingType")}
|
||||
value={SchedulingType.ROUND_ROBIN}
|
||||
className="w-1/2 text-sm">
|
||||
<strong className="mb-1 block">{t("round_robin")}</strong>
|
||||
<strong className="block mb-1">{t("round_robin")}</strong>
|
||||
<p>{t("round_robin_description")}</p>
|
||||
</RadioArea.Item>
|
||||
</RadioArea.Group>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-8 flex flex-row-reverse gap-x-2">
|
||||
<div className="flex flex-row-reverse mt-8 gap-x-2">
|
||||
<Button type="submit" loading={createMutation.isLoading}>
|
||||
{t("continue")}
|
||||
</Button>
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { Prisma, SchedulingType } from "@prisma/client";
|
||||
import MarkdownIt from "markdown-it";
|
||||
import { useMemo } from "react";
|
||||
import { FormattedNumber, IntlProvider } from "react-intl";
|
||||
import { z } from "zod";
|
||||
|
@ -28,9 +29,16 @@ export type EventTypeDescriptionProps = {
|
|||
seatsPerTimeSlot?: number;
|
||||
};
|
||||
className?: string;
|
||||
shortenDescription?: true;
|
||||
};
|
||||
|
||||
export const EventTypeDescription = ({ eventType, className }: EventTypeDescriptionProps) => {
|
||||
const md = new MarkdownIt("default", { html: true, breaks: false, linkify: true });
|
||||
|
||||
export const EventTypeDescription = ({
|
||||
eventType,
|
||||
className,
|
||||
shortenDescription,
|
||||
}: EventTypeDescriptionProps) => {
|
||||
const { t } = useLocale();
|
||||
|
||||
const recurringEvent = useMemo(
|
||||
|
@ -44,10 +52,17 @@ export const EventTypeDescription = ({ eventType, className }: EventTypeDescript
|
|||
<>
|
||||
<div className={classNames("dark:text-darkgray-800 text-gray-500", className)}>
|
||||
{eventType.description && (
|
||||
<p className="dark:text-darkgray-800 max-w-[280px] break-words py-1 text-sm text-gray-500 sm:max-w-[500px]">
|
||||
{eventType.description.substring(0, 300)}
|
||||
{eventType.description.length > 300 && "..."}
|
||||
</p>
|
||||
<div
|
||||
className={classNames(
|
||||
"dark:text-darkgray-800 max-w-[280px] break-words py-1 text-sm text-gray-500 sm:max-w-[500px] [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600",
|
||||
shortenDescription ? "line-clamp-4" : ""
|
||||
)}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: shortenDescription
|
||||
? md.render(eventType.description?.replace(/<p><br><\/p>|\n/g, " "))
|
||||
: md.render(eventType.description),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<ul className="mt-2 flex flex-wrap space-x-2 rtl:space-x-reverse">
|
||||
{eventType.metadata?.multipleDuration ? (
|
||||
|
|
|
@ -1,13 +1,7 @@
|
|||
import type { PrismaClient, EventType } from "@prisma/client";
|
||||
import MarkdownIt from "markdown-it";
|
||||
|
||||
const md = new MarkdownIt("zero").enable([
|
||||
//
|
||||
"emphasis",
|
||||
"list",
|
||||
"newline",
|
||||
"strikethrough",
|
||||
]);
|
||||
const md = new MarkdownIt("default", { html: true, breaks: true, linkify: true });
|
||||
|
||||
function parseAndSanitize(description: string) {
|
||||
const parsedMarkdown = md.render(description);
|
||||
|
|
|
@ -30,6 +30,7 @@ export type TextEditorProps = {
|
|||
excludedToolbarItems?: string[];
|
||||
variables?: string[];
|
||||
height?: string;
|
||||
placeholder?: string;
|
||||
};
|
||||
|
||||
const editorConfig = {
|
||||
|
@ -67,7 +68,7 @@ export const Editor = (props: TextEditorProps) => {
|
|||
<div className="editor-inner" style={{ height: props.height }}>
|
||||
<RichTextPlugin
|
||||
contentEditable={<ContentEditable style={{ height: props.height }} className="editor-input" />}
|
||||
placeholder=""
|
||||
placeholder={<div className="-mt-11 p-3 text-sm text-gray-300">{props.placeholder || ""}</div>}
|
||||
/>
|
||||
<AutoFocusPlugin />
|
||||
<ListPlugin />
|
||||
|
|
Loading…
Reference in New Issue
Block a user