From 455d55a43e5765abfec93d90b070d8e8a7862e4c Mon Sep 17 00:00:00 2001 From: Devika Date: Wed, 18 Jun 2025 13:07:23 +0530 Subject: [PATCH] Refatcored the Add New Table Modal for ER Diagram Screen --- src/components/AddTableModal.jsx | 492 ++++++++++++++++++----- src/components/ERDiagramCanvas.jsx | 7 +- src/components/SchemaCreationExample.jsx | 143 +++++++ src/examples/createSchemaExample.js | 45 +++ 4 files changed, 575 insertions(+), 112 deletions(-) create mode 100644 src/components/SchemaCreationExample.jsx create mode 100644 src/examples/createSchemaExample.js diff --git a/src/components/AddTableModal.jsx b/src/components/AddTableModal.jsx index cc86bdf..095f747 100644 --- a/src/components/AddTableModal.jsx +++ b/src/components/AddTableModal.jsx @@ -17,7 +17,15 @@ import { Grid, Paper, Chip, - Alert + Alert, + Autocomplete, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Checkbox } from '@mui/material'; import { FaPlus as AddIcon, @@ -25,7 +33,8 @@ import { FaTimes as CloseIcon, FaTable as TableIcon, FaKey as KeyIcon, - FaLink as LinkIcon + FaLink as LinkIcon, + FaArrowLeft as BackIcon } from 'react-icons/fa'; const AddTableModal = ({ @@ -72,6 +81,10 @@ const AddTableModal = ({ // Validation and UI state const [errors, setErrors] = useState({}); const [isSubmitting, setIsSubmitting] = useState(false); + + // Keys section navigation state + const [keysViewMode, setKeysViewMode] = useState('keys'); // 'keys' or 'columns' + const [selectedKeyForColumns, setSelectedKeyForColumns] = useState(null); // Reset form when modal opens/closes useEffect(() => { @@ -92,6 +105,8 @@ const AddTableModal = ({ setRelations([]); setErrors({}); setIsSubmitting(false); + setKeysViewMode('keys'); + setSelectedKeyForColumns(null); }; // Form field handlers @@ -126,12 +141,38 @@ const AddTableModal = ({ setColumns(prev => prev.map(col => col.id === id ? { ...col, [field]: value } : col )); + + // Clear column errors when user starts typing + if (field === 'name' && errors.columns?.[id]) { + setErrors(prev => ({ + ...prev, + columns: { + ...prev.columns, + [id]: null + } + })); + } + }; + + // Helper function to check if column name is duplicate + const isColumnNameDuplicate = (columnId, columnName) => { + if (!columnName.trim()) return false; + + const trimmedName = columnName.trim().toLowerCase(); + return columns.some(col => + col.id !== columnId && + col.name.trim().toLowerCase() === trimmedName + ); }; const removeColumn = (id) => { setColumns(prev => prev.filter(col => col.id !== id)); - // Remove any keys that reference this column - setKeys(prev => prev.filter(key => key.columnId !== id)); + // Remove any keys that reference this column or update key columns + setKeys(prev => prev.map(key => ({ + ...key, + columnIds: key.columnIds?.filter(colId => colId !== id) || [], + keyColumns: key.keyColumns?.filter(keyCol => keyCol.columnId !== id) || [] + })).filter(key => key.keyColumns?.length > 0 || key.columnIds?.length > 0)); }; // Key management @@ -139,9 +180,9 @@ const AddTableModal = ({ const newKey = { id: Date.now(), name: '', - columnId: '', - sequence: 1, - keyType: 'PRIMARY' // PRIMARY, FOREIGN, UNIQUE + columnIds: [], // Changed to array for multi-select + keyType: 'PRIMARY', // PRIMARY, FOREIGN, UNIQUE + keyColumns: [] // Array of {columnId, sequence} objects }; setKeys(prev => [...prev, newKey]); }; @@ -156,6 +197,70 @@ const AddTableModal = ({ setKeys(prev => prev.filter(key => key.id !== id)); }; + // Key column management + const addColumnToKey = (keyId, columnId) => { + setKeys(prev => prev.map(key => { + if (key.id === keyId) { + const existingSequences = key.keyColumns?.map(kc => kc.sequence) || []; + const nextSequence = existingSequences.length > 0 ? Math.max(...existingSequences) + 1 : 1; + + return { + ...key, + columnIds: [...(key.columnIds || []), columnId], + keyColumns: [...(key.keyColumns || []), { columnId, sequence: nextSequence }] + }; + } + return key; + })); + }; + + const removeColumnFromKey = (keyId, columnId) => { + setKeys(prev => prev.map(key => { + if (key.id === keyId) { + return { + ...key, + columnIds: (key.columnIds || []).filter(id => id !== columnId), + keyColumns: (key.keyColumns || []).filter(kc => kc.columnId !== columnId) + }; + } + return key; + })); + }; + + const updateKeyColumnSequence = (keyId, columnId, sequence) => { + setKeys(prev => prev.map(key => { + if (key.id === keyId) { + return { + ...key, + keyColumns: (key.keyColumns || []).map(kc => + kc.columnId === columnId ? { ...kc, sequence: parseInt(sequence) || 1 } : kc + ) + }; + } + return key; + })); + }; + + // Helper function to check for duplicate sequences within a key + const hasSequenceDuplicates = (keyId) => { + const key = keys.find(k => k.id === keyId); + if (!key || !key.keyColumns) return false; + + const sequences = key.keyColumns.map(kc => kc.sequence); + return sequences.length !== new Set(sequences).size; + }; + + // Keys section navigation functions + const handleKeyRowClick = (keyId) => { + setSelectedKeyForColumns(keyId); + setKeysViewMode('columns'); + }; + + const handleBackToKeys = () => { + setKeysViewMode('keys'); + setSelectedKeyForColumns(null); + }; + // Relation management const addRelation = () => { const newRelation = { @@ -181,11 +286,18 @@ const AddTableModal = ({ // Get available keys for relations const getAvailableKeys = () => { return keys.map(key => { - const column = columns.find(col => col.id === key.columnId); + const keyColumnNames = (key.keyColumns || []) + .sort((a, b) => a.sequence - b.sequence) + .map(kc => { + const column = columns.find(col => col.id === kc.columnId); + return column?.name || 'Unknown Column'; + }) + .join(', '); + return { id: key.id, name: key.name, - columnName: column?.name || 'Unknown Column' + columnName: keyColumnNames || 'No columns selected' }; }); }; @@ -222,9 +334,27 @@ const AddTableModal = ({ // Validate columns const columnErrors = {}; + const columnNames = new Set(); + const duplicateNames = new Set(); + + // First pass: identify duplicate names columns.forEach(col => { - if (!col.name.trim()) { + const trimmedName = col.name.trim().toLowerCase(); + if (trimmedName && columnNames.has(trimmedName)) { + duplicateNames.add(trimmedName); + } + if (trimmedName) { + columnNames.add(trimmedName); + } + }); + + // Second pass: validate each column + columns.forEach(col => { + const trimmedName = col.name.trim(); + if (!trimmedName) { columnErrors[col.id] = 'Column name is required'; + } else if (duplicateNames.has(trimmedName.toLowerCase())) { + columnErrors[col.id] = 'Column name must be unique within the table'; } }); @@ -238,8 +368,15 @@ const AddTableModal = ({ if (!key.name.trim()) { keyErrors[key.id] = 'Key name is required'; } - if (!key.columnId) { - keyErrors[key.id] = 'Column selection is required'; + if (!key.keyColumns || key.keyColumns.length === 0) { + keyErrors[key.id] = 'At least one column must be selected'; + } else { + // Check for duplicate sequences within the key + const sequences = key.keyColumns.map(kc => kc.sequence); + const duplicateSequences = sequences.filter((seq, index) => sequences.indexOf(seq) !== index); + if (duplicateSequences.length > 0) { + keyErrors[key.id] = `Duplicate sequence numbers found: ${[...new Set(duplicateSequences)].join(', ')}`; + } } }); @@ -269,16 +406,22 @@ const AddTableModal = ({ columns: columns.map(col => ({ name: col.name.trim(), data_type: col.type, - is_primary_key: keys.some(key => key.columnId === col.id && key.keyType === 'PRIMARY'), - is_foreign_key: keys.some(key => key.columnId === col.id && key.keyType === 'FOREIGN'), + is_primary_key: keys.some(key => + key.keyColumns?.some(kc => kc.columnId === col.id) && key.keyType === 'PRIMARY' + ), + is_foreign_key: keys.some(key => + key.keyColumns?.some(kc => kc.columnId === col.id) && key.keyType === 'FOREIGN' + ), is_nullable: col.isNullable })), - keys: keys.map(key => ({ - name: key.name.trim(), - column_name: columns.find(col => col.id === key.columnId)?.name || '', - key_type: key.keyType, - sequence: key.sequence - })), + keys: keys.flatMap(key => + (key.keyColumns || []).map(kc => ({ + name: key.name.trim(), + column_name: columns.find(col => col.id === kc.columnId)?.name || '', + key_type: key.keyType, + sequence: kc.sequence + })) + ), relations: relations.map(rel => ({ target_table: rel.targetTable, source_key: rel.sourceKey, @@ -429,8 +572,11 @@ const AddTableModal = ({ label="Column Name" value={column.name} onChange={(e) => updateColumn(column.id, 'name', e.target.value)} - error={!!errors.columns?.[column.id]} - helperText={errors.columns?.[column.id]} + error={!!errors.columns?.[column.id] || isColumnNameDuplicate(column.id, column.name)} + helperText={ + errors.columns?.[column.id] || + (isColumnNameDuplicate(column.id, column.name) ? 'Column name must be unique within the table' : '') + } size="small" /> @@ -483,95 +629,227 @@ const AddTableModal = ({ Keys + {keysViewMode === 'columns' && selectedKeyForColumns && ( + + - {keys.find(k => k.id === selectedKeyForColumns)?.name || 'Unnamed Key'} + + )} - + + {keysViewMode === 'keys' ? ( + + ) : ( + + )} - {keys.length === 0 ? ( - - {columns.length === 0 - ? "Add columns first before defining keys." - : "No keys defined yet. Click \"Add Key\" to create primary or foreign keys." - } - + {keysViewMode === 'keys' ? ( + // Keys Table View + + + + + + + + Key Name + Key Type + Actions + + + + {keys.map((key) => ( + handleKeyRowClick(key.id)} + sx={{ cursor: 'pointer' }} + > + + e.stopPropagation()} + /> + + + { + e.stopPropagation(); + updateKey(key.id, 'name', e.target.value); + }} + onClick={(e) => e.stopPropagation()} + size="small" + variant="standard" + error={!!errors.keys?.[key.id]} + helperText={errors.keys?.[key.id]} + placeholder="Enter key name" + fullWidth + /> + + + + + + + + { + e.stopPropagation(); + removeKey(key.id); + }} + color="error" + size="small" + > + + + + + ))} + {/* Empty row for adding new key */} + + + + + + + Click to add new key... + + + + + - + + + + + + + +
+
) : ( - - {keys.map((key) => ( - - - - updateKey(key.id, 'name', e.target.value)} - error={!!errors.keys?.[key.id]} - helperText={errors.keys?.[key.id]} - size="small" - /> - - - - Column - - - - - - Key Type - - - - - updateKey(key.id, 'sequence', parseInt(e.target.value) || 1)} - size="small" - inputProps={{ min: 1 }} - /> - - - removeKey(key.id)} - color="error" - size="small" - > - - - - - - ))} - + // Columns Table View for Selected Key + selectedKeyForColumns && ( + + + + + + + + Column Name + Sequence + Actions + + + + {columns.map((column) => { + const selectedKey = keys.find(k => k.id === selectedKeyForColumns); + const keyColumn = selectedKey?.keyColumns?.find(kc => kc.columnId === column.id); + const isSelected = !!keyColumn; + const hasSequenceError = hasSequenceDuplicates(selectedKeyForColumns); + + return ( + + + { + if (e.target.checked) { + addColumnToKey(selectedKeyForColumns, column.id); + } else { + removeColumnFromKey(selectedKeyForColumns, column.id); + } + }} + /> + + {column.name || 'Unnamed Column'} + + {isSelected ? ( + updateKeyColumnSequence(selectedKeyForColumns, column.id, e.target.value)} + size="small" + inputProps={{ min: 1, style: { width: '80px' } }} + error={hasSequenceError} + helperText={hasSequenceError ? 'Duplicate' : ''} + /> + ) : ( + - + )} + + + {isSelected && ( + removeColumnFromKey(selectedKeyForColumns, column.id)} + > + + + )} + + + ); + })} + {/* Empty row for adding new column */} + + + + + + + Select columns above to add to this key... + + + + + - + + + + + + + +
+
+ ) + )} + + {/* Show message when no columns exist */} + {columns.length === 0 && ( + + Add columns first before defining keys. + )} diff --git a/src/components/ERDiagramCanvas.jsx b/src/components/ERDiagramCanvas.jsx index 87b6eac..4f6c09f 100644 --- a/src/components/ERDiagramCanvas.jsx +++ b/src/components/ERDiagramCanvas.jsx @@ -1229,11 +1229,8 @@ const ERDiagramCanvasContent = () => { const rows = Math.ceil(tableCount / tablesPerRow); // Calculate schema dimensions with proper padding for tables - const tableWidth = 260; // Width of each table node - const tableHeight = 280; // Height of each table node (including spacing) const tableSpacingCalc = 330; // Horizontal spacing between tables const tableRowSpacingCalc = 320; // Vertical spacing between table rows - const schemaPadding = 200; // Padding around the schema content // Calculate required width and height based on table layout const tableStartX = 90; // Starting X position within schema @@ -1241,8 +1238,8 @@ const ERDiagramCanvasContent = () => { const requiredWidth = tableStartX + (tablesPerRow * tableSpacingCalc) + 100; // Extra margin const requiredHeight = tableStartY + (rows * tableRowSpacingCalc) + 100; // Extra margin - const schemaWidth = Math.max(900, requiredWidth); - const schemaHeight = Math.max(650, requiredHeight); + const schemaWidth = Math.max(1000, requiredWidth); + const schemaHeight = Math.max(850, requiredHeight); console.log(`Schema "${schema.name || schema.sch}" layout: ${tableCount} tables, ${tablesPerRow} per row, ${rows} rows`); console.log(`Required dimensions: ${requiredWidth}x${requiredHeight}, Final: ${schemaWidth}x${schemaHeight}`); diff --git a/src/components/SchemaCreationExample.jsx b/src/components/SchemaCreationExample.jsx new file mode 100644 index 0000000..6443686 --- /dev/null +++ b/src/components/SchemaCreationExample.jsx @@ -0,0 +1,143 @@ +import React, { useState } from 'react'; +import { createSchema } from './mockData'; + +const SchemaCreationExample = () => { + // State for form inputs + const [schemaName, setSchemaName] = useState(''); + const [schemaDescription, setSchemaDescription] = useState(''); + const [dbSlug, setDbSlug] = useState('my_dwh'); // Default to my_dwh + + // State for API response + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [success, setSuccess] = useState(false); + const [createdSchema, setCreatedSchema] = useState(null); + + // Handle form submission + const handleSubmit = async (e) => { + e.preventDefault(); + + // Reset states + setLoading(true); + setError(null); + setSuccess(false); + setCreatedSchema(null); + + try { + // Call the createSchema function + const newSchema = await createSchema(dbSlug, schemaName, schemaDescription); + + // Handle success + setSuccess(true); + setCreatedSchema(newSchema); + + // Clear form + setSchemaName(''); + setSchemaDescription(''); + } catch (err) { + // Handle error + setError(err.message || 'An error occurred while creating the schema'); + } finally { + setLoading(false); + } + }; + + return ( +
+

Create New Schema

+ + {/* Form for schema creation */} +
+
+ + +
+ +
+ + setSchemaName(e.target.value)} + required + placeholder="Enter schema name" + /> +
+ +
+ +