CrudEndpoint kaldırıldı o kısım SqlQueryManager taşındı
This commit is contained in:
parent
f5ba73f875
commit
a0121f8e11
23 changed files with 2080 additions and 1480 deletions
21
README.md
21
README.md
|
|
@ -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ı açı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`.
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
147
api/src/Sozsoft.Platform.DbMigrator/Migrations/CrudDataSeeder.cs
Normal file
147
api/src/Sozsoft.Platform.DbMigrator/Migrations/CrudDataSeeder.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using System.Collections.Generic;
|
||||
using Sozsoft.Platform.Entities;
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
33
api/src/Sozsoft.Platform.Domain/Data/CrudEndpointSeedFile.cs
Normal file
33
api/src/Sozsoft.Platform.Domain/Data/CrudEndpointSeedFile.cs
Normal 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;
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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}`,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
855
ui/src/views/developerKit/CrudEndpointDialog.tsx
Normal file
855
ui/src/views/developerKit/CrudEndpointDialog.tsx
Normal 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
|
|
@ -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>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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'}`}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Reference in a new issue