diff --git a/api/modules/Sozsoft.Notifications/Sozsoft.Notifications.Application/NotificationAppService.cs b/api/modules/Sozsoft.Notifications/Sozsoft.Notifications.Application/NotificationAppService.cs index caf0a4a..2a5c5f2 100644 --- a/api/modules/Sozsoft.Notifications/Sozsoft.Notifications.Application/NotificationAppService.cs +++ b/api/modules/Sozsoft.Notifications/Sozsoft.Notifications.Application/NotificationAppService.cs @@ -153,6 +153,32 @@ public class NotificationAppService : ReadOnlyAppService< return ObjectMapper.Map(item); } + public async Task UpdateReadManyAsync(List notificationIds, bool isRead) + { + if (notificationIds.Count == 0) + { + return 0; + } + + var query = await repository.GetQueryableAsync(); + return await query + .Where(a => a.UserId == CurrentUser.Id && notificationIds.Contains(a.Id) && a.IsRead != isRead) + .ExecuteUpdateAsync(setters => setters.SetProperty(a => a.IsRead, isRead)); + } + + public async Task UpdateSentManyAsync(List notificationIds, bool isSent) + { + if (notificationIds.Count == 0) + { + return 0; + } + + var query = await repository.GetQueryableAsync(); + return await query + .Where(a => a.UserId == CurrentUser.Id && notificationIds.Contains(a.Id) && a.IsSent != isSent) + .ExecuteUpdateAsync(setters => setters.SetProperty(a => a.IsSent, isSent)); + } + [RemoteService(false)] public override Task GetAsync(Guid id) => throw new NotImplementedException(); [RemoteService(false)] diff --git a/api/src/Sozsoft.Platform.Application.Contracts/DeveloperKit/CustomComponentDto.cs b/api/src/Sozsoft.Platform.Application.Contracts/DeveloperKit/CustomComponentDto.cs index 6e205fd..c691584 100644 --- a/api/src/Sozsoft.Platform.Application.Contracts/DeveloperKit/CustomComponentDto.cs +++ b/api/src/Sozsoft.Platform.Application.Contracts/DeveloperKit/CustomComponentDto.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel.DataAnnotations; using Volo.Abp.Application.Dtos; namespace Sozsoft.Platform.DeveloperKit; @@ -6,6 +7,7 @@ namespace Sozsoft.Platform.DeveloperKit; public class CustomComponentDto : FullAuditedEntityDto { public string Name { get; set; } = string.Empty; + public string RoutePath { get; set; } = string.Empty; public string Code { get; set; } = string.Empty; public string? Props { get; set; } public string? Description { get; set; } @@ -16,6 +18,10 @@ public class CustomComponentDto : FullAuditedEntityDto public class CreateUpdateCustomComponentDto { public string Name { get; set; } = string.Empty; + [Required] + [StringLength(512)] + [RegularExpression(@"^/.*", ErrorMessage = "RoutePath must start with '/'.")] + public string RoutePath { get; set; } = string.Empty; public string Code { get; set; } = string.Empty; public string? Props { get; set; } public string? Description { get; set; } diff --git a/api/src/Sozsoft.Platform.Application/Forum/ForumAppService.cs b/api/src/Sozsoft.Platform.Application/Forum/ForumAppService.cs index 88982ce..e06e5f7 100644 --- a/api/src/Sozsoft.Platform.Application/Forum/ForumAppService.cs +++ b/api/src/Sozsoft.Platform.Application/Forum/ForumAppService.cs @@ -49,7 +49,7 @@ public class ForumAppService : PlatformAppService, IForumAppService if (string.IsNullOrWhiteSpace(input.Query)) return result; - var query = input.Query.ToLower(); + var query = input.Query.Trim(); // Search in categories if (input.SearchInCategories) @@ -57,8 +57,8 @@ public class ForumAppService : PlatformAppService, IForumAppService var categoryQuery = await _categoryRepository.GetQueryableAsync(); var categories = await AsyncExecuter.ToListAsync( categoryQuery.Where(c => c.IsActive && - (c.Name.ToLower().Contains(query) || - c.Description.ToLower().Contains(query))) + (c.Name.Contains(query) || + c.Description.Contains(query))) .Take(10) ); @@ -71,9 +71,9 @@ public class ForumAppService : PlatformAppService, IForumAppService var topicQuery = await _topicRepository.GetQueryableAsync(); var topics = await AsyncExecuter.ToListAsync( topicQuery.Where(t => - t.Title.ToLower().Contains(query) || - t.Content.ToLower().Contains(query) || - t.AuthorName.ToLower().Contains(query)) + t.Title.Contains(query) || + t.Content.Contains(query) || + t.AuthorName.Contains(query)) .OrderByDescending(t => t.CreationTime) .Take(20) ); @@ -87,8 +87,8 @@ public class ForumAppService : PlatformAppService, IForumAppService var postQuery = await _postRepository.GetQueryableAsync(); var posts = await AsyncExecuter.ToListAsync( postQuery.Where(p => - p.Content.ToLower().Contains(query) || - p.AuthorName.ToLower().Contains(query)) + p.Content.Contains(query) || + p.AuthorName.Contains(query)) .OrderByDescending(p => p.CreationTime) .Take(30) ); @@ -112,10 +112,10 @@ public class ForumAppService : PlatformAppService, IForumAppService if (!string.IsNullOrWhiteSpace(input.Search)) { - var search = input.Search.ToLower(); + var search = input.Search.Trim(); queryable = queryable.Where(c => - c.Name.ToLower().Contains(search) || - c.Description.ToLower().Contains(search)); + c.Name.Contains(search) || + c.Description.Contains(search)); } queryable = queryable.OrderBy(c => c.DisplayOrder); @@ -126,13 +126,34 @@ public class ForumAppService : PlatformAppService, IForumAppService var maxResultCount = input.MaxResultCount > 0 ? input.MaxResultCount : 10; var categories = await AsyncExecuter.ToListAsync( - queryable.Skip(input.SkipCount).Take(input.MaxResultCount) + queryable.Skip(skipCount).Take(maxResultCount) ); - return new PagedResultDto( - totalCount, - ObjectMapper.Map, List>(categories) - ); + var categoryDtos = ObjectMapper.Map, List>(categories); + var categoryIds = categories.Select(category => category.Id).ToList(); + if (categoryIds.Count > 0) + { + var topicQuery = await _topicRepository.GetQueryableAsync(); + var postQuery = await _postRepository.GetQueryableAsync(); + var topicCountItems = await AsyncExecuter.ToListAsync( + topicQuery.Where(topic => categoryIds.Contains(topic.CategoryId)) + .GroupBy(topic => topic.CategoryId) + .Select(group => new { CategoryId = group.Key, Count = group.Count() })); + var topicCounts = topicCountItems.ToDictionary(item => item.CategoryId, item => item.Count); + var postCountItems = await AsyncExecuter.ToListAsync( + postQuery.Where(post => categoryIds.Contains(post.Topic.CategoryId)) + .GroupBy(post => post.Topic.CategoryId) + .Select(group => new { CategoryId = group.Key, Count = group.Count() })); + var postCounts = postCountItems.ToDictionary(item => item.CategoryId, item => item.Count); + + foreach (var category in categoryDtos) + { + category.TopicCount = topicCounts.GetValueOrDefault(category.Id); + category.PostCount = postCounts.GetValueOrDefault(category.Id); + } + } + + return new PagedResultDto(totalCount, categoryDtos); } public async Task GetCategoryAsync(Guid id) @@ -251,10 +272,10 @@ public class ForumAppService : PlatformAppService, IForumAppService if (!string.IsNullOrWhiteSpace(input.Search)) { - var search = input.Search.ToLower(); + var search = input.Search.Trim(); queryable = queryable.Where(t => - t.Title.ToLower().Contains(search) || - t.Content.ToLower().Contains(search)); + t.Title.Contains(search) || + t.Content.Contains(search)); } queryable = queryable.OrderByDescending(t => t.IsPinned) @@ -265,10 +286,24 @@ public class ForumAppService : PlatformAppService, IForumAppService queryable.Skip(input.SkipCount).Take(input.MaxResultCount) ); - return new PagedResultDto( - totalCount, - ObjectMapper.Map, List>(topics) - ); + var topicDtos = ObjectMapper.Map, List>(topics); + var topicIds = topics.Select(topic => topic.Id).ToList(); + if (topicIds.Count > 0) + { + var postQuery = await _postRepository.GetQueryableAsync(); + var replyCountItems = await AsyncExecuter.ToListAsync( + postQuery.Where(post => topicIds.Contains(post.TopicId)) + .GroupBy(post => post.TopicId) + .Select(group => new { TopicId = group.Key, Count = group.Count() })); + var replyCounts = replyCountItems.ToDictionary(item => item.TopicId, item => item.Count); + + foreach (var topic in topicDtos) + { + topic.ReplyCount = replyCounts.GetValueOrDefault(topic.Id); + } + } + + return new PagedResultDto(totalCount, topicDtos); } public async Task GetTopicAsync(Guid id) @@ -368,9 +403,6 @@ public class ForumAppService : PlatformAppService, IForumAppService if (input.TopicId.HasValue) { queryable = queryable.Where(p => p.TopicId == input.TopicId.Value); - - // Increment view count - var topic = await _topicRepository.GetAsync(input.TopicId.Value); } if (input.IsAcceptedAnswer.HasValue) @@ -380,8 +412,8 @@ public class ForumAppService : PlatformAppService, IForumAppService if (!string.IsNullOrWhiteSpace(input.Search)) { - var search = input.Search.ToLower(); - queryable = queryable.Where(p => p.Content.ToLower().Contains(search)); + var search = input.Search.Trim(); + queryable = queryable.Where(p => p.Content.Contains(search)); } queryable = queryable.OrderBy(p => p.CreationTime); @@ -465,7 +497,7 @@ public class ForumAppService : PlatformAppService, IForumAppService await _postRepository.DeleteAsync(id); topic.ReplyCount = Math.Max(0, topic.ReplyCount - 1); - category.PostCount = Math.Max(0, category.PostCount ?? 0 - 1); + category.PostCount = Math.Max(0, (category.PostCount ?? 0) - 1); // Last post değişti mi kontrol et var postsQueryable = await _postRepository.GetQueryableAsync(); diff --git a/api/src/Sozsoft.Platform.Application/Identity/PlatformIdentityAppService.cs b/api/src/Sozsoft.Platform.Application/Identity/PlatformIdentityAppService.cs index 8d310e1..2261c4c 100644 --- a/api/src/Sozsoft.Platform.Application/Identity/PlatformIdentityAppService.cs +++ b/api/src/Sozsoft.Platform.Application/Identity/PlatformIdentityAppService.cs @@ -95,7 +95,8 @@ public class PlatformIdentityAppService : ApplicationService //Branch var queryBranch = await branchUsersRepository.GetQueryableAsync(); - var branchUsers = queryBranch.Where(a => a.UserId == UserId).Select(r => r.BranchId).ToList(); + var branchUsers = await AsyncExecuter.ToListAsync( + queryBranch.Where(a => a.UserId == UserId).Select(r => r.BranchId)); var branchList = await branchRepository.GetListAsync(a => a.TenantId == currentTenantId); var branches = branchList.Select(branch => new AssignedBranchViewModel { @@ -129,7 +130,7 @@ public class PlatformIdentityAppService : ApplicationService }).ToArray(); var departmentList = await departmentRepository.GetListAsync(); - var departments = (await departmentRepository.GetListAsync()).Select(department => new AssignedDepartmentViewModel + var departments = departmentList.Select(department => new AssignedDepartmentViewModel { Id = department.Id, Name = department.Name, @@ -137,7 +138,7 @@ public class PlatformIdentityAppService : ApplicationService }).ToArray(); var jobPositionList = await jobPositionRepository.GetListAsync(); - var jobPositions = (await jobPositionRepository.GetListAsync()).Select(jobPosition => new AssignedJobPoisitionViewModel + var jobPositions = jobPositionList.Select(jobPosition => new AssignedJobPoisitionViewModel { Id = jobPosition.Id, Name = jobPosition.Name, diff --git a/api/src/Sozsoft.Platform.Application/Intranet/IntranetAppService.cs b/api/src/Sozsoft.Platform.Application/Intranet/IntranetAppService.cs index d1814e0..f122a6e 100644 --- a/api/src/Sozsoft.Platform.Application/Intranet/IntranetAppService.cs +++ b/api/src/Sozsoft.Platform.Application/Intranet/IntranetAppService.cs @@ -25,6 +25,8 @@ namespace Sozsoft.Platform.Intranet; [Authorize] public class IntranetAppService : PlatformAppService, IIntranetAppService { + private const int DashboardItemLimit = 50; + private Task<(Dictionary DepartmentDict, Dictionary JobPositionDict)>? _userLookupDictionariesTask; private readonly ICurrentTenant _currentTenant; private readonly BlobManager _blobContainer; private readonly IConfiguration _configuration; @@ -32,6 +34,7 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService private readonly IRepository _eventRepository; private readonly IIdentityUserAppService _identityUserAppService; private readonly IIdentityUserRepository _identityUserRepository; + private readonly IRepository _identityUserEntityRepository; private readonly IRepository _departmentRepository; private readonly IRepository _jobPositionRepository; private readonly IRepository _announcementRepository; @@ -57,6 +60,7 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService IRepository eventRepository, IIdentityUserAppService identityUserAppService, IIdentityUserRepository identityUserRepository, + IRepository identityUserEntityRepository, IRepository departmentRepository, IRepository jobPositionRepository, IRepository announcementRepository, @@ -81,6 +85,7 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService _eventRepository = eventRepository; _identityUserAppService = identityUserAppService; _identityUserRepository = identityUserRepository; + _identityUserEntityRepository = identityUserEntityRepository; _departmentRepository = departmentRepository; _jobPositionRepository = jobPositionRepository; _announcementRepository = announcementRepository; @@ -111,7 +116,12 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService }; } - private async Task<(Dictionary DepartmentDict, Dictionary JobPositionDict)> GetUserLookupDictionariesAsync() + private Task<(Dictionary DepartmentDict, Dictionary JobPositionDict)> GetUserLookupDictionariesAsync() + { + return _userLookupDictionariesTask ??= LoadUserLookupDictionariesAsync(); + } + + private async Task<(Dictionary DepartmentDict, Dictionary JobPositionDict)> LoadUserLookupDictionariesAsync() { var departments = await _departmentRepository.GetListAsync(); var jobPositions = await _jobPositionRepository.GetListAsync(); @@ -122,6 +132,17 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService ); } + private async Task> GetUsersByIdsAsync(IEnumerable userIds) + { + var ids = userIds.Where(id => id != Guid.Empty).Distinct().ToList(); + if (ids.Count == 0) + { + return []; + } + + return await _identityUserEntityRepository.GetListAsync(user => ids.Contains(user.Id)); + } + private UserInfoViewModel MapUserInfoViewModel( Volo.Abp.Identity.IdentityUser user, IReadOnlyDictionary departmentDict, @@ -153,7 +174,10 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService .WithDetailsAsync(e => e.Category, e => e.Type); var events = await AsyncExecuter.ToListAsync( - queryable.Where(e => e.isPublished).OrderByDescending(e => e.CreationTime) + queryable + .Where(e => e.isPublished) + .OrderByDescending(e => e.CreationTime) + .Take(DashboardItemLimit) ); if (events.Count == 0) @@ -170,7 +194,8 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService // Load all likes for these events var likesQueryable = await _eventLikeRepository.GetQueryableAsync(); var allLikes = await AsyncExecuter.ToListAsync( - likesQueryable.Where(l => eventIds.Contains(l.EventId)) + likesQueryable.Where(l => + eventIds.Contains(l.EventId) && l.UserId == CurrentUser.Id) ); var likedEventIds = allLikes .Where(l => l.UserId == CurrentUser.Id) @@ -192,9 +217,8 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService var (departmentDict, jobPositionDict) = await GetUserLookupDictionariesAsync(); var fallbackUser = await GetDashboardFallbackUserAsync(departmentDict, jobPositionDict); - var users = await _identityUserRepository.GetListAsync(); + var users = await GetUsersByIdsAsync(userIds); var userDict = users - .Where(u => userIds.Contains(u.Id)) .ToDictionary(u => u.Id, u => MapUserInfoViewModel(u, departmentDict, jobPositionDict)); var commentsByEvent = allComments.GroupBy(c => c.EventId) @@ -264,9 +288,8 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService var userIds = comments.Select(c => c.UserId).Distinct().ToList(); var (departmentDict, jobPositionDict) = await GetUserLookupDictionariesAsync(); - var users = await _identityUserRepository.GetListAsync(); + var users = await GetUsersByIdsAsync(userIds); var userDict = users - .Where(u => userIds.Contains(u.Id)) .ToDictionary(u => u.Id, u => MapUserInfoViewModel(u, departmentDict, jobPositionDict)); return comments.Select(c => new EventCommentDto @@ -371,7 +394,11 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService private async Task> GetAnnouncementsAsync() { - var announcements = await _announcementRepository.GetListAsync(); + var announcementQueryable = await _announcementRepository.GetQueryableAsync(); + var announcements = await AsyncExecuter.ToListAsync( + announcementQueryable + .OrderByDescending(announcement => announcement.CreationTime) + .Take(DashboardItemLimit)); var announcementDtos = new List(); if (announcements.Count == 0) @@ -386,7 +413,8 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService var likesQueryable = await _announcementLikeRepository.GetQueryableAsync(); var allLikes = await AsyncExecuter.ToListAsync( - likesQueryable.Where(l => announcementIds.Contains(l.AnnouncementId)) + likesQueryable.Where(l => + announcementIds.Contains(l.AnnouncementId) && l.UserId == CurrentUser.Id) ); var likedAnnouncementIds = allLikes @@ -403,9 +431,8 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService var (departmentDict, jobPositionDict) = await GetUserLookupDictionariesAsync(); var fallbackUser = await GetDashboardFallbackUserAsync(departmentDict, jobPositionDict); - var users = await _identityUserRepository.GetListAsync(); + var users = await GetUsersByIdsAsync(userIds); var userDict = users - .Where(u => userIds.Contains(u.Id)) .ToDictionary(u => u.Id, u => MapUserInfoViewModel(u, departmentDict, jobPositionDict)); var commentsByAnnouncement = allComments @@ -454,9 +481,8 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService var userIds = comments.Select(c => c.UserId).Distinct().ToList(); var (departmentDict, jobPositionDict) = await GetUserLookupDictionariesAsync(); - var users = await _identityUserRepository.GetListAsync(); + var users = await GetUsersByIdsAsync(userIds); var userDict = users - .Where(u => userIds.Contains(u.Id)) .ToDictionary(u => u.Id, u => MapUserInfoViewModel(u, departmentDict, jobPositionDict)); return comments.Select(c => new AnnouncementCommentDto @@ -617,9 +643,8 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService { var (departmentDict, jobPositionDict) = await GetUserLookupDictionariesAsync(); - var users = await _identityUserRepository.GetListAsync(); + var users = await GetUsersByIdsAsync(userIds); var userMap = users - .Where(u => userIds.Contains(u.Id)) .ToDictionary(u => u.Id, u => MapUserInfoViewModel(u, departmentDict, jobPositionDict)); foreach (var dto in dtos) @@ -947,9 +972,8 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService if (userIds.Count > 0) { var (departmentDict, jobPositionDict) = await GetUserLookupDictionariesAsync(); - var users = await _identityUserRepository.GetListAsync(); + var users = await GetUsersByIdsAsync(userIds); var userMap = users - .Where(u => userIds.Contains(u.Id)) .ToDictionary(u => u.Id, u => MapUserInfoViewModel(u, departmentDict, jobPositionDict)); if (dto.UserId.HasValue && userMap.TryGetValue(dto.UserId.Value, out var postUser)) diff --git a/api/src/Sozsoft.Platform.Application/Messenger/MessengerAppService.cs b/api/src/Sozsoft.Platform.Application/Messenger/MessengerAppService.cs index 8c2ed50..758fdf4 100644 --- a/api/src/Sozsoft.Platform.Application/Messenger/MessengerAppService.cs +++ b/api/src/Sozsoft.Platform.Application/Messenger/MessengerAppService.cs @@ -28,7 +28,7 @@ public class MessengerAppService : ApplicationService private readonly IRepository _userRepository; private readonly IRepository _conversationRepository; private readonly IRepository _messageRepository; - private readonly IIdentitySessionRepository _identitySessionRepository; + private readonly IRepository _identitySessionRepository; private readonly BlobManager _blobManager; private readonly IConfiguration _configuration; private readonly IHubContext _messengerHubContext; @@ -37,7 +37,7 @@ public class MessengerAppService : ApplicationService IRepository userRepository, IRepository conversationRepository, IRepository messageRepository, - IIdentitySessionRepository identitySessionRepository, + IRepository identitySessionRepository, BlobManager blobManager, IConfiguration configuration, IHubContext messengerHubContext) @@ -450,11 +450,14 @@ public class MessengerAppService : ApplicationService return new HashSet(); } - var sessions = await _identitySessionRepository.GetListAsync(); - return sessions - .Where(session => normalizedUserIds.Contains(session.UserId)) - .Select(session => session.UserId) - .ToHashSet(); + var sessionsQuery = await _identitySessionRepository.GetQueryableAsync(); + var onlineUserIds = await AsyncExecuter.ToListAsync( + sessionsQuery + .Where(session => normalizedUserIds.Contains(session.UserId)) + .Select(session => session.UserId) + .Distinct()); + + return onlineUserIds.ToHashSet(); } private async Task FindConversationByParticipantKeyAsync(string participantKey) diff --git a/api/src/Sozsoft.Platform.Application/OrgChart/OrgChartAppService.cs b/api/src/Sozsoft.Platform.Application/OrgChart/OrgChartAppService.cs index 84279cb..397d6bd 100644 --- a/api/src/Sozsoft.Platform.Application/OrgChart/OrgChartAppService.cs +++ b/api/src/Sozsoft.Platform.Application/OrgChart/OrgChartAppService.cs @@ -37,15 +37,15 @@ public class OrgChartAppService : PlatformAppService var users = await _userRepository.GetListAsync(); var jobById = jobPositions.ToDictionary(x => x.Id); + var positionsByDepartment = jobPositions + .GroupBy(position => position.DepartmentId) + .ToDictionary(group => group.Key, group => group.OrderBy(position => position.Name).ToList()); + var usersByJobPosition = GroupUsersByJobPosition(users); var topPositionByDepartment = new Dictionary(); foreach (var department in departments) { - var departmentPositions = jobPositions - .Where(x => x.DepartmentId == department.Id) - .ToList(); - - if (!departmentPositions.Any()) + if (!positionsByDepartment.TryGetValue(department.Id, out var departmentPositions)) { continue; } @@ -77,19 +77,9 @@ public class OrgChartAppService : PlatformAppService ParentId = d.ParentId, JobPositionId = topPosition?.Id, JobPositionName = topPosition?.Name, - Users = users - .Where(u => - topPosition != null && - u.GetJobPositionId() == topPosition.Id) - .Select(u => new OrgChartUserDto - { - Id = u.Id, - FullName = u.GetFullName(), - Email = u.Email, - UserName = u.UserName, - PhoneNumber = u.PhoneNumber - }) - .ToList(), + Users = topPosition != null && usersByJobPosition.TryGetValue(topPosition.Id, out var positionUsers) + ? positionUsers + : [], }; }).ToList(); @@ -104,6 +94,7 @@ public class OrgChartAppService : PlatformAppService var users = await _userRepository.GetListAsync(); var deptDict = departments.ToDictionary(d => d.Id, d => d.Name); + var usersByJobPosition = GroupUsersByJobPosition(users); var nodes = jobPositions.Select(jp => new OrgChartNodeDto { @@ -114,19 +105,28 @@ public class OrgChartAppService : PlatformAppService DepartmentName = deptDict.TryGetValue(jp.DepartmentId, out var name) ? name : null, JobPositionId = jp.Id, JobPositionName = jp.Name, - Users = users - .Where(u => u.GetJobPositionId() == jp.Id) - .Select(u => new OrgChartUserDto - { - Id = u.Id, - FullName = u.GetFullName(), - Email = u.Email, - UserName = u.UserName, - PhoneNumber = u.PhoneNumber - }) - .ToList(), + Users = usersByJobPosition.TryGetValue(jp.Id, out var positionUsers) + ? positionUsers + : [], }).ToList(); return nodes; } + + private static Dictionary> GroupUsersByJobPosition( + IEnumerable users) + { + return users + .GroupBy(user => user.GetJobPositionId()) + .ToDictionary( + group => group.Key, + group => group.Select(user => new OrgChartUserDto + { + Id = user.Id, + FullName = user.GetFullName(), + Email = user.Email, + UserName = user.UserName, + PhoneNumber = user.PhoneNumber + }).ToList()); + } } diff --git a/api/src/Sozsoft.Platform.DbMigrator/Seeds/ListFormSeeder_Saas.cs b/api/src/Sozsoft.Platform.DbMigrator/Seeds/ListFormSeeder_Saas.cs index 7395f9f..82b983d 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/Seeds/ListFormSeeder_Saas.cs +++ b/api/src/Sozsoft.Platform.DbMigrator/Seeds/ListFormSeeder_Saas.cs @@ -5675,7 +5675,7 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency ValueExpr = "key", LookupQuery = JsonSerializer.Serialize(new LookupDataDto[] { new () { Key="normal",Name="Normal" }, - new () { Key="dynamic",Name="Dynamic" }, + // new () { Key="dynamic",Name="Dynamic" }, }), }), ValidationRuleJson = DefaultValidationRuleRequiredJson, diff --git a/api/src/Sozsoft.Platform.DbMigrator/Seeds/MenusData.json b/api/src/Sozsoft.Platform.DbMigrator/Seeds/MenusData.json index 98a07f6..763b2d6 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/Seeds/MenusData.json +++ b/api/src/Sozsoft.Platform.DbMigrator/Seeds/MenusData.json @@ -1,13 +1,5 @@ { "Routes": [ - { - "key": "roleListComponent", - "path": "/admin/RoleListComponent", - "componentType": "dynamic", - "componentPath": "RoleListComponent", - "routeType": "protected", - "authority": [] - }, { "key": "home", "path": "/home", diff --git a/api/src/Sozsoft.Platform.Domain/Entities/Tenant/Administration/DeveloperKit/CustomComponent.cs b/api/src/Sozsoft.Platform.Domain/Entities/Tenant/Administration/DeveloperKit/CustomComponent.cs index e36f8e6..6b89de4 100644 --- a/api/src/Sozsoft.Platform.Domain/Entities/Tenant/Administration/DeveloperKit/CustomComponent.cs +++ b/api/src/Sozsoft.Platform.Domain/Entities/Tenant/Administration/DeveloperKit/CustomComponent.cs @@ -9,15 +9,17 @@ public class CustomComponent : FullAuditedEntity, IMultiTenant public virtual Guid? TenantId { get; protected set; } public string Name { get; set; } = string.Empty; + public string RoutePath { get; set; } = string.Empty; public string Code { get; set; } = string.Empty; public string? Props { get; set; } public string? Description { get; set; } public bool IsActive { get; set; } = true; public string? Dependencies { get; set; } // JSON string of component names - public CustomComponent(string name, string code, string? props, string? description, bool isActive, string? dependencies) + public CustomComponent(string name, string routePath, string code, string? props, string? description, bool isActive, string? dependencies) { Name = name; + RoutePath = routePath; Code = code; Props = props; Description = description; diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/EntityFrameworkCore/PlatformDbContext.cs b/api/src/Sozsoft.Platform.EntityFrameworkCore/EntityFrameworkCore/PlatformDbContext.cs index dbb5ad4..20b9b2c 100644 --- a/api/src/Sozsoft.Platform.EntityFrameworkCore/EntityFrameworkCore/PlatformDbContext.cs +++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/EntityFrameworkCore/PlatformDbContext.cs @@ -637,12 +637,14 @@ public class PlatformDbContext : b.ConfigureByConvention(); b.Property(x => x.Name).IsRequired().HasMaxLength(128); + b.Property(x => x.RoutePath).IsRequired().HasMaxLength(512); b.Property(x => x.Code).IsRequired(); b.Property(x => x.Props).HasMaxLength(1024); b.Property(x => x.Description).HasMaxLength(512); b.Property(x => x.Dependencies).HasMaxLength(2048); b.HasIndex(x => new { x.TenantId, x.Name }).IsUnique().HasFilter("[IsDeleted] = 0"); + b.HasIndex(x => new { x.TenantId, x.RoutePath }).IsUnique().HasFilter("[IsDeleted] = 0"); }); builder.Entity(b => diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260709175406_Initial.Designer.cs b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260711200250_Initial.Designer.cs similarity index 99% rename from api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260709175406_Initial.Designer.cs rename to api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260711200250_Initial.Designer.cs index d69c8e9..561e54b 100644 --- a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260709175406_Initial.Designer.cs +++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260711200250_Initial.Designer.cs @@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore; namespace Sozsoft.Platform.Migrations { [DbContext(typeof(PlatformDbContext))] - [Migration("20260709175406_Initial")] + [Migration("20260711200250_Initial")] partial class Initial { /// @@ -1826,6 +1826,11 @@ namespace Sozsoft.Platform.Migrations .HasMaxLength(1024) .HasColumnType("nvarchar(1024)"); + b.Property("RoutePath") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("nvarchar(512)"); + b.Property("TenantId") .HasColumnType("uniqueidentifier") .HasColumnName("TenantId"); @@ -1836,6 +1841,10 @@ namespace Sozsoft.Platform.Migrations .IsUnique() .HasFilter("[IsDeleted] = 0"); + b.HasIndex("TenantId", "RoutePath") + .IsUnique() + .HasFilter("[IsDeleted] = 0"); + b.ToTable("Sas_H_CustomComponent", (string)null); }); diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260709175406_Initial.cs b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260711200250_Initial.cs similarity index 99% rename from api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260709175406_Initial.cs rename to api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260711200250_Initial.cs index 10499d1..7e24513 100644 --- a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260709175406_Initial.cs +++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/20260711200250_Initial.cs @@ -1128,6 +1128,7 @@ namespace Sozsoft.Platform.Migrations Id = table.Column(type: "uniqueidentifier", nullable: false), TenantId = table.Column(type: "uniqueidentifier", nullable: true), Name = table.Column(type: "nvarchar(128)", maxLength: 128, nullable: false), + RoutePath = table.Column(type: "nvarchar(512)", maxLength: 512, nullable: false), Code = table.Column(type: "nvarchar(max)", nullable: false), Props = table.Column(type: "nvarchar(1024)", maxLength: 1024, nullable: true), Description = table.Column(type: "nvarchar(512)", maxLength: 512, nullable: true), @@ -4020,6 +4021,13 @@ namespace Sozsoft.Platform.Migrations unique: true, filter: "[IsDeleted] = 0"); + migrationBuilder.CreateIndex( + name: "IX_Sas_H_CustomComponent_TenantId_RoutePath", + table: "Sas_H_CustomComponent", + columns: new[] { "TenantId", "RoutePath" }, + unique: true, + filter: "[IsDeleted] = 0"); + migrationBuilder.CreateIndex( name: "IX_Sas_H_CustomEndpoint_TenantId_Name", table: "Sas_H_CustomEndpoint", diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/PlatformDbContextModelSnapshot.cs b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/PlatformDbContextModelSnapshot.cs index ba0de3d..6cf3ade 100644 --- a/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/PlatformDbContextModelSnapshot.cs +++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/Migrations/PlatformDbContextModelSnapshot.cs @@ -1823,6 +1823,11 @@ namespace Sozsoft.Platform.Migrations .HasMaxLength(1024) .HasColumnType("nvarchar(1024)"); + b.Property("RoutePath") + .IsRequired() + .HasMaxLength(512) + .HasColumnType("nvarchar(512)"); + b.Property("TenantId") .HasColumnType("uniqueidentifier") .HasColumnName("TenantId"); @@ -1833,6 +1838,10 @@ namespace Sozsoft.Platform.Migrations .IsUnique() .HasFilter("[IsDeleted] = 0"); + b.HasIndex("TenantId", "RoutePath") + .IsUnique() + .HasFilter("[IsDeleted] = 0"); + b.ToTable("Sas_H_CustomComponent", (string)null); }); diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/Seeds/TenantData.json b/api/src/Sozsoft.Platform.EntityFrameworkCore/Seeds/TenantData.json index b6d5499..1a8f6c1 100644 --- a/api/src/Sozsoft.Platform.EntityFrameworkCore/Seeds/TenantData.json +++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/Seeds/TenantData.json @@ -349,6 +349,7 @@ "CustomComponents": [ { "name": "DynamicEntityComponent", + "routePath": "/admin/dynamic-entity", "code": "import React, { useEffect, useState } from \"react\";\nimport axios from \"axios\";\n\ninterface DynamicEntityComponentProps {\n title: string;\n}\n\nconst api = axios.create({\n baseURL: \"https://localhost:44344\",\n});\n\nconst DynamicEntityComponent: React.FC = ({ title }) => {\n const [data, setData] = useState>([]);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState(null);\n\n useEffect(() => {\n const fetchData = async () => {\n setLoading(true);\n setError(null);\n\n try {\n const res = await api.get(`/api/app/crudendpoint/${title}`);\n const raw = Array.isArray(res.data) ? res.data : res.data?.items ?? [];\n\n const filtered = raw.map((item: any) => ({\n id: item.Id ?? item.id,\n name: item.Name ?? item.name,\n }));\n\n setData(filtered);\n } catch (err: any) {\n setError(err.message || \"Failed to fetch data\");\n } finally {\n setLoading(false);\n }\n };\n\n if (title) fetchData();\n }, [title]);\n\n if (loading) return
Loading...
;\n if (error) return
Error: {error}
;\n if (!data.length) return
No records found
;\n\n const headers = [\"id\", \"name\", \"actions\"];\n\n return (\n
\n \n \n \n {headers.map((key) => (\n \n {key === \"actions\" ? \"Actions\" : key}\n \n ))}\n \n \n \n {data.map((item, rowIndex) => (\n \n \n \n \n \n ))}\n \n
\n {item.id}\n \n {item.name}\n \n alert(item.name)}\n className=\"bg-blue-600 hover:bg-blue-700 dark:bg-blue-800 dark:hover:bg-blue-900 text-white text-sm px-3 py-1 rounded-lg shadow-sm transition\"\n >\n Show Name\n \n
\n
\n );\n};\n\nexport default DynamicEntityComponent;", "props": null, "description": null, @@ -357,6 +358,7 @@ }, { "name": "RoleListComponent", + "routePath": "/admin/roles-list", "code": "const RoleListComponent = ({\n title = \"AbpRoles\"\n}) => {\n return (\n \n );\n};\n\nexport default RoleListComponent;", "props": null, "description": null, diff --git a/api/src/Sozsoft.Platform.EntityFrameworkCore/Seeds/TenantDataSeeder.cs b/api/src/Sozsoft.Platform.EntityFrameworkCore/Seeds/TenantDataSeeder.cs index 9d099ac..cdeb566 100644 --- a/api/src/Sozsoft.Platform.EntityFrameworkCore/Seeds/TenantDataSeeder.cs +++ b/api/src/Sozsoft.Platform.EntityFrameworkCore/Seeds/TenantDataSeeder.cs @@ -354,6 +354,7 @@ public class InstallmentOptionSeedDto public class CustomComponentSeedDto { public string Name { get; set; } + public string RoutePath { get; set; } public string Code { get; set; } public string Props { get; set; } public string Description { get; set; } @@ -851,6 +852,7 @@ public class TenantDataSeeder : IDataSeedContributor, ITransientDependency { await _customComponentRepository.InsertAsync(new CustomComponent( item.Name, + item.RoutePath, item.Code, item.Props, item.Description, diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/PlatformHttpApiHostModule.cs b/api/src/Sozsoft.Platform.HttpApi.Host/PlatformHttpApiHostModule.cs index 378a162..3b0c7c8 100644 --- a/api/src/Sozsoft.Platform.HttpApi.Host/PlatformHttpApiHostModule.cs +++ b/api/src/Sozsoft.Platform.HttpApi.Host/PlatformHttpApiHostModule.cs @@ -21,6 +21,7 @@ using Microsoft.AspNetCore.Cors; using Microsoft.AspNetCore.Extensions.DependencyInjection; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.ResponseCompression; using Microsoft.Extensions.Caching.StackExchangeRedis; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -141,10 +142,22 @@ public class PlatformHttpApiHostModule : AbpModule ConfigureBlobStoring(configuration); ConfigureAuditing(); + context.Services.AddResponseCompression(options => + { + options.EnableForHttps = true; + options.Providers.Add(); + options.Providers.Add(); + }); + ConfigureDynamicServices(context); ConfigureDevExpressReporting(context, configuration); - context.Services.AddSignalR(); + var signalR = context.Services.AddSignalR(); + if (configuration.GetValue("Redis:IsEnabled") && + !configuration["Redis:Configuration"].IsNullOrWhiteSpace()) + { + signalR.AddStackExchangeRedis(configuration["Redis:Configuration"]!); + } Configure(options => { @@ -489,6 +502,7 @@ public class PlatformHttpApiHostModule : AbpModule // } app.UseCorrelationId(); + app.UseResponseCompression(); app.MapAbpStaticAssets(); app.UseRouting(); app.UseCors(); diff --git a/api/src/Sozsoft.Platform.HttpApi.Host/Sozsoft.Platform.HttpApi.Host.csproj b/api/src/Sozsoft.Platform.HttpApi.Host/Sozsoft.Platform.HttpApi.Host.csproj index 8a5b8d5..023e1bd 100644 --- a/api/src/Sozsoft.Platform.HttpApi.Host/Sozsoft.Platform.HttpApi.Host.csproj +++ b/api/src/Sozsoft.Platform.HttpApi.Host/Sozsoft.Platform.HttpApi.Host.csproj @@ -28,6 +28,7 @@ + diff --git a/ui/public/version.json b/ui/public/version.json index be7e68d..373bea6 100644 --- a/ui/public/version.json +++ b/ui/public/version.json @@ -1,5 +1,5 @@ { - "commit": "c42a8d2", + "commit": "351eddf", "releases": [ { "version": "1.1.06", diff --git a/ui/src/App.tsx b/ui/src/App.tsx index bceba49..311af5c 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -9,7 +9,14 @@ import { ComponentProvider } from './contexts/ComponentContext' import { registerServiceWorker } from './views/version/swRegistration' import { useEffect } from 'react' -const queryClient = new QueryClient() +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 30_000, + refetchOnWindowFocus: false, + }, + }, +}) function App() { useEffect(() => { diff --git a/ui/src/components/codeLayout/ErrorBoundary.tsx b/ui/src/components/codeLayout/ErrorBoundary.tsx deleted file mode 100644 index e7ded9d..0000000 --- a/ui/src/components/codeLayout/ErrorBoundary.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import React from "react"; -import type { ErrorInfo } from "react"; - -interface Props { - children: React.ReactNode; -} - -interface State { - hasError: boolean; - error: Error | null; -} - -class ErrorBoundary extends React.Component { - constructor(props: Props) { - super(props); - this.state = { hasError: false, error: null }; - } - - static getDerivedStateFromError(error: Error): State { - return { hasError: true, error }; - } - - componentDidUpdate(prevProps: Props) { - // Eğer component değişmişse, hata state'ini sıfırla - if (prevProps.children !== this.props.children && this.state.hasError) { - this.setState({ hasError: false, error: null }); - } - } - - componentDidCatch(error: Error, info: ErrorInfo) { - console.error("Render hatası:", error, info); - } - - render() { - if (this.state.hasError) { - return ( -
- ⚠️ Render Hatası: -
-
- Detayları Göster - {this.state.error?.message || String(this.state.error)} -
-
- ); - } - - return this.props.children; - } -} - -export default ErrorBoundary; diff --git a/ui/src/components/template/MessengerWidget.tsx b/ui/src/components/template/MessengerWidget.tsx index 9addf60..e9c9513 100644 --- a/ui/src/components/template/MessengerWidget.tsx +++ b/ui/src/components/template/MessengerWidget.tsx @@ -19,9 +19,9 @@ import { messengerSignalR } from '@/services/messenger.signalr' import { useStoreState } from '@/store' import withHeaderItem from '@/utils/hoc/withHeaderItem' import dayjs from 'dayjs' -import EmojiPicker, { EmojiClickData } from 'emoji-picker-react' +import type { EmojiClickData } from 'emoji-picker-react' import classNames from 'classnames' -import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { lazy, Suspense, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { FaArrowLeft, FaPaperclip, @@ -38,6 +38,7 @@ import { useLocation } from 'react-router-dom' const MAX_UPLOAD_SIZE = 10 * 1024 * 1024 const CONTACT_REFRESH_INTERVAL = 30000 +const EmojiPicker = lazy(() => import('emoji-picker-react')) const formatFileSize = (size: number) => { if (size < 1024 * 1024) return `${Math.max(1, Math.round(size / 1024))} KB` @@ -704,11 +705,13 @@ const MessengerWidgetContent = ({ className }: { className?: string }) => {
{showEmoji && (
- + }> + +
)}
diff --git a/ui/src/components/template/Notification.tsx b/ui/src/components/template/Notification.tsx index 49dc77b..79ba45b 100644 --- a/ui/src/components/template/Notification.tsx +++ b/ui/src/components/template/Notification.tsx @@ -8,7 +8,13 @@ import Tooltip from '@/components/ui/Tooltip' import { AVATAR_URL } from '@/constants/app.constant' import NotificationChannels from '@/constants/notification-channel.enum' import { ROUTES_ENUM } from '@/routes/route.constant' -import { getList, updateRead, updateReadAll, updateSent } from '@/services/notification.service' +import { + getList, + updateRead, + updateReadAll, + updateReadMany, + updateSentMany, +} from '@/services/notification.service' import { useStoreState } from '@/store' import withHeaderItem from '@/utils/hoc/withHeaderItem' import { useLocalization } from '@/utils/hooks/useLocalization' @@ -122,8 +128,8 @@ const _Notification = ({ className }: { className?: string }) => { maxResultCount: 1000, }) const items = resp.data.items ?? [] - for (const notification of items) { - await updateSent(notification.id, true) + if (items.length > 0) { + await updateSentMany(items.map((notification) => notification.id), true) } const newNotificationList = items.map( (a) => @@ -208,18 +214,18 @@ const _Notification = ({ className }: { className?: string }) => { , { placement: 'bottom-end' }, ) - await updateSent(notification.id, true) - await updateRead(notification.id, true) } // Desktop - if (desktopGranted) { - const newDesktopList = items.filter( + const newDesktopList = desktopGranted + ? items.filter( (a) => a.notificationChannel === NotificationChannels.Desktop && !desktopNotificationList.current.includes(a.id) && !a.isSent, ) + : [] + if (desktopGranted) { desktopNotificationList.current = [ ...desktopNotificationList.current, ...newDesktopList.map((a) => a.id), @@ -238,11 +244,14 @@ const _Notification = ({ className }: { className?: string }) => { } else { new window.Notification(title, options) } - - await updateSent(notification.id, true) - await updateRead(notification.id, true) } } + + const processedIds = [...newToastList, ...newDesktopList].map((notification) => notification.id) + if (processedIds.length > 0) { + await updateSentMany(processedIds, true) + await updateReadMany(processedIds, true) + } } useEffect(() => { diff --git a/ui/src/contexts/ComponentContext.tsx b/ui/src/contexts/ComponentContext.tsx index c0dd121..e511e08 100644 --- a/ui/src/contexts/ComponentContext.tsx +++ b/ui/src/contexts/ComponentContext.tsx @@ -6,7 +6,6 @@ import { import { developerKitService } from '@/services/developerKit.service' import { useStoreState } from '@/store/store' import React, { createContext, useContext, useState, useEffect, useCallback } from 'react' -import * as Babel from '@babel/standalone' import { Alert, Avatar, @@ -262,53 +261,56 @@ export const ComponentProvider: React.FC<{ children: React.ReactNode }> = ({ chi useEffect(() => { if (!components || !components?.length) return - try { - const activeComponents = components?.filter((c) => c.isActive) + let cancelled = false - if (!activeComponents.length) { - setCompiledComponents({}) - return - } + const compileComponents = async () => { + try { + const activeComponents = components?.filter((c) => c.isActive) - const componentInfos = activeComponents.map((comp) => { - const name = comp.name - const nameCapitalized = name.charAt(0).toUpperCase() + name.slice(1) - - return { - name: name, - nameCapitalized: nameCapitalized, - internalName: extractComponentInfo(comp.code, nameCapitalized), - code: comp.code - .replace(/import\s+.*?;/g, '') - .replace(/export\s+default\s+/, '') - .trim(), + if (!activeComponents.length) { + setCompiledComponents({}) + return } - }) - // Create cross-referencing bundle - const componentDeclarations = componentInfos - .map((info) => `let ${info.name}_Component;`) - .join('\n') + const componentInfos = activeComponents.map((comp) => { + const name = comp.name + const nameCapitalized = name.charAt(0).toUpperCase() + name.slice(1) - const componentDefinitions = componentInfos - .map((info) => { - const componentVariables = componentInfos - .filter((other) => other.name !== info.name) - .map((other) => `const ${other.name} = ${other.name}_Component;`) - .join('\n ') + return { + name: name, + nameCapitalized: nameCapitalized, + internalName: extractComponentInfo(comp.code, nameCapitalized), + code: comp.code + .replace(/import\s+.*?;/g, '') + .replace(/export\s+default\s+/, '') + .trim(), + } + }) - return ` + // Create cross-referencing bundle + const componentDeclarations = componentInfos + .map((info) => `let ${info.name}_Component;`) + .join('\n') + + const componentDefinitions = componentInfos + .map((info) => { + const componentVariables = componentInfos + .filter((other) => other.name !== info.name) + .map((other) => `const ${other.name} = ${other.name}_Component;`) + .join('\n ') + + return ` ${info.name}_Component = (function() { ${componentVariables} ${info.code} return ${info.internalName}; })();` - }) - .join('\n') + }) + .join('\n') - const componentBundle = componentDeclarations + '\n' + componentDefinitions + const componentBundle = componentDeclarations + '\n' + componentDefinitions - const bundledCode = ` + const bundledCode = ` (function(React, Alert, Avatar, Badge, Button, Calendar, Card, Checkbox, ConfigProvider, DatePicker, Dialog, Drawer, Dropdown, FormItem, FormContainer, Input, InputGroup, Menu, MenuItem, Notification, Pagination, Progress, Radio, RangeCalendar, ScrollBar, Segment, Select, Skeleton, Spinner, Steps, Switcher, Table, Tabs, Tag, TimeInput, Timeline, toast, Tooltip, Upload, axios) { const { useState, useEffect, useCallback, useMemo, useRef, createContext, useContext } = React; const componentRegistry = {}; @@ -328,106 +330,118 @@ export const ComponentProvider: React.FC<{ children: React.ReactNode }> = ({ chi })(React, Alert, Avatar, Badge, Button, Calendar, Card, Checkbox, ConfigProvider, DatePicker, Dialog, Drawer, Dropdown, FormItem, FormContainer, Input, InputGroup, Menu, MenuItem, Notification, Pagination, Progress, Radio, RangeCalendar, ScrollBar, Segment, Select, Skeleton, Spinner, Steps, Switcher, Table, Tabs, Tag, TimeInput, Timeline, toast, Tooltip, Upload, axios) ` - const compiledBundle = Babel.transform(bundledCode, { - presets: ['react', 'typescript'], - filename: 'components-bundle.tsx', - }).code + // Babel is several megabytes and is only needed when an active runtime + // component exists. Keep it out of the application startup bundle. + const Babel = await import('@babel/standalone') + if (cancelled) return - if (!compiledBundle) { - throw new Error('Failed to compile components bundle') + const compiledBundle = Babel.transform(bundledCode, { + presets: ['react', 'typescript'], + filename: 'components-bundle.tsx', + }).code + + if (!compiledBundle) { + throw new Error('Failed to compile components bundle') + } + + const componentsFactory = new Function( + 'React', + 'Alert', + 'Avatar', + 'Badge', + 'Button', + 'Calendar', + 'Card', + 'Checkbox', + 'ConfigProvider', + 'DatePicker', + 'Dialog', + 'Drawer', + 'Dropdown', + 'FormItem', + 'FormContainer', + 'Input', + 'InputGroup', + 'Menu', + 'MenuItem', + 'Notification', + 'Pagination', + 'Progress', + 'Radio', + 'RangeCalendar', + 'ScrollBar', + 'Segment', + 'Select', + 'Skeleton', + 'Spinner', + 'Steps', + 'Switcher', + 'Table', + 'Tabs', + 'Tag', + 'TimeInput', + 'Timeline', + 'toast', + 'Tooltip', + 'Upload', + 'axios', + `return ${compiledBundle}`, + ) + + const compiledComponentsRegistry = componentsFactory( + React, + Alert, + Avatar, + Badge, + Button, + Calendar, + Card, + Checkbox, + ConfigProvider, + DatePicker, + Dialog, + Drawer, + Dropdown, + FormItem, + FormContainer, + Input, + InputGroup, + Menu, + MenuItem, + Notification, + Pagination, + Progress, + Radio, + RangeCalendar, + ScrollBar, + Segment, + Select, + Skeleton, + Spinner, + Steps, + Switcher, + Table, + Tabs, + Tag, + TimeInput, + Timeline, + toast, + Tooltip, + Upload, + axios, + ) + + if (!cancelled) setCompiledComponents(compiledComponentsRegistry) + } catch (error) { + console.error('Error compiling components bundle:', error) + if (!cancelled) setCompiledComponents({}) } + } - const componentsFactory = new Function( - 'React', - 'Alert', - 'Avatar', - 'Badge', - 'Button', - 'Calendar', - 'Card', - 'Checkbox', - 'ConfigProvider', - 'DatePicker', - 'Dialog', - 'Drawer', - 'Dropdown', - 'FormItem', - 'FormContainer', - 'Input', - 'InputGroup', - 'Menu', - 'MenuItem', - 'Notification', - 'Pagination', - 'Progress', - 'Radio', - 'RangeCalendar', - 'ScrollBar', - 'Segment', - 'Select', - 'Skeleton', - 'Spinner', - 'Steps', - 'Switcher', - 'Table', - 'Tabs', - 'Tag', - 'TimeInput', - 'Timeline', - 'toast', - 'Tooltip', - 'Upload', - 'axios', - `return ${compiledBundle}`, - ) + void compileComponents() - const compiledComponentsRegistry = componentsFactory( - React, - Alert, - Avatar, - Badge, - Button, - Calendar, - Card, - Checkbox, - ConfigProvider, - DatePicker, - Dialog, - Drawer, - Dropdown, - FormItem, - FormContainer, - Input, - InputGroup, - Menu, - MenuItem, - Notification, - Pagination, - Progress, - Radio, - RangeCalendar, - ScrollBar, - Segment, - Select, - Skeleton, - Spinner, - Steps, - Switcher, - Table, - Tabs, - Tag, - TimeInput, - Timeline, - toast, - Tooltip, - Upload, - axios, - ) - - setCompiledComponents(compiledComponentsRegistry) - } catch (error) { - console.error('Error compiling components bundle:', error) - setCompiledComponents({}) + return () => { + cancelled = true } }, [components, extractComponentInfo]) diff --git a/ui/src/main.tsx b/ui/src/main.tsx index 902f87d..b710be2 100644 --- a/ui/src/main.tsx +++ b/ui/src/main.tsx @@ -4,16 +4,6 @@ import App from './App' import './index.css' import { UiEvalService } from './services/UiEvalService' -import 'devextreme/ui/autocomplete' -import 'devextreme/ui/slider' -import 'devextreme/ui/text_area' -import 'devextreme/ui/html_editor' - -import 'devextreme-react/text-area' -import 'devextreme-react/html-editor' -import 'devextreme-react/autocomplete' -import 'devextreme-react/slider' - import config from 'devextreme/core/config' import { licenseKey } from './devextreme-license' diff --git a/ui/src/proxy/developerKit/models.ts b/ui/src/proxy/developerKit/models.ts index e8881a1..a955de6 100644 --- a/ui/src/proxy/developerKit/models.ts +++ b/ui/src/proxy/developerKit/models.ts @@ -1,61 +1,64 @@ export interface CrudEndpoint { - id: string; + id: string - tenantId: string; - entityName: string; - method: "GET" | "POST" | "PUT" | "DELETE"; - path: string; - operationType: string; - isActive: boolean; - csharpCode: string; - creationTime: string; - lastModificationTime?: string; + tenantId: string + entityName: string + method: 'GET' | 'POST' | 'PUT' | 'DELETE' + path: string + operationType: string + isActive: boolean + csharpCode: string + creationTime: string + lastModificationTime?: string } export interface CreateUpdateCrudEndpointDto { - tenantId: string; - entityName: string; - method: "GET" | "POST" | "PUT" | "DELETE"; - path: string; - operationType: string; - csharpCode: string; - isActive: boolean; + tenantId: string + entityName: string + method: 'GET' | 'POST' | 'PUT' | 'DELETE' + path: string + operationType: string + csharpCode: string + isActive: boolean } export interface CustomComponent { - id: string; - tenantId?: string; - name: string; - code: string; - props?: string; - description?: string; - isActive: boolean; - dependencies?: string; - creationTime: string; - lastModificationTime?: string; + id: string + tenantId?: string + name: string + routePath: string + code: string + props?: string + description?: string + isActive: boolean + dependencies?: string + creationTime: string + lastModificationTime?: string } export interface CustomComponentDto { - id: string; - tenantId?: string; - name: string; - code: string; - props?: string; - description?: string; - isActive: boolean; - dependencies?: string; // JSON string of component names - creationTime: string; - lastModificationTime: string; + id: string + tenantId?: string + name: string + routePath: string + code: string + props?: string + description?: string + isActive: boolean + dependencies?: string // JSON string of component names + creationTime: string + lastModificationTime: string } export interface CreateUpdateCustomComponentDto { - tenantId?: string; - name: string; - code: string; - props?: string; - description?: string; - isActive: boolean; - dependencies?: string; + tenantId?: string + name: string + routePath: string + code: string + props?: string + description?: string + isActive: boolean + dependencies?: string } export type RelationshipType = 'OneToOne' | 'OneToMany' @@ -73,4 +76,4 @@ export interface SqlTableRelation { cascadeUpdate: CascadeBehavior isRequired: boolean description: string -} \ No newline at end of file +} diff --git a/ui/src/routes/dynamicRouteLoader.tsx b/ui/src/routes/dynamicRouteLoader.tsx index 654bdc3..e06877e 100644 --- a/ui/src/routes/dynamicRouteLoader.tsx +++ b/ui/src/routes/dynamicRouteLoader.tsx @@ -1,5 +1,6 @@ import React, { lazy } from 'react' import { RouteDto } from '@/proxy/routes/models' +import { CustomComponent } from '@/proxy/developerKit/models' // Tüm view bileşenlerini import et (vite özel) // shared klasörü hariç, çünkü bu bileşenler genellikle başka yerlerde statik import ediliyor @@ -128,3 +129,27 @@ export function mapDynamicRoutes(routes: RouteDto[]): DynamicReactRoute[] { componentPath: route.componentPath, })) } + +// Custom components are the single source of truth for runtime routes. +export function mapCustomComponentRoutes( + components: Pick[], +): DynamicReactRoute[] { + return components + .filter((component) => component.isActive && component.routePath?.trim()) + .map((component) => ({ + key: `custom-component-${component.id}`, + path: component.routePath.startsWith('/') ? component.routePath : `/${component.routePath}`, + getComponent: (registeredComponents, renderComponent, isComponentRegistered) => + loadComponent( + 'dynamic', + component.name, + registeredComponents, + renderComponent, + isComponentRegistered, + ), + routeType: component.routePath.startsWith('/admin/') ? 'protected' : 'public', + authority: [], + componentType: 'dynamic', + componentPath: component.name, + })) +} diff --git a/ui/src/routes/dynamicRouter.tsx b/ui/src/routes/dynamicRouter.tsx index 2a02b0c..9cb0dcb 100644 --- a/ui/src/routes/dynamicRouter.tsx +++ b/ui/src/routes/dynamicRouter.tsx @@ -1,7 +1,7 @@ // DynamicRouter.tsx import React from 'react' import { Routes, Route, Navigate, useLocation } from 'react-router-dom' -import { mapDynamicRoutes } from './dynamicRouteLoader' +import { mapCustomComponentRoutes, mapDynamicRoutes } from './dynamicRouteLoader' import { useDynamicRoutes } from './dynamicRoutesContext' import { useComponents } from '@/contexts/ComponentContext' import ProtectedRoute from '@/components/route/ProtectedRoute' @@ -48,10 +48,17 @@ const LegacyEmailConfirmationRedirect = () => { export const DynamicRouter: React.FC = () => { const { routes, loading, error } = useDynamicRoutes() - const { registeredComponents, renderComponent, isComponentRegistered } = useComponents() + const { components, registeredComponents, renderComponent, isComponentRegistered } = + useComponents() const location = useLocation() - const dynamicRoutes = React.useMemo(() => mapDynamicRoutes(routes), [routes]) + const dynamicRoutes = React.useMemo( + () => [ + ...mapDynamicRoutes(routes), + ...mapCustomComponentRoutes(components), + ], + [routes, components], + ) // /setup path'inde loading bekleme — setup route her zaman erişilebilir olmalı if (loading && location.pathname !== '/setup') return
Loading...
diff --git a/ui/src/services/notification.service.ts b/ui/src/services/notification.service.ts index bf7ddd3..147bf8e 100644 --- a/ui/src/services/notification.service.ts +++ b/ui/src/services/notification.service.ts @@ -36,6 +36,22 @@ export const updateSent = (notificationId: string, isSent: boolean) => params: { isSent }, }) +export const updateReadMany = (notificationIds: string[], isRead: boolean) => + apiService.fetchData({ + method: 'PUT', + url: `/api/app/notification/read-many`, + params: { isRead }, + data: notificationIds, + }) + +export const updateSentMany = (notificationIds: string[], isSent: boolean) => + apiService.fetchData({ + method: 'PUT', + url: `/api/app/notification/sent-many`, + params: { isSent }, + data: notificationIds, + }) + export const postMyNotificationByNotificationRuleId = (data: { id: string; message: string }) => apiService.fetchData({ method: 'POST', diff --git a/ui/src/utils/hooks/useForumSearch.ts b/ui/src/utils/hooks/useForumSearch.ts index 5e860cf..1e696dc 100644 --- a/ui/src/utils/hooks/useForumSearch.ts +++ b/ui/src/utils/hooks/useForumSearch.ts @@ -1,5 +1,5 @@ import { ForumCategory, ForumPost, ForumTopic } from '@/proxy/forum/forum'; -import { useState, useEffect, useMemo } from 'react'; +import { useState, useMemo } from 'react'; interface UseSearchProps { categories: ForumCategory[]; @@ -16,14 +16,7 @@ interface SearchResult { export function useForumSearch({ categories, topics, posts }: UseSearchProps) { const [searchQuery, setSearchQuery] = useState(''); - const [searchResults, setSearchResults] = useState({ - categories: [], - topics: [], - posts: [], - totalCount: 0 - }); - - const performSearch = useMemo(() => { + const searchResults: SearchResult = useMemo(() => { if (!searchQuery.trim()) { return { categories: [], @@ -62,10 +55,6 @@ export function useForumSearch({ categories, topics, posts }: UseSearchProps) { }; }, [searchQuery, categories, topics, posts]); - useEffect(() => { - setSearchResults(performSearch); - }, [performSearch]); - const clearSearch = () => { setSearchQuery(''); }; @@ -77,4 +66,4 @@ export function useForumSearch({ categories, topics, posts }: UseSearchProps) { clearSearch, hasResults: searchResults.totalCount > 0 }; -} \ No newline at end of file +} diff --git a/ui/src/utils/registerDevExtremeEditors.ts b/ui/src/utils/registerDevExtremeEditors.ts new file mode 100644 index 0000000..a201ef0 --- /dev/null +++ b/ui/src/utils/registerDevExtremeEditors.ts @@ -0,0 +1,11 @@ +// Metadata-driven forms resolve these editors by their DevExtreme widget names. +// Import this module from lazy List/Form boundaries before those forms render. +import 'devextreme/ui/autocomplete' +import 'devextreme/ui/slider' +import 'devextreme/ui/text_area' +import 'devextreme/ui/html_editor' + +import 'devextreme-react/text-area' +import 'devextreme-react/html-editor' +import 'devextreme-react/autocomplete' +import 'devextreme-react/slider' diff --git a/ui/src/views/admin/videoroom/RoomParticipant.tsx b/ui/src/views/admin/videoroom/RoomParticipant.tsx index d1ccccb..87493cb 100644 --- a/ui/src/views/admin/videoroom/RoomParticipant.tsx +++ b/ui/src/views/admin/videoroom/RoomParticipant.tsx @@ -1,8 +1,12 @@ +import { lazy, Suspense } from 'react' import { FaMicrophoneSlash, FaUserTimes } from 'react-icons/fa' -import { VideoPlayer } from './VideoPlayer' import { VideoroomParticipantDto, VideoroomLayoutDto } from '@/proxy/videoroom/models' import { Button } from '@/components/ui' +const VideoPlayer = lazy(() => + import('./VideoPlayer').then((module) => ({ default: module.VideoPlayer })), +) + interface RoomParticipantProps { participants: VideoroomParticipantDto[] localStream?: MediaStream | null @@ -216,17 +220,19 @@ export const RoomParticipant = ({ style={{ minHeight: 0, minWidth: 0 }} >
- + }> + +
{/* Teacher controls for students */} {isTeacher && participant.id !== currentUserId && ( diff --git a/ui/src/views/developerKit/ComponentEditor.tsx b/ui/src/views/developerKit/ComponentEditor.tsx index db097f3..4a65ec9 100644 --- a/ui/src/views/developerKit/ComponentEditor.tsx +++ b/ui/src/views/developerKit/ComponentEditor.tsx @@ -12,6 +12,7 @@ import { Button, Checkbox, FormContainer, FormItem, Input } from '@/components/u // Validation schema const validationSchema = Yup.object({ name: Yup.string().required(), + routePath: Yup.string().required().matches(/^\//, 'Route path must start with /'), description: Yup.string(), dependencies: Yup.array().of(Yup.string()), code: Yup.string(), @@ -32,6 +33,7 @@ const ComponentEditor: React.FC = () => { // Initial values for Formik const [initialValues, setInitialValues] = useState({ name: '', + routePath: '', description: '', dependencies: [] as string[], code: '', @@ -54,6 +56,7 @@ const ComponentEditor: React.FC = () => { const values = { name: component.name, + routePath: component.routePath, description: component.description || '', dependencies: deps, code: component.code, @@ -97,6 +100,7 @@ export default ${pascalCaseName}Component;` try { const componentData = { name: values.name.trim(), + routePath: values.routePath.trim(), description: values.description.trim(), dependencies: JSON.stringify(values.dependencies), // Serialize dependencies to JSON string code: values.code.trim(), @@ -104,9 +108,9 @@ export default ${pascalCaseName}Component;` } if (isEditing && id) { - updateComponent(id, componentData) + await updateComponent(id, componentData) } else { - addComponent(componentData) + await addComponent(componentData) } navigate(ROUTES_ENUM.protected.saas.developerKit.components) @@ -240,6 +244,19 @@ export default ${pascalCaseName}Component;` /> + + + + { {/* Sol taraf */}
-

{component.name}

+

+ {component.name} +

{ />
-

+

{(() => { try { const parsed = JSON.parse(component.dependencies ?? '[]') @@ -181,8 +183,14 @@ const ComponentManager: React.FC = () => { })()}

+

+ {component.routePath} +

+ {component.description && ( -

{component.description}

+

+ {component.description} +

)}
@@ -249,7 +257,7 @@ const ComponentManager: React.FC = () => { } title={translate('::App.DeveloperKit.Component.Edit')} > - +
diff --git a/ui/src/views/developerKit/SqlQueryManager.tsx b/ui/src/views/developerKit/SqlQueryManager.tsx index c952083..4351e1a 100644 --- a/ui/src/views/developerKit/SqlQueryManager.tsx +++ b/ui/src/views/developerKit/SqlQueryManager.tsx @@ -1,4 +1,4 @@ -import { useState, useCallback, useEffect, useRef } from 'react' +import { lazy, Suspense, useState, useCallback, useEffect, useRef } from 'react' import type { Dispatch, SetStateAction } from 'react' import { Button, Checkbox, Dialog, Notification, toast } from '@/components/ui' import Container from '@/components/shared/Container' @@ -18,13 +18,11 @@ import { FaArrowRight, FaEye, FaCheckCircle, - FaFolderOpen + FaFolderOpen, } from 'react-icons/fa' import { useLocalization } from '@/utils/hooks/useLocalization' import SqlObjectExplorer, { type SqlExplorerSelectedObject } from './SqlObjectExplorer' import SqlEditor, { SqlEditorRef } from './SqlEditor' -import SqlResultsGrid from './SqlResultsGrid' -import SqlTableDesignerDialog from './SqlTableDesignerDialog' import { Splitter } from '@/components/codeLayout/Splitter' import { Helmet } from 'react-helmet' import { useStoreState } from '@/store/store' @@ -32,6 +30,9 @@ import { APP_NAME } from '@/constants/app.constant' import { UiEvalService } from '@/services/UiEvalService' import { FcAcceptDatabase } from 'react-icons/fc' +const SqlResultsGrid = lazy(() => import('./SqlResultsGrid')) +const SqlTableDesignerDialog = lazy(() => import('./SqlTableDesignerDialog')) + interface SqlManagerState { dataSources: DataSourceDto[] selectedDataSource: string | null @@ -1345,13 +1346,15 @@ GO`,
- + {translate('::App.Loading')}
}> + + @@ -1412,9 +1415,7 @@ GO`, {isLoadingSqlDataFiles ? ( -

- {translate('::App.Loading')} -

+

{translate('::App.Loading')}

) : (
{renderSqlDataPane( @@ -1501,18 +1502,22 @@ GO`, {/* Table Designer Dialog */} - { - setShowTableDesignerDialog(false) - setDesignTableData(null) - }} - dataSource={state.selectedDataSource ?? ''} - initialTableData={designTableData} - onDeployed={() => { - setState((prev) => ({ ...prev, refreshTrigger: prev.refreshTrigger + 1 })) - }} - /> + {showTableDesignerDialog && ( + + { + setShowTableDesignerDialog(false) + setDesignTableData(null) + }} + dataSource={state.selectedDataSource ?? ''} + initialTableData={designTableData} + onDeployed={() => { + setState((prev) => ({ ...prev, refreshTrigger: prev.refreshTrigger + 1 })) + }} + /> + + )} createCategory(data).then(() => {})} onUpdateCategory={(id, data) => updateCategory(id, data).then(() => {})} diff --git a/ui/src/views/forum/admin/AdminView.tsx b/ui/src/views/forum/admin/AdminView.tsx index 209af28..e51e855 100644 --- a/ui/src/views/forum/admin/AdminView.tsx +++ b/ui/src/views/forum/admin/AdminView.tsx @@ -12,6 +12,7 @@ interface AdminViewProps { categories: ForumCategory[] topics: ForumTopic[] posts: ForumPost[] + totalCounts: { categories: number; topics: number; posts: number } loading: boolean onCreateCategory: (category: { name: string @@ -54,6 +55,7 @@ export function AdminView({ categories, topics, posts, + totalCounts, loading, onCreateCategory, onUpdateCategory, @@ -128,7 +130,7 @@ export function AdminView({ {/* Main Content */}
{activeSection === 'stats' && ( - + )} {activeSection === 'categories' && ( @@ -146,6 +148,7 @@ export function AdminView({ {activeSection === 'topics' && ( c.isActive).length - const totalTopics = topics.length + const totalTopics = totalCounts.topics const solvedTopics = topics.filter((t) => t.isSolved).length - const totalPosts = posts.length + const totalPosts = totalCounts.posts const acceptedAnswers = posts.filter((p) => p.isAcceptedAnswer).length const stats = [ diff --git a/ui/src/views/forum/admin/PostManagement.tsx b/ui/src/views/forum/admin/PostManagement.tsx index 597da61..9dcc898 100644 --- a/ui/src/views/forum/admin/PostManagement.tsx +++ b/ui/src/views/forum/admin/PostManagement.tsx @@ -28,6 +28,7 @@ import { interface PostManagementProps { posts: ForumPost[] + totalCount: number topics: ForumTopic[] loading: boolean onCreatePost: (post: { @@ -44,6 +45,7 @@ interface PostManagementProps { export function PostManagement({ posts, + totalCount, topics, loading, onCreatePost, @@ -270,7 +272,7 @@ export function PostManagement({ {/* Posts List */}
-

Posts ({posts.length})

+

Posts ({totalCount})

)}
diff --git a/ui/src/views/list/Grid.tsx b/ui/src/views/list/Grid.tsx index 2e1ae40..460d00f 100644 --- a/ui/src/views/list/Grid.tsx +++ b/ui/src/views/list/Grid.tsx @@ -1,4 +1,5 @@ import Container from '@/components/shared/Container' +import '@/utils/registerDevExtremeEditors' import { Dialog, Notification, toast } from '@/components/ui' import { APP_NAME, DX_CLASSNAMES } from '@/constants/app.constant' import { diff --git a/ui/src/views/list/List.tsx b/ui/src/views/list/List.tsx index 53c4f09..94e62c1 100644 --- a/ui/src/views/list/List.tsx +++ b/ui/src/views/list/List.tsx @@ -1,4 +1,5 @@ import React, { Suspense, startTransition, useCallback, useEffect, useMemo, useState } from 'react' +import '@/utils/registerDevExtremeEditors' import { useParams, useSearchParams } from 'react-router-dom' import classNames from 'classnames' diff --git a/ui/src/views/list/SchedulerView.tsx b/ui/src/views/list/SchedulerView.tsx index f20de1a..83b9d8d 100644 --- a/ui/src/views/list/SchedulerView.tsx +++ b/ui/src/views/list/SchedulerView.tsx @@ -1,4 +1,5 @@ import Container from '@/components/shared/Container' +import '@/utils/registerDevExtremeEditors' import { APP_NAME, DX_CLASSNAMES } from '@/constants/app.constant' import { DbTypeEnum, diff --git a/ui/src/views/list/Tree.tsx b/ui/src/views/list/Tree.tsx index eac18e3..86ae448 100644 --- a/ui/src/views/list/Tree.tsx +++ b/ui/src/views/list/Tree.tsx @@ -1,4 +1,5 @@ import Container from '@/components/shared/Container' +import '@/utils/registerDevExtremeEditors' import { Dialog, Notification, toast } from '@/components/ui' import { APP_NAME, DX_CLASSNAMES } from '@/constants/app.constant' import { executeEditorScript } from '@/utils/editorScriptRuntime' diff --git a/ui/vite.config.ts b/ui/vite.config.ts index 72ba039..9feee8f 100644 --- a/ui/vite.config.ts +++ b/ui/vite.config.ts @@ -31,7 +31,9 @@ export default defineConfig(async ({ mode }) => { mode === 'production' ? { globDirectory: 'dist', - globPatterns: ['**/*.{js,css,html,wasm}'], + // Precache only the application shell. Feature chunks and the + // many optional DevExtreme themes are cached on first use. + globPatterns: ['index.html', 'assets/css/index-*.css', 'assets/js/index-*.js'], // Büyük asset'leri de cache'leyebil maximumFileSizeToCacheInBytes: 15 * 1024 * 1024, @@ -49,14 +51,28 @@ export default defineConfig(async ({ mode }) => { // ⭐⭐ BU KISMI EKLEYİN: Cache sorununu çözecek runtime caching runtimeCaching: [ { - urlPattern: /\.(?:js|css|json)$/, + urlPattern: /\.(?:js|css|wasm)$/, + handler: 'CacheFirst', + options: { + cacheName: 'static-resources-v2', + expiration: { + maxEntries: 200, + maxAgeSeconds: 30 * 24 * 60 * 60, + }, + cacheableResponse: { + statuses: [0, 200], + }, + }, + }, + { + urlPattern: /\.json$/, handler: 'NetworkFirst', options: { - cacheName: 'static-resources-v1', - networkTimeoutSeconds: 10, + cacheName: 'json-resources-v1', + networkTimeoutSeconds: 5, expiration: { maxEntries: 50, - maxAgeSeconds: 24 * 60 * 60, // 24 saat + maxAgeSeconds: 24 * 60 * 60, }, cacheableResponse: { statuses: [0, 200], @@ -119,8 +135,8 @@ export default defineConfig(async ({ mode }) => { port: 3000, // ⭐ YENİ EKLENEN: Hot reload için polling watch: { - usePolling: true, - interval: 1000, + usePolling: env.VITE_USE_POLLING === 'true', + interval: env.VITE_USE_POLLING === 'true' ? 1000 : undefined, }, }, @@ -152,41 +168,6 @@ export default defineConfig(async ({ mode }) => { } return 'assets/[name]-[hash][extname]' }, - manualChunks(id) { - // 🔴 DEVEXTREME (esm + non-esm + react wrapper) - if ( - id.includes('node_modules/devextreme') || - id.includes('node_modules/devextreme-react') - ) { - return 'dx-framework' - } - - // 🔴 REPORTING - if (id.includes('devexpress-reporting') || id.includes('@devexpress')) { - return 'dx-reporting' - } - - // 🟣 EXPORT TOOLS - if ( - id.includes('exceljs') || - id.includes('xlsx') || - id.includes('jspdf') || - id.includes('html2canvas') || - id.includes('file-saver') - ) { - return 'export-tools' - } - - // 🟡 EDITOR - if (id.includes('monaco-editor') || id.includes('codemirror')) { - return 'editor' - } - - // ⚪ KALAN VENDOR - if (id.includes('node_modules')) { - return 'vendor' - } - }, }, }, },