forked from github/plane
[fix]: Error Handling for Images and Table Fix for Form Submissions in Editor (#2710)
* cancellable uploads and image limits with better error handling * fixed table row/column picker behaviour on modals * Merge branch 'rerender-debounce-editor-fix' into editor-draggable-nodes * fix: added mention suggestions and highlights in `create-issue-modal` * removed uncessary files * solved lint error of trailing spaces * added plane/ui dependency for tooltips --------- Co-authored-by: Henit Chobisa <chobisa.henit@gmail.com>
This commit is contained in:
parent
faaba45e59
commit
206f5744a3
@ -3,21 +3,28 @@ import TrackImageDeletionPlugin from "../../plugins/delete-image";
|
|||||||
import UploadImagesPlugin from "../../plugins/upload-image";
|
import UploadImagesPlugin from "../../plugins/upload-image";
|
||||||
import { DeleteImage } from "../../../types/delete-image";
|
import { DeleteImage } from "../../../types/delete-image";
|
||||||
|
|
||||||
const ImageExtension = (deleteImage: DeleteImage) => Image.extend({
|
const ImageExtension = (
|
||||||
addProseMirrorPlugins() {
|
deleteImage: DeleteImage,
|
||||||
return [UploadImagesPlugin(), TrackImageDeletionPlugin(deleteImage)];
|
cancelUploadImage?: () => any,
|
||||||
},
|
) =>
|
||||||
addAttributes() {
|
Image.extend({
|
||||||
return {
|
addProseMirrorPlugins() {
|
||||||
...this.parent?.(),
|
return [
|
||||||
width: {
|
UploadImagesPlugin(cancelUploadImage),
|
||||||
default: "35%",
|
TrackImageDeletionPlugin(deleteImage),
|
||||||
},
|
];
|
||||||
height: {
|
},
|
||||||
default: null,
|
addAttributes() {
|
||||||
},
|
return {
|
||||||
};
|
...this.parent?.(),
|
||||||
},
|
width: {
|
||||||
});
|
default: "35%",
|
||||||
|
},
|
||||||
|
height: {
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
export default ImageExtension;
|
export default ImageExtension;
|
||||||
|
@ -20,82 +20,89 @@ import { isValidHttpUrl } from "../../lib/utils";
|
|||||||
import { IMentionSuggestion } from "../../types/mention-suggestion";
|
import { IMentionSuggestion } from "../../types/mention-suggestion";
|
||||||
import { Mentions } from "../mentions";
|
import { Mentions } from "../mentions";
|
||||||
|
|
||||||
|
|
||||||
export const CoreEditorExtensions = (
|
export const CoreEditorExtensions = (
|
||||||
mentionConfig: { mentionSuggestions: IMentionSuggestion[], mentionHighlights: string[] },
|
mentionConfig: {
|
||||||
|
mentionSuggestions: IMentionSuggestion[];
|
||||||
|
mentionHighlights: string[];
|
||||||
|
},
|
||||||
deleteFile: DeleteImage,
|
deleteFile: DeleteImage,
|
||||||
|
cancelUploadImage?: () => any,
|
||||||
) => [
|
) => [
|
||||||
StarterKit.configure({
|
StarterKit.configure({
|
||||||
bulletList: {
|
bulletList: {
|
||||||
HTMLAttributes: {
|
HTMLAttributes: {
|
||||||
class: "list-disc list-outside leading-3 -mt-2",
|
class: "list-disc list-outside leading-3 -mt-2",
|
||||||
},
|
|
||||||
},
|
},
|
||||||
orderedList: {
|
},
|
||||||
HTMLAttributes: {
|
orderedList: {
|
||||||
class: "list-decimal list-outside leading-3 -mt-2",
|
HTMLAttributes: {
|
||||||
},
|
class: "list-decimal list-outside leading-3 -mt-2",
|
||||||
},
|
},
|
||||||
listItem: {
|
},
|
||||||
HTMLAttributes: {
|
listItem: {
|
||||||
class: "leading-normal -mb-2",
|
HTMLAttributes: {
|
||||||
},
|
class: "leading-normal -mb-2",
|
||||||
},
|
},
|
||||||
blockquote: {
|
},
|
||||||
HTMLAttributes: {
|
blockquote: {
|
||||||
class: "border-l-4 border-custom-border-300",
|
HTMLAttributes: {
|
||||||
},
|
class: "border-l-4 border-custom-border-300",
|
||||||
},
|
},
|
||||||
code: {
|
},
|
||||||
HTMLAttributes: {
|
code: {
|
||||||
class:
|
|
||||||
"rounded-md bg-custom-primary-30 mx-1 px-1 py-1 font-mono font-medium text-custom-text-1000",
|
|
||||||
spellcheck: "false",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
codeBlock: false,
|
|
||||||
horizontalRule: false,
|
|
||||||
dropcursor: {
|
|
||||||
color: "rgba(var(--color-text-100))",
|
|
||||||
width: 2,
|
|
||||||
},
|
|
||||||
gapcursor: false,
|
|
||||||
}),
|
|
||||||
Gapcursor,
|
|
||||||
TiptapLink.configure({
|
|
||||||
protocols: ["http", "https"],
|
|
||||||
validate: (url) => isValidHttpUrl(url),
|
|
||||||
HTMLAttributes: {
|
HTMLAttributes: {
|
||||||
class:
|
class:
|
||||||
"text-custom-primary-300 underline underline-offset-[3px] hover:text-custom-primary-500 transition-colors cursor-pointer",
|
"rounded-md bg-custom-primary-30 mx-1 px-1 py-1 font-mono font-medium text-custom-text-1000",
|
||||||
|
spellcheck: "false",
|
||||||
},
|
},
|
||||||
}),
|
},
|
||||||
ImageExtension(deleteFile).configure({
|
codeBlock: false,
|
||||||
HTMLAttributes: {
|
horizontalRule: false,
|
||||||
class: "rounded-lg border border-custom-border-300",
|
dropcursor: {
|
||||||
},
|
color: "rgba(var(--color-text-100))",
|
||||||
}),
|
width: 2,
|
||||||
TiptapUnderline,
|
},
|
||||||
TextStyle,
|
gapcursor: false,
|
||||||
Color,
|
}),
|
||||||
TaskList.configure({
|
Gapcursor,
|
||||||
HTMLAttributes: {
|
TiptapLink.configure({
|
||||||
class: "not-prose pl-2",
|
protocols: ["http", "https"],
|
||||||
},
|
validate: (url) => isValidHttpUrl(url),
|
||||||
}),
|
HTMLAttributes: {
|
||||||
TaskItem.configure({
|
class:
|
||||||
HTMLAttributes: {
|
"text-custom-primary-300 underline underline-offset-[3px] hover:text-custom-primary-500 transition-colors cursor-pointer",
|
||||||
class: "flex items-start my-4",
|
},
|
||||||
},
|
}),
|
||||||
nested: true,
|
ImageExtension(deleteFile, cancelUploadImage).configure({
|
||||||
}),
|
HTMLAttributes: {
|
||||||
Markdown.configure({
|
class: "rounded-lg border border-custom-border-300",
|
||||||
html: true,
|
},
|
||||||
transformCopiedText: true,
|
}),
|
||||||
}),
|
TiptapUnderline,
|
||||||
Table,
|
TextStyle,
|
||||||
TableHeader,
|
Color,
|
||||||
TableCell,
|
TaskList.configure({
|
||||||
TableRow,
|
HTMLAttributes: {
|
||||||
Mentions(mentionConfig.mentionSuggestions, mentionConfig.mentionHighlights, false),
|
class: "not-prose pl-2",
|
||||||
];
|
},
|
||||||
|
}),
|
||||||
|
TaskItem.configure({
|
||||||
|
HTMLAttributes: {
|
||||||
|
class: "flex items-start my-4",
|
||||||
|
},
|
||||||
|
nested: true,
|
||||||
|
}),
|
||||||
|
Markdown.configure({
|
||||||
|
html: true,
|
||||||
|
transformCopiedText: true,
|
||||||
|
}),
|
||||||
|
Table,
|
||||||
|
TableHeader,
|
||||||
|
TableCell,
|
||||||
|
TableRow,
|
||||||
|
Mentions(
|
||||||
|
mentionConfig.mentionSuggestions,
|
||||||
|
mentionConfig.mentionHighlights,
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
@ -202,6 +202,7 @@ function createToolbox({
|
|||||||
"div",
|
"div",
|
||||||
{
|
{
|
||||||
className: "toolboxItem",
|
className: "toolboxItem",
|
||||||
|
itemType: "button",
|
||||||
onClick() {
|
onClick() {
|
||||||
onClickItem(item);
|
onClickItem(item);
|
||||||
},
|
},
|
||||||
@ -253,6 +254,7 @@ function createColorPickerToolbox({
|
|||||||
"div",
|
"div",
|
||||||
{
|
{
|
||||||
className: "toolboxItem",
|
className: "toolboxItem",
|
||||||
|
itemType: "button",
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
onSelectColor(value);
|
onSelectColor(value);
|
||||||
colorPicker.hide();
|
colorPicker.hide();
|
||||||
@ -331,7 +333,9 @@ export class TableView implements NodeView {
|
|||||||
this.rowsControl = h(
|
this.rowsControl = h(
|
||||||
"div",
|
"div",
|
||||||
{ className: "rowsControl" },
|
{ className: "rowsControl" },
|
||||||
h("button", {
|
h("div", {
|
||||||
|
itemType: "button",
|
||||||
|
className: "rowsControlDiv",
|
||||||
onClick: () => this.selectRow(),
|
onClick: () => this.selectRow(),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@ -339,7 +343,9 @@ export class TableView implements NodeView {
|
|||||||
this.columnsControl = h(
|
this.columnsControl = h(
|
||||||
"div",
|
"div",
|
||||||
{ className: "columnsControl" },
|
{ className: "columnsControl" },
|
||||||
h("button", {
|
h("div", {
|
||||||
|
itemType: "button",
|
||||||
|
className: "columnsControlDiv",
|
||||||
onClick: () => this.selectColumn(),
|
onClick: () => this.selectColumn(),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@ -352,7 +358,7 @@ export class TableView implements NodeView {
|
|||||||
);
|
);
|
||||||
|
|
||||||
this.columnsToolbox = createToolbox({
|
this.columnsToolbox = createToolbox({
|
||||||
triggerButton: this.columnsControl.querySelector("button"),
|
triggerButton: this.columnsControl.querySelector(".columnsControlDiv"),
|
||||||
items: columnsToolboxItems,
|
items: columnsToolboxItems,
|
||||||
tippyOptions: {
|
tippyOptions: {
|
||||||
...defaultTippyOptions,
|
...defaultTippyOptions,
|
||||||
|
@ -29,11 +29,13 @@ interface CustomEditorProps {
|
|||||||
forwardedRef?: any;
|
forwardedRef?: any;
|
||||||
mentionHighlights?: string[];
|
mentionHighlights?: string[];
|
||||||
mentionSuggestions?: IMentionSuggestion[];
|
mentionSuggestions?: IMentionSuggestion[];
|
||||||
|
cancelUploadImage?: () => any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useEditor = ({
|
export const useEditor = ({
|
||||||
uploadFile,
|
uploadFile,
|
||||||
deleteFile,
|
deleteFile,
|
||||||
|
cancelUploadImage,
|
||||||
editorProps = {},
|
editorProps = {},
|
||||||
value,
|
value,
|
||||||
extensions = [],
|
extensions = [],
|
||||||
@ -42,7 +44,7 @@ export const useEditor = ({
|
|||||||
forwardedRef,
|
forwardedRef,
|
||||||
setShouldShowAlert,
|
setShouldShowAlert,
|
||||||
mentionHighlights,
|
mentionHighlights,
|
||||||
mentionSuggestions
|
mentionSuggestions,
|
||||||
}: CustomEditorProps) => {
|
}: CustomEditorProps) => {
|
||||||
const editor = useCustomEditor(
|
const editor = useCustomEditor(
|
||||||
{
|
{
|
||||||
@ -50,7 +52,17 @@ export const useEditor = ({
|
|||||||
...CoreEditorProps(uploadFile, setIsSubmitting),
|
...CoreEditorProps(uploadFile, setIsSubmitting),
|
||||||
...editorProps,
|
...editorProps,
|
||||||
},
|
},
|
||||||
extensions: [...CoreEditorExtensions({ mentionSuggestions: mentionSuggestions ?? [], mentionHighlights: mentionHighlights ?? []}, deleteFile), ...extensions],
|
extensions: [
|
||||||
|
...CoreEditorExtensions(
|
||||||
|
{
|
||||||
|
mentionSuggestions: mentionSuggestions ?? [],
|
||||||
|
mentionHighlights: mentionHighlights ?? [],
|
||||||
|
},
|
||||||
|
deleteFile,
|
||||||
|
cancelUploadImage,
|
||||||
|
),
|
||||||
|
...extensions,
|
||||||
|
],
|
||||||
content:
|
content:
|
||||||
typeof value === "string" && value.trim() !== "" ? value : "<p></p>",
|
typeof value === "string" && value.trim() !== "" ? value : "<p></p>",
|
||||||
onUpdate: async ({ editor }) => {
|
onUpdate: async ({ editor }) => {
|
||||||
@ -82,4 +94,5 @@ export const useEditor = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return editor;
|
return editor;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,16 +0,0 @@
|
|||||||
const InsertBottomTableIcon = (props: any) => (
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width={24}
|
|
||||||
height={24}
|
|
||||||
viewBox="0 -960 960 960"
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M212.309-152.31q-30.308 0-51.308-21t-21-51.307V-360q0-30.307 21-51.307 21-21 51.308-21h535.382q30.308 0 51.308 21t21 51.307v135.383q0 30.307-21 51.307-21 21-51.308 21H212.309Zm0-375.383q-30.308 0-51.308-21t-21-51.307v-135.383q0-30.307 21-51.307 21-21 51.308-21h535.382q30.308 0 51.308 21t21 51.307V-600q0 30.307-21 51.307-21 21-51.308 21H212.309Zm535.382-219.998H212.309q-4.616 0-8.463 3.846-3.846 3.846-3.846 8.462V-600q0 4.616 3.846 8.462 3.847 3.847 8.463 3.847h535.382q4.616 0 8.463-3.847Q760-595.384 760-600v-135.383q0-4.616-3.846-8.462-3.847-3.846-8.463-3.846ZM200-587.691v-160 160Z"
|
|
||||||
fill="rgb(var(--color-text-300))"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default InsertBottomTableIcon;
|
|
@ -1,15 +0,0 @@
|
|||||||
const InsertLeftTableIcon = (props: any) => (
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width={24}
|
|
||||||
height={24}
|
|
||||||
viewBox="0 -960 960 960"
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M224.617-140.001q-30.307 0-51.307-21-21-21-21-51.308v-535.382q0-30.308 21-51.308t51.307-21H360q30.307 0 51.307 21 21 21 21 51.308v535.382q0 30.308-21 51.308t-51.307 21H224.617Zm375.383 0q-30.307 0-51.307-21-21-21-21-51.308v-535.382q0-30.308 21-51.308t51.307-21h135.383q30.307 0 51.307 21 21 21 21 51.308v535.382q0 30.308-21 51.308t-51.307 21H600Zm147.691-607.69q0-4.616-3.846-8.463-3.846-3.846-8.462-3.846H600q-4.616 0-8.462 3.846-3.847 3.847-3.847 8.463v535.382q0 4.616 3.847 8.463Q595.384-200 600-200h135.383q4.616 0 8.462-3.846 3.846-3.847 3.846-8.463v-535.382ZM587.691-200h160-160Z"
|
|
||||||
fill="rgb(var(--color-text-300))"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
export default InsertLeftTableIcon;
|
|
@ -1,16 +0,0 @@
|
|||||||
const InsertRightTableIcon = (props: any) => (
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width={24}
|
|
||||||
height={24}
|
|
||||||
viewBox="0 -960 960 960"
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M600-140.001q-30.307 0-51.307-21-21-21-21-51.308v-535.382q0-30.308 21-51.308t51.307-21h135.383q30.307 0 51.307 21 21 21 21 51.308v535.382q0 30.308-21 51.308t-51.307 21H600Zm-375.383 0q-30.307 0-51.307-21-21-21-21-51.308v-535.382q0-30.308 21-51.308t51.307-21H360q30.307 0 51.307 21 21 21 21 51.308v535.382q0 30.308-21 51.308t-51.307 21H224.617Zm-12.308-607.69v535.382q0 4.616 3.846 8.463 3.846 3.846 8.462 3.846H360q4.616 0 8.462-3.846 3.847-3.847 3.847-8.463v-535.382q0-4.616-3.847-8.463Q364.616-760 360-760H224.617q-4.616 0-8.462 3.846-3.846 3.847-3.846 8.463Zm160 547.691h-160 160Z"
|
|
||||||
fill="rgb(var(--color-text-300))"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default InsertRightTableIcon;
|
|
@ -1,15 +0,0 @@
|
|||||||
const InsertTopTableIcon = (props: any) => (
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width={24}
|
|
||||||
height={24}
|
|
||||||
viewBox="0 -960 960 960"
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M212.309-527.693q-30.308 0-51.308-21t-21-51.307v-135.383q0-30.307 21-51.307 21-21 51.308-21h535.382q30.308 0 51.308 21t21 51.307V-600q0 30.307-21 51.307-21 21-51.308 21H212.309Zm0 375.383q-30.308 0-51.308-21t-21-51.307V-360q0-30.307 21-51.307 21-21 51.308-21h535.382q30.308 0 51.308 21t21 51.307v135.383q0 30.307-21 51.307-21 21-51.308 21H212.309Zm0-59.999h535.382q4.616 0 8.463-3.846 3.846-3.846 3.846-8.462V-360q0-4.616-3.846-8.462-3.847-3.847-8.463-3.847H212.309q-4.616 0-8.463 3.847Q200-364.616 200-360v135.383q0 4.616 3.846 8.462 3.847 3.846 8.463 3.846Zm-12.309-160v160-160Z"
|
|
||||||
fill="rgb(var(--color-text-300))"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
export default InsertTopTableIcon;
|
|
@ -1,77 +0,0 @@
|
|||||||
import * as React from 'react';
|
|
||||||
|
|
||||||
// next-themes
|
|
||||||
import { useTheme } from "next-themes";
|
|
||||||
// tooltip2
|
|
||||||
import { Tooltip2 } from "@blueprintjs/popover2";
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
tooltipHeading?: string;
|
|
||||||
tooltipContent: string | React.ReactNode;
|
|
||||||
position?:
|
|
||||||
| "top"
|
|
||||||
| "right"
|
|
||||||
| "bottom"
|
|
||||||
| "left"
|
|
||||||
| "auto"
|
|
||||||
| "auto-end"
|
|
||||||
| "auto-start"
|
|
||||||
| "bottom-left"
|
|
||||||
| "bottom-right"
|
|
||||||
| "left-bottom"
|
|
||||||
| "left-top"
|
|
||||||
| "right-bottom"
|
|
||||||
| "right-top"
|
|
||||||
| "top-left"
|
|
||||||
| "top-right";
|
|
||||||
children: JSX.Element;
|
|
||||||
disabled?: boolean;
|
|
||||||
className?: string;
|
|
||||||
openDelay?: number;
|
|
||||||
closeDelay?: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Tooltip: React.FC<Props> = ({
|
|
||||||
tooltipHeading,
|
|
||||||
tooltipContent,
|
|
||||||
position = "top",
|
|
||||||
children,
|
|
||||||
disabled = false,
|
|
||||||
className = "",
|
|
||||||
openDelay = 200,
|
|
||||||
closeDelay,
|
|
||||||
}) => {
|
|
||||||
const { theme } = useTheme();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tooltip2
|
|
||||||
disabled={disabled}
|
|
||||||
hoverOpenDelay={openDelay}
|
|
||||||
hoverCloseDelay={closeDelay}
|
|
||||||
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}`}
|
|
||||||
>
|
|
||||||
{tooltipHeading && (
|
|
||||||
<h5
|
|
||||||
className={`font-medium ${
|
|
||||||
theme === "custom" ? "text-custom-text-100" : "text-white"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{tooltipHeading}
|
|
||||||
</h5>
|
|
||||||
)}
|
|
||||||
{tooltipContent}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
position={position}
|
|
||||||
renderTarget={({ isOpen: isTooltipOpen, ref: eleReference, ...tooltipProps }) =>
|
|
||||||
React.cloneElement(children, { ref: eleReference, ...tooltipProps, ...children.props })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
@ -15,7 +15,11 @@ interface ImageNode extends ProseMirrorNode {
|
|||||||
const TrackImageDeletionPlugin = (deleteImage: DeleteImage): Plugin =>
|
const TrackImageDeletionPlugin = (deleteImage: DeleteImage): Plugin =>
|
||||||
new Plugin({
|
new Plugin({
|
||||||
key: deleteKey,
|
key: deleteKey,
|
||||||
appendTransaction: (transactions: readonly Transaction[], oldState: EditorState, newState: EditorState) => {
|
appendTransaction: (
|
||||||
|
transactions: readonly Transaction[],
|
||||||
|
oldState: EditorState,
|
||||||
|
newState: EditorState,
|
||||||
|
) => {
|
||||||
const newImageSources = new Set<string>();
|
const newImageSources = new Set<string>();
|
||||||
newState.doc.descendants((node) => {
|
newState.doc.descendants((node) => {
|
||||||
if (node.type.name === IMAGE_NODE_TYPE) {
|
if (node.type.name === IMAGE_NODE_TYPE) {
|
||||||
@ -55,7 +59,10 @@ const TrackImageDeletionPlugin = (deleteImage: DeleteImage): Plugin =>
|
|||||||
|
|
||||||
export default TrackImageDeletionPlugin;
|
export default TrackImageDeletionPlugin;
|
||||||
|
|
||||||
async function onNodeDeleted(src: string, deleteImage: DeleteImage): Promise<void> {
|
async function onNodeDeleted(
|
||||||
|
src: string,
|
||||||
|
deleteImage: DeleteImage,
|
||||||
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const assetUrlWithWorkspaceId = new URL(src).pathname.substring(1);
|
const assetUrlWithWorkspaceId = new URL(src).pathname.substring(1);
|
||||||
const resStatus = await deleteImage(assetUrlWithWorkspaceId);
|
const resStatus = await deleteImage(assetUrlWithWorkspaceId);
|
||||||
|
@ -4,7 +4,7 @@ import { Decoration, DecorationSet, EditorView } from "@tiptap/pm/view";
|
|||||||
|
|
||||||
const uploadKey = new PluginKey("upload-image");
|
const uploadKey = new PluginKey("upload-image");
|
||||||
|
|
||||||
const UploadImagesPlugin = () =>
|
const UploadImagesPlugin = (cancelUploadImage?: () => any) =>
|
||||||
new Plugin({
|
new Plugin({
|
||||||
key: uploadKey,
|
key: uploadKey,
|
||||||
state: {
|
state: {
|
||||||
@ -21,15 +21,46 @@ const UploadImagesPlugin = () =>
|
|||||||
const placeholder = document.createElement("div");
|
const placeholder = document.createElement("div");
|
||||||
placeholder.setAttribute("class", "img-placeholder");
|
placeholder.setAttribute("class", "img-placeholder");
|
||||||
const image = document.createElement("img");
|
const image = document.createElement("img");
|
||||||
image.setAttribute("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;
|
image.src = src;
|
||||||
placeholder.appendChild(image);
|
placeholder.appendChild(image);
|
||||||
|
|
||||||
|
// Create cancel button
|
||||||
|
const cancelButton = document.createElement("button");
|
||||||
|
cancelButton.style.position = "absolute";
|
||||||
|
cancelButton.style.right = "3px";
|
||||||
|
cancelButton.style.top = "3px";
|
||||||
|
cancelButton.setAttribute("class", "opacity-90 rounded-lg");
|
||||||
|
|
||||||
|
cancelButton.onclick = () => {
|
||||||
|
cancelUploadImage?.();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
cancelButton.appendChild(svgElement);
|
||||||
|
placeholder.appendChild(cancelButton);
|
||||||
const deco = Decoration.widget(pos + 1, placeholder, {
|
const deco = Decoration.widget(pos + 1, placeholder, {
|
||||||
id,
|
id,
|
||||||
});
|
});
|
||||||
set = set.add(tr.doc, [deco]);
|
set = set.add(tr.doc, [deco]);
|
||||||
} else if (action && action.remove) {
|
} else if (action && action.remove) {
|
||||||
set = set.remove(set.find(undefined, undefined, (spec) => spec.id == action.remove.id));
|
set = set.remove(
|
||||||
|
set.find(
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
(spec) => spec.id == action.remove.id,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return set;
|
return set;
|
||||||
},
|
},
|
||||||
@ -48,19 +79,39 @@ function findPlaceholder(state: EditorState, id: {}) {
|
|||||||
const found = decos.find(
|
const found = decos.find(
|
||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
(spec: { id: number | undefined }) => spec.id == id
|
(spec: { id: number | undefined }) => spec.id == id,
|
||||||
);
|
);
|
||||||
return found.length ? found[0].from : null;
|
return found.length ? found[0].from : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const removePlaceholder = (view: EditorView, id: {}) => {
|
||||||
|
const removePlaceholderTr = view.state.tr.setMeta(uploadKey, {
|
||||||
|
remove: { id },
|
||||||
|
});
|
||||||
|
view.dispatch(removePlaceholderTr);
|
||||||
|
};
|
||||||
|
|
||||||
export async function startImageUpload(
|
export async function startImageUpload(
|
||||||
file: File,
|
file: File,
|
||||||
view: EditorView,
|
view: EditorView,
|
||||||
pos: number,
|
pos: number,
|
||||||
uploadFile: UploadImage,
|
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.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!file.type.includes("image/")) {
|
if (!file.type.includes("image/")) {
|
||||||
|
alert("Invalid file type. Please select an image file.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.size > 5 * 1024 * 1024) {
|
||||||
|
alert("File size too large. Please select a file smaller than 5MB.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,28 +133,42 @@ export async function startImageUpload(
|
|||||||
view.dispatch(tr);
|
view.dispatch(tr);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle FileReader errors
|
||||||
|
reader.onerror = (error) => {
|
||||||
|
console.error("FileReader error: ", error);
|
||||||
|
removePlaceholder(view, id);
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
setIsSubmitting?.("submitting");
|
setIsSubmitting?.("submitting");
|
||||||
const src = await UploadImageHandler(file, uploadFile);
|
|
||||||
const { schema } = view.state;
|
|
||||||
pos = findPlaceholder(view.state, id);
|
|
||||||
|
|
||||||
if (pos == null) return;
|
try {
|
||||||
const imageSrc = typeof src === "object" ? reader.result : src;
|
const src = await UploadImageHandler(file, uploadFile);
|
||||||
|
const { schema } = view.state;
|
||||||
|
pos = findPlaceholder(view.state, id);
|
||||||
|
|
||||||
const node = schema.nodes.image.create({ src: imageSrc });
|
if (pos == null) return;
|
||||||
const transaction = view.state.tr
|
const imageSrc = typeof src === "object" ? reader.result : src;
|
||||||
.replaceWith(pos, pos, node)
|
|
||||||
.setMeta(uploadKey, { remove: { id } });
|
const node = schema.nodes.image.create({ src: imageSrc });
|
||||||
view.dispatch(transaction);
|
const transaction = view.state.tr
|
||||||
|
.replaceWith(pos, pos, node)
|
||||||
|
.setMeta(uploadKey, { remove: { id } });
|
||||||
|
view.dispatch(transaction);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Upload error: ", error);
|
||||||
|
removePlaceholder(view, id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const UploadImageHandler = (file: File,
|
const UploadImageHandler = (
|
||||||
uploadFile: UploadImage
|
file: File,
|
||||||
|
uploadFile: UploadImage,
|
||||||
): Promise<string> => {
|
): Promise<string> => {
|
||||||
try {
|
try {
|
||||||
return new Promise(async (resolve, reject) => {
|
return new Promise(async (resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
const imageUrl = await uploadFile(file)
|
const imageUrl = await uploadFile(file);
|
||||||
|
|
||||||
const image = new Image();
|
const image = new Image();
|
||||||
image.src = imageUrl;
|
image.src = imageUrl;
|
||||||
@ -118,9 +183,6 @@ const UploadImageHandler = (file: File,
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Error) {
|
|
||||||
console.log(error.message);
|
|
||||||
}
|
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -29,6 +29,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@plane/editor-core": "*",
|
"@plane/editor-core": "*",
|
||||||
|
"@plane/ui": "*",
|
||||||
"@tiptap/extension-list-item": "^2.1.11",
|
"@tiptap/extension-list-item": "^2.1.11",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^1.2.1",
|
"clsx": "^1.2.1",
|
||||||
|
@ -47,6 +47,7 @@ interface ILiteTextEditor {
|
|||||||
}[];
|
}[];
|
||||||
};
|
};
|
||||||
onEnterKeyPress?: (e?: any) => void;
|
onEnterKeyPress?: (e?: any) => void;
|
||||||
|
cancelUploadImage?: () => any;
|
||||||
mentionHighlights?: string[];
|
mentionHighlights?: string[];
|
||||||
mentionSuggestions?: IMentionSuggestion[];
|
mentionSuggestions?: IMentionSuggestion[];
|
||||||
submitButton?: React.ReactNode;
|
submitButton?: React.ReactNode;
|
||||||
@ -64,6 +65,7 @@ interface EditorHandle {
|
|||||||
const LiteTextEditor = (props: LiteTextEditorProps) => {
|
const LiteTextEditor = (props: LiteTextEditorProps) => {
|
||||||
const {
|
const {
|
||||||
onChange,
|
onChange,
|
||||||
|
cancelUploadImage,
|
||||||
debouncedUpdatesEnabled,
|
debouncedUpdatesEnabled,
|
||||||
setIsSubmitting,
|
setIsSubmitting,
|
||||||
setShouldShowAlert,
|
setShouldShowAlert,
|
||||||
@ -84,6 +86,7 @@ const LiteTextEditor = (props: LiteTextEditorProps) => {
|
|||||||
|
|
||||||
const editor = useEditor({
|
const editor = useEditor({
|
||||||
onChange,
|
onChange,
|
||||||
|
cancelUploadImage,
|
||||||
debouncedUpdatesEnabled,
|
debouncedUpdatesEnabled,
|
||||||
setIsSubmitting,
|
setIsSubmitting,
|
||||||
setShouldShowAlert,
|
setShouldShowAlert,
|
||||||
|
@ -14,8 +14,8 @@ import {
|
|||||||
TableItem,
|
TableItem,
|
||||||
UnderLineItem,
|
UnderLineItem,
|
||||||
} from "@plane/editor-core";
|
} from "@plane/editor-core";
|
||||||
import { Tooltip } from "../../tooltip";
|
import { Tooltip } from "@plane/ui";
|
||||||
import { UploadImage } from "../..";
|
import { UploadImage } from "../../";
|
||||||
|
|
||||||
export interface BubbleMenuItem {
|
export interface BubbleMenuItem {
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -1,8 +1,13 @@
|
|||||||
"use client"
|
"use client";
|
||||||
import * as React from 'react';
|
import * as React from "react";
|
||||||
import { EditorContainer, EditorContentWrapper, getEditorClassNames, useEditor } from '@plane/editor-core';
|
import {
|
||||||
import { EditorBubbleMenu } from './menus/bubble-menu';
|
EditorContainer,
|
||||||
import { RichTextEditorExtensions } from './extensions';
|
EditorContentWrapper,
|
||||||
|
getEditorClassNames,
|
||||||
|
useEditor,
|
||||||
|
} from "@plane/editor-core";
|
||||||
|
import { EditorBubbleMenu } from "./menus/bubble-menu";
|
||||||
|
import { RichTextEditorExtensions } from "./extensions";
|
||||||
|
|
||||||
export type UploadImage = (file: File) => Promise<string>;
|
export type UploadImage = (file: File) => Promise<string>;
|
||||||
export type DeleteImage = (assetUrlWithWorkspaceId: string) => Promise<any>;
|
export type DeleteImage = (assetUrlWithWorkspaceId: string) => Promise<any>;
|
||||||
@ -14,9 +19,9 @@ export type IMentionSuggestion = {
|
|||||||
title: string;
|
title: string;
|
||||||
subtitle: string;
|
subtitle: string;
|
||||||
redirect_uri: string;
|
redirect_uri: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type IMentionHighlight = string
|
export type IMentionHighlight = string;
|
||||||
|
|
||||||
interface IRichTextEditor {
|
interface IRichTextEditor {
|
||||||
value: string;
|
value: string;
|
||||||
@ -24,10 +29,13 @@ interface IRichTextEditor {
|
|||||||
deleteFile: DeleteImage;
|
deleteFile: DeleteImage;
|
||||||
noBorder?: boolean;
|
noBorder?: boolean;
|
||||||
borderOnFocus?: boolean;
|
borderOnFocus?: boolean;
|
||||||
|
cancelUploadImage?: () => any;
|
||||||
customClassName?: string;
|
customClassName?: string;
|
||||||
editorContentCustomClassNames?: string;
|
editorContentCustomClassNames?: string;
|
||||||
onChange?: (json: any, html: string) => void;
|
onChange?: (json: any, html: string) => void;
|
||||||
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void;
|
setIsSubmitting?: (
|
||||||
|
isSubmitting: "submitting" | "submitted" | "saved",
|
||||||
|
) => void;
|
||||||
setShouldShowAlert?: (showAlert: boolean) => void;
|
setShouldShowAlert?: (showAlert: boolean) => void;
|
||||||
forwardedRef?: any;
|
forwardedRef?: any;
|
||||||
debouncedUpdatesEnabled?: boolean;
|
debouncedUpdatesEnabled?: boolean;
|
||||||
@ -54,11 +62,12 @@ const RichTextEditor = ({
|
|||||||
uploadFile,
|
uploadFile,
|
||||||
deleteFile,
|
deleteFile,
|
||||||
noBorder,
|
noBorder,
|
||||||
|
cancelUploadImage,
|
||||||
borderOnFocus,
|
borderOnFocus,
|
||||||
customClassName,
|
customClassName,
|
||||||
forwardedRef,
|
forwardedRef,
|
||||||
mentionHighlights,
|
mentionHighlights,
|
||||||
mentionSuggestions
|
mentionSuggestions,
|
||||||
}: RichTextEditorProps) => {
|
}: RichTextEditorProps) => {
|
||||||
const editor = useEditor({
|
const editor = useEditor({
|
||||||
onChange,
|
onChange,
|
||||||
@ -67,14 +76,19 @@ const RichTextEditor = ({
|
|||||||
setShouldShowAlert,
|
setShouldShowAlert,
|
||||||
value,
|
value,
|
||||||
uploadFile,
|
uploadFile,
|
||||||
|
cancelUploadImage,
|
||||||
deleteFile,
|
deleteFile,
|
||||||
forwardedRef,
|
forwardedRef,
|
||||||
extensions: RichTextEditorExtensions(uploadFile, setIsSubmitting),
|
extensions: RichTextEditorExtensions(uploadFile, setIsSubmitting),
|
||||||
mentionHighlights,
|
mentionHighlights,
|
||||||
mentionSuggestions
|
mentionSuggestions,
|
||||||
});
|
});
|
||||||
|
|
||||||
const editorClassNames = getEditorClassNames({ noBorder, borderOnFocus, customClassName });
|
const editorClassNames = getEditorClassNames({
|
||||||
|
noBorder,
|
||||||
|
borderOnFocus,
|
||||||
|
customClassName,
|
||||||
|
});
|
||||||
|
|
||||||
if (!editor) return null;
|
if (!editor) return null;
|
||||||
|
|
||||||
@ -82,16 +96,19 @@ const RichTextEditor = ({
|
|||||||
<EditorContainer editor={editor} editorClassNames={editorClassNames}>
|
<EditorContainer editor={editor} editorClassNames={editorClassNames}>
|
||||||
{editor && <EditorBubbleMenu editor={editor} />}
|
{editor && <EditorBubbleMenu editor={editor} />}
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<EditorContentWrapper editor={editor} editorContentCustomClassNames={editorContentCustomClassNames} />
|
<EditorContentWrapper
|
||||||
|
editor={editor}
|
||||||
|
editorContentCustomClassNames={editorContentCustomClassNames}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</EditorContainer >
|
</EditorContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const RichTextEditorWithRef = React.forwardRef<EditorHandle, IRichTextEditor>((props, ref) => (
|
const RichTextEditorWithRef = React.forwardRef<EditorHandle, IRichTextEditor>(
|
||||||
<RichTextEditor {...props} forwardedRef={ref} />
|
(props, ref) => <RichTextEditor {...props} forwardedRef={ref} />,
|
||||||
));
|
);
|
||||||
|
|
||||||
RichTextEditorWithRef.displayName = "RichTextEditorWithRef";
|
RichTextEditorWithRef.displayName = "RichTextEditorWithRef";
|
||||||
|
|
||||||
export { RichTextEditor, RichTextEditorWithRef};
|
export { RichTextEditor, RichTextEditorWithRef };
|
||||||
|
@ -76,6 +76,7 @@ export const AddComment: React.FC<Props> = observer((props) => {
|
|||||||
handleSubmit(onSubmit)(e);
|
handleSubmit(onSubmit)(e);
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
cancelUploadImage={fileService.cancelUpload}
|
||||||
uploadFile={fileService.getUploadFileFunction(workspace_slug as string)}
|
uploadFile={fileService.getUploadFileFunction(workspace_slug as string)}
|
||||||
deleteFile={fileService.deleteImage}
|
deleteFile={fileService.deleteImage}
|
||||||
ref={editorRef}
|
ref={editorRef}
|
||||||
|
@ -103,6 +103,7 @@ export const CommentCard: React.FC<Props> = observer((props) => {
|
|||||||
render={({ field: { onChange, value } }) => (
|
render={({ field: { onChange, value } }) => (
|
||||||
<LiteTextEditorWithRef
|
<LiteTextEditorWithRef
|
||||||
onEnterKeyPress={handleSubmit(handleCommentUpdate)}
|
onEnterKeyPress={handleSubmit(handleCommentUpdate)}
|
||||||
|
cancelUploadImage={fileService.cancelUpload}
|
||||||
uploadFile={fileService.getUploadFileFunction(workspaceSlug)}
|
uploadFile={fileService.getUploadFileFunction(workspaceSlug)}
|
||||||
deleteFile={fileService.deleteImage}
|
deleteFile={fileService.deleteImage}
|
||||||
ref={editorRef}
|
ref={editorRef}
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import APIService from "services/api.service";
|
import APIService from "services/api.service";
|
||||||
import { API_BASE_URL } from "helpers/common.helper";
|
import { API_BASE_URL } from "helpers/common.helper";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
interface UnSplashImage {
|
interface UnSplashImage {
|
||||||
id: string;
|
id: string;
|
||||||
@ -26,25 +27,37 @@ interface UnSplashImageUrls {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class FileService extends APIService {
|
class FileService extends APIService {
|
||||||
|
private cancelSource: any;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super(API_BASE_URL);
|
super(API_BASE_URL);
|
||||||
this.uploadFile = this.uploadFile.bind(this);
|
this.uploadFile = this.uploadFile.bind(this);
|
||||||
this.deleteImage = this.deleteImage.bind(this);
|
this.deleteImage = this.deleteImage.bind(this);
|
||||||
|
this.cancelUpload = this.cancelUpload.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
async uploadFile(workspaceSlug: string, file: FormData): Promise<any> {
|
async uploadFile(workspaceSlug: string, file: FormData): Promise<any> {
|
||||||
|
this.cancelSource = axios.CancelToken.source();
|
||||||
return this.post(`/api/workspaces/${workspaceSlug}/file-assets/`, file, {
|
return this.post(`/api/workspaces/${workspaceSlug}/file-assets/`, file, {
|
||||||
headers: {
|
headers: {
|
||||||
...this.getHeaders(),
|
...this.getHeaders(),
|
||||||
"Content-Type": "multipart/form-data",
|
"Content-Type": "multipart/form-data",
|
||||||
},
|
},
|
||||||
|
cancelToken: this.cancelSource.token,
|
||||||
})
|
})
|
||||||
.then((response) => response?.data)
|
.then((response) => response?.data)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
throw error?.response?.data;
|
if (axios.isCancel(error)) {
|
||||||
|
console.log(error.message);
|
||||||
|
} else {
|
||||||
|
throw error?.response?.data;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cancelUpload() {
|
||||||
|
this.cancelSource.cancel("Upload cancelled");
|
||||||
|
}
|
||||||
getUploadFileFunction(workspaceSlug: string): (file: File) => Promise<string> {
|
getUploadFileFunction(workspaceSlug: string): (file: File) => Promise<string> {
|
||||||
return async (file: File) => {
|
return async (file: File) => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
|
@ -92,7 +92,7 @@
|
|||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tableWrapper .tableControls .columnsControl > button {
|
.tableWrapper .tableControls .columnsControl .columnsControlDiv {
|
||||||
color: white;
|
color: white;
|
||||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='none' d='M0 0h24v24H0z'/%3E%3Cpath fill='%238F95B2' d='M4.5 10.5c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5S6 12.825 6 12s-.675-1.5-1.5-1.5zm15 0c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5S21 12.825 21 12s-.675-1.5-1.5-1.5zm-7.5 0c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5 1.5-.675 1.5-1.5-.675-1.5-1.5-1.5z'/%3E%3C/svg%3E");
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='none' d='M0 0h24v24H0z'/%3E%3Cpath fill='%238F95B2' d='M4.5 10.5c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5S6 12.825 6 12s-.675-1.5-1.5-1.5zm15 0c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5S21 12.825 21 12s-.675-1.5-1.5-1.5zm-7.5 0c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5 1.5-.675 1.5-1.5-.675-1.5-1.5-1.5z'/%3E%3C/svg%3E");
|
||||||
width: 30px;
|
width: 30px;
|
||||||
@ -104,26 +104,42 @@
|
|||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tableWrapper .tableControls .rowsControl > button {
|
.tableWrapper .tableControls .rowsControl .rowsControlDiv {
|
||||||
color: white;
|
color: white;
|
||||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='none' d='M0 0h24v24H0z'/%3E%3Cpath fill='%238F95B2' d='M12 3c-.825 0-1.5.675-1.5 1.5S11.175 6 12 6s1.5-.675 1.5-1.5S12.825 3 12 3zm0 15c-.825 0-1.5.675-1.5 1.5S11.175 21 12 21s1.5-.675 1.5-1.5S12.825 18 12 18zm0-7.5c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5 1.5-.675 1.5-1.5-.675-1.5-1.5-1.5z'/%3E%3C/svg%3E");
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='none' d='M0 0h24v24H0z'/%3E%3Cpath fill='%238F95B2' d='M12 3c-.825 0-1.5.675-1.5 1.5S11.175 6 12 6s1.5-.675 1.5-1.5S12.825 3 12 3zm0 15c-.825 0-1.5.675-1.5 1.5S11.175 21 12 21s1.5-.675 1.5-1.5S12.825 18 12 18zm0-7.5c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5 1.5-.675 1.5-1.5-.675-1.5-1.5-1.5z'/%3E%3C/svg%3E");
|
||||||
height: 30px;
|
height: 30px;
|
||||||
width: 15px;
|
width: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tableWrapper .tableControls button {
|
.tableWrapper .tableControls .rowsControlDiv {
|
||||||
background-color: rgba(var(--color-primary-100));
|
background-color: rgba(var(--color-primary-100));
|
||||||
border: 1px solid rgba(var(--color-border-200));
|
border: 1px solid rgba(var(--color-border-200));
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
background-size: 1.25rem;
|
background-size: 1.25rem;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
transition: transform ease-out 100ms, background-color ease-out 100ms;
|
transition:
|
||||||
|
transform ease-out 100ms,
|
||||||
|
background-color ease-out 100ms;
|
||||||
outline: none;
|
outline: none;
|
||||||
box-shadow: #000 0px 2px 4px;
|
box-shadow: #000 0px 2px 4px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tableWrapper .tableControls .columnsControlDiv {
|
||||||
|
background-color: rgba(var(--color-primary-100));
|
||||||
|
border: 1px solid rgba(var(--color-border-200));
|
||||||
|
border-radius: 2px;
|
||||||
|
background-size: 1.25rem;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
transition:
|
||||||
|
transform ease-out 100ms,
|
||||||
|
background-color ease-out 100ms;
|
||||||
|
outline: none;
|
||||||
|
box-shadow: #000 0px 2px 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
.tableWrapper .tableControls .tableToolbox,
|
.tableWrapper .tableControls .tableToolbox,
|
||||||
.tableWrapper .tableControls .tableColorPickerToolbox {
|
.tableWrapper .tableControls .tableColorPickerToolbox {
|
||||||
border: 1px solid rgba(var(--color-border-300));
|
border: 1px solid rgba(var(--color-border-300));
|
||||||
|
@ -15,6 +15,7 @@ import { IssuePrioritySelect } from "components/issues/select";
|
|||||||
import { Button, Input, ToggleSwitch } from "@plane/ui";
|
import { Button, Input, ToggleSwitch } from "@plane/ui";
|
||||||
// types
|
// types
|
||||||
import { IIssue } from "types";
|
import { IIssue } from "types";
|
||||||
|
import useEditorSuggestions from "hooks/use-editor-suggestions";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@ -40,6 +41,8 @@ export const CreateInboxIssueModal: React.FC<Props> = observer((props) => {
|
|||||||
|
|
||||||
const editorRef = useRef<any>(null);
|
const editorRef = useRef<any>(null);
|
||||||
|
|
||||||
|
const editorSuggestion = useEditorSuggestions()
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { workspaceSlug, projectId, inboxId } = router.query;
|
const { workspaceSlug, projectId, inboxId } = router.query;
|
||||||
|
|
||||||
@ -134,6 +137,7 @@ export const CreateInboxIssueModal: React.FC<Props> = observer((props) => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<RichTextEditorWithRef
|
<RichTextEditorWithRef
|
||||||
|
cancelUploadImage={fileService.cancelUpload}
|
||||||
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
||||||
deleteFile={fileService.deleteImage}
|
deleteFile={fileService.deleteImage}
|
||||||
ref={editorRef}
|
ref={editorRef}
|
||||||
@ -143,6 +147,8 @@ export const CreateInboxIssueModal: React.FC<Props> = observer((props) => {
|
|||||||
onChange={(description, description_html: string) => {
|
onChange={(description, description_html: string) => {
|
||||||
onChange(description_html);
|
onChange(description_html);
|
||||||
}}
|
}}
|
||||||
|
mentionSuggestions={editorSuggestion.mentionSuggestions}
|
||||||
|
mentionHighlights={editorSuggestion.mentionHighlights}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
@ -84,6 +84,7 @@ export const AddComment: React.FC<Props> = ({ disabled = false, onSubmit, showAc
|
|||||||
render={({ field: { onChange: onCommentChange, value: commentValue } }) => (
|
render={({ field: { onChange: onCommentChange, value: commentValue } }) => (
|
||||||
<LiteTextEditorWithRef
|
<LiteTextEditorWithRef
|
||||||
onEnterKeyPress={handleSubmit(handleAddComment)}
|
onEnterKeyPress={handleSubmit(handleAddComment)}
|
||||||
|
cancelUploadImage={fileService.cancelUpload}
|
||||||
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
||||||
deleteFile={fileService.deleteImage}
|
deleteFile={fileService.deleteImage}
|
||||||
ref={editorRef}
|
ref={editorRef}
|
||||||
|
@ -105,6 +105,7 @@ export const CommentCard: React.FC<Props> = ({
|
|||||||
<div>
|
<div>
|
||||||
<LiteTextEditorWithRef
|
<LiteTextEditorWithRef
|
||||||
onEnterKeyPress={handleSubmit(onEnter)}
|
onEnterKeyPress={handleSubmit(onEnter)}
|
||||||
|
cancelUploadImage={fileService.cancelUpload}
|
||||||
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
||||||
deleteFile={fileService.deleteImage}
|
deleteFile={fileService.deleteImage}
|
||||||
ref={editorRef}
|
ref={editorRef}
|
||||||
|
@ -145,6 +145,7 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = (props) => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<RichTextEditor
|
<RichTextEditor
|
||||||
|
cancelUploadImage={fileService.cancelUpload}
|
||||||
uploadFile={fileService.getUploadFileFunction(workspaceSlug)}
|
uploadFile={fileService.getUploadFileFunction(workspaceSlug)}
|
||||||
deleteFile={fileService.deleteImage}
|
deleteFile={fileService.deleteImage}
|
||||||
value={value}
|
value={value}
|
||||||
|
@ -425,6 +425,7 @@ export const DraftIssueForm: FC<IssueFormProps> = (props) => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<RichTextEditorWithRef
|
<RichTextEditorWithRef
|
||||||
|
cancelUploadImage={fileService.cancelUpload}
|
||||||
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
||||||
deleteFile={fileService.deleteImage}
|
deleteFile={fileService.deleteImage}
|
||||||
ref={editorRef}
|
ref={editorRef}
|
||||||
|
@ -381,6 +381,7 @@ export const IssueForm: FC<IssueFormProps> = observer((props) => {
|
|||||||
control={control}
|
control={control}
|
||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<RichTextEditorWithRef
|
<RichTextEditorWithRef
|
||||||
|
cancelUploadImage={fileService.cancelUpload}
|
||||||
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
||||||
deleteFile={fileService.deleteImage}
|
deleteFile={fileService.deleteImage}
|
||||||
ref={editorRef}
|
ref={editorRef}
|
||||||
|
@ -114,6 +114,7 @@ export const IssueCommentCard: React.FC<IIssueCommentCard> = (props) => {
|
|||||||
<div>
|
<div>
|
||||||
<LiteTextEditorWithRef
|
<LiteTextEditorWithRef
|
||||||
onEnterKeyPress={handleSubmit(formSubmit)}
|
onEnterKeyPress={handleSubmit(formSubmit)}
|
||||||
|
cancelUploadImage={fileService.cancelUpload}
|
||||||
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
||||||
deleteFile={fileService.deleteImage}
|
deleteFile={fileService.deleteImage}
|
||||||
ref={editorRef}
|
ref={editorRef}
|
||||||
|
@ -85,6 +85,7 @@ export const IssueCommentEditor: React.FC<IIssueCommentEditor> = (props) => {
|
|||||||
render={({ field: { onChange: onCommentChange, value: commentValue } }) => (
|
render={({ field: { onChange: onCommentChange, value: commentValue } }) => (
|
||||||
<LiteTextEditorWithRef
|
<LiteTextEditorWithRef
|
||||||
onEnterKeyPress={handleSubmit(handleAddComment)}
|
onEnterKeyPress={handleSubmit(handleAddComment)}
|
||||||
|
cancelUploadImage={fileService.cancelUpload}
|
||||||
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
||||||
deleteFile={fileService.deleteImage}
|
deleteFile={fileService.deleteImage}
|
||||||
ref={editorRef}
|
ref={editorRef}
|
||||||
|
@ -140,6 +140,7 @@ export const PeekOverviewIssueDetails: FC<IPeekOverviewIssueDetails> = (props) =
|
|||||||
</div>
|
</div>
|
||||||
<span>{errors.name ? errors.name.message : null}</span>
|
<span>{errors.name ? errors.name.message : null}</span>
|
||||||
<RichTextEditor
|
<RichTextEditor
|
||||||
|
cancelUploadImage={fileService.cancelUpload}
|
||||||
uploadFile={fileService.getUploadFileFunction(workspaceSlug)}
|
uploadFile={fileService.getUploadFileFunction(workspaceSlug)}
|
||||||
deleteFile={fileService.deleteImage}
|
deleteFile={fileService.deleteImage}
|
||||||
value={issue?.description_html}
|
value={issue?.description_html}
|
||||||
|
@ -295,6 +295,7 @@ export const CreateUpdateBlockInline: FC<Props> = ({
|
|||||||
if (!data)
|
if (!data)
|
||||||
return (
|
return (
|
||||||
<RichTextEditorWithRef
|
<RichTextEditorWithRef
|
||||||
|
cancelUploadImage={fileService.cancelUpload}
|
||||||
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
||||||
deleteFile={fileService.deleteImage}
|
deleteFile={fileService.deleteImage}
|
||||||
ref={editorRef}
|
ref={editorRef}
|
||||||
@ -316,6 +317,7 @@ export const CreateUpdateBlockInline: FC<Props> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<RichTextEditorWithRef
|
<RichTextEditorWithRef
|
||||||
|
cancelUploadImage={fileService.cancelUpload}
|
||||||
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
||||||
deleteFile={fileService.deleteImage}
|
deleteFile={fileService.deleteImage}
|
||||||
ref={editorRef}
|
ref={editorRef}
|
||||||
|
@ -420,6 +420,7 @@ export const SinglePageBlock: React.FC<Props> = ({ block, projectDetails, showBl
|
|||||||
{showBlockDetails
|
{showBlockDetails
|
||||||
? block.description_html.length > 7 && (
|
? block.description_html.length > 7 && (
|
||||||
<RichTextEditor
|
<RichTextEditor
|
||||||
|
cancelUploadImage={fileService.cancelUpload}
|
||||||
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
uploadFile={fileService.getUploadFileFunction(workspaceSlug as string)}
|
||||||
deleteFile={fileService.deleteImage}
|
deleteFile={fileService.deleteImage}
|
||||||
value={block.description_html}
|
value={block.description_html}
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
import { APIService } from "services/api.service";
|
import { APIService } from "services/api.service";
|
||||||
// helpers
|
// helpers
|
||||||
import { API_BASE_URL } from "helpers/common.helper";
|
import { API_BASE_URL } from "helpers/common.helper";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
export interface UnSplashImage {
|
export interface UnSplashImage {
|
||||||
id: string;
|
id: string;
|
||||||
@ -28,25 +29,38 @@ export interface UnSplashImageUrls {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class FileService extends APIService {
|
export class FileService extends APIService {
|
||||||
|
private cancelSource: any;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super(API_BASE_URL);
|
super(API_BASE_URL);
|
||||||
this.uploadFile = this.uploadFile.bind(this);
|
this.uploadFile = this.uploadFile.bind(this);
|
||||||
this.deleteImage = this.deleteImage.bind(this);
|
this.deleteImage = this.deleteImage.bind(this);
|
||||||
|
this.cancelUpload = this.cancelUpload.bind(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
async uploadFile(workspaceSlug: string, file: FormData): Promise<any> {
|
async uploadFile(workspaceSlug: string, file: FormData): Promise<any> {
|
||||||
|
this.cancelSource = axios.CancelToken.source();
|
||||||
return this.post(`/api/workspaces/${workspaceSlug}/file-assets/`, file, {
|
return this.post(`/api/workspaces/${workspaceSlug}/file-assets/`, file, {
|
||||||
headers: {
|
headers: {
|
||||||
...this.getHeaders(),
|
...this.getHeaders(),
|
||||||
"Content-Type": "multipart/form-data",
|
"Content-Type": "multipart/form-data",
|
||||||
},
|
},
|
||||||
|
cancelToken: this.cancelSource.token,
|
||||||
})
|
})
|
||||||
.then((response) => response?.data)
|
.then((response) => response?.data)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
throw error?.response?.data;
|
if (axios.isCancel(error)) {
|
||||||
|
console.log(error.message);
|
||||||
|
} else {
|
||||||
|
throw error?.response?.data;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cancelUpload() {
|
||||||
|
this.cancelSource.cancel("Upload cancelled");
|
||||||
|
}
|
||||||
|
|
||||||
getUploadFileFunction(workspaceSlug: string): (file: File) => Promise<string> {
|
getUploadFileFunction(workspaceSlug: string): (file: File) => Promise<string> {
|
||||||
return async (file: File) => {
|
return async (file: File) => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
|
@ -92,7 +92,7 @@
|
|||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tableWrapper .tableControls .columnsControl > button {
|
.tableWrapper .tableControls .columnsControl .columnsControlDiv {
|
||||||
color: white;
|
color: white;
|
||||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='none' d='M0 0h24v24H0z'/%3E%3Cpath fill='%238F95B2' d='M4.5 10.5c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5S6 12.825 6 12s-.675-1.5-1.5-1.5zm15 0c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5S21 12.825 21 12s-.675-1.5-1.5-1.5zm-7.5 0c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5 1.5-.675 1.5-1.5-.675-1.5-1.5-1.5z'/%3E%3C/svg%3E");
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='none' d='M0 0h24v24H0z'/%3E%3Cpath fill='%238F95B2' d='M4.5 10.5c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5S6 12.825 6 12s-.675-1.5-1.5-1.5zm15 0c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5S21 12.825 21 12s-.675-1.5-1.5-1.5zm-7.5 0c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5 1.5-.675 1.5-1.5-.675-1.5-1.5-1.5z'/%3E%3C/svg%3E");
|
||||||
width: 30px;
|
width: 30px;
|
||||||
@ -104,14 +104,14 @@
|
|||||||
transform: translateX(-50%);
|
transform: translateX(-50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.tableWrapper .tableControls .rowsControl > button {
|
.tableWrapper .tableControls .rowsControl .rowsControlDiv {
|
||||||
color: white;
|
color: white;
|
||||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='none' d='M0 0h24v24H0z'/%3E%3Cpath fill='%238F95B2' d='M12 3c-.825 0-1.5.675-1.5 1.5S11.175 6 12 6s1.5-.675 1.5-1.5S12.825 3 12 3zm0 15c-.825 0-1.5.675-1.5 1.5S11.175 21 12 21s1.5-.675 1.5-1.5S12.825 18 12 18zm0-7.5c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5 1.5-.675 1.5-1.5-.675-1.5-1.5-1.5z'/%3E%3C/svg%3E");
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='24' height='24'%3E%3Cpath fill='none' d='M0 0h24v24H0z'/%3E%3Cpath fill='%238F95B2' d='M12 3c-.825 0-1.5.675-1.5 1.5S11.175 6 12 6s1.5-.675 1.5-1.5S12.825 3 12 3zm0 15c-.825 0-1.5.675-1.5 1.5S11.175 21 12 21s1.5-.675 1.5-1.5S12.825 18 12 18zm0-7.5c-.825 0-1.5.675-1.5 1.5s.675 1.5 1.5 1.5 1.5-.675 1.5-1.5-.675-1.5-1.5-1.5z'/%3E%3C/svg%3E");
|
||||||
height: 30px;
|
height: 30px;
|
||||||
width: 15px;
|
width: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tableWrapper .tableControls button {
|
.tableWrapper .tableControls .rowsControlDiv {
|
||||||
background-color: rgba(var(--color-primary-100));
|
background-color: rgba(var(--color-primary-100));
|
||||||
border: 1px solid rgba(var(--color-border-200));
|
border: 1px solid rgba(var(--color-border-200));
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
@ -124,6 +124,18 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tableWrapper .tableControls .columnsControlDiv {
|
||||||
|
background-color: rgba(var(--color-primary-100));
|
||||||
|
border: 1px solid rgba(var(--color-border-200));
|
||||||
|
border-radius: 2px;
|
||||||
|
background-size: 1.25rem;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
transition: transform ease-out 100ms, background-color ease-out 100ms;
|
||||||
|
outline: none;
|
||||||
|
box-shadow: #000 0px 2px 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
.tableWrapper .tableControls .tableToolbox,
|
.tableWrapper .tableControls .tableToolbox,
|
||||||
.tableWrapper .tableControls .tableColorPickerToolbox {
|
.tableWrapper .tableControls .tableColorPickerToolbox {
|
||||||
border: 1px solid rgba(var(--color-border-300));
|
border: 1px solid rgba(var(--color-border-300));
|
||||||
|
Loading…
Reference in New Issue
Block a user