diff --git a/api/src/Sozsoft.Platform.DbMigrator/Seeds/LanguagesData.json b/api/src/Sozsoft.Platform.DbMigrator/Seeds/LanguagesData.json index c2ba40a..a34ef69 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/Seeds/LanguagesData.json +++ b/api/src/Sozsoft.Platform.DbMigrator/Seeds/LanguagesData.json @@ -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", diff --git a/ui/src/views/developerKit/SqlTableDesignerDialog.tsx b/ui/src/views/developerKit/SqlTableDesignerDialog.tsx index 6d9bb5f..db6d877 100644 --- a/ui/src/views/developerKit/SqlTableDesignerDialog.tsx +++ b/ui/src/views/developerKit/SqlTableDesignerDialog.tsx @@ -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 = { description: '', } +const EMPTY_INDEX: Omit = { + 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() + originalIndexes.forEach((ix) => origIdxById.set(ix.id, ix)) + const curIdxById = new Map() + 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([]) const [targetColsLoading, setTargetColsLoading] = useState(false) + const [indexes, setIndexes] = useState([]) + const [originalIndexes, setOriginalIndexes] = useState([]) + const [indexesLoading, setIndexesLoading] = useState(false) + const [indexModalOpen, setIndexModalOpen] = useState(false) + const [editingIndexId, setEditingIndexId] = useState(null) + const [indexForm, setIndexForm] = useState>(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() + 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 */}
-
{translate('::App.SqlQueryManager.ColumnName')}
+
{translate('::App.SqlQueryManager.ColumnName')}
{translate('::App.SqlQueryManager.DataType')}
{translate('::App.SqlQueryManager.Max')}
{translate('::App.SqlQueryManager.Nullable')}
{translate('::App.SqlQueryManager.DefaultValue')}
-
{translate('::App.SqlQueryManager.Note')}
{translate('::App.SqlQueryManager.Actions')}
@@ -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}`} > -
+
updateColumn(col.id, 'defaultValue', e.target.value)} />
-
- updateColumn(col.id, 'description', e.target.value)} - /> -
) - // ── Step 3: T-SQL Preview ────────────────────────────────────────────────── + // ── Step 3: Index / Key ──────────────────────────────────────────────────── + + const INDEX_TYPE_STYLES: Record = { + 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 = () => ( +
+ {indexesLoading && ( +
+ {translate('::App.SqlQueryManager.Loading')} +
+ )} + {!indexesLoading && ( + <> + {/* Header */} +
+ + {indexes.length === 0 + ? translate('::App.SqlQueryManager.NoIndexesDefined') + : `${indexes.length} ${translate('::App.SqlQueryManager.IndexKey')}`} + + +
+ + {/* Empty state */} + {indexes.length === 0 && ( +
+ +

{translate('::App.SqlQueryManager.NoIndexesDefined')}

+

{translate('::App.SqlQueryManager.StepIsOptional')}

+
+ )} + + {/* Index cards */} +
+ {indexes.map((idx) => { + const styles = INDEX_TYPE_STYLES[idx.indexType] + return ( +
+
+ +
+
+
+ + [{idx.indexName}] + + + {idx.indexType} + + + {idx.isClustered ? 'CLUSTERED' : 'NONCLUSTERED'} + +
+
+ {idx.columns.map((col, ci) => ( + + {col.columnName} {col.order} + + ))} +
+ {idx.description && ( +

{idx.description}

+ )} +
+
+ + +
+
+ ) + })} +
+ + )} + + {/* Index Add/Edit Modal */} + {indexModalOpen && + createPortal( +
+
+ {/* Modal Header */} +
+

+ {editingIndexId ? translate('::App.SqlQueryManager.EditIndexKey') : translate('::App.SqlQueryManager.AddNewIndexKey')} +

+ +
+ + {/* Modal Body */} +
+ {/* Index Type */} +
+ +
+ {INDEX_TYPES.map((t) => ( + + ))} +
+
+ + {/* Index / Constraint Name */} +
+ + 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" + /> +
+ + {/* Clustered */} +
+ +
+ + {/* Column picker */} +
+ +
+
+
+
{translate('::App.SqlQueryManager.Column')}
+
{translate('::App.SqlQueryManager.SortOrder')}
+
+
+ {columns + .filter((c) => c.columnName.trim()) + .map((col) => { + const existing = indexForm.columns.find( + (ic) => ic.columnName === col.columnName, + ) + return ( +
+
+ { + 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" + /> +
+
+ {col.columnName} +
+
+ {existing && ( + + )} +
+
+ ) + })} +
+
+ {indexForm.columns.length > 0 && ( +

+ {translate('::App.SqlQueryManager.Selected')}{' '} + {indexForm.columns.map((c) => `[${c.columnName}] ${c.order}`).join(', ')} +

+ )} +
+ + {/* Description */} +
+ +