Grid Approval birden fazla kişi seçilebilir.
This commit is contained in:
parent
04e88fc969
commit
17e4716ee8
9 changed files with 194 additions and 38 deletions
|
|
@ -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("|", "/");
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<string, object>();
|
||||
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<string, string>? sender = null;
|
||||
|
|
@ -557,22 +561,42 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
|
|||
sender = new KeyValuePair<string, string>(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<List<string>> ResolveApproverEmailsAsync(string approver)
|
||||
{
|
||||
var emails = new List<string>();
|
||||
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<string> 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<string> GetCurrentUserWorkflowIdentities()
|
||||
{
|
||||
return new[] { CurrentUser.UserName, CurrentUser.Email, CurrentUser.Name }
|
||||
.Where(identity => !identity.IsNullOrWhiteSpace())
|
||||
.Select(identity => identity.Trim());
|
||||
}
|
||||
|
||||
private static IEnumerable<string> SplitWorkflowApprovers(string value)
|
||||
{
|
||||
return (value ?? string.Empty)
|
||||
.Split(PlatformConsts.MultiValueDelimiter, StringSplitOptions.RemoveEmptyEntries)
|
||||
.Select(item => item.Trim())
|
||||
.Where(item => !item.IsNullOrWhiteSpace());
|
||||
}
|
||||
|
||||
private async Task<string> 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(
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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("'", "''");
|
||||
}
|
||||
|
||||
/// <summary> FieldName operator Id seklinde olan filtreleri ayirir
|
||||
/// </summary>
|
||||
/// <param name="filter"></param>
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string, Endpoint[]>()
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -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 && (
|
||||
<div className="text-[11px] text-gray-500 dark:text-gray-400">
|
||||
Approver: {criteria.approver}
|
||||
Approver: {formatWorkflowApprovers(criteria.approver)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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')}
|
||||
>
|
||||
<SelectField
|
||||
<MultiSelectField
|
||||
required
|
||||
options={userList}
|
||||
value={formValues.approver}
|
||||
|
|
@ -527,6 +529,48 @@ function SelectField({
|
|||
)
|
||||
}
|
||||
|
||||
function MultiSelectField({
|
||||
options,
|
||||
value,
|
||||
onChange,
|
||||
required = false,
|
||||
}: {
|
||||
options: SelectBoxOption[]
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
required?: boolean
|
||||
}) {
|
||||
const values = splitWorkflowApprovers(value)
|
||||
const selectedOptions = options.filter((option) => values.includes(String(option.value)))
|
||||
|
||||
return (
|
||||
<>
|
||||
<Select
|
||||
isMulti
|
||||
options={options}
|
||||
value={selectedOptions}
|
||||
onChange={(selected: any) =>
|
||||
onChange(
|
||||
(Array.isArray(selected) ? selected : [])
|
||||
.map((option) => option?.value)
|
||||
.filter(Boolean)
|
||||
.join(MULTIVALUE_DELIMITER),
|
||||
)
|
||||
}
|
||||
/>
|
||||
{required && (
|
||||
<input
|
||||
required
|
||||
tabIndex={-1}
|
||||
className="pointer-events-none h-0 w-0 opacity-0"
|
||||
value={value}
|
||||
onChange={() => undefined}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function criteriaSummaryContent(item: WorkflowCriteriaDto) {
|
||||
if (item.kind === 'Compare') {
|
||||
const outcomes = item.compareOutcomes || []
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
Loading…
Reference in a new issue