Codex 5.6 Sol Genel Optimizasyon
This commit is contained in:
parent
351eddfc0a
commit
e7be51a1be
48 changed files with 678 additions and 485 deletions
|
|
@ -153,6 +153,32 @@ public class NotificationAppService : ReadOnlyAppService<
|
|||
return ObjectMapper.Map<Notification, NotificationDto>(item);
|
||||
}
|
||||
|
||||
public async Task<int> UpdateReadManyAsync(List<Guid> 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<int> UpdateSentManyAsync(List<Guid> 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<NotificationDto> GetAsync(Guid id) => throw new NotImplementedException();
|
||||
[RemoteService(false)]
|
||||
|
|
|
|||
|
|
@ -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<Guid>
|
||||
{
|
||||
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<Guid>
|
|||
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; }
|
||||
|
|
|
|||
|
|
@ -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<ForumCategoryDto>(
|
||||
totalCount,
|
||||
ObjectMapper.Map<List<ForumCategory>, List<ForumCategoryDto>>(categories)
|
||||
);
|
||||
var categoryDtos = ObjectMapper.Map<List<ForumCategory>, List<ForumCategoryDto>>(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<ForumCategoryDto>(totalCount, categoryDtos);
|
||||
}
|
||||
|
||||
public async Task<ForumCategoryDto> 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<ForumTopicDto>(
|
||||
totalCount,
|
||||
ObjectMapper.Map<List<ForumTopic>, List<ForumTopicDto>>(topics)
|
||||
);
|
||||
var topicDtos = ObjectMapper.Map<List<ForumTopic>, List<ForumTopicDto>>(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<ForumTopicDto>(totalCount, topicDtos);
|
||||
}
|
||||
|
||||
public async Task<ForumTopicDto> 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();
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ namespace Sozsoft.Platform.Intranet;
|
|||
[Authorize]
|
||||
public class IntranetAppService : PlatformAppService, IIntranetAppService
|
||||
{
|
||||
private const int DashboardItemLimit = 50;
|
||||
private Task<(Dictionary<Guid, string> DepartmentDict, Dictionary<Guid, JobPosition> 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<Event, Guid> _eventRepository;
|
||||
private readonly IIdentityUserAppService _identityUserAppService;
|
||||
private readonly IIdentityUserRepository _identityUserRepository;
|
||||
private readonly IRepository<Volo.Abp.Identity.IdentityUser, Guid> _identityUserEntityRepository;
|
||||
private readonly IRepository<Department, Guid> _departmentRepository;
|
||||
private readonly IRepository<JobPosition, Guid> _jobPositionRepository;
|
||||
private readonly IRepository<Announcement, Guid> _announcementRepository;
|
||||
|
|
@ -57,6 +60,7 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService
|
|||
IRepository<Event, Guid> eventRepository,
|
||||
IIdentityUserAppService identityUserAppService,
|
||||
IIdentityUserRepository identityUserRepository,
|
||||
IRepository<Volo.Abp.Identity.IdentityUser, Guid> identityUserEntityRepository,
|
||||
IRepository<Department, Guid> departmentRepository,
|
||||
IRepository<JobPosition, Guid> jobPositionRepository,
|
||||
IRepository<Announcement, Guid> 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<Guid, string> DepartmentDict, Dictionary<Guid, JobPosition> JobPositionDict)> GetUserLookupDictionariesAsync()
|
||||
private Task<(Dictionary<Guid, string> DepartmentDict, Dictionary<Guid, JobPosition> JobPositionDict)> GetUserLookupDictionariesAsync()
|
||||
{
|
||||
return _userLookupDictionariesTask ??= LoadUserLookupDictionariesAsync();
|
||||
}
|
||||
|
||||
private async Task<(Dictionary<Guid, string> DepartmentDict, Dictionary<Guid, JobPosition> JobPositionDict)> LoadUserLookupDictionariesAsync()
|
||||
{
|
||||
var departments = await _departmentRepository.GetListAsync();
|
||||
var jobPositions = await _jobPositionRepository.GetListAsync();
|
||||
|
|
@ -122,6 +132,17 @@ public class IntranetAppService : PlatformAppService, IIntranetAppService
|
|||
);
|
||||
}
|
||||
|
||||
private async Task<List<Volo.Abp.Identity.IdentityUser>> GetUsersByIdsAsync(IEnumerable<Guid> 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<Guid, string> 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<List<AnnouncementDto>> 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<AnnouncementDto>();
|
||||
|
||||
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))
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ public class MessengerAppService : ApplicationService
|
|||
private readonly IRepository<IdentityUser, Guid> _userRepository;
|
||||
private readonly IRepository<MessengerConversation, Guid> _conversationRepository;
|
||||
private readonly IRepository<MessengerConversationMessage, Guid> _messageRepository;
|
||||
private readonly IIdentitySessionRepository _identitySessionRepository;
|
||||
private readonly IRepository<IdentitySession, Guid> _identitySessionRepository;
|
||||
private readonly BlobManager _blobManager;
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly IHubContext<MessengerHub> _messengerHubContext;
|
||||
|
|
@ -37,7 +37,7 @@ public class MessengerAppService : ApplicationService
|
|||
IRepository<IdentityUser, Guid> userRepository,
|
||||
IRepository<MessengerConversation, Guid> conversationRepository,
|
||||
IRepository<MessengerConversationMessage, Guid> messageRepository,
|
||||
IIdentitySessionRepository identitySessionRepository,
|
||||
IRepository<IdentitySession, Guid> identitySessionRepository,
|
||||
BlobManager blobManager,
|
||||
IConfiguration configuration,
|
||||
IHubContext<MessengerHub> messengerHubContext)
|
||||
|
|
@ -450,11 +450,14 @@ public class MessengerAppService : ApplicationService
|
|||
return new HashSet<Guid>();
|
||||
}
|
||||
|
||||
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<MessengerConversation?> FindConversationByParticipantKeyAsync(string participantKey)
|
||||
|
|
|
|||
|
|
@ -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<Guid, JobPosition>();
|
||||
|
||||
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<Guid, List<OrgChartUserDto>> GroupUsersByJobPosition(
|
||||
IEnumerable<IdentityUser> 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());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -1,13 +1,5 @@
|
|||
{
|
||||
"Routes": [
|
||||
{
|
||||
"key": "roleListComponent",
|
||||
"path": "/admin/RoleListComponent",
|
||||
"componentType": "dynamic",
|
||||
"componentPath": "RoleListComponent",
|
||||
"routeType": "protected",
|
||||
"authority": []
|
||||
},
|
||||
{
|
||||
"key": "home",
|
||||
"path": "/home",
|
||||
|
|
|
|||
|
|
@ -9,15 +9,17 @@ public class CustomComponent : FullAuditedEntity<Guid>, 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;
|
||||
|
|
|
|||
|
|
@ -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<ReportCategory>(b =>
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore;
|
|||
namespace Sozsoft.Platform.Migrations
|
||||
{
|
||||
[DbContext(typeof(PlatformDbContext))]
|
||||
[Migration("20260709175406_Initial")]
|
||||
[Migration("20260711200250_Initial")]
|
||||
partial class Initial
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
|
@ -1826,6 +1826,11 @@ namespace Sozsoft.Platform.Migrations
|
|||
.HasMaxLength(1024)
|
||||
.HasColumnType("nvarchar(1024)");
|
||||
|
||||
b.Property<string>("RoutePath")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("nvarchar(512)");
|
||||
|
||||
b.Property<Guid?>("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);
|
||||
});
|
||||
|
||||
|
|
@ -1128,6 +1128,7 @@ namespace Sozsoft.Platform.Migrations
|
|||
Id = table.Column<Guid>(type: "uniqueidentifier", nullable: false),
|
||||
TenantId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
Name = table.Column<string>(type: "nvarchar(128)", maxLength: 128, nullable: false),
|
||||
RoutePath = table.Column<string>(type: "nvarchar(512)", maxLength: 512, nullable: false),
|
||||
Code = table.Column<string>(type: "nvarchar(max)", nullable: false),
|
||||
Props = table.Column<string>(type: "nvarchar(1024)", maxLength: 1024, nullable: true),
|
||||
Description = table.Column<string>(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",
|
||||
|
|
@ -1823,6 +1823,11 @@ namespace Sozsoft.Platform.Migrations
|
|||
.HasMaxLength(1024)
|
||||
.HasColumnType("nvarchar(1024)");
|
||||
|
||||
b.Property<string>("RoutePath")
|
||||
.IsRequired()
|
||||
.HasMaxLength(512)
|
||||
.HasColumnType("nvarchar(512)");
|
||||
|
||||
b.Property<Guid?>("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);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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<DynamicEntityComponentProps> = ({ title }) => {\n const [data, setData] = useState<Array<{ id: string; name: string }>>([]);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<string | null>(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 <div>Loading...</div>;\n if (error) return <div className=\"text-red-600 dark:text-red-400\">Error: {error}</div>;\n if (!data.length) return <div>No records found</div>;\n\n const headers = [\"id\", \"name\", \"actions\"];\n\n return (\n <div className=\"overflow-auto\">\n <table className=\"min-w-full bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-700 shadow-sm rounded-lg\">\n <thead className=\"bg-slate-100 dark:bg-slate-800\">\n <tr>\n {headers.map((key) => (\n <th\n key={key}\n className=\"text-left px-4 py-2 border-b border-slate-200 dark:border-slate-700 text-sm font-medium text-slate-700 dark:text-slate-200\"\n >\n {key === \"actions\" ? \"Actions\" : key}\n </th>\n ))}\n </tr>\n </thead>\n <tbody>\n {data.map((item, rowIndex) => (\n <tr key={rowIndex} className=\"hover:bg-slate-50 dark:hover:bg-slate-800\">\n <td className=\"px-4 py-2 border-b border-slate-100 dark:border-slate-800 text-sm text-slate-800 dark:text-slate-100\">\n {item.id}\n </td>\n <td className=\"px-4 py-2 border-b border-slate-100 dark:border-slate-800 text-sm text-slate-800 dark:text-slate-100\">\n {item.name}\n </td>\n <td className=\"px-4 py-2 border-b border-slate-100 dark:border-slate-800\">\n <button\n type=\"button\"\n onClick={() => 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 </button>\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\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 <DynamicEntityComponent id=\"c_mdljvvmq_fno52v\" title={title} />\n );\n};\n\nexport default RoleListComponent;",
|
||||
"props": null,
|
||||
"description": null,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<BrotliCompressionProvider>();
|
||||
options.Providers.Add<GzipCompressionProvider>();
|
||||
});
|
||||
|
||||
ConfigureDynamicServices(context);
|
||||
ConfigureDevExpressReporting(context, configuration);
|
||||
|
||||
context.Services.AddSignalR();
|
||||
var signalR = context.Services.AddSignalR();
|
||||
if (configuration.GetValue<bool>("Redis:IsEnabled") &&
|
||||
!configuration["Redis:Configuration"].IsNullOrWhiteSpace())
|
||||
{
|
||||
signalR.AddStackExchangeRedis(configuration["Redis:Configuration"]!);
|
||||
}
|
||||
|
||||
Configure<AbpExceptionHttpStatusCodeOptions>(options =>
|
||||
{
|
||||
|
|
@ -489,6 +502,7 @@ public class PlatformHttpApiHostModule : AbpModule
|
|||
// }
|
||||
|
||||
app.UseCorrelationId();
|
||||
app.UseResponseCompression();
|
||||
app.MapAbpStaticAssets();
|
||||
app.UseRouting();
|
||||
app.UseCors();
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@
|
|||
<PackageReference Include="Serilog.Settings.Configuration" Version="10.0.0" />
|
||||
<PackageReference Include="Serilog.Sinks.PostgreSQL" Version="2.3.0" />
|
||||
<PackageReference Include="Serilog.Sinks.MSSqlServer" Version="8.2.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.StackExchangeRedis" Version="10.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"commit": "c42a8d2",
|
||||
"commit": "351eddf",
|
||||
"releases": [
|
||||
{
|
||||
"version": "1.1.06",
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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<Props, State> {
|
||||
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 (
|
||||
<div
|
||||
style={{
|
||||
color: "red",
|
||||
background: "#fff0f0",
|
||||
border: "1px solid #f44336",
|
||||
padding: "1rem",
|
||||
borderRadius: "8px",
|
||||
fontFamily: "monospace",
|
||||
whiteSpace: "pre-wrap",
|
||||
}}
|
||||
>
|
||||
⚠️ Render Hatası:
|
||||
<br />
|
||||
<details open style={{ marginTop: "0.5rem" }}>
|
||||
<summary>Detayları Göster</summary>
|
||||
{this.state.error?.message || String(this.state.error)}
|
||||
</details>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
export default ErrorBoundary;
|
||||
|
|
@ -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 }) => {
|
|||
<div className="relative border-t border-gray-200 p-3 dark:border-gray-700">
|
||||
{showEmoji && (
|
||||
<div className="absolute bottom-16 left-2 z-10 sm:left-10">
|
||||
<EmojiPicker
|
||||
onEmojiClick={onEmojiClick}
|
||||
width="min(320px, calc(100vw - 32px))"
|
||||
height={380}
|
||||
/>
|
||||
<Suspense fallback={<div className="h-[380px] w-[320px] max-w-[calc(100vw-32px)]" />}>
|
||||
<EmojiPicker
|
||||
onEmojiClick={onEmojiClick}
|
||||
width="min(320px, calc(100vw - 32px))"
|
||||
height={380}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex min-h-[52px] items-center gap-2 rounded-md border border-gray-300 bg-white px-2 py-1.5 focus-within:border-emerald-500 focus-within:ring-1 focus-within:ring-emerald-500 dark:border-gray-600 dark:bg-gray-800">
|
||||
|
|
|
|||
|
|
@ -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 }) => {
|
|||
</Notify>,
|
||||
{ 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(() => {
|
||||
|
|
|
|||
|
|
@ -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])
|
||||
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<CustomComponent, 'id' | 'name' | 'routePath' | 'isActive'>[],
|
||||
): 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,
|
||||
}))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 <div>Loading...</div>
|
||||
|
|
|
|||
|
|
@ -36,6 +36,22 @@ export const updateSent = (notificationId: string, isSent: boolean) =>
|
|||
params: { isSent },
|
||||
})
|
||||
|
||||
export const updateReadMany = (notificationIds: string[], isRead: boolean) =>
|
||||
apiService.fetchData<number>({
|
||||
method: 'PUT',
|
||||
url: `/api/app/notification/read-many`,
|
||||
params: { isRead },
|
||||
data: notificationIds,
|
||||
})
|
||||
|
||||
export const updateSentMany = (notificationIds: string[], isSent: boolean) =>
|
||||
apiService.fetchData<number>({
|
||||
method: 'PUT',
|
||||
url: `/api/app/notification/sent-many`,
|
||||
params: { isSent },
|
||||
data: notificationIds,
|
||||
})
|
||||
|
||||
export const postMyNotificationByNotificationRuleId = (data: { id: string; message: string }) =>
|
||||
apiService.fetchData({
|
||||
method: 'POST',
|
||||
|
|
|
|||
|
|
@ -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<SearchResult>({
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
11
ui/src/utils/registerDevExtremeEditors.ts
Normal file
11
ui/src/utils/registerDevExtremeEditors.ts
Normal file
|
|
@ -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'
|
||||
|
|
@ -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 }}
|
||||
>
|
||||
<div className="absolute inset-0 w-full h-full">
|
||||
<VideoPlayer
|
||||
stream={participant.stream}
|
||||
isLocal={participant.id === currentUserId}
|
||||
userName={participant.name}
|
||||
isAudioEnabled={
|
||||
participant.id === currentUserId ? isAudioEnabled : !participant.isAudioMuted
|
||||
}
|
||||
isVideoEnabled={
|
||||
participant.id === currentUserId ? isVideoEnabled : !participant.isVideoMuted
|
||||
}
|
||||
/>
|
||||
<Suspense fallback={<div className="h-full w-full bg-gray-900" />}>
|
||||
<VideoPlayer
|
||||
stream={participant.stream}
|
||||
isLocal={participant.id === currentUserId}
|
||||
userName={participant.name}
|
||||
isAudioEnabled={
|
||||
participant.id === currentUserId ? isAudioEnabled : !participant.isAudioMuted
|
||||
}
|
||||
isVideoEnabled={
|
||||
participant.id === currentUserId ? isVideoEnabled : !participant.isVideoMuted
|
||||
}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
{/* Teacher controls for students */}
|
||||
{isTeacher && participant.id !== currentUserId && (
|
||||
|
|
|
|||
|
|
@ -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;`
|
|||
/>
|
||||
</FormItem>
|
||||
|
||||
<FormItem
|
||||
label="Route Path"
|
||||
invalid={!!(errors.routePath && touched.routePath)}
|
||||
errorMessage={errors.routePath as string}
|
||||
>
|
||||
<Field
|
||||
name="routePath"
|
||||
type="text"
|
||||
component={Input}
|
||||
placeholder="e.g., /roles or /admin/reports"
|
||||
/>
|
||||
</FormItem>
|
||||
|
||||
<FormItem
|
||||
label={translate('::ListForms.ListFormEdit.DetailsDescription')}
|
||||
invalid={!!(errors.description && touched.description)}
|
||||
|
|
|
|||
|
|
@ -160,7 +160,9 @@ const ComponentManager: React.FC = () => {
|
|||
{/* Sol taraf */}
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<h3 className="text-lg font-semibold text-slate-900 dark:text-gray-100">{component.name}</h3>
|
||||
<h3 className="text-lg font-semibold text-slate-900 dark:text-gray-100">
|
||||
{component.name}
|
||||
</h3>
|
||||
<div
|
||||
className={`w-2 h-2 rounded-full ${
|
||||
component.isActive ? 'bg-green-500' : 'bg-slate-300 dark:bg-gray-700'
|
||||
|
|
@ -168,7 +170,7 @@ const ComponentManager: React.FC = () => {
|
|||
/>
|
||||
</div>
|
||||
|
||||
<p className="text-slate-600 dark:text-gray-300 text-sm mb-3">
|
||||
<p className="text-slate-600 dark:text-gray-300 text-sm mb-2">
|
||||
{(() => {
|
||||
try {
|
||||
const parsed = JSON.parse(component.dependencies ?? '[]')
|
||||
|
|
@ -181,8 +183,14 @@ const ComponentManager: React.FC = () => {
|
|||
})()}
|
||||
</p>
|
||||
|
||||
<p className="text-slate-600 dark:text-gray-300 text-sm mb-2">
|
||||
{component.routePath}
|
||||
</p>
|
||||
|
||||
{component.description && (
|
||||
<p className="text-slate-600 dark:text-gray-300 text-sm mb-3">{component.description}</p>
|
||||
<p className="text-slate-600 dark:text-gray-300 text-sm mb-2">
|
||||
{component.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
|
@ -249,7 +257,7 @@ const ComponentManager: React.FC = () => {
|
|||
}
|
||||
title={translate('::App.DeveloperKit.Component.Edit')}
|
||||
>
|
||||
<FaRegEdit className="w-4 h-4" />
|
||||
<FaRegEdit className="w-3 h-3" />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
|
|
@ -266,7 +274,7 @@ const ComponentManager: React.FC = () => {
|
|||
}
|
||||
title={translate('::App.DeveloperKit.Component.View')}
|
||||
>
|
||||
<FaEye className="w-4 h-4" />
|
||||
<FaEye className="w-3 h-3" />
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
|
|
@ -276,7 +284,7 @@ const ComponentManager: React.FC = () => {
|
|||
onClick={() => handleDelete(component.id)}
|
||||
title={translate('::App.DeveloperKit.Component.Delete')}
|
||||
>
|
||||
<FaTrashAlt className="w-4 h-4" />
|
||||
<FaTrashAlt className="w-3 h-3" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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`,
|
|||
</div>
|
||||
</div>
|
||||
<div className="flex-1 overflow-hidden p-2">
|
||||
<SqlResultsGrid
|
||||
result={(state.executionResult || state.tableColumns)!}
|
||||
queryText={state.lastExecutedQuery}
|
||||
dataSourceCode={state.selectedDataSource}
|
||||
isPostgreSql={isPostgreSql}
|
||||
onExecuteMutation={handleResultsMutation}
|
||||
/>
|
||||
<Suspense fallback={<div className="p-4">{translate('::App.Loading')}</div>}>
|
||||
<SqlResultsGrid
|
||||
result={(state.executionResult || state.tableColumns)!}
|
||||
queryText={state.lastExecutedQuery}
|
||||
dataSourceCode={state.selectedDataSource}
|
||||
isPostgreSql={isPostgreSql}
|
||||
onExecuteMutation={handleResultsMutation}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
</Splitter>
|
||||
|
|
@ -1412,9 +1415,7 @@ GO`,
|
|||
</div>
|
||||
|
||||
{isLoadingSqlDataFiles ? (
|
||||
<p className="mb-4 text-gray-600 dark:text-gray-400">
|
||||
{translate('::App.Loading')}
|
||||
</p>
|
||||
<p className="mb-4 text-gray-600 dark:text-gray-400">{translate('::App.Loading')}</p>
|
||||
) : (
|
||||
<div className="flex min-h-0 flex-1 flex-col gap-3 lg:flex-row">
|
||||
{renderSqlDataPane(
|
||||
|
|
@ -1501,18 +1502,22 @@ GO`,
|
|||
</Dialog>
|
||||
|
||||
{/* Table Designer Dialog */}
|
||||
<SqlTableDesignerDialog
|
||||
isOpen={showTableDesignerDialog}
|
||||
onClose={() => {
|
||||
setShowTableDesignerDialog(false)
|
||||
setDesignTableData(null)
|
||||
}}
|
||||
dataSource={state.selectedDataSource ?? ''}
|
||||
initialTableData={designTableData}
|
||||
onDeployed={() => {
|
||||
setState((prev) => ({ ...prev, refreshTrigger: prev.refreshTrigger + 1 }))
|
||||
}}
|
||||
/>
|
||||
{showTableDesignerDialog && (
|
||||
<Suspense fallback={null}>
|
||||
<SqlTableDesignerDialog
|
||||
isOpen
|
||||
onClose={() => {
|
||||
setShowTableDesignerDialog(false)
|
||||
setDesignTableData(null)
|
||||
}}
|
||||
dataSource={state.selectedDataSource ?? ''}
|
||||
initialTableData={designTableData}
|
||||
onDeployed={() => {
|
||||
setState((prev) => ({ ...prev, refreshTrigger: prev.refreshTrigger + 1 }))
|
||||
}}
|
||||
/>
|
||||
</Suspense>
|
||||
)}
|
||||
|
||||
<Dialog
|
||||
isOpen={showCopyDialog}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { DX_CLASSNAMES, MULTIVALUE_DELIMITER } from '@/constants/app.constant'
|
||||
import '@/utils/registerDevExtremeEditors'
|
||||
import { executeEditorScript } from '@/utils/editorScriptRuntime'
|
||||
import {
|
||||
Form as FormDx,
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ export function Management() {
|
|||
categories,
|
||||
topics,
|
||||
posts,
|
||||
totalCounts,
|
||||
loading,
|
||||
error,
|
||||
createCategory,
|
||||
|
|
@ -81,6 +82,7 @@ export function Management() {
|
|||
categories={categories}
|
||||
topics={topics}
|
||||
posts={posts}
|
||||
totalCounts={totalCounts}
|
||||
loading={loading}
|
||||
onCreateCategory={(data) => createCategory(data).then(() => {})}
|
||||
onUpdateCategory={(id, data) => updateCategory(id, data).then(() => {})}
|
||||
|
|
|
|||
|
|
@ -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 */}
|
||||
<div className="flex-1 bg-white dark:bg-gray-900 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 p-4">
|
||||
{activeSection === 'stats' && (
|
||||
<AdminStats categories={categories} topics={topics} posts={posts} />
|
||||
<AdminStats categories={categories} topics={topics} posts={posts} totalCounts={totalCounts} />
|
||||
)}
|
||||
|
||||
{activeSection === 'categories' && (
|
||||
|
|
@ -146,6 +148,7 @@ export function AdminView({
|
|||
{activeSection === 'topics' && (
|
||||
<TopicManagement
|
||||
topics={topics}
|
||||
totalCount={totalCounts.topics}
|
||||
categories={categories}
|
||||
loading={loading}
|
||||
onCreateTopic={onCreateTopic}
|
||||
|
|
@ -163,6 +166,7 @@ export function AdminView({
|
|||
{activeSection === 'posts' && (
|
||||
<PostManagement
|
||||
posts={posts}
|
||||
totalCount={totalCounts.posts}
|
||||
topics={topics}
|
||||
loading={loading}
|
||||
onCreatePost={onCreatePost}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ interface AdminStatsProps {
|
|||
categories: ForumCategory[]
|
||||
topics: ForumTopic[]
|
||||
posts: ForumPost[]
|
||||
totalCounts: { categories: number; topics: number; posts: number }
|
||||
}
|
||||
|
||||
interface Activity {
|
||||
|
|
@ -17,13 +18,13 @@ interface Activity {
|
|||
date: Date
|
||||
}
|
||||
|
||||
export function AdminStats({ categories, topics, posts }: AdminStatsProps) {
|
||||
export function AdminStats({ categories, topics, posts, totalCounts }: AdminStatsProps) {
|
||||
const { translate } = useLocalization()
|
||||
const totalCategories = categories.length
|
||||
const totalCategories = totalCounts.categories
|
||||
const activeCategories = categories.filter((c) => 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 = [
|
||||
|
|
|
|||
|
|
@ -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 */}
|
||||
<div className="bg-white dark:bg-gray-900 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
|
||||
<div className="flex items-center justify-between px-3 py-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">Posts ({posts.length})</h3>
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">Posts ({totalCount})</h3>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="solid"
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import { formatForumDate } from '../forum/utils'
|
|||
|
||||
interface TopicManagementProps {
|
||||
topics: ForumTopic[]
|
||||
totalCount: number
|
||||
categories: ForumCategory[]
|
||||
loading: boolean
|
||||
onCreateTopic: (topic: {
|
||||
|
|
@ -45,6 +46,7 @@ interface TopicManagementProps {
|
|||
|
||||
export function TopicManagement({
|
||||
topics,
|
||||
totalCount,
|
||||
categories,
|
||||
loading,
|
||||
onCreateTopic,
|
||||
|
|
@ -265,7 +267,7 @@ export function TopicManagement({
|
|||
<div className="bg-white dark:bg-gray-900 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
|
||||
<div className="flex items-center justify-between px-3 py-4 border-b border-gray-200 dark:border-gray-700">
|
||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
|
||||
{translate('::App.Forum.Dashboard.Topics')} ({topics.length})
|
||||
{translate('::App.Forum.Dashboard.Topics')} ({totalCount})
|
||||
</h3>
|
||||
<Button
|
||||
size="sm"
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ export function useForumData() {
|
|||
const [categories, setCategories] = useState<ForumCategory[]>([])
|
||||
const [topics, setTopics] = useState<ForumTopic[]>([])
|
||||
const [posts, setPosts] = useState<ForumPost[]>([])
|
||||
const [totalCounts, setTotalCounts] = useState({ categories: 0, topics: 0, posts: 0 })
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
|
|
@ -21,6 +22,7 @@ export function useForumData() {
|
|||
setLoading(true)
|
||||
const response = await forumService.getCategories()
|
||||
setCategories(response.items)
|
||||
setTotalCounts((prev) => ({ ...prev, categories: response.totalCount }))
|
||||
} catch (err) {
|
||||
setError('Failed to load categories')
|
||||
console.error('Error loading categories:', err)
|
||||
|
|
@ -34,6 +36,7 @@ export function useForumData() {
|
|||
setLoading(true)
|
||||
const response = await forumService.getTopics({ categoryId })
|
||||
setTopics(response.items)
|
||||
if (!categoryId) setTotalCounts((prev) => ({ ...prev, topics: response.totalCount }))
|
||||
} catch (err) {
|
||||
setError('Failed to load topics')
|
||||
console.error('Error loading topics:', err)
|
||||
|
|
@ -47,6 +50,7 @@ export function useForumData() {
|
|||
setLoading(true)
|
||||
const response = await forumService.getPosts({ topicId })
|
||||
setPosts(response.items)
|
||||
if (!topicId) setTotalCounts((prev) => ({ ...prev, posts: response.totalCount }))
|
||||
} catch (err) {
|
||||
setError('Failed to load posts')
|
||||
console.error('Error loading posts:', err)
|
||||
|
|
@ -61,6 +65,7 @@ export function useForumData() {
|
|||
setLoading(true)
|
||||
const newCategory = await forumService.createCategory(categoryData)
|
||||
setCategories((prev) => [...prev, newCategory])
|
||||
setTotalCounts((prev) => ({ ...prev, categories: prev.categories + 1 }))
|
||||
return newCategory
|
||||
} catch (err) {
|
||||
setError('Failed to create category')
|
||||
|
|
@ -100,12 +105,13 @@ export function useForumData() {
|
|||
try {
|
||||
setLoading(true)
|
||||
await forumService.deleteCategory(id)
|
||||
setCategories((prev) => prev.filter((cat) => cat.id !== id))
|
||||
// Also remove related topics and posts
|
||||
const topicsToDelete = topics.filter((topic) => topic.categoryId === id)
|
||||
const topicIds = topicsToDelete.map((t) => t.id)
|
||||
setCategories((prev) => prev.filter((cat) => cat.id !== id))
|
||||
setTopics((prev) => prev.filter((topic) => topic.categoryId !== id))
|
||||
setPosts((prev) => prev.filter((post) => !topicIds.includes(post.topicId)))
|
||||
await Promise.all([loadCategories(), loadTopics(), loadPosts()])
|
||||
} catch (err) {
|
||||
setError('Failed to delete category')
|
||||
console.error('Error deleting category:', err)
|
||||
|
|
@ -121,6 +127,7 @@ export function useForumData() {
|
|||
setLoading(true)
|
||||
const newTopic = await forumService.createTopic(topicData)
|
||||
setTopics((prev) => [...prev, newTopic])
|
||||
setTotalCounts((prev) => ({ ...prev, topics: prev.topics + 1 }))
|
||||
// Update category topic count
|
||||
setCategories((prev) =>
|
||||
prev.map((cat) =>
|
||||
|
|
@ -177,6 +184,7 @@ export function useForumData() {
|
|||
),
|
||||
)
|
||||
}
|
||||
await Promise.all([loadCategories(), loadTopics(), loadPosts()])
|
||||
} catch (err) {
|
||||
setError('Failed to delete topic')
|
||||
console.error('Error deleting topic:', err)
|
||||
|
|
@ -264,6 +272,7 @@ export function useForumData() {
|
|||
setLoading(true)
|
||||
const newPost = await forumService.createPost(postData)
|
||||
setPosts((prev) => [...prev, newPost])
|
||||
setTotalCounts((prev) => ({ ...prev, posts: prev.posts + 1 }))
|
||||
|
||||
// Update topic and category post counts
|
||||
const topic = topics.find((t) => t.id === postData.topicId)
|
||||
|
|
@ -309,6 +318,7 @@ export function useForumData() {
|
|||
const post = posts.find((p) => p.id === id)
|
||||
await forumService.deletePost(id)
|
||||
setPosts((prev) => prev.filter((p) => p.id !== id))
|
||||
setTotalCounts((prev) => ({ ...prev, posts: Math.max(0, prev.posts - 1) }))
|
||||
|
||||
// Update topic and category counts
|
||||
if (post) {
|
||||
|
|
@ -391,6 +401,7 @@ export function useForumData() {
|
|||
categories,
|
||||
topics,
|
||||
posts,
|
||||
totalCounts,
|
||||
loading,
|
||||
error,
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import React, { useState, useRef } from 'react'
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import classNames from 'classnames'
|
||||
import EmojiPicker, { EmojiClickData, Theme } from 'emoji-picker-react'
|
||||
import type { EmojiClickData, Theme } from 'emoji-picker-react'
|
||||
import { FaChartBar, FaSmile, FaTimes, FaImages, FaMapMarkerAlt } from 'react-icons/fa'
|
||||
import MediaManager from './MediaManager'
|
||||
import LocationPicker from './LocationPicker'
|
||||
|
|
@ -26,6 +26,8 @@ interface CreatePostProps {
|
|||
}) => void
|
||||
}
|
||||
|
||||
const EmojiPicker = React.lazy(() => import('emoji-picker-react'))
|
||||
|
||||
const CreatePost: React.FC<CreatePostProps> = ({ onCreatePost }) => {
|
||||
const { translate } = useLocalization()
|
||||
const [content, setContent] = useState('')
|
||||
|
|
@ -471,13 +473,15 @@ const CreatePost: React.FC<CreatePostProps> = ({ onCreatePost }) => {
|
|||
ref={emojiPickerRef}
|
||||
className="absolute bottom-6 left-0 z-50 bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 p-2"
|
||||
>
|
||||
<EmojiPicker
|
||||
searchDisabled
|
||||
theme={theme.mode === 'dark' ? Theme.DARK : Theme.LIGHT}
|
||||
height={350}
|
||||
onEmojiClick={handleEmojiClick}
|
||||
autoFocusSearch={false}
|
||||
/>
|
||||
<React.Suspense fallback={<div className="h-[350px] w-[350px] max-w-[calc(100vw-32px)]" />}>
|
||||
<EmojiPicker
|
||||
searchDisabled
|
||||
theme={(theme.mode === 'dark' ? 'dark' : 'light') as Theme}
|
||||
height={350}
|
||||
onEmojiClick={handleEmojiClick}
|
||||
autoFocusSearch={false}
|
||||
/>
|
||||
</React.Suspense>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import Container from '@/components/shared/Container'
|
||||
import '@/utils/registerDevExtremeEditors'
|
||||
import { APP_NAME, DX_CLASSNAMES } from '@/constants/app.constant'
|
||||
import {
|
||||
DbTypeEnum,
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in a new issue