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
4
.gitignore
vendored
4
.gitignore
vendored
@ -70,4 +70,6 @@ package-lock.json
|
|||||||
# lock files
|
# lock files
|
||||||
package-lock.json
|
package-lock.json
|
||||||
pnpm-lock.yaml
|
pnpm-lock.yaml
|
||||||
pnpm-workspace.yaml
|
pnpm-workspace.yaml
|
||||||
|
|
||||||
|
.npmrc
|
||||||
|
@ -7,9 +7,9 @@ import useReloadConfirmations from "hooks/use-reload-confirmation";
|
|||||||
// components
|
// components
|
||||||
import { TextArea } from "components/ui";
|
import { TextArea } from "components/ui";
|
||||||
|
|
||||||
import Tiptap from "./tiptap";
|
|
||||||
// types
|
// types
|
||||||
import { IIssue } from "types";
|
import { IIssue } from "types";
|
||||||
|
import Tiptap from "components/tiptap";
|
||||||
|
|
||||||
export interface IssueDescriptionFormValues {
|
export interface IssueDescriptionFormValues {
|
||||||
name: string;
|
name: string;
|
||||||
@ -126,7 +126,6 @@ export const IssueDescriptionForm: FC<IssueDetailsProps> = ({
|
|||||||
setIsSubmitting={setIsSubmitting}
|
setIsSubmitting={setIsSubmitting}
|
||||||
onChange={(description: Object, description_html: string) => {
|
onChange={(description: Object, description_html: string) => {
|
||||||
onChange(description_html);
|
onChange(description_html);
|
||||||
// setValue("description_html", description_html);
|
|
||||||
setValue("description", description);
|
setValue("description", description);
|
||||||
handleSubmit(handleDescriptionFormSubmit)().finally(() => setIsSubmitting(false));
|
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 { NodeSelector } from "./node-selector";
|
||||||
import { LinkSelector } from "./link-selector";
|
import { LinkSelector } from "./link-selector";
|
||||||
import { cn } from "./utils";
|
import { cn } from "../utils"
|
||||||
|
|
||||||
export interface BubbleMenuItem {
|
export interface BubbleMenuItem {
|
||||||
name: string;
|
name: string;
|
@ -1,7 +1,7 @@
|
|||||||
import { Editor } from "@tiptap/core";
|
import { Editor } from "@tiptap/core";
|
||||||
import { Check, Trash } from "lucide-react";
|
import { Check, Trash } from "lucide-react";
|
||||||
import { Dispatch, FC, SetStateAction, useEffect, useRef } from "react";
|
import { Dispatch, FC, SetStateAction, useEffect, useRef } from "react";
|
||||||
import { cn } from './utils';
|
import { cn } from '../utils';
|
||||||
|
|
||||||
interface LinkSelectorProps {
|
interface LinkSelectorProps {
|
||||||
editor: Editor;
|
editor: Editor;
|
@ -13,8 +13,8 @@ import {
|
|||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Dispatch, FC, SetStateAction } from "react";
|
import { Dispatch, FC, SetStateAction } from "react";
|
||||||
|
|
||||||
import { BubbleMenuItem } from "./EditorBubbleMenu";
|
import { BubbleMenuItem } from "../bubble-menu";
|
||||||
import { cn } from "./utils";
|
import { cn } from "../utils";
|
||||||
|
|
||||||
interface NodeSelectorProps {
|
interface NodeSelectorProps {
|
||||||
editor: Editor;
|
editor: Editor;
|
@ -12,13 +12,14 @@ import { Markdown } from "tiptap-markdown";
|
|||||||
import Highlight from "@tiptap/extension-highlight";
|
import Highlight from "@tiptap/extension-highlight";
|
||||||
import CodeBlockLowlight from "@tiptap/extension-code-block-lowlight";
|
import CodeBlockLowlight from "@tiptap/extension-code-block-lowlight";
|
||||||
import { lowlight } from 'lowlight/lib/core'
|
import { lowlight } from 'lowlight/lib/core'
|
||||||
import SlashCommand from "./slash-command";
|
import SlashCommand from "../slash-command";
|
||||||
import { InputRule } from "@tiptap/core";
|
import { InputRule } from "@tiptap/core";
|
||||||
|
|
||||||
import ts from 'highlight.js/lib/languages/typescript'
|
import ts from 'highlight.js/lib/languages/typescript'
|
||||||
|
|
||||||
import 'highlight.js/styles/github-dark.css';
|
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)
|
lowlight.registerLanguage('ts', ts)
|
||||||
|
|
||||||
@ -115,6 +116,9 @@ export const TiptapExtensions = [
|
|||||||
},
|
},
|
||||||
includeChildren: true,
|
includeChildren: true,
|
||||||
}),
|
}),
|
||||||
|
UniqueID.configure({
|
||||||
|
types: ['heading', 'paragraph', 'image'],
|
||||||
|
}),
|
||||||
SlashCommand,
|
SlashCommand,
|
||||||
TiptapUnderline,
|
TiptapUnderline,
|
||||||
TextStyle,
|
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: {
|
props: {
|
||||||
decorations(state) {
|
decorations(state) {
|
||||||
return this.getState(state);
|
return this.getState(state);
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
@ -21,7 +21,9 @@ import {
|
|||||||
Code,
|
Code,
|
||||||
MinusSquare,
|
MinusSquare,
|
||||||
CheckSquare,
|
CheckSquare,
|
||||||
|
ImageIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import { startImageUpload } from "../plugins/upload-image";
|
||||||
|
|
||||||
interface CommandItemProps {
|
interface CommandItemProps {
|
||||||
title: string;
|
title: string;
|
||||||
@ -180,6 +182,27 @@ const getSuggestionItems = ({ query }: { query: string }) =>
|
|||||||
command: ({ editor, range }: CommandProps) =>
|
command: ({ editor, range }: CommandProps) =>
|
||||||
editor.chain().focus().deleteRange(range).toggleCodeBlock().run(),
|
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) => {
|
].filter((item) => {
|
||||||
if (typeof query === "string" && query.length > 0) {
|
if (typeof query === "string" && query.length > 0) {
|
||||||
const search = query.toLowerCase();
|
const search = query.toLowerCase();
|
@ -27,6 +27,7 @@
|
|||||||
"@nivo/scatterplot": "0.80.0",
|
"@nivo/scatterplot": "0.80.0",
|
||||||
"@sentry/nextjs": "^7.36.0",
|
"@sentry/nextjs": "^7.36.0",
|
||||||
"@tailwindcss/typography": "^0.5.9",
|
"@tailwindcss/typography": "^0.5.9",
|
||||||
|
"@tiptap-pro/extension-unique-id": "^2.1.0",
|
||||||
"@tiptap/extension-code-block-lowlight": "^2.0.4",
|
"@tiptap/extension-code-block-lowlight": "^2.0.4",
|
||||||
"@tiptap/extension-color": "^2.0.4",
|
"@tiptap/extension-color": "^2.0.4",
|
||||||
"@tiptap/extension-highlight": "^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> {
|
async deleteFile(assetUrlWithWorkspaceId: string): Promise<any> {
|
||||||
const lastIndex = assetUrl.lastIndexOf("/");
|
return this.delete(`/api/workspaces/file-assets/${assetUrlWithWorkspaceId}/`)
|
||||||
const assetId = assetUrl.substring(lastIndex + 1);
|
.then((response) => response?.status)
|
||||||
|
|
||||||
return this.delete(`/api/workspaces/file-assets/${workspaceId}/${assetId}/`)
|
|
||||||
.then((response) => response?.data)
|
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
throw error?.response?.data;
|
throw error?.response?.data;
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user