From ff58614f0d1ad853c1f57271214e84292e174c39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sedat=20=C3=96ZT=C3=9CRK?= <76204082+iamsedatozturk@users.noreply.github.com> Date: Wed, 18 Mar 2026 12:00:11 +0300 Subject: [PATCH] =?UTF-8?q?SQL=20Query=20Manager=20d=C3=BCzenlemesi?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Seeds/LanguagesData.json | 91 ++- .../views/developerKit/SqlObjectExplorer.tsx | 113 +++- ui/src/views/developerKit/SqlQueryManager.tsx | 634 +++++++++++++++++- .../developerKit/SqlTableDesignerDialog.tsx | 88 ++- 4 files changed, 905 insertions(+), 21 deletions(-) diff --git a/api/src/Sozsoft.Platform.DbMigrator/Seeds/LanguagesData.json b/api/src/Sozsoft.Platform.DbMigrator/Seeds/LanguagesData.json index 95014a6..bcd9da5 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/Seeds/LanguagesData.json +++ b/api/src/Sozsoft.Platform.DbMigrator/Seeds/LanguagesData.json @@ -10356,6 +10356,12 @@ "tr": "Hata", "en": "Error" }, + { + "resourceName": "Platform", + "key": "App.Platform.Skipped", + "tr": "Atlandı", + "en": "Skipped" + }, { "resourceName": "Platform", "key": "App.Platform.Edit", @@ -10404,6 +10410,36 @@ "tr": "Yeni Sorgu", "en": "New Query" }, + { + "resourceName": "Platform", + "key": "App.Platform.CopySelectedObjects", + "tr": "Seçili Nesneleri Kopyala", + "en": "Copy Selected Objects" + }, + { + "resourceName": "Platform", + "key": "App.Platform.SelectAllTargets", + "tr": "Tüm Hedefleri Seç", + "en": "Select All Targets" + }, + { + "resourceName": "Platform", + "key": "App.Platform.SelectedObjects", + "tr": "Seçili Nesneler", + "en": "Selected Objects" + }, + { + "resourceName": "Platform", + "key": "App.Platform.SourceDataSource", + "tr": "Kaynak Data Source", + "en": "Source Data Source" + }, + { + "resourceName": "Platform", + "key": "App.Platform.TargetDataSources", + "tr": "Hedef Data Sourcelar", + "en": "Target Data Sources" + }, { "resourceName": "Platform", "key": "App.Platform.Save", @@ -10698,6 +10734,30 @@ "tr": "Durum", "en": "Status" }, + { + "resourceName": "Platform", + "key": "App.Platform.Object", + "tr": "Nesne", + "en": "Object" + }, + { + "resourceName": "Platform", + "key": "App.Platform.Type", + "tr": "Tip", + "en": "Type" + }, + { + "resourceName": "Platform", + "key": "App.Platform.Target", + "tr": "Hedef", + "en": "Target" + }, + { + "resourceName": "Platform", + "key": "App.Platform.Message", + "tr": "Mesaj", + "en": "Message" + }, { "resourceName": "Platform", "key": "App.Platform.Category", @@ -16985,7 +17045,18 @@ "en": "Copy", "tr": "Kopyala" }, - + { + "resourceName": "Platform", + "key": "App.Platform.OverwriteIfExists", + "en": "Overwrite if exists", + "tr": "Varsa üzerine yaz" + }, + { + "resourceName": "Platform", + "key": "App.Platform.OverwriteIfExistsDesc", + "en": "If checked, existing object in the target will be dropped and recreated.", + "tr": "İşaretlenirse hedefteki mevcut obje silinip yeniden oluşturulur." + }, { "resourceName": "Platform", "key": "App.SqlQueryManager.Cancel", @@ -17035,6 +17106,24 @@ "en": "Success", "tr": "Başarılı" }, + { + "resourceName": "Platform", + "key": "App.SqlQueryManager.Skipped", + "en": "Skipped", + "tr": "Atlandı" + }, + { + "resourceName": "Platform", + "key": "App.SqlQueryManager.SkippedDescription", + "en": "Already exists in target. Skipped because overwrite option is not enabled.", + "tr": "Hedefte mevcut. Üzerine yaz seçeneği kapalı olduğu için atlandı." + }, + { + "resourceName": "Platform", + "key": "App.SqlQueryManager.Error", + "en": "Error", + "tr": "Hata" + }, { "resourceName": "Platform", "key": "App.SqlQueryManager.TableUpdated", diff --git a/ui/src/views/developerKit/SqlObjectExplorer.tsx b/ui/src/views/developerKit/SqlObjectExplorer.tsx index b2eadfd..9ce2d7a 100644 --- a/ui/src/views/developerKit/SqlObjectExplorer.tsx +++ b/ui/src/views/developerKit/SqlObjectExplorer.tsx @@ -31,11 +31,21 @@ interface SqlObjectExplorerProps { dataSource: string | null onTemplateSelect?: (template: string, templateType: string) => void onViewDefinition?: (schemaName: string, objectName: string) => void + onGenerateTableScript?: (schemaName: string, tableName: string) => void onDesignTable?: (schemaName: string, tableName: string) => void onNewTable?: () => void + onSelectedObjectsChange?: (objects: SqlExplorerSelectedObject[]) => void refreshTrigger?: number } +export interface SqlExplorerSelectedObject { + id: string + objectType: 'table' | 'view' | 'procedure' | 'function' + schemaName: string + objectName: string + fullName: string +} + const FOLDER_META: Record = { tables: { label: 'Tables', color: 'text-teal-500' }, views: { label: 'Views', color: 'text-purple-500' }, @@ -47,8 +57,10 @@ const SqlObjectExplorer = ({ dataSource, onTemplateSelect, onViewDefinition, + onGenerateTableScript, onDesignTable, onNewTable, + onSelectedObjectsChange, refreshTrigger, }: SqlObjectExplorerProps) => { const { translate } = useLocalization() @@ -56,6 +68,7 @@ const SqlObjectExplorer = ({ const [expandedNodes, setExpandedNodes] = useState>(new Set(['root'])) const [loading, setLoading] = useState(false) const [filterText, setFilterText] = useState('') + const [selectedObjectIds, setSelectedObjectIds] = useState>(new Set()) const [dropConfirm, setDropConfirm] = useState<{ node: TreeNode } | null>(null) const [dropping, setDropping] = useState(false) const [contextMenu, setContextMenu] = useState<{ @@ -63,10 +76,20 @@ const SqlObjectExplorer = ({ }>({ show: false, x: 0, y: 0, node: null }) useEffect(() => { - if (dataSource) loadObjects() - else setTreeData([]) + if (dataSource) { + loadObjects() + } else { + setTreeData([]) + setSelectedObjectIds(new Set()) + onSelectedObjectsChange?.([]) + } }, [dataSource, refreshTrigger]) + useEffect(() => { + const selected = getSelectedObjects(treeData, selectedObjectIds) + onSelectedObjectsChange?.(selected) + }, [treeData, selectedObjectIds, onSelectedObjectsChange]) + const loadObjects = async () => { if (!dataSource) return setLoading(true) @@ -167,6 +190,63 @@ const SqlObjectExplorer = ({ } } + const mapNodeToSelectedObject = (node: TreeNode): SqlExplorerSelectedObject | null => { + if (node.type !== 'object' || !node.folder || !node.data) return null + + if (node.folder === 'tables') { + const t = node.data as DatabaseTableDto + return { + id: node.id, + objectType: 'table', + schemaName: t.schemaName, + objectName: t.tableName, + fullName: t.fullName ?? `[${t.schemaName}].[${t.tableName}]`, + } + } + + const obj = node.data as SqlNativeObjectDto + const objectType = + node.folder === 'views' + ? 'view' + : node.folder === 'procedures' + ? 'procedure' + : 'function' + + return { + id: node.id, + objectType, + schemaName: obj.schemaName, + objectName: obj.objectName, + fullName: obj.fullName ?? `[${obj.schemaName}].[${obj.objectName}]`, + } + } + + const getSelectedObjects = (nodes: TreeNode[], ids: Set): SqlExplorerSelectedObject[] => { + const selected: SqlExplorerSelectedObject[] = [] + + const walk = (list: TreeNode[]) => { + for (const node of list) { + if (node.type === 'object' && ids.has(node.id)) { + const mapped = mapNodeToSelectedObject(node) + if (mapped) selected.push(mapped) + } + if (node.children?.length) walk(node.children) + } + } + + walk(nodes) + return selected + } + + const toggleObjectSelection = (nodeId: string, checked: boolean) => { + setSelectedObjectIds((prev) => { + const next = new Set(prev) + if (checked) next.add(nodeId) + else next.delete(nodeId) + return next + }) + } + const buildDropSql = (node: TreeNode): string => { if (node.folder === 'tables') { const t = node.data as DatabaseTableDto @@ -217,6 +297,7 @@ const SqlObjectExplorer = ({ const renderNode = (node: TreeNode, level = 0) => { const isExpanded = expandedNodes.has(node.id) + const isChecked = selectedObjectIds.has(node.id) return (
handleNodeClick(node)} onContextMenu={(e) => { e.preventDefault(); setContextMenu({ show: true, x: e.clientX, y: e.clientY, node }) }} > + {node.type === 'object' && ( + toggleObjectSelection(node.id, e.target.checked)} + onClick={(e) => e.stopPropagation()} + /> + )} {getIcon(node)} {node.label} {node.type === 'object' && ( @@ -295,12 +385,27 @@ const SqlObjectExplorer = ({ > {/* TABLE object � Design */} {isTableObj && ( - + )} + + {isTableObj && ( + )} diff --git a/ui/src/views/developerKit/SqlQueryManager.tsx b/ui/src/views/developerKit/SqlQueryManager.tsx index 7aaa939..28196d3 100644 --- a/ui/src/views/developerKit/SqlQueryManager.tsx +++ b/ui/src/views/developerKit/SqlQueryManager.tsx @@ -8,7 +8,7 @@ import { sqlObjectManagerService } from '@/services/sql-query-manager.service' import { FaDatabase, FaPlay, FaFileAlt } from 'react-icons/fa' import { FaCheckCircle } from 'react-icons/fa' import { useLocalization } from '@/utils/hooks/useLocalization' -import SqlObjectExplorer from './SqlObjectExplorer' +import SqlObjectExplorer, { type SqlExplorerSelectedObject } from './SqlObjectExplorer' import SqlEditor, { SqlEditorRef } from './SqlEditor' import SqlResultsGrid from './SqlResultsGrid' import SqlTableDesignerDialog from './SqlTableDesignerDialog' @@ -29,6 +29,14 @@ interface SqlManagerState { refreshTrigger: number } +interface SqlCopyResultItem { + targetDataSource: string + objectFullName: string + objectType: SqlExplorerSelectedObject['objectType'] + status: 'success' | 'error' | 'skipped' + message: string +} + const SqlQueryManager = () => { const { translate } = useLocalization() const editorRef = useRef(null) @@ -55,6 +63,15 @@ const SqlQueryManager = () => { schemaName: string tableName: string } | null>(null) + const [selectedExplorerObjects, setSelectedExplorerObjects] = useState< + SqlExplorerSelectedObject[] + >([]) + const [showCopyDialog, setShowCopyDialog] = useState(false) + const [copyTargetDataSources, setCopyTargetDataSources] = useState([]) + const [overwriteIfExists, setOverwriteIfExists] = useState(false) + const [isCopyingObjects, setIsCopyingObjects] = useState(false) + const [copyResults, setCopyResults] = useState([]) + const [showCopyResultDialog, setShowCopyResultDialog] = useState(false) useEffect(() => { loadDataSources() @@ -109,6 +126,167 @@ const SqlQueryManager = () => { })) }, []) + const escapeSqlLiteral = (value: string) => value.replace(/'/g, "''") + + const escapeSqlIdentifier = (value: string) => value.replace(/]/g, ']]') + + const getSafeFullName = (schemaName: string, objectName: string) => + `[${escapeSqlIdentifier(schemaName)}].[${escapeSqlIdentifier(objectName)}]` + + const buildTableScriptQuery = (schemaName: string, tableName: string) => { + const fullName = getSafeFullName(schemaName, tableName) + const escapedFullName = escapeSqlLiteral(fullName) + + return `DECLARE @ObjectId INT = OBJECT_ID(N'${escapedFullName}'); + +IF @ObjectId IS NULL +BEGIN + SELECT CAST('' AS NVARCHAR(MAX)) AS Script; + RETURN; +END; + +;WITH cols AS +( + SELECT + c.column_id, + ' ' + QUOTENAME(c.name) + ' ' + + CASE + WHEN t.name IN ('varchar', 'char', 'varbinary', 'binary') THEN + t.name + '(' + CASE WHEN c.max_length = -1 THEN 'MAX' ELSE CAST(c.max_length AS VARCHAR(10)) END + ')' + WHEN t.name IN ('nvarchar', 'nchar') THEN + t.name + '(' + CASE WHEN c.max_length = -1 THEN 'MAX' ELSE CAST(c.max_length / 2 AS VARCHAR(10)) END + ')' + WHEN t.name IN ('decimal', 'numeric') THEN + t.name + '(' + CAST(c.precision AS VARCHAR(10)) + ',' + CAST(c.scale AS VARCHAR(10)) + ')' + WHEN t.name IN ('datetime2', 'datetimeoffset', 'time') THEN + t.name + '(' + CAST(c.scale AS VARCHAR(10)) + ')' + ELSE t.name + END + + CASE + WHEN ic.object_id IS NOT NULL + THEN ' IDENTITY(' + CAST(ic.seed_value AS VARCHAR(30)) + ',' + CAST(ic.increment_value AS VARCHAR(30)) + ')' + ELSE '' + END + + CASE WHEN c.is_nullable = 1 THEN ' NULL' ELSE ' NOT NULL' END + + ISNULL(' DEFAULT ' + dc.definition, '') AS line + FROM sys.columns c + INNER JOIN sys.types t ON c.user_type_id = t.user_type_id + LEFT JOIN sys.identity_columns ic ON c.object_id = ic.object_id AND c.column_id = ic.column_id + LEFT JOIN sys.default_constraints dc ON c.default_object_id = dc.object_id + WHERE c.object_id = @ObjectId +), +pk AS +( + SELECT + ' CONSTRAINT ' + QUOTENAME(k.name) + ' PRIMARY KEY ' + + CASE WHEN i.type = 1 THEN 'CLUSTERED' ELSE 'NONCLUSTERED' END + + ' (' + + STUFF( + ( + SELECT ', ' + QUOTENAME(c.name) + CASE WHEN ic.is_descending_key = 1 THEN ' DESC' ELSE ' ASC' END + FROM sys.index_columns ic + INNER JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id + WHERE ic.object_id = i.object_id + AND ic.index_id = i.index_id + AND ic.is_included_column = 0 + ORDER BY ic.key_ordinal + FOR XML PATH(''), TYPE + ).value('.', 'NVARCHAR(MAX)') + ,1,2,'') + ')' AS line + FROM sys.key_constraints k + INNER JOIN sys.indexes i ON k.parent_object_id = i.object_id AND k.unique_index_id = i.index_id + WHERE k.parent_object_id = @ObjectId + AND k.type = 'PK' +) +SELECT + 'CREATE TABLE ${fullName}' + CHAR(13) + CHAR(10) + + '(' + CHAR(13) + CHAR(10) + + STUFF( + ( + SELECT ',' + CHAR(13) + CHAR(10) + line + FROM cols + ORDER BY column_id + FOR XML PATH(''), TYPE + ).value('.', 'NVARCHAR(MAX)') + ,1,3,'') + + ISNULL( + ( + SELECT ',' + CHAR(13) + CHAR(10) + line + FROM pk + ), + '' + ) + CHAR(13) + CHAR(10) + ');' AS Script;` + } + + const getTableCreateScript = async (schemaName: string, tableName: string): Promise => { + if (!state.selectedDataSource) return '' + + const result = await sqlObjectManagerService.executeQuery({ + queryText: buildTableScriptQuery(schemaName, tableName), + dataSourceCode: state.selectedDataSource, + }) + + const firstRow = result.data?.data?.[0] + if (!firstRow) return '' + + return firstRow.Script || firstRow.script || '' + } + + const normalizeNativeDefinitionToCreate = (definition: string) => { + if (!definition?.trim()) return '' + return definition.replace(/^\s*ALTER\s+/i, 'CREATE ') + } + + const buildDropIfExistsScript = (obj: SqlExplorerSelectedObject) => { + const fullName = getSafeFullName(obj.schemaName, obj.objectName) + + if (obj.objectType === 'table') { + return `IF OBJECT_ID(N'${escapeSqlLiteral(fullName)}', N'U') IS NOT NULL DROP TABLE ${fullName};` + } + + if (obj.objectType === 'view') { + return `IF OBJECT_ID(N'${escapeSqlLiteral(fullName)}', N'V') IS NOT NULL DROP VIEW ${fullName};` + } + + if (obj.objectType === 'procedure') { + return `IF OBJECT_ID(N'${escapeSqlLiteral(fullName)}', N'P') IS NOT NULL DROP PROCEDURE ${fullName};` + } + + return `IF OBJECT_ID(N'${escapeSqlLiteral(fullName)}', N'FN') IS NOT NULL OR OBJECT_ID(N'${escapeSqlLiteral(fullName)}', N'IF') IS NOT NULL OR OBJECT_ID(N'${escapeSqlLiteral(fullName)}', N'TF') IS NOT NULL DROP FUNCTION ${fullName};` + } + + const buildObjectExistsCheckQuery = (obj: SqlExplorerSelectedObject) => { + const fullName = getSafeFullName(obj.schemaName, obj.objectName) + const escapedFullName = escapeSqlLiteral(fullName) + + if (obj.objectType === 'table') { + return `SELECT CASE WHEN OBJECT_ID(N'${escapedFullName}', N'U') IS NOT NULL THEN 1 ELSE 0 END AS ExistsFlag;` + } + + if (obj.objectType === 'view') { + return `SELECT CASE WHEN OBJECT_ID(N'${escapedFullName}', N'V') IS NOT NULL THEN 1 ELSE 0 END AS ExistsFlag;` + } + + if (obj.objectType === 'procedure') { + return `SELECT CASE WHEN OBJECT_ID(N'${escapedFullName}', N'P') IS NOT NULL THEN 1 ELSE 0 END AS ExistsFlag;` + } + + return `SELECT CASE WHEN OBJECT_ID(N'${escapedFullName}', N'FN') IS NOT NULL OR OBJECT_ID(N'${escapedFullName}', N'IF') IS NOT NULL OR OBJECT_ID(N'${escapedFullName}', N'TF') IS NOT NULL THEN 1 ELSE 0 END AS ExistsFlag;` + } + + const checkObjectExistsInTarget = async ( + targetDataSource: string, + obj: SqlExplorerSelectedObject, + ) => { + const result = await sqlObjectManagerService.executeQuery({ + queryText: buildObjectExistsCheckQuery(obj), + dataSourceCode: targetDataSource, + }) + + const firstRow = result.data?.data?.[0] + const flag = firstRow?.ExistsFlag ?? firstRow?.existsFlag ?? 0 + return Number(flag) === 1 + } + const getTemplateContent = (templateType: string): string => { const templates: Record = { select: `-- Basic SELECT query @@ -393,6 +571,204 @@ GO`, } } + const handleGenerateTableScript = async (schemaName: string, tableName: string) => { + if (!state.selectedDataSource) return + + try { + const script = await getTableCreateScript(schemaName, tableName) + + if (!script?.trim()) { + toast.push( + + {translate('::App.Platform.ScriptNotGenerated') || 'Tablo scripti olusturulamadi.'} + , + { placement: 'top-center' }, + ) + return + } + + setState((prev) => ({ + ...prev, + editorContent: script, + executionResult: null, + tableColumns: null, + isDirty: false, + })) + + toast.push( + + {'Script Query Editor alanina yuklendi.'} + , + { placement: 'top-center' }, + ) + } catch (error: any) { + toast.push( + + {error.response?.data?.error?.message || 'Tablo scripti olusturulurken hata olustu.'} + , + { placement: 'top-center' }, + ) + } + } + + const handleOpenCopyDialog = () => { + if (!state.selectedDataSource) return + + if (selectedExplorerObjects.length === 0) { + toast.push( + + {'Lutfen kopyalamak icin en az bir obje secin.'} + , + { placement: 'top-center' }, + ) + return + } + + setCopyTargetDataSources([]) + setOverwriteIfExists(false) + setShowCopyDialog(true) + } + + const handleCopyObjects = async () => { + if ( + !state.selectedDataSource || + selectedExplorerObjects.length === 0 || + copyTargetDataSources.length === 0 + ) { + return + } + + setIsCopyingObjects(true) + + const results: SqlCopyResultItem[] = [] + + try { + const scriptsByObjectId = new Map() + + for (const obj of selectedExplorerObjects) { + let createScript = '' + + if (obj.objectType === 'table') { + createScript = await getTableCreateScript(obj.schemaName, obj.objectName) + } else { + const response = await sqlObjectManagerService.getNativeObjectDefinition( + state.selectedDataSource, + obj.schemaName, + obj.objectName, + ) + createScript = normalizeNativeDefinitionToCreate(response.data || '') + } + + if (!createScript?.trim()) { + results.push({ + targetDataSource: state.selectedDataSource, + objectFullName: obj.fullName, + objectType: obj.objectType, + status: 'error', + message: 'Kaynak objenin scripti olusturulamadi.', + }) + continue + } + + scriptsByObjectId.set(obj.id, createScript) + } + + for (const targetDataSource of copyTargetDataSources) { + for (const obj of selectedExplorerObjects) { + const createScript = scriptsByObjectId.get(obj.id) + if (!createScript) continue + + if (!overwriteIfExists) { + const exists = await checkObjectExistsInTarget(targetDataSource, obj) + if (exists) { + results.push({ + targetDataSource, + objectFullName: obj.fullName, + objectType: obj.objectType, + status: 'skipped', + message: translate('::App.SqlQueryManager.SkippedDescription') , + }) + continue + } + } + + const command = overwriteIfExists + ? `${buildDropIfExistsScript(obj)}\n${createScript}` + : createScript + + try { + await sqlObjectManagerService.executeQuery({ + queryText: command, + dataSourceCode: targetDataSource, + }) + results.push({ + targetDataSource, + objectFullName: obj.fullName, + objectType: obj.objectType, + status: 'success', + message: 'Basariyla kopyalandi.', + }) + } catch (error: any) { + results.push({ + targetDataSource, + objectFullName: obj.fullName, + objectType: obj.objectType, + status: 'error', + message: error.response?.data?.error?.message || 'Kopyalama basarisiz.', + }) + } + } + } + + const successCount = results.filter((x) => x.status === 'success').length + const errorCount = results.filter((x) => x.status === 'error').length + const skippedCount = results.filter((x) => x.status === 'skipped').length + + const notificationType = errorCount > 0 ? 'warning' : 'success' + toast.push( + 0 + ? translate('::App.Platform.Warning') + : translate('::App.Platform.Success') + } + > + {`Kopyalama tamamlandi. Basarili: ${successCount}, Atlanan: ${skippedCount}, Hata: ${errorCount}`} + , + { placement: 'top-center' }, + ) + + setCopyResults(results) + setShowCopyResultDialog(true) + + setShowCopyDialog(false) + } finally { + setIsCopyingObjects(false) + } + } + + const availableTargetDataSourceCodes = state.dataSources + .filter((d) => d.code && d.code !== state.selectedDataSource) + .map((d) => d.code || '') + + const allTargetsSelected = + availableTargetDataSourceCodes.length > 0 && + availableTargetDataSourceCodes.every((code) => copyTargetDataSources.includes(code)) + + const handleToggleSelectAllTargets = (checked: boolean) => { + if (checked) { + setCopyTargetDataSources(availableTargetDataSourceCodes) + return + } + + setCopyTargetDataSources([]) + } + + const copySuccessCount = copyResults.filter((x) => x.status === 'success').length + const copyErrorCount = copyResults.filter((x) => x.status === 'error').length + const copySkippedCount = copyResults.filter((x) => x.status === 'skipped').length + return (
+
-
{/* Main Content Area */} @@ -466,9 +850,11 @@ GO`, dataSource={state.selectedDataSource} onTemplateSelect={handleTemplateSelect} onViewDefinition={handleViewDefinition} + onGenerateTableScript={handleGenerateTableScript} refreshTrigger={state.refreshTrigger} onNewTable={handleNewTable} onDesignTable={handleDesignTable} + onSelectedObjectsChange={setSelectedExplorerObjects} /> @@ -583,6 +969,250 @@ GO`, setState((prev) => ({ ...prev, refreshTrigger: prev.refreshTrigger + 1 })) }} /> + + !isCopyingObjects && setShowCopyDialog(false)} + onRequestClose={() => !isCopyingObjects && setShowCopyDialog(false)} + contentClassName="max-h-[85vh] overflow-hidden" + > +
+
{translate('::App.Platform.CopySelectedObjects')}
+
+
+

+ {translate('::App.Platform.SourceDataSource')}:{' '} + {state.selectedDataSource} +

+ +
+ +
+ {selectedExplorerObjects.map((obj) => ( +
+ {obj.objectType.toUpperCase()} - {obj.fullName} +
+ ))} +
+ +
+
+ {translate('::App.Platform.TargetDataSources')} +
+ +
+ {availableTargetDataSourceCodes.map((code) => { + const checked = copyTargetDataSources.includes(code) + return ( + + ) + })} +
+
+
+
+ + +
+
+
+ + setShowCopyResultDialog(false)} + onRequestClose={() => setShowCopyResultDialog(false)} + width={1050} + contentClassName="max-h-[85vh] overflow-hidden" + > +
+
+ {translate('::App.Platform.Results') || 'Kopyalama Sonuc Detaylari'} +
+ +
+
+ {translate('::App.Platform.Success')}: {copySuccessCount} +
+
+ {translate('::App.Platform.Skipped')}: {copySkippedCount} +
+
+ {translate('::App.Platform.Error')}: {copyErrorCount} +
+
+ +
+
+ {copyResults.map((row, idx) => { + const isError = row.status === 'error' + const isSkipped = row.status === 'skipped' + const cardClass = isError + ? 'border-red-200 bg-red-50/60 dark:bg-red-900/20' + : isSkipped + ? 'border-amber-200 bg-amber-50/60 dark:bg-amber-900/20' + : 'border-gray-200 bg-white dark:bg-gray-800' + + return ( +
+
+ + {row.objectType} + + {row.status === 'success' && ( + + {translate('::App.Platform.Success')} + + )} + {row.status === 'error' && ( + + {translate('::App.Platform.Error')} + + )} + {row.status === 'skipped' && ( + + {translate('::App.Platform.Skipped')} + + )} +
+
{row.objectFullName}
+
Hedef: {row.targetDataSource}
+
+ {row.message} +
+
+ ) + })} + {copyResults.length === 0 && ( +
+ {translate('::App.Platform.NoResults')} +
+ )} +
+ + + + + + + + + + + + + {copyResults.map((row, idx) => { + const isError = row.status === 'error' + const isSkipped = row.status === 'skipped' + const rowClass = isError + ? 'bg-red-50/60 dark:bg-red-900/20' + : isSkipped + ? 'bg-amber-50/60 dark:bg-amber-900/20' + : idx % 2 === 0 + ? 'bg-white dark:bg-gray-800' + : 'bg-gray-50/40 dark:bg-gray-800/70' + + return ( + + + + + + + + ) + })} + {copyResults.length === 0 && ( + + + + )} + +
{translate('::App.Platform.Status')}{translate('::App.Platform.Object')}{translate('::App.Platform.Type')}{translate('::App.Platform.Target')}{translate('::App.Platform.Message')}
+ {row.status === 'success' && ( + + {translate('::App.Platform.Success')} + + )} + {row.status === 'error' && ( + + {translate('::App.Platform.Error')} + + )} + {row.status === 'skipped' && ( + + {translate('::App.Platform.Skipped')} + + )} + + {row.objectFullName} + {row.objectType}{row.targetDataSource} + {row.message} +
+ {translate('::App.Platform.NoResults')} +
+
+
+ +
+ +
+
) } diff --git a/ui/src/views/developerKit/SqlTableDesignerDialog.tsx b/ui/src/views/developerKit/SqlTableDesignerDialog.tsx index a6e201d..c1fdc05 100644 --- a/ui/src/views/developerKit/SqlTableDesignerDialog.tsx +++ b/ui/src/views/developerKit/SqlTableDesignerDialog.tsx @@ -315,14 +315,14 @@ function generateCreateTableSql( const lines: string[] = [ `/* ── Table: ${fullTableName} ── */`, ...(settings.entityName ? [`/* Entity Name: ${settings.entityName} */`] : []), - ...(fkLines.length > 0 ? ['/* Foreign Key Constraints */'] : []), '', `CREATE TABLE ${fullTableName}`, `(`, ...bodyLines, `);`, - ...fkLines, ...indexLines, + ...(fkLines.length > 0 ? ['/* Foreign Key Constraints */'] : []), + ...fkLines, '', `/* Verify: SELECT TOP 10 * FROM ${fullTableName}; */`, ] @@ -603,7 +603,7 @@ function generateAlterTableSql( return lines.join('\n') } -const STEPS = ['Sütun Tasarımı', 'Entity Ayarları', 'İlişkiler', 'Index / Key', 'T-SQL Önizleme'] as const +const STEPS = ['Sütun Tasarımı', 'Entity Ayarları', 'Index / Key', 'İlişkiler', 'T-SQL Önizleme'] as const type Step = 0 | 1 | 2 | 3 | 4 // ─── Simple Menu Tree (read-only selection) ─────────────────────────────────── @@ -787,6 +787,7 @@ const SqlTableDesignerDialog = ({ const [fkForm, setFkForm] = useState>(EMPTY_FK) const [dbTables, setDbTables] = useState<{ schemaName: string; tableName: string }[]>([]) const [targetTableColumns, setTargetTableColumns] = useState([]) + const [targetTableKeyColumns, setTargetTableKeyColumns] = useState([]) const [targetColsLoading, setTargetColsLoading] = useState(false) const [indexes, setIndexes] = useState([]) const [originalIndexes, setOriginalIndexes] = useState([]) @@ -1114,15 +1115,38 @@ const SqlTableDesignerDialog = ({ const loadTargetColumns = (tableName: string) => { if (!tableName || !dataSource) { setTargetTableColumns([]) + setTargetTableKeyColumns([]) return } const tbl = dbTables.find((t) => t.tableName === tableName) if (!tbl) return setTargetColsLoading(true) - sqlObjectManagerService - .getTableColumns(dataSource, tbl.schemaName, tbl.tableName) - .then((res) => setTargetTableColumns((res.data ?? []).map((c) => c.columnName))) - .catch(() => setTargetTableColumns([])) + const objectName = `[${tbl.schemaName}].[${tbl.tableName}]` + const keyColsQuery = [ + 'SELECT DISTINCT c.name AS columnName', + '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 c ON ic.object_id = c.object_id AND ic.column_id = c.column_id', + `WHERE i.object_id = OBJECT_ID('${objectName}')`, + ' AND ic.is_included_column = 0', + ' AND (i.is_primary_key = 1 OR i.is_unique = 1 OR i.is_unique_constraint = 1)', + 'ORDER BY c.name', + ].join('\n') + + Promise.all([ + sqlObjectManagerService.getTableColumns(dataSource, tbl.schemaName, tbl.tableName), + sqlObjectManagerService.executeQuery({ queryText: keyColsQuery, dataSourceCode: dataSource }), + ]) + .then(([colsRes, keyColsRes]) => { + const allCols = (colsRes.data ?? []).map((c) => c.columnName) + const keyCols = ((keyColsRes.data?.data ?? []) as any[]).map((r) => r.columnName) + setTargetTableColumns(allCols) + setTargetTableKeyColumns(keyCols) + }) + .catch(() => { + setTargetTableColumns([]) + setTargetTableKeyColumns([]) + }) .finally(() => setTargetColsLoading(false)) } @@ -1130,6 +1154,7 @@ const SqlTableDesignerDialog = ({ setEditingFkId(null) setFkForm(EMPTY_FK) setTargetTableColumns([]) + setTargetTableKeyColumns([]) setFkModalOpen(true) } @@ -1142,7 +1167,23 @@ const SqlTableDesignerDialog = ({ } const saveFk = () => { - if (!fkForm.fkColumnName.trim() || !fkForm.referencedTable.trim()) return + if (!fkForm.fkColumnName.trim() || !fkForm.referencedTable.trim() || !fkForm.referencedColumn.trim()) { + return + } + + const isTargetKeyColumn = targetTableKeyColumns.some( + (c) => c.toLowerCase() === fkForm.referencedColumn.trim().toLowerCase(), + ) + if (!isTargetKeyColumn) { + toast.push( + + Referans kolon PK/UNIQUE olmalı. Lütfen hedef tablodan anahtar bir kolon seçin. + , + { placement: 'top-center' }, + ) + return + } + if (editingFkId) { setRelationships((prev) => prev.map((r) => (r.id === editingFkId ? { ...fkForm, id: editingFkId } : r)), @@ -1289,6 +1330,7 @@ const SqlTableDesignerDialog = ({ setOriginalIndexes([]) setDbTables([]) setTargetTableColumns([]) + setTargetTableKeyColumns([]) setSelectedMenuCode('') setMenuAddDialogOpen(false) onClose() @@ -1299,8 +1341,8 @@ const SqlTableDesignerDialog = ({ const STEP_LABELS = [ translate('::App.SqlQueryManager.ColumnDesign'), translate('::App.SqlQueryManager.EntitySettings'), - translate('::App.SqlQueryManager.Relationships'), translate('::App.SqlQueryManager.IndexKeys'), + translate('::App.SqlQueryManager.Relationships'), translate('::App.SqlQueryManager.TSqlPreview'), ] @@ -1846,11 +1888,22 @@ const SqlTableDesignerDialog = ({ {translate('::App.SqlQueryManager.SelectTargetTableFirst')} {targetTableColumns.map((col) => ( - ))} + {fkForm.referencedTable && !targetColsLoading && targetTableKeyColumns.length === 0 && ( +

+ Seçilen tabloda FK için uygun PK/UNIQUE kolon bulunamadı. +

+ )} {/* Cascade */} @@ -1939,7 +1992,14 @@ const SqlTableDesignerDialog = ({