2026-03-01 17:40:25 +00:00
|
|
|
|
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,
|
|
|
|
|
|
FaLink,
|
|
|
|
|
|
FaEdit,
|
|
|
|
|
|
FaTimes,
|
|
|
|
|
|
FaArrowRight,
|
|
|
|
|
|
} from 'react-icons/fa'
|
|
|
|
|
|
import { sqlObjectManagerService } from '@/services/sql-query-manager.service'
|
|
|
|
|
|
import { MenuService } from '@/services/menu.service'
|
|
|
|
|
|
import { developerKitService } from '@/services/developerKit.service'
|
2026-03-01 20:43:25 +00:00
|
|
|
|
import type { CreateUpdateSqlTableDto, CreateUpdateSqlTableFieldDto } from '@/proxy/developerKit/models'
|
2026-03-01 17:40:25 +00:00
|
|
|
|
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<FkRelationshipDefinition, 'id'> = {
|
|
|
|
|
|
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<string, ColumnDefinition>()
|
|
|
|
|
|
originalCols.forEach((c) => origById.set(c.id, c))
|
|
|
|
|
|
|
|
|
|
|
|
const curById = new Map<string, ColumnDefinition>()
|
|
|
|
|
|
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<string, FkRelationshipDefinition>()
|
|
|
|
|
|
originalRelationships.forEach((r) => origRelById.set(r.id, r))
|
|
|
|
|
|
const curRelById = new Map<string, FkRelationshipDefinition>()
|
|
|
|
|
|
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')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-01 20:43:25 +00:00
|
|
|
|
// Map TableDesigner data-type → SqlTableField type
|
2026-03-01 17:40:25 +00:00
|
|
|
|
function colTypeToEntityFieldType(
|
|
|
|
|
|
dt: SqlDataType,
|
2026-03-01 20:43:25 +00:00
|
|
|
|
): CreateUpdateSqlTableFieldDto['type'] {
|
2026-03-01 17:40:25 +00:00
|
|
|
|
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 ────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
2026-03-01 20:43:25 +00:00
|
|
|
|
const SqlTableDesignerDialog = ({
|
2026-03-01 17:40:25 +00:00
|
|
|
|
isOpen,
|
|
|
|
|
|
onClose,
|
|
|
|
|
|
dataSource,
|
|
|
|
|
|
onDeployed,
|
|
|
|
|
|
initialTableData,
|
|
|
|
|
|
}: TableDesignerDialogProps) => {
|
|
|
|
|
|
const { translate } = useLocalization()
|
|
|
|
|
|
|
|
|
|
|
|
const isEditMode = !!initialTableData
|
|
|
|
|
|
|
|
|
|
|
|
const [step, setStep] = useState<Step>(0)
|
|
|
|
|
|
const [isDeploying, setIsDeploying] = useState(false)
|
|
|
|
|
|
const [columns, setColumns] = useState<ColumnDefinition[]>([createEmptyColumn()])
|
|
|
|
|
|
const [originalColumns, setOriginalColumns] = useState<ColumnDefinition[]>([])
|
|
|
|
|
|
const [colsLoading, setColsLoading] = useState(false)
|
|
|
|
|
|
const [settings, setSettings] = useState<TableSettings>(DEFAULT_SETTINGS)
|
|
|
|
|
|
const [menuOptions, setMenuOptions] = useState<MenuOption[]>([])
|
|
|
|
|
|
const [menuLoading, setMenuLoading] = useState(false)
|
|
|
|
|
|
const [relationships, setRelationships] = useState<FkRelationshipDefinition[]>([])
|
|
|
|
|
|
const [originalRelationships, setOriginalRelationships] = useState<FkRelationshipDefinition[]>([])
|
|
|
|
|
|
const [fksLoading, setFksLoading] = useState(false)
|
|
|
|
|
|
const [fkModalOpen, setFkModalOpen] = useState(false)
|
|
|
|
|
|
const [editingFkId, setEditingFkId] = useState<string | null>(null)
|
|
|
|
|
|
const [fkForm, setFkForm] = useState<Omit<FkRelationshipDefinition, 'id'>>(EMPTY_FK)
|
|
|
|
|
|
const [dbTables, setDbTables] = useState<{ schemaName: string; tableName: string }[]>([])
|
|
|
|
|
|
const [targetTableColumns, setTargetTableColumns] = useState<string[]>([])
|
|
|
|
|
|
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))
|
|
|
|
|
|
|
2026-03-01 20:43:25 +00:00
|
|
|
|
// Load SqlTable metadata (menu, entityName, displayName, flags…)
|
2026-03-01 17:40:25 +00:00
|
|
|
|
developerKitService
|
2026-03-01 20:43:25 +00:00
|
|
|
|
.getSqlTables()
|
2026-03-01 17:40:25 +00:00
|
|
|
|
.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,
|
|
|
|
|
|
}))
|
2026-03-01 21:54:46 +00:00
|
|
|
|
} else {
|
|
|
|
|
|
// Table not registered in developerKit — derive defaults from the table name.
|
|
|
|
|
|
// e.g. "Sas_T_SqlStoredProcedure" → menu: "Sas", entityName/displayName: "SqlStoredProcedure"
|
|
|
|
|
|
const parts = initialTableData.tableName.split('_')
|
|
|
|
|
|
const derivedMenu = parts[0] ?? ''
|
|
|
|
|
|
console.log('Derived menu from table name:', derivedMenu)
|
|
|
|
|
|
const derivedEntity = parts[parts.length - 1] ?? initialTableData.tableName
|
|
|
|
|
|
setSettings((s) => ({
|
|
|
|
|
|
...s,
|
|
|
|
|
|
menuValue: derivedMenu,
|
|
|
|
|
|
menuPrefix: derivedMenu,
|
|
|
|
|
|
entityName: derivedEntity,
|
|
|
|
|
|
displayName: derivedEntity,
|
|
|
|
|
|
}))
|
2026-03-01 17:40:25 +00:00
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
.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(
|
|
|
|
|
|
<K extends keyof ColumnDefinition>(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 ─────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
2026-03-01 20:43:25 +00:00
|
|
|
|
const syncSqlTableMetadata = async (deployedTableName: string) => {
|
2026-03-01 17:40:25 +00:00
|
|
|
|
try {
|
|
|
|
|
|
const namedCols = columns.filter((c) => c.columnName.trim())
|
|
|
|
|
|
|
2026-03-01 20:43:25 +00:00
|
|
|
|
// Find existing SqlTable record by table name
|
|
|
|
|
|
const listResult = await developerKitService.getSqlTables()
|
2026-03-01 17:40:25 +00:00
|
|
|
|
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)
|
2026-03-01 20:43:25 +00:00
|
|
|
|
const fieldDtos: CreateUpdateSqlTableFieldDto[] = namedCols.map((col, i) => {
|
2026-03-01 17:40:25 +00:00
|
|
|
|
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,
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
2026-03-01 20:43:25 +00:00
|
|
|
|
const updateDto: CreateUpdateSqlTableDto = {
|
2026-03-01 17:40:25 +00:00
|
|
|
|
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,
|
|
|
|
|
|
}
|
2026-03-01 20:43:25 +00:00
|
|
|
|
await developerKitService.updateSqlTable(existing.id, updateDto)
|
2026-03-01 17:40:25 +00:00
|
|
|
|
} else {
|
2026-03-01 20:43:25 +00:00
|
|
|
|
// Create new SqlTable record
|
|
|
|
|
|
const fieldDtos: CreateUpdateSqlTableFieldDto[] = namedCols.map((col, i) => {
|
2026-03-01 17:40:25 +00:00
|
|
|
|
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,
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
2026-03-01 20:43:25 +00:00
|
|
|
|
const createDto: CreateUpdateSqlTableDto = {
|
2026-03-01 17:40:25 +00:00
|
|
|
|
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,
|
|
|
|
|
|
}
|
2026-03-01 20:43:25 +00:00
|
|
|
|
await developerKitService.createSqlTable(createDto)
|
2026-03-01 17:40:25 +00:00
|
|
|
|
}
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
// Silent — SQL deploy already succeeded; metadata sync failure is non-critical
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleDeploy = async () => {
|
|
|
|
|
|
if (!dataSource) {
|
|
|
|
|
|
toast.push(
|
|
|
|
|
|
<Notification type="warning" title="Uyari">
|
|
|
|
|
|
Lutfen once bir veri kaynagi secin.
|
|
|
|
|
|
</Notification>,
|
|
|
|
|
|
{ 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 || ''
|
2026-03-01 20:43:25 +00:00
|
|
|
|
// Sync entity metadata to SqlTable / SqlTableField tables
|
|
|
|
|
|
await syncSqlTableMetadata(deployedTable)
|
2026-03-01 17:40:25 +00:00
|
|
|
|
toast.push(
|
|
|
|
|
|
<Notification type="success" title="Basarili">
|
|
|
|
|
|
Tablo basariyla {isEditMode ? 'güncellendi' : 'oluşturuldu'}: [dbo].[{deployedTable}]
|
|
|
|
|
|
</Notification>,
|
|
|
|
|
|
{ placement: 'top-center' },
|
|
|
|
|
|
)
|
|
|
|
|
|
onDeployed?.()
|
|
|
|
|
|
handleClose()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
toast.push(
|
|
|
|
|
|
<Notification type="danger" title="Hata">
|
|
|
|
|
|
{result.data.message || 'Tablo olusturulamadi.'}
|
|
|
|
|
|
</Notification>,
|
|
|
|
|
|
{ placement: 'top-center' },
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
|
toast.push(
|
|
|
|
|
|
<Notification type="danger" title="Hata">
|
|
|
|
|
|
{error.response?.data?.error?.message || 'Tablo deploy edilemedi.'}
|
|
|
|
|
|
</Notification>,
|
|
|
|
|
|
{ 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 = () => (
|
|
|
|
|
|
<div className="flex items-center gap-2 mb-2">
|
|
|
|
|
|
{STEP_LABELS.map((label, i) => {
|
|
|
|
|
|
const isDone = i < step
|
|
|
|
|
|
const isActive = i === step
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div key={i} className="flex items-center gap-2">
|
|
|
|
|
|
<div
|
|
|
|
|
|
className={`w-7 h-7 rounded-full flex items-center justify-center text-xs font-bold transition-colors
|
|
|
|
|
|
${isDone ? 'bg-green-500 text-white' : isActive ? 'bg-blue-600 text-white' : 'bg-gray-200 text-gray-500 dark:bg-gray-700 dark:text-gray-400'}`}
|
|
|
|
|
|
>
|
|
|
|
|
|
{isDone ? <FaCheck /> : i + 1}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<span
|
|
|
|
|
|
className={`text-sm font-medium ${isActive ? 'text-blue-600 dark:text-blue-400' : 'text-gray-500'}`}
|
|
|
|
|
|
>
|
|
|
|
|
|
{label}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
{i < STEP_LABELS.length - 1 && (
|
|
|
|
|
|
<FaChevronRight className="text-gray-300 dark:text-gray-600 mx-1" />
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
})}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// ── Step 0: Column Designer ────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
const renderColumnDesigner = () => (
|
|
|
|
|
|
<div className="flex flex-col">
|
|
|
|
|
|
{colsLoading && (
|
|
|
|
|
|
<div className="flex items-center justify-center py-12 text-gray-500">
|
|
|
|
|
|
<span className="animate-spin mr-2">◠</span> Tablo sütunları yükleniyor...
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{!colsLoading && (
|
|
|
|
|
|
<>
|
|
|
|
|
|
<div className="flex items-center justify-between py-3">
|
|
|
|
|
|
{isEditMode ? (
|
|
|
|
|
|
<div className="p-2 bg-blue-50 dark:bg-blue-900/10 border border-blue-200 dark:border-blue-800 rounded text-xs text-blue-700 dark:text-blue-300">
|
|
|
|
|
|
📝 Mevcut sütunlar yüklendi. Değişiklikler T-SQL Önizleme adımında <strong>ALTER TABLE</strong> olarak gösterilecek.
|
|
|
|
|
|
|
|
|
|
|
|
|
<span className="ml-1">
|
|
|
|
|
|
<span className="inline-block w-2.5 h-2.5 rounded bg-green-200 border border-green-400 mr-0.5"></span>Yeni
|
|
|
|
|
|
<span className="inline-block w-2.5 h-2.5 rounded bg-blue-200 border border-blue-400 mx-0.5 ml-2"></span>Ad Değişti (sp_rename)
|
|
|
|
|
|
<span className="inline-block w-2.5 h-2.5 rounded bg-yellow-200 border border-yellow-400 mx-0.5 ml-2"></span>Tip/Null Değişti
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<div className="p-2 bg-amber-50 dark:bg-amber-900/10 border border-amber-200 dark:border-amber-800 rounded text-xs text-amber-700 dark:text-amber-300">
|
|
|
|
|
|
Sonraki adımda seceğiniz{' '}
|
|
|
|
|
|
<strong>Full Audited Entity</strong> / <strong>Multi-Tenant</strong> ayarlarina gore ek
|
|
|
|
|
|
sutunlar da dahil edilecektir.
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
<Button size="xs" variant="solid" color="blue-600" icon={<FaPlus />} onClick={addColumn}>
|
|
|
|
|
|
Ekle
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Header row */}
|
|
|
|
|
|
<div className="grid grid-cols-12 gap-2 px-2 py-1 bg-gray-50 dark:bg-gray-800 rounded text-xs font-semibold text-gray-600 dark:text-gray-300">
|
|
|
|
|
|
<div className="col-span-3">Sutun Adi *</div>
|
|
|
|
|
|
<div className="col-span-3">Veri Tipi *</div>
|
|
|
|
|
|
<div className="col-span-1 text-center">Max</div>
|
|
|
|
|
|
<div className="col-span-1 text-center">Null</div>
|
|
|
|
|
|
<div className="col-span-2">Varsayilan</div>
|
|
|
|
|
|
<div className="col-span-1">Not</div>
|
|
|
|
|
|
<div className="col-span-1 text-center">Islem</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Editable column rows */}
|
|
|
|
|
|
{duplicateColumnNames.size > 0 && (
|
|
|
|
|
|
<div className="px-2 py-1.5 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded text-xs text-red-600 dark:text-red-400">
|
|
|
|
|
|
⚠️ Aynı isimde sütun tanımlanamaz:{' '}
|
|
|
|
|
|
{[...duplicateColumnNames].map((n) => (
|
|
|
|
|
|
<code key={n} className="bg-red-100 dark:bg-red-900/40 px-1 rounded mr-1">
|
|
|
|
|
|
{n}
|
|
|
|
|
|
</code>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
<div className="flex flex-col gap-1 max-h-80 overflow-y-auto pr-1">
|
|
|
|
|
|
{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 (
|
|
|
|
|
|
<div
|
|
|
|
|
|
key={col.id}
|
|
|
|
|
|
className={`grid grid-cols-12 gap-2 py-1 bg-white dark:bg-gray-800 rounded items-center ${rowBg}`}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div className="col-span-3">
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
className={`w-full px-2 py-1 text-sm border rounded dark:bg-gray-700 dark:border-gray-600 dark:text-white ${
|
|
|
|
|
|
isDuplicate ? 'border-red-400' : ''
|
|
|
|
|
|
}`}
|
|
|
|
|
|
placeholder="SutunAdi"
|
|
|
|
|
|
value={col.columnName}
|
|
|
|
|
|
onChange={(e) => updateColumn(col.id, 'columnName', e.target.value)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="col-span-3">
|
|
|
|
|
|
<select
|
|
|
|
|
|
className="w-full px-2 py-1 text-sm border rounded dark:bg-gray-700 dark:border-gray-600 dark:text-white"
|
|
|
|
|
|
value={col.dataType}
|
|
|
|
|
|
onChange={(e) => {
|
|
|
|
|
|
const dt = e.target.value as SqlDataType
|
|
|
|
|
|
updateColumn(col.id, 'dataType', dt)
|
|
|
|
|
|
if (dt !== 'nvarchar') updateColumn(col.id, 'maxLength', '')
|
|
|
|
|
|
else if (!col.maxLength) updateColumn(col.id, 'maxLength', '100')
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
{DATA_TYPES.map((t) => (
|
|
|
|
|
|
<option key={t.value} value={t.value}>
|
|
|
|
|
|
{t.label}
|
|
|
|
|
|
</option>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="col-span-1">
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
className="w-full px-2 py-1 text-sm border rounded dark:bg-gray-700 dark:border-gray-600 dark:text-white text-center"
|
|
|
|
|
|
placeholder="-"
|
|
|
|
|
|
value={col.maxLength}
|
|
|
|
|
|
disabled={col.dataType !== 'nvarchar'}
|
|
|
|
|
|
onChange={(e) => updateColumn(col.id, 'maxLength', e.target.value)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="col-span-1 flex justify-center">
|
|
|
|
|
|
<Checkbox
|
|
|
|
|
|
checked={col.isNullable}
|
|
|
|
|
|
onChange={(checked) => updateColumn(col.id, 'isNullable', checked as boolean)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="col-span-2">
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
className="w-full px-2 py-1 text-sm border rounded dark:bg-gray-700 dark:border-gray-600 dark:text-white"
|
|
|
|
|
|
placeholder="Varsayilan"
|
|
|
|
|
|
value={col.defaultValue}
|
|
|
|
|
|
onChange={(e) => updateColumn(col.id, 'defaultValue', e.target.value)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="col-span-1">
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
className="w-full px-2 py-1 text-xs border rounded dark:bg-gray-700 dark:border-gray-600 dark:text-white"
|
|
|
|
|
|
placeholder="Not"
|
|
|
|
|
|
value={col.description}
|
|
|
|
|
|
onChange={(e) => updateColumn(col.id, 'description', e.target.value)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="col-span-1 flex items-center justify-center gap-1">
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => moveColumn(col.id, 'up')}
|
|
|
|
|
|
disabled={idx === 0}
|
|
|
|
|
|
className="p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-700 disabled:opacity-30 text-gray-500"
|
|
|
|
|
|
title="Yukari tasi"
|
|
|
|
|
|
>
|
|
|
|
|
|
<FaArrowUp className="text-xs" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => moveColumn(col.id, 'down')}
|
|
|
|
|
|
disabled={idx === columns.length - 1}
|
|
|
|
|
|
className="p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-700 disabled:opacity-30 text-gray-500"
|
|
|
|
|
|
title="Asagi tasi"
|
|
|
|
|
|
>
|
|
|
|
|
|
<FaArrowDown className="text-xs" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => removeColumn(col.id)}
|
|
|
|
|
|
className="p-1 rounded hover:bg-red-50 dark:hover:bg-red-900/30 text-red-500"
|
|
|
|
|
|
title="Sil"
|
|
|
|
|
|
>
|
|
|
|
|
|
<FaTrash className="text-xs" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
})}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Id warning when Full Audited is off */}
|
|
|
|
|
|
{!isEditMode && !settings.isFullAudited &&
|
|
|
|
|
|
!columns.some((c) => c.columnName.trim().toLowerCase() === 'id') && (
|
|
|
|
|
|
<div className="px-2 py-1.5 bg-orange-50 dark:bg-orange-900/20 border border-orange-300 dark:border-orange-700 rounded text-xs text-orange-700 dark:text-orange-300">
|
|
|
|
|
|
⚠️ <strong>Full Audited Entity</strong> seçilmedi. Tablonun birincil anahtarı için{' '}
|
|
|
|
|
|
<code className="bg-orange-100 dark:bg-orange-900/40 px-1 rounded">Id</code> sütununu
|
|
|
|
|
|
elle eklemeniz gerekmektedir.
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// ── Step 1: Entity Settings ────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
const renderEntitySettings = () => (
|
|
|
|
|
|
<div className="flex flex-col gap-2">
|
|
|
|
|
|
<div className="grid grid-cols-2 gap-2">
|
|
|
|
|
|
{/* Menu Name */}
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label className="block text-sm font-medium mb-1">
|
|
|
|
|
|
Menu Name <span className="text-red-500">*</span>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<select
|
|
|
|
|
|
className="w-full px-3 py-2 text-sm border rounded dark:bg-gray-700 dark:border-gray-600 dark:text-white"
|
|
|
|
|
|
value={settings.menuValue}
|
|
|
|
|
|
disabled={menuLoading}
|
|
|
|
|
|
onChange={(e) => onMenuChange(e.target.value)}
|
|
|
|
|
|
>
|
|
|
|
|
|
<option value=""></option>
|
|
|
|
|
|
{menuOptions.map((opt) => (
|
|
|
|
|
|
<option key={opt.value} value={opt.value}>
|
|
|
|
|
|
{opt.label} ({opt.value})
|
|
|
|
|
|
</option>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</select>
|
|
|
|
|
|
{menuLoading && <p className="text-xs text-gray-400 mt-1">Yukleniyor...</p>}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Entity Name */}
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label className="block text-sm font-medium mb-1">
|
|
|
|
|
|
Entity Name <span className="text-red-500">*</span>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
className="w-full px-3 py-2 text-sm border rounded dark:bg-gray-700 dark:border-gray-600 dark:text-white"
|
|
|
|
|
|
value={settings.entityName}
|
|
|
|
|
|
onChange={(e) => onEntityNameChange(e.target.value)}
|
|
|
|
|
|
placeholder="e.g. Product, User, Order"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Table Name (readonly, auto-generated) */}
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label className="block text-sm font-medium mb-1">Table Name</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
readOnly
|
|
|
|
|
|
className="w-full px-3 py-2 text-sm border rounded bg-gray-50 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 cursor-not-allowed"
|
|
|
|
|
|
value={isEditMode ? (initialTableData?.tableName ?? '') : settings.tableName}
|
|
|
|
|
|
placeholder="Menu ve Entity Name seçince otomatik oluşur"
|
|
|
|
|
|
/>
|
|
|
|
|
|
{isEditMode && (
|
|
|
|
|
|
<p className="text-xs text-blue-500 mt-0.5">Mevcut tablo — ad değiştirilemez.</p>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Display Name */}
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label className="block text-sm font-medium mb-1">Display Name</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="text"
|
|
|
|
|
|
className="w-full px-3 py-2 text-sm border rounded dark:bg-gray-700 dark:border-gray-600 dark:text-white"
|
|
|
|
|
|
value={settings.displayName}
|
|
|
|
|
|
onChange={(e) => setSettings((s) => ({ ...s, displayName: e.target.value }))}
|
|
|
|
|
|
placeholder="e.g. Ürün, Kullanıcı, Sipariş"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Description */}
|
|
|
|
|
|
<div className="col-span-2">
|
|
|
|
|
|
<label className="block text-sm font-medium mb-1">Description</label>
|
|
|
|
|
|
<textarea
|
|
|
|
|
|
rows={2}
|
|
|
|
|
|
className="w-full px-3 py-2 text-sm border rounded dark:bg-gray-700 dark:border-gray-600 dark:text-white resize-none"
|
|
|
|
|
|
value={settings.description}
|
|
|
|
|
|
onChange={(e) => setSettings((s) => ({ ...s, description: e.target.value }))}
|
|
|
|
|
|
placeholder="Brief description of this entity"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Feature flags */}
|
|
|
|
|
|
<div className="border rounded-lg p-2 bg-gray-50 dark:bg-gray-800 space-y-2">
|
|
|
|
|
|
<h6 className="text-sm font-semibold mb-1">Entity Ozellikleri</h6>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Active */}
|
|
|
|
|
|
<div className="flex items-start gap-3">
|
|
|
|
|
|
<Checkbox
|
|
|
|
|
|
checked={settings.isActive}
|
|
|
|
|
|
onChange={(checked) => setSettings((s) => ({ ...s, isActive: checked as boolean }))}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<p className="text-sm font-medium">Active</p>
|
|
|
|
|
|
<p className="text-xs text-gray-500">Entity aktif mi olacak?</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Full Audited */}
|
|
|
|
|
|
<div className="flex items-start gap-2">
|
|
|
|
|
|
<Checkbox
|
|
|
|
|
|
checked={settings.isFullAudited}
|
|
|
|
|
|
onChange={(checked) =>
|
|
|
|
|
|
setSettings((s) => ({ ...s, isFullAudited: checked as boolean }))
|
|
|
|
|
|
}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<p className="text-sm font-medium">Full Audited Entity</p>
|
|
|
|
|
|
<p className="text-xs text-gray-500 mt-0.5">
|
|
|
|
|
|
{[
|
|
|
|
|
|
'Id',
|
|
|
|
|
|
'CreationTime',
|
|
|
|
|
|
'CreatorId',
|
|
|
|
|
|
'LastModificationTime',
|
|
|
|
|
|
'LastModifierId',
|
|
|
|
|
|
'IsDeleted',
|
|
|
|
|
|
'DeletionTime',
|
|
|
|
|
|
'DeleterId',
|
|
|
|
|
|
].map((c) => (
|
|
|
|
|
|
<code key={c} className="bg-gray-100 dark:bg-gray-700 px-1 rounded mr-0.5 text-xs">
|
|
|
|
|
|
{c}
|
|
|
|
|
|
</code>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Multi-Tenant */}
|
|
|
|
|
|
<div className="flex items-start gap-3">
|
|
|
|
|
|
<Checkbox
|
|
|
|
|
|
checked={settings.isMultiTenant}
|
|
|
|
|
|
onChange={(checked) =>
|
|
|
|
|
|
setSettings((s) => ({ ...s, isMultiTenant: checked as boolean }))
|
|
|
|
|
|
}
|
|
|
|
|
|
/>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<p className="text-sm font-medium">Multi-Tenant</p>
|
|
|
|
|
|
<p className="text-xs text-gray-500 mt-0.5">
|
|
|
|
|
|
<code className="bg-gray-100 dark:bg-gray-700 rounded text-xs">TenantId</code>{' '}
|
|
|
|
|
|
(uniqueidentifier NULL)
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Warning: no Id column when Full Audited is off */}
|
|
|
|
|
|
{!isEditMode && !settings.isFullAudited &&
|
|
|
|
|
|
!columns.some((c) => c.columnName.trim().toLowerCase() === 'id') && (
|
|
|
|
|
|
<div className="px-3 py-2 bg-red-50 dark:bg-red-900/20 border border-red-300 dark:border-red-700 rounded text-xs text-red-700 dark:text-red-300">
|
|
|
|
|
|
⛔ <strong>Full Audited Entity</strong> seçili değil. Geri dönüp{' '}
|
|
|
|
|
|
<code className="bg-red-100 dark:bg-red-900/40 px-1 rounded">Id</code> sütununu{' '}
|
|
|
|
|
|
<code className="bg-red-100 dark:bg-red-900/40 px-1 rounded">uniqueidentifier</code>{' '}
|
|
|
|
|
|
tipinde manuel olarak eklemelisiniz.
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// ── Step 2: Relationships ─────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
const renderRelationships = () => (
|
|
|
|
|
|
<div className="space-y-3">
|
|
|
|
|
|
{/* Loading indicator for edit mode */}
|
|
|
|
|
|
{fksLoading && (
|
|
|
|
|
|
<div className="flex items-center justify-center py-10 text-gray-500 text-sm gap-2">
|
|
|
|
|
|
<span className="animate-spin">◠</span> Mevcut FK kısıtlamaları yükleniyor...
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{!fksLoading && <>
|
|
|
|
|
|
{/* Header */}
|
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
|
<span className="text-xs text-gray-500">
|
|
|
|
|
|
{relationships.length === 0 ? 'İlişki tanımlanmamış' : `${relationships.length} ilişki`}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={openAddFk}
|
|
|
|
|
|
className="flex items-center gap-1 px-3 py-1.5 bg-indigo-600 hover:bg-indigo-700 text-white text-xs rounded-lg transition-colors"
|
|
|
|
|
|
>
|
|
|
|
|
|
<FaPlus className="w-2.5 h-2.5" /> İlişki Ekle
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Empty state */}
|
|
|
|
|
|
{relationships.length === 0 && (
|
|
|
|
|
|
<div className="py-8 text-center border-2 border-dashed border-gray-200 dark:border-gray-700 rounded-xl bg-gray-50 dark:bg-gray-800/50">
|
|
|
|
|
|
<FaLink className="text-3xl mx-auto text-gray-300 dark:text-gray-600 mb-2" />
|
|
|
|
|
|
<p className="text-sm text-gray-500">Bu tablo için henüz ilişki tanımlanmamış</p>
|
|
|
|
|
|
<p className="text-xs text-gray-400 mt-1">Bu adım isteğe bağlıdır, atlayabilirsiniz</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{/* Relationship cards */}
|
|
|
|
|
|
<div className="space-y-2 max-h-72 overflow-y-auto pr-1">
|
|
|
|
|
|
{relationships.map((rel) => (
|
|
|
|
|
|
<div
|
|
|
|
|
|
key={rel.id}
|
|
|
|
|
|
className="border border-slate-200 dark:border-gray-700 rounded-xl px-4 py-3 flex items-start gap-3 bg-white dark:bg-gray-800"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div className="p-2 rounded-lg mt-0.5 flex-shrink-0 bg-indigo-50 dark:bg-indigo-900/20 text-indigo-600 dark:text-indigo-400">
|
|
|
|
|
|
<FaLink className="w-3 h-3" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="flex-1 min-w-0">
|
|
|
|
|
|
<div className="flex items-center gap-2 flex-wrap">
|
|
|
|
|
|
<code className="text-sm font-semibold text-slate-900 dark:text-white">
|
|
|
|
|
|
[{rel.fkColumnName}]
|
|
|
|
|
|
</code>
|
|
|
|
|
|
<FaArrowRight className="w-3 h-3 text-gray-400 flex-shrink-0" />
|
|
|
|
|
|
<code className="text-sm font-semibold text-slate-900 dark:text-white">
|
|
|
|
|
|
[dbo].[{rel.referencedTable}].[{rel.referencedColumn || 'Id'}]
|
|
|
|
|
|
</code>
|
|
|
|
|
|
<span className="text-xs px-2 py-0.5 rounded-full bg-indigo-100 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-300 font-medium">
|
|
|
|
|
|
{REL_TYPES.find((t) => t.value === rel.relationshipType)?.label}
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="flex items-center gap-3 mt-1 text-xs text-gray-500 flex-wrap">
|
|
|
|
|
|
<span>ON DELETE: <strong>{rel.cascadeDelete}</strong></span>
|
|
|
|
|
|
<span>ON UPDATE: <strong>{rel.cascadeUpdate}</strong></span>
|
|
|
|
|
|
{rel.isRequired && (
|
|
|
|
|
|
<span className="text-orange-600 dark:text-orange-400">Zorunlu</span>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{rel.description && (
|
|
|
|
|
|
<span className="text-gray-400 italic">{rel.description}</span>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="flex items-center gap-1 flex-shrink-0">
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => openEditFk(rel)}
|
|
|
|
|
|
title="Düzenle"
|
|
|
|
|
|
className="p-1.5 text-gray-400 hover:text-indigo-600 hover:bg-indigo-50 dark:hover:bg-indigo-900/20 rounded transition-colors"
|
|
|
|
|
|
>
|
|
|
|
|
|
<FaEdit className="w-3.5 h-3.5" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => deleteFk(rel.id)}
|
|
|
|
|
|
title="Sil"
|
|
|
|
|
|
className="p-1.5 text-gray-400 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-900/20 rounded transition-colors"
|
|
|
|
|
|
>
|
|
|
|
|
|
<FaTrash className="w-3.5 h-3.5" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</>}
|
|
|
|
|
|
|
|
|
|
|
|
{/* FK Add/Edit Modal */}
|
|
|
|
|
|
{fkModalOpen && (
|
|
|
|
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm">
|
|
|
|
|
|
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow-2xl w-full max-w-lg flex flex-col">
|
|
|
|
|
|
{/* Modal Header */}
|
|
|
|
|
|
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-700">
|
|
|
|
|
|
<h2 className="text-base font-bold text-gray-900 dark:text-white">
|
|
|
|
|
|
{editingFkId ? 'İlişkiyi Düzenle' : 'Yeni İlişki Ekle'}
|
|
|
|
|
|
</h2>
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => setFkModalOpen(false)}
|
|
|
|
|
|
className="p-2 text-gray-400 hover:text-gray-600 rounded-lg transition-colors"
|
|
|
|
|
|
>
|
|
|
|
|
|
<FaTimes className="w-4 h-4" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Modal Body */}
|
|
|
|
|
|
<div className="flex-1 overflow-y-auto px-6 py-4 space-y-4">
|
|
|
|
|
|
{/* Relationship type */}
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label className="block text-xs font-semibold text-gray-700 dark:text-gray-300 mb-2">
|
|
|
|
|
|
İlişki Tipi
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<div className="flex gap-2">
|
|
|
|
|
|
{REL_TYPES.map((t) => (
|
|
|
|
|
|
<button
|
|
|
|
|
|
key={t.value}
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => setFkForm((f) => ({ ...f, relationshipType: t.value }))}
|
|
|
|
|
|
className={`flex-1 py-2 px-3 rounded-lg border text-sm font-medium transition-all ${
|
|
|
|
|
|
fkForm.relationshipType === t.value
|
|
|
|
|
|
? 'bg-indigo-600 border-indigo-600 text-white'
|
|
|
|
|
|
: 'bg-white dark:bg-gray-700 border-gray-200 dark:border-gray-600 text-gray-600 dark:text-gray-300 hover:border-indigo-300'
|
|
|
|
|
|
}`}
|
|
|
|
|
|
>
|
|
|
|
|
|
{t.label}
|
|
|
|
|
|
<span className="block text-xs opacity-70">{t.desc}</span>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* FK column / Referenced table & column */}
|
|
|
|
|
|
<div className="grid grid-cols-2 gap-3">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label className="block text-xs font-semibold text-gray-700 dark:text-gray-300 mb-1.5">
|
|
|
|
|
|
Bu Tablodaki FK Sütunu *
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<select
|
|
|
|
|
|
value={fkForm.fkColumnName}
|
|
|
|
|
|
onChange={(e) => setFkForm((f) => ({ ...f, fkColumnName: e.target.value }))}
|
|
|
|
|
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg text-sm font-mono dark:bg-gray-700 dark:text-white focus:ring-2 focus:ring-indigo-500"
|
|
|
|
|
|
>
|
|
|
|
|
|
<option value="">— Seçiniz —</option>
|
|
|
|
|
|
{columns
|
|
|
|
|
|
.filter((c) => c.columnName.trim())
|
|
|
|
|
|
.map((c) => (
|
|
|
|
|
|
<option key={c.id} value={c.columnName}>
|
|
|
|
|
|
{c.columnName}
|
|
|
|
|
|
</option>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label className="block text-xs font-semibold text-gray-700 dark:text-gray-300 mb-1.5">
|
|
|
|
|
|
Hedef Tablo *
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<select
|
|
|
|
|
|
value={fkForm.referencedTable}
|
|
|
|
|
|
onChange={(e) => {
|
|
|
|
|
|
const val = e.target.value
|
|
|
|
|
|
setFkForm((f) => ({ ...f, referencedTable: val, referencedColumn: '' }))
|
|
|
|
|
|
loadTargetColumns(val)
|
|
|
|
|
|
}}
|
|
|
|
|
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg text-sm font-mono dark:bg-gray-700 dark:text-white focus:ring-2 focus:ring-indigo-500"
|
|
|
|
|
|
>
|
|
|
|
|
|
<option value="">— Seçiniz —</option>
|
|
|
|
|
|
{dbTables.map((t) => (
|
|
|
|
|
|
<option key={`${t.schemaName}.${t.tableName}`} value={t.tableName}>
|
|
|
|
|
|
[{t.schemaName}].[{t.tableName}]
|
|
|
|
|
|
</option>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label className="block text-xs font-semibold text-gray-700 dark:text-gray-300 mb-1.5">
|
|
|
|
|
|
Hedef Sütun (PK){targetColsLoading ? ' — Yükleniyor...' : ''}
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<select
|
|
|
|
|
|
value={fkForm.referencedColumn}
|
|
|
|
|
|
onChange={(e) => setFkForm((f) => ({ ...f, referencedColumn: e.target.value }))}
|
|
|
|
|
|
disabled={targetColsLoading || targetTableColumns.length === 0}
|
|
|
|
|
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg text-sm font-mono dark:bg-gray-700 dark:text-white focus:ring-2 focus:ring-indigo-500 disabled:opacity-60"
|
|
|
|
|
|
>
|
|
|
|
|
|
<option value="">— Önce hedef tablo seçin —</option>
|
|
|
|
|
|
{targetTableColumns.map((col) => (
|
|
|
|
|
|
<option key={col} value={col}>
|
|
|
|
|
|
{col}
|
|
|
|
|
|
</option>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Cascade */}
|
|
|
|
|
|
<div className="grid grid-cols-2 gap-3">
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label className="block text-xs font-semibold text-gray-700 dark:text-gray-300 mb-1.5">
|
|
|
|
|
|
Cascade Delete
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<select
|
|
|
|
|
|
value={fkForm.cascadeDelete}
|
|
|
|
|
|
onChange={(e) =>
|
|
|
|
|
|
setFkForm((f) => ({ ...f, cascadeDelete: e.target.value as CascadeBehavior }))
|
|
|
|
|
|
}
|
|
|
|
|
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg text-sm dark:bg-gray-700 dark:text-white focus:ring-2 focus:ring-indigo-500"
|
|
|
|
|
|
>
|
|
|
|
|
|
{CASCADE_OPTIONS.map((o) => (
|
|
|
|
|
|
<option key={o.value} value={o.value}>{o.label}</option>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label className="block text-xs font-semibold text-gray-700 dark:text-gray-300 mb-1.5">
|
|
|
|
|
|
Cascade Update
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<select
|
|
|
|
|
|
value={fkForm.cascadeUpdate}
|
|
|
|
|
|
onChange={(e) =>
|
|
|
|
|
|
setFkForm((f) => ({ ...f, cascadeUpdate: e.target.value as CascadeBehavior }))
|
|
|
|
|
|
}
|
|
|
|
|
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg text-sm dark:bg-gray-700 dark:text-white focus:ring-2 focus:ring-indigo-500"
|
|
|
|
|
|
>
|
|
|
|
|
|
{CASCADE_OPTIONS.map((o) => (
|
|
|
|
|
|
<option key={o.value} value={o.value}>{o.label}</option>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Options */}
|
|
|
|
|
|
<div className="flex items-center gap-6">
|
|
|
|
|
|
<label className="flex items-center gap-2 cursor-pointer">
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="checkbox"
|
|
|
|
|
|
checked={fkForm.isRequired}
|
|
|
|
|
|
onChange={(e) => setFkForm((f) => ({ ...f, isRequired: e.target.checked }))}
|
|
|
|
|
|
className="w-4 h-4 text-indigo-600 rounded"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<span className="text-sm text-gray-700 dark:text-gray-300">Zorunlu</span>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Description */}
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label className="block text-xs font-semibold text-gray-700 dark:text-gray-300 mb-1.5">
|
|
|
|
|
|
Açıklama
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<textarea
|
|
|
|
|
|
value={fkForm.description}
|
|
|
|
|
|
onChange={(e) => setFkForm((f) => ({ ...f, description: e.target.value }))}
|
|
|
|
|
|
rows={2}
|
|
|
|
|
|
placeholder="İsteğe bağlı açıklama..."
|
|
|
|
|
|
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg text-sm dark:bg-gray-700 dark:text-white focus:ring-2 focus:ring-indigo-500 resize-none"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Modal Footer */}
|
|
|
|
|
|
<div className="flex items-center justify-end gap-2 px-6 py-4 border-t border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800/50 rounded-b-2xl">
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={() => setFkModalOpen(false)}
|
|
|
|
|
|
className="px-4 py-2 text-sm text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
|
|
|
|
|
>
|
|
|
|
|
|
İptal
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button
|
|
|
|
|
|
onClick={saveFk}
|
|
|
|
|
|
disabled={!fkForm.fkColumnName.trim() || !fkForm.referencedTable.trim()}
|
|
|
|
|
|
className="px-4 py-2 text-sm bg-indigo-600 hover:bg-indigo-700 disabled:opacity-50 text-white rounded-lg transition-colors"
|
|
|
|
|
|
>
|
|
|
|
|
|
Kaydet
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// ── Step 3: T-SQL Preview ──────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
const renderSqlPreview = () => (
|
|
|
|
|
|
<div className="flex flex-col gap-3">
|
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
|
<p className="text-sm text-gray-600 dark:text-gray-400">
|
|
|
|
|
|
Olusturulan T-SQL kodu. <strong>Deploy Et</strong> ile veritabanina uygulayin.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<button
|
|
|
|
|
|
className="text-xs text-blue-500 hover:underline"
|
|
|
|
|
|
onClick={() => navigator.clipboard.writeText(generatedSql)}
|
|
|
|
|
|
>
|
|
|
|
|
|
Kopyala
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<pre className="bg-gray-900 text-green-300 rounded-lg p-4 text-xs overflow-auto max-h-96 font-mono leading-relaxed whitespace-pre">
|
|
|
|
|
|
{generatedSql}
|
|
|
|
|
|
</pre>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// ── Render ─────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<Dialog isOpen={isOpen} onClose={handleClose} onRequestClose={handleClose} width={900}>
|
|
|
|
|
|
<div className="flex flex-col gap-2">
|
|
|
|
|
|
{/* Header */}
|
|
|
|
|
|
<div className="flex items-center gap-3 border-b pb-3">
|
|
|
|
|
|
<FaTable className="text-2xl text-blue-500" />
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<h5 className="font-bold">
|
|
|
|
|
|
{isEditMode
|
|
|
|
|
|
? `Tablo Düzenle — ${initialTableData?.tableName}`
|
|
|
|
|
|
: 'Tablo Tasarımcısı'}
|
|
|
|
|
|
</h5>
|
|
|
|
|
|
<p className="text-xs text-gray-500">
|
|
|
|
|
|
{isEditMode
|
|
|
|
|
|
? 'Mevcut sütunları düzenleyin, yeni ekleyin veya ilişki tanımlayın — ALTER TABLE SQL üretilir'
|
|
|
|
|
|
: 'Sütunları tanımlayın, ayarları yapın ve T-SQL ile deploy edin'}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Steps */}
|
|
|
|
|
|
{renderStepIndicator()}
|
|
|
|
|
|
|
|
|
|
|
|
{/* Content */}
|
|
|
|
|
|
<div className="min-h-[420px]">
|
|
|
|
|
|
{step === 0 && renderColumnDesigner()}
|
|
|
|
|
|
{step === 1 && renderEntitySettings()}
|
|
|
|
|
|
{step === 2 && renderRelationships()}
|
|
|
|
|
|
{step === 3 && renderSqlPreview()}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
{/* Footer */}
|
|
|
|
|
|
<div className="flex justify-between items-center border-t pt-3 mt-1">
|
|
|
|
|
|
<Button variant="plain" onClick={handleClose}>
|
|
|
|
|
|
Iptal
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
|
{step > 0 && (
|
|
|
|
|
|
<Button variant="default" onClick={handleBack}>
|
|
|
|
|
|
Geri
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{step < 3 ? (
|
|
|
|
|
|
<Button variant="solid" color="blue-600" onClick={handleNext} disabled={!canGoNext()}>
|
|
|
|
|
|
İleri
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<Button
|
|
|
|
|
|
variant="solid"
|
|
|
|
|
|
color="green-600"
|
|
|
|
|
|
icon={<FaCloudUploadAlt />}
|
|
|
|
|
|
onClick={handleDeploy}
|
|
|
|
|
|
loading={isDeploying}
|
|
|
|
|
|
disabled={!dataSource || isDeploying || (isEditMode && generatedSql.includes('Henüz değişiklik yapılmadı'))}
|
|
|
|
|
|
>
|
|
|
|
|
|
Deploy Et
|
|
|
|
|
|
</Button>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</Dialog>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-01 20:43:25 +00:00
|
|
|
|
export default SqlTableDesignerDialog
|