From 17e4716ee85015c5ae355ca1185cd6c406250b3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sedat=20=C3=96ZT=C3=9CRK?= <76204082+iamsedatozturk@users.noreply.github.com> Date: Wed, 8 Jul 2026 10:29:45 +0300 Subject: [PATCH] =?UTF-8?q?Grid=20Approval=20birden=20fazla=20ki=C5=9Fi=20?= =?UTF-8?q?se=C3=A7ilebilir.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FileManagementAppService.cs | 2 - .../ListForms/ListFormWorkflowAppService.cs | 98 ++++++++++++++++--- .../Extensions/AbpPermissionsExtensions.cs | 6 +- .../Queries/SelectQueryManager.cs | 41 ++++++-- .../DynamicReportImageHelper.cs | 4 +- ui/src/utils/workflow/workflowHelpers.ts | 19 +++- .../admin/listForm/wizard/WizardStep7.tsx | 3 +- .../listForm/workflow/WorkflowCriteria.tsx | 46 ++++++++- ui/src/views/list/useToolbar.tsx | 13 ++- 9 files changed, 194 insertions(+), 38 deletions(-) diff --git a/api/src/Sozsoft.Platform.Application/FileManagement/FileManagementAppService.cs b/api/src/Sozsoft.Platform.Application/FileManagement/FileManagementAppService.cs index 8ca0365..997c7f4 100644 --- a/api/src/Sozsoft.Platform.Application/FileManagement/FileManagementAppService.cs +++ b/api/src/Sozsoft.Platform.Application/FileManagement/FileManagementAppService.cs @@ -100,13 +100,11 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe private string EncodePathAsId(string path) { - // Path'deki '/' karakterlerini '|' ile değiştir URL-safe hale getirmek için return path.Replace("/", "|"); } private string DecodeIdAsPath(string id) { - // ID'deki '|' karakterlerini '/' ile geri değiştir return id.Replace("|", "/"); } diff --git a/api/src/Sozsoft.Platform.Application/ListForms/ListFormWorkflowAppService.cs b/api/src/Sozsoft.Platform.Application/ListForms/ListFormWorkflowAppService.cs index cb21480..aba05c4 100644 --- a/api/src/Sozsoft.Platform.Application/ListForms/ListFormWorkflowAppService.cs +++ b/api/src/Sozsoft.Platform.Application/ListForms/ListFormWorkflowAppService.cs @@ -357,6 +357,10 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA { throw new UserFriendlyException("Seçili kayıt bu onay adımında beklemiyor."); } + if (!IsCurrentUserWorkflowApprover(context, current)) + { + throw new UserFriendlyException("Bu kayıt için onay yetkiniz yok."); + } var update = new Dictionary(); if (!context.Workflow.ApprovalUserFieldName.IsNullOrWhiteSpace()) @@ -548,7 +552,7 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA private async Task SendInformEmailAsync(WorkflowRunContext context, ListFormWorkflow node) { - var recipientEmail = await ResolveApproverEmailAsync(node.Approver); + var recipientEmails = await ResolveApproverEmailsAsync(node.Approver); var senderName = await settingProvider.GetOrNullAsync(SeedConsts.AbpSettings.Mailing.Default.DefaultFromDisplayName); var senderEmail = await settingProvider.GetOrNullAsync(SeedConsts.AbpSettings.Mailing.Default.DefaultFromAddress); KeyValuePair? sender = null; @@ -557,22 +561,42 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA sender = new KeyValuePair(senderName, senderEmail); } - var result = await erpEmailSender.SendEmailAsync( - recipientEmail, - sender, - new { }, - BuildInformEmailBody(context, node, await BuildPreviousWorkflowNotesHtmlAsync(context)), - $"Workflow Bilgilendirme: {node.Title}", - null, - true); - - if (!result.Success) + var body = BuildInformEmailBody(context, node, await BuildPreviousWorkflowNotesHtmlAsync(context)); + foreach (var recipientEmail in recipientEmails) { - throw new UserFriendlyException($"Bilgilendirme maili gonderilemedi: {result.ErrorMessage}"); + var result = await erpEmailSender.SendEmailAsync( + recipientEmail, + sender, + new { }, + body, + $"Workflow Bilgilendirme: {node.Title}", + null, + true); + + if (!result.Success) + { + throw new UserFriendlyException($"Bilgilendirme maili gonderilemedi: {result.ErrorMessage}"); + } } } + private async Task> ResolveApproverEmailsAsync(string approver) + { + var emails = new List(); + foreach (var item in SplitWorkflowApprovers(approver)) + { + emails.Add(await ResolveApproverEmailAsync(item)); + } + + if (emails.Count == 0) + { + throw new UserFriendlyException("Bilgilendirme dugumu icin approver tanimli degil."); + } + + return emails; + } + private async Task ResolveApproverEmailAsync(string approver) { if (approver.IsNullOrWhiteSpace()) @@ -594,6 +618,45 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA return user.Email; } + private bool IsCurrentUserWorkflowApprover(WorkflowRunContext context, ListFormWorkflow current) + { + if (context.Workflow.ApprovalUserFieldName.IsNullOrWhiteSpace()) + { + return true; + } + + var approverValue = GetRowValue(context.Row, context.Workflow.ApprovalUserFieldName)?.ToString(); + if (approverValue.IsNullOrWhiteSpace()) + { + approverValue = current.Approver; + } + + var approvers = SplitWorkflowApprovers(approverValue) + .ToHashSet(StringComparer.OrdinalIgnoreCase); + + if (approvers.Count == 0) + { + return true; + } + + return GetCurrentUserWorkflowIdentities().Any(approvers.Contains); + } + + private IEnumerable GetCurrentUserWorkflowIdentities() + { + return new[] { CurrentUser.UserName, CurrentUser.Email, CurrentUser.Name } + .Where(identity => !identity.IsNullOrWhiteSpace()) + .Select(identity => identity.Trim()); + } + + private static IEnumerable SplitWorkflowApprovers(string value) + { + return (value ?? string.Empty) + .Split(PlatformConsts.MultiValueDelimiter, StringSplitOptions.RemoveEmptyEntries) + .Select(item => item.Trim()) + .Where(item => !item.IsNullOrWhiteSpace()); + } + private async Task BuildPreviousWorkflowNotesHtmlAsync(WorkflowRunContext context) { var key = context.Keys?.FirstOrDefault()?.ToString(); @@ -633,7 +696,7 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA var keyText = string.Join(", ", context.Keys.Select(key => WebUtility.HtmlEncode(key?.ToString() ?? string.Empty))); var listFormCode = WebUtility.HtmlEncode(context.ListFormCode ?? string.Empty); var nodeTitle = WebUtility.HtmlEncode(node.Title ?? string.Empty); - var recipient = WebUtility.HtmlEncode(node.Approver ?? string.Empty); + var recipient = WebUtility.HtmlEncode(FormatWorkflowApprovers(node.Approver)); var processRows = BuildWorkflowNoteContent(context.WorkflowNoteRows); var previousNotesSection = previousWorkflowNotesHtml.IsNullOrWhiteSpace() ? string.Empty @@ -697,7 +760,7 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA if (!node.Approver.IsNullOrWhiteSpace()) { - context.WorkflowNoteRows.Add((node.Kind == "Inform" ? "Inform: " : "Approver: ", node.Approver)); + context.WorkflowNoteRows.Add((node.Kind == "Inform" ? "Inform: " : "Approver: ", FormatWorkflowApprovers(node.Approver))); } } @@ -790,7 +853,12 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA { return userName.IsNullOrWhiteSpace() ? localizer["ListForms.ListForm.Workflow.UndefinedUser"].Value - : userName; + : FormatWorkflowApprovers(userName); + } + + private static string FormatWorkflowApprovers(string value) + { + return string.Join(", ", SplitWorkflowApprovers(value)); } private async Task InsertWorkflowNoteAsync( diff --git a/api/src/Sozsoft.Platform.Domain/Extensions/AbpPermissionsExtensions.cs b/api/src/Sozsoft.Platform.Domain/Extensions/AbpPermissionsExtensions.cs index b6d8376..30e5c51 100644 --- a/api/src/Sozsoft.Platform.Domain/Extensions/AbpPermissionsExtensions.cs +++ b/api/src/Sozsoft.Platform.Domain/Extensions/AbpPermissionsExtensions.cs @@ -20,9 +20,9 @@ public static class AbpPermissionsExtensions // 🔹 Gereksiz boşlukları temizle var cleaned = string.Join( - '|', + "|", menuGroup - .Split('|', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .Split(PlatformConsts.MultiValueDelimiter, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) .Distinct() ); @@ -44,7 +44,7 @@ public static class AbpPermissionsExtensions // 🔹 "Erp|Kurs" -> ["Erp", "Kurs"] return value - .Split('|', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .Split(PlatformConsts.MultiValueDelimiter, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) .Distinct() .ToArray(); } diff --git a/api/src/Sozsoft.Platform.Domain/Queries/SelectQueryManager.cs b/api/src/Sozsoft.Platform.Domain/Queries/SelectQueryManager.cs index f03101f..684644b 100644 --- a/api/src/Sozsoft.Platform.Domain/Queries/SelectQueryManager.cs +++ b/api/src/Sozsoft.Platform.Domain/Queries/SelectQueryManager.cs @@ -479,11 +479,7 @@ public class SelectQueryManager : PlatformDomainService, ISelectQueryManager // hem de ApprovalUserFieldName alanı CurrentUserName'e eşit olan kayıtları getirmek istiyoruz, // Boş olanları getirmemizin sebebi workflow start edebilmektir. // İlk kayıt eklenince onaylayacak kişi atanmaz, böylece o kayıt onaysız olarak kalmaz ve workflow başlatılabilir olur. - whereParts.Add( - $"(\"{workflow.ApprovalUserFieldName}\" = '{CurrentUser.UserName}' " + - $"OR \"{workflow.ApprovalUserFieldName}\" IS NULL " + - $"OR \"{workflow.ApprovalUserFieldName}\" = '')" - ); + whereParts.Add(GetWorkflowApproverFilter(workflow.ApprovalUserFieldName)); } } @@ -495,6 +491,38 @@ public class SelectQueryManager : PlatformDomainService, ISelectQueryManager return whereParts; } + private string GetWorkflowApproverFilter(string approvalUserFieldName) + { + if (approvalUserFieldName.IsNullOrWhiteSpace()) + { + return "1 = 1"; + } + + var field = $"\"{approvalUserFieldName}\""; + var identities = new[] { CurrentUser.UserName, CurrentUser.Email, CurrentUser.Name } + .Where(identity => !identity.IsNullOrWhiteSpace()) + .Select(EscapeSqlLiteral) + .Distinct() + .ToList(); + + if (identities.Count == 0) + { + return $"({field} IS NULL OR {field} = '')"; + } + + var identityList = string.Join(",", identities.Select(identity => $"'{identity}'")); + var approverMatch = DataSourceType == DataSourceTypeEnum.Postgresql + ? $"EXISTS (SELECT 1 FROM unnest(string_to_array(CAST({field} AS TEXT), '{PlatformConsts.MultiValueDelimiter}')) AS split(value) WHERE btrim(split.value) IN ({identityList}))" + : $"EXISTS (SELECT 1 FROM STRING_SPLIT(CAST({field} AS NVARCHAR(MAX)), N'{PlatformConsts.MultiValueDelimiter}') AS split WHERE LTRIM(RTRIM(split.value)) IN ({identityList}))"; + + return $"({approverMatch} OR {field} IS NULL OR {field} = '')"; + } + + private static string EscapeSqlLiteral(string value) + { + return (value ?? string.Empty).Replace("'", "''"); + } + /// FieldName operator Id seklinde olan filtreleri ayirir /// /// @@ -829,8 +857,7 @@ public class SelectQueryManager : PlatformDomainService, ISelectQueryManager } else { - // Birden fazla → '|' ile birleştir - argumentExpression = string.Join(" + '|' + ", argumentFields.Select(f => $"[{f}]")); + argumentExpression = string.Join(PlatformConsts.MultiValueDelimiter, argumentFields.Select(f => $"[{f}]")); } // Select sütunlarını oluştur diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicReportImageHelper.cs b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicReportImageHelper.cs index c767177..82244cf 100644 --- a/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicReportImageHelper.cs +++ b/api/src/Sozsoft.Platform.HttpApi.Host/PredefinedReports/DynamicReportImageHelper.cs @@ -1854,7 +1854,7 @@ internal static class DynamicReportImageHelper private static bool IsImageDelimiter(string text, int index) { - return text[index] == '|' || + return text[index] == PlatformConsts.MultiValueDelimiter || text[index] == '\n' || text[index] == '\r' || (text[index] == ',' && !IsDataImageComma(text, index)); @@ -1868,7 +1868,7 @@ internal static class DynamicReportImageHelper return false; } - var previousDelimiterIndex = text.LastIndexOfAny(['|', '\r', '\n'], index); + var previousDelimiterIndex = text.LastIndexOfAny([PlatformConsts.MultiValueDelimiter, '\r', '\n'], index); return previousDelimiterIndex < dataImageIndex && text.IndexOf(";base64,", dataImageIndex, index - dataImageIndex + 1, StringComparison.OrdinalIgnoreCase) >= 0; } diff --git a/ui/src/utils/workflow/workflowHelpers.ts b/ui/src/utils/workflow/workflowHelpers.ts index fbd49d2..eb943ae 100644 --- a/ui/src/utils/workflow/workflowHelpers.ts +++ b/ui/src/utils/workflow/workflowHelpers.ts @@ -1,4 +1,4 @@ -import { useLocalization } from '../hooks/useLocalization' +import { MULTIVALUE_DELIMITER } from '@/constants/app.constant' import { getNodeHeight, nodeSize } from './workflowConstants' import type { CompareOutcomeDto, @@ -179,10 +179,10 @@ export function collectLinks(criteria: WorkflowCriteriaDto[]) { field: 'nextOnReject', }) }) - return assignLinkSlots(links, criteria) + return assignLinkSlots(links) } -export function assignLinkSlots(links: WorkflowLink[], criteria: WorkflowCriteriaDto[]) { +export function assignLinkSlots(links: WorkflowLink[]) { const endpointGroups = new Map() const addEndpoint = (nodeId: string, side: string, endpoint: Endpoint) => { const key = `${nodeId}:${side}` @@ -588,11 +588,22 @@ export function criteriaSummary(item: WorkflowCriteriaDto) { ) } if (item.kind === 'Approval' || item.kind === 'Inform') { - return `${item.title} ${item.approver ? `- ${item.approver}` : ''}` + return `${item.title} ${item.approver ? `- ${formatWorkflowApprovers(item.approver)}` : ''}` } return item.title } +export function splitWorkflowApprovers(value?: string | null) { + return String(value ?? '') + .split(MULTIVALUE_DELIMITER) + .map((item) => item.trim()) + .filter(Boolean) +} + +export function formatWorkflowApprovers(value?: string | null) { + return splitWorkflowApprovers(value).join(', ') +} + export function targetTitle(criteria: WorkflowCriteriaDto[], id?: string | null) { if (!id) return '-' const item = criteria.find((candidate) => candidate.id === id) diff --git a/ui/src/views/admin/listForm/wizard/WizardStep7.tsx b/ui/src/views/admin/listForm/wizard/WizardStep7.tsx index 44685fa..8359647 100644 --- a/ui/src/views/admin/listForm/wizard/WizardStep7.tsx +++ b/ui/src/views/admin/listForm/wizard/WizardStep7.tsx @@ -20,6 +20,7 @@ import { WidgetEditDto, WorkflowDto, } from '@/proxy/form/models' +import { formatWorkflowApprovers } from '@/utils/workflow/workflowHelpers' // ─── Types ──────────────────────────────────────────────────────────────────── @@ -559,7 +560,7 @@ const WizardStep7 = ({ {(criteria.kind === 'Approval' || criteria.kind === 'Inform') && criteria.approver && (
- Approver: {criteria.approver} + Approver: {formatWorkflowApprovers(criteria.approver)}
)} diff --git a/ui/src/views/admin/listForm/workflow/WorkflowCriteria.tsx b/ui/src/views/admin/listForm/workflow/WorkflowCriteria.tsx index b9e729c..1ec56d3 100644 --- a/ui/src/views/admin/listForm/workflow/WorkflowCriteria.tsx +++ b/ui/src/views/admin/listForm/workflow/WorkflowCriteria.tsx @@ -12,12 +12,14 @@ import { compareOutcomeRuleText, criteriaSummary, emptyCompareOutcome1, + splitWorkflowApprovers, targetTitle, } from '@/utils/workflow/workflowHelpers' import type { CompareOutcomeDto, WorkflowCriteriaDto } from '@/services/workflow.service' import { SelectBoxOption } from '@/types/shared' import { useLocalization } from '@/utils/hooks/useLocalization' import type { DatabaseColumnDto } from '@/proxy/sql-query-manager/models' +import { MULTIVALUE_DELIMITER } from '@/constants/app.constant' type WorkflowCriteriaProps = { criteria: WorkflowCriteriaDto[] @@ -265,7 +267,7 @@ export function WorkflowCriteria({ asterisk label={translate('::App.Listform.ListformField.Approver')} > - void + required?: boolean +}) { + const values = splitWorkflowApprovers(value) + const selectedOptions = options.filter((option) => values.includes(String(option.value))) + + return ( + <> + undefined} + /> + )} + + ) +} + function criteriaSummaryContent(item: WorkflowCriteriaDto) { if (item.kind === 'Compare') { const outcomes = item.compareOutcomes || [] diff --git a/ui/src/views/list/useToolbar.tsx b/ui/src/views/list/useToolbar.tsx index b830ce2..53e803b 100644 --- a/ui/src/views/list/useToolbar.tsx +++ b/ui/src/views/list/useToolbar.tsx @@ -6,16 +6,18 @@ import { usePermission } from '@/utils/hooks/usePermission' import { DataGridTypes } from 'devextreme-react/data-grid' import { ToolbarItem } from 'devextreme/ui/data_grid_types' import { useEffect, useState } from 'react' +import type { ReactNode } from 'react' import { useDialogContext } from '../shared/DialogContext' import { usePWA } from '@/utils/hooks/usePWA' import { layoutTypes, ListViewLayoutType } from '../admin/listForm/edit/types' import { useStoreState } from '@/store' import { workflowService } from '@/services/workflow.service' import type { WorkflowRunResultDto } from '@/services/workflow.service' +import { splitWorkflowApprovers } from '@/utils/workflow/workflowHelpers' type ToolbarModalData = { open: boolean - content?: JSX.Element + content?: ReactNode } const showWorkflowToastMessages = (results: WorkflowRunResultDto | WorkflowRunResultDto[]) => { @@ -626,8 +628,13 @@ function isWorkflowApprovalCriteriaActive( return true } - const approver = normalizeWorkflowValue(row?.[workflowOptions.approvalUserFieldName]) - return currentUserIdentities.some((identity) => normalizeWorkflowValue(identity) === approver) + const approvers = splitWorkflowApprovers( + String(row?.[workflowOptions.approvalUserFieldName] ?? ''), + ).map(normalizeWorkflowValue) + + return currentUserIdentities.some((identity) => + approvers.includes(normalizeWorkflowValue(identity)), + ) } function normalizeWorkflowValue(value: unknown) {