SQL Query Manager düzenlemesi

This commit is contained in:
Sedat ÖZTÜRK 2026-03-18 12:00:11 +03:00
parent 3baeee8002
commit ff58614f0d
4 changed files with 905 additions and 21 deletions

View file

@ -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",

View file

@ -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<FolderKey, { label: string; color: string }> = {
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<Set<string>>(new Set(['root']))
const [loading, setLoading] = useState(false)
const [filterText, setFilterText] = useState('')
const [selectedObjectIds, setSelectedObjectIds] = useState<Set<string>>(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<string>): 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 (
<div key={node.id}>
<div
@ -225,6 +306,15 @@ const SqlObjectExplorer = ({
onClick={() => handleNodeClick(node)}
onContextMenu={(e) => { e.preventDefault(); setContextMenu({ show: true, x: e.clientX, y: e.clientY, node }) }}
>
{node.type === 'object' && (
<input
type="checkbox"
className="h-4 w-4"
checked={isChecked}
onChange={(e) => toggleObjectSelection(node.id, e.target.checked)}
onClick={(e) => e.stopPropagation()}
/>
)}
{getIcon(node)}
<span className="text-sm flex-1 truncate">{node.label}</span>
{node.type === 'object' && (
@ -295,12 +385,27 @@ const SqlObjectExplorer = ({
>
{/* TABLE object <20> Design */}
{isTableObj && (
<button className="w-full px-4 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-700 text-sm flex items-center gap-2"
<button
className="w-full px-4 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-700 text-sm flex items-center gap-2"
onClick={() => {
const t = ctxNode!.data as DatabaseTableDto
onGenerateTableScript?.(t.schemaName, t.tableName)
closeCtx()
}}
>
<FaCode className="text-blue-600" /> Script olustur
</button>
)}
{isTableObj && (
<button
className="w-full px-4 py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-700 text-sm flex items-center gap-2"
onClick={() => {
const t = ctxNode!.data as DatabaseTableDto
onDesignTable?.(t.schemaName, t.tableName)
closeCtx()
}}>
}}
>
<FaTable className="text-teal-600" /> Design Table
</button>
)}

View file

@ -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<SqlEditorRef>(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<string[]>([])
const [overwriteIfExists, setOverwriteIfExists] = useState(false)
const [isCopyingObjects, setIsCopyingObjects] = useState(false)
const [copyResults, setCopyResults] = useState<SqlCopyResultItem[]>([])
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<string> => {
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<string, string> = {
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(
<Notification type="warning" title={translate('::App.Platform.Warning')}>
{translate('::App.Platform.ScriptNotGenerated') || 'Tablo scripti olusturulamadi.'}
</Notification>,
{ placement: 'top-center' },
)
return
}
setState((prev) => ({
...prev,
editorContent: script,
executionResult: null,
tableColumns: null,
isDirty: false,
}))
toast.push(
<Notification type="success" title={translate('::App.Platform.Success') || 'Basarili'}>
{'Script Query Editor alanina yuklendi.'}
</Notification>,
{ placement: 'top-center' },
)
} catch (error: any) {
toast.push(
<Notification type="danger" title={translate('::App.Platform.Error')}>
{error.response?.data?.error?.message || 'Tablo scripti olusturulurken hata olustu.'}
</Notification>,
{ placement: 'top-center' },
)
}
}
const handleOpenCopyDialog = () => {
if (!state.selectedDataSource) return
if (selectedExplorerObjects.length === 0) {
toast.push(
<Notification type="warning" title={translate('::App.Platform.Warning')}>
{'Lutfen kopyalamak icin en az bir obje secin.'}
</Notification>,
{ 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<string, string>()
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(
<Notification
type={notificationType}
title={
errorCount > 0
? translate('::App.Platform.Warning')
: translate('::App.Platform.Success')
}
>
{`Kopyalama tamamlandi. Basarili: ${successCount}, Atlanan: ${skippedCount}, Hata: ${errorCount}`}
</Notification>,
{ 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 (
<Container className="flex flex-col overflow-hidden" style={{ height: 'calc(100vh - 130px)' }}>
<Helmet
@ -425,6 +801,15 @@ GO`,
</div>
<div className="flex flex-wrap items-center gap-2 sm:gap-3">
<Button
size="sm"
variant="default"
onClick={handleOpenCopyDialog}
disabled={!state.selectedDataSource || selectedExplorerObjects.length === 0}
className="shadow-sm"
>
{translate('::App.Platform.CopySelectedObjects') || 'Copy Selected Objects'}
</Button>
<Button
size="sm"
variant="default"
@ -449,7 +834,6 @@ GO`,
</Button>
</div>
</div>
</div>
{/* 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}
/>
</div>
</div>
@ -583,6 +969,250 @@ GO`,
setState((prev) => ({ ...prev, refreshTrigger: prev.refreshTrigger + 1 }))
}}
/>
<Dialog
isOpen={showCopyDialog}
onClose={() => !isCopyingObjects && setShowCopyDialog(false)}
onRequestClose={() => !isCopyingObjects && setShowCopyDialog(false)}
contentClassName="max-h-[85vh] overflow-hidden"
>
<div className="flex h-full max-h-[85vh] flex-col">
<h5 className="mb-3">{translate('::App.Platform.CopySelectedObjects')}</h5>
<div className="flex-1 overflow-y-auto pr-1">
<div className="mb-2 flex items-center justify-between gap-4">
<p className="text-sm text-gray-600 dark:text-gray-400 mb-0">
{translate('::App.Platform.SourceDataSource')}:{' '}
<strong>{state.selectedDataSource}</strong>
</p>
<label className="flex items-center gap-2 text-sm cursor-pointer shrink-0">
<input
type="checkbox"
checked={overwriteIfExists}
onChange={(e) => setOverwriteIfExists(e.target.checked)}
disabled={isCopyingObjects}
/>
<span>{translate('::App.Platform.OverwriteIfExists')}</span>
</label>
</div>
<div className="mb-4 max-h-36 overflow-auto border rounded p-2">
{selectedExplorerObjects.map((obj) => (
<div key={obj.id} className="text-sm py-0.5">
{obj.objectType.toUpperCase()} - {obj.fullName}
</div>
))}
</div>
<div className="mb-4">
<div className="text-sm font-medium mb-2">
{translate('::App.Platform.TargetDataSources')}
</div>
<label className="flex items-center gap-2 text-sm cursor-pointer mb-2">
<input
type="checkbox"
checked={allTargetsSelected}
onChange={(e) => handleToggleSelectAllTargets(e.target.checked)}
/>
<span>{translate('::App.Platform.SelectAllTargets') || 'Tumunu sec'}</span>
</label>
<div className="max-h-44 overflow-auto border-t pt-2">
{availableTargetDataSourceCodes.map((code) => {
const checked = copyTargetDataSources.includes(code)
return (
<label key={code} className="flex items-center gap-2 text-sm cursor-pointer">
<input
type="checkbox"
checked={checked}
onChange={(e) => {
setCopyTargetDataSources((prev) => {
if (e.target.checked) return [...prev, code]
return prev.filter((x) => x !== code)
})
}}
/>
<span>{code}</span>
</label>
)
})}
</div>
</div>
</div>
<div className="mt-2 flex justify-end gap-2 border-t pt-3">
<Button
variant="plain"
onClick={() => setShowCopyDialog(false)}
disabled={isCopyingObjects}
>
{translate('::Cancel')}
</Button>
<Button
variant="solid"
onClick={handleCopyObjects}
loading={isCopyingObjects}
disabled={copyTargetDataSources.length === 0 || selectedExplorerObjects.length === 0}
>
{translate('::Copy')}
</Button>
</div>
</div>
</Dialog>
<Dialog
isOpen={showCopyResultDialog}
onClose={() => setShowCopyResultDialog(false)}
onRequestClose={() => setShowCopyResultDialog(false)}
width={1050}
contentClassName="max-h-[85vh] overflow-hidden"
>
<div className="flex h-full max-h-[85vh] flex-col">
<h5 className="mb-3">
{translate('::App.Platform.Results') || 'Kopyalama Sonuc Detaylari'}
</h5>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2 mb-3 text-xs sm:text-sm">
<div className="rounded border border-green-200 bg-green-50 px-3 py-2 text-green-700">
{translate('::App.Platform.Success')}: <strong>{copySuccessCount}</strong>
</div>
<div className="rounded border border-amber-200 bg-amber-50 px-3 py-2 text-amber-700">
{translate('::App.Platform.Skipped')}: <strong>{copySkippedCount}</strong>
</div>
<div className="rounded border border-red-200 bg-red-50 px-3 py-2 text-red-700">
{translate('::App.Platform.Error')}: <strong>{copyErrorCount}</strong>
</div>
</div>
<div className="flex-1 overflow-auto border rounded">
<div className="md:hidden p-2 space-y-2">
{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 (
<div
key={`${row.targetDataSource}-${row.objectFullName}-${idx}`}
className={`rounded border p-3 ${cardClass}`}
>
<div className="flex items-center justify-between gap-2 mb-2">
<span className="text-xs font-semibold uppercase text-gray-500">
{row.objectType}
</span>
{row.status === 'success' && (
<span className="inline-flex rounded-full bg-green-100 text-green-700 px-2 py-0.5 text-xs font-semibold">
{translate('::App.Platform.Success')}
</span>
)}
{row.status === 'error' && (
<span className="inline-flex rounded-full bg-red-100 text-red-700 px-2 py-0.5 text-xs font-semibold">
{translate('::App.Platform.Error')}
</span>
)}
{row.status === 'skipped' && (
<span className="inline-flex rounded-full bg-amber-100 text-amber-700 px-2 py-0.5 text-xs font-semibold">
{translate('::App.Platform.Skipped')}
</span>
)}
</div>
<div className="text-sm font-medium break-words mb-1">{row.objectFullName}</div>
<div className="text-xs text-gray-500 mb-2">Hedef: {row.targetDataSource}</div>
<div
className={`text-sm whitespace-normal break-words ${isError ? 'text-red-700 dark:text-red-300 font-medium' : 'text-gray-700 dark:text-gray-200'}`}
>
{row.message}
</div>
</div>
)
})}
{copyResults.length === 0 && (
<div className="px-3 py-8 text-center text-gray-500">
{translate('::App.Platform.NoResults')}
</div>
)}
</div>
<table className="hidden md:table w-full table-fixed text-sm">
<thead className="bg-gray-50 dark:bg-gray-700 sticky top-0 z-10">
<tr>
<th className="w-[110px] text-left px-3 py-2 border-b">{translate('::App.Platform.Status')}</th>
<th className="w-[270px] text-left px-3 py-2 border-b">{translate('::App.Platform.Object')}</th>
<th className="w-[90px] text-left px-3 py-2 border-b">{translate('::App.Platform.Type')}</th>
<th className="w-[140px] text-left px-3 py-2 border-b">{translate('::App.Platform.Target')}</th>
<th className="text-left px-3 py-2 border-b">{translate('::App.Platform.Message')}</th>
</tr>
</thead>
<tbody>
{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 (
<tr
key={`${row.targetDataSource}-${row.objectFullName}-${idx}`}
className={rowClass}
>
<td className="px-3 py-2 border-b align-top">
{row.status === 'success' && (
<span className="inline-flex rounded-full bg-green-100 text-green-700 px-2 py-0.5 text-xs font-semibold">
{translate('::App.Platform.Success')}
</span>
)}
{row.status === 'error' && (
<span className="inline-flex rounded-full bg-red-100 text-red-700 px-2 py-0.5 text-xs font-semibold">
{translate('::App.Platform.Error')}
</span>
)}
{row.status === 'skipped' && (
<span className="inline-flex rounded-full bg-amber-100 text-amber-700 px-2 py-0.5 text-xs font-semibold">
{translate('::App.Platform.Skipped')}
</span>
)}
</td>
<td
className="px-3 py-2 border-b align-top truncate"
title={row.objectFullName}
>
{row.objectFullName}
</td>
<td className="px-3 py-2 border-b align-top uppercase">{row.objectType}</td>
<td className="px-3 py-2 border-b align-top">{row.targetDataSource}</td>
<td
className={`px-3 py-2 border-b align-top whitespace-normal break-words leading-5 ${isError ? 'text-red-700 dark:text-red-300 font-medium' : 'text-gray-700 dark:text-gray-200'}`}
title={row.message}
>
{row.message}
</td>
</tr>
)
})}
{copyResults.length === 0 && (
<tr>
<td colSpan={5} className="px-3 py-8 text-center text-gray-500">
{translate('::App.Platform.NoResults')}
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
<div className="flex justify-end gap-2 mt-4">
<Button variant="solid" onClick={() => setShowCopyResultDialog(false)}>
Kapat
</Button>
</div>
</Dialog>
</Container>
)
}

View file

@ -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<Omit<SqlTableRelation, 'id'>>(EMPTY_FK)
const [dbTables, setDbTables] = useState<{ schemaName: string; tableName: string }[]>([])
const [targetTableColumns, setTargetTableColumns] = useState<string[]>([])
const [targetTableKeyColumns, setTargetTableKeyColumns] = useState<string[]>([])
const [targetColsLoading, setTargetColsLoading] = useState(false)
const [indexes, setIndexes] = useState<TableIndex[]>([])
const [originalIndexes, setOriginalIndexes] = useState<TableIndex[]>([])
@ -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(
<Notification type="warning" title={translate('::App.SqlQueryManager.Warning')}>
Referans kolon PK/UNIQUE olmalı. Lütfen hedef tablodan anahtar bir kolon seçin.
</Notification>,
{ 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')}
</option>
{targetTableColumns.map((col) => (
<option key={col} value={col}>
{col}
<option
key={col}
value={col}
disabled={!targetTableKeyColumns.some((k) => k.toLowerCase() === col.toLowerCase())}
>
{targetTableKeyColumns.some((k) => k.toLowerCase() === col.toLowerCase())
? `${col} (PK/UNIQUE)`
: `${col} (Not Key)`}
</option>
))}
</select>
{fkForm.referencedTable && !targetColsLoading && targetTableKeyColumns.length === 0 && (
<p className="mt-1 text-xs text-amber-600 dark:text-amber-400">
Seçilen tabloda FK için uygun PK/UNIQUE kolon bulunamadı.
</p>
)}
</div>
{/* Cascade */}
@ -1939,7 +1992,14 @@ const SqlTableDesignerDialog = ({
</button>
<button
onClick={saveFk}
disabled={!fkForm.fkColumnName.trim() || !fkForm.referencedTable.trim()}
disabled={
!fkForm.fkColumnName.trim() ||
!fkForm.referencedTable.trim() ||
!fkForm.referencedColumn.trim() ||
!targetTableKeyColumns.some(
(c) => c.toLowerCase() === fkForm.referencedColumn.trim().toLowerCase(),
)
}
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')}
@ -2327,8 +2387,8 @@ const SqlTableDesignerDialog = ({
<div className="min-h-[420px]">
{step === 0 && renderColumnDesigner()}
{step === 1 && renderEntitySettings()}
{step === 2 && renderRelationships()}
{step === 3 && renderIndexes()}
{step === 2 && renderIndexes()}
{step === 3 && renderRelationships()}
{step === 4 && renderSqlPreview()}
</div>