Compare commits
15 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e73cdbbb7a | ||
|
|
233c9b7502 | ||
|
|
12f046f262 | ||
|
|
d0cccde53f | ||
|
|
1d15c44a3d | ||
|
|
bade0bab98 | ||
|
|
c204eef755 | ||
|
|
27e65f05f0 | ||
|
|
64084679e8 | ||
|
|
2f1b9d4e77 | ||
|
|
1c472a7d9a | ||
|
|
119c3650f0 | ||
|
|
ebab6ea114 | ||
|
|
975bc8dd6c | ||
|
|
97a2a4b38d |
73 changed files with 3643 additions and 801 deletions
|
|
@ -0,0 +1,9 @@
|
||||||
|
using Volo.Abp.Application.Dtos;
|
||||||
|
|
||||||
|
namespace Sozsoft.Platform.AuditLogs;
|
||||||
|
|
||||||
|
public class AuditLogListRequestDto : PagedAndSortedResultRequestDto
|
||||||
|
{
|
||||||
|
public string ListFormCode { get; set; }
|
||||||
|
public string EntityId { get; set; }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
using System;
|
||||||
|
using Volo.Abp.Content;
|
||||||
|
|
||||||
|
namespace Sozsoft.Platform.Identity.Dto;
|
||||||
|
|
||||||
|
public class UserAvatarUpdateInput
|
||||||
|
{
|
||||||
|
public Guid UserId { get; set; }
|
||||||
|
|
||||||
|
public IRemoteStreamContent Avatar { get; set; }
|
||||||
|
}
|
||||||
|
|
@ -5,6 +5,7 @@ namespace Sozsoft.Platform.ListForms;
|
||||||
public class WorkflowDto
|
public 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; }
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,7 @@ 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; }
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ 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; }
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace Sozsoft.Platform.ListForms;
|
namespace Sozsoft.Platform.ListForms;
|
||||||
|
|
||||||
public class WorkflowRunResultDto
|
public class WorkflowRunResultDto
|
||||||
|
|
@ -8,5 +10,6 @@ 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; } = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -220,7 +220,7 @@ public static class LookupQueryValues
|
||||||
$"\"Name\" AS \"Name\" " +
|
$"\"Name\" AS \"Name\" " +
|
||||||
$"FROM \"{TableNameResolver.GetFullTableName(nameof(TableNameEnum.JobPosition))}\" " +
|
$"FROM \"{TableNameResolver.GetFullTableName(nameof(TableNameEnum.JobPosition))}\" " +
|
||||||
$"WHERE " +
|
$"WHERE " +
|
||||||
$"(\"DepartmentId\" = @param0 OR @param0 IS NULL) " +
|
$"(\"DepartmentId\" = @param0 OR \"ParentId\" IS NULL OR @param0 IS NULL) " +
|
||||||
$"AND \"IsDeleted\" = 'false' " +
|
$"AND \"IsDeleted\" = 'false' " +
|
||||||
$"ORDER BY \"Name\";";
|
$"ORDER BY \"Name\";";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,43 @@
|
||||||
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>
|
: ICrudAppService<AuditLogDto, Guid, AuditLogListRequestDto>
|
||||||
{
|
{
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[Authorize(AppCodes.IdentityManagement.AuditLogs)]
|
[Authorize(AppCodes.IdentityManagement.AuditLogs)]
|
||||||
public class AuditLogAppService
|
public class AuditLogAppService : CrudAppService<
|
||||||
: CrudAppService<AuditLog, AuditLogDto, Guid>
|
AuditLog,
|
||||||
, IAuditLogAppService
|
AuditLogDto,
|
||||||
|
Guid,
|
||||||
|
AuditLogListRequestDto>, IAuditLogAppService
|
||||||
{
|
{
|
||||||
public AuditLogAppService(IAuditLogRepository auditLogRepository) : base(auditLogRepository)
|
private readonly IRepository<ListForm, Guid> _listFormRepository;
|
||||||
|
|
||||||
|
public AuditLogAppService(
|
||||||
|
IAuditLogRepository auditLogRepository,
|
||||||
|
IRepository<ListForm, Guid> listFormRepository
|
||||||
|
) : base(auditLogRepository)
|
||||||
{
|
{
|
||||||
|
_listFormRepository = listFormRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
public override async Task<AuditLogDto> GetAsync(Guid id)
|
public override async Task<AuditLogDto> GetAsync(Guid id)
|
||||||
|
|
@ -35,23 +48,26 @@ public class AuditLogAppService
|
||||||
}
|
}
|
||||||
|
|
||||||
[UnitOfWork]
|
[UnitOfWork]
|
||||||
public override async Task<PagedResultDto<AuditLogDto>> GetListAsync(PagedAndSortedResultRequestDto input)
|
public override async Task<PagedResultDto<AuditLogDto>> GetListAsync(AuditLogListRequestDto input)
|
||||||
{
|
{
|
||||||
var query = await CreateFilteredQueryAsync(input);
|
var query = await Repository.WithDetailsAsync();
|
||||||
|
|
||||||
|
if (!input.ListFormCode.IsNullOrWhiteSpace())
|
||||||
|
{
|
||||||
|
var filterRules = await GetListFormFilterRulesAsync(input.ListFormCode);
|
||||||
|
query = ApplyAuditLogActionParametersFilter(query, filterRules, input.EntityId);
|
||||||
|
}
|
||||||
|
else if (!input.EntityId.IsNullOrWhiteSpace())
|
||||||
|
{
|
||||||
|
query = query.Where(a => a.Actions.Any(action => action.Parameters.Contains(input.EntityId)));
|
||||||
|
}
|
||||||
|
|
||||||
var totalCount = await AsyncExecuter.CountAsync(query);
|
var totalCount = await AsyncExecuter.CountAsync(query);
|
||||||
|
|
||||||
query = ApplySorting(query, input);
|
query = ApplySorting(query, input);
|
||||||
query = ApplyPaging(query, input);
|
query = ApplyPaging(query, input);
|
||||||
|
|
||||||
// EntityChanges ile birlikte getir (N+1 query önlenir)
|
var auditLogsWithDetails = await AsyncExecuter.ToListAsync(query);
|
||||||
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);
|
||||||
|
|
@ -69,6 +85,102 @@ public class AuditLogAppService
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<List<AuditLogListFormFilterRule>> GetListFormFilterRulesAsync(string listFormCode)
|
||||||
|
{
|
||||||
|
var rules = new List<AuditLogListFormFilterRule>
|
||||||
|
{
|
||||||
|
new(listFormCode, null)
|
||||||
|
};
|
||||||
|
|
||||||
|
var listForm = await _listFormRepository.FirstOrDefaultAsync(a => a.ListFormCode == listFormCode);
|
||||||
|
if (listForm?.SubFormsJson.IsNullOrWhiteSpace() != false)
|
||||||
|
{
|
||||||
|
return rules;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var subForms = JsonSerializer.Deserialize<List<SubFormDto>>(listForm.SubFormsJson) ?? [];
|
||||||
|
foreach (var subForm in subForms.Where(a => !a.Code.IsNullOrWhiteSpace()))
|
||||||
|
{
|
||||||
|
var childFieldNames = subForm.Relation?
|
||||||
|
.Select(a => a.ChildFieldName)
|
||||||
|
.Where(a => !a.IsNullOrWhiteSpace())
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToList() ?? [];
|
||||||
|
|
||||||
|
rules.AddRange(childFieldNames.Select(childFieldName => new AuditLogListFormFilterRule(subForm.Code, childFieldName)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (JsonException)
|
||||||
|
{
|
||||||
|
// Invalid subform JSON should not block audit log listing for the main form.
|
||||||
|
}
|
||||||
|
|
||||||
|
return rules
|
||||||
|
.DistinctBy(a => $"{a.ListFormCode}|{a.ChildFieldName}".ToLowerInvariant())
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IQueryable<AuditLog> ApplyAuditLogActionParametersFilter(
|
||||||
|
IQueryable<AuditLog> query,
|
||||||
|
List<AuditLogListFormFilterRule> rules,
|
||||||
|
string entityId)
|
||||||
|
{
|
||||||
|
var validRules = rules
|
||||||
|
.Where(rule => !rule.ListFormCode.IsNullOrWhiteSpace())
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (validRules.Count == 0)
|
||||||
|
{
|
||||||
|
return query;
|
||||||
|
}
|
||||||
|
|
||||||
|
var auditLog = Expression.Parameter(typeof(AuditLog), "auditLog");
|
||||||
|
var action = Expression.Parameter(typeof(AuditLogAction), "action");
|
||||||
|
var parameters = Expression.Property(action, nameof(AuditLogAction.Parameters));
|
||||||
|
|
||||||
|
Expression actionBody = Expression.Constant(false);
|
||||||
|
foreach (var rule in validRules)
|
||||||
|
{
|
||||||
|
Expression ruleBody = Contains(parameters, rule.ListFormCode);
|
||||||
|
if (!entityId.IsNullOrWhiteSpace())
|
||||||
|
{
|
||||||
|
ruleBody = Expression.AndAlso(ruleBody, Contains(parameters, entityId));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rule.ChildFieldName.IsNullOrWhiteSpace())
|
||||||
|
{
|
||||||
|
ruleBody = Expression.AndAlso(ruleBody, Contains(parameters, rule.ChildFieldName));
|
||||||
|
}
|
||||||
|
|
||||||
|
actionBody = Expression.OrElse(actionBody, ruleBody);
|
||||||
|
}
|
||||||
|
|
||||||
|
var actions = Expression.Property(auditLog, nameof(AuditLog.Actions));
|
||||||
|
var actionPredicate = Expression.Lambda<Func<AuditLogAction, bool>>(actionBody, action);
|
||||||
|
var anyCall = Expression.Call(
|
||||||
|
typeof(Enumerable),
|
||||||
|
nameof(Enumerable.Any),
|
||||||
|
[typeof(AuditLogAction)],
|
||||||
|
actions,
|
||||||
|
actionPredicate);
|
||||||
|
|
||||||
|
var auditLogPredicate = Expression.Lambda<Func<AuditLog, bool>>(anyCall, auditLog);
|
||||||
|
return query.Where(auditLogPredicate);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static MethodCallExpression Contains(MemberExpression source, string value)
|
||||||
|
{
|
||||||
|
return Expression.Call(
|
||||||
|
source,
|
||||||
|
nameof(string.Contains),
|
||||||
|
Type.EmptyTypes,
|
||||||
|
Expression.Constant(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed record AuditLogListFormFilterRule(string ListFormCode, string? ChildFieldName);
|
||||||
|
|
||||||
// Audit Log kayitlarini gormek istiyoruz fakat degistirmek istemiyoruz
|
// 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)
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,12 @@ 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;
|
||||||
|
|
@ -32,6 +34,7 @@ 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,
|
||||||
|
|
@ -45,6 +48,7 @@ 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
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
|
|
@ -55,6 +59,7 @@ 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;
|
||||||
|
|
@ -273,6 +278,22 @@ 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();
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,6 @@ 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;
|
||||||
|
|
@ -60,10 +58,21 @@ 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 titleLangKey = WizardConsts.WizardKeyTitle(wizardName);
|
var code = string.IsNullOrWhiteSpace(input.MenuCode)
|
||||||
var nameLangKey = WizardConsts.WizardKey(wizardName);
|
? WizardConsts.WizardKey(wizardName)
|
||||||
var descLangKey = WizardConsts.WizardKeyDesc(wizardName);
|
: input.MenuCode.Trim();
|
||||||
var code = WizardConsts.WizardKey(wizardName);
|
var listFormCode = string.IsNullOrWhiteSpace(input.ListFormCode)
|
||||||
|
? code
|
||||||
|
: input.ListFormCode.Trim();
|
||||||
|
var titleLangKey = $"{listFormCode}.Title";
|
||||||
|
var nameLangKey = code;
|
||||||
|
var descLangKey = $"{listFormCode}.Desc";
|
||||||
|
var permCreateName = $"{code}.Create";
|
||||||
|
var permUpdateName = $"{code}.Update";
|
||||||
|
var permDeleteName = $"{code}.Delete";
|
||||||
|
var permExportName = $"{code}.Export";
|
||||||
|
var permImportName = $"{code}.Import";
|
||||||
|
var permNoteName = $"{code}.Note";
|
||||||
|
|
||||||
// Eklenen kayıtları takip et (silme işleminde kullanılır)
|
// Eklenen kayıtları takip et (silme işleminde kullanılır)
|
||||||
var inserted = new WizardInsertedRecordsDto();
|
var inserted = new WizardInsertedRecordsDto();
|
||||||
|
|
@ -78,16 +87,15 @@ 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);
|
||||||
await CreateLangKey(groupName, groupName, groupName, inserted);
|
if (string.Equals(groupName, input.MenuParentCode, StringComparison.OrdinalIgnoreCase))
|
||||||
|
await EnsureLangKey(groupName, inserted);
|
||||||
|
else
|
||||||
|
await CreateLangKey(groupName, groupName, groupName, inserted);
|
||||||
inserted.PermissionGroupNames.Add(groupName);
|
inserted.PermissionGroupNames.Add(groupName);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Permission'ları tek seferde kontrol et ve oluştur
|
// Permission'ları tek seferde kontrol et ve oluştur
|
||||||
var queryable = await repoPerm.GetQueryableAsync();
|
var existingPerms = await repoPerm.GetListAsync(a => a.GroupName == groupName);
|
||||||
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)
|
||||||
{
|
{
|
||||||
|
|
@ -95,45 +103,45 @@ public class ListFormWizardAppService(
|
||||||
inserted.PermissionNames.Add(permRead.Name);
|
inserted.PermissionNames.Add(permRead.Name);
|
||||||
}
|
}
|
||||||
|
|
||||||
var permCreate = existingPerms.FirstOrDefault(a => a.Name == WizardConsts.PermCreate(wizardName));
|
var permCreate = existingPerms.FirstOrDefault(a => a.Name == permCreateName);
|
||||||
if (permCreate == null)
|
if (permCreate == null)
|
||||||
{
|
{
|
||||||
permCreate = await repoPerm.InsertAsync(new PermissionDefinitionRecord(Guid.NewGuid(), groupName, WizardConsts.PermCreate(wizardName), permRead.Name, WizardConsts.LangKeyCreate, true, MultiTenancySides.Both), autoSave: false);
|
permCreate = await repoPerm.InsertAsync(new PermissionDefinitionRecord(Guid.NewGuid(), groupName, permCreateName, permRead.Name, WizardConsts.LangKeyCreate, true, MultiTenancySides.Both), autoSave: false);
|
||||||
inserted.PermissionNames.Add(permCreate.Name);
|
inserted.PermissionNames.Add(permCreate.Name);
|
||||||
}
|
}
|
||||||
|
|
||||||
var permUpdate = existingPerms.FirstOrDefault(a => a.Name == WizardConsts.PermUpdate(wizardName));
|
var permUpdate = existingPerms.FirstOrDefault(a => a.Name == permUpdateName);
|
||||||
if (permUpdate == null)
|
if (permUpdate == null)
|
||||||
{
|
{
|
||||||
permUpdate = await repoPerm.InsertAsync(new PermissionDefinitionRecord(Guid.NewGuid(), groupName, WizardConsts.PermUpdate(wizardName), permRead.Name, WizardConsts.LangKeyUpdate, true, MultiTenancySides.Both), autoSave: false);
|
permUpdate = await repoPerm.InsertAsync(new PermissionDefinitionRecord(Guid.NewGuid(), groupName, permUpdateName, permRead.Name, WizardConsts.LangKeyUpdate, true, MultiTenancySides.Both), autoSave: false);
|
||||||
inserted.PermissionNames.Add(permUpdate.Name);
|
inserted.PermissionNames.Add(permUpdate.Name);
|
||||||
}
|
}
|
||||||
|
|
||||||
var permDelete = existingPerms.FirstOrDefault(a => a.Name == WizardConsts.PermDelete(wizardName));
|
var permDelete = existingPerms.FirstOrDefault(a => a.Name == permDeleteName);
|
||||||
if (permDelete == null)
|
if (permDelete == null)
|
||||||
{
|
{
|
||||||
permDelete = await repoPerm.InsertAsync(new PermissionDefinitionRecord(Guid.NewGuid(), groupName, WizardConsts.PermDelete(wizardName), permRead.Name, WizardConsts.LangKeyDelete, true, MultiTenancySides.Both), autoSave: false);
|
permDelete = await repoPerm.InsertAsync(new PermissionDefinitionRecord(Guid.NewGuid(), groupName, permDeleteName, permRead.Name, WizardConsts.LangKeyDelete, true, MultiTenancySides.Both), autoSave: false);
|
||||||
inserted.PermissionNames.Add(permDelete.Name);
|
inserted.PermissionNames.Add(permDelete.Name);
|
||||||
}
|
}
|
||||||
|
|
||||||
var permExport = existingPerms.FirstOrDefault(a => a.Name == WizardConsts.PermExport(wizardName));
|
var permExport = existingPerms.FirstOrDefault(a => a.Name == permExportName);
|
||||||
if (permExport == null)
|
if (permExport == null)
|
||||||
{
|
{
|
||||||
permExport = await repoPerm.InsertAsync(new PermissionDefinitionRecord(Guid.NewGuid(), groupName, WizardConsts.PermExport(wizardName), permRead.Name, WizardConsts.LangKeyExport, true, MultiTenancySides.Both), autoSave: false);
|
permExport = await repoPerm.InsertAsync(new PermissionDefinitionRecord(Guid.NewGuid(), groupName, permExportName, permRead.Name, WizardConsts.LangKeyExport, true, MultiTenancySides.Both), autoSave: false);
|
||||||
inserted.PermissionNames.Add(permExport.Name);
|
inserted.PermissionNames.Add(permExport.Name);
|
||||||
}
|
}
|
||||||
|
|
||||||
var permImport = existingPerms.FirstOrDefault(a => a.Name == WizardConsts.PermImport(wizardName));
|
var permImport = existingPerms.FirstOrDefault(a => a.Name == permImportName);
|
||||||
if (permImport == null)
|
if (permImport == null)
|
||||||
{
|
{
|
||||||
permImport = await repoPerm.InsertAsync(new PermissionDefinitionRecord(Guid.NewGuid(), groupName, WizardConsts.PermImport(wizardName), permRead.Name, WizardConsts.LangKeyImport, true, MultiTenancySides.Both), autoSave: false);
|
permImport = await repoPerm.InsertAsync(new PermissionDefinitionRecord(Guid.NewGuid(), groupName, permImportName, permRead.Name, WizardConsts.LangKeyImport, true, MultiTenancySides.Both), autoSave: false);
|
||||||
inserted.PermissionNames.Add(permImport.Name);
|
inserted.PermissionNames.Add(permImport.Name);
|
||||||
}
|
}
|
||||||
|
|
||||||
var permNote = existingPerms.FirstOrDefault(a => a.Name == WizardConsts.PermNote(wizardName));
|
var permNote = existingPerms.FirstOrDefault(a => a.Name == permNoteName);
|
||||||
if (permNote == null)
|
if (permNote == null)
|
||||||
{
|
{
|
||||||
permNote = await repoPerm.InsertAsync(new PermissionDefinitionRecord(Guid.NewGuid(), groupName, WizardConsts.PermNote(wizardName), permRead.Name, WizardConsts.LangKeyNote, true, MultiTenancySides.Both), autoSave: false);
|
permNote = await repoPerm.InsertAsync(new PermissionDefinitionRecord(Guid.NewGuid(), groupName, permNoteName, permRead.Name, WizardConsts.LangKeyNote, true, MultiTenancySides.Both), autoSave: false);
|
||||||
inserted.PermissionNames.Add(permNote.Name);
|
inserted.PermissionNames.Add(permNote.Name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -161,12 +169,18 @@ 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;
|
||||||
await CreateLangKey(WizardConsts.WizardKeyParent(wizardName), input.LanguageTextMenuParentEn, input.LanguageTextMenuParentTr, inserted);
|
var menuParentIcon = !string.IsNullOrWhiteSpace(input.MenuParentIcon)
|
||||||
|
? input.MenuParentIcon
|
||||||
|
: !string.IsNullOrWhiteSpace(input.MenuIcon)
|
||||||
|
? input.MenuIcon
|
||||||
|
: WizardConsts.MenuIcon;
|
||||||
|
await CreateLangKey(input.MenuParentCode, input.LanguageTextMenuParentEn, input.LanguageTextMenuParentTr, inserted);
|
||||||
menuParent = await repoMenu.InsertAsync(new Menu
|
menuParent = await repoMenu.InsertAsync(new Menu
|
||||||
{
|
{
|
||||||
Code = input.MenuParentCode,
|
Code = input.MenuParentCode,
|
||||||
DisplayName = WizardConsts.WizardKeyParent(wizardName),
|
DisplayName = input.MenuParentCode,
|
||||||
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);
|
||||||
|
|
@ -218,7 +232,7 @@ public class ListFormWizardAppService(
|
||||||
ColSpan = g.ColCount,
|
ColSpan = g.ColCount,
|
||||||
ItemType = "group",
|
ItemType = "group",
|
||||||
Items = g.Items
|
Items = g.Items
|
||||||
.Where(i => i.DataField != input.KeyFieldName)
|
.Where(i => i.IncludeInEditingForm && i.DataField != input.KeyFieldName)
|
||||||
.Select((it, ii) => new EditingFormItemDto
|
.Select((it, ii) => new EditingFormItemDto
|
||||||
{
|
{
|
||||||
Order = ii + 1,
|
Order = ii + 1,
|
||||||
|
|
@ -231,6 +245,7 @@ public class ListFormWizardAppService(
|
||||||
})
|
})
|
||||||
.ToArray()
|
.ToArray()
|
||||||
})
|
})
|
||||||
|
.Where(g => g.Items.Length > 0)
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
//ListForm - varsa sil, yeniden ekle
|
//ListForm - varsa sil, yeniden ekle
|
||||||
|
|
@ -257,14 +272,15 @@ public class ListFormWizardAppService(
|
||||||
var isCreated = tableColumns.Contains("CreatorId");
|
var isCreated = tableColumns.Contains("CreatorId");
|
||||||
input.Workflow ??= new WorkflowDto();
|
input.Workflow ??= new WorkflowDto();
|
||||||
input.Workflow.Criteria = input.WorkflowCriteria;
|
input.Workflow.Criteria = input.WorkflowCriteria;
|
||||||
|
EnsureUniqueWorkflowCriteriaTitles(input.WorkflowCriteria);
|
||||||
|
|
||||||
var listForm = await repoListForm.InsertAsync(new ListForm
|
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,
|
ShowNote = input.SubForms.Count > 0 || input.WorkflowCriteria.Count > 0,
|
||||||
LayoutJson = WizardConsts.DefaultLayoutJson(input.DefaultLayout, input.Grid, input.Pivot, input.Tree, input.Chart, input.Gantt, input.Scheduler),
|
LayoutJson = WizardConsts.DefaultLayoutJson(input.DefaultLayout, input.Grid, input.Pivot, input.Tree, input.Chart, input.Gantt, input.Scheduler),
|
||||||
CultureName = LanguageCodes.En,
|
CultureName = LanguageCodes.En,
|
||||||
ListFormCode = input.ListFormCode,
|
ListFormCode = input.ListFormCode,
|
||||||
|
|
@ -280,12 +296,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 = GridOptions.SortModeSingle,
|
SortMode = PlatformConsts.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,
|
SelectionJson = WizardConsts.DefaultSelectionSingleJson(input.WorkflowCriteria.Count > 0 ? PlatformConsts.GridOptions.SelectionModeSingle : PlatformConsts.GridOptions.SelectionModeNone),
|
||||||
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,
|
||||||
|
|
@ -395,13 +411,65 @@ 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.
|
||||||
|
|
@ -677,4 +745,16 @@ 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,12 @@ 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;
|
||||||
|
|
@ -29,6 +31,7 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
|
||||||
private const string SystemApprovalDescription = "Sistem tarafından otomatik olarak onaylandı.";
|
private const string SystemApprovalDescription = "Sistem tarafından otomatik olarak onaylandı.";
|
||||||
|
|
||||||
private readonly IRepository<ListFormWorkflow, string> criteriaRepository;
|
private readonly IRepository<ListFormWorkflow, string> criteriaRepository;
|
||||||
|
private readonly IRepository<Note, Guid> noteRepository;
|
||||||
private readonly IListFormManager listFormManager;
|
private readonly IListFormManager listFormManager;
|
||||||
private readonly IListFormAuthorizationManager authManager;
|
private readonly IListFormAuthorizationManager authManager;
|
||||||
private readonly IListFormSelectAppService listFormSelectAppService;
|
private readonly IListFormSelectAppService listFormSelectAppService;
|
||||||
|
|
@ -36,18 +39,22 @@ 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;
|
||||||
|
|
@ -55,6 +62,7 @@ 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")]
|
||||||
|
|
@ -188,7 +196,7 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
|
||||||
|
|
||||||
criteria.ListFormCode = code;
|
criteria.ListFormCode = code;
|
||||||
criteria.Kind = NormalizeRequired(input.Kind, "Compare");
|
criteria.Kind = NormalizeRequired(input.Kind, "Compare");
|
||||||
criteria.Title = NormalizeRequired(input.Title, criteria.Kind);
|
criteria.Title = await NormalizeUniqueTitleAsync(code, criteria.Id, input.Kind, input.Title);
|
||||||
criteria.CompareColumn = NormalizeRequired(input.CompareColumn, "Price");
|
criteria.CompareColumn = NormalizeRequired(input.CompareColumn, "Price");
|
||||||
criteria.CompareOperator = NormalizeRequired(input.CompareOperator, ">");
|
criteria.CompareOperator = NormalizeRequired(input.CompareOperator, ">");
|
||||||
criteria.CompareValue = input.CompareValue;
|
criteria.CompareValue = input.CompareValue;
|
||||||
|
|
@ -300,7 +308,14 @@ 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ı.");
|
||||||
|
|
||||||
return await RunUntilWaitAsync(context, start);
|
context.WorkflowNoteRows.Add(("Started By: ", ResolveCurrentUserDisplayName()));
|
||||||
|
var result = await RunUntilWaitAsync(context, start);
|
||||||
|
await InsertWorkflowNoteAsync(
|
||||||
|
context,
|
||||||
|
$"Workflow Started: {start.Title}",
|
||||||
|
BuildWorkflowNoteContent(context.WorkflowNoteRows));
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("decision")]
|
[HttpPost("decision")]
|
||||||
|
|
@ -365,7 +380,15 @@ 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);
|
||||||
return await RunUntilWaitAsync(context, next);
|
context.WorkflowNoteRows.Add(("Decision By: ", ResolveCurrentUserDisplayName()));
|
||||||
|
AddWorkflowDecisionRows(context, current, input.Approved, input.Note ?? string.Empty);
|
||||||
|
var result = await RunUntilWaitAsync(context, next);
|
||||||
|
await InsertWorkflowNoteAsync(
|
||||||
|
context,
|
||||||
|
$"Workflow {(input.Approved ? "Approved" : "Rejected")}: {current.Title}",
|
||||||
|
BuildWorkflowNoteContent(context.WorkflowNoteRows));
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<WorkflowRunResultDto> RunForEachKeyAsync(
|
private async Task<WorkflowRunResultDto> RunForEachKeyAsync(
|
||||||
|
|
@ -387,7 +410,10 @@ 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()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -419,7 +445,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, listForm.KeyFieldName, keys, workflow, criteria, row);
|
return new WorkflowRunContext(code, keys, workflow, criteria, row);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<IDictionary<string, object>> GetRowAsync(string listFormCode, string keyFieldName, object key)
|
private async Task<IDictionary<string, object>> GetRowAsync(string listFormCode, string keyFieldName, object key)
|
||||||
|
|
@ -511,6 +537,8 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
|
||||||
|
|
||||||
await UpdateRowAsync(context, update);
|
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")
|
||||||
{
|
{
|
||||||
|
|
@ -533,7 +561,7 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
|
||||||
recipientEmail,
|
recipientEmail,
|
||||||
sender,
|
sender,
|
||||||
new { },
|
new { },
|
||||||
BuildInformEmailBody(context, node),
|
BuildInformEmailBody(context, node, await BuildPreviousWorkflowNotesHtmlAsync(context)),
|
||||||
$"Workflow Bilgilendirme: {node.Title}",
|
$"Workflow Bilgilendirme: {node.Title}",
|
||||||
null,
|
null,
|
||||||
true);
|
true);
|
||||||
|
|
@ -542,6 +570,7 @@ 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)
|
||||||
|
|
@ -565,22 +594,257 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
|
||||||
return user.Email;
|
return user.Email;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string BuildInformEmailBody(WorkflowRunContext context, ListFormWorkflow node)
|
private async Task<string> BuildPreviousWorkflowNotesHtmlAsync(WorkflowRunContext context)
|
||||||
|
{
|
||||||
|
var key = context.Keys?.FirstOrDefault()?.ToString();
|
||||||
|
if (key.IsNullOrWhiteSpace())
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
var notes = (await noteRepository.GetListAsync(note =>
|
||||||
|
note.EntityName == context.ListFormCode &&
|
||||||
|
note.EntityId == key &&
|
||||||
|
note.Type == "workflow"))
|
||||||
|
.OrderBy(note => note.CreationTime)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (notes.Count == 0)
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
var noteItems = notes.Select(note =>
|
||||||
|
$"""
|
||||||
|
<div style="margin: 0 0 12px 0; padding: 10px 12px; border: 1px solid #e5e7eb; border-radius: 6px;">
|
||||||
|
<div style="font-weight: 600; margin-bottom: 6px;">{Encode(note.Subject)}</div>
|
||||||
|
<div>{note.Content}</div>
|
||||||
|
</div>
|
||||||
|
""");
|
||||||
|
|
||||||
|
return string.Join(string.Empty, noteItems);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BuildInformEmailBody(
|
||||||
|
WorkflowRunContext context,
|
||||||
|
ListFormWorkflow node,
|
||||||
|
string previousWorkflowNotesHtml)
|
||||||
{
|
{
|
||||||
var keyText = string.Join(", ", context.Keys.Select(key => WebUtility.HtmlEncode(key?.ToString() ?? string.Empty)));
|
var 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 $"""
|
||||||
<p>Workflow bilgilendirme adimina ulasildi.</p>
|
<div style="font-family: Arial, sans-serif; color: #111827; line-height: 1.45;">
|
||||||
<table>
|
<p>Workflow sürecinde bilgilendirme adımına ulaşıldı.</p>
|
||||||
<tr><td><strong>Liste Formu</strong></td><td>{listFormCode}</td></tr>
|
<table style="border-collapse: collapse; width: 100%; margin-bottom: 16px;">
|
||||||
<tr><td><strong>Adim</strong></td><td>{nodeTitle}</td></tr>
|
<tr><td style="padding: 6px 8px;"><strong>Liste Formu</strong></td><td style="padding: 6px 8px;">{listFormCode}</td></tr>
|
||||||
<tr><td><strong>Kayit</strong></td><td>{keyText}</td></tr>
|
<tr><td style="padding: 6px 8px;"><strong>Kayıt</strong></td><td style="padding: 6px 8px;">{keyText}</td></tr>
|
||||||
</table>
|
<tr><td style="padding: 6px 8px;"><strong>Bilgilendirme Adımı</strong></td><td style="padding: 6px 8px;">{nodeTitle}</td></tr>
|
||||||
|
<tr><td style="padding: 6px 8px;"><strong>Alıcı</strong></td><td style="padding: 6px 8px;">{recipient}</td></tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h3 style="margin: 18px 0 8px 0;">Bu İşlemdeki Süreç Özeti</h3>
|
||||||
|
{processRows}
|
||||||
|
|
||||||
|
{previousNotesSection}
|
||||||
|
</div>
|
||||||
""";
|
""";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string ResolveCurrentUserDisplayName()
|
||||||
|
{
|
||||||
|
return CurrentUser.UserName
|
||||||
|
?? CurrentUser.Name
|
||||||
|
?? CurrentUser.Id?.ToString()
|
||||||
|
?? "System";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddWorkflowDecisionRows(
|
||||||
|
WorkflowRunContext context,
|
||||||
|
ListFormWorkflow current,
|
||||||
|
bool approved,
|
||||||
|
string description)
|
||||||
|
{
|
||||||
|
var action = approved ? "Approved" : "Rejected";
|
||||||
|
context.WorkflowNoteRows.Add(("Decision: ", $"{action}: {FormatNode(current)}"));
|
||||||
|
context.WorkflowNoteRows.Add(("Description: ", description ?? string.Empty));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddWorkflowNodeRows(
|
||||||
|
WorkflowRunContext context,
|
||||||
|
ListFormWorkflow node)
|
||||||
|
{
|
||||||
|
var action = node.Kind switch
|
||||||
|
{
|
||||||
|
"Start" => "Started",
|
||||||
|
"Compare" => "Evaluated",
|
||||||
|
"Approval" => "Waiting Approval",
|
||||||
|
"Inform" => "Informed",
|
||||||
|
"End" => "Completed",
|
||||||
|
_ => "Processed"
|
||||||
|
};
|
||||||
|
|
||||||
|
context.WorkflowNoteRows.Add(($"{action}: ", $"{FormatNode(node)}"));
|
||||||
|
|
||||||
|
if (!node.Approver.IsNullOrWhiteSpace())
|
||||||
|
{
|
||||||
|
context.WorkflowNoteRows.Add((node.Kind == "Inform" ? "Inform: " : "Approver: ", node.Approver));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddWorkflowToastMessage(
|
||||||
|
WorkflowRunContext context,
|
||||||
|
ListFormWorkflow node)
|
||||||
|
{
|
||||||
|
var userName = ResolveCurrentUserDisplayName();
|
||||||
|
var next = FindNextToastNode(context, node);
|
||||||
|
var messageLines = node.Kind switch
|
||||||
|
{
|
||||||
|
"Start" => new[]
|
||||||
|
{
|
||||||
|
localizer["ListForms.ListForm.Workflow.WorkflowStarted"].Value,
|
||||||
|
localizer["ListForms.ListForm.Workflow.Step", node.Title].Value,
|
||||||
|
localizer["ListForms.ListForm.Workflow.PerformedBy", userName].Value
|
||||||
|
}.Concat(FormatNextToastNode(next)),
|
||||||
|
"Inform" => new[]
|
||||||
|
{
|
||||||
|
localizer["ListForms.ListForm.Workflow.InformReached"].Value,
|
||||||
|
localizer["ListForms.ListForm.Workflow.Step", node.Title].Value,
|
||||||
|
localizer["ListForms.ListForm.Workflow.InformUser", FormatToastUser(node.Approver)].Value
|
||||||
|
}.Concat(FormatNextToastNode(next)),
|
||||||
|
"End" => new[]
|
||||||
|
{
|
||||||
|
localizer["ListForms.ListForm.Workflow.WorkflowCompleted"].Value,
|
||||||
|
localizer["ListForms.ListForm.Workflow.Step", node.Title].Value,
|
||||||
|
localizer["ListForms.ListForm.Workflow.PerformedBy", userName].Value
|
||||||
|
},
|
||||||
|
_ => null
|
||||||
|
};
|
||||||
|
|
||||||
|
if (messageLines != null)
|
||||||
|
{
|
||||||
|
context.ToastMessages.Add(string.Join(Environment.NewLine, messageLines));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ListFormWorkflow FindNextToastNode(
|
||||||
|
WorkflowRunContext context,
|
||||||
|
ListFormWorkflow node)
|
||||||
|
{
|
||||||
|
var current = FindNextCriteria(context.Criteria, ResolveNextNodeId(context, node));
|
||||||
|
var visited = new HashSet<string>();
|
||||||
|
|
||||||
|
while (current != null && visited.Add(current.Id))
|
||||||
|
{
|
||||||
|
if (current.Kind is "Approval" or "Inform" or "End")
|
||||||
|
{
|
||||||
|
return current;
|
||||||
|
}
|
||||||
|
|
||||||
|
current = FindNextCriteria(context.Criteria, ResolveNextNodeId(context, current));
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IEnumerable<string> FormatNextToastNode(ListFormWorkflow node)
|
||||||
|
{
|
||||||
|
if (node == null)
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return node.Kind switch
|
||||||
|
{
|
||||||
|
"Approval" =>
|
||||||
|
[
|
||||||
|
localizer["ListForms.ListForm.Workflow.NextStep.Approval"].Value,
|
||||||
|
localizer["ListForms.ListForm.Workflow.NextStepName", node.Title].Value,
|
||||||
|
localizer["ListForms.ListForm.Workflow.ApproverUser", FormatToastUser(node.Approver)].Value
|
||||||
|
],
|
||||||
|
"Inform" =>
|
||||||
|
[
|
||||||
|
localizer["ListForms.ListForm.Workflow.NextStep.Inform"].Value,
|
||||||
|
localizer["ListForms.ListForm.Workflow.NextStepName", node.Title].Value,
|
||||||
|
localizer["ListForms.ListForm.Workflow.InformUser", FormatToastUser(node.Approver)].Value
|
||||||
|
],
|
||||||
|
"End" =>
|
||||||
|
[
|
||||||
|
localizer["ListForms.ListForm.Workflow.NextStep.End"].Value,
|
||||||
|
localizer["ListForms.ListForm.Workflow.NextStepName", node.Title].Value
|
||||||
|
],
|
||||||
|
_ => [localizer["ListForms.ListForm.Workflow.NextStep", node.Title].Value]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private string FormatToastUser(string userName)
|
||||||
|
{
|
||||||
|
return userName.IsNullOrWhiteSpace()
|
||||||
|
? localizer["ListForms.ListForm.Workflow.UndefinedUser"].Value
|
||||||
|
: userName;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task InsertWorkflowNoteAsync(
|
||||||
|
WorkflowRunContext context,
|
||||||
|
string subject,
|
||||||
|
string content)
|
||||||
|
{
|
||||||
|
var key = context.Keys?.FirstOrDefault();
|
||||||
|
if (key == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var note = new Note(GuidGenerator.Create())
|
||||||
|
{
|
||||||
|
TenantId = CurrentTenant.Id,
|
||||||
|
EntityName = context.ListFormCode,
|
||||||
|
EntityId = key.ToString(),
|
||||||
|
Type = "workflow",
|
||||||
|
Subject = subject,
|
||||||
|
Content = content,
|
||||||
|
FilesJson = "[]"
|
||||||
|
};
|
||||||
|
|
||||||
|
await noteRepository.InsertAsync(note, autoSave: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BuildWorkflowNoteContent(List<(string Label, string Value)> rows)
|
||||||
|
{
|
||||||
|
var tableRows = rows
|
||||||
|
.Where(row => !row.Value.IsNullOrWhiteSpace())
|
||||||
|
.Select(row =>
|
||||||
|
$"<tr><td><strong>{Encode(row.Label)}</strong></td><td>{Encode(row.Value)}</td></tr>");
|
||||||
|
|
||||||
|
return $"<table class=\"workflow-note-log\">{string.Join(string.Empty, tableRows)}</table>";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FormatNode(ListFormWorkflow node)
|
||||||
|
{
|
||||||
|
if (node == null)
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
var title = node.Title ?? string.Empty;
|
||||||
|
var kind = node.Kind ?? string.Empty;
|
||||||
|
return $"{title} ({kind} - {node.Id})";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string Encode(string value)
|
||||||
|
{
|
||||||
|
return WebUtility.HtmlEncode(value ?? string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
private async Task UpdateRowAsync(WorkflowRunContext context, Dictionary<string, object> data)
|
private async Task UpdateRowAsync(WorkflowRunContext context, Dictionary<string, object> data)
|
||||||
{
|
{
|
||||||
await queryManager.GenerateAndRunQueryAsync<int>(
|
await queryManager.GenerateAndRunQueryAsync<int>(
|
||||||
|
|
@ -762,7 +1026,8 @@ 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()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -906,6 +1171,36 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
|
||||||
return value.IsNullOrWhiteSpace() ? fallback : value.Trim();
|
return value.IsNullOrWhiteSpace() ? fallback : value.Trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<string> NormalizeUniqueTitleAsync(
|
||||||
|
string listFormCode,
|
||||||
|
string criteriaId,
|
||||||
|
string kind,
|
||||||
|
string title)
|
||||||
|
{
|
||||||
|
var baseTitle = NormalizeRequired(title, kind);
|
||||||
|
var existingTitles = (await criteriaRepository.GetListAsync(x =>
|
||||||
|
x.ListFormCode == listFormCode &&
|
||||||
|
x.Id != criteriaId))
|
||||||
|
.Select(x => x.Title?.Trim())
|
||||||
|
.Where(x => !x.IsNullOrWhiteSpace())
|
||||||
|
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
if (!existingTitles.Contains(baseTitle))
|
||||||
|
{
|
||||||
|
return baseTitle;
|
||||||
|
}
|
||||||
|
|
||||||
|
var index = 1;
|
||||||
|
var candidate = $"{baseTitle}{index}";
|
||||||
|
while (existingTitles.Contains(candidate))
|
||||||
|
{
|
||||||
|
index++;
|
||||||
|
candidate = $"{baseTitle}{index}";
|
||||||
|
}
|
||||||
|
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
|
||||||
private static string SerializeCompareOutcomes(List<CompareOutcomeDto> outcomes)
|
private static string SerializeCompareOutcomes(List<CompareOutcomeDto> outcomes)
|
||||||
{
|
{
|
||||||
return JsonSerializer.Serialize(outcomes ?? []);
|
return JsonSerializer.Serialize(outcomes ?? []);
|
||||||
|
|
@ -947,12 +1242,14 @@ 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; } = [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,13 +30,15 @@ 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;
|
||||||
|
|
@ -44,6 +46,7 @@ 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";
|
||||||
|
|
@ -275,7 +278,7 @@ public class MenuAppService : CrudAppService<
|
||||||
if (existingEnText != null)
|
if (existingEnText != null)
|
||||||
{
|
{
|
||||||
existingEnText.Value = input.MenuTextEn;
|
existingEnText.Value = input.MenuTextEn;
|
||||||
await _repositoryText.UpdateAsync(existingEnText);
|
await _repositoryText.UpdateAsync(existingEnText, autoSave: true);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -285,7 +288,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.
|
||||||
|
|
@ -297,7 +300,7 @@ public class MenuAppService : CrudAppService<
|
||||||
if (existingTrText != null)
|
if (existingTrText != null)
|
||||||
{
|
{
|
||||||
existingTrText.Value = input.MenuTextTr;
|
existingTrText.Value = input.MenuTextTr;
|
||||||
await _repositoryText.UpdateAsync(existingTrText);
|
await _repositoryText.UpdateAsync(existingTrText, autoSave: true);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -307,9 +310,12 @@ 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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": "False",
|
"defaultValue": "True",
|
||||||
"isVisibleToClients": false,
|
"isVisibleToClients": false,
|
||||||
"providers": "G|D",
|
"providers": "G|D",
|
||||||
"isInherited": false,
|
"isInherited": false,
|
||||||
|
|
@ -1071,6 +1071,22 @@
|
||||||
"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": [],
|
||||||
|
|
|
||||||
|
|
@ -3642,6 +3642,36 @@
|
||||||
"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",
|
||||||
|
|
@ -3735,8 +3765,8 @@
|
||||||
{
|
{
|
||||||
"resourceName": "Platform",
|
"resourceName": "Platform",
|
||||||
"key": "ListForms.ListForm.AddNewRecord",
|
"key": "ListForms.ListForm.AddNewRecord",
|
||||||
"en": "Add New Record",
|
"en": "Add",
|
||||||
"tr": "Yeni Kayıt Ekle"
|
"tr": "Ekle"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"resourceName": "Platform",
|
"resourceName": "Platform",
|
||||||
|
|
@ -4154,9 +4184,9 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"resourceName": "Platform",
|
"resourceName": "Platform",
|
||||||
"key": "ListForms.ListForm.NoteModal.Type.Activity",
|
"key": "ListForms.ListForm.NoteModal.Type.Workflow",
|
||||||
"en": "Activity",
|
"en": "Workflow",
|
||||||
"tr": "Aktivite"
|
"tr": "Akış"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"resourceName": "Platform",
|
"resourceName": "Platform",
|
||||||
|
|
@ -16736,6 +16766,24 @@
|
||||||
"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",
|
||||||
|
|
@ -17474,6 +17522,12 @@
|
||||||
"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",
|
||||||
|
|
@ -17714,6 +17768,12 @@
|
||||||
"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",
|
||||||
|
|
@ -17738,6 +17798,24 @@
|
||||||
"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",
|
||||||
|
|
@ -18038,6 +18116,12 @@
|
||||||
"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",
|
||||||
|
|
@ -18986,6 +19070,12 @@
|
||||||
"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",
|
||||||
|
|
@ -19231,6 +19321,84 @@
|
||||||
"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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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, 730, true, true, true, true, false),
|
EditingOptionJson = DefaultEditingOptionJson(listFormName, 500, 710, 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 },
|
||||||
|
|
|
||||||
|
|
@ -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\"=@Id";
|
return $"UPDATE \"{TableNameResolver.GetFullTableName(tableName)}\" SET \"DeleterId\"=@DeleterId, \"DeletionTime\"=CURRENT_TIMESTAMP, \"IsDeleted\"='true' WHERE \"Id\" IN @Id";
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string DefaultInsertFieldsDefaultValueJson(DbType dbType = DbType.Guid, string newId = "@NEWID") => JsonSerializer.Serialize(new FieldsDefaultValue[]
|
public static string DefaultInsertFieldsDefaultValueJson(DbType dbType = DbType.Guid, string newId = "@NEWID") => JsonSerializer.Serialize(new FieldsDefaultValue[]
|
||||||
|
|
|
||||||
|
|
@ -2302,7 +2302,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
|
||||||
DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(),
|
DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(),
|
||||||
InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(DbType.String, "Name"),
|
InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(DbType.String, "Name"),
|
||||||
PagerOptionJson = DefaultPagerOptionJson,
|
PagerOptionJson = DefaultPagerOptionJson,
|
||||||
EditingOptionJson = DefaultEditingOptionJson(listFormName, 600, 300, true, true, true, false, false),
|
EditingOptionJson = DefaultEditingOptionJson(listFormName, 600, 550, true, true, true, false, false),
|
||||||
EditingFormJson = JsonSerializer.Serialize(new List<EditingFormDto>() {
|
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,8 +4025,8 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
|
||||||
HeaderFilterJson = DefaultHeaderFilterJson,
|
HeaderFilterJson = DefaultHeaderFilterJson,
|
||||||
SearchPanelJson = DefaultSearchPanelJson,
|
SearchPanelJson = DefaultSearchPanelJson,
|
||||||
GroupPanelJson = DefaultGroupPanelJson,
|
GroupPanelJson = DefaultGroupPanelJson,
|
||||||
SelectionJson = DefaultSelectionMultipleJson,
|
SelectionJson = DefaultSelectionSingleJson,
|
||||||
ColumnOptionJson = DefaultColumnOptionJson(false),
|
ColumnOptionJson = DefaultColumnOptionJson(),
|
||||||
PermissionJson = DefaultPermissionJson(listFormName),
|
PermissionJson = DefaultPermissionJson(listFormName),
|
||||||
PagerOptionJson = DefaultPagerOptionJson,
|
PagerOptionJson = DefaultPagerOptionJson,
|
||||||
EditingOptionJson = DefaultEditingOptionJson(listFormName, 950, 650, true, true, true, true, false),
|
EditingOptionJson = DefaultEditingOptionJson(listFormName, 950, 650, true, true, true, true, false),
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
IF OBJECT_ID(N'[dbo].[Sal_T_Approval]', 'U') IS NULL
|
||||||
|
BEGIN
|
||||||
|
CREATE TABLE [dbo].[Sal_T_Approval]
|
||||||
|
(
|
||||||
|
[Id] uniqueidentifier NOT NULL DEFAULT NEWID(),
|
||||||
|
[CreationTime] datetime2 NOT NULL DEFAULT GETUTCDATE(),
|
||||||
|
[CreatorId] uniqueidentifier NULL,
|
||||||
|
[LastModificationTime] datetime2 NULL,
|
||||||
|
[LastModifierId] uniqueidentifier NULL,
|
||||||
|
[IsDeleted] bit NOT NULL DEFAULT 0,
|
||||||
|
[DeletionTime] datetime2 NULL,
|
||||||
|
[DeleterId] uniqueidentifier NULL,
|
||||||
|
[TenantId] uniqueidentifier NULL,
|
||||||
|
[ApprovalUserName] nvarchar(256) NULL,
|
||||||
|
[ApprovalStatus] nvarchar(50) NULL,
|
||||||
|
[ApprovalDate] datetime NULL,
|
||||||
|
[ApprovalDescription] nvarchar(200) NULL,
|
||||||
|
[Name] nvarchar(100) NULL,
|
||||||
|
CONSTRAINT [PK_Sal_T_Approval] PRIMARY KEY NONCLUSTERED
|
||||||
|
(
|
||||||
|
[Id] ASC
|
||||||
|
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON, OPTIMIZE_FOR_SEQUENTIAL_KEY = OFF) ON [PRIMARY]
|
||||||
|
) ON [PRIMARY]
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
|
@ -0,0 +1,385 @@
|
||||||
|
{
|
||||||
|
"Wizard": {
|
||||||
|
"WizardName": "Approval",
|
||||||
|
"ListFormCode": "App.Wizard.Approval",
|
||||||
|
"MenuCode": "App.Wizard.Approval",
|
||||||
|
"IsTenant": true,
|
||||||
|
"IsBranch": false,
|
||||||
|
"IsOrganizationUnit": false,
|
||||||
|
"AllowAdding": true,
|
||||||
|
"AllowUpdating": true,
|
||||||
|
"AllowDeleting": true,
|
||||||
|
"AllowDetail": false,
|
||||||
|
"ConfirmDelete": true,
|
||||||
|
"DefaultLayout": "grid",
|
||||||
|
"Grid": true,
|
||||||
|
"Pivot": true,
|
||||||
|
"Tree": true,
|
||||||
|
"Chart": true,
|
||||||
|
"Gantt": true,
|
||||||
|
"Scheduler": true,
|
||||||
|
"LanguageTextMenuEn": "Approval",
|
||||||
|
"LanguageTextMenuTr": "Approval",
|
||||||
|
"LanguageTextTitleEn": "Approval",
|
||||||
|
"LanguageTextTitleTr": "Onaylama",
|
||||||
|
"LanguageTextDescEn": "Approval",
|
||||||
|
"LanguageTextDescTr": "Onaylama",
|
||||||
|
"LanguageTextMenuParentEn": "Sales",
|
||||||
|
"LanguageTextMenuParentTr": "Sat\u0131\u015F",
|
||||||
|
"PermissionGroupName": "App.Wizard.Sales",
|
||||||
|
"MenuParentCode": "App.Wizard.Sales",
|
||||||
|
"MenuParentIcon": "FcAssistant",
|
||||||
|
"MenuIcon": "FcAndroidOs",
|
||||||
|
"DataSourceCode": "Default",
|
||||||
|
"DataSourceConnectionString": "",
|
||||||
|
"SelectCommandType": 1,
|
||||||
|
"SelectCommand": "Sal_T_Approval",
|
||||||
|
"KeyFieldName": "Id",
|
||||||
|
"KeyFieldDbSourceType": 9,
|
||||||
|
"TreeKeyExpr": "",
|
||||||
|
"TreeParentIdExpr": "",
|
||||||
|
"TreeAutoExpandAll": false,
|
||||||
|
"GanttKeyExpr": "",
|
||||||
|
"GanttParentIdExpr": "",
|
||||||
|
"GanttAutoExpandAll": false,
|
||||||
|
"GanttTitleExpr": "",
|
||||||
|
"GanttStartExpr": "",
|
||||||
|
"GanttEndExpr": "",
|
||||||
|
"GanttProgressExpr": "",
|
||||||
|
"SchedulerTextExpr": "",
|
||||||
|
"SchedulerStartDateExpr": "",
|
||||||
|
"SchedulerEndDateExpr": "",
|
||||||
|
"Groups": [
|
||||||
|
{
|
||||||
|
"Caption": "",
|
||||||
|
"ColCount": 1,
|
||||||
|
"Items": [
|
||||||
|
{
|
||||||
|
"DataField": "Id",
|
||||||
|
"CaptionName": "App.Listform.ListformField.Id",
|
||||||
|
"EditorType": "dxTextBox",
|
||||||
|
"EditorOptions": "",
|
||||||
|
"EditorScript": "",
|
||||||
|
"ColSpan": 1,
|
||||||
|
"IsRequired": true,
|
||||||
|
"IncludeInEditingForm": true,
|
||||||
|
"DbSourceType": 9,
|
||||||
|
"TurkishCaption": "Id",
|
||||||
|
"EnglishCaption": "Id",
|
||||||
|
"LookupDataSourceType": 1,
|
||||||
|
"ValueExpr": "Key",
|
||||||
|
"DisplayExpr": "Name",
|
||||||
|
"LookupQuery": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"DataField": "Name",
|
||||||
|
"CaptionName": "App.Listform.ListformField.Name",
|
||||||
|
"EditorType": "dxTextBox",
|
||||||
|
"EditorOptions": "",
|
||||||
|
"EditorScript": "",
|
||||||
|
"ColSpan": 1,
|
||||||
|
"IsRequired": false,
|
||||||
|
"IncludeInEditingForm": true,
|
||||||
|
"DbSourceType": 16,
|
||||||
|
"TurkishCaption": "Name",
|
||||||
|
"EnglishCaption": "Name",
|
||||||
|
"LookupDataSourceType": 1,
|
||||||
|
"ValueExpr": "Key",
|
||||||
|
"DisplayExpr": "Name",
|
||||||
|
"LookupQuery": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"DataField": "ApprovalUserName",
|
||||||
|
"CaptionName": "App.Listform.ListformField.ApprovalUserName",
|
||||||
|
"EditorType": "dxTextBox",
|
||||||
|
"EditorOptions": "",
|
||||||
|
"EditorScript": "",
|
||||||
|
"ColSpan": 1,
|
||||||
|
"IsRequired": false,
|
||||||
|
"IncludeInEditingForm": false,
|
||||||
|
"DbSourceType": 16,
|
||||||
|
"TurkishCaption": "Approval User Name",
|
||||||
|
"EnglishCaption": "Approval User Name",
|
||||||
|
"LookupDataSourceType": 1,
|
||||||
|
"ValueExpr": "Key",
|
||||||
|
"DisplayExpr": "Name",
|
||||||
|
"LookupQuery": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"DataField": "ApprovalStatus",
|
||||||
|
"CaptionName": "App.Listform.ListformField.ApprovalStatus",
|
||||||
|
"EditorType": "dxTextBox",
|
||||||
|
"EditorOptions": "",
|
||||||
|
"EditorScript": "",
|
||||||
|
"ColSpan": 1,
|
||||||
|
"IsRequired": false,
|
||||||
|
"IncludeInEditingForm": false,
|
||||||
|
"DbSourceType": 16,
|
||||||
|
"TurkishCaption": "Approval Status",
|
||||||
|
"EnglishCaption": "Approval Status",
|
||||||
|
"LookupDataSourceType": 1,
|
||||||
|
"ValueExpr": "Key",
|
||||||
|
"DisplayExpr": "Name",
|
||||||
|
"LookupQuery": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"DataField": "ApprovalDate",
|
||||||
|
"CaptionName": "App.Listform.ListformField.ApprovalDate",
|
||||||
|
"EditorType": "dxDateBox",
|
||||||
|
"EditorOptions": "",
|
||||||
|
"EditorScript": "",
|
||||||
|
"ColSpan": 1,
|
||||||
|
"IsRequired": false,
|
||||||
|
"IncludeInEditingForm": false,
|
||||||
|
"DbSourceType": 6,
|
||||||
|
"TurkishCaption": "Approval Date",
|
||||||
|
"EnglishCaption": "Approval Date",
|
||||||
|
"LookupDataSourceType": 1,
|
||||||
|
"ValueExpr": "Key",
|
||||||
|
"DisplayExpr": "Name",
|
||||||
|
"LookupQuery": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"DataField": "ApprovalDescription",
|
||||||
|
"CaptionName": "App.Listform.ListformField.ApprovalDescription",
|
||||||
|
"EditorType": "dxTextBox",
|
||||||
|
"EditorOptions": "",
|
||||||
|
"EditorScript": "",
|
||||||
|
"ColSpan": 1,
|
||||||
|
"IsRequired": false,
|
||||||
|
"IncludeInEditingForm": false,
|
||||||
|
"DbSourceType": 16,
|
||||||
|
"TurkishCaption": "Approval Description",
|
||||||
|
"EnglishCaption": "Approval Description",
|
||||||
|
"LookupDataSourceType": 1,
|
||||||
|
"ValueExpr": "Key",
|
||||||
|
"DisplayExpr": "Name",
|
||||||
|
"LookupQuery": ""
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"SubForms": [],
|
||||||
|
"Widgets": [],
|
||||||
|
"Workflow": {
|
||||||
|
"ApprovalUserFieldName": "ApprovalUserName",
|
||||||
|
"IsFilterUserName": true,
|
||||||
|
"ApprovalDateFieldName": "ApprovalDate",
|
||||||
|
"ApprovalStatusFieldName": "ApprovalStatus",
|
||||||
|
"ApprovalDescriptionFieldName": "ApprovalDescription",
|
||||||
|
"Criteria": [
|
||||||
|
{
|
||||||
|
"ListFormCode": "App.Wizard.Approval",
|
||||||
|
"Kind": "Start",
|
||||||
|
"Title": "\u0130\u015F Ak\u0131\u015F\u0131 Ba\u015Flat1",
|
||||||
|
"CompareColumn": "Price",
|
||||||
|
"CompareOperator": "\u003E",
|
||||||
|
"CompareValue": 5000,
|
||||||
|
"Approver": "",
|
||||||
|
"NextOnStart": "N002",
|
||||||
|
"NextOnTrue": "",
|
||||||
|
"NextOnFalse": "",
|
||||||
|
"NextOnApprove": "",
|
||||||
|
"NextOnReject": "",
|
||||||
|
"PositionX": 34,
|
||||||
|
"PositionY": 104,
|
||||||
|
"CompareOutcomes": [],
|
||||||
|
"Id": "N001"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ListFormCode": "App.Wizard.Approval",
|
||||||
|
"Kind": "Approval",
|
||||||
|
"Title": "Onay1",
|
||||||
|
"CompareColumn": "Price",
|
||||||
|
"CompareOperator": "\u003E",
|
||||||
|
"CompareValue": 5000,
|
||||||
|
"Approver": "admin@sozsoft.com",
|
||||||
|
"NextOnStart": "",
|
||||||
|
"NextOnTrue": "",
|
||||||
|
"NextOnFalse": "",
|
||||||
|
"NextOnApprove": "N003",
|
||||||
|
"NextOnReject": "N004",
|
||||||
|
"PositionX": 323,
|
||||||
|
"PositionY": 104,
|
||||||
|
"CompareOutcomes": [],
|
||||||
|
"Id": "N002"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ListFormCode": "App.Wizard.Approval",
|
||||||
|
"Kind": "Approval",
|
||||||
|
"Title": "Onay2",
|
||||||
|
"CompareColumn": "Price",
|
||||||
|
"CompareOperator": "\u003E",
|
||||||
|
"CompareValue": 5000,
|
||||||
|
"Approver": "demo@sozsoft.com",
|
||||||
|
"NextOnStart": "",
|
||||||
|
"NextOnTrue": "",
|
||||||
|
"NextOnFalse": "",
|
||||||
|
"NextOnApprove": "N004",
|
||||||
|
"NextOnReject": "N004",
|
||||||
|
"PositionX": 586,
|
||||||
|
"PositionY": 104,
|
||||||
|
"CompareOutcomes": [],
|
||||||
|
"Id": "N003"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ListFormCode": "App.Wizard.Approval",
|
||||||
|
"Kind": "Inform",
|
||||||
|
"Title": "Bilgilendirme1",
|
||||||
|
"CompareColumn": "Price",
|
||||||
|
"CompareOperator": "\u003E",
|
||||||
|
"CompareValue": 5000,
|
||||||
|
"Approver": "system@sozsoft.com",
|
||||||
|
"NextOnStart": "N005",
|
||||||
|
"NextOnTrue": "",
|
||||||
|
"NextOnFalse": "",
|
||||||
|
"NextOnApprove": "",
|
||||||
|
"NextOnReject": "",
|
||||||
|
"PositionX": 458,
|
||||||
|
"PositionY": 411,
|
||||||
|
"CompareOutcomes": [],
|
||||||
|
"Id": "N004"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ListFormCode": "App.Wizard.Approval",
|
||||||
|
"Kind": "End",
|
||||||
|
"Title": "\u0130\u015F Ak\u0131\u015F\u0131 Bitir1",
|
||||||
|
"CompareColumn": "Price",
|
||||||
|
"CompareOperator": "\u003E",
|
||||||
|
"CompareValue": 5000,
|
||||||
|
"Approver": "",
|
||||||
|
"NextOnStart": "",
|
||||||
|
"NextOnTrue": "",
|
||||||
|
"NextOnFalse": "",
|
||||||
|
"NextOnApprove": "",
|
||||||
|
"NextOnReject": "",
|
||||||
|
"PositionX": 792,
|
||||||
|
"PositionY": 412,
|
||||||
|
"CompareOutcomes": [],
|
||||||
|
"Id": "N005"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"WorkflowCriteria": [
|
||||||
|
{
|
||||||
|
"ListFormCode": "App.Wizard.Approval",
|
||||||
|
"Kind": "Start",
|
||||||
|
"Title": "\u0130\u015F Ak\u0131\u015F\u0131 Ba\u015Flat1",
|
||||||
|
"CompareColumn": "Price",
|
||||||
|
"CompareOperator": "\u003E",
|
||||||
|
"CompareValue": 5000,
|
||||||
|
"Approver": "",
|
||||||
|
"NextOnStart": "N002",
|
||||||
|
"NextOnTrue": "",
|
||||||
|
"NextOnFalse": "",
|
||||||
|
"NextOnApprove": "",
|
||||||
|
"NextOnReject": "",
|
||||||
|
"PositionX": 34,
|
||||||
|
"PositionY": 104,
|
||||||
|
"CompareOutcomes": [],
|
||||||
|
"Id": "N001"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ListFormCode": "App.Wizard.Approval",
|
||||||
|
"Kind": "Approval",
|
||||||
|
"Title": "Onay1",
|
||||||
|
"CompareColumn": "Price",
|
||||||
|
"CompareOperator": "\u003E",
|
||||||
|
"CompareValue": 5000,
|
||||||
|
"Approver": "admin@sozsoft.com",
|
||||||
|
"NextOnStart": "",
|
||||||
|
"NextOnTrue": "",
|
||||||
|
"NextOnFalse": "",
|
||||||
|
"NextOnApprove": "N003",
|
||||||
|
"NextOnReject": "N004",
|
||||||
|
"PositionX": 323,
|
||||||
|
"PositionY": 104,
|
||||||
|
"CompareOutcomes": [],
|
||||||
|
"Id": "N002"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ListFormCode": "App.Wizard.Approval",
|
||||||
|
"Kind": "Approval",
|
||||||
|
"Title": "Onay2",
|
||||||
|
"CompareColumn": "Price",
|
||||||
|
"CompareOperator": "\u003E",
|
||||||
|
"CompareValue": 5000,
|
||||||
|
"Approver": "demo@sozsoft.com",
|
||||||
|
"NextOnStart": "",
|
||||||
|
"NextOnTrue": "",
|
||||||
|
"NextOnFalse": "",
|
||||||
|
"NextOnApprove": "N004",
|
||||||
|
"NextOnReject": "N004",
|
||||||
|
"PositionX": 586,
|
||||||
|
"PositionY": 104,
|
||||||
|
"CompareOutcomes": [],
|
||||||
|
"Id": "N003"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ListFormCode": "App.Wizard.Approval",
|
||||||
|
"Kind": "Inform",
|
||||||
|
"Title": "Bilgilendirme1",
|
||||||
|
"CompareColumn": "Price",
|
||||||
|
"CompareOperator": "\u003E",
|
||||||
|
"CompareValue": 5000,
|
||||||
|
"Approver": "system@sozsoft.com",
|
||||||
|
"NextOnStart": "N005",
|
||||||
|
"NextOnTrue": "",
|
||||||
|
"NextOnFalse": "",
|
||||||
|
"NextOnApprove": "",
|
||||||
|
"NextOnReject": "",
|
||||||
|
"PositionX": 458,
|
||||||
|
"PositionY": 411,
|
||||||
|
"CompareOutcomes": [],
|
||||||
|
"Id": "N004"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ListFormCode": "App.Wizard.Approval",
|
||||||
|
"Kind": "End",
|
||||||
|
"Title": "\u0130\u015F Ak\u0131\u015F\u0131 Bitir1",
|
||||||
|
"CompareColumn": "Price",
|
||||||
|
"CompareOperator": "\u003E",
|
||||||
|
"CompareValue": 5000,
|
||||||
|
"Approver": "",
|
||||||
|
"NextOnStart": "",
|
||||||
|
"NextOnTrue": "",
|
||||||
|
"NextOnFalse": "",
|
||||||
|
"NextOnApprove": "",
|
||||||
|
"NextOnReject": "",
|
||||||
|
"PositionX": 792,
|
||||||
|
"PositionY": 412,
|
||||||
|
"CompareOutcomes": [],
|
||||||
|
"Id": "N005"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"IsDeletedField": true,
|
||||||
|
"IsCreatedField": true,
|
||||||
|
"InsertedRecords": {
|
||||||
|
"LanguageKeys": [
|
||||||
|
"App.Wizard.Approval",
|
||||||
|
"App.Wizard.Approval.Title",
|
||||||
|
"App.Wizard.Approval.Desc",
|
||||||
|
"App.Listform.ListformField.ApprovalUserName",
|
||||||
|
"App.Listform.ListformField.ApprovalStatus",
|
||||||
|
"App.Listform.ListformField.ApprovalDate",
|
||||||
|
"App.Listform.ListformField.ApprovalDescription"
|
||||||
|
],
|
||||||
|
"PermissionGroupNames": [
|
||||||
|
"App.Wizard.Sales"
|
||||||
|
],
|
||||||
|
"PermissionNames": [
|
||||||
|
"App.Wizard.Approval",
|
||||||
|
"App.Wizard.Approval.Create",
|
||||||
|
"App.Wizard.Approval.Update",
|
||||||
|
"App.Wizard.Approval.Delete",
|
||||||
|
"App.Wizard.Approval.Export",
|
||||||
|
"App.Wizard.Approval.Import",
|
||||||
|
"App.Wizard.Approval.Note"
|
||||||
|
],
|
||||||
|
"MenuCodes": [
|
||||||
|
"App.Wizard.Approval"
|
||||||
|
],
|
||||||
|
"DataSourceCodes": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -36,6 +36,7 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
|
||||||
private readonly IRepository<DataSource, Guid> _repoDataSource;
|
private readonly IRepository<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;
|
||||||
|
|
@ -51,6 +52,7 @@ 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;
|
||||||
|
|
@ -62,6 +64,7 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
|
||||||
_repoDataSource = repoDataSource;
|
_repoDataSource = repoDataSource;
|
||||||
_repoListForm = repoListForm;
|
_repoListForm = repoListForm;
|
||||||
_repoListFormField = repoListFormField;
|
_repoListFormField = repoListFormField;
|
||||||
|
_repoListFormWorkflow = repoListFormWorkflow;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -134,13 +137,29 @@ 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 titleLangKey = WizardConsts.WizardKeyTitle(wizardName);
|
var code = string.IsNullOrWhiteSpace(input.MenuCode)
|
||||||
var nameLangKey = WizardConsts.WizardKey(wizardName);
|
? WizardConsts.WizardKey(wizardName)
|
||||||
var descLangKey = WizardConsts.WizardKeyDesc(wizardName);
|
: input.MenuCode.Trim();
|
||||||
var code = WizardConsts.WizardKey(wizardName);
|
var listFormCode = string.IsNullOrWhiteSpace(input.ListFormCode)
|
||||||
|
? code
|
||||||
|
: input.ListFormCode.Trim();
|
||||||
|
var titleLangKey = $"{listFormCode}.Title";
|
||||||
|
var nameLangKey = code;
|
||||||
|
var descLangKey = $"{listFormCode}.Desc";
|
||||||
|
var permCreateName = $"{code}.Create";
|
||||||
|
var permUpdateName = $"{code}.Update";
|
||||||
|
var permDeleteName = $"{code}.Delete";
|
||||||
|
var permExportName = $"{code}.Export";
|
||||||
|
var permImportName = $"{code}.Import";
|
||||||
|
var permNoteName = $"{code}.Note";
|
||||||
|
|
||||||
// Dil - Language Keys
|
// Dil - Language Keys
|
||||||
await CreateLangKeyAsync(nameLangKey, input.LanguageTextMenuEn, input.LanguageTextMenuTr);
|
await CreateLangKeyAsync(nameLangKey, input.LanguageTextMenuEn, input.LanguageTextMenuTr);
|
||||||
|
|
@ -153,7 +172,10 @@ 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);
|
||||||
await CreateLangKeyAsync(groupName, groupName, groupName);
|
if (string.Equals(groupName, input.MenuParentCode, StringComparison.OrdinalIgnoreCase))
|
||||||
|
await EnsureLangKeyAsync(groupName);
|
||||||
|
else
|
||||||
|
await CreateLangKeyAsync(groupName, groupName, groupName);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Permissions - tek seferde mevcut permission'ları çek, sonra her birini kontrol et
|
// Permissions - tek seferde mevcut permission'ları çek, sonra her birini kontrol et
|
||||||
|
|
@ -165,35 +187,35 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
|
||||||
permRead = await _repoPerm.InsertAsync(new PermissionDefinitionRecord(
|
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 == WizardConsts.PermCreate(wizardName));
|
var permCreate = existingPerms.FirstOrDefault(a => a.Name == permCreateName);
|
||||||
if (permCreate == null)
|
if (permCreate == null)
|
||||||
permCreate = await _repoPerm.InsertAsync(new PermissionDefinitionRecord(
|
permCreate = await _repoPerm.InsertAsync(new PermissionDefinitionRecord(
|
||||||
Guid.NewGuid(), groupName, WizardConsts.PermCreate(wizardName), permRead.Name, WizardConsts.LangKeyCreate, true, MultiTenancySides.Both), autoSave: true);
|
Guid.NewGuid(), groupName, permCreateName, permRead.Name, WizardConsts.LangKeyCreate, true, MultiTenancySides.Both), autoSave: true);
|
||||||
|
|
||||||
var permUpdate = existingPerms.FirstOrDefault(a => a.Name == WizardConsts.PermUpdate(wizardName));
|
var permUpdate = existingPerms.FirstOrDefault(a => a.Name == permUpdateName);
|
||||||
if (permUpdate == null)
|
if (permUpdate == null)
|
||||||
permUpdate = await _repoPerm.InsertAsync(new PermissionDefinitionRecord(
|
permUpdate = await _repoPerm.InsertAsync(new PermissionDefinitionRecord(
|
||||||
Guid.NewGuid(), groupName, WizardConsts.PermUpdate(wizardName), permRead.Name, WizardConsts.LangKeyUpdate, true, MultiTenancySides.Both), autoSave: true);
|
Guid.NewGuid(), groupName, permUpdateName, permRead.Name, WizardConsts.LangKeyUpdate, true, MultiTenancySides.Both), autoSave: true);
|
||||||
|
|
||||||
var permDelete = existingPerms.FirstOrDefault(a => a.Name == WizardConsts.PermDelete(wizardName));
|
var permDelete = existingPerms.FirstOrDefault(a => a.Name == permDeleteName);
|
||||||
if (permDelete == null)
|
if (permDelete == null)
|
||||||
permDelete = await _repoPerm.InsertAsync(new PermissionDefinitionRecord(
|
permDelete = await _repoPerm.InsertAsync(new PermissionDefinitionRecord(
|
||||||
Guid.NewGuid(), groupName, WizardConsts.PermDelete(wizardName), permRead.Name, WizardConsts.LangKeyDelete, true, MultiTenancySides.Both), autoSave: true);
|
Guid.NewGuid(), groupName, permDeleteName, permRead.Name, WizardConsts.LangKeyDelete, true, MultiTenancySides.Both), autoSave: true);
|
||||||
|
|
||||||
var permExport = existingPerms.FirstOrDefault(a => a.Name == WizardConsts.PermExport(wizardName));
|
var permExport = existingPerms.FirstOrDefault(a => a.Name == permExportName);
|
||||||
if (permExport == null)
|
if (permExport == null)
|
||||||
permExport = await _repoPerm.InsertAsync(new PermissionDefinitionRecord(
|
permExport = await _repoPerm.InsertAsync(new PermissionDefinitionRecord(
|
||||||
Guid.NewGuid(), groupName, WizardConsts.PermExport(wizardName), permRead.Name, WizardConsts.LangKeyExport, true, MultiTenancySides.Both), autoSave: true);
|
Guid.NewGuid(), groupName, permExportName, permRead.Name, WizardConsts.LangKeyExport, true, MultiTenancySides.Both), autoSave: true);
|
||||||
|
|
||||||
var permImport = existingPerms.FirstOrDefault(a => a.Name == WizardConsts.PermImport(wizardName));
|
var permImport = existingPerms.FirstOrDefault(a => a.Name == permImportName);
|
||||||
if (permImport == null)
|
if (permImport == null)
|
||||||
permImport = await _repoPerm.InsertAsync(new PermissionDefinitionRecord(
|
permImport = await _repoPerm.InsertAsync(new PermissionDefinitionRecord(
|
||||||
Guid.NewGuid(), groupName, WizardConsts.PermImport(wizardName), permRead.Name, WizardConsts.LangKeyImport, true, MultiTenancySides.Both), autoSave: true);
|
Guid.NewGuid(), groupName, permImportName, permRead.Name, WizardConsts.LangKeyImport, true, MultiTenancySides.Both), autoSave: true);
|
||||||
|
|
||||||
var permNote = existingPerms.FirstOrDefault(a => a.Name == WizardConsts.PermNote(wizardName));
|
var permNote = existingPerms.FirstOrDefault(a => a.Name == permNoteName);
|
||||||
if (permNote == null)
|
if (permNote == null)
|
||||||
permNote = await _repoPerm.InsertAsync(new PermissionDefinitionRecord(
|
permNote = await _repoPerm.InsertAsync(new PermissionDefinitionRecord(
|
||||||
Guid.NewGuid(), groupName, WizardConsts.PermNote(wizardName), permRead.Name, WizardConsts.LangKeyNote, true, MultiTenancySides.Both), autoSave: true);
|
Guid.NewGuid(), groupName, permNoteName, permRead.Name, WizardConsts.LangKeyNote, true, MultiTenancySides.Both), autoSave: true);
|
||||||
|
|
||||||
// // Permission Grants - Admin role için, sadece eksik olanları ekle
|
// // 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);
|
||||||
|
|
@ -219,12 +241,18 @@ 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;
|
||||||
await CreateLangKeyAsync(WizardConsts.WizardKeyParent(wizardName), input.LanguageTextMenuParentEn, input.LanguageTextMenuParentTr);
|
var menuParentIcon = !string.IsNullOrWhiteSpace(input.MenuParentIcon)
|
||||||
|
? input.MenuParentIcon
|
||||||
|
: !string.IsNullOrWhiteSpace(input.MenuIcon)
|
||||||
|
? input.MenuIcon
|
||||||
|
: WizardConsts.MenuIcon;
|
||||||
|
await CreateLangKeyAsync(input.MenuParentCode, input.LanguageTextMenuParentEn, input.LanguageTextMenuParentTr);
|
||||||
menuParent = await _repoMenu.InsertAsync(new Menu
|
menuParent = await _repoMenu.InsertAsync(new Menu
|
||||||
{
|
{
|
||||||
Code = input.MenuParentCode,
|
Code = input.MenuParentCode,
|
||||||
DisplayName = WizardConsts.WizardKeyParent(wizardName),
|
DisplayName = input.MenuParentCode,
|
||||||
IsDisabled = false,
|
IsDisabled = false,
|
||||||
|
Icon = menuParentIcon,
|
||||||
Order = maxRootOrder + 1,
|
Order = maxRootOrder + 1,
|
||||||
}, autoSave: true);
|
}, autoSave: true);
|
||||||
}
|
}
|
||||||
|
|
@ -276,7 +304,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.DataField != input.KeyFieldName)
|
.Where(i => i.IncludeInEditingForm && i.DataField != input.KeyFieldName)
|
||||||
.Select((it, ii) => new EditingFormItemDto
|
.Select((it, ii) => new EditingFormItemDto
|
||||||
{
|
{
|
||||||
Order = ii + 1,
|
Order = ii + 1,
|
||||||
|
|
@ -289,6 +317,7 @@ 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
|
||||||
|
|
@ -304,6 +333,12 @@ 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
|
||||||
{
|
{
|
||||||
|
|
@ -311,7 +346,7 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
|
||||||
PageSize = 10,
|
PageSize = 10,
|
||||||
ExportJson = WizardConsts.DefaultExportJson,
|
ExportJson = WizardConsts.DefaultExportJson,
|
||||||
IsSubForm = false,
|
IsSubForm = false,
|
||||||
ShowNote = input.SubForms.Count > 0,
|
ShowNote = input.SubForms.Count > 0 || input.WorkflowCriteria.Count > 0,
|
||||||
LayoutJson = WizardConsts.DefaultLayoutJson(input.DefaultLayout, input.Grid, input.Pivot, input.Tree, input.Chart, input.Gantt, input.Scheduler),
|
LayoutJson = WizardConsts.DefaultLayoutJson(input.DefaultLayout, input.Grid, input.Pivot, input.Tree, input.Chart, input.Gantt, input.Scheduler),
|
||||||
CultureName = LanguageCodes.En,
|
CultureName = LanguageCodes.En,
|
||||||
ListFormCode = input.ListFormCode,
|
ListFormCode = input.ListFormCode,
|
||||||
|
|
@ -332,7 +367,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,
|
SelectionJson = WizardConsts.DefaultSelectionSingleJson(input.WorkflowCriteria.Count > 0 ? GridOptions.SelectionModeSingle : GridOptions.SelectionModeNone),
|
||||||
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,
|
||||||
|
|
@ -381,6 +416,34 @@ 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)
|
||||||
|
|
@ -394,6 +457,60 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void EnsureUniqueWorkflowCriteriaTitles(List<ListFormWorkflowCriteriaDto> criteria)
|
||||||
|
{
|
||||||
|
if (criteria == null || criteria.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var baseTitles = criteria
|
||||||
|
.Select(x => string.IsNullOrWhiteSpace(x.Title) ? NormalizeWorkflowTitleFallback(x.Kind) : x.Title.Trim())
|
||||||
|
.ToList();
|
||||||
|
var duplicateTitles = baseTitles
|
||||||
|
.GroupBy(x => x, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.Where(x => x.Count() > 1)
|
||||||
|
.Select(x => x.Key)
|
||||||
|
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||||
|
var titleCounts = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
var usedTitles = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
foreach (var item in criteria)
|
||||||
|
{
|
||||||
|
var baseTitle = string.IsNullOrWhiteSpace(item.Title) ? NormalizeWorkflowTitleFallback(item.Kind) : item.Title.Trim();
|
||||||
|
var title = baseTitle;
|
||||||
|
|
||||||
|
if (duplicateTitles.Contains(baseTitle))
|
||||||
|
{
|
||||||
|
titleCounts.TryGetValue(baseTitle, out var count);
|
||||||
|
count++;
|
||||||
|
titleCounts[baseTitle] = count;
|
||||||
|
title = $"{baseTitle}{count}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (usedTitles.Contains(title))
|
||||||
|
{
|
||||||
|
var index = 1;
|
||||||
|
var candidate = $"{title}{index}";
|
||||||
|
while (usedTitles.Contains(candidate))
|
||||||
|
{
|
||||||
|
index++;
|
||||||
|
candidate = $"{title}{index}";
|
||||||
|
}
|
||||||
|
|
||||||
|
title = candidate;
|
||||||
|
}
|
||||||
|
|
||||||
|
item.Title = title;
|
||||||
|
usedTitles.Add(title);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string NormalizeWorkflowTitleFallback(string kind)
|
||||||
|
{
|
||||||
|
return string.IsNullOrWhiteSpace(kind) ? "Step" : kind.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
private async Task CreateLangKeyAsync(string key, string textEn, string textTr)
|
private async Task CreateLangKeyAsync(string key, string textEn, string textTr)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(key)) return;
|
if (string.IsNullOrWhiteSpace(key)) return;
|
||||||
|
|
@ -427,6 +544,16 @@ 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -773,10 +773,10 @@ public static class PlatformConsts
|
||||||
{
|
{
|
||||||
public static class ParameterTypes
|
public static class ParameterTypes
|
||||||
{
|
{
|
||||||
public const string Static = "S";
|
public const string Static = "Static";
|
||||||
public const string Query = "Q";
|
public const string Query = "Query";
|
||||||
public const string Path = "P";
|
public const string Path = "Path";
|
||||||
public const string Body = "B";
|
public const string Body = "Body";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -95,9 +95,9 @@ public static class WizardConsts
|
||||||
public static readonly string DefaultSearchPanelJson = JsonSerializer.Serialize(new { Visible = true });
|
public static readonly string 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 readonly string DefaultSelectionSingleJson = JsonSerializer.Serialize(new
|
public static string DefaultSelectionSingleJson(string Mode = "none") => JsonSerializer.Serialize(new
|
||||||
{
|
{
|
||||||
Mode = GridOptions.SelectionModeNone,
|
Mode = Mode,
|
||||||
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 = false,
|
I = true,
|
||||||
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\"=@Id";
|
return $"UPDATE \"{tableName}\" SET \"DeleterId\"=@DeleterId, \"DeletionTime\"=CURRENT_TIMESTAMP, \"IsDeleted\"='true' WHERE \"Id\" IN @Id";
|
||||||
}
|
}
|
||||||
|
|
||||||
public static string DefaultInsertFieldsDefaultValueJson(DbType dbType = DbType.Guid)
|
public static string DefaultInsertFieldsDefaultValueJson(DbType dbType = DbType.Guid)
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,14 @@ namespace Sozsoft.Platform.Entities;
|
||||||
|
|
||||||
public class Note : FullAuditedEntity<Guid>, IMultiTenant
|
public class Note : FullAuditedEntity<Guid>, IMultiTenant
|
||||||
{
|
{
|
||||||
|
public Note()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public Note(Guid id) : base(id)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
public Guid? TenantId { get; set; }
|
public Guid? TenantId { get; set; }
|
||||||
public string EntityName { get; set; }
|
public string EntityName { get; set; }
|
||||||
public string EntityId { get; set; }
|
public string EntityId { get; set; }
|
||||||
|
|
|
||||||
|
|
@ -89,7 +89,9 @@ 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 = keys?.FirstOrDefault();
|
value = op == OperationEnum.Delete
|
||||||
|
? 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)
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
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;
|
||||||
|
|
@ -169,7 +170,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 = command;
|
sql = NormalizeCollectionParameterCommand(command, parameters, dataSourceType);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -189,7 +190,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,
|
||||||
};
|
};
|
||||||
|
|
@ -209,7 +210,14 @@ public class QueryManager : PlatformDomainService, IQueryManager
|
||||||
string where = string.Empty;
|
string where = string.Empty;
|
||||||
if (parameters.Any())
|
if (parameters.Any())
|
||||||
{
|
{
|
||||||
where = string.Join(" AND ", parameters.Select(a => $"\"{a.Key}\" IN (@{a.Key})").ToList());
|
where = string.Join(
|
||||||
|
" AND ",
|
||||||
|
parameters.Select(a => dataSourceType switch
|
||||||
|
{
|
||||||
|
DataSourceTypeEnum.Mssql => $"\"{a.Key}\" IN @{a.Key}",
|
||||||
|
DataSourceTypeEnum.Postgresql => $"\"{a.Key}\" = ANY(@{a.Key})",
|
||||||
|
_ => "1 = 0",
|
||||||
|
}).ToList());
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|
@ -220,7 +228,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",
|
||||||
};
|
};
|
||||||
|
|
@ -231,5 +239,78 @@ 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -465,6 +465,28 @@ public class SelectQueryManager : PlatformDomainService, ISelectQueryManager
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (listform.WorkflowJson.IsNullOrWhiteSpace() == false)
|
||||||
|
{
|
||||||
|
var workflow = JsonSerializer.Deserialize<Workflow>(listform.WorkflowJson);
|
||||||
|
if (workflow != null && workflow.IsFilterUserName)
|
||||||
|
{
|
||||||
|
if (whereParts.Any())
|
||||||
|
{
|
||||||
|
whereParts.Add("AND");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hem CurrentUserName alanı boş olan kayıtları
|
||||||
|
// hem de ApprovalUserFieldName alanı CurrentUserName'e eşit olan kayıtları getirmek istiyoruz,
|
||||||
|
// Boş olanları getirmemizin sebebi workflow start edebilmektir.
|
||||||
|
// İlk kayıt eklenince onaylayacak kişi atanmaz, böylece o kayıt onaysız olarak kalmaz ve workflow başlatılabilir olur.
|
||||||
|
whereParts.Add(
|
||||||
|
$"(\"{workflow.ApprovalUserFieldName}\" = '{CurrentUser.UserName}' " +
|
||||||
|
$"OR \"{workflow.ApprovalUserFieldName}\" IS NULL " +
|
||||||
|
$"OR \"{workflow.ApprovalUserFieldName}\" = '')"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!whereParts.Any())
|
if (!whereParts.Any())
|
||||||
{
|
{
|
||||||
whereParts.Add("1 = 1");
|
whereParts.Add("1 = 1");
|
||||||
|
|
|
||||||
23
api/src/Sozsoft.Platform.Domain/Queries/Workflow.cs
Normal file
23
api/src/Sozsoft.Platform.Domain/Queries/Workflow.cs
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Volo.Abp.Domain.Values;
|
||||||
|
|
||||||
|
namespace Sozsoft.Platform.Queries;
|
||||||
|
|
||||||
|
public class Workflow : ValueObject
|
||||||
|
{
|
||||||
|
public string ApprovalUserFieldName { get; set; }
|
||||||
|
public bool IsFilterUserName { get; set; }
|
||||||
|
public string ApprovalDateFieldName { get; set; }
|
||||||
|
public string ApprovalStatusFieldName { get; set; }
|
||||||
|
public string ApprovalDescriptionFieldName { get; set; }
|
||||||
|
|
||||||
|
protected override IEnumerable<object> GetAtomicValues()
|
||||||
|
{
|
||||||
|
yield return ApprovalUserFieldName;
|
||||||
|
yield return IsFilterUserName;
|
||||||
|
yield return ApprovalDateFieldName;
|
||||||
|
yield return ApprovalStatusFieldName;
|
||||||
|
yield return ApprovalDescriptionFieldName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -2,8 +2,12 @@
|
||||||
using System.Collections.Generic;
|
using System.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;
|
||||||
|
|
@ -178,7 +182,7 @@ public class MsDynamicDataRepository : IDynamicDataRepository, IScopedDependency
|
||||||
|
|
||||||
public virtual async Task<List<T>> QueryAsync<T>(string sql, string cs, Dictionary<string, object> parameters = null)
|
public virtual async Task<List<T>> QueryAsync<T>(string sql, string cs, Dictionary<string, object> parameters = null)
|
||||||
{
|
{
|
||||||
var param = new DynamicParameters(parameters);
|
var param = CreateDynamicParameters(parameters);
|
||||||
var dbConnection = await GetOrCreateConnectionAsync(cs);
|
var dbConnection = await GetOrCreateConnectionAsync(cs);
|
||||||
var transaction = await GetOrCreateTransactionAsync(dbConnection, cs);
|
var transaction = await GetOrCreateTransactionAsync(dbConnection, cs);
|
||||||
|
|
||||||
|
|
@ -188,7 +192,7 @@ public class MsDynamicDataRepository : IDynamicDataRepository, IScopedDependency
|
||||||
|
|
||||||
public virtual async Task<IEnumerable<dynamic>> QueryAsync(string sql, string cs, Dictionary<string, object> parameters = null)
|
public virtual async Task<IEnumerable<dynamic>> QueryAsync(string sql, string cs, Dictionary<string, object> parameters = null)
|
||||||
{
|
{
|
||||||
var param = new DynamicParameters(parameters);
|
var param = CreateDynamicParameters(parameters);
|
||||||
var dbConnection = await GetOrCreateConnectionAsync(cs);
|
var dbConnection = await GetOrCreateConnectionAsync(cs);
|
||||||
var transaction = await GetOrCreateTransactionAsync(dbConnection, cs);
|
var transaction = await GetOrCreateTransactionAsync(dbConnection, cs);
|
||||||
|
|
||||||
|
|
@ -197,7 +201,7 @@ public class MsDynamicDataRepository : IDynamicDataRepository, IScopedDependency
|
||||||
|
|
||||||
public virtual async Task<T> QuerySingleAsync<T>(string sql, string cs, Dictionary<string, object> parameters = null)
|
public virtual async Task<T> QuerySingleAsync<T>(string sql, string cs, Dictionary<string, object> parameters = null)
|
||||||
{
|
{
|
||||||
var param = new DynamicParameters(parameters);
|
var param = CreateDynamicParameters(parameters);
|
||||||
var dbConnection = await GetOrCreateConnectionAsync(cs);
|
var dbConnection = await GetOrCreateConnectionAsync(cs);
|
||||||
var transaction = await GetOrCreateTransactionAsync(dbConnection, cs);
|
var transaction = await GetOrCreateTransactionAsync(dbConnection, cs);
|
||||||
|
|
||||||
|
|
@ -206,7 +210,7 @@ public class MsDynamicDataRepository : IDynamicDataRepository, IScopedDependency
|
||||||
|
|
||||||
public virtual async Task<T> ExecuteScalarAsync<T>(string sql, string cs, Dictionary<string, object> parameters = null)
|
public virtual async Task<T> ExecuteScalarAsync<T>(string sql, string cs, Dictionary<string, object> parameters = null)
|
||||||
{
|
{
|
||||||
var param = new DynamicParameters(parameters);
|
var param = CreateDynamicParameters(parameters);
|
||||||
var dbConnection = await GetOrCreateConnectionAsync(cs);
|
var dbConnection = await GetOrCreateConnectionAsync(cs);
|
||||||
var transaction = await GetOrCreateTransactionAsync(dbConnection, cs);
|
var transaction = await GetOrCreateTransactionAsync(dbConnection, cs);
|
||||||
|
|
||||||
|
|
@ -237,13 +241,257 @@ public class MsDynamicDataRepository : IDynamicDataRepository, IScopedDependency
|
||||||
|
|
||||||
public virtual async Task<int> ExecuteAsync(string sql, string cs, Dictionary<string, object> parameters = null)
|
public virtual async Task<int> ExecuteAsync(string sql, string cs, Dictionary<string, object> parameters = null)
|
||||||
{
|
{
|
||||||
var param = new DynamicParameters(parameters);
|
var param = CreateDynamicParameters(parameters);
|
||||||
var dbConnection = await GetOrCreateConnectionAsync(cs);
|
var 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()
|
||||||
|
|
|
||||||
|
|
@ -507,6 +507,7 @@ public class PlatformDbContext :
|
||||||
b.Property(x => x.PositionX).IsRequired();
|
b.Property(x => x.PositionX).IsRequired();
|
||||||
b.Property(x => x.PositionY).IsRequired();
|
b.Property(x => x.PositionY).IsRequired();
|
||||||
b.Property(x => x.CompareOutcomesJson).HasColumnType("text");
|
b.Property(x => x.CompareOutcomesJson).HasColumnType("text");
|
||||||
|
b.HasIndex(x => new { x.ListFormCode, x.Title }).IsUnique();
|
||||||
});
|
});
|
||||||
|
|
||||||
builder.Entity<Note>(b =>
|
builder.Entity<Note>(b =>
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore;
|
||||||
namespace Sozsoft.Platform.Migrations
|
namespace Sozsoft.Platform.Migrations
|
||||||
{
|
{
|
||||||
[DbContext(typeof(PlatformDbContext))]
|
[DbContext(typeof(PlatformDbContext))]
|
||||||
[Migration("20260602070242_Initial")]
|
[Migration("20260606212623_Initial")]
|
||||||
partial class Initial
|
partial class Initial
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|
@ -3486,6 +3486,9 @@ namespace Sozsoft.Platform.Migrations
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ListFormCode", "Title")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
b.ToTable("Sas_H_ListFormWorkflow", (string)null);
|
b.ToTable("Sas_H_ListFormWorkflow", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -3905,6 +3905,12 @@ namespace Sozsoft.Platform.Migrations
|
||||||
table: "Sas_H_ListFormImportLog",
|
table: "Sas_H_ListFormImportLog",
|
||||||
column: "ImportId");
|
column: "ImportId");
|
||||||
|
|
||||||
|
migrationBuilder.CreateIndex(
|
||||||
|
name: "IX_Sas_H_ListFormWorkflow_ListFormCode_Title",
|
||||||
|
table: "Sas_H_ListFormWorkflow",
|
||||||
|
columns: new[] { "ListFormCode", "Title" },
|
||||||
|
unique: true);
|
||||||
|
|
||||||
migrationBuilder.CreateIndex(
|
migrationBuilder.CreateIndex(
|
||||||
name: "IX_Sas_H_Menu_Code",
|
name: "IX_Sas_H_Menu_Code",
|
||||||
table: "Sas_H_Menu",
|
table: "Sas_H_Menu",
|
||||||
|
|
@ -3483,6 +3483,9 @@ namespace Sozsoft.Platform.Migrations
|
||||||
|
|
||||||
b.HasKey("Id");
|
b.HasKey("Id");
|
||||||
|
|
||||||
|
b.HasIndex("ListFormCode", "Title")
|
||||||
|
.IsUnique();
|
||||||
|
|
||||||
b.ToTable("Sas_H_ListFormWorkflow", (string)null);
|
b.ToTable("Sas_H_ListFormWorkflow", (string)null);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -324,16 +324,16 @@
|
||||||
"Url": "/dil/",
|
"Url": "/dil/",
|
||||||
"Method": "GET",
|
"Method": "GET",
|
||||||
"DataSourceCode": "Default",
|
"DataSourceCode": "Default",
|
||||||
"Sql": "SELECT * FROM Plat_H_Language WHERE IsEnabled = @IsEnabled AND CultureName = @CultureName",
|
"Sql": "SELECT * FROM Sas_H_Language WHERE IsEnabled = @IsEnabled AND CultureName = @CultureName",
|
||||||
"ParametersJson": [
|
"ParametersJson": [
|
||||||
{
|
{
|
||||||
"Type": "P",
|
"Type": "Path",
|
||||||
"Name": "CultureName",
|
"Name": "CultureName",
|
||||||
"DefaultValue": "ar",
|
"DefaultValue": "ar",
|
||||||
"Path": "/dil/:CultureName/"
|
"Path": "/dil/:CultureName/"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"Type": "S",
|
"Type": "Static",
|
||||||
"Name": "IsEnabled",
|
"Name": "IsEnabled",
|
||||||
"DefaultValue": "true"
|
"DefaultValue": "true"
|
||||||
}
|
}
|
||||||
|
|
@ -1456,6 +1456,36 @@
|
||||||
"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": [
|
||||||
|
|
|
||||||
|
|
@ -117,6 +117,38 @@ server {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# dashboard.sozsoft.com
|
||||||
|
server {
|
||||||
|
listen 443 ssl http2;
|
||||||
|
server_name dashboard.sozsoft.com;
|
||||||
|
|
||||||
|
ssl_certificate /etc/letsencrypt/live/dashboard.sozsoft.com/fullchain.pem;
|
||||||
|
ssl_trusted_certificate /etc/ssl/sozsoft.com/chain1.pem;
|
||||||
|
ssl_certificate_key /etc/letsencrypt/live/dashboard.sozsoft.com/privkey.pem;
|
||||||
|
|
||||||
|
auth_basic "Restricted";
|
||||||
|
auth_basic_user_file /etc/nginx/.htpasswd;
|
||||||
|
#sudo htpasswd -c /etc/nginx/.htpasswd sedat.ozturk
|
||||||
|
#yukarıdaki komut ile kullanıcı adı ve şifre oluşturabilirsiniz
|
||||||
|
|
||||||
|
proxy_headers_hash_max_size 2048;
|
||||||
|
proxy_headers_hash_bucket_size 128;
|
||||||
|
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:19999;
|
||||||
|
proxy_http_version 1.1;
|
||||||
|
|
||||||
|
include /etc/nginx/proxy_params;
|
||||||
|
|
||||||
|
proxy_set_header X-Forwarded-Host $host;
|
||||||
|
proxy_set_header X-Forwarded-Server $host;
|
||||||
|
|
||||||
|
proxy_read_timeout 300;
|
||||||
|
proxy_connect_timeout 300;
|
||||||
|
proxy_send_timeout 300;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# sozsoft.com
|
# sozsoft.com
|
||||||
server {
|
server {
|
||||||
listen 443 ssl http2;
|
listen 443 ssl http2;
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,9 @@ volumes:
|
||||||
rocket_mongodb_data:
|
rocket_mongodb_data:
|
||||||
driver: local
|
driver: local
|
||||||
n8n_data:
|
n8n_data:
|
||||||
|
netdataconfig:
|
||||||
|
netdatalib:
|
||||||
|
netdatacache:
|
||||||
|
|
||||||
services:
|
services:
|
||||||
forgejo:
|
forgejo:
|
||||||
|
|
@ -108,3 +111,30 @@ 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
|
||||||
|
|
@ -20,6 +20,7 @@ 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..."
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,19 @@
|
||||||
{
|
{
|
||||||
"commit": "0d4703c",
|
"commit": "dc293fc",
|
||||||
"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",
|
||||||
|
|
|
||||||
|
|
@ -47,12 +47,30 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.dialog-content {
|
.dialog-content {
|
||||||
@apply p-6 rounded-lg shadow-xl sm:my-16 relative h-full bg-white dark:bg-gray-800;
|
max-height: calc(100vh - 2rem);
|
||||||
|
@apply p-6 rounded-lg shadow-xl my-4 relative bg-white dark:bg-gray-800 flex flex-col overflow-hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
@screen sm {
|
||||||
|
.dialog-content {
|
||||||
|
max-height: calc(100vh - 8rem);
|
||||||
|
@apply my-16;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-header,
|
||||||
|
.dialog-footer {
|
||||||
|
@apply flex-shrink-0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dialog-body {
|
||||||
|
@apply flex-1 min-h-0 overflow-y-auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dialog-content.maximized {
|
.dialog-content.maximized {
|
||||||
border-radius: 0 !important;
|
border-radius: 0 !important;
|
||||||
height: 100vh !important;
|
height: 100vh !important;
|
||||||
|
max-height: 100vh !important;
|
||||||
margin: 0 !important;
|
margin: 0 !important;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,9 @@ export const FileUploadArea: React.FC<FileUploadAreaProps> = ({
|
||||||
{!selectedFile ? (
|
{!selectedFile ? (
|
||||||
<div
|
<div
|
||||||
className={`relative border-2 border-dashed rounded-lg p-3 transition-all duration-200 ${
|
className={`relative border-2 border-dashed rounded-lg p-3 transition-all duration-200 ${
|
||||||
dragActive ? 'border-blue-400 bg-blue-50' : 'border-slate-300 hover:border-slate-400'
|
dragActive
|
||||||
|
? 'border-blue-400 bg-blue-50 dark:bg-blue-950/30'
|
||||||
|
: 'border-slate-300 hover:border-slate-400 dark:border-slate-600 dark:hover:border-slate-500'
|
||||||
} ${loading ? 'opacity-50 pointer-events-none' : ''}`}
|
} ${loading ? 'opacity-50 pointer-events-none' : ''}`}
|
||||||
onDragEnter={handleDrag}
|
onDragEnter={handleDrag}
|
||||||
onDragLeave={handleDrag}
|
onDragLeave={handleDrag}
|
||||||
|
|
@ -117,35 +119,39 @@ export const FileUploadArea: React.FC<FileUploadAreaProps> = ({
|
||||||
|
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<FaUpload
|
<FaUpload
|
||||||
className={`mx-auto h-3 w-3 ${dragActive ? 'text-blue-500' : 'text-slate-400'}`}
|
className={`mx-auto h-3 w-3 ${dragActive ? 'text-blue-500 dark:text-blue-400' : 'text-slate-400 dark:text-slate-500'}`}
|
||||||
/>
|
/>
|
||||||
<div className="text-lg font-medium text-slate-700 mb-2">
|
<div className="text-lg font-medium text-slate-700 dark:text-slate-200 mb-2">
|
||||||
{dragActive
|
{dragActive
|
||||||
? translate('::App.Listforms.ImportManager.DropHere')
|
? translate('::App.Listforms.ImportManager.DropHere')
|
||||||
: translate('::App.Listforms.ImportManager.UploadYourFile')}
|
: translate('::App.Listforms.ImportManager.UploadYourFile')}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-slate-500 mb-4">
|
<p className="text-slate-500 dark:text-slate-400 mb-4">
|
||||||
{translate('::App.Listforms.ImportManager.DragOrClick')}
|
{translate('::App.Listforms.ImportManager.DragOrClick')}
|
||||||
</p>
|
</p>
|
||||||
<div className="text-sm text-slate-400">
|
<div className="text-sm text-slate-400 dark:text-slate-500">
|
||||||
{translate('::App.Listforms.ImportManager.SupportedFormats')}{' '}
|
{translate('::App.Listforms.ImportManager.SupportedFormats')}{' '}
|
||||||
{acceptedFormats.join(', ')} • Max size: {maxSize}MB
|
{acceptedFormats.join(', ')} • Max size: {maxSize}MB
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="border border-slate-200 rounded-lg p-4">
|
<div className="border border-slate-200 dark:border-slate-700 rounded-lg p-4 dark:bg-slate-900/40">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center space-x-3 min-w-0 flex-1">
|
<div className="flex items-center space-x-3 min-w-0 flex-1">
|
||||||
<FaFile className="w-8 h-8 text-blue-500 flex-shrink-0" />
|
<FaFile className="w-8 h-8 text-blue-500 flex-shrink-0" />
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="font-medium text-slate-800 truncate">{selectedFile.name}</div>
|
<div className="font-medium text-slate-800 dark:text-slate-100 truncate">
|
||||||
<div className="text-sm text-slate-500">{formatFileSize(selectedFile.size)}</div>
|
{selectedFile.name}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-slate-500 dark:text-slate-400">
|
||||||
|
{formatFileSize(selectedFile.size)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={clearFile}
|
onClick={clearFile}
|
||||||
className="p-2 text-slate-400 hover:text-slate-600 hover:bg-slate-100 rounded-lg transition-colors"
|
className="p-2 text-slate-400 hover:text-slate-600 hover:bg-slate-100 dark:text-slate-500 dark:hover:text-slate-300 dark:hover:bg-slate-800 rounded-lg transition-colors"
|
||||||
>
|
>
|
||||||
<FaTimes className="w-4 h-4" />
|
<FaTimes className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -154,9 +160,9 @@ export const FileUploadArea: React.FC<FileUploadAreaProps> = ({
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="flex items-center space-x-2 p-3 bg-red-50 border border-red-200 rounded-lg">
|
<div className="flex items-center space-x-2 p-3 bg-red-50 dark:bg-red-950/30 border border-red-200 dark:border-red-900/60 rounded-lg">
|
||||||
<FaRegCircle className="w-5 h-5 text-red-500" />
|
<FaRegCircle className="w-5 h-5 text-red-500" />
|
||||||
<span className="text-red-700">{error}</span>
|
<span className="text-red-700 dark:text-red-300">{error}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -195,17 +195,17 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'uploaded':
|
case 'uploaded':
|
||||||
case 'executed':
|
case 'executed':
|
||||||
return 'bg-green-50 text-green-700 border-green-200'
|
return 'bg-green-50 text-green-700 border-green-200 dark:bg-green-950/30 dark:text-green-300 dark:border-green-900/60'
|
||||||
case 'executed_with_errors':
|
case 'executed_with_errors':
|
||||||
return 'bg-orange-50 text-orange-700 border-orange-200'
|
return 'bg-orange-50 text-orange-700 border-orange-200 dark:bg-orange-950/30 dark:text-orange-300 dark:border-orange-900/60'
|
||||||
case 'failed':
|
case 'failed':
|
||||||
case 'execute_failed':
|
case 'execute_failed':
|
||||||
return 'bg-red-50 text-red-700 border-red-200'
|
return 'bg-red-50 text-red-700 border-red-200 dark:bg-red-950/30 dark:text-red-300 dark:border-red-900/60'
|
||||||
case 'processing':
|
case 'processing':
|
||||||
case 'validating':
|
case 'validating':
|
||||||
return 'bg-blue-50 text-blue-700 border-blue-200'
|
return 'bg-blue-50 text-blue-700 border-blue-200 dark:bg-blue-950/30 dark:text-blue-300 dark:border-blue-900/60'
|
||||||
default:
|
default:
|
||||||
return 'bg-yellow-50 text-yellow-700 border-yellow-200'
|
return 'bg-yellow-50 text-yellow-700 border-yellow-200 dark:bg-yellow-950/30 dark:text-yellow-300 dark:border-yellow-900/60'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -269,17 +269,17 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
|
||||||
const getExecuteStatusColor = (status: string) => {
|
const getExecuteStatusColor = (status: string) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'completed':
|
case 'completed':
|
||||||
return 'text-green-600'
|
return 'text-green-600 dark:text-green-300'
|
||||||
case 'completed_with_errors':
|
case 'completed_with_errors':
|
||||||
return 'text-orange-600'
|
return 'text-orange-600 dark:text-orange-300'
|
||||||
case 'processing':
|
case 'processing':
|
||||||
return 'text-blue-600'
|
return 'text-blue-600 dark:text-blue-300'
|
||||||
case 'validating':
|
case 'validating':
|
||||||
return 'text-yellow-600'
|
return 'text-yellow-600 dark:text-yellow-300'
|
||||||
case 'failed':
|
case 'failed':
|
||||||
return 'text-red-600'
|
return 'text-red-600 dark:text-red-300'
|
||||||
default:
|
default:
|
||||||
return 'text-slate-600'
|
return 'text-slate-600 dark:text-slate-400'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -331,7 +331,7 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col w-full mt-4">
|
<div className="flex flex-col w-full mt-4">
|
||||||
{/* Navigation Tabs */}
|
{/* Navigation Tabs */}
|
||||||
<div className="flex space-x-1 mb-4 bg-white rounded-lg p-1 shadow-sm border border-slate-200 flex-shrink-0">
|
<div className="flex space-x-1 mb-4 bg-white dark:bg-slate-900 rounded-lg p-1 shadow-sm border border-slate-200 dark:border-slate-700 flex-shrink-0">
|
||||||
{['import', 'preview', 'history'].map((tab) => (
|
{['import', 'preview', 'history'].map((tab) => (
|
||||||
<button
|
<button
|
||||||
key={tab}
|
key={tab}
|
||||||
|
|
@ -339,7 +339,7 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
|
||||||
className={`px-3 py-2 rounded-md font-medium transition-all duration-200 flex items-center space-x-2 ${
|
className={`px-3 py-2 rounded-md font-medium transition-all duration-200 flex items-center space-x-2 ${
|
||||||
activeTab === tab
|
activeTab === tab
|
||||||
? 'bg-blue-500 text-white shadow-md'
|
? 'bg-blue-500 text-white shadow-md'
|
||||||
: 'text-slate-600 hover:text-slate-800 hover:bg-slate-50'
|
: 'text-slate-600 hover:text-slate-800 hover:bg-slate-50 dark:text-slate-300 dark:hover:text-slate-100 dark:hover:bg-slate-800'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{tab === 'import' && <FaUpload className="w-4 h-4" />}
|
{tab === 'import' && <FaUpload className="w-4 h-4" />}
|
||||||
|
|
@ -358,9 +358,9 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||||
{/* Template Generator - 2/3 width on large screens, full width on mobile */}
|
{/* Template Generator - 2/3 width on large screens, full width on mobile */}
|
||||||
<div className="lg:col-span-2">
|
<div className="lg:col-span-2">
|
||||||
<div className="bg-white rounded-xl shadow-sm border border-slate-200">
|
<div className="bg-white dark:bg-slate-900 rounded-xl shadow-sm border border-slate-200 dark:border-slate-700">
|
||||||
<div className="px-3 py-3 border-b flex items-center justify-between">
|
<div className="px-3 py-3 border-b border-slate-200 dark:border-slate-700 flex items-center justify-between">
|
||||||
<h3 className="text-xl font-semibold text-slate-800 flex items-center">
|
<h3 className="text-xl font-semibold text-slate-800 dark:text-slate-100 flex items-center">
|
||||||
<FaDownload className="w-4 h-4 mr-2" />
|
<FaDownload className="w-4 h-4 mr-2" />
|
||||||
{translate('::App.Listforms.ImportManager.TemplateColumns')} (
|
{translate('::App.Listforms.ImportManager.TemplateColumns')} (
|
||||||
{editableColumns.length})
|
{editableColumns.length})
|
||||||
|
|
@ -371,10 +371,10 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
|
||||||
<button
|
<button
|
||||||
onClick={() => generateTemplate('excel')}
|
onClick={() => generateTemplate('excel')}
|
||||||
disabled={generating}
|
disabled={generating}
|
||||||
className="flex items-center gap-1.5 px-3 py-1.5 border border-green-200 rounded-md hover:border-green-300 hover:bg-green-50 transition-all duration-200 group disabled:opacity-50 disabled:cursor-not-allowed bg-white text-xs"
|
className="flex items-center gap-1.5 px-3 py-1.5 border border-green-200 dark:border-green-900/60 rounded-md hover:border-green-300 hover:bg-green-50 dark:hover:bg-green-950/30 transition-all duration-200 group disabled:opacity-50 disabled:cursor-not-allowed bg-white dark:bg-slate-900 text-xs"
|
||||||
>
|
>
|
||||||
<FaFileExcel className="w-3.5 h-3.5 text-green-500 group-hover:scale-110 transition-transform" />
|
<FaFileExcel className="w-3.5 h-3.5 text-green-500 group-hover:scale-110 transition-transform" />
|
||||||
<span className="font-medium text-slate-700">
|
<span className="font-medium text-slate-700 dark:text-slate-200">
|
||||||
{translate('::App.Listforms.ImportManager.ExcelTemplate')}
|
{translate('::App.Listforms.ImportManager.ExcelTemplate')}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -382,10 +382,10 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
|
||||||
<button
|
<button
|
||||||
onClick={() => generateTemplate('csv')}
|
onClick={() => generateTemplate('csv')}
|
||||||
disabled={generating}
|
disabled={generating}
|
||||||
className="flex items-center gap-1.5 px-3 py-1.5 border border-blue-200 rounded-md hover:border-blue-300 hover:bg-blue-50 transition-all duration-200 group disabled:opacity-50 disabled:cursor-not-allowed bg-white text-xs"
|
className="flex items-center gap-1.5 px-3 py-1.5 border border-blue-200 dark:border-blue-900/60 rounded-md hover:border-blue-300 hover:bg-blue-50 dark:hover:bg-blue-950/30 transition-all duration-200 group disabled:opacity-50 disabled:cursor-not-allowed bg-white dark:bg-slate-900 text-xs"
|
||||||
>
|
>
|
||||||
<FaFileAlt className="w-3.5 h-3.5 text-blue-500 group-hover:scale-110 transition-transform" />
|
<FaFileAlt className="w-3.5 h-3.5 text-blue-500 group-hover:scale-110 transition-transform" />
|
||||||
<span className="font-medium text-slate-700">
|
<span className="font-medium text-slate-700 dark:text-slate-200">
|
||||||
{translate('::App.Listforms.ImportManager.CsvTemplate')}
|
{translate('::App.Listforms.ImportManager.CsvTemplate')}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -394,38 +394,38 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
|
||||||
|
|
||||||
<div className="max-h-96 overflow-y-auto">
|
<div className="max-h-96 overflow-y-auto">
|
||||||
<table className="w-full">
|
<table className="w-full">
|
||||||
<thead className="bg-slate-100 sticky top-0">
|
<thead className="bg-slate-100 dark:bg-slate-800 sticky top-0">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-4 py-2 text-left text-xs font-medium text-slate-500 uppercase">
|
<th className="px-4 py-2 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase">
|
||||||
{translate('::App.Listform.ListformField.Column')}
|
{translate('::App.Listform.ListformField.Column')}
|
||||||
</th>
|
</th>
|
||||||
<th className="px-4 py-2 text-left text-xs font-medium text-slate-500 uppercase">
|
<th className="px-4 py-2 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase">
|
||||||
{translate('::ListForms.ListFormEdit.Type')}
|
{translate('::ListForms.ListFormEdit.Type')}
|
||||||
</th>
|
</th>
|
||||||
<th className="px-4 py-2 text-left text-xs font-medium text-slate-500 uppercase">
|
<th className="px-4 py-2 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase">
|
||||||
{translate('::App.Required')}
|
{translate('::App.Required')}
|
||||||
</th>
|
</th>
|
||||||
<th className="px-4 py-2 text-left text-xs font-medium text-slate-500 uppercase">
|
<th className="px-4 py-2 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase">
|
||||||
{translate('::Abp.Mailing.Default')}
|
{translate('::Abp.Mailing.Default')}
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-slate-100">
|
<tbody className="divide-y divide-slate-100 dark:divide-slate-800">
|
||||||
{editableColumns.map((column: any) => (
|
{editableColumns.map((column: any) => (
|
||||||
<tr key={column.fieldName} className="hover:bg-slate-50">
|
<tr key={column.fieldName} className="hover:bg-slate-50 dark:hover:bg-slate-800/70">
|
||||||
<td className="px-2 py-2 font-medium text-slate-800">
|
<td className="px-2 py-2 font-medium text-slate-800 dark:text-slate-100">
|
||||||
{column.fieldName}
|
{column.fieldName}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-2 text-slate-600">
|
<td className="px-4 py-2 text-slate-600 dark:text-slate-300">
|
||||||
<span
|
<span
|
||||||
className={`px-2 py-1 rounded text-xs font-medium ${
|
className={`px-2 py-1 rounded text-xs font-medium ${
|
||||||
column.dataType === 'string'
|
column.dataType === 'string'
|
||||||
? 'bg-blue-100 text-blue-800'
|
? 'bg-blue-100 text-blue-800 dark:bg-blue-950/40 dark:text-blue-300'
|
||||||
: column.dataType === 'number'
|
: column.dataType === 'number'
|
||||||
? 'bg-green-100 text-green-800'
|
? 'bg-green-100 text-green-800 dark:bg-green-950/40 dark:text-green-300'
|
||||||
: column.dataType === 'boolean'
|
: column.dataType === 'boolean'
|
||||||
? 'bg-purple-100 text-purple-800'
|
? 'bg-purple-100 text-purple-800 dark:bg-purple-950/40 dark:text-purple-300'
|
||||||
: 'bg-orange-100 text-orange-800'
|
: 'bg-orange-100 text-orange-800 dark:bg-orange-950/40 dark:text-orange-300'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{column.dataType}
|
{column.dataType}
|
||||||
|
|
@ -435,16 +435,16 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
|
||||||
{column.validationRuleDto.some(
|
{column.validationRuleDto.some(
|
||||||
(rule: any) => rule.type === 'required',
|
(rule: any) => rule.type === 'required',
|
||||||
) ? (
|
) ? (
|
||||||
<span className="text-red-500 font-medium">
|
<span className="text-red-500 dark:text-red-300 font-medium">
|
||||||
{translate('::App.Listforms.ImportManager.Yes')}
|
{translate('::App.Listforms.ImportManager.Yes')}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-slate-400">
|
<span className="text-slate-400 dark:text-slate-500">
|
||||||
{translate('::App.Listforms.ImportManager.No')}
|
{translate('::App.Listforms.ImportManager.No')}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-2 text-slate-600 text-sm">
|
<td className="px-4 py-2 text-slate-600 dark:text-slate-300 text-sm">
|
||||||
{typeof column.defaultValue === 'object'
|
{typeof column.defaultValue === 'object'
|
||||||
? JSON.stringify(column.defaultValue)
|
? JSON.stringify(column.defaultValue)
|
||||||
: column.defaultValue}
|
: column.defaultValue}
|
||||||
|
|
@ -458,7 +458,7 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
|
||||||
{generating && (
|
{generating && (
|
||||||
<div className="flex items-center justify-center py-4">
|
<div className="flex items-center justify-center py-4">
|
||||||
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-500"></div>
|
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-blue-500"></div>
|
||||||
<span className="ml-2 text-slate-600">
|
<span className="ml-2 text-slate-600 dark:text-slate-400">
|
||||||
{translate('::App.Listforms.ImportManager.GeneratingTemplate')}
|
{translate('::App.Listforms.ImportManager.GeneratingTemplate')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -468,8 +468,8 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
|
||||||
|
|
||||||
{/* File Upload - 1/3 width on large screens, full width on mobile */}
|
{/* File Upload - 1/3 width on large screens, full width on mobile */}
|
||||||
<div className="lg:col-span-1">
|
<div className="lg:col-span-1">
|
||||||
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-4 h-full">
|
<div className="bg-white dark:bg-slate-900 rounded-xl shadow-sm border border-slate-200 dark:border-slate-700 p-4 h-full">
|
||||||
<h2 className="text-xl font-semibold text-slate-800 mb-4 flex items-center">
|
<h2 className="text-xl font-semibold text-slate-800 dark:text-slate-100 mb-4 flex items-center">
|
||||||
<FaUpload className="w-5 h-5 mr-2 text-green-500" />
|
<FaUpload className="w-5 h-5 mr-2 text-green-500" />
|
||||||
{translate('::App.Listforms.ImportManager.UploadData')}
|
{translate('::App.Listforms.ImportManager.UploadData')}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
@ -506,9 +506,9 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-12">
|
<div className="bg-white dark:bg-slate-900 rounded-xl shadow-sm border border-slate-200 dark:border-slate-700 p-12">
|
||||||
<div className="text-center text-slate-500">
|
<div className="text-center text-slate-500 dark:text-slate-400">
|
||||||
<FaEye className="w-16 h-16 mx-auto mb-4 text-slate-300" />
|
<FaEye className="w-16 h-16 mx-auto mb-4 text-slate-300 dark:text-slate-600" />
|
||||||
<div className="text-xl font-medium mb-2">
|
<div className="text-xl font-medium mb-2">
|
||||||
{translate('::App.Listforms.ImportManager.NoDataToPreview')}
|
{translate('::App.Listforms.ImportManager.NoDataToPreview')}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -520,22 +520,22 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{activeTab === 'history' && (
|
{activeTab === 'history' && (
|
||||||
<div className="bg-white rounded-xl shadow-sm border border-slate-200">
|
<div className="bg-white dark:bg-slate-900 rounded-xl shadow-sm border border-slate-200 dark:border-slate-700">
|
||||||
<div className="p-3 border-b border-slate-200">
|
<div className="p-3 border-b border-slate-200 dark:border-slate-700">
|
||||||
<h2 className="text-xl font-semibold text-slate-800 flex items-center">
|
<h2 className="text-xl font-semibold text-slate-800 dark:text-slate-100 flex items-center">
|
||||||
<FaClock className="w-5 h-5 mr-2 text-indigo-500" />
|
<FaClock className="w-5 h-5 mr-2 text-indigo-500" />
|
||||||
{translate('::App.Listforms.ImportManager.ImportHistory')}
|
{translate('::App.Listforms.ImportManager.ImportHistory')}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="divide-y divide-slate-100">
|
<div className="divide-y divide-slate-100 dark:divide-slate-800">
|
||||||
{importHistory.map((session) => (
|
{importHistory.map((session) => (
|
||||||
<div
|
<div
|
||||||
key={session.id}
|
key={session.id}
|
||||||
className={`p-2 transition-colors border-l-4 ${
|
className={`p-2 transition-colors border-l-4 ${
|
||||||
currentSession?.id === session.id
|
currentSession?.id === session.id
|
||||||
? 'bg-blue-50 border-l-blue-500 hover:bg-blue-100'
|
? 'bg-blue-50 border-l-blue-500 hover:bg-blue-100 dark:bg-blue-950/30 dark:hover:bg-blue-950/40'
|
||||||
: 'border-l-transparent hover:bg-slate-50'
|
: 'border-l-transparent hover:bg-slate-50 dark:hover:bg-slate-800/70'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
|
|
@ -543,13 +543,15 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
|
||||||
{getStatusIcon(session.status)}
|
{getStatusIcon(session.status)}
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<div>
|
<div>
|
||||||
<div className="font-medium text-slate-800">{session.blobName}</div>
|
<div className="font-medium text-slate-800 dark:text-slate-100">
|
||||||
<div className="text-sm text-slate-500">
|
{session.blobName}
|
||||||
|
</div>
|
||||||
|
<div className="text-sm text-slate-500 dark:text-slate-400">
|
||||||
{new Date(session.creationTime).toLocaleString()}
|
{new Date(session.creationTime).toLocaleString()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{currentSession?.id === session.id && (
|
{currentSession?.id === session.id && (
|
||||||
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
|
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-950/40 dark:text-blue-300">
|
||||||
{translate('::App.Status.Active')}
|
{translate('::App.Status.Active')}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
@ -558,7 +560,7 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
|
||||||
|
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
<div className="text-right">
|
<div className="text-right">
|
||||||
<div className="text-sm font-medium text-slate-800">
|
<div className="text-sm font-medium text-slate-800 dark:text-slate-100">
|
||||||
{session.totalRows} {translate('::App.Listforms.ImportManager.TotalRows')}
|
{session.totalRows} {translate('::App.Listforms.ImportManager.TotalRows')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -576,8 +578,8 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
|
||||||
onClick={() => toggleSessionExecutes(session.id)}
|
onClick={() => toggleSessionExecutes(session.id)}
|
||||||
className={`p-2 rounded-lg transition-colors ${
|
className={`p-2 rounded-lg transition-colors ${
|
||||||
expandedSessions.has(session.id)
|
expandedSessions.has(session.id)
|
||||||
? 'text-red-500 bg-red-50 hover:text-red-600 hover:bg-red-100'
|
? 'text-red-500 bg-red-50 hover:text-red-600 hover:bg-red-100 dark:bg-red-950/30 dark:text-red-300 dark:hover:bg-red-950/40'
|
||||||
: 'text-slate-400 hover:text-slate-600 hover:bg-slate-100'
|
: 'text-slate-400 hover:text-slate-600 hover:bg-slate-100 dark:text-slate-500 dark:hover:text-slate-300 dark:hover:bg-slate-800'
|
||||||
}`}
|
}`}
|
||||||
title={translate('::App.Listforms.ImportManager.ViewExecutionDetails')}
|
title={translate('::App.Listforms.ImportManager.ViewExecutionDetails')}
|
||||||
>
|
>
|
||||||
|
|
@ -607,7 +609,7 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="p-2 rounded-lg transition-colors text-slate-400 hover:text-blue-500 hover:bg-blue-50"
|
className="p-2 rounded-lg transition-colors text-slate-400 hover:text-blue-500 hover:bg-blue-50 dark:text-slate-500 dark:hover:text-blue-300 dark:hover:bg-blue-950/30"
|
||||||
title={translate('::App.Listforms.ImportManager.RefreshExecutionDetails')}
|
title={translate('::App.Listforms.ImportManager.RefreshExecutionDetails')}
|
||||||
>
|
>
|
||||||
<FaSync className="w-4 h-4" />
|
<FaSync className="w-4 h-4" />
|
||||||
|
|
@ -623,8 +625,8 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
|
||||||
disabled={currentSession?.id === session.id}
|
disabled={currentSession?.id === session.id}
|
||||||
className={`p-2 rounded-lg transition-colors ${
|
className={`p-2 rounded-lg transition-colors ${
|
||||||
currentSession?.id === session.id
|
currentSession?.id === session.id
|
||||||
? 'text-slate-300 cursor-not-allowed'
|
? 'text-slate-300 dark:text-slate-600 cursor-not-allowed'
|
||||||
: 'text-slate-400 hover:text-red-500 hover:bg-red-50'
|
: 'text-slate-400 hover:text-red-500 hover:bg-red-50 dark:text-slate-500 dark:hover:text-red-300 dark:hover:bg-red-950/30'
|
||||||
}`}
|
}`}
|
||||||
title={
|
title={
|
||||||
currentSession?.id === session.id
|
currentSession?.id === session.id
|
||||||
|
|
@ -645,10 +647,10 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
|
||||||
session.status === 'executed_with_errors' ||
|
session.status === 'executed_with_errors' ||
|
||||||
session.status === 'failed' ||
|
session.status === 'failed' ||
|
||||||
session.status === 'execute_failed'
|
session.status === 'execute_failed'
|
||||||
? 'bg-red-50 text-red-700 border border-red-200'
|
? 'bg-red-50 text-red-700 border border-red-200 dark:bg-red-950/30 dark:text-red-300 dark:border-red-900/60'
|
||||||
: session.status === 'uploaded'
|
: session.status === 'uploaded'
|
||||||
? 'bg-blue-50 text-blue-700 border border-blue-200'
|
? 'bg-blue-50 text-blue-700 border border-blue-200 dark:bg-blue-950/30 dark:text-blue-300 dark:border-blue-900/60'
|
||||||
: 'bg-green-50 text-green-700 border border-green-200'
|
: 'bg-green-50 text-green-700 border border-green-200 dark:bg-green-950/30 dark:text-green-300 dark:border-green-900/60'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<span className="mt-0.5 flex-shrink-0">
|
<span className="mt-0.5 flex-shrink-0">
|
||||||
|
|
@ -668,10 +670,10 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
|
||||||
|
|
||||||
{/* Execute Details Section */}
|
{/* Execute Details Section */}
|
||||||
{expandedSessions.has(session.id) && (
|
{expandedSessions.has(session.id) && (
|
||||||
<div className="mt-3 bg-gradient-to-r from-indigo-50 to-blue-50 border border-indigo-100 rounded-lg shadow-sm hover:shadow-md transition-shadow">
|
<div className="mt-3 bg-gradient-to-r from-indigo-50 to-blue-50 dark:from-slate-800 dark:to-slate-800 border border-indigo-100 dark:border-slate-700 rounded-lg shadow-sm hover:shadow-md transition-shadow">
|
||||||
<div className="p-3">
|
<div className="p-3">
|
||||||
{loadingExecutes.has(session.id) ? (
|
{loadingExecutes.has(session.id) ? (
|
||||||
<div className="flex items-center space-x-2 text-slate-500 py-2">
|
<div className="flex items-center space-x-2 text-slate-500 dark:text-slate-400 py-2">
|
||||||
<FaSync className="w-4 h-4 animate-spin" />
|
<FaSync className="w-4 h-4 animate-spin" />
|
||||||
<span className="text-sm">
|
<span className="text-sm">
|
||||||
{translate('::App.Listforms.ImportManager.LoadingExecutionDetails')}
|
{translate('::App.Listforms.ImportManager.LoadingExecutionDetails')}
|
||||||
|
|
@ -681,38 +683,38 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
|
||||||
sessionExecutes[session.id].length > 0 ? (
|
sessionExecutes[session.id].length > 0 ? (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{sessionExecutes[session.id].map((execute) => (
|
{sessionExecutes[session.id].map((execute) => (
|
||||||
<div key={execute.id} className="p-3 rounded-lg">
|
<div key={execute.id} className="p-3 rounded-lg dark:bg-slate-900/40">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
{/* Sol: Tarih */}
|
{/* Sol: Tarih */}
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
<div className="text-lg text-slate-500">
|
<div className="text-lg text-slate-500 dark:text-slate-400">
|
||||||
{new Date(execute.creationTime).toLocaleString()}
|
{new Date(execute.creationTime).toLocaleString()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Orta: Executed, Valid, Errors */}
|
{/* Orta: Executed, Valid, Errors */}
|
||||||
<div className="flex items-center space-x-4 text-xs text-slate-600">
|
<div className="flex items-center space-x-4 text-xs text-slate-600 dark:text-slate-400">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="font-medium text-slate-800">
|
<div className="font-medium text-slate-800 dark:text-slate-100">
|
||||||
{execute.execRows}
|
{execute.execRows}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-slate-500">
|
<div className="text-slate-500 dark:text-slate-400">
|
||||||
{translate('::App.Listforms.ImportManager.Executed')}
|
{translate('::App.Listforms.ImportManager.Executed')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="font-medium text-green-600">
|
<div className="font-medium text-green-600 dark:text-green-300">
|
||||||
{execute.validRows}
|
{execute.validRows}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-slate-500">
|
<div className="text-slate-500 dark:text-slate-400">
|
||||||
{translate('::App.Listforms.ImportManager.Valid')}
|
{translate('::App.Listforms.ImportManager.Valid')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<div className="font-medium text-red-600">
|
<div className="font-medium text-red-600 dark:text-red-300">
|
||||||
{execute.errorRows}
|
{execute.errorRows}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-slate-500">
|
<div className="text-slate-500 dark:text-slate-400">
|
||||||
{translate('::App.Listforms.ImportManager.Errors')}
|
{translate('::App.Listforms.ImportManager.Errors')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -737,7 +739,7 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<button
|
<button
|
||||||
onClick={() => toggleErrors(execute.id)}
|
onClick={() => toggleErrors(execute.id)}
|
||||||
className="flex items-center space-x-1 text-xs text-orange-600 hover:text-orange-700 font-medium"
|
className="flex items-center space-x-1 text-xs text-orange-600 hover:text-orange-700 dark:text-orange-300 dark:hover:text-orange-200 font-medium"
|
||||||
>
|
>
|
||||||
{expandedErrors.has(execute.id) ? (
|
{expandedErrors.has(execute.id) ? (
|
||||||
<FaChevronUp className="w-3 h-3" />
|
<FaChevronUp className="w-3 h-3" />
|
||||||
|
|
@ -751,26 +753,29 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
{expandedErrors.has(execute.id) && (
|
{expandedErrors.has(execute.id) && (
|
||||||
<div className="mt-2 max-h-48 overflow-y-auto rounded border border-orange-200 bg-orange-50">
|
<div className="mt-2 max-h-48 overflow-y-auto rounded border border-orange-200 bg-orange-50 dark:border-orange-900/60 dark:bg-orange-950/30">
|
||||||
{parseErrors(execute.errorsJson).length > 0 ? (
|
{parseErrors(execute.errorsJson).length > 0 ? (
|
||||||
<table className="w-full text-xs">
|
<table className="w-full text-xs">
|
||||||
<thead className="bg-orange-100 sticky top-0">
|
<thead className="bg-orange-100 dark:bg-orange-950/50 sticky top-0">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-3 py-1 text-left font-medium text-orange-700 w-16">
|
<th className="px-3 py-1 text-left font-medium text-orange-700 dark:text-orange-300 w-16">
|
||||||
Satır
|
Satır
|
||||||
</th>
|
</th>
|
||||||
<th className="px-3 py-1 text-left font-medium text-orange-700">
|
<th className="px-3 py-1 text-left font-medium text-orange-700 dark:text-orange-300">
|
||||||
Hata Mesajı
|
Hata Mesajı
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-orange-100">
|
<tbody className="divide-y divide-orange-100 dark:divide-orange-900/50">
|
||||||
{parseErrors(execute.errorsJson).map((err, idx) => (
|
{parseErrors(execute.errorsJson).map((err, idx) => (
|
||||||
<tr key={idx} className="hover:bg-orange-100">
|
<tr
|
||||||
<td className="px-3 py-1 text-orange-700 font-medium">
|
key={idx}
|
||||||
|
className="hover:bg-orange-100 dark:hover:bg-orange-950/50"
|
||||||
|
>
|
||||||
|
<td className="px-3 py-1 text-orange-700 dark:text-orange-300 font-medium">
|
||||||
{err.row}
|
{err.row}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-3 py-1 text-slate-700">
|
<td className="px-3 py-1 text-slate-700 dark:text-slate-200">
|
||||||
{err.message}
|
{err.message}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -778,7 +783,7 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
) : (
|
) : (
|
||||||
<p className="px-3 py-2 text-orange-600">
|
<p className="px-3 py-2 text-orange-600 dark:text-orange-300">
|
||||||
Hata detayı mevcut değil.
|
Hata detayı mevcut değil.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
@ -790,7 +795,7 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-sm text-slate-500 py-2">
|
<div className="text-sm text-slate-500 dark:text-slate-400 py-2">
|
||||||
{translate('::App.Listforms.ImportManager.NoExecutionRecords')}
|
{translate('::App.Listforms.ImportManager.NoExecutionRecords')}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -801,8 +806,8 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{importHistory.length === 0 && (
|
{importHistory.length === 0 && (
|
||||||
<div className="p-12 text-center text-slate-500">
|
<div className="p-12 text-center text-slate-500 dark:text-slate-400">
|
||||||
<FaClock className="w-12 h-12 mx-auto mb-4 text-slate-300" />
|
<FaClock className="w-12 h-12 mx-auto mb-4 text-slate-300 dark:text-slate-600" />
|
||||||
<div className="text-lg font-medium mb-2">
|
<div className="text-lg font-medium mb-2">
|
||||||
{translate('::App.Listforms.ImportManager.NoImportHistory')}
|
{translate('::App.Listforms.ImportManager.NoImportHistory')}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -100,13 +100,13 @@ export const ImportPreview: React.FC<ImportPreviewProps> = ({
|
||||||
const getStatusColor = (status: string) => {
|
const getStatusColor = (status: string) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'uploaded':
|
case 'uploaded':
|
||||||
return 'text-green-600 bg-green-50 border-green-200'
|
return 'text-green-600 bg-green-50 border-green-200 dark:text-green-300 dark:bg-green-950/30 dark:border-green-900/60'
|
||||||
case 'failed':
|
case 'failed':
|
||||||
return 'text-red-600 bg-red-50 border-red-200'
|
return 'text-red-600 bg-red-50 border-red-200 dark:text-red-300 dark:bg-red-950/30 dark:border-red-900/60'
|
||||||
case 'validating':
|
case 'validating':
|
||||||
return 'text-yellow-600 bg-yellow-50 border-yellow-200'
|
return 'text-yellow-600 bg-yellow-50 border-yellow-200 dark:text-yellow-300 dark:bg-yellow-950/30 dark:border-yellow-900/60'
|
||||||
default:
|
default:
|
||||||
return 'text-blue-600 bg-blue-50 border-blue-200'
|
return 'text-blue-600 bg-blue-50 border-blue-200 dark:text-blue-300 dark:bg-blue-950/30 dark:border-blue-900/60'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -145,14 +145,14 @@ export const ImportPreview: React.FC<ImportPreviewProps> = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white rounded-xl shadow-sm border border-slate-200">
|
<div className="bg-white dark:bg-slate-900 rounded-xl shadow-sm border border-slate-200 dark:border-slate-700">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="p-3 border-b border-slate-200">
|
<div className="p-3 border-b border-slate-200 dark:border-slate-700">
|
||||||
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-center gap-4">
|
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-center gap-4">
|
||||||
{/* Başlık kısmı - Üstte mobile, solda desktop */}
|
{/* Başlık kısmı - Üstte mobile, solda desktop */}
|
||||||
<div className="flex items-center order-1 lg:order-none">
|
<div className="flex items-center order-1 lg:order-none">
|
||||||
<FaEye className="w-5 h-5 mr-2 text-blue-500" />
|
<FaEye className="w-5 h-5 mr-2 text-blue-500" />
|
||||||
<h3 className="text-xl font-semibold text-slate-800">
|
<h3 className="text-xl font-semibold text-slate-800 dark:text-slate-100">
|
||||||
{translate('::App.Listforms.ImportManager.ImportPreviewTitle')}
|
{translate('::App.Listforms.ImportManager.ImportPreviewTitle')}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -160,9 +160,9 @@ export const ImportPreview: React.FC<ImportPreviewProps> = ({
|
||||||
{/* İstatistik kartları - Mobile'da alt alta, desktop'ta yan yana */}
|
{/* İstatistik kartları - Mobile'da alt alta, desktop'ta yan yana */}
|
||||||
<div className="order-3 lg:order-none lg:absolute lg:left-1/2 lg:transform lg:-translate-x-1/2">
|
<div className="order-3 lg:order-none lg:absolute lg:left-1/2 lg:transform lg:-translate-x-1/2">
|
||||||
<div className="flex flex-col sm:flex-row justify-center gap-2">
|
<div className="flex flex-col sm:flex-row justify-center gap-2">
|
||||||
<div className="text-center px-3 py-1 bg-blue-50 rounded-full border border-blue-200 font-bold text-blue-600">
|
<div className="text-center px-3 py-1 bg-blue-50 dark:bg-blue-950/30 rounded-full border border-blue-200 dark:border-blue-900/60 font-bold text-blue-600 dark:text-blue-300">
|
||||||
{previewData?.rows?.length || session.totalRows || 0}{' '}
|
{previewData?.rows?.length || session.totalRows || 0}{' '}
|
||||||
<span className="text-xs text-blue-700">
|
<span className="text-xs text-blue-700 dark:text-blue-300">
|
||||||
{translate('::App.Listforms.ImportManager.TotalRows')}
|
{translate('::App.Listforms.ImportManager.TotalRows')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -184,16 +184,16 @@ export const ImportPreview: React.FC<ImportPreviewProps> = ({
|
||||||
|
|
||||||
{/* Preview Data */}
|
{/* Preview Data */}
|
||||||
{previewData && previewData.headers && previewData.headers.length > 0 ? (
|
{previewData && previewData.headers && previewData.headers.length > 0 ? (
|
||||||
<div className="p-3 border-b border-slate-200">
|
<div className="p-3 border-b border-slate-200 dark:border-slate-700">
|
||||||
<h4 className="font-semibold text-slate-800 mb-4">
|
<h4 className="font-semibold text-slate-800 dark:text-slate-100 mb-4">
|
||||||
{translate('::App.Listforms.ImportManager.DataPreviewTitle')}
|
{translate('::App.Listforms.ImportManager.DataPreviewTitle')}
|
||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
<div className="overflow-auto border border-slate-200 rounded-lg max-h-90">
|
<div className="overflow-auto border border-slate-200 dark:border-slate-700 rounded-lg max-h-90">
|
||||||
<table className="w-full text-sm min-w-full">
|
<table className="w-full text-sm min-w-full">
|
||||||
<thead className="bg-slate-50 sticky top-0 z-10">
|
<thead className="bg-slate-50 dark:bg-slate-800 sticky top-0 z-10">
|
||||||
<tr>
|
<tr>
|
||||||
<th className="px-4 py-2 text-left font-medium text-slate-700 whitespace-nowrap w-12">
|
<th className="px-4 py-2 text-left font-medium text-slate-700 dark:text-slate-200 whitespace-nowrap w-12">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={selectAll}
|
checked={selectAll}
|
||||||
|
|
@ -204,19 +204,19 @@ export const ImportPreview: React.FC<ImportPreviewProps> = ({
|
||||||
{previewData.headers.map((header: string, index: number) => (
|
{previewData.headers.map((header: string, index: number) => (
|
||||||
<th
|
<th
|
||||||
key={index}
|
key={index}
|
||||||
className="px-4 py-2 text-left font-medium text-slate-700 whitespace-nowrap"
|
className="px-4 py-2 text-left font-medium text-slate-700 dark:text-slate-200 whitespace-nowrap"
|
||||||
>
|
>
|
||||||
{header}
|
{header}
|
||||||
</th>
|
</th>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="divide-y divide-slate-100">
|
<tbody className="divide-y divide-slate-100 dark:divide-slate-800">
|
||||||
{previewData.rows.map((row: any[], rowIndex: number) => (
|
{previewData.rows.map((row: any[], rowIndex: number) => (
|
||||||
<tr
|
<tr
|
||||||
key={rowIndex}
|
key={rowIndex}
|
||||||
className={`hover:bg-slate-50 ${
|
className={`hover:bg-slate-50 dark:hover:bg-slate-800/70 ${
|
||||||
selectedRows.includes(rowIndex) ? 'bg-blue-50' : ''
|
selectedRows.includes(rowIndex) ? 'bg-blue-50 dark:bg-blue-950/30' : ''
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<td className="px-4 py-2">
|
<td className="px-4 py-2">
|
||||||
|
|
@ -230,7 +230,7 @@ export const ImportPreview: React.FC<ImportPreviewProps> = ({
|
||||||
{row.map((cell, cellIndex) => (
|
{row.map((cell, cellIndex) => (
|
||||||
<td
|
<td
|
||||||
key={cellIndex}
|
key={cellIndex}
|
||||||
className="px-4 py-2 text-slate-600 whitespace-nowrap max-w-xs truncate"
|
className="px-4 py-2 text-slate-600 dark:text-slate-300 whitespace-nowrap max-w-xs truncate"
|
||||||
>
|
>
|
||||||
{cell?.toString() || '-'}
|
{cell?.toString() || '-'}
|
||||||
</td>
|
</td>
|
||||||
|
|
@ -242,25 +242,25 @@ export const ImportPreview: React.FC<ImportPreviewProps> = ({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : previewData && previewData.headers && previewData.headers.length === 0 ? (
|
) : previewData && previewData.headers && previewData.headers.length === 0 ? (
|
||||||
<div className="p-6 border-b border-slate-200">
|
<div className="p-6 border-b border-slate-200 dark:border-slate-700">
|
||||||
<div className="text-center py-8">
|
<div className="text-center py-8">
|
||||||
<FaExclamationTriangle className="w-12 h-12 mx-auto mb-4 text-yellow-500" />
|
<FaExclamationTriangle className="w-12 h-12 mx-auto mb-4 text-yellow-500" />
|
||||||
<h4 className="font-semibold text-slate-800 mb-2">
|
<h4 className="font-semibold text-slate-800 dark:text-slate-100 mb-2">
|
||||||
{translate('::App.Listforms.ImportManager.NoDataFoundTitle')}
|
{translate('::App.Listforms.ImportManager.NoDataFoundTitle')}
|
||||||
</h4>
|
</h4>
|
||||||
<p className="text-slate-600">
|
<p className="text-slate-600 dark:text-slate-400">
|
||||||
{translate('::App.Listforms.ImportManager.NoDataFoundDescription')}
|
{translate('::App.Listforms.ImportManager.NoDataFoundDescription')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="p-6 border-b border-slate-200">
|
<div className="p-6 border-b border-slate-200 dark:border-slate-700">
|
||||||
<div className="text-center py-8">
|
<div className="text-center py-8">
|
||||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500 mx-auto mb-4"></div>
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-500 mx-auto mb-4"></div>
|
||||||
<h4 className="font-semibold text-slate-800 mb-2">
|
<h4 className="font-semibold text-slate-800 dark:text-slate-100 mb-2">
|
||||||
{translate('::App.Listforms.ImportManager.LoadingPreviewTitle')}
|
{translate('::App.Listforms.ImportManager.LoadingPreviewTitle')}
|
||||||
</h4>
|
</h4>
|
||||||
<p className="text-slate-600">
|
<p className="text-slate-600 dark:text-slate-400">
|
||||||
{translate('::App.Listforms.ImportManager.LoadingPreviewDescription')}
|
{translate('::App.Listforms.ImportManager.LoadingPreviewDescription')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -271,8 +271,8 @@ export const ImportPreview: React.FC<ImportPreviewProps> = ({
|
||||||
<div className="p-3">
|
<div className="p-3">
|
||||||
{/* Success Message */}
|
{/* Success Message */}
|
||||||
{showSuccessMessage && lastExecutionResult && (
|
{showSuccessMessage && lastExecutionResult && (
|
||||||
<div className="mb-4 p-4 bg-green-50 border border-green-200 rounded-lg">
|
<div className="mb-4 p-4 bg-green-50 dark:bg-green-950/30 border border-green-200 dark:border-green-900/60 rounded-lg">
|
||||||
<div className="flex items-center space-x-2 text-green-700">
|
<div className="flex items-center space-x-2 text-green-700 dark:text-green-300">
|
||||||
<FaCheckCircle className="w-5 h-5" />
|
<FaCheckCircle className="w-5 h-5" />
|
||||||
<span className="font-medium">
|
<span className="font-medium">
|
||||||
{translate('::App.Listforms.ImportManager.ImportProgress.Status.Uploaded')}{' '}
|
{translate('::App.Listforms.ImportManager.ImportProgress.Status.Uploaded')}{' '}
|
||||||
|
|
@ -286,7 +286,7 @@ export const ImportPreview: React.FC<ImportPreviewProps> = ({
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
{selectedRows.length === 0 && (previewData?.rows?.length || 0) > 0 && (
|
{selectedRows.length === 0 && (previewData?.rows?.length || 0) > 0 && (
|
||||||
<div className="flex items-center space-x-2 text-orange-600">
|
<div className="flex items-center space-x-2 text-orange-600 dark:text-orange-300">
|
||||||
<FaExclamationTriangle className="w-5 h-5" />
|
<FaExclamationTriangle className="w-5 h-5" />
|
||||||
<span className="font-medium">
|
<span className="font-medium">
|
||||||
{translate('::App.Listforms.ImportManager.SelectRowsWarning')}
|
{translate('::App.Listforms.ImportManager.SelectRowsWarning')}
|
||||||
|
|
@ -295,7 +295,7 @@ export const ImportPreview: React.FC<ImportPreviewProps> = ({
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{selectedRows.length > 0 && (
|
{selectedRows.length > 0 && (
|
||||||
<div className="flex items-center space-x-2 text-blue-600">
|
<div className="flex items-center space-x-2 text-blue-600 dark:text-blue-300">
|
||||||
<FaCheckCircle className="w-5 h-5" />
|
<FaCheckCircle className="w-5 h-5" />
|
||||||
<span className="font-medium">
|
<span className="font-medium">
|
||||||
{selectedRows.length} {translate('::App.Listforms.ImportManager.RowsSelected')}
|
{selectedRows.length} {translate('::App.Listforms.ImportManager.RowsSelected')}
|
||||||
|
|
@ -304,7 +304,7 @@ export const ImportPreview: React.FC<ImportPreviewProps> = ({
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{(previewData?.rows?.length || 0) === 0 && (
|
{(previewData?.rows?.length || 0) === 0 && (
|
||||||
<div className="flex items-center space-x-2 text-red-600">
|
<div className="flex items-center space-x-2 text-red-600 dark:text-red-300">
|
||||||
<FaExclamationTriangle className="w-5 h-5" />
|
<FaExclamationTriangle className="w-5 h-5" />
|
||||||
<span className="font-medium">
|
<span className="font-medium">
|
||||||
{translate('::App.Listforms.ImportManager.NoRowsAvailable')}
|
{translate('::App.Listforms.ImportManager.NoRowsAvailable')}
|
||||||
|
|
@ -314,7 +314,7 @@ export const ImportPreview: React.FC<ImportPreviewProps> = ({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex space-x-3">
|
<div className="flex space-x-3">
|
||||||
<button className="px-4 py-2 text-slate-600 hover:text-slate-800 hover:bg-slate-100 rounded-lg transition-colors flex items-center space-x-2">
|
<button className="px-4 py-2 text-slate-600 hover:text-slate-800 hover:bg-slate-100 dark:text-slate-300 dark:hover:text-slate-100 dark:hover:bg-slate-800 rounded-lg transition-colors flex items-center space-x-2">
|
||||||
<FaTimes className="w-4 h-4" />
|
<FaTimes className="w-4 h-4" />
|
||||||
<span>{translate('::Cancel')}</span>
|
<span>{translate('::Cancel')}</span>
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -59,15 +59,17 @@ export const ImportProgress: React.FC<ImportProgressProps> = ({ session }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
|
<div className="bg-white dark:bg-slate-900 rounded-xl shadow-sm border border-slate-200 dark:border-slate-700 p-6">
|
||||||
<div className="text-center space-y-6">
|
<div className="text-center space-y-6">
|
||||||
{/* Status Icon */}
|
{/* Status Icon */}
|
||||||
<div className="flex justify-center">{getStatusIcon()}</div>
|
<div className="flex justify-center">{getStatusIcon()}</div>
|
||||||
|
|
||||||
{/* Status Message */}
|
{/* Status Message */}
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-xl font-semibold text-slate-800 mb-2">{getStatusMessage()}</h3>
|
<h3 className="text-xl font-semibold text-slate-800 dark:text-slate-100 mb-2">
|
||||||
<p className="text-slate-600">
|
{getStatusMessage()}
|
||||||
|
</h3>
|
||||||
|
<p className="text-slate-600 dark:text-slate-400">
|
||||||
{translate('::App.Listforms.Status.Processing')}{' '}
|
{translate('::App.Listforms.Status.Processing')}{' '}
|
||||||
{session.blobName}
|
{session.blobName}
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -75,11 +77,11 @@ export const ImportProgress: React.FC<ImportProgressProps> = ({ session }) => {
|
||||||
|
|
||||||
{/* Progress Bar */}
|
{/* Progress Bar */}
|
||||||
<div className="w-full max-w-md mx-auto">
|
<div className="w-full max-w-md mx-auto">
|
||||||
<div className="flex justify-between text-sm text-slate-600 mb-2">
|
<div className="flex justify-between text-sm text-slate-600 dark:text-slate-400 mb-2">
|
||||||
<span>{translate('::App.Listforms.ImportManager.ImportProgress.ProgressLabel')}</span>
|
<span>{translate('::App.Listforms.ImportManager.ImportProgress.ProgressLabel')}</span>
|
||||||
<span>{Math.round(getProgressPercentage())}%</span>
|
<span>{Math.round(getProgressPercentage())}%</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full bg-slate-200 rounded-full h-2">
|
<div className="w-full bg-slate-200 dark:bg-slate-700 rounded-full h-2">
|
||||||
<div
|
<div
|
||||||
className="bg-blue-500 h-2 rounded-full transition-all duration-300 ease-out"
|
className="bg-blue-500 h-2 rounded-full transition-all duration-300 ease-out"
|
||||||
style={{ width: `${getProgressPercentage()}%` }}
|
style={{ width: `${getProgressPercentage()}%` }}
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ const MobileNav = () => {
|
||||||
title="Navigation"
|
title="Navigation"
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
bodyClass={classNames(navColor(), 'p-0')}
|
bodyClass={classNames(navColor(), 'p-0')}
|
||||||
width={330}
|
width={320}
|
||||||
placement={direction === DIR_RTL ? 'right' : 'left'}
|
placement={direction === DIR_RTL ? 'right' : 'left'}
|
||||||
onClose={onDrawerClose}
|
onClose={onDrawerClose}
|
||||||
onRequestClose={onDrawerClose}
|
onRequestClose={onDrawerClose}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,19 @@ const DialogContext = createContext<DialogContextValue>({ isMaximized: false })
|
||||||
|
|
||||||
export const useDialogContext = () => useContext(DialogContext)
|
export const useDialogContext = () => useContext(DialogContext)
|
||||||
|
|
||||||
|
const DialogHeader = ({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
children?: ReactNode
|
||||||
|
className?: string
|
||||||
|
}) => (
|
||||||
|
<div className={classNames('dialog-header', className)}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
DialogHeader.displayName = 'Dialog.Header'
|
||||||
|
|
||||||
const DialogBody = ({
|
const DialogBody = ({
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
|
|
@ -24,14 +37,8 @@ const DialogBody = ({
|
||||||
children?: ReactNode
|
children?: ReactNode
|
||||||
className?: string
|
className?: string
|
||||||
}) => {
|
}) => {
|
||||||
const { isMaximized } = useContext(DialogContext)
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div className={classNames('dialog-body', className)}>
|
||||||
className={classNames(
|
|
||||||
isMaximized && 'flex-1 min-h-0 flex flex-col overflow-hidden',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
@ -45,9 +52,8 @@ const DialogFooter = ({
|
||||||
children?: ReactNode
|
children?: ReactNode
|
||||||
className?: string
|
className?: string
|
||||||
}) => {
|
}) => {
|
||||||
const { isMaximized } = useContext(DialogContext)
|
|
||||||
return (
|
return (
|
||||||
<div className={classNames(isMaximized && 'flex-shrink-0', className)}>
|
<div className={classNames('dialog-footer', className)}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
@ -231,13 +237,7 @@ const Dialog = (props: DialogProps) => {
|
||||||
{closable && !showWindowControls && renderCloseButton}
|
{closable && !showWindowControls && renderCloseButton}
|
||||||
{closable && showWindowControls && renderWindowControls}
|
{closable && showWindowControls && renderWindowControls}
|
||||||
<DialogContext.Provider value={{ isMaximized }}>
|
<DialogContext.Provider value={{ isMaximized }}>
|
||||||
{isMaximized ? (
|
{children}
|
||||||
<div className="flex-1 min-h-0 flex flex-col overflow-hidden">
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
children
|
|
||||||
)}
|
|
||||||
</DialogContext.Provider>
|
</DialogContext.Provider>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
@ -247,11 +247,13 @@ const Dialog = (props: DialogProps) => {
|
||||||
Dialog.displayName = 'Dialog'
|
Dialog.displayName = 'Dialog'
|
||||||
|
|
||||||
type DialogType = typeof Dialog & {
|
type DialogType = typeof Dialog & {
|
||||||
|
Header: typeof DialogHeader
|
||||||
Body: typeof DialogBody
|
Body: typeof DialogBody
|
||||||
Footer: typeof DialogFooter
|
Footer: typeof DialogFooter
|
||||||
}
|
}
|
||||||
|
|
||||||
const DialogWithSubComponents = Dialog as DialogType
|
const DialogWithSubComponents = Dialog as DialogType
|
||||||
|
DialogWithSubComponents.Header = DialogHeader
|
||||||
DialogWithSubComponents.Body = DialogBody
|
DialogWithSubComponents.Body = DialogBody
|
||||||
DialogWithSubComponents.Footer = DialogFooter
|
DialogWithSubComponents.Footer = DialogFooter
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ export interface ListFormWizardColumnItemDto {
|
||||||
editorScript: string
|
editorScript: string
|
||||||
colSpan: number
|
colSpan: number
|
||||||
isRequired: boolean
|
isRequired: boolean
|
||||||
|
includeInEditingForm: boolean
|
||||||
dbSourceType: number
|
dbSourceType: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -54,6 +55,7 @@ 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
|
||||||
|
|
@ -113,6 +115,7 @@ 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
|
||||||
|
|
|
||||||
|
|
@ -908,6 +908,7 @@ 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
|
||||||
|
|
|
||||||
24
ui/src/services/auditLog.service.ts
Normal file
24
ui/src/services/auditLog.service.ts
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
import { PagedResultDto } from '@/proxy'
|
||||||
|
import { AuditLogDto } from '@/proxy/auditLog/audit-log'
|
||||||
|
import apiService from '@/services/api.service'
|
||||||
|
|
||||||
|
export interface AuditLogListRequestDto {
|
||||||
|
skipCount?: number
|
||||||
|
maxResultCount?: number
|
||||||
|
sorting?: string
|
||||||
|
listFormCode?: string
|
||||||
|
entityId?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
class AuditLogService {
|
||||||
|
async getList(params?: AuditLogListRequestDto): Promise<PagedResultDto<AuditLogDto>> {
|
||||||
|
const response = await apiService.fetchData<PagedResultDto<AuditLogDto>>({
|
||||||
|
url: '/api/app/audit-log',
|
||||||
|
method: 'GET',
|
||||||
|
params,
|
||||||
|
})
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const auditLogService = new AuditLogService()
|
||||||
|
|
@ -11,6 +11,11 @@ import { ListResultDto, PagedAndSortedResultRequestDto, PagedResultDto } from '.
|
||||||
import { AuditLogDto } from '../proxy/auditLog/audit-log'
|
import { 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',
|
||||||
|
|
@ -36,6 +41,21 @@ 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',
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ 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'> & {
|
||||||
|
|
|
||||||
|
|
@ -328,6 +328,91 @@ export function emptyCriteria(kind = 'Compare', listFormCode = ''): WorkflowCrit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function uniqueCriteriaTitle(
|
||||||
|
kind: string,
|
||||||
|
criteria: Array<Pick<WorkflowCriteriaDto, 'id' | 'kind' | 'title'>>,
|
||||||
|
currentId?: string | null,
|
||||||
|
preferredTitle?: string | null,
|
||||||
|
) {
|
||||||
|
const hasPreferredTitle = Boolean(preferredTitle?.trim())
|
||||||
|
const baseTitle = (preferredTitle || defaultTitle(kind)).trim()
|
||||||
|
const usedTitles = new Set(
|
||||||
|
criteria
|
||||||
|
.filter((item) => !currentId || item.id !== currentId)
|
||||||
|
.map((item) => (item.title || '').trim().toLocaleLowerCase('tr-TR'))
|
||||||
|
.filter(Boolean),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!hasPreferredTitle) {
|
||||||
|
const sameKindCount = criteria.filter(
|
||||||
|
(item) =>
|
||||||
|
(!currentId || item.id !== currentId) &&
|
||||||
|
item.kind === kind &&
|
||||||
|
isDefaultTitleVariant(item.title, baseTitle),
|
||||||
|
).length
|
||||||
|
let index = sameKindCount + 1
|
||||||
|
let candidate = `${baseTitle}${index}`
|
||||||
|
while (usedTitles.has(candidate.toLocaleLowerCase('tr-TR'))) {
|
||||||
|
index += 1
|
||||||
|
candidate = `${baseTitle}${index}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return candidate
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!usedTitles.has(baseTitle.toLocaleLowerCase('tr-TR'))) {
|
||||||
|
return baseTitle
|
||||||
|
}
|
||||||
|
|
||||||
|
let index = 1
|
||||||
|
let candidate = `${baseTitle}${index}`
|
||||||
|
while (usedTitles.has(candidate.toLocaleLowerCase('tr-TR'))) {
|
||||||
|
index += 1
|
||||||
|
candidate = `${baseTitle}${index}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return candidate
|
||||||
|
}
|
||||||
|
|
||||||
|
export function uniqueCriteriaId(
|
||||||
|
criteria: Array<Pick<WorkflowCriteriaDto, 'id'>>,
|
||||||
|
reservedIds: string[] = [],
|
||||||
|
) {
|
||||||
|
const usedIds = new Set(
|
||||||
|
[...criteria.map((item) => item.id), ...reservedIds]
|
||||||
|
.map((id) => (id || '').trim().toLocaleLowerCase('tr-TR'))
|
||||||
|
.filter(Boolean),
|
||||||
|
)
|
||||||
|
const maxNumber = [...usedIds].reduce((max, id) => Math.max(max, parseCriteriaIdNumber(id)), 0)
|
||||||
|
let nextNumber = maxNumber + 1
|
||||||
|
let candidate = formatCriteriaId(nextNumber)
|
||||||
|
|
||||||
|
while (usedIds.has(candidate.toLocaleLowerCase('tr-TR'))) {
|
||||||
|
nextNumber += 1
|
||||||
|
candidate = formatCriteriaId(nextNumber)
|
||||||
|
}
|
||||||
|
|
||||||
|
return candidate
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseCriteriaIdNumber(id: string) {
|
||||||
|
const match = id.match(/^(?:n)?(\d+)$/iu)
|
||||||
|
return match ? Number(match[1]) : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatCriteriaId(number: number) {
|
||||||
|
return `N${String(number).padStart(3, '0')}`.slice(-4)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isDefaultTitleVariant(title: string | null | undefined, baseTitle: string) {
|
||||||
|
const normalized = (title || '').trim()
|
||||||
|
return normalized === baseTitle || new RegExp(`^${escapeRegExp(baseTitle)}\\d+$`, 'u').test(normalized)
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeRegExp(value: string) {
|
||||||
|
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||||
|
}
|
||||||
|
|
||||||
export function toCriteriaForm(item: WorkflowCriteriaDto): WorkflowCriteriaForm {
|
export function toCriteriaForm(item: WorkflowCriteriaDto): WorkflowCriteriaForm {
|
||||||
const sharedPerson = item.approver || ''
|
const sharedPerson = item.approver || ''
|
||||||
|
|
||||||
|
|
@ -347,7 +432,8 @@ export function toCriteriaForm(item: WorkflowCriteriaDto): WorkflowCriteriaForm
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeCriteria(item: WorkflowCriteriaForm): SaveCriteriaInput {
|
export function normalizeCriteria(item: WorkflowCriteriaForm): SaveCriteriaInput {
|
||||||
const sharedPerson = item.approver || ''
|
const sharedPerson =
|
||||||
|
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())
|
||||||
|
|
@ -504,7 +590,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} ${item.approver ? `- ${item.approver}` : ''}`
|
return item.title
|
||||||
}
|
}
|
||||||
|
|
||||||
export function targetTitle(criteria: WorkflowCriteriaDto[], id?: string | null) {
|
export function targetTitle(criteria: WorkflowCriteriaDto[], id?: string | null) {
|
||||||
|
|
|
||||||
|
|
@ -5,15 +5,16 @@ 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, FormContainer, FormItem, Input, Select } from '@/components/ui'
|
import { Button, Card, Checkbox, FormContainer, FormItem, Input, Select } from '@/components/ui'
|
||||||
import { ListFormEditTabs } from '@/proxy/admin/list-form/options'
|
import { ListFormEditTabs } from '@/proxy/admin/list-form/options'
|
||||||
import { object, string } from 'yup'
|
import { bool, 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'
|
||||||
|
|
@ -112,9 +113,17 @@ export function FormTabWorkflow(
|
||||||
const saveCriteria = (event: FormEvent<HTMLFormElement>) => {
|
const saveCriteria = (event: FormEvent<HTMLFormElement>) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
runAction(async () => {
|
runAction(async () => {
|
||||||
|
const normalized = normalizeCriteria(criteriaForm)
|
||||||
|
const preferredTitle = criteriaForm.title || normalized.title
|
||||||
await workflowService.saveCriteria({
|
await workflowService.saveCriteria({
|
||||||
...normalizeCriteria(criteriaForm),
|
...normalized,
|
||||||
listFormCode: props.listFormCode,
|
listFormCode: props.listFormCode,
|
||||||
|
title: uniqueCriteriaTitle(
|
||||||
|
normalized.kind || '',
|
||||||
|
currentCriteria,
|
||||||
|
normalized.id,
|
||||||
|
preferredTitle,
|
||||||
|
),
|
||||||
})
|
})
|
||||||
setSelectedId('')
|
setSelectedId('')
|
||||||
})
|
})
|
||||||
|
|
@ -123,9 +132,11 @@ export function FormTabWorkflow(
|
||||||
const addCriteria = (kind: string) => {
|
const addCriteria = (kind: string) => {
|
||||||
setDesignerTab('flow')
|
setDesignerTab('flow')
|
||||||
runAction(async () => {
|
runAction(async () => {
|
||||||
|
const nextTitle = uniqueCriteriaTitle(kind, currentCriteria)
|
||||||
const saved = await workflowService.saveCriteria({
|
const saved = await workflowService.saveCriteria({
|
||||||
...normalizeCriteria(emptyCriteria(kind, props.listFormCode)),
|
...normalizeCriteria(emptyCriteria(kind, props.listFormCode)),
|
||||||
listFormCode: props.listFormCode,
|
listFormCode: props.listFormCode,
|
||||||
|
title: nextTitle,
|
||||||
positionX: 80 + (currentCriteria.length % 5) * 230,
|
positionX: 80 + (currentCriteria.length % 5) * 230,
|
||||||
positionY: 220 + Math.floor(currentCriteria.length / 5) * 140,
|
positionY: 220 + Math.floor(currentCriteria.length / 5) * 140,
|
||||||
})
|
})
|
||||||
|
|
@ -296,10 +307,13 @@ export function FormTabWorkflow(
|
||||||
}
|
}
|
||||||
|
|
||||||
const schema = object().shape({
|
const schema = object().shape({
|
||||||
approvalUserFieldName: string().required(),
|
workflowDto: object().shape({
|
||||||
approvalStatusFieldName: string().required(),
|
approvalUserFieldName: string().required(),
|
||||||
approvalDateFieldName: string(),
|
isFilterUserName: bool(),
|
||||||
approvalDescriptionFieldName: string(),
|
approvalStatusFieldName: string().required(),
|
||||||
|
approvalDateFieldName: string(),
|
||||||
|
approvalDescriptionFieldName: string(),
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
const initialValues = useStoreState((s) => s.admin.lists.values)
|
const initialValues = useStoreState((s) => s.admin.lists.values)
|
||||||
|
|
@ -320,7 +334,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-4 gap-2">
|
<div className="grid grid-cols-1 md:grid-cols-5 gap-2">
|
||||||
<FormItem
|
<FormItem
|
||||||
asterisk
|
asterisk
|
||||||
label={translate('::ListForms.ListFormEdit.Workflow.ApprovalUserFieldName')}
|
label={translate('::ListForms.ListFormEdit.Workflow.ApprovalUserFieldName')}
|
||||||
|
|
@ -421,9 +435,22 @@ 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" loading={isSubmitting}>
|
<Button block variant="solid" type="submit" loading={isSubmitting}>
|
||||||
{isSubmitting ? translate('::SavingWithThreeDot') : translate('::Save')}
|
{isSubmitting ? translate('::SavingWithThreeDot') : translate('::Save')}
|
||||||
</Button>
|
</Button>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ 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'
|
||||||
|
|
@ -70,6 +71,7 @@ const initialValues: ListFormWizardDto = {
|
||||||
languageTextMenuParentTr: '',
|
languageTextMenuParentTr: '',
|
||||||
permissionGroupName: '',
|
permissionGroupName: '',
|
||||||
menuParentCode: '',
|
menuParentCode: '',
|
||||||
|
menuParentIcon: '',
|
||||||
menuIcon: '',
|
menuIcon: '',
|
||||||
dataSourceCode: '',
|
dataSourceCode: '',
|
||||||
dataSourceConnectionString: '',
|
dataSourceConnectionString: '',
|
||||||
|
|
@ -94,6 +96,7 @@ const initialValues: ListFormWizardDto = {
|
||||||
widgets: [],
|
widgets: [],
|
||||||
workflow: {
|
workflow: {
|
||||||
approvalUserFieldName: '',
|
approvalUserFieldName: '',
|
||||||
|
isFilterUserName: false,
|
||||||
approvalDateFieldName: '',
|
approvalDateFieldName: '',
|
||||||
approvalStatusFieldName: '',
|
approvalStatusFieldName: '',
|
||||||
approvalDescriptionFieldName: '',
|
approvalDescriptionFieldName: '',
|
||||||
|
|
@ -221,6 +224,7 @@ 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: '',
|
||||||
|
|
@ -240,6 +244,18 @@ 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) {
|
||||||
|
|
@ -253,12 +269,13 @@ 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 selectableColumns = cols.filter((c) => !isAuditColumn(c.columnName))
|
const colNames = new Set(cols.map((c) => c.columnName.toLowerCase()))
|
||||||
|
const hasTenantColumn = colNames.has('tenantid')
|
||||||
|
const selectableColumns = cols.filter((c) => isAutoSelectedColumn(c.columnName, hasTenantColumn))
|
||||||
setSelectedColumns(new Set(selectableColumns.map((c) => c.columnName)))
|
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
|
||||||
const colNames = new Set(cols.map((c) => c.columnName.toLowerCase()))
|
formikRef.current?.setFieldValue('isTenant', hasTenantColumn)
|
||||||
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) {
|
||||||
|
|
@ -284,17 +301,23 @@ const Wizard = () => {
|
||||||
return next
|
return next
|
||||||
})
|
})
|
||||||
|
|
||||||
const toggleAllColumns = (all: boolean) =>
|
const toggleAllColumns = (all: boolean, isTenant = formikRef.current?.values.isTenant ?? false) =>
|
||||||
setSelectedColumns(
|
setSelectedColumns(
|
||||||
all
|
all
|
||||||
? new Set(
|
? new Set(
|
||||||
selectCommandColumns
|
selectCommandColumns
|
||||||
.filter((c) => !isAuditColumn(c.columnName))
|
.filter((c) => isAutoSelectedColumn(c.columnName, isTenant))
|
||||||
.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()
|
||||||
|
|
@ -391,6 +414,7 @@ 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 ?? '',
|
||||||
|
|
@ -415,6 +439,7 @@ const Wizard = () => {
|
||||||
widgets: w.widgets ?? [],
|
widgets: w.widgets ?? [],
|
||||||
workflow: w.workflow ?? {
|
workflow: w.workflow ?? {
|
||||||
approvalUserFieldName: '',
|
approvalUserFieldName: '',
|
||||||
|
isFilterUserName: false,
|
||||||
approvalDateFieldName: '',
|
approvalDateFieldName: '',
|
||||||
approvalStatusFieldName: '',
|
approvalStatusFieldName: '',
|
||||||
approvalDescriptionFieldName: '',
|
approvalDescriptionFieldName: '',
|
||||||
|
|
@ -453,6 +478,7 @@ 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}`,
|
||||||
|
|
@ -477,6 +503,7 @@ const Wizard = () => {
|
||||||
setWorkflow(
|
setWorkflow(
|
||||||
w.workflow ?? {
|
w.workflow ?? {
|
||||||
approvalUserFieldName: '',
|
approvalUserFieldName: '',
|
||||||
|
isFilterUserName: false,
|
||||||
approvalDateFieldName: '',
|
approvalDateFieldName: '',
|
||||||
approvalStatusFieldName: '',
|
approvalStatusFieldName: '',
|
||||||
approvalDescriptionFieldName: '',
|
approvalDescriptionFieldName: '',
|
||||||
|
|
@ -516,24 +543,38 @@ 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)
|
||||||
|
|
||||||
formikRef.current?.setFieldValue('wizardName', name)
|
const setAutoText = (field: keyof Pick<
|
||||||
formikRef.current?.setFieldValue('listFormCode', derived)
|
ListFormWizardDto,
|
||||||
formikRef.current?.setFieldValue('menuCode', derived)
|
| 'languageTextMenuEn'
|
||||||
formikRef.current?.setFieldValue('languageTextMenuEn', spacedLabel)
|
| 'languageTextMenuTr'
|
||||||
formikRef.current?.setFieldValue('languageTextMenuTr', spacedLabel)
|
| 'languageTextTitleEn'
|
||||||
formikRef.current?.setFieldValue('languageTextTitleEn', spacedLabel)
|
| 'languageTextTitleTr'
|
||||||
formikRef.current?.setFieldValue('languageTextTitleTr', spacedLabel)
|
| 'languageTextDescEn'
|
||||||
formikRef.current?.setFieldValue('languageTextDescEn', spacedLabel)
|
| 'languageTextDescTr'
|
||||||
formikRef.current?.setFieldValue('languageTextDescTr', spacedLabel)
|
>) => {
|
||||||
|
const current = formik?.values[field]
|
||||||
|
if (!current || current === previousSpacedLabel) {
|
||||||
|
formik?.setFieldValue(field, spacedLabel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
formik?.setFieldValue('wizardName', name)
|
||||||
|
formik?.setFieldValue('listFormCode', derived)
|
||||||
|
formik?.setFieldValue('menuCode', derived)
|
||||||
|
setAutoText('languageTextMenuEn')
|
||||||
|
setAutoText('languageTextMenuTr')
|
||||||
|
setAutoText('languageTextTitleEn')
|
||||||
|
setAutoText('languageTextTitleTr')
|
||||||
|
setAutoText('languageTextDescEn')
|
||||||
|
setAutoText('languageTextDescTr')
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleMenuParentChange = (code: string) => {
|
const applyPermissionGroupFromRoot = (rootCode: string) => {
|
||||||
formikRef.current?.setFieldValue('menuParentCode', code)
|
|
||||||
if (!code) return
|
|
||||||
const rootCode = findRootCode(rawMenuItems, code)
|
|
||||||
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
|
||||||
|
|
@ -565,6 +606,32 @@ 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()
|
||||||
|
|
@ -586,6 +653,19 @@ 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()
|
||||||
|
|
@ -627,6 +707,7 @@ 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,
|
||||||
|
|
@ -676,7 +757,10 @@ const Wizard = () => {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="mb-6 mt-2">
|
<div className="mb-6 mt-2">
|
||||||
<Steps current={currentStep}>
|
<Steps
|
||||||
|
current={currentStep}
|
||||||
|
className="flex flex-row flex-wrap !justify-start gap-y-2 lg:flex-nowrap lg:!justify-between"
|
||||||
|
>
|
||||||
<Steps.Item title={translate('::ListForms.Wizard.MenuInfo') || 'Menu Info'} />
|
<Steps.Item title={translate('::ListForms.Wizard.MenuInfo') || 'Menu Info'} />
|
||||||
<Steps.Item
|
<Steps.Item
|
||||||
title={translate('::ListForms.Wizard.ListFormSettings') || 'List Form Settings'}
|
title={translate('::ListForms.Wizard.ListFormSettings') || 'List Form Settings'}
|
||||||
|
|
@ -701,40 +785,12 @@ const Wizard = () => {
|
||||||
innerRef={formikRef}
|
innerRef={formikRef}
|
||||||
initialValues={{ ...initialValues }}
|
initialValues={{ ...initialValues }}
|
||||||
validationSchema={listFormValidationSchema}
|
validationSchema={listFormValidationSchema}
|
||||||
onSubmit={async (values, { setSubmitting }) => {
|
onSubmit={(_, { setSubmitting }) => {
|
||||||
setSubmitting(true)
|
setSubmitting(false)
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{({ touched, errors, isSubmitting, values }) => (
|
{({ touched, errors, isSubmitting, values }) => (
|
||||||
<Form>
|
<Form onKeyDown={preventEnterSubmit}>
|
||||||
<FormContainer size={currentStep >= 2 ? undefined : 'sm'}>
|
<FormContainer size={currentStep >= 2 ? undefined : 'sm'}>
|
||||||
{/* ─── Step 1: Basic Info ─────────────────────────────── */}
|
{/* ─── Step 1: Basic Info ─────────────────────────────── */}
|
||||||
{currentStep === 0 && (
|
{currentStep === 0 && (
|
||||||
|
|
@ -748,7 +804,11 @@ const Wizard = () => {
|
||||||
menuTree={menuTree}
|
menuTree={menuTree}
|
||||||
isLoadingMenu={isLoadingMenu}
|
isLoadingMenu={isLoadingMenu}
|
||||||
onMenuParentChange={handleMenuParentChange}
|
onMenuParentChange={handleMenuParentChange}
|
||||||
onClearMenuParent={() => formikRef.current?.setFieldValue('menuParentCode', '')}
|
onClearMenuParent={() => {
|
||||||
|
formikRef.current?.setFieldValue('menuParentCode', '')
|
||||||
|
formikRef.current?.setFieldValue('menuParentIcon', '')
|
||||||
|
}}
|
||||||
|
onMenuCreated={handleMenuCreated}
|
||||||
onReloadMenu={getMenuList}
|
onReloadMenu={getMenuList}
|
||||||
permissionGroupList={permissionGroupList}
|
permissionGroupList={permissionGroupList}
|
||||||
isLoadingPermissionGroup={isLoadingPermissionGroup}
|
isLoadingPermissionGroup={isLoadingPermissionGroup}
|
||||||
|
|
@ -770,6 +830,7 @@ 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}
|
||||||
|
|
@ -779,7 +840,8 @@ const Wizard = () => {
|
||||||
setSelectedColumns(new Set())
|
setSelectedColumns(new Set())
|
||||||
}}
|
}}
|
||||||
onToggleColumn={toggleColumn}
|
onToggleColumn={toggleColumn}
|
||||||
onToggleAllColumns={toggleAllColumns}
|
onToggleAllColumns={(all) => toggleAllColumns(all, values.isTenant)}
|
||||||
|
onTenantChange={handleTenantChange}
|
||||||
translate={translate}
|
translate={translate}
|
||||||
onBack={handleBack}
|
onBack={handleBack}
|
||||||
onNext={handleNext2}
|
onNext={handleNext2}
|
||||||
|
|
|
||||||
|
|
@ -414,6 +414,14 @@ 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
|
||||||
|
|
@ -432,6 +440,7 @@ const WizardStep1 = ({
|
||||||
isLoadingMenu,
|
isLoadingMenu,
|
||||||
onMenuParentChange,
|
onMenuParentChange,
|
||||||
onClearMenuParent,
|
onClearMenuParent,
|
||||||
|
onMenuCreated,
|
||||||
onReloadMenu,
|
onReloadMenu,
|
||||||
permissionGroupList,
|
permissionGroupList,
|
||||||
isLoadingPermissionGroup,
|
isLoadingPermissionGroup,
|
||||||
|
|
@ -544,7 +553,7 @@ const WizardStep1 = ({
|
||||||
initialParentCode={menuDialogParentCode}
|
initialParentCode={menuDialogParentCode}
|
||||||
initialOrder={menuDialogInitialOrder}
|
initialOrder={menuDialogInitialOrder}
|
||||||
rawItems={rawMenuItems}
|
rawItems={rawMenuItems}
|
||||||
onSaved={onReloadMenu}
|
onSaved={onMenuCreated}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,13 @@ 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 } from 'formik'
|
import { Field, FieldProps, FormikErrors, FormikTouched, useFormikContext } from 'formik'
|
||||||
|
import { useState } from 'react'
|
||||||
import CreatableSelect from 'react-select/creatable'
|
import CreatableSelect from 'react-select/creatable'
|
||||||
import { FaArrowLeft, FaArrowRight } from 'react-icons/fa'
|
import { FaArrowLeft, FaArrowRight, FaPlus, FaTable } 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 ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
@ -23,6 +25,7 @@ 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
|
||||||
|
|
@ -31,6 +34,7 @@ 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
|
||||||
|
|
@ -50,6 +54,7 @@ const WizardStep2 = ({
|
||||||
onDataSourceNewChange,
|
onDataSourceNewChange,
|
||||||
dbObjects,
|
dbObjects,
|
||||||
isLoadingDbObjects,
|
isLoadingDbObjects,
|
||||||
|
onDbObjectsRefresh,
|
||||||
selectCommandColumns,
|
selectCommandColumns,
|
||||||
isLoadingColumns,
|
isLoadingColumns,
|
||||||
selectedColumns,
|
selectedColumns,
|
||||||
|
|
@ -57,10 +62,50 @@ const WizardStep2 = ({
|
||||||
onClearColumns,
|
onClearColumns,
|
||||||
onToggleColumn,
|
onToggleColumn,
|
||||||
onToggleAllColumns,
|
onToggleAllColumns,
|
||||||
|
onTenantChange,
|
||||||
translate,
|
translate,
|
||||||
onBack,
|
onBack,
|
||||||
onNext,
|
onNext,
|
||||||
}: WizardStep2Props) => {
|
}: WizardStep2Props) => {
|
||||||
|
const [showTableDesignerDialog, setShowTableDesignerDialog] = useState(false)
|
||||||
|
const [designTableData, setDesignTableData] = useState<{
|
||||||
|
schemaName: string
|
||||||
|
tableName: string
|
||||||
|
} | null>(null)
|
||||||
|
const formik = useFormikContext<ListFormWizardDto>()
|
||||||
|
|
||||||
|
const selectedTable = values.selectCommand
|
||||||
|
? dbObjects?.tables.find(
|
||||||
|
(table) =>
|
||||||
|
table.tableName === values.selectCommand ||
|
||||||
|
table.fullName === values.selectCommand ||
|
||||||
|
`${table.schemaName}.${table.tableName}` === values.selectCommand,
|
||||||
|
)
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
const handleNewTable = () => {
|
||||||
|
setDesignTableData(null)
|
||||||
|
setShowTableDesignerDialog(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDesignTable = () => {
|
||||||
|
if (!selectedTable) return
|
||||||
|
setDesignTableData({
|
||||||
|
schemaName: selectedTable.schemaName,
|
||||||
|
tableName: selectedTable.tableName,
|
||||||
|
})
|
||||||
|
setShowTableDesignerDialog(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
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'),
|
||||||
|
|
@ -217,7 +262,9 @@ const WizardStep2 = ({
|
||||||
]
|
]
|
||||||
: []
|
: []
|
||||||
return (
|
return (
|
||||||
<Select
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<Select
|
||||||
field={field}
|
field={field}
|
||||||
form={form}
|
form={form}
|
||||||
isClearable
|
isClearable
|
||||||
|
|
@ -256,7 +303,27 @@ const WizardStep2 = ({
|
||||||
form.setFieldTouched('keyFieldName', false)
|
form.setFieldTouched('keyFieldName', false)
|
||||||
onClearColumns()
|
onClearColumns()
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="solid"
|
||||||
|
icon={<FaPlus />}
|
||||||
|
disabled={!values.dataSourceCode}
|
||||||
|
onClick={handleNewTable}
|
||||||
|
>
|
||||||
|
New Table
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="default"
|
||||||
|
icon={<FaTable />}
|
||||||
|
disabled={!values.dataSourceCode || !selectedTable}
|
||||||
|
onClick={handleDesignTable}
|
||||||
|
>
|
||||||
|
Design Table
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
</Field>
|
</Field>
|
||||||
|
|
@ -328,7 +395,18 @@ const WizardStep2 = ({
|
||||||
invalid={!!(errors.isTenant && touched.isTenant)}
|
invalid={!!(errors.isTenant && touched.isTenant)}
|
||||||
errorMessage={errors.isTenant}
|
errorMessage={errors.isTenant}
|
||||||
>
|
>
|
||||||
<Field type="checkbox" autoComplete="off" name="isTenant" component={Checkbox} />
|
<Field name="isTenant">
|
||||||
|
{({ field, form }: FieldProps<boolean>) => (
|
||||||
|
<Checkbox
|
||||||
|
name={field.name}
|
||||||
|
checked={Boolean(field.value)}
|
||||||
|
onChange={(checked) => {
|
||||||
|
form.setFieldValue(field.name, checked)
|
||||||
|
onTenantChange(checked)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Field>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|
||||||
<FormItem
|
<FormItem
|
||||||
|
|
@ -700,6 +778,7 @@ 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"
|
||||||
>
|
>
|
||||||
|
|
@ -707,6 +786,7 @@ 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"
|
||||||
>
|
>
|
||||||
|
|
@ -785,6 +865,17 @@ const WizardStep2 = ({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<SqlTableDesignerDialog
|
||||||
|
isOpen={showTableDesignerDialog}
|
||||||
|
onClose={() => {
|
||||||
|
setShowTableDesignerDialog(false)
|
||||||
|
setDesignTableData(null)
|
||||||
|
}}
|
||||||
|
dataSource={values.dataSourceCode}
|
||||||
|
initialTableData={designTableData}
|
||||||
|
onDeployed={handleTableDeployed}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@ 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
|
||||||
|
|
@ -114,6 +115,7 @@ 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}`,
|
||||||
|
|
@ -179,6 +181,7 @@ 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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -194,6 +197,7 @@ function SortableItem({
|
||||||
onEditorScriptChange,
|
onEditorScriptChange,
|
||||||
onColSpanChange,
|
onColSpanChange,
|
||||||
onRequiredChange,
|
onRequiredChange,
|
||||||
|
onIncludeInEditingFormChange,
|
||||||
onCaptionNameChange,
|
onCaptionNameChange,
|
||||||
onLookupDataSourceTypeChange,
|
onLookupDataSourceTypeChange,
|
||||||
onDisplayExprChange,
|
onDisplayExprChange,
|
||||||
|
|
@ -561,7 +565,7 @@ function SortableItem({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Bottom row: ColSpan + Required */}
|
{/* Bottom row: ColSpan + Editing Form + 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">
|
||||||
|
|
@ -579,6 +583,20 @@ 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"
|
||||||
|
|
@ -723,6 +741,9 @@ 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 })
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 variant="plain" onClick={() => setDeleteIndex(null)}>
|
<Button type="button" variant="plain" onClick={() => setDeleteIndex(null)}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="solid" onClick={removeSubForm}>
|
<Button type="button" variant="solid" onClick={removeSubForm}>
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
</Dialog.Footer>
|
</Dialog.Footer>
|
||||||
|
|
|
||||||
|
|
@ -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 variant="plain" onClick={() => setDeleteIndex(null)}>
|
<Button type="button" variant="plain" onClick={() => setDeleteIndex(null)}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="solid" onClick={removeWidget}>
|
<Button type="button" variant="solid" onClick={removeWidget}>
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
</Dialog.Footer>
|
</Dialog.Footer>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Button, Card, FormContainer, FormItem, Select } from '@/components/ui'
|
import { Button, Card, Checkbox, FormContainer, FormItem, Select } from '@/components/ui'
|
||||||
import type { DatabaseColumnDto } from '@/proxy/sql-query-manager/models'
|
import 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,6 +9,8 @@ import {
|
||||||
emptyCriteria,
|
emptyCriteria,
|
||||||
normalizeCriteria,
|
normalizeCriteria,
|
||||||
toCriteriaForm,
|
toCriteriaForm,
|
||||||
|
uniqueCriteriaId,
|
||||||
|
uniqueCriteriaTitle,
|
||||||
type WorkflowCriteriaForm,
|
type WorkflowCriteriaForm,
|
||||||
} from '@/utils/workflow/workflowHelpers'
|
} from '@/utils/workflow/workflowHelpers'
|
||||||
import { Field, FieldProps, Form, Formik } from 'formik'
|
import { Field, FieldProps, Form, Formik } from 'formik'
|
||||||
|
|
@ -16,6 +18,7 @@ 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
|
||||||
|
|
@ -44,8 +47,6 @@ const toDesignerCriteria = (items: ListFormWorkflowCriteriaDto[]): WorkflowCrite
|
||||||
const toWizardCriteria = (items: WorkflowCriteriaDto[]): ListFormWorkflowCriteriaDto[] =>
|
const toWizardCriteria = (items: WorkflowCriteriaDto[]): ListFormWorkflowCriteriaDto[] =>
|
||||||
items.map(({ nodeId: _nodeId, ...item }) => item)
|
items.map(({ nodeId: _nodeId, ...item }) => item)
|
||||||
|
|
||||||
const nextId = () => `WF${Date.now()}${Math.floor(Math.random() * 1000)}`
|
|
||||||
|
|
||||||
function WizardStep6({
|
function WizardStep6({
|
||||||
listFormCode,
|
listFormCode,
|
||||||
workflow,
|
workflow,
|
||||||
|
|
@ -77,7 +78,7 @@ function WizardStep6({
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getUsers(0, 1000).then((response) => {
|
getUsers(0, 1000).then((response) => {
|
||||||
setUserList(
|
setUserList(
|
||||||
(response.data?.items ?? []).map((user: any) => ({
|
(response.data?.items ?? []).map((user: IdentityUserDto) => ({
|
||||||
value: user.userName,
|
value: user.userName,
|
||||||
label: `${user.userName} (${user.name} ${user.surname})`,
|
label: `${user.userName} (${user.name} ${user.surname})`,
|
||||||
})),
|
})),
|
||||||
|
|
@ -95,9 +96,10 @@ function WizardStep6({
|
||||||
const saveCriteria = (event: FormEvent<HTMLFormElement>) => {
|
const saveCriteria = (event: FormEvent<HTMLFormElement>) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
const normalized = normalizeCriteria({ ...criteriaForm, listFormCode })
|
const normalized = normalizeCriteria({ ...criteriaForm, listFormCode })
|
||||||
const id = normalized.id || nextId()
|
const id = normalized.id || uniqueCriteriaId(currentCriteria)
|
||||||
const nextItem = { ...normalized, id, nodeId: id } as WorkflowCriteriaDto
|
|
||||||
const exists = currentCriteria.some((item) => item.id === id)
|
const exists = currentCriteria.some((item) => item.id === id)
|
||||||
|
const title = uniqueCriteriaTitle(normalized.kind || '', currentCriteria, id, normalized.title)
|
||||||
|
const nextItem = { ...normalized, id, nodeId: id, title } as WorkflowCriteriaDto
|
||||||
updateCriteria(
|
updateCriteria(
|
||||||
exists
|
exists
|
||||||
? currentCriteria.map((item) => (item.id === id ? nextItem : item))
|
? currentCriteria.map((item) => (item.id === id ? nextItem : item))
|
||||||
|
|
@ -108,11 +110,12 @@ function WizardStep6({
|
||||||
}
|
}
|
||||||
|
|
||||||
const addCriteria = (kind: string) => {
|
const addCriteria = (kind: string) => {
|
||||||
const id = nextId()
|
const id = uniqueCriteriaId(currentCriteria)
|
||||||
const nextItem = {
|
const nextItem = {
|
||||||
...normalizeCriteria(emptyCriteria(kind, listFormCode)),
|
...normalizeCriteria(emptyCriteria(kind, listFormCode)),
|
||||||
id,
|
id,
|
||||||
nodeId: id,
|
nodeId: id,
|
||||||
|
title: uniqueCriteriaTitle(kind, currentCriteria),
|
||||||
positionX: 80 + (currentCriteria.length % 5) * 230,
|
positionX: 80 + (currentCriteria.length % 5) * 230,
|
||||||
positionY: 220 + Math.floor(currentCriteria.length / 5) * 140,
|
positionY: 220 + Math.floor(currentCriteria.length / 5) * 140,
|
||||||
} as WorkflowCriteriaDto
|
} as WorkflowCriteriaDto
|
||||||
|
|
@ -192,32 +195,57 @@ function WizardStep6({
|
||||||
}
|
}
|
||||||
|
|
||||||
const resetDemo = () => {
|
const resetDemo = () => {
|
||||||
const startId = nextId()
|
const startId = uniqueCriteriaId([])
|
||||||
const approvalId = nextId()
|
const approval1Id = uniqueCriteriaId([], [startId])
|
||||||
const endId = nextId()
|
const approval2Id = uniqueCriteriaId([], [startId, approval1Id])
|
||||||
|
const informId = uniqueCriteriaId([], [startId, approval1Id, approval2Id])
|
||||||
|
const endId = uniqueCriteriaId([], [startId, approval1Id, approval2Id, informId])
|
||||||
updateCriteria([
|
updateCriteria([
|
||||||
{
|
{
|
||||||
...normalizeCriteria(emptyCriteria('Start', listFormCode)),
|
...normalizeCriteria(emptyCriteria('Start', listFormCode)),
|
||||||
id: startId,
|
id: startId,
|
||||||
nodeId: startId,
|
nodeId: startId,
|
||||||
nextOnStart: approvalId,
|
title: 'İş Akışı Başlat1',
|
||||||
positionX: 72,
|
nextOnStart: approval1Id,
|
||||||
positionY: 160,
|
positionX: 34,
|
||||||
|
positionY: 28,
|
||||||
} as WorkflowCriteriaDto,
|
} as WorkflowCriteriaDto,
|
||||||
{
|
{
|
||||||
...normalizeCriteria(emptyCriteria('Approval', listFormCode)),
|
...normalizeCriteria(emptyCriteria('Approval', listFormCode)),
|
||||||
id: approvalId,
|
id: approval1Id,
|
||||||
nodeId: approvalId,
|
nodeId: approval1Id,
|
||||||
nextOnApprove: endId,
|
title: 'Onay1',
|
||||||
positionX: 360,
|
nextOnApprove: approval2Id,
|
||||||
positionY: 160,
|
nextOnReject: informId,
|
||||||
|
positionX: 323,
|
||||||
|
positionY: 28,
|
||||||
|
} as WorkflowCriteriaDto,
|
||||||
|
{
|
||||||
|
...normalizeCriteria(emptyCriteria('Approval', listFormCode)),
|
||||||
|
id: approval2Id,
|
||||||
|
nodeId: approval2Id,
|
||||||
|
title: 'Onay2',
|
||||||
|
nextOnApprove: informId,
|
||||||
|
nextOnReject: informId,
|
||||||
|
positionX: 586,
|
||||||
|
positionY: 28,
|
||||||
|
} as WorkflowCriteriaDto,
|
||||||
|
{
|
||||||
|
...normalizeCriteria(emptyCriteria('Inform', listFormCode)),
|
||||||
|
id: informId,
|
||||||
|
nodeId: informId,
|
||||||
|
title: 'Bilgilendirme1',
|
||||||
|
nextOnStart: endId,
|
||||||
|
positionX: 458,
|
||||||
|
positionY: 336,
|
||||||
} as WorkflowCriteriaDto,
|
} as WorkflowCriteriaDto,
|
||||||
{
|
{
|
||||||
...normalizeCriteria(emptyCriteria('End', listFormCode)),
|
...normalizeCriteria(emptyCriteria('End', listFormCode)),
|
||||||
id: endId,
|
id: endId,
|
||||||
nodeId: endId,
|
nodeId: endId,
|
||||||
positionX: 650,
|
title: 'İş Akışı Bitir1',
|
||||||
positionY: 160,
|
positionX: 792,
|
||||||
|
positionY: 336,
|
||||||
} as WorkflowCriteriaDto,
|
} as WorkflowCriteriaDto,
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
@ -234,7 +262,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-4 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-5 gap-4">
|
||||||
{[
|
{[
|
||||||
[
|
[
|
||||||
'approvalUserFieldName',
|
'approvalUserFieldName',
|
||||||
|
|
@ -277,6 +305,26 @@ 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>
|
||||||
|
|
|
||||||
|
|
@ -226,6 +226,13 @@ 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 ||
|
||||||
|
|
@ -298,91 +305,157 @@ 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={translate('::ListForms.Wizard.Step4.SelectedColumns')}
|
title={
|
||||||
badge={selectedColumns.size}
|
translate('::ListForms.Wizard.Step4.ColumnsAndFormLayout') || 'Columns & Form Layout'
|
||||||
|
}
|
||||||
|
badge={`${selectedColumns.size} ${translate('::App.Listform.ListformField.Column')} / ${editingFormFields.length} ${translate('::ListForms.Wizard.Step4.EditingForm') || 'Popup Form'}`}
|
||||||
>
|
>
|
||||||
<div className="flex flex-wrap gap-1.5">
|
<div className="mb-3 grid grid-cols-3 gap-2">
|
||||||
{[...selectedColumns].map((col) => {
|
{[
|
||||||
const meta = selectCommandColumns.find((c) => c.columnName === col)
|
{
|
||||||
return (
|
label: translate('::ListForms.Wizard.Step4.SelectedColumns'),
|
||||||
<span
|
value: selectedColumns.size,
|
||||||
key={col}
|
className: 'text-indigo-600 dark:text-indigo-400',
|
||||||
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}
|
label: translate('::ListForms.Wizard.Step4.EditingForm') || 'Popup Form',
|
||||||
{meta?.dataType && (
|
value: editingFormFields.length,
|
||||||
<span className="text-[10px] text-indigo-400 opacity-70">{meta.dataType}</span>
|
className: 'text-emerald-600 dark:text-emerald-400',
|
||||||
)}
|
},
|
||||||
</span>
|
{
|
||||||
)
|
label: translate('::ListForms.Wizard.Step4.FormGroups'),
|
||||||
})}
|
value: groups.length,
|
||||||
</div>
|
className: 'text-gray-700 dark:text-gray-200',
|
||||||
</Section>
|
},
|
||||||
|
].map((item) => (
|
||||||
<Section title={translate('::ListForms.Wizard.Step4.FormGroups')} badge={groups.length}>
|
<div
|
||||||
<div className="flex flex-col gap-3">
|
key={item.label}
|
||||||
{groups.map((g) => (
|
className="rounded-lg border border-gray-100 dark:border-gray-800 bg-gray-50 dark:bg-gray-800 px-3 py-2"
|
||||||
<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">
|
<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 ? (
|
{g.items.length === 0 ? (
|
||||||
<span className="text-xs text-gray-300 italic">
|
<span className="block px-3 py-2 text-xs text-gray-300 italic">
|
||||||
{translate('::ListForms.Wizard.Step4.NoFields') || 'Alan yok'}
|
{translate('::ListForms.Wizard.Step4.NoFields') || 'Alan yok'}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
g.items.map((item) => (
|
g.items.map((item) => {
|
||||||
<div
|
const meta = selectCommandColumns.find((c) => c.columnName === item.dataField)
|
||||||
key={item.id}
|
const isKeyField = item.dataField === values.keyFieldName
|
||||||
className="flex items-center gap-2 py-1 border-b border-gray-100 dark:border-gray-800 last:border-0"
|
const isPopupField = item.includeInEditingForm && !isKeyField
|
||||||
>
|
return (
|
||||||
<span className="text-xs font-medium text-indigo-600 dark:text-indigo-400 w-36 shrink-0 truncate">
|
<div
|
||||||
{item.dataField}
|
key={item.id}
|
||||||
</span>
|
className="grid grid-cols-[minmax(150px,1fr)_auto] items-center gap-3 px-3 py-1.5"
|
||||||
<span className="text-[10px] text-gray-400 bg-gray-100 dark:bg-gray-800 px-1.5 py-0.5 rounded">
|
>
|
||||||
{item.editorType}
|
<div className="flex min-w-0 items-center gap-2">
|
||||||
</span>
|
<span className="text-xs font-medium text-indigo-600 dark:text-indigo-400 truncate">
|
||||||
<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">
|
{item.dataField}
|
||||||
col-span-{item.colSpan}
|
</span>
|
||||||
{item.isRequired && (
|
{meta?.dataType && (
|
||||||
<span className="ml-1 text-red-400 font-semibold">*</span>
|
<span className="text-[10px] text-gray-400 truncate">
|
||||||
)}
|
{meta.dataType}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
)}
|
||||||
))
|
</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>
|
||||||
</Section>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
{ungroupedSelectedColumns.length > 0 && (
|
||||||
|
<div className="rounded-lg border border-amber-100 dark:border-amber-900/40 bg-amber-50/50 dark:bg-amber-900/10 px-3 py-2">
|
||||||
|
<div className="mb-1.5 text-xs font-semibold text-amber-700 dark:text-amber-300">
|
||||||
|
{translate('::ListForms.Wizard.Step4.UngroupedColumns') || 'Ungrouped columns'}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-1.5">
|
||||||
|
{ungroupedSelectedColumns.map((col) => {
|
||||||
|
const meta = selectCommandColumns.find((c) => c.columnName === col)
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
key={col}
|
||||||
|
className="inline-flex items-center gap-1 rounded-full border border-amber-200 dark:border-amber-800 bg-white dark:bg-gray-900 px-2 py-0.5 text-xs text-amber-700 dark:text-amber-300"
|
||||||
|
>
|
||||||
|
{col}
|
||||||
|
{meta?.dataType && (
|
||||||
|
<span className="text-[10px] text-amber-500 opacity-70">
|
||||||
|
{meta.dataType}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
|
|
@ -460,6 +533,7 @@ 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">
|
||||||
|
|
@ -471,12 +545,18 @@ 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>
|
||||||
<div className="mt-1 text-[11px] text-gray-500 dark:text-gray-400">
|
{criteria.kind === 'Compare' && (
|
||||||
{criteria.compareColumn} {criteria.compareOperator} {criteria.compareValue}
|
<div className="mt-1 text-[11px] text-gray-500 dark:text-gray-400">
|
||||||
</div>
|
{criteria.compareColumn} {criteria.compareOperator}{' '}
|
||||||
<div className="text-[11px] text-gray-500 dark:text-gray-400">
|
{criteria.compareValue}
|
||||||
Approver: {criteria.approver}
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
{(criteria.kind === 'Approval' || criteria.kind === 'Inform') &&
|
||||||
|
criteria.approver && (
|
||||||
|
<div className="text-[11px] text-gray-500 dark:text-gray-400">
|
||||||
|
Approver: {criteria.approver}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -489,10 +569,14 @@ 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-3 gap-2">
|
<div className="grid grid-cols-7 gap-2">
|
||||||
{[
|
{[
|
||||||
{ label: translate('::ListForms.Wizard.Step4.StatGroup'), value: groups.length },
|
{ label: translate('::ListForms.Wizard.Step4.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,
|
||||||
|
|
|
||||||
|
|
@ -257,17 +257,19 @@ export function WorkflowCriteria({
|
||||||
onChange={(event) => setField('title', event.target.value)}
|
onChange={(event) => setField('title', event.target.value)}
|
||||||
/>
|
/>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
<FormItem
|
{(formValues.kind === 'Approval' || formValues.kind === 'Inform') && (
|
||||||
label={translate('::App.Listform.ListformField.Approver')}
|
<FormItem
|
||||||
asterisk={formValues.kind === 'Approval' || formValues.kind === 'Inform'}
|
label={translate('::App.Listform.ListformField.Approver')}
|
||||||
>
|
asterisk
|
||||||
<SelectField
|
>
|
||||||
required={formValues.kind === 'Approval' || formValues.kind === 'Inform'}
|
<SelectField
|
||||||
options={userList}
|
required
|
||||||
value={formValues.approver}
|
options={userList}
|
||||||
onChange={(value) => setField('approver', value)}
|
value={formValues.approver}
|
||||||
/>
|
onChange={(value) => setField('approver', value)}
|
||||||
</FormItem>
|
/>
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
|
||||||
{(formValues.kind === 'Start' || formValues.kind === 'Inform') && (
|
{(formValues.kind === 'Start' || formValues.kind === 'Inform') && (
|
||||||
<FormItem
|
<FormItem
|
||||||
|
|
|
||||||
|
|
@ -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,6 +97,7 @@ 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)
|
||||||
|
|
@ -111,15 +112,26 @@ 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) => {
|
||||||
const { data } = await getUserDetail(userId || '')
|
if (!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))
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
|
@ -131,8 +143,10 @@ 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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -246,34 +260,49 @@ 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 })
|
||||||
let keepCurrentAvatar = false
|
if (values.id === auth.user.id) {
|
||||||
|
|
||||||
const avatar = await getCroppedAvatar()
|
|
||||||
const resp = await updateProfile({
|
|
||||||
name: values.name ?? '',
|
|
||||||
surname: values.surname ?? '',
|
|
||||||
avatar: avatar ? new File([avatar], 'avatar') : undefined,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (resp.status === 200) {
|
|
||||||
const avatarUrl =
|
|
||||||
AVATAR_URL(auth.user.id, auth.tenant?.tenantId) + `?${dayjs().unix()}`
|
|
||||||
|
|
||||||
setUser({
|
setUser({
|
||||||
...auth.user,
|
...auth.user,
|
||||||
name: `${resp.data.name} ${resp.data.surname}`.trim(),
|
name: `${values.name ?? ''} ${values.surname ?? ''}`.trim(),
|
||||||
avatar: avatarUrl,
|
|
||||||
})
|
})
|
||||||
setImage(avatarUrl)
|
}
|
||||||
keepCurrentAvatar = true
|
let keepCurrentAvatar = false
|
||||||
} else {
|
|
||||||
toast.push(<Notification title={resp?.error?.message} type="danger" />, {
|
if (hasAvatarChange) {
|
||||||
placement: 'top-end',
|
const avatar = await getCroppedAvatar()
|
||||||
|
const resp = await putUserAvatar({
|
||||||
|
userId: values.id!,
|
||||||
|
avatar: avatar ? new File([avatar], 'avatar') : undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (resp.status === 200) {
|
||||||
|
const avatarUrl =
|
||||||
|
AVATAR_URL(values.id, values.tenantId) + `?${dayjs().unix()}`
|
||||||
|
|
||||||
|
if (values.id === auth.user.id) {
|
||||||
|
setUser({
|
||||||
|
...auth.user,
|
||||||
|
name: `${values.name ?? ''} ${values.surname ?? ''}`.trim(),
|
||||||
|
avatar: avatarUrl,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
setImage(avatarUrl)
|
||||||
|
setHasAvatarChange(false)
|
||||||
|
keepCurrentAvatar = true
|
||||||
|
} else {
|
||||||
|
const errorMessage =
|
||||||
|
(resp as { error?: { message?: string } })?.error?.message || 'Hata'
|
||||||
|
|
||||||
|
toast.push(<Notification title={errorMessage} type="danger" />, {
|
||||||
|
placement: 'top-end',
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
toast.push(
|
toast.push(
|
||||||
|
|
@ -542,7 +571,10 @@ function UserDetails() {
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
icon={<FaTrashAlt />}
|
icon={<FaTrashAlt />}
|
||||||
onClick={() => setImage(undefined)}
|
onClick={() => {
|
||||||
|
setImage(undefined)
|
||||||
|
setHasAvatarChange(true)
|
||||||
|
}}
|
||||||
></Button>
|
></Button>
|
||||||
</div>
|
</div>
|
||||||
{image && (
|
{image && (
|
||||||
|
|
@ -574,6 +606,7 @@ 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)
|
||||||
|
|
@ -685,6 +718,7 @@ 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)
|
||||||
|
|
@ -866,6 +900,7 @@ 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)
|
||||||
|
|
@ -1222,6 +1257,7 @@ 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)
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
import { createPortal } from 'react-dom'
|
import { createPortal } from 'react-dom'
|
||||||
import { Button, Dialog, Notification, toast, Checkbox } from '@/components/ui'
|
import { Button, Dialog, Notification, toast, Checkbox } from '@/components/ui'
|
||||||
import { useDialogContext } from '@/components/ui/Dialog/Dialog'
|
|
||||||
import {
|
import {
|
||||||
FaPlus,
|
FaPlus,
|
||||||
FaTrash,
|
FaTrash,
|
||||||
|
|
@ -42,6 +41,7 @@ type SqlDataType =
|
||||||
| 'decimal'
|
| 'decimal'
|
||||||
| 'float'
|
| 'float'
|
||||||
| 'bit'
|
| 'bit'
|
||||||
|
| 'datetime'
|
||||||
| 'datetime2'
|
| 'datetime2'
|
||||||
| 'date'
|
| 'date'
|
||||||
| 'uniqueidentifier'
|
| 'uniqueidentifier'
|
||||||
|
|
@ -68,7 +68,7 @@ interface TableDesignerDialogProps {
|
||||||
isOpen: boolean
|
isOpen: boolean
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
dataSource: string | null
|
dataSource: string | null
|
||||||
onDeployed?: () => void
|
onDeployed?: (table: { schemaName: string; tableName: string }) => void | Promise<void>
|
||||||
initialTableData?: { schemaName: string; tableName: string } | null
|
initialTableData?: { schemaName: string; tableName: string } | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -98,6 +98,7 @@ 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)' },
|
||||||
|
|
@ -226,6 +227,45 @@ 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 ──────────────────────────────────────────────────────────
|
||||||
|
|
@ -405,6 +445,8 @@ 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') {
|
||||||
|
|
@ -476,6 +518,7 @@ 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: '' }
|
||||||
}
|
}
|
||||||
|
|
@ -791,9 +834,8 @@ const STEPS = ['Sütun Tasarımı', 'Entity Ayarları', 'Index / Key', 'İlişki
|
||||||
type Step = 0 | 1 | 2 | 3 | 4
|
type Step = 0 | 1 | 2 | 3 | 4
|
||||||
|
|
||||||
function StepContentWrapper({ children }: { children: React.ReactNode }) {
|
function StepContentWrapper({ children }: { children: React.ReactNode }) {
|
||||||
const { isMaximized } = useDialogContext()
|
|
||||||
return (
|
return (
|
||||||
<div className={isMaximized ? 'flex-1 min-h-0 overflow-auto' : 'min-h-[420px]'}>
|
<div className="min-h-[420px] flex flex-col">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
@ -1224,6 +1266,18 @@ const SqlTableDesignerDialog = ({
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const addWorkflowColumns = () => {
|
||||||
|
const existingNames = new Set(columns.map((c) => c.columnName.trim().toLowerCase()))
|
||||||
|
const toAdd = WORKFLOW_COLUMNS.filter((c) => !existingNames.has(c.columnName.toLowerCase()))
|
||||||
|
|
||||||
|
if (toAdd.length > 0) {
|
||||||
|
setColumns((prev) => {
|
||||||
|
const nonEmpty = prev.filter((c) => c.columnName.trim() !== '')
|
||||||
|
return [...nonEmpty, ...toAdd.map((c) => ({ ...c })), createEmptyColumn()]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const importColumnsFromRememberedCreateTable = async () => {
|
const importColumnsFromRememberedCreateTable = async () => {
|
||||||
let script = ''
|
let script = ''
|
||||||
|
|
||||||
|
|
@ -1591,7 +1645,10 @@ 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
|
||||||
}
|
}
|
||||||
onDeployed?.()
|
await onDeployed?.({
|
||||||
|
schemaName: initialTableData?.schemaName || 'dbo',
|
||||||
|
tableName: deployedTable,
|
||||||
|
})
|
||||||
handleClose()
|
handleClose()
|
||||||
} else {
|
} else {
|
||||||
toast.push(
|
toast.push(
|
||||||
|
|
@ -1690,6 +1747,9 @@ 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"
|
||||||
|
|
@ -1748,7 +1808,7 @@ const SqlTableDesignerDialog = ({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex flex-col gap-1 max-h-80 overflow-y-auto pr-1">
|
<div className="flex flex-col gap-1 pr-1">
|
||||||
{columns.map((col, idx) => {
|
{columns.map((col, idx) => {
|
||||||
const isDuplicate =
|
const isDuplicate =
|
||||||
col.columnName.trim() !== '' &&
|
col.columnName.trim() !== '' &&
|
||||||
|
|
@ -1943,7 +2003,14 @@ const SqlTableDesignerDialog = ({
|
||||||
initialParentCode={selectedMenuCode}
|
initialParentCode={selectedMenuCode}
|
||||||
initialOrder={999}
|
initialOrder={999}
|
||||||
rawItems={rawMenuItems}
|
rawItems={rawMenuItems}
|
||||||
onSaved={() => reloadMenus()}
|
onSaved={(menu) =>
|
||||||
|
reloadMenus((items) => {
|
||||||
|
const savedMenu = items.find((item) => item.code === menu.code)
|
||||||
|
if (savedMenu?.code && savedMenu.shortName) {
|
||||||
|
onMenuCodeSelect(savedMenu.code)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -2671,8 +2738,8 @@ const SqlTableDesignerDialog = ({
|
||||||
// ── Render ─────────────────────────────────────────────────────────────────
|
// ── Render ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog isOpen={isOpen} onClose={handleClose} onRequestClose={handleClose} width={900}>
|
<Dialog isOpen={isOpen} onClose={handleClose} onRequestClose={handleClose} width={1100}>
|
||||||
<Dialog.Body className="flex flex-col gap-2">
|
<Dialog.Header 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">
|
||||||
<FaTable className="text-2xl text-blue-500" />
|
<FaTable className="text-2xl text-blue-500" />
|
||||||
|
|
@ -2692,7 +2759,9 @@ const SqlTableDesignerDialog = ({
|
||||||
|
|
||||||
{/* Steps */}
|
{/* Steps */}
|
||||||
<div className="flex-shrink-0">{renderStepIndicator()}</div>
|
<div className="flex-shrink-0">{renderStepIndicator()}</div>
|
||||||
|
</Dialog.Header>
|
||||||
|
|
||||||
|
<Dialog.Body className="flex flex-col gap-2">
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<StepContentWrapper>
|
<StepContentWrapper>
|
||||||
{step === 0 && renderColumnDesigner()}
|
{step === 0 && renderColumnDesigner()}
|
||||||
|
|
|
||||||
|
|
@ -140,7 +140,9 @@ const getValueByField = (data: Record<string, any> = {}, field?: string) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const shouldRunEditorScriptOnContentReady = (script?: string) =>
|
const shouldRunEditorScriptOnContentReady = (script?: string) =>
|
||||||
Boolean(script && (script.includes('setEditorReadOnly') || script.includes('runtimeSetEditorReadOnly')))
|
Boolean(
|
||||||
|
script && (script.includes('setEditorReadOnly') || script.includes('runtimeSetEditorReadOnly')),
|
||||||
|
)
|
||||||
|
|
||||||
const FormDevExpress = (props: {
|
const FormDevExpress = (props: {
|
||||||
listFormCode: string
|
listFormCode: string
|
||||||
|
|
@ -158,9 +160,15 @@ const FormDevExpress = (props: {
|
||||||
const formItemsRef = useRef(formItems)
|
const formItemsRef = useRef(formItems)
|
||||||
const formInstanceRef = useRef<any>()
|
const formInstanceRef = useRef<any>()
|
||||||
const lastContentReadyScriptKeyRef = useRef<string>()
|
const lastContentReadyScriptKeyRef = useRef<string>()
|
||||||
|
const didAutoFocusRef = useRef(false)
|
||||||
const [runtimeReadOnlyFields, setRuntimeReadOnlyFields] = useState<Record<string, boolean>>({})
|
const [runtimeReadOnlyFields, setRuntimeReadOnlyFields] = useState<Record<string, boolean>>({})
|
||||||
const runtimeReadOnlyFieldsRef = useRef<Record<string, boolean>>({})
|
const runtimeReadOnlyFieldsRef = useRef<Record<string, boolean>>({})
|
||||||
|
|
||||||
|
const isTouchLikeDevice = () =>
|
||||||
|
typeof window !== 'undefined' &&
|
||||||
|
(window.matchMedia?.('(pointer: coarse)').matches ||
|
||||||
|
window.matchMedia?.('(hover: none)').matches)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
formDataRef.current = formData
|
formDataRef.current = formData
|
||||||
}, [formData])
|
}, [formData])
|
||||||
|
|
@ -173,6 +181,10 @@ const FormDevExpress = (props: {
|
||||||
runtimeReadOnlyFieldsRef.current = runtimeReadOnlyFields
|
runtimeReadOnlyFieldsRef.current = runtimeReadOnlyFields
|
||||||
}, [runtimeReadOnlyFields])
|
}, [runtimeReadOnlyFields])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
didAutoFocusRef.current = false
|
||||||
|
}, [listFormCode, mode])
|
||||||
|
|
||||||
const setRuntimeEditorReadOnly = (field: string, readOnly: boolean) => {
|
const setRuntimeEditorReadOnly = (field: string, readOnly: boolean) => {
|
||||||
const resolvedField = findFormFieldKey(formItemsRef.current, field)
|
const resolvedField = findFormFieldKey(formItemsRef.current, field)
|
||||||
const key = String(resolvedField || field || '').toLowerCase()
|
const key = String(resolvedField || field || '').toLowerCase()
|
||||||
|
|
@ -202,11 +214,7 @@ const FormDevExpress = (props: {
|
||||||
setTimeout(() => setFormEditorReadOnly(formInstanceRef.current ?? form, field, readOnly), 0)
|
setTimeout(() => setFormEditorReadOnly(formInstanceRef.current ?? form, field, readOnly), 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
const runEditorScript = (
|
const runEditorScript = (formItem: SimpleItemWithColData, eventValue: any, component?: any) => {
|
||||||
formItem: SimpleItemWithColData,
|
|
||||||
eventValue: any,
|
|
||||||
component?: any,
|
|
||||||
) => {
|
|
||||||
if (!formItem?.editorScript) {
|
if (!formItem?.editorScript) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -250,7 +258,9 @@ const FormDevExpress = (props: {
|
||||||
const prevOnValueChanged = formItem.editorOptions?.onValueChanged
|
const prevOnValueChanged = formItem.editorOptions?.onValueChanged
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...(index !== undefined && mode !== 'view' ? { autoFocus: index === 1 } : {}),
|
...(index !== undefined && mode !== 'view' && !isTouchLikeDevice()
|
||||||
|
? { autoFocus: index === 1 }
|
||||||
|
: {}),
|
||||||
...(formItem.editorType === 'dxDateBox'
|
...(formItem.editorType === 'dxDateBox'
|
||||||
? {
|
? {
|
||||||
useMaskBehavior: true,
|
useMaskBehavior: true,
|
||||||
|
|
@ -466,7 +476,6 @@ const FormDevExpress = (props: {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
updateCascadeDisabledStates()
|
updateCascadeDisabledStates()
|
||||||
}, 0)
|
}, 0)
|
||||||
|
|
||||||
}}
|
}}
|
||||||
onContentReady={(e) => {
|
onContentReady={(e) => {
|
||||||
formInstanceRef.current = e.component
|
formInstanceRef.current = e.component
|
||||||
|
|
@ -478,7 +487,8 @@ const FormDevExpress = (props: {
|
||||||
const groupItems = e.component.option('items') as any[]
|
const groupItems = e.component.option('items') as any[]
|
||||||
const firstItem = groupItems?.[0]?.items?.[0]
|
const firstItem = groupItems?.[0]?.items?.[0]
|
||||||
|
|
||||||
if (firstItem?.dataField) {
|
if (!didAutoFocusRef.current && firstItem?.dataField && !isTouchLikeDevice()) {
|
||||||
|
didAutoFocusRef.current = true
|
||||||
const editor = e.component.getEditor(firstItem.dataField)
|
const editor = e.component.getEditor(firstItem.dataField)
|
||||||
mode !== 'view' && editor?.focus()
|
mode !== 'view' && editor?.focus()
|
||||||
}
|
}
|
||||||
|
|
@ -492,98 +502,100 @@ const FormDevExpress = (props: {
|
||||||
colSpan={formGroupItem.colSpan}
|
colSpan={formGroupItem.colSpan}
|
||||||
caption={formGroupItem.caption}
|
caption={formGroupItem.caption}
|
||||||
>
|
>
|
||||||
{(formGroupItem.items as SimpleItemWithColData[])?.filter((formItem) => {
|
{(formGroupItem.items as SimpleItemWithColData[])
|
||||||
if (mode === 'edit') return formItem.allowEditing !== false
|
?.filter((formItem) => {
|
||||||
if (mode === 'new') return formItem.allowAdding !== false
|
if (mode === 'edit') return formItem.allowEditing !== false
|
||||||
return true
|
if (mode === 'new') return formItem.allowAdding !== false
|
||||||
}).map((formItem, i) => {
|
return true
|
||||||
return formItem.editorType2 === PlatformEditorTypes.dxTagBox ? (
|
})
|
||||||
<SimpleItemDx
|
.map((formItem, i) => {
|
||||||
cssClass="font-semibold"
|
return formItem.editorType2 === PlatformEditorTypes.dxTagBox ? (
|
||||||
key={getFormItemKey(formItem, i)}
|
<SimpleItemDx
|
||||||
{...formItem}
|
cssClass="font-semibold"
|
||||||
render={() => (
|
key={getFormItemKey(formItem, i)}
|
||||||
<TagBoxEditorComponent
|
{...formItem}
|
||||||
value={formData[formItem.dataField!] || []}
|
render={() => (
|
||||||
setDefaultValue={false}
|
<TagBoxEditorComponent
|
||||||
values={formData}
|
value={formData[formItem.dataField!] || []}
|
||||||
options={formItem.tagBoxOptions}
|
setDefaultValue={false}
|
||||||
col={formItem.colData}
|
values={formData}
|
||||||
onValueChanged={(e: any) => {
|
options={formItem.tagBoxOptions}
|
||||||
const newData = { ...formDataRef.current, [formItem.dataField!]: e }
|
col={formItem.colData}
|
||||||
formDataRef.current = newData
|
onValueChanged={(e: any) => {
|
||||||
setFormData(newData)
|
const newData = { ...formDataRef.current, [formItem.dataField!]: e }
|
||||||
runEditorScript(formItem, e, formInstanceRef.current)
|
formDataRef.current = newData
|
||||||
}}
|
setFormData(newData)
|
||||||
editorOptions={getEditorOptions(formItem)}
|
runEditorScript(formItem, e, formInstanceRef.current)
|
||||||
></TagBoxEditorComponent>
|
}}
|
||||||
)}
|
editorOptions={getEditorOptions(formItem)}
|
||||||
label={{
|
></TagBoxEditorComponent>
|
||||||
text: translate('::' + formItem.colData?.captionName),
|
)}
|
||||||
className: 'font-semibold',
|
label={{
|
||||||
}}
|
text: translate('::' + formItem.colData?.captionName),
|
||||||
></SimpleItemDx>
|
className: 'font-semibold',
|
||||||
) : formItem.editorType2 === PlatformEditorTypes.dxGridBox ? (
|
}}
|
||||||
<SimpleItemDx
|
></SimpleItemDx>
|
||||||
cssClass="font-semibold"
|
) : formItem.editorType2 === PlatformEditorTypes.dxGridBox ? (
|
||||||
key={getFormItemKey(formItem, i)}
|
<SimpleItemDx
|
||||||
{...formItem}
|
cssClass="font-semibold"
|
||||||
render={() => (
|
key={getFormItemKey(formItem, i)}
|
||||||
<GridBoxEditorComponent
|
{...formItem}
|
||||||
value={formData[formItem.dataField!] || []}
|
render={() => (
|
||||||
values={formData}
|
<GridBoxEditorComponent
|
||||||
options={formItem.gridBoxOptions}
|
value={formData[formItem.dataField!] || []}
|
||||||
col={formItem.colData}
|
values={formData}
|
||||||
onValueChanged={(e: any) => {
|
options={formItem.gridBoxOptions}
|
||||||
const newData = { ...formDataRef.current, [formItem.dataField!]: e }
|
col={formItem.colData}
|
||||||
formDataRef.current = newData
|
onValueChanged={(e: any) => {
|
||||||
setFormData(newData)
|
const newData = { ...formDataRef.current, [formItem.dataField!]: e }
|
||||||
runEditorScript(formItem, e, formInstanceRef.current)
|
formDataRef.current = newData
|
||||||
}}
|
setFormData(newData)
|
||||||
editorOptions={getEditorOptions(formItem)}
|
runEditorScript(formItem, e, formInstanceRef.current)
|
||||||
></GridBoxEditorComponent>
|
}}
|
||||||
)}
|
editorOptions={getEditorOptions(formItem)}
|
||||||
label={{
|
></GridBoxEditorComponent>
|
||||||
text: translate('::' + formItem.colData?.captionName),
|
)}
|
||||||
className: 'font-semibold',
|
label={{
|
||||||
}}
|
text: translate('::' + formItem.colData?.captionName),
|
||||||
></SimpleItemDx>
|
className: 'font-semibold',
|
||||||
) : formItem.editorType2 === PlatformEditorTypes.dxImageUpload ? (
|
}}
|
||||||
<SimpleItemDx
|
></SimpleItemDx>
|
||||||
cssClass="font-semibold"
|
) : formItem.editorType2 === PlatformEditorTypes.dxImageUpload ? (
|
||||||
key={getFormItemKey(formItem, i)}
|
<SimpleItemDx
|
||||||
dataField={formItem.dataField}
|
cssClass="font-semibold"
|
||||||
name={formItem.name}
|
key={getFormItemKey(formItem, i)}
|
||||||
colSpan={formItem.colSpan}
|
dataField={formItem.dataField}
|
||||||
isRequired={formItem.isRequired}
|
name={formItem.name}
|
||||||
render={() => (
|
colSpan={formItem.colSpan}
|
||||||
<ImageUploadEditorComponent
|
isRequired={formItem.isRequired}
|
||||||
value={formData[formItem.dataField!]}
|
render={() => (
|
||||||
options={formItem.imageUploadOptions}
|
<ImageUploadEditorComponent
|
||||||
onValueChanged={(val: any) => {
|
value={formData[formItem.dataField!]}
|
||||||
const newData = { ...formDataRef.current, [formItem.dataField!]: val }
|
options={formItem.imageUploadOptions}
|
||||||
formDataRef.current = newData
|
onValueChanged={(val: any) => {
|
||||||
setFormData(newData)
|
const newData = { ...formDataRef.current, [formItem.dataField!]: val }
|
||||||
runEditorScript(formItem, val, formInstanceRef.current)
|
formDataRef.current = newData
|
||||||
}}
|
setFormData(newData)
|
||||||
editorOptions={getEditorOptions(formItem)}
|
runEditorScript(formItem, val, formInstanceRef.current)
|
||||||
/>
|
}}
|
||||||
)}
|
editorOptions={getEditorOptions(formItem)}
|
||||||
label={{
|
/>
|
||||||
text: translate('::' + formItem.colData?.captionName),
|
)}
|
||||||
className: 'font-semibold',
|
label={{
|
||||||
}}
|
text: translate('::' + formItem.colData?.captionName),
|
||||||
></SimpleItemDx>
|
className: 'font-semibold',
|
||||||
) : (
|
}}
|
||||||
<SimpleItemDx
|
></SimpleItemDx>
|
||||||
cssClass="font-semibold"
|
) : (
|
||||||
key={getFormItemKey(formItem, i)}
|
<SimpleItemDx
|
||||||
{...formItem}
|
cssClass="font-semibold"
|
||||||
editorOptions={getEditorOptions(formItem, i)}
|
key={getFormItemKey(formItem, i)}
|
||||||
label={{ text: translate('::' + formItem.colData?.captionName) }}
|
{...formItem}
|
||||||
/>
|
editorOptions={getEditorOptions(formItem, i)}
|
||||||
)
|
label={{ text: translate('::' + formItem.colData?.captionName) }}
|
||||||
})}
|
/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
</GroupItemDx>
|
</GroupItemDx>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|
|
||||||
|
|
@ -17,16 +17,17 @@ 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
|
||||||
}
|
}
|
||||||
|
|
@ -35,7 +36,9 @@ export const NoteList: React.FC<NoteListProps> = ({
|
||||||
notes,
|
notes,
|
||||||
entityName,
|
entityName,
|
||||||
entityId,
|
entityId,
|
||||||
|
isVisible = true,
|
||||||
onAddNote,
|
onAddNote,
|
||||||
|
onRefreshNotes,
|
||||||
onDeleteNote,
|
onDeleteNote,
|
||||||
onDownloadFile,
|
onDownloadFile,
|
||||||
}) => {
|
}) => {
|
||||||
|
|
@ -70,6 +73,11 @@ export const NoteList: React.FC<NoteListProps> = ({
|
||||||
icon: <FaEnvelope className="text-green-500" />,
|
icon: <FaEnvelope className="text-green-500" />,
|
||||||
border: 'border-green-400',
|
border: 'border-green-400',
|
||||||
}
|
}
|
||||||
|
case 'workflow':
|
||||||
|
return {
|
||||||
|
icon: <FaHistory className="text-purple-500" />,
|
||||||
|
border: 'border-purple-400',
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return {
|
return {
|
||||||
icon: <FaStickyNote className="text-gray-400" />,
|
icon: <FaStickyNote className="text-gray-400" />,
|
||||||
|
|
@ -215,7 +223,18 @@ 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 || inputFormCode !== listFormCodeNormalized) return null
|
if (!inputFormCode) return null
|
||||||
|
const data = input?.data
|
||||||
|
const isMainListForm = inputFormCode === listFormCodeNormalized
|
||||||
|
|
||||||
|
if (!isMainListForm) {
|
||||||
|
if (!data || typeof data !== 'object') return null
|
||||||
|
const hit = findMatchingValueInData(data, entityIdNormalized)
|
||||||
|
if (!hit) return null
|
||||||
|
|
||||||
|
const nameValue = (data as any)?.Name ?? (data as any)?.name
|
||||||
|
return nameValue ? String(nameValue) : String(hit.value)
|
||||||
|
}
|
||||||
|
|
||||||
const keys = getKeysFromInput(input)
|
const keys = getKeysFromInput(input)
|
||||||
.map((k) => normalize(k))
|
.map((k) => normalize(k))
|
||||||
|
|
@ -227,7 +246,6 @@ export const NoteList: React.FC<NoteListProps> = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
// Some entities may use a different PK than the visible row key; allow strict match via input.data too.
|
// 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) {
|
||||||
|
|
@ -240,7 +258,6 @@ export const NoteList: React.FC<NoteListProps> = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
// insert: keys is null, match by scanning input.data for entity id/name/code/etc.
|
// 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
|
||||||
|
|
@ -277,17 +294,15 @@ export const NoteList: React.FC<NoteListProps> = ({
|
||||||
setAuditLoading(true)
|
setAuditLoading(true)
|
||||||
setAuditError(null)
|
setAuditError(null)
|
||||||
try {
|
try {
|
||||||
const response = await apiService.fetchData<PagedResultDto<AuditLogDto>>({
|
const response = await auditLogService.getList({
|
||||||
method: 'GET',
|
skipCount: 0,
|
||||||
url: '/api/app/audit-log',
|
maxResultCount: 200,
|
||||||
params: {
|
sorting: 'ExecutionTime DESC',
|
||||||
skipCount: 0,
|
listFormCode: entityName,
|
||||||
maxResultCount: 200,
|
entityId,
|
||||||
sorting: 'ExecutionTime DESC',
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const items = response.data?.items ?? []
|
const items = response?.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)
|
||||||
|
|
@ -305,13 +320,13 @@ export const NoteList: React.FC<NoteListProps> = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentTab !== 'audit') return
|
if (!isVisible) 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
|
||||||
}, [currentTab, listFormCodeNormalized, entityIdNormalized])
|
}, [isVisible, 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="?" />
|
||||||
|
|
@ -330,7 +345,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-4 border-0 dark:bg-gray-800">
|
<TabList className="mb-2 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}`} />
|
||||||
|
|
@ -341,21 +356,48 @@ export const NoteList: React.FC<NoteListProps> = ({
|
||||||
</TabNav>
|
</TabNav>
|
||||||
</TabList>
|
</TabList>
|
||||||
|
|
||||||
<TabContent value="notes">
|
<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">
|
||||||
<div className="flex items-center justify-end mb-2">
|
{currentTab === 'notes' ? (
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
size="xs"
|
||||||
|
icon={<FaPlus />}
|
||||||
|
type="button"
|
||||||
|
onClick={onAddNote}
|
||||||
|
disabled={!onAddNote}
|
||||||
|
className="flex items-center"
|
||||||
|
>
|
||||||
|
{translate('::ListForms.ListForm.AddNote')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
variant="default"
|
||||||
|
type="button"
|
||||||
|
icon={<FaSyncAlt />}
|
||||||
|
onClick={onRefreshNotes}
|
||||||
|
disabled={!onRefreshNotes}
|
||||||
|
className="flex items-center"
|
||||||
|
>
|
||||||
|
{translate('::ListForms.ListForm.Refresh')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<Button
|
<Button
|
||||||
|
size="xs"
|
||||||
variant="default"
|
variant="default"
|
||||||
size="sm"
|
|
||||||
icon={<FaPlus className="mr-1" />}
|
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onAddNote}
|
icon={<FaSyncAlt />}
|
||||||
disabled={!onAddNote}
|
onClick={loadAuditLogs}
|
||||||
|
disabled={auditLoading}
|
||||||
className="flex items-center"
|
className="flex items-center"
|
||||||
>
|
>
|
||||||
{translate('::ListForms.ListForm.AddNote')}
|
{translate('::ListForms.ListForm.Refresh')}
|
||||||
</Button>
|
</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" />
|
||||||
|
|
@ -398,7 +440,7 @@ export const NoteList: React.FC<NoteListProps> = ({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Sil butonu */}
|
{/* Sil butonu */}
|
||||||
{user?.id === note.creatorId && (
|
{user?.id === note.creatorId && note.type !== 'workflow' && (
|
||||||
<Button
|
<Button
|
||||||
variant="plain"
|
variant="plain"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|
@ -449,20 +491,6 @@ 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} />
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,6 @@ function NoteModalContent({
|
||||||
const types = [
|
const types = [
|
||||||
{ value: 'note', label: translate('::ListForms.ListForm.NoteModal.Type.Note') },
|
{ value: 'note', label: translate('::ListForms.ListForm.NoteModal.Type.Note') },
|
||||||
{ value: 'message', label: translate('::ListForms.ListForm.NoteModal.Type.Message') },
|
{ value: 'message', label: translate('::ListForms.ListForm.NoteModal.Type.Message') },
|
||||||
{ value: 'activity', label: translate('::ListForms.ListForm.NoteModal.Type.Activity') },
|
|
||||||
]
|
]
|
||||||
|
|
||||||
const handleSave = async (values: any) => {
|
const handleSave = async (values: any) => {
|
||||||
|
|
@ -92,7 +91,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-2 bg-purple-100 rounded-full">
|
<div className="p-1 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')}
|
||||||
|
|
@ -116,7 +115,7 @@ function NoteModalContent({
|
||||||
{types.map((t) => (
|
{types.map((t) => (
|
||||||
<label
|
<label
|
||||||
key={t.value}
|
key={t.value}
|
||||||
className="flex items-center gap-2 px-2 py-1 text-black rounded-md cursor-pointer transition-all duration-200 dark:bg-gray-800 dark:text-gray-300"
|
className="flex items-center gap-2 px-1 py-1 text-black rounded-md cursor-pointer transition-all duration-200 dark:bg-gray-800 dark:text-gray-300"
|
||||||
>
|
>
|
||||||
<Radio
|
<Radio
|
||||||
value={t.value}
|
value={t.value}
|
||||||
|
|
@ -139,8 +138,9 @@ 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-3 text-center hover:border-purple-400 transition-colors duration-200">
|
<div className="border-2 border-dashed border-gray-300 rounded-lg p-2 text-center hover:border-purple-400 transition-colors duration-200">
|
||||||
<Upload
|
<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-5 flex justify-between items-center pt-4 border-t border-gray-200">
|
<Dialog.Footer className="mt-2 flex justify-between items-center pt-4 border-t border-gray-200">
|
||||||
<Button variant="default" size="md" onClick={onClose} disabled={uploading}>
|
<Button variant="default" size="md" onClick={onClose} disabled={uploading}>
|
||||||
{translate('::Cancel')}
|
{translate('::Cancel')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ export const NotePanel: React.FC<NotePanelProps> = ({
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isVisible) fetchActivities()
|
if (isVisible) fetchActivities()
|
||||||
}, [isVisible])
|
}, [isVisible, entityName, entityId])
|
||||||
|
|
||||||
const handleDownloadFile = async (fileData: any) => {
|
const handleDownloadFile = async (fileData: any) => {
|
||||||
if (!fileData?.SavedFileName) return
|
if (!fileData?.SavedFileName) return
|
||||||
|
|
@ -62,8 +62,6 @@ 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
|
||||||
|
|
@ -134,7 +132,6 @@ 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>
|
||||||
|
|
@ -187,7 +184,9 @@ 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}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,13 @@ import { layoutTypes } from '../admin/listForm/edit/types'
|
||||||
import { useListFormCustomDataSource } from '../list/useListFormCustomDataSource'
|
import { useListFormCustomDataSource } from '../list/useListFormCustomDataSource'
|
||||||
import { useListFormColumns } from '../list/useListFormColumns'
|
import { useListFormColumns } from '../list/useListFormColumns'
|
||||||
|
|
||||||
|
const flattenFormItems = (items: any[] = []): SimpleItemWithColData[] =>
|
||||||
|
items.flatMap((item) => [
|
||||||
|
...(item?.dataField ? [item] : []),
|
||||||
|
...flattenFormItems(item?.items || []),
|
||||||
|
...(item?.tabs || []).flatMap((tab: any) => flattenFormItems(tab?.items || [])),
|
||||||
|
])
|
||||||
|
|
||||||
const useGridData = (props: {
|
const useGridData = (props: {
|
||||||
mode: RowMode
|
mode: RowMode
|
||||||
listFormCode: string
|
listFormCode: string
|
||||||
|
|
@ -41,6 +48,7 @@ const useGridData = (props: {
|
||||||
const [permissionResults, setPermissionResults] = useState<PermissionResults>()
|
const [permissionResults, setPermissionResults] = useState<PermissionResults>()
|
||||||
|
|
||||||
const refForm = useRef<FormRef>(null)
|
const refForm = useRef<FormRef>(null)
|
||||||
|
const previousFormDataRef = useRef<any>()
|
||||||
const [searchParams] = useSearchParams()
|
const [searchParams] = useSearchParams()
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { translate } = useLocalization()
|
const { translate } = useLocalization()
|
||||||
|
|
@ -306,18 +314,36 @@ const useGridData = (props: {
|
||||||
setGridReady(true)
|
setGridReady(true)
|
||||||
}, [gridDto])
|
}, [gridDto])
|
||||||
|
|
||||||
// formData değiştiğinde sadece lookup datasource'ları güncelle
|
// formData değiştiğinde sadece etkilenen cascading lookup datasource'ları güncelle
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!gridDto || !formItems.length) {
|
if (!gridDto || !formItems.length) {
|
||||||
|
previousFormDataRef.current = formData
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// View mode'da formData olsa da olmasa da cascading alanlar için dataSource oluşturulmalı
|
const previousFormData = previousFormDataRef.current
|
||||||
const updatedItems = formItems.map((groupItem) => ({
|
const changedFields = previousFormData
|
||||||
...groupItem,
|
? Object.keys({ ...(previousFormData || {}), ...(formData || {}) }).filter(
|
||||||
items: (groupItem.items as SimpleItemWithColData[])?.map((item) => {
|
(field) => !Object.is(previousFormData?.[field], formData?.[field]),
|
||||||
|
)
|
||||||
|
: []
|
||||||
|
|
||||||
|
const shouldRefreshLookup = (item: SimpleItemWithColData) => {
|
||||||
|
const cascadeParentFields = item.colData?.lookupDto?.cascadeParentFields
|
||||||
|
?.split(',')
|
||||||
|
.map((field: string) => field.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
|
||||||
|
return (
|
||||||
|
!previousFormData ||
|
||||||
|
cascadeParentFields?.some((field: string) => changedFields.includes(field))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateItems = (items: any[] = []) =>
|
||||||
|
items.map((item) => {
|
||||||
const colData = gridDto.columnFormats.find((x) => x.fieldName === item.dataField)
|
const colData = gridDto.columnFormats.find((x) => x.fieldName === item.dataField)
|
||||||
if (colData?.lookupDto?.dataSourceType) {
|
if (colData?.lookupDto?.dataSourceType && shouldRefreshLookup(item)) {
|
||||||
const currentDataSource = item.editorOptions?.dataSource
|
const currentDataSource = item.editorOptions?.dataSource
|
||||||
const keepCustomDataSource =
|
const keepCustomDataSource =
|
||||||
currentDataSource !== undefined && typeof currentDataSource?.load !== 'function'
|
currentDataSource !== undefined && typeof currentDataSource?.load !== 'function'
|
||||||
|
|
@ -330,16 +356,55 @@ const useGridData = (props: {
|
||||||
dataSource: keepCustomDataSource
|
dataSource: keepCustomDataSource
|
||||||
? currentDataSource
|
? currentDataSource
|
||||||
: getLookupDataSource(colData?.editorOptions, colData, formData || null),
|
: getLookupDataSource(colData?.editorOptions, colData, formData || null),
|
||||||
valueExpr: item.editorOptions?.valueExpr ?? colData?.lookupDto?.valueExpr?.toLowerCase(),
|
valueExpr:
|
||||||
|
item.editorOptions?.valueExpr ?? colData?.lookupDto?.valueExpr?.toLowerCase(),
|
||||||
displayExpr:
|
displayExpr:
|
||||||
item.editorOptions?.displayExpr ?? colData?.lookupDto?.displayExpr?.toLowerCase(),
|
item.editorOptions?.displayExpr ?? colData?.lookupDto?.displayExpr?.toLowerCase(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (item?.items?.length) {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
items: updateItems(item.items),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (item?.tabs?.length) {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
tabs: item.tabs.map((tab: any) => ({
|
||||||
|
...tab,
|
||||||
|
items: updateItems(tab.items),
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return item
|
return item
|
||||||
}),
|
})
|
||||||
|
|
||||||
|
const hasAffectedLookup =
|
||||||
|
!previousFormData ||
|
||||||
|
formItems
|
||||||
|
.flatMap((group) => flattenFormItems([group]))
|
||||||
|
.some((item) => item.colData?.lookupDto?.dataSourceType && shouldRefreshLookup(item))
|
||||||
|
|
||||||
|
if (!hasAffectedLookup) {
|
||||||
|
previousFormDataRef.current = formData
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedItems = formItems.map((groupItem) => ({
|
||||||
|
...groupItem,
|
||||||
|
items: updateItems(groupItem.items as any[]),
|
||||||
|
tabs: (groupItem as any).tabs?.map((tab: any) => ({
|
||||||
|
...tab,
|
||||||
|
items: updateItems(tab.items),
|
||||||
|
})),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
previousFormDataRef.current = formData
|
||||||
setFormItems(updatedItems)
|
setFormItems(updatedItems)
|
||||||
}, [formData, gridDto])
|
}, [formData, gridDto])
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -375,7 +375,7 @@ const IntranetDashboard: React.FC = () => {
|
||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
style={{
|
style={{
|
||||||
touchAction: 'none',
|
touchAction: isDesignMode ? 'none' : 'pan-y',
|
||||||
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',
|
||||||
|
|
|
||||||
|
|
@ -77,6 +77,7 @@ import { useListFormColumns } from './useListFormColumns'
|
||||||
import { Loading } from '@/components/shared'
|
import { Loading } from '@/components/shared'
|
||||||
import { useStoreState } from '@/store'
|
import { useStoreState } from '@/store'
|
||||||
import { workflowService } from '@/services/workflow.service'
|
import { workflowService } from '@/services/workflow.service'
|
||||||
|
import { NotePanel } from '../form/notes/NotePanel'
|
||||||
|
|
||||||
interface GridProps {
|
interface GridProps {
|
||||||
listFormCode: string
|
listFormCode: string
|
||||||
|
|
@ -238,19 +239,28 @@ const getValueByField = (data: Record<string, any> = {}, field?: string) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const shouldRunEditorScriptOnContentReady = (script?: string) =>
|
const shouldRunEditorScriptOnContentReady = (script?: string) =>
|
||||||
Boolean(script && (script.includes('setEditorReadOnly') || script.includes('runtimeSetEditorReadOnly')))
|
Boolean(
|
||||||
|
script && (script.includes('setEditorReadOnly') || script.includes('runtimeSetEditorReadOnly')),
|
||||||
|
)
|
||||||
|
|
||||||
|
const isTouchLikeDevice = () =>
|
||||||
|
typeof window !== 'undefined' &&
|
||||||
|
(window.matchMedia?.('(pointer: coarse)').matches ||
|
||||||
|
window.matchMedia?.('(hover: none)').matches)
|
||||||
|
|
||||||
const Grid = (props: GridProps) => {
|
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 config = useStoreState((state) => state.abpConfig.config)
|
const currentUser = useStoreState((state) => state.auth.user)
|
||||||
|
const useMobileEditPopup = smaller.md || isTouchLikeDevice()
|
||||||
|
|
||||||
const gridRef = useRef<DataGridRef>()
|
const gridRef = useRef<DataGridRef>()
|
||||||
const refListFormCode = useRef('')
|
const refListFormCode = useRef('')
|
||||||
const widgetGroupRef = useRef<HTMLDivElement>(null)
|
const widgetGroupRef = useRef<HTMLDivElement>(null)
|
||||||
const editingFormDataRef = useRef<Record<string, any>>({})
|
const editingFormDataRef = useRef<Record<string, any>>({})
|
||||||
const editingFormInstanceRef = useRef<any>()
|
const editingFormInstanceRef = useRef<any>()
|
||||||
|
const lastEditingContentReadyScriptKeyRef = useRef<string>()
|
||||||
// Edit popup state kaydetmeyi engellemek için flag
|
// Edit popup state kaydetmeyi engellemek için flag
|
||||||
const isEditingRef = useRef(false)
|
const isEditingRef = useRef(false)
|
||||||
|
|
||||||
|
|
@ -262,6 +272,10 @@ const Grid = (props: GridProps) => {
|
||||||
const [gridDto, setGridDto] = useState<GridDto>()
|
const [gridDto, setGridDto] = useState<GridDto>()
|
||||||
const [isPopupFullScreen, setIsPopupFullScreen] = useState(false)
|
const [isPopupFullScreen, setIsPopupFullScreen] = useState(false)
|
||||||
const [widgetGroupHeight, setWidgetGroupHeight] = useState(0)
|
const [widgetGroupHeight, setWidgetGroupHeight] = useState(0)
|
||||||
|
const [notePanelTarget, setNotePanelTarget] = useState<{
|
||||||
|
entityName: string
|
||||||
|
entityId: string
|
||||||
|
} | null>(null)
|
||||||
|
|
||||||
type EditorOptionsWithButtons = {
|
type EditorOptionsWithButtons = {
|
||||||
buttons?: any[]
|
buttons?: any[]
|
||||||
|
|
@ -338,11 +352,29 @@ const Grid = (props: GridProps) => {
|
||||||
})
|
})
|
||||||
|
|
||||||
const { createSelectDataSource } = useListFormCustomDataSource({ gridRef })
|
const { createSelectDataSource } = useListFormCustomDataSource({ gridRef })
|
||||||
|
|
||||||
|
const openNotePanel = useCallback(
|
||||||
|
(rowData: Record<string, any>) => {
|
||||||
|
const keyFieldName = gridDto?.gridOptions.keyFieldName
|
||||||
|
const entityId = getValueByField(rowData, keyFieldName)
|
||||||
|
if (entityId === undefined || entityId === null || entityId === '') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setNotePanelTarget({
|
||||||
|
entityName: gridDto?.gridOptions.listFormCode ?? listFormCode,
|
||||||
|
entityId: String(entityId),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[gridDto, listFormCode],
|
||||||
|
)
|
||||||
|
|
||||||
const { getBandedColumns } = useListFormColumns({
|
const { getBandedColumns } = useListFormColumns({
|
||||||
gridDto,
|
gridDto,
|
||||||
listFormCode,
|
listFormCode,
|
||||||
isSubForm,
|
isSubForm,
|
||||||
gridRef,
|
gridRef,
|
||||||
|
onShowNote: openNotePanel,
|
||||||
})
|
})
|
||||||
|
|
||||||
const getSelectedRowKeys = useCallback(async () => {
|
const getSelectedRowKeys = useCallback(async () => {
|
||||||
|
|
@ -374,10 +406,10 @@ const Grid = (props: GridProps) => {
|
||||||
grd,
|
grd,
|
||||||
gridDto?.gridOptions.workflowDto,
|
gridDto?.gridOptions.workflowDto,
|
||||||
selectedRowsData ?? grd.getSelectedRowsData(),
|
selectedRowsData ?? grd.getSelectedRowsData(),
|
||||||
config?.currentUser,
|
currentUser,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
[config?.currentUser, gridDto],
|
[currentUser, gridDto],
|
||||||
)
|
)
|
||||||
|
|
||||||
const refreshData = useCallback(() => {
|
const refreshData = useCallback(() => {
|
||||||
|
|
@ -705,6 +737,12 @@ const Grid = (props: GridProps) => {
|
||||||
const onEditorPreparing = useCallback(
|
const onEditorPreparing = useCallback(
|
||||||
(editor: DataGridTypes.EditorPreparingEvent<any, any>) => {
|
(editor: DataGridTypes.EditorPreparingEvent<any, any>) => {
|
||||||
if (editor.parentType === 'dataRow' && editor.dataField && gridDto) {
|
if (editor.parentType === 'dataRow' && editor.dataField && gridDto) {
|
||||||
|
if (isTouchLikeDevice()) {
|
||||||
|
editor.editorOptions.autoFocus = false
|
||||||
|
editor.editorOptions.focusStateEnabled = false
|
||||||
|
editor.editorOptions.selectTextOnFocus = false
|
||||||
|
}
|
||||||
|
|
||||||
const formItem = gridDto.gridOptions.editingFormDto
|
const formItem = gridDto.gridOptions.editingFormDto
|
||||||
.flatMap((group) => flattenEditingFormItems([group]))
|
.flatMap((group) => flattenEditingFormItems([group]))
|
||||||
.find((i) => i.dataField === editor.dataField)
|
.find((i) => i.dataField === editor.dataField)
|
||||||
|
|
@ -997,7 +1035,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 || !config) return undefined
|
if (!gridDto) return undefined
|
||||||
|
|
||||||
const cols = getBandedColumns()
|
const cols = getBandedColumns()
|
||||||
|
|
||||||
|
|
@ -1038,7 +1076,7 @@ const Grid = (props: GridProps) => {
|
||||||
})
|
})
|
||||||
|
|
||||||
return cols
|
return cols
|
||||||
}, [gridDto, config])
|
}, [gridDto])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setColumnData(memoizedColumns)
|
setColumnData(memoizedColumns)
|
||||||
|
|
@ -1158,20 +1196,20 @@ const Grid = (props: GridProps) => {
|
||||||
|
|
||||||
if (listFormField?.sourceDbType === DbTypeEnum.Date) {
|
if (listFormField?.sourceDbType === DbTypeEnum.Date) {
|
||||||
Object.assign(defaultEditorOptions, {
|
Object.assign(defaultEditorOptions, {
|
||||||
type: 'date',
|
type: 'date',
|
||||||
dateSerializationFormat: 'yyyy-MM-dd',
|
dateSerializationFormat: 'yyyy-MM-dd',
|
||||||
displayFormat: 'shortDate',
|
displayFormat: 'shortDate',
|
||||||
})
|
})
|
||||||
} else if (
|
} else if (
|
||||||
listFormField?.sourceDbType === DbTypeEnum.DateTime ||
|
listFormField?.sourceDbType === DbTypeEnum.DateTime ||
|
||||||
listFormField?.sourceDbType === DbTypeEnum.DateTime2 ||
|
listFormField?.sourceDbType === DbTypeEnum.DateTime2 ||
|
||||||
listFormField?.sourceDbType === DbTypeEnum.DateTimeOffset
|
listFormField?.sourceDbType === DbTypeEnum.DateTimeOffset
|
||||||
) {
|
) {
|
||||||
Object.assign(defaultEditorOptions, {
|
Object.assign(defaultEditorOptions, {
|
||||||
type: 'datetime',
|
type: 'datetime',
|
||||||
dateSerializationFormat: 'yyyy-MM-ddTHH:mm:ss',
|
dateSerializationFormat: 'yyyy-MM-ddTHH:mm:ss',
|
||||||
displayFormat: 'shortDateShortTime',
|
displayFormat: 'shortDateShortTime',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Her item'a placeholder olarak captionName ekle
|
// Her item'a placeholder olarak captionName ekle
|
||||||
|
|
@ -1194,6 +1232,15 @@ const Grid = (props: GridProps) => {
|
||||||
...forcedEditorOptions,
|
...forcedEditorOptions,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (useMobileEditPopup) {
|
||||||
|
editorOptions = {
|
||||||
|
...editorOptions,
|
||||||
|
autoFocus: false,
|
||||||
|
focusStateEnabled: false,
|
||||||
|
selectTextOnFocus: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (editorOptions?.buttons) {
|
if (editorOptions?.buttons) {
|
||||||
editorOptions.buttons = (editorOptions?.buttons || []).map((btn: any) => {
|
editorOptions.buttons = (editorOptions?.buttons || []).map((btn: any) => {
|
||||||
if (btn?.options?.onClick && typeof btn.options.onClick === 'string') {
|
if (btn?.options?.onClick && typeof btn.options.onClick === 'string') {
|
||||||
|
|
@ -1238,7 +1285,7 @@ const Grid = (props: GridProps) => {
|
||||||
|
|
||||||
return item
|
return item
|
||||||
},
|
},
|
||||||
[gridDto, mode, searchParams, extraFilters],
|
[gridDto, mode, searchParams, extraFilters, useMobileEditPopup],
|
||||||
)
|
)
|
||||||
|
|
||||||
// WidgetGroup yüksekliğini hesapla
|
// WidgetGroup yüksekliğini hesapla
|
||||||
|
|
@ -1451,7 +1498,27 @@ const Grid = (props: GridProps) => {
|
||||||
) {
|
) {
|
||||||
workflowService
|
workflowService
|
||||||
.startWorkflow(listFormCode, [insertedKey])
|
.startWorkflow(listFormCode, [insertedKey])
|
||||||
.then(() => gridRef.current?.instance()?.refresh())
|
.then((result) => {
|
||||||
|
const messages = result.toastMessages ?? []
|
||||||
|
if (messages.length > 0) {
|
||||||
|
toast.push(
|
||||||
|
<Notification type="info" duration={7000}>
|
||||||
|
{messages.map((message, messageIndex) => (
|
||||||
|
<div
|
||||||
|
key={messageIndex}
|
||||||
|
className={messageIndex > 0 ? 'mt-2 border-t pt-2' : undefined}
|
||||||
|
>
|
||||||
|
{message.split('\n').map((line, lineIndex) => (
|
||||||
|
<div key={lineIndex}>{line}</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</Notification>,
|
||||||
|
{ placement: 'top-end' },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
gridRef.current?.instance()?.refresh()
|
||||||
|
})
|
||||||
.catch(console.error)
|
.catch(console.error)
|
||||||
}
|
}
|
||||||
props.refreshData?.()
|
props.refreshData?.()
|
||||||
|
|
@ -1471,7 +1538,7 @@ const Grid = (props: GridProps) => {
|
||||||
/>
|
/>
|
||||||
<Editing
|
<Editing
|
||||||
refreshMode={gridDto.gridOptions.editingOptionDto?.refreshMode}
|
refreshMode={gridDto.gridOptions.editingOptionDto?.refreshMode}
|
||||||
mode={smaller.md ? 'form' : gridDto.gridOptions.editingOptionDto?.mode}
|
mode={gridDto.gridOptions.editingOptionDto?.mode}
|
||||||
allowDeleting={gridDto.gridOptions.editingOptionDto?.allowDeleting}
|
allowDeleting={gridDto.gridOptions.editingOptionDto?.allowDeleting}
|
||||||
allowUpdating={gridDto.gridOptions.editingOptionDto?.allowUpdating}
|
allowUpdating={gridDto.gridOptions.editingOptionDto?.allowUpdating}
|
||||||
allowAdding={gridDto.gridOptions.editingOptionDto?.allowAdding}
|
allowAdding={gridDto.gridOptions.editingOptionDto?.allowAdding}
|
||||||
|
|
@ -1491,10 +1558,18 @@ const Grid = (props: GridProps) => {
|
||||||
showTitle: gridDto.gridOptions.editingOptionDto?.popup?.showTitle,
|
showTitle: gridDto.gridOptions.editingOptionDto?.popup?.showTitle,
|
||||||
hideOnOutsideClick:
|
hideOnOutsideClick:
|
||||||
gridDto.gridOptions.editingOptionDto?.popup?.hideOnOutsideClick,
|
gridDto.gridOptions.editingOptionDto?.popup?.hideOnOutsideClick,
|
||||||
width: gridDto.gridOptions.editingOptionDto?.popup?.width,
|
width: useMobileEditPopup
|
||||||
height: gridDto.gridOptions.editingOptionDto?.popup?.height,
|
? '100%'
|
||||||
resizeEnabled: gridDto.gridOptions.editingOptionDto?.popup?.resizeEnabled,
|
: gridDto.gridOptions.editingOptionDto?.popup?.width,
|
||||||
fullScreen: isPopupFullScreen,
|
height: useMobileEditPopup
|
||||||
|
? '100dvh'
|
||||||
|
: gridDto.gridOptions.editingOptionDto?.popup?.height,
|
||||||
|
resizeEnabled:
|
||||||
|
!useMobileEditPopup &&
|
||||||
|
gridDto.gridOptions.editingOptionDto?.popup?.resizeEnabled,
|
||||||
|
fullScreen: useMobileEditPopup || isPopupFullScreen,
|
||||||
|
dragEnabled: !useMobileEditPopup,
|
||||||
|
restorePosition: !useMobileEditPopup,
|
||||||
toolbarItems: [
|
toolbarItems: [
|
||||||
{
|
{
|
||||||
widget: 'dxButton',
|
widget: 'dxButton',
|
||||||
|
|
@ -1538,6 +1613,7 @@ const Grid = (props: GridProps) => {
|
||||||
}}
|
}}
|
||||||
form={{
|
form={{
|
||||||
colCount: 1,
|
colCount: 1,
|
||||||
|
focusStateEnabled: !useMobileEditPopup,
|
||||||
onContentReady: (e) => {
|
onContentReady: (e) => {
|
||||||
editingFormInstanceRef.current = e.component
|
editingFormInstanceRef.current = e.component
|
||||||
|
|
||||||
|
|
@ -1558,15 +1634,27 @@ const Grid = (props: GridProps) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const runReadOnlyScripts = () => {
|
const runReadOnlyScripts = () => {
|
||||||
const editorValues = gridDto.gridOptions.editingFormDto
|
const formItems = gridDto.gridOptions.editingFormDto.flatMap((group) =>
|
||||||
.flatMap((group) => flattenEditingFormItems([group]))
|
flattenEditingFormItems([group]),
|
||||||
.reduce<Record<string, any>>((values, formItem) => {
|
)
|
||||||
|
const scriptItems = formItems.filter((formItem) =>
|
||||||
|
shouldRunEditorScriptOnContentReady(formItem.editorScript),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!scriptItems.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const editorValues = formItems.reduce<Record<string, any>>(
|
||||||
|
(values, formItem) => {
|
||||||
const editorInstance = form?.getEditor?.(formItem.dataField)
|
const editorInstance = form?.getEditor?.(formItem.dataField)
|
||||||
if (editorInstance?.option) {
|
if (editorInstance?.option) {
|
||||||
values[formItem.dataField] = editorInstance.option('value')
|
values[formItem.dataField] = editorInstance.option('value')
|
||||||
}
|
}
|
||||||
return values
|
return values
|
||||||
}, {})
|
},
|
||||||
|
{},
|
||||||
|
)
|
||||||
const formData = {
|
const formData = {
|
||||||
...editingFormDataRef.current,
|
...editingFormDataRef.current,
|
||||||
...(form?.option?.('formData') || {}),
|
...(form?.option?.('formData') || {}),
|
||||||
|
|
@ -1574,38 +1662,47 @@ const Grid = (props: GridProps) => {
|
||||||
}
|
}
|
||||||
editingFormDataRef.current = { ...formData }
|
editingFormDataRef.current = { ...formData }
|
||||||
|
|
||||||
gridDto.gridOptions.editingFormDto
|
const scriptKey = `${mode}|${String(rowKey)}|${scriptItems
|
||||||
.flatMap((group) => flattenEditingFormItems([group]))
|
.map((formItem) => formItem.dataField)
|
||||||
.filter((formItem) =>
|
.join('|')}|${JSON.stringify(formData)}`
|
||||||
shouldRunEditorScriptOnContentReady(formItem.editorScript),
|
|
||||||
)
|
|
||||||
.forEach((formItem) => {
|
|
||||||
try {
|
|
||||||
const editorInstance = form?.getEditor?.(formItem.dataField)
|
|
||||||
const editorValue =
|
|
||||||
editorInstance?.option?.('value') ??
|
|
||||||
getValueByField(formData, formItem.dataField)
|
|
||||||
const editor = {
|
|
||||||
dataField: formItem.dataField,
|
|
||||||
component: grid,
|
|
||||||
}
|
|
||||||
const e = {
|
|
||||||
component: form,
|
|
||||||
dataField: formItem.dataField,
|
|
||||||
value: editorValue,
|
|
||||||
}
|
|
||||||
|
|
||||||
executeEditorScript(formItem.editorScript!, {
|
if (lastEditingContentReadyScriptKeyRef.current === scriptKey) {
|
||||||
formData,
|
return
|
||||||
e,
|
}
|
||||||
editor,
|
|
||||||
runtimeSetEditorReadOnly,
|
lastEditingContentReadyScriptKeyRef.current = scriptKey
|
||||||
setFormData,
|
|
||||||
})
|
scriptItems.forEach((formItem) => {
|
||||||
} catch (err) {
|
try {
|
||||||
console.error('Script exec error on contentReady', formItem.dataField, err)
|
const editorInstance = form?.getEditor?.(formItem.dataField)
|
||||||
|
const editorValue =
|
||||||
|
editorInstance?.option?.('value') ??
|
||||||
|
getValueByField(formData, formItem.dataField)
|
||||||
|
const editor = {
|
||||||
|
dataField: formItem.dataField,
|
||||||
|
component: grid,
|
||||||
}
|
}
|
||||||
})
|
const e = {
|
||||||
|
component: form,
|
||||||
|
dataField: formItem.dataField,
|
||||||
|
value: editorValue,
|
||||||
|
}
|
||||||
|
|
||||||
|
executeEditorScript(formItem.editorScript!, {
|
||||||
|
formData,
|
||||||
|
e,
|
||||||
|
editor,
|
||||||
|
runtimeSetEditorReadOnly,
|
||||||
|
setFormData,
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
console.error(
|
||||||
|
'Script exec error on contentReady',
|
||||||
|
formItem.dataField,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
runReadOnlyScripts()
|
runReadOnlyScripts()
|
||||||
|
|
@ -1661,7 +1758,6 @@ const Grid = (props: GridProps) => {
|
||||||
console.error('Script exec error', err)
|
console.error('Script exec error', err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
items:
|
items:
|
||||||
|
|
@ -1874,6 +1970,14 @@ const Grid = (props: GridProps) => {
|
||||||
{toolbarModalData?.content}
|
{toolbarModalData?.content}
|
||||||
</Dialog>
|
</Dialog>
|
||||||
<GridFilterDialogs gridRef={gridRef} listFormCode={listFormCode} {...filterData} />
|
<GridFilterDialogs gridRef={gridRef} listFormCode={listFormCode} {...filterData} />
|
||||||
|
{notePanelTarget && (
|
||||||
|
<NotePanel
|
||||||
|
entityName={notePanelTarget.entityName}
|
||||||
|
entityId={notePanelTarget.entityId}
|
||||||
|
isVisible
|
||||||
|
onToggle={() => setNotePanelTarget(null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Container>
|
</Container>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,7 @@ import { useStoreState } from '@/store/store'
|
||||||
import SubForms from '../form/SubForms'
|
import SubForms from '../form/SubForms'
|
||||||
import { ImportDashboard } from '@/components/importManager/ImportDashboard'
|
import { ImportDashboard } from '@/components/importManager/ImportDashboard'
|
||||||
import { workflowService } from '@/services/workflow.service'
|
import { workflowService } from '@/services/workflow.service'
|
||||||
|
import { NotePanel } from '../form/notes/NotePanel'
|
||||||
|
|
||||||
interface TreeProps {
|
interface TreeProps {
|
||||||
listFormCode: string
|
listFormCode: string
|
||||||
|
|
@ -226,18 +227,28 @@ const getValueByField = (data: Record<string, any> = {}, field?: string) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const shouldRunEditorScriptOnContentReady = (script?: string) =>
|
const shouldRunEditorScriptOnContentReady = (script?: string) =>
|
||||||
Boolean(script && (script.includes('setEditorReadOnly') || script.includes('runtimeSetEditorReadOnly')))
|
Boolean(
|
||||||
|
script && (script.includes('setEditorReadOnly') || script.includes('runtimeSetEditorReadOnly')),
|
||||||
|
)
|
||||||
|
|
||||||
|
const isTouchLikeDevice = () =>
|
||||||
|
typeof window !== 'undefined' &&
|
||||||
|
(window.matchMedia?.('(pointer: coarse)').matches ||
|
||||||
|
window.matchMedia?.('(hover: none)').matches)
|
||||||
|
|
||||||
const Tree = (props: TreeProps) => {
|
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 useMobileEditPopup = smaller.md || isTouchLikeDevice()
|
||||||
|
|
||||||
const gridRef = useRef<TreeListRef>()
|
const gridRef = useRef<TreeListRef>()
|
||||||
const refListFormCode = useRef('')
|
const refListFormCode = useRef('')
|
||||||
const widgetGroupRef = useRef<HTMLDivElement>(null)
|
const widgetGroupRef = useRef<HTMLDivElement>(null)
|
||||||
const editingFormDataRef = useRef<Record<string, any>>({})
|
const editingFormDataRef = useRef<Record<string, any>>({})
|
||||||
const editingFormInstanceRef = useRef<any>()
|
const editingFormInstanceRef = useRef<any>()
|
||||||
|
const lastEditingContentReadyScriptKeyRef = useRef<string>()
|
||||||
// Edit popup state kaydetmeyi engellemek için flag
|
// Edit popup state kaydetmeyi engellemek için flag
|
||||||
const isEditingRef = useRef(false)
|
const isEditingRef = useRef(false)
|
||||||
|
|
||||||
|
|
@ -249,9 +260,12 @@ 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[]
|
||||||
|
|
@ -343,11 +357,29 @@ const Tree = (props: TreeProps) => {
|
||||||
})
|
})
|
||||||
|
|
||||||
const { createSelectDataSource } = useListFormCustomDataSource({ gridRef })
|
const { createSelectDataSource } = useListFormCustomDataSource({ gridRef })
|
||||||
|
|
||||||
|
const openNotePanel = useCallback(
|
||||||
|
(rowData: Record<string, any>) => {
|
||||||
|
const keyFieldName = gridDto?.gridOptions.keyFieldName
|
||||||
|
const entityId = getValueByField(rowData, keyFieldName)
|
||||||
|
if (entityId === undefined || entityId === null || entityId === '') {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setNotePanelTarget({
|
||||||
|
entityName: gridDto?.gridOptions.listFormCode ?? listFormCode,
|
||||||
|
entityId: String(entityId),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
[gridDto, listFormCode],
|
||||||
|
)
|
||||||
|
|
||||||
const { getBandedColumns } = useListFormColumns({
|
const { getBandedColumns } = useListFormColumns({
|
||||||
gridDto,
|
gridDto,
|
||||||
listFormCode,
|
listFormCode,
|
||||||
isSubForm,
|
isSubForm,
|
||||||
gridRef,
|
gridRef,
|
||||||
|
onShowNote: openNotePanel,
|
||||||
})
|
})
|
||||||
|
|
||||||
const extractSearchParamsFields = useCallback((filter: any): [string, string, any][] => {
|
const extractSearchParamsFields = useCallback((filter: any): [string, string, any][] => {
|
||||||
|
|
@ -409,10 +441,10 @@ const Tree = (props: TreeProps) => {
|
||||||
tree,
|
tree,
|
||||||
gridDto?.gridOptions.workflowDto,
|
gridDto?.gridOptions.workflowDto,
|
||||||
selectedRowsData ?? tree.getSelectedRowsData(),
|
selectedRowsData ?? tree.getSelectedRowsData(),
|
||||||
config?.currentUser,
|
currentUser,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
[config?.currentUser, gridDto],
|
[currentUser, gridDto],
|
||||||
)
|
)
|
||||||
|
|
||||||
const refreshData = useCallback(() => {
|
const refreshData = useCallback(() => {
|
||||||
|
|
@ -654,6 +686,12 @@ const Tree = (props: TreeProps) => {
|
||||||
|
|
||||||
function onEditorPreparing(editor: TreeListTypes.EditorPreparingEvent) {
|
function onEditorPreparing(editor: TreeListTypes.EditorPreparingEvent) {
|
||||||
if (editor.parentType === 'dataRow' && editor.dataField && gridDto) {
|
if (editor.parentType === 'dataRow' && editor.dataField && gridDto) {
|
||||||
|
if (isTouchLikeDevice()) {
|
||||||
|
editor.editorOptions.autoFocus = false
|
||||||
|
editor.editorOptions.focusStateEnabled = false
|
||||||
|
editor.editorOptions.selectTextOnFocus = false
|
||||||
|
}
|
||||||
|
|
||||||
const formItem = gridDto.gridOptions.editingFormDto
|
const formItem = gridDto.gridOptions.editingFormDto
|
||||||
.flatMap((group) => flattenEditingFormItems([group]))
|
.flatMap((group) => flattenEditingFormItems([group]))
|
||||||
.find((i) => i.dataField === editor.dataField)
|
.find((i) => i.dataField === editor.dataField)
|
||||||
|
|
@ -900,7 +938,7 @@ const Tree = (props: TreeProps) => {
|
||||||
}, [gridDto])
|
}, [gridDto])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!gridDto || !config) return
|
if (!gridDto) return
|
||||||
|
|
||||||
const cols = getBandedColumns()
|
const cols = getBandedColumns()
|
||||||
setColumnData(cols)
|
setColumnData(cols)
|
||||||
|
|
@ -913,7 +951,7 @@ const Tree = (props: TreeProps) => {
|
||||||
cols,
|
cols,
|
||||||
)
|
)
|
||||||
setTreeListDataSource(dataSource)
|
setTreeListDataSource(dataSource)
|
||||||
}, [gridDto, searchParams, config])
|
}, [gridDto, searchParams])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const activeFilters = extraFilters.filter((f) => f.value)
|
const activeFilters = extraFilters.filter((f) => f.value)
|
||||||
|
|
@ -1110,7 +1148,27 @@ const Tree = (props: TreeProps) => {
|
||||||
) {
|
) {
|
||||||
workflowService
|
workflowService
|
||||||
.startWorkflow(listFormCode, [insertedKey])
|
.startWorkflow(listFormCode, [insertedKey])
|
||||||
.then(() => gridRef.current?.instance()?.refresh())
|
.then((result) => {
|
||||||
|
const messages = result.toastMessages ?? []
|
||||||
|
if (messages.length > 0) {
|
||||||
|
toast.push(
|
||||||
|
<Notification type="info" duration={7000}>
|
||||||
|
{messages.map((message, messageIndex) => (
|
||||||
|
<div
|
||||||
|
key={messageIndex}
|
||||||
|
className={messageIndex > 0 ? 'mt-2 border-t pt-2' : undefined}
|
||||||
|
>
|
||||||
|
{message.split('\n').map((line, lineIndex) => (
|
||||||
|
<div key={lineIndex}>{line}</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</Notification>,
|
||||||
|
{ placement: 'top-end' },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
gridRef.current?.instance()?.refresh()
|
||||||
|
})
|
||||||
.catch(console.error)
|
.catch(console.error)
|
||||||
}
|
}
|
||||||
props.refreshData?.()
|
props.refreshData?.()
|
||||||
|
|
@ -1137,7 +1195,7 @@ const Tree = (props: TreeProps) => {
|
||||||
<RemoteOperations filtering={true} sorting={true} grouping={false} />
|
<RemoteOperations filtering={true} sorting={true} grouping={false} />
|
||||||
<Editing
|
<Editing
|
||||||
refreshMode={gridDto.gridOptions.editingOptionDto?.refreshMode}
|
refreshMode={gridDto.gridOptions.editingOptionDto?.refreshMode}
|
||||||
mode={smaller.md ? 'form' : gridDto.gridOptions.editingOptionDto?.mode}
|
mode={gridDto.gridOptions.editingOptionDto?.mode}
|
||||||
allowDeleting={gridDto.gridOptions.editingOptionDto?.allowDeleting}
|
allowDeleting={gridDto.gridOptions.editingOptionDto?.allowDeleting}
|
||||||
allowUpdating={gridDto.gridOptions.editingOptionDto?.allowUpdating}
|
allowUpdating={gridDto.gridOptions.editingOptionDto?.allowUpdating}
|
||||||
allowAdding={gridDto.gridOptions.editingOptionDto?.allowAdding}
|
allowAdding={gridDto.gridOptions.editingOptionDto?.allowAdding}
|
||||||
|
|
@ -1153,10 +1211,18 @@ const Tree = (props: TreeProps) => {
|
||||||
showTitle: gridDto.gridOptions.editingOptionDto?.popup?.showTitle,
|
showTitle: gridDto.gridOptions.editingOptionDto?.popup?.showTitle,
|
||||||
hideOnOutsideClick:
|
hideOnOutsideClick:
|
||||||
gridDto.gridOptions.editingOptionDto?.popup?.hideOnOutsideClick,
|
gridDto.gridOptions.editingOptionDto?.popup?.hideOnOutsideClick,
|
||||||
width: gridDto.gridOptions.editingOptionDto?.popup?.width,
|
width: useMobileEditPopup
|
||||||
height: gridDto.gridOptions.editingOptionDto?.popup?.height,
|
? '100%'
|
||||||
resizeEnabled: gridDto.gridOptions.editingOptionDto?.popup?.resizeEnabled,
|
: gridDto.gridOptions.editingOptionDto?.popup?.width,
|
||||||
fullScreen: isPopupFullScreen,
|
height: useMobileEditPopup
|
||||||
|
? '100dvh'
|
||||||
|
: gridDto.gridOptions.editingOptionDto?.popup?.height,
|
||||||
|
resizeEnabled:
|
||||||
|
!useMobileEditPopup &&
|
||||||
|
gridDto.gridOptions.editingOptionDto?.popup?.resizeEnabled,
|
||||||
|
fullScreen: useMobileEditPopup || isPopupFullScreen,
|
||||||
|
dragEnabled: !useMobileEditPopup,
|
||||||
|
restorePosition: !useMobileEditPopup,
|
||||||
toolbarItems: [
|
toolbarItems: [
|
||||||
{
|
{
|
||||||
widget: 'dxButton',
|
widget: 'dxButton',
|
||||||
|
|
@ -1200,6 +1266,7 @@ const Tree = (props: TreeProps) => {
|
||||||
}}
|
}}
|
||||||
form={{
|
form={{
|
||||||
colCount: 1,
|
colCount: 1,
|
||||||
|
focusStateEnabled: !useMobileEditPopup,
|
||||||
onContentReady: (e) => {
|
onContentReady: (e) => {
|
||||||
editingFormInstanceRef.current = e.component
|
editingFormInstanceRef.current = e.component
|
||||||
|
|
||||||
|
|
@ -1220,15 +1287,27 @@ const Tree = (props: TreeProps) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const runReadOnlyScripts = () => {
|
const runReadOnlyScripts = () => {
|
||||||
const editorValues = gridDto.gridOptions.editingFormDto
|
const formItems = gridDto.gridOptions.editingFormDto.flatMap((group) =>
|
||||||
.flatMap((group) => flattenEditingFormItems([group]))
|
flattenEditingFormItems([group]),
|
||||||
.reduce<Record<string, any>>((values, formItem) => {
|
)
|
||||||
|
const scriptItems = formItems.filter((formItem) =>
|
||||||
|
shouldRunEditorScriptOnContentReady(formItem.editorScript),
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!scriptItems.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const editorValues = formItems.reduce<Record<string, any>>(
|
||||||
|
(values, formItem) => {
|
||||||
const editorInstance = form?.getEditor?.(formItem.dataField)
|
const editorInstance = form?.getEditor?.(formItem.dataField)
|
||||||
if (editorInstance?.option) {
|
if (editorInstance?.option) {
|
||||||
values[formItem.dataField] = editorInstance.option('value')
|
values[formItem.dataField] = editorInstance.option('value')
|
||||||
}
|
}
|
||||||
return values
|
return values
|
||||||
}, {})
|
},
|
||||||
|
{},
|
||||||
|
)
|
||||||
const formData = {
|
const formData = {
|
||||||
...editingFormDataRef.current,
|
...editingFormDataRef.current,
|
||||||
...(form?.option?.('formData') || {}),
|
...(form?.option?.('formData') || {}),
|
||||||
|
|
@ -1236,38 +1315,47 @@ const Tree = (props: TreeProps) => {
|
||||||
}
|
}
|
||||||
editingFormDataRef.current = { ...formData }
|
editingFormDataRef.current = { ...formData }
|
||||||
|
|
||||||
gridDto.gridOptions.editingFormDto
|
const scriptKey = `${mode}|${String(rowKey)}|${scriptItems
|
||||||
.flatMap((group) => flattenEditingFormItems([group]))
|
.map((formItem) => formItem.dataField)
|
||||||
.filter((formItem) =>
|
.join('|')}|${JSON.stringify(formData)}`
|
||||||
shouldRunEditorScriptOnContentReady(formItem.editorScript),
|
|
||||||
)
|
|
||||||
.forEach((formItem) => {
|
|
||||||
try {
|
|
||||||
const editorInstance = form?.getEditor?.(formItem.dataField)
|
|
||||||
const editorValue =
|
|
||||||
editorInstance?.option?.('value') ??
|
|
||||||
getValueByField(formData, formItem.dataField)
|
|
||||||
const editor = {
|
|
||||||
dataField: formItem.dataField,
|
|
||||||
component: grid,
|
|
||||||
}
|
|
||||||
const e = {
|
|
||||||
component: form,
|
|
||||||
dataField: formItem.dataField,
|
|
||||||
value: editorValue,
|
|
||||||
}
|
|
||||||
|
|
||||||
executeEditorScript(formItem.editorScript!, {
|
if (lastEditingContentReadyScriptKeyRef.current === scriptKey) {
|
||||||
formData,
|
return
|
||||||
e,
|
}
|
||||||
editor,
|
|
||||||
runtimeSetEditorReadOnly,
|
lastEditingContentReadyScriptKeyRef.current = scriptKey
|
||||||
setFormData,
|
|
||||||
})
|
scriptItems.forEach((formItem) => {
|
||||||
} catch (err) {
|
try {
|
||||||
console.error('Script exec error on contentReady', formItem.dataField, err)
|
const editorInstance = form?.getEditor?.(formItem.dataField)
|
||||||
|
const editorValue =
|
||||||
|
editorInstance?.option?.('value') ??
|
||||||
|
getValueByField(formData, formItem.dataField)
|
||||||
|
const editor = {
|
||||||
|
dataField: formItem.dataField,
|
||||||
|
component: grid,
|
||||||
}
|
}
|
||||||
})
|
const e = {
|
||||||
|
component: form,
|
||||||
|
dataField: formItem.dataField,
|
||||||
|
value: editorValue,
|
||||||
|
}
|
||||||
|
|
||||||
|
executeEditorScript(formItem.editorScript!, {
|
||||||
|
formData,
|
||||||
|
e,
|
||||||
|
editor,
|
||||||
|
runtimeSetEditorReadOnly,
|
||||||
|
setFormData,
|
||||||
|
})
|
||||||
|
} catch (err) {
|
||||||
|
console.error(
|
||||||
|
'Script exec error on contentReady',
|
||||||
|
formItem.dataField,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
runReadOnlyScripts()
|
runReadOnlyScripts()
|
||||||
|
|
@ -1323,7 +1411,6 @@ const Tree = (props: TreeProps) => {
|
||||||
console.error('Script exec error', err)
|
console.error('Script exec error', err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
items:
|
items:
|
||||||
|
|
@ -1345,7 +1432,9 @@ const Tree = (props: TreeProps) => {
|
||||||
let parsedEditorOptions: EditorOptionsWithButtons = {}
|
let parsedEditorOptions: EditorOptionsWithButtons = {}
|
||||||
const forcedEditorOptions: EditorOptionsWithButtons = {}
|
const forcedEditorOptions: EditorOptionsWithButtons = {}
|
||||||
try {
|
try {
|
||||||
parsedEditorOptions = i.editorOptions ? JSON.parse(i.editorOptions) : {}
|
parsedEditorOptions = i.editorOptions
|
||||||
|
? JSON.parse(i.editorOptions)
|
||||||
|
: {}
|
||||||
|
|
||||||
const rawFilter = searchParams?.get('filter')
|
const rawFilter = searchParams?.get('filter')
|
||||||
if (rawFilter) {
|
if (rawFilter) {
|
||||||
|
|
@ -1377,20 +1466,20 @@ const Tree = (props: TreeProps) => {
|
||||||
|
|
||||||
if (listFormField?.sourceDbType === DbTypeEnum.Date) {
|
if (listFormField?.sourceDbType === DbTypeEnum.Date) {
|
||||||
Object.assign(defaultEditorOptions, {
|
Object.assign(defaultEditorOptions, {
|
||||||
type: 'date',
|
type: 'date',
|
||||||
dateSerializationFormat: 'yyyy-MM-dd',
|
dateSerializationFormat: 'yyyy-MM-dd',
|
||||||
displayFormat: 'shortDate',
|
displayFormat: 'shortDate',
|
||||||
})
|
})
|
||||||
} else if (
|
} else if (
|
||||||
listFormField?.sourceDbType === DbTypeEnum.DateTime ||
|
listFormField?.sourceDbType === DbTypeEnum.DateTime ||
|
||||||
listFormField?.sourceDbType === DbTypeEnum.DateTime2 ||
|
listFormField?.sourceDbType === DbTypeEnum.DateTime2 ||
|
||||||
listFormField?.sourceDbType === DbTypeEnum.DateTimeOffset
|
listFormField?.sourceDbType === DbTypeEnum.DateTimeOffset
|
||||||
) {
|
) {
|
||||||
Object.assign(defaultEditorOptions, {
|
Object.assign(defaultEditorOptions, {
|
||||||
type: 'datetime',
|
type: 'datetime',
|
||||||
dateSerializationFormat: 'yyyy-MM-ddTHH:mm:ss',
|
dateSerializationFormat: 'yyyy-MM-ddTHH:mm:ss',
|
||||||
displayFormat: 'shortDateShortTime',
|
displayFormat: 'shortDateShortTime',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
editorOptions = {
|
editorOptions = {
|
||||||
|
|
@ -1399,6 +1488,15 @@ const Tree = (props: TreeProps) => {
|
||||||
...forcedEditorOptions,
|
...forcedEditorOptions,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (useMobileEditPopup) {
|
||||||
|
editorOptions = {
|
||||||
|
...editorOptions,
|
||||||
|
autoFocus: false,
|
||||||
|
focusStateEnabled: false,
|
||||||
|
selectTextOnFocus: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (editorOptions?.buttons) {
|
if (editorOptions?.buttons) {
|
||||||
editorOptions.buttons = (editorOptions?.buttons || []).map(
|
editorOptions.buttons = (editorOptions?.buttons || []).map(
|
||||||
(btn: any) => {
|
(btn: any) => {
|
||||||
|
|
@ -1613,6 +1711,14 @@ const Tree = (props: TreeProps) => {
|
||||||
{toolbarModalData?.content}
|
{toolbarModalData?.content}
|
||||||
</Dialog>
|
</Dialog>
|
||||||
<GridFilterDialogs gridRef={gridRef as any} listFormCode={listFormCode} {...filterData} />
|
<GridFilterDialogs gridRef={gridRef as any} listFormCode={listFormCode} {...filterData} />
|
||||||
|
{notePanelTarget && (
|
||||||
|
<NotePanel
|
||||||
|
entityName={notePanelTarget.entityName}
|
||||||
|
entityId={notePanelTarget.entityId}
|
||||||
|
isVisible
|
||||||
|
onToggle={() => setNotePanelTarget(null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Container>
|
</Container>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -247,11 +247,13 @@ const useListFormColumns = ({
|
||||||
listFormCode,
|
listFormCode,
|
||||||
isSubForm,
|
isSubForm,
|
||||||
gridRef,
|
gridRef,
|
||||||
|
onShowNote,
|
||||||
}: {
|
}: {
|
||||||
gridDto?: GridDto
|
gridDto?: GridDto
|
||||||
listFormCode: string
|
listFormCode: string
|
||||||
isSubForm?: boolean
|
isSubForm?: boolean
|
||||||
gridRef?: any
|
gridRef?: any
|
||||||
|
onShowNote?: (rowData: Record<string, any>) => void
|
||||||
}) => {
|
}) => {
|
||||||
const dialog: any = useDialogContext()
|
const dialog: any = useDialogContext()
|
||||||
const { translate } = useLocalization()
|
const { translate } = useLocalization()
|
||||||
|
|
@ -435,6 +437,11 @@ const useListFormColumns = ({
|
||||||
gridDto.gridOptions.editingOptionDto.allowDetail &&
|
gridDto.gridOptions.editingOptionDto.allowDetail &&
|
||||||
checkPermission(gridDto.gridOptions.permissionDto.u)
|
checkPermission(gridDto.gridOptions.permissionDto.u)
|
||||||
|
|
||||||
|
const hasShowNote =
|
||||||
|
gridDto.gridOptions.showNote &&
|
||||||
|
checkPermission(gridDto.gridOptions.permissionDto.n) &&
|
||||||
|
typeof onShowNote === 'function'
|
||||||
|
|
||||||
const hasDuplicate =
|
const hasDuplicate =
|
||||||
gridDto.gridOptions.editingOptionDto.allowDuplicate &&
|
gridDto.gridOptions.editingOptionDto.allowDuplicate &&
|
||||||
checkPermission(gridDto.gridOptions.permissionDto.i)
|
checkPermission(gridDto.gridOptions.permissionDto.i)
|
||||||
|
|
@ -442,7 +449,15 @@ const useListFormColumns = ({
|
||||||
const hasCommandButtons = gridDto.gridOptions.commandColumnDto.length > 0
|
const hasCommandButtons = gridDto.gridOptions.commandColumnDto.length > 0
|
||||||
|
|
||||||
// Eğer hiçbir buton eklenecek durumda değilse: çık
|
// Eğer hiçbir buton eklenecek durumda değilse: çık
|
||||||
if (!hasUpdate && !hasDelete && !hasCreate && !hasCommandButtons) {
|
if (
|
||||||
|
!hasUpdate &&
|
||||||
|
!hasDelete &&
|
||||||
|
!hasCreate &&
|
||||||
|
!hasDetail &&
|
||||||
|
!hasShowNote &&
|
||||||
|
!hasDuplicate &&
|
||||||
|
!hasCommandButtons
|
||||||
|
) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -485,6 +500,20 @@ const useListFormColumns = ({
|
||||||
buttons.push(item)
|
buttons.push(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (hasShowNote) {
|
||||||
|
buttons.push({
|
||||||
|
name: 'note',
|
||||||
|
text: translate('::ListForms.ListForm.NoteModal.Type.Note'),
|
||||||
|
onClick: (e: any) => {
|
||||||
|
if (typeof e.event?.preventDefault === 'function') {
|
||||||
|
e.event.preventDefault()
|
||||||
|
}
|
||||||
|
|
||||||
|
onShowNote?.(e.row.data)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if (hasDuplicate) {
|
if (hasDuplicate) {
|
||||||
const item = {
|
const item = {
|
||||||
name: 'duplicate',
|
name: 'duplicate',
|
||||||
|
|
@ -600,7 +629,7 @@ const useListFormColumns = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
return column as GridColumnData
|
return column as GridColumnData
|
||||||
}, [gridDto, checkPermission, translate, listFormCode, isPwaMode, dialog, gridRef])
|
}, [gridDto, checkPermission, translate, listFormCode, isPwaMode, dialog, gridRef, onShowNote])
|
||||||
|
|
||||||
const getColumns = useCallback(
|
const getColumns = useCallback(
|
||||||
(columnFormats: ColumnFormatDto[]) => {
|
(columnFormats: ColumnFormatDto[]) => {
|
||||||
|
|
|
||||||
|
|
@ -11,12 +11,35 @@ 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 = ({
|
||||||
|
|
@ -32,7 +55,7 @@ const useToolbar = ({
|
||||||
}: {
|
}: {
|
||||||
gridDto?: GridDto
|
gridDto?: GridDto
|
||||||
listFormCode: string
|
listFormCode: string
|
||||||
getSelectedRowKeys: () => void
|
getSelectedRowKeys: () => unknown[] | Promise<unknown[]>
|
||||||
getSelectedRowsData: () => any
|
getSelectedRowsData: () => any
|
||||||
refreshData: () => void
|
refreshData: () => void
|
||||||
getFilter: () => void
|
getFilter: () => void
|
||||||
|
|
@ -48,7 +71,7 @@ const useToolbar = ({
|
||||||
const { translate } = useLocalization()
|
const { translate } = useLocalization()
|
||||||
const { checkPermission } = usePermission()
|
const { checkPermission } = usePermission()
|
||||||
const isPwaMode = usePWA()
|
const isPwaMode = usePWA()
|
||||||
const config = useStoreState((state) => state.abpConfig.config)
|
const currentUser = useStoreState((state) => state.auth.user)
|
||||||
|
|
||||||
const [toolbarData, setToolbarData] = useState<ToolbarItem[]>([])
|
const [toolbarData, setToolbarData] = useState<ToolbarItem[]>([])
|
||||||
const [toolbarModalData, setToolbarModalData] = useState<ToolbarModalData>()
|
const [toolbarModalData, setToolbarModalData] = useState<ToolbarModalData>()
|
||||||
|
|
@ -113,7 +136,8 @@ const useToolbar = ({
|
||||||
})
|
})
|
||||||
|
|
||||||
const workflowOptions = grdOpt.workflowDto
|
const workflowOptions = grdOpt.workflowDto
|
||||||
const approvalCriteria = workflowOptions?.criteria?.filter((item) => item.kind === 'Approval') ?? []
|
const approvalCriteria =
|
||||||
|
workflowOptions?.criteria?.filter((item) => item.kind === 'Approval') ?? []
|
||||||
if (
|
if (
|
||||||
workflowOptions?.approvalStatusFieldName &&
|
workflowOptions?.approvalStatusFieldName &&
|
||||||
approvalCriteria.length > 0 &&
|
approvalCriteria.length > 0 &&
|
||||||
|
|
@ -149,7 +173,7 @@ const useToolbar = ({
|
||||||
) {
|
) {
|
||||||
toast.push(
|
toast.push(
|
||||||
<Notification type="warning" duration={2500}>
|
<Notification type="warning" duration={2500}>
|
||||||
Secili kayit icin workflow zaten baslamis.
|
{translate('::WorkflowAlreadyStarted')}
|
||||||
</Notification>,
|
</Notification>,
|
||||||
{ placement: 'top-end' },
|
{ placement: 'top-end' },
|
||||||
)
|
)
|
||||||
|
|
@ -157,7 +181,8 @@ const useToolbar = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await workflowService.startWorkflow(listFormCode, keys)
|
const result = await workflowService.startWorkflow(listFormCode, keys)
|
||||||
|
showWorkflowToastMessages(result)
|
||||||
refreshData()
|
refreshData()
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toast.push(
|
toast.push(
|
||||||
|
|
@ -204,14 +229,14 @@ const useToolbar = ({
|
||||||
row,
|
row,
|
||||||
workflowOptions,
|
workflowOptions,
|
||||||
criteria.title,
|
criteria.title,
|
||||||
getCurrentUserWorkflowIdentities(config?.currentUser),
|
getCurrentUserWorkflowIdentities(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}>
|
||||||
Secili kayit bu onay adiminda veya onay kullanicisinda beklemiyor.
|
{translate('::SeciliKayitBekliyor')}
|
||||||
</Notification>,
|
</Notification>,
|
||||||
{ placement: 'top-end' },
|
{ placement: 'top-end' },
|
||||||
)
|
)
|
||||||
|
|
@ -302,16 +327,75 @@ const useToolbar = ({
|
||||||
text: translate('::ListForms.ListForm.DeleteSelectedRecords'),
|
text: translate('::ListForms.ListForm.DeleteSelectedRecords'),
|
||||||
icon: 'trash',
|
icon: 'trash',
|
||||||
visible: false,
|
visible: false,
|
||||||
onClick() {
|
async onClick() {
|
||||||
if (!grdOpt.deleteServiceAddress) {
|
if (!grdOpt.deleteServiceAddress) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
dynamicFetch(grdOpt.deleteServiceAddress, 'POST', null, {
|
const selectedKeys = await Promise.resolve(getSelectedRowKeys())
|
||||||
keys: getSelectedRowKeys(),
|
const keys = Array.isArray(selectedKeys) ? [...selectedKeys] : []
|
||||||
listFormCode,
|
|
||||||
}).then(() => {
|
if (!keys.length) {
|
||||||
refreshData()
|
toast.push(
|
||||||
|
<Notification type="warning" duration={2000}>
|
||||||
|
{translate('::ListForms.ListForm.SelectRecord')}
|
||||||
|
</Notification>,
|
||||||
|
{ placement: 'top-end' },
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setToolbarModalData({
|
||||||
|
open: true,
|
||||||
|
content: (
|
||||||
|
<>
|
||||||
|
<h5 className="mb-4">
|
||||||
|
{translate('::ListForms.ListForm.DeleteSelectedRecords')}
|
||||||
|
</h5>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
{translate('::SeciliKayitlarSilmekIstiyormusunuz', {
|
||||||
|
0: keys.length,
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="text-right mt-6">
|
||||||
|
<Button
|
||||||
|
className="ltr:mr-2 rtl:ml-2"
|
||||||
|
variant="plain"
|
||||||
|
onClick={() => setToolbarModalData(undefined)}
|
||||||
|
>
|
||||||
|
{translate('::Cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="solid"
|
||||||
|
onClick={() => {
|
||||||
|
dynamicFetch(grdOpt.deleteServiceAddress!, 'POST', null, {
|
||||||
|
keys,
|
||||||
|
listFormCode,
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
refreshData()
|
||||||
|
setToolbarModalData(undefined)
|
||||||
|
})
|
||||||
|
.catch((error: any) => {
|
||||||
|
toast.push(
|
||||||
|
<Notification type="danger" duration={3000}>
|
||||||
|
{error?.response?.data?.error?.message ||
|
||||||
|
error?.response?.data?.message ||
|
||||||
|
error?.message ||
|
||||||
|
translate('::SilmeIslemiBasarisiz')}
|
||||||
|
</Notification>,
|
||||||
|
{ placement: 'top-end' },
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{translate('::Delete')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
),
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -342,9 +426,13 @@ const useToolbar = ({
|
||||||
open: true,
|
open: true,
|
||||||
content: (
|
content: (
|
||||||
<>
|
<>
|
||||||
<h5 className="mb-4">Delete All Records</h5>
|
<h5 className="mb-4">{translate('::ListForms.ListForm.DeleteAllRecords')}</h5>
|
||||||
|
|
||||||
<p>Are you sure to delete all {r.data.totalCount} records?</p>
|
<p>
|
||||||
|
{translate('::TumKayitlariSilmekIstiyormusunuz', {
|
||||||
|
0: r.data.totalCount,
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
|
||||||
<div className="text-right mt-6">
|
<div className="text-right mt-6">
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -362,7 +450,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}>
|
||||||
{'Tüm kayıtlar silindi.'}
|
{translate('::TumKayitlarSilindi')}
|
||||||
</Notification>,
|
</Notification>,
|
||||||
{
|
{
|
||||||
placement: 'top-end',
|
placement: 'top-end',
|
||||||
|
|
@ -483,10 +571,9 @@ const useToolbar = ({
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!gridDto && !listFormCode) return
|
if (!gridDto && !listFormCode) return
|
||||||
if (!config) return
|
|
||||||
|
|
||||||
getToolbarData()
|
getToolbarData()
|
||||||
}, [gridDto, listFormCode, config])
|
}, [gridDto, listFormCode, currentUser])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
toolbarData,
|
toolbarData,
|
||||||
|
|
@ -522,7 +609,9 @@ function isWorkflowApprovalCriteriaActive(
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeWorkflowValue(value: unknown) {
|
function normalizeWorkflowValue(value: unknown) {
|
||||||
return String(value ?? '').trim().toLocaleLowerCase('tr-TR')
|
return String(value ?? '')
|
||||||
|
.trim()
|
||||||
|
.toLocaleLowerCase('tr-TR')
|
||||||
}
|
}
|
||||||
|
|
||||||
function isWorkflowNotStarted(row: Record<string, unknown>, workflowOptions: WorkflowDto) {
|
function isWorkflowNotStarted(row: Record<string, unknown>, workflowOptions: WorkflowDto) {
|
||||||
|
|
@ -547,7 +636,8 @@ export function updateWorkflowApprovalToolbarItems(
|
||||||
name?: string
|
name?: string
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
const approvalCriteria = workflowOptions?.criteria?.filter((item) => item.kind === 'Approval') ?? []
|
const approvalCriteria =
|
||||||
|
workflowOptions?.criteria?.filter((item) => item.kind === 'Approval') ?? []
|
||||||
if (!component || !workflowOptions?.approvalStatusFieldName || !approvalCriteria.length) {
|
if (!component || !workflowOptions?.approvalStatusFieldName || !approvalCriteria.length) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -586,7 +676,12 @@ export function updateWorkflowApprovalToolbarItems(
|
||||||
const enabled =
|
const enabled =
|
||||||
selectedRowsData.length > 0 &&
|
selectedRowsData.length > 0 &&
|
||||||
selectedRowsData.every((row) =>
|
selectedRowsData.every((row) =>
|
||||||
isWorkflowApprovalCriteriaActive(row, workflowOptions, criteria.title, currentUserIdentities),
|
isWorkflowApprovalCriteriaActive(
|
||||||
|
row,
|
||||||
|
workflowOptions,
|
||||||
|
criteria.title,
|
||||||
|
currentUserIdentities,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
const optionPath = `toolbar.items[${toolbarItemIndex}].options.disabled`
|
const optionPath = `toolbar.items[${toolbarItemIndex}].options.disabled`
|
||||||
|
|
@ -619,17 +714,12 @@ function WorkflowApprovalDecisionDialog({
|
||||||
const decide = async (approved: boolean) => {
|
const decide = async (approved: boolean) => {
|
||||||
setSubmitting(true)
|
setSubmitting(true)
|
||||||
try {
|
try {
|
||||||
await Promise.all(
|
const results = await Promise.all(
|
||||||
keys.map((key) =>
|
keys.map((key) =>
|
||||||
workflowService.decideWorkflow(
|
workflowService.decideWorkflow(listFormCode, [key], approved, note, criteriaId),
|
||||||
listFormCode,
|
|
||||||
[key],
|
|
||||||
approved,
|
|
||||||
note,
|
|
||||||
criteriaId,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
showWorkflowToastMessages(results)
|
||||||
onCompleted()
|
onCompleted()
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
toast.push(
|
toast.push(
|
||||||
|
|
@ -649,24 +739,33 @@ function WorkflowApprovalDecisionDialog({
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h5 className="mb-4">{criteriaTitle}</h5>
|
<h5 className="mb-4">{criteriaTitle}</h5>
|
||||||
<p>{keys.length} kayit icin workflow karari verilecek.</p>
|
<p className="mb-4">
|
||||||
<label className="mb-2 block font-semibold">Not</label>
|
{translate('::App.Listform.ListformField.WorkflowDecisionMessage', {
|
||||||
|
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}
|
||||||
placeholder="Onay veya red aciklamasi"
|
autoFocus
|
||||||
|
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 className="ltr:mr-2 rtl:ml-2" variant="plain" disabled={submitting} onClick={onCancel}>
|
<Button
|
||||||
|
className="ltr:mr-2 rtl:ml-2"
|
||||||
|
variant="plain"
|
||||||
|
disabled={submitting}
|
||||||
|
onClick={onCancel}
|
||||||
|
>
|
||||||
{translate('::Cancel')}
|
{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)}>
|
||||||
Reddet
|
{translate('::App.Listform.ListformField.Rejecter')}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="solid" disabled={submitting} onClick={() => decide(true)}>
|
<Button variant="solid" disabled={submitting} onClick={() => decide(true)}>
|
||||||
Onayla
|
{translate('::App.Listform.ListformField.Approver')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -128,7 +128,14 @@ export interface MenuAddDialogProps {
|
||||||
initialParentCode: string
|
initialParentCode: string
|
||||||
initialOrder: number
|
initialOrder: number
|
||||||
rawItems: (MenuItem & { id?: string })[]
|
rawItems: (MenuItem & { id?: string })[]
|
||||||
onSaved: () => void
|
onSaved: (menu: {
|
||||||
|
code: string
|
||||||
|
parentCode?: string
|
||||||
|
menuTextEn: string
|
||||||
|
menuTextTr: string
|
||||||
|
icon?: string
|
||||||
|
shortName?: string
|
||||||
|
}) => void | Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MenuAddDialog({
|
export function MenuAddDialog({
|
||||||
|
|
@ -172,7 +179,7 @@ export function MenuAddDialog({
|
||||||
if (shortNameRequired && !form.shortName.trim()) return
|
if (shortNameRequired && !form.shortName.trim()) return
|
||||||
setSaving(true)
|
setSaving(true)
|
||||||
try {
|
try {
|
||||||
await menuService.createWithLanguageKeyText({
|
const savedMenu = {
|
||||||
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,
|
||||||
|
|
@ -182,9 +189,18 @@ 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
|
||||||
|
|
||||||
onSaved()
|
await menuService.createWithLanguageKeyText(savedMenu)
|
||||||
|
|
||||||
|
await onSaved({
|
||||||
|
code: savedMenu.code!,
|
||||||
|
parentCode: savedMenu.parentCode,
|
||||||
|
menuTextEn: savedMenu.menuTextEn!,
|
||||||
|
menuTextTr: savedMenu.menuTextTr!,
|
||||||
|
icon: savedMenu.icon,
|
||||||
|
shortName: savedMenu.shortName,
|
||||||
|
})
|
||||||
onClose()
|
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' })
|
||||||
|
|
@ -342,10 +358,11 @@ 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 size="sm" variant="plain" onClick={onClose}>
|
<Button type="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}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue