diff --git a/src/components/EditorCanvas/Table.jsx b/src/components/EditorCanvas/Table.jsx deleted file mode 100644 index 1e656f9a..00000000 --- a/src/components/EditorCanvas/Table.jsx +++ /dev/null @@ -1,376 +0,0 @@ -import { useState } from "react"; -import { - Tab, - ObjectType, - tableFieldHeight, - tableHeaderHeight, - tableColorStripHeight, -} from "../../data/constants"; -import { - IconEdit, - IconMore, - IconMinus, - IconDeleteStroked, - IconKeyStroked, -} from "@douyinfe/semi-icons"; -import { Popover, Tag, Button, SideSheet } from "@douyinfe/semi-ui"; -import { useLayout, useSettings, useDiagram, useSelect } from "../../hooks"; -import TableInfo from "../EditorSidePanel/TablesTab/TableInfo"; -import { useTranslation } from "react-i18next"; -import { dbToTypes } from "../../data/datatypes"; -import { isRtl } from "../../i18n/utils/rtl"; -import i18n from "../../i18n/i18n"; - -export default function Table(props) { - const [hoveredField, setHoveredField] = useState(-1); - const { database } = useDiagram(); - const { - tableData, - onPointerDown, - setHoveredTable, - handleGripField, - setLinkingLine, - } = props; - const { layout } = useLayout(); - const { deleteTable, deleteField } = useDiagram(); - const { settings } = useSettings(); - const { t } = useTranslation(); - const { selectedElement, setSelectedElement } = useSelect(); - - const height = - tableData.fields.length * tableFieldHeight + tableHeaderHeight + 7; - const openEditor = () => { - if (!layout.sidebar) { - setSelectedElement((prev) => ({ - ...prev, - element: ObjectType.TABLE, - id: tableData.id, - open: true, - })); - } else { - setSelectedElement((prev) => ({ - ...prev, - currentTab: Tab.TABLES, - element: ObjectType.TABLE, - id: tableData.id, - open: true, - })); - if (selectedElement.currentTab !== Tab.TABLES) return; - document - .getElementById(`scroll_table_${tableData.id}`) - .scrollIntoView({ behavior: "smooth" }); - } - }; - - return ( - <> - <foreignObject - key={tableData.id} - x={tableData.x} - y={tableData.y} - width={settings.tableWidth} - height={height} - className="group drop-shadow-lg rounded-md cursor-move" - onPointerDown={onPointerDown} - > - <div - onDoubleClick={openEditor} - className={`border-2 hover:border-dashed hover:border-blue-500 - select-none rounded-lg w-full ${ - settings.mode === "light" - ? "bg-zinc-100 text-zinc-800" - : "bg-zinc-800 text-zinc-200" - } ${ - selectedElement.id === tableData.id && - selectedElement.element === ObjectType.TABLE - ? "border-solid border-blue-500" - : "border-zinc-500" - }`} - style={{ direction: "ltr" }} - > - <div - className="h-[10px] w-full rounded-t-md" - style={{ backgroundColor: tableData.color }} - /> - <div - className={`overflow-hidden font-bold h-[40px] flex justify-between items-center border-b border-gray-400 ${ - settings.mode === "light" ? "bg-zinc-200" : "bg-zinc-900" - }`} - > - <div className=" px-3 overflow-hidden text-ellipsis whitespace-nowrap"> - {tableData.name} - </div> - <div className="hidden group-hover:block"> - <div className="flex justify-end items-center mx-2"> - <Button - icon={<IconEdit />} - size="small" - theme="solid" - style={{ - backgroundColor: "#2f68adb3", - marginRight: "6px", - }} - onClick={openEditor} - /> - <Popover - key={tableData.key} - content={ - <div className="popover-theme"> - <div className="mb-2"> - <strong>{t("comment")}:</strong>{" "} - {tableData.comment === "" ? ( - t("not_set") - ) : ( - <div>{tableData.comment}</div> - )} - </div> - <div> - <strong - className={`${ - tableData.indices.length === 0 ? "" : "block" - }`} - > - {t("indices")}: - </strong>{" "} - {tableData.indices.length === 0 ? ( - t("not_set") - ) : ( - <div> - {tableData.indices.map((index, k) => ( - <div - key={k} - className={`flex items-center my-1 px-2 py-1 rounded ${ - settings.mode === "light" - ? "bg-gray-100" - : "bg-zinc-800" - }`} - > - <i className="fa-solid fa-thumbtack me-2 mt-1 text-slate-500"></i> - <div> - {index.fields.map((f) => ( - <Tag color="blue" key={f} className="me-1"> - {f} - </Tag> - ))} - </div> - </div> - ))} - </div> - )} - </div> - <Button - icon={<IconDeleteStroked />} - type="danger" - block - style={{ marginTop: "8px" }} - onClick={() => deleteTable(tableData.id)} - > - {t("delete")} - </Button> - </div> - } - position="rightTop" - showArrow - trigger="click" - style={{ width: "200px", wordBreak: "break-word" }} - > - <Button - icon={<IconMore />} - type="tertiary" - size="small" - style={{ - backgroundColor: "#808080b3", - color: "white", - }} - /> - </Popover> - </div> - </div> - </div> - {tableData.fields.map((e, i) => { - return settings.showFieldSummary ? ( - <Popover - key={i} - content={ - <div className="popover-theme"> - <div - className="flex justify-between items-center pb-2" - style={{ direction: "ltr" }} - > - <p className="me-4 font-bold">{e.name}</p> - <p className="ms-4"> - {e.type + - ((dbToTypes[database][e.type].isSized || - dbToTypes[database][e.type].hasPrecision) && - e.size && - e.size !== "" - ? "(" + e.size + ")" - : "")} - </p> - </div> - <hr /> - {e.primary && ( - <Tag color="blue" className="me-2 my-2"> - {t("primary")} - </Tag> - )} - {e.unique && ( - <Tag color="amber" className="me-2 my-2"> - {t("unique")} - </Tag> - )} - {e.notNull && ( - <Tag color="purple" className="me-2 my-2"> - {t("not_null")} - </Tag> - )} - {e.increment && ( - <Tag color="green" className="me-2 my-2"> - {t("autoincrement")} - </Tag> - )} - <p> - <strong>{t("default_value")}: </strong> - {e.default === "" ? t("not_set") : e.default} - </p> - <p> - <strong>{t("comment")}: </strong> - {e.comment === "" ? t("not_set") : e.comment} - </p> - </div> - } - position="right" - showArrow - style={ - isRtl(i18n.language) - ? { direction: "rtl" } - : { direction: "ltr" } - } - > - {field(e, i)} - </Popover> - ) : ( - field(e, i) - ); - })} - </div> - </foreignObject> - <SideSheet - title={t("edit")} - size="small" - visible={ - selectedElement.element === ObjectType.TABLE && - selectedElement.id === tableData.id && - selectedElement.open && - !layout.sidebar - } - onCancel={() => - setSelectedElement((prev) => ({ - ...prev, - open: !prev.open, - })) - } - style={{ paddingBottom: "16px" }} - > - <div className="sidesheet-theme"> - <TableInfo data={tableData} /> - </div> - </SideSheet> - </> - ); - - function field(fieldData, index) { - return ( - <div - className={`${ - index === tableData.fields.length - 1 - ? "" - : "border-b border-gray-400" - } group h-[36px] px-2 py-1 flex justify-between items-center gap-1 w-full overflow-hidden`} - onPointerEnter={(e) => { - if (!e.isPrimary) return; - - setHoveredField(index); - setHoveredTable({ - tableId: tableData.id, - field: index, - }); - }} - onPointerLeave={(e) => { - if (!e.isPrimary) return; - - setHoveredField(-1); - }} - onPointerDown={(e) => { - // Required for onPointerLeave to trigger when a touch pointer leaves - // https://stackoverflow.com/a/70976017/1137077 - e.target.releasePointerCapture(e.pointerId); - }} - > - <div - className={`${ - hoveredField === index ? "text-zinc-400" : "" - } flex items-center gap-2 overflow-hidden`} - > - <button - className="flex-shrink-0 w-[10px] h-[10px] bg-[#2f68adcc] rounded-full" - onPointerDown={(e) => { - if (!e.isPrimary) return; - - handleGripField(index); - setLinkingLine((prev) => ({ - ...prev, - startFieldId: index, - startTableId: tableData.id, - startX: tableData.x + 15, - startY: - tableData.y + - index * tableFieldHeight + - tableHeaderHeight + - tableColorStripHeight + - 12, - endX: tableData.x + 15, - endY: - tableData.y + - index * tableFieldHeight + - tableHeaderHeight + - tableColorStripHeight + - 12, - })); - }} - /> - <span className="overflow-hidden text-ellipsis whitespace-nowrap"> - {fieldData.name} - </span> - </div> - <div className="text-zinc-400"> - {hoveredField === index ? ( - <Button - theme="solid" - size="small" - style={{ - backgroundColor: "#d42020b3", - }} - icon={<IconMinus />} - onClick={() => deleteField(fieldData, tableData.id)} - /> - ) : ( - <div className="flex gap-1 items-center"> - {fieldData.primary && <IconKeyStroked />} - {!fieldData.notNull && <span>?</span>} - <span> - {fieldData.type + - ((dbToTypes[database][fieldData.type].isSized || - dbToTypes[database][fieldData.type].hasPrecision) && - fieldData.size && - fieldData.size !== "" - ? "(" + fieldData.size + ")" - : "")} - </span> - </div> - )} - </div> - </div> - ); - } -} diff --git a/src/components/EditorCanvas/Table/components/TableField.jsx b/src/components/EditorCanvas/Table/components/TableField.jsx new file mode 100644 index 00000000..e96ce7b8 --- /dev/null +++ b/src/components/EditorCanvas/Table/components/TableField.jsx @@ -0,0 +1,133 @@ +import React, { forwardRef } from "react"; +import { dbToTypes } from "../../../../data/datatypes"; +import { useDiagram } from "../../../../hooks"; +import { Button } from "@douyinfe/semi-ui"; +import { IconMinus, IconKeyStroked } from "@douyinfe/semi-icons"; + +const TableField = forwardRef((props, ref) => { + const { + tableData, + fieldData, + index, + setHoveredTable, + handleGripField, + setLinkingLine, + setHoveredField, + hoveredField, + tableFieldHeight, + tableHeaderHeight, + tableColorStripHeight, + } = props; + const { database, deleteField } = useDiagram(); + + const FieldSize = React.memo(({ field }) => { + let hasSize = + dbToTypes[database][field.type].isSized || + dbToTypes[database][field.type].hasPrecision; + let sizeValid = field.size && field.size !== ""; + + if (hasSize && sizeValid) { + return field.type + `(${field.size})`; + } else { + return field.type; + } + }); + + FieldSize.displayName = "FieldSize"; + + return ( + <div + // Popover children needs forwardRef and props destructuring to work with + // Functiona Components (https://semi.design/en-US/show/popover#Cautions) + ref={ref} + {...props} + className={`${ + index === tableData.fields.length - 1 ? "" : "border-b border-gray-400" + } group h-[36px] px-2 py-1 flex justify-between items-center gap-1 w-full overflow-hidden`} + onPointerEnter={(e) => { + if (!e.isPrimary) return; + + setHoveredField(index); + setHoveredTable({ + tableId: tableData.id, + field: index, + }); + }} + onPointerLeave={(e) => { + if (!e.isPrimary) return; + + setHoveredField(-1); + }} + onPointerDown={(e) => { + // Required for onPointerLeave to trigger when a touch pointer leaves + // https://stackoverflow.com/a/70976017/1137077 + e.target.releasePointerCapture(e.pointerId); + }} + > + <div + className={`${ + hoveredField === index ? "text-zinc-400" : "" + } flex items-center gap-2 overflow-hidden`} + > + <button + className="flex-shrink-0 w-[10px] h-[10px] bg-[#2f68adcc] rounded-full" + onPointerDown={(e) => { + if (!e.isPrimary) return; + + handleGripField(index); + setLinkingLine((prev) => ({ + ...prev, + startFieldId: index, + startTableId: tableData.id, + startX: tableData.x + 15, + startY: + tableData.y + + index * tableFieldHeight + + tableHeaderHeight + + tableColorStripHeight + + 12, + endX: tableData.x + 15, + endY: + tableData.y + + index * tableFieldHeight + + tableHeaderHeight + + tableColorStripHeight + + 12, + })); + }} + /> + <span className="overflow-hidden text-ellipsis whitespace-nowrap"> + {fieldData.name} + </span> + </div> + + <div className="text-zinc-400"> + {hoveredField === index ? ( + <Button + theme="solid" + size="small" + style={{ + backgroundColor: "#d42020b3", + }} + icon={<IconMinus />} + onClick={() => { + deleteField(fieldData, tableData.id); + }} + /> + ) : ( + <div className="flex gap-1 items-center"> + {fieldData.primary && <IconKeyStroked />} + {!fieldData.notNull && <span>?</span>} + <span> + <FieldSize field={fieldData} /> + </span> + </div> + )} + </div> + </div> + ); +}); + +TableField.displayName = "TableField"; + +export default TableField; diff --git a/src/components/EditorCanvas/Table/components/TableFieldPopover.jsx b/src/components/EditorCanvas/Table/components/TableFieldPopover.jsx new file mode 100644 index 00000000..00d19bee --- /dev/null +++ b/src/components/EditorCanvas/Table/components/TableFieldPopover.jsx @@ -0,0 +1,83 @@ +import React from "react"; +import { useTranslation } from "react-i18next"; +import i18n from "../../../../i18n/i18n"; +import { isRtl } from "../../../../i18n/utils/rtl"; +import { Popover, Tag } from "@douyinfe/semi-ui"; +import { dbToTypes } from "../../../../data/datatypes"; +import { useDiagram } from "../../../../hooks"; + +export default function TableFieldPopover({ fieldData, children, visible }) { + const { database } = useDiagram(); + const { t } = useTranslation(); + + if (!visible) { + return <React.Fragment>{children}</React.Fragment>; + } + + const FieldSize = React.memo(({ field }) => { + let hasSize = + dbToTypes[database][field.type].isSized || + dbToTypes[database][field.type].hasPrecision; + let sizeValid = field.size && field.size !== ""; + + if (hasSize && sizeValid) { + return `(${field.size})`; + } else { + return ""; + } + }); + + FieldSize.displayName = "FieldSize"; + + return ( + <Popover + content={ + <div className="popover-theme"> + <div + className="flex justify-between items-center pb-2" + style={{ direction: "ltr" }} + > + <p className="me-4 font-bold">{fieldData.name}</p> + <p className="ms-4">{<FieldSize field={fieldData} />}</p> + </div> + + <hr /> + + {fieldData.primary && ( + <Tag color="blue" className="me-2 my-2"> + {t("primary")} + </Tag> + )} + {fieldData.unique && ( + <Tag color="amber" className="me-2 my-2"> + {t("unique")} + </Tag> + )} + {fieldData.notNull && ( + <Tag color="purple" className="me-2 my-2"> + {t("not_null")} + </Tag> + )} + {fieldData.increment && ( + <Tag color="green" className="me-2 my-2"> + {t("autoincrement")} + </Tag> + )} + <p> + <strong>{t("default_value")}: </strong> + {fieldData.default === "" ? t("not_set") : fieldData.default} + </p> + <p> + <strong>{t("comment")}: </strong> + {fieldData.comment === "" ? t("not_set") : fieldData.comment} + </p> + </div> + } + position="right" + showArrow + style={isRtl(i18n.language) ? { direction: "rtl" } : { direction: "ltr" }} + > + {children} + </Popover> + ); +} diff --git a/src/components/EditorCanvas/Table/components/TableHeader.jsx b/src/components/EditorCanvas/Table/components/TableHeader.jsx new file mode 100644 index 00000000..7d214ac5 --- /dev/null +++ b/src/components/EditorCanvas/Table/components/TableHeader.jsx @@ -0,0 +1,105 @@ +import { IconEdit, IconMore, IconDeleteStroked } from "@douyinfe/semi-icons"; +import { Popover, Tag, Button } from "@douyinfe/semi-ui"; +import { useDiagram } from "../../../../hooks"; + +export default function TableHeader({ tableData, settings, openEditor, t }) { + const { deleteTable } = useDiagram(); + + return ( + <div + className={`overflow-hidden font-bold h-[40px] flex justify-between items-center border-b border-gray-400 ${ + settings.mode === "light" ? "bg-zinc-200" : "bg-zinc-900" + }`} + > + <div className="px-3 overflow-hidden text-ellipsis whitespace-nowrap"> + {tableData.name} + </div> + <div className="hidden group-hover:block"> + <div className="flex justify-end items-center mx-2"> + <Button + icon={<IconEdit />} + size="small" + theme="solid" + style={{ + backgroundColor: "#2f68adb3", + marginRight: "6px", + }} + onClick={openEditor} + /> + <Popover + key={tableData.key} + content={ + <div className="popover-theme"> + <div className="mb-2"> + <strong>{t("comment")}:</strong>{" "} + {tableData.comment === "" ? ( + t("not_set") + ) : ( + <div>{tableData.comment}</div> + )} + </div> + <div> + <strong + className={`${ + tableData.indices.length === 0 ? "" : "block" + }`} + > + {t("indices")}: + </strong>{" "} + {tableData.indices.length === 0 ? ( + t("not_set") + ) : ( + <div> + {tableData.indices.map((index, k) => ( + <div + key={k} + className={`flex items-center my-1 px-2 py-1 rounded ${ + settings.mode === "light" + ? "bg-gray-100" + : "bg-zinc-800" + }`} + > + <i className="fa-solid fa-thumbtack me-2 mt-1 text-slate-500"></i> + <div> + {index.fields.map((f) => ( + <Tag color="blue" key={f} className="me-1"> + {f} + </Tag> + ))} + </div> + </div> + ))} + </div> + )} + </div> + <Button + icon={<IconDeleteStroked />} + type="danger" + block + style={{ marginTop: "8px" }} + onClick={() => deleteTable(tableData.id)} + > + {t("delete")} + </Button> + </div> + } + position="rightTop" + showArrow + trigger="click" + style={{ width: "200px", wordBreak: "break-word" }} + > + <Button + icon={<IconMore />} + type="tertiary" + size="small" + style={{ + backgroundColor: "#808080b3", + color: "white", + }} + /> + </Popover> + </div> + </div> + </div> + ); +} diff --git a/src/components/EditorCanvas/Table/index.jsx b/src/components/EditorCanvas/Table/index.jsx new file mode 100644 index 00000000..69bc5208 --- /dev/null +++ b/src/components/EditorCanvas/Table/index.jsx @@ -0,0 +1,156 @@ +import React, { useState } from "react"; +import { useTranslation } from "react-i18next"; +import { SideSheet } from "@douyinfe/semi-ui"; +import { useLayout, useSettings, useSelect } from "../../../hooks"; + +import { + Tab, + ObjectType, + tableFieldHeight, + tableHeaderHeight, + tableColorStripHeight, +} from "../../../data/constants"; + +import TableFieldPopover from "./components/TableFieldPopover"; +import TableField from "./components/TableField"; +import TableHeader from "./components/TableHeader"; + +import TableInfo from "../../EditorSidePanel/TablesTab/TableInfo"; + +export default function Table(props) { + const [hoveredField, setHoveredField] = useState(-1); + const { + tableData, + onPointerDown, + setHoveredTable, + handleGripField, + setLinkingLine, + } = props; + const { layout } = useLayout(); + const { settings } = useSettings(); + const { t } = useTranslation(); + const { selectedElement, setSelectedElement } = useSelect(); + + const height = + tableData.fields.length * tableFieldHeight + tableHeaderHeight + 7; + + const openEditor = () => { + if (!layout.sidebar) { + setSelectedElement((prev) => ({ + ...prev, + element: ObjectType.TABLE, + id: tableData.id, + open: true, + })); + } else { + setSelectedElement((prev) => ({ + ...prev, + currentTab: Tab.TABLES, + element: ObjectType.TABLE, + id: tableData.id, + open: true, + })); + if (selectedElement.currentTab !== Tab.TABLES) return; + document + .getElementById(`scroll_table_${tableData.id}`) + .scrollIntoView({ behavior: "smooth" }); + } + }; + + const TableHeaderBand = React.memo(({ color }) => { + return ( + <div + className="h-[10px] w-full rounded-t-md" + style={{ backgroundColor: color }} + /> + ); + }); + + TableHeaderBand.displayName = "TableHeaderBand"; + + return ( + <> + <foreignObject + key={tableData.id} + x={tableData.x} + y={tableData.y} + width={settings.tableWidth} + height={height} + className="group drop-shadow-lg rounded-md cursor-move" + onPointerDown={onPointerDown} + > + <div + onDoubleClick={openEditor} + className={`border-2 hover:border-dashed hover:border-blue-500 + select-none rounded-lg w-full ${ + settings.mode === "light" + ? "bg-zinc-100 text-zinc-800" + : "bg-zinc-800 text-zinc-200" + } ${ + selectedElement.id === tableData.id && + selectedElement.element === ObjectType.TABLE + ? "border-solid border-blue-500" + : "border-zinc-500" + }`} + style={{ direction: "ltr" }} + > + <TableHeaderBand color={tableData.color} /> + + <TableHeader + tableData={tableData} + settings={settings} + openEditor={openEditor} + t={t} + /> + + {tableData.fields.map((fieldData, index) => { + return ( + <TableFieldPopover + key={index} + visible={settings.showFieldSummary} + fieldData={fieldData} + > + <TableField + key={index} + tableData={tableData} + fieldData={fieldData} + index={index} + setHoveredTable={setHoveredTable} + handleGripField={handleGripField} + setLinkingLine={setLinkingLine} + setHoveredField={setHoveredField} + hoveredField={hoveredField} + tableFieldHeight={tableFieldHeight} + tableHeaderHeight={tableHeaderHeight} + tableColorStripHeight={tableColorStripHeight} + /> + </TableFieldPopover> + ); + })} + </div> + </foreignObject> + + <SideSheet + title={t("edit")} + size="small" + visible={ + selectedElement.element === ObjectType.TABLE && + selectedElement.id === tableData.id && + selectedElement.open && + !layout.sidebar + } + onCancel={() => + setSelectedElement((prev) => ({ + ...prev, + open: !prev.open, + })) + } + style={{ paddingBottom: "16px" }} + > + <div className="sidesheet-theme"> + <TableInfo data={tableData} /> + </div> + </SideSheet> + </> + ); +}