Workflow Listlerinin Note Özelliği eklendi

This commit is contained in:
Sedat Öztürk 2026-06-07 01:22:35 +03:00
parent 27e65f05f0
commit c204eef755
20 changed files with 532 additions and 432 deletions

View file

@ -278,6 +278,7 @@ public class ListFormWizardAppService(
var isCreated = tableColumns.Contains("CreatorId"); var isCreated = tableColumns.Contains("CreatorId");
input.Workflow ??= new WorkflowDto(); input.Workflow ??= new WorkflowDto();
input.Workflow.Criteria = input.WorkflowCriteria; input.Workflow.Criteria = input.WorkflowCriteria;
EnsureUniqueWorkflowCriteriaTitles(input.WorkflowCriteria);
var listForm = await repoListForm.InsertAsync(new ListForm var listForm = await repoListForm.InsertAsync(new ListForm
{ {
@ -285,7 +286,7 @@ public class ListFormWizardAppService(
PageSize = 10, PageSize = 10,
ExportJson = WizardConsts.DefaultExportJson, ExportJson = WizardConsts.DefaultExportJson,
IsSubForm = false, 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), LayoutJson = WizardConsts.DefaultLayoutJson(input.DefaultLayout, input.Grid, input.Pivot, input.Tree, input.Chart, input.Gantt, input.Scheduler),
CultureName = LanguageCodes.En, CultureName = LanguageCodes.En,
ListFormCode = input.ListFormCode, 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> /// <summary>
/// Wizard konfigürasyonunu JSON dosyası olarak kaydeder. /// Wizard konfigürasyonunu JSON dosyası olarak kaydeder.
/// Önce ContentRootPath'ten yukarı çıkarak Sozsoft.Platform.DbMigrator/Seeds/WizardData dizinini arar. /// Önce ContentRootPath'ten yukarı çıkarak Sozsoft.Platform.DbMigrator/Seeds/WizardData dizinini arar.

View file

@ -29,6 +29,7 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
private const string SystemApprovalDescription = "Sistem tarafından otomatik olarak onaylandı."; private const string SystemApprovalDescription = "Sistem tarafından otomatik olarak onaylandı.";
private readonly IRepository<ListFormWorkflow, string> criteriaRepository; private readonly IRepository<ListFormWorkflow, string> criteriaRepository;
private readonly IRepository<Note, Guid> noteRepository;
private readonly IListFormManager listFormManager; private readonly IListFormManager listFormManager;
private readonly IListFormAuthorizationManager authManager; private readonly IListFormAuthorizationManager authManager;
private readonly IListFormSelectAppService listFormSelectAppService; private readonly IListFormSelectAppService listFormSelectAppService;
@ -39,6 +40,7 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
public ListFormWorkflowAppService( public ListFormWorkflowAppService(
IRepository<ListFormWorkflow, string> criteriaRepository, IRepository<ListFormWorkflow, string> criteriaRepository,
IRepository<Note, Guid> noteRepository,
IListFormManager listFormManager, IListFormManager listFormManager,
IListFormAuthorizationManager authManager, IListFormAuthorizationManager authManager,
IListFormSelectAppService listFormSelectAppService, IListFormSelectAppService listFormSelectAppService,
@ -48,6 +50,7 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
ISettingProvider settingProvider) ISettingProvider settingProvider)
{ {
this.criteriaRepository = criteriaRepository; this.criteriaRepository = criteriaRepository;
this.noteRepository = noteRepository;
this.listFormManager = listFormManager; this.listFormManager = listFormManager;
this.authManager = authManager; this.authManager = authManager;
this.listFormSelectAppService = listFormSelectAppService; this.listFormSelectAppService = listFormSelectAppService;
@ -188,7 +191,7 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
criteria.ListFormCode = code; criteria.ListFormCode = code;
criteria.Kind = NormalizeRequired(input.Kind, "Compare"); 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.CompareColumn = NormalizeRequired(input.CompareColumn, "Price");
criteria.CompareOperator = NormalizeRequired(input.CompareOperator, ">"); criteria.CompareOperator = NormalizeRequired(input.CompareOperator, ">");
criteria.CompareValue = input.CompareValue; criteria.CompareValue = input.CompareValue;
@ -365,6 +368,7 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
} }
var next = FindNextCriteria(context.Criteria, input.Approved ? current.NextOnApprove : current.NextOnReject); 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); return await RunUntilWaitAsync(context, next);
} }
@ -512,13 +516,16 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
await UpdateRowAsync(context, update); await UpdateRowAsync(context, update);
MergeRowValues(context.Row, update); MergeRowValues(context.Row, update);
string informedRecipient = null;
if (node.Kind == "Inform") if (node.Kind == "Inform")
{ {
await SendInformEmailAsync(context, node); informedRecipient = await SendInformEmailAsync(context, node);
} }
await LogWorkflowNodeAsync(context, node, informedRecipient);
} }
private async Task SendInformEmailAsync(WorkflowRunContext context, ListFormWorkflow node) private async Task<string> SendInformEmailAsync(WorkflowRunContext context, ListFormWorkflow node)
{ {
var recipientEmail = await ResolveApproverEmailAsync(node.Approver); var recipientEmail = await ResolveApproverEmailAsync(node.Approver);
var senderName = await settingProvider.GetOrNullAsync(SeedConsts.AbpSettings.Mailing.Default.DefaultFromDisplayName); 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}"); throw new UserFriendlyException($"Bilgilendirme maili gonderilemedi: {result.ErrorMessage}");
} }
return recipientEmail;
} }
private async Task<string> ResolveApproverEmailAsync(string approver) 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) private async Task UpdateRowAsync(WorkflowRunContext context, Dictionary<string, object> data)
{ {
await queryManager.GenerateAndRunQueryAsync<int>( await queryManager.GenerateAndRunQueryAsync<int>(
@ -906,6 +1058,36 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
return value.IsNullOrWhiteSpace() ? fallback : value.Trim(); 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) private static string SerializeCompareOutcomes(List<CompareOutcomeDto> outcomes)
{ {
return JsonSerializer.Serialize(outcomes ?? []); return JsonSerializer.Serialize(outcomes ?? []);

View file

@ -4154,9 +4154,9 @@
}, },
{ {
"resourceName": "Platform", "resourceName": "Platform",
"key": "ListForms.ListForm.NoteModal.Type.Activity", "key": "ListForms.ListForm.NoteModal.Type.Workflow",
"en": "Activity", "en": "Workflow",
"tr": "Aktivite" "tr": "Akış"
}, },
{ {
"resourceName": "Platform", "resourceName": "Platform",

View file

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

View file

@ -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": []
}
}

View file

@ -135,6 +135,7 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
input.Workflow ??= new WorkflowDto(); input.Workflow ??= new WorkflowDto();
input.WorkflowCriteria ??= new List<ListFormWorkflowCriteriaDto>(); input.WorkflowCriteria ??= new List<ListFormWorkflowCriteriaDto>();
input.Workflow.Criteria = input.WorkflowCriteria; input.Workflow.Criteria = input.WorkflowCriteria;
EnsureUniqueWorkflowCriteriaTitles(input.WorkflowCriteria);
var wizardName = input.WizardName.Trim(); var wizardName = input.WizardName.Trim();
var code = string.IsNullOrWhiteSpace(input.MenuCode) var code = string.IsNullOrWhiteSpace(input.MenuCode)
@ -332,7 +333,7 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
PageSize = 10, PageSize = 10,
ExportJson = WizardConsts.DefaultExportJson, ExportJson = WizardConsts.DefaultExportJson,
IsSubForm = false, 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), LayoutJson = WizardConsts.DefaultLayoutJson(input.DefaultLayout, input.Grid, input.Pivot, input.Tree, input.Chart, input.Gantt, input.Scheduler),
CultureName = LanguageCodes.En, CultureName = LanguageCodes.En,
ListFormCode = input.ListFormCode, 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) private async Task CreateLangKeyAsync(string key, string textEn, string textTr)
{ {
if (string.IsNullOrWhiteSpace(key)) return; if (string.IsNullOrWhiteSpace(key)) return;

View file

@ -134,7 +134,7 @@ public static class WizardConsts
R = permissionName, R = permissionName,
U = permissionName + ".Update", U = permissionName + ".Update",
E = true, E = true,
I = false, I = true,
Deny = false Deny = false
}); });
} }

View file

@ -6,6 +6,14 @@ namespace Sozsoft.Platform.Entities;
public class Note : FullAuditedEntity<Guid>, IMultiTenant public class Note : FullAuditedEntity<Guid>, IMultiTenant
{ {
public Note()
{
}
public Note(Guid id) : base(id)
{
}
public Guid? TenantId { get; set; } public Guid? TenantId { get; set; }
public string EntityName { get; set; } public string EntityName { get; set; }
public string EntityId { get; set; } public string EntityId { get; set; }

View file

@ -507,6 +507,7 @@ public class PlatformDbContext :
b.Property(x => x.PositionX).IsRequired(); b.Property(x => x.PositionX).IsRequired();
b.Property(x => x.PositionY).IsRequired(); b.Property(x => x.PositionY).IsRequired();
b.Property(x => x.CompareOutcomesJson).HasColumnType("text"); b.Property(x => x.CompareOutcomesJson).HasColumnType("text");
b.HasIndex(x => new { x.ListFormCode, x.Title }).IsUnique();
}); });
builder.Entity<Note>(b => builder.Entity<Note>(b =>

View file

@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore;
namespace Sozsoft.Platform.Migrations namespace Sozsoft.Platform.Migrations
{ {
[DbContext(typeof(PlatformDbContext))] [DbContext(typeof(PlatformDbContext))]
[Migration("20260602070242_Initial")] [Migration("20260606212623_Initial")]
partial class Initial partial class Initial
{ {
/// <inheritdoc /> /// <inheritdoc />
@ -3486,6 +3486,9 @@ namespace Sozsoft.Platform.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("ListFormCode", "Title")
.IsUnique();
b.ToTable("Sas_H_ListFormWorkflow", (string)null); b.ToTable("Sas_H_ListFormWorkflow", (string)null);
}); });

View file

@ -3905,6 +3905,12 @@ namespace Sozsoft.Platform.Migrations
table: "Sas_H_ListFormImportLog", table: "Sas_H_ListFormImportLog",
column: "ImportId"); column: "ImportId");
migrationBuilder.CreateIndex(
name: "IX_Sas_H_ListFormWorkflow_ListFormCode_Title",
table: "Sas_H_ListFormWorkflow",
columns: new[] { "ListFormCode", "Title" },
unique: true);
migrationBuilder.CreateIndex( migrationBuilder.CreateIndex(
name: "IX_Sas_H_Menu_Code", name: "IX_Sas_H_Menu_Code",
table: "Sas_H_Menu", table: "Sas_H_Menu",

View file

@ -3483,6 +3483,9 @@ namespace Sozsoft.Platform.Migrations
b.HasKey("Id"); b.HasKey("Id");
b.HasIndex("ListFormCode", "Title")
.IsUnique();
b.ToTable("Sas_H_ListFormWorkflow", (string)null); b.ToTable("Sas_H_ListFormWorkflow", (string)null);
}); });

View file

@ -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 { export function toCriteriaForm(item: WorkflowCriteriaDto): WorkflowCriteriaForm {
const sharedPerson = item.approver || '' const sharedPerson = item.approver || ''

View file

@ -5,6 +5,7 @@ import {
emptyCriteria, emptyCriteria,
normalizeCriteria, normalizeCriteria,
toCriteriaForm, toCriteriaForm,
uniqueCriteriaTitle,
type WorkflowCriteriaForm, type WorkflowCriteriaForm,
} from '@/utils/workflow/workflowHelpers' } from '@/utils/workflow/workflowHelpers'
import { workflowService, type WorkflowCriteriaDto } from '@/services/workflow.service' import { workflowService, type WorkflowCriteriaDto } from '@/services/workflow.service'
@ -112,9 +113,17 @@ export function FormTabWorkflow(
const saveCriteria = (event: FormEvent<HTMLFormElement>) => { const saveCriteria = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault() event.preventDefault()
runAction(async () => { runAction(async () => {
const normalized = normalizeCriteria(criteriaForm)
const preferredTitle = criteriaForm.title || normalized.title
await workflowService.saveCriteria({ await workflowService.saveCriteria({
...normalizeCriteria(criteriaForm), ...normalized,
listFormCode: props.listFormCode, listFormCode: props.listFormCode,
title: uniqueCriteriaTitle(
normalized.kind || '',
currentCriteria,
normalized.id,
preferredTitle,
),
}) })
setSelectedId('') setSelectedId('')
}) })
@ -123,9 +132,11 @@ export function FormTabWorkflow(
const addCriteria = (kind: string) => { const addCriteria = (kind: string) => {
setDesignerTab('flow') setDesignerTab('flow')
runAction(async () => { runAction(async () => {
const nextTitle = uniqueCriteriaTitle(kind, currentCriteria)
const saved = await workflowService.saveCriteria({ const saved = await workflowService.saveCriteria({
...normalizeCriteria(emptyCriteria(kind, props.listFormCode)), ...normalizeCriteria(emptyCriteria(kind, props.listFormCode)),
listFormCode: props.listFormCode, listFormCode: props.listFormCode,
title: nextTitle,
positionX: 80 + (currentCriteria.length % 5) * 230, positionX: 80 + (currentCriteria.length % 5) * 230,
positionY: 220 + Math.floor(currentCriteria.length / 5) * 140, positionY: 220 + Math.floor(currentCriteria.length / 5) * 140,
}) })

View file

@ -9,6 +9,8 @@ import {
emptyCriteria, emptyCriteria,
normalizeCriteria, normalizeCriteria,
toCriteriaForm, toCriteriaForm,
uniqueCriteriaId,
uniqueCriteriaTitle,
type WorkflowCriteriaForm, type WorkflowCriteriaForm,
} from '@/utils/workflow/workflowHelpers' } from '@/utils/workflow/workflowHelpers'
import { Field, FieldProps, Form, Formik } from 'formik' import { Field, FieldProps, Form, Formik } from 'formik'
@ -45,8 +47,6 @@ const toDesignerCriteria = (items: ListFormWorkflowCriteriaDto[]): WorkflowCrite
const toWizardCriteria = (items: WorkflowCriteriaDto[]): ListFormWorkflowCriteriaDto[] => const toWizardCriteria = (items: WorkflowCriteriaDto[]): ListFormWorkflowCriteriaDto[] =>
items.map(({ nodeId: _nodeId, ...item }) => item) items.map(({ nodeId: _nodeId, ...item }) => item)
const nextId = () => `WF${Date.now()}${Math.floor(Math.random() * 1000)}`
function WizardStep6({ function WizardStep6({
listFormCode, listFormCode,
workflow, workflow,
@ -96,9 +96,10 @@ function WizardStep6({
const saveCriteria = (event: FormEvent<HTMLFormElement>) => { const saveCriteria = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault() event.preventDefault()
const normalized = normalizeCriteria({ ...criteriaForm, listFormCode }) const normalized = normalizeCriteria({ ...criteriaForm, listFormCode })
const id = normalized.id || nextId() const id = normalized.id || uniqueCriteriaId(currentCriteria)
const nextItem = { ...normalized, id, nodeId: id } as WorkflowCriteriaDto
const exists = currentCriteria.some((item) => item.id === id) 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( updateCriteria(
exists exists
? currentCriteria.map((item) => (item.id === id ? nextItem : item)) ? currentCriteria.map((item) => (item.id === id ? nextItem : item))
@ -109,11 +110,12 @@ function WizardStep6({
} }
const addCriteria = (kind: string) => { const addCriteria = (kind: string) => {
const id = nextId() const id = uniqueCriteriaId(currentCriteria)
const nextItem = { const nextItem = {
...normalizeCriteria(emptyCriteria(kind, listFormCode)), ...normalizeCriteria(emptyCriteria(kind, listFormCode)),
id, id,
nodeId: id, nodeId: id,
title: uniqueCriteriaTitle(kind, currentCriteria),
positionX: 80 + (currentCriteria.length % 5) * 230, positionX: 80 + (currentCriteria.length % 5) * 230,
positionY: 220 + Math.floor(currentCriteria.length / 5) * 140, positionY: 220 + Math.floor(currentCriteria.length / 5) * 140,
} as WorkflowCriteriaDto } as WorkflowCriteriaDto
@ -193,14 +195,15 @@ function WizardStep6({
} }
const resetDemo = () => { const resetDemo = () => {
const startId = nextId() const startId = uniqueCriteriaId([])
const approvalId = nextId() const approvalId = uniqueCriteriaId([], [startId])
const endId = nextId() const endId = uniqueCriteriaId([], [startId, approvalId])
updateCriteria([ updateCriteria([
{ {
...normalizeCriteria(emptyCriteria('Start', listFormCode)), ...normalizeCriteria(emptyCriteria('Start', listFormCode)),
id: startId, id: startId,
nodeId: startId, nodeId: startId,
title: uniqueCriteriaTitle('Start', []),
nextOnStart: approvalId, nextOnStart: approvalId,
positionX: 72, positionX: 72,
positionY: 160, positionY: 160,
@ -209,6 +212,7 @@ function WizardStep6({
...normalizeCriteria(emptyCriteria('Approval', listFormCode)), ...normalizeCriteria(emptyCriteria('Approval', listFormCode)),
id: approvalId, id: approvalId,
nodeId: approvalId, nodeId: approvalId,
title: uniqueCriteriaTitle('Approval', []),
nextOnApprove: endId, nextOnApprove: endId,
positionX: 360, positionX: 360,
positionY: 160, positionY: 160,
@ -217,6 +221,7 @@ function WizardStep6({
...normalizeCriteria(emptyCriteria('End', listFormCode)), ...normalizeCriteria(emptyCriteria('End', listFormCode)),
id: endId, id: endId,
nodeId: endId, nodeId: endId,
title: uniqueCriteriaTitle('End', []),
positionX: 650, positionX: 650,
positionY: 160, positionY: 160,
} as WorkflowCriteriaDto, } as WorkflowCriteriaDto,

View file

@ -73,6 +73,11 @@ export const NoteList: React.FC<NoteListProps> = ({
icon: <FaEnvelope className="text-green-500" />, icon: <FaEnvelope className="text-green-500" />,
border: 'border-green-400', border: 'border-green-400',
} }
case 'workflow':
return {
icon: <FaHistory className="text-purple-500" />,
border: 'border-purple-400',
}
default: default:
return { return {
icon: <FaStickyNote className="text-gray-400" />, icon: <FaStickyNote className="text-gray-400" />,
@ -435,7 +440,7 @@ export const NoteList: React.FC<NoteListProps> = ({
</div> </div>
{/* Sil butonu */} {/* Sil butonu */}
{user?.id === note.creatorId && ( {user?.id === note.creatorId && note.type !== 'workflow' && (
<Button <Button
variant="plain" variant="plain"
size="sm" size="sm"

View file

@ -49,7 +49,6 @@ function NoteModalContent({
const types = [ const types = [
{ value: 'note', label: translate('::ListForms.ListForm.NoteModal.Type.Note') }, { value: 'note', label: translate('::ListForms.ListForm.NoteModal.Type.Note') },
{ value: 'message', label: translate('::ListForms.ListForm.NoteModal.Type.Message') }, { value: 'message', label: translate('::ListForms.ListForm.NoteModal.Type.Message') },
{ value: 'activity', label: translate('::ListForms.ListForm.NoteModal.Type.Activity') },
] ]
const handleSave = async (values: any) => { const handleSave = async (values: any) => {

View file

@ -77,6 +77,7 @@ import { useListFormColumns } from './useListFormColumns'
import { Loading } from '@/components/shared' import { Loading } from '@/components/shared'
import { useStoreState } from '@/store' import { useStoreState } from '@/store'
import { workflowService } from '@/services/workflow.service' import { workflowService } from '@/services/workflow.service'
import { NotePanel } from '../form/notes/NotePanel'
interface GridProps { interface GridProps {
listFormCode: string listFormCode: string
@ -262,6 +263,10 @@ const Grid = (props: GridProps) => {
const [gridDto, setGridDto] = useState<GridDto>() const [gridDto, setGridDto] = useState<GridDto>()
const [isPopupFullScreen, setIsPopupFullScreen] = useState(false) const [isPopupFullScreen, setIsPopupFullScreen] = useState(false)
const [widgetGroupHeight, setWidgetGroupHeight] = useState(0) const [widgetGroupHeight, setWidgetGroupHeight] = useState(0)
const [notePanelTarget, setNotePanelTarget] = useState<{
entityName: string
entityId: string
} | null>(null)
type EditorOptionsWithButtons = { type EditorOptionsWithButtons = {
buttons?: any[] buttons?: any[]
@ -338,11 +343,29 @@ const Grid = (props: GridProps) => {
}) })
const { createSelectDataSource } = useListFormCustomDataSource({ gridRef }) 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({ const { getBandedColumns } = useListFormColumns({
gridDto, gridDto,
listFormCode, listFormCode,
isSubForm, isSubForm,
gridRef, gridRef,
onShowNote: openNotePanel,
}) })
const getSelectedRowKeys = useCallback(async () => { const getSelectedRowKeys = useCallback(async () => {
@ -1874,6 +1897,14 @@ const Grid = (props: GridProps) => {
{toolbarModalData?.content} {toolbarModalData?.content}
</Dialog> </Dialog>
<GridFilterDialogs gridRef={gridRef} listFormCode={listFormCode} {...filterData} /> <GridFilterDialogs gridRef={gridRef} listFormCode={listFormCode} {...filterData} />
{notePanelTarget && (
<NotePanel
entityName={notePanelTarget.entityName}
entityId={notePanelTarget.entityId}
isVisible
onToggle={() => setNotePanelTarget(null)}
/>
)}
</Container> </Container>
</> </>
) )

View file

@ -68,6 +68,7 @@ import { useStoreState } from '@/store/store'
import SubForms from '../form/SubForms' import SubForms from '../form/SubForms'
import { ImportDashboard } from '@/components/importManager/ImportDashboard' import { ImportDashboard } from '@/components/importManager/ImportDashboard'
import { workflowService } from '@/services/workflow.service' import { workflowService } from '@/services/workflow.service'
import { NotePanel } from '../form/notes/NotePanel'
interface TreeProps { interface TreeProps {
listFormCode: string listFormCode: string
@ -250,6 +251,10 @@ const Tree = (props: TreeProps) => {
const [gridDto, setGridDto] = useState<GridDto>() const [gridDto, setGridDto] = useState<GridDto>()
const [isPopupFullScreen, setIsPopupFullScreen] = useState(false) const [isPopupFullScreen, setIsPopupFullScreen] = useState(false)
const [widgetGroupHeight, setWidgetGroupHeight] = useState(0) const [widgetGroupHeight, setWidgetGroupHeight] = useState(0)
const [notePanelTarget, setNotePanelTarget] = useState<{
entityName: string
entityId: string
} | null>(null)
const [expandedRowKeys, setExpandedRowKeys] = useState<any[]>([]) const [expandedRowKeys, setExpandedRowKeys] = useState<any[]>([])
@ -343,11 +348,29 @@ const Tree = (props: TreeProps) => {
}) })
const { createSelectDataSource } = useListFormCustomDataSource({ gridRef }) 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({ const { getBandedColumns } = useListFormColumns({
gridDto, gridDto,
listFormCode, listFormCode,
isSubForm, isSubForm,
gridRef, gridRef,
onShowNote: openNotePanel,
}) })
const extractSearchParamsFields = useCallback((filter: any): [string, string, any][] => { const extractSearchParamsFields = useCallback((filter: any): [string, string, any][] => {
@ -1613,6 +1636,14 @@ const Tree = (props: TreeProps) => {
{toolbarModalData?.content} {toolbarModalData?.content}
</Dialog> </Dialog>
<GridFilterDialogs gridRef={gridRef as any} listFormCode={listFormCode} {...filterData} /> <GridFilterDialogs gridRef={gridRef as any} listFormCode={listFormCode} {...filterData} />
{notePanelTarget && (
<NotePanel
entityName={notePanelTarget.entityName}
entityId={notePanelTarget.entityId}
isVisible
onToggle={() => setNotePanelTarget(null)}
/>
)}
</Container> </Container>
</> </>
) )

View file

@ -247,11 +247,13 @@ const useListFormColumns = ({
listFormCode, listFormCode,
isSubForm, isSubForm,
gridRef, gridRef,
onShowNote,
}: { }: {
gridDto?: GridDto gridDto?: GridDto
listFormCode: string listFormCode: string
isSubForm?: boolean isSubForm?: boolean
gridRef?: any gridRef?: any
onShowNote?: (rowData: Record<string, any>) => void
}) => { }) => {
const dialog: any = useDialogContext() const dialog: any = useDialogContext()
const { translate } = useLocalization() const { translate } = useLocalization()
@ -435,6 +437,11 @@ const useListFormColumns = ({
gridDto.gridOptions.editingOptionDto.allowDetail && gridDto.gridOptions.editingOptionDto.allowDetail &&
checkPermission(gridDto.gridOptions.permissionDto.u) checkPermission(gridDto.gridOptions.permissionDto.u)
const hasShowNote =
gridDto.gridOptions.showNote &&
checkPermission(gridDto.gridOptions.permissionDto.n) &&
typeof onShowNote === 'function'
const hasDuplicate = const hasDuplicate =
gridDto.gridOptions.editingOptionDto.allowDuplicate && gridDto.gridOptions.editingOptionDto.allowDuplicate &&
checkPermission(gridDto.gridOptions.permissionDto.i) checkPermission(gridDto.gridOptions.permissionDto.i)
@ -442,7 +449,15 @@ const useListFormColumns = ({
const hasCommandButtons = gridDto.gridOptions.commandColumnDto.length > 0 const hasCommandButtons = gridDto.gridOptions.commandColumnDto.length > 0
// Eğer hiçbir buton eklenecek durumda değilse: çık // Eğer hiçbir buton eklenecek durumda değilse: çık
if (!hasUpdate && !hasDelete && !hasCreate && !hasCommandButtons) { if (
!hasUpdate &&
!hasDelete &&
!hasCreate &&
!hasDetail &&
!hasShowNote &&
!hasDuplicate &&
!hasCommandButtons
) {
return return
} }
@ -485,6 +500,20 @@ const useListFormColumns = ({
buttons.push(item) 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) { if (hasDuplicate) {
const item = { const item = {
name: 'duplicate', name: 'duplicate',
@ -600,7 +629,7 @@ const useListFormColumns = ({
} }
return column as GridColumnData return column as GridColumnData
}, [gridDto, checkPermission, translate, listFormCode, isPwaMode, dialog, gridRef]) }, [gridDto, checkPermission, translate, listFormCode, isPwaMode, dialog, gridRef, onShowNote])
const getColumns = useCallback( const getColumns = useCallback(
(columnFormats: ColumnFormatDto[]) => { (columnFormats: ColumnFormatDto[]) => {