Compare commits

..

No commits in common. "main" and "1.1.04" have entirely different histories.
main ... 1.1.04

63 changed files with 423 additions and 2986 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,43 +1,30 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Linq.Expressions;
using System.Text.Json;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Sozsoft.Platform.Entities;
using Sozsoft.Platform.ListForms;
using Volo.Abp; using Volo.Abp;
using Volo.Abp.Application.Dtos; using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services; using Volo.Abp.Application.Services;
using Volo.Abp.AuditLogging; using Volo.Abp.AuditLogging;
using Volo.Abp.Domain.Repositories;
using Volo.Abp.Uow; using Volo.Abp.Uow;
using static Sozsoft.Platform.Data.Seeds.SeedConsts; using static Sozsoft.Platform.Data.Seeds.SeedConsts;
namespace Sozsoft.Platform.AuditLogs; namespace Sozsoft.Platform.AuditLogs;
public interface IAuditLogAppService public interface IAuditLogAppService
: ICrudAppService<AuditLogDto, Guid, AuditLogListRequestDto> : ICrudAppService<AuditLogDto, Guid>
{ {
} }
[Authorize(AppCodes.IdentityManagement.AuditLogs)] [Authorize(AppCodes.IdentityManagement.AuditLogs)]
public class AuditLogAppService : CrudAppService< public class AuditLogAppService
AuditLog, : CrudAppService<AuditLog, AuditLogDto, Guid>
AuditLogDto, , IAuditLogAppService
Guid,
AuditLogListRequestDto>, IAuditLogAppService
{ {
private readonly IRepository<ListForm, Guid> _listFormRepository; public AuditLogAppService(IAuditLogRepository auditLogRepository) : base(auditLogRepository)
public AuditLogAppService(
IAuditLogRepository auditLogRepository,
IRepository<ListForm, Guid> listFormRepository
) : base(auditLogRepository)
{ {
_listFormRepository = listFormRepository;
} }
public override async Task<AuditLogDto> GetAsync(Guid id) public override async Task<AuditLogDto> GetAsync(Guid id)
@ -48,26 +35,23 @@ public class AuditLogAppService : CrudAppService<
} }
[UnitOfWork] [UnitOfWork]
public override async Task<PagedResultDto<AuditLogDto>> GetListAsync(AuditLogListRequestDto input) public override async Task<PagedResultDto<AuditLogDto>> GetListAsync(PagedAndSortedResultRequestDto input)
{ {
var query = await Repository.WithDetailsAsync(); var query = await CreateFilteredQueryAsync(input);
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); var totalCount = await AsyncExecuter.CountAsync(query);
query = ApplySorting(query, input); query = ApplySorting(query, input);
query = ApplyPaging(query, input); query = ApplyPaging(query, input);
var auditLogsWithDetails = await AsyncExecuter.ToListAsync(query); // 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
);
// Mapping tek seferde yap // Mapping tek seferde yap
var entityDtos = ObjectMapper.Map<List<AuditLog>, List<AuditLogDto>>(auditLogsWithDetails); var entityDtos = ObjectMapper.Map<List<AuditLog>, List<AuditLogDto>>(auditLogsWithDetails);
@ -85,102 +69,6 @@ public class AuditLogAppService : CrudAppService<
); );
} }
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 // Audit Log kayitlarini gormek istiyoruz fakat degistirmek istemiyoruz
[RemoteService(IsEnabled = false)] [RemoteService(IsEnabled = false)]
public override Task<AuditLogDto> CreateAsync(AuditLogDto input) public override Task<AuditLogDto> CreateAsync(AuditLogDto input)

View file

@ -3,12 +3,10 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Security.Claims; using System.Security.Claims;
using System.Threading.Tasks; using System.Threading.Tasks;
using Sozsoft.Platform.BlobStoring;
using Sozsoft.Platform.Entities; using Sozsoft.Platform.Entities;
using Sozsoft.Platform.Extensions; using Sozsoft.Platform.Extensions;
using Sozsoft.Platform.Identity.Dto; using Sozsoft.Platform.Identity.Dto;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using OpenIddict.Abstractions; using OpenIddict.Abstractions;
using Volo.Abp.Application.Services; using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Repositories; using Volo.Abp.Domain.Repositories;
@ -34,7 +32,6 @@ public class PlatformIdentityAppService : ApplicationService
public IRepository<WorkHour, Guid> workHourRepository { get; } public IRepository<WorkHour, Guid> workHourRepository { get; }
public IRepository<Department, Guid> departmentRepository { get; } public IRepository<Department, Guid> departmentRepository { get; }
public IRepository<JobPosition, Guid> jobPositionRepository { get; } public IRepository<JobPosition, Guid> jobPositionRepository { get; }
public BlobManager BlobCdnManager { get; }
public PlatformIdentityAppService( public PlatformIdentityAppService(
IIdentityUserAppService identityUserAppService, IIdentityUserAppService identityUserAppService,
@ -48,7 +45,6 @@ public class PlatformIdentityAppService : ApplicationService
IRepository<WorkHour, Guid> workHourRepository, IRepository<WorkHour, Guid> workHourRepository,
IRepository<Department, Guid> departmentRepository, IRepository<Department, Guid> departmentRepository,
IRepository<JobPosition, Guid> jobPositionRepository, IRepository<JobPosition, Guid> jobPositionRepository,
BlobManager blobCdnManager,
IGuidGenerator guidGenerator IGuidGenerator guidGenerator
) )
{ {
@ -59,7 +55,6 @@ public class PlatformIdentityAppService : ApplicationService
this.workHourRepository = workHourRepository; this.workHourRepository = workHourRepository;
this.departmentRepository = departmentRepository; this.departmentRepository = departmentRepository;
this.jobPositionRepository = jobPositionRepository; this.jobPositionRepository = jobPositionRepository;
this.BlobCdnManager = blobCdnManager;
this.permissionRepository = permissionRepository; this.permissionRepository = permissionRepository;
this.branchRepository = branchRepository; this.branchRepository = branchRepository;
this.branchUsersRepository = branchUsersRepository; this.branchUsersRepository = branchUsersRepository;
@ -278,22 +273,6 @@ public class PlatformIdentityAppService : ApplicationService
await UserManager.UpdateAsync(user); 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() public async Task<List<PermissionDefinitionRecord>> GetPermissionList()
{ {
var list = await permissionRepository.GetListAsync(); var list = await permissionRepository.GetListAsync();

View file

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

View file

@ -7,12 +7,10 @@ using System.Text.Json;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Localization;
using Sozsoft.Platform.Data.Seeds; using Sozsoft.Platform.Data.Seeds;
using Sozsoft.Platform.Entities; using Sozsoft.Platform.Entities;
using Sozsoft.Platform.Enums; using Sozsoft.Platform.Enums;
using Sozsoft.Platform.ListForms.Select; using Sozsoft.Platform.ListForms.Select;
using Sozsoft.Platform.Localization;
using Sozsoft.Platform.Queries; using Sozsoft.Platform.Queries;
using Sozsoft.Sender.Mail; using Sozsoft.Sender.Mail;
using Volo.Abp; using Volo.Abp;
@ -31,7 +29,6 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
private const string SystemApprovalDescription = "Sistem tarafından otomatik olarak onaylandı."; private const string SystemApprovalDescription = "Sistem tarafından otomatik olarak onaylandı.";
private readonly IRepository<ListFormWorkflow, string> criteriaRepository; private readonly IRepository<ListFormWorkflow, string> criteriaRepository;
private readonly IRepository<Note, Guid> noteRepository;
private readonly IListFormManager listFormManager; private readonly IListFormManager listFormManager;
private readonly IListFormAuthorizationManager authManager; private readonly IListFormAuthorizationManager authManager;
private readonly IListFormSelectAppService listFormSelectAppService; private readonly IListFormSelectAppService listFormSelectAppService;
@ -39,22 +36,18 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
private readonly IdentityUserManager identityUserManager; private readonly IdentityUserManager identityUserManager;
private readonly ISozsoftEmailSender erpEmailSender; private readonly ISozsoftEmailSender erpEmailSender;
private readonly ISettingProvider settingProvider; private readonly ISettingProvider settingProvider;
private readonly IStringLocalizer<PlatformResource> localizer;
public ListFormWorkflowAppService( public ListFormWorkflowAppService(
IRepository<ListFormWorkflow, string> criteriaRepository, IRepository<ListFormWorkflow, string> criteriaRepository,
IRepository<Note, Guid> noteRepository,
IListFormManager listFormManager, IListFormManager listFormManager,
IListFormAuthorizationManager authManager, IListFormAuthorizationManager authManager,
IListFormSelectAppService listFormSelectAppService, IListFormSelectAppService listFormSelectAppService,
IQueryManager queryManager, IQueryManager queryManager,
IdentityUserManager identityUserManager, IdentityUserManager identityUserManager,
ISozsoftEmailSender erpEmailSender, ISozsoftEmailSender erpEmailSender,
ISettingProvider settingProvider, ISettingProvider settingProvider)
IStringLocalizer<PlatformResource> localizer)
{ {
this.criteriaRepository = criteriaRepository; this.criteriaRepository = criteriaRepository;
this.noteRepository = noteRepository;
this.listFormManager = listFormManager; this.listFormManager = listFormManager;
this.authManager = authManager; this.authManager = authManager;
this.listFormSelectAppService = listFormSelectAppService; this.listFormSelectAppService = listFormSelectAppService;
@ -62,7 +55,6 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
this.identityUserManager = identityUserManager; this.identityUserManager = identityUserManager;
this.erpEmailSender = erpEmailSender; this.erpEmailSender = erpEmailSender;
this.settingProvider = settingProvider; this.settingProvider = settingProvider;
this.localizer = localizer;
} }
[HttpGet("criteria")] [HttpGet("criteria")]
@ -196,7 +188,7 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
criteria.ListFormCode = code; criteria.ListFormCode = code;
criteria.Kind = NormalizeRequired(input.Kind, "Compare"); criteria.Kind = NormalizeRequired(input.Kind, "Compare");
criteria.Title = await NormalizeUniqueTitleAsync(code, criteria.Id, input.Kind, input.Title); criteria.Title = NormalizeRequired(input.Title, criteria.Kind);
criteria.CompareColumn = NormalizeRequired(input.CompareColumn, "Price"); criteria.CompareColumn = NormalizeRequired(input.CompareColumn, "Price");
criteria.CompareOperator = NormalizeRequired(input.CompareOperator, ">"); criteria.CompareOperator = NormalizeRequired(input.CompareOperator, ">");
criteria.CompareValue = input.CompareValue; criteria.CompareValue = input.CompareValue;
@ -308,14 +300,7 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
var start = context.Criteria.FirstOrDefault(x => x.Kind == "Start") var start = context.Criteria.FirstOrDefault(x => x.Kind == "Start")
?? throw new UserFriendlyException("Workflow başlangıç adımı bulunamadı."); ?? throw new UserFriendlyException("Workflow başlangıç adımı bulunamadı.");
context.WorkflowNoteRows.Add(("Started By: ", ResolveCurrentUserDisplayName())); return await RunUntilWaitAsync(context, start);
var result = await RunUntilWaitAsync(context, start);
await InsertWorkflowNoteAsync(
context,
$"Workflow Started: {start.Title}",
BuildWorkflowNoteContent(context.WorkflowNoteRows));
return result;
} }
[HttpPost("decision")] [HttpPost("decision")]
@ -380,15 +365,7 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
} }
var next = FindNextCriteria(context.Criteria, input.Approved ? current.NextOnApprove : current.NextOnReject); var next = FindNextCriteria(context.Criteria, input.Approved ? current.NextOnApprove : current.NextOnReject);
context.WorkflowNoteRows.Add(("Decision By: ", ResolveCurrentUserDisplayName())); return await RunUntilWaitAsync(context, next);
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( private async Task<WorkflowRunResultDto> RunForEachKeyAsync(
@ -410,10 +387,7 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
CurrentNodeTitle = last?.CurrentNodeTitle, CurrentNodeTitle = last?.CurrentNodeTitle,
CurrentNodeKind = last?.CurrentNodeKind, CurrentNodeKind = last?.CurrentNodeKind,
WaitingApproval = results.Any(x => x.WaitingApproval), 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()
}; };
} }
@ -445,7 +419,7 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
.ToList(); .ToList();
var row = await GetRowAsync(code, listForm.KeyFieldName, keys[0]); var row = await GetRowAsync(code, listForm.KeyFieldName, keys[0]);
return new WorkflowRunContext(code, keys, workflow, criteria, row); return new WorkflowRunContext(code, listForm.KeyFieldName, keys, workflow, criteria, row);
} }
private async Task<IDictionary<string, object>> GetRowAsync(string listFormCode, string keyFieldName, object key) private async Task<IDictionary<string, object>> GetRowAsync(string listFormCode, string keyFieldName, object key)
@ -537,8 +511,6 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
await UpdateRowAsync(context, update); await UpdateRowAsync(context, update);
MergeRowValues(context.Row, update); MergeRowValues(context.Row, update);
AddWorkflowNodeRows(context, node);
AddWorkflowToastMessage(context, node);
if (node.Kind == "Inform") if (node.Kind == "Inform")
{ {
@ -561,7 +533,7 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
recipientEmail, recipientEmail,
sender, sender,
new { }, new { },
BuildInformEmailBody(context, node, await BuildPreviousWorkflowNotesHtmlAsync(context)), BuildInformEmailBody(context, node),
$"Workflow Bilgilendirme: {node.Title}", $"Workflow Bilgilendirme: {node.Title}",
null, null,
true); true);
@ -570,7 +542,6 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
{ {
throw new UserFriendlyException($"Bilgilendirme maili gonderilemedi: {result.ErrorMessage}"); throw new UserFriendlyException($"Bilgilendirme maili gonderilemedi: {result.ErrorMessage}");
} }
} }
private async Task<string> ResolveApproverEmailAsync(string approver) private async Task<string> ResolveApproverEmailAsync(string approver)
@ -594,257 +565,22 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
return user.Email; return user.Email;
} }
private async Task<string> BuildPreviousWorkflowNotesHtmlAsync(WorkflowRunContext context) private static string BuildInformEmailBody(WorkflowRunContext context, ListFormWorkflow node)
{
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 keyText = string.Join(", ", context.Keys.Select(key => WebUtility.HtmlEncode(key?.ToString() ?? string.Empty)));
var listFormCode = WebUtility.HtmlEncode(context.ListFormCode ?? string.Empty); var listFormCode = WebUtility.HtmlEncode(context.ListFormCode ?? string.Empty);
var nodeTitle = WebUtility.HtmlEncode(node.Title ?? 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 $""" return $"""
<div style="font-family: Arial, sans-serif; color: #111827; line-height: 1.45;"> <p>Workflow bilgilendirme adimina ulasildi.</p>
<p>Workflow sürecinde bilgilendirme adımına ulaşıldı.</p> <table>
<table style="border-collapse: collapse; width: 100%; margin-bottom: 16px;"> <tr><td><strong>Liste Formu</strong></td><td>{listFormCode}</td></tr>
<tr><td style="padding: 6px 8px;"><strong>Liste Formu</strong></td><td style="padding: 6px 8px;">{listFormCode}</td></tr> <tr><td><strong>Adim</strong></td><td>{nodeTitle}</td></tr>
<tr><td style="padding: 6px 8px;"><strong>Kayıt</strong></td><td style="padding: 6px 8px;">{keyText}</td></tr> <tr><td><strong>Kayit</strong></td><td>{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> </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) private async Task UpdateRowAsync(WorkflowRunContext context, Dictionary<string, object> data)
{ {
await queryManager.GenerateAndRunQueryAsync<int>( await queryManager.GenerateAndRunQueryAsync<int>(
@ -1026,8 +762,7 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
CurrentNodeTitle = node?.Title, CurrentNodeTitle = node?.Title,
CurrentNodeKind = node?.Kind, CurrentNodeKind = node?.Kind,
WaitingApproval = waitingApproval, WaitingApproval = waitingApproval,
Completed = completed, Completed = completed
ToastMessages = context.ToastMessages.ToList()
}; };
} }
@ -1171,36 +906,6 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
return value.IsNullOrWhiteSpace() ? fallback : value.Trim(); return value.IsNullOrWhiteSpace() ? fallback : value.Trim();
} }
private async Task<string> NormalizeUniqueTitleAsync(
string listFormCode,
string criteriaId,
string kind,
string title)
{
var baseTitle = NormalizeRequired(title, kind);
var existingTitles = (await criteriaRepository.GetListAsync(x =>
x.ListFormCode == listFormCode &&
x.Id != criteriaId))
.Select(x => x.Title?.Trim())
.Where(x => !x.IsNullOrWhiteSpace())
.ToHashSet(StringComparer.OrdinalIgnoreCase);
if (!existingTitles.Contains(baseTitle))
{
return baseTitle;
}
var index = 1;
var candidate = $"{baseTitle}{index}";
while (existingTitles.Contains(candidate))
{
index++;
candidate = $"{baseTitle}{index}";
}
return candidate;
}
private static string SerializeCompareOutcomes(List<CompareOutcomeDto> outcomes) private static string SerializeCompareOutcomes(List<CompareOutcomeDto> outcomes)
{ {
return JsonSerializer.Serialize(outcomes ?? []); return JsonSerializer.Serialize(outcomes ?? []);
@ -1242,14 +947,12 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
private sealed record WorkflowRunContext( private sealed record WorkflowRunContext(
string ListFormCode, string ListFormCode,
string KeyFieldName,
object[] Keys, object[] Keys,
WorkflowDto Workflow, WorkflowDto Workflow,
List<ListFormWorkflow> Criteria, List<ListFormWorkflow> Criteria,
IDictionary<string, object> Row) IDictionary<string, object> Row)
{ {
public HashSet<string> UserUpdatedFields { get; } = []; public HashSet<string> UserUpdatedFields { get; } = [];
public List<(string Label, string Value)> WorkflowNoteRows { get; } = [];
public List<string> ToastMessages { get; } = [];
} }
} }

View file

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

View file

@ -674,7 +674,7 @@
"code": "Abp.Account.EnableLocalLogin", "code": "Abp.Account.EnableLocalLogin",
"nameKey": "Abp.Account.EnableLocalLogin", "nameKey": "Abp.Account.EnableLocalLogin",
"descriptionKey": "Abp.Account.EnableLocalLogin.Description", "descriptionKey": "Abp.Account.EnableLocalLogin.Description",
"defaultValue": "True", "defaultValue": "False",
"isVisibleToClients": false, "isVisibleToClients": false,
"providers": "G|D", "providers": "G|D",
"isInherited": false, "isInherited": false,
@ -1071,22 +1071,6 @@
"dataType": "Number", "dataType": "Number",
"selectOptions": {}, "selectOptions": {},
"order": 80 "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": [], "NotificationTypes": [],

View file

@ -3642,36 +3642,6 @@
"en": "The record was deleted", "en": "The record was deleted",
"tr": "Kayıt silindi" "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", "resourceName": "Platform",
"key": "KayitEklendi", "key": "KayitEklendi",
@ -3765,8 +3735,8 @@
{ {
"resourceName": "Platform", "resourceName": "Platform",
"key": "ListForms.ListForm.AddNewRecord", "key": "ListForms.ListForm.AddNewRecord",
"en": "Add", "en": "Add New Record",
"tr": "Ekle" "tr": "Yeni Kayıt Ekle"
}, },
{ {
"resourceName": "Platform", "resourceName": "Platform",
@ -4184,9 +4154,9 @@
}, },
{ {
"resourceName": "Platform", "resourceName": "Platform",
"key": "ListForms.ListForm.NoteModal.Type.Workflow", "key": "ListForms.ListForm.NoteModal.Type.Activity",
"en": "Workflow", "en": "Activity",
"tr": "Akış" "tr": "Aktivite"
}, },
{ {
"resourceName": "Platform", "resourceName": "Platform",
@ -16766,24 +16736,6 @@
"en": "Approver", "en": "Approver",
"tr": "Onayla" "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", "resourceName": "Platform",
"key": "App.Listform.ListformField.NextOnStart", "key": "App.Listform.ListformField.NextOnStart",
@ -17522,12 +17474,6 @@
"en": "Columns load after selecting a Select Command", "en": "Columns load after selecting a Select Command",
"tr": "Select Command seçince sütunlar yüklenir" "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", "resourceName": "Platform",
"key": "ListForms.Wizard.Step3.GenerateFromTable", "key": "ListForms.Wizard.Step3.GenerateFromTable",
@ -17768,12 +17714,6 @@
"en": "Key Field Type", "en": "Key Field Type",
"tr": "Anahtar Alan Tipi" "tr": "Anahtar Alan Tipi"
}, },
{
"resourceName": "Platform",
"key": "ListForms.Wizard.Step4.ColumnsAndFormLayout",
"en": "Columns & Form Layout",
"tr": "Sütunlar ve Form Yerleşimi"
},
{ {
"resourceName": "Platform", "resourceName": "Platform",
"key": "ListForms.Wizard.Step4.SelectedColumns", "key": "ListForms.Wizard.Step4.SelectedColumns",
@ -17798,24 +17738,6 @@
"en": "Field", "en": "Field",
"tr": "Alan" "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", "resourceName": "Platform",
"key": "ListForms.Wizard.Step4.DeployAndSave", "key": "ListForms.Wizard.Step4.DeployAndSave",
@ -18116,12 +18038,6 @@
"en": "Add Multi-Tenant Column", "en": "Add Multi-Tenant Column",
"tr": "MultiTenant Sütunları Ekle" "tr": "MultiTenant Sütunları Ekle"
}, },
{
"resourceName": "Platform",
"key": "App.SqlQueryManager.AddWorkflowColumns",
"en": "Add Workflow Column",
"tr": "Workflow Sütunları Ekle"
},
{ {
"resourceName": "Platform", "resourceName": "Platform",
"key": "App.SqlQueryManager.ClearAllColumns", "key": "App.SqlQueryManager.ClearAllColumns",
@ -19070,12 +18986,6 @@
"en": "Approval Status Field Name", "en": "Approval Status Field Name",
"tr": "Onay Durumu Alanı Adı" "tr": "Onay Durumu Alanı Adı"
}, },
{
"resourceName": "Platform",
"key": "ListForms.ListFormEdit.Workflow.IsFilterUserName",
"en": "Filter User Name?",
"tr": "Kullanıcı Adı Filtresin mi?"
},
{ {
"resourceName": "Platform", "resourceName": "Platform",
"key": "ListForms.ListFormEdit.Workflow.ApprovalDescriptionFieldName", "key": "ListForms.ListFormEdit.Workflow.ApprovalDescriptionFieldName",
@ -19321,84 +19231,6 @@
"key": "FileManager.SortByModifiedDesc", "key": "FileManager.SortByModifiedDesc",
"en": "Modified (Newest)", "en": "Modified (Newest)",
"tr": "Değiştirilme (En Yeni)" "tr": "Değiştirilme (En Yeni)"
},
{
"resourceName": "Platform",
"key": "ListForms.ListForm.Workflow.WorkflowStarted",
"en": "Operation: Workflow started",
"tr": "İşlem: Workflow başlatıldı"
},
{
"resourceName": "Platform",
"key": "ListForms.ListForm.Workflow.InformReached",
"en": "Operation: Workflow inform step reached",
"tr": "İşlem: Workflow bilgilendirme adımına ulaştı"
},
{
"resourceName": "Platform",
"key": "ListForms.ListForm.Workflow.WorkflowCompleted",
"en": "Operation: Workflow completed",
"tr": "İşlem: Workflow tamamlandı"
},
{
"resourceName": "Platform",
"key": "ListForms.ListForm.Workflow.Step",
"en": "Step: {0}",
"tr": "Adım: {0}"
},
{
"resourceName": "Platform",
"key": "ListForms.ListForm.Workflow.PerformedBy",
"en": "Performed by: {0}",
"tr": "İşlemi yapan: {0}"
},
{
"resourceName": "Platform",
"key": "ListForms.ListForm.Workflow.InformUser",
"en": "User to inform: {0}",
"tr": "Bilgilendirilecek kullanıcı: {0}"
},
{
"resourceName": "Platform",
"key": "ListForms.ListForm.Workflow.ApproverUser",
"en": "Approver user: {0}",
"tr": "Onaylayacak kullanıcı: {0}"
},
{
"resourceName": "Platform",
"key": "ListForms.ListForm.Workflow.NextStep.Approval",
"en": "Next step: Approval",
"tr": "Sonraki adım: Onay"
},
{
"resourceName": "Platform",
"key": "ListForms.ListForm.Workflow.NextStep.Inform",
"en": "Next step: Inform",
"tr": "Sonraki adım: Bilgilendirme"
},
{
"resourceName": "Platform",
"key": "ListForms.ListForm.Workflow.NextStep.End",
"en": "Next step: Workflow end",
"tr": "Sonraki adım: Workflow bitiş"
},
{
"resourceName": "Platform",
"key": "ListForms.ListForm.Workflow.NextStepName",
"en": "Next step name: {0}",
"tr": "Sonraki adım adı: {0}"
},
{
"resourceName": "Platform",
"key": "ListForms.ListForm.Workflow.NextStep",
"en": "Next step: {0}",
"tr": "Sonraki adım: {0}"
},
{
"resourceName": "Platform",
"key": "ListForms.ListForm.Workflow.UndefinedUser",
"en": "Undefined",
"tr": "Tanımsız"
} }
] ]
} }

View file

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

View file

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

View file

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

View file

@ -1,25 +0,0 @@
IF OBJECT_ID(N'[dbo].[Sal_T_Approval]', 'U') IS NULL
BEGIN
CREATE TABLE [dbo].[Sal_T_Approval]
(
[Id] uniqueidentifier NOT NULL DEFAULT NEWID(),
[CreationTime] datetime2 NOT NULL DEFAULT GETUTCDATE(),
[CreatorId] uniqueidentifier NULL,
[LastModificationTime] datetime2 NULL,
[LastModifierId] uniqueidentifier NULL,
[IsDeleted] bit NOT NULL DEFAULT 0,
[DeletionTime] datetime2 NULL,
[DeleterId] uniqueidentifier NULL,
[TenantId] uniqueidentifier NULL,
[ApprovalUserName] nvarchar(256) NULL,
[ApprovalStatus] nvarchar(50) NULL,
[ApprovalDate] datetime NULL,
[ApprovalDescription] nvarchar(200) NULL,
[Name] nvarchar(100) NULL,
CONSTRAINT [PK_Sal_T_Approval] PRIMARY KEY NONCLUSTERED
(
[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
) ON [PRIMARY]
END
GO

View file

@ -1,385 +0,0 @@
{
"Wizard": {
"WizardName": "Approval",
"ListFormCode": "App.Wizard.Approval",
"MenuCode": "App.Wizard.Approval",
"IsTenant": true,
"IsBranch": false,
"IsOrganizationUnit": false,
"AllowAdding": true,
"AllowUpdating": true,
"AllowDeleting": true,
"AllowDetail": false,
"ConfirmDelete": true,
"DefaultLayout": "grid",
"Grid": true,
"Pivot": true,
"Tree": true,
"Chart": true,
"Gantt": true,
"Scheduler": true,
"LanguageTextMenuEn": "Approval",
"LanguageTextMenuTr": "Approval",
"LanguageTextTitleEn": "Approval",
"LanguageTextTitleTr": "Onaylama",
"LanguageTextDescEn": "Approval",
"LanguageTextDescTr": "Onaylama",
"LanguageTextMenuParentEn": "Sales",
"LanguageTextMenuParentTr": "Sat\u0131\u015F",
"PermissionGroupName": "App.Wizard.Sales",
"MenuParentCode": "App.Wizard.Sales",
"MenuParentIcon": "FcAssistant",
"MenuIcon": "FcAndroidOs",
"DataSourceCode": "Default",
"DataSourceConnectionString": "",
"SelectCommandType": 1,
"SelectCommand": "Sal_T_Approval",
"KeyFieldName": "Id",
"KeyFieldDbSourceType": 9,
"TreeKeyExpr": "",
"TreeParentIdExpr": "",
"TreeAutoExpandAll": false,
"GanttKeyExpr": "",
"GanttParentIdExpr": "",
"GanttAutoExpandAll": false,
"GanttTitleExpr": "",
"GanttStartExpr": "",
"GanttEndExpr": "",
"GanttProgressExpr": "",
"SchedulerTextExpr": "",
"SchedulerStartDateExpr": "",
"SchedulerEndDateExpr": "",
"Groups": [
{
"Caption": "",
"ColCount": 1,
"Items": [
{
"DataField": "Id",
"CaptionName": "App.Listform.ListformField.Id",
"EditorType": "dxTextBox",
"EditorOptions": "",
"EditorScript": "",
"ColSpan": 1,
"IsRequired": true,
"IncludeInEditingForm": true,
"DbSourceType": 9,
"TurkishCaption": "Id",
"EnglishCaption": "Id",
"LookupDataSourceType": 1,
"ValueExpr": "Key",
"DisplayExpr": "Name",
"LookupQuery": ""
},
{
"DataField": "Name",
"CaptionName": "App.Listform.ListformField.Name",
"EditorType": "dxTextBox",
"EditorOptions": "",
"EditorScript": "",
"ColSpan": 1,
"IsRequired": false,
"IncludeInEditingForm": true,
"DbSourceType": 16,
"TurkishCaption": "Name",
"EnglishCaption": "Name",
"LookupDataSourceType": 1,
"ValueExpr": "Key",
"DisplayExpr": "Name",
"LookupQuery": ""
},
{
"DataField": "ApprovalUserName",
"CaptionName": "App.Listform.ListformField.ApprovalUserName",
"EditorType": "dxTextBox",
"EditorOptions": "",
"EditorScript": "",
"ColSpan": 1,
"IsRequired": false,
"IncludeInEditingForm": false,
"DbSourceType": 16,
"TurkishCaption": "Approval User Name",
"EnglishCaption": "Approval User Name",
"LookupDataSourceType": 1,
"ValueExpr": "Key",
"DisplayExpr": "Name",
"LookupQuery": ""
},
{
"DataField": "ApprovalStatus",
"CaptionName": "App.Listform.ListformField.ApprovalStatus",
"EditorType": "dxTextBox",
"EditorOptions": "",
"EditorScript": "",
"ColSpan": 1,
"IsRequired": false,
"IncludeInEditingForm": false,
"DbSourceType": 16,
"TurkishCaption": "Approval Status",
"EnglishCaption": "Approval Status",
"LookupDataSourceType": 1,
"ValueExpr": "Key",
"DisplayExpr": "Name",
"LookupQuery": ""
},
{
"DataField": "ApprovalDate",
"CaptionName": "App.Listform.ListformField.ApprovalDate",
"EditorType": "dxDateBox",
"EditorOptions": "",
"EditorScript": "",
"ColSpan": 1,
"IsRequired": false,
"IncludeInEditingForm": false,
"DbSourceType": 6,
"TurkishCaption": "Approval Date",
"EnglishCaption": "Approval Date",
"LookupDataSourceType": 1,
"ValueExpr": "Key",
"DisplayExpr": "Name",
"LookupQuery": ""
},
{
"DataField": "ApprovalDescription",
"CaptionName": "App.Listform.ListformField.ApprovalDescription",
"EditorType": "dxTextBox",
"EditorOptions": "",
"EditorScript": "",
"ColSpan": 1,
"IsRequired": false,
"IncludeInEditingForm": false,
"DbSourceType": 16,
"TurkishCaption": "Approval Description",
"EnglishCaption": "Approval Description",
"LookupDataSourceType": 1,
"ValueExpr": "Key",
"DisplayExpr": "Name",
"LookupQuery": ""
}
]
}
],
"SubForms": [],
"Widgets": [],
"Workflow": {
"ApprovalUserFieldName": "ApprovalUserName",
"IsFilterUserName": true,
"ApprovalDateFieldName": "ApprovalDate",
"ApprovalStatusFieldName": "ApprovalStatus",
"ApprovalDescriptionFieldName": "ApprovalDescription",
"Criteria": [
{
"ListFormCode": "App.Wizard.Approval",
"Kind": "Start",
"Title": "\u0130\u015F Ak\u0131\u015F\u0131 Ba\u015Flat1",
"CompareColumn": "Price",
"CompareOperator": "\u003E",
"CompareValue": 5000,
"Approver": "",
"NextOnStart": "N002",
"NextOnTrue": "",
"NextOnFalse": "",
"NextOnApprove": "",
"NextOnReject": "",
"PositionX": 34,
"PositionY": 104,
"CompareOutcomes": [],
"Id": "N001"
},
{
"ListFormCode": "App.Wizard.Approval",
"Kind": "Approval",
"Title": "Onay1",
"CompareColumn": "Price",
"CompareOperator": "\u003E",
"CompareValue": 5000,
"Approver": "admin@sozsoft.com",
"NextOnStart": "",
"NextOnTrue": "",
"NextOnFalse": "",
"NextOnApprove": "N003",
"NextOnReject": "N004",
"PositionX": 323,
"PositionY": 104,
"CompareOutcomes": [],
"Id": "N002"
},
{
"ListFormCode": "App.Wizard.Approval",
"Kind": "Approval",
"Title": "Onay2",
"CompareColumn": "Price",
"CompareOperator": "\u003E",
"CompareValue": 5000,
"Approver": "demo@sozsoft.com",
"NextOnStart": "",
"NextOnTrue": "",
"NextOnFalse": "",
"NextOnApprove": "N004",
"NextOnReject": "N004",
"PositionX": 586,
"PositionY": 104,
"CompareOutcomes": [],
"Id": "N003"
},
{
"ListFormCode": "App.Wizard.Approval",
"Kind": "Inform",
"Title": "Bilgilendirme1",
"CompareColumn": "Price",
"CompareOperator": "\u003E",
"CompareValue": 5000,
"Approver": "system@sozsoft.com",
"NextOnStart": "N005",
"NextOnTrue": "",
"NextOnFalse": "",
"NextOnApprove": "",
"NextOnReject": "",
"PositionX": 458,
"PositionY": 411,
"CompareOutcomes": [],
"Id": "N004"
},
{
"ListFormCode": "App.Wizard.Approval",
"Kind": "End",
"Title": "\u0130\u015F Ak\u0131\u015F\u0131 Bitir1",
"CompareColumn": "Price",
"CompareOperator": "\u003E",
"CompareValue": 5000,
"Approver": "",
"NextOnStart": "",
"NextOnTrue": "",
"NextOnFalse": "",
"NextOnApprove": "",
"NextOnReject": "",
"PositionX": 792,
"PositionY": 412,
"CompareOutcomes": [],
"Id": "N005"
}
]
},
"WorkflowCriteria": [
{
"ListFormCode": "App.Wizard.Approval",
"Kind": "Start",
"Title": "\u0130\u015F Ak\u0131\u015F\u0131 Ba\u015Flat1",
"CompareColumn": "Price",
"CompareOperator": "\u003E",
"CompareValue": 5000,
"Approver": "",
"NextOnStart": "N002",
"NextOnTrue": "",
"NextOnFalse": "",
"NextOnApprove": "",
"NextOnReject": "",
"PositionX": 34,
"PositionY": 104,
"CompareOutcomes": [],
"Id": "N001"
},
{
"ListFormCode": "App.Wizard.Approval",
"Kind": "Approval",
"Title": "Onay1",
"CompareColumn": "Price",
"CompareOperator": "\u003E",
"CompareValue": 5000,
"Approver": "admin@sozsoft.com",
"NextOnStart": "",
"NextOnTrue": "",
"NextOnFalse": "",
"NextOnApprove": "N003",
"NextOnReject": "N004",
"PositionX": 323,
"PositionY": 104,
"CompareOutcomes": [],
"Id": "N002"
},
{
"ListFormCode": "App.Wizard.Approval",
"Kind": "Approval",
"Title": "Onay2",
"CompareColumn": "Price",
"CompareOperator": "\u003E",
"CompareValue": 5000,
"Approver": "demo@sozsoft.com",
"NextOnStart": "",
"NextOnTrue": "",
"NextOnFalse": "",
"NextOnApprove": "N004",
"NextOnReject": "N004",
"PositionX": 586,
"PositionY": 104,
"CompareOutcomes": [],
"Id": "N003"
},
{
"ListFormCode": "App.Wizard.Approval",
"Kind": "Inform",
"Title": "Bilgilendirme1",
"CompareColumn": "Price",
"CompareOperator": "\u003E",
"CompareValue": 5000,
"Approver": "system@sozsoft.com",
"NextOnStart": "N005",
"NextOnTrue": "",
"NextOnFalse": "",
"NextOnApprove": "",
"NextOnReject": "",
"PositionX": 458,
"PositionY": 411,
"CompareOutcomes": [],
"Id": "N004"
},
{
"ListFormCode": "App.Wizard.Approval",
"Kind": "End",
"Title": "\u0130\u015F Ak\u0131\u015F\u0131 Bitir1",
"CompareColumn": "Price",
"CompareOperator": "\u003E",
"CompareValue": 5000,
"Approver": "",
"NextOnStart": "",
"NextOnTrue": "",
"NextOnFalse": "",
"NextOnApprove": "",
"NextOnReject": "",
"PositionX": 792,
"PositionY": 412,
"CompareOutcomes": [],
"Id": "N005"
}
]
},
"IsDeletedField": true,
"IsCreatedField": true,
"InsertedRecords": {
"LanguageKeys": [
"App.Wizard.Approval",
"App.Wizard.Approval.Title",
"App.Wizard.Approval.Desc",
"App.Listform.ListformField.ApprovalUserName",
"App.Listform.ListformField.ApprovalStatus",
"App.Listform.ListformField.ApprovalDate",
"App.Listform.ListformField.ApprovalDescription"
],
"PermissionGroupNames": [
"App.Wizard.Sales"
],
"PermissionNames": [
"App.Wizard.Approval",
"App.Wizard.Approval.Create",
"App.Wizard.Approval.Update",
"App.Wizard.Approval.Delete",
"App.Wizard.Approval.Export",
"App.Wizard.Approval.Import",
"App.Wizard.Approval.Note"
],
"MenuCodes": [
"App.Wizard.Approval"
],
"DataSourceCodes": []
}
}

View file

@ -36,7 +36,6 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
private readonly IRepository<DataSource, Guid> _repoDataSource; private readonly IRepository<DataSource, Guid> _repoDataSource;
private readonly IRepository<ListForm, Guid> _repoListForm; private readonly IRepository<ListForm, Guid> _repoListForm;
private readonly IRepository<ListFormField, Guid> _repoListFormField; private readonly IRepository<ListFormField, Guid> _repoListFormField;
private readonly IRepository<ListFormWorkflow, string> _repoListFormWorkflow;
private readonly ILogger<WizardDataSeeder> _logger; private readonly ILogger<WizardDataSeeder> _logger;
private readonly string _cultureNameDefault = PlatformConsts.DefaultLanguage; private readonly string _cultureNameDefault = PlatformConsts.DefaultLanguage;
@ -52,7 +51,6 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
IRepository<DataSource, Guid> repoDataSource, IRepository<DataSource, Guid> repoDataSource,
IRepository<ListForm, Guid> repoListForm, IRepository<ListForm, Guid> repoListForm,
IRepository<ListFormField, Guid> repoListFormField, IRepository<ListFormField, Guid> repoListFormField,
IRepository<ListFormWorkflow, string> repoListFormWorkflow,
ILogger<WizardDataSeeder> logger) ILogger<WizardDataSeeder> logger)
{ {
_repoLangKey = repoLangKey; _repoLangKey = repoLangKey;
@ -64,7 +62,6 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
_repoDataSource = repoDataSource; _repoDataSource = repoDataSource;
_repoListForm = repoListForm; _repoListForm = repoListForm;
_repoListFormField = repoListFormField; _repoListFormField = repoListFormField;
_repoListFormWorkflow = repoListFormWorkflow;
_logger = logger; _logger = logger;
} }
@ -137,29 +134,13 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
input.Widgets ??= new List<WidgetEditDto>(); input.Widgets ??= new List<WidgetEditDto>();
input.Workflow ??= new WorkflowDto(); input.Workflow ??= new WorkflowDto();
input.WorkflowCriteria ??= new List<ListFormWorkflowCriteriaDto>(); 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; input.Workflow.Criteria = input.WorkflowCriteria;
EnsureUniqueWorkflowCriteriaTitles(input.WorkflowCriteria);
var wizardName = input.WizardName.Trim(); var wizardName = input.WizardName.Trim();
var code = string.IsNullOrWhiteSpace(input.MenuCode) var titleLangKey = WizardConsts.WizardKeyTitle(wizardName);
? WizardConsts.WizardKey(wizardName) var nameLangKey = WizardConsts.WizardKey(wizardName);
: input.MenuCode.Trim(); var descLangKey = WizardConsts.WizardKeyDesc(wizardName);
var listFormCode = string.IsNullOrWhiteSpace(input.ListFormCode) var code = WizardConsts.WizardKey(wizardName);
? 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 // Dil - Language Keys
await CreateLangKeyAsync(nameLangKey, input.LanguageTextMenuEn, input.LanguageTextMenuTr); await CreateLangKeyAsync(nameLangKey, input.LanguageTextMenuEn, input.LanguageTextMenuTr);
@ -172,9 +153,6 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
{ {
await _repoPermGroup.InsertAsync( await _repoPermGroup.InsertAsync(
new PermissionGroupDefinitionRecord(Guid.NewGuid(), groupName, groupName), autoSave: true); new PermissionGroupDefinitionRecord(Guid.NewGuid(), groupName, groupName), autoSave: true);
if (string.Equals(groupName, input.MenuParentCode, StringComparison.OrdinalIgnoreCase))
await EnsureLangKeyAsync(groupName);
else
await CreateLangKeyAsync(groupName, groupName, groupName); await CreateLangKeyAsync(groupName, groupName, groupName);
} }
@ -187,35 +165,35 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
permRead = await _repoPerm.InsertAsync(new PermissionDefinitionRecord( permRead = await _repoPerm.InsertAsync(new PermissionDefinitionRecord(
Guid.NewGuid(), groupName, code, null, nameLangKey, true, MultiTenancySides.Both), autoSave: true); Guid.NewGuid(), groupName, code, null, nameLangKey, true, MultiTenancySides.Both), autoSave: true);
var permCreate = existingPerms.FirstOrDefault(a => a.Name == permCreateName); var permCreate = existingPerms.FirstOrDefault(a => a.Name == WizardConsts.PermCreate(wizardName));
if (permCreate == null) if (permCreate == null)
permCreate = await _repoPerm.InsertAsync(new PermissionDefinitionRecord( permCreate = await _repoPerm.InsertAsync(new PermissionDefinitionRecord(
Guid.NewGuid(), groupName, permCreateName, permRead.Name, WizardConsts.LangKeyCreate, true, MultiTenancySides.Both), autoSave: true); Guid.NewGuid(), groupName, WizardConsts.PermCreate(wizardName), permRead.Name, WizardConsts.LangKeyCreate, true, MultiTenancySides.Both), autoSave: true);
var permUpdate = existingPerms.FirstOrDefault(a => a.Name == permUpdateName); var permUpdate = existingPerms.FirstOrDefault(a => a.Name == WizardConsts.PermUpdate(wizardName));
if (permUpdate == null) if (permUpdate == null)
permUpdate = await _repoPerm.InsertAsync(new PermissionDefinitionRecord( permUpdate = await _repoPerm.InsertAsync(new PermissionDefinitionRecord(
Guid.NewGuid(), groupName, permUpdateName, permRead.Name, WizardConsts.LangKeyUpdate, true, MultiTenancySides.Both), autoSave: true); Guid.NewGuid(), groupName, WizardConsts.PermUpdate(wizardName), permRead.Name, WizardConsts.LangKeyUpdate, true, MultiTenancySides.Both), autoSave: true);
var permDelete = existingPerms.FirstOrDefault(a => a.Name == permDeleteName); var permDelete = existingPerms.FirstOrDefault(a => a.Name == WizardConsts.PermDelete(wizardName));
if (permDelete == null) if (permDelete == null)
permDelete = await _repoPerm.InsertAsync(new PermissionDefinitionRecord( permDelete = await _repoPerm.InsertAsync(new PermissionDefinitionRecord(
Guid.NewGuid(), groupName, permDeleteName, permRead.Name, WizardConsts.LangKeyDelete, true, MultiTenancySides.Both), autoSave: true); Guid.NewGuid(), groupName, WizardConsts.PermDelete(wizardName), permRead.Name, WizardConsts.LangKeyDelete, true, MultiTenancySides.Both), autoSave: true);
var permExport = existingPerms.FirstOrDefault(a => a.Name == permExportName); var permExport = existingPerms.FirstOrDefault(a => a.Name == WizardConsts.PermExport(wizardName));
if (permExport == null) if (permExport == null)
permExport = await _repoPerm.InsertAsync(new PermissionDefinitionRecord( permExport = await _repoPerm.InsertAsync(new PermissionDefinitionRecord(
Guid.NewGuid(), groupName, permExportName, permRead.Name, WizardConsts.LangKeyExport, true, MultiTenancySides.Both), autoSave: true); Guid.NewGuid(), groupName, WizardConsts.PermExport(wizardName), permRead.Name, WizardConsts.LangKeyExport, true, MultiTenancySides.Both), autoSave: true);
var permImport = existingPerms.FirstOrDefault(a => a.Name == permImportName); var permImport = existingPerms.FirstOrDefault(a => a.Name == WizardConsts.PermImport(wizardName));
if (permImport == null) if (permImport == null)
permImport = await _repoPerm.InsertAsync(new PermissionDefinitionRecord( permImport = await _repoPerm.InsertAsync(new PermissionDefinitionRecord(
Guid.NewGuid(), groupName, permImportName, permRead.Name, WizardConsts.LangKeyImport, true, MultiTenancySides.Both), autoSave: true); Guid.NewGuid(), groupName, WizardConsts.PermImport(wizardName), permRead.Name, WizardConsts.LangKeyImport, true, MultiTenancySides.Both), autoSave: true);
var permNote = existingPerms.FirstOrDefault(a => a.Name == permNoteName); var permNote = existingPerms.FirstOrDefault(a => a.Name == WizardConsts.PermNote(wizardName));
if (permNote == null) if (permNote == null)
permNote = await _repoPerm.InsertAsync(new PermissionDefinitionRecord( permNote = await _repoPerm.InsertAsync(new PermissionDefinitionRecord(
Guid.NewGuid(), groupName, permNoteName, permRead.Name, WizardConsts.LangKeyNote, true, MultiTenancySides.Both), autoSave: true); Guid.NewGuid(), groupName, WizardConsts.PermNote(wizardName), permRead.Name, WizardConsts.LangKeyNote, true, MultiTenancySides.Both), autoSave: true);
// // Permission Grants - Admin role için, sadece eksik olanları ekle // // Permission Grants - Admin role için, sadece eksik olanları ekle
// var existingGrants = await _permissionGrantRepository.GetListAsync("R", PlatformConsts.AbpIdentity.User.AdminRoleName); // var existingGrants = await _permissionGrantRepository.GetListAsync("R", PlatformConsts.AbpIdentity.User.AdminRoleName);
@ -241,18 +219,12 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
if (menuParent == null) if (menuParent == null)
{ {
var maxRootOrder = menuQueryable.Where(a => a.ParentCode == null || a.ParentCode == "").Select(a => (int?)a.Order).Max() ?? 0; var maxRootOrder = menuQueryable.Where(a => a.ParentCode == null || a.ParentCode == "").Select(a => (int?)a.Order).Max() ?? 0;
var menuParentIcon = !string.IsNullOrWhiteSpace(input.MenuParentIcon) await CreateLangKeyAsync(WizardConsts.WizardKeyParent(wizardName), input.LanguageTextMenuParentEn, input.LanguageTextMenuParentTr);
? input.MenuParentIcon
: !string.IsNullOrWhiteSpace(input.MenuIcon)
? input.MenuIcon
: WizardConsts.MenuIcon;
await CreateLangKeyAsync(input.MenuParentCode, input.LanguageTextMenuParentEn, input.LanguageTextMenuParentTr);
menuParent = await _repoMenu.InsertAsync(new Menu menuParent = await _repoMenu.InsertAsync(new Menu
{ {
Code = input.MenuParentCode, Code = input.MenuParentCode,
DisplayName = input.MenuParentCode, DisplayName = WizardConsts.WizardKeyParent(wizardName),
IsDisabled = false, IsDisabled = false,
Icon = menuParentIcon,
Order = maxRootOrder + 1, Order = maxRootOrder + 1,
}, autoSave: true); }, autoSave: true);
} }
@ -304,7 +276,7 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
ColSpan = g.ColCount, ColSpan = g.ColCount,
ItemType = "group", ItemType = "group",
Items = g.Items Items = g.Items
.Where(i => i.IncludeInEditingForm && i.DataField != input.KeyFieldName) .Where(i => i.DataField != input.KeyFieldName)
.Select((it, ii) => new EditingFormItemDto .Select((it, ii) => new EditingFormItemDto
{ {
Order = ii + 1, Order = ii + 1,
@ -317,7 +289,6 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
}) })
.ToArray() .ToArray()
}) })
.Where(g => g.Items.Length > 0)
.ToList(); .ToList();
// ListForm - varsa sil, yeniden ekle // ListForm - varsa sil, yeniden ekle
@ -333,12 +304,6 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
await _repoListFormField.DeleteManyAsync(existingListFormFields, autoSave: true); 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 // ListForm
await _repoListForm.InsertAsync(new ListForm await _repoListForm.InsertAsync(new ListForm
{ {
@ -346,7 +311,7 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
PageSize = 10, PageSize = 10,
ExportJson = WizardConsts.DefaultExportJson, ExportJson = WizardConsts.DefaultExportJson,
IsSubForm = false, IsSubForm = false,
ShowNote = input.SubForms.Count > 0 || input.WorkflowCriteria.Count > 0, ShowNote = input.SubForms.Count > 0,
LayoutJson = WizardConsts.DefaultLayoutJson(input.DefaultLayout, input.Grid, input.Pivot, input.Tree, input.Chart, input.Gantt, input.Scheduler), LayoutJson = WizardConsts.DefaultLayoutJson(input.DefaultLayout, input.Grid, input.Pivot, input.Tree, input.Chart, input.Gantt, input.Scheduler),
CultureName = LanguageCodes.En, CultureName = LanguageCodes.En,
ListFormCode = input.ListFormCode, ListFormCode = input.ListFormCode,
@ -367,7 +332,7 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
HeaderFilterJson = WizardConsts.DefaultHeaderFilterJson, HeaderFilterJson = WizardConsts.DefaultHeaderFilterJson,
SearchPanelJson = WizardConsts.DefaultSearchPanelJson, SearchPanelJson = WizardConsts.DefaultSearchPanelJson,
GroupPanelJson = JsonSerializer.Serialize(new { Visible = false }), GroupPanelJson = JsonSerializer.Serialize(new { Visible = false }),
SelectionJson = WizardConsts.DefaultSelectionSingleJson(input.WorkflowCriteria.Count > 0 ? GridOptions.SelectionModeSingle : GridOptions.SelectionModeNone), SelectionJson = WizardConsts.DefaultSelectionSingleJson,
ColumnOptionJson = WizardConsts.DefaultColumnOptionJson(), ColumnOptionJson = WizardConsts.DefaultColumnOptionJson(),
PermissionJson = WizardConsts.DefaultPermissionJson(code), PermissionJson = WizardConsts.DefaultPermissionJson(code),
DeleteCommand = isDeleted ? WizardConsts.DefaultDeleteCommand(input.SelectCommand) : null, DeleteCommand = isDeleted ? WizardConsts.DefaultDeleteCommand(input.SelectCommand) : null,
@ -416,34 +381,6 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
await CreateLangKeyAsync(item.CaptionName, item.EnglishCaption, item.TurkishCaption); 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) private static bool HasWorkflow(WorkflowDto workflow, List<ListFormWorkflowCriteriaDto> criteria)
@ -457,60 +394,6 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
); );
} }
private static void EnsureUniqueWorkflowCriteriaTitles(List<ListFormWorkflowCriteriaDto> criteria)
{
if (criteria == null || criteria.Count == 0)
{
return;
}
var baseTitles = criteria
.Select(x => string.IsNullOrWhiteSpace(x.Title) ? NormalizeWorkflowTitleFallback(x.Kind) : x.Title.Trim())
.ToList();
var duplicateTitles = baseTitles
.GroupBy(x => x, StringComparer.OrdinalIgnoreCase)
.Where(x => x.Count() > 1)
.Select(x => x.Key)
.ToHashSet(StringComparer.OrdinalIgnoreCase);
var titleCounts = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
var usedTitles = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var item in criteria)
{
var baseTitle = string.IsNullOrWhiteSpace(item.Title) ? NormalizeWorkflowTitleFallback(item.Kind) : item.Title.Trim();
var title = baseTitle;
if (duplicateTitles.Contains(baseTitle))
{
titleCounts.TryGetValue(baseTitle, out var count);
count++;
titleCounts[baseTitle] = count;
title = $"{baseTitle}{count}";
}
if (usedTitles.Contains(title))
{
var index = 1;
var candidate = $"{title}{index}";
while (usedTitles.Contains(candidate))
{
index++;
candidate = $"{title}{index}";
}
title = candidate;
}
item.Title = title;
usedTitles.Add(title);
}
}
private static string NormalizeWorkflowTitleFallback(string kind)
{
return string.IsNullOrWhiteSpace(kind) ? "Step" : kind.Trim();
}
private async Task CreateLangKeyAsync(string key, string textEn, string textTr) private async Task CreateLangKeyAsync(string key, string textEn, string textTr)
{ {
if (string.IsNullOrWhiteSpace(key)) return; if (string.IsNullOrWhiteSpace(key)) return;
@ -544,16 +427,6 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
}, autoSave: true); }, autoSave: true);
} }
} }
private async Task EnsureLangKeyAsync(string key)
{
if (string.IsNullOrWhiteSpace(key)) return;
if (!await _repoLangKey.AnyAsync(a => a.ResourceName == _appName && a.Key == key))
{
await _repoLangKey.InsertAsync(new LanguageKey { ResourceName = _appName, Key = key }, autoSave: true);
}
}
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -465,28 +465,6 @@ 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()) if (!whereParts.Any())
{ {
whereParts.Add("1 = 1"); whereParts.Add("1 = 1");

View file

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

View file

@ -2,12 +2,8 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Data; using System.Data;
using System.Data.Common; using System.Data.Common;
using System.Globalization;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks; using System.Threading.Tasks;
using Dapper; using Dapper;
using Sozsoft.Platform;
using Sozsoft.Platform.DynamicData; using Sozsoft.Platform.DynamicData;
using Microsoft.Data.SqlClient; using Microsoft.Data.SqlClient;
using Volo.Abp.DependencyInjection; using Volo.Abp.DependencyInjection;
@ -182,7 +178,7 @@ public class MsDynamicDataRepository : IDynamicDataRepository, IScopedDependency
public virtual async Task<List<T>> QueryAsync<T>(string sql, string cs, Dictionary<string, object> parameters = null) public virtual async Task<List<T>> QueryAsync<T>(string sql, string cs, Dictionary<string, object> parameters = null)
{ {
var param = CreateDynamicParameters(parameters); var param = new DynamicParameters(parameters);
var dbConnection = await GetOrCreateConnectionAsync(cs); var dbConnection = await GetOrCreateConnectionAsync(cs);
var transaction = await GetOrCreateTransactionAsync(dbConnection, cs); var transaction = await GetOrCreateTransactionAsync(dbConnection, cs);
@ -192,7 +188,7 @@ public class MsDynamicDataRepository : IDynamicDataRepository, IScopedDependency
public virtual async Task<IEnumerable<dynamic>> QueryAsync(string sql, string cs, Dictionary<string, object> parameters = null) public virtual async Task<IEnumerable<dynamic>> QueryAsync(string sql, string cs, Dictionary<string, object> parameters = null)
{ {
var param = CreateDynamicParameters(parameters); var param = new DynamicParameters(parameters);
var dbConnection = await GetOrCreateConnectionAsync(cs); var dbConnection = await GetOrCreateConnectionAsync(cs);
var transaction = await GetOrCreateTransactionAsync(dbConnection, cs); var transaction = await GetOrCreateTransactionAsync(dbConnection, cs);
@ -201,7 +197,7 @@ public class MsDynamicDataRepository : IDynamicDataRepository, IScopedDependency
public virtual async Task<T> QuerySingleAsync<T>(string sql, string cs, Dictionary<string, object> parameters = null) public virtual async Task<T> QuerySingleAsync<T>(string sql, string cs, Dictionary<string, object> parameters = null)
{ {
var param = CreateDynamicParameters(parameters); var param = new DynamicParameters(parameters);
var dbConnection = await GetOrCreateConnectionAsync(cs); var dbConnection = await GetOrCreateConnectionAsync(cs);
var transaction = await GetOrCreateTransactionAsync(dbConnection, cs); var transaction = await GetOrCreateTransactionAsync(dbConnection, cs);
@ -210,7 +206,7 @@ public class MsDynamicDataRepository : IDynamicDataRepository, IScopedDependency
public virtual async Task<T> ExecuteScalarAsync<T>(string sql, string cs, Dictionary<string, object> parameters = null) public virtual async Task<T> ExecuteScalarAsync<T>(string sql, string cs, Dictionary<string, object> parameters = null)
{ {
var param = CreateDynamicParameters(parameters); var param = new DynamicParameters(parameters);
var dbConnection = await GetOrCreateConnectionAsync(cs); var dbConnection = await GetOrCreateConnectionAsync(cs);
var transaction = await GetOrCreateTransactionAsync(dbConnection, cs); var transaction = await GetOrCreateTransactionAsync(dbConnection, cs);
@ -241,257 +237,13 @@ public class MsDynamicDataRepository : IDynamicDataRepository, IScopedDependency
public virtual async Task<int> ExecuteAsync(string sql, string cs, Dictionary<string, object> parameters = null) public virtual async Task<int> ExecuteAsync(string sql, string cs, Dictionary<string, object> parameters = null)
{ {
var param = CreateDynamicParameters(parameters); var param = new DynamicParameters(parameters);
var dbConnection = await GetOrCreateConnectionAsync(cs); var dbConnection = await GetOrCreateConnectionAsync(cs);
var transaction = await GetOrCreateTransactionAsync(dbConnection, cs); var transaction = await GetOrCreateTransactionAsync(dbConnection, cs);
return await dbConnection.ExecuteAsync(sql, param, transaction); 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 ------------------ // ------------------ Dispose ------------------
public void Dispose() public void Dispose()

View file

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

View file

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

View file

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

View file

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

View file

@ -324,16 +324,16 @@
"Url": "/dil/", "Url": "/dil/",
"Method": "GET", "Method": "GET",
"DataSourceCode": "Default", "DataSourceCode": "Default",
"Sql": "SELECT * FROM Sas_H_Language WHERE IsEnabled = @IsEnabled AND CultureName = @CultureName", "Sql": "SELECT * FROM Plat_H_Language WHERE IsEnabled = @IsEnabled AND CultureName = @CultureName",
"ParametersJson": [ "ParametersJson": [
{ {
"Type": "Path", "Type": "P",
"Name": "CultureName", "Name": "CultureName",
"DefaultValue": "ar", "DefaultValue": "ar",
"Path": "/dil/:CultureName/" "Path": "/dil/:CultureName/"
}, },
{ {
"Type": "Static", "Type": "S",
"Name": "IsEnabled", "Name": "IsEnabled",
"DefaultValue": "true" "DefaultValue": "true"
} }
@ -1456,36 +1456,6 @@
"DepartmentName": "Muhasebe", "DepartmentName": "Muhasebe",
"Name": "Muhasebe Şefi", "Name": "Muhasebe Şefi",
"ParentName": "Muhasebe Müdürü" "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": [ "Announcements": [

View file

@ -117,38 +117,6 @@ 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 # sozsoft.com
server { server {
listen 443 ssl http2; listen 443 ssl http2;

View file

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

View file

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

View file

@ -1,19 +1,6 @@
{ {
"commit": "dc293fc", "commit": "0d4703c",
"releases": [ "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", "version": "1.1.03",
"buildDate": "2026-05-30", "buildDate": "2026-05-30",

View file

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

View file

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

View file

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

View file

@ -11,11 +11,6 @@ import { ListResultDto, PagedAndSortedResultRequestDto, PagedResultDto } from '.
import { AuditLogDto } from '../proxy/auditLog/audit-log' import { AuditLogDto } from '../proxy/auditLog/audit-log'
import apiService from './api.service' import apiService from './api.service'
export interface UserAvatarUpdateInput {
userId: string
avatar?: File
}
export const getRoles = (skipCount = 0, maxResultCount = 10) => export const getRoles = (skipCount = 0, maxResultCount = 10) =>
apiService.fetchData<ListResultDto<IdentityRoleDto>>({ apiService.fetchData<ListResultDto<IdentityRoleDto>>({
method: 'GET', method: 'GET',
@ -41,21 +36,6 @@ export const putUserDetail = (input: UserInfoViewModel) =>
data: input, 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) => export const putUserLookout = (input: UserInfoViewModel) =>
apiService.fetchData({ apiService.fetchData({
method: 'PUT', method: 'PUT',

View file

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

View file

@ -328,91 +328,6 @@ export function emptyCriteria(kind = 'Compare', listFormCode = ''): WorkflowCrit
} }
} }
export function uniqueCriteriaTitle(
kind: string,
criteria: Array<Pick<WorkflowCriteriaDto, 'id' | 'kind' | 'title'>>,
currentId?: string | null,
preferredTitle?: string | null,
) {
const hasPreferredTitle = Boolean(preferredTitle?.trim())
const baseTitle = (preferredTitle || defaultTitle(kind)).trim()
const usedTitles = new Set(
criteria
.filter((item) => !currentId || item.id !== currentId)
.map((item) => (item.title || '').trim().toLocaleLowerCase('tr-TR'))
.filter(Boolean),
)
if (!hasPreferredTitle) {
const sameKindCount = criteria.filter(
(item) =>
(!currentId || item.id !== currentId) &&
item.kind === kind &&
isDefaultTitleVariant(item.title, baseTitle),
).length
let index = sameKindCount + 1
let candidate = `${baseTitle}${index}`
while (usedTitles.has(candidate.toLocaleLowerCase('tr-TR'))) {
index += 1
candidate = `${baseTitle}${index}`
}
return candidate
}
if (!usedTitles.has(baseTitle.toLocaleLowerCase('tr-TR'))) {
return baseTitle
}
let index = 1
let candidate = `${baseTitle}${index}`
while (usedTitles.has(candidate.toLocaleLowerCase('tr-TR'))) {
index += 1
candidate = `${baseTitle}${index}`
}
return candidate
}
export function uniqueCriteriaId(
criteria: Array<Pick<WorkflowCriteriaDto, 'id'>>,
reservedIds: string[] = [],
) {
const usedIds = new Set(
[...criteria.map((item) => item.id), ...reservedIds]
.map((id) => (id || '').trim().toLocaleLowerCase('tr-TR'))
.filter(Boolean),
)
const maxNumber = [...usedIds].reduce((max, id) => Math.max(max, parseCriteriaIdNumber(id)), 0)
let nextNumber = maxNumber + 1
let candidate = formatCriteriaId(nextNumber)
while (usedIds.has(candidate.toLocaleLowerCase('tr-TR'))) {
nextNumber += 1
candidate = formatCriteriaId(nextNumber)
}
return candidate
}
function parseCriteriaIdNumber(id: string) {
const match = id.match(/^(?:n)?(\d+)$/iu)
return match ? Number(match[1]) : 0
}
function formatCriteriaId(number: number) {
return `N${String(number).padStart(3, '0')}`.slice(-4)
}
function isDefaultTitleVariant(title: string | null | undefined, baseTitle: string) {
const normalized = (title || '').trim()
return normalized === baseTitle || new RegExp(`^${escapeRegExp(baseTitle)}\\d+$`, 'u').test(normalized)
}
function escapeRegExp(value: string) {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
}
export function toCriteriaForm(item: WorkflowCriteriaDto): WorkflowCriteriaForm { export function toCriteriaForm(item: WorkflowCriteriaDto): WorkflowCriteriaForm {
const sharedPerson = item.approver || '' const sharedPerson = item.approver || ''
@ -432,8 +347,7 @@ export function toCriteriaForm(item: WorkflowCriteriaDto): WorkflowCriteriaForm
} }
export function normalizeCriteria(item: WorkflowCriteriaForm): SaveCriteriaInput { export function normalizeCriteria(item: WorkflowCriteriaForm): SaveCriteriaInput {
const sharedPerson = const sharedPerson = item.approver || ''
item.kind === 'Approval' || item.kind === 'Inform' ? item.approver || '' : ''
const compareOutcomes = (item.compareOutcomes || []) const compareOutcomes = (item.compareOutcomes || [])
.slice(0, 4) .slice(0, 4)
.filter((outcome) => outcome.label?.trim()) .filter((outcome) => outcome.label?.trim())
@ -590,7 +504,7 @@ export function criteriaSummary(item: WorkflowCriteriaDto) {
if (item.kind === 'Approval' || item.kind === 'Inform') { if (item.kind === 'Approval' || item.kind === 'Inform') {
return `${item.title} ${item.approver ? `- ${item.approver}` : ''}` return `${item.title} ${item.approver ? `- ${item.approver}` : ''}`
} }
return item.title return `${item.title} ${item.approver ? `- ${item.approver}` : ''}`
} }
export function targetTitle(criteria: WorkflowCriteriaDto[], id?: string | null) { export function targetTitle(criteria: WorkflowCriteriaDto[], id?: string | null) {

View file

@ -5,16 +5,15 @@ import {
emptyCriteria, emptyCriteria,
normalizeCriteria, normalizeCriteria,
toCriteriaForm, toCriteriaForm,
uniqueCriteriaTitle,
type WorkflowCriteriaForm, type WorkflowCriteriaForm,
} from '@/utils/workflow/workflowHelpers' } from '@/utils/workflow/workflowHelpers'
import { workflowService, type WorkflowCriteriaDto } from '@/services/workflow.service' import { workflowService, type WorkflowCriteriaDto } from '@/services/workflow.service'
import { WorkflowDesigner } from '../workflow/WorkflowDesigner' import { WorkflowDesigner } from '../workflow/WorkflowDesigner'
import { SelectBoxOption } from '@/types/shared' import { SelectBoxOption } from '@/types/shared'
import { Field, FieldProps, Form, Formik } from 'formik' import { Field, FieldProps, Form, Formik } from 'formik'
import { Button, Card, Checkbox, FormContainer, FormItem, Input, Select } from '@/components/ui' import { Button, Card, FormContainer, FormItem, Input, Select } from '@/components/ui'
import { ListFormEditTabs } from '@/proxy/admin/list-form/options' import { ListFormEditTabs } from '@/proxy/admin/list-form/options'
import { bool, object, string } from 'yup' import { object, string } from 'yup'
import { useStoreState } from '@/store/store' import { useStoreState } from '@/store/store'
import { FormEditProps } from './FormEdit' import { FormEditProps } from './FormEdit'
import { useLocalization } from '@/utils/hooks/useLocalization' import { useLocalization } from '@/utils/hooks/useLocalization'
@ -113,17 +112,9 @@ export function FormTabWorkflow(
const saveCriteria = (event: FormEvent<HTMLFormElement>) => { const saveCriteria = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault() event.preventDefault()
runAction(async () => { runAction(async () => {
const normalized = normalizeCriteria(criteriaForm)
const preferredTitle = criteriaForm.title || normalized.title
await workflowService.saveCriteria({ await workflowService.saveCriteria({
...normalized, ...normalizeCriteria(criteriaForm),
listFormCode: props.listFormCode, listFormCode: props.listFormCode,
title: uniqueCriteriaTitle(
normalized.kind || '',
currentCriteria,
normalized.id,
preferredTitle,
),
}) })
setSelectedId('') setSelectedId('')
}) })
@ -132,11 +123,9 @@ export function FormTabWorkflow(
const addCriteria = (kind: string) => { const addCriteria = (kind: string) => {
setDesignerTab('flow') setDesignerTab('flow')
runAction(async () => { runAction(async () => {
const nextTitle = uniqueCriteriaTitle(kind, currentCriteria)
const saved = await workflowService.saveCriteria({ const saved = await workflowService.saveCriteria({
...normalizeCriteria(emptyCriteria(kind, props.listFormCode)), ...normalizeCriteria(emptyCriteria(kind, props.listFormCode)),
listFormCode: props.listFormCode, listFormCode: props.listFormCode,
title: nextTitle,
positionX: 80 + (currentCriteria.length % 5) * 230, positionX: 80 + (currentCriteria.length % 5) * 230,
positionY: 220 + Math.floor(currentCriteria.length / 5) * 140, positionY: 220 + Math.floor(currentCriteria.length / 5) * 140,
}) })
@ -307,13 +296,10 @@ export function FormTabWorkflow(
} }
const schema = object().shape({ const schema = object().shape({
workflowDto: object().shape({
approvalUserFieldName: string().required(), approvalUserFieldName: string().required(),
isFilterUserName: bool(),
approvalStatusFieldName: string().required(), approvalStatusFieldName: string().required(),
approvalDateFieldName: string(), approvalDateFieldName: string(),
approvalDescriptionFieldName: string(), approvalDescriptionFieldName: string(),
}),
}) })
const initialValues = useStoreState((s) => s.admin.lists.values) const initialValues = useStoreState((s) => s.admin.lists.values)
@ -334,7 +320,7 @@ export function FormTabWorkflow(
<Form> <Form>
<FormContainer size="sm"> <FormContainer size="sm">
<Card className="my-2"> <Card className="my-2">
<div className="grid grid-cols-1 md:grid-cols-5 gap-2"> <div className="grid grid-cols-1 md:grid-cols-4 gap-2">
<FormItem <FormItem
asterisk asterisk
label={translate('::ListForms.ListFormEdit.Workflow.ApprovalUserFieldName')} label={translate('::ListForms.ListFormEdit.Workflow.ApprovalUserFieldName')}
@ -435,22 +421,9 @@ export function FormTabWorkflow(
)} )}
</Field> </Field>
</FormItem> </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> </div>
<Button block variant="solid" type="submit" loading={isSubmitting}> <Button block variant="solid" loading={isSubmitting}>
{isSubmitting ? translate('::SavingWithThreeDot') : translate('::Save')} {isSubmitting ? translate('::SavingWithThreeDot') : translate('::Save')}
</Button> </Button>
</Card> </Card>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,4 @@
import { Button, Card, Checkbox, FormContainer, FormItem, Select } from '@/components/ui' import { Button, Card, FormContainer, FormItem, Select } from '@/components/ui'
import type { DatabaseColumnDto } from '@/proxy/sql-query-manager/models' import type { DatabaseColumnDto } from '@/proxy/sql-query-manager/models'
import { ListFormWorkflowCriteriaDto, WorkflowDto } from '@/proxy/form/models' import { ListFormWorkflowCriteriaDto, WorkflowDto } from '@/proxy/form/models'
import { getUsers } from '@/services/identity.service' import { getUsers } from '@/services/identity.service'
@ -9,8 +9,6 @@ import {
emptyCriteria, emptyCriteria,
normalizeCriteria, normalizeCriteria,
toCriteriaForm, toCriteriaForm,
uniqueCriteriaId,
uniqueCriteriaTitle,
type WorkflowCriteriaForm, type WorkflowCriteriaForm,
} from '@/utils/workflow/workflowHelpers' } from '@/utils/workflow/workflowHelpers'
import { Field, FieldProps, Form, Formik } from 'formik' import { Field, FieldProps, Form, Formik } from 'formik'
@ -18,7 +16,6 @@ import { useEffect, useMemo, useRef, useState } from 'react'
import type { FormEvent } from 'react' import type { FormEvent } from 'react'
import { FaArrowLeft, FaArrowRight } from 'react-icons/fa' import { FaArrowLeft, FaArrowRight } from 'react-icons/fa'
import { WorkflowDesigner } from '../workflow/WorkflowDesigner' import { WorkflowDesigner } from '../workflow/WorkflowDesigner'
import { IdentityUserDto } from '@/proxy/admin/models'
type Props = { type Props = {
listFormCode: string listFormCode: string
@ -47,6 +44,8 @@ const toDesignerCriteria = (items: ListFormWorkflowCriteriaDto[]): WorkflowCrite
const toWizardCriteria = (items: WorkflowCriteriaDto[]): ListFormWorkflowCriteriaDto[] => const toWizardCriteria = (items: WorkflowCriteriaDto[]): ListFormWorkflowCriteriaDto[] =>
items.map(({ nodeId: _nodeId, ...item }) => item) items.map(({ nodeId: _nodeId, ...item }) => item)
const nextId = () => `WF${Date.now()}${Math.floor(Math.random() * 1000)}`
function WizardStep6({ function WizardStep6({
listFormCode, listFormCode,
workflow, workflow,
@ -78,7 +77,7 @@ function WizardStep6({
useEffect(() => { useEffect(() => {
getUsers(0, 1000).then((response) => { getUsers(0, 1000).then((response) => {
setUserList( setUserList(
(response.data?.items ?? []).map((user: IdentityUserDto) => ({ (response.data?.items ?? []).map((user: any) => ({
value: user.userName, value: user.userName,
label: `${user.userName} (${user.name} ${user.surname})`, label: `${user.userName} (${user.name} ${user.surname})`,
})), })),
@ -96,10 +95,9 @@ function WizardStep6({
const saveCriteria = (event: FormEvent<HTMLFormElement>) => { const saveCriteria = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault() event.preventDefault()
const normalized = normalizeCriteria({ ...criteriaForm, listFormCode }) const normalized = normalizeCriteria({ ...criteriaForm, listFormCode })
const id = normalized.id || uniqueCriteriaId(currentCriteria) const id = normalized.id || nextId()
const nextItem = { ...normalized, id, nodeId: id } as WorkflowCriteriaDto
const exists = currentCriteria.some((item) => item.id === id) const exists = currentCriteria.some((item) => item.id === id)
const title = uniqueCriteriaTitle(normalized.kind || '', currentCriteria, id, normalized.title)
const nextItem = { ...normalized, id, nodeId: id, title } as WorkflowCriteriaDto
updateCriteria( updateCriteria(
exists exists
? currentCriteria.map((item) => (item.id === id ? nextItem : item)) ? currentCriteria.map((item) => (item.id === id ? nextItem : item))
@ -110,12 +108,11 @@ function WizardStep6({
} }
const addCriteria = (kind: string) => { const addCriteria = (kind: string) => {
const id = uniqueCriteriaId(currentCriteria) const id = nextId()
const nextItem = { const nextItem = {
...normalizeCriteria(emptyCriteria(kind, listFormCode)), ...normalizeCriteria(emptyCriteria(kind, listFormCode)),
id, id,
nodeId: id, nodeId: id,
title: uniqueCriteriaTitle(kind, currentCriteria),
positionX: 80 + (currentCriteria.length % 5) * 230, positionX: 80 + (currentCriteria.length % 5) * 230,
positionY: 220 + Math.floor(currentCriteria.length / 5) * 140, positionY: 220 + Math.floor(currentCriteria.length / 5) * 140,
} as WorkflowCriteriaDto } as WorkflowCriteriaDto
@ -195,15 +192,14 @@ function WizardStep6({
} }
const resetDemo = () => { const resetDemo = () => {
const startId = uniqueCriteriaId([]) const startId = nextId()
const approvalId = uniqueCriteriaId([], [startId]) const approvalId = nextId()
const endId = uniqueCriteriaId([], [startId, approvalId]) const endId = nextId()
updateCriteria([ updateCriteria([
{ {
...normalizeCriteria(emptyCriteria('Start', listFormCode)), ...normalizeCriteria(emptyCriteria('Start', listFormCode)),
id: startId, id: startId,
nodeId: startId, nodeId: startId,
title: uniqueCriteriaTitle('Start', []),
nextOnStart: approvalId, nextOnStart: approvalId,
positionX: 72, positionX: 72,
positionY: 160, positionY: 160,
@ -212,7 +208,6 @@ function WizardStep6({
...normalizeCriteria(emptyCriteria('Approval', listFormCode)), ...normalizeCriteria(emptyCriteria('Approval', listFormCode)),
id: approvalId, id: approvalId,
nodeId: approvalId, nodeId: approvalId,
title: uniqueCriteriaTitle('Approval', []),
nextOnApprove: endId, nextOnApprove: endId,
positionX: 360, positionX: 360,
positionY: 160, positionY: 160,
@ -221,7 +216,6 @@ function WizardStep6({
...normalizeCriteria(emptyCriteria('End', listFormCode)), ...normalizeCriteria(emptyCriteria('End', listFormCode)),
id: endId, id: endId,
nodeId: endId, nodeId: endId,
title: uniqueCriteriaTitle('End', []),
positionX: 650, positionX: 650,
positionY: 160, positionY: 160,
} as WorkflowCriteriaDto, } as WorkflowCriteriaDto,
@ -240,7 +234,7 @@ function WizardStep6({
<Form> <Form>
<Card className="mb-4" header={translate('::ListForms.ListFormEdit.TabWorkflow')}> <Card className="mb-4" header={translate('::ListForms.ListFormEdit.TabWorkflow')}>
<FormContainer> <FormContainer>
<div className="grid grid-cols-1 md:grid-cols-5 gap-4"> <div className="grid grid-cols-4 gap-4">
{[ {[
[ [
'approvalUserFieldName', 'approvalUserFieldName',
@ -283,26 +277,6 @@ function WizardStep6({
</Field> </Field>
</FormItem> </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> </div>
</FormContainer> </FormContainer>
</Card> </Card>

View file

@ -226,13 +226,6 @@ const WizardStep7 = ({
} }
const totalFields = groups.reduce((acc, g) => acc + g.items.length, 0) 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( const hasWorkflowFields = Boolean(
workflow.approvalUserFieldName || workflow.approvalUserFieldName ||
workflow.approvalDateFieldName || workflow.approvalDateFieldName ||
@ -305,157 +298,91 @@ const WizardStep7 = ({
label={translate('::App.Listform.ListformField.ConnectionString')} label={translate('::App.Listform.ListformField.ConnectionString')}
value={values.dataSourceConnectionString} value={values.dataSourceConnectionString}
/> />
<Row
label={translate('::ListForms.Wizard.Step4.CommandType')}
value={
selectCommandTypeOptions.find((o) => o.value === values.selectCommandType)?.label ||
values.selectCommandType
}
/>
<Row <Row
label={translate('::ListForms.Wizard.Step4.SelectCommand')} 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 <Row
label={translate('::ListForms.Wizard.Step4.KeyField')} label={translate('::ListForms.Wizard.Step4.KeyField')}
value={`${values.keyFieldName} (${ value={values.keyFieldName}
/>
<Row
label={translate('::ListForms.Wizard.Step4.KeyFieldType')}
value={
dbSourceTypeOptions.find((o: any) => o.value === values.keyFieldDbSourceType) dbSourceTypeOptions.find((o: any) => o.value === values.keyFieldDbSourceType)
?.label ?? String(values.keyFieldDbSourceType) ?.label ?? String(values.keyFieldDbSourceType)
})`} }
/> />
</Section> </Section>
</div> </div>
<Section <Section
title={ title={translate('::ListForms.Wizard.Step4.SelectedColumns')}
translate('::ListForms.Wizard.Step4.ColumnsAndFormLayout') || 'Columns & Form Layout' badge={selectedColumns.size}
}
badge={`${selectedColumns.size} ${translate('::App.Listform.ListformField.Column')} / ${editingFormFields.length} ${translate('::ListForms.Wizard.Step4.EditingForm') || 'Popup Form'}`}
> >
<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={`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="block px-3 py-2 text-xs text-gray-300 italic">
{translate('::ListForms.Wizard.Step4.NoFields') || 'Alan yok'}
</span>
) : (
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>
</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"> <div className="flex flex-wrap gap-1.5">
{ungroupedSelectedColumns.map((col) => { {[...selectedColumns].map((col) => {
const meta = selectCommandColumns.find((c) => c.columnName === col) const meta = selectCommandColumns.find((c) => c.columnName === col)
return ( return (
<span <span
key={col} 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" 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} {col}
{meta?.dataType && ( {meta?.dataType && (
<span className="text-[10px] text-amber-500 opacity-70"> <span className="text-[10px] text-indigo-400 opacity-70">{meta.dataType}</span>
{meta.dataType}
</span>
)} )}
</span> </span>
) )
})} })}
</div> </div>
</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="grid grid-cols-2 gap-2">
{g.items.length === 0 ? (
<span className="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>
))
)}
</div>
</Section>
))}
</div> </div>
</Section> </Section>
@ -533,7 +460,6 @@ const WizardStep7 = ({
<Row label="Approval Date" value={workflow.approvalDateFieldName} /> <Row label="Approval Date" value={workflow.approvalDateFieldName} />
<Row label="Approval Status" value={workflow.approvalStatusFieldName} /> <Row label="Approval Status" value={workflow.approvalStatusFieldName} />
<Row label="Approval Description" value={workflow.approvalDescriptionFieldName} /> <Row label="Approval Description" value={workflow.approvalDescriptionFieldName} />
<Row label="Is Filter User Name?" value={workflow.isFilterUserName === true ? 'Yes' : 'No'} />
</div> </div>
{workflowItems.length > 0 && ( {workflowItems.length > 0 && (
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
@ -545,18 +471,12 @@ const WizardStep7 = ({
<div className="text-xs font-semibold text-gray-700 dark:text-gray-200"> <div className="text-xs font-semibold text-gray-700 dark:text-gray-200">
{criteria.title || criteria.kind || `Criteria ${index + 1}`} {criteria.title || criteria.kind || `Criteria ${index + 1}`}
</div> </div>
{criteria.kind === 'Compare' && (
<div className="mt-1 text-[11px] text-gray-500 dark:text-gray-400"> <div className="mt-1 text-[11px] text-gray-500 dark:text-gray-400">
{criteria.compareColumn} {criteria.compareOperator}{' '} {criteria.compareColumn} {criteria.compareOperator} {criteria.compareValue}
{criteria.compareValue}
</div> </div>
)}
{(criteria.kind === 'Approval' || criteria.kind === 'Inform') &&
criteria.approver && (
<div className="text-[11px] text-gray-500 dark:text-gray-400"> <div className="text-[11px] text-gray-500 dark:text-gray-400">
Approver: {criteria.approver} Approver: {criteria.approver}
</div> </div>
)}
</div> </div>
))} ))}
</div> </div>
@ -569,14 +489,10 @@ const WizardStep7 = ({
{/* ── Right: Deploy ──────────────────────────────────────────── */} {/* ── Right: Deploy ──────────────────────────────────────────── */}
<div className="sticky top-4 flex flex-col gap-3 max-h-[calc(100vh-200px)]"> <div className="sticky top-4 flex flex-col gap-3 max-h-[calc(100vh-200px)]">
{/* Stats */} {/* Stats */}
<div className="grid grid-cols-7 gap-2"> <div className="grid grid-cols-3 gap-2">
{[ {[
{ label: translate('::ListForms.Wizard.Step4.StatGroup'), value: groups.length }, { label: translate('::ListForms.Wizard.Step4.StatGroup'), value: groups.length },
{ label: translate('::ListForms.Wizard.Step4.StatField'), value: totalFields }, { 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'), label: translate('::App.Listform.ListformField.Column'),
value: selectedColumns.size, value: selectedColumns.size,

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -77,7 +77,6 @@ import { useListFormColumns } from './useListFormColumns'
import { Loading } from '@/components/shared' import { Loading } from '@/components/shared'
import { useStoreState } from '@/store' import { useStoreState } from '@/store'
import { workflowService } from '@/services/workflow.service' import { workflowService } from '@/services/workflow.service'
import { NotePanel } from '../form/notes/NotePanel'
interface GridProps { interface GridProps {
listFormCode: string listFormCode: string
@ -245,7 +244,7 @@ const Grid = (props: GridProps) => {
const { listFormCode, searchParams, isSubForm, level, gridDto: extGridDto } = props const { listFormCode, searchParams, isSubForm, level, gridDto: extGridDto } = props
const { translate } = useLocalization() const { translate } = useLocalization()
const { smaller } = useResponsive() const { smaller } = useResponsive()
const currentUser = useStoreState((state) => state.auth.user) const config = useStoreState((state) => state.abpConfig.config)
const gridRef = useRef<DataGridRef>() const gridRef = useRef<DataGridRef>()
const refListFormCode = useRef('') const refListFormCode = useRef('')
@ -263,10 +262,6 @@ const Grid = (props: GridProps) => {
const [gridDto, setGridDto] = useState<GridDto>() const [gridDto, setGridDto] = useState<GridDto>()
const [isPopupFullScreen, setIsPopupFullScreen] = useState(false) const [isPopupFullScreen, setIsPopupFullScreen] = useState(false)
const [widgetGroupHeight, setWidgetGroupHeight] = useState(0) const [widgetGroupHeight, setWidgetGroupHeight] = useState(0)
const [notePanelTarget, setNotePanelTarget] = useState<{
entityName: string
entityId: string
} | null>(null)
type EditorOptionsWithButtons = { type EditorOptionsWithButtons = {
buttons?: any[] buttons?: any[]
@ -343,29 +338,11 @@ const Grid = (props: GridProps) => {
}) })
const { createSelectDataSource } = useListFormCustomDataSource({ gridRef }) const { createSelectDataSource } = useListFormCustomDataSource({ gridRef })
const openNotePanel = useCallback(
(rowData: Record<string, any>) => {
const keyFieldName = gridDto?.gridOptions.keyFieldName
const entityId = getValueByField(rowData, keyFieldName)
if (entityId === undefined || entityId === null || entityId === '') {
return
}
setNotePanelTarget({
entityName: gridDto?.gridOptions.listFormCode ?? listFormCode,
entityId: String(entityId),
})
},
[gridDto, listFormCode],
)
const { getBandedColumns } = useListFormColumns({ const { getBandedColumns } = useListFormColumns({
gridDto, gridDto,
listFormCode, listFormCode,
isSubForm, isSubForm,
gridRef, gridRef,
onShowNote: openNotePanel,
}) })
const getSelectedRowKeys = useCallback(async () => { const getSelectedRowKeys = useCallback(async () => {
@ -397,10 +374,10 @@ const Grid = (props: GridProps) => {
grd, grd,
gridDto?.gridOptions.workflowDto, gridDto?.gridOptions.workflowDto,
selectedRowsData ?? grd.getSelectedRowsData(), selectedRowsData ?? grd.getSelectedRowsData(),
currentUser, config?.currentUser,
) )
}, },
[currentUser, gridDto], [config?.currentUser, gridDto],
) )
const refreshData = useCallback(() => { const refreshData = useCallback(() => {
@ -1020,7 +997,7 @@ const Grid = (props: GridProps) => {
// Kolonları oluştur - dil değiştiğinde güncelle // Kolonları oluştur - dil değiştiğinde güncelle
const memoizedColumns = useMemo(() => { const memoizedColumns = useMemo(() => {
if (!gridDto) return undefined if (!gridDto || !config) return undefined
const cols = getBandedColumns() const cols = getBandedColumns()
@ -1061,7 +1038,7 @@ const Grid = (props: GridProps) => {
}) })
return cols return cols
}, [gridDto]) }, [gridDto, config])
useEffect(() => { useEffect(() => {
setColumnData(memoizedColumns) setColumnData(memoizedColumns)
@ -1474,27 +1451,7 @@ const Grid = (props: GridProps) => {
) { ) {
workflowService workflowService
.startWorkflow(listFormCode, [insertedKey]) .startWorkflow(listFormCode, [insertedKey])
.then((result) => { .then(() => gridRef.current?.instance()?.refresh())
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) .catch(console.error)
} }
props.refreshData?.() props.refreshData?.()
@ -1917,14 +1874,6 @@ const Grid = (props: GridProps) => {
{toolbarModalData?.content} {toolbarModalData?.content}
</Dialog> </Dialog>
<GridFilterDialogs gridRef={gridRef} listFormCode={listFormCode} {...filterData} /> <GridFilterDialogs gridRef={gridRef} listFormCode={listFormCode} {...filterData} />
{notePanelTarget && (
<NotePanel
entityName={notePanelTarget.entityName}
entityId={notePanelTarget.entityId}
isVisible
onToggle={() => setNotePanelTarget(null)}
/>
)}
</Container> </Container>
</> </>
) )

View file

@ -68,7 +68,6 @@ import { useStoreState } from '@/store/store'
import SubForms from '../form/SubForms' import SubForms from '../form/SubForms'
import { ImportDashboard } from '@/components/importManager/ImportDashboard' import { ImportDashboard } from '@/components/importManager/ImportDashboard'
import { workflowService } from '@/services/workflow.service' import { workflowService } from '@/services/workflow.service'
import { NotePanel } from '../form/notes/NotePanel'
interface TreeProps { interface TreeProps {
listFormCode: string listFormCode: string
@ -233,7 +232,6 @@ const Tree = (props: TreeProps) => {
const { listFormCode, searchParams, isSubForm, level, gridDto: extGridDto } = props const { listFormCode, searchParams, isSubForm, level, gridDto: extGridDto } = props
const { translate } = useLocalization() const { translate } = useLocalization()
const { smaller } = useResponsive() const { smaller } = useResponsive()
const currentUser = useStoreState((state) => state.auth.user)
const gridRef = useRef<TreeListRef>() const gridRef = useRef<TreeListRef>()
const refListFormCode = useRef('') const refListFormCode = useRef('')
@ -251,12 +249,9 @@ const Tree = (props: TreeProps) => {
const [gridDto, setGridDto] = useState<GridDto>() const [gridDto, setGridDto] = useState<GridDto>()
const [isPopupFullScreen, setIsPopupFullScreen] = useState(false) const [isPopupFullScreen, setIsPopupFullScreen] = useState(false)
const [widgetGroupHeight, setWidgetGroupHeight] = useState(0) const [widgetGroupHeight, setWidgetGroupHeight] = useState(0)
const [notePanelTarget, setNotePanelTarget] = useState<{
entityName: string
entityId: string
} | null>(null)
const [expandedRowKeys, setExpandedRowKeys] = useState<any[]>([]) const [expandedRowKeys, setExpandedRowKeys] = useState<any[]>([])
const config = useStoreState((state) => state.abpConfig.config)
type EditorOptionsWithButtons = { type EditorOptionsWithButtons = {
buttons?: any[] buttons?: any[]
@ -348,29 +343,11 @@ const Tree = (props: TreeProps) => {
}) })
const { createSelectDataSource } = useListFormCustomDataSource({ gridRef }) const { createSelectDataSource } = useListFormCustomDataSource({ gridRef })
const openNotePanel = useCallback(
(rowData: Record<string, any>) => {
const keyFieldName = gridDto?.gridOptions.keyFieldName
const entityId = getValueByField(rowData, keyFieldName)
if (entityId === undefined || entityId === null || entityId === '') {
return
}
setNotePanelTarget({
entityName: gridDto?.gridOptions.listFormCode ?? listFormCode,
entityId: String(entityId),
})
},
[gridDto, listFormCode],
)
const { getBandedColumns } = useListFormColumns({ const { getBandedColumns } = useListFormColumns({
gridDto, gridDto,
listFormCode, listFormCode,
isSubForm, isSubForm,
gridRef, gridRef,
onShowNote: openNotePanel,
}) })
const extractSearchParamsFields = useCallback((filter: any): [string, string, any][] => { const extractSearchParamsFields = useCallback((filter: any): [string, string, any][] => {
@ -432,10 +409,10 @@ const Tree = (props: TreeProps) => {
tree, tree,
gridDto?.gridOptions.workflowDto, gridDto?.gridOptions.workflowDto,
selectedRowsData ?? tree.getSelectedRowsData(), selectedRowsData ?? tree.getSelectedRowsData(),
currentUser, config?.currentUser,
) )
}, },
[currentUser, gridDto], [config?.currentUser, gridDto],
) )
const refreshData = useCallback(() => { const refreshData = useCallback(() => {
@ -923,7 +900,7 @@ const Tree = (props: TreeProps) => {
}, [gridDto]) }, [gridDto])
useEffect(() => { useEffect(() => {
if (!gridDto) return if (!gridDto || !config) return
const cols = getBandedColumns() const cols = getBandedColumns()
setColumnData(cols) setColumnData(cols)
@ -936,7 +913,7 @@ const Tree = (props: TreeProps) => {
cols, cols,
) )
setTreeListDataSource(dataSource) setTreeListDataSource(dataSource)
}, [gridDto, searchParams]) }, [gridDto, searchParams, config])
useEffect(() => { useEffect(() => {
const activeFilters = extraFilters.filter((f) => f.value) const activeFilters = extraFilters.filter((f) => f.value)
@ -1133,27 +1110,7 @@ const Tree = (props: TreeProps) => {
) { ) {
workflowService workflowService
.startWorkflow(listFormCode, [insertedKey]) .startWorkflow(listFormCode, [insertedKey])
.then((result) => { .then(() => gridRef.current?.instance()?.refresh())
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) .catch(console.error)
} }
props.refreshData?.() props.refreshData?.()
@ -1656,14 +1613,6 @@ const Tree = (props: TreeProps) => {
{toolbarModalData?.content} {toolbarModalData?.content}
</Dialog> </Dialog>
<GridFilterDialogs gridRef={gridRef as any} listFormCode={listFormCode} {...filterData} /> <GridFilterDialogs gridRef={gridRef as any} listFormCode={listFormCode} {...filterData} />
{notePanelTarget && (
<NotePanel
entityName={notePanelTarget.entityName}
entityId={notePanelTarget.entityId}
isVisible
onToggle={() => setNotePanelTarget(null)}
/>
)}
</Container> </Container>
</> </>
) )

View file

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

View file

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

View file

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