Codex 5.6 Sol Genel Optimizasyon

This commit is contained in:
Sedat Öztürk 2026-07-11 23:35:32 +03:00
parent 351eddfc0a
commit e7be51a1be
48 changed files with 678 additions and 485 deletions

View file

@ -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)]

View file

@ -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; }

View file

@ -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();

View file

@ -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,

View file

@ -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))

View file

@ -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)

View file

@ -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());
}
}

View file

@ -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,

View file

@ -1,13 +1,5 @@
{
"Routes": [
{
"key": "roleListComponent",
"path": "/admin/RoleListComponent",
"componentType": "dynamic",
"componentPath": "RoleListComponent",
"routeType": "protected",
"authority": []
},
{
"key": "home",
"path": "/home",

View file

@ -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;

View file

@ -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 =>

View file

@ -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);
});

View file

@ -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",

View file

@ -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);
});

View file

@ -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,

View file

@ -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,

View file

@ -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();

View file

@ -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>

View file

@ -1,5 +1,5 @@
{
"commit": "c42a8d2",
"commit": "351eddf",
"releases": [
{
"version": "1.1.06",

View file

@ -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(() => {

View file

@ -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;

View file

@ -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">

View file

@ -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(() => {

View file

@ -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])

View file

@ -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'

View file

@ -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
}
}

View file

@ -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,
}))
}

View file

@ -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>

View file

@ -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',

View file

@ -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
};
}
}

View 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'

View file

@ -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 && (

View file

@ -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)}

View file

@ -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>

View file

@ -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}

View file

@ -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,

View file

@ -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(() => {})}

View file

@ -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}

View file

@ -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 = [

View file

@ -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"

View file

@ -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"

View file

@ -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,

View file

@ -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>

View file

@ -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 {

View file

@ -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'

View file

@ -1,4 +1,5 @@
import Container from '@/components/shared/Container'
import '@/utils/registerDevExtremeEditors'
import { APP_NAME, DX_CLASSNAMES } from '@/constants/app.constant'
import {
DbTypeEnum,

View file

@ -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'

View file

@ -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'
}
},
},
},
},