mirror of
https://github.com/makeplane/plane
synced 2024-06-14 14:31:34 +00:00
improved file structure and delete image function implemented
This commit is contained in:
parent
c078d59916
commit
5228ab8d0a
2
.gitignore
vendored
2
.gitignore
vendored
@ -71,3 +71,5 @@ package-lock.json
|
||||
package-lock.json
|
||||
pnpm-lock.yaml
|
||||
pnpm-workspace.yaml
|
||||
|
||||
.npmrc
|
||||
|
@ -7,9 +7,9 @@ import useReloadConfirmations from "hooks/use-reload-confirmation";
|
||||
// components
|
||||
import { TextArea } from "components/ui";
|
||||
|
||||
import Tiptap from "./tiptap";
|
||||
// types
|
||||
import { IIssue } from "types";
|
||||
import Tiptap from "components/tiptap";
|
||||
|
||||
export interface IssueDescriptionFormValues {
|
||||
name: string;
|
||||
@ -126,7 +126,6 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
|
||||
setIsSubmitting={setIsSubmitting}
|
||||
onChange={(description: Object, description_html: string) => {
|
||||
onChange(description_html);
|
||||
// setValue("description_html", description_html);
|
||||
setValue("description", description);
|
||||
handleSubmit(handleDescriptionFormSubmit)().finally(() => setIsSubmitting(false));
|
||||
}}
|
||||
|
@ -1,28 +0,0 @@
|
||||
import fileService from 'services/file.service';
|
||||
|
||||
const UploadImageHandler = (file: File): Promise<string> => {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append("asset", file);
|
||||
formData.append("attributes", JSON.stringify({}));
|
||||
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const imageUrl = await fileService
|
||||
.uploadFile("plane", formData)
|
||||
.then((response) => response.asset);
|
||||
|
||||
console.log(imageUrl, "imageurl")
|
||||
const image = new Image();
|
||||
image.src = imageUrl;
|
||||
image.onload = () => {
|
||||
resolve(imageUrl);
|
||||
};
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
console.log(error)
|
||||
return Promise.reject(error);
|
||||
}
|
||||
};
|
||||
|
||||
export default UploadImageHandler;
|
@ -1,55 +0,0 @@
|
||||
import { useEditor, EditorContent } from '@tiptap/react';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import { EditorBubbleMenu } from './EditorBubbleMenu';
|
||||
import { TiptapExtensions } from './extensions';
|
||||
import { TiptapEditorProps } from './props';
|
||||
|
||||
type TiptapProps = {
|
||||
value: string;
|
||||
noBorder?: boolean;
|
||||
borderOnFocus?: boolean;
|
||||
customClassName?: string;
|
||||
onChange?: (json: any, html: string) => void;
|
||||
setIsSubmitting: (isSubmitting: boolean) => void;
|
||||
}
|
||||
|
||||
const Tiptap = ({ onChange, setIsSubmitting, value, noBorder, borderOnFocus, customClassName }: TiptapProps) => {
|
||||
const editor = useEditor({
|
||||
editorProps: TiptapEditorProps,
|
||||
extensions: TiptapExtensions,
|
||||
content: value,
|
||||
onUpdate: async ({ editor }) => {
|
||||
setIsSubmitting(true);
|
||||
debouncedUpdates({ onChange, editor });
|
||||
}
|
||||
});
|
||||
|
||||
const debouncedUpdates = useDebouncedCallback(async ({ onChange, editor }) => {
|
||||
setTimeout(async () => {
|
||||
if (onChange) {
|
||||
onChange(editor.getJSON(), editor.getHTML());
|
||||
}
|
||||
}, 500);
|
||||
}, 1000);
|
||||
|
||||
const editorClassNames = `mt-2 p-3 relative focus:outline-none rounded-md focus:border-custom-border-200
|
||||
${noBorder ? '' : 'border border-custom-border-200'
|
||||
} ${borderOnFocus ? 'focus:border border-custom-border-200' : 'focus:border-0'
|
||||
} ${customClassName}`;
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={() => {
|
||||
editor?.chain().focus().run();
|
||||
}}
|
||||
className={`tiptap-editor-container relative min-h-[150px] ${editorClassNames}`}
|
||||
>
|
||||
{editor && <EditorBubbleMenu editor={editor} />}
|
||||
<div className="pt-8">
|
||||
<EditorContent editor={editor} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tiptap;
|
@ -10,7 +10,7 @@ import {
|
||||
|
||||
import { NodeSelector } from "./node-selector";
|
||||
import { LinkSelector } from "./link-selector";
|
||||
import { cn } from "./utils";
|
||||
import { cn } from "../utils"
|
||||
|
||||
export interface BubbleMenuItem {
|
||||
name: string;
|
@ -1,7 +1,7 @@
|
||||
import { Editor } from "@tiptap/core";
|
||||
import { Check, Trash } from "lucide-react";
|
||||
import { Dispatch, FC, SetStateAction, useEffect, useRef } from "react";
|
||||
import { cn } from './utils';
|
||||
import { cn } from '../utils';
|
||||
|
||||
interface LinkSelectorProps {
|
||||
editor: Editor;
|
@ -13,8 +13,8 @@ import {
|
||||
} from "lucide-react";
|
||||
import { Dispatch, FC, SetStateAction } from "react";
|
||||
|
||||
import { BubbleMenuItem } from "./EditorBubbleMenu";
|
||||
import { cn } from "./utils";
|
||||
import { BubbleMenuItem } from "../bubble-menu";
|
||||
import { cn } from "../utils";
|
||||
|
||||
interface NodeSelectorProps {
|
||||
editor: Editor;
|
@ -12,13 +12,14 @@ import { Markdown } from "tiptap-markdown";
|
||||
import Highlight from "@tiptap/extension-highlight";
|
||||
import CodeBlockLowlight from "@tiptap/extension-code-block-lowlight";
|
||||
import { lowlight } from 'lowlight/lib/core'
|
||||
import SlashCommand from "./slash-command";
|
||||
import SlashCommand from "../slash-command";
|
||||
import { InputRule } from "@tiptap/core";
|
||||
|
||||
import ts from 'highlight.js/lib/languages/typescript'
|
||||
|
||||
import 'highlight.js/styles/github-dark.css';
|
||||
import UploadImagesPlugin from "./plugins/upload-image";
|
||||
import UploadImagesPlugin from "../plugins/upload-image";
|
||||
import UniqueID from "@tiptap-pro/extension-unique-id";
|
||||
|
||||
lowlight.registerLanguage('ts', ts)
|
||||
|
||||
@ -115,6 +116,9 @@ export const TiptapExtensions = [
|
||||
},
|
||||
includeChildren: true,
|
||||
}),
|
||||
UniqueID.configure({
|
||||
types: ['heading', 'paragraph', 'image'],
|
||||
}),
|
||||
SlashCommand,
|
||||
TiptapUnderline,
|
||||
TextStyle,
|
110
apps/app/components/tiptap/index.tsx
Normal file
110
apps/app/components/tiptap/index.tsx
Normal file
@ -0,0 +1,110 @@
|
||||
import { useEditor, EditorContent } from '@tiptap/react';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import { EditorBubbleMenu } from './bubble-menu';
|
||||
import { TiptapExtensions } from './extensions';
|
||||
import { TiptapEditorProps } from './props';
|
||||
import { Node } from "@tiptap/pm/model";
|
||||
import { Editor as CoreEditor } from "@tiptap/core";
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { EditorState } from '@tiptap/pm/state';
|
||||
import fileService from 'services/file.service';
|
||||
|
||||
type TiptapProps = {
|
||||
value: string;
|
||||
noBorder?: boolean;
|
||||
borderOnFocus?: boolean;
|
||||
customClassName?: string;
|
||||
onChange?: (json: any, html: string) => void;
|
||||
setIsSubmitting: (isSubmitting: boolean) => void;
|
||||
}
|
||||
|
||||
const Tiptap = ({ onChange, setIsSubmitting, value, noBorder, borderOnFocus, customClassName }: TiptapProps) => {
|
||||
const editor = useEditor({
|
||||
editorProps: TiptapEditorProps,
|
||||
extensions: TiptapExtensions,
|
||||
content: value,
|
||||
onUpdate: async ({ editor }) => {
|
||||
setIsSubmitting(true);
|
||||
checkForNodeDeletions(editor)
|
||||
debouncedUpdates({ onChange, editor });
|
||||
}
|
||||
});
|
||||
|
||||
const previousState = useRef<EditorState>();
|
||||
|
||||
const extractPath = useCallback((url: string, searchString: string) => {
|
||||
if (url.startsWith(searchString)) {
|
||||
console.log("chala", url, searchString)
|
||||
return url.substring(searchString.length);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const onNodeDeleted = useCallback(
|
||||
async (node: Node) => {
|
||||
if (node.type.name === 'image') {
|
||||
const assetUrlWithWorkspaceId = new URL(node.attrs.src).pathname.substring(1);
|
||||
const resStatus = await fileService.deleteFile(assetUrlWithWorkspaceId);
|
||||
if (resStatus === 204) {
|
||||
console.log("file deleted successfully");
|
||||
}
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const checkForNodeDeletions = useCallback(
|
||||
(editor: CoreEditor) => {
|
||||
const prevNodesById: Record<string, Node> = {};
|
||||
previousState.current?.doc.forEach((node) => {
|
||||
if (node.attrs.id) {
|
||||
prevNodesById[node.attrs.id] = node;
|
||||
}
|
||||
});
|
||||
|
||||
const nodesById: Record<string, Node> = {};
|
||||
editor.state?.doc.forEach((node) => {
|
||||
if (node.attrs.id) {
|
||||
nodesById[node.attrs.id] = node;
|
||||
}
|
||||
});
|
||||
|
||||
previousState.current = editor.state;
|
||||
|
||||
for (const [id, node] of Object.entries(prevNodesById)) {
|
||||
if (nodesById[id] === undefined) {
|
||||
onNodeDeleted(node);
|
||||
}
|
||||
}
|
||||
},
|
||||
[onNodeDeleted],
|
||||
);
|
||||
|
||||
const debouncedUpdates = useDebouncedCallback(async ({ onChange, editor }) => {
|
||||
setTimeout(async () => {
|
||||
if (onChange) {
|
||||
onChange(editor.getJSON(), editor.getHTML());
|
||||
}
|
||||
}, 500);
|
||||
}, 1000);
|
||||
|
||||
const editorClassNames = `mt-2 p-3 relative focus:outline-none rounded-md focus:border-custom-border-200
|
||||
${noBorder ? '' : 'border border-custom-border-200'
|
||||
} ${borderOnFocus ? 'focus:border border-custom-border-200' : 'focus:border-0'
|
||||
} ${customClassName}`;
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={() => {
|
||||
editor?.chain().focus().run();
|
||||
}}
|
||||
className={`tiptap-editor-container relative min-h-[150px] ${editorClassNames}`}
|
||||
>
|
||||
{editor && <EditorBubbleMenu editor={editor} />}
|
||||
<div className="pt-8">
|
||||
<EditorContent editor={editor} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tiptap;
|
@ -42,7 +42,7 @@ const UploadImagesPlugin = () =>
|
||||
props: {
|
||||
decorations(state) {
|
||||
return this.getState(state);
|
||||
},
|
||||
}
|
||||
},
|
||||
});
|
||||
|
@ -21,7 +21,9 @@ import {
|
||||
Code,
|
||||
MinusSquare,
|
||||
CheckSquare,
|
||||
ImageIcon,
|
||||
} from "lucide-react";
|
||||
import { startImageUpload } from "../plugins/upload-image";
|
||||
|
||||
interface CommandItemProps {
|
||||
title: string;
|
||||
@ -180,6 +182,27 @@ const getSuggestionItems = ({ query }: { query: string }) =>
|
||||
command: ({ editor, range }: CommandProps) =>
|
||||
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) => {
|
||||
editor.chain().focus().deleteRange(range).run();
|
||||
// upload image
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.accept = "image/*";
|
||||
input.onchange = async () => {
|
||||
if (input.files?.length) {
|
||||
const file = input.files[0];
|
||||
const pos = editor.view.state.selection.from;
|
||||
startImageUpload(file, editor.view, pos);
|
||||
}
|
||||
};
|
||||
input.click();
|
||||
},
|
||||
},
|
||||
].filter((item) => {
|
||||
if (typeof query === "string" && query.length > 0) {
|
||||
const search = query.toLowerCase();
|
@ -27,6 +27,7 @@
|
||||
"@nivo/scatterplot": "0.80.0",
|
||||
"@sentry/nextjs": "^7.36.0",
|
||||
"@tailwindcss/typography": "^0.5.9",
|
||||
"@tiptap-pro/extension-unique-id": "^2.1.0",
|
||||
"@tiptap/extension-code-block-lowlight": "^2.0.4",
|
||||
"@tiptap/extension-color": "^2.0.4",
|
||||
"@tiptap/extension-highlight": "^2.0.4",
|
||||
|
@ -40,12 +40,9 @@ class FileServices extends APIService {
|
||||
});
|
||||
}
|
||||
|
||||
async deleteFile(workspaceId: string, assetUrl: string): Promise<any> {
|
||||
const lastIndex = assetUrl.lastIndexOf("/");
|
||||
const assetId = assetUrl.substring(lastIndex + 1);
|
||||
|
||||
return this.delete(`/api/workspaces/file-assets/${workspaceId}/${assetId}/`)
|
||||
.then((response) => response?.data)
|
||||
async deleteFile(assetUrlWithWorkspaceId: string): Promise<any> {
|
||||
return this.delete(`/api/workspaces/file-assets/${assetUrlWithWorkspaceId}/`)
|
||||
.then((response) => response?.status)
|
||||
.catch((error) => {
|
||||
throw error?.response?.data;
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user