diff --git a/src/components/EditorCanvas/Canvas.jsx b/src/components/EditorCanvas/Canvas.jsx index ac66fc56c..e0c1695e5 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, @@ -259,7 +262,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); const changeCardinalityInputRef = useRef(null); @@ -303,6 +318,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]); + // Auto select when the change cardinality modal opens useEffect(() => { if (changeCardinalityModal.visible && changeCardinalityInputRef.current) { @@ -1701,6 +1744,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 @@ -3294,6 +3410,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: , 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..5fa7e7dea 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]", - }), - }); - }} - /> - - - ({ + label: value, + value: value, + })), + ...types.map((type) => ({ + label: type.name.toUpperCase(), + value: type.name.toUpperCase(), + })), + ...enums.map((type) => ({ + label: type.name.toUpperCase(), + value: type.name.toUpperCase(), + })), + ]} + filter + value={data.type} + validateStatus={data.type === "" ? "error" : "default"} + placeholder="Type" onChange={(value) => { if (value === data.type) return; pushUndo({ @@ -135,55 +160,55 @@ export default function TableField({ data, tid, index }) { extra: "[field]", }), }); - const incr = - data.increment && !!dbToTypes[database][value].canIncrement; + const incr = + data.increment && !!dbToTypes[database][value].canIncrement; - if (value === "ENUM" || value === "SET") { - updateField(tid, index, { - type: value, - default: "", - values: data.values ? [...data.values] : [], - increment: incr, - }); - } else if ( - dbToTypes[database][value].isSized || - dbToTypes[database][value].hasPrecision - ) { - updateField(tid, index, { - type: value, - size: dbToTypes[database][value].defaultSize, - increment: incr, - }); - } else if (!dbToTypes[database][value].hasDefault || incr) { - updateField(tid, index, { - type: value, - increment: incr, - default: "", - size: "", - values: [], - }); - } else if (dbToTypes[database][value].hasCheck) { - updateField(tid, index, { - type: value, - check: "", - increment: incr, - }); - } else { - updateField(tid, index, { - type: value, - increment: incr, - size: "", - values: [], - }); - } - }} - /> - - - - - - + + +
} - 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 - > -