CustomEntity ve DapperTransaction

This commit is contained in:
Sedat Öztürk 2025-11-06 21:13:35 +03:00
parent a0fa1b7e2b
commit fe38608bc7
21 changed files with 409 additions and 71 deletions

View file

@ -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;

View file

@ -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,

View file

@ -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",

View file

@ -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",

View file

@ -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": {

View file

@ -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": {

View file

@ -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": {

View file

@ -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
@ -328,7 +326,7 @@ public static class PlatformConsts
public static class DynamicServices
{
public const string DynamicService = Default + ".DynamicServices";
public const string Create = DynamicService + ".Create";
public const string Edit = DynamicService + ".Edit";
public const string Delete = DynamicService + ".Delete";
@ -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";

View file

@ -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;

View file

@ -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)
{
await DbTransaction.CommitAsync(CancellationTokenProvider.FallbackToProvider(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.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)
{
await DbTransaction.RollbackAsync(CancellationTokenProvider.FallbackToProvider(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;
}
}
}

View file

@ -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;
}
// Connection is not open, will handle outside lock
}
else
{
// Create new connection
connection = new SqlConnection(cs);
connections[key] = connection;
}
}
if (connection.State != ConnectionState.Open)
// 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,16 +255,42 @@ public class MsDynamicDataRepository : IDynamicDataRepository, IScopedDependency
{
if (disposing)
{
foreach (var connection in connections.Values)
lock (_lock)
{
if (connection != null)
// Dispose transactions first
foreach (var transaction in transactions.Values)
{
if (connection.State == ConnectionState.Open)
try
{
connection.Close();
transaction?.Dispose();
}
catch
{
// Ignore disposal errors
}
connection.Dispose();
}
transactions.Clear();
// Then dispose connections
foreach (var connection in connections.Values)
{
try
{
if (connection != null)
{
if (connection.State == ConnectionState.Open)
{
connection.Close();
}
connection.Dispose();
}
}
catch
{
// Ignore disposal errors
}
}
connections.Clear();
}
}
IsDisposed = true;

View file

@ -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");

View file

@ -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),

View file

@ -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");

View file

@ -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": {

View file

@ -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": {

View file

@ -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": {

View file

@ -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,13 +253,15 @@ 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>
</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">
@ -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>
@ -378,7 +440,7 @@ const EntityEditor: React.FC = () => {
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>

View file

@ -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
View 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)

View file

@ -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