From 816b60850d03a81c1d101fa69357ddecd493200e Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 12 Sep 2023 13:31:02 +1000 Subject: [PATCH] [PUI] Implement Notes editor (#5529) * Add react-simplemde-editor React wrapper for simplemde which we already use * Barebones implementation of markdown editor field * Implement notes editor * Implement drag-and-drop image uplaod --- src/frontend/package.json | 2 + .../src/components/widgets/MarkdownEditor.tsx | 165 ++++++++++++++++++ src/frontend/src/pages/part/PartDetail.tsx | 17 +- src/frontend/yarn.lock | 64 +++++++ 4 files changed, 247 insertions(+), 1 deletion(-) create mode 100644 src/frontend/src/components/widgets/MarkdownEditor.tsx diff --git a/src/frontend/package.json b/src/frontend/package.json index d6097a9ce5..d645fe2512 100644 --- a/src/frontend/package.json +++ b/src/frontend/package.json @@ -31,6 +31,7 @@ "@tanstack/react-query": "^4.33.0", "axios": "^1.5.0", "dayjs": "^1.11.9", + "easymde": "^2.18.0", "embla-carousel-react": "^8.0.0-rc12", "html5-qrcode": "^2.3.8", "mantine-datatable": "^2.9.13", @@ -39,6 +40,7 @@ "react-grid-layout": "^1.3.4", "react-router-dom": "^6.15.0", "react-select": "^5.7.4", + "react-simplemde-editor": "^5.2.0", "zustand": "^4.4.1" }, "devDependencies": { diff --git a/src/frontend/src/components/widgets/MarkdownEditor.tsx b/src/frontend/src/components/widgets/MarkdownEditor.tsx new file mode 100644 index 0000000000..ee458206a5 --- /dev/null +++ b/src/frontend/src/components/widgets/MarkdownEditor.tsx @@ -0,0 +1,165 @@ +import { t } from '@lingui/macro'; +import { showNotification } from '@mantine/notifications'; +import EasyMDE from 'easymde'; +import 'easymde/dist/easymde.min.css'; +import { ReactNode, useCallback, useMemo } from 'react'; +import { useState } from 'react'; +import SimpleMDE from 'react-simplemde-editor'; + +import { api } from '../../App'; + +/** + * Markdon editor component. Uses react-simplemde-editor + */ +export function MarkdownEditor({ + data, + allowEdit, + saveValue +}: { + data?: string; + allowEdit?: boolean; + saveValue?: (value: string) => void; +}): ReactNode { + const [value, setValue] = useState(data); + + // Construct markdown editor options + const options = useMemo(() => { + // Custom set of toolbar icons for the editor + let icons: any[] = ['preview', 'side-by-side']; + + if (allowEdit) { + icons.push( + '|', + + // Heading icons + 'heading-1', + 'heading-2', + 'heading-3', + '|', + + // Font styles + 'bold', + 'italic', + 'strikethrough', + '|', + + // Text formatting + 'unordered-list', + 'ordered-list', + 'code', + 'quote', + '|', + + // Link and image icons + 'table', + 'link', + 'image' + ); + } + + if (allowEdit) { + icons.push( + '|', + + // Save button + { + name: 'save', + action: (editor: EasyMDE) => { + if (saveValue) { + saveValue(editor.value()); + } + }, + className: 'fa fa-save', + title: t`Save` + } + ); + } + + return { + minHeight: '400px', + toolbar: icons, + sideBySideFullscreen: false, + uploadImage: allowEdit, + imagePathAbsolute: true, + imageUploadFunction: ( + file: File, + onSuccess: (url: string) => void, + onError: (error: string) => void + ) => { + api + .post( + '/notes-image-upload/', + { + image: file + }, + { + headers: { + 'Content-Type': 'multipart/form-data' + } + } + ) + .then((response) => { + if (response.data?.image) { + onSuccess(response.data.image); + } + }) + .catch((error) => { + showNotification({ + title: t`Error`, + message: t`Failed to upload image`, + color: 'red' + }); + onError(error); + }); + } + }; + }, [allowEdit]); + + return ( + setValue(v)} + /> + ); +} + +/** + * Custom implementation of the MarkdownEditor widget for editing notes. + * Includes a callback hook for saving the notes to the server. + */ +export function NotesEditor({ + url, + data, + allowEdit +}: { + url: string; + data?: string; + allowEdit?: boolean; +}): ReactNode { + // Callback function to upload data to the server + const uploadData = useCallback((value: string) => { + api + .patch(url, { notes: value }) + .then((response) => { + showNotification({ + title: t`Success`, + message: t`Notes saved`, + color: 'green' + }); + return response; + }) + .catch((error) => { + showNotification({ + title: t`Error`, + message: t`Failed to save notes`, + color: 'red' + }); + return error; + }); + }, []); + + return ( + + ); +} diff --git a/src/frontend/src/pages/part/PartDetail.tsx b/src/frontend/src/pages/part/PartDetail.tsx index ca989027e2..404c0d23e8 100644 --- a/src/frontend/src/pages/part/PartDetail.tsx +++ b/src/frontend/src/pages/part/PartDetail.tsx @@ -35,6 +35,10 @@ import { api } from '../../App'; import { PanelGroup, PanelType } from '../../components/nav/PanelGroup'; import { AttachmentTable } from '../../components/tables/AttachmentTable'; import { StockItemTable } from '../../components/tables/stock/StockItemTable'; +import { + MarkdownEditor, + NotesEditor +} from '../../components/widgets/MarkdownEditor'; import { editPart } from '../../functions/forms/PartForms'; export default function PartDetail() { @@ -136,7 +140,7 @@ export default function PartDetail() { name: 'notes', label: t`Notes`, icon: , - content: part notes go here + content: partNotesTab() } ]; }, [part]); @@ -167,6 +171,17 @@ export default function PartDetail() { ); } + function partNotesTab(): React.ReactNode { + // TODO: Set edit permission based on user permissions + return ( + + ); + } + function partStockTab(): React.ReactNode { return (