Merge pull request #3056 from makeplane/promote/develop-preview

Promote: Develop to Preview
This commit is contained in:
sriram veeraghanta 2023-12-10 15:51:41 +05:30 committed by GitHub
commit 0f28008fa5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
723 changed files with 3889 additions and 4795 deletions

View File

@ -1,8 +1,5 @@
# Helm Chart # Helm Chart
Click on the below link to access the helm chart instructions. Click on the below link to access the helm chart instructions.
[![Artifact Hub](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/makeplane)](https://artifacthub.io/packages/search?repo=makeplane) [![Artifact Hub](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/makeplane)](https://artifacthub.io/packages/search?repo=makeplane)

View File

@ -7,12 +7,14 @@ We will cover two main options for setting up your self-hosted environment: usin
Let's get started! Let's get started!
## Setting up Docker Environment ## Setting up Docker Environment
<details> <details>
<summary>Option 1 - Using Cloud Server</summary> <summary>Option 1 - Using Cloud Server</summary>
<p>Best way to start is to create EC2 maching on AWS. It must of minimum t3.medium/t3a/medium</p> <p>Best way to start is to create EC2 maching on AWS. It must of minimum t3.medium/t3a/medium</p>
<p>Run the below command to install docker engine.</p> <p>Run the below command to install docker engine.</p>
```curl -fsSL https://get.docker.com -o install-docker.sh``` `curl -fsSL https://get.docker.com -o install-docker.sh`
</details> </details>
--- ---
@ -20,21 +22,24 @@ Let's get started!
<details> <details>
<summary>Option 2 - Using Desktop</summary> <summary>Option 2 - Using Desktop</summary>
#### For Mac #### For Mac
<ol> <ol>
<li> Download Docker Desktop for Mac from the <a href="https://hub.docker.com/editions/community/docker-ce-desktop-mac/" target="_blank">Docker Hub</a>. </li> <li> Download Docker Desktop for Mac from the <a href="https://hub.docker.com/editions/community/docker-ce-desktop-mac/" target="_blank">Docker Hub</a>. </li>
<li> Double-click the downloaded `.dmg` file and drag the Docker app icon to the Applications folder. </li> <li> Double-click the downloaded `.dmg` file and drag the Docker app icon to the Applications folder. </li>
<li>Open Docker Desktop from the Applications folder. You might be asked to provide your system password to install additional software.</li> <li>Open Docker Desktop from the Applications folder. You might be asked to provide your system password to install additional software.</li>
</ol> </ol>
#### For Windows: #### For Windows:
<ol> <ol>
<li>Download Docker Desktop for Windows from the <a href="https://hub.docker.com/editions/community/docker-ce-desktop-windows/" target="_blank">Docker Hub</a>.</li> <li>Download Docker Desktop for Windows from the <a href="https://hub.docker.com/editions/community/docker-ce-desktop-windows/" target="_blank">Docker Hub</a>.</li>
<li>Run the installer and follow the instructions. You might be asked to enable Hyper-V and "Containers" Windows features.</li> <li>Run the installer and follow the instructions. You might be asked to enable Hyper-V and "Containers" Windows features.</li>
<li>Open Docker Desktop. You might be asked to log out and log back in, or restart your machine, for changes to take effect.</li> <li>Open Docker Desktop. You might be asked to log out and log back in, or restart your machine, for changes to take effect.</li>
</ol> </ol>
After installation, you can verify the installation by opening a terminal (Command Prompt on Windows, Terminal app on Mac) and running the command `docker --version`. This should display the installed version of Docker. After installation, you can verify the installation by opening a terminal (Command Prompt on Windows, Terminal app on Mac) and running the command `docker --version`. This should display the installed version of Docker.
</details> </details>
--- ---
@ -44,6 +49,7 @@ Let's get started!
Installing plane is a very easy and minimal step process. Installing plane is a very easy and minimal step process.
### Prerequisite ### Prerequisite
- Docker installed and running - Docker installed and running
- OS with bash scripting enabled (Ubuntu, Linux AMI, macos). Windows systems need to have [gitbash](https://git-scm.com/download/win) - OS with bash scripting enabled (Ubuntu, Linux AMI, macos). Windows systems need to have [gitbash](https://git-scm.com/download/win)
- User context used must have access to docker services. In most cases, use sudo su to switch as root user - User context used must have access to docker services. In most cases, use sudo su to switch as root user
@ -103,6 +109,7 @@ Action [2]: 1
For the 1st time setup, type "1" as action input. For the 1st time setup, type "1" as action input.
This will create a create a folder `plane-app` or `plane-app-preview` (in case of preview deployment) and will download 2 files inside that This will create a create a folder `plane-app` or `plane-app-preview` (in case of preview deployment) and will download 2 files inside that
- `docker-compose.yaml` - `docker-compose.yaml`
- `.env` - `.env`
@ -113,13 +120,13 @@ Again the `options [1-6]` will be popped up and this time hit `6` to exit.
### Continue with setup - Environment Settings ### Continue with setup - Environment Settings
Before proceeding, we suggest used to review `.env` file and set the values. Before proceeding, we suggest used to review `.env` file and set the values.
Below are the most import keys you must refer to. *<span style="color: #fcba03">You can use any text editor to edit this file</span>*. Below are the most import keys you must refer to. _<span style="color: #fcba03">You can use any text editor to edit this file</span>_.
> `NGINX_PORT` - This is default set to `80`. Make sure the port you choose to use is not preoccupied. (e.g `NGINX_PORT=8080`) > `NGINX_PORT` - This is default set to `80`. Make sure the port you choose to use is not preoccupied. (e.g `NGINX_PORT=8080`)
> `WEB_URL` - This is default set to `http://localhost`. Change this to the FQDN you plan to use along with NGINX_PORT (eg. `https://plane.example.com:8080` or `http://[IP-ADDRESS]:8080`) > `WEB_URL` - This is default set to `http://localhost`. Change this to the FQDN you plan to use along with NGINX_PORT (eg. `https://plane.example.com:8080` or `http://[IP-ADDRESS]:8080`)
> `CORS_ALLOWED_ORIGINS` - This is default set to `http://localhost`. Change this to the FQDN you plan to use along with NGINX_PORT (eg. `https://plane.example.com:8080` or `http://[IP-ADDRESS]:8080`) > `CORS_ALLOWED_ORIGINS` - This is default set to `http://localhost`. Change this to the FQDN you plan to use along with NGINX_PORT (eg. `https://plane.example.com:8080` or `http://[IP-ADDRESS]:8080`)
There are many other settings you can play with, but we suggest you configure `EMAIL SETTINGS` as it will enable you to invite your teammates onto the platform. There are many other settings you can play with, but we suggest you configure `EMAIL SETTINGS` as it will enable you to invite your teammates onto the platform.
@ -150,7 +157,7 @@ Be patient as it might take sometime based on download speed and system configur
This is the confirmation that all images were downloaded and the services are up & running. This is the confirmation that all images were downloaded and the services are up & running.
You have successfully self hosted `Plane` instance. Access the application by going to IP or domain you have configured it (e.g `https://plane.example.com:8080` or `http://[IP-ADDRESS]:8080`) You have successfully self hosted `Plane` instance. Access the application by going to IP or domain you have configured it (e.g `https://plane.example.com:8080` or `http://[IP-ADDRESS]:8080`)
--- ---
@ -232,7 +239,6 @@ Once done, choose `6` to exit from prompt.
Once done with making changes in `.env` file, jump on to `Start Server` Once done with making changes in `.env` file, jump on to `Start Server`
## Upgrading from v0.13.2 to v0.14.x ## Upgrading from v0.13.2 to v0.14.x
This is one time activity for users who are upgrading from v0.13.2 to v0.14.0 This is one time activity for users who are upgrading from v0.13.2 to v0.14.0
@ -289,9 +295,9 @@ For every command you must see 2 records something like shown in above example o
To move forward, you would need PREFIX of old setup and new setup. As per above example, `v0132` is the prefix of v0.13.2 and `plane-app` is the prefix of v0.14.0 setup To move forward, you would need PREFIX of old setup and new setup. As per above example, `v0132` is the prefix of v0.13.2 and `plane-app` is the prefix of v0.14.0 setup
**Back to original terminal window**, *Provide the Source Volume Prefix* and hit ENTER. **Back to original terminal window**, _Provide the Source Volume Prefix_ and hit ENTER.
Now you will be prompted to *Provide Destination Volume Prefix*. Provide the value and hit ENTER Now you will be prompted to _Provide Destination Volume Prefix_. Provide the value and hit ENTER
``` ```
Provide the Source Volume Prefix : v0132 Provide the Source Volume Prefix : v0132
@ -305,5 +311,3 @@ In case the suffixes are wrong or the mentioned volumes are not found, you will
In case of successful migration, it will be a silent exit without error. In case of successful migration, it will be a silent exit without error.
Now its time to restart v0.14.0 setup. Now its time to restart v0.14.0 setup.

View File

@ -27,7 +27,7 @@
"prettier": "latest", "prettier": "latest",
"prettier-plugin-tailwindcss": "^0.5.4", "prettier-plugin-tailwindcss": "^0.5.4",
"tailwindcss": "^3.3.3", "tailwindcss": "^3.3.3",
"turbo": "^1.10.16" "turbo": "^1.11.1"
}, },
"resolutions": { "resolutions": {
"@types/react": "18.2.42" "@types/react": "18.2.42"

View File

@ -0,0 +1,4 @@
module.exports = {
root: true,
extends: ["custom"],
};

View File

@ -0,0 +1,6 @@
.next
.vercel
.tubro
out/
dis/
build/

View File

@ -0,0 +1,5 @@
{
"printWidth": 120,
"tabWidth": 2,
"trailingComma": "es5"
}

View File

@ -4,35 +4,17 @@ import { startImageUpload } from "../ui/plugins/upload-image";
import { findTableAncestor } from "./utils"; import { findTableAncestor } from "./utils";
export const toggleHeadingOne = (editor: Editor, range?: Range) => { export const toggleHeadingOne = (editor: Editor, range?: Range) => {
if (range) if (range) editor.chain().focus().deleteRange(range).setNode("heading", { level: 1 }).run();
editor
.chain()
.focus()
.deleteRange(range)
.setNode("heading", { level: 1 })
.run();
else editor.chain().focus().toggleHeading({ level: 1 }).run(); else editor.chain().focus().toggleHeading({ level: 1 }).run();
}; };
export const toggleHeadingTwo = (editor: Editor, range?: Range) => { export const toggleHeadingTwo = (editor: Editor, range?: Range) => {
if (range) if (range) editor.chain().focus().deleteRange(range).setNode("heading", { level: 2 }).run();
editor
.chain()
.focus()
.deleteRange(range)
.setNode("heading", { level: 2 })
.run();
else editor.chain().focus().toggleHeading({ level: 2 }).run(); else editor.chain().focus().toggleHeading({ level: 2 }).run();
}; };
export const toggleHeadingThree = (editor: Editor, range?: Range) => { export const toggleHeadingThree = (editor: Editor, range?: Range) => {
if (range) if (range) editor.chain().focus().deleteRange(range).setNode("heading", { level: 3 }).run();
editor
.chain()
.focus()
.deleteRange(range)
.setNode("heading", { level: 3 })
.run();
else editor.chain().focus().toggleHeading({ level: 3 }).run(); else editor.chain().focus().toggleHeading({ level: 3 }).run();
}; };
@ -57,8 +39,7 @@ export const toggleCodeBlock = (editor: Editor, range?: Range) => {
}; };
export const toggleOrderedList = (editor: Editor, range?: Range) => { export const toggleOrderedList = (editor: Editor, range?: Range) => {
if (range) if (range) editor.chain().focus().deleteRange(range).toggleOrderedList().run();
editor.chain().focus().deleteRange(range).toggleOrderedList().run();
else editor.chain().focus().toggleOrderedList().run(); else editor.chain().focus().toggleOrderedList().run();
}; };
@ -78,21 +59,8 @@ export const toggleStrike = (editor: Editor, range?: Range) => {
}; };
export const toggleBlockquote = (editor: Editor, range?: Range) => { export const toggleBlockquote = (editor: Editor, range?: Range) => {
if (range) if (range) editor.chain().focus().deleteRange(range).toggleNode("paragraph", "paragraph").toggleBlockquote().run();
editor else editor.chain().focus().toggleNode("paragraph", "paragraph").toggleBlockquote().run();
.chain()
.focus()
.deleteRange(range)
.toggleNode("paragraph", "paragraph")
.toggleBlockquote()
.run();
else
editor
.chain()
.focus()
.toggleNode("paragraph", "paragraph")
.toggleBlockquote()
.run();
}; };
export const insertTableCommand = (editor: Editor, range?: Range) => { export const insertTableCommand = (editor: Editor, range?: Range) => {
@ -105,19 +73,8 @@ export const insertTableCommand = (editor: Editor, range?: Range) => {
} }
} }
} }
if (range) if (range) editor.chain().focus().deleteRange(range).insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run();
editor else editor.chain().focus().insertTable({ rows: 3, cols: 3, withHeaderRow: true }).run();
.chain()
.focus()
.deleteRange(range)
.insertTable({ rows: 3, cols: 3, withHeaderRow: true })
.run();
else
editor
.chain()
.focus()
.insertTable({ rows: 3, cols: 3, withHeaderRow: true })
.run();
}; };
export const unsetLinkEditor = (editor: Editor) => { export const unsetLinkEditor = (editor: Editor) => {
@ -131,10 +88,8 @@ export const setLinkEditor = (editor: Editor, url: string) => {
export const insertImageCommand = ( export const insertImageCommand = (
editor: Editor, editor: Editor,
uploadFile: UploadImage, uploadFile: UploadImage,
setIsSubmitting?: ( setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void,
isSubmitting: "submitting" | "submitted" | "saved", range?: Range
) => void,
range?: Range,
) => { ) => {
if (range) editor.chain().focus().deleteRange(range).run(); if (range) editor.chain().focus().deleteRange(range).run();
const input = document.createElement("input"); const input = document.createElement("input");

View File

@ -6,25 +6,19 @@ interface EditorClassNames {
customClassName?: string; customClassName?: string;
} }
export const getEditorClassNames = ({ export const getEditorClassNames = ({ noBorder, borderOnFocus, customClassName }: EditorClassNames) =>
noBorder,
borderOnFocus,
customClassName,
}: EditorClassNames) =>
cn( cn(
"relative w-full max-w-full sm:rounded-lg mt-2 p-3 relative focus:outline-none rounded-md", "relative w-full max-w-full sm:rounded-lg mt-2 p-3 relative focus:outline-none rounded-md",
noBorder ? "" : "border border-custom-border-200", noBorder ? "" : "border border-custom-border-200",
borderOnFocus ? "focus:border border-custom-border-300" : "focus:border-0", borderOnFocus ? "focus:border border-custom-border-300" : "focus:border-0",
customClassName, customClassName
); );
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)); return twMerge(clsx(inputs));
} }
export const findTableAncestor = ( export const findTableAncestor = (node: Node | null): HTMLTableElement | null => {
node: Node | null,
): HTMLTableElement | null => {
while (node !== null && node.nodeName !== "TABLE") { while (node !== null && node.nodeName !== "TABLE") {
node = node.parentNode; node = node.parentNode;
} }

View File

@ -7,11 +7,7 @@ interface EditorContainerProps {
children: ReactNode; children: ReactNode;
} }
export const EditorContainer = ({ export const EditorContainer = ({ editor, editorClassNames, children }: EditorContainerProps) => (
editor,
editorClassNames,
children,
}: EditorContainerProps) => (
<div <div
id="editor-container" id="editor-container"
onClick={() => { onClick={() => {

View File

@ -8,16 +8,10 @@ interface EditorContentProps {
children?: ReactNode; children?: ReactNode;
} }
export const EditorContentWrapper = ({ export const EditorContentWrapper = ({ editor, editorContentCustomClassNames = "", children }: EditorContentProps) => (
editor,
editorContentCustomClassNames = "",
children,
}: EditorContentProps) => (
<div className={`contentEditor ${editorContentCustomClassNames}`}> <div className={`contentEditor ${editorContentCustomClassNames}`}>
<EditorContent editor={editor} /> <EditorContent editor={editor} />
{editor?.isActive("image") && editor?.isEditable && ( {editor?.isActive("image") && editor?.isEditable && <ImageResizer editor={editor} />}
<ImageResizer editor={editor} />
)}
{children} {children}
</div> </div>
); );

View File

@ -2,10 +2,7 @@ import { getNodeType } from "@tiptap/core";
import { NodeType } from "@tiptap/pm/model"; import { NodeType } from "@tiptap/pm/model";
import { EditorState } from "@tiptap/pm/state"; import { EditorState } from "@tiptap/pm/state";
export const findListItemPos = ( export const findListItemPos = (typeOrName: string | NodeType, state: EditorState) => {
typeOrName: string | NodeType,
state: EditorState,
) => {
const { $from } = state.selection; const { $from } = state.selection;
const nodeType = getNodeType(typeOrName, state.schema); const nodeType = getNodeType(typeOrName, state.schema);

View File

@ -10,11 +10,7 @@ export const getNextListDepth = (typeOrName: string, state: EditorState) => {
return false; return false;
} }
const [, depth] = getNodeAtPosition( const [, depth] = getNodeAtPosition(state, typeOrName, listItemPos.$pos.pos + 4);
state,
typeOrName,
listItemPos.$pos.pos + 4,
);
return depth; return depth;
}; };

View File

@ -4,11 +4,7 @@ import { Node } from "@tiptap/pm/model";
import { findListItemPos } from "./find-list-item-pos"; import { findListItemPos } from "./find-list-item-pos";
import { hasListBefore } from "./has-list-before"; import { hasListBefore } from "./has-list-before";
export const handleBackspace = ( export const handleBackspace = (editor: Editor, name: string, parentListTypes: string[]) => {
editor: Editor,
name: string,
parentListTypes: string[],
) => {
// this is required to still handle the undo handling // this is required to still handle the undo handling
if (editor.commands.undoInputRule()) { if (editor.commands.undoInputRule()) {
return true; return true;
@ -23,10 +19,7 @@ export const handleBackspace = (
// if the current item is NOT inside a list item & // if the current item is NOT inside a list item &
// the previous item is a list (orderedList or bulletList) // the previous item is a list (orderedList or bulletList)
// move the cursor into the list and delete the current item // move the cursor into the list and delete the current item
if ( if (!isNodeActive(editor.state, name) && hasListBefore(editor.state, name, parentListTypes)) {
!isNodeActive(editor.state, name) &&
hasListBefore(editor.state, name, parentListTypes)
) {
const { $anchor } = editor.state.selection; const { $anchor } = editor.state.selection;
const $listPos = editor.state.doc.resolve($anchor.before() - 1); const $listPos = editor.state.doc.resolve($anchor.before() - 1);
@ -45,16 +38,11 @@ export const handleBackspace = (
return false; return false;
} }
const $lastItemPos = editor.state.doc.resolve( const $lastItemPos = editor.state.doc.resolve($listPos.start() + lastItem.pos + 1);
$listPos.start() + lastItem.pos + 1,
);
return editor return editor
.chain() .chain()
.cut( .cut({ from: $anchor.start() - 1, to: $anchor.end() + 1 }, $lastItemPos.end())
{ from: $anchor.start() - 1, to: $anchor.end() + 1 },
$lastItemPos.end(),
)
.joinForward() .joinForward()
.run(); .run();
} }

View File

@ -1,10 +1,6 @@
import { EditorState } from "@tiptap/pm/state"; import { EditorState } from "@tiptap/pm/state";
export const hasListBefore = ( export const hasListBefore = (editorState: EditorState, name: string, parentListTypes: string[]) => {
editorState: EditorState,
name: string,
parentListTypes: string[],
) => {
const { $anchor } = editorState.selection; const { $anchor } = editorState.selection;
const previousNodePos = Math.max(0, $anchor.pos - 2); const previousNodePos = Math.max(0, $anchor.pos - 2);

View File

@ -1,9 +1,6 @@
import { EditorState } from "@tiptap/pm/state"; import { EditorState } from "@tiptap/pm/state";
export const hasListItemAfter = ( export const hasListItemAfter = (typeOrName: string, state: EditorState): boolean => {
typeOrName: string,
state: EditorState,
): boolean => {
const { $anchor } = state.selection; const { $anchor } = state.selection;
const $targetPos = state.doc.resolve($anchor.pos - $anchor.parentOffset - 2); const $targetPos = state.doc.resolve($anchor.pos - $anchor.parentOffset - 2);

View File

@ -1,9 +1,6 @@
import { EditorState } from "@tiptap/pm/state"; import { EditorState } from "@tiptap/pm/state";
export const hasListItemBefore = ( export const hasListItemBefore = (typeOrName: string, state: EditorState): boolean => {
typeOrName: string,
state: EditorState,
): boolean => {
const { $anchor } = state.selection; const { $anchor } = state.selection;
const $targetPos = state.doc.resolve($anchor.pos - 2); const $targetPos = state.doc.resolve($anchor.pos - 2);

View File

@ -1,12 +1,6 @@
import { TextSelection } from "prosemirror-state"; import { TextSelection } from "prosemirror-state";
import { import { InputRule, mergeAttributes, Node, nodeInputRule, wrappingInputRule } from "@tiptap/core";
InputRule,
mergeAttributes,
Node,
nodeInputRule,
wrappingInputRule,
} from "@tiptap/core";
/** /**
* Extension based on: * Extension based on:
@ -83,8 +77,7 @@ export default Node.create<HorizontalRuleOptions>({
tr.setSelection(TextSelection.create(tr.doc, $to.pos)); tr.setSelection(TextSelection.create(tr.doc, $to.pos));
} else { } else {
// add node after horizontal rule if its the end of the document // add node after horizontal rule if its the end of the document
const node = const node = $to.parent.type.contentMatch.defaultType?.create();
$to.parent.type.contentMatch.defaultType?.create();
if (node) { if (node) {
tr.insert(posAfter, node); tr.insert(posAfter, node);

View File

@ -4,9 +4,7 @@ import Moveable from "react-moveable";
export const ImageResizer = ({ editor }: { editor: Editor }) => { export const ImageResizer = ({ editor }: { editor: Editor }) => {
const updateMediaSize = () => { const updateMediaSize = () => {
const imageInfo = document.querySelector( const imageInfo = document.querySelector(".ProseMirror-selectednode") as HTMLImageElement;
".ProseMirror-selectednode",
) as HTMLImageElement;
if (imageInfo) { if (imageInfo) {
const selection = editor.state.selection; const selection = editor.state.selection;
editor.commands.setImage({ editor.commands.setImage({
@ -32,9 +30,7 @@ export const ImageResizer = ({ editor }: { editor: Editor }) => {
resizable resizable
throttleResize={0} throttleResize={0}
onResizeStart={() => { onResizeStart={() => {
const imageInfo = document.querySelector( const imageInfo = document.querySelector(".ProseMirror-selectednode") as HTMLImageElement;
".ProseMirror-selectednode",
) as HTMLImageElement;
if (imageInfo) { if (imageInfo) {
const originalWidth = Number(imageInfo.width); const originalWidth = Number(imageInfo.width);
const originalHeight = Number(imageInfo.height); const originalHeight = Number(imageInfo.height);

View File

@ -15,22 +15,14 @@ interface ImageNode extends ProseMirrorNode {
const deleteKey = new PluginKey("delete-image"); const deleteKey = new PluginKey("delete-image");
const IMAGE_NODE_TYPE = "image"; const IMAGE_NODE_TYPE = "image";
const ImageExtension = ( const ImageExtension = (deleteImage: DeleteImage, restoreFile: RestoreImage, cancelUploadImage?: () => any) =>
deleteImage: DeleteImage,
restoreFile: RestoreImage,
cancelUploadImage?: () => any,
) =>
ImageExt.extend({ ImageExt.extend({
addProseMirrorPlugins() { addProseMirrorPlugins() {
return [ return [
UploadImagesPlugin(cancelUploadImage), UploadImagesPlugin(cancelUploadImage),
new Plugin({ new Plugin({
key: deleteKey, key: deleteKey,
appendTransaction: ( appendTransaction: (transactions: readonly Transaction[], oldState: EditorState, newState: EditorState) => {
transactions: readonly Transaction[],
oldState: EditorState,
newState: EditorState,
) => {
const newImageSources = new Set<string>(); const newImageSources = new Set<string>();
newState.doc.descendants((node) => { newState.doc.descendants((node) => {
if (node.type.name === IMAGE_NODE_TYPE) { if (node.type.name === IMAGE_NODE_TYPE) {
@ -67,11 +59,7 @@ const ImageExtension = (
}), }),
new Plugin({ new Plugin({
key: new PluginKey("imageRestoration"), key: new PluginKey("imageRestoration"),
appendTransaction: ( appendTransaction: (transactions: readonly Transaction[], oldState: EditorState, newState: EditorState) => {
transactions: readonly Transaction[],
oldState: EditorState,
newState: EditorState,
) => {
const oldImageSources = new Set<string>(); const oldImageSources = new Set<string>();
oldState.doc.descendants((node) => { oldState.doc.descendants((node) => {
if (node.type.name === IMAGE_NODE_TYPE) { if (node.type.name === IMAGE_NODE_TYPE) {

View File

@ -22,11 +22,7 @@ import { CustomKeymap } from "./keymap";
import { CustomCodeBlock } from "./code"; import { CustomCodeBlock } from "./code";
import { CustomQuoteExtension } from "./quote"; import { CustomQuoteExtension } from "./quote";
import { ListKeymap } from "./custom-list-keymap"; import { ListKeymap } from "./custom-list-keymap";
import { import { IMentionSuggestion, DeleteImage, RestoreImage } from "@plane/editor-types";
IMentionSuggestion,
DeleteImage,
RestoreImage,
} from "@plane/editor-types";
export const CoreEditorExtensions = ( export const CoreEditorExtensions = (
mentionConfig: { mentionConfig: {
@ -109,9 +105,5 @@ export const CoreEditorExtensions = (
TableHeader, TableHeader,
TableCell, TableCell,
TableRow, TableRow,
Mentions( Mentions(mentionConfig.mentionSuggestions, mentionConfig.mentionHighlights, false),
mentionConfig.mentionSuggestions,
mentionConfig.mentionHighlights,
false
),
]; ];

View File

@ -22,10 +22,6 @@ export default Node.create<TableRowOptions>({
}, },
renderHTML({ HTMLAttributes }) { renderHTML({ HTMLAttributes }) {
return [ return ["tr", mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0];
"tr",
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
0,
];
}, },
}); });

View File

@ -20,15 +20,12 @@ export function tableControls() {
mousemove: (view, event) => { mousemove: (view, event) => {
const pluginState = key.getState(view.state); const pluginState = key.getState(view.state);
if ( if (!(event.target as HTMLElement).closest(".tableWrapper") && pluginState.values.hoveredTable) {
!(event.target as HTMLElement).closest(".tableWrapper") &&
pluginState.values.hoveredTable
) {
return view.dispatch( return view.dispatch(
view.state.tr.setMeta(key, { view.state.tr.setMeta(key, {
setHoveredTable: null, setHoveredTable: null,
setHoveredCell: null, setHoveredCell: null,
}), })
); );
} }
@ -40,13 +37,11 @@ export function tableControls() {
if (!pos) return; if (!pos) return;
const table = findParentNode((node) => node.type.name === "table")( const table = findParentNode((node) => node.type.name === "table")(
TextSelection.create(view.state.doc, pos.pos), TextSelection.create(view.state.doc, pos.pos)
);
const cell = findParentNode((node) => node.type.name === "tableCell" || node.type.name === "tableHeader")(
TextSelection.create(view.state.doc, pos.pos)
); );
const cell = findParentNode(
(node) =>
node.type.name === "tableCell" ||
node.type.name === "tableHeader",
)(TextSelection.create(view.state.doc, pos.pos));
if (!table || !cell) return; if (!table || !cell) return;
@ -55,7 +50,7 @@ export function tableControls() {
view.state.tr.setMeta(key, { view.state.tr.setMeta(key, {
setHoveredTable: table, setHoveredTable: table,
setHoveredCell: cell, setHoveredCell: cell,
}), })
); );
} }
}, },
@ -68,12 +63,7 @@ export function tableControls() {
const { hoveredTable, hoveredCell } = pluginState.values; const { hoveredTable, hoveredCell } = pluginState.values;
const docSize = state.doc.content.size; const docSize = state.doc.content.size;
if ( if (hoveredTable && hoveredCell && hoveredTable.pos < docSize && hoveredCell.pos < docSize) {
hoveredTable &&
hoveredCell &&
hoveredTable.pos < docSize &&
hoveredCell.pos < docSize
) {
const decorations = [ const decorations = [
Decoration.node( Decoration.node(
hoveredTable.pos, hoveredTable.pos,
@ -82,7 +72,7 @@ export function tableControls() {
{ {
hoveredTable, hoveredTable,
hoveredCell, hoveredCell,
}, }
), ),
]; ];

View File

@ -4,11 +4,7 @@ import { Decoration, NodeView } from "@tiptap/pm/view";
import tippy, { Instance, Props } from "tippy.js"; import tippy, { Instance, Props } from "tippy.js";
import { Editor } from "@tiptap/core"; import { Editor } from "@tiptap/core";
import { import { CellSelection, TableMap, updateColumnsOnResize } from "@tiptap/prosemirror-tables";
CellSelection,
TableMap,
updateColumnsOnResize,
} from "@tiptap/prosemirror-tables";
import icons from "./icons"; import icons from "./icons";
@ -18,7 +14,7 @@ export function updateColumns(
table: HTMLElement, table: HTMLElement,
cellMinWidth: number, cellMinWidth: number,
overrideCol?: number, overrideCol?: number,
overrideValue?: any, overrideValue?: any
) { ) {
let totalWidth = 0; let totalWidth = 0;
let fixedWidth = true; let fixedWidth = true;
@ -31,8 +27,7 @@ export function updateColumns(
const { colspan, colwidth } = row.child(i).attrs; const { colspan, colwidth } = row.child(i).attrs;
for (let j = 0; j < colspan; j += 1, col += 1) { for (let j = 0; j < colspan; j += 1, col += 1) {
const hasWidth = const hasWidth = overrideCol === col ? overrideValue : colwidth && colwidth[j];
overrideCol === col ? overrideValue : colwidth && colwidth[j];
const cssWidth = hasWidth ? `${hasWidth}px` : ""; const cssWidth = hasWidth ? `${hasWidth}px` : "";
totalWidth += hasWidth || cellMinWidth; totalWidth += hasWidth || cellMinWidth;
@ -42,8 +37,7 @@ export function updateColumns(
} }
if (!nextDOM) { if (!nextDOM) {
colgroup.appendChild(document.createElement("col")).style.width = colgroup.appendChild(document.createElement("col")).style.width = cssWidth;
cssWidth;
} else { } else {
if (nextDOM.style.width !== cssWidth) { if (nextDOM.style.width !== cssWidth) {
nextDOM.style.width = cssWidth; nextDOM.style.width = cssWidth;
@ -98,14 +92,12 @@ const columnsToolboxItems = [
{ {
label: "Add Column Before", label: "Add Column Before",
icon: icons.insertLeftTableIcon, icon: icons.insertLeftTableIcon,
action: ({ editor }: { editor: Editor }) => action: ({ editor }: { editor: Editor }) => editor.chain().focus().addColumnBefore().run(),
editor.chain().focus().addColumnBefore().run(),
}, },
{ {
label: "Add Column After", label: "Add Column After",
icon: icons.insertRightTableIcon, icon: icons.insertRightTableIcon,
action: ({ editor }: { editor: Editor }) => action: ({ editor }: { editor: Editor }) => editor.chain().focus().addColumnAfter().run(),
editor.chain().focus().addColumnAfter().run(),
}, },
{ {
label: "Pick Column Color", label: "Pick Column Color",
@ -131,8 +123,7 @@ const columnsToolboxItems = [
{ {
label: "Delete Column", label: "Delete Column",
icon: icons.deleteColumn, icon: icons.deleteColumn,
action: ({ editor }: { editor: Editor }) => action: ({ editor }: { editor: Editor }) => editor.chain().focus().deleteColumn().run(),
editor.chain().focus().deleteColumn().run(),
}, },
]; ];
@ -140,14 +131,12 @@ const rowsToolboxItems = [
{ {
label: "Add Row Above", label: "Add Row Above",
icon: icons.insertTopTableIcon, icon: icons.insertTopTableIcon,
action: ({ editor }: { editor: Editor }) => action: ({ editor }: { editor: Editor }) => editor.chain().focus().addRowBefore().run(),
editor.chain().focus().addRowBefore().run(),
}, },
{ {
label: "Add Row Below", label: "Add Row Below",
icon: icons.insertBottomTableIcon, icon: icons.insertBottomTableIcon,
action: ({ editor }: { editor: Editor }) => action: ({ editor }: { editor: Editor }) => editor.chain().focus().addRowAfter().run(),
editor.chain().focus().addRowAfter().run(),
}, },
{ {
label: "Pick Row Color", label: "Pick Row Color",
@ -159,11 +148,7 @@ const rowsToolboxItems = [
}: { }: {
editor: Editor; editor: Editor;
triggerButton: HTMLButtonElement; triggerButton: HTMLButtonElement;
controlsContainer: controlsContainer: Element | "parent" | ((ref: Element) => Element) | undefined;
| Element
| "parent"
| ((ref: Element) => Element)
| undefined;
}) => { }) => {
createColorPickerToolbox({ createColorPickerToolbox({
triggerButton, triggerButton,
@ -177,8 +162,7 @@ const rowsToolboxItems = [
{ {
label: "Delete Row", label: "Delete Row",
icon: icons.deleteRow, icon: icons.deleteRow,
action: ({ editor }: { editor: Editor }) => action: ({ editor }: { editor: Editor }) => editor.chain().focus().deleteRow().run(),
editor.chain().focus().deleteRow().run(),
}, },
]; ];
@ -213,9 +197,9 @@ function createToolbox({
innerHTML: item.icon, innerHTML: item.icon,
}), }),
h("div", { className: "label" }, item.label), h("div", { className: "label" }, item.label),
], ]
), )
), )
), ),
...tippyOptions, ...tippyOptions,
}); });
@ -272,11 +256,11 @@ function createColorPickerToolbox({
{ {
className: "label", className: "label",
}, },
key, key
), ),
], ]
), )
), )
), ),
onHidden: (instance) => { onHidden: (instance) => {
instance.destroy(); instance.destroy();
@ -319,7 +303,7 @@ export class TableView implements NodeView {
cellMinWidth: number, cellMinWidth: number,
decorations: Decoration[], decorations: Decoration[],
editor: Editor, editor: Editor,
getPos: () => number, getPos: () => number
) { ) {
this.node = node; this.node = node;
this.cellMinWidth = cellMinWidth; this.cellMinWidth = cellMinWidth;
@ -337,7 +321,7 @@ export class TableView implements NodeView {
itemType: "button", itemType: "button",
className: "rowsControlDiv", className: "rowsControlDiv",
onClick: () => this.selectRow(), onClick: () => this.selectRow(),
}), })
); );
this.columnsControl = h( this.columnsControl = h(
@ -347,14 +331,14 @@ export class TableView implements NodeView {
itemType: "button", itemType: "button",
className: "columnsControlDiv", className: "columnsControlDiv",
onClick: () => this.selectColumn(), onClick: () => this.selectColumn(),
}), })
); );
this.controls = h( this.controls = h(
"div", "div",
{ className: "tableControls", contentEditable: "false" }, { className: "tableControls", contentEditable: "false" },
this.rowsControl, this.rowsControl,
this.columnsControl, this.columnsControl
); );
this.columnsToolbox = createToolbox({ this.columnsToolbox = createToolbox({
@ -397,7 +381,7 @@ export class TableView implements NodeView {
this.colgroup = h( this.colgroup = h(
"colgroup", "colgroup",
null, null,
Array.from({ length: this.map.width }, () => 1).map(() => h("col")), Array.from({ length: this.map.width }, () => 1).map(() => h("col"))
); );
this.tbody = h("tbody"); this.tbody = h("tbody");
this.table = h("table", null, this.colgroup, this.tbody); this.table = h("table", null, this.colgroup, this.tbody);
@ -408,7 +392,7 @@ export class TableView implements NodeView {
className: "tableWrapper controls--disabled", className: "tableWrapper controls--disabled",
}, },
this.controls, this.controls,
this.table, this.table
); );
this.render(); this.render();
@ -434,18 +418,11 @@ export class TableView implements NodeView {
render() { render() {
if (this.colgroup.children.length !== this.map.width) { if (this.colgroup.children.length !== this.map.width) {
const cols = Array.from({ length: this.map.width }, () => 1).map(() => const cols = Array.from({ length: this.map.width }, () => 1).map(() => h("col"));
h("col"),
);
this.colgroup.replaceChildren(...cols); this.colgroup.replaceChildren(...cols);
} }
updateColumnsOnResize( updateColumnsOnResize(this.node, this.colgroup, this.table, this.cellMinWidth);
this.node,
this.colgroup,
this.table,
this.cellMinWidth,
);
} }
ignoreMutation() { ignoreMutation() {
@ -453,9 +430,7 @@ export class TableView implements NodeView {
} }
updateControls() { updateControls() {
const { hoveredTable: table, hoveredCell: cell } = Object.values( const { hoveredTable: table, hoveredCell: cell } = Object.values(this.decorations).reduce(
this.decorations,
).reduce(
(acc, curr) => { (acc, curr) => {
if (curr.spec.hoveredCell !== undefined) { if (curr.spec.hoveredCell !== undefined) {
acc["hoveredCell"] = curr.spec.hoveredCell; acc["hoveredCell"] = curr.spec.hoveredCell;
@ -466,7 +441,7 @@ export class TableView implements NodeView {
} }
return acc; return acc;
}, },
{} as Record<string, HTMLElement>, {} as Record<string, HTMLElement>
) as any; ) as any;
if (table === undefined || cell === undefined) { if (table === undefined || cell === undefined) {
@ -481,9 +456,7 @@ export class TableView implements NodeView {
const tableRect = this.table.getBoundingClientRect(); const tableRect = this.table.getBoundingClientRect();
const cellRect = cellDom.getBoundingClientRect(); const cellRect = cellDom.getBoundingClientRect();
this.columnsControl.style.left = `${ this.columnsControl.style.left = `${cellRect.left - tableRect.left - this.table.parentElement!.scrollLeft}px`;
cellRect.left - tableRect.left - this.table.parentElement!.scrollLeft
}px`;
this.columnsControl.style.width = `${cellRect.width}px`; this.columnsControl.style.width = `${cellRect.width}px`;
this.rowsControl.style.top = `${cellRect.top - tableRect.top}px`; this.rowsControl.style.top = `${cellRect.top - tableRect.top}px`;
@ -493,22 +466,14 @@ export class TableView implements NodeView {
selectColumn() { selectColumn() {
if (!this.hoveredCell) return; if (!this.hoveredCell) return;
const colIndex = this.map.colCount( const colIndex = this.map.colCount(this.hoveredCell.pos - (this.getPos() + 1));
this.hoveredCell.pos - (this.getPos() + 1),
);
const anchorCellPos = this.hoveredCell.pos; const anchorCellPos = this.hoveredCell.pos;
const headCellPos = const headCellPos = this.map.map[colIndex + this.map.width * (this.map.height - 1)] + (this.getPos() + 1);
this.map.map[colIndex + this.map.width * (this.map.height - 1)] +
(this.getPos() + 1);
const cellSelection = CellSelection.create( const cellSelection = CellSelection.create(this.editor.view.state.doc, anchorCellPos, headCellPos);
this.editor.view.state.doc,
anchorCellPos,
headCellPos,
);
this.editor.view.dispatch( this.editor.view.dispatch(
// @ts-ignore // @ts-ignore
this.editor.state.tr.setSelection(cellSelection), this.editor.state.tr.setSelection(cellSelection)
); );
} }
@ -516,21 +481,13 @@ export class TableView implements NodeView {
if (!this.hoveredCell) return; if (!this.hoveredCell) return;
const anchorCellPos = this.hoveredCell.pos; const anchorCellPos = this.hoveredCell.pos;
const anchorCellIndex = this.map.map.indexOf( const anchorCellIndex = this.map.map.indexOf(anchorCellPos - (this.getPos() + 1));
anchorCellPos - (this.getPos() + 1), const headCellPos = this.map.map[anchorCellIndex + (this.map.width - 1)] + (this.getPos() + 1);
);
const headCellPos =
this.map.map[anchorCellIndex + (this.map.width - 1)] +
(this.getPos() + 1);
const cellSelection = CellSelection.create( const cellSelection = CellSelection.create(this.editor.state.doc, anchorCellPos, headCellPos);
this.editor.state.doc,
anchorCellPos,
headCellPos,
);
this.editor.view.dispatch( this.editor.view.dispatch(
// @ts-ignore // @ts-ignore
this.editor.view.state.tr.setSelection(cellSelection), this.editor.view.state.tr.setSelection(cellSelection)
); );
} }
} }

View File

@ -1,12 +1,6 @@
import { TextSelection } from "@tiptap/pm/state"; import { TextSelection } from "@tiptap/pm/state";
import { import { callOrReturn, getExtensionField, mergeAttributes, Node, ParentConfig } from "@tiptap/core";
callOrReturn,
getExtensionField,
mergeAttributes,
Node,
ParentConfig,
} from "@tiptap/core";
import { import {
addColumnAfter, addColumnAfter,
addColumnBefore, addColumnBefore,
@ -44,11 +38,7 @@ export interface TableOptions {
declare module "@tiptap/core" { declare module "@tiptap/core" {
interface Commands<ReturnType> { interface Commands<ReturnType> {
table: { table: {
insertTable: (options?: { insertTable: (options?: { rows?: number; cols?: number; withHeaderRow?: boolean }) => ReturnType;
rows?: number;
cols?: number;
withHeaderRow?: boolean;
}) => ReturnType;
addColumnBefore: () => ReturnType; addColumnBefore: () => ReturnType;
addColumnAfter: () => ReturnType; addColumnAfter: () => ReturnType;
deleteColumn: () => ReturnType; deleteColumn: () => ReturnType;
@ -66,10 +56,7 @@ declare module "@tiptap/core" {
goToNextCell: () => ReturnType; goToNextCell: () => ReturnType;
goToPreviousCell: () => ReturnType; goToPreviousCell: () => ReturnType;
fixTables: () => ReturnType; fixTables: () => ReturnType;
setCellSelection: (position: { setCellSelection: (position: { anchorCell: number; headCell?: number }) => ReturnType;
anchorCell: number;
headCell?: number;
}) => ReturnType;
}; };
} }
@ -114,11 +101,7 @@ export default Node.create({
}, },
renderHTML({ HTMLAttributes }) { renderHTML({ HTMLAttributes }) {
return [ return ["table", mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), ["tbody", 0]];
"table",
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
["tbody", 0],
];
}, },
addCommands() { addCommands() {
@ -220,11 +203,7 @@ export default Node.create({
(position) => (position) =>
({ tr, dispatch }) => { ({ tr, dispatch }) => {
if (dispatch) { if (dispatch) {
const selection = CellSelection.create( const selection = CellSelection.create(tr.doc, position.anchorCell, position.headCell);
tr.doc,
position.anchorCell,
position.headCell,
);
// @ts-ignore // @ts-ignore
tr.setSelection(selection); tr.setSelection(selection);
@ -260,13 +239,7 @@ export default Node.create({
return ({ editor, getPos, node, decorations }) => { return ({ editor, getPos, node, decorations }) => {
const { cellMinWidth } = this.options; const { cellMinWidth } = this.options;
return new TableView( return new TableView(node, cellMinWidth, decorations, editor, getPos as () => number);
node,
cellMinWidth,
decorations,
editor,
getPos as () => number,
);
}; };
}, },
@ -289,7 +262,7 @@ export default Node.create({
// @ts-ignore // @ts-ignore
lastColumnResizable: this.options.lastColumnResizable, lastColumnResizable: this.options.lastColumnResizable,
}), })
); );
} }
@ -304,9 +277,7 @@ export default Node.create({
}; };
return { return {
tableRole: callOrReturn( tableRole: callOrReturn(getExtensionField(extension, "tableRole", context)),
getExtensionField(extension, "tableRole", context),
),
}; };
}, },
}); });

View File

@ -2,7 +2,7 @@ import { Fragment, Node as ProsemirrorNode, NodeType } from "prosemirror-model";
export function createCell( export function createCell(
cellType: NodeType, cellType: NodeType,
cellContent?: Fragment | ProsemirrorNode | Array<ProsemirrorNode>, cellContent?: Fragment | ProsemirrorNode | Array<ProsemirrorNode>
): ProsemirrorNode | null | undefined { ): ProsemirrorNode | null | undefined {
if (cellContent) { if (cellContent) {
return cellType.createChecked(null, cellContent); return cellType.createChecked(null, cellContent);

View File

@ -8,7 +8,7 @@ export function createTable(
rowsCount: number, rowsCount: number,
colsCount: number, colsCount: number,
withHeaderRow: boolean, withHeaderRow: boolean,
cellContent?: Fragment | ProsemirrorNode | Array<ProsemirrorNode>, cellContent?: Fragment | ProsemirrorNode | Array<ProsemirrorNode>
): ProsemirrorNode { ): ProsemirrorNode {
const types = getTableNodeTypes(schema); const types = getTableNodeTypes(schema);
const headerCells: ProsemirrorNode[] = []; const headerCells: ProsemirrorNode[] = [];
@ -33,12 +33,7 @@ export function createTable(
const rows: ProsemirrorNode[] = []; const rows: ProsemirrorNode[] = [];
for (let index = 0; index < rowsCount; index += 1) { for (let index = 0; index < rowsCount; index += 1) {
rows.push( rows.push(types.row.createChecked(null, withHeaderRow && index === 0 ? headerCells : cells));
types.row.createChecked(
null,
withHeaderRow && index === 0 ? headerCells : cells,
),
);
} }
return types.table.createChecked(null, rows); return types.table.createChecked(null, rows);

View File

@ -1,13 +1,8 @@
import { import { findParentNodeClosestToPos, KeyboardShortcutCommand } from "@tiptap/core";
findParentNodeClosestToPos,
KeyboardShortcutCommand,
} from "@tiptap/core";
import { isCellSelection } from "./is-cell-selection"; import { isCellSelection } from "./is-cell-selection";
export const deleteTableWhenAllCellsSelected: KeyboardShortcutCommand = ({ export const deleteTableWhenAllCellsSelected: KeyboardShortcutCommand = ({ editor }) => {
editor,
}) => {
const { selection } = editor.state; const { selection } = editor.state;
if (!isCellSelection(selection)) { if (!isCellSelection(selection)) {
@ -15,10 +10,7 @@ export const deleteTableWhenAllCellsSelected: KeyboardShortcutCommand = ({
} }
let cellCount = 0; let cellCount = 0;
const table = findParentNodeClosestToPos( const table = findParentNodeClosestToPos(selection.ranges[0].$from, (node) => node.type.name === "table");
selection.ranges[0].$from,
(node) => node.type.name === "table",
);
table?.node.descendants((node) => { table?.node.descendants((node) => {
if (node.type.name === "table") { if (node.type.name === "table") {

View File

@ -4,12 +4,7 @@ import { CoreEditorProps } from "../props";
import { CoreEditorExtensions } from "../extensions"; import { CoreEditorExtensions } from "../extensions";
import { EditorProps } from "@tiptap/pm/view"; import { EditorProps } from "@tiptap/pm/view";
import { getTrimmedHTML } from "../../lib/utils"; import { getTrimmedHTML } from "../../lib/utils";
import { import { DeleteImage, IMentionSuggestion, RestoreImage, UploadImage } from "@plane/editor-types";
DeleteImage,
IMentionSuggestion,
RestoreImage,
UploadImage,
} from "@plane/editor-types";
interface CustomEditorProps { interface CustomEditorProps {
uploadFile: UploadImage; uploadFile: UploadImage;
@ -20,9 +15,7 @@ interface CustomEditorProps {
}; };
deleteFile: DeleteImage; deleteFile: DeleteImage;
cancelUploadImage?: () => any; cancelUploadImage?: () => any;
setIsSubmitting?: ( setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void;
isSubmitting: "submitting" | "submitted" | "saved",
) => void;
setShouldShowAlert?: (showAlert: boolean) => void; setShouldShowAlert?: (showAlert: boolean) => void;
value: string; value: string;
debouncedUpdatesEnabled?: boolean; debouncedUpdatesEnabled?: boolean;
@ -66,12 +59,11 @@ export const useEditor = ({
}, },
deleteFile, deleteFile,
restoreFile, restoreFile,
cancelUploadImage, cancelUploadImage
), ),
...extensions, ...extensions,
], ],
content: content: typeof value === "string" && value.trim() !== "" ? value : "<p></p>",
typeof value === "string" && value.trim() !== "" ? value : "<p></p>",
onCreate: async ({ editor }) => { onCreate: async ({ editor }) => {
onStart?.(editor.getJSON(), getTrimmedHTML(editor.getHTML())); onStart?.(editor.getJSON(), getTrimmedHTML(editor.getHTML()));
}, },
@ -82,7 +74,7 @@ export const useEditor = ({
onChange?.(editor.getJSON(), getTrimmedHTML(editor.getHTML())); onChange?.(editor.getJSON(), getTrimmedHTML(editor.getHTML()));
}, },
}, },
[rerenderOnPropsChange], [rerenderOnPropsChange]
); );
const editorRef: MutableRefObject<Editor | null> = useRef(null); const editorRef: MutableRefObject<Editor | null> = useRef(null);

View File

@ -30,8 +30,7 @@ export const useReadOnlyEditor = ({
const editor = useCustomEditor( const editor = useCustomEditor(
{ {
editable: false, editable: false,
content: content: typeof value === "string" && value.trim() !== "" ? value : "<p></p>",
typeof value === "string" && value.trim() !== "" ? value : "<p></p>",
editorProps: { editorProps: {
...CoreReadOnlyEditorProps, ...CoreReadOnlyEditorProps,
...editorProps, ...editorProps,
@ -44,7 +43,7 @@ export const useReadOnlyEditor = ({
...extensions, ...extensions,
], ],
}, },
[rerenderOnPropsChange], [rerenderOnPropsChange]
); );
const editorRef: MutableRefObject<Editor | null> = useRef(null); const editorRef: MutableRefObject<Editor | null> = useRef(null);

View File

@ -1,21 +1,10 @@
import { IMentionSuggestion } from "@plane/editor-types"; import { IMentionSuggestion } from "@plane/editor-types";
import { Editor } from "@tiptap/react"; import { Editor } from "@tiptap/react";
import React, { import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useState } from "react";
forwardRef,
useCallback,
useEffect,
useImperativeHandle,
useState,
} from "react";
interface MentionListProps { interface MentionListProps {
items: IMentionSuggestion[]; items: IMentionSuggestion[];
command: (item: { command: (item: { id: string; label: string; target: string; redirect_uri: string }) => void;
id: string;
label: string;
target: string;
redirect_uri: string;
}) => void;
editor: Editor; editor: Editor;
} }
@ -37,9 +26,7 @@ const MentionList = forwardRef((props: MentionListProps, ref) => {
}; };
const upHandler = () => { const upHandler = () => {
setSelectedIndex( setSelectedIndex((selectedIndex + props.items.length - 1) % props.items.length);
(selectedIndex + props.items.length - 1) % props.items.length,
);
}; };
const downHandler = () => { const downHandler = () => {
@ -76,31 +63,27 @@ const MentionList = forwardRef((props: MentionListProps, ref) => {
})); }));
return props.items && props.items.length !== 0 ? ( return props.items && props.items.length !== 0 ? (
<div className="mentions absolute max-h-40 bg-custom-background-100 rounded-md shadow-custom-shadow-sm text-custom-text-300 text-sm overflow-y-auto w-48 p-1 space-y-0.5"> <div className="mentions absolute max-h-40 w-48 space-y-0.5 overflow-y-auto rounded-md bg-custom-background-100 p-1 text-sm text-custom-text-300 shadow-custom-shadow-sm">
{props.items.length ? ( {props.items.length ? (
props.items.map((item, index) => ( props.items.map((item, index) => (
<div <div
key={item.id} key={item.id}
className={`flex items-center gap-2 rounded p-1 hover:bg-custom-background-80 cursor-pointer ${ className={`flex cursor-pointer items-center gap-2 rounded p-1 hover:bg-custom-background-80 ${
index === selectedIndex ? "bg-custom-background-80" : "" index === selectedIndex ? "bg-custom-background-80" : ""
}`} }`}
onClick={() => selectItem(index)} onClick={() => selectItem(index)}
> >
<div className="flex-shrink-0 h-4 w-4 grid place-items-center overflow-hidden"> <div className="grid h-4 w-4 flex-shrink-0 place-items-center overflow-hidden">
{item.avatar && item.avatar.trim() !== "" ? ( {item.avatar && item.avatar.trim() !== "" ? (
<img <img src={item.avatar} className="h-full w-full rounded-sm object-cover" alt={item.title} />
src={item.avatar}
className="h-full w-full object-cover rounded-sm"
alt={item.title}
/>
) : ( ) : (
<div className="h-full w-full grid place-items-center text-xs capitalize text-white rounded-sm bg-gray-700"> <div className="grid h-full w-full place-items-center rounded-sm bg-gray-700 text-xs capitalize text-white">
{item.title[0]} {item.title[0]}
</div> </div>
)} )}
</div> </div>
<div className="flex-grow space-y-1 truncate"> <div className="flex-grow space-y-1 truncate">
<p className="text-sm font-medium truncate">{item.title}</p> <p className="truncate text-sm font-medium">{item.title}</p>
{/* <p className="text-xs text-gray-400">{item.subtitle}</p> */} {/* <p className="text-xs text-gray-400">{item.subtitle}</p> */}
</div> </div>
</div> </div>

View File

@ -4,11 +4,7 @@ import suggestion from "./suggestion";
import { CustomMention } from "./custom"; import { CustomMention } from "./custom";
import { IMentionHighlight, IMentionSuggestion } from "@plane/editor-types"; import { IMentionHighlight, IMentionSuggestion } from "@plane/editor-types";
export const Mentions = ( export const Mentions = (mentionSuggestions: IMentionSuggestion[], mentionHighlights: IMentionHighlight[], readonly) =>
mentionSuggestions: IMentionSuggestion[],
mentionHighlights: IMentionHighlight[],
readonly,
) =>
CustomMention.configure({ CustomMention.configure({
HTMLAttributes: { HTMLAttributes: {
class: "mention", class: "mention",

View File

@ -8,8 +8,7 @@ import { IMentionHighlight } from "@plane/editor-types";
// eslint-disable-next-line import/no-anonymous-default-export // eslint-disable-next-line import/no-anonymous-default-export
export default (props) => { export default (props) => {
const router = useRouter(); const router = useRouter();
const highlights = props.extension.options const highlights = props.extension.options.mentionHighlights as IMentionHighlight[];
.mentionHighlights as IMentionHighlight[];
const handleClick = () => { const handleClick = () => {
if (!props.extension.options.readonly) { if (!props.extension.options.readonly) {
@ -18,18 +17,13 @@ export default (props) => {
}; };
return ( return (
<NodeViewWrapper className="w-fit inline mention-component"> <NodeViewWrapper className="mention-component inline w-fit">
<span <span
className={cn( className={cn("mention rounded bg-custom-primary-100/20 px-1 py-0.5 font-medium text-custom-primary-100", {
"px-1 py-0.5 bg-custom-primary-100/20 text-custom-primary-100 rounded font-medium mention", "bg-yellow-500/20 text-yellow-500": highlights ? highlights.includes(props.node.attrs.id) : false,
{ "cursor-pointer": !props.extension.options.readonly,
"text-yellow-500 bg-yellow-500/20": highlights // "hover:bg-custom-primary-300" : !props.extension.options.readonly && !highlights.includes(props.node.attrs.id)
? highlights.includes(props.node.attrs.id) })}
: false,
"cursor-pointer": !props.extension.options.readonly,
// "hover:bg-custom-primary-300" : !props.extension.options.readonly && !highlights.includes(props.node.attrs.id)
},
)}
onClick={handleClick} onClick={handleClick}
data-mention-target={props.node.attrs.target} data-mention-target={props.node.attrs.target}
data-mention-id={props.node.attrs.id} data-mention-id={props.node.attrs.id}

View File

@ -7,11 +7,7 @@ import { IMentionSuggestion } from "@plane/editor-types";
const Suggestion = (suggestions: IMentionSuggestion[]) => ({ const Suggestion = (suggestions: IMentionSuggestion[]) => ({
items: ({ query }: { query: string }) => items: ({ query }: { query: string }) =>
suggestions suggestions.filter((suggestion) => suggestion.title.toLowerCase().startsWith(query.toLowerCase())).slice(0, 5),
.filter((suggestion) =>
suggestion.title.toLowerCase().startsWith(query.toLowerCase()),
)
.slice(0, 5),
render: () => { render: () => {
let reactRenderer: ReactRenderer | null = null; let reactRenderer: ReactRenderer | null = null;
let popup: any | null = null; let popup: any | null = null;

View File

@ -134,9 +134,7 @@ export const TableItem = (editor: Editor): EditorMenuItem => ({
export const ImageItem = ( export const ImageItem = (
editor: Editor, editor: Editor,
uploadFile: UploadImage, uploadFile: UploadImage,
setIsSubmitting?: ( setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void
isSubmitting: "submitting" | "submitted" | "saved",
) => void,
): EditorMenuItem => ({ ): EditorMenuItem => ({
name: "image", name: "image",
isActive: () => editor?.isActive("image"), isActive: () => editor?.isActive("image"),

View File

@ -15,11 +15,7 @@ interface ImageNode extends ProseMirrorNode {
const TrackImageDeletionPlugin = (deleteImage: DeleteImage): Plugin => const TrackImageDeletionPlugin = (deleteImage: DeleteImage): Plugin =>
new Plugin({ new Plugin({
key: deleteKey, key: deleteKey,
appendTransaction: ( appendTransaction: (transactions: readonly Transaction[], oldState: EditorState, newState: EditorState) => {
transactions: readonly Transaction[],
oldState: EditorState,
newState: EditorState,
) => {
const newImageSources = new Set<string>(); const newImageSources = new Set<string>();
newState.doc.descendants((node) => { newState.doc.descendants((node) => {
if (node.type.name === IMAGE_NODE_TYPE) { if (node.type.name === IMAGE_NODE_TYPE) {
@ -59,10 +55,7 @@ const TrackImageDeletionPlugin = (deleteImage: DeleteImage): Plugin =>
export default TrackImageDeletionPlugin; export default TrackImageDeletionPlugin;
export async function onNodeDeleted( export async function onNodeDeleted(src: string, deleteImage: DeleteImage): Promise<void> {
src: string,
deleteImage: DeleteImage,
): Promise<void> {
try { try {
const assetUrlWithWorkspaceId = new URL(src).pathname.substring(1); const assetUrlWithWorkspaceId = new URL(src).pathname.substring(1);
const resStatus = await deleteImage(assetUrlWithWorkspaceId); const resStatus = await deleteImage(assetUrlWithWorkspaceId);
@ -74,10 +67,7 @@ export async function onNodeDeleted(
} }
} }
export async function onNodeRestored( export async function onNodeRestored(src: string, restoreImage: RestoreImage): Promise<void> {
src: string,
restoreImage: RestoreImage,
): Promise<void> {
try { try {
const assetUrlWithWorkspaceId = new URL(src).pathname.substring(1); const assetUrlWithWorkspaceId = new URL(src).pathname.substring(1);
const resStatus = await restoreImage(assetUrlWithWorkspaceId); const resStatus = await restoreImage(assetUrlWithWorkspaceId);

View File

@ -21,10 +21,7 @@ const UploadImagesPlugin = (cancelUploadImage?: () => any) =>
const placeholder = document.createElement("div"); const placeholder = document.createElement("div");
placeholder.setAttribute("class", "img-placeholder"); placeholder.setAttribute("class", "img-placeholder");
const image = document.createElement("img"); const image = document.createElement("img");
image.setAttribute( image.setAttribute("class", "opacity-10 rounded-lg border border-custom-border-300");
"class",
"opacity-10 rounded-lg border border-custom-border-300",
);
image.src = src; image.src = src;
placeholder.appendChild(image); placeholder.appendChild(image);
@ -42,10 +39,7 @@ const UploadImagesPlugin = (cancelUploadImage?: () => any) =>
// Create an SVG element from the SVG string // Create an SVG element from the SVG string
const svgString = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-circle"><circle cx="12" cy="12" r="10"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/></svg>`; const svgString = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-x-circle"><circle cx="12" cy="12" r="10"/><path d="m15 9-6 6"/><path d="m9 9 6 6"/></svg>`;
const parser = new DOMParser(); const parser = new DOMParser();
const svgElement = parser.parseFromString( const svgElement = parser.parseFromString(svgString, "image/svg+xml").documentElement;
svgString,
"image/svg+xml",
).documentElement;
cancelButton.appendChild(svgElement); cancelButton.appendChild(svgElement);
placeholder.appendChild(cancelButton); placeholder.appendChild(cancelButton);
@ -54,13 +48,7 @@ const UploadImagesPlugin = (cancelUploadImage?: () => any) =>
}); });
set = set.add(tr.doc, [deco]); set = set.add(tr.doc, [deco]);
} else if (action && action.remove) { } else if (action && action.remove) {
set = set.remove( set = set.remove(set.find(undefined, undefined, (spec) => spec.id == action.remove.id));
set.find(
undefined,
undefined,
(spec) => spec.id == action.remove.id,
),
);
} }
return set; return set;
}, },
@ -76,11 +64,7 @@ export default UploadImagesPlugin;
function findPlaceholder(state: EditorState, id: {}) { function findPlaceholder(state: EditorState, id: {}) {
const decos = uploadKey.getState(state); const decos = uploadKey.getState(state);
const found = decos.find( const found = decos.find(undefined, undefined, (spec: { id: number | undefined }) => spec.id == id);
undefined,
undefined,
(spec: { id: number | undefined }) => spec.id == id,
);
return found.length ? found[0].from : null; return found.length ? found[0].from : null;
} }
@ -96,9 +80,7 @@ export async function startImageUpload(
view: EditorView, view: EditorView,
pos: number, pos: number,
uploadFile: UploadImage, uploadFile: UploadImage,
setIsSubmitting?: ( setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void
isSubmitting: "submitting" | "submitted" | "saved",
) => void,
) { ) {
if (!file) { if (!file) {
alert("No file selected. Please select a file to upload."); alert("No file selected. Please select a file to upload.");
@ -151,9 +133,7 @@ export async function startImageUpload(
const imageSrc = typeof src === "object" ? reader.result : src; const imageSrc = typeof src === "object" ? reader.result : src;
const node = schema.nodes.image.create({ src: imageSrc }); const node = schema.nodes.image.create({ src: imageSrc });
const transaction = view.state.tr const transaction = view.state.tr.replaceWith(pos, pos, node).setMeta(uploadKey, { remove: { id } });
.replaceWith(pos, pos, node)
.setMeta(uploadKey, { remove: { id } });
view.dispatch(transaction); view.dispatch(transaction);
} catch (error) { } catch (error) {
console.error("Upload error: ", error); console.error("Upload error: ", error);
@ -161,10 +141,7 @@ export async function startImageUpload(
} }
} }
const UploadImageHandler = ( const UploadImageHandler = (file: File, uploadFile: UploadImage): Promise<string> => {
file: File,
uploadFile: UploadImage,
): Promise<string> => {
try { try {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
try { try {

View File

@ -5,9 +5,7 @@ import { startImageUpload } from "./plugins/upload-image";
export function CoreEditorProps( export function CoreEditorProps(
uploadFile: UploadImage, uploadFile: UploadImage,
setIsSubmitting?: ( setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void
isSubmitting: "submitting" | "submitted" | "saved",
) => void,
): EditorProps { ): EditorProps {
return { return {
attributes: { attributes: {
@ -34,11 +32,7 @@ export function CoreEditorProps(
} }
} }
} }
if ( if (event.clipboardData && event.clipboardData.files && event.clipboardData.files[0]) {
event.clipboardData &&
event.clipboardData.files &&
event.clipboardData.files[0]
) {
event.preventDefault(); event.preventDefault();
const file = event.clipboardData.files[0]; const file = event.clipboardData.files[0];
const pos = view.state.selection.from; const pos = view.state.selection.from;
@ -57,12 +51,7 @@ export function CoreEditorProps(
} }
} }
} }
if ( if (!moved && event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files[0]) {
!moved &&
event.dataTransfer &&
event.dataTransfer.files &&
event.dataTransfer.files[0]
) {
event.preventDefault(); event.preventDefault();
const file = event.dataTransfer.files[0]; const file = event.dataTransfer.files[0];
const coordinates = view.posAtCoords({ const coordinates = view.posAtCoords({
@ -70,13 +59,7 @@ export function CoreEditorProps(
top: event.clientY, top: event.clientY,
}); });
if (coordinates) { if (coordinates) {
startImageUpload( startImageUpload(file, view, coordinates.pos - 1, uploadFile, setIsSubmitting);
file,
view,
coordinates.pos - 1,
uploadFile,
setIsSubmitting,
);
} }
return true; return true;
} }

View File

@ -45,8 +45,7 @@ export const CoreReadOnlyEditorExtensions = (mentionConfig: {
}, },
code: { code: {
HTMLAttributes: { HTMLAttributes: {
class: class: "rounded-md bg-custom-primary-30 mx-1 px-1 py-1 font-mono font-medium text-custom-text-1000",
"rounded-md bg-custom-primary-30 mx-1 px-1 py-1 font-mono font-medium text-custom-text-1000",
spellcheck: "false", spellcheck: "false",
}, },
}, },
@ -94,9 +93,5 @@ export const CoreReadOnlyEditorExtensions = (mentionConfig: {
TableHeader, TableHeader,
TableCell, TableCell,
TableRow, TableRow,
Mentions( Mentions(mentionConfig.mentionSuggestions, mentionConfig.mentionHighlights, true),
mentionConfig.mentionSuggestions,
mentionConfig.mentionHighlights,
true,
),
]; ];

View File

@ -0,0 +1,4 @@
module.exports = {
root: true,
extends: ["custom"],
};

View File

@ -0,0 +1,6 @@
.next
.vercel
.tubro
out/
dis/
build/

View File

@ -0,0 +1,5 @@
{
"printWidth": 120,
"tabWidth": 2,
"trailingComma": "es5"
}

View File

@ -36,9 +36,6 @@
"@tiptap/extension-placeholder": "^2.1.11", "@tiptap/extension-placeholder": "^2.1.11",
"@tiptap/pm": "^2.1.12", "@tiptap/pm": "^2.1.12",
"@tiptap/suggestion": "^2.1.12", "@tiptap/suggestion": "^2.1.12",
"@types/node": "18.15.3",
"@types/react": "^18.2.39",
"@types/react-dom": "18.0.11",
"eslint": "8.36.0", "eslint": "8.36.0",
"eslint-config-next": "13.2.4", "eslint-config-next": "13.2.4",
"react-popper": "^2.3.0", "react-popper": "^2.3.0",

View File

@ -1,6 +1,3 @@
export { DocumentEditor, DocumentEditorWithRef } from "./ui"; export { DocumentEditor, DocumentEditorWithRef } from "./ui";
export { export { DocumentReadOnlyEditor, DocumentReadOnlyEditorWithRef } from "./ui/readonly";
DocumentReadOnlyEditor,
DocumentReadOnlyEditorWithRef,
} from "./ui/readonly";
export { FixedMenu } from "./ui/menu/fixed-menu"; export { FixedMenu } from "./ui/menu/fixed-menu";

View File

@ -12,7 +12,7 @@ export const AlertLabel = (props: IAlertLabelProps) => {
return ( return (
<div <div
className={`h-7 flex items-center gap-2 font-medium py-0.5 px-3 rounded-full text-xs ${backgroundColor} ${textColor}`} className={`flex h-7 items-center gap-2 rounded-full px-3 py-0.5 text-xs font-medium ${backgroundColor} ${textColor}`}
> >
{Icon && <Icon className="h-3 w-3" />} {Icon && <Icon className="h-3 w-3" />}
<span>{label}</span> <span>{label}</span>

View File

@ -1,8 +1,4 @@
import { import { HeadingComp, HeadingThreeComp, SubheadingComp } from "./heading-component";
HeadingComp,
HeadingThreeComp,
SubheadingComp,
} from "./heading-component";
import { IMarking } from ".."; import { IMarking } from "..";
import { Editor } from "@tiptap/react"; import { Editor } from "@tiptap/react";
import { scrollSummary } from "../utils/editor-summary-utils"; import { scrollSummary } from "../utils/editor-summary-utils";
@ -16,32 +12,21 @@ export const ContentBrowser = (props: ContentBrowserProps) => {
const { editor, markings } = props; const { editor, markings } = props;
return ( return (
<div className="flex flex-col h-full overflow-hidden"> <div className="flex h-full flex-col overflow-hidden">
<h2 className="font-medium">Table of Contents</h2> <h2 className="font-medium">Table of Contents</h2>
<div className="h-full overflow-y-auto"> <div className="h-full overflow-y-auto">
{markings.length !== 0 ? ( {markings.length !== 0 ? (
markings.map((marking) => markings.map((marking) =>
marking.level === 1 ? ( marking.level === 1 ? (
<HeadingComp <HeadingComp onClick={() => scrollSummary(editor, marking)} heading={marking.text} />
onClick={() => scrollSummary(editor, marking)}
heading={marking.text}
/>
) : marking.level === 2 ? ( ) : marking.level === 2 ? (
<SubheadingComp <SubheadingComp onClick={() => scrollSummary(editor, marking)} subHeading={marking.text} />
onClick={() => scrollSummary(editor, marking)}
subHeading={marking.text}
/>
) : ( ) : (
<HeadingThreeComp <HeadingThreeComp heading={marking.text} onClick={() => scrollSummary(editor, marking)} />
heading={marking.text} )
onClick={() => scrollSummary(editor, marking)}
/>
),
) )
) : ( ) : (
<p className="mt-3 text-xs text-custom-text-400"> <p className="mt-3 text-xs text-custom-text-400">Headings will be displayed here for navigation</p>
Headings will be displayed here for navigation
</p>
)} )}
</div> </div>
</div> </div>

View File

@ -5,10 +5,7 @@ import { FixedMenu } from "../menu";
import { UploadImage } from "@plane/editor-types"; import { UploadImage } from "@plane/editor-types";
import { DocumentDetails } from "../types/editor-types"; import { DocumentDetails } from "../types/editor-types";
import { AlertLabel } from "./alert-label"; import { AlertLabel } from "./alert-label";
import { import { IVerticalDropdownItemProps, VerticalDropdownMenu } from "./vertical-dropdown-menu";
IVerticalDropdownItemProps,
VerticalDropdownMenu,
} from "./vertical-dropdown-menu";
import { SummaryPopover } from "./summary-popover"; import { SummaryPopover } from "./summary-popover";
import { InfoPopover } from "./info-popover"; import { InfoPopover } from "./info-popover";
@ -23,9 +20,7 @@ interface IEditorHeader {
archivedAt?: Date; archivedAt?: Date;
readonly: boolean; readonly: boolean;
uploadFile?: UploadImage; uploadFile?: UploadImage;
setIsSubmitting?: ( setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void;
isSubmitting: "submitting" | "submitted" | "saved",
) => void;
documentDetails: DocumentDetails; documentDetails: DocumentDetails;
isSubmitting?: "submitting" | "submitted" | "saved"; isSubmitting?: "submitting" | "submitted" | "saved";
} }
@ -48,8 +43,8 @@ export const EditorHeader = (props: IEditorHeader) => {
} = props; } = props;
return ( return (
<div className="flex items-center border-b border-custom-border-200 py-2 px-5"> <div className="flex items-center border-b border-custom-border-200 px-5 py-2">
<div className="flex-shrink-0 w-56 lg:w-72"> <div className="w-56 flex-shrink-0 lg:w-72">
<SummaryPopover <SummaryPopover
editor={editor} editor={editor}
markings={markings} markings={markings}
@ -60,15 +55,11 @@ export const EditorHeader = (props: IEditorHeader) => {
<div className="flex-shrink-0"> <div className="flex-shrink-0">
{!readonly && uploadFile && ( {!readonly && uploadFile && (
<FixedMenu <FixedMenu editor={editor} uploadFile={uploadFile} setIsSubmitting={setIsSubmitting} />
editor={editor}
uploadFile={uploadFile}
setIsSubmitting={setIsSubmitting}
/>
)} )}
</div> </div>
<div className="flex-grow flex items-center justify-end gap-3"> <div className="flex flex-grow items-center justify-end gap-3">
{isLocked && ( {isLocked && (
<AlertLabel <AlertLabel
Icon={Lock} Icon={Lock}
@ -88,7 +79,7 @@ export const EditorHeader = (props: IEditorHeader) => {
{!isLocked && !isArchived ? ( {!isLocked && !isArchived ? (
<div <div
className={`flex absolute right-[120px] transition-all duration-300 items-center gap-x-2 ${ className={`absolute right-[120px] flex items-center gap-x-2 transition-all duration-300 ${
isSubmitting === "saved" ? "fadeOut" : "fadeIn" isSubmitting === "saved" ? "fadeOut" : "fadeIn"
}`} }`}
> >

View File

@ -23,7 +23,7 @@ export const SubheadingComp = ({
}) => ( }) => (
<p <p
onClick={onClick} onClick={onClick}
className="ml-6 mt-2 text-xs cursor-pointer font-medium tracking-tight text-gray-400 hover:text-custom-primary" className="ml-6 mt-2 cursor-pointer text-xs font-medium tracking-tight text-gray-400 hover:text-custom-primary"
role="button" role="button"
> >
{subHeading} {subHeading}
@ -39,7 +39,7 @@ export const HeadingThreeComp = ({
}) => ( }) => (
<p <p
onClick={onClick} onClick={onClick}
className="ml-8 mt-2 text-xs cursor-pointer font-medium tracking-tight text-gray-400 hover:text-custom-primary" className="ml-8 mt-2 cursor-pointer text-xs font-medium tracking-tight text-gray-400 hover:text-custom-primary"
role="button" role="button"
> >
{heading} {heading}

View File

@ -19,10 +19,7 @@ const renderDate = (date: Date): string => {
hour12: true, hour12: true,
}; };
const formattedDate: string = new Intl.DateTimeFormat( const formattedDate: string = new Intl.DateTimeFormat("en-US", options).format(date);
"en-US",
options,
).format(date);
return formattedDate; return formattedDate;
}; };
@ -32,42 +29,35 @@ export const InfoPopover: React.FC<Props> = (props) => {
const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false); const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false);
const [referenceElement, setReferenceElement] = const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
useState<HTMLButtonElement | null>(null); const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(
null,
);
const { styles: infoPopoverStyles, attributes: infoPopoverAttributes } = const { styles: infoPopoverStyles, attributes: infoPopoverAttributes } = usePopper(referenceElement, popperElement, {
usePopper(referenceElement, popperElement, { placement: "bottom-start",
placement: "bottom-start", });
});
return ( return (
<div <div onMouseEnter={() => setIsPopoverOpen(true)} onMouseLeave={() => setIsPopoverOpen(false)}>
onMouseEnter={() => setIsPopoverOpen(true)}
onMouseLeave={() => setIsPopoverOpen(false)}
>
<button type="button" ref={setReferenceElement} className="block"> <button type="button" ref={setReferenceElement} className="block">
<Info className="h-3.5 w-3.5" /> <Info className="h-3.5 w-3.5" />
</button> </button>
{isPopoverOpen && ( {isPopoverOpen && (
<div <div
className="z-10 w-64 shadow-custom-shadow-rg rounded border-[0.5px] border-custom-border-200 bg-custom-background-100 p-3 space-y-2.5" className="z-10 w-64 space-y-2.5 rounded border-[0.5px] border-custom-border-200 bg-custom-background-100 p-3 shadow-custom-shadow-rg"
ref={setPopperElement} ref={setPopperElement}
style={infoPopoverStyles.popper} style={infoPopoverStyles.popper}
{...infoPopoverAttributes.popper} {...infoPopoverAttributes.popper}
> >
<div className="space-y-1.5"> <div className="space-y-1.5">
<h6 className="text-custom-text-400 text-xs">Last updated on</h6> <h6 className="text-xs text-custom-text-400">Last updated on</h6>
<h5 className="text-sm flex items-center gap-1"> <h5 className="flex items-center gap-1 text-sm">
<History className="h-3 w-3" /> <History className="h-3 w-3" />
{renderDate(new Date(documentDetails.last_updated_at))} {renderDate(new Date(documentDetails.last_updated_at))}
</h5> </h5>
</div> </div>
<div className="space-y-1.5"> <div className="space-y-1.5">
<h6 className="text-custom-text-400 text-xs">Created on</h6> <h6 className="text-xs text-custom-text-400">Created on</h6>
<h5 className="text-sm flex items-center gap-1"> <h5 className="flex items-center gap-1 text-sm">
<Calendar className="h-3 w-3" /> <Calendar className="h-3 w-3" />
{renderDate(new Date(documentDetails.created_on))} {renderDate(new Date(documentDetails.created_on))}
</h5> </h5>

View File

@ -25,14 +25,7 @@ const debounce = (func: (...args: any[]) => void, wait: number) => {
}; };
export const PageRenderer = (props: IPageRenderer) => { export const PageRenderer = (props: IPageRenderer) => {
const { const { documentDetails, editor, editorClassNames, editorContentCustomClassNames, updatePageTitle, readonly } = props;
documentDetails,
editor,
editorClassNames,
editorContentCustomClassNames,
updatePageTitle,
readonly,
} = props;
const [pageTitle, setPagetitle] = useState(documentDetails.title); const [pageTitle, setPagetitle] = useState(documentDetails.title);
@ -44,27 +37,24 @@ export const PageRenderer = (props: IPageRenderer) => {
}; };
return ( return (
<div className="w-full pl-7 pt-5 pb-64"> <div className="w-full pb-64 pl-7 pt-5">
{!readonly ? ( {!readonly ? (
<input <input
onChange={(e) => handlePageTitleChange(e.target.value)} onChange={(e) => handlePageTitleChange(e.target.value)}
className="text-4xl bg-custom-background font-bold break-words pr-5 -mt-2 w-full border-none outline-none" className="-mt-2 w-full break-words border-none bg-custom-background pr-5 text-4xl font-bold outline-none"
value={pageTitle} value={pageTitle}
/> />
) : ( ) : (
<input <input
onChange={(e) => handlePageTitleChange(e.target.value)} onChange={(e) => handlePageTitleChange(e.target.value)}
className="text-4xl bg-custom-background font-bold break-words pr-5 -mt-2 w-full border-none outline-none overflow-x-clip" className="-mt-2 w-full overflow-x-clip break-words border-none bg-custom-background pr-5 text-4xl font-bold outline-none"
value={pageTitle} value={pageTitle}
disabled disabled
/> />
)} )}
<div className="flex flex-col h-full w-full pr-5"> <div className="flex h-full w-full flex-col pr-5">
<EditorContainer editor={editor} editorClassNames={editorClassNames}> <EditorContainer editor={editor} editorClassNames={editorClassNames}>
<EditorContentWrapper <EditorContentWrapper editor={editor} editorContentCustomClassNames={editorContentCustomClassNames} />
editor={editor}
editorContentCustomClassNames={editorContentCustomClassNames}
/>
</EditorContainer> </EditorContainer>
</div> </div>
</div> </div>

View File

@ -17,26 +17,24 @@ type Props = {
export const SummaryPopover: React.FC<Props> = (props) => { export const SummaryPopover: React.FC<Props> = (props) => {
const { editor, markings, sidePeekVisible, setSidePeekVisible } = props; const { editor, markings, sidePeekVisible, setSidePeekVisible } = props;
const [referenceElement, setReferenceElement] = const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
useState<HTMLButtonElement | null>(null); const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(
null,
);
const { styles: summaryPopoverStyles, attributes: summaryPopoverAttributes } = const { styles: summaryPopoverStyles, attributes: summaryPopoverAttributes } = usePopper(
usePopper(referenceElement, popperElement, { referenceElement,
popperElement,
{
placement: "bottom-start", placement: "bottom-start",
}); }
);
return ( return (
<div className="group/summary-popover w-min whitespace-nowrap"> <div className="group/summary-popover w-min whitespace-nowrap">
<button <button
type="button" type="button"
ref={setReferenceElement} ref={setReferenceElement}
className={`h-7 w-7 grid place-items-center rounded ${ className={`grid h-7 w-7 place-items-center rounded ${
sidePeekVisible sidePeekVisible ? "bg-custom-primary-100/20 text-custom-primary-100" : "text-custom-text-300"
? "bg-custom-primary-100/20 text-custom-primary-100"
: "text-custom-text-300"
}`} }`}
onClick={() => setSidePeekVisible(!sidePeekVisible)} onClick={() => setSidePeekVisible(!sidePeekVisible)}
> >
@ -44,7 +42,7 @@ export const SummaryPopover: React.FC<Props> = (props) => {
</button> </button>
{!sidePeekVisible && ( {!sidePeekVisible && (
<div <div
className="hidden group-hover/summary-popover:block z-10 max-h-80 w-64 shadow-custom-shadow-rg rounded border-[0.5px] border-custom-border-200 bg-custom-background-100 p-3 overflow-y-auto" className="z-10 hidden max-h-80 w-64 overflow-y-auto rounded border-[0.5px] border-custom-border-200 bg-custom-background-100 p-3 shadow-custom-shadow-rg group-hover/summary-popover:block"
ref={setPopperElement} ref={setPopperElement}
style={summaryPopoverStyles.popper} style={summaryPopoverStyles.popper}
{...summaryPopoverAttributes.popper} {...summaryPopoverAttributes.popper}

View File

@ -8,14 +8,10 @@ interface ISummarySideBarProps {
sidePeekVisible: boolean; sidePeekVisible: boolean;
} }
export const SummarySideBar = ({ export const SummarySideBar = ({ editor, markings, sidePeekVisible }: ISummarySideBarProps) => {
editor,
markings,
sidePeekVisible,
}: ISummarySideBarProps) => {
return ( return (
<div <div
className={`h-full p-5 transition-all duration-200 transform overflow-hidden ${ className={`h-full transform overflow-hidden p-5 transition-all duration-200 ${
sidePeekVisible ? "translate-x-0" : "-translate-x-full" sidePeekVisible ? "translate-x-0" : "-translate-x-full"
}`} }`}
> >

View File

@ -23,11 +23,7 @@ export interface IVerticalDropdownMenuProps {
items: IVerticalDropdownItemProps[]; items: IVerticalDropdownItemProps[];
} }
const VerticalDropdownItem = ({ const VerticalDropdownItem = ({ Icon, label, action }: IVerticalDropdownItemProps) => {
Icon,
label,
action,
}: IVerticalDropdownItemProps) => {
return ( return (
<CustomMenu.MenuItem onClick={action} className="flex items-center gap-2"> <CustomMenu.MenuItem onClick={action} className="flex items-center gap-2">
<Icon className="h-3 w-3" /> <Icon className="h-3 w-3" />
@ -42,19 +38,11 @@ export const VerticalDropdownMenu = ({ items }: IVerticalDropdownMenuProps) => {
maxHeight={"md"} maxHeight={"md"}
className={"h-4.5 mt-1"} className={"h-4.5 mt-1"}
placement={"bottom-start"} placement={"bottom-start"}
optionsClassName={ optionsClassName={"border-custom-border border-r border-solid transition-all duration-200 ease-in-out "}
"border-custom-border border-r border-solid transition-all duration-200 ease-in-out "
}
customButton={<MoreVertical size={14} />} customButton={<MoreVertical size={14} />}
> >
{items.map((item, index) => ( {items.map((item, index) => (
<VerticalDropdownItem <VerticalDropdownItem key={index} type={item.type} Icon={item.Icon} label={item.label} action={item.action} />
key={index}
type={item.type}
Icon={item.Icon}
label={item.label}
action={item.action}
/>
))} ))}
</CustomMenu> </CustomMenu>
); );

View File

@ -11,23 +11,22 @@ import { LayersIcon } from "@plane/ui";
export const DocumentEditorExtensions = ( export const DocumentEditorExtensions = (
uploadFile: UploadImage, uploadFile: UploadImage,
issueEmbedConfig?: IIssueEmbedConfig, issueEmbedConfig?: IIssueEmbedConfig,
setIsSubmitting?: ( setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void
isSubmitting: "submitting" | "submitted" | "saved",
) => void,
) => { ) => {
const additonalOptions: ISlashCommandItem[] = [ const additionalOptions: ISlashCommandItem[] = [
{ {
title: "Issue Embed", key: "issue_embed",
description: "Embed an issue from the project", title: "Issue embed",
searchTerms: ["Issue", "Iss"], description: "Embed an issue from the project.",
icon: <LayersIcon height={"20px"} width={"20px"} />, searchTerms: ["issue", "link", "embed"],
icon: <LayersIcon className="h-3.5 w-3.5" />,
command: ({ editor, range }) => { command: ({ editor, range }) => {
editor editor
.chain() .chain()
.focus() .focus()
.insertContentAt( .insertContentAt(
range, range,
"<p class='text-sm bg-gray-300 w-fit pl-3 pr-3 pt-1 pb-1 rounded shadow-sm'>#issue_</p>", "<p class='text-sm bg-gray-300 w-fit pl-3 pr-3 pt-1 pb-1 rounded shadow-sm'>#issue_</p>"
) )
.run(); .run();
}, },
@ -35,7 +34,7 @@ export const DocumentEditorExtensions = (
]; ];
return [ return [
SlashCommand(uploadFile, setIsSubmitting, additonalOptions), SlashCommand(uploadFile, setIsSubmitting, additionalOptions),
DragAndDrop, DragAndDrop,
Placeholder.configure({ Placeholder.configure({
placeholder: ({ node }) => { placeholder: ({ node }) => {

View File

@ -18,34 +18,32 @@ export interface IIssueListSuggestion {
} }
export const IssueSuggestions = (suggestions: any[]) => { export const IssueSuggestions = (suggestions: any[]) => {
const mappedSuggestions: IIssueListSuggestion[] = suggestions.map( const mappedSuggestions: IIssueListSuggestion[] = suggestions.map((suggestion): IIssueListSuggestion => {
(suggestion): IIssueListSuggestion => { let transactionId = uuidv4();
let transactionId = uuidv4(); return {
return { title: suggestion.name,
title: suggestion.name, priority: suggestion.priority.toString(),
priority: suggestion.priority.toString(), identifier: `${suggestion.project_detail.identifier}-${suggestion.sequence_id}`,
identifier: `${suggestion.project_detail.identifier}-${suggestion.sequence_id}`, state: suggestion.state_detail.name,
state: suggestion.state_detail.name, command: ({ editor, range }) => {
command: ({ editor, range }) => { editor
editor .chain()
.chain() .focus()
.focus() .insertContentAt(range, {
.insertContentAt(range, { type: "issue-embed-component",
type: "issue-embed-component", attrs: {
attrs: { entity_identifier: suggestion.id,
entity_identifier: suggestion.id, id: transactionId,
id: transactionId, title: suggestion.name,
title: suggestion.name, project_identifier: suggestion.project_detail.identifier,
project_identifier: suggestion.project_detail.identifier, sequence_id: suggestion.sequence_id,
sequence_id: suggestion.sequence_id, entity_name: "issue",
entity_name: "issue", },
}, })
}) .run();
.run(); },
}, };
}; });
},
);
return IssueEmbedSuggestions.configure({ return IssueEmbedSuggestions.configure({
suggestion: { suggestion: {

View File

@ -9,15 +9,7 @@ export const IssueEmbedSuggestions = Extension.create({
addOptions() { addOptions() {
return { return {
suggestion: { suggestion: {
command: ({ command: ({ editor, range, props }: { editor: Editor; range: Range; props: any }) => {
editor,
range,
props,
}: {
editor: Editor;
range: Range;
props: any;
}) => {
props.command({ editor, range }); props.command({ editor, range });
}, },
}, },

View File

@ -1,8 +1,6 @@
import { IIssueListSuggestion } from "."; import { IIssueListSuggestion } from ".";
export const getIssueSuggestionItems = ( export const getIssueSuggestionItems = (issueSuggestions: Array<IIssueListSuggestion>) => {
issueSuggestions: Array<IIssueListSuggestion>,
) => {
return ({ query }: { query: string }) => { return ({ query }: { query: string }) => {
const search = query.toLowerCase(); const search = query.toLowerCase();
const filteredSuggestions = issueSuggestions.filter((item) => { const filteredSuggestions = issueSuggestions.filter((item) => {

View File

@ -2,13 +2,7 @@ import { cn } from "@plane/editor-core";
import { Editor } from "@tiptap/core"; import { Editor } from "@tiptap/core";
import tippy from "tippy.js"; import tippy from "tippy.js";
import { ReactRenderer } from "@tiptap/react"; import { ReactRenderer } from "@tiptap/react";
import { import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
useCallback,
useEffect,
useLayoutEffect,
useRef,
useState,
} from "react";
import { PriorityIcon } from "@plane/ui"; import { PriorityIcon } from "@plane/ui";
const updateScrollView = (container: HTMLElement, item: HTMLElement) => { const updateScrollView = (container: HTMLElement, item: HTMLElement) => {
@ -62,9 +56,7 @@ const IssueSuggestionList = ({
let newDisplayedItems: { [key: string]: IssueSuggestionProps[] } = {}; let newDisplayedItems: { [key: string]: IssueSuggestionProps[] } = {};
let totalLength = 0; let totalLength = 0;
sections.forEach((section) => { sections.forEach((section) => {
newDisplayedItems[section] = items newDisplayedItems[section] = items.filter((item) => item.state === section).slice(0, 5);
.filter((item) => item.state === section)
.slice(0, 5);
totalLength += newDisplayedItems[section].length; totalLength += newDisplayedItems[section].length;
}); });
@ -79,7 +71,7 @@ const IssueSuggestionList = ({
command(item); command(item);
} }
}, },
[command, displayedItems, currentSection], [command, displayedItems, currentSection]
); );
useEffect(() => { useEffect(() => {
@ -93,22 +85,17 @@ const IssueSuggestionList = ({
// } // }
if (e.key === "ArrowUp") { if (e.key === "ArrowUp") {
setSelectedIndex( setSelectedIndex(
(selectedIndex + displayedItems[currentSection].length - 1) % (selectedIndex + displayedItems[currentSection].length - 1) % displayedItems[currentSection].length
displayedItems[currentSection].length,
); );
return true; return true;
} }
if (e.key === "ArrowDown") { if (e.key === "ArrowDown") {
const nextIndex = const nextIndex = (selectedIndex + 1) % displayedItems[currentSection].length;
(selectedIndex + 1) % displayedItems[currentSection].length;
setSelectedIndex(nextIndex); setSelectedIndex(nextIndex);
if (nextIndex === 4) { if (nextIndex === 4) {
const nextItems = items const nextItems = items
.filter((item) => item.state === currentSection) .filter((item) => item.state === currentSection)
.slice( .slice(displayedItems[currentSection].length, displayedItems[currentSection].length + 5);
displayedItems[currentSection].length,
displayedItems[currentSection].length + 5,
);
setDisplayedItems((prevItems) => ({ setDisplayedItems((prevItems) => ({
...prevItems, ...prevItems,
[currentSection]: [...prevItems[currentSection], ...nextItems], [currentSection]: [...prevItems[currentSection], ...nextItems],
@ -138,29 +125,17 @@ const IssueSuggestionList = ({
return () => { return () => {
document.removeEventListener("keydown", onKeyDown); document.removeEventListener("keydown", onKeyDown);
}; };
}, [ }, [displayedItems, selectedIndex, setSelectedIndex, selectItem, currentSection]);
displayedItems,
selectedIndex,
setSelectedIndex,
selectItem,
currentSection,
]);
useLayoutEffect(() => { useLayoutEffect(() => {
const container = commandListContainer?.current; const container = commandListContainer?.current;
if (container) { if (container) {
const sectionContainer = container?.querySelector( const sectionContainer = container?.querySelector(`#${currentSection}-container`) as HTMLDivElement;
`#${currentSection}-container`,
) as HTMLDivElement;
if (sectionContainer) { if (sectionContainer) {
updateScrollView(container, sectionContainer); updateScrollView(container, sectionContainer);
} }
const sectionScrollContainer = container?.querySelector( const sectionScrollContainer = container?.querySelector(`#${currentSection}`) as HTMLElement;
`#${currentSection}`, const item = sectionScrollContainer?.children[selectedIndex] as HTMLElement;
) as HTMLElement;
const item = sectionScrollContainer?.children[
selectedIndex
] as HTMLElement;
if (item && sectionScrollContainer) { if (item && sectionScrollContainer) {
updateScrollView(sectionScrollContainer, item); updateScrollView(sectionScrollContainer, item);
} }
@ -171,56 +146,41 @@ const IssueSuggestionList = ({
<div <div
id="issue-list-container" id="issue-list-container"
ref={commandListContainer} ref={commandListContainer}
className="z-[10] fixed max-h-80 w-60 overflow-y-auto overflow-x-hidden rounded-md border border-custom-border-100 bg-custom-background-100 px-1 shadow-custom-shadow-xs transition-all" className="fixed z-[10] max-h-80 w-60 overflow-y-auto overflow-x-hidden rounded-md border border-custom-border-100 bg-custom-background-100 px-1 shadow-custom-shadow-xs transition-all"
> >
{sections.map((section) => { {sections.map((section) => {
const sectionItems = displayedItems[section]; const sectionItems = displayedItems[section];
return ( return (
sectionItems && sectionItems &&
sectionItems.length > 0 && ( sectionItems.length > 0 && (
<div <div className={"flex h-full w-full flex-col"} key={`${section}-container`} id={`${section}-container`}>
className={"h-full w-full flex flex-col"}
key={`${section}-container`}
id={`${section}-container`}
>
<h6 <h6
className={ className={
"sticky top-0 z-[10] bg-custom-background-100 text-xs text-custom-text-400 font-medium px-2 py-1" "sticky top-0 z-[10] bg-custom-background-100 px-2 py-1 text-xs font-medium text-custom-text-400"
} }
> >
{section} {section}
</h6> </h6>
<div <div key={section} id={section} className={"max-h-[140px] overflow-x-hidden overflow-y-scroll"}>
key={section} {sectionItems.map((item: IssueSuggestionProps, index: number) => (
id={section} <button
className={"max-h-[140px] overflow-y-scroll overflow-x-hidden"} className={cn(
> `flex w-full items-center space-x-2 rounded-md px-2 py-1 text-left text-sm text-custom-text-200 hover:bg-custom-primary-100/5 hover:text-custom-text-100`,
{sectionItems.map( {
(item: IssueSuggestionProps, index: number) => ( "bg-custom-primary-100/5 text-custom-text-100":
<button section === currentSection && index === selectedIndex,
className={cn( }
`flex w-full items-center space-x-2 rounded-md px-2 py-1 text-left text-sm text-custom-text-200 hover:bg-custom-primary-100/5 hover:text-custom-text-100`, )}
{ key={index}
"bg-custom-primary-100/5 text-custom-text-100": onClick={() => selectItem(index)}
section === currentSection && >
index === selectedIndex, <h5 className="whitespace-nowrap text-xs text-custom-text-300">{item.identifier}</h5>
}, <PriorityIcon priority={item.priority} />
)} <div>
key={index} <p className="flex-grow truncate text-xs">{item.title}</p>
onClick={() => selectItem(index)} </div>
> </button>
<h5 className="text-xs text-custom-text-300 whitespace-nowrap"> ))}
{item.identifier}
</h5>
<PriorityIcon priority={item.priority} />
<div>
<p className="flex-grow text-xs truncate">
{item.title}
</p>
</div>
</button>
),
)}
</div> </div>
</div> </div>
) )

View File

@ -5,8 +5,7 @@ interface IssueWidgetExtensionProps {
issueEmbedConfig?: IIssueEmbedConfig; issueEmbedConfig?: IIssueEmbedConfig;
} }
export const IssueWidgetExtension = ({ export const IssueWidgetExtension = ({ issueEmbedConfig }: IssueWidgetExtensionProps) =>
issueEmbedConfig, IssueWidget.configure({
}: IssueWidgetExtensionProps) => IssueWidget.configure({ issueEmbedConfig,
issueEmbedConfig, });
});

View File

@ -30,15 +30,13 @@ const IssueWidgetCard = (props) => {
{loading == 0 ? ( {loading == 0 ? (
<div <div
onClick={completeIssueEmbedAction} onClick={completeIssueEmbedAction}
className="cursor-pointer w-full space-y-2 border-[0.5px] border-custom-border-200 rounded-md p-3 shadow-custom-shadow-2xs" className="w-full cursor-pointer space-y-2 rounded-md border-[0.5px] border-custom-border-200 p-3 shadow-custom-shadow-2xs"
> >
<h5 className="text-xs text-custom-text-300"> <h5 className="text-xs text-custom-text-300">
{issueDetails.project_detail.identifier}-{issueDetails.sequence_id} {issueDetails.project_detail.identifier}-{issueDetails.sequence_id}
</h5> </h5>
<h4 className="break-words text-sm font-medium"> <h4 className="break-words text-sm font-medium">{issueDetails.name}</h4>
{issueDetails.name} <div className="flex flex-wrap items-center gap-x-3 gap-y-2">
</h4>
<div className="flex items-center flex-wrap gap-x-3 gap-y-2">
<div> <div>
<PriorityIcon priority={issueDetails.priority} /> <PriorityIcon priority={issueDetails.priority} />
</div> </div>
@ -46,18 +44,13 @@ const IssueWidgetCard = (props) => {
<AvatarGroup size="sm"> <AvatarGroup size="sm">
{issueDetails.assignee_details.map((assignee) => { {issueDetails.assignee_details.map((assignee) => {
return ( return (
<Avatar <Avatar key={assignee.id} name={assignee.display_name} src={assignee.avatar} className={"m-0"} />
key={assignee.id}
name={assignee.display_name}
src={assignee.avatar}
className={"m-0"}
/>
); );
})} })}
</AvatarGroup> </AvatarGroup>
</div> </div>
{issueDetails.target_date && ( {issueDetails.target_date && (
<div className="rounded flex px-2.5 py-1 items-center border-[0.5px] border-custom-border-300 gap-1 text-custom-text-100 text-xs h-5"> <div className="flex h-5 items-center gap-1 rounded border-[0.5px] border-custom-border-300 px-2.5 py-1 text-xs text-custom-text-100">
<Calendar className="h-3 w-3" strokeWidth={1.5} /> <Calendar className="h-3 w-3" strokeWidth={1.5} />
{new Date(issueDetails.target_date).toLocaleDateString()} {new Date(issueDetails.target_date).toLocaleDateString()}
</div> </div>
@ -65,17 +58,15 @@ const IssueWidgetCard = (props) => {
</div> </div>
</div> </div>
) : loading == -1 ? ( ) : loading == -1 ? (
<div className="flex gap-[8px] items-center pb-[10px] pt-[10px] pl-[13px] rounded border-[#D97706] border-2 bg-[#FFFBEB] text-[#D97706]"> <div className="flex items-center gap-[8px] rounded border-2 border-[#D97706] bg-[#FFFBEB] pb-[10px] pl-[13px] pt-[10px] text-[#D97706]">
<AlertTriangle color={"#D97706"} /> <AlertTriangle color={"#D97706"} />
{ {"This Issue embed is not found in any project. It can no longer be updated or accessed from here."}
"This Issue embed is not found in any project. It can no longer be updated or accessed from here."
}
</div> </div>
) : ( ) : (
<div className="w-full space-y-2 border-[0.5px] border-custom-border-200 rounded-md p-3 shadow-custom-shadow-2xs"> <div className="w-full space-y-2 rounded-md border-[0.5px] border-custom-border-200 p-3 shadow-custom-shadow-2xs">
<Loader className={"px-6"}> <Loader className={"px-6"}>
<Loader.Item height={"30px"} /> <Loader.Item height={"30px"} />
<div className={"space-y-2 mt-3"}> <div className={"mt-3 space-y-2"}>
<Loader.Item height={"20px"} width={"70%"} /> <Loader.Item height={"20px"} width={"70%"} />
<Loader.Item height={"20px"} width={"60%"} /> <Loader.Item height={"20px"} width={"60%"} />
</div> </div>

View File

@ -35,10 +35,7 @@ export const IssueWidget = Node.create({
addNodeView() { addNodeView() {
return ReactNodeViewRenderer((props: Object) => ( return ReactNodeViewRenderer((props: Object) => (
<IssueWidgetCard <IssueWidgetCard {...props} issueEmbedConfig={this.options.issueEmbedConfig} />
{...props}
issueEmbedConfig={this.options.issueEmbedConfig}
/>
)); ));
}, },

View File

@ -5,5 +5,5 @@ export interface IEmbedConfig {
export interface IIssueEmbedConfig { export interface IIssueEmbedConfig {
fetchIssue: (issueId: string) => Promise<any>; fetchIssue: (issueId: string) => Promise<any>;
clickAction: (issueId: string, issueTitle: string) => void; clickAction: (issueId: string, issueTitle: string) => void;
issues: Array<any>; issues: Array<any>;
} }

View File

@ -15,21 +15,14 @@ export const useEditorMarkings = () => {
nodes.forEach((node) => { nodes.forEach((node) => {
if ( if (
node.type === "heading" && node.type === "heading" &&
(node.attrs.level === 1 || (node.attrs.level === 1 || node.attrs.level === 2 || node.attrs.level === 3) &&
node.attrs.level === 2 ||
node.attrs.level === 3) &&
node.content node.content
) { ) {
tempMarkings.push({ tempMarkings.push({
type: "heading", type: "heading",
level: node.attrs.level, level: node.attrs.level,
text: node.content[0].text, text: node.content[0].text,
sequence: sequence: node.attrs.level === 1 ? ++h1Sequence : node.attrs.level === 2 ? ++h2Sequence : ++h3Sequence,
node.attrs.level === 1
? ++h1Sequence
: node.attrs.level === 2
? ++h2Sequence
: ++h3Sequence,
}); });
} }
}); });

View File

@ -2,11 +2,7 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { getEditorClassNames, useEditor } from "@plane/editor-core"; import { getEditorClassNames, useEditor } from "@plane/editor-core";
import { DocumentEditorExtensions } from "./extensions"; import { DocumentEditorExtensions } from "./extensions";
import { import { IDuplicationConfig, IPageArchiveConfig, IPageLockConfig } from "./types/menu-actions";
IDuplicationConfig,
IPageArchiveConfig,
IPageLockConfig,
} from "./types/menu-actions";
import { EditorHeader } from "./components/editor-header"; import { EditorHeader } from "./components/editor-header";
import { useEditorMarkings } from "./hooks/use-editor-markings"; import { useEditorMarkings } from "./hooks/use-editor-markings";
import { SummarySideBar } from "./components/summary-side-bar"; import { SummarySideBar } from "./components/summary-side-bar";
@ -41,9 +37,7 @@ interface IDocumentEditor {
customClassName?: string; customClassName?: string;
editorContentCustomClassNames?: string; editorContentCustomClassNames?: string;
onChange: (json: any, html: string) => void; onChange: (json: any, html: string) => void;
setIsSubmitting?: ( setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void;
isSubmitting: "submitting" | "submitted" | "saved",
) => void;
setShouldShowAlert?: (showAlert: boolean) => void; setShouldShowAlert?: (showAlert: boolean) => void;
forwardedRef?: any; forwardedRef?: any;
updatePageTitle: (title: string) => Promise<void>; updatePageTitle: (title: string) => Promise<void>;
@ -118,11 +112,7 @@ const DocumentEditor = ({
cancelUploadImage, cancelUploadImage,
rerenderOnPropsChange, rerenderOnPropsChange,
forwardedRef, forwardedRef,
extensions: DocumentEditorExtensions( extensions: DocumentEditorExtensions(uploadFile, embedConfig?.issueEmbedConfig, setIsSubmitting),
uploadFile,
embedConfig?.issueEmbedConfig,
setIsSubmitting,
),
}); });
if (!editor) { if (!editor) {
@ -147,7 +137,7 @@ const DocumentEditor = ({
if (!editor) return null; if (!editor) return null;
return ( return (
<div className="h-full w-full flex flex-col overflow-hidden"> <div className="flex h-full w-full flex-col overflow-hidden">
<EditorHeader <EditorHeader
readonly={false} readonly={false}
KanbanMenuOptions={KanbanMenuOptions} KanbanMenuOptions={KanbanMenuOptions}
@ -163,13 +153,9 @@ const DocumentEditor = ({
documentDetails={documentDetails} documentDetails={documentDetails}
isSubmitting={isSubmitting} isSubmitting={isSubmitting}
/> />
<div className="h-full w-full flex overflow-y-auto"> <div className="flex h-full w-full overflow-y-auto">
<div className="flex-shrink-0 h-full w-56 lg:w-72 sticky top-0"> <div className="sticky top-0 h-full w-56 flex-shrink-0 lg:w-72">
<SummarySideBar <SummarySideBar editor={editor} markings={markings} sidePeekVisible={sidePeekVisible} />
editor={editor}
markings={markings}
sidePeekVisible={sidePeekVisible}
/>
</div> </div>
<div className="h-full w-[calc(100%-14rem)] lg:w-[calc(100%-18rem-18rem)]"> <div className="h-full w-[calc(100%-14rem)] lg:w-[calc(100%-18rem-18rem)]">
<PageRenderer <PageRenderer
@ -181,15 +167,15 @@ const DocumentEditor = ({
updatePageTitle={updatePageTitle} updatePageTitle={updatePageTitle}
/> />
</div> </div>
<div className="hidden lg:block flex-shrink-0 w-56 lg:w-72" /> <div className="hidden w-56 flex-shrink-0 lg:block lg:w-72" />
</div> </div>
</div> </div>
); );
}; };
const DocumentEditorWithRef = React.forwardRef<EditorHandle, IDocumentEditor>( const DocumentEditorWithRef = React.forwardRef<EditorHandle, IDocumentEditor>((props, ref) => (
(props, ref) => <DocumentEditor {...props} forwardedRef={ref} />, <DocumentEditor {...props} forwardedRef={ref} />
); ));
DocumentEditorWithRef.displayName = "DocumentEditorWithRef"; DocumentEditorWithRef.displayName = "DocumentEditorWithRef";

View File

@ -1,6 +1,4 @@
import { Editor } from "@tiptap/react"; import { Editor } from "@tiptap/react";
import { BoldIcon } from "lucide-react";
import { import {
BoldItem, BoldItem,
BulletListItem, BulletListItem,
@ -18,22 +16,16 @@ import {
HeadingTwoItem, HeadingTwoItem,
HeadingThreeItem, HeadingThreeItem,
findTableAncestor, findTableAncestor,
EditorMenuItem,
} from "@plane/editor-core"; } from "@plane/editor-core";
import { UploadImage } from "@plane/editor-types"; import { UploadImage } from "@plane/editor-types";
export interface BubbleMenuItem { export type BubbleMenuItem = EditorMenuItem;
name: string;
isActive: () => boolean;
command: () => void;
icon: typeof BoldIcon;
}
type EditorBubbleMenuProps = { type EditorBubbleMenuProps = {
editor: Editor; editor: Editor;
uploadFile: UploadImage; uploadFile: UploadImage;
setIsSubmitting?: ( setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void;
isSubmitting: "submitting" | "submitted" | "saved",
) => void;
}; };
export const FixedMenu = (props: EditorBubbleMenuProps) => { export const FixedMenu = (props: EditorBubbleMenuProps) => {
@ -49,15 +41,9 @@ export const FixedMenu = (props: EditorBubbleMenuProps) => {
StrikeThroughItem(editor), StrikeThroughItem(editor),
]; ];
const listItems: BubbleMenuItem[] = [ const listItems: BubbleMenuItem[] = [BulletListItem(editor), NumberedListItem(editor)];
BulletListItem(editor),
NumberedListItem(editor),
];
const userActionItems: BubbleMenuItem[] = [ const userActionItems: BubbleMenuItem[] = [QuoteItem(editor), CodeItem(editor)];
QuoteItem(editor),
CodeItem(editor),
];
function getComplexItems(): BubbleMenuItem[] { function getComplexItems(): BubbleMenuItem[] {
const items: BubbleMenuItem[] = [TableItem(editor)]; const items: BubbleMenuItem[] = [TableItem(editor)];
@ -99,10 +85,10 @@ export const FixedMenu = (props: EditorBubbleMenuProps) => {
type="button" type="button"
onClick={item.command} onClick={item.command}
className={cn( className={cn(
"h-7 w-7 grid place-items-center text-custom-text-300 hover:bg-custom-background-80 rounded", "grid h-7 w-7 place-items-center rounded text-custom-text-300 hover:bg-custom-background-80",
{ {
"text-custom-text-100 bg-custom-background-80": item.isActive(), "bg-custom-background-80 text-custom-text-100": item.isActive(),
}, }
)} )}
> >
<item.icon className="h-4 w-4" /> <item.icon className="h-4 w-4" />
@ -116,10 +102,10 @@ export const FixedMenu = (props: EditorBubbleMenuProps) => {
type="button" type="button"
onClick={item.command} onClick={item.command}
className={cn( className={cn(
"h-7 w-7 grid place-items-center text-custom-text-300 hover:bg-custom-background-80 rounded", "grid h-7 w-7 place-items-center rounded text-custom-text-300 hover:bg-custom-background-80",
{ {
"text-custom-text-100 bg-custom-background-80": item.isActive(), "bg-custom-background-80 text-custom-text-100": item.isActive(),
}, }
)} )}
> >
<item.icon <item.icon
@ -137,10 +123,10 @@ export const FixedMenu = (props: EditorBubbleMenuProps) => {
type="button" type="button"
onClick={item.command} onClick={item.command}
className={cn( className={cn(
"h-7 w-7 grid place-items-center text-custom-text-300 hover:bg-custom-background-80 rounded", "grid h-7 w-7 place-items-center rounded text-custom-text-300 hover:bg-custom-background-80",
{ {
"text-custom-text-100 bg-custom-background-80": item.isActive(), "bg-custom-background-80 text-custom-text-100": item.isActive(),
}, }
)} )}
> >
<item.icon <item.icon
@ -158,10 +144,10 @@ export const FixedMenu = (props: EditorBubbleMenuProps) => {
type="button" type="button"
onClick={item.command} onClick={item.command}
className={cn( className={cn(
"h-7 w-7 grid place-items-center text-custom-text-300 hover:bg-custom-background-80 rounded", "grid h-7 w-7 place-items-center rounded text-custom-text-300 hover:bg-custom-background-80",
{ {
"text-custom-text-100 bg-custom-background-80": item.isActive(), "bg-custom-background-80 text-custom-text-100": item.isActive(),
}, }
)} )}
> >
<item.icon <item.icon

View File

@ -6,9 +6,5 @@ type Props = {
}; };
export const Icon: React.FC<Props> = ({ iconName, className = "" }) => ( export const Icon: React.FC<Props> = ({ iconName, className = "" }) => (
<span <span className={`material-symbols-rounded text-sm font-light leading-5 ${className}`}>{iconName}</span>
className={`material-symbols-rounded text-sm leading-5 font-light ${className}`}
>
{iconName}
</span>
); );

View File

@ -8,11 +8,7 @@ import { IssueWidgetExtension } from "../extensions/widgets/IssueEmbedWidget";
import { IEmbedConfig } from "../extensions/widgets/IssueEmbedWidget/types"; import { IEmbedConfig } from "../extensions/widgets/IssueEmbedWidget/types";
import { useEditorMarkings } from "../hooks/use-editor-markings"; import { useEditorMarkings } from "../hooks/use-editor-markings";
import { DocumentDetails } from "../types/editor-types"; import { DocumentDetails } from "../types/editor-types";
import { import { IPageArchiveConfig, IPageLockConfig, IDuplicationConfig } from "../types/menu-actions";
IPageArchiveConfig,
IPageLockConfig,
IDuplicationConfig,
} from "../types/menu-actions";
import { getMenuOptions } from "../utils/menu-options"; import { getMenuOptions } from "../utils/menu-options";
interface IDocumentReadOnlyEditor { interface IDocumentReadOnlyEditor {
@ -67,9 +63,7 @@ const DocumentReadOnlyEditor = ({
value, value,
forwardedRef, forwardedRef,
rerenderOnPropsChange, rerenderOnPropsChange,
extensions: [ extensions: [IssueWidgetExtension({ issueEmbedConfig: embedConfig?.issueEmbedConfig })],
IssueWidgetExtension({ issueEmbedConfig: embedConfig?.issueEmbedConfig }),
],
}); });
useEffect(() => { useEffect(() => {
@ -98,7 +92,7 @@ const DocumentReadOnlyEditor = ({
}); });
return ( return (
<div className="h-full w-full flex flex-col overflow-hidden"> <div className="flex h-full w-full flex-col overflow-hidden">
<EditorHeader <EditorHeader
isLocked={!pageLockConfig ? false : pageLockConfig.is_locked} isLocked={!pageLockConfig ? false : pageLockConfig.is_locked}
isArchived={!pageArchiveConfig ? false : pageArchiveConfig.is_archived} isArchived={!pageArchiveConfig ? false : pageArchiveConfig.is_archived}
@ -111,13 +105,9 @@ const DocumentReadOnlyEditor = ({
documentDetails={documentDetails} documentDetails={documentDetails}
archivedAt={pageArchiveConfig && pageArchiveConfig.archived_at} archivedAt={pageArchiveConfig && pageArchiveConfig.archived_at}
/> />
<div className="h-full w-full flex overflow-y-auto"> <div className="flex h-full w-full overflow-y-auto">
<div className="flex-shrink-0 h-full w-56 lg:w-80 sticky top-0"> <div className="sticky top-0 h-full w-56 flex-shrink-0 lg:w-80">
<SummarySideBar <SummarySideBar editor={editor} markings={markings} sidePeekVisible={sidePeekVisible} />
editor={editor}
markings={markings}
sidePeekVisible={sidePeekVisible}
/>
</div> </div>
<div className="h-full w-full"> <div className="h-full w-full">
<PageRenderer <PageRenderer
@ -128,16 +118,15 @@ const DocumentReadOnlyEditor = ({
documentDetails={documentDetails} documentDetails={documentDetails}
/> />
</div> </div>
<div className="hidden lg:block flex-shrink-0 w-56 lg:w-80" /> <div className="hidden w-56 flex-shrink-0 lg:block lg:w-80" />
</div> </div>
</div> </div>
); );
}; };
const DocumentReadOnlyEditorWithRef = forwardRef< const DocumentReadOnlyEditorWithRef = forwardRef<EditorHandle, IDocumentReadOnlyEditor>((props, ref) => (
EditorHandle, <DocumentReadOnlyEditor {...props} forwardedRef={ref} />
IDocumentReadOnlyEditor ));
>((props, ref) => <DocumentReadOnlyEditor {...props} forwardedRef={ref} />);
DocumentReadOnlyEditorWithRef.displayName = "DocumentReadOnlyEditorWithRef"; DocumentReadOnlyEditorWithRef.displayName = "DocumentReadOnlyEditorWithRef";

View File

@ -51,17 +51,11 @@ export const Tooltip: React.FC<Props> = ({
content={ content={
<div <div
className={`relative z-50 max-w-xs gap-1 rounded-md p-2 text-xs shadow-md ${ className={`relative z-50 max-w-xs gap-1 rounded-md p-2 text-xs shadow-md ${
theme === "custom" theme === "custom" ? "bg-custom-background-100 text-custom-text-200" : "bg-black text-gray-400"
? "bg-custom-background-100 text-custom-text-200" } overflow-hidden break-words ${className}`}
: "bg-black text-gray-400"
} break-words overflow-hidden ${className}`}
> >
{tooltipHeading && ( {tooltipHeading && (
<h5 <h5 className={`font-medium ${theme === "custom" ? "text-custom-text-100" : "text-white"}`}>
className={`font-medium ${
theme === "custom" ? "text-custom-text-100" : "text-white"
}`}
>
{tooltipHeading} {tooltipHeading}
</h5> </h5>
)} )}
@ -69,11 +63,7 @@ export const Tooltip: React.FC<Props> = ({
</div> </div>
} }
position={position} position={position}
renderTarget={({ renderTarget={({ isOpen: isTooltipOpen, ref: eleReference, ...tooltipProps }) =>
isOpen: isTooltipOpen,
ref: eleReference,
...tooltipProps
}) =>
React.cloneElement(children, { React.cloneElement(children, {
ref: eleReference, ref: eleReference,
...tooltipProps, ...tooltipProps,

View File

@ -12,11 +12,7 @@ import {
} from "lucide-react"; } from "lucide-react";
import { NextRouter } from "next/router"; import { NextRouter } from "next/router";
import { IVerticalDropdownItemProps } from "../components/vertical-dropdown-menu"; import { IVerticalDropdownItemProps } from "../components/vertical-dropdown-menu";
import { import { IDuplicationConfig, IPageArchiveConfig, IPageLockConfig } from "../types/menu-actions";
IDuplicationConfig,
IPageArchiveConfig,
IPageLockConfig,
} from "../types/menu-actions";
import { copyMarkdownToClipboard, CopyPageLink } from "./menu-actions"; import { copyMarkdownToClipboard, CopyPageLink } from "./menu-actions";
export interface MenuOptionsProps { export interface MenuOptionsProps {
@ -90,8 +86,7 @@ export const getMenuOptions = ({
.then(() => { .then(() => {
onActionCompleteHandler({ onActionCompleteHandler({
title: "Page Copied", title: "Page Copied",
message: message: "Page has been copied as 'Copy of' followed by page title",
"Page has been copied as 'Copy of' followed by page title",
type: "success", type: "success",
}); });
}) })

View File

@ -0,0 +1,4 @@
module.exports = {
root: true,
extends: ["custom"],
};

View File

@ -0,0 +1,6 @@
.next
.vercel
.tubro
out/
dis/
build/

View File

@ -0,0 +1,5 @@
{
"printWidth": 120,
"tabWidth": 2,
"trailingComma": "es5"
}

View File

@ -35,7 +35,7 @@
"@plane/editor-core": "*", "@plane/editor-core": "*",
"eslint": "8.36.0", "eslint": "8.36.0",
"eslint-config-next": "13.2.4", "eslint-config-next": "13.2.4",
"lucide-react": "^0.244.0", "lucide-react": "^0.293.0",
"tippy.js": "^6.3.7", "tippy.js": "^6.3.7",
"@tiptap/pm": "^2.1.7" "@tiptap/pm": "^2.1.7"
}, },

View File

@ -43,24 +43,22 @@ function absoluteRect(node: Element) {
} }
function nodeDOMAtCoords(coords: { x: number; y: number }) { function nodeDOMAtCoords(coords: { x: number; y: number }) {
return document return document.elementsFromPoint(coords.x, coords.y).find((elem: Element) => {
.elementsFromPoint(coords.x, coords.y) return (
.find((elem: Element) => { elem.parentElement?.matches?.(".ProseMirror") ||
return ( elem.matches(
elem.parentElement?.matches?.(".ProseMirror") || [
elem.matches( "li",
[ "p:not(:first-child)",
"li", "pre",
"p:not(:first-child)", "blockquote",
"pre", "h1, h2, h3",
"blockquote", "[data-type=horizontalRule]",
"h1, h2, h3", ".tableWrapper",
"[data-type=horizontalRule]", ].join(", ")
".tableWrapper", )
].join(", "), );
) });
);
});
} }
function nodePosAtDOM(node: Element, view: EditorView) { function nodePosAtDOM(node: Element, view: EditorView) {
@ -104,9 +102,7 @@ function DragHandle(options: DragHandleOptions) {
const nodePos = nodePosAtDOM(node, view); const nodePos = nodePosAtDOM(node, view);
if (nodePos === null || nodePos === undefined || nodePos < 0) return; if (nodePos === null || nodePos === undefined || nodePos < 0) return;
view.dispatch( view.dispatch(view.state.tr.setSelection(NodeSelection.create(view.state.doc, nodePos)));
view.state.tr.setSelection(NodeSelection.create(view.state.doc, nodePos)),
);
const slice = view.state.selection.content(); const slice = view.state.selection.content();
const { dom, text } = __serializeForClipboard(view, slice); const { dom, text } = __serializeForClipboard(view, slice);
@ -137,9 +133,7 @@ function DragHandle(options: DragHandleOptions) {
if (nodePos === null || nodePos === undefined || nodePos < 0) return; if (nodePos === null || nodePos === undefined || nodePos < 0) return;
view.dispatch( view.dispatch(view.state.tr.setSelection(NodeSelection.create(view.state.doc, nodePos)));
view.state.tr.setSelection(NodeSelection.create(view.state.doc, nodePos)),
);
} }
let dragHandleElement: HTMLElement | null = null; let dragHandleElement: HTMLElement | null = null;

View File

@ -1,28 +1,21 @@
import { import { useState, useEffect, useCallback, ReactNode, useRef, useLayoutEffect } from "react";
useState,
useEffect,
useCallback,
ReactNode,
useRef,
useLayoutEffect,
} from "react";
import { Editor, Range, Extension } from "@tiptap/core"; import { Editor, Range, Extension } from "@tiptap/core";
import Suggestion from "@tiptap/suggestion"; import Suggestion from "@tiptap/suggestion";
import { ReactRenderer } from "@tiptap/react"; import { ReactRenderer } from "@tiptap/react";
import tippy from "tippy.js"; import tippy from "tippy.js";
import type { UploadImage, ISlashCommandItem, CommandProps } from "@plane/editor-types"; import type { UploadImage, ISlashCommandItem, CommandProps } from "@plane/editor-types";
import { import {
CaseSensitive,
Code2,
Heading1, Heading1,
Heading2, Heading2,
Heading3, Heading3,
ImageIcon,
List, List,
ListOrdered, ListOrdered,
Text, ListTodo,
TextQuote,
Code,
MinusSquare, MinusSquare,
CheckSquare, Quote,
ImageIcon,
Table, Table,
} from "lucide-react"; } from "lucide-react";
import { import {
@ -39,6 +32,7 @@ import {
} from "@plane/editor-core"; } from "@plane/editor-core";
interface CommandItemProps { interface CommandItemProps {
key: string;
title: string; title: string;
description: string; description: string;
icon: ReactNode; icon: ReactNode;
@ -50,15 +44,7 @@ const Command = Extension.create({
return { return {
suggestion: { suggestion: {
char: "/", char: "/",
command: ({ command: ({ editor, range, props }: { editor: Editor; range: Range; props: any }) => {
editor,
range,
props,
}: {
editor: Editor;
range: Range;
props: any;
}) => {
props.command({ editor, range }); props.command({ editor, range });
}, },
}, },
@ -80,149 +66,152 @@ const Command = Extension.create({
const getSuggestionItems = const getSuggestionItems =
( (
uploadFile: UploadImage, uploadFile: UploadImage,
setIsSubmitting?: ( setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void,
isSubmitting: "submitting" | "submitted" | "saved", additionalOptions?: Array<ISlashCommandItem>
) => void,
additonalOptions?: Array<ISlashCommandItem>
) => ) =>
({ query }: { query: string }) => { ({ query }: { query: string }) => {
let slashCommands: ISlashCommandItem[] = [ let slashCommands: ISlashCommandItem[] = [
{ {
title: "Text", key: "text",
description: "Just start typing with plain text.", title: "Text",
searchTerms: ["p", "paragraph"], description: "Just start typing with plain text.",
icon: <Text size={18} />, searchTerms: ["p", "paragraph"],
command: ({ editor, range }: CommandProps) => { icon: <CaseSensitive className="h-3.5 w-3.5" />,
editor command: ({ editor, range }: CommandProps) => {
.chain() editor.chain().focus().deleteRange(range).toggleNode("paragraph", "paragraph").run();
.focus()
.deleteRange(range)
.toggleNode("paragraph", "paragraph")
.run();
},
}, },
{ },
title: "Heading 1", {
description: "Big section heading.", key: "heading_1",
searchTerms: ["title", "big", "large"], title: "Heading 1",
icon: <Heading1 size={18} />, description: "Big section heading.",
command: ({ editor, range }: CommandProps) => { searchTerms: ["title", "big", "large"],
toggleHeadingOne(editor, range); icon: <Heading1 className="h-3.5 w-3.5" />,
}, command: ({ editor, range }: CommandProps) => {
toggleHeadingOne(editor, range);
}, },
{ },
title: "Heading 2", {
description: "Medium section heading.", key: "heading_2",
searchTerms: ["subtitle", "medium"], title: "Heading 2",
icon: <Heading2 size={18} />, description: "Medium section heading.",
command: ({ editor, range }: CommandProps) => { searchTerms: ["subtitle", "medium"],
toggleHeadingTwo(editor, range); icon: <Heading2 className="h-3.5 w-3.5" />,
}, command: ({ editor, range }: CommandProps) => {
toggleHeadingTwo(editor, range);
}, },
{ },
title: "Heading 3", {
description: "Small section heading.", key: "heading_3",
searchTerms: ["subtitle", "small"], title: "Heading 3",
icon: <Heading3 size={18} />, description: "Small section heading.",
command: ({ editor, range }: CommandProps) => { searchTerms: ["subtitle", "small"],
toggleHeadingThree(editor, range); icon: <Heading3 className="h-3.5 w-3.5" />,
}, command: ({ editor, range }: CommandProps) => {
toggleHeadingThree(editor, range);
}, },
{ },
title: "To-do List", {
description: "Track tasks with a to-do list.", key: "todo_list",
searchTerms: ["todo", "task", "list", "check", "checkbox"], title: "To do",
icon: <CheckSquare size={18} />, description: "Track tasks with a to-do list.",
command: ({ editor, range }: CommandProps) => { searchTerms: ["todo", "task", "list", "check", "checkbox"],
toggleTaskList(editor, range); icon: <ListTodo className="h-3.5 w-3.5" />,
}, command: ({ editor, range }: CommandProps) => {
toggleTaskList(editor, range);
}, },
{ },
title: "Bullet List", {
description: "Create a simple bullet list.", key: "bullet_list",
searchTerms: ["unordered", "point"], title: "Bullet list",
icon: <List size={18} />, description: "Create a simple bullet list.",
command: ({ editor, range }: CommandProps) => { searchTerms: ["unordered", "point"],
toggleBulletList(editor, range); icon: <List className="h-3.5 w-3.5" />,
}, command: ({ editor, range }: CommandProps) => {
toggleBulletList(editor, range);
}, },
{ },
title: "Divider", {
description: "Visually divide blocks", key: "numbered_list",
searchTerms: ["line", "divider", "horizontal", "rule", "separate"], title: "Numbered list",
icon: <MinusSquare size={18} />, description: "Create a list with numbering.",
command: ({ editor, range }: CommandProps) => { searchTerms: ["ordered"],
// @ts-expect-error I have to move this to the core icon: <ListOrdered className="h-3.5 w-3.5" />,
editor.chain().focus().deleteRange(range).setHorizontalRule().run(); command: ({ editor, range }: CommandProps) => {
}, toggleOrderedList(editor, range);
}, },
{ },
title: "Table", {
description: "Create a Table", key: "table",
searchTerms: ["table", "cell", "db", "data", "tabular"], title: "Table",
icon: <Table size={18} />, description: "Create a table",
command: ({ editor, range }: CommandProps) => { searchTerms: ["table", "cell", "db", "data", "tabular"],
insertTableCommand(editor, range); icon: <Table className="h-3.5 w-3.5" />,
}, command: ({ editor, range }: CommandProps) => {
insertTableCommand(editor, range);
}, },
{ },
title: "Numbered List", {
description: "Create a list with numbering.", key: "quote_block",
searchTerms: ["ordered"], title: "Quote",
icon: <ListOrdered size={18} />, description: "Capture a quote.",
command: ({ editor, range }: CommandProps) => { searchTerms: ["blockquote"],
toggleOrderedList(editor, range); icon: <Quote className="h-3.5 w-3.5" />,
}, command: ({ editor, range }: CommandProps) => toggleBlockquote(editor, range),
},
{
key: "code_block",
title: "Code",
description: "Capture a code snippet.",
searchTerms: ["codeblock"],
icon: <Code2 className="h-3.5 w-3.5" />,
command: ({ editor, range }: CommandProps) =>
// @ts-expect-error I have to move this to the core
editor.chain().focus().deleteRange(range).toggleCodeBlock().run(),
},
{
key: "image",
title: "Image",
description: "Upload an image from your computer.",
searchTerms: ["photo", "picture", "media"],
icon: <ImageIcon className="h-3.5 w-3.5" />,
command: ({ editor, range }: CommandProps) => {
insertImageCommand(editor, uploadFile, setIsSubmitting, range);
}, },
{ },
title: "Quote", {
description: "Capture a quote.", key: "divider",
searchTerms: ["blockquote"], title: "Divider",
icon: <TextQuote size={18} />, description: "Visually divide blocks.",
command: ({ editor, range }: CommandProps) => searchTerms: ["line", "divider", "horizontal", "rule", "separate"],
toggleBlockquote(editor, range), icon: <MinusSquare className="h-3.5 w-3.5" />,
command: ({ editor, range }: CommandProps) => {
// @ts-expect-error I have to move this to the core
editor.chain().focus().deleteRange(range).setHorizontalRule().run();
}, },
{ },
title: "Code", ];
description: "Capture a code snippet.",
searchTerms: ["codeblock"],
icon: <Code size={18} />,
command: ({ editor, range }: CommandProps) =>
// @ts-expect-error I have to move this to the core
editor.chain().focus().deleteRange(range).toggleCodeBlock().run(),
},
{
title: "Image",
description: "Upload an image from your computer.",
searchTerms: ["photo", "picture", "media"],
icon: <ImageIcon size={18} />,
command: ({ editor, range }: CommandProps) => {
insertImageCommand(editor, uploadFile, setIsSubmitting, range);
},
},
]
if (additonalOptions) { if (additionalOptions) {
additonalOptions.map(item => { additionalOptions.map((item) => {
slashCommands.push(item) slashCommands.push(item);
}) });
}
slashCommands = slashCommands.filter((item) => {
if (typeof query === "string" && query.length > 0) {
const search = query.toLowerCase();
return (
item.title.toLowerCase().includes(search) ||
item.description.toLowerCase().includes(search) ||
(item.searchTerms && item.searchTerms.some((term: string) => term.includes(search)))
);
} }
return true;
});
slashCommands = slashCommands.filter((item) => { return slashCommands;
if (typeof query === "string" && query.length > 0) { };
const search = query.toLowerCase();
return (
item.title.toLowerCase().includes(search) ||
item.description.toLowerCase().includes(search) ||
(item.searchTerms &&
item.searchTerms.some((term: string) => term.includes(search)))
);
}
return true;
})
return slashCommands
};
export const updateScrollView = (container: HTMLElement, item: HTMLElement) => { export const updateScrollView = (container: HTMLElement, item: HTMLElement) => {
const containerHeight = container.offsetHeight; const containerHeight = container.offsetHeight;
@ -238,15 +227,7 @@ export const updateScrollView = (container: HTMLElement, item: HTMLElement) => {
} }
}; };
const CommandList = ({ const CommandList = ({ items, command }: { items: CommandItemProps[]; command: any; editor: any; range: any }) => {
items,
command,
}: {
items: CommandItemProps[];
command: any;
editor: any;
range: any;
}) => {
const [selectedIndex, setSelectedIndex] = useState(0); const [selectedIndex, setSelectedIndex] = useState(0);
const selectItem = useCallback( const selectItem = useCallback(
@ -256,7 +237,7 @@ const CommandList = ({
command(item); command(item);
} }
}, },
[command, items], [command, items]
); );
useEffect(() => { useEffect(() => {
@ -303,27 +284,21 @@ const CommandList = ({
<div <div
id="slash-command" id="slash-command"
ref={commandListContainer} ref={commandListContainer}
className="z-50 fixed h-auto max-h-[330px] w-72 overflow-y-auto rounded-md border border-custom-border-300 bg-custom-background-100 px-1 py-2 shadow-md transition-all" className="fixed z-50 h-auto max-h-[330px] w-52 overflow-y-auto rounded-md border border-custom-border-300 bg-custom-background-100 px-1 py-2 shadow-md transition-all"
> >
{items.map((item: CommandItemProps, index: number) => ( {items.map((item, index) => (
<button <button
key={item.key}
className={cn( className={cn(
`flex w-full items-center space-x-2 rounded-md px-2 py-1 text-left text-sm text-custom-text-200 hover:bg-custom-primary-100/5 hover:text-custom-text-100`, `flex w-full items-center gap-2 rounded-md px-2.5 py-1.5 text-sm text-custom-text-100 hover:bg-custom-primary-100/5`,
{ {
"bg-custom-primary-100/5 text-custom-text-100": "bg-custom-primary-100/5": index === selectedIndex,
index === selectedIndex, }
},
)} )}
key={index}
onClick={() => selectItem(index)} onClick={() => selectItem(index)}
> >
<div className="flex h-10 w-10 items-center justify-center rounded-md border border-custom-border-300 bg-custom-background-100"> <div className="grid flex-shrink-0 place-items-center">{item.icon}</div>
{item.icon} <p>{item.title}</p>
</div>
<div>
<p className="font-medium">{item.title}</p>
<p className="text-xs text-custom-text-300">{item.description}</p>
</div>
</button> </button>
))} ))}
</div> </div>
@ -380,14 +355,12 @@ const renderItems = () => {
export const SlashCommand = ( export const SlashCommand = (
uploadFile: UploadImage, uploadFile: UploadImage,
setIsSubmitting?: ( setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void,
isSubmitting: "submitting" | "submitted" | "saved", additionalOptions?: Array<ISlashCommandItem>
) => void,
additonalOptions?: Array<ISlashCommandItem>,
) => ) =>
Command.configure({ Command.configure({
suggestion: { suggestion: {
items: getSuggestionItems(uploadFile, setIsSubmitting, additonalOptions), items: getSuggestionItems(uploadFile, setIsSubmitting, additionalOptions),
render: renderItems, render: renderItems,
}, },
}); });

View File

@ -0,0 +1,4 @@
module.exports = {
root: true,
extends: ["custom"],
};

View File

@ -0,0 +1,6 @@
.next
.vercel
.tubro
out/
dis/
build/

View File

@ -0,0 +1,5 @@
{
"printWidth": 120,
"tabWidth": 2,
"trailingComma": "es5"
}

View File

@ -1,6 +1,3 @@
export { LiteTextEditor, LiteTextEditorWithRef } from "./ui"; export { LiteTextEditor, LiteTextEditorWithRef } from "./ui";
export { LiteReadOnlyEditor, LiteReadOnlyEditorWithRef } from "./ui/read-only"; export { LiteReadOnlyEditor, LiteReadOnlyEditorWithRef } from "./ui/read-only";
export type { export type { IMentionSuggestion, IMentionHighlight } from "@plane/editor-types";
IMentionSuggestion,
IMentionHighlight,
} from "@plane/editor-types";

View File

@ -1,18 +1,8 @@
import * as React from "react"; import * as React from "react";
import { import { EditorContainer, EditorContentWrapper, getEditorClassNames, useEditor } from "@plane/editor-core";
EditorContainer,
EditorContentWrapper,
getEditorClassNames,
useEditor,
} from "@plane/editor-core";
import { FixedMenu } from "./menus/fixed-menu"; import { FixedMenu } from "./menus/fixed-menu";
import { LiteTextEditorExtensions } from "./extensions"; import { LiteTextEditorExtensions } from "./extensions";
import { import { UploadImage, DeleteImage, IMentionSuggestion, RestoreImage } from "@plane/editor-types";
UploadImage,
DeleteImage,
IMentionSuggestion,
RestoreImage,
} from "@plane/editor-types";
interface ILiteTextEditor { interface ILiteTextEditor {
value: string; value: string;
@ -25,9 +15,7 @@ interface ILiteTextEditor {
customClassName?: string; customClassName?: string;
editorContentCustomClassNames?: string; editorContentCustomClassNames?: string;
onChange?: (json: any, html: string) => void; onChange?: (json: any, html: string) => void;
setIsSubmitting?: ( setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void;
isSubmitting: "submitting" | "submitted" | "saved",
) => void;
setShouldShowAlert?: (showAlert: boolean) => void; setShouldShowAlert?: (showAlert: boolean) => void;
forwardedRef?: any; forwardedRef?: any;
debouncedUpdatesEnabled?: boolean; debouncedUpdatesEnabled?: boolean;
@ -107,11 +95,8 @@ const LiteTextEditor = (props: LiteTextEditorProps) => {
return ( return (
<EditorContainer editor={editor} editorClassNames={editorClassNames}> <EditorContainer editor={editor} editorClassNames={editorClassNames}>
<div className="flex flex-col"> <div className="flex flex-col">
<EditorContentWrapper <EditorContentWrapper editor={editor} editorContentCustomClassNames={editorContentCustomClassNames} />
editor={editor} <div className="mt-4 w-full">
editorContentCustomClassNames={editorContentCustomClassNames}
/>
<div className="w-full mt-4">
<FixedMenu <FixedMenu
editor={editor} editor={editor}
uploadFile={uploadFile} uploadFile={uploadFile}
@ -125,9 +110,9 @@ const LiteTextEditor = (props: LiteTextEditorProps) => {
); );
}; };
const LiteTextEditorWithRef = React.forwardRef<EditorHandle, ILiteTextEditor>( const LiteTextEditorWithRef = React.forwardRef<EditorHandle, ILiteTextEditor>((props, ref) => (
(props, ref) => <LiteTextEditor {...props} forwardedRef={ref} />, <LiteTextEditor {...props} forwardedRef={ref} />
); ));
LiteTextEditorWithRef.displayName = "LiteTextEditorWithRef"; LiteTextEditorWithRef.displayName = "LiteTextEditorWithRef";

View File

@ -6,9 +6,5 @@ type Props = {
}; };
export const Icon: React.FC<Props> = ({ iconName, className = "" }) => ( export const Icon: React.FC<Props> = ({ iconName, className = "" }) => (
<span <span className={`material-symbols-rounded text-sm font-light leading-5 ${className}`}>{iconName}</span>
className={`material-symbols-rounded text-sm leading-5 font-light ${className}`}
>
{iconName}
</span>
); );

View File

@ -47,9 +47,7 @@ type EditorBubbleMenuProps = {
| undefined; | undefined;
}; };
uploadFile: UploadImage; uploadFile: UploadImage;
setIsSubmitting?: ( setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void;
isSubmitting: "submitting" | "submitted" | "saved",
) => void;
submitButton: React.ReactNode; submitButton: React.ReactNode;
}; };
@ -61,23 +59,15 @@ export const FixedMenu = (props: EditorBubbleMenuProps) => {
StrikeThroughItem(props.editor), StrikeThroughItem(props.editor),
]; ];
const listFormattingItems: BubbleMenuItem[] = [ const listFormattingItems: BubbleMenuItem[] = [BulletListItem(props.editor), NumberedListItem(props.editor)];
BulletListItem(props.editor),
NumberedListItem(props.editor),
];
const userActionItems: BubbleMenuItem[] = [ const userActionItems: BubbleMenuItem[] = [QuoteItem(props.editor), CodeItem(props.editor)];
QuoteItem(props.editor),
CodeItem(props.editor),
];
function getComplexItems(): BubbleMenuItem[] { function getComplexItems(): BubbleMenuItem[] {
const items: BubbleMenuItem[] = [TableItem(props.editor)]; const items: BubbleMenuItem[] = [TableItem(props.editor)];
if (shouldShowImageItem()) { if (shouldShowImageItem()) {
items.push( items.push(ImageItem(props.editor, props.uploadFile, props.setIsSubmitting));
ImageItem(props.editor, props.uploadFile, props.setIsSubmitting),
);
} }
return items; return items;
@ -109,22 +99,20 @@ export const FixedMenu = (props: EditorBubbleMenuProps) => {
}; };
return ( return (
<div className="flex items-stretch gap-1.5 w-full h-9 overflow-x-scroll"> <div className="flex h-9 w-full items-stretch gap-1.5 overflow-x-scroll">
{props.commentAccessSpecifier && ( {props.commentAccessSpecifier && (
<div className="flex-shrink-0 flex items-stretch gap-0.5 border-[0.5px] border-custom-border-200 rounded p-1"> <div className="flex flex-shrink-0 items-stretch gap-0.5 rounded border-[0.5px] border-custom-border-200 p-1">
{props?.commentAccessSpecifier.commentAccess?.map((access) => ( {props?.commentAccessSpecifier.commentAccess?.map((access) => (
<Tooltip key={access.key} tooltipContent={access.label}> <Tooltip key={access.key} tooltipContent={access.label}>
<button <button
type="button" type="button"
onClick={() => handleAccessChange(access.key)} onClick={() => handleAccessChange(access.key)}
className={`aspect-square grid place-items-center p-1 rounded-sm hover:bg-custom-background-90 ${ className={`grid aspect-square place-items-center rounded-sm p-1 hover:bg-custom-background-90 ${
props.commentAccessSpecifier?.accessValue === access.key props.commentAccessSpecifier?.accessValue === access.key ? "bg-custom-background-90" : ""
? "bg-custom-background-90"
: ""
}`} }`}
> >
<access.icon <access.icon
className={`w-3.5 h-3.5 ${ className={`h-3.5 w-3.5 ${
props.commentAccessSpecifier?.accessValue === access.key props.commentAccessSpecifier?.accessValue === access.key
? "text-custom-text-100" ? "text-custom-text-100"
: "text-custom-text-400" : "text-custom-text-400"
@ -136,23 +124,19 @@ export const FixedMenu = (props: EditorBubbleMenuProps) => {
))} ))}
</div> </div>
)} )}
<div className="flex items-stretch justify-between gap-2 w-full border-[0.5px] border-custom-border-200 bg-custom-background-90 rounded p-1"> <div className="flex w-full items-stretch justify-between gap-2 rounded border-[0.5px] border-custom-border-200 bg-custom-background-90 p-1">
<div className="flex items-stretch"> <div className="flex items-stretch">
<div className="flex items-stretch gap-0.5 pr-2.5 border-r border-custom-border-200"> <div className="flex items-stretch gap-0.5 border-r border-custom-border-200 pr-2.5">
{basicTextFormattingItems.map((item, index) => ( {basicTextFormattingItems.map((item, index) => (
<Tooltip <Tooltip key={index} tooltipContent={<span className="capitalize">{item.name}</span>}>
key={index}
tooltipContent={<span className="capitalize">{item.name}</span>}
>
<button <button
type="button" type="button"
onClick={item.command} onClick={item.command}
className={cn( className={cn(
"p-1 aspect-square text-custom-text-400 hover:bg-custom-background-80 rounded-sm grid place-items-center", "grid aspect-square place-items-center rounded-sm p-1 text-custom-text-400 hover:bg-custom-background-80",
{ {
"text-custom-text-100 bg-custom-background-80": "bg-custom-background-80 text-custom-text-100": item.isActive(),
item.isActive(), }
},
)} )}
> >
<item.icon <item.icon
@ -165,21 +149,17 @@ export const FixedMenu = (props: EditorBubbleMenuProps) => {
</Tooltip> </Tooltip>
))} ))}
</div> </div>
<div className="flex items-stretch gap-0.5 px-2.5 border-r border-custom-border-200"> <div className="flex items-stretch gap-0.5 border-r border-custom-border-200 px-2.5">
{listFormattingItems.map((item, index) => ( {listFormattingItems.map((item, index) => (
<Tooltip <Tooltip key={index} tooltipContent={<span className="capitalize">{item.name}</span>}>
key={index}
tooltipContent={<span className="capitalize">{item.name}</span>}
>
<button <button
type="button" type="button"
onClick={item.command} onClick={item.command}
className={cn( className={cn(
"p-1 aspect-square text-custom-text-400 hover:bg-custom-background-80 rounded-sm grid place-items-center", "grid aspect-square place-items-center rounded-sm p-1 text-custom-text-400 hover:bg-custom-background-80",
{ {
"text-custom-text-100 bg-custom-background-80": "bg-custom-background-80 text-custom-text-100": item.isActive(),
item.isActive(), }
},
)} )}
> >
<item.icon <item.icon
@ -192,21 +172,17 @@ export const FixedMenu = (props: EditorBubbleMenuProps) => {
</Tooltip> </Tooltip>
))} ))}
</div> </div>
<div className="flex items-stretch gap-0.5 px-2.5 border-r border-custom-border-200"> <div className="flex items-stretch gap-0.5 border-r border-custom-border-200 px-2.5">
{userActionItems.map((item, index) => ( {userActionItems.map((item, index) => (
<Tooltip <Tooltip key={index} tooltipContent={<span className="capitalize">{item.name}</span>}>
key={index}
tooltipContent={<span className="capitalize">{item.name}</span>}
>
<button <button
type="button" type="button"
onClick={item.command} onClick={item.command}
className={cn( className={cn(
"p-1 aspect-square text-custom-text-400 hover:bg-custom-background-80 rounded-sm grid place-items-center", "grid aspect-square place-items-center rounded-sm p-1 text-custom-text-400 hover:bg-custom-background-80",
{ {
"text-custom-text-100 bg-custom-background-80": "bg-custom-background-80 text-custom-text-100": item.isActive(),
item.isActive(), }
},
)} )}
> >
<item.icon <item.icon
@ -221,19 +197,15 @@ export const FixedMenu = (props: EditorBubbleMenuProps) => {
</div> </div>
<div className="flex items-stretch gap-0.5 pl-2.5"> <div className="flex items-stretch gap-0.5 pl-2.5">
{complexItems.map((item, index) => ( {complexItems.map((item, index) => (
<Tooltip <Tooltip key={index} tooltipContent={<span className="capitalize">{item.name}</span>}>
key={index}
tooltipContent={<span className="capitalize">{item.name}</span>}
>
<button <button
type="button" type="button"
onClick={item.command} onClick={item.command}
className={cn( className={cn(
"p-1 aspect-square text-custom-text-400 hover:bg-custom-background-80 rounded-sm grid place-items-center", "grid aspect-square place-items-center rounded-sm p-1 text-custom-text-400 hover:bg-custom-background-80",
{ {
"text-custom-text-100 bg-custom-background-80": "bg-custom-background-80 text-custom-text-100": item.isActive(),
item.isActive(), }
},
)} )}
> >
<item.icon <item.icon

View File

@ -1,10 +1,5 @@
import * as React from "react"; import * as React from "react";
import { import { EditorContainer, EditorContentWrapper, getEditorClassNames, useReadOnlyEditor } from "@plane/editor-core";
EditorContainer,
EditorContentWrapper,
getEditorClassNames,
useReadOnlyEditor,
} from "@plane/editor-core";
interface ICoreReadOnlyEditor { interface ICoreReadOnlyEditor {
value: string; value: string;
@ -50,19 +45,15 @@ const LiteReadOnlyEditor = ({
return ( return (
<EditorContainer editor={editor} editorClassNames={editorClassNames}> <EditorContainer editor={editor} editorClassNames={editorClassNames}>
<div className="flex flex-col"> <div className="flex flex-col">
<EditorContentWrapper <EditorContentWrapper editor={editor} editorContentCustomClassNames={editorContentCustomClassNames} />
editor={editor}
editorContentCustomClassNames={editorContentCustomClassNames}
/>
</div> </div>
</EditorContainer> </EditorContainer>
); );
}; };
const LiteReadOnlyEditorWithRef = React.forwardRef< const LiteReadOnlyEditorWithRef = React.forwardRef<EditorHandle, ICoreReadOnlyEditor>((props, ref) => (
EditorHandle, <LiteReadOnlyEditor {...props} forwardedRef={ref} />
ICoreReadOnlyEditor ));
>((props, ref) => <LiteReadOnlyEditor {...props} forwardedRef={ref} />);
LiteReadOnlyEditorWithRef.displayName = "LiteReadOnlyEditorWithRef"; LiteReadOnlyEditorWithRef.displayName = "LiteReadOnlyEditorWithRef";

View File

@ -50,17 +50,11 @@ export const Tooltip: React.FC<Props> = ({
content={ content={
<div <div
className={`relative z-50 max-w-xs gap-1 rounded-md p-2 text-xs shadow-md ${ className={`relative z-50 max-w-xs gap-1 rounded-md p-2 text-xs shadow-md ${
theme === "custom" theme === "custom" ? "bg-custom-background-100 text-custom-text-200" : "bg-black text-gray-400"
? "bg-custom-background-100 text-custom-text-200" } overflow-hidden break-words ${className}`}
: "bg-black text-gray-400"
} break-words overflow-hidden ${className}`}
> >
{tooltipHeading && ( {tooltipHeading && (
<h5 <h5 className={`font-medium ${theme === "custom" ? "text-custom-text-100" : "text-white"}`}>
className={`font-medium ${
theme === "custom" ? "text-custom-text-100" : "text-white"
}`}
>
{tooltipHeading} {tooltipHeading}
</h5> </h5>
)} )}
@ -68,11 +62,7 @@ export const Tooltip: React.FC<Props> = ({
</div> </div>
} }
position={position} position={position}
renderTarget={({ renderTarget={({ isOpen: isTooltipOpen, ref: eleReference, ...tooltipProps }) =>
isOpen: isTooltipOpen,
ref: eleReference,
...tooltipProps
}) =>
React.cloneElement(children, { React.cloneElement(children, {
ref: eleReference, ref: eleReference,
...tooltipProps, ...tooltipProps,

View File

@ -0,0 +1,4 @@
module.exports = {
root: true,
extends: ["custom"],
};

View File

@ -0,0 +1,6 @@
.next
.vercel
.tubro
out/
dis/
build/

View File

@ -0,0 +1,5 @@
{
"printWidth": 120,
"tabWidth": 2,
"trailingComma": "es5"
}

View File

@ -41,9 +41,7 @@ The `@plane/rich-text-editor` package extends from the `editor-core` package, in
debouncedUpdatesEnabled={true} debouncedUpdatesEnabled={true}
setShouldShowAlert={setShowAlert} setShouldShowAlert={setShowAlert}
setIsSubmitting={setIsSubmitting} setIsSubmitting={setIsSubmitting}
customClassName={ customClassName={isAllowed ? "min-h-[150px] shadow-sm" : "!p-0 !pt-2 text-custom-text-200"}
isAllowed ? "min-h-[150px] shadow-sm" : "!p-0 !pt-2 text-custom-text-200"
}
noBorder={!isAllowed} noBorder={!isAllowed}
onChange={(description: Object, description_html: string) => { onChange={(description: Object, description_html: string) => {
setShowAlert(true); setShowAlert(true);
@ -96,8 +94,5 @@ return (
Here is an example of how to use the `RichReadOnlyEditor` component Here is an example of how to use the `RichReadOnlyEditor` component
```tsx ```tsx
<RichReadOnlyEditor <RichReadOnlyEditor value={issueDetails.description_html} customClassName="p-3 min-h-[50px] shadow-sm" />
value={issueDetails.description_html}
customClassName="p-3 min-h-[50px] shadow-sm"
/>
``` ```

View File

@ -1,7 +1,4 @@
export { RichTextEditor, RichTextEditorWithRef } from "./ui"; export { RichTextEditor, RichTextEditorWithRef } from "./ui";
export { RichReadOnlyEditor, RichReadOnlyEditorWithRef } from "./ui/read-only"; export { RichReadOnlyEditor, RichReadOnlyEditorWithRef } from "./ui/read-only";
export type { RichTextEditorProps, IRichTextEditor } from "./ui"; export type { RichTextEditorProps, IRichTextEditor } from "./ui";
export type { export type { IMentionHighlight, IMentionSuggestion } from "@plane/editor-types";
IMentionHighlight,
IMentionSuggestion,
} from "@plane/editor-types";

View File

@ -5,10 +5,8 @@ import { UploadImage } from "@plane/editor-types";
export const RichTextEditorExtensions = ( export const RichTextEditorExtensions = (
uploadFile: UploadImage, uploadFile: UploadImage,
setIsSubmitting?: ( setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void,
isSubmitting: "submitting" | "submitted" | "saved", dragDropEnabled?: boolean
) => void,
dragDropEnabled?: boolean,
) => [ ) => [
SlashCommand(uploadFile, setIsSubmitting), SlashCommand(uploadFile, setIsSubmitting),
dragDropEnabled === true && DragAndDrop, dragDropEnabled === true && DragAndDrop,

View File

@ -1,19 +1,9 @@
"use client"; "use client";
import * as React from "react"; import * as React from "react";
import { import { EditorContainer, EditorContentWrapper, getEditorClassNames, useEditor } from "@plane/editor-core";
EditorContainer,
EditorContentWrapper,
getEditorClassNames,
useEditor,
} from "@plane/editor-core";
import { EditorBubbleMenu } from "./menus/bubble-menu"; import { EditorBubbleMenu } from "./menus/bubble-menu";
import { RichTextEditorExtensions } from "./extensions"; import { RichTextEditorExtensions } from "./extensions";
import { import { DeleteImage, IMentionSuggestion, RestoreImage, UploadImage } from "@plane/editor-types";
DeleteImage,
IMentionSuggestion,
RestoreImage,
UploadImage,
} from "@plane/editor-types";
export type IRichTextEditor = { export type IRichTextEditor = {
value: string; value: string;
@ -31,9 +21,7 @@ export type IRichTextEditor = {
customClassName?: string; customClassName?: string;
editorContentCustomClassNames?: string; editorContentCustomClassNames?: string;
onChange?: (json: any, html: string) => void; onChange?: (json: any, html: string) => void;
setIsSubmitting?: ( setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void;
isSubmitting: "submitting" | "submitted" | "saved",
) => void;
setShouldShowAlert?: (showAlert: boolean) => void; setShouldShowAlert?: (showAlert: boolean) => void;
forwardedRef?: any; forwardedRef?: any;
debouncedUpdatesEnabled?: boolean; debouncedUpdatesEnabled?: boolean;
@ -82,11 +70,7 @@ const RichTextEditor = ({
restoreFile, restoreFile,
forwardedRef, forwardedRef,
rerenderOnPropsChange, rerenderOnPropsChange,
extensions: RichTextEditorExtensions( extensions: RichTextEditorExtensions(uploadFile, setIsSubmitting, dragDropEnabled),
uploadFile,
setIsSubmitting,
dragDropEnabled,
),
mentionHighlights, mentionHighlights,
mentionSuggestions, mentionSuggestions,
}); });
@ -103,18 +87,15 @@ const RichTextEditor = ({
<EditorContainer editor={editor} editorClassNames={editorClassNames}> <EditorContainer editor={editor} editorClassNames={editorClassNames}>
{editor && <EditorBubbleMenu editor={editor} />} {editor && <EditorBubbleMenu editor={editor} />}
<div className="flex flex-col"> <div className="flex flex-col">
<EditorContentWrapper <EditorContentWrapper editor={editor} editorContentCustomClassNames={editorContentCustomClassNames} />
editor={editor}
editorContentCustomClassNames={editorContentCustomClassNames}
/>
</div> </div>
</EditorContainer> </EditorContainer>
); );
}; };
const RichTextEditorWithRef = React.forwardRef<EditorHandle, IRichTextEditor>( const RichTextEditorWithRef = React.forwardRef<EditorHandle, IRichTextEditor>((props, ref) => (
(props, ref) => <RichTextEditor {...props} forwardedRef={ref} />, <RichTextEditor {...props} forwardedRef={ref} />
); ));
RichTextEditorWithRef.displayName = "RichTextEditorWithRef"; RichTextEditorWithRef.displayName = "RichTextEditorWithRef";

View File

@ -123,11 +123,10 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
type="button" type="button"
onClick={item.command} onClick={item.command}
className={cn( className={cn(
"p-2 text-custom-text-300 hover:bg-custom-primary-100/5 active:bg-custom-primary-100/5 transition-colors", "p-2 text-custom-text-300 transition-colors hover:bg-custom-primary-100/5 active:bg-custom-primary-100/5",
{ {
"text-custom-text-100 bg-custom-primary-100/5": "bg-custom-primary-100/5 text-custom-text-100": item.isActive(),
item.isActive(), }
},
)} )}
> >
<item.icon <item.icon

View File

@ -1,19 +1,7 @@
import { Editor } from "@tiptap/core"; import { Editor } from "@tiptap/core";
import { Check, Trash } from "lucide-react"; import { Check, Trash } from "lucide-react";
import { import { Dispatch, FC, SetStateAction, useCallback, useEffect, useRef } from "react";
Dispatch, import { cn, isValidHttpUrl, setLinkEditor, unsetLinkEditor } from "@plane/editor-core";
FC,
SetStateAction,
useCallback,
useEffect,
useRef,
} from "react";
import {
cn,
isValidHttpUrl,
setLinkEditor,
unsetLinkEditor,
} from "@plane/editor-core";
interface LinkSelectorProps { interface LinkSelectorProps {
editor: Editor; editor: Editor;
@ -21,11 +9,7 @@ interface LinkSelectorProps {
setIsOpen: Dispatch<SetStateAction<boolean>>; setIsOpen: Dispatch<SetStateAction<boolean>>;
} }
export const LinkSelector: FC<LinkSelectorProps> = ({ export const LinkSelector: FC<LinkSelectorProps> = ({ editor, isOpen, setIsOpen }) => {
editor,
isOpen,
setIsOpen,
}) => {
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
const onLinkSubmit = useCallback(() => { const onLinkSubmit = useCallback(() => {
@ -47,7 +31,7 @@ export const LinkSelector: FC<LinkSelectorProps> = ({
type="button" type="button"
className={cn( className={cn(
"flex h-full items-center space-x-2 px-3 py-1.5 text-sm font-medium text-custom-text-300 hover:bg-custom-background-100 active:bg-custom-background-100", "flex h-full items-center space-x-2 px-3 py-1.5 text-sm font-medium text-custom-text-300 hover:bg-custom-background-100 active:bg-custom-background-100",
{ "bg-custom-background-100": isOpen }, { "bg-custom-background-100": isOpen }
)} )}
onClick={() => { onClick={() => {
setIsOpen(!isOpen); setIsOpen(!isOpen);
@ -64,7 +48,7 @@ export const LinkSelector: FC<LinkSelectorProps> = ({
</button> </button>
{isOpen && ( {isOpen && (
<div <div
className="fixed top-full z-[99999] mt-1 flex w-60 overflow-hidden rounded border border-custom-border-300 bg-custom-background-100 dow-xl animate-in fade-in slide-in-from-top-1" className="dow-xl fixed top-full z-[99999] mt-1 flex w-60 overflow-hidden rounded border border-custom-border-300 bg-custom-background-100 animate-in fade-in slide-in-from-top-1"
onKeyDown={(e) => { onKeyDown={(e) => {
if (e.key === "Enter") { if (e.key === "Enter") {
e.preventDefault(); e.preventDefault();
@ -76,7 +60,7 @@ export const LinkSelector: FC<LinkSelectorProps> = ({
ref={inputRef} ref={inputRef}
type="url" type="url"
placeholder="Paste a link" placeholder="Paste a link"
className="flex-1 bg-custom-background-100 border-r border-custom-border-300 p-1 text-sm outline-none placeholder:text-custom-text-400" className="flex-1 border-r border-custom-border-300 bg-custom-background-100 p-1 text-sm outline-none placeholder:text-custom-text-400"
defaultValue={editor.getAttributes("link").href || ""} defaultValue={editor.getAttributes("link").href || ""}
/> />
{editor.getAttributes("link").href ? ( {editor.getAttributes("link").href ? (

View File

@ -21,21 +21,13 @@ interface NodeSelectorProps {
setIsOpen: Dispatch<SetStateAction<boolean>>; setIsOpen: Dispatch<SetStateAction<boolean>>;
} }
export const NodeSelector: FC<NodeSelectorProps> = ({ export const NodeSelector: FC<NodeSelectorProps> = ({ editor, isOpen, setIsOpen }) => {
editor,
isOpen,
setIsOpen,
}) => {
const items: BubbleMenuItem[] = [ const items: BubbleMenuItem[] = [
{ {
name: "Text", name: "Text",
icon: TextIcon, icon: TextIcon,
command: () => command: () => editor.chain().focus().toggleNode("paragraph", "paragraph").run(),
editor.chain().focus().toggleNode("paragraph", "paragraph").run(), isActive: () => editor.isActive("paragraph") && !editor.isActive("bulletList") && !editor.isActive("orderedList"),
isActive: () =>
editor.isActive("paragraph") &&
!editor.isActive("bulletList") &&
!editor.isActive("orderedList"),
}, },
HeadingOneItem(editor), HeadingOneItem(editor),
HeadingTwoItem(editor), HeadingTwoItem(editor),
@ -75,9 +67,8 @@ export const NodeSelector: FC<NodeSelectorProps> = ({
className={cn( className={cn(
"flex items-center justify-between rounded-sm px-2 py-1 text-sm text-custom-text-200 hover:bg-custom-primary-100/5 hover:text-custom-text-100", "flex items-center justify-between rounded-sm px-2 py-1 text-sm text-custom-text-200 hover:bg-custom-primary-100/5 hover:text-custom-text-100",
{ {
"bg-custom-primary-100/5 text-custom-text-100": "bg-custom-primary-100/5 text-custom-text-100": activeItem.name === item.name,
activeItem.name === item.name, }
},
)} )}
> >
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">

View File

@ -1,10 +1,5 @@
"use client"; "use client";
import { import { EditorContainer, EditorContentWrapper, getEditorClassNames, useReadOnlyEditor } from "@plane/editor-core";
EditorContainer,
EditorContentWrapper,
getEditorClassNames,
useReadOnlyEditor,
} from "@plane/editor-core";
import * as React from "react"; import * as React from "react";
interface IRichTextReadOnlyEditor { interface IRichTextReadOnlyEditor {
@ -51,19 +46,15 @@ const RichReadOnlyEditor = ({
return ( return (
<EditorContainer editor={editor} editorClassNames={editorClassNames}> <EditorContainer editor={editor} editorClassNames={editorClassNames}>
<div className="flex flex-col"> <div className="flex flex-col">
<EditorContentWrapper <EditorContentWrapper editor={editor} editorContentCustomClassNames={editorContentCustomClassNames} />
editor={editor}
editorContentCustomClassNames={editorContentCustomClassNames}
/>
</div> </div>
</EditorContainer> </EditorContainer>
); );
}; };
const RichReadOnlyEditorWithRef = React.forwardRef< const RichReadOnlyEditorWithRef = React.forwardRef<EditorHandle, IRichTextReadOnlyEditor>((props, ref) => (
EditorHandle, <RichReadOnlyEditor {...props} forwardedRef={ref} />
IRichTextReadOnlyEditor ));
>((props, ref) => <RichReadOnlyEditor {...props} forwardedRef={ref} />);
RichReadOnlyEditorWithRef.displayName = "RichReadOnlyEditorWithRef"; RichReadOnlyEditorWithRef.displayName = "RichReadOnlyEditorWithRef";

View File

@ -0,0 +1,4 @@
module.exports = {
root: true,
extends: ["custom"],
};

View File

@ -0,0 +1,6 @@
.next
.vercel
.tubro
out/
dis/
build/

View File

@ -0,0 +1,5 @@
{
"printWidth": 120,
"tabWidth": 2,
"trailingComma": "es5"
}

View File

@ -1,8 +1,5 @@
export type { DeleteImage } from "./types/delete-image"; export type { DeleteImage } from "./types/delete-image";
export type { UploadImage } from "./types/upload-image"; export type { UploadImage } from "./types/upload-image";
export type { RestoreImage } from "./types/restore-image"; export type { RestoreImage } from "./types/restore-image";
export type { export type { IMentionHighlight, IMentionSuggestion } from "./types/mention-suggestion";
IMentionHighlight, export type { ISlashCommandItem, CommandProps } from "./types/slash-commands-suggestion";
IMentionSuggestion,
} from "./types/mention-suggestion";
export type { ISlashCommandItem, CommandProps } from "./types/slash-commands-suggestion"

View File

@ -1,15 +1,16 @@
import { ReactNode } from "react"; import { ReactNode } from "react";
import { Editor, Range } from "@tiptap/core" import { Editor, Range } from "@tiptap/core";
export type CommandProps = { export type CommandProps = {
editor: Editor; editor: Editor;
range: Range; range: Range;
} };
export type ISlashCommandItem = { export type ISlashCommandItem = {
key: string;
title: string; title: string;
description: string; description: string;
searchTerms: string[]; searchTerms: string[];
icon: ReactNode; icon: ReactNode;
command: ({ editor, range }: CommandProps) => void; command: ({ editor, range }: CommandProps) => void;
} };

View File

@ -8,10 +8,11 @@
"eslint": "^7.23.0", "eslint": "^7.23.0",
"eslint-config-next": "13.0.0", "eslint-config-next": "13.0.0",
"eslint-config-prettier": "^8.3.0", "eslint-config-prettier": "^8.3.0",
"eslint-plugin-react": "7.31.8", "eslint-config-turbo": "latest",
"eslint-config-turbo": "latest" "eslint-plugin-react": "7.31.8"
}, },
"devDependencies": { "devDependencies": {
"@typescript-eslint/eslint-plugin": "^6.13.2",
"typescript": "^4.7.4" "typescript": "^4.7.4"
}, },
"publishConfig": { "publishConfig": {

Some files were not shown because too many files have changed in this diff Show More