From 46eab30a4817a51fa93bba3f1807175f4b20545c Mon Sep 17 00:00:00 2001 From: Tyouxik Date: Thu, 8 Aug 2024 09:58:52 +0200 Subject: [PATCH] feat: support markdown for description --- .../model/panels/left/ComponentTab.js | 70 +++++++++++++++---- .../model/panels/left/DescriptionPreview.js | 63 +++++++++++++++++ package-lock.json | 20 +++++- package.json | 4 +- 4 files changed, 141 insertions(+), 16 deletions(-) create mode 100644 app/src/components/model/panels/left/DescriptionPreview.js diff --git a/app/src/components/model/panels/left/ComponentTab.js b/app/src/components/model/panels/left/ComponentTab.js index c2303fa1..ff046266 100644 --- a/app/src/components/model/panels/left/ComponentTab.js +++ b/app/src/components/model/panels/left/ComponentTab.js @@ -6,7 +6,7 @@ import { TextField, Typography, } from "@mui/material"; -import { useEffect, useState } from "react"; +import { useEffect, useState, useRef } from "react"; import { useDispatch } from "react-redux"; import { patchComponent } from "../../../../actions/model/patchComponent"; import { useReadOnly } from "../../../../hooks/useReadOnly"; @@ -14,6 +14,8 @@ import { MultipleSystemsDropdown } from "../../../elements/MultipleSystemsDropdo import { COMPONENT_TYPE } from "../../board/constants"; import { useSelectedComponent } from "../../hooks/useSelectedComponent"; import { TechStacksDropdown } from "./TechStackDropdown"; +import { DescriptionPreview } from "./DescriptionPreview"; +import { set } from "lodash"; export function ComponentTab() { const dispatch = useDispatch(); @@ -24,6 +26,15 @@ export function ComponentTab() { const { type, classes, systems } = component; const [name, setName] = useState(component.name); const [description, setDescription] = useState(component.description || ""); + const [showDescriptionPreview, setShowDescriptionPreview] = useState( + component.description !== "" || readOnly + ); + const descriptionTextFieldRef = useRef(null); + + function showDescriptionTextField() { + setShowDescriptionPreview(false); + setTimeout(() => descriptionTextFieldRef.current.focus(), 1); // Time out needed for the ref to be set + } // const [type, setType] = useState(component.type); // const [techStacks, setTechStacks] = useState(component.classes || []); @@ -38,8 +49,22 @@ export function ComponentTab() { setDescription( component.description === undefined ? "" : component.description ); + + setShowDescriptionPreview((_) => { + if (readOnly) { + return true; + } + if (!component.description) { + return false; + } + if (component.description.trim() === "") { + return false; + } + return true; + }); }, [component.description]); + useEffect(() => {}, [component.description]); // useEffect(() => { // setType(component.type); // }, [component.type]); @@ -51,6 +76,14 @@ export function ComponentTab() { // useEffect(() => { // setSystemId(component.systemId === undefined ? "" : component.systemId); // }, [component.systemId]); + function handleDescriptionOnBlur(newFields) { + if (description.trim() === "") { + setShowDescriptionPreview(false); + } else { + setShowDescriptionPreview(true); + } + updateFields(newFields); + } function updateFields(newFields) { dispatch( @@ -93,7 +126,6 @@ export function ComponentTab() { e.target.blur(); } } - // console.log(systems, classes); return ( @@ -166,18 +198,28 @@ export function ComponentTab() { }} readOnly={readOnly} /> - - updateFields({ description })} - onChange={(e) => setDescription(e.target.value)} - onKeyDown={(e) => shouldBlur(e)} - /> + {!showDescriptionPreview && !readOnly && ( + (descriptionTextFieldRef.current = e)} + onBlur={() => handleDescriptionOnBlur({ description })} + onChange={(e) => setDescription(e.target.value)} + onKeyDown={(e) => shouldBlur(e)} + /> + )} + {(showDescriptionPreview || readOnly) && ( + + )} diff --git a/app/src/components/model/panels/left/DescriptionPreview.js b/app/src/components/model/panels/left/DescriptionPreview.js new file mode 100644 index 00000000..0a802948 --- /dev/null +++ b/app/src/components/model/panels/left/DescriptionPreview.js @@ -0,0 +1,63 @@ +import { marked } from "marked"; +import DOMPurify from "dompurify"; +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; + +marked.use({ + extensions: [ + { + name: "heading", + renderer({ text, depth }) { + return `${text}`; + }, + }, + { + name: "image", + renderer(_) { + return "Images are not supported"; + }, + }, + { + name: "list", + renderer(token) { + const itemList = token.items.map((i) => { + return ( + "
  • " + i.raw.replace(/\n+$/, "") + "
  • " + ); + }); + if (token.ordered) { + return `
      ${itemList.join("\n")}
    `; + } else { + return `
      ${itemList.join("\n")}
    `; + } + }, + }, + ], +}); + +export const DescriptionPreview = ({ + description, + showDescriptionTextField, + readOnly, +}) => { + const sanitizedHtml = DOMPurify.sanitize(marked.parse(description)); + + if (readOnly) { + return ( + <> + Description + + + ); + } else { + return ( + <> + Description + + + ); + } +}; diff --git a/package-lock.json b/package-lock.json index 30a89303..6c3fc48c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,9 @@ "plugins/*" ], "dependencies": { - "log4js": "^6.9.1" + "dompurify": "^3.1.6", + "log4js": "^6.9.1", + "marked": "^14.0.0" }, "devDependencies": { "@tsconfig/node18": "^18.2.2", @@ -13444,6 +13446,11 @@ "url": "https://github.com/fb55/domhandler?sponsor=1" } }, + "node_modules/dompurify": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.6.tgz", + "integrity": "sha512-cTOAhc36AalkjtBpfG6O8JimdTMWNXjiePT2xQH/ppBGi/4uIpmj8eKyIkMJErXWARyINV/sB38yf8JCLF5pbQ==" + }, "node_modules/domutils": { "version": "2.8.0", "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", @@ -21702,6 +21709,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/marked": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz", + "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, "node_modules/matcher": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/matcher/-/matcher-3.0.0.tgz", diff --git a/package.json b/package.json index 1a10bbe9..1697c021 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,8 @@ "typescript": "^5.2.2" }, "dependencies": { - "log4js": "^6.9.1" + "dompurify": "^3.1.6", + "log4js": "^6.9.1", + "marked": "^14.0.0" } }