forked from github/plane
Merge pull request #3056 from makeplane/promote/develop-preview
Promote: Develop to Preview
This commit is contained in:
commit
0f28008fa5
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
@ -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>
|
||||||
|
|
||||||
---
|
---
|
||||||
@ -21,6 +23,7 @@ Let's get started!
|
|||||||
<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>
|
||||||
@ -28,6 +31,7 @@ Let's get started!
|
|||||||
</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>
|
||||||
@ -35,6 +39,7 @@ Let's get started!
|
|||||||
</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,7 +120,7 @@ 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`)
|
||||||
|
|
||||||
@ -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.
|
||||||
|
|
||||||
|
|
||||||
|
@ -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"
|
||||||
|
4
packages/editor/core/.eslintrc.js
Normal file
4
packages/editor/core/.eslintrc.js
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
extends: ["custom"],
|
||||||
|
};
|
6
packages/editor/core/.prettierignore
Normal file
6
packages/editor/core/.prettierignore
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
.next
|
||||||
|
.vercel
|
||||||
|
.tubro
|
||||||
|
out/
|
||||||
|
dis/
|
||||||
|
build/
|
5
packages/editor/core/.prettierrc
Normal file
5
packages/editor/core/.prettierrc
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"printWidth": 120,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"trailingComma": "es5"
|
||||||
|
}
|
@ -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");
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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={() => {
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -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;
|
||||||
};
|
};
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
|
@ -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 it’s the end of the document
|
// add node after horizontal rule if it’s 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);
|
||||||
|
@ -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);
|
||||||
|
@ -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) {
|
||||||
|
@ -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
|
|
||||||
),
|
|
||||||
];
|
];
|
||||||
|
@ -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,
|
|
||||||
];
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -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,
|
||||||
},
|
}
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -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)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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),
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
|
@ -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") {
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
|
@ -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>
|
||||||
|
@ -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",
|
||||||
|
@ -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,
|
||||||
{
|
|
||||||
"text-yellow-500 bg-yellow-500/20": highlights
|
|
||||||
? highlights.includes(props.node.attrs.id)
|
|
||||||
: false,
|
|
||||||
"cursor-pointer": !props.extension.options.readonly,
|
"cursor-pointer": !props.extension.options.readonly,
|
||||||
// "hover:bg-custom-primary-300" : !props.extension.options.readonly && !highlights.includes(props.node.attrs.id)
|
// "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}
|
||||||
|
@ -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;
|
||||||
|
@ -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"),
|
||||||
|
@ -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);
|
||||||
|
@ -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 {
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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,
|
|
||||||
),
|
|
||||||
];
|
];
|
||||||
|
4
packages/editor/document-editor/.eslintrc.js
Normal file
4
packages/editor/document-editor/.eslintrc.js
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
extends: ["custom"],
|
||||||
|
};
|
6
packages/editor/document-editor/.prettierignore
Normal file
6
packages/editor/document-editor/.prettierignore
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
.next
|
||||||
|
.vercel
|
||||||
|
.tubro
|
||||||
|
out/
|
||||||
|
dis/
|
||||||
|
build/
|
5
packages/editor/document-editor/.prettierrc
Normal file
5
packages/editor/document-editor/.prettierrc
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"printWidth": 120,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"trailingComma": "es5"
|
||||||
|
}
|
@ -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",
|
||||||
|
@ -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";
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
@ -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}
|
||||||
|
@ -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>
|
||||||
|
@ -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>
|
||||||
|
@ -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}
|
||||||
|
@ -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"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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 }) => {
|
||||||
|
@ -18,8 +18,7 @@ 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,
|
||||||
@ -44,8 +43,7 @@ export const IssueSuggestions = (suggestions: any[]) => {
|
|||||||
.run();
|
.run();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
return IssueEmbedSuggestions.configure({
|
return IssueEmbedSuggestions.configure({
|
||||||
suggestion: {
|
suggestion: {
|
||||||
|
@ -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 });
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -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) => {
|
||||||
|
@ -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}
|
|
||||||
className={"max-h-[140px] overflow-y-scroll overflow-x-hidden"}
|
|
||||||
>
|
|
||||||
{sectionItems.map(
|
|
||||||
(item: IssueSuggestionProps, index: number) => (
|
|
||||||
<button
|
<button
|
||||||
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 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`,
|
||||||
{
|
{
|
||||||
"bg-custom-primary-100/5 text-custom-text-100":
|
"bg-custom-primary-100/5 text-custom-text-100":
|
||||||
section === currentSection &&
|
section === currentSection && index === selectedIndex,
|
||||||
index === selectedIndex,
|
}
|
||||||
},
|
|
||||||
)}
|
)}
|
||||||
key={index}
|
key={index}
|
||||||
onClick={() => selectItem(index)}
|
onClick={() => selectItem(index)}
|
||||||
>
|
>
|
||||||
<h5 className="text-xs text-custom-text-300 whitespace-nowrap">
|
<h5 className="whitespace-nowrap text-xs text-custom-text-300">{item.identifier}</h5>
|
||||||
{item.identifier}
|
|
||||||
</h5>
|
|
||||||
<PriorityIcon priority={item.priority} />
|
<PriorityIcon priority={item.priority} />
|
||||||
<div>
|
<div>
|
||||||
<p className="flex-grow text-xs truncate">
|
<p className="flex-grow truncate text-xs">{item.title}</p>
|
||||||
{item.title}
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
),
|
))}
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
@ -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,
|
||||||
});
|
});
|
||||||
|
@ -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>
|
||||||
|
@ -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}
|
|
||||||
/>
|
|
||||||
));
|
));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -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,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -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";
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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>
|
|
||||||
);
|
);
|
||||||
|
@ -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";
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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",
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
4
packages/editor/extensions/.eslintrc.js
Normal file
4
packages/editor/extensions/.eslintrc.js
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
extends: ["custom"],
|
||||||
|
};
|
6
packages/editor/extensions/.prettierignore
Normal file
6
packages/editor/extensions/.prettierignore
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
.next
|
||||||
|
.vercel
|
||||||
|
.tubro
|
||||||
|
out/
|
||||||
|
dis/
|
||||||
|
build/
|
5
packages/editor/extensions/.prettierrc
Normal file
5
packages/editor/extensions/.prettierrc
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"printWidth": 120,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"trailingComma": "es5"
|
||||||
|
}
|
@ -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"
|
||||||
},
|
},
|
||||||
|
@ -43,9 +43,7 @@ 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)
|
|
||||||
.find((elem: Element) => {
|
|
||||||
return (
|
return (
|
||||||
elem.parentElement?.matches?.(".ProseMirror") ||
|
elem.parentElement?.matches?.(".ProseMirror") ||
|
||||||
elem.matches(
|
elem.matches(
|
||||||
@ -57,7 +55,7 @@ function nodeDOMAtCoords(coords: { x: number; y: number }) {
|
|||||||
"h1, h2, h3",
|
"h1, h2, h3",
|
||||||
"[data-type=horizontalRule]",
|
"[data-type=horizontalRule]",
|
||||||
".tableWrapper",
|
".tableWrapper",
|
||||||
].join(", "),
|
].join(", ")
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -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;
|
||||||
|
@ -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,132 +66,136 @@ 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[] = [
|
||||||
{
|
{
|
||||||
|
key: "text",
|
||||||
title: "Text",
|
title: "Text",
|
||||||
description: "Just start typing with plain text.",
|
description: "Just start typing with plain text.",
|
||||||
searchTerms: ["p", "paragraph"],
|
searchTerms: ["p", "paragraph"],
|
||||||
icon: <Text size={18} />,
|
icon: <CaseSensitive className="h-3.5 w-3.5" />,
|
||||||
command: ({ editor, range }: CommandProps) => {
|
command: ({ editor, range }: CommandProps) => {
|
||||||
editor
|
editor.chain().focus().deleteRange(range).toggleNode("paragraph", "paragraph").run();
|
||||||
.chain()
|
|
||||||
.focus()
|
|
||||||
.deleteRange(range)
|
|
||||||
.toggleNode("paragraph", "paragraph")
|
|
||||||
.run();
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
key: "heading_1",
|
||||||
title: "Heading 1",
|
title: "Heading 1",
|
||||||
description: "Big section heading.",
|
description: "Big section heading.",
|
||||||
searchTerms: ["title", "big", "large"],
|
searchTerms: ["title", "big", "large"],
|
||||||
icon: <Heading1 size={18} />,
|
icon: <Heading1 className="h-3.5 w-3.5" />,
|
||||||
command: ({ editor, range }: CommandProps) => {
|
command: ({ editor, range }: CommandProps) => {
|
||||||
toggleHeadingOne(editor, range);
|
toggleHeadingOne(editor, range);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
key: "heading_2",
|
||||||
title: "Heading 2",
|
title: "Heading 2",
|
||||||
description: "Medium section heading.",
|
description: "Medium section heading.",
|
||||||
searchTerms: ["subtitle", "medium"],
|
searchTerms: ["subtitle", "medium"],
|
||||||
icon: <Heading2 size={18} />,
|
icon: <Heading2 className="h-3.5 w-3.5" />,
|
||||||
command: ({ editor, range }: CommandProps) => {
|
command: ({ editor, range }: CommandProps) => {
|
||||||
toggleHeadingTwo(editor, range);
|
toggleHeadingTwo(editor, range);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
key: "heading_3",
|
||||||
title: "Heading 3",
|
title: "Heading 3",
|
||||||
description: "Small section heading.",
|
description: "Small section heading.",
|
||||||
searchTerms: ["subtitle", "small"],
|
searchTerms: ["subtitle", "small"],
|
||||||
icon: <Heading3 size={18} />,
|
icon: <Heading3 className="h-3.5 w-3.5" />,
|
||||||
command: ({ editor, range }: CommandProps) => {
|
command: ({ editor, range }: CommandProps) => {
|
||||||
toggleHeadingThree(editor, range);
|
toggleHeadingThree(editor, range);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "To-do List",
|
key: "todo_list",
|
||||||
|
title: "To do",
|
||||||
description: "Track tasks with a to-do list.",
|
description: "Track tasks with a to-do list.",
|
||||||
searchTerms: ["todo", "task", "list", "check", "checkbox"],
|
searchTerms: ["todo", "task", "list", "check", "checkbox"],
|
||||||
icon: <CheckSquare size={18} />,
|
icon: <ListTodo className="h-3.5 w-3.5" />,
|
||||||
command: ({ editor, range }: CommandProps) => {
|
command: ({ editor, range }: CommandProps) => {
|
||||||
toggleTaskList(editor, range);
|
toggleTaskList(editor, range);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Bullet List",
|
key: "bullet_list",
|
||||||
|
title: "Bullet list",
|
||||||
description: "Create a simple bullet list.",
|
description: "Create a simple bullet list.",
|
||||||
searchTerms: ["unordered", "point"],
|
searchTerms: ["unordered", "point"],
|
||||||
icon: <List size={18} />,
|
icon: <List className="h-3.5 w-3.5" />,
|
||||||
command: ({ editor, range }: CommandProps) => {
|
command: ({ editor, range }: CommandProps) => {
|
||||||
toggleBulletList(editor, range);
|
toggleBulletList(editor, range);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: "Divider",
|
key: "numbered_list",
|
||||||
description: "Visually divide blocks",
|
title: "Numbered list",
|
||||||
searchTerms: ["line", "divider", "horizontal", "rule", "separate"],
|
|
||||||
icon: <MinusSquare size={18} />,
|
|
||||||
command: ({ editor, range }: CommandProps) => {
|
|
||||||
// @ts-expect-error I have to move this to the core
|
|
||||||
editor.chain().focus().deleteRange(range).setHorizontalRule().run();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Table",
|
|
||||||
description: "Create a Table",
|
|
||||||
searchTerms: ["table", "cell", "db", "data", "tabular"],
|
|
||||||
icon: <Table size={18} />,
|
|
||||||
command: ({ editor, range }: CommandProps) => {
|
|
||||||
insertTableCommand(editor, range);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Numbered List",
|
|
||||||
description: "Create a list with numbering.",
|
description: "Create a list with numbering.",
|
||||||
searchTerms: ["ordered"],
|
searchTerms: ["ordered"],
|
||||||
icon: <ListOrdered size={18} />,
|
icon: <ListOrdered className="h-3.5 w-3.5" />,
|
||||||
command: ({ editor, range }: CommandProps) => {
|
command: ({ editor, range }: CommandProps) => {
|
||||||
toggleOrderedList(editor, range);
|
toggleOrderedList(editor, range);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
key: "table",
|
||||||
|
title: "Table",
|
||||||
|
description: "Create a table",
|
||||||
|
searchTerms: ["table", "cell", "db", "data", "tabular"],
|
||||||
|
icon: <Table className="h-3.5 w-3.5" />,
|
||||||
|
command: ({ editor, range }: CommandProps) => {
|
||||||
|
insertTableCommand(editor, range);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "quote_block",
|
||||||
title: "Quote",
|
title: "Quote",
|
||||||
description: "Capture a quote.",
|
description: "Capture a quote.",
|
||||||
searchTerms: ["blockquote"],
|
searchTerms: ["blockquote"],
|
||||||
icon: <TextQuote size={18} />,
|
icon: <Quote className="h-3.5 w-3.5" />,
|
||||||
command: ({ editor, range }: CommandProps) =>
|
command: ({ editor, range }: CommandProps) => toggleBlockquote(editor, range),
|
||||||
toggleBlockquote(editor, range),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
key: "code_block",
|
||||||
title: "Code",
|
title: "Code",
|
||||||
description: "Capture a code snippet.",
|
description: "Capture a code snippet.",
|
||||||
searchTerms: ["codeblock"],
|
searchTerms: ["codeblock"],
|
||||||
icon: <Code size={18} />,
|
icon: <Code2 className="h-3.5 w-3.5" />,
|
||||||
command: ({ editor, range }: CommandProps) =>
|
command: ({ editor, range }: CommandProps) =>
|
||||||
// @ts-expect-error I have to move this to the core
|
// @ts-expect-error I have to move this to the core
|
||||||
editor.chain().focus().deleteRange(range).toggleCodeBlock().run(),
|
editor.chain().focus().deleteRange(range).toggleCodeBlock().run(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
key: "image",
|
||||||
title: "Image",
|
title: "Image",
|
||||||
description: "Upload an image from your computer.",
|
description: "Upload an image from your computer.",
|
||||||
searchTerms: ["photo", "picture", "media"],
|
searchTerms: ["photo", "picture", "media"],
|
||||||
icon: <ImageIcon size={18} />,
|
icon: <ImageIcon className="h-3.5 w-3.5" />,
|
||||||
command: ({ editor, range }: CommandProps) => {
|
command: ({ editor, range }: CommandProps) => {
|
||||||
insertImageCommand(editor, uploadFile, setIsSubmitting, range);
|
insertImageCommand(editor, uploadFile, setIsSubmitting, range);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
]
|
{
|
||||||
|
key: "divider",
|
||||||
|
title: "Divider",
|
||||||
|
description: "Visually divide blocks.",
|
||||||
|
searchTerms: ["line", "divider", "horizontal", "rule", "separate"],
|
||||||
|
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();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
if (additonalOptions) {
|
if (additionalOptions) {
|
||||||
additonalOptions.map(item => {
|
additionalOptions.map((item) => {
|
||||||
slashCommands.push(item)
|
slashCommands.push(item);
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
slashCommands = slashCommands.filter((item) => {
|
slashCommands = slashCommands.filter((item) => {
|
||||||
@ -214,14 +204,13 @@ const getSuggestionItems =
|
|||||||
return (
|
return (
|
||||||
item.title.toLowerCase().includes(search) ||
|
item.title.toLowerCase().includes(search) ||
|
||||||
item.description.toLowerCase().includes(search) ||
|
item.description.toLowerCase().includes(search) ||
|
||||||
(item.searchTerms &&
|
(item.searchTerms && item.searchTerms.some((term: string) => term.includes(search)))
|
||||||
item.searchTerms.some((term: string) => term.includes(search)))
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
})
|
});
|
||||||
|
|
||||||
return slashCommands
|
return slashCommands;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateScrollView = (container: HTMLElement, item: HTMLElement) => {
|
export const updateScrollView = (container: HTMLElement, item: HTMLElement) => {
|
||||||
@ -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,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
4
packages/editor/lite-text-editor/.eslintrc.js
Normal file
4
packages/editor/lite-text-editor/.eslintrc.js
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
extends: ["custom"],
|
||||||
|
};
|
6
packages/editor/lite-text-editor/.prettierignore
Normal file
6
packages/editor/lite-text-editor/.prettierignore
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
.next
|
||||||
|
.vercel
|
||||||
|
.tubro
|
||||||
|
out/
|
||||||
|
dis/
|
||||||
|
build/
|
5
packages/editor/lite-text-editor/.prettierrc
Normal file
5
packages/editor/lite-text-editor/.prettierrc
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"printWidth": 120,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"trailingComma": "es5"
|
||||||
|
}
|
@ -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";
|
|
||||||
|
@ -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";
|
||||||
|
|
||||||
|
@ -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>
|
|
||||||
);
|
);
|
||||||
|
@ -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
|
||||||
|
@ -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";
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
4
packages/editor/rich-text-editor/.eslintrc.js
Normal file
4
packages/editor/rich-text-editor/.eslintrc.js
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
extends: ["custom"],
|
||||||
|
};
|
6
packages/editor/rich-text-editor/.prettierignore
Normal file
6
packages/editor/rich-text-editor/.prettierignore
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
.next
|
||||||
|
.vercel
|
||||||
|
.tubro
|
||||||
|
out/
|
||||||
|
dis/
|
||||||
|
build/
|
5
packages/editor/rich-text-editor/.prettierrc
Normal file
5
packages/editor/rich-text-editor/.prettierrc
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"printWidth": 120,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"trailingComma": "es5"
|
||||||
|
}
|
@ -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"
|
|
||||||
/>
|
|
||||||
```
|
```
|
||||||
|
@ -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";
|
|
||||||
|
@ -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,
|
||||||
|
@ -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";
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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 ? (
|
||||||
|
@ -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">
|
||||||
|
@ -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";
|
||||||
|
|
||||||
|
4
packages/editor/types/.eslintrc.js
Normal file
4
packages/editor/types/.eslintrc.js
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
extends: ["custom"],
|
||||||
|
};
|
6
packages/editor/types/.prettierignore
Normal file
6
packages/editor/types/.prettierignore
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
.next
|
||||||
|
.vercel
|
||||||
|
.tubro
|
||||||
|
out/
|
||||||
|
dis/
|
||||||
|
build/
|
5
packages/editor/types/.prettierrc
Normal file
5
packages/editor/types/.prettierrc
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"printWidth": 120,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"trailingComma": "es5"
|
||||||
|
}
|
@ -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"
|
|
||||||
|
@ -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;
|
||||||
}
|
};
|
||||||
|
@ -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": {
|
||||||
|
4
packages/ui/.eslintrc.js
Normal file
4
packages/ui/.eslintrc.js
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
extends: ["custom"],
|
||||||
|
};
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user