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",
|
"en": "Select key column",
|
||||||
"tr": "Anahtar sütunu seç"
|
"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",
|
"resourceName": "Platform",
|
||||||
"key": "App.SqlQueryManager.ColumnDesign",
|
"key": "App.SqlQueryManager.ColumnDesign",
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import {
|
||||||
FaArrowRight,
|
FaArrowRight,
|
||||||
FaChevronDown,
|
FaChevronDown,
|
||||||
FaChevronRight,
|
FaChevronRight,
|
||||||
|
FaKey,
|
||||||
} from 'react-icons/fa'
|
} from 'react-icons/fa'
|
||||||
import { sqlObjectManagerService } from '@/services/sql-query-manager.service'
|
import { sqlObjectManagerService } from '@/services/sql-query-manager.service'
|
||||||
import { getMenus } from '@/services/menu.service'
|
import { getMenus } from '@/services/menu.service'
|
||||||
|
|
@ -69,6 +70,22 @@ interface TableDesignerDialogProps {
|
||||||
initialTableData?: { schemaName: string; tableName: string } | null
|
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 ────────────────────────────────────────────────────────────────
|
// ─── Constants ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const DATA_TYPES: { value: SqlDataType; label: string }[] = [
|
const DATA_TYPES: { value: SqlDataType; label: string }[] = [
|
||||||
|
|
@ -183,6 +200,20 @@ const EMPTY_FK: Omit<SqlTableRelation, 'id'> = {
|
||||||
description: '',
|
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 = {
|
const TENANT_COLUMN: ColumnDefinition = {
|
||||||
id: '__TenantId',
|
id: '__TenantId',
|
||||||
columnName: 'TenantId',
|
columnName: 'TenantId',
|
||||||
|
|
@ -219,6 +250,7 @@ function generateCreateTableSql(
|
||||||
columns: ColumnDefinition[],
|
columns: ColumnDefinition[],
|
||||||
settings: TableSettings,
|
settings: TableSettings,
|
||||||
relationships: SqlTableRelation[],
|
relationships: SqlTableRelation[],
|
||||||
|
indexes: TableIndex[],
|
||||||
): string {
|
): string {
|
||||||
const tableName = settings.tableName || 'NewTable'
|
const tableName = settings.tableName || 'NewTable'
|
||||||
const fullTableName = `[dbo].[${tableName}]`
|
const fullTableName = `[dbo].[${tableName}]`
|
||||||
|
|
@ -264,6 +296,29 @@ function generateCreateTableSql(
|
||||||
fkLines.push(` ON UPDATE ${cascadeUpdate};`)
|
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[] = [
|
const lines: string[] = [
|
||||||
`/* ── Table: ${fullTableName} ── */`,
|
`/* ── Table: ${fullTableName} ── */`,
|
||||||
...(settings.entityName ? [`/* Entity Name: ${settings.entityName} */`] : []),
|
...(settings.entityName ? [`/* Entity Name: ${settings.entityName} */`] : []),
|
||||||
|
|
@ -274,6 +329,7 @@ function generateCreateTableSql(
|
||||||
...bodyLines,
|
...bodyLines,
|
||||||
`);`,
|
`);`,
|
||||||
...fkLines,
|
...fkLines,
|
||||||
|
...indexLines,
|
||||||
'',
|
'',
|
||||||
`/* Verify: SELECT TOP 10 * FROM ${fullTableName}; */`,
|
`/* Verify: SELECT TOP 10 * FROM ${fullTableName}; */`,
|
||||||
]
|
]
|
||||||
|
|
@ -335,6 +391,8 @@ function generateAlterTableSql(
|
||||||
tableName: string,
|
tableName: string,
|
||||||
relationships: SqlTableRelation[],
|
relationships: SqlTableRelation[],
|
||||||
originalRelationships: SqlTableRelation[],
|
originalRelationships: SqlTableRelation[],
|
||||||
|
indexes: TableIndex[],
|
||||||
|
originalIndexes: TableIndex[],
|
||||||
): string {
|
): string {
|
||||||
const fullTableName = `[dbo].[${tableName}]`
|
const fullTableName = `[dbo].[${tableName}]`
|
||||||
const lines: string[] = [`/* ── ALTER TABLE: ${fullTableName} ── */`, '']
|
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) {
|
if (!hasChanges) {
|
||||||
lines.push(
|
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')
|
return lines.join('\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
const STEPS = ['Sütun Tasarımı', 'Entity Ayarları', 'İlişkiler', 'T-SQL Önizleme'] as const
|
const STEPS = ['Sütun Tasarımı', 'Entity Ayarları', 'İlişkiler', 'Index / Key', 'T-SQL Önizleme'] as const
|
||||||
type Step = 0 | 1 | 2 | 3
|
type Step = 0 | 1 | 2 | 3 | 4
|
||||||
|
|
||||||
// ─── Simple Menu Tree (read-only selection) ───────────────────────────────────
|
// ─── Simple Menu Tree (read-only selection) ───────────────────────────────────
|
||||||
|
|
||||||
|
|
@ -668,6 +795,12 @@ const SqlTableDesignerDialog = ({
|
||||||
const [dbTables, setDbTables] = useState<{ schemaName: string; tableName: string }[]>([])
|
const [dbTables, setDbTables] = useState<{ schemaName: string; tableName: string }[]>([])
|
||||||
const [targetTableColumns, setTargetTableColumns] = useState<string[]>([])
|
const [targetTableColumns, setTargetTableColumns] = useState<string[]>([])
|
||||||
const [targetColsLoading, setTargetColsLoading] = useState(false)
|
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) => {
|
const reloadMenus = (onLoaded?: (items: MenuItem[]) => void) => {
|
||||||
setMenuLoading(true)
|
setMenuLoading(true)
|
||||||
|
|
@ -775,6 +908,56 @@ const SqlTableDesignerDialog = ({
|
||||||
})
|
})
|
||||||
.catch(() => {})
|
.catch(() => {})
|
||||||
.finally(() => setFksLoading(false))
|
.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])
|
}, [isOpen, dataSource, initialTableData])
|
||||||
|
|
||||||
|
|
@ -787,8 +970,10 @@ const SqlTableDesignerDialog = ({
|
||||||
settings.tableName || initialTableData?.tableName || '',
|
settings.tableName || initialTableData?.tableName || '',
|
||||||
relationships,
|
relationships,
|
||||||
originalRelationships,
|
originalRelationships,
|
||||||
|
indexes,
|
||||||
|
originalIndexes,
|
||||||
)
|
)
|
||||||
: generateCreateTableSql(columns, settings, relationships),
|
: generateCreateTableSql(columns, settings, relationships, indexes),
|
||||||
[
|
[
|
||||||
isEditMode,
|
isEditMode,
|
||||||
originalColumns,
|
originalColumns,
|
||||||
|
|
@ -797,6 +982,8 @@ const SqlTableDesignerDialog = ({
|
||||||
initialTableData,
|
initialTableData,
|
||||||
relationships,
|
relationships,
|
||||||
originalRelationships,
|
originalRelationships,
|
||||||
|
indexes,
|
||||||
|
originalIndexes,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -813,6 +1000,22 @@ const SqlTableDesignerDialog = ({
|
||||||
const nonEmpty = prev.filter((c) => c.columnName.trim() !== '')
|
const nonEmpty = prev.filter((c) => c.columnName.trim() !== '')
|
||||||
return [...nonEmpty, ...toAdd.map((c) => ({ ...c })), createEmptyColumn()]
|
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 = () => {
|
const addMultiTenantColumns = () => {
|
||||||
|
|
@ -851,28 +1054,43 @@ const SqlTableDesignerDialog = ({
|
||||||
const buildTableName = (prefix: string, entity: string) =>
|
const buildTableName = (prefix: string, entity: string) =>
|
||||||
prefix && entity ? `${prefix}_D_${entity}` : ''
|
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) => {
|
const onMenuCodeSelect = (code: string) => {
|
||||||
if (isEditMode) return
|
if (isEditMode) return
|
||||||
const item = rawMenuItems.find((m) => m.code === code)
|
const item = rawMenuItems.find((m) => m.code === code)
|
||||||
const prefix = item?.shortName ?? ''
|
const prefix = item?.shortName ?? ''
|
||||||
setSelectedMenuCode(code)
|
setSelectedMenuCode(code)
|
||||||
|
const newTableName = buildTableName(prefix, settings.entityName)
|
||||||
setSettings((s) => ({
|
setSettings((s) => ({
|
||||||
...s,
|
...s,
|
||||||
menuValue: prefix,
|
menuValue: prefix,
|
||||||
menuPrefix: prefix,
|
menuPrefix: prefix,
|
||||||
tableName: buildTableName(prefix, s.entityName),
|
tableName: newTableName,
|
||||||
}))
|
}))
|
||||||
|
syncAutoPkName(newTableName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Strip spaces and special chars — only alphanumeric + underscore allowed
|
// Strip spaces and special chars — only alphanumeric + underscore allowed
|
||||||
const onEntityNameChange = (value: string) => {
|
const onEntityNameChange = (value: string) => {
|
||||||
const sanitized = value.replace(/[^A-Za-z0-9_]/g, '')
|
const sanitized = value.replace(/[^A-Za-z0-9_]/g, '')
|
||||||
|
const newTableName = buildTableName(settings.menuPrefix, sanitized)
|
||||||
setSettings((s) => ({
|
setSettings((s) => ({
|
||||||
...s,
|
...s,
|
||||||
entityName: sanitized,
|
entityName: sanitized,
|
||||||
tableName: buildTableName(s.menuPrefix, sanitized),
|
tableName: newTableName,
|
||||||
displayName: sanitized, // always mirrors entity name
|
displayName: sanitized, // always mirrors entity name
|
||||||
}))
|
}))
|
||||||
|
syncAutoPkName(newTableName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── FK Relationship handlers ───────────────────────────────────────────────
|
// ── FK Relationship handlers ───────────────────────────────────────────────
|
||||||
|
|
@ -923,6 +1141,48 @@ const SqlTableDesignerDialog = ({
|
||||||
setRelationships((prev) => prev.filter((r) => r.id !== id))
|
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 ─────────────────────────────────────────────────────────────
|
// ── Navigation ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
// Compute duplicate column names (lowercased)
|
// Compute duplicate column names (lowercased)
|
||||||
|
|
@ -947,7 +1207,7 @@ const SqlTableDesignerDialog = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleNext = () => {
|
const handleNext = () => {
|
||||||
if (step < 3) setStep((s) => (s + 1) as Step)
|
if (step < 4) setStep((s) => (s + 1) as Step)
|
||||||
}
|
}
|
||||||
const handleBack = () => {
|
const handleBack = () => {
|
||||||
if (step > 0) setStep((s) => (s - 1) as Step)
|
if (step > 0) setStep((s) => (s - 1) as Step)
|
||||||
|
|
@ -1009,6 +1269,8 @@ const SqlTableDesignerDialog = ({
|
||||||
setSettings(DEFAULT_SETTINGS)
|
setSettings(DEFAULT_SETTINGS)
|
||||||
setRelationships([])
|
setRelationships([])
|
||||||
setOriginalRelationships([])
|
setOriginalRelationships([])
|
||||||
|
setIndexes([])
|
||||||
|
setOriginalIndexes([])
|
||||||
setDbTables([])
|
setDbTables([])
|
||||||
setTargetTableColumns([])
|
setTargetTableColumns([])
|
||||||
setSelectedMenuCode('')
|
setSelectedMenuCode('')
|
||||||
|
|
@ -1022,6 +1284,7 @@ const SqlTableDesignerDialog = ({
|
||||||
translate('::App.SqlQueryManager.ColumnDesign'),
|
translate('::App.SqlQueryManager.ColumnDesign'),
|
||||||
translate('::App.SqlQueryManager.EntitySettings'),
|
translate('::App.SqlQueryManager.EntitySettings'),
|
||||||
translate('::App.SqlQueryManager.Relationships'),
|
translate('::App.SqlQueryManager.Relationships'),
|
||||||
|
translate('::App.SqlQueryManager.IndexKeys'),
|
||||||
translate('::App.SqlQueryManager.TSqlPreview'),
|
translate('::App.SqlQueryManager.TSqlPreview'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -1099,14 +1362,13 @@ const SqlTableDesignerDialog = ({
|
||||||
|
|
||||||
{/* Header row */}
|
{/* 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="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-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.Max')}</div>
|
||||||
<div className="col-span-1 text-center">
|
<div className="col-span-1 text-center">
|
||||||
{translate('::App.SqlQueryManager.Nullable')}
|
{translate('::App.SqlQueryManager.Nullable')}
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-2">{translate('::App.SqlQueryManager.DefaultValue')}</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">
|
<div className="col-span-1 text-center">
|
||||||
{translate('::App.SqlQueryManager.Actions')}
|
{translate('::App.SqlQueryManager.Actions')}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -1161,7 +1423,7 @@ const SqlTableDesignerDialog = ({
|
||||||
key={col.id}
|
key={col.id}
|
||||||
className={`grid grid-cols-12 gap-2 bg-white dark:bg-gray-800 rounded items-center ${rowBg}`}
|
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
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className={`w-full px-2 py-1 text-sm border rounded dark:bg-gray-700 dark:border-gray-600 dark:text-white ${
|
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)}
|
onChange={(e) => updateColumn(col.id, 'defaultValue', e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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">
|
<div className="col-span-1 flex items-center justify-center gap-1">
|
||||||
<button
|
<button
|
||||||
onClick={() => moveColumn(col.id, 'up')}
|
onClick={() => moveColumn(col.id, 'up')}
|
||||||
|
|
@ -1683,7 +1936,332 @@ const SqlTableDesignerDialog = ({
|
||||||
</div>
|
</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 = () => (
|
const renderSqlPreview = () => (
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
|
|
@ -1734,7 +2312,8 @@ const SqlTableDesignerDialog = ({
|
||||||
{step === 0 && renderColumnDesigner()}
|
{step === 0 && renderColumnDesigner()}
|
||||||
{step === 1 && renderEntitySettings()}
|
{step === 1 && renderEntitySettings()}
|
||||||
{step === 2 && renderRelationships()}
|
{step === 2 && renderRelationships()}
|
||||||
{step === 3 && renderSqlPreview()}
|
{step === 3 && renderIndexes()}
|
||||||
|
{step === 4 && renderSqlPreview()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
|
|
@ -1748,7 +2327,7 @@ const SqlTableDesignerDialog = ({
|
||||||
{translate('::App.SqlQueryManager.Back')}
|
{translate('::App.SqlQueryManager.Back')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{step < 3 ? (
|
{step < 4 ? (
|
||||||
<Button variant="solid" color="blue-600" onClick={handleNext} disabled={!canGoNext()}>
|
<Button variant="solid" color="blue-600" onClick={handleNext} disabled={!canGoNext()}>
|
||||||
{translate('::App.SqlQueryManager.Next')}
|
{translate('::App.SqlQueryManager.Next')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue