Compare commits

..

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

71 changed files with 785 additions and 3567 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -47,30 +47,12 @@
}
.dialog-content {
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;
@apply p-6 rounded-lg shadow-xl sm:my-16 relative h-full bg-white dark:bg-gray-800;
}
.dialog-content.maximized {
border-radius: 0 !important;
height: 100vh !important;
max-height: 100vh !important;
margin: 0 !important;
overflow: hidden;
display: flex;

View file

@ -99,9 +99,7 @@ export const FileUploadArea: React.FC<FileUploadAreaProps> = ({
{!selectedFile ? (
<div
className={`relative border-2 border-dashed rounded-lg p-3 transition-all duration-200 ${
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'
dragActive ? 'border-blue-400 bg-blue-50' : 'border-slate-300 hover:border-slate-400'
} ${loading ? 'opacity-50 pointer-events-none' : ''}`}
onDragEnter={handleDrag}
onDragLeave={handleDrag}
@ -119,39 +117,35 @@ export const FileUploadArea: React.FC<FileUploadAreaProps> = ({
<div className="text-center">
<FaUpload
className={`mx-auto h-3 w-3 ${dragActive ? 'text-blue-500 dark:text-blue-400' : 'text-slate-400 dark:text-slate-500'}`}
className={`mx-auto h-3 w-3 ${dragActive ? 'text-blue-500' : 'text-slate-400'}`}
/>
<div className="text-lg font-medium text-slate-700 dark:text-slate-200 mb-2">
<div className="text-lg font-medium text-slate-700 mb-2">
{dragActive
? translate('::App.Listforms.ImportManager.DropHere')
: translate('::App.Listforms.ImportManager.UploadYourFile')}
</div>
<p className="text-slate-500 dark:text-slate-400 mb-4">
<p className="text-slate-500 mb-4">
{translate('::App.Listforms.ImportManager.DragOrClick')}
</p>
<div className="text-sm text-slate-400 dark:text-slate-500">
<div className="text-sm text-slate-400">
{translate('::App.Listforms.ImportManager.SupportedFormats')}{' '}
{acceptedFormats.join(', ')} Max size: {maxSize}MB
</div>
</div>
</div>
) : (
<div className="border border-slate-200 dark:border-slate-700 rounded-lg p-4 dark:bg-slate-900/40">
<div className="border border-slate-200 rounded-lg p-4">
<div className="flex items-center justify-between">
<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" />
<div className="min-w-0 flex-1">
<div className="font-medium text-slate-800 dark:text-slate-100 truncate">
{selectedFile.name}
</div>
<div className="text-sm text-slate-500 dark:text-slate-400">
{formatFileSize(selectedFile.size)}
</div>
<div className="font-medium text-slate-800 truncate">{selectedFile.name}</div>
<div className="text-sm text-slate-500">{formatFileSize(selectedFile.size)}</div>
</div>
</div>
<button
onClick={clearFile}
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"
className="p-2 text-slate-400 hover:text-slate-600 hover:bg-slate-100 rounded-lg transition-colors"
>
<FaTimes className="w-4 h-4" />
</button>
@ -160,9 +154,9 @@ export const FileUploadArea: React.FC<FileUploadAreaProps> = ({
)}
{error && (
<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">
<div className="flex items-center space-x-2 p-3 bg-red-50 border border-red-200 rounded-lg">
<FaRegCircle className="w-5 h-5 text-red-500" />
<span className="text-red-700 dark:text-red-300">{error}</span>
<span className="text-red-700">{error}</span>
</div>
)}

View file

@ -195,17 +195,17 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
switch (status) {
case 'uploaded':
case 'executed':
return 'bg-green-50 text-green-700 border-green-200 dark:bg-green-950/30 dark:text-green-300 dark:border-green-900/60'
return 'bg-green-50 text-green-700 border-green-200'
case 'executed_with_errors':
return 'bg-orange-50 text-orange-700 border-orange-200 dark:bg-orange-950/30 dark:text-orange-300 dark:border-orange-900/60'
return 'bg-orange-50 text-orange-700 border-orange-200'
case 'failed':
case 'execute_failed':
return 'bg-red-50 text-red-700 border-red-200 dark:bg-red-950/30 dark:text-red-300 dark:border-red-900/60'
return 'bg-red-50 text-red-700 border-red-200'
case 'processing':
case 'validating':
return 'bg-blue-50 text-blue-700 border-blue-200 dark:bg-blue-950/30 dark:text-blue-300 dark:border-blue-900/60'
return 'bg-blue-50 text-blue-700 border-blue-200'
default:
return 'bg-yellow-50 text-yellow-700 border-yellow-200 dark:bg-yellow-950/30 dark:text-yellow-300 dark:border-yellow-900/60'
return 'bg-yellow-50 text-yellow-700 border-yellow-200'
}
}
@ -269,17 +269,17 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
const getExecuteStatusColor = (status: string) => {
switch (status) {
case 'completed':
return 'text-green-600 dark:text-green-300'
return 'text-green-600'
case 'completed_with_errors':
return 'text-orange-600 dark:text-orange-300'
return 'text-orange-600'
case 'processing':
return 'text-blue-600 dark:text-blue-300'
return 'text-blue-600'
case 'validating':
return 'text-yellow-600 dark:text-yellow-300'
return 'text-yellow-600'
case 'failed':
return 'text-red-600 dark:text-red-300'
return 'text-red-600'
default:
return 'text-slate-600 dark:text-slate-400'
return 'text-slate-600'
}
}
@ -331,7 +331,7 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
return (
<div className="flex flex-col w-full mt-4">
{/* Navigation Tabs */}
<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">
<div className="flex space-x-1 mb-4 bg-white rounded-lg p-1 shadow-sm border border-slate-200 flex-shrink-0">
{['import', 'preview', 'history'].map((tab) => (
<button
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 ${
activeTab === tab
? 'bg-blue-500 text-white shadow-md'
: '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'
: 'text-slate-600 hover:text-slate-800 hover:bg-slate-50'
}`}
>
{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">
{/* Template Generator - 2/3 width on large screens, full width on mobile */}
<div className="lg:col-span-2">
<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 border-slate-200 dark:border-slate-700 flex items-center justify-between">
<h3 className="text-xl font-semibold text-slate-800 dark:text-slate-100 flex items-center">
<div className="bg-white rounded-xl shadow-sm border border-slate-200">
<div className="px-3 py-3 border-b flex items-center justify-between">
<h3 className="text-xl font-semibold text-slate-800 flex items-center">
<FaDownload className="w-4 h-4 mr-2" />
{translate('::App.Listforms.ImportManager.TemplateColumns')} (
{editableColumns.length})
@ -371,10 +371,10 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
<button
onClick={() => generateTemplate('excel')}
disabled={generating}
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"
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"
>
<FaFileExcel className="w-3.5 h-3.5 text-green-500 group-hover:scale-110 transition-transform" />
<span className="font-medium text-slate-700 dark:text-slate-200">
<span className="font-medium text-slate-700">
{translate('::App.Listforms.ImportManager.ExcelTemplate')}
</span>
</button>
@ -382,10 +382,10 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
<button
onClick={() => generateTemplate('csv')}
disabled={generating}
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"
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"
>
<FaFileAlt className="w-3.5 h-3.5 text-blue-500 group-hover:scale-110 transition-transform" />
<span className="font-medium text-slate-700 dark:text-slate-200">
<span className="font-medium text-slate-700">
{translate('::App.Listforms.ImportManager.CsvTemplate')}
</span>
</button>
@ -394,38 +394,38 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
<div className="max-h-96 overflow-y-auto">
<table className="w-full">
<thead className="bg-slate-100 dark:bg-slate-800 sticky top-0">
<thead className="bg-slate-100 sticky top-0">
<tr>
<th className="px-4 py-2 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase">
<th className="px-4 py-2 text-left text-xs font-medium text-slate-500 uppercase">
{translate('::App.Listform.ListformField.Column')}
</th>
<th className="px-4 py-2 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase">
<th className="px-4 py-2 text-left text-xs font-medium text-slate-500 uppercase">
{translate('::ListForms.ListFormEdit.Type')}
</th>
<th className="px-4 py-2 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase">
<th className="px-4 py-2 text-left text-xs font-medium text-slate-500 uppercase">
{translate('::App.Required')}
</th>
<th className="px-4 py-2 text-left text-xs font-medium text-slate-500 dark:text-slate-400 uppercase">
<th className="px-4 py-2 text-left text-xs font-medium text-slate-500 uppercase">
{translate('::Abp.Mailing.Default')}
</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100 dark:divide-slate-800">
<tbody className="divide-y divide-slate-100">
{editableColumns.map((column: any) => (
<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 dark:text-slate-100">
<tr key={column.fieldName} className="hover:bg-slate-50">
<td className="px-2 py-2 font-medium text-slate-800">
{column.fieldName}
</td>
<td className="px-4 py-2 text-slate-600 dark:text-slate-300">
<td className="px-4 py-2 text-slate-600">
<span
className={`px-2 py-1 rounded text-xs font-medium ${
column.dataType === 'string'
? 'bg-blue-100 text-blue-800 dark:bg-blue-950/40 dark:text-blue-300'
? 'bg-blue-100 text-blue-800'
: column.dataType === 'number'
? 'bg-green-100 text-green-800 dark:bg-green-950/40 dark:text-green-300'
? 'bg-green-100 text-green-800'
: column.dataType === 'boolean'
? 'bg-purple-100 text-purple-800 dark:bg-purple-950/40 dark:text-purple-300'
: 'bg-orange-100 text-orange-800 dark:bg-orange-950/40 dark:text-orange-300'
? 'bg-purple-100 text-purple-800'
: 'bg-orange-100 text-orange-800'
}`}
>
{column.dataType}
@ -435,16 +435,16 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
{column.validationRuleDto.some(
(rule: any) => rule.type === 'required',
) ? (
<span className="text-red-500 dark:text-red-300 font-medium">
<span className="text-red-500 font-medium">
{translate('::App.Listforms.ImportManager.Yes')}
</span>
) : (
<span className="text-slate-400 dark:text-slate-500">
<span className="text-slate-400">
{translate('::App.Listforms.ImportManager.No')}
</span>
)}
</td>
<td className="px-4 py-2 text-slate-600 dark:text-slate-300 text-sm">
<td className="px-4 py-2 text-slate-600 text-sm">
{typeof column.defaultValue === 'object'
? JSON.stringify(column.defaultValue)
: column.defaultValue}
@ -458,7 +458,7 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
{generating && (
<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>
<span className="ml-2 text-slate-600 dark:text-slate-400">
<span className="ml-2 text-slate-600">
{translate('::App.Listforms.ImportManager.GeneratingTemplate')}
</span>
</div>
@ -468,8 +468,8 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
{/* File Upload - 1/3 width on large screens, full width on mobile */}
<div className="lg:col-span-1">
<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 dark:text-slate-100 mb-4 flex items-center">
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-4 h-full">
<h2 className="text-xl font-semibold text-slate-800 mb-4 flex items-center">
<FaUpload className="w-5 h-5 mr-2 text-green-500" />
{translate('::App.Listforms.ImportManager.UploadData')}
</h2>
@ -506,9 +506,9 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
)}
</div>
) : (
<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 dark:text-slate-400">
<FaEye className="w-16 h-16 mx-auto mb-4 text-slate-300 dark:text-slate-600" />
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-12">
<div className="text-center text-slate-500">
<FaEye className="w-16 h-16 mx-auto mb-4 text-slate-300" />
<div className="text-xl font-medium mb-2">
{translate('::App.Listforms.ImportManager.NoDataToPreview')}
</div>
@ -520,22 +520,22 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
)}
{activeTab === 'history' && (
<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 dark:border-slate-700">
<h2 className="text-xl font-semibold text-slate-800 dark:text-slate-100 flex items-center">
<div className="bg-white rounded-xl shadow-sm border border-slate-200">
<div className="p-3 border-b border-slate-200">
<h2 className="text-xl font-semibold text-slate-800 flex items-center">
<FaClock className="w-5 h-5 mr-2 text-indigo-500" />
{translate('::App.Listforms.ImportManager.ImportHistory')}
</h2>
</div>
<div className="divide-y divide-slate-100 dark:divide-slate-800">
<div className="divide-y divide-slate-100">
{importHistory.map((session) => (
<div
key={session.id}
className={`p-2 transition-colors border-l-4 ${
currentSession?.id === session.id
? '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 dark:hover:bg-slate-800/70'
? 'bg-blue-50 border-l-blue-500 hover:bg-blue-100'
: 'border-l-transparent hover:bg-slate-50'
}`}
>
<div className="flex items-center justify-between">
@ -543,15 +543,13 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
{getStatusIcon(session.status)}
<div className="flex items-center space-x-2">
<div>
<div className="font-medium text-slate-800 dark:text-slate-100">
{session.blobName}
</div>
<div className="text-sm text-slate-500 dark:text-slate-400">
<div className="font-medium text-slate-800">{session.blobName}</div>
<div className="text-sm text-slate-500">
{new Date(session.creationTime).toLocaleString()}
</div>
</div>
{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 dark:bg-blue-950/40 dark:text-blue-300">
<span className="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
{translate('::App.Status.Active')}
</span>
)}
@ -560,7 +558,7 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
<div className="flex items-center space-x-4">
<div className="text-right">
<div className="text-sm font-medium text-slate-800 dark:text-slate-100">
<div className="text-sm font-medium text-slate-800">
{session.totalRows} {translate('::App.Listforms.ImportManager.TotalRows')}
</div>
</div>
@ -578,8 +576,8 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
onClick={() => toggleSessionExecutes(session.id)}
className={`p-2 rounded-lg transition-colors ${
expandedSessions.has(session.id)
? '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 dark:text-slate-500 dark:hover:text-slate-300 dark:hover:bg-slate-800'
? 'text-red-500 bg-red-50 hover:text-red-600 hover:bg-red-100'
: 'text-slate-400 hover:text-slate-600 hover:bg-slate-100'
}`}
title={translate('::App.Listforms.ImportManager.ViewExecutionDetails')}
>
@ -609,7 +607,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 dark:text-slate-500 dark:hover:text-blue-300 dark:hover:bg-blue-950/30"
className="p-2 rounded-lg transition-colors text-slate-400 hover:text-blue-500 hover:bg-blue-50"
title={translate('::App.Listforms.ImportManager.RefreshExecutionDetails')}
>
<FaSync className="w-4 h-4" />
@ -625,8 +623,8 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
disabled={currentSession?.id === session.id}
className={`p-2 rounded-lg transition-colors ${
currentSession?.id === session.id
? 'text-slate-300 dark:text-slate-600 cursor-not-allowed'
: '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'
? 'text-slate-300 cursor-not-allowed'
: 'text-slate-400 hover:text-red-500 hover:bg-red-50'
}`}
title={
currentSession?.id === session.id
@ -647,10 +645,10 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
session.status === 'executed_with_errors' ||
session.status === 'failed' ||
session.status === 'execute_failed'
? 'bg-red-50 text-red-700 border border-red-200 dark:bg-red-950/30 dark:text-red-300 dark:border-red-900/60'
? 'bg-red-50 text-red-700 border border-red-200'
: session.status === 'uploaded'
? '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 dark:bg-green-950/30 dark:text-green-300 dark:border-green-900/60'
? 'bg-blue-50 text-blue-700 border border-blue-200'
: 'bg-green-50 text-green-700 border border-green-200'
}`}
>
<span className="mt-0.5 flex-shrink-0">
@ -670,10 +668,10 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
{/* Execute Details Section */}
{expandedSessions.has(session.id) && (
<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="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="p-3">
{loadingExecutes.has(session.id) ? (
<div className="flex items-center space-x-2 text-slate-500 dark:text-slate-400 py-2">
<div className="flex items-center space-x-2 text-slate-500 py-2">
<FaSync className="w-4 h-4 animate-spin" />
<span className="text-sm">
{translate('::App.Listforms.ImportManager.LoadingExecutionDetails')}
@ -683,38 +681,38 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
sessionExecutes[session.id].length > 0 ? (
<div className="space-y-2">
{sessionExecutes[session.id].map((execute) => (
<div key={execute.id} className="p-3 rounded-lg dark:bg-slate-900/40">
<div key={execute.id} className="p-3 rounded-lg">
<div className="flex items-center justify-between">
{/* Sol: Tarih */}
<div className="flex-shrink-0">
<div className="text-lg text-slate-500 dark:text-slate-400">
<div className="text-lg text-slate-500">
{new Date(execute.creationTime).toLocaleString()}
</div>
</div>
{/* Orta: Executed, Valid, Errors */}
<div className="flex items-center space-x-4 text-xs text-slate-600 dark:text-slate-400">
<div className="flex items-center space-x-4 text-xs text-slate-600">
<div className="text-center">
<div className="font-medium text-slate-800 dark:text-slate-100">
<div className="font-medium text-slate-800">
{execute.execRows}
</div>
<div className="text-slate-500 dark:text-slate-400">
<div className="text-slate-500">
{translate('::App.Listforms.ImportManager.Executed')}
</div>
</div>
<div className="text-center">
<div className="font-medium text-green-600 dark:text-green-300">
<div className="font-medium text-green-600">
{execute.validRows}
</div>
<div className="text-slate-500 dark:text-slate-400">
<div className="text-slate-500">
{translate('::App.Listforms.ImportManager.Valid')}
</div>
</div>
<div className="text-center">
<div className="font-medium text-red-600 dark:text-red-300">
<div className="font-medium text-red-600">
{execute.errorRows}
</div>
<div className="text-slate-500 dark:text-slate-400">
<div className="text-slate-500">
{translate('::App.Listforms.ImportManager.Errors')}
</div>
</div>
@ -739,7 +737,7 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
<div className="mt-2">
<button
onClick={() => toggleErrors(execute.id)}
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"
className="flex items-center space-x-1 text-xs text-orange-600 hover:text-orange-700 font-medium"
>
{expandedErrors.has(execute.id) ? (
<FaChevronUp className="w-3 h-3" />
@ -753,29 +751,26 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
</button>
{expandedErrors.has(execute.id) && (
<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">
<div className="mt-2 max-h-48 overflow-y-auto rounded border border-orange-200 bg-orange-50">
{parseErrors(execute.errorsJson).length > 0 ? (
<table className="w-full text-xs">
<thead className="bg-orange-100 dark:bg-orange-950/50 sticky top-0">
<thead className="bg-orange-100 sticky top-0">
<tr>
<th className="px-3 py-1 text-left font-medium text-orange-700 dark:text-orange-300 w-16">
<th className="px-3 py-1 text-left font-medium text-orange-700 w-16">
Satır
</th>
<th className="px-3 py-1 text-left font-medium text-orange-700 dark:text-orange-300">
<th className="px-3 py-1 text-left font-medium text-orange-700">
Hata Mesajı
</th>
</tr>
</thead>
<tbody className="divide-y divide-orange-100 dark:divide-orange-900/50">
<tbody className="divide-y divide-orange-100">
{parseErrors(execute.errorsJson).map((err, idx) => (
<tr
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">
<tr key={idx} className="hover:bg-orange-100">
<td className="px-3 py-1 text-orange-700 font-medium">
{err.row}
</td>
<td className="px-3 py-1 text-slate-700 dark:text-slate-200">
<td className="px-3 py-1 text-slate-700">
{err.message}
</td>
</tr>
@ -783,7 +778,7 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
</tbody>
</table>
) : (
<p className="px-3 py-2 text-orange-600 dark:text-orange-300">
<p className="px-3 py-2 text-orange-600">
Hata detayı mevcut değil.
</p>
)}
@ -795,7 +790,7 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
))}
</div>
) : (
<div className="text-sm text-slate-500 dark:text-slate-400 py-2">
<div className="text-sm text-slate-500 py-2">
{translate('::App.Listforms.ImportManager.NoExecutionRecords')}
</div>
)}
@ -806,8 +801,8 @@ export const ImportDashboard: React.FC<ImportDashboardProps> = ({ gridDto }) =>
))}
{importHistory.length === 0 && (
<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 dark:text-slate-600" />
<div className="p-12 text-center text-slate-500">
<FaClock className="w-12 h-12 mx-auto mb-4 text-slate-300" />
<div className="text-lg font-medium mb-2">
{translate('::App.Listforms.ImportManager.NoImportHistory')}
</div>

View file

@ -100,13 +100,13 @@ export const ImportPreview: React.FC<ImportPreviewProps> = ({
const getStatusColor = (status: string) => {
switch (status) {
case 'uploaded':
return 'text-green-600 bg-green-50 border-green-200 dark:text-green-300 dark:bg-green-950/30 dark:border-green-900/60'
return 'text-green-600 bg-green-50 border-green-200'
case 'failed':
return 'text-red-600 bg-red-50 border-red-200 dark:text-red-300 dark:bg-red-950/30 dark:border-red-900/60'
return 'text-red-600 bg-red-50 border-red-200'
case 'validating':
return 'text-yellow-600 bg-yellow-50 border-yellow-200 dark:text-yellow-300 dark:bg-yellow-950/30 dark:border-yellow-900/60'
return 'text-yellow-600 bg-yellow-50 border-yellow-200'
default:
return 'text-blue-600 bg-blue-50 border-blue-200 dark:text-blue-300 dark:bg-blue-950/30 dark:border-blue-900/60'
return 'text-blue-600 bg-blue-50 border-blue-200'
}
}
@ -145,14 +145,14 @@ export const ImportPreview: React.FC<ImportPreviewProps> = ({
}
return (
<div className="bg-white dark:bg-slate-900 rounded-xl shadow-sm border border-slate-200 dark:border-slate-700">
<div className="bg-white rounded-xl shadow-sm border border-slate-200">
{/* Header */}
<div className="p-3 border-b border-slate-200 dark:border-slate-700">
<div className="p-3 border-b border-slate-200">
<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 */}
<div className="flex items-center order-1 lg:order-none">
<FaEye className="w-5 h-5 mr-2 text-blue-500" />
<h3 className="text-xl font-semibold text-slate-800 dark:text-slate-100">
<h3 className="text-xl font-semibold text-slate-800">
{translate('::App.Listforms.ImportManager.ImportPreviewTitle')}
</h3>
</div>
@ -160,9 +160,9 @@ export const ImportPreview: React.FC<ImportPreviewProps> = ({
{/* İ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="flex flex-col sm:flex-row justify-center gap-2">
<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">
<div className="text-center px-3 py-1 bg-blue-50 rounded-full border border-blue-200 font-bold text-blue-600">
{previewData?.rows?.length || session.totalRows || 0}{' '}
<span className="text-xs text-blue-700 dark:text-blue-300">
<span className="text-xs text-blue-700">
{translate('::App.Listforms.ImportManager.TotalRows')}
</span>
</div>
@ -184,16 +184,16 @@ export const ImportPreview: React.FC<ImportPreviewProps> = ({
{/* Preview Data */}
{previewData && previewData.headers && previewData.headers.length > 0 ? (
<div className="p-3 border-b border-slate-200 dark:border-slate-700">
<h4 className="font-semibold text-slate-800 dark:text-slate-100 mb-4">
<div className="p-3 border-b border-slate-200">
<h4 className="font-semibold text-slate-800 mb-4">
{translate('::App.Listforms.ImportManager.DataPreviewTitle')}
</h4>
<div className="overflow-auto border border-slate-200 dark:border-slate-700 rounded-lg max-h-90">
<div className="overflow-auto border border-slate-200 rounded-lg max-h-90">
<table className="w-full text-sm min-w-full">
<thead className="bg-slate-50 dark:bg-slate-800 sticky top-0 z-10">
<thead className="bg-slate-50 sticky top-0 z-10">
<tr>
<th className="px-4 py-2 text-left font-medium text-slate-700 dark:text-slate-200 whitespace-nowrap w-12">
<th className="px-4 py-2 text-left font-medium text-slate-700 whitespace-nowrap w-12">
<input
type="checkbox"
checked={selectAll}
@ -204,19 +204,19 @@ export const ImportPreview: React.FC<ImportPreviewProps> = ({
{previewData.headers.map((header: string, index: number) => (
<th
key={index}
className="px-4 py-2 text-left font-medium text-slate-700 dark:text-slate-200 whitespace-nowrap"
className="px-4 py-2 text-left font-medium text-slate-700 whitespace-nowrap"
>
{header}
</th>
))}
</tr>
</thead>
<tbody className="divide-y divide-slate-100 dark:divide-slate-800">
<tbody className="divide-y divide-slate-100">
{previewData.rows.map((row: any[], rowIndex: number) => (
<tr
key={rowIndex}
className={`hover:bg-slate-50 dark:hover:bg-slate-800/70 ${
selectedRows.includes(rowIndex) ? 'bg-blue-50 dark:bg-blue-950/30' : ''
className={`hover:bg-slate-50 ${
selectedRows.includes(rowIndex) ? 'bg-blue-50' : ''
}`}
>
<td className="px-4 py-2">
@ -230,7 +230,7 @@ export const ImportPreview: React.FC<ImportPreviewProps> = ({
{row.map((cell, cellIndex) => (
<td
key={cellIndex}
className="px-4 py-2 text-slate-600 dark:text-slate-300 whitespace-nowrap max-w-xs truncate"
className="px-4 py-2 text-slate-600 whitespace-nowrap max-w-xs truncate"
>
{cell?.toString() || '-'}
</td>
@ -242,25 +242,25 @@ export const ImportPreview: React.FC<ImportPreviewProps> = ({
</div>
</div>
) : previewData && previewData.headers && previewData.headers.length === 0 ? (
<div className="p-6 border-b border-slate-200 dark:border-slate-700">
<div className="p-6 border-b border-slate-200">
<div className="text-center py-8">
<FaExclamationTriangle className="w-12 h-12 mx-auto mb-4 text-yellow-500" />
<h4 className="font-semibold text-slate-800 dark:text-slate-100 mb-2">
<h4 className="font-semibold text-slate-800 mb-2">
{translate('::App.Listforms.ImportManager.NoDataFoundTitle')}
</h4>
<p className="text-slate-600 dark:text-slate-400">
<p className="text-slate-600">
{translate('::App.Listforms.ImportManager.NoDataFoundDescription')}
</p>
</div>
</div>
) : (
<div className="p-6 border-b border-slate-200 dark:border-slate-700">
<div className="p-6 border-b border-slate-200">
<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>
<h4 className="font-semibold text-slate-800 dark:text-slate-100 mb-2">
<h4 className="font-semibold text-slate-800 mb-2">
{translate('::App.Listforms.ImportManager.LoadingPreviewTitle')}
</h4>
<p className="text-slate-600 dark:text-slate-400">
<p className="text-slate-600">
{translate('::App.Listforms.ImportManager.LoadingPreviewDescription')}
</p>
</div>
@ -271,8 +271,8 @@ export const ImportPreview: React.FC<ImportPreviewProps> = ({
<div className="p-3">
{/* Success Message */}
{showSuccessMessage && lastExecutionResult && (
<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 dark:text-green-300">
<div className="mb-4 p-4 bg-green-50 border border-green-200 rounded-lg">
<div className="flex items-center space-x-2 text-green-700">
<FaCheckCircle className="w-5 h-5" />
<span className="font-medium">
{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 space-x-4">
{selectedRows.length === 0 && (previewData?.rows?.length || 0) > 0 && (
<div className="flex items-center space-x-2 text-orange-600 dark:text-orange-300">
<div className="flex items-center space-x-2 text-orange-600">
<FaExclamationTriangle className="w-5 h-5" />
<span className="font-medium">
{translate('::App.Listforms.ImportManager.SelectRowsWarning')}
@ -295,7 +295,7 @@ export const ImportPreview: React.FC<ImportPreviewProps> = ({
)}
{selectedRows.length > 0 && (
<div className="flex items-center space-x-2 text-blue-600 dark:text-blue-300">
<div className="flex items-center space-x-2 text-blue-600">
<FaCheckCircle className="w-5 h-5" />
<span className="font-medium">
{selectedRows.length} {translate('::App.Listforms.ImportManager.RowsSelected')}
@ -304,7 +304,7 @@ export const ImportPreview: React.FC<ImportPreviewProps> = ({
)}
{(previewData?.rows?.length || 0) === 0 && (
<div className="flex items-center space-x-2 text-red-600 dark:text-red-300">
<div className="flex items-center space-x-2 text-red-600">
<FaExclamationTriangle className="w-5 h-5" />
<span className="font-medium">
{translate('::App.Listforms.ImportManager.NoRowsAvailable')}
@ -314,7 +314,7 @@ export const ImportPreview: React.FC<ImportPreviewProps> = ({
</div>
<div className="flex space-x-3">
<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">
<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">
<FaTimes className="w-4 h-4" />
<span>{translate('::Cancel')}</span>
</button>

View file

@ -59,17 +59,15 @@ export const ImportProgress: React.FC<ImportProgressProps> = ({ session }) => {
}
return (
<div className="bg-white dark:bg-slate-900 rounded-xl shadow-sm border border-slate-200 dark:border-slate-700 p-6">
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<div className="text-center space-y-6">
{/* Status Icon */}
<div className="flex justify-center">{getStatusIcon()}</div>
{/* Status Message */}
<div>
<h3 className="text-xl font-semibold text-slate-800 dark:text-slate-100 mb-2">
{getStatusMessage()}
</h3>
<p className="text-slate-600 dark:text-slate-400">
<h3 className="text-xl font-semibold text-slate-800 mb-2">{getStatusMessage()}</h3>
<p className="text-slate-600">
{translate('::App.Listforms.Status.Processing')}{' '}
{session.blobName}
</p>
@ -77,11 +75,11 @@ export const ImportProgress: React.FC<ImportProgressProps> = ({ session }) => {
{/* Progress Bar */}
<div className="w-full max-w-md mx-auto">
<div className="flex justify-between text-sm text-slate-600 dark:text-slate-400 mb-2">
<div className="flex justify-between text-sm text-slate-600 mb-2">
<span>{translate('::App.Listforms.ImportManager.ImportProgress.ProgressLabel')}</span>
<span>{Math.round(getProgressPercentage())}%</span>
</div>
<div className="w-full bg-slate-200 dark:bg-slate-700 rounded-full h-2">
<div className="w-full bg-slate-200 rounded-full h-2">
<div
className="bg-blue-500 h-2 rounded-full transition-all duration-300 ease-out"
style={{ width: `${getProgressPercentage()}%` }}

View file

@ -17,19 +17,6 @@ const DialogContext = createContext<DialogContextValue>({ isMaximized: false })
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 = ({
children,
className,
@ -37,8 +24,14 @@ const DialogBody = ({
children?: ReactNode
className?: string
}) => {
const { isMaximized } = useContext(DialogContext)
return (
<div className={classNames('dialog-body', className)}>
<div
className={classNames(
isMaximized && 'flex-1 min-h-0 flex flex-col overflow-hidden',
className,
)}
>
{children}
</div>
)
@ -52,8 +45,9 @@ const DialogFooter = ({
children?: ReactNode
className?: string
}) => {
const { isMaximized } = useContext(DialogContext)
return (
<div className={classNames('dialog-footer', className)}>
<div className={classNames(isMaximized && 'flex-shrink-0', className)}>
{children}
</div>
)
@ -237,7 +231,13 @@ const Dialog = (props: DialogProps) => {
{closable && !showWindowControls && renderCloseButton}
{closable && showWindowControls && renderWindowControls}
<DialogContext.Provider value={{ isMaximized }}>
{children}
{isMaximized ? (
<div className="flex-1 min-h-0 flex flex-col overflow-hidden">
{children}
</div>
) : (
children
)}
</DialogContext.Provider>
</motion.div>
</Modal>
@ -247,13 +247,11 @@ const Dialog = (props: DialogProps) => {
Dialog.displayName = 'Dialog'
type DialogType = typeof Dialog & {
Header: typeof DialogHeader
Body: typeof DialogBody
Footer: typeof DialogFooter
}
const DialogWithSubComponents = Dialog as DialogType
DialogWithSubComponents.Header = DialogHeader
DialogWithSubComponents.Body = DialogBody
DialogWithSubComponents.Footer = DialogFooter

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -2,6 +2,7 @@
import classNames from 'classnames'
import { createPortal } from 'react-dom'
import { Button, Dialog, Notification, toast, Checkbox } from '@/components/ui'
import { useDialogContext } from '@/components/ui/Dialog/Dialog'
import {
FaPlus,
FaTrash,
@ -41,7 +42,6 @@ type SqlDataType =
| 'decimal'
| 'float'
| 'bit'
| 'datetime'
| 'datetime2'
| 'date'
| 'uniqueidentifier'
@ -68,7 +68,7 @@ interface TableDesignerDialogProps {
isOpen: boolean
onClose: () => void
dataSource: string | null
onDeployed?: (table: { schemaName: string; tableName: string }) => void | Promise<void>
onDeployed?: () => void
initialTableData?: { schemaName: string; tableName: string } | null
}
@ -98,7 +98,6 @@ const DATA_TYPES: { value: SqlDataType; label: string }[] = [
{ value: 'decimal', label: 'Decimal (decimal 18,4)' },
{ value: 'float', label: 'Float (float)' },
{ value: 'bit', label: 'Bool (bit)' },
{ value: 'datetime', label: 'DateTime (datetime)' },
{ value: 'datetime2', label: 'DateTime (datetime2)' },
{ value: 'date', label: 'Date (date)' },
{ value: 'uniqueidentifier', label: 'Guid (uniqueidentifier)' },
@ -227,45 +226,6 @@ const TENANT_COLUMN: ColumnDefinition = {
description: 'Tenant ID for multi-tenancy',
}
const WORKFLOW_COLUMNS: ColumnDefinition[] = [
{
id: '__ApprovalUserId',
columnName: 'ApprovalUserName',
dataType: 'nvarchar',
maxLength: '256',
isNullable: true,
defaultValue: '',
description: 'Workflow approval user name',
},
{
id: '__ApprovalStatus',
columnName: 'ApprovalStatus',
dataType: 'nvarchar',
maxLength: '50',
isNullable: true,
defaultValue: '',
description: 'Workflow approval status',
},
{
id: '__ApprovalDate',
columnName: 'ApprovalDate',
dataType: 'datetime',
maxLength: '',
isNullable: true,
defaultValue: '',
description: 'Workflow approval date',
},
{
id: '__ApprovalDescription',
columnName: 'ApprovalDescription',
dataType: 'nvarchar',
maxLength: '200',
isNullable: true,
defaultValue: '',
description: 'Workflow approval description',
},
]
const CREATE_TABLE_SCRIPT_STORAGE_KEY = 'sqlQueryManager.lastCreateTableScript'
// ─── T-SQL Generator ──────────────────────────────────────────────────────────
@ -445,8 +405,6 @@ function dbColToColumnDef(col: {
dataType = 'float'
} else if (dt === 'bit') {
dataType = 'bit'
} else if (dt === 'datetime') {
dataType = 'datetime'
} else if (dt.startsWith('datetime') || dt === 'smalldatetime') {
dataType = 'datetime2'
} else if (dt === 'date') {
@ -518,7 +476,6 @@ function mapSqlTypeToDesigner(dataTypeRaw: string, lengthRaw: string): {
if (dt === 'decimal' || dt === 'numeric') return { dataType: 'decimal', maxLength: '' }
if (dt === 'float' || dt === 'real') return { dataType: 'float', maxLength: '' }
if (dt === 'bit') return { dataType: 'bit', maxLength: '' }
if (dt === 'datetime') return { dataType: 'datetime', maxLength: '' }
if (dt.startsWith('datetime') || dt === 'smalldatetime' || dt === 'time') {
return { dataType: 'datetime2', maxLength: '' }
}
@ -834,8 +791,9 @@ const STEPS = ['Sütun Tasarımı', 'Entity Ayarları', 'Index / Key', 'İlişki
type Step = 0 | 1 | 2 | 3 | 4
function StepContentWrapper({ children }: { children: React.ReactNode }) {
const { isMaximized } = useDialogContext()
return (
<div className="min-h-[420px] flex flex-col">
<div className={isMaximized ? 'flex-1 min-h-0 overflow-auto' : 'min-h-[420px]'}>
{children}
</div>
)
@ -1266,18 +1224,6 @@ const SqlTableDesignerDialog = ({
}
}
const addWorkflowColumns = () => {
const existingNames = new Set(columns.map((c) => c.columnName.trim().toLowerCase()))
const toAdd = WORKFLOW_COLUMNS.filter((c) => !existingNames.has(c.columnName.toLowerCase()))
if (toAdd.length > 0) {
setColumns((prev) => {
const nonEmpty = prev.filter((c) => c.columnName.trim() !== '')
return [...nonEmpty, ...toAdd.map((c) => ({ ...c })), createEmptyColumn()]
})
}
}
const importColumnsFromRememberedCreateTable = async () => {
let script = ''
@ -1645,10 +1591,7 @@ const SqlTableDesignerDialog = ({
} catch {
// Non-blocking: seed file save failure does not affect deploy success
}
await onDeployed?.({
schemaName: initialTableData?.schemaName || 'dbo',
tableName: deployedTable,
})
onDeployed?.()
handleClose()
} else {
toast.push(
@ -1747,9 +1690,6 @@ const SqlTableDesignerDialog = ({
<Button size="xs" variant="solid" color="green-600" onClick={addMultiTenantColumns}>
{translate('::App.SqlQueryManager.AddMultiTenantColumns')}
</Button>
<Button size="xs" variant="solid" color="indigo-600" onClick={addWorkflowColumns}>
{translate('::App.SqlQueryManager.AddWorkflowColumns')}
</Button>
<Button
size="xs"
variant="solid"
@ -1808,7 +1748,7 @@ const SqlTableDesignerDialog = ({
</div>
)}
<div className="flex flex-col gap-1 pr-1">
<div className="flex flex-col gap-1 max-h-80 overflow-y-auto pr-1">
{columns.map((col, idx) => {
const isDuplicate =
col.columnName.trim() !== '' &&
@ -2003,14 +1943,7 @@ const SqlTableDesignerDialog = ({
initialParentCode={selectedMenuCode}
initialOrder={999}
rawItems={rawMenuItems}
onSaved={(menu) =>
reloadMenus((items) => {
const savedMenu = items.find((item) => item.code === menu.code)
if (savedMenu?.code && savedMenu.shortName) {
onMenuCodeSelect(savedMenu.code)
}
})
}
onSaved={() => reloadMenus()}
/>
</div>
@ -2738,8 +2671,8 @@ const SqlTableDesignerDialog = ({
// ── Render ─────────────────────────────────────────────────────────────────
return (
<Dialog isOpen={isOpen} onClose={handleClose} onRequestClose={handleClose} width={1100}>
<Dialog.Header className="flex flex-col gap-2">
<Dialog isOpen={isOpen} onClose={handleClose} onRequestClose={handleClose} width={900}>
<Dialog.Body className="flex flex-col gap-2">
{/* Header */}
<div className="flex items-center gap-3 border-b pb-3 flex-shrink-0">
<FaTable className="text-2xl text-blue-500" />
@ -2759,9 +2692,7 @@ const SqlTableDesignerDialog = ({
{/* Steps */}
<div className="flex-shrink-0">{renderStepIndicator()}</div>
</Dialog.Header>
<Dialog.Body className="flex flex-col gap-2">
{/* Content */}
<StepContentWrapper>
{step === 0 && renderColumnDesigner()}

View file

@ -140,9 +140,7 @@ const getValueByField = (data: Record<string, any> = {}, field?: string) => {
}
const shouldRunEditorScriptOnContentReady = (script?: string) =>
Boolean(
script && (script.includes('setEditorReadOnly') || script.includes('runtimeSetEditorReadOnly')),
)
Boolean(script && (script.includes('setEditorReadOnly') || script.includes('runtimeSetEditorReadOnly')))
const FormDevExpress = (props: {
listFormCode: string
@ -160,15 +158,9 @@ const FormDevExpress = (props: {
const formItemsRef = useRef(formItems)
const formInstanceRef = useRef<any>()
const lastContentReadyScriptKeyRef = useRef<string>()
const didAutoFocusRef = useRef(false)
const [runtimeReadOnlyFields, setRuntimeReadOnlyFields] = useState<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(() => {
formDataRef.current = formData
}, [formData])
@ -181,10 +173,6 @@ const FormDevExpress = (props: {
runtimeReadOnlyFieldsRef.current = runtimeReadOnlyFields
}, [runtimeReadOnlyFields])
useEffect(() => {
didAutoFocusRef.current = false
}, [listFormCode, mode])
const setRuntimeEditorReadOnly = (field: string, readOnly: boolean) => {
const resolvedField = findFormFieldKey(formItemsRef.current, field)
const key = String(resolvedField || field || '').toLowerCase()
@ -214,7 +202,11 @@ const FormDevExpress = (props: {
setTimeout(() => setFormEditorReadOnly(formInstanceRef.current ?? form, field, readOnly), 0)
}
const runEditorScript = (formItem: SimpleItemWithColData, eventValue: any, component?: any) => {
const runEditorScript = (
formItem: SimpleItemWithColData,
eventValue: any,
component?: any,
) => {
if (!formItem?.editorScript) {
return
}
@ -258,9 +250,7 @@ const FormDevExpress = (props: {
const prevOnValueChanged = formItem.editorOptions?.onValueChanged
return {
...(index !== undefined && mode !== 'view' && !isTouchLikeDevice()
? { autoFocus: index === 1 }
: {}),
...(index !== undefined && mode !== 'view' ? { autoFocus: index === 1 } : {}),
...(formItem.editorType === 'dxDateBox'
? {
useMaskBehavior: true,
@ -476,6 +466,7 @@ const FormDevExpress = (props: {
setTimeout(() => {
updateCascadeDisabledStates()
}, 0)
}}
onContentReady={(e) => {
formInstanceRef.current = e.component
@ -487,8 +478,7 @@ const FormDevExpress = (props: {
const groupItems = e.component.option('items') as any[]
const firstItem = groupItems?.[0]?.items?.[0]
if (!didAutoFocusRef.current && firstItem?.dataField && !isTouchLikeDevice()) {
didAutoFocusRef.current = true
if (firstItem?.dataField) {
const editor = e.component.getEditor(firstItem.dataField)
mode !== 'view' && editor?.focus()
}
@ -502,100 +492,98 @@ const FormDevExpress = (props: {
colSpan={formGroupItem.colSpan}
caption={formGroupItem.caption}
>
{(formGroupItem.items as SimpleItemWithColData[])
?.filter((formItem) => {
if (mode === 'edit') return formItem.allowEditing !== false
if (mode === 'new') return formItem.allowAdding !== false
return true
})
.map((formItem, i) => {
return formItem.editorType2 === PlatformEditorTypes.dxTagBox ? (
<SimpleItemDx
cssClass="font-semibold"
key={getFormItemKey(formItem, i)}
{...formItem}
render={() => (
<TagBoxEditorComponent
value={formData[formItem.dataField!] || []}
setDefaultValue={false}
values={formData}
options={formItem.tagBoxOptions}
col={formItem.colData}
onValueChanged={(e: any) => {
const newData = { ...formDataRef.current, [formItem.dataField!]: e }
formDataRef.current = newData
setFormData(newData)
runEditorScript(formItem, e, formInstanceRef.current)
}}
editorOptions={getEditorOptions(formItem)}
></TagBoxEditorComponent>
)}
label={{
text: translate('::' + formItem.colData?.captionName),
className: 'font-semibold',
}}
></SimpleItemDx>
) : formItem.editorType2 === PlatformEditorTypes.dxGridBox ? (
<SimpleItemDx
cssClass="font-semibold"
key={getFormItemKey(formItem, i)}
{...formItem}
render={() => (
<GridBoxEditorComponent
value={formData[formItem.dataField!] || []}
values={formData}
options={formItem.gridBoxOptions}
col={formItem.colData}
onValueChanged={(e: any) => {
const newData = { ...formDataRef.current, [formItem.dataField!]: e }
formDataRef.current = newData
setFormData(newData)
runEditorScript(formItem, e, formInstanceRef.current)
}}
editorOptions={getEditorOptions(formItem)}
></GridBoxEditorComponent>
)}
label={{
text: translate('::' + formItem.colData?.captionName),
className: 'font-semibold',
}}
></SimpleItemDx>
) : formItem.editorType2 === PlatformEditorTypes.dxImageUpload ? (
<SimpleItemDx
cssClass="font-semibold"
key={getFormItemKey(formItem, i)}
dataField={formItem.dataField}
name={formItem.name}
colSpan={formItem.colSpan}
isRequired={formItem.isRequired}
render={() => (
<ImageUploadEditorComponent
value={formData[formItem.dataField!]}
options={formItem.imageUploadOptions}
onValueChanged={(val: any) => {
const newData = { ...formDataRef.current, [formItem.dataField!]: val }
formDataRef.current = newData
setFormData(newData)
runEditorScript(formItem, val, formInstanceRef.current)
}}
editorOptions={getEditorOptions(formItem)}
/>
)}
label={{
text: translate('::' + formItem.colData?.captionName),
className: 'font-semibold',
}}
></SimpleItemDx>
) : (
<SimpleItemDx
cssClass="font-semibold"
key={getFormItemKey(formItem, i)}
{...formItem}
editorOptions={getEditorOptions(formItem, i)}
label={{ text: translate('::' + formItem.colData?.captionName) }}
/>
)
})}
{(formGroupItem.items as SimpleItemWithColData[])?.filter((formItem) => {
if (mode === 'edit') return formItem.allowEditing !== false
if (mode === 'new') return formItem.allowAdding !== false
return true
}).map((formItem, i) => {
return formItem.editorType2 === PlatformEditorTypes.dxTagBox ? (
<SimpleItemDx
cssClass="font-semibold"
key={getFormItemKey(formItem, i)}
{...formItem}
render={() => (
<TagBoxEditorComponent
value={formData[formItem.dataField!] || []}
setDefaultValue={false}
values={formData}
options={formItem.tagBoxOptions}
col={formItem.colData}
onValueChanged={(e: any) => {
const newData = { ...formDataRef.current, [formItem.dataField!]: e }
formDataRef.current = newData
setFormData(newData)
runEditorScript(formItem, e, formInstanceRef.current)
}}
editorOptions={getEditorOptions(formItem)}
></TagBoxEditorComponent>
)}
label={{
text: translate('::' + formItem.colData?.captionName),
className: 'font-semibold',
}}
></SimpleItemDx>
) : formItem.editorType2 === PlatformEditorTypes.dxGridBox ? (
<SimpleItemDx
cssClass="font-semibold"
key={getFormItemKey(formItem, i)}
{...formItem}
render={() => (
<GridBoxEditorComponent
value={formData[formItem.dataField!] || []}
values={formData}
options={formItem.gridBoxOptions}
col={formItem.colData}
onValueChanged={(e: any) => {
const newData = { ...formDataRef.current, [formItem.dataField!]: e }
formDataRef.current = newData
setFormData(newData)
runEditorScript(formItem, e, formInstanceRef.current)
}}
editorOptions={getEditorOptions(formItem)}
></GridBoxEditorComponent>
)}
label={{
text: translate('::' + formItem.colData?.captionName),
className: 'font-semibold',
}}
></SimpleItemDx>
) : formItem.editorType2 === PlatformEditorTypes.dxImageUpload ? (
<SimpleItemDx
cssClass="font-semibold"
key={getFormItemKey(formItem, i)}
dataField={formItem.dataField}
name={formItem.name}
colSpan={formItem.colSpan}
isRequired={formItem.isRequired}
render={() => (
<ImageUploadEditorComponent
value={formData[formItem.dataField!]}
options={formItem.imageUploadOptions}
onValueChanged={(val: any) => {
const newData = { ...formDataRef.current, [formItem.dataField!]: val }
formDataRef.current = newData
setFormData(newData)
runEditorScript(formItem, val, formInstanceRef.current)
}}
editorOptions={getEditorOptions(formItem)}
/>
)}
label={{
text: translate('::' + formItem.colData?.captionName),
className: 'font-semibold',
}}
></SimpleItemDx>
) : (
<SimpleItemDx
cssClass="font-semibold"
key={getFormItemKey(formItem, i)}
{...formItem}
editorOptions={getEditorOptions(formItem, i)}
label={{ text: translate('::' + formItem.colData?.captionName) }}
/>
)
})}
</GroupItemDx>
)
})}

View file

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

View file

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

View file

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

View file

@ -19,13 +19,6 @@ import { layoutTypes } from '../admin/listForm/edit/types'
import { useListFormCustomDataSource } from '../list/useListFormCustomDataSource'
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: {
mode: RowMode
listFormCode: string
@ -48,7 +41,6 @@ const useGridData = (props: {
const [permissionResults, setPermissionResults] = useState<PermissionResults>()
const refForm = useRef<FormRef>(null)
const previousFormDataRef = useRef<any>()
const [searchParams] = useSearchParams()
const navigate = useNavigate()
const { translate } = useLocalization()
@ -314,36 +306,18 @@ const useGridData = (props: {
setGridReady(true)
}, [gridDto])
// formData değiştiğinde sadece etkilenen cascading lookup datasource'ları güncelle
// formData değiştiğinde sadece lookup datasource'ları güncelle
useEffect(() => {
if (!gridDto || !formItems.length) {
previousFormDataRef.current = formData
return
}
const previousFormData = previousFormDataRef.current
const changedFields = previousFormData
? Object.keys({ ...(previousFormData || {}), ...(formData || {}) }).filter(
(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) => {
// View mode'da formData olsa da olmasa da cascading alanlar için dataSource oluşturulmalı
const updatedItems = formItems.map((groupItem) => ({
...groupItem,
items: (groupItem.items as SimpleItemWithColData[])?.map((item) => {
const colData = gridDto.columnFormats.find((x) => x.fieldName === item.dataField)
if (colData?.lookupDto?.dataSourceType && shouldRefreshLookup(item)) {
if (colData?.lookupDto?.dataSourceType) {
const currentDataSource = item.editorOptions?.dataSource
const keepCustomDataSource =
currentDataSource !== undefined && typeof currentDataSource?.load !== 'function'
@ -356,55 +330,16 @@ const useGridData = (props: {
dataSource: keepCustomDataSource
? currentDataSource
: getLookupDataSource(colData?.editorOptions, colData, formData || null),
valueExpr:
item.editorOptions?.valueExpr ?? colData?.lookupDto?.valueExpr?.toLowerCase(),
valueExpr: item.editorOptions?.valueExpr ?? colData?.lookupDto?.valueExpr?.toLowerCase(),
displayExpr:
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
})
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)
}, [formData, gridDto])

View file

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

View file

@ -77,7 +77,6 @@ import { useListFormColumns } from './useListFormColumns'
import { Loading } from '@/components/shared'
import { useStoreState } from '@/store'
import { workflowService } from '@/services/workflow.service'
import { NotePanel } from '../form/notes/NotePanel'
interface GridProps {
listFormCode: string
@ -239,22 +238,19 @@ const getValueByField = (data: Record<string, any> = {}, field?: string) => {
}
const shouldRunEditorScriptOnContentReady = (script?: string) =>
Boolean(
script && (script.includes('setEditorReadOnly') || script.includes('runtimeSetEditorReadOnly')),
)
Boolean(script && (script.includes('setEditorReadOnly') || script.includes('runtimeSetEditorReadOnly')))
const Grid = (props: GridProps) => {
const { listFormCode, searchParams, isSubForm, level, gridDto: extGridDto } = props
const { translate } = useLocalization()
const { smaller } = useResponsive()
const currentUser = useStoreState((state) => state.auth.user)
const config = useStoreState((state) => state.abpConfig.config)
const gridRef = useRef<DataGridRef>()
const refListFormCode = useRef('')
const widgetGroupRef = useRef<HTMLDivElement>(null)
const editingFormDataRef = useRef<Record<string, any>>({})
const editingFormInstanceRef = useRef<any>()
const lastEditingContentReadyScriptKeyRef = useRef<string>()
// Edit popup state kaydetmeyi engellemek için flag
const isEditingRef = useRef(false)
@ -266,10 +262,6 @@ const Grid = (props: GridProps) => {
const [gridDto, setGridDto] = useState<GridDto>()
const [isPopupFullScreen, setIsPopupFullScreen] = useState(false)
const [widgetGroupHeight, setWidgetGroupHeight] = useState(0)
const [notePanelTarget, setNotePanelTarget] = useState<{
entityName: string
entityId: string
} | null>(null)
type EditorOptionsWithButtons = {
buttons?: any[]
@ -346,29 +338,11 @@ const Grid = (props: GridProps) => {
})
const { createSelectDataSource } = useListFormCustomDataSource({ gridRef })
const openNotePanel = useCallback(
(rowData: Record<string, any>) => {
const keyFieldName = gridDto?.gridOptions.keyFieldName
const entityId = getValueByField(rowData, keyFieldName)
if (entityId === undefined || entityId === null || entityId === '') {
return
}
setNotePanelTarget({
entityName: gridDto?.gridOptions.listFormCode ?? listFormCode,
entityId: String(entityId),
})
},
[gridDto, listFormCode],
)
const { getBandedColumns } = useListFormColumns({
gridDto,
listFormCode,
isSubForm,
gridRef,
onShowNote: openNotePanel,
})
const getSelectedRowKeys = useCallback(async () => {
@ -400,10 +374,10 @@ const Grid = (props: GridProps) => {
grd,
gridDto?.gridOptions.workflowDto,
selectedRowsData ?? grd.getSelectedRowsData(),
currentUser,
config?.currentUser,
)
},
[currentUser, gridDto],
[config?.currentUser, gridDto],
)
const refreshData = useCallback(() => {
@ -1023,7 +997,7 @@ const Grid = (props: GridProps) => {
// Kolonları oluştur - dil değiştiğinde güncelle
const memoizedColumns = useMemo(() => {
if (!gridDto) return undefined
if (!gridDto || !config) return undefined
const cols = getBandedColumns()
@ -1064,7 +1038,7 @@ const Grid = (props: GridProps) => {
})
return cols
}, [gridDto])
}, [gridDto, config])
useEffect(() => {
setColumnData(memoizedColumns)
@ -1184,20 +1158,20 @@ const Grid = (props: GridProps) => {
if (listFormField?.sourceDbType === DbTypeEnum.Date) {
Object.assign(defaultEditorOptions, {
type: 'date',
dateSerializationFormat: 'yyyy-MM-dd',
displayFormat: 'shortDate',
})
type: 'date',
dateSerializationFormat: 'yyyy-MM-dd',
displayFormat: 'shortDate',
})
} else if (
listFormField?.sourceDbType === DbTypeEnum.DateTime ||
listFormField?.sourceDbType === DbTypeEnum.DateTime2 ||
listFormField?.sourceDbType === DbTypeEnum.DateTimeOffset
) {
Object.assign(defaultEditorOptions, {
type: 'datetime',
dateSerializationFormat: 'yyyy-MM-ddTHH:mm:ss',
displayFormat: 'shortDateShortTime',
})
type: 'datetime',
dateSerializationFormat: 'yyyy-MM-ddTHH:mm:ss',
displayFormat: 'shortDateShortTime',
})
}
// Her item'a placeholder olarak captionName ekle
@ -1477,27 +1451,7 @@ const Grid = (props: GridProps) => {
) {
workflowService
.startWorkflow(listFormCode, [insertedKey])
.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()
})
.then(() => gridRef.current?.instance()?.refresh())
.catch(console.error)
}
props.refreshData?.()
@ -1604,27 +1558,15 @@ const Grid = (props: GridProps) => {
}
const runReadOnlyScripts = () => {
const formItems = gridDto.gridOptions.editingFormDto.flatMap((group) =>
flattenEditingFormItems([group]),
)
const scriptItems = formItems.filter((formItem) =>
shouldRunEditorScriptOnContentReady(formItem.editorScript),
)
if (!scriptItems.length) {
return
}
const editorValues = formItems.reduce<Record<string, any>>(
(values, formItem) => {
const editorValues = gridDto.gridOptions.editingFormDto
.flatMap((group) => flattenEditingFormItems([group]))
.reduce<Record<string, any>>((values, formItem) => {
const editorInstance = form?.getEditor?.(formItem.dataField)
if (editorInstance?.option) {
values[formItem.dataField] = editorInstance.option('value')
}
return values
},
{},
)
}, {})
const formData = {
...editingFormDataRef.current,
...(form?.option?.('formData') || {}),
@ -1632,47 +1574,38 @@ const Grid = (props: GridProps) => {
}
editingFormDataRef.current = { ...formData }
const scriptKey = `${mode}|${String(rowKey)}|${scriptItems
.map((formItem) => formItem.dataField)
.join('|')}|${JSON.stringify(formData)}`
gridDto.gridOptions.editingFormDto
.flatMap((group) => flattenEditingFormItems([group]))
.filter((formItem) =>
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,
}
if (lastEditingContentReadyScriptKeyRef.current === scriptKey) {
return
}
lastEditingContentReadyScriptKeyRef.current = scriptKey
scriptItems.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,
executeEditorScript(formItem.editorScript!, {
formData,
e,
editor,
runtimeSetEditorReadOnly,
setFormData,
})
} catch (err) {
console.error('Script exec error on contentReady', formItem.dataField, err)
}
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()
@ -1728,6 +1661,7 @@ const Grid = (props: GridProps) => {
console.error('Script exec error', err)
}
}
}
},
items:
@ -1940,14 +1874,6 @@ const Grid = (props: GridProps) => {
{toolbarModalData?.content}
</Dialog>
<GridFilterDialogs gridRef={gridRef} listFormCode={listFormCode} {...filterData} />
{notePanelTarget && (
<NotePanel
entityName={notePanelTarget.entityName}
entityId={notePanelTarget.entityId}
isVisible
onToggle={() => setNotePanelTarget(null)}
/>
)}
</Container>
</>
)

View file

@ -68,7 +68,6 @@ import { useStoreState } from '@/store/store'
import SubForms from '../form/SubForms'
import { ImportDashboard } from '@/components/importManager/ImportDashboard'
import { workflowService } from '@/services/workflow.service'
import { NotePanel } from '../form/notes/NotePanel'
interface TreeProps {
listFormCode: string
@ -227,22 +226,18 @@ const getValueByField = (data: Record<string, any> = {}, field?: string) => {
}
const shouldRunEditorScriptOnContentReady = (script?: string) =>
Boolean(
script && (script.includes('setEditorReadOnly') || script.includes('runtimeSetEditorReadOnly')),
)
Boolean(script && (script.includes('setEditorReadOnly') || script.includes('runtimeSetEditorReadOnly')))
const Tree = (props: TreeProps) => {
const { listFormCode, searchParams, isSubForm, level, gridDto: extGridDto } = props
const { translate } = useLocalization()
const { smaller } = useResponsive()
const currentUser = useStoreState((state) => state.auth.user)
const gridRef = useRef<TreeListRef>()
const refListFormCode = useRef('')
const widgetGroupRef = useRef<HTMLDivElement>(null)
const editingFormDataRef = useRef<Record<string, any>>({})
const editingFormInstanceRef = useRef<any>()
const lastEditingContentReadyScriptKeyRef = useRef<string>()
// Edit popup state kaydetmeyi engellemek için flag
const isEditingRef = useRef(false)
@ -254,12 +249,9 @@ const Tree = (props: TreeProps) => {
const [gridDto, setGridDto] = useState<GridDto>()
const [isPopupFullScreen, setIsPopupFullScreen] = useState(false)
const [widgetGroupHeight, setWidgetGroupHeight] = useState(0)
const [notePanelTarget, setNotePanelTarget] = useState<{
entityName: string
entityId: string
} | null>(null)
const [expandedRowKeys, setExpandedRowKeys] = useState<any[]>([])
const config = useStoreState((state) => state.abpConfig.config)
type EditorOptionsWithButtons = {
buttons?: any[]
@ -351,29 +343,11 @@ const Tree = (props: TreeProps) => {
})
const { createSelectDataSource } = useListFormCustomDataSource({ gridRef })
const openNotePanel = useCallback(
(rowData: Record<string, any>) => {
const keyFieldName = gridDto?.gridOptions.keyFieldName
const entityId = getValueByField(rowData, keyFieldName)
if (entityId === undefined || entityId === null || entityId === '') {
return
}
setNotePanelTarget({
entityName: gridDto?.gridOptions.listFormCode ?? listFormCode,
entityId: String(entityId),
})
},
[gridDto, listFormCode],
)
const { getBandedColumns } = useListFormColumns({
gridDto,
listFormCode,
isSubForm,
gridRef,
onShowNote: openNotePanel,
})
const extractSearchParamsFields = useCallback((filter: any): [string, string, any][] => {
@ -435,10 +409,10 @@ const Tree = (props: TreeProps) => {
tree,
gridDto?.gridOptions.workflowDto,
selectedRowsData ?? tree.getSelectedRowsData(),
currentUser,
config?.currentUser,
)
},
[currentUser, gridDto],
[config?.currentUser, gridDto],
)
const refreshData = useCallback(() => {
@ -926,7 +900,7 @@ const Tree = (props: TreeProps) => {
}, [gridDto])
useEffect(() => {
if (!gridDto) return
if (!gridDto || !config) return
const cols = getBandedColumns()
setColumnData(cols)
@ -939,7 +913,7 @@ const Tree = (props: TreeProps) => {
cols,
)
setTreeListDataSource(dataSource)
}, [gridDto, searchParams])
}, [gridDto, searchParams, config])
useEffect(() => {
const activeFilters = extraFilters.filter((f) => f.value)
@ -1136,27 +1110,7 @@ const Tree = (props: TreeProps) => {
) {
workflowService
.startWorkflow(listFormCode, [insertedKey])
.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()
})
.then(() => gridRef.current?.instance()?.refresh())
.catch(console.error)
}
props.refreshData?.()
@ -1266,27 +1220,15 @@ const Tree = (props: TreeProps) => {
}
const runReadOnlyScripts = () => {
const formItems = gridDto.gridOptions.editingFormDto.flatMap((group) =>
flattenEditingFormItems([group]),
)
const scriptItems = formItems.filter((formItem) =>
shouldRunEditorScriptOnContentReady(formItem.editorScript),
)
if (!scriptItems.length) {
return
}
const editorValues = formItems.reduce<Record<string, any>>(
(values, formItem) => {
const editorValues = gridDto.gridOptions.editingFormDto
.flatMap((group) => flattenEditingFormItems([group]))
.reduce<Record<string, any>>((values, formItem) => {
const editorInstance = form?.getEditor?.(formItem.dataField)
if (editorInstance?.option) {
values[formItem.dataField] = editorInstance.option('value')
}
return values
},
{},
)
}, {})
const formData = {
...editingFormDataRef.current,
...(form?.option?.('formData') || {}),
@ -1294,47 +1236,38 @@ const Tree = (props: TreeProps) => {
}
editingFormDataRef.current = { ...formData }
const scriptKey = `${mode}|${String(rowKey)}|${scriptItems
.map((formItem) => formItem.dataField)
.join('|')}|${JSON.stringify(formData)}`
gridDto.gridOptions.editingFormDto
.flatMap((group) => flattenEditingFormItems([group]))
.filter((formItem) =>
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,
}
if (lastEditingContentReadyScriptKeyRef.current === scriptKey) {
return
}
lastEditingContentReadyScriptKeyRef.current = scriptKey
scriptItems.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,
executeEditorScript(formItem.editorScript!, {
formData,
e,
editor,
runtimeSetEditorReadOnly,
setFormData,
})
} catch (err) {
console.error('Script exec error on contentReady', formItem.dataField, err)
}
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()
@ -1390,6 +1323,7 @@ const Tree = (props: TreeProps) => {
console.error('Script exec error', err)
}
}
}
},
items:
@ -1411,9 +1345,7 @@ const Tree = (props: TreeProps) => {
let parsedEditorOptions: EditorOptionsWithButtons = {}
const forcedEditorOptions: EditorOptionsWithButtons = {}
try {
parsedEditorOptions = i.editorOptions
? JSON.parse(i.editorOptions)
: {}
parsedEditorOptions = i.editorOptions ? JSON.parse(i.editorOptions) : {}
const rawFilter = searchParams?.get('filter')
if (rawFilter) {
@ -1445,20 +1377,20 @@ const Tree = (props: TreeProps) => {
if (listFormField?.sourceDbType === DbTypeEnum.Date) {
Object.assign(defaultEditorOptions, {
type: 'date',
dateSerializationFormat: 'yyyy-MM-dd',
displayFormat: 'shortDate',
})
type: 'date',
dateSerializationFormat: 'yyyy-MM-dd',
displayFormat: 'shortDate',
})
} else if (
listFormField?.sourceDbType === DbTypeEnum.DateTime ||
listFormField?.sourceDbType === DbTypeEnum.DateTime2 ||
listFormField?.sourceDbType === DbTypeEnum.DateTimeOffset
) {
Object.assign(defaultEditorOptions, {
type: 'datetime',
dateSerializationFormat: 'yyyy-MM-ddTHH:mm:ss',
displayFormat: 'shortDateShortTime',
})
type: 'datetime',
dateSerializationFormat: 'yyyy-MM-ddTHH:mm:ss',
displayFormat: 'shortDateShortTime',
})
}
editorOptions = {
@ -1681,14 +1613,6 @@ const Tree = (props: TreeProps) => {
{toolbarModalData?.content}
</Dialog>
<GridFilterDialogs gridRef={gridRef as any} listFormCode={listFormCode} {...filterData} />
{notePanelTarget && (
<NotePanel
entityName={notePanelTarget.entityName}
entityId={notePanelTarget.entityId}
isVisible
onToggle={() => setNotePanelTarget(null)}
/>
)}
</Container>
</>
)

View file

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

View file

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

View file

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