Developer Kits düzenlemesi

This commit is contained in:
Sedat ÖZTÜRK 2025-11-05 17:52:01 +03:00
parent 2ec7f085e4
commit a0fa1b7e2b
13 changed files with 314 additions and 104 deletions

View file

@ -11,8 +11,8 @@ public class CustomEntityDto : FullAuditedEntityDto<Guid>
public string TableName { get; set; } = string.Empty;
public string? Description { get; set; }
public bool IsActive { get; set; } = true;
public bool HasAuditFields { get; set; } = true;
public bool HasSoftDelete { get; set; } = true;
public bool IsFullAuditedEntity { get; set; } = true;
public bool IsMultiTenant { get; set; } = false;
public string MigrationStatus { get; set; } = "pending";
public Guid? MigrationId { get; set; }
public string EndpointStatus { get; set; } = "pending";
@ -27,8 +27,8 @@ public class CreateUpdateCustomEntityDto
public string TableName { get; set; } = string.Empty;
public string? Description { get; set; }
public bool IsActive { get; set; } = true;
public bool HasAuditFields { get; set; } = true;
public bool HasSoftDelete { get; set; } = true;
public bool IsFullAuditedEntity { get; set; } = true;
public bool IsMultiTenant { get; set; } = false;
public List<CreateUpdateCustomEntityFieldDto> Fields { get; set; } = [];
}

View file

@ -96,9 +96,20 @@ public class CrudMigrationAppService : CrudAppService<
if (entity == null)
throw new EntityNotFoundException($"CustomEntity with id {entityId} not found");
var fileName = $"{DateTime.UtcNow:yyyyMMddHHmmss}_Create{entity.Name}Table.sql";
// 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();
var sqlScript = GenerateSqlScript(entity);
// 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
{
@ -118,7 +129,7 @@ public class CrudMigrationAppService : CrudAppService<
return ObjectMapper.Map<CrudMigration, CrudMigrationDto>(migration);
}
private string GenerateSqlScript(CustomEntity entity)
private string GenerateCreateTableScript(CustomEntity entity)
{
if (entity.Fields == null || !entity.Fields.Any())
throw new InvalidOperationException($"Entity '{entity.Name}' does not have any fields defined.");
@ -156,19 +167,22 @@ public class CrudMigrationAppService : CrudAppService<
sb.AppendLine(",");
}
if (entity.HasAuditFields)
// 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 DEFAULT SYSUTCDATETIME() NOT NULL,");
sb.AppendLine(" [LastModificationTime] DATETIME2 NULL,");
sb.AppendLine(" [LastModifierId] UNIQUEIDENTIFIER NULL,");
}
if (entity.HasSoftDelete)
{
sb.AppendLine(" [IsDeleted] BIT DEFAULT 0 NOT NULL,");
sb.AppendLine(" [DeleterId] UNIQUEIDENTIFIER NULL,");
sb.AppendLine(" [DeletionTime] DATETIME2 DEFAULT SYSUTCDATETIME() NOT NULL,");
sb.AppendLine(" [DeletionTime] DATETIME2 NULL,");
}
// Remove last comma
@ -178,6 +192,87 @@ public class CrudMigrationAppService : CrudAppService<
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

View file

@ -105,13 +105,26 @@ public class CustomEntityAppService : CrudAppService<
if (entity == null)
throw new EntityNotFoundException(typeof(CustomEntity), 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 = "pending";
entity.MigrationId = null;
}
entity.Name = input.Name;
entity.DisplayName = input.DisplayName;
entity.TableName = input.TableName;
entity.Description = input.Description;
entity.IsActive = input.IsActive;
entity.HasAuditFields = input.HasAuditFields;
entity.HasSoftDelete = input.HasSoftDelete;
entity.IsFullAuditedEntity = input.IsFullAuditedEntity;
entity.IsMultiTenant = input.IsMultiTenant;
var updatedFields = new List<CustomEntityField>();
@ -183,8 +196,8 @@ public class CustomEntityAppService : CrudAppService<
TableName = input.TableName,
Description = input.Description,
IsActive = input.IsActive,
HasAuditFields = input.HasAuditFields,
HasSoftDelete = input.HasSoftDelete,
IsFullAuditedEntity = input.IsFullAuditedEntity,
IsMultiTenant = input.IsMultiTenant,
MigrationStatus = "pending"
};
@ -212,6 +225,32 @@ public class CustomEntityAppService : CrudAppService<
return ObjectMapper.Map<CustomEntity, CustomEntityDto>(entity);
}
private bool FieldsHaveChanged(ICollection<CustomEntityField> existingFields, List<CreateUpdateCustomEntityFieldDto> inputFields)
{
if (existingFields.Count != inputFields.Count)
return true;
var existingFieldsList = existingFields.OrderBy(f => f.DisplayOrder).ToList();
var inputFieldsList = inputFields.OrderBy(f => f.DisplayOrder).ToList();
for (int i = 0; i < existingFieldsList.Count; i++)
{
var existing = existingFieldsList[i];
var input = inputFieldsList[i];
if (existing.Name != input.Name ||
existing.Type != input.Type ||
existing.IsRequired != input.IsRequired ||
existing.MaxLength != input.MaxLength ||
existing.IsUnique != input.IsUnique)
{
return true;
}
}
return false;
}
public override async Task DeleteAsync(Guid id)
{
// İlgili entity'nin var olup olmadığını kontrol et

View file

@ -14,8 +14,8 @@ public class CustomEntity : FullAuditedEntity<Guid>, IMultiTenant
public string TableName { get; set; } = string.Empty;
public string? Description { get; set; }
public bool IsActive { get; set; } = true;
public bool HasAuditFields { get; set; } = true;
public bool HasSoftDelete { get; set; } = true;
public bool IsFullAuditedEntity { get; set; } = true;
public bool IsMultiTenant { get; set; } = false;
public string MigrationStatus { get; set; } = "pending";
public Guid? MigrationId { get; set; }
public string EndpointStatus { get; set; } = "pending"; // "pending" | "applied" | "failed"

View file

@ -54,10 +54,16 @@ public class DynamicEntityManager : IDynamicEntityManager
}
}
if (entity.HasAuditFields)
if (entity.IsMultiTenant && data.TryGetProperty("TenantId", out var tenantIdValue))
{
columns.AddRange(new[] { "[CreationTime]", "[LastModificationTime]" });
values.AddRange(new[] { "SYSUTCDATETIME()", "SYSUTCDATETIME()" });
columns.Add("[TenantId]");
values.Add($"'{tenantIdValue.GetGuid()}'");
}
if (entity.IsFullAuditedEntity)
{
columns.AddRange(new[] { "[CreationTime]", "[LastModificationTime]", "[IsDeleted]" });
values.AddRange(new[] { "SYSUTCDATETIME()", "NULL", "0" });
}
var insertQuery = $"INSERT INTO [{tableName}] ({string.Join(", ", columns)}) VALUES ({string.Join(", ", values)})";
@ -86,7 +92,7 @@ public class DynamicEntityManager : IDynamicEntityManager
}
}
if (entity.HasAuditFields)
if (entity.IsFullAuditedEntity)
setParts.Add("[LastModificationTime] = SYSUTCDATETIME()");
var updateQuery = $"UPDATE [{tableName}] SET {string.Join(", ", setParts)} WHERE Id = '{id}'";
@ -107,12 +113,10 @@ public class DynamicEntityManager : IDynamicEntityManager
return false;
string deleteQuery;
if (entity.HasSoftDelete)
if (entity.IsFullAuditedEntity)
{
var parts = new List<string> { "[IsDeleted] = 1" };
if (entity.HasAuditFields)
parts.Add("[DeletionTime] = SYSUTCDATETIME()");
deleteQuery = $"UPDATE [{tableName}] SET {string.Join(", ", parts)} WHERE Id = '{id}'";
// Full audited entities always have soft delete
deleteQuery = $"UPDATE [{tableName}] SET [IsDeleted] = 1, [DeletionTime] = SYSUTCDATETIME() WHERE Id = '{id}'";
}
else
{

View file

@ -39,7 +39,7 @@ public class MsDynamicDataRepository : IDynamicDataRepository, IScopedDependency
{
transaction = await con.BeginTransactionAsync();
unitOfWorkManager.Current.AddTransactionApi(key, new DapperTransactionApi(transaction, cancellationTokenProvider));
transactions.Add(key, transaction);
transactions[key] = transaction;
unitOfWorkManager.Current.OnCompleted(() =>
{
transaction = null;
@ -56,7 +56,7 @@ public class MsDynamicDataRepository : IDynamicDataRepository, IScopedDependency
if (connection == null)
{
connection = new SqlConnection(cs);
connections.Add(key, connection);
connections[key] = connection; // Use indexer instead of Add to avoid duplicate key exception
}
if (connection.State != ConnectionState.Open)
{

View file

@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore;
namespace Kurs.Platform.Migrations
{
[DbContext(typeof(PlatformDbContext))]
[Migration("20251105111749_Initial")]
[Migration("20251105142841_Initial")]
partial class Initial
{
/// <inheritdoc />
@ -2928,12 +2928,6 @@ namespace Kurs.Platform.Migrations
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<bool>("HasAuditFields")
.HasColumnType("bit");
b.Property<bool>("HasSoftDelete")
.HasColumnType("bit");
b.Property<bool>("IsActive")
.HasColumnType("bit");
@ -2943,6 +2937,12 @@ namespace Kurs.Platform.Migrations
.HasDefaultValue(false)
.HasColumnName("IsDeleted");
b.Property<bool>("IsFullAuditedEntity")
.HasColumnType("bit");
b.Property<bool>("IsMultiTenant")
.HasColumnType("bit");
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2")
.HasColumnName("LastModificationTime");

View file

@ -2137,8 +2137,8 @@ namespace Kurs.Platform.Migrations
TableName = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
Description = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
IsActive = table.Column<bool>(type: "bit", nullable: false),
HasAuditFields = table.Column<bool>(type: "bit", nullable: false),
HasSoftDelete = 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(50)", maxLength: 50, nullable: false),
MigrationId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
EndpointStatus = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),

View file

@ -2925,12 +2925,6 @@ namespace Kurs.Platform.Migrations
.HasMaxLength(50)
.HasColumnType("nvarchar(50)");
b.Property<bool>("HasAuditFields")
.HasColumnType("bit");
b.Property<bool>("HasSoftDelete")
.HasColumnType("bit");
b.Property<bool>("IsActive")
.HasColumnType("bit");
@ -2940,6 +2934,12 @@ namespace Kurs.Platform.Migrations
.HasDefaultValue(false)
.HasColumnName("IsDeleted");
b.Property<bool>("IsFullAuditedEntity")
.HasColumnType("bit");
b.Property<bool>("IsMultiTenant")
.HasColumnType("bit");
b.Property<DateTime?>("LastModificationTime")
.HasColumnType("datetime2")
.HasColumnName("LastModificationTime");

View file

@ -485,14 +485,14 @@ const CrudEndpointManager: React.FC = () => {
{/* Endpoints List */}
{filteredEndpoints.length > 0 ? (
<div className="grid grid-cols-1 xl:grid-cols-2 gap-6">
<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-6 cursor-pointer hover:bg-slate-50 transition-colors"
className="p-4 cursor-pointer hover:bg-slate-50 transition-colors"
onClick={() => {
const newSelectedEndpoint = selectedEndpoint === endpoint.id ? null : endpoint.id
setSelectedEndpoint(newSelectedEndpoint)
@ -505,7 +505,7 @@ const CrudEndpointManager: React.FC = () => {
{/* Sol taraf */}
<div className="flex items-center gap-4">
<span
className={`px-3 py-1 text-sm font-medium rounded-full border ${getMethodColor(
className={`px-4 py-1 text-sm font-medium rounded-full border ${getMethodColor(
endpoint.method,
)}`}
>
@ -513,37 +513,33 @@ const CrudEndpointManager: React.FC = () => {
</span>
<div>
<h3 className="text-lg font-semibold text-slate-900">{endpoint.name}</h3>
<code className="text-sm bg-slate-100 text-slate-700 px-2 py-1 rounded">
{endpoint.path}
</code>
</div>
</div>
{/* Sağ taraf */}
<div className="flex items-center gap-3">
{endpoint.type === 'generated' && (
<span className="bg-emerald-100 text-emerald-700 text-xs px-2 py-1 rounded-full">
{translate('::App.DeveloperKit.Endpoint.AutoGenerated')}
</span>
)}
<div className="flex items-center gap-1">
<FaCheckCircle className="w-5 h-5 text-green-500" />
<FaCheckCircle className="w-3 h-3 text-green-500" />
<span className="text-sm text-slate-500">
{translate('::App.DeveloperKit.Endpoint.Active')}
</span>
</div>
</div>
</div>
{endpoint.description && (
<p className="text-slate-600 text-sm mt-2">{endpoint.description}</p>
)}
<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-6 bg-slate-50">
<div className="border-t border-slate-200 p-4 bg-slate-50">
<div className="grid grid-cols-1 gap-6">
{/* Request Details */}
<div>

View file

@ -40,8 +40,8 @@ const validationSchema = Yup.object({
)
.min(1, 'At least one field is required'),
isActive: Yup.boolean(),
hasAuditFields: Yup.boolean(),
hasSoftDelete: Yup.boolean(),
isFullAuditedEntity: Yup.boolean(),
isMultiTenant: Yup.boolean(),
})
const EntityEditor: React.FC = () => {
@ -72,15 +72,10 @@ const EntityEditor: React.FC = () => {
},
] as CustomEntityField[],
isActive: true,
hasAuditFields: true,
hasSoftDelete: true,
isFullAuditedEntity: true,
isMultiTenant: true,
})
// Check if migration is applied to disable certain fields
const isMigrationApplied = Boolean(
isEditing && id && getEntity(id)?.migrationStatus === 'applied',
)
useEffect(() => {
if (isEditing && id) {
const entity = getEntity(id)
@ -98,8 +93,8 @@ const EntityEditor: React.FC = () => {
description: entity.description || '',
fields: sortedFields,
isActive: entity.isActive,
hasAuditFields: entity.hasAuditFields,
hasSoftDelete: entity.hasSoftDelete,
isFullAuditedEntity: entity.isFullAuditedEntity,
isMultiTenant: entity.isMultiTenant,
})
}
}
@ -132,8 +127,8 @@ const EntityEditor: React.FC = () => {
description: values.description.trim(),
fields: sanitizedFields,
isActive: values.isActive,
hasAuditFields: values.hasAuditFields,
hasSoftDelete: values.hasSoftDelete,
isFullAuditedEntity: values.isFullAuditedEntity,
isMultiTenant: values.isMultiTenant,
}
if (isEditing && id) {
@ -208,7 +203,17 @@ const EntityEditor: React.FC = () => {
type="button"
onClick={submitForm}
disabled={isSubmitting || !isValid}
className="flex items-center gap-1 bg-gradient-to-r from-green-600 to-green-700 hover:from-green-700 hover:to-green-800 text-white font-semibold px-2 py-1.5 rounded shadow transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed text-sm"
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')}
@ -219,6 +224,25 @@ const EntityEditor: React.FC = () => {
</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">
@ -248,7 +272,6 @@ const EntityEditor: React.FC = () => {
setFieldValue('displayName', values.name)
}
}}
disabled={isMigrationApplied}
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"
/>
@ -276,7 +299,6 @@ const EntityEditor: React.FC = () => {
<Field
name="tableName"
component={Input}
disabled={isMigrationApplied}
placeholder="e.g., Products, Users, Orders"
/>
</FormItem>
@ -290,6 +312,8 @@ const EntityEditor: React.FC = () => {
name="description"
component={Input}
placeholder="Brief description of this entity"
textArea={true}
rows={3}
/>
</FormItem>
@ -297,12 +321,18 @@ const EntityEditor: React.FC = () => {
<Field name="isActive" component={Checkbox} />
</FormItem>
<FormItem label={translate('::App.DeveloperKit.EntityEditor.Audit')}>
<Field name="hasAuditFields" component={Checkbox} />
<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={translate('::App.DeveloperKit.EntityEditor.SoftDelete')}>
<Field name="hasSoftDelete" component={Checkbox} />
<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>
@ -334,19 +364,28 @@ const EntityEditor: React.FC = () => {
type: 'string',
isRequired: false,
description: '',
// Assign next sequential displayOrder
displayOrder: (values.fields?.length ?? 0) + 1,
})
}
className="flex items-center gap-1 bg-gradient-to-r from-blue-600 to-blue-700 text-white px-2 py-1.5 rounded hover:from-blue-700 hover:to-blue-800 transition-all duration-200 text-sm"
>
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">Order *</div>
<div className="col-span-1 font-bold">Order *</div>
<div className="col-span-2 font-bold">
{translate('::App.DeveloperKit.EntityEditor.FieldName')} *

View file

@ -14,14 +14,16 @@ import {
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 } = useEntities()
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
@ -77,6 +79,19 @@ const EntityManager: React.FC = () => {
}
}
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,
@ -287,17 +302,39 @@ const EntityManager: React.FC = () => {
<span className="text-sm font-medium text-slate-700">
{translate('::App.DeveloperKit.Entity.MigrationStatus')}
</span>
<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>
<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">

View file

@ -5,8 +5,8 @@ export interface CustomEntity {
tableName: string;
description?: string;
isActive: boolean;
hasAuditFields: boolean;
hasSoftDelete: boolean;
isFullAuditedEntity: boolean;
isMultiTenant: boolean;
migrationStatus: "pending" | "applied" | "failed";
endpointStatus: "pending" | "applied" | "failed";
migrationId?: string;
@ -48,8 +48,8 @@ export interface CreateUpdateCustomEntityDto {
tableName: string;
description?: string;
isActive: boolean;
hasAuditFields: boolean;
hasSoftDelete: boolean;
isFullAuditedEntity: boolean;
isMultiTenant: boolean;
fields: CreateUpdateCustomEntityFieldDto[];
}