SqlViewDesignerDialog

This commit is contained in:
Sedat ÖZTÜRK 2026-08-19 10:50:33 +03:00
parent eb77285e59
commit ece4ee9783
16 changed files with 4007 additions and 69 deletions

View file

@ -20538,6 +20538,360 @@
"en": "No suitable PK/UNIQUE column was found in the selected table for the FK.", "en": "No suitable PK/UNIQUE column was found in the selected table for the FK.",
"tr": "Seçilen tabloda FK için uygun PK/UNIQUE kolon bulunamadı." "tr": "Seçilen tabloda FK için uygun PK/UNIQUE kolon bulunamadı."
}, },
{
"resourceName": "Platform",
"key": "App.SqlViewDesigner.Title",
"en": "View Designer",
"tr": "View Tasarımcısı"
},
{
"resourceName": "Platform",
"key": "App.SqlViewDesigner.Description",
"en": "Pick sources, build joins, filters and aggregations, then deploy with T-SQL",
"tr": "Kaynakları seçin, join / filtre / özet tanımlayın ve T-SQL ile deploy edin"
},
{
"resourceName": "Platform",
"key": "App.SqlViewDesigner.EditView",
"en": "Edit View",
"tr": "View Düzenle"
},
{
"resourceName": "Platform",
"key": "App.SqlViewDesigner.SearchObjects",
"en": "Search tables and views",
"tr": "Tablo ve view ara"
},
{
"resourceName": "Platform",
"key": "App.SqlViewDesigner.NoObjectsFound",
"en": "No matching object",
"tr": "Eşleşen nesne yok"
},
{
"resourceName": "Platform",
"key": "App.SqlViewDesigner.PaneDiagram",
"en": "Diagram pane",
"tr": "Diyagram paneli"
},
{
"resourceName": "Platform",
"key": "App.SqlViewDesigner.PaneCriteria",
"en": "Criteria pane",
"tr": "Kriter paneli"
},
{
"resourceName": "Platform",
"key": "App.SqlViewDesigner.PaneSql",
"en": "SQL pane",
"tr": "SQL paneli"
},
{
"resourceName": "Platform",
"key": "App.SqlViewDesigner.AddTable",
"en": "Add Table",
"tr": "Tablo Ekle"
},
{
"resourceName": "Platform",
"key": "App.SqlViewDesigner.EmptyCanvasHint",
"en": "Add a table or view from the left panel, then drag from a column handle to another column to create a join.",
"tr": "Soldaki panelden tablo veya view ekleyin; join kurmak için bir kolonun tutamacından diğer kolona tıklayın."
},
{
"resourceName": "Platform",
"key": "App.SqlViewDesigner.PendingLinkHint",
"en": "Pick the column to join with (Esc / click empty area to cancel)",
"tr": "Bağlanacak kolonu seçin (Esc / boş alana tıklayarak iptal)"
},
{
"resourceName": "Platform",
"key": "App.SqlViewDesigner.ToggleAllColumns",
"en": "Select / clear all columns",
"tr": "Tüm kolonları seç / temizle"
},
{
"resourceName": "Platform",
"key": "App.SqlViewDesigner.ShowSelectedOnly",
"en": "Show only selected columns",
"tr": "Yalnızca seçili kolonları göster"
},
{
"resourceName": "Platform",
"key": "App.SqlViewDesigner.ShowAllColumns",
"en": "Show all columns",
"tr": "Tüm kolonları göster"
},
{
"resourceName": "Platform",
"key": "App.SqlViewDesigner.JoinType",
"en": "Join type",
"tr": "Join tipi"
},
{
"resourceName": "Platform",
"key": "App.SqlViewDesigner.RemoveJoin",
"en": "Remove join",
"tr": "Join'i kaldır"
},
{
"resourceName": "Platform",
"key": "App.SqlViewDesigner.RemoveTable",
"en": "Remove table",
"tr": "Tabloyu kaldır"
},
{
"resourceName": "Platform",
"key": "App.SqlViewDesigner.TableColumn",
"en": "Table",
"tr": "Tablo"
},
{
"resourceName": "Platform",
"key": "App.SqlViewDesigner.Output",
"en": "Output",
"tr": ıktı"
},
{
"resourceName": "Platform",
"key": "App.SqlViewDesigner.GroupByColumn",
"en": "Group By",
"tr": "Group By"
},
{
"resourceName": "Platform",
"key": "App.SqlViewDesigner.SortType",
"en": "Sort Type",
"tr": "Sıralama Tipi"
},
{
"resourceName": "Platform",
"key": "App.SqlViewDesigner.SortOrder",
"en": "Sort Order",
"tr": "Sıra No"
},
{
"resourceName": "Platform",
"key": "App.SqlViewDesigner.Filter",
"en": "Filter",
"tr": "Filtre"
},
{
"resourceName": "Platform",
"key": "App.SqlViewDesigner.OrColumn",
"en": "Or...",
"tr": "Veya..."
},
{
"resourceName": "Platform",
"key": "App.SqlViewDesigner.AddRow",
"en": "Add row",
"tr": "Satır ekle"
},
{
"resourceName": "Platform",
"key": "App.SqlViewDesigner.FilterBuilder",
"en": "Filter builder",
"tr": "Filtre yardımcısı"
},
{
"resourceName": "Platform",
"key": "App.SqlViewDesigner.Apply",
"en": "Apply",
"tr": "Uygula"
},
{
"resourceName": "Platform",
"key": "App.SqlViewDesigner.GroupByToggle",
"en": "Toggle Group By (Σ)",
"tr": "Group By'ı aç / kapat (Σ)"
},
{
"resourceName": "Platform",
"key": "App.SqlViewDesigner.ResizeBox",
"en": "Drag to resize",
"tr": "Sürükleyerek boyutlandır"
},
{
"resourceName": "Platform",
"key": "App.SqlViewDesigner.AddExpression",
"en": "Add expression",
"tr": "İfade ekle"
},
{
"resourceName": "Platform",
"key": "App.SqlViewDesigner.ExpressionPlaceholder",
"en": "CASE WHEN … END",
"tr": "CASE WHEN … END"
},
{
"resourceName": "Platform",
"key": "App.SqlViewDesigner.AliasLocked",
"en": "Alias is locked: free expressions or subqueries reference it by name",
"tr": "Takma ad kilitli: serbest ifadeler veya alt sorgular bu adı metin olarak kullanıyor"
},
{
"resourceName": "Platform",
"key": "App.SqlViewDesigner.ValidationExpressionAlias",
"en": "Expression columns need an output name",
"tr": "İfade kolonlarına çıktı adı verilmelidir"
},
{
"resourceName": "Platform",
"key": "App.SqlViewDesigner.Alias",
"en": "Alias",
"tr": "Takma ad"
},
{
"resourceName": "Platform",
"key": "App.SqlViewDesigner.Column",
"en": "Column",
"tr": "Kolon"
},
{
"resourceName": "Platform",
"key": "App.SqlViewDesigner.OutputAlias",
"en": "Output name",
"tr": ıktı adı"
},
{
"resourceName": "Platform",
"key": "App.SqlViewDesigner.NoColumnSelected",
"en": "No column selected yet",
"tr": "Henüz kolon seçilmedi"
},
{
"resourceName": "Platform",
"key": "App.SqlViewDesigner.Value",
"en": "Value",
"tr": "Değer"
},
{
"resourceName": "Platform",
"key": "App.SqlViewDesigner.ValueModeLiteral",
"en": "Literal",
"tr": "Sabit değer"
},
{
"resourceName": "Platform",
"key": "App.SqlViewDesigner.ValueModeExpression",
"en": "SQL expression",
"tr": "SQL ifadesi"
},
{
"resourceName": "Platform",
"key": "App.SqlViewDesigner.Schema",
"en": "Schema",
"tr": "Şema"
},
{
"resourceName": "Platform",
"key": "App.SqlViewDesigner.ViewName",
"en": "View name",
"tr": "View adı"
},
{
"resourceName": "Platform",
"key": "App.SqlViewDesigner.NothingToGenerate",
"en": "Add at least one source and one column",
"tr": "En az bir kaynak ve bir kolon ekleyin"
},
{
"resourceName": "Platform",
"key": "App.SqlViewDesigner.OpenInEditor",
"en": "Open in Query Editor",
"tr": "Query Editor'da aç"
},
{
"resourceName": "Platform",
"key": "App.SqlViewDesigner.DeploySucceeded",
"en": "View deployed successfully",
"tr": "View başarıyla oluşturuldu"
},
{
"resourceName": "Platform",
"key": "App.SqlViewDesigner.DeployFailed",
"en": "View could not be deployed",
"tr": "View oluşturulamadı"
},
{
"resourceName": "Platform",
"key": "App.SqlViewDesigner.ParseFailedTitle",
"en": "This view cannot be opened in the visual designer",
"tr": "Bu view görsel tasarımcıda açılamıyor"
},
{
"resourceName": "Platform",
"key": "App.SqlViewDesigner.ParseFailedDescription",
"en": "Its definition uses SQL the designer does not model. Continue in the Query Editor, or redesign it from scratch.",
"tr": "Tanımı tasarımcının modellemediği bir SQL yapısı içeriyor. Query Editor'dan devam edebilir ya da sıfırdan tasarlayabilirsiniz."
},
{
"resourceName": "Platform",
"key": "App.SqlViewDesigner.RedesignFromScratch",
"en": "Redesign from scratch",
"tr": "Sıfırdan tasarla"
},
{
"resourceName": "Platform",
"key": "App.SqlViewDesigner.ValidationNoSource",
"en": "Add at least one source",
"tr": "En az bir kaynak ekleyin"
},
{
"resourceName": "Platform",
"key": "App.SqlViewDesigner.ValidationAliasRequired",
"en": "alias is required",
"tr": "takma ad zorunlu"
},
{
"resourceName": "Platform",
"key": "App.SqlViewDesigner.ValidationAliasDuplicate",
"en": "alias is used more than once",
"tr": "takma ad birden fazla kez kullanılmış"
},
{
"resourceName": "Platform",
"key": "App.SqlViewDesigner.ValidationJoinRequired",
"en": "at least one join condition is required",
"tr": "en az bir join koşulu gerekli"
},
{
"resourceName": "Platform",
"key": "App.SqlViewDesigner.ValidationNoColumn",
"en": "Select at least one column",
"tr": "En az bir kolon seçin"
},
{
"resourceName": "Platform",
"key": "App.SqlViewDesigner.ValidationColumnAliasDuplicate",
"en": "output name is used more than once",
"tr": ıktı adı birden fazla kez kullanılmış"
},
{
"resourceName": "Platform",
"key": "App.SqlViewDesigner.ValidationViewNameRequired",
"en": "View name is required",
"tr": "View adı zorunludur"
},
{
"resourceName": "Platform",
"key": "App.SqlViewDesigner.ValidationViewNameInvalid",
"en": "View name contains invalid characters",
"tr": "View adı geçersiz karakter içeriyor"
},
{
"resourceName": "Platform",
"key": "App.DeveloperKitTableDesigner.ColumnTemplates",
"en": "Column Templates",
"tr": "Kolon Şablonları"
},
{
"resourceName": "Platform",
"key": "App.DeveloperKitTableDesigner.ColumnTemplatesHint",
"en": "Pick a ready-made column set to append",
"tr": "Eklemek istediğiniz hazır kolon setini seçin"
},
{ {
"resourceName": "Platform", "resourceName": "Platform",
"key": "App.DeveloperKitTableDesigner.OneToMany", "key": "App.DeveloperKitTableDesigner.OneToMany",

View file

@ -48,6 +48,10 @@ interface SqlObjectExplorerProps {
onGenerateTableScript?: (schemaName: string, tableName: string) => void onGenerateTableScript?: (schemaName: string, tableName: string) => void
onDesignTable?: (schemaName: string, tableName: string) => void onDesignTable?: (schemaName: string, tableName: string) => void
onNewTable?: () => void onNewTable?: () => void
/** Gorsel View Designer'i yeni view icin acar; verilmezse SQL sablonu kullanilir. */
onNewView?: () => void
/** Mevcut bir view'i gorsel tasarimcida acar. */
onDesignView?: (schemaName: string, objectName: string) => void
onSelectedObjectsChange?: (objects: SqlExplorerSelectedObject[]) => void onSelectedObjectsChange?: (objects: SqlExplorerSelectedObject[]) => void
/** Entity adina gore acilmis CRUD endpoint'leri; tablo/view satirlarinda ikon olarak gosterilir. */ /** Entity adina gore acilmis CRUD endpoint'leri; tablo/view satirlarinda ikon olarak gosterilir. */
crudEndpointsByEntity?: Map<string, CrudEndpoint[]> crudEndpointsByEntity?: Map<string, CrudEndpoint[]>
@ -89,6 +93,8 @@ const SqlObjectExplorer = ({
onGenerateTableScript, onGenerateTableScript,
onDesignTable, onDesignTable,
onNewTable, onNewTable,
onNewView,
onDesignView,
onSelectedObjectsChange, onSelectedObjectsChange,
crudEndpointsByEntity, crudEndpointsByEntity,
onManageCrudEndpoints, onManageCrudEndpoints,
@ -377,9 +383,7 @@ const SqlObjectExplorer = ({
if (dropped.length > 0) { if (dropped.length > 0) {
// configs/seeds/SqlData altindaki eslesen seed dosyalari da silinir. // configs/seeds/SqlData altindaki eslesen seed dosyalari da silinir.
try { try {
const fileNames = [ const fileNames = [...new Set(dropped.flatMap(getSqlDataFileCandidates).filter(Boolean))]
...new Set(dropped.flatMap(getSqlDataFileCandidates).filter(Boolean)),
]
if (fileNames.length > 0) { if (fileNames.length > 0) {
await sqlObjectManagerService.deleteSqlDataFiles({ fileNames }) await sqlObjectManagerService.deleteSqlDataFiles({ fileNames })
} }
@ -730,6 +734,21 @@ const SqlObjectExplorer = ({
</Button> </Button>
)} )}
{isNativeObj && ctxNode!.folder === 'views' && onDesignView && (
<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
onDesignView(obj.schemaName, obj.objectName)
closeCtx()
}}
>
<FaEye className="text-purple-500" /> Design View
</Button>
)}
{ctxCrudObjectType && onManageCrudEndpoints && ( {ctxCrudObjectType && onManageCrudEndpoints && (
<Button <Button
variant="plain" variant="plain"
@ -785,7 +804,8 @@ const SqlObjectExplorer = ({
shape="none" 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" 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={() => { onClick={() => {
onTemplateSelect?.('', 'create-view') if (onNewView) onNewView()
else onTemplateSelect?.('', 'create-view')
closeCtx() closeCtx()
}} }}
> >

View file

@ -36,6 +36,7 @@ import { usePermission } from '@/utils/hooks/usePermission'
const SqlResultsGrid = lazy(() => import('./SqlResultsGrid')) const SqlResultsGrid = lazy(() => import('./SqlResultsGrid'))
const SqlTableDesignerDialog = lazy(() => import('./SqlTableDesignerDialog')) const SqlTableDesignerDialog = lazy(() => import('./SqlTableDesignerDialog'))
const SqlViewDesignerDialog = lazy(() => import('./SqlViewDesignerDialog'))
interface SqlManagerState { interface SqlManagerState {
dataSources: DataSourceDto[] dataSources: DataSourceDto[]
@ -85,6 +86,11 @@ const SqlQueryManager = () => {
schemaName: string schemaName: string
tableName: string tableName: string
} | null>(null) } | null>(null)
const [showViewDesignerDialog, setShowViewDesignerDialog] = useState(false)
const [designViewData, setDesignViewData] = useState<{
schemaName: string
objectName: string
} | null>(null)
const [selectedExplorerObjects, setSelectedExplorerObjects] = useState< const [selectedExplorerObjects, setSelectedExplorerObjects] = useState<
SqlExplorerSelectedObject[] SqlExplorerSelectedObject[]
>([]) >([])
@ -238,6 +244,16 @@ const SqlQueryManager = () => {
setShowTableDesignerDialog(true) setShowTableDesignerDialog(true)
}, []) }, [])
const handleNewView = useCallback(() => {
setDesignViewData(null)
setShowViewDesignerDialog(true)
}, [])
const handleDesignView = useCallback((schemaName: string, objectName: string) => {
setDesignViewData({ schemaName, objectName })
setShowViewDesignerDialog(true)
}, [])
const handleEditorChange = useCallback((value: string | undefined) => { const handleEditorChange = useCallback((value: string | undefined) => {
setState((prev) => ({ setState((prev) => ({
...prev, ...prev,
@ -862,8 +878,7 @@ GO`,
objectType: obj.objectType, objectType: obj.objectType,
status: 'error', status: 'error',
message: message:
error.response?.data?.error?.message || error.response?.data?.error?.message || translate('::App.Platform.IslemBasarisiz'),
translate('::App.Platform.IslemBasarisiz'),
}) })
} }
} }
@ -941,8 +956,7 @@ GO`,
objectType: 'script', objectType: 'script',
status: 'error', status: 'error',
message: message:
error.response?.data?.error?.message || error.response?.data?.error?.message || translate('::App.Platform.IslemBasarisiz'),
translate('::App.Platform.IslemBasarisiz'),
}) })
} }
} }
@ -1141,6 +1155,8 @@ GO`,
refreshTrigger={state.refreshTrigger} refreshTrigger={state.refreshTrigger}
onNewTable={handleNewTable} onNewTable={handleNewTable}
onDesignTable={handleDesignTable} onDesignTable={handleDesignTable}
onNewView={handleNewView}
onDesignView={handleDesignView}
onSelectedObjectsChange={setSelectedExplorerObjects} onSelectedObjectsChange={setSelectedExplorerObjects}
crudEndpointsByEntity={crudEndpointsByEntity} crudEndpointsByEntity={crudEndpointsByEntity}
onManageCrudEndpoints={ onManageCrudEndpoints={
@ -1207,7 +1223,11 @@ GO`,
</div> </div>
</div> </div>
<div className="flex-1 overflow-hidden p-2"> <div className="flex-1 overflow-hidden p-2">
<Suspense fallback={<div className="p-4">{translate('::App.Platform.LoadingWithThreeDot')}</div>}> <Suspense
fallback={
<div className="p-4">{translate('::App.Platform.LoadingWithThreeDot')}</div>
}
>
<SqlResultsGrid <SqlResultsGrid
result={state.executionResult} result={state.executionResult}
queryText={state.lastExecutedQuery} queryText={state.lastExecutedQuery}
@ -1288,6 +1308,36 @@ GO`,
</Suspense> </Suspense>
)} )}
{/* View Designer Dialog */}
{showViewDesignerDialog && (
<Suspense fallback={null}>
<SqlViewDesignerDialog
isOpen
onClose={() => {
setShowViewDesignerDialog(false)
setDesignViewData(null)
}}
dataSource={state.selectedDataSource ?? ''}
isPostgreSql={isPostgreSql}
initialViewData={designViewData}
onOpenInEditor={(sql) => {
setState((prev) => ({
...prev,
editorContent: sql,
executionResult: null,
isDirty: false,
}))
}}
onDeployed={async () => {
setState((prev) => ({ ...prev, refreshTrigger: prev.refreshTrigger + 1 }))
if (canManageCrudEndpoints) {
await loadCrudEndpoints()
}
}}
/>
</Suspense>
)}
{/* CRUD Endpoint Dialog */} {/* CRUD Endpoint Dialog */}
{crudDialogObject && canManageCrudEndpoints && ( {crudDialogObject && canManageCrudEndpoints && (
<CrudEndpointDialog <CrudEndpointDialog

View file

@ -1,4 +1,4 @@
import React, { useState, useCallback, useMemo, useEffect } from 'react' import React, { useState, useCallback, useMemo, useEffect, useRef } from 'react'
import { createPortal } from 'react-dom' import { createPortal } from 'react-dom'
import { Button, Dialog, Notification, toast, Checkbox } from '@/components/ui' import { Button, Dialog, Notification, toast, Checkbox } from '@/components/ui'
import { import {
@ -16,6 +16,14 @@ import {
FaChevronDown, FaChevronDown,
FaChevronRight, FaChevronRight,
FaKey, FaKey,
FaLayerGroup,
FaRegClipboard,
FaUsers,
FaHistory,
FaProjectDiagram,
FaTasks,
FaChartBar,
FaRegCalendarAlt,
} from 'react-icons/fa' } from 'react-icons/fa'
import { sqlObjectManagerService } from '@/services/sql-query-manager.service' import { sqlObjectManagerService } from '@/services/sql-query-manager.service'
import { getMenus } from '@/services/menu.service' import { getMenus } from '@/services/menu.service'
@ -252,8 +260,8 @@ const WORKFLOW_COLUMNS: ColumnDefinition[] = [
description: 'Workflow primary key', description: 'Workflow primary key',
}, },
{ {
id: '__ApprovalUserId', id: '__UserId',
columnName: 'ApprovalUserName', columnName: 'UserName',
dataType: 'nvarchar', dataType: 'nvarchar',
maxLength: '256', maxLength: '256',
isNullable: true, isNullable: true,
@ -261,8 +269,8 @@ const WORKFLOW_COLUMNS: ColumnDefinition[] = [
description: 'Workflow approval user name', description: 'Workflow approval user name',
}, },
{ {
id: '__ApprovalStatus', id: '__Status',
columnName: 'ApprovalStatus', columnName: 'Status',
dataType: 'nvarchar', dataType: 'nvarchar',
maxLength: '50', maxLength: '50',
isNullable: true, isNullable: true,
@ -270,8 +278,8 @@ const WORKFLOW_COLUMNS: ColumnDefinition[] = [
description: 'Workflow approval status', description: 'Workflow approval status',
}, },
{ {
id: '__ApprovalDate', id: '__Date',
columnName: 'ApprovalDate', columnName: 'Date',
dataType: 'datetime', dataType: 'datetime',
maxLength: '', maxLength: '',
isNullable: true, isNullable: true,
@ -279,8 +287,8 @@ const WORKFLOW_COLUMNS: ColumnDefinition[] = [
description: 'Workflow approval date', description: 'Workflow approval date',
}, },
{ {
id: '__ApprovalDescription', id: '__Description',
columnName: 'ApprovalDescription', columnName: 'Description',
dataType: 'nvarchar', dataType: 'nvarchar',
maxLength: '200', maxLength: '200',
isNullable: true, isNullable: true,
@ -1007,9 +1015,7 @@ function generateAlterTableSql(
if (changed) { if (changed) {
hasChanges = true hasChanges = true
const cname = orig.constraintName ?? `FK_${tableName}_${orig.fkColumnName}` const cname = orig.constraintName ?? `FK_${tableName}_${orig.fkColumnName}`
lines.push( lines.push(`-- ✏️ ${translate('::App.TableDesignerDiff.UpdateFk', { name: cname })}`)
`-- ✏️ ${translate('::App.TableDesignerDiff.UpdateFk', { name: cname })}`,
)
lines.push(`ALTER TABLE ${fullTableName}`) lines.push(`ALTER TABLE ${fullTableName}`)
lines.push(` DROP CONSTRAINT [${cname}];`) lines.push(` DROP CONSTRAINT [${cname}];`)
lines.push('') lines.push('')
@ -1030,15 +1036,11 @@ function generateAlterTableSql(
const dropIndexSql = (ix: TableIndex) => { const dropIndexSql = (ix: TableIndex) => {
if (ix.indexType === 'PrimaryKey' || ix.indexType === 'UniqueKey') { if (ix.indexType === 'PrimaryKey' || ix.indexType === 'UniqueKey') {
lines.push( lines.push(`-- ❌ ${translate('::App.TableDesignerDiff.DropIndex', { name: ix.indexName })}`)
`-- ❌ ${translate('::App.TableDesignerDiff.DropIndex', { name: ix.indexName })}`,
)
lines.push(`ALTER TABLE ${fullTableName}`) lines.push(`ALTER TABLE ${fullTableName}`)
lines.push(` DROP CONSTRAINT [${ix.indexName}];`) lines.push(` DROP CONSTRAINT [${ix.indexName}];`)
} else { } else {
lines.push( lines.push(`-- ❌ ${translate('::App.TableDesignerDiff.DropIndex', { name: ix.indexName })}`)
`-- ❌ ${translate('::App.TableDesignerDiff.DropIndex', { name: ix.indexName })}`,
)
lines.push(`DROP INDEX [${ix.indexName}] ON ${fullTableName};`) lines.push(`DROP INDEX [${ix.indexName}] ON ${fullTableName};`)
} }
lines.push('') lines.push('')
@ -1315,6 +1317,9 @@ const SqlTableDesignerDialog = ({
const [columns, setColumns] = useState<ColumnDefinition[]>([createEmptyColumn()]) const [columns, setColumns] = useState<ColumnDefinition[]>([createEmptyColumn()])
const [originalColumns, setOriginalColumns] = useState<ColumnDefinition[]>([]) const [originalColumns, setOriginalColumns] = useState<ColumnDefinition[]>([])
const [colsLoading, setColsLoading] = useState(false) const [colsLoading, setColsLoading] = useState(false)
/** Hazir kolon sablonlari panelinin acik/kapali durumu. */
const [templatesOpen, setTemplatesOpen] = useState(false)
const templatesRef = useRef<HTMLDivElement | null>(null)
const [settings, setSettings] = useState<TableSettings>(DEFAULT_SETTINGS) const [settings, setSettings] = useState<TableSettings>(DEFAULT_SETTINGS)
const [rawMenuItems, setRawMenuItems] = useState<MenuItem[]>([]) const [rawMenuItems, setRawMenuItems] = useState<MenuItem[]>([])
const [menuTree, setMenuTree] = useState<MenuTreeNode[]>([]) const [menuTree, setMenuTree] = useState<MenuTreeNode[]>([])
@ -2077,6 +2082,7 @@ const SqlTableDesignerDialog = ({
setTargetTableKeyColumns([]) setTargetTableKeyColumns([])
setSelectedMenuCode('') setSelectedMenuCode('')
setMenuAddDialogOpen(false) setMenuAddDialogOpen(false)
setTemplatesOpen(false)
setGenerateCrud(false) setGenerateCrud(false)
setCrudOperations([...CRUD_OPERATION_TYPES]) setCrudOperations([...CRUD_OPERATION_TYPES])
onClose() onClose()
@ -2125,6 +2131,83 @@ const SqlTableDesignerDialog = ({
// ── Step 0: Column Designer ──────────────────────────────────────────────── // ── Step 0: Column Designer ────────────────────────────────────────────────
/** Hazir kolon setleri; buton kalabaligi yerine tek bir sablon panelinde toplanir. */
const columnTemplates: {
key: string
label: string
hint: string
icon: React.ReactNode
color: string
apply: () => void
}[] = [
{
key: 'multiTenant',
label: translate('::App.SqlQueryManager.AddMultiTenantColumns'),
hint: 'TenantId',
icon: <FaUsers />,
color: 'bg-emerald-500',
apply: addMultiTenantColumns,
},
{
key: 'fullAudited',
label: translate('::App.SqlQueryManager.AddFullAuditedColumns'),
hint: 'Creation / Modification / Deletion',
icon: <FaHistory />,
color: 'bg-blue-500',
apply: addFullAuditedColumns,
},
{
key: 'workflow',
label: translate('::App.SqlQueryManager.AddWorkflowColumns'),
hint: 'Workflow state columns',
icon: <FaProjectDiagram />,
color: 'bg-indigo-500',
apply: addWorkflowColumns,
},
{
key: 'todo',
label: 'Todo Columns',
hint: 'Status / Priority / DueDate',
icon: <FaTasks />,
color: 'bg-cyan-500',
apply: addTodoColumns,
},
{
key: 'gantt',
label: 'Gantt Columns',
hint: 'Start / End / Progress',
icon: <FaChartBar />,
color: 'bg-violet-500',
apply: addGanttColumns,
},
{
key: 'scheduler',
label: 'Scheduler Columns',
hint: 'Event start / end / recurrence',
icon: <FaRegCalendarAlt />,
color: 'bg-teal-500',
apply: addSchedulerColumns,
},
]
useEffect(() => {
if (!templatesOpen) return
const onDocClick = (e: MouseEvent) => {
if (templatesRef.current && !templatesRef.current.contains(e.target as Node)) {
setTemplatesOpen(false)
}
}
const onEsc = (e: KeyboardEvent) => {
if (e.key === 'Escape') setTemplatesOpen(false)
}
document.addEventListener('mousedown', onDocClick)
document.addEventListener('keydown', onEsc)
return () => {
document.removeEventListener('mousedown', onDocClick)
document.removeEventListener('keydown', onEsc)
}
}, [templatesOpen])
const renderColumnDesigner = () => ( const renderColumnDesigner = () => (
<div className="flex flex-col"> <div className="flex flex-col">
{colsLoading && ( {colsLoading && (
@ -2135,47 +2218,81 @@ const SqlTableDesignerDialog = ({
)} )}
{!colsLoading && ( {!colsLoading && (
<> <>
<div className="flex items-start justify-between gap-2 py-2"> <div className="flex flex-wrap items-center justify-between gap-2 py-2">
<div className="flex flex-col gap-2"> <div className="flex items-center gap-2">
<div className="flex flex-wrap items-center gap-2"> <div className="relative" ref={templatesRef}>
<Button size="xs" variant="solid" color="green-600" onClick={addMultiTenantColumns}>
{translate('::App.SqlQueryManager.AddMultiTenantColumns')}
</Button>
<Button size="xs" variant="solid" color="blue-600" onClick={addFullAuditedColumns}>
{translate('::App.SqlQueryManager.AddFullAuditedColumns')}
</Button>
<Button <Button
size="xs" size="xs"
variant="solid" variant="solid"
color="amber-600" color="blue-600"
icon={<FaLayerGroup />}
onClick={() => setTemplatesOpen((o) => !o)}
>
<span className="inline-flex items-center gap-1.5">
{translate('::App.DeveloperKitTableDesigner.ColumnTemplates')}
<FaChevronDown
className={`text-[9px] transition-transform ${templatesOpen ? 'rotate-180' : ''}`}
/>
</span>
</Button>
{templatesOpen && (
<div className="absolute left-0 top-full z-50 mt-1.5 w-[440px] max-w-[85vw] rounded-lg border border-gray-200 bg-white p-2 shadow-xl dark:border-gray-700 dark:bg-gray-800">
<div className="px-1.5 pb-1.5 text-[10px] font-semibold uppercase tracking-wider text-gray-400">
{translate('::App.DeveloperKitTableDesigner.ColumnTemplatesHint')}
</div>
<div className="grid grid-cols-2 gap-0.5">
{columnTemplates.map((tpl) => (
<button
key={tpl.key}
type="button"
className="flex items-start gap-2 rounded-md p-2 text-left transition-colors hover:bg-gray-100 dark:hover:bg-gray-700/60"
onClick={() => {
tpl.apply()
setTemplatesOpen(false)
}}
>
<span
className={`mt-0.5 flex h-6 w-6 shrink-0 items-center justify-center rounded-md text-[11px] text-white ${tpl.color}`}
>
{tpl.icon}
</span>
<span className="min-w-0">
<span className="block truncate text-xs font-semibold text-gray-700 dark:text-gray-100">
{tpl.label}
</span>
<span className="block truncate text-[10px] leading-snug text-gray-400">
{tpl.hint}
</span>
</span>
</button>
))}
</div>
</div>
)}
</div>
<Button
size="xs"
variant="plain"
icon={<FaRegClipboard />}
onClick={importColumnsFromRememberedCreateTable} onClick={importColumnsFromRememberedCreateTable}
disabled={isEditMode} disabled={isEditMode}
> >
{translate('::App.DeveloperKitTableDesigner.PasteCreateTableColumns')} {translate('::App.DeveloperKitTableDesigner.PasteCreateTableColumns')}
</Button> </Button>
</div> </div>
<div className="flex flex-wrap items-center gap-2">
<Button size="xs" variant="solid" color="indigo-600" onClick={addWorkflowColumns}> <div className="flex items-center gap-2">
{translate('::App.SqlQueryManager.AddWorkflowColumns')} <span className="rounded-full bg-gray-100 px-2 py-0.5 text-[10px] font-medium text-gray-500 dark:bg-gray-700 dark:text-gray-300">
</Button> {columns.length} {translate('::App.SqlQueryManager.ColumnName')}
<Button size="xs" variant="solid" color="cyan-600" onClick={addTodoColumns}> </span>
Add Todo Columns
</Button>
<Button size="xs" variant="solid" color="violet-600" onClick={addGanttColumns}>
Add Gantt Columns
</Button>
<Button size="xs" variant="solid" color="teal-600" onClick={addSchedulerColumns}>
Add Scheduler Columns
</Button>
</div>
</div>
<div className="flex flex-col items-stretch gap-2">
<Button <Button
size="xs" size="xs"
variant="solid" variant="plain"
color="red-600"
icon={<FaTrash />} icon={<FaTrash />}
onClick={clearAllColumns} onClick={clearAllColumns}
title={translate('::App.SqlQueryManager.ClearAllColumns')}
> >
{translate('::App.SqlQueryManager.ClearAllColumns')} {translate('::App.SqlQueryManager.ClearAllColumns')}
</Button> </Button>
@ -2678,7 +2795,9 @@ const SqlTableDesignerDialog = ({
<div> <div>
<label className="block text-xs font-semibold text-gray-700 dark:text-gray-300 mb-1.5"> <label className="block text-xs font-semibold text-gray-700 dark:text-gray-300 mb-1.5">
{translate('::App.SqlQueryManager.TargetColumn')} {translate('::App.SqlQueryManager.TargetColumn')}
{targetColsLoading ? `${translate('::App.Platform.LoadingWithThreeDot')}` : ''} {targetColsLoading
? `${translate('::App.Platform.LoadingWithThreeDot')}`
: ''}
</label> </label>
<select <select
value={fkForm.referencedColumn} value={fkForm.referencedColumn}
@ -2843,7 +2962,8 @@ const SqlTableDesignerDialog = ({
<div className="space-y-3"> <div className="space-y-3">
{indexesLoading && ( {indexesLoading && (
<div className="flex items-center justify-center py-10 text-gray-500 text-sm gap-2"> <div className="flex items-center justify-center py-10 text-gray-500 text-sm gap-2">
<span className="animate-spin">&#9696;</span> {translate('::App.Platform.LoadingWithThreeDot')} <span className="animate-spin">&#9696;</span>{' '}
{translate('::App.Platform.LoadingWithThreeDot')}
</div> </div>
)} )}
{!indexesLoading && ( {!indexesLoading && (
@ -3203,9 +3323,7 @@ const SqlTableDesignerDialog = ({
size="xs" size="xs"
variant="plain" variant="plain"
className="!h-auto !px-1 !py-0 text-xs text-blue-500 hover:underline" className="!h-auto !px-1 !py-0 text-xs text-blue-500 hover:underline"
onClick={() => onClick={() => setCrudOperations(allCrudSelected ? [] : [...CRUD_OPERATION_TYPES])}
setCrudOperations(allCrudSelected ? [] : [...CRUD_OPERATION_TYPES])
}
> >
{allCrudSelected {allCrudSelected
? translate('::App.SqlQueryManager.ClearSelection') ? translate('::App.SqlQueryManager.ClearSelection')
@ -3332,9 +3450,7 @@ const SqlTableDesignerDialog = ({
!dataSource || !dataSource ||
isDeploying || isDeploying ||
(isEditMode && (isEditMode &&
generatedSql.includes( generatedSql.includes(translate('::App.TableDesignerDiff.NoChanges')))
translate('::App.TableDesignerDiff.NoChanges'),
))
} }
> >
{translate('::App.Platform.Deploy')} {translate('::App.Platform.Deploy')}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,472 @@
/**
* View Designer Criteria grid (SSMS Query Designer'in orta bolumu).
*
* Sutunlar: Column · Alias · Table · Output · Group By · Sort Type · Sort Order
* · Filter · Or... Filtre hucreleri serbest yuklemdir ("> 100", "LIKE '%a%'");
* yaninda kucuk bir yardimci ile de doldurulabilir.
*/
import { useState } from 'react'
import { FaCode, FaGripVertical, FaMagic, FaPlus, FaTrash } from 'react-icons/fa'
import type { DatabaseColumnDto } from '@/proxy/sql-query-manager/models'
import {
GROUP_MODES,
GROUP_MODE_LABEL,
isAggregateMode,
orColumnCount,
quoteLiteral,
type CriteriaRow,
type GroupMode,
type ViewSource,
} from './viewModel'
interface CriteriaGridProps {
rows: CriteriaRow[]
sources: ViewSource[]
columnsOf: (sourceId: string) => DatabaseColumnDto[]
grouped: boolean
onUpdateRow: (id: string, patch: Partial<CriteriaRow>) => void
onUpdateFilter: (id: string, index: number, value: string) => void
onRemoveRow: (id: string) => void
onMoveRow: (id: string, delta: number) => void
onAddRow: () => void
onAddExpression: () => void
labels: {
column: string
alias: string
table: string
output: string
groupBy: string
sortType: string
sortOrder: string
filter: string
or: string
addRow: string
addExpression: string
expressionPlaceholder: string
empty: string
builder: string
builderValue: string
builderLiteral: string
builderExpression: string
apply: string
}
}
const OPERATORS = [
'=',
'<>',
'>',
'>=',
'<',
'<=',
'LIKE',
'NOT LIKE',
'IN',
'NOT IN',
'IS NULL',
'IS NOT NULL',
'BETWEEN',
]
export function CriteriaGrid({
rows,
sources,
columnsOf,
grouped,
onUpdateRow,
onUpdateFilter,
onRemoveRow,
onMoveRow,
onAddRow,
onAddExpression,
labels,
}: CriteriaGridProps) {
const [builder, setBuilder] = useState<{ rowId: string; index: number } | null>(null)
const orCount = orColumnCount(rows)
const cellCls =
'w-full rounded border border-transparent bg-transparent px-1 py-0.5 text-xs hover:border-gray-300 focus:border-blue-500 focus:bg-white dark:text-gray-100 dark:hover:border-gray-600 dark:focus:bg-gray-700'
return (
<div className="flex h-full flex-col overflow-hidden rounded-lg border border-gray-200 dark:border-gray-700">
<div className="flex items-center gap-2 border-b border-gray-200 bg-gray-50 px-2 py-1 dark:border-gray-700 dark:bg-gray-800">
<button
type="button"
className="flex items-center gap-1 rounded px-1.5 py-0.5 text-[11px] text-blue-600 hover:bg-blue-50 dark:hover:bg-blue-900/30"
onClick={onAddRow}
disabled={sources.length === 0}
>
<FaPlus className="text-[9px]" /> {labels.addRow}
</button>
<button
type="button"
className="flex items-center gap-1 rounded px-1.5 py-0.5 text-[11px] text-indigo-600 hover:bg-indigo-50 dark:hover:bg-indigo-900/30"
onClick={onAddExpression}
>
<FaCode className="text-[9px]" /> {labels.addExpression}
</button>
<span className="text-[10px] text-gray-400">{rows.length}</span>
</div>
<div className="flex-1 overflow-auto">
<table className="w-full border-collapse text-xs">
<thead className="sticky top-0 z-10 bg-gray-100 text-[10px] font-semibold uppercase tracking-wide text-gray-500 dark:bg-gray-800 dark:text-gray-300">
<tr>
<th className="w-6 border border-gray-200 px-1 py-1 dark:border-gray-700" />
<th className="min-w-[150px] border border-gray-200 px-1 py-1 text-left dark:border-gray-700">
{labels.column}
</th>
<th className="min-w-[110px] border border-gray-200 px-1 py-1 text-left dark:border-gray-700">
{labels.alias}
</th>
<th className="min-w-[90px] border border-gray-200 px-1 py-1 text-left dark:border-gray-700">
{labels.table}
</th>
<th className="w-14 border border-gray-200 px-1 py-1 dark:border-gray-700">
{labels.output}
</th>
{grouped && (
<th className="min-w-[110px] border border-gray-200 px-1 py-1 text-left dark:border-gray-700">
{labels.groupBy}
</th>
)}
<th className="w-[86px] border border-gray-200 px-1 py-1 text-left dark:border-gray-700">
{labels.sortType}
</th>
<th className="w-[70px] border border-gray-200 px-1 py-1 text-left dark:border-gray-700">
{labels.sortOrder}
</th>
{Array.from({ length: orCount }).map((_, i) => (
<th
key={i}
className="min-w-[130px] border border-gray-200 px-1 py-1 text-left dark:border-gray-700"
>
{i === 0 ? labels.filter : labels.or}
</th>
))}
<th className="w-8 border border-gray-200 px-1 py-1 dark:border-gray-700" />
</tr>
</thead>
<tbody>
{rows.length === 0 && (
<tr>
<td
className="border border-gray-200 px-2 py-6 text-center text-xs text-gray-400 dark:border-gray-700"
colSpan={(grouped ? 9 : 8) + orCount}
>
{labels.empty}
</td>
</tr>
)}
{rows.map((row) => {
const isAgg = grouped && isAggregateMode(row.groupMode)
const isExpression = row.expression !== undefined
return (
<tr
key={row.id}
className={
row.output ? 'bg-white dark:bg-gray-900' : 'bg-gray-50/70 dark:bg-gray-800/40'
}
>
<td className="border border-gray-200 px-0.5 text-center dark:border-gray-700">
<div className="flex flex-col items-center leading-none">
<button
type="button"
className="text-[8px] text-gray-300 hover:text-blue-500"
onClick={() => onMoveRow(row.id, -1)}
>
</button>
<FaGripVertical className="text-[8px] text-gray-300" />
<button
type="button"
className="text-[8px] text-gray-300 hover:text-blue-500"
onClick={() => onMoveRow(row.id, 1)}
>
</button>
</div>
</td>
<td className="border border-gray-200 px-0.5 dark:border-gray-700">
{isExpression ? (
<input
className={`${cellCls} font-mono text-[11px] text-indigo-700 dark:text-indigo-300`}
placeholder={labels.expressionPlaceholder}
value={row.expression ?? ''}
title={row.expression}
onChange={(e) => onUpdateRow(row.id, { expression: e.target.value })}
/>
) : (
<select
className={cellCls}
value={row.columnName}
onChange={(e) => onUpdateRow(row.id, { columnName: e.target.value })}
>
<option value=""></option>
{columnsOf(row.sourceId).map((c) => (
<option key={c.columnName} value={c.columnName}>
{c.columnName}
</option>
))}
</select>
)}
</td>
<td className="border border-gray-200 px-0.5 dark:border-gray-700">
<input
className={cellCls}
placeholder={row.columnName}
value={row.alias}
onChange={(e) => onUpdateRow(row.id, { alias: e.target.value })}
/>
</td>
<td className="border border-gray-200 px-0.5 dark:border-gray-700">
{isExpression ? (
<span className="px-1 text-[10px] italic text-gray-400">SQL</span>
) : (
<select
className={cellCls}
value={row.sourceId}
onChange={(e) =>
onUpdateRow(row.id, { sourceId: e.target.value, columnName: '' })
}
>
{sources.map((s) => (
<option key={s.id} value={s.id}>
{s.alias || s.objectName}
</option>
))}
</select>
)}
</td>
<td className="border border-gray-200 text-center dark:border-gray-700">
<input
type="checkbox"
className="h-3.5 w-3.5"
checked={row.output}
disabled={row.groupMode === 'Where'}
onChange={(e) => onUpdateRow(row.id, { output: e.target.checked })}
/>
</td>
{grouped && (
<td className="border border-gray-200 px-0.5 dark:border-gray-700">
<select
className={cellCls}
value={row.groupMode}
disabled={isExpression}
onChange={(e) => {
const mode = e.target.value as GroupMode
onUpdateRow(row.id, {
groupMode: mode,
...(mode === 'Where' ? { output: false } : {}),
})
}}
>
{GROUP_MODES.map((m) => (
<option key={m} value={m}>
{GROUP_MODE_LABEL[m]}
</option>
))}
</select>
</td>
)}
<td className="border border-gray-200 px-0.5 dark:border-gray-700">
<select
className={cellCls}
value={row.sortType}
onChange={(e) => {
const sortType = e.target.value as '' | 'ASC' | 'DESC'
onUpdateRow(row.id, {
sortType,
sortOrder: sortType
? row.sortOrder || rows.filter((r) => r.sortType).length + 1
: 0,
})
}}
>
<option value=""></option>
<option value="ASC">Ascending</option>
<option value="DESC">Descending</option>
</select>
</td>
<td className="border border-gray-200 px-0.5 dark:border-gray-700">
<input
type="number"
min={0}
className={cellCls}
value={row.sortOrder || ''}
disabled={!row.sortType}
onChange={(e) =>
onUpdateRow(row.id, { sortOrder: Number(e.target.value) || 0 })
}
/>
</td>
{Array.from({ length: orCount }).map((_, i) => (
<td
key={i}
className="relative border border-gray-200 px-0.5 dark:border-gray-700"
>
<div className="flex items-center gap-0.5">
<input
className={cellCls}
placeholder={i === 0 ? "> 100 · = 'A' · IS NULL" : ''}
value={row.filters[i] ?? ''}
onChange={(e) => onUpdateFilter(row.id, i, e.target.value)}
/>
<button
type="button"
className="shrink-0 text-[9px] text-gray-300 hover:text-blue-500"
title={labels.builder}
onClick={() =>
setBuilder(
builder?.rowId === row.id && builder.index === i
? null
: { rowId: row.id, index: i },
)
}
>
<FaMagic />
</button>
</div>
{builder?.rowId === row.id && builder.index === i && (
<FilterBuilder
labels={labels}
onApply={(text) => {
onUpdateFilter(row.id, i, text)
setBuilder(null)
}}
onClose={() => setBuilder(null)}
/>
)}
{isAgg && (row.filters[i] ?? '').trim() && (
<span className="pointer-events-none absolute -top-1 right-6 rounded bg-indigo-100 px-1 text-[8px] font-semibold text-indigo-700 dark:bg-indigo-900/50 dark:text-indigo-300">
HAVING
</span>
)}
</td>
))}
<td className="border border-gray-200 text-center dark:border-gray-700">
<button
type="button"
className="text-[10px] text-gray-400 hover:text-red-500"
onClick={() => onRemoveRow(row.id)}
>
<FaTrash />
</button>
</td>
</tr>
)
})}
</tbody>
</table>
</div>
</div>
)
}
// ─── Filtre yardimcisi ────────────────────────────────────────────────────────
function FilterBuilder({
labels,
onApply,
onClose,
}: {
labels: CriteriaGridProps['labels']
onApply: (text: string) => void
onClose: () => void
}) {
const [operator, setOperator] = useState('=')
const [value, setValue] = useState('')
const [value2, setValue2] = useState('')
const [mode, setMode] = useState<'literal' | 'expression'>('literal')
const needsValue = operator !== 'IS NULL' && operator !== 'IS NOT NULL'
const build = () => {
if (!needsValue) return operator
const render = (v: string) => (mode === 'literal' ? quoteLiteral(v.trim()) : v.trim())
if (operator === 'BETWEEN') return `BETWEEN ${render(value)} AND ${render(value2)}`
if (operator === 'IN' || operator === 'NOT IN') {
if (mode === 'expression') return `${operator} (${value.trim()})`
const items = value
.split(',')
.map((v) => v.trim())
.filter(Boolean)
.map(quoteLiteral)
return `${operator} (${items.join(', ')})`
}
return `${operator} ${render(value)}`
}
return (
<div
className="absolute left-0 top-full z-50 mt-1 w-[230px] rounded-lg border border-gray-200 bg-white p-2 shadow-xl dark:border-gray-700 dark:bg-gray-800"
onClick={(e) => e.stopPropagation()}
>
<select
className="mb-1 w-full rounded border px-1 py-1 text-xs dark:border-gray-600 dark:bg-gray-700 dark:text-white"
value={operator}
onChange={(e) => setOperator(e.target.value)}
>
{OPERATORS.map((op) => (
<option key={op} value={op}>
{op}
</option>
))}
</select>
{needsValue && (
<>
<input
className="mb-1 w-full rounded border px-1 py-1 text-xs dark:border-gray-600 dark:bg-gray-700 dark:text-white"
placeholder={labels.builderValue}
value={value}
onChange={(e) => setValue(e.target.value)}
/>
{operator === 'BETWEEN' && (
<input
className="mb-1 w-full rounded border px-1 py-1 text-xs dark:border-gray-600 dark:bg-gray-700 dark:text-white"
placeholder={labels.builderValue}
value={value2}
onChange={(e) => setValue2(e.target.value)}
/>
)}
<select
className="mb-1 w-full rounded border px-1 py-1 text-[11px] dark:border-gray-600 dark:bg-gray-700 dark:text-white"
value={mode}
onChange={(e) => setMode(e.target.value as 'literal' | 'expression')}
>
<option value="literal">{labels.builderLiteral}</option>
<option value="expression">{labels.builderExpression}</option>
</select>
</>
)}
<div className="flex items-center gap-1">
<button
type="button"
className="flex-1 rounded bg-blue-600 px-1 py-1 text-[11px] font-semibold text-white hover:bg-blue-700"
onClick={() => onApply(build())}
>
{labels.apply}
</button>
<button
type="button"
className="rounded px-2 py-1 text-[11px] text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-700"
onClick={onClose}
>
</button>
</div>
</div>
)
}
export default CriteriaGrid

View file

@ -0,0 +1,726 @@
/**
* View Designer Diagram paneli (SSMS Query Designer'in ust bolumu).
*
* Tablolar surukle-birak ile konumlandirilir (WorkflowCanvas ile ayni
* `@dnd-kit/core` deseni), kolonlarin yanindaki tutamaca tiklanip hedef kolona
* tiklanarak JOIN kurulur, join cizgisine tiklanarak tipi degistirilir.
*
* Kutular sabit yukseklikte tutulur; kolon sayisi fazlaysa kutu icinde
* kaydirilir ve alt kenardan yukseklik ayarlanabilir. Kaydirilan kolonun capa
* noktasi kutu kenarina kenetlenir, boylece cizgi her zaman kutuya baglanir.
*/
import { useCallback, useMemo, useRef, useState } from 'react'
import { useDraggable } from '@dnd-kit/core'
import { CSS } from '@dnd-kit/utilities'
import classNames from 'classnames'
import {
FaChevronDown,
FaChevronRight,
FaCompressAlt,
FaExpandAlt,
FaEye,
FaLayerGroup,
FaTable,
FaTimes,
} from 'react-icons/fa'
import type { DatabaseColumnDto } from '@/proxy/sql-query-manager/models'
import {
BOX_FOOTER_HEIGHT,
BOX_HEADER_HEIGHT,
BOX_MIN_ROWS,
BOX_PADDING,
BOX_ROW_HEIGHT,
BOX_WIDTH,
JOIN_KINDS,
JOIN_OPERATORS,
boxHeight,
isDerivedSource,
type JoinKind,
type JoinOperator,
type ViewSource,
} from './viewModel'
export interface PendingLink {
sourceId: string
columnName: string
}
/** Diagramda cizilecek tek bir join kosulu. */
interface JoinLine {
key: string
/** Join'i tasiyan (sag taraftaki) kaynak. */
sourceId: string
conditionId: string
leftSourceId: string
leftColumn: string
rightColumn: string
operator: JoinOperator
joinKind: JoinKind
}
interface DiagramPaneProps {
sources: ViewSource[]
columnsOf: (sourceId: string) => DatabaseColumnDto[]
loadingSourceIds: Set<string>
/** Bir kolon ciktiya secili mi. */
isColumnSelected: (sourceId: string, columnName: string) => boolean
zoom: number
dragPreview: { id: string; delta: { x: number; y: number } } | null
selectedJoinKey: string | null
pendingLink: PendingLink | null
/** Ifade/alt sorgu metinleri alias'lara metin olarak baglidir; kilitli tutulur. */
lockAliases: boolean
onToggleColumn: (sourceId: string, columnName: string) => void
onToggleAllColumns: (sourceId: string) => void
onRemoveSource: (sourceId: string) => void
onUpdateSource: (sourceId: string, patch: Partial<ViewSource>) => void
onBeginLink: (link: PendingLink | null) => void
onCompleteLink: (target: PendingLink) => void
onSelectJoin: (key: string | null) => void
onUpdateJoin: (sourceId: string, conditionId: string, patch: { operator?: JoinOperator }) => void
onRemoveJoin: (sourceId: string, conditionId: string) => void
emptyHint: string
pendingHint: string
labels: {
alias: string
selectAll: string
onlySelected: string
allColumns: string
joinType: string
removeJoin: string
removeTable: string
resize: string
aliasLocked: string
}
}
/** Kutunun gorunur kolon listesi. */
const visibleColumns = (
src: ViewSource,
all: DatabaseColumnDto[],
isSelected: (sourceId: string, columnName: string) => boolean,
) => (src.showSelectedOnly ? all.filter((c) => isSelected(src.id, c.columnName)) : all)
/** Kolon listesinin kaydirilabilir alaninin yuksekligi. */
const listHeight = (src: ViewSource, columnCount: number) =>
Math.max(1, Math.min(src.visibleRows, columnCount || 1)) * BOX_ROW_HEIGHT
const JOIN_COLOR: Record<JoinKind, string> = {
INNER: '#2563eb',
LEFT: '#16a34a',
RIGHT: '#b45309',
FULL: '#7c3aed',
CROSS: '#64748b',
}
export function DiagramPane({
sources,
columnsOf,
loadingSourceIds,
isColumnSelected,
zoom,
dragPreview,
selectedJoinKey,
pendingLink,
lockAliases,
onToggleColumn,
onToggleAllColumns,
onRemoveSource,
onUpdateSource,
onBeginLink,
onCompleteLink,
onSelectJoin,
onUpdateJoin,
onRemoveJoin,
emptyHint,
pendingHint,
labels,
}: DiagramPaneProps) {
const [joinMenu, setJoinMenu] = useState<{ key: string; x: number; y: number } | null>(null)
const [hoveredJoin, setHoveredJoin] = useState<string | null>(null)
/** Kutu ici kaydirma ofsetleri; capa noktalari bunlara gore kayar. */
const [scrollTops, setScrollTops] = useState<Record<string, number>>({})
const handleScroll = useCallback(
(sourceId: string, top: number) =>
setScrollTops((prev) => (prev[sourceId] === top ? prev : { ...prev, [sourceId]: top })),
[],
)
/** Surukleme onizlemesi uygulanmis konumlar. */
const positioned = useMemo(
() =>
sources.map((s) =>
dragPreview?.id === s.id
? { ...s, x: s.x + dragPreview.delta.x, y: s.y + dragPreview.delta.y }
: s,
),
[sources, dragPreview],
)
const layout = useMemo(() => {
const map = new Map<
string,
{
src: ViewSource
columns: DatabaseColumnDto[]
height: number
listH: number
indexOf: Map<string, number>
}
>()
positioned.forEach((s) => {
const cols = visibleColumns(s, columnsOf(s.id), isColumnSelected)
map.set(s.id, {
src: s,
columns: cols,
height: boxHeight(s, cols.length),
listH: listHeight(s, cols.length),
indexOf: new Map(cols.map((c, i) => [c.columnName.toLowerCase(), i])),
})
})
return map
}, [positioned, columnsOf, isColumnSelected])
const lines = useMemo<JoinLine[]>(() => {
const out: JoinLine[] = []
sources.slice(1).forEach((s) => {
if (s.joinKind === 'CROSS') return
s.conditions.forEach((c) => {
if (!c.leftSourceId || !c.leftColumn || !c.rightColumn) return
out.push({
key: `${s.id}:${c.id}`,
sourceId: s.id,
conditionId: c.id,
leftSourceId: c.leftSourceId,
leftColumn: c.leftColumn,
rightColumn: c.rightColumn,
operator: c.operator,
joinKind: s.joinKind,
})
})
})
return out
}, [sources])
const canvasWidth = Math.max(900, ...positioned.map((s) => s.x + BOX_WIDTH + 80))
const canvasHeight = Math.max(
340,
...positioned.map((s) => s.y + (layout.get(s.id)?.height ?? 120) + 60),
)
/**
* Kolonun capa noktasi. Kolon kaydirma yuzunden gorunmuyorsa liste alaninin
* ust/alt kenarina kenetlenir; kutu daraltilmissa baslik hizasi kullanilir.
*/
const anchorOf = (sourceId: string, columnName: string) => {
const entry = layout.get(sourceId)
if (!entry) return null
const { src, listH, indexOf } = entry
if (src.collapsed) {
return { boxX: src.x, y: src.y + BOX_HEADER_HEIGHT / 2 }
}
const idx = indexOf.get(columnName.toLowerCase())
const listTop = src.y + BOX_HEADER_HEIGHT + BOX_PADDING
if (idx === undefined) {
return { boxX: src.x, y: listTop + listH / 2 }
}
const scroll = scrollTops[src.id] ?? 0
const raw = listTop + idx * BOX_ROW_HEIGHT + BOX_ROW_HEIGHT / 2 - scroll
const min = listTop + 3
const max = listTop + listH - 3
return { boxX: src.x, y: Math.min(max, Math.max(min, raw)) }
}
const renderLine = (line: JoinLine) => {
const left = anchorOf(line.leftSourceId, line.leftColumn)
const right = anchorOf(line.sourceId, line.rightColumn)
if (!left || !right) return null
// Kutular arasindaki yone gore capa kenarini sec.
const leftIsWest = left.boxX <= right.boxX
const x1 = leftIsWest ? left.boxX + BOX_WIDTH : left.boxX
const x2 = leftIsWest ? right.boxX : right.boxX + BOX_WIDTH
const y1 = left.y
const y2 = right.y
const dir = leftIsWest ? 1 : -1
const stub = 18
const midX = (x1 + x2) / 2
const midY = (y1 + y2) / 2
const r = Math.min(8, Math.abs(y2 - y1) / 2, Math.abs(midX - (x1 + dir * stub)) || 8)
// Ortogonal yol: kisa cikis -> dikey -> kisa giris (SSMS'teki gibi).
const path =
Math.abs(y2 - y1) < 2
? `M ${x1} ${y1} L ${x2} ${y2}`
: [
`M ${x1} ${y1}`,
`L ${midX - dir * r} ${y1}`,
`Q ${midX} ${y1} ${midX} ${y1 + Math.sign(y2 - y1) * r}`,
`L ${midX} ${y2 - Math.sign(y2 - y1) * r}`,
`Q ${midX} ${y2} ${midX + dir * r} ${y2}`,
`L ${x2} ${y2}`,
].join(' ')
const color = JOIN_COLOR[line.joinKind]
const active = selectedJoinKey === line.key || hoveredJoin === line.key
const open = (e: React.MouseEvent) => {
e.stopPropagation()
onSelectJoin(line.key)
setJoinMenu({ key: line.key, x: midX, y: midY })
}
return (
<g key={line.key} data-join-line>
<path d={path} fill="none" stroke={color} strokeWidth={active ? 2.25 : 1.5} />
{/* Uc noktalari kutuya kenetler */}
<circle cx={x1} cy={y1} r={3} fill={color} />
<circle cx={x2} cy={y2} r={3} fill={color} />
{/* Tiklama alanini genisleten seffaf yol */}
<path
d={path}
fill="none"
stroke="transparent"
strokeWidth={14}
className="pointer-events-auto cursor-pointer"
onClick={open}
onMouseEnter={() => setHoveredJoin(line.key)}
onMouseLeave={() => setHoveredJoin(null)}
/>
{/* SSMS'teki elmas isaret; detay yalnizca hover/secimde acilir */}
<g
className="pointer-events-auto cursor-pointer"
onClick={open}
onMouseEnter={() => setHoveredJoin(line.key)}
onMouseLeave={() => setHoveredJoin(null)}
>
<title>{`${line.joinKind} JOIN · ${line.leftColumn} ${line.operator} ${line.rightColumn}`}</title>
<rect
x={midX - 7}
y={midY - 7}
width={14}
height={14}
rx={2}
transform={`rotate(45 ${midX} ${midY})`}
fill="#fff"
stroke={color}
strokeWidth={active ? 2 : 1.25}
/>
<text
x={midX}
y={midY + 3}
textAnchor="middle"
fontSize={8}
fontWeight={700}
fill={color}
>
{line.operator}
</text>
</g>
{active && (
<g className="pointer-events-none">
<rect
x={midX - 30}
y={midY - 26}
width={60}
height={15}
rx={7}
fill={color}
opacity={0.95}
/>
<text x={midX} y={midY - 15} textAnchor="middle" fontSize={9} fill="#fff">
{line.joinKind}
</text>
</g>
)}
</g>
)
}
const activeJoin = joinMenu ? lines.find((l) => l.key === joinMenu.key) : undefined
const activeJoinSource = activeJoin
? sources.find((s) => s.id === activeJoin.sourceId)
: undefined
return (
<div
className="relative h-full overflow-auto rounded-lg border border-gray-200 bg-white [--grid-color:#edf1f6] dark:border-gray-700 dark:bg-gray-950 dark:[--grid-color:#243244]"
style={{
backgroundImage:
'linear-gradient(var(--grid-color) 1px, transparent 1px), linear-gradient(90deg, var(--grid-color) 1px, transparent 1px)',
backgroundSize: '24px 24px',
}}
onClick={() => {
setJoinMenu(null)
onSelectJoin(null)
onBeginLink(null)
}}
>
{pendingLink && (
<div className="sticky left-2 top-2 z-50 m-2 inline-flex items-center rounded-md border border-blue-300 bg-blue-50 px-3 py-1 text-[11px] text-blue-800 shadow dark:border-blue-500/60 dark:bg-blue-900/40 dark:text-blue-100">
{pendingHint}
</div>
)}
{sources.length === 0 && (
<div className="sticky left-4 top-4 z-30 inline-block max-w-[380px] rounded-lg border border-gray-300 bg-white/95 p-3 text-xs text-gray-500 shadow dark:border-gray-700 dark:bg-gray-900/95 dark:text-gray-300">
{emptyHint}
</div>
)}
<div style={{ width: canvasWidth * zoom, height: canvasHeight * zoom }}>
<div
className="relative origin-top-left"
style={{ width: canvasWidth, height: canvasHeight, transform: `scale(${zoom})` }}
>
<svg
className="pointer-events-none absolute inset-0 z-20 overflow-visible"
width={canvasWidth}
height={canvasHeight}
aria-hidden="true"
>
{lines.map(renderLine)}
</svg>
{positioned.map((src) => {
const entry = layout.get(src.id)
return (
<TableBox
key={src.id}
src={src}
columns={entry?.columns ?? []}
listH={entry?.listH ?? BOX_ROW_HEIGHT}
loading={loadingSourceIds.has(src.id)}
isColumnSelected={isColumnSelected}
pendingLink={pendingLink}
lockAliases={lockAliases}
labels={labels}
zoom={zoom}
onScroll={handleScroll}
onToggleColumn={onToggleColumn}
onToggleAllColumns={onToggleAllColumns}
onRemoveSource={onRemoveSource}
onUpdateSource={onUpdateSource}
onBeginLink={onBeginLink}
onCompleteLink={onCompleteLink}
/>
)
})}
{joinMenu && activeJoin && activeJoinSource && (
<div
className="absolute z-[80] w-[190px] rounded-lg border border-gray-200 bg-white p-2 text-xs shadow-xl dark:border-gray-700 dark:bg-gray-800"
style={{ left: joinMenu.x - 95, top: joinMenu.y + 16 }}
onClick={(e) => e.stopPropagation()}
>
<div className="mb-1 text-[10px] font-semibold uppercase tracking-wide text-gray-400">
{labels.joinType}
</div>
<select
className="mb-1.5 w-full rounded border px-1 py-1 text-xs dark:border-gray-600 dark:bg-gray-700 dark:text-white"
value={activeJoinSource.joinKind}
onChange={(e) =>
onUpdateSource(activeJoinSource.id, { joinKind: e.target.value as JoinKind })
}
>
{JOIN_KINDS.filter((k) => k !== 'CROSS').map((k) => (
<option key={k} value={k}>
{k === 'INNER' ? 'INNER JOIN' : `${k} OUTER JOIN`}
</option>
))}
</select>
<select
className="mb-1.5 w-full rounded border px-1 py-1 text-xs dark:border-gray-600 dark:bg-gray-700 dark:text-white"
value={activeJoin.operator}
onChange={(e) =>
onUpdateJoin(activeJoin.sourceId, activeJoin.conditionId, {
operator: e.target.value as JoinOperator,
})
}
>
{JOIN_OPERATORS.map((op) => (
<option key={op} value={op}>
{`${activeJoin.leftColumn} ${op} ${activeJoin.rightColumn}`}
</option>
))}
</select>
<button
type="button"
className="w-full rounded px-1 py-1 text-left text-[11px] text-red-600 hover:bg-red-50 dark:hover:bg-red-900/20"
onClick={() => {
onRemoveJoin(activeJoin.sourceId, activeJoin.conditionId)
setJoinMenu(null)
}}
>
{labels.removeJoin}
</button>
</div>
)}
</div>
</div>
</div>
)
}
// ─── Tablo kutusu ─────────────────────────────────────────────────────────────
interface TableBoxProps {
src: ViewSource
columns: DatabaseColumnDto[]
listH: number
loading: boolean
isColumnSelected: (sourceId: string, columnName: string) => boolean
pendingLink: PendingLink | null
lockAliases: boolean
labels: DiagramPaneProps['labels']
zoom: number
onScroll: (sourceId: string, top: number) => void
onToggleColumn: (sourceId: string, columnName: string) => void
onToggleAllColumns: (sourceId: string) => void
onRemoveSource: (sourceId: string) => void
onUpdateSource: (sourceId: string, patch: Partial<ViewSource>) => void
onBeginLink: (link: PendingLink | null) => void
onCompleteLink: (target: PendingLink) => void
}
function TableBox({
src,
columns,
listH,
loading,
isColumnSelected,
pendingLink,
lockAliases,
labels,
zoom,
onScroll,
onToggleColumn,
onToggleAllColumns,
onRemoveSource,
onUpdateSource,
onBeginLink,
onCompleteLink,
}: TableBoxProps) {
const { attributes, listeners, setNodeRef, transform } = useDraggable({
id: src.id,
disabled: Boolean(pendingLink),
})
const resizeState = useRef<{ startY: number; startRows: number } | null>(null)
const isLinkSource = pendingLink?.sourceId === src.id
const derived = isDerivedSource(src)
const maxRows = Math.max(1, columns.length)
const beginResize = (e: React.PointerEvent) => {
e.stopPropagation()
e.preventDefault()
resizeState.current = { startY: e.clientY, startRows: Math.min(src.visibleRows, maxRows) }
e.currentTarget.setPointerCapture(e.pointerId)
}
const onResize = (e: React.PointerEvent) => {
const state = resizeState.current
if (!state) return
const deltaRows = Math.round((e.clientY - state.startY) / zoom / BOX_ROW_HEIGHT)
const next = Math.min(maxRows, Math.max(BOX_MIN_ROWS, state.startRows + deltaRows))
if (next !== src.visibleRows) onUpdateSource(src.id, { visibleRows: next })
}
const endResize = (e: React.PointerEvent) => {
resizeState.current = null
e.currentTarget.releasePointerCapture(e.pointerId)
}
return (
<div
ref={setNodeRef}
data-diagram-box
className={classNames(
'absolute z-40 touch-none rounded-lg border-2 bg-white shadow-lg dark:bg-gray-900',
isLinkSource
? 'border-blue-500 outline outline-[3px] outline-blue-500/20'
: derived
? 'border-indigo-400 dark:border-indigo-500'
: 'border-gray-400 dark:border-gray-600',
)}
style={{
left: src.x,
top: src.y,
width: BOX_WIDTH,
transform: CSS.Translate.toString(transform),
}}
onClick={(e) => e.stopPropagation()}
>
{/* Baslik — surukleme tutamaci */}
<div
className="flex cursor-move items-center gap-1 rounded-t-md bg-gray-100 px-1.5 dark:bg-gray-800"
style={{ height: BOX_HEADER_HEIGHT }}
{...listeners}
{...attributes}
>
{derived ? (
<FaLayerGroup className="shrink-0 text-[11px] text-indigo-500" />
) : (
<FaTable className="shrink-0 text-[11px] text-teal-500" />
)}
<span
className="truncate text-[11px] font-semibold text-gray-700 dark:text-gray-100"
title={derived ? src.derivedSql : `${src.schemaName}.${src.objectName}`}
>
{src.objectName}
</span>
{derived && (
<span className="shrink-0 rounded bg-indigo-100 px-1 text-[8px] font-semibold text-indigo-700 dark:bg-indigo-900/50 dark:text-indigo-300">
{src.applyKind ? `${src.applyKind} APPLY` : 'SUB'}
</span>
)}
<input
className="ml-auto w-[46px] shrink-0 rounded border border-gray-300 bg-white px-1 py-0 text-[10px] text-gray-600 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200"
value={src.alias}
disabled={lockAliases}
title={lockAliases ? labels.aliasLocked : labels.alias}
onPointerDown={(e) => e.stopPropagation()}
onChange={(e) => onUpdateSource(src.id, { alias: e.target.value })}
/>
<button
type="button"
className="shrink-0 text-[10px] text-gray-400 hover:text-blue-500"
title={src.showSelectedOnly ? labels.allColumns : labels.onlySelected}
onPointerDown={(e) => e.stopPropagation()}
onClick={() => onUpdateSource(src.id, { showSelectedOnly: !src.showSelectedOnly })}
>
{src.showSelectedOnly ? <FaExpandAlt /> : <FaCompressAlt />}
</button>
<button
type="button"
className="shrink-0 text-[10px] text-gray-400 hover:text-blue-500"
onPointerDown={(e) => e.stopPropagation()}
onClick={() => onUpdateSource(src.id, { collapsed: !src.collapsed })}
>
{src.collapsed ? <FaChevronRight /> : <FaChevronDown />}
</button>
<button
type="button"
className="shrink-0 text-[10px] text-gray-400 hover:text-red-500"
title={labels.removeTable}
onPointerDown={(e) => e.stopPropagation()}
onClick={() => onRemoveSource(src.id)}
>
<FaTimes />
</button>
</div>
{!src.collapsed && (
<>
<div
className="overflow-y-auto overflow-x-hidden"
style={{ height: listH, marginTop: BOX_PADDING, marginBottom: BOX_PADDING }}
onScroll={(e) => onScroll(src.id, e.currentTarget.scrollTop)}
>
{loading && (
<div
className="px-2 text-[10px] text-gray-400"
style={{ height: BOX_ROW_HEIGHT, lineHeight: `${BOX_ROW_HEIGHT}px` }}
>
</div>
)}
{!loading && columns.length === 0 && (
<div
className="px-2 text-[10px] text-gray-400"
style={{ height: BOX_ROW_HEIGHT, lineHeight: `${BOX_ROW_HEIGHT}px` }}
>
</div>
)}
{columns.map((col) => {
const selected = isColumnSelected(src.id, col.columnName)
const isPendingCell =
pendingLink?.sourceId === src.id && pendingLink.columnName === col.columnName
const linkable = pendingLink && pendingLink.sourceId !== src.id
return (
<div
key={col.columnName}
className={classNames(
'group flex items-center gap-1 px-1 text-[11px]',
isPendingCell && 'bg-blue-100 dark:bg-blue-900/40',
linkable && 'cursor-crosshair hover:bg-green-100 dark:hover:bg-green-900/30',
)}
style={{ height: BOX_ROW_HEIGHT }}
onClick={() => {
if (linkable) onCompleteLink({ sourceId: src.id, columnName: col.columnName })
}}
>
<input
type="checkbox"
className="h-3 w-3 shrink-0"
checked={selected}
onPointerDown={(e) => e.stopPropagation()}
onChange={() => onToggleColumn(src.id, col.columnName)}
/>
<span
className="truncate text-gray-700 dark:text-gray-200"
title={`${col.columnName} · ${col.dataType}`}
>
{col.columnName}
</span>
{/* Join tutamaci — tiklayip hedef kolona tiklanir */}
<button
type="button"
className={classNames(
'ml-auto h-2.5 w-2.5 shrink-0 rounded-full border transition-opacity',
isPendingCell
? 'border-blue-600 bg-blue-500 opacity-100'
: 'border-gray-400 bg-white opacity-0 group-hover:opacity-100 dark:bg-gray-700',
)}
title={labels.joinType}
onPointerDown={(e) => e.stopPropagation()}
onClick={(e) => {
e.stopPropagation()
if (isPendingCell) onBeginLink(null)
else if (pendingLink && pendingLink.sourceId !== src.id)
onCompleteLink({ sourceId: src.id, columnName: col.columnName })
else onBeginLink({ sourceId: src.id, columnName: col.columnName })
}}
/>
</div>
)
})}
</div>
<div
className="flex items-center gap-1 rounded-b-md border-t border-gray-100 px-1.5 dark:border-gray-700"
style={{ height: BOX_FOOTER_HEIGHT }}
>
<button
type="button"
className="truncate text-[10px] text-blue-600 hover:underline"
onPointerDown={(e) => e.stopPropagation()}
onClick={() => onToggleAllColumns(src.id)}
>
<FaEye className="mr-1 inline text-[9px]" />
{labels.selectAll}
</button>
<span className="ml-auto shrink-0 text-[9px] text-gray-400">
{Math.min(src.visibleRows, maxRows)}/{columns.length}
</span>
{/* Yukseklik tutamaci */}
<span
className="h-3 w-3 shrink-0 cursor-ns-resize text-gray-300 hover:text-blue-500"
title={labels.resize}
onPointerDown={beginResize}
onPointerMove={onResize}
onPointerUp={endResize}
onPointerCancel={endResize}
>
<svg viewBox="0 0 10 10" className="h-3 w-3">
<path d="M1 6 L9 6 M1 8.5 L9 8.5" stroke="currentColor" strokeWidth="1.2" />
</svg>
</span>
</div>
</>
)}
</div>
)
}
export default DiagramPane

File diff suppressed because it is too large Load diff