From 859fef24f45b208bcbe6f91ad5c1e01527c3dc75 Mon Sep 17 00:00:00 2001 From: Aaryan Khandelwal <65252264+aaryan610@users.noreply.github.com> Date: Tue, 7 Feb 2023 11:20:41 +0530 Subject: [PATCH] feat: link option in remirror (#240) * feat: link option in remirror * fix: removed link import from remirror toolbar --- .../issues/sidebar-select/blocker.tsx | 2 +- apps/app/components/issues/sidebar.tsx | 48 ++- .../issues/sub-issues-list-modal.tsx | 21 +- .../app/components/rich-text-editor/index.tsx | 5 +- .../rich-text-editor/toolbar/index.tsx | 1 - .../rich-text-editor/toolbar/link.tsx | 403 ++++++++---------- .../rich-text-editor/toolbar/undo.tsx | 1 + apps/app/next.config.js | 6 +- .../projects/[projectId]/issues/[issueId].tsx | 46 +- apps/app/styles/editor.css | 13 + 10 files changed, 273 insertions(+), 273 deletions(-) diff --git a/apps/app/components/issues/sidebar-select/blocker.tsx b/apps/app/components/issues/sidebar-select/blocker.tsx index 789c11d1ca..2ab39f6af9 100644 --- a/apps/app/components/issues/sidebar-select/blocker.tsx +++ b/apps/app/components/issues/sidebar-select/blocker.tsx @@ -16,7 +16,7 @@ import issuesServices from "services/issues.service"; // ui import { Button } from "components/ui"; // icons -import { FolderIcon, MagnifyingGlassIcon, XMarkIcon } from "@heroicons/react/24/outline"; +import { MagnifyingGlassIcon, XMarkIcon } from "@heroicons/react/24/outline"; import { BlockerIcon, LayerDiagonalIcon } from "components/icons"; // types import { IIssue, UserAuth } from "types"; diff --git a/apps/app/components/issues/sidebar.tsx b/apps/app/components/issues/sidebar.tsx index 54f94f6abf..d5be6949ff 100644 --- a/apps/app/components/issues/sidebar.tsx +++ b/apps/app/components/issues/sidebar.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useCallback, useState } from "react"; import { useRouter } from "next/router"; @@ -113,29 +113,35 @@ export const IssueDetailsSidebar: React.FC = ({ }); }; - const handleCycleChange = (cycleDetail: ICycle) => { - if (!workspaceSlug || !projectId || !issueDetail) return; + const handleCycleChange = useCallback( + (cycleDetail: ICycle) => { + if (!workspaceSlug || !projectId || !issueDetail) return; - issuesServices - .addIssueToCycle(workspaceSlug as string, projectId as string, cycleDetail.id, { - issues: [issueDetail.id], - }) - .then((res) => { - mutate(ISSUE_DETAILS(issueId as string)); - }); - }; + issuesServices + .addIssueToCycle(workspaceSlug as string, projectId as string, cycleDetail.id, { + issues: [issueDetail.id], + }) + .then((res) => { + mutate(ISSUE_DETAILS(issueId as string)); + }); + }, + [workspaceSlug, projectId, issueId, issueDetail] + ); - const handleModuleChange = (moduleDetail: IModule) => { - if (!workspaceSlug || !projectId || !issueDetail) return; + const handleModuleChange = useCallback( + (moduleDetail: IModule) => { + if (!workspaceSlug || !projectId || !issueDetail) return; - modulesService - .addIssuesToModule(workspaceSlug as string, projectId as string, moduleDetail.id, { - issues: [issueDetail.id], - }) - .then((res) => { - mutate(ISSUE_DETAILS(issueId as string)); - }); - }; + modulesService + .addIssuesToModule(workspaceSlug as string, projectId as string, moduleDetail.id, { + issues: [issueDetail.id], + }) + .then((res) => { + mutate(ISSUE_DETAILS(issueId as string)); + }); + }, + [workspaceSlug, projectId, issueId, issueDetail] + ); const isNotAllowed = userAuth.isGuest || userAuth.isViewer; diff --git a/apps/app/components/issues/sub-issues-list-modal.tsx b/apps/app/components/issues/sub-issues-list-modal.tsx index f3ffe50ad8..897a9d0971 100644 --- a/apps/app/components/issues/sub-issues-list-modal.tsx +++ b/apps/app/components/issues/sub-issues-list-modal.tsx @@ -10,6 +10,8 @@ import { Combobox, Dialog, Transition } from "@headlessui/react"; import { RectangleStackIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline"; // services import issuesServices from "services/issues.service"; +// helpers +import { orderArrayBy } from "helpers/array.helper"; // types import { IIssue, IssueResponse } from "types"; // constants @@ -47,11 +49,24 @@ export const SubIssuesListModal: React.FC = ({ isOpen, handleClose, paren setQuery(""); }; - const addAsSubIssue = (issueId: string) => { + const addAsSubIssue = (issue: IIssue) => { if (!workspaceSlug || !projectId) return; + mutate( + SUB_ISSUES(parent?.id ?? ""), + (prevData) => { + let newSubIssues = [...(prevData as IIssue[])]; + newSubIssues.push(issue); + + newSubIssues = orderArrayBy(newSubIssues, "created_at", "descending"); + + return newSubIssues; + }, + false + ); + issuesServices - .patchIssue(workspaceSlug as string, projectId as string, issueId, { parent: parent?.id }) + .patchIssue(workspaceSlug as string, projectId as string, issue.id, { parent: parent?.id }) .then((res) => { mutate(SUB_ISSUES(parent?.id ?? "")); mutate( @@ -146,7 +161,7 @@ export const SubIssuesListModal: React.FC = ({ isOpen, handleClose, paren }` } onClick={() => { - addAsSubIssue(issue.id); + addAsSubIssue(issue); handleClose(); }} > diff --git a/apps/app/components/rich-text-editor/index.tsx b/apps/app/components/rich-text-editor/index.tsx index 2bddfe5f61..2a7f2f522e 100644 --- a/apps/app/components/rich-text-editor/index.tsx +++ b/apps/app/components/rich-text-editor/index.tsx @@ -36,6 +36,7 @@ import { Spinner } from "components/ui"; // components import { RichTextToolbar } from "./toolbar"; import { MentionAutoComplete } from "./mention-autocomplete"; +import { FloatingLinkToolbar } from "./toolbar/link"; export interface IRemirrorRichTextEditor { placeholder?: string; @@ -125,7 +126,7 @@ const RemirrorRichTextEditor: FC = (props) => { new CalloutExtension({ defaultType: "warn" }), new CodeBlockExtension(), new CodeExtension(), - new PlaceholderExtension({ placeholder: placeholder || `Enter text...` }), + new PlaceholderExtension({ placeholder: placeholder || "Enter text..." }), new HistoryExtension(), new LinkExtension({ autoLink: true }), new ImageExtension({ @@ -165,6 +166,7 @@ const RemirrorRichTextEditor: FC = (props) => { setJsonValue(json); onJSONChange(json); }; + const handleHTMLChange = (value: string) => { setHtmlValue(value); onHTMLChange(value); @@ -194,6 +196,7 @@ const RemirrorRichTextEditor: FC = (props) => { )} {/* */} + {} {} diff --git a/apps/app/components/rich-text-editor/toolbar/index.tsx b/apps/app/components/rich-text-editor/toolbar/index.tsx index f5ff2db705..1167c8d22c 100644 --- a/apps/app/components/rich-text-editor/toolbar/index.tsx +++ b/apps/app/components/rich-text-editor/toolbar/index.tsx @@ -6,7 +6,6 @@ import { BoldButton } from "./bold"; import { ItalicButton } from "./italic"; import { UnderlineButton } from "./underline"; import { StrikeButton } from "./strike"; -import { LinkButton } from "./link"; // headings import HeadingControls from "./heading-controls"; // list diff --git a/apps/app/components/rich-text-editor/toolbar/link.tsx b/apps/app/components/rich-text-editor/toolbar/link.tsx index 8c1d65fdb5..bef9315205 100644 --- a/apps/app/components/rich-text-editor/toolbar/link.tsx +++ b/apps/app/components/rich-text-editor/toolbar/link.tsx @@ -1,241 +1,196 @@ -import { useCommands, useActive } from "@remirror/react"; +import React, { + ChangeEvent, + HTMLProps, + KeyboardEvent, + useCallback, + useEffect, + useLayoutEffect, + useMemo, + useRef, + useState, +} from "react"; -export const LinkButton = () => { - const { focus } = useCommands(); +import { createMarkPositioner, LinkExtension, ShortcutHandlerProps } from "remirror/extensions"; +import { + CommandButton, + FloatingToolbar, + FloatingWrapper, + useActive, + useAttrs, + useChainedCommands, + useCurrentSelection, + useExtensionEvent, + useUpdateReason, +} from "@remirror/react"; - const active = useActive(); +const useLinkShortcut = () => { + const [linkShortcut, setLinkShortcut] = useState(); + const [isEditing, setIsEditing] = useState(false); - return ( - + useExtensionEvent( + LinkExtension, + "onShortcut", + useCallback( + (props) => { + if (!isEditing) { + setIsEditing(true); + } + + return setLinkShortcut(props); + }, + [isEditing] + ) + ); + + return { linkShortcut, isEditing, setIsEditing }; +}; + +const useFloatingLinkState = () => { + const chain = useChainedCommands(); + const { isEditing, linkShortcut, setIsEditing } = useLinkShortcut(); + const { to, empty } = useCurrentSelection(); + + const url = (useAttrs().link()?.href as string) ?? ""; + const [href, setHref] = useState(url); + + // A positioner which only shows for links. + const linkPositioner = useMemo(() => createMarkPositioner({ type: "link" }), []); + + const onRemove = useCallback(() => chain.removeLink().focus().run(), [chain]); + + const updateReason = useUpdateReason(); + + useLayoutEffect(() => { + if (!isEditing) { + return; + } + + if (updateReason.doc || updateReason.selection) { + setIsEditing(false); + } + }, [isEditing, setIsEditing, updateReason.doc, updateReason.selection]); + + useEffect(() => { + setHref(url); + }, [url]); + + const submitHref = useCallback(() => { + setIsEditing(false); + const range = linkShortcut ?? undefined; + + if (href === "") { + chain.removeLink(); + } else { + chain.updateLink({ href, auto: false }, range); + } + + chain.focus(range?.to ?? to).run(); + }, [setIsEditing, linkShortcut, chain, href, to]); + + const cancelHref = useCallback(() => { + setIsEditing(false); + }, [setIsEditing]); + + const clickEdit = useCallback(() => { + if (empty) { + chain.selectLink(); + } + + setIsEditing(true); + }, [chain, empty, setIsEditing]); + + return useMemo( + () => ({ + href, + setHref, + linkShortcut, + linkPositioner, + isEditing, + clickEdit, + onRemove, + submitHref, + cancelHref, + }), + [href, linkShortcut, linkPositioner, isEditing, clickEdit, onRemove, submitHref, cancelHref] ); }; -// import type { ChangeEvent, HTMLProps, KeyboardEvent } from 'react'; -// import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; -// import { createMarkPositioner, LinkExtension, ShortcutHandlerProps } from 'remirror/extensions'; -// import { -// CommandButton, -// EditorComponent, -// FloatingToolbar, -// FloatingWrapper, -// Remirror, -// ThemeProvider, -// useActive, -// useAttrs, -// useChainedCommands, -// useCurrentSelection, -// useExtensionEvent, -// useRemirror, -// useUpdateReason, -// } from '@remirror/react'; +const DelayAutoFocusInput = ({ autoFocus, ...rest }: HTMLProps) => { + const inputRef = useRef(null); -// function useLinkShortcut() { -// const [linkShortcut, setLinkShortcut] = useState(); -// const [isEditing, setIsEditing] = useState(false); + useEffect(() => { + if (!autoFocus) { + return; + } -// useExtensionEvent( -// LinkExtension, -// 'onShortcut', -// useCallback( -// (props) => { -// if (!isEditing) { -// setIsEditing(true); -// } + const frame = window.requestAnimationFrame(() => { + inputRef.current?.focus(); + }); -// return setLinkShortcut(props); -// }, -// [isEditing], -// ), -// ); + return () => { + window.cancelAnimationFrame(frame); + }; + }, [autoFocus]); -// return { linkShortcut, isEditing, setIsEditing }; -// } + return ; +}; -// function useFloatingLinkState() { -// const chain = useChainedCommands(); -// const { isEditing, linkShortcut, setIsEditing } = useLinkShortcut(); -// const { to, empty } = useCurrentSelection(); +export const FloatingLinkToolbar = () => { + const { isEditing, linkPositioner, clickEdit, onRemove, submitHref, href, setHref, cancelHref } = + useFloatingLinkState(); + const active = useActive(); + const activeLink = active.link(); + const { empty } = useCurrentSelection(); -// const url = (useAttrs().link()?.href as string) ?? ''; -// const [href, setHref] = useState(url); + const handleClickEdit = useCallback(() => { + clickEdit(); + }, [clickEdit]); -// // A positioner which only shows for links. -// const linkPositioner = useMemo(() => createMarkPositioner({ type: 'link' }), []); + const linkEditButtons = activeLink ? ( + <> + + + + ) : ( + + ); -// const onRemove = useCallback(() => { -// return chain.removeLink().focus().run(); -// }, [chain]); + return ( + <> + {!isEditing && {linkEditButtons}} + {!isEditing && empty && ( + {linkEditButtons} + )} -// const updateReason = useUpdateReason(); + + ) => setHref(e.target.value)} + value={href} + onKeyDown={(e: KeyboardEvent) => { + const { code } = e; -// useLayoutEffect(() => { -// if (!isEditing) { -// return; -// } + if (code === "Enter") { + submitHref(); + } -// if (updateReason.doc || updateReason.selection) { -// setIsEditing(false); -// } -// }, [isEditing, setIsEditing, updateReason.doc, updateReason.selection]); - -// useEffect(() => { -// setHref(url); -// }, [url]); - -// const submitHref = useCallback(() => { -// setIsEditing(false); -// const range = linkShortcut ?? undefined; - -// if (href === '') { -// chain.removeLink(); -// } else { -// chain.updateLink({ href, auto: false }, range); -// } - -// chain.focus(range?.to ?? to).run(); -// }, [setIsEditing, linkShortcut, chain, href, to]); - -// const cancelHref = useCallback(() => { -// setIsEditing(false); -// }, [setIsEditing]); - -// const clickEdit = useCallback(() => { -// if (empty) { -// chain.selectLink(); -// } - -// setIsEditing(true); -// }, [chain, empty, setIsEditing]); - -// return useMemo( -// () => ({ -// href, -// setHref, -// linkShortcut, -// linkPositioner, -// isEditing, -// clickEdit, -// onRemove, -// submitHref, -// cancelHref, -// }), -// [href, linkShortcut, linkPositioner, isEditing, clickEdit, onRemove, submitHref, cancelHref], -// ); -// } - -// const DelayAutoFocusInput = ({ autoFocus, ...rest }: HTMLProps) => { -// const inputRef = useRef(null); - -// useEffect(() => { -// if (!autoFocus) { -// return; -// } - -// const frame = window.requestAnimationFrame(() => { -// inputRef.current?.focus(); -// }); - -// return () => { -// window.cancelAnimationFrame(frame); -// }; -// }, [autoFocus]); - -// return ; -// }; - -// const FloatingLinkToolbar = () => { -// const { isEditing, linkPositioner, clickEdit, onRemove, submitHref, href, setHref, cancelHref } = -// useFloatingLinkState(); -// const active = useActive(); -// const activeLink = active.link(); -// const { empty } = useCurrentSelection(); - -// const handleClickEdit = useCallback(() => { -// clickEdit(); -// }, [clickEdit]); - -// const linkEditButtons = activeLink ? ( -// <> -// -// -// -// ) : ( -// -// ); - -// return ( -// <> -// {!isEditing && {linkEditButtons}} -// {!isEditing && empty && ( -// {linkEditButtons} -// )} - -// -// ) => setHref(event.target.value)} -// value={href} -// onKeyPress={(event: KeyboardEvent) => { -// const { code } = event; - -// if (code === 'Enter') { -// submitHref(); -// } - -// if (code === 'Escape') { -// cancelHref(); -// } -// }} -// /> -// -// -// ); -// }; - -// const EditDialog = (): JSX.Element => { -// const { manager, state } = useRemirror({ -// extensions: () => [new LinkExtension({ autoLink: true })], -// content: `Click this link to edit it`, -// stringHandler: 'html', -// }); - -// return ( -// -// -// -// -// -// -// ); -// }; - -// export default EditDialog; + if (code === "Escape") { + cancelHref(); + } + }} + /> + + + ); +}; diff --git a/apps/app/components/rich-text-editor/toolbar/undo.tsx b/apps/app/components/rich-text-editor/toolbar/undo.tsx index bed64fc9cc..6c35b96aed 100644 --- a/apps/app/components/rich-text-editor/toolbar/undo.tsx +++ b/apps/app/components/rich-text-editor/toolbar/undo.tsx @@ -4,6 +4,7 @@ import { useCommands } from "@remirror/react"; export const UndoButton = () => { const { undo } = useCommands(); + return (