From eb77285e599cc26a36b2aef6603c4b3dfb2cd70c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Sedat=20=C3=96ZT=C3=9CRK?=
<76204082+iamsedatozturk@users.noreply.github.com>
Date: Tue, 18 Aug 2026 22:36:51 +0300
Subject: [PATCH] =?UTF-8?q?WizardManager=20i=C3=A7in=20Import=20=C3=B6zell?=
=?UTF-8?q?i=C4=9Fi=20eklendi?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.gitignore | 4 +-
.../Wizard/IListFormWizardAppService.cs | 18 +-
.../ListForms/Wizard/ListFormWizardDto.cs | 9 +
.../ListForms/Wizard/WizardImportDtos.cs | 72 ++
.../DeveloperKit/CustomComponentAppService.cs | 14 +
.../ListForms/ListFormWizardAppService.cs | 432 +++++++++++-
.../Migrations/LanguagesData.json | 164 ++++-
.../Migrations/PermissionsData.json | 72 ++
.../Enums/WizardImportEntryStatusEnum.cs | 14 +
.../PlatformConsts.cs | 26 +
.../host/wizard/20260818220914_RoleList.json | 148 ++++
ui/src/constants/permission.constant.ts | 28 +
ui/src/proxy/admin/wizard/models.ts | 41 ++
ui/src/services/wizard.service.ts | 52 +-
ui/src/views/admin/listForm/wizard/Wizard.tsx | 68 +-
.../listForm/wizard/WizardFileManager.tsx | 162 +++--
.../listForm/wizard/WizardImportDialog.tsx | 662 ++++++++++++++++++
.../wizard/WizardStepCustomComponent.tsx | 1 +
...rdStep2.tsx => WizardStepDataSettings.tsx} | 15 +-
...{WizardStep11.tsx => WizardStepDeploy.tsx} | 17 +-
.../{WizardStep3.tsx => WizardStepFields.tsx} | 23 +-
...ardStep9.tsx => WizardStepGanttLayout.tsx} | 16 +-
.../{WizardStep1.tsx => WizardStepMenu.tsx} | 15 +-
...ep10.tsx => WizardStepSchedulerLayout.tsx} | 15 +-
...WizardStep4.tsx => WizardStepSubForms.tsx} | 9 +-
...zardStep7.tsx => WizardStepTodoLayout.tsx} | 12 +-
...zardStep8.tsx => WizardStepTreeLayout.tsx} | 15 +-
...{WizardStep5.tsx => WizardStepWidgets.tsx} | 8 +-
...WizardStep6.tsx => WizardStepWorkflow.tsx} | 22 +-
ui/src/views/developerKit/ComponentEditor.tsx | 10 +-
.../views/developerKit/ComponentManager.tsx | 89 ++-
.../developerKit/SqlTableDesignerDialog.tsx | 2 +-
32 files changed, 2065 insertions(+), 190 deletions(-)
create mode 100644 api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/WizardImportDtos.cs
create mode 100644 api/src/Sozsoft.Platform.Domain.Shared/Enums/WizardImportEntryStatusEnum.cs
create mode 100644 configs/seeds/host/wizard/20260818220914_RoleList.json
create mode 100644 ui/src/views/admin/listForm/wizard/WizardImportDialog.tsx
rename ui/src/views/admin/listForm/wizard/{WizardStep2.tsx => WizardStepDataSettings.tsx} (98%)
rename ui/src/views/admin/listForm/wizard/{WizardStep11.tsx => WizardStepDeploy.tsx} (98%)
rename ui/src/views/admin/listForm/wizard/{WizardStep3.tsx => WizardStepFields.tsx} (98%)
rename ui/src/views/admin/listForm/wizard/{WizardStep9.tsx => WizardStepGanttLayout.tsx} (92%)
rename ui/src/views/admin/listForm/wizard/{WizardStep1.tsx => WizardStepMenu.tsx} (98%)
rename ui/src/views/admin/listForm/wizard/{WizardStep10.tsx => WizardStepSchedulerLayout.tsx} (94%)
rename ui/src/views/admin/listForm/wizard/{WizardStep4.tsx => WizardStepSubForms.tsx} (98%)
rename ui/src/views/admin/listForm/wizard/{WizardStep7.tsx => WizardStepTodoLayout.tsx} (93%)
rename ui/src/views/admin/listForm/wizard/{WizardStep8.tsx => WizardStepTreeLayout.tsx} (89%)
rename ui/src/views/admin/listForm/wizard/{WizardStep5.tsx => WizardStepWidgets.tsx} (98%)
rename ui/src/views/admin/listForm/wizard/{WizardStep6.tsx => WizardStepWorkflow.tsx} (97%)
diff --git a/.gitignore b/.gitignore
index 7eea4de7..c2cafb96 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,4 +7,6 @@ logs/
# Deploy sırasında git tag'lerinden üretilir (configs/change-logs/generate-change-logs.sh)
# ve API konteynerine mount edilir. Takip edilirse deploy'daki `git pull` yerel
# değişiklikle çakışır ve dosya eskir.
-configs/change-logs/change-logs.json
\ No newline at end of file
+configs/change-logs/change-logs.json
+# Wizard import staging klasoru (gecici; oturum bitince silinir)
+configs/seeds/.imports/
diff --git a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/IListFormWizardAppService.cs b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/IListFormWizardAppService.cs
index 8fee504d..1452eec0 100644
--- a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/IListFormWizardAppService.cs
+++ b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/IListFormWizardAppService.cs
@@ -1,4 +1,4 @@
-using System.Collections.Generic;
+using System.Collections.Generic;
using System.Threading.Tasks;
using Volo.Abp.Content;
@@ -13,5 +13,19 @@ public interface IListFormWizardAppService
/// Wizard dosyasini ve bagli seed dosyalarini zip olarak dondurur.
Task ExportFile(string fileName);
-}
+ ///
+ /// Export zip'ini staging klasorune acar ve her dosya icin hedefteki karsiligiyla
+ /// karsilastirma sonucunu dondurur. Bu adim hicbir hedef dosyaya dokunmaz.
+ ///
+ Task AnalyzeImport(IRemoteStreamContent file);
+
+ /// Analizde donen tek bir dosyayi hedefe yazar; onceki icerik rollback icin yedeklenir.
+ Task ApplyImport(WizardImportApplyInput input);
+
+ /// Import oturumunu basariyla kapatir ve staging/yedek klasorunu siler.
+ Task CompleteImport(string importId);
+
+ /// O ana kadar yazilan dosyalari geri alir ve staging klasorunu siler.
+ Task RollbackImport(string importId);
+}
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 04558a4f..6c515f41 100644
--- a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/ListFormWizardDto.cs
+++ b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/ListFormWizardDto.cs
@@ -1,6 +1,7 @@
using Sozsoft.Platform.Enums;
using System.Collections.Generic;
using System.Data;
+using System.Text.Json.Serialization;
namespace Sozsoft.Platform.ListForms;
@@ -13,6 +14,14 @@ public class ListFormWizardDto
///
public WizardComponentKindEnum ComponentKind { get; set; } = WizardComponentKindEnum.List;
+ ///
+ /// Duzenleme modunda, yerine gecilecek seed dosyasinin adi. Doluysa istek bir guncellemedir:
+ /// sunucu once bu dosyayi ve urettigi kayitlari siler, sonra yenisini uretir.
+ /// Yalnizca istek girdisidir; seed dosyasina yazilmaz.
+ ///
+ [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
+ public string EditFileName { get; set; }
+
public string WizardName { get; set; }
public string ListFormCode { get; set; }
public string MenuCode { get; set; }
diff --git a/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/WizardImportDtos.cs b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/WizardImportDtos.cs
new file mode 100644
index 00000000..b43c3553
--- /dev/null
+++ b/api/src/Sozsoft.Platform.Application.Contracts/ListForms/Wizard/WizardImportDtos.cs
@@ -0,0 +1,72 @@
+using System.Collections.Generic;
+using Sozsoft.Platform.Enums;
+
+namespace Sozsoft.Platform.ListForms;
+
+///
+/// Import edilen zip icindeki tek bir seed dosyasinin analiz sonucu.
+/// Yol her zaman kapsam klasorune (host / tenants/{tenantId}) goreli tutulur.
+///
+public class WizardImportEntryDto
+{
+ /// Kapsam klasorune goreli yol, ornegin custom/Orders.json.
+ public string Path { get; set; }
+
+ /// Yolun ilk segmenti; listede gruplama icin kullanilir (wizard, crud, custom, sql, postgres).
+ public string Folder { get; set; }
+
+ public string FileName { get; set; }
+
+ public WizardImportEntryStatusEnum Status { get; set; }
+
+ public long IncomingSize { get; set; }
+
+ public long ExistingSize { get; set; }
+
+ /// Zip'ten gelen icerik; yalnizca Conflict durumunda doldurulur.
+ public string IncomingContent { get; set; }
+
+ /// Hedefteki mevcut icerik; yalnizca Conflict durumunda doldurulur.
+ public string ExistingContent { get; set; }
+}
+
+/// Zip analizinin tamami; uygulama adimi bu ile yurutulur.
+public class WizardImportAnalysisDto
+{
+ /// Staging klasorunu ve rollback kaydini tanimlayan oturum kimligi.
+ public string ImportId { get; set; }
+
+ /// Dosyalarin yazilacagi kapsam klasoru (host ya da tenants/{tenantId}).
+ public string ScopeFolder { get; set; }
+
+ public List Entries { get; set; } = [];
+
+ /// Guvenlik/format nedeniyle atlanan zip girdileri icin aciklamalar.
+ public List Warnings { get; set; } = [];
+}
+
+/// Tek bir dosyanin hedefe yazilmasi istegi.
+public class WizardImportApplyInput
+{
+ public string ImportId { get; set; }
+
+ /// Analizde donen kapsam-goreli yol.
+ public string Path { get; set; }
+
+ ///
+ /// Kullanicinin diff ekraninda birlestirdigi icerik. Bos birakilirsa zip'ten gelen
+ /// icerik oldugu gibi yazilir.
+ ///
+ public string Content { get; set; }
+}
+
+/// Tek dosya yazma sonucu; ilerleme gostergesinde satir olarak gosterilir.
+public class WizardImportApplyResultDto
+{
+ public string Path { get; set; }
+
+ /// Dosya hedefte zaten varken uzerine yazildiysa true.
+ public bool Overwritten { get; set; }
+
+ public long Size { get; set; }
+}
diff --git a/api/src/Sozsoft.Platform.Application/DeveloperKit/CustomComponentAppService.cs b/api/src/Sozsoft.Platform.Application/DeveloperKit/CustomComponentAppService.cs
index 67b81f7b..865f7fe1 100644
--- a/api/src/Sozsoft.Platform.Application/DeveloperKit/CustomComponentAppService.cs
+++ b/api/src/Sozsoft.Platform.Application/DeveloperKit/CustomComponentAppService.cs
@@ -11,12 +11,20 @@ using Microsoft.Extensions.Logging;
using Sozsoft.Platform.Data.Seeds;
using Sozsoft.Platform.DeveloperKit;
using Sozsoft.Platform.Entities;
+using static Sozsoft.Platform.PlatformConsts;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Repositories;
namespace Platform.Api.Application;
+///
+/// Custom component yonetimi. Sinif seviyesindeki yalnizca
+/// kimlik dogrulamasi ister; ekrani gorme ve CRUD haklari asagida ABP policy adlarina baglanir.
+/// App.DeveloperKit.Components okuma, .Create/.Update/.Delete aksiyon
+/// yetkileridir. calisma zamani okuma yoludur ve yalnizca
+/// oturum acmis olmayi ister.
+///
[Authorize]
public class CustomComponentAppService : CrudAppService<
CustomComponent,
@@ -47,6 +55,12 @@ public class CustomComponentAppService : CrudAppService<
{
_crudEndpointRepository = crudEndpointRepository;
_configuration = configuration;
+
+ GetPolicyName = AppCodes.DeveloperKits.Components.Component;
+ GetListPolicyName = AppCodes.DeveloperKits.Components.Component;
+ CreatePolicyName = AppCodes.DeveloperKits.Components.Create;
+ UpdatePolicyName = AppCodes.DeveloperKits.Components.Update;
+ DeletePolicyName = AppCodes.DeveloperKits.Components.Delete;
}
public override async Task CreateAsync(CreateUpdateCustomComponentDto input)
diff --git a/api/src/Sozsoft.Platform.Application/ListForms/ListFormWizardAppService.cs b/api/src/Sozsoft.Platform.Application/ListForms/ListFormWizardAppService.cs
index 354888d4..f5eb6a25 100644
--- a/api/src/Sozsoft.Platform.Application/ListForms/ListFormWizardAppService.cs
+++ b/api/src/Sozsoft.Platform.Application/ListForms/ListFormWizardAppService.cs
@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
+using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
@@ -31,6 +32,10 @@ namespace Sozsoft.Platform.ListForms;
///
/// Wizard, global menu/permission/language kayitlari ve seed dosyalari uretir.
/// Bu nedenle sadece host tarafinda tanimli olan App.Listforms.Wizard yetkisi ile calisir.
+///
+/// Sinif seviyesindeki yetki ekrani gorme (okuma) hakkidir; aksiyonlar ayrica kendi alt
+/// yetkilerini ister: .Create, .Update, .Delete, .Export, .Import.
+///
///
[Authorize(AppCodes.Listforms.Wizard)]
public class ListFormWizardAppService(
@@ -76,6 +81,19 @@ public class ListFormWizardAppService(
{
Check.NotNull(input, nameof(input));
+ // Duzenleme, eski dosyanin (ve urettigi kayitlarin) silinip yeniden uretilmesidir.
+ // Ayrimi istemci degil sunucu yapar; boylece yetki dogru politikaya baglanir ve
+ // silme ile yeniden uretme arasinda yarim kalmis bir durum olusmaz.
+ var editFileName = input.EditFileName?.Trim();
+ var isEdit = !string.IsNullOrWhiteSpace(editFileName);
+
+ await AuthorizationService.CheckAsync(isEdit
+ ? AppCodes.Listforms.Wizards.Update
+ : AppCodes.Listforms.Wizards.Create);
+
+ if (isEdit)
+ await DeleteFileInternalAsync(editFileName!);
+
var wizardName = input.WizardName?.Trim();
if (string.IsNullOrWhiteSpace(wizardName))
throw new UserFriendlyException("Wizard adı zorunludur.");
@@ -583,6 +601,9 @@ public class ListFormWizardAppService(
var outputPath = ResolveWizardSeedOutputPath();
Directory.CreateDirectory(outputPath);
+ // Duzenleme bilgisi yalnizca istege aittir; seed dosyasinda yeri yoktur.
+ input.EditFileName = null;
+
var seedData = new WizardSeedFileDto
{
Wizard = input,
@@ -694,7 +715,17 @@ public class ListFormWizardAppService(
}
[UnitOfWork]
- public async Task DeleteFile(string fileName)
+ [Authorize(AppCodes.Listforms.Wizards.Delete)]
+ public Task DeleteFile(string fileName)
+ {
+ return DeleteFileInternalAsync(fileName);
+ }
+
+ ///
+ /// Seed dosyasini ve wizard'in urettigi kayitlari siler. Yetki kontrolu cagiran uca aittir:
+ /// silme yolunda Delete, duzenleme yolunda Update politikasi kullanilir.
+ ///
+ private async Task DeleteFileInternalAsync(string fileName)
{
var filePath = ResolveWizardSeedFilePath(fileName);
@@ -906,6 +937,7 @@ public class ListFormWizardAppService(
/// {sql|postgres}/{object|execute}/{nesne}.sql ve varsa crud/{nesne}.json.
///
///
+ [Authorize(AppCodes.Listforms.Wizards.Export)]
[HttpGet("api/app/list-form-wizard/export")]
public async Task ExportFile(string fileName)
{
@@ -951,6 +983,404 @@ public class ListFormWizardAppService(
return new RemoteStreamContent(memory, $"{safeName}.zip", "application/zip");
}
+ // ── Import ─────────────────────────────────────────────────────────────────────
+ // Import iki adimlidir: once zip staging klasorune acilir ve her dosya hedefteki
+ // karsiligiyla karsilastirilir (AnalyzeImport), kullanici catismalari cozdukten sonra
+ // dosyalar tek tek yazilir (ApplyImport). Yazilan her dosyanin onceki hali staging
+ // altindaki backup klasorune alindigi icin hata durumunda RollbackImport ile tum
+ // toplu islem geri alinabilir.
+
+ /// Staging oturumlarinin tutuldugu klasor; seed kokunun altinda nokta ile baslar, seeder taramalarina girmez.
+ private const string ImportSessionsFolder = ".imports";
+ private const string ImportStagedFolder = "staged";
+ private const string ImportBackupFolder = "backup";
+ private const string ImportAppliedLogFile = "applied.log";
+
+ private const long ImportMaxEntryBytes = 5 * 1024 * 1024;
+ private const long ImportMaxArchiveBytes = 50 * 1024 * 1024;
+ private const int ImportMaxEntryCount = 500;
+
+ /// Kapsam klasorunde import edilmesine izin verilen kok klasorler.
+ private static readonly string[] ImportAllowedRootFolders =
+ [
+ SeedPathResolver.WizardFolder,
+ SeedPathResolver.CrudFolder,
+ SeedPathResolver.CustomFolder,
+ SeedPathResolver.SqlFolder,
+ SeedPathResolver.PostgresFolder,
+ ];
+
+ /// SQL saglayici klasorlerinin altinda izin verilen script klasorleri.
+ private static readonly string[] ImportAllowedScriptFolders =
+ [
+ SeedPathResolver.ObjectFolder,
+ SeedPathResolver.ExecuteFolder,
+ ];
+
+ private static readonly UTF8Encoding ImportEncoding = new(encoderShouldEmitUTF8Identifier: false);
+
+ [Authorize(AppCodes.Listforms.Wizards.Import)]
+ [HttpPost("api/app/list-form-wizard/analyze-import")]
+ public async Task AnalyzeImport([FromForm] IRemoteStreamContent file)
+ {
+ if (file == null)
+ throw new UserFriendlyException("Dosya bulunamadı.");
+
+ CleanupStaleImportSessions();
+
+ using var buffer = new MemoryStream();
+ await using (var upload = file.GetStream())
+ await upload.CopyToAsync(buffer);
+
+ if (buffer.Length == 0)
+ throw new UserFriendlyException("Dosya boş.");
+
+ if (buffer.Length > ImportMaxArchiveBytes)
+ throw new UserFriendlyException("Zip dosyası çok büyük.");
+
+ buffer.Position = 0;
+
+ ZipArchive archive;
+ try
+ {
+ archive = new ZipArchive(buffer, ZipArchiveMode.Read);
+ }
+ catch (InvalidDataException)
+ {
+ throw new UserFriendlyException("Geçerli bir zip dosyası değil.");
+ }
+
+ var importId = GuidGenerator.Create().ToString("N");
+ var sessionPath = GetImportSessionPath(importId);
+ var stagedRoot = Path.Combine(sessionPath, ImportStagedFolder);
+ var scopeRoot = Path.GetFullPath(SeedPathResolver.GetScopePath(_configuration, CurrentTenant.Id));
+
+ var result = new WizardImportAnalysisDto
+ {
+ ImportId = importId,
+ ScopeFolder = SeedPathResolver.GetScopeFolderName(CurrentTenant.Id),
+ };
+
+ using (archive)
+ {
+ Directory.CreateDirectory(stagedRoot);
+
+ var seen = new HashSet(StringComparer.OrdinalIgnoreCase);
+
+ foreach (var zipEntry in archive.Entries)
+ {
+ // Klasor girdilerinin adi bostur; icerik tasimazlar.
+ if (string.IsNullOrEmpty(zipEntry.Name))
+ continue;
+
+ var relativePath = NormalizeImportEntryPath(zipEntry.FullName);
+ if (relativePath == null)
+ {
+ result.Warnings.Add($"{zipEntry.FullName}: desteklenmeyen klasör/dosya, atlandı.");
+ continue;
+ }
+
+ if (zipEntry.Length > ImportMaxEntryBytes)
+ {
+ result.Warnings.Add($"{relativePath}: dosya çok büyük, atlandı.");
+ continue;
+ }
+
+ if (!seen.Add(relativePath))
+ {
+ result.Warnings.Add($"{relativePath}: zip içinde birden fazla kez var, ilki kullanıldı.");
+ continue;
+ }
+
+ if (seen.Count > ImportMaxEntryCount)
+ throw new UserFriendlyException("Zip içindeki dosya sayısı çok fazla.");
+
+ byte[] incoming;
+ await using (var source = zipEntry.Open())
+ using (var entryBuffer = new MemoryStream())
+ {
+ await source.CopyToAsync(entryBuffer);
+ incoming = entryBuffer.ToArray();
+ }
+
+ var stagedPath = Path.Combine(stagedRoot, relativePath.Replace('/', Path.DirectorySeparatorChar));
+ Directory.CreateDirectory(Path.GetDirectoryName(stagedPath)!);
+ await File.WriteAllBytesAsync(stagedPath, incoming);
+
+ var targetPath = Path.Combine(scopeRoot, relativePath.Replace('/', Path.DirectorySeparatorChar));
+ var segments = relativePath.Split('/');
+
+ var entry = new WizardImportEntryDto
+ {
+ Path = relativePath,
+ Folder = segments[0],
+ FileName = segments[^1],
+ IncomingSize = incoming.LongLength,
+ };
+
+ if (!File.Exists(targetPath))
+ {
+ entry.Status = WizardImportEntryStatusEnum.New;
+ }
+ else
+ {
+ var existing = await File.ReadAllBytesAsync(targetPath);
+ entry.ExistingSize = existing.LongLength;
+
+ var incomingText = NormalizeImportText(incoming);
+ var existingText = NormalizeImportText(existing);
+
+ if (string.Equals(incomingText, existingText, StringComparison.Ordinal))
+ {
+ entry.Status = WizardImportEntryStatusEnum.Identical;
+ }
+ else
+ {
+ // Karsilastirma ekrani yalnizca farkli dosyalar icin acildigindan
+ // icerikler sadece bu durumda tasinir.
+ entry.Status = WizardImportEntryStatusEnum.Conflict;
+ entry.IncomingContent = incomingText;
+ entry.ExistingContent = existingText;
+ }
+ }
+
+ result.Entries.Add(entry);
+ }
+ }
+
+ if (result.Entries.Count == 0)
+ {
+ DeleteImportSession(sessionPath);
+ throw new UserFriendlyException("Zip içinde import edilebilecek seed dosyası bulunamadı.");
+ }
+
+ result.Entries = [.. result.Entries.OrderBy(x => x.Path, StringComparer.OrdinalIgnoreCase)];
+ return result;
+ }
+
+ [Authorize(AppCodes.Listforms.Wizards.Import)]
+ [HttpPost("api/app/list-form-wizard/apply-import")]
+ public async Task ApplyImport(WizardImportApplyInput input)
+ {
+ Check.NotNull(input, nameof(input));
+
+ var sessionPath = GetExistingImportSessionPath(input.ImportId);
+ var relativePath = NormalizeImportEntryPath(input.Path)
+ ?? throw new UserFriendlyException("Geçersiz dosya yolu.");
+
+ var systemPath = relativePath.Replace('/', Path.DirectorySeparatorChar);
+ var stagedPath = Path.Combine(sessionPath, ImportStagedFolder, systemPath);
+ if (!File.Exists(stagedPath))
+ throw new UserFriendlyException($"Import oturumunda dosya bulunamadı: {relativePath}");
+
+ var scopeRoot = Path.GetFullPath(SeedPathResolver.GetScopePath(_configuration, CurrentTenant.Id));
+ var targetPath = Path.GetFullPath(Path.Combine(scopeRoot, systemPath));
+ if (!targetPath.StartsWith(scopeRoot + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase))
+ throw new UserFriendlyException("Geçersiz dosya yolu.");
+
+ // Icerik gonderildiyse kullanici diff ekraninda birlestirmis demektir; gonderilmediyse
+ // zip'ten gelen dosya oldugu gibi yazilir.
+ var content = input.Content == null
+ ? await File.ReadAllBytesAsync(stagedPath)
+ : ImportEncoding.GetBytes(input.Content);
+
+ var overwritten = File.Exists(targetPath);
+ if (overwritten)
+ {
+ var backupPath = Path.Combine(sessionPath, ImportBackupFolder, systemPath);
+ // Ayni dosya ikinci kez yazilirsa ilk yedek korunur; rollback en bastaki hale doner.
+ if (!File.Exists(backupPath))
+ {
+ Directory.CreateDirectory(Path.GetDirectoryName(backupPath)!);
+ File.Copy(targetPath, backupPath, overwrite: true);
+ }
+ }
+
+ // Kayit yazma isleminden once atilir; yazma sirasinda surec olse bile rollback dosyayi bulur.
+ await File.AppendAllTextAsync(
+ Path.Combine(sessionPath, ImportAppliedLogFile),
+ $"{(overwritten ? "E" : "N")}|{relativePath}{Environment.NewLine}");
+
+ Directory.CreateDirectory(Path.GetDirectoryName(targetPath)!);
+ await File.WriteAllBytesAsync(targetPath, content);
+
+ logger.LogInformation("Wizard import applied: {RelativePath} (overwritten: {Overwritten})", relativePath, overwritten);
+
+ return new WizardImportApplyResultDto
+ {
+ Path = relativePath,
+ Overwritten = overwritten,
+ Size = content.LongLength,
+ };
+ }
+
+ [Authorize(AppCodes.Listforms.Wizards.Import)]
+ [HttpPost("api/app/list-form-wizard/complete-import")]
+ public Task CompleteImport(string importId)
+ {
+ DeleteImportSession(GetExistingImportSessionPath(importId));
+ return Task.CompletedTask;
+ }
+
+ [Authorize(AppCodes.Listforms.Wizards.Import)]
+ [HttpPost("api/app/list-form-wizard/rollback-import")]
+ public async Task RollbackImport(string importId)
+ {
+ var sessionPath = GetExistingImportSessionPath(importId);
+ var logPath = Path.Combine(sessionPath, ImportAppliedLogFile);
+ var scopeRoot = Path.GetFullPath(SeedPathResolver.GetScopePath(_configuration, CurrentTenant.Id));
+
+ if (File.Exists(logPath))
+ {
+ var lines = await File.ReadAllLinesAsync(logPath);
+
+ // Yazma sirasinin tersine gidilir; ayni dosya birden fazla kez yazildiysa
+ // en eski yedek (ilk kayit) en sonda uygulanir.
+ foreach (var line in lines.Reverse())
+ {
+ var parts = line.Split('|', 2);
+ if (parts.Length != 2)
+ continue;
+
+ var systemPath = parts[1].Replace('/', Path.DirectorySeparatorChar);
+ var targetPath = Path.GetFullPath(Path.Combine(scopeRoot, systemPath));
+ if (!targetPath.StartsWith(scopeRoot + Path.DirectorySeparatorChar, StringComparison.OrdinalIgnoreCase))
+ continue;
+
+ try
+ {
+ if (parts[0] == "E")
+ {
+ var backupPath = Path.Combine(sessionPath, ImportBackupFolder, systemPath);
+ if (File.Exists(backupPath))
+ File.Copy(backupPath, targetPath, overwrite: true);
+ }
+ else if (File.Exists(targetPath))
+ {
+ File.Delete(targetPath);
+ }
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Wizard import rollback failed for {RelativePath}", parts[1]);
+ }
+ }
+ }
+
+ DeleteImportSession(sessionPath);
+ }
+
+ ///
+ /// Zip girdisini kapsam klasorune goreli yola cevirir. Zip, kapsam klasorunun icinden
+ /// (wizard/x.json) ya da depo kokunden (configs/seeds/host/wizard/x.json)
+ /// alinmis olabilir; her iki durumda da izin verilen ilk kok klasorden itibaren okunur.
+ /// Kural disi bir yol icin null doner.
+ ///
+ private static string? NormalizeImportEntryPath(string? entryPath)
+ {
+ var normalized = entryPath?.Replace('\\', '/').Trim('/');
+ if (string.IsNullOrWhiteSpace(normalized) || normalized.Contains("..", StringComparison.Ordinal))
+ return null;
+
+ var segments = normalized.Split('/', StringSplitOptions.RemoveEmptyEntries);
+ var start = Array.FindIndex(segments, s => ImportAllowedRootFolders.Contains(s, StringComparer.OrdinalIgnoreCase));
+ if (start < 0)
+ return null;
+
+ segments = segments[start..];
+
+ var isScriptFolder = segments[0].Equals(SeedPathResolver.SqlFolder, StringComparison.OrdinalIgnoreCase)
+ || segments[0].Equals(SeedPathResolver.PostgresFolder, StringComparison.OrdinalIgnoreCase);
+
+ // sql/postgres altinda object|execute klasoru zorunludur; digerlerinde alt klasor yoktur.
+ if (isScriptFolder)
+ {
+ if (segments.Length != 3 || !ImportAllowedScriptFolders.Contains(segments[1], StringComparer.OrdinalIgnoreCase))
+ return null;
+ }
+ else if (segments.Length != 2)
+ {
+ return null;
+ }
+
+ var fileName = segments[^1];
+ var extension = Path.GetExtension(fileName);
+ var expectedExtension = isScriptFolder ? ".sql" : ".json";
+
+ if (!extension.Equals(expectedExtension, StringComparison.OrdinalIgnoreCase))
+ return null;
+
+ if (fileName.Length == extension.Length || fileName.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0)
+ return null;
+
+ return string.Join('/', segments);
+ }
+
+ ///
+ /// Karsilastirmayi satir sonu ve BOM farklarindan bagimsiz hale getirir; ayni icerigin
+ /// farkli platformlarda uretilmis hali catisma olarak gorunmez.
+ ///
+ private static string NormalizeImportText(byte[] bytes)
+ {
+ return Encoding.UTF8.GetString(bytes)
+ .TrimStart('')
+ .Replace("\r\n", "\n")
+ .Replace("\r", "\n")
+ .TrimEnd();
+ }
+
+ private string GetImportSessionPath(string importId)
+ {
+ return Path.Combine(SeedPathResolver.GetPath(_configuration, ImportSessionsFolder), importId);
+ }
+
+ private string GetExistingImportSessionPath(string importId)
+ {
+ // Oturum kimligi klasor adina donustugu icin yalnizca uretilen format kabul edilir.
+ if (string.IsNullOrWhiteSpace(importId) || !Guid.TryParseExact(importId, "N", out _))
+ throw new UserFriendlyException("Geçersiz import oturumu.");
+
+ var sessionPath = GetImportSessionPath(importId);
+ if (!Directory.Exists(sessionPath))
+ throw new UserFriendlyException("Import oturumu bulunamadı veya süresi dolmuş.");
+
+ return sessionPath;
+ }
+
+ private void DeleteImportSession(string sessionPath)
+ {
+ try
+ {
+ if (Directory.Exists(sessionPath))
+ Directory.Delete(sessionPath, recursive: true);
+ }
+ catch (Exception ex)
+ {
+ logger.LogWarning(ex, "Wizard import session could not be deleted: {SessionPath}", sessionPath);
+ }
+ }
+
+ /// Yarim kalan (tarayici kapatilmis) oturumlarin klasorlerini temizler.
+ private void CleanupStaleImportSessions()
+ {
+ try
+ {
+ var root = SeedPathResolver.GetPath(_configuration, ImportSessionsFolder);
+ if (!Directory.Exists(root))
+ return;
+
+ foreach (var directory in Directory.GetDirectories(root))
+ {
+ if (Directory.GetLastWriteTimeUtc(directory) < DateTime.UtcNow.AddDays(-1))
+ Directory.Delete(directory, recursive: true);
+ }
+ }
+ catch (Exception ex)
+ {
+ logger.LogWarning(ex, "Stale wizard import sessions could not be cleaned up.");
+ }
+ }
+
///
/// Custom component seed dosyasini, bagimli oldugu component'leri ve bunlarin data source'larinin
/// isaret ettigi crud seed dosyalarini toplar. Bagimlilik zinciri dongu icerse bile
diff --git a/api/src/Sozsoft.Platform.DbMigrator/Migrations/LanguagesData.json b/api/src/Sozsoft.Platform.DbMigrator/Migrations/LanguagesData.json
index 0782cd25..634a2e66 100644
--- a/api/src/Sozsoft.Platform.DbMigrator/Migrations/LanguagesData.json
+++ b/api/src/Sozsoft.Platform.DbMigrator/Migrations/LanguagesData.json
@@ -1,4 +1,4 @@
-{
+{
"Languages": [
{
"cultureName": "ar",
@@ -26033,6 +26033,168 @@
"key": "App.PublicDemo.MailCompanyInfo",
"en": "Company Information",
"tr": "Firma Bilgileri"
+ },
+ {
+ "resourceName": "Platform",
+ "key": "App.ListForm.WizardImportButton",
+ "en": "Import",
+ "tr": "İçe Aktar"
+ },
+ {
+ "resourceName": "Platform",
+ "key": "App.ListForm.WizardImportTitle",
+ "en": "Import wizard package (zip)",
+ "tr": "Wizard paketini içe aktar (zip)"
+ },
+ {
+ "resourceName": "Platform",
+ "key": "App.ListForm.WizardImportAnalyzing",
+ "en": "Analyzing package...",
+ "tr": "Paket inceleniyor..."
+ },
+ {
+ "resourceName": "Platform",
+ "key": "App.ListForm.WizardImportNew",
+ "en": "new",
+ "tr": "yeni"
+ },
+ {
+ "resourceName": "Platform",
+ "key": "App.ListForm.WizardImportIdentical",
+ "en": "no change",
+ "tr": "değişiklik yok"
+ },
+ {
+ "resourceName": "Platform",
+ "key": "App.ListForm.WizardImportConflict",
+ "en": "conflict",
+ "tr": "çakışma"
+ },
+ {
+ "resourceName": "Platform",
+ "key": "App.ListForm.WizardImportResolvedApply",
+ "en": "will be imported",
+ "tr": "aktarılacak"
+ },
+ {
+ "resourceName": "Platform",
+ "key": "App.ListForm.WizardImportResolvedSkip",
+ "en": "skipped",
+ "tr": "atlanacak"
+ },
+ {
+ "resourceName": "Platform",
+ "key": "App.ListForm.WizardImportInclude",
+ "en": "Include in import",
+ "tr": "İçe aktarmaya dahil et"
+ },
+ {
+ "resourceName": "Platform",
+ "key": "App.ListForm.WizardImportShowIdentical",
+ "en": "identical files, show",
+ "tr": "aynı dosya var, göster"
+ },
+ {
+ "resourceName": "Platform",
+ "key": "App.ListForm.WizardImportHideIdentical",
+ "en": "Hide identical files",
+ "tr": "Aynı dosyaları gizle"
+ },
+ {
+ "resourceName": "Platform",
+ "key": "App.ListForm.WizardImportExistingSide",
+ "en": "Current file",
+ "tr": "Mevcut dosya"
+ },
+ {
+ "resourceName": "Platform",
+ "key": "App.ListForm.WizardImportIncomingSide",
+ "en": "Incoming file (editable)",
+ "tr": "Gelen dosya (düzenlenebilir)"
+ },
+ {
+ "resourceName": "Platform",
+ "key": "App.ListForm.WizardImportKeepExisting",
+ "en": "Keep current",
+ "tr": "Mevcut kalsın"
+ },
+ {
+ "resourceName": "Platform",
+ "key": "App.ListForm.WizardImportUseThis",
+ "en": "Import this content",
+ "tr": "Bu içeriği aktar"
+ },
+ {
+ "resourceName": "Platform",
+ "key": "App.ListForm.WizardImportResetIncoming",
+ "en": "Reset",
+ "tr": "Sıfırla"
+ },
+ {
+ "resourceName": "Platform",
+ "key": "App.ListForm.WizardImportUnresolved",
+ "en": "conflicts must be resolved before import.",
+ "tr": "çakışma import öncesinde çözülmeli."
+ },
+ {
+ "resourceName": "Platform",
+ "key": "App.ListForm.WizardImportReady",
+ "en": "All files are reviewed, import can be started.",
+ "tr": "Tüm dosyalar gözden geçirildi, import başlatılabilir."
+ },
+ {
+ "resourceName": "Platform",
+ "key": "App.ListForm.WizardImportWarnings",
+ "en": "Skipped entries",
+ "tr": "Atlanan kayıtlar"
+ },
+ {
+ "resourceName": "Platform",
+ "key": "App.ListForm.WizardImportStart",
+ "en": "Start import",
+ "tr": "İçe aktarmayı başlat"
+ },
+ {
+ "resourceName": "Platform",
+ "key": "App.ListForm.WizardImportWriting",
+ "en": "Writing",
+ "tr": "Yazılıyor"
+ },
+ {
+ "resourceName": "Platform",
+ "key": "App.ListForm.WizardImportCreated",
+ "en": "created",
+ "tr": "oluşturuldu"
+ },
+ {
+ "resourceName": "Platform",
+ "key": "App.ListForm.WizardImportOverwritten",
+ "en": "overwritten",
+ "tr": "üzerine yazıldı"
+ },
+ {
+ "resourceName": "Platform",
+ "key": "App.ListForm.WizardImportSuccess",
+ "en": "Import completed successfully.",
+ "tr": "İçe aktarma başarıyla tamamlandı."
+ },
+ {
+ "resourceName": "Platform",
+ "key": "App.ListForm.WizardImportRunMigrateHint",
+ "en": "Run DB Migrate to load the imported seed files into the database.",
+ "tr": "İçe aktarılan seed dosyalarının veritabanına işlenmesi için DB Migrate çalıştırın."
+ },
+ {
+ "resourceName": "Platform",
+ "key": "App.ListForm.WizardImportRolledBack",
+ "en": "Import failed, all written files were restored.",
+ "tr": "İçe aktarma başarısız oldu, yazılan tüm dosyalar geri alındı."
+ },
+ {
+ "resourceName": "Platform",
+ "key": "App.ListForm.WizardImportRollbackFailed",
+ "en": "Rollback could not be completed",
+ "tr": "Geri alma tamamlanamadı"
}
]
}
diff --git a/api/src/Sozsoft.Platform.DbMigrator/Migrations/PermissionsData.json b/api/src/Sozsoft.Platform.DbMigrator/Migrations/PermissionsData.json
index 2b23359d..58e234de 100644
--- a/api/src/Sozsoft.Platform.DbMigrator/Migrations/PermissionsData.json
+++ b/api/src/Sozsoft.Platform.DbMigrator/Migrations/PermissionsData.json
@@ -2480,6 +2480,33 @@
"MultiTenancySide": 2,
"MenuGroup": "Erp|Kurs"
},
+ {
+ "GroupName": "App.Saas",
+ "Name": "App.DeveloperKit.Components.Create",
+ "ParentName": "App.DeveloperKit.Components",
+ "DisplayName": "App.Platform.Create",
+ "IsEnabled": true,
+ "MultiTenancySide": 2,
+ "MenuGroup": "Erp|Kurs"
+ },
+ {
+ "GroupName": "App.Saas",
+ "Name": "App.DeveloperKit.Components.Update",
+ "ParentName": "App.DeveloperKit.Components",
+ "DisplayName": "App.Platform.Update",
+ "IsEnabled": true,
+ "MultiTenancySide": 2,
+ "MenuGroup": "Erp|Kurs"
+ },
+ {
+ "GroupName": "App.Saas",
+ "Name": "App.DeveloperKit.Components.Delete",
+ "ParentName": "App.DeveloperKit.Components",
+ "DisplayName": "App.Platform.Delete",
+ "IsEnabled": true,
+ "MultiTenancySide": 2,
+ "MenuGroup": "Erp|Kurs"
+ },
{
"GroupName": "App.Saas",
"Name": "App.DeveloperKit.DynamicServices",
@@ -2561,6 +2588,51 @@
"MultiTenancySide": 2,
"MenuGroup": "Erp|Kurs"
},
+ {
+ "GroupName": "App.Saas",
+ "Name": "App.Listforms.Wizard.Create",
+ "ParentName": "App.Listforms.Wizard",
+ "DisplayName": "App.Platform.Create",
+ "IsEnabled": true,
+ "MultiTenancySide": 2,
+ "MenuGroup": "Erp|Kurs"
+ },
+ {
+ "GroupName": "App.Saas",
+ "Name": "App.Listforms.Wizard.Update",
+ "ParentName": "App.Listforms.Wizard",
+ "DisplayName": "App.Platform.Update",
+ "IsEnabled": true,
+ "MultiTenancySide": 2,
+ "MenuGroup": "Erp|Kurs"
+ },
+ {
+ "GroupName": "App.Saas",
+ "Name": "App.Listforms.Wizard.Delete",
+ "ParentName": "App.Listforms.Wizard",
+ "DisplayName": "App.Platform.Delete",
+ "IsEnabled": true,
+ "MultiTenancySide": 2,
+ "MenuGroup": "Erp|Kurs"
+ },
+ {
+ "GroupName": "App.Saas",
+ "Name": "App.Listforms.Wizard.Export",
+ "ParentName": "App.Listforms.Wizard",
+ "DisplayName": "App.Platform.Export",
+ "IsEnabled": true,
+ "MultiTenancySide": 2,
+ "MenuGroup": "Erp|Kurs"
+ },
+ {
+ "GroupName": "App.Saas",
+ "Name": "App.Listforms.Wizard.Import",
+ "ParentName": "App.Listforms.Wizard",
+ "DisplayName": "App.Platform.Import",
+ "IsEnabled": true,
+ "MultiTenancySide": 2,
+ "MenuGroup": "Erp|Kurs"
+ },
{
"GroupName": "App.Administration",
"Name": "App.Setting",
diff --git a/api/src/Sozsoft.Platform.Domain.Shared/Enums/WizardImportEntryStatusEnum.cs b/api/src/Sozsoft.Platform.Domain.Shared/Enums/WizardImportEntryStatusEnum.cs
new file mode 100644
index 00000000..d3bfa0a6
--- /dev/null
+++ b/api/src/Sozsoft.Platform.Domain.Shared/Enums/WizardImportEntryStatusEnum.cs
@@ -0,0 +1,14 @@
+namespace Sozsoft.Platform.Enums;
+
+/// Import edilen zip girdisinin hedef kapsam klasorundeki dosyayla karsilastirma sonucu.
+public enum WizardImportEntryStatusEnum
+{
+ /// Hedefte ayni yolda dosya yok; dogrudan kopyalanir.
+ New = 0,
+
+ /// Hedefteki dosya ile icerik ayni; kullaniciya sorulmaz, islem yapilmaz.
+ Identical = 1,
+
+ /// Hedefte farkli icerikli bir dosya var; kullanici karsilastirip karar vermelidir.
+ Conflict = 2,
+}
diff --git a/api/src/Sozsoft.Platform.Domain.Shared/PlatformConsts.cs b/api/src/Sozsoft.Platform.Domain.Shared/PlatformConsts.cs
index cc39205f..5d39273a 100644
--- a/api/src/Sozsoft.Platform.Domain.Shared/PlatformConsts.cs
+++ b/api/src/Sozsoft.Platform.Domain.Shared/PlatformConsts.cs
@@ -442,6 +442,19 @@ public static class PlatformConsts
public const string Listform = Default + ".Listform";
public const string ListformField = Default + ".ListformField";
public const string Chart = Default + ".Chart";
+
+ ///
+ /// Wizard dosya yoneticisinin aksiyon yetkileri. Hepsi altindadir;
+ /// ekrani gorme (okuma) yetkisidir.
+ ///
+ public static class Wizards
+ {
+ public const string Create = Wizard + ".Create";
+ public const string Update = Wizard + ".Update";
+ public const string Delete = Wizard + ".Delete";
+ public const string Export = Wizard + ".Export";
+ public const string Import = Wizard + ".Import";
+ }
}
public static class Notifications
@@ -486,6 +499,19 @@ public static class PlatformConsts
public const string Put = CustomEndpoints + ".Put";
public const string Remove = CustomEndpoints + ".Remove";
+ ///
+ /// Custom component yoneticisinin yetkileri. ekrani gorme
+ /// (okuma) yetkisidir; aksiyonlar onun altindaki alt yetkileri ister.
+ ///
+ public static class Components
+ {
+ public const string Component = Default + ".Components";
+
+ public const string Create = Component + ".Create";
+ public const string Update = Component + ".Update";
+ public const string Delete = Component + ".Delete";
+ }
+
public static class DynamicServices
{
public const string DynamicService = Default + ".DynamicServices";
diff --git a/configs/seeds/host/wizard/20260818220914_RoleList.json b/configs/seeds/host/wizard/20260818220914_RoleList.json
new file mode 100644
index 00000000..287e5e44
--- /dev/null
+++ b/configs/seeds/host/wizard/20260818220914_RoleList.json
@@ -0,0 +1,148 @@
+{
+ "Wizard": {
+ "ComponentKind": 1,
+ "WizardName": "RoleList",
+ "ListFormCode": "App.Wizard.RoleList",
+ "MenuCode": "App.Wizard.RoleList",
+ "MenuOrder": 1,
+ "CustomComponentName": "RoleListComponent",
+ "MenuUrl": "/admin/roles-list",
+ "IsTenant": false,
+ "IsBranch": false,
+ "IsOrganizationUnit": false,
+ "AllowAdding": true,
+ "AllowUpdating": true,
+ "AllowDeleting": true,
+ "AllowDetail": false,
+ "ConfirmDelete": true,
+ "DefaultLayout": "grid",
+ "Grid": true,
+ "Card": true,
+ "Pivot": true,
+ "Tree": false,
+ "Chart": true,
+ "Gantt": false,
+ "Scheduler": false,
+ "Todo": false,
+ "LanguageTextMenuEn": "Role List",
+ "LanguageTextMenuTr": "Role List",
+ "LanguageTextTitleEn": "Role List",
+ "LanguageTextTitleTr": "Role List",
+ "LanguageTextDescEn": "Role List",
+ "LanguageTextDescTr": "Role List",
+ "LanguageTextMenuParentEn": "Sample",
+ "LanguageTextMenuParentTr": "Sample",
+ "PermissionGroupName": "App.Wizard.Sample",
+ "MenuParentCode": "App.Wizard.Sample",
+ "MenuParentShortName": "Sam",
+ "MenuParentIcon": "FcBookmark",
+ "MenuIcon": "FcBiohazard",
+ "DataSourceCode": "Default",
+ "DataSourceConnectionString": "",
+ "SelectCommandType": 1,
+ "SelectCommand": "",
+ "KeyFieldName": "",
+ "KeyFieldDbSourceType": 11,
+ "TreeOptionDto": {
+ "KeyExpr": "",
+ "ParentIdExpr": "",
+ "HasItemsExpr": "",
+ "RootValue": "",
+ "ExpandedRowKeys": [],
+ "AutoExpandAll": false,
+ "RecursiveSelection": false,
+ "TitleExpr": null,
+ "StartExpr": null,
+ "EndExpr": null,
+ "ProgressExpr": null
+ },
+ "GanttOptionDto": {
+ "KeyExpr": "",
+ "ParentIdExpr": "",
+ "HasItemsExpr": "",
+ "RootValue": "",
+ "ExpandedRowKeys": [],
+ "AutoExpandAll": false,
+ "RecursiveSelection": false,
+ "TitleExpr": "",
+ "ScaleType": "weeks",
+ "StartExpr": "",
+ "EndExpr": "",
+ "ProgressExpr": "",
+ "AllowEditing": false,
+ "AllowTaskAdding": false,
+ "AllowTaskUpdating": false,
+ "AllowTaskResourceUpdating": false,
+ "AllowTaskDeleting": false,
+ "AllowDependencyAdding": false,
+ "AllowDependencyDeleting": false,
+ "AllowResourceAdding": false,
+ "AllowResourceUpdating": false,
+ "AllowResourceDeleting": false
+ },
+ "SchedulerOptionDto": {
+ "TextExpr": "",
+ "UserNameExpr": null,
+ "DescriptionExpr": null,
+ "StartDateExpr": "",
+ "EndDateExpr": "",
+ "AllDayExpr": "",
+ "RecurrenceRuleExpr": "",
+ "RecurrenceExceptionExpr": "",
+ "StartDayHour": 8,
+ "EndDayHour": 20,
+ "DefaultView": "week",
+ "ShowAllDayPanel": true,
+ "CellDuration": 30,
+ "FirstDayOfWeek": 1,
+ "CrossScrollingEnabled": false,
+ "AllowResizing": false,
+ "AllowDragging": false,
+ "AllowDeleting": false,
+ "AllowEditing": false,
+ "AllowAdding": false
+ },
+ "TodoOptionDto": {
+ "TitleExpr": "",
+ "StatusExpr": "",
+ "DescriptionExpr": "",
+ "DueDateExpr": "",
+ "TagExpr": "",
+ "AssigneeExpr": "",
+ "PriorityExpr": "",
+ "CompletedExpr": "",
+ "OrderExpr": "",
+ "StatusOrder": "Backlog, Ready to Start, In Progress, Code Review, Testing, Staging",
+ "AllowDragging": true
+ },
+ "WorkflowDto": {
+ "ApprovalUserFieldName": "",
+ "ApprovalDateFieldName": "",
+ "ApprovalStatusFieldName": "",
+ "ApprovalDescriptionFieldName": "",
+ "ApprovalIsFilterUserName": false,
+ "ApprovalIsResetWorkflow": false,
+ "Criteria": []
+ },
+ "Groups": [],
+ "SubForms": [],
+ "Widgets": []
+ },
+ "IsDeletedField": false,
+ "IsCreatedField": false,
+ "InsertedRecords": {
+ "LanguageKeys": [
+ "App.Wizard.RoleList"
+ ],
+ "PermissionGroupNames": [
+ "App.Wizard.Sample"
+ ],
+ "PermissionNames": [
+ "App.Wizard.RoleList"
+ ],
+ "MenuCodes": [
+ "App.Wizard.RoleList"
+ ],
+ "DataSourceCodes": []
+ }
+}
\ No newline at end of file
diff --git a/ui/src/constants/permission.constant.ts b/ui/src/constants/permission.constant.ts
index a5acc93c..317232e1 100644
--- a/ui/src/constants/permission.constant.ts
+++ b/ui/src/constants/permission.constant.ts
@@ -15,3 +15,31 @@ export const PUBLIC_PAGE_DESIGN = {
SERVICES: 'App.Services.Design',
CONTACT: 'App.Contact.Design',
} as const
+
+/**
+ * Wizard dosya yöneticisi yetkileri. `PermissionsData.json` içinde tanımlıdır ve seeder ile
+ * yönetilir; buradaki kontroller yalnızca butonları gizler, asıl kontrol
+ * `ListFormWizardAppService` üzerindedir.
+ */
+export const WIZARD_PERMISSION = {
+ /** Ekranı görme (okuma) yetkisi; alt yetkilerin tamamının üstüdür. */
+ DEFAULT: 'App.Listforms.Wizard',
+ CREATE: 'App.Listforms.Wizard.Create',
+ UPDATE: 'App.Listforms.Wizard.Update',
+ DELETE: 'App.Listforms.Wizard.Delete',
+ EXPORT: 'App.Listforms.Wizard.Export',
+ IMPORT: 'App.Listforms.Wizard.Import',
+} as const
+
+/**
+ * Custom component yöneticisi yetkileri. `PermissionsData.json` içinde tanımlıdır ve seeder ile
+ * yönetilir; buradaki kontroller yalnızca butonları gizler, asıl kontrol
+ * `CustomComponentAppService` üzerindeki ABP policy adlarındadır.
+ */
+export const COMPONENT_PERMISSION = {
+ /** Ekranı görme (okuma) yetkisi; alt yetkilerin tamamının üstüdür. */
+ DEFAULT: 'App.DeveloperKit.Components',
+ CREATE: 'App.DeveloperKit.Components.Create',
+ UPDATE: 'App.DeveloperKit.Components.Update',
+ DELETE: 'App.DeveloperKit.Components.Delete',
+} as const
diff --git a/ui/src/proxy/admin/wizard/models.ts b/ui/src/proxy/admin/wizard/models.ts
index c2a76818..178246d6 100644
--- a/ui/src/proxy/admin/wizard/models.ts
+++ b/ui/src/proxy/admin/wizard/models.ts
@@ -36,6 +36,12 @@ export enum WizardComponentKindEnum {
export interface ListFormWizardDto {
componentKind: WizardComponentKindEnum
+ /**
+ * Düzenleme modunda, yerine geçilecek seed dosyasının adı. Doluysa sunucu önce bu dosyayı
+ * ve ürettiği kayıtları siler, sonra yenisini üretir; istek güncelleme yetkisiyle çalışır.
+ */
+ editFileName?: string
+
wizardName: string
listFormCode: string
menuCode: string
@@ -149,3 +155,38 @@ export interface WizardSeedFileDto {
isCreatedField: boolean
insertedRecords: WizardInsertedRecordsDto
}
+
+/** Import edilen dosyanin hedefteki karsiligiyla karsilastirma sonucu; backend enum'u ile birebir. */
+export enum WizardImportEntryStatusEnum {
+ New = 0,
+ Identical = 1,
+ Conflict = 2,
+}
+
+export interface WizardImportEntryDto {
+ /** Kapsam klasorune goreli yol, ornegin `custom/Orders.json`. */
+ path: string
+ folder: string
+ fileName: string
+ status: WizardImportEntryStatusEnum
+ incomingSize: number
+ existingSize: number
+ /** Yalnizca Conflict durumunda doludur. */
+ incomingContent?: string
+ /** Yalnizca Conflict durumunda doludur. */
+ existingContent?: string
+}
+
+export interface WizardImportAnalysisDto {
+ importId: string
+ /** Dosyalarin yazilacagi kapsam klasoru (`host` ya da `tenants/{tenantId}`). */
+ scopeFolder: string
+ entries: WizardImportEntryDto[]
+ warnings: string[]
+}
+
+export interface WizardImportApplyResultDto {
+ path: string
+ overwritten: boolean
+ size: number
+}
diff --git a/ui/src/services/wizard.service.ts b/ui/src/services/wizard.service.ts
index daac2eca..5a28157e 100644
--- a/ui/src/services/wizard.service.ts
+++ b/ui/src/services/wizard.service.ts
@@ -1,4 +1,10 @@
-import { ListFormWizardDto, WizardFileInfoDto, WizardSeedFileDto } from '@/proxy/admin/wizard/models'
+import {
+ ListFormWizardDto,
+ WizardFileInfoDto,
+ WizardImportAnalysisDto,
+ WizardImportApplyResultDto,
+ WizardSeedFileDto,
+} from '@/proxy/admin/wizard/models'
import apiService from './api.service'
export const postListFormWizard = (input: ListFormWizardDto) =>
@@ -51,4 +57,46 @@ export const downloadBlob = (blob: Blob, fileName: string) => {
link.click()
document.body.removeChild(link)
window.URL.revokeObjectURL(url)
-}
\ No newline at end of file
+}
+
+/**
+ * Zip'i sunucudaki gecici bir oturuma acar ve her dosyayi hedefteki karsiligiyla karsilastirir.
+ * Hicbir hedef dosyaya dokunmaz; donen `importId` uygulama adimlarinda kullanilir.
+ */
+export const analyzeWizardImport = (file: File) => {
+ const formData = new FormData()
+ formData.append('file', file)
+ formData.append('FileName', file.name)
+ formData.append('ContentType', file.type || 'application/zip')
+ formData.append('ContentLength', file.size.toString())
+
+ return apiService.fetchData({
+ method: 'POST',
+ url: `/api/app/list-form-wizard/analyze-import`,
+ data: formData as any,
+ })
+}
+
+/** Tek bir dosyayi hedefe yazar. `content` verilmezse zip'ten gelen icerik oldugu gibi yazilir. */
+export const applyWizardImport = (importId: string, path: string, content?: string) =>
+ apiService.fetchData({
+ method: 'POST',
+ url: `/api/app/list-form-wizard/apply-import`,
+ data: { importId, path, content: content ?? null },
+ })
+
+/** Import oturumunu kapatir; staging ve yedek klasorleri silinir. */
+export const completeWizardImport = (importId: string) =>
+ apiService.fetchData({
+ method: 'POST',
+ url: `/api/app/list-form-wizard/complete-import`,
+ params: { importId },
+ })
+
+/** O ana kadar yazilmis dosyalari geri alir ve oturumu kapatir. */
+export const rollbackWizardImport = (importId: string) =>
+ apiService.fetchData({
+ method: 'POST',
+ url: `/api/app/list-form-wizard/rollback-import`,
+ params: { importId },
+ })
diff --git a/ui/src/views/admin/listForm/wizard/Wizard.tsx b/ui/src/views/admin/listForm/wizard/Wizard.tsx
index 09b007e9..88d8354a 100644
--- a/ui/src/views/admin/listForm/wizard/Wizard.tsx
+++ b/ui/src/views/admin/listForm/wizard/Wizard.tsx
@@ -15,31 +15,27 @@ import { getDataSources } from '@/services/data-source.service'
import { sqlObjectManagerService } from '@/services/sql-query-manager.service'
import type { SqlObjectExplorerDto, DatabaseColumnDto } from '@/proxy/sql-query-manager/models'
import { MenuItem } from '@/proxy/menus/menu'
-import WizardStep1, {
+// Adım bileşenleri, sihirbazda göründükleri sırayla.
+import WizardStepMenu, {
MenuTreeNode,
buildMenuTree,
filterNonLinkNodes,
findRootCode,
-} from './WizardStep1'
-import WizardStep2 from './WizardStep2'
-import WizardStep3, { WizardGroup, WizardGroupItem } from './WizardStep3'
-import WizardStep7 from './WizardStep7'
-import WizardStep8 from './WizardStep8'
-import WizardStep9 from './WizardStep9'
-import WizardStep10 from './WizardStep10'
-import WizardStep11 from './WizardStep11'
-import WizardStep4 from './WizardStep4'
-import WizardStep5 from './WizardStep5'
-import WizardStep6 from './WizardStep6'
+} from './WizardStepMenu'
+import WizardStepDataSettings from './WizardStepDataSettings'
+import WizardStepFields, { WizardGroup, WizardGroupItem } from './WizardStepFields'
+import WizardStepSubForms from './WizardStepSubForms'
+import WizardStepWidgets from './WizardStepWidgets'
+import WizardStepWorkflow from './WizardStepWorkflow'
+import WizardStepTodoLayout from './WizardStepTodoLayout'
+import WizardStepTreeLayout from './WizardStepTreeLayout'
+import WizardStepGanttLayout from './WizardStepGanttLayout'
+import WizardStepSchedulerLayout from './WizardStepSchedulerLayout'
+import WizardStepDeploy from './WizardStepDeploy'
import { Container } from '@/components/shared'
import { sqlDataTypeToDbType } from '../edit/options'
import { useStoreActions } from '@/store/store'
-import {
- deleteWizardFile,
- getWizardFile,
- getWizardFiles,
- postListFormWizard,
-} from '@/services/wizard.service'
+import { getWizardFile, getWizardFiles, postListFormWizard } from '@/services/wizard.service'
import { ListFormWizardDto, WizardComponentKindEnum } from '@/proxy/admin/wizard/models'
import WizardStepComponent from './WizardStepComponent'
import WizardStepCustomComponent from './WizardStepCustomComponent'
@@ -451,8 +447,7 @@ const Wizard = () => {
formikRef.current?.setFieldValue('isBranch', colNames.has('branchid'))
// Anahtar alan: tabloda Id sutunu varsa her zaman odur; sutun sirasi anahtari
// belirlemez. Id yoksa son care olarak ilk sutuna dusulur.
- const keyColumn =
- cols.find((c) => c.columnName.toLowerCase() === 'id') ?? cols[0]
+ const keyColumn = cols.find((c) => c.columnName.toLowerCase() === 'id') ?? cols[0]
if (keyColumn) {
formikRef.current?.setFieldValue('keyFieldName', keyColumn.columnName)
formikRef.current?.setFieldValue(
@@ -949,16 +944,16 @@ const Wizard = () => {
const values = formik.values
const isCustom = values.componentKind === WizardComponentKindEnum.Custom
- try {
- // Edit modunda: önce eski dosyayı sil (DB kayıtlarını temizler)
- if (isEditMode && editFileName) {
- await deleteWizardFile(editFileName)
- }
+ // Edit modunda eski dosyanın silinmesini (ve DB kayıtlarının temizlenmesini) sunucu yapar;
+ // istek `editFileName` ile geldiğinde güncelleme yetkisiyle çalışır.
+ const editing = isEditMode && editFileName ? { editFileName } : {}
+ try {
// Custom yolunda ListForm'a ait hiçbir koleksiyon gönderilmez.
if (isCustom) {
await postListFormWizard({
...values,
+ ...editing,
groups: [],
subForms: [],
widgets: [],
@@ -979,6 +974,7 @@ const Wizard = () => {
await postListFormWizard({
...values,
+ ...editing,
groups: editingGroups.map((g) => ({
caption: g.caption,
colCount: g.colCount,
@@ -1173,7 +1169,7 @@ const Wizard = () => {
{/* ─── Step 3: Menu ───────────────────────────────────── */}
{resolvedStep === STEP.menu && (
- {
{/* ─── Step 4: Data Settings ───────────────────────────── */}
{resolvedStep === STEP.settings && (
- {
{/* ─── Step 5: List Form Fields ───────────────────────────── */}
{resolvedStep === STEP.fields && (
- {
{/* ─── Step 6: Sub Forms ───────────────────────────── */}
{resolvedStep === STEP.subForms && (
- {
{/* ─── Step 7: Widgets ───────────────────────────── */}
{resolvedStep === STEP.widgets && (
- {
{/* ─── Step 8: Workflow ───────────────────────────── */}
{resolvedStep === STEP.workflow && (
- {
{/* ─── Step 9: Todo / Kanban ────────────────────── */}
{resolvedStep === STEP.todo && values.todo && (
- {
{/* ─── Step 10: Tree ─────────────────────────────── */}
{resolvedStep === STEP.tree && values.tree && (
- goToAdjacentStep(-1)}
@@ -1314,7 +1310,7 @@ const Wizard = () => {
{/* ─── Step 11: Gantt ────────────────────────────── */}
{resolvedStep === STEP.gantt && values.gantt && (
- goToAdjacentStep(-1)}
@@ -1324,7 +1320,7 @@ const Wizard = () => {
{/* ─── Step 12: Scheduler ───────────────────────── */}
{resolvedStep === STEP.scheduler && values.scheduler && (
- goToAdjacentStep(-1)}
@@ -1334,7 +1330,7 @@ const Wizard = () => {
{/* ─── Step 13: Deploy ──────────────────────────── */}
{resolvedStep === STEP.deploy && (
- {
const [exportingFile, setExportingFile] = useState(null)
const [confirm, setConfirm] = useState(null)
const [search, setSearch] = useState('')
+ const [importFile, setImportFile] = useState(null)
+ const importInputRef = useRef(null)
+
+ // Butonlar yalnizca gizlenir; asil kontrol ListFormWizardAppService'te yapilir.
+ const { checkPermission } = usePermission()
+ const canCreate = checkPermission(WIZARD_PERMISSION.CREATE)
+ const canUpdate = checkPermission(WIZARD_PERMISSION.UPDATE)
+ const canDelete = checkPermission(WIZARD_PERMISSION.DELETE)
+ const canExport = checkPermission(WIZARD_PERMISSION.EXPORT)
+ const canImport = checkPermission(WIZARD_PERMISSION.IMPORT)
const filteredFiles = useMemo(() => {
const q = search.trim().toLowerCase()
@@ -104,6 +118,14 @@ const WizardFileManager = () => {
}
}
+ // Zip secildikten sonra tum akis WizardImportDialog icinde yurur; ayni dosya yeniden
+ // secilebilsin diye input degeri her seferinde sifirlanir.
+ const handleImportFileChange = (e: React.ChangeEvent) => {
+ const file = e.target.files?.[0]
+ e.target.value = ''
+ if (file) setImportFile(file)
+ }
+
const handleDeleteConfirm = async () => {
if (!confirm) return
setDeletingFile(confirm.fileName)
@@ -166,18 +188,44 @@ const WizardFileManager = () => {
>
+ {canImport && (
+ <>
+
+
+ >
+ )}
-
+ {canCreate && (
+
+ )}
@@ -280,44 +328,50 @@ const WizardFileManager = () => {
)}
-
-
-
+ {canExport && (
+
+ )}
+ {canUpdate && (
+
+ )}
+ {canDelete && (
+
+ )}
)
@@ -325,6 +379,14 @@ const WizardFileManager = () => {
+ {importFile && (
+ setImportFile(null)}
+ onImported={loadFiles}
+ />
+ )}
+
{/* Delete Confirm Dialog */}
- {!searchTerm && filterActive === 'all' && (
+ {canCreate && !searchTerm && filterActive === 'all' && (