Seçilenleri ve Tümünü Sil butonları

This commit is contained in:
Sedat Öztürk 2026-06-07 14:07:48 +03:00
parent bade0bab98
commit 1d15c44a3d
7 changed files with 476 additions and 38 deletions

View file

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

View file

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

View file

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

View file

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

View file

@ -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<string, object> 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,
$@"(?<column>(""[^""]+""|\[[^\]]+\]|`[^`]+`|\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,
$@"(?<column>(""[^""]+""|\[[^\]]+\]|`[^`]+`|\w+))\s*=\s*@{escapedParameterName}\b",
match => $"{match.Groups["column"].Value} = ANY(@{parameterName})",
RegexOptions.IgnoreCase);
}
}

View file

@ -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<List<T>> QueryAsync<T>(string sql, string cs, Dictionary<string, object> 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<IEnumerable<dynamic>> QueryAsync(string sql, string cs, Dictionary<string, object> 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<T> QuerySingleAsync<T>(string sql, string cs, Dictionary<string, object> 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<T> ExecuteScalarAsync<T>(string sql, string cs, Dictionary<string, object> 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<int> ExecuteAsync(string sql, string cs, Dictionary<string, object> 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<string, object> 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<object>()
.Select(NormalizeParameterValue)
.Where(value => value != null && value != DBNull.Value)
.ToArray();
if (normalizedValues.Length == 0)
{
return Array.Empty<string>();
}
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()

View file

@ -32,7 +32,7 @@ const useToolbar = ({
}: {
gridDto?: GridDto
listFormCode: string
getSelectedRowKeys: () => void
getSelectedRowKeys: () => unknown[] | Promise<unknown[]>
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(
<Notification type="warning" duration={2500}>
Secili kayit icin workflow zaten baslamis.
{translate('::WorkflowAlreadyStarted')}
</Notification>,
{ placement: 'top-end' },
)
@ -211,7 +212,7 @@ const useToolbar = ({
if (activeRows.length !== selectedRows.length) {
toast.push(
<Notification type="warning" duration={2500}>
Secili kayit bu onay adiminda veya onay kullanicisinda beklemiyor.
{translate('::SeciliKayitBekliyor')}
</Notification>,
{ 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(
<Notification type="warning" duration={2000}>
{translate('::ListForms.ListForm.SelectRecord')}
</Notification>,
{ placement: 'top-end' },
)
return
}
setToolbarModalData({
open: true,
content: (
<>
<h5 className="mb-4">
{translate('::ListForms.ListForm.DeleteSelectedRecords')}
</h5>
<p>
{translate('::SeciliKayitlarSilmekIstiyormusunuz', {
0: keys.length,
})}
</p>
<div className="text-right mt-6">
<Button
className="ltr:mr-2 rtl:ml-2"
variant="plain"
onClick={() => setToolbarModalData(undefined)}
>
{translate('::Cancel')}
</Button>
<Button
variant="solid"
onClick={() => {
dynamicFetch(grdOpt.deleteServiceAddress!, 'POST', null, {
keys,
listFormCode,
})
.then(() => {
refreshData()
setToolbarModalData(undefined)
})
.catch((error: any) => {
toast.push(
<Notification type="danger" duration={3000}>
{error?.response?.data?.error?.message ||
error?.response?.data?.message ||
error?.message ||
translate('::SilmeIslemiBasarisiz')}
</Notification>,
{ placement: 'top-end' },
)
})
}}
>
{translate('::Delete')}
</Button>
</div>
</>
),
})
},
},
@ -342,9 +402,13 @@ const useToolbar = ({
open: true,
content: (
<>
<h5 className="mb-4">Delete All Records</h5>
<h5 className="mb-4">{translate('::ListForms.ListForm.DeleteAllRecords')}</h5>
<p>Are you sure to delete all {r.data.totalCount} records?</p>
<p>
{translate('::TumKayitlariSilmekIstiyormusunuz', {
0: r.data.totalCount,
})}
</p>
<div className="text-right mt-6">
<Button
@ -362,7 +426,7 @@ const useToolbar = ({
dynamicFetch('list-form-select/select', 'GET', parameters).then(() => {
toast.push(
<Notification type="success" duration={2000}>
{'Tüm kayıtlar silindi.'}
{translate('::TumKayitlarSilindi')}
</Notification>,
{
placement: 'top-end',
@ -521,7 +585,9 @@ function isWorkflowApprovalCriteriaActive(
}
function normalizeWorkflowValue(value: unknown) {
return String(value ?? '').trim().toLocaleLowerCase('tr-TR')
return String(value ?? '')
.trim()
.toLocaleLowerCase('tr-TR')
}
function isWorkflowNotStarted(row: Record<string, unknown>, workflowOptions: WorkflowDto) {
@ -546,7 +612,8 @@ export function updateWorkflowApprovalToolbarItems(
name?: string
},
) {
const approvalCriteria = workflowOptions?.criteria?.filter((item) => item.kind === 'Approval') ?? []
const approvalCriteria =
workflowOptions?.criteria?.filter((item) => item.kind === 'Approval') ?? []
if (!component || !workflowOptions?.approvalStatusFieldName || !approvalCriteria.length) {
return
}
@ -585,7 +652,12 @@ export function updateWorkflowApprovalToolbarItems(
const enabled =
selectedRowsData.length > 0 &&
selectedRowsData.every((row) =>
isWorkflowApprovalCriteriaActive(row, workflowOptions, criteria.title, currentUserIdentities),
isWorkflowApprovalCriteriaActive(
row,
workflowOptions,
criteria.title,
currentUserIdentities,
),
)
const optionPath = `toolbar.items[${toolbarItemIndex}].options.disabled`
@ -620,13 +692,7 @@ function WorkflowApprovalDecisionDialog({
try {
await Promise.all(
keys.map((key) =>
workflowService.decideWorkflow(
listFormCode,
[key],
approved,
note,
criteriaId,
),
workflowService.decideWorkflow(listFormCode, [key], approved, note, criteriaId),
),
)
onCompleted()
@ -659,14 +725,19 @@ function WorkflowApprovalDecisionDialog({
onChange={(event) => setNote(event.target.value)}
/>
<div className="text-right mt-6">
<Button className="ltr:mr-2 rtl:ml-2" variant="plain" disabled={submitting} onClick={onCancel}>
<Button
className="ltr:mr-2 rtl:ml-2"
variant="plain"
disabled={submitting}
onClick={onCancel}
>
{translate('::Cancel')}
</Button>
<Button className="ltr:mr-2 rtl:ml-2" disabled={submitting} onClick={() => decide(false)}>
Reddet
{translate('::App.Listform.ListformField.Rejecter')}
</Button>
<Button variant="solid" disabled={submitting} onClick={() => decide(true)}>
Onayla
{translate('::App.Listform.ListformField.Approver')}
</Button>
</div>
</>