refactor: modularize and organize table component

This commit is contained in:
ademarsj 2024-11-14 22:19:58 -03:00
parent ce85bb6680
commit 44b3d3429c
5 changed files with 477 additions and 376 deletions

View File

@ -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>
);
}
}

View File

@ -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;

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
</>
);
}