From b3d4808e4c95a8b3dc25c70e85c7376a2caa3cc2 Mon Sep 17 00:00:00 2001
From: lethalSopaper <2bluisugartechea@gmail.com>
Date: Wed, 8 Oct 2025 08:56:10 -0600
Subject: [PATCH 1/4] feat: split precision fields and move size/precision from
popover to main UI
- Split single precision input into separate total/decimal digit fields for numeric types
- Move precision/size configuration from three-dots menu to directly visible in table fields
---
.../TablesTab/FieldDetails.jsx | 75 +--
.../EditorSidePanel/TablesTab/TableField.jsx | 519 +++++++++++-------
2 files changed, 338 insertions(+), 256 deletions(-)
diff --git a/src/components/EditorSidePanel/TablesTab/FieldDetails.jsx b/src/components/EditorSidePanel/TablesTab/FieldDetails.jsx
index c8e142b1d..f013555d8 100644
--- a/src/components/EditorSidePanel/TablesTab/FieldDetails.jsx
+++ b/src/components/EditorSidePanel/TablesTab/FieldDetails.jsx
@@ -4,7 +4,6 @@ import {
TextArea,
Button,
TagInput,
- InputNumber,
Checkbox,
Toast,
} from "@douyinfe/semi-ui";
@@ -32,7 +31,7 @@ export default function FieldDetails({ data, tid, index }) {
disabled={dbToTypes[database][data.type].noDefault || data.increment}
onChange={(value) => updateField(tid, index, { default: value })}
onFocus={(e) => setEditField({ default: e.target.value })}
- onBlur={(e) => {
+ onBlur={(e) => {
if (e.target.value === editField.default) return;
pushUndo({
action: Action.EDIT,
@@ -87,67 +86,6 @@ export default function FieldDetails({ data, tid, index }) {
/>
>
)}
- {dbToTypes[database][data.type].isSized && (
- <>
-
{t("size")}
- updateField(tid, index, { size: value })}
- onFocus={(e) => setEditField({ size: e.target.value })}
- onBlur={(e) => {
- if (e.target.value === editField.size) return;
- pushUndo({
- action: Action.EDIT,
- element: ObjectType.TABLE,
- component: "field",
- tid: tid,
- fid: index,
- undo: editField,
- redo: { size: e.target.value },
- message: t("edit_table", {
- tableName: tables[tid].name,
- extra: "[field]",
- }),
- });
- }}
- />
- >
- )}
- {dbToTypes[database][data.type].hasPrecision && (
- <>
- {t("precision")}
- updateField(tid, index, { size: value })}
- onFocus={(e) => setEditField({ size: e.target.value })}
- onBlur={(e) => {
- if (e.target.value === editField.size) return;
- pushUndo({
- action: Action.EDIT,
- element: ObjectType.TABLE,
- component: "field",
- tid: tid,
- fid: index,
- undo: editField,
- redo: { size: e.target.value },
- message: t("edit_table", {
- tableName: tables[tid].name,
- extra: "[field]",
- }),
- });
- }}
- />
- >
- )}
{dbToTypes[database][data.type].hasCheck && (
<>
{t("check")}
@@ -183,7 +121,7 @@ export default function FieldDetails({ data, tid, index }) {
{
+ onChange={(checkedValues) => {
// If trying to remove unique from a primary key, show warning and don't change
if (data.primary && data.unique && !checkedValues.target.checked) {
Toast.info(t("pk_has_to_be_unique"));
@@ -216,7 +154,7 @@ export default function FieldDetails({ data, tid, index }) {
disabled={
!dbToTypes[database][data.type].canIncrement || data.isArray
}
- onChange={(checkedValues) => {
+ onChange={(checkedValues) => {
pushUndo({
action: Action.EDIT,
element: ObjectType.TABLE,
@@ -277,7 +215,7 @@ export default function FieldDetails({ data, tid, index }) {
dbToTypes[database][data.type].signed && (
{t("Unsigned")}
-
{
@@ -288,8 +226,7 @@ export default function FieldDetails({ data, tid, index }) {
tid: tid,
fid: index,
undo: {
- [checkedValues.target.value]:
- !checkedValues.target.checked,
+ [checkedValues.target.value]: !checkedValues.target.checked,
},
redo: {
[checkedValues.target.value]: checkedValues.target.checked,
@@ -313,7 +250,7 @@ export default function FieldDetails({ data, tid, index }) {
value={data.comment}
autosize
rows={2}
- onChange={(value) => updateField(tid, index, { comment: value })}
+ onChange={(value) => updateField(tid, index, { comment: value })}
onFocus={(e) => setEditField({ comment: e.target.value })}
onBlur={(e) => {
if (e.target.value === editField.comment) return;
diff --git a/src/components/EditorSidePanel/TablesTab/TableField.jsx b/src/components/EditorSidePanel/TablesTab/TableField.jsx
index a92fd9410..3d9cca9d7 100644
--- a/src/components/EditorSidePanel/TablesTab/TableField.jsx
+++ b/src/components/EditorSidePanel/TablesTab/TableField.jsx
@@ -1,7 +1,21 @@
import { Action, ObjectType } from "../../../data/constants";
-import { Row, Col, Input, Button, Popover, Select } from "@douyinfe/semi-ui";
+import {
+ Row,
+ Col,
+ Input,
+ Button,
+ Popover,
+ Select,
+ InputNumber,
+} from "@douyinfe/semi-ui";
import { IconMore, IconKeyStroked } from "@douyinfe/semi-icons";
-import { useEnums, useDiagram, useTypes, useUndoRedo, useSettings } from "../../../hooks";
+import {
+ useEnums,
+ useDiagram,
+ useTypes,
+ useUndoRedo,
+ useSettings,
+} from "../../../hooks";
import { useState } from "react";
import FieldDetails from "./FieldDetails";
import { useTranslation } from "react-i18next";
@@ -17,17 +31,18 @@ export default function TableField({ data, tid, index }) {
const { t } = useTranslation();
const { pushUndo } = useUndoRedo();
const [editField, setEditField] = useState({});
- const { settings } = useSettings()
+ const { settings } = useSettings();
// Function to check if the FK field belongs to a subtype relationship
const isSubtypeForeignKey = () => {
if (!data.foreignK || !data.foreignKey) return false;
// Search for subtype relationships where this table is a child table
- return relationships.some(rel => {
+ return relationships.some((rel) => {
// Check if it is a subtype relationship
if (!rel.subtype) return false;
// Check if this table is a child table in the subtype relationship
- const isChildTable = rel.endTableId === tid ||
+ const isChildTable =
+ rel.endTableId === tid ||
(rel.endTableIds && rel.endTableIds.includes(tid));
// Check if the FK points to the parent table of the subtype relationship
const pointsToParent = rel.startTableId === data.foreignKey.tableId;
@@ -36,90 +51,100 @@ export default function TableField({ data, tid, index }) {
};
const inconsistencyOfData = () => {
- if(!data.primary) return false;
- return relationships.some(rel => {
+ if (!data.primary) return false;
+ return relationships.some((rel) => {
const parentTable = rel.startTableId === tid;
return parentTable;
});
};
return (
-
-
- updateField(tid, index, {
- name: settings.upperCaseFields ? value.toUpperCase() : value.toLowerCase()
- })}
- onKeyUp={(e) => {
- if (e.key === "Enter") {
+
+ {/* Main field row */}
+
+
+
+ updateField(tid, index, {
+ name: settings.upperCaseFields
+ ? value.toUpperCase()
+ : value.toLowerCase(),
+ })
+ }
+ onKeyUp={(e) => {
+ if (e.key === "Enter") {
//When pressing enter, focus the next input, if there is no next input, create a new field and focus it
- const input = document.getElementById(`scroll_table_${tid}_input_${index+1}`);
+ const input = document.getElementById(
+ `scroll_table_${tid}_input_${index + 1}`,
+ );
if (input) input.focus();
else {
- createNewField({
- data,
- settings,
- database,
- dbToTypes,
- addFieldToTable,
- pushUndo,
- t,
- tid,
- });
- setTimeout(() => {
- const newInput = document.getElementById(`scroll_table_${tid}_input_${index+1}`);
- if (newInput) newInput.focus();
- }, 0);
+ createNewField({
+ data,
+ settings,
+ database,
+ dbToTypes,
+ addFieldToTable,
+ pushUndo,
+ t,
+ tid,
+ });
+ setTimeout(() => {
+ const newInput = document.getElementById(
+ `scroll_table_${tid}_input_${index + 1}`,
+ );
+ if (newInput) newInput.focus();
+ }, 0);
}
- }
- }}
- onFocus={(e) => setEditField({ name: e.target.value })}
- onBlur={(e) => {
- if (e.target.value === editField.name) return;
- const transformedValue = settings.upperCaseFields
- ? e.target.value.toUpperCase()
- : e.target.value.toLowerCase();
- pushUndo({
- action: Action.EDIT,
- element: ObjectType.TABLE,
- component: "field",
- tid: tid,
- fid: index,
- undo: editField,
- redo: { name: transformedValue },
- message: t("edit_table", {
- tableName: tables[tid].name,
- extra: "[field]",
- }),
- });
- }}
- />
-
-
-
}
- pushUndo({
- action: Action.EDIT,
- element: ObjectType.TABLE,
- component: "field",
- tid: tid,
- fid: index,
- message: t("edit_table", {
- tableName: tables[tid].name,
- extra: "[field]",
- }),
- });
- updateField(tid, index, { primary: newStatePK, notNull: stateNull });
- }}
- icon={}
- />
-
-
-
-
-
- }
- trigger="click"
- position="right"
- showArrow
- >
- } />
-
-
-
+ trigger="click"
+ position="right"
+ showArrow
+ >
+ } />
+
+
+
+
+ {/* Precision/Size row - only show if field type supports it */}
+ {(dbToTypes[database][data.type].isSized ||
+ dbToTypes[database][data.type].hasPrecision) && (
+
+ {dbToTypes[database][data.type].hasPrecision ? (
+ <>
+
+ {
+ const scale =
+ data.size && data.size.includes(",")
+ ? data.size.split(",")[1]?.trim() || "0"
+ : "0";
+ const newSize = value ? `${value},${scale}` : "";
+ updateField(tid, index, { size: newSize });
+ }}
+ onFocus={(e) => setEditField({ size: data.size })}
+ onBlur={(e) => {
+ if (data.size === editField.size) return;
+ pushUndo({
+ action: Action.EDIT,
+ element: ObjectType.TABLE,
+ component: "field",
+ tid: tid,
+ fid: index,
+ undo: editField,
+ redo: { size: data.size },
+ message: t("edit_table", {
+ tableName: tables[tid].name,
+ extra: "[field]",
+ }),
+ });
+ }}
+ />
+
+
+ {
+ const precision =
+ data.size && data.size.includes(",")
+ ? data.size.split(",")[0] || "10"
+ : "10";
+ const newSize =
+ typeof value === "number"
+ ? `${precision},${value}`
+ : precision;
+ updateField(tid, index, { size: newSize });
+ }}
+ onFocus={(e) => setEditField({ size: data.size })}
+ onBlur={(e) => {
+ if (data.size === editField.size) return;
+ pushUndo({
+ action: Action.EDIT,
+ element: ObjectType.TABLE,
+ component: "field",
+ tid: tid,
+ fid: index,
+ undo: editField,
+ redo: { size: data.size },
+ message: t("edit_table", {
+ tableName: tables[tid].name,
+ extra: "[field]",
+ }),
+ });
+ }}
+ />
+
+ >
+ ) : (
+
+ updateField(tid, index, { size: value })}
+ onFocus={(e) => setEditField({ size: e.target.value })}
+ onBlur={(e) => {
+ if (e.target.value === editField.size) return;
+ pushUndo({
+ action: Action.EDIT,
+ element: ObjectType.TABLE,
+ component: "field",
+ tid: tid,
+ fid: index,
+ undo: editField,
+ redo: { size: e.target.value },
+ message: t("edit_table", {
+ tableName: tables[tid].name,
+ extra: "[field]",
+ }),
+ });
+ }}
+ />
+
+ )}
+
+ )}
+
);
}
From 4a6dc6d27713eefabf35fb8bf21a2fe38f8c1dc2 Mon Sep 17 00:00:00 2001
From: lethalSopaper <2bluisugartechea@gmail.com>
Date: Wed, 8 Oct 2025 09:04:23 -0600
Subject: [PATCH 2/4] fix: remove unused event parameters in precision input
handlers
---
src/components/EditorSidePanel/TablesTab/TableField.jsx | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/components/EditorSidePanel/TablesTab/TableField.jsx b/src/components/EditorSidePanel/TablesTab/TableField.jsx
index 3d9cca9d7..d1a4d0c3d 100644
--- a/src/components/EditorSidePanel/TablesTab/TableField.jsx
+++ b/src/components/EditorSidePanel/TablesTab/TableField.jsx
@@ -321,8 +321,8 @@ export default function TableField({ data, tid, index }) {
const newSize = value ? `${value},${scale}` : "";
updateField(tid, index, { size: newSize });
}}
- onFocus={(e) => setEditField({ size: data.size })}
- onBlur={(e) => {
+ onFocus={() => setEditField({ size: data.size })}
+ onBlur={() => {
if (data.size === editField.size) return;
pushUndo({
action: Action.EDIT,
@@ -362,8 +362,8 @@ export default function TableField({ data, tid, index }) {
: precision;
updateField(tid, index, { size: newSize });
}}
- onFocus={(e) => setEditField({ size: data.size })}
- onBlur={(e) => {
+ onFocus={() => setEditField({ size: data.size })}
+ onBlur={() => {
if (data.size === editField.size) return;
pushUndo({
action: Action.EDIT,
From 83c724cd9f0a10ffe106b1d1cdcd09a1e0a10615 Mon Sep 17 00:00:00 2001
From: lethalSopaper <2bluisugartechea@gmail.com>
Date: Wed, 8 Oct 2025 09:15:46 -0600
Subject: [PATCH 3/4] feat: enforce accuracy < precision constraint for numeric
field validation
---
.../EditorSidePanel/TablesTab/TableField.jsx | 18 ++++++++++++++----
1 file changed, 14 insertions(+), 4 deletions(-)
diff --git a/src/components/EditorSidePanel/TablesTab/TableField.jsx b/src/components/EditorSidePanel/TablesTab/TableField.jsx
index d1a4d0c3d..5fa7e7dea 100644
--- a/src/components/EditorSidePanel/TablesTab/TableField.jsx
+++ b/src/components/EditorSidePanel/TablesTab/TableField.jsx
@@ -318,7 +318,10 @@ export default function TableField({ data, tid, index }) {
data.size && data.size.includes(",")
? data.size.split(",")[1]?.trim() || "0"
: "0";
- const newSize = value ? `${value},${scale}` : "";
+ const scaleNum = parseInt(scale) || 0;
+ const maxAccuracy = Math.max(0, value - 1);
+ const clampedScale = Math.min(scaleNum, maxAccuracy);
+ const newSize = value ? `${value},${clampedScale}` : "";
updateField(tid, index, { size: newSize });
}}
onFocus={() => setEditField({ size: data.size })}
@@ -350,15 +353,22 @@ export default function TableField({ data, tid, index }) {
}
className="w-full"
min={0}
- max={30}
+ max={
+ data.size && data.size.includes(",")
+ ? Math.max(0, parseInt(data.size.split(",")[0]) - 1)
+ : 9
+ }
onChange={(value) => {
const precision =
data.size && data.size.includes(",")
? data.size.split(",")[0] || "10"
: "10";
+ const precisionNum = parseInt(precision);
+ const maxAccuracy = Math.max(0, precisionNum - 1);
+ const clampedValue = Math.min(value || 0, maxAccuracy);
const newSize =
- typeof value === "number"
- ? `${precision},${value}`
+ typeof clampedValue === "number"
+ ? `${precision},${clampedValue}`
: precision;
updateField(tid, index, { size: newSize });
}}
From ff4e02ebdda35fefa61317442877b5a1f4f0db9a Mon Sep 17 00:00:00 2001
From: lethalSopaper <2bluisugartechea@gmail.com>
Date: Wed, 8 Oct 2025 09:32:22 -0600
Subject: [PATCH 4/4] feat: Field context menu now support field properties.
- Size
- Precision
- Accuracy
---
src/components/EditorCanvas/Canvas.jsx | 249 +++++++++++++++++-
.../EditorCanvas/FieldContextMenu.jsx | 9 +
2 files changed, 257 insertions(+), 1 deletion(-)
diff --git a/src/components/EditorCanvas/Canvas.jsx b/src/components/EditorCanvas/Canvas.jsx
index 7a9c4aaef..621dab42a 100644
--- a/src/components/EditorCanvas/Canvas.jsx
+++ b/src/components/EditorCanvas/Canvas.jsx
@@ -13,7 +13,8 @@ import {
Notation,
Tab,
} from "../../data/constants";
-import { Toast, Modal, Input } from "@douyinfe/semi-ui";
+import { dbToTypes } from "../../data/datatypes";
+import { Toast, Modal, Input, InputNumber } from "@douyinfe/semi-ui";
import Table from "./Table";
import Area from "./Area";
import Relationship from "./Relationship";
@@ -82,7 +83,9 @@ export default function Canvas() {
const {
tables,
+ database,
updateTable,
+ updateField,
relationships,
addRelationship,
addTable,
@@ -252,7 +255,19 @@ export default function Canvas() {
relatedField: null, // { tableId, fieldId, tableName, fieldName }
});
+ const [fieldPropertiesModal, setFieldPropertiesModal] = useState({
+ visible: false,
+ tableId: null,
+ fieldId: null,
+ field: null,
+ });
+
+ // Field properties modal internal state
+ const [fieldPropertiesPrecision, setFieldPropertiesPrecision] = useState("");
+ const [fieldPropertiesAccuracy, setFieldPropertiesAccuracy] = useState("");
+
const fieldRenameInputRef = useRef(null);
+ const fieldPropertiesInputRef = useRef(null);
const tableRenameInputRef = useRef(null);
const relationshipRenameInputRef = useRef(null);
@@ -295,6 +310,34 @@ export default function Canvas() {
}
}, [relationshipRenameModal.visible]);
+ // Initialize field properties modal values when it opens
+ useEffect(() => {
+ if (fieldPropertiesModal.visible && fieldPropertiesModal.field) {
+ const field = fieldPropertiesModal.field;
+ if (field.size && field.size.includes(",")) {
+ const [prec, acc] = field.size.split(",");
+ setFieldPropertiesPrecision(parseInt(prec) || "");
+ setFieldPropertiesAccuracy(parseInt(acc?.trim()) || "");
+ } else {
+ setFieldPropertiesPrecision("");
+ setFieldPropertiesAccuracy("");
+ }
+ }
+ }, [fieldPropertiesModal.visible, fieldPropertiesModal.field]);
+
+ // Auto-focus field properties input when modal opens
+ useEffect(() => {
+ if (fieldPropertiesModal.visible && fieldPropertiesInputRef.current) {
+ const timer = setTimeout(() => {
+ if (fieldPropertiesInputRef.current) {
+ fieldPropertiesInputRef.current.focus();
+ fieldPropertiesInputRef.current.select();
+ }
+ }, 100);
+ return () => clearTimeout(timer);
+ }
+ }, [fieldPropertiesModal.visible]);
+
// Centralized function to close all context menus
const closeAllContextMenus = () => {
setContextMenu({
@@ -1600,6 +1643,79 @@ export default function Canvas() {
});
};
+ const handleFieldEditProperties = () => {
+ if (
+ fieldContextMenu.tableId !== null &&
+ fieldContextMenu.fieldId !== null
+ ) {
+ const table = tables.find((t) => t.id === fieldContextMenu.tableId);
+ const field = table?.fields.find(
+ (f) => f.id === fieldContextMenu.fieldId,
+ );
+
+ setFieldPropertiesModal({
+ visible: true,
+ tableId: fieldContextMenu.tableId,
+ fieldId: fieldContextMenu.fieldId,
+ field: field,
+ });
+ handleFieldContextMenuClose();
+ }
+ };
+
+ const handleFieldPropertiesConfirm = () => {
+ const { tableId, fieldId } = fieldPropertiesModal;
+
+ let newSize = "";
+ if (fieldPropertiesPrecision && fieldPropertiesAccuracy !== "") {
+ newSize = `${fieldPropertiesPrecision},${fieldPropertiesAccuracy}`;
+ } else if (fieldPropertiesPrecision) {
+ newSize = fieldPropertiesPrecision.toString();
+ }
+
+ const updates = { size: newSize };
+
+ pushUndo({
+ action: Action.EDIT,
+ element: ObjectType.TABLE,
+ component: "field",
+ tid: tableId,
+ fid: fieldId,
+ undo: {
+ size:
+ tables
+ .find((t) => t.id === tableId)
+ ?.fields.find((f) => f.id === fieldId)?.size || "",
+ },
+ redo: updates,
+ message: t("edit_table", {
+ tableName: tables.find((t) => t.id === tableId)?.name || "",
+ extra: "[field properties]",
+ }),
+ });
+
+ updateField(tableId, fieldId, updates);
+ setFieldPropertiesModal({
+ visible: false,
+ tableId: null,
+ fieldId: null,
+ field: null,
+ });
+ setFieldPropertiesPrecision("");
+ setFieldPropertiesAccuracy("");
+ };
+
+ const handleFieldPropertiesCancel = () => {
+ setFieldPropertiesModal({
+ visible: false,
+ tableId: null,
+ fieldId: null,
+ field: null,
+ });
+ setFieldPropertiesPrecision("");
+ setFieldPropertiesAccuracy("");
+ };
+
const handleEditNoteContent = () => {
if (noteContextMenu.noteId !== null) {
// Focus on the textarea for the note
@@ -3193,6 +3309,7 @@ export default function Canvas() {
onToggleNotNull={handleToggleFieldNotNull}
onToggleUnique={handleToggleFieldUnique}
onToggleAutoIncrement={handleToggleFieldAutoIncrement}
+ onEditProperties={handleFieldEditProperties}
/>
+
+
+
+ {fieldPropertiesModal.field &&
+ dbToTypes[database] &&
+ dbToTypes[database][fieldPropertiesModal.field.type] &&
+ dbToTypes[database][fieldPropertiesModal.field.type].hasPrecision ? (
+ <>
+
+
+ {
+ setFieldPropertiesPrecision(value);
+ // Auto-adjust accuracy if it would exceed the new limit
+ if (
+ fieldPropertiesAccuracy !== "" &&
+ value &&
+ fieldPropertiesAccuracy >= value
+ ) {
+ setFieldPropertiesAccuracy(Math.max(0, value - 1));
+ }
+ }}
+ placeholder="Enter total digits"
+ min={1}
+ max={65}
+ className="w-full"
+ onKeyPress={(e) => {
+ if (e.key === "Enter") {
+ handleFieldPropertiesConfirm();
+ }
+ }}
+ />
+
+
+
+ {
+ // Ensure accuracy doesn't exceed precision - 1
+ const maxAccuracy = fieldPropertiesPrecision
+ ? Math.max(0, fieldPropertiesPrecision - 1)
+ : 9;
+ const clampedValue = fieldPropertiesPrecision
+ ? Math.min(value || 0, maxAccuracy)
+ : value;
+ setFieldPropertiesAccuracy(clampedValue);
+ }}
+ placeholder="Enter decimal digits"
+ min={0}
+ max={
+ fieldPropertiesPrecision
+ ? Math.max(0, fieldPropertiesPrecision - 1)
+ : 9
+ }
+ className="w-full"
+ onKeyPress={(e) => {
+ if (e.key === "Enter") {
+ handleFieldPropertiesConfirm();
+ }
+ }}
+ />
+
+
+ Example: Precision=10, Accuracy=2 allows values like 12345678.90
+
+ >
+ ) : (
+
+
+ {
+ if (e.key === "Enter") {
+ handleFieldPropertiesConfirm();
+ }
+ }}
+ />
+
+ )}
+
+
);
}
diff --git a/src/components/EditorCanvas/FieldContextMenu.jsx b/src/components/EditorCanvas/FieldContextMenu.jsx
index a33dd33a9..ac179f2f8 100644
--- a/src/components/EditorCanvas/FieldContextMenu.jsx
+++ b/src/components/EditorCanvas/FieldContextMenu.jsx
@@ -24,6 +24,7 @@ export default function FieldContextMenu({
onToggleNotNull,
onToggleUnique,
onToggleAutoIncrement,
+ onEditProperties,
}) {
const { t } = useTranslation();
const { settings } = useSettings();
@@ -78,6 +79,14 @@ export default function FieldContextMenu({
onClose();
},
},
+ {
+ label: "Edit Properties",
+ icon: ,
+ onClick: () => {
+ onEditProperties();
+ onClose();
+ },
+ },
{
label: field?.primary ? "Remove Primary Key" : "Set as Primary Key",
icon: ,