Compare commits
14 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
233c9b7502 | ||
|
|
12f046f262 | ||
|
|
d0cccde53f | ||
|
|
1d15c44a3d | ||
|
|
bade0bab98 | ||
|
|
c204eef755 | ||
|
|
27e65f05f0 | ||
|
|
64084679e8 | ||
|
|
2f1b9d4e77 | ||
|
|
1c472a7d9a | ||
|
|
119c3650f0 | ||
|
|
ebab6ea114 | ||
|
|
975bc8dd6c | ||
|
|
97a2a4b38d |
63 changed files with 2989 additions and 426 deletions
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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; } = [];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; } = [];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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": [],
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -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[]
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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": []
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
23
api/src/Sozsoft.Platform.Domain/Queries/Workflow.cs
Normal file
23
api/src/Sozsoft.Platform.Domain/Queries/Workflow.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -507,6 +507,7 @@ public class PlatformDbContext :
|
|||
b.Property(x => x.PositionX).IsRequired();
|
||||
b.Property(x => x.PositionY).IsRequired();
|
||||
b.Property(x => x.CompareOutcomesJson).HasColumnType("text");
|
||||
b.HasIndex(x => new { x.ListFormCode, x.Title }).IsUnique();
|
||||
});
|
||||
|
||||
builder.Entity<Note>(b =>
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore;
|
|||
namespace Sozsoft.Platform.Migrations
|
||||
{
|
||||
[DbContext(typeof(PlatformDbContext))]
|
||||
[Migration("20260602070242_Initial")]
|
||||
[Migration("20260606212623_Initial")]
|
||||
partial class Initial
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
|
@ -3486,6 +3486,9 @@ namespace Sozsoft.Platform.Migrations
|
|||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ListFormCode", "Title")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Sas_H_ListFormWorkflow", (string)null);
|
||||
});
|
||||
|
||||
|
|
@ -3905,6 +3905,12 @@ namespace Sozsoft.Platform.Migrations
|
|||
table: "Sas_H_ListFormImportLog",
|
||||
column: "ImportId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Sas_H_ListFormWorkflow_ListFormCode_Title",
|
||||
table: "Sas_H_ListFormWorkflow",
|
||||
columns: new[] { "ListFormCode", "Title" },
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Sas_H_Menu_Code",
|
||||
table: "Sas_H_Menu",
|
||||
|
|
@ -3483,6 +3483,9 @@ namespace Sozsoft.Platform.Migrations
|
|||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("ListFormCode", "Title")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("Sas_H_ListFormWorkflow", (string)null);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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": [
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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..."
|
||||
|
|
|
|||
|
|
@ -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 @@
|
|||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -908,6 +908,7 @@ export interface WidgetEditDto {
|
|||
|
||||
export interface WorkflowDto {
|
||||
approvalUserFieldName: string
|
||||
isFilterUserName: boolean
|
||||
approvalDateFieldName: string
|
||||
approvalStatusFieldName: string
|
||||
approvalDescriptionFieldName: string
|
||||
|
|
|
|||
24
ui/src/services/auditLog.service.ts
Normal file
24
ui/src/services/auditLog.service.ts
Normal 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()
|
||||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ export interface WorkflowRunResultDto {
|
|||
currentNodeKind?: string | null
|
||||
waitingApproval: boolean
|
||||
completed: boolean
|
||||
toastMessages?: string[]
|
||||
}
|
||||
|
||||
export type SaveCriteriaInput = Omit<Partial<WorkflowCriteriaDto>, 'id'> & {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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} />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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[]) => {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
Loading…
Reference in a new issue