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
|
||||
|
||||
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)
|
||||
|
||||
|
||||
|
@ -7,12 +7,14 @@ We will cover two main options for setting up your self-hosted environment: usin
|
||||
Let's get started!
|
||||
|
||||
## Setting up Docker Environment
|
||||
|
||||
<details>
|
||||
<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>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>
|
||||
|
||||
---
|
||||
@ -20,21 +22,24 @@ Let's get started!
|
||||
<details>
|
||||
<summary>Option 2 - Using Desktop</summary>
|
||||
|
||||
#### For Mac
|
||||
#### For Mac
|
||||
|
||||
<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> Double-click the downloaded `.dmg` file and drag the Docker app icon to the Applications folder. </li>
|
||||
<li>Open Docker Desktop from the Applications folder. You might be asked to provide your system password to install additional software.</li>
|
||||
</ol>
|
||||
|
||||
#### For Windows:
|
||||
#### For Windows:
|
||||
|
||||
<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>Run the installer and follow the instructions. You might be asked to enable Hyper-V and "Containers" Windows features.</li>
|
||||
<li>Open Docker Desktop. You might be asked to log out and log back in, or restart your machine, for changes to take effect.</li>
|
||||
</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>
|
||||
|
||||
---
|
||||
@ -44,6 +49,7 @@ Let's get started!
|
||||
Installing plane is a very easy and minimal step process.
|
||||
|
||||
### Prerequisite
|
||||
|
||||
- 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)
|
||||
- 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.
|
||||
|
||||
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`
|
||||
- `.env`
|
||||
|
||||
@ -113,13 +120,13 @@ Again the `options [1-6]` will be popped up and this time hit `6` to exit.
|
||||
### Continue with setup - Environment Settings
|
||||
|
||||
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`)
|
||||
|
||||
> `WEB_URL` - This is default set to `http://localhost`. Change this to the FQDN you plan to use along with NGINX_PORT (eg. `https://plane.example.com:8080` or `http://[IP-ADDRESS]:8080`)
|
||||
|
||||
> `CORS_ALLOWED_ORIGINS` - This is default set to `http://localhost`. Change this to the FQDN you plan to use along with NGINX_PORT (eg. `https://plane.example.com:8080` or `http://[IP-ADDRESS]:8080`)
|
||||
> `CORS_ALLOWED_ORIGINS` - This is default set to `http://localhost`. Change this to the FQDN you plan to use along with NGINX_PORT (eg. `https://plane.example.com:8080` or `http://[IP-ADDRESS]:8080`)
|
||||
|
||||
There are many other settings you can play with, but we suggest you configure `EMAIL SETTINGS` as it will enable you to invite your teammates onto the platform.
|
||||
|
||||
@ -150,7 +157,7 @@ Be patient as it might take sometime based on download speed and system configur
|
||||
|
||||
This is the confirmation that all images were downloaded and the services are up & running.
|
||||
|
||||
You have successfully self hosted `Plane` instance. Access the application by going to IP or domain you have configured it (e.g `https://plane.example.com:8080` or `http://[IP-ADDRESS]:8080`)
|
||||
You have successfully self hosted `Plane` instance. Access the application by going to IP or domain you have configured it (e.g `https://plane.example.com:8080` or `http://[IP-ADDRESS]:8080`)
|
||||
|
||||
---
|
||||
|
||||
@ -232,7 +239,6 @@ Once done, choose `6` to exit from prompt.
|
||||
|
||||
Once done with making changes in `.env` file, jump on to `Start Server`
|
||||
|
||||
|
||||
## 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
|
||||
@ -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
|
||||
|
||||
**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
|
||||
@ -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.
|
||||
|
||||
Now its time to restart v0.14.0 setup.
|
||||
|
||||
|
||||
|
@ -27,7 +27,7 @@
|
||||
"prettier": "latest",
|
||||
"prettier-plugin-tailwindcss": "^0.5.4",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"turbo": "^1.10.16"
|
||||
"turbo": "^1.11.1"
|
||||
},
|
||||
"resolutions": {
|
||||
"@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";
|
||||
|
||||
export const toggleHeadingOne = (editor: Editor, range?: Range) => {
|
||||
if (range)
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.deleteRange(range)
|
||||
.setNode("heading", { level: 1 })
|
||||
.run();
|
||||
if (range) editor.chain().focus().deleteRange(range).setNode("heading", { level: 1 }).run();
|
||||
else editor.chain().focus().toggleHeading({ level: 1 }).run();
|
||||
};
|
||||
|
||||
export const toggleHeadingTwo = (editor: Editor, range?: Range) => {
|
||||
if (range)
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.deleteRange(range)
|
||||
.setNode("heading", { level: 2 })
|
||||
.run();
|
||||
if (range) editor.chain().focus().deleteRange(range).setNode("heading", { level: 2 }).run();
|
||||
else editor.chain().focus().toggleHeading({ level: 2 }).run();
|
||||
};
|
||||
|
||||
export const toggleHeadingThree = (editor: Editor, range?: Range) => {
|
||||
if (range)
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.deleteRange(range)
|
||||
.setNode("heading", { level: 3 })
|
||||
.run();
|
||||
if (range) editor.chain().focus().deleteRange(range).setNode("heading", { 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) => {
|
||||
if (range)
|
||||
editor.chain().focus().deleteRange(range).toggleOrderedList().run();
|
||||
if (range) editor.chain().focus().deleteRange(range).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) => {
|
||||
if (range)
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.deleteRange(range)
|
||||
.toggleNode("paragraph", "paragraph")
|
||||
.toggleBlockquote()
|
||||
.run();
|
||||
else
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.toggleNode("paragraph", "paragraph")
|
||||
.toggleBlockquote()
|
||||
.run();
|
||||
if (range) editor.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) => {
|
||||
@ -105,19 +73,8 @@ export const insertTableCommand = (editor: Editor, range?: Range) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (range)
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.deleteRange(range)
|
||||
.insertTable({ rows: 3, cols: 3, withHeaderRow: true })
|
||||
.run();
|
||||
else
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.insertTable({ rows: 3, cols: 3, withHeaderRow: true })
|
||||
.run();
|
||||
if (range) editor.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) => {
|
||||
@ -131,10 +88,8 @@ export const setLinkEditor = (editor: Editor, url: string) => {
|
||||
export const insertImageCommand = (
|
||||
editor: Editor,
|
||||
uploadFile: UploadImage,
|
||||
setIsSubmitting?: (
|
||||
isSubmitting: "submitting" | "submitted" | "saved",
|
||||
) => void,
|
||||
range?: Range,
|
||||
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void,
|
||||
range?: Range
|
||||
) => {
|
||||
if (range) editor.chain().focus().deleteRange(range).run();
|
||||
const input = document.createElement("input");
|
||||
|
@ -6,25 +6,19 @@ interface EditorClassNames {
|
||||
customClassName?: string;
|
||||
}
|
||||
|
||||
export const getEditorClassNames = ({
|
||||
noBorder,
|
||||
borderOnFocus,
|
||||
customClassName,
|
||||
}: EditorClassNames) =>
|
||||
export const getEditorClassNames = ({ noBorder, borderOnFocus, customClassName }: EditorClassNames) =>
|
||||
cn(
|
||||
"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",
|
||||
borderOnFocus ? "focus:border border-custom-border-300" : "focus:border-0",
|
||||
customClassName,
|
||||
customClassName
|
||||
);
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
export const findTableAncestor = (
|
||||
node: Node | null,
|
||||
): HTMLTableElement | null => {
|
||||
export const findTableAncestor = (node: Node | null): HTMLTableElement | null => {
|
||||
while (node !== null && node.nodeName !== "TABLE") {
|
||||
node = node.parentNode;
|
||||
}
|
||||
|
@ -7,11 +7,7 @@ interface EditorContainerProps {
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export const EditorContainer = ({
|
||||
editor,
|
||||
editorClassNames,
|
||||
children,
|
||||
}: EditorContainerProps) => (
|
||||
export const EditorContainer = ({ editor, editorClassNames, children }: EditorContainerProps) => (
|
||||
<div
|
||||
id="editor-container"
|
||||
onClick={() => {
|
||||
|
@ -8,16 +8,10 @@ interface EditorContentProps {
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export const EditorContentWrapper = ({
|
||||
editor,
|
||||
editorContentCustomClassNames = "",
|
||||
children,
|
||||
}: EditorContentProps) => (
|
||||
export const EditorContentWrapper = ({ editor, editorContentCustomClassNames = "", children }: EditorContentProps) => (
|
||||
<div className={`contentEditor ${editorContentCustomClassNames}`}>
|
||||
<EditorContent editor={editor} />
|
||||
{editor?.isActive("image") && editor?.isEditable && (
|
||||
<ImageResizer editor={editor} />
|
||||
)}
|
||||
{editor?.isActive("image") && editor?.isEditable && <ImageResizer editor={editor} />}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
@ -2,10 +2,7 @@ import { getNodeType } from "@tiptap/core";
|
||||
import { NodeType } from "@tiptap/pm/model";
|
||||
import { EditorState } from "@tiptap/pm/state";
|
||||
|
||||
export const findListItemPos = (
|
||||
typeOrName: string | NodeType,
|
||||
state: EditorState,
|
||||
) => {
|
||||
export const findListItemPos = (typeOrName: string | NodeType, state: EditorState) => {
|
||||
const { $from } = state.selection;
|
||||
const nodeType = getNodeType(typeOrName, state.schema);
|
||||
|
||||
|
@ -10,11 +10,7 @@ export const getNextListDepth = (typeOrName: string, state: EditorState) => {
|
||||
return false;
|
||||
}
|
||||
|
||||
const [, depth] = getNodeAtPosition(
|
||||
state,
|
||||
typeOrName,
|
||||
listItemPos.$pos.pos + 4,
|
||||
);
|
||||
const [, depth] = getNodeAtPosition(state, typeOrName, listItemPos.$pos.pos + 4);
|
||||
|
||||
return depth;
|
||||
};
|
||||
|
@ -4,11 +4,7 @@ import { Node } from "@tiptap/pm/model";
|
||||
import { findListItemPos } from "./find-list-item-pos";
|
||||
import { hasListBefore } from "./has-list-before";
|
||||
|
||||
export const handleBackspace = (
|
||||
editor: Editor,
|
||||
name: string,
|
||||
parentListTypes: string[],
|
||||
) => {
|
||||
export const handleBackspace = (editor: Editor, name: string, parentListTypes: string[]) => {
|
||||
// this is required to still handle the undo handling
|
||||
if (editor.commands.undoInputRule()) {
|
||||
return true;
|
||||
@ -23,10 +19,7 @@ export const handleBackspace = (
|
||||
// if the current item is NOT inside a list item &
|
||||
// the previous item is a list (orderedList or bulletList)
|
||||
// move the cursor into the list and delete the current item
|
||||
if (
|
||||
!isNodeActive(editor.state, name) &&
|
||||
hasListBefore(editor.state, name, parentListTypes)
|
||||
) {
|
||||
if (!isNodeActive(editor.state, name) && hasListBefore(editor.state, name, parentListTypes)) {
|
||||
const { $anchor } = editor.state.selection;
|
||||
|
||||
const $listPos = editor.state.doc.resolve($anchor.before() - 1);
|
||||
@ -45,16 +38,11 @@ export const handleBackspace = (
|
||||
return false;
|
||||
}
|
||||
|
||||
const $lastItemPos = editor.state.doc.resolve(
|
||||
$listPos.start() + lastItem.pos + 1,
|
||||
);
|
||||
const $lastItemPos = editor.state.doc.resolve($listPos.start() + lastItem.pos + 1);
|
||||
|
||||
return editor
|
||||
.chain()
|
||||
.cut(
|
||||
{ from: $anchor.start() - 1, to: $anchor.end() + 1 },
|
||||
$lastItemPos.end(),
|
||||
)
|
||||
.cut({ from: $anchor.start() - 1, to: $anchor.end() + 1 }, $lastItemPos.end())
|
||||
.joinForward()
|
||||
.run();
|
||||
}
|
||||
|
@ -1,10 +1,6 @@
|
||||
import { EditorState } from "@tiptap/pm/state";
|
||||
|
||||
export const hasListBefore = (
|
||||
editorState: EditorState,
|
||||
name: string,
|
||||
parentListTypes: string[],
|
||||
) => {
|
||||
export const hasListBefore = (editorState: EditorState, name: string, parentListTypes: string[]) => {
|
||||
const { $anchor } = editorState.selection;
|
||||
|
||||
const previousNodePos = Math.max(0, $anchor.pos - 2);
|
||||
|
@ -1,9 +1,6 @@
|
||||
import { EditorState } from "@tiptap/pm/state";
|
||||
|
||||
export const hasListItemAfter = (
|
||||
typeOrName: string,
|
||||
state: EditorState,
|
||||
): boolean => {
|
||||
export const hasListItemAfter = (typeOrName: string, state: EditorState): boolean => {
|
||||
const { $anchor } = state.selection;
|
||||
|
||||
const $targetPos = state.doc.resolve($anchor.pos - $anchor.parentOffset - 2);
|
||||
|
@ -1,9 +1,6 @@
|
||||
import { EditorState } from "@tiptap/pm/state";
|
||||
|
||||
export const hasListItemBefore = (
|
||||
typeOrName: string,
|
||||
state: EditorState,
|
||||
): boolean => {
|
||||
export const hasListItemBefore = (typeOrName: string, state: EditorState): boolean => {
|
||||
const { $anchor } = state.selection;
|
||||
|
||||
const $targetPos = state.doc.resolve($anchor.pos - 2);
|
||||
|
@ -1,12 +1,6 @@
|
||||
import { TextSelection } from "prosemirror-state";
|
||||
|
||||
import {
|
||||
InputRule,
|
||||
mergeAttributes,
|
||||
Node,
|
||||
nodeInputRule,
|
||||
wrappingInputRule,
|
||||
} from "@tiptap/core";
|
||||
import { InputRule, mergeAttributes, Node, nodeInputRule, wrappingInputRule } from "@tiptap/core";
|
||||
|
||||
/**
|
||||
* Extension based on:
|
||||
@ -83,8 +77,7 @@ export default Node.create<HorizontalRuleOptions>({
|
||||
tr.setSelection(TextSelection.create(tr.doc, $to.pos));
|
||||
} else {
|
||||
// add node after horizontal rule if it’s the end of the document
|
||||
const node =
|
||||
$to.parent.type.contentMatch.defaultType?.create();
|
||||
const node = $to.parent.type.contentMatch.defaultType?.create();
|
||||
|
||||
if (node) {
|
||||
tr.insert(posAfter, node);
|
||||
|
@ -4,9 +4,7 @@ import Moveable from "react-moveable";
|
||||
|
||||
export const ImageResizer = ({ editor }: { editor: Editor }) => {
|
||||
const updateMediaSize = () => {
|
||||
const imageInfo = document.querySelector(
|
||||
".ProseMirror-selectednode",
|
||||
) as HTMLImageElement;
|
||||
const imageInfo = document.querySelector(".ProseMirror-selectednode") as HTMLImageElement;
|
||||
if (imageInfo) {
|
||||
const selection = editor.state.selection;
|
||||
editor.commands.setImage({
|
||||
@ -32,9 +30,7 @@ export const ImageResizer = ({ editor }: { editor: Editor }) => {
|
||||
resizable
|
||||
throttleResize={0}
|
||||
onResizeStart={() => {
|
||||
const imageInfo = document.querySelector(
|
||||
".ProseMirror-selectednode",
|
||||
) as HTMLImageElement;
|
||||
const imageInfo = document.querySelector(".ProseMirror-selectednode") as HTMLImageElement;
|
||||
if (imageInfo) {
|
||||
const originalWidth = Number(imageInfo.width);
|
||||
const originalHeight = Number(imageInfo.height);
|
||||
|
@ -15,22 +15,14 @@ interface ImageNode extends ProseMirrorNode {
|
||||
const deleteKey = new PluginKey("delete-image");
|
||||
const IMAGE_NODE_TYPE = "image";
|
||||
|
||||
const ImageExtension = (
|
||||
deleteImage: DeleteImage,
|
||||
restoreFile: RestoreImage,
|
||||
cancelUploadImage?: () => any,
|
||||
) =>
|
||||
const ImageExtension = (deleteImage: DeleteImage, restoreFile: RestoreImage, cancelUploadImage?: () => any) =>
|
||||
ImageExt.extend({
|
||||
addProseMirrorPlugins() {
|
||||
return [
|
||||
UploadImagesPlugin(cancelUploadImage),
|
||||
new Plugin({
|
||||
key: deleteKey,
|
||||
appendTransaction: (
|
||||
transactions: readonly Transaction[],
|
||||
oldState: EditorState,
|
||||
newState: EditorState,
|
||||
) => {
|
||||
appendTransaction: (transactions: readonly Transaction[], oldState: EditorState, newState: EditorState) => {
|
||||
const newImageSources = new Set<string>();
|
||||
newState.doc.descendants((node) => {
|
||||
if (node.type.name === IMAGE_NODE_TYPE) {
|
||||
@ -67,11 +59,7 @@ const ImageExtension = (
|
||||
}),
|
||||
new Plugin({
|
||||
key: new PluginKey("imageRestoration"),
|
||||
appendTransaction: (
|
||||
transactions: readonly Transaction[],
|
||||
oldState: EditorState,
|
||||
newState: EditorState,
|
||||
) => {
|
||||
appendTransaction: (transactions: readonly Transaction[], oldState: EditorState, newState: EditorState) => {
|
||||
const oldImageSources = new Set<string>();
|
||||
oldState.doc.descendants((node) => {
|
||||
if (node.type.name === IMAGE_NODE_TYPE) {
|
||||
|
@ -22,11 +22,7 @@ import { CustomKeymap } from "./keymap";
|
||||
import { CustomCodeBlock } from "./code";
|
||||
import { CustomQuoteExtension } from "./quote";
|
||||
import { ListKeymap } from "./custom-list-keymap";
|
||||
import {
|
||||
IMentionSuggestion,
|
||||
DeleteImage,
|
||||
RestoreImage,
|
||||
} from "@plane/editor-types";
|
||||
import { IMentionSuggestion, DeleteImage, RestoreImage } from "@plane/editor-types";
|
||||
|
||||
export const CoreEditorExtensions = (
|
||||
mentionConfig: {
|
||||
@ -109,9 +105,5 @@ export const CoreEditorExtensions = (
|
||||
TableHeader,
|
||||
TableCell,
|
||||
TableRow,
|
||||
Mentions(
|
||||
mentionConfig.mentionSuggestions,
|
||||
mentionConfig.mentionHighlights,
|
||||
false
|
||||
),
|
||||
Mentions(mentionConfig.mentionSuggestions, mentionConfig.mentionHighlights, false),
|
||||
];
|
||||
|
@ -22,10 +22,6 @@ export default Node.create<TableRowOptions>({
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return [
|
||||
"tr",
|
||||
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
|
||||
0,
|
||||
];
|
||||
return ["tr", mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0];
|
||||
},
|
||||
});
|
||||
|
@ -20,15 +20,12 @@ export function tableControls() {
|
||||
mousemove: (view, event) => {
|
||||
const pluginState = key.getState(view.state);
|
||||
|
||||
if (
|
||||
!(event.target as HTMLElement).closest(".tableWrapper") &&
|
||||
pluginState.values.hoveredTable
|
||||
) {
|
||||
if (!(event.target as HTMLElement).closest(".tableWrapper") && pluginState.values.hoveredTable) {
|
||||
return view.dispatch(
|
||||
view.state.tr.setMeta(key, {
|
||||
setHoveredTable: null,
|
||||
setHoveredCell: null,
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@ -40,13 +37,11 @@ export function tableControls() {
|
||||
if (!pos) return;
|
||||
|
||||
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;
|
||||
|
||||
@ -55,7 +50,7 @@ export function tableControls() {
|
||||
view.state.tr.setMeta(key, {
|
||||
setHoveredTable: table,
|
||||
setHoveredCell: cell,
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
},
|
||||
@ -68,12 +63,7 @@ export function tableControls() {
|
||||
|
||||
const { hoveredTable, hoveredCell } = pluginState.values;
|
||||
const docSize = state.doc.content.size;
|
||||
if (
|
||||
hoveredTable &&
|
||||
hoveredCell &&
|
||||
hoveredTable.pos < docSize &&
|
||||
hoveredCell.pos < docSize
|
||||
) {
|
||||
if (hoveredTable && hoveredCell && hoveredTable.pos < docSize && hoveredCell.pos < docSize) {
|
||||
const decorations = [
|
||||
Decoration.node(
|
||||
hoveredTable.pos,
|
||||
@ -82,7 +72,7 @@ export function tableControls() {
|
||||
{
|
||||
hoveredTable,
|
||||
hoveredCell,
|
||||
},
|
||||
}
|
||||
),
|
||||
];
|
||||
|
||||
|
@ -4,11 +4,7 @@ import { Decoration, NodeView } from "@tiptap/pm/view";
|
||||
import tippy, { Instance, Props } from "tippy.js";
|
||||
|
||||
import { Editor } from "@tiptap/core";
|
||||
import {
|
||||
CellSelection,
|
||||
TableMap,
|
||||
updateColumnsOnResize,
|
||||
} from "@tiptap/prosemirror-tables";
|
||||
import { CellSelection, TableMap, updateColumnsOnResize } from "@tiptap/prosemirror-tables";
|
||||
|
||||
import icons from "./icons";
|
||||
|
||||
@ -18,7 +14,7 @@ export function updateColumns(
|
||||
table: HTMLElement,
|
||||
cellMinWidth: number,
|
||||
overrideCol?: number,
|
||||
overrideValue?: any,
|
||||
overrideValue?: any
|
||||
) {
|
||||
let totalWidth = 0;
|
||||
let fixedWidth = true;
|
||||
@ -31,8 +27,7 @@ export function updateColumns(
|
||||
const { colspan, colwidth } = row.child(i).attrs;
|
||||
|
||||
for (let j = 0; j < colspan; j += 1, col += 1) {
|
||||
const hasWidth =
|
||||
overrideCol === col ? overrideValue : colwidth && colwidth[j];
|
||||
const hasWidth = overrideCol === col ? overrideValue : colwidth && colwidth[j];
|
||||
const cssWidth = hasWidth ? `${hasWidth}px` : "";
|
||||
|
||||
totalWidth += hasWidth || cellMinWidth;
|
||||
@ -42,8 +37,7 @@ export function updateColumns(
|
||||
}
|
||||
|
||||
if (!nextDOM) {
|
||||
colgroup.appendChild(document.createElement("col")).style.width =
|
||||
cssWidth;
|
||||
colgroup.appendChild(document.createElement("col")).style.width = cssWidth;
|
||||
} else {
|
||||
if (nextDOM.style.width !== cssWidth) {
|
||||
nextDOM.style.width = cssWidth;
|
||||
@ -98,14 +92,12 @@ const columnsToolboxItems = [
|
||||
{
|
||||
label: "Add Column Before",
|
||||
icon: icons.insertLeftTableIcon,
|
||||
action: ({ editor }: { editor: Editor }) =>
|
||||
editor.chain().focus().addColumnBefore().run(),
|
||||
action: ({ editor }: { editor: Editor }) => editor.chain().focus().addColumnBefore().run(),
|
||||
},
|
||||
{
|
||||
label: "Add Column After",
|
||||
icon: icons.insertRightTableIcon,
|
||||
action: ({ editor }: { editor: Editor }) =>
|
||||
editor.chain().focus().addColumnAfter().run(),
|
||||
action: ({ editor }: { editor: Editor }) => editor.chain().focus().addColumnAfter().run(),
|
||||
},
|
||||
{
|
||||
label: "Pick Column Color",
|
||||
@ -131,8 +123,7 @@ const columnsToolboxItems = [
|
||||
{
|
||||
label: "Delete Column",
|
||||
icon: icons.deleteColumn,
|
||||
action: ({ editor }: { editor: Editor }) =>
|
||||
editor.chain().focus().deleteColumn().run(),
|
||||
action: ({ editor }: { editor: Editor }) => editor.chain().focus().deleteColumn().run(),
|
||||
},
|
||||
];
|
||||
|
||||
@ -140,14 +131,12 @@ const rowsToolboxItems = [
|
||||
{
|
||||
label: "Add Row Above",
|
||||
icon: icons.insertTopTableIcon,
|
||||
action: ({ editor }: { editor: Editor }) =>
|
||||
editor.chain().focus().addRowBefore().run(),
|
||||
action: ({ editor }: { editor: Editor }) => editor.chain().focus().addRowBefore().run(),
|
||||
},
|
||||
{
|
||||
label: "Add Row Below",
|
||||
icon: icons.insertBottomTableIcon,
|
||||
action: ({ editor }: { editor: Editor }) =>
|
||||
editor.chain().focus().addRowAfter().run(),
|
||||
action: ({ editor }: { editor: Editor }) => editor.chain().focus().addRowAfter().run(),
|
||||
},
|
||||
{
|
||||
label: "Pick Row Color",
|
||||
@ -159,11 +148,7 @@ const rowsToolboxItems = [
|
||||
}: {
|
||||
editor: Editor;
|
||||
triggerButton: HTMLButtonElement;
|
||||
controlsContainer:
|
||||
| Element
|
||||
| "parent"
|
||||
| ((ref: Element) => Element)
|
||||
| undefined;
|
||||
controlsContainer: Element | "parent" | ((ref: Element) => Element) | undefined;
|
||||
}) => {
|
||||
createColorPickerToolbox({
|
||||
triggerButton,
|
||||
@ -177,8 +162,7 @@ const rowsToolboxItems = [
|
||||
{
|
||||
label: "Delete Row",
|
||||
icon: icons.deleteRow,
|
||||
action: ({ editor }: { editor: Editor }) =>
|
||||
editor.chain().focus().deleteRow().run(),
|
||||
action: ({ editor }: { editor: Editor }) => editor.chain().focus().deleteRow().run(),
|
||||
},
|
||||
];
|
||||
|
||||
@ -213,9 +197,9 @@ function createToolbox({
|
||||
innerHTML: item.icon,
|
||||
}),
|
||||
h("div", { className: "label" }, item.label),
|
||||
],
|
||||
),
|
||||
),
|
||||
]
|
||||
)
|
||||
)
|
||||
),
|
||||
...tippyOptions,
|
||||
});
|
||||
@ -272,11 +256,11 @@ function createColorPickerToolbox({
|
||||
{
|
||||
className: "label",
|
||||
},
|
||||
key,
|
||||
key
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
]
|
||||
)
|
||||
)
|
||||
),
|
||||
onHidden: (instance) => {
|
||||
instance.destroy();
|
||||
@ -319,7 +303,7 @@ export class TableView implements NodeView {
|
||||
cellMinWidth: number,
|
||||
decorations: Decoration[],
|
||||
editor: Editor,
|
||||
getPos: () => number,
|
||||
getPos: () => number
|
||||
) {
|
||||
this.node = node;
|
||||
this.cellMinWidth = cellMinWidth;
|
||||
@ -337,7 +321,7 @@ export class TableView implements NodeView {
|
||||
itemType: "button",
|
||||
className: "rowsControlDiv",
|
||||
onClick: () => this.selectRow(),
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
this.columnsControl = h(
|
||||
@ -347,14 +331,14 @@ export class TableView implements NodeView {
|
||||
itemType: "button",
|
||||
className: "columnsControlDiv",
|
||||
onClick: () => this.selectColumn(),
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
this.controls = h(
|
||||
"div",
|
||||
{ className: "tableControls", contentEditable: "false" },
|
||||
this.rowsControl,
|
||||
this.columnsControl,
|
||||
this.columnsControl
|
||||
);
|
||||
|
||||
this.columnsToolbox = createToolbox({
|
||||
@ -397,7 +381,7 @@ export class TableView implements NodeView {
|
||||
this.colgroup = h(
|
||||
"colgroup",
|
||||
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.table = h("table", null, this.colgroup, this.tbody);
|
||||
@ -408,7 +392,7 @@ export class TableView implements NodeView {
|
||||
className: "tableWrapper controls--disabled",
|
||||
},
|
||||
this.controls,
|
||||
this.table,
|
||||
this.table
|
||||
);
|
||||
|
||||
this.render();
|
||||
@ -434,18 +418,11 @@ export class TableView implements NodeView {
|
||||
|
||||
render() {
|
||||
if (this.colgroup.children.length !== this.map.width) {
|
||||
const cols = Array.from({ length: this.map.width }, () => 1).map(() =>
|
||||
h("col"),
|
||||
);
|
||||
const cols = Array.from({ length: this.map.width }, () => 1).map(() => h("col"));
|
||||
this.colgroup.replaceChildren(...cols);
|
||||
}
|
||||
|
||||
updateColumnsOnResize(
|
||||
this.node,
|
||||
this.colgroup,
|
||||
this.table,
|
||||
this.cellMinWidth,
|
||||
);
|
||||
updateColumnsOnResize(this.node, this.colgroup, this.table, this.cellMinWidth);
|
||||
}
|
||||
|
||||
ignoreMutation() {
|
||||
@ -453,9 +430,7 @@ export class TableView implements NodeView {
|
||||
}
|
||||
|
||||
updateControls() {
|
||||
const { hoveredTable: table, hoveredCell: cell } = Object.values(
|
||||
this.decorations,
|
||||
).reduce(
|
||||
const { hoveredTable: table, hoveredCell: cell } = Object.values(this.decorations).reduce(
|
||||
(acc, curr) => {
|
||||
if (curr.spec.hoveredCell !== undefined) {
|
||||
acc["hoveredCell"] = curr.spec.hoveredCell;
|
||||
@ -466,7 +441,7 @@ export class TableView implements NodeView {
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, HTMLElement>,
|
||||
{} as Record<string, HTMLElement>
|
||||
) as any;
|
||||
|
||||
if (table === undefined || cell === undefined) {
|
||||
@ -481,9 +456,7 @@ export class TableView implements NodeView {
|
||||
const tableRect = this.table.getBoundingClientRect();
|
||||
const cellRect = cellDom.getBoundingClientRect();
|
||||
|
||||
this.columnsControl.style.left = `${
|
||||
cellRect.left - tableRect.left - this.table.parentElement!.scrollLeft
|
||||
}px`;
|
||||
this.columnsControl.style.left = `${cellRect.left - tableRect.left - this.table.parentElement!.scrollLeft}px`;
|
||||
this.columnsControl.style.width = `${cellRect.width}px`;
|
||||
|
||||
this.rowsControl.style.top = `${cellRect.top - tableRect.top}px`;
|
||||
@ -493,22 +466,14 @@ export class TableView implements NodeView {
|
||||
selectColumn() {
|
||||
if (!this.hoveredCell) return;
|
||||
|
||||
const colIndex = this.map.colCount(
|
||||
this.hoveredCell.pos - (this.getPos() + 1),
|
||||
);
|
||||
const colIndex = this.map.colCount(this.hoveredCell.pos - (this.getPos() + 1));
|
||||
const anchorCellPos = this.hoveredCell.pos;
|
||||
const headCellPos =
|
||||
this.map.map[colIndex + this.map.width * (this.map.height - 1)] +
|
||||
(this.getPos() + 1);
|
||||
const headCellPos = this.map.map[colIndex + this.map.width * (this.map.height - 1)] + (this.getPos() + 1);
|
||||
|
||||
const cellSelection = CellSelection.create(
|
||||
this.editor.view.state.doc,
|
||||
anchorCellPos,
|
||||
headCellPos,
|
||||
);
|
||||
const cellSelection = CellSelection.create(this.editor.view.state.doc, anchorCellPos, headCellPos);
|
||||
this.editor.view.dispatch(
|
||||
// @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;
|
||||
|
||||
const anchorCellPos = this.hoveredCell.pos;
|
||||
const anchorCellIndex = this.map.map.indexOf(
|
||||
anchorCellPos - (this.getPos() + 1),
|
||||
);
|
||||
const headCellPos =
|
||||
this.map.map[anchorCellIndex + (this.map.width - 1)] +
|
||||
(this.getPos() + 1);
|
||||
const anchorCellIndex = this.map.map.indexOf(anchorCellPos - (this.getPos() + 1));
|
||||
const headCellPos = this.map.map[anchorCellIndex + (this.map.width - 1)] + (this.getPos() + 1);
|
||||
|
||||
const cellSelection = CellSelection.create(
|
||||
this.editor.state.doc,
|
||||
anchorCellPos,
|
||||
headCellPos,
|
||||
);
|
||||
const cellSelection = CellSelection.create(this.editor.state.doc, anchorCellPos, headCellPos);
|
||||
this.editor.view.dispatch(
|
||||
// @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 {
|
||||
callOrReturn,
|
||||
getExtensionField,
|
||||
mergeAttributes,
|
||||
Node,
|
||||
ParentConfig,
|
||||
} from "@tiptap/core";
|
||||
import { callOrReturn, getExtensionField, mergeAttributes, Node, ParentConfig } from "@tiptap/core";
|
||||
import {
|
||||
addColumnAfter,
|
||||
addColumnBefore,
|
||||
@ -44,11 +38,7 @@ export interface TableOptions {
|
||||
declare module "@tiptap/core" {
|
||||
interface Commands<ReturnType> {
|
||||
table: {
|
||||
insertTable: (options?: {
|
||||
rows?: number;
|
||||
cols?: number;
|
||||
withHeaderRow?: boolean;
|
||||
}) => ReturnType;
|
||||
insertTable: (options?: { rows?: number; cols?: number; withHeaderRow?: boolean }) => ReturnType;
|
||||
addColumnBefore: () => ReturnType;
|
||||
addColumnAfter: () => ReturnType;
|
||||
deleteColumn: () => ReturnType;
|
||||
@ -66,10 +56,7 @@ declare module "@tiptap/core" {
|
||||
goToNextCell: () => ReturnType;
|
||||
goToPreviousCell: () => ReturnType;
|
||||
fixTables: () => ReturnType;
|
||||
setCellSelection: (position: {
|
||||
anchorCell: number;
|
||||
headCell?: number;
|
||||
}) => ReturnType;
|
||||
setCellSelection: (position: { anchorCell: number; headCell?: number }) => ReturnType;
|
||||
};
|
||||
}
|
||||
|
||||
@ -114,11 +101,7 @@ export default Node.create({
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return [
|
||||
"table",
|
||||
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes),
|
||||
["tbody", 0],
|
||||
];
|
||||
return ["table", mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), ["tbody", 0]];
|
||||
},
|
||||
|
||||
addCommands() {
|
||||
@ -220,11 +203,7 @@ export default Node.create({
|
||||
(position) =>
|
||||
({ tr, dispatch }) => {
|
||||
if (dispatch) {
|
||||
const selection = CellSelection.create(
|
||||
tr.doc,
|
||||
position.anchorCell,
|
||||
position.headCell,
|
||||
);
|
||||
const selection = CellSelection.create(tr.doc, position.anchorCell, position.headCell);
|
||||
|
||||
// @ts-ignore
|
||||
tr.setSelection(selection);
|
||||
@ -260,13 +239,7 @@ export default Node.create({
|
||||
return ({ editor, getPos, node, decorations }) => {
|
||||
const { cellMinWidth } = this.options;
|
||||
|
||||
return new TableView(
|
||||
node,
|
||||
cellMinWidth,
|
||||
decorations,
|
||||
editor,
|
||||
getPos as () => number,
|
||||
);
|
||||
return new TableView(node, cellMinWidth, decorations, editor, getPos as () => number);
|
||||
};
|
||||
},
|
||||
|
||||
@ -289,7 +262,7 @@ export default Node.create({
|
||||
|
||||
// @ts-ignore
|
||||
lastColumnResizable: this.options.lastColumnResizable,
|
||||
}),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@ -304,9 +277,7 @@ export default Node.create({
|
||||
};
|
||||
|
||||
return {
|
||||
tableRole: callOrReturn(
|
||||
getExtensionField(extension, "tableRole", context),
|
||||
),
|
||||
tableRole: callOrReturn(getExtensionField(extension, "tableRole", context)),
|
||||
};
|
||||
},
|
||||
});
|
||||
|
@ -2,7 +2,7 @@ import { Fragment, Node as ProsemirrorNode, NodeType } from "prosemirror-model";
|
||||
|
||||
export function createCell(
|
||||
cellType: NodeType,
|
||||
cellContent?: Fragment | ProsemirrorNode | Array<ProsemirrorNode>,
|
||||
cellContent?: Fragment | ProsemirrorNode | Array<ProsemirrorNode>
|
||||
): ProsemirrorNode | null | undefined {
|
||||
if (cellContent) {
|
||||
return cellType.createChecked(null, cellContent);
|
||||
|
@ -8,7 +8,7 @@ export function createTable(
|
||||
rowsCount: number,
|
||||
colsCount: number,
|
||||
withHeaderRow: boolean,
|
||||
cellContent?: Fragment | ProsemirrorNode | Array<ProsemirrorNode>,
|
||||
cellContent?: Fragment | ProsemirrorNode | Array<ProsemirrorNode>
|
||||
): ProsemirrorNode {
|
||||
const types = getTableNodeTypes(schema);
|
||||
const headerCells: ProsemirrorNode[] = [];
|
||||
@ -33,12 +33,7 @@ export function createTable(
|
||||
const rows: ProsemirrorNode[] = [];
|
||||
|
||||
for (let index = 0; index < rowsCount; index += 1) {
|
||||
rows.push(
|
||||
types.row.createChecked(
|
||||
null,
|
||||
withHeaderRow && index === 0 ? headerCells : cells,
|
||||
),
|
||||
);
|
||||
rows.push(types.row.createChecked(null, withHeaderRow && index === 0 ? headerCells : cells));
|
||||
}
|
||||
|
||||
return types.table.createChecked(null, rows);
|
||||
|
@ -1,13 +1,8 @@
|
||||
import {
|
||||
findParentNodeClosestToPos,
|
||||
KeyboardShortcutCommand,
|
||||
} from "@tiptap/core";
|
||||
import { findParentNodeClosestToPos, KeyboardShortcutCommand } from "@tiptap/core";
|
||||
|
||||
import { isCellSelection } from "./is-cell-selection";
|
||||
|
||||
export const deleteTableWhenAllCellsSelected: KeyboardShortcutCommand = ({
|
||||
editor,
|
||||
}) => {
|
||||
export const deleteTableWhenAllCellsSelected: KeyboardShortcutCommand = ({ editor }) => {
|
||||
const { selection } = editor.state;
|
||||
|
||||
if (!isCellSelection(selection)) {
|
||||
@ -15,10 +10,7 @@ export const deleteTableWhenAllCellsSelected: KeyboardShortcutCommand = ({
|
||||
}
|
||||
|
||||
let cellCount = 0;
|
||||
const table = findParentNodeClosestToPos(
|
||||
selection.ranges[0].$from,
|
||||
(node) => node.type.name === "table",
|
||||
);
|
||||
const table = findParentNodeClosestToPos(selection.ranges[0].$from, (node) => node.type.name === "table");
|
||||
|
||||
table?.node.descendants((node) => {
|
||||
if (node.type.name === "table") {
|
||||
|
@ -4,12 +4,7 @@ import { CoreEditorProps } from "../props";
|
||||
import { CoreEditorExtensions } from "../extensions";
|
||||
import { EditorProps } from "@tiptap/pm/view";
|
||||
import { getTrimmedHTML } from "../../lib/utils";
|
||||
import {
|
||||
DeleteImage,
|
||||
IMentionSuggestion,
|
||||
RestoreImage,
|
||||
UploadImage,
|
||||
} from "@plane/editor-types";
|
||||
import { DeleteImage, IMentionSuggestion, RestoreImage, UploadImage } from "@plane/editor-types";
|
||||
|
||||
interface CustomEditorProps {
|
||||
uploadFile: UploadImage;
|
||||
@ -20,9 +15,7 @@ interface CustomEditorProps {
|
||||
};
|
||||
deleteFile: DeleteImage;
|
||||
cancelUploadImage?: () => any;
|
||||
setIsSubmitting?: (
|
||||
isSubmitting: "submitting" | "submitted" | "saved",
|
||||
) => void;
|
||||
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void;
|
||||
setShouldShowAlert?: (showAlert: boolean) => void;
|
||||
value: string;
|
||||
debouncedUpdatesEnabled?: boolean;
|
||||
@ -66,12 +59,11 @@ export const useEditor = ({
|
||||
},
|
||||
deleteFile,
|
||||
restoreFile,
|
||||
cancelUploadImage,
|
||||
cancelUploadImage
|
||||
),
|
||||
...extensions,
|
||||
],
|
||||
content:
|
||||
typeof value === "string" && value.trim() !== "" ? value : "<p></p>",
|
||||
content: typeof value === "string" && value.trim() !== "" ? value : "<p></p>",
|
||||
onCreate: async ({ editor }) => {
|
||||
onStart?.(editor.getJSON(), getTrimmedHTML(editor.getHTML()));
|
||||
},
|
||||
@ -82,7 +74,7 @@ export const useEditor = ({
|
||||
onChange?.(editor.getJSON(), getTrimmedHTML(editor.getHTML()));
|
||||
},
|
||||
},
|
||||
[rerenderOnPropsChange],
|
||||
[rerenderOnPropsChange]
|
||||
);
|
||||
|
||||
const editorRef: MutableRefObject<Editor | null> = useRef(null);
|
||||
|
@ -30,8 +30,7 @@ export const useReadOnlyEditor = ({
|
||||
const editor = useCustomEditor(
|
||||
{
|
||||
editable: false,
|
||||
content:
|
||||
typeof value === "string" && value.trim() !== "" ? value : "<p></p>",
|
||||
content: typeof value === "string" && value.trim() !== "" ? value : "<p></p>",
|
||||
editorProps: {
|
||||
...CoreReadOnlyEditorProps,
|
||||
...editorProps,
|
||||
@ -44,7 +43,7 @@ export const useReadOnlyEditor = ({
|
||||
...extensions,
|
||||
],
|
||||
},
|
||||
[rerenderOnPropsChange],
|
||||
[rerenderOnPropsChange]
|
||||
);
|
||||
|
||||
const editorRef: MutableRefObject<Editor | null> = useRef(null);
|
||||
|
@ -1,21 +1,10 @@
|
||||
import { IMentionSuggestion } from "@plane/editor-types";
|
||||
import { Editor } from "@tiptap/react";
|
||||
import React, {
|
||||
forwardRef,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useState,
|
||||
} from "react";
|
||||
import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useState } from "react";
|
||||
|
||||
interface MentionListProps {
|
||||
items: IMentionSuggestion[];
|
||||
command: (item: {
|
||||
id: string;
|
||||
label: string;
|
||||
target: string;
|
||||
redirect_uri: string;
|
||||
}) => void;
|
||||
command: (item: { id: string; label: string; target: string; redirect_uri: string }) => void;
|
||||
editor: Editor;
|
||||
}
|
||||
|
||||
@ -37,9 +26,7 @@ const MentionList = forwardRef((props: MentionListProps, ref) => {
|
||||
};
|
||||
|
||||
const upHandler = () => {
|
||||
setSelectedIndex(
|
||||
(selectedIndex + props.items.length - 1) % props.items.length,
|
||||
);
|
||||
setSelectedIndex((selectedIndex + props.items.length - 1) % props.items.length);
|
||||
};
|
||||
|
||||
const downHandler = () => {
|
||||
@ -76,31 +63,27 @@ const MentionList = forwardRef((props: MentionListProps, ref) => {
|
||||
}));
|
||||
|
||||
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.map((item, index) => (
|
||||
<div
|
||||
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" : ""
|
||||
}`}
|
||||
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() !== "" ? (
|
||||
<img
|
||||
src={item.avatar}
|
||||
className="h-full w-full object-cover rounded-sm"
|
||||
alt={item.title}
|
||||
/>
|
||||
<img src={item.avatar} className="h-full w-full rounded-sm object-cover" 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]}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<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> */}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -4,11 +4,7 @@ import suggestion from "./suggestion";
|
||||
import { CustomMention } from "./custom";
|
||||
import { IMentionHighlight, IMentionSuggestion } from "@plane/editor-types";
|
||||
|
||||
export const Mentions = (
|
||||
mentionSuggestions: IMentionSuggestion[],
|
||||
mentionHighlights: IMentionHighlight[],
|
||||
readonly,
|
||||
) =>
|
||||
export const Mentions = (mentionSuggestions: IMentionSuggestion[], mentionHighlights: IMentionHighlight[], readonly) =>
|
||||
CustomMention.configure({
|
||||
HTMLAttributes: {
|
||||
class: "mention",
|
||||
|
@ -8,8 +8,7 @@ import { IMentionHighlight } from "@plane/editor-types";
|
||||
// eslint-disable-next-line import/no-anonymous-default-export
|
||||
export default (props) => {
|
||||
const router = useRouter();
|
||||
const highlights = props.extension.options
|
||||
.mentionHighlights as IMentionHighlight[];
|
||||
const highlights = props.extension.options.mentionHighlights as IMentionHighlight[];
|
||||
|
||||
const handleClick = () => {
|
||||
if (!props.extension.options.readonly) {
|
||||
@ -18,18 +17,13 @@ export default (props) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<NodeViewWrapper className="w-fit inline mention-component">
|
||||
<NodeViewWrapper className="mention-component inline w-fit">
|
||||
<span
|
||||
className={cn(
|
||||
"px-1 py-0.5 bg-custom-primary-100/20 text-custom-primary-100 rounded font-medium mention",
|
||||
{
|
||||
"text-yellow-500 bg-yellow-500/20": highlights
|
||||
? highlights.includes(props.node.attrs.id)
|
||||
: false,
|
||||
"cursor-pointer": !props.extension.options.readonly,
|
||||
// "hover:bg-custom-primary-300" : !props.extension.options.readonly && !highlights.includes(props.node.attrs.id)
|
||||
},
|
||||
)}
|
||||
className={cn("mention rounded bg-custom-primary-100/20 px-1 py-0.5 font-medium text-custom-primary-100", {
|
||||
"bg-yellow-500/20 text-yellow-500": highlights ? highlights.includes(props.node.attrs.id) : false,
|
||||
"cursor-pointer": !props.extension.options.readonly,
|
||||
// "hover:bg-custom-primary-300" : !props.extension.options.readonly && !highlights.includes(props.node.attrs.id)
|
||||
})}
|
||||
onClick={handleClick}
|
||||
data-mention-target={props.node.attrs.target}
|
||||
data-mention-id={props.node.attrs.id}
|
||||
|
@ -7,11 +7,7 @@ import { IMentionSuggestion } from "@plane/editor-types";
|
||||
|
||||
const Suggestion = (suggestions: IMentionSuggestion[]) => ({
|
||||
items: ({ query }: { query: string }) =>
|
||||
suggestions
|
||||
.filter((suggestion) =>
|
||||
suggestion.title.toLowerCase().startsWith(query.toLowerCase()),
|
||||
)
|
||||
.slice(0, 5),
|
||||
suggestions.filter((suggestion) => suggestion.title.toLowerCase().startsWith(query.toLowerCase())).slice(0, 5),
|
||||
render: () => {
|
||||
let reactRenderer: ReactRenderer | null = null;
|
||||
let popup: any | null = null;
|
||||
|
@ -134,9 +134,7 @@ export const TableItem = (editor: Editor): EditorMenuItem => ({
|
||||
export const ImageItem = (
|
||||
editor: Editor,
|
||||
uploadFile: UploadImage,
|
||||
setIsSubmitting?: (
|
||||
isSubmitting: "submitting" | "submitted" | "saved",
|
||||
) => void,
|
||||
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void
|
||||
): EditorMenuItem => ({
|
||||
name: "image",
|
||||
isActive: () => editor?.isActive("image"),
|
||||
|
@ -15,11 +15,7 @@ interface ImageNode extends ProseMirrorNode {
|
||||
const TrackImageDeletionPlugin = (deleteImage: DeleteImage): Plugin =>
|
||||
new Plugin({
|
||||
key: deleteKey,
|
||||
appendTransaction: (
|
||||
transactions: readonly Transaction[],
|
||||
oldState: EditorState,
|
||||
newState: EditorState,
|
||||
) => {
|
||||
appendTransaction: (transactions: readonly Transaction[], oldState: EditorState, newState: EditorState) => {
|
||||
const newImageSources = new Set<string>();
|
||||
newState.doc.descendants((node) => {
|
||||
if (node.type.name === IMAGE_NODE_TYPE) {
|
||||
@ -59,10 +55,7 @@ const TrackImageDeletionPlugin = (deleteImage: DeleteImage): Plugin =>
|
||||
|
||||
export default TrackImageDeletionPlugin;
|
||||
|
||||
export async function onNodeDeleted(
|
||||
src: string,
|
||||
deleteImage: DeleteImage,
|
||||
): Promise<void> {
|
||||
export async function onNodeDeleted(src: string, deleteImage: DeleteImage): Promise<void> {
|
||||
try {
|
||||
const assetUrlWithWorkspaceId = new URL(src).pathname.substring(1);
|
||||
const resStatus = await deleteImage(assetUrlWithWorkspaceId);
|
||||
@ -74,10 +67,7 @@ export async function onNodeDeleted(
|
||||
}
|
||||
}
|
||||
|
||||
export async function onNodeRestored(
|
||||
src: string,
|
||||
restoreImage: RestoreImage,
|
||||
): Promise<void> {
|
||||
export async function onNodeRestored(src: string, restoreImage: RestoreImage): Promise<void> {
|
||||
try {
|
||||
const assetUrlWithWorkspaceId = new URL(src).pathname.substring(1);
|
||||
const resStatus = await restoreImage(assetUrlWithWorkspaceId);
|
||||
|
@ -21,10 +21,7 @@ const UploadImagesPlugin = (cancelUploadImage?: () => any) =>
|
||||
const placeholder = document.createElement("div");
|
||||
placeholder.setAttribute("class", "img-placeholder");
|
||||
const image = document.createElement("img");
|
||||
image.setAttribute(
|
||||
"class",
|
||||
"opacity-10 rounded-lg border border-custom-border-300",
|
||||
);
|
||||
image.setAttribute("class", "opacity-10 rounded-lg border border-custom-border-300");
|
||||
image.src = src;
|
||||
placeholder.appendChild(image);
|
||||
|
||||
@ -42,10 +39,7 @@ const UploadImagesPlugin = (cancelUploadImage?: () => any) =>
|
||||
// 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 parser = new DOMParser();
|
||||
const svgElement = parser.parseFromString(
|
||||
svgString,
|
||||
"image/svg+xml",
|
||||
).documentElement;
|
||||
const svgElement = parser.parseFromString(svgString, "image/svg+xml").documentElement;
|
||||
|
||||
cancelButton.appendChild(svgElement);
|
||||
placeholder.appendChild(cancelButton);
|
||||
@ -54,13 +48,7 @@ const UploadImagesPlugin = (cancelUploadImage?: () => any) =>
|
||||
});
|
||||
set = set.add(tr.doc, [deco]);
|
||||
} else if (action && action.remove) {
|
||||
set = set.remove(
|
||||
set.find(
|
||||
undefined,
|
||||
undefined,
|
||||
(spec) => spec.id == action.remove.id,
|
||||
),
|
||||
);
|
||||
set = set.remove(set.find(undefined, undefined, (spec) => spec.id == action.remove.id));
|
||||
}
|
||||
return set;
|
||||
},
|
||||
@ -76,11 +64,7 @@ export default UploadImagesPlugin;
|
||||
|
||||
function findPlaceholder(state: EditorState, id: {}) {
|
||||
const decos = uploadKey.getState(state);
|
||||
const found = decos.find(
|
||||
undefined,
|
||||
undefined,
|
||||
(spec: { id: number | undefined }) => spec.id == id,
|
||||
);
|
||||
const found = decos.find(undefined, undefined, (spec: { id: number | undefined }) => spec.id == id);
|
||||
return found.length ? found[0].from : null;
|
||||
}
|
||||
|
||||
@ -96,9 +80,7 @@ export async function startImageUpload(
|
||||
view: EditorView,
|
||||
pos: number,
|
||||
uploadFile: UploadImage,
|
||||
setIsSubmitting?: (
|
||||
isSubmitting: "submitting" | "submitted" | "saved",
|
||||
) => void,
|
||||
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void
|
||||
) {
|
||||
if (!file) {
|
||||
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 node = schema.nodes.image.create({ src: imageSrc });
|
||||
const transaction = view.state.tr
|
||||
.replaceWith(pos, pos, node)
|
||||
.setMeta(uploadKey, { remove: { id } });
|
||||
const transaction = view.state.tr.replaceWith(pos, pos, node).setMeta(uploadKey, { remove: { id } });
|
||||
view.dispatch(transaction);
|
||||
} catch (error) {
|
||||
console.error("Upload error: ", error);
|
||||
@ -161,10 +141,7 @@ export async function startImageUpload(
|
||||
}
|
||||
}
|
||||
|
||||
const UploadImageHandler = (
|
||||
file: File,
|
||||
uploadFile: UploadImage,
|
||||
): Promise<string> => {
|
||||
const UploadImageHandler = (file: File, uploadFile: UploadImage): Promise<string> => {
|
||||
try {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
try {
|
||||
|
@ -5,9 +5,7 @@ import { startImageUpload } from "./plugins/upload-image";
|
||||
|
||||
export function CoreEditorProps(
|
||||
uploadFile: UploadImage,
|
||||
setIsSubmitting?: (
|
||||
isSubmitting: "submitting" | "submitted" | "saved",
|
||||
) => void,
|
||||
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void
|
||||
): EditorProps {
|
||||
return {
|
||||
attributes: {
|
||||
@ -34,11 +32,7 @@ export function CoreEditorProps(
|
||||
}
|
||||
}
|
||||
}
|
||||
if (
|
||||
event.clipboardData &&
|
||||
event.clipboardData.files &&
|
||||
event.clipboardData.files[0]
|
||||
) {
|
||||
if (event.clipboardData && event.clipboardData.files && event.clipboardData.files[0]) {
|
||||
event.preventDefault();
|
||||
const file = event.clipboardData.files[0];
|
||||
const pos = view.state.selection.from;
|
||||
@ -57,12 +51,7 @@ export function CoreEditorProps(
|
||||
}
|
||||
}
|
||||
}
|
||||
if (
|
||||
!moved &&
|
||||
event.dataTransfer &&
|
||||
event.dataTransfer.files &&
|
||||
event.dataTransfer.files[0]
|
||||
) {
|
||||
if (!moved && event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files[0]) {
|
||||
event.preventDefault();
|
||||
const file = event.dataTransfer.files[0];
|
||||
const coordinates = view.posAtCoords({
|
||||
@ -70,13 +59,7 @@ export function CoreEditorProps(
|
||||
top: event.clientY,
|
||||
});
|
||||
if (coordinates) {
|
||||
startImageUpload(
|
||||
file,
|
||||
view,
|
||||
coordinates.pos - 1,
|
||||
uploadFile,
|
||||
setIsSubmitting,
|
||||
);
|
||||
startImageUpload(file, view, coordinates.pos - 1, uploadFile, setIsSubmitting);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
@ -45,8 +45,7 @@ export const CoreReadOnlyEditorExtensions = (mentionConfig: {
|
||||
},
|
||||
code: {
|
||||
HTMLAttributes: {
|
||||
class:
|
||||
"rounded-md bg-custom-primary-30 mx-1 px-1 py-1 font-mono font-medium text-custom-text-1000",
|
||||
class: "rounded-md bg-custom-primary-30 mx-1 px-1 py-1 font-mono font-medium text-custom-text-1000",
|
||||
spellcheck: "false",
|
||||
},
|
||||
},
|
||||
@ -94,9 +93,5 @@ export const CoreReadOnlyEditorExtensions = (mentionConfig: {
|
||||
TableHeader,
|
||||
TableCell,
|
||||
TableRow,
|
||||
Mentions(
|
||||
mentionConfig.mentionSuggestions,
|
||||
mentionConfig.mentionHighlights,
|
||||
true,
|
||||
),
|
||||
Mentions(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/pm": "^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-config-next": "13.2.4",
|
||||
"react-popper": "^2.3.0",
|
||||
|
@ -1,6 +1,3 @@
|
||||
export { DocumentEditor, DocumentEditorWithRef } from "./ui";
|
||||
export {
|
||||
DocumentReadOnlyEditor,
|
||||
DocumentReadOnlyEditorWithRef,
|
||||
} from "./ui/readonly";
|
||||
export { DocumentReadOnlyEditor, DocumentReadOnlyEditorWithRef } from "./ui/readonly";
|
||||
export { FixedMenu } from "./ui/menu/fixed-menu";
|
||||
|
@ -12,7 +12,7 @@ export const AlertLabel = (props: IAlertLabelProps) => {
|
||||
|
||||
return (
|
||||
<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" />}
|
||||
<span>{label}</span>
|
||||
|
@ -1,8 +1,4 @@
|
||||
import {
|
||||
HeadingComp,
|
||||
HeadingThreeComp,
|
||||
SubheadingComp,
|
||||
} from "./heading-component";
|
||||
import { HeadingComp, HeadingThreeComp, SubheadingComp } from "./heading-component";
|
||||
import { IMarking } from "..";
|
||||
import { Editor } from "@tiptap/react";
|
||||
import { scrollSummary } from "../utils/editor-summary-utils";
|
||||
@ -16,32 +12,21 @@ export const ContentBrowser = (props: ContentBrowserProps) => {
|
||||
const { editor, markings } = props;
|
||||
|
||||
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>
|
||||
<div className="h-full overflow-y-auto">
|
||||
{markings.length !== 0 ? (
|
||||
markings.map((marking) =>
|
||||
marking.level === 1 ? (
|
||||
<HeadingComp
|
||||
onClick={() => scrollSummary(editor, marking)}
|
||||
heading={marking.text}
|
||||
/>
|
||||
<HeadingComp onClick={() => scrollSummary(editor, marking)} heading={marking.text} />
|
||||
) : marking.level === 2 ? (
|
||||
<SubheadingComp
|
||||
onClick={() => scrollSummary(editor, marking)}
|
||||
subHeading={marking.text}
|
||||
/>
|
||||
<SubheadingComp onClick={() => scrollSummary(editor, marking)} subHeading={marking.text} />
|
||||
) : (
|
||||
<HeadingThreeComp
|
||||
heading={marking.text}
|
||||
onClick={() => scrollSummary(editor, marking)}
|
||||
/>
|
||||
),
|
||||
<HeadingThreeComp heading={marking.text} onClick={() => scrollSummary(editor, marking)} />
|
||||
)
|
||||
)
|
||||
) : (
|
||||
<p className="mt-3 text-xs text-custom-text-400">
|
||||
Headings will be displayed here for navigation
|
||||
</p>
|
||||
<p className="mt-3 text-xs text-custom-text-400">Headings will be displayed here for navigation</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -5,10 +5,7 @@ import { FixedMenu } from "../menu";
|
||||
import { UploadImage } from "@plane/editor-types";
|
||||
import { DocumentDetails } from "../types/editor-types";
|
||||
import { AlertLabel } from "./alert-label";
|
||||
import {
|
||||
IVerticalDropdownItemProps,
|
||||
VerticalDropdownMenu,
|
||||
} from "./vertical-dropdown-menu";
|
||||
import { IVerticalDropdownItemProps, VerticalDropdownMenu } from "./vertical-dropdown-menu";
|
||||
import { SummaryPopover } from "./summary-popover";
|
||||
import { InfoPopover } from "./info-popover";
|
||||
|
||||
@ -23,9 +20,7 @@ interface IEditorHeader {
|
||||
archivedAt?: Date;
|
||||
readonly: boolean;
|
||||
uploadFile?: UploadImage;
|
||||
setIsSubmitting?: (
|
||||
isSubmitting: "submitting" | "submitted" | "saved",
|
||||
) => void;
|
||||
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void;
|
||||
documentDetails: DocumentDetails;
|
||||
isSubmitting?: "submitting" | "submitted" | "saved";
|
||||
}
|
||||
@ -48,8 +43,8 @@ export const EditorHeader = (props: IEditorHeader) => {
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div className="flex items-center border-b border-custom-border-200 py-2 px-5">
|
||||
<div className="flex-shrink-0 w-56 lg:w-72">
|
||||
<div className="flex items-center border-b border-custom-border-200 px-5 py-2">
|
||||
<div className="w-56 flex-shrink-0 lg:w-72">
|
||||
<SummaryPopover
|
||||
editor={editor}
|
||||
markings={markings}
|
||||
@ -60,15 +55,11 @@ export const EditorHeader = (props: IEditorHeader) => {
|
||||
|
||||
<div className="flex-shrink-0">
|
||||
{!readonly && uploadFile && (
|
||||
<FixedMenu
|
||||
editor={editor}
|
||||
uploadFile={uploadFile}
|
||||
setIsSubmitting={setIsSubmitting}
|
||||
/>
|
||||
<FixedMenu editor={editor} uploadFile={uploadFile} setIsSubmitting={setIsSubmitting} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex-grow flex items-center justify-end gap-3">
|
||||
<div className="flex flex-grow items-center justify-end gap-3">
|
||||
{isLocked && (
|
||||
<AlertLabel
|
||||
Icon={Lock}
|
||||
@ -88,7 +79,7 @@ export const EditorHeader = (props: IEditorHeader) => {
|
||||
|
||||
{!isLocked && !isArchived ? (
|
||||
<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"
|
||||
}`}
|
||||
>
|
||||
|
@ -23,7 +23,7 @@ export const SubheadingComp = ({
|
||||
}) => (
|
||||
<p
|
||||
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"
|
||||
>
|
||||
{subHeading}
|
||||
@ -39,7 +39,7 @@ export const HeadingThreeComp = ({
|
||||
}) => (
|
||||
<p
|
||||
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"
|
||||
>
|
||||
{heading}
|
||||
|
@ -19,10 +19,7 @@ const renderDate = (date: Date): string => {
|
||||
hour12: true,
|
||||
};
|
||||
|
||||
const formattedDate: string = new Intl.DateTimeFormat(
|
||||
"en-US",
|
||||
options,
|
||||
).format(date);
|
||||
const formattedDate: string = new Intl.DateTimeFormat("en-US", options).format(date);
|
||||
|
||||
return formattedDate;
|
||||
};
|
||||
@ -32,42 +29,35 @@ export const InfoPopover: React.FC<Props> = (props) => {
|
||||
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false);
|
||||
|
||||
const [referenceElement, setReferenceElement] =
|
||||
useState<HTMLButtonElement | null>(null);
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(
|
||||
null,
|
||||
);
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||
|
||||
const { styles: infoPopoverStyles, attributes: infoPopoverAttributes } =
|
||||
usePopper(referenceElement, popperElement, {
|
||||
placement: "bottom-start",
|
||||
});
|
||||
const { styles: infoPopoverStyles, attributes: infoPopoverAttributes } = usePopper(referenceElement, popperElement, {
|
||||
placement: "bottom-start",
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
onMouseEnter={() => setIsPopoverOpen(true)}
|
||||
onMouseLeave={() => setIsPopoverOpen(false)}
|
||||
>
|
||||
<div onMouseEnter={() => setIsPopoverOpen(true)} onMouseLeave={() => setIsPopoverOpen(false)}>
|
||||
<button type="button" ref={setReferenceElement} className="block">
|
||||
<Info className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
{isPopoverOpen && (
|
||||
<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}
|
||||
style={infoPopoverStyles.popper}
|
||||
{...infoPopoverAttributes.popper}
|
||||
>
|
||||
<div className="space-y-1.5">
|
||||
<h6 className="text-custom-text-400 text-xs">Last updated on</h6>
|
||||
<h5 className="text-sm flex items-center gap-1">
|
||||
<h6 className="text-xs text-custom-text-400">Last updated on</h6>
|
||||
<h5 className="flex items-center gap-1 text-sm">
|
||||
<History className="h-3 w-3" />
|
||||
{renderDate(new Date(documentDetails.last_updated_at))}
|
||||
</h5>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<h6 className="text-custom-text-400 text-xs">Created on</h6>
|
||||
<h5 className="text-sm flex items-center gap-1">
|
||||
<h6 className="text-xs text-custom-text-400">Created on</h6>
|
||||
<h5 className="flex items-center gap-1 text-sm">
|
||||
<Calendar className="h-3 w-3" />
|
||||
{renderDate(new Date(documentDetails.created_on))}
|
||||
</h5>
|
||||
|
@ -25,14 +25,7 @@ const debounce = (func: (...args: any[]) => void, wait: number) => {
|
||||
};
|
||||
|
||||
export const PageRenderer = (props: IPageRenderer) => {
|
||||
const {
|
||||
documentDetails,
|
||||
editor,
|
||||
editorClassNames,
|
||||
editorContentCustomClassNames,
|
||||
updatePageTitle,
|
||||
readonly,
|
||||
} = props;
|
||||
const { documentDetails, editor, editorClassNames, editorContentCustomClassNames, updatePageTitle, readonly } = props;
|
||||
|
||||
const [pageTitle, setPagetitle] = useState(documentDetails.title);
|
||||
|
||||
@ -44,27 +37,24 @@ export const PageRenderer = (props: IPageRenderer) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full pl-7 pt-5 pb-64">
|
||||
<div className="w-full pb-64 pl-7 pt-5">
|
||||
{!readonly ? (
|
||||
<input
|
||||
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}
|
||||
/>
|
||||
) : (
|
||||
<input
|
||||
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}
|
||||
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}>
|
||||
<EditorContentWrapper
|
||||
editor={editor}
|
||||
editorContentCustomClassNames={editorContentCustomClassNames}
|
||||
/>
|
||||
<EditorContentWrapper editor={editor} editorContentCustomClassNames={editorContentCustomClassNames} />
|
||||
</EditorContainer>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -17,26 +17,24 @@ type Props = {
|
||||
export const SummaryPopover: React.FC<Props> = (props) => {
|
||||
const { editor, markings, sidePeekVisible, setSidePeekVisible } = props;
|
||||
|
||||
const [referenceElement, setReferenceElement] =
|
||||
useState<HTMLButtonElement | null>(null);
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(
|
||||
null,
|
||||
);
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLButtonElement | null>(null);
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||
|
||||
const { styles: summaryPopoverStyles, attributes: summaryPopoverAttributes } =
|
||||
usePopper(referenceElement, popperElement, {
|
||||
const { styles: summaryPopoverStyles, attributes: summaryPopoverAttributes } = usePopper(
|
||||
referenceElement,
|
||||
popperElement,
|
||||
{
|
||||
placement: "bottom-start",
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="group/summary-popover w-min whitespace-nowrap">
|
||||
<button
|
||||
type="button"
|
||||
ref={setReferenceElement}
|
||||
className={`h-7 w-7 grid place-items-center rounded ${
|
||||
sidePeekVisible
|
||||
? "bg-custom-primary-100/20 text-custom-primary-100"
|
||||
: "text-custom-text-300"
|
||||
className={`grid h-7 w-7 place-items-center rounded ${
|
||||
sidePeekVisible ? "bg-custom-primary-100/20 text-custom-primary-100" : "text-custom-text-300"
|
||||
}`}
|
||||
onClick={() => setSidePeekVisible(!sidePeekVisible)}
|
||||
>
|
||||
@ -44,7 +42,7 @@ export const SummaryPopover: React.FC<Props> = (props) => {
|
||||
</button>
|
||||
{!sidePeekVisible && (
|
||||
<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}
|
||||
style={summaryPopoverStyles.popper}
|
||||
{...summaryPopoverAttributes.popper}
|
||||
|
@ -8,14 +8,10 @@ interface ISummarySideBarProps {
|
||||
sidePeekVisible: boolean;
|
||||
}
|
||||
|
||||
export const SummarySideBar = ({
|
||||
editor,
|
||||
markings,
|
||||
sidePeekVisible,
|
||||
}: ISummarySideBarProps) => {
|
||||
export const SummarySideBar = ({ editor, markings, sidePeekVisible }: ISummarySideBarProps) => {
|
||||
return (
|
||||
<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"
|
||||
}`}
|
||||
>
|
||||
|
@ -23,11 +23,7 @@ export interface IVerticalDropdownMenuProps {
|
||||
items: IVerticalDropdownItemProps[];
|
||||
}
|
||||
|
||||
const VerticalDropdownItem = ({
|
||||
Icon,
|
||||
label,
|
||||
action,
|
||||
}: IVerticalDropdownItemProps) => {
|
||||
const VerticalDropdownItem = ({ Icon, label, action }: IVerticalDropdownItemProps) => {
|
||||
return (
|
||||
<CustomMenu.MenuItem onClick={action} className="flex items-center gap-2">
|
||||
<Icon className="h-3 w-3" />
|
||||
@ -42,19 +38,11 @@ export const VerticalDropdownMenu = ({ items }: IVerticalDropdownMenuProps) => {
|
||||
maxHeight={"md"}
|
||||
className={"h-4.5 mt-1"}
|
||||
placement={"bottom-start"}
|
||||
optionsClassName={
|
||||
"border-custom-border border-r border-solid transition-all duration-200 ease-in-out "
|
||||
}
|
||||
optionsClassName={"border-custom-border border-r border-solid transition-all duration-200 ease-in-out "}
|
||||
customButton={<MoreVertical size={14} />}
|
||||
>
|
||||
{items.map((item, index) => (
|
||||
<VerticalDropdownItem
|
||||
key={index}
|
||||
type={item.type}
|
||||
Icon={item.Icon}
|
||||
label={item.label}
|
||||
action={item.action}
|
||||
/>
|
||||
<VerticalDropdownItem key={index} type={item.type} Icon={item.Icon} label={item.label} action={item.action} />
|
||||
))}
|
||||
</CustomMenu>
|
||||
);
|
||||
|
@ -11,23 +11,22 @@ import { LayersIcon } from "@plane/ui";
|
||||
export const DocumentEditorExtensions = (
|
||||
uploadFile: UploadImage,
|
||||
issueEmbedConfig?: IIssueEmbedConfig,
|
||||
setIsSubmitting?: (
|
||||
isSubmitting: "submitting" | "submitted" | "saved",
|
||||
) => void,
|
||||
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void
|
||||
) => {
|
||||
const additonalOptions: ISlashCommandItem[] = [
|
||||
const additionalOptions: ISlashCommandItem[] = [
|
||||
{
|
||||
title: "Issue Embed",
|
||||
description: "Embed an issue from the project",
|
||||
searchTerms: ["Issue", "Iss"],
|
||||
icon: <LayersIcon height={"20px"} width={"20px"} />,
|
||||
key: "issue_embed",
|
||||
title: "Issue embed",
|
||||
description: "Embed an issue from the project.",
|
||||
searchTerms: ["issue", "link", "embed"],
|
||||
icon: <LayersIcon className="h-3.5 w-3.5" />,
|
||||
command: ({ editor, range }) => {
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.insertContentAt(
|
||||
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();
|
||||
},
|
||||
@ -35,7 +34,7 @@ export const DocumentEditorExtensions = (
|
||||
];
|
||||
|
||||
return [
|
||||
SlashCommand(uploadFile, setIsSubmitting, additonalOptions),
|
||||
SlashCommand(uploadFile, setIsSubmitting, additionalOptions),
|
||||
DragAndDrop,
|
||||
Placeholder.configure({
|
||||
placeholder: ({ node }) => {
|
||||
|
@ -18,34 +18,32 @@ export interface IIssueListSuggestion {
|
||||
}
|
||||
|
||||
export const IssueSuggestions = (suggestions: any[]) => {
|
||||
const mappedSuggestions: IIssueListSuggestion[] = suggestions.map(
|
||||
(suggestion): IIssueListSuggestion => {
|
||||
let transactionId = uuidv4();
|
||||
return {
|
||||
title: suggestion.name,
|
||||
priority: suggestion.priority.toString(),
|
||||
identifier: `${suggestion.project_detail.identifier}-${suggestion.sequence_id}`,
|
||||
state: suggestion.state_detail.name,
|
||||
command: ({ editor, range }) => {
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.insertContentAt(range, {
|
||||
type: "issue-embed-component",
|
||||
attrs: {
|
||||
entity_identifier: suggestion.id,
|
||||
id: transactionId,
|
||||
title: suggestion.name,
|
||||
project_identifier: suggestion.project_detail.identifier,
|
||||
sequence_id: suggestion.sequence_id,
|
||||
entity_name: "issue",
|
||||
},
|
||||
})
|
||||
.run();
|
||||
},
|
||||
};
|
||||
},
|
||||
);
|
||||
const mappedSuggestions: IIssueListSuggestion[] = suggestions.map((suggestion): IIssueListSuggestion => {
|
||||
let transactionId = uuidv4();
|
||||
return {
|
||||
title: suggestion.name,
|
||||
priority: suggestion.priority.toString(),
|
||||
identifier: `${suggestion.project_detail.identifier}-${suggestion.sequence_id}`,
|
||||
state: suggestion.state_detail.name,
|
||||
command: ({ editor, range }) => {
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.insertContentAt(range, {
|
||||
type: "issue-embed-component",
|
||||
attrs: {
|
||||
entity_identifier: suggestion.id,
|
||||
id: transactionId,
|
||||
title: suggestion.name,
|
||||
project_identifier: suggestion.project_detail.identifier,
|
||||
sequence_id: suggestion.sequence_id,
|
||||
entity_name: "issue",
|
||||
},
|
||||
})
|
||||
.run();
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return IssueEmbedSuggestions.configure({
|
||||
suggestion: {
|
||||
|
@ -9,15 +9,7 @@ export const IssueEmbedSuggestions = Extension.create({
|
||||
addOptions() {
|
||||
return {
|
||||
suggestion: {
|
||||
command: ({
|
||||
editor,
|
||||
range,
|
||||
props,
|
||||
}: {
|
||||
editor: Editor;
|
||||
range: Range;
|
||||
props: any;
|
||||
}) => {
|
||||
command: ({ editor, range, props }: { editor: Editor; range: Range; props: any }) => {
|
||||
props.command({ editor, range });
|
||||
},
|
||||
},
|
||||
|
@ -1,8 +1,6 @@
|
||||
import { IIssueListSuggestion } from ".";
|
||||
|
||||
export const getIssueSuggestionItems = (
|
||||
issueSuggestions: Array<IIssueListSuggestion>,
|
||||
) => {
|
||||
export const getIssueSuggestionItems = (issueSuggestions: Array<IIssueListSuggestion>) => {
|
||||
return ({ query }: { query: string }) => {
|
||||
const search = query.toLowerCase();
|
||||
const filteredSuggestions = issueSuggestions.filter((item) => {
|
||||
|
@ -2,13 +2,7 @@ import { cn } from "@plane/editor-core";
|
||||
import { Editor } from "@tiptap/core";
|
||||
import tippy from "tippy.js";
|
||||
import { ReactRenderer } from "@tiptap/react";
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
|
||||
import { PriorityIcon } from "@plane/ui";
|
||||
|
||||
const updateScrollView = (container: HTMLElement, item: HTMLElement) => {
|
||||
@ -62,9 +56,7 @@ const IssueSuggestionList = ({
|
||||
let newDisplayedItems: { [key: string]: IssueSuggestionProps[] } = {};
|
||||
let totalLength = 0;
|
||||
sections.forEach((section) => {
|
||||
newDisplayedItems[section] = items
|
||||
.filter((item) => item.state === section)
|
||||
.slice(0, 5);
|
||||
newDisplayedItems[section] = items.filter((item) => item.state === section).slice(0, 5);
|
||||
|
||||
totalLength += newDisplayedItems[section].length;
|
||||
});
|
||||
@ -79,7 +71,7 @@ const IssueSuggestionList = ({
|
||||
command(item);
|
||||
}
|
||||
},
|
||||
[command, displayedItems, currentSection],
|
||||
[command, displayedItems, currentSection]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@ -93,22 +85,17 @@ const IssueSuggestionList = ({
|
||||
// }
|
||||
if (e.key === "ArrowUp") {
|
||||
setSelectedIndex(
|
||||
(selectedIndex + displayedItems[currentSection].length - 1) %
|
||||
displayedItems[currentSection].length,
|
||||
(selectedIndex + displayedItems[currentSection].length - 1) % displayedItems[currentSection].length
|
||||
);
|
||||
return true;
|
||||
}
|
||||
if (e.key === "ArrowDown") {
|
||||
const nextIndex =
|
||||
(selectedIndex + 1) % displayedItems[currentSection].length;
|
||||
const nextIndex = (selectedIndex + 1) % displayedItems[currentSection].length;
|
||||
setSelectedIndex(nextIndex);
|
||||
if (nextIndex === 4) {
|
||||
const nextItems = items
|
||||
.filter((item) => item.state === currentSection)
|
||||
.slice(
|
||||
displayedItems[currentSection].length,
|
||||
displayedItems[currentSection].length + 5,
|
||||
);
|
||||
.slice(displayedItems[currentSection].length, displayedItems[currentSection].length + 5);
|
||||
setDisplayedItems((prevItems) => ({
|
||||
...prevItems,
|
||||
[currentSection]: [...prevItems[currentSection], ...nextItems],
|
||||
@ -138,29 +125,17 @@ const IssueSuggestionList = ({
|
||||
return () => {
|
||||
document.removeEventListener("keydown", onKeyDown);
|
||||
};
|
||||
}, [
|
||||
displayedItems,
|
||||
selectedIndex,
|
||||
setSelectedIndex,
|
||||
selectItem,
|
||||
currentSection,
|
||||
]);
|
||||
}, [displayedItems, selectedIndex, setSelectedIndex, selectItem, currentSection]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const container = commandListContainer?.current;
|
||||
if (container) {
|
||||
const sectionContainer = container?.querySelector(
|
||||
`#${currentSection}-container`,
|
||||
) as HTMLDivElement;
|
||||
const sectionContainer = container?.querySelector(`#${currentSection}-container`) as HTMLDivElement;
|
||||
if (sectionContainer) {
|
||||
updateScrollView(container, sectionContainer);
|
||||
}
|
||||
const sectionScrollContainer = container?.querySelector(
|
||||
`#${currentSection}`,
|
||||
) as HTMLElement;
|
||||
const item = sectionScrollContainer?.children[
|
||||
selectedIndex
|
||||
] as HTMLElement;
|
||||
const sectionScrollContainer = container?.querySelector(`#${currentSection}`) as HTMLElement;
|
||||
const item = sectionScrollContainer?.children[selectedIndex] as HTMLElement;
|
||||
if (item && sectionScrollContainer) {
|
||||
updateScrollView(sectionScrollContainer, item);
|
||||
}
|
||||
@ -171,56 +146,41 @@ const IssueSuggestionList = ({
|
||||
<div
|
||||
id="issue-list-container"
|
||||
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) => {
|
||||
const sectionItems = displayedItems[section];
|
||||
return (
|
||||
sectionItems &&
|
||||
sectionItems.length > 0 && (
|
||||
<div
|
||||
className={"h-full w-full flex flex-col"}
|
||||
key={`${section}-container`}
|
||||
id={`${section}-container`}
|
||||
>
|
||||
<div className={"flex h-full w-full flex-col"} key={`${section}-container`} id={`${section}-container`}>
|
||||
<h6
|
||||
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}
|
||||
</h6>
|
||||
<div
|
||||
key={section}
|
||||
id={section}
|
||||
className={"max-h-[140px] overflow-y-scroll overflow-x-hidden"}
|
||||
>
|
||||
{sectionItems.map(
|
||||
(item: IssueSuggestionProps, index: number) => (
|
||||
<button
|
||||
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`,
|
||||
{
|
||||
"bg-custom-primary-100/5 text-custom-text-100":
|
||||
section === currentSection &&
|
||||
index === selectedIndex,
|
||||
},
|
||||
)}
|
||||
key={index}
|
||||
onClick={() => selectItem(index)}
|
||||
>
|
||||
<h5 className="text-xs text-custom-text-300 whitespace-nowrap">
|
||||
{item.identifier}
|
||||
</h5>
|
||||
<PriorityIcon priority={item.priority} />
|
||||
<div>
|
||||
<p className="flex-grow text-xs truncate">
|
||||
{item.title}
|
||||
</p>
|
||||
</div>
|
||||
</button>
|
||||
),
|
||||
)}
|
||||
<div key={section} id={section} className={"max-h-[140px] overflow-x-hidden overflow-y-scroll"}>
|
||||
{sectionItems.map((item: IssueSuggestionProps, index: number) => (
|
||||
<button
|
||||
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`,
|
||||
{
|
||||
"bg-custom-primary-100/5 text-custom-text-100":
|
||||
section === currentSection && index === selectedIndex,
|
||||
}
|
||||
)}
|
||||
key={index}
|
||||
onClick={() => selectItem(index)}
|
||||
>
|
||||
<h5 className="whitespace-nowrap text-xs text-custom-text-300">{item.identifier}</h5>
|
||||
<PriorityIcon priority={item.priority} />
|
||||
<div>
|
||||
<p className="flex-grow truncate text-xs">{item.title}</p>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
@ -5,8 +5,7 @@ interface IssueWidgetExtensionProps {
|
||||
issueEmbedConfig?: IIssueEmbedConfig;
|
||||
}
|
||||
|
||||
export const IssueWidgetExtension = ({
|
||||
issueEmbedConfig,
|
||||
}: IssueWidgetExtensionProps) => IssueWidget.configure({
|
||||
issueEmbedConfig,
|
||||
});
|
||||
export const IssueWidgetExtension = ({ issueEmbedConfig }: IssueWidgetExtensionProps) =>
|
||||
IssueWidget.configure({
|
||||
issueEmbedConfig,
|
||||
});
|
||||
|
@ -30,15 +30,13 @@ const IssueWidgetCard = (props) => {
|
||||
{loading == 0 ? (
|
||||
<div
|
||||
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">
|
||||
{issueDetails.project_detail.identifier}-{issueDetails.sequence_id}
|
||||
</h5>
|
||||
<h4 className="break-words text-sm font-medium">
|
||||
{issueDetails.name}
|
||||
</h4>
|
||||
<div className="flex items-center flex-wrap gap-x-3 gap-y-2">
|
||||
<h4 className="break-words text-sm font-medium">{issueDetails.name}</h4>
|
||||
<div className="flex flex-wrap items-center gap-x-3 gap-y-2">
|
||||
<div>
|
||||
<PriorityIcon priority={issueDetails.priority} />
|
||||
</div>
|
||||
@ -46,18 +44,13 @@ const IssueWidgetCard = (props) => {
|
||||
<AvatarGroup size="sm">
|
||||
{issueDetails.assignee_details.map((assignee) => {
|
||||
return (
|
||||
<Avatar
|
||||
key={assignee.id}
|
||||
name={assignee.display_name}
|
||||
src={assignee.avatar}
|
||||
className={"m-0"}
|
||||
/>
|
||||
<Avatar key={assignee.id} name={assignee.display_name} src={assignee.avatar} className={"m-0"} />
|
||||
);
|
||||
})}
|
||||
</AvatarGroup>
|
||||
</div>
|
||||
{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} />
|
||||
{new Date(issueDetails.target_date).toLocaleDateString()}
|
||||
</div>
|
||||
@ -65,17 +58,15 @@ const IssueWidgetCard = (props) => {
|
||||
</div>
|
||||
</div>
|
||||
) : 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"} />
|
||||
{
|
||||
"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 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.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={"60%"} />
|
||||
</div>
|
||||
|
@ -35,10 +35,7 @@ export const IssueWidget = Node.create({
|
||||
|
||||
addNodeView() {
|
||||
return ReactNodeViewRenderer((props: Object) => (
|
||||
<IssueWidgetCard
|
||||
{...props}
|
||||
issueEmbedConfig={this.options.issueEmbedConfig}
|
||||
/>
|
||||
<IssueWidgetCard {...props} issueEmbedConfig={this.options.issueEmbedConfig} />
|
||||
));
|
||||
},
|
||||
|
||||
|
@ -5,5 +5,5 @@ export interface IEmbedConfig {
|
||||
export interface IIssueEmbedConfig {
|
||||
fetchIssue: (issueId: string) => Promise<any>;
|
||||
clickAction: (issueId: string, issueTitle: string) => void;
|
||||
issues: Array<any>;
|
||||
issues: Array<any>;
|
||||
}
|
||||
|
@ -15,21 +15,14 @@ export const useEditorMarkings = () => {
|
||||
nodes.forEach((node) => {
|
||||
if (
|
||||
node.type === "heading" &&
|
||||
(node.attrs.level === 1 ||
|
||||
node.attrs.level === 2 ||
|
||||
node.attrs.level === 3) &&
|
||||
(node.attrs.level === 1 || node.attrs.level === 2 || node.attrs.level === 3) &&
|
||||
node.content
|
||||
) {
|
||||
tempMarkings.push({
|
||||
type: "heading",
|
||||
level: node.attrs.level,
|
||||
text: node.content[0].text,
|
||||
sequence:
|
||||
node.attrs.level === 1
|
||||
? ++h1Sequence
|
||||
: node.attrs.level === 2
|
||||
? ++h2Sequence
|
||||
: ++h3Sequence,
|
||||
sequence: node.attrs.level === 1 ? ++h1Sequence : node.attrs.level === 2 ? ++h2Sequence : ++h3Sequence,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -2,11 +2,7 @@
|
||||
import React, { useState } from "react";
|
||||
import { getEditorClassNames, useEditor } from "@plane/editor-core";
|
||||
import { DocumentEditorExtensions } from "./extensions";
|
||||
import {
|
||||
IDuplicationConfig,
|
||||
IPageArchiveConfig,
|
||||
IPageLockConfig,
|
||||
} from "./types/menu-actions";
|
||||
import { IDuplicationConfig, IPageArchiveConfig, IPageLockConfig } from "./types/menu-actions";
|
||||
import { EditorHeader } from "./components/editor-header";
|
||||
import { useEditorMarkings } from "./hooks/use-editor-markings";
|
||||
import { SummarySideBar } from "./components/summary-side-bar";
|
||||
@ -41,9 +37,7 @@ interface IDocumentEditor {
|
||||
customClassName?: string;
|
||||
editorContentCustomClassNames?: string;
|
||||
onChange: (json: any, html: string) => void;
|
||||
setIsSubmitting?: (
|
||||
isSubmitting: "submitting" | "submitted" | "saved",
|
||||
) => void;
|
||||
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void;
|
||||
setShouldShowAlert?: (showAlert: boolean) => void;
|
||||
forwardedRef?: any;
|
||||
updatePageTitle: (title: string) => Promise<void>;
|
||||
@ -118,11 +112,7 @@ const DocumentEditor = ({
|
||||
cancelUploadImage,
|
||||
rerenderOnPropsChange,
|
||||
forwardedRef,
|
||||
extensions: DocumentEditorExtensions(
|
||||
uploadFile,
|
||||
embedConfig?.issueEmbedConfig,
|
||||
setIsSubmitting,
|
||||
),
|
||||
extensions: DocumentEditorExtensions(uploadFile, embedConfig?.issueEmbedConfig, setIsSubmitting),
|
||||
});
|
||||
|
||||
if (!editor) {
|
||||
@ -147,7 +137,7 @@ const DocumentEditor = ({
|
||||
if (!editor) return null;
|
||||
|
||||
return (
|
||||
<div className="h-full w-full flex flex-col overflow-hidden">
|
||||
<div className="flex h-full w-full flex-col overflow-hidden">
|
||||
<EditorHeader
|
||||
readonly={false}
|
||||
KanbanMenuOptions={KanbanMenuOptions}
|
||||
@ -163,13 +153,9 @@ const DocumentEditor = ({
|
||||
documentDetails={documentDetails}
|
||||
isSubmitting={isSubmitting}
|
||||
/>
|
||||
<div className="h-full w-full flex overflow-y-auto">
|
||||
<div className="flex-shrink-0 h-full w-56 lg:w-72 sticky top-0">
|
||||
<SummarySideBar
|
||||
editor={editor}
|
||||
markings={markings}
|
||||
sidePeekVisible={sidePeekVisible}
|
||||
/>
|
||||
<div className="flex h-full w-full overflow-y-auto">
|
||||
<div className="sticky top-0 h-full w-56 flex-shrink-0 lg:w-72">
|
||||
<SummarySideBar editor={editor} markings={markings} sidePeekVisible={sidePeekVisible} />
|
||||
</div>
|
||||
<div className="h-full w-[calc(100%-14rem)] lg:w-[calc(100%-18rem-18rem)]">
|
||||
<PageRenderer
|
||||
@ -181,15 +167,15 @@ const DocumentEditor = ({
|
||||
updatePageTitle={updatePageTitle}
|
||||
/>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
const DocumentEditorWithRef = React.forwardRef<EditorHandle, IDocumentEditor>(
|
||||
(props, ref) => <DocumentEditor {...props} forwardedRef={ref} />,
|
||||
);
|
||||
const DocumentEditorWithRef = React.forwardRef<EditorHandle, IDocumentEditor>((props, ref) => (
|
||||
<DocumentEditor {...props} forwardedRef={ref} />
|
||||
));
|
||||
|
||||
DocumentEditorWithRef.displayName = "DocumentEditorWithRef";
|
||||
|
||||
|
@ -1,6 +1,4 @@
|
||||
import { Editor } from "@tiptap/react";
|
||||
import { BoldIcon } from "lucide-react";
|
||||
|
||||
import {
|
||||
BoldItem,
|
||||
BulletListItem,
|
||||
@ -18,22 +16,16 @@ import {
|
||||
HeadingTwoItem,
|
||||
HeadingThreeItem,
|
||||
findTableAncestor,
|
||||
EditorMenuItem,
|
||||
} from "@plane/editor-core";
|
||||
import { UploadImage } from "@plane/editor-types";
|
||||
|
||||
export interface BubbleMenuItem {
|
||||
name: string;
|
||||
isActive: () => boolean;
|
||||
command: () => void;
|
||||
icon: typeof BoldIcon;
|
||||
}
|
||||
export type BubbleMenuItem = EditorMenuItem;
|
||||
|
||||
type EditorBubbleMenuProps = {
|
||||
editor: Editor;
|
||||
uploadFile: UploadImage;
|
||||
setIsSubmitting?: (
|
||||
isSubmitting: "submitting" | "submitted" | "saved",
|
||||
) => void;
|
||||
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void;
|
||||
};
|
||||
|
||||
export const FixedMenu = (props: EditorBubbleMenuProps) => {
|
||||
@ -49,15 +41,9 @@ export const FixedMenu = (props: EditorBubbleMenuProps) => {
|
||||
StrikeThroughItem(editor),
|
||||
];
|
||||
|
||||
const listItems: BubbleMenuItem[] = [
|
||||
BulletListItem(editor),
|
||||
NumberedListItem(editor),
|
||||
];
|
||||
const listItems: BubbleMenuItem[] = [BulletListItem(editor), NumberedListItem(editor)];
|
||||
|
||||
const userActionItems: BubbleMenuItem[] = [
|
||||
QuoteItem(editor),
|
||||
CodeItem(editor),
|
||||
];
|
||||
const userActionItems: BubbleMenuItem[] = [QuoteItem(editor), CodeItem(editor)];
|
||||
|
||||
function getComplexItems(): BubbleMenuItem[] {
|
||||
const items: BubbleMenuItem[] = [TableItem(editor)];
|
||||
@ -99,10 +85,10 @@ export const FixedMenu = (props: EditorBubbleMenuProps) => {
|
||||
type="button"
|
||||
onClick={item.command}
|
||||
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" />
|
||||
@ -116,10 +102,10 @@ export const FixedMenu = (props: EditorBubbleMenuProps) => {
|
||||
type="button"
|
||||
onClick={item.command}
|
||||
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
|
||||
@ -137,10 +123,10 @@ export const FixedMenu = (props: EditorBubbleMenuProps) => {
|
||||
type="button"
|
||||
onClick={item.command}
|
||||
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
|
||||
@ -158,10 +144,10 @@ export const FixedMenu = (props: EditorBubbleMenuProps) => {
|
||||
type="button"
|
||||
onClick={item.command}
|
||||
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
|
||||
|
@ -6,9 +6,5 @@ type Props = {
|
||||
};
|
||||
|
||||
export const Icon: React.FC<Props> = ({ iconName, className = "" }) => (
|
||||
<span
|
||||
className={`material-symbols-rounded text-sm leading-5 font-light ${className}`}
|
||||
>
|
||||
{iconName}
|
||||
</span>
|
||||
<span className={`material-symbols-rounded text-sm font-light leading-5 ${className}`}>{iconName}</span>
|
||||
);
|
||||
|
@ -8,11 +8,7 @@ import { IssueWidgetExtension } from "../extensions/widgets/IssueEmbedWidget";
|
||||
import { IEmbedConfig } from "../extensions/widgets/IssueEmbedWidget/types";
|
||||
import { useEditorMarkings } from "../hooks/use-editor-markings";
|
||||
import { DocumentDetails } from "../types/editor-types";
|
||||
import {
|
||||
IPageArchiveConfig,
|
||||
IPageLockConfig,
|
||||
IDuplicationConfig,
|
||||
} from "../types/menu-actions";
|
||||
import { IPageArchiveConfig, IPageLockConfig, IDuplicationConfig } from "../types/menu-actions";
|
||||
import { getMenuOptions } from "../utils/menu-options";
|
||||
|
||||
interface IDocumentReadOnlyEditor {
|
||||
@ -67,9 +63,7 @@ const DocumentReadOnlyEditor = ({
|
||||
value,
|
||||
forwardedRef,
|
||||
rerenderOnPropsChange,
|
||||
extensions: [
|
||||
IssueWidgetExtension({ issueEmbedConfig: embedConfig?.issueEmbedConfig }),
|
||||
],
|
||||
extensions: [IssueWidgetExtension({ issueEmbedConfig: embedConfig?.issueEmbedConfig })],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
@ -98,7 +92,7 @@ const DocumentReadOnlyEditor = ({
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="h-full w-full flex flex-col overflow-hidden">
|
||||
<div className="flex h-full w-full flex-col overflow-hidden">
|
||||
<EditorHeader
|
||||
isLocked={!pageLockConfig ? false : pageLockConfig.is_locked}
|
||||
isArchived={!pageArchiveConfig ? false : pageArchiveConfig.is_archived}
|
||||
@ -111,13 +105,9 @@ const DocumentReadOnlyEditor = ({
|
||||
documentDetails={documentDetails}
|
||||
archivedAt={pageArchiveConfig && pageArchiveConfig.archived_at}
|
||||
/>
|
||||
<div className="h-full w-full flex overflow-y-auto">
|
||||
<div className="flex-shrink-0 h-full w-56 lg:w-80 sticky top-0">
|
||||
<SummarySideBar
|
||||
editor={editor}
|
||||
markings={markings}
|
||||
sidePeekVisible={sidePeekVisible}
|
||||
/>
|
||||
<div className="flex h-full w-full overflow-y-auto">
|
||||
<div className="sticky top-0 h-full w-56 flex-shrink-0 lg:w-80">
|
||||
<SummarySideBar editor={editor} markings={markings} sidePeekVisible={sidePeekVisible} />
|
||||
</div>
|
||||
<div className="h-full w-full">
|
||||
<PageRenderer
|
||||
@ -128,16 +118,15 @@ const DocumentReadOnlyEditor = ({
|
||||
documentDetails={documentDetails}
|
||||
/>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
const DocumentReadOnlyEditorWithRef = forwardRef<
|
||||
EditorHandle,
|
||||
IDocumentReadOnlyEditor
|
||||
>((props, ref) => <DocumentReadOnlyEditor {...props} forwardedRef={ref} />);
|
||||
const DocumentReadOnlyEditorWithRef = forwardRef<EditorHandle, IDocumentReadOnlyEditor>((props, ref) => (
|
||||
<DocumentReadOnlyEditor {...props} forwardedRef={ref} />
|
||||
));
|
||||
|
||||
DocumentReadOnlyEditorWithRef.displayName = "DocumentReadOnlyEditorWithRef";
|
||||
|
||||
|
@ -51,17 +51,11 @@ export const Tooltip: React.FC<Props> = ({
|
||||
content={
|
||||
<div
|
||||
className={`relative z-50 max-w-xs gap-1 rounded-md p-2 text-xs shadow-md ${
|
||||
theme === "custom"
|
||||
? "bg-custom-background-100 text-custom-text-200"
|
||||
: "bg-black text-gray-400"
|
||||
} break-words overflow-hidden ${className}`}
|
||||
theme === "custom" ? "bg-custom-background-100 text-custom-text-200" : "bg-black text-gray-400"
|
||||
} overflow-hidden break-words ${className}`}
|
||||
>
|
||||
{tooltipHeading && (
|
||||
<h5
|
||||
className={`font-medium ${
|
||||
theme === "custom" ? "text-custom-text-100" : "text-white"
|
||||
}`}
|
||||
>
|
||||
<h5 className={`font-medium ${theme === "custom" ? "text-custom-text-100" : "text-white"}`}>
|
||||
{tooltipHeading}
|
||||
</h5>
|
||||
)}
|
||||
@ -69,11 +63,7 @@ export const Tooltip: React.FC<Props> = ({
|
||||
</div>
|
||||
}
|
||||
position={position}
|
||||
renderTarget={({
|
||||
isOpen: isTooltipOpen,
|
||||
ref: eleReference,
|
||||
...tooltipProps
|
||||
}) =>
|
||||
renderTarget={({ isOpen: isTooltipOpen, ref: eleReference, ...tooltipProps }) =>
|
||||
React.cloneElement(children, {
|
||||
ref: eleReference,
|
||||
...tooltipProps,
|
||||
|
@ -12,11 +12,7 @@ import {
|
||||
} from "lucide-react";
|
||||
import { NextRouter } from "next/router";
|
||||
import { IVerticalDropdownItemProps } from "../components/vertical-dropdown-menu";
|
||||
import {
|
||||
IDuplicationConfig,
|
||||
IPageArchiveConfig,
|
||||
IPageLockConfig,
|
||||
} from "../types/menu-actions";
|
||||
import { IDuplicationConfig, IPageArchiveConfig, IPageLockConfig } from "../types/menu-actions";
|
||||
import { copyMarkdownToClipboard, CopyPageLink } from "./menu-actions";
|
||||
|
||||
export interface MenuOptionsProps {
|
||||
@ -90,8 +86,7 @@ export const getMenuOptions = ({
|
||||
.then(() => {
|
||||
onActionCompleteHandler({
|
||||
title: "Page Copied",
|
||||
message:
|
||||
"Page has been copied as 'Copy of' followed by page title",
|
||||
message: "Page has been copied as 'Copy of' followed by page title",
|
||||
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": "*",
|
||||
"eslint": "8.36.0",
|
||||
"eslint-config-next": "13.2.4",
|
||||
"lucide-react": "^0.244.0",
|
||||
"lucide-react": "^0.293.0",
|
||||
"tippy.js": "^6.3.7",
|
||||
"@tiptap/pm": "^2.1.7"
|
||||
},
|
||||
|
@ -43,24 +43,22 @@ function absoluteRect(node: Element) {
|
||||
}
|
||||
|
||||
function nodeDOMAtCoords(coords: { x: number; y: number }) {
|
||||
return document
|
||||
.elementsFromPoint(coords.x, coords.y)
|
||||
.find((elem: Element) => {
|
||||
return (
|
||||
elem.parentElement?.matches?.(".ProseMirror") ||
|
||||
elem.matches(
|
||||
[
|
||||
"li",
|
||||
"p:not(:first-child)",
|
||||
"pre",
|
||||
"blockquote",
|
||||
"h1, h2, h3",
|
||||
"[data-type=horizontalRule]",
|
||||
".tableWrapper",
|
||||
].join(", "),
|
||||
)
|
||||
);
|
||||
});
|
||||
return document.elementsFromPoint(coords.x, coords.y).find((elem: Element) => {
|
||||
return (
|
||||
elem.parentElement?.matches?.(".ProseMirror") ||
|
||||
elem.matches(
|
||||
[
|
||||
"li",
|
||||
"p:not(:first-child)",
|
||||
"pre",
|
||||
"blockquote",
|
||||
"h1, h2, h3",
|
||||
"[data-type=horizontalRule]",
|
||||
".tableWrapper",
|
||||
].join(", ")
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function nodePosAtDOM(node: Element, view: EditorView) {
|
||||
@ -104,9 +102,7 @@ function DragHandle(options: DragHandleOptions) {
|
||||
const nodePos = nodePosAtDOM(node, view);
|
||||
if (nodePos === null || nodePos === undefined || nodePos < 0) return;
|
||||
|
||||
view.dispatch(
|
||||
view.state.tr.setSelection(NodeSelection.create(view.state.doc, nodePos)),
|
||||
);
|
||||
view.dispatch(view.state.tr.setSelection(NodeSelection.create(view.state.doc, nodePos)));
|
||||
|
||||
const slice = view.state.selection.content();
|
||||
const { dom, text } = __serializeForClipboard(view, slice);
|
||||
@ -137,9 +133,7 @@ function DragHandle(options: DragHandleOptions) {
|
||||
|
||||
if (nodePos === null || nodePos === undefined || nodePos < 0) return;
|
||||
|
||||
view.dispatch(
|
||||
view.state.tr.setSelection(NodeSelection.create(view.state.doc, nodePos)),
|
||||
);
|
||||
view.dispatch(view.state.tr.setSelection(NodeSelection.create(view.state.doc, nodePos)));
|
||||
}
|
||||
|
||||
let dragHandleElement: HTMLElement | null = null;
|
||||
|
@ -1,28 +1,21 @@
|
||||
import {
|
||||
useState,
|
||||
useEffect,
|
||||
useCallback,
|
||||
ReactNode,
|
||||
useRef,
|
||||
useLayoutEffect,
|
||||
} from "react";
|
||||
import { useState, useEffect, useCallback, ReactNode, useRef, useLayoutEffect } from "react";
|
||||
import { Editor, Range, Extension } from "@tiptap/core";
|
||||
import Suggestion from "@tiptap/suggestion";
|
||||
import { ReactRenderer } from "@tiptap/react";
|
||||
import tippy from "tippy.js";
|
||||
import type { UploadImage, ISlashCommandItem, CommandProps } from "@plane/editor-types";
|
||||
import type { UploadImage, ISlashCommandItem, CommandProps } from "@plane/editor-types";
|
||||
import {
|
||||
CaseSensitive,
|
||||
Code2,
|
||||
Heading1,
|
||||
Heading2,
|
||||
Heading3,
|
||||
ImageIcon,
|
||||
List,
|
||||
ListOrdered,
|
||||
Text,
|
||||
TextQuote,
|
||||
Code,
|
||||
ListTodo,
|
||||
MinusSquare,
|
||||
CheckSquare,
|
||||
ImageIcon,
|
||||
Quote,
|
||||
Table,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
@ -39,6 +32,7 @@ import {
|
||||
} from "@plane/editor-core";
|
||||
|
||||
interface CommandItemProps {
|
||||
key: string;
|
||||
title: string;
|
||||
description: string;
|
||||
icon: ReactNode;
|
||||
@ -50,15 +44,7 @@ const Command = Extension.create({
|
||||
return {
|
||||
suggestion: {
|
||||
char: "/",
|
||||
command: ({
|
||||
editor,
|
||||
range,
|
||||
props,
|
||||
}: {
|
||||
editor: Editor;
|
||||
range: Range;
|
||||
props: any;
|
||||
}) => {
|
||||
command: ({ editor, range, props }: { editor: Editor; range: Range; props: any }) => {
|
||||
props.command({ editor, range });
|
||||
},
|
||||
},
|
||||
@ -80,149 +66,152 @@ const Command = Extension.create({
|
||||
const getSuggestionItems =
|
||||
(
|
||||
uploadFile: UploadImage,
|
||||
setIsSubmitting?: (
|
||||
isSubmitting: "submitting" | "submitted" | "saved",
|
||||
) => void,
|
||||
additonalOptions?: Array<ISlashCommandItem>
|
||||
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void,
|
||||
additionalOptions?: Array<ISlashCommandItem>
|
||||
) =>
|
||||
({ query }: { query: string }) => {
|
||||
let slashCommands: ISlashCommandItem[] = [
|
||||
{
|
||||
title: "Text",
|
||||
description: "Just start typing with plain text.",
|
||||
searchTerms: ["p", "paragraph"],
|
||||
icon: <Text size={18} />,
|
||||
command: ({ editor, range }: CommandProps) => {
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.deleteRange(range)
|
||||
.toggleNode("paragraph", "paragraph")
|
||||
.run();
|
||||
},
|
||||
({ query }: { query: string }) => {
|
||||
let slashCommands: ISlashCommandItem[] = [
|
||||
{
|
||||
key: "text",
|
||||
title: "Text",
|
||||
description: "Just start typing with plain text.",
|
||||
searchTerms: ["p", "paragraph"],
|
||||
icon: <CaseSensitive className="h-3.5 w-3.5" />,
|
||||
command: ({ editor, range }: CommandProps) => {
|
||||
editor.chain().focus().deleteRange(range).toggleNode("paragraph", "paragraph").run();
|
||||
},
|
||||
{
|
||||
title: "Heading 1",
|
||||
description: "Big section heading.",
|
||||
searchTerms: ["title", "big", "large"],
|
||||
icon: <Heading1 size={18} />,
|
||||
command: ({ editor, range }: CommandProps) => {
|
||||
toggleHeadingOne(editor, range);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "heading_1",
|
||||
title: "Heading 1",
|
||||
description: "Big section heading.",
|
||||
searchTerms: ["title", "big", "large"],
|
||||
icon: <Heading1 className="h-3.5 w-3.5" />,
|
||||
command: ({ editor, range }: CommandProps) => {
|
||||
toggleHeadingOne(editor, range);
|
||||
},
|
||||
{
|
||||
title: "Heading 2",
|
||||
description: "Medium section heading.",
|
||||
searchTerms: ["subtitle", "medium"],
|
||||
icon: <Heading2 size={18} />,
|
||||
command: ({ editor, range }: CommandProps) => {
|
||||
toggleHeadingTwo(editor, range);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "heading_2",
|
||||
title: "Heading 2",
|
||||
description: "Medium section heading.",
|
||||
searchTerms: ["subtitle", "medium"],
|
||||
icon: <Heading2 className="h-3.5 w-3.5" />,
|
||||
command: ({ editor, range }: CommandProps) => {
|
||||
toggleHeadingTwo(editor, range);
|
||||
},
|
||||
{
|
||||
title: "Heading 3",
|
||||
description: "Small section heading.",
|
||||
searchTerms: ["subtitle", "small"],
|
||||
icon: <Heading3 size={18} />,
|
||||
command: ({ editor, range }: CommandProps) => {
|
||||
toggleHeadingThree(editor, range);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "heading_3",
|
||||
title: "Heading 3",
|
||||
description: "Small section heading.",
|
||||
searchTerms: ["subtitle", "small"],
|
||||
icon: <Heading3 className="h-3.5 w-3.5" />,
|
||||
command: ({ editor, range }: CommandProps) => {
|
||||
toggleHeadingThree(editor, range);
|
||||
},
|
||||
{
|
||||
title: "To-do List",
|
||||
description: "Track tasks with a to-do list.",
|
||||
searchTerms: ["todo", "task", "list", "check", "checkbox"],
|
||||
icon: <CheckSquare size={18} />,
|
||||
command: ({ editor, range }: CommandProps) => {
|
||||
toggleTaskList(editor, range);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "todo_list",
|
||||
title: "To do",
|
||||
description: "Track tasks with a to-do list.",
|
||||
searchTerms: ["todo", "task", "list", "check", "checkbox"],
|
||||
icon: <ListTodo className="h-3.5 w-3.5" />,
|
||||
command: ({ editor, range }: CommandProps) => {
|
||||
toggleTaskList(editor, range);
|
||||
},
|
||||
{
|
||||
title: "Bullet List",
|
||||
description: "Create a simple bullet list.",
|
||||
searchTerms: ["unordered", "point"],
|
||||
icon: <List size={18} />,
|
||||
command: ({ editor, range }: CommandProps) => {
|
||||
toggleBulletList(editor, range);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "bullet_list",
|
||||
title: "Bullet list",
|
||||
description: "Create a simple bullet list.",
|
||||
searchTerms: ["unordered", "point"],
|
||||
icon: <List className="h-3.5 w-3.5" />,
|
||||
command: ({ editor, range }: CommandProps) => {
|
||||
toggleBulletList(editor, range);
|
||||
},
|
||||
{
|
||||
title: "Divider",
|
||||
description: "Visually divide blocks",
|
||||
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();
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "numbered_list",
|
||||
title: "Numbered list",
|
||||
description: "Create a list with numbering.",
|
||||
searchTerms: ["ordered"],
|
||||
icon: <ListOrdered className="h-3.5 w-3.5" />,
|
||||
command: ({ editor, range }: CommandProps) => {
|
||||
toggleOrderedList(editor, range);
|
||||
},
|
||||
{
|
||||
title: "Table",
|
||||
description: "Create a Table",
|
||||
searchTerms: ["table", "cell", "db", "data", "tabular"],
|
||||
icon: <Table size={18} />,
|
||||
command: ({ editor, range }: CommandProps) => {
|
||||
insertTableCommand(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);
|
||||
},
|
||||
{
|
||||
title: "Numbered List",
|
||||
description: "Create a list with numbering.",
|
||||
searchTerms: ["ordered"],
|
||||
icon: <ListOrdered size={18} />,
|
||||
command: ({ editor, range }: CommandProps) => {
|
||||
toggleOrderedList(editor, range);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "quote_block",
|
||||
title: "Quote",
|
||||
description: "Capture a quote.",
|
||||
searchTerms: ["blockquote"],
|
||||
icon: <Quote className="h-3.5 w-3.5" />,
|
||||
command: ({ editor, range }: CommandProps) => toggleBlockquote(editor, range),
|
||||
},
|
||||
{
|
||||
key: "code_block",
|
||||
title: "Code",
|
||||
description: "Capture a code snippet.",
|
||||
searchTerms: ["codeblock"],
|
||||
icon: <Code2 className="h-3.5 w-3.5" />,
|
||||
command: ({ editor, range }: CommandProps) =>
|
||||
// @ts-expect-error I have to move this to the core
|
||||
editor.chain().focus().deleteRange(range).toggleCodeBlock().run(),
|
||||
},
|
||||
{
|
||||
key: "image",
|
||||
title: "Image",
|
||||
description: "Upload an image from your computer.",
|
||||
searchTerms: ["photo", "picture", "media"],
|
||||
icon: <ImageIcon className="h-3.5 w-3.5" />,
|
||||
command: ({ editor, range }: CommandProps) => {
|
||||
insertImageCommand(editor, uploadFile, setIsSubmitting, range);
|
||||
},
|
||||
{
|
||||
title: "Quote",
|
||||
description: "Capture a quote.",
|
||||
searchTerms: ["blockquote"],
|
||||
icon: <TextQuote size={18} />,
|
||||
command: ({ editor, range }: CommandProps) =>
|
||||
toggleBlockquote(editor, 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();
|
||||
},
|
||||
{
|
||||
title: "Code",
|
||||
description: "Capture a code snippet.",
|
||||
searchTerms: ["codeblock"],
|
||||
icon: <Code size={18} />,
|
||||
command: ({ editor, range }: CommandProps) =>
|
||||
// @ts-expect-error I have to move this to the core
|
||||
editor.chain().focus().deleteRange(range).toggleCodeBlock().run(),
|
||||
},
|
||||
{
|
||||
title: "Image",
|
||||
description: "Upload an image from your computer.",
|
||||
searchTerms: ["photo", "picture", "media"],
|
||||
icon: <ImageIcon size={18} />,
|
||||
command: ({ editor, range }: CommandProps) => {
|
||||
insertImageCommand(editor, uploadFile, setIsSubmitting, range);
|
||||
},
|
||||
},
|
||||
]
|
||||
},
|
||||
];
|
||||
|
||||
if (additonalOptions) {
|
||||
additonalOptions.map(item => {
|
||||
slashCommands.push(item)
|
||||
})
|
||||
if (additionalOptions) {
|
||||
additionalOptions.map((item) => {
|
||||
slashCommands.push(item);
|
||||
});
|
||||
}
|
||||
|
||||
slashCommands = slashCommands.filter((item) => {
|
||||
if (typeof query === "string" && query.length > 0) {
|
||||
const search = query.toLowerCase();
|
||||
return (
|
||||
item.title.toLowerCase().includes(search) ||
|
||||
item.description.toLowerCase().includes(search) ||
|
||||
(item.searchTerms && item.searchTerms.some((term: string) => term.includes(search)))
|
||||
);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
slashCommands = slashCommands.filter((item) => {
|
||||
if (typeof query === "string" && query.length > 0) {
|
||||
const search = query.toLowerCase();
|
||||
return (
|
||||
item.title.toLowerCase().includes(search) ||
|
||||
item.description.toLowerCase().includes(search) ||
|
||||
(item.searchTerms &&
|
||||
item.searchTerms.some((term: string) => term.includes(search)))
|
||||
);
|
||||
}
|
||||
return true;
|
||||
})
|
||||
|
||||
return slashCommands
|
||||
};
|
||||
return slashCommands;
|
||||
};
|
||||
|
||||
export const updateScrollView = (container: HTMLElement, item: HTMLElement) => {
|
||||
const containerHeight = container.offsetHeight;
|
||||
@ -238,15 +227,7 @@ export const updateScrollView = (container: HTMLElement, item: HTMLElement) => {
|
||||
}
|
||||
};
|
||||
|
||||
const CommandList = ({
|
||||
items,
|
||||
command,
|
||||
}: {
|
||||
items: CommandItemProps[];
|
||||
command: any;
|
||||
editor: any;
|
||||
range: any;
|
||||
}) => {
|
||||
const CommandList = ({ items, command }: { items: CommandItemProps[]; command: any; editor: any; range: any }) => {
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
|
||||
const selectItem = useCallback(
|
||||
@ -256,7 +237,7 @@ const CommandList = ({
|
||||
command(item);
|
||||
}
|
||||
},
|
||||
[command, items],
|
||||
[command, items]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
@ -303,27 +284,21 @@ const CommandList = ({
|
||||
<div
|
||||
id="slash-command"
|
||||
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
|
||||
key={item.key}
|
||||
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":
|
||||
index === selectedIndex,
|
||||
},
|
||||
"bg-custom-primary-100/5": index === selectedIndex,
|
||||
}
|
||||
)}
|
||||
key={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">
|
||||
{item.icon}
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-medium">{item.title}</p>
|
||||
<p className="text-xs text-custom-text-300">{item.description}</p>
|
||||
</div>
|
||||
<div className="grid flex-shrink-0 place-items-center">{item.icon}</div>
|
||||
<p>{item.title}</p>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@ -380,14 +355,12 @@ const renderItems = () => {
|
||||
|
||||
export const SlashCommand = (
|
||||
uploadFile: UploadImage,
|
||||
setIsSubmitting?: (
|
||||
isSubmitting: "submitting" | "submitted" | "saved",
|
||||
) => void,
|
||||
additonalOptions?: Array<ISlashCommandItem>,
|
||||
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void,
|
||||
additionalOptions?: Array<ISlashCommandItem>
|
||||
) =>
|
||||
Command.configure({
|
||||
suggestion: {
|
||||
items: getSuggestionItems(uploadFile, setIsSubmitting, additonalOptions),
|
||||
items: getSuggestionItems(uploadFile, setIsSubmitting, additionalOptions),
|
||||
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 { LiteReadOnlyEditor, LiteReadOnlyEditorWithRef } from "./ui/read-only";
|
||||
export type {
|
||||
IMentionSuggestion,
|
||||
IMentionHighlight,
|
||||
} from "@plane/editor-types";
|
||||
export type { IMentionSuggestion, IMentionHighlight } from "@plane/editor-types";
|
||||
|
@ -1,18 +1,8 @@
|
||||
import * as React from "react";
|
||||
import {
|
||||
EditorContainer,
|
||||
EditorContentWrapper,
|
||||
getEditorClassNames,
|
||||
useEditor,
|
||||
} from "@plane/editor-core";
|
||||
import { EditorContainer, EditorContentWrapper, getEditorClassNames, useEditor } from "@plane/editor-core";
|
||||
import { FixedMenu } from "./menus/fixed-menu";
|
||||
import { LiteTextEditorExtensions } from "./extensions";
|
||||
import {
|
||||
UploadImage,
|
||||
DeleteImage,
|
||||
IMentionSuggestion,
|
||||
RestoreImage,
|
||||
} from "@plane/editor-types";
|
||||
import { UploadImage, DeleteImage, IMentionSuggestion, RestoreImage } from "@plane/editor-types";
|
||||
|
||||
interface ILiteTextEditor {
|
||||
value: string;
|
||||
@ -25,9 +15,7 @@ interface ILiteTextEditor {
|
||||
customClassName?: string;
|
||||
editorContentCustomClassNames?: string;
|
||||
onChange?: (json: any, html: string) => void;
|
||||
setIsSubmitting?: (
|
||||
isSubmitting: "submitting" | "submitted" | "saved",
|
||||
) => void;
|
||||
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void;
|
||||
setShouldShowAlert?: (showAlert: boolean) => void;
|
||||
forwardedRef?: any;
|
||||
debouncedUpdatesEnabled?: boolean;
|
||||
@ -107,11 +95,8 @@ const LiteTextEditor = (props: LiteTextEditorProps) => {
|
||||
return (
|
||||
<EditorContainer editor={editor} editorClassNames={editorClassNames}>
|
||||
<div className="flex flex-col">
|
||||
<EditorContentWrapper
|
||||
editor={editor}
|
||||
editorContentCustomClassNames={editorContentCustomClassNames}
|
||||
/>
|
||||
<div className="w-full mt-4">
|
||||
<EditorContentWrapper editor={editor} editorContentCustomClassNames={editorContentCustomClassNames} />
|
||||
<div className="mt-4 w-full">
|
||||
<FixedMenu
|
||||
editor={editor}
|
||||
uploadFile={uploadFile}
|
||||
@ -125,9 +110,9 @@ const LiteTextEditor = (props: LiteTextEditorProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
const LiteTextEditorWithRef = React.forwardRef<EditorHandle, ILiteTextEditor>(
|
||||
(props, ref) => <LiteTextEditor {...props} forwardedRef={ref} />,
|
||||
);
|
||||
const LiteTextEditorWithRef = React.forwardRef<EditorHandle, ILiteTextEditor>((props, ref) => (
|
||||
<LiteTextEditor {...props} forwardedRef={ref} />
|
||||
));
|
||||
|
||||
LiteTextEditorWithRef.displayName = "LiteTextEditorWithRef";
|
||||
|
||||
|
@ -6,9 +6,5 @@ type Props = {
|
||||
};
|
||||
|
||||
export const Icon: React.FC<Props> = ({ iconName, className = "" }) => (
|
||||
<span
|
||||
className={`material-symbols-rounded text-sm leading-5 font-light ${className}`}
|
||||
>
|
||||
{iconName}
|
||||
</span>
|
||||
<span className={`material-symbols-rounded text-sm font-light leading-5 ${className}`}>{iconName}</span>
|
||||
);
|
||||
|
@ -47,9 +47,7 @@ type EditorBubbleMenuProps = {
|
||||
| undefined;
|
||||
};
|
||||
uploadFile: UploadImage;
|
||||
setIsSubmitting?: (
|
||||
isSubmitting: "submitting" | "submitted" | "saved",
|
||||
) => void;
|
||||
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void;
|
||||
submitButton: React.ReactNode;
|
||||
};
|
||||
|
||||
@ -61,23 +59,15 @@ export const FixedMenu = (props: EditorBubbleMenuProps) => {
|
||||
StrikeThroughItem(props.editor),
|
||||
];
|
||||
|
||||
const listFormattingItems: BubbleMenuItem[] = [
|
||||
BulletListItem(props.editor),
|
||||
NumberedListItem(props.editor),
|
||||
];
|
||||
const listFormattingItems: BubbleMenuItem[] = [BulletListItem(props.editor), NumberedListItem(props.editor)];
|
||||
|
||||
const userActionItems: BubbleMenuItem[] = [
|
||||
QuoteItem(props.editor),
|
||||
CodeItem(props.editor),
|
||||
];
|
||||
const userActionItems: BubbleMenuItem[] = [QuoteItem(props.editor), CodeItem(props.editor)];
|
||||
|
||||
function getComplexItems(): BubbleMenuItem[] {
|
||||
const items: BubbleMenuItem[] = [TableItem(props.editor)];
|
||||
|
||||
if (shouldShowImageItem()) {
|
||||
items.push(
|
||||
ImageItem(props.editor, props.uploadFile, props.setIsSubmitting),
|
||||
);
|
||||
items.push(ImageItem(props.editor, props.uploadFile, props.setIsSubmitting));
|
||||
}
|
||||
|
||||
return items;
|
||||
@ -109,22 +99,20 @@ export const FixedMenu = (props: EditorBubbleMenuProps) => {
|
||||
};
|
||||
|
||||
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 && (
|
||||
<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) => (
|
||||
<Tooltip key={access.key} tooltipContent={access.label}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleAccessChange(access.key)}
|
||||
className={`aspect-square grid place-items-center p-1 rounded-sm hover:bg-custom-background-90 ${
|
||||
props.commentAccessSpecifier?.accessValue === access.key
|
||||
? "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 ? "bg-custom-background-90" : ""
|
||||
}`}
|
||||
>
|
||||
<access.icon
|
||||
className={`w-3.5 h-3.5 ${
|
||||
className={`h-3.5 w-3.5 ${
|
||||
props.commentAccessSpecifier?.accessValue === access.key
|
||||
? "text-custom-text-100"
|
||||
: "text-custom-text-400"
|
||||
@ -136,23 +124,19 @@ export const FixedMenu = (props: EditorBubbleMenuProps) => {
|
||||
))}
|
||||
</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 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) => (
|
||||
<Tooltip
|
||||
key={index}
|
||||
tooltipContent={<span className="capitalize">{item.name}</span>}
|
||||
>
|
||||
<Tooltip key={index} tooltipContent={<span className="capitalize">{item.name}</span>}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={item.command}
|
||||
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":
|
||||
item.isActive(),
|
||||
},
|
||||
"bg-custom-background-80 text-custom-text-100": item.isActive(),
|
||||
}
|
||||
)}
|
||||
>
|
||||
<item.icon
|
||||
@ -165,21 +149,17 @@ export const FixedMenu = (props: EditorBubbleMenuProps) => {
|
||||
</Tooltip>
|
||||
))}
|
||||
</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) => (
|
||||
<Tooltip
|
||||
key={index}
|
||||
tooltipContent={<span className="capitalize">{item.name}</span>}
|
||||
>
|
||||
<Tooltip key={index} tooltipContent={<span className="capitalize">{item.name}</span>}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={item.command}
|
||||
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":
|
||||
item.isActive(),
|
||||
},
|
||||
"bg-custom-background-80 text-custom-text-100": item.isActive(),
|
||||
}
|
||||
)}
|
||||
>
|
||||
<item.icon
|
||||
@ -192,21 +172,17 @@ export const FixedMenu = (props: EditorBubbleMenuProps) => {
|
||||
</Tooltip>
|
||||
))}
|
||||
</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) => (
|
||||
<Tooltip
|
||||
key={index}
|
||||
tooltipContent={<span className="capitalize">{item.name}</span>}
|
||||
>
|
||||
<Tooltip key={index} tooltipContent={<span className="capitalize">{item.name}</span>}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={item.command}
|
||||
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":
|
||||
item.isActive(),
|
||||
},
|
||||
"bg-custom-background-80 text-custom-text-100": item.isActive(),
|
||||
}
|
||||
)}
|
||||
>
|
||||
<item.icon
|
||||
@ -221,19 +197,15 @@ export const FixedMenu = (props: EditorBubbleMenuProps) => {
|
||||
</div>
|
||||
<div className="flex items-stretch gap-0.5 pl-2.5">
|
||||
{complexItems.map((item, index) => (
|
||||
<Tooltip
|
||||
key={index}
|
||||
tooltipContent={<span className="capitalize">{item.name}</span>}
|
||||
>
|
||||
<Tooltip key={index} tooltipContent={<span className="capitalize">{item.name}</span>}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={item.command}
|
||||
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":
|
||||
item.isActive(),
|
||||
},
|
||||
"bg-custom-background-80 text-custom-text-100": item.isActive(),
|
||||
}
|
||||
)}
|
||||
>
|
||||
<item.icon
|
||||
|
@ -1,10 +1,5 @@
|
||||
import * as React from "react";
|
||||
import {
|
||||
EditorContainer,
|
||||
EditorContentWrapper,
|
||||
getEditorClassNames,
|
||||
useReadOnlyEditor,
|
||||
} from "@plane/editor-core";
|
||||
import { EditorContainer, EditorContentWrapper, getEditorClassNames, useReadOnlyEditor } from "@plane/editor-core";
|
||||
|
||||
interface ICoreReadOnlyEditor {
|
||||
value: string;
|
||||
@ -50,19 +45,15 @@ const LiteReadOnlyEditor = ({
|
||||
return (
|
||||
<EditorContainer editor={editor} editorClassNames={editorClassNames}>
|
||||
<div className="flex flex-col">
|
||||
<EditorContentWrapper
|
||||
editor={editor}
|
||||
editorContentCustomClassNames={editorContentCustomClassNames}
|
||||
/>
|
||||
<EditorContentWrapper editor={editor} editorContentCustomClassNames={editorContentCustomClassNames} />
|
||||
</div>
|
||||
</EditorContainer>
|
||||
);
|
||||
};
|
||||
|
||||
const LiteReadOnlyEditorWithRef = React.forwardRef<
|
||||
EditorHandle,
|
||||
ICoreReadOnlyEditor
|
||||
>((props, ref) => <LiteReadOnlyEditor {...props} forwardedRef={ref} />);
|
||||
const LiteReadOnlyEditorWithRef = React.forwardRef<EditorHandle, ICoreReadOnlyEditor>((props, ref) => (
|
||||
<LiteReadOnlyEditor {...props} forwardedRef={ref} />
|
||||
));
|
||||
|
||||
LiteReadOnlyEditorWithRef.displayName = "LiteReadOnlyEditorWithRef";
|
||||
|
||||
|
@ -50,17 +50,11 @@ export const Tooltip: React.FC<Props> = ({
|
||||
content={
|
||||
<div
|
||||
className={`relative z-50 max-w-xs gap-1 rounded-md p-2 text-xs shadow-md ${
|
||||
theme === "custom"
|
||||
? "bg-custom-background-100 text-custom-text-200"
|
||||
: "bg-black text-gray-400"
|
||||
} break-words overflow-hidden ${className}`}
|
||||
theme === "custom" ? "bg-custom-background-100 text-custom-text-200" : "bg-black text-gray-400"
|
||||
} overflow-hidden break-words ${className}`}
|
||||
>
|
||||
{tooltipHeading && (
|
||||
<h5
|
||||
className={`font-medium ${
|
||||
theme === "custom" ? "text-custom-text-100" : "text-white"
|
||||
}`}
|
||||
>
|
||||
<h5 className={`font-medium ${theme === "custom" ? "text-custom-text-100" : "text-white"}`}>
|
||||
{tooltipHeading}
|
||||
</h5>
|
||||
)}
|
||||
@ -68,11 +62,7 @@ export const Tooltip: React.FC<Props> = ({
|
||||
</div>
|
||||
}
|
||||
position={position}
|
||||
renderTarget={({
|
||||
isOpen: isTooltipOpen,
|
||||
ref: eleReference,
|
||||
...tooltipProps
|
||||
}) =>
|
||||
renderTarget={({ isOpen: isTooltipOpen, ref: eleReference, ...tooltipProps }) =>
|
||||
React.cloneElement(children, {
|
||||
ref: eleReference,
|
||||
...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}
|
||||
setShouldShowAlert={setShowAlert}
|
||||
setIsSubmitting={setIsSubmitting}
|
||||
customClassName={
|
||||
isAllowed ? "min-h-[150px] shadow-sm" : "!p-0 !pt-2 text-custom-text-200"
|
||||
}
|
||||
customClassName={isAllowed ? "min-h-[150px] shadow-sm" : "!p-0 !pt-2 text-custom-text-200"}
|
||||
noBorder={!isAllowed}
|
||||
onChange={(description: Object, description_html: string) => {
|
||||
setShowAlert(true);
|
||||
@ -96,8 +94,5 @@ return (
|
||||
Here is an example of how to use the `RichReadOnlyEditor` component
|
||||
|
||||
```tsx
|
||||
<RichReadOnlyEditor
|
||||
value={issueDetails.description_html}
|
||||
customClassName="p-3 min-h-[50px] shadow-sm"
|
||||
/>
|
||||
<RichReadOnlyEditor value={issueDetails.description_html} customClassName="p-3 min-h-[50px] shadow-sm" />
|
||||
```
|
||||
|
@ -1,7 +1,4 @@
|
||||
export { RichTextEditor, RichTextEditorWithRef } from "./ui";
|
||||
export { RichReadOnlyEditor, RichReadOnlyEditorWithRef } from "./ui/read-only";
|
||||
export type { RichTextEditorProps, IRichTextEditor } from "./ui";
|
||||
export type {
|
||||
IMentionHighlight,
|
||||
IMentionSuggestion,
|
||||
} from "@plane/editor-types";
|
||||
export type { IMentionHighlight, IMentionSuggestion } from "@plane/editor-types";
|
||||
|
@ -5,10 +5,8 @@ import { UploadImage } from "@plane/editor-types";
|
||||
|
||||
export const RichTextEditorExtensions = (
|
||||
uploadFile: UploadImage,
|
||||
setIsSubmitting?: (
|
||||
isSubmitting: "submitting" | "submitted" | "saved",
|
||||
) => void,
|
||||
dragDropEnabled?: boolean,
|
||||
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void,
|
||||
dragDropEnabled?: boolean
|
||||
) => [
|
||||
SlashCommand(uploadFile, setIsSubmitting),
|
||||
dragDropEnabled === true && DragAndDrop,
|
||||
|
@ -1,19 +1,9 @@
|
||||
"use client";
|
||||
import * as React from "react";
|
||||
import {
|
||||
EditorContainer,
|
||||
EditorContentWrapper,
|
||||
getEditorClassNames,
|
||||
useEditor,
|
||||
} from "@plane/editor-core";
|
||||
import { EditorContainer, EditorContentWrapper, getEditorClassNames, useEditor } from "@plane/editor-core";
|
||||
import { EditorBubbleMenu } from "./menus/bubble-menu";
|
||||
import { RichTextEditorExtensions } from "./extensions";
|
||||
import {
|
||||
DeleteImage,
|
||||
IMentionSuggestion,
|
||||
RestoreImage,
|
||||
UploadImage,
|
||||
} from "@plane/editor-types";
|
||||
import { DeleteImage, IMentionSuggestion, RestoreImage, UploadImage } from "@plane/editor-types";
|
||||
|
||||
export type IRichTextEditor = {
|
||||
value: string;
|
||||
@ -31,9 +21,7 @@ export type IRichTextEditor = {
|
||||
customClassName?: string;
|
||||
editorContentCustomClassNames?: string;
|
||||
onChange?: (json: any, html: string) => void;
|
||||
setIsSubmitting?: (
|
||||
isSubmitting: "submitting" | "submitted" | "saved",
|
||||
) => void;
|
||||
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void;
|
||||
setShouldShowAlert?: (showAlert: boolean) => void;
|
||||
forwardedRef?: any;
|
||||
debouncedUpdatesEnabled?: boolean;
|
||||
@ -82,11 +70,7 @@ const RichTextEditor = ({
|
||||
restoreFile,
|
||||
forwardedRef,
|
||||
rerenderOnPropsChange,
|
||||
extensions: RichTextEditorExtensions(
|
||||
uploadFile,
|
||||
setIsSubmitting,
|
||||
dragDropEnabled,
|
||||
),
|
||||
extensions: RichTextEditorExtensions(uploadFile, setIsSubmitting, dragDropEnabled),
|
||||
mentionHighlights,
|
||||
mentionSuggestions,
|
||||
});
|
||||
@ -103,18 +87,15 @@ const RichTextEditor = ({
|
||||
<EditorContainer editor={editor} editorClassNames={editorClassNames}>
|
||||
{editor && <EditorBubbleMenu editor={editor} />}
|
||||
<div className="flex flex-col">
|
||||
<EditorContentWrapper
|
||||
editor={editor}
|
||||
editorContentCustomClassNames={editorContentCustomClassNames}
|
||||
/>
|
||||
<EditorContentWrapper editor={editor} editorContentCustomClassNames={editorContentCustomClassNames} />
|
||||
</div>
|
||||
</EditorContainer>
|
||||
);
|
||||
};
|
||||
|
||||
const RichTextEditorWithRef = React.forwardRef<EditorHandle, IRichTextEditor>(
|
||||
(props, ref) => <RichTextEditor {...props} forwardedRef={ref} />,
|
||||
);
|
||||
const RichTextEditorWithRef = React.forwardRef<EditorHandle, IRichTextEditor>((props, ref) => (
|
||||
<RichTextEditor {...props} forwardedRef={ref} />
|
||||
));
|
||||
|
||||
RichTextEditorWithRef.displayName = "RichTextEditorWithRef";
|
||||
|
||||
|
@ -123,11 +123,10 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props: any) => {
|
||||
type="button"
|
||||
onClick={item.command}
|
||||
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":
|
||||
item.isActive(),
|
||||
},
|
||||
"bg-custom-primary-100/5 text-custom-text-100": item.isActive(),
|
||||
}
|
||||
)}
|
||||
>
|
||||
<item.icon
|
||||
|
@ -1,19 +1,7 @@
|
||||
import { Editor } from "@tiptap/core";
|
||||
import { Check, Trash } from "lucide-react";
|
||||
import {
|
||||
Dispatch,
|
||||
FC,
|
||||
SetStateAction,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useRef,
|
||||
} from "react";
|
||||
import {
|
||||
cn,
|
||||
isValidHttpUrl,
|
||||
setLinkEditor,
|
||||
unsetLinkEditor,
|
||||
} from "@plane/editor-core";
|
||||
import { Dispatch, FC, SetStateAction, useCallback, useEffect, useRef } from "react";
|
||||
import { cn, isValidHttpUrl, setLinkEditor, unsetLinkEditor } from "@plane/editor-core";
|
||||
|
||||
interface LinkSelectorProps {
|
||||
editor: Editor;
|
||||
@ -21,11 +9,7 @@ interface LinkSelectorProps {
|
||||
setIsOpen: Dispatch<SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
export const LinkSelector: FC<LinkSelectorProps> = ({
|
||||
editor,
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
}) => {
|
||||
export const LinkSelector: FC<LinkSelectorProps> = ({ editor, isOpen, setIsOpen }) => {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const onLinkSubmit = useCallback(() => {
|
||||
@ -47,7 +31,7 @@ export const LinkSelector: FC<LinkSelectorProps> = ({
|
||||
type="button"
|
||||
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",
|
||||
{ "bg-custom-background-100": isOpen },
|
||||
{ "bg-custom-background-100": isOpen }
|
||||
)}
|
||||
onClick={() => {
|
||||
setIsOpen(!isOpen);
|
||||
@ -64,7 +48,7 @@ export const LinkSelector: FC<LinkSelectorProps> = ({
|
||||
</button>
|
||||
{isOpen && (
|
||||
<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) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
@ -76,7 +60,7 @@ export const LinkSelector: FC<LinkSelectorProps> = ({
|
||||
ref={inputRef}
|
||||
type="url"
|
||||
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 || ""}
|
||||
/>
|
||||
{editor.getAttributes("link").href ? (
|
||||
|
@ -21,21 +21,13 @@ interface NodeSelectorProps {
|
||||
setIsOpen: Dispatch<SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
export const NodeSelector: FC<NodeSelectorProps> = ({
|
||||
editor,
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
}) => {
|
||||
export const NodeSelector: FC<NodeSelectorProps> = ({ editor, isOpen, setIsOpen }) => {
|
||||
const items: BubbleMenuItem[] = [
|
||||
{
|
||||
name: "Text",
|
||||
icon: TextIcon,
|
||||
command: () =>
|
||||
editor.chain().focus().toggleNode("paragraph", "paragraph").run(),
|
||||
isActive: () =>
|
||||
editor.isActive("paragraph") &&
|
||||
!editor.isActive("bulletList") &&
|
||||
!editor.isActive("orderedList"),
|
||||
command: () => editor.chain().focus().toggleNode("paragraph", "paragraph").run(),
|
||||
isActive: () => editor.isActive("paragraph") && !editor.isActive("bulletList") && !editor.isActive("orderedList"),
|
||||
},
|
||||
HeadingOneItem(editor),
|
||||
HeadingTwoItem(editor),
|
||||
@ -75,9 +67,8 @@ export const NodeSelector: FC<NodeSelectorProps> = ({
|
||||
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",
|
||||
{
|
||||
"bg-custom-primary-100/5 text-custom-text-100":
|
||||
activeItem.name === item.name,
|
||||
},
|
||||
"bg-custom-primary-100/5 text-custom-text-100": activeItem.name === item.name,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center space-x-2">
|
||||
|
@ -1,10 +1,5 @@
|
||||
"use client";
|
||||
import {
|
||||
EditorContainer,
|
||||
EditorContentWrapper,
|
||||
getEditorClassNames,
|
||||
useReadOnlyEditor,
|
||||
} from "@plane/editor-core";
|
||||
import { EditorContainer, EditorContentWrapper, getEditorClassNames, useReadOnlyEditor } from "@plane/editor-core";
|
||||
import * as React from "react";
|
||||
|
||||
interface IRichTextReadOnlyEditor {
|
||||
@ -51,19 +46,15 @@ const RichReadOnlyEditor = ({
|
||||
return (
|
||||
<EditorContainer editor={editor} editorClassNames={editorClassNames}>
|
||||
<div className="flex flex-col">
|
||||
<EditorContentWrapper
|
||||
editor={editor}
|
||||
editorContentCustomClassNames={editorContentCustomClassNames}
|
||||
/>
|
||||
<EditorContentWrapper editor={editor} editorContentCustomClassNames={editorContentCustomClassNames} />
|
||||
</div>
|
||||
</EditorContainer>
|
||||
);
|
||||
};
|
||||
|
||||
const RichReadOnlyEditorWithRef = React.forwardRef<
|
||||
EditorHandle,
|
||||
IRichTextReadOnlyEditor
|
||||
>((props, ref) => <RichReadOnlyEditor {...props} forwardedRef={ref} />);
|
||||
const RichReadOnlyEditorWithRef = React.forwardRef<EditorHandle, IRichTextReadOnlyEditor>((props, ref) => (
|
||||
<RichReadOnlyEditor {...props} forwardedRef={ref} />
|
||||
));
|
||||
|
||||
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 { UploadImage } from "./types/upload-image";
|
||||
export type { RestoreImage } from "./types/restore-image";
|
||||
export type {
|
||||
IMentionHighlight,
|
||||
IMentionSuggestion,
|
||||
} from "./types/mention-suggestion";
|
||||
export type { ISlashCommandItem, CommandProps } from "./types/slash-commands-suggestion"
|
||||
export type { IMentionHighlight, IMentionSuggestion } from "./types/mention-suggestion";
|
||||
export type { ISlashCommandItem, CommandProps } from "./types/slash-commands-suggestion";
|
||||
|
@ -1,15 +1,16 @@
|
||||
import { ReactNode } from "react";
|
||||
import { Editor, Range } from "@tiptap/core"
|
||||
import { Editor, Range } from "@tiptap/core";
|
||||
|
||||
export type CommandProps = {
|
||||
editor: Editor;
|
||||
range: Range;
|
||||
}
|
||||
};
|
||||
|
||||
export type ISlashCommandItem = {
|
||||
key: string;
|
||||
title: string;
|
||||
description: string;
|
||||
searchTerms: string[];
|
||||
icon: ReactNode;
|
||||
command: ({ editor, range }: CommandProps) => void;
|
||||
}
|
||||
};
|
||||
|
@ -8,10 +8,11 @@
|
||||
"eslint": "^7.23.0",
|
||||
"eslint-config-next": "13.0.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": {
|
||||
"@typescript-eslint/eslint-plugin": "^6.13.2",
|
||||
"typescript": "^4.7.4"
|
||||
},
|
||||
"publishConfig": {
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user