Compare commits

...

14 commits
1.1.04 ... main

Author SHA1 Message Date
Sedat Öztürk
233c9b7502 Kullanıcı Detayları ve Avatar 2026-06-07 23:45:39 +03:00
Sedat Öztürk
12f046f262 Workflow ve WizardSeeder düzenlemeleri yapıldı. 2026-06-07 22:42:02 +03:00
Sedat Öztürk
d0cccde53f Workflow için IsFilterUserName özelliği eklendi. 2026-06-07 19:26:19 +03:00
Sedat Öztürk
1d15c44a3d Seçilenleri ve Tümünü Sil butonları 2026-06-07 14:07:48 +03:00
Sedat Öztürk
bade0bab98 Workflow düzeltmeleri 2026-06-07 10:52:28 +03:00
Sedat Öztürk
c204eef755 Workflow Listlerinin Note Özelliği eklendi 2026-06-07 01:22:35 +03:00
Sedat Öztürk
27e65f05f0 Wizard kısmında sütunların Popupformda görünüp görünmeyeceğini belirlenmesi 2026-06-06 22:52:56 +03:00
Sedat Öztürk
64084679e8 Workflow problemleri 2026-06-06 21:31:03 +03:00
Sedat Öztürk
2f1b9d4e77 Wizard problemleri giderildi. 2026-06-06 18:22:14 +03:00
Sedat Öztürk
1c472a7d9a AuditLog
SubForms kısmında ListFormCode dahil edildi
2026-06-05 22:05:57 +03:00
Sedat ÖZTÜRK
119c3650f0 Netdata dashboard kurulumu ve sunucu performansı 2026-06-05 17:22:02 +03:00
Sedat ÖZTÜRK
ebab6ea114 Wizard üzerindeki problemler giderildi. 2026-06-05 12:30:40 +03:00
Sedat ÖZTÜRK
975bc8dd6c intranet kaydırılma işlemi 2026-06-05 08:44:01 +03:00
Sedat Öztürk
97a2a4b38d Wizard üzerinden Entity eklenebiliyor 2026-06-04 23:25:19 +03:00
63 changed files with 2989 additions and 426 deletions

View file

@ -0,0 +1,9 @@
using Volo.Abp.Application.Dtos;
namespace Sozsoft.Platform.AuditLogs;
public class AuditLogListRequestDto : PagedAndSortedResultRequestDto
{
public string ListFormCode { get; set; }
public string EntityId { get; set; }
}

View file

@ -0,0 +1,11 @@
using System;
using Volo.Abp.Content;
namespace Sozsoft.Platform.Identity.Dto;
public class UserAvatarUpdateInput
{
public Guid UserId { get; set; }
public IRemoteStreamContent Avatar { get; set; }
}

View file

@ -5,6 +5,7 @@ namespace Sozsoft.Platform.ListForms;
public class WorkflowDto
{
public string ApprovalUserFieldName { get; set; }
public bool IsFilterUserName { get; set; }
public string ApprovalDateFieldName { get; set; }
public string ApprovalStatusFieldName { get; set; }
public string ApprovalDescriptionFieldName { get; set; }

View file

@ -39,6 +39,7 @@ public class ListFormWizardDto
public string PermissionGroupName { get; set; }
public string MenuParentCode { get; set; }
public string MenuParentIcon { get; set; }
public string MenuIcon { get; set; }
public string DataSourceCode { get; set; }
public string DataSourceConnectionString { get; set; }

View file

@ -12,6 +12,7 @@ public class WizardColumnItemInputDto
public string EditorScript { get; set; }
public int ColSpan { get; set; } = 1;
public bool IsRequired { get; set; }
public bool IncludeInEditingForm { get; set; } = true;
public DbType DbSourceType { get; set; } = DbType.String;
public string TurkishCaption { get; set; }
public string EnglishCaption { get; set; }

View file

@ -1,3 +1,5 @@
using System.Collections.Generic;
namespace Sozsoft.Platform.ListForms;
public class WorkflowRunResultDto
@ -8,5 +10,6 @@ public class WorkflowRunResultDto
public string CurrentNodeKind { get; set; }
public bool WaitingApproval { get; set; }
public bool Completed { get; set; }
public List<string> ToastMessages { get; set; } = [];
}

View file

@ -1,30 +1,43 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Sozsoft.Platform.Entities;
using Sozsoft.Platform.ListForms;
using Volo.Abp;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
using Volo.Abp.AuditLogging;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.Uow;
using static Sozsoft.Platform.Data.Seeds.SeedConsts;
namespace Sozsoft.Platform.AuditLogs;
public interface IAuditLogAppService
: ICrudAppService<AuditLogDto, Guid>
: ICrudAppService<AuditLogDto, Guid, AuditLogListRequestDto>
{
}
[Authorize(AppCodes.IdentityManagement.AuditLogs)]
public class AuditLogAppService
: CrudAppService<AuditLog, AuditLogDto, Guid>
, IAuditLogAppService
public class AuditLogAppService : CrudAppService<
AuditLog,
AuditLogDto,
Guid,
AuditLogListRequestDto>, IAuditLogAppService
{
public AuditLogAppService(IAuditLogRepository auditLogRepository) : base(auditLogRepository)
private readonly IRepository<ListForm, Guid> _listFormRepository;
public AuditLogAppService(
IAuditLogRepository auditLogRepository,
IRepository<ListForm, Guid> listFormRepository
) : base(auditLogRepository)
{
_listFormRepository = listFormRepository;
}
public override async Task<AuditLogDto> GetAsync(Guid id)
@ -35,27 +48,30 @@ public class AuditLogAppService
}
[UnitOfWork]
public override async Task<PagedResultDto<AuditLogDto>> GetListAsync(PagedAndSortedResultRequestDto input)
public override async Task<PagedResultDto<AuditLogDto>> GetListAsync(AuditLogListRequestDto input)
{
var query = await CreateFilteredQueryAsync(input);
var query = await Repository.WithDetailsAsync();
if (!input.ListFormCode.IsNullOrWhiteSpace())
{
var filterRules = await GetListFormFilterRulesAsync(input.ListFormCode);
query = ApplyAuditLogActionParametersFilter(query, filterRules, input.EntityId);
}
else if (!input.EntityId.IsNullOrWhiteSpace())
{
query = query.Where(a => a.Actions.Any(action => action.Parameters.Contains(input.EntityId)));
}
var totalCount = await AsyncExecuter.CountAsync(query);
query = ApplySorting(query, input);
query = ApplyPaging(query, input);
// EntityChanges ile birlikte getir (N+1 query önlenir)
var auditLogRepository = (IAuditLogRepository)Repository;
var auditLogsWithDetails = await auditLogRepository.GetListAsync(
sorting: input.Sorting,
maxResultCount: input.MaxResultCount,
skipCount: input.SkipCount,
includeDetails: true
);
var auditLogsWithDetails = await AsyncExecuter.ToListAsync(query);
// Mapping tek seferde yap
var entityDtos = ObjectMapper.Map<List<AuditLog>, List<AuditLogDto>>(auditLogsWithDetails);
// EntityChangeCount'u doldur (artık EntityChanges yüklü)
foreach (var dto in entityDtos)
{
@ -69,6 +85,102 @@ public class AuditLogAppService
);
}
private async Task<List<AuditLogListFormFilterRule>> GetListFormFilterRulesAsync(string listFormCode)
{
var rules = new List<AuditLogListFormFilterRule>
{
new(listFormCode, null)
};
var listForm = await _listFormRepository.FirstOrDefaultAsync(a => a.ListFormCode == listFormCode);
if (listForm?.SubFormsJson.IsNullOrWhiteSpace() != false)
{
return rules;
}
try
{
var subForms = JsonSerializer.Deserialize<List<SubFormDto>>(listForm.SubFormsJson) ?? [];
foreach (var subForm in subForms.Where(a => !a.Code.IsNullOrWhiteSpace()))
{
var childFieldNames = subForm.Relation?
.Select(a => a.ChildFieldName)
.Where(a => !a.IsNullOrWhiteSpace())
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList() ?? [];
rules.AddRange(childFieldNames.Select(childFieldName => new AuditLogListFormFilterRule(subForm.Code, childFieldName)));
}
}
catch (JsonException)
{
// Invalid subform JSON should not block audit log listing for the main form.
}
return rules
.DistinctBy(a => $"{a.ListFormCode}|{a.ChildFieldName}".ToLowerInvariant())
.ToList();
}
private static IQueryable<AuditLog> ApplyAuditLogActionParametersFilter(
IQueryable<AuditLog> query,
List<AuditLogListFormFilterRule> rules,
string entityId)
{
var validRules = rules
.Where(rule => !rule.ListFormCode.IsNullOrWhiteSpace())
.ToList();
if (validRules.Count == 0)
{
return query;
}
var auditLog = Expression.Parameter(typeof(AuditLog), "auditLog");
var action = Expression.Parameter(typeof(AuditLogAction), "action");
var parameters = Expression.Property(action, nameof(AuditLogAction.Parameters));
Expression actionBody = Expression.Constant(false);
foreach (var rule in validRules)
{
Expression ruleBody = Contains(parameters, rule.ListFormCode);
if (!entityId.IsNullOrWhiteSpace())
{
ruleBody = Expression.AndAlso(ruleBody, Contains(parameters, entityId));
}
if (!rule.ChildFieldName.IsNullOrWhiteSpace())
{
ruleBody = Expression.AndAlso(ruleBody, Contains(parameters, rule.ChildFieldName));
}
actionBody = Expression.OrElse(actionBody, ruleBody);
}
var actions = Expression.Property(auditLog, nameof(AuditLog.Actions));
var actionPredicate = Expression.Lambda<Func<AuditLogAction, bool>>(actionBody, action);
var anyCall = Expression.Call(
typeof(Enumerable),
nameof(Enumerable.Any),
[typeof(AuditLogAction)],
actions,
actionPredicate);
var auditLogPredicate = Expression.Lambda<Func<AuditLog, bool>>(anyCall, auditLog);
return query.Where(auditLogPredicate);
}
private static MethodCallExpression Contains(MemberExpression source, string value)
{
return Expression.Call(
source,
nameof(string.Contains),
Type.EmptyTypes,
Expression.Constant(value));
}
private sealed record AuditLogListFormFilterRule(string ListFormCode, string? ChildFieldName);
// Audit Log kayitlarini gormek istiyoruz fakat degistirmek istemiyoruz
[RemoteService(IsEnabled = false)]
public override Task<AuditLogDto> CreateAsync(AuditLogDto input)

View file

@ -3,10 +3,12 @@ using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using Sozsoft.Platform.BlobStoring;
using Sozsoft.Platform.Entities;
using Sozsoft.Platform.Extensions;
using Sozsoft.Platform.Identity.Dto;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using OpenIddict.Abstractions;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Repositories;
@ -32,6 +34,7 @@ public class PlatformIdentityAppService : ApplicationService
public IRepository<WorkHour, Guid> workHourRepository { get; }
public IRepository<Department, Guid> departmentRepository { get; }
public IRepository<JobPosition, Guid> jobPositionRepository { get; }
public BlobManager BlobCdnManager { get; }
public PlatformIdentityAppService(
IIdentityUserAppService identityUserAppService,
@ -45,6 +48,7 @@ public class PlatformIdentityAppService : ApplicationService
IRepository<WorkHour, Guid> workHourRepository,
IRepository<Department, Guid> departmentRepository,
IRepository<JobPosition, Guid> jobPositionRepository,
BlobManager blobCdnManager,
IGuidGenerator guidGenerator
)
{
@ -55,6 +59,7 @@ public class PlatformIdentityAppService : ApplicationService
this.workHourRepository = workHourRepository;
this.departmentRepository = departmentRepository;
this.jobPositionRepository = jobPositionRepository;
this.BlobCdnManager = blobCdnManager;
this.permissionRepository = permissionRepository;
this.branchRepository = branchRepository;
this.branchUsersRepository = branchUsersRepository;
@ -273,6 +278,22 @@ public class PlatformIdentityAppService : ApplicationService
await UserManager.UpdateAsync(user);
}
[Authorize(IdentityPermissions.Users.Update)]
public async Task UpdateAvatarAsync([FromForm] UserAvatarUpdateInput input)
{
var user = await UserManager.GetByIdAsync(input.UserId);
var fileName = $"{user.Id}.jpg";
if (input.Avatar is null || input.Avatar.ContentLength == 0)
{
await BlobCdnManager.DeleteAsync(BlobContainerNames.Avatar, fileName);
}
else
{
await BlobCdnManager.SaveAsync(BlobContainerNames.Avatar, fileName, input.Avatar.GetStream());
}
}
public async Task<List<PermissionDefinitionRecord>> GetPermissionList()
{
var list = await permissionRepository.GetListAsync();

View file

@ -12,8 +12,6 @@ using Volo.Abp.Domain.Repositories;
using Volo.Abp.MultiTenancy;
using Volo.Abp.PermissionManagement;
using Volo.Abp.Uow;
using static Sozsoft.Platform.PlatformConsts;
using System.Data;
using Microsoft.Extensions.Hosting;
using Sozsoft.Languages;
using Sozsoft.Platform.DynamicData;
@ -60,10 +58,21 @@ public class ListFormWizardAppService(
public async Task Create(ListFormWizardDto input)
{
var wizardName = input.WizardName.Trim();
var titleLangKey = WizardConsts.WizardKeyTitle(wizardName);
var nameLangKey = WizardConsts.WizardKey(wizardName);
var descLangKey = WizardConsts.WizardKeyDesc(wizardName);
var code = WizardConsts.WizardKey(wizardName);
var code = string.IsNullOrWhiteSpace(input.MenuCode)
? WizardConsts.WizardKey(wizardName)
: input.MenuCode.Trim();
var listFormCode = string.IsNullOrWhiteSpace(input.ListFormCode)
? code
: input.ListFormCode.Trim();
var titleLangKey = $"{listFormCode}.Title";
var nameLangKey = code;
var descLangKey = $"{listFormCode}.Desc";
var permCreateName = $"{code}.Create";
var permUpdateName = $"{code}.Update";
var permDeleteName = $"{code}.Delete";
var permExportName = $"{code}.Export";
var permImportName = $"{code}.Import";
var permNoteName = $"{code}.Note";
// Eklenen kayıtları takip et (silme işleminde kullanılır)
var inserted = new WizardInsertedRecordsDto();
@ -78,16 +87,15 @@ public class ListFormWizardAppService(
if (!await repoPermGroup.AnyAsync(a => a.Name == groupName))
{
await repoPermGroup.InsertAsync(new PermissionGroupDefinitionRecord(GuidGenerator.Create(), groupName, groupName), autoSave: false);
await CreateLangKey(groupName, groupName, groupName, inserted);
if (string.Equals(groupName, input.MenuParentCode, StringComparison.OrdinalIgnoreCase))
await EnsureLangKey(groupName, inserted);
else
await CreateLangKey(groupName, groupName, groupName, inserted);
inserted.PermissionGroupNames.Add(groupName);
}
// Permission'ları tek seferde kontrol et ve oluştur
var queryable = await repoPerm.GetQueryableAsync();
var existingPerms = await AsyncExecuter.ToListAsync(
queryable.Where(a => a.GroupName == groupName)
);
var existingPerms = await repoPerm.GetListAsync(a => a.GroupName == groupName);
var permRead = existingPerms.FirstOrDefault(a => a.Name == code);
if (permRead == null)
{
@ -95,45 +103,45 @@ public class ListFormWizardAppService(
inserted.PermissionNames.Add(permRead.Name);
}
var permCreate = existingPerms.FirstOrDefault(a => a.Name == WizardConsts.PermCreate(wizardName));
var permCreate = existingPerms.FirstOrDefault(a => a.Name == permCreateName);
if (permCreate == null)
{
permCreate = await repoPerm.InsertAsync(new PermissionDefinitionRecord(Guid.NewGuid(), groupName, WizardConsts.PermCreate(wizardName), permRead.Name, WizardConsts.LangKeyCreate, true, MultiTenancySides.Both), autoSave: false);
permCreate = await repoPerm.InsertAsync(new PermissionDefinitionRecord(Guid.NewGuid(), groupName, permCreateName, permRead.Name, WizardConsts.LangKeyCreate, true, MultiTenancySides.Both), autoSave: false);
inserted.PermissionNames.Add(permCreate.Name);
}
var permUpdate = existingPerms.FirstOrDefault(a => a.Name == WizardConsts.PermUpdate(wizardName));
var permUpdate = existingPerms.FirstOrDefault(a => a.Name == permUpdateName);
if (permUpdate == null)
{
permUpdate = await repoPerm.InsertAsync(new PermissionDefinitionRecord(Guid.NewGuid(), groupName, WizardConsts.PermUpdate(wizardName), permRead.Name, WizardConsts.LangKeyUpdate, true, MultiTenancySides.Both), autoSave: false);
permUpdate = await repoPerm.InsertAsync(new PermissionDefinitionRecord(Guid.NewGuid(), groupName, permUpdateName, permRead.Name, WizardConsts.LangKeyUpdate, true, MultiTenancySides.Both), autoSave: false);
inserted.PermissionNames.Add(permUpdate.Name);
}
var permDelete = existingPerms.FirstOrDefault(a => a.Name == WizardConsts.PermDelete(wizardName));
var permDelete = existingPerms.FirstOrDefault(a => a.Name == permDeleteName);
if (permDelete == null)
{
permDelete = await repoPerm.InsertAsync(new PermissionDefinitionRecord(Guid.NewGuid(), groupName, WizardConsts.PermDelete(wizardName), permRead.Name, WizardConsts.LangKeyDelete, true, MultiTenancySides.Both), autoSave: false);
permDelete = await repoPerm.InsertAsync(new PermissionDefinitionRecord(Guid.NewGuid(), groupName, permDeleteName, permRead.Name, WizardConsts.LangKeyDelete, true, MultiTenancySides.Both), autoSave: false);
inserted.PermissionNames.Add(permDelete.Name);
}
var permExport = existingPerms.FirstOrDefault(a => a.Name == WizardConsts.PermExport(wizardName));
var permExport = existingPerms.FirstOrDefault(a => a.Name == permExportName);
if (permExport == null)
{
permExport = await repoPerm.InsertAsync(new PermissionDefinitionRecord(Guid.NewGuid(), groupName, WizardConsts.PermExport(wizardName), permRead.Name, WizardConsts.LangKeyExport, true, MultiTenancySides.Both), autoSave: false);
permExport = await repoPerm.InsertAsync(new PermissionDefinitionRecord(Guid.NewGuid(), groupName, permExportName, permRead.Name, WizardConsts.LangKeyExport, true, MultiTenancySides.Both), autoSave: false);
inserted.PermissionNames.Add(permExport.Name);
}
var permImport = existingPerms.FirstOrDefault(a => a.Name == WizardConsts.PermImport(wizardName));
var permImport = existingPerms.FirstOrDefault(a => a.Name == permImportName);
if (permImport == null)
{
permImport = await repoPerm.InsertAsync(new PermissionDefinitionRecord(Guid.NewGuid(), groupName, WizardConsts.PermImport(wizardName), permRead.Name, WizardConsts.LangKeyImport, true, MultiTenancySides.Both), autoSave: false);
permImport = await repoPerm.InsertAsync(new PermissionDefinitionRecord(Guid.NewGuid(), groupName, permImportName, permRead.Name, WizardConsts.LangKeyImport, true, MultiTenancySides.Both), autoSave: false);
inserted.PermissionNames.Add(permImport.Name);
}
var permNote = existingPerms.FirstOrDefault(a => a.Name == WizardConsts.PermNote(wizardName));
var permNote = existingPerms.FirstOrDefault(a => a.Name == permNoteName);
if (permNote == null)
{
permNote = await repoPerm.InsertAsync(new PermissionDefinitionRecord(Guid.NewGuid(), groupName, WizardConsts.PermNote(wizardName), permRead.Name, WizardConsts.LangKeyNote, true, MultiTenancySides.Both), autoSave: false);
permNote = await repoPerm.InsertAsync(new PermissionDefinitionRecord(Guid.NewGuid(), groupName, permNoteName, permRead.Name, WizardConsts.LangKeyNote, true, MultiTenancySides.Both), autoSave: false);
inserted.PermissionNames.Add(permNote.Name);
}
@ -161,12 +169,18 @@ public class ListFormWizardAppService(
if (menuParent == null)
{
var maxRootOrder = menuQueryable.Where(a => a.ParentCode == null || a.ParentCode == "").Select(a => (int?)a.Order).Max() ?? 0;
await CreateLangKey(WizardConsts.WizardKeyParent(wizardName), input.LanguageTextMenuParentEn, input.LanguageTextMenuParentTr, inserted);
var menuParentIcon = !string.IsNullOrWhiteSpace(input.MenuParentIcon)
? input.MenuParentIcon
: !string.IsNullOrWhiteSpace(input.MenuIcon)
? input.MenuIcon
: WizardConsts.MenuIcon;
await CreateLangKey(input.MenuParentCode, input.LanguageTextMenuParentEn, input.LanguageTextMenuParentTr, inserted);
menuParent = await repoMenu.InsertAsync(new Menu
{
Code = input.MenuParentCode,
DisplayName = WizardConsts.WizardKeyParent(wizardName),
DisplayName = input.MenuParentCode,
IsDisabled = false,
Icon = menuParentIcon,
Order = maxRootOrder + 1,
}, autoSave: false);
inserted.MenuCodes.Add(input.MenuParentCode);
@ -218,7 +232,7 @@ public class ListFormWizardAppService(
ColSpan = g.ColCount,
ItemType = "group",
Items = g.Items
.Where(i => i.DataField != input.KeyFieldName)
.Where(i => i.IncludeInEditingForm && i.DataField != input.KeyFieldName)
.Select((it, ii) => new EditingFormItemDto
{
Order = ii + 1,
@ -231,6 +245,7 @@ public class ListFormWizardAppService(
})
.ToArray()
})
.Where(g => g.Items.Length > 0)
.ToList();
//ListForm - varsa sil, yeniden ekle
@ -257,14 +272,15 @@ 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
await repoListForm.InsertAsync(new ListForm
{
ListFormType = ListFormTypeEnum.List,
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,
@ -280,12 +296,12 @@ public class ListFormWizardAppService(
KeyFieldName = input.KeyFieldName,
KeyFieldDbSourceType = input.KeyFieldDbSourceType,
DefaultFilter = isDeleted ? WizardConsts.DefaultFilterJson : null,
SortMode = GridOptions.SortModeSingle,
SortMode = PlatformConsts.GridOptions.SortModeSingle,
FilterRowJson = WizardConsts.DefaultFilterRowJson,
HeaderFilterJson = WizardConsts.DefaultHeaderFilterJson,
SearchPanelJson = WizardConsts.DefaultSearchPanelJson,
GroupPanelJson = JsonSerializer.Serialize(new { Visible = false }),
SelectionJson = WizardConsts.DefaultSelectionSingleJson,
SelectionJson = WizardConsts.DefaultSelectionSingleJson(input.WorkflowCriteria.Count > 0 ? PlatformConsts.GridOptions.SelectionModeSingle : PlatformConsts.GridOptions.SelectionModeNone),
ColumnOptionJson = WizardConsts.DefaultColumnOptionJson(),
PermissionJson = WizardConsts.DefaultPermissionJson(code),
DeleteCommand = isDeleted ? WizardConsts.DefaultDeleteCommand(input.SelectCommand) : null,
@ -395,13 +411,65 @@ public class ListFormWizardAppService(
{
return workflow != null && (
!string.IsNullOrWhiteSpace(workflow.ApprovalUserFieldName) ||
!string.IsNullOrWhiteSpace(workflow.ApprovalDateFieldName) ||
!string.IsNullOrWhiteSpace(workflow.ApprovalStatusFieldName) ||
!string.IsNullOrWhiteSpace(workflow.ApprovalDescriptionFieldName) ||
criteria.Count > 0
);
}
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.
@ -677,4 +745,16 @@ public class ListFormWizardAppService(
return existing;
}
private async Task<LanguageKey> EnsureLangKey(string key, WizardInsertedRecordsDto inserted = null)
{
var res = PlatformConsts.AppName;
var existing = await repoLangKey.FirstOrDefaultAsync(a => a.ResourceName == res && a.Key == key);
if (existing != null) return existing;
existing = await repoLangKey.InsertAsync(new LanguageKey { ResourceName = res, Key = key }, autoSave: true);
inserted?.LanguageKeys.Add(key);
return existing;
}
}

View file

@ -7,10 +7,12 @@ using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Localization;
using Sozsoft.Platform.Data.Seeds;
using Sozsoft.Platform.Entities;
using Sozsoft.Platform.Enums;
using Sozsoft.Platform.ListForms.Select;
using Sozsoft.Platform.Localization;
using Sozsoft.Platform.Queries;
using Sozsoft.Sender.Mail;
using Volo.Abp;
@ -29,6 +31,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;
@ -36,18 +39,22 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
private readonly IdentityUserManager identityUserManager;
private readonly ISozsoftEmailSender erpEmailSender;
private readonly ISettingProvider settingProvider;
private readonly IStringLocalizer<PlatformResource> localizer;
public ListFormWorkflowAppService(
IRepository<ListFormWorkflow, string> criteriaRepository,
IRepository<Note, Guid> noteRepository,
IListFormManager listFormManager,
IListFormAuthorizationManager authManager,
IListFormSelectAppService listFormSelectAppService,
IQueryManager queryManager,
IdentityUserManager identityUserManager,
ISozsoftEmailSender erpEmailSender,
ISettingProvider settingProvider)
ISettingProvider settingProvider,
IStringLocalizer<PlatformResource> localizer)
{
this.criteriaRepository = criteriaRepository;
this.noteRepository = noteRepository;
this.listFormManager = listFormManager;
this.authManager = authManager;
this.listFormSelectAppService = listFormSelectAppService;
@ -55,6 +62,7 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
this.identityUserManager = identityUserManager;
this.erpEmailSender = erpEmailSender;
this.settingProvider = settingProvider;
this.localizer = localizer;
}
[HttpGet("criteria")]
@ -188,7 +196,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;
@ -300,7 +308,14 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
var start = context.Criteria.FirstOrDefault(x => x.Kind == "Start")
?? throw new UserFriendlyException("Workflow başlangıç adımı bulunamadı.");
return await RunUntilWaitAsync(context, start);
context.WorkflowNoteRows.Add(("Started By: ", ResolveCurrentUserDisplayName()));
var result = await RunUntilWaitAsync(context, start);
await InsertWorkflowNoteAsync(
context,
$"Workflow Started: {start.Title}",
BuildWorkflowNoteContent(context.WorkflowNoteRows));
return result;
}
[HttpPost("decision")]
@ -365,7 +380,15 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
}
var next = FindNextCriteria(context.Criteria, input.Approved ? current.NextOnApprove : current.NextOnReject);
return await RunUntilWaitAsync(context, next);
context.WorkflowNoteRows.Add(("Decision By: ", ResolveCurrentUserDisplayName()));
AddWorkflowDecisionRows(context, current, input.Approved, input.Note ?? string.Empty);
var result = await RunUntilWaitAsync(context, next);
await InsertWorkflowNoteAsync(
context,
$"Workflow {(input.Approved ? "Approved" : "Rejected")}: {current.Title}",
BuildWorkflowNoteContent(context.WorkflowNoteRows));
return result;
}
private async Task<WorkflowRunResultDto> RunForEachKeyAsync(
@ -387,7 +410,10 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
CurrentNodeTitle = last?.CurrentNodeTitle,
CurrentNodeKind = last?.CurrentNodeKind,
WaitingApproval = results.Any(x => x.WaitingApproval),
Completed = results.All(x => x.Completed)
Completed = results.All(x => x.Completed),
ToastMessages = results
.SelectMany(result => result.ToastMessages ?? [])
.ToList()
};
}
@ -419,7 +445,7 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
.ToList();
var row = await GetRowAsync(code, listForm.KeyFieldName, keys[0]);
return new WorkflowRunContext(code, listForm.KeyFieldName, keys, workflow, criteria, row);
return new WorkflowRunContext(code, keys, workflow, criteria, row);
}
private async Task<IDictionary<string, object>> GetRowAsync(string listFormCode, string keyFieldName, object key)
@ -511,6 +537,8 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
await UpdateRowAsync(context, update);
MergeRowValues(context.Row, update);
AddWorkflowNodeRows(context, node);
AddWorkflowToastMessage(context, node);
if (node.Kind == "Inform")
{
@ -533,7 +561,7 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
recipientEmail,
sender,
new { },
BuildInformEmailBody(context, node),
BuildInformEmailBody(context, node, await BuildPreviousWorkflowNotesHtmlAsync(context)),
$"Workflow Bilgilendirme: {node.Title}",
null,
true);
@ -542,6 +570,7 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
{
throw new UserFriendlyException($"Bilgilendirme maili gonderilemedi: {result.ErrorMessage}");
}
}
private async Task<string> ResolveApproverEmailAsync(string approver)
@ -565,22 +594,257 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
return user.Email;
}
private static string BuildInformEmailBody(WorkflowRunContext context, ListFormWorkflow node)
private async Task<string> BuildPreviousWorkflowNotesHtmlAsync(WorkflowRunContext context)
{
var key = context.Keys?.FirstOrDefault()?.ToString();
if (key.IsNullOrWhiteSpace())
{
return string.Empty;
}
var notes = (await noteRepository.GetListAsync(note =>
note.EntityName == context.ListFormCode &&
note.EntityId == key &&
note.Type == "workflow"))
.OrderBy(note => note.CreationTime)
.ToList();
if (notes.Count == 0)
{
return string.Empty;
}
var noteItems = notes.Select(note =>
$"""
<div style="margin: 0 0 12px 0; padding: 10px 12px; border: 1px solid #e5e7eb; border-radius: 6px;">
<div style="font-weight: 600; margin-bottom: 6px;">{Encode(note.Subject)}</div>
<div>{note.Content}</div>
</div>
""");
return string.Join(string.Empty, noteItems);
}
private static string BuildInformEmailBody(
WorkflowRunContext context,
ListFormWorkflow node,
string previousWorkflowNotesHtml)
{
var keyText = string.Join(", ", context.Keys.Select(key => WebUtility.HtmlEncode(key?.ToString() ?? string.Empty)));
var listFormCode = WebUtility.HtmlEncode(context.ListFormCode ?? string.Empty);
var nodeTitle = WebUtility.HtmlEncode(node.Title ?? string.Empty);
var recipient = WebUtility.HtmlEncode(node.Approver ?? string.Empty);
var processRows = BuildWorkflowNoteContent(context.WorkflowNoteRows);
var previousNotesSection = previousWorkflowNotesHtml.IsNullOrWhiteSpace()
? string.Empty
: $"""
<h3 style="margin: 18px 0 8px 0;">Önceki Workflow Notları</h3>
{previousWorkflowNotesHtml}
""";
return $"""
<p>Workflow bilgilendirme adimina ulasildi.</p>
<table>
<tr><td><strong>Liste Formu</strong></td><td>{listFormCode}</td></tr>
<tr><td><strong>Adim</strong></td><td>{nodeTitle}</td></tr>
<tr><td><strong>Kayit</strong></td><td>{keyText}</td></tr>
</table>
<div style="font-family: Arial, sans-serif; color: #111827; line-height: 1.45;">
<p>Workflow sürecinde bilgilendirme adımına ulaşıldı.</p>
<table style="border-collapse: collapse; width: 100%; margin-bottom: 16px;">
<tr><td style="padding: 6px 8px;"><strong>Liste Formu</strong></td><td style="padding: 6px 8px;">{listFormCode}</td></tr>
<tr><td style="padding: 6px 8px;"><strong>Kayıt</strong></td><td style="padding: 6px 8px;">{keyText}</td></tr>
<tr><td style="padding: 6px 8px;"><strong>Bilgilendirme Adımı</strong></td><td style="padding: 6px 8px;">{nodeTitle}</td></tr>
<tr><td style="padding: 6px 8px;"><strong>Alıcı</strong></td><td style="padding: 6px 8px;">{recipient}</td></tr>
</table>
<h3 style="margin: 18px 0 8px 0;">Bu İşlemdeki Süreç Özeti</h3>
{processRows}
{previousNotesSection}
</div>
""";
}
private string ResolveCurrentUserDisplayName()
{
return CurrentUser.UserName
?? CurrentUser.Name
?? CurrentUser.Id?.ToString()
?? "System";
}
private static void AddWorkflowDecisionRows(
WorkflowRunContext context,
ListFormWorkflow current,
bool approved,
string description)
{
var action = approved ? "Approved" : "Rejected";
context.WorkflowNoteRows.Add(("Decision: ", $"{action}: {FormatNode(current)}"));
context.WorkflowNoteRows.Add(("Description: ", description ?? string.Empty));
}
private static void AddWorkflowNodeRows(
WorkflowRunContext context,
ListFormWorkflow node)
{
var action = node.Kind switch
{
"Start" => "Started",
"Compare" => "Evaluated",
"Approval" => "Waiting Approval",
"Inform" => "Informed",
"End" => "Completed",
_ => "Processed"
};
context.WorkflowNoteRows.Add(($"{action}: ", $"{FormatNode(node)}"));
if (!node.Approver.IsNullOrWhiteSpace())
{
context.WorkflowNoteRows.Add((node.Kind == "Inform" ? "Inform: " : "Approver: ", node.Approver));
}
}
private void AddWorkflowToastMessage(
WorkflowRunContext context,
ListFormWorkflow node)
{
var userName = ResolveCurrentUserDisplayName();
var next = FindNextToastNode(context, node);
var messageLines = node.Kind switch
{
"Start" => new[]
{
localizer["ListForms.ListForm.Workflow.WorkflowStarted"].Value,
localizer["ListForms.ListForm.Workflow.Step", node.Title].Value,
localizer["ListForms.ListForm.Workflow.PerformedBy", userName].Value
}.Concat(FormatNextToastNode(next)),
"Inform" => new[]
{
localizer["ListForms.ListForm.Workflow.InformReached"].Value,
localizer["ListForms.ListForm.Workflow.Step", node.Title].Value,
localizer["ListForms.ListForm.Workflow.InformUser", FormatToastUser(node.Approver)].Value
}.Concat(FormatNextToastNode(next)),
"End" => new[]
{
localizer["ListForms.ListForm.Workflow.WorkflowCompleted"].Value,
localizer["ListForms.ListForm.Workflow.Step", node.Title].Value,
localizer["ListForms.ListForm.Workflow.PerformedBy", userName].Value
},
_ => null
};
if (messageLines != null)
{
context.ToastMessages.Add(string.Join(Environment.NewLine, messageLines));
}
}
private ListFormWorkflow FindNextToastNode(
WorkflowRunContext context,
ListFormWorkflow node)
{
var current = FindNextCriteria(context.Criteria, ResolveNextNodeId(context, node));
var visited = new HashSet<string>();
while (current != null && visited.Add(current.Id))
{
if (current.Kind is "Approval" or "Inform" or "End")
{
return current;
}
current = FindNextCriteria(context.Criteria, ResolveNextNodeId(context, current));
}
return null;
}
private IEnumerable<string> FormatNextToastNode(ListFormWorkflow node)
{
if (node == null)
{
return [];
}
return node.Kind switch
{
"Approval" =>
[
localizer["ListForms.ListForm.Workflow.NextStep.Approval"].Value,
localizer["ListForms.ListForm.Workflow.NextStepName", node.Title].Value,
localizer["ListForms.ListForm.Workflow.ApproverUser", FormatToastUser(node.Approver)].Value
],
"Inform" =>
[
localizer["ListForms.ListForm.Workflow.NextStep.Inform"].Value,
localizer["ListForms.ListForm.Workflow.NextStepName", node.Title].Value,
localizer["ListForms.ListForm.Workflow.InformUser", FormatToastUser(node.Approver)].Value
],
"End" =>
[
localizer["ListForms.ListForm.Workflow.NextStep.End"].Value,
localizer["ListForms.ListForm.Workflow.NextStepName", node.Title].Value
],
_ => [localizer["ListForms.ListForm.Workflow.NextStep", node.Title].Value]
};
}
private string FormatToastUser(string userName)
{
return userName.IsNullOrWhiteSpace()
? localizer["ListForms.ListForm.Workflow.UndefinedUser"].Value
: userName;
}
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 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 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 Encode(string value)
{
return WebUtility.HtmlEncode(value ?? string.Empty);
}
private async Task UpdateRowAsync(WorkflowRunContext context, Dictionary<string, object> data)
{
await queryManager.GenerateAndRunQueryAsync<int>(
@ -762,7 +1026,8 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
CurrentNodeTitle = node?.Title,
CurrentNodeKind = node?.Kind,
WaitingApproval = waitingApproval,
Completed = completed
Completed = completed,
ToastMessages = context.ToastMessages.ToList()
};
}
@ -906,6 +1171,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 ?? []);
@ -947,12 +1242,14 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
private sealed record WorkflowRunContext(
string ListFormCode,
string KeyFieldName,
object[] Keys,
WorkflowDto Workflow,
List<ListFormWorkflow> Criteria,
IDictionary<string, object> Row)
{
public HashSet<string> UserUpdatedFields { get; } = [];
public List<(string Label, string Value)> WorkflowNoteRows { get; } = [];
public List<string> ToastMessages { get; } = [];
}
}

View file

@ -30,13 +30,15 @@ public class MenuAppService : CrudAppService<
private readonly IRepository<LanguageText, Guid> _repositoryText;
private readonly ITenantRepository _tenantRepository;
private readonly IPermissionDefinitionRecordRepository _permissionRepository;
private readonly LanguageTextAppService _languageTextAppService;
public MenuAppService(
IRepository<Menu, Guid> menuRepository,
IRepository<LanguageKey, Guid> languageKeyRepository,
IRepository<LanguageText, Guid> languageTextRepository,
ITenantRepository tenantRepository,
IPermissionDefinitionRecordRepository permissionRepository
IPermissionDefinitionRecordRepository permissionRepository,
LanguageTextAppService languageTextAppService
) : base(menuRepository)
{
_menuRepository = menuRepository;
@ -44,6 +46,7 @@ public class MenuAppService : CrudAppService<
_repositoryText = languageTextRepository;
_tenantRepository = tenantRepository;
_permissionRepository = permissionRepository;
_languageTextAppService = languageTextAppService;
CreatePolicyName = $"{AppCodes.Menus.Menu}.Create";
UpdatePolicyName = $"{AppCodes.Menus.Menu}.Update";
@ -275,7 +278,7 @@ public class MenuAppService : CrudAppService<
if (existingEnText != null)
{
existingEnText.Value = input.MenuTextEn;
await _repositoryText.UpdateAsync(existingEnText);
await _repositoryText.UpdateAsync(existingEnText, autoSave: true);
}
else
{
@ -285,7 +288,7 @@ public class MenuAppService : CrudAppService<
CultureName = "en",
Value = input.MenuTextEn,
ResourceName = PlatformConsts.AppName
});
}, autoSave: true);
}
// Türkçe text oluşturuluyor veya güncelleniyor.
@ -297,7 +300,7 @@ public class MenuAppService : CrudAppService<
if (existingTrText != null)
{
existingTrText.Value = input.MenuTextTr;
await _repositoryText.UpdateAsync(existingTrText);
await _repositoryText.UpdateAsync(existingTrText, autoSave: true);
}
else
{
@ -307,9 +310,12 @@ public class MenuAppService : CrudAppService<
CultureName = "tr",
Value = input.MenuTextTr,
ResourceName = PlatformConsts.AppName
});
}, autoSave: true);
}
// Clear Redis Cache
await _languageTextAppService.ClearRedisCacheAsync();
return await base.CreateAsync(input);
}
}

View file

@ -674,7 +674,7 @@
"code": "Abp.Account.EnableLocalLogin",
"nameKey": "Abp.Account.EnableLocalLogin",
"descriptionKey": "Abp.Account.EnableLocalLogin.Description",
"defaultValue": "False",
"defaultValue": "True",
"isVisibleToClients": false,
"providers": "G|D",
"isInherited": false,
@ -1071,6 +1071,22 @@
"dataType": "Number",
"selectOptions": {},
"order": 80
},
{
"code": "Abp.Identity.OrganizationUnit.MaxUserMembershipCount",
"nameKey": "Abp.Identity.OrganizationUnit.MaxUserMembershipCount",
"descriptionKey": "Abp.Identity.OrganizationUnit.MaxUserMembershipCount.Description",
"defaultValue": "2147483647",
"isVisibleToClients": true,
"providers": "T|G|D",
"isInherited": true,
"isEncrypted": false,
"mainGroupKey": "Abp.Identity",
"subGroupKey": "Abp.Identity.OrganizationUnits",
"requiredPermissionName": "Abp.Identity.OrganizationUnits",
"dataType": "Number",
"selectOptions": {},
"order": 90
}
],
"NotificationTypes": [],

View file

@ -3642,6 +3642,36 @@
"en": "The record was deleted",
"tr": "Kayıt silindi"
},
{
"resourceName": "Platform",
"key": "TumKayitlarSilindi",
"en": "All records were deleted.",
"tr": "Tüm kayıtlar silindi."
},
{
"resourceName": "Platform",
"key": "SeciliKayitBekliyor",
"en": "The selected record is not waiting for this approval step or approval user.",
"tr": "Seçili kayit bu onay adımında veya onay kullanıcısında beklemiyor."
},
{
"resourceName": "Platform",
"key": "WorkflowAlreadyStarted",
"en": "Workflow has already been started for the selected record",
"tr": "Seçili kayıt icin workflow zaten başlamış."
},
{
"resourceName": "Platform",
"key": "SeciliKayitlarSilmekIstiyormusunuz",
"en": "{0} records will be deleted. Are you sure you want to delete?",
"tr": "{0} kayit silinecek. Silmek istediginize emin misiniz?"
},
{
"resourceName": "Platform",
"key": "TumKayitlariSilmekIstiyormusunuz",
"en": "Are you sure to delete all {0} records?",
"tr": "Tüm {0} kayıtları silmek istediğinize emin misiniz?"
},
{
"resourceName": "Platform",
"key": "KayitEklendi",
@ -3735,8 +3765,8 @@
{
"resourceName": "Platform",
"key": "ListForms.ListForm.AddNewRecord",
"en": "Add New Record",
"tr": "Yeni Kayıt Ekle"
"en": "Add",
"tr": "Ekle"
},
{
"resourceName": "Platform",
@ -4154,9 +4184,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",
@ -16736,6 +16766,24 @@
"en": "Approver",
"tr": "Onayla"
},
{
"resourceName": "Platform",
"key": "App.Listform.ListformField.Rejecter",
"en": "Rejecter",
"tr": "Reddet"
},
{
"resourceName": "Platform",
"key": "App.Listform.ListformField.ApprovalComment",
"en": "Approval or Rejection Comment",
"tr": "Onay veya red açıklaması"
},
{
"resourceName": "Platform",
"key": "App.Listform.ListformField.WorkflowDecisionMessage",
"en": "Workflow decision will be made for {0} record(s).",
"tr": "{0} kayit icin workflow karari verilecek."
},
{
"resourceName": "Platform",
"key": "App.Listform.ListformField.NextOnStart",
@ -17474,6 +17522,12 @@
"en": "Columns load after selecting a Select Command",
"tr": "Select Command seçince sütunlar yüklenir"
},
{
"resourceName": "Platform",
"key": "ListForms.Wizard.Step3.IncludeInEditingForm",
"en": "Include in Editing Form",
"tr": "Düzenleme Formunda Dahil Et"
},
{
"resourceName": "Platform",
"key": "ListForms.Wizard.Step3.GenerateFromTable",
@ -17714,6 +17768,12 @@
"en": "Key Field Type",
"tr": "Anahtar Alan Tipi"
},
{
"resourceName": "Platform",
"key": "ListForms.Wizard.Step4.ColumnsAndFormLayout",
"en": "Columns & Form Layout",
"tr": "Sütunlar ve Form Yerleşimi"
},
{
"resourceName": "Platform",
"key": "ListForms.Wizard.Step4.SelectedColumns",
@ -17738,6 +17798,24 @@
"en": "Field",
"tr": "Alan"
},
{
"resourceName": "Platform",
"key": "ListForms.Wizard.Step4.EditingForm",
"en": "Popup Form",
"tr": "Popup Form"
},
{
"resourceName": "Platform",
"key": "ListForms.Wizard.Step4.EditingFormColumns",
"en": "Popup Form Columns",
"tr": "Popup Form Sütunları"
},
{
"resourceName": "Platform",
"key": "ListForms.Wizard.Step4.UngroupedColumns",
"en": "Ungrouped Columns",
"tr": "Gruplanmamış Sütunlar"
},
{
"resourceName": "Platform",
"key": "ListForms.Wizard.Step4.DeployAndSave",
@ -18038,6 +18116,12 @@
"en": "Add Multi-Tenant Column",
"tr": "MultiTenant Sütunları Ekle"
},
{
"resourceName": "Platform",
"key": "App.SqlQueryManager.AddWorkflowColumns",
"en": "Add Workflow Column",
"tr": "Workflow Sütunları Ekle"
},
{
"resourceName": "Platform",
"key": "App.SqlQueryManager.ClearAllColumns",
@ -18986,6 +19070,12 @@
"en": "Approval Status Field Name",
"tr": "Onay Durumu Alanı Adı"
},
{
"resourceName": "Platform",
"key": "ListForms.ListFormEdit.Workflow.IsFilterUserName",
"en": "Filter User Name?",
"tr": "Kullanıcı Adı Filtresin mi?"
},
{
"resourceName": "Platform",
"key": "ListForms.ListFormEdit.Workflow.ApprovalDescriptionFieldName",
@ -19231,6 +19321,84 @@
"key": "FileManager.SortByModifiedDesc",
"en": "Modified (Newest)",
"tr": "Değiştirilme (En Yeni)"
},
{
"resourceName": "Platform",
"key": "ListForms.ListForm.Workflow.WorkflowStarted",
"en": "Operation: Workflow started",
"tr": "İşlem: Workflow başlatıldı"
},
{
"resourceName": "Platform",
"key": "ListForms.ListForm.Workflow.InformReached",
"en": "Operation: Workflow inform step reached",
"tr": "İşlem: Workflow bilgilendirme adımına ulaştı"
},
{
"resourceName": "Platform",
"key": "ListForms.ListForm.Workflow.WorkflowCompleted",
"en": "Operation: Workflow completed",
"tr": "İşlem: Workflow tamamlandı"
},
{
"resourceName": "Platform",
"key": "ListForms.ListForm.Workflow.Step",
"en": "Step: {0}",
"tr": "Adım: {0}"
},
{
"resourceName": "Platform",
"key": "ListForms.ListForm.Workflow.PerformedBy",
"en": "Performed by: {0}",
"tr": "İşlemi yapan: {0}"
},
{
"resourceName": "Platform",
"key": "ListForms.ListForm.Workflow.InformUser",
"en": "User to inform: {0}",
"tr": "Bilgilendirilecek kullanıcı: {0}"
},
{
"resourceName": "Platform",
"key": "ListForms.ListForm.Workflow.ApproverUser",
"en": "Approver user: {0}",
"tr": "Onaylayacak kullanıcı: {0}"
},
{
"resourceName": "Platform",
"key": "ListForms.ListForm.Workflow.NextStep.Approval",
"en": "Next step: Approval",
"tr": "Sonraki adım: Onay"
},
{
"resourceName": "Platform",
"key": "ListForms.ListForm.Workflow.NextStep.Inform",
"en": "Next step: Inform",
"tr": "Sonraki adım: Bilgilendirme"
},
{
"resourceName": "Platform",
"key": "ListForms.ListForm.Workflow.NextStep.End",
"en": "Next step: Workflow end",
"tr": "Sonraki adım: Workflow bitiş"
},
{
"resourceName": "Platform",
"key": "ListForms.ListForm.Workflow.NextStepName",
"en": "Next step name: {0}",
"tr": "Sonraki adım adı: {0}"
},
{
"resourceName": "Platform",
"key": "ListForms.ListForm.Workflow.NextStep",
"en": "Next step: {0}",
"tr": "Sonraki adım: {0}"
},
{
"resourceName": "Platform",
"key": "ListForms.ListForm.Workflow.UndefinedUser",
"en": "Undefined",
"tr": "Tanımsız"
}
]
}

View file

@ -797,7 +797,7 @@ public class ListFormSeeder_Administration : IDataSeedContributor, ITransientDep
PermissionJson = DefaultPermissionJson(PlatformConsts.IdentityPermissions.Users.Create, listFormName, PlatformConsts.IdentityPermissions.Users.Update, PlatformConsts.IdentityPermissions.Users.Delete, PlatformConsts.IdentityPermissions.Users.Export, PlatformConsts.IdentityPermissions.Users.Import, PlatformConsts.IdentityPermissions.Users.Note),
DeleteCommand = $"UPDATE \"AbpUsers\" SET \"DeleterId\"=@DeleterId, \"DeletionTime\"=CURRENT_TIMESTAMP, \"IsDeleted\"='true' WHERE \"Id\"=@Id",
DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(),
EditingOptionJson = DefaultEditingOptionJson(listFormName, 500, 730, true, true, true, true, false),
EditingOptionJson = DefaultEditingOptionJson(listFormName, 500, 710, true, true, true, true, false),
EditingFormJson = JsonSerializer.Serialize(new List<EditingFormDto>() {
new () { Order=1,ColCount=1,ColSpan=1,ItemType="group",Items=[
new EditingFormItemDto { Order=1, DataField="Email", ColSpan=1, IsRequired=true, EditorType2=EditorTypes.dxTextBox },

View file

@ -12,7 +12,7 @@ public static class ListFormSeeder_DefaultJsons
{
public static string DefaultDeleteCommand(string tableName)
{
return $"UPDATE \"{TableNameResolver.GetFullTableName(tableName)}\" SET \"DeleterId\"=@DeleterId, \"DeletionTime\"=CURRENT_TIMESTAMP, \"IsDeleted\"='true' WHERE \"Id\"=@Id";
return $"UPDATE \"{TableNameResolver.GetFullTableName(tableName)}\" SET \"DeleterId\"=@DeleterId, \"DeletionTime\"=CURRENT_TIMESTAMP, \"IsDeleted\"='true' WHERE \"Id\" IN @Id";
}
public static string DefaultInsertFieldsDefaultValueJson(DbType dbType = DbType.Guid, string newId = "@NEWID") => JsonSerializer.Serialize(new FieldsDefaultValue[]

View file

@ -2302,7 +2302,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(),
InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(DbType.String, "Name"),
PagerOptionJson = DefaultPagerOptionJson,
EditingOptionJson = DefaultEditingOptionJson(listFormName, 600, 300, true, true, true, false, false),
EditingOptionJson = DefaultEditingOptionJson(listFormName, 600, 550, true, true, true, false, false),
EditingFormJson = JsonSerializer.Serialize(new List<EditingFormDto>() {
new() {
Order=1, ColCount=1, ColSpan=1, ItemType="group", Items=[
@ -4025,7 +4025,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
HeaderFilterJson = DefaultHeaderFilterJson,
SearchPanelJson = DefaultSearchPanelJson,
GroupPanelJson = DefaultGroupPanelJson,
SelectionJson = DefaultSelectionMultipleJson,
SelectionJson = DefaultSelectionSingleJson,
ColumnOptionJson = DefaultColumnOptionJson(false),
PermissionJson = DefaultPermissionJson(listFormName),
PagerOptionJson = DefaultPagerOptionJson,

View file

@ -0,0 +1,25 @@
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

@ -0,0 +1,385 @@
{
"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": "Onaylama",
"LanguageTextDescEn": "Approval",
"LanguageTextDescTr": "Onaylama",
"LanguageTextMenuParentEn": "Sales",
"LanguageTextMenuParentTr": "Sat\u0131\u015F",
"PermissionGroupName": "App.Wizard.Sales",
"MenuParentCode": "App.Wizard.Sales",
"MenuParentIcon": "FcAssistant",
"MenuIcon": "FcAndroidOs",
"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",
"IsFilterUserName": true,
"ApprovalDateFieldName": "ApprovalDate",
"ApprovalStatusFieldName": "ApprovalStatus",
"ApprovalDescriptionFieldName": "ApprovalDescription",
"Criteria": [
{
"ListFormCode": "App.Wizard.Approval",
"Kind": "Start",
"Title": "\u0130\u015F Ak\u0131\u015F\u0131 Ba\u015Flat1",
"CompareColumn": "Price",
"CompareOperator": "\u003E",
"CompareValue": 5000,
"Approver": "",
"NextOnStart": "N002",
"NextOnTrue": "",
"NextOnFalse": "",
"NextOnApprove": "",
"NextOnReject": "",
"PositionX": 34,
"PositionY": 104,
"CompareOutcomes": [],
"Id": "N001"
},
{
"ListFormCode": "App.Wizard.Approval",
"Kind": "Approval",
"Title": "Onay1",
"CompareColumn": "Price",
"CompareOperator": "\u003E",
"CompareValue": 5000,
"Approver": "admin@sozsoft.com",
"NextOnStart": "",
"NextOnTrue": "",
"NextOnFalse": "",
"NextOnApprove": "N003",
"NextOnReject": "N004",
"PositionX": 323,
"PositionY": 104,
"CompareOutcomes": [],
"Id": "N002"
},
{
"ListFormCode": "App.Wizard.Approval",
"Kind": "Approval",
"Title": "Onay2",
"CompareColumn": "Price",
"CompareOperator": "\u003E",
"CompareValue": 5000,
"Approver": "demo@sozsoft.com",
"NextOnStart": "",
"NextOnTrue": "",
"NextOnFalse": "",
"NextOnApprove": "N004",
"NextOnReject": "N004",
"PositionX": 586,
"PositionY": 104,
"CompareOutcomes": [],
"Id": "N003"
},
{
"ListFormCode": "App.Wizard.Approval",
"Kind": "Inform",
"Title": "Bilgilendirme1",
"CompareColumn": "Price",
"CompareOperator": "\u003E",
"CompareValue": 5000,
"Approver": "system@sozsoft.com",
"NextOnStart": "N005",
"NextOnTrue": "",
"NextOnFalse": "",
"NextOnApprove": "",
"NextOnReject": "",
"PositionX": 458,
"PositionY": 411,
"CompareOutcomes": [],
"Id": "N004"
},
{
"ListFormCode": "App.Wizard.Approval",
"Kind": "End",
"Title": "\u0130\u015F Ak\u0131\u015F\u0131 Bitir1",
"CompareColumn": "Price",
"CompareOperator": "\u003E",
"CompareValue": 5000,
"Approver": "",
"NextOnStart": "",
"NextOnTrue": "",
"NextOnFalse": "",
"NextOnApprove": "",
"NextOnReject": "",
"PositionX": 792,
"PositionY": 412,
"CompareOutcomes": [],
"Id": "N005"
}
]
},
"WorkflowCriteria": [
{
"ListFormCode": "App.Wizard.Approval",
"Kind": "Start",
"Title": "\u0130\u015F Ak\u0131\u015F\u0131 Ba\u015Flat1",
"CompareColumn": "Price",
"CompareOperator": "\u003E",
"CompareValue": 5000,
"Approver": "",
"NextOnStart": "N002",
"NextOnTrue": "",
"NextOnFalse": "",
"NextOnApprove": "",
"NextOnReject": "",
"PositionX": 34,
"PositionY": 104,
"CompareOutcomes": [],
"Id": "N001"
},
{
"ListFormCode": "App.Wizard.Approval",
"Kind": "Approval",
"Title": "Onay1",
"CompareColumn": "Price",
"CompareOperator": "\u003E",
"CompareValue": 5000,
"Approver": "admin@sozsoft.com",
"NextOnStart": "",
"NextOnTrue": "",
"NextOnFalse": "",
"NextOnApprove": "N003",
"NextOnReject": "N004",
"PositionX": 323,
"PositionY": 104,
"CompareOutcomes": [],
"Id": "N002"
},
{
"ListFormCode": "App.Wizard.Approval",
"Kind": "Approval",
"Title": "Onay2",
"CompareColumn": "Price",
"CompareOperator": "\u003E",
"CompareValue": 5000,
"Approver": "demo@sozsoft.com",
"NextOnStart": "",
"NextOnTrue": "",
"NextOnFalse": "",
"NextOnApprove": "N004",
"NextOnReject": "N004",
"PositionX": 586,
"PositionY": 104,
"CompareOutcomes": [],
"Id": "N003"
},
{
"ListFormCode": "App.Wizard.Approval",
"Kind": "Inform",
"Title": "Bilgilendirme1",
"CompareColumn": "Price",
"CompareOperator": "\u003E",
"CompareValue": 5000,
"Approver": "system@sozsoft.com",
"NextOnStart": "N005",
"NextOnTrue": "",
"NextOnFalse": "",
"NextOnApprove": "",
"NextOnReject": "",
"PositionX": 458,
"PositionY": 411,
"CompareOutcomes": [],
"Id": "N004"
},
{
"ListFormCode": "App.Wizard.Approval",
"Kind": "End",
"Title": "\u0130\u015F Ak\u0131\u015F\u0131 Bitir1",
"CompareColumn": "Price",
"CompareOperator": "\u003E",
"CompareValue": 5000,
"Approver": "",
"NextOnStart": "",
"NextOnTrue": "",
"NextOnFalse": "",
"NextOnApprove": "",
"NextOnReject": "",
"PositionX": 792,
"PositionY": 412,
"CompareOutcomes": [],
"Id": "N005"
}
]
},
"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

@ -36,6 +36,7 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
private readonly IRepository<DataSource, Guid> _repoDataSource;
private readonly IRepository<ListForm, Guid> _repoListForm;
private readonly IRepository<ListFormField, Guid> _repoListFormField;
private readonly IRepository<ListFormWorkflow, string> _repoListFormWorkflow;
private readonly ILogger<WizardDataSeeder> _logger;
private readonly string _cultureNameDefault = PlatformConsts.DefaultLanguage;
@ -51,6 +52,7 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
IRepository<DataSource, Guid> repoDataSource,
IRepository<ListForm, Guid> repoListForm,
IRepository<ListFormField, Guid> repoListFormField,
IRepository<ListFormWorkflow, string> repoListFormWorkflow,
ILogger<WizardDataSeeder> logger)
{
_repoLangKey = repoLangKey;
@ -62,6 +64,7 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
_repoDataSource = repoDataSource;
_repoListForm = repoListForm;
_repoListFormField = repoListFormField;
_repoListFormWorkflow = repoListFormWorkflow;
_logger = logger;
}
@ -134,13 +137,29 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
input.Widgets ??= new List<WidgetEditDto>();
input.Workflow ??= new WorkflowDto();
input.WorkflowCriteria ??= new List<ListFormWorkflowCriteriaDto>();
if (input.WorkflowCriteria.Count == 0 && input.Workflow.Criteria?.Count > 0)
{
input.WorkflowCriteria = input.Workflow.Criteria;
}
input.Workflow.Criteria = input.WorkflowCriteria;
EnsureUniqueWorkflowCriteriaTitles(input.WorkflowCriteria);
var wizardName = input.WizardName.Trim();
var titleLangKey = WizardConsts.WizardKeyTitle(wizardName);
var nameLangKey = WizardConsts.WizardKey(wizardName);
var descLangKey = WizardConsts.WizardKeyDesc(wizardName);
var code = WizardConsts.WizardKey(wizardName);
var code = string.IsNullOrWhiteSpace(input.MenuCode)
? WizardConsts.WizardKey(wizardName)
: input.MenuCode.Trim();
var listFormCode = string.IsNullOrWhiteSpace(input.ListFormCode)
? code
: input.ListFormCode.Trim();
var titleLangKey = $"{listFormCode}.Title";
var nameLangKey = code;
var descLangKey = $"{listFormCode}.Desc";
var permCreateName = $"{code}.Create";
var permUpdateName = $"{code}.Update";
var permDeleteName = $"{code}.Delete";
var permExportName = $"{code}.Export";
var permImportName = $"{code}.Import";
var permNoteName = $"{code}.Note";
// Dil - Language Keys
await CreateLangKeyAsync(nameLangKey, input.LanguageTextMenuEn, input.LanguageTextMenuTr);
@ -153,7 +172,10 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
{
await _repoPermGroup.InsertAsync(
new PermissionGroupDefinitionRecord(Guid.NewGuid(), groupName, groupName), autoSave: true);
await CreateLangKeyAsync(groupName, groupName, groupName);
if (string.Equals(groupName, input.MenuParentCode, StringComparison.OrdinalIgnoreCase))
await EnsureLangKeyAsync(groupName);
else
await CreateLangKeyAsync(groupName, groupName, groupName);
}
// Permissions - tek seferde mevcut permission'ları çek, sonra her birini kontrol et
@ -165,35 +187,35 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
permRead = await _repoPerm.InsertAsync(new PermissionDefinitionRecord(
Guid.NewGuid(), groupName, code, null, nameLangKey, true, MultiTenancySides.Both), autoSave: true);
var permCreate = existingPerms.FirstOrDefault(a => a.Name == WizardConsts.PermCreate(wizardName));
var permCreate = existingPerms.FirstOrDefault(a => a.Name == permCreateName);
if (permCreate == null)
permCreate = await _repoPerm.InsertAsync(new PermissionDefinitionRecord(
Guid.NewGuid(), groupName, WizardConsts.PermCreate(wizardName), permRead.Name, WizardConsts.LangKeyCreate, true, MultiTenancySides.Both), autoSave: true);
Guid.NewGuid(), groupName, permCreateName, permRead.Name, WizardConsts.LangKeyCreate, true, MultiTenancySides.Both), autoSave: true);
var permUpdate = existingPerms.FirstOrDefault(a => a.Name == WizardConsts.PermUpdate(wizardName));
var permUpdate = existingPerms.FirstOrDefault(a => a.Name == permUpdateName);
if (permUpdate == null)
permUpdate = await _repoPerm.InsertAsync(new PermissionDefinitionRecord(
Guid.NewGuid(), groupName, WizardConsts.PermUpdate(wizardName), permRead.Name, WizardConsts.LangKeyUpdate, true, MultiTenancySides.Both), autoSave: true);
Guid.NewGuid(), groupName, permUpdateName, permRead.Name, WizardConsts.LangKeyUpdate, true, MultiTenancySides.Both), autoSave: true);
var permDelete = existingPerms.FirstOrDefault(a => a.Name == WizardConsts.PermDelete(wizardName));
var permDelete = existingPerms.FirstOrDefault(a => a.Name == permDeleteName);
if (permDelete == null)
permDelete = await _repoPerm.InsertAsync(new PermissionDefinitionRecord(
Guid.NewGuid(), groupName, WizardConsts.PermDelete(wizardName), permRead.Name, WizardConsts.LangKeyDelete, true, MultiTenancySides.Both), autoSave: true);
Guid.NewGuid(), groupName, permDeleteName, permRead.Name, WizardConsts.LangKeyDelete, true, MultiTenancySides.Both), autoSave: true);
var permExport = existingPerms.FirstOrDefault(a => a.Name == WizardConsts.PermExport(wizardName));
var permExport = existingPerms.FirstOrDefault(a => a.Name == permExportName);
if (permExport == null)
permExport = await _repoPerm.InsertAsync(new PermissionDefinitionRecord(
Guid.NewGuid(), groupName, WizardConsts.PermExport(wizardName), permRead.Name, WizardConsts.LangKeyExport, true, MultiTenancySides.Both), autoSave: true);
Guid.NewGuid(), groupName, permExportName, permRead.Name, WizardConsts.LangKeyExport, true, MultiTenancySides.Both), autoSave: true);
var permImport = existingPerms.FirstOrDefault(a => a.Name == WizardConsts.PermImport(wizardName));
var permImport = existingPerms.FirstOrDefault(a => a.Name == permImportName);
if (permImport == null)
permImport = await _repoPerm.InsertAsync(new PermissionDefinitionRecord(
Guid.NewGuid(), groupName, WizardConsts.PermImport(wizardName), permRead.Name, WizardConsts.LangKeyImport, true, MultiTenancySides.Both), autoSave: true);
Guid.NewGuid(), groupName, permImportName, permRead.Name, WizardConsts.LangKeyImport, true, MultiTenancySides.Both), autoSave: true);
var permNote = existingPerms.FirstOrDefault(a => a.Name == WizardConsts.PermNote(wizardName));
var permNote = existingPerms.FirstOrDefault(a => a.Name == permNoteName);
if (permNote == null)
permNote = await _repoPerm.InsertAsync(new PermissionDefinitionRecord(
Guid.NewGuid(), groupName, WizardConsts.PermNote(wizardName), permRead.Name, WizardConsts.LangKeyNote, true, MultiTenancySides.Both), autoSave: true);
Guid.NewGuid(), groupName, permNoteName, permRead.Name, WizardConsts.LangKeyNote, true, MultiTenancySides.Both), autoSave: true);
// // Permission Grants - Admin role için, sadece eksik olanları ekle
// var existingGrants = await _permissionGrantRepository.GetListAsync("R", PlatformConsts.AbpIdentity.User.AdminRoleName);
@ -219,12 +241,18 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
if (menuParent == null)
{
var maxRootOrder = menuQueryable.Where(a => a.ParentCode == null || a.ParentCode == "").Select(a => (int?)a.Order).Max() ?? 0;
await CreateLangKeyAsync(WizardConsts.WizardKeyParent(wizardName), input.LanguageTextMenuParentEn, input.LanguageTextMenuParentTr);
var menuParentIcon = !string.IsNullOrWhiteSpace(input.MenuParentIcon)
? input.MenuParentIcon
: !string.IsNullOrWhiteSpace(input.MenuIcon)
? input.MenuIcon
: WizardConsts.MenuIcon;
await CreateLangKeyAsync(input.MenuParentCode, input.LanguageTextMenuParentEn, input.LanguageTextMenuParentTr);
menuParent = await _repoMenu.InsertAsync(new Menu
{
Code = input.MenuParentCode,
DisplayName = WizardConsts.WizardKeyParent(wizardName),
DisplayName = input.MenuParentCode,
IsDisabled = false,
Icon = menuParentIcon,
Order = maxRootOrder + 1,
}, autoSave: true);
}
@ -276,7 +304,7 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
ColSpan = g.ColCount,
ItemType = "group",
Items = g.Items
.Where(i => i.DataField != input.KeyFieldName)
.Where(i => i.IncludeInEditingForm && i.DataField != input.KeyFieldName)
.Select((it, ii) => new EditingFormItemDto
{
Order = ii + 1,
@ -289,6 +317,7 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
})
.ToArray()
})
.Where(g => g.Items.Length > 0)
.ToList();
// ListForm - varsa sil, yeniden ekle
@ -304,6 +333,12 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
await _repoListFormField.DeleteManyAsync(existingListFormFields, autoSave: true);
}
var existingWorkflowCriteria = await _repoListFormWorkflow.GetListAsync(a => a.ListFormCode == input.ListFormCode);
if (existingWorkflowCriteria.Count > 0)
{
await _repoListFormWorkflow.DeleteManyAsync(existingWorkflowCriteria, autoSave: true);
}
// ListForm
await _repoListForm.InsertAsync(new ListForm
{
@ -311,7 +346,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,
@ -332,7 +367,7 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
HeaderFilterJson = WizardConsts.DefaultHeaderFilterJson,
SearchPanelJson = WizardConsts.DefaultSearchPanelJson,
GroupPanelJson = JsonSerializer.Serialize(new { Visible = false }),
SelectionJson = WizardConsts.DefaultSelectionSingleJson,
SelectionJson = WizardConsts.DefaultSelectionSingleJson(input.WorkflowCriteria.Count > 0 ? GridOptions.SelectionModeSingle : GridOptions.SelectionModeNone),
ColumnOptionJson = WizardConsts.DefaultColumnOptionJson(),
PermissionJson = WizardConsts.DefaultPermissionJson(code),
DeleteCommand = isDeleted ? WizardConsts.DefaultDeleteCommand(input.SelectCommand) : null,
@ -381,6 +416,34 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
await CreateLangKeyAsync(item.CaptionName, item.EnglishCaption, item.TurkishCaption);
}
}
foreach (var criteria in input.WorkflowCriteria)
{
if (string.IsNullOrWhiteSpace(criteria.Id))
{
_logger.LogWarning("Workflow criteria skipped because Id is empty. ListFormCode: {ListFormCode}, Title: {Title}", input.ListFormCode, criteria.Title);
continue;
}
await _repoListFormWorkflow.InsertAsync(new ListFormWorkflow(criteria.Id)
{
ListFormCode = string.IsNullOrWhiteSpace(criteria.ListFormCode) ? input.ListFormCode : criteria.ListFormCode,
Kind = criteria.Kind,
Title = criteria.Title,
CompareColumn = criteria.CompareColumn,
CompareOperator = criteria.CompareOperator,
CompareValue = criteria.CompareValue,
Approver = criteria.Approver,
NextOnStart = criteria.NextOnStart,
NextOnTrue = criteria.NextOnTrue,
NextOnFalse = criteria.NextOnFalse,
NextOnApprove = criteria.NextOnApprove,
NextOnReject = criteria.NextOnReject,
PositionX = criteria.PositionX,
PositionY = criteria.PositionY,
CompareOutcomesJson = JsonSerializer.Serialize(criteria.CompareOutcomes ?? []),
}, autoSave: true);
}
}
private static bool HasWorkflow(WorkflowDto workflow, List<ListFormWorkflowCriteriaDto> criteria)
@ -394,6 +457,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;
@ -427,6 +544,16 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
}, autoSave: true);
}
}
private async Task EnsureLangKeyAsync(string key)
{
if (string.IsNullOrWhiteSpace(key)) return;
if (!await _repoLangKey.AnyAsync(a => a.ResourceName == _appName && a.Key == key))
{
await _repoLangKey.InsertAsync(new LanguageKey { ResourceName = _appName, Key = key }, autoSave: true);
}
}
}

View file

@ -773,10 +773,10 @@ public static class PlatformConsts
{
public static class ParameterTypes
{
public const string Static = "S";
public const string Query = "Q";
public const string Path = "P";
public const string Body = "B";
public const string Static = "Static";
public const string Query = "Query";
public const string Path = "Path";
public const string Body = "Body";
}
}

View file

@ -95,9 +95,9 @@ public static class WizardConsts
public static readonly string DefaultSearchPanelJson = JsonSerializer.Serialize(new { Visible = true });
public static readonly string DefaultGroupPanelJson = JsonSerializer.Serialize(new { Visible = true });
public static readonly string DefaultSelectionSingleJson = JsonSerializer.Serialize(new
public static string DefaultSelectionSingleJson(string Mode = "none") => JsonSerializer.Serialize(new
{
Mode = GridOptions.SelectionModeNone,
Mode = Mode,
AllowSelectAll = false
});
@ -134,7 +134,7 @@ public static class WizardConsts
R = permissionName,
U = permissionName + ".Update",
E = true,
I = false,
I = true,
Deny = false
});
}
@ -163,7 +163,7 @@ public static class WizardConsts
public static string DefaultDeleteCommand(string tableName)
{
return $"UPDATE \"{tableName}\" SET \"DeleterId\"=@DeleterId, \"DeletionTime\"=CURRENT_TIMESTAMP, \"IsDeleted\"='true' WHERE \"Id\"=@Id";
return $"UPDATE \"{tableName}\" SET \"DeleterId\"=@DeleterId, \"DeletionTime\"=CURRENT_TIMESTAMP, \"IsDeleted\"='true' WHERE \"Id\" IN @Id";
}
public static string DefaultInsertFieldsDefaultValueJson(DbType dbType = DbType.Guid)

View file

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

View file

@ -89,7 +89,9 @@ public class DefaultValueManager : PlatformDomainService, IDefaultValueManager
else if (defaultField.Value == PlatformConsts.DefaultValues.Year)
value = Clock.Now.Year;
else if (defaultField.Value == PlatformConsts.DefaultValues.Id)
value = keys?.FirstOrDefault();
value = op == OperationEnum.Delete
? keys
: keys?.FirstOrDefault();
else if (defaultField.Value == PlatformConsts.DefaultValues.NewId)
value = Guid.NewGuid();
else if (defaultField.Value == PlatformConsts.DefaultValues.Selected_Ids)

View file

@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Data;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Sozsoft.Platform.DynamicData;
using Sozsoft.Platform.Entities;
@ -169,7 +170,7 @@ public class QueryManager : PlatformDomainService, IQueryManager
// oncelik Command alanindadir, dolu ise silme islemi buradaki sorguya yonlendirilir
if (!string.IsNullOrEmpty(command))
{
sql = command;
sql = NormalizeCollectionParameterCommand(command, parameters, dataSourceType);
}
else
{
@ -189,7 +190,7 @@ public class QueryManager : PlatformDomainService, IQueryManager
{
var where = dataSourceType switch
{
DataSourceTypeEnum.Mssql => $"\"{listForm.KeyFieldName}\" IN (@{listForm.KeyFieldName})",
DataSourceTypeEnum.Mssql => $"\"{listForm.KeyFieldName}\" IN @{listForm.KeyFieldName}",
DataSourceTypeEnum.Postgresql => $"\"{listForm.KeyFieldName}\" = ANY(@{listForm.KeyFieldName})",
_ => string.Empty,
};
@ -209,7 +210,14 @@ public class QueryManager : PlatformDomainService, IQueryManager
string where = string.Empty;
if (parameters.Any())
{
where = string.Join(" AND ", parameters.Select(a => $"\"{a.Key}\" IN (@{a.Key})").ToList());
where = string.Join(
" AND ",
parameters.Select(a => dataSourceType switch
{
DataSourceTypeEnum.Mssql => $"\"{a.Key}\" IN @{a.Key}",
DataSourceTypeEnum.Postgresql => $"\"{a.Key}\" = ANY(@{a.Key})",
_ => "1 = 0",
}).ToList());
}
else
{
@ -220,7 +228,7 @@ public class QueryManager : PlatformDomainService, IQueryManager
where = dataSourceType switch
{
DataSourceTypeEnum.Mssql => $"\"{listForm.KeyFieldName}\" IN (@{listForm.KeyFieldName})",
DataSourceTypeEnum.Mssql => $"\"{listForm.KeyFieldName}\" IN @{listForm.KeyFieldName}",
DataSourceTypeEnum.Postgresql => $"\"{listForm.KeyFieldName}\" = ANY(@{listForm.KeyFieldName})",
_ => "1 = 0",
};
@ -231,5 +239,78 @@ public class QueryManager : PlatformDomainService, IQueryManager
return sql;
}
private static string NormalizeCollectionParameterCommand(
string command,
Dictionary<string, object> parameters,
DataSourceTypeEnum dataSourceType)
{
if (string.IsNullOrWhiteSpace(command) || parameters == null || parameters.Count == 0)
{
return command;
}
var sql = command;
foreach (var parameter in parameters)
{
if (!IsCollectionParameter(parameter.Value))
{
continue;
}
var escapedParameterName = Regex.Escape(parameter.Key);
sql = dataSourceType switch
{
DataSourceTypeEnum.Mssql => NormalizeMssqlCollectionParameter(sql, escapedParameterName, parameter.Key),
DataSourceTypeEnum.Postgresql => NormalizePostgresqlCollectionParameter(sql, escapedParameterName, parameter.Key),
_ => sql
};
}
return sql;
}
private static bool IsCollectionParameter(object value)
{
return value is System.Collections.IEnumerable
&& value is not string
&& value is not byte[];
}
private static string NormalizeMssqlCollectionParameter(
string sql,
string escapedParameterName,
string parameterName)
{
sql = Regex.Replace(
sql,
$@"IN\s*\(\s*@{escapedParameterName}\s*\)",
$"IN @{parameterName}",
RegexOptions.IgnoreCase);
return Regex.Replace(
sql,
$@"(?<column>(""[^""]+""|\[[^\]]+\]|`[^`]+`|\w+))\s*=\s*@{escapedParameterName}\b",
match => $"{match.Groups["column"].Value} IN @{parameterName}",
RegexOptions.IgnoreCase);
}
private static string NormalizePostgresqlCollectionParameter(
string sql,
string escapedParameterName,
string parameterName)
{
sql = Regex.Replace(
sql,
$@"IN\s*\(\s*@{escapedParameterName}\s*\)",
$"= ANY(@{parameterName})",
RegexOptions.IgnoreCase);
return Regex.Replace(
sql,
$@"(?<column>(""[^""]+""|\[[^\]]+\]|`[^`]+`|\w+))\s*=\s*@{escapedParameterName}\b",
match => $"{match.Groups["column"].Value} = ANY(@{parameterName})",
RegexOptions.IgnoreCase);
}
}

View file

@ -465,6 +465,28 @@ public class SelectQueryManager : PlatformDomainService, ISelectQueryManager
}
}
if (listform.WorkflowJson.IsNullOrWhiteSpace() == false)
{
var workflow = JsonSerializer.Deserialize<Workflow>(listform.WorkflowJson);
if (workflow != null && workflow.IsFilterUserName)
{
if (whereParts.Any())
{
whereParts.Add("AND");
}
// Hem CurrentUserName alanı boş olan kayıtları
// hem de ApprovalUserFieldName alanı CurrentUserName'e eşit olan kayıtları getirmek istiyoruz,
// Boş olanları getirmemizin sebebi workflow start edebilmektir.
// İlk kayıt eklenince onaylayacak kişi atanmaz, böylece o kayıt onaysız olarak kalmaz ve workflow başlatılabilir olur.
whereParts.Add(
$"(\"{workflow.ApprovalUserFieldName}\" = '{CurrentUser.UserName}' " +
$"OR \"{workflow.ApprovalUserFieldName}\" IS NULL " +
$"OR \"{workflow.ApprovalUserFieldName}\" = '')"
);
}
}
if (!whereParts.Any())
{
whereParts.Add("1 = 1");

View file

@ -0,0 +1,23 @@
using System.Collections.Generic;
using Volo.Abp.Domain.Values;
namespace Sozsoft.Platform.Queries;
public class Workflow : ValueObject
{
public string ApprovalUserFieldName { get; set; }
public bool IsFilterUserName { get; set; }
public string ApprovalDateFieldName { get; set; }
public string ApprovalStatusFieldName { get; set; }
public string ApprovalDescriptionFieldName { get; set; }
protected override IEnumerable<object> GetAtomicValues()
{
yield return ApprovalUserFieldName;
yield return IsFilterUserName;
yield return ApprovalDateFieldName;
yield return ApprovalStatusFieldName;
yield return ApprovalDescriptionFieldName;
}
}

View file

@ -2,8 +2,12 @@
using System.Collections.Generic;
using System.Data;
using System.Data.Common;
using System.Globalization;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Dapper;
using Sozsoft.Platform;
using Sozsoft.Platform.DynamicData;
using Microsoft.Data.SqlClient;
using Volo.Abp.DependencyInjection;
@ -178,7 +182,7 @@ public class MsDynamicDataRepository : IDynamicDataRepository, IScopedDependency
public virtual async Task<List<T>> QueryAsync<T>(string sql, string cs, Dictionary<string, object> parameters = null)
{
var param = new DynamicParameters(parameters);
var param = CreateDynamicParameters(parameters);
var dbConnection = await GetOrCreateConnectionAsync(cs);
var transaction = await GetOrCreateTransactionAsync(dbConnection, cs);
@ -188,7 +192,7 @@ public class MsDynamicDataRepository : IDynamicDataRepository, IScopedDependency
public virtual async Task<IEnumerable<dynamic>> QueryAsync(string sql, string cs, Dictionary<string, object> parameters = null)
{
var param = new DynamicParameters(parameters);
var param = CreateDynamicParameters(parameters);
var dbConnection = await GetOrCreateConnectionAsync(cs);
var transaction = await GetOrCreateTransactionAsync(dbConnection, cs);
@ -197,7 +201,7 @@ public class MsDynamicDataRepository : IDynamicDataRepository, IScopedDependency
public virtual async Task<T> QuerySingleAsync<T>(string sql, string cs, Dictionary<string, object> parameters = null)
{
var param = new DynamicParameters(parameters);
var param = CreateDynamicParameters(parameters);
var dbConnection = await GetOrCreateConnectionAsync(cs);
var transaction = await GetOrCreateTransactionAsync(dbConnection, cs);
@ -206,7 +210,7 @@ public class MsDynamicDataRepository : IDynamicDataRepository, IScopedDependency
public virtual async Task<T> ExecuteScalarAsync<T>(string sql, string cs, Dictionary<string, object> parameters = null)
{
var param = new DynamicParameters(parameters);
var param = CreateDynamicParameters(parameters);
var dbConnection = await GetOrCreateConnectionAsync(cs);
var transaction = await GetOrCreateTransactionAsync(dbConnection, cs);
@ -237,13 +241,257 @@ public class MsDynamicDataRepository : IDynamicDataRepository, IScopedDependency
public virtual async Task<int> ExecuteAsync(string sql, string cs, Dictionary<string, object> parameters = null)
{
var param = new DynamicParameters(parameters);
var param = CreateDynamicParameters(parameters);
var dbConnection = await GetOrCreateConnectionAsync(cs);
var transaction = await GetOrCreateTransactionAsync(dbConnection, cs);
return await dbConnection.ExecuteAsync(sql, param, transaction);
}
private static DynamicParameters CreateDynamicParameters(Dictionary<string, object> parameters)
{
var dynamicParameters = new DynamicParameters();
if (parameters == null)
{
return dynamicParameters;
}
foreach (var parameter in parameters)
{
dynamicParameters.Add(parameter.Key, NormalizeParameterValue(parameter.Value));
}
return dynamicParameters;
}
private static object NormalizeParameterValue(object value)
{
if (value == null || value == DBNull.Value)
{
return value;
}
if (value is JsonElement jsonElement)
{
return NormalizeJsonElement(jsonElement);
}
if (value is Array array && value is not byte[])
{
return NormalizeArrayParameter(array);
}
return value;
}
private static object NormalizeArrayParameter(Array values)
{
var normalizedValues = values
.Cast<object>()
.Select(NormalizeParameterValue)
.Where(value => value != null && value != DBNull.Value)
.ToArray();
if (normalizedValues.Length == 0)
{
return Array.Empty<string>();
}
if (TryBuildGuidArray(normalizedValues, out var guidValues))
{
return guidValues;
}
if (TryBuildIntArray(normalizedValues, out var intValues))
{
return intValues;
}
if (TryBuildLongArray(normalizedValues, out var longValues))
{
return longValues;
}
if (TryBuildDecimalArray(normalizedValues, out var decimalValues))
{
return decimalValues;
}
if (TryBuildBoolArray(normalizedValues, out var boolValues))
{
return boolValues;
}
if (TryBuildDateTimeOffsetArray(normalizedValues, out var dateTimeOffsetValues))
{
return dateTimeOffsetValues;
}
var stringValues = normalizedValues.Select(value => value.ToString()).ToArray();
if (stringValues.Length == 1 && stringValues[0]?.Contains(PlatformConsts.MultiValueDelimiter) == true)
{
return stringValues[0].Split(PlatformConsts.MultiValueDelimiter, StringSplitOptions.RemoveEmptyEntries);
}
return stringValues;
}
private static object NormalizeJsonElement(JsonElement value)
{
return value.ValueKind switch
{
JsonValueKind.String => value.GetString(),
JsonValueKind.Number when value.TryGetInt32(out var intValue) => intValue,
JsonValueKind.Number when value.TryGetInt64(out var longValue) => longValue,
JsonValueKind.Number when value.TryGetDecimal(out var decimalValue) => decimalValue,
JsonValueKind.True => true,
JsonValueKind.False => false,
JsonValueKind.Null => null,
JsonValueKind.Undefined => null,
_ => value.ToString()
};
}
private static bool TryBuildGuidArray(object[] values, out Guid[] result)
{
result = new Guid[values.Length];
for (var i = 0; i < values.Length; i++)
{
if (values[i] is Guid guidValue)
{
result[i] = guidValue;
continue;
}
if (!Guid.TryParse(values[i]?.ToString(), out result[i]))
{
result = null;
return false;
}
}
return true;
}
private static bool TryBuildIntArray(object[] values, out int[] result)
{
result = new int[values.Length];
for (var i = 0; i < values.Length; i++)
{
if (values[i] is int intValue)
{
result[i] = intValue;
continue;
}
if (!int.TryParse(values[i]?.ToString(), NumberStyles.Integer, CultureInfo.InvariantCulture, out result[i]))
{
result = null;
return false;
}
}
return true;
}
private static bool TryBuildLongArray(object[] values, out long[] result)
{
result = new long[values.Length];
for (var i = 0; i < values.Length; i++)
{
if (values[i] is long longValue)
{
result[i] = longValue;
continue;
}
if (!long.TryParse(values[i]?.ToString(), NumberStyles.Integer, CultureInfo.InvariantCulture, out result[i]))
{
result = null;
return false;
}
}
return true;
}
private static bool TryBuildDecimalArray(object[] values, out decimal[] result)
{
result = new decimal[values.Length];
for (var i = 0; i < values.Length; i++)
{
if (values[i] is decimal decimalValue)
{
result[i] = decimalValue;
continue;
}
if (!decimal.TryParse(values[i]?.ToString(), NumberStyles.Any, CultureInfo.InvariantCulture, out result[i]))
{
result = null;
return false;
}
}
return true;
}
private static bool TryBuildBoolArray(object[] values, out bool[] result)
{
result = new bool[values.Length];
for (var i = 0; i < values.Length; i++)
{
if (values[i] is bool boolValue)
{
result[i] = boolValue;
continue;
}
if (!bool.TryParse(values[i]?.ToString(), out result[i]))
{
result = null;
return false;
}
}
return true;
}
private static bool TryBuildDateTimeOffsetArray(object[] values, out DateTimeOffset[] result)
{
result = new DateTimeOffset[values.Length];
for (var i = 0; i < values.Length; i++)
{
if (values[i] is DateTimeOffset dateTimeOffsetValue)
{
result[i] = dateTimeOffsetValue;
continue;
}
if (values[i] is DateTime dateTimeValue)
{
result[i] = dateTimeValue;
continue;
}
if (!DateTimeOffset.TryParse(values[i]?.ToString(), CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out result[i]))
{
result = null;
return false;
}
}
return true;
}
// ------------------ Dispose ------------------
public void Dispose()

View file

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

View file

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

View file

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

View file

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

View file

@ -324,16 +324,16 @@
"Url": "/dil/",
"Method": "GET",
"DataSourceCode": "Default",
"Sql": "SELECT * FROM Plat_H_Language WHERE IsEnabled = @IsEnabled AND CultureName = @CultureName",
"Sql": "SELECT * FROM Sas_H_Language WHERE IsEnabled = @IsEnabled AND CultureName = @CultureName",
"ParametersJson": [
{
"Type": "P",
"Type": "Path",
"Name": "CultureName",
"DefaultValue": "ar",
"Path": "/dil/:CultureName/"
},
{
"Type": "S",
"Type": "Static",
"Name": "IsEnabled",
"DefaultValue": "true"
}
@ -1456,6 +1456,36 @@
"DepartmentName": "Muhasebe",
"Name": "Muhasebe Şefi",
"ParentName": "Muhasebe Müdürü"
},
{
"Id": "b7c8d9e0-f1a2-4b3c-8d9e-0f1a2b3c4d2e",
"DepartmentName": "Bilgi İşlem",
"Name": "Bilgi İşlem Müdürü",
"ParentName": "Genel Müdür"
},
{
"Id": "b7c8d9e0-f1a2-4b3c-1d9e-0f1a2b3c4d2e",
"DepartmentName": "Finans",
"Name": "Finans Müdürü",
"ParentName": "Genel Müdür"
},
{
"Id": "b7c8d9e0-f1b2-4b3c-1d9e-0f1a2b3c4d2e",
"DepartmentName": "Satış",
"Name": "İhracat Müdürü",
"ParentName": "Genel Müdür"
},
{
"Id": "b2c8d9e0-f1b2-4b3c-1d9e-0f1a2b3c4d2e",
"DepartmentName": "Satış",
"Name": "İç Piyasa Müdürü",
"ParentName": "Genel Müdür"
},
{
"Id": "b2c8d9e0-f1b2-2b3c-1d9e-0f1a2b3c4d2e",
"DepartmentName": "Üretim",
"Name": "Üretim Müdürü",
"ParentName": "Genel Müdür"
}
],
"Announcements": [

View file

@ -117,6 +117,38 @@ server {
}
# dashboard.sozsoft.com
server {
listen 443 ssl http2;
server_name dashboard.sozsoft.com;
ssl_certificate /etc/letsencrypt/live/dashboard.sozsoft.com/fullchain.pem;
ssl_trusted_certificate /etc/ssl/sozsoft.com/chain1.pem;
ssl_certificate_key /etc/letsencrypt/live/dashboard.sozsoft.com/privkey.pem;
auth_basic "Restricted";
auth_basic_user_file /etc/nginx/.htpasswd;
#sudo htpasswd -c /etc/nginx/.htpasswd sedat.ozturk
#yukarıdaki komut ile kullanıcı adı ve şifre oluşturabilirsiniz
proxy_headers_hash_max_size 2048;
proxy_headers_hash_bucket_size 128;
location / {
proxy_pass http://127.0.0.1:19999;
proxy_http_version 1.1;
include /etc/nginx/proxy_params;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Server $host;
proxy_read_timeout 300;
proxy_connect_timeout 300;
proxy_send_timeout 300;
}
}
# sozsoft.com
server {
listen 443 ssl http2;

View file

@ -13,6 +13,9 @@ volumes:
rocket_mongodb_data:
driver: local
n8n_data:
netdataconfig:
netdatalib:
netdatacache:
services:
forgejo:
@ -108,3 +111,30 @@ services:
- /etc/ssl/sozsoft.com:/etc/ssl/sozsoft.com:ro # Sertifikaları mount ettik
- ./logs/coturn:/var/log # Logları dışarı al (opsiyonel)
command: ["turnserver", "-c", "/etc/coturn/turnserver.conf"]
dashboard:
image: netdata/netdata:stable
container_name: dashboard
hostname: kursserver
restart: unless-stopped
pid: host
network_mode: host
cap_add:
- SYS_PTRACE
- SYS_ADMIN
security_opt:
- apparmor:unconfined
volumes:
- netdataconfig:/etc/netdata
- netdatalib:/var/lib/netdata
- netdatacache:/var/cache/netdata
- /:/host/root:ro,rslave
- /etc/passwd:/host/etc/passwd:ro
- /etc/group:/host/etc/group:ro
- /etc/localtime:/etc/localtime:ro
- /proc:/host/proc:ro
- /sys:/host/sys:ro
- /etc/os-release:/host/etc/os-release:ro
- /var/log:/host/var/log:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
- /run/dbus:/run/dbus:ro

View file

@ -20,6 +20,7 @@ SUBDOMAINS=(
"sozsoft.com"
"www.sozsoft.com"
"demo.sozsoft.com"
"dashboard.sozsoft.com"
)
echo "Subdomain'ler için SSL sertifikaları alınıyor..."

View file

@ -1,6 +1,19 @@
{
"commit": "0d4703c",
"commit": "dc293fc",
"releases": [
{
"version": "1.1.04",
"buildDate": "2026-06-04",
"commit": "20e7fae481ce69e9a678508ce03b5ed7831aea9f",
"changeLog": [
"- Settingde yapılan ayarlar Auth komponentlerine uygulandı.",
"- Public home ve diğer sayfaların tasarım değişikliği yapıldı.",
"- Route Type Dinamik ve Normal olarak ayrıldı.",
"- Form Devexpress DefaultValue özelliği eklendi.",
"- Devexpress DarkModa uygun şekilde güncellendi.",
"- Grid, Tree ve FormDevexpress setReadonly özelliği eklendi."
]
},
{
"version": "1.1.03",
"buildDate": "2026-05-30",
@ -137,4 +150,4 @@
]
}
]
}
}

View file

@ -14,6 +14,7 @@ export interface ListFormWizardColumnItemDto {
editorScript: string
colSpan: number
isRequired: boolean
includeInEditingForm: boolean
dbSourceType: number
}
@ -54,6 +55,7 @@ export interface ListFormWizardDto {
languageTextMenuParentTr: string
permissionGroupName: string
menuParentCode: string
menuParentIcon?: string
menuIcon: string
dataSourceCode: string
dataSourceConnectionString: string
@ -113,6 +115,7 @@ export interface WizardSeedFileItemDto {
editorScript: string
colSpan: number
isRequired: boolean
includeInEditingForm?: boolean
dbSourceType: number
turkishCaption?: string
englishCaption?: string

View file

@ -908,6 +908,7 @@ export interface WidgetEditDto {
export interface WorkflowDto {
approvalUserFieldName: string
isFilterUserName: boolean
approvalDateFieldName: string
approvalStatusFieldName: string
approvalDescriptionFieldName: string

View file

@ -0,0 +1,24 @@
import { PagedResultDto } from '@/proxy'
import { AuditLogDto } from '@/proxy/auditLog/audit-log'
import apiService from '@/services/api.service'
export interface AuditLogListRequestDto {
skipCount?: number
maxResultCount?: number
sorting?: string
listFormCode?: string
entityId?: string
}
class AuditLogService {
async getList(params?: AuditLogListRequestDto): Promise<PagedResultDto<AuditLogDto>> {
const response = await apiService.fetchData<PagedResultDto<AuditLogDto>>({
url: '/api/app/audit-log',
method: 'GET',
params,
})
return response.data
}
}
export const auditLogService = new AuditLogService()

View file

@ -11,6 +11,11 @@ import { ListResultDto, PagedAndSortedResultRequestDto, PagedResultDto } from '.
import { AuditLogDto } from '../proxy/auditLog/audit-log'
import apiService from './api.service'
export interface UserAvatarUpdateInput {
userId: string
avatar?: File
}
export const getRoles = (skipCount = 0, maxResultCount = 10) =>
apiService.fetchData<ListResultDto<IdentityRoleDto>>({
method: 'GET',
@ -36,6 +41,21 @@ export const putUserDetail = (input: UserInfoViewModel) =>
data: input,
})
export const putUserAvatar = (input: UserAvatarUpdateInput) => {
const formData = new FormData()
formData.append('userId', input.userId)
if (input.avatar) {
formData.append('avatar', input.avatar)
}
return apiService.fetchData({
method: 'PUT',
url: `/api/app/platform-identity/avatar`,
data: formData,
})
}
export const putUserLookout = (input: UserInfoViewModel) =>
apiService.fetchData({
method: 'PUT',

View file

@ -43,6 +43,7 @@ export interface WorkflowRunResultDto {
currentNodeKind?: string | null
waitingApproval: boolean
completed: boolean
toastMessages?: string[]
}
export type SaveCriteriaInput = Omit<Partial<WorkflowCriteriaDto>, 'id'> & {

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 {
const sharedPerson = item.approver || ''
@ -347,7 +432,8 @@ export function toCriteriaForm(item: WorkflowCriteriaDto): WorkflowCriteriaForm
}
export function normalizeCriteria(item: WorkflowCriteriaForm): SaveCriteriaInput {
const sharedPerson = item.approver || ''
const sharedPerson =
item.kind === 'Approval' || item.kind === 'Inform' ? item.approver || '' : ''
const compareOutcomes = (item.compareOutcomes || [])
.slice(0, 4)
.filter((outcome) => outcome.label?.trim())
@ -504,7 +590,7 @@ export function criteriaSummary(item: WorkflowCriteriaDto) {
if (item.kind === 'Approval' || item.kind === 'Inform') {
return `${item.title} ${item.approver ? `- ${item.approver}` : ''}`
}
return `${item.title} ${item.approver ? `- ${item.approver}` : ''}`
return item.title
}
export function targetTitle(criteria: WorkflowCriteriaDto[], id?: string | null) {

View file

@ -5,15 +5,16 @@ import {
emptyCriteria,
normalizeCriteria,
toCriteriaForm,
uniqueCriteriaTitle,
type WorkflowCriteriaForm,
} from '@/utils/workflow/workflowHelpers'
import { workflowService, type WorkflowCriteriaDto } from '@/services/workflow.service'
import { WorkflowDesigner } from '../workflow/WorkflowDesigner'
import { SelectBoxOption } from '@/types/shared'
import { Field, FieldProps, Form, Formik } from 'formik'
import { Button, Card, FormContainer, FormItem, Input, Select } from '@/components/ui'
import { Button, Card, Checkbox, FormContainer, FormItem, Input, Select } from '@/components/ui'
import { ListFormEditTabs } from '@/proxy/admin/list-form/options'
import { object, string } from 'yup'
import { bool, object, string } from 'yup'
import { useStoreState } from '@/store/store'
import { FormEditProps } from './FormEdit'
import { useLocalization } from '@/utils/hooks/useLocalization'
@ -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,
})
@ -296,10 +307,13 @@ export function FormTabWorkflow(
}
const schema = object().shape({
approvalUserFieldName: string().required(),
approvalStatusFieldName: string().required(),
approvalDateFieldName: string(),
approvalDescriptionFieldName: string(),
workflowDto: object().shape({
approvalUserFieldName: string().required(),
isFilterUserName: bool(),
approvalStatusFieldName: string().required(),
approvalDateFieldName: string(),
approvalDescriptionFieldName: string(),
}),
})
const initialValues = useStoreState((s) => s.admin.lists.values)
@ -320,7 +334,7 @@ export function FormTabWorkflow(
<Form>
<FormContainer size="sm">
<Card className="my-2">
<div className="grid grid-cols-1 md:grid-cols-4 gap-2">
<div className="grid grid-cols-1 md:grid-cols-5 gap-2">
<FormItem
asterisk
label={translate('::ListForms.ListFormEdit.Workflow.ApprovalUserFieldName')}
@ -370,7 +384,7 @@ export function FormTabWorkflow(
)}
</Field>
</FormItem>
<FormItem
label={translate('::ListForms.ListFormEdit.Workflow.ApprovalDateFieldName')}
invalid={
@ -421,9 +435,22 @@ export function FormTabWorkflow(
)}
</Field>
</FormItem>
<FormItem
label={translate('::ListForms.ListFormEdit.Workflow.IsFilterUserName')}
invalid={
!!(
errors.workflowDto?.isFilterUserName &&
touched.workflowDto?.isFilterUserName
)
}
errorMessage={errors.workflowDto?.isFilterUserName as string}
>
<Field name="workflowDto.isFilterUserName" component={Checkbox} />
</FormItem>
</div>
<Button block variant="solid" loading={isSubmitting}>
<Button block variant="solid" type="submit" loading={isSubmitting}>
{isSubmitting ? translate('::SavingWithThreeDot') : translate('::Save')}
</Button>
</Card>

View file

@ -3,6 +3,7 @@ import { ROUTES_ENUM } from '@/routes/route.constant'
import { SelectBoxOption } from '@/types/shared'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { Form, Formik, FormikProps } from 'formik'
import type { KeyboardEvent } from 'react'
import { useEffect, useRef, useState } from 'react'
import { Helmet } from 'react-helmet'
import { useLocation, useNavigate } from 'react-router-dom'
@ -70,6 +71,7 @@ const initialValues: ListFormWizardDto = {
languageTextMenuParentTr: '',
permissionGroupName: '',
menuParentCode: '',
menuParentIcon: '',
menuIcon: '',
dataSourceCode: '',
dataSourceConnectionString: '',
@ -94,6 +96,7 @@ const initialValues: ListFormWizardDto = {
widgets: [],
workflow: {
approvalUserFieldName: '',
isFilterUserName: false,
approvalDateFieldName: '',
approvalStatusFieldName: '',
approvalDescriptionFieldName: '',
@ -221,6 +224,7 @@ const Wizard = () => {
const [widgets, setWidgets] = useState<WidgetEditDto[]>([])
const [workflow, setWorkflow] = useState<WorkflowDto>({
approvalUserFieldName: '',
isFilterUserName: false,
approvalDateFieldName: '',
approvalStatusFieldName: '',
approvalDescriptionFieldName: '',
@ -240,6 +244,18 @@ const Wizard = () => {
])
const isAuditColumn = (columnName: string) => AUDIT_COLUMNS.has(columnName.toLowerCase())
const isTenantColumn = (columnName: string) => columnName.toLowerCase() === 'tenantid'
const isAutoSelectedColumn = (columnName: string, isTenant = false) =>
!isAuditColumn(columnName) && !(isTenant && isTenantColumn(columnName))
const removeTenantColumn = (columns: Set<string>) =>
new Set([...columns].filter((columnName) => !isTenantColumn(columnName)))
const removeTenantGroupItems = (groups: WizardGroup[]) =>
groups.map((group) => ({
...group,
items: group.items.filter((item) => !isTenantColumn(item.dataField)),
}))
const loadColumns = async (dsCode: string, schema: string, name: string) => {
if (!dsCode || !name) {
@ -253,12 +269,13 @@ const Wizard = () => {
const res = await sqlObjectManagerService.getTableColumns(dsCode, schema, name)
const cols = res.data ?? []
setSelectCommandColumns(cols)
const selectableColumns = cols.filter((c) => !isAuditColumn(c.columnName))
const colNames = new Set(cols.map((c) => c.columnName.toLowerCase()))
const hasTenantColumn = colNames.has('tenantid')
const selectableColumns = cols.filter((c) => isAutoSelectedColumn(c.columnName, hasTenantColumn))
setSelectedColumns(new Set(selectableColumns.map((c) => c.columnName)))
setEditingGroups([])
// Auto-check isTenant / isBranch based on column presence
const colNames = new Set(cols.map((c) => c.columnName.toLowerCase()))
formikRef.current?.setFieldValue('isTenant', colNames.has('tenantid'))
formikRef.current?.setFieldValue('isTenant', hasTenantColumn)
formikRef.current?.setFieldValue('isBranch', colNames.has('branchid'))
// Auto-select first column as key field
if (cols.length > 0) {
@ -284,17 +301,23 @@ const Wizard = () => {
return next
})
const toggleAllColumns = (all: boolean) =>
const toggleAllColumns = (all: boolean, isTenant = formikRef.current?.values.isTenant ?? false) =>
setSelectedColumns(
all
? new Set(
selectCommandColumns
.filter((c) => !isAuditColumn(c.columnName))
.filter((c) => isAutoSelectedColumn(c.columnName, isTenant))
.map((c) => c.columnName),
)
: new Set(),
)
const handleTenantChange = (isTenant: boolean) => {
if (!isTenant) return
setSelectedColumns((prev) => removeTenantColumn(prev))
setEditingGroups((prev) => removeTenantGroupItems(prev))
}
const getDataSourceList = async () => {
setIsLoadingDataSource(true)
const response = await getDataSources()
@ -391,6 +414,7 @@ const Wizard = () => {
languageTextMenuParentTr: w.languageTextMenuParentTr ?? '',
permissionGroupName: w.permissionGroupName ?? '',
menuParentCode: w.menuParentCode ?? '',
menuParentIcon: w.menuParentIcon ?? '',
menuIcon: w.menuIcon ?? '',
dataSourceCode: w.dataSourceCode ?? '',
dataSourceConnectionString: w.dataSourceConnectionString ?? '',
@ -415,6 +439,7 @@ const Wizard = () => {
widgets: w.widgets ?? [],
workflow: w.workflow ?? {
approvalUserFieldName: '',
isFilterUserName: false,
approvalDateFieldName: '',
approvalStatusFieldName: '',
approvalDescriptionFieldName: '',
@ -453,6 +478,7 @@ const Wizard = () => {
editorScript: it.editorScript ?? '',
colSpan: it.colSpan ?? 1,
isRequired: it.isRequired ?? false,
includeInEditingForm: it.includeInEditingForm ?? true,
turkishCaption: it.turkishCaption ?? it.dataField,
englishCaption: it.englishCaption ?? it.dataField,
captionName: it.captionName ?? `App.Listform.ListformField.${it.dataField}`,
@ -477,6 +503,7 @@ const Wizard = () => {
setWorkflow(
w.workflow ?? {
approvalUserFieldName: '',
isFilterUserName: false,
approvalDateFieldName: '',
approvalStatusFieldName: '',
approvalDescriptionFieldName: '',
@ -516,24 +543,38 @@ const Wizard = () => {
.trim()
const handleWizardNameChange = (name: string) => {
const formik = formikRef.current
const spacedLabel = toSpacedLabel(name)
const previousSpacedLabel = toSpacedLabel(formik?.values.wizardName ?? '')
const derived = deriveListFormCode(name)
formikRef.current?.setFieldValue('wizardName', name)
formikRef.current?.setFieldValue('listFormCode', derived)
formikRef.current?.setFieldValue('menuCode', derived)
formikRef.current?.setFieldValue('languageTextMenuEn', spacedLabel)
formikRef.current?.setFieldValue('languageTextMenuTr', spacedLabel)
formikRef.current?.setFieldValue('languageTextTitleEn', spacedLabel)
formikRef.current?.setFieldValue('languageTextTitleTr', spacedLabel)
formikRef.current?.setFieldValue('languageTextDescEn', spacedLabel)
formikRef.current?.setFieldValue('languageTextDescTr', spacedLabel)
const setAutoText = (field: keyof Pick<
ListFormWizardDto,
| 'languageTextMenuEn'
| 'languageTextMenuTr'
| 'languageTextTitleEn'
| 'languageTextTitleTr'
| 'languageTextDescEn'
| 'languageTextDescTr'
>) => {
const current = formik?.values[field]
if (!current || current === previousSpacedLabel) {
formik?.setFieldValue(field, spacedLabel)
}
}
formik?.setFieldValue('wizardName', name)
formik?.setFieldValue('listFormCode', derived)
formik?.setFieldValue('menuCode', derived)
setAutoText('languageTextMenuEn')
setAutoText('languageTextMenuTr')
setAutoText('languageTextTitleEn')
setAutoText('languageTextTitleTr')
setAutoText('languageTextDescEn')
setAutoText('languageTextDescTr')
}
const handleMenuParentChange = (code: string) => {
formikRef.current?.setFieldValue('menuParentCode', code)
if (!code) return
const rootCode = findRootCode(rawMenuItems, code)
const applyPermissionGroupFromRoot = (rootCode: string) => {
const rootItem = rawMenuItems.find((i) => i.code === rootCode)
// 1. Use group field if set
@ -565,6 +606,32 @@ const Wizard = () => {
formikRef.current?.setFieldValue('permissionGroupName', rootCode)
}
const handleMenuParentChange = (code: string) => {
formikRef.current?.setFieldValue('menuParentCode', code)
const selectedMenu = rawMenuItems.find((item) => item.code === code)
formikRef.current?.setFieldValue('menuParentIcon', selectedMenu?.icon ?? '')
if (!code) return
applyPermissionGroupFromRoot(findRootCode(rawMenuItems, code))
}
const handleMenuCreated = async (menu: {
code: string
parentCode?: string
menuTextEn: string
menuTextTr: string
icon?: string
}) => {
formikRef.current?.setFieldValue('menuParentCode', menu.code)
formikRef.current?.setFieldValue('menuParentIcon', menu.icon ?? '')
formikRef.current?.setFieldValue('languageTextMenuParentEn', menu.menuTextEn)
formikRef.current?.setFieldValue('languageTextMenuParentTr', menu.menuTextTr)
const rootCode = menu.parentCode ? findRootCode(rawMenuItems, menu.parentCode) : menu.code
applyPermissionGroupFromRoot(rootCode)
await getMenuList()
}
const handleNext = async () => {
if (!formikRef.current) return
const errors = await formikRef.current.validateForm()
@ -586,6 +653,19 @@ const Wizard = () => {
const handleBack = () => setCurrentStep(0)
const { getConfig } = useStoreActions((a) => a.abpConfig)
const preventEnterSubmit = (event: KeyboardEvent<HTMLFormElement>) => {
if (event.key !== 'Enter') return
const target = event.target as HTMLElement
const tagName = target.tagName.toLowerCase()
const isTextArea = tagName === 'textarea'
const isExplicitSubmit = target.getAttribute('type') === 'submit'
if (!isTextArea && !isExplicitSubmit) {
event.preventDefault()
}
}
const handleNext2 = async () => {
if (!formikRef.current) return
const errors = await formikRef.current.validateForm()
@ -627,6 +707,7 @@ const Wizard = () => {
editorScript: item.editorScript ?? '',
colSpan: item.colSpan,
isRequired: item.isRequired,
includeInEditingForm: item.includeInEditingForm,
dbSourceType: col ? sqlDataTypeToDbType(col.dataType) : 12,
turkishCaption: item.turkishCaption,
englishCaption: item.englishCaption,
@ -676,7 +757,10 @@ const Wizard = () => {
/>
<div className="mb-6 mt-2">
<Steps current={currentStep}>
<Steps
current={currentStep}
className="flex flex-row flex-wrap !justify-start gap-y-2 lg:flex-nowrap lg:!justify-between"
>
<Steps.Item title={translate('::ListForms.Wizard.MenuInfo') || 'Menu Info'} />
<Steps.Item
title={translate('::ListForms.Wizard.ListFormSettings') || 'List Form Settings'}
@ -701,40 +785,12 @@ const Wizard = () => {
innerRef={formikRef}
initialValues={{ ...initialValues }}
validationSchema={listFormValidationSchema}
onSubmit={async (values, { setSubmitting }) => {
setSubmitting(true)
try {
// 🔴 1. Kaydet (bekle)
await postListFormWizard({ ...values })
// 🔴 2. Config güncelle (bekle)
await getConfig(true)
// 🔴 3. Navigate
navigate(
ROUTES_ENUM.protected.admin.list.replace(':listFormCode', values.listFormCode),
{ replace: true },
)
// 🔴 4. Toast (istersen navigate öncesi de olabilir)
toast.push(
<Notification type="success" duration={2000}>
{translate('::ListForms.FormBilgileriKaydedildi')}
</Notification>,
{ placement: 'top-end' },
)
} catch (error: any) {
toast.push(<Notification title={error.message} type="danger" />, {
placement: 'top-end',
})
} finally {
setSubmitting(false)
}
onSubmit={(_, { setSubmitting }) => {
setSubmitting(false)
}}
>
{({ touched, errors, isSubmitting, values }) => (
<Form>
<Form onKeyDown={preventEnterSubmit}>
<FormContainer size={currentStep >= 2 ? undefined : 'sm'}>
{/* ─── Step 1: Basic Info ─────────────────────────────── */}
{currentStep === 0 && (
@ -748,7 +804,11 @@ const Wizard = () => {
menuTree={menuTree}
isLoadingMenu={isLoadingMenu}
onMenuParentChange={handleMenuParentChange}
onClearMenuParent={() => formikRef.current?.setFieldValue('menuParentCode', '')}
onClearMenuParent={() => {
formikRef.current?.setFieldValue('menuParentCode', '')
formikRef.current?.setFieldValue('menuParentIcon', '')
}}
onMenuCreated={handleMenuCreated}
onReloadMenu={getMenuList}
permissionGroupList={permissionGroupList}
isLoadingPermissionGroup={isLoadingPermissionGroup}
@ -770,6 +830,7 @@ const Wizard = () => {
onDataSourceNewChange={setIsDataSourceNew}
dbObjects={dbObjects}
isLoadingDbObjects={isLoadingDbObjects}
onDbObjectsRefresh={loadDbObjects}
selectCommandColumns={selectCommandColumns}
isLoadingColumns={isLoadingColumns}
selectedColumns={selectedColumns}
@ -779,7 +840,8 @@ const Wizard = () => {
setSelectedColumns(new Set())
}}
onToggleColumn={toggleColumn}
onToggleAllColumns={toggleAllColumns}
onToggleAllColumns={(all) => toggleAllColumns(all, values.isTenant)}
onTenantChange={handleTenantChange}
translate={translate}
onBack={handleBack}
onNext={handleNext2}

View file

@ -414,6 +414,14 @@ export interface WizardStep1Props {
isLoadingMenu: boolean
onMenuParentChange: (code: string) => void
onClearMenuParent: () => void
onMenuCreated: (menu: {
code: string
parentCode?: string
menuTextEn: string
menuTextTr: string
icon?: string
shortName?: string
}) => void | Promise<void>
onReloadMenu: () => void
permissionGroupList: SelectBoxOption[]
isLoadingPermissionGroup: boolean
@ -432,6 +440,7 @@ const WizardStep1 = ({
isLoadingMenu,
onMenuParentChange,
onClearMenuParent,
onMenuCreated,
onReloadMenu,
permissionGroupList,
isLoadingPermissionGroup,
@ -544,7 +553,7 @@ const WizardStep1 = ({
initialParentCode={menuDialogParentCode}
initialOrder={menuDialogInitialOrder}
rawItems={rawMenuItems}
onSaved={onReloadMenu}
onSaved={onMenuCreated}
/>
</div>

View file

@ -2,11 +2,13 @@ import { Button, Checkbox, FormItem, Input, Select } from '@/components/ui'
import { SelectCommandTypeEnum } from '@/proxy/form/models'
import type { DatabaseColumnDto, SqlObjectExplorerDto } from '@/proxy/sql-query-manager/models'
import { SelectBoxOption } from '@/types/shared'
import { Field, FieldProps, FormikErrors, FormikTouched } from 'formik'
import { Field, FieldProps, FormikErrors, FormikTouched, useFormikContext } from 'formik'
import { useState } from 'react'
import CreatableSelect from 'react-select/creatable'
import { FaArrowLeft, FaArrowRight } from 'react-icons/fa'
import { FaArrowLeft, FaArrowRight, FaPlus } from 'react-icons/fa'
import { dbSourceTypeOptions, listFormDefaultLayoutOptions, selectCommandTypeOptions, sqlDataTypeToDbType } from '../edit/options'
import { ListFormWizardDto } from '@/proxy/admin/wizard/models'
import SqlTableDesignerDialog from '@/views/developerKit/SqlTableDesignerDialog'
// ─── Props ────────────────────────────────────────────────────────────────────
@ -23,6 +25,7 @@ export interface WizardStep2Props {
// DB Objects
dbObjects: SqlObjectExplorerDto | null
isLoadingDbObjects: boolean
onDbObjectsRefresh: (dsCode: string) => void | Promise<void>
// Columns
selectCommandColumns: DatabaseColumnDto[]
isLoadingColumns: boolean
@ -31,6 +34,7 @@ export interface WizardStep2Props {
onClearColumns: () => void
onToggleColumn: (col: string) => void
onToggleAllColumns: (all: boolean) => void
onTenantChange: (isTenant: boolean) => void
// Navigation
translate: (key: string) => string
onBack: () => void
@ -50,6 +54,7 @@ const WizardStep2 = ({
onDataSourceNewChange,
dbObjects,
isLoadingDbObjects,
onDbObjectsRefresh,
selectCommandColumns,
isLoadingColumns,
selectedColumns,
@ -57,10 +62,23 @@ const WizardStep2 = ({
onClearColumns,
onToggleColumn,
onToggleAllColumns,
onTenantChange,
translate,
onBack,
onNext,
}: WizardStep2Props) => {
const [showTableDesignerDialog, setShowTableDesignerDialog] = useState(false)
const formik = useFormikContext<ListFormWizardDto>()
const handleTableDeployed = async (table: { schemaName: string; tableName: string }) => {
await onDbObjectsRefresh(values.dataSourceCode)
formik.setFieldValue('selectCommand', table.tableName)
formik.setFieldValue('selectCommandType', SelectCommandTypeEnum.Table)
formik.setFieldValue('keyFieldName', '')
formik.setFieldTouched('keyFieldName', false)
onLoadColumns(values.dataSourceCode, table.schemaName || 'dbo', table.tableName)
}
const step2Missing = [
!values.listFormCode && translate('::App.Listform.ListformField.ListFormCode'),
!values.dataSourceCode && translate('::ListForms.Wizard.Step4.DataSource'),
@ -217,7 +235,9 @@ const WizardStep2 = ({
]
: []
return (
<Select
<div className="flex items-center gap-2">
<div className="flex-1 min-w-0">
<Select
field={field}
form={form}
isClearable
@ -256,7 +276,18 @@ const WizardStep2 = ({
form.setFieldTouched('keyFieldName', false)
onClearColumns()
}}
/>
/>
</div>
<Button
type="button"
variant="solid"
icon={<FaPlus />}
disabled={!values.dataSourceCode}
onClick={() => setShowTableDesignerDialog(true)}
>
New Table
</Button>
</div>
)
}}
</Field>
@ -328,7 +359,18 @@ const WizardStep2 = ({
invalid={!!(errors.isTenant && touched.isTenant)}
errorMessage={errors.isTenant}
>
<Field type="checkbox" autoComplete="off" name="isTenant" component={Checkbox} />
<Field name="isTenant">
{({ field, form }: FieldProps<boolean>) => (
<Checkbox
name={field.name}
checked={Boolean(field.value)}
onChange={(checked) => {
form.setFieldValue(field.name, checked)
onTenantChange(checked)
}}
/>
)}
</Field>
</FormItem>
<FormItem
@ -700,6 +742,7 @@ const WizardStep2 = ({
<div className="flex items-center gap-2 ml-3">
<Button
variant="solid"
type="button"
onClick={() => onToggleAllColumns(true)}
className="text-xs px-2 py-0.5 rounded bg-indigo-500 text-white hover:bg-indigo-600"
>
@ -707,6 +750,7 @@ const WizardStep2 = ({
</Button>
<Button
variant="default"
type="button"
onClick={() => onToggleAllColumns(false)}
className="text-xs px-2 py-0.5 rounded border border-gray-300 dark:border-gray-600 text-gray-500 hover:text-red-500 hover:border-red-400"
>
@ -785,6 +829,14 @@ const WizardStep2 = ({
</div>
</div>
</div>
<SqlTableDesignerDialog
isOpen={showTableDesignerDialog}
onClose={() => setShowTableDesignerDialog(false)}
dataSource={values.dataSourceCode}
initialTableData={null}
onDeployed={handleTableDeployed}
/>
</div>
)
}

View file

@ -40,6 +40,7 @@ export interface WizardGroupItem {
editorScript: string
colSpan: number
isRequired: boolean
includeInEditingForm: boolean
turkishCaption?: string
englishCaption?: string
captionName?: string
@ -114,6 +115,7 @@ function newGroupItem(colName: string, meta?: DatabaseColumnDto): WizardGroupIte
editorScript: '',
colSpan: 1,
isRequired: meta?.isNullable === false,
includeInEditingForm: true,
turkishCaption: formatLabel(colName),
englishCaption: formatLabel(colName),
captionName: `App.Listform.ListformField.${colName}`,
@ -179,6 +181,7 @@ interface SortableItemProps {
onLookupQueryChange: (val: string) => void
onColSpanChange: (val: number) => void
onRequiredChange: (val: boolean) => void
onIncludeInEditingFormChange: (val: boolean) => void
onRemove: () => void
}
@ -194,6 +197,7 @@ function SortableItem({
onEditorScriptChange,
onColSpanChange,
onRequiredChange,
onIncludeInEditingFormChange,
onCaptionNameChange,
onLookupDataSourceTypeChange,
onDisplayExprChange,
@ -561,7 +565,7 @@ function SortableItem({
/>
</div>
{/* Bottom row: ColSpan + Required */}
{/* Bottom row: ColSpan + Editing Form + Required */}
<div className="flex items-center gap-2">
<div className="flex items-center gap-1">
<span className="text-[10px] text-gray-400">
@ -579,6 +583,20 @@ function SortableItem({
))}
</select>
</div>
<label
className="flex items-center gap-1 cursor-pointer ml-auto"
title={translate('::ListForms.Wizard.Step3.IncludeInEditingForm') || 'Editing form'}
>
<input
type="checkbox"
checked={item.includeInEditingForm}
onChange={(e) => onIncludeInEditingFormChange(e.target.checked)}
className="w-3 h-3 accent-indigo-500"
/>
<span className="text-[10px] text-gray-400">
{translate('::ListForms.Wizard.Step3.IncludeInEditingForm') || 'Editing form'}
</span>
</label>
<label className="flex items-center gap-1 cursor-pointer ml-auto" title="Required">
<input
type="checkbox"
@ -723,6 +741,9 @@ function GroupCard({
onEditorScriptChange={(val) => onItemChange(item.id, { editorScript: val })}
onColSpanChange={(val) => onItemChange(item.id, { colSpan: val })}
onRequiredChange={(val) => onItemChange(item.id, { isRequired: val })}
onIncludeInEditingFormChange={(val) =>
onItemChange(item.id, { includeInEditingForm: val })
}
onLookupDataSourceTypeChange={(val: UiLookupDataSourceTypeEnum) =>
onItemChange(item.id, { lookupDataSourceType: val })
}

View file

@ -462,10 +462,10 @@ function WizardStep4({
<p>Silmek istediğinize emin misiniz?</p>
</Dialog.Body>
<Dialog.Footer className="flex justify-end gap-2">
<Button variant="plain" onClick={() => setDeleteIndex(null)}>
<Button type="button" variant="plain" onClick={() => setDeleteIndex(null)}>
Cancel
</Button>
<Button variant="solid" onClick={removeSubForm}>
<Button type="button" variant="solid" onClick={removeSubForm}>
Delete
</Button>
</Dialog.Footer>

View file

@ -340,10 +340,10 @@ function WizardStep5({ widgets, translate, onChange, onBack, onNext }: Props) {
<p>Silmek istediğinize emin misiniz?</p>
</Dialog.Body>
<Dialog.Footer className="flex justify-end gap-2">
<Button variant="plain" onClick={() => setDeleteIndex(null)}>
<Button type="button" variant="plain" onClick={() => setDeleteIndex(null)}>
Cancel
</Button>
<Button variant="solid" onClick={removeWidget}>
<Button type="button" variant="solid" onClick={removeWidget}>
Delete
</Button>
</Dialog.Footer>

View file

@ -1,4 +1,4 @@
import { Button, Card, FormContainer, FormItem, Select } from '@/components/ui'
import { Button, Card, Checkbox, FormContainer, FormItem, Select } from '@/components/ui'
import type { DatabaseColumnDto } from '@/proxy/sql-query-manager/models'
import { ListFormWorkflowCriteriaDto, WorkflowDto } from '@/proxy/form/models'
import { getUsers } from '@/services/identity.service'
@ -9,6 +9,8 @@ import {
emptyCriteria,
normalizeCriteria,
toCriteriaForm,
uniqueCriteriaId,
uniqueCriteriaTitle,
type WorkflowCriteriaForm,
} from '@/utils/workflow/workflowHelpers'
import { Field, FieldProps, Form, Formik } from 'formik'
@ -16,6 +18,7 @@ import { useEffect, useMemo, useRef, useState } from 'react'
import type { FormEvent } from 'react'
import { FaArrowLeft, FaArrowRight } from 'react-icons/fa'
import { WorkflowDesigner } from '../workflow/WorkflowDesigner'
import { IdentityUserDto } from '@/proxy/admin/models'
type Props = {
listFormCode: string
@ -44,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,
@ -77,7 +78,7 @@ function WizardStep6({
useEffect(() => {
getUsers(0, 1000).then((response) => {
setUserList(
(response.data?.items ?? []).map((user: any) => ({
(response.data?.items ?? []).map((user: IdentityUserDto) => ({
value: user.userName,
label: `${user.userName} (${user.name} ${user.surname})`,
})),
@ -95,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))
@ -108,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
@ -192,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,
@ -208,6 +212,7 @@ function WizardStep6({
...normalizeCriteria(emptyCriteria('Approval', listFormCode)),
id: approvalId,
nodeId: approvalId,
title: uniqueCriteriaTitle('Approval', []),
nextOnApprove: endId,
positionX: 360,
positionY: 160,
@ -216,6 +221,7 @@ function WizardStep6({
...normalizeCriteria(emptyCriteria('End', listFormCode)),
id: endId,
nodeId: endId,
title: uniqueCriteriaTitle('End', []),
positionX: 650,
positionY: 160,
} as WorkflowCriteriaDto,
@ -234,7 +240,7 @@ function WizardStep6({
<Form>
<Card className="mb-4" header={translate('::ListForms.ListFormEdit.TabWorkflow')}>
<FormContainer>
<div className="grid grid-cols-4 gap-4">
<div className="grid grid-cols-1 md:grid-cols-5 gap-4">
{[
[
'approvalUserFieldName',
@ -277,6 +283,26 @@ function WizardStep6({
</Field>
</FormItem>
))}
<FormItem
label={translate('::ListForms.ListFormEdit.Workflow.IsFilterUserName')}
>
<Field name="isFilterUserName">
{({ field, form }: FieldProps<boolean>) => (
<Checkbox
name={field.name}
checked={Boolean(values.isFilterUserName)}
onChange={(checked) => {
form.setFieldValue(field.name, checked)
onWorkflowChange({
...values,
isFilterUserName: checked,
criteria,
})
}}
/>
)}
</Field>
</FormItem>
</div>
</FormContainer>
</Card>

View file

@ -226,6 +226,13 @@ const WizardStep7 = ({
}
const totalFields = groups.reduce((acc, g) => acc + g.items.length, 0)
const editingFormFields = groups.flatMap((g) =>
g.items
.filter((item) => item.includeInEditingForm && item.dataField !== values.keyFieldName)
.map((item) => ({ ...item, groupCaption: g.caption })),
)
const groupedFieldNames = new Set(groups.flatMap((g) => g.items.map((item) => item.dataField)))
const ungroupedSelectedColumns = [...selectedColumns].filter((col) => !groupedFieldNames.has(col))
const hasWorkflowFields = Boolean(
workflow.approvalUserFieldName ||
workflow.approvalDateFieldName ||
@ -298,91 +305,157 @@ const WizardStep7 = ({
label={translate('::App.Listform.ListformField.ConnectionString')}
value={values.dataSourceConnectionString}
/>
<Row
label={translate('::ListForms.Wizard.Step4.CommandType')}
value={
selectCommandTypeOptions.find((o) => o.value === values.selectCommandType)?.label ||
values.selectCommandType
}
/>
<Row
label={translate('::ListForms.Wizard.Step4.SelectCommand')}
value={values.selectCommand}
value={`${values.selectCommand} (${
selectCommandTypeOptions.find((o: any) => o.value === values.selectCommandType)
?.label ?? String(values.selectCommandType)
})`}
/>
<Row
label={translate('::ListForms.Wizard.Step4.KeyField')}
value={values.keyFieldName}
/>
<Row
label={translate('::ListForms.Wizard.Step4.KeyFieldType')}
value={
value={`${values.keyFieldName} (${
dbSourceTypeOptions.find((o: any) => o.value === values.keyFieldDbSourceType)
?.label ?? String(values.keyFieldDbSourceType)
}
})`}
/>
</Section>
</div>
<Section
title={translate('::ListForms.Wizard.Step4.SelectedColumns')}
badge={selectedColumns.size}
title={
translate('::ListForms.Wizard.Step4.ColumnsAndFormLayout') || 'Columns & Form Layout'
}
badge={`${selectedColumns.size} ${translate('::App.Listform.ListformField.Column')} / ${editingFormFields.length} ${translate('::ListForms.Wizard.Step4.EditingForm') || 'Popup Form'}`}
>
<div className="flex flex-wrap gap-1.5">
{[...selectedColumns].map((col) => {
const meta = selectCommandColumns.find((c) => c.columnName === col)
return (
<span
key={col}
className="inline-flex items-center gap-1 px-2 py-0.5 text-xs rounded-full bg-indigo-50 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-300 border border-indigo-200 dark:border-indigo-700"
>
{col}
{meta?.dataType && (
<span className="text-[10px] text-indigo-400 opacity-70">{meta.dataType}</span>
)}
</span>
)
})}
</div>
</Section>
<Section title={translate('::ListForms.Wizard.Step4.FormGroups')} badge={groups.length}>
<div className="flex flex-col gap-3">
{groups.map((g) => (
<Section
key={g.id}
title={g.caption || `(${translate('::ListForms.Wizard.Step4.StatGroup')})`}
badge={`${g.items.length} ${translate('::ListForms.Wizard.Step4.StatField')} · ${g.colCount} ${translate('::App.Listform.ListformField.Column')}`}
defaultOpen={false}
<div className="mb-3 grid grid-cols-3 gap-2">
{[
{
label: translate('::ListForms.Wizard.Step4.SelectedColumns'),
value: selectedColumns.size,
className: 'text-indigo-600 dark:text-indigo-400',
},
{
label: translate('::ListForms.Wizard.Step4.EditingForm') || 'Popup Form',
value: editingFormFields.length,
className: 'text-emerald-600 dark:text-emerald-400',
},
{
label: translate('::ListForms.Wizard.Step4.FormGroups'),
value: groups.length,
className: 'text-gray-700 dark:text-gray-200',
},
].map((item) => (
<div
key={item.label}
className="rounded-lg border border-gray-100 dark:border-gray-800 bg-gray-50 dark:bg-gray-800 px-3 py-2"
>
<div className="grid grid-cols-2 gap-2">
<div className={`text-lg font-semibold leading-none ${item.className}`}>
{item.value}
</div>
<div className="mt-1 text-[11px] text-gray-400 truncate">{item.label}</div>
</div>
))}
</div>
<div className="flex flex-col gap-2">
{groups.map((g) => (
<div
key={g.id}
className="overflow-hidden rounded-lg border border-gray-100 dark:border-gray-800"
>
<div className="flex items-center justify-between gap-3 bg-gray-50 dark:bg-gray-800 px-3 py-2">
<span className="text-xs font-semibold text-gray-700 dark:text-gray-200 truncate">
{g.caption || `(${translate('::ListForms.Wizard.Step4.StatGroup')})`}
</span>
<div className="flex items-center gap-1.5 text-[10px] text-gray-400 shrink-0">
<span className="rounded bg-white dark:bg-gray-900 px-1.5 py-0.5">
{g.items.length} {translate('::ListForms.Wizard.Step4.StatField')}
</span>
<span className="rounded bg-white dark:bg-gray-900 px-1.5 py-0.5">
{g.colCount} {translate('::App.Listform.ListformField.Column')}
</span>
</div>
</div>
<div className="divide-y divide-gray-100 dark:divide-gray-800">
{g.items.length === 0 ? (
<span className="text-xs text-gray-300 italic">
<span className="block px-3 py-2 text-xs text-gray-300 italic">
{translate('::ListForms.Wizard.Step4.NoFields') || 'Alan yok'}
</span>
) : (
g.items.map((item) => (
<div
key={item.id}
className="flex items-center gap-2 py-1 border-b border-gray-100 dark:border-gray-800 last:border-0"
>
<span className="text-xs font-medium text-indigo-600 dark:text-indigo-400 w-36 shrink-0 truncate">
{item.dataField}
</span>
<span className="text-[10px] text-gray-400 bg-gray-100 dark:bg-gray-800 px-1.5 py-0.5 rounded">
{item.editorType}
</span>
<span className="text-[10px] text-gray-400 mr-auto shrink-0 bg-gray-100 dark:bg-gray-800 px-1.5 py-0.5 rounded">
col-span-{item.colSpan}
{item.isRequired && (
<span className="ml-1 text-red-400 font-semibold">*</span>
)}
</span>
</div>
))
g.items.map((item) => {
const meta = selectCommandColumns.find((c) => c.columnName === item.dataField)
const isKeyField = item.dataField === values.keyFieldName
const isPopupField = item.includeInEditingForm && !isKeyField
return (
<div
key={item.id}
className="grid grid-cols-[minmax(150px,1fr)_auto] items-center gap-3 px-3 py-1.5"
>
<div className="flex min-w-0 items-center gap-2">
<span className="text-xs font-medium text-indigo-600 dark:text-indigo-400 truncate">
{item.dataField}
</span>
{meta?.dataType && (
<span className="text-[10px] text-gray-400 truncate">
{meta.dataType}
</span>
)}
</div>
<div className="flex flex-wrap justify-end gap-1">
<span className="rounded bg-indigo-50 dark:bg-indigo-900/30 px-1.5 py-0.5 text-[10px] text-indigo-600 dark:text-indigo-300">
{translate('::ListForms.Wizard.Step4.SelectedColumns') || 'List'}
</span>
{isPopupField && (
<span className="rounded bg-emerald-50 dark:bg-emerald-900/30 px-1.5 py-0.5 text-[10px] text-emerald-600 dark:text-emerald-300">
{translate('::ListForms.Wizard.Step4.EditingForm') || 'Popup Form'}
</span>
)}
{isKeyField && (
<span className="rounded bg-amber-50 dark:bg-amber-900/30 px-1.5 py-0.5 text-[10px] text-amber-600 dark:text-amber-300">
{translate('::ListForms.Wizard.Step4.KeyField')}
</span>
)}
<span className="rounded bg-gray-100 dark:bg-gray-800 px-1.5 py-0.5 text-[10px] text-gray-400">
{item.editorType}
</span>
<span className="rounded bg-gray-100 dark:bg-gray-800 px-1.5 py-0.5 text-[10px] text-gray-400">
span-{item.colSpan}
{item.isRequired && <span className="ml-1 text-red-400">*</span>}
</span>
</div>
</div>
)
})
)}
</div>
</Section>
</div>
))}
{ungroupedSelectedColumns.length > 0 && (
<div className="rounded-lg border border-amber-100 dark:border-amber-900/40 bg-amber-50/50 dark:bg-amber-900/10 px-3 py-2">
<div className="mb-1.5 text-xs font-semibold text-amber-700 dark:text-amber-300">
{translate('::ListForms.Wizard.Step4.UngroupedColumns') || 'Ungrouped columns'}
</div>
<div className="flex flex-wrap gap-1.5">
{ungroupedSelectedColumns.map((col) => {
const meta = selectCommandColumns.find((c) => c.columnName === col)
return (
<span
key={col}
className="inline-flex items-center gap-1 rounded-full border border-amber-200 dark:border-amber-800 bg-white dark:bg-gray-900 px-2 py-0.5 text-xs text-amber-700 dark:text-amber-300"
>
{col}
{meta?.dataType && (
<span className="text-[10px] text-amber-500 opacity-70">
{meta.dataType}
</span>
)}
</span>
)
})}
</div>
</div>
)}
</div>
</Section>
@ -460,6 +533,7 @@ const WizardStep7 = ({
<Row label="Approval Date" value={workflow.approvalDateFieldName} />
<Row label="Approval Status" value={workflow.approvalStatusFieldName} />
<Row label="Approval Description" value={workflow.approvalDescriptionFieldName} />
<Row label="Is Filter User Name?" value={workflow.isFilterUserName === true ? 'Yes' : 'No'} />
</div>
{workflowItems.length > 0 && (
<div className="flex flex-col gap-2">
@ -471,12 +545,18 @@ const WizardStep7 = ({
<div className="text-xs font-semibold text-gray-700 dark:text-gray-200">
{criteria.title || criteria.kind || `Criteria ${index + 1}`}
</div>
<div className="mt-1 text-[11px] text-gray-500 dark:text-gray-400">
{criteria.compareColumn} {criteria.compareOperator} {criteria.compareValue}
</div>
<div className="text-[11px] text-gray-500 dark:text-gray-400">
Approver: {criteria.approver}
</div>
{criteria.kind === 'Compare' && (
<div className="mt-1 text-[11px] text-gray-500 dark:text-gray-400">
{criteria.compareColumn} {criteria.compareOperator}{' '}
{criteria.compareValue}
</div>
)}
{(criteria.kind === 'Approval' || criteria.kind === 'Inform') &&
criteria.approver && (
<div className="text-[11px] text-gray-500 dark:text-gray-400">
Approver: {criteria.approver}
</div>
)}
</div>
))}
</div>
@ -489,10 +569,14 @@ const WizardStep7 = ({
{/* ── Right: Deploy ──────────────────────────────────────────── */}
<div className="sticky top-4 flex flex-col gap-3 max-h-[calc(100vh-200px)]">
{/* Stats */}
<div className="grid grid-cols-3 gap-2">
<div className="grid grid-cols-7 gap-2">
{[
{ label: translate('::ListForms.Wizard.Step4.StatGroup'), value: groups.length },
{ label: translate('::ListForms.Wizard.Step4.StatField'), value: totalFields },
{
label: translate('::ListForms.Wizard.Step4.EditingForm') || 'Popup Form',
value: editingFormFields.length,
},
{
label: translate('::App.Listform.ListformField.Column'),
value: selectedColumns.size,

View file

@ -257,17 +257,19 @@ export function WorkflowCriteria({
onChange={(event) => setField('title', event.target.value)}
/>
</FormItem>
<FormItem
label={translate('::App.Listform.ListformField.Approver')}
asterisk={formValues.kind === 'Approval' || formValues.kind === 'Inform'}
>
<SelectField
required={formValues.kind === 'Approval' || formValues.kind === 'Inform'}
options={userList}
value={formValues.approver}
onChange={(value) => setField('approver', value)}
/>
</FormItem>
{(formValues.kind === 'Approval' || formValues.kind === 'Inform') && (
<FormItem
label={translate('::App.Listform.ListformField.Approver')}
asterisk
>
<SelectField
required
options={userList}
value={formValues.approver}
onChange={(value) => setField('approver', value)}
/>
</FormItem>
)}
{(formValues.kind === 'Start' || formValues.kind === 'Inform') && (
<FormItem

View file

@ -26,11 +26,11 @@ import {
deleteClaimUser,
getUserDetail,
postClaimUser,
putUserAvatar,
putUserDetail,
putUserLookout,
putUserPermission,
} from '@/services/identity.service'
import { updateProfile } from '@/services/account.service'
import { CountryDto, getCountry } from '@/services/home.service'
import { useLocalization } from '@/utils/hooks/useLocalization'
import dayjs from 'dayjs'
@ -97,6 +97,7 @@ function UserDetails() {
const [confirmDeleteClaim, setConfirmDeleteClaim] = useState<AssignedClaimViewModel | null>(null)
const [countries, setCountries] = useState<CountryDto[]>([])
const [image, setImage] = useState<string | undefined>()
const [hasAvatarChange, setHasAvatarChange] = useState(false)
const cropperRef = useRef<CropperRef>(null)
const previewRef = useRef<CropperPreviewRef>(null)
const auth = useStoreState((state) => state.auth)
@ -111,15 +112,26 @@ function UserDetails() {
const isTwoFactorEnabled = setting('Abp.Account.TwoFactor.Enabled')
const getUser = async (syncAvatar = true) => {
const { data } = await getUserDetail(userId || '')
if (!userId) {
return
}
const { data } = await getUserDetail(userId)
setUserDetails(data)
if (syncAvatar) {
setImage(`${AVATAR_URL(data.id, data.tenantId)}?${dayjs().unix()}`)
setHasAvatarChange(false)
}
}
useEffect(() => {
setUserDetails(undefined)
setImage(undefined)
setHasAvatarChange(false)
getUser()
}, [userId])
useEffect(() => {
getCountry().then(({ data }) => setCountries(data))
}, [])
@ -131,8 +143,10 @@ function UserDetails() {
const onChooseImage = async (file: File[]) => {
if (file[0]) {
setImage(URL.createObjectURL(file[0]))
setHasAvatarChange(true)
} else {
setImage(undefined)
setHasAvatarChange(true)
}
}
@ -246,34 +260,49 @@ function UserDetails() {
<TabContent value="user">
<div className="px-4 py-6">
<Formik
enableReinitialize
initialValues={userDetails}
onSubmit={async (values, { resetForm, setSubmitting }) => {
setSubmitting(true)
await putUserDetail({ ...values })
let keepCurrentAvatar = false
const avatar = await getCroppedAvatar()
const resp = await updateProfile({
name: values.name ?? '',
surname: values.surname ?? '',
avatar: avatar ? new File([avatar], 'avatar') : undefined,
})
if (resp.status === 200) {
const avatarUrl =
AVATAR_URL(auth.user.id, auth.tenant?.tenantId) + `?${dayjs().unix()}`
if (values.id === auth.user.id) {
setUser({
...auth.user,
name: `${resp.data.name} ${resp.data.surname}`.trim(),
avatar: avatarUrl,
name: `${values.name ?? ''} ${values.surname ?? ''}`.trim(),
})
setImage(avatarUrl)
keepCurrentAvatar = true
} else {
toast.push(<Notification title={resp?.error?.message} type="danger" />, {
placement: 'top-end',
}
let keepCurrentAvatar = false
if (hasAvatarChange) {
const avatar = await getCroppedAvatar()
const resp = await putUserAvatar({
userId: values.id!,
avatar: avatar ? new File([avatar], 'avatar') : undefined,
})
if (resp.status === 200) {
const avatarUrl =
AVATAR_URL(values.id, values.tenantId) + `?${dayjs().unix()}`
if (values.id === auth.user.id) {
setUser({
...auth.user,
name: `${values.name ?? ''} ${values.surname ?? ''}`.trim(),
avatar: avatarUrl,
})
}
setImage(avatarUrl)
setHasAvatarChange(false)
keepCurrentAvatar = true
} else {
const errorMessage =
(resp as { error?: { message?: string } })?.error?.message || 'Hata'
toast.push(<Notification title={errorMessage} type="danger" />, {
placement: 'top-end',
})
}
}
toast.push(
@ -542,7 +571,10 @@ function UserDetails() {
<Button
type="button"
icon={<FaTrashAlt />}
onClick={() => setImage(undefined)}
onClick={() => {
setImage(undefined)
setHasAvatarChange(true)
}}
></Button>
</div>
{image && (
@ -574,6 +606,7 @@ function UserDetails() {
<TabContent value="permission">
<div className="px-4 py-6">
<Formik
enableReinitialize
initialValues={userDetails}
onSubmit={async (values, { setSubmitting }) => {
setSubmitting(true)
@ -685,6 +718,7 @@ function UserDetails() {
<TabContent value="work">
<div className="px-4 py-6">
<Formik
enableReinitialize
initialValues={userDetails}
onSubmit={async (values, { resetForm, setSubmitting }) => {
setSubmitting(true)
@ -866,6 +900,7 @@ function UserDetails() {
<TabContent value="identity">
<div className="px-4 py-6">
<Formik
enableReinitialize
initialValues={userDetails}
onSubmit={async (values, { resetForm, setSubmitting }) => {
setSubmitting(true)
@ -1222,6 +1257,7 @@ function UserDetails() {
<TabContent value="lockout">
<div className="px-4 py-6">
<Formik
enableReinitialize
initialValues={userDetails}
onSubmit={async (values, { setSubmitting }) => {
setSubmitting(true)

View file

@ -42,6 +42,7 @@ type SqlDataType =
| 'decimal'
| 'float'
| 'bit'
| 'datetime'
| 'datetime2'
| 'date'
| 'uniqueidentifier'
@ -68,7 +69,7 @@ interface TableDesignerDialogProps {
isOpen: boolean
onClose: () => void
dataSource: string | null
onDeployed?: () => void
onDeployed?: (table: { schemaName: string; tableName: string }) => void | Promise<void>
initialTableData?: { schemaName: string; tableName: string } | null
}
@ -98,6 +99,7 @@ const DATA_TYPES: { value: SqlDataType; label: string }[] = [
{ value: 'decimal', label: 'Decimal (decimal 18,4)' },
{ value: 'float', label: 'Float (float)' },
{ value: 'bit', label: 'Bool (bit)' },
{ value: 'datetime', label: 'DateTime (datetime)' },
{ value: 'datetime2', label: 'DateTime (datetime2)' },
{ value: 'date', label: 'Date (date)' },
{ value: 'uniqueidentifier', label: 'Guid (uniqueidentifier)' },
@ -226,6 +228,45 @@ const TENANT_COLUMN: ColumnDefinition = {
description: 'Tenant ID for multi-tenancy',
}
const WORKFLOW_COLUMNS: ColumnDefinition[] = [
{
id: '__ApprovalUserId',
columnName: 'ApprovalUserName',
dataType: 'nvarchar',
maxLength: '256',
isNullable: true,
defaultValue: '',
description: 'Workflow approval user name',
},
{
id: '__ApprovalStatus',
columnName: 'ApprovalStatus',
dataType: 'nvarchar',
maxLength: '50',
isNullable: true,
defaultValue: '',
description: 'Workflow approval status',
},
{
id: '__ApprovalDate',
columnName: 'ApprovalDate',
dataType: 'datetime',
maxLength: '',
isNullable: true,
defaultValue: '',
description: 'Workflow approval date',
},
{
id: '__ApprovalDescription',
columnName: 'ApprovalDescription',
dataType: 'nvarchar',
maxLength: '200',
isNullable: true,
defaultValue: '',
description: 'Workflow approval description',
},
]
const CREATE_TABLE_SCRIPT_STORAGE_KEY = 'sqlQueryManager.lastCreateTableScript'
// ─── T-SQL Generator ──────────────────────────────────────────────────────────
@ -405,6 +446,8 @@ function dbColToColumnDef(col: {
dataType = 'float'
} else if (dt === 'bit') {
dataType = 'bit'
} else if (dt === 'datetime') {
dataType = 'datetime'
} else if (dt.startsWith('datetime') || dt === 'smalldatetime') {
dataType = 'datetime2'
} else if (dt === 'date') {
@ -476,6 +519,7 @@ function mapSqlTypeToDesigner(dataTypeRaw: string, lengthRaw: string): {
if (dt === 'decimal' || dt === 'numeric') return { dataType: 'decimal', maxLength: '' }
if (dt === 'float' || dt === 'real') return { dataType: 'float', maxLength: '' }
if (dt === 'bit') return { dataType: 'bit', maxLength: '' }
if (dt === 'datetime') return { dataType: 'datetime', maxLength: '' }
if (dt.startsWith('datetime') || dt === 'smalldatetime' || dt === 'time') {
return { dataType: 'datetime2', maxLength: '' }
}
@ -1224,6 +1268,18 @@ const SqlTableDesignerDialog = ({
}
}
const addWorkflowColumns = () => {
const existingNames = new Set(columns.map((c) => c.columnName.trim().toLowerCase()))
const toAdd = WORKFLOW_COLUMNS.filter((c) => !existingNames.has(c.columnName.toLowerCase()))
if (toAdd.length > 0) {
setColumns((prev) => {
const nonEmpty = prev.filter((c) => c.columnName.trim() !== '')
return [...nonEmpty, ...toAdd.map((c) => ({ ...c })), createEmptyColumn()]
})
}
}
const importColumnsFromRememberedCreateTable = async () => {
let script = ''
@ -1591,7 +1647,10 @@ const SqlTableDesignerDialog = ({
} catch {
// Non-blocking: seed file save failure does not affect deploy success
}
onDeployed?.()
await onDeployed?.({
schemaName: initialTableData?.schemaName || 'dbo',
tableName: deployedTable,
})
handleClose()
} else {
toast.push(
@ -1690,6 +1749,9 @@ const SqlTableDesignerDialog = ({
<Button size="xs" variant="solid" color="green-600" onClick={addMultiTenantColumns}>
{translate('::App.SqlQueryManager.AddMultiTenantColumns')}
</Button>
<Button size="xs" variant="solid" color="indigo-600" onClick={addWorkflowColumns}>
{translate('::App.SqlQueryManager.AddWorkflowColumns')}
</Button>
<Button
size="xs"
variant="solid"
@ -1943,7 +2005,14 @@ const SqlTableDesignerDialog = ({
initialParentCode={selectedMenuCode}
initialOrder={999}
rawItems={rawMenuItems}
onSaved={() => reloadMenus()}
onSaved={(menu) =>
reloadMenus((items) => {
const savedMenu = items.find((item) => item.code === menu.code)
if (savedMenu?.code && savedMenu.shortName) {
onMenuCodeSelect(savedMenu.code)
}
})
}
/>
</div>
@ -2671,7 +2740,7 @@ const SqlTableDesignerDialog = ({
// ── Render ─────────────────────────────────────────────────────────────────
return (
<Dialog isOpen={isOpen} onClose={handleClose} onRequestClose={handleClose} width={900}>
<Dialog isOpen={isOpen} onClose={handleClose} onRequestClose={handleClose} width={1100}>
<Dialog.Body className="flex flex-col gap-2">
{/* Header */}
<div className="flex items-center gap-3 border-b pb-3 flex-shrink-0">

View file

@ -17,16 +17,17 @@ import TabNav from '@/components/ui/Tabs/TabNav'
import { NoteDto } from '@/proxy/note/models'
import { AVATAR_URL } from '@/constants/app.constant'
import { useStoreState } from '@/store/store'
import apiService from '@/services/api.service'
import { PagedResultDto } from '@/proxy'
import { AuditLogActionDto, AuditLogDto } from '@/proxy/auditLog/audit-log'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { auditLogService } from '@/services/auditLog.service'
interface NoteListProps {
notes: NoteDto[]
entityName: string
entityId: string
isVisible?: boolean
onAddNote?: () => void
onRefreshNotes?: () => void
onDeleteNote?: (noteId: string) => void
onDownloadFile?: (fileData: any) => void
}
@ -35,7 +36,9 @@ export const NoteList: React.FC<NoteListProps> = ({
notes,
entityName,
entityId,
isVisible = true,
onAddNote,
onRefreshNotes,
onDeleteNote,
onDownloadFile,
}) => {
@ -70,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" />,
@ -215,7 +223,18 @@ export const NoteList: React.FC<NoteListProps> = ({
const getRowLabelIfMatches = (input: any): string | null => {
if (!entityIdNormalized) return null
const inputFormCode = normalize(input?.listFormCode)
if (!inputFormCode || inputFormCode !== listFormCodeNormalized) return null
if (!inputFormCode) return null
const data = input?.data
const isMainListForm = inputFormCode === listFormCodeNormalized
if (!isMainListForm) {
if (!data || typeof data !== 'object') return null
const hit = findMatchingValueInData(data, entityIdNormalized)
if (!hit) return null
const nameValue = (data as any)?.Name ?? (data as any)?.name
return nameValue ? String(nameValue) : String(hit.value)
}
const keys = getKeysFromInput(input)
.map((k) => normalize(k))
@ -227,7 +246,6 @@ export const NoteList: React.FC<NoteListProps> = ({
}
// Some entities may use a different PK than the visible row key; allow strict match via input.data too.
const data = input?.data
if (data && typeof data === 'object') {
const hit = findMatchingValueInData(data, entityIdNormalized)
if (hit) {
@ -240,7 +258,6 @@ export const NoteList: React.FC<NoteListProps> = ({
}
// insert: keys is null, match by scanning input.data for entity id/name/code/etc.
const data = input?.data
if (data && typeof data === 'object') {
const hit = findMatchingValueInData(data, entityIdNormalized)
if (!hit) return null
@ -277,17 +294,15 @@ export const NoteList: React.FC<NoteListProps> = ({
setAuditLoading(true)
setAuditError(null)
try {
const response = await apiService.fetchData<PagedResultDto<AuditLogDto>>({
method: 'GET',
url: '/api/app/audit-log',
params: {
skipCount: 0,
maxResultCount: 200,
sorting: 'ExecutionTime DESC',
},
const response = await auditLogService.getList({
skipCount: 0,
maxResultCount: 200,
sorting: 'ExecutionTime DESC',
listFormCode: entityName,
entityId,
})
const items = response.data?.items ?? []
const items = response?.items ?? []
const filtered = items
.map((log) => ({ log, matchedActions: buildMatchedActions(log) }))
.filter((x) => x.matchedActions.length > 0)
@ -305,13 +320,13 @@ export const NoteList: React.FC<NoteListProps> = ({
}
useEffect(() => {
if (currentTab !== 'audit') return
if (!isVisible) return
if (!listFormCodeNormalized && !entityIdNormalized) return
const key = `${listFormCodeNormalized}|${entityIdNormalized}`
if (auditLoadedKey === key) return
loadAuditLogs()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentTab, listFormCodeNormalized, entityIdNormalized])
}, [isVisible, listFormCodeNormalized, entityIdNormalized])
const getStatusBadge = (statusCode?: number) => {
if (!statusCode) return <Badge className="bg-gray-500" content="?" />
@ -330,7 +345,7 @@ export const NoteList: React.FC<NoteListProps> = ({
onChange={(val) => setCurrentTab(val as 'notes' | 'audit')}
variant="underline"
>
<TabList className="mb-4 border-0 dark:bg-gray-800">
<TabList className="mb-2 border-0 dark:bg-gray-800">
<TabNav value="notes">
{translate('::ListForms.ListForm.Notes')}
<Badge className="ml-2 bg-blue-500" content={`${notes?.length ?? 0}`} />
@ -341,21 +356,48 @@ export const NoteList: React.FC<NoteListProps> = ({
</TabNav>
</TabList>
<TabContent value="notes">
<div className="flex items-center justify-end mb-2">
<div className="mb-2 flex min-h-10 items-center justify-end border-y border-gray-200 bg-gray-50 px-1 py-1 dark:border-gray-700 dark:bg-gray-900">
{currentTab === 'notes' ? (
<div className="flex items-center gap-2">
<Button
variant="default"
size="xs"
icon={<FaPlus />}
type="button"
onClick={onAddNote}
disabled={!onAddNote}
className="flex items-center"
>
{translate('::ListForms.ListForm.AddNote')}
</Button>
<Button
size="xs"
variant="default"
type="button"
icon={<FaSyncAlt />}
onClick={onRefreshNotes}
disabled={!onRefreshNotes}
className="flex items-center"
>
{translate('::ListForms.ListForm.Refresh')}
</Button>
</div>
) : (
<Button
size="xs"
variant="default"
size="sm"
icon={<FaPlus className="mr-1" />}
type="button"
onClick={onAddNote}
disabled={!onAddNote}
icon={<FaSyncAlt />}
onClick={loadAuditLogs}
disabled={auditLoading}
className="flex items-center"
>
{translate('::ListForms.ListForm.AddNote')}
{translate('::ListForms.ListForm.Refresh')}
</Button>
</div>
)}
</div>
<TabContent value="notes">
{(notes?.length ?? 0) === 0 ? (
<div className="flex flex-col items-center justify-center h-32 text-gray-500">
<FaStickyNote className="text-4xl mb-2 opacity-50" />
@ -398,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"
@ -449,20 +491,6 @@ export const NoteList: React.FC<NoteListProps> = ({
</TabContent>
<TabContent value="audit">
<div className="flex items-center justify-end mb-2">
<Button
size="sm"
variant="default"
type="button"
icon={<FaSyncAlt className="mr-1" />}
onClick={loadAuditLogs}
disabled={auditLoading}
className="flex items-center"
>
{translate('::ListForms.ListForm.Refresh')}
</Button>
</div>
{auditLoading ? (
<div className="flex items-center justify-center py-10">
<Spinner size={32} />

View file

@ -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) => {
@ -92,7 +91,7 @@ function NoteModalContent({
{/* Başlık */}
<div className="flex items-center justify-between mb-5 flex-shrink-0">
<h3 className="text-xl font-semibold flex items-center gap-3">
<div className="p-2 bg-purple-100 rounded-full">
<div className="p-1 bg-purple-100 rounded-full">
<FaPlus className="text-purple-600 text-lg" />
</div>
{translate('::ListForms.ListForm.AddNote')}
@ -116,7 +115,7 @@ function NoteModalContent({
{types.map((t) => (
<label
key={t.value}
className="flex items-center gap-2 px-2 py-1 text-black rounded-md cursor-pointer transition-all duration-200 dark:bg-gray-800 dark:text-gray-300"
className="flex items-center gap-2 px-1 py-1 text-black rounded-md cursor-pointer transition-all duration-200 dark:bg-gray-800 dark:text-gray-300"
>
<Radio
value={t.value}
@ -139,8 +138,9 @@ function NoteModalContent({
<Field
type="text"
name="subject"
as={Input}
placeholder={translate('::ListForms.ListForm.NoteModal.Subject')}
component={Input}
autoFocus
/>
</FormItem>
@ -198,7 +198,7 @@ function NoteModalContent({
{/* DOSYA YÜKLEME */}
<FormItem>
<div className="border-2 border-dashed border-gray-300 rounded-lg p-3 text-center hover:border-purple-400 transition-colors duration-200">
<div className="border-2 border-dashed border-gray-300 rounded-lg p-2 text-center hover:border-purple-400 transition-colors duration-200">
<Upload
className="cursor-pointer"
showList={false}
@ -255,7 +255,7 @@ function NoteModalContent({
</FormContainer>
{/* ALT BUTONLAR */}
<Dialog.Footer className="mt-5 flex justify-between items-center pt-4 border-t border-gray-200">
<Dialog.Footer className="mt-2 flex justify-between items-center pt-4 border-t border-gray-200">
<Button variant="default" size="md" onClick={onClose} disabled={uploading}>
{translate('::Cancel')}
</Button>

View file

@ -41,7 +41,7 @@ export const NotePanel: React.FC<NotePanelProps> = ({
useEffect(() => {
if (isVisible) fetchActivities()
}, [isVisible])
}, [isVisible, entityName, entityId])
const handleDownloadFile = async (fileData: any) => {
if (!fileData?.SavedFileName) return
@ -62,8 +62,6 @@ export const NotePanel: React.FC<NotePanelProps> = ({
}
}
const getTotalCount = () => activities.length
// Draggable button handlers
const handleMouseDown = (e: React.MouseEvent) => {
if (!buttonRef.current) return
@ -134,7 +132,6 @@ export const NotePanel: React.FC<NotePanelProps> = ({
>
<div className="flex items-center gap-2">
{isVisible ? <FaChevronRight /> : <FaChevronLeft />}
{getTotalCount() > 0 && <Badge content={getTotalCount()} />}
</div>
</Button>
</div>
@ -187,7 +184,9 @@ export const NotePanel: React.FC<NotePanelProps> = ({
notes={activities}
entityName={entityName}
entityId={entityId}
isVisible={isVisible}
onAddNote={() => setShowAddModal(true)}
onRefreshNotes={fetchActivities}
onDeleteNote={handleDeleteActivity}
onDownloadFile={handleDownloadFile}
/>

View file

@ -375,7 +375,7 @@ const IntranetDashboard: React.FC = () => {
}
`}
style={{
touchAction: 'none',
touchAction: isDesignMode ? 'none' : 'pan-y',
transition:
'border-color 0.3s ease-out, opacity 0.3s ease-out, box-shadow 0.3s ease-out',
willChange: isDragging ? 'opacity' : 'auto',

View file

@ -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
@ -244,7 +245,7 @@ const Grid = (props: GridProps) => {
const { listFormCode, searchParams, isSubForm, level, gridDto: extGridDto } = props
const { translate } = useLocalization()
const { smaller } = useResponsive()
const config = useStoreState((state) => state.abpConfig.config)
const currentUser = useStoreState((state) => state.auth.user)
const gridRef = useRef<DataGridRef>()
const refListFormCode = useRef('')
@ -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 () => {
@ -374,10 +397,10 @@ const Grid = (props: GridProps) => {
grd,
gridDto?.gridOptions.workflowDto,
selectedRowsData ?? grd.getSelectedRowsData(),
config?.currentUser,
currentUser,
)
},
[config?.currentUser, gridDto],
[currentUser, gridDto],
)
const refreshData = useCallback(() => {
@ -997,7 +1020,7 @@ const Grid = (props: GridProps) => {
// Kolonları oluştur - dil değiştiğinde güncelle
const memoizedColumns = useMemo(() => {
if (!gridDto || !config) return undefined
if (!gridDto) return undefined
const cols = getBandedColumns()
@ -1038,7 +1061,7 @@ const Grid = (props: GridProps) => {
})
return cols
}, [gridDto, config])
}, [gridDto])
useEffect(() => {
setColumnData(memoizedColumns)
@ -1451,7 +1474,27 @@ const Grid = (props: GridProps) => {
) {
workflowService
.startWorkflow(listFormCode, [insertedKey])
.then(() => gridRef.current?.instance()?.refresh())
.then((result) => {
const messages = result.toastMessages ?? []
if (messages.length > 0) {
toast.push(
<Notification type="info" duration={7000}>
{messages.map((message, messageIndex) => (
<div
key={messageIndex}
className={messageIndex > 0 ? 'mt-2 border-t pt-2' : undefined}
>
{message.split('\n').map((line, lineIndex) => (
<div key={lineIndex}>{line}</div>
))}
</div>
))}
</Notification>,
{ placement: 'top-end' },
)
}
gridRef.current?.instance()?.refresh()
})
.catch(console.error)
}
props.refreshData?.()
@ -1874,6 +1917,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>
</>
)

View file

@ -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
@ -232,6 +233,7 @@ const Tree = (props: TreeProps) => {
const { listFormCode, searchParams, isSubForm, level, gridDto: extGridDto } = props
const { translate } = useLocalization()
const { smaller } = useResponsive()
const currentUser = useStoreState((state) => state.auth.user)
const gridRef = useRef<TreeListRef>()
const refListFormCode = useRef('')
@ -249,9 +251,12 @@ 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[]>([])
const config = useStoreState((state) => state.abpConfig.config)
type EditorOptionsWithButtons = {
buttons?: 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][] => {
@ -409,10 +432,10 @@ const Tree = (props: TreeProps) => {
tree,
gridDto?.gridOptions.workflowDto,
selectedRowsData ?? tree.getSelectedRowsData(),
config?.currentUser,
currentUser,
)
},
[config?.currentUser, gridDto],
[currentUser, gridDto],
)
const refreshData = useCallback(() => {
@ -900,7 +923,7 @@ const Tree = (props: TreeProps) => {
}, [gridDto])
useEffect(() => {
if (!gridDto || !config) return
if (!gridDto) return
const cols = getBandedColumns()
setColumnData(cols)
@ -913,7 +936,7 @@ const Tree = (props: TreeProps) => {
cols,
)
setTreeListDataSource(dataSource)
}, [gridDto, searchParams, config])
}, [gridDto, searchParams])
useEffect(() => {
const activeFilters = extraFilters.filter((f) => f.value)
@ -1110,7 +1133,27 @@ const Tree = (props: TreeProps) => {
) {
workflowService
.startWorkflow(listFormCode, [insertedKey])
.then(() => gridRef.current?.instance()?.refresh())
.then((result) => {
const messages = result.toastMessages ?? []
if (messages.length > 0) {
toast.push(
<Notification type="info" duration={7000}>
{messages.map((message, messageIndex) => (
<div
key={messageIndex}
className={messageIndex > 0 ? 'mt-2 border-t pt-2' : undefined}
>
{message.split('\n').map((line, lineIndex) => (
<div key={lineIndex}>{line}</div>
))}
</div>
))}
</Notification>,
{ placement: 'top-end' },
)
}
gridRef.current?.instance()?.refresh()
})
.catch(console.error)
}
props.refreshData?.()
@ -1613,6 +1656,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>
</>
)

View file

@ -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[]) => {

View file

@ -11,12 +11,35 @@ import { usePWA } from '@/utils/hooks/usePWA'
import { layoutTypes, ListViewLayoutType } from '../admin/listForm/edit/types'
import { useStoreState } from '@/store'
import { workflowService } from '@/services/workflow.service'
import type { WorkflowRunResultDto } from '@/services/workflow.service'
type ToolbarModalData = {
open: boolean
content?: JSX.Element
}
const showWorkflowToastMessages = (results: WorkflowRunResultDto | WorkflowRunResultDto[]) => {
const list = Array.isArray(results) ? results : [results]
const messages = list.flatMap((result) => result.toastMessages ?? [])
if (!messages.length) {
return
}
toast.push(
<Notification type="info" duration={7000}>
{messages.map((message, messageIndex) => (
<div key={messageIndex} className={messageIndex > 0 ? 'mt-2 border-t pt-2' : undefined}>
{message.split('\n').map((line, lineIndex) => (
<div key={lineIndex}>{line}</div>
))}
</div>
))}
</Notification>,
{ placement: 'top-end' },
)
}
// https://js.devexpress.com/Documentation/ApiReference/UI_Components/dxDataGrid/Configuration/toolbar/
// item.name > Accepted Values: 'addRowButton', 'applyFilterButton', 'columnChooserButton', 'exportButton', 'groupPanel', 'revertButton', 'saveButton', 'searchPanel'
const useToolbar = ({
@ -32,7 +55,7 @@ const useToolbar = ({
}: {
gridDto?: GridDto
listFormCode: string
getSelectedRowKeys: () => void
getSelectedRowKeys: () => unknown[] | Promise<unknown[]>
getSelectedRowsData: () => any
refreshData: () => void
getFilter: () => void
@ -48,7 +71,7 @@ const useToolbar = ({
const { translate } = useLocalization()
const { checkPermission } = usePermission()
const isPwaMode = usePWA()
const config = useStoreState((state) => state.abpConfig.config)
const currentUser = useStoreState((state) => state.auth.user)
const [toolbarData, setToolbarData] = useState<ToolbarItem[]>([])
const [toolbarModalData, setToolbarModalData] = useState<ToolbarModalData>()
@ -113,7 +136,8 @@ const useToolbar = ({
})
const workflowOptions = grdOpt.workflowDto
const approvalCriteria = workflowOptions?.criteria?.filter((item) => item.kind === 'Approval') ?? []
const approvalCriteria =
workflowOptions?.criteria?.filter((item) => item.kind === 'Approval') ?? []
if (
workflowOptions?.approvalStatusFieldName &&
approvalCriteria.length > 0 &&
@ -149,7 +173,7 @@ const useToolbar = ({
) {
toast.push(
<Notification type="warning" duration={2500}>
Secili kayit icin workflow zaten baslamis.
{translate('::WorkflowAlreadyStarted')}
</Notification>,
{ placement: 'top-end' },
)
@ -157,7 +181,8 @@ const useToolbar = ({
}
try {
await workflowService.startWorkflow(listFormCode, keys)
const result = await workflowService.startWorkflow(listFormCode, keys)
showWorkflowToastMessages(result)
refreshData()
} catch (error: any) {
toast.push(
@ -204,14 +229,14 @@ const useToolbar = ({
row,
workflowOptions,
criteria.title,
getCurrentUserWorkflowIdentities(config?.currentUser),
getCurrentUserWorkflowIdentities(currentUser),
),
)
if (activeRows.length !== selectedRows.length) {
toast.push(
<Notification type="warning" duration={2500}>
Secili kayit bu onay adiminda veya onay kullanicisinda beklemiyor.
{translate('::SeciliKayitBekliyor')}
</Notification>,
{ placement: 'top-end' },
)
@ -302,16 +327,75 @@ const useToolbar = ({
text: translate('::ListForms.ListForm.DeleteSelectedRecords'),
icon: 'trash',
visible: false,
onClick() {
async onClick() {
if (!grdOpt.deleteServiceAddress) {
return
}
dynamicFetch(grdOpt.deleteServiceAddress, 'POST', null, {
keys: getSelectedRowKeys(),
listFormCode,
}).then(() => {
refreshData()
const selectedKeys = await Promise.resolve(getSelectedRowKeys())
const keys = Array.isArray(selectedKeys) ? [...selectedKeys] : []
if (!keys.length) {
toast.push(
<Notification type="warning" duration={2000}>
{translate('::ListForms.ListForm.SelectRecord')}
</Notification>,
{ placement: 'top-end' },
)
return
}
setToolbarModalData({
open: true,
content: (
<>
<h5 className="mb-4">
{translate('::ListForms.ListForm.DeleteSelectedRecords')}
</h5>
<p>
{translate('::SeciliKayitlarSilmekIstiyormusunuz', {
0: keys.length,
})}
</p>
<div className="text-right mt-6">
<Button
className="ltr:mr-2 rtl:ml-2"
variant="plain"
onClick={() => setToolbarModalData(undefined)}
>
{translate('::Cancel')}
</Button>
<Button
variant="solid"
onClick={() => {
dynamicFetch(grdOpt.deleteServiceAddress!, 'POST', null, {
keys,
listFormCode,
})
.then(() => {
refreshData()
setToolbarModalData(undefined)
})
.catch((error: any) => {
toast.push(
<Notification type="danger" duration={3000}>
{error?.response?.data?.error?.message ||
error?.response?.data?.message ||
error?.message ||
translate('::SilmeIslemiBasarisiz')}
</Notification>,
{ placement: 'top-end' },
)
})
}}
>
{translate('::Delete')}
</Button>
</div>
</>
),
})
},
},
@ -342,9 +426,13 @@ const useToolbar = ({
open: true,
content: (
<>
<h5 className="mb-4">Delete All Records</h5>
<h5 className="mb-4">{translate('::ListForms.ListForm.DeleteAllRecords')}</h5>
<p>Are you sure to delete all {r.data.totalCount} records?</p>
<p>
{translate('::TumKayitlariSilmekIstiyormusunuz', {
0: r.data.totalCount,
})}
</p>
<div className="text-right mt-6">
<Button
@ -362,7 +450,7 @@ const useToolbar = ({
dynamicFetch('list-form-select/select', 'GET', parameters).then(() => {
toast.push(
<Notification type="success" duration={2000}>
{'Tüm kayıtlar silindi.'}
{translate('::TumKayitlarSilindi')}
</Notification>,
{
placement: 'top-end',
@ -483,10 +571,9 @@ const useToolbar = ({
useEffect(() => {
if (!gridDto && !listFormCode) return
if (!config) return
getToolbarData()
}, [gridDto, listFormCode, config])
}, [gridDto, listFormCode, currentUser])
return {
toolbarData,
@ -522,7 +609,9 @@ function isWorkflowApprovalCriteriaActive(
}
function normalizeWorkflowValue(value: unknown) {
return String(value ?? '').trim().toLocaleLowerCase('tr-TR')
return String(value ?? '')
.trim()
.toLocaleLowerCase('tr-TR')
}
function isWorkflowNotStarted(row: Record<string, unknown>, workflowOptions: WorkflowDto) {
@ -547,7 +636,8 @@ export function updateWorkflowApprovalToolbarItems(
name?: string
},
) {
const approvalCriteria = workflowOptions?.criteria?.filter((item) => item.kind === 'Approval') ?? []
const approvalCriteria =
workflowOptions?.criteria?.filter((item) => item.kind === 'Approval') ?? []
if (!component || !workflowOptions?.approvalStatusFieldName || !approvalCriteria.length) {
return
}
@ -586,7 +676,12 @@ export function updateWorkflowApprovalToolbarItems(
const enabled =
selectedRowsData.length > 0 &&
selectedRowsData.every((row) =>
isWorkflowApprovalCriteriaActive(row, workflowOptions, criteria.title, currentUserIdentities),
isWorkflowApprovalCriteriaActive(
row,
workflowOptions,
criteria.title,
currentUserIdentities,
),
)
const optionPath = `toolbar.items[${toolbarItemIndex}].options.disabled`
@ -619,17 +714,12 @@ function WorkflowApprovalDecisionDialog({
const decide = async (approved: boolean) => {
setSubmitting(true)
try {
await Promise.all(
const results = await Promise.all(
keys.map((key) =>
workflowService.decideWorkflow(
listFormCode,
[key],
approved,
note,
criteriaId,
),
workflowService.decideWorkflow(listFormCode, [key], approved, note, criteriaId),
),
)
showWorkflowToastMessages(results)
onCompleted()
} catch (error: any) {
toast.push(
@ -649,24 +739,33 @@ function WorkflowApprovalDecisionDialog({
return (
<>
<h5 className="mb-4">{criteriaTitle}</h5>
<p>{keys.length} kayit icin workflow karari verilecek.</p>
<label className="mb-2 block font-semibold">Not</label>
<p className="mb-4">
{translate('::App.Listform.ListformField.WorkflowDecisionMessage', {
0: keys.length,
})}
</p>
<textarea
className="input input-textarea mb-4 min-h-[96px] w-full resize-y"
rows={4}
value={note}
placeholder="Onay veya red aciklamasi"
autoFocus
placeholder={translate('::App.Listform.ListformField.ApprovalComment')}
onChange={(event) => setNote(event.target.value)}
/>
<div className="text-right mt-6">
<Button className="ltr:mr-2 rtl:ml-2" variant="plain" disabled={submitting} onClick={onCancel}>
<Button
className="ltr:mr-2 rtl:ml-2"
variant="plain"
disabled={submitting}
onClick={onCancel}
>
{translate('::Cancel')}
</Button>
<Button className="ltr:mr-2 rtl:ml-2" disabled={submitting} onClick={() => decide(false)}>
Reddet
{translate('::App.Listform.ListformField.Rejecter')}
</Button>
<Button variant="solid" disabled={submitting} onClick={() => decide(true)}>
Onayla
{translate('::App.Listform.ListformField.Approver')}
</Button>
</div>
</>

View file

@ -128,7 +128,14 @@ export interface MenuAddDialogProps {
initialParentCode: string
initialOrder: number
rawItems: (MenuItem & { id?: string })[]
onSaved: () => void
onSaved: (menu: {
code: string
parentCode?: string
menuTextEn: string
menuTextTr: string
icon?: string
shortName?: string
}) => void | Promise<void>
}
export function MenuAddDialog({
@ -172,7 +179,7 @@ export function MenuAddDialog({
if (shortNameRequired && !form.shortName.trim()) return
setSaving(true)
try {
await menuService.createWithLanguageKeyText({
const savedMenu = {
code: form.code.trim(),
displayName: form.code.trim(),
parentCode: form.parentCode.trim() || undefined,
@ -182,9 +189,18 @@ export function MenuAddDialog({
isDisabled: false,
menuTextTr: form.menuTextTr.trim(),
menuTextEn: form.menuTextEn.trim(),
} as MenuDto)
} as MenuDto
onSaved()
await menuService.createWithLanguageKeyText(savedMenu)
await onSaved({
code: savedMenu.code!,
parentCode: savedMenu.parentCode,
menuTextEn: savedMenu.menuTextEn!,
menuTextTr: savedMenu.menuTextTr!,
icon: savedMenu.icon,
shortName: savedMenu.shortName,
})
onClose()
} catch (e: any) {
toast.push(<Notification title={e.message} type="danger" />, { placement: 'top-end' })
@ -342,10 +358,11 @@ export function MenuAddDialog({
{/* Footer */}
<div className="flex justify-end gap-2 pt-1 border-t border-gray-100 dark:border-gray-700">
<Button size="sm" variant="plain" onClick={onClose}>
<Button type="button" size="sm" variant="plain" onClick={onClose}>
{translate('::Cancel') || 'İptal'}
</Button>
<Button
type="button"
size="sm"
variant="solid"
loading={saving}