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",
|
"tr": "Hata",
|
||||||
"en": "Error"
|
"en": "Error"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"resourceName": "Platform",
|
||||||
|
"key": "App.Platform.Skipped",
|
||||||
|
"tr": "Atlandı",
|
||||||
|
"en": "Skipped"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"resourceName": "Platform",
|
"resourceName": "Platform",
|
||||||
"key": "App.Platform.Edit",
|
"key": "App.Platform.Edit",
|
||||||
|
|
@ -10404,6 +10410,36 @@
|
||||||
"tr": "Yeni Sorgu",
|
"tr": "Yeni Sorgu",
|
||||||
"en": "New Query"
|
"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",
|
"resourceName": "Platform",
|
||||||
"key": "App.Platform.Save",
|
"key": "App.Platform.Save",
|
||||||
|
|
@ -10698,6 +10734,30 @@
|
||||||
"tr": "Durum",
|
"tr": "Durum",
|
||||||
"en": "Status"
|
"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",
|
"resourceName": "Platform",
|
||||||
"key": "App.Platform.Category",
|
"key": "App.Platform.Category",
|
||||||
|
|
@ -16985,7 +17045,18 @@
|
||||||
"en": "Copy",
|
"en": "Copy",
|
||||||
"tr": "Kopyala"
|
"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",
|
"resourceName": "Platform",
|
||||||
"key": "App.SqlQueryManager.Cancel",
|
"key": "App.SqlQueryManager.Cancel",
|
||||||
|
|
@ -17035,6 +17106,24 @@
|
||||||
"en": "Success",
|
"en": "Success",
|
||||||
"tr": "Başarılı"
|
"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",
|
"resourceName": "Platform",
|
||||||
"key": "App.SqlQueryManager.TableUpdated",
|
"key": "App.SqlQueryManager.TableUpdated",
|
||||||
|
|
|
||||||
|
|
@ -31,11 +31,21 @@ interface SqlObjectExplorerProps {
|
||||||
dataSource: string | null
|
dataSource: string | null
|
||||||
onTemplateSelect?: (template: string, templateType: string) => void
|
onTemplateSelect?: (template: string, templateType: string) => void
|
||||||
onViewDefinition?: (schemaName: string, objectName: string) => void
|
onViewDefinition?: (schemaName: string, objectName: string) => void
|
||||||
|
onGenerateTableScript?: (schemaName: string, tableName: string) => void
|
||||||
onDesignTable?: (schemaName: string, tableName: string) => void
|
onDesignTable?: (schemaName: string, tableName: string) => void
|
||||||
onNewTable?: () => void
|
onNewTable?: () => void
|
||||||
|
onSelectedObjectsChange?: (objects: SqlExplorerSelectedObject[]) => void
|
||||||
refreshTrigger?: number
|
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 }> = {
|
const FOLDER_META: Record<FolderKey, { label: string; color: string }> = {
|
||||||
tables: { label: 'Tables', color: 'text-teal-500' },
|
tables: { label: 'Tables', color: 'text-teal-500' },
|
||||||
views: { label: 'Views', color: 'text-purple-500' },
|
views: { label: 'Views', color: 'text-purple-500' },
|
||||||
|
|
@ -47,8 +57,10 @@ const SqlObjectExplorer = ({
|
||||||
dataSource,
|
dataSource,
|
||||||
onTemplateSelect,
|
onTemplateSelect,
|
||||||
onViewDefinition,
|
onViewDefinition,
|
||||||
|
onGenerateTableScript,
|
||||||
onDesignTable,
|
onDesignTable,
|
||||||
onNewTable,
|
onNewTable,
|
||||||
|
onSelectedObjectsChange,
|
||||||
refreshTrigger,
|
refreshTrigger,
|
||||||
}: SqlObjectExplorerProps) => {
|
}: SqlObjectExplorerProps) => {
|
||||||
const { translate } = useLocalization()
|
const { translate } = useLocalization()
|
||||||
|
|
@ -56,6 +68,7 @@ const SqlObjectExplorer = ({
|
||||||
const [expandedNodes, setExpandedNodes] = useState<Set<string>>(new Set(['root']))
|
const [expandedNodes, setExpandedNodes] = useState<Set<string>>(new Set(['root']))
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [filterText, setFilterText] = useState('')
|
const [filterText, setFilterText] = useState('')
|
||||||
|
const [selectedObjectIds, setSelectedObjectIds] = useState<Set<string>>(new Set())
|
||||||
const [dropConfirm, setDropConfirm] = useState<{ node: TreeNode } | null>(null)
|
const [dropConfirm, setDropConfirm] = useState<{ node: TreeNode } | null>(null)
|
||||||
const [dropping, setDropping] = useState(false)
|
const [dropping, setDropping] = useState(false)
|
||||||
const [contextMenu, setContextMenu] = useState<{
|
const [contextMenu, setContextMenu] = useState<{
|
||||||
|
|
@ -63,10 +76,20 @@ const SqlObjectExplorer = ({
|
||||||
}>({ show: false, x: 0, y: 0, node: null })
|
}>({ show: false, x: 0, y: 0, node: null })
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (dataSource) loadObjects()
|
if (dataSource) {
|
||||||
else setTreeData([])
|
loadObjects()
|
||||||
|
} else {
|
||||||
|
setTreeData([])
|
||||||
|
setSelectedObjectIds(new Set())
|
||||||
|
onSelectedObjectsChange?.([])
|
||||||
|
}
|
||||||
}, [dataSource, refreshTrigger])
|
}, [dataSource, refreshTrigger])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const selected = getSelectedObjects(treeData, selectedObjectIds)
|
||||||
|
onSelectedObjectsChange?.(selected)
|
||||||
|
}, [treeData, selectedObjectIds, onSelectedObjectsChange])
|
||||||
|
|
||||||
const loadObjects = async () => {
|
const loadObjects = async () => {
|
||||||
if (!dataSource) return
|
if (!dataSource) return
|
||||||
setLoading(true)
|
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 => {
|
const buildDropSql = (node: TreeNode): string => {
|
||||||
if (node.folder === 'tables') {
|
if (node.folder === 'tables') {
|
||||||
const t = node.data as DatabaseTableDto
|
const t = node.data as DatabaseTableDto
|
||||||
|
|
@ -217,6 +297,7 @@ const SqlObjectExplorer = ({
|
||||||
|
|
||||||
const renderNode = (node: TreeNode, level = 0) => {
|
const renderNode = (node: TreeNode, level = 0) => {
|
||||||
const isExpanded = expandedNodes.has(node.id)
|
const isExpanded = expandedNodes.has(node.id)
|
||||||
|
const isChecked = selectedObjectIds.has(node.id)
|
||||||
return (
|
return (
|
||||||
<div key={node.id}>
|
<div key={node.id}>
|
||||||
<div
|
<div
|
||||||
|
|
@ -225,6 +306,15 @@ const SqlObjectExplorer = ({
|
||||||
onClick={() => handleNodeClick(node)}
|
onClick={() => handleNodeClick(node)}
|
||||||
onContextMenu={(e) => { e.preventDefault(); setContextMenu({ show: true, x: e.clientX, y: e.clientY, 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)}
|
{getIcon(node)}
|
||||||
<span className="text-sm flex-1 truncate">{node.label}</span>
|
<span className="text-sm flex-1 truncate">{node.label}</span>
|
||||||
{node.type === 'object' && (
|
{node.type === 'object' && (
|
||||||
|
|
@ -295,12 +385,27 @@ const SqlObjectExplorer = ({
|
||||||
>
|
>
|
||||||
{/* TABLE object <20> Design */}
|
{/* TABLE object <20> Design */}
|
||||||
{isTableObj && (
|
{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={() => {
|
onClick={() => {
|
||||||
const t = ctxNode!.data as DatabaseTableDto
|
const t = ctxNode!.data as DatabaseTableDto
|
||||||
onDesignTable?.(t.schemaName, t.tableName)
|
onDesignTable?.(t.schemaName, t.tableName)
|
||||||
closeCtx()
|
closeCtx()
|
||||||
}}>
|
}}
|
||||||
|
>
|
||||||
<FaTable className="text-teal-600" /> Design Table
|
<FaTable className="text-teal-600" /> Design Table
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import { sqlObjectManagerService } from '@/services/sql-query-manager.service'
|
||||||
import { FaDatabase, FaPlay, FaFileAlt } from 'react-icons/fa'
|
import { FaDatabase, FaPlay, FaFileAlt } from 'react-icons/fa'
|
||||||
import { FaCheckCircle } from 'react-icons/fa'
|
import { FaCheckCircle } from 'react-icons/fa'
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
import SqlObjectExplorer from './SqlObjectExplorer'
|
import SqlObjectExplorer, { type SqlExplorerSelectedObject } from './SqlObjectExplorer'
|
||||||
import SqlEditor, { SqlEditorRef } from './SqlEditor'
|
import SqlEditor, { SqlEditorRef } from './SqlEditor'
|
||||||
import SqlResultsGrid from './SqlResultsGrid'
|
import SqlResultsGrid from './SqlResultsGrid'
|
||||||
import SqlTableDesignerDialog from './SqlTableDesignerDialog'
|
import SqlTableDesignerDialog from './SqlTableDesignerDialog'
|
||||||
|
|
@ -29,6 +29,14 @@ interface SqlManagerState {
|
||||||
refreshTrigger: number
|
refreshTrigger: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface SqlCopyResultItem {
|
||||||
|
targetDataSource: string
|
||||||
|
objectFullName: string
|
||||||
|
objectType: SqlExplorerSelectedObject['objectType']
|
||||||
|
status: 'success' | 'error' | 'skipped'
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
|
||||||
const SqlQueryManager = () => {
|
const SqlQueryManager = () => {
|
||||||
const { translate } = useLocalization()
|
const { translate } = useLocalization()
|
||||||
const editorRef = useRef<SqlEditorRef>(null)
|
const editorRef = useRef<SqlEditorRef>(null)
|
||||||
|
|
@ -55,6 +63,15 @@ const SqlQueryManager = () => {
|
||||||
schemaName: string
|
schemaName: string
|
||||||
tableName: string
|
tableName: string
|
||||||
} | null>(null)
|
} | 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(() => {
|
useEffect(() => {
|
||||||
loadDataSources()
|
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 getTemplateContent = (templateType: string): string => {
|
||||||
const templates: Record<string, string> = {
|
const templates: Record<string, string> = {
|
||||||
select: `-- Basic SELECT query
|
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 (
|
return (
|
||||||
<Container className="flex flex-col overflow-hidden" style={{ height: 'calc(100vh - 130px)' }}>
|
<Container className="flex flex-col overflow-hidden" style={{ height: 'calc(100vh - 130px)' }}>
|
||||||
<Helmet
|
<Helmet
|
||||||
|
|
@ -425,6 +801,15 @@ GO`,
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-wrap items-center gap-2 sm:gap-3">
|
<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
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="default"
|
variant="default"
|
||||||
|
|
@ -449,7 +834,6 @@ GO`,
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Main Content Area */}
|
{/* Main Content Area */}
|
||||||
|
|
@ -466,9 +850,11 @@ GO`,
|
||||||
dataSource={state.selectedDataSource}
|
dataSource={state.selectedDataSource}
|
||||||
onTemplateSelect={handleTemplateSelect}
|
onTemplateSelect={handleTemplateSelect}
|
||||||
onViewDefinition={handleViewDefinition}
|
onViewDefinition={handleViewDefinition}
|
||||||
|
onGenerateTableScript={handleGenerateTableScript}
|
||||||
refreshTrigger={state.refreshTrigger}
|
refreshTrigger={state.refreshTrigger}
|
||||||
onNewTable={handleNewTable}
|
onNewTable={handleNewTable}
|
||||||
onDesignTable={handleDesignTable}
|
onDesignTable={handleDesignTable}
|
||||||
|
onSelectedObjectsChange={setSelectedExplorerObjects}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -583,6 +969,250 @@ GO`,
|
||||||
setState((prev) => ({ ...prev, refreshTrigger: prev.refreshTrigger + 1 }))
|
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>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -315,14 +315,14 @@ function generateCreateTableSql(
|
||||||
const lines: string[] = [
|
const lines: string[] = [
|
||||||
`/* ── Table: ${fullTableName} ── */`,
|
`/* ── Table: ${fullTableName} ── */`,
|
||||||
...(settings.entityName ? [`/* Entity Name: ${settings.entityName} */`] : []),
|
...(settings.entityName ? [`/* Entity Name: ${settings.entityName} */`] : []),
|
||||||
...(fkLines.length > 0 ? ['/* Foreign Key Constraints */'] : []),
|
|
||||||
'',
|
'',
|
||||||
`CREATE TABLE ${fullTableName}`,
|
`CREATE TABLE ${fullTableName}`,
|
||||||
`(`,
|
`(`,
|
||||||
...bodyLines,
|
...bodyLines,
|
||||||
`);`,
|
`);`,
|
||||||
...fkLines,
|
|
||||||
...indexLines,
|
...indexLines,
|
||||||
|
...(fkLines.length > 0 ? ['/* Foreign Key Constraints */'] : []),
|
||||||
|
...fkLines,
|
||||||
'',
|
'',
|
||||||
`/* Verify: SELECT TOP 10 * FROM ${fullTableName}; */`,
|
`/* Verify: SELECT TOP 10 * FROM ${fullTableName}; */`,
|
||||||
]
|
]
|
||||||
|
|
@ -603,7 +603,7 @@ function generateAlterTableSql(
|
||||||
return lines.join('\n')
|
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
|
type Step = 0 | 1 | 2 | 3 | 4
|
||||||
|
|
||||||
// ─── Simple Menu Tree (read-only selection) ───────────────────────────────────
|
// ─── Simple Menu Tree (read-only selection) ───────────────────────────────────
|
||||||
|
|
@ -787,6 +787,7 @@ const SqlTableDesignerDialog = ({
|
||||||
const [fkForm, setFkForm] = useState<Omit<SqlTableRelation, 'id'>>(EMPTY_FK)
|
const [fkForm, setFkForm] = useState<Omit<SqlTableRelation, 'id'>>(EMPTY_FK)
|
||||||
const [dbTables, setDbTables] = useState<{ schemaName: string; tableName: string }[]>([])
|
const [dbTables, setDbTables] = useState<{ schemaName: string; tableName: string }[]>([])
|
||||||
const [targetTableColumns, setTargetTableColumns] = useState<string[]>([])
|
const [targetTableColumns, setTargetTableColumns] = useState<string[]>([])
|
||||||
|
const [targetTableKeyColumns, setTargetTableKeyColumns] = useState<string[]>([])
|
||||||
const [targetColsLoading, setTargetColsLoading] = useState(false)
|
const [targetColsLoading, setTargetColsLoading] = useState(false)
|
||||||
const [indexes, setIndexes] = useState<TableIndex[]>([])
|
const [indexes, setIndexes] = useState<TableIndex[]>([])
|
||||||
const [originalIndexes, setOriginalIndexes] = useState<TableIndex[]>([])
|
const [originalIndexes, setOriginalIndexes] = useState<TableIndex[]>([])
|
||||||
|
|
@ -1114,15 +1115,38 @@ const SqlTableDesignerDialog = ({
|
||||||
const loadTargetColumns = (tableName: string) => {
|
const loadTargetColumns = (tableName: string) => {
|
||||||
if (!tableName || !dataSource) {
|
if (!tableName || !dataSource) {
|
||||||
setTargetTableColumns([])
|
setTargetTableColumns([])
|
||||||
|
setTargetTableKeyColumns([])
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const tbl = dbTables.find((t) => t.tableName === tableName)
|
const tbl = dbTables.find((t) => t.tableName === tableName)
|
||||||
if (!tbl) return
|
if (!tbl) return
|
||||||
setTargetColsLoading(true)
|
setTargetColsLoading(true)
|
||||||
sqlObjectManagerService
|
const objectName = `[${tbl.schemaName}].[${tbl.tableName}]`
|
||||||
.getTableColumns(dataSource, tbl.schemaName, tbl.tableName)
|
const keyColsQuery = [
|
||||||
.then((res) => setTargetTableColumns((res.data ?? []).map((c) => c.columnName)))
|
'SELECT DISTINCT c.name AS columnName',
|
||||||
.catch(() => setTargetTableColumns([]))
|
'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))
|
.finally(() => setTargetColsLoading(false))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1130,6 +1154,7 @@ const SqlTableDesignerDialog = ({
|
||||||
setEditingFkId(null)
|
setEditingFkId(null)
|
||||||
setFkForm(EMPTY_FK)
|
setFkForm(EMPTY_FK)
|
||||||
setTargetTableColumns([])
|
setTargetTableColumns([])
|
||||||
|
setTargetTableKeyColumns([])
|
||||||
setFkModalOpen(true)
|
setFkModalOpen(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1142,7 +1167,23 @@ const SqlTableDesignerDialog = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveFk = () => {
|
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) {
|
if (editingFkId) {
|
||||||
setRelationships((prev) =>
|
setRelationships((prev) =>
|
||||||
prev.map((r) => (r.id === editingFkId ? { ...fkForm, id: editingFkId } : r)),
|
prev.map((r) => (r.id === editingFkId ? { ...fkForm, id: editingFkId } : r)),
|
||||||
|
|
@ -1289,6 +1330,7 @@ const SqlTableDesignerDialog = ({
|
||||||
setOriginalIndexes([])
|
setOriginalIndexes([])
|
||||||
setDbTables([])
|
setDbTables([])
|
||||||
setTargetTableColumns([])
|
setTargetTableColumns([])
|
||||||
|
setTargetTableKeyColumns([])
|
||||||
setSelectedMenuCode('')
|
setSelectedMenuCode('')
|
||||||
setMenuAddDialogOpen(false)
|
setMenuAddDialogOpen(false)
|
||||||
onClose()
|
onClose()
|
||||||
|
|
@ -1299,8 +1341,8 @@ const SqlTableDesignerDialog = ({
|
||||||
const STEP_LABELS = [
|
const STEP_LABELS = [
|
||||||
translate('::App.SqlQueryManager.ColumnDesign'),
|
translate('::App.SqlQueryManager.ColumnDesign'),
|
||||||
translate('::App.SqlQueryManager.EntitySettings'),
|
translate('::App.SqlQueryManager.EntitySettings'),
|
||||||
translate('::App.SqlQueryManager.Relationships'),
|
|
||||||
translate('::App.SqlQueryManager.IndexKeys'),
|
translate('::App.SqlQueryManager.IndexKeys'),
|
||||||
|
translate('::App.SqlQueryManager.Relationships'),
|
||||||
translate('::App.SqlQueryManager.TSqlPreview'),
|
translate('::App.SqlQueryManager.TSqlPreview'),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -1846,11 +1888,22 @@ const SqlTableDesignerDialog = ({
|
||||||
{translate('::App.SqlQueryManager.SelectTargetTableFirst')}
|
{translate('::App.SqlQueryManager.SelectTargetTableFirst')}
|
||||||
</option>
|
</option>
|
||||||
{targetTableColumns.map((col) => (
|
{targetTableColumns.map((col) => (
|
||||||
<option key={col} value={col}>
|
<option
|
||||||
{col}
|
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>
|
</option>
|
||||||
))}
|
))}
|
||||||
</select>
|
</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>
|
</div>
|
||||||
|
|
||||||
{/* Cascade */}
|
{/* Cascade */}
|
||||||
|
|
@ -1939,7 +1992,14 @@ const SqlTableDesignerDialog = ({
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={saveFk}
|
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"
|
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')}
|
{translate('::App.SqlQueryManager.Save')}
|
||||||
|
|
@ -2327,8 +2387,8 @@ const SqlTableDesignerDialog = ({
|
||||||
<div className="min-h-[420px]">
|
<div className="min-h-[420px]">
|
||||||
{step === 0 && renderColumnDesigner()}
|
{step === 0 && renderColumnDesigner()}
|
||||||
{step === 1 && renderEntitySettings()}
|
{step === 1 && renderEntitySettings()}
|
||||||
{step === 2 && renderRelationships()}
|
{step === 2 && renderIndexes()}
|
||||||
{step === 3 && renderIndexes()}
|
{step === 3 && renderRelationships()}
|
||||||
{step === 4 && renderSqlPreview()}
|
{step === 4 && renderSqlPreview()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue