Developer Kit düzenlemeleri

This commit is contained in:
Sedat Öztürk 2026-03-01 23:43:25 +03:00
parent a746fea95e
commit cc4efd4396
60 changed files with 1627 additions and 5232 deletions

View file

@ -2,7 +2,7 @@
public static class Prefix public static class Prefix
{ {
public static string MenuPrefix { get; set; } = "Sqm"; public static string MenuPrefix { get; set; } = "Sas";
public static string HostPrefix { get; set; } = "T"; public static string HostPrefix { get; set; } = "T";
public static string? DbSchema { get; set; } = null; public static string? DbSchema { get; set; } = null;

View file

@ -1,28 +0,0 @@
// Migration DTOs
using System;
using Volo.Abp.Application.Dtos;
namespace Sozsoft.Platform.DeveloperKit;
public class CrudMigrationDto : AuditedEntityDto<Guid>
{
public Guid EntityId { get; set; }
public string EntityName { get; set; } = string.Empty;
public string FileName { get; set; } = string.Empty;
public string SqlScript { get; set; } = string.Empty;
public string Status { get; set; } = "Askıda"; // "pending" | "applied" | "failed"
public DateTime? AppliedAt { get; set; }
public string? ErrorMessage { get; set; }
}
public class CreateUpdateCrudMigrationDto
{
public Guid EntityId { get; set; }
public string EntityName { get; set; } = string.Empty;
public string FileName { get; set; } = string.Empty;
public string SqlScript { get; set; } = string.Empty;
public string Status { get; set; } = "Askıda";
public DateTime? AppliedAt { get; set; }
public string? ErrorMessage { get; set; }
}

View file

@ -1,19 +0,0 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
namespace Sozsoft.Platform.DeveloperKit;
public interface ICrudMigrationAppService : ICrudAppService<
CrudMigrationDto,
Guid,
PagedAndSortedResultRequestDto,
CreateUpdateCrudMigrationDto>
{
Task<CrudMigrationDto> ApplyMigrationAsync(Guid id);
Task<List<CrudMigrationDto>> GetPendingMigrationsAsync();
Task<CrudMigrationDto> GenerateMigrationAsync(Guid entityId);
}

View file

@ -6,13 +6,13 @@ using Volo.Abp.Application.Services;
namespace Sozsoft.Platform.DeveloperKit; namespace Sozsoft.Platform.DeveloperKit;
public interface ICustomEntityAppService : ICrudAppService< public interface ISqlTableAppService : ICrudAppService<
CustomEntityDto, SqlTableDto,
Guid, Guid,
PagedAndSortedResultRequestDto, PagedAndSortedResultRequestDto,
CreateUpdateCustomEntityDto> CreateUpdateSqlTableDto>
{ {
Task<List<CustomEntityDto>> GetActiveEntitiesAsync(); Task<List<SqlTableDto>> GetActiveEntitiesAsync();
Task<CustomEntityDto> ToggleActiveStatusAsync(Guid id); Task<SqlTableDto> ToggleActiveStatusAsync(Guid id);
} }

View file

@ -4,7 +4,7 @@ using Volo.Abp.Application.Dtos;
namespace Sozsoft.Platform.DeveloperKit; namespace Sozsoft.Platform.DeveloperKit;
public class CustomEntityDto : FullAuditedEntityDto<Guid> public class SqlTableDto : FullAuditedEntityDto<Guid>
{ {
public string Menu { get; set; } = string.Empty; public string Menu { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty; public string Name { get; set; } = string.Empty;
@ -14,14 +14,12 @@ public class CustomEntityDto : FullAuditedEntityDto<Guid>
public bool IsActive { get; set; } = true; public bool IsActive { get; set; } = true;
public bool IsFullAuditedEntity { get; set; } = true; public bool IsFullAuditedEntity { get; set; } = true;
public bool IsMultiTenant { get; set; } = false; public bool IsMultiTenant { get; set; } = false;
public string MigrationStatus { get; set; } = "Askıda"; public string EndpointStatus { get; set; } = "pending";
public Guid? MigrationId { get; set; }
public string EndpointStatus { get; set; } = "Askıda";
public List<EntityFieldDto> Fields { get; set; } = []; public List<SqlTableFieldDto> Fields { get; set; } = [];
} }
public class CreateUpdateCustomEntityDto public class CreateUpdateSqlTableDto
{ {
public string Menu { get; set; } = string.Empty; public string Menu { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty; public string Name { get; set; } = string.Empty;
@ -32,10 +30,10 @@ public class CreateUpdateCustomEntityDto
public bool IsFullAuditedEntity { get; set; } = true; public bool IsFullAuditedEntity { get; set; } = true;
public bool IsMultiTenant { get; set; } = false; public bool IsMultiTenant { get; set; } = false;
public List<CreateUpdateCustomEntityFieldDto> Fields { get; set; } = []; public List<CreateUpdateSqlTableFieldDto> Fields { get; set; } = [];
} }
public class EntityFieldDto : FullAuditedEntityDto<Guid> public class SqlTableFieldDto : FullAuditedEntityDto<Guid>
{ {
public Guid EntityId { get; set; } public Guid EntityId { get; set; }
public string Name { get; set; } = string.Empty; public string Name { get; set; } = string.Empty;
@ -48,7 +46,7 @@ public class EntityFieldDto : FullAuditedEntityDto<Guid>
public int DisplayOrder { get; set; } = 0; public int DisplayOrder { get; set; } = 0;
} }
public class CreateUpdateCustomEntityFieldDto public class CreateUpdateSqlTableFieldDto
{ {
public Guid? Id { get; set; } public Guid? Id { get; set; }

View file

@ -18,19 +18,16 @@ public class CrudEndpointGenerateAppService : CrudAppService<
PagedAndSortedResultRequestDto, PagedAndSortedResultRequestDto,
CreateUpdateCrudEndpointDto>, ICrudEndpointAppService CreateUpdateCrudEndpointDto>, ICrudEndpointAppService
{ {
private readonly IRepository<CustomEntity, Guid> _entityRepository; private readonly IRepository<SqlTable, Guid> _entityRepository;
private readonly IRepository<CrudMigration, Guid> _migrationRepository;
private readonly IRepository<CrudEndpoint, Guid> _endpointRepository; private readonly IRepository<CrudEndpoint, Guid> _endpointRepository;
public CrudEndpointGenerateAppService( public CrudEndpointGenerateAppService(
IRepository<CrudEndpoint, Guid> repository, IRepository<CrudEndpoint, Guid> repository,
IRepository<CustomEntity, Guid> entityRepository, IRepository<SqlTable, Guid> entityRepository,
IRepository<CrudMigration, Guid> migrationRepository,
IRepository<CrudEndpoint, Guid> endpointRepository) IRepository<CrudEndpoint, Guid> endpointRepository)
: base(repository) : base(repository)
{ {
_entityRepository = entityRepository; _entityRepository = entityRepository;
_migrationRepository = migrationRepository;
_endpointRepository = endpointRepository; _endpointRepository = endpointRepository;
} }
@ -59,17 +56,6 @@ public class CrudEndpointGenerateAppService : CrudAppService<
throw new Exception($"Entity with ID {entityId} not found"); throw new Exception($"Entity with ID {entityId} not found");
} }
// Migration kontrolü
var migrationQueryable = await _migrationRepository.GetQueryableAsync();
var migration = await migrationQueryable
.Where(x => x.EntityId == entityId && x.Status == "applied")
.FirstOrDefaultAsync();
if (migration == null)
{
throw new Exception($"No applied migration found for entity {entity.Name}. Please apply migration first.");
}
// CRUD endpointleri oluştur // CRUD endpointleri oluştur
var endpoints = new List<CrudEndpoint>(); var endpoints = new List<CrudEndpoint>();
var entityName = entity.Name; var entityName = entity.Name;

View file

@ -1,302 +0,0 @@
using Microsoft.EntityFrameworkCore;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.Application.Dtos;
using System.Text;
using Volo.Abp.Domain.Entities;
using Sozsoft.Platform.Entities;
using Sozsoft.Platform.DeveloperKit;
using System;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Linq;
using Sozsoft.Platform.Domain.DeveloperKit;
namespace Platform.Api.Application;
public class CrudMigrationAppService : CrudAppService<
CrudMigration,
CrudMigrationDto,
Guid,
PagedAndSortedResultRequestDto,
CreateUpdateCrudMigrationDto>, ICrudMigrationAppService
{
private readonly IRepository<CrudMigration, Guid> _migrationRepository;
private readonly IRepository<CustomEntity, Guid> _entityRepository;
private readonly IRepository<CustomEntity, Guid> _fieldRepository;
private readonly IApiMigrationRepository _customSqlExecutor;
public CrudMigrationAppService(
IRepository<CrudMigration, Guid> migrationRepository,
IRepository<CustomEntity, Guid> entityRepository,
IRepository<CustomEntity, Guid> fieldRepository,
IApiMigrationRepository customSqlExecutor
) : base(migrationRepository)
{
_migrationRepository = migrationRepository;
_entityRepository = entityRepository;
_fieldRepository = fieldRepository;
_customSqlExecutor = customSqlExecutor;
}
public virtual async Task<CrudMigrationDto> ApplyMigrationAsync(Guid id)
{
var migration = await _migrationRepository.GetAsync(id);
try
{
await _customSqlExecutor.ExecuteSqlAsync(migration.SqlScript);
migration.Status = "Uygulandı";
migration.AppliedAt = DateTime.UtcNow;
migration.ErrorMessage = null;
await _migrationRepository.UpdateAsync(migration, autoSave: true);
var entity = await _entityRepository.GetAsync(migration.EntityId);
entity.MigrationStatus = "Uygulandı";
await _entityRepository.UpdateAsync(entity, autoSave: true);
return ObjectMapper.Map<CrudMigration, CrudMigrationDto>(migration);
}
catch (Exception ex)
{
migration.Status = "failed";
migration.ErrorMessage = ex.Message;
await _migrationRepository.UpdateAsync(migration, autoSave: true);
var entity = await _entityRepository.GetAsync(migration.EntityId);
entity.MigrationStatus = "failed";
await _entityRepository.UpdateAsync(entity, autoSave: true);
throw;
}
}
public virtual async Task<List<CrudMigrationDto>> GetPendingMigrationsAsync()
{
var queryable = await _migrationRepository.GetQueryableAsync();
var pending = await queryable
.Where(m => m.Status == "pending")
.OrderBy(m => m.CreationTime)
.ToListAsync();
return ObjectMapper.Map<List<CrudMigration>, List<CrudMigrationDto>>(pending);
}
public virtual async Task<CrudMigrationDto> GenerateMigrationAsync(Guid entityId)
{
var queryable = await _entityRepository.GetQueryableAsync();
var entity = await queryable
.Include(x => x.Fields)
.FirstOrDefaultAsync(x => x.Id == entityId);
if (entity == null)
throw new EntityNotFoundException($"CustomEntity with id {entityId} not found");
// Check if there are any previously applied migrations for this entity
var migrationQueryable = await _migrationRepository.GetQueryableAsync();
var lastAppliedMigration = await migrationQueryable
.Where(m => m.EntityId == entityId && m.Status == "applied")
.OrderByDescending(m => m.CreationTime)
.FirstOrDefaultAsync();
// Use "Update" if migrations were applied before, otherwise "Create"
var operationType = lastAppliedMigration != null ? "Update" : "Create";
var fileName = $"{DateTime.UtcNow:yyyyMMddHHmmss}_{operationType}{entity.Name}Table.sql";
var sqlScript = lastAppliedMigration != null
? GenerateAlterTableScript(entity, lastAppliedMigration)
: GenerateCreateTableScript(entity);
var migration = new CrudMigration
{
EntityId = entityId,
EntityName = entity.Name,
FileName = fileName,
SqlScript = sqlScript,
Status = "Askıda"
};
migration = await _migrationRepository.InsertAsync(migration, autoSave: true);
entity.MigrationId = migration.Id;
entity.MigrationStatus = "Askıda";
await _entityRepository.UpdateAsync(entity, autoSave: true);
return ObjectMapper.Map<CrudMigration, CrudMigrationDto>(migration);
}
private string GenerateCreateTableScript(CustomEntity entity)
{
if (entity.Fields == null || !entity.Fields.Any())
throw new InvalidOperationException($"Entity '{entity.Name}' does not have any fields defined.");
var sb = new StringBuilder();
sb.AppendLine($@"/*
# Create {entity.DisplayName} table
1. New Table
- [{entity.TableName}]");
foreach (var field in entity.Fields)
{
sb.AppendLine($" - [{field.Name}] ({field.Type}{(field.IsRequired ? ", required" : "")})");
}
sb.AppendLine("*/");
sb.AppendLine($"\nIF OBJECT_ID(N'{entity.TableName}', N'U') IS NULL");
sb.AppendLine($"CREATE TABLE [{entity.TableName}] (");
sb.AppendLine(" [Id] UNIQUEIDENTIFIER PRIMARY KEY DEFAULT NEWID(),");
foreach (var field in entity.Fields)
{
var sqlType = GetSqlType(field.Type, field.MaxLength);
sb.Append($" [{field.Name}] {sqlType}");
if (field.IsRequired)
sb.Append(" NOT NULL");
if (!string.IsNullOrWhiteSpace(field.DefaultValue))
sb.Append($" DEFAULT {FormatDefaultValue(field.Type, field.DefaultValue)}");
sb.AppendLine(",");
}
// Add TenantId if entity is multi-tenant
if (entity.IsMultiTenant)
{
sb.AppendLine(" [TenantId] UNIQUEIDENTIFIER NULL,");
}
// Add Full Audited Entity fields (includes both audit and soft delete)
if (entity.IsFullAuditedEntity)
{
sb.AppendLine(" [CreationTime] DATETIME2 DEFAULT SYSUTCDATETIME() NOT NULL,");
sb.AppendLine(" [CreatorId] UNIQUEIDENTIFIER NULL,");
sb.AppendLine(" [LastModificationTime] DATETIME2 NULL,");
sb.AppendLine(" [LastModifierId] UNIQUEIDENTIFIER NULL,");
sb.AppendLine(" [IsDeleted] BIT DEFAULT 0 NOT NULL,");
sb.AppendLine(" [DeleterId] UNIQUEIDENTIFIER NULL,");
sb.AppendLine(" [DeletionTime] DATETIME2 NULL,");
}
// Remove last comma
var script = sb.ToString().TrimEnd(',', '\r', '\n');
script += "\n);\n";
return script;
}
private string GenerateAlterTableScript(CustomEntity entity, CrudMigration lastAppliedMigration)
{
if (entity.Fields == null || !entity.Fields.Any())
throw new InvalidOperationException($"Entity '{entity.Name}' does not have any fields defined.");
// Parse the last applied migration's SQL to understand what fields existed
var existingFieldNames = ExtractFieldNamesFromCreateScript(lastAppliedMigration.SqlScript);
var sb = new StringBuilder();
sb.AppendLine($@"/*
# Update {entity.DisplayName} table
1. Alter Table
- [{entity.TableName}]
- Adding new fields or modifying existing ones");
sb.AppendLine("*/\n");
// Find new fields that need to be added
var newFields = entity.Fields.Where(f => !existingFieldNames.Contains(f.Name)).ToList();
if (newFields.Any())
{
foreach (var field in newFields)
{
var sqlType = GetSqlType(field.Type, field.MaxLength);
sb.Append($"IF NOT EXISTS (SELECT * FROM sys.columns WHERE object_id = OBJECT_ID(N'[{entity.TableName}]') AND name = '{field.Name}')\n");
sb.Append($"BEGIN\n");
sb.Append($" ALTER TABLE [{entity.TableName}] ADD [{field.Name}] {sqlType}");
if (field.IsRequired)
sb.Append(" NOT NULL");
else
sb.Append(" NULL");
if (!string.IsNullOrWhiteSpace(field.DefaultValue))
sb.Append($" DEFAULT {FormatDefaultValue(field.Type, field.DefaultValue)}");
sb.AppendLine(";");
sb.AppendLine($"END\n");
}
}
else
{
sb.AppendLine("-- No new fields to add");
}
return sb.ToString();
}
private HashSet<string> ExtractFieldNamesFromCreateScript(string sqlScript)
{
var fieldNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
// Simple parsing - extract field names between [ and ]
var lines = sqlScript.Split('\n');
foreach (var line in lines)
{
var trimmedLine = line.Trim();
if (trimmedLine.StartsWith("[") && trimmedLine.Contains("]"))
{
var fieldName = trimmedLine.Substring(1, trimmedLine.IndexOf("]") - 1);
if (fieldName != "Id" &&
fieldName != "TenantId" &&
fieldName != "CreationTime" &&
fieldName != "CreatorId" &&
fieldName != "LastModificationTime" &&
fieldName != "LastModifierId" &&
fieldName != "IsDeleted" &&
fieldName != "DeleterId" &&
fieldName != "DeletionTime")
{
fieldNames.Add(fieldName);
}
}
}
return fieldNames;
}
private string GetSqlType(string fieldType, int? maxLength)
{
return fieldType.ToLower() switch
{
"string" => maxLength.HasValue ? $"NVARCHAR({maxLength.Value})" : "NVARCHAR(MAX)",
"number" => "INT",
"decimal" => "DECIMAL(18, 2)",
"boolean" => "BIT",
"date" => "DATETIME2",
"guid" => "UNIQUEIDENTIFIER",
_ => "NVARCHAR(MAX)"
};
}
private string FormatDefaultValue(string fieldType, string defaultValue)
{
return fieldType.ToLower() switch
{
"string" => $"N'{defaultValue}'",
"date" => $"'{defaultValue}'", // ISO format beklenir
"guid" => $"'{defaultValue}'",
"boolean" => (defaultValue.ToLower() == "true" || defaultValue == "1") ? "1" : "0",
_ => defaultValue
};
}
}

View file

@ -7,13 +7,13 @@ public class DeveloperKitAutoMapperProfile : Profile
{ {
public DeveloperKitAutoMapperProfile() public DeveloperKitAutoMapperProfile()
{ {
// CustomEntity mappings // SqlTable mappings
CreateMap<CustomEntity, CustomEntityDto>(); CreateMap<SqlTable, SqlTableDto>();
CreateMap<CreateUpdateCustomEntityDto, CustomEntity>(); CreateMap<CreateUpdateSqlTableDto, SqlTable>();
// EntityField mappings // EntityField mappings
CreateMap<CustomEntityField, EntityFieldDto>(); CreateMap<SqlTableField, SqlTableFieldDto>();
CreateMap<CreateUpdateCustomEntityFieldDto, CustomEntityField>(); CreateMap<CreateUpdateSqlTableFieldDto, SqlTableField>();
// CustomComponent mappings // CustomComponent mappings
CreateMap<CustomComponent, CustomComponentDto>(); CreateMap<CustomComponent, CustomComponentDto>();
@ -24,10 +24,6 @@ public class DeveloperKitAutoMapperProfile : Profile
.ForMember(dest => dest.EntityDisplayName, opt => opt.MapFrom(src => src.Entity != null ? src.Entity.DisplayName : null)); .ForMember(dest => dest.EntityDisplayName, opt => opt.MapFrom(src => src.Entity != null ? src.Entity.DisplayName : null));
CreateMap<CreateUpdateCrudEndpointDto, CrudEndpoint>(); CreateMap<CreateUpdateCrudEndpointDto, CrudEndpoint>();
// Migration mappings
CreateMap<CrudMigration, CrudMigrationDto>();
CreateMap<CreateUpdateCrudMigrationDto, CrudMigration>();
CreateMap<DynamicService, DynamicServiceDto>() CreateMap<DynamicService, DynamicServiceDto>()
.ForMember(dest => dest.CompilationStatus, opt => opt.MapFrom(src => src.CompilationStatus.ToString())); .ForMember(dest => dest.CompilationStatus, opt => opt.MapFrom(src => src.CompilationStatus.ToString()));
} }

View file

@ -9,35 +9,31 @@ using System;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using Microsoft.Extensions.Logging;
namespace Platform.Api.Application; namespace Platform.Api.Application;
public class CustomEntityAppService : CrudAppService< public class SqlTableAppService : CrudAppService<
CustomEntity, SqlTable,
CustomEntityDto, SqlTableDto,
Guid, Guid,
PagedAndSortedResultRequestDto, PagedAndSortedResultRequestDto,
CreateUpdateCustomEntityDto>, ICustomEntityAppService CreateUpdateSqlTableDto>, ISqlTableAppService
{ {
private readonly IRepository<CustomEntity, Guid> _repository; private readonly IRepository<SqlTable, Guid> _repository;
private readonly IRepository<CrudMigration, Guid> _migrationRepository; private readonly IRepository<SqlTableField, Guid> _fieldRepository;
private readonly IRepository<CrudEndpoint, Guid> _endpointRepository; private readonly IRepository<CrudEndpoint, Guid> _endpointRepository;
private readonly IRepository<CustomEntityField, Guid> _fieldRepository;
public CustomEntityAppService( public SqlTableAppService(
IRepository<CustomEntity, Guid> repository, IRepository<SqlTable, Guid> repository,
IRepository<CrudMigration, Guid> migrationRepository, IRepository<SqlTableField, Guid> fieldRepository,
IRepository<CrudEndpoint, Guid> endpointRepository, IRepository<CrudEndpoint, Guid> endpointRepository) : base(repository)
IRepository<CustomEntityField, Guid> fieldRepository) : base(repository)
{ {
_repository = repository; _repository = repository;
_migrationRepository = migrationRepository;
_endpointRepository = endpointRepository;
_fieldRepository = fieldRepository; _fieldRepository = fieldRepository;
_endpointRepository = endpointRepository;
} }
public override async Task<PagedResultDto<CustomEntityDto>> GetListAsync(PagedAndSortedResultRequestDto input) public override async Task<PagedResultDto<SqlTableDto>> GetListAsync(PagedAndSortedResultRequestDto input)
{ {
var query = await _repository.GetQueryableAsync(); var query = await _repository.GetQueryableAsync();
var fullQuery = query.Include(x => x.Fields.OrderBy(f => f.DisplayOrder)); var fullQuery = query.Include(x => x.Fields.OrderBy(f => f.DisplayOrder));
@ -50,12 +46,12 @@ public class CustomEntityAppService : CrudAppService<
.Take(input.MaxResultCount) .Take(input.MaxResultCount)
.ToListAsync(); .ToListAsync();
var dtos = ObjectMapper.Map<List<CustomEntity>, List<CustomEntityDto>>(entities); var dtos = ObjectMapper.Map<List<SqlTable>, List<SqlTableDto>>(entities);
return new PagedResultDto<CustomEntityDto>(totalCount, dtos); return new PagedResultDto<SqlTableDto>(totalCount, dtos);
} }
public override async Task<CustomEntityDto> GetAsync(Guid id) public override async Task<SqlTableDto> GetAsync(Guid id)
{ {
var query = await _repository.GetQueryableAsync(); var query = await _repository.GetQueryableAsync();
var entity = await query var entity = await query
@ -65,10 +61,10 @@ public class CustomEntityAppService : CrudAppService<
if (entity == null) if (entity == null)
throw new EntityNotFoundException($"CustomEntity with id {id} not found"); throw new EntityNotFoundException($"CustomEntity with id {id} not found");
return ObjectMapper.Map<CustomEntity, CustomEntityDto>(entity); return ObjectMapper.Map<SqlTable, SqlTableDto>(entity);
} }
public async Task<List<CustomEntityDto>> GetActiveEntitiesAsync() public async Task<List<SqlTableDto>> GetActiveEntitiesAsync()
{ {
var query = await _repository.GetQueryableAsync(); var query = await _repository.GetQueryableAsync();
var entities = await query var entities = await query
@ -76,10 +72,10 @@ public class CustomEntityAppService : CrudAppService<
.Where(x => x.IsActive) .Where(x => x.IsActive)
.ToListAsync(); .ToListAsync();
return ObjectMapper.Map<List<CustomEntity>, List<CustomEntityDto>>(entities); return ObjectMapper.Map<List<SqlTable>, List<SqlTableDto>>(entities);
} }
public async Task<CustomEntityDto> ToggleActiveStatusAsync(Guid id) public async Task<SqlTableDto> ToggleActiveStatusAsync(Guid id)
{ {
var query = await _repository.GetQueryableAsync(); var query = await _repository.GetQueryableAsync();
var entity = await query var entity = await query
@ -92,10 +88,10 @@ public class CustomEntityAppService : CrudAppService<
entity.IsActive = !entity.IsActive; entity.IsActive = !entity.IsActive;
await _repository.UpdateAsync(entity, autoSave: true); await _repository.UpdateAsync(entity, autoSave: true);
return ObjectMapper.Map<CustomEntity, CustomEntityDto>(entity); return ObjectMapper.Map<SqlTable, SqlTableDto>(entity);
} }
public override async Task<CustomEntityDto> UpdateAsync(Guid id, CreateUpdateCustomEntityDto input) public override async Task<SqlTableDto> UpdateAsync(Guid id, CreateUpdateSqlTableDto input)
{ {
var query = await _repository.GetQueryableAsync(); var query = await _repository.GetQueryableAsync();
var entity = await query var entity = await query
@ -103,20 +99,7 @@ public class CustomEntityAppService : CrudAppService<
.FirstOrDefaultAsync(e => e.Id == id); .FirstOrDefaultAsync(e => e.Id == id);
if (entity == null) if (entity == null)
throw new EntityNotFoundException(typeof(CustomEntity), id); throw new EntityNotFoundException(typeof(SqlTable), id);
// If entity structure has changed and migration was applied, reset migration status
bool structureChanged = entity.Name != input.Name ||
entity.TableName != input.TableName ||
entity.IsFullAuditedEntity != input.IsFullAuditedEntity ||
entity.IsMultiTenant != input.IsMultiTenant ||
FieldsHaveChanged(entity.Fields, input.Fields);
if (structureChanged && entity.MigrationStatus == "applied")
{
entity.MigrationStatus = "Askıda";
entity.MigrationId = null;
}
entity.Menu = input.Menu; entity.Menu = input.Menu;
entity.Name = input.Name; entity.Name = input.Name;
@ -127,12 +110,12 @@ public class CustomEntityAppService : CrudAppService<
entity.IsFullAuditedEntity = input.IsFullAuditedEntity; entity.IsFullAuditedEntity = input.IsFullAuditedEntity;
entity.IsMultiTenant = input.IsMultiTenant; entity.IsMultiTenant = input.IsMultiTenant;
var updatedFields = new List<CustomEntityField>(); var updatedFields = new List<SqlTableField>();
for (int i = 0; i < input.Fields.Count; i++) for (int i = 0; i < input.Fields.Count; i++)
{ {
var dtoField = input.Fields[i]; var dtoField = input.Fields[i];
CustomEntityField? existingField = null; SqlTableField? existingField = null;
if (dtoField.Id.HasValue) if (dtoField.Id.HasValue)
{ {
@ -154,7 +137,7 @@ public class CustomEntityAppService : CrudAppService<
} }
else else
{ {
var newField = new CustomEntityField var newField = new SqlTableField
{ {
EntityId = entity.Id, EntityId = entity.Id,
Name = dtoField.Name, Name = dtoField.Name,
@ -184,13 +167,13 @@ public class CustomEntityAppService : CrudAppService<
await _repository.UpdateAsync(entity, autoSave: true); await _repository.UpdateAsync(entity, autoSave: true);
return ObjectMapper.Map<CustomEntity, CustomEntityDto>(entity); return ObjectMapper.Map<SqlTable, SqlTableDto>(entity);
} }
public override async Task<CustomEntityDto> CreateAsync(CreateUpdateCustomEntityDto input) public override async Task<SqlTableDto> CreateAsync(CreateUpdateSqlTableDto input)
{ {
// Entity oluştur // Entity oluştur
var entity = new CustomEntity var entity = new SqlTable
{ {
Menu = input.Menu, Menu = input.Menu,
Name = input.Name, Name = input.Name,
@ -200,14 +183,13 @@ public class CustomEntityAppService : CrudAppService<
IsActive = input.IsActive, IsActive = input.IsActive,
IsFullAuditedEntity = input.IsFullAuditedEntity, IsFullAuditedEntity = input.IsFullAuditedEntity,
IsMultiTenant = input.IsMultiTenant, IsMultiTenant = input.IsMultiTenant,
MigrationStatus = "Askıda"
}; };
// Fields ekle - sıralama ile // Fields ekle - sıralama ile
for (int i = 0; i < input.Fields.Count; i++) for (int i = 0; i < input.Fields.Count; i++)
{ {
var fieldDto = input.Fields[i]; var fieldDto = input.Fields[i];
var field = new CustomEntityField var field = new SqlTableField
{ {
EntityId = entity.Id, EntityId = entity.Id,
Name = fieldDto.Name, Name = fieldDto.Name,
@ -224,10 +206,10 @@ public class CustomEntityAppService : CrudAppService<
entity = await _repository.InsertAsync(entity, autoSave: true); entity = await _repository.InsertAsync(entity, autoSave: true);
return ObjectMapper.Map<CustomEntity, CustomEntityDto>(entity); return ObjectMapper.Map<SqlTable, SqlTableDto>(entity);
} }
private bool FieldsHaveChanged(ICollection<CustomEntityField> existingFields, List<CreateUpdateCustomEntityFieldDto> inputFields) private bool FieldsHaveChanged(ICollection<SqlTableField> existingFields, List<CreateUpdateSqlTableFieldDto> inputFields)
{ {
if (existingFields.Count != inputFields.Count) if (existingFields.Count != inputFields.Count)
return true; return true;
@ -252,31 +234,5 @@ public class CustomEntityAppService : CrudAppService<
return false; return false;
} }
public override async Task DeleteAsync(Guid id)
{
// İlgili entity'nin var olup olmadığını kontrol et
var entity = await _repository.GetAsync(id);
// İlgili ApiMigration kayıtlarını sil
var relatedMigrations = await _migrationRepository.GetListAsync(m => m.EntityId == id);
if (relatedMigrations.Any())
{
Logger.LogInformation($"Deleting {relatedMigrations.Count} related migrations for entity {entity.Name} (ID: {id})");
await _migrationRepository.DeleteManyAsync(relatedMigrations);
}
var relatedEndpoints = await _endpointRepository.GetListAsync(e => e.EntityId == id);
if (relatedEndpoints.Any())
{
Logger.LogInformation($"Deleting {relatedEndpoints.Count} related API endpoints for entity {entity.Name} (ID: {id})");
await _endpointRepository.DeleteManyAsync(relatedEndpoints);
}
Logger.LogInformation($"Deleting CustomEntity {entity.Name} (ID: {id}) and its fields");
// Ana entity'yi sil (CustomEntityField'lar otomatik silinecek - cascade delete)
await base.DeleteAsync(id);
}
} }

View file

@ -624,12 +624,6 @@
"en": "Custom Entity", "en": "Custom Entity",
"tr": "Özel Varlık" "tr": "Özel Varlık"
}, },
{
"resourceName": "Platform",
"key": "App.DeveloperKit.Migrations",
"en": "Crud Migrations",
"tr": "Crud Geçişleri"
},
{ {
"resourceName": "Platform", "resourceName": "Platform",
"key": "App.DeveloperKit.CrudEndpoints", "key": "App.DeveloperKit.CrudEndpoints",

View file

@ -283,84 +283,49 @@
{ {
"key": "admin.sqlQueryManager", "key": "admin.sqlQueryManager",
"path": "/admin/sqlQueryManager", "path": "/admin/sqlQueryManager",
"componentPath": "@/views/sqlQueryManager/SqlQueryManager", "componentPath": "@/views/developerKit/SqlQueryManager",
"routeType": "protected", "routeType": "protected",
"authority": ["App.SqlQueryManager"] "authority": ["App.SqlQueryManager"]
}, },
{
"key": "admin.developerkit",
"path": "/admin/developerkit",
"componentPath": "@/views/developerKit/DashboardPage",
"routeType": "protected",
"authority": ["App.DeveloperKit"]
},
{
"key": "admin.developerkit.entities",
"path": "/admin/developerkit/entities",
"componentPath": "@/views/developerKit/EntityPage",
"routeType": "protected",
"authority": ["App.DeveloperKit.Entity"]
},
{
"key": "admin.developerkit.entities.new",
"path": "/admin/developerkit/entities/new",
"componentPath": "@/views/developerKit/EntityDetailPage",
"routeType": "protected",
"authority": ["App.DeveloperKit.Entity"]
},
{
"key": "admin.developerkit.entities.edit",
"path": "/admin/developerkit/entities/edit/:id",
"componentPath": "@/views/developerKit/EntityDetailPage",
"routeType": "protected",
"authority": ["App.DeveloperKit.Entity"]
},
{
"key": "admin.developerkit.migrations",
"path": "/admin/developerkit/migrations",
"componentPath": "@/views/developerKit/MigrationPage",
"routeType": "protected",
"authority": ["App.DeveloperKit.Migrations"]
},
{ {
"key": "admin.developerkit.endpoints", "key": "admin.developerkit.endpoints",
"path": "/admin/developerkit/endpoints", "path": "/admin/developerkit/endpoints",
"componentPath": "@/views/developerKit/CrudEndpointPage", "componentPath": "@/views/developerKit/CrudEndpointManager",
"routeType": "protected", "routeType": "protected",
"authority": ["App.DeveloperKit.CrudEndpoints"] "authority": ["App.DeveloperKit.CrudEndpoints"]
}, },
{ {
"key": "admin.developerkit.dynamic-services", "key": "admin.developerkit.dynamic-services",
"path": "/admin/developerkit/dynamic-services", "path": "/admin/developerkit/dynamic-services",
"componentPath": "@/views/developerKit/DynamicServicePage", "componentPath": "@/views/developerKit/DynamicServiceEditor",
"routeType": "protected", "routeType": "protected",
"authority": ["App.DeveloperKit.DynamicServices"] "authority": ["App.DeveloperKit.DynamicServices"]
}, },
{ {
"key": "admin.developerkit.components", "key": "admin.developerkit.components",
"path": "/admin/developerkit/components", "path": "/admin/developerkit/components",
"componentPath": "@/views/developerKit/ComponentPage", "componentPath": "@/views/developerKit/ComponentManagerPage",
"routeType": "protected", "routeType": "protected",
"authority": ["App.DeveloperKit.Components"] "authority": ["App.DeveloperKit.Components"]
}, },
{ {
"key": "admin.developerkit.components.new", "key": "admin.developerkit.components.new",
"path": "/admin/developerkit/components/new", "path": "/admin/developerkit/components/new",
"componentPath": "@/views/developerKit/ComponentDetailPage", "componentPath": "@/views/developerKit/ComponentEditorPage",
"routeType": "protected", "routeType": "protected",
"authority": ["App.DeveloperKit.Components"] "authority": ["App.DeveloperKit.Components"]
}, },
{ {
"key": "admin.developerkit.components.view", "key": "admin.developerkit.components.view",
"path": "/admin/developerkit/components/view/:id", "path": "/admin/developerkit/components/view/:id",
"componentPath": "@/views/developerKit/ComponentDetailPage", "componentPath": "@/views/developerKit/ComponentEditorPage",
"routeType": "protected", "routeType": "protected",
"authority": ["App.DeveloperKit.Components"] "authority": ["App.DeveloperKit.Components"]
}, },
{ {
"key": "admin.developerkit.components.edit", "key": "admin.developerkit.components.edit",
"path": "/admin/developerkit/components/edit/:id", "path": "/admin/developerkit/components/edit/:id",
"componentPath": "@/views/developerKit/CodePage", "componentPath": "@/views/developerKit/CodeLayout",
"routeType": "protected", "routeType": "protected",
"authority": ["App.DeveloperKit.Components"] "authority": ["App.DeveloperKit.Components"]
}, },
@ -971,39 +936,19 @@
}, },
{ {
"ParentCode": "App.DeveloperKit", "ParentCode": "App.DeveloperKit",
"Code": "App.DeveloperKit.Dashboard", "Code": "App.SqlQueryManager",
"DisplayName": "App.Coordinator.Classroom.Dashboard", "DisplayName": "App.SqlQueryManager",
"Order": 1, "Order": 1,
"Url": "/admin/developerkit", "Url": "/admin/sqlQueryManager",
"Icon": "FcBinoculars", "Icon": "FaDatabase",
"RequiredPermissionName": "App.DeveloperKit", "RequiredPermissionName": "App.SqlQueryManager",
"IsDisabled": false
},
{
"ParentCode": "App.DeveloperKit",
"Code": "App.DeveloperKit.Entity",
"DisplayName": "App.DeveloperKit.Entity",
"Order": 2,
"Url": "/admin/developerkit/entities",
"Icon": "FcAddRow",
"RequiredPermissionName": "App.DeveloperKit.Entity",
"IsDisabled": false
},
{
"ParentCode": "App.DeveloperKit",
"Code": "App.DeveloperKit.Migrations",
"DisplayName": "App.DeveloperKit.Migrations",
"Order": 3,
"Url": "/admin/developerkit/migrations",
"Icon": "FcAddDatabase",
"RequiredPermissionName": "App.DeveloperKit.Migrations",
"IsDisabled": false "IsDisabled": false
}, },
{ {
"ParentCode": "App.DeveloperKit", "ParentCode": "App.DeveloperKit",
"Code": "App.DeveloperKit.CrudEndpoints", "Code": "App.DeveloperKit.CrudEndpoints",
"DisplayName": "App.DeveloperKit.CrudEndpoints", "DisplayName": "App.DeveloperKit.CrudEndpoints",
"Order": 4, "Order": 2,
"Url": "/admin/developerkit/endpoints", "Url": "/admin/developerkit/endpoints",
"Icon": "FcOrgUnit", "Icon": "FcOrgUnit",
"RequiredPermissionName": "App.DeveloperKit.CrudEndpoints", "RequiredPermissionName": "App.DeveloperKit.CrudEndpoints",
@ -1013,7 +958,7 @@
"ParentCode": "App.DeveloperKit", "ParentCode": "App.DeveloperKit",
"Code": "App.DeveloperKit.CustomEndpoints", "Code": "App.DeveloperKit.CustomEndpoints",
"DisplayName": "App.DeveloperKit.CustomEndpoints", "DisplayName": "App.DeveloperKit.CustomEndpoints",
"Order": 5, "Order": 3,
"Url": "/admin/list/App.DeveloperKit.CustomEndpoints", "Url": "/admin/list/App.DeveloperKit.CustomEndpoints",
"Icon": "FcMindMap", "Icon": "FcMindMap",
"RequiredPermissionName": "App.DeveloperKit.CustomEndpoints", "RequiredPermissionName": "App.DeveloperKit.CustomEndpoints",
@ -1023,7 +968,7 @@
"ParentCode": "App.DeveloperKit", "ParentCode": "App.DeveloperKit",
"Code": "App.DeveloperKit.Components", "Code": "App.DeveloperKit.Components",
"DisplayName": "App.DeveloperKit.Components", "DisplayName": "App.DeveloperKit.Components",
"Order": 6, "Order": 4,
"Url": "/admin/developerkit/components", "Url": "/admin/developerkit/components",
"Icon": "FcBiohazard", "Icon": "FcBiohazard",
"RequiredPermissionName": "App.DeveloperKit.Components", "RequiredPermissionName": "App.DeveloperKit.Components",
@ -1033,27 +978,17 @@
"ParentCode": "App.DeveloperKit", "ParentCode": "App.DeveloperKit",
"Code": "App.DeveloperKit.DynamicServices", "Code": "App.DeveloperKit.DynamicServices",
"DisplayName": "App.DeveloperKit.DynamicServices", "DisplayName": "App.DeveloperKit.DynamicServices",
"Order": 7, "Order": 5,
"Url": "/admin/developerkit/dynamic-services", "Url": "/admin/developerkit/dynamic-services",
"Icon": "FcCommandLine", "Icon": "FcCommandLine",
"RequiredPermissionName": "App.DeveloperKit.DynamicServices", "RequiredPermissionName": "App.DeveloperKit.DynamicServices",
"IsDisabled": false "IsDisabled": false
}, },
{
"ParentCode": "App.DeveloperKit",
"Code": "App.SqlQueryManager",
"DisplayName": "App.SqlQueryManager",
"Order": 8,
"Url": "/admin/sqlQueryManager",
"Icon": "FaDatabase",
"RequiredPermissionName": "App.SqlQueryManager",
"IsDisabled": false
},
{ {
"ParentCode": "App.DeveloperKit", "ParentCode": "App.DeveloperKit",
"Code": "App.Listforms.Wizard", "Code": "App.Listforms.Wizard",
"DisplayName": "App.Listforms.Wizard", "DisplayName": "App.Listforms.Wizard",
"Order": 9, "Order": 6,
"Url": "/admin/listform/wizard", "Url": "/admin/listform/wizard",
"Icon": "FcFlashAuto", "Icon": "FcFlashAuto",
"RequiredPermissionName": "App.Listforms.Wizard", "RequiredPermissionName": "App.Listforms.Wizard",

View file

@ -2233,24 +2233,6 @@
"MultiTenancySide": 3, "MultiTenancySide": 3,
"MenuGroup": "Erp|Kurs" "MenuGroup": "Erp|Kurs"
}, },
{
"GroupName": "App.Administration",
"Name": "App.DeveloperKit.Entity",
"ParentName": "App.DeveloperKit",
"DisplayName": "App.DeveloperKit.Entity",
"IsEnabled": true,
"MultiTenancySide": 3,
"MenuGroup": "Erp|Kurs"
},
{
"GroupName": "App.Administration",
"Name": "App.DeveloperKit.Migrations",
"ParentName": "App.DeveloperKit",
"DisplayName": "App.DeveloperKit.Migrations",
"IsEnabled": true,
"MultiTenancySide": 3,
"MenuGroup": "Erp|Kurs"
},
{ {
"GroupName": "App.Administration", "GroupName": "App.Administration",
"Name": "App.DeveloperKit.CrudEndpoints", "Name": "App.DeveloperKit.CrudEndpoints",

View file

@ -29,10 +29,13 @@ public enum TableNameEnum
ForumCategory, ForumCategory,
ForumTopic, ForumTopic,
ForumPost, ForumPost,
SqlTable,
SqlTableField,
SqlView,
SqlStoredProcedure,
SqlFunction,
SqlQuery,
CrudEndpoint, CrudEndpoint,
CrudMigration,
CustomEntity,
CustomEntityField,
CustomEndpoint, CustomEndpoint,
CustomComponent, CustomComponent,
DynamicService, DynamicService,

View file

@ -50,9 +50,8 @@ public static class TableNameResolver
{ nameof(TableNameEnum.Branch), (TablePrefix.TenantByName, MenuPrefix.Saas) }, { nameof(TableNameEnum.Branch), (TablePrefix.TenantByName, MenuPrefix.Saas) },
{ nameof(TableNameEnum.BranchUsers), (TablePrefix.TenantByName, MenuPrefix.Saas) }, { nameof(TableNameEnum.BranchUsers), (TablePrefix.TenantByName, MenuPrefix.Saas) },
{ nameof(TableNameEnum.GlobalSearch), (TablePrefix.TenantByName, MenuPrefix.Saas) }, { nameof(TableNameEnum.GlobalSearch), (TablePrefix.TenantByName, MenuPrefix.Saas) },
{ nameof(TableNameEnum.CustomEntity), (TablePrefix.TenantByName, MenuPrefix.Saas) }, { nameof(TableNameEnum.SqlTable), (TablePrefix.TenantByName, MenuPrefix.Saas) },
{ nameof(TableNameEnum.CustomEntityField), (TablePrefix.TenantByName, MenuPrefix.Saas) }, { nameof(TableNameEnum.SqlTableField), (TablePrefix.TenantByName, MenuPrefix.Saas) },
{ nameof(TableNameEnum.CrudMigration), (TablePrefix.TenantByName, MenuPrefix.Saas) },
{ nameof(TableNameEnum.CrudEndpoint), (TablePrefix.TenantByName, MenuPrefix.Saas) }, { nameof(TableNameEnum.CrudEndpoint), (TablePrefix.TenantByName, MenuPrefix.Saas) },
{ nameof(TableNameEnum.CustomEndpoint), (TablePrefix.TenantByName, MenuPrefix.Saas) }, { nameof(TableNameEnum.CustomEndpoint), (TablePrefix.TenantByName, MenuPrefix.Saas) },
{ nameof(TableNameEnum.CustomComponent), (TablePrefix.TenantByName, MenuPrefix.Saas) }, { nameof(TableNameEnum.CustomComponent), (TablePrefix.TenantByName, MenuPrefix.Saas) },

View file

@ -1,12 +0,0 @@
using System;
using System.Threading.Tasks;
using Sozsoft.Platform.Entities;
using Volo.Abp.Domain.Repositories;
namespace Sozsoft.Platform.Domain.DeveloperKit;
public interface IApiMigrationRepository : IRepository<CrudMigration, Guid>
{
Task ExecuteSqlAsync(string sql);
}

View file

@ -15,9 +15,9 @@ public class CrudEndpoint : FullAuditedEntity<Guid>, IMultiTenant
public string CsharpCode { get; set; } = string.Empty; public string CsharpCode { get; set; } = string.Empty;
public bool IsActive { get; set; } = true; public bool IsActive { get; set; } = true;
// Foreign key to CustomEntity // Foreign key to SqlTable
public Guid EntityId { get; set; } public Guid EntityId { get; set; }
public virtual CustomEntity Entity { get; set; } = null!; public virtual SqlTable Entity { get; set; } = null!;
public CrudEndpoint() public CrudEndpoint()
{ {
Id = Guid.NewGuid(); Id = Guid.NewGuid();

View file

@ -1,19 +0,0 @@
using System;
using Volo.Abp.Domain.Entities.Auditing;
using Volo.Abp.MultiTenancy;
namespace Sozsoft.Platform.Entities;
public class CrudMigration : AuditedEntity<Guid>, IMultiTenant
{
public virtual Guid? TenantId { get; protected set; }
public Guid EntityId { get; set; }
public string EntityName { get; set; } = string.Empty;
public string FileName { get; set; } = string.Empty;
public string SqlScript { get; set; } = string.Empty;
public string Status { get; set; } = "Askıda"; // "pending" | "applied" | "failed"
public DateTime? AppliedAt { get; set; }
public string? ErrorMessage { get; set; }
public virtual CustomEntity Entity { get; set; } = null!;
}

View file

@ -5,7 +5,7 @@ using Volo.Abp.MultiTenancy;
namespace Sozsoft.Platform.Entities; namespace Sozsoft.Platform.Entities;
public class CustomEntity : FullAuditedEntity<Guid>, IMultiTenant public class SqlTable : FullAuditedEntity<Guid>, IMultiTenant
{ {
public virtual Guid? TenantId { get; protected set; } public virtual Guid? TenantId { get; protected set; }
@ -17,19 +17,17 @@ public class CustomEntity : FullAuditedEntity<Guid>, IMultiTenant
public bool IsActive { get; set; } = true; public bool IsActive { get; set; } = true;
public bool IsFullAuditedEntity { get; set; } = true; public bool IsFullAuditedEntity { get; set; } = true;
public bool IsMultiTenant { get; set; } = false; public bool IsMultiTenant { get; set; } = false;
public string MigrationStatus { get; set; } = "Askıda"; public string EndpointStatus { get; set; } = "pending"; // "pending" | "applied" | "failed"
public Guid? MigrationId { get; set; }
public string EndpointStatus { get; set; } = "Askıda"; // "pending" | "applied" | "failed"
public virtual ICollection<CustomEntityField> Fields { get; set; } = []; public virtual ICollection<SqlTableField> Fields { get; set; } = [];
public CustomEntity() public SqlTable()
{ {
Id = Guid.NewGuid(); // Burada erişim mümkün çünkü sınıfın içi Id = Guid.NewGuid(); // Burada erişim mümkün çünkü sınıfın içi
} }
} }
public class CustomEntityField : FullAuditedEntity<Guid> public class SqlTableField : FullAuditedEntity<Guid>
{ {
public Guid EntityId { get; set; } = Guid.NewGuid(); public Guid EntityId { get; set; } = Guid.NewGuid();
public string Name { get; set; } = string.Empty; public string Name { get; set; } = string.Empty;
@ -41,9 +39,9 @@ public class CustomEntityField : FullAuditedEntity<Guid>
public string? Description { get; set; } public string? Description { get; set; }
public int DisplayOrder { get; set; } = 0; public int DisplayOrder { get; set; } = 0;
public virtual CustomEntity Entity { get; set; } = null!; public virtual SqlTable Entity { get; set; } = null!;
public CustomEntityField() public SqlTableField()
{ {
Id = Guid.NewGuid(); // Burada erişim mümkün çünkü sınıfın içi Id = Guid.NewGuid(); // Burada erişim mümkün çünkü sınıfın içi
} }

View file

@ -1,24 +0,0 @@
using System;
using System.Threading.Tasks;
using Sozsoft.Platform.Entities;
using Sozsoft.Platform.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using Volo.Abp.Domain.Repositories.EntityFrameworkCore;
using Volo.Abp.EntityFrameworkCore;
namespace Sozsoft.Platform.Domain.DeveloperKit;
public class PlatformApiMigrationRepository : EfCoreRepository<PlatformDbContext, CrudMigration, Guid>, IApiMigrationRepository
{
public PlatformApiMigrationRepository(IDbContextProvider<PlatformDbContext> dbContextProvider)
: base(dbContextProvider)
{
}
public async Task ExecuteSqlAsync(string sql)
{
var dbContext = await GetDbContextAsync();
await dbContext.Database.ExecuteSqlRawAsync(sql);
}
}

View file

@ -15,14 +15,14 @@ using Volo.Abp.EntityFrameworkCore;
public class DynamicEntityManager : IDynamicEntityManager public class DynamicEntityManager : IDynamicEntityManager
{ {
private readonly IDbContextProvider<PlatformDbContext> _dbContextProvider; private readonly IDbContextProvider<PlatformDbContext> _dbContextProvider;
private readonly IRepository<CustomEntity, Guid> _customEntityRepository; private readonly IRepository<SqlTable, Guid> _sqlTableRepository;
public DynamicEntityManager( public DynamicEntityManager(
IDbContextProvider<PlatformDbContext> dbContextProvider, IDbContextProvider<PlatformDbContext> dbContextProvider,
IRepository<CustomEntity, Guid> customEntityRepository) IRepository<SqlTable, Guid> sqlTableRepository)
{ {
_dbContextProvider = dbContextProvider; _dbContextProvider = dbContextProvider;
_customEntityRepository = customEntityRepository; _sqlTableRepository = sqlTableRepository;
} }
public async Task<List<object>?> GetEntityListAsync(string entityName) public async Task<List<object>?> GetEntityListAsync(string entityName)
@ -167,9 +167,9 @@ public class DynamicEntityManager : IDynamicEntityManager
} }
} }
private async Task<CustomEntity> GetEntityDefinitionAsync(string entityName) private async Task<SqlTable> GetEntityDefinitionAsync(string entityName)
{ {
var queryable = await _customEntityRepository.WithDetailsAsync(x => x.Fields); var queryable = await _sqlTableRepository.WithDetailsAsync(x => x.Fields);
var entity = await queryable.FirstOrDefaultAsync(x => x.Name.ToLower() == entityName.ToLower()); var entity = await queryable.FirstOrDefaultAsync(x => x.Name.ToLower() == entityName.ToLower());
return entity ?? throw new UserFriendlyException($"Entity '{entityName}' not found."); return entity ?? throw new UserFriendlyException($"Entity '{entityName}' not found.");

View file

@ -58,9 +58,8 @@ public class PlatformDbContext :
public DbSet<ForumCategory> ForumCategories { get; set; } public DbSet<ForumCategory> ForumCategories { get; set; }
public DbSet<ForumTopic> ForumTopics { get; set; } public DbSet<ForumTopic> ForumTopics { get; set; }
public DbSet<ForumPost> ForumPosts { get; set; } public DbSet<ForumPost> ForumPosts { get; set; }
public DbSet<CustomEntity> CustomEntities { get; set; } public DbSet<SqlTable> CustomEntities { get; set; }
public DbSet<CustomEntityField> EntityFields { get; set; } public DbSet<SqlTableField> EntityFields { get; set; }
public DbSet<CrudMigration> Migrations { get; set; }
public DbSet<CrudEndpoint> GeneratedEndpoints { get; set; } public DbSet<CrudEndpoint> GeneratedEndpoints { get; set; }
public DbSet<CustomEndpoint> CustomEndpoints { get; set; } public DbSet<CustomEndpoint> CustomEndpoints { get; set; }
public DbSet<CustomComponent> CustomComponents { get; set; } public DbSet<CustomComponent> CustomComponents { get; set; }
@ -516,9 +515,9 @@ public class PlatformDbContext :
.OnDelete(DeleteBehavior.Restrict); .OnDelete(DeleteBehavior.Restrict);
}); });
builder.Entity<CustomEntity>(b => builder.Entity<SqlTable>(b =>
{ {
b.ToTable(TableNameResolver.GetFullTableName(nameof(TableNameEnum.CustomEntity)), Prefix.DbSchema); b.ToTable(TableNameResolver.GetFullTableName(nameof(TableNameEnum.SqlTable)), Prefix.DbSchema);
b.ConfigureByConvention(); b.ConfigureByConvention();
b.HasKey(x => x.Id); b.HasKey(x => x.Id);
@ -526,7 +525,6 @@ public class PlatformDbContext :
b.Property(x => x.DisplayName).IsRequired().HasMaxLength(128); b.Property(x => x.DisplayName).IsRequired().HasMaxLength(128);
b.Property(x => x.TableName).IsRequired().HasMaxLength(128); b.Property(x => x.TableName).IsRequired().HasMaxLength(128);
b.Property(x => x.Description).HasMaxLength(512); b.Property(x => x.Description).HasMaxLength(512);
b.Property(x => x.MigrationStatus).IsRequired().HasMaxLength(64);
b.Property(x => x.EndpointStatus).IsRequired().HasMaxLength(64); b.Property(x => x.EndpointStatus).IsRequired().HasMaxLength(64);
b.Property(x => x.Menu).IsRequired().HasMaxLength(128); b.Property(x => x.Menu).IsRequired().HasMaxLength(128);
@ -536,9 +534,9 @@ public class PlatformDbContext :
.OnDelete(DeleteBehavior.Cascade); .OnDelete(DeleteBehavior.Cascade);
}); });
builder.Entity<CustomEntityField>(b => builder.Entity<SqlTableField>(b =>
{ {
b.ToTable(TableNameResolver.GetFullTableName(nameof(TableNameEnum.CustomEntityField)), Prefix.DbSchema); b.ToTable(TableNameResolver.GetFullTableName(nameof(TableNameEnum.SqlTableField)), Prefix.DbSchema);
b.ConfigureByConvention(); b.ConfigureByConvention();
b.HasKey(x => x.Id); b.HasKey(x => x.Id);
@ -548,24 +546,6 @@ public class PlatformDbContext :
b.Property(x => x.DefaultValue).HasMaxLength(256); b.Property(x => x.DefaultValue).HasMaxLength(256);
}); });
builder.Entity<CrudMigration>(b =>
{
b.ToTable(TableNameResolver.GetFullTableName(nameof(TableNameEnum.CrudMigration)), Prefix.DbSchema);
b.ConfigureByConvention();
b.HasKey(x => x.Id);
b.Property(x => x.EntityName).IsRequired().HasMaxLength(128);
b.Property(x => x.FileName).IsRequired().HasMaxLength(256);
b.Property(x => x.SqlScript).IsRequired();
b.Property(x => x.Status).IsRequired().HasMaxLength(64);
b.Property(x => x.ErrorMessage).HasMaxLength(1024);
b.HasOne(x => x.Entity)
.WithMany()
.HasForeignKey(x => x.EntityId)
.OnDelete(DeleteBehavior.Restrict);
});
builder.Entity<CrudEndpoint>(b => builder.Entity<CrudEndpoint>(b =>
{ {
b.ToTable(TableNameResolver.GetFullTableName(nameof(TableNameEnum.CrudEndpoint)), Prefix.DbSchema); b.ToTable(TableNameResolver.GetFullTableName(nameof(TableNameEnum.CrudEndpoint)), Prefix.DbSchema);

View file

@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore;
namespace Sozsoft.Platform.Migrations namespace Sozsoft.Platform.Migrations
{ {
[DbContext(typeof(PlatformDbContext))] [DbContext(typeof(PlatformDbContext))]
[Migration("20260301173343_Initial")] [Migration("20260301203437_Initial")]
partial class Initial partial class Initial
{ {
/// <inheritdoc /> /// <inheritdoc />
@ -1419,67 +1419,6 @@ namespace Sozsoft.Platform.Migrations
b.ToTable("Sas_T_CrudEndpoint", (string)null); b.ToTable("Sas_T_CrudEndpoint", (string)null);
}); });
modelBuilder.Entity("Sozsoft.Platform.Entities.CrudMigration", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier");
b.Property<DateTime?>("AppliedAt")
.HasColumnType("datetime2");
b.Property<DateTime>("CreationTime")
.HasColumnType("datetime2")
.HasColumnName("CreationTime");
b.Property<Guid?>("CreatorId")
.HasColumnType("uniqueidentifier")
.HasColumnName("CreatorId");
b.Property<Guid>("EntityId")
.HasColumnType("uniqueidentifier");
b.Property<string>("EntityName")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("ErrorMessage")
.HasMaxLength(1024)
.HasColumnType("nvarchar(1024)");
b.Property<string>("FileName")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2")
.HasColumnName("LastModificationTime");
b.Property<Guid?>("LastModifierId")
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<string>("SqlScript")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<Guid?>("TenantId")
.HasColumnType("uniqueidentifier")
.HasColumnName("TenantId");
b.HasKey("Id");
b.HasIndex("EntityId");
b.ToTable("Sas_T_CrudMigration", (string)null);
});
modelBuilder.Entity("Sozsoft.Platform.Entities.Currency", b => modelBuilder.Entity("Sozsoft.Platform.Entities.Currency", b =>
{ {
b.Property<string>("Id") b.Property<string>("Id")
@ -1688,171 +1627,6 @@ namespace Sozsoft.Platform.Migrations
b.ToTable("Sas_T_CustomEndpoint", (string)null); b.ToTable("Sas_T_CustomEndpoint", (string)null);
}); });
modelBuilder.Entity("Sozsoft.Platform.Entities.CustomEntity", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("CreationTime")
.HasColumnType("datetime2")
.HasColumnName("CreationTime");
b.Property<Guid?>("CreatorId")
.HasColumnType("uniqueidentifier")
.HasColumnName("CreatorId");
b.Property<Guid?>("DeleterId")
.HasColumnType("uniqueidentifier")
.HasColumnName("DeleterId");
b.Property<DateTime?>("DeletionTime")
.HasColumnType("datetime2")
.HasColumnName("DeletionTime");
b.Property<string>("Description")
.HasMaxLength(512)
.HasColumnType("nvarchar(512)");
b.Property<string>("DisplayName")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("EndpointStatus")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<bool>("IsActive")
.HasColumnType("bit");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false)
.HasColumnName("IsDeleted");
b.Property<bool>("IsFullAuditedEntity")
.HasColumnType("bit");
b.Property<bool>("IsMultiTenant")
.HasColumnType("bit");
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2")
.HasColumnName("LastModificationTime");
b.Property<Guid?>("LastModifierId")
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<string>("Menu")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<Guid?>("MigrationId")
.HasColumnType("uniqueidentifier");
b.Property<string>("MigrationStatus")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("TableName")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<Guid?>("TenantId")
.HasColumnType("uniqueidentifier")
.HasColumnName("TenantId");
b.HasKey("Id");
b.ToTable("Sas_T_CustomEntity", (string)null);
});
modelBuilder.Entity("Sozsoft.Platform.Entities.CustomEntityField", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("CreationTime")
.HasColumnType("datetime2")
.HasColumnName("CreationTime");
b.Property<Guid?>("CreatorId")
.HasColumnType("uniqueidentifier")
.HasColumnName("CreatorId");
b.Property<string>("DefaultValue")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<Guid?>("DeleterId")
.HasColumnType("uniqueidentifier")
.HasColumnName("DeleterId");
b.Property<DateTime?>("DeletionTime")
.HasColumnType("datetime2")
.HasColumnName("DeletionTime");
b.Property<string>("Description")
.HasMaxLength(512)
.HasColumnType("nvarchar(512)");
b.Property<int>("DisplayOrder")
.HasColumnType("int");
b.Property<Guid>("EntityId")
.HasColumnType("uniqueidentifier");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false)
.HasColumnName("IsDeleted");
b.Property<bool>("IsRequired")
.HasColumnType("bit");
b.Property<bool>("IsUnique")
.HasColumnType("bit");
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2")
.HasColumnName("LastModificationTime");
b.Property<Guid?>("LastModifierId")
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<int?>("MaxLength")
.HasColumnType("int");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.HasKey("Id");
b.HasIndex("EntityId");
b.ToTable("Sas_T_CustomEntityField", (string)null);
});
modelBuilder.Entity("Sozsoft.Platform.Entities.DataSource", b => modelBuilder.Entity("Sozsoft.Platform.Entities.DataSource", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
@ -4001,6 +3775,163 @@ namespace Sozsoft.Platform.Migrations
b.ToTable("Adm_T_SkillType", (string)null); b.ToTable("Adm_T_SkillType", (string)null);
}); });
modelBuilder.Entity("Sozsoft.Platform.Entities.SqlTable", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("CreationTime")
.HasColumnType("datetime2")
.HasColumnName("CreationTime");
b.Property<Guid?>("CreatorId")
.HasColumnType("uniqueidentifier")
.HasColumnName("CreatorId");
b.Property<Guid?>("DeleterId")
.HasColumnType("uniqueidentifier")
.HasColumnName("DeleterId");
b.Property<DateTime?>("DeletionTime")
.HasColumnType("datetime2")
.HasColumnName("DeletionTime");
b.Property<string>("Description")
.HasMaxLength(512)
.HasColumnType("nvarchar(512)");
b.Property<string>("DisplayName")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("EndpointStatus")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<bool>("IsActive")
.HasColumnType("bit");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false)
.HasColumnName("IsDeleted");
b.Property<bool>("IsFullAuditedEntity")
.HasColumnType("bit");
b.Property<bool>("IsMultiTenant")
.HasColumnType("bit");
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2")
.HasColumnName("LastModificationTime");
b.Property<Guid?>("LastModifierId")
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<string>("Menu")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("TableName")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<Guid?>("TenantId")
.HasColumnType("uniqueidentifier")
.HasColumnName("TenantId");
b.HasKey("Id");
b.ToTable("Sas_T_SqlTable", (string)null);
});
modelBuilder.Entity("Sozsoft.Platform.Entities.SqlTableField", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("CreationTime")
.HasColumnType("datetime2")
.HasColumnName("CreationTime");
b.Property<Guid?>("CreatorId")
.HasColumnType("uniqueidentifier")
.HasColumnName("CreatorId");
b.Property<string>("DefaultValue")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<Guid?>("DeleterId")
.HasColumnType("uniqueidentifier")
.HasColumnName("DeleterId");
b.Property<DateTime?>("DeletionTime")
.HasColumnType("datetime2")
.HasColumnName("DeletionTime");
b.Property<string>("Description")
.HasMaxLength(512)
.HasColumnType("nvarchar(512)");
b.Property<int>("DisplayOrder")
.HasColumnType("int");
b.Property<Guid>("EntityId")
.HasColumnType("uniqueidentifier");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false)
.HasColumnName("IsDeleted");
b.Property<bool>("IsRequired")
.HasColumnType("bit");
b.Property<bool>("IsUnique")
.HasColumnType("bit");
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2")
.HasColumnName("LastModificationTime");
b.Property<Guid?>("LastModifierId")
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<int?>("MaxLength")
.HasColumnType("int");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.HasKey("Id");
b.HasIndex("EntityId");
b.ToTable("Sas_T_SqlTableField", (string)null);
});
modelBuilder.Entity("Sozsoft.Platform.Entities.Uom", b => modelBuilder.Entity("Sozsoft.Platform.Entities.Uom", b =>
{ {
b.Property<string>("Id") b.Property<string>("Id")
@ -4663,7 +4594,7 @@ namespace Sozsoft.Platform.Migrations
b.HasIndex("SchemaName", "FunctionName"); b.HasIndex("SchemaName", "FunctionName");
b.ToTable("Sqm_T_SqlFunction", (string)null); b.ToTable("Sas_T_SqlFunction", (string)null);
}); });
modelBuilder.Entity("Sozsoft.SqlQueryManager.Domain.Entities.SqlQuery", b => modelBuilder.Entity("Sozsoft.SqlQueryManager.Domain.Entities.SqlQuery", b =>
@ -4763,7 +4694,7 @@ namespace Sozsoft.Platform.Migrations
b.HasIndex("Status"); b.HasIndex("Status");
b.ToTable("Sqm_T_SqlQuery", (string)null); b.ToTable("Sas_T_SqlQuery", (string)null);
}); });
modelBuilder.Entity("Sozsoft.SqlQueryManager.Domain.Entities.SqlStoredProcedure", b => modelBuilder.Entity("Sozsoft.SqlQueryManager.Domain.Entities.SqlStoredProcedure", b =>
@ -4861,7 +4792,7 @@ namespace Sozsoft.Platform.Migrations
b.HasIndex("SchemaName", "ProcedureName"); b.HasIndex("SchemaName", "ProcedureName");
b.ToTable("Sqm_T_SqlStoredProcedure", (string)null); b.ToTable("Sas_T_SqlStoredProcedure", (string)null);
}); });
modelBuilder.Entity("Sozsoft.SqlQueryManager.Domain.Entities.SqlView", b => modelBuilder.Entity("Sozsoft.SqlQueryManager.Domain.Entities.SqlView", b =>
@ -4958,7 +4889,7 @@ namespace Sozsoft.Platform.Migrations
b.HasIndex("SchemaName", "ViewName"); b.HasIndex("SchemaName", "ViewName");
b.ToTable("Sqm_T_SqlView", (string)null); b.ToTable("Sas_T_SqlView", (string)null);
}); });
modelBuilder.Entity("Volo.Abp.AuditLogging.AuditLog", b => modelBuilder.Entity("Volo.Abp.AuditLogging.AuditLog", b =>
@ -6879,7 +6810,7 @@ namespace Sozsoft.Platform.Migrations
modelBuilder.Entity("Sozsoft.Platform.Entities.CrudEndpoint", b => modelBuilder.Entity("Sozsoft.Platform.Entities.CrudEndpoint", b =>
{ {
b.HasOne("Sozsoft.Platform.Entities.CustomEntity", "Entity") b.HasOne("Sozsoft.Platform.Entities.SqlTable", "Entity")
.WithMany() .WithMany()
.HasForeignKey("EntityId") .HasForeignKey("EntityId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
@ -6888,28 +6819,6 @@ namespace Sozsoft.Platform.Migrations
b.Navigation("Entity"); b.Navigation("Entity");
}); });
modelBuilder.Entity("Sozsoft.Platform.Entities.CrudMigration", b =>
{
b.HasOne("Sozsoft.Platform.Entities.CustomEntity", "Entity")
.WithMany()
.HasForeignKey("EntityId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Entity");
});
modelBuilder.Entity("Sozsoft.Platform.Entities.CustomEntityField", b =>
{
b.HasOne("Sozsoft.Platform.Entities.CustomEntity", "Entity")
.WithMany("Fields")
.HasForeignKey("EntityId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Entity");
});
modelBuilder.Entity("Sozsoft.Platform.Entities.District", b => modelBuilder.Entity("Sozsoft.Platform.Entities.District", b =>
{ {
b.HasOne("Sozsoft.Platform.Entities.City", null) b.HasOne("Sozsoft.Platform.Entities.City", null)
@ -7000,6 +6909,17 @@ namespace Sozsoft.Platform.Migrations
b.Navigation("SkillType"); b.Navigation("SkillType");
}); });
modelBuilder.Entity("Sozsoft.Platform.Entities.SqlTableField", b =>
{
b.HasOne("Sozsoft.Platform.Entities.SqlTable", "Entity")
.WithMany("Fields")
.HasForeignKey("EntityId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Entity");
});
modelBuilder.Entity("Sozsoft.Platform.Entities.Uom", b => modelBuilder.Entity("Sozsoft.Platform.Entities.Uom", b =>
{ {
b.HasOne("Sozsoft.Platform.Entities.UomCategory", "UomCategory") b.HasOne("Sozsoft.Platform.Entities.UomCategory", "UomCategory")
@ -7222,11 +7142,6 @@ namespace Sozsoft.Platform.Migrations
b.Navigation("Cities"); b.Navigation("Cities");
}); });
modelBuilder.Entity("Sozsoft.Platform.Entities.CustomEntity", b =>
{
b.Navigation("Fields");
});
modelBuilder.Entity("Sozsoft.Platform.Entities.Order", b => modelBuilder.Entity("Sozsoft.Platform.Entities.Order", b =>
{ {
b.Navigation("Items"); b.Navigation("Items");
@ -7244,6 +7159,11 @@ namespace Sozsoft.Platform.Migrations
b.Navigation("Skills"); b.Navigation("Skills");
}); });
modelBuilder.Entity("Sozsoft.Platform.Entities.SqlTable", b =>
{
b.Navigation("Fields");
});
modelBuilder.Entity("Sozsoft.Platform.Entities.UomCategory", b => modelBuilder.Entity("Sozsoft.Platform.Entities.UomCategory", b =>
{ {
b.Navigation("Uoms"); b.Navigation("Uoms");

View file

@ -1362,36 +1362,6 @@ namespace Sozsoft.Platform.Migrations
table.PrimaryKey("PK_Sas_T_CustomEndpoint", x => x.Id); table.PrimaryKey("PK_Sas_T_CustomEndpoint", x => x.Id);
}); });
migrationBuilder.CreateTable(
name: "Sas_T_CustomEntity",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
TenantId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
Menu = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
Name = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
DisplayName = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
TableName = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
Description = table.Column<string>(type: "nvarchar(512)", maxLength: 512, nullable: true),
IsActive = table.Column<bool>(type: "bit", nullable: false),
IsFullAuditedEntity = table.Column<bool>(type: "bit", nullable: false),
IsMultiTenant = table.Column<bool>(type: "bit", nullable: false),
MigrationStatus = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
MigrationId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
EndpointStatus = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false),
CreatorId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModifierId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
IsDeleted = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
DeleterId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
DeletionTime = table.Column<DateTime>(type: "datetime2", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Sas_T_CustomEntity", x => x.Id);
});
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "Sas_T_DynamicService", name: "Sas_T_DynamicService",
columns: table => new columns: table => new
@ -1531,7 +1501,7 @@ namespace Sozsoft.Platform.Migrations
}); });
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "Sqm_T_SqlFunction", name: "Sas_T_SqlFunction",
columns: table => new columns: table => new
{ {
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false), Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
@ -1559,11 +1529,11 @@ namespace Sozsoft.Platform.Migrations
}, },
constraints: table => constraints: table =>
{ {
table.PrimaryKey("PK_Sqm_T_SqlFunction", x => x.Id); table.PrimaryKey("PK_Sas_T_SqlFunction", x => x.Id);
}); });
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "Sqm_T_SqlQuery", name: "Sas_T_SqlQuery",
columns: table => new columns: table => new
{ {
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false), Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
@ -1590,11 +1560,11 @@ namespace Sozsoft.Platform.Migrations
}, },
constraints: table => constraints: table =>
{ {
table.PrimaryKey("PK_Sqm_T_SqlQuery", x => x.Id); table.PrimaryKey("PK_Sas_T_SqlQuery", x => x.Id);
}); });
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "Sqm_T_SqlStoredProcedure", name: "Sas_T_SqlStoredProcedure",
columns: table => new columns: table => new
{ {
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false), Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
@ -1620,11 +1590,39 @@ namespace Sozsoft.Platform.Migrations
}, },
constraints: table => constraints: table =>
{ {
table.PrimaryKey("PK_Sqm_T_SqlStoredProcedure", x => x.Id); table.PrimaryKey("PK_Sas_T_SqlStoredProcedure", x => x.Id);
}); });
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "Sqm_T_SqlView", name: "Sas_T_SqlTable",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
TenantId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
Menu = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
Name = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
DisplayName = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
TableName = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
Description = table.Column<string>(type: "nvarchar(512)", maxLength: 512, nullable: true),
IsActive = table.Column<bool>(type: "bit", nullable: false),
IsFullAuditedEntity = table.Column<bool>(type: "bit", nullable: false),
IsMultiTenant = table.Column<bool>(type: "bit", nullable: false),
EndpointStatus = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false),
CreatorId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModifierId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
IsDeleted = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
DeleterId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
DeletionTime = table.Column<DateTime>(type: "datetime2", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Sas_T_SqlTable", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Sas_T_SqlView",
columns: table => new columns: table => new
{ {
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false), Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
@ -1650,7 +1648,7 @@ namespace Sozsoft.Platform.Migrations
}, },
constraints: table => constraints: table =>
{ {
table.PrimaryKey("PK_Sqm_T_SqlView", x => x.Id); table.PrimaryKey("PK_Sas_T_SqlView", x => x.Id);
}); });
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
@ -2376,100 +2374,6 @@ namespace Sozsoft.Platform.Migrations
onDelete: ReferentialAction.Restrict); onDelete: ReferentialAction.Restrict);
}); });
migrationBuilder.CreateTable(
name: "Sas_T_CrudEndpoint",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
TenantId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
EntityName = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
Method = table.Column<string>(type: "nvarchar(10)", maxLength: 10, nullable: false),
Path = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: false),
OperationType = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
CsharpCode = table.Column<string>(type: "nvarchar(max)", nullable: false),
IsActive = table.Column<bool>(type: "bit", nullable: false),
EntityId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false),
CreatorId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModifierId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
IsDeleted = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
DeleterId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
DeletionTime = table.Column<DateTime>(type: "datetime2", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Sas_T_CrudEndpoint", x => x.Id);
table.ForeignKey(
name: "FK_Sas_T_CrudEndpoint_Sas_T_CustomEntity_EntityId",
column: x => x.EntityId,
principalTable: "Sas_T_CustomEntity",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "Sas_T_CrudMigration",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
TenantId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
EntityId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
EntityName = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
FileName = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: false),
SqlScript = table.Column<string>(type: "nvarchar(max)", nullable: false),
Status = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
AppliedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
ErrorMessage = table.Column<string>(type: "nvarchar(1024)", maxLength: 1024, nullable: true),
CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false),
CreatorId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModifierId = table.Column<Guid>(type: "uniqueidentifier", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Sas_T_CrudMigration", x => x.Id);
table.ForeignKey(
name: "FK_Sas_T_CrudMigration_Sas_T_CustomEntity_EntityId",
column: x => x.EntityId,
principalTable: "Sas_T_CustomEntity",
principalColumn: "Id",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateTable(
name: "Sas_T_CustomEntityField",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
EntityId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
Name = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
Type = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
IsRequired = table.Column<bool>(type: "bit", nullable: false),
MaxLength = table.Column<int>(type: "int", nullable: true),
IsUnique = table.Column<bool>(type: "bit", nullable: false),
DefaultValue = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
Description = table.Column<string>(type: "nvarchar(512)", maxLength: 512, nullable: true),
DisplayOrder = table.Column<int>(type: "int", nullable: false),
CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false),
CreatorId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModifierId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
IsDeleted = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
DeleterId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
DeletionTime = table.Column<DateTime>(type: "datetime2", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Sas_T_CustomEntityField", x => x.Id);
table.ForeignKey(
name: "FK_Sas_T_CustomEntityField_Sas_T_CustomEntity_EntityId",
column: x => x.EntityId,
principalTable: "Sas_T_CustomEntity",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "Sas_T_ReportTemplate", name: "Sas_T_ReportTemplate",
columns: table => new columns: table => new
@ -2500,6 +2404,71 @@ namespace Sozsoft.Platform.Migrations
onDelete: ReferentialAction.Cascade); onDelete: ReferentialAction.Cascade);
}); });
migrationBuilder.CreateTable(
name: "Sas_T_CrudEndpoint",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
TenantId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
EntityName = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
Method = table.Column<string>(type: "nvarchar(10)", maxLength: 10, nullable: false),
Path = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: false),
OperationType = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
CsharpCode = table.Column<string>(type: "nvarchar(max)", nullable: false),
IsActive = table.Column<bool>(type: "bit", nullable: false),
EntityId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false),
CreatorId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModifierId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
IsDeleted = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
DeleterId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
DeletionTime = table.Column<DateTime>(type: "datetime2", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Sas_T_CrudEndpoint", x => x.Id);
table.ForeignKey(
name: "FK_Sas_T_CrudEndpoint_Sas_T_SqlTable_EntityId",
column: x => x.EntityId,
principalTable: "Sas_T_SqlTable",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable(
name: "Sas_T_SqlTableField",
columns: table => new
{
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
EntityId = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
Name = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
Type = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
IsRequired = table.Column<bool>(type: "bit", nullable: false),
MaxLength = table.Column<int>(type: "int", nullable: true),
IsUnique = table.Column<bool>(type: "bit", nullable: false),
DefaultValue = table.Column<string>(type: "nvarchar(256)", maxLength: 256, nullable: true),
Description = table.Column<string>(type: "nvarchar(512)", maxLength: 512, nullable: true),
DisplayOrder = table.Column<int>(type: "int", nullable: false),
CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false),
CreatorId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),
LastModifierId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
IsDeleted = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
DeleterId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
DeletionTime = table.Column<DateTime>(type: "datetime2", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_Sas_T_SqlTableField", x => x.Id);
table.ForeignKey(
name: "FK_Sas_T_SqlTableField_Sas_T_SqlTable_EntityId",
column: x => x.EntityId,
principalTable: "Sas_T_SqlTable",
principalColumn: "Id",
onDelete: ReferentialAction.Cascade);
});
migrationBuilder.CreateTable( migrationBuilder.CreateTable(
name: "AbpEntityPropertyChanges", name: "AbpEntityPropertyChanges",
columns: table => new columns: table => new
@ -3148,104 +3117,99 @@ namespace Sozsoft.Platform.Migrations
table: "Sas_T_CrudEndpoint", table: "Sas_T_CrudEndpoint",
column: "EntityId"); column: "EntityId");
migrationBuilder.CreateIndex(
name: "IX_Sas_T_CrudMigration_EntityId",
table: "Sas_T_CrudMigration",
column: "EntityId");
migrationBuilder.CreateIndex(
name: "IX_Sas_T_CustomEntityField_EntityId",
table: "Sas_T_CustomEntityField",
column: "EntityId");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_Sas_T_ReportTemplate_CategoryId", name: "IX_Sas_T_ReportTemplate_CategoryId",
table: "Sas_T_ReportTemplate", table: "Sas_T_ReportTemplate",
column: "CategoryId"); column: "CategoryId");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_Sqm_T_SqlFunction_DataSourceCode", name: "IX_Sas_T_SqlFunction_DataSourceCode",
table: "Sqm_T_SqlFunction", table: "Sas_T_SqlFunction",
column: "DataSourceCode"); column: "DataSourceCode");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_Sqm_T_SqlFunction_FunctionType", name: "IX_Sas_T_SqlFunction_FunctionType",
table: "Sqm_T_SqlFunction", table: "Sas_T_SqlFunction",
column: "FunctionType"); column: "FunctionType");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_Sqm_T_SqlFunction_IsDeployed", name: "IX_Sas_T_SqlFunction_IsDeployed",
table: "Sqm_T_SqlFunction", table: "Sas_T_SqlFunction",
column: "IsDeployed"); column: "IsDeployed");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_Sqm_T_SqlFunction_SchemaName_FunctionName", name: "IX_Sas_T_SqlFunction_SchemaName_FunctionName",
table: "Sqm_T_SqlFunction", table: "Sas_T_SqlFunction",
columns: new[] { "SchemaName", "FunctionName" }); columns: new[] { "SchemaName", "FunctionName" });
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_Sqm_T_SqlFunction_Status", name: "IX_Sas_T_SqlFunction_Status",
table: "Sqm_T_SqlFunction", table: "Sas_T_SqlFunction",
column: "Status"); column: "Status");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_Sqm_T_SqlQuery_Category", name: "IX_Sas_T_SqlQuery_Category",
table: "Sqm_T_SqlQuery", table: "Sas_T_SqlQuery",
column: "Category"); column: "Category");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_Sqm_T_SqlQuery_Code", name: "IX_Sas_T_SqlQuery_Code",
table: "Sqm_T_SqlQuery", table: "Sas_T_SqlQuery",
column: "Code"); column: "Code");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_Sqm_T_SqlQuery_DataSourceCode", name: "IX_Sas_T_SqlQuery_DataSourceCode",
table: "Sqm_T_SqlQuery", table: "Sas_T_SqlQuery",
column: "DataSourceCode"); column: "DataSourceCode");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_Sqm_T_SqlQuery_Status", name: "IX_Sas_T_SqlQuery_Status",
table: "Sqm_T_SqlQuery", table: "Sas_T_SqlQuery",
column: "Status"); column: "Status");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_Sqm_T_SqlStoredProcedure_DataSourceCode", name: "IX_Sas_T_SqlStoredProcedure_DataSourceCode",
table: "Sqm_T_SqlStoredProcedure", table: "Sas_T_SqlStoredProcedure",
column: "DataSourceCode"); column: "DataSourceCode");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_Sqm_T_SqlStoredProcedure_IsDeployed", name: "IX_Sas_T_SqlStoredProcedure_IsDeployed",
table: "Sqm_T_SqlStoredProcedure", table: "Sas_T_SqlStoredProcedure",
column: "IsDeployed"); column: "IsDeployed");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_Sqm_T_SqlStoredProcedure_SchemaName_ProcedureName", name: "IX_Sas_T_SqlStoredProcedure_SchemaName_ProcedureName",
table: "Sqm_T_SqlStoredProcedure", table: "Sas_T_SqlStoredProcedure",
columns: new[] { "SchemaName", "ProcedureName" }); columns: new[] { "SchemaName", "ProcedureName" });
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_Sqm_T_SqlStoredProcedure_Status", name: "IX_Sas_T_SqlStoredProcedure_Status",
table: "Sqm_T_SqlStoredProcedure", table: "Sas_T_SqlStoredProcedure",
column: "Status"); column: "Status");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_Sqm_T_SqlView_DataSourceCode", name: "IX_Sas_T_SqlTableField_EntityId",
table: "Sqm_T_SqlView", table: "Sas_T_SqlTableField",
column: "EntityId");
migrationBuilder.CreateIndex(
name: "IX_Sas_T_SqlView_DataSourceCode",
table: "Sas_T_SqlView",
column: "DataSourceCode"); column: "DataSourceCode");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_Sqm_T_SqlView_IsDeployed", name: "IX_Sas_T_SqlView_IsDeployed",
table: "Sqm_T_SqlView", table: "Sas_T_SqlView",
column: "IsDeployed"); column: "IsDeployed");
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_Sqm_T_SqlView_SchemaName_ViewName", name: "IX_Sas_T_SqlView_SchemaName_ViewName",
table: "Sqm_T_SqlView", table: "Sas_T_SqlView",
columns: new[] { "SchemaName", "ViewName" }); columns: new[] { "SchemaName", "ViewName" });
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_Sqm_T_SqlView_Status", name: "IX_Sas_T_SqlView_Status",
table: "Sqm_T_SqlView", table: "Sas_T_SqlView",
column: "Status"); column: "Status");
} }
@ -3426,18 +3390,12 @@ namespace Sozsoft.Platform.Migrations
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "Sas_T_CrudEndpoint"); name: "Sas_T_CrudEndpoint");
migrationBuilder.DropTable(
name: "Sas_T_CrudMigration");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "Sas_T_CustomComponent"); name: "Sas_T_CustomComponent");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "Sas_T_CustomEndpoint"); name: "Sas_T_CustomEndpoint");
migrationBuilder.DropTable(
name: "Sas_T_CustomEntityField");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "Sas_T_DynamicService"); name: "Sas_T_DynamicService");
@ -3457,16 +3415,19 @@ namespace Sozsoft.Platform.Migrations
name: "Sas_T_Sector"); name: "Sas_T_Sector");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "Sqm_T_SqlFunction"); name: "Sas_T_SqlFunction");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "Sqm_T_SqlQuery"); name: "Sas_T_SqlQuery");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "Sqm_T_SqlStoredProcedure"); name: "Sas_T_SqlStoredProcedure");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "Sqm_T_SqlView"); name: "Sas_T_SqlTableField");
migrationBuilder.DropTable(
name: "Sas_T_SqlView");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "AbpEntityChanges"); name: "AbpEntityChanges");
@ -3523,10 +3484,10 @@ namespace Sozsoft.Platform.Migrations
name: "Sas_T_Branch"); name: "Sas_T_Branch");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "Sas_T_CustomEntity"); name: "Sas_T_ReportCategory");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "Sas_T_ReportCategory"); name: "Sas_T_SqlTable");
migrationBuilder.DropTable( migrationBuilder.DropTable(
name: "AbpAuditLogs"); name: "AbpAuditLogs");

View file

@ -1416,67 +1416,6 @@ namespace Sozsoft.Platform.Migrations
b.ToTable("Sas_T_CrudEndpoint", (string)null); b.ToTable("Sas_T_CrudEndpoint", (string)null);
}); });
modelBuilder.Entity("Sozsoft.Platform.Entities.CrudMigration", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier");
b.Property<DateTime?>("AppliedAt")
.HasColumnType("datetime2");
b.Property<DateTime>("CreationTime")
.HasColumnType("datetime2")
.HasColumnName("CreationTime");
b.Property<Guid?>("CreatorId")
.HasColumnType("uniqueidentifier")
.HasColumnName("CreatorId");
b.Property<Guid>("EntityId")
.HasColumnType("uniqueidentifier");
b.Property<string>("EntityName")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("ErrorMessage")
.HasMaxLength(1024)
.HasColumnType("nvarchar(1024)");
b.Property<string>("FileName")
.IsRequired()
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2")
.HasColumnName("LastModificationTime");
b.Property<Guid?>("LastModifierId")
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<string>("SqlScript")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<Guid?>("TenantId")
.HasColumnType("uniqueidentifier")
.HasColumnName("TenantId");
b.HasKey("Id");
b.HasIndex("EntityId");
b.ToTable("Sas_T_CrudMigration", (string)null);
});
modelBuilder.Entity("Sozsoft.Platform.Entities.Currency", b => modelBuilder.Entity("Sozsoft.Platform.Entities.Currency", b =>
{ {
b.Property<string>("Id") b.Property<string>("Id")
@ -1685,171 +1624,6 @@ namespace Sozsoft.Platform.Migrations
b.ToTable("Sas_T_CustomEndpoint", (string)null); b.ToTable("Sas_T_CustomEndpoint", (string)null);
}); });
modelBuilder.Entity("Sozsoft.Platform.Entities.CustomEntity", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("CreationTime")
.HasColumnType("datetime2")
.HasColumnName("CreationTime");
b.Property<Guid?>("CreatorId")
.HasColumnType("uniqueidentifier")
.HasColumnName("CreatorId");
b.Property<Guid?>("DeleterId")
.HasColumnType("uniqueidentifier")
.HasColumnName("DeleterId");
b.Property<DateTime?>("DeletionTime")
.HasColumnType("datetime2")
.HasColumnName("DeletionTime");
b.Property<string>("Description")
.HasMaxLength(512)
.HasColumnType("nvarchar(512)");
b.Property<string>("DisplayName")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("EndpointStatus")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<bool>("IsActive")
.HasColumnType("bit");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false)
.HasColumnName("IsDeleted");
b.Property<bool>("IsFullAuditedEntity")
.HasColumnType("bit");
b.Property<bool>("IsMultiTenant")
.HasColumnType("bit");
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2")
.HasColumnName("LastModificationTime");
b.Property<Guid?>("LastModifierId")
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<string>("Menu")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<Guid?>("MigrationId")
.HasColumnType("uniqueidentifier");
b.Property<string>("MigrationStatus")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("TableName")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<Guid?>("TenantId")
.HasColumnType("uniqueidentifier")
.HasColumnName("TenantId");
b.HasKey("Id");
b.ToTable("Sas_T_CustomEntity", (string)null);
});
modelBuilder.Entity("Sozsoft.Platform.Entities.CustomEntityField", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("CreationTime")
.HasColumnType("datetime2")
.HasColumnName("CreationTime");
b.Property<Guid?>("CreatorId")
.HasColumnType("uniqueidentifier")
.HasColumnName("CreatorId");
b.Property<string>("DefaultValue")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<Guid?>("DeleterId")
.HasColumnType("uniqueidentifier")
.HasColumnName("DeleterId");
b.Property<DateTime?>("DeletionTime")
.HasColumnType("datetime2")
.HasColumnName("DeletionTime");
b.Property<string>("Description")
.HasMaxLength(512)
.HasColumnType("nvarchar(512)");
b.Property<int>("DisplayOrder")
.HasColumnType("int");
b.Property<Guid>("EntityId")
.HasColumnType("uniqueidentifier");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false)
.HasColumnName("IsDeleted");
b.Property<bool>("IsRequired")
.HasColumnType("bit");
b.Property<bool>("IsUnique")
.HasColumnType("bit");
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2")
.HasColumnName("LastModificationTime");
b.Property<Guid?>("LastModifierId")
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<int?>("MaxLength")
.HasColumnType("int");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.HasKey("Id");
b.HasIndex("EntityId");
b.ToTable("Sas_T_CustomEntityField", (string)null);
});
modelBuilder.Entity("Sozsoft.Platform.Entities.DataSource", b => modelBuilder.Entity("Sozsoft.Platform.Entities.DataSource", b =>
{ {
b.Property<Guid>("Id") b.Property<Guid>("Id")
@ -3998,6 +3772,163 @@ namespace Sozsoft.Platform.Migrations
b.ToTable("Adm_T_SkillType", (string)null); b.ToTable("Adm_T_SkillType", (string)null);
}); });
modelBuilder.Entity("Sozsoft.Platform.Entities.SqlTable", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("CreationTime")
.HasColumnType("datetime2")
.HasColumnName("CreationTime");
b.Property<Guid?>("CreatorId")
.HasColumnType("uniqueidentifier")
.HasColumnName("CreatorId");
b.Property<Guid?>("DeleterId")
.HasColumnType("uniqueidentifier")
.HasColumnName("DeleterId");
b.Property<DateTime?>("DeletionTime")
.HasColumnType("datetime2")
.HasColumnName("DeletionTime");
b.Property<string>("Description")
.HasMaxLength(512)
.HasColumnType("nvarchar(512)");
b.Property<string>("DisplayName")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("EndpointStatus")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.Property<bool>("IsActive")
.HasColumnType("bit");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false)
.HasColumnName("IsDeleted");
b.Property<bool>("IsFullAuditedEntity")
.HasColumnType("bit");
b.Property<bool>("IsMultiTenant")
.HasColumnType("bit");
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2")
.HasColumnName("LastModificationTime");
b.Property<Guid?>("LastModifierId")
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<string>("Menu")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("TableName")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<Guid?>("TenantId")
.HasColumnType("uniqueidentifier")
.HasColumnName("TenantId");
b.HasKey("Id");
b.ToTable("Sas_T_SqlTable", (string)null);
});
modelBuilder.Entity("Sozsoft.Platform.Entities.SqlTableField", b =>
{
b.Property<Guid>("Id")
.HasColumnType("uniqueidentifier");
b.Property<DateTime>("CreationTime")
.HasColumnType("datetime2")
.HasColumnName("CreationTime");
b.Property<Guid?>("CreatorId")
.HasColumnType("uniqueidentifier")
.HasColumnName("CreatorId");
b.Property<string>("DefaultValue")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<Guid?>("DeleterId")
.HasColumnType("uniqueidentifier")
.HasColumnName("DeleterId");
b.Property<DateTime?>("DeletionTime")
.HasColumnType("datetime2")
.HasColumnName("DeletionTime");
b.Property<string>("Description")
.HasMaxLength(512)
.HasColumnType("nvarchar(512)");
b.Property<int>("DisplayOrder")
.HasColumnType("int");
b.Property<Guid>("EntityId")
.HasColumnType("uniqueidentifier");
b.Property<bool>("IsDeleted")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false)
.HasColumnName("IsDeleted");
b.Property<bool>("IsRequired")
.HasColumnType("bit");
b.Property<bool>("IsUnique")
.HasColumnType("bit");
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2")
.HasColumnName("LastModificationTime");
b.Property<Guid?>("LastModifierId")
.HasColumnType("uniqueidentifier")
.HasColumnName("LastModifierId");
b.Property<int?>("MaxLength")
.HasColumnType("int");
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(128)
.HasColumnType("nvarchar(128)");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(64)
.HasColumnType("nvarchar(64)");
b.HasKey("Id");
b.HasIndex("EntityId");
b.ToTable("Sas_T_SqlTableField", (string)null);
});
modelBuilder.Entity("Sozsoft.Platform.Entities.Uom", b => modelBuilder.Entity("Sozsoft.Platform.Entities.Uom", b =>
{ {
b.Property<string>("Id") b.Property<string>("Id")
@ -4660,7 +4591,7 @@ namespace Sozsoft.Platform.Migrations
b.HasIndex("SchemaName", "FunctionName"); b.HasIndex("SchemaName", "FunctionName");
b.ToTable("Sqm_T_SqlFunction", (string)null); b.ToTable("Sas_T_SqlFunction", (string)null);
}); });
modelBuilder.Entity("Sozsoft.SqlQueryManager.Domain.Entities.SqlQuery", b => modelBuilder.Entity("Sozsoft.SqlQueryManager.Domain.Entities.SqlQuery", b =>
@ -4760,7 +4691,7 @@ namespace Sozsoft.Platform.Migrations
b.HasIndex("Status"); b.HasIndex("Status");
b.ToTable("Sqm_T_SqlQuery", (string)null); b.ToTable("Sas_T_SqlQuery", (string)null);
}); });
modelBuilder.Entity("Sozsoft.SqlQueryManager.Domain.Entities.SqlStoredProcedure", b => modelBuilder.Entity("Sozsoft.SqlQueryManager.Domain.Entities.SqlStoredProcedure", b =>
@ -4858,7 +4789,7 @@ namespace Sozsoft.Platform.Migrations
b.HasIndex("SchemaName", "ProcedureName"); b.HasIndex("SchemaName", "ProcedureName");
b.ToTable("Sqm_T_SqlStoredProcedure", (string)null); b.ToTable("Sas_T_SqlStoredProcedure", (string)null);
}); });
modelBuilder.Entity("Sozsoft.SqlQueryManager.Domain.Entities.SqlView", b => modelBuilder.Entity("Sozsoft.SqlQueryManager.Domain.Entities.SqlView", b =>
@ -4955,7 +4886,7 @@ namespace Sozsoft.Platform.Migrations
b.HasIndex("SchemaName", "ViewName"); b.HasIndex("SchemaName", "ViewName");
b.ToTable("Sqm_T_SqlView", (string)null); b.ToTable("Sas_T_SqlView", (string)null);
}); });
modelBuilder.Entity("Volo.Abp.AuditLogging.AuditLog", b => modelBuilder.Entity("Volo.Abp.AuditLogging.AuditLog", b =>
@ -6876,7 +6807,7 @@ namespace Sozsoft.Platform.Migrations
modelBuilder.Entity("Sozsoft.Platform.Entities.CrudEndpoint", b => modelBuilder.Entity("Sozsoft.Platform.Entities.CrudEndpoint", b =>
{ {
b.HasOne("Sozsoft.Platform.Entities.CustomEntity", "Entity") b.HasOne("Sozsoft.Platform.Entities.SqlTable", "Entity")
.WithMany() .WithMany()
.HasForeignKey("EntityId") .HasForeignKey("EntityId")
.OnDelete(DeleteBehavior.Cascade) .OnDelete(DeleteBehavior.Cascade)
@ -6885,28 +6816,6 @@ namespace Sozsoft.Platform.Migrations
b.Navigation("Entity"); b.Navigation("Entity");
}); });
modelBuilder.Entity("Sozsoft.Platform.Entities.CrudMigration", b =>
{
b.HasOne("Sozsoft.Platform.Entities.CustomEntity", "Entity")
.WithMany()
.HasForeignKey("EntityId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Entity");
});
modelBuilder.Entity("Sozsoft.Platform.Entities.CustomEntityField", b =>
{
b.HasOne("Sozsoft.Platform.Entities.CustomEntity", "Entity")
.WithMany("Fields")
.HasForeignKey("EntityId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Entity");
});
modelBuilder.Entity("Sozsoft.Platform.Entities.District", b => modelBuilder.Entity("Sozsoft.Platform.Entities.District", b =>
{ {
b.HasOne("Sozsoft.Platform.Entities.City", null) b.HasOne("Sozsoft.Platform.Entities.City", null)
@ -6997,6 +6906,17 @@ namespace Sozsoft.Platform.Migrations
b.Navigation("SkillType"); b.Navigation("SkillType");
}); });
modelBuilder.Entity("Sozsoft.Platform.Entities.SqlTableField", b =>
{
b.HasOne("Sozsoft.Platform.Entities.SqlTable", "Entity")
.WithMany("Fields")
.HasForeignKey("EntityId")
.OnDelete(DeleteBehavior.Cascade)
.IsRequired();
b.Navigation("Entity");
});
modelBuilder.Entity("Sozsoft.Platform.Entities.Uom", b => modelBuilder.Entity("Sozsoft.Platform.Entities.Uom", b =>
{ {
b.HasOne("Sozsoft.Platform.Entities.UomCategory", "UomCategory") b.HasOne("Sozsoft.Platform.Entities.UomCategory", "UomCategory")
@ -7219,11 +7139,6 @@ namespace Sozsoft.Platform.Migrations
b.Navigation("Cities"); b.Navigation("Cities");
}); });
modelBuilder.Entity("Sozsoft.Platform.Entities.CustomEntity", b =>
{
b.Navigation("Fields");
});
modelBuilder.Entity("Sozsoft.Platform.Entities.Order", b => modelBuilder.Entity("Sozsoft.Platform.Entities.Order", b =>
{ {
b.Navigation("Items"); b.Navigation("Items");
@ -7241,6 +7156,11 @@ namespace Sozsoft.Platform.Migrations
b.Navigation("Skills"); b.Navigation("Skills");
}); });
modelBuilder.Entity("Sozsoft.Platform.Entities.SqlTable", b =>
{
b.Navigation("Fields");
});
modelBuilder.Entity("Sozsoft.Platform.Entities.UomCategory", b => modelBuilder.Entity("Sozsoft.Platform.Entities.UomCategory", b =>
{ {
b.Navigation("Uoms"); b.Navigation("Uoms");

View file

@ -95,8 +95,8 @@
"dependencies": [] "dependencies": []
}, },
{ {
"name": "ProductListComponent", "name": "RoleListComponent",
"code": "const ProductListComponent = ({\n title = \"Product\"\n}) => {\n return (\n <DynamicEntityComponent id=\"c_mdljvvmq_fno52v\" title={title} />\n );\n};\n\nexport default ProductListComponent;", "code": "const RoleListComponent = ({\n title = \"AbpRoles\"\n}) => {\n return (\n <DynamicEntityComponent id=\"c_mdljvvmq_fno52v\" title={title} />\n );\n};\n\nexport default RoleListComponent;",
"props": null, "props": null,
"description": null, "description": null,
"isActive": true, "isActive": true,

View file

@ -1,790 +0,0 @@
import React, { useState } from 'react'
import { useEntities } from '../../contexts/EntityContext'
import axios from 'axios'
import {
FaBook,
FaSearch,
FaFilter,
FaGlobe,
FaCopy,
FaCheckCircle,
FaExclamationCircle,
FaDatabase,
FaSyncAlt,
FaPaperPlane,
FaPlusCircle,
FaEdit,
FaTrash,
} from 'react-icons/fa'
import { useLocalization } from '@/utils/hooks/useLocalization'
interface EndpointType {
id: string
name: string
method: string
path: string
description?: string
type: 'generated'
operationType?: string
entityName?: string
}
interface TestResult {
success: boolean
status: number
data?: unknown
error?: unknown
timestamp: string
}
interface ParameterInput {
name: string
value: string
type: 'path' | 'query' | 'body'
required: boolean
description?: string
}
const CrudEndpointManager: React.FC = () => {
const { generatedEndpoints } = useEntities()
const { translate } = useLocalization()
const [searchTerm, setSearchTerm] = useState('')
const [filterMethod, setFilterMethod] = useState<'all' | 'GET' | 'POST' | 'PUT' | 'DELETE'>('all')
const [selectedEndpoint, setSelectedEndpoint] = useState<string | null>(null)
const [testResults, setTestResults] = useState<Record<string, TestResult>>({})
const [loadingEndpoints, setLoadingEndpoints] = useState<Set<string>>(new Set())
const [parameterValues, setParameterValues] = useState<Record<string, Record<string, string>>>({})
const [requestBodies, setRequestBodies] = useState<Record<string, string>>({})
// Only show generated CRUD endpoints
const allEndpoints: EndpointType[] = [
...generatedEndpoints
.filter((e) => e.isActive)
.map((e) => ({
id: e.id,
name: `${e.entityName} ${e.operationType}`,
method: e.method,
path: e.path,
description: `${e.operationType} operation for ${e.entityName} entity`,
type: 'generated' as const,
operationType: e.operationType,
entityName: e.entityName,
})),
]
const filteredEndpoints = allEndpoints.filter((endpoint) => {
const matchesSearch =
endpoint.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
endpoint.path.toLowerCase().includes(searchTerm.toLowerCase()) ||
(endpoint.description || '').toLowerCase().includes(searchTerm.toLowerCase())
const matchesMethodFilter = filterMethod === 'all' || endpoint.method === filterMethod
return matchesSearch && matchesMethodFilter
})
const getMethodColor = (method: string) => {
switch (method) {
case 'GET':
return 'bg-blue-100 text-blue-800 border-blue-200'
case 'POST':
return 'bg-green-100 text-green-800 border-green-200'
case 'PUT':
return 'bg-yellow-100 text-yellow-800 border-yellow-200'
case 'DELETE':
return 'bg-red-100 text-red-800 border-red-200'
default:
return 'bg-slate-100 text-slate-800 border-slate-200'
}
}
const getResponseExample = (endpoint: EndpointType) => {
switch (endpoint.operationType) {
case 'GetList':
case 'GetPaged':
return {
data: [
{
id: '3fa85f64-5717-4562-b3fc-2c963f66afa6',
name: 'Sample Item',
creationTime: '2024-01-15T10:30:00Z',
},
],
totalCount: 1,
pageSize: 10,
currentPage: 1,
}
case 'GetById':
return {
id: '3fa85f64-5717-4562-b3fc-2c963f66afa6',
name: 'Sample Item',
creationTime: '2024-01-15T10:30:00Z',
lastModificationTime: '2024-01-15T10:30:00Z',
}
case 'Create':
case 'Update':
return {
id: '3fa85f64-5717-4562-b3fc-2c963f66afa6',
name: 'Sample Item',
creationTime: '2024-01-15T10:30:00Z',
lastModificationTime: '2024-01-15T10:30:00Z',
}
case 'Delete':
return {
success: true,
message: 'Item deleted successfully',
}
default:
return {
message: 'Hello from API!',
timestamp: '2024-01-15T10:30:00Z',
success: true,
}
}
}
const getRequestExample = (endpoint: EndpointType) => {
if (endpoint.operationType === 'Create' || endpoint.operationType === 'Update') {
return {
Name: 'New Item',
Description: 'Item description',
IsActive: true,
}
}
return null
}
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text)
}
// Get required parameters for each endpoint type
const getEndpointParameters = (endpoint: EndpointType): ParameterInput[] => {
const parameters: ParameterInput[] = []
const currentValues = parameterValues[endpoint.id] || {}
switch (endpoint.operationType) {
case 'GetById':
case 'Update':
case 'Delete':
parameters.push({
name: 'id',
value: currentValues.id || '3fa85f64-5717-4562-b3fc-2c963f66afa6',
type: 'path',
required: true,
description: 'Unique identifier for the entity',
})
break
case 'GetList':
case 'GetPaged':
parameters.push(
{
name: 'SkipCount',
value: currentValues.SkipCount || '0',
type: 'query',
required: false,
description: 'Number of records to skip',
},
{
name: 'MaxResultCount',
value: currentValues.MaxResultCount || '10',
type: 'query',
required: false,
description: 'Maximum number of records to return',
},
)
break
}
return parameters
}
// Check if endpoint needs request body
const needsRequestBody = (endpoint: EndpointType): boolean => {
return endpoint.operationType === 'Create' || endpoint.operationType === 'Update'
}
// Update parameter value
const updateParameterValue = (endpointId: string, paramName: string, value: string) => {
setParameterValues((prev) => ({
...prev,
[endpointId]: {
...prev[endpointId],
[paramName]: value,
},
}))
}
// Update request body
const updateRequestBody = (endpointId: string, body: string) => {
setRequestBodies((prev) => ({
...prev,
[endpointId]: body,
}))
}
// Initialize default values for endpoint when first opened
const initializeEndpointDefaults = (endpoint: EndpointType) => {
if (!parameterValues[endpoint.id]) {
const parameters = getEndpointParameters(endpoint)
const defaultValues: Record<string, string> = {}
parameters.forEach((param) => {
defaultValues[param.name] = param.value
})
setParameterValues((prev) => ({
...prev,
[endpoint.id]: defaultValues,
}))
}
if (!requestBodies[endpoint.id] && needsRequestBody(endpoint)) {
const example = getRequestExample(endpoint)
if (example) {
setRequestBodies((prev) => ({
...prev,
[endpoint.id]: JSON.stringify(example, null, 2),
}))
}
}
}
// Get current request body for endpoint
const getCurrentRequestBody = (endpoint: EndpointType): string => {
const stored = requestBodies[endpoint.id]
if (stored) return stored
const example = getRequestExample(endpoint)
return example ? JSON.stringify(example, null, 2) : ''
}
const testEndpoint = async (endpoint: EndpointType) => {
const endpointId = endpoint.id
// Add to loading set
setLoadingEndpoints((prev) => new Set(prev).add(endpointId))
try {
let url = ''
const method = endpoint.method
let data = null
// For generated endpoints, use the Crud API
url = `${import.meta.env.VITE_API_URL}/api/app/crudendpoint/${endpoint.entityName}`
// Get parameters and modify URL based on operation type
const parameters = getEndpointParameters(endpoint)
const pathParams = parameters.filter((p) => p.type === 'path')
const queryParams = parameters.filter((p) => p.type === 'query')
// Handle path parameters
if (pathParams.length > 0) {
const idParam = pathParams.find((p) => p.name === 'id')
if (idParam) {
url += `/${idParam.value}`
}
}
// Handle query parameters
if (queryParams.length > 0) {
const queryString = queryParams
.filter((p) => p.value.trim() !== '')
.map((p) => `${p.name}=${encodeURIComponent(p.value)}`)
.join('&')
if (queryString) {
url += `?${queryString}`
}
}
// Handle request body
if (needsRequestBody(endpoint)) {
const requestBodyText = getCurrentRequestBody(endpoint)
try {
data = requestBodyText ? JSON.parse(requestBodyText) : getRequestExample(endpoint)
} catch (e) {
throw new Error(`Invalid JSON in request body: ${e}`)
}
}
const config = {
method,
url,
timeout: 10000,
headers: {
'Content-Type': 'application/json',
},
data: data || undefined,
}
const response = await axios(config)
setTestResults((prev) => ({
...prev,
[endpointId]: {
success: true,
status: response.status,
data: response.data,
timestamp: new Date().toISOString(),
},
}))
} catch (error: unknown) {
const axiosError = error as {
response?: { status?: number; data?: unknown }
message?: string
}
setTestResults((prev) => ({
...prev,
[endpointId]: {
success: false,
status: axiosError.response?.status || 0,
error: axiosError.response?.data || axiosError.message,
timestamp: new Date().toISOString(),
},
}))
} finally {
// Remove from loading set
setLoadingEndpoints((prev) => {
const newSet = new Set(prev)
newSet.delete(endpointId)
return newSet
})
}
}
const stats = {
total: allEndpoints.length,
custom: 0, // No more custom endpoints
generated: generatedEndpoints.filter((e) => e.isActive).length,
byMethod: {
GET: allEndpoints.filter((e) => e.method === 'GET').length,
POST: allEndpoints.filter((e) => e.method === 'POST').length,
PUT: allEndpoints.filter((e) => e.method === 'PUT').length,
DELETE: allEndpoints.filter((e) => e.method === 'DELETE').length,
},
}
return (
<div className="space-y-4">
<div className="flex items-center justify-between mb-4">
<div>
<h1 className="text-2xl font-bold text-slate-900">
{translate('::App.DeveloperKit.CrudEndpoints')}
</h1>
<p className="text-slate-600">{translate('::App.DeveloperKit.Endpoint.Description')}</p>
</div>
<div className="flex items-center gap-3">
<div className="flex items-center gap-2 bg-blue-100 text-blue-700 px-3 py-1 rounded-full text-sm font-medium">
<FaGlobe className="w-4 h-4" />
{translate('::App.DeveloperKit.Endpoint.SwaggerCompatible')}
</div>
</div>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-5 gap-6 mb-4">
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-slate-600">
{translate('::App.DeveloperKit.Endpoint.GeneratedCrud')}
</p>
<p className="text-2xl font-bold text-slate-900 mt-1">{stats.generated}</p>
</div>
<div className="bg-emerald-100 text-emerald-600 p-3 rounded-lg">
<FaDatabase className="w-6 h-6" />
</div>
</div>
</div>
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-slate-600">
{translate('::App.DeveloperKit.Endpoint.GetCount')}
</p>
<p className="text-2xl font-bold text-slate-900 mt-1">{stats.byMethod.GET}</p>
</div>
<div className="bg-blue-100 text-blue-600 p-3 rounded-lg">
<FaBook className="w-6 h-6" />
</div>
</div>
</div>
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-slate-600">
{translate('::App.DeveloperKit.Endpoint.PostCount')}
</p>
<p className="text-2xl font-bold text-slate-900 mt-1">{stats.byMethod.POST}</p>
</div>
<div className="bg-purple-100 text-purple-600 p-3 rounded-lg">
<FaPlusCircle className="w-6 h-6" />
</div>
</div>
</div>
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-slate-600">
{translate('::App.DeveloperKit.Endpoint.PutCount')}
</p>
<p className="text-2xl font-bold text-slate-900 mt-1">{stats.byMethod.PUT}</p>
</div>
<div className="bg-purple-100 text-purple-600 p-3 rounded-lg">
<FaEdit className="w-6 h-6" />
</div>
</div>
</div>
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-slate-600">
{translate('::App.DeveloperKit.Endpoint.DeleteCount')}
</p>
<p className="text-2xl font-bold text-slate-900 mt-1">{stats.byMethod.DELETE}</p>
</div>
<div className="bg-purple-100 text-purple-600 p-3 rounded-lg">
<FaTrash className="w-6 h-6" />
</div>
</div>
</div>
</div>
{/* Filters */}
<div className="flex flex-col lg:flex-row gap-4">
<div className="flex-1 relative">
<FaSearch className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-slate-400" />
<input
type="text"
placeholder={translate('::App.DeveloperKit.Endpoint.SearchPlaceholder')}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-10 pr-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors"
/>
</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<FaFilter className="w-5 h-5 text-slate-500" />
<select
value={filterMethod}
onChange={(e) =>
setFilterMethod(e.target.value as 'all' | 'GET' | 'POST' | 'PUT' | 'DELETE')
}
className="px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors"
>
<option value="all">{translate('::App.DeveloperKit.Endpoint.AllMethods')}</option>
<option value="GET">GET</option>
<option value="POST">POST</option>
<option value="PUT">PUT</option>
<option value="DELETE">DELETE</option>
</select>
</div>
</div>
</div>
{/* Endpoints List */}
{filteredEndpoints.length > 0 ? (
<div className="grid grid-cols-1 xl:grid-cols-5 gap-6">
{filteredEndpoints.map((endpoint) => (
<div
key={endpoint.id}
className="bg-white rounded-lg border border-slate-200 shadow-sm"
>
<div
className="p-4 cursor-pointer hover:bg-slate-50 transition-colors"
onClick={() => {
const newSelectedEndpoint = selectedEndpoint === endpoint.id ? null : endpoint.id
setSelectedEndpoint(newSelectedEndpoint)
if (newSelectedEndpoint === endpoint.id) {
initializeEndpointDefaults(endpoint)
}
}}
>
<div className="flex items-center justify-between">
{/* Sol taraf */}
<div className="flex items-center gap-4">
<span
className={`px-4 py-1 text-sm font-medium rounded-full border ${getMethodColor(
endpoint.method,
)}`}
>
{endpoint.method}
</span>
<div>
<h3 className="text-lg font-semibold text-slate-900">{endpoint.name}</h3>
</div>
</div>
{/* Sağ taraf */}
<div className="flex items-center gap-3">
<div className="flex items-center gap-1">
<FaCheckCircle className="w-3 h-3 text-green-500" />
<span className="text-sm text-slate-500">
{translate('::App.Status.Active')}
</span>
</div>
</div>
</div>
<div className="mt-2">
<code className="text-sm bg-slate-100 text-slate-700 px-2 py-1 rounded">
{endpoint.path}
</code>
{endpoint.description && (
<p className="text-slate-600 text-sm mt-2">{endpoint.description}</p>
)}
</div>
</div>
{/* Expanded Details */}
{selectedEndpoint === endpoint.id && (
<div className="border-t border-slate-200 p-4 bg-slate-50">
<div className="grid grid-cols-1 gap-6">
{/* Request Details */}
<div>
<h4 className="font-semibold text-slate-900 mb-3">
{translate('::App.DeveloperKit.Endpoint.RequestTitle')}
</h4>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">
{translate('::App.DeveloperKit.Endpoint.UrlLabel')}
</label>
<div className="flex items-center gap-2">
<code className="flex-1 bg-white border border-slate-300 rounded px-3 py-2 text-sm">
{endpoint.method} {endpoint.path}
</code>
<button
onClick={() => copyToClipboard(`${endpoint.method} ${endpoint.path}`)}
className="p-2 text-slate-600 hover:text-slate-900 transition-colors"
title={translate('::App.DeveloperKit.Endpoint.CopyUrl')}
>
<FaCopy className="w-4 h-4" />
</button>
</div>
</div>
{/* Parameters Section */}
{getEndpointParameters(endpoint).length > 0 && (
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">
{translate('::App.DeveloperKit.Endpoint.ParametersLabel')}
</label>
<div className="space-y-3">
{getEndpointParameters(endpoint).map((param) => (
<div
key={param.name}
className="bg-white border border-slate-300 rounded p-3"
>
<div className="flex items-center gap-2 mb-2">
<span className="text-sm font-medium text-slate-900">
{param.name}
</span>
<span
className={`text-xs px-2 py-1 rounded ${
param.type === 'path'
? 'bg-blue-100 text-blue-700'
: 'bg-green-100 text-green-700'
}`}
>
{param.type}
</span>
{param.required && (
<span className="text-xs px-2 py-1 rounded bg-red-100 text-red-700">
{translate('::App.Required')}
</span>
)}
</div>
{param.description && (
<p className="text-xs text-slate-600 mb-2">
{param.description}
</p>
)}
<input
type="text"
value={param.value}
onChange={(e) =>
updateParameterValue(endpoint.id, param.name, e.target.value)
}
placeholder={`Enter ${param.name}`}
className="w-full px-3 py-2 text-sm border border-slate-300 rounded focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
))}
</div>
</div>
)}
{/* Request Body Section */}
{needsRequestBody(endpoint) && (
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">
{translate('::App.DeveloperKit.Endpoint.RequestBodyLabel')}
</label>
<div className="relative">
<textarea
value={getCurrentRequestBody(endpoint)}
onChange={(e) => updateRequestBody(endpoint.id, e.target.value)}
placeholder={translate(
'::App.DeveloperKit.Endpoint.RequestBodyPlaceholder',
)}
rows={8}
className="w-full px-3 py-2 text-sm border border-slate-300 rounded font-mono focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
<button
onClick={() => copyToClipboard(getCurrentRequestBody(endpoint))}
className="absolute top-2 right-2 p-1 text-slate-600 hover:text-slate-900 transition-colors"
title={translate('::App.DeveloperKit.Endpoint.CopyRequestBody')}
>
<FaCopy className="w-3 h-3" />
</button>
</div>
</div>
)}
</div>
</div>
{/* Response Details */}
<div>
<h4 className="font-semibold text-slate-900 mb-3">Response</h4>
<div className="space-y-3">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">
{translate('::App.DeveloperKit.Endpoint.ResponseSuccessLabel')}
</label>
<div className="relative">
<pre className="bg-white border border-slate-300 rounded p-3 text-sm overflow-x-auto">
{JSON.stringify(getResponseExample(endpoint), null, 2)}
</pre>
<button
onClick={() =>
copyToClipboard(
JSON.stringify(getResponseExample(endpoint), null, 2),
)
}
className="absolute top-2 right-2 p-1 text-slate-600 hover:text-slate-900 transition-colors"
title={translate('::App.DeveloperKit.Endpoint.CopyResponse')}
>
<FaCopy className="w-3 h-3" />
</button>
</div>
</div>
{/* Test Section */}
<div className="bg-white border border-slate-300 rounded p-4">
<h5 className="font-medium text-slate-900 mb-3">
{translate('::App.DeveloperKit.Endpoint.TestSectionTitle')}
</h5>
<div className="flex gap-2">
<button
onClick={() => testEndpoint(endpoint)}
disabled={loadingEndpoints.has(endpoint.id)}
className="flex items-center gap-2 bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 disabled:bg-blue-400 disabled:cursor-not-allowed transition-colors text-sm font-medium"
>
{loadingEndpoints.has(endpoint.id) ? (
<FaSyncAlt className="w-4 h-4 animate-spin" />
) : (
<FaPaperPlane className="w-4 h-4" />
)}
{loadingEndpoints.has(endpoint.id)
? translate('::App.DeveloperKit.Endpoint.SendLoading')
: translate('::App.DeveloperKit.Endpoint.SendRequest')}
</button>
{testResults[endpoint.id] && (
<button
onClick={() =>
setTestResults((prev) => {
const newResults = { ...prev }
delete newResults[endpoint.id]
return newResults
})
}
className="px-3 py-2 text-sm text-slate-600 hover:text-slate-900 transition-colors"
>
{translate('::App.DeveloperKit.Endpoint.ClearResult')}
</button>
)}
</div>
</div>
</div>
</div>
</div>
{/* Test Results */}
{testResults[endpoint.id] && (
<div className="mt-6 p-4 bg-slate-100 rounded-lg">
<div className="flex items-center gap-2 mb-3">
{testResults[endpoint.id].success ? (
<FaCheckCircle className="w-5 h-5 text-green-500" />
) : (
<FaExclamationCircle className="w-5 h-5 text-red-500" />
)}
<h5 className="font-semibold text-slate-900">
{translate('::App.DeveloperKit.Endpoint.TestResultLabel')} (
{testResults[endpoint.id].status})
</h5>
<span className="text-xs text-slate-500">
{new Date(testResults[endpoint.id].timestamp).toLocaleTimeString()}
</span>
</div>
<div className="relative">
<pre className="bg-white border border-slate-300 rounded p-3 text-sm overflow-x-auto max-h-96">
{JSON.stringify(
testResults[endpoint.id].success
? testResults[endpoint.id].data
: testResults[endpoint.id].error,
null,
2,
)}
</pre>
<button
onClick={() =>
copyToClipboard(
JSON.stringify(
testResults[endpoint.id].success
? testResults[endpoint.id].data
: testResults[endpoint.id].error,
null,
2,
),
)
}
className="absolute top-2 right-2 p-1 text-slate-600 hover:text-slate-900 transition-colors"
title={translate('::App.DeveloperKit.Endpoint.CopyResult')}
>
<FaCopy className="w-3 h-3" />
</button>
</div>
</div>
)}
</div>
)}
</div>
))}
</div>
) : (
<div className="text-center py-12">
<div className="max-w-md mx-auto">
<div className="bg-slate-100 rounded-full w-16 h-16 flex items-center justify-center mx-auto mb-4">
<FaBook className="w-8 h-8 text-slate-500" />
</div>
<h3 className="text-lg font-medium text-slate-900 mb-2">
{searchTerm || filterMethod !== 'all'
? translate('::App.DeveloperKit.Endpoint.EmptyFilteredTitle')
: translate('::App.DeveloperKit.Endpoint.EmptyInitialTitle')}
</h3>
<p className="text-slate-600">
{searchTerm || filterMethod !== 'all'
? translate('::App.DeveloperKit.EmptyFilteredDescription')
: translate('::App.DeveloperKit.Endpoint.EmptyInitialDescription')}
</p>
</div>
</div>
)}
</div>
)
}
export default CrudEndpointManager

View file

@ -1,529 +0,0 @@
import React from 'react'
import { Link } from 'react-router-dom'
import { useComponents } from '../../contexts/ComponentContext'
import { useEntities } from '../../contexts/EntityContext'
import { useSystemHealth } from '../../utils/hooks/useDeveloperKit'
import {
FaDatabase,
FaBolt,
FaServer,
FaPuzzlePiece,
FaCog,
FaChartLine,
FaCode,
FaCheckCircle,
FaArrowRight,
FaExclamationCircle,
FaWifi,
FaWindowClose,
} from 'react-icons/fa'
import { ROUTES_ENUM } from '@/routes/route.constant'
import { useLocalization } from '@/utils/hooks/useLocalization'
const Dashboard: React.FC = () => {
const { components } = useComponents()
const { entities, migrations, generatedEndpoints } = useEntities()
const { isOnline, lastCheck, recheckHealth } = useSystemHealth()
const { translate } = useLocalization()
const stats = [
{
name: translate('::App.DeveloperKit.Dashboard.Stats.Entities'),
value: entities.filter((e) => e.isActive).length,
total: entities.length,
icon: FaDatabase,
color: 'text-blue-600',
bgColor: 'bg-blue-100',
href: ROUTES_ENUM.protected.saas.developerKit.entities,
},
{
name: translate('::App.DeveloperKit.Dashboard.Stats.Migrations'),
value: migrations.filter((m) => m.status === 'pending').length,
total: migrations.length,
icon: FaBolt,
color: 'text-yellow-600',
bgColor: 'bg-yellow-100',
href: ROUTES_ENUM.protected.saas.developerKit.migrations,
},
{
name: translate('::App.DeveloperKit.Dashboard.Stats.Apis'),
value: generatedEndpoints.filter((e) => e.isActive).length,
total: generatedEndpoints.length,
icon: FaServer,
color: 'text-emerald-600',
bgColor: 'bg-emerald-100',
href: ROUTES_ENUM.protected.saas.developerKit.endpoints,
},
{
name: translate('::App.DeveloperKit.Components'),
value: components?.filter((c) => c.isActive).length,
total: components?.length,
icon: FaPuzzlePiece,
color: 'text-purple-600',
bgColor: 'bg-purple-100',
href: ROUTES_ENUM.protected.saas.developerKit.components,
},
]
const developmentFlow = [
{
step: 1,
title: translate('::App.DeveloperKit.Entity.CreateEntity'),
description: translate('::App.DeveloperKit.Dashboard.Flow.CreateEntity.Desc'),
icon: FaDatabase,
color: 'bg-blue-600',
href: ROUTES_ENUM.protected.saas.developerKit.entitiesNew,
status: 'ready',
},
{
step: 2,
title: translate('::App.DeveloperKit.Dashboard.Flow.GenerateMigration'),
description: translate('::App.DeveloperKit.Dashboard.Flow.GenerateMigration.Desc'),
icon: FaBolt,
color: 'bg-yellow-600',
href: ROUTES_ENUM.protected.saas.developerKit.migrations,
status: entities.some((e) => e.migrationStatus === 'pending') ? 'action-needed' : 'ready',
},
{
step: 3,
title: translate('::App.DeveloperKit.Dashboard.Flow.ApplyMigration'),
description: translate('::App.DeveloperKit.Dashboard.Flow.ApplyMigration.Desc'),
icon: FaCheckCircle,
color: 'bg-green-600',
href: ROUTES_ENUM.protected.saas.developerKit.migrations,
status: migrations.some((m) => m.status === 'pending') ? 'action-needed' : 'ready',
},
{
step: 4,
title: translate('::App.DeveloperKit.Dashboard.Flow.GenerateApi'),
description: translate('::App.DeveloperKit.Dashboard.Flow.GenerateApi.Desc'),
icon: FaServer,
color: 'bg-emerald-600',
href: ROUTES_ENUM.protected.saas.developerKit.endpoints,
status: 'ready',
},
{
step: 5,
title: translate('::App.DeveloperKit.Dashboard.Flow.BuildComponent'),
description: translate('::App.DeveloperKit.Dashboard.Flow.BuildComponent.Desc'),
icon: FaPuzzlePiece,
color: 'bg-purple-600',
href: ROUTES_ENUM.protected.saas.developerKit.componentsNew,
status: 'ready',
},
]
const recentEntities = entities.slice(0, 3)
const recentMigrations = migrations.slice(0, 3)
const recentEndpoints = [
...generatedEndpoints.map((e) => ({
id: e.id,
name: `${e.entityName} ${e.operationType}`,
method: e.method,
path: e.path,
description: `Generated ${e.operationType} for ${e.entityName}`,
isActive: e.isActive,
lastModificationTime: e.lastModificationTime,
creationTime: e.creationTime,
})),
].slice(0, 3)
const systemHealth = [
{
name: translate('::App.DeveloperKit.Dashboard.SystemHealth.Frontend'),
status: translate('::App.DeveloperKit.Dashboard.SystemHealth.Healthy'),
icon: FaCode,
},
{
name: translate('::App.DeveloperKit.Dashboard.SystemHealth.Backend'),
status: isOnline
? translate('::App.DeveloperKit.Dashboard.SystemHealth.Healthy')
: translate('::App.DeveloperKit.Dashboard.SystemHealth.Offline'),
icon: FaServer,
},
{
name: translate('::ListForms.ListFormEdit.TabDatabase'),
status: isOnline
? translate('::App.DeveloperKit.Dashboard.SystemHealth.Healthy')
: translate('::App.DeveloperKit.Dashboard.SystemHealth.Unknown'),
icon: FaDatabase,
},
{
name: translate('::App.DeveloperKit.Dashboard.SystemHealth.Migrations'),
status: migrations.some((m) => m.status === 'failed')
? translate('::App.DeveloperKit.Dashboard.SystemHealth.Warning')
: translate('::App.DeveloperKit.Dashboard.SystemHealth.Healthy'),
icon: FaBolt,
},
]
return (
<div className="space-y-4">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-slate-900">
{translate('::App.Coordinator.Classroom.Dashboard')}
</h1>
<p className="text-slate-600">{translate('::App.DeveloperKit.Dashboard.Description')}</p>
</div>
<div className="flex items-center gap-3">
<button
onClick={recheckHealth}
className={`flex items-center gap-2 px-3 py-1 rounded-full text-sm font-medium transition-colors duration-200 ${
isOnline
? 'bg-green-100 text-green-700 hover:bg-green-200'
: 'bg-red-100 text-red-700 hover:bg-red-200'
}`}
title={`Son kontrol: ${lastCheck.toLocaleTimeString()}`}
>
<div
className={`w-2 h-2 rounded-full ${
isOnline ? 'bg-green-500 animate-pulse' : 'bg-red-500 animate-pulse'
}`}
/>
{isOnline ? (
<>
<FaWifi className="w-4 h-4" />
{translate('::App.DeveloperKit.Dashboard.SystemHealth.Online')}
</>
) : (
<>
<FaWindowClose className="w-4 h-4" />
{translate('::App.DeveloperKit.Dashboard.SystemHealth.OfflineStatus')}
</>
)}
</button>
</div>
</div>
{/* Stats Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{stats.map((stat) => {
const Icon = stat.icon
return (
<Link
key={stat.name}
to={stat.href}
className="bg-white rounded-xl shadow-sm border border-slate-200 p-4 hover:shadow-md transition-all duration-200 group"
>
<div className="flex items-center justify-between">
<div
className={`${stat.bgColor} ${stat.color} p-3 rounded-lg group-hover:scale-110 transition-transform`}
>
<Icon className="w-6 h-6" />
</div>
<FaArrowRight className="w-4 h-4 text-slate-400 group-hover:text-slate-600 transition-colors" />
</div>
<div>
<p className="text-sm font-medium text-slate-600">{stat.name}</p>
<p className="text-2xl font-bold text-slate-900 mt-1">
{stat.value}
<span className="text-sm font-normal text-slate-500 ml-1">/ {stat.total}</span>
</p>
</div>
</Link>
)
})}
</div>
{/* Development Flow */}
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-4">
<div className="flex items-center gap-2 mb-4">
<FaCog className="w-6 h-6 text-blue-600" />
<h2 className="text-xl font-semibold text-slate-900">
{translate('::App.DeveloperKit.Dashboard.Flow.Title')}
</h2>
</div>
<div className="grid grid-cols-1 lg:grid-cols-5 gap-8">
{developmentFlow.map((flow, index) => {
const Icon = flow.icon
return (
<Link key={flow.step} to={flow.href} className="group relative">
<div
className={`${flow.color} text-white p-4 rounded-lg transition-all duration-200 transform group-hover:scale-105`}
>
<div className="flex items-center justify-between mb-4">
<div className="bg-white bg-opacity-20 rounded-lg p-2">
<Icon className="w-6 h-6" />
</div>
<div className="bg-white bg-opacity-20 rounded-full w-8 h-8 flex items-center justify-center">
<span className="text-sm font-bold">{flow.step}</span>
</div>
</div>
<h3 className="font-semibold text-lg mb-2 text-white">{flow.title}</h3>
<p className="text-xs opacity-90">{flow.description}</p>
{flow.status === 'action-needed' && (
<div className="absolute -top-2 -right-2">
<div className="bg-red-500 text-white rounded-full w-6 h-6 flex items-center justify-center">
<FaExclamationCircle className="w-4 h-4" />
</div>
</div>
)}
</div>
{index < developmentFlow.length - 1 && (
<div className="hidden lg:block absolute top-1/2 -right-3 transform -translate-y-1/2">
<FaArrowRight className="w-6 h-6 text-slate-300" />
</div>
)}
</Link>
)
})}
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-4 gap-4">
{/* System Health */}
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-4">
<div className="flex items-center gap-2 mb-4">
<FaCog className="w-5 h-5 text-green-500" />
<h3 className="text-lg font-semibold text-slate-900">
{translate('::App.DeveloperKit.Dashboard.SystemHealth.Title')}
</h3>
</div>
<div className="space-y-3">
{systemHealth.map((system) => {
const Icon = system.icon
return (
<div
key={system.name}
className="flex items-center justify-between p-3 bg-slate-50 rounded-lg"
>
<div className="flex items-center gap-3">
<Icon className="w-4 h-4 text-slate-600" />
<span className="font-medium text-slate-900">{system.name}</span>
</div>
<div className="flex items-center gap-2">
<div
className={`w-2 h-2 rounded-full ${
system.status === 'healthy'
? 'bg-green-500'
: system.status === 'warning'
? 'bg-yellow-500'
: system.status === 'offline'
? 'bg-red-500'
: 'bg-gray-500'
}`}
/>
<span
className={`text-sm capitalize ${
system.status === 'healthy'
? 'text-green-600'
: system.status === 'warning'
? 'text-yellow-600'
: system.status === 'offline'
? 'text-red-600'
: 'text-gray-600'
}`}
>
{system.status === 'offline' ? 'Offline' : system.status}
</span>
</div>
</div>
)
})}
</div>
</div>
{/* Recent Entities */}
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-slate-900">
{translate('::App.DeveloperKit.Dashboard.RecentEntities.Title')}
</h3>
<Link
to={ROUTES_ENUM.protected.saas.developerKit.entities}
className="text-blue-600 hover:text-blue-700 text-sm font-medium"
>
{translate('::App.DeveloperKit.Dashboard.ViewAll')}
</Link>
</div>
<div className="space-y-3">
{recentEntities.length > 0 ? (
recentEntities.map((entity) => (
<div
key={entity.id}
className="flex items-center justify-between p-3 bg-slate-50 rounded-lg hover:bg-slate-100 transition-colors"
>
<div>
<p className="font-medium text-slate-900">{entity.displayName}</p>
<p className="text-sm text-slate-500">
{entity.fields.length} fields {entity.migrationStatus}
</p>
</div>
<div className="flex items-center gap-2">
<div
className={`w-2 h-2 rounded-full ${
entity.migrationStatus === 'applied'
? 'bg-green-500'
: entity.migrationStatus === 'pending'
? 'bg-yellow-500'
: 'bg-red-500'
}`}
/>
<Link
to={ROUTES_ENUM.protected.saas.developerKit.entitiesEdit.replace(
':id',
entity.id,
)}
className="text-blue-600 hover:text-blue-700"
>
<FaDatabase className="w-4 h-4" />
</Link>
</div>
</div>
))
) : (
<div className="text-center py-8">
<FaDatabase className="w-12 h-12 text-slate-300 mx-auto mb-2" />
<p className="text-slate-500 mb-2">
{translate('::App.DeveloperKit.Dashboard.Empty.Entity')}
</p>
<Link
to={ROUTES_ENUM.protected.saas.developerKit.entitiesNew}
className="text-blue-600 hover:text-blue-700 text-sm font-medium"
>
Create your first entity
</Link>
</div>
)}
</div>
</div>
{/* Recent Migrations */}
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-slate-900">
{translate('::App.DeveloperKit.Dashboard.RecentMigrations.Title')}
</h3>
<Link
to={ROUTES_ENUM.protected.saas.developerKit.migrations}
className="text-yellow-600 hover:text-yellow-700 text-sm font-medium"
>
{translate('::App.DeveloperKit.Dashboard.ViewAll')}
</Link>
</div>
<div className="space-y-3">
{recentMigrations.length > 0 ? (
recentMigrations.map((migration) => (
<div
key={migration.id}
className="flex items-center justify-between p-3 bg-slate-50 rounded-lg hover:bg-slate-100 transition-colors"
>
<div>
<p className="font-medium text-slate-900">{migration.entityName}</p>
<p className="text-sm text-slate-500">
{migration.status} {new Date(migration.creationTime).toLocaleDateString()}
</p>
</div>
<div className="flex items-center gap-2">
<div
className={`w-2 h-2 rounded-full ${
migration.status === 'applied'
? 'bg-green-500'
: migration.status === 'pending'
? 'bg-yellow-500'
: 'bg-red-500'
}`}
/>
<Link
to={ROUTES_ENUM.protected.saas.developerKit.migrations}
className="text-yellow-600 hover:text-yellow-700"
>
<FaBolt className="w-4 h-4" />
</Link>
</div>
</div>
))
) : (
<div className="text-center py-8">
<FaBolt className="w-12 h-12 text-slate-300 mx-auto mb-2" />
<p className="text-slate-500 mb-2">
{translate('::App.DeveloperKit.Dashboard.Empty.Migration')}
</p>
<Link
to={ROUTES_ENUM.protected.saas.developerKit.entitiesNew}
className="text-yellow-600 hover:text-yellow-700 text-sm font-medium"
>
{translate('::App.DeveloperKit.Dashboard.Action.GenerateMigrations')}
</Link>
</div>
)}
</div>
</div>
{/* Recent API Endpoints */}
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-slate-900">
{translate('::App.DeveloperKit.Dashboard.RecentEndpoints.Title')}
</h3>
<Link
to={ROUTES_ENUM.protected.saas.developerKit.endpoints}
className="text-emerald-600 hover:text-emerald-700 text-sm font-medium"
>
{translate('::App.DeveloperKit.Dashboard.ViewAll')}
</Link>
</div>
<div className="space-y-3">
{recentEndpoints.length > 0 ? (
recentEndpoints.map((endpoint) => (
<div
key={endpoint.id}
className="flex items-center justify-between p-3 bg-slate-50 rounded-lg hover:bg-slate-100 transition-colors"
>
<div>
<div className="flex items-center gap-2 mb-1">
<span
className={`px-2 py-0.5 text-xs font-medium rounded ${
endpoint.method === 'GET'
? 'bg-blue-100 text-blue-800'
: endpoint.method === 'POST'
? 'bg-green-100 text-green-800'
: endpoint.method === 'PUT'
? 'bg-yellow-100 text-yellow-800'
: 'bg-red-100 text-red-800'
}`}
>
{endpoint.method}
</span>
<p className="font-medium text-slate-900 text-sm">{endpoint.name}</p>
</div>
<p className="text-xs text-slate-500">{endpoint.path}</p>
</div>
<div className="flex items-center gap-2">
<div
className={`w-2 h-2 rounded-full ${
endpoint.isActive ? 'bg-green-500' : 'bg-slate-300'
}`}
/>
<Link to="/docs" className="text-emerald-600 hover:text-emerald-700">
<FaServer className="w-4 h-4" />
</Link>
</div>
</div>
))
) : (
<div className="text-center py-8">
<FaServer className="w-12 h-12 text-slate-300 mx-auto mb-2" />
<p className="text-slate-500 mb-2">
{translate('::App.DeveloperKit.Dashboard.Empty.Endpoint')}
</p>
<Link
to={ROUTES_ENUM.protected.saas.developerKit.endpointsNew}
className="text-emerald-600 hover:text-emerald-700 text-sm font-medium"
>
{translate('::App.DeveloperKit.Dashboard.Action.CreateEntity')}
</Link>
</div>
)}
</div>
</div>
</div>
</div>
)
}
export default Dashboard

View file

@ -1,678 +0,0 @@
import React, { useState, useEffect, useMemo } from 'react'
import { useParams, useNavigate } from 'react-router-dom'
import { useEntities, EntityFieldType } from '../../contexts/EntityContext'
import {
FaSave,
FaArrowLeft,
FaPlus,
FaTrashAlt,
FaDatabase,
FaCog,
FaTable,
FaColumns,
} from 'react-icons/fa'
import { CustomEntityField } from '@/proxy/developerKit/models'
import { ROUTES_ENUM } from '@/routes/route.constant'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { Formik, Form, Field, FieldProps, FieldArray } from 'formik'
import * as Yup from 'yup'
import { FormItem, Input, Select, Checkbox, FormContainer, Button } from '@/components/ui'
import { SelectBoxOption } from '@/types/shared'
import { MenuService } from '@/services/menu.service'
import { MenuDto } from '@/proxy/menus/models'
// Validation schema
const validationSchema = Yup.object({
menu: Yup.string().required('Menu is required'),
name: Yup.string().required('Entity name is required'),
displayName: Yup.string().required('Display name is required'),
tableName: Yup.string().required('Table name is required'),
description: Yup.string(),
fields: Yup.array()
.of(
Yup.object({
name: Yup.string().required('Field name is required'),
type: Yup.string().required('Field type is required'),
isRequired: Yup.boolean(),
maxLength: Yup.number().nullable(),
isUnique: Yup.boolean(),
defaultValue: Yup.string().notRequired(),
description: Yup.string().notRequired(),
displayOrder: Yup.number().required(),
}),
)
.min(1, 'At least one field is required'),
isActive: Yup.boolean(),
isFullAuditedEntity: Yup.boolean(),
isMultiTenant: Yup.boolean(),
})
const EntityEditor: React.FC = () => {
const { id } = useParams()
const navigate = useNavigate()
const { translate } = useLocalization()
const { getEntity, addEntity, updateEntity } = useEntities()
const isEditing = !!id
// Menu options state
const [menuOptions, setMenuOptions] = useState<SelectBoxOption[]>([])
// Fetch menu options on mount
useEffect(() => {
const fetchMenuOptions = async () => {
try {
const menuService = new MenuService()
const response = await menuService.getListMainMenu()
if (response.data && Array.isArray(response.data)) {
const options = response.data.map((menuItem: any) => ({
value: menuItem.shortName,
label: translate('::' + menuItem.displayName),
}))
setMenuOptions(options)
}
} catch (error) {
console.error('Error fetching menu options:', error)
}
}
fetchMenuOptions()
}, [])
// Initial values for Formik
const [initialValues, setInitialValues] = useState({
menu: '',
name: '',
displayName: '',
tableName: '',
description: '',
fields: [
{
id: crypto.randomUUID(),
entityId: id || '',
name: 'Name',
type: 'string' as EntityFieldType,
isRequired: true,
maxLength: 100,
description: 'Entity name',
displayOrder: 1,
},
] as CustomEntityField[],
isActive: true,
isFullAuditedEntity: true,
isMultiTenant: true,
})
useEffect(() => {
if (isEditing && id) {
const entity = getEntity(id)
if (entity) {
// Ensure fields are sorted by displayOrder and normalized to sequential values
const sortedFields = (entity.fields || [])
.slice()
.sort((a, b) => (a.displayOrder ?? 0) - (b.displayOrder ?? 0))
.map((f, idx) => ({ ...f, displayOrder: f.displayOrder ?? idx + 1 }))
setInitialValues({
menu: entity.menu,
name: entity.name,
displayName: entity.displayName,
tableName: entity.tableName,
description: entity.description || '',
fields: sortedFields,
isActive: entity.isActive,
isFullAuditedEntity: entity.isFullAuditedEntity,
isMultiTenant: entity.isMultiTenant,
})
}
}
}, [id, isEditing, getEntity])
const handleSubmit = async (values: typeof initialValues, { setSubmitting }: any) => {
try {
const sanitizedFields = values.fields.map((f) => {
// send both `displayOrder` (frontend proxy) and `order` (backend DTO) to be safe
const sanitized: any = {
...(f.id && isEditing ? { id: f.id } : {}),
name: f.name.trim(),
type: f.type,
isRequired: f.isRequired,
maxLength: f.maxLength,
isUnique: f.isUnique || false,
defaultValue: f.defaultValue,
description: f.description,
displayOrder: f.displayOrder,
order: f.displayOrder,
}
return sanitized
})
const entityData = {
menu: values.menu.trim(),
name: values.name.trim(),
displayName: values.displayName.trim(),
tableName: values.tableName.trim(),
description: values.description.trim(),
fields: sanitizedFields,
isActive: values.isActive,
isFullAuditedEntity: values.isFullAuditedEntity,
isMultiTenant: values.isMultiTenant,
}
if (isEditing && id) {
await updateEntity(id, entityData)
} else {
await addEntity(entityData)
}
navigate(ROUTES_ENUM.protected.saas.developerKit.entities, { replace: true })
} catch (error) {
console.error('Error saving entity:', error)
alert('Failed to save entity. Please try again.')
} finally {
setSubmitting(false)
}
}
// Helper function to generate table name
const generateTableName = (menuValue: string, entityName: string): string => {
if (!menuValue || !entityName) return ''
const selectedMenu = menuOptions.find((opt) => opt.value === menuValue)
if (!selectedMenu) return ''
return `${selectedMenu.value}_D_${entityName}`
}
const fieldTypes = [
{ value: 'string', label: 'String' },
{ value: 'number', label: 'Number' },
{ value: 'decimal', label: 'Decimal' },
{ value: 'boolean', label: 'Boolean' },
{ value: 'date', label: 'Date' },
{ value: 'guid', label: 'Guid' },
]
return (
<Formik
enableReinitialize
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={handleSubmit}
>
{({ values, touched, errors, isSubmitting, setFieldValue, submitForm, isValid }) => (
<>
{/* Enhanced Header */}
<div className="bg-white shadow border-b border-slate-200 sticky top-0 z-10">
<div className="px-4 py-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<button
type="button"
onClick={() => navigate(ROUTES_ENUM.protected.saas.developerKit.entities)}
className="flex items-center gap-2 text-slate-600 hover:text-blue-600 hover:bg-blue-50 px-3 py-2 rounded-lg transition-all duration-200"
>
<FaArrowLeft className="w-4 h-4" />
<span className="text-sm">
{translate('::App.DeveloperKit.EntityEditor.Back')}
</span>
</button>
<div className="h-6 w-px bg-slate-300"></div>
<div className="flex items-center gap-3">
<div className="bg-gradient-to-r from-green-500 to-blue-600 p-1 rounded">
<FaDatabase className="w-5 h-5 text-white" />
</div>
<div>
<h1 className="text-xl font-bold text-slate-900">
{isEditing
? `${translate('::App.DeveloperKit.EntityEditor.Title.Edit')} - ${values.name || initialValues.name || 'Entity'}`
: translate('::App.DeveloperKit.Entity.CreateEntity')}
</h1>
<p className="text-sm text-slate-600">
{isEditing ? 'Modify your entity' : 'Create a new entity'}
</p>
</div>
</div>
</div>
{/* Save Button in Header */}
<div className="flex items-center gap-2">
<button
type="button"
onClick={submitForm}
disabled={isSubmitting || !isValid}
className={`
flex items-center gap-1
px-2 py-1.5 rounded text-sm transition-all duration-200
text-white
bg-gradient-to-r from-green-600 to-green-700
hover:from-green-700 hover:to-green-800
disabled:from-gray-400 disabled:to-gray-500
disabled:text-gray-200
disabled:cursor-not-allowed
disabled:hover:from-gray-400 disabled:hover:to-gray-500
`}
>
<FaSave className="w-3 h-3" />
{isSubmitting ? translate('::SavingWithThreeDot') : translate('::Save')}
</button>
</div>
</div>
</div>
</div>
<Form className="grid grid-cols-1 lg:grid-cols-4 gap-4 pt-2">
{/* Migration Status Info Banner */}
{isEditing && id && getEntity(id)?.migrationStatus === 'applied' && (
<div className="col-span-4 bg-yellow-50 border border-yellow-200 rounded-lg p-4">
<div className="flex items-start gap-3">
<div className="bg-yellow-100 p-2 rounded-lg">
<FaDatabase className="w-5 h-5 text-yellow-600" />
</div>
<div className="flex-1">
<h3 className="text-sm font-semibold text-yellow-900 mb-1">
Migration Applied - Changes Will Require New Migration
</h3>
<p className="text-xs text-yellow-700">
This entity has been migrated to the database. Any structural changes you make
will reset the migration status to "pending", and you'll need to generate and
apply a new migration to update the database schema.
</p>
</div>
</div>
</div>
)}
{/* Basic Entity Information */}
<div className="space-y-4 col-span-1">
<div className="bg-white rounded-lg shadow-sm border border-slate-200 p-3">
<div className="flex items-center gap-2 mb-4">
<div className="bg-blue-100 p-1.5 rounded-lg">
<FaCog className="w-4 h-4 text-blue-600" />
</div>
<h2 className="text-sm font-semibold text-slate-900">Entity Settings</h2>
</div>
<FormContainer size="sm">
<FormItem
label={translate('::App.DeveloperKit.EntityEditor.MenuName')}
invalid={!!(errors.menu && touched.menu)}
errorMessage={errors.menu as string}
>
<Field name="menu">
{({ field, form }: FieldProps<SelectBoxOption>) => (
<Select
field={field}
form={form}
options={menuOptions}
isClearable={true}
value={menuOptions.filter((option) => option.value === values.menu)}
onChange={(option) => {
const newMenuValue = option?.value || ''
form.setFieldValue(field.name, newMenuValue)
// Update table name when menu changes
if (values.name && newMenuValue) {
const newTableName = generateTableName(newMenuValue, values.name)
form.setFieldValue('tableName', newTableName)
}
}}
/>
)}
</Field>
</FormItem>
<FormItem
label={translate('::App.DeveloperKit.EntityEditor.EntityName')}
invalid={!!(errors.name && touched.name)}
errorMessage={errors.name as string}
>
<Field name="name">
{({ field }: FieldProps) => (
<Input
{...field}
onBlur={(e) => {
field.onBlur(e)
const entityName = e.target.value.trim()
// Update table name based on menu prefix and entity name
if (entityName && values.menu) {
const newTableName = generateTableName(values.menu, entityName)
setFieldValue('tableName', newTableName)
}
// Update display name if empty
if (entityName && !values.displayName) {
setFieldValue('displayName', entityName)
}
}}
placeholder="e.g., Product, User, Order"
className="px-2 py-1.5 bg-slate-50 focus:bg-white transition-all duration-200 text-sm h-7"
/>
)}
</Field>
</FormItem>
<FormItem
label={translate('::ListForms.ListFormEdit.DatabaseDataSourceTableName')}
invalid={!!(errors.tableName && touched.tableName)}
errorMessage={errors.tableName as string}
>
<Field
name="tableName"
component={Input}
placeholder="e.g., Adm_D_Product, Net_D_User"
disabled={true}
/>
<p className="text-xs text-slate-500 mt-1">
Format:{' '}
{values.menu
? `${menuOptions.find((opt) => opt.value === values.menu)?.value || '[Prefix]'}_D_`
: '[Prefix]_D_'}
EntityName
</p>
</FormItem>
<FormItem
label={translate('::Abp.Identity.OrganizationUnit.DisplayName')}
invalid={!!(errors.displayName && touched.displayName)}
errorMessage={errors.displayName as string}
>
<Field
name="displayName"
component={Input}
placeholder="Display name for UI (e.g., Product, User)"
/>
<p className="text-xs text-slate-500 mt-1">
User-friendly name shown in the interface
</p>
</FormItem>
<FormItem
label={translate('::ListForms.ListFormEdit.DetailsDescription')}
invalid={!!(errors.description && touched.description)}
errorMessage={errors.description as string}
>
<Field
name="description"
component={Input}
placeholder="Brief description of this entity"
textArea={true}
rows={2}
/>
</FormItem>
<FormItem label={translate('::App.Status.Active')}>
<Field name="isActive" component={Checkbox} />
</FormItem>
<FormItem label="Full Audited Entity">
<Field name="isFullAuditedEntity" component={Checkbox} />
<p className="text-xs text-slate-500 mt-1">
Includes CreationTime, CreatorId, LastModificationTime, LastModifierId,
IsDeleted, DeleterId, DeletionTime
</p>
</FormItem>
<FormItem label="Multi-Tenant">
<Field name="isMultiTenant" component={Checkbox} />
<p className="text-xs text-slate-500 mt-1">
Adds TenantId column for multi-tenancy support
</p>
</FormItem>
</FormContainer>
</div>
</div>
{/* Fields Section */}
<div className="space-y-4 col-span-3">
<div className="bg-white rounded-lg shadow-sm border border-slate-200 p-3">
<FormContainer size="sm">
<FieldArray name="fields">
{({ push, remove }) => (
<>
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-1">
<div className="bg-green-100 p-1.5 rounded">
<FaColumns className="w-4 h-4 text-green-600" />
</div>
<h2 className="text-sm font-semibold text-slate-900">
{translate('::App.DeveloperKit.EntityEditor.Fields')}
</h2>
</div>
<button
type="button"
onClick={() =>
push({
id: crypto.randomUUID(),
entityId: id || '',
name: '',
type: 'string',
isRequired: false,
description: '',
displayOrder: (values.fields?.length ?? 0) + 1,
})
}
className={`
flex items-center gap-1
px-2 py-1.5 rounded text-sm transition-all duration-200
text-white
bg-gradient-to-r from-blue-600 to-blue-700
hover:from-blue-700 hover:to-blue-800
disabled:from-gray-400 disabled:to-gray-500
disabled:text-gray-200
disabled:cursor-not-allowed
disabled:hover:from-gray-400 disabled:hover:to-gray-500
`}
>
<FaPlus className="w-2.5 h-2.5" />
{translate('::App.DeveloperKit.EntityEditor.AddField')}
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-12 gap-2 mb-2">
<div className="col-span-1 font-bold">Order *</div>
<div className="col-span-2 font-bold">
{translate('::ListForms.ListFormFieldEdit.FieldName')} *
</div>
<div className="col-span-1 font-bold">
{translate('::ListForms.ListFormEdit.Type')} *
</div>
<div className="col-span-2 font-bold">
{translate('::ListForms.ListFormEdit.ExtraDefaultValue')}
</div>
<div className="font-bold">
{translate('::App.DeveloperKit.EntityEditor.MaxLength')}
</div>
<div className="col-span-2 font-bold">
{translate('::ListForms.ListFormEdit.DetailsDescription')}
</div>
<div className="items-center font-bold">
{translate('::App.Required')}
</div>
<div className="items-center font-bold">
{translate('::App.DeveloperKit.EntityEditor.Unique')}
</div>
</div>
{values.fields.map((field, index) => (
<div key={field.id || `new-${index}`}>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-12 gap-2">
<FormItem className="col-span-1">
<Field
type="number"
name={`fields.${index}.displayOrder`}
component={Input}
/>
</FormItem>
<FormItem
invalid={
!!(
errors.fields &&
(errors.fields as any)[index]?.name &&
touched.fields &&
(touched.fields as any)[index]?.name
)
}
errorMessage={(errors.fields as any)?.[index]?.name as string}
className="col-span-2"
>
<Field
name={`fields.${index}.name`}
component={Input}
placeholder="e.g., Name, Email, Age"
/>
</FormItem>
<FormItem
invalid={
!!(
errors.fields &&
(errors.fields as any)[index]?.type &&
touched.fields &&
(touched.fields as any)[index]?.type
)
}
errorMessage={(errors.fields as any)?.[index]?.type as string}
className="col-span-1"
>
<Field name={`fields.${index}.type`}>
{({ field, form }: FieldProps<SelectBoxOption>) => (
<Select
field={field}
form={form}
options={fieldTypes.map((type) => ({
value: type.value,
label: type.label,
}))}
value={fieldTypes
.map((type) => ({ value: type.value, label: type.label }))
.filter((option) => option.value === field.value)}
onChange={(option) =>
form.setFieldValue(field.name, option?.value)
}
/>
)}
</Field>
</FormItem>
<FormItem
invalid={
!!(
errors.fields &&
(errors.fields as any)[index]?.defaultValue &&
touched.fields &&
(touched.fields as any)[index]?.defaultValue
)
}
errorMessage={
(errors.fields as any)?.[index]?.defaultValue as string
}
className="col-span-2"
>
<Field
name={`fields.${index}.defaultValue`}
component={Input}
placeholder="Optional default value"
/>
</FormItem>
<FormItem
invalid={
!!(
errors.fields &&
(errors.fields as any)[index]?.maxLength &&
touched.fields &&
(touched.fields as any)[index]?.maxLength
)
}
errorMessage={(errors.fields as any)?.[index]?.maxLength as string}
>
<Field
name={`fields.${index}.maxLength`}
component={Input}
type="number"
placeholder="e.g., 100"
disabled={field.type !== 'string'}
/>
</FormItem>
<FormItem
invalid={
!!(
errors.fields &&
(errors.fields as any)[index]?.description &&
touched.fields &&
(touched.fields as any)[index]?.description
)
}
errorMessage={
(errors.fields as any)?.[index]?.description as string
}
className="col-span-2"
>
<Field
name={`fields.${index}.description`}
component={Input}
placeholder="Field description"
/>
</FormItem>
<FormItem className="items-center">
<Field name={`fields.${index}.isRequired`} component={Checkbox} />
</FormItem>
<FormItem className="items-center">
<Field name={`fields.${index}.isUnique`} component={Checkbox} />
</FormItem>
<Button
size="xs"
onClick={() => {
remove(index)
const newFields = values.fields ? [...values.fields] : []
newFields.splice(index, 1)
newFields.forEach((f, i) => {
f.displayOrder = i + 1
})
setFieldValue('fields', newFields)
}}
className="!px-0 !py-0 !border-0 text-red-600 hover:text-red-800 rounded transition-all duration-200"
title="Remove field"
>
<FaTrashAlt className="w-5 h-5" />
</Button>
</div>
</div>
))}
{values.fields.length === 0 && (
<div className="text-center py-4 bg-slate-50 rounded border-2 border-dashed border-slate-300">
<FaTable className="w-8 h-8 mx-auto mb-2 text-slate-400" />
<h3 className="text-sm font-medium text-slate-600 mb-1">
{translate('::App.DeveloperKit.EntityEditor.NoFields')}
</h3>
<p className="text-sm text-slate-500">
{translate('::App.DeveloperKit.EntityEditor.NoFieldsDescription')}
</p>
</div>
)}
</>
)}
</FieldArray>
</FormContainer>
</div>
</div>
</Form>
</>
)}
</Formik>
)
}
export default EntityEditor

View file

@ -1,438 +0,0 @@
import React, { useState, useEffect } from 'react'
import { Link } from 'react-router-dom'
import { useEntities } from '../../contexts/EntityContext'
import {
FaPlus,
FaSearch,
FaEdit,
FaTrashAlt,
FaEye,
FaEyeSlash,
FaFilter,
FaCalendarAlt,
FaDatabase,
FaCheckCircle,
FaTable,
FaBolt,
FaSync,
} from 'react-icons/fa'
import { ROUTES_ENUM } from '@/routes/route.constant'
import { useLocalization } from '@/utils/hooks/useLocalization'
const EntityManager: React.FC = () => {
const { entities, deleteEntity, toggleEntityActiveStatus, refreshEntities, generateMigration } = useEntities()
const [searchTerm, setSearchTerm] = useState('')
const [filterActive, setFilterActive] = useState<'all' | 'active' | 'inactive'>('all')
const [generatingMigration, setGeneratingMigration] = useState<string | null>(null)
const { translate } = useLocalization()
// Sayfa odaklandığında varlıkları yenile
useEffect(() => {
const handleVisibilityChange = () => {
if (!document.hidden) {
refreshEntities()
}
}
// Visibility change event listener ekle
document.addEventListener('visibilitychange', handleVisibilityChange)
return () => {
document.removeEventListener('visibilitychange', handleVisibilityChange)
}
}, [refreshEntities])
const filteredEntities = entities.filter((entity) => {
const matchesSearch =
entity.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
entity.displayName.toLowerCase().includes(searchTerm.toLowerCase()) ||
(entity.description || '').toLowerCase().includes(searchTerm.toLowerCase())
const matchesActiveFilter =
filterActive === 'all' ||
(filterActive === 'active' && entity.isActive) ||
(filterActive === 'inactive' && !entity.isActive)
return matchesSearch && matchesActiveFilter
})
const handleToggleActive = async (id: string) => {
try {
await toggleEntityActiveStatus(id)
} catch (err) {
console.error('Failed to toggle entity status:', err)
}
}
const handleDelete = async (id: string, name: string) => {
if (
window.confirm(
`Are you sure you want to delete the "${name}" entity?\n\nThis action will permanently remove:\n• The entity and all its fields\n• All related migrations\n• All related API endpoints\n\nThis cannot be undone.`,
)
) {
try {
await deleteEntity(id)
} catch (err) {
console.error('Failed to delete entity:', err)
alert(translate('::App.DeveloperKit.Entity.DeleteFailed'))
}
}
}
const handleGenerateMigration = async (entityId: string) => {
try {
setGeneratingMigration(entityId)
await generateMigration(entityId)
alert('Migration generated successfully!')
} catch (err) {
console.error('Failed to generate migration:', err)
alert('Failed to generate migration. Please try again.')
} finally {
setGeneratingMigration(null)
}
}
const stats = {
total: entities.length,
active: entities.filter((e) => e.isActive).length,
migrationsPending: entities.filter((e) => e.migrationStatus === 'pending').length,
endpointsApplied: entities.filter((e) => e.endpointStatus === 'applied').length,
}
return (
<div className="space-y-4">
<div className="flex items-center justify-between mb-4">
<div>
<h1 className="text-2xl font-bold text-slate-900">
{translate('::App.DeveloperKit.Entity')}
</h1>
<p className="text-slate-600">{translate('::App.DeveloperKit.Entity.Description')}</p>
</div>
<Link
to={ROUTES_ENUM.protected.saas.developerKit.entitiesNew}
className="flex items-center gap-2 bg-emerald-600 text-white px-4 py-2 rounded-lg hover:bg-emerald-700 transition-colors shadow-sm hover:shadow-md"
>
<FaPlus className="w-4 h-4" />
{translate('::App.DeveloperKit.Entity.NewEntity')}
</Link>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-slate-600">
{translate('::App.DeveloperKit.Entity.TotalEntities')}
</p>
<p className="text-2xl font-bold text-slate-900 mt-1">{stats.total}</p>
</div>
<div className="bg-blue-100 text-blue-600 p-3 rounded-lg">
<FaDatabase className="w-6 h-6" />
</div>
</div>
</div>
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-slate-600">
{translate('::App.DeveloperKit.Entity.ActiveEntities')}
</p>
<p className="text-2xl font-bold text-slate-900 mt-1">
{stats.active}
<span className="text-sm font-normal text-slate-500 ml-1">/ {stats.total}</span>
</p>
</div>
<div className="bg-green-100 text-green-600 p-3 rounded-lg">
<FaCheckCircle className="w-6 h-6" />
</div>
</div>
</div>
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-slate-600">
{translate('::App.DeveloperKit.Entity.PendingMigrations')}
</p>
<p className="text-2xl font-bold text-slate-900 mt-1">{stats.migrationsPending}</p>
</div>
<div className="bg-purple-100 text-purple-600 p-3 rounded-lg">
<FaBolt className="w-6 h-6" />
</div>
</div>
</div>
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-slate-600">
{translate('::App.DeveloperKit.Entity.CrudEndpoints')}
</p>
<p className="text-2xl font-bold text-slate-900 mt-1">{stats.endpointsApplied}</p>
</div>
<div className="bg-emerald-100 text-emerald-600 p-3 rounded-lg">
<FaBolt className="w-6 h-6" />
</div>
</div>
</div>
</div>
{/* Filters */}
<div className="flex flex-col lg:flex-row gap-4">
<div className="flex-1 relative">
<FaSearch className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-slate-400" />
<input
type="text"
placeholder={translate('::App.DeveloperKit.Entity.SearchPlaceholder')}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-10 pr-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-emerald-500 focus:border-transparent transition-colors"
/>
</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<FaFilter className="w-5 h-5 text-slate-500" />
<select
value={filterActive}
onChange={(e) => setFilterActive(e.target.value as 'all' | 'active' | 'inactive')}
className="px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-emerald-500 focus:border-transparent transition-colors"
>
<option value="all">{translate('::App.DeveloperKit.Entity.Filter.All')}</option>
<option value="active">{translate('::App.DeveloperKit.Entity.Filter.Active')}</option>
<option value="inactive">
{translate('::App.DeveloperKit.Entity.Filter.Inactive')}
</option>
</select>
</div>
</div>
</div>
{/* Entities List */}
{filteredEntities.length > 0 ? (
<div className="grid grid-cols-1 xl:grid-cols-3 gap-6">
{filteredEntities.map((entity) => {
return (
<div
key={entity.id}
className="bg-white rounded-lg border border-slate-200 shadow-sm hover:shadow-md transition-all duration-200 group"
>
<div className="p-5">
<div className="flex items-start justify-between mb-4">
<div className="flex-1">
<div className="flex items-center justify-between mb-2">
{/* Sol taraf */}
<div className="flex items-center gap-3">
<div className="bg-blue-100 text-blue-600 p-2 rounded-lg">
<FaTable className="w-5 h-5" />
</div>
<div>
<h3 className="text-lg font-semibold text-slate-900">
{entity.displayName}
</h3>
<p className="text-sm text-slate-500">
{translate('::ListForms.ListFormEdit.DatabaseDataSourceTableName')}:{' '}
{entity.tableName}
</p>
</div>
</div>
{/* Sağ taraf */}
<div className="flex flex-col items-end text-sm text-slate-600 gap-1">
<div className="flex items-center gap-1">
<FaCalendarAlt className="w-3 h-3" />
<span>
{translate('::App.DeveloperKit.Entity.Updated')}:{' '}
{entity.lastModificationTime
? new Date(entity.lastModificationTime).toLocaleDateString()
: 'Never'}
</span>
</div>
<div className="flex items-center gap-1">
<FaDatabase className="w-3 h-3" />
<span>
{entity.fields.length} {translate('::ListForms.ListFormEdit.TabFields')}
</span>
</div>
</div>
</div>
{entity.description && (
<p className="text-slate-600 text-sm mb-3">{entity.description}</p>
)}
</div>
</div>
{/* Entity Fields Preview */}
<div className="mb-4">
<div className="bg-slate-50 rounded-lg p-3">
<div className="grid grid-cols-3 gap-2 text-xs">
{entity.fields.slice(0, 4).map((field) => (
<div key={field.id} className="flex items-center gap-2">
<span
className={`px-2 py-1 rounded text-xs font-medium ${
field.type === 'string'
? 'bg-blue-100 text-blue-700'
: field.type === 'number' || field.type === 'decimal'
? 'bg-green-100 text-green-700'
: field.type === 'boolean'
? 'bg-purple-100 text-purple-700'
: field.type === 'date'
? 'bg-orange-100 text-orange-700'
: 'bg-slate-100 text-slate-700'
}`}
>
{field.type}
</span>
<span className="text-slate-600">{field.name}</span>
{field.isRequired && <span className="text-red-500">*</span>}
</div>
))}
{entity.fields.length > 4 && (
<div className="text-slate-500 italic">
+{entity.fields.length - 4}{' '}
{translate('::App.DeveloperKit.Entity.More')}
</div>
)}
</div>
</div>
</div>
{/* Migration and Endpoint Status */}
<div className="mb-4 space-y-3">
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-slate-700">
{translate('::App.DeveloperKit.Entity.MigrationStatus')}
</span>
<div className="flex items-center gap-2">
<span
className={`text-xs px-2 py-1 rounded-full ${
entity.migrationStatus === 'applied'
? 'bg-green-100 text-green-700'
: entity.migrationStatus === 'pending'
? 'bg-yellow-100 text-yellow-700'
: 'bg-red-100 text-red-700'
}`}
>
{entity.migrationStatus}
</span>
{entity.migrationStatus === 'pending' && (
<button
onClick={() => handleGenerateMigration(entity.id)}
disabled={generatingMigration === entity.id}
className="flex items-center gap-1 px-2 py-1 text-xs bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
title="Generate Migration"
>
{generatingMigration === entity.id ? (
<>
<FaSync className="w-3 h-3 animate-spin" />
<span>Generating...</span>
</>
) : (
<>
<FaBolt className="w-3 h-3" />
<span>Generate</span>
</>
)}
</button>
)}
</div>
</div>
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-slate-700">
{translate('::App.DeveloperKit.Entity.EndpointStatus')}
</span>
<span
className={`text-xs px-2 py-1 rounded-full ${
entity.endpointStatus === 'applied'
? 'bg-blue-100 text-blue-700'
: entity.endpointStatus === 'pending'
? 'bg-orange-100 text-orange-700'
: 'bg-red-100 text-red-700'
}`}
>
{entity.endpointStatus}
</span>
</div>
</div>
{/* Actions */}
<div className="flex items-center justify-between pt-4 border-t border-slate-100">
<div className="flex items-center gap-2">
<button
onClick={() => handleToggleActive(entity.id)}
className={`flex items-center gap-1 px-3 py-1 rounded-full text-xs font-medium transition-colors ${
entity.isActive
? 'bg-green-100 text-green-700 hover:bg-green-200'
: 'bg-slate-100 text-slate-600 hover:bg-slate-200'
}`}
>
{entity.isActive ? (
<>
<FaEye className="w-3 h-3" />
{translate('::App.Status.Active')}
</>
) : (
<>
<FaEyeSlash className="w-3 h-3" />
{translate('::App.Status.Inactive')}
</>
)}
</button>
</div>
<div className="flex items-center gap-1">
<Link
to={ROUTES_ENUM.protected.saas.developerKit.entitiesEdit.replace(
':id',
entity.id,
)}
className="p-2 text-slate-600 hover:text-blue-600 hover:bg-blue-50 rounded transition-colors"
title={translate('::App.DeveloperKit.Entity.Edit')}
>
<FaEdit className="w-4 h-4" />
</Link>
<button
onClick={() => handleDelete(entity.id, entity.displayName)}
className="p-2 text-slate-600 hover:text-red-600 hover:bg-red-50 rounded transition-colors"
title={translate('::App.DeveloperKit.Entity.Delete')}
>
<FaTrashAlt className="w-4 h-4" />
</button>
</div>
</div>
</div>
</div>
)
})}
</div>
) : (
<div className="text-center py-12">
<div className="max-w-md mx-auto">
<div className="bg-slate-100 rounded-full w-16 h-16 flex items-center justify-center mx-auto mb-4">
<FaDatabase className="w-8 h-8 text-slate-500" />
</div>
<h3 className="text-lg font-medium text-slate-900 mb-2">
{searchTerm || filterActive !== 'all'
? translate('::App.DeveloperKit.Entity.NoEntitiesFound')
: translate('::App.DeveloperKit.Entity.NoEntitiesYet')}
</h3>
<p className="text-slate-600 mb-6">
{searchTerm || filterActive !== 'all'
? translate('::App.DeveloperKit.EmptyFilteredDescription')
: translate('::App.DeveloperKit.Entity.CreateFirstEntity')}
</p>
{!searchTerm && filterActive === 'all' && (
<Link
to={ROUTES_ENUM.protected.saas.developerKit.entitiesNew}
className="inline-flex items-center gap-2 bg-emerald-600 text-white px-4 py-2 rounded-lg hover:bg-emerald-700 transition-colors shadow-sm hover:shadow-md"
>
<FaPlus className="w-4 h-4" />
{translate('::App.DeveloperKit.Entity.CreateEntity')}
</Link>
)}
</div>
</div>
)}
</div>
)
}
export default EntityManager

View file

@ -1,533 +0,0 @@
import React, { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { useEntities } from '../../contexts/EntityContext'
import {
FaBolt,
FaSearch,
FaFilter,
FaCalendarAlt,
FaDatabase,
FaPlay,
FaCheckCircle,
FaRegBell,
FaRegClock,
FaRegFileAlt,
FaDownload,
FaEye,
FaSync,
FaTrashAlt,
FaPlus,
} from 'react-icons/fa'
import { ROUTES_ENUM } from '@/routes/route.constant'
import { useLocalization } from '@/utils/hooks/useLocalization'
const MigrationManager: React.FC = () => {
const navigate = useNavigate()
const {
entities,
migrations,
generateMigration,
applyMigration,
deleteMigration,
generateCrudEndpoints,
loading,
error,
} = useEntities()
const [searchTerm, setSearchTerm] = useState('')
const [filterStatus, setFilterStatus] = useState<'all' | 'pending' | 'applied' | 'failed'>('all')
const [selectedMigration, setSelectedMigration] = useState<string | null>(null)
const [applyingMigration, setApplyingMigration] = useState<string | null>(null)
const [deletingMigration, setDeletingMigration] = useState<string | null>(null)
const [generatingEndpoints, setGeneratingEndpoints] = useState<string | null>(null)
const { translate } = useLocalization()
const filteredMigrations = migrations.filter((migration) => {
const matchesSearch =
migration.entityName.toLowerCase().includes(searchTerm.toLowerCase()) ||
(migration.fileName && migration.fileName.toLowerCase().includes(searchTerm.toLowerCase()))
const matchesStatusFilter = filterStatus === 'all' || migration.status === filterStatus
return matchesSearch && matchesStatusFilter
})
const handleGenerateMigration = async (entityId: string) => {
try {
await generateMigration(entityId)
} catch (error) {
console.error('Failed to generate migration:', error)
}
}
const handleApplyMigration = async (migrationId: string) => {
try {
setApplyingMigration(migrationId)
await applyMigration(migrationId)
} catch (error) {
console.error('Failed to apply migration:', error)
} finally {
setApplyingMigration(null)
}
}
const handleDeleteMigration = async (migrationId: string) => {
try {
setDeletingMigration(migrationId)
await deleteMigration(migrationId)
} catch (error) {
console.error('Failed to delete migration:', error)
} finally {
setDeletingMigration(null)
}
}
const handleDownloadMigration = (migration: (typeof migrations)[0]) => {
if (!migration.sqlScript) return
const blob = new Blob([migration.sqlScript], { type: 'text/sql' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download =
migration.fileName ||
`migration_${migration.entityName}_${new Date().toISOString().split('T')[0]}.sql`
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
}
const handleGenerateCrudEndpoints = async (entityId: string) => {
try {
setGeneratingEndpoints(entityId)
await generateCrudEndpoints(entityId)
// Navigate to API Endpoints page after successful generation
navigate(ROUTES_ENUM.protected.saas.developerKit.migrations)
} catch (error) {
console.error('Failed to generate CRUD endpoints:', error)
} finally {
setGeneratingEndpoints(null)
}
}
const getStatusColor = (status: string) => {
switch (status) {
case 'pending':
return 'bg-yellow-100 text-yellow-800 border-yellow-200'
case 'applied':
return 'bg-green-100 text-green-800 border-green-200'
case 'failed':
return 'bg-red-100 text-red-800 border-red-200'
default:
return 'bg-slate-100 text-slate-800 border-slate-200'
}
}
const getStatusIcon = (status: string) => {
switch (status) {
case 'pending':
return FaRegClock
case 'applied':
return FaCheckCircle
case 'failed':
return FaRegBell
default:
return FaRegClock
}
}
const stats = {
total: migrations.length,
pending: migrations.filter((m) => m.status === 'pending').length,
applied: migrations.filter((m) => m.status === 'applied').length,
failed: migrations.filter((m) => m.status === 'failed').length,
}
const entitiesNeedingMigration = entities.filter(
(e) => e.migrationStatus === 'pending' || !e.migrationId,
)
return (
<div className="space-y-4">
{/* Error Message */}
{error && (
<div className="mb-6 bg-red-50 border border-red-200 rounded-lg p-4">
<div className="flex items-center gap-2 text-red-800">
<FaRegBell className="w-5 h-5" />
<span className="font-medium">Error</span>
</div>
<p className="text-red-700 text-sm mt-1">{error}</p>
</div>
)}
<div className="flex items-center justify-between mb-4">
<div>
<h1 className="text-2xl font-bold text-slate-900">
{translate('::App.DeveloperKit.Migrations')}
</h1>
<p className="text-slate-600">{translate('::App.DeveloperKit.Migration.Description')}</p>
</div>
<div className="flex items-center gap-3">
<div className="flex items-center gap-2 bg-blue-100 text-blue-700 px-3 py-1 rounded-full text-sm font-medium">
<FaDatabase className="w-4 h-4" />
{translate('::App.DeveloperKit.Migration.SchemaManagement')}
</div>
{loading && (
<div className="flex items-center gap-2 bg-yellow-100 text-yellow-700 px-3 py-1 rounded-full text-sm font-medium">
<FaSync className="w-4 h-4 animate-spin" />
{translate('::App.Loading')}
</div>
)}
</div>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-4">
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-slate-600">
{translate('::App.DeveloperKit.Migration.Total')}
</p>
<p className="text-2xl font-bold text-slate-900 mt-1">{stats.total}</p>
</div>
<div className="bg-blue-100 text-blue-600 p-3 rounded-lg">
<FaRegFileAlt className="w-6 h-6" />
</div>
</div>
</div>
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-slate-600">
{translate('::App.DeveloperKit.Migration.Pending')}
</p>
<p className="text-2xl font-bold text-slate-900 mt-1">{stats.pending}</p>
</div>
<div className="bg-yellow-100 text-yellow-600 p-3 rounded-lg">
<FaRegClock className="w-6 h-6" />
</div>
</div>
</div>
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-slate-600">
{translate('::App.DeveloperKit.Migration.Applied')}
</p>
<p className="text-2xl font-bold text-slate-900 mt-1">{stats.applied}</p>
</div>
<div className="bg-green-100 text-green-600 p-3 rounded-lg">
<FaCheckCircle className="w-6 h-6" />
</div>
</div>
</div>
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-slate-600">
{translate('::App.DeveloperKit.Migration.Failed')}
</p>
<p className="text-2xl font-bold text-slate-900 mt-1">{stats.failed}</p>
</div>
<div className="bg-red-100 text-red-600 p-3 rounded-lg">
<FaRegBell className="w-6 h-6" />
</div>
</div>
</div>
</div>
{/* Entities Needing Migration */}
{entitiesNeedingMigration.length > 0 && (
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4 mb-4">
<div className="flex items-start gap-4">
<div className="bg-yellow-100 rounded-lg p-2">
<FaBolt className="w-6 h-6 text-yellow-600" />
</div>
<div className="flex-1">
<h3 className="text-lg font-semibold text-slate-900">
{translate('::App.DeveloperKit.Migration.EntitiesRequireMigration')}
</h3>
<p className="text-slate-700 mb-2">
{translate('::App.DeveloperKit.Migration.EntitiesRequireMigrationDescription')}
</p>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-6 gap-3">
{entitiesNeedingMigration.map((entity) => (
<div key={entity.id} className="bg-white border border-yellow-300 rounded-lg p-4">
<div className="flex items-center justify-between">
<div>
<h4 className="font-medium text-slate-900">{entity.displayName}</h4>
<p className="text-sm text-slate-600">
{entity.fields.length} {translate('::ListForms.ListFormEdit.TabFields')}
</p>
</div>
<button
onClick={() => handleGenerateMigration(entity.id)}
disabled={loading}
className="bg-yellow-600 text-white px-3 py-1 rounded text-sm hover:bg-yellow-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{loading
? translate('::App.DeveloperKit.Migration.Generating')
: translate('::Create')}
</button>
</div>
</div>
))}
</div>
</div>
</div>
</div>
)}
{/* Filters */}
<div className="flex flex-col lg:flex-row gap-4">
<div className="flex-1 relative">
<FaSearch className="absolute left-3 top-1/2 transform -translate-y-1/2 w-5 h-5 text-slate-400" />
<input
type="text"
placeholder={translate('::App.DeveloperKit.Migration.SearchPlaceholder')}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-10 pr-4 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors"
/>
</div>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<FaFilter className="w-5 h-5 text-slate-500" />
<select
value={filterStatus}
onChange={(e) =>
setFilterStatus(e.target.value as 'all' | 'pending' | 'applied' | 'failed')
}
className="px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-colors"
>
<option value="all">{translate('::App.DeveloperKit.Migration.AllStatus')}</option>
<option value="pending">Pending</option>
<option value="applied">Applied</option>
<option value="failed">Failed</option>
</select>
</div>
</div>
</div>
{/* Migrations List */}
{filteredMigrations.length > 0 ? (
<div className="grid grid-cols-1 xl:grid-cols-3 gap-6">
{filteredMigrations.map((migration) => {
const StatusIcon = getStatusIcon(migration.status)
return (
<div
key={migration.id}
className="bg-white rounded-lg border border-slate-200 shadow-sm"
>
<div className="p-6">
<div className="flex items-start justify-between mb-4">
{/* Sol taraf */}
<div className="flex items-center gap-3">
<div className="bg-blue-100 text-blue-600 p-2 rounded-lg">
<FaDatabase className="w-5 h-5" />
</div>
<div>
<h3 className="text-lg font-semibold text-slate-900">
{migration.fileName || `Migration for ${migration.entityName}`}
</h3>
<p className="text-sm text-slate-600">Entity: {migration.entityName}</p>
</div>
</div>
{/* Sağ taraf */}
<div className="flex flex-col items-end gap-2 text-right">
{/* Status etiketi */}
<span
className={`px-3 py-1 text-sm font-medium rounded-full border ${getStatusColor(
migration.status,
)}`}
>
<StatusIcon className="w-4 h-4 inline mr-1" />
{migration.status.charAt(0).toUpperCase() + migration.status.slice(1)}
</span>
{/* Tarihler */}
<div className="flex flex-col items-end text-xs text-slate-500">
<div className="flex items-center gap-1">
<FaCalendarAlt className="w-3 h-3" />
<span>
{translate('::App.DeveloperKit.Migration.Created')}{' '}
{new Date(migration.creationTime).toLocaleDateString()}
</span>
</div>
{migration.appliedAt && (
<div className="flex items-center gap-1">
<FaCheckCircle className="w-3 h-3" />
<span>
{translate('::App.DeveloperKit.Migration.Applied')}{' '}
{new Date(migration.appliedAt).toLocaleDateString()}
</span>
</div>
)}
</div>
</div>
</div>
{/* Actions */}
<div className="flex items-center justify-between pt-4 border-t border-slate-100">
<div className="flex items-center gap-2">
<button
onClick={() =>
setSelectedMigration(
selectedMigration === migration.id ? null : migration.id,
)
}
className={`flex items-center gap-1 px-3 py-1 text-sm rounded transition-colors ${
migration.sqlScript
? 'text-slate-600 hover:text-slate-900 hover:bg-slate-100'
: 'text-orange-600 hover:text-orange-900 hover:bg-orange-50'
}`}
>
<FaEye className="w-4 h-4" />
{translate('::App.DeveloperKit.Migration.ViewSQL')}
{!migration.sqlScript && (
<span className="ml-1 text-xs text-orange-500">
({translate('::App.DeveloperKit.Migration.NoSQL')})
</span>
)}
</button>
<button
onClick={() => handleDownloadMigration(migration)}
disabled={!migration.sqlScript}
className={`flex items-center gap-1 px-3 py-1 text-sm rounded transition-colors ${
migration.sqlScript
? 'text-slate-600 hover:text-slate-900 hover:bg-slate-100'
: 'text-slate-400 cursor-not-allowed'
}`}
>
<FaDownload className="w-4 h-4" />
{translate('::App.DeveloperKit.Migration.Download')}
</button>
</div>
<div className="flex items-center gap-2">
{migration.status === 'pending' && (
<>
<button
onClick={() => handleApplyMigration(migration.id)}
disabled={applyingMigration === migration.id || loading}
className="flex items-center gap-2 bg-green-600 text-white px-4 py-2 rounded-lg hover:bg-green-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{applyingMigration === migration.id ? (
<>
<FaSync className="w-4 h-4 animate-spin" />
{translate('::App.DeveloperKit.Migration.Applying')}
</>
) : (
<>
<FaPlay className="w-4 h-4" />
{translate('::App.DeveloperKit.Migration.ApplyMigration')}
</>
)}
</button>
<button
onClick={() => handleDeleteMigration(migration.id)}
disabled={deletingMigration === migration.id || loading}
className="flex items-center gap-2 bg-red-600 text-white px-3 py-2 rounded-lg hover:bg-red-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{deletingMigration === migration.id ? (
<FaSync className="w-4 h-4 animate-spin" />
) : (
<FaTrashAlt className="w-4 h-4" />
)}
</button>
</>
)}
{migration.status === 'applied' && (
<button
onClick={() => handleGenerateCrudEndpoints(migration.entityId)}
disabled={generatingEndpoints === migration.entityId || loading}
className="flex items-center gap-2 bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{generatingEndpoints === migration.entityId ? (
<>
<FaSync className="w-4 h-4 animate-spin" />
{translate('::App.DeveloperKit.Migration.Generating')}
</>
) : (
<>
<FaPlus className="w-4 h-4" />
{translate('::App.DeveloperKit.Migration.GenerateCrudEndpoints')}
</>
)}
</button>
)}
</div>
</div>
{/* SQL Script Preview */}
{selectedMigration === migration.id && (
<div className="mt-4 pt-4 border-t border-slate-200">
<h4 className="font-medium text-slate-900 mb-2">
{translate('::ListForms.ListFormEdit.SqlQuery')}
</h4>
{migration.sqlScript ? (
<div className="bg-slate-900 rounded-lg p-4 overflow-x-auto">
<pre className="text-green-400 text-sm">
<code>{migration.sqlScript}</code>
</pre>
</div>
) : (
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
<div className="flex items-center gap-2 text-yellow-800">
<FaRegBell className="w-5 h-5" />
<span className="font-medium">
{translate('::App.DeveloperKit.Migration.NoSQLScript')}
</span>
</div>
<p className="text-yellow-700 text-sm mt-1">
{translate('::App.DeveloperKit.Migration.NoSQLScriptDesc')}
</p>
</div>
)}
</div>
)}
{/* Error Message */}
{migration.status === 'failed' && migration.errorMessage && (
<div className="mt-4 pt-4 border-t border-slate-200">
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
<div className="flex items-center gap-2 text-red-800 mb-2">
<FaRegBell className="w-5 h-5" />
<span className="font-medium">
{translate('::App.DeveloperKit.Migration.MigrationFailed')}
</span>
</div>
<p className="text-red-700 text-sm">{migration.errorMessage}</p>
</div>
</div>
)}
</div>
</div>
)
})}
</div>
) : (
<div className="text-center py-12">
<div className="max-w-md mx-auto">
<div className="bg-slate-100 rounded-full w-16 h-16 flex items-center justify-center mx-auto mb-4">
<FaBolt className="w-8 h-8 text-slate-500" />
</div>
<h3 className="text-lg font-medium text-slate-900 mb-2">
{searchTerm || filterStatus !== 'all'
? translate('::App.DeveloperKit.Migration.NoMigrationsFound')
: translate('::App.DeveloperKit.Migration.NoMigrationsYet')}
</h3>
<p className="text-slate-600">
{searchTerm || filterStatus !== 'all'
? translate('::App.DeveloperKit.EmptyFilteredDescription')
: translate('::App.DeveloperKit.Migration.CreateEntitiesForMigration')}
</p>
</div>
</div>
)}
</div>
)
}
export default MigrationManager

View file

@ -15,24 +15,12 @@ const DeveloperLayout: React.FC<DeveloperLayoutProps> = ({ children }) => {
const navigate = useNavigate() const navigate = useNavigate()
const navigation = [ const navigation = [
{
id: 'dashboard',
label: translate('::App.DeveloperKit.Dashboard'),
icon: FaTachometerAlt,
path: ROUTES_ENUM.protected.saas.developerKit.dashboard,
},
{ {
id: 'entities', id: 'entities',
label: translate('::App.DeveloperKit.Entity'), label: translate('::App.DeveloperKit.Entity'),
icon: FaDatabase, icon: FaDatabase,
path: ROUTES_ENUM.protected.saas.developerKit.entities, path: ROUTES_ENUM.protected.saas.developerKit.entities,
}, },
{
id: 'migrations',
label: translate('::App.DeveloperKit.Migrations'),
icon: FaBolt,
path: ROUTES_ENUM.protected.saas.developerKit.migrations,
},
{ {
id: 'endpoints', id: 'endpoints',
label: translate('::App.DeveloperKit.CrudEndpoints'), label: translate('::App.DeveloperKit.CrudEndpoints'),

View file

@ -1,8 +1,10 @@
import React, { createContext, useContext, useState, useEffect } from "react"; import React, { createContext, useContext, useState, useEffect } from "react";
import { developerKitService } from "@/services/developerKit.service";
import { import {
developerKitService, CrudEndpoint,
} from "@/services/developerKit.service"; CreateUpdateSqlTableDto,
import { CrudEndpoint, ApiMigration, CreateUpdateCustomEntityDto, CustomEntity } from "@/proxy/developerKit/models"; SqlTable,
} from "@/proxy/developerKit/models";
export const FIELD_TYPE_OPTIONS = [ export const FIELD_TYPE_OPTIONS = [
{ label: "String", value: "string" }, { label: "String", value: "string" },
@ -16,23 +18,17 @@ export const FIELD_TYPE_OPTIONS = [
export type EntityFieldType = (typeof FIELD_TYPE_OPTIONS)[number]["value"]; export type EntityFieldType = (typeof FIELD_TYPE_OPTIONS)[number]["value"];
interface EntityContextType { interface EntityContextType {
entities: CustomEntity[]; entities: SqlTable[];
migrations: ApiMigration[];
generatedEndpoints: CrudEndpoint[]; generatedEndpoints: CrudEndpoint[];
loading: boolean; loading: boolean;
error: string | null; error: string | null;
// Entity operations // Entity operations
addEntity: (entity: CreateUpdateCustomEntityDto) => Promise<void>; addEntity: (entity: CreateUpdateSqlTableDto) => Promise<void>;
updateEntity: (id: string, entity: CreateUpdateCustomEntityDto) => Promise<void>; updateEntity: (id: string, entity: CreateUpdateSqlTableDto) => Promise<void>;
deleteEntity: (id: string) => Promise<void>; deleteEntity: (id: string) => Promise<void>;
getEntity: (id: string) => CustomEntity | undefined; getEntity: (id: string) => SqlTable | undefined;
refreshEntities: () => Promise<void>; refreshEntities: () => Promise<void>;
toggleEntityActiveStatus: (id: string) => Promise<void>; toggleEntityActiveStatus: (id: string) => Promise<void>;
// Migration operations
generateMigration: (entityId: string) => Promise<void>;
applyMigration: (migrationId: string) => Promise<void>;
getEntityMigrations: (entityId: string) => ApiMigration[];
deleteMigration: (id: string) => Promise<void>;
// Generated endpoint operations // Generated endpoint operations
generateCrudEndpoints: (entityId: string) => Promise<void>; generateCrudEndpoints: (entityId: string) => Promise<void>;
toggleEndpoint: (endpointId: string) => Promise<void>; toggleEndpoint: (endpointId: string) => Promise<void>;
@ -51,28 +47,21 @@ export const useEntities = () => {
return context; return context;
}; };
export const EntityProvider: React.FC<{ children: React.ReactNode }> = ({ export const EntityProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
children, const [entities, setEntities] = useState<SqlTable[]>([]);
}) => { const [generatedEndpoints, setGeneratedEndpoints] = useState<CrudEndpoint[]>([]);
const [entities, setEntities] = useState<CustomEntity[]>([]);
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [migrations, setMigrations] = useState<ApiMigration[]>([]);
const [generatedEndpoints, setGeneratedEndpoints] = useState<CrudEndpoint[]>([]);
const refreshEntities = async () => { const refreshEntities = async () => {
try { try {
setLoading(true); setLoading(true);
setError(null); setError(null);
const [entitiesData, migrationsData, generatedEndpointsData] = const [entitiesData, generatedEndpointsData] = await Promise.all([
await Promise.all([ developerKitService.getSqlTables(),
developerKitService.getCustomEntities(),
developerKitService.getMigrations(),
developerKitService.getGeneratedEndpoints(), developerKitService.getGeneratedEndpoints(),
]); ]);
setEntities(entitiesData.items || []); setEntities(entitiesData.items || []);
setMigrations(migrationsData.items || []);
setGeneratedEndpoints(generatedEndpointsData.items || []); setGeneratedEndpoints(generatedEndpointsData.items || []);
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : "Failed to fetch data"); setError(err instanceof Error ? err.message : "Failed to fetch data");
@ -86,11 +75,11 @@ export const EntityProvider: React.FC<{ children: React.ReactNode }> = ({
refreshEntities(); refreshEntities();
}, []); }, []);
const addEntity = async (entityData: CreateUpdateCustomEntityDto) => { const addEntity = async (entityData: CreateUpdateSqlTableDto) => {
try { try {
setLoading(true); setLoading(true);
setError(null); setError(null);
const newEntity = await developerKitService.createCustomEntity(entityData); const newEntity = await developerKitService.createSqlTable(entityData);
setEntities((prev) => [...prev, newEntity]); setEntities((prev) => [...prev, newEntity]);
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : "Failed to create entity"); setError(err instanceof Error ? err.message : "Failed to create entity");
@ -100,21 +89,12 @@ export const EntityProvider: React.FC<{ children: React.ReactNode }> = ({
} }
}; };
const updateEntity = async ( const updateEntity = async (id: string, entityData: CreateUpdateSqlTableDto) => {
id: string,
entityData: CreateUpdateCustomEntityDto
) => {
try { try {
setLoading(true); setLoading(true);
setError(null); setError(null);
const updatedEntity = await developerKitService.updateCustomEntity(id, entityData); const updatedEntity = await developerKitService.updateSqlTable(id, entityData);
setEntities((prev) => prev.map((e) => (e.id === id ? updatedEntity : e)));
// State'i güncelle
setEntities((prev) =>
prev.map((entity) => (entity.id === id ? updatedEntity : entity))
);
// Güvenlik için tüm varlıkları yenile
await refreshEntities(); await refreshEntities();
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : "Failed to update entity"); setError(err instanceof Error ? err.message : "Failed to update entity");
@ -128,16 +108,9 @@ export const EntityProvider: React.FC<{ children: React.ReactNode }> = ({
try { try {
setLoading(true); setLoading(true);
setError(null); setError(null);
await developerKitService.deleteCustomEntity(id); await developerKitService.deleteSqlTable(id);
setEntities((prev) => prev.filter((e) => e.id !== id));
// Remove entity from state setGeneratedEndpoints((prev) => prev.filter((ep) => ep.entityId !== id));
setEntities((prev) => prev.filter((entity) => entity.id !== id));
// Remove related migrations from state
setMigrations((prev) => prev.filter((migration) => migration.entityId !== id));
// Remove related endpoints from state
setGeneratedEndpoints((prev) => prev.filter((endpoint) => endpoint.entityId !== id));
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : "Failed to delete entity"); setError(err instanceof Error ? err.message : "Failed to delete entity");
throw err; throw err;
@ -150,88 +123,28 @@ export const EntityProvider: React.FC<{ children: React.ReactNode }> = ({
try { try {
setLoading(true); setLoading(true);
setError(null); setError(null);
await developerKitService.toggleCustomEntityActiveStatus(id); await developerKitService.toggleSqlTableActiveStatus(id);
// Refresh entities to get updated status
await refreshEntities(); await refreshEntities();
} catch (err) { } catch (err) {
setError( setError(err instanceof Error ? err.message : "Failed to toggle entity status");
err instanceof Error ? err.message : "Failed to toggle entity status"
);
throw err; throw err;
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; };
const getEntity = (id: string): CustomEntity | undefined => { const getEntity = (id: string): SqlTable | undefined =>
return entities.find((entity) => entity.id === id); entities.find((e) => e.id === id);
};
// Migration operations
const generateMigration = async (entityId: string) => {
try {
setLoading(true);
setError(null);
const migration = await developerKitService.generateMigration(entityId);
setMigrations((prev) => [...prev, migration]);
// Refresh entities to get updated migration status
await refreshEntities();
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to generate migration");
throw err;
} finally {
setLoading(false);
}
};
const applyMigration = async (migrationId: string) => {
try {
setLoading(true);
setError(null);
const appliedMigration = await developerKitService.applyMigration(migrationId);
setMigrations((prev) =>
prev.map((m) => (m.id === migrationId ? appliedMigration : m))
);
// Refresh entities to get updated migration status
await refreshEntities();
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to apply migration");
throw err;
} finally {
setLoading(false);
}
};
const getEntityMigrations = (entityId: string): ApiMigration[] => {
return migrations.filter((migration) => migration.entityId === entityId);
};
const deleteMigration = async (id: string) => {
try {
setLoading(true);
setError(null);
await developerKitService.deleteMigration(id);
setMigrations((prev) => prev.filter((migration) => migration.id !== id));
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to delete migration");
throw err;
} finally {
setLoading(false);
}
};
// Generated endpoint operations
const generateCrudEndpoints = async (entityId: string) => { const generateCrudEndpoints = async (entityId: string) => {
try { try {
setLoading(true); setLoading(true);
setError(null); setError(null);
const endpointsData = await developerKitService.generateCrudEndpoints(entityId); const endpointsData = await developerKitService.generateCrudEndpoints(entityId);
// Replace existing endpoints for this entity
setGeneratedEndpoints((prev) => [ setGeneratedEndpoints((prev) => [
...prev.filter((e) => e.entityId !== entityId), ...prev.filter((e) => e.entityId !== entityId),
...endpointsData.items || [], ...(endpointsData.items || []),
]); ]);
// Refresh entities to get updated endpoint status
await refreshEntities(); await refreshEntities();
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : "Failed to generate CRUD endpoints"); setError(err instanceof Error ? err.message : "Failed to generate CRUD endpoints");
@ -247,7 +160,7 @@ export const EntityProvider: React.FC<{ children: React.ReactNode }> = ({
setError(null); setError(null);
const updatedEndpoint = await developerKitService.toggleGeneratedEndpoint(endpointId); const updatedEndpoint = await developerKitService.toggleGeneratedEndpoint(endpointId);
setGeneratedEndpoints((prev) => setGeneratedEndpoints((prev) =>
prev.map((e) => (e.id === endpointId ? updatedEndpoint : e)) prev.map((e) => (e.id === endpointId ? updatedEndpoint : e)),
); );
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : "Failed to toggle endpoint"); setError(err instanceof Error ? err.message : "Failed to toggle endpoint");
@ -257,16 +170,15 @@ export const EntityProvider: React.FC<{ children: React.ReactNode }> = ({
} }
}; };
const getEntityEndpoints = (entityId: string): CrudEndpoint[] => { const getEntityEndpoints = (entityId: string): CrudEndpoint[] =>
return generatedEndpoints.filter((endpoint) => endpoint.entityId === entityId); generatedEndpoints.filter((e) => e.entityId === entityId);
};
const deleteGeneratedEndpoint = async (id: string) => { const deleteGeneratedEndpoint = async (id: string) => {
try { try {
setLoading(true); setLoading(true);
setError(null); setError(null);
await developerKitService.deleteGeneratedEndpoint(id); await developerKitService.deleteGeneratedEndpoint(id);
setGeneratedEndpoints((prev) => prev.filter((endpoint) => endpoint.id !== id)); setGeneratedEndpoints((prev) => prev.filter((e) => e.id !== id));
} catch (err) { } catch (err) {
setError(err instanceof Error ? err.message : "Failed to delete generated endpoint"); setError(err instanceof Error ? err.message : "Failed to delete generated endpoint");
throw err; throw err;
@ -279,7 +191,6 @@ export const EntityProvider: React.FC<{ children: React.ReactNode }> = ({
<EntityContext.Provider <EntityContext.Provider
value={{ value={{
entities, entities,
migrations,
generatedEndpoints, generatedEndpoints,
loading, loading,
error, error,
@ -289,10 +200,6 @@ export const EntityProvider: React.FC<{ children: React.ReactNode }> = ({
getEntity, getEntity,
refreshEntities, refreshEntities,
toggleEntityActiveStatus, toggleEntityActiveStatus,
generateMigration,
applyMigration,
getEntityMigrations,
deleteMigration,
generateCrudEndpoints, generateCrudEndpoints,
toggleEndpoint, toggleEndpoint,
getEntityEndpoints, getEntityEndpoints,

View file

@ -1,4 +1,4 @@
export interface CustomEntity { export interface SqlTable {
id: string; id: string;
menu: string; menu: string;
name: string; name: string;
@ -8,15 +8,13 @@ export interface CustomEntity {
isActive: boolean; isActive: boolean;
isFullAuditedEntity: boolean; isFullAuditedEntity: boolean;
isMultiTenant: boolean; isMultiTenant: boolean;
migrationStatus: "pending" | "applied" | "failed";
endpointStatus: "pending" | "applied" | "failed"; endpointStatus: "pending" | "applied" | "failed";
migrationId?: string; fields: SqlTableField[];
fields: CustomEntityField[];
creationTime: string; creationTime: string;
lastModificationTime?: string; lastModificationTime?: string;
} }
export interface CustomEntityField { export interface SqlTableField {
id: string; id: string;
entityId: string; entityId: string;
name: string; name: string;
@ -31,7 +29,7 @@ export interface CustomEntityField {
displayOrder: number; displayOrder: number;
} }
export interface CreateUpdateCustomEntityFieldDto { export interface CreateUpdateSqlTableFieldDto {
id?: string; id?: string;
name: string; name: string;
type: "string" | "number" | "boolean" | "date" | "guid" | "decimal"; type: "string" | "number" | "boolean" | "date" | "guid" | "decimal";
@ -43,7 +41,7 @@ export interface CreateUpdateCustomEntityFieldDto {
displayOrder: number; displayOrder: number;
} }
export interface CreateUpdateCustomEntityDto { export interface CreateUpdateSqlTableDto {
menu: string; menu: string;
name: string; name: string;
displayName: string; displayName: string;
@ -52,7 +50,7 @@ export interface CreateUpdateCustomEntityDto {
isActive: boolean; isActive: boolean;
isFullAuditedEntity: boolean; isFullAuditedEntity: boolean;
isMultiTenant: boolean; isMultiTenant: boolean;
fields: CreateUpdateCustomEntityFieldDto[]; fields: CreateUpdateSqlTableFieldDto[];
} }
export interface CrudEndpoint { export interface CrudEndpoint {
@ -110,25 +108,3 @@ export interface CreateUpdateCustomComponentDto {
isActive: boolean; isActive: boolean;
dependencies?: string; dependencies?: string;
} }
export interface ApiMigration {
id: string;
entityId: string;
entityName: string;
fileName?: string;
sqlScript?: string;
status: "pending" | "applied" | "failed";
creationTime: string;
lastModificationTime?: string;
appliedAt?: string;
errorMessage?: string;
}
export interface CreateUpdateApiMigrationDto {
entityId: string;
entityName: string;
fileName?: string;
sqlScript?: string;
status?: "pending" | "applied" | "failed";
errorMessage?: string;
}

View file

@ -36,7 +36,6 @@ export const ROUTES_ENUM = {
entities: '/admin/developerkit/entities', entities: '/admin/developerkit/entities',
entitiesNew: '/admin/developerkit/entities/new', entitiesNew: '/admin/developerkit/entities/new',
entitiesEdit: '/admin/developerkit/entities/edit/:id', entitiesEdit: '/admin/developerkit/entities/edit/:id',
migrations: '/admin/developerkit/migrations',
endpoints: '/admin/developerkit/endpoints', endpoints: '/admin/developerkit/endpoints',
endpointsNew: '/admin/developerkit/endpoints/new', endpointsNew: '/admin/developerkit/endpoints/new',
components: '/admin/developerkit/components', components: '/admin/developerkit/components',

View file

@ -2,69 +2,67 @@ import { PagedResultDto } from '@/proxy'
import apiService from './api.service' import apiService from './api.service'
import { import {
CrudEndpoint, CrudEndpoint,
ApiMigration,
CreateUpdateCrudEndpointDto, CreateUpdateCrudEndpointDto,
CreateUpdateApiMigrationDto,
CreateUpdateCustomComponentDto, CreateUpdateCustomComponentDto,
CreateUpdateCustomEntityDto, CreateUpdateSqlTableDto,
CustomComponent, CustomComponent,
CustomComponentDto, CustomComponentDto,
CustomEntity, SqlTable,
} from '@/proxy/developerKit/models' } from '@/proxy/developerKit/models'
class DeveloperKitService { class DeveloperKitService {
async getCustomEntities(): Promise<PagedResultDto<CustomEntity>> { async getSqlTables(): Promise<PagedResultDto<SqlTable>> {
const response = await apiService.fetchData<PagedResultDto<CustomEntity>>({ const response = await apiService.fetchData<PagedResultDto<SqlTable>>({
url: '/api/app/custom-entity', url: '/api/app/sql-table',
method: 'GET', method: 'GET',
}) })
return response.data return response.data
} }
async getActiveCustomEntities(): Promise<PagedResultDto<CustomEntity>> { async getActiveSqlTables(): Promise<PagedResultDto<SqlTable>> {
const response = await apiService.fetchData<PagedResultDto<CustomEntity>>({ const response = await apiService.fetchData<PagedResultDto<SqlTable>>({
url: '/api/app/custom-entity/active-entities', url: '/api/app/sql-table/active-tables',
method: 'GET', method: 'GET',
}) })
return response.data return response.data
} }
async getCustomEntity(id: string): Promise<CustomEntity> { async getSqlTable(id: string): Promise<SqlTable> {
const response = await apiService.fetchData<CustomEntity>({ const response = await apiService.fetchData<SqlTable>({
url: `/api/app/custom-entity/${id}`, url: `/api/app/sql-table/${id}`,
method: 'GET', method: 'GET',
}) })
return response.data return response.data
} }
async createCustomEntity(data: CreateUpdateCustomEntityDto): Promise<CustomEntity> { async createSqlTable(data: CreateUpdateSqlTableDto): Promise<SqlTable> {
const response = await apiService.fetchData<CustomEntity>({ const response = await apiService.fetchData<SqlTable>({
url: '/api/app/custom-entity', url: '/api/app/sql-table',
method: 'POST', method: 'POST',
data: data as any, data: data as any,
}) })
return response.data return response.data
} }
async updateCustomEntity(id: string, entity: CreateUpdateCustomEntityDto): Promise<CustomEntity> { async updateSqlTable(id: string, entity: CreateUpdateSqlTableDto): Promise<SqlTable> {
const response = await apiService.fetchData<CustomEntity>({ const response = await apiService.fetchData<SqlTable>({
url: `/api/app/custom-entity/${id}`, url: `/api/app/sql-table/${id}`,
method: 'PUT', method: 'PUT',
data: entity as any, data: entity as any,
}) })
return response.data return response.data
} }
async deleteCustomEntity(id: string): Promise<void> { async deleteSqlTable(id: string): Promise<void> {
await apiService.fetchData<void>({ await apiService.fetchData<void>({
url: `/api/app/custom-entity/${id}`, url: `/api/app/sql-table/${id}`,
method: 'DELETE', method: 'DELETE',
}) })
} }
async toggleCustomEntityActiveStatus(id: string): Promise<void> { async toggleSqlTableActiveStatus(id: string): Promise<void> {
await apiService.fetchData<void>({ await apiService.fetchData<void>({
url: `/api/app/custom-entity/${id}/toggle-active-status`, url: `/api/app/sql-table/${id}/toggle-active-status`,
method: 'POST', method: 'POST',
}) })
} }
@ -122,72 +120,6 @@ class DeveloperKitService {
}) })
} }
// Migration endpoints
async getMigrations(): Promise<PagedResultDto<ApiMigration>> {
const response = await apiService.fetchData<PagedResultDto<ApiMigration>>({
url: '/api/app/crud-migration',
method: 'GET',
})
return response.data
}
async getMigration(id: string): Promise<ApiMigration> {
const response = await apiService.fetchData<ApiMigration>({
url: `/api/app/crud-migration/${id}`,
method: 'GET',
})
return response.data
}
async createMigration(data: CreateUpdateApiMigrationDto): Promise<ApiMigration> {
const response = await apiService.fetchData<ApiMigration>({
url: '/api/app/crud-migration',
method: 'POST',
data: data as any,
})
return response.data
}
async updateMigration(id: string, data: CreateUpdateApiMigrationDto): Promise<ApiMigration> {
const response = await apiService.fetchData<ApiMigration>({
url: `/api/app/crud-migration/${id}`,
method: 'PUT',
data: data as any,
})
return response.data
}
async deleteMigration(id: string): Promise<void> {
await apiService.fetchData<void>({
url: `/api/app/crud-migration/${id}`,
method: 'DELETE',
})
}
async applyMigration(id: string): Promise<ApiMigration> {
const response = await apiService.fetchData<ApiMigration>({
url: `/api/app/crud-migration/${id}/apply-migration`,
method: 'POST',
})
return response.data
}
async getPendingMigrations(): Promise<PagedResultDto<ApiMigration>> {
const response = await apiService.fetchData<PagedResultDto<ApiMigration>>({
url: '/api/app/crud-migration/pending-migrations',
method: 'GET',
})
return response.data
}
async generateMigration(entityId: string): Promise<ApiMigration> {
const response = await apiService.fetchData<ApiMigration>({
url: `/api/app/crud-migration/generate-migration/${entityId}`,
method: 'POST',
})
return response.data
}
// Generated Endpoint endpoints // Generated Endpoint endpoints
async getGeneratedEndpoints(): Promise<PagedResultDto<CrudEndpoint>> { async getGeneratedEndpoints(): Promise<PagedResultDto<CrudEndpoint>> {
const response = await apiService.fetchData<PagedResultDto<CrudEndpoint>>({ const response = await apiService.fetchData<PagedResultDto<CrudEndpoint>>({

View file

@ -1,128 +0,0 @@
import { ApiMigration, CrudEndpoint, CustomEntity } from '@/proxy/developerKit/models';
import { developerKitService } from '@/services/developerKit.service';
import { useState, useEffect } from 'react';
export const useEntities = () => {
const [entities, setEntities] = useState<CustomEntity[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const fetchEntities = async () => {
try {
setLoading(true);
const data = await developerKitService.getCustomEntities();
setEntities(data.items || []);
setError(null);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to fetch entities');
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchEntities();
}, []);
return {
entities,
loading,
error,
refetch: fetchEntities,
};
};
export const useMigrations = () => {
const [migrations, setMigrations] = useState<ApiMigration[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const fetchMigrations = async () => {
try {
setLoading(true);
const data = await developerKitService.getMigrations();
setMigrations(data.items || []);
setError(null);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to fetch migrations');
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchMigrations();
}, []);
return {
migrations,
loading,
error,
refetch: fetchMigrations,
};
};
export const useGeneratedEndpoints = () => {
const [endpoints, setEndpoints] = useState<CrudEndpoint[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const fetchEndpoints = async () => {
try {
setLoading(true);
const data = await developerKitService.getGeneratedEndpoints();
setEndpoints(data.items || []);
setError(null);
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to fetch endpoints');
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchEndpoints();
}, []);
return {
endpoints,
loading,
error,
refetch: fetchEndpoints,
};
};
// System health durumunu kontrol eden hook
export const useSystemHealth = () => {
const [isOnline, setIsOnline] = useState(true);
const [lastCheck, setLastCheck] = useState<Date>(new Date());
const checkSystemHealth = async () => {
try {
// API'ye basit bir health check yapalım
await developerKitService.getCustomEntities();
setIsOnline(true);
setLastCheck(new Date());
} catch (error) {
console.error('System health check failed:', error);
setIsOnline(false);
setLastCheck(new Date());
}
};
useEffect(() => {
// İlk kontrol
checkSystemHealth();
// Her 30 saniyede bir kontrol et
const interval = setInterval(checkSystemHealth, 30000);
return () => clearInterval(interval);
}, []);
return {
isOnline,
lastCheck,
recheckHealth: checkSystemHealth,
};
};

View file

@ -355,7 +355,7 @@ const Wizard = () => {
defaultTitle={APP_NAME} defaultTitle={APP_NAME}
/> />
<div className="mb-8"> <div className="mb-6 mt-2">
<Steps current={currentStep}> <Steps current={currentStep}>
<Steps.Item title={translate('::ListForms.Wizard.MenuInfo') || 'Menu Info'} /> <Steps.Item title={translate('::ListForms.Wizard.MenuInfo') || 'Menu Info'} />
<Steps.Item <Steps.Item

View file

@ -10,7 +10,7 @@ import {
toast, toast,
Tooltip, Tooltip,
} from '@/components/ui' } from '@/components/ui'
import SqlEditor from '@/views/sqlQueryManager/components/SqlEditor' import SqlEditor from '@/views/developerKit/SqlEditor'
import { ListFormJsonRowDto } from '@/proxy/admin/list-form/models' import { ListFormJsonRowDto } from '@/proxy/admin/list-form/models'
import { SelectBoxOption } from '@/types/shared' import { SelectBoxOption } from '@/types/shared'
import { useStoreActions, useStoreState } from '@/store' import { useStoreActions, useStoreState } from '@/store'

View file

@ -1,8 +0,0 @@
import CodeLayout from '@/components/developerKit/CodeLayout'
import React from 'react'
const CodePage: React.FC = () => {
return <CodeLayout />
}
export default CodePage

View file

@ -1,8 +1,8 @@
import ComponentEditor from '@/components/developerKit/ComponentEditor' import ComponentEditor from '@/views/developerKit/ComponentEditor'
import DeveloperLayout from '@/components/layouts/DeveloperLayout' import DeveloperLayout from '@/components/layouts/DeveloperLayout'
import React from 'react' import React from 'react'
const ComponentDetailPage: React.FC = () => { const ComponentEditorPage: React.FC = () => {
return ( return (
<DeveloperLayout> <DeveloperLayout>
<ComponentEditor /> <ComponentEditor />
@ -10,4 +10,4 @@ const ComponentDetailPage: React.FC = () => {
) )
} }
export default ComponentDetailPage export default ComponentEditorPage

View file

@ -16,7 +16,9 @@ import {
} from 'react-icons/fa' } from 'react-icons/fa'
import { ROUTES_ENUM } from '@/routes/route.constant' import { ROUTES_ENUM } from '@/routes/route.constant'
import { useLocalization } from '@/utils/hooks/useLocalization' import { useLocalization } from '@/utils/hooks/useLocalization'
import { Loading } from '../shared' import { Loading } from '../../components/shared'
import { APP_NAME } from '@/constants/app.constant'
import { Helmet } from 'react-helmet'
const ComponentManager: React.FC = () => { const ComponentManager: React.FC = () => {
const { components, loading, updateComponent, deleteComponent } = useComponents() const { components, loading, updateComponent, deleteComponent } = useComponents()
@ -89,24 +91,13 @@ const ComponentManager: React.FC = () => {
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<div className="flex items-center justify-between mb-4"> <Helmet
<div> titleTemplate={`%s | ${APP_NAME}`}
<h1 className="text-2xl font-bold text-slate-900"> title={translate('::' + 'App.DeveloperKit.Components')}
{translate('::App.DeveloperKit.Components')} defaultTitle={APP_NAME}
</h1> ></Helmet>
<p className="text-slate-600">{translate('::App.DeveloperKit.Component.Description')}</p>
</div>
<Link
to={ROUTES_ENUM.protected.saas.developerKit.componentsNew}
className="flex items-center gap-2 bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors"
>
<FaPlus className="w-4 h-4" />
{translate('::App.DeveloperKit.Component.New')}
</Link>
</div>
{/* Statistics Cards */} <div className="grid grid-cols-1 md:grid-cols-3 gap-6 mt-2 mb-4">
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-4">
{stats.map((stat, index) => ( {stats.map((stat, index) => (
<div key={index} className="bg-white rounded-lg border border-slate-200 p-6"> <div key={index} className="bg-white rounded-lg border border-slate-200 p-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@ -150,6 +141,15 @@ const ComponentManager: React.FC = () => {
</option> </option>
</select> </select>
</div> </div>
<div>
<Link
to={ROUTES_ENUM.protected.saas.developerKit.componentsNew}
className="flex items-center gap-2 bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors"
>
<FaPlus className="w-4 h-4" />
{translate('::App.DeveloperKit.Component.New')}
</Link>
</div>
</div> </div>
{/* Components List */} {/* Components List */}

View file

@ -1,8 +1,8 @@
import ComponentManager from '@/components/developerKit/ComponentManager' import ComponentManager from '@/views/developerKit/ComponentManager'
import DeveloperLayout from '@/components/layouts/DeveloperLayout' import DeveloperLayout from '@/components/layouts/DeveloperLayout'
import React from 'react' import React from 'react'
const ComponentPage: React.FC = () => { const ComponentManagerPage: React.FC = () => {
return ( return (
<DeveloperLayout> <DeveloperLayout>
<ComponentManager /> <ComponentManager />
@ -10,4 +10,4 @@ const ComponentPage: React.FC = () => {
) )
} }
export default ComponentPage export default ComponentManagerPage

View file

@ -0,0 +1,876 @@
import React, { useState, useEffect, useCallback } from 'react'
import axios from 'axios'
import {
FaSearch,
FaGlobe,
FaCopy,
FaCheckCircle,
FaExclamationCircle,
FaDatabase,
FaSyncAlt,
FaPaperPlane,
FaTrash,
FaBolt,
FaTable,
FaToggleOn,
FaToggleOff,
FaChevronRight,
FaChevronDown,
FaServer,
} from 'react-icons/fa'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { getDataSources } from '@/services/data-source.service'
import { sqlObjectManagerService } from '@/services/sql-query-manager.service'
import { developerKitService } from '@/services/developerKit.service'
import type { DataSourceDto } from '@/proxy/data-source'
import type { DatabaseTableDto } from '@/proxy/sql-query-manager/models'
import type { CrudEndpoint, SqlTable } from '@/proxy/developerKit/models'
import { Helmet } from 'react-helmet'
import { APP_NAME } from '@/constants/app.constant'
interface TestResult {
success: boolean
status: number
data?: unknown
error?: unknown
timestamp: string
}
interface ParameterInput {
name: string
value: string
type: 'path' | 'query' | 'body'
required: boolean
description?: string
}
// Helper: tableName -> PascalCase entity name
function toPascalCase(tableName: string): string {
const name = tableName.replace(/^.*\./g, '')
return name
.replace(/[_-]([a-z])/gi, (_: string, c: string) => c.toUpperCase())
.replace(/^([a-z])/, (c: string) => c.toUpperCase())
}
const METHOD_COLOR: Record<string, string> = {
GET: 'bg-blue-100 text-blue-800 border-blue-200',
POST: 'bg-green-100 text-green-800 border-green-200',
PUT: 'bg-yellow-100 text-yellow-800 border-yellow-200',
DELETE: 'bg-red-100 text-red-800 border-red-200',
}
const CrudEndpointManager: React.FC = () => {
const { translate } = useLocalization()
// Local entity + endpoint state (no context dependency)
const [entities, setEntities] = useState<SqlTable[]>([])
const [generatedEndpoints, setGeneratedEndpoints] = useState<CrudEndpoint[]>([])
const refreshData = useCallback(async () => {
try {
const [entitiesRes, endpointsRes] = await Promise.all([
developerKitService.getSqlTables(),
developerKitService.getGeneratedEndpoints(),
])
setEntities(entitiesRes.items || [])
setGeneratedEndpoints(endpointsRes.items || [])
} catch (err) {
console.error('Failed to refresh data', err)
}
}, [])
useEffect(() => {
refreshData()
}, [])
// Data source + tables
const [dataSources, setDataSources] = useState<DataSourceDto[]>([])
const [selectedDataSource, setSelectedDataSource] = useState<string | null>(null)
const [dbTables, setDbTables] = useState<DatabaseTableDto[]>([])
const [loadingTables, setLoadingTables] = useState(false)
// Selection
const [selectedTable, setSelectedTable] = useState<DatabaseTableDto | null>(null)
const [tableSearch, setTableSearch] = useState('')
const [crudFilter, setCrudFilter] = useState<'all' | 'with' | 'without'>('all')
// Endpoint management state
const [generatingFor, setGeneratingFor] = useState<string | null>(null)
const [deletingAll, setDeletingAll] = useState<string | null>(null)
const [togglingId, setTogglingId] = useState<string | null>(null)
const [expandedEndpoint, setExpandedEndpoint] = useState<string | null>(null)
// Test state
const [testResults, setTestResults] = useState<Record<string, TestResult>>({})
const [loadingEndpoints, setLoadingEndpoints] = useState<Set<string>>(new Set())
const [parameterValues, setParameterValues] = useState<Record<string, Record<string, string>>>({})
const [requestBodies, setRequestBodies] = useState<Record<string, string>>({})
// Load data sources on mount
useEffect(() => {
getDataSources()
.then((res) => {
const items = res.data.items || []
setDataSources(items)
if (items.length > 0) {
setSelectedDataSource(items[0].code ?? null)
}
})
.catch(console.error)
}, [])
// Load tables when datasource changes
useEffect(() => {
if (!selectedDataSource) return
setLoadingTables(true)
setDbTables([])
setSelectedTable(null)
sqlObjectManagerService
.getAllObjects(selectedDataSource)
.then((res) => {
setDbTables(res.data.tables || [])
})
.catch(console.error)
.finally(() => setLoadingTables(false))
}, [selectedDataSource])
// Helpers
const getEntityForTable = useCallback(
(tableName: string): SqlTable | undefined =>
entities.find((e) => e.tableName.toLowerCase() === tableName.toLowerCase()),
[entities],
)
const getEndpointsForTable = useCallback(
(tableName: string): CrudEndpoint[] => {
const entity = getEntityForTable(tableName)
if (!entity) return []
return generatedEndpoints.filter((ep) => ep.entityId === entity.id)
},
[entities, generatedEndpoints, getEntityForTable],
)
const activeEndpointCount = (tableName: string) =>
getEndpointsForTable(tableName).filter((ep) => ep.isActive).length
const allEndpointCount = (tableName: string) => getEndpointsForTable(tableName).length
// Filtered table list
const filteredTables = dbTables.filter((t) => {
const matchesSearch =
t.fullName.toLowerCase().includes(tableSearch.toLowerCase()) ||
t.tableName.toLowerCase().includes(tableSearch.toLowerCase())
const hasCrud = allEndpointCount(t.tableName) > 0
const matchesCrudFilter =
crudFilter === 'all' ||
(crudFilter === 'with' && hasCrud) ||
(crudFilter === 'without' && !hasCrud)
return matchesSearch && matchesCrudFilter
})
// Group by schema
const tablesBySchema = filteredTables.reduce<Record<string, DatabaseTableDto[]>>((acc, t) => {
const schema = t.schemaName || 'dbo'
if (!acc[schema]) acc[schema] = []
acc[schema].push(t)
return acc
}, {})
// Generate CRUD endpoints for selected table
const handleGenerate = async (table: DatabaseTableDto) => {
const key = table.fullName
setGeneratingFor(key)
try {
let entity = getEntityForTable(table.tableName)
if (!entity) {
const entityName = toPascalCase(table.tableName)
await developerKitService.createSqlTable({
menu: 'auto',
name: entityName,
displayName: entityName,
tableName: table.tableName,
description: `Auto-created from ${table.fullName}`,
isActive: true,
isFullAuditedEntity: false,
isMultiTenant: false,
fields: [],
})
const fresh = await developerKitService.getSqlTables()
setEntities(fresh.items || [])
entity = (fresh.items || []).find(
(e) => e.tableName.toLowerCase() === table.tableName.toLowerCase(),
)
}
if (!entity) throw new Error('Entity could not be created')
const result = await developerKitService.generateCrudEndpoints(entity.id)
setGeneratedEndpoints((prev) => [
...prev.filter((ep) => ep.entityId !== entity!.id),
...(result.items || []),
])
} catch (err) {
console.error('Generate failed', err)
} finally {
setGeneratingFor(null)
}
}
// Delete all endpoints for a table
const handleDeleteAll = async (table: DatabaseTableDto) => {
const key = table.fullName
setDeletingAll(key)
try {
const endpoints = getEndpointsForTable(table.tableName)
await Promise.all(endpoints.map((ep) => developerKitService.deleteGeneratedEndpoint(ep.id)))
const deletedIds = new Set(endpoints.map((ep) => ep.id))
setGeneratedEndpoints((prev) => prev.filter((ep) => !deletedIds.has(ep.id)))
} catch (err) {
console.error('Delete failed', err)
} finally {
setDeletingAll(null)
}
}
// Toggle single endpoint
const handleToggle = async (endpointId: string) => {
setTogglingId(endpointId)
try {
const updated = await developerKitService.toggleGeneratedEndpoint(endpointId)
setGeneratedEndpoints((prev) => prev.map((ep) => (ep.id === endpointId ? updated : ep)))
} catch (err) {
console.error('Toggle failed', err)
} finally {
setTogglingId(null)
}
}
// Test endpoint helpers
const getEndpointParameters = (endpoint: CrudEndpoint): ParameterInput[] => {
const params: ParameterInput[] = []
const vals = parameterValues[endpoint.id] || {}
switch (endpoint.operationType) {
case 'GetById':
case 'Update':
case 'Delete':
params.push({
name: 'id',
value: vals.id || '3fa85f64-5717-4562-b3fc-2c963f66afa6',
type: 'path',
required: true,
description: 'Entity ID',
})
break
case 'GetList':
params.push(
{
name: 'SkipCount',
value: vals.SkipCount || '0',
type: 'query',
required: false,
description: 'Skip count',
},
{
name: 'MaxResultCount',
value: vals.MaxResultCount || '10',
type: 'query',
required: false,
description: 'Max records',
},
)
break
}
return params
}
const needsBody = (ep: CrudEndpoint) =>
ep.operationType === 'Create' || ep.operationType === 'Update'
const getRequestBody = (ep: CrudEndpoint) => {
if (requestBodies[ep.id]) return requestBodies[ep.id]
return JSON.stringify(
{ name: 'Sample Item', description: 'Description', isActive: true },
null,
2,
)
}
const testEndpoint = async (endpoint: CrudEndpoint) => {
setLoadingEndpoints((prev) => new Set(prev).add(endpoint.id))
try {
let url = `${import.meta.env.VITE_API_URL}/api/app/crudendpoint/${endpoint.entityName?.toLowerCase()}`
const params = getEndpointParameters(endpoint)
const pathParam = params.find((p) => p.type === 'path')
if (pathParam) url += `/${pathParam.value}`
const queryParams = params.filter((p) => p.type === 'query')
if (queryParams.length) {
url += '?' + queryParams.map((p) => `${p.name}=${encodeURIComponent(p.value)}`).join('&')
}
let data = undefined
if (needsBody(endpoint)) {
try {
data = JSON.parse(getRequestBody(endpoint))
} catch {
data = {}
}
}
const res = await axios({
method: endpoint.method,
url,
timeout: 10000,
headers: { 'Content-Type': 'application/json' },
data,
})
setTestResults((prev) => ({
...prev,
[endpoint.id]: {
success: true,
status: res.status,
data: res.data,
timestamp: new Date().toISOString(),
},
}))
} catch (error: unknown) {
const axiosErr = error as { response?: { status?: number; data?: unknown }; message?: string }
setTestResults((prev) => ({
...prev,
[endpoint.id]: {
success: false,
status: axiosErr.response?.status || 0,
error: axiosErr.response?.data || axiosErr.message,
timestamp: new Date().toISOString(),
},
}))
} finally {
setLoadingEndpoints((prev) => {
const s = new Set(prev)
s.delete(endpoint.id)
return s
})
}
}
// Derived stats
const tablesWithEndpoints = dbTables.filter((t) => allEndpointCount(t.tableName) > 0).length
const totalActiveEndpoints = generatedEndpoints.filter((ep) => ep.isActive).length
const selectedTableEndpoints = selectedTable ? getEndpointsForTable(selectedTable.tableName) : []
const selectedEntity = selectedTable ? getEntityForTable(selectedTable.tableName) : undefined
return (
<div className="flex flex-col h-full gap-4">
<Helmet
titleTemplate={`%s | ${APP_NAME}`}
title={translate('::' + 'App.DeveloperKit.CrudEndpoints')}
defaultTitle={APP_NAME}
></Helmet>
{/* Stats Row */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mt-2">
<div className="bg-white rounded-lg border border-slate-200 p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-slate-600 mb-1">Toplam Tablo</p>
<p className="text-3xl font-bold text-slate-900">{dbTables.length}</p>
</div>
<div className="p-3 rounded-lg bg-blue-100">
<FaDatabase className="w-6 h-6 text-blue-600" />
</div>
</div>
</div>
<div className="bg-white rounded-lg border border-slate-200 p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-slate-600 mb-1">Endpoint Kurulu</p>
<p className="text-3xl font-bold text-slate-900">{tablesWithEndpoints}</p>
</div>
<div className="p-3 rounded-lg bg-green-100">
<FaCheckCircle className="w-6 h-6 text-green-600" />
</div>
</div>
</div>
<div className="bg-white rounded-lg border border-slate-200 p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-slate-600 mb-1">Aktif Endpoint</p>
<p className="text-3xl font-bold text-slate-900">{totalActiveEndpoints}</p>
</div>
<div className="p-3 rounded-lg bg-emerald-100">
<FaBolt className="w-6 h-6 text-emerald-600" />
</div>
</div>
</div>
<div className="bg-white rounded-lg border border-slate-200 p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-slate-600 mb-1">Veri Kaynagi</p>
<p className="text-3xl font-bold text-slate-900">{dataSources.length}</p>
</div>
<div className="p-3 rounded-lg bg-purple-100">
<FaServer className="w-6 h-6 text-purple-600" />
</div>
</div>
</div>
</div>
{/* Main two-panel layout */}
<div className="flex gap-4" style={{ height: 'calc(100vh - 250px)', minHeight: 400 }}>
{/* Left Panel: Table List */}
<div className="w-72 flex-shrink-0 flex flex-col bg-white rounded-lg border border-slate-200 overflow-hidden">
{/* DataSource selector */}
<div className="p-3 border-b border-slate-200 bg-slate-50">
<select
value={selectedDataSource || ''}
onChange={(e) => setSelectedDataSource(e.target.value)}
className="w-full px-2 py-1.5 text-sm border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent bg-white"
>
{dataSources.map((ds) => (
<option key={ds.id} value={ds.code ?? ''}>
{ds.code}
</option>
))}
{dataSources.length === 0 && <option value="">Yukleniyor...</option>}
</select>
</div>
{/* Search + CRUD filter */}
<div className="p-3 border-b border-slate-200 space-y-2">
<div className="relative">
<FaSearch className="absolute left-2.5 top-1/2 -translate-y-1/2 text-slate-400 text-xs" />
<input
type="text"
placeholder="Tablo ara..."
value={tableSearch}
onChange={(e) => setTableSearch(e.target.value)}
className="w-full pl-7 pr-3 py-1.5 text-sm border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<div className="flex rounded-lg border border-slate-200 overflow-hidden text-xs font-medium">
{(['all', 'with', 'without'] as const).map((f) => {
const labels = {
all: `Tümü (${dbTables.length})`,
with: `VAR (${dbTables.filter((t) => allEndpointCount(t.tableName) > 0).length})`,
without: `YOK (${dbTables.filter((t) => allEndpointCount(t.tableName) === 0).length})`,
}
const active = crudFilter === f
return (
<button
key={f}
onClick={() => setCrudFilter(f)}
className={`flex-1 py-1.5 transition-colors ${
active
? f === 'with'
? 'bg-green-500 text-white'
: f === 'without'
? 'bg-slate-500 text-white'
: 'bg-blue-500 text-white'
: 'bg-white text-slate-500 hover:bg-slate-50'
}`}
>
{labels[f]}
</button>
)
})}
</div>
</div>
{/* Table list */}
<div className="flex-1 overflow-y-auto">
{loadingTables ? (
<div className="flex items-center justify-center p-8 text-slate-400">
<FaSyncAlt className="animate-spin mr-2" />
<span className="text-sm">Yukleniyor...</span>
</div>
) : filteredTables.length === 0 ? (
<div className="p-6 text-center text-slate-400 text-sm">
{selectedDataSource ? 'Tablo bulunamadi' : 'Veri kaynagi secin'}
</div>
) : (
Object.entries(tablesBySchema).map(([schema, tables]) => (
<div key={schema}>
<div className="px-3 py-1.5 text-xs font-semibold text-slate-400 uppercase tracking-wide bg-slate-50 border-b border-slate-100 sticky top-0">
{schema}
</div>
{tables.map((table) => {
const active = activeEndpointCount(table.tableName)
const total = allEndpointCount(table.tableName)
const isSelected = selectedTable?.fullName === table.fullName
const hasEndpoints = total > 0
return (
<button
key={table.fullName}
onClick={() => setSelectedTable(table)}
className={`w-full flex items-center justify-between px-3 py-1 text-left hover:bg-slate-50 transition-colors border-b border-slate-50 ${
isSelected ? 'bg-blue-50 border-l-2 border-l-blue-500' : ''
}`}
>
<div className="flex items-center gap-2 min-w-0">
<FaTable
className={`flex-shrink-0 text-xs ${
hasEndpoints ? 'text-green-500' : 'text-slate-300'
}`}
/>
<span className="text-sm text-slate-700 truncate">{table.tableName}</span>
</div>
{hasEndpoints && (
<span
className={`flex-shrink-0 text-xs px-1.5 py-0.5 rounded-full font-medium ${
active > 0
? 'bg-green-100 text-green-700'
: 'bg-slate-100 text-slate-500'
}`}
>
{active}/{total}
</span>
)}
{isSelected && (
<FaChevronRight className="flex-shrink-0 text-blue-400 text-xs ml-1" />
)}
</button>
)
})}
</div>
))
)}
</div>
</div>
{/* Right Panel: Endpoint Management */}
<div className="flex-1 min-w-0 flex flex-col bg-white rounded-lg border border-slate-200 overflow-hidden">
{!selectedTable ? (
<div className="flex-1 flex flex-col items-center justify-center text-slate-400 p-8">
<FaDatabase className="text-4xl mb-3 text-slate-200" />
<p className="text-base font-medium">Soldan bir tablo secin</p>
<p className="text-sm mt-1">
Secilen tablo icin CRUD endpointlerini yonetebilirsiniz
</p>
</div>
) : (
<div className="flex flex-col h-full overflow-hidden">
{/* Table header */}
<div className="flex items-center justify-between p-4 border-b border-slate-200 bg-slate-50">
<div className="flex items-center gap-3">
<div className="bg-blue-100 text-blue-600 p-2 rounded-lg">
<FaTable />
</div>
<div>
<h4 className="text-slate-900">{selectedTable.schemaName}.{selectedTable.tableName}</h4>
</div>
{selectedEntity && (
<span className="ml-2 text-xs bg-blue-100 text-blue-700 px-2 py-1 rounded-full">
Entity: {selectedEntity.name}
</span>
)}
</div>
<div className="flex items-center gap-2">
{selectedTableEndpoints.length > 0 && (
<button
onClick={() => handleDeleteAll(selectedTable)}
disabled={deletingAll === selectedTable.fullName}
className="flex items-center gap-2 px-3 py-1.5 text-sm text-red-600 border border-red-200 rounded-lg hover:bg-red-50 disabled:opacity-50 transition-colors"
>
{deletingAll === selectedTable.fullName ? (
<FaSyncAlt className="animate-spin" />
) : (
<FaTrash />
)}
Tumunu Sil
</button>
)}
<button
onClick={() => handleGenerate(selectedTable)}
disabled={generatingFor === selectedTable.fullName}
className="flex items-center gap-2 px-4 py-1.5 text-sm font-medium bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 transition-colors"
>
{generatingFor === selectedTable.fullName ? (
<FaSyncAlt className="animate-spin" />
) : (
<FaBolt />
)}
{selectedTableEndpoints.length > 0
? 'Yeniden Olustur'
: 'CRUD Endpoint Olustur'}
</button>
</div>
</div>
{/* Endpoints list */}
<div className="flex-1 overflow-y-auto p-4">
{selectedTableEndpoints.length === 0 ? (
<div className="flex flex-col items-center justify-center py-16 text-slate-400">
<FaBolt className="text-3xl mb-3 text-slate-200" />
<p className="font-medium">Henuz endpoint yok</p>
<p className="text-sm mt-1">
{selectedEntity
? '"CRUD Endpoint Olustur" butonuna tiklayin'
: 'Bu tablo icin Sql Table otomatik olusturulacak ve 5 endpoint uretilecektir'}
</p>
</div>
) : (
<div className="space-y-3">
{selectedTableEndpoints.map((ep) => {
const isExpanded = expandedEndpoint === ep.id
const testResult = testResults[ep.id]
return (
<div
key={ep.id}
className="border border-slate-200 rounded-lg overflow-hidden"
>
{/* Endpoint row */}
<div className="flex items-center gap-3 p-3 bg-white hover:bg-slate-50 transition-colors">
{/* Toggle */}
<button
onClick={() => handleToggle(ep.id)}
disabled={togglingId === ep.id}
title={ep.isActive ? 'Devre disi birak' : 'Etkinlestir'}
className={`flex-shrink-0 text-xl transition-colors ${
ep.isActive
? 'text-green-500 hover:text-green-700'
: 'text-slate-300 hover:text-slate-500'
} disabled:opacity-40`}
>
{togglingId === ep.id ? (
<FaSyncAlt className="animate-spin text-base" />
) : ep.isActive ? (
<FaToggleOn />
) : (
<FaToggleOff />
)}
</button>
{/* Method badge */}
<span
className={`flex-shrink-0 px-2 py-0.5 text-xs font-bold rounded border ${
METHOD_COLOR[ep.method] ||
'bg-slate-100 text-slate-700 border-slate-200'
}`}
>
{ep.method}
</span>
{/* Operation */}
<div className="flex-1 min-w-0">
<span className="font-medium text-slate-800 text-sm">
{ep.operationType}
</span>
<code className="ml-3 text-xs text-slate-500 bg-slate-100 px-2 py-0.5 rounded">
{ep.path}
</code>
</div>
{/* Status */}
{ep.isActive ? (
<span className="text-xs text-green-600 font-medium">Aktif</span>
) : (
<span className="text-xs text-slate-400">Devre disi</span>
)}
{/* Copy path */}
<button
onClick={() => navigator.clipboard.writeText(ep.path)}
className="p-1.5 text-slate-400 hover:text-slate-700 transition-colors"
title="Path kopyala"
>
<FaCopy className="text-xs" />
</button>
{/* Expand */}
<button
onClick={() => setExpandedEndpoint(isExpanded ? null : ep.id)}
className="p-1.5 text-slate-400 hover:text-slate-700 transition-colors"
title="Test et / detaylar"
>
{isExpanded ? (
<FaChevronDown className="text-xs" />
) : (
<FaChevronRight className="text-xs" />
)}
</button>
</div>
{/* Expanded detail + test */}
{isExpanded && (
<div className="border-t border-slate-200 bg-slate-50 p-4 space-y-4">
{/* Parameters */}
{getEndpointParameters(ep).length > 0 && (
<div>
<p className="text-xs font-semibold text-slate-600 mb-2">
Parametreler
</p>
<div className="space-y-2">
{getEndpointParameters(ep).map((param) => (
<div key={param.name} className="flex items-center gap-2">
<span
className={`text-xs px-1.5 py-0.5 rounded font-mono ${
param.type === 'path'
? 'bg-blue-100 text-blue-700'
: 'bg-green-100 text-green-700'
}`}
>
{param.name}
</span>
<input
type="text"
value={
parameterValues[ep.id]?.[param.name] ?? param.value
}
onChange={(e) =>
setParameterValues((prev) => ({
...prev,
[ep.id]: {
...prev[ep.id],
[param.name]: e.target.value,
},
}))
}
className="flex-1 px-2 py-1 text-xs border border-slate-300 rounded focus:ring-1 focus:ring-blue-500"
/>
</div>
))}
</div>
</div>
)}
{/* Request body */}
{needsBody(ep) && (
<div>
<p className="text-xs font-semibold text-slate-600 mb-2">
Request Body (JSON)
</p>
<textarea
value={getRequestBody(ep)}
onChange={(e) =>
setRequestBodies((prev) => ({
...prev,
[ep.id]: e.target.value,
}))
}
rows={5}
className="w-full px-2 py-1.5 text-xs border border-slate-300 rounded font-mono focus:ring-1 focus:ring-blue-500 bg-white"
/>
</div>
)}
{/* Test button */}
<div className="flex items-center gap-2">
<button
onClick={() => testEndpoint(ep)}
disabled={loadingEndpoints.has(ep.id)}
className="flex items-center gap-2 bg-blue-600 text-white px-4 py-1.5 text-sm rounded-lg hover:bg-blue-700 disabled:opacity-50 transition-colors"
>
{loadingEndpoints.has(ep.id) ? (
<FaSyncAlt className="animate-spin" />
) : (
<FaPaperPlane />
)}
{loadingEndpoints.has(ep.id) ? 'Gonderiliyor...' : 'Test Et'}
</button>
{testResult && (
<button
onClick={() =>
setTestResults((prev) => {
const n = { ...prev }
delete n[ep.id]
return n
})
}
className="text-xs text-slate-500 hover:text-slate-700 px-2 py-1.5"
>
Temizle
</button>
)}
</div>
{/* Test result */}
{testResult && (
<div
className={`rounded-lg border p-3 ${
testResult.success
? 'border-green-200 bg-green-50'
: 'border-red-200 bg-red-50'
}`}
>
<div className="flex items-center gap-2 mb-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="text-xs text-slate-500 ml-auto">
{new Date(testResult.timestamp).toLocaleTimeString()}
</span>
</div>
<div className="relative">
<pre className="text-xs bg-white border border-slate-200 rounded p-2 overflow-x-auto max-h-48">
{JSON.stringify(
testResult.success ? testResult.data : testResult.error,
null,
2,
)}
</pre>
<button
onClick={() =>
navigator.clipboard.writeText(
JSON.stringify(
testResult.success ? testResult.data : testResult.error,
null,
2,
),
)
}
className="absolute top-1.5 right-1.5 p-1 text-slate-400 hover:text-slate-700"
>
<FaCopy className="text-xs" />
</button>
</div>
</div>
)}
{/* C# Code Preview */}
{ep.csharpCode && (
<div>
<div className="flex items-center justify-between mb-1">
<p className="text-xs font-semibold text-slate-600">C# Kodu</p>
<button
onClick={() => navigator.clipboard.writeText(ep.csharpCode)}
className="text-xs text-slate-400 hover:text-slate-700 flex items-center gap-1"
>
<FaCopy /> Kopyala
</button>
</div>
<pre className="text-xs bg-slate-800 text-green-300 rounded-lg p-3 overflow-x-auto max-h-48 font-mono">
{ep.csharpCode}
</pre>
</div>
)}
</div>
)}
</div>
)
})}
</div>
)}
</div>
{/* Footer info */}
{selectedTableEndpoints.length > 0 && (
<div className="border-t border-slate-200 px-2 py-1 bg-slate-50 flex items-center gap-4 text-xs text-slate-500">
<span className="flex items-center gap-1">
<FaCheckCircle className="text-green-400" />
{selectedTableEndpoints.filter((e) => e.isActive).length} aktif
</span>
<span>{selectedTableEndpoints.filter((e) => !e.isActive).length} devre disi</span>
<span className="ml-auto">
5 endpoint: GetList, GetById, Create, Update, Delete
</span>
</div>
)}
</div>
)}
</div>
</div>
</div>
)
}
export default CrudEndpointManager

View file

@ -1,16 +0,0 @@
import React from 'react'
import { EntityProvider } from '@/contexts/EntityContext'
import DeveloperLayout from '@/components/layouts/DeveloperLayout'
import CrudEndpointManager from '@/components/developerKit/CrudEndpointManager'
const CrudEndpointPage: React.FC = () => {
return (
<DeveloperLayout>
<EntityProvider>
<CrudEndpointManager />
</EntityProvider>
</DeveloperLayout>
)
}
export default CrudEndpointPage

View file

@ -1,28 +0,0 @@
import React from 'react'
import { EntityProvider } from '@/contexts/EntityContext'
import DeveloperLayout from '@/components/layouts/DeveloperLayout'
import Dashboard from '@/components/developerKit/Dashboard'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { Helmet } from 'react-helmet'
import { APP_NAME } from '@/constants/app.constant'
const DashboardPage: React.FC = () => {
const { translate } = useLocalization()
return (
<>
<Helmet
titleTemplate={`%s | ${APP_NAME}`}
title={translate('::' + 'App.DeveloperKit')}
defaultTitle={APP_NAME}
></Helmet>
<DeveloperLayout>
<EntityProvider>
<Dashboard />
</EntityProvider>
</DeveloperLayout>
</>
)
}
export default DashboardPage

View file

@ -21,6 +21,8 @@ import {
postTestCompile, postTestCompile,
TestCompileDto, TestCompileDto,
} from '@/services/dynamicService.service' } from '@/services/dynamicService.service'
import { Helmet } from 'react-helmet'
import { APP_NAME } from '@/constants/app.constant'
const DynamicAppServiceEditor: React.FC = () => { const DynamicAppServiceEditor: React.FC = () => {
// State // State
@ -254,6 +256,12 @@ namespace DynamicServices
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<Helmet
titleTemplate={`%s | ${APP_NAME}`}
title={translate('::' + 'App.DeveloperKit.DynamicServices')}
defaultTitle={APP_NAME}
></Helmet>
{/* Header */} {/* Header */}
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<div> <div>

View file

@ -1,16 +0,0 @@
import React from 'react'
import DynamicAppServiceEditor from '@/components/developerKit/DynamicAppServiceEditor'
import { useLocalization } from '@/utils/hooks/useLocalization'
import DeveloperLayout from '@/components/layouts/DeveloperLayout'
const DynamicServicePage: React.FC = () => {
const { translate } = useLocalization()
return (
<DeveloperLayout>
<DynamicAppServiceEditor />
</DeveloperLayout>
)
}
export default DynamicServicePage

View file

@ -1,16 +0,0 @@
import React from 'react'
import { EntityProvider } from '@/contexts/EntityContext'
import DeveloperLayout from '@/components/layouts/DeveloperLayout'
import EntityEditor from '@/components/developerKit/EntityEditor'
const EntityDetailPage: React.FC = () => {
return (
<DeveloperLayout>
<EntityProvider>
<EntityEditor />
</EntityProvider>
</DeveloperLayout>
)
}
export default EntityDetailPage

View file

@ -1,16 +0,0 @@
import React from 'react'
import { EntityProvider } from '@/contexts/EntityContext'
import DeveloperLayout from '@/components/layouts/DeveloperLayout'
import EntityManager from '@/components/developerKit/EntityManager'
const EntityPage: React.FC = () => {
return (
<DeveloperLayout>
<EntityProvider>
<EntityManager />
</EntityProvider>
</DeveloperLayout>
)
}
export default EntityPage

View file

@ -1,16 +0,0 @@
import React from 'react'
import { EntityProvider } from '@/contexts/EntityContext'
import DeveloperLayout from '@/components/layouts/DeveloperLayout'
import MigrationManager from '@/components/developerKit/MigrationManager'
const MigrationPage: React.FC = () => {
return (
<DeveloperLayout>
<EntityProvider>
<MigrationManager />
</EntityProvider>
</DeveloperLayout>
)
}
export default MigrationPage

View file

@ -13,15 +13,14 @@ import type {
} from '@/proxy/sql-query-manager/models' } from '@/proxy/sql-query-manager/models'
import { SqlObjectType } from '@/proxy/sql-query-manager/models' import { SqlObjectType } from '@/proxy/sql-query-manager/models'
import { sqlObjectManagerService } from '@/services/sql-query-manager.service' import { sqlObjectManagerService } from '@/services/sql-query-manager.service'
import { FaDatabase, FaPlay, FaSave, FaCloudUploadAlt, FaCode, FaTable } from 'react-icons/fa' import { FaDatabase, FaPlay, FaSave, FaCloudUploadAlt, FaCode } from 'react-icons/fa'
import { FaCheckCircle } from 'react-icons/fa' import { FaCheckCircle } from 'react-icons/fa'
import { useLocalization } from '@/utils/hooks/useLocalization' import { useLocalization } from '@/utils/hooks/useLocalization'
import SqlObjectExplorer from './components/SqlObjectExplorer' import SqlObjectExplorer from './SqlObjectExplorer'
import SqlEditor, { SqlEditorRef } from './components/SqlEditor' import SqlEditor, { SqlEditorRef } from './SqlEditor'
import SqlResultsGrid from './components/SqlResultsGrid' import SqlResultsGrid from './SqlResultsGrid'
import SqlObjectProperties from './components/SqlObjectProperties' import SqlObjectProperties from './SqlObjectProperties'
import TemplateDialog from './components/TemplateDialog' import SqlTableDesignerDialog from './SqlTableDesignerDialog'
import TableDesignerDialog from './components/TableDesignerDialog'
import { Splitter } from '@/components/codeLayout/Splitter' import { Splitter } from '@/components/codeLayout/Splitter'
import { Helmet } from 'react-helmet' import { Helmet } from 'react-helmet'
import { useStoreState } from '@/store/store' import { useStoreState } from '@/store/store'
@ -72,18 +71,12 @@ const SqlQueryManager = () => {
detectedName: '', detectedName: '',
isExistingObject: false, isExistingObject: false,
}) })
const [showTemplateDialog, setShowTemplateDialog] = useState(false)
const [showTableDesignerDialog, setShowTableDesignerDialog] = useState(false)
const [designTableData, setDesignTableData] = useState<{ schemaName: string; tableName: string } | null>(null)
const [showTemplateConfirmDialog, setShowTemplateConfirmDialog] = useState(false) const [showTemplateConfirmDialog, setShowTemplateConfirmDialog] = useState(false)
const [pendingTemplate, setPendingTemplate] = useState<{ content: string; type: string } | null>( const [pendingTemplate, setPendingTemplate] = useState<{ content: string; type: string } | null>(
null, null,
) )
const [showTableDesignerDialog, setShowTableDesignerDialog] = useState(false)
const handleDesignTable = (schemaName: string, tableName: string) => { const [designTableData, setDesignTableData] = useState<{ schemaName: string; tableName: string } | null>(null)
setDesignTableData({ schemaName, tableName })
setShowTableDesignerDialog(true)
}
useEffect(() => { useEffect(() => {
loadDataSources() loadDataSources()
@ -121,6 +114,16 @@ const SqlQueryManager = () => {
})) }))
}, []) }, [])
const handleNewTable = useCallback(() => {
setDesignTableData(null)
setShowTableDesignerDialog(true)
}, [])
const handleDesignTable = useCallback((schemaName: string, tableName: string) => {
setDesignTableData({ schemaName, tableName })
setShowTableDesignerDialog(true)
}, [])
const handleObjectSelect = useCallback( const handleObjectSelect = useCallback(
async (object: SqlObject | null, objectType: SqlObjectType | null) => { async (object: SqlObject | null, objectType: SqlObjectType | null) => {
if (state.isDirty) { if (state.isDirty) {
@ -878,12 +881,9 @@ GO`,
selectedObject={state.selectedObject} selectedObject={state.selectedObject}
onTemplateSelect={handleTemplateSelect} onTemplateSelect={handleTemplateSelect}
onShowTableColumns={handleShowTableColumns} onShowTableColumns={handleShowTableColumns}
onDesignTable={handleDesignTable}
onNewTable={() => {
setDesignTableData(null)
setShowTableDesignerDialog(true)
}}
refreshTrigger={state.refreshTrigger} refreshTrigger={state.refreshTrigger}
onNewTable={handleNewTable}
onDesignTable={handleDesignTable}
/> />
</div> </div>
</AdaptableCard> </AdaptableCard>
@ -975,27 +975,6 @@ GO`,
</div> </div>
</div> </div>
{/* Table Designer Dialog */}
<TableDesignerDialog
isOpen={showTableDesignerDialog}
onClose={() => {
setShowTableDesignerDialog(false)
setDesignTableData(null)
}}
dataSource={state.selectedDataSource}
initialTableData={designTableData}
onDeployed={() => {
setState((prev) => ({ ...prev, refreshTrigger: prev.refreshTrigger + 1 }))
}}
/>
{/* Template Dialog */}
<TemplateDialog
isOpen={showTemplateDialog}
onClose={() => setShowTemplateDialog(false)}
onUseTemplate={handleUseTemplateFromDialog}
/>
{/* Template Confirmation Dialog */} {/* Template Confirmation Dialog */}
<Dialog <Dialog
isOpen={showTemplateConfirmDialog} isOpen={showTemplateConfirmDialog}
@ -1016,6 +995,20 @@ GO`,
</div> </div>
</Dialog> </Dialog>
{/* Table Designer Dialog */}
<SqlTableDesignerDialog
isOpen={showTableDesignerDialog}
onClose={() => {
setShowTableDesignerDialog(false)
setDesignTableData(null)
}}
dataSource={state.selectedDataSource ?? ''}
initialTableData={designTableData}
onDeployed={() => {
setState((prev) => ({ ...prev, refreshTrigger: prev.refreshTrigger + 1 }))
}}
/>
{/* Save Dialog */} {/* Save Dialog */}
<Dialog <Dialog
isOpen={showSaveDialog} isOpen={showSaveDialog}

View file

@ -9,19 +9,15 @@ import {
FaCloudUploadAlt, FaCloudUploadAlt,
FaCheck, FaCheck,
FaChevronRight, FaChevronRight,
FaCog,
FaDatabase,
FaLink, FaLink,
FaEdit, FaEdit,
FaTimes, FaTimes,
FaArrowRight, FaArrowRight,
FaInfoCircle,
FaExclamationTriangle,
} from 'react-icons/fa' } from 'react-icons/fa'
import { sqlObjectManagerService } from '@/services/sql-query-manager.service' import { sqlObjectManagerService } from '@/services/sql-query-manager.service'
import { MenuService } from '@/services/menu.service' import { MenuService } from '@/services/menu.service'
import { developerKitService } from '@/services/developerKit.service' import { developerKitService } from '@/services/developerKit.service'
import type { CreateUpdateCustomEntityDto, CreateUpdateCustomEntityFieldDto } from '@/proxy/developerKit/models' import type { CreateUpdateSqlTableDto, CreateUpdateSqlTableFieldDto } from '@/proxy/developerKit/models'
import { useLocalization } from '@/utils/hooks/useLocalization' import { useLocalization } from '@/utils/hooks/useLocalization'
// ─── Types ──────────────────────────────────────────────────────────────────── // ─── Types ────────────────────────────────────────────────────────────────────
@ -482,10 +478,10 @@ function generateAlterTableSql(
return lines.join('\n') return lines.join('\n')
} }
// Map TableDesigner data-type → CustomEntityField type // Map TableDesigner data-type → SqlTableField type
function colTypeToEntityFieldType( function colTypeToEntityFieldType(
dt: SqlDataType, dt: SqlDataType,
): CreateUpdateCustomEntityFieldDto['type'] { ): CreateUpdateSqlTableFieldDto['type'] {
if (dt === 'int' || dt === 'bigint') return 'number' if (dt === 'int' || dt === 'bigint') return 'number'
if (dt === 'bit') return 'boolean' if (dt === 'bit') return 'boolean'
if (dt === 'datetime2' || dt === 'date') return 'date' if (dt === 'datetime2' || dt === 'date') return 'date'
@ -521,7 +517,7 @@ const DEFAULT_SETTINGS: TableSettings = {
// ─── Component ──────────────────────────────────────────────────────────────── // ─── Component ────────────────────────────────────────────────────────────────
const TableDesignerDialog = ({ const SqlTableDesignerDialog = ({
isOpen, isOpen,
onClose, onClose,
dataSource, dataSource,
@ -587,9 +583,9 @@ const TableDesignerDialog = ({
.catch(() => {}) .catch(() => {})
.finally(() => setColsLoading(false)) .finally(() => setColsLoading(false))
// Load CustomEntity metadata (menu, entityName, displayName, flags…) // Load SqlTable metadata (menu, entityName, displayName, flags…)
developerKitService developerKitService
.getCustomEntities() .getSqlTables()
.then((res) => { .then((res) => {
const match = (res.items ?? []).find( const match = (res.items ?? []).find(
(e) => e.tableName.toLowerCase() === initialTableData.tableName.toLowerCase(), (e) => e.tableName.toLowerCase() === initialTableData.tableName.toLowerCase(),
@ -808,19 +804,19 @@ const TableDesignerDialog = ({
// ── Deploy ───────────────────────────────────────────────────────────────── // ── Deploy ─────────────────────────────────────────────────────────────────
const syncCustomEntityMetadata = async (deployedTableName: string) => { const syncSqlTableMetadata = async (deployedTableName: string) => {
try { try {
const namedCols = columns.filter((c) => c.columnName.trim()) const namedCols = columns.filter((c) => c.columnName.trim())
// Find existing CustomEntity record by table name // Find existing SqlTable record by table name
const listResult = await developerKitService.getCustomEntities() const listResult = await developerKitService.getSqlTables()
const existing = (listResult.items ?? []).find( const existing = (listResult.items ?? []).find(
(e) => e.tableName.toLowerCase() === deployedTableName.toLowerCase(), (e) => e.tableName.toLowerCase() === deployedTableName.toLowerCase(),
) )
if (existing) { if (existing) {
// Update: keep existing metadata, sync fields (match by name to preserve IDs) // Update: keep existing metadata, sync fields (match by name to preserve IDs)
const fieldDtos: CreateUpdateCustomEntityFieldDto[] = namedCols.map((col, i) => { const fieldDtos: CreateUpdateSqlTableFieldDto[] = namedCols.map((col, i) => {
const existingField = existing.fields?.find( const existingField = existing.fields?.find(
(f) => f.name.toLowerCase() === col.columnName.trim().toLowerCase(), (f) => f.name.toLowerCase() === col.columnName.trim().toLowerCase(),
) )
@ -837,7 +833,7 @@ const TableDesignerDialog = ({
displayOrder: i, displayOrder: i,
} }
}) })
const updateDto: CreateUpdateCustomEntityDto = { const updateDto: CreateUpdateSqlTableDto = {
menu: existing.menu, menu: existing.menu,
name: existing.name, name: existing.name,
displayName: existing.displayName, displayName: existing.displayName,
@ -848,10 +844,10 @@ const TableDesignerDialog = ({
isMultiTenant: existing.isMultiTenant, isMultiTenant: existing.isMultiTenant,
fields: fieldDtos, fields: fieldDtos,
} }
await developerKitService.updateCustomEntity(existing.id, updateDto) await developerKitService.updateSqlTable(existing.id, updateDto)
} else { } else {
// Create new CustomEntity record // Create new SqlTable record
const fieldDtos: CreateUpdateCustomEntityFieldDto[] = namedCols.map((col, i) => { const fieldDtos: CreateUpdateSqlTableFieldDto[] = namedCols.map((col, i) => {
const maxLen = col.maxLength ? parseInt(col.maxLength, 10) || undefined : undefined const maxLen = col.maxLength ? parseInt(col.maxLength, 10) || undefined : undefined
return { return {
name: col.columnName.trim(), name: col.columnName.trim(),
@ -864,7 +860,7 @@ const TableDesignerDialog = ({
displayOrder: i, displayOrder: i,
} }
}) })
const createDto: CreateUpdateCustomEntityDto = { const createDto: CreateUpdateSqlTableDto = {
menu: settings.menuValue, menu: settings.menuValue,
name: settings.entityName || deployedTableName, name: settings.entityName || deployedTableName,
displayName: settings.displayName || deployedTableName, displayName: settings.displayName || deployedTableName,
@ -875,7 +871,7 @@ const TableDesignerDialog = ({
isMultiTenant: settings.isMultiTenant, isMultiTenant: settings.isMultiTenant,
fields: fieldDtos, fields: fieldDtos,
} }
await developerKitService.createCustomEntity(createDto) await developerKitService.createSqlTable(createDto)
} }
} catch { } catch {
// Silent — SQL deploy already succeeded; metadata sync failure is non-critical // Silent — SQL deploy already succeeded; metadata sync failure is non-critical
@ -900,8 +896,8 @@ const TableDesignerDialog = ({
}) })
if (result.data.success) { if (result.data.success) {
const deployedTable = settings.tableName || initialTableData?.tableName || '' const deployedTable = settings.tableName || initialTableData?.tableName || ''
// Sync entity metadata to CustomEntity / CustomEntityField tables // Sync entity metadata to SqlTable / SqlTableField tables
await syncCustomEntityMetadata(deployedTable) await syncSqlTableMetadata(deployedTable)
toast.push( toast.push(
<Notification type="success" title="Basarili"> <Notification type="success" title="Basarili">
Tablo basariyla {isEditMode ? 'güncellendi' : 'oluşturuldu'}: [dbo].[{deployedTable}] Tablo basariyla {isEditMode ? 'güncellendi' : 'oluşturuldu'}: [dbo].[{deployedTable}]
@ -1700,4 +1696,4 @@ const TableDesignerDialog = ({
) )
} }
export default TableDesignerDialog export default SqlTableDesignerDialog

View file

@ -1,292 +0,0 @@
import { useState } from 'react'
import { Dialog, Button } from '@/components/ui'
import { FaCode } from 'react-icons/fa'
import { useLocalization } from '@/utils/hooks/useLocalization'
interface TemplateDialogProps {
isOpen: boolean
onClose: () => void
onUseTemplate: (templateContent: string, templateType: string) => void
}
interface Template {
id: string
label: string
description: string
type: string
}
const templates: Template[] = [
{
id: 'select',
label: 'SELECT Query',
description: 'Basic SELECT query template',
type: 'select',
},
{
id: 'insert',
label: 'INSERT Query',
description: 'Basic INSERT query template',
type: 'insert',
},
{
id: 'update',
label: 'UPDATE Query',
description: 'Basic UPDATE query template',
type: 'update',
},
{
id: 'delete',
label: 'DELETE Query',
description: 'Basic DELETE query template',
type: 'delete',
},
{
id: 'create-procedure',
label: 'Stored Procedure',
description: 'Create Stored Procedure template',
type: 'create-procedure',
},
{
id: 'create-view',
label: 'View',
description: 'Create View template',
type: 'create-view',
},
{
id: 'create-scalar-function',
label: 'Scalar Function',
description: 'Create Scalar Function template',
type: 'create-scalar-function',
},
{
id: 'create-table-function',
label: 'Table-Valued Function',
description: 'Create Table-Valued Function template',
type: 'create-table-function',
},
]
const getTemplateContent = (templateType: string): string => {
const templateContents: Record<string, string> = {
select: `-- Basic SELECT query
SELECT
Column1,
Column2,
Column3
FROM
TableName
WHERE
-- Add your conditions
Column1 = 'value'
AND IsActive = 1
ORDER BY
Column1 ASC;`,
insert: `-- Basic INSERT query
INSERT INTO TableName (Column1, Column2, Column3)
VALUES
('Value1', 'Value2', 'Value3');`,
update: `-- Basic UPDATE query
UPDATE TableName
SET
Column1 = 'NewValue1',
Column2 = 'NewValue2'
WHERE
-- Add your conditions
Id = 1;`,
delete: `-- Basic DELETE query
DELETE FROM TableName
WHERE
-- Add your conditions
Id = 1;`,
'create-procedure': `-- Create Stored Procedure
CREATE PROCEDURE [dbo].[ProcedureName]
@Parameter1 INT,
@Parameter2 NVARCHAR(100)
AS
BEGIN
SET NOCOUNT ON;
-- Add your logic here
SELECT
Column1,
Column2
FROM
TableName
WHERE
Column1 = @Parameter1
AND Column2 = @Parameter2;
END
GO`,
'create-view': `-- Create View
CREATE VIEW [dbo].[ViewName]
AS
SELECT
t1.Column1,
t1.Column2,
t2.Column3
FROM
TableName1 t1
INNER JOIN TableName2 t2 ON t1.Id = t2.TableName1Id
WHERE
t1.IsActive = 1;
GO`,
'create-scalar-function': `-- Create Scalar Function
CREATE FUNCTION [dbo].[ScalarFunctionName]
(
@Parameter1 INT,
@Parameter2 NVARCHAR(100)
)
RETURNS NVARCHAR(200)
AS
BEGIN
DECLARE @Result NVARCHAR(200);
-- Add your logic here
SELECT @Result = Column1 + ' ' + @Parameter2
FROM TableName
WHERE Id = @Parameter1;
RETURN @Result;
END
GO`,
'create-table-function': `-- Create Table-Valued Function
CREATE FUNCTION [dbo].[TableFunctionName]
(
@Parameter1 INT,
@Parameter2 NVARCHAR(100)
)
RETURNS TABLE
AS
RETURN
(
SELECT
t.Column1,
t.Column2,
t.Column3,
t.Column4
FROM
TableName t
WHERE
t.Id = @Parameter1
AND t.Column2 LIKE '%' + @Parameter2 + '%'
AND t.IsActive = 1
)
GO`,
}
return templateContents[templateType] || templateContents['select']
}
const TemplateDialog = ({ isOpen, onClose, onUseTemplate }: TemplateDialogProps) => {
const { translate } = useLocalization()
const [selectedTemplate, setSelectedTemplate] = useState<Template | null>(null)
const handleTemplateSelect = (template: Template) => {
setSelectedTemplate(template)
}
const handleUseTemplate = () => {
if (selectedTemplate) {
const content = getTemplateContent(selectedTemplate.type)
onUseTemplate(content, selectedTemplate.type)
onClose()
setSelectedTemplate(null)
}
}
const handleClose = () => {
onClose()
setSelectedTemplate(null)
}
return (
<Dialog isOpen={isOpen} onClose={handleClose} onRequestClose={handleClose} width={900}>
<div className="flex flex-col h-[600px]">
<h5 className="mb-4 text-lg font-semibold">
{translate('::App.Platform.SelectTemplate')}
</h5>
<div className="flex flex-1 gap-4 min-h-0">
{/* Template List */}
<div className="w-1/3 border rounded-lg p-2 overflow-y-auto">
<div className="space-y-1">
{templates.map((template) => (
<div
key={template.id}
className={`flex items-start gap-3 p-3 rounded-lg cursor-pointer transition-colors ${
selectedTemplate?.id === template.id
? 'bg-blue-100 dark:bg-blue-900 border-2 border-blue-500'
: 'bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700 border-2 border-transparent'
}`}
onClick={() => handleTemplateSelect(template)}
>
<FaCode
className={`text-lg mt-1 ${
selectedTemplate?.id === template.id
? 'text-blue-600 dark:text-blue-400'
: 'text-gray-500'
}`}
/>
<div className="flex-1">
<div className="font-medium text-sm">{template.label}</div>
<div className="text-xs text-gray-600 dark:text-gray-400 mt-1">
{template.description}
</div>
</div>
</div>
))}
</div>
</div>
{/* Template Preview */}
<div className="flex-1 border rounded-lg p-4 overflow-hidden flex flex-col">
{selectedTemplate ? (
<>
<div className="mb-3">
<h6 className="font-semibold text-sm mb-1">{selectedTemplate.label}</h6>
<p className="text-xs text-gray-600 dark:text-gray-400">
{selectedTemplate.description}
</p>
</div>
<div className="flex-1 overflow-auto bg-gray-900 rounded p-3">
<pre className="text-sm text-gray-100 font-mono">
{getTemplateContent(selectedTemplate.type)}
</pre>
</div>
</>
) : (
<div className="flex items-center justify-center h-full text-gray-500 dark:text-gray-400">
{translate('::App.Platform.SelectATemplateToPreview')}
</div>
)}
</div>
</div>
{/* Action Buttons */}
<div className="flex justify-end gap-2 mt-4 pt-4 border-t">
<Button variant="plain" onClick={handleClose}>
{translate('::Cancel')}
</Button>
<Button
variant="solid"
onClick={handleUseTemplate}
disabled={!selectedTemplate}
color="blue-600"
>
{translate('::App.Platform.UseTemplate')}
</Button>
</div>
</div>
</Dialog>
)
}
export default TemplateDialog