SqlViewDesignerDialog
This commit is contained in:
parent
eb77285e59
commit
ece4ee9783
16 changed files with 4007 additions and 69 deletions
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
@ -671,7 +687,7 @@ GO`,
|
||||||
...prev,
|
...prev,
|
||||||
executionResult: result.data,
|
executionResult: result.data,
|
||||||
isExecuting: false,
|
isExecuting: false,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
// Seçili metni geri yükle
|
// Seçili metni geri yükle
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|
@ -712,7 +728,7 @@ GO`,
|
||||||
...prev,
|
...prev,
|
||||||
editorContent: definition,
|
editorContent: definition,
|
||||||
executionResult: null,
|
executionResult: null,
|
||||||
isDirty: false,
|
isDirty: false,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
|
@ -746,7 +762,7 @@ GO`,
|
||||||
...prev,
|
...prev,
|
||||||
editorContent: script,
|
editorContent: script,
|
||||||
executionResult: null,
|
executionResult: null,
|
||||||
isDirty: false,
|
isDirty: false,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
toast.push(
|
toast.push(
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
onClick={importColumnsFromRememberedCreateTable}
|
icon={<FaLayerGroup />}
|
||||||
disabled={isEditMode}
|
onClick={() => setTemplatesOpen((o) => !o)}
|
||||||
>
|
>
|
||||||
{translate('::App.DeveloperKitTableDesigner.PasteCreateTableColumns')}
|
<span className="inline-flex items-center gap-1.5">
|
||||||
|
{translate('::App.DeveloperKitTableDesigner.ColumnTemplates')}
|
||||||
|
<FaChevronDown
|
||||||
|
className={`text-[9px] transition-transform ${templatesOpen ? 'rotate-180' : ''}`}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
</Button>
|
</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>
|
</div>
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
|
||||||
<Button size="xs" variant="solid" color="indigo-600" onClick={addWorkflowColumns}>
|
|
||||||
{translate('::App.SqlQueryManager.AddWorkflowColumns')}
|
|
||||||
</Button>
|
|
||||||
<Button size="xs" variant="solid" color="cyan-600" onClick={addTodoColumns}>
|
|
||||||
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={<FaRegClipboard />}
|
||||||
|
onClick={importColumnsFromRememberedCreateTable}
|
||||||
|
disabled={isEditMode}
|
||||||
|
>
|
||||||
|
{translate('::App.DeveloperKitTableDesigner.PasteCreateTableColumns')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<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">
|
||||||
|
{columns.length} {translate('::App.SqlQueryManager.ColumnName')}
|
||||||
|
</span>
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
variant="plain"
|
||||||
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">◠</span> {translate('::App.Platform.LoadingWithThreeDot')}
|
<span className="animate-spin">◠</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')}
|
||||||
|
|
|
||||||
1019
ui/src/views/developerKit/SqlViewDesignerDialog.tsx
Normal file
1019
ui/src/views/developerKit/SqlViewDesignerDialog.tsx
Normal file
File diff suppressed because it is too large
Load diff
472
ui/src/views/developerKit/sqlViewDesigner/CriteriaGrid.tsx
Normal file
472
ui/src/views/developerKit/sqlViewDesigner/CriteriaGrid.tsx
Normal 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
|
||||||
726
ui/src/views/developerKit/sqlViewDesigner/DiagramPane.tsx
Normal file
726
ui/src/views/developerKit/sqlViewDesigner/DiagramPane.tsx
Normal 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
|
||||||
1181
ui/src/views/developerKit/sqlViewDesigner/viewModel.ts
Normal file
1181
ui/src/views/developerKit/sqlViewDesigner/viewModel.ts
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue