seed/data Seeder Forum
This commit is contained in:
parent
ed143c01d0
commit
3ec9a94860
23 changed files with 11652 additions and 12745 deletions
|
|
@ -1184,7 +1184,10 @@ soneklerini alır.
|
||||||
| **Developer Kit › Components** | `App.DeveloperKit.Components` · `/admin/developerkit/components` | `Sas_H_CustomComponent` | host | Custom component listesi ve **Visual Designer** girişi (§10). |
|
| **Developer Kit › Components** | `App.DeveloperKit.Components` · `/admin/developerkit/components` | `Sas_H_CustomComponent` | host | Custom component listesi ve **Visual Designer** girişi (§10). |
|
||||||
| **Developer Kit › ListForm** | `App.Listforms.Listform` | `Sas_H_ListForm` · `Sas_H_ListFormField` · `Sas_H_ListFormWorkflow` · `Sas_H_ListFormCustomization` · `Sas_H_ListFormImport` | host | Ekran tanımlarının kendisi. Bu dosyada anlatılan bütün JSON kolonlarının canlı hâli; düzenleyicisi `/admin/listform/edit/{kod}`. |
|
| **Developer Kit › ListForm** | `App.Listforms.Listform` | `Sas_H_ListForm` · `Sas_H_ListFormField` · `Sas_H_ListFormWorkflow` · `Sas_H_ListFormCustomization` · `Sas_H_ListFormImport` | host | Ekran tanımlarının kendisi. Bu dosyada anlatılan bütün JSON kolonlarının canlı hâli; düzenleyicisi `/admin/listform/edit/{kod}`. |
|
||||||
| **Developer Kit › Wizard Manager** | menü kodu `App.Listforms.WizardManager` · yetki `App.Listforms.Wizard` · `/admin/listform/wizardManager` | — | host | Wizard seed dosyalarının listesi, düzenlemesi, silinmesi, export/import'u. Sihirbazın kendisi ayrı bir rotadadır: `/admin/listform/wizard`. |
|
| **Developer Kit › Wizard Manager** | menü kodu `App.Listforms.WizardManager` · yetki `App.Listforms.Wizard` · `/admin/listform/wizardManager` | — | host | Wizard seed dosyalarının listesi, düzenlemesi, silinmesi, export/import'u. Sihirbazın kendisi ayrı bir rotadadır: `/admin/listform/wizard`. |
|
||||||
| **Forum Management** | `App.ForumManagement` · `/admin/forumManagement` | `Sas_T_ForumCategory` · `Sas_T_ForumTopic` · `Sas_T_ForumPost` | tenant | Forum kategori/konu yönetimi; yayın tarafı Administration'daki `/admin/forum`. |
|
| **Forum Management** | `App.ForumManagement` (grup menüsü) | `Sas_T_ForumCategory` · `Sas_T_ForumTopic` · `Sas_T_ForumPost` | tenant | Altında Categories / Topics / Posts ListForm ekranları; yayın tarafı Administration'daki `/admin/forum`. |
|
||||||
|
| **Forum Categories** | `App.ForumManagement.Categories` · `/admin/list/App.ForumManagement.Categories` | `Sas_T_ForumCategory` | tenant | Forum kategorilerinin ListForm ekranı; veri `configs/seeds/host/data/App.ForumManagement.Categories.json` dosyasından seed edilir. |
|
||||||
|
| **Forum Topics** | `App.ForumManagement.Topics` · `/admin/list/App.ForumManagement.Topics` | `Sas_T_ForumTopic` | tenant | Forum konuları; kategori lookup'ı, sayaçlar ve sabitle/kilitle/çözüldü bayrakları. |
|
||||||
|
| **Forum Posts** | `App.ForumManagement.Posts` · `/admin/list/App.ForumManagement.Posts` | `Sas_T_ForumPost` | tenant | Forum gönderileri; konu lookup'ı, kabul edilen yanıt bayrağı. |
|
||||||
|
|
||||||
> Menü kodu ile yetki adı **her zaman aynı değildir**: Wizard Manager'da kod
|
> Menü kodu ile yetki adı **her zaman aynı değildir**: Wizard Manager'da kod
|
||||||
> `App.Listforms.WizardManager`, yetki `App.Listforms.Wizard`'dır; Administration'daki Forum
|
> `App.Listforms.WizardManager`, yetki `App.Listforms.Wizard`'dır; Administration'daki Forum
|
||||||
|
|
@ -1274,7 +1277,7 @@ görünürlük `checkPermission` ile bileşen içinde kurulur (`lowcode` §7.5).
|
||||||
| `/admin/settings` | `settings/Settings` | `App.Setting` |
|
| `/admin/settings` | `settings/Settings` | `App.Setting` |
|
||||||
| `/admin/files` | `admin/files/FileManager` | `App.Files` |
|
| `/admin/files` | `admin/files/FileManager` | `App.Files` |
|
||||||
| `/admin/ous` · `/admin/organization` | `organization-unit/OrganizationUnits` · `hr/OrgChart` | `Abp.Identity.OrganizationUnits` · `App.Definitions.Department` |
|
| `/admin/ous` · `/admin/organization` | `organization-unit/OrganizationUnits` · `hr/OrgChart` | `Abp.Identity.OrganizationUnits` · `App.Definitions.Department` |
|
||||||
| `/admin/forum` · `/admin/forumManagement` | `forum/Forum` · `forum/Management` | `App.ForumManagement.Publish` · `App.ForumManagement` |
|
| `/admin/forum` | `forum/Forum` | `App.ForumManagement.Publish` |
|
||||||
| `/admin/videoroom/{dashboard,list,room/:id}` | `admin/videoroom/{Dashboard,RoomList,RoomDetail}` | `App.Videoroom{,.List,.Detail}` |
|
| `/admin/videoroom/{dashboard,list,room/:id}` | `admin/videoroom/{Dashboard,RoomList,RoomDetail}` | `App.Videoroom{,.List,.Detail}` |
|
||||||
| `/admin/ai` | `ai/Assistant` | `App.Definitions.AiBot.Asistant` |
|
| `/admin/ai` | `ai/Assistant` | `App.Definitions.AiBot.Asistant` |
|
||||||
| `/admin/dashboard` · `/admin/activityLog` · `/admin/changeLog` · `/admin/profile/*` | `Dashboard` · `admin/activityLog/ActivityLog` · `version/ChangeLog` · `admin/profile/Profile` | `[]` |
|
| `/admin/dashboard` · `/admin/activityLog` · `/admin/changeLog` · `/admin/profile/*` | `Dashboard` · `admin/activityLog/ActivityLog` · `version/ChangeLog` · `admin/profile/Profile` | `[]` |
|
||||||
|
|
|
||||||
|
|
@ -800,7 +800,7 @@ içindedir; oradaki kontroller yalnızca butonları gizler, asıl kontrol AppSer
|
||||||
| Dosya yöneticisi | `/admin/files` | Klasör/dosya işlemleri. |
|
| Dosya yöneticisi | `/admin/files` | Klasör/dosya işlemleri. |
|
||||||
| Aktivite ve denetim kaydı | `/admin/activityLog`, `/admin/changeLog` | Kullanıcı hareketleri ve ABP audit log detayı. |
|
| Aktivite ve denetim kaydı | `/admin/activityLog`, `/admin/changeLog` | Kullanıcı hareketleri ve ABP audit log detayı. |
|
||||||
| Bildirimler | `/admin/profile/notification-settings`, bildirim ekranları | Kanal tercihleri ve bildirim oluşturma. |
|
| Bildirimler | `/admin/profile/notification-settings`, bildirim ekranları | Kanal tercihleri ve bildirim oluşturma. |
|
||||||
| Forum | `/admin/forum`, `/admin/forumManagement` | Forum kullanımı ve yönetimi. |
|
| Forum | `/admin/forum` | Forum kullanımı; yönetim `App.ForumManagement` altındaki ListForm ekranlarıyla yapılır. |
|
||||||
| Intranet | Intranet dashboard ve widget'ları | Duyuru, anket, sosyal duvar, etkinlik. Hangi widget'ın hangi kolonda, hangi sırada ve hangi yetkiyle yayınlandığı `Adm_T_IntranetWidget` kaydından gelir; kayıtlar `configs/seeds/{kapsam}/data/App.DeveloperKit.IntranetWidgets.json` ile senkron tutulur. |
|
| Intranet | Intranet dashboard ve widget'ları | Duyuru, anket, sosyal duvar, etkinlik. Hangi widget'ın hangi kolonda, hangi sırada ve hangi yetkiyle yayınlandığı `Adm_T_IntranetWidget` kaydından gelir; kayıtlar `configs/seeds/{kapsam}/data/App.DeveloperKit.IntranetWidgets.json` ile senkron tutulur. |
|
||||||
| Video oda | `/admin/videoroom/*` | Oda listesi, detay, planlama, katılımcı ve ekran paylaşımı. |
|
| Video oda | `/admin/videoroom/*` | Oda listesi, detay, planlama, katılımcı ve ekran paylaşımı. |
|
||||||
| Raporlar | `/admin/reports/:report/view|design/...` | DevExpress rapor görüntüleyici ve tasarımcı. |
|
| Raporlar | `/admin/reports/:report/view|design/...` | DevExpress rapor görüntüleyici ve tasarımcı. |
|
||||||
|
|
|
||||||
|
|
@ -44,34 +44,6 @@ public class ForumCategoryDto : FullAuditedEntityDto<Guid>
|
||||||
public Guid? LastPostUserId { get; set; }
|
public Guid? LastPostUserId { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class CreateForumCategoryDto
|
|
||||||
{
|
|
||||||
public Guid? TenantId { get; set; }
|
|
||||||
|
|
||||||
[Required]
|
|
||||||
[StringLength(100)]
|
|
||||||
public string Name { get; set; }
|
|
||||||
|
|
||||||
[Required]
|
|
||||||
[StringLength(100)]
|
|
||||||
public string Slug { get; set; }
|
|
||||||
|
|
||||||
[Required]
|
|
||||||
[StringLength(500)]
|
|
||||||
public string Description { get; set; }
|
|
||||||
|
|
||||||
[StringLength(10)]
|
|
||||||
public string Icon { get; set; }
|
|
||||||
|
|
||||||
public int DisplayOrder { get; set; }
|
|
||||||
public bool IsActive { get; set; } = true;
|
|
||||||
public bool IsLocked { get; set; } = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public class UpdateForumCategoryDto : CreateForumCategoryDto
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public class GetCategoriesInput : PagedAndSortedResultRequestDto
|
public class GetCategoriesInput : PagedAndSortedResultRequestDto
|
||||||
{
|
{
|
||||||
public bool? IsActive { get; set; }
|
public bool? IsActive { get; set; }
|
||||||
|
|
|
||||||
|
|
@ -14,9 +14,6 @@ public interface IForumAppService : IApplicationService
|
||||||
Task<PagedResultDto<ForumCategoryDto>> GetCategoriesAsync(GetCategoriesInput input);
|
Task<PagedResultDto<ForumCategoryDto>> GetCategoriesAsync(GetCategoriesInput input);
|
||||||
Task<ForumCategoryDto> GetCategoryAsync(Guid id);
|
Task<ForumCategoryDto> GetCategoryAsync(Guid id);
|
||||||
Task<ForumCategoryDto> GetCategoryBySlugAsync(string slug);
|
Task<ForumCategoryDto> GetCategoryBySlugAsync(string slug);
|
||||||
Task<ForumCategoryDto> CreateCategoryAsync(CreateForumCategoryDto input);
|
|
||||||
Task<ForumCategoryDto> UpdateCategoryAsync(Guid id, UpdateForumCategoryDto input);
|
|
||||||
Task DeleteCategoryAsync(Guid id);
|
|
||||||
|
|
||||||
// Topics
|
// Topics
|
||||||
Task<PagedResultDto<ForumTopicDto>> GetTopicsAsync(GetTopicsInput input);
|
Task<PagedResultDto<ForumTopicDto>> GetTopicsAsync(GetTopicsInput input);
|
||||||
|
|
|
||||||
|
|
@ -179,84 +179,6 @@ public class ForumAppService : PlatformAppService, IForumAppService
|
||||||
return ObjectMapper.Map<ForumCategory, ForumCategoryDto>(category);
|
return ObjectMapper.Map<ForumCategory, ForumCategoryDto>(category);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Authorize("App.ForumManagement.Create")]
|
|
||||||
public async Task<ForumCategoryDto> CreateCategoryAsync(CreateForumCategoryDto input)
|
|
||||||
{
|
|
||||||
var category = new ForumCategory
|
|
||||||
{
|
|
||||||
Name = input.Name,
|
|
||||||
Slug = input.Slug,
|
|
||||||
Description = input.Description,
|
|
||||||
Icon = input.Icon,
|
|
||||||
DisplayOrder = input.DisplayOrder,
|
|
||||||
IsActive = input.IsActive,
|
|
||||||
IsLocked = input.IsLocked
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
await _categoryRepository.InsertAsync(category, autoSave: true);
|
|
||||||
return ObjectMapper.Map<ForumCategory, ForumCategoryDto>(category);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Authorize("App.ForumManagement.Update")]
|
|
||||||
public async Task<ForumCategoryDto> UpdateCategoryAsync(Guid id, UpdateForumCategoryDto input)
|
|
||||||
{
|
|
||||||
var category = await _categoryRepository.GetAsync(id);
|
|
||||||
|
|
||||||
category.Name = input.Name;
|
|
||||||
category.Slug = input.Slug;
|
|
||||||
category.Description = input.Description;
|
|
||||||
category.Icon = input.Icon;
|
|
||||||
category.DisplayOrder = input.DisplayOrder;
|
|
||||||
category.IsActive = input.IsActive;
|
|
||||||
category.IsLocked = input.IsLocked;
|
|
||||||
|
|
||||||
await _categoryRepository.UpdateAsync(category, autoSave: true);
|
|
||||||
return ObjectMapper.Map<ForumCategory, ForumCategoryDto>(category);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Authorize("App.ForumManagement.Update")]
|
|
||||||
public async Task<ForumCategoryDto> UpdateCategoryLockAsync(Guid id)
|
|
||||||
{
|
|
||||||
var category = await _categoryRepository.GetAsync(id);
|
|
||||||
category.IsLocked = !category.IsLocked;
|
|
||||||
|
|
||||||
await _categoryRepository.UpdateAsync(category, autoSave: true);
|
|
||||||
return ObjectMapper.Map<ForumCategory, ForumCategoryDto>(category);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Authorize("App.ForumManagement.Update")]
|
|
||||||
public async Task<ForumCategoryDto> UpdateCategoryActiveAsync(Guid id)
|
|
||||||
{
|
|
||||||
var category = await _categoryRepository.GetAsync(id);
|
|
||||||
category.IsActive = !category.IsActive;
|
|
||||||
|
|
||||||
await _categoryRepository.UpdateAsync(category, autoSave: true);
|
|
||||||
return ObjectMapper.Map<ForumCategory, ForumCategoryDto>(category);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Authorize("App.ForumManagement.Delete")]
|
|
||||||
[UnitOfWork]
|
|
||||||
public async Task DeleteCategoryAsync(Guid id)
|
|
||||||
{
|
|
||||||
// Tüm topic ID'lerini al
|
|
||||||
var topicQueryable = await _topicRepository.GetQueryableAsync();
|
|
||||||
var topicIds = await AsyncExecuter.ToListAsync(
|
|
||||||
topicQueryable
|
|
||||||
.Where(t => t.CategoryId == id)
|
|
||||||
.Select(t => t.Id)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (topicIds.Any())
|
|
||||||
{
|
|
||||||
// Bulk delete - daha performanslı
|
|
||||||
await _postRepository.DeleteAsync(p => topicIds.Contains(p.TopicId));
|
|
||||||
await _topicRepository.DeleteAsync(t => t.CategoryId == id);
|
|
||||||
}
|
|
||||||
|
|
||||||
await _categoryRepository.DeleteAsync(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Topic management
|
// Topic management
|
||||||
public async Task<PagedResultDto<ForumTopicDto>> GetTopicsAsync(GetTopicsInput input)
|
public async Task<PagedResultDto<ForumTopicDto>> GetTopicsAsync(GetTopicsInput input)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -612,11 +612,101 @@
|
||||||
"en": "Forum Management",
|
"en": "Forum Management",
|
||||||
"tr": "Forum Yönetimi"
|
"tr": "Forum Yönetimi"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"resourceName": "Platform",
|
||||||
|
"key": "App.ForumManagement.Categories",
|
||||||
|
"en": "Forum Categories",
|
||||||
|
"tr": "Forum Kategorileri"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"resourceName": "Platform",
|
||||||
|
"key": "App.ForumManagement.Categories.Description",
|
||||||
|
"en": "Forum categories",
|
||||||
|
"tr": "Forum kategorileri"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"resourceName": "Platform",
|
||||||
|
"key": "App.ForumManagement.Topics",
|
||||||
|
"en": "Forum Topics",
|
||||||
|
"tr": "Forum Konuları"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"resourceName": "Platform",
|
||||||
|
"key": "App.ForumManagement.Topics.Description",
|
||||||
|
"en": "Forum topics",
|
||||||
|
"tr": "Forum konuları"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"resourceName": "Platform",
|
||||||
|
"key": "App.ForumManagement.Posts",
|
||||||
|
"en": "Forum Posts",
|
||||||
|
"tr": "Forum Gönderileri"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"resourceName": "Platform",
|
||||||
|
"key": "App.ForumManagement.Posts.Description",
|
||||||
|
"en": "Forum posts",
|
||||||
|
"tr": "Forum gönderileri"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"resourceName": "Platform",
|
||||||
|
"key": "App.Listform.ListformField.ReplyCount",
|
||||||
|
"en": "Replies",
|
||||||
|
"tr": "Yanıt Sayısı"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"resourceName": "Platform",
|
||||||
|
"key": "App.Listform.ListformField.IsSolved",
|
||||||
|
"en": "Solved",
|
||||||
|
"tr": "Çözüldü"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"resourceName": "Platform",
|
||||||
|
"key": "App.Listform.ListformField.AuthorName",
|
||||||
|
"en": "Author Name",
|
||||||
|
"tr": "Yazar Adı"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"resourceName": "Platform",
|
||||||
|
"key": "App.Listform.ListformField.AuthorTitle",
|
||||||
|
"en": "Author Title",
|
||||||
|
"tr": "Yazar Ünvanı"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"resourceName": "Platform",
|
||||||
|
"key": "App.Listform.ListformField.PhoneNumber",
|
||||||
|
"en": "Phone Number",
|
||||||
|
"tr": "Telefon"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"resourceName": "Platform",
|
||||||
|
"key": "App.Listform.ListformField.Topic",
|
||||||
|
"en": "Topic",
|
||||||
|
"tr": "Konu"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"resourceName": "Platform",
|
||||||
|
"key": "App.Listform.ListformField.IsAcceptedAnswer",
|
||||||
|
"en": "Accepted Answer",
|
||||||
|
"tr": "Kabul Edilen Yanıt"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"resourceName": "Platform",
|
||||||
|
"key": "App.Listform.ListformField.ParentPost",
|
||||||
|
"en": "Parent Post",
|
||||||
|
"tr": "Üst Gönderi"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"resourceName": "Platform",
|
||||||
|
"key": "App.Listform.ListformField.IsLocked",
|
||||||
|
"en": "Locked",
|
||||||
|
"tr": "Kilitli"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"resourceName": "Platform",
|
"resourceName": "Platform",
|
||||||
"key": "App.ForumManagement.Publish",
|
"key": "App.ForumManagement.Publish",
|
||||||
"en": "Forum Publishing",
|
"en": "Forum",
|
||||||
"tr": "Forum Yayınlama"
|
"tr": "Forum"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"resourceName": "Platform",
|
"resourceName": "Platform",
|
||||||
|
|
|
||||||
|
|
@ -8925,6 +8925,878 @@ public class ListFormSeeder_Saas : IDataSeedContributor, ITransientDependency
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region ForumCategory
|
||||||
|
listFormName = AppCodes.ForumManagement.Categories;
|
||||||
|
if (!await _listFormRepository.AnyAsync(a => a.ListFormCode == listFormName))
|
||||||
|
{
|
||||||
|
var listForm = await _listFormRepository.InsertAsync(
|
||||||
|
new ListForm
|
||||||
|
{
|
||||||
|
ListFormType = ListFormTypeEnum.List,
|
||||||
|
PageSize = 10,
|
||||||
|
ExportJson = DefaultExportJson,
|
||||||
|
IsSubForm = false,
|
||||||
|
ShowNote = false,
|
||||||
|
LayoutJson = DefaultLayoutJson(),
|
||||||
|
CultureName = LanguageCodes.En,
|
||||||
|
ListFormCode = listFormName,
|
||||||
|
Name = listFormName,
|
||||||
|
Title = listFormName,
|
||||||
|
DataSourceCode = SeedConsts.DataSources.DefaultCode,
|
||||||
|
IsTenant = true,
|
||||||
|
IsBranch = false,
|
||||||
|
IsOrganizationUnit = false,
|
||||||
|
Description = $"{listFormName}.Description",
|
||||||
|
SelectCommandType = SelectCommandTypeEnum.Table,
|
||||||
|
SelectCommand = TableNameResolver.GetFullTableName(nameof(TableNameEnum.ForumCategory)),
|
||||||
|
KeyFieldName = "Id",
|
||||||
|
KeyFieldDbSourceType = DbType.Guid,
|
||||||
|
DefaultFilter = DefaultFilterJson,
|
||||||
|
SortMode = GridOptions.SortModeSingle,
|
||||||
|
FilterRowJson = DefaultFilterRowJson(),
|
||||||
|
HeaderFilterJson = DefaultHeaderFilterJson(),
|
||||||
|
SearchPanelJson = DefaultSearchPanelJson(),
|
||||||
|
GroupPanelJson = DefaultGroupPanelJson(),
|
||||||
|
SelectionJson = DefaultSelectionSingleJson,
|
||||||
|
ColumnOptionJson = DefaultColumnOptionJson(),
|
||||||
|
PermissionJson = DefaultPermissionJson(listFormName),
|
||||||
|
DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.ForumCategory)),
|
||||||
|
SeedFilePath = SeedPathResolver.GetDataFilePath(listFormName),
|
||||||
|
SeedSyncInsert = false,
|
||||||
|
SeedSyncUpdate = false,
|
||||||
|
SeedSyncDelete = false,
|
||||||
|
DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(),
|
||||||
|
PagerOptionJson = DefaultPagerOptionJson,
|
||||||
|
EditingOptionJson = DefaultEditingOptionJson(listFormName, 600, 550, true, true, true, true, false),
|
||||||
|
EditingFormJson = JsonSerializer.Serialize(new List<EditingFormDto>
|
||||||
|
{
|
||||||
|
new() { Order = 1, ColCount = 1, ColSpan = 1, ItemType = "group" },
|
||||||
|
}),
|
||||||
|
InsertFieldsDefaultValueJson = DefaultInsertFieldsDefaultValueJson(),
|
||||||
|
FormFieldsDefaultValueJson = JsonSerializer.Serialize(new FieldsDefaultValue[] {
|
||||||
|
new() { FieldName = "IsActive", FieldDbType = DbType.Boolean, Value = "true", CustomValueType = FieldCustomValueTypeEnum.Value },
|
||||||
|
new() { FieldName = "IsLocked", FieldDbType = DbType.Boolean, Value = "false", CustomValueType = FieldCustomValueTypeEnum.Value },
|
||||||
|
new() { FieldName = "DisplayOrder", FieldDbType = DbType.Int32, Value = "1", CustomValueType = FieldCustomValueTypeEnum.Value }
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
#region ForumCategory Fields
|
||||||
|
await _listFormFieldRepository.InsertManyAsync([
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
ListFormCode = listForm.ListFormCode,
|
||||||
|
CultureName = LanguageCodes.En,
|
||||||
|
SourceDbType = DbType.Guid,
|
||||||
|
FieldName = "Id",
|
||||||
|
CaptionName = "App.Listform.ListformField.Id",
|
||||||
|
Width = 0,
|
||||||
|
ListOrderNo = 1,
|
||||||
|
Visible = false,
|
||||||
|
IsActive = true,
|
||||||
|
ColumnCustomizationJson = DefaultColumnCustomizationJson,
|
||||||
|
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
|
||||||
|
PivotSettingsJson = DefaultPivotSettingsJson,
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
ListFormCode = listForm.ListFormCode,
|
||||||
|
CultureName = LanguageCodes.En,
|
||||||
|
SourceDbType = DbType.String,
|
||||||
|
FieldName = "Name",
|
||||||
|
EditGroupOrderNo = 1,
|
||||||
|
EditOrderNo = 1,
|
||||||
|
EditorType2 = EditorTypes.dxTextBox,
|
||||||
|
ColSpan = 1,
|
||||||
|
CaptionName = "App.Listform.ListformField.Name",
|
||||||
|
Width = 0,
|
||||||
|
ListOrderNo = 2,
|
||||||
|
Visible = true,
|
||||||
|
IsActive = true,
|
||||||
|
AllowSearch = true,
|
||||||
|
ValidationRuleJson = DefaultValidationRuleRequiredJson,
|
||||||
|
ColumnCustomizationJson = DefaultColumnCustomizationJson,
|
||||||
|
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
|
||||||
|
PivotSettingsJson = DefaultPivotSettingsJson,
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
ListFormCode = listForm.ListFormCode,
|
||||||
|
CultureName = LanguageCodes.En,
|
||||||
|
SourceDbType = DbType.String,
|
||||||
|
FieldName = "Slug",
|
||||||
|
EditGroupOrderNo = 1,
|
||||||
|
EditOrderNo = 2,
|
||||||
|
EditorType2 = EditorTypes.dxTextBox,
|
||||||
|
ColSpan = 1,
|
||||||
|
CaptionName = "App.Listform.ListformField.Slug",
|
||||||
|
Width = 0,
|
||||||
|
ListOrderNo = 3,
|
||||||
|
Visible = true,
|
||||||
|
IsActive = true,
|
||||||
|
AllowSearch = true,
|
||||||
|
ValidationRuleJson = DefaultValidationRuleRequiredJson,
|
||||||
|
ColumnCustomizationJson = DefaultColumnCustomizationJson,
|
||||||
|
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
|
||||||
|
PivotSettingsJson = DefaultPivotSettingsJson,
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
ListFormCode = listForm.ListFormCode,
|
||||||
|
CultureName = LanguageCodes.En,
|
||||||
|
SourceDbType = DbType.String,
|
||||||
|
FieldName = "Description",
|
||||||
|
EditGroupOrderNo = 1,
|
||||||
|
EditOrderNo = 3,
|
||||||
|
EditorType2 = EditorTypes.dxTextArea,
|
||||||
|
ColSpan = 1,
|
||||||
|
CaptionName = "App.Listform.ListformField.Description",
|
||||||
|
Width = 0,
|
||||||
|
ListOrderNo = 4,
|
||||||
|
Visible = true,
|
||||||
|
IsActive = true,
|
||||||
|
AllowSearch = true,
|
||||||
|
ColumnCustomizationJson = DefaultColumnCustomizationJson,
|
||||||
|
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
|
||||||
|
PivotSettingsJson = DefaultPivotSettingsJson,
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
ListFormCode = listForm.ListFormCode,
|
||||||
|
CultureName = LanguageCodes.En,
|
||||||
|
SourceDbType = DbType.String,
|
||||||
|
FieldName = "Icon",
|
||||||
|
EditGroupOrderNo = 1,
|
||||||
|
EditOrderNo = 4,
|
||||||
|
EditorType2 = EditorTypes.dxTextBox,
|
||||||
|
ColSpan = 1,
|
||||||
|
CaptionName = "App.Listform.ListformField.Icon",
|
||||||
|
Width = 0,
|
||||||
|
ListOrderNo = 5,
|
||||||
|
Visible = true,
|
||||||
|
IsActive = true,
|
||||||
|
AllowSearch = true,
|
||||||
|
ColumnCustomizationJson = DefaultColumnCustomizationJson,
|
||||||
|
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
|
||||||
|
PivotSettingsJson = DefaultPivotSettingsJson,
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
ListFormCode = listForm.ListFormCode,
|
||||||
|
CultureName = LanguageCodes.En,
|
||||||
|
SourceDbType = DbType.Int32,
|
||||||
|
FieldName = "DisplayOrder",
|
||||||
|
EditGroupOrderNo = 1,
|
||||||
|
EditOrderNo = 5,
|
||||||
|
EditorType2 = EditorTypes.dxNumberBox,
|
||||||
|
ColSpan = 1,
|
||||||
|
CaptionName = "App.Listform.ListformField.DisplayOrder",
|
||||||
|
Alignment = "right",
|
||||||
|
Width = 0,
|
||||||
|
ListOrderNo = 6,
|
||||||
|
Visible = true,
|
||||||
|
IsActive = true,
|
||||||
|
AllowSearch = true,
|
||||||
|
ValidationRuleJson = DefaultValidationRuleRequiredJson,
|
||||||
|
ColumnCustomizationJson = DefaultColumnCustomizationJson,
|
||||||
|
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
|
||||||
|
PivotSettingsJson = DefaultPivotSettingsJson,
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
ListFormCode = listForm.ListFormCode,
|
||||||
|
CultureName = LanguageCodes.En,
|
||||||
|
SourceDbType = DbType.Boolean,
|
||||||
|
FieldName = "IsActive",
|
||||||
|
EditGroupOrderNo = 1,
|
||||||
|
EditOrderNo = 6,
|
||||||
|
EditorType2 = EditorTypes.dxCheckBox,
|
||||||
|
ColSpan = 1,
|
||||||
|
CaptionName = "App.Listform.ListformField.IsActive",
|
||||||
|
Width = 0,
|
||||||
|
ListOrderNo = 7,
|
||||||
|
Visible = true,
|
||||||
|
IsActive = true,
|
||||||
|
AllowSearch = true,
|
||||||
|
ColumnCustomizationJson = DefaultColumnCustomizationJson,
|
||||||
|
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
|
||||||
|
PivotSettingsJson = DefaultPivotSettingsJson,
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
ListFormCode = listForm.ListFormCode,
|
||||||
|
CultureName = LanguageCodes.En,
|
||||||
|
SourceDbType = DbType.Boolean,
|
||||||
|
FieldName = "IsLocked",
|
||||||
|
EditGroupOrderNo = 1,
|
||||||
|
EditOrderNo = 7,
|
||||||
|
EditorType2 = EditorTypes.dxCheckBox,
|
||||||
|
ColSpan = 1,
|
||||||
|
CaptionName = "App.Listform.ListformField.IsLocked",
|
||||||
|
Width = 0,
|
||||||
|
ListOrderNo = 8,
|
||||||
|
Visible = true,
|
||||||
|
IsActive = true,
|
||||||
|
AllowSearch = true,
|
||||||
|
ColumnCustomizationJson = DefaultColumnCustomizationJson,
|
||||||
|
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
|
||||||
|
PivotSettingsJson = DefaultPivotSettingsJson,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region ForumTopic
|
||||||
|
listFormName = AppCodes.ForumManagement.Topics;
|
||||||
|
if (!await _listFormRepository.AnyAsync(a => a.ListFormCode == listFormName))
|
||||||
|
{
|
||||||
|
var listForm = await _listFormRepository.InsertAsync(
|
||||||
|
new ListForm
|
||||||
|
{
|
||||||
|
ListFormType = ListFormTypeEnum.List,
|
||||||
|
PageSize = 10,
|
||||||
|
ExportJson = DefaultExportJson,
|
||||||
|
IsSubForm = false,
|
||||||
|
ShowNote = false,
|
||||||
|
LayoutJson = DefaultLayoutJson(),
|
||||||
|
CultureName = LanguageCodes.En,
|
||||||
|
ListFormCode = listFormName,
|
||||||
|
Name = listFormName,
|
||||||
|
Title = listFormName,
|
||||||
|
DataSourceCode = SeedConsts.DataSources.DefaultCode,
|
||||||
|
IsTenant = true,
|
||||||
|
IsBranch = false,
|
||||||
|
IsOrganizationUnit = false,
|
||||||
|
Description = $"{listFormName}.Description",
|
||||||
|
SelectCommandType = SelectCommandTypeEnum.Table,
|
||||||
|
SelectCommand = TableNameResolver.GetFullTableName(nameof(TableNameEnum.ForumTopic)),
|
||||||
|
KeyFieldName = "Id",
|
||||||
|
KeyFieldDbSourceType = DbType.Guid,
|
||||||
|
DefaultFilter = DefaultFilterJson,
|
||||||
|
SortMode = GridOptions.SortModeSingle,
|
||||||
|
FilterRowJson = DefaultFilterRowJson(),
|
||||||
|
HeaderFilterJson = DefaultHeaderFilterJson(),
|
||||||
|
SearchPanelJson = DefaultSearchPanelJson(),
|
||||||
|
GroupPanelJson = DefaultGroupPanelJson(),
|
||||||
|
SelectionJson = DefaultSelectionSingleJson,
|
||||||
|
ColumnOptionJson = DefaultColumnOptionJson(),
|
||||||
|
PermissionJson = DefaultPermissionJson(listFormName),
|
||||||
|
DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.ForumTopic)),
|
||||||
|
SeedFilePath = SeedPathResolver.GetDataFilePath(listFormName),
|
||||||
|
SeedSyncInsert = false,
|
||||||
|
SeedSyncUpdate = false,
|
||||||
|
SeedSyncDelete = false,
|
||||||
|
DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(),
|
||||||
|
PagerOptionJson = DefaultPagerOptionJson,
|
||||||
|
EditingOptionJson = DefaultEditingOptionJson(listFormName, 800, 650, true, true, true, true, false),
|
||||||
|
EditingFormJson = JsonSerializer.Serialize(new List<EditingFormDto>
|
||||||
|
{
|
||||||
|
new() { Order = 1, ColCount = 1, ColSpan = 1, ItemType = "group" },
|
||||||
|
}),
|
||||||
|
InsertFieldsDefaultValueJson = JsonSerializer.Serialize(new FieldsDefaultValue[] {
|
||||||
|
new() { FieldName = "CreationTime", FieldDbType = DbType.DateTimeOffset, Value = "@NOW", CustomValueType = FieldCustomValueTypeEnum.CustomKey },
|
||||||
|
new() { FieldName = "CreatorId", FieldDbType = DbType.Guid, Value = "@USERID", CustomValueType = FieldCustomValueTypeEnum.CustomKey },
|
||||||
|
new() { FieldName = "Id", FieldDbType = DbType.Guid, Value = "@NEWID", CustomValueType = FieldCustomValueTypeEnum.CustomKey },
|
||||||
|
new() { FieldName = "AuthorId", FieldDbType = DbType.Guid, Value = "@USERID", CustomValueType = FieldCustomValueTypeEnum.CustomKey },
|
||||||
|
new() { FieldName = "ViewCount", FieldDbType = DbType.Int32, Value = "0", CustomValueType = FieldCustomValueTypeEnum.Value },
|
||||||
|
new() { FieldName = "ReplyCount", FieldDbType = DbType.Int32, Value = "0", CustomValueType = FieldCustomValueTypeEnum.Value },
|
||||||
|
new() { FieldName = "LikeCount", FieldDbType = DbType.Int32, Value = "0", CustomValueType = FieldCustomValueTypeEnum.Value },
|
||||||
|
new() { FieldName = "IsPinned", FieldDbType = DbType.Boolean, Value = "false", CustomValueType = FieldCustomValueTypeEnum.Value },
|
||||||
|
new() { FieldName = "IsLocked", FieldDbType = DbType.Boolean, Value = "false", CustomValueType = FieldCustomValueTypeEnum.Value },
|
||||||
|
new() { FieldName = "IsSolved", FieldDbType = DbType.Boolean, Value = "false", CustomValueType = FieldCustomValueTypeEnum.Value }
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
#region ForumTopic Fields
|
||||||
|
await _listFormFieldRepository.InsertManyAsync([
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
ListFormCode = listForm.ListFormCode,
|
||||||
|
CultureName = LanguageCodes.En,
|
||||||
|
SourceDbType = DbType.Guid,
|
||||||
|
FieldName = "Id",
|
||||||
|
CaptionName = "App.Listform.ListformField.Id",
|
||||||
|
Width = 0,
|
||||||
|
ListOrderNo = 1,
|
||||||
|
Visible = false,
|
||||||
|
IsActive = true,
|
||||||
|
ColumnCustomizationJson = DefaultColumnCustomizationJson,
|
||||||
|
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
|
||||||
|
PivotSettingsJson = DefaultPivotSettingsJson,
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
ListFormCode = listForm.ListFormCode,
|
||||||
|
CultureName = LanguageCodes.En,
|
||||||
|
SourceDbType = DbType.String,
|
||||||
|
FieldName = "Title",
|
||||||
|
EditGroupOrderNo = 1,
|
||||||
|
EditOrderNo = 1,
|
||||||
|
EditorType2 = EditorTypes.dxTextBox,
|
||||||
|
ColSpan = 1,
|
||||||
|
CaptionName = "App.Listform.ListformField.Title",
|
||||||
|
Width = 0,
|
||||||
|
ListOrderNo = 2,
|
||||||
|
Visible = true,
|
||||||
|
IsActive = true,
|
||||||
|
AllowSearch = true,
|
||||||
|
ValidationRuleJson = DefaultValidationRuleRequiredJson,
|
||||||
|
ColumnCustomizationJson = DefaultColumnCustomizationJson,
|
||||||
|
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
|
||||||
|
PivotSettingsJson = DefaultPivotSettingsJson,
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
ListFormCode = listForm.ListFormCode,
|
||||||
|
CultureName = LanguageCodes.En,
|
||||||
|
SourceDbType = DbType.Guid,
|
||||||
|
FieldName = "CategoryId",
|
||||||
|
EditGroupOrderNo = 1,
|
||||||
|
EditOrderNo = 2,
|
||||||
|
EditorType2 = EditorTypes.dxSelectBox,
|
||||||
|
ColSpan = 1,
|
||||||
|
EditorOptions = EditorOptions.ShowClearButton(),
|
||||||
|
CaptionName = "App.Listform.ListformField.Category",
|
||||||
|
Width = 0,
|
||||||
|
ListOrderNo = 3,
|
||||||
|
Visible = true,
|
||||||
|
IsActive = true,
|
||||||
|
AllowSearch = true,
|
||||||
|
ValidationRuleJson = DefaultValidationRuleRequiredJson,
|
||||||
|
LookupJson = LookupQueryValues.DefaultLookupQueryJson(nameof(TableNameEnum.ForumCategory), "Id", "Name"),
|
||||||
|
ColumnCustomizationJson = DefaultColumnCustomizationJson,
|
||||||
|
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
|
||||||
|
PivotSettingsJson = DefaultPivotSettingsJson,
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
ListFormCode = listForm.ListFormCode,
|
||||||
|
CultureName = LanguageCodes.En,
|
||||||
|
SourceDbType = DbType.String,
|
||||||
|
FieldName = "Content",
|
||||||
|
EditGroupOrderNo = 1,
|
||||||
|
EditOrderNo = 3,
|
||||||
|
EditorType2 = EditorTypes.dxTextArea,
|
||||||
|
ColSpan = 1,
|
||||||
|
CaptionName = "App.Listform.ListformField.Content",
|
||||||
|
Width = 0,
|
||||||
|
ListOrderNo = 4,
|
||||||
|
Visible = false,
|
||||||
|
IsActive = true,
|
||||||
|
ValidationRuleJson = DefaultValidationRuleRequiredJson,
|
||||||
|
ColumnCustomizationJson = DefaultColumnCustomizationJson,
|
||||||
|
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
|
||||||
|
PivotSettingsJson = DefaultPivotSettingsJson,
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
ListFormCode = listForm.ListFormCode,
|
||||||
|
CultureName = LanguageCodes.En,
|
||||||
|
SourceDbType = DbType.Guid,
|
||||||
|
FieldName = "AuthorId",
|
||||||
|
CaptionName = "App.Listform.ListformField.Author",
|
||||||
|
Width = 0,
|
||||||
|
ListOrderNo = 5,
|
||||||
|
Visible = false,
|
||||||
|
IsActive = true,
|
||||||
|
ColumnCustomizationJson = DefaultColumnCustomizationJson,
|
||||||
|
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
|
||||||
|
PivotSettingsJson = DefaultPivotSettingsJson,
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
ListFormCode = listForm.ListFormCode,
|
||||||
|
CultureName = LanguageCodes.En,
|
||||||
|
SourceDbType = DbType.String,
|
||||||
|
FieldName = "AuthorName",
|
||||||
|
EditGroupOrderNo = 1,
|
||||||
|
EditOrderNo = 4,
|
||||||
|
EditorType2 = EditorTypes.dxTextBox,
|
||||||
|
ColSpan = 1,
|
||||||
|
CaptionName = "App.Listform.ListformField.AuthorName",
|
||||||
|
Width = 0,
|
||||||
|
ListOrderNo = 6,
|
||||||
|
Visible = true,
|
||||||
|
IsActive = true,
|
||||||
|
AllowSearch = true,
|
||||||
|
ColumnCustomizationJson = DefaultColumnCustomizationJson,
|
||||||
|
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
|
||||||
|
PivotSettingsJson = DefaultPivotSettingsJson,
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
ListFormCode = listForm.ListFormCode,
|
||||||
|
CultureName = LanguageCodes.En,
|
||||||
|
SourceDbType = DbType.String,
|
||||||
|
FieldName = "AuthorTitle",
|
||||||
|
EditGroupOrderNo = 1,
|
||||||
|
EditOrderNo = 5,
|
||||||
|
EditorType2 = EditorTypes.dxTextBox,
|
||||||
|
ColSpan = 1,
|
||||||
|
CaptionName = "App.Listform.ListformField.AuthorTitle",
|
||||||
|
Width = 0,
|
||||||
|
ListOrderNo = 7,
|
||||||
|
Visible = false,
|
||||||
|
IsActive = true,
|
||||||
|
ColumnCustomizationJson = DefaultColumnCustomizationJson,
|
||||||
|
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
|
||||||
|
PivotSettingsJson = DefaultPivotSettingsJson,
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
ListFormCode = listForm.ListFormCode,
|
||||||
|
CultureName = LanguageCodes.En,
|
||||||
|
SourceDbType = DbType.String,
|
||||||
|
FieldName = "AuthorEmail",
|
||||||
|
EditGroupOrderNo = 1,
|
||||||
|
EditOrderNo = 6,
|
||||||
|
EditorType2 = EditorTypes.dxTextBox,
|
||||||
|
ColSpan = 1,
|
||||||
|
CaptionName = "App.Listform.ListformField.Email",
|
||||||
|
Width = 0,
|
||||||
|
ListOrderNo = 8,
|
||||||
|
Visible = false,
|
||||||
|
IsActive = true,
|
||||||
|
ColumnCustomizationJson = DefaultColumnCustomizationJson,
|
||||||
|
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
|
||||||
|
PivotSettingsJson = DefaultPivotSettingsJson,
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
ListFormCode = listForm.ListFormCode,
|
||||||
|
CultureName = LanguageCodes.En,
|
||||||
|
SourceDbType = DbType.String,
|
||||||
|
FieldName = "AuthorPhoneNumber",
|
||||||
|
EditGroupOrderNo = 1,
|
||||||
|
EditOrderNo = 7,
|
||||||
|
EditorType2 = EditorTypes.dxTextBox,
|
||||||
|
ColSpan = 1,
|
||||||
|
CaptionName = "App.Listform.ListformField.PhoneNumber",
|
||||||
|
Width = 0,
|
||||||
|
ListOrderNo = 9,
|
||||||
|
Visible = false,
|
||||||
|
IsActive = true,
|
||||||
|
ColumnCustomizationJson = DefaultColumnCustomizationJson,
|
||||||
|
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
|
||||||
|
PivotSettingsJson = DefaultPivotSettingsJson,
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
ListFormCode = listForm.ListFormCode,
|
||||||
|
CultureName = LanguageCodes.En,
|
||||||
|
SourceDbType = DbType.String,
|
||||||
|
FieldName = "AuthorDepartment",
|
||||||
|
EditGroupOrderNo = 1,
|
||||||
|
EditOrderNo = 8,
|
||||||
|
EditorType2 = EditorTypes.dxTextBox,
|
||||||
|
ColSpan = 1,
|
||||||
|
CaptionName = "App.Listform.ListformField.Department",
|
||||||
|
Width = 0,
|
||||||
|
ListOrderNo = 10,
|
||||||
|
Visible = false,
|
||||||
|
IsActive = true,
|
||||||
|
ColumnCustomizationJson = DefaultColumnCustomizationJson,
|
||||||
|
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
|
||||||
|
PivotSettingsJson = DefaultPivotSettingsJson,
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
ListFormCode = listForm.ListFormCode,
|
||||||
|
CultureName = LanguageCodes.En,
|
||||||
|
SourceDbType = DbType.Int32,
|
||||||
|
FieldName = "ViewCount",
|
||||||
|
CaptionName = "App.Listform.ListformField.ViewCount",
|
||||||
|
Alignment = "right",
|
||||||
|
Width = 0,
|
||||||
|
ListOrderNo = 11,
|
||||||
|
Visible = true,
|
||||||
|
IsActive = true,
|
||||||
|
ColumnCustomizationJson = DefaultColumnCustomizationJson,
|
||||||
|
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
|
||||||
|
PivotSettingsJson = DefaultPivotSettingsJson,
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
ListFormCode = listForm.ListFormCode,
|
||||||
|
CultureName = LanguageCodes.En,
|
||||||
|
SourceDbType = DbType.Int32,
|
||||||
|
FieldName = "ReplyCount",
|
||||||
|
CaptionName = "App.Listform.ListformField.ReplyCount",
|
||||||
|
Alignment = "right",
|
||||||
|
Width = 0,
|
||||||
|
ListOrderNo = 12,
|
||||||
|
Visible = true,
|
||||||
|
IsActive = true,
|
||||||
|
ColumnCustomizationJson = DefaultColumnCustomizationJson,
|
||||||
|
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
|
||||||
|
PivotSettingsJson = DefaultPivotSettingsJson,
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
ListFormCode = listForm.ListFormCode,
|
||||||
|
CultureName = LanguageCodes.En,
|
||||||
|
SourceDbType = DbType.Int32,
|
||||||
|
FieldName = "LikeCount",
|
||||||
|
CaptionName = "App.Listform.ListformField.LikeCount",
|
||||||
|
Alignment = "right",
|
||||||
|
Width = 0,
|
||||||
|
ListOrderNo = 13,
|
||||||
|
Visible = true,
|
||||||
|
IsActive = true,
|
||||||
|
ColumnCustomizationJson = DefaultColumnCustomizationJson,
|
||||||
|
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
|
||||||
|
PivotSettingsJson = DefaultPivotSettingsJson,
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
ListFormCode = listForm.ListFormCode,
|
||||||
|
CultureName = LanguageCodes.En,
|
||||||
|
SourceDbType = DbType.Boolean,
|
||||||
|
FieldName = "IsPinned",
|
||||||
|
EditGroupOrderNo = 1,
|
||||||
|
EditOrderNo = 9,
|
||||||
|
EditorType2 = EditorTypes.dxCheckBox,
|
||||||
|
ColSpan = 1,
|
||||||
|
CaptionName = "App.Listform.ListformField.IsPinned",
|
||||||
|
Width = 0,
|
||||||
|
ListOrderNo = 14,
|
||||||
|
Visible = true,
|
||||||
|
IsActive = true,
|
||||||
|
AllowSearch = true,
|
||||||
|
ColumnCustomizationJson = DefaultColumnCustomizationJson,
|
||||||
|
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
|
||||||
|
PivotSettingsJson = DefaultPivotSettingsJson,
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
ListFormCode = listForm.ListFormCode,
|
||||||
|
CultureName = LanguageCodes.En,
|
||||||
|
SourceDbType = DbType.Boolean,
|
||||||
|
FieldName = "IsLocked",
|
||||||
|
EditGroupOrderNo = 1,
|
||||||
|
EditOrderNo = 10,
|
||||||
|
EditorType2 = EditorTypes.dxCheckBox,
|
||||||
|
ColSpan = 1,
|
||||||
|
CaptionName = "App.Listform.ListformField.IsLocked",
|
||||||
|
Width = 0,
|
||||||
|
ListOrderNo = 15,
|
||||||
|
Visible = true,
|
||||||
|
IsActive = true,
|
||||||
|
AllowSearch = true,
|
||||||
|
ColumnCustomizationJson = DefaultColumnCustomizationJson,
|
||||||
|
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
|
||||||
|
PivotSettingsJson = DefaultPivotSettingsJson,
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
ListFormCode = listForm.ListFormCode,
|
||||||
|
CultureName = LanguageCodes.En,
|
||||||
|
SourceDbType = DbType.Boolean,
|
||||||
|
FieldName = "IsSolved",
|
||||||
|
EditGroupOrderNo = 1,
|
||||||
|
EditOrderNo = 11,
|
||||||
|
EditorType2 = EditorTypes.dxCheckBox,
|
||||||
|
ColSpan = 1,
|
||||||
|
CaptionName = "App.Listform.ListformField.IsSolved",
|
||||||
|
Width = 0,
|
||||||
|
ListOrderNo = 16,
|
||||||
|
Visible = true,
|
||||||
|
IsActive = true,
|
||||||
|
AllowSearch = true,
|
||||||
|
ColumnCustomizationJson = DefaultColumnCustomizationJson,
|
||||||
|
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
|
||||||
|
PivotSettingsJson = DefaultPivotSettingsJson,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region ForumPost
|
||||||
|
listFormName = AppCodes.ForumManagement.Posts;
|
||||||
|
if (!await _listFormRepository.AnyAsync(a => a.ListFormCode == listFormName))
|
||||||
|
{
|
||||||
|
var listForm = await _listFormRepository.InsertAsync(
|
||||||
|
new ListForm
|
||||||
|
{
|
||||||
|
ListFormType = ListFormTypeEnum.List,
|
||||||
|
PageSize = 10,
|
||||||
|
ExportJson = DefaultExportJson,
|
||||||
|
IsSubForm = false,
|
||||||
|
ShowNote = false,
|
||||||
|
LayoutJson = DefaultLayoutJson(),
|
||||||
|
CultureName = LanguageCodes.En,
|
||||||
|
ListFormCode = listFormName,
|
||||||
|
Name = listFormName,
|
||||||
|
Title = listFormName,
|
||||||
|
DataSourceCode = SeedConsts.DataSources.DefaultCode,
|
||||||
|
IsTenant = true,
|
||||||
|
IsBranch = false,
|
||||||
|
IsOrganizationUnit = false,
|
||||||
|
Description = $"{listFormName}.Description",
|
||||||
|
SelectCommandType = SelectCommandTypeEnum.Table,
|
||||||
|
SelectCommand = TableNameResolver.GetFullTableName(nameof(TableNameEnum.ForumPost)),
|
||||||
|
KeyFieldName = "Id",
|
||||||
|
KeyFieldDbSourceType = DbType.Guid,
|
||||||
|
DefaultFilter = DefaultFilterJson,
|
||||||
|
SortMode = GridOptions.SortModeSingle,
|
||||||
|
FilterRowJson = DefaultFilterRowJson(),
|
||||||
|
HeaderFilterJson = DefaultHeaderFilterJson(),
|
||||||
|
SearchPanelJson = DefaultSearchPanelJson(),
|
||||||
|
GroupPanelJson = DefaultGroupPanelJson(),
|
||||||
|
SelectionJson = DefaultSelectionSingleJson,
|
||||||
|
ColumnOptionJson = DefaultColumnOptionJson(),
|
||||||
|
PermissionJson = DefaultPermissionJson(listFormName),
|
||||||
|
DeleteCommand = DefaultDeleteCommand(nameof(TableNameEnum.ForumPost)),
|
||||||
|
SeedFilePath = SeedPathResolver.GetDataFilePath(listFormName),
|
||||||
|
SeedSyncInsert = false,
|
||||||
|
SeedSyncUpdate = false,
|
||||||
|
SeedSyncDelete = false,
|
||||||
|
DeleteFieldsDefaultValueJson = DefaultDeleteFieldsDefaultValueJson(),
|
||||||
|
PagerOptionJson = DefaultPagerOptionJson,
|
||||||
|
EditingOptionJson = DefaultEditingOptionJson(listFormName, 800, 600, true, true, true, true, false),
|
||||||
|
EditingFormJson = JsonSerializer.Serialize(new List<EditingFormDto>
|
||||||
|
{
|
||||||
|
new() { Order = 1, ColCount = 1, ColSpan = 1, ItemType = "group" },
|
||||||
|
}),
|
||||||
|
InsertFieldsDefaultValueJson = JsonSerializer.Serialize(new FieldsDefaultValue[] {
|
||||||
|
new() { FieldName = "CreationTime", FieldDbType = DbType.DateTimeOffset, Value = "@NOW", CustomValueType = FieldCustomValueTypeEnum.CustomKey },
|
||||||
|
new() { FieldName = "CreatorId", FieldDbType = DbType.Guid, Value = "@USERID", CustomValueType = FieldCustomValueTypeEnum.CustomKey },
|
||||||
|
new() { FieldName = "Id", FieldDbType = DbType.Guid, Value = "@NEWID", CustomValueType = FieldCustomValueTypeEnum.CustomKey },
|
||||||
|
new() { FieldName = "AuthorId", FieldDbType = DbType.Guid, Value = "@USERID", CustomValueType = FieldCustomValueTypeEnum.CustomKey },
|
||||||
|
new() { FieldName = "LikeCount", FieldDbType = DbType.Int32, Value = "0", CustomValueType = FieldCustomValueTypeEnum.Value },
|
||||||
|
new() { FieldName = "IsAcceptedAnswer", FieldDbType = DbType.Boolean, Value = "false", CustomValueType = FieldCustomValueTypeEnum.Value }
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
#region ForumPost Fields
|
||||||
|
await _listFormFieldRepository.InsertManyAsync([
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
ListFormCode = listForm.ListFormCode,
|
||||||
|
CultureName = LanguageCodes.En,
|
||||||
|
SourceDbType = DbType.Guid,
|
||||||
|
FieldName = "Id",
|
||||||
|
CaptionName = "App.Listform.ListformField.Id",
|
||||||
|
Width = 0,
|
||||||
|
ListOrderNo = 1,
|
||||||
|
Visible = false,
|
||||||
|
IsActive = true,
|
||||||
|
ColumnCustomizationJson = DefaultColumnCustomizationJson,
|
||||||
|
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
|
||||||
|
PivotSettingsJson = DefaultPivotSettingsJson,
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
ListFormCode = listForm.ListFormCode,
|
||||||
|
CultureName = LanguageCodes.En,
|
||||||
|
SourceDbType = DbType.Guid,
|
||||||
|
FieldName = "TopicId",
|
||||||
|
EditGroupOrderNo = 1,
|
||||||
|
EditOrderNo = 1,
|
||||||
|
EditorType2 = EditorTypes.dxSelectBox,
|
||||||
|
ColSpan = 1,
|
||||||
|
EditorOptions = EditorOptions.ShowClearButton(),
|
||||||
|
CaptionName = "App.Listform.ListformField.Topic",
|
||||||
|
Width = 0,
|
||||||
|
ListOrderNo = 2,
|
||||||
|
Visible = true,
|
||||||
|
IsActive = true,
|
||||||
|
AllowSearch = true,
|
||||||
|
ValidationRuleJson = DefaultValidationRuleRequiredJson,
|
||||||
|
LookupJson = LookupQueryValues.DefaultLookupQueryJson(nameof(TableNameEnum.ForumTopic), "Id", "Title"),
|
||||||
|
ColumnCustomizationJson = DefaultColumnCustomizationJson,
|
||||||
|
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
|
||||||
|
PivotSettingsJson = DefaultPivotSettingsJson,
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
ListFormCode = listForm.ListFormCode,
|
||||||
|
CultureName = LanguageCodes.En,
|
||||||
|
SourceDbType = DbType.String,
|
||||||
|
FieldName = "Content",
|
||||||
|
EditGroupOrderNo = 1,
|
||||||
|
EditOrderNo = 2,
|
||||||
|
EditorType2 = EditorTypes.dxTextArea,
|
||||||
|
ColSpan = 1,
|
||||||
|
CaptionName = "App.Listform.ListformField.Content",
|
||||||
|
Width = 0,
|
||||||
|
ListOrderNo = 3,
|
||||||
|
Visible = true,
|
||||||
|
IsActive = true,
|
||||||
|
AllowSearch = true,
|
||||||
|
ValidationRuleJson = DefaultValidationRuleRequiredJson,
|
||||||
|
ColumnCustomizationJson = DefaultColumnCustomizationJson,
|
||||||
|
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
|
||||||
|
PivotSettingsJson = DefaultPivotSettingsJson,
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
ListFormCode = listForm.ListFormCode,
|
||||||
|
CultureName = LanguageCodes.En,
|
||||||
|
SourceDbType = DbType.Guid,
|
||||||
|
FieldName = "AuthorId",
|
||||||
|
CaptionName = "App.Listform.ListformField.Author",
|
||||||
|
Width = 0,
|
||||||
|
ListOrderNo = 4,
|
||||||
|
Visible = false,
|
||||||
|
IsActive = true,
|
||||||
|
ColumnCustomizationJson = DefaultColumnCustomizationJson,
|
||||||
|
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
|
||||||
|
PivotSettingsJson = DefaultPivotSettingsJson,
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
ListFormCode = listForm.ListFormCode,
|
||||||
|
CultureName = LanguageCodes.En,
|
||||||
|
SourceDbType = DbType.String,
|
||||||
|
FieldName = "AuthorName",
|
||||||
|
EditGroupOrderNo = 1,
|
||||||
|
EditOrderNo = 3,
|
||||||
|
EditorType2 = EditorTypes.dxTextBox,
|
||||||
|
ColSpan = 1,
|
||||||
|
CaptionName = "App.Listform.ListformField.AuthorName",
|
||||||
|
Width = 0,
|
||||||
|
ListOrderNo = 5,
|
||||||
|
Visible = true,
|
||||||
|
IsActive = true,
|
||||||
|
AllowSearch = true,
|
||||||
|
ColumnCustomizationJson = DefaultColumnCustomizationJson,
|
||||||
|
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
|
||||||
|
PivotSettingsJson = DefaultPivotSettingsJson,
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
ListFormCode = listForm.ListFormCode,
|
||||||
|
CultureName = LanguageCodes.En,
|
||||||
|
SourceDbType = DbType.String,
|
||||||
|
FieldName = "AuthorTitle",
|
||||||
|
EditGroupOrderNo = 1,
|
||||||
|
EditOrderNo = 4,
|
||||||
|
EditorType2 = EditorTypes.dxTextBox,
|
||||||
|
ColSpan = 1,
|
||||||
|
CaptionName = "App.Listform.ListformField.AuthorTitle",
|
||||||
|
Width = 0,
|
||||||
|
ListOrderNo = 6,
|
||||||
|
Visible = false,
|
||||||
|
IsActive = true,
|
||||||
|
ColumnCustomizationJson = DefaultColumnCustomizationJson,
|
||||||
|
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
|
||||||
|
PivotSettingsJson = DefaultPivotSettingsJson,
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
ListFormCode = listForm.ListFormCode,
|
||||||
|
CultureName = LanguageCodes.En,
|
||||||
|
SourceDbType = DbType.String,
|
||||||
|
FieldName = "AuthorEmail",
|
||||||
|
EditGroupOrderNo = 1,
|
||||||
|
EditOrderNo = 5,
|
||||||
|
EditorType2 = EditorTypes.dxTextBox,
|
||||||
|
ColSpan = 1,
|
||||||
|
CaptionName = "App.Listform.ListformField.Email",
|
||||||
|
Width = 0,
|
||||||
|
ListOrderNo = 7,
|
||||||
|
Visible = false,
|
||||||
|
IsActive = true,
|
||||||
|
ColumnCustomizationJson = DefaultColumnCustomizationJson,
|
||||||
|
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
|
||||||
|
PivotSettingsJson = DefaultPivotSettingsJson,
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
ListFormCode = listForm.ListFormCode,
|
||||||
|
CultureName = LanguageCodes.En,
|
||||||
|
SourceDbType = DbType.String,
|
||||||
|
FieldName = "AuthorPhoneNumber",
|
||||||
|
EditGroupOrderNo = 1,
|
||||||
|
EditOrderNo = 6,
|
||||||
|
EditorType2 = EditorTypes.dxTextBox,
|
||||||
|
ColSpan = 1,
|
||||||
|
CaptionName = "App.Listform.ListformField.PhoneNumber",
|
||||||
|
Width = 0,
|
||||||
|
ListOrderNo = 8,
|
||||||
|
Visible = false,
|
||||||
|
IsActive = true,
|
||||||
|
ColumnCustomizationJson = DefaultColumnCustomizationJson,
|
||||||
|
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
|
||||||
|
PivotSettingsJson = DefaultPivotSettingsJson,
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
ListFormCode = listForm.ListFormCode,
|
||||||
|
CultureName = LanguageCodes.En,
|
||||||
|
SourceDbType = DbType.String,
|
||||||
|
FieldName = "AuthorDepartment",
|
||||||
|
EditGroupOrderNo = 1,
|
||||||
|
EditOrderNo = 7,
|
||||||
|
EditorType2 = EditorTypes.dxTextBox,
|
||||||
|
ColSpan = 1,
|
||||||
|
CaptionName = "App.Listform.ListformField.Department",
|
||||||
|
Width = 0,
|
||||||
|
ListOrderNo = 9,
|
||||||
|
Visible = false,
|
||||||
|
IsActive = true,
|
||||||
|
ColumnCustomizationJson = DefaultColumnCustomizationJson,
|
||||||
|
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
|
||||||
|
PivotSettingsJson = DefaultPivotSettingsJson,
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
ListFormCode = listForm.ListFormCode,
|
||||||
|
CultureName = LanguageCodes.En,
|
||||||
|
SourceDbType = DbType.Int32,
|
||||||
|
FieldName = "LikeCount",
|
||||||
|
CaptionName = "App.Listform.ListformField.LikeCount",
|
||||||
|
Alignment = "right",
|
||||||
|
Width = 0,
|
||||||
|
ListOrderNo = 10,
|
||||||
|
Visible = true,
|
||||||
|
IsActive = true,
|
||||||
|
ColumnCustomizationJson = DefaultColumnCustomizationJson,
|
||||||
|
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
|
||||||
|
PivotSettingsJson = DefaultPivotSettingsJson,
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
ListFormCode = listForm.ListFormCode,
|
||||||
|
CultureName = LanguageCodes.En,
|
||||||
|
SourceDbType = DbType.Boolean,
|
||||||
|
FieldName = "IsAcceptedAnswer",
|
||||||
|
EditGroupOrderNo = 1,
|
||||||
|
EditOrderNo = 8,
|
||||||
|
EditorType2 = EditorTypes.dxCheckBox,
|
||||||
|
ColSpan = 1,
|
||||||
|
CaptionName = "App.Listform.ListformField.IsAcceptedAnswer",
|
||||||
|
Width = 0,
|
||||||
|
ListOrderNo = 11,
|
||||||
|
Visible = true,
|
||||||
|
IsActive = true,
|
||||||
|
AllowSearch = true,
|
||||||
|
ColumnCustomizationJson = DefaultColumnCustomizationJson,
|
||||||
|
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
|
||||||
|
PivotSettingsJson = DefaultPivotSettingsJson,
|
||||||
|
},
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
ListFormCode = listForm.ListFormCode,
|
||||||
|
CultureName = LanguageCodes.En,
|
||||||
|
SourceDbType = DbType.Guid,
|
||||||
|
FieldName = "ParentPostId",
|
||||||
|
CaptionName = "App.Listform.ListformField.ParentPost",
|
||||||
|
Width = 0,
|
||||||
|
ListOrderNo = 12,
|
||||||
|
Visible = false,
|
||||||
|
IsActive = true,
|
||||||
|
ColumnCustomizationJson = DefaultColumnCustomizationJson,
|
||||||
|
PermissionJson = DefaultFieldPermissionJson(listForm.Name),
|
||||||
|
PivotSettingsJson = DefaultPivotSettingsJson,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
#endregion
|
||||||
|
|
||||||
#region BlogCategory
|
#region BlogCategory
|
||||||
listFormName = AppCodes.BlogManagement.BlogCategory;
|
listFormName = AppCodes.BlogManagement.BlogCategory;
|
||||||
if (!await _listFormRepository.AnyAsync(a => a.ListFormCode == listFormName))
|
if (!await _listFormRepository.AnyAsync(a => a.ListFormCode == listFormName))
|
||||||
|
|
|
||||||
|
|
@ -192,14 +192,6 @@
|
||||||
"routeType": "protected",
|
"routeType": "protected",
|
||||||
"authority": []
|
"authority": []
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"key": "admin.forumManagement",
|
|
||||||
"path": "/admin/forumManagement",
|
|
||||||
"componentType": "normal",
|
|
||||||
"componentPath": "@/views/forum/Management",
|
|
||||||
"routeType": "protected",
|
|
||||||
"authority": ["App.ForumManagement"]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"key": "admin.ai",
|
"key": "admin.ai",
|
||||||
"path": "/admin/ai",
|
"path": "/admin/ai",
|
||||||
|
|
@ -1076,12 +1068,45 @@
|
||||||
"Code": "App.ForumManagement",
|
"Code": "App.ForumManagement",
|
||||||
"DisplayName": "App.ForumManagement",
|
"DisplayName": "App.ForumManagement",
|
||||||
"Order": 16,
|
"Order": 16,
|
||||||
"Url": "/admin/forumManagement",
|
"Url": null,
|
||||||
"Icon": "FcReading",
|
"Icon": "FcReading",
|
||||||
"RequiredPermissionName": "App.ForumManagement",
|
"RequiredPermissionName": "App.ForumManagement",
|
||||||
"IsDisabled": false,
|
"IsDisabled": false,
|
||||||
"ModuleId": "Sas"
|
"ModuleId": "Sas"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"ParentCode": "App.ForumManagement",
|
||||||
|
"Code": "App.ForumManagement.Categories",
|
||||||
|
"DisplayName": "App.ForumManagement.Categories",
|
||||||
|
"Order": 1,
|
||||||
|
"Url": "/admin/list/App.ForumManagement.Categories",
|
||||||
|
"Icon": "FcFolder",
|
||||||
|
"RequiredPermissionName": "App.ForumManagement.Categories",
|
||||||
|
"IsDisabled": false,
|
||||||
|
"ModuleId": "Sas"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ParentCode": "App.ForumManagement",
|
||||||
|
"Code": "App.ForumManagement.Topics",
|
||||||
|
"DisplayName": "App.ForumManagement.Topics",
|
||||||
|
"Order": 2,
|
||||||
|
"Url": "/admin/list/App.ForumManagement.Topics",
|
||||||
|
"Icon": "FcRules",
|
||||||
|
"RequiredPermissionName": "App.ForumManagement.Topics",
|
||||||
|
"IsDisabled": false,
|
||||||
|
"ModuleId": "Sas"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ParentCode": "App.ForumManagement",
|
||||||
|
"Code": "App.ForumManagement.Posts",
|
||||||
|
"DisplayName": "App.ForumManagement.Posts",
|
||||||
|
"Order": 3,
|
||||||
|
"Url": "/admin/list/App.ForumManagement.Posts",
|
||||||
|
"Icon": "FcFeedback",
|
||||||
|
"RequiredPermissionName": "App.ForumManagement.Posts",
|
||||||
|
"IsDisabled": false,
|
||||||
|
"ModuleId": "Sas"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"ParentCode": null,
|
"ParentCode": null,
|
||||||
"Code": "App.Administration",
|
"Code": "App.Administration",
|
||||||
|
|
|
||||||
|
|
@ -2723,8 +2723,17 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"GroupName": "App.Saas",
|
"GroupName": "App.Saas",
|
||||||
"Name": "App.ForumManagement.Create",
|
"Name": "App.ForumManagement.Categories",
|
||||||
"ParentName": "App.ForumManagement",
|
"ParentName": "App.ForumManagement",
|
||||||
|
"DisplayName": "App.ForumManagement.Categories",
|
||||||
|
"IsEnabled": true,
|
||||||
|
"MultiTenancySide": 2,
|
||||||
|
"Modules": "Sas|Adm"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"GroupName": "App.Saas",
|
||||||
|
"Name": "App.ForumManagement.Categories.Create",
|
||||||
|
"ParentName": "App.ForumManagement.Categories",
|
||||||
"DisplayName": "App.Platform.Create",
|
"DisplayName": "App.Platform.Create",
|
||||||
"IsEnabled": true,
|
"IsEnabled": true,
|
||||||
"MultiTenancySide": 2,
|
"MultiTenancySide": 2,
|
||||||
|
|
@ -2732,8 +2741,17 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"GroupName": "App.Saas",
|
"GroupName": "App.Saas",
|
||||||
"Name": "App.ForumManagement.Delete",
|
"Name": "App.ForumManagement.Categories.Update",
|
||||||
"ParentName": "App.ForumManagement",
|
"ParentName": "App.ForumManagement.Categories",
|
||||||
|
"DisplayName": "App.Platform.Update",
|
||||||
|
"IsEnabled": true,
|
||||||
|
"MultiTenancySide": 2,
|
||||||
|
"Modules": "Sas|Adm"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"GroupName": "App.Saas",
|
||||||
|
"Name": "App.ForumManagement.Categories.Delete",
|
||||||
|
"ParentName": "App.ForumManagement.Categories",
|
||||||
"DisplayName": "App.Platform.Delete",
|
"DisplayName": "App.Platform.Delete",
|
||||||
"IsEnabled": true,
|
"IsEnabled": true,
|
||||||
"MultiTenancySide": 2,
|
"MultiTenancySide": 2,
|
||||||
|
|
@ -2741,8 +2759,8 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"GroupName": "App.Saas",
|
"GroupName": "App.Saas",
|
||||||
"Name": "App.ForumManagement.Export",
|
"Name": "App.ForumManagement.Categories.Export",
|
||||||
"ParentName": "App.ForumManagement",
|
"ParentName": "App.ForumManagement.Categories",
|
||||||
"DisplayName": "Export",
|
"DisplayName": "Export",
|
||||||
"IsEnabled": true,
|
"IsEnabled": true,
|
||||||
"MultiTenancySide": 2,
|
"MultiTenancySide": 2,
|
||||||
|
|
@ -2750,8 +2768,8 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"GroupName": "App.Saas",
|
"GroupName": "App.Saas",
|
||||||
"Name": "App.ForumManagement.Import",
|
"Name": "App.ForumManagement.Categories.Import",
|
||||||
"ParentName": "App.ForumManagement",
|
"ParentName": "App.ForumManagement.Categories",
|
||||||
"DisplayName": "Import",
|
"DisplayName": "Import",
|
||||||
"IsEnabled": true,
|
"IsEnabled": true,
|
||||||
"MultiTenancySide": 2,
|
"MultiTenancySide": 2,
|
||||||
|
|
@ -2759,13 +2777,112 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"GroupName": "App.Saas",
|
"GroupName": "App.Saas",
|
||||||
"Name": "App.ForumManagement.Update",
|
"Name": "App.ForumManagement.Topics",
|
||||||
"ParentName": "App.ForumManagement",
|
"ParentName": "App.ForumManagement",
|
||||||
|
"DisplayName": "App.ForumManagement.Topics",
|
||||||
|
"IsEnabled": true,
|
||||||
|
"MultiTenancySide": 2,
|
||||||
|
"Modules": "Sas|Adm"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"GroupName": "App.Saas",
|
||||||
|
"Name": "App.ForumManagement.Topics.Create",
|
||||||
|
"ParentName": "App.ForumManagement.Topics",
|
||||||
|
"DisplayName": "App.Platform.Create",
|
||||||
|
"IsEnabled": true,
|
||||||
|
"MultiTenancySide": 2,
|
||||||
|
"Modules": "Sas|Adm"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"GroupName": "App.Saas",
|
||||||
|
"Name": "App.ForumManagement.Topics.Update",
|
||||||
|
"ParentName": "App.ForumManagement.Topics",
|
||||||
"DisplayName": "App.Platform.Update",
|
"DisplayName": "App.Platform.Update",
|
||||||
"IsEnabled": true,
|
"IsEnabled": true,
|
||||||
"MultiTenancySide": 2,
|
"MultiTenancySide": 2,
|
||||||
"Modules": "Sas|Adm"
|
"Modules": "Sas|Adm"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"GroupName": "App.Saas",
|
||||||
|
"Name": "App.ForumManagement.Topics.Delete",
|
||||||
|
"ParentName": "App.ForumManagement.Topics",
|
||||||
|
"DisplayName": "App.Platform.Delete",
|
||||||
|
"IsEnabled": true,
|
||||||
|
"MultiTenancySide": 2,
|
||||||
|
"Modules": "Sas|Adm"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"GroupName": "App.Saas",
|
||||||
|
"Name": "App.ForumManagement.Topics.Export",
|
||||||
|
"ParentName": "App.ForumManagement.Topics",
|
||||||
|
"DisplayName": "Export",
|
||||||
|
"IsEnabled": true,
|
||||||
|
"MultiTenancySide": 2,
|
||||||
|
"Modules": "Sas|Adm"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"GroupName": "App.Saas",
|
||||||
|
"Name": "App.ForumManagement.Topics.Import",
|
||||||
|
"ParentName": "App.ForumManagement.Topics",
|
||||||
|
"DisplayName": "Import",
|
||||||
|
"IsEnabled": true,
|
||||||
|
"MultiTenancySide": 2,
|
||||||
|
"Modules": "Sas|Adm"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"GroupName": "App.Saas",
|
||||||
|
"Name": "App.ForumManagement.Posts",
|
||||||
|
"ParentName": "App.ForumManagement",
|
||||||
|
"DisplayName": "App.ForumManagement.Posts",
|
||||||
|
"IsEnabled": true,
|
||||||
|
"MultiTenancySide": 2,
|
||||||
|
"Modules": "Sas|Adm"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"GroupName": "App.Saas",
|
||||||
|
"Name": "App.ForumManagement.Posts.Create",
|
||||||
|
"ParentName": "App.ForumManagement.Posts",
|
||||||
|
"DisplayName": "App.Platform.Create",
|
||||||
|
"IsEnabled": true,
|
||||||
|
"MultiTenancySide": 2,
|
||||||
|
"Modules": "Sas|Adm"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"GroupName": "App.Saas",
|
||||||
|
"Name": "App.ForumManagement.Posts.Update",
|
||||||
|
"ParentName": "App.ForumManagement.Posts",
|
||||||
|
"DisplayName": "App.Platform.Update",
|
||||||
|
"IsEnabled": true,
|
||||||
|
"MultiTenancySide": 2,
|
||||||
|
"Modules": "Sas|Adm"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"GroupName": "App.Saas",
|
||||||
|
"Name": "App.ForumManagement.Posts.Delete",
|
||||||
|
"ParentName": "App.ForumManagement.Posts",
|
||||||
|
"DisplayName": "App.Platform.Delete",
|
||||||
|
"IsEnabled": true,
|
||||||
|
"MultiTenancySide": 2,
|
||||||
|
"Modules": "Sas|Adm"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"GroupName": "App.Saas",
|
||||||
|
"Name": "App.ForumManagement.Posts.Export",
|
||||||
|
"ParentName": "App.ForumManagement.Posts",
|
||||||
|
"DisplayName": "Export",
|
||||||
|
"IsEnabled": true,
|
||||||
|
"MultiTenancySide": 2,
|
||||||
|
"Modules": "Sas|Adm"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"GroupName": "App.Saas",
|
||||||
|
"Name": "App.ForumManagement.Posts.Import",
|
||||||
|
"ParentName": "App.ForumManagement.Posts",
|
||||||
|
"DisplayName": "Import",
|
||||||
|
"IsEnabled": true,
|
||||||
|
"MultiTenancySide": 2,
|
||||||
|
"Modules": "Sas|Adm"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"GroupName": "App.Administration",
|
"GroupName": "App.Administration",
|
||||||
"Name": "App.Administration.Definitions",
|
"Name": "App.Administration.Definitions",
|
||||||
|
|
|
||||||
|
|
@ -476,6 +476,15 @@ public static class SeedConsts
|
||||||
|
|
||||||
public const string Forum = Prefix.App + ".Forum";
|
public const string Forum = Prefix.App + ".Forum";
|
||||||
|
|
||||||
|
public static class ForumManagement
|
||||||
|
{
|
||||||
|
public const string Default = Prefix.App + ".ForumManagement";
|
||||||
|
|
||||||
|
public const string Categories = Default + ".Categories";
|
||||||
|
public const string Topics = Default + ".Topics";
|
||||||
|
public const string Posts = Default + ".Posts";
|
||||||
|
}
|
||||||
|
|
||||||
//Administration
|
//Administration
|
||||||
public const string Administration = Prefix.App + ".Administration";
|
public const string Administration = Prefix.App + ".Administration";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,38 +1,4 @@
|
||||||
{
|
{
|
||||||
"ForumCategories": [
|
|
||||||
{
|
|
||||||
"Name": "Genel Tartışma",
|
|
||||||
"Slug": "genel-tartisma",
|
|
||||||
"Description": "Her türlü konunun tartışılabileceği genel forum alanı",
|
|
||||||
"Icon": "💬",
|
|
||||||
"DisplayOrder": 1,
|
|
||||||
"IsActive": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Name": "Teknik Destek",
|
|
||||||
"Slug": "teknik-destek",
|
|
||||||
"Description": "Teknik sorunlar ve çözümler için destek forumu",
|
|
||||||
"Icon": "🔧",
|
|
||||||
"DisplayOrder": 2,
|
|
||||||
"IsActive": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Name": "Öneriler",
|
|
||||||
"Slug": "oneriler",
|
|
||||||
"Description": "Platform geliştirmeleri için öneri ve istekler",
|
|
||||||
"Icon": "💡",
|
|
||||||
"DisplayOrder": 3,
|
|
||||||
"IsActive": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"Name": "Duyurular",
|
|
||||||
"Slug": "duyurular",
|
|
||||||
"Description": "Platform duyuruları ve güncellemeler",
|
|
||||||
"Icon": "📢",
|
|
||||||
"DisplayOrder": 4,
|
|
||||||
"IsActive": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"Abouts": [
|
"Abouts": [
|
||||||
{
|
{
|
||||||
"stats": [
|
"stats": [
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,6 @@ public class TenantSeederDto
|
||||||
|
|
||||||
|
|
||||||
//Forum
|
//Forum
|
||||||
public List<ForumCategorySeedDto> ForumCategories { get; set; }
|
|
||||||
|
|
||||||
//Intranet
|
//Intranet
|
||||||
public List<SocialPostSeedDto> SocialPosts { get; set; }
|
public List<SocialPostSeedDto> SocialPosts { get; set; }
|
||||||
|
|
@ -132,16 +131,6 @@ public class EventPhotoSeedDto
|
||||||
public string Url { get; set; }
|
public string Url { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public class ForumCategorySeedDto
|
|
||||||
{
|
|
||||||
public string Name { get; set; }
|
|
||||||
public string Slug { get; set; }
|
|
||||||
public string Description { get; set; }
|
|
||||||
public string Icon { get; set; }
|
|
||||||
public int DisplayOrder { get; set; }
|
|
||||||
public bool IsActive { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class ServiceSeedDto
|
public class ServiceSeedDto
|
||||||
{
|
{
|
||||||
public string Icon { get; set; }
|
public string Icon { get; set; }
|
||||||
|
|
@ -228,7 +217,6 @@ public class TenantDataSeeder : IDataSeedContributor, ITransientDependency
|
||||||
private readonly IRepository<About, Guid> _aboutRepository;
|
private readonly IRepository<About, Guid> _aboutRepository;
|
||||||
private readonly IRepository<Service, Guid> _servicesRepository;
|
private readonly IRepository<Service, Guid> _servicesRepository;
|
||||||
private readonly IRepository<Contact, Guid> _contactRepository;
|
private readonly IRepository<Contact, Guid> _contactRepository;
|
||||||
private readonly IRepository<ForumCategory, Guid> _forumCategoryRepository;
|
|
||||||
private readonly IIdentityUserRepository _repositoryUser;
|
private readonly IIdentityUserRepository _repositoryUser;
|
||||||
private readonly IRepository<SocialPost, Guid> _socialPostRepository;
|
private readonly IRepository<SocialPost, Guid> _socialPostRepository;
|
||||||
private readonly IRepository<SocialMedia, Guid> _socialMediaRepository;
|
private readonly IRepository<SocialMedia, Guid> _socialMediaRepository;
|
||||||
|
|
@ -249,7 +237,6 @@ public class TenantDataSeeder : IDataSeedContributor, ITransientDependency
|
||||||
IRepository<About, Guid> aboutRepository,
|
IRepository<About, Guid> aboutRepository,
|
||||||
IRepository<Service, Guid> servicesRepository,
|
IRepository<Service, Guid> servicesRepository,
|
||||||
IRepository<Contact, Guid> contactRepository,
|
IRepository<Contact, Guid> contactRepository,
|
||||||
IRepository<ForumCategory, Guid> forumCategoryRepository,
|
|
||||||
|
|
||||||
IRepository<SocialPost, Guid> socialPostRepository,
|
IRepository<SocialPost, Guid> socialPostRepository,
|
||||||
IRepository<SocialMedia, Guid> socialMediaRepository,
|
IRepository<SocialMedia, Guid> socialMediaRepository,
|
||||||
|
|
@ -268,7 +255,6 @@ public class TenantDataSeeder : IDataSeedContributor, ITransientDependency
|
||||||
_servicesRepository = servicesRepository;
|
_servicesRepository = servicesRepository;
|
||||||
_aboutRepository = aboutRepository;
|
_aboutRepository = aboutRepository;
|
||||||
_contactRepository = contactRepository;
|
_contactRepository = contactRepository;
|
||||||
_forumCategoryRepository = forumCategoryRepository;
|
|
||||||
_socialPostRepository = socialPostRepository;
|
_socialPostRepository = socialPostRepository;
|
||||||
_socialMediaRepository = socialMediaRepository;
|
_socialMediaRepository = socialMediaRepository;
|
||||||
_socialPollOptionRepository = socialPollOptionRepository;
|
_socialPollOptionRepository = socialPollOptionRepository;
|
||||||
|
|
@ -293,25 +279,6 @@ public class TenantDataSeeder : IDataSeedContributor, ITransientDependency
|
||||||
|
|
||||||
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
|
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
|
||||||
|
|
||||||
foreach (var item in items.ForumCategories)
|
|
||||||
{
|
|
||||||
var exists = await _forumCategoryRepository.AnyAsync(x => x.Name == item.Name);
|
|
||||||
|
|
||||||
if (!exists)
|
|
||||||
{
|
|
||||||
var newCategory = new ForumCategory
|
|
||||||
{
|
|
||||||
Name = item.Name,
|
|
||||||
Slug = item.Slug,
|
|
||||||
Description = item.Description,
|
|
||||||
Icon = item.Icon,
|
|
||||||
DisplayOrder = item.DisplayOrder
|
|
||||||
};
|
|
||||||
|
|
||||||
await _forumCategoryRepository.InsertAsync(newCategory, autoSave: true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var item in items.Services)
|
foreach (var item in items.Services)
|
||||||
{
|
{
|
||||||
var exists = await _servicesRepository.AnyAsync(x => x.Title == item.Title);
|
var exists = await _servicesRepository.AnyAsync(x => x.Title == item.Title);
|
||||||
|
|
|
||||||
48
configs/seeds/host/data/App.ForumManagement.Categories.json
Normal file
48
configs/seeds/host/data/App.ForumManagement.Categories.json
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
{
|
||||||
|
"ListFormCode": "App.ForumManagement.Categories",
|
||||||
|
"KeyFieldName": "Id",
|
||||||
|
"GeneratedAt": "2026-09-04T00:00:00Z",
|
||||||
|
"Order": 0,
|
||||||
|
"Rows": [
|
||||||
|
{
|
||||||
|
"Id": "c37dd21d-cbcb-535c-a97d-be0e2f5570d1",
|
||||||
|
"Name": "Genel Tartışma",
|
||||||
|
"Slug": "genel-tartisma",
|
||||||
|
"Description": "Her türlü konunun tartışılabileceği genel forum alanı",
|
||||||
|
"Icon": "💬",
|
||||||
|
"DisplayOrder": 1,
|
||||||
|
"IsActive": true,
|
||||||
|
"IsLocked": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Id": "1f9b8e9a-6b39-512f-9edc-33a9b5948a84",
|
||||||
|
"Name": "Teknik Destek",
|
||||||
|
"Slug": "teknik-destek",
|
||||||
|
"Description": "Teknik sorunlar ve çözümler için destek forumu",
|
||||||
|
"Icon": "🔧",
|
||||||
|
"DisplayOrder": 2,
|
||||||
|
"IsActive": true,
|
||||||
|
"IsLocked": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Id": "657f4136-42b1-534f-afe1-fbb8045a023c",
|
||||||
|
"Name": "Öneriler",
|
||||||
|
"Slug": "oneriler",
|
||||||
|
"Description": "Platform geliştirmeleri için öneri ve istekler",
|
||||||
|
"Icon": "💡",
|
||||||
|
"DisplayOrder": 3,
|
||||||
|
"IsActive": true,
|
||||||
|
"IsLocked": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Id": "3a505a92-5302-5852-a95b-1764fd60ad77",
|
||||||
|
"Name": "Duyurular",
|
||||||
|
"Slug": "duyurular",
|
||||||
|
"Description": "Platform duyuruları ve güncellemeler",
|
||||||
|
"Icon": "📢",
|
||||||
|
"DisplayOrder": 4,
|
||||||
|
"IsActive": true,
|
||||||
|
"IsLocked": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
export const ROUTES_ENUM = {
|
export const ROUTES_ENUM = {
|
||||||
setup: '/setup',
|
setup: '/setup',
|
||||||
public: {
|
public: {
|
||||||
home: '/home',
|
home: '/home',
|
||||||
|
|
@ -34,7 +34,6 @@ export const ROUTES_ENUM = {
|
||||||
wizardManager: '/admin/listform/wizardManager',
|
wizardManager: '/admin/listform/wizardManager',
|
||||||
edit: '/admin/listform/edit/:listFormCode',
|
edit: '/admin/listform/edit/:listFormCode',
|
||||||
},
|
},
|
||||||
forumManagement: '/admin/forumManagement',
|
|
||||||
developerKit: {
|
developerKit: {
|
||||||
dashboard: '/admin/developerkit',
|
dashboard: '/admin/developerkit',
|
||||||
entities: '/admin/developerkit/entities',
|
entities: '/admin/developerkit/entities',
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { ForumCategory, ForumPost, ForumTopic } from '@/proxy/forum/forum'
|
import { ForumCategory, ForumPost, ForumTopic } from '@/proxy/forum/forum'
|
||||||
import apiService from '@/services/api.service'
|
import apiService from '@/services/api.service'
|
||||||
|
|
||||||
export interface ForumSearchResult {
|
export interface ForumSearchResult {
|
||||||
|
|
@ -27,17 +27,6 @@ export interface PaginatedResponse<T> {
|
||||||
totalPages: number
|
totalPages: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateCategoryRequest {
|
|
||||||
name: string
|
|
||||||
slug: string
|
|
||||||
description: string
|
|
||||||
icon: string
|
|
||||||
displayOrder: number
|
|
||||||
isActive: boolean
|
|
||||||
isLocked: boolean
|
|
||||||
tenantId?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CreateTopicRequest {
|
export interface CreateTopicRequest {
|
||||||
title: string
|
title: string
|
||||||
content: string
|
content: string
|
||||||
|
|
@ -116,49 +105,6 @@ class ForumService {
|
||||||
return response.data
|
return response.data
|
||||||
}
|
}
|
||||||
|
|
||||||
async createCategory(data: CreateCategoryRequest): Promise<ForumCategory> {
|
|
||||||
const response = await apiService.fetchData<ForumCategory>({
|
|
||||||
url: '/api/app/forum/category',
|
|
||||||
method: 'POST',
|
|
||||||
data: {...data},
|
|
||||||
})
|
|
||||||
return response.data
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateCategory(id: string, data: Partial<CreateCategoryRequest>): Promise<ForumCategory> {
|
|
||||||
const response = await apiService.fetchData<ForumCategory>({
|
|
||||||
url: `/api/app/forum/${id}/category`,
|
|
||||||
method: 'PUT',
|
|
||||||
data,
|
|
||||||
})
|
|
||||||
return response.data
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateCategoryLockState(id: string): Promise<ForumCategory> {
|
|
||||||
const response = await apiService.fetchData<ForumCategory>({
|
|
||||||
url: `/api/app/forum/${id}/category-lock`,
|
|
||||||
method: 'PUT',
|
|
||||||
})
|
|
||||||
|
|
||||||
return response.data
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateCategoryActiveState(id: string): Promise<ForumCategory> {
|
|
||||||
const response = await apiService.fetchData<ForumCategory>({
|
|
||||||
url: `/api/app/forum/${id}/category-active`,
|
|
||||||
method: 'PUT',
|
|
||||||
})
|
|
||||||
|
|
||||||
return response.data
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteCategory(id: string): Promise<void> {
|
|
||||||
await apiService.fetchData({
|
|
||||||
url: `/api/app/forum/${id}/category`,
|
|
||||||
method: 'DELETE',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Topics
|
// Topics
|
||||||
async getTopics(params: TopicListParams = {}): Promise<PaginatedResponse<ForumTopic>> {
|
async getTopics(params: TopicListParams = {}): Promise<PaginatedResponse<ForumTopic>> {
|
||||||
const response = await apiService.fetchData<PaginatedResponse<ForumTopic>>({
|
const response = await apiService.fetchData<PaginatedResponse<ForumTopic>>({
|
||||||
|
|
|
||||||
|
|
@ -1,82 +0,0 @@
|
||||||
import { useEffect } from 'react'
|
|
||||||
import { useForumData } from './useForumData'
|
|
||||||
import { AdminView } from './admin/AdminView'
|
|
||||||
import { ForumErrorBanner } from './ForumErrorBanner'
|
|
||||||
import { Container } from '@/components/shared'
|
|
||||||
import PageTitle from '@/components/shared/PageTitle'
|
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
|
||||||
|
|
||||||
export function Management() {
|
|
||||||
const {
|
|
||||||
categories,
|
|
||||||
topics,
|
|
||||||
posts,
|
|
||||||
totalCounts,
|
|
||||||
loading,
|
|
||||||
error,
|
|
||||||
createCategory,
|
|
||||||
updateCategory,
|
|
||||||
updateCategoryLockState,
|
|
||||||
updateCategoryActiveState,
|
|
||||||
deleteCategory,
|
|
||||||
createTopic,
|
|
||||||
updateTopic,
|
|
||||||
deleteTopic,
|
|
||||||
pinTopic,
|
|
||||||
unpinTopic,
|
|
||||||
lockTopic,
|
|
||||||
unlockTopic,
|
|
||||||
solvedTopic,
|
|
||||||
unsolvedTopic,
|
|
||||||
createPost,
|
|
||||||
updatePost,
|
|
||||||
deletePost,
|
|
||||||
markPostAsAcceptedAnswer,
|
|
||||||
unmarkPostAsAcceptedAnswer,
|
|
||||||
clearError,
|
|
||||||
} = useForumData()
|
|
||||||
const { translate } = useLocalization()
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!error) return
|
|
||||||
const timer = setTimeout(clearError, 5000)
|
|
||||||
return () => clearTimeout(timer)
|
|
||||||
}, [error, clearError])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Container>
|
|
||||||
<PageTitle title={translate('::App.ForumManagement')} />
|
|
||||||
|
|
||||||
<ForumErrorBanner error={error} onDismiss={clearError} />
|
|
||||||
|
|
||||||
<AdminView
|
|
||||||
categories={categories}
|
|
||||||
topics={topics}
|
|
||||||
posts={posts}
|
|
||||||
totalCounts={totalCounts}
|
|
||||||
loading={loading}
|
|
||||||
onCreateCategory={createCategory}
|
|
||||||
onUpdateCategory={updateCategory}
|
|
||||||
onUpdateCategoryLockState={updateCategoryLockState}
|
|
||||||
onUpdateCategoryActiveState={updateCategoryActiveState}
|
|
||||||
onDeleteCategory={deleteCategory}
|
|
||||||
onCreateTopic={createTopic}
|
|
||||||
onUpdateTopic={updateTopic}
|
|
||||||
onDeleteTopic={deleteTopic}
|
|
||||||
onPinTopic={pinTopic}
|
|
||||||
onUnpinTopic={unpinTopic}
|
|
||||||
onLockTopic={lockTopic}
|
|
||||||
onUnlockTopic={unlockTopic}
|
|
||||||
onMarkTopicAsSolved={solvedTopic}
|
|
||||||
onMarkTopicAsUnsolved={unsolvedTopic}
|
|
||||||
onCreatePost={createPost}
|
|
||||||
onUpdatePost={updatePost}
|
|
||||||
onDeletePost={deletePost}
|
|
||||||
onMarkPostAsAcceptedAnswer={markPostAsAcceptedAnswer}
|
|
||||||
onUnmarkPostAsAcceptedAnswer={unmarkPostAsAcceptedAnswer}
|
|
||||||
/>
|
|
||||||
</Container>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Management
|
|
||||||
|
|
@ -1,174 +0,0 @@
|
||||||
import { useState } from 'react'
|
|
||||||
import { FaFolder, FaCommentAlt, FaFileAlt, FaChartBar } from 'react-icons/fa'
|
|
||||||
import { CategoryManagement } from './CategoryManagement'
|
|
||||||
import { TopicManagement } from './TopicManagement'
|
|
||||||
import { PostManagement } from './PostManagement'
|
|
||||||
import { ForumCategory, ForumPost, ForumTopic } from '@/proxy/forum/forum'
|
|
||||||
import {
|
|
||||||
CreateCategoryRequest,
|
|
||||||
CreatePostRequest,
|
|
||||||
CreateTopicRequest,
|
|
||||||
} from '@/services/forum.service'
|
|
||||||
import { AdminStats } from './Dashboard'
|
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
|
||||||
import { Button } from '@/components/ui'
|
|
||||||
|
|
||||||
interface AdminViewProps {
|
|
||||||
categories: ForumCategory[]
|
|
||||||
topics: ForumTopic[]
|
|
||||||
posts: ForumPost[]
|
|
||||||
totalCounts: { categories: number; topics: number; posts: number }
|
|
||||||
loading: boolean
|
|
||||||
onCreateCategory: (category: CreateCategoryRequest) => Promise<unknown>
|
|
||||||
onUpdateCategory: (id: string, category: Partial<ForumCategory>) => Promise<unknown>
|
|
||||||
onUpdateCategoryLockState: (id: string) => Promise<unknown>
|
|
||||||
onUpdateCategoryActiveState: (id: string) => Promise<unknown>
|
|
||||||
onDeleteCategory: (id: string) => Promise<unknown>
|
|
||||||
onCreateTopic: (topic: CreateTopicRequest) => Promise<unknown>
|
|
||||||
onUpdateTopic: (id: string, topic: Partial<ForumTopic>) => Promise<unknown>
|
|
||||||
onDeleteTopic: (id: string) => Promise<unknown>
|
|
||||||
onPinTopic: (id: string) => Promise<unknown>
|
|
||||||
onUnpinTopic: (id: string) => Promise<unknown>
|
|
||||||
onLockTopic: (id: string) => Promise<unknown>
|
|
||||||
onUnlockTopic: (id: string) => Promise<unknown>
|
|
||||||
onMarkTopicAsSolved: (id: string) => Promise<unknown>
|
|
||||||
onMarkTopicAsUnsolved: (id: string) => Promise<unknown>
|
|
||||||
onCreatePost: (post: CreatePostRequest) => Promise<unknown>
|
|
||||||
onUpdatePost: (id: string, post: Partial<ForumPost>) => Promise<unknown>
|
|
||||||
onDeletePost: (id: string) => Promise<unknown>
|
|
||||||
onMarkPostAsAcceptedAnswer: (id: string) => Promise<unknown>
|
|
||||||
onUnmarkPostAsAcceptedAnswer: (id: string) => Promise<unknown>
|
|
||||||
}
|
|
||||||
|
|
||||||
type AdminSection = 'stats' | 'categories' | 'topics' | 'posts'
|
|
||||||
|
|
||||||
export function AdminView({
|
|
||||||
categories,
|
|
||||||
topics,
|
|
||||||
posts,
|
|
||||||
totalCounts,
|
|
||||||
loading,
|
|
||||||
onCreateCategory,
|
|
||||||
onUpdateCategory,
|
|
||||||
onDeleteCategory,
|
|
||||||
onUpdateCategoryLockState,
|
|
||||||
onUpdateCategoryActiveState,
|
|
||||||
onCreateTopic,
|
|
||||||
onUpdateTopic,
|
|
||||||
onDeleteTopic,
|
|
||||||
onPinTopic,
|
|
||||||
onUnpinTopic,
|
|
||||||
onLockTopic,
|
|
||||||
onUnlockTopic,
|
|
||||||
onMarkTopicAsSolved,
|
|
||||||
onMarkTopicAsUnsolved,
|
|
||||||
onCreatePost,
|
|
||||||
onUpdatePost,
|
|
||||||
onDeletePost,
|
|
||||||
onMarkPostAsAcceptedAnswer,
|
|
||||||
onUnmarkPostAsAcceptedAnswer,
|
|
||||||
}: AdminViewProps) {
|
|
||||||
const [activeSection, setActiveSection] = useState<AdminSection>('stats')
|
|
||||||
const { translate } = useLocalization()
|
|
||||||
|
|
||||||
const navigationItems = [
|
|
||||||
{
|
|
||||||
id: 'stats' as AdminSection,
|
|
||||||
label: translate('::App.Forum.Dashboard'),
|
|
||||||
icon: FaChartBar,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'categories' as AdminSection,
|
|
||||||
label: translate('::App.Platform.Category'),
|
|
||||||
icon: FaFolder,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'topics' as AdminSection,
|
|
||||||
label: translate('::App.ForumDashboard.Topics'),
|
|
||||||
icon: FaCommentAlt,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'posts' as AdminSection,
|
|
||||||
label: translate('::App.ForumDashboard.Posts'),
|
|
||||||
icon: FaFileAlt,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col lg:flex-row gap-4 mt-3">
|
|
||||||
{/* Sidebar Navigation */}
|
|
||||||
<div className="lg:w-64 flex-shrink-0 p-4 bg-gray-50 dark:bg-gray-900 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700">
|
|
||||||
<nav className="space-y-2">
|
|
||||||
{navigationItems.map((item) => {
|
|
||||||
const Icon = item.icon
|
|
||||||
const isActive = activeSection === item.id
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
key={item.id}
|
|
||||||
variant={isActive ? 'solid' : 'plain'}
|
|
||||||
active={isActive}
|
|
||||||
aria-current={isActive ? 'page' : undefined}
|
|
||||||
className="flex w-full items-center space-x-3 px-4 py-3 text-left transition-colors"
|
|
||||||
onClick={() => setActiveSection(item.id)}
|
|
||||||
>
|
|
||||||
<Icon className="h-5 w-5" />
|
|
||||||
<span className="font-medium">{item.label}</span>
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* 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} totalCounts={totalCounts} />
|
|
||||||
)}
|
|
||||||
|
|
||||||
{activeSection === 'categories' && (
|
|
||||||
<CategoryManagement
|
|
||||||
categories={categories}
|
|
||||||
loading={loading}
|
|
||||||
onCreateCategory={onCreateCategory}
|
|
||||||
onUpdateCategory={onUpdateCategory}
|
|
||||||
onDeleteCategory={onDeleteCategory}
|
|
||||||
onUpdateCategoryLockState={onUpdateCategoryLockState}
|
|
||||||
onUpdateCategoryActiveState={onUpdateCategoryActiveState}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{activeSection === 'topics' && (
|
|
||||||
<TopicManagement
|
|
||||||
topics={topics}
|
|
||||||
totalCount={totalCounts.topics}
|
|
||||||
categories={categories}
|
|
||||||
loading={loading}
|
|
||||||
onCreateTopic={onCreateTopic}
|
|
||||||
onUpdateTopic={onUpdateTopic}
|
|
||||||
onDeleteTopic={onDeleteTopic}
|
|
||||||
onPinTopic={onPinTopic}
|
|
||||||
onUnpinTopic={onUnpinTopic}
|
|
||||||
onLockTopic={onLockTopic}
|
|
||||||
onUnlockTopic={onUnlockTopic}
|
|
||||||
onMarkTopicAsSolved={onMarkTopicAsSolved}
|
|
||||||
onMarkTopicAsUnsolved={onMarkTopicAsUnsolved}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{activeSection === 'posts' && (
|
|
||||||
<PostManagement
|
|
||||||
posts={posts}
|
|
||||||
totalCount={totalCounts.posts}
|
|
||||||
topics={topics}
|
|
||||||
loading={loading}
|
|
||||||
onCreatePost={onCreatePost}
|
|
||||||
onUpdatePost={onUpdatePost}
|
|
||||||
onDeletePost={onDeletePost}
|
|
||||||
onMarkPostAsAcceptedAnswer={onMarkPostAsAcceptedAnswer}
|
|
||||||
onUnmarkPostAsAcceptedAnswer={onUnmarkPostAsAcceptedAnswer}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,423 +0,0 @@
|
||||||
import { useState } from 'react'
|
|
||||||
import {
|
|
||||||
FaPlus,
|
|
||||||
FaEdit,
|
|
||||||
FaTrash,
|
|
||||||
FaLock,
|
|
||||||
FaUnlock,
|
|
||||||
FaEye,
|
|
||||||
FaEyeSlash,
|
|
||||||
FaSpinner,
|
|
||||||
} from 'react-icons/fa'
|
|
||||||
import { ForumCategory } from '@/proxy/forum/forum'
|
|
||||||
import { CreateCategoryRequest } from '@/services/forum.service'
|
|
||||||
import { useStoreState } from '@/store/store'
|
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
|
||||||
import { Formik, Form, Field, FieldProps } from 'formik'
|
|
||||||
import * as Yup from 'yup'
|
|
||||||
import { FormContainer, FormItem } from '@/components/ui/Form'
|
|
||||||
import { Input, Checkbox, Button, Select, Dialog } from '@/components/ui'
|
|
||||||
import { ConfirmDialog } from '@/components/shared'
|
|
||||||
import { CategoryIconOption, resolveCategoryIconOptions } from './categoryIcons'
|
|
||||||
|
|
||||||
const FIELD_CLASS =
|
|
||||||
'w-full border border-gray-300 dark:border-gray-700 rounded-lg px-3 py-2 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500'
|
|
||||||
|
|
||||||
const CategorySchema = Yup.object().shape({
|
|
||||||
name: Yup.string().trim().required(),
|
|
||||||
slug: Yup.string().trim().required(),
|
|
||||||
description: Yup.string().trim().required(),
|
|
||||||
icon: Yup.string(),
|
|
||||||
displayOrder: Yup.number().required(),
|
|
||||||
isActive: Yup.boolean(),
|
|
||||||
isLocked: Yup.boolean(),
|
|
||||||
})
|
|
||||||
|
|
||||||
interface CategoryManagementProps {
|
|
||||||
categories: ForumCategory[]
|
|
||||||
loading: boolean
|
|
||||||
onCreateCategory: (category: CreateCategoryRequest) => Promise<unknown>
|
|
||||||
onUpdateCategory: (id: string, category: Partial<ForumCategory>) => Promise<unknown>
|
|
||||||
onDeleteCategory: (id: string) => Promise<unknown>
|
|
||||||
onUpdateCategoryLockState: (id: string) => Promise<unknown>
|
|
||||||
onUpdateCategoryActiveState: (id: string) => Promise<unknown>
|
|
||||||
}
|
|
||||||
|
|
||||||
export function CategoryManagement({
|
|
||||||
categories,
|
|
||||||
loading,
|
|
||||||
onCreateCategory,
|
|
||||||
onUpdateCategory,
|
|
||||||
onDeleteCategory,
|
|
||||||
onUpdateCategoryLockState,
|
|
||||||
onUpdateCategoryActiveState,
|
|
||||||
}: CategoryManagementProps) {
|
|
||||||
const { translate } = useLocalization()
|
|
||||||
const { tenant } = useStoreState((state) => state.auth)
|
|
||||||
const [showCreateForm, setShowCreateForm] = useState(false)
|
|
||||||
const [editingCategory, setEditingCategory] = useState<ForumCategory | null>(null)
|
|
||||||
const [showConfirm, setShowConfirm] = useState(false)
|
|
||||||
const [categoryToDelete, setCategoryToDelete] = useState<ForumCategory | null>(null)
|
|
||||||
|
|
||||||
const confirmDeleteCategory = (category: ForumCategory) => {
|
|
||||||
setCategoryToDelete(category)
|
|
||||||
setShowConfirm(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleEdit = (category: ForumCategory) => {
|
|
||||||
setEditingCategory(category)
|
|
||||||
setShowCreateForm(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleCloseForm = () => {
|
|
||||||
setShowCreateForm(false)
|
|
||||||
setEditingCategory(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleToggleActive = async (category: ForumCategory) => {
|
|
||||||
try {
|
|
||||||
await onUpdateCategoryActiveState(category.id)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error toggling category status:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleToggleLocked = async (category: ForumCategory) => {
|
|
||||||
try {
|
|
||||||
await onUpdateCategoryLockState(category.id)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error toggling category lock:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-3">
|
|
||||||
{/* Create/Edit Modal */}
|
|
||||||
<Dialog
|
|
||||||
isOpen={showCreateForm}
|
|
||||||
width={720}
|
|
||||||
onClose={handleCloseForm}
|
|
||||||
onRequestClose={handleCloseForm}
|
|
||||||
>
|
|
||||||
<div className="pb-4 border-b border-gray-200 dark:border-gray-700">
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
|
|
||||||
{editingCategory
|
|
||||||
? translate('::App.ForumCategoryManagement.EditCategory')
|
|
||||||
: translate('::App.ForumCategoryManagement.AddCategory')}
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="pt-4 max-h-[70vh] overflow-y-auto">
|
|
||||||
<Formik
|
|
||||||
enableReinitialize
|
|
||||||
initialValues={{
|
|
||||||
name: editingCategory?.name ?? '',
|
|
||||||
slug: editingCategory?.slug ?? '',
|
|
||||||
description: editingCategory?.description ?? '',
|
|
||||||
icon: editingCategory?.icon ?? '',
|
|
||||||
displayOrder: editingCategory?.displayOrder ?? 0,
|
|
||||||
isActive: editingCategory?.isActive ?? true,
|
|
||||||
isLocked: editingCategory?.isLocked ?? false,
|
|
||||||
tenantId: tenant?.tenantId ?? '',
|
|
||||||
}}
|
|
||||||
validationSchema={CategorySchema}
|
|
||||||
onSubmit={async (values, { setSubmitting, resetForm }) => {
|
|
||||||
try {
|
|
||||||
setSubmitting(true)
|
|
||||||
if (editingCategory) {
|
|
||||||
await onUpdateCategory(editingCategory.id, values)
|
|
||||||
} else {
|
|
||||||
await onCreateCategory(values)
|
|
||||||
}
|
|
||||||
resetForm()
|
|
||||||
setShowCreateForm(false)
|
|
||||||
setEditingCategory(null)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error submitting form:', error)
|
|
||||||
} finally {
|
|
||||||
setSubmitting(false)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{({ values, errors, touched, isSubmitting }) => (
|
|
||||||
<Form>
|
|
||||||
<FormContainer className="space-y-4">
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<FormItem
|
|
||||||
asterisk
|
|
||||||
label={translate('::App.Listform.ListformField.Name')}
|
|
||||||
invalid={!!errors.name && !!touched.name}
|
|
||||||
errorMessage={errors.name}
|
|
||||||
>
|
|
||||||
<Field name="name" className={FIELD_CLASS} component={Input} />
|
|
||||||
</FormItem>
|
|
||||||
|
|
||||||
<FormItem
|
|
||||||
asterisk
|
|
||||||
label={translate('::App.Listform.ListformField.Slug')}
|
|
||||||
invalid={!!errors.slug && !!touched.slug}
|
|
||||||
errorMessage={errors.slug}
|
|
||||||
>
|
|
||||||
<Field name="slug" className={FIELD_CLASS} component={Input} />
|
|
||||||
</FormItem>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<FormItem
|
|
||||||
asterisk
|
|
||||||
label={translate('::App.Listform.ListformField.Description')}
|
|
||||||
invalid={!!errors.description && !!touched.description}
|
|
||||||
errorMessage={errors.description}
|
|
||||||
>
|
|
||||||
<Field
|
|
||||||
textArea
|
|
||||||
name="description"
|
|
||||||
className={FIELD_CLASS}
|
|
||||||
component={Input}
|
|
||||||
/>
|
|
||||||
</FormItem>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<FormItem
|
|
||||||
label={translate('::App.MenuField.Icon')}
|
|
||||||
invalid={!!errors.icon && !!touched.icon}
|
|
||||||
errorMessage={errors.icon}
|
|
||||||
>
|
|
||||||
<Field name="icon">
|
|
||||||
{({ field, form }: FieldProps<string>) => {
|
|
||||||
const iconOptions = resolveCategoryIconOptions(values.icon)
|
|
||||||
return (
|
|
||||||
<Select<CategoryIconOption>
|
|
||||||
isClearable
|
|
||||||
field={field}
|
|
||||||
form={form}
|
|
||||||
options={iconOptions}
|
|
||||||
placeholder="💬"
|
|
||||||
value={iconOptions.filter((option) => option.value === values.icon)}
|
|
||||||
formatOptionLabel={(option) => (
|
|
||||||
<span className="flex items-center gap-2">
|
|
||||||
<span className="text-lg">{option.value}</span>
|
|
||||||
<span>{option.label}</span>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
onChange={(option) =>
|
|
||||||
form.setFieldValue('icon', option?.value ?? '')
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</Field>
|
|
||||||
</FormItem>
|
|
||||||
|
|
||||||
<FormItem
|
|
||||||
asterisk
|
|
||||||
label={translate('::App.Listform.ListformField.DisplayOrder')}
|
|
||||||
invalid={!!errors.displayOrder && !!touched.displayOrder}
|
|
||||||
errorMessage={errors.displayOrder}
|
|
||||||
>
|
|
||||||
<Field
|
|
||||||
name="displayOrder"
|
|
||||||
className={FIELD_CLASS}
|
|
||||||
placeholder="0"
|
|
||||||
type="number"
|
|
||||||
component={Input}
|
|
||||||
/>
|
|
||||||
</FormItem>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center space-x-4 pt-6">
|
|
||||||
<FormItem label={translate('::App.Listform.ListformField.Status')}>
|
|
||||||
<Field as={Checkbox} name="isActive" />
|
|
||||||
</FormItem>
|
|
||||||
<FormItem label={translate('::App.Platform.Locked')}>
|
|
||||||
<Field as={Checkbox} name="isLocked" />
|
|
||||||
</FormItem>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mt-4 flex justify-end space-x-3">
|
|
||||||
<Button variant="plain" type="button" onClick={handleCloseForm}>
|
|
||||||
{translate('::App.Platform.Cancel')}
|
|
||||||
</Button>
|
|
||||||
<Button variant="solid" type="submit" loading={isSubmitting}>
|
|
||||||
{editingCategory
|
|
||||||
? translate('::App.Platform.Update')
|
|
||||||
: translate('::App.Platform.Create')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</FormContainer>
|
|
||||||
</Form>
|
|
||||||
)}
|
|
||||||
</Formik>
|
|
||||||
</div>
|
|
||||||
</Dialog>
|
|
||||||
|
|
||||||
{/* Categories 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">
|
|
||||||
{translate('::App.Platform.Category')} ({categories.length})
|
|
||||||
</h3>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="solid"
|
|
||||||
disabled={loading}
|
|
||||||
className="flex items-center space-x-2"
|
|
||||||
onClick={() => setShowCreateForm(true)}
|
|
||||||
>
|
|
||||||
<FaPlus className="w-4 h-4" />
|
|
||||||
<span>{translate('::App.ForumCategoryManagement.AddCategory')}</span>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{loading ? (
|
|
||||||
<div className="p-8 text-center">
|
|
||||||
<FaSpinner className="w-8 h-8 animate-spin mx-auto mb-4 text-blue-600 dark:text-blue-400" />
|
|
||||||
<p className="text-gray-500 dark:text-gray-400">
|
|
||||||
{translate('::App.ForumCategoryManagement.Loadingcategories')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="divide-y divide-gray-200 dark:divide-gray-800">
|
|
||||||
{categories.length === 0 && (
|
|
||||||
<p className="p-8 text-center text-gray-500 dark:text-gray-400">
|
|
||||||
{translate('::App.Platform.NoDataFound')}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{[...categories]
|
|
||||||
.sort((a, b) => a.displayOrder - b.displayOrder)
|
|
||||||
.map((category) => (
|
|
||||||
<div
|
|
||||||
key={category.id}
|
|
||||||
className="p-6 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
|
|
||||||
>
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-start space-x-4">
|
|
||||||
<div className="text-2xl">{category.icon}</div>
|
|
||||||
<div>
|
|
||||||
<div className="flex items-center space-x-2 mb-1">
|
|
||||||
<h4 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
|
|
||||||
{category.name}
|
|
||||||
</h4>
|
|
||||||
{!category.isActive && (
|
|
||||||
<span className="px-2 py-1 bg-red-100 dark:bg-red-900 text-red-700 dark:text-red-300 text-xs rounded-full">
|
|
||||||
{translate('::App.Platform.Passive')}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{category.isLocked && (
|
|
||||||
<span className="px-2 py-1 bg-yellow-100 dark:bg-yellow-900 text-yellow-700 dark:text-yellow-300 text-xs rounded-full">
|
|
||||||
{translate('::App.Platform.Locked')}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<p className="text-gray-600 dark:text-gray-300 mb-2">
|
|
||||||
{category.description}
|
|
||||||
</p>
|
|
||||||
<div className="flex items-center space-x-4 text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
<span>
|
|
||||||
{category.topicCount} {translate('::App.ForumDashboard.Topics')}
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
{category.postCount} {translate('::App.ForumDashboard.Posts')}
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
{translate('::App.MenuField.Order')}: {category.displayOrder}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
className={`p-1 rounded-lg transition-colors ${
|
|
||||||
category.isActive
|
|
||||||
? 'text-green-600 dark:text-green-400 hover:bg-green-100 dark:hover:bg-green-900'
|
|
||||||
: 'text-red-600 dark:text-red-400 hover:bg-red-100 dark:hover:bg-red-900'
|
|
||||||
}`}
|
|
||||||
title={
|
|
||||||
category.isActive
|
|
||||||
? translate('::App.ForumCategoryManagement.HideCategory')
|
|
||||||
: translate('::App.ForumCategoryManagement.ShowCategory')
|
|
||||||
}
|
|
||||||
onClick={() => handleToggleActive(category)}
|
|
||||||
>
|
|
||||||
{category.isActive ? (
|
|
||||||
<FaEye className="w-4 h-4" />
|
|
||||||
) : (
|
|
||||||
<FaEyeSlash className="w-4 h-4" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
className={`p-1 rounded-lg transition-colors ${
|
|
||||||
category.isLocked
|
|
||||||
? 'text-yellow-600 dark:text-yellow-300 hover:bg-yellow-100 dark:hover:bg-yellow-900'
|
|
||||||
: 'text-green-600 dark:text-green-400 hover:bg-green-100 dark:hover:bg-green-900'
|
|
||||||
}`}
|
|
||||||
title={
|
|
||||||
category.isLocked
|
|
||||||
? translate('::App.ForumCategoryManagement.UnlockCategory')
|
|
||||||
: translate('::App.ForumCategoryManagement.LockCategory')
|
|
||||||
}
|
|
||||||
onClick={() => handleToggleLocked(category)}
|
|
||||||
>
|
|
||||||
{category.isLocked ? (
|
|
||||||
<FaLock className="w-4 h-4" />
|
|
||||||
) : (
|
|
||||||
<FaUnlock className="w-4 h-4" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
className="p-1 text-blue-600 dark:text-blue-400 hover:bg-blue-100 dark:hover:bg-blue-900 rounded-lg transition-colors"
|
|
||||||
title={translate('::App.ForumCategoryManagement.EditCategory')}
|
|
||||||
onClick={() => handleEdit(category)}
|
|
||||||
>
|
|
||||||
<FaEdit className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
className="p-1 text-red-600 dark:text-red-400 hover:bg-red-100 dark:hover:bg-red-900 rounded-lg transition-colors"
|
|
||||||
title={translate('::App.ForumCategoryManagement.DeleteCategory')}
|
|
||||||
onClick={() => confirmDeleteCategory(category)}
|
|
||||||
>
|
|
||||||
<FaTrash className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{categoryToDelete && (
|
|
||||||
<ConfirmDialog
|
|
||||||
isOpen={showConfirm}
|
|
||||||
type="danger"
|
|
||||||
title={translate('::App.Platform.DeleteConfirmation')}
|
|
||||||
confirmText={translate('::App.Platform.Delete')}
|
|
||||||
cancelText={translate('::App.Platform.Cancel')}
|
|
||||||
onCancel={() => {
|
|
||||||
setShowConfirm(false)
|
|
||||||
setCategoryToDelete(null)
|
|
||||||
}}
|
|
||||||
onConfirm={async () => {
|
|
||||||
try {
|
|
||||||
if (categoryToDelete) {
|
|
||||||
await onDeleteCategory(categoryToDelete.id)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error deleting category:', error)
|
|
||||||
} finally {
|
|
||||||
setShowConfirm(false)
|
|
||||||
setCategoryToDelete(null)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
></ConfirmDialog>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,161 +0,0 @@
|
||||||
import { useMemo } from 'react'
|
|
||||||
import Widget from '@/components/ui/Widget/Widget'
|
|
||||||
import { ForumCategory, ForumPost, ForumTopic } from '@/proxy/forum/forum'
|
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
|
||||||
import dayjs from 'dayjs'
|
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
|
||||||
import 'dayjs/locale/tr'
|
|
||||||
|
|
||||||
// Plugin kaydı modül seviyesinde yapılır; her render'da tekrarlanması gereksiz iştir.
|
|
||||||
dayjs.extend(relativeTime)
|
|
||||||
|
|
||||||
const MAX_ACTIVITIES = 5
|
|
||||||
|
|
||||||
interface AdminStatsProps {
|
|
||||||
categories: ForumCategory[]
|
|
||||||
topics: ForumTopic[]
|
|
||||||
posts: ForumPost[]
|
|
||||||
totalCounts: { categories: number; topics: number; posts: number }
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Activity {
|
|
||||||
id: string
|
|
||||||
message: string
|
|
||||||
color: string
|
|
||||||
date: Date
|
|
||||||
}
|
|
||||||
|
|
||||||
export function AdminStats({ categories, topics, posts, totalCounts }: AdminStatsProps) {
|
|
||||||
const { translate, cultureName } = useLocalization()
|
|
||||||
|
|
||||||
const activeCategories = categories.filter((c) => c.isActive).length
|
|
||||||
const solvedTopics = topics.filter((t) => t.isSolved).length
|
|
||||||
const acceptedAnswers = posts.filter((p) => p.isAcceptedAnswer).length
|
|
||||||
|
|
||||||
const stats = [
|
|
||||||
{
|
|
||||||
key: 'categories',
|
|
||||||
title: translate('::App.ForumDashboard.TotalCategories'),
|
|
||||||
value: totalCounts.categories,
|
|
||||||
subTitle: translate('::App.ForumDashboard.ActiveCategories', { 0: activeCategories }),
|
|
||||||
icon: 'FaFolder' as const,
|
|
||||||
color: 'blue' as const,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'topics',
|
|
||||||
title: translate('::App.ForumDashboard.TotalTopics'),
|
|
||||||
value: totalCounts.topics,
|
|
||||||
subTitle: translate('::App.ForumDashboard.SolvedTopics', { 0: solvedTopics }),
|
|
||||||
icon: 'FaCommentDots' as const,
|
|
||||||
color: 'green' as const,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'posts',
|
|
||||||
title: translate('::App.ForumDashboard.TotalPosts'),
|
|
||||||
value: totalCounts.posts,
|
|
||||||
subTitle: translate('::App.ForumDashboard.AcceptedAnswers', { 0: acceptedAnswers }),
|
|
||||||
icon: 'FaFileAlt' as const,
|
|
||||||
color: 'orange' as const,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'engagement',
|
|
||||||
title: translate('::App.ForumDashboard.EngagementRate'),
|
|
||||||
value:
|
|
||||||
totalCounts.topics > 0
|
|
||||||
? Math.round((totalCounts.posts / totalCounts.topics) * 100) / 100
|
|
||||||
: 0,
|
|
||||||
subTitle: translate('::App.ForumDashboard.PostsPerTopic'),
|
|
||||||
icon: 'FaChartLine' as const,
|
|
||||||
color: 'purple' as const,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const latestActivities = useMemo<Activity[]>(() => {
|
|
||||||
const categoryNameById = new Map(categories.map((c) => [c.id, c.name]))
|
|
||||||
const activities: Activity[] = []
|
|
||||||
|
|
||||||
topics.forEach((topic) => {
|
|
||||||
if (!topic.creationTime) return
|
|
||||||
activities.push({
|
|
||||||
id: `topic-${topic.id}`,
|
|
||||||
message: `${translate('::App.ForumTopicManagement.NewTopic')} — ${
|
|
||||||
categoryNameById.get(topic.categoryId) ?? translate('::App.Platform.Unknown')
|
|
||||||
}`,
|
|
||||||
color: 'bg-blue-500',
|
|
||||||
date: new Date(topic.creationTime),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
posts.forEach((post) => {
|
|
||||||
if (!post.isAcceptedAnswer || !post.creationTime) return
|
|
||||||
activities.push({
|
|
||||||
id: `post-${post.id}`,
|
|
||||||
message: translate('::App.ForumDashboard.Postmarked'),
|
|
||||||
color: 'bg-emerald-500',
|
|
||||||
date: new Date(post.creationTime),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
categories.forEach((category) => {
|
|
||||||
if (!category.creationTime) return
|
|
||||||
activities.push({
|
|
||||||
id: `category-${category.id}`,
|
|
||||||
message: `${translate('::App.ForumDashboard.NewCategoryCreated')} — ${category.name}`,
|
|
||||||
color: 'bg-orange-500',
|
|
||||||
date: new Date(category.creationTime),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
return activities
|
|
||||||
.sort((a, b) => b.date.getTime() - a.date.getTime())
|
|
||||||
.slice(0, MAX_ACTIVITIES)
|
|
||||||
}, [categories, posts, topics, translate])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-4">
|
|
||||||
{stats.map((stat) => (
|
|
||||||
<Widget
|
|
||||||
key={stat.key}
|
|
||||||
title={stat.title}
|
|
||||||
value={stat.value}
|
|
||||||
subTitle={stat.subTitle}
|
|
||||||
icon={stat.icon}
|
|
||||||
color={stat.color}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Recent Activity */}
|
|
||||||
<div className="rounded-xl border border-gray-200 bg-white p-6 shadow-sm dark:border-gray-700 dark:bg-gray-900">
|
|
||||||
<h3 className="mb-4 text-lg font-semibold text-gray-900 dark:text-white">
|
|
||||||
{translate('::App.ForumDashboard.RecentActivity')}
|
|
||||||
</h3>
|
|
||||||
<div className="space-y-4">
|
|
||||||
{latestActivities.length === 0 ? (
|
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
{translate('::App.Platform.NoDataFound')}
|
|
||||||
</p>
|
|
||||||
) : (
|
|
||||||
latestActivities.map((activity) => (
|
|
||||||
<div
|
|
||||||
key={activity.id}
|
|
||||||
className="flex items-start space-x-3 rounded-lg bg-gray-50 p-3 dark:bg-gray-800"
|
|
||||||
>
|
|
||||||
<div className={`mt-2 h-2 w-2 ${activity.color} rounded-full`} />
|
|
||||||
<div>
|
|
||||||
<p className="text-sm text-gray-900 dark:text-white">{activity.message}</p>
|
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400">
|
|
||||||
{dayjs(activity.date)
|
|
||||||
.locale(cultureName?.startsWith('tr') ? 'tr' : 'en')
|
|
||||||
.fromNow()}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,416 +0,0 @@
|
||||||
import { useState } from 'react'
|
|
||||||
import {
|
|
||||||
FaPlus,
|
|
||||||
FaEdit,
|
|
||||||
FaTrashAlt,
|
|
||||||
FaCheckCircle,
|
|
||||||
FaCircle,
|
|
||||||
FaHeart,
|
|
||||||
FaSpinner,
|
|
||||||
} from 'react-icons/fa'
|
|
||||||
import { ForumPost, ForumTopic } from '@/proxy/forum/forum'
|
|
||||||
import { CreatePostRequest } from '@/services/forum.service'
|
|
||||||
import { HtmlEditor, ImageUpload, Item, MediaResizing, Toolbar } from 'devextreme-react/html-editor'
|
|
||||||
import { useStoreState } from '@/store/store'
|
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
|
||||||
import { Formik, Form, Field, FieldProps } from 'formik'
|
|
||||||
import * as Yup from 'yup'
|
|
||||||
import { FormContainer, FormItem, Button, Dialog } from '@/components/ui'
|
|
||||||
import { ConfirmDialog } from '@/components/shared'
|
|
||||||
import { formatForumDate, sanitizeForumHtml, stripHtml } from '../forum/utils'
|
|
||||||
import {
|
|
||||||
fontFamilyOptions,
|
|
||||||
fontSizeOptions,
|
|
||||||
fontValues,
|
|
||||||
headerOptions,
|
|
||||||
headerValues,
|
|
||||||
sizeValues,
|
|
||||||
} from '@/proxy/reports/data'
|
|
||||||
|
|
||||||
const postValidationSchema = Yup.object().shape({
|
|
||||||
topicId: Yup.string().required(),
|
|
||||||
content: Yup.string().test('not-empty', 'Content is required', (value) => !!stripHtml(value)),
|
|
||||||
})
|
|
||||||
|
|
||||||
interface PostManagementProps {
|
|
||||||
posts: ForumPost[]
|
|
||||||
totalCount: number
|
|
||||||
topics: ForumTopic[]
|
|
||||||
loading: boolean
|
|
||||||
onCreatePost: (post: CreatePostRequest) => Promise<unknown>
|
|
||||||
onUpdatePost: (id: string, post: Partial<ForumPost>) => Promise<unknown>
|
|
||||||
onDeletePost: (id: string) => Promise<unknown>
|
|
||||||
onMarkPostAsAcceptedAnswer: (id: string) => Promise<unknown>
|
|
||||||
onUnmarkPostAsAcceptedAnswer: (id: string) => Promise<unknown>
|
|
||||||
}
|
|
||||||
|
|
||||||
export function PostManagement({
|
|
||||||
posts,
|
|
||||||
totalCount,
|
|
||||||
topics,
|
|
||||||
loading,
|
|
||||||
onCreatePost,
|
|
||||||
onUpdatePost,
|
|
||||||
onDeletePost,
|
|
||||||
onMarkPostAsAcceptedAnswer,
|
|
||||||
onUnmarkPostAsAcceptedAnswer,
|
|
||||||
}: PostManagementProps) {
|
|
||||||
const { translate } = useLocalization()
|
|
||||||
const { tenant } = useStoreState((state) => state.auth)
|
|
||||||
const [showCreateForm, setShowCreateForm] = useState(false)
|
|
||||||
const [editingPost, setEditingPost] = useState<ForumPost | null>(null)
|
|
||||||
const [showConfirm, setShowConfirm] = useState(false)
|
|
||||||
const [postToDelete, setPostToDelete] = useState<ForumPost | null>(null)
|
|
||||||
|
|
||||||
const confirmDeletePost = (post: ForumPost) => {
|
|
||||||
setPostToDelete(post)
|
|
||||||
setShowConfirm(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleEdit = (post: ForumPost) => {
|
|
||||||
setEditingPost(post)
|
|
||||||
setShowCreateForm(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleCloseForm = () => {
|
|
||||||
setShowCreateForm(false)
|
|
||||||
setEditingPost(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleToggleAcceptedAnswer = async (post: ForumPost) => {
|
|
||||||
try {
|
|
||||||
if (post.isAcceptedAnswer) {
|
|
||||||
await onUnmarkPostAsAcceptedAnswer(post.id)
|
|
||||||
} else {
|
|
||||||
await onMarkPostAsAcceptedAnswer(post.id)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error toggling accepted answer:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getTopicTitle = (topicId: string) =>
|
|
||||||
topics.find((t) => t.id === topicId)?.title ?? translate('::App.Platform.Unknown')
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-3">
|
|
||||||
{/* Create/Edit Modal */}
|
|
||||||
<Dialog
|
|
||||||
isOpen={showCreateForm}
|
|
||||||
width={900}
|
|
||||||
onClose={handleCloseForm}
|
|
||||||
onRequestClose={handleCloseForm}
|
|
||||||
>
|
|
||||||
<div className="pb-4 border-b border-gray-200 dark:border-gray-700">
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
|
|
||||||
{editingPost
|
|
||||||
? translate('::App.ForumPostManagement.EditPost')
|
|
||||||
: translate('::App.ForumPostManagement.AddPost')}
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="pt-4 max-h-[70vh] overflow-y-auto">
|
|
||||||
<Formik
|
|
||||||
enableReinitialize
|
|
||||||
initialValues={{
|
|
||||||
topicId: editingPost?.topicId ?? '',
|
|
||||||
content: editingPost?.content ?? '',
|
|
||||||
isAcceptedAnswer: editingPost?.isAcceptedAnswer ?? false,
|
|
||||||
tenantId: tenant?.tenantId ?? '',
|
|
||||||
}}
|
|
||||||
validationSchema={postValidationSchema}
|
|
||||||
onSubmit={async (values, { setSubmitting, resetForm }) => {
|
|
||||||
try {
|
|
||||||
if (editingPost) {
|
|
||||||
await onUpdatePost(editingPost.id, values)
|
|
||||||
} else {
|
|
||||||
// parentPostId gönderilmez: yönetim ekranından açılan post her zaman kök gönderidir.
|
|
||||||
await onCreatePost({
|
|
||||||
topicId: values.topicId,
|
|
||||||
content: values.content,
|
|
||||||
tenantId: values.tenantId || undefined,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
resetForm()
|
|
||||||
setShowCreateForm(false)
|
|
||||||
setEditingPost(null)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error submitting post:', error)
|
|
||||||
} finally {
|
|
||||||
setSubmitting(false)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{({ values, setFieldValue, errors, touched, isSubmitting }) => (
|
|
||||||
<Form>
|
|
||||||
<FormContainer className="space-y-4">
|
|
||||||
<FormItem
|
|
||||||
asterisk
|
|
||||||
label={translate('::App.ForumTopicManagement.Baslik')}
|
|
||||||
invalid={!!errors.topicId && touched.topicId}
|
|
||||||
errorMessage={errors.topicId}
|
|
||||||
>
|
|
||||||
<Field
|
|
||||||
as="select"
|
|
||||||
name="topicId"
|
|
||||||
className="w-full border border-gray-300 dark:border-gray-700 rounded-lg px-3 py-2 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100"
|
|
||||||
>
|
|
||||||
<option value="">{translate('::App.ForumPostManagement.SelectTopic')}</option>
|
|
||||||
{topics.map((topic) => (
|
|
||||||
<option key={topic.id} value={topic.id}>
|
|
||||||
{topic.title}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</Field>
|
|
||||||
</FormItem>
|
|
||||||
|
|
||||||
<FormItem
|
|
||||||
asterisk
|
|
||||||
label={translate('::App.Listform.ListformField.Content')}
|
|
||||||
invalid={!!errors.content && touched.content}
|
|
||||||
errorMessage={errors.content}
|
|
||||||
>
|
|
||||||
<Field name="content">
|
|
||||||
{({ field }: FieldProps) => (
|
|
||||||
<HtmlEditor
|
|
||||||
value={field.value}
|
|
||||||
height={400}
|
|
||||||
placeholder={translate('::App.ForumPostManagement.MessageNew')}
|
|
||||||
onValueChanged={(e) => setFieldValue('content', e.value)}
|
|
||||||
>
|
|
||||||
<MediaResizing enabled={true} />
|
|
||||||
<ImageUpload fileUploadMode="base64" />
|
|
||||||
<Toolbar multiline={true}>
|
|
||||||
<Item name="undo" />
|
|
||||||
<Item name="redo" />
|
|
||||||
<Item name="separator" />
|
|
||||||
<Item
|
|
||||||
name="size"
|
|
||||||
acceptedValues={sizeValues}
|
|
||||||
options={fontSizeOptions}
|
|
||||||
/>
|
|
||||||
<Item
|
|
||||||
name="font"
|
|
||||||
acceptedValues={fontValues}
|
|
||||||
options={fontFamilyOptions}
|
|
||||||
/>
|
|
||||||
<Item name="separator" />
|
|
||||||
<Item name="bold" />
|
|
||||||
<Item name="italic" />
|
|
||||||
<Item name="strike" />
|
|
||||||
<Item name="underline" />
|
|
||||||
<Item name="separator" />
|
|
||||||
<Item name="alignLeft" />
|
|
||||||
<Item name="alignCenter" />
|
|
||||||
<Item name="alignRight" />
|
|
||||||
<Item name="alignJustify" />
|
|
||||||
<Item name="separator" />
|
|
||||||
<Item name="orderedList" />
|
|
||||||
<Item name="bulletList" />
|
|
||||||
<Item name="separator" />
|
|
||||||
<Item
|
|
||||||
name="header"
|
|
||||||
acceptedValues={headerValues}
|
|
||||||
options={headerOptions}
|
|
||||||
/>
|
|
||||||
<Item name="separator" />
|
|
||||||
<Item name="color" />
|
|
||||||
<Item name="background" />
|
|
||||||
<Item name="separator" />
|
|
||||||
<Item name="link" />
|
|
||||||
<Item name="image" />
|
|
||||||
<Item name="separator" />
|
|
||||||
<Item name="clear" />
|
|
||||||
<Item name="codeBlock" />
|
|
||||||
<Item name="blockquote" />
|
|
||||||
<Item name="separator" />
|
|
||||||
<Item name="insertTable" />
|
|
||||||
<Item name="deleteTable" />
|
|
||||||
<Item name="insertRowAbove" />
|
|
||||||
<Item name="insertRowBelow" />
|
|
||||||
<Item name="deleteRow" />
|
|
||||||
<Item name="insertColumnLeft" />
|
|
||||||
<Item name="insertColumnRight" />
|
|
||||||
<Item name="deleteColumn" />
|
|
||||||
</Toolbar>
|
|
||||||
</HtmlEditor>
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
</FormItem>
|
|
||||||
|
|
||||||
<FormItem label={translate('::App.ForumPostManagement.AcceptedAnswer')}>
|
|
||||||
<label className="flex items-center">
|
|
||||||
<Field type="checkbox" name="isAcceptedAnswer" className="mr-2" />
|
|
||||||
{translate('::App.ForumPostManagement.MarkAsAcceptedAnswer')}
|
|
||||||
</label>
|
|
||||||
</FormItem>
|
|
||||||
|
|
||||||
<div className="flex justify-end space-x-3 pt-2">
|
|
||||||
<Button variant="plain" type="button" onClick={handleCloseForm}>
|
|
||||||
{translate('::App.Platform.Cancel')}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="solid"
|
|
||||||
type="submit"
|
|
||||||
loading={isSubmitting}
|
|
||||||
disabled={!values.topicId || !stripHtml(values.content)}
|
|
||||||
>
|
|
||||||
{editingPost
|
|
||||||
? translate('::App.Platform.Update')
|
|
||||||
: translate('::App.Platform.Create')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</FormContainer>
|
|
||||||
</Form>
|
|
||||||
)}
|
|
||||||
</Formik>
|
|
||||||
</div>
|
|
||||||
</Dialog>
|
|
||||||
|
|
||||||
{/* 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">
|
|
||||||
{translate('::App.ForumDashboard.Posts')} ({totalCount})
|
|
||||||
</h3>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="solid"
|
|
||||||
disabled={loading}
|
|
||||||
className="flex items-center space-x-2"
|
|
||||||
onClick={() => setShowCreateForm(true)}
|
|
||||||
>
|
|
||||||
<FaPlus className="w-4 h-4" />
|
|
||||||
<span>{translate('::App.ForumPostManagement.AddPost')}</span>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{loading ? (
|
|
||||||
<div className="p-8 text-center">
|
|
||||||
<FaSpinner className="w-8 h-8 animate-spin mx-auto mb-4 text-blue-600 dark:text-blue-400" />
|
|
||||||
<p className="text-gray-500 dark:text-gray-400">{translate('::App.ForumPostManagement.Loadingtopics')}</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="divide-y divide-gray-200 dark:divide-gray-800">
|
|
||||||
{posts.length === 0 && (
|
|
||||||
<p className="p-8 text-center text-gray-500 dark:text-gray-400">
|
|
||||||
{translate('::App.Platform.NoDataFound')}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{[...posts]
|
|
||||||
.sort(
|
|
||||||
(a, b) => new Date(b.creationTime).getTime() - new Date(a.creationTime).getTime(),
|
|
||||||
)
|
|
||||||
.map((post) => (
|
|
||||||
<div key={post.id} className="p-6 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors">
|
|
||||||
<div className="flex items-start justify-between">
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<div className="flex items-center space-x-2 mb-2">
|
|
||||||
<h4 className="text-sm font-semibold text-gray-900 dark:text-gray-100">{post.authorName}</h4>
|
|
||||||
{post.isAcceptedAnswer && (
|
|
||||||
<div className="flex items-center space-x-1 bg-emerald-100 dark:bg-emerald-900 text-emerald-700 dark:text-emerald-300 px-2 py-1 rounded-full text-xs">
|
|
||||||
<FaCheckCircle className="w-4 h-4" />
|
|
||||||
<span>{translate('::App.ForumPostManagement.AcceptedAnswer')}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mb-3">
|
|
||||||
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">
|
|
||||||
{translate('::App.ForumPostManagement.ReplyTo')}:{' '}
|
|
||||||
<span className="font-medium">{getTopicTitle(post.topicId)}</span>
|
|
||||||
</p>
|
|
||||||
<div
|
|
||||||
className="line-clamp-3 break-words text-gray-700 dark:text-gray-200"
|
|
||||||
dangerouslySetInnerHTML={{ __html: sanitizeForumHtml(post.content) }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-between text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
<div className="flex items-center space-x-4">
|
|
||||||
<span>{formatForumDate(post.creationTime)}</span>
|
|
||||||
<div className="flex items-center space-x-1">
|
|
||||||
<FaHeart className="w-4 h-4" />
|
|
||||||
<span>
|
|
||||||
{post.likeCount} {translate('::App.Listform.ListformField.Likes')}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center space-x-2 ml-4">
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
className={`p-1 rounded-lg transition-colors ${
|
|
||||||
post.isAcceptedAnswer
|
|
||||||
? 'text-emerald-600 dark:text-emerald-400 hover:bg-emerald-100 dark:hover:bg-emerald-900'
|
|
||||||
: 'text-gray-400 dark:text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800'
|
|
||||||
}`}
|
|
||||||
title={
|
|
||||||
post.isAcceptedAnswer
|
|
||||||
? translate('::App.ForumPostManagement.RemoveAcceptedAnswer')
|
|
||||||
: translate('::App.ForumPostManagement.MarkAsAcceptedAnswer')
|
|
||||||
}
|
|
||||||
onClick={() => handleToggleAcceptedAnswer(post)}
|
|
||||||
>
|
|
||||||
{post.isAcceptedAnswer ? (
|
|
||||||
<FaCheckCircle className="w-4 h-4" />
|
|
||||||
) : (
|
|
||||||
<FaCircle className="w-4 h-4" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
className="p-1 text-blue-600 dark:text-blue-400 hover:bg-blue-100 dark:hover:bg-blue-900 rounded-lg transition-colors"
|
|
||||||
title={translate('::App.ForumPostManagement.EditPost')}
|
|
||||||
onClick={() => handleEdit(post)}
|
|
||||||
>
|
|
||||||
<FaEdit className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
className="p-1 text-red-600 dark:text-red-400 hover:bg-red-100 dark:hover:bg-red-900 rounded-lg transition-colors"
|
|
||||||
title={translate('::App.ForumPostManagement.DeletePost')}
|
|
||||||
onClick={() => confirmDeletePost(post)}
|
|
||||||
>
|
|
||||||
<FaTrashAlt className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{postToDelete && (
|
|
||||||
<ConfirmDialog
|
|
||||||
isOpen={showConfirm}
|
|
||||||
type="danger"
|
|
||||||
title={translate('::App.Platform.DeleteConfirmation')}
|
|
||||||
confirmText={translate('::App.Platform.Delete')}
|
|
||||||
cancelText={translate('::App.Platform.Cancel')}
|
|
||||||
onCancel={() => {
|
|
||||||
setShowConfirm(false)
|
|
||||||
setPostToDelete(null)
|
|
||||||
}}
|
|
||||||
onConfirm={async () => {
|
|
||||||
try {
|
|
||||||
if (postToDelete) {
|
|
||||||
await onDeletePost(postToDelete.id)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error deleting post:', error)
|
|
||||||
} finally {
|
|
||||||
setShowConfirm(false)
|
|
||||||
setPostToDelete(null)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
></ConfirmDialog>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,484 +0,0 @@
|
||||||
import { useState } from 'react'
|
|
||||||
import {
|
|
||||||
FaPlus,
|
|
||||||
FaEdit,
|
|
||||||
FaTrashAlt,
|
|
||||||
FaLock,
|
|
||||||
FaUnlock,
|
|
||||||
FaThumbtack,
|
|
||||||
FaCheckCircle,
|
|
||||||
FaCircle,
|
|
||||||
FaEye,
|
|
||||||
FaSpinner,
|
|
||||||
} from 'react-icons/fa'
|
|
||||||
import { ForumCategory, ForumTopic } from '@/proxy/forum/forum'
|
|
||||||
import { CreateTopicRequest } from '@/services/forum.service'
|
|
||||||
import { useStoreState } from '@/store/store'
|
|
||||||
import { useLocalization } from '@/utils/hooks/useLocalization'
|
|
||||||
import { Formik, Form, Field, FieldProps } from 'formik'
|
|
||||||
import * as Yup from 'yup'
|
|
||||||
import { FormContainer, FormItem, Button, Select, Dialog } from '@/components/ui'
|
|
||||||
import { ConfirmDialog } from '@/components/shared'
|
|
||||||
import { formatForumDate } from '../forum/utils'
|
|
||||||
|
|
||||||
const FIELD_CLASS =
|
|
||||||
'w-full border border-gray-300 dark:border-gray-700 rounded-lg px-3 py-2 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500'
|
|
||||||
|
|
||||||
interface CategoryOption {
|
|
||||||
value: string
|
|
||||||
label: string
|
|
||||||
icon?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const topicValidationSchema = Yup.object().shape({
|
|
||||||
title: Yup.string().trim().required(),
|
|
||||||
content: Yup.string().trim().required(),
|
|
||||||
categoryId: Yup.string().required(),
|
|
||||||
})
|
|
||||||
|
|
||||||
interface TopicManagementProps {
|
|
||||||
topics: ForumTopic[]
|
|
||||||
totalCount: number
|
|
||||||
categories: ForumCategory[]
|
|
||||||
loading: boolean
|
|
||||||
onCreateTopic: (topic: CreateTopicRequest) => Promise<unknown>
|
|
||||||
onUpdateTopic: (id: string, topic: Partial<ForumTopic>) => Promise<unknown>
|
|
||||||
onDeleteTopic: (id: string) => Promise<unknown>
|
|
||||||
onPinTopic: (id: string) => Promise<unknown>
|
|
||||||
onUnpinTopic: (id: string) => Promise<unknown>
|
|
||||||
onLockTopic: (id: string) => Promise<unknown>
|
|
||||||
onUnlockTopic: (id: string) => Promise<unknown>
|
|
||||||
onMarkTopicAsSolved: (id: string) => Promise<unknown>
|
|
||||||
onMarkTopicAsUnsolved: (id: string) => Promise<unknown>
|
|
||||||
}
|
|
||||||
|
|
||||||
export function TopicManagement({
|
|
||||||
topics,
|
|
||||||
totalCount,
|
|
||||||
categories,
|
|
||||||
loading,
|
|
||||||
onCreateTopic,
|
|
||||||
onUpdateTopic,
|
|
||||||
onDeleteTopic,
|
|
||||||
onPinTopic,
|
|
||||||
onUnpinTopic,
|
|
||||||
onLockTopic,
|
|
||||||
onUnlockTopic,
|
|
||||||
onMarkTopicAsSolved,
|
|
||||||
onMarkTopicAsUnsolved,
|
|
||||||
}: TopicManagementProps) {
|
|
||||||
const { translate } = useLocalization()
|
|
||||||
const { tenant } = useStoreState((state) => state.auth)
|
|
||||||
const [showCreateForm, setShowCreateForm] = useState(false)
|
|
||||||
const [editingTopic, setEditingTopic] = useState<ForumTopic | null>(null)
|
|
||||||
const [showConfirm, setShowConfirm] = useState(false)
|
|
||||||
const [topicToDelete, setTopicToDelete] = useState<ForumTopic | null>(null)
|
|
||||||
|
|
||||||
const confirmDeleteTopic = (topic: ForumTopic) => {
|
|
||||||
setTopicToDelete(topic)
|
|
||||||
setShowConfirm(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleEdit = (topic: ForumTopic) => {
|
|
||||||
setEditingTopic(topic)
|
|
||||||
setShowCreateForm(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleCloseForm = () => {
|
|
||||||
setShowCreateForm(false)
|
|
||||||
setEditingTopic(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handlePin = async (topic: ForumTopic) => {
|
|
||||||
try {
|
|
||||||
if (topic.isPinned) {
|
|
||||||
await onUnpinTopic(topic.id)
|
|
||||||
} else {
|
|
||||||
await onPinTopic(topic.id)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error toggling pin:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleLock = async (topic: ForumTopic) => {
|
|
||||||
try {
|
|
||||||
if (topic.isLocked) {
|
|
||||||
await onUnlockTopic(topic.id)
|
|
||||||
} else {
|
|
||||||
await onLockTopic(topic.id)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error toggling lock:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSolved = async (topic: ForumTopic) => {
|
|
||||||
try {
|
|
||||||
if (topic.isSolved) {
|
|
||||||
await onMarkTopicAsUnsolved(topic.id)
|
|
||||||
} else {
|
|
||||||
await onMarkTopicAsSolved(topic.id)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error toggling solved status:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getCategoryName = (categoryId: string) =>
|
|
||||||
categories.find((c) => c.id === categoryId)?.name ?? translate('::App.Platform.Unknown')
|
|
||||||
|
|
||||||
const categoryOptions: CategoryOption[] = categories.map((category) => ({
|
|
||||||
value: category.id,
|
|
||||||
label: category.name,
|
|
||||||
icon: category.icon,
|
|
||||||
}))
|
|
||||||
|
|
||||||
const topicInitialValues = {
|
|
||||||
title: '',
|
|
||||||
content: '',
|
|
||||||
categoryId: '',
|
|
||||||
isPinned: false,
|
|
||||||
isLocked: false,
|
|
||||||
isSolved: false,
|
|
||||||
tenantId: tenant?.tenantId ?? '',
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="space-y-3">
|
|
||||||
{/* Create/Edit Modal */}
|
|
||||||
<Dialog
|
|
||||||
isOpen={showCreateForm}
|
|
||||||
width={720}
|
|
||||||
onClose={handleCloseForm}
|
|
||||||
onRequestClose={handleCloseForm}
|
|
||||||
>
|
|
||||||
<div className="pb-4 border-b border-gray-200 dark:border-gray-700">
|
|
||||||
<h3 className="text-lg font-semibold text-gray-900 dark:text-gray-100">
|
|
||||||
{editingTopic
|
|
||||||
? translate('::App.ForumTopicManagement.EditTopic')
|
|
||||||
: translate('::App.ForumTopicManagement.AddTopic')}
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="pt-4 max-h-[70vh] overflow-y-auto">
|
|
||||||
<Formik
|
|
||||||
enableReinitialize
|
|
||||||
initialValues={
|
|
||||||
editingTopic
|
|
||||||
? {
|
|
||||||
title: editingTopic.title,
|
|
||||||
content: editingTopic.content,
|
|
||||||
categoryId: editingTopic.categoryId,
|
|
||||||
isPinned: editingTopic.isPinned,
|
|
||||||
isLocked: editingTopic.isLocked,
|
|
||||||
isSolved: editingTopic.isSolved,
|
|
||||||
tenantId: tenant.tenantId || '',
|
|
||||||
}
|
|
||||||
: topicInitialValues
|
|
||||||
}
|
|
||||||
validationSchema={topicValidationSchema}
|
|
||||||
onSubmit={async (values, { setSubmitting, resetForm }) => {
|
|
||||||
try {
|
|
||||||
if (editingTopic) {
|
|
||||||
await onUpdateTopic(editingTopic.id, values)
|
|
||||||
} else {
|
|
||||||
await onCreateTopic(values)
|
|
||||||
}
|
|
||||||
resetForm()
|
|
||||||
setShowCreateForm(false)
|
|
||||||
setEditingTopic(null)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error submitting topic:', error)
|
|
||||||
} finally {
|
|
||||||
setSubmitting(false)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{({ values, isSubmitting, errors, touched }) => (
|
|
||||||
<Form>
|
|
||||||
<FormContainer className="space-y-4">
|
|
||||||
<FormItem
|
|
||||||
asterisk
|
|
||||||
label={translate('::App.Listform.ListformField.Title')}
|
|
||||||
invalid={!!errors.title && touched.title}
|
|
||||||
errorMessage={errors.title}
|
|
||||||
>
|
|
||||||
<Field
|
|
||||||
name="title"
|
|
||||||
className={FIELD_CLASS}
|
|
||||||
/>
|
|
||||||
</FormItem>
|
|
||||||
|
|
||||||
<FormItem
|
|
||||||
asterisk
|
|
||||||
label={translate('::App.Platform.Category')}
|
|
||||||
invalid={!!errors.categoryId && !!touched.categoryId}
|
|
||||||
errorMessage={errors.categoryId}
|
|
||||||
>
|
|
||||||
<Field name="categoryId">
|
|
||||||
{({ field, form }: FieldProps<string>) => (
|
|
||||||
<Select<CategoryOption>
|
|
||||||
field={field}
|
|
||||||
form={form}
|
|
||||||
options={categoryOptions}
|
|
||||||
placeholder={translate('::App.ForumTopicManagement.SelectCategory')}
|
|
||||||
value={categoryOptions.filter(
|
|
||||||
(option) => option.value === values.categoryId,
|
|
||||||
)}
|
|
||||||
formatOptionLabel={(option) => (
|
|
||||||
<span className="flex items-center gap-2">
|
|
||||||
<span className="text-lg">{option.icon}</span>
|
|
||||||
<span>{option.label}</span>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
onChange={(option) =>
|
|
||||||
form.setFieldValue('categoryId', option?.value ?? '')
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
</FormItem>
|
|
||||||
|
|
||||||
<FormItem
|
|
||||||
asterisk
|
|
||||||
label={translate('::App.Listform.ListformField.Content')}
|
|
||||||
invalid={!!errors.content && touched.content}
|
|
||||||
errorMessage={errors.content}
|
|
||||||
>
|
|
||||||
<Field
|
|
||||||
as="textarea"
|
|
||||||
name="content"
|
|
||||||
rows={6}
|
|
||||||
className={FIELD_CLASS}
|
|
||||||
/>
|
|
||||||
</FormItem>
|
|
||||||
|
|
||||||
<FormItem label={translate('::App.Listform.ListformField.Options')}>
|
|
||||||
<div className="flex items-center space-x-6 pt-1">
|
|
||||||
<label className="flex items-center">
|
|
||||||
<Field type="checkbox" name="isPinned" className="mr-2" />
|
|
||||||
{translate('::App.ForumTopicManagement.Pinned')}
|
|
||||||
</label>
|
|
||||||
<label className="flex items-center">
|
|
||||||
<Field type="checkbox" name="isLocked" className="mr-2" />
|
|
||||||
{translate('::App.Platform.Locked')}
|
|
||||||
</label>
|
|
||||||
<label className="flex items-center">
|
|
||||||
<Field type="checkbox" name="isSolved" className="mr-2" />
|
|
||||||
{translate('::App.ForumTopicManagement.Solved')}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</FormItem>
|
|
||||||
|
|
||||||
<div className="flex justify-end space-x-3 pt-2">
|
|
||||||
<Button variant="plain" type="button" onClick={handleCloseForm}>
|
|
||||||
{translate('::App.Platform.Cancel')}
|
|
||||||
</Button>
|
|
||||||
<Button variant="solid" type="submit" loading={isSubmitting}>
|
|
||||||
{editingTopic ? translate('::App.Platform.Update') : translate('::App.Platform.Create')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</FormContainer>
|
|
||||||
</Form>
|
|
||||||
)}
|
|
||||||
</Formik>
|
|
||||||
</div>
|
|
||||||
</Dialog>
|
|
||||||
|
|
||||||
{/* Topics 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">
|
|
||||||
{translate('::App.ForumDashboard.Topics')} ({totalCount})
|
|
||||||
</h3>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="solid"
|
|
||||||
disabled={loading}
|
|
||||||
className="flex items-center space-x-2"
|
|
||||||
onClick={() => setShowCreateForm(true)}
|
|
||||||
>
|
|
||||||
<FaPlus className="w-4 h-4" />
|
|
||||||
<span>{translate('::App.ForumTopicManagement.AddTopic')}</span>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{loading ? (
|
|
||||||
<div className="p-8 text-center">
|
|
||||||
<FaSpinner className="w-8 h-8 animate-spin mx-auto mb-4 text-blue-600 dark:text-blue-400" />
|
|
||||||
<p className="text-gray-500 dark:text-gray-400">
|
|
||||||
{translate('::App.ForumTopicManagement.Loadingtopics')}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="divide-y divide-gray-200 dark:divide-gray-800">
|
|
||||||
{topics.length === 0 && (
|
|
||||||
<p className="p-8 text-center text-gray-500 dark:text-gray-400">
|
|
||||||
{translate('::App.Platform.NoDataFound')}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{[...topics]
|
|
||||||
.sort(
|
|
||||||
(a, b) => new Date(b.creationTime).getTime() - new Date(a.creationTime).getTime(),
|
|
||||||
)
|
|
||||||
.map((topic) => (
|
|
||||||
<div
|
|
||||||
key={topic.id}
|
|
||||||
className="p-6 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
|
|
||||||
>
|
|
||||||
<div className="flex items-start justify-between">
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<div className="flex items-center space-x-2 mb-2">
|
|
||||||
{topic.isPinned && (
|
|
||||||
<FaThumbtack className="w-4 h-4 text-orange-500 dark:text-orange-400" />
|
|
||||||
)}
|
|
||||||
{topic.isLocked && (
|
|
||||||
<FaLock className="w-4 h-4 text-gray-400 dark:text-gray-500" />
|
|
||||||
)}
|
|
||||||
{topic.isSolved && (
|
|
||||||
<FaCheckCircle className="w-4 h-4 text-emerald-500 dark:text-emerald-400" />
|
|
||||||
)}
|
|
||||||
<h4 className="text-lg font-semibold text-gray-900 dark:text-gray-100 line-clamp-1">
|
|
||||||
{topic.title}
|
|
||||||
</h4>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p className="text-gray-600 dark:text-gray-300 mb-3 line-clamp-2">
|
|
||||||
{topic.content}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-between text-sm text-gray-500 dark:text-gray-400">
|
|
||||||
<div className="flex items-center space-x-4">
|
|
||||||
<span className="font-medium">{getCategoryName(topic.categoryId)}</span>
|
|
||||||
<span>{topic.authorName}</span>
|
|
||||||
<span>{formatForumDate(topic.creationTime)}</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center space-x-4">
|
|
||||||
<div className="flex items-center space-x-1">
|
|
||||||
<FaEye className="w-4 h-4" />
|
|
||||||
<span>{topic.viewCount}</span>
|
|
||||||
</div>
|
|
||||||
<span>
|
|
||||||
{topic.replyCount} {translate('::App.Platform.Replies')}
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
{topic.likeCount} {translate('::App.Listform.ListformField.Likes')}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center space-x-2 ml-4">
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
className={`p-1 rounded-lg transition-colors ${
|
|
||||||
topic.isPinned
|
|
||||||
? 'text-orange-600 dark:text-orange-400 hover:bg-orange-100 dark:hover:bg-orange-900'
|
|
||||||
: 'text-gray-400 dark:text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800'
|
|
||||||
}`}
|
|
||||||
title={
|
|
||||||
topic.isPinned
|
|
||||||
? translate('::App.ForumTopicManagement.UnpinTopic')
|
|
||||||
: translate('::App.ForumTopicManagement.PinTopic')
|
|
||||||
}
|
|
||||||
onClick={() => handlePin(topic)}
|
|
||||||
>
|
|
||||||
<FaThumbtack className="h-4 w-4" />
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
className={`p-1 rounded-lg transition-colors ${
|
|
||||||
topic.isLocked
|
|
||||||
? 'text-yellow-600 dark:text-yellow-300 hover:bg-yellow-100 dark:hover:bg-yellow-900'
|
|
||||||
: 'text-green-600 dark:text-green-400 hover:bg-green-100 dark:hover:bg-green-900'
|
|
||||||
}`}
|
|
||||||
title={
|
|
||||||
topic.isLocked
|
|
||||||
? translate('::App.ForumTopicManagement.UnlockTopic')
|
|
||||||
: translate('::App.ForumTopicManagement.LockTopic')
|
|
||||||
}
|
|
||||||
onClick={() => handleLock(topic)}
|
|
||||||
>
|
|
||||||
{topic.isLocked ? (
|
|
||||||
<FaLock className="w-4 h-4" />
|
|
||||||
) : (
|
|
||||||
<FaUnlock className="w-4 h-4" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
className={`p-1 rounded-lg transition-colors ${
|
|
||||||
topic.isSolved
|
|
||||||
? 'text-emerald-600 dark:text-emerald-400 hover:bg-emerald-100 dark:hover:bg-emerald-900'
|
|
||||||
: 'text-gray-400 dark:text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800'
|
|
||||||
}`}
|
|
||||||
title={
|
|
||||||
topic.isSolved
|
|
||||||
? translate('::App.ForumTopicManagement.MarkAsUnsolved')
|
|
||||||
: translate('::App.ForumTopicManagement.MarkAsSolved')
|
|
||||||
}
|
|
||||||
onClick={() => handleSolved(topic)}
|
|
||||||
>
|
|
||||||
{topic.isSolved ? (
|
|
||||||
<FaCheckCircle className="w-4 h-4" />
|
|
||||||
) : (
|
|
||||||
<FaCircle className="w-4 h-4" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
className="p-1 text-blue-600 dark:text-blue-400 hover:bg-blue-100 dark:hover:bg-blue-900 rounded-lg transition-colors"
|
|
||||||
title={translate('::App.ForumTopicManagement.EditTopic')}
|
|
||||||
onClick={() => handleEdit(topic)}
|
|
||||||
>
|
|
||||||
<FaEdit className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
className="p-1 text-red-600 dark:text-red-400 hover:bg-red-100 dark:hover:bg-red-900 rounded-lg transition-colors"
|
|
||||||
title={translate('::App.ForumTopicManagement.DeleteTopic')}
|
|
||||||
onClick={() => confirmDeleteTopic(topic)}
|
|
||||||
>
|
|
||||||
<FaTrashAlt className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{topicToDelete && (
|
|
||||||
<ConfirmDialog
|
|
||||||
isOpen={showConfirm}
|
|
||||||
type="danger"
|
|
||||||
title={translate('::App.Platform.DeleteConfirmation')}
|
|
||||||
confirmText={translate('::App.Platform.Delete')}
|
|
||||||
cancelText={translate('::App.Platform.Cancel')}
|
|
||||||
onCancel={() => {
|
|
||||||
setShowConfirm(false)
|
|
||||||
setTopicToDelete(null)
|
|
||||||
}}
|
|
||||||
onConfirm={async () => {
|
|
||||||
try {
|
|
||||||
if (topicToDelete) {
|
|
||||||
await onDeleteTopic(topicToDelete.id)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error deleting topic:', error)
|
|
||||||
} finally {
|
|
||||||
setShowConfirm(false)
|
|
||||||
setTopicToDelete(null)
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
></ConfirmDialog>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
export interface CategoryIconOption {
|
|
||||||
/** Kategoride saklanan emoji değeri. */
|
|
||||||
value: string
|
|
||||||
/** Aramada eşleşen etiket. */
|
|
||||||
label: string
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Forum kategorileri için hazır emoji seti. */
|
|
||||||
export const CATEGORY_ICON_OPTIONS: CategoryIconOption[] = [
|
|
||||||
{ value: '💬', label: 'Discussion' },
|
|
||||||
{ value: '📢', label: 'Announcement' },
|
|
||||||
{ value: '❓', label: 'Question' },
|
|
||||||
{ value: '💡', label: 'Idea' },
|
|
||||||
{ value: '🛠️', label: 'Support' },
|
|
||||||
{ value: '🐞', label: 'Bug' },
|
|
||||||
{ value: '🚀', label: 'Release' },
|
|
||||||
{ value: '📚', label: 'Documentation' },
|
|
||||||
{ value: '📝', label: 'Notes' },
|
|
||||||
{ value: '📌', label: 'Pinned' },
|
|
||||||
{ value: '⭐', label: 'Featured' },
|
|
||||||
{ value: '🔥', label: 'Popular' },
|
|
||||||
{ value: '🎯', label: 'Goals' },
|
|
||||||
{ value: '📊', label: 'Reports' },
|
|
||||||
{ value: '📈', label: 'Analytics' },
|
|
||||||
{ value: '⚙️', label: 'Settings' },
|
|
||||||
{ value: '🔒', label: 'Private' },
|
|
||||||
{ value: '🧩', label: 'Integration' },
|
|
||||||
{ value: '🖥️', label: 'Development' },
|
|
||||||
{ value: '📱', label: 'Mobile' },
|
|
||||||
{ value: '🗄️', label: 'Database' },
|
|
||||||
{ value: '🔐', label: 'Security' },
|
|
||||||
{ value: '👥', label: 'Community' },
|
|
||||||
{ value: '🏢', label: 'Company' },
|
|
||||||
{ value: '🧾', label: 'Finance' },
|
|
||||||
{ value: '🛒', label: 'Sales' },
|
|
||||||
{ value: '🚚', label: 'Logistics' },
|
|
||||||
{ value: '🏭', label: 'Production' },
|
|
||||||
{ value: '🎓', label: 'Training' },
|
|
||||||
{ value: '🎉', label: 'Events' },
|
|
||||||
{ value: '☕', label: 'Off topic' },
|
|
||||||
{ value: '🗳️', label: 'Poll' },
|
|
||||||
]
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Kayıtlı ikon hazır sette yoksa kaybolmasın diye listenin başına eklenir.
|
|
||||||
*/
|
|
||||||
export function resolveCategoryIconOptions(current?: string): CategoryIconOption[] {
|
|
||||||
if (!current || CATEGORY_ICON_OPTIONS.some((option) => option.value === current)) {
|
|
||||||
return CATEGORY_ICON_OPTIONS
|
|
||||||
}
|
|
||||||
return [{ value: current, label: current }, ...CATEGORY_ICON_OPTIONS]
|
|
||||||
}
|
|
||||||
|
|
@ -1,23 +1,11 @@
|
||||||
import { ForumCategory, ForumPost, ForumTopic } from '@/proxy/forum/forum'
|
import { ForumCategory, ForumPost, ForumTopic } from '@/proxy/forum/forum'
|
||||||
import {
|
import { CreatePostRequest, CreateTopicRequest, forumService } from '@/services/forum.service'
|
||||||
CreateCategoryRequest,
|
|
||||||
CreatePostRequest,
|
|
||||||
CreateTopicRequest,
|
|
||||||
forumService,
|
|
||||||
} from '@/services/forum.service'
|
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
|
|
||||||
type TotalCounts = { categories: number; topics: number; posts: number }
|
|
||||||
|
|
||||||
export function useForumData() {
|
export function useForumData() {
|
||||||
const [categories, setCategories] = useState<ForumCategory[]>([])
|
const [categories, setCategories] = useState<ForumCategory[]>([])
|
||||||
const [topics, setTopics] = useState<ForumTopic[]>([])
|
const [topics, setTopics] = useState<ForumTopic[]>([])
|
||||||
const [posts, setPosts] = useState<ForumPost[]>([])
|
const [posts, setPosts] = useState<ForumPost[]>([])
|
||||||
const [totalCounts, setTotalCounts] = useState<TotalCounts>({
|
|
||||||
categories: 0,
|
|
||||||
topics: 0,
|
|
||||||
posts: 0,
|
|
||||||
})
|
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
||||||
|
|
@ -56,7 +44,6 @@ export function useForumData() {
|
||||||
const response = await forumService.getCategories(params)
|
const response = await forumService.getCategories(params)
|
||||||
if (!mounted.current) return
|
if (!mounted.current) return
|
||||||
setCategories(response.items)
|
setCategories(response.items)
|
||||||
setTotalCounts((prev) => ({ ...prev, categories: response.totalCount }))
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
fail('Failed to load categories', err)
|
fail('Failed to load categories', err)
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -73,7 +60,6 @@ export function useForumData() {
|
||||||
const response = await forumService.getTopics({ categoryId })
|
const response = await forumService.getTopics({ categoryId })
|
||||||
if (!mounted.current) return
|
if (!mounted.current) return
|
||||||
setTopics(response.items)
|
setTopics(response.items)
|
||||||
if (!categoryId) setTotalCounts((prev) => ({ ...prev, topics: response.totalCount }))
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
fail('Failed to load topics', err)
|
fail('Failed to load topics', err)
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -90,7 +76,6 @@ export function useForumData() {
|
||||||
const response = await forumService.getPosts({ topicId })
|
const response = await forumService.getPosts({ topicId })
|
||||||
if (!mounted.current) return
|
if (!mounted.current) return
|
||||||
setPosts(response.items)
|
setPosts(response.items)
|
||||||
if (!topicId) setTotalCounts((prev) => ({ ...prev, posts: response.totalCount }))
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
fail('Failed to load posts', err)
|
fail('Failed to load posts', err)
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -111,87 +96,11 @@ export function useForumData() {
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// ---- Category operations -------------------------------------------------
|
|
||||||
|
|
||||||
const createCategory = useCallback(
|
|
||||||
async (categoryData: CreateCategoryRequest) => {
|
|
||||||
try {
|
|
||||||
const newCategory = await forumService.createCategory(categoryData)
|
|
||||||
setCategories((prev) => [...prev, newCategory])
|
|
||||||
setTotalCounts((prev) => ({ ...prev, categories: prev.categories + 1 }))
|
|
||||||
return newCategory
|
|
||||||
} catch (err) {
|
|
||||||
fail('Failed to create category', err)
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[fail],
|
|
||||||
)
|
|
||||||
|
|
||||||
const updateCategory = useCallback(
|
|
||||||
async (id: string, updates: Partial<ForumCategory>) => {
|
|
||||||
try {
|
|
||||||
const updatedCategory = await forumService.updateCategory(id, updates)
|
|
||||||
setCategories((prev) => prev.map((cat) => (cat.id === id ? updatedCategory : cat)))
|
|
||||||
return updatedCategory
|
|
||||||
} catch (err) {
|
|
||||||
fail('Failed to update category', err)
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[fail],
|
|
||||||
)
|
|
||||||
|
|
||||||
const updateCategoryLockState = useCallback(
|
|
||||||
async (id: string) => {
|
|
||||||
try {
|
|
||||||
const updated = await forumService.updateCategoryLockState(id)
|
|
||||||
setCategories((prev) => prev.map((cat) => (cat.id === id ? updated : cat)))
|
|
||||||
return updated
|
|
||||||
} catch (err) {
|
|
||||||
fail('Failed to update category lock state', err)
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[fail],
|
|
||||||
)
|
|
||||||
|
|
||||||
const updateCategoryActiveState = useCallback(
|
|
||||||
async (id: string) => {
|
|
||||||
try {
|
|
||||||
const updated = await forumService.updateCategoryActiveState(id)
|
|
||||||
setCategories((prev) => prev.map((cat) => (cat.id === id ? updated : cat)))
|
|
||||||
return updated
|
|
||||||
} catch (err) {
|
|
||||||
fail('Failed to update category active state', err)
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[fail],
|
|
||||||
)
|
|
||||||
|
|
||||||
const deleteCategory = useCallback(
|
|
||||||
async (id: string) => {
|
|
||||||
try {
|
|
||||||
await forumService.deleteCategory(id)
|
|
||||||
// Kategori silindiğinde bağlı topic/post sayıları da değişir; tek kaynak sunucu.
|
|
||||||
await reloadAll()
|
|
||||||
} catch (err) {
|
|
||||||
fail('Failed to delete category', err)
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[fail, reloadAll],
|
|
||||||
)
|
|
||||||
|
|
||||||
// ---- Topic operations ----------------------------------------------------
|
|
||||||
|
|
||||||
const createTopic = useCallback(
|
const createTopic = useCallback(
|
||||||
async (topicData: CreateTopicRequest) => {
|
async (topicData: CreateTopicRequest) => {
|
||||||
try {
|
try {
|
||||||
const newTopic = await forumService.createTopic(topicData)
|
const newTopic = await forumService.createTopic(topicData)
|
||||||
setTopics((prev) => [...prev, newTopic])
|
setTopics((prev) => [...prev, newTopic])
|
||||||
setTotalCounts((prev) => ({ ...prev, topics: prev.topics + 1 }))
|
|
||||||
setCategories((prev) =>
|
setCategories((prev) =>
|
||||||
prev.map((cat) =>
|
prev.map((cat) =>
|
||||||
cat.id === topicData.categoryId ? { ...cat, topicCount: cat.topicCount + 1 } : cat,
|
cat.id === topicData.categoryId ? { ...cat, topicCount: cat.topicCount + 1 } : cat,
|
||||||
|
|
@ -206,89 +115,11 @@ export function useForumData() {
|
||||||
[fail],
|
[fail],
|
||||||
)
|
)
|
||||||
|
|
||||||
const updateTopic = useCallback(
|
|
||||||
async (id: string, updates: Partial<ForumTopic>) => {
|
|
||||||
try {
|
|
||||||
const updatedTopic = await forumService.updateTopic(id, updates)
|
|
||||||
setTopics((prev) => prev.map((topic) => (topic.id === id ? updatedTopic : topic)))
|
|
||||||
return updatedTopic
|
|
||||||
} catch (err) {
|
|
||||||
fail('Failed to update topic', err)
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[fail],
|
|
||||||
)
|
|
||||||
|
|
||||||
const deleteTopic = useCallback(
|
|
||||||
async (id: string) => {
|
|
||||||
try {
|
|
||||||
await forumService.deleteTopic(id)
|
|
||||||
await reloadAll()
|
|
||||||
} catch (err) {
|
|
||||||
fail('Failed to delete topic', err)
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[fail, reloadAll],
|
|
||||||
)
|
|
||||||
|
|
||||||
const applyTopicUpdate = useCallback((updated: ForumTopic) => {
|
|
||||||
setTopics((prev) => prev.map((topic) => (topic.id === updated.id ? updated : topic)))
|
|
||||||
return updated
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const topicAction = useCallback(
|
|
||||||
async (
|
|
||||||
id: string,
|
|
||||||
action: (id: string) => Promise<ForumTopic>,
|
|
||||||
errorMessage: string,
|
|
||||||
): Promise<ForumTopic> => {
|
|
||||||
try {
|
|
||||||
return applyTopicUpdate(await action(id))
|
|
||||||
} catch (err) {
|
|
||||||
fail(errorMessage, err)
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[applyTopicUpdate, fail],
|
|
||||||
)
|
|
||||||
|
|
||||||
const pinTopic = useCallback(
|
|
||||||
(id: string) => topicAction(id, (x) => forumService.pinTopic(x), 'Failed to pin topic'),
|
|
||||||
[topicAction],
|
|
||||||
)
|
|
||||||
const unpinTopic = useCallback(
|
|
||||||
(id: string) => topicAction(id, (x) => forumService.unpinTopic(x), 'Failed to unpin topic'),
|
|
||||||
[topicAction],
|
|
||||||
)
|
|
||||||
const lockTopic = useCallback(
|
|
||||||
(id: string) => topicAction(id, (x) => forumService.lockTopic(x), 'Failed to lock topic'),
|
|
||||||
[topicAction],
|
|
||||||
)
|
|
||||||
const unlockTopic = useCallback(
|
|
||||||
(id: string) => topicAction(id, (x) => forumService.unlockTopic(x), 'Failed to unlock topic'),
|
|
||||||
[topicAction],
|
|
||||||
)
|
|
||||||
const solvedTopic = useCallback(
|
|
||||||
(id: string) =>
|
|
||||||
topicAction(id, (x) => forumService.solvedTopic(x), 'Failed to mark topic as solved'),
|
|
||||||
[topicAction],
|
|
||||||
)
|
|
||||||
const unsolvedTopic = useCallback(
|
|
||||||
(id: string) =>
|
|
||||||
topicAction(id, (x) => forumService.unsolvedTopic(x), 'Failed to mark topic as unsolved'),
|
|
||||||
[topicAction],
|
|
||||||
)
|
|
||||||
|
|
||||||
// ---- Post operations -----------------------------------------------------
|
|
||||||
|
|
||||||
const createPost = useCallback(
|
const createPost = useCallback(
|
||||||
async (postData: CreatePostRequest) => {
|
async (postData: CreatePostRequest) => {
|
||||||
try {
|
try {
|
||||||
const newPost = await forumService.createPost(postData)
|
const newPost = await forumService.createPost(postData)
|
||||||
setPosts((prev) => [...prev, newPost])
|
setPosts((prev) => [...prev, newPost])
|
||||||
setTotalCounts((prev) => ({ ...prev, posts: prev.posts + 1 }))
|
|
||||||
|
|
||||||
setTopics((prev) =>
|
setTopics((prev) =>
|
||||||
prev.map((t) => (t.id === postData.topicId ? { ...t, replyCount: t.replyCount + 1 } : t)),
|
prev.map((t) => (t.id === postData.topicId ? { ...t, replyCount: t.replyCount + 1 } : t)),
|
||||||
|
|
@ -312,33 +143,6 @@ export function useForumData() {
|
||||||
[fail, topics],
|
[fail, topics],
|
||||||
)
|
)
|
||||||
|
|
||||||
const updatePost = useCallback(
|
|
||||||
async (id: string, updates: Partial<ForumPost>) => {
|
|
||||||
try {
|
|
||||||
const updatedPost = await forumService.updatePost(id, updates)
|
|
||||||
setPosts((prev) => prev.map((post) => (post.id === id ? updatedPost : post)))
|
|
||||||
return updatedPost
|
|
||||||
} catch (err) {
|
|
||||||
fail('Failed to update post', err)
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[fail],
|
|
||||||
)
|
|
||||||
|
|
||||||
const deletePost = useCallback(
|
|
||||||
async (id: string) => {
|
|
||||||
try {
|
|
||||||
await forumService.deletePost(id)
|
|
||||||
await reloadAll()
|
|
||||||
} catch (err) {
|
|
||||||
fail('Failed to delete post', err)
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[fail, reloadAll],
|
|
||||||
)
|
|
||||||
|
|
||||||
const postAction = useCallback(
|
const postAction = useCallback(
|
||||||
async (
|
async (
|
||||||
id: string,
|
id: string,
|
||||||
|
|
@ -365,24 +169,6 @@ export function useForumData() {
|
||||||
(id: string) => postAction(id, (x) => forumService.unlikePost(x), 'Failed to unlike post'),
|
(id: string) => postAction(id, (x) => forumService.unlikePost(x), 'Failed to unlike post'),
|
||||||
[postAction],
|
[postAction],
|
||||||
)
|
)
|
||||||
const markPostAsAcceptedAnswer = useCallback(
|
|
||||||
(id: string) =>
|
|
||||||
postAction(
|
|
||||||
id,
|
|
||||||
(x) => forumService.markPostAsAcceptedAnswer(x),
|
|
||||||
'Failed to mark post as accepted answer',
|
|
||||||
),
|
|
||||||
[postAction],
|
|
||||||
)
|
|
||||||
const unmarkPostAsAcceptedAnswer = useCallback(
|
|
||||||
(id: string) =>
|
|
||||||
postAction(
|
|
||||||
id,
|
|
||||||
(x) => forumService.unmarkPostAsAcceptedAnswer(x),
|
|
||||||
'Failed to unmark post as accepted answer',
|
|
||||||
),
|
|
||||||
[postAction],
|
|
||||||
)
|
|
||||||
|
|
||||||
const clearError = useCallback(() => setError(null), [])
|
const clearError = useCallback(() => setError(null), [])
|
||||||
|
|
||||||
|
|
@ -391,7 +177,6 @@ export function useForumData() {
|
||||||
categories,
|
categories,
|
||||||
topics,
|
topics,
|
||||||
posts,
|
posts,
|
||||||
totalCounts,
|
|
||||||
loading,
|
loading,
|
||||||
error,
|
error,
|
||||||
|
|
||||||
|
|
@ -401,32 +186,13 @@ export function useForumData() {
|
||||||
loadPosts,
|
loadPosts,
|
||||||
reloadAll,
|
reloadAll,
|
||||||
|
|
||||||
// Category operations
|
|
||||||
createCategory,
|
|
||||||
updateCategory,
|
|
||||||
updateCategoryLockState,
|
|
||||||
updateCategoryActiveState,
|
|
||||||
deleteCategory,
|
|
||||||
|
|
||||||
// Topic operations
|
// Topic operations
|
||||||
createTopic,
|
createTopic,
|
||||||
updateTopic,
|
|
||||||
deleteTopic,
|
|
||||||
pinTopic,
|
|
||||||
unpinTopic,
|
|
||||||
lockTopic,
|
|
||||||
unlockTopic,
|
|
||||||
solvedTopic,
|
|
||||||
unsolvedTopic,
|
|
||||||
|
|
||||||
// Post operations
|
// Post operations
|
||||||
createPost,
|
createPost,
|
||||||
updatePost,
|
|
||||||
deletePost,
|
|
||||||
likePost,
|
likePost,
|
||||||
unlikePost,
|
unlikePost,
|
||||||
markPostAsAcceptedAnswer,
|
|
||||||
unmarkPostAsAcceptedAnswer,
|
|
||||||
|
|
||||||
// Utility
|
// Utility
|
||||||
clearError,
|
clearError,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue