diff --git a/package-lock.json b/package-lock.json index 0449a40..66c6f1b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "jsonschema": "^1.4.1", "jspdf": "^2.5.1", "lexical": "^0.12.5", + "node-sql-parser": "^4.17.0", "react": "^18.2.0", "react-cookie": "^7.0.1", "react-dom": "^18.2.0", @@ -1975,6 +1976,14 @@ "resolved": "https://registry.npmjs.org/bezier-easing/-/bezier-easing-2.1.0.tgz", "integrity": "sha512-gbIqZ/eslnUFC1tjEvtz0sgx+xTK20wDnYMIA27VA04R7w6xxXQPZDbibjA9DTWZRA2CXtwHykkVzlCaAJAZig==" }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "engines": { + "node": ">=0.6" + } + }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -4122,6 +4131,17 @@ "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", "dev": true }, + "node_modules/node-sql-parser": { + "version": "4.17.0", + "resolved": "https://registry.npmjs.org/node-sql-parser/-/node-sql-parser-4.17.0.tgz", + "integrity": "sha512-3IhovpmUBpcETnoKK/KBdkz2mz53kVG5E1dnqz1QuYvtzdxYZW5xaGGEvW9u6Yyy2ivwR3eUZrn9inmEVef02w==", + "dependencies": { + "big-integer": "^1.6.48" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", diff --git a/package.json b/package.json index 92bc695..1edc03a 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "jsonschema": "^1.4.1", "jspdf": "^2.5.1", "lexical": "^0.12.5", + "node-sql-parser": "^4.17.0", "react": "^18.2.0", "react-cookie": "^7.0.1", "react-dom": "^18.2.0", diff --git a/src/components/ControlPanel.jsx b/src/components/ControlPanel.jsx index 86d647a..b203faf 100644 --- a/src/components/ControlPanel.jsx +++ b/src/components/ControlPanel.jsx @@ -64,6 +64,7 @@ import { areaSchema, noteSchema, tableSchema } from "../data/schemas"; import { Editor } from "@monaco-editor/react"; import { db } from "../data/db"; import { useLiveQuery } from "dexie-react-hooks"; +import { Parser } from "node-sql-parser"; import Todo from "./Todo"; export default function ControlPanel({ @@ -84,6 +85,7 @@ export default function ControlPanel({ OPEN: 5, SAVEAS: 6, NEW: 7, + IMPORT_SRC: 8, }; const STATUS = { NONE: 0, @@ -823,10 +825,13 @@ export default function ControlPanel({ .catch(() => Toast.error("Oops! Something went wrong.")); }, }, - Import: { + "Import diagram": { function: fileImport, shortcut: "Ctrl+I", }, + "Import from source": { + function: () => setVisible(MODAL.IMPORT_SRC) + }, "Export as": { children: [ { @@ -1172,6 +1177,7 @@ export default function ControlPanel({ const getModalTitle = () => { switch (visible) { case MODAL.IMPORT: + case MODAL.IMPORT_SRC: return "Import diagram"; case MODAL.CODE: return "Export source"; @@ -1193,6 +1199,7 @@ export default function ControlPanel({ const getOkText = () => { switch (visible) { case MODAL.IMPORT: + case MODAL.IMPORT_SRC: return "Import"; case MODAL.CODE: case MODAL.IMG: @@ -1235,6 +1242,9 @@ export default function ControlPanel({ setRedoStack([]); } return; + case MODAL.IMPORT_SRC: + setVisible(MODAL.NONE) + return; case MODAL.OPEN: if (selectedDiagramId === 0) return; loadDiagram(selectedDiagramId); @@ -1373,6 +1383,76 @@ export default function ControlPanel({ ); }; + const importSrcModalBody = () => { + return ( + <> + { + const f = fileList[0].fileInstance; + if (!f) { + return; + } + const reader = new FileReader(); + reader.onload = async (e) => { + console.log(e.target.result); + const parser = new Parser(); + const ast = parser.astify(e.target.result); + console.log(ast); + }; + reader.readAsText(f); + + return { + autoRemove: false, + fileInstance: file.fileInstance, + status: "success", + shouldUpload: false, + }; + }} + draggable={true} + dragMainText="Drag and drop the file here or click to upload." + dragSubText="Upload an sql file to autogenerate your tables and columns." + accept=".sql" + onRemove={() => + setError({ + type: STATUS.NONE, + message: "", + }) + } + onFileChange={() => + setError({ + type: STATUS.NONE, + message: "", + }) + } + limit={1} + > + + {error.type === STATUS.ERROR ? ( + {error.message}} + /> + ) : error.type === STATUS.OK ? ( + {error.message}} + /> + ) : ( + error.type === STATUS.WARNING && ( + {error.message}} + /> + ) + )} + + ); + }; + const newModalBody = () => (
@@ -1408,6 +1488,8 @@ export default function ControlPanel({ switch (visible) { case MODAL.IMPORT: return importModalBody(); + case MODAL.IMPORT_SRC: + return importSrcModalBody(); case MODAL.NEW: return newModalBody(); case MODAL.RENAME: diff --git a/src/utils/index.js b/src/utils/index.js index 8a152ce..243cfb6 100644 --- a/src/utils/index.js +++ b/src/utils/index.js @@ -95,8 +95,8 @@ function getTypeString(field, dbms = "mysql") { if (isSized(field.type)) { return `${field.type}(${field.size})`; } - if (hasPrecision(field.type) && field.size !== "") { - return `${field.type}${field.size}`; + if (hasPrecision(field.type)) { + return `${field.type}${field.size ? `(${field.size})` : ""}`; } if (field.type === "SET" || field.type === "ENUM") { return `${field.type}(${field.values.map((v) => `"${v}"`).join(", ")})`; @@ -132,8 +132,8 @@ function getTypeString(field, dbms = "mysql") { field.type === "BINARY" ? "bit" : field.type === "VARBINARY" - ? "bit varying" - : field.type.toLowerCase(); + ? "bit varying" + : field.type.toLowerCase(); return `${type}(${field.size})`; } if (hasPrecision(field.type) && field.size !== "") { @@ -161,68 +161,56 @@ function jsonToMySQL(obj) { return `${obj.tables .map( (table) => - `${ - table.comment === "" ? "" : `/* ${table.comment} */\n` + `${table.comment === "" ? "" : `/* ${table.comment} */\n` }CREATE TABLE \`${table.name}\` (\n${table.fields .map( (field) => - `${field.comment === "" ? "" : `\t-- ${field.comment}\n`}\t\`${ - field.name - }\` ${getTypeString(field)}${field.notNull ? " NOT NULL" : ""}${ - field.increment ? " AUTO_INCREMENT" : "" - }${field.unique ? " UNIQUE" : ""}${ - field.default !== "" - ? ` DEFAULT ${ - hasQuotes(field.type) && - field.default.toLowerCase() !== "null" - ? `"${field.default}"` - : `${field.default}` - }` + `${field.comment === "" ? "" : `\t-- ${field.comment}\n`}\t\`${field.name + }\` ${getTypeString(field)}${field.notNull ? " NOT NULL" : ""}${field.increment ? " AUTO_INCREMENT" : "" + }${field.unique ? " UNIQUE" : ""}${field.default !== "" + ? ` DEFAULT ${hasQuotes(field.type) && + field.default.toLowerCase() !== "null" + ? `"${field.default}"` + : `${field.default}` + }` + : "" + }${field.check === "" || !hasCheck(field.type) + ? !sqlDataTypes.includes(field.type) + ? ` CHECK(\n\t\tJSON_SCHEMA_VALID("${generateSchema( + obj.types.find( + (t) => t.name === field.type.toLowerCase() + ) + )}", \`${field.name}\`))` : "" - }${ - field.check === "" || !hasCheck(field.type) - ? !sqlDataTypes.includes(field.type) - ? ` CHECK(\n\t\tJSON_SCHEMA_VALID("${generateSchema( - obj.types.find( - (t) => t.name === field.type.toLowerCase() - ) - )}", \`${field.name}\`))` - : "" - : ` CHECK(${field.check})` + : ` CHECK(${field.check})` }` ) - .join(",\n")}${ - table.fields.filter((f) => f.primary).length > 0 + .join(",\n")}${table.fields.filter((f) => f.primary).length > 0 ? `,\n\tPRIMARY KEY(${table.fields - .filter((f) => f.primary) - .map((f) => `\`${f.name}\``) - .join(", ")})` - : "" - }\n);\n${ - table.indices.length > 0 - ? `\n${table.indices.map( - (i) => - `\nCREATE ${i.unique ? "UNIQUE " : ""}INDEX \`${ - i.name - }\`\nON \`${table.name}\` (${i.fields - .map((f) => `\`${f}\``) - .join(", ")});` - )}` + .filter((f) => f.primary) + .map((f) => `\`${f.name}\``) + .join(", ")})` : "" + }\n);\n${table.indices.length > 0 + ? `\n${table.indices.map( + (i) => + `\nCREATE ${i.unique ? "UNIQUE " : ""}INDEX \`${i.name + }\`\nON \`${table.name}\` (${i.fields + .map((f) => `\`${f}\``) + .join(", ")});` + )}` + : "" }` ) .join("\n")}\n${obj.references - .map( - (r) => - `ALTER TABLE \`${ - obj.tables[r.startTableId].name - }\`\nADD FOREIGN KEY(\`${ - obj.tables[r.startTableId].fields[r.startFieldId].name - }\`) REFERENCES \`${obj.tables[r.endTableId].name}\`(\`${ - obj.tables[r.endTableId].fields[r.endFieldId].name - }\`)\nON UPDATE ${r.updateConstraint.toUpperCase()} ON DELETE ${r.deleteConstraint.toUpperCase()};` - ) - .join("\n")}`; + .map( + (r) => + `ALTER TABLE \`${obj.tables[r.startTableId].name + }\`\nADD FOREIGN KEY(\`${obj.tables[r.startTableId].fields[r.startFieldId].name + }\`) REFERENCES \`${obj.tables[r.endTableId].name}\`(\`${obj.tables[r.endTableId].fields[r.endFieldId].name + }\`)\nON UPDATE ${r.updateConstraint.toUpperCase()} ON DELETE ${r.deleteConstraint.toUpperCase()};` + ) + .join("\n")}`; } function jsonToPostgreSQL(obj) { @@ -238,86 +226,73 @@ function jsonToPostgreSQL(obj) { if (typeStatements.length > 0) { return ( typeStatements.join("") + - `${ - type.comment === "" ? "" : `/**\n${type.comment}\n*/\n` + `${type.comment === "" ? "" : `/**\n${type.comment}\n*/\n` }CREATE TYPE ${type.name} AS (\n${type.fields .map((f) => `\t${f.name} ${getTypeString(f, "postgres")}`) .join("\n")}\n);` ); } else { - return `${ - type.comment === "" ? "" : `/**\n${type.comment}\n*/\n` - }CREATE TYPE ${type.name} AS (\n${type.fields - .map((f) => `\t${f.name} ${getTypeString(f, "postgres")}`) - .join("\n")}\n);`; + return `${type.comment === "" ? "" : `/**\n${type.comment}\n*/\n` + }CREATE TYPE ${type.name} AS (\n${type.fields + .map((f) => `\t${f.name} ${getTypeString(f, "postgres")}`) + .join("\n")}\n);`; } })}\n${obj.tables .map( (table) => - `${table.comment === "" ? "" : `/**\n${table.comment}\n*/\n`}${ - table.fields.filter((f) => f.type === "ENUM" || f.type === "SET") - .length > 0 - ? `${table.fields - .filter((f) => f.type === "ENUM" || f.type === "SET") - .map( - (f) => - `CREATE TYPE "${f.name}_t" AS ENUM (${f.values - .map((v) => `'${v}'`) - .join(", ")});\n\n` - )}` - : "" + `${table.comment === "" ? "" : `/**\n${table.comment}\n*/\n`}${table.fields.filter((f) => f.type === "ENUM" || f.type === "SET") + .length > 0 + ? `${table.fields + .filter((f) => f.type === "ENUM" || f.type === "SET") + .map( + (f) => + `CREATE TYPE "${f.name}_t" AS ENUM (${f.values + .map((v) => `'${v}'`) + .join(", ")});\n\n` + )}` + : "" }CREATE TABLE "${table.name}" (\n${table.fields .map( (field) => - `${field.comment === "" ? "" : `\t-- ${field.comment}\n`}\t"${ - field.name - }" ${getTypeString(field, "postgres")}${ - field.notNull ? " NOT NULL" : "" - }${ - field.default !== "" - ? ` DEFAULT ${ - hasQuotes(field.type) && - field.default.toLowerCase() !== "null" - ? `'${field.default}'` - : `${field.default}` - }` - : "" - }${ - field.check === "" || !hasCheck(field.type) - ? "" - : ` CHECK(${field.check})` + `${field.comment === "" ? "" : `\t-- ${field.comment}\n`}\t"${field.name + }" ${getTypeString(field, "postgres")}${field.notNull ? " NOT NULL" : "" + }${field.default !== "" + ? ` DEFAULT ${hasQuotes(field.type) && + field.default.toLowerCase() !== "null" + ? `'${field.default}'` + : `${field.default}` + }` + : "" + }${field.check === "" || !hasCheck(field.type) + ? "" + : ` CHECK(${field.check})` }` ) - .join(",\n")}${ - table.fields.filter((f) => f.primary).length > 0 + .join(",\n")}${table.fields.filter((f) => f.primary).length > 0 ? `,\n\tPRIMARY KEY(${table.fields - .filter((f) => f.primary) - .map((f) => `"${f.name}"`) - .join(", ")})` - : "" - }\n);\n${ - table.indices.length > 0 - ? `${table.indices.map( - (i) => - `\nCREATE ${i.unique ? "UNIQUE " : ""}INDEX "${ - i.name - }"\nON "${table.name}" (${i.fields - .map((f) => `"${f}"`) - .join(", ")});` - )}` + .filter((f) => f.primary) + .map((f) => `"${f.name}"`) + .join(", ")})` : "" + }\n);\n${table.indices.length > 0 + ? `${table.indices.map( + (i) => + `\nCREATE ${i.unique ? "UNIQUE " : ""}INDEX "${i.name + }"\nON "${table.name}" (${i.fields + .map((f) => `"${f}"`) + .join(", ")});` + )}` + : "" }` ) .join("\n")}\n${obj.references - .map( - (r) => - `ALTER TABLE "${obj.tables[r.startTableId].name}"\nADD FOREIGN KEY("${ - obj.tables[r.startTableId].fields[r.startFieldId].name - }") REFERENCES "${obj.tables[r.endTableId].name}"("${ - obj.tables[r.endTableId].fields[r.endFieldId].name - }")\nON UPDATE ${r.updateConstraint.toUpperCase()} ON DELETE ${r.deleteConstraint.toUpperCase()};` - ) - .join("\n")}`; + .map( + (r) => + `ALTER TABLE "${obj.tables[r.startTableId].name}"\nADD FOREIGN KEY("${obj.tables[r.startTableId].fields[r.startFieldId].name + }") REFERENCES "${obj.tables[r.endTableId].name}"("${obj.tables[r.endTableId].fields[r.endFieldId].name + }")\nON UPDATE ${r.updateConstraint.toUpperCase()} ON DELETE ${r.deleteConstraint.toUpperCase()};` + ) + .join("\n")}`; } function arrayIsEqual(arr1, arr2) { @@ -325,13 +300,13 @@ function arrayIsEqual(arr1, arr2) { } function isSized(type) { - return ["CHAR", "VARCHAR", "BINARY", "VARBINARY", "TEXT", "FLOAT"].includes( + return ["CHAR", "VARCHAR", "BINARY", "VARBINARY", "TEXT"].includes( type ); } function hasPrecision(type) { - return ["DOUBLE", "NUMERIC", "DECIMAL"].includes(type); + return ["DOUBLE", "NUMERIC", "DECIMAL", "FLOAT"].includes(type); } function hasCheck(type) { @@ -569,11 +544,9 @@ function validateDiagram(diagram) { diagram.tables[r.startTableId].fields[r.startFieldId].type !== diagram.tables[r.endTableId].fields[r.endFieldId].type ) { - issues.push(`Referencing column "${ - diagram.tables[r.endTableId].fields[r.endFieldId].name - }" and referenced column "${ - diagram.tables[r.startTableId].fields[r.startFieldId].name - }" are incompatible. + issues.push(`Referencing column "${diagram.tables[r.endTableId].fields[r.endFieldId].name + }" and referenced column "${diagram.tables[r.startTableId].fields[r.startFieldId].name + }" are incompatible. `); } }); @@ -632,81 +605,56 @@ const calcPath = (x1, x2, y1, y2, startFieldId, endFieldId, zoom = 1) => { if (y1 <= y2) { if (x1 + tableWidth <= x2) { - return `M ${x1 + tableWidth - offsetX * 2} ${y1} L ${ - midX - r - } ${y1} A ${r} ${r} 0 0 1 ${midX} ${y1 + r} L ${midX} ${ - y2 - r - } A ${r} ${r} 0 0 0 ${midX + r} ${y2} L ${endX} ${y2}`; + return `M ${x1 + tableWidth - offsetX * 2} ${y1} L ${midX - r + } ${y1} A ${r} ${r} 0 0 1 ${midX} ${y1 + r} L ${midX} ${y2 - r + } A ${r} ${r} 0 0 0 ${midX + r} ${y2} L ${endX} ${y2}`; } else if (x2 <= x1 + tableWidth && x1 <= x2) { - return `M ${x1 + tableWidth - 2 * offsetX} ${y1} L ${ - x2 + tableWidth - } ${y1} A ${r} ${r} 0 0 1 ${x2 + tableWidth + r} ${y1 + r} L ${ - x2 + tableWidth + r - } ${y2 - r} A ${r} ${r} 0 0 1 ${x2 + tableWidth} ${y2} L ${ - x2 + tableWidth - 2 * offsetX - } ${y2}`; + return `M ${x1 + tableWidth - 2 * offsetX} ${y1} L ${x2 + tableWidth + } ${y1} A ${r} ${r} 0 0 1 ${x2 + tableWidth + r} ${y1 + r} L ${x2 + tableWidth + r + } ${y2 - r} A ${r} ${r} 0 0 1 ${x2 + tableWidth} ${y2} L ${x2 + tableWidth - 2 * offsetX + } ${y2}`; } else if (x2 + tableWidth >= x1 && x2 + tableWidth <= x1 + tableWidth) { - return `M ${x1} ${y1} L ${x2 - r} ${y1} A ${r} ${r} 0 0 0 ${x2 - r - r} ${ - y1 + r - } L ${x2 - r - r} ${y2 - r} A ${r} ${r} 0 0 0 ${ - x2 - r - } ${y2} L ${x2} ${y2}`; + return `M ${x1} ${y1} L ${x2 - r} ${y1} A ${r} ${r} 0 0 0 ${x2 - r - r} ${y1 + r + } L ${x2 - r - r} ${y2 - r} A ${r} ${r} 0 0 0 ${x2 - r + } ${y2} L ${x2} ${y2}`; } else { - return `M ${x1} ${y1} L ${midX + r} ${y1} A ${r} ${r} 0 0 0 ${midX} ${ - y1 + r - } L ${midX} ${y2 - r} A ${r} ${r} 0 0 1 ${ - midX - r - } ${y2} L ${endX} ${y2}`; + return `M ${x1} ${y1} L ${midX + r} ${y1} A ${r} ${r} 0 0 0 ${midX} ${y1 + r + } L ${midX} ${y2 - r} A ${r} ${r} 0 0 1 ${midX - r + } ${y2} L ${endX} ${y2}`; } } else { if (x1 + tableWidth <= x2) { - return `M ${x1 + tableWidth - offsetX * 2} ${y1} L ${ - midX - r - } ${y1} A ${r} ${r} 0 0 0 ${midX} ${y1 - r} L ${midX} ${ - y2 + r - } A ${r} ${r} 0 0 1 ${midX + r} ${y2} L ${endX} ${y2}`; + return `M ${x1 + tableWidth - offsetX * 2} ${y1} L ${midX - r + } ${y1} A ${r} ${r} 0 0 0 ${midX} ${y1 - r} L ${midX} ${y2 + r + } A ${r} ${r} 0 0 1 ${midX + r} ${y2} L ${endX} ${y2}`; } else if (x1 + tableWidth >= x2 && x1 + tableWidth <= x2 + tableWidth) { // this for the overlap remember if (startTableY < y2) { - return `M ${x1} ${y1} L ${x1 - r - r} ${y1} A ${r} ${r} 0 0 1 ${ - x1 - r - r - r - } ${y1 - r} L ${x1 - r - r - r} ${y2 + r} A ${r} ${r} 0 0 1 ${ - x1 - r - r - } ${y2} L ${x1 - r - 4} ${y2}`; + return `M ${x1} ${y1} L ${x1 - r - r} ${y1} A ${r} ${r} 0 0 1 ${x1 - r - r - r + } ${y1 - r} L ${x1 - r - r - r} ${y2 + r} A ${r} ${r} 0 0 1 ${x1 - r - r + } ${y2} L ${x1 - r - 4} ${y2}`; } - return `M ${x1} ${y1} L ${x1 - r - r} ${y1} A ${r} ${r} 0 0 1 ${ - x1 - r - r - r - } ${y1 - r} L ${x1 - r - r - r} ${y2 + r} A ${r} ${r} 0 0 1 ${ - x1 - r - r - } ${y2} L ${endX} ${y2}`; + return `M ${x1} ${y1} L ${x1 - r - r} ${y1} A ${r} ${r} 0 0 1 ${x1 - r - r - r + } ${y1 - r} L ${x1 - r - r - r} ${y2 + r} A ${r} ${r} 0 0 1 ${x1 - r - r + } ${y2} L ${endX} ${y2}`; } else if (x1 >= x2 && x1 <= x2 + tableWidth) { // this for the overlap remember if (startTableY < y2) { - return `M ${x1 + tableWidth - 2 * offsetX} ${y1} L ${ - x1 + tableWidth - 2 * offsetX + r - } ${y1} A ${r} ${r} 0 0 0 ${x1 + tableWidth - 2 * offsetX + r + r} ${ - y1 - r - } L ${x1 + tableWidth - 2 * offsetX + r + r} ${ - y2 + r - } A ${r} ${r} 0 0 0 ${x1 + tableWidth - 2 * offsetX + r} ${y2} L ${ - x1 + tableWidth - 16 - } ${y2}`; + return `M ${x1 + tableWidth - 2 * offsetX} ${y1} L ${x1 + tableWidth - 2 * offsetX + r + } ${y1} A ${r} ${r} 0 0 0 ${x1 + tableWidth - 2 * offsetX + r + r} ${y1 - r + } L ${x1 + tableWidth - 2 * offsetX + r + r} ${y2 + r + } A ${r} ${r} 0 0 0 ${x1 + tableWidth - 2 * offsetX + r} ${y2} L ${x1 + tableWidth - 16 + } ${y2}`; } - return `M ${x1 + tableWidth - 2 * offsetX} ${y1} L ${ - x1 + tableWidth - 2 * offsetX + r - } ${y1} A ${r} ${r} 0 0 0 ${x1 + tableWidth - 2 * offsetX + r + r} ${ - y1 - r - } L ${x1 + tableWidth - 2 * offsetX + r + r} ${ - y2 + r - } A ${r} ${r} 0 0 0 ${x1 + tableWidth - 2 * offsetX + r} ${y2} L ${ - x2 + tableWidth - 2 * offsetX - } ${y2}`; + return `M ${x1 + tableWidth - 2 * offsetX} ${y1} L ${x1 + tableWidth - 2 * offsetX + r + } ${y1} A ${r} ${r} 0 0 0 ${x1 + tableWidth - 2 * offsetX + r + r} ${y1 - r + } L ${x1 + tableWidth - 2 * offsetX + r + r} ${y2 + r + } A ${r} ${r} 0 0 0 ${x1 + tableWidth - 2 * offsetX + r} ${y2} L ${x2 + tableWidth - 2 * offsetX + } ${y2}`; } else { - return `M ${x1} ${y1} L ${midX + r} ${y1} A ${r} ${r} 0 0 1 ${midX} ${ - y1 - r - } L ${midX} ${y2 + r} A ${r} ${r} 0 0 0 ${ - midX - r - } ${y2} L ${endX} ${y2}`; + return `M ${x1} ${y1} L ${midX + r} ${y1} A ${r} ${r} 0 0 1 ${midX} ${y1 - r + } L ${midX} ${y2 + r} A ${r} ${r} 0 0 0 ${midX - r + } ${y2} L ${endX} ${y2}`; } } };