SQL Query Manager düzenlemesi
This commit is contained in:
parent
3baeee8002
commit
ff58614f0d
4 changed files with 905 additions and 21 deletions
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue