Sql Query Manager Index ve PrimaryKey
This commit is contained in:
parent
f03612b619
commit
dbd3e9f32a
2 changed files with 691 additions and 23 deletions
|
|
@ -16430,7 +16430,96 @@
|
|||
"en": "Select key column",
|
||||
"tr": "Anahtar sütunu seç"
|
||||
},
|
||||
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.SqlQueryManager.IndexKeys",
|
||||
"en": "Index Keys",
|
||||
"tr": "Dizin Anahtarları"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.SqlQueryManager.NoIndexesDefined",
|
||||
"en": "No indexes defined",
|
||||
"tr": "Dizin tanımlanmamış"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.SqlQueryManager.IndexKey",
|
||||
"en": "Index Key",
|
||||
"tr": "Dizin Anahtarı"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.SqlQueryManager.AddIndexKey",
|
||||
"en": "Add Index Key",
|
||||
"tr": "Dizin Anahtarı Ekle"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.SqlQueryManager.EditIndexKey",
|
||||
"en": "Edit Index Key",
|
||||
"tr": "Dizin Anahtarı Düzenle"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.SqlQueryManager.AddNewIndexKey",
|
||||
"en": "Add New Index Key",
|
||||
"tr": "Yeni Dizin Anahtarı Ekle"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.SqlQueryManager.IndexKeyType",
|
||||
"en": "Index Key Type",
|
||||
"tr": "Dizin Anahtarı Türü"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.SqlQueryManager.IndexConstraintName",
|
||||
"en": "Index Constraint Name",
|
||||
"tr": "Dizin Kısıtlaması Adı"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.SqlQueryManager.Columns",
|
||||
"en": "Columns",
|
||||
"tr": "Sütunlar"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.SqlQueryManager.Column",
|
||||
"en": "Column",
|
||||
"tr": "Sütun"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.SqlQueryManager.SortOrder",
|
||||
"en": "Sort Order",
|
||||
"tr": "Sıralama Düzeni"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.SqlQueryManager.Selected",
|
||||
"en": "Selected",
|
||||
"tr": "Seçili"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.SqlQueryManager.IndexType_PrimaryKey_Desc",
|
||||
"en": "Primary Key",
|
||||
"tr": "Birincil anahtar"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.SqlQueryManager.IndexType_UniqueKey_Desc",
|
||||
"en": "Unique Key",
|
||||
"tr": "Benzersiz anahtar"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.SqlQueryManager.IndexType_Index_Desc",
|
||||
"en": "Index",
|
||||
"tr": "Dizin"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.SqlQueryManager.ColumnDesign",
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import {
|
|||
FaArrowRight,
|
||||
FaChevronDown,
|
||||
FaChevronRight,
|
||||
FaKey,
|
||||
} from 'react-icons/fa'
|
||||
import { sqlObjectManagerService } from '@/services/sql-query-manager.service'
|
||||
import { getMenus } from '@/services/menu.service'
|
||||
|
|
@ -69,6 +70,22 @@ interface TableDesignerDialogProps {
|
|||
initialTableData?: { schemaName: string; tableName: string } | null
|
||||
}
|
||||
|
||||
type IndexType = 'PrimaryKey' | 'UniqueKey' | 'Index'
|
||||
|
||||
interface IndexColumnEntry {
|
||||
columnName: string
|
||||
order: 'ASC' | 'DESC'
|
||||
}
|
||||
|
||||
interface TableIndex {
|
||||
id: string
|
||||
indexName: string
|
||||
indexType: IndexType
|
||||
isClustered: boolean
|
||||
columns: IndexColumnEntry[]
|
||||
description: string
|
||||
}
|
||||
|
||||
// ─── Constants ────────────────────────────────────────────────────────────────
|
||||
|
||||
const DATA_TYPES: { value: SqlDataType; label: string }[] = [
|
||||
|
|
@ -183,6 +200,20 @@ const EMPTY_FK: Omit<SqlTableRelation, 'id'> = {
|
|||
description: '',
|
||||
}
|
||||
|
||||
const EMPTY_INDEX: Omit<TableIndex, 'id'> = {
|
||||
indexName: '',
|
||||
indexType: 'Index',
|
||||
isClustered: false,
|
||||
columns: [],
|
||||
description: '',
|
||||
}
|
||||
|
||||
const INDEX_TYPES: { value: IndexType; label: string; desc: string }[] = [
|
||||
{ value: 'PrimaryKey', label: 'Primary Key', desc: 'App.SqlQueryManager.IndexType_PrimaryKey_Desc' },
|
||||
{ value: 'UniqueKey', label: 'Unique Key', desc: 'App.SqlQueryManager.IndexType_UniqueKey_Desc' },
|
||||
{ value: 'Index', label: 'Index', desc: 'App.SqlQueryManager.IndexType_Index_Desc' },
|
||||
]
|
||||
|
||||
const TENANT_COLUMN: ColumnDefinition = {
|
||||
id: '__TenantId',
|
||||
columnName: 'TenantId',
|
||||
|
|
@ -219,6 +250,7 @@ function generateCreateTableSql(
|
|||
columns: ColumnDefinition[],
|
||||
settings: TableSettings,
|
||||
relationships: SqlTableRelation[],
|
||||
indexes: TableIndex[],
|
||||
): string {
|
||||
const tableName = settings.tableName || 'NewTable'
|
||||
const fullTableName = `[dbo].[${tableName}]`
|
||||
|
|
@ -264,6 +296,29 @@ function generateCreateTableSql(
|
|||
fkLines.push(` ON UPDATE ${cascadeUpdate};`)
|
||||
}
|
||||
|
||||
// Index / Key SQL lines
|
||||
const indexLines: string[] = []
|
||||
for (const idx of indexes) {
|
||||
if (idx.columns.length === 0) continue
|
||||
const clustered = idx.isClustered ? 'CLUSTERED' : 'NONCLUSTERED'
|
||||
const colsSql = idx.columns.map((c) => `[${c.columnName}] ${c.order}`).join(', ')
|
||||
indexLines.push('')
|
||||
if (idx.indexType === 'PrimaryKey') {
|
||||
indexLines.push(`-- 🔑 Primary Key: [${idx.indexName}]`)
|
||||
indexLines.push(`ALTER TABLE ${fullTableName}`)
|
||||
indexLines.push(` ADD CONSTRAINT [${idx.indexName}] PRIMARY KEY ${clustered} (${colsSql});`)
|
||||
} else if (idx.indexType === 'UniqueKey') {
|
||||
indexLines.push(`-- 🔒 Unique Key: [${idx.indexName}]`)
|
||||
indexLines.push(`ALTER TABLE ${fullTableName}`)
|
||||
indexLines.push(` ADD CONSTRAINT [${idx.indexName}] UNIQUE ${clustered} (${colsSql});`)
|
||||
} else {
|
||||
indexLines.push(`-- 📋 Index: [${idx.indexName}]`)
|
||||
indexLines.push(
|
||||
`CREATE ${idx.isClustered ? 'CLUSTERED ' : ''}INDEX [${idx.indexName}] ON ${fullTableName} (${colsSql});`,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const lines: string[] = [
|
||||
`/* ── Table: ${fullTableName} ── */`,
|
||||
...(settings.entityName ? [`/* Entity Name: ${settings.entityName} */`] : []),
|
||||
|
|
@ -274,6 +329,7 @@ function generateCreateTableSql(
|
|||
...bodyLines,
|
||||
`);`,
|
||||
...fkLines,
|
||||
...indexLines,
|
||||
'',
|
||||
`/* Verify: SELECT TOP 10 * FROM ${fullTableName}; */`,
|
||||
]
|
||||
|
|
@ -335,6 +391,8 @@ function generateAlterTableSql(
|
|||
tableName: string,
|
||||
relationships: SqlTableRelation[],
|
||||
originalRelationships: SqlTableRelation[],
|
||||
indexes: TableIndex[],
|
||||
originalIndexes: TableIndex[],
|
||||
): string {
|
||||
const fullTableName = `[dbo].[${tableName}]`
|
||||
const lines: string[] = [`/* ── ALTER TABLE: ${fullTableName} ── */`, '']
|
||||
|
|
@ -474,17 +532,86 @@ function generateAlterTableSql(
|
|||
}
|
||||
})
|
||||
|
||||
// 🔑 Index / Key Diff
|
||||
const origIdxById = new Map<string, TableIndex>()
|
||||
originalIndexes.forEach((ix) => origIdxById.set(ix.id, ix))
|
||||
const curIdxById = new Map<string, TableIndex>()
|
||||
indexes.forEach((ix) => curIdxById.set(ix.id, ix))
|
||||
|
||||
const dropIndexSql = (ix: TableIndex) => {
|
||||
if (ix.indexType === 'PrimaryKey' || ix.indexType === 'UniqueKey') {
|
||||
lines.push(`-- ❌ Kaldır: [${ix.indexName}]`)
|
||||
lines.push(`ALTER TABLE ${fullTableName}`)
|
||||
lines.push(` DROP CONSTRAINT [${ix.indexName}];`)
|
||||
} else {
|
||||
lines.push(`-- ❌ Kaldır: [${ix.indexName}]`)
|
||||
lines.push(`DROP INDEX [${ix.indexName}] ON ${fullTableName};`)
|
||||
}
|
||||
lines.push('')
|
||||
}
|
||||
|
||||
const addIndexSql = (ix: TableIndex) => {
|
||||
if (ix.columns.length === 0) return
|
||||
const clustered = ix.isClustered ? 'CLUSTERED' : 'NONCLUSTERED'
|
||||
const colsSql = ix.columns.map((c) => `[${c.columnName}] ${c.order}`).join(', ')
|
||||
if (ix.indexType === 'PrimaryKey') {
|
||||
lines.push(`-- 🔑 Primary Key: [${ix.indexName}]`)
|
||||
lines.push(`ALTER TABLE ${fullTableName}`)
|
||||
lines.push(` ADD CONSTRAINT [${ix.indexName}] PRIMARY KEY ${clustered} (${colsSql});`)
|
||||
} else if (ix.indexType === 'UniqueKey') {
|
||||
lines.push(`-- 🔒 Unique Key: [${ix.indexName}]`)
|
||||
lines.push(`ALTER TABLE ${fullTableName}`)
|
||||
lines.push(` ADD CONSTRAINT [${ix.indexName}] UNIQUE ${clustered} (${colsSql});`)
|
||||
} else {
|
||||
lines.push(`-- 📋 Index: [${ix.indexName}]`)
|
||||
lines.push(
|
||||
`CREATE ${ix.isClustered ? 'CLUSTERED ' : ''}INDEX [${ix.indexName}] ON ${fullTableName} (${colsSql});`,
|
||||
)
|
||||
}
|
||||
lines.push('')
|
||||
}
|
||||
|
||||
// Removed indexes
|
||||
originalIndexes.forEach((orig) => {
|
||||
if (!curIdxById.has(orig.id)) {
|
||||
hasChanges = true
|
||||
dropIndexSql(orig)
|
||||
}
|
||||
})
|
||||
|
||||
// New and modified indexes
|
||||
indexes.forEach((cur) => {
|
||||
if (cur.columns.length === 0) return
|
||||
const orig = origIdxById.get(cur.id)
|
||||
if (!orig) {
|
||||
hasChanges = true
|
||||
addIndexSql(cur)
|
||||
} else {
|
||||
const changed =
|
||||
orig.indexName !== cur.indexName ||
|
||||
orig.indexType !== cur.indexType ||
|
||||
orig.isClustered !== cur.isClustered ||
|
||||
JSON.stringify(orig.columns) !== JSON.stringify(cur.columns)
|
||||
if (changed) {
|
||||
hasChanges = true
|
||||
lines.push(`-- ✏️ Index Güncelle (drop + re-create): [${orig.indexName}]`)
|
||||
dropIndexSql(orig)
|
||||
addIndexSql(cur)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (!hasChanges) {
|
||||
lines.push(
|
||||
'/* ℹ️ Henüz değişiklik yapılmadı. Sütunları ekleyin/silin/düzeyin ya da ilişki ekleyin. */',
|
||||
'/* ℹ️ Henüz değişiklik yapılmadı. Sütunları ekleyin/silin/düzeyin ya da ilişki/index ekleyin. */',
|
||||
)
|
||||
}
|
||||
|
||||
return lines.join('\n')
|
||||
}
|
||||
|
||||
const STEPS = ['Sütun Tasarımı', 'Entity Ayarları', 'İlişkiler', 'T-SQL Önizleme'] as const
|
||||
type Step = 0 | 1 | 2 | 3
|
||||
const STEPS = ['Sütun Tasarımı', 'Entity Ayarları', 'İlişkiler', 'Index / Key', 'T-SQL Önizleme'] as const
|
||||
type Step = 0 | 1 | 2 | 3 | 4
|
||||
|
||||
// ─── Simple Menu Tree (read-only selection) ───────────────────────────────────
|
||||
|
||||
|
|
@ -668,6 +795,12 @@ const SqlTableDesignerDialog = ({
|
|||
const [dbTables, setDbTables] = useState<{ schemaName: string; tableName: string }[]>([])
|
||||
const [targetTableColumns, setTargetTableColumns] = useState<string[]>([])
|
||||
const [targetColsLoading, setTargetColsLoading] = useState(false)
|
||||
const [indexes, setIndexes] = useState<TableIndex[]>([])
|
||||
const [originalIndexes, setOriginalIndexes] = useState<TableIndex[]>([])
|
||||
const [indexesLoading, setIndexesLoading] = useState(false)
|
||||
const [indexModalOpen, setIndexModalOpen] = useState(false)
|
||||
const [editingIndexId, setEditingIndexId] = useState<string | null>(null)
|
||||
const [indexForm, setIndexForm] = useState<Omit<TableIndex, 'id'>>(EMPTY_INDEX)
|
||||
|
||||
const reloadMenus = (onLoaded?: (items: MenuItem[]) => void) => {
|
||||
setMenuLoading(true)
|
||||
|
|
@ -775,6 +908,56 @@ const SqlTableDesignerDialog = ({
|
|||
})
|
||||
.catch(() => {})
|
||||
.finally(() => setFksLoading(false))
|
||||
|
||||
// Load existing indexes / keys
|
||||
const idxQuery = [
|
||||
'SELECT',
|
||||
' i.name AS indexName,',
|
||||
' CASE',
|
||||
" WHEN i.is_primary_key = 1 THEN 'PrimaryKey'",
|
||||
" WHEN i.is_unique_constraint = 1 THEN 'UniqueKey'",
|
||||
" ELSE 'Index'",
|
||||
' END AS indexType,',
|
||||
' CAST(CASE WHEN i.type = 1 THEN 1 ELSE 0 END AS BIT) AS isClustered,',
|
||||
' col.name AS columnName,',
|
||||
' ic.is_descending_key AS isDescending,',
|
||||
' ic.key_ordinal AS keyOrdinal',
|
||||
'FROM sys.indexes i',
|
||||
'INNER JOIN sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id',
|
||||
'INNER JOIN sys.columns col ON ic.object_id = col.object_id AND ic.column_id = col.column_id',
|
||||
`WHERE i.object_id = OBJECT_ID('${initialTableData.schemaName}.${initialTableData.tableName}')`,
|
||||
' AND ic.is_included_column = 0',
|
||||
'ORDER BY i.name, ic.key_ordinal',
|
||||
].join('\n')
|
||||
|
||||
setIndexesLoading(true)
|
||||
sqlObjectManagerService
|
||||
.executeQuery({ queryText: idxQuery, dataSourceCode: dataSource })
|
||||
.then((res) => {
|
||||
const rows: any[] = res.data?.data ?? []
|
||||
const idxMap = new Map<string, TableIndex>()
|
||||
rows.forEach((row) => {
|
||||
if (!idxMap.has(row.indexName)) {
|
||||
idxMap.set(row.indexName, {
|
||||
id: crypto.randomUUID(),
|
||||
indexName: row.indexName,
|
||||
indexType: row.indexType as IndexType,
|
||||
isClustered: row.isClustered === true || row.isClustered === 1,
|
||||
columns: [],
|
||||
description: '',
|
||||
})
|
||||
}
|
||||
idxMap.get(row.indexName)!.columns.push({
|
||||
columnName: row.columnName,
|
||||
order: row.isDescending ? 'DESC' : 'ASC',
|
||||
})
|
||||
})
|
||||
const idxDefs = Array.from(idxMap.values())
|
||||
setIndexes(idxDefs)
|
||||
setOriginalIndexes(idxDefs)
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => setIndexesLoading(false))
|
||||
}
|
||||
}, [isOpen, dataSource, initialTableData])
|
||||
|
||||
|
|
@ -787,8 +970,10 @@ const SqlTableDesignerDialog = ({
|
|||
settings.tableName || initialTableData?.tableName || '',
|
||||
relationships,
|
||||
originalRelationships,
|
||||
indexes,
|
||||
originalIndexes,
|
||||
)
|
||||
: generateCreateTableSql(columns, settings, relationships),
|
||||
: generateCreateTableSql(columns, settings, relationships, indexes),
|
||||
[
|
||||
isEditMode,
|
||||
originalColumns,
|
||||
|
|
@ -797,6 +982,8 @@ const SqlTableDesignerDialog = ({
|
|||
initialTableData,
|
||||
relationships,
|
||||
originalRelationships,
|
||||
indexes,
|
||||
originalIndexes,
|
||||
],
|
||||
)
|
||||
|
||||
|
|
@ -813,6 +1000,22 @@ const SqlTableDesignerDialog = ({
|
|||
const nonEmpty = prev.filter((c) => c.columnName.trim() !== '')
|
||||
return [...nonEmpty, ...toAdd.map((c) => ({ ...c })), createEmptyColumn()]
|
||||
})
|
||||
// Auto-add PK for Id column if not already defined
|
||||
const hasPk = indexes.some((ix) => ix.indexType === 'PrimaryKey')
|
||||
if (!hasPk) {
|
||||
const tblName = settings.tableName || initialTableData?.tableName || 'Table'
|
||||
setIndexes((prev) => [
|
||||
...prev,
|
||||
{
|
||||
id: `auto-pk-${crypto.randomUUID()}`,
|
||||
indexName: `PK_${tblName}`,
|
||||
indexType: 'PrimaryKey',
|
||||
isClustered: false,
|
||||
columns: [{ columnName: 'Id', order: 'ASC' }],
|
||||
description: 'Primary key',
|
||||
},
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
const addMultiTenantColumns = () => {
|
||||
|
|
@ -851,28 +1054,43 @@ const SqlTableDesignerDialog = ({
|
|||
const buildTableName = (prefix: string, entity: string) =>
|
||||
prefix && entity ? `${prefix}_D_${entity}` : ''
|
||||
|
||||
const syncAutoPkName = (newTableName: string) => {
|
||||
if (!newTableName) return
|
||||
setIndexes((prev) =>
|
||||
prev.map((ix) =>
|
||||
ix.indexType === 'PrimaryKey' && ix.id.startsWith('auto-pk')
|
||||
? { ...ix, indexName: `PK_${newTableName}` }
|
||||
: ix,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
const onMenuCodeSelect = (code: string) => {
|
||||
if (isEditMode) return
|
||||
const item = rawMenuItems.find((m) => m.code === code)
|
||||
const prefix = item?.shortName ?? ''
|
||||
setSelectedMenuCode(code)
|
||||
const newTableName = buildTableName(prefix, settings.entityName)
|
||||
setSettings((s) => ({
|
||||
...s,
|
||||
menuValue: prefix,
|
||||
menuPrefix: prefix,
|
||||
tableName: buildTableName(prefix, s.entityName),
|
||||
tableName: newTableName,
|
||||
}))
|
||||
syncAutoPkName(newTableName)
|
||||
}
|
||||
|
||||
// Strip spaces and special chars — only alphanumeric + underscore allowed
|
||||
const onEntityNameChange = (value: string) => {
|
||||
const sanitized = value.replace(/[^A-Za-z0-9_]/g, '')
|
||||
const newTableName = buildTableName(settings.menuPrefix, sanitized)
|
||||
setSettings((s) => ({
|
||||
...s,
|
||||
entityName: sanitized,
|
||||
tableName: buildTableName(s.menuPrefix, sanitized),
|
||||
tableName: newTableName,
|
||||
displayName: sanitized, // always mirrors entity name
|
||||
}))
|
||||
syncAutoPkName(newTableName)
|
||||
}
|
||||
|
||||
// ── FK Relationship handlers ───────────────────────────────────────────────
|
||||
|
|
@ -923,6 +1141,48 @@ const SqlTableDesignerDialog = ({
|
|||
setRelationships((prev) => prev.filter((r) => r.id !== id))
|
||||
}
|
||||
|
||||
// ── Index / Key handlers ─────────────────────────────────────────────────
|
||||
|
||||
const buildIndexName = (type: IndexType, cols: IndexColumnEntry[]): string => {
|
||||
const prefix = type === 'PrimaryKey' ? 'PK' : type === 'UniqueKey' ? 'UQ' : 'IX'
|
||||
const entityName = settings.entityName || initialTableData?.tableName || ''
|
||||
const colPart = cols.map((c) => c.columnName).join('_')
|
||||
if (entityName && colPart) return `${prefix}_${entityName}_${colPart}`
|
||||
if (entityName) return `${prefix}_${entityName}`
|
||||
if (colPart) return `${prefix}_${colPart}`
|
||||
return prefix
|
||||
}
|
||||
|
||||
const openAddIndex = () => {
|
||||
setEditingIndexId(null)
|
||||
const entityName = settings.entityName || initialTableData?.tableName || ''
|
||||
setIndexForm({ ...EMPTY_INDEX, indexName: entityName ? `IX_${entityName}` : '' })
|
||||
setIndexModalOpen(true)
|
||||
}
|
||||
|
||||
const openEditIndex = (idx: TableIndex) => {
|
||||
setEditingIndexId(idx.id)
|
||||
const { id: _id, ...rest } = idx
|
||||
setIndexForm(rest)
|
||||
setIndexModalOpen(true)
|
||||
}
|
||||
|
||||
const saveIndex = () => {
|
||||
if (!indexForm.indexName.trim() || indexForm.columns.length === 0) return
|
||||
if (editingIndexId) {
|
||||
setIndexes((prev) =>
|
||||
prev.map((ix) => (ix.id === editingIndexId ? { ...indexForm, id: editingIndexId } : ix)),
|
||||
)
|
||||
} else {
|
||||
setIndexes((prev) => [...prev, { ...indexForm, id: crypto.randomUUID() }])
|
||||
}
|
||||
setIndexModalOpen(false)
|
||||
}
|
||||
|
||||
const deleteIndex = (id: string) => {
|
||||
setIndexes((prev) => prev.filter((ix) => ix.id !== id))
|
||||
}
|
||||
|
||||
// ── Navigation ─────────────────────────────────────────────────────────────
|
||||
|
||||
// Compute duplicate column names (lowercased)
|
||||
|
|
@ -947,7 +1207,7 @@ const SqlTableDesignerDialog = ({
|
|||
}
|
||||
|
||||
const handleNext = () => {
|
||||
if (step < 3) setStep((s) => (s + 1) as Step)
|
||||
if (step < 4) setStep((s) => (s + 1) as Step)
|
||||
}
|
||||
const handleBack = () => {
|
||||
if (step > 0) setStep((s) => (s - 1) as Step)
|
||||
|
|
@ -1009,6 +1269,8 @@ const SqlTableDesignerDialog = ({
|
|||
setSettings(DEFAULT_SETTINGS)
|
||||
setRelationships([])
|
||||
setOriginalRelationships([])
|
||||
setIndexes([])
|
||||
setOriginalIndexes([])
|
||||
setDbTables([])
|
||||
setTargetTableColumns([])
|
||||
setSelectedMenuCode('')
|
||||
|
|
@ -1022,6 +1284,7 @@ const SqlTableDesignerDialog = ({
|
|||
translate('::App.SqlQueryManager.ColumnDesign'),
|
||||
translate('::App.SqlQueryManager.EntitySettings'),
|
||||
translate('::App.SqlQueryManager.Relationships'),
|
||||
translate('::App.SqlQueryManager.IndexKeys'),
|
||||
translate('::App.SqlQueryManager.TSqlPreview'),
|
||||
]
|
||||
|
||||
|
|
@ -1099,14 +1362,13 @@ const SqlTableDesignerDialog = ({
|
|||
|
||||
{/* Header row */}
|
||||
<div className="grid grid-cols-12 gap-1 px-1 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">{translate('::App.SqlQueryManager.ColumnName')}</div>
|
||||
<div className="col-span-4">{translate('::App.SqlQueryManager.ColumnName')}</div>
|
||||
<div className="col-span-3">{translate('::App.SqlQueryManager.DataType')}</div>
|
||||
<div className="col-span-1 text-center">{translate('::App.SqlQueryManager.Max')}</div>
|
||||
<div className="col-span-1 text-center">
|
||||
{translate('::App.SqlQueryManager.Nullable')}
|
||||
</div>
|
||||
<div className="col-span-2">{translate('::App.SqlQueryManager.DefaultValue')}</div>
|
||||
<div className="col-span-1">{translate('::App.SqlQueryManager.Note')}</div>
|
||||
<div className="col-span-1 text-center">
|
||||
{translate('::App.SqlQueryManager.Actions')}
|
||||
</div>
|
||||
|
|
@ -1161,7 +1423,7 @@ const SqlTableDesignerDialog = ({
|
|||
key={col.id}
|
||||
className={`grid grid-cols-12 gap-2 bg-white dark:bg-gray-800 rounded items-center ${rowBg}`}
|
||||
>
|
||||
<div className="col-span-3">
|
||||
<div className="col-span-4">
|
||||
<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 ${
|
||||
|
|
@ -1215,15 +1477,6 @@ const SqlTableDesignerDialog = ({
|
|||
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={translate('::App.SqlQueryManager.Note')}
|
||||
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')}
|
||||
|
|
@ -1683,7 +1936,332 @@ const SqlTableDesignerDialog = ({
|
|||
</div>
|
||||
)
|
||||
|
||||
// ── Step 3: T-SQL Preview ──────────────────────────────────────────────────
|
||||
// ── Step 3: Index / Key ────────────────────────────────────────────────────
|
||||
|
||||
const INDEX_TYPE_STYLES: Record<IndexType, { icon: string; badge: string }> = {
|
||||
PrimaryKey: {
|
||||
icon: 'bg-yellow-50 dark:bg-yellow-900/20 text-yellow-600 dark:text-yellow-400',
|
||||
badge: 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-700 dark:text-yellow-300',
|
||||
},
|
||||
UniqueKey: {
|
||||
icon: 'bg-green-50 dark:bg-green-900/20 text-green-600 dark:text-green-400',
|
||||
badge: 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300',
|
||||
},
|
||||
Index: {
|
||||
icon: 'bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400',
|
||||
badge: 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300',
|
||||
},
|
||||
}
|
||||
|
||||
const renderIndexes = () => (
|
||||
<div className="space-y-3">
|
||||
{indexesLoading && (
|
||||
<div className="flex items-center justify-center py-10 text-gray-500 text-sm gap-2">
|
||||
<span className="animate-spin">◠</span> {translate('::App.SqlQueryManager.Loading')}
|
||||
</div>
|
||||
)}
|
||||
{!indexesLoading && (
|
||||
<>
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs text-gray-500">
|
||||
{indexes.length === 0
|
||||
? translate('::App.SqlQueryManager.NoIndexesDefined')
|
||||
: `${indexes.length} ${translate('::App.SqlQueryManager.IndexKey')}`}
|
||||
</span>
|
||||
<button
|
||||
onClick={openAddIndex}
|
||||
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" /> {translate('::App.SqlQueryManager.AddIndexKey')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Empty state */}
|
||||
{indexes.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">
|
||||
<FaKey className="text-3xl mx-auto text-gray-300 dark:text-gray-600 mb-2" />
|
||||
<p className="text-sm text-gray-500">{translate('::App.SqlQueryManager.NoIndexesDefined')}</p>
|
||||
<p className="text-xs text-gray-400 mt-1">{translate('::App.SqlQueryManager.StepIsOptional')}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Index cards */}
|
||||
<div className="space-y-2 max-h-72 overflow-y-auto pr-1">
|
||||
{indexes.map((idx) => {
|
||||
const styles = INDEX_TYPE_STYLES[idx.indexType]
|
||||
return (
|
||||
<div
|
||||
key={idx.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 ${styles.icon}`}>
|
||||
<FaKey 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">
|
||||
[{idx.indexName}]
|
||||
</code>
|
||||
<span
|
||||
className={`text-xs px-2 py-0.5 rounded-full font-medium ${styles.badge}`}
|
||||
>
|
||||
{idx.indexType}
|
||||
</span>
|
||||
<span className="text-xs text-gray-500">
|
||||
{idx.isClustered ? 'CLUSTERED' : 'NONCLUSTERED'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 mt-1 flex-wrap">
|
||||
{idx.columns.map((col, ci) => (
|
||||
<span
|
||||
key={ci}
|
||||
className="text-xs bg-gray-100 dark:bg-gray-700 px-1.5 py-0.5 rounded font-mono"
|
||||
>
|
||||
{col.columnName} {col.order}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
{idx.description && (
|
||||
<p className="text-xs text-gray-400 mt-1 italic">{idx.description}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1 flex-shrink-0">
|
||||
<button
|
||||
onClick={() => openEditIndex(idx)}
|
||||
title={translate('::App.SqlQueryManager.Edit')}
|
||||
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={() => deleteIndex(idx.id)}
|
||||
title={translate('::App.SqlQueryManager.Delete')}
|
||||
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>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Index Add/Edit Modal */}
|
||||
{indexModalOpen &&
|
||||
createPortal(
|
||||
<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 max-h-[90vh]">
|
||||
{/* 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">
|
||||
{editingIndexId ? translate('::App.SqlQueryManager.EditIndexKey') : translate('::App.SqlQueryManager.AddNewIndexKey')}
|
||||
</h2>
|
||||
<button
|
||||
onClick={() => setIndexModalOpen(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">
|
||||
{/* Index Type */}
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-gray-700 dark:text-gray-300 mb-2">
|
||||
{translate('::App.SqlQueryManager.IndexKeyType')}
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
{INDEX_TYPES.map((t) => (
|
||||
<button
|
||||
key={t.value}
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setIndexForm((f) => ({
|
||||
...f,
|
||||
indexType: t.value,
|
||||
indexName: buildIndexName(t.value, f.columns),
|
||||
}))
|
||||
}
|
||||
className={`flex-1 py-2 px-3 rounded-lg border text-sm font-medium transition-all ${
|
||||
indexForm.indexType === 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">{translate('::' + t.desc)}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Index / Constraint Name */}
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-gray-700 dark:text-gray-300 mb-1.5">
|
||||
{translate('::App.SqlQueryManager.IndexConstraintName')}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={indexForm.indexName}
|
||||
onChange={(e) => setIndexForm((f) => ({ ...f, indexName: e.target.value }))}
|
||||
placeholder={buildIndexName(indexForm.indexType, indexForm.columns) || `PK_EntityName_Id`}
|
||||
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"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Clustered */}
|
||||
<div className="flex items-center gap-3">
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={indexForm.isClustered}
|
||||
onChange={(e) =>
|
||||
setIndexForm((f) => ({ ...f, isClustered: e.target.checked }))
|
||||
}
|
||||
className="w-4 h-4 text-indigo-600 rounded"
|
||||
/>
|
||||
<span className="text-sm text-gray-700 dark:text-gray-300">Clustered</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Column picker */}
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-gray-700 dark:text-gray-300 mb-2">
|
||||
{translate('::App.SqlQueryManager.Columns')}
|
||||
</label>
|
||||
<div className="border border-gray-200 dark:border-gray-600 rounded-lg overflow-hidden">
|
||||
<div className="grid grid-cols-12 gap-2 px-3 py-1.5 bg-gray-50 dark:bg-gray-700 text-xs font-semibold text-gray-500">
|
||||
<div className="col-span-1" />
|
||||
<div className="col-span-7">{translate('::App.SqlQueryManager.Column')}</div>
|
||||
<div className="col-span-4">{translate('::App.SqlQueryManager.SortOrder')}</div>
|
||||
</div>
|
||||
<div className="max-h-44 overflow-y-auto divide-y divide-gray-100 dark:divide-gray-700">
|
||||
{columns
|
||||
.filter((c) => c.columnName.trim())
|
||||
.map((col) => {
|
||||
const existing = indexForm.columns.find(
|
||||
(ic) => ic.columnName === col.columnName,
|
||||
)
|
||||
return (
|
||||
<div
|
||||
key={col.id}
|
||||
className="grid grid-cols-12 gap-2 px-3 py-1.5 items-center"
|
||||
>
|
||||
<div className="col-span-1">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={!!existing}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
setIndexForm((f) => {
|
||||
const newCols = [
|
||||
...f.columns,
|
||||
{ columnName: col.columnName, order: 'ASC' as const },
|
||||
]
|
||||
return {
|
||||
...f,
|
||||
columns: newCols,
|
||||
indexName: buildIndexName(f.indexType, newCols),
|
||||
}
|
||||
})
|
||||
} else {
|
||||
setIndexForm((f) => {
|
||||
const newCols = f.columns.filter(
|
||||
(ic) => ic.columnName !== col.columnName,
|
||||
)
|
||||
return {
|
||||
...f,
|
||||
columns: newCols,
|
||||
indexName: buildIndexName(f.indexType, newCols),
|
||||
}
|
||||
})
|
||||
}
|
||||
}}
|
||||
className="w-4 h-4 text-indigo-600 rounded"
|
||||
/>
|
||||
</div>
|
||||
<div className="col-span-7 text-sm font-mono text-gray-800 dark:text-gray-200">
|
||||
{col.columnName}
|
||||
</div>
|
||||
<div className="col-span-4">
|
||||
{existing && (
|
||||
<select
|
||||
value={existing.order}
|
||||
onChange={(e) =>
|
||||
setIndexForm((f) => ({
|
||||
...f,
|
||||
columns: f.columns.map((ic) =>
|
||||
ic.columnName === col.columnName
|
||||
? { ...ic, order: e.target.value as 'ASC' | 'DESC' }
|
||||
: ic,
|
||||
),
|
||||
}))
|
||||
}
|
||||
className="w-full px-2 py-0.5 text-xs border border-gray-300 dark:border-gray-600 rounded dark:bg-gray-700 dark:text-white"
|
||||
>
|
||||
<option value="ASC">ASC</option>
|
||||
<option value="DESC">DESC</option>
|
||||
</select>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
{indexForm.columns.length > 0 && (
|
||||
<p className="text-xs text-gray-400 mt-1">
|
||||
{translate('::App.SqlQueryManager.Selected')}{' '}
|
||||
{indexForm.columns.map((c) => `[${c.columnName}] ${c.order}`).join(', ')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div>
|
||||
<label className="block text-xs font-semibold text-gray-700 dark:text-gray-300 mb-1.5">
|
||||
{translate('::App.SqlQueryManager.Description')}
|
||||
</label>
|
||||
<textarea
|
||||
value={indexForm.description}
|
||||
onChange={(e) =>
|
||||
setIndexForm((f) => ({ ...f, description: e.target.value }))
|
||||
}
|
||||
rows={2}
|
||||
placeholder={translate('::App.SqlQueryManager.OptionalDescriptionPlaceholder')}
|
||||
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={() => setIndexModalOpen(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"
|
||||
>
|
||||
{translate('::App.SqlQueryManager.Cancel')}
|
||||
</button>
|
||||
<button
|
||||
onClick={saveIndex}
|
||||
disabled={!indexForm.indexName.trim() || indexForm.columns.length === 0}
|
||||
className="px-4 py-2 text-sm bg-indigo-600 hover:bg-indigo-700 disabled:opacity-50 text-white rounded-lg transition-colors"
|
||||
>
|
||||
{translate('::App.SqlQueryManager.Save')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>,
|
||||
document.body,
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
|
||||
// ── Step 4: T-SQL Preview ──────────────────────────────────────────────────
|
||||
|
||||
const renderSqlPreview = () => (
|
||||
<div className="flex flex-col gap-3">
|
||||
|
|
@ -1734,7 +2312,8 @@ const SqlTableDesignerDialog = ({
|
|||
{step === 0 && renderColumnDesigner()}
|
||||
{step === 1 && renderEntitySettings()}
|
||||
{step === 2 && renderRelationships()}
|
||||
{step === 3 && renderSqlPreview()}
|
||||
{step === 3 && renderIndexes()}
|
||||
{step === 4 && renderSqlPreview()}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
|
|
@ -1748,7 +2327,7 @@ const SqlTableDesignerDialog = ({
|
|||
{translate('::App.SqlQueryManager.Back')}
|
||||
</Button>
|
||||
)}
|
||||
{step < 3 ? (
|
||||
{step < 4 ? (
|
||||
<Button variant="solid" color="blue-600" onClick={handleNext} disabled={!canGoNext()}>
|
||||
{translate('::App.SqlQueryManager.Next')}
|
||||
</Button>
|
||||
|
|
|
|||
Loading…
Reference in a new issue