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

View File

@ -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<Props> = ({
});
};
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;

View File

@ -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<Props> = ({ isOpen, handleClose, paren
setQuery("");
};
const addAsSubIssue = (issueId: string) => {
const addAsSubIssue = (issue: IIssue) => {
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
.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<IssueResponse>(
@ -146,7 +161,7 @@ export const SubIssuesListModal: React.FC<Props> = ({ isOpen, handleClose, paren
}`
}
onClick={() => {
addAsSubIssue(issue.id);
addAsSubIssue(issue);
handleClose();
}}
>

View File

@ -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<IRemirrorRichTextEditor> = (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<IRemirrorRichTextEditor> = (props) => {
setJsonValue(json);
onJSONChange(json);
};
const handleHTMLChange = (value: string) => {
setHtmlValue(value);
onHTMLChange(value);
@ -194,6 +196,7 @@ const RemirrorRichTextEditor: FC<IRemirrorRichTextEditor> = (props) => {
</div>
)}
{/* <TableComponents /> */}
<FloatingLinkToolbar />
<MentionAutoComplete mentions={mentions} tags={tags} />
{<OnChangeJSON onChange={handleJSONChange} />}
{<OnChangeHTML onChange={handleHTMLChange} />}

View File

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

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 = () => {
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<ShortcutHandlerProps | undefined>();
const [isEditing, setIsEditing] = useState(false);
return (
<button
type="button"
onClick={() => {
// toggleLink();
focus();
}}
className={`${active.link() ? "bg-gray-200" : "hover:bg-gray-100"} rounded p-1`}
>
<svg
xmlns="http://www.w3.org/2000/svg"
height="18"
width="18"
fill="black"
viewBox="0 0 48 48"
>
<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>
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<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';
// 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<HTMLInputElement>) => {
const inputRef = useRef<HTMLInputElement>(null);
// function useLinkShortcut() {
// const [linkShortcut, setLinkShortcut] = useState<ShortcutHandlerProps | undefined>();
// 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 <input ref={inputRef} {...rest} />;
};
// 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<string>(url);
const handleClickEdit = useCallback(() => {
clickEdit();
}, [clickEdit]);
// // A positioner which only shows for links.
// const linkPositioner = useMemo(() => createMarkPositioner({ type: 'link' }), []);
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 />
);
// const onRemove = useCallback(() => {
// return chain.removeLink().focus().run();
// }, [chain]);
return (
<>
{!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 (!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<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;
if (code === "Escape") {
cancelHref();
}
}}
/>
</FloatingWrapper>
</>
);
};

View File

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

View File

@ -11,7 +11,11 @@ const nextConfig = {
reactStrictMode: false,
swcMinify: true,
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",
experimental: {

View File

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

View File

@ -401,6 +401,19 @@ img.ProseMirror-separator {
/* end table 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 {
color: blue;
text-decoration: underline;