CrudEndpoint kaldırıldı o kısım SqlQueryManager taşındı

This commit is contained in:
Sedat ÖZTÜRK 2026-08-17 17:26:51 +03:00
parent f5ba73f875
commit a0121f8e11
23 changed files with 2080 additions and 1480 deletions

View file

@ -443,13 +443,27 @@ Teknik kullanıcılar için `/admin/developerkit` altında toplanan araçlar:
| Araç | Route | Ne işe yarar |
| --- | --- | --- |
| **SQL Query Manager** | `/admin/sql-manager` | Nesne gezgini, Monaco tabanlı SQL editörü, sonuç grid'i, nesne özellikleri ve **tablo tasarımcısı**. |
| **Custom Endpoint** | `/admin/developerkit/endpoints` | SQL veya servis tanımından REST endpoint üretimi; GET/POST/PUT/DELETE için ayrı yetki kapıları (`App.DeveloperKit.CustomEndpoints.*`) ve kullanıcı/rol/global erişim kuralları. |
| **CRUD Endpoint** | `/admin/developerkit/entities` | Tablo tanımından tam CRUD endpoint kümesi üretimi. |
| **SQL Query Manager** | `/admin/sql-manager` | Nesne gezgini, Monaco tabanlı SQL editörü, sonuç grid'i, **tablo tasarımcısı** ve **CRUD endpoint yönetimi**. |
| **Custom Endpoint** | `/admin/list/App.DeveloperKit.CustomEndpoints` | SQL veya servis tanımından REST endpoint üretimi; GET/POST/PUT/DELETE için ayrı yetki kapıları (`App.DeveloperKit.CustomEndpoints.*`) ve kullanıcı/rol/global erişim kuralları. |
| **CRUD Endpoint** | SQL Query Manager içinde | Tablo tanımından tam CRUD endpoint kümesi üretimi. Ayrı ekranı yoktur; bkz. aşağıdaki not. |
| **Dynamic Service** | `/admin/developerkit/dynamic-services` | C# kodunu tarayıcıda yazıp Roslyn ile derleme (`TestCompile`), yayınlama (`Publish`) ve çalışan uygulamaya controller olarak kaydetme. Yetkiler: Create/Edit/Delete/Manage/TestCompile/Publish/ViewCode. |
| **Custom Component** | `/admin/developerkit/components` | React bileşenini veritabanında saklama; `@babel/standalone` ile tarayıcıda derleyip route'a bağlama. |
| **Visual Designer** | Component editörü içinde | Sürükle-bırak kanvas ile bileşen üretimi ve kod üretimi (`visualDesigner/codeGenerator.ts`). |
> **CRUD Endpoint, SQL Query Manager ile birleştirildi.** Ayrı `/admin/developerkit/endpoints`
> ekranı ve menüsü kaldırıldı. Endpoint üretme, aktif/pasif etme, test etme ve silme işlemleri
> artık iki yerden yapılır: SQL Query Manager nesne gezgininde tablo satırının **CRUD Endpoints**
> aksiyonu (ve seçili tablolar için toolbar'daki toplu üretim butonu), ayrıca ListForm
> sihirbazının **Adım 2 Veri Kaynağı** ekranındaki aynı diyalog. Ayrıca **tablo
> tasarımcısında deploy öncesi bir adım** vardır: endpoint üretilip üretilmeyeceği ve
> hangi operasyonların (GetList/GetById/Create/Update/Delete) aktif başlayacağı seçilir
> ("tümünü seç" destekli); seçilmeyenler pasif olarak kaydedilir, sonradan diyalogdan
> açılabilir. Hepsi `CrudEndpointDialog` bileşenini kullanır. Yetki `App.SqlQueryManager` altındaki
> `App.SqlQueryManager.CrudEndpoints`'tir; izin verilmemişse butonlar görünmez ve
> `crud-endpoint-generate` uçlarının tamamı (base CRUD metotları dahil) reddedilir.
> Üretilen endpoint'ler `configs/seeds/{host|tenants/{tenantId}}/crud/{EntityName}.json` olarak
> seed dosyasına yazılır; `CrudDataSeeder` aynı dosyaları okuyup endpoint'leri geri yükler.
> **Tablo tasarımı varsayılanı:** Yeni tablo oluştururken tenant ve tam denetim (audit) kolonları
> varsayılan olarak eklenir: `Id`, `TenantId`, `CreationTime`, `CreatorId`, `LastModificationTime`,
> `LastModifierId`, `IsDeleted`, `DeletionTime`, `DeleterId`. Kullanıcııkça istemedikçe bu
@ -484,6 +498,7 @@ Route.Authority ──┘ └► Permission
`PermissionsData.json` ile seed edilir. Ana gruplar: `App.Saas`, `App.Branches`, `App.Intranet.*`,
`App.Definitions.*`, `App.Restrictions.*`, `App.Languages.*`, `App.Listforms.*`,
`App.Notifications.*`, `App.BackgroundWorkers.*`, `App.Menus.*`, `App.DeveloperKit.*`,
`App.SqlQueryManager.*`,
`App.Orders.*`, `App.BlogManagement.*`, `App.IdentityManagement.*`, `App.Reports.*`,
`App.Administration`, `App.Setting`, `App.Setup.Migrate`.

View file

@ -12,9 +12,13 @@ public interface ICrudEndpointAppService : ICrudAppService<
PagedAndSortedResultRequestDto,
CreateUpdateCrudEndpointDto>
{
Task<List<CrudEndpointDto>> GetActiveEndpointsAsync();
Task<List<CrudEndpointDto>> GetEndpointsByEntityAsync(string entityName);
Task<CrudEndpointDto> ToggleAsync(Guid id);
Task<PagedResultDto<CrudEndpointDto>> GenerateCrudEndpointsAsync(string entityName);
/// <param name="operationTypes">
/// Aktif baslatilacak operasyonlarin virgulle ayrilmis listesi
/// (GetList, GetById, Create, Update, Delete). Bos birakilirsa hepsi aktif olur.
/// </param>
Task<PagedResultDto<CrudEndpointDto>> GenerateCrudEndpointsAsync(string entityName, string operationTypes = null);
Task DeleteByEntityAsync(string entityName);
}

View file

@ -1,6 +1,10 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Sozsoft.Platform.Data.Seeds;
using Sozsoft.Platform.DeveloperKit;
using Sozsoft.Platform.Entities;
using Volo.Abp.Application.Dtos;
@ -8,6 +12,8 @@ using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Repositories;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Sozsoft.Platform;
namespace Platform.Api.Application;
@ -19,22 +25,31 @@ public class CrudEndpointGenerateAppService : CrudAppService<
PagedAndSortedResultRequestDto,
CreateUpdateCrudEndpointDto>, ICrudEndpointAppService
{
private static readonly JsonSerializerOptions SeedJsonWriteOptions = new()
{
WriteIndented = true
};
private readonly IConfiguration _configuration;
public CrudEndpointGenerateAppService(
IRepository<CrudEndpoint, Guid> repository)
IRepository<CrudEndpoint, Guid> repository,
IConfiguration configuration)
: base(repository)
{
}
_configuration = configuration;
[HttpGet("api/app/crud-endpoint-generate/active-endpoints")]
[Authorize(PlatformConsts.AppCodes.DeveloperKits.CrudEndpoints)]
public virtual async Task<List<CrudEndpointDto>> GetActiveEndpointsAsync()
{
var endpoints = await Repository.GetListAsync(x => x.IsActive);
return await MapToGetListOutputDtosAsync(endpoints);
// CrudAppService'ten miras alinan GetList/Get/Create/Update/Delete de
// ayni izne baglanir; aksi halde base metotlar izinsiz cagrilabilir.
GetPolicyName = PlatformConsts.AppCodes.SqlQueryManagers.CrudEndpoints;
GetListPolicyName = PlatformConsts.AppCodes.SqlQueryManagers.CrudEndpoints;
CreatePolicyName = PlatformConsts.AppCodes.SqlQueryManagers.CrudEndpoints;
UpdatePolicyName = PlatformConsts.AppCodes.SqlQueryManagers.CrudEndpoints;
DeletePolicyName = PlatformConsts.AppCodes.SqlQueryManagers.CrudEndpoints;
}
[HttpGet("api/app/crud-endpoint-generate/endpoints-by-entity/{entityName}")]
[Authorize(PlatformConsts.AppCodes.DeveloperKits.CrudEndpoints)]
[Authorize(PlatformConsts.AppCodes.SqlQueryManagers.CrudEndpoints)]
public virtual async Task<List<CrudEndpointDto>> GetEndpointsByEntityAsync(string entityName)
{
var endpoints = await Repository.GetListAsync(x => x.EntityName == entityName);
@ -42,19 +57,34 @@ public class CrudEndpointGenerateAppService : CrudAppService<
}
[HttpPost("api/app/crud-endpoint-generate/{id}/toggle")]
[Authorize(PlatformConsts.AppCodes.DeveloperKits.CrudEndpoints)]
[Authorize(PlatformConsts.AppCodes.SqlQueryManagers.CrudEndpoints)]
public virtual async Task<CrudEndpointDto> ToggleAsync(Guid id)
{
var endpoint = await Repository.GetAsync(id);
endpoint.IsActive = !endpoint.IsActive;
await Repository.UpdateAsync(endpoint, autoSave: true);
await SyncSeedFileAsync(endpoint.EntityName);
return ObjectMapper.Map<CrudEndpoint, CrudEndpointDto>(endpoint);
}
[HttpPost("api/app/crud-endpoint-generate/generate-crud-endpoints/{entityName}")]
[Authorize(PlatformConsts.AppCodes.DeveloperKits.CrudEndpoints)]
public virtual async Task<PagedResultDto<CrudEndpointDto>> GenerateCrudEndpointsAsync(string entityName)
[Authorize(PlatformConsts.AppCodes.SqlQueryManagers.CrudEndpoints)]
public virtual async Task<PagedResultDto<CrudEndpointDto>> GenerateCrudEndpointsAsync(
string entityName,
[FromQuery] string operationTypes = null)
{
// Bes endpoint her zaman uretilir; operationTypes verilmisse yalnizca
// listedekiler aktif baslar, digerleri pasif olarak kaydedilir ve
// kullanici diyalogdan sonradan acabilir. Bos/null ise hepsi aktiftir.
var activeOperations = operationTypes
?.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
.ToHashSet(StringComparer.OrdinalIgnoreCase);
bool IsActiveOperation(string operationType) =>
activeOperations == null || activeOperations.Count == 0 || activeOperations.Contains(operationType);
// CRUD endpointleri oluştur
var endpoints = new List<CrudEndpoint>
{
@ -63,7 +93,7 @@ public class CrudEndpointGenerateAppService : CrudAppService<
Method = "GET",
Path = $"/api/app/crudendpoint/{entityName}",
OperationType = "GetList",
IsActive = true,
IsActive = IsActiveOperation("GetList"),
CsharpCode = GenerateGetAllCode(entityName)
},
new() {
@ -71,7 +101,7 @@ public class CrudEndpointGenerateAppService : CrudAppService<
Method = "GET",
Path = $"/api/app/crudendpoint/{entityName}/{{id}}",
OperationType = "GetById",
IsActive = true,
IsActive = IsActiveOperation("GetById"),
CsharpCode = GenerateGetByIdCode(entityName)
},
new() {
@ -79,7 +109,7 @@ public class CrudEndpointGenerateAppService : CrudAppService<
Method = "POST",
Path = $"/api/app/crudendpoint/{entityName}",
OperationType = "Create",
IsActive = true,
IsActive = IsActiveOperation("Create"),
CsharpCode = GenerateCreateCode(entityName)
},
new() {
@ -87,7 +117,7 @@ public class CrudEndpointGenerateAppService : CrudAppService<
Method = "PUT",
Path = $"/api/app/crudendpoint/{entityName}/{{id}}",
OperationType = "Update",
IsActive = true,
IsActive = IsActiveOperation("Update"),
CsharpCode = GenerateUpdateCode(entityName)
},
new() {
@ -95,7 +125,7 @@ public class CrudEndpointGenerateAppService : CrudAppService<
Method = "DELETE",
Path = $"/api/app/crudendpoint/{entityName}/{{id}}",
OperationType = "Delete",
IsActive = true,
IsActive = IsActiveOperation("Delete"),
CsharpCode = GenerateDeleteCode(entityName)
}
};
@ -109,6 +139,8 @@ public class CrudEndpointGenerateAppService : CrudAppService<
// Yeni endpointleri ekle
await Repository.InsertManyAsync(endpoints, autoSave: true);
await SyncSeedFileAsync(entityName);
var result = ObjectMapper.Map<List<CrudEndpoint>, List<CrudEndpointDto>>(endpoints);
return new PagedResultDto<CrudEndpointDto>
@ -118,6 +150,128 @@ public class CrudEndpointGenerateAppService : CrudAppService<
};
}
/// <summary>Bir entity'nin tum CRUD endpoint'lerini ve seed dosyasini kaldirir.</summary>
[HttpDelete("api/app/crud-endpoint-generate/by-entity/{entityName}")]
[Authorize(PlatformConsts.AppCodes.SqlQueryManagers.CrudEndpoints)]
public virtual async Task DeleteByEntityAsync(string entityName)
{
var endpoints = await Repository.GetListAsync(x => x.EntityName == entityName);
if (endpoints.Count > 0)
{
await Repository.DeleteManyAsync(endpoints, autoSave: true);
}
await SyncSeedFileAsync(entityName);
}
public override async Task DeleteAsync(Guid id)
{
var endpoint = await Repository.FindAsync(id);
await base.DeleteAsync(id);
if (endpoint != null)
{
await SyncSeedFileAsync(endpoint.EntityName);
}
}
public override async Task<CrudEndpointDto> CreateAsync(CreateUpdateCrudEndpointDto input)
{
var result = await base.CreateAsync(input);
await SyncSeedFileAsync(result.EntityName);
return result;
}
public override async Task<CrudEndpointDto> UpdateAsync(Guid id, CreateUpdateCrudEndpointDto input)
{
var result = await base.UpdateAsync(id, input);
await SyncSeedFileAsync(result.EntityName);
return result;
}
/// <summary>
/// Entity'nin guncel endpoint'lerini <c>{host|tenants/{tenantId}}/crud/{EntityName}.json</c>
/// dosyasina yazar; endpoint kalmamissa dosyayi siler. Dosya islemi hatasi CRUD islemini
/// engellemez, yalnizca loglanir.
/// </summary>
private async Task SyncSeedFileAsync(string entityName)
{
var safeFileName = GetSafeFileName(entityName);
if (safeFileName == null)
{
return;
}
try
{
var outputPath = ResolveCrudSeedOutputPath();
var filePath = Path.Combine(outputPath, $"{safeFileName}.json");
var endpoints = await Repository.GetListAsync(x => x.EntityName == entityName);
if (endpoints.Count == 0)
{
if (File.Exists(filePath))
{
File.Delete(filePath);
Logger.LogInformation("CRUD seed file deleted: {FilePath}", filePath);
}
return;
}
Directory.CreateDirectory(outputPath);
var seedFile = new CrudEndpointSeedFileDto
{
EntityName = entityName,
GeneratedAt = Clock.Now.ToUniversalTime(),
Endpoints = endpoints
.OrderBy(x => x.OperationType)
.Select(x => new CrudEndpointSeedItemDto
{
Method = x.Method,
Path = x.Path,
OperationType = x.OperationType,
IsActive = x.IsActive
})
.ToList()
};
await File.WriteAllTextAsync(filePath, JsonSerializer.Serialize(seedFile, SeedJsonWriteOptions));
Logger.LogInformation("CRUD seed file saved: {FilePath}", filePath);
}
catch (Exception ex)
{
Logger.LogError(ex, "CRUD seed file could not be synchronized for {EntityName}: {Message}", entityName, ex.Message);
}
}
/// <summary>
/// CRUD seed dosyalarinin yazilacagi klasor; kapsam aktif tenant'i izler
/// (host baglaminda <c>host/crud</c>, tenant baglaminda <c>tenants/{tenantId}/crud</c>).
/// </summary>
private string ResolveCrudSeedOutputPath()
{
return SeedPathResolver.GetScopePath(_configuration, CurrentTenant.Id, SeedPathResolver.CrudFolder);
}
/// <summary>Entity adindan guvenli bir dosya adi uretir; path traversal denemelerini reddeder.</summary>
private static string? GetSafeFileName(string entityName)
{
var trimmed = entityName?.Trim();
if (string.IsNullOrWhiteSpace(trimmed) ||
trimmed.Contains('/') ||
trimmed.Contains('\\') ||
trimmed.Contains(".."))
{
return null;
}
var safeFileName = string.Concat(trimmed.Split(Path.GetInvalidFileNameChars()));
return string.IsNullOrWhiteSpace(safeFileName) ? null : safeFileName;
}
private string GenerateGetAllCode(string entityName)
{
return $@"[HttpGet]

View file

@ -0,0 +1,147 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Sozsoft.Platform.Entities;
using Volo.Abp.Data;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Domain.Repositories;
namespace Sozsoft.Platform.Data.Seeds;
/// <summary>
/// SqlQueryManager uzerinden uretilen CRUD endpoint'lerini
/// <c>{host|tenants/{tenantId}}/crud/*.json</c> dosyalarindan okuyarak veritabanina uygular.
/// <para>
/// <see cref="SqlDataSeeder"/> tablolari olusturduktan hemen sonra calisir; boylece endpoint'ler
/// her zaman dayandiklari tablolar hazir olduktan sonra yazilir. ABP'nin contributor sirasi
/// garanti edilmedigi icin bilincli olarak <see cref="IDataSeedContributor"/> degildir,
/// SqlDataSeeder tarafindan dogrudan cagrilir.
/// </para>
/// Her dosya bir entity'ye aittir ve o entity'nin endpoint listesinin tamamini icerir:
/// dosyadaki liste kaynak kabul edilir, veritabanindaki eski kayitlar dosyaya gore guncellenir.
/// </summary>
public class CrudDataSeeder : ITransientDependency
{
private static readonly JsonSerializerOptions JsonReadOptions = new()
{
PropertyNameCaseInsensitive = true
};
private readonly IRepository<CrudEndpoint, Guid> _repository;
private readonly IConfiguration _configuration;
private readonly ILogger<CrudDataSeeder> _logger;
public CrudDataSeeder(
IRepository<CrudEndpoint, Guid> repository,
IConfiguration configuration,
ILogger<CrudDataSeeder> logger)
{
_repository = repository;
_configuration = configuration;
_logger = logger;
}
public async Task SeedAsync(DataSeedContext context)
{
var scopeFolderName = SeedPathResolver.GetScopeFolderName(context.TenantId);
var crudDataPath = SeedPathResolver.GetScopePath(_configuration, context.TenantId, SeedPathResolver.CrudFolder);
if (!Directory.Exists(crudDataPath))
{
_logger.LogInformation("Seeds/{Scope}/crud directory not found, skipping CrudDataSeeder.", scopeFolderName);
return;
}
var jsonFiles = Directory.GetFiles(crudDataPath, "*.json").OrderBy(Path.GetFileName).ToArray();
if (jsonFiles.Length == 0)
{
_logger.LogInformation("No JSON files found in Seeds/{Scope}/crud directory, skipping CrudDataSeeder.", scopeFolderName);
return;
}
_logger.LogInformation("CrudDataSeeder started. {Count} file(s) to be processed.", jsonFiles.Length);
foreach (var filePath in jsonFiles)
{
var fileName = Path.GetFileName(filePath);
try
{
var json = await File.ReadAllTextAsync(filePath);
var seedFile = JsonSerializer.Deserialize<CrudEndpointSeedFileDto>(json, JsonReadOptions);
var entityName = string.IsNullOrWhiteSpace(seedFile?.EntityName)
? Path.GetFileNameWithoutExtension(filePath)
: seedFile!.EntityName.Trim();
if (seedFile?.Endpoints == null || seedFile.Endpoints.Count == 0)
{
_logger.LogWarning("[{File}] No endpoint found, skipped.", fileName);
continue;
}
await ApplyAsync(entityName, seedFile.Endpoints);
_logger.LogInformation("[{File}] '{EntityName}' applied with {Count} endpoint(s).", fileName, entityName, seedFile.Endpoints.Count);
}
catch (Exception ex)
{
// Tek bir dosyanin hatasi diger endpoint'lerin seed edilmesini engellemez.
_logger.LogError(ex, "Failed to process CRUD seed file: {FileName}", fileName);
}
}
_logger.LogInformation("CrudDataSeeder completed. {Count} file(s) processed.", jsonFiles.Length);
}
private async Task ApplyAsync(string entityName, List<CrudEndpointSeedItemDto> endpoints)
{
var existing = await _repository.GetListAsync(x => x.EntityName == entityName);
foreach (var item in endpoints)
{
if (string.IsNullOrWhiteSpace(item.OperationType))
{
continue;
}
var current = existing.FirstOrDefault(x =>
string.Equals(x.OperationType, item.OperationType, StringComparison.OrdinalIgnoreCase));
if (current == null)
{
// CsharpCode seed dosyasinda tutulmaz; endpoint'in calismasi icin gerekli degildir,
// gerektiginde SqlQueryManager'daki "Yeniden Olustur" ile uretilir.
await _repository.InsertAsync(new CrudEndpoint
{
EntityName = entityName,
Method = item.Method,
Path = item.Path,
OperationType = item.OperationType,
IsActive = item.IsActive
}, autoSave: true);
continue;
}
current.Method = item.Method;
current.Path = item.Path;
current.IsActive = item.IsActive;
await _repository.UpdateAsync(current, autoSave: true);
}
// Dosyada artik yer almayan operasyonlar kaldirilir; dosya tek kaynaktir.
var seededOperations = endpoints
.Select(x => x.OperationType)
.Where(x => !string.IsNullOrWhiteSpace(x))
.ToHashSet(StringComparer.OrdinalIgnoreCase);
var obsolete = existing.Where(x => !seededOperations.Contains(x.OperationType)).ToList();
if (obsolete.Count > 0)
{
await _repository.DeleteManyAsync(obsolete, autoSave: true);
}
}
}

View file

@ -638,7 +638,7 @@
},
{
"resourceName": "Platform",
"key": "App.DeveloperKit.CrudEndpoints",
"key": "App.SqlQueryManager.CrudEndpoints",
"en": "Crud Endpoints",
"tr": "Crud Uç Noktaları"
},
@ -9084,12 +9084,6 @@
"en": "Apply Migration",
"tr": "Geçişi Uygula"
},
{
"resourceName": "Platform",
"key": "App.DeveloperKitMigration.GenerateCrudEndpoints",
"en": "Generate Crud Endpoints",
"tr": "Crud Uç Noktalarını Oluştur"
},
{
"resourceName": "Platform",
"key": "App.DeveloperKitMigration.NoSQLScript",
@ -16719,8 +16713,8 @@
{
"resourceName": "Platform",
"key": "App.SqlQueryManager.OpenFileManager",
"en": "Seed Files",
"tr": "Seed Dosyaları"
"en": "File Manager",
"tr": "File Manager"
},
{
"resourceName": "Platform",
@ -16842,6 +16836,96 @@
"en": "T-SQL Preview",
"tr": "T-SQL Önizleme"
},
{
"resourceName": "Platform",
"key": "App.SqlQueryManager.GenerateCrudEndpointsAfterDeploy",
"en": "Generate CRUD endpoints after deploy",
"tr": "Deploy sonrası CRUD endpoint'lerini oluştur"
},
{
"resourceName": "Platform",
"key": "App.SqlQueryManager.GenerateCrudEndpointsHint",
"en": "Endpoints are created for this entity right after the table is deployed",
"tr": "Endpoint'ler tablo deploy edildikten hemen sonra bu varlık için üretilir"
},
{
"resourceName": "Platform",
"key": "App.SqlQueryManager.SelectCrudOperations",
"en": "Operations to enable",
"tr": "Açılacak operasyonlar"
},
{
"resourceName": "Platform",
"key": "App.SqlQueryManager.SelectAtLeastOneCrudOperation",
"en": "Select at least one operation or turn generation off",
"tr": "En az bir operasyon seçin ya da üretimi kapatın"
},
{
"resourceName": "Platform",
"key": "App.SqlQueryManager.CrudEndpointsInactiveNote",
"en": "Unselected operations are still created but stay inactive; you can enable them later from the CRUD Endpoints dialog.",
"tr": "Seçilmeyen operasyonlar yine oluşturulur ancak pasif kalır; daha sonra CRUD Endpoint diyaloğundan açabilirsiniz."
},
{
"resourceName": "Platform",
"key": "App.SqlQueryManager.ClearSelection",
"en": "Clear selection",
"tr": "Seçimi temizle"
},
{
"resourceName": "Platform",
"key": "App.SqlQueryManager.DropSelected",
"en": "Drop selected",
"tr": "Seçilenleri sil"
},
{
"resourceName": "Platform",
"key": "App.SqlQueryManager.DropObjects",
"en": "Drop objects",
"tr": "Nesneleri sil"
},
{
"resourceName": "Platform",
"key": "App.SqlQueryManager.DropObjectsWarning",
"en": "The following objects will be permanently dropped:",
"tr": "Aşağıdaki nesneler kalıcı olarak silinecek:"
},
{
"resourceName": "Platform",
"key": "App.SqlQueryManager.Drop",
"en": "Drop",
"tr": "Sil"
},
{
"resourceName": "Platform",
"key": "App.SqlQueryManager.CrudOperationGetList",
"en": "List records",
"tr": "Kayıtları listeler"
},
{
"resourceName": "Platform",
"key": "App.SqlQueryManager.CrudOperationGetById",
"en": "Get single record",
"tr": "Tek kayıt getirir"
},
{
"resourceName": "Platform",
"key": "App.SqlQueryManager.CrudOperationCreate",
"en": "Create record",
"tr": "Kayıt oluşturur"
},
{
"resourceName": "Platform",
"key": "App.SqlQueryManager.CrudOperationUpdate",
"en": "Update record",
"tr": "Kayıt günceller"
},
{
"resourceName": "Platform",
"key": "App.SqlQueryManager.CrudOperationDelete",
"en": "Delete record",
"tr": "Kayıt siler"
},
{
"resourceName": "Platform",
"key": "App.SqlQueryManager.TableDesigner",
@ -17166,84 +17250,6 @@
"en": "Table could not be deployed.",
"tr": "Tablo deploy edilemedi."
},
{
"resourceName": "Platform",
"key": "App.DeveloperKitCrudEndpoints.TotalTables",
"en": "Total Tables",
"tr": "Toplam Tablo"
},
{
"resourceName": "Platform",
"key": "App.DeveloperKitCrudEndpoints.TotalTablesDescription",
"en": "Number of total tables",
"tr": "Toplam tabloların sayısı"
},
{
"resourceName": "Platform",
"key": "App.DeveloperKitCrudEndpoints.EndpointInstalled",
"en": "Endpoint Installed",
"tr": "Endpoint Kurulu"
},
{
"resourceName": "Platform",
"key": "App.DeveloperKitCrudEndpoints.EndpointInstalledDescription",
"en": "Number of installed endpoints",
"tr": "Kurulu endpointlerin sayısı"
},
{
"resourceName": "Platform",
"key": "App.DeveloperKitCrudEndpoints.ActiveEndpoint",
"en": "Active Endpoint",
"tr": "Aktif Endpoint"
},
{
"resourceName": "Platform",
"key": "App.DeveloperKitCrudEndpoints.ActiveEndpointDescription",
"en": "Number of active endpoints",
"tr": "Aktif endpointlerin sayısı"
},
{
"resourceName": "Platform",
"key": "App.DeveloperKitCrudEndpoints.DataSourceDescription",
"en": "Data source of the CRUD endpoints",
"tr": "CRUD endpointlerinin veri kaynağı"
},
{
"resourceName": "Platform",
"key": "App.DeveloperKitCrudEndpoints.SearchTable",
"en": "Search table...",
"tr": "Tablo ara..."
},
{
"resourceName": "Platform",
"key": "App.DeveloperKitCrudEndpoints.FilterWith",
"en": "WITH",
"tr": "VAR"
},
{
"resourceName": "Platform",
"key": "App.DeveloperKitCrudEndpoints.FilterWithout",
"en": "WITHOUT",
"tr": "YOK"
},
{
"resourceName": "Platform",
"key": "App.DeveloperKitCrudEndpoints.SelectDataSource",
"en": "Select a data source",
"tr": "Veri kaynağı seçin"
},
{
"resourceName": "Platform",
"key": "App.DeveloperKitCrudEndpoints.SelectTablePrompt",
"en": "Select a table from the left",
"tr": "Soldan bir tablo seçin"
},
{
"resourceName": "Platform",
"key": "App.DeveloperKitCrudEndpoints.SelectTableDescription",
"en": "You can manage CRUD endpoints for the selected table",
"tr": "Seçilen tablo için CRUD endpointlerini yönetebilirsiniz"
},
{
"resourceName": "Platform",
"key": "App.DeveloperKitCrudEndpoints.DeleteAll",
@ -17262,6 +17268,24 @@
"en": "Create CRUD Endpoint",
"tr": "CRUD Endpoint Oluştur"
},
{
"resourceName": "Platform",
"key": "App.Platform.SelectOrOpenHint",
"en": "Click to select, double-click to open in the editor",
"tr": "Seçmek için tıklayın, editörde açmak için çift tıklayın"
},
{
"resourceName": "Platform",
"key": "App.Platform.InvalidJson",
"en": "Request body is not valid JSON",
"tr": "İstek gövdesi geçerli bir JSON değil"
},
{
"resourceName": "Platform",
"key": "App.DeveloperKitCrudEndpoints.CreateCrudEndpointHint",
"en": "Generates CRUD endpoints for the selected tables",
"tr": "Seçili tablolar için CRUD endpointlerini oluşturur"
},
{
"resourceName": "Platform",
"key": "App.DeveloperKitCrudEndpoints.NoEndpointsYet",
@ -17310,24 +17334,12 @@
"en": "Test",
"tr": "Test Et"
},
{
"resourceName": "Platform",
"key": "App.DeveloperKitCrudEndpoints.CsharpCode",
"en": "C# Code",
"tr": "C# Kodu"
},
{
"resourceName": "Platform",
"key": "App.DeveloperKitCrudEndpoints.InactiveCount",
"en": "inactive",
"tr": "devre dışı"
},
{
"resourceName": "Platform",
"key": "App.DeveloperKitCrudEndpoints.EndpointSummary",
"en": "5 endpoints: GetList, GetById, Create, Update, Delete",
"tr": "5 endpoint: GetList, GetById, Create, Update, Delete"
},
{
"resourceName": "Platform",
"key": "App.DeveloperKitDynamicServices.SuccessfulDescription",

View file

@ -1,5 +1,6 @@
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Collections.Generic;
using Sozsoft.Platform.Entities;

View file

@ -336,14 +336,6 @@
"routeType": "protected",
"authority": ["App.SqlQueryManager"]
},
{
"key": "admin.developerkit.endpoints",
"path": "/admin/developerkit/endpoints",
"componentType": "normal",
"componentPath": "@/views/developerKit/CrudEndpointManager",
"routeType": "protected",
"authority": ["App.DeveloperKit.CrudEndpoints"]
},
{
"key": "admin.developerkit.dynamic-services",
"path": "/admin/developerkit/dynamic-services",
@ -956,16 +948,6 @@
"RequiredPermissionName": "App.SqlQueryManager",
"IsDisabled": false
},
{
"ParentCode": "App.DeveloperKit",
"Code": "App.DeveloperKit.CrudEndpoints",
"DisplayName": "App.DeveloperKit.CrudEndpoints",
"Order": 2,
"Url": "/admin/developerkit/endpoints",
"Icon": "FcOrgUnit",
"RequiredPermissionName": "App.DeveloperKit.CrudEndpoints",
"IsDisabled": false
},
{
"ParentCode": "App.DeveloperKit",
"Code": "App.DeveloperKit.CustomEndpoints",

View file

@ -2374,9 +2374,9 @@
},
{
"GroupName": "App.Saas",
"Name": "App.DeveloperKit.CrudEndpoints",
"ParentName": "App.DeveloperKit",
"DisplayName": "App.DeveloperKit.CrudEndpoints",
"Name": "App.SqlQueryManager.CrudEndpoints",
"ParentName": "App.SqlQueryManager",
"DisplayName": "App.SqlQueryManager.CrudEndpoints",
"IsEnabled": true,
"MultiTenancySide": 2,
"MenuGroup": "Erp|Kurs"

View file

@ -46,19 +46,33 @@ public class SqlDataSeeder : IDataSeedContributor, ITransientDependency
private readonly IDbContextProvider<PlatformDbContext> _dbContextProvider;
private readonly IConfiguration _configuration;
private readonly CrudDataSeeder _crudDataSeeder;
private readonly ILogger<SqlDataSeeder> _logger;
public SqlDataSeeder(
IDbContextProvider<PlatformDbContext> dbContextProvider,
IConfiguration configuration,
CrudDataSeeder crudDataSeeder,
ILogger<SqlDataSeeder> logger)
{
_dbContextProvider = dbContextProvider;
_configuration = configuration;
_crudDataSeeder = crudDataSeeder;
_logger = logger;
}
/// <summary>
/// Once SQL nesneleri uygulanir, ardindan ayni kapsam icin
/// <see cref="CrudDataSeeder"/> calisir — CRUD endpoint'leri dayandiklari
/// tablolardan sonra olusturulmus olur.
/// </summary>
public async Task SeedAsync(DataSeedContext context)
{
await SeedSqlObjectsAsync(context);
await _crudDataSeeder.SeedAsync(context);
}
private async Task SeedSqlObjectsAsync(DataSeedContext context)
{
var dataDirectoryName = $"{SeedPathResolver.GetScopeFolderName(context.TenantId)}/{GetProviderDirectoryName()}";
var sqlDataPath = SeedPathResolver.GetScopePath(_configuration, context.TenantId, GetProviderDirectoryName());

View file

@ -486,8 +486,6 @@ public static class PlatformConsts
public const string Put = CustomEndpoints + ".Put";
public const string Remove = CustomEndpoints + ".Remove";
public const string CrudEndpoints = Default + ".CrudEndpoints";
public static class DynamicServices
{
public const string DynamicService = Default + ".DynamicServices";
@ -502,6 +500,13 @@ public static class PlatformConsts
}
}
public static class SqlQueryManagers
{
public const string Default = Prefix.App + ".SqlQueryManager";
public const string CrudEndpoints = Default + ".CrudEndpoints";
}
//Web Site
public const string Home = Prefix.App + ".Home";
public const string About = Prefix.App + ".About";

View file

@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
namespace Sozsoft.Platform.Data.Seeds;
/// <summary>
/// Bir entity icin uretilen CRUD endpoint'lerinin seed dosyasi.
/// SqlQueryManager endpoint uretince <c>{host|tenants/{tenantId}}/crud/{EntityName}.json</c>
/// olarak yazilir; veritabani silinip yeniden olusturuldugunda
/// <c>CrudDataSeeder</c> ayni dosyalari okuyarak endpoint'leri geri yukler.
/// </summary>
public class CrudEndpointSeedFileDto
{
/// <summary>PascalCase entity adi; dosya adi ile ayni olmasi beklenir.</summary>
public string EntityName { get; set; } = string.Empty;
/// <summary>Dosyanin en son yazildigi an (UTC) — yalnizca bilgi amaclidir.</summary>
public DateTime GeneratedAt { get; set; }
public List<CrudEndpointSeedItemDto> Endpoints { get; set; } = [];
}
/// <summary>
/// Endpoint'in seed edilebilir tanimi. Uretilen C# kodu bilincli olarak dosyada tutulmaz:
/// entity adi ve operasyondan yeniden turetilebilir bir ciktidir, dosyayi gereksiz sisirir.
/// </summary>
public class CrudEndpointSeedItemDto
{
public string Method { get; set; } = string.Empty;
public string Path { get; set; } = string.Empty;
public string OperationType { get; set; } = string.Empty;
public bool IsActive { get; set; } = true;
}

View file

@ -75,6 +75,11 @@ export default [
...tsPlugin.configs.recommended.rules,
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unused-vars': 'off',
// TypeScript zaten tanimsiz tanimlayicilari yakalar; ESLint'in kapsam
// analizi ise `EventListener` / `NotificationOptions` gibi yalnizca tip
// olarak var olan DOM global'lerini bilmedigi icin yanlis alarm veriyor.
// typescript-eslint'in de onerdigi ayar.
'no-undef': 'off',
},
},
prettier,

View file

@ -49,12 +49,17 @@ function DbMigrateLogPanel({ onClose }: DbMigrateLogPanelProps) {
<div className="flex flex-col w-[700px] max-w-[95vw] h-[520px] max-h-[90vh] rounded-xl shadow-2xl bg-gray-900 border border-gray-700">
<div className="flex items-center justify-between px-4 py-3 border-b border-gray-700">
<span className="text-white font-semibold text-sm">DB Migration Logs</span>
{/*
Panel her iki temada da koyu; buton varsayilan olarak acik temayi
izledigi icin renkler burada sabitlenir (aksi halde hover'da beyaz
zemin uzerine beyaz yazi kaliyordu).
*/}
{done && (
<Button
onClick={onClose}
variant="default"
size="xs"
className="border-gray-600 text-gray-400 hover:border-gray-400 hover:text-white"
className="!bg-gray-800 !border-gray-600 !text-gray-300 hover:!bg-gray-700 hover:!border-gray-400 hover:!text-white"
onClick={onClose}
>
Close
</Button>

View file

@ -12,16 +12,6 @@ export interface CrudEndpoint {
lastModificationTime?: string
}
export interface CreateUpdateCrudEndpointDto {
tenantId: string
entityName: string
method: 'GET' | 'POST' | 'PUT' | 'DELETE'
path: string
operationType: string
csharpCode: string
isActive: boolean
}
export interface CustomComponent {
id: string
tenantId?: string

View file

@ -40,8 +40,6 @@ export const ROUTES_ENUM = {
entities: '/admin/developerkit/entities',
entitiesNew: '/admin/developerkit/entities/new',
entitiesEdit: '/admin/developerkit/entities/edit/:id',
endpoints: '/admin/developerkit/endpoints',
endpointsNew: '/admin/developerkit/endpoints/new',
components: '/admin/developerkit/components',
componentsEdit: '/admin/developerkit/components/edit/:id',
dynamicServices: '/admin/developerkit/dynamic-services',

View file

@ -2,7 +2,6 @@ import { PagedResultDto } from '@/proxy'
import apiService from './api.service'
import {
CrudEndpoint,
CreateUpdateCrudEndpointDto,
CreateUpdateCustomComponentDto,
CustomComponent,
CustomComponentDto,
@ -68,14 +67,6 @@ class DeveloperKitService {
}
// Generated Endpoint endpoints
async getActiveGeneratedEndpoints(): Promise<PagedResultDto<CrudEndpoint>> {
const response = await apiService.fetchData<PagedResultDto<CrudEndpoint>>({
url: '/api/app/crud-endpoint-generate/active-endpoints',
method: 'GET',
})
return response.data
}
async toggleGeneratedEndpoint(id: string): Promise<CrudEndpoint> {
const response = await apiService.fetchData<CrudEndpoint>({
url: `/api/app/crud-endpoint-generate/${id}/toggle`,
@ -84,14 +75,37 @@ class DeveloperKitService {
return response.data
}
async generateCrudEndpoints(entityName: string): Promise<PagedResultDto<CrudEndpoint>> {
/**
* @param operationTypes Aktif baslatilacak operasyonlar; verilmezse hepsi aktif uretilir.
* Listede olmayanlar pasif olarak kaydedilir.
*/
async generateCrudEndpoints(
entityName: string,
operationTypes?: string[],
): Promise<PagedResultDto<CrudEndpoint>> {
const response = await apiService.fetchData<PagedResultDto<CrudEndpoint>>({
url: `/api/app/crud-endpoint-generate/generate-crud-endpoints/${entityName}`,
method: 'POST',
params: operationTypes?.length ? { operationTypes: operationTypes.join(',') } : undefined,
})
return response.data
}
async getEndpointsByEntity(entityName: string): Promise<CrudEndpoint[]> {
const response = await apiService.fetchData<CrudEndpoint[]>({
url: `/api/app/crud-endpoint-generate/endpoints-by-entity/${entityName}`,
method: 'GET',
})
return response.data
}
async deleteGeneratedEndpointsByEntity(entityName: string): Promise<void> {
await apiService.fetchData<void>({
url: `/api/app/crud-endpoint-generate/by-entity/${entityName}`,
method: 'DELETE',
})
}
async getGeneratedListEndpoints(): Promise<PagedResultDto<CrudEndpoint>> {
const response = await apiService.fetchData<PagedResultDto<CrudEndpoint>>({
url: '/api/app/crud-endpoint-generate',
@ -101,35 +115,6 @@ class DeveloperKitService {
return response.data
}
async getGeneratedEndpoint(id: string): Promise<CrudEndpoint> {
const response = await apiService.fetchData<CrudEndpoint>({
url: `/api/app/crud-endpoint-generate/${id}`,
method: 'GET',
})
return response.data
}
async createGeneratedEndpoint(data: CreateUpdateCrudEndpointDto): Promise<CrudEndpoint> {
const response = await apiService.fetchData<CrudEndpoint>({
url: '/api/app/crud-endpoint-generate',
method: 'POST',
data: data as any,
})
return response.data
}
async updateGeneratedEndpoint(
id: string,
endpoint: CreateUpdateCrudEndpointDto,
): Promise<CrudEndpoint> {
const response = await apiService.fetchData<CrudEndpoint>({
url: `/api/app/crud-endpoint-generate/${id}`,
method: 'PUT',
data: endpoint as any,
})
return response.data
}
async deleteGeneratedEndpoint(id: string): Promise<void> {
await apiService.fetchData<void>({
url: `/api/app/crud-endpoint-generate/${id}`,

View file

@ -5,7 +5,7 @@ import { SelectBoxOption } from '@/types/shared'
import { Field, FieldProps, FormikErrors, FormikTouched, useFormikContext } from 'formik'
import { useState } from 'react'
import CreatableSelect from 'react-select/creatable'
import { FaArrowLeft, FaArrowRight, FaPlus, FaTable } from 'react-icons/fa'
import { FaArrowLeft, FaArrowRight, FaBolt, FaPlus, FaTable } from 'react-icons/fa'
import {
dbSourceTypeOptions,
listFormDefaultLayoutOptions,
@ -14,6 +14,10 @@ import {
} from '../edit/options'
import { ListFormWizardDto } from '@/proxy/admin/wizard/models'
import SqlTableDesignerDialog from '@/views/developerKit/SqlTableDesignerDialog'
import CrudEndpointDialog, {
CRUD_ENDPOINT_PERMISSION,
} from '@/views/developerKit/CrudEndpointDialog'
import { usePermission } from '@/utils/hooks/usePermission'
// ─── Props ────────────────────────────────────────────────────────────────────
@ -77,6 +81,10 @@ const WizardStep2 = ({
schemaName: string
tableName: string
} | null>(null)
const [showCrudEndpointDialog, setShowCrudEndpointDialog] = useState(false)
const { checkPermission } = usePermission()
/** CRUD endpoint uretimi/yonetimi yalnizca bu izin verildiginde gorunur. */
const canManageCrudEndpoints = checkPermission(CRUD_ENDPOINT_PERMISSION)
const formik = useFormikContext<ListFormWizardDto>()
const selectedTable = values.selectCommand
@ -330,6 +338,18 @@ const WizardStep2 = ({
>
Design Table
</Button>
{canManageCrudEndpoints && (
<Button
type="button"
variant="default"
icon={<FaBolt />}
disabled={!values.dataSourceCode || !selectedTable}
onClick={() => setShowCrudEndpointDialog(true)}
title={translate('::App.DeveloperKitCrudEndpoints.CreateCrudEndpointHint')}
>
{translate('::App.SqlQueryManager.CrudEndpoints')}
</Button>
)}
</div>
)
}}
@ -790,6 +810,17 @@ const WizardStep2 = ({
initialTableData={designTableData}
onDeployed={handleTableDeployed}
/>
{/* Secili tablonun CRUD endpoint'leri: uretme, aktif/pasif ve silme */}
{showCrudEndpointDialog && selectedTable && canManageCrudEndpoints && (
<CrudEndpointDialog
isOpen
schemaName={selectedTable.schemaName}
tableName={selectedTable.tableName}
dataSourceCode={values.dataSourceCode}
onClose={() => setShowCrudEndpointDialog(false)}
/>
)}
</div>
)
}

View file

@ -0,0 +1,855 @@
import { useCallback, useEffect, useMemo, useState, type ReactNode } from 'react'
import {
FaBolt,
FaSyncAlt,
FaToggleOn,
FaToggleOff,
FaTrash,
FaCopy,
FaChevronDown,
FaChevronRight,
FaPaperPlane,
FaCheckCircle,
FaExclamationCircle,
FaList,
FaSearch,
FaPlus,
FaPen,
} from 'react-icons/fa'
import { Button, Dialog, Notification, toast } from '@/components/ui'
import { ConfirmDialog } from '@/components/shared'
import { developerKitService } from '@/services/developerKit.service'
import { sqlObjectManagerService } from '@/services/sql-query-manager.service'
import apiService from '@/services/api.service'
import type { CrudEndpoint } from '@/proxy/developerKit/models'
import type { DatabaseColumnDto } from '@/proxy/sql-query-manager/models'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { usePermission } from '@/utils/hooks/usePermission'
// ─── Paylasilan yardimcilar ───────────────────────────────────────────────────
// Endpoint'leri ureten/gosteren tum ekranlar (SqlQueryManager, SqlObjectExplorer,
// SqlTableDesignerDialog, ListForm Wizard) bu dosyadaki export'lari kullanir.
/**
* CRUD endpoint uretimi/yonetimi icin gereken izin.
* Backend tarafinda PlatformConsts.AppCodes.SqlQueryManagers.CrudEndpoints ile ayni.
*/
export const CRUD_ENDPOINT_PERMISSION = 'App.SqlQueryManager.CrudEndpoints'
/** Uretilen endpoint operasyonlari; backend ile ayni sirada ve adlarla. */
export const CRUD_OPERATION_TYPES = [
'GetList',
'GetById',
'Create',
'Update',
'Delete',
] as const
export type CrudOperationType = (typeof CRUD_OPERATION_TYPES)[number]
/**
* Endpoint'lerde kullanilan entity adini turetir.
*
* Entity adi calisma zamaninda `DynamicEntityManager` tarafindan dogrudan tablo adi
* olarak SQL'e girer (`SELECT * FROM {entityName}`), bu yuzden tablo adi aynen korunur;
* yalnizca `dbo.` gibi sema oneki ayiklanir. Ad normalize edilirse (`Adm_D_Approval`
* `AdmDApproval`) uretilen endpoint calisma aninda olmayan bir tabloyu sorgular.
*/
export const toEntityName = (tableName: string): string =>
(tableName || '').replace(/^.*\./g, '').trim()
export const METHOD_COLOR: Record<string, string> = {
GET: 'bg-blue-100 text-blue-800 border-blue-200 dark:bg-blue-900/30 dark:text-blue-300 dark:border-blue-800',
POST: 'bg-green-100 text-green-800 border-green-200 dark:bg-green-900/30 dark:text-green-300 dark:border-green-800',
PUT: 'bg-yellow-100 text-yellow-800 border-yellow-200 dark:bg-yellow-900/30 dark:text-yellow-300 dark:border-yellow-800',
DELETE:
'bg-red-100 text-red-800 border-red-200 dark:bg-red-900/30 dark:text-red-300 dark:border-red-800',
}
interface OperationMeta {
icon: ReactNode
/** Aktif endpoint rengi; pasif olanlar gri gosterilir. */
color: string
}
const OPERATION_META: Record<string, OperationMeta> = {
GetList: { icon: <FaList />, color: 'text-blue-500' },
GetById: { icon: <FaSearch />, color: 'text-sky-500' },
Create: { icon: <FaPlus />, color: 'text-green-500' },
Update: { icon: <FaPen />, color: 'text-amber-500' },
Delete: { icon: <FaTrash />, color: 'text-red-500' },
}
export const getOperationMeta = (operationType: string): OperationMeta =>
OPERATION_META[operationType] ?? { icon: <FaList />, color: 'text-gray-400' }
/** Endpoint'leri CRUD_OPERATION_TYPES sirasina gore siralar; bilinmeyenler sona gider. */
export const sortByCrudOperation = <T extends { operationType: string }>(endpoints: T[]): T[] => {
const order = (operationType: string) => {
const index = CRUD_OPERATION_TYPES.indexOf(operationType as CrudOperationType)
return index === -1 ? CRUD_OPERATION_TYPES.length : index
}
return [...endpoints].sort((a, b) => order(a.operationType) - order(b.operationType))
}
/** Endpoint listesini entity adina gore gruplar. */
export const groupEndpointsByEntity = (endpoints: CrudEndpoint[]): Map<string, CrudEndpoint[]> => {
const map = new Map<string, CrudEndpoint[]>()
for (const endpoint of endpoints) {
const list = map.get(endpoint.entityName)
if (list) list.push(endpoint)
else map.set(endpoint.entityName, [endpoint])
}
// Explorer rozetleri de diyalogla ayni sirada cizilir.
for (const [entityName, list] of map) {
map.set(entityName, sortByCrudOperation(list))
}
return map
}
export const getErrorMessage = (error: unknown, fallback: string): string => {
const response = (
error as { response?: { data?: { error?: { message?: string }; message?: string } } }
)?.response
return (
response?.data?.error?.message ||
response?.data?.message ||
(error instanceof Error ? error.message : undefined) ||
fallback
)
}
interface CrudEndpointBadgesProps {
endpoints: CrudEndpoint[]
/** Ikon uzerindeki tooltip metnini uretir: (operationType, method, path, isActive) */
buildTitle: (endpoint: CrudEndpoint) => string
onClick?: () => void
}
/**
* Bir tablonun acilmis CRUD endpoint'lerini kucuk ikonlarla gosterir.
* Aktif olanlar renkli, pasif olanlar soluk gri cizilir; tooltip operasyonu aciklar.
*/
export const CrudEndpointBadges = ({ endpoints, buildTitle, onClick }: CrudEndpointBadgesProps) => {
if (endpoints.length === 0) return null
return (
<span
className="flex flex-shrink-0 items-center gap-1"
onClick={
onClick
? (e) => {
e.stopPropagation()
onClick()
}
: undefined
}
>
{endpoints.map((endpoint) => {
const meta = getOperationMeta(endpoint.operationType)
return (
<span
key={endpoint.id}
title={buildTitle(endpoint)}
className={`text-[10px] ${endpoint.isActive ? meta.color : 'text-gray-300 dark:text-gray-600'}`}
>
{meta.icon}
</span>
)
})}
</span>
)
}
// ─── Dialog ───────────────────────────────────────────────────────────────────
interface CrudEndpointDialogProps {
isOpen: boolean
/** Endpoint'leri yonetilecek tablo; entity adi tablo adindan turetilir. */
tableName: string
schemaName?: string
/** Verilirse istek govdesi tablonun gercek kolonlarindan uretilir. */
dataSourceCode?: string | null
onClose: () => void
/** Endpoint listesi degistiginde cagrilir; cagiran taraf kendi state'ini tazeler. */
onChanged?: (entityName: string, endpoints: CrudEndpoint[]) => void
}
interface TestResult {
success: boolean
status: number
durationMs: number
payload: string
}
interface EndpointParameter {
name: string
type: 'path' | 'query'
value: string
}
/** Create istegine gonderilmemesi gereken ABP audit/sistem kolonlari. */
const SYSTEM_COLUMNS = new Set(
[
'Id',
'TenantId',
'CreationTime',
'CreatorId',
'LastModificationTime',
'LastModifierId',
'IsDeleted',
'DeleterId',
'DeletionTime',
'ConcurrencyStamp',
'ExtraProperties',
].map((name) => name.toLowerCase()),
)
const EMPTY_GUID = '00000000-0000-0000-0000-000000000000'
/** SQL veri tipine gore ornek govde degeri uretir. */
const getSampleValue = (column: DatabaseColumnDto): unknown => {
const type = (column.dataType || '').toLowerCase()
if (column.isNullable) return null
if (type.includes('guid') || type === 'uuid') return EMPTY_GUID
if (type.includes('bit') || type.includes('bool')) return false
if (
type.includes('int') ||
type.includes('decimal') ||
type.includes('numeric') ||
type.includes('money') ||
type.includes('float') ||
type.includes('real') ||
type.includes('double')
) {
return 0
}
if (type.includes('date') || type.includes('time')) return new Date().toISOString()
return ''
}
const GENERIC_BODY = JSON.stringify({ name: 'Sample', isActive: true }, null, 2)
/** Endpoint'in path'indeki {placeholder} adlarini cikarir. */
const getPathParameterNames = (path: string): string[] =>
[...(path || '').matchAll(/\{(\w+)\}/g)].map((match) => match[1])
const needsBody = (endpoint: CrudEndpoint) =>
endpoint.operationType === 'Create' || endpoint.operationType === 'Update'
/**
* Tek bir tablonun CRUD endpoint'lerini yonetir: uretme/yeniden uretme, tekil
* aktif/pasif, tekil silme, tumunu silme ve endpoint'i parametreleriyle calistirip
* sonucunu gorme. SqlQueryManager ve ListForm Wizard ayni diyalogu kullanir.
*/
const CrudEndpointDialog = ({
isOpen,
tableName,
schemaName,
dataSourceCode,
onClose,
onChanged,
}: CrudEndpointDialogProps) => {
const { translate } = useLocalization()
const { checkPermission } = usePermission()
const canManageCrudEndpoints = checkPermission(CRUD_ENDPOINT_PERMISSION)
const entityName = toEntityName(tableName)
const [endpoints, setEndpoints] = useState<CrudEndpoint[]>([])
const [columns, setColumns] = useState<DatabaseColumnDto[]>([])
const [loading, setLoading] = useState(false)
const [generating, setGenerating] = useState(false)
const [deletingAll, setDeletingAll] = useState(false)
const [busyId, setBusyId] = useState<string | null>(null)
const [expandedId, setExpandedId] = useState<string | null>(null)
const [confirmDeleteAll, setConfirmDeleteAll] = useState(false)
const [testingId, setTestingId] = useState<string | null>(null)
const [testResults, setTestResults] = useState<Record<string, TestResult>>({})
const [parameterValues, setParameterValues] = useState<Record<string, Record<string, string>>>({})
const [requestBodies, setRequestBodies] = useState<Record<string, string>>({})
const pushError = useCallback((message: string) => {
toast.push(
<Notification type="danger" duration={3000}>
{message}
</Notification>,
{ placement: 'bottom-end' },
)
}, [])
const pushSuccess = useCallback((message: string) => {
toast.push(
<Notification type="success" duration={2000}>
{message}
</Notification>,
{ placement: 'bottom-end' },
)
}, [])
const publish = useCallback(
(next: CrudEndpoint[]) => {
// Liste veritabani sirasiyla gelir; kullaniciya her zaman CRUD sirasinda gosterilir.
const sorted = sortByCrudOperation(next)
setEndpoints(sorted)
onChanged?.(entityName, sorted)
},
[entityName, onChanged],
)
useEffect(() => {
if (!isOpen || !entityName) return
let cancelled = false
setLoading(true)
setExpandedId(null)
setTestResults({})
developerKitService
.getEndpointsByEntity(entityName)
.then((items) => {
if (!cancelled) setEndpoints(sortByCrudOperation(items))
})
.catch((err) => {
if (!cancelled) pushError(getErrorMessage(err, translate('::App.Platform.ErrorLoading')))
})
.finally(() => {
if (!cancelled) setLoading(false)
})
return () => {
cancelled = true
}
}, [isOpen, entityName, pushError, translate])
// Govde sablonu icin tablo kolonlari; datasource bilinmiyorsa genel sablon kullanilir.
useEffect(() => {
if (!isOpen || !dataSourceCode) {
setColumns([])
return
}
let cancelled = false
sqlObjectManagerService
.getTableColumns(dataSourceCode, schemaName || 'dbo', tableName)
.then((res) => {
if (!cancelled) setColumns(res.data || [])
})
.catch(() => {
// Kolonlar okunamazsa tester genel govde sablonuyla calismaya devam eder.
if (!cancelled) setColumns([])
})
return () => {
cancelled = true
}
}, [isOpen, dataSourceCode, schemaName, tableName])
const sampleBodies = useMemo(() => {
if (columns.length === 0) return { create: GENERIC_BODY, update: GENERIC_BODY }
const build = (includeId: boolean) => {
const body: Record<string, unknown> = {}
for (const column of columns) {
const isSystem = SYSTEM_COLUMNS.has(column.columnName.toLowerCase())
const isId = column.columnName.toLowerCase() === 'id'
if (isSystem && !(includeId && isId)) continue
body[column.columnName] = isId ? EMPTY_GUID : getSampleValue(column)
}
return JSON.stringify(body, null, 2)
}
return { create: build(false), update: build(true) }
}, [columns])
const getDefaultBody = useCallback(
(endpoint: CrudEndpoint) =>
endpoint.operationType === 'Update' ? sampleBodies.update : sampleBodies.create,
[sampleBodies],
)
const getParameters = useCallback(
(endpoint: CrudEndpoint): EndpointParameter[] => {
const values = parameterValues[endpoint.id]
const valueOf = (name: string, fallback: string) => values?.[name] ?? fallback
const pathParams: EndpointParameter[] = getPathParameterNames(endpoint.path).map((name) => ({
name,
type: 'path',
value: valueOf(name, name.toLowerCase() === 'id' ? EMPTY_GUID : ''),
}))
if (endpoint.operationType !== 'GetList') return pathParams
return [
...pathParams,
{ name: 'SkipCount', type: 'query', value: valueOf('SkipCount', '0') },
{ name: 'MaxResultCount', type: 'query', value: valueOf('MaxResultCount', '10') },
{ name: 'Sorting', type: 'query', value: valueOf('Sorting', '') },
]
},
[parameterValues],
)
const handleGenerate = async () => {
setGenerating(true)
try {
const result = await developerKitService.generateCrudEndpoints(entityName)
publish(result.items || [])
pushSuccess(translate('::App.Platform.OperationCompleted'))
} catch (err) {
pushError(getErrorMessage(err, translate('::App.Platform.IslemBasarisiz')))
} finally {
setGenerating(false)
}
}
const handleDeleteAll = async () => {
setDeletingAll(true)
try {
await developerKitService.deleteGeneratedEndpointsByEntity(entityName)
publish([])
pushSuccess(translate('::App.Platform.ObjectDeletedSuccessfully'))
} catch (err) {
pushError(getErrorMessage(err, translate('::App.Platform.ErrorDeleting')))
} finally {
setDeletingAll(false)
}
}
const handleToggle = async (endpoint: CrudEndpoint) => {
setBusyId(endpoint.id)
try {
const updated = await developerKitService.toggleGeneratedEndpoint(endpoint.id)
publish(endpoints.map((ep) => (ep.id === endpoint.id ? updated : ep)))
} catch (err) {
pushError(getErrorMessage(err, translate('::App.Platform.IslemBasarisiz')))
} finally {
setBusyId(null)
}
}
const handleDelete = async (endpoint: CrudEndpoint) => {
setBusyId(endpoint.id)
try {
await developerKitService.deleteGeneratedEndpoint(endpoint.id)
publish(endpoints.filter((ep) => ep.id !== endpoint.id))
} catch (err) {
pushError(getErrorMessage(err, translate('::App.Platform.ErrorDeleting')))
} finally {
setBusyId(null)
}
}
/**
* Endpoint'i doldurulan parametre ve govde ile cagirir. Istek apiService uzerinden
* gider; Authorization, __tenant ve Accept-Language interceptor'lari uygulanir.
*/
const handleTest = async (endpoint: CrudEndpoint) => {
setTestingId(endpoint.id)
const startedAt = performance.now()
try {
const parameters = getParameters(endpoint)
const pathValues = new Map(
parameters.filter((p) => p.type === 'path').map((p) => [p.name, p.value]),
)
const url = endpoint.path.replace(/\{(\w+)\}/g, (match, name: string) => {
const value = pathValues.get(name)
return value ? encodeURIComponent(value) : match
})
const params = Object.fromEntries(
parameters
.filter((p) => p.type === 'query' && p.value !== '')
.map((p) => [p.name, p.value]),
)
let data: unknown
if (needsBody(endpoint)) {
const raw = requestBodies[endpoint.id] ?? getDefaultBody(endpoint)
try {
data = JSON.parse(raw)
} catch {
setTestResults((prev) => ({
...prev,
[endpoint.id]: {
success: false,
status: 0,
durationMs: 0,
payload: translate('::App.Platform.InvalidJson'),
},
}))
return
}
}
const response = await apiService.fetchData({
method: endpoint.method,
url,
params: Object.keys(params).length > 0 ? params : undefined,
data,
timeout: 15000,
})
setTestResults((prev) => ({
...prev,
[endpoint.id]: {
success: true,
status: response.status,
durationMs: Math.round(performance.now() - startedAt),
payload: JSON.stringify(response.data, null, 2),
},
}))
} catch (error: unknown) {
const axiosError = error as { response?: { status?: number; data?: unknown }; message?: string }
setTestResults((prev) => ({
...prev,
[endpoint.id]: {
success: false,
status: axiosError.response?.status ?? 0,
durationMs: Math.round(performance.now() - startedAt),
payload: JSON.stringify(axiosError.response?.data ?? axiosError.message, null, 2),
},
}))
} finally {
setTestingId(null)
}
}
const copyToClipboard = async (text: string) => {
try {
await navigator.clipboard.writeText(text)
pushSuccess(translate('::App.Platform.Copied'))
} catch (err) {
pushError(getErrorMessage(err, translate('::App.Platform.CopyFailed')))
}
}
const activeCount = endpoints.reduce((sum, ep) => sum + (ep.isActive ? 1 : 0), 0)
const busy = loading || generating || deletingAll
// Izin yoksa dialog hic acilmaz; cagiran ekranlar da butonu gizler.
if (!canManageCrudEndpoints) return null
return (
<Dialog isOpen={isOpen} onClose={onClose} onRequestClose={onClose} width={900}>
<Dialog.Body className="flex flex-col gap-3 mt-2">
<div>
<h5 className="mb-0">{translate('::App.SqlQueryManager.CrudEndpoints')}</h5>
<p className="mb-0 text-sm text-gray-500 dark:text-gray-400">
{schemaName ? `${schemaName}.${tableName}` : tableName}
{' — '}
{entityName}
</p>
</div>
<div className="min-h-[180px] max-h-[55vh] overflow-y-auto">
{loading ? (
<div className="flex items-center justify-center gap-2 py-12 text-gray-400">
<FaSyncAlt className="animate-spin" />
<span className="text-sm">{translate('::App.Platform.LoadingWithThreeDot')}</span>
</div>
) : endpoints.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12 text-gray-400 dark:text-gray-500">
<FaBolt className="mb-3 text-3xl text-gray-200 dark:text-gray-700" />
<p className="mb-0 font-medium">
{translate('::App.DeveloperKitCrudEndpoints.NoEndpointsYet')}
</p>
<p className="mt-1 mb-0 text-sm">
{translate('::App.DeveloperKitCrudEndpoints.ClickToCreate')}
</p>
</div>
) : (
<div className="space-y-2">
{endpoints.map((ep) => {
const meta = getOperationMeta(ep.operationType)
const isExpanded = expandedId === ep.id
const parameters = isExpanded ? getParameters(ep) : []
const testResult = testResults[ep.id]
const isTesting = testingId === ep.id
return (
<div
key={ep.id}
className="overflow-hidden rounded-lg border border-gray-200 dark:border-gray-700"
>
<div className="flex items-center gap-3 bg-white p-2 dark:bg-gray-800">
<Button
size="xs"
variant="plain"
shape="circle"
disabled={busyId === ep.id}
title={
ep.isActive
? translate('::App.DeveloperKitCrudEndpoints.Disable')
: translate('::App.DeveloperKitCrudEndpoints.Enable')
}
onClick={() => handleToggle(ep)}
icon={
busyId === ep.id ? (
<FaSyncAlt className="animate-spin" />
) : ep.isActive ? (
<FaToggleOn className="text-lg text-green-500" />
) : (
<FaToggleOff className="text-lg text-gray-300" />
)
}
/>
<span
className={`flex-shrink-0 rounded border px-2 py-0.5 text-xs font-bold ${
METHOD_COLOR[ep.method] ||
'border-gray-200 bg-gray-100 text-gray-700 dark:border-gray-700 dark:bg-gray-700 dark:text-gray-200'
}`}
>
{ep.method}
</span>
<span className={`flex-shrink-0 ${meta.color}`}>{meta.icon}</span>
<div className="min-w-0 flex-1">
<span className="text-sm font-medium text-gray-800 dark:text-gray-100">
{ep.operationType}
</span>
<code className="ml-2 rounded bg-gray-100 px-2 py-0.5 text-xs text-gray-500 dark:bg-gray-700 dark:text-gray-300">
{ep.path}
</code>
</div>
{testResult && !isExpanded && (
<span
className={`flex-shrink-0 text-xs font-semibold ${
testResult.success ? 'text-green-600' : 'text-red-600'
}`}
>
{testResult.status}
</span>
)}
<Button
size="xs"
variant="plain"
shape="circle"
title={translate('::App.Platform.Delete')}
disabled={busyId === ep.id}
onClick={() => handleDelete(ep)}
icon={<FaTrash className="text-red-500" />}
/>
<Button
size="xs"
variant="plain"
shape="circle"
aria-expanded={isExpanded}
title={translate('::App.DeveloperKitCrudEndpoints.TestDetails')}
onClick={() => setExpandedId(isExpanded ? null : ep.id)}
icon={isExpanded ? <FaChevronDown /> : <FaChevronRight />}
/>
</div>
{isExpanded && (
<div className="space-y-3 border-t border-gray-200 bg-gray-50 p-3 dark:border-gray-700 dark:bg-gray-900">
{parameters.length > 0 && (
<div>
<p className="mb-2 text-xs font-semibold text-gray-600 dark:text-gray-300">
{translate('::App.DeveloperKitCrudEndpoints.Parameters')}
</p>
<div className="space-y-2">
{parameters.map((param) => (
<div key={param.name} className="flex items-center gap-2">
<span
className={`w-32 flex-shrink-0 rounded px-1.5 py-0.5 font-mono text-xs ${
param.type === 'path'
? 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300'
: 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300'
}`}
>
{param.name}
</span>
<input
type="text"
value={param.value}
aria-label={param.name}
onChange={(e) =>
setParameterValues((prev) => ({
...prev,
[ep.id]: { ...prev[ep.id], [param.name]: e.target.value },
}))
}
className="flex-1 rounded border border-gray-300 bg-white px-2 py-1 text-xs text-gray-900 focus:ring-1 focus:ring-blue-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100"
/>
</div>
))}
</div>
</div>
)}
{needsBody(ep) && (
<div>
<div className="mb-1 flex items-center justify-between">
<span className="text-xs font-semibold text-gray-600 dark:text-gray-300">
{translate('::App.DeveloperKitCrudEndpoints.RequestBody')}
</span>
<Button
size="xs"
variant="plain"
onClick={() =>
setRequestBodies((prev) => ({
...prev,
[ep.id]: getDefaultBody(ep),
}))
}
>
{translate('::App.Platform.Reset')}
</Button>
</div>
<textarea
rows={8}
spellCheck={false}
value={requestBodies[ep.id] ?? getDefaultBody(ep)}
aria-label={translate('::App.DeveloperKitCrudEndpoints.RequestBody')}
onChange={(e) =>
setRequestBodies((prev) => ({ ...prev, [ep.id]: e.target.value }))
}
className="w-full rounded border border-gray-300 bg-white px-2 py-1.5 font-mono text-xs text-gray-900 focus:ring-1 focus:ring-blue-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100"
/>
</div>
)}
<div className="flex items-center gap-2">
<Button
size="sm"
variant="solid"
disabled={isTesting}
onClick={() => handleTest(ep)}
icon={
isTesting ? (
<FaSyncAlt className="animate-spin" />
) : (
<FaPaperPlane />
)
}
>
{isTesting
? translate('::App.DeveloperKitEndpoint.SendLoading')
: translate('::App.DeveloperKitCrudEndpoints.Test')}
</Button>
{testResult && (
<Button
size="sm"
variant="plain"
onClick={() =>
setTestResults((prev) => {
const next = { ...prev }
delete next[ep.id]
return next
})
}
>
{translate('::App.Platform.Clear')}
</Button>
)}
{!ep.isActive && (
<span className="text-xs text-amber-600 dark:text-amber-400">
{translate('::App.DeveloperKitCrudEndpoints.InactiveCount')}
</span>
)}
</div>
{testResult && (
<div
className={`rounded-lg border p-2 ${
testResult.success
? 'border-green-200 bg-green-50 dark:border-green-700 dark:bg-green-900/20'
: 'border-red-200 bg-red-50 dark:border-red-700 dark:bg-red-900/20'
}`}
>
<div className="mb-2 flex items-center gap-2">
{testResult.success ? (
<FaCheckCircle className="text-green-500" />
) : (
<FaExclamationCircle className="text-red-500" />
)}
<span className="text-sm font-medium">HTTP {testResult.status}</span>
<span className="ml-auto text-xs text-gray-500 dark:text-gray-400">
{testResult.durationMs} ms
</span>
<Button
size="xs"
variant="plain"
shape="circle"
title={translate('::App.Platform.CopyToClipboard')}
onClick={() => copyToClipboard(testResult.payload)}
icon={<FaCopy className="text-xs" />}
/>
</div>
<pre className="max-h-56 overflow-auto rounded border border-gray-200 bg-white p-2 text-xs text-gray-900 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100">
{testResult.payload}
</pre>
</div>
)}
</div>
)}
</div>
)
})}
</div>
)}
</div>
{endpoints.length > 0 && (
<div className="flex items-center gap-4 border-t pt-2 text-xs text-gray-500 dark:text-gray-400">
<span>
{activeCount} {translate('::App.Listform.ListformField.IsActive')}
</span>
<span>
{endpoints.length - activeCount}{' '}
{translate('::App.DeveloperKitCrudEndpoints.InactiveCount')}
</span>
</div>
)}
</Dialog.Body>
<Dialog.Footer className="mt-1 flex flex-wrap items-center gap-2 border-t pt-3">
{endpoints.length > 0 && (
<Button
size="sm"
variant="default"
icon={deletingAll ? <FaSyncAlt className="animate-spin" /> : <FaTrash />}
disabled={busy}
onClick={() => setConfirmDeleteAll(true)}
className="text-red-600"
>
{translate('::App.DeveloperKitCrudEndpoints.DeleteAll')}
</Button>
)}
<Button
size="sm"
variant="solid"
icon={generating ? <FaSyncAlt className="animate-spin" /> : <FaBolt />}
disabled={busy}
onClick={handleGenerate}
>
{endpoints.length > 0
? translate('::App.DeveloperKitCrudEndpoints.Regenerate')
: translate('::App.DeveloperKitCrudEndpoints.CreateCrudEndpoint')}
</Button>
<Button className="ml-auto" variant="solid" onClick={onClose}>
{translate('::App.Platform.Close')}
</Button>
</Dialog.Footer>
<ConfirmDialog
isOpen={confirmDeleteAll}
type="danger"
title={translate('::App.Platform.DeleteConfirmation')}
confirmText={translate('::App.DeveloperKitCrudEndpoints.DeleteAll')}
cancelText={translate('::App.Platform.Cancel')}
confirmButtonColor="red-600"
onCancel={() => setConfirmDeleteAll(false)}
onConfirm={() => {
setConfirmDeleteAll(false)
handleDeleteAll()
}}
>
<p>{translate('::App.Platform.DeleteConfirmationMessage')}</p>
</ConfirmDialog>
</Dialog>
)
}
export default CrudEndpointDialog

File diff suppressed because it is too large Load diff

View file

@ -11,12 +11,18 @@ import {
FaCode,
FaDatabase,
FaTrash,
FaBolt,
FaCopy,
} from 'react-icons/fa'
import type { DatabaseTableDto, SqlNativeObjectDto } from '@/proxy/sql-query-manager/models'
import type { CrudEndpoint } from '@/proxy/developerKit/models'
import { DataSourceTypeEnum } from '@/proxy/form/models'
import { sqlObjectManagerService } from '@/services/sql-query-manager.service'
import { useLocalization } from '@/utils/hooks/useLocalization'
import Button from '@/components/ui/Button'
import { Notification, toast } from '@/components/ui'
import { CrudEndpointBadges, getErrorMessage, toEntityName } from './CrudEndpointDialog'
import { developerKitService } from '@/services/developerKit.service'
type FolderKey = 'tables' | 'views' | 'procedures' | 'functions'
@ -38,6 +44,16 @@ interface SqlObjectExplorerProps {
onDesignTable?: (schemaName: string, tableName: string) => void
onNewTable?: () => void
onSelectedObjectsChange?: (objects: SqlExplorerSelectedObject[]) => void
/** Entity adina gore acilmis CRUD endpoint'leri; tablo satirlarinda ikon olarak gosterilir. */
crudEndpointsByEntity?: Map<string, CrudEndpoint[]>
onManageCrudEndpoints?: (schemaName: string, tableName: string) => void
/** Secili tablolar icin CRUD endpoint uretir; verilmezse buton gosterilmez. */
onGenerateCrudEndpoints?: () => void
isGeneratingCrudEndpoints?: boolean
/** Kopyalama / dogrudan SQL diyalogunu acar; verilmezse buton gosterilmez. */
onCopySelectedObjects?: () => void
/** Explorer endpoint'leri degistirdiginde (orn. tablo drop) listeyi tazelemek icin cagrilir. */
onCrudEndpointsChanged?: () => void
refreshTrigger?: number
}
@ -65,6 +81,12 @@ const SqlObjectExplorer = ({
onDesignTable,
onNewTable,
onSelectedObjectsChange,
crudEndpointsByEntity,
onManageCrudEndpoints,
onGenerateCrudEndpoints,
isGeneratingCrudEndpoints,
onCopySelectedObjects,
onCrudEndpointsChanged,
refreshTrigger,
}: SqlObjectExplorerProps) => {
const { translate } = useLocalization()
@ -74,7 +96,12 @@ const SqlObjectExplorer = ({
const [loading, setLoading] = useState(false)
const [filterText, setFilterText] = useState('')
const [selectedObjectIds, setSelectedObjectIds] = useState<Set<string>>(new Set())
const [dropConfirm, setDropConfirm] = useState<{ node: TreeNode } | null>(null)
/**
* Silinmek uzere onay bekleyen nesneler. Satirdaki cop kutusu tek nesneyi,
* baslikaki "secilenleri sil" tum secimi ayni listeye koyar; boylece tekil ve
* toplu silme tek kod yolundan gecer.
*/
const [dropTargets, setDropTargets] = useState<TreeNode[] | null>(null)
const [dropping, setDropping] = useState(false)
const [contextMenu, setContextMenu] = useState<{
show: boolean
@ -190,12 +217,22 @@ const SqlObjectExplorer = ({
.filter(Boolean) as TreeNode[]
}
/**
* Tek tik nesneyi secer/secimi kaldirir (checkbox ile ayni state), klasorlerde acar/kapatir.
* Sorgu sablonu ya da tanim yuklemek icin cift tik kullanilir.
*/
const handleNodeClick = (node: TreeNode) => {
if (node.type !== 'object') {
toggleNode(node.id)
return
}
toggleObjectSelection(node.id, !selectedObjectIds.has(node.id))
}
const handleNodeDoubleClick = (node: TreeNode) => {
if (node.type !== 'object') return
if (node.folder === 'tables') {
// Generate SELECT template for tables
const t = node.data as DatabaseTableDto
@ -242,15 +279,12 @@ const SqlObjectExplorer = ({
}
}
const getSelectedObjects = (nodes: TreeNode[], ids: Set<string>): SqlExplorerSelectedObject[] => {
const selected: SqlExplorerSelectedObject[] = []
const getSelectedNodes = (nodes: TreeNode[], ids: Set<string>): TreeNode[] => {
const selected: TreeNode[] = []
const walk = (list: TreeNode[]) => {
for (const node of list) {
if (node.type === 'object' && ids.has(node.id)) {
const mapped = mapNodeToSelectedObject(node)
if (mapped) selected.push(mapped)
}
if (node.type === 'object' && ids.has(node.id)) selected.push(node)
if (node.children?.length) walk(node.children)
}
}
@ -259,6 +293,11 @@ const SqlObjectExplorer = ({
return selected
}
const getSelectedObjects = (nodes: TreeNode[], ids: Set<string>): SqlExplorerSelectedObject[] =>
getSelectedNodes(nodes, ids)
.map(mapNodeToSelectedObject)
.filter((obj): obj is SqlExplorerSelectedObject => obj !== null)
const toggleObjectSelection = (nodeId: string, checked: boolean) => {
setSelectedObjectIds((prev) => {
const next = new Set(prev)
@ -297,29 +336,88 @@ const SqlObjectExplorer = ({
return [obj.objectName, `${obj.schemaName}_${obj.objectName}`]
}
/**
* Onaylanan nesneleri tek tek dusurur; bir nesnenin hatasi digerlerini engellemez.
* Basarili olanlar icin SQL seed dosyalari ve (tablolarda) CRUD endpoint'leri de temizlenir.
*/
const handleDrop = async () => {
if (!dropConfirm || !dataSource) return
setDropping(true)
try {
await sqlObjectManagerService.executeQuery({
queryText: buildDropSql(dropConfirm.node),
dataSourceCode: dataSource,
})
if (!dropTargets?.length || !dataSource) return
// If a matching seed file exists under the seeds folder (configs/seeds/SqlData), delete it too.
try {
const fileNames = [...new Set(getSqlDataFileCandidates(dropConfirm.node).filter(Boolean))]
if (fileNames.length > 0) {
await sqlObjectManagerService.deleteSqlDataFiles({ fileNames })
setDropping(true)
const dropped: TreeNode[] = []
const failures: string[] = []
try {
for (const node of dropTargets) {
try {
const result = await sqlObjectManagerService.executeQuery({
queryText: buildDropSql(node),
dataSourceCode: dataSource,
})
if (result.data?.success === false) {
throw new Error(result.data.error || result.data.message)
}
dropped.push(node)
} catch (err) {
failures.push(
`${node.label}: ${getErrorMessage(err, translate('::App.Platform.IslemBasarisiz'))}`,
)
}
} catch {
// Non-blocking: object drop succeeded even if seed file cleanup fails.
}
setDropConfirm(null)
if (dropped.length > 0) {
// configs/seeds/SqlData altindaki eslesen seed dosyalari da silinir.
try {
const fileNames = [
...new Set(dropped.flatMap(getSqlDataFileCandidates).filter(Boolean)),
]
if (fileNames.length > 0) {
await sqlObjectManagerService.deleteSqlDataFiles({ fileNames })
}
} catch {
// Non-blocking: object drop succeeded even if seed file cleanup fails.
}
// Tablo dusurulduyse ona ait CRUD endpoint'leri de anlamsiz kalir; kayitlar
// silinince backend crud/{EntityName}.json seed dosyasini da kaldirir.
const droppedTables = dropped.filter((node) => node.folder === 'tables')
if (droppedTables.length > 0) {
for (const node of droppedTables) {
try {
const table = node.data as DatabaseTableDto
await developerKitService.deleteGeneratedEndpointsByEntity(
toEntityName(table.tableName),
)
} catch {
// Non-blocking: izin yoksa ya da endpoint yoksa drop yine basarilidir.
}
}
onCrudEndpointsChanged?.()
}
// Silinen nesneler secimde kalmasin; aksi halde toolbar sayaci yaniltir.
const droppedIds = new Set(dropped.map((node) => node.id))
setSelectedObjectIds((prev) => new Set([...prev].filter((id) => !droppedIds.has(id))))
}
toast.push(
<Notification
type={failures.length > 0 ? 'warning' : 'success'}
title={
failures.length > 0
? translate('::App.Platform.Warning')
: translate('::App.Platform.Success')
}
>
{failures.length > 0
? failures.join(' | ')
: `${translate('::App.Platform.OperationCompleted')} (${dropped.length})`}
</Notification>,
{ placement: 'bottom-end' },
)
setDropTargets(null)
loadObjects()
} catch (err: any) {
console.error('Drop failed', err)
} finally {
setDropping(false)
}
@ -341,15 +439,35 @@ const SqlObjectExplorer = ({
return <FaColumns className="text-gray-400" />
}
const getNodeCrudEndpoints = (node: TreeNode): CrudEndpoint[] => {
if (node.type !== 'object' || node.folder !== 'tables' || !crudEndpointsByEntity) return []
const table = node.data as DatabaseTableDto
return crudEndpointsByEntity.get(toEntityName(table.tableName)) ?? []
}
const buildCrudEndpointTitle = (endpoint: CrudEndpoint) =>
`${endpoint.operationType}${endpoint.method} ${endpoint.path} (${
endpoint.isActive
? translate('::App.Listform.ListformField.IsActive')
: translate('::App.DeveloperKitCrudEndpoints.InactiveCount')
})`
const renderNode = (node: TreeNode, level = 0) => {
const isExpanded = expandedNodes.has(node.id)
const isChecked = selectedObjectIds.has(node.id)
const crudEndpoints = getNodeCrudEndpoints(node)
return (
<div key={node.id}>
<div
className="group flex items-center gap-2 py-1 px-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-700 rounded"
className={`group flex items-center gap-2 py-1 px-2 cursor-pointer rounded ${
isChecked
? 'bg-blue-50 dark:bg-blue-900/30 hover:bg-blue-100 dark:hover:bg-blue-900/40'
: 'hover:bg-gray-100 dark:hover:bg-gray-700'
}`}
style={{ paddingLeft: `${level * 16 + 8}px` }}
title={node.type === 'object' ? translate('::App.Platform.SelectOrOpenHint') : undefined}
onClick={() => handleNodeClick(node)}
onDoubleClick={() => handleNodeDoubleClick(node)}
onContextMenu={(e) => {
e.preventDefault()
setContextMenu({ show: true, x: e.clientX, y: e.clientY, node })
@ -366,6 +484,20 @@ const SqlObjectExplorer = ({
)}
{getIcon(node)}
<span className="text-sm flex-1 truncate">{node.label}</span>
{crudEndpoints.length > 0 && (
<CrudEndpointBadges
endpoints={crudEndpoints}
buildTitle={buildCrudEndpointTitle}
onClick={
onManageCrudEndpoints
? () => {
const table = node.data as DatabaseTableDto
onManageCrudEndpoints(table.schemaName, table.tableName)
}
: undefined
}
/>
)}
{node.type === 'object' && (
<Button
title="Drop"
@ -376,7 +508,7 @@ const SqlObjectExplorer = ({
className="opacity-0 group-hover:opacity-100 !h-6 !w-6 !px-0 hover:!bg-red-100 dark:hover:!bg-red-900 text-red-500 transition-opacity flex-shrink-0"
onClick={(e) => {
e.stopPropagation()
setDropConfirm({ node })
setDropTargets([node])
}}
/>
)}
@ -390,6 +522,11 @@ const SqlObjectExplorer = ({
const filteredTree = filterTree(treeData, filterText)
/** CRUD endpoint uretimi yalnizca tablolar icin anlamli. */
const selectedTableCount = getSelectedNodes(treeData, selectedObjectIds).filter(
(node) => node.folder === 'tables',
).length
useEffect(() => {
if (filterText.trim()) {
const allIds = new Set<string>()
@ -418,6 +555,66 @@ const SqlObjectExplorer = ({
return (
<div className="flex-1 flex flex-col min-h-0">
{/*
Nesne islemleri seridi. Yuksekligi sabit (min-h) secim yapildikca
butonlar gorunse de agac asagi kaymaz.
*/}
<div className="flex min-h-[34px] items-center gap-2 border-b px-3 py-1 bg-gray-50 dark:bg-gray-800 flex-shrink-0 rounded-t-lg text-xs">
{selectedObjectIds.size > 0 && (
<>
<span className="font-medium text-blue-700 dark:text-blue-300 whitespace-nowrap">
{selectedObjectIds.size} {translate('::App.SqlQueryManager.Selected')}
</span>
<Button
size="xs"
variant="plain"
className="!h-auto !px-1 !py-0 text-xs text-blue-600 hover:underline dark:text-blue-300"
onClick={() => setSelectedObjectIds(new Set())}
>
{translate('::App.SqlQueryManager.ClearSelection')}
</Button>
</>
)}
<div className="ml-auto flex flex-wrap items-center justify-end gap-2">
{onCopySelectedObjects && selectedObjectIds.size > 0 && (
<Button
size="xs"
variant="plain"
icon={<FaCopy className="text-xs" />}
className="!h-auto !px-1 !py-0 text-xs text-gray-600 hover:underline dark:text-gray-300"
title={translate('::App.Platform.CopyOrExecuteSql')}
onClick={onCopySelectedObjects}
>
{translate('::App.Platform.CopySelectedObjects')}
</Button>
)}
{selectedObjectIds.size > 0 && (
<Button
size="xs"
variant="plain"
icon={<FaTrash className="text-xs" />}
className="!h-auto !px-1 !py-0 text-xs text-red-600 hover:underline dark:text-red-400"
onClick={() => setDropTargets(getSelectedNodes(treeData, selectedObjectIds))}
>
{translate('::App.SqlQueryManager.DropSelected')}
</Button>
)}
{onGenerateCrudEndpoints && selectedTableCount > 0 && (
<Button
size="xs"
variant="plain"
icon={<FaBolt className="text-xs" />}
loading={isGeneratingCrudEndpoints}
className="!h-auto !px-1 !py-0 text-xs text-amber-600 hover:underline dark:text-amber-400"
title={translate('::App.DeveloperKitCrudEndpoints.CreateCrudEndpointHint')}
onClick={onGenerateCrudEndpoints}
>
{translate('::App.DeveloperKitCrudEndpoints.CreateCrudEndpoint')}
</Button>
)}
</div>
</div>
{/* Search + refresh */}
<div className="p-2 border-b flex gap-2 flex-shrink-0">
<input
@ -500,6 +697,22 @@ const SqlObjectExplorer = ({
</Button>
)}
{isTableObj && onManageCrudEndpoints && (
<Button
variant="plain"
shape="none"
className="w-full !h-auto !justify-start !rounded-none !px-4 !py-2 text-left hover:bg-gray-100 dark:hover:bg-gray-700 text-sm flex items-center gap-2"
onClick={() => {
const t = ctxNode!.data as DatabaseTableDto
onManageCrudEndpoints(t.schemaName, t.tableName)
closeCtx()
}}
>
<FaBolt className="text-amber-500" />{' '}
{translate('::App.SqlQueryManager.CrudEndpoints')}
</Button>
)}
{/* NATIVE object <20> View Definition */}
{isNativeObj && (
<Button
@ -607,52 +820,56 @@ const SqlObjectExplorer = ({
</>
)}
{/* Drop Confirm Dialog */}
{dropConfirm && (
<>
{/* Drop Confirm Dialog — tekil ve toplu silmede ayni diyalog */}
{dropTargets && dropTargets.length > 0 && (
<div
className="fixed inset-0 z-50 bg-black/40 flex items-center justify-center"
onClick={() => !dropping && setDropTargets(null)}
>
<div
className="fixed inset-0 z-50 bg-black/40 flex items-center justify-center"
onClick={() => !dropping && setDropConfirm(null)}
className="bg-white dark:bg-gray-800 rounded-lg shadow-xl p-6 max-w-lg w-full mx-4"
onClick={(e) => e.stopPropagation()}
>
<div
className="bg-white dark:bg-gray-800 rounded-lg shadow-xl p-6 max-w-sm w-full mx-4"
onClick={(e) => e.stopPropagation()}
>
<div className="flex items-center gap-3 mb-3">
<FaTrash className="text-red-500 text-lg flex-shrink-0" />
<h6 className="font-semibold text-gray-900 dark:text-gray-100">Drop Object</h6>
</div>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-1">
The following object will be permanently dropped:
</p>
<code className="block text-sm bg-gray-100 dark:bg-gray-700 rounded px-3 py-2 mb-4 break-all">
{buildDropSql(dropConfirm.node)}
</code>
<div className="flex justify-end gap-2">
<Button
disabled={dropping}
size="sm"
variant="default"
className="disabled:opacity-50"
onClick={() => setDropConfirm(null)}
>
{translate('::App.Platform.Cancel')}
</Button>
<Button
disabled={dropping}
size="sm"
variant="solid"
color="red-600"
className="hover:!bg-red-700 disabled:opacity-50 flex items-center gap-2"
onClick={handleDrop}
>
{dropping && <FaSyncAlt className="animate-spin text-xs" />}
Drop
</Button>
</div>
<div className="flex items-center gap-3 mb-3">
<FaTrash className="text-red-500 text-lg flex-shrink-0" />
<h6 className="font-semibold text-gray-900 dark:text-gray-100">
{translate('::App.SqlQueryManager.DropObjects')} ({dropTargets.length})
</h6>
</div>
<p className="text-sm text-gray-600 dark:text-gray-400 mb-1">
{translate('::App.SqlQueryManager.DropObjectsWarning')}
</p>
<div className="mb-4 max-h-60 overflow-auto rounded bg-gray-100 dark:bg-gray-700">
{dropTargets.map((node) => (
<code key={node.id} className="block px-3 py-1 text-sm break-all">
{buildDropSql(node)}
</code>
))}
</div>
<div className="flex justify-end gap-2">
<Button
disabled={dropping}
size="sm"
variant="default"
className="disabled:opacity-50"
onClick={() => setDropTargets(null)}
>
{translate('::App.Platform.Cancel')}
</Button>
<Button
disabled={dropping}
size="sm"
variant="solid"
color="red-600"
className="hover:!bg-red-700 disabled:opacity-50 flex items-center gap-2"
onClick={handleDrop}
>
{dropping && <FaSyncAlt className="animate-spin text-xs" />}
{translate('::App.SqlQueryManager.Drop')}
</Button>
</div>
</div>
</>
</div>
)}
</div>
)

View file

@ -1,23 +0,0 @@
interface SqlObjectPropertiesProps {
object?: any
}
const SqlObjectProperties = ({ object }: SqlObjectPropertiesProps) => {
if (!object) {
return (
<div className="flex items-center justify-center h-full text-gray-400 text-sm">
Select an object to view properties
</div>
)
}
return (
<div className="p-4 text-sm">
<pre className="text-xs text-gray-600 dark:text-gray-400 overflow-auto">
{JSON.stringify(object, null, 2)}
</pre>
</div>
)
}
export default SqlObjectProperties

View file

@ -1,16 +1,17 @@
import { lazy, Suspense, useState, useCallback, useEffect, useRef } from 'react'
import { lazy, Suspense, useState, useCallback, useEffect, useMemo, useRef } from 'react'
import { Button, Dialog, Notification, toast } from '@/components/ui'
import Container from '@/components/shared/Container'
import { getDataSources } from '@/services/data-source.service'
import type { DataSourceDto } from '@/proxy/data-source'
import { DataSourceTypeEnum } from '@/proxy/form/models'
import type { SqlQueryExecutionResultDto } from '@/proxy/sql-query-manager/models'
import type { CrudEndpoint } from '@/proxy/developerKit/models'
import { sqlObjectManagerService } from '@/services/sql-query-manager.service'
import { developerKitService } from '@/services/developerKit.service'
import {
FaDatabase,
FaPlay,
FaFileAlt,
FaCopy,
FaExclamationTriangle,
FaCheckCircle,
FaFolderOpen,
@ -24,6 +25,13 @@ import { Splitter } from '@/components/codeLayout/Splitter'
import PageTitle from '@/components/shared/PageTitle'
import { useStoreState } from '@/store/store'
import DbMigrateButton from '@/components/shared/DbMigrateButton'
import CrudEndpointDialog, {
CRUD_ENDPOINT_PERMISSION,
getErrorMessage,
groupEndpointsByEntity,
toEntityName,
} from './CrudEndpointDialog'
import { usePermission } from '@/utils/hooks/usePermission'
const SqlResultsGrid = lazy(() => import('./SqlResultsGrid'))
const SqlTableDesignerDialog = lazy(() => import('./SqlTableDesignerDialog'))
@ -34,9 +42,7 @@ interface SqlManagerState {
editorContent: string
isExecuting: boolean
executionResult: SqlQueryExecutionResultDto | null
showProperties: boolean
isDirty: boolean
tableColumns: any | null
refreshTrigger: number
lastExecutedQuery: string
}
@ -44,7 +50,8 @@ interface SqlManagerState {
interface SqlCopyResultItem {
targetDataSource: string
objectFullName: string
objectType: SqlExplorerSelectedObject['objectType']
/** Dogrudan SQL modunda tek bir 'script' satiri raporlanir. */
objectType: SqlExplorerSelectedObject['objectType'] | 'script'
status: 'success' | 'error' | 'skipped'
message: string
}
@ -53,6 +60,9 @@ const SqlQueryManager = () => {
const { translate } = useLocalization()
const editorRef = useRef<SqlEditorRef>(null)
const tenantName = useStoreState((state) => state.locale.currentTenantName)
const { checkPermission } = usePermission()
/** CRUD endpoint uretimi/yonetimi yalnizca bu izin verildiginde gorunur. */
const canManageCrudEndpoints = checkPermission(CRUD_ENDPOINT_PERMISSION)
const [state, setState] = useState<SqlManagerState>({
dataSources: [],
@ -61,9 +71,7 @@ const SqlQueryManager = () => {
isExecuting: false,
refreshTrigger: 0,
executionResult: null,
showProperties: false,
isDirty: false,
tableColumns: null,
lastExecutedQuery: '',
})
@ -87,10 +95,19 @@ const SqlQueryManager = () => {
const [showCopyResultDialog, setShowCopyResultDialog] = useState(false)
const [copyDialogMode, setCopyDialogMode] = useState<'objects' | 'sql'>('objects')
const [sqlScriptForCopy, setSqlScriptForCopy] = useState('')
const [crudEndpoints, setCrudEndpoints] = useState<CrudEndpoint[]>([])
const [crudDialogTable, setCrudDialogTable] = useState<{
schemaName: string
tableName: string
} | null>(null)
const [isGeneratingCrud, setIsGeneratingCrud] = useState(false)
useEffect(() => {
loadDataSources()
}, [])
if (canManageCrudEndpoints) {
loadCrudEndpoints()
}
}, [canManageCrudEndpoints])
const loadDataSources = async () => {
try {
@ -100,7 +117,11 @@ const SqlQueryManager = () => {
setState((prev) => ({
...prev,
dataSources: items,
selectedDataSource: items[0].code ?? null,
// Tenant'in kendi datasource'u varsa onunla baslar, yoksa ilk kayit secilir.
selectedDataSource:
items.find((item) => item.code === prev.selectedDataSource)?.code ??
items[0].code ??
null,
}))
}
} catch {
@ -113,6 +134,73 @@ const SqlQueryManager = () => {
}
}
const loadCrudEndpoints = async () => {
try {
const result = await developerKitService.getGeneratedListEndpoints()
setCrudEndpoints(result.items || [])
} catch {
// CRUD endpoint listesi okunamazsa SQL yonetimi calismaya devam eder;
// yalnizca tablo satirlarindaki ikonlar gosterilemez.
}
}
const crudEndpointsByEntity = useMemo(
() => groupEndpointsByEntity(crudEndpoints),
[crudEndpoints],
)
/** Dialog ya da toplu uretim sonrasi ilgili entity'nin endpoint'lerini state'te tazeler. */
const applyEntityEndpoints = useCallback((entityName: string, endpoints: CrudEndpoint[]) => {
setCrudEndpoints((prev) => [...prev.filter((ep) => ep.entityName !== entityName), ...endpoints])
}, [])
const selectedExplorerTables = useMemo(
() => selectedExplorerObjects.filter((obj) => obj.objectType === 'table'),
[selectedExplorerObjects],
)
/** Explorer'da secili tum tablolar icin CRUD endpoint'lerini uretir. */
const handleGenerateCrudEndpoints = async () => {
if (!canManageCrudEndpoints || selectedExplorerTables.length === 0) return
setIsGeneratingCrud(true)
try {
const failures: string[] = []
for (const table of selectedExplorerTables) {
const entityName = toEntityName(table.objectName)
try {
const result = await developerKitService.generateCrudEndpoints(entityName)
applyEntityEndpoints(entityName, result.items || [])
} catch (error) {
failures.push(`${table.fullName}: ${getErrorMessage(error, entityName)}`)
}
}
toast.push(
<Notification
type={failures.length > 0 ? 'warning' : 'success'}
title={
failures.length > 0
? translate('::App.Platform.Warning')
: translate('::App.Platform.Success')
}
>
{failures.length > 0
? failures.join(' | ')
: `${translate('::App.Platform.OperationCompleted')} (${selectedExplorerTables.length})`}
</Notification>,
{ placement: 'bottom-end' },
)
} finally {
setIsGeneratingCrud(false)
}
}
const handleManageCrudEndpoints = useCallback((schemaName: string, tableName: string) => {
setCrudDialogTable({ schemaName, tableName })
}, [])
const handleDataSourceChange = useCallback((dataSource: DataSourceDto) => {
setState((prev) => ({
...prev,
@ -496,7 +584,7 @@ GO`,
applyTemplate(templateContent)
}
},
[translate, state.editorContent, state.isDirty, applyTemplate],
[state.editorContent, state.isDirty, applyTemplate],
)
const handleConfirmTemplateReplace = useCallback(() => {
@ -517,7 +605,6 @@ GO`,
...prev,
editorContent: '',
executionResult: null,
tableColumns: null,
lastExecutedQuery: '',
isDirty: false,
}))
@ -555,7 +642,6 @@ GO`,
...prev,
isExecuting: true,
executionResult: null,
tableColumns: null,
lastExecutedQuery: queryToExecute,
}))
@ -569,8 +655,7 @@ GO`,
...prev,
executionResult: result.data,
isExecuting: false,
tableColumns: null,
}))
}))
// Seçili metni geri yükle
setTimeout(() => {
@ -611,8 +696,7 @@ GO`,
...prev,
editorContent: definition,
executionResult: null,
tableColumns: null,
isDirty: false,
isDirty: false,
}))
}
} catch (error: any) {
@ -646,8 +730,7 @@ GO`,
...prev,
editorContent: script,
executionResult: null,
tableColumns: null,
isDirty: false,
isDirty: false,
}))
toast.push(
@ -675,13 +758,6 @@ GO`,
setOverwriteIfExists(false)
setSqlScriptForCopy('')
setShowCopyDialog(true)
// Eğer seçili obje yoksa uyarı göster
if (selectedExplorerObjects.length === 0) {
// SQL mode'da obje seçimi zorunlu değil, object mode'da zorunlu
// Bu uyarı sadece object mode'da gerekirse
return
}
}
const handleCopyObjects = async () => {
@ -769,7 +845,9 @@ GO`,
objectFullName: obj.fullName,
objectType: obj.objectType,
status: 'error',
message: error.response?.data?.error?.message || 'Kopyalama basarisiz.',
message:
error.response?.data?.error?.message ||
translate('::App.Platform.IslemBasarisiz'),
})
}
}
@ -779,18 +857,16 @@ GO`,
const errorCount = results.filter((x) => x.status === 'error').length
const skippedCount = results.filter((x) => x.status === 'skipped').length
const notificationType = errorCount > 0 ? 'warning' : 'success'
toast.push(
<Notification
type={notificationType}
type={errorCount > 0 ? 'warning' : 'success'}
title={
errorCount > 0
? translate('::App.Platform.Warning')
: translate('::App.Platform.Success')
}
>
{translate('::App.Platform.CopyCompleted') ||
`translate('::App.Platform.Success'): ${successCount}, ${translate('::App.Platform.Error')}: ${errorCount}, ${translate('::App.Platform.Skipped')}: ${skippedCount}`}
{`${translate('::App.Platform.CopyCompleted')}${translate('::App.Platform.Success')}: ${successCount}, ${translate('::App.Platform.Skipped')}: ${skippedCount}, ${translate('::App.Platform.Error')}: ${errorCount}`}
</Notification>,
{ placement: 'bottom-end' },
)
@ -838,17 +914,19 @@ GO`,
results.push({
targetDataSource,
objectFullName: 'SQL Script',
objectType: 'script' as any,
objectType: 'script',
status: 'success',
message: 'Basariyla calistirildi.',
message: translate('::App.Platform.OperationCompleted'),
})
} catch (error: any) {
results.push({
targetDataSource,
objectFullName: 'SQL Script',
objectType: 'script' as any,
objectType: 'script',
status: 'error',
message: error.response?.data?.error?.message || 'Calistirma basarisiz.',
message:
error.response?.data?.error?.message ||
translate('::App.Platform.IslemBasarisiz'),
})
}
}
@ -856,18 +934,16 @@ GO`,
const successCount = results.filter((x) => x.status === 'success').length
const errorCount = results.filter((x) => x.status === 'error').length
const notificationType = errorCount > 0 ? 'warning' : 'success'
toast.push(
<Notification
type={notificationType}
type={errorCount > 0 ? 'warning' : 'success'}
title={
errorCount > 0
? translate('::App.Platform.Warning')
: translate('::App.Platform.Success')
}
>
{translate('::App.Platform.ExecutionCompleted') ||
`translate('::App.Platform.Success'): ${successCount}, ${translate('::App.Platform.Error')}: ${errorCount}`}
{`${translate('::App.Platform.ExecutionCompleted')}${translate('::App.Platform.Success')}: ${successCount}, ${translate('::App.Platform.Error')}: ${errorCount}`}
</Notification>,
{ placement: 'bottom-end' },
)
@ -897,6 +973,39 @@ GO`,
setCopyTargetDataSources([])
}
/** Hedef veri kaynagi secici; kopyalama ve dogrudan SQL sekmelerinde ayni. */
const renderTargetDataSourcePicker = () => (
<div className="mb-4">
<div className="text-sm font-medium mb-2">
{translate('::App.Platform.TargetDataSources')}
</div>
<label className="flex items-center gap-2 text-sm cursor-pointer mb-2">
<input
type="checkbox"
checked={allTargetsSelected}
onChange={(e) => handleToggleSelectAllTargets(e.target.checked)}
/>
<span>{translate('::App.Platform.SelectAllTargets')}</span>
</label>
<div className="max-h-44 overflow-auto border-t pt-2">
{availableTargetDataSourceCodes.map((code) => (
<label key={code} className="flex items-center gap-2 text-sm cursor-pointer">
<input
type="checkbox"
checked={copyTargetDataSources.includes(code)}
onChange={(e) =>
setCopyTargetDataSources((prev) =>
e.target.checked ? [...prev, code] : prev.filter((x) => x !== code),
)
}
/>
<span>{code}</span>
</label>
))}
</div>
</div>
)
const copySuccessCount = copyResults.filter((x) => x.status === 'success').length
const copyErrorCount = copyResults.filter((x) => x.status === 'error').length
const copySkippedCount = copyResults.filter((x) => x.status === 'skipped').length
@ -921,7 +1030,9 @@ GO`,
} catch (error: any) {
toast.push(
<Notification type="danger" title={translate('::App.Platform.Error')}>
{error.response?.data?.error?.message || error.message || 'Record operation failed.'}
{error.response?.data?.error?.message ||
error.message ||
translate('::App.Platform.IslemBasarisiz')}
</Notification>,
{ placement: 'bottom-end' },
)
@ -944,7 +1055,7 @@ GO`,
<FaDatabase className="text-lg text-blue-500" />
<select
className="border border-gray-300 rounded px-2 py-1 max-w-full dark:bg-gray-700 dark:border-gray-600"
disabled={state.selectedDataSource?.length === 0}
disabled={state.dataSources.length === 0}
value={state.selectedDataSource || ''}
onChange={(e) => {
const ds = state.dataSources.find((d) => d.code === e.target.value)
@ -973,17 +1084,7 @@ GO`,
</div>
<div className="flex flex-wrap items-center gap-2 sm:gap-3">
<Button
size="sm"
variant="default"
icon={<FaCopy />}
onClick={handleOpenCopyDialog}
disabled={!state.selectedDataSource}
className="shadow-sm px-2 py-1"
title={translate('::App.Platform.CopyOrExecuteSql')}
>
{translate('::App.Platform.CopySelectedObjects')}
</Button>
{/* Nesne bazli islemler (kopyala, sil, CRUD endpoint) explorer seridinde. */}
<Button
size="sm"
variant="default"
@ -1013,11 +1114,7 @@ GO`,
<div className="flex-1 flex min-h-0 flex-col gap-3 lg:flex-row lg:gap-4">
{/* Left Panel - Object Explorer */}
<div className="w-full lg:w-1/3 flex-shrink-0 flex flex-col h-[220px] min-h-[220px] max-h-[260px] sm:h-[260px] sm:min-h-[260px] sm:max-h-[300px] lg:h-auto lg:min-h-0 lg:max-h-none bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-600 shadow">
<div className="border-b px-4 py-2 bg-gray-50 dark:bg-gray-800 flex-shrink-0 rounded-t-lg">
<h6 className="font-semibold text-sm">
{translate('::App.Platform.ObjectExplorer')}
</h6>
</div>
{/* Baslik explorer'in kendi header seridinde; secim islemleri de orada. */}
<div className="flex-1 min-h-0 flex flex-col overflow-hidden">
<SqlObjectExplorer
dataSource={state.selectedDataSource}
@ -1029,15 +1126,25 @@ GO`,
onNewTable={handleNewTable}
onDesignTable={handleDesignTable}
onSelectedObjectsChange={setSelectedExplorerObjects}
crudEndpointsByEntity={crudEndpointsByEntity}
onManageCrudEndpoints={
canManageCrudEndpoints ? handleManageCrudEndpoints : undefined
}
onGenerateCrudEndpoints={
canManageCrudEndpoints ? handleGenerateCrudEndpoints : undefined
}
isGeneratingCrudEndpoints={isGeneratingCrud}
onCopySelectedObjects={handleOpenCopyDialog}
onCrudEndpointsChanged={canManageCrudEndpoints ? loadCrudEndpoints : undefined}
/>
</div>
</div>
{/* Center Panel - Editor and Results */}
<div
className={`flex flex-1 flex-col min-h-0 ${state.executionResult || state.tableColumns ? 'min-h-[620px] lg:min-h-0' : ''}`}
className={`flex flex-1 flex-col min-h-0 ${state.executionResult ? 'min-h-[620px] lg:min-h-0' : ''}`}
>
{state.executionResult || state.tableColumns ? (
{state.executionResult ? (
<Splitter direction="vertical" initialSize={250} minSize={150} maxSize={1200}>
<div className="border rounded-lg shadow-sm bg-white dark:bg-gray-800 flex flex-col h-full">
<div className="border-b px-4 py-2 bg-gray-50 dark:bg-gray-800 flex-shrink-0">
@ -1061,7 +1168,6 @@ GO`,
<FaCheckCircle className="text-green-500" />
<span className="text-sm text-green-700 dark:text-green-400">
{state.executionResult?.message ||
state.tableColumns?.message ||
translate('::App.Platform.QueryExecutedSuccessfully')}
</span>
</div>
@ -1072,7 +1178,6 @@ GO`,
<strong>
{state.executionResult?.rowsAffected ||
state.executionResult?.data?.length ||
state.tableColumns?.rowsAffected ||
0}
</strong>
</span>
@ -1088,7 +1193,7 @@ GO`,
<div className="flex-1 overflow-hidden p-2">
<Suspense fallback={<div className="p-4">{translate('::App.Platform.LoadingWithThreeDot')}</div>}>
<SqlResultsGrid
result={(state.executionResult || state.tableColumns)!}
result={state.executionResult}
queryText={state.lastExecutedQuery}
dataSourceCode={state.selectedDataSource}
isPostgreSql={isPostgreSql}
@ -1155,13 +1260,30 @@ GO`,
}}
dataSource={state.selectedDataSource ?? ''}
initialTableData={designTableData}
onDeployed={() => {
onDeployed={async () => {
setState((prev) => ({ ...prev, refreshTrigger: prev.refreshTrigger + 1 }))
// Tasarimci deploy sonrasi CRUD endpoint uretmis olabilir; explorer
// rozetlerinin gorunmesi icin liste tazelenir.
if (canManageCrudEndpoints) {
await loadCrudEndpoints()
}
}}
/>
</Suspense>
)}
{/* CRUD Endpoint Dialog */}
{crudDialogTable && canManageCrudEndpoints && (
<CrudEndpointDialog
isOpen
schemaName={crudDialogTable.schemaName}
tableName={crudDialogTable.tableName}
dataSourceCode={state.selectedDataSource}
onClose={() => setCrudDialogTable(null)}
onChanged={applyEntityEndpoints}
/>
)}
<Dialog
isOpen={showCopyDialog}
onClose={() => !isCopyingObjects && setShowCopyDialog(false)}
@ -1235,42 +1357,7 @@ GO`,
)}
</div>
<div className="mb-4">
<div className="text-sm font-medium mb-2">
{translate('::App.Platform.TargetDataSources')}
</div>
<label className="flex items-center gap-2 text-sm cursor-pointer mb-2">
<input
type="checkbox"
checked={allTargetsSelected}
onChange={(e) => handleToggleSelectAllTargets(e.target.checked)}
/>
<span>{translate('::App.Platform.SelectAllTargets')}</span>
</label>
<div className="max-h-44 overflow-auto border-t pt-2">
{availableTargetDataSourceCodes.map((code) => {
const checked = copyTargetDataSources.includes(code)
return (
<label
key={code}
className="flex items-center gap-2 text-sm cursor-pointer"
>
<input
type="checkbox"
checked={checked}
onChange={(e) => {
setCopyTargetDataSources((prev) => {
if (e.target.checked) return [...prev, code]
return prev.filter((x) => x !== code)
})
}}
/>
<span>{code}</span>
</label>
)
})}
</div>
</div>
{renderTargetDataSourcePicker()}
</>
) : (
<>
@ -1290,42 +1377,7 @@ GO`,
/>
</div>
<div className="mb-4">
<div className="text-sm font-medium mb-2">
{translate('::App.Platform.TargetDataSources')}
</div>
<label className="flex items-center gap-2 text-sm cursor-pointer mb-2">
<input
type="checkbox"
checked={allTargetsSelected}
onChange={(e) => handleToggleSelectAllTargets(e.target.checked)}
/>
<span>{translate('::App.Platform.SelectAllTargets')}</span>
</label>
<div className="max-h-44 overflow-auto border-t pt-2">
{availableTargetDataSourceCodes.map((code) => {
const checked = copyTargetDataSources.includes(code)
return (
<label
key={code}
className="flex items-center gap-2 text-sm cursor-pointer"
>
<input
type="checkbox"
checked={checked}
onChange={(e) => {
setCopyTargetDataSources((prev) => {
if (e.target.checked) return [...prev, code]
return prev.filter((x) => x !== code)
})
}}
/>
<span>{code}</span>
</label>
)
})}
</div>
</div>
{renderTargetDataSourcePicker()}
</>
)}
</div>
@ -1415,7 +1467,9 @@ GO`,
)}
</div>
<div className="text-sm font-medium break-words mb-1">{row.objectFullName}</div>
<div className="text-xs text-gray-500 mb-2">Hedef: {row.targetDataSource}</div>
<div className="text-xs text-gray-500 mb-2">
{translate('::App.Listform.ListformField.Target')}: {row.targetDataSource}
</div>
<div
className={`text-sm whitespace-normal break-words ${isError ? 'text-red-700 dark:text-red-300 font-medium' : 'text-gray-700 dark:text-gray-200'}`}
>

View file

@ -30,6 +30,16 @@ import {
} from '@/views/admin/listForm/wizard/WizardStep1'
import { MenuAddDialog } from '../shared/MenuAddDialog'
import { useStoreActions, useStoreState } from '@/store'
import { usePermission } from '@/utils/hooks/usePermission'
import { developerKitService } from '@/services/developerKit.service'
import {
CRUD_ENDPOINT_PERMISSION,
CRUD_OPERATION_TYPES,
type CrudOperationType,
getErrorMessage,
getOperationMeta,
toEntityName,
} from './CrudEndpointDialog'
// ─── Types ────────────────────────────────────────────────────────────────────
@ -1094,7 +1104,11 @@ function generateAlterTableSql(
return lines.join('\n')
}
type Step = 0 | 1 | 2 | 3 | 4
/**
* Adim sirasi. CRUD adimi yalnizca `App.SqlQueryManager.CrudEndpoints` izni olan
* kullanicilarda gosterildigi icin index'ler sabit degil; anahtarla eslestirilir.
*/
type StepKey = 'columns' | 'settings' | 'indexes' | 'relationships' | 'crud' | 'sql'
function StepContentWrapper({ children }: { children: React.ReactNode }) {
return <div className="min-h-[420px] flex flex-col">{children}</div>
@ -1273,8 +1287,31 @@ const SqlTableDesignerDialog = ({
const isEditMode = !!initialTableData
const [step, setStep] = useState<Step>(0)
const { checkPermission } = usePermission()
const canManageCrudEndpoints = checkPermission(CRUD_ENDPOINT_PERMISSION)
/** Izin yoksa CRUD adimi hic gosterilmez. */
const stepKeys = useMemo<StepKey[]>(
() => [
'columns',
'settings',
'indexes',
'relationships',
...(canManageCrudEndpoints ? (['crud'] as StepKey[]) : []),
'sql',
],
[canManageCrudEndpoints],
)
const lastStep = stepKeys.length - 1
const [step, setStep] = useState(0)
const currentStepKey = stepKeys[step]
const [isDeploying, setIsDeploying] = useState(false)
/** Deploy sonrasi CRUD endpoint uretilsin mi; varsayilan kapali, kullanici acar. */
const [generateCrud, setGenerateCrud] = useState(false)
const [crudOperations, setCrudOperations] = useState<CrudOperationType[]>([
...CRUD_OPERATION_TYPES,
])
const [columns, setColumns] = useState<ColumnDefinition[]>([createEmptyColumn()])
const [originalColumns, setOriginalColumns] = useState<ColumnDefinition[]>([])
const [colsLoading, setColsLoading] = useState(false)
@ -1899,24 +1936,36 @@ const SqlTableDesignerDialog = ({
}, [columns])
const canGoNext = (): boolean => {
if (step === 0) {
if (currentStepKey === 'columns') {
const hasNamed = columns.some((c) => c.columnName.trim().length > 0)
return hasNamed && duplicateColumnNames.size === 0
}
if (step === 1) {
if (currentStepKey === 'settings') {
if (isEditMode) return true // table name is locked in edit mode
const baseOk =
!!settings.tableName.trim() && !!settings.entityName.trim() && !!settings.menuValue
return baseOk
}
if (currentStepKey === 'crud') {
// Uretim isteniyorsa en az bir operasyon secilmis olmali.
return !generateCrud || crudOperations.length > 0
}
return true
}
const handleNext = () => {
if (step < 4) setStep((s) => (s + 1) as Step)
if (step < lastStep) setStep((s) => s + 1)
}
const handleBack = () => {
if (step > 0) setStep((s) => (s - 1) as Step)
if (step > 0) setStep((s) => s - 1)
}
const toggleCrudOperation = (operation: CrudOperationType) => {
setCrudOperations((prev) =>
prev.includes(operation)
? prev.filter((op) => op !== operation)
: CRUD_OPERATION_TYPES.filter((op) => op === operation || prev.includes(op)),
)
}
// ── Deploy ─────────────────────────────────────────────────────────────────
@ -1964,6 +2013,30 @@ const SqlTableDesignerDialog = ({
} catch {
// Non-blocking: seed file save failure does not affect deploy success
}
// Kullanici CRUD adiminda istediyse endpoint'leri uretir; secilmeyen
// operasyonlar pasif olarak kaydedilir. Hata deploy'u geri almaz.
if (canManageCrudEndpoints && generateCrud && crudOperations.length > 0 && deployedTable) {
try {
await developerKitService.generateCrudEndpoints(
toEntityName(deployedTable),
crudOperations,
)
toast.push(
<Notification type="success" title={translate('::App.Platform.Success')}>
{`${translate('::App.SqlQueryManager.CrudEndpoints')}: ${crudOperations.join(', ')}`}
</Notification>,
{ placement: 'bottom-end' },
)
} catch (crudError) {
toast.push(
<Notification type="warning" title={translate('::App.Platform.Warning')}>
{getErrorMessage(crudError, translate('::App.Platform.IslemBasarisiz'))}
</Notification>,
{ placement: 'bottom-end' },
)
}
}
await onDeployed?.({
schemaName: initialTableData?.schemaName || 'dbo',
tableName: deployedTable,
@ -2004,18 +2077,22 @@ const SqlTableDesignerDialog = ({
setTargetTableKeyColumns([])
setSelectedMenuCode('')
setMenuAddDialogOpen(false)
setGenerateCrud(false)
setCrudOperations([...CRUD_OPERATION_TYPES])
onClose()
}
// ── Step Indicator ─────────────────────────────────────────────────────────
const STEP_LABELS = [
translate('::App.SqlQueryManager.ColumnDesign'),
translate('::App.SqlQueryManager.EntitySettings'),
translate('::App.SqlQueryManager.IndexKeys'),
translate('::App.SqlQueryManager.Relationships'),
translate('::App.SqlQueryManager.TSqlPreview'),
]
const STEP_LABEL: Record<StepKey, string> = {
columns: translate('::App.SqlQueryManager.ColumnDesign'),
settings: translate('::App.SqlQueryManager.EntitySettings'),
indexes: translate('::App.SqlQueryManager.IndexKeys'),
relationships: translate('::App.SqlQueryManager.Relationships'),
crud: translate('::App.SqlQueryManager.CrudEndpoints'),
sql: translate('::App.SqlQueryManager.TSqlPreview'),
}
const STEP_LABELS = stepKeys.map((key) => STEP_LABEL[key])
const renderStepIndicator = () => (
<div className="flex items-center justify-between mb-2">
@ -3095,7 +3172,81 @@ const SqlTableDesignerDialog = ({
</div>
)
// ── Step 4: T-SQL Preview ──────────────────────────────────────────────────
// ── Step: CRUD Endpoints ───────────────────────────────────────────────────
const crudEntityName = toEntityName(
settings.tableName || initialTableData?.tableName || settings.entityName || '',
)
const allCrudSelected = crudOperations.length === CRUD_OPERATION_TYPES.length
const renderCrudEndpoints = () => (
<div className="flex flex-col gap-4">
<label className="flex items-start gap-3 cursor-pointer rounded-lg border p-3 dark:border-gray-700">
<Checkbox checked={generateCrud} onChange={(checked) => setGenerateCrud(checked)} />
<span>
<span className="block font-medium">
{translate('::App.SqlQueryManager.GenerateCrudEndpointsAfterDeploy')}
</span>
<span className="block text-xs text-gray-500 dark:text-gray-400">
{translate('::App.SqlQueryManager.GenerateCrudEndpointsHint')}
{crudEntityName ? `${crudEntityName}` : ''}
</span>
</span>
</label>
<div className={generateCrud ? '' : 'opacity-40 pointer-events-none'}>
<div className="mb-2 flex items-center justify-between">
<span className="text-sm font-medium">
{translate('::App.SqlQueryManager.SelectCrudOperations')}
</span>
<Button
size="xs"
variant="plain"
className="!h-auto !px-1 !py-0 text-xs text-blue-500 hover:underline"
onClick={() =>
setCrudOperations(allCrudSelected ? [] : [...CRUD_OPERATION_TYPES])
}
>
{allCrudSelected
? translate('::App.SqlQueryManager.ClearSelection')
: translate('::App.Platform.SelectAll')}
</Button>
</div>
<div className="flex flex-col gap-2">
{CRUD_OPERATION_TYPES.map((operation) => {
const meta = getOperationMeta(operation)
const checked = crudOperations.includes(operation)
return (
<label
key={operation}
className="flex items-center gap-3 cursor-pointer rounded-lg border px-3 py-2 dark:border-gray-700"
>
<Checkbox checked={checked} onChange={() => toggleCrudOperation(operation)} />
<span className={meta.color}>{meta.icon}</span>
<span className="text-sm font-medium">{operation}</span>
<span className="ml-auto text-xs text-gray-400">
{translate(`::App.SqlQueryManager.CrudOperation${operation}`)}
</span>
</label>
)
})}
</div>
{generateCrud && crudOperations.length === 0 && (
<p className="mt-2 mb-0 text-xs text-amber-600 dark:text-amber-400">
{translate('::App.SqlQueryManager.SelectAtLeastOneCrudOperation')}
</p>
)}
</div>
<p className="mb-0 text-xs text-gray-500 dark:text-gray-400">
{translate('::App.SqlQueryManager.CrudEndpointsInactiveNote')}
</p>
</div>
)
// ── Step: T-SQL Preview ────────────────────────────────────────────────────
const renderSqlPreview = () => (
<div className="flex flex-col gap-3">
@ -3147,11 +3298,12 @@ const SqlTableDesignerDialog = ({
<Dialog.Body className="flex flex-col gap-2">
{/* Content */}
<StepContentWrapper>
{step === 0 && renderColumnDesigner()}
{step === 1 && renderEntitySettings()}
{step === 2 && renderIndexes()}
{step === 3 && renderRelationships()}
{step === 4 && renderSqlPreview()}
{currentStepKey === 'columns' && renderColumnDesigner()}
{currentStepKey === 'settings' && renderEntitySettings()}
{currentStepKey === 'indexes' && renderIndexes()}
{currentStepKey === 'relationships' && renderRelationships()}
{currentStepKey === 'crud' && renderCrudEndpoints()}
{currentStepKey === 'sql' && renderSqlPreview()}
</StepContentWrapper>
</Dialog.Body>
@ -3165,7 +3317,7 @@ const SqlTableDesignerDialog = ({
{translate('::App.Platform.Back')}
</Button>
)}
{step < 4 ? (
{step < lastStep ? (
<Button variant="solid" color="blue-600" onClick={handleNext} disabled={!canGoNext()}>
{translate('::App.Platform.Next')}
</Button>