diff --git a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/ListFormWizardDto.cs b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/ListFormWizardDto.cs index 9c23b272..04558a4f 100644 --- a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/ListFormWizardDto.cs +++ b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/ListFormWizardDto.cs @@ -6,11 +6,28 @@ namespace Sozsoft.Platform.ListForms; public class ListFormWizardDto { + /// + /// Wizard'in yol ayrimi. Varsayilan oldugu icin + /// bu alani icermeyen eski seed dosyalari mevcut ListForm davranisiyla calismaya devam eder. + /// Seed dosyasinda ilk alan olarak yer alir; dosyaya bakan kisi once yolu gorur. + /// + public WizardComponentKindEnum ComponentKind { get; set; } = WizardComponentKindEnum.List; + public string WizardName { get; set; } public string ListFormCode { get; set; } public string MenuCode { get; set; } public int MenuOrder { get; set; } + /// Custom yolunda menuye baglanacak CustomComponent'in adi. + public string CustomComponentName { get; set; } + + /// + /// Menu kaydina yazilan URL; Custom yolunda ayni zamanda rotanin tek kaynagidir. + /// List yolunda /admin/list/{menuCode} olarak hesaplanir, Custom yolunda + /// secilen CustomComponent'in RoutePath degeri buraya yazilir. + /// + public string MenuUrl { get; set; } + public bool IsTenant { get; set; } public bool IsBranch { get; set; } public bool IsOrganizationUnit { get; set; } diff --git a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/WizardSeedFileDto.cs b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/WizardSeedFileDto.cs index f4aa2f2c..671c6c7c 100644 --- a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/WizardSeedFileDto.cs +++ b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/WizardSeedFileDto.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System.Collections.Generic; +using Sozsoft.Platform.Enums; namespace Sozsoft.Platform.ListForms; @@ -56,4 +57,13 @@ public class WizardFileInfoDto public string MenuIcon { get; set; } public string CreatedAt { get; set; } public bool HasInsertedRecords { get; set; } + + /// Wizard'in List mi Custom Component mi urettigi; listede rozet olarak gosterilir. + public WizardComponentKindEnum ComponentKind { get; set; } + + /// Custom yolunda bagli CustomComponent adi. + public string CustomComponentName { get; set; } + + /// Menu kaydina yazilan URL; Custom yolunda bilesenin rota yoludur. + public string MenuUrl { get; set; } } diff --git a/api/src/Sozsoft.Platform.Application/ListForms/ListFormWizardAppService.cs b/api/src/Sozsoft.Platform.Application/ListForms/ListFormWizardAppService.cs index e061253a..3ac4cfe3 100644 --- a/api/src/Sozsoft.Platform.Application/ListForms/ListFormWizardAppService.cs +++ b/api/src/Sozsoft.Platform.Application/ListForms/ListFormWizardAppService.cs @@ -86,6 +86,11 @@ public class ListFormWizardAppService( input.ListFormCode = listFormCode; input.MenuCode = code; + // Yol ayrımı: Custom seçildiğinde ListForm ekosistemi (veri kaynağı, ListForm, + // alanlar, workflow) hiç üretilmez; menü doğrudan CustomComponent rotasına bağlanır. + var isCustomComponent = input.ComponentKind == WizardComponentKindEnum.Custom; + var customRoutePath = NormalizeCustomRoutePath(input, isCustomComponent); + var titleLangKey = $"{listFormCode}.Title"; var nameLangKey = code; var descLangKey = $"{listFormCode}.Desc"; @@ -95,8 +100,13 @@ public class ListFormWizardAppService( //Dil - Language Keys await CreateLangKey(nameLangKey, input.LanguageTextMenuEn, input.LanguageTextMenuTr, inserted); - await CreateLangKey(titleLangKey, input.LanguageTextTitleEn, input.LanguageTextTitleTr, inserted); - await CreateLangKey(descLangKey, input.LanguageTextDescEn, input.LanguageTextDescTr, inserted); + // Title/Desc anahtarları yalnızca ListForm tarafından tüketilir; Custom yolunda + // öksüz kayıt bırakmamak için üretilmez. + if (!isCustomComponent) + { + await CreateLangKey(titleLangKey, input.LanguageTextTitleEn, input.LanguageTextTitleTr, inserted); + await CreateLangKey(descLangKey, input.LanguageTextDescEn, input.LanguageTextDescTr, inserted); + } //Permission Group var groupName = input.PermissionGroupName ?? PlatformConsts.AppName; @@ -133,19 +143,23 @@ public class ListFormWizardAppService( ("Note", WizardConsts.LangKeyNote), }; + // Custom yolunda kayıt üzerinde CRUD yapılmadığı için yalnızca read izni üretilir. var permissionNames = new List { permRead.Name }; - foreach (var (suffix, langKey) in childPermissions) + if (!isCustomComponent) { - var permissionName = $"{code}.{suffix}"; - permissionNames.Add(permissionName); + foreach (var (suffix, langKey) in childPermissions) + { + var permissionName = $"{code}.{suffix}"; + permissionNames.Add(permissionName); - if (existingPerms.Any(a => a.Name == permissionName)) - continue; + if (existingPerms.Any(a => a.Name == permissionName)) + continue; - await repoPerm.InsertAsync( - new PermissionDefinitionRecord(GuidGenerator.Create(), groupName, permissionName, permRead.Name, langKey, true, MultiTenancySides.Both), - autoSave: false); - inserted.PermissionNames.Add(permissionName); + await repoPerm.InsertAsync( + new PermissionDefinitionRecord(GuidGenerator.Create(), groupName, permissionName, permRead.Name, langKey, true, MultiTenancySides.Both), + autoSave: false); + inserted.PermissionNames.Add(permissionName); + } } // Permission Grants - Bulk Insert (only missing ones) @@ -196,6 +210,11 @@ public class ListFormWizardAppService( var maxChildOrder = menuQueryable.Where(a => a.ParentCode == menuParent.Code).Select(a => (int?)a.Order).Max() ?? 0; var menuOrder = input.MenuOrder > 0 ? input.MenuOrder : maxChildOrder + 1; input.MenuOrder = menuOrder; + // Menünün açacağı adres: Custom yolunda CustomComponent'in kendi rotası (bu rota + // mapCustomComponentRoutes tarafından zaten kayıtlıdır), aksi halde ListForm adresi. + // Seed dosyasından da görülebilmesi için input'a geri yazılır. + var menuUrl = isCustomComponent ? customRoutePath : WizardConsts.MenuUrl(code); + input.MenuUrl = menuUrl; var existingMenu = await AsyncExecuter.FirstOrDefaultAsync(menuQueryable.Where(a => a.Code == code)); if (existingMenu == null) { @@ -209,18 +228,28 @@ public class ListFormWizardAppService( Target = null, ElementId = null, CssClass = null, - Url = WizardConsts.MenuUrl(code), + Url = menuUrl, RequiredPermissionName = permRead.Name, Order = menuOrder, }, autoSave: false); inserted.MenuCodes.Add(code); } - else if (existingMenu.Order != menuOrder) + else if (existingMenu.Order != menuOrder || existingMenu.Url != menuUrl) { existingMenu.Order = menuOrder; + existingMenu.Url = menuUrl; await repoMenu.UpdateAsync(existingMenu, autoSave: false); } + // Custom yolunda ListForm ekosistemi üretilmez: veri kaynağı, ListForm, alanlar, + // subform/widget/workflow kayıtlarının hiçbirine ihtiyaç yoktur. + if (isCustomComponent) + { + await _languageTextAppService.ClearRedisCacheAsync(); + await SaveWizardSeedFileAsync(input, false, false, inserted); + return; + } + //DataSource kodu ile iligli kod blogu var dataSourceQueryable = await repoDataSource.GetQueryableAsync(); var dataSource = await AsyncExecuter.FirstOrDefaultAsync(dataSourceQueryable.Where(a => a.Code == input.DataSourceCode)); @@ -572,6 +601,36 @@ public class ListFormWizardAppService( } } + /// + /// Custom yolunda component adı ve rota zorunludur. Rota + /// üzerinden taşınır ve CustomComponent sözleşmesiyle (CreateUpdateCustomComponentDto.RoutePath) + /// aynı şekilde '/' ile başlamalıdır; aksi halde menü, router'a kayıtlı olmayan bir adrese bağlanır. + /// List yolunda bileşen adı temizlenerek seed dosyasında artık veri bırakılmaz. + /// + private static string? NormalizeCustomRoutePath(ListFormWizardDto input, bool isCustomComponent) + { + if (!isCustomComponent) + { + input.CustomComponentName = null; + return null; + } + + var componentName = input.CustomComponentName?.Trim(); + if (string.IsNullOrWhiteSpace(componentName)) + throw new UserFriendlyException("Custom Component seçimi zorunludur."); + + var routePath = input.MenuUrl?.Trim(); + if (string.IsNullOrWhiteSpace(routePath)) + throw new UserFriendlyException($"'{componentName}' bileşeninin rota yolu (Route Path) boş olamaz."); + + if (!routePath.StartsWith('/')) + routePath = "/" + routePath; + + input.CustomComponentName = componentName; + input.MenuUrl = routePath; + return routePath; + } + public async Task> GetFiles() { var outputPath = ResolveWizardSeedOutputPath(); @@ -594,6 +653,9 @@ public class ListFormWizardAppService( DefaultLayout = seed?.Wizard?.DefaultLayout ?? string.Empty, MenuIcon = seed?.Wizard?.MenuIcon ?? string.Empty, ListFormCode = seed?.Wizard?.ListFormCode ?? string.Empty, + ComponentKind = seed?.Wizard?.ComponentKind ?? WizardComponentKindEnum.List, + CustomComponentName = seed?.Wizard?.CustomComponentName ?? string.Empty, + MenuUrl = seed?.Wizard?.MenuUrl ?? string.Empty, CreatedAt = fileName.Length >= 12 ? fileName[..12] : fileName, HasInsertedRecords = seed?.InsertedRecords != null && (seed.InsertedRecords.LanguageKeys.Count > 0 || @@ -675,8 +737,10 @@ public class ListFormWizardAppService( var listFormCode = seed.Wizard?.ListFormCode; var menuParentCode = seed.Wizard?.MenuParentCode; - // ListForm ve alanları her zaman sil (wizard bunları her zaman oluşturur) - if (!string.IsNullOrWhiteSpace(listFormCode)) + // ListForm ve alanlarını sil. Custom yolunda wizard hiç ListForm üretmediği için + // bu blok atlanır; aksi halde aynı kodu taşıyan bir List wizard'ının ListForm'u silinirdi. + var isCustomComponent = seed.Wizard?.ComponentKind == WizardComponentKindEnum.Custom; + if (!isCustomComponent && !string.IsNullOrWhiteSpace(listFormCode)) { var lf = await repoListForm.FirstOrDefaultAsync(a => a.ListFormCode == listFormCode); if (lf != null) await repoListForm.DeleteAsync(lf, autoSave: true); diff --git a/api/src/Sozsoft.Platform.Application/Menu/MenuAppService.cs b/api/src/Sozsoft.Platform.Application/Menu/MenuAppService.cs index 5071e879..ed7e7b3a 100644 --- a/api/src/Sozsoft.Platform.Application/Menu/MenuAppService.cs +++ b/api/src/Sozsoft.Platform.Application/Menu/MenuAppService.cs @@ -281,6 +281,73 @@ public class MenuAppService : CrudAppService< return entityDtos; } + /// + /// Menüyü tüm alanlarıyla günceller ve görünen adın dil metinlerini de yazar. + /// Kod (Code) dil anahtarı ve yetki eşleşmesi olduğu için değiştirilemez; gönderilen + /// değer yok sayılıp mevcut kod korunur. + /// + public async Task UpdateWithLanguageKeyTextAsync(Guid id, MenuDto input) + { + await CheckUpdatePolicyAsync(); + + var menu = await _menuRepository.GetAsync(id); + + // Kod değiştirilemez: dil anahtarı, alt menülerin ParentCode'u ve permission + // kayıtları bu değere bağlıdır. + input.Code = menu.Code; + + await UpsertMenuLanguageTextsAsync(menu.Code, input.MenuTextEn, input.MenuTextTr); + + return await base.UpdateAsync(id, input); + } + + /// + /// Menü koduna karşılık gelen dil anahtarını ve EN/TR metinlerini oluşturur veya günceller. + /// + private async Task UpsertMenuLanguageTextsAsync(string code, string menuTextEn, string menuTextTr) + { + var keyExists = await _repositoryKey.AnyAsync( + a => a.Key == code && + a.ResourceName == PlatformConsts.AppName); + if (!keyExists) + { + await _repositoryKey.InsertAsync(new LanguageKey + { + Key = code, + ResourceName = PlatformConsts.AppName + }, autoSave: true); + } + + foreach (var (culture, value) in new[] { ("en", menuTextEn), ("tr", menuTextTr) }) + { + if (value == null) + continue; + + var existing = await _repositoryText.FirstOrDefaultAsync( + a => a.CultureName == culture && + a.Key == code && + a.ResourceName == PlatformConsts.AppName); + + if (existing != null) + { + existing.Value = value; + await _repositoryText.UpdateAsync(existing, autoSave: true); + } + else + { + await _repositoryText.InsertAsync(new LanguageText + { + Key = code, + CultureName = culture, + Value = value, + ResourceName = PlatformConsts.AppName + }, autoSave: true); + } + } + + await _languageTextAppService.ClearRedisCacheAsync(); + } + public async Task CreateWithLanguageKeyTextAsync(MenuDto input) { await CheckCreatePolicyAsync(); diff --git a/api/src/Sozsoft.Platform.DbMigrator/Migrations/LanguagesData.json b/api/src/Sozsoft.Platform.DbMigrator/Migrations/LanguagesData.json index 51a71671..067825c8 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/Migrations/LanguagesData.json +++ b/api/src/Sozsoft.Platform.DbMigrator/Migrations/LanguagesData.json @@ -16254,6 +16254,12 @@ "en": "Clear All", "tr": "Tümünü Temizle" }, + { + "resourceName": "Platform", + "key": "App.WizardStep2.DeployUrl", + "en": "Deploy URL", + "tr": "Yayınlama URL'si" + }, { "resourceName": "Platform", "key": "App.WizardStep2.ListFormCodeHint", @@ -16674,6 +16680,120 @@ "en": "Deploying to server…", "tr": "Sunucuya deploy ediliyor…" }, + { + "resourceName": "Platform", + "key": "App.Step4Log.LinkingCustomComponent", + "en": "Linking custom component '{0}'…", + "tr": "'{0}' özel bileşeni bağlanıyor…" + }, + { + "resourceName": "Platform", + "key": "App.Platform.EditMenu", + "en": "Edit Menu", + "tr": "Menüyü Düzenle" + }, + { + "resourceName": "Platform", + "key": "App.Platform.Status", + "en": "Status", + "tr": "Durum" + }, + { + "resourceName": "Platform", + "key": "App.WizardStep1.RootMenu", + "en": "Root menu (no parent)", + "tr": "Kök menü (üst menü yok)" + }, + { + "resourceName": "Platform", + "key": "App.WizardStepComponent.MenuUrl", + "en": "Menu URL", + "tr": "Menü URL" + }, + { + "resourceName": "Platform", + "key": "App.WizardStepComponent.MenuUrlFromComponent", + "en": "Comes from the selected custom component", + "tr": "Seçilen özel bileşenden geliyor" + }, + { + "resourceName": "Platform", + "key": "App.WizardStepComponent.MenuUrlFromList", + "en": "Derived from the list form to be generated", + "tr": "Üretilecek liste formundan türetiliyor" + }, + { + "resourceName": "Platform", + "key": "App.WizardStepComponent.ComponentType", + "en": "Component Type", + "tr": "Bileşen Tipi" + }, + { + "resourceName": "Platform", + "key": "App.WizardStepComponent.ComponentTypeHint", + "en": "Decides whether the wizard generates a list form or links an existing custom component", + "tr": "Wizard'ın liste formu mu üreteceğini yoksa mevcut bir özel bileşene mi bağlanacağını belirler" + }, + { + "resourceName": "Platform", + "key": "App.WizardStepComponent.ListComponent", + "en": "List Component", + "tr": "Liste Bileşeni" + }, + { + "resourceName": "Platform", + "key": "App.WizardStepComponent.ListComponentHint", + "en": "Generates the platform list form: data source, fields, sub forms, widgets and workflow", + "tr": "Platform liste formunu üretir: veri kaynağı, alanlar, alt formlar, widget'lar ve iş akışı" + }, + { + "resourceName": "Platform", + "key": "App.WizardStepComponent.CustomComponent", + "en": "Custom Component", + "tr": "Özel Bileşen" + }, + { + "resourceName": "Platform", + "key": "App.WizardStepComponent.CustomComponentHint", + "en": "Links the menu to a component built in Developer Kit; no list form is generated", + "tr": "Menüyü Developer Kit'te üretilmiş bir bileşene bağlar; liste formu üretilmez" + }, + { + "resourceName": "Platform", + "key": "App.WizardStepComponent.ComponentName", + "en": "Component Name", + "tr": "Bileşen Adı" + }, + { + "resourceName": "Platform", + "key": "App.WizardStepComponent.SelectComponent", + "en": "Select component…", + "tr": "Bileşen seçin…" + }, + { + "resourceName": "Platform", + "key": "App.WizardStepComponent.RoutePath", + "en": "Route Path", + "tr": "Rota Yolu" + }, + { + "resourceName": "Platform", + "key": "App.WizardStepComponent.RoutePathHint", + "en": "The menu item created after deploy opens this address", + "tr": "Deploy sonrası oluşan menü bu adresi açar" + }, + { + "resourceName": "Platform", + "key": "App.WizardStepComponent.Preview", + "en": "Preview", + "tr": "Önizleme" + }, + { + "resourceName": "Platform", + "key": "App.WizardStepComponent.NoComponents", + "en": "No active custom component with a route path was found. Create one in Developer Kit → Components first.", + "tr": "Rota yolu tanımlı aktif bir özel bileşen bulunamadı. Önce Developer Kit → Bileşenler ekranından oluşturun." + }, { "resourceName": "Platform", "key": "App.Step4Log.Completed", diff --git a/api/src/Sozsoft.Platform.DbMigrator/Migrations/WizardDataSeeder.cs b/api/src/Sozsoft.Platform.DbMigrator/Migrations/WizardDataSeeder.cs index dcacfc0f..8139d40d 100644 --- a/api/src/Sozsoft.Platform.DbMigrator/Migrations/WizardDataSeeder.cs +++ b/api/src/Sozsoft.Platform.DbMigrator/Migrations/WizardDataSeeder.cs @@ -115,8 +115,13 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency await BackfillMenuParentShortNameAsync(seedFile.Wizard); - // Zaten seeded mi kontrol et (ListForm var mı?) - if (await _repoListForm.AnyAsync(a => a.ListFormCode == seedFile.Wizard.ListFormCode)) + // Zaten seeded mi kontrol et. Custom yolunda ListForm üretilmediği için + // idempotency menü kaydı üzerinden değerlendirilir. + var alreadySeeded = seedFile.Wizard.ComponentKind == WizardComponentKindEnum.Custom + ? await _repoMenu.AnyAsync(a => a.Code == ResolveMenuCode(seedFile.Wizard)) + : await _repoListForm.AnyAsync(a => a.ListFormCode == seedFile.Wizard.ListFormCode); + + if (alreadySeeded) { _logger.LogInformation("[{File}] '{WizardName}' already exists, skipped.", fileName, wizardName); continue; @@ -163,10 +168,17 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency var permImportName = $"{code}.Import"; var permNoteName = $"{code}.Note"; + // Yol ayrımı: Custom yolunda ListForm ekosistemi üretilmez (AppService.Create ile aynı davranış). + var isCustomComponent = input.ComponentKind == WizardComponentKindEnum.Custom; + var customRoutePath = NormalizeSeedCustomRoutePath(input); + // Dil - Language Keys await CreateLangKeyAsync(nameLangKey, input.LanguageTextMenuEn, input.LanguageTextMenuTr); - await CreateLangKeyAsync(titleLangKey, input.LanguageTextTitleEn, input.LanguageTextTitleTr); - await CreateLangKeyAsync(descLangKey, input.LanguageTextDescEn, input.LanguageTextDescTr); + if (!isCustomComponent) + { + await CreateLangKeyAsync(titleLangKey, input.LanguageTextTitleEn, input.LanguageTextTitleTr); + await CreateLangKeyAsync(descLangKey, input.LanguageTextDescEn, input.LanguageTextDescTr); + } // Permission Group var groupName = input.PermissionGroupName ?? AppName; @@ -189,35 +201,33 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency permRead = await _repoPerm.InsertAsync(new PermissionDefinitionRecord( Guid.NewGuid(), groupName, code, null, nameLangKey, true, MultiTenancySides.Both), autoSave: true); - var permCreate = existingPerms.FirstOrDefault(a => a.Name == permCreateName); - if (permCreate == null) - permCreate = await _repoPerm.InsertAsync(new PermissionDefinitionRecord( - Guid.NewGuid(), groupName, permCreateName, permRead.Name, WizardConsts.LangKeyCreate, true, MultiTenancySides.Both), autoSave: true); + // Custom yolunda kayıt üzerinde CRUD yapılmadığı için yalnızca read izni üretilir. + if (!isCustomComponent) + { + if (!existingPerms.Any(a => a.Name == permCreateName)) + await _repoPerm.InsertAsync(new PermissionDefinitionRecord( + Guid.NewGuid(), groupName, permCreateName, permRead.Name, WizardConsts.LangKeyCreate, true, MultiTenancySides.Both), autoSave: true); - var permUpdate = existingPerms.FirstOrDefault(a => a.Name == permUpdateName); - if (permUpdate == null) - permUpdate = await _repoPerm.InsertAsync(new PermissionDefinitionRecord( - Guid.NewGuid(), groupName, permUpdateName, permRead.Name, WizardConsts.LangKeyUpdate, true, MultiTenancySides.Both), autoSave: true); + if (!existingPerms.Any(a => a.Name == permUpdateName)) + await _repoPerm.InsertAsync(new PermissionDefinitionRecord( + Guid.NewGuid(), groupName, permUpdateName, permRead.Name, WizardConsts.LangKeyUpdate, true, MultiTenancySides.Both), autoSave: true); - var permDelete = existingPerms.FirstOrDefault(a => a.Name == permDeleteName); - if (permDelete == null) - permDelete = await _repoPerm.InsertAsync(new PermissionDefinitionRecord( - Guid.NewGuid(), groupName, permDeleteName, permRead.Name, WizardConsts.LangKeyDelete, true, MultiTenancySides.Both), autoSave: true); + if (!existingPerms.Any(a => a.Name == permDeleteName)) + await _repoPerm.InsertAsync(new PermissionDefinitionRecord( + Guid.NewGuid(), groupName, permDeleteName, permRead.Name, WizardConsts.LangKeyDelete, true, MultiTenancySides.Both), autoSave: true); - var permExport = existingPerms.FirstOrDefault(a => a.Name == permExportName); - if (permExport == null) - permExport = await _repoPerm.InsertAsync(new PermissionDefinitionRecord( - Guid.NewGuid(), groupName, permExportName, permRead.Name, WizardConsts.LangKeyExport, true, MultiTenancySides.Both), autoSave: true); + if (!existingPerms.Any(a => a.Name == permExportName)) + await _repoPerm.InsertAsync(new PermissionDefinitionRecord( + Guid.NewGuid(), groupName, permExportName, permRead.Name, WizardConsts.LangKeyExport, true, MultiTenancySides.Both), autoSave: true); - var permImport = existingPerms.FirstOrDefault(a => a.Name == permImportName); - if (permImport == null) - permImport = await _repoPerm.InsertAsync(new PermissionDefinitionRecord( - Guid.NewGuid(), groupName, permImportName, permRead.Name, WizardConsts.LangKeyImport, true, MultiTenancySides.Both), autoSave: true); + if (!existingPerms.Any(a => a.Name == permImportName)) + await _repoPerm.InsertAsync(new PermissionDefinitionRecord( + Guid.NewGuid(), groupName, permImportName, permRead.Name, WizardConsts.LangKeyImport, true, MultiTenancySides.Both), autoSave: true); - var permNote = existingPerms.FirstOrDefault(a => a.Name == permNoteName); - if (permNote == null) - permNote = await _repoPerm.InsertAsync(new PermissionDefinitionRecord( - Guid.NewGuid(), groupName, permNoteName, permRead.Name, WizardConsts.LangKeyNote, true, MultiTenancySides.Both), autoSave: true); + if (!existingPerms.Any(a => a.Name == permNoteName)) + await _repoPerm.InsertAsync(new PermissionDefinitionRecord( + Guid.NewGuid(), groupName, permNoteName, permRead.Name, WizardConsts.LangKeyNote, true, MultiTenancySides.Both), autoSave: true); + } // // Permission Grants - Admin role için, sadece eksik olanları ekle // var existingGrants = await _permissionGrantRepository.GetListAsync("R", PlatformConsts.AbpIdentity.User.AdminRoleName); @@ -264,6 +274,14 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency var maxChildOrder = menuQueryable.Where(a => a.ParentCode == menuParent.Code).Select(a => (int?)a.Order).Max() ?? 0; var menuOrder = input.MenuOrder > 0 ? input.MenuOrder : maxChildOrder + 1; input.MenuOrder = menuOrder; + // Menünün açacağı adres. Custom yolunda normalize edilmis rota, List yolunda + // MenuUrl alani (tasimayan eski dosyalar icin AppService.Create ile aynı kural). + var menuUrl = isCustomComponent + ? customRoutePath + : !string.IsNullOrWhiteSpace(input.MenuUrl) + ? input.MenuUrl.Trim() + : WizardConsts.MenuUrl(code); + input.MenuUrl = menuUrl; var existingMenu = await _repoMenu.FirstOrDefaultAsync(a => a.Code == code); if (existingMenu == null) { @@ -277,17 +295,22 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency Target = null, ElementId = null, CssClass = null, - Url = WizardConsts.MenuUrl(code), + Url = menuUrl, RequiredPermissionName = permRead.Name, Order = menuOrder, }, autoSave: true); } - else if (existingMenu.Order != menuOrder) + else if (existingMenu.Order != menuOrder || existingMenu.Url != menuUrl) { existingMenu.Order = menuOrder; + existingMenu.Url = menuUrl; await _repoMenu.UpdateAsync(existingMenu, autoSave: true); } + // Custom yolunda ListForm ekosistemi üretilmez. + if (isCustomComponent) + return; + // DataSource var existingDataSource = await _repoDataSource.FirstOrDefaultAsync(a => a.Code == input.DataSourceCode); if (existingDataSource == null) @@ -527,6 +550,34 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency } } + /// + /// Wizard'in menu kodunu, ApplyWizardSeedAsync ile ayni kurala gore cozer. + /// Custom yolunda idempotency kontrolu ListForm yerine bu kod uzerinden yapilir. + /// + private static string ResolveMenuCode(ListFormWizardDto input) + { + return string.IsNullOrWhiteSpace(input.MenuCode) + ? WizardConsts.WizardKey(input.WizardName.Trim()) + : input.MenuCode.Trim(); + } + + /// + /// Custom yolunda menunun acacagi rotayi normalize eder. Rota MenuUrl uzerinden + /// tasinir; bos ise menu hicbir yere gitmeyecegi icin seed dosyasi hatali kabul edilir. + /// + private static string NormalizeSeedCustomRoutePath(ListFormWizardDto input) + { + if (input.ComponentKind != WizardComponentKindEnum.Custom) + return null; + + var routePath = input.MenuUrl?.Trim(); + if (string.IsNullOrWhiteSpace(routePath)) + throw new InvalidOperationException( + $"'{input.WizardName}' custom component wizard'inda Route Path bos olamaz."); + + return routePath.StartsWith('/') ? routePath : "/" + routePath; + } + private async Task BackfillMenuParentShortNameAsync(ListFormWizardDto input) { if (string.IsNullOrWhiteSpace(input.MenuParentCode) || diff --git a/api/src/Sozsoft.Platform.Domain.Shared/Enums/WizardComponentKindEnum.cs b/api/src/Sozsoft.Platform.Domain.Shared/Enums/WizardComponentKindEnum.cs new file mode 100644 index 00000000..9e67cf93 --- /dev/null +++ b/api/src/Sozsoft.Platform.Domain.Shared/Enums/WizardComponentKindEnum.cs @@ -0,0 +1,14 @@ +namespace Sozsoft.Platform.Enums; + +/// +/// Wizard'in hangi yolu urettigini belirler. Varsayilan oldugu icin +/// bu alani icermeyen eski wizard seed dosyalari da mevcut davranisla calismaya devam eder. +/// +public enum WizardComponentKindEnum +{ + /// Platformun dinamik ListForm ekosistemi uretilir (varsayilan). + List = 0, + + /// DeveloperKit'te uretilmis bir CustomComponent'e menu baglanir; ListForm uretilmez. + Custom = 1, +} diff --git a/configs/seeds/host/crud/AbpRoles.json b/configs/seeds/host/crud/AbpRoles.json new file mode 100644 index 00000000..59a3dd84 --- /dev/null +++ b/configs/seeds/host/crud/AbpRoles.json @@ -0,0 +1,36 @@ +{ + "EntityName": "AbpRoles", + "GeneratedAt": "2026-08-18T07:26:49.511788Z", + "Endpoints": [ + { + "Method": "POST", + "Path": "/api/app/crudendpoint/AbpRoles", + "OperationType": "Create", + "IsActive": true + }, + { + "Method": "DELETE", + "Path": "/api/app/crudendpoint/AbpRoles/{id}", + "OperationType": "Delete", + "IsActive": true + }, + { + "Method": "GET", + "Path": "/api/app/crudendpoint/AbpRoles/{id}", + "OperationType": "GetById", + "IsActive": true + }, + { + "Method": "GET", + "Path": "/api/app/crudendpoint/AbpRoles", + "OperationType": "GetList", + "IsActive": true + }, + { + "Method": "PUT", + "Path": "/api/app/crudendpoint/AbpRoles/{id}", + "OperationType": "Update", + "IsActive": true + } + ] +} \ No newline at end of file diff --git a/ui/src/proxy/admin/wizard/models.ts b/ui/src/proxy/admin/wizard/models.ts index b862e837..c2a76818 100644 --- a/ui/src/proxy/admin/wizard/models.ts +++ b/ui/src/proxy/admin/wizard/models.ts @@ -27,11 +27,23 @@ export interface ListFormWizardColumnGroupDto { items: ListFormWizardColumnItemDto[] } +/** Wizard'ın yol ayrımı; backend `WizardComponentKindEnum` ile birebir eşleşir. */ +export enum WizardComponentKindEnum { + List = 0, + Custom = 1, +} + export interface ListFormWizardDto { + componentKind: WizardComponentKindEnum + wizardName: string listFormCode: string menuCode: string menuOrder: number + + customComponentName: string + /** Menünün açtığı adres; Custom yolunda seçilen bileşenin rota yolu. */ + menuUrl: string isTenant: boolean isBranch: boolean isOrganizationUnit: boolean @@ -90,6 +102,9 @@ export interface WizardFileInfoDto { menuIcon: string createdAt: string hasInsertedRecords: boolean + componentKind: WizardComponentKindEnum + customComponentName: string + menuUrl: string } export interface WizardInsertedRecordsDto { diff --git a/ui/src/services/menu.service.ts b/ui/src/services/menu.service.ts index d0a22aae..a966b054 100644 --- a/ui/src/services/menu.service.ts +++ b/ui/src/services/menu.service.ts @@ -31,6 +31,17 @@ export class MenuService { { apiName: this.apiName }, ) + /** Menüyü tüm alanlarıyla günceller; görünen adın EN/TR dil metinlerini de yazar. */ + updateWithLanguageKeyText = (id: string, input: MenuDto) => + apiService.fetchData( + { + method: 'PUT', + url: `/api/app/menu/${id}/with-language-key-text`, + data: input, + }, + { apiName: this.apiName }, + ) + createWithLanguageKeyText = (input: MenuDto) => apiService.fetchData( { diff --git a/ui/src/views/admin/listForm/wizard/Wizard.tsx b/ui/src/views/admin/listForm/wizard/Wizard.tsx index 5181cc24..c36f00ab 100644 --- a/ui/src/views/admin/listForm/wizard/Wizard.tsx +++ b/ui/src/views/admin/listForm/wizard/Wizard.tsx @@ -35,7 +35,9 @@ import { Container } from '@/components/shared' import { sqlDataTypeToDbType } from '../edit/options' import { useStoreActions } from '@/store/store' import { deleteWizardFile, getWizardFile, postListFormWizard } from '@/services/wizard.service' -import { ListFormWizardDto } from '@/proxy/admin/wizard/models' +import { ListFormWizardDto, WizardComponentKindEnum } from '@/proxy/admin/wizard/models' +import WizardStepComponent from './WizardStepComponent' +import WizardStepCustomComponent from './WizardStepCustomComponent' import { ListFormWorkflowCriteriaDto, SubFormDto, @@ -52,6 +54,9 @@ const initialValues: ListFormWizardDto = { listFormCode: '', menuCode: '', menuOrder: 0, + componentKind: WizardComponentKindEnum.List, + customComponentName: '', + menuUrl: '', isTenant: false, isBranch: false, isOrganizationUnit: false, @@ -174,18 +179,29 @@ const step1ValidationSchema = Yup.object().shape({ languageTextMenuTr: Yup.string().required(), }) +/** + * Custom Component yolunda Settings adımı hiç gösterilmez; bu alanların zorunluluğu + * da kalkmalıdır, aksi halde deploy'a kadar temizlenemeyen hatalar taşınır. + */ +const requiredForListComponent = (schema: T) => + schema.when('componentKind', { + is: (kind: number) => kind !== WizardComponentKindEnum.Custom, + then: (s: T) => s.required(), + otherwise: (s: T) => s.optional(), + }) + const step2ValidationSchema = Yup.object().shape({ - listFormCode: Yup.string().required(), - languageTextTitleEn: Yup.string().required(), - languageTextTitleTr: Yup.string().required(), - languageTextDescEn: Yup.string().required(), - languageTextDescTr: Yup.string().required(), - dataSourceCode: Yup.string().required(), + listFormCode: requiredForListComponent(Yup.string()), + languageTextTitleEn: requiredForListComponent(Yup.string()), + languageTextTitleTr: requiredForListComponent(Yup.string()), + languageTextDescEn: requiredForListComponent(Yup.string()), + languageTextDescTr: requiredForListComponent(Yup.string()), + dataSourceCode: requiredForListComponent(Yup.string()), dataSourceConnectionString: Yup.string(), - selectCommandType: Yup.string().required(), - selectCommand: Yup.string().required(), - keyFieldName: Yup.string().required(), - keyFieldDbSourceType: Yup.number().required(), + selectCommandType: requiredForListComponent(Yup.string()), + selectCommand: requiredForListComponent(Yup.string()), + keyFieldName: requiredForListComponent(Yup.string()), + keyFieldDbSourceType: requiredForListComponent(Yup.number()), isTenant: Yup.boolean(), isBranch: Yup.boolean(), isOrganizationUnit: Yup.boolean(), @@ -225,34 +241,57 @@ const removeTenantGroupItems = (groups: WizardGroup[]) => items: group.items.filter((item) => !isTenantColumn(item.fieldName)), })) +/** Yeni wizard açıldığında ön seçilen veri kaynağı kodu. */ +const DEFAULT_DATA_SOURCE_CODE = 'Default' + // Auto-derive listFormCode from wizardName const deriveListFormCode = (name: string) => { const sanitized = name.replace(/\s/g, '') return sanitized ? `App.Wizard.${sanitized}` : '' } +/** + * Adım kimlikleri. `resolvedStep` geri düşerken `id < currentStep` karşılaştırması + * yaptığı için kimlikler görüntüleme sırasıyla artan olmak zorundadır. + */ +const STEP = { + component: 0, + customComponent: 1, + menu: 2, + settings: 3, + fields: 4, + subForms: 5, + widgets: 6, + workflow: 7, + todo: 8, + tree: 9, + gantt: 10, + scheduler: 11, + deploy: 12, +} as const + /** Yalnızca ilgili layout seçiliyken gösterilen adımlar. */ const OPTIONAL_STEPS = [ { - id: 6, + id: STEP.todo, flag: 'todo', translationKey: '::App.Platform.TabTodo', fallbackTitle: 'Todo / Kanban', }, { - id: 7, + id: STEP.tree, flag: 'tree', translationKey: '::App.Platform.TabTree', fallbackTitle: 'Tree', }, { - id: 8, + id: STEP.gantt, flag: 'gantt', translationKey: '::App.Platform.TabGantt', fallbackTitle: 'Gantt', }, { - id: 9, + id: STEP.scheduler, flag: 'scheduler', translationKey: '::App.Platform.TabScheduler', fallbackTitle: 'Scheduler', @@ -445,9 +484,23 @@ const Wizard = () => { setIsLoadingDataSource(true) const response = await getDataSources() if (response.data?.items) { - setDataSourceList( - response.data.items.map((item: any) => ({ value: item.code, label: item.code })), - ) + const options: SelectBoxOption[] = response.data.items.map((item: any) => ({ + value: item.code, + label: item.code, + })) + setDataSourceList(options) + + // Yeni wizard'da veri kaynağı elle seçilmesin; platform varsayılanı otomatik gelir. + // Edit modunda kayıtlı değer sonradan yükleniyor, burada dokunulmaz. + if (!isEditMode && !formikRef.current?.values.dataSourceCode) { + const defaultOption = options.find( + (o) => String(o.value).toLowerCase() === DEFAULT_DATA_SOURCE_CODE.toLowerCase(), + ) + if (defaultOption) { + formikRef.current?.setFieldValue('dataSourceCode', defaultOption.value) + setCurrentDataSource(String(defaultOption.value)) + } + } } setIsLoadingDataSource(false) } @@ -512,6 +565,10 @@ const Wizard = () => { wizardName: w.wizardName ?? '', listFormCode: w.listFormCode ?? '', menuCode: w.menuCode ?? '', + // Bu alanları içermeyen eski seed dosyaları List yolunda açılır. + componentKind: w.componentKind ?? WizardComponentKindEnum.List, + customComponentName: w.customComponentName ?? '', + menuUrl: w.menuUrl ?? '', menuOrder: w.menuOrder && w.menuOrder > 0 ? w.menuOrder @@ -768,10 +825,30 @@ const Wizard = () => { setCurrentStep(targetStep) } - const handleNext = () => validateAndGo(step1FieldNames, 1) - const handleNext2 = () => validateAndGo(step2FieldNames, 2) + /** Menu adimindan sonra List yolunda Settings, Custom yolunda dogrudan Deploy gelir. */ + const handleMenuNext = (isCustom: boolean) => + validateAndGo(step1FieldNames, isCustom ? STEP.deploy : STEP.settings) + const handleNext2 = () => validateAndGo(step2FieldNames, STEP.fields) - const handleBack = () => setCurrentStep(0) + const handleBack = () => setCurrentStep(STEP.menu) + + /** + * Yol ayrımı değişince karşı yolun taşıdığı veriler temizlenir; aksi halde + * List'ten Custom'a geçip geri dönen kullanıcı, seed dosyasına artık alan yazar. + */ + const handleComponentKindChange = (kind: WizardComponentKindEnum) => { + const formik = formikRef.current + formik?.setFieldValue('componentKind', kind) + if (kind !== WizardComponentKindEnum.Custom) { + formik?.setFieldValue('customComponentName', '') + formik?.setFieldValue('menuUrl', '') + } + } + + const handleCustomComponentChange = (name: string, routePath: string) => { + formikRef.current?.setFieldValue('customComponentName', name) + formikRef.current?.setFieldValue('menuUrl', routePath) + } const { getConfig } = useStoreActions((a) => a.abpConfig) const preventEnterSubmit = (event: KeyboardEvent) => { @@ -792,6 +869,7 @@ const Wizard = () => { if (!formik) throw new Error(translate('::App.Platform.Unknown')) const values = formik.values + const isCustom = values.componentKind === WizardComponentKindEnum.Custom try { // Edit modunda: önce eski dosyayı sil (DB kayıtlarını temizler) @@ -799,6 +877,28 @@ const Wizard = () => { await deleteWizardFile(editFileName) } + // Custom yolunda ListForm'a ait hiçbir koleksiyon gönderilmez. + if (isCustom) { + await postListFormWizard({ + ...values, + groups: [], + subForms: [], + widgets: [], + }) + + await getConfig(true) + + toast.push( + + {translate('::App.Platform.FormBilgileriKaydedildi')} + , + { placement: 'bottom-end' }, + ) + + navigate(values.menuUrl, { replace: true }) + return + } + await postListFormWizard({ ...values, groups: editingGroups.map((g) => ({ @@ -880,33 +980,53 @@ const Wizard = () => { }} > {({ touched, errors, values }) => { + // Custom Component yolunda ListForm'a özgü adımların (Settings, Fields, + // SubForms, Widgets, Workflow ve layout adımları) hiçbirine ihtiyaç yok. + const isCustomComponent = values.componentKind === WizardComponentKindEnum.Custom + const activeSteps = [ - { id: 0, title: translate('::App.VideoRoom.Menu') }, { - id: 1, - title: translate('::App.Setting'), + id: STEP.component, + title: translate('::App.WizardStepComponent.ComponentType'), }, - { - id: 2, - title: translate('::App.ListFormWizard.ListFormFields'), - }, - { - id: 3, - title: translate('::App.ListFormEdit.SubForms'), - }, - { - id: 4, - title: translate('::App.ListFormEdit.TabWidgets'), - }, - { - id: 5, - title: translate('::App.ListFormEdit.TabWorkflow'), - }, - ...OPTIONAL_STEPS.filter((step) => values[step.flag]).map((step) => ({ - id: step.id, - title: translate(step.translationKey) || step.fallbackTitle, - })), - { id: 10, title: translate('::App.Platform.Deploy') }, + ...(isCustomComponent + ? [ + { + id: STEP.customComponent, + title: translate('::App.WizardStepComponent.CustomComponent'), + }, + ] + : []), + { id: STEP.menu, title: translate('::App.VideoRoom.Menu') }, + ...(isCustomComponent + ? [] + : [ + { + id: STEP.settings, + title: translate('::App.Setting'), + }, + { + id: STEP.fields, + title: translate('::App.ListFormWizard.ListFormFields'), + }, + { + id: STEP.subForms, + title: translate('::App.ListFormEdit.SubForms'), + }, + { + id: STEP.widgets, + title: translate('::App.ListFormEdit.TabWidgets'), + }, + { + id: STEP.workflow, + title: translate('::App.ListFormEdit.TabWorkflow'), + }, + ...OPTIONAL_STEPS.filter((step) => values[step.flag]).map((step) => ({ + id: step.id, + title: translate(step.translationKey) || step.fallbackTitle, + })), + ]), + { id: STEP.deploy, title: translate('::App.Platform.Deploy') }, ] // Opsiyonel bir adım (todo/tree/gantt/scheduler) sonradan kapatılırsa @@ -946,8 +1066,35 @@ const Wizard = () => {
= 2 ? undefined : 'sm'}> - {/* ─── Step 1: Basic Info ─────────────────────────────── */} - {resolvedStep === 0 && ( + {/* ─── Step 1: Component Type (yol ayrımı) ─────────────── */} + {resolvedStep === STEP.component && ( + + setCurrentStep( + values.componentKind === WizardComponentKindEnum.Custom + ? STEP.customComponent + : STEP.menu, + ) + } + /> + )} + + {/* ─── Step 2: Custom Component seçimi ─────────────────── */} + {resolvedStep === STEP.customComponent && ( + setCurrentStep(STEP.component)} + onNext={() => setCurrentStep(STEP.menu)} + /> + )} + + {/* ─── Step 3: Menu ───────────────────────────────────── */} + {resolvedStep === STEP.menu && ( { onReloadMenu={getMenuList} permissionGroupList={permissionGroupList} isLoadingPermissionGroup={isLoadingPermissionGroup} - onNext={handleNext} + onBack={() => + setCurrentStep( + values.componentKind === WizardComponentKindEnum.Custom + ? STEP.customComponent + : STEP.component, + ) + } + onNext={() => + handleMenuNext(values.componentKind === WizardComponentKindEnum.Custom) + } translate={translate} /> )} - {/* ─── Step 2: Data Settings ───────────────────────────── */} - {resolvedStep === 1 && ( + {/* ─── Step 4: Data Settings ───────────────────────────── */} + {resolvedStep === STEP.settings && ( { /> )} - {/* ─── Step 3: List Form Fields ───────────────────────────── */} - {resolvedStep === 2 && ( + {/* ─── Step 5: List Form Fields ───────────────────────────── */} + {resolvedStep === STEP.fields && ( { dbObjects={dbObjects} dsCode={currentDataSource} translate={translate} - onBack={() => setCurrentStep(1)} - onNext={() => setCurrentStep(3)} + onBack={() => setCurrentStep(STEP.settings)} + onNext={() => setCurrentStep(STEP.subForms)} /> )} - {/* ─── Step 4: Sub Forms ───────────────────────────── */} - {resolvedStep === 3 && ( + {/* ─── Step 6: Sub Forms ───────────────────────────── */} + {resolvedStep === STEP.subForms && ( setCurrentStep(2)} - onNext={() => setCurrentStep(4)} + onBack={() => setCurrentStep(STEP.fields)} + onNext={() => setCurrentStep(STEP.widgets)} /> )} - {/* ─── Step 5: Widgets ───────────────────────────── */} - {resolvedStep === 4 && ( + {/* ─── Step 7: Widgets ───────────────────────────── */} + {resolvedStep === STEP.widgets && ( setCurrentStep(3)} - onNext={() => setCurrentStep(5)} + onBack={() => setCurrentStep(STEP.subForms)} + onNext={() => setCurrentStep(STEP.workflow)} /> )} - {/* ─── Step 6: Workflow ───────────────────────────── */} - {resolvedStep === 5 && ( + {/* ─── Step 8: Workflow ───────────────────────────── */} + {resolvedStep === STEP.workflow && ( { translate={translate} onWorkflowChange={setWorkflow} onCriteriaChange={setCriteria} - onBack={() => setCurrentStep(4)} + onBack={() => setCurrentStep(STEP.widgets)} onNext={() => goToAdjacentStep(1)} /> )} - {/* ─── Step 7: Todo / Kanban ────────────────────── */} - {resolvedStep === 6 && values.todo && ( + {/* ─── Step 9: Todo / Kanban ────────────────────── */} + {resolvedStep === STEP.todo && values.todo && ( { /> )} - {/* ─── Step 8: Tree ─────────────────────────────── */} - {resolvedStep === 7 && values.tree && ( + {/* ─── Step 10: Tree ─────────────────────────────── */} + {resolvedStep === STEP.tree && values.tree && ( { /> )} - {/* ─── Step 9: Gantt ────────────────────────────── */} - {resolvedStep === 8 && values.gantt && ( + {/* ─── Step 11: Gantt ────────────────────────────── */} + {resolvedStep === STEP.gantt && values.gantt && ( { /> )} - {/* ─── Step 10: Scheduler ───────────────────────── */} - {resolvedStep === 9 && values.scheduler && ( + {/* ─── Step 12: Scheduler ───────────────────────── */} + {resolvedStep === STEP.scheduler && values.scheduler && ( { /> )} - {/* ─── Step 11: Deploy ──────────────────────────── */} - {resolvedStep === 10 && ( + {/* ─── Step 13: Deploy ──────────────────────────── */} + {resolvedStep === STEP.deploy && ( { className="flex items-center" > - {translate('::App.ListFormWizard.AddNewRecord')} + + {translate('::App.ListFormWizard.AddNewRecord')} + @@ -165,6 +171,7 @@ const WizardFileManager = () => {
{filteredFiles.map((f) => { const Icon = navigationIcon[f.menuIcon] ?? FaDatabase + const isCustom = f.componentKind === WizardComponentKindEnum.Custom return (
{ >
-
+
+ {/* 1. satır: wizard adı */}
- {f.wizardName || f.fileName} + {f.wizardName || f.fileName}
-
+ + {/* 2. satır: dosya adı ve üretilen kod */} +
+ {f.fileName} + - {f.fileName} - {f.listFormCode} + {isCustom ? f.customComponentName : f.listFormCode}
+ + {/* 3. satır: yol ayrımı (Custom / List), layout ve menü adresi */} +
+ + {isCustom ? : } + {translate( + isCustom + ? '::App.WizardStepComponent.CustomComponent' + : '::App.WizardStepComponent.ListComponent', + )} + + + {/* Custom yolunda layout kavramı yoktur. */} + {!isCustom && f.defaultLayout && ( + + {f.defaultLayout} + + )} + + {f.menuUrl && ( + + + {f.menuUrl} + + )} +
@@ -193,6 +238,19 @@ const WizardFileManager = () => { )} + {/* Menünün açtığı adres yeni sekmede açılır; adres yoksa buton anlamsızdır. */} + {f.menuUrl && ( + + )}
{isExpanded && @@ -266,13 +212,8 @@ function TreeNode({ onSelect={onSelect} expanded={expanded} onToggle={onToggle} - editingCode={editingCode} - editingValue={editingValue} saving={saving} onStartEdit={onStartEdit} - onEditChange={onEditChange} - onSaveEdit={onSaveEdit} - onCancelEdit={onCancelEdit} onDelete={onDelete} /> ))} @@ -304,6 +245,8 @@ interface MenuTreeInlineProps { isLoading: boolean invalid?: boolean onReload: () => void + /** Düzenleme dialogunu ilgili menü ile açar. */ + onEditMenu: (node: MenuTreeNode & { id?: string }) => void initialExpanded?: Set } @@ -315,6 +258,7 @@ function MenuTreeInline({ isLoading, invalid, onReload, + onEditMenu, initialExpanded, }: MenuTreeInlineProps) { const [expanded, setExpanded] = useState>(() => initialExpanded ?? new Set()) @@ -330,8 +274,6 @@ function MenuTreeInline({ return next }) }, [value, rawItems]) - const [editingCode, setEditingCode] = useState(null) - const [editingValue, setEditingValue] = useState('') const [saving, setSaving] = useState(false) const { translate } = useLocalization() @@ -343,38 +285,10 @@ function MenuTreeInline({ return n }) - const handleStartEdit = (code: string, currentName: string) => { - setEditingCode(code) - setEditingValue(currentName) - } - - const handleSaveEdit = async (node: MenuTreeNode & { id?: string }) => { - if (!editingValue.trim() || !node.id) return - setSaving(true) - try { - const original = rawItems.find((i) => i.code === node.code) - if (!original) return - await menuService.update(node.id, { - ...original, - displayName: editingValue.trim(), - } as MenuDto) - onReload() - setEditingCode(null) - } catch (e: any) { - toast.push(, { - placement: 'bottom-end', - }) - } finally { - setSaving(false) - } - } - const handleDelete = async (node: MenuTreeNode & { id?: string }) => { if (!node.id) return if ( - !window.confirm( - `"${node.displayName}" ${translate('::App.WizardStep1.DeleteMenuConfirm')}`, - ) + !window.confirm(`"${node.displayName}" ${translate('::App.WizardStep1.DeleteMenuConfirm')}`) ) return setSaving(true) @@ -404,21 +318,17 @@ function MenuTreeInline({ const sharedNodeProps = { expanded, onToggle: toggle, - editingCode, - editingValue, saving, - onStartEdit: handleStartEdit, - onEditChange: setEditingValue, - onSaveEdit: handleSaveEdit, - onCancelEdit: () => setEditingCode(null), + onStartEdit: onEditMenu, onDelete: handleDelete, } return (
-
+ {/* Yükseklik sağdaki alan kolonuna göre esner; taşma bu alanda kaydırılır. */} +
{isLoading ? (
Loading…
) : enrichedNodes.length === 0 ? ( @@ -466,6 +376,7 @@ export interface WizardStep1Props { onReloadMenu: () => void permissionGroupList: SelectBoxOption[] isLoadingPermissionGroup: boolean + onBack: () => void onNext: () => void translate: (key: string) => string } @@ -485,11 +396,27 @@ const WizardStep1 = ({ onReloadMenu, permissionGroupList, isLoadingPermissionGroup, + onBack, onNext, translate, }: WizardStep1Props) => { const [menuDialogOpen, setMenuDialogOpen] = useState(false) const [menuDialogParentCode, setMenuDialogParentCode] = useState('') + // Dolu ise dialog düzenleme modunda açılır. + const [menuBeingEdited, setMenuBeingEdited] = useState<(MenuItem & { id?: string }) | null>(null) + + /** Ağaç düğümünü ham menü kaydıyla eşleyip düzenleme dialogunu açar. */ + const handleEditMenu = (node: MenuTreeNode & { id?: string }) => { + const original = rawMenuItems.find((item) => item.code === node.code) + if (!original) return + setMenuBeingEdited(original) + setMenuDialogOpen(true) + } + + const closeMenuDialog = () => { + setMenuDialogOpen(false) + setMenuBeingEdited(null) + } const menuDialogInitialOrder = useMemo(() => { const maxOrder = rawMenuItems.reduce((max, item) => { @@ -500,6 +427,17 @@ const WizardStep1 = ({ return maxOrder + 100 }, [rawMenuItems]) + /** + * Deploy'da menü kaydına yazılacak adres (backend: WizardConsts.MenuUrl / RoutePath). + * Custom yolunda bileşenin kendi rotası, List yolunda üretilecek ListForm adresi. + */ + const isCustomComponent = values.componentKind === WizardComponentKindEnum.Custom + const menuUrl = isCustomComponent + ? values.menuUrl + : values.menuCode + ? `/admin/list/${values.menuCode}` + : '' + const step1Missing = [ !wizardName && translate('::App.WizardStep1.WizardName'), !values.menuCode && translate('::App.WizardStep1.MenuCode'), @@ -538,9 +476,11 @@ const WizardStep1 = ({ {/* Menu Code / Menu Text / Permission Group — 2-column grid */}
{/* Col 1 */} -
+
{/* Menu Parent */} { + setMenuBeingEdited(null) setMenuDialogParentCode( values.menuParentCode ? findRootCode(rawMenuItems, values.menuParentCode) @@ -591,6 +532,7 @@ const WizardStep1 = ({ isLoading={isLoadingMenu} invalid={false} onReload={onReloadMenu} + onEditMenu={handleEditMenu} initialExpanded={ values.menuParentCode ? getAncestorCodes(rawMenuItems, values.menuParentCode) @@ -603,11 +545,19 @@ const WizardStep1 = ({ setMenuDialogOpen(false)} + onClose={closeMenuDialog} initialParentCode={menuDialogParentCode} initialOrder={menuDialogInitialOrder} rawItems={rawMenuItems} - onSaved={onMenuCreated} + editMenu={menuBeingEdited} + onSaved={async (menu) => { + // Düzenlemede seçili parent'ı değiştirmemek için yalnızca ağaç tazelenir. + if (menuBeingEdited) { + await onReloadMenu() + return + } + await onMenuCreated(menu) + }} />
@@ -739,13 +689,34 @@ const WizardStep1 = ({ )} + + {/* Deploy sonrası menü ikonunun açacağı adres; seçilen yola göre değişir. */} + + {translate( + isCustomComponent + ? '::App.WizardStepComponent.MenuUrlFromComponent' + : '::App.WizardStepComponent.MenuUrlFromList', + )} + + } + > +
+ {menuUrl || '—'} +
+
{/* ─── Fixed Footer ─────────────────────────────── */}
-
+ +
{!step1CanGo && ( ⚠ {translate('::App.ListFormWizard.Required')}: {step1Missing.join(', ')} diff --git a/ui/src/views/admin/listForm/wizard/WizardStep11.tsx b/ui/src/views/admin/listForm/wizard/WizardStep11.tsx index eaf79ba5..fd8d55c8 100644 --- a/ui/src/views/admin/listForm/wizard/WizardStep11.tsx +++ b/ui/src/views/admin/listForm/wizard/WizardStep11.tsx @@ -13,7 +13,7 @@ import { } from 'react-icons/fa' import { WizardGroup } from './WizardStep3' import { dbSourceTypeOptions, selectCommandTypeOptions } from '../edit/options' -import { ListFormWizardDto } from '@/proxy/admin/wizard/models' +import { ListFormWizardDto, WizardComponentKindEnum } from '@/proxy/admin/wizard/models' import { ListFormWorkflowCriteriaDto, SubFormDto, @@ -69,6 +69,39 @@ function buildLogSteps( translate: (key: string, params?: Record) => string, ): Omit[] { const totalFields = groups.reduce((acc, g) => acc + g.items.length, 0) + + // Custom yolunda veri kaynağı / ListForm / alan üretimi hiç çalışmaz. + if (values.componentKind === WizardComponentKindEnum.Custom) { + return [ + { id: 1, label: translate('::App.Step4Log.ValidatingConfig') }, + { + id: 2, + label: translate('::App.Step4Log.CreatingMenu', { 0: values.menuCode }), + detail: `Parent: ${values.menuParentCode}`, + }, + { + id: 3, + label: translate('::App.Step4Log.SavingLanguageTexts'), + detail: `EN: ${values.languageTextMenuEn} / TR: ${values.languageTextMenuTr}`, + }, + { + id: 4, + label: translate('::App.Step4Log.ConfiguringPermission', { + 0: values.permissionGroupName, + }), + }, + { + id: 5, + label: translate('::App.Step4Log.LinkingCustomComponent', { + 0: values.customComponentName, + }), + detail: values.menuUrl, + }, + { id: 8, label: translate('::App.Step4Log.Deploying') }, + { id: 9, label: translate('::App.Step4Log.Completed') }, + ] + } + return [ { id: 1, label: translate('::App.Step4Log.ValidatingConfig') }, { @@ -259,6 +292,13 @@ const WizardStep11 = ({ ) const workflowItems = criteria.length > 0 ? criteria : (workflow.criteria ?? []) + /** + * Custom Component yolunda ListForm üretilmediği için layout, alanlar, subform, + * widget ve workflow özetlerinin hiçbiri anlamlı değildir; yalnızca menü ve + * bağlanan bileşen gösterilir. + */ + const isCustom = values.componentKind === WizardComponentKindEnum.Custom + return (
{/* ── Left: Summary ──────────────────────────────────────────── */} @@ -267,23 +307,14 @@ const WizardStep11 = ({
- + - - + +
-
- - - - - - - - o.value === values.selectCommandType) - ?.label ?? String(values.selectCommandType) - })`} - /> - o.value === values.keyFieldDbSourceType) - ?.label ?? String(values.keyFieldDbSourceType) - })`} - /> -
+ {isCustom ? ( +
+ + +
+ ) : ( +
+ + + + + + + + o.value === values.selectCommandType) + ?.label ?? String(values.selectCommandType) + })`} + /> + o.value === values.keyFieldDbSourceType) + ?.label ?? String(values.keyFieldDbSourceType) + })`} + /> +
+ )}
-
-
- {[ - { - label: translate('::App.WizardStep4.SelectedColumns'), - value: selectedColumns.size, - className: 'text-indigo-600 dark:text-indigo-400', - }, - { - label: translate('::App.WizardStep4.EditingForm'), - value: editingFormFields.length, - className: 'text-emerald-600 dark:text-emerald-400', - }, - { - label: translate('::App.WizardStep4.FormGroups'), - value: groups.length, - className: 'text-gray-700 dark:text-gray-200', - }, - ].map((item) => ( -
-
- {item.value} + {!isCustom && ( +
+
+ {[ + { + label: translate('::App.WizardStep4.SelectedColumns'), + value: selectedColumns.size, + className: 'text-indigo-600 dark:text-indigo-400', + }, + { + label: translate('::App.WizardStep4.EditingForm'), + value: editingFormFields.length, + className: 'text-emerald-600 dark:text-emerald-400', + }, + { + label: translate('::App.WizardStep4.FormGroups'), + value: groups.length, + className: 'text-gray-700 dark:text-gray-200', + }, + ].map((item) => ( +
+
+ {item.value} +
+
{item.label}
-
{item.label}
-
- ))} -
+ ))} +
-
- {groups.map((g) => ( -
-
- - {g.caption || `(${translate('::App.Listform.ListformField.Group')})`} - -
- - {g.items.length} {translate('::App.Platform.Field')} - - - {g.colCount} {translate('::App.Listform.ListformField.Column')} +
+ {groups.map((g) => ( +
+
+ + {g.caption || `(${translate('::App.Listform.ListformField.Group')})`} +
+ + {g.items.length} {translate('::App.Platform.Field')} + + + {g.colCount} {translate('::App.Listform.ListformField.Column')} + +
+
+
+ {g.items.length === 0 ? ( + + {translate('::App.WizardStep4.NoFields')} + + ) : ( + g.items.map((item) => { + const meta = selectCommandColumns.find( + (c) => c.columnName === item.fieldName, + ) + const isKeyField = item.fieldName === values.keyFieldName + const isPopupField = item.includeInEditingForm && !isKeyField + return ( +
+
+ + {item.fieldName} + + {meta?.dataType && ( + + {meta.dataType} + + )} +
+
+ + {translate('::App.WizardStep4.SelectedColumns')} + + {isPopupField && ( + + {translate('::App.WizardStep4.EditingForm')} + + )} + {isKeyField && ( + + {translate('::App.WizardStep4.KeyField')} + + )} + + {item.editorType} + + + span-{item.colSpan} + {item.isRequired && *} + +
+
+ ) + }) + )}
-
- {g.items.length === 0 ? ( - - {translate('::App.WizardStep4.NoFields')} - - ) : ( - g.items.map((item) => { - const meta = selectCommandColumns.find((c) => c.columnName === item.fieldName) - const isKeyField = item.fieldName === values.keyFieldName - const isPopupField = item.includeInEditingForm && !isKeyField + ))} + {ungroupedSelectedColumns.length > 0 && ( +
+
+ {translate('::App.WizardStep4.UngroupedColumns')} +
+
+ {ungroupedSelectedColumns.map((col) => { + const meta = selectCommandColumns.find((c) => c.columnName === col) return ( -
-
- - {item.fieldName} + {col} + {meta?.dataType && ( + + {meta.dataType} - {meta?.dataType && ( - - {meta.dataType} - - )} -
-
- - {translate('::App.WizardStep4.SelectedColumns')} - - {isPopupField && ( - - {translate('::App.WizardStep4.EditingForm')} - - )} - {isKeyField && ( - - {translate('::App.WizardStep4.KeyField')} - - )} - - {item.editorType} - - - span-{item.colSpan} - {item.isRequired && *} - -
-
+ )} + ) - }) - )} + })} +
-
- ))} - {ungroupedSelectedColumns.length > 0 && ( -
-
- {translate('::App.WizardStep4.UngroupedColumns')} -
-
- {ungroupedSelectedColumns.map((col) => { - const meta = selectCommandColumns.find((c) => c.columnName === col) - return ( - - {col} - {meta?.dataType && ( - - {meta.dataType} - - )} - - ) - })} -
-
- )} -
-
+ )} +
+ + )} - {subForms.length > 0 && ( + {!isCustom && subForms.length > 0 && (
{subForms.map((subForm, index) => ( @@ -508,7 +553,7 @@ const WizardStep11 = ({
)} - {widgets.length > 0 && ( + {!isCustom && widgets.length > 0 && (
{widgets.map((widget, index) => ( @@ -532,11 +577,8 @@ const WizardStep11 = ({
)} - {(hasWorkflowFields || workflowItems.length > 0) && ( -
+ {!isCustom && (hasWorkflowFields || workflowItems.length > 0) && ( +
@@ -582,7 +624,7 @@ const WizardStep11 = ({
)} - {values.todo && ( + {!isCustom && values.todo && (
- {/* Stats */} -
- {[ - { label: translate('::App.Listform.ListformField.Group'), value: groups.length }, - { label: translate('::App.Platform.Field'), value: totalFields }, - { - label: translate('::App.WizardStep4.EditingForm'), - value: editingFormFields.length, - }, - { - label: translate('::App.Listform.ListformField.Column'), - value: selectedColumns.size, - }, - { - label: translate('::App.ListFormEdit.SubForms'), - value: subForms.length, - }, - { - label: translate('::App.ListFormEdit.TabWidgets'), - value: widgets.length, - }, - { - label: translate('::App.ListFormEdit.TabWorkflow'), - value: workflowItems.length, - }, - { - label: translate('::App.Platform.TabTodo'), - value: values.todo ? 1 : 0, - }, - ].map((s) => ( -
-
- {s.value} + {/* Stats — Custom yolunda sayilacak ListForm artefakti yok. */} + {!isCustom && ( +
+ {[ + { label: translate('::App.Listform.ListformField.Group'), value: groups.length }, + { label: translate('::App.Platform.Field'), value: totalFields }, + { + label: translate('::App.WizardStep4.EditingForm'), + value: editingFormFields.length, + }, + { + label: translate('::App.Listform.ListformField.Column'), + value: selectedColumns.size, + }, + { + label: translate('::App.ListFormEdit.SubForms'), + value: subForms.length, + }, + { + label: translate('::App.ListFormEdit.TabWidgets'), + value: widgets.length, + }, + { + label: translate('::App.ListFormEdit.TabWorkflow'), + value: workflowItems.length, + }, + { + label: translate('::App.Platform.TabTodo'), + value: values.todo ? 1 : 0, + }, + ].map((s) => ( +
+
+ {s.value} +
+
{s.label}
-
{s.label}
-
- ))} -
+ ))} +
+ )} {/* Log panel — grows to fill remaining height */}
diff --git a/ui/src/views/admin/listForm/wizard/WizardStep2.tsx b/ui/src/views/admin/listForm/wizard/WizardStep2.tsx index dec85eb5..51cf3279 100644 --- a/ui/src/views/admin/listForm/wizard/WizardStep2.tsx +++ b/ui/src/views/admin/listForm/wizard/WizardStep2.tsx @@ -501,7 +501,8 @@ const WizardStep2 = ({
-
+ {/* Menü URL'i Menu adımında gösterilir; burada yalnızca layout seçimi kalır. */} +
void + translate: (key: string) => string + onNext: () => void +} + +// ─── Kind card ──────────────────────────────────────────────────────────────── + +interface KindCardProps { + active: boolean + icon: React.ReactNode + title: string + description: string + onSelect: () => void +} + +function KindCard({ active, icon, title, description, onSelect }: KindCardProps) { + return ( + + ) +} + +// ─── WizardStepComponent ────────────────────────────────────────────────────── + +/** + * Wizard'ın yol ayrımı adımı: yalnızca tip seçimi yapılır. + * List seçilirse mevcut ListForm akışı aynen devam eder; Custom seçilirse bir + * sonraki adımda bileşen seçimi yapılır ve ListForm'a ait tüm adımlar devre dışı kalır. + */ +const WizardStepComponent = ({ + values, + onKindChange, + translate, + onNext, +}: WizardStepComponentProps) => { + const isCustom = values.componentKind === WizardComponentKindEnum.Custom + + return ( +
+ {/* Bu adımda tek bir karar var; seçim ekranın ortasına yerleşir. */} +
+
+
+

+ {translate('::App.WizardStepComponent.ComponentType')} +

+

+ {translate('::App.WizardStepComponent.ComponentTypeHint')} +

+
+ +
+ } + title={translate('::App.WizardStepComponent.ListComponent')} + description={translate('::App.WizardStepComponent.ListComponentHint')} + onSelect={() => onKindChange(WizardComponentKindEnum.List)} + /> + } + title={translate('::App.WizardStepComponent.CustomComponent')} + description={translate('::App.WizardStepComponent.CustomComponentHint')} + onSelect={() => onKindChange(WizardComponentKindEnum.Custom)} + /> +
+
+
+ + {/* ─── Fixed Footer ─────────────────────────────── */} +
+ {/* Wizard'in ilk adimi oldugu icin geri butonu yok. */} +
+ +
+
+
+ ) +} + +export default WizardStepComponent diff --git a/ui/src/views/admin/listForm/wizard/WizardStepCustomComponent.tsx b/ui/src/views/admin/listForm/wizard/WizardStepCustomComponent.tsx new file mode 100644 index 00000000..3048df39 --- /dev/null +++ b/ui/src/views/admin/listForm/wizard/WizardStepCustomComponent.tsx @@ -0,0 +1,196 @@ +import { Button, FormItem, Select } from '@/components/ui' +import { useComponents } from '@/contexts/ComponentContext' +import { ListFormWizardDto } from '@/proxy/admin/wizard/models' +import { Field, FieldProps } from 'formik' +import { useMemo } from 'react' +import { FaArrowLeft, FaArrowRight, FaCube, FaExternalLinkAlt } from 'react-icons/fa' + +// ─── Props ──────────────────────────────────────────────────────────────────── + +export interface WizardStepCustomComponentProps { + values: ListFormWizardDto + onCustomComponentChange: (name: string, routePath: string) => void + translate: (key: string) => string + onBack: () => void + onNext: () => void +} + +// ─── WizardStepCustomComponent ──────────────────────────────────────────────── + +/** + * Custom yolunda menüye bağlanacak bileşenin seçildiği adım. Seçimle birlikte + * bileşenin rota yolu ve canlı önizlemesi gösterilir; deploy sonrası menü ikonu + * tam olarak bu rotayı açar. + */ +const WizardStepCustomComponent = ({ + values, + onCustomComponentChange, + translate, + onBack, + onNext, +}: WizardStepCustomComponentProps) => { + const { components, compiling, componentErrors, renderComponent } = useComponents() + + // Yalnızca aktif ve rota tanımlı bileşenler menüye bağlanabilir; pasif bir + // bileşenin rotası router'a hiç kaydedilmediği için menü boşa düşerdi. + const selectableComponents = useMemo( + () => components.filter((c) => c.isActive && c.routePath?.trim()), + [components], + ) + + const componentOptions = useMemo( + () => + selectableComponents.map((c) => ({ + value: c.name, + label: c.description?.trim() ? `${c.name} — ${c.description.trim()}` : c.name, + })), + [selectableComponents], + ) + + const selectedComponent = useMemo( + () => selectableComponents.find((c) => c.name === values.customComponentName), + [selectableComponents, values.customComponentName], + ) + + const compileError = values.customComponentName + ? componentErrors[values.customComponentName] + : undefined + + const stepMissing = [ + !values.customComponentName && translate('::App.WizardStepComponent.ComponentName'), + !!values.customComponentName && + !values.menuUrl && + translate('::App.WizardStepComponent.RoutePath'), + ].filter(Boolean) as string[] + const canGo = stepMissing.length === 0 + + return ( +
+
+ + + {({ field, form }: FieldProps) => ( + { const codeToken = toCodeToken(e.target.value) @@ -287,7 +349,7 @@ export function MenuAddDialog({ })) }} placeholder="MyMenu" - className={fieldCls} + className={isEditMode ? disabledCls : fieldCls} />
@@ -346,7 +408,22 @@ export function MenuAddDialog({
- + {isEditMode ? ( + + ) : ( + + )}
@@ -378,12 +455,43 @@ export function MenuAddDialog({ className={`${fieldCls}`} /> {shortNameRequired && ( -

- {translate('::App.Platform.MenuCodeHint')} -

+

{translate('::App.Platform.MenuCodeHint')}

)}
+ {/* Row 6 — Url | Durum (yalnızca düzenlemede) */} + {isEditMode && ( +
+
+ + setForm((p) => ({ ...p, url: e.target.value }))} + placeholder="/admin/list/App.Wizard.MyMenu" + className={`${fieldCls} font-mono`} + /> +
+
+ + +
+
+ )} + {/* Footer */}