Workflow Listlerinin Note Özelliği eklendi
This commit is contained in:
parent
27e65f05f0
commit
c204eef755
20 changed files with 532 additions and 432 deletions
|
|
@ -278,6 +278,7 @@ public class ListFormWizardAppService(
|
|||
var isCreated = tableColumns.Contains("CreatorId");
|
||||
input.Workflow ??= new WorkflowDto();
|
||||
input.Workflow.Criteria = input.WorkflowCriteria;
|
||||
EnsureUniqueWorkflowCriteriaTitles(input.WorkflowCriteria);
|
||||
|
||||
var listForm = await repoListForm.InsertAsync(new ListForm
|
||||
{
|
||||
|
|
@ -285,7 +286,7 @@ public class ListFormWizardAppService(
|
|||
PageSize = 10,
|
||||
ExportJson = WizardConsts.DefaultExportJson,
|
||||
IsSubForm = false,
|
||||
ShowNote = input.SubForms.Count > 0,
|
||||
ShowNote = input.SubForms.Count > 0 || input.WorkflowCriteria.Count > 0,
|
||||
LayoutJson = WizardConsts.DefaultLayoutJson(input.DefaultLayout, input.Grid, input.Pivot, input.Tree, input.Chart, input.Gantt, input.Scheduler),
|
||||
CultureName = LanguageCodes.En,
|
||||
ListFormCode = input.ListFormCode,
|
||||
|
|
@ -423,6 +424,60 @@ public class ListFormWizardAppService(
|
|||
);
|
||||
}
|
||||
|
||||
private static void EnsureUniqueWorkflowCriteriaTitles(List<ListFormWorkflowCriteriaDto> criteria)
|
||||
{
|
||||
if (criteria == null || criteria.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var baseTitles = criteria
|
||||
.Select(x => string.IsNullOrWhiteSpace(x.Title) ? NormalizeWorkflowTitleFallback(x.Kind) : x.Title.Trim())
|
||||
.ToList();
|
||||
var duplicateTitles = baseTitles
|
||||
.GroupBy(x => x, StringComparer.OrdinalIgnoreCase)
|
||||
.Where(x => x.Count() > 1)
|
||||
.Select(x => x.Key)
|
||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
var titleCounts = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||
var usedTitles = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var item in criteria)
|
||||
{
|
||||
var baseTitle = string.IsNullOrWhiteSpace(item.Title) ? NormalizeWorkflowTitleFallback(item.Kind) : item.Title.Trim();
|
||||
var title = baseTitle;
|
||||
|
||||
if (duplicateTitles.Contains(baseTitle))
|
||||
{
|
||||
titleCounts.TryGetValue(baseTitle, out var count);
|
||||
count++;
|
||||
titleCounts[baseTitle] = count;
|
||||
title = $"{baseTitle}{count}";
|
||||
}
|
||||
|
||||
if (usedTitles.Contains(title))
|
||||
{
|
||||
var index = 1;
|
||||
var candidate = $"{title}{index}";
|
||||
while (usedTitles.Contains(candidate))
|
||||
{
|
||||
index++;
|
||||
candidate = $"{title}{index}";
|
||||
}
|
||||
|
||||
title = candidate;
|
||||
}
|
||||
|
||||
item.Title = title;
|
||||
usedTitles.Add(title);
|
||||
}
|
||||
}
|
||||
|
||||
private static string NormalizeWorkflowTitleFallback(string kind)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(kind) ? "Step" : kind.Trim();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wizard konfigürasyonunu JSON dosyası olarak kaydeder.
|
||||
/// Önce ContentRootPath'ten yukarı çıkarak Sozsoft.Platform.DbMigrator/Seeds/WizardData dizinini arar.
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
|
|||
private const string SystemApprovalDescription = "Sistem tarafından otomatik olarak onaylandı.";
|
||||
|
||||
private readonly IRepository<ListFormWorkflow, string> criteriaRepository;
|
||||
private readonly IRepository<Note, Guid> noteRepository;
|
||||
private readonly IListFormManager listFormManager;
|
||||
private readonly IListFormAuthorizationManager authManager;
|
||||
private readonly IListFormSelectAppService listFormSelectAppService;
|
||||
|
|
@ -39,6 +40,7 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
|
|||
|
||||
public ListFormWorkflowAppService(
|
||||
IRepository<ListFormWorkflow, string> criteriaRepository,
|
||||
IRepository<Note, Guid> noteRepository,
|
||||
IListFormManager listFormManager,
|
||||
IListFormAuthorizationManager authManager,
|
||||
IListFormSelectAppService listFormSelectAppService,
|
||||
|
|
@ -48,6 +50,7 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
|
|||
ISettingProvider settingProvider)
|
||||
{
|
||||
this.criteriaRepository = criteriaRepository;
|
||||
this.noteRepository = noteRepository;
|
||||
this.listFormManager = listFormManager;
|
||||
this.authManager = authManager;
|
||||
this.listFormSelectAppService = listFormSelectAppService;
|
||||
|
|
@ -188,7 +191,7 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
|
|||
|
||||
criteria.ListFormCode = code;
|
||||
criteria.Kind = NormalizeRequired(input.Kind, "Compare");
|
||||
criteria.Title = NormalizeRequired(input.Title, criteria.Kind);
|
||||
criteria.Title = await NormalizeUniqueTitleAsync(code, criteria.Id, input.Kind, input.Title);
|
||||
criteria.CompareColumn = NormalizeRequired(input.CompareColumn, "Price");
|
||||
criteria.CompareOperator = NormalizeRequired(input.CompareOperator, ">");
|
||||
criteria.CompareValue = input.CompareValue;
|
||||
|
|
@ -365,6 +368,7 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
|
|||
}
|
||||
|
||||
var next = FindNextCriteria(context.Criteria, input.Approved ? current.NextOnApprove : current.NextOnReject);
|
||||
await LogWorkflowDecisionAsync(context, current, next, input.Approved, input.Note);
|
||||
return await RunUntilWaitAsync(context, next);
|
||||
}
|
||||
|
||||
|
|
@ -512,13 +516,16 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
|
|||
await UpdateRowAsync(context, update);
|
||||
MergeRowValues(context.Row, update);
|
||||
|
||||
string informedRecipient = null;
|
||||
if (node.Kind == "Inform")
|
||||
{
|
||||
await SendInformEmailAsync(context, node);
|
||||
}
|
||||
informedRecipient = await SendInformEmailAsync(context, node);
|
||||
}
|
||||
|
||||
private async Task SendInformEmailAsync(WorkflowRunContext context, ListFormWorkflow node)
|
||||
await LogWorkflowNodeAsync(context, node, informedRecipient);
|
||||
}
|
||||
|
||||
private async Task<string> SendInformEmailAsync(WorkflowRunContext context, ListFormWorkflow node)
|
||||
{
|
||||
var recipientEmail = await ResolveApproverEmailAsync(node.Approver);
|
||||
var senderName = await settingProvider.GetOrNullAsync(SeedConsts.AbpSettings.Mailing.Default.DefaultFromDisplayName);
|
||||
|
|
@ -542,6 +549,8 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
|
|||
{
|
||||
throw new UserFriendlyException($"Bilgilendirme maili gonderilemedi: {result.ErrorMessage}");
|
||||
}
|
||||
|
||||
return recipientEmail;
|
||||
}
|
||||
|
||||
private async Task<string> ResolveApproverEmailAsync(string approver)
|
||||
|
|
@ -581,6 +590,149 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
|
|||
""";
|
||||
}
|
||||
|
||||
private async Task LogWorkflowDecisionAsync(
|
||||
WorkflowRunContext context,
|
||||
ListFormWorkflow current,
|
||||
ListFormWorkflow next,
|
||||
bool approved,
|
||||
string description)
|
||||
{
|
||||
var action = approved ? "Approved" : "Rejected";
|
||||
var subject = $"Workflow {action}: {current.Title}";
|
||||
var rows = new List<(string Label, string Value)>();
|
||||
|
||||
rows.Add(("Description", description ?? string.Empty));
|
||||
rows.Add(("Next Step", FormatNode(next)));
|
||||
|
||||
await InsertWorkflowNoteAsync(context, subject, BuildWorkflowNoteContent(rows));
|
||||
}
|
||||
|
||||
private async Task LogWorkflowNodeAsync(
|
||||
WorkflowRunContext context,
|
||||
ListFormWorkflow node,
|
||||
string informedRecipient)
|
||||
{
|
||||
var action = node.Kind switch
|
||||
{
|
||||
"Start" => "Started",
|
||||
"Compare" => "Evaluated",
|
||||
"Approval" => "Waiting Approval",
|
||||
"Inform" => "Informed",
|
||||
"End" => "Completed",
|
||||
_ => "Processed"
|
||||
};
|
||||
|
||||
var subject = $"Workflow {action}: {node.Title}";
|
||||
var rows = new List<(string Label, string Value)>();
|
||||
|
||||
if (!node.Approver.IsNullOrWhiteSpace())
|
||||
{
|
||||
rows.Add((node.Kind == "Inform" ? "Inform Target" : "Approver", node.Approver));
|
||||
}
|
||||
if (!informedRecipient.IsNullOrWhiteSpace())
|
||||
{
|
||||
rows.Add(("Informed Email", informedRecipient));
|
||||
}
|
||||
|
||||
await InsertWorkflowNoteAsync(context, subject, BuildWorkflowNoteContent(rows));
|
||||
}
|
||||
|
||||
private async Task InsertWorkflowNoteAsync(
|
||||
WorkflowRunContext context,
|
||||
string subject,
|
||||
string content)
|
||||
{
|
||||
var key = context.Keys?.FirstOrDefault();
|
||||
if (key == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var note = new Note(GuidGenerator.Create())
|
||||
{
|
||||
TenantId = CurrentTenant.Id,
|
||||
EntityName = context.ListFormCode,
|
||||
EntityId = key.ToString(),
|
||||
Type = "workflow",
|
||||
Subject = subject,
|
||||
Content = content,
|
||||
FilesJson = "[]"
|
||||
};
|
||||
|
||||
await noteRepository.InsertAsync(note, autoSave: true);
|
||||
}
|
||||
|
||||
private static void AddWorkflowFieldRow(
|
||||
List<(string Label, string Value)> rows,
|
||||
WorkflowRunContext context,
|
||||
string label,
|
||||
string fieldName)
|
||||
{
|
||||
if (fieldName.IsNullOrWhiteSpace())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
rows.Add((label, FormatRowValue(GetRowValue(context.Row, fieldName))));
|
||||
}
|
||||
|
||||
private static string BuildWorkflowNoteContent(List<(string Label, string Value)> rows)
|
||||
{
|
||||
var tableRows = rows
|
||||
.Where(row => !row.Value.IsNullOrWhiteSpace())
|
||||
.Select(row =>
|
||||
$"<tr><td><strong>{Encode(row.Label)}</strong></td><td>{Encode(row.Value)}</td></tr>");
|
||||
|
||||
return $"<table class=\"workflow-note-log\">{string.Join(string.Empty, tableRows)}</table>";
|
||||
}
|
||||
|
||||
private string ResolveCurrentUserDisplayName()
|
||||
{
|
||||
return CurrentUser.UserName
|
||||
?? CurrentUser.Name
|
||||
?? CurrentUser.Id?.ToString()
|
||||
?? "System";
|
||||
}
|
||||
|
||||
private static string FormatNode(ListFormWorkflow node)
|
||||
{
|
||||
if (node == null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
var title = node.Title ?? string.Empty;
|
||||
var kind = node.Kind ?? string.Empty;
|
||||
return $"{title} ({kind} - {node.Id})";
|
||||
}
|
||||
|
||||
private static string FormatRowValue(object value)
|
||||
{
|
||||
if (value == null || value == DBNull.Value)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return value is DateTime dateTime
|
||||
? dateTime.ToString("yyyy-MM-dd HH:mm:ss")
|
||||
: value.ToString();
|
||||
}
|
||||
|
||||
private static string Encode(string value)
|
||||
{
|
||||
return WebUtility.HtmlEncode(value ?? string.Empty);
|
||||
}
|
||||
|
||||
private static string Truncate(string value, int maxLength)
|
||||
{
|
||||
if (value == null || value.Length <= maxLength)
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
return value[..maxLength];
|
||||
}
|
||||
|
||||
private async Task UpdateRowAsync(WorkflowRunContext context, Dictionary<string, object> data)
|
||||
{
|
||||
await queryManager.GenerateAndRunQueryAsync<int>(
|
||||
|
|
@ -906,6 +1058,36 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
|
|||
return value.IsNullOrWhiteSpace() ? fallback : value.Trim();
|
||||
}
|
||||
|
||||
private async Task<string> NormalizeUniqueTitleAsync(
|
||||
string listFormCode,
|
||||
string criteriaId,
|
||||
string kind,
|
||||
string title)
|
||||
{
|
||||
var baseTitle = NormalizeRequired(title, kind);
|
||||
var existingTitles = (await criteriaRepository.GetListAsync(x =>
|
||||
x.ListFormCode == listFormCode &&
|
||||
x.Id != criteriaId))
|
||||
.Select(x => x.Title?.Trim())
|
||||
.Where(x => !x.IsNullOrWhiteSpace())
|
||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
if (!existingTitles.Contains(baseTitle))
|
||||
{
|
||||
return baseTitle;
|
||||
}
|
||||
|
||||
var index = 1;
|
||||
var candidate = $"{baseTitle}{index}";
|
||||
while (existingTitles.Contains(candidate))
|
||||
{
|
||||
index++;
|
||||
candidate = $"{baseTitle}{index}";
|
||||
}
|
||||
|
||||
return candidate;
|
||||
}
|
||||
|
||||
private static string SerializeCompareOutcomes(List<CompareOutcomeDto> outcomes)
|
||||
{
|
||||
return JsonSerializer.Serialize(outcomes ?? []);
|
||||
|
|
|
|||
|
|
@ -4154,9 +4154,9 @@
|
|||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "ListForms.ListForm.NoteModal.Type.Activity",
|
||||
"en": "Activity",
|
||||
"tr": "Aktivite"
|
||||
"key": "ListForms.ListForm.NoteModal.Type.Workflow",
|
||||
"en": "Workflow",
|
||||
"tr": "Akış"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
|
|
|
|||
|
|
@ -1,25 +0,0 @@
|
|||
IF OBJECT_ID(N'[dbo].[Sal_T_Approval]', 'U') IS NULL
|
||||
BEGIN
|
||||
CREATE TABLE [dbo].[Sal_T_Approval]
|
||||
(
|
||||
[Id] uniqueidentifier NOT NULL DEFAULT NEWID(),
|
||||
[CreationTime] datetime2 NOT NULL DEFAULT GETUTCDATE(),
|
||||
[CreatorId] uniqueidentifier NULL,
|
||||
[LastModificationTime] datetime2 NULL,
|
||||
[LastModifierId] uniqueidentifier NULL,
|
||||
[IsDeleted] bit NOT NULL DEFAULT 0,
|
||||
[DeletionTime] datetime2 NULL,
|
||||
[DeleterId] uniqueidentifier NULL,
|
||||
[TenantId] uniqueidentifier NULL,
|
||||
[ApprovalUserName] nvarchar(256) NULL,
|
||||
[ApprovalStatus] nvarchar(50) NULL,
|
||||
[ApprovalDate] datetime NULL,
|
||||
[ApprovalDescription] nvarchar(200) NULL,
|
||||
[Name] nvarchar(100) NULL,
|
||||
CONSTRAINT [PK_Sal_T_Approval] PRIMARY KEY NONCLUSTERED
|
||||
(
|
||||
[Id] ASC
|
||||
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
|
||||
) ON [PRIMARY]
|
||||
END
|
||||
GO
|
||||
|
|
@ -1,384 +0,0 @@
|
|||
{
|
||||
"Wizard": {
|
||||
"WizardName": "Approval",
|
||||
"ListFormCode": "App.Wizard.Approval",
|
||||
"MenuCode": "App.Wizard.Approval",
|
||||
"IsTenant": true,
|
||||
"IsBranch": false,
|
||||
"IsOrganizationUnit": false,
|
||||
"AllowAdding": true,
|
||||
"AllowUpdating": true,
|
||||
"AllowDeleting": true,
|
||||
"AllowDetail": false,
|
||||
"ConfirmDelete": true,
|
||||
"DefaultLayout": "grid",
|
||||
"Grid": true,
|
||||
"Pivot": true,
|
||||
"Tree": true,
|
||||
"Chart": true,
|
||||
"Gantt": true,
|
||||
"Scheduler": true,
|
||||
"LanguageTextMenuEn": "Approval",
|
||||
"LanguageTextMenuTr": "Approval",
|
||||
"LanguageTextTitleEn": "Approval",
|
||||
"LanguageTextTitleTr": "Approval",
|
||||
"LanguageTextDescEn": "Approval",
|
||||
"LanguageTextDescTr": "Approval",
|
||||
"LanguageTextMenuParentEn": "",
|
||||
"LanguageTextMenuParentTr": "",
|
||||
"PermissionGroupName": "App.Wizard.Sales",
|
||||
"MenuParentCode": "App.Wizard.Sales",
|
||||
"MenuParentIcon": "FcAssistant",
|
||||
"MenuIcon": "FcBrokenLink",
|
||||
"DataSourceCode": "Default",
|
||||
"DataSourceConnectionString": "",
|
||||
"SelectCommandType": 1,
|
||||
"SelectCommand": "Sal_T_Approval",
|
||||
"KeyFieldName": "Id",
|
||||
"KeyFieldDbSourceType": 9,
|
||||
"TreeKeyExpr": "",
|
||||
"TreeParentIdExpr": "",
|
||||
"TreeAutoExpandAll": false,
|
||||
"GanttKeyExpr": "",
|
||||
"GanttParentIdExpr": "",
|
||||
"GanttAutoExpandAll": false,
|
||||
"GanttTitleExpr": "",
|
||||
"GanttStartExpr": "",
|
||||
"GanttEndExpr": "",
|
||||
"GanttProgressExpr": "",
|
||||
"SchedulerTextExpr": "",
|
||||
"SchedulerStartDateExpr": "",
|
||||
"SchedulerEndDateExpr": "",
|
||||
"Groups": [
|
||||
{
|
||||
"Caption": "",
|
||||
"ColCount": 1,
|
||||
"Items": [
|
||||
{
|
||||
"DataField": "Id",
|
||||
"CaptionName": "App.Listform.ListformField.Id",
|
||||
"EditorType": "dxTextBox",
|
||||
"EditorOptions": "",
|
||||
"EditorScript": "",
|
||||
"ColSpan": 1,
|
||||
"IsRequired": true,
|
||||
"IncludeInEditingForm": true,
|
||||
"DbSourceType": 9,
|
||||
"TurkishCaption": "Id",
|
||||
"EnglishCaption": "Id",
|
||||
"LookupDataSourceType": 1,
|
||||
"ValueExpr": "Key",
|
||||
"DisplayExpr": "Name",
|
||||
"LookupQuery": ""
|
||||
},
|
||||
{
|
||||
"DataField": "Name",
|
||||
"CaptionName": "App.Listform.ListformField.Name",
|
||||
"EditorType": "dxTextBox",
|
||||
"EditorOptions": "",
|
||||
"EditorScript": "",
|
||||
"ColSpan": 1,
|
||||
"IsRequired": false,
|
||||
"IncludeInEditingForm": true,
|
||||
"DbSourceType": 16,
|
||||
"TurkishCaption": "Name",
|
||||
"EnglishCaption": "Name",
|
||||
"LookupDataSourceType": 1,
|
||||
"ValueExpr": "Key",
|
||||
"DisplayExpr": "Name",
|
||||
"LookupQuery": ""
|
||||
},
|
||||
{
|
||||
"DataField": "ApprovalUserName",
|
||||
"CaptionName": "App.Listform.ListformField.ApprovalUserName",
|
||||
"EditorType": "dxTextBox",
|
||||
"EditorOptions": "",
|
||||
"EditorScript": "",
|
||||
"ColSpan": 1,
|
||||
"IsRequired": false,
|
||||
"IncludeInEditingForm": false,
|
||||
"DbSourceType": 16,
|
||||
"TurkishCaption": "Approval User Name",
|
||||
"EnglishCaption": "Approval User Name",
|
||||
"LookupDataSourceType": 1,
|
||||
"ValueExpr": "Key",
|
||||
"DisplayExpr": "Name",
|
||||
"LookupQuery": ""
|
||||
},
|
||||
{
|
||||
"DataField": "ApprovalStatus",
|
||||
"CaptionName": "App.Listform.ListformField.ApprovalStatus",
|
||||
"EditorType": "dxTextBox",
|
||||
"EditorOptions": "",
|
||||
"EditorScript": "",
|
||||
"ColSpan": 1,
|
||||
"IsRequired": false,
|
||||
"IncludeInEditingForm": false,
|
||||
"DbSourceType": 16,
|
||||
"TurkishCaption": "Approval Status",
|
||||
"EnglishCaption": "Approval Status",
|
||||
"LookupDataSourceType": 1,
|
||||
"ValueExpr": "Key",
|
||||
"DisplayExpr": "Name",
|
||||
"LookupQuery": ""
|
||||
},
|
||||
{
|
||||
"DataField": "ApprovalDate",
|
||||
"CaptionName": "App.Listform.ListformField.ApprovalDate",
|
||||
"EditorType": "dxDateBox",
|
||||
"EditorOptions": "",
|
||||
"EditorScript": "",
|
||||
"ColSpan": 1,
|
||||
"IsRequired": false,
|
||||
"IncludeInEditingForm": false,
|
||||
"DbSourceType": 6,
|
||||
"TurkishCaption": "Approval Date",
|
||||
"EnglishCaption": "Approval Date",
|
||||
"LookupDataSourceType": 1,
|
||||
"ValueExpr": "Key",
|
||||
"DisplayExpr": "Name",
|
||||
"LookupQuery": ""
|
||||
},
|
||||
{
|
||||
"DataField": "ApprovalDescription",
|
||||
"CaptionName": "App.Listform.ListformField.ApprovalDescription",
|
||||
"EditorType": "dxTextBox",
|
||||
"EditorOptions": "",
|
||||
"EditorScript": "",
|
||||
"ColSpan": 1,
|
||||
"IsRequired": false,
|
||||
"IncludeInEditingForm": false,
|
||||
"DbSourceType": 16,
|
||||
"TurkishCaption": "Approval Description",
|
||||
"EnglishCaption": "Approval Description",
|
||||
"LookupDataSourceType": 1,
|
||||
"ValueExpr": "Key",
|
||||
"DisplayExpr": "Name",
|
||||
"LookupQuery": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"SubForms": [],
|
||||
"Widgets": [],
|
||||
"Workflow": {
|
||||
"ApprovalUserFieldName": "ApprovalUserName",
|
||||
"ApprovalDateFieldName": "ApprovalDate",
|
||||
"ApprovalStatusFieldName": "ApprovalStatus",
|
||||
"ApprovalDescriptionFieldName": "ApprovalDescription",
|
||||
"Criteria": [
|
||||
{
|
||||
"ListFormCode": "App.Wizard.Approval",
|
||||
"Kind": "Start",
|
||||
"Title": "\u0130\u015F Ak\u0131\u015F\u0131 Ba\u015Flat",
|
||||
"CompareColumn": "Price",
|
||||
"CompareOperator": "\u003E",
|
||||
"CompareValue": 5000,
|
||||
"Approver": "",
|
||||
"NextOnStart": "WF1780768059929317",
|
||||
"NextOnTrue": "",
|
||||
"NextOnFalse": "",
|
||||
"NextOnApprove": "",
|
||||
"NextOnReject": "",
|
||||
"PositionX": 53,
|
||||
"PositionY": 42,
|
||||
"CompareOutcomes": [],
|
||||
"Id": "WF1780768057089996"
|
||||
},
|
||||
{
|
||||
"ListFormCode": "App.Wizard.Approval",
|
||||
"Kind": "Approval",
|
||||
"Title": "Onay1",
|
||||
"CompareColumn": "Price",
|
||||
"CompareOperator": "\u003E",
|
||||
"CompareValue": 5000,
|
||||
"Approver": "admin@sozsoft.com",
|
||||
"NextOnStart": "",
|
||||
"NextOnTrue": "",
|
||||
"NextOnFalse": "",
|
||||
"NextOnApprove": "WF1780768062793138",
|
||||
"NextOnReject": "WF1780768065817524",
|
||||
"PositionX": 356,
|
||||
"PositionY": 41,
|
||||
"CompareOutcomes": [],
|
||||
"Id": "WF1780768059929317"
|
||||
},
|
||||
{
|
||||
"ListFormCode": "App.Wizard.Approval",
|
||||
"Kind": "Approval",
|
||||
"Title": "Onay2",
|
||||
"CompareColumn": "Price",
|
||||
"CompareOperator": "\u003E",
|
||||
"CompareValue": 5000,
|
||||
"Approver": "demo@sozsoft.com",
|
||||
"NextOnStart": "",
|
||||
"NextOnTrue": "",
|
||||
"NextOnFalse": "",
|
||||
"NextOnApprove": "WF1780768065817524",
|
||||
"NextOnReject": "WF1780768065817524",
|
||||
"PositionX": 632,
|
||||
"PositionY": 38,
|
||||
"CompareOutcomes": [],
|
||||
"Id": "WF1780768062793138"
|
||||
},
|
||||
{
|
||||
"ListFormCode": "App.Wizard.Approval",
|
||||
"Kind": "Inform",
|
||||
"Title": "Bilgilendirme",
|
||||
"CompareColumn": "Price",
|
||||
"CompareOperator": "\u003E",
|
||||
"CompareValue": 5000,
|
||||
"Approver": "system@sozsoft.com",
|
||||
"NextOnStart": "WF1780768071444394",
|
||||
"NextOnTrue": "",
|
||||
"NextOnFalse": "",
|
||||
"NextOnApprove": "",
|
||||
"NextOnReject": "",
|
||||
"PositionX": 472,
|
||||
"PositionY": 417,
|
||||
"CompareOutcomes": [],
|
||||
"Id": "WF1780768065817524"
|
||||
},
|
||||
{
|
||||
"ListFormCode": "App.Wizard.Approval",
|
||||
"Kind": "End",
|
||||
"Title": "\u0130\u015F Ak\u0131\u015F\u0131 Bitir",
|
||||
"CompareColumn": "Price",
|
||||
"CompareOperator": "\u003E",
|
||||
"CompareValue": 5000,
|
||||
"Approver": "",
|
||||
"NextOnStart": "",
|
||||
"NextOnTrue": "",
|
||||
"NextOnFalse": "",
|
||||
"NextOnApprove": "",
|
||||
"NextOnReject": "",
|
||||
"PositionX": 850,
|
||||
"PositionY": 417,
|
||||
"CompareOutcomes": [],
|
||||
"Id": "WF1780768071444394"
|
||||
}
|
||||
]
|
||||
},
|
||||
"WorkflowCriteria": [
|
||||
{
|
||||
"ListFormCode": "App.Wizard.Approval",
|
||||
"Kind": "Start",
|
||||
"Title": "\u0130\u015F Ak\u0131\u015F\u0131 Ba\u015Flat",
|
||||
"CompareColumn": "Price",
|
||||
"CompareOperator": "\u003E",
|
||||
"CompareValue": 5000,
|
||||
"Approver": "",
|
||||
"NextOnStart": "WF1780768059929317",
|
||||
"NextOnTrue": "",
|
||||
"NextOnFalse": "",
|
||||
"NextOnApprove": "",
|
||||
"NextOnReject": "",
|
||||
"PositionX": 53,
|
||||
"PositionY": 42,
|
||||
"CompareOutcomes": [],
|
||||
"Id": "WF1780768057089996"
|
||||
},
|
||||
{
|
||||
"ListFormCode": "App.Wizard.Approval",
|
||||
"Kind": "Approval",
|
||||
"Title": "Onay1",
|
||||
"CompareColumn": "Price",
|
||||
"CompareOperator": "\u003E",
|
||||
"CompareValue": 5000,
|
||||
"Approver": "admin@sozsoft.com",
|
||||
"NextOnStart": "",
|
||||
"NextOnTrue": "",
|
||||
"NextOnFalse": "",
|
||||
"NextOnApprove": "WF1780768062793138",
|
||||
"NextOnReject": "WF1780768065817524",
|
||||
"PositionX": 356,
|
||||
"PositionY": 41,
|
||||
"CompareOutcomes": [],
|
||||
"Id": "WF1780768059929317"
|
||||
},
|
||||
{
|
||||
"ListFormCode": "App.Wizard.Approval",
|
||||
"Kind": "Approval",
|
||||
"Title": "Onay2",
|
||||
"CompareColumn": "Price",
|
||||
"CompareOperator": "\u003E",
|
||||
"CompareValue": 5000,
|
||||
"Approver": "demo@sozsoft.com",
|
||||
"NextOnStart": "",
|
||||
"NextOnTrue": "",
|
||||
"NextOnFalse": "",
|
||||
"NextOnApprove": "WF1780768065817524",
|
||||
"NextOnReject": "WF1780768065817524",
|
||||
"PositionX": 632,
|
||||
"PositionY": 38,
|
||||
"CompareOutcomes": [],
|
||||
"Id": "WF1780768062793138"
|
||||
},
|
||||
{
|
||||
"ListFormCode": "App.Wizard.Approval",
|
||||
"Kind": "Inform",
|
||||
"Title": "Bilgilendirme",
|
||||
"CompareColumn": "Price",
|
||||
"CompareOperator": "\u003E",
|
||||
"CompareValue": 5000,
|
||||
"Approver": "system@sozsoft.com",
|
||||
"NextOnStart": "WF1780768071444394",
|
||||
"NextOnTrue": "",
|
||||
"NextOnFalse": "",
|
||||
"NextOnApprove": "",
|
||||
"NextOnReject": "",
|
||||
"PositionX": 472,
|
||||
"PositionY": 417,
|
||||
"CompareOutcomes": [],
|
||||
"Id": "WF1780768065817524"
|
||||
},
|
||||
{
|
||||
"ListFormCode": "App.Wizard.Approval",
|
||||
"Kind": "End",
|
||||
"Title": "\u0130\u015F Ak\u0131\u015F\u0131 Bitir",
|
||||
"CompareColumn": "Price",
|
||||
"CompareOperator": "\u003E",
|
||||
"CompareValue": 5000,
|
||||
"Approver": "",
|
||||
"NextOnStart": "",
|
||||
"NextOnTrue": "",
|
||||
"NextOnFalse": "",
|
||||
"NextOnApprove": "",
|
||||
"NextOnReject": "",
|
||||
"PositionX": 850,
|
||||
"PositionY": 417,
|
||||
"CompareOutcomes": [],
|
||||
"Id": "WF1780768071444394"
|
||||
}
|
||||
]
|
||||
},
|
||||
"IsDeletedField": true,
|
||||
"IsCreatedField": true,
|
||||
"InsertedRecords": {
|
||||
"LanguageKeys": [
|
||||
"App.Wizard.Approval",
|
||||
"App.Wizard.Approval.Title",
|
||||
"App.Wizard.Approval.Desc",
|
||||
"App.Listform.ListformField.ApprovalUserName",
|
||||
"App.Listform.ListformField.ApprovalStatus",
|
||||
"App.Listform.ListformField.ApprovalDate",
|
||||
"App.Listform.ListformField.ApprovalDescription"
|
||||
],
|
||||
"PermissionGroupNames": [
|
||||
"App.Wizard.Sales"
|
||||
],
|
||||
"PermissionNames": [
|
||||
"App.Wizard.Approval",
|
||||
"App.Wizard.Approval.Create",
|
||||
"App.Wizard.Approval.Update",
|
||||
"App.Wizard.Approval.Delete",
|
||||
"App.Wizard.Approval.Export",
|
||||
"App.Wizard.Approval.Import",
|
||||
"App.Wizard.Approval.Note"
|
||||
],
|
||||
"MenuCodes": [
|
||||
"App.Wizard.Approval"
|
||||
],
|
||||
"DataSourceCodes": []
|
||||
}
|
||||
}
|
||||
|
|
@ -135,6 +135,7 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
|
|||
input.Workflow ??= new WorkflowDto();
|
||||
input.WorkflowCriteria ??= new List<ListFormWorkflowCriteriaDto>();
|
||||
input.Workflow.Criteria = input.WorkflowCriteria;
|
||||
EnsureUniqueWorkflowCriteriaTitles(input.WorkflowCriteria);
|
||||
|
||||
var wizardName = input.WizardName.Trim();
|
||||
var code = string.IsNullOrWhiteSpace(input.MenuCode)
|
||||
|
|
@ -332,7 +333,7 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
|
|||
PageSize = 10,
|
||||
ExportJson = WizardConsts.DefaultExportJson,
|
||||
IsSubForm = false,
|
||||
ShowNote = input.SubForms.Count > 0,
|
||||
ShowNote = input.SubForms.Count > 0 || input.WorkflowCriteria.Count > 0,
|
||||
LayoutJson = WizardConsts.DefaultLayoutJson(input.DefaultLayout, input.Grid, input.Pivot, input.Tree, input.Chart, input.Gantt, input.Scheduler),
|
||||
CultureName = LanguageCodes.En,
|
||||
ListFormCode = input.ListFormCode,
|
||||
|
|
@ -415,6 +416,60 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
|
|||
);
|
||||
}
|
||||
|
||||
private static void EnsureUniqueWorkflowCriteriaTitles(List<ListFormWorkflowCriteriaDto> criteria)
|
||||
{
|
||||
if (criteria == null || criteria.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var baseTitles = criteria
|
||||
.Select(x => string.IsNullOrWhiteSpace(x.Title) ? NormalizeWorkflowTitleFallback(x.Kind) : x.Title.Trim())
|
||||
.ToList();
|
||||
var duplicateTitles = baseTitles
|
||||
.GroupBy(x => x, StringComparer.OrdinalIgnoreCase)
|
||||
.Where(x => x.Count() > 1)
|
||||
.Select(x => x.Key)
|
||||
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||
var titleCounts = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||
var usedTitles = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var item in criteria)
|
||||
{
|
||||
var baseTitle = string.IsNullOrWhiteSpace(item.Title) ? NormalizeWorkflowTitleFallback(item.Kind) : item.Title.Trim();
|
||||
var title = baseTitle;
|
||||
|
||||
if (duplicateTitles.Contains(baseTitle))
|
||||
{
|
||||
titleCounts.TryGetValue(baseTitle, out var count);
|
||||
count++;
|
||||
titleCounts[baseTitle] = count;
|
||||
title = $"{baseTitle}{count}";
|
||||
}
|
||||
|
||||
if (usedTitles.Contains(title))
|
||||
{
|
||||
var index = 1;
|
||||
var candidate = $"{title}{index}";
|
||||
while (usedTitles.Contains(candidate))
|
||||
{
|
||||
index++;
|
||||
candidate = $"{title}{index}";
|
||||
}
|
||||
|
||||
title = candidate;
|
||||
}
|
||||
|
||||
item.Title = title;
|
||||
usedTitles.Add(title);
|
||||
}
|
||||
}
|
||||
|
||||
private static string NormalizeWorkflowTitleFallback(string kind)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(kind) ? "Step" : kind.Trim();
|
||||
}
|
||||
|
||||
private async Task CreateLangKeyAsync(string key, string textEn, string textTr)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key)) return;
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@ public static class WizardConsts
|
|||
R = permissionName,
|
||||
U = permissionName + ".Update",
|
||||
E = true,
|
||||
I = false,
|
||||
I = true,
|
||||
Deny = false
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,14 @@ namespace Sozsoft.Platform.Entities;
|
|||
|
||||
public class Note : FullAuditedEntity<Guid>, IMultiTenant
|
||||
{
|
||||
public Note()
|
||||
{
|
||||
}
|
||||
|
||||
public Note(Guid id) : base(id)
|
||||
{
|
||||
}
|
||||
|
||||
public Guid? TenantId { get; set; }
|
||||
public string EntityName { get; set; }
|
||||
public string EntityId { get; set; }
|
||||
|
|
|
|||
|
|
@ -507,6 +507,7 @@ public class PlatformDbContext :
|
|||
b.Property(x => x.PositionX).IsRequired();
|
||||
b.Property(x => x.PositionY).IsRequired();
|
||||
b.Property(x => x.CompareOutcomesJson).HasColumnType("text");
|
||||
b.HasIndex(x => new { x.ListFormCode, x.Title }).IsUnique();
|
||||
});
|
||||
|
||||
builder.Entity<Note>(b =>
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore;
|
|||
namespace Sozsoft.Platform.Migrations
|
||||
{
|
||||
[DbContext(typeof(PlatformDbContext))]
|
||||
[Migration("20260602070242_Initial")]
|
||||
[Migration("20260606212623_Initial")]
|
||||
partial class Initial
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
|
@ -3486,6 +3486,9 @@ namespace Sozsoft.Platform.Migrations
|
|||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ListFormCode", "Title")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Sas_H_ListFormWorkflow", (string)null);
|
||||
});
|
||||
|
||||
|
|
@ -3905,6 +3905,12 @@ namespace Sozsoft.Platform.Migrations
|
|||
table: "Sas_H_ListFormImportLog",
|
||||
column: "ImportId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Sas_H_ListFormWorkflow_ListFormCode_Title",
|
||||
table: "Sas_H_ListFormWorkflow",
|
||||
columns: new[] { "ListFormCode", "Title" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Sas_H_Menu_Code",
|
||||
table: "Sas_H_Menu",
|
||||
|
|
@ -3483,6 +3483,9 @@ namespace Sozsoft.Platform.Migrations
|
|||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ListFormCode", "Title")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Sas_H_ListFormWorkflow", (string)null);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -328,6 +328,91 @@ export function emptyCriteria(kind = 'Compare', listFormCode = ''): WorkflowCrit
|
|||
}
|
||||
}
|
||||
|
||||
export function uniqueCriteriaTitle(
|
||||
kind: string,
|
||||
criteria: Array<Pick<WorkflowCriteriaDto, 'id' | 'kind' | 'title'>>,
|
||||
currentId?: string | null,
|
||||
preferredTitle?: string | null,
|
||||
) {
|
||||
const hasPreferredTitle = Boolean(preferredTitle?.trim())
|
||||
const baseTitle = (preferredTitle || defaultTitle(kind)).trim()
|
||||
const usedTitles = new Set(
|
||||
criteria
|
||||
.filter((item) => !currentId || item.id !== currentId)
|
||||
.map((item) => (item.title || '').trim().toLocaleLowerCase('tr-TR'))
|
||||
.filter(Boolean),
|
||||
)
|
||||
|
||||
if (!hasPreferredTitle) {
|
||||
const sameKindCount = criteria.filter(
|
||||
(item) =>
|
||||
(!currentId || item.id !== currentId) &&
|
||||
item.kind === kind &&
|
||||
isDefaultTitleVariant(item.title, baseTitle),
|
||||
).length
|
||||
let index = sameKindCount + 1
|
||||
let candidate = `${baseTitle}${index}`
|
||||
while (usedTitles.has(candidate.toLocaleLowerCase('tr-TR'))) {
|
||||
index += 1
|
||||
candidate = `${baseTitle}${index}`
|
||||
}
|
||||
|
||||
return candidate
|
||||
}
|
||||
|
||||
if (!usedTitles.has(baseTitle.toLocaleLowerCase('tr-TR'))) {
|
||||
return baseTitle
|
||||
}
|
||||
|
||||
let index = 1
|
||||
let candidate = `${baseTitle}${index}`
|
||||
while (usedTitles.has(candidate.toLocaleLowerCase('tr-TR'))) {
|
||||
index += 1
|
||||
candidate = `${baseTitle}${index}`
|
||||
}
|
||||
|
||||
return candidate
|
||||
}
|
||||
|
||||
export function uniqueCriteriaId(
|
||||
criteria: Array<Pick<WorkflowCriteriaDto, 'id'>>,
|
||||
reservedIds: string[] = [],
|
||||
) {
|
||||
const usedIds = new Set(
|
||||
[...criteria.map((item) => item.id), ...reservedIds]
|
||||
.map((id) => (id || '').trim().toLocaleLowerCase('tr-TR'))
|
||||
.filter(Boolean),
|
||||
)
|
||||
const maxNumber = [...usedIds].reduce((max, id) => Math.max(max, parseCriteriaIdNumber(id)), 0)
|
||||
let nextNumber = maxNumber + 1
|
||||
let candidate = formatCriteriaId(nextNumber)
|
||||
|
||||
while (usedIds.has(candidate.toLocaleLowerCase('tr-TR'))) {
|
||||
nextNumber += 1
|
||||
candidate = formatCriteriaId(nextNumber)
|
||||
}
|
||||
|
||||
return candidate
|
||||
}
|
||||
|
||||
function parseCriteriaIdNumber(id: string) {
|
||||
const match = id.match(/^(?:n)?(\d+)$/iu)
|
||||
return match ? Number(match[1]) : 0
|
||||
}
|
||||
|
||||
function formatCriteriaId(number: number) {
|
||||
return `N${String(number).padStart(3, '0')}`.slice(-4)
|
||||
}
|
||||
|
||||
function isDefaultTitleVariant(title: string | null | undefined, baseTitle: string) {
|
||||
const normalized = (title || '').trim()
|
||||
return normalized === baseTitle || new RegExp(`^${escapeRegExp(baseTitle)}\\d+$`, 'u').test(normalized)
|
||||
}
|
||||
|
||||
function escapeRegExp(value: string) {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
}
|
||||
|
||||
export function toCriteriaForm(item: WorkflowCriteriaDto): WorkflowCriteriaForm {
|
||||
const sharedPerson = item.approver || ''
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import {
|
|||
emptyCriteria,
|
||||
normalizeCriteria,
|
||||
toCriteriaForm,
|
||||
uniqueCriteriaTitle,
|
||||
type WorkflowCriteriaForm,
|
||||
} from '@/utils/workflow/workflowHelpers'
|
||||
import { workflowService, type WorkflowCriteriaDto } from '@/services/workflow.service'
|
||||
|
|
@ -112,9 +113,17 @@ export function FormTabWorkflow(
|
|||
const saveCriteria = (event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault()
|
||||
runAction(async () => {
|
||||
const normalized = normalizeCriteria(criteriaForm)
|
||||
const preferredTitle = criteriaForm.title || normalized.title
|
||||
await workflowService.saveCriteria({
|
||||
...normalizeCriteria(criteriaForm),
|
||||
...normalized,
|
||||
listFormCode: props.listFormCode,
|
||||
title: uniqueCriteriaTitle(
|
||||
normalized.kind || '',
|
||||
currentCriteria,
|
||||
normalized.id,
|
||||
preferredTitle,
|
||||
),
|
||||
})
|
||||
setSelectedId('')
|
||||
})
|
||||
|
|
@ -123,9 +132,11 @@ export function FormTabWorkflow(
|
|||
const addCriteria = (kind: string) => {
|
||||
setDesignerTab('flow')
|
||||
runAction(async () => {
|
||||
const nextTitle = uniqueCriteriaTitle(kind, currentCriteria)
|
||||
const saved = await workflowService.saveCriteria({
|
||||
...normalizeCriteria(emptyCriteria(kind, props.listFormCode)),
|
||||
listFormCode: props.listFormCode,
|
||||
title: nextTitle,
|
||||
positionX: 80 + (currentCriteria.length % 5) * 230,
|
||||
positionY: 220 + Math.floor(currentCriteria.length / 5) * 140,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ import {
|
|||
emptyCriteria,
|
||||
normalizeCriteria,
|
||||
toCriteriaForm,
|
||||
uniqueCriteriaId,
|
||||
uniqueCriteriaTitle,
|
||||
type WorkflowCriteriaForm,
|
||||
} from '@/utils/workflow/workflowHelpers'
|
||||
import { Field, FieldProps, Form, Formik } from 'formik'
|
||||
|
|
@ -45,8 +47,6 @@ const toDesignerCriteria = (items: ListFormWorkflowCriteriaDto[]): WorkflowCrite
|
|||
const toWizardCriteria = (items: WorkflowCriteriaDto[]): ListFormWorkflowCriteriaDto[] =>
|
||||
items.map(({ nodeId: _nodeId, ...item }) => item)
|
||||
|
||||
const nextId = () => `WF${Date.now()}${Math.floor(Math.random() * 1000)}`
|
||||
|
||||
function WizardStep6({
|
||||
listFormCode,
|
||||
workflow,
|
||||
|
|
@ -96,9 +96,10 @@ function WizardStep6({
|
|||
const saveCriteria = (event: FormEvent<HTMLFormElement>) => {
|
||||
event.preventDefault()
|
||||
const normalized = normalizeCriteria({ ...criteriaForm, listFormCode })
|
||||
const id = normalized.id || nextId()
|
||||
const nextItem = { ...normalized, id, nodeId: id } as WorkflowCriteriaDto
|
||||
const id = normalized.id || uniqueCriteriaId(currentCriteria)
|
||||
const exists = currentCriteria.some((item) => item.id === id)
|
||||
const title = uniqueCriteriaTitle(normalized.kind || '', currentCriteria, id, normalized.title)
|
||||
const nextItem = { ...normalized, id, nodeId: id, title } as WorkflowCriteriaDto
|
||||
updateCriteria(
|
||||
exists
|
||||
? currentCriteria.map((item) => (item.id === id ? nextItem : item))
|
||||
|
|
@ -109,11 +110,12 @@ function WizardStep6({
|
|||
}
|
||||
|
||||
const addCriteria = (kind: string) => {
|
||||
const id = nextId()
|
||||
const id = uniqueCriteriaId(currentCriteria)
|
||||
const nextItem = {
|
||||
...normalizeCriteria(emptyCriteria(kind, listFormCode)),
|
||||
id,
|
||||
nodeId: id,
|
||||
title: uniqueCriteriaTitle(kind, currentCriteria),
|
||||
positionX: 80 + (currentCriteria.length % 5) * 230,
|
||||
positionY: 220 + Math.floor(currentCriteria.length / 5) * 140,
|
||||
} as WorkflowCriteriaDto
|
||||
|
|
@ -193,14 +195,15 @@ function WizardStep6({
|
|||
}
|
||||
|
||||
const resetDemo = () => {
|
||||
const startId = nextId()
|
||||
const approvalId = nextId()
|
||||
const endId = nextId()
|
||||
const startId = uniqueCriteriaId([])
|
||||
const approvalId = uniqueCriteriaId([], [startId])
|
||||
const endId = uniqueCriteriaId([], [startId, approvalId])
|
||||
updateCriteria([
|
||||
{
|
||||
...normalizeCriteria(emptyCriteria('Start', listFormCode)),
|
||||
id: startId,
|
||||
nodeId: startId,
|
||||
title: uniqueCriteriaTitle('Start', []),
|
||||
nextOnStart: approvalId,
|
||||
positionX: 72,
|
||||
positionY: 160,
|
||||
|
|
@ -209,6 +212,7 @@ function WizardStep6({
|
|||
...normalizeCriteria(emptyCriteria('Approval', listFormCode)),
|
||||
id: approvalId,
|
||||
nodeId: approvalId,
|
||||
title: uniqueCriteriaTitle('Approval', []),
|
||||
nextOnApprove: endId,
|
||||
positionX: 360,
|
||||
positionY: 160,
|
||||
|
|
@ -217,6 +221,7 @@ function WizardStep6({
|
|||
...normalizeCriteria(emptyCriteria('End', listFormCode)),
|
||||
id: endId,
|
||||
nodeId: endId,
|
||||
title: uniqueCriteriaTitle('End', []),
|
||||
positionX: 650,
|
||||
positionY: 160,
|
||||
} as WorkflowCriteriaDto,
|
||||
|
|
|
|||
|
|
@ -73,6 +73,11 @@ export const NoteList: React.FC<NoteListProps> = ({
|
|||
icon: <FaEnvelope className="text-green-500" />,
|
||||
border: 'border-green-400',
|
||||
}
|
||||
case 'workflow':
|
||||
return {
|
||||
icon: <FaHistory className="text-purple-500" />,
|
||||
border: 'border-purple-400',
|
||||
}
|
||||
default:
|
||||
return {
|
||||
icon: <FaStickyNote className="text-gray-400" />,
|
||||
|
|
@ -435,7 +440,7 @@ export const NoteList: React.FC<NoteListProps> = ({
|
|||
</div>
|
||||
|
||||
{/* Sil butonu */}
|
||||
{user?.id === note.creatorId && (
|
||||
{user?.id === note.creatorId && note.type !== 'workflow' && (
|
||||
<Button
|
||||
variant="plain"
|
||||
size="sm"
|
||||
|
|
|
|||
|
|
@ -49,7 +49,6 @@ function NoteModalContent({
|
|||
const types = [
|
||||
{ value: 'note', label: translate('::ListForms.ListForm.NoteModal.Type.Note') },
|
||||
{ value: 'message', label: translate('::ListForms.ListForm.NoteModal.Type.Message') },
|
||||
{ value: 'activity', label: translate('::ListForms.ListForm.NoteModal.Type.Activity') },
|
||||
]
|
||||
|
||||
const handleSave = async (values: any) => {
|
||||
|
|
|
|||
|
|
@ -77,6 +77,7 @@ import { useListFormColumns } from './useListFormColumns'
|
|||
import { Loading } from '@/components/shared'
|
||||
import { useStoreState } from '@/store'
|
||||
import { workflowService } from '@/services/workflow.service'
|
||||
import { NotePanel } from '../form/notes/NotePanel'
|
||||
|
||||
interface GridProps {
|
||||
listFormCode: string
|
||||
|
|
@ -262,6 +263,10 @@ const Grid = (props: GridProps) => {
|
|||
const [gridDto, setGridDto] = useState<GridDto>()
|
||||
const [isPopupFullScreen, setIsPopupFullScreen] = useState(false)
|
||||
const [widgetGroupHeight, setWidgetGroupHeight] = useState(0)
|
||||
const [notePanelTarget, setNotePanelTarget] = useState<{
|
||||
entityName: string
|
||||
entityId: string
|
||||
} | null>(null)
|
||||
|
||||
type EditorOptionsWithButtons = {
|
||||
buttons?: any[]
|
||||
|
|
@ -338,11 +343,29 @@ const Grid = (props: GridProps) => {
|
|||
})
|
||||
|
||||
const { createSelectDataSource } = useListFormCustomDataSource({ gridRef })
|
||||
|
||||
const openNotePanel = useCallback(
|
||||
(rowData: Record<string, any>) => {
|
||||
const keyFieldName = gridDto?.gridOptions.keyFieldName
|
||||
const entityId = getValueByField(rowData, keyFieldName)
|
||||
if (entityId === undefined || entityId === null || entityId === '') {
|
||||
return
|
||||
}
|
||||
|
||||
setNotePanelTarget({
|
||||
entityName: gridDto?.gridOptions.listFormCode ?? listFormCode,
|
||||
entityId: String(entityId),
|
||||
})
|
||||
},
|
||||
[gridDto, listFormCode],
|
||||
)
|
||||
|
||||
const { getBandedColumns } = useListFormColumns({
|
||||
gridDto,
|
||||
listFormCode,
|
||||
isSubForm,
|
||||
gridRef,
|
||||
onShowNote: openNotePanel,
|
||||
})
|
||||
|
||||
const getSelectedRowKeys = useCallback(async () => {
|
||||
|
|
@ -1874,6 +1897,14 @@ const Grid = (props: GridProps) => {
|
|||
{toolbarModalData?.content}
|
||||
</Dialog>
|
||||
<GridFilterDialogs gridRef={gridRef} listFormCode={listFormCode} {...filterData} />
|
||||
{notePanelTarget && (
|
||||
<NotePanel
|
||||
entityName={notePanelTarget.entityName}
|
||||
entityId={notePanelTarget.entityId}
|
||||
isVisible
|
||||
onToggle={() => setNotePanelTarget(null)}
|
||||
/>
|
||||
)}
|
||||
</Container>
|
||||
</>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -68,6 +68,7 @@ import { useStoreState } from '@/store/store'
|
|||
import SubForms from '../form/SubForms'
|
||||
import { ImportDashboard } from '@/components/importManager/ImportDashboard'
|
||||
import { workflowService } from '@/services/workflow.service'
|
||||
import { NotePanel } from '../form/notes/NotePanel'
|
||||
|
||||
interface TreeProps {
|
||||
listFormCode: string
|
||||
|
|
@ -250,6 +251,10 @@ const Tree = (props: TreeProps) => {
|
|||
const [gridDto, setGridDto] = useState<GridDto>()
|
||||
const [isPopupFullScreen, setIsPopupFullScreen] = useState(false)
|
||||
const [widgetGroupHeight, setWidgetGroupHeight] = useState(0)
|
||||
const [notePanelTarget, setNotePanelTarget] = useState<{
|
||||
entityName: string
|
||||
entityId: string
|
||||
} | null>(null)
|
||||
|
||||
const [expandedRowKeys, setExpandedRowKeys] = useState<any[]>([])
|
||||
|
||||
|
|
@ -343,11 +348,29 @@ const Tree = (props: TreeProps) => {
|
|||
})
|
||||
|
||||
const { createSelectDataSource } = useListFormCustomDataSource({ gridRef })
|
||||
|
||||
const openNotePanel = useCallback(
|
||||
(rowData: Record<string, any>) => {
|
||||
const keyFieldName = gridDto?.gridOptions.keyFieldName
|
||||
const entityId = getValueByField(rowData, keyFieldName)
|
||||
if (entityId === undefined || entityId === null || entityId === '') {
|
||||
return
|
||||
}
|
||||
|
||||
setNotePanelTarget({
|
||||
entityName: gridDto?.gridOptions.listFormCode ?? listFormCode,
|
||||
entityId: String(entityId),
|
||||
})
|
||||
},
|
||||
[gridDto, listFormCode],
|
||||
)
|
||||
|
||||
const { getBandedColumns } = useListFormColumns({
|
||||
gridDto,
|
||||
listFormCode,
|
||||
isSubForm,
|
||||
gridRef,
|
||||
onShowNote: openNotePanel,
|
||||
})
|
||||
|
||||
const extractSearchParamsFields = useCallback((filter: any): [string, string, any][] => {
|
||||
|
|
@ -1613,6 +1636,14 @@ const Tree = (props: TreeProps) => {
|
|||
{toolbarModalData?.content}
|
||||
</Dialog>
|
||||
<GridFilterDialogs gridRef={gridRef as any} listFormCode={listFormCode} {...filterData} />
|
||||
{notePanelTarget && (
|
||||
<NotePanel
|
||||
entityName={notePanelTarget.entityName}
|
||||
entityId={notePanelTarget.entityId}
|
||||
isVisible
|
||||
onToggle={() => setNotePanelTarget(null)}
|
||||
/>
|
||||
)}
|
||||
</Container>
|
||||
</>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -247,11 +247,13 @@ const useListFormColumns = ({
|
|||
listFormCode,
|
||||
isSubForm,
|
||||
gridRef,
|
||||
onShowNote,
|
||||
}: {
|
||||
gridDto?: GridDto
|
||||
listFormCode: string
|
||||
isSubForm?: boolean
|
||||
gridRef?: any
|
||||
onShowNote?: (rowData: Record<string, any>) => void
|
||||
}) => {
|
||||
const dialog: any = useDialogContext()
|
||||
const { translate } = useLocalization()
|
||||
|
|
@ -435,6 +437,11 @@ const useListFormColumns = ({
|
|||
gridDto.gridOptions.editingOptionDto.allowDetail &&
|
||||
checkPermission(gridDto.gridOptions.permissionDto.u)
|
||||
|
||||
const hasShowNote =
|
||||
gridDto.gridOptions.showNote &&
|
||||
checkPermission(gridDto.gridOptions.permissionDto.n) &&
|
||||
typeof onShowNote === 'function'
|
||||
|
||||
const hasDuplicate =
|
||||
gridDto.gridOptions.editingOptionDto.allowDuplicate &&
|
||||
checkPermission(gridDto.gridOptions.permissionDto.i)
|
||||
|
|
@ -442,7 +449,15 @@ const useListFormColumns = ({
|
|||
const hasCommandButtons = gridDto.gridOptions.commandColumnDto.length > 0
|
||||
|
||||
// Eğer hiçbir buton eklenecek durumda değilse: çık
|
||||
if (!hasUpdate && !hasDelete && !hasCreate && !hasCommandButtons) {
|
||||
if (
|
||||
!hasUpdate &&
|
||||
!hasDelete &&
|
||||
!hasCreate &&
|
||||
!hasDetail &&
|
||||
!hasShowNote &&
|
||||
!hasDuplicate &&
|
||||
!hasCommandButtons
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -485,6 +500,20 @@ const useListFormColumns = ({
|
|||
buttons.push(item)
|
||||
}
|
||||
|
||||
if (hasShowNote) {
|
||||
buttons.push({
|
||||
name: 'note',
|
||||
text: translate('::ListForms.ListForm.NoteModal.Type.Note'),
|
||||
onClick: (e: any) => {
|
||||
if (typeof e.event?.preventDefault === 'function') {
|
||||
e.event.preventDefault()
|
||||
}
|
||||
|
||||
onShowNote?.(e.row.data)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if (hasDuplicate) {
|
||||
const item = {
|
||||
name: 'duplicate',
|
||||
|
|
@ -600,7 +629,7 @@ const useListFormColumns = ({
|
|||
}
|
||||
|
||||
return column as GridColumnData
|
||||
}, [gridDto, checkPermission, translate, listFormCode, isPwaMode, dialog, gridRef])
|
||||
}, [gridDto, checkPermission, translate, listFormCode, isPwaMode, dialog, gridRef, onShowNote])
|
||||
|
||||
const getColumns = useCallback(
|
||||
(columnFormats: ColumnFormatDto[]) => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue