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:
Carina Wollendorfer 2023-03-02 14:55:25 -05:00 committed by GitHub
parent e12b21a73c
commit cfb625e934
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 56 additions and 39 deletions

View File

@ -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>

View File

@ -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")}

View File

@ -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;

View File

@ -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 };

View File

@ -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>
);

View File

@ -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 (

View File

@ -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>

View File

@ -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 ? (

View File

@ -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);

View File

@ -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 />