sozsoft-platform/ui/src/views/developerKit/SqlObjectExplorer.tsx

878 lines
32 KiB
TypeScript
Raw Blame History

import { useState, useEffect } from 'react'
import {
FaRegFolder,
FaRegFolderOpen,
FaColumns,
FaSyncAlt,
FaTable,
FaPlus,
FaEye,
FaCog,
FaCode,
FaDatabase,
FaTrash,
FaBolt,
FaCopy,
} from 'react-icons/fa'
import type { DatabaseTableDto, SqlNativeObjectDto } from '@/proxy/sql-query-manager/models'
import type { CrudEndpoint } from '@/proxy/developerKit/models'
import { DataSourceTypeEnum } from '@/proxy/form/models'
import { sqlObjectManagerService } from '@/services/sql-query-manager.service'
import { useLocalization } from '@/utils/hooks/useLocalization'
import Button from '@/components/ui/Button'
import { Notification, toast } from '@/components/ui'
import { CrudEndpointBadges, getErrorMessage, toEntityName } from './CrudEndpointDialog'
import { developerKitService } from '@/services/developerKit.service'
type FolderKey = 'tables' | 'views' | 'procedures' | 'functions'
interface TreeNode {
id: string
label: string
type: 'root' | 'folder' | 'object'
folder?: FolderKey
data?: DatabaseTableDto | SqlNativeObjectDto
children?: TreeNode[]
}
interface SqlObjectExplorerProps {
dataSource: string | null
dataSourceType?: DataSourceTypeEnum
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
/** Entity adina gore acilmis CRUD endpoint'leri; tablo satirlarinda ikon olarak gosterilir. */
crudEndpointsByEntity?: Map<string, CrudEndpoint[]>
onManageCrudEndpoints?: (schemaName: string, tableName: string) => void
/** Secili tablolar icin CRUD endpoint uretir; verilmezse buton gosterilmez. */
onGenerateCrudEndpoints?: () => void
isGeneratingCrudEndpoints?: boolean
/** Kopyalama / dogrudan SQL diyalogunu acar; verilmezse buton gosterilmez. */
onCopySelectedObjects?: () => void
/** Explorer endpoint'leri degistirdiginde (orn. tablo drop) listeyi tazelemek icin cagrilir. */
onCrudEndpointsChanged?: () => 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' },
procedures: { label: 'Stored Procedures', color: 'text-green-600' },
functions: { label: 'Functions', color: 'text-orange-500' },
}
const SqlObjectExplorer = ({
dataSource,
dataSourceType,
onTemplateSelect,
onViewDefinition,
onGenerateTableScript,
onDesignTable,
onNewTable,
onSelectedObjectsChange,
crudEndpointsByEntity,
onManageCrudEndpoints,
onGenerateCrudEndpoints,
isGeneratingCrudEndpoints,
onCopySelectedObjects,
onCrudEndpointsChanged,
refreshTrigger,
}: SqlObjectExplorerProps) => {
const { translate } = useLocalization()
const isPostgreSql = dataSourceType === DataSourceTypeEnum.Postgresql
const [treeData, setTreeData] = useState<TreeNode[]>([])
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())
/**
* Silinmek uzere onay bekleyen nesneler. Satirdaki cop kutusu tek nesneyi,
* baslikaki "secilenleri sil" tum secimi ayni listeye koyar; boylece tekil ve
* toplu silme tek kod yolundan gecer.
*/
const [dropTargets, setDropTargets] = useState<TreeNode[] | null>(null)
const [dropping, setDropping] = useState(false)
const [contextMenu, setContextMenu] = useState<{
show: boolean
x: number
y: number
node: TreeNode | null
}>({ show: false, x: 0, y: 0, node: null })
useEffect(() => {
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)
try {
const { data } = await sqlObjectManagerService.getAllObjects(dataSource)
const makeObjectNode = (folder: FolderKey, obj: SqlNativeObjectDto): TreeNode => ({
id: `${folder}-${obj.schemaName}-${obj.objectName}`,
label: obj.fullName ?? `[${obj.schemaName}].[${obj.objectName}]`,
type: 'object',
folder,
data: obj,
})
const tree: TreeNode[] = [
{
id: 'root',
label: dataSource,
type: 'root',
children: [
{
id: 'tables',
label: `Tables (${data.tables.length})`,
type: 'folder',
folder: 'tables',
children: data.tables.map((t) => ({
id: `tables-${t.schemaName}-${t.tableName}`,
label: t.fullName ?? `[${t.schemaName}].[${t.tableName}]`,
type: 'object' as const,
folder: 'tables' as FolderKey,
data: t,
})),
},
{
id: 'views',
label: `Views (${data.views.length})`,
type: 'folder',
folder: 'views',
children: data.views.map((v) => makeObjectNode('views', v)),
},
{
id: 'procedures',
label: `Stored Procedures (${data.storedProcedures.length})`,
type: 'folder',
folder: 'procedures',
children: data.storedProcedures.map((p) => makeObjectNode('procedures', p)),
},
{
id: 'functions',
label: `Functions (${data.functions.length})`,
type: 'folder',
folder: 'functions',
children: data.functions.map((f) => makeObjectNode('functions', f)),
},
],
},
]
setTreeData(tree)
} catch (error: any) {
console.error('Failed to load objects', error)
} finally {
setLoading(false)
}
}
const toggleNode = (nodeId: string) => {
setExpandedNodes((prev) => {
const next = new Set(prev)
if (next.has(nodeId)) {
next.delete(nodeId)
} else {
next.add(nodeId)
}
return next
})
}
const filterTree = (nodes: TreeNode[], search: string): TreeNode[] => {
if (!search.trim()) return nodes
const q = search.toLowerCase()
return nodes
.map((node) => {
const match = node.label.toLowerCase().includes(q)
const kids = node.children ? filterTree(node.children, search) : []
if (match || kids.length > 0)
return { ...node, children: kids.length > 0 ? kids : node.children } as TreeNode
return null
})
.filter(Boolean) as TreeNode[]
}
/**
* Tek tik nesneyi secer/secimi kaldirir (checkbox ile ayni state), klasorlerde acar/kapatir.
* Sorgu sablonu ya da tanim yuklemek icin cift tik kullanilir.
*/
const handleNodeClick = (node: TreeNode) => {
if (node.type !== 'object') {
toggleNode(node.id)
return
}
toggleObjectSelection(node.id, !selectedObjectIds.has(node.id))
}
const handleNodeDoubleClick = (node: TreeNode) => {
if (node.type !== 'object') return
if (node.folder === 'tables') {
// Generate SELECT template for tables
const t = node.data as DatabaseTableDto
const fullName =
t.fullName ??
(isPostgreSql ? `"${t.schemaName}"."${t.tableName}"` : `[${t.schemaName}].[${t.tableName}]`)
onTemplateSelect?.(
isPostgreSql
? `SELECT *\nFROM ${fullName}\nLIMIT 10;`
: `SELECT TOP 10 *\nFROM ${fullName};`,
'table-select',
)
} else {
// Load native object definition into editor
const obj = node.data as SqlNativeObjectDto
onViewDefinition?.(obj.schemaName, obj.objectName)
}
}
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 getSelectedNodes = (nodes: TreeNode[], ids: Set<string>): TreeNode[] => {
const selected: TreeNode[] = []
const walk = (list: TreeNode[]) => {
for (const node of list) {
if (node.type === 'object' && ids.has(node.id)) selected.push(node)
if (node.children?.length) walk(node.children)
}
}
walk(nodes)
return selected
}
const getSelectedObjects = (nodes: TreeNode[], ids: Set<string>): SqlExplorerSelectedObject[] =>
getSelectedNodes(nodes, ids)
.map(mapNodeToSelectedObject)
.filter((obj): obj is SqlExplorerSelectedObject => obj !== null)
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
const fullName =
t.fullName ??
(isPostgreSql ? `"${t.schemaName}"."${t.tableName}"` : `[${t.schemaName}].[${t.tableName}]`)
return isPostgreSql ? `DROP TABLE IF EXISTS ${fullName};` : `DROP TABLE ${fullName};`
}
const obj = node.data as SqlNativeObjectDto
const fullName =
obj.fullName ??
(isPostgreSql
? `"${obj.schemaName}"."${obj.objectName}"`
: `[${obj.schemaName}].[${obj.objectName}]`)
const keyword =
node.folder === 'views' ? 'VIEW' : node.folder === 'procedures' ? 'PROCEDURE' : 'FUNCTION'
return isPostgreSql ? `DROP ${keyword} IF EXISTS ${fullName};` : `DROP ${keyword} ${fullName};`
}
const getSqlDataFileCandidates = (node: TreeNode): string[] => {
if (node.folder === 'tables') {
const t = node.data as DatabaseTableDto
return [t.tableName, `${t.schemaName}_${t.tableName}`]
}
const obj = node.data as SqlNativeObjectDto
return [obj.objectName, `${obj.schemaName}_${obj.objectName}`]
}
/**
* Onaylanan nesneleri tek tek dusurur; bir nesnenin hatasi digerlerini engellemez.
* Basarili olanlar icin SQL seed dosyalari ve (tablolarda) CRUD endpoint'leri de temizlenir.
*/
const handleDrop = async () => {
if (!dropTargets?.length || !dataSource) return
setDropping(true)
const dropped: TreeNode[] = []
const failures: string[] = []
try {
for (const node of dropTargets) {
try {
const result = await sqlObjectManagerService.executeQuery({
queryText: buildDropSql(node),
dataSourceCode: dataSource,
})
if (result.data?.success === false) {
throw new Error(result.data.error || result.data.message)
}
dropped.push(node)
} catch (err) {
failures.push(
`${node.label}: ${getErrorMessage(err, translate('::App.Platform.IslemBasarisiz'))}`,
)
}
}
if (dropped.length > 0) {
// configs/seeds/SqlData altindaki eslesen seed dosyalari da silinir.
try {
const fileNames = [
...new Set(dropped.flatMap(getSqlDataFileCandidates).filter(Boolean)),
]
if (fileNames.length > 0) {
await sqlObjectManagerService.deleteSqlDataFiles({ fileNames })
}
} catch {
// Non-blocking: object drop succeeded even if seed file cleanup fails.
}
// Tablo dusurulduyse ona ait CRUD endpoint'leri de anlamsiz kalir; kayitlar
// silinince backend crud/{EntityName}.json seed dosyasini da kaldirir.
const droppedTables = dropped.filter((node) => node.folder === 'tables')
if (droppedTables.length > 0) {
for (const node of droppedTables) {
try {
const table = node.data as DatabaseTableDto
await developerKitService.deleteGeneratedEndpointsByEntity(
toEntityName(table.tableName),
)
} catch {
// Non-blocking: izin yoksa ya da endpoint yoksa drop yine basarilidir.
}
}
onCrudEndpointsChanged?.()
}
// Silinen nesneler secimde kalmasin; aksi halde toolbar sayaci yaniltir.
const droppedIds = new Set(dropped.map((node) => node.id))
setSelectedObjectIds((prev) => new Set([...prev].filter((id) => !droppedIds.has(id))))
}
toast.push(
<Notification
type={failures.length > 0 ? 'warning' : 'success'}
title={
failures.length > 0
? translate('::App.Platform.Warning')
: translate('::App.Platform.Success')
}
>
{failures.length > 0
? failures.join(' | ')
: `${translate('::App.Platform.OperationCompleted')} (${dropped.length})`}
</Notification>,
{ placement: 'bottom-end' },
)
setDropTargets(null)
loadObjects()
} finally {
setDropping(false)
}
}
const closeCtx = () => setContextMenu({ show: false, x: 0, y: 0, node: null })
const getIcon = (node: TreeNode) => {
if (node.type === 'root') return <FaDatabase className="text-blue-500" />
if (node.type === 'folder') {
const open = expandedNodes.has(node.id)
const cls = FOLDER_META[node.folder!]?.color ?? 'text-blue-500'
return open ? <FaRegFolderOpen className={cls} /> : <FaRegFolder className={cls} />
}
if (node.folder === 'tables') return <FaTable className="text-teal-500" />
if (node.folder === 'views') return <FaEye className="text-purple-500" />
if (node.folder === 'procedures') return <FaCog className="text-green-600" />
if (node.folder === 'functions') return <FaCode className="text-orange-500" />
return <FaColumns className="text-gray-400" />
}
const getNodeCrudEndpoints = (node: TreeNode): CrudEndpoint[] => {
if (node.type !== 'object' || node.folder !== 'tables' || !crudEndpointsByEntity) return []
const table = node.data as DatabaseTableDto
return crudEndpointsByEntity.get(toEntityName(table.tableName)) ?? []
}
const buildCrudEndpointTitle = (endpoint: CrudEndpoint) =>
`${endpoint.operationType}${endpoint.method} ${endpoint.path} (${
endpoint.isActive
? translate('::App.Listform.ListformField.IsActive')
: translate('::App.DeveloperKitCrudEndpoints.InactiveCount')
})`
const renderNode = (node: TreeNode, level = 0) => {
const isExpanded = expandedNodes.has(node.id)
const isChecked = selectedObjectIds.has(node.id)
const crudEndpoints = getNodeCrudEndpoints(node)
return (
<div key={node.id}>
<div
className={`group flex items-center gap-2 py-1 px-2 cursor-pointer rounded ${
isChecked
? 'bg-blue-50 dark:bg-blue-900/30 hover:bg-blue-100 dark:hover:bg-blue-900/40'
: 'hover:bg-gray-100 dark:hover:bg-gray-700'
}`}
style={{ paddingLeft: `${level * 16 + 8}px` }}
title={node.type === 'object' ? translate('::App.Platform.SelectOrOpenHint') : undefined}
onClick={() => handleNodeClick(node)}
onDoubleClick={() => handleNodeDoubleClick(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>
{crudEndpoints.length > 0 && (
<CrudEndpointBadges
endpoints={crudEndpoints}
buildTitle={buildCrudEndpointTitle}
onClick={
onManageCrudEndpoints
? () => {
const table = node.data as DatabaseTableDto
onManageCrudEndpoints(table.schemaName, table.tableName)
}
: undefined
}
/>
)}
{node.type === 'object' && (
<Button
title="Drop"
size="xs"
variant="plain"
shape="circle"
icon={<FaTrash className="text-xs" />}
className="opacity-0 group-hover:opacity-100 !h-6 !w-6 !px-0 hover:!bg-red-100 dark:hover:!bg-red-900 text-red-500 transition-opacity flex-shrink-0"
onClick={(e) => {
e.stopPropagation()
setDropTargets([node])
}}
/>
)}
</div>
{isExpanded && node.children && (
<div>{node.children.map((c) => renderNode(c, level + 1))}</div>
)}
</div>
)
}
const filteredTree = filterTree(treeData, filterText)
/** CRUD endpoint uretimi yalnizca tablolar icin anlamli. */
const selectedTableCount = getSelectedNodes(treeData, selectedObjectIds).filter(
(node) => node.folder === 'tables',
).length
useEffect(() => {
if (filterText.trim()) {
const allIds = new Set<string>()
const collect = (nodes: TreeNode[]) => {
for (const node of nodes) {
allIds.add(node.id)
if (node.children?.length) collect(node.children)
}
}
collect(filteredTree)
setExpandedNodes(allIds)
} else {
setExpandedNodes(new Set(['root']))
}
}, [filterText])
// Context menu items per folder
const ctxNode = contextMenu.node
const isTableObj = ctxNode?.type === 'object' && ctxNode.folder === 'tables'
const isNativeObj = ctxNode?.type === 'object' && ctxNode.folder !== 'tables'
const isTablesDir = ctxNode?.id === 'tables'
const isViewsDir = ctxNode?.id === 'views'
const isProcsDir = ctxNode?.id === 'procedures'
const isFuncsDir = ctxNode?.id === 'functions'
const isFolderNode = ctxNode?.type === 'folder'
return (
<div className="flex-1 flex flex-col min-h-0">
{/*
Nesne islemleri seridi. Yuksekligi sabit (min-h) — secim yapildikca
butonlar gorunse de agac asagi kaymaz.
*/}
<div className="flex min-h-[34px] items-center gap-2 border-b px-3 py-1 bg-gray-50 dark:bg-gray-800 flex-shrink-0 rounded-t-lg text-xs">
{selectedObjectIds.size > 0 && (
<>
<span className="font-medium text-blue-700 dark:text-blue-300 whitespace-nowrap">
{selectedObjectIds.size} {translate('::App.SqlQueryManager.Selected')}
</span>
<Button
size="xs"
variant="plain"
className="!h-auto !px-1 !py-0 text-xs text-blue-600 hover:underline dark:text-blue-300"
onClick={() => setSelectedObjectIds(new Set())}
>
{translate('::App.SqlQueryManager.ClearSelection')}
</Button>
</>
)}
<div className="ml-auto flex flex-wrap items-center justify-end gap-2">
{onCopySelectedObjects && selectedObjectIds.size > 0 && (
<Button
size="xs"
variant="plain"
icon={<FaCopy className="text-xs" />}
className="!h-auto !px-1 !py-0 text-xs text-gray-600 hover:underline dark:text-gray-300"
title={translate('::App.Platform.CopyOrExecuteSql')}
onClick={onCopySelectedObjects}
>
{translate('::App.Platform.CopySelectedObjects')}
</Button>
)}
{selectedObjectIds.size > 0 && (
<Button
size="xs"
variant="plain"
icon={<FaTrash className="text-xs" />}
className="!h-auto !px-1 !py-0 text-xs text-red-600 hover:underline dark:text-red-400"
onClick={() => setDropTargets(getSelectedNodes(treeData, selectedObjectIds))}
>
{translate('::App.SqlQueryManager.DropSelected')}
</Button>
)}
{onGenerateCrudEndpoints && selectedTableCount > 0 && (
<Button
size="xs"
variant="plain"
icon={<FaBolt className="text-xs" />}
loading={isGeneratingCrudEndpoints}
className="!h-auto !px-1 !py-0 text-xs text-amber-600 hover:underline dark:text-amber-400"
title={translate('::App.DeveloperKitCrudEndpoints.CreateCrudEndpointHint')}
onClick={onGenerateCrudEndpoints}
>
{translate('::App.DeveloperKitCrudEndpoints.CreateCrudEndpoint')}
</Button>
)}
</div>
</div>
{/* Search + refresh */}
<div className="p-2 border-b flex gap-2 flex-shrink-0">
<input
type="text"
placeholder={translate('::App.Platform.Search')}
value={filterText}
onChange={(e) => setFilterText(e.target.value)}
className="flex-1 px-3 py-1.5 text-sm border rounded-md bg-white dark:bg-gray-800 border-gray-300 dark:border-gray-700 text-gray-900 dark:text-gray-100"
/>
<Button
onClick={loadObjects}
disabled={loading || !dataSource}
size="sm"
variant="default"
shape="none"
icon={<FaSyncAlt className={loading ? 'animate-spin' : ''} />}
className="!rounded-md border-gray-300 dark:border-gray-700 hover:bg-gray-100 dark:hover:bg-gray-700 disabled:opacity-50 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100"
title={translate('::App.Platform.Refresh')}
/>
</div>
{/* Tree */}
<div className="flex-1 overflow-auto">
{loading && (
<div className="text-center py-8 text-gray-500 text-sm">
{translate('::App.Platform.LoadingWithThreeDot')}
</div>
)}
{!loading && treeData.length === 0 && (
<div className="text-center py-8 text-gray-500 text-sm">
{translate('::App.Platform.NoDataSourceSelected')}
</div>
)}
{!loading && filteredTree.length > 0 && (
<div className="p-1">{filteredTree.map((n) => renderNode(n))}</div>
)}
{!loading && treeData.length > 0 && filteredTree.length === 0 && (
<div className="text-center py-8 text-gray-500 text-sm">
{translate('::App.Platform.NoResults')}
</div>
)}
</div>
{/* Context menu */}
{contextMenu.show && (
<>
<div className="fixed inset-0 z-40" onClick={closeCtx} />
<div
className="fixed z-50 bg-white dark:bg-gray-800 shadow-lg rounded border border-gray-200 dark:border-gray-700 py-1 min-w-[180px]"
style={{ top: contextMenu.y, left: contextMenu.x }}
>
{/* TABLE object <20> Design */}
{isTableObj && (
<Button
variant="plain"
shape="none"
className="w-full !h-auto !justify-start !rounded-none !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
variant="plain"
shape="none"
className="w-full !h-auto !justify-start !rounded-none !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>
)}
{isTableObj && onManageCrudEndpoints && (
<Button
variant="plain"
shape="none"
className="w-full !h-auto !justify-start !rounded-none !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
onManageCrudEndpoints(t.schemaName, t.tableName)
closeCtx()
}}
>
<FaBolt className="text-amber-500" />{' '}
{translate('::App.SqlQueryManager.CrudEndpoints')}
</Button>
)}
{/* NATIVE object <20> View Definition */}
{isNativeObj && (
<Button
variant="plain"
shape="none"
className="w-full !h-auto !justify-start !rounded-none !px-4 !py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-700 text-sm flex items-center gap-2"
onClick={() => {
const obj = ctxNode!.data as SqlNativeObjectDto
onViewDefinition?.(obj.schemaName, obj.objectName)
closeCtx()
}}
>
{ctxNode!.folder === 'views' && <FaEye className="text-purple-500" />}
{ctxNode!.folder === 'procedures' && <FaCog className="text-green-600" />}
{ctxNode!.folder === 'functions' && <FaCode className="text-orange-500" />}
View Definition
</Button>
)}
{/* FOLDER <20> New ... */}
{isTablesDir && (
<Button
variant="plain"
shape="none"
className="w-full !h-auto !justify-start !rounded-none !px-4 !py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-700 text-sm flex items-center gap-2"
onClick={() => {
onNewTable?.()
closeCtx()
}}
>
<FaPlus className="text-teal-600" /> New Table
</Button>
)}
{isViewsDir && (
<Button
variant="plain"
shape="none"
className="w-full !h-auto !justify-start !rounded-none !px-4 !py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-700 text-sm flex items-center gap-2"
onClick={() => {
onTemplateSelect?.('', 'create-view')
closeCtx()
}}
>
<FaPlus className="text-purple-500" /> New View
</Button>
)}
{isProcsDir && (
<Button
variant="plain"
shape="none"
className="w-full !h-auto !justify-start !rounded-none !px-4 !py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-700 text-sm flex items-center gap-2"
onClick={() => {
onTemplateSelect?.('', 'create-procedure')
closeCtx()
}}
>
<FaPlus className="text-green-600" /> New Stored Procedure
</Button>
)}
{isFuncsDir && (
<>
<Button
variant="plain"
shape="none"
className="w-full !h-auto !justify-start !rounded-none !px-4 !py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-700 text-sm flex items-center gap-2"
onClick={() => {
onTemplateSelect?.('', 'create-scalar-function')
closeCtx()
}}
>
<FaPlus className="text-orange-500" /> New Scalar Function
</Button>
<Button
variant="plain"
shape="none"
className="w-full !h-auto !justify-start !rounded-none !px-4 !py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-700 text-sm flex items-center gap-2"
onClick={() => {
onTemplateSelect?.('', 'create-table-function')
closeCtx()
}}
>
<FaPlus className="text-orange-500" /> New Table-Valued Function
</Button>
</>
)}
{/* Separator + Refresh for folders */}
{isFolderNode && (
<>
<div className="my-1 border-t border-gray-100 dark:border-gray-700" />
<Button
variant="plain"
shape="none"
className="w-full !h-auto !justify-start !rounded-none !px-4 !py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-700 text-sm flex items-center gap-2"
onClick={() => {
loadObjects()
closeCtx()
}}
>
<FaSyncAlt /> {translate('::App.Platform.Refresh')}
</Button>
</>
)}
</div>
</>
)}
{/* Drop Confirm Dialog — tekil ve toplu silmede ayni diyalog */}
{dropTargets && dropTargets.length > 0 && (
<div
className="fixed inset-0 z-50 bg-black/40 flex items-center justify-center"
onClick={() => !dropping && setDropTargets(null)}
>
<div
className="bg-white dark:bg-gray-800 rounded-lg shadow-xl p-6 max-w-lg w-full mx-4"
onClick={(e) => e.stopPropagation()}
>
<div className="flex items-center gap-3 mb-3">
<FaTrash className="text-red-500 text-lg flex-shrink-0" />
<h6 className="font-semibold text-gray-900 dark:text-gray-100">
{translate('::App.SqlQueryManager.DropObjects')} ({dropTargets.length})
</h6>
</div>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-1">
{translate('::App.SqlQueryManager.DropObjectsWarning')}
</p>
<div className="mb-4 max-h-60 overflow-auto rounded bg-gray-100 dark:bg-gray-700">
{dropTargets.map((node) => (
<code key={node.id} className="block px-3 py-1 text-sm break-all">
{buildDropSql(node)}
</code>
))}
</div>
<div className="flex justify-end gap-2">
<Button
disabled={dropping}
size="sm"
variant="default"
className="disabled:opacity-50"
onClick={() => setDropTargets(null)}
>
{translate('::App.Platform.Cancel')}
</Button>
<Button
disabled={dropping}
size="sm"
variant="solid"
color="red-600"
className="hover:!bg-red-700 disabled:opacity-50 flex items-center gap-2"
onClick={handleDrop}
>
{dropping && <FaSyncAlt className="animate-spin text-xs" />}
{translate('::App.SqlQueryManager.Drop')}
</Button>
</div>
</div>
</div>
)}
</div>
)
}
export default SqlObjectExplorer