forked from github/plane
refactored editor to not require workspace slug
This commit is contained in:
parent
d639a0126d
commit
4298b0500e
@ -6,7 +6,10 @@
|
|||||||
"web",
|
"web",
|
||||||
"space",
|
"space",
|
||||||
"packages/editor/*",
|
"packages/editor/*",
|
||||||
"packages/*"
|
"packages/eslint-config-custom",
|
||||||
|
"packages/tailwind-config-custom",
|
||||||
|
"packages/tsconfig",
|
||||||
|
"packages/ui"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "turbo run build",
|
"build": "turbo run build",
|
||||||
|
@ -1 +1 @@
|
|||||||
export type UploadImage = (workspaceSlug: string, formData: FormData) => Promise<any>;
|
export type UploadImage = (file: File) => Promise<string>;
|
||||||
|
@ -32,7 +32,6 @@ import "highlight.js/styles/github-dark.css";
|
|||||||
lowlight.registerLanguage("ts", ts);
|
lowlight.registerLanguage("ts", ts);
|
||||||
|
|
||||||
export const TiptapExtensions = (
|
export const TiptapExtensions = (
|
||||||
workspaceSlug: string,
|
|
||||||
uploadFile: UploadImage,
|
uploadFile: UploadImage,
|
||||||
deleteFile: DeleteImage,
|
deleteFile: DeleteImage,
|
||||||
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void
|
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void
|
||||||
@ -126,7 +125,7 @@ export const TiptapExtensions = (
|
|||||||
},
|
},
|
||||||
includeChildren: true,
|
includeChildren: true,
|
||||||
}),
|
}),
|
||||||
SlashCommand(workspaceSlug, uploadFile, setIsSubmitting),
|
SlashCommand(uploadFile, setIsSubmitting),
|
||||||
TiptapUnderline,
|
TiptapUnderline,
|
||||||
TextStyle,
|
TextStyle,
|
||||||
Color,
|
Color,
|
||||||
|
@ -59,7 +59,6 @@ const Command = Extension.create({
|
|||||||
|
|
||||||
const getSuggestionItems =
|
const getSuggestionItems =
|
||||||
(
|
(
|
||||||
workspaceSlug: string,
|
|
||||||
uploadFile: UploadImage,
|
uploadFile: UploadImage,
|
||||||
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void
|
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void
|
||||||
) =>
|
) =>
|
||||||
@ -185,7 +184,7 @@ const getSuggestionItems =
|
|||||||
if (input.files?.length) {
|
if (input.files?.length) {
|
||||||
const file = input.files[0];
|
const file = input.files[0];
|
||||||
const pos = editor.view.state.selection.from;
|
const pos = editor.view.state.selection.from;
|
||||||
startImageUpload(file, editor.view, pos, workspaceSlug, uploadFile, setIsSubmitting);
|
startImageUpload(file, editor.view, pos, uploadFile, setIsSubmitting);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
input.click();
|
input.click();
|
||||||
@ -351,13 +350,12 @@ const renderItems = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const SlashCommand = (
|
export const SlashCommand = (
|
||||||
workspaceSlug: string,
|
|
||||||
uploadFile: UploadImage,
|
uploadFile: UploadImage,
|
||||||
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void
|
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void
|
||||||
) =>
|
) =>
|
||||||
Command.configure({
|
Command.configure({
|
||||||
suggestion: {
|
suggestion: {
|
||||||
items: getSuggestionItems(workspaceSlug, uploadFile, setIsSubmitting),
|
items: getSuggestionItems(uploadFile, setIsSubmitting),
|
||||||
render: renderItems,
|
render: renderItems,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -24,7 +24,6 @@ interface ITiptapEditor {
|
|||||||
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;
|
||||||
workspaceSlug: string;
|
|
||||||
editable?: boolean;
|
editable?: boolean;
|
||||||
forwardedRef?: any;
|
forwardedRef?: any;
|
||||||
debouncedUpdatesEnabled?: boolean;
|
debouncedUpdatesEnabled?: boolean;
|
||||||
@ -59,7 +58,6 @@ const TiptapEditor = ({
|
|||||||
uploadFile,
|
uploadFile,
|
||||||
deleteFile,
|
deleteFile,
|
||||||
noBorder,
|
noBorder,
|
||||||
workspaceSlug,
|
|
||||||
borderOnFocus,
|
borderOnFocus,
|
||||||
customClassName,
|
customClassName,
|
||||||
forwardedRef,
|
forwardedRef,
|
||||||
@ -69,9 +67,9 @@ const TiptapEditor = ({
|
|||||||
}: TiptapProps) => {
|
}: TiptapProps) => {
|
||||||
const editor = useEditor({
|
const editor = useEditor({
|
||||||
editable: editable ?? true,
|
editable: editable ?? true,
|
||||||
editorProps: TiptapEditorProps(workspaceSlug, uploadFile, setIsSubmitting),
|
editorProps: TiptapEditorProps(uploadFile, setIsSubmitting),
|
||||||
// @ts-expect-err
|
// @ts-expect-err
|
||||||
extensions: TiptapExtensions(workspaceSlug, uploadFile, deleteFile, setIsSubmitting),
|
extensions: TiptapExtensions(uploadFile, deleteFile, setIsSubmitting),
|
||||||
content: (typeof value === "string" && value.trim() !== "") ? value : "<p></p>",
|
content: (typeof value === "string" && value.trim() !== "") ? value : "<p></p>",
|
||||||
onUpdate: async ({ editor }) => {
|
onUpdate: async ({ editor }) => {
|
||||||
// for instant feedback loop
|
// for instant feedback loop
|
||||||
|
@ -57,7 +57,6 @@ export async function startImageUpload(
|
|||||||
file: File,
|
file: File,
|
||||||
view: EditorView,
|
view: EditorView,
|
||||||
pos: number,
|
pos: number,
|
||||||
workspaceSlug: string,
|
|
||||||
uploadFile: UploadImage,
|
uploadFile: UploadImage,
|
||||||
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void
|
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void
|
||||||
) {
|
) {
|
||||||
@ -83,11 +82,8 @@ export async function startImageUpload(
|
|||||||
view.dispatch(tr);
|
view.dispatch(tr);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!workspaceSlug) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setIsSubmitting?.("submitting");
|
setIsSubmitting?.("submitting");
|
||||||
const src = await UploadImageHandler(file, workspaceSlug, uploadFile);
|
const src = await UploadImageHandler(file, uploadFile);
|
||||||
const { schema } = view.state;
|
const { schema } = view.state;
|
||||||
pos = findPlaceholder(view.state, id);
|
pos = findPlaceholder(view.state, id);
|
||||||
|
|
||||||
@ -101,21 +97,13 @@ export async function startImageUpload(
|
|||||||
view.dispatch(transaction);
|
view.dispatch(transaction);
|
||||||
}
|
}
|
||||||
|
|
||||||
const UploadImageHandler = (file: File, workspaceSlug: string,
|
const UploadImageHandler = (file: File,
|
||||||
uploadFile: UploadImage
|
uploadFile: UploadImage
|
||||||
): Promise<string> => {
|
): Promise<string> => {
|
||||||
if (!workspaceSlug) {
|
|
||||||
return Promise.reject("Workspace slug is missing");
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
const formData = new FormData();
|
|
||||||
formData.append("asset", file);
|
|
||||||
formData.append("attributes", JSON.stringify({}));
|
|
||||||
|
|
||||||
return new Promise(async (resolve, reject) => {
|
return new Promise(async (resolve, reject) => {
|
||||||
try {
|
try {
|
||||||
const imageUrl = await uploadFile(workspaceSlug, formData)
|
const imageUrl = await uploadFile(file)
|
||||||
.then((response: { asset: string }) => response.asset);
|
|
||||||
|
|
||||||
const image = new Image();
|
const image = new Image();
|
||||||
image.src = imageUrl;
|
image.src = imageUrl;
|
||||||
|
@ -4,7 +4,6 @@ import { startImageUpload } from "@/ui/plugins/upload-image";
|
|||||||
import { UploadImage } from "@/types/upload-image";
|
import { UploadImage } from "@/types/upload-image";
|
||||||
|
|
||||||
export function TiptapEditorProps(
|
export function TiptapEditorProps(
|
||||||
workspaceSlug: string,
|
|
||||||
uploadFile: UploadImage,
|
uploadFile: UploadImage,
|
||||||
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void
|
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void
|
||||||
): EditorProps {
|
): EditorProps {
|
||||||
@ -37,7 +36,7 @@ export function TiptapEditorProps(
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const file = event.clipboardData.files[0];
|
const file = event.clipboardData.files[0];
|
||||||
const pos = view.state.selection.from;
|
const pos = view.state.selection.from;
|
||||||
startImageUpload(file, view, pos, workspaceSlug, uploadFile, setIsSubmitting);
|
startImageUpload(file, view, pos, uploadFile, setIsSubmitting);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@ -61,7 +60,7 @@ export function TiptapEditorProps(
|
|||||||
});
|
});
|
||||||
// here we deduct 1 from the pos or else the image will create an extra node
|
// here we deduct 1 from the pos or else the image will create an extra node
|
||||||
if (coordinates) {
|
if (coordinates) {
|
||||||
startImageUpload(file, view, coordinates.pos - 1, workspaceSlug, uploadFile, setIsSubmitting);
|
startImageUpload(file, view, coordinates.pos - 1, uploadFile, setIsSubmitting);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
@ -1,35 +1,35 @@
|
|||||||
import {
|
import {
|
||||||
useEditor as useEditorCore,
|
useEditor as useEditorCore,
|
||||||
} from "@tiptap/react";
|
} from "@tiptap/react";
|
||||||
import { findTableAncestor } from "@/lib/utils";
|
import { findTableAncestor } from "@/lib/utils";
|
||||||
|
|
||||||
export const useEditor = (props: any) => useEditorCore({
|
export const useEditor = (props: any) => useEditorCore({
|
||||||
editorProps: {
|
editorProps: {
|
||||||
attributes: {
|
attributes: {
|
||||||
class: `prose prose-brand max-w-full prose-headings:font-display font-default focus:outline-none`,
|
class: `prose prose-brand max-w-full prose-headings:font-display font-default focus:outline-none`,
|
||||||
},
|
},
|
||||||
handleDOMEvents: {
|
handleDOMEvents: {
|
||||||
keydown: (_view, event) => {
|
keydown: (_view, event) => {
|
||||||
// prevent default event listeners from firing when slash command is active
|
// prevent default event listeners from firing when slash command is active
|
||||||
if (["ArrowUp", "ArrowDown", "Enter"].includes(event.key)) {
|
if (["ArrowUp", "ArrowDown", "Enter"].includes(event.key)) {
|
||||||
const slashCommand = document.querySelector("#slash-command");
|
const slashCommand = document.querySelector("#slash-command");
|
||||||
if (slashCommand) {
|
if (slashCommand) {
|
||||||
return true;
|
return true;
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
handlePaste: () => {
|
|
||||||
if (typeof window !== "undefined") {
|
|
||||||
const selection: any = window?.getSelection();
|
|
||||||
if (selection.rangeCount !== 0) {
|
|
||||||
const range = selection.getRangeAt(0);
|
|
||||||
if (findTableAncestor(range.startContainer)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
...props,
|
},
|
||||||
});
|
handlePaste: () => {
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
const selection: any = window?.getSelection();
|
||||||
|
if (selection.rangeCount !== 0) {
|
||||||
|
const range = selection.getRangeAt(0);
|
||||||
|
if (findTableAncestor(range.startContainer)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
...props,
|
||||||
|
});
|
||||||
|
@ -135,10 +135,9 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<TiptapEditor
|
<TiptapEditor
|
||||||
uploadFile={fileService.uploadFile}
|
uploadFile={fileService.getUploadFileFunction(workspaceSlug)}
|
||||||
deleteFile={fileService.deleteImage}
|
deleteFile={fileService.deleteImage}
|
||||||
value={value}
|
value={value}
|
||||||
workspaceSlug={workspaceSlug}
|
|
||||||
debouncedUpdatesEnabled={true}
|
debouncedUpdatesEnabled={true}
|
||||||
setShouldShowAlert={setShowAlert}
|
setShouldShowAlert={setShowAlert}
|
||||||
setIsSubmitting={setIsSubmitting}
|
setIsSubmitting={setIsSubmitting}
|
||||||
|
@ -34,13 +34,29 @@ class FileService extends APIService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async uploadFile(workspaceSlug: string, file: FormData): Promise<any> {
|
async uploadFile(workspaceSlug: string, file: FormData): Promise<any> {
|
||||||
return this.mediaUpload(`/api/workspaces/${workspaceSlug}/file-assets/`, file)
|
return this.post(`/api/workspaces/${workspaceSlug}/file-assets/`, file, {
|
||||||
|
headers: {
|
||||||
|
...this.getHeaders(),
|
||||||
|
"Content-Type": "multipart/form-data",
|
||||||
|
},
|
||||||
|
})
|
||||||
.then((response) => response?.data)
|
.then((response) => response?.data)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
throw error?.response?.data;
|
throw error?.response?.data;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getUploadFileFunction(workspaceSlug: string): (file: File) => Promise<string> {
|
||||||
|
return async (file: File) => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("asset", file);
|
||||||
|
formData.append("attributes", JSON.stringify({}));
|
||||||
|
|
||||||
|
const data = await this.uploadFile(workspaceSlug, formData);
|
||||||
|
return data.asset;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async deleteImage(assetUrlWithWorkspaceId: string): Promise<any> {
|
async deleteImage(assetUrlWithWorkspaceId: string): Promise<any> {
|
||||||
return this.delete(`/api/workspaces/file-assets/${assetUrlWithWorkspaceId}/`)
|
return this.delete(`/api/workspaces/file-assets/${assetUrlWithWorkspaceId}/`)
|
||||||
.then((response) => response?.status)
|
.then((response) => response?.status)
|
||||||
@ -59,6 +75,7 @@ class FileService extends APIService {
|
|||||||
throw error?.response?.data;
|
throw error?.response?.data;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async uploadUserFile(file: FormData): Promise<any> {
|
async uploadUserFile(file: FormData): Promise<any> {
|
||||||
return this.mediaUpload(`/api/users/file-assets/`, file)
|
return this.mediaUpload(`/api/users/file-assets/`, file)
|
||||||
.then((response) => response?.data)
|
.then((response) => response?.data)
|
||||||
|
Loading…
Reference in New Issue
Block a user