From db843f32f3085b14c9a6bc5e28b574d2dd9b7aaa Mon Sep 17 00:00:00 2001 From: 1ilit Date: Tue, 19 Sep 2023 15:49:28 +0300 Subject: [PATCH] File validation --- package-lock.json | 24 +++++ package.json | 2 + src/components/control_panel.jsx | 158 ++++++++++++++++++++++--------- src/schemas/index.js | 144 ++++++++++++++++++++++++++++ src/utils/index.js | 9 +- 5 files changed, 290 insertions(+), 47 deletions(-) create mode 100644 src/schemas/index.js diff --git a/package-lock.json b/package-lock.json index ea7a1c6..381cecb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "codemirror": "^5.65.13", "file-saver": "^2.0.5", "html-to-image": "^1.11.11", + "jsonschema": "^1.4.1", "jspdf": "^2.5.1", "node-sql-parser": "^4.7.0", "react": "^18.2.0", @@ -28,6 +29,7 @@ "react-dom": "^18.2.0", "react-router-dom": "^6.11.2", "react-scripts": "5.0.1", + "url": "^0.11.1", "web-vitals": "^2.1.4" }, "devDependencies": { @@ -12507,6 +12509,14 @@ "node": ">=0.10.0" } }, + "node_modules/jsonschema": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.4.1.tgz", + "integrity": "sha512-S6cATIPVv1z0IlxdN+zUk5EPjkGCdnhN4wVSBlvoUO1tOLJootbo9CquNJmbIh4yikWHiUedhRYrNPn1arpEmQ==", + "engines": { + "node": "*" + } + }, "node_modules/jspdf": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-2.5.1.tgz", @@ -17290,6 +17300,15 @@ "punycode": "^2.1.0" } }, + "node_modules/url": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.1.tgz", + "integrity": "sha512-rWS3H04/+mzzJkv0eZ7vEDGiQbgquI1fGfOad6zKvgYQi1SzMmhl7c/DdRGxhaWrVH6z0qWITo8rpnxK/RfEhA==", + "dependencies": { + "punycode": "^1.4.1", + "qs": "^6.11.0" + } + }, "node_modules/url-parse": { "version": "1.5.10", "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", @@ -17299,6 +17318,11 @@ "requires-port": "^1.0.0" } }, + "node_modules/url/node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==" + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/package.json b/package.json index cf43fd4..7c72169 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "codemirror": "^5.65.13", "file-saver": "^2.0.5", "html-to-image": "^1.11.11", + "jsonschema": "^1.4.1", "jspdf": "^2.5.1", "node-sql-parser": "^4.7.0", "react": "^18.2.0", @@ -23,6 +24,7 @@ "react-dom": "^18.2.0", "react-router-dom": "^6.11.2", "react-scripts": "5.0.1", + "url": "^0.11.1", "web-vitals": "^2.1.4" }, "scripts": { diff --git a/src/components/control_panel.jsx b/src/components/control_panel.jsx index a1136da..2f776d7 100644 --- a/src/components/control_panel.jsx +++ b/src/components/control_panel.jsx @@ -23,10 +23,16 @@ import { Modal, Spin, Input, + Upload, + Banner, } from "@douyinfe/semi-ui"; import { toPng, toJpeg, toSvg } from "html-to-image"; import { saveAs } from "file-saver"; -import { enterFullscreen, exitFullscreen } from "../utils"; +import { + diagramObjectIsValid, + enterFullscreen, + exitFullscreen, +} from "../utils"; import { AreaContext, LayoutContext, @@ -45,6 +51,7 @@ export default function ControlPanel(props) { NONE: 0, IMG: 1, CODE: 2, + IMPORT: 3, }; const [visible, setVisible] = useState(MODAL.NONE); const [exportData, setExportData] = useState({ @@ -52,6 +59,7 @@ export default function ControlPanel(props) { filename: `diagram_${new Date().toISOString()}`, extension: "", }); + const [error, setError] = useState(null); const { layout, setLayout } = useContext(LayoutContext); const { setSettings } = useContext(SettingsContext); const { relationships, tables, setTables } = useContext(TableContext); @@ -86,7 +94,9 @@ export default function ControlPanel(props) { }, Import: { children: [], - function: () => {}, + function: () => { + setVisible(MODAL.IMPORT); + }, }, "Export as": { children: [ @@ -125,7 +135,7 @@ export default function ControlPanel(props) { tables: tables, relationships: relationships, notes: notes, - "subject areas": areas, + subjectAreas: areas, }, null, 2 @@ -155,23 +165,21 @@ export default function ControlPanel(props) { { PDF: () => { const canvas = document.getElementById("canvas"); - toJpeg(canvas).then( - function (dataUrl) { - const doc = new jsPDF("l", "px", [ - canvas.offsetWidth, - canvas.offsetHeight, - ]); - doc.addImage( - dataUrl, - "jpeg", - 0, - 0, - canvas.offsetWidth, - canvas.offsetHeight - ); - doc.save(`${exportData.filename}.pdf`); - } - ); + toJpeg(canvas).then(function (dataUrl) { + const doc = new jsPDF("l", "px", [ + canvas.offsetWidth, + canvas.offsetHeight, + ]); + doc.addImage( + dataUrl, + "jpeg", + 0, + 0, + canvas.offsetWidth, + canvas.offsetHeight + ); + doc.save(`${exportData.filename}.pdf`); + }); }, }, ], @@ -494,7 +502,7 @@ export default function ControlPanel(props) { { if (visible === MODAL.IMG) { @@ -507,6 +515,7 @@ export default function ControlPanel(props) { type: "text/plain;charset=utf-8", }); saveAs(blob, `${exportData.filename}.${exportData.extension}`); + } else if (visible === MODAL.IMPORT) { } }} afterClose={() => { @@ -515,44 +524,101 @@ export default function ControlPanel(props) { extension: "", filename: `diagram_${new Date().toISOString()}`, })); + setError(null); }} onCancel={() => setVisible(MODAL.NONE)} centered closeOnEsc={true} - okText="Export" + okText={`${visible === MODAL.IMPORT ? "Import" : "Export"}`} cancelText="Cancel" width={520} > - {exportData.data !== "" || exportData.data ? ( - visible === MODAL.IMG ? ( - Diagram - ) : ( -
- + { + const f = fileList[0].fileInstance; + if (!f) { + return; + } + + const reader = new FileReader(); + reader.onload = function (event) { + if (f.type === "application/json") { + let jsonObject = null; + try { + jsonObject = JSON.parse(event.target.result); + } catch (error) { + setError("Invalid JSON. The file contains an error."); + return; + } + + if (!diagramObjectIsValid(jsonObject)) { + setError( + "The file is missing necessary properties for a diagram." + ); + return; + } + } + }; + reader.readAsText(f); + + return { + autoRemove: false, + fileInstance: file.fileInstance, + status: "success", + shouldUpload: false, + }; + }} + draggable={true} + dragMainText="Click to upload the file or drag and drop the file here" + dragSubText="Support json" + accept="application/json,.txt" + onRemove={() => setError(null)} + onFileChange={() => setError(null)} + limit={1} + > + {error && ( + {error}
} /> - - ) + )} + + ) : exportData.data !== "" || exportData.data ? ( + <> + {visible === MODAL.IMG ? ( + Diagram + ) : ( +
+ +
+ )} +
Filename:
+ {`.${exportData.extension}`}} + onChange={(value) => + setExportData((prev) => ({ ...prev, filename: value })) + } + field="filename" + /> + ) : (
)} -
Filename :
- {`.${exportData.extension}`}} - onChange={(value) => - setExportData((prev) => ({ ...prev, filename: value })) - } - field="filename" - />
); diff --git a/src/schemas/index.js b/src/schemas/index.js new file mode 100644 index 0000000..4990fca --- /dev/null +++ b/src/schemas/index.js @@ -0,0 +1,144 @@ +const jsonSchema = { + type: "object", + properties: { + tables: { + type: "array", + items: { + type: "object", + properties: { + id: { type: "integer" }, + name: { type: "string" }, + x: { type: "number" }, + y: { type: "number" }, + fields: { + type: "array", + items: { + type: "object", + properties: { + name: { type: "string" }, + type: { type: "string" }, + default: { type: "string" }, + check: { type: "string" }, + primary: { type: "boolean" }, + unique: { type: "boolean" }, + notNull: { type: "boolean" }, + increment: { type: "boolean" }, + comment: { type: "string" }, + }, + required: [ + "name", + "type", + "default", + "check", + "primary", + "unique", + "notNull", + "increment", + "comment", + ], + }, + }, + comment: { type: "string" }, + indices: { + type: "array", + items: { + type: "object", + properties: { + name: { type: "string" }, + fields: { + type: "array", + items: { type: "string" }, + }, + }, + required: ["name", "fields"], + }, + }, + color: { type: "string", pattern: "^#[0-9a-fA-F]{6}$" }, + }, + required: [ + "id", + "name", + "x", + "y", + "fields", + "comment", + "indices", + "color", + ], + }, + }, + relationships: { + type: "array", + items: { + type: "object", + properties: { + startTableId: { type: "integer" }, + startFieldId: { type: "integer" }, + endTableId: { type: "integer" }, + endFieldId: { type: "integer" }, + startX: { type: "number" }, + startY: { type: "number" }, + endX: { type: "number" }, + endY: { type: "number" }, + name: { type: "string" }, + cardinality: { type: "string" }, + updateConstraint: { type: "string" }, + deleteConstraint: { type: "string" }, + mandatory: { type: "boolean" }, + id: { type: "integer" }, + }, + required: [ + "startTableId", + "startFieldId", + "endTableId", + "endFieldId", + "startX", + "startY", + "endX", + "endY", + "name", + "cardinality", + "updateConstraint", + "deleteConstraint", + "mandatory", + "id", + ], + }, + }, + notes: { + type: "array", + items: { + type: "object", + properties: { + id: { type: "integer" }, + x: { type: "number" }, + y: { type: "number" }, + title: { type: "string" }, + content: { type: "string" }, + color: { type: "string", pattern: "^#[0-9a-fA-F]{6}$" }, + height: { type: "number" }, + }, + required: ["id", "x", "y", "title", "content", "color", "height"], + }, + }, + subjectAreas: { + type: "array", + items: { + type: "object", + properties: { + id: { type: "integer" }, + name: { type: "string" }, + x: { type: "number" }, + y: { type: "number" }, + width: { type: "number" }, + height: { type: "number" }, + color: { type: "string", pattern: "^#[0-9a-fA-F]{6}$" }, + }, + required: ["id", "name", "x", "y", "width", "height", "color"], + }, + }, + }, + required: ["tables", "relationships", "notes", "subjectAreas"], +}; + +export { jsonSchema }; diff --git a/src/utils/index.js b/src/utils/index.js index cc83830..0150fd2 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -1,3 +1,6 @@ +import { Validator } from "jsonschema"; +import { jsonSchema } from "../schemas"; + const enterFullscreen = () => { const element = document.documentElement; if (element.requestFullscreen) { @@ -23,4 +26,8 @@ const exitFullscreen = () => { } }; -export { enterFullscreen, exitFullscreen }; +const diagramObjectIsValid = (obj) => { + return new Validator().validate(obj, jsonSchema).valid; +}; + +export { enterFullscreen, exitFullscreen, diagramObjectIsValid };