feat: link option in remirror (#240)

* feat: link option in remirror

* fix: removed link import from remirror toolbar
This commit is contained in:
Aaryan Khandelwal 2023-02-07 11:20:41 +05:30 committed by GitHub
parent f308fe2ce1
commit 859fef24f4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 273 additions and 273 deletions

View File

@ -16,7 +16,7 @@ import issuesServices from "services/issues.service";
// ui // ui
import { Button } from "components/ui"; import { Button } from "components/ui";
// icons // icons
import { FolderIcon, MagnifyingGlassIcon, XMarkIcon } from "@heroicons/react/24/outline"; import { MagnifyingGlassIcon, XMarkIcon } from "@heroicons/react/24/outline";
import { BlockerIcon, LayerDiagonalIcon } from "components/icons"; import { BlockerIcon, LayerDiagonalIcon } from "components/icons";
// types // types
import { IIssue, UserAuth } from "types"; import { IIssue, UserAuth } from "types";

View File

@ -1,4 +1,4 @@
import React, { useState } from "react"; import React, { useCallback, useState } from "react";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
@ -113,7 +113,8 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
}); });
}; };
const handleCycleChange = (cycleDetail: ICycle) => { const handleCycleChange = useCallback(
(cycleDetail: ICycle) => {
if (!workspaceSlug || !projectId || !issueDetail) return; if (!workspaceSlug || !projectId || !issueDetail) return;
issuesServices issuesServices
@ -123,9 +124,12 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
.then((res) => { .then((res) => {
mutate(ISSUE_DETAILS(issueId as string)); mutate(ISSUE_DETAILS(issueId as string));
}); });
}; },
[workspaceSlug, projectId, issueId, issueDetail]
);
const handleModuleChange = (moduleDetail: IModule) => { const handleModuleChange = useCallback(
(moduleDetail: IModule) => {
if (!workspaceSlug || !projectId || !issueDetail) return; if (!workspaceSlug || !projectId || !issueDetail) return;
modulesService modulesService
@ -135,7 +139,9 @@ export const IssueDetailsSidebar: React.FC<Props> = ({
.then((res) => { .then((res) => {
mutate(ISSUE_DETAILS(issueId as string)); mutate(ISSUE_DETAILS(issueId as string));
}); });
}; },
[workspaceSlug, projectId, issueId, issueDetail]
);
const isNotAllowed = userAuth.isGuest || userAuth.isViewer; const isNotAllowed = userAuth.isGuest || userAuth.isViewer;

View File

@ -10,6 +10,8 @@ import { Combobox, Dialog, Transition } from "@headlessui/react";
import { RectangleStackIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline"; import { RectangleStackIcon, MagnifyingGlassIcon } from "@heroicons/react/24/outline";
// services // services
import issuesServices from "services/issues.service"; import issuesServices from "services/issues.service";
// helpers
import { orderArrayBy } from "helpers/array.helper";
// types // types
import { IIssue, IssueResponse } from "types"; import { IIssue, IssueResponse } from "types";
// constants // constants
@ -47,11 +49,24 @@ export const SubIssuesListModal: React.FC<Props> = ({ isOpen, handleClose, paren
setQuery(""); setQuery("");
}; };
const addAsSubIssue = (issueId: string) => { const addAsSubIssue = (issue: IIssue) => {
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
mutate<IIssue[]>(
SUB_ISSUES(parent?.id ?? ""),
(prevData) => {
let newSubIssues = [...(prevData as IIssue[])];
newSubIssues.push(issue);
newSubIssues = orderArrayBy(newSubIssues, "created_at", "descending");
return newSubIssues;
},
false
);
issuesServices 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) => { .then((res) => {
mutate(SUB_ISSUES(parent?.id ?? "")); mutate(SUB_ISSUES(parent?.id ?? ""));
mutate<IssueResponse>( mutate<IssueResponse>(
@ -146,7 +161,7 @@ export const SubIssuesListModal: React.FC<Props> = ({ isOpen, handleClose, paren
}` }`
} }
onClick={() => { onClick={() => {
addAsSubIssue(issue.id); addAsSubIssue(issue);
handleClose(); handleClose();
}} }}
> >

View File

@ -36,6 +36,7 @@ import { Spinner } from "components/ui";
// components // components
import { RichTextToolbar } from "./toolbar"; import { RichTextToolbar } from "./toolbar";
import { MentionAutoComplete } from "./mention-autocomplete"; import { MentionAutoComplete } from "./mention-autocomplete";
import { FloatingLinkToolbar } from "./toolbar/link";
export interface IRemirrorRichTextEditor { export interface IRemirrorRichTextEditor {
placeholder?: string; placeholder?: string;
@ -125,7 +126,7 @@ const RemirrorRichTextEditor: FC<IRemirrorRichTextEditor> = (props) => {
new CalloutExtension({ defaultType: "warn" }), new CalloutExtension({ defaultType: "warn" }),
new CodeBlockExtension(), new CodeBlockExtension(),
new CodeExtension(), new CodeExtension(),
new PlaceholderExtension({ placeholder: placeholder || `Enter text...` }), new PlaceholderExtension({ placeholder: placeholder || "Enter text..." }),
new HistoryExtension(), new HistoryExtension(),
new LinkExtension({ autoLink: true }), new LinkExtension({ autoLink: true }),
new ImageExtension({ new ImageExtension({
@ -165,6 +166,7 @@ const RemirrorRichTextEditor: FC<IRemirrorRichTextEditor> = (props) => {
setJsonValue(json); setJsonValue(json);
onJSONChange(json); onJSONChange(json);
}; };
const handleHTMLChange = (value: string) => { const handleHTMLChange = (value: string) => {
setHtmlValue(value); setHtmlValue(value);
onHTMLChange(value); onHTMLChange(value);
@ -194,6 +196,7 @@ const RemirrorRichTextEditor: FC<IRemirrorRichTextEditor> = (props) => {
</div> </div>
)} )}
{/* <TableComponents /> */} {/* <TableComponents /> */}
<FloatingLinkToolbar />
<MentionAutoComplete mentions={mentions} tags={tags} /> <MentionAutoComplete mentions={mentions} tags={tags} />
{<OnChangeJSON onChange={handleJSONChange} />} {<OnChangeJSON onChange={handleJSONChange} />}
{<OnChangeHTML onChange={handleHTMLChange} />} {<OnChangeHTML onChange={handleHTMLChange} />}

View File

@ -6,7 +6,6 @@ import { BoldButton } from "./bold";
import { ItalicButton } from "./italic"; import { ItalicButton } from "./italic";
import { UnderlineButton } from "./underline"; import { UnderlineButton } from "./underline";
import { StrikeButton } from "./strike"; import { StrikeButton } from "./strike";
import { LinkButton } from "./link";
// headings // headings
import HeadingControls from "./heading-controls"; import HeadingControls from "./heading-controls";
// list // list

View File

@ -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 = () => { import { createMarkPositioner, LinkExtension, ShortcutHandlerProps } from "remirror/extensions";
const { focus } = useCommands(); import {
CommandButton,
FloatingToolbar,
FloatingWrapper,
useActive,
useAttrs,
useChainedCommands,
useCurrentSelection,
useExtensionEvent,
useUpdateReason,
} from "@remirror/react";
const active = useActive(); const useLinkShortcut = () => {
const [linkShortcut, setLinkShortcut] = useState<ShortcutHandlerProps | undefined>();
const [isEditing, setIsEditing] = useState(false);
return ( useExtensionEvent(
<button LinkExtension,
type="button" "onShortcut",
onClick={() => { useCallback(
// toggleLink(); (props) => {
focus(); if (!isEditing) {
}} setIsEditing(true);
className={`${active.link() ? "bg-gray-200" : "hover:bg-gray-100"} rounded p-1`} }
>
<svg return setLinkShortcut(props);
xmlns="http://www.w3.org/2000/svg" },
height="18" [isEditing]
width="18" )
fill="black" );
viewBox="0 0 48 48"
> return { linkShortcut, isEditing, setIsEditing };
<path d="M22.5 34H14q-4.15 0-7.075-2.925T4 24q0-4.15 2.925-7.075T14 14h8.5v3H14q-2.9 0-4.95 2.05Q7 21.1 7 24q0 2.9 2.05 4.95Q11.1 31 14 31h8.5Zm-6.25-8.5v-3h15.5v3ZM25.5 34v-3H34q2.9 0 4.95-2.05Q41 26.9 41 24q0-2.9-2.05-4.95Q36.9 17 34 17h-8.5v-3H34q4.15 0 7.075 2.925T44 24q0 4.15-2.925 7.075T34 34Z" /> };
</svg>
</button> 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<string>(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'; const DelayAutoFocusInput = ({ autoFocus, ...rest }: HTMLProps<HTMLInputElement>) => {
// import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; const inputRef = useRef<HTMLInputElement>(null);
// import { createMarkPositioner, LinkExtension, ShortcutHandlerProps } from 'remirror/extensions';
// import {
// CommandButton,
// EditorComponent,
// FloatingToolbar,
// FloatingWrapper,
// Remirror,
// ThemeProvider,
// useActive,
// useAttrs,
// useChainedCommands,
// useCurrentSelection,
// useExtensionEvent,
// useRemirror,
// useUpdateReason,
// } from '@remirror/react';
// function useLinkShortcut() { useEffect(() => {
// const [linkShortcut, setLinkShortcut] = useState<ShortcutHandlerProps | undefined>(); if (!autoFocus) {
// const [isEditing, setIsEditing] = useState(false); return;
}
// useExtensionEvent( const frame = window.requestAnimationFrame(() => {
// LinkExtension, inputRef.current?.focus();
// 'onShortcut', });
// useCallback(
// (props) => {
// if (!isEditing) {
// setIsEditing(true);
// }
// return setLinkShortcut(props); return () => {
// }, window.cancelAnimationFrame(frame);
// [isEditing], };
// ), }, [autoFocus]);
// );
// return { linkShortcut, isEditing, setIsEditing }; return <input ref={inputRef} {...rest} />;
// } };
// function useFloatingLinkState() { export const FloatingLinkToolbar = () => {
// const chain = useChainedCommands(); const { isEditing, linkPositioner, clickEdit, onRemove, submitHref, href, setHref, cancelHref } =
// const { isEditing, linkShortcut, setIsEditing } = useLinkShortcut(); useFloatingLinkState();
// const { to, empty } = useCurrentSelection(); const active = useActive();
const activeLink = active.link();
const { empty } = useCurrentSelection();
// const url = (useAttrs().link()?.href as string) ?? ''; const handleClickEdit = useCallback(() => {
// const [href, setHref] = useState<string>(url); clickEdit();
}, [clickEdit]);
// // A positioner which only shows for links. const linkEditButtons = activeLink ? (
// const linkPositioner = useMemo(() => createMarkPositioner({ type: 'link' }), []); <>
<CommandButton
commandName="updateLink"
onSelect={handleClickEdit}
icon="pencilLine"
enabled
/>
<CommandButton commandName="removeLink" onSelect={onRemove} icon="linkUnlink" enabled />
</>
) : (
<CommandButton commandName="updateLink" onSelect={handleClickEdit} icon="link" enabled />
);
// const onRemove = useCallback(() => { return (
// return chain.removeLink().focus().run(); <>
// }, [chain]); {!isEditing && <FloatingToolbar>{linkEditButtons}</FloatingToolbar>}
{!isEditing && empty && (
<FloatingToolbar positioner={linkPositioner}>{linkEditButtons}</FloatingToolbar>
)}
// const updateReason = useUpdateReason(); <FloatingWrapper
positioner="always"
placement="bottom"
enabled={isEditing}
renderOutsideEditor
>
<DelayAutoFocusInput
autoFocus
placeholder="Enter link..."
onChange={(e: ChangeEvent<HTMLInputElement>) => setHref(e.target.value)}
value={href}
onKeyDown={(e: KeyboardEvent<HTMLInputElement>) => {
const { code } = e;
// useLayoutEffect(() => { if (code === "Enter") {
// if (!isEditing) { submitHref();
// return; }
// }
// if (updateReason.doc || updateReason.selection) { if (code === "Escape") {
// setIsEditing(false); cancelHref();
// } }
// }, [isEditing, setIsEditing, updateReason.doc, updateReason.selection]); }}
/>
// useEffect(() => { </FloatingWrapper>
// 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<HTMLInputElement>) => {
// const inputRef = useRef<HTMLInputElement>(null);
// useEffect(() => {
// if (!autoFocus) {
// return;
// }
// const frame = window.requestAnimationFrame(() => {
// inputRef.current?.focus();
// });
// return () => {
// window.cancelAnimationFrame(frame);
// };
// }, [autoFocus]);
// return <input ref={inputRef} {...rest} />;
// };
// 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 ? (
// <>
// <CommandButton
// commandName='updateLink'
// onSelect={handleClickEdit}
// icon='pencilLine'
// enabled
// />
// <CommandButton commandName='removeLink' onSelect={onRemove} icon='linkUnlink' enabled />
// </>
// ) : (
// <CommandButton commandName='updateLink' onSelect={handleClickEdit} icon='link' enabled />
// );
// return (
// <>
// {!isEditing && <FloatingToolbar>{linkEditButtons}</FloatingToolbar>}
// {!isEditing && empty && (
// <FloatingToolbar positioner={linkPositioner}>{linkEditButtons}</FloatingToolbar>
// )}
// <FloatingWrapper
// positioner='always'
// placement='bottom'
// enabled={isEditing}
// renderOutsideEditor
// >
// <DelayAutoFocusInput
// style={{ zIndex: 20 }}
// autoFocus
// placeholder='Enter link...'
// onChange={(event: ChangeEvent<HTMLInputElement>) => setHref(event.target.value)}
// value={href}
// onKeyPress={(event: KeyboardEvent<HTMLInputElement>) => {
// const { code } = event;
// if (code === 'Enter') {
// submitHref();
// }
// if (code === 'Escape') {
// cancelHref();
// }
// }}
// />
// </FloatingWrapper>
// </>
// );
// };
// const EditDialog = (): JSX.Element => {
// const { manager, state } = useRemirror({
// extensions: () => [new LinkExtension({ autoLink: true })],
// content: `Click this <a href="https://remirror.io" target="_blank">link</a> to edit it`,
// stringHandler: 'html',
// });
// return (
// <ThemeProvider>
// <Remirror manager={manager} initialContent={state}>
// <EditorComponent />
// <FloatingLinkToolbar />
// </Remirror>
// </ThemeProvider>
// );
// };
// export default EditDialog;

View File

@ -4,6 +4,7 @@ import { useCommands } from "@remirror/react";
export const UndoButton = () => { export const UndoButton = () => {
const { undo } = useCommands(); const { undo } = useCommands();
return ( return (
<button <button
type="button" type="button"

View File

@ -11,7 +11,11 @@ const nextConfig = {
reactStrictMode: false, reactStrictMode: false,
swcMinify: true, swcMinify: true,
images: { images: {
domains: ["vinci-web.s3.amazonaws.com", "planefs-staging.s3.ap-south-1.amazonaws.com"], domains: [
"vinci-web.s3.amazonaws.com",
"planefs-staging.s3.ap-south-1.amazonaws.com",
"planefs.s3.amazonaws.com",
],
}, },
output: "standalone", output: "standalone",
experimental: { experimental: {

View File

@ -73,7 +73,7 @@ const IssueDetailsPage: NextPage<UserAuth> = (props) => {
: null : null
); );
const { data: subIssues } = useSWR( const { data: subIssues } = useSWR<IIssue[] | undefined>(
issueId && workspaceSlug && projectId ? SUB_ISSUES(issueId as string) : null, issueId && workspaceSlug && projectId ? SUB_ISSUES(issueId as string) : null,
issueId && workspaceSlug && projectId issueId && workspaceSlug && projectId
? () => ? () =>
@ -126,7 +126,6 @@ const IssueDetailsPage: NextPage<UserAuth> = (props) => {
issuesService issuesService
.patchIssue(workspaceSlug as string, projectId as string, issueId as string, payload) .patchIssue(workspaceSlug as string, projectId as string, issueId as string, payload)
.then((res) => { .then((res) => {
console.log(res);
mutateIssueDetails(); mutateIssueDetails();
mutateIssueActivities(); mutateIssueActivities();
}) })
@ -140,11 +139,16 @@ const IssueDetailsPage: NextPage<UserAuth> = (props) => {
const handleSubIssueRemove = (issueId: string) => { const handleSubIssueRemove = (issueId: string) => {
if (!workspaceSlug || !projectId) return; if (!workspaceSlug || !projectId) return;
mutate<IIssue[]>(
SUB_ISSUES(issueDetails?.id ?? ""),
(prevData) => prevData?.filter((i) => i.id !== issueId),
false
);
issuesService issuesService
.patchIssue(workspaceSlug as string, projectId as string, issueId, { parent: null }) .patchIssue(workspaceSlug as string, projectId as string, issueId, { parent: null })
.then((res) => { .then((res) => {
mutate(SUB_ISSUES(issueDetails?.id ?? "")); mutate(SUB_ISSUES(issueDetails?.id ?? ""));
mutateIssueActivities();
mutate<IssueResponse>( mutate<IssueResponse>(
PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string), PROJECT_ISSUES_LIST(workspaceSlug as string, projectId as string),
@ -169,7 +173,8 @@ const IssueDetailsPage: NextPage<UserAuth> = (props) => {
}; };
useEffect(() => { useEffect(() => {
if (issueDetails) { if (!issueDetails) return;
mutateIssueActivities(); mutateIssueActivities();
reset({ reset({
...issueDetails, ...issueDetails,
@ -177,14 +182,13 @@ const IssueDetailsPage: NextPage<UserAuth> = (props) => {
issueDetails.blockers_list ?? issueDetails.blockers_list ??
issueDetails.blocker_issues?.map((issue) => issue.blocker_issue_detail?.id), issueDetails.blocker_issues?.map((issue) => issue.blocker_issue_detail?.id),
blocked_list: blocked_list:
issueDetails.blocked_list ?? issueDetails.blocks_list ??
issueDetails.blocked_issues?.map((issue) => issue.blocked_issue_detail?.id), issueDetails.blocked_issues?.map((issue) => issue.blocked_issue_detail?.id),
assignees_list: assignees_list:
issueDetails.assignees_list ?? issueDetails.assignee_details?.map((user) => user.id), issueDetails.assignees_list ?? issueDetails.assignee_details?.map((user) => user.id),
labels_list: issueDetails.labels_list ?? issueDetails.labels, labels_list: issueDetails.labels_list ?? issueDetails.labels,
labels: issueDetails.labels_list ?? issueDetails.labels, labels: issueDetails.labels_list ?? issueDetails.labels,
}); });
}
}, [issueDetails, reset, mutateIssueActivities]); }, [issueDetails, reset, mutateIssueActivities]);
const isNotAllowed = props.isGuest || props.isViewer; const isNotAllowed = props.isGuest || props.isViewer;
@ -280,9 +284,9 @@ const IssueDetailsPage: NextPage<UserAuth> = (props) => {
userAuth={props} userAuth={props}
/> />
<div className="mt-2"> <div className="mt-2">
{issueId && workspaceSlug && projectId && subIssues?.length > 0 ? ( {issueId && workspaceSlug && projectId && subIssues && subIssues.length > 0 ? (
<SubIssuesList <SubIssuesList
issues={subIssues} issues={subIssues ?? []}
parentIssue={issueDetails} parentIssue={issueDetails}
projectId={projectId?.toString()} projectId={projectId?.toString()}
workspaceSlug={workspaceSlug?.toString()} workspaceSlug={workspaceSlug?.toString()}

View File

@ -401,6 +401,19 @@ img.ProseMirror-separator {
/* end table styling */ /* end table styling */
/* link styling */ /* link styling */
.remirror-floating-popover {
z-index: 20 !important;
}
.remirror-floating-popover input {
font-size: 0.75rem;
border-radius: 5px;
padding: 5px;
border: 1px solid #a8a6a6;
box-shadow: 1px 1px 5px #c0bebe;
outline: none;
}
.remirror-editor-wrapper a { .remirror-editor-wrapper a {
color: blue; color: blue;
text-decoration: underline; text-decoration: underline;