diff --git a/api/src/Sozsoft.Platform.DbMigrator/Seeds/LanguagesData.json b/api/src/Sozsoft.Platform.DbMigrator/Seeds/LanguagesData.json index c7ec54e..996ecd3 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/Seeds/LanguagesData.json +++ b/api/src/Sozsoft.Platform.DbMigrator/Seeds/LanguagesData.json @@ -3642,6 +3642,36 @@ "en": "The record was deleted", "tr": "Kayıt silindi" }, + { + "resourceName": "Platform", + "key": "TumKayitlarSilindi", + "en": "All records were deleted.", + "tr": "Tüm kayıtlar silindi." + }, + { + "resourceName": "Platform", + "key": "SeciliKayitBekliyor", + "en": "The selected record is not waiting for this approval step or approval user.", + "tr": "Seçili kayit bu onay adımında veya onay kullanıcısında beklemiyor." + }, + { + "resourceName": "Platform", + "key": "WorkflowAlreadyStarted", + "en": "Workflow has already been started for the selected record", + "tr": "Seçili kayıt icin workflow zaten başlamış." + }, + { + "resourceName": "Platform", + "key": "SeciliKayitlarSilmekIstiyormusunuz", + "en": "{0} records will be deleted. Are you sure you want to delete?", + "tr": "{0} kayit silinecek. Silmek istediginize emin misiniz?" + }, + { + "resourceName": "Platform", + "key": "TumKayitlariSilmekIstiyormusunuz", + "en": "Are you sure to delete all {0} records?", + "tr": "Tüm {0} kayıtları silmek istediğinize emin misiniz?" + }, { "resourceName": "Platform", "key": "KayitEklendi", @@ -16736,6 +16766,12 @@ "en": "Approver", "tr": "Onayla" }, + { + "resourceName": "Platform", + "key": "App.Listform.ListformField.Rejecter", + "en": "Rejecter", + "tr": "Reddet" + }, { "resourceName": "Platform", "key": "App.Listform.ListformField.NextOnStart", diff --git a/api/src/Sozsoft.Platform.DbMigrator/Seeds/ListFormSeeder_DefaultJsons.cs b/api/src/Sozsoft.Platform.DbMigrator/Seeds/ListFormSeeder_DefaultJsons.cs index d33065f..41a494f 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/Seeds/ListFormSeeder_DefaultJsons.cs +++ b/api/src/Sozsoft.Platform.DbMigrator/Seeds/ListFormSeeder_DefaultJsons.cs @@ -12,7 +12,7 @@ public static class ListFormSeeder_DefaultJsons { public static string DefaultDeleteCommand(string tableName) { - return $"UPDATE \"{TableNameResolver.GetFullTableName(tableName)}\" SET \"DeleterId\"=@DeleterId, \"DeletionTime\"=CURRENT_TIMESTAMP, \"IsDeleted\"='true' WHERE \"Id\"=@Id"; + return $"UPDATE \"{TableNameResolver.GetFullTableName(tableName)}\" SET \"DeleterId\"=@DeleterId, \"DeletionTime\"=CURRENT_TIMESTAMP, \"IsDeleted\"='true' WHERE \"Id\" IN @Id"; } public static string DefaultInsertFieldsDefaultValueJson(DbType dbType = DbType.Guid, string newId = "@NEWID") => JsonSerializer.Serialize(new FieldsDefaultValue[] diff --git a/api/src/Sozsoft.Platform.Domain.Shared/WizardConsts.cs b/api/src/Sozsoft.Platform.Domain.Shared/WizardConsts.cs index 294ae9a..54b6654 100644 --- a/api/src/Sozsoft.Platform.Domain.Shared/WizardConsts.cs +++ b/api/src/Sozsoft.Platform.Domain.Shared/WizardConsts.cs @@ -163,7 +163,7 @@ public static class WizardConsts public static string DefaultDeleteCommand(string tableName) { - return $"UPDATE \"{tableName}\" SET \"DeleterId\"=@DeleterId, \"DeletionTime\"=CURRENT_TIMESTAMP, \"IsDeleted\"='true' WHERE \"Id\"=@Id"; + return $"UPDATE \"{tableName}\" SET \"DeleterId\"=@DeleterId, \"DeletionTime\"=CURRENT_TIMESTAMP, \"IsDeleted\"='true' WHERE \"Id\" IN @Id"; } public static string DefaultInsertFieldsDefaultValueJson(DbType dbType = DbType.Guid) diff --git a/api/src/Sozsoft.Platform.Domain/Queries/DefaultValueManager.cs b/api/src/Sozsoft.Platform.Domain/Queries/DefaultValueManager.cs index 0263bc3..d99430e 100644 --- a/api/src/Sozsoft.Platform.Domain/Queries/DefaultValueManager.cs +++ b/api/src/Sozsoft.Platform.Domain/Queries/DefaultValueManager.cs @@ -89,7 +89,9 @@ public class DefaultValueManager : PlatformDomainService, IDefaultValueManager else if (defaultField.Value == PlatformConsts.DefaultValues.Year) value = Clock.Now.Year; else if (defaultField.Value == PlatformConsts.DefaultValues.Id) - value = keys?.FirstOrDefault(); + value = op == OperationEnum.Delete + ? keys + : keys?.FirstOrDefault(); else if (defaultField.Value == PlatformConsts.DefaultValues.NewId) value = Guid.NewGuid(); else if (defaultField.Value == PlatformConsts.DefaultValues.Selected_Ids) diff --git a/api/src/Sozsoft.Platform.Domain/Queries/QueryManager.cs b/api/src/Sozsoft.Platform.Domain/Queries/QueryManager.cs index 4c8dca5..69b1a72 100644 --- a/api/src/Sozsoft.Platform.Domain/Queries/QueryManager.cs +++ b/api/src/Sozsoft.Platform.Domain/Queries/QueryManager.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Data; using System.Linq; +using System.Text.RegularExpressions; using System.Threading.Tasks; using Sozsoft.Platform.DynamicData; using Sozsoft.Platform.Entities; @@ -169,7 +170,7 @@ public class QueryManager : PlatformDomainService, IQueryManager // oncelik Command alanindadir, dolu ise silme islemi buradaki sorguya yonlendirilir if (!string.IsNullOrEmpty(command)) { - sql = command; + sql = NormalizeCollectionParameterCommand(command, parameters, dataSourceType); } else { @@ -189,7 +190,7 @@ public class QueryManager : PlatformDomainService, IQueryManager { var where = dataSourceType switch { - DataSourceTypeEnum.Mssql => $"\"{listForm.KeyFieldName}\" IN (@{listForm.KeyFieldName})", + DataSourceTypeEnum.Mssql => $"\"{listForm.KeyFieldName}\" IN @{listForm.KeyFieldName}", DataSourceTypeEnum.Postgresql => $"\"{listForm.KeyFieldName}\" = ANY(@{listForm.KeyFieldName})", _ => string.Empty, }; @@ -209,7 +210,14 @@ public class QueryManager : PlatformDomainService, IQueryManager string where = string.Empty; if (parameters.Any()) { - where = string.Join(" AND ", parameters.Select(a => $"\"{a.Key}\" IN (@{a.Key})").ToList()); + where = string.Join( + " AND ", + parameters.Select(a => dataSourceType switch + { + DataSourceTypeEnum.Mssql => $"\"{a.Key}\" IN @{a.Key}", + DataSourceTypeEnum.Postgresql => $"\"{a.Key}\" = ANY(@{a.Key})", + _ => "1 = 0", + }).ToList()); } else { @@ -220,7 +228,7 @@ public class QueryManager : PlatformDomainService, IQueryManager where = dataSourceType switch { - DataSourceTypeEnum.Mssql => $"\"{listForm.KeyFieldName}\" IN (@{listForm.KeyFieldName})", + DataSourceTypeEnum.Mssql => $"\"{listForm.KeyFieldName}\" IN @{listForm.KeyFieldName}", DataSourceTypeEnum.Postgresql => $"\"{listForm.KeyFieldName}\" = ANY(@{listForm.KeyFieldName})", _ => "1 = 0", }; @@ -231,5 +239,78 @@ public class QueryManager : PlatformDomainService, IQueryManager return sql; } + + private static string NormalizeCollectionParameterCommand( + string command, + Dictionary parameters, + DataSourceTypeEnum dataSourceType) + { + if (string.IsNullOrWhiteSpace(command) || parameters == null || parameters.Count == 0) + { + return command; + } + + var sql = command; + foreach (var parameter in parameters) + { + if (!IsCollectionParameter(parameter.Value)) + { + continue; + } + + var escapedParameterName = Regex.Escape(parameter.Key); + sql = dataSourceType switch + { + DataSourceTypeEnum.Mssql => NormalizeMssqlCollectionParameter(sql, escapedParameterName, parameter.Key), + DataSourceTypeEnum.Postgresql => NormalizePostgresqlCollectionParameter(sql, escapedParameterName, parameter.Key), + _ => sql + }; + } + + return sql; + } + + private static bool IsCollectionParameter(object value) + { + return value is System.Collections.IEnumerable + && value is not string + && value is not byte[]; + } + + private static string NormalizeMssqlCollectionParameter( + string sql, + string escapedParameterName, + string parameterName) + { + sql = Regex.Replace( + sql, + $@"IN\s*\(\s*@{escapedParameterName}\s*\)", + $"IN @{parameterName}", + RegexOptions.IgnoreCase); + + return Regex.Replace( + sql, + $@"(?(""[^""]+""|\[[^\]]+\]|`[^`]+`|\w+))\s*=\s*@{escapedParameterName}\b", + match => $"{match.Groups["column"].Value} IN @{parameterName}", + RegexOptions.IgnoreCase); + } + + private static string NormalizePostgresqlCollectionParameter( + string sql, + string escapedParameterName, + string parameterName) + { + sql = Regex.Replace( + sql, + $@"IN\s*\(\s*@{escapedParameterName}\s*\)", + $"= ANY(@{parameterName})", + RegexOptions.IgnoreCase); + + return Regex.Replace( + sql, + $@"(?(""[^""]+""|\[[^\]]+\]|`[^`]+`|\w+))\s*=\s*@{escapedParameterName}\b", + match => $"{match.Groups["column"].Value} = ANY(@{parameterName})", + RegexOptions.IgnoreCase); + } } diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/DynamicData/MsDynamicDataRepository.cs b/api/src/Sozsoft.Platform.EntityFrameworkCore/DynamicData/MsDynamicDataRepository.cs index 486e08d..af694de 100644 --- a/api/src/Sozsoft.Platform.EntityFrameworkCore/DynamicData/MsDynamicDataRepository.cs +++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/DynamicData/MsDynamicDataRepository.cs @@ -2,8 +2,12 @@ using System.Collections.Generic; using System.Data; using System.Data.Common; +using System.Globalization; +using System.Linq; +using System.Text.Json; using System.Threading.Tasks; using Dapper; +using Sozsoft.Platform; using Sozsoft.Platform.DynamicData; using Microsoft.Data.SqlClient; using Volo.Abp.DependencyInjection; @@ -178,7 +182,7 @@ public class MsDynamicDataRepository : IDynamicDataRepository, IScopedDependency public virtual async Task> QueryAsync(string sql, string cs, Dictionary parameters = null) { - var param = new DynamicParameters(parameters); + var param = CreateDynamicParameters(parameters); var dbConnection = await GetOrCreateConnectionAsync(cs); var transaction = await GetOrCreateTransactionAsync(dbConnection, cs); @@ -188,7 +192,7 @@ public class MsDynamicDataRepository : IDynamicDataRepository, IScopedDependency public virtual async Task> QueryAsync(string sql, string cs, Dictionary parameters = null) { - var param = new DynamicParameters(parameters); + var param = CreateDynamicParameters(parameters); var dbConnection = await GetOrCreateConnectionAsync(cs); var transaction = await GetOrCreateTransactionAsync(dbConnection, cs); @@ -197,7 +201,7 @@ public class MsDynamicDataRepository : IDynamicDataRepository, IScopedDependency public virtual async Task QuerySingleAsync(string sql, string cs, Dictionary parameters = null) { - var param = new DynamicParameters(parameters); + var param = CreateDynamicParameters(parameters); var dbConnection = await GetOrCreateConnectionAsync(cs); var transaction = await GetOrCreateTransactionAsync(dbConnection, cs); @@ -206,7 +210,7 @@ public class MsDynamicDataRepository : IDynamicDataRepository, IScopedDependency public virtual async Task ExecuteScalarAsync(string sql, string cs, Dictionary parameters = null) { - var param = new DynamicParameters(parameters); + var param = CreateDynamicParameters(parameters); var dbConnection = await GetOrCreateConnectionAsync(cs); var transaction = await GetOrCreateTransactionAsync(dbConnection, cs); @@ -237,13 +241,257 @@ public class MsDynamicDataRepository : IDynamicDataRepository, IScopedDependency public virtual async Task ExecuteAsync(string sql, string cs, Dictionary parameters = null) { - var param = new DynamicParameters(parameters); + var param = CreateDynamicParameters(parameters); var dbConnection = await GetOrCreateConnectionAsync(cs); var transaction = await GetOrCreateTransactionAsync(dbConnection, cs); return await dbConnection.ExecuteAsync(sql, param, transaction); } + private static DynamicParameters CreateDynamicParameters(Dictionary parameters) + { + var dynamicParameters = new DynamicParameters(); + + if (parameters == null) + { + return dynamicParameters; + } + + foreach (var parameter in parameters) + { + dynamicParameters.Add(parameter.Key, NormalizeParameterValue(parameter.Value)); + } + + return dynamicParameters; + } + + private static object NormalizeParameterValue(object value) + { + if (value == null || value == DBNull.Value) + { + return value; + } + + if (value is JsonElement jsonElement) + { + return NormalizeJsonElement(jsonElement); + } + + if (value is Array array && value is not byte[]) + { + return NormalizeArrayParameter(array); + } + + return value; + } + + private static object NormalizeArrayParameter(Array values) + { + var normalizedValues = values + .Cast() + .Select(NormalizeParameterValue) + .Where(value => value != null && value != DBNull.Value) + .ToArray(); + + if (normalizedValues.Length == 0) + { + return Array.Empty(); + } + + if (TryBuildGuidArray(normalizedValues, out var guidValues)) + { + return guidValues; + } + + if (TryBuildIntArray(normalizedValues, out var intValues)) + { + return intValues; + } + + if (TryBuildLongArray(normalizedValues, out var longValues)) + { + return longValues; + } + + if (TryBuildDecimalArray(normalizedValues, out var decimalValues)) + { + return decimalValues; + } + + if (TryBuildBoolArray(normalizedValues, out var boolValues)) + { + return boolValues; + } + + if (TryBuildDateTimeOffsetArray(normalizedValues, out var dateTimeOffsetValues)) + { + return dateTimeOffsetValues; + } + + var stringValues = normalizedValues.Select(value => value.ToString()).ToArray(); + + if (stringValues.Length == 1 && stringValues[0]?.Contains(PlatformConsts.MultiValueDelimiter) == true) + { + return stringValues[0].Split(PlatformConsts.MultiValueDelimiter, StringSplitOptions.RemoveEmptyEntries); + } + + return stringValues; + } + + private static object NormalizeJsonElement(JsonElement value) + { + return value.ValueKind switch + { + JsonValueKind.String => value.GetString(), + JsonValueKind.Number when value.TryGetInt32(out var intValue) => intValue, + JsonValueKind.Number when value.TryGetInt64(out var longValue) => longValue, + JsonValueKind.Number when value.TryGetDecimal(out var decimalValue) => decimalValue, + JsonValueKind.True => true, + JsonValueKind.False => false, + JsonValueKind.Null => null, + JsonValueKind.Undefined => null, + _ => value.ToString() + }; + } + + private static bool TryBuildGuidArray(object[] values, out Guid[] result) + { + result = new Guid[values.Length]; + + for (var i = 0; i < values.Length; i++) + { + if (values[i] is Guid guidValue) + { + result[i] = guidValue; + continue; + } + + if (!Guid.TryParse(values[i]?.ToString(), out result[i])) + { + result = null; + return false; + } + } + + return true; + } + + private static bool TryBuildIntArray(object[] values, out int[] result) + { + result = new int[values.Length]; + + for (var i = 0; i < values.Length; i++) + { + if (values[i] is int intValue) + { + result[i] = intValue; + continue; + } + + if (!int.TryParse(values[i]?.ToString(), NumberStyles.Integer, CultureInfo.InvariantCulture, out result[i])) + { + result = null; + return false; + } + } + + return true; + } + + private static bool TryBuildLongArray(object[] values, out long[] result) + { + result = new long[values.Length]; + + for (var i = 0; i < values.Length; i++) + { + if (values[i] is long longValue) + { + result[i] = longValue; + continue; + } + + if (!long.TryParse(values[i]?.ToString(), NumberStyles.Integer, CultureInfo.InvariantCulture, out result[i])) + { + result = null; + return false; + } + } + + return true; + } + + private static bool TryBuildDecimalArray(object[] values, out decimal[] result) + { + result = new decimal[values.Length]; + + for (var i = 0; i < values.Length; i++) + { + if (values[i] is decimal decimalValue) + { + result[i] = decimalValue; + continue; + } + + if (!decimal.TryParse(values[i]?.ToString(), NumberStyles.Any, CultureInfo.InvariantCulture, out result[i])) + { + result = null; + return false; + } + } + + return true; + } + + private static bool TryBuildBoolArray(object[] values, out bool[] result) + { + result = new bool[values.Length]; + + for (var i = 0; i < values.Length; i++) + { + if (values[i] is bool boolValue) + { + result[i] = boolValue; + continue; + } + + if (!bool.TryParse(values[i]?.ToString(), out result[i])) + { + result = null; + return false; + } + } + + return true; + } + + private static bool TryBuildDateTimeOffsetArray(object[] values, out DateTimeOffset[] result) + { + result = new DateTimeOffset[values.Length]; + + for (var i = 0; i < values.Length; i++) + { + if (values[i] is DateTimeOffset dateTimeOffsetValue) + { + result[i] = dateTimeOffsetValue; + continue; + } + + if (values[i] is DateTime dateTimeValue) + { + result[i] = dateTimeValue; + continue; + } + + if (!DateTimeOffset.TryParse(values[i]?.ToString(), CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out result[i])) + { + result = null; + return false; + } + } + + return true; + } + // ------------------ Dispose ------------------ public void Dispose() diff --git a/ui/src/views/list/useToolbar.tsx b/ui/src/views/list/useToolbar.tsx index a2506aa..e0a6def 100644 --- a/ui/src/views/list/useToolbar.tsx +++ b/ui/src/views/list/useToolbar.tsx @@ -32,7 +32,7 @@ const useToolbar = ({ }: { gridDto?: GridDto listFormCode: string - getSelectedRowKeys: () => void + getSelectedRowKeys: () => unknown[] | Promise getSelectedRowsData: () => any refreshData: () => void getFilter: () => void @@ -113,7 +113,8 @@ const useToolbar = ({ }) const workflowOptions = grdOpt.workflowDto - const approvalCriteria = workflowOptions?.criteria?.filter((item) => item.kind === 'Approval') ?? [] + const approvalCriteria = + workflowOptions?.criteria?.filter((item) => item.kind === 'Approval') ?? [] if ( workflowOptions?.approvalStatusFieldName && approvalCriteria.length > 0 && @@ -149,7 +150,7 @@ const useToolbar = ({ ) { toast.push( - Secili kayit icin workflow zaten baslamis. + {translate('::WorkflowAlreadyStarted')} , { placement: 'top-end' }, ) @@ -211,7 +212,7 @@ const useToolbar = ({ if (activeRows.length !== selectedRows.length) { toast.push( - Secili kayit bu onay adiminda veya onay kullanicisinda beklemiyor. + {translate('::SeciliKayitBekliyor')} , { placement: 'top-end' }, ) @@ -302,16 +303,75 @@ const useToolbar = ({ text: translate('::ListForms.ListForm.DeleteSelectedRecords'), icon: 'trash', visible: false, - onClick() { + async onClick() { if (!grdOpt.deleteServiceAddress) { return } - dynamicFetch(grdOpt.deleteServiceAddress, 'POST', null, { - keys: getSelectedRowKeys(), - listFormCode, - }).then(() => { - refreshData() + const selectedKeys = await Promise.resolve(getSelectedRowKeys()) + const keys = Array.isArray(selectedKeys) ? [...selectedKeys] : [] + + if (!keys.length) { + toast.push( + + {translate('::ListForms.ListForm.SelectRecord')} + , + { placement: 'top-end' }, + ) + return + } + + setToolbarModalData({ + open: true, + content: ( + <> +
+ {translate('::ListForms.ListForm.DeleteSelectedRecords')} +
+ +

+ {translate('::SeciliKayitlarSilmekIstiyormusunuz', { + 0: keys.length, + })} +

+ +
+ + +
+ + ), }) }, }, @@ -342,9 +402,13 @@ const useToolbar = ({ open: true, content: ( <> -
Delete All Records
+
{translate('::ListForms.ListForm.DeleteAllRecords')}
-

Are you sure to delete all {r.data.totalCount} records?

+

+ {translate('::TumKayitlariSilmekIstiyormusunuz', { + 0: r.data.totalCount, + })} +