[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
This commit is contained in:
Oliver 2023-09-12 13:31:02 +10:00 committed by GitHub
parent 7e753523d1
commit 816b60850d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 247 additions and 1 deletions

View File

@ -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": {

View File

@ -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 (
<SimpleMDE
value={value}
options={options}
onChange={(v: string) => 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 (
<MarkdownEditor data={data} allowEdit={allowEdit} saveValue={uploadData} />
);
}

View File

@ -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: <IconNotes size="18" />,
content: <Text>part notes go here</Text>
content: partNotesTab()
}
];
}, [part]);
@ -167,6 +171,17 @@ export default function PartDetail() {
);
}
function partNotesTab(): React.ReactNode {
// TODO: Set edit permission based on user permissions
return (
<NotesEditor
url={`/part/${part.pk}/`}
data={part.notes ?? ''}
allowEdit={true}
/>
);
}
function partStockTab(): React.ReactNode {
return (
<StockItemTable

View File

@ -1196,6 +1196,18 @@
dependencies:
"@babel/types" "^7.20.7"
"@types/codemirror@^5.60.4", "@types/codemirror@~5.60.5":
version "5.60.10"
resolved "https://registry.yarnpkg.com/@types/codemirror/-/codemirror-5.60.10.tgz#ac836a3ac20483988a0507cdbbaeb6ee0affa1e6"
integrity sha512-ZTA3teiCWKT8HUUofqlGPlShu5ojdIajizsS0HpH6GL0/iEdjRt7fXbCLHHqKYP5k7dC/HnnWIjZAiELUwBdjQ==
dependencies:
"@types/tern" "*"
"@types/estree@*":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.1.tgz#aa22750962f3bf0e79d753d3cc067f010c95f194"
integrity sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==
"@types/history@^4.7.11":
version "4.7.11"
resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.11.tgz#56588b17ae8f50c53983a524fc3cc47437969d64"
@ -1220,6 +1232,11 @@
dependencies:
"@types/istanbul-lib-report" "*"
"@types/marked@^4.0.7":
version "4.3.1"
resolved "https://registry.yarnpkg.com/@types/marked/-/marked-4.3.1.tgz#45fb6dfd47afb595766c71ed7749ead23f137de3"
integrity sha512-vSSbKZFbNktrQ15v7o1EaH78EbWV+sPQbPjHG+Cp8CaNcPFUEfjZ0Iml/V0bFDwsTlYe8o6XC5Hfdp91cqPV2g==
"@types/node@*", "@types/node@^20.5.9":
version "20.5.9"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.5.9.tgz#a70ec9d8fa0180a314c3ede0e20ea56ff71aed9a"
@ -1287,6 +1304,13 @@
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.3.tgz#cef09e3ec9af1d63d2a6cc5b383a737e24e6dcf5"
integrity sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==
"@types/tern@*":
version "0.23.4"
resolved "https://registry.yarnpkg.com/@types/tern/-/tern-0.23.4.tgz#03926eb13dbeaf3ae0d390caf706b2643a0127fb"
integrity sha512-JAUw1iXGO1qaWwEOzxTKJZ/5JxVeON9kvGZ/osgZaJImBnyjyn0cjovPsf6FNLmyGY8Vw9DoXZCMlfMkMwHRWg==
dependencies:
"@types/estree" "*"
"@types/yargs-parser@*":
version "21.0.0"
resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b"
@ -1536,6 +1560,18 @@ clsx@^1.1.1:
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.2.1.tgz#0ddc4a20a549b59c93a4116bb26f5294ca17dc12"
integrity sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==
codemirror-spell-checker@1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/codemirror-spell-checker/-/codemirror-spell-checker-1.1.2.tgz#1c660f9089483ccb5113b9ba9ca19c3f4993371e"
integrity sha512-2Tl6n0v+GJRsC9K3MLCdLaMOmvWL0uukajNJseorZJsslaxZyZMgENocPU8R0DyoTAiKsyqiemSOZo7kjGV0LQ==
dependencies:
typo-js "*"
codemirror@^5.63.1:
version "5.65.15"
resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.65.15.tgz#66899278f44a7acde0eb641388cd563fe6dfbe19"
integrity sha512-YC4EHbbwQeubZzxLl5G4nlbLc1T21QTrKGaOal/Pkm9dVDMZXMH7+ieSPEOZCtO9I68i8/oteJKOxzHC2zR+0g==
color-convert@^1.9.0:
version "1.9.3"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
@ -1667,6 +1703,17 @@ dom-helpers@^5.0.1:
"@babel/runtime" "^7.8.7"
csstype "^3.0.2"
easymde@^2.18.0:
version "2.18.0"
resolved "https://registry.yarnpkg.com/easymde/-/easymde-2.18.0.tgz#ff1397d07329b1a7b9187d2d0c20766fa16b3b1b"
integrity sha512-IxVVUxNWIoXLeqtBU4BLc+eS/ScYhT1Dcb6yF5Wchoj1iXAV+TIIDWx+NCaZhY7RcSHqDPKllbYq7nwGKILnoA==
dependencies:
"@types/codemirror" "^5.60.4"
"@types/marked" "^4.0.7"
codemirror "^5.63.1"
codemirror-spell-checker "1.1.2"
marked "^4.1.0"
electron-to-chromium@^1.4.477:
version "1.4.508"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.508.tgz#5641ff2f5ba11df4bd960fe6a2f9f70aa8b9af96"
@ -2146,6 +2193,11 @@ mantine-datatable@^2.9.13:
resolved "https://registry.yarnpkg.com/mantine-datatable/-/mantine-datatable-2.9.13.tgz#2c94a8f3b596216b794f1c7881acc20150ab1186"
integrity sha512-k0Q+FKC3kx7IiNJxeLP2PXJHVxuL704U5OVvtVYP/rexlPW8tqZud3WIZDuqfDCkZ83VYoszSTzauCssW+7mLw==
marked@^4.1.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/marked/-/marked-4.3.0.tgz#796362821b019f734054582038b116481b456cf3"
integrity sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==
memoize-one@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-6.0.0.tgz#b2591b871ed82948aee4727dc6abceeeac8c1045"
@ -2497,6 +2549,13 @@ react-select@^5.7.4:
react-transition-group "^4.3.0"
use-isomorphic-layout-effect "^1.1.2"
react-simplemde-editor@^5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/react-simplemde-editor/-/react-simplemde-editor-5.2.0.tgz#7a4c8b97e4989cb129b45ba140145d71bdc0684e"
integrity sha512-GkTg1MlQHVK2Rks++7sjuQr/GVS/xm6y+HchZ4GPBWrhcgLieh4CjK04GTKbsfYorSRYKa0n37rtNSJmOzEDkQ==
dependencies:
"@types/codemirror" "~5.60.5"
react-style-singleton@^2.2.1:
version "2.2.1"
resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.1.tgz#f99e420492b2d8f34d38308ff660b60d0b1205b4"
@ -2751,6 +2810,11 @@ typescript@^5.2.2:
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.2.2.tgz#5ebb5e5a5b75f085f22bc3f8460fba308310fa78"
integrity sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==
typo-js@*:
version "1.2.3"
resolved "https://registry.yarnpkg.com/typo-js/-/typo-js-1.2.3.tgz#aa7fab3cfcc3bba01746df06fceb93b7f786c6ac"
integrity sha512-67Hyl94beZX8gmTap7IDPrG5hy2cHftgsCAcGvE1tzuxGT+kRB+zSBin0wIMwysYw8RUCBCvv9UfQl8TNM75dA==
unraw@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/unraw/-/unraw-2.0.1.tgz#7b51dcdfb1e43d59d5e52cdb44d349d029edbaba"