diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260225082054_Initial.Designer.cs b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260301173343_Initial.Designer.cs similarity index 99% rename from api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260225082054_Initial.Designer.cs rename to api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260301173343_Initial.Designer.cs index 0e7522b..1b02102 100644 --- a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260225082054_Initial.Designer.cs +++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260301173343_Initial.Designer.cs @@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore; namespace Sozsoft.Platform.Migrations { [DbContext(typeof(PlatformDbContext))] - [Migration("20260225082054_Initial")] + [Migration("20260301173343_Initial")] partial class Initial { /// diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260225082054_Initial.cs b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260301173343_Initial.cs similarity index 100% rename from api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260225082054_Initial.cs rename to api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260301173343_Initial.cs diff --git a/ui/src/views/sqlQueryManager/SqlQueryManager.tsx b/ui/src/views/sqlQueryManager/SqlQueryManager.tsx index 2496a1c..97456f4 100644 --- a/ui/src/views/sqlQueryManager/SqlQueryManager.tsx +++ b/ui/src/views/sqlQueryManager/SqlQueryManager.tsx @@ -13,7 +13,7 @@ import type { } from '@/proxy/sql-query-manager/models' import { SqlObjectType } from '@/proxy/sql-query-manager/models' import { sqlObjectManagerService } from '@/services/sql-query-manager.service' -import { FaDatabase, FaPlay, FaSave, FaSyncAlt, FaCloudUploadAlt, FaCode } from 'react-icons/fa' +import { FaDatabase, FaPlay, FaSave, FaCloudUploadAlt, FaCode, FaTable } from 'react-icons/fa' import { FaCheckCircle } from 'react-icons/fa' import { useLocalization } from '@/utils/hooks/useLocalization' import SqlObjectExplorer from './components/SqlObjectExplorer' @@ -21,6 +21,7 @@ import SqlEditor, { SqlEditorRef } from './components/SqlEditor' import SqlResultsGrid from './components/SqlResultsGrid' import SqlObjectProperties from './components/SqlObjectProperties' import TemplateDialog from './components/TemplateDialog' +import TableDesignerDialog from './components/TableDesignerDialog' import { Splitter } from '@/components/codeLayout/Splitter' import { Helmet } from 'react-helmet' import { useStoreState } from '@/store/store' @@ -72,11 +73,18 @@ const SqlQueryManager = () => { isExistingObject: false, }) const [showTemplateDialog, setShowTemplateDialog] = useState(false) + const [showTableDesignerDialog, setShowTableDesignerDialog] = useState(false) + const [designTableData, setDesignTableData] = useState<{ schemaName: string; tableName: string } | null>(null) const [showTemplateConfirmDialog, setShowTemplateConfirmDialog] = useState(false) const [pendingTemplate, setPendingTemplate] = useState<{ content: string; type: string } | null>( null, ) + const handleDesignTable = (schemaName: string, tableName: string) => { + setDesignTableData({ schemaName, tableName }) + setShowTableDesignerDialog(true) + } + useEffect(() => { loadDataSources() }, []) @@ -802,16 +810,6 @@ GO`,
-
@@ -972,6 +975,20 @@ GO`, + {/* Table Designer Dialog */} + { + setShowTableDesignerDialog(false) + setDesignTableData(null) + }} + dataSource={state.selectedDataSource} + initialTableData={designTableData} + onDeployed={() => { + setState((prev) => ({ ...prev, refreshTrigger: prev.refreshTrigger + 1 })) + }} + /> + {/* Template Dialog */} void onShowTableColumns?: (schemaName: string, tableName: string) => void + onDesignTable?: (schemaName: string, tableName: string) => void + onNewTable?: () => void refreshTrigger?: number } @@ -51,6 +54,8 @@ const SqlObjectExplorer = ({ selectedObject, onTemplateSelect, onShowTableColumns, + onDesignTable, + onNewTable, refreshTrigger, }: SqlObjectExplorerProps) => { const { translate } = useLocalization() @@ -506,6 +511,22 @@ const SqlObjectExplorer = ({ className="fixed z-50 bg-white dark:bg-gray-800 shadow-lg rounded border border-gray-200 dark:border-gray-700 py-1" style={{ top: contextMenu.y, left: contextMenu.x }} > + {contextMenu.node?.id?.startsWith('table-') && ( + + )} + {contextMenu.node?.type === 'object' && contextMenu.node?.objectType && contextMenu.node?.data?.isCustom && ( <> + <> + {/* Tables folder */} + {contextMenu.node.id === 'tables' && ( + + )} + + {/* Stored Procedures folder */} + {contextMenu.node.id === 'procedures' && ( + + )} + + {/* Views folder */} + {contextMenu.node.id === 'views' && ( + + )} + + {/* Functions folder */} + {contextMenu.node.id === 'functions' && ( + + )} + + {/* Queries folder */} + {contextMenu.node.id === 'queries' && ( + + )} + + {/* Separator */} + {contextMenu.node.id !== 'root' && ( +
+ )} + + {/* Refresh — all folders */} + + )}
diff --git a/ui/src/views/sqlQueryManager/components/TableDesignerDialog.tsx b/ui/src/views/sqlQueryManager/components/TableDesignerDialog.tsx new file mode 100644 index 0000000..50c7f28 --- /dev/null +++ b/ui/src/views/sqlQueryManager/components/TableDesignerDialog.tsx @@ -0,0 +1,1703 @@ +import { useState, useCallback, useMemo, useEffect } from 'react' +import { Button, Dialog, Notification, toast, Checkbox } from '@/components/ui' +import { + FaPlus, + FaTrash, + FaArrowUp, + FaArrowDown, + FaTable, + FaCloudUploadAlt, + FaCheck, + FaChevronRight, + FaCog, + FaDatabase, + FaLink, + FaEdit, + FaTimes, + FaArrowRight, + FaInfoCircle, + FaExclamationTriangle, +} from 'react-icons/fa' +import { sqlObjectManagerService } from '@/services/sql-query-manager.service' +import { MenuService } from '@/services/menu.service' +import { developerKitService } from '@/services/developerKit.service' +import type { CreateUpdateCustomEntityDto, CreateUpdateCustomEntityFieldDto } from '@/proxy/developerKit/models' +import { useLocalization } from '@/utils/hooks/useLocalization' + +// ─── Types ──────────────────────────────────────────────────────────────────── + +type SqlDataType = + | 'nvarchar' + | 'nvarchar(MAX)' + | 'int' + | 'bigint' + | 'decimal' + | 'float' + | 'bit' + | 'datetime2' + | 'date' + | 'uniqueidentifier' + | 'money' + +interface ColumnDefinition { + id: string + columnName: string + dataType: SqlDataType + maxLength: string + isNullable: boolean + defaultValue: string + description: string +} + +interface MenuOption { + value: string + label: string +} + +interface TableSettings { + menuValue: string + menuPrefix: string + entityName: string + tableName: string + displayName: string + description: string + isActive: boolean + isFullAudited: boolean + isMultiTenant: boolean +} + +interface TableDesignerDialogProps { + isOpen: boolean + onClose: () => void + dataSource: string | null + onDeployed?: () => void + initialTableData?: { schemaName: string; tableName: string } | null +} + +type RelationshipType = 'OneToOne' | 'OneToMany' +type CascadeBehavior = 'NoAction' | 'Cascade' | 'SetNull' | 'Restrict' + +interface FkRelationshipDefinition { + id: string + /** Existing DB constraint name — set when loaded from database (edit mode) */ + constraintName?: string + relationshipType: RelationshipType + fkColumnName: string + referencedTable: string + referencedColumn: string + cascadeDelete: CascadeBehavior + cascadeUpdate: CascadeBehavior + isRequired: boolean + description: string +} + +// ─── Constants ──────────────────────────────────────────────────────────────── + +const DATA_TYPES: { value: SqlDataType; label: string }[] = [ + { value: 'nvarchar', label: 'String (nvarchar)' }, + { value: 'nvarchar(MAX)', label: 'Text (nvarchar MAX)' }, + { value: 'int', label: 'Int (int)' }, + { value: 'bigint', label: 'Long (bigint)' }, + { value: 'decimal', label: 'Decimal (decimal 18,4)' }, + { value: 'float', label: 'Float (float)' }, + { value: 'bit', label: 'Bool (bit)' }, + { value: 'datetime2', label: 'DateTime (datetime2)' }, + { value: 'date', label: 'Date (date)' }, + { value: 'uniqueidentifier', label: 'Guid (uniqueidentifier)' }, + { value: 'money', label: 'Money (money)' }, +] + +const FULL_AUDIT_COLUMNS: ColumnDefinition[] = [ + { + id: '__CreationTime', + columnName: 'CreationTime', + dataType: 'datetime2', + maxLength: '', + isNullable: false, + defaultValue: 'GETUTCDATE()', + description: 'Record creation time', + }, + { + id: '__CreatorId', + columnName: 'CreatorId', + dataType: 'uniqueidentifier', + maxLength: '', + isNullable: true, + defaultValue: '', + description: 'Creator user ID', + }, + { + id: '__LastModificationTime', + columnName: 'LastModificationTime', + dataType: 'datetime2', + maxLength: '', + isNullable: true, + defaultValue: '', + description: 'Last modification time', + }, + { + id: '__LastModifierId', + columnName: 'LastModifierId', + dataType: 'uniqueidentifier', + maxLength: '', + isNullable: true, + defaultValue: '', + description: 'Last modifier user ID', + }, + { + id: '__IsDeleted', + columnName: 'IsDeleted', + dataType: 'bit', + maxLength: '', + isNullable: false, + defaultValue: '0', + description: 'Soft delete flag', + }, + { + id: '__DeletionTime', + columnName: 'DeletionTime', + dataType: 'datetime2', + maxLength: '', + isNullable: true, + defaultValue: '', + description: 'Deletion time', + }, + { + id: '__DeleterId', + columnName: 'DeleterId', + dataType: 'uniqueidentifier', + maxLength: '', + isNullable: true, + defaultValue: '', + description: 'Deleter user ID', + }, +] + +const REL_TYPES: { value: RelationshipType; label: string; desc: string }[] = [ + { value: 'OneToMany', label: '1 → N', desc: 'Bire-çok' }, + { value: 'OneToOne', label: '1 → 1', desc: 'Bire-bir' }, +] + +const CASCADE_OPTIONS: { value: CascadeBehavior; label: string }[] = [ + { value: 'NoAction', label: 'No Action' }, + { value: 'Cascade', label: 'Cascade' }, + { value: 'SetNull', label: 'Set Null' }, + { value: 'Restrict', label: 'Restrict' }, +] + +const EMPTY_FK: Omit = { + relationshipType: 'OneToMany', + fkColumnName: '', + referencedTable: '', + referencedColumn: 'Id', + cascadeDelete: 'NoAction', + cascadeUpdate: 'NoAction', + isRequired: false, + description: '', +} + +const TENANT_COLUMN: ColumnDefinition = { + id: '__TenantId', + columnName: 'TenantId', + dataType: 'uniqueidentifier', + maxLength: '', + isNullable: true, + defaultValue: '', + description: 'Tenant ID for multi-tenancy', +} + +// ─── T-SQL Generator ────────────────────────────────────────────────────────── + +function colToSqlLine(col: ColumnDefinition, addComma = true): string { + let typeSql: string + switch (col.dataType) { + case 'nvarchar': + typeSql = `nvarchar(${col.maxLength || '100'})` + break + case 'nvarchar(MAX)': + typeSql = 'nvarchar(MAX)' + break + case 'decimal': + typeSql = 'decimal(18, 4)' + break + default: + typeSql = col.dataType + } + const nullPart = col.isNullable ? 'NULL' : 'NOT NULL' + const defaultPart = col.defaultValue ? ` DEFAULT ${col.defaultValue}` : '' + return ` [${col.columnName}] ${typeSql} ${nullPart}${defaultPart}${addComma ? ',' : ''}` +} + +function generateCreateTableSql( + columns: ColumnDefinition[], + settings: TableSettings, + relationships: FkRelationshipDefinition[], +): string { + const tableName = settings.tableName || 'NewTable' + const fullTableName = `[dbo].[${tableName}]` + + const userCols = columns.filter((c) => c.columnName.trim()) + const auditCols = settings.isFullAudited ? FULL_AUDIT_COLUMNS : [] + const tenantCols = settings.isMultiTenant ? [TENANT_COLUMN] : [] + const allBodyCols = [...userCols, ...auditCols, ...tenantCols] + + // When Full Audited: auto-add Id (PK) + audit cols + // When not Full Audited: user is responsible for their own Id column + const bodyLines = settings.isFullAudited + ? [ + ` [Id] uniqueidentifier NOT NULL DEFAULT NEWID(),`, + ...allBodyCols.map((c) => colToSqlLine(c, true)), + ` CONSTRAINT [PK_${tableName}] PRIMARY KEY NONCLUSTERED ([Id])`, + ] + : allBodyCols.map((c, i) => colToSqlLine(c, i < allBodyCols.length - 1)) + + // FK constraint lines + const fkLines: string[] = [] + for (const rel of relationships) { + if (!rel.fkColumnName.trim() || !rel.referencedTable.trim()) continue + const constraintName = `FK_${tableName}_${rel.fkColumnName}` + const cascadeDelete = rel.cascadeDelete === 'NoAction' ? 'NO ACTION' : rel.cascadeDelete.replace(/([A-Z])/g, ' $1').trim().toUpperCase() + const cascadeUpdate = rel.cascadeUpdate === 'NoAction' ? 'NO ACTION' : rel.cascadeUpdate.replace(/([A-Z])/g, ' $1').trim().toUpperCase() + fkLines.push('') + fkLines.push(`ALTER TABLE ${fullTableName}`) + fkLines.push(` ADD CONSTRAINT [${constraintName}]`) + fkLines.push(` FOREIGN KEY ([${rel.fkColumnName}])`) + fkLines.push(` REFERENCES [dbo].[${rel.referencedTable}] ([${rel.referencedColumn || 'Id'}])`) + fkLines.push(` ON DELETE ${cascadeDelete}`) + fkLines.push(` ON UPDATE ${cascadeUpdate};`) + } + + const lines: string[] = [ + `/* ── Table: ${fullTableName} ── */`, + ...(settings.entityName ? [`/* Entity Name: ${settings.entityName} */`] : []), + ...(settings.isFullAudited ? ['/* Full Audited Entity (Id + audit columns auto-added) */'] : []), + ...(settings.isMultiTenant ? ['/* Multi-Tenant */'] : []), + ...(fkLines.length > 0 ? ['/* Foreign Key Constraints */'] : []), + '', + `CREATE TABLE ${fullTableName}`, + `(`, + ...bodyLines, + `);`, + ...fkLines, + '', + `/* Verify: SELECT TOP 10 * FROM ${fullTableName}; */`, + ] + + return lines.join('\n') +} + +// ─── Helpers ────────────────────────────────────────────────────────────────── + +/** Convert a DatabaseColumnDto (from API) to a ColumnDefinition for the grid */ +function dbColToColumnDef(col: { + columnName: string + dataType: string + isNullable: boolean + maxLength?: number +}): ColumnDefinition { + const dt = col.dataType.toLowerCase().trim() + let dataType: SqlDataType = 'nvarchar' + let maxLength = '' + + if (dt === 'nvarchar' || dt === 'varchar' || dt === 'char' || dt === 'nchar') { + dataType = col.maxLength === -1 ? 'nvarchar(MAX)' : 'nvarchar' + maxLength = col.maxLength && col.maxLength > 0 ? String(col.maxLength) : '' + } else if (dt === 'int') { + dataType = 'int' + } else if (dt === 'bigint') { + dataType = 'bigint' + } else if (dt === 'decimal' || dt === 'numeric') { + dataType = 'decimal' + } else if (dt === 'float' || dt === 'real') { + dataType = 'float' + } else if (dt === 'bit') { + dataType = 'bit' + } else if (dt.startsWith('datetime') || dt === 'smalldatetime') { + dataType = 'datetime2' + } else if (dt === 'date') { + dataType = 'date' + } else if (dt === 'uniqueidentifier') { + dataType = 'uniqueidentifier' + } else if (dt === 'money' || dt === 'smallmoney') { + dataType = 'money' + } + + return { + id: crypto.randomUUID(), + columnName: col.columnName, + dataType, + maxLength, + isNullable: col.isNullable, + defaultValue: '', + description: '', + } +} + +/** Generate ALTER TABLE diff SQL (edit mode) */ +function generateAlterTableSql( + originalCols: ColumnDefinition[], + currentCols: ColumnDefinition[], + tableName: string, + relationships: FkRelationshipDefinition[], + originalRelationships: FkRelationshipDefinition[], +): string { + const fullTableName = `[dbo].[${tableName}]` + const lines: string[] = [`/* ── ALTER TABLE: ${fullTableName} ── */`, ''] + let hasChanges = false + + // Build id-based maps for O(1) lookup + const origById = new Map() + originalCols.forEach((c) => origById.set(c.id, c)) + + const curById = new Map() + currentCols.filter((c) => c.columnName.trim()).forEach((c) => curById.set(c.id, c)) + + // ➕ Added columns (id exists in current but NOT in original) + currentCols + .filter((c) => c.columnName.trim()) + .forEach((col) => { + if (!origById.has(col.id)) { + hasChanges = true + lines.push(`-- ➕ Yeni Sütun: [${col.columnName}]`) + lines.push(`ALTER TABLE ${fullTableName}`) + lines.push(` ADD ${colToSqlLine(col, false).trimStart()};`) + lines.push('') + } + }) + + // ❌ Silinen sütunlar (id exists in original but NOT in current) + originalCols.forEach((col) => { + if (!curById.has(col.id)) { + hasChanges = true + lines.push(`-- ❌ Silinen Sütun: [${col.columnName}]`) + lines.push(`ALTER TABLE ${fullTableName}`) + lines.push(` DROP COLUMN [${col.columnName}];`) + lines.push('') + } + }) + + // ✏️ Renamed / Modified columns (same id in both) + currentCols + .filter((c) => c.columnName.trim()) + .forEach((col) => { + const orig = origById.get(col.id) + if (!orig) return // new column, already handled above + + const nameChanged = orig.columnName.trim().toLowerCase() !== col.columnName.trim().toLowerCase() + const typeChanged = orig.dataType !== col.dataType || orig.maxLength !== col.maxLength + const nullChanged = orig.isNullable !== col.isNullable + + // Rename: use sp_rename to preserve data + if (nameChanged) { + hasChanges = true + lines.push(`-- 🔄 Sütun Yeniden Adlandırma: [${orig.columnName}] → [${col.columnName}]`) + lines.push( + `EXEC sp_rename 'dbo.${tableName}.${orig.columnName}', '${col.columnName}', 'COLUMN';`, + ) + lines.push('') + } + + // Type or nullability change: ALTER COLUMN (uses new name if also renamed) + if (typeChanged || nullChanged) { + hasChanges = true + const label = nameChanged + ? `-- ✏️ Değiştirilen Sütun (yeniden adlandırıldıktan sonra): [${col.columnName}]` + : `-- ✏️ Değiştirilen Sütun: [${col.columnName}]` + lines.push(label) + lines.push(`ALTER TABLE ${fullTableName}`) + lines.push( + ` ALTER COLUMN ${colToSqlLine({ ...col, defaultValue: '' }, false).trimStart()};`, + ) + lines.push('') + } + }) + + // 🔗 FK Diff: drop removed / drop+re-add modified / add new + const fkCascadeSql = (b: CascadeBehavior) => + b === 'NoAction' ? 'NO ACTION' : b.replace(/([A-Z])/g, ' $1').trim().toUpperCase() + + const addFkSql = (rel: FkRelationshipDefinition) => { + const cname = rel.constraintName ?? `FK_${tableName}_${rel.fkColumnName}` + lines.push(`-- 🔗 FK Ekle: [${rel.fkColumnName}] → [${rel.referencedTable}]`) + lines.push(`ALTER TABLE ${fullTableName}`) + lines.push(` ADD CONSTRAINT [${cname}]`) + lines.push(` FOREIGN KEY ([${rel.fkColumnName}])`) + lines.push(` REFERENCES [dbo].[${rel.referencedTable}] ([${rel.referencedColumn || 'Id'}])`) + lines.push(` ON DELETE ${fkCascadeSql(rel.cascadeDelete)}`) + lines.push(` ON UPDATE ${fkCascadeSql(rel.cascadeUpdate)};`) + lines.push('') + } + + const origRelById = new Map() + originalRelationships.forEach((r) => origRelById.set(r.id, r)) + const curRelById = new Map() + relationships.forEach((r) => curRelById.set(r.id, r)) + + // Removed FKs → DROP CONSTRAINT + originalRelationships.forEach((orig) => { + if (!curRelById.has(orig.id)) { + hasChanges = true + const cname = orig.constraintName ?? `FK_${tableName}_${orig.fkColumnName}` + lines.push(`-- ❌ FK Kaldır: [${orig.constraintName ?? cname}]`) + lines.push(`ALTER TABLE ${fullTableName}`) + lines.push(` DROP CONSTRAINT [${cname}];`) + lines.push('') + } + }) + + // Changed FKs → DROP old + ADD new + relationships.forEach((rel) => { + if (!rel.fkColumnName.trim() || !rel.referencedTable.trim()) return + const orig = origRelById.get(rel.id) + if (orig) { + // Existing FK — check if anything changed + const changed = + orig.fkColumnName !== rel.fkColumnName || + orig.referencedTable !== rel.referencedTable || + orig.referencedColumn !== rel.referencedColumn || + orig.cascadeDelete !== rel.cascadeDelete || + orig.cascadeUpdate !== rel.cascadeUpdate + if (changed) { + hasChanges = true + const cname = orig.constraintName ?? `FK_${tableName}_${orig.fkColumnName}` + lines.push(`-- ✏️ FK Güncelle (drop + re-add): [${cname}]`) + lines.push(`ALTER TABLE ${fullTableName}`) + lines.push(` DROP CONSTRAINT [${cname}];`) + lines.push('') + addFkSql(rel) + } + } else { + // New FK + hasChanges = true + addFkSql(rel) + } + }) + + if (!hasChanges) { + lines.push( + '/* ℹ️ Henüz değişiklik yapılmadı. Sütunları ekleyin/silin/düzeyin ya da ilişki ekleyin. */', + ) + } + + return lines.join('\n') +} + +// Map TableDesigner data-type → CustomEntityField type +function colTypeToEntityFieldType( + dt: SqlDataType, +): CreateUpdateCustomEntityFieldDto['type'] { + if (dt === 'int' || dt === 'bigint') return 'number' + if (dt === 'bit') return 'boolean' + if (dt === 'datetime2' || dt === 'date') return 'date' + if (dt === 'uniqueidentifier') return 'guid' + if (dt === 'decimal' || dt === 'float' || dt === 'money') return 'decimal' + return 'string' // nvarchar, nvarchar(MAX), etc. +} + +const STEPS = ['Sütun Tasarımı', 'Entity Ayarları', 'İlişkiler', 'T-SQL Önizleme'] as const +type Step = 0 | 1 | 2 | 3 + +const createEmptyColumn = (): ColumnDefinition => ({ + id: crypto.randomUUID(), + columnName: '', + dataType: 'nvarchar', + maxLength: '100', + isNullable: true, + defaultValue: '', + description: '', +}) + +const DEFAULT_SETTINGS: TableSettings = { + menuValue: '', + menuPrefix: '', + entityName: '', + tableName: '', + displayName: '', + description: '', + isActive: true, + isFullAudited: true, + isMultiTenant: false, +} + +// ─── Component ──────────────────────────────────────────────────────────────── + +const TableDesignerDialog = ({ + isOpen, + onClose, + dataSource, + onDeployed, + initialTableData, +}: TableDesignerDialogProps) => { + const { translate } = useLocalization() + + const isEditMode = !!initialTableData + + const [step, setStep] = useState(0) + const [isDeploying, setIsDeploying] = useState(false) + const [columns, setColumns] = useState([createEmptyColumn()]) + const [originalColumns, setOriginalColumns] = useState([]) + const [colsLoading, setColsLoading] = useState(false) + const [settings, setSettings] = useState(DEFAULT_SETTINGS) + const [menuOptions, setMenuOptions] = useState([]) + const [menuLoading, setMenuLoading] = useState(false) + const [relationships, setRelationships] = useState([]) + const [originalRelationships, setOriginalRelationships] = useState([]) + const [fksLoading, setFksLoading] = useState(false) + const [fkModalOpen, setFkModalOpen] = useState(false) + const [editingFkId, setEditingFkId] = useState(null) + const [fkForm, setFkForm] = useState>(EMPTY_FK) + const [dbTables, setDbTables] = useState<{ schemaName: string; tableName: string }[]>([]) + const [targetTableColumns, setTargetTableColumns] = useState([]) + const [targetColsLoading, setTargetColsLoading] = useState(false) + + useEffect(() => { + if (!isOpen) return + setMenuLoading(true) + const svc = new MenuService() + svc + .getListMainMenu() + .then((res) => { + const opts: MenuOption[] = (res.data || []).map((m: any) => ({ + value: m.shortName, + label: m.displayName, + })) + setMenuOptions(opts) + }) + .catch(() => {}) + .finally(() => setMenuLoading(false)) + + if (dataSource) { + sqlObjectManagerService + .getAllObjects(dataSource) + .then((res) => setDbTables(res.data?.tables ?? [])) + .catch(() => {}) + } + + // Edit mode: load table's existing columns + FK constraints + if (initialTableData && dataSource) { + setColsLoading(true) + sqlObjectManagerService + .getTableColumns(dataSource, initialTableData.schemaName, initialTableData.tableName) + .then((res) => { + const defs = (res.data ?? []).map(dbColToColumnDef) + setColumns(defs.length > 0 ? defs : [createEmptyColumn()]) + setOriginalColumns(defs) + setSettings((s) => ({ ...s, tableName: initialTableData.tableName })) + }) + .catch(() => {}) + .finally(() => setColsLoading(false)) + + // Load CustomEntity metadata (menu, entityName, displayName, flags…) + developerKitService + .getCustomEntities() + .then((res) => { + const match = (res.items ?? []).find( + (e) => e.tableName.toLowerCase() === initialTableData.tableName.toLowerCase(), + ) + if (match) { + setSettings((s) => ({ + ...s, + menuValue: match.menu, + menuPrefix: match.menu, + entityName: match.name, + displayName: match.displayName, + description: match.description ?? '', + isActive: match.isActive, + isFullAudited: match.isFullAuditedEntity, + isMultiTenant: match.isMultiTenant, + })) + } + }) + .catch(() => {}) + + // Load existing FK constraints + const fkQuery = [ + 'SELECT', + ' fk.name AS constraintName,', + ' col.name AS fkColumnName,', + ' ref_t.name AS referencedTable,', + ' ref_c.name AS referencedColumn,', + ' fk.delete_referential_action_desc AS cascadeDelete,', + ' fk.update_referential_action_desc AS cascadeUpdate', + 'FROM sys.foreign_keys fk', + 'INNER JOIN sys.foreign_key_columns fkc ON fk.object_id = fkc.constraint_object_id', + 'INNER JOIN sys.columns col ON fkc.parent_object_id = col.object_id AND fkc.parent_column_id = col.column_id', + 'INNER JOIN sys.tables ref_t ON fkc.referenced_object_id = ref_t.object_id', + 'INNER JOIN sys.columns ref_c ON fkc.referenced_object_id = ref_c.object_id AND fkc.referenced_column_id = ref_c.column_id', + `WHERE fk.parent_object_id = OBJECT_ID('${initialTableData.schemaName}.${initialTableData.tableName}')`, + ].join('\n') + + const mapCascade = (v: string): CascadeBehavior => { + if (v === 'CASCADE') return 'Cascade' + if (v === 'SET_NULL') return 'SetNull' + if (v === 'SET_DEFAULT') return 'Restrict' + return 'NoAction' + } + + setFksLoading(true) + sqlObjectManagerService + .executeQuery({ queryText: fkQuery, dataSourceCode: dataSource }) + .then((res) => { + const rows: any[] = res.data?.data ?? [] + const fkDefs: FkRelationshipDefinition[] = rows.map((r) => ({ + id: crypto.randomUUID(), + constraintName: r.constraintName, + relationshipType: 'OneToMany' as RelationshipType, + fkColumnName: r.fkColumnName, + referencedTable: r.referencedTable, + referencedColumn: r.referencedColumn, + cascadeDelete: mapCascade(r.cascadeDelete), + cascadeUpdate: mapCascade(r.cascadeUpdate), + isRequired: false, + description: '', + })) + setRelationships(fkDefs) + setOriginalRelationships(fkDefs) + }) + .catch(() => {}) + .finally(() => setFksLoading(false)) + } + }, [isOpen, dataSource, initialTableData]) + + const generatedSql = useMemo( + () => + isEditMode + ? generateAlterTableSql( + originalColumns, + columns, + settings.tableName || initialTableData?.tableName || '', + relationships, + originalRelationships, + ) + : generateCreateTableSql(columns, settings, relationships), + [isEditMode, originalColumns, columns, settings, initialTableData, relationships, originalRelationships], + ) + + // ── Column operations ────────────────────────────────────────────────────── + + const addColumn = () => setColumns((prev) => [...prev, createEmptyColumn()]) + + const removeColumn = (id: string) => setColumns((prev) => prev.filter((c) => c.id !== id)) + + const moveColumn = (id: string, direction: 'up' | 'down') => { + setColumns((prev) => { + const idx = prev.findIndex((c) => c.id === id) + if (idx < 0) return prev + const next = [...prev] + const swap = direction === 'up' ? idx - 1 : idx + 1 + if (swap < 0 || swap >= next.length) return prev + ;[next[idx], next[swap]] = [next[swap], next[idx]] + return next + }) + } + + const updateColumn = useCallback( + (id: string, field: K, value: ColumnDefinition[K]) => { + setColumns((prev) => prev.map((c) => (c.id === id ? { ...c, [field]: value } : c))) + }, + [], + ) + + // ── Settings helpers ─────────────────────────────────────────────────────── + + const buildTableName = (prefix: string, entity: string) => + prefix && entity ? `${prefix}_D_${entity}` : '' + + const onMenuChange = (value: string) => { + const opt = menuOptions.find((o) => o.value === value) + const prefix = opt?.value ?? '' + setSettings((s) => ({ + ...s, + menuValue: value, + menuPrefix: prefix, + tableName: buildTableName(prefix, s.entityName), + })) + } + + // Strip spaces and special chars — only alphanumeric + underscore allowed + const onEntityNameChange = (value: string) => { + const sanitized = value.replace(/[^A-Za-z0-9_]/g, '') + setSettings((s) => ({ + ...s, + entityName: sanitized, + tableName: buildTableName(s.menuPrefix, sanitized), + displayName: sanitized, // always mirrors entity name + })) + } + + // ── FK Relationship handlers ─────────────────────────────────────────────── + + const loadTargetColumns = (tableName: string) => { + if (!tableName || !dataSource) { + setTargetTableColumns([]) + return + } + const tbl = dbTables.find((t) => t.tableName === tableName) + if (!tbl) return + setTargetColsLoading(true) + sqlObjectManagerService + .getTableColumns(dataSource, tbl.schemaName, tbl.tableName) + .then((res) => setTargetTableColumns((res.data ?? []).map((c) => c.columnName))) + .catch(() => setTargetTableColumns([])) + .finally(() => setTargetColsLoading(false)) + } + + const openAddFk = () => { + setEditingFkId(null) + setFkForm(EMPTY_FK) + setTargetTableColumns([]) + setFkModalOpen(true) + } + + const openEditFk = (rel: FkRelationshipDefinition) => { + setEditingFkId(rel.id) + const { id: _id, ...rest } = rel + setFkForm(rest) + loadTargetColumns(rest.referencedTable) + setFkModalOpen(true) + } + + const saveFk = () => { + if (!fkForm.fkColumnName.trim() || !fkForm.referencedTable.trim()) return + if (editingFkId) { + setRelationships((prev) => + prev.map((r) => (r.id === editingFkId ? { ...fkForm, id: editingFkId } : r)), + ) + } else { + setRelationships((prev) => [...prev, { ...fkForm, id: crypto.randomUUID() }]) + } + setFkModalOpen(false) + } + + const deleteFk = (id: string) => { + setRelationships((prev) => prev.filter((r) => r.id !== id)) + } + + // ── Navigation ───────────────────────────────────────────────────────────── + + // Compute duplicate column names (lowercased) + const duplicateColumnNames = useMemo(() => { + const names = columns.map((c) => c.columnName.trim().toLowerCase()).filter(Boolean) + return new Set(names.filter((n, i) => names.indexOf(n) !== i)) + }, [columns]) + + const canGoNext = (): boolean => { + if (step === 0) { + const hasNamed = columns.some((c) => c.columnName.trim().length > 0) + return hasNamed && duplicateColumnNames.size === 0 + } + if (step === 1) { + if (isEditMode) return true // table name is locked in edit mode + const baseOk = + !!settings.tableName.trim() && !!settings.entityName.trim() && !!settings.menuValue + if (!baseOk) return false + if (!settings.isFullAudited) { + return columns.some((c) => c.columnName.trim().toLowerCase() === 'id') + } + return true + } + return true + } + + const handleNext = () => { + if (step < 3) setStep((s) => (s + 1) as Step) + } + const handleBack = () => { + if (step > 0) setStep((s) => (s - 1) as Step) + } + + // ── Deploy ───────────────────────────────────────────────────────────────── + + const syncCustomEntityMetadata = async (deployedTableName: string) => { + try { + const namedCols = columns.filter((c) => c.columnName.trim()) + + // Find existing CustomEntity record by table name + const listResult = await developerKitService.getCustomEntities() + const existing = (listResult.items ?? []).find( + (e) => e.tableName.toLowerCase() === deployedTableName.toLowerCase(), + ) + + if (existing) { + // Update: keep existing metadata, sync fields (match by name to preserve IDs) + const fieldDtos: CreateUpdateCustomEntityFieldDto[] = namedCols.map((col, i) => { + const existingField = existing.fields?.find( + (f) => f.name.toLowerCase() === col.columnName.trim().toLowerCase(), + ) + const maxLen = col.maxLength ? parseInt(col.maxLength, 10) || undefined : undefined + return { + id: existingField?.id, + name: col.columnName.trim(), + type: colTypeToEntityFieldType(col.dataType), + isRequired: !col.isNullable, + maxLength: maxLen, + isUnique: false, + defaultValue: col.defaultValue || undefined, + description: col.description || undefined, + displayOrder: i, + } + }) + const updateDto: CreateUpdateCustomEntityDto = { + menu: existing.menu, + name: existing.name, + displayName: existing.displayName, + tableName: existing.tableName, + description: existing.description, + isActive: existing.isActive, + isFullAuditedEntity: existing.isFullAuditedEntity, + isMultiTenant: existing.isMultiTenant, + fields: fieldDtos, + } + await developerKitService.updateCustomEntity(existing.id, updateDto) + } else { + // Create new CustomEntity record + const fieldDtos: CreateUpdateCustomEntityFieldDto[] = namedCols.map((col, i) => { + const maxLen = col.maxLength ? parseInt(col.maxLength, 10) || undefined : undefined + return { + name: col.columnName.trim(), + type: colTypeToEntityFieldType(col.dataType), + isRequired: !col.isNullable, + maxLength: maxLen, + isUnique: false, + defaultValue: col.defaultValue || undefined, + description: col.description || undefined, + displayOrder: i, + } + }) + const createDto: CreateUpdateCustomEntityDto = { + menu: settings.menuValue, + name: settings.entityName || deployedTableName, + displayName: settings.displayName || deployedTableName, + tableName: deployedTableName, + description: settings.description || undefined, + isActive: settings.isActive, + isFullAuditedEntity: settings.isFullAudited, + isMultiTenant: settings.isMultiTenant, + fields: fieldDtos, + } + await developerKitService.createCustomEntity(createDto) + } + } catch { + // Silent — SQL deploy already succeeded; metadata sync failure is non-critical + } + } + + const handleDeploy = async () => { + if (!dataSource) { + toast.push( + + Lutfen once bir veri kaynagi secin. + , + { placement: 'top-center' }, + ) + return + } + setIsDeploying(true) + try { + const result = await sqlObjectManagerService.executeQuery({ + queryText: generatedSql, + dataSourceCode: dataSource, + }) + if (result.data.success) { + const deployedTable = settings.tableName || initialTableData?.tableName || '' + // Sync entity metadata to CustomEntity / CustomEntityField tables + await syncCustomEntityMetadata(deployedTable) + toast.push( + + Tablo basariyla {isEditMode ? 'güncellendi' : 'oluşturuldu'}: [dbo].[{deployedTable}] + , + { placement: 'top-center' }, + ) + onDeployed?.() + handleClose() + } else { + toast.push( + + {result.data.message || 'Tablo olusturulamadi.'} + , + { placement: 'top-center' }, + ) + } + } catch (error: any) { + toast.push( + + {error.response?.data?.error?.message || 'Tablo deploy edilemedi.'} + , + { placement: 'top-center' }, + ) + } finally { + setIsDeploying(false) + } + } + + const handleClose = () => { + setStep(0) + setColumns([createEmptyColumn()]) + setOriginalColumns([]) + setSettings(DEFAULT_SETTINGS) + setRelationships([]) + setOriginalRelationships([]) + setDbTables([]) + setTargetTableColumns([]) + onClose() + } + + // ── Step Indicator ───────────────────────────────────────────────────────── + + const STEP_LABELS = ['Sütun Tasarımı', 'Entity Ayarları', 'İlişkiler', 'T-SQL Önizleme'] + + const renderStepIndicator = () => ( +
+ {STEP_LABELS.map((label, i) => { + const isDone = i < step + const isActive = i === step + return ( +
+
+ {isDone ? : i + 1} +
+ + {label} + + {i < STEP_LABELS.length - 1 && ( + + )} +
+ ) + })} +
+ ) + + // ── Step 0: Column Designer ──────────────────────────────────────────────── + + const renderColumnDesigner = () => ( +
+ {colsLoading && ( +
+ Tablo sütunları yükleniyor... +
+ )} + {!colsLoading && ( + <> +
+ {isEditMode ? ( +
+ 📝 Mevcut sütunlar yüklendi. Değişiklikler T-SQL Önizleme adımında ALTER TABLE olarak gösterilecek. +  | + + Yeni + Ad Değişti (sp_rename) + Tip/Null Değişti + +
+ ) : ( +
+ Sonraki adımda seceğiniz{' '} + Full Audited Entity / Multi-Tenant ayarlarina gore ek + sutunlar da dahil edilecektir. +
+ )} + +
+ + {/* Header row */} +
+
Sutun Adi *
+
Veri Tipi *
+
Max
+
Null
+
Varsayilan
+
Not
+
Islem
+
+ + {/* Editable column rows */} + {duplicateColumnNames.size > 0 && ( +
+ ⚠️ Aynı isimde sütun tanımlanamaz:{' '} + {[...duplicateColumnNames].map((n) => ( + + {n} + + ))} +
+ )} + +
+ {columns.map((col, idx) => { + const isDuplicate = + col.columnName.trim() !== '' && + duplicateColumnNames.has(col.columnName.trim().toLowerCase()) + const isNewRow = + isEditMode && + !originalColumns.some((o) => o.id === col.id) + + const origRow = isEditMode ? originalColumns.find((o) => o.id === col.id) : undefined + const isRenamedRow = + isEditMode && + !!origRow && + origRow.columnName.trim().toLowerCase() !== col.columnName.trim().toLowerCase() + const isModifiedRow = + isEditMode && + !!origRow && + !isNewRow && + (origRow.dataType !== col.dataType || + origRow.maxLength !== col.maxLength || + origRow.isNullable !== col.isNullable) + + const rowBg = isDuplicate + ? 'outline outline-1 outline-red-400' + : isNewRow + ? 'bg-green-50 dark:bg-green-900/20 outline outline-1 outline-green-400' + : isRenamedRow && isModifiedRow + ? 'bg-purple-50 dark:bg-purple-900/20 outline outline-1 outline-purple-400' + : isRenamedRow + ? 'bg-blue-50 dark:bg-blue-900/20 outline outline-1 outline-blue-400' + : isModifiedRow + ? 'bg-yellow-50 dark:bg-yellow-900/20 outline outline-1 outline-yellow-400' + : '' + + return ( +
+
+ updateColumn(col.id, 'columnName', e.target.value)} + /> +
+
+ +
+
+ updateColumn(col.id, 'maxLength', e.target.value)} + /> +
+
+ updateColumn(col.id, 'isNullable', checked as boolean)} + /> +
+
+ updateColumn(col.id, 'defaultValue', e.target.value)} + /> +
+
+ updateColumn(col.id, 'description', e.target.value)} + /> +
+
+ + + +
+
+ ) + })} +
+ + {/* Id warning when Full Audited is off */} + {!isEditMode && !settings.isFullAudited && + !columns.some((c) => c.columnName.trim().toLowerCase() === 'id') && ( +
+ ⚠️ Full Audited Entity seçilmedi. Tablonun birincil anahtarı için{' '} + Id sütununu + elle eklemeniz gerekmektedir. +
+ )} + + )} +
+ ) + + // ── Step 1: Entity Settings ──────────────────────────────────────────────── + + const renderEntitySettings = () => ( +
+
+ {/* Menu Name */} +
+ + + {menuLoading &&

Yukleniyor...

} +
+ + {/* Entity Name */} +
+ + onEntityNameChange(e.target.value)} + placeholder="e.g. Product, User, Order" + /> +
+ + {/* Table Name (readonly, auto-generated) */} +
+ + + {isEditMode && ( +

Mevcut tablo — ad değiştirilemez.

+ )} +
+ + {/* Display Name */} +
+ + setSettings((s) => ({ ...s, displayName: e.target.value }))} + placeholder="e.g. Ürün, Kullanıcı, Sipariş" + /> +
+ + {/* Description */} +
+ +