CustomEntity ve DapperTransaction
This commit is contained in:
parent
a0fa1b7e2b
commit
fe38608bc7
21 changed files with 409 additions and 71 deletions
|
|
@ -6,6 +6,7 @@ namespace Kurs.Platform.DeveloperKit;
|
|||
|
||||
public class CustomEntityDto : FullAuditedEntityDto<Guid>
|
||||
{
|
||||
public string Menu { get; set; } = string.Empty;
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string DisplayName { get; set; } = string.Empty;
|
||||
public string TableName { get; set; } = string.Empty;
|
||||
|
|
@ -22,6 +23,7 @@ public class CustomEntityDto : FullAuditedEntityDto<Guid>
|
|||
|
||||
public class CreateUpdateCustomEntityDto
|
||||
{
|
||||
public string Menu { get; set; } = string.Empty;
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string DisplayName { get; set; } = string.Empty;
|
||||
public string TableName { get; set; } = string.Empty;
|
||||
|
|
|
|||
|
|
@ -118,6 +118,7 @@ public class CustomEntityAppService : CrudAppService<
|
|||
entity.MigrationId = null;
|
||||
}
|
||||
|
||||
entity.Menu = input.Menu;
|
||||
entity.Name = input.Name;
|
||||
entity.DisplayName = input.DisplayName;
|
||||
entity.TableName = input.TableName;
|
||||
|
|
@ -191,6 +192,7 @@ public class CustomEntityAppService : CrudAppService<
|
|||
// Entity oluştur
|
||||
var entity = new CustomEntity
|
||||
{
|
||||
Menu = input.Menu,
|
||||
Name = input.Name,
|
||||
DisplayName = input.DisplayName,
|
||||
TableName = input.TableName,
|
||||
|
|
|
|||
|
|
@ -10471,6 +10471,12 @@
|
|||
"en": "Entity Name",
|
||||
"tr": "Varlık Adı"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.DeveloperKit.EntityEditor.MenuName",
|
||||
"en": "Menu Name",
|
||||
"tr": "Menü Adı"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.DeveloperKit.EntityEditor.DisplayName",
|
||||
|
|
|
|||
|
|
@ -1419,7 +1419,7 @@
|
|||
"ParentCode": "App.Administration",
|
||||
"Code": "Abp.Identity",
|
||||
"DisplayName": "Abp.Identity",
|
||||
"Order": 2,
|
||||
"Order": 3,
|
||||
"Url": null,
|
||||
"Icon": "FcConferenceCall",
|
||||
"RequiredPermissionName": null,
|
||||
|
|
@ -1519,7 +1519,7 @@
|
|||
"ParentCode": "App.Administration",
|
||||
"Code": "App.DeveloperKit",
|
||||
"DisplayName": "App.DeveloperKit",
|
||||
"Order": 6,
|
||||
"Order": 8,
|
||||
"Url": null,
|
||||
"Icon": "FcAndroidOs",
|
||||
"RequiredPermissionName": null,
|
||||
|
|
@ -1599,7 +1599,7 @@
|
|||
"ParentCode": "App.Administration",
|
||||
"Code": "App.Reports.Management",
|
||||
"DisplayName": "App.Reports.Management",
|
||||
"Order": 7,
|
||||
"Order": 6,
|
||||
"Url": null,
|
||||
"Icon": "FcDocument",
|
||||
"RequiredPermissionName": null,
|
||||
|
|
@ -1629,7 +1629,7 @@
|
|||
"ParentCode": "App.Administration",
|
||||
"Code": "App.Public",
|
||||
"DisplayName": "App.Public",
|
||||
"Order": 8,
|
||||
"Order": 7,
|
||||
"Url": null,
|
||||
"Icon": "FcGenealogy",
|
||||
"RequiredPermissionName": null,
|
||||
|
|
@ -1739,7 +1739,7 @@
|
|||
"ParentCode": "App.Administration",
|
||||
"Code": "App.Definitions",
|
||||
"DisplayName": "App.Definitions",
|
||||
"Order": 9,
|
||||
"Order": 2,
|
||||
"Url": null,
|
||||
"Icon": "FcFilingCabinet",
|
||||
"RequiredPermissionName": null,
|
||||
|
|
@ -1749,7 +1749,7 @@
|
|||
"ParentCode": "App.Administration",
|
||||
"Code": "App.Files",
|
||||
"DisplayName": "App.Files",
|
||||
"Order": 5,
|
||||
"Order": 4,
|
||||
"Url": "/admin/files",
|
||||
"Icon": "FcFolder",
|
||||
"RequiredPermissionName": "App.Files",
|
||||
|
|
@ -1825,7 +1825,6 @@
|
|||
"RequiredPermissionName": "App.Definitions.District",
|
||||
"IsDisabled": false
|
||||
},
|
||||
|
||||
{
|
||||
"ParentCode": "App.Definitions",
|
||||
"Code": "App.Definitions.WorkHour",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"ConnectionStrings": {
|
||||
"SqlServer": "Server=sql;Database=Erp;User Id=sa;password=NvQp8s@l;Trusted_Connection=False;Encrypt=False;TrustServerCertificate=True;Connection Timeout=60;",
|
||||
"SqlServer": "Server=sql;Database=Erp;User Id=sa;password=NvQp8s@l;Trusted_Connection=False;Encrypt=False;TrustServerCertificate=True;Connection Timeout=60;MultipleActiveResultSets=true;",
|
||||
"PostgreSql": "User ID=sa;Password=NvQp8s@l;Host=postgres;Port=5432;Database=Erp;"
|
||||
},
|
||||
"Redis": {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"ConnectionStrings": {
|
||||
"SqlServer": "Server=sql;Database=Erp;User Id=sa;password=NvQp8s@l;Trusted_Connection=False;Encrypt=False;TrustServerCertificate=True;Connection Timeout=60;",
|
||||
"SqlServer": "Server=sql;Database=Erp;User Id=sa;password=NvQp8s@l;Trusted_Connection=False;Encrypt=False;TrustServerCertificate=True;Connection Timeout=60;MultipleActiveResultSets=true;",
|
||||
"PostgreSql": "User ID=sa;Password=NvQp8s@l;Host=postgres;Port=5432;Database=Erp;"
|
||||
},
|
||||
"Redis": {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"Seed": false,
|
||||
"ConnectionStrings": {
|
||||
"SqlServer": "Server=localhost;Database=Erp;User Id=sa;password=NvQp8s@l;Trusted_Connection=False;Encrypt=False;TrustServerCertificate=True;Connection Timeout=60;",
|
||||
"SqlServer": "Server=localhost;Database=Erp;User Id=sa;password=NvQp8s@l;Trusted_Connection=False;Encrypt=False;TrustServerCertificate=True;Connection Timeout=60;MultipleActiveResultSets=true;",
|
||||
"PostgreSql": "User ID=sa;Password=NvQp8s@l;Host=localhost;Port=5432;Database=Erp;"
|
||||
},
|
||||
"Redis": {
|
||||
|
|
|
|||
|
|
@ -283,8 +283,6 @@ public static class PlatformConsts
|
|||
public static class AppCodes
|
||||
{
|
||||
public const string Home = Prefix.App + ".Home";
|
||||
|
||||
//Saas
|
||||
public const string Saas = Prefix.App + ".Saas";
|
||||
public const string Branches = Prefix.App + ".Branches";
|
||||
public static class Settings
|
||||
|
|
@ -338,14 +336,10 @@ public static class PlatformConsts
|
|||
public const string ViewCode = DynamicService + ".ViewCode";
|
||||
}
|
||||
}
|
||||
|
||||
public const string Blog = Prefix.App + ".Blog";
|
||||
public const string Forum = Prefix.App + ".Forum";
|
||||
|
||||
//Administration
|
||||
public const string Administration = Prefix.App + ".Administration";
|
||||
public const string Setting = Prefix.App + ".Setting";
|
||||
|
||||
public static class IdentityManagement
|
||||
{
|
||||
public const string ClaimTypes = Prefix.App + ".ClaimType";
|
||||
|
|
@ -393,8 +387,6 @@ public static class PlatformConsts
|
|||
public const string Class = Default + ".Class";
|
||||
public const string Level = Default + ".Level";
|
||||
}
|
||||
|
||||
//Hr
|
||||
public static class Hr
|
||||
{
|
||||
public const string Default = Prefix.App + ".Hr";
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ public class CustomEntity : FullAuditedEntity<Guid>, IMultiTenant
|
|||
{
|
||||
public virtual Guid? TenantId { get; protected set; }
|
||||
|
||||
public string Menu { get; set; } = string.Empty;
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string DisplayName { get; set; } = string.Empty;
|
||||
public string TableName { get; set; } = string.Empty;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
using System.Data.Common;
|
||||
using System.Data;
|
||||
using System.Data.Common;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Volo.Abp.Threading;
|
||||
|
|
@ -9,6 +10,7 @@ namespace Kurs.Platform.Domain.DynamicData;
|
|||
public class DapperTransactionApi : ITransactionApi, ISupportsRollback
|
||||
{
|
||||
public DbTransaction DbTransaction { get; }
|
||||
private bool _isCompleted;
|
||||
|
||||
protected ICancellationTokenProvider CancellationTokenProvider { get; }
|
||||
|
||||
|
|
@ -18,20 +20,82 @@ public class DapperTransactionApi : ITransactionApi, ISupportsRollback
|
|||
{
|
||||
DbTransaction = dbTransaction;
|
||||
CancellationTokenProvider = cancellationTokenProvider;
|
||||
_isCompleted = false;
|
||||
}
|
||||
|
||||
public async Task CommitAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
// Check if transaction is still active
|
||||
if (_isCompleted)
|
||||
{
|
||||
return; // Already completed, nothing to do
|
||||
}
|
||||
|
||||
// Check if connection is still open and transaction is not disposed
|
||||
if (DbTransaction?.Connection == null || DbTransaction.Connection.State != ConnectionState.Open)
|
||||
{
|
||||
_isCompleted = true;
|
||||
return; // Connection closed or transaction disposed
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await DbTransaction.CommitAsync(CancellationTokenProvider.FallbackToProvider(cancellationToken));
|
||||
_isCompleted = true;
|
||||
}
|
||||
catch (System.InvalidOperationException)
|
||||
{
|
||||
// Transaction already completed or disposed
|
||||
_isCompleted = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
DbTransaction.Dispose();
|
||||
if (!_isCompleted && DbTransaction?.Connection != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
// If not completed, rollback before disposing
|
||||
DbTransaction?.Rollback();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore rollback errors during disposal
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isCompleted = true;
|
||||
}
|
||||
}
|
||||
|
||||
DbTransaction?.Dispose();
|
||||
}
|
||||
|
||||
public async Task RollbackAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
// Check if transaction is still active
|
||||
if (_isCompleted)
|
||||
{
|
||||
return; // Already completed, nothing to do
|
||||
}
|
||||
|
||||
// Check if connection is still open and transaction is not disposed
|
||||
if (DbTransaction?.Connection == null || DbTransaction.Connection.State != ConnectionState.Open)
|
||||
{
|
||||
_isCompleted = true;
|
||||
return; // Connection closed or transaction disposed
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await DbTransaction.RollbackAsync(CancellationTokenProvider.FallbackToProvider(cancellationToken));
|
||||
_isCompleted = true;
|
||||
}
|
||||
catch (System.InvalidOperationException)
|
||||
{
|
||||
// Transaction already completed or disposed
|
||||
_isCompleted = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,8 +17,10 @@ public class MsDynamicDataRepository : IDynamicDataRepository, IScopedDependency
|
|||
{
|
||||
private readonly IUnitOfWorkManager unitOfWorkManager;
|
||||
private readonly ICancellationTokenProvider cancellationTokenProvider;
|
||||
private Dictionary<string, DbTransaction> transactions;
|
||||
private Dictionary<string, SqlConnection> connections;
|
||||
private readonly Dictionary<string, DbTransaction> transactions;
|
||||
private readonly Dictionary<string, SqlConnection> connections;
|
||||
private readonly HashSet<string> registeredTransactions; // Track registered transactions
|
||||
private readonly object _lock = new object();
|
||||
public bool IsDisposed { get; private set; }
|
||||
|
||||
public MsDynamicDataRepository(
|
||||
|
|
@ -27,41 +29,159 @@ public class MsDynamicDataRepository : IDynamicDataRepository, IScopedDependency
|
|||
{
|
||||
this.unitOfWorkManager = unitOfWorkManager;
|
||||
this.cancellationTokenProvider = cancellationTokenProvider;
|
||||
transactions = [];
|
||||
connections = [];
|
||||
transactions = new Dictionary<string, DbTransaction>();
|
||||
connections = new Dictionary<string, SqlConnection>();
|
||||
registeredTransactions = new HashSet<string>();
|
||||
}
|
||||
|
||||
private async Task<DbTransaction> GetOrCreateTransactionAsync(SqlConnection con)
|
||||
{
|
||||
var key = $"Dapper_{con.ConnectionString}";
|
||||
var transaction = transactions.GetOrDefault(key);
|
||||
if (transaction == null || transaction.Connection == null)
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
transaction = await con.BeginTransactionAsync();
|
||||
unitOfWorkManager.Current.AddTransactionApi(key, new DapperTransactionApi(transaction, cancellationTokenProvider));
|
||||
transactions[key] = transaction;
|
||||
// Check if we have a valid transaction for this connection
|
||||
if (transactions.TryGetValue(key, out var transaction))
|
||||
{
|
||||
// Validate transaction is still usable
|
||||
if (transaction?.Connection != null &&
|
||||
transaction.Connection == con &&
|
||||
transaction.Connection.State == ConnectionState.Open)
|
||||
{
|
||||
return transaction;
|
||||
}
|
||||
|
||||
// Invalid transaction, remove it
|
||||
try { transaction?.Dispose(); } catch { }
|
||||
transactions.Remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure connection is open
|
||||
if (con.State != ConnectionState.Open)
|
||||
{
|
||||
await con.OpenAsync();
|
||||
}
|
||||
|
||||
// Create new transaction
|
||||
var newTransaction = await con.BeginTransactionAsync();
|
||||
bool shouldRegister = false;
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
transactions[key] = newTransaction;
|
||||
|
||||
// Only register with UnitOfWork once per transaction key
|
||||
if (!registeredTransactions.Contains(key))
|
||||
{
|
||||
registeredTransactions.Add(key);
|
||||
shouldRegister = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Register with UnitOfWork if available and not already registered
|
||||
if (shouldRegister && unitOfWorkManager.Current != null)
|
||||
{
|
||||
unitOfWorkManager.Current.AddTransactionApi(key, new DapperTransactionApi(newTransaction, cancellationTokenProvider));
|
||||
|
||||
unitOfWorkManager.Current.OnCompleted(() =>
|
||||
{
|
||||
transaction = null;
|
||||
lock (_lock)
|
||||
{
|
||||
transactions.Remove(key);
|
||||
registeredTransactions.Remove(key);
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
}
|
||||
return transaction;
|
||||
|
||||
return newTransaction;
|
||||
}
|
||||
|
||||
private async Task<SqlConnection> GetOrCreateConnectionAsync(string cs)
|
||||
{
|
||||
SqlConnection connection;
|
||||
var key = $"Dapper_{cs}";
|
||||
var connection = connections.GetOrDefault(key);
|
||||
if (connection == null)
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
connection = new SqlConnection(cs);
|
||||
connections[key] = connection; // Use indexer instead of Add to avoid duplicate key exception
|
||||
// Check if we have an existing connection
|
||||
if (connections.TryGetValue(key, out connection))
|
||||
{
|
||||
// Connection exists, check its state
|
||||
if (connection.State == ConnectionState.Open)
|
||||
{
|
||||
return connection;
|
||||
}
|
||||
if (connection.State != ConnectionState.Open)
|
||||
|
||||
// Connection is not open, will handle outside lock
|
||||
}
|
||||
else
|
||||
{
|
||||
// Create new connection
|
||||
connection = new SqlConnection(cs);
|
||||
connections[key] = connection;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle connection state outside of lock
|
||||
try
|
||||
{
|
||||
if (connection.State == ConnectionState.Broken)
|
||||
{
|
||||
connection.Close();
|
||||
}
|
||||
|
||||
if (connection.State == ConnectionState.Closed)
|
||||
{
|
||||
await connection.OpenAsync();
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// If connection failed, create a new one
|
||||
lock (_lock)
|
||||
{
|
||||
connections.Remove(key);
|
||||
}
|
||||
|
||||
connection = new SqlConnection(cs);
|
||||
lock (_lock)
|
||||
{
|
||||
connections[key] = connection;
|
||||
}
|
||||
await connection.OpenAsync();
|
||||
}
|
||||
|
||||
// Register cleanup on UnitOfWork completion (only once)
|
||||
if (unitOfWorkManager.Current != null)
|
||||
{
|
||||
unitOfWorkManager.Current.OnCompleted(async () =>
|
||||
{
|
||||
SqlConnection conn = null;
|
||||
lock (_lock)
|
||||
{
|
||||
if (connections.TryGetValue(key, out conn))
|
||||
{
|
||||
connections.Remove(key);
|
||||
}
|
||||
}
|
||||
|
||||
if (conn != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (conn.State != ConnectionState.Closed)
|
||||
{
|
||||
await conn.CloseAsync();
|
||||
}
|
||||
conn.Dispose();
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return connection;
|
||||
}
|
||||
|
||||
|
|
@ -135,7 +255,26 @@ public class MsDynamicDataRepository : IDynamicDataRepository, IScopedDependency
|
|||
{
|
||||
if (disposing)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
// Dispose transactions first
|
||||
foreach (var transaction in transactions.Values)
|
||||
{
|
||||
try
|
||||
{
|
||||
transaction?.Dispose();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore disposal errors
|
||||
}
|
||||
}
|
||||
transactions.Clear();
|
||||
|
||||
// Then dispose connections
|
||||
foreach (var connection in connections.Values)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (connection != null)
|
||||
{
|
||||
|
|
@ -146,6 +285,13 @@ public class MsDynamicDataRepository : IDynamicDataRepository, IScopedDependency
|
|||
connection.Dispose();
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore disposal errors
|
||||
}
|
||||
}
|
||||
connections.Clear();
|
||||
}
|
||||
}
|
||||
IsDisposed = true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore;
|
|||
namespace Kurs.Platform.Migrations
|
||||
{
|
||||
[DbContext(typeof(PlatformDbContext))]
|
||||
[Migration("20251105142841_Initial")]
|
||||
[Migration("20251106171552_Initial")]
|
||||
partial class Initial
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
|
@ -2951,6 +2951,9 @@ namespace Kurs.Platform.Migrations
|
|||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("LastModifierId");
|
||||
|
||||
b.Property<string>("Menu")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<Guid?>("MigrationId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
|
|
@ -2132,6 +2132,7 @@ namespace Kurs.Platform.Migrations
|
|||
{
|
||||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
TenantId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
Menu = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
Name = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
|
||||
DisplayName = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
|
||||
TableName = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
|
||||
|
|
@ -2948,6 +2948,9 @@ namespace Kurs.Platform.Migrations
|
|||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("LastModifierId");
|
||||
|
||||
b.Property<string>("Menu")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<Guid?>("MigrationId")
|
||||
.HasColumnType("uniqueidentifier");
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
"BaseDomain": "sozsoft.com"
|
||||
},
|
||||
"ConnectionStrings": {
|
||||
"SqlServer": "Server=sql;Database=Erp;User Id=sa;password=NvQp8s@l;Trusted_Connection=False;Encrypt=False;TrustServerCertificate=True;Connection Timeout=60;",
|
||||
"SqlServer": "Server=sql;Database=Erp;User Id=sa;password=NvQp8s@l;Trusted_Connection=False;Encrypt=False;TrustServerCertificate=True;Connection Timeout=60;MultipleActiveResultSets=true;",
|
||||
"PostgreSql": "User ID=sa;Password=NvQp8s@l;Host=postgres;Port=5432;Database=Erp;"
|
||||
},
|
||||
"Redis": {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
"BaseDomain": "sozsoft.com"
|
||||
},
|
||||
"ConnectionStrings": {
|
||||
"SqlServer": "Server=sql;Database=Erp;User Id=sa;password=NvQp8s@l;Trusted_Connection=False;Encrypt=False;TrustServerCertificate=True;Connection Timeout=60;",
|
||||
"SqlServer": "Server=sql;Database=Erp;User Id=sa;password=NvQp8s@l;Trusted_Connection=False;Encrypt=False;TrustServerCertificate=True;Connection Timeout=60;MultipleActiveResultSets=true;",
|
||||
"PostgreSql": "User ID=sa;Password=NvQp8s@l;Host=postgres;Port=5432;Database=Erp;"
|
||||
},
|
||||
"Redis": {
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
"Version": "1.0.1"
|
||||
},
|
||||
"ConnectionStrings": {
|
||||
"SqlServer": "Server=localhost;Database=Erp;User Id=sa;password=NvQp8s@l;Trusted_Connection=False;Encrypt=False;TrustServerCertificate=True;Connection Timeout=60;",
|
||||
"SqlServer": "Server=localhost;Database=Erp;User Id=sa;password=NvQp8s@l;Trusted_Connection=False;Encrypt=False;TrustServerCertificate=True;Connection Timeout=60;MultipleActiveResultSets=true;",
|
||||
"PostgreSql": "User ID=sa;Password=NvQp8s@l;Host=localhost;Port=5432;Database=Erp;"
|
||||
},
|
||||
"Redis": {
|
||||
|
|
|
|||
|
|
@ -18,9 +18,11 @@ 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 '@/shared/types'
|
||||
import { MenuPrefixEnum, menuPrefixValues, toPrefix } from '@/types/menu'
|
||||
|
||||
// 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'),
|
||||
|
|
@ -55,6 +57,7 @@ const EntityEditor: React.FC = () => {
|
|||
|
||||
// Initial values for Formik
|
||||
const [initialValues, setInitialValues] = useState({
|
||||
menu: '',
|
||||
name: '',
|
||||
displayName: '',
|
||||
tableName: '',
|
||||
|
|
@ -87,6 +90,7 @@ const EntityEditor: React.FC = () => {
|
|||
.map((f, idx) => ({ ...f, displayOrder: f.displayOrder ?? idx + 1 }))
|
||||
|
||||
setInitialValues({
|
||||
menu: entity.menu,
|
||||
name: entity.name,
|
||||
displayName: entity.displayName,
|
||||
tableName: entity.tableName,
|
||||
|
|
@ -121,6 +125,7 @@ const EntityEditor: React.FC = () => {
|
|||
})
|
||||
|
||||
const entityData = {
|
||||
menu: values.menu.trim(),
|
||||
name: values.name.trim(),
|
||||
displayName: values.displayName.trim(),
|
||||
tableName: values.tableName.trim(),
|
||||
|
|
@ -146,6 +151,18 @@ const EntityEditor: React.FC = () => {
|
|||
}
|
||||
}
|
||||
|
||||
// Helper function to generate table name
|
||||
const generateTableName = (menuValue: string, entityName: string): string => {
|
||||
if (!menuValue || !entityName) return ''
|
||||
try {
|
||||
const menuPrefix = toPrefix(menuValue as MenuPrefixEnum)
|
||||
return `${menuPrefix}_D_${entityName}`
|
||||
} catch (error) {
|
||||
console.error('Error generating table name:', error)
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
const fieldTypes = [
|
||||
{ value: 'string', label: 'String' },
|
||||
{ value: 'number', label: 'Number' },
|
||||
|
|
@ -236,7 +253,9 @@ const EntityEditor: React.FC = () => {
|
|||
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.
|
||||
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>
|
||||
|
|
@ -254,6 +273,33 @@ const EntityEditor: React.FC = () => {
|
|||
</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={menuPrefixValues}
|
||||
isClearable={true}
|
||||
value={menuPrefixValues.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)}
|
||||
|
|
@ -265,11 +311,15 @@ const EntityEditor: React.FC = () => {
|
|||
{...field}
|
||||
onBlur={(e) => {
|
||||
field.onBlur(e)
|
||||
if (!values.tableName) {
|
||||
setFieldValue('tableName', values.name + 's')
|
||||
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)
|
||||
}
|
||||
if (!values.displayName) {
|
||||
setFieldValue('displayName', values.name)
|
||||
// Update display name if empty
|
||||
if (entityName && !values.displayName) {
|
||||
setFieldValue('displayName', entityName)
|
||||
}
|
||||
}}
|
||||
placeholder="e.g., Product, User, Order"
|
||||
|
|
@ -279,18 +329,6 @@ const EntityEditor: React.FC = () => {
|
|||
</Field>
|
||||
</FormItem>
|
||||
|
||||
<FormItem
|
||||
label={translate('::App.DeveloperKit.EntityEditor.DisplayName')}
|
||||
invalid={!!(errors.displayName && touched.displayName)}
|
||||
errorMessage={errors.displayName as string}
|
||||
>
|
||||
<Field
|
||||
name="displayName"
|
||||
component={Input}
|
||||
placeholder="e.g., Product, User, Order"
|
||||
/>
|
||||
</FormItem>
|
||||
|
||||
<FormItem
|
||||
label={translate('::App.DeveloperKit.EntityEditor.TableName')}
|
||||
invalid={!!(errors.tableName && touched.tableName)}
|
||||
|
|
@ -299,8 +337,31 @@ const EntityEditor: React.FC = () => {
|
|||
<Field
|
||||
name="tableName"
|
||||
component={Input}
|
||||
placeholder="e.g., Products, Users, Orders"
|
||||
placeholder="e.g., Adm_D_Product, Net_D_User"
|
||||
disabled={true}
|
||||
/>
|
||||
<p className="text-xs text-slate-500 mt-1">
|
||||
Format:{' '}
|
||||
{values.menu
|
||||
? `${toPrefix(values.menu as MenuPrefixEnum)}_D_`
|
||||
: '[Prefix]_D_'}
|
||||
EntityName
|
||||
</p>
|
||||
</FormItem>
|
||||
|
||||
<FormItem
|
||||
label={translate('::App.DeveloperKit.EntityEditor.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
|
||||
|
|
@ -313,7 +374,7 @@ const EntityEditor: React.FC = () => {
|
|||
component={Input}
|
||||
placeholder="Brief description of this entity"
|
||||
textArea={true}
|
||||
rows={3}
|
||||
rows={2}
|
||||
/>
|
||||
</FormItem>
|
||||
|
||||
|
|
@ -324,7 +385,8 @@ const EntityEditor: React.FC = () => {
|
|||
<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
|
||||
Includes CreationTime, CreatorId, LastModificationTime, LastModifierId,
|
||||
IsDeleted, DeleterId, DeletionTime
|
||||
</p>
|
||||
</FormItem>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
export interface CustomEntity {
|
||||
id: string;
|
||||
menu: string;
|
||||
name: string;
|
||||
displayName: string;
|
||||
tableName: string;
|
||||
|
|
@ -43,6 +44,7 @@ export interface CreateUpdateCustomEntityFieldDto {
|
|||
}
|
||||
|
||||
export interface CreateUpdateCustomEntityDto {
|
||||
menu: string;
|
||||
name: string;
|
||||
displayName: string;
|
||||
tableName: string;
|
||||
|
|
|
|||
55
ui/src/types/menu.ts
Normal file
55
ui/src/types/menu.ts
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import { enumToList } from "@/utils/enumUtils";
|
||||
|
||||
export enum MenuPrefixEnum {
|
||||
Platform = "Platform",
|
||||
Saas = "Saas",
|
||||
Administration = "Administration",
|
||||
Intranet = "Intranet",
|
||||
Participant = "Participant",
|
||||
Coordinator = "Coordinator",
|
||||
Crm = "Crm",
|
||||
SupplyChain = "SupplyChain",
|
||||
Maintenance = "Maintenance",
|
||||
Warehouse = "Warehouse",
|
||||
Project = "Project",
|
||||
Hr = "Hr",
|
||||
Mrp = "Mrp",
|
||||
Accounting = "Accounting"
|
||||
}
|
||||
|
||||
export function toPrefix(menu: MenuPrefixEnum): string {
|
||||
switch (menu) {
|
||||
case MenuPrefixEnum.Platform:
|
||||
return "Plat";
|
||||
case MenuPrefixEnum.Saas:
|
||||
return "Sas";
|
||||
case MenuPrefixEnum.Administration:
|
||||
return "Adm";
|
||||
case MenuPrefixEnum.Intranet:
|
||||
return "Net";
|
||||
case MenuPrefixEnum.Participant:
|
||||
return "Prt";
|
||||
case MenuPrefixEnum.Coordinator:
|
||||
return "Crd";
|
||||
case MenuPrefixEnum.Crm:
|
||||
return "Crm";
|
||||
case MenuPrefixEnum.SupplyChain:
|
||||
return "Scp";
|
||||
case MenuPrefixEnum.Maintenance:
|
||||
return "Mnt";
|
||||
case MenuPrefixEnum.Warehouse:
|
||||
return "Wh";
|
||||
case MenuPrefixEnum.Project:
|
||||
return "Prj";
|
||||
case MenuPrefixEnum.Hr:
|
||||
return "Hr";
|
||||
case MenuPrefixEnum.Mrp:
|
||||
return "Mrp";
|
||||
case MenuPrefixEnum.Accounting:
|
||||
return "Acc";
|
||||
default:
|
||||
throw new Error(`Unhandled menu prefix: ${menu}`);
|
||||
}
|
||||
}
|
||||
|
||||
export const menuPrefixValues = enumToList<string>(MenuPrefixEnum)
|
||||
|
|
@ -279,7 +279,7 @@ function TenantConnectionString({
|
|||
'value',
|
||||
'Server=sql;Database=' +
|
||||
name +
|
||||
';User Id=sa;password=@Password;Trusted_Connection=False;TrustServerCertificate=True;',
|
||||
';User Id=sa;password=@Password;Trusted_Connection=False;TrustServerCertificate=True;Connection Timeout=60;MultipleActiveResultSets=true;',
|
||||
)
|
||||
else if (option?.value == 2)
|
||||
//MsSql
|
||||
|
|
|
|||
Loading…
Reference in a new issue