WizardManager için Import özelliği eklendi
This commit is contained in:
parent
010594ac27
commit
eb77285e59
32 changed files with 2065 additions and 190 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -8,3 +8,5 @@ logs/
|
||||||
# ve API konteynerine mount edilir. Takip edilirse deploy'daki `git pull` yerel
|
# ve API konteynerine mount edilir. Takip edilirse deploy'daki `git pull` yerel
|
||||||
# değişiklikle çakışır ve dosya eskir.
|
# değişiklikle çakışır ve dosya eskir.
|
||||||
configs/change-logs/change-logs.json
|
configs/change-logs/change-logs.json
|
||||||
|
# Wizard import staging klasoru (gecici; oturum bitince silinir)
|
||||||
|
configs/seeds/.imports/
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Volo.Abp.Content;
|
using Volo.Abp.Content;
|
||||||
|
|
||||||
|
|
@ -13,5 +13,19 @@ public interface IListFormWizardAppService
|
||||||
|
|
||||||
/// <summary>Wizard dosyasini ve bagli seed dosyalarini zip olarak dondurur.</summary>
|
/// <summary>Wizard dosyasini ve bagli seed dosyalarini zip olarak dondurur.</summary>
|
||||||
Task<IRemoteStreamContent> ExportFile(string fileName);
|
Task<IRemoteStreamContent> ExportFile(string fileName);
|
||||||
}
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Export zip'ini staging klasorune acar ve her dosya icin hedefteki karsiligiyla
|
||||||
|
/// karsilastirma sonucunu dondurur. Bu adim hicbir hedef dosyaya dokunmaz.
|
||||||
|
/// </summary>
|
||||||
|
Task<WizardImportAnalysisDto> AnalyzeImport(IRemoteStreamContent file);
|
||||||
|
|
||||||
|
/// <summary>Analizde donen tek bir dosyayi hedefe yazar; onceki icerik rollback icin yedeklenir.</summary>
|
||||||
|
Task<WizardImportApplyResultDto> ApplyImport(WizardImportApplyInput input);
|
||||||
|
|
||||||
|
/// <summary>Import oturumunu basariyla kapatir ve staging/yedek klasorunu siler.</summary>
|
||||||
|
Task CompleteImport(string importId);
|
||||||
|
|
||||||
|
/// <summary>O ana kadar yazilan dosyalari geri alir ve staging klasorunu siler.</summary>
|
||||||
|
Task RollbackImport(string importId);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
using Sozsoft.Platform.Enums;
|
using Sozsoft.Platform.Enums;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Data;
|
using System.Data;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace Sozsoft.Platform.ListForms;
|
namespace Sozsoft.Platform.ListForms;
|
||||||
|
|
||||||
|
|
@ -13,6 +14,14 @@ public class ListFormWizardDto
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public WizardComponentKindEnum ComponentKind { get; set; } = WizardComponentKindEnum.List;
|
public WizardComponentKindEnum ComponentKind { get; set; } = WizardComponentKindEnum.List;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)]
|
||||||
|
public string EditFileName { get; set; }
|
||||||
|
|
||||||
public string WizardName { get; set; }
|
public string WizardName { get; set; }
|
||||||
public string ListFormCode { get; set; }
|
public string ListFormCode { get; set; }
|
||||||
public string MenuCode { get; set; }
|
public string MenuCode { get; set; }
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Sozsoft.Platform.Enums;
|
||||||
|
|
||||||
|
namespace Sozsoft.Platform.ListForms;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Import edilen zip icindeki tek bir seed dosyasinin analiz sonucu.
|
||||||
|
/// Yol her zaman kapsam klasorune (host / tenants/{tenantId}) goreli tutulur.
|
||||||
|
/// </summary>
|
||||||
|
public class WizardImportEntryDto
|
||||||
|
{
|
||||||
|
/// <summary>Kapsam klasorune goreli yol, ornegin <c>custom/Orders.json</c>.</summary>
|
||||||
|
public string Path { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Yolun ilk segmenti; listede gruplama icin kullanilir (wizard, crud, custom, sql, postgres).</summary>
|
||||||
|
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; }
|
||||||
|
|
||||||
|
/// <summary>Zip'ten gelen icerik; yalnizca Conflict durumunda doldurulur.</summary>
|
||||||
|
public string IncomingContent { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Hedefteki mevcut icerik; yalnizca Conflict durumunda doldurulur.</summary>
|
||||||
|
public string ExistingContent { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Zip analizinin tamami; uygulama adimi bu <see cref="ImportId"/> ile yurutulur.</summary>
|
||||||
|
public class WizardImportAnalysisDto
|
||||||
|
{
|
||||||
|
/// <summary>Staging klasorunu ve rollback kaydini tanimlayan oturum kimligi.</summary>
|
||||||
|
public string ImportId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Dosyalarin yazilacagi kapsam klasoru (<c>host</c> ya da <c>tenants/{tenantId}</c>).</summary>
|
||||||
|
public string ScopeFolder { get; set; }
|
||||||
|
|
||||||
|
public List<WizardImportEntryDto> Entries { get; set; } = [];
|
||||||
|
|
||||||
|
/// <summary>Guvenlik/format nedeniyle atlanan zip girdileri icin aciklamalar.</summary>
|
||||||
|
public List<string> Warnings { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Tek bir dosyanin hedefe yazilmasi istegi.</summary>
|
||||||
|
public class WizardImportApplyInput
|
||||||
|
{
|
||||||
|
public string ImportId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Analizde donen kapsam-goreli yol.</summary>
|
||||||
|
public string Path { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Kullanicinin diff ekraninda birlestirdigi icerik. Bos birakilirsa zip'ten gelen
|
||||||
|
/// icerik oldugu gibi yazilir.
|
||||||
|
/// </summary>
|
||||||
|
public string Content { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Tek dosya yazma sonucu; ilerleme gostergesinde satir olarak gosterilir.</summary>
|
||||||
|
public class WizardImportApplyResultDto
|
||||||
|
{
|
||||||
|
public string Path { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Dosya hedefte zaten varken uzerine yazildiysa true.</summary>
|
||||||
|
public bool Overwritten { get; set; }
|
||||||
|
|
||||||
|
public long Size { get; set; }
|
||||||
|
}
|
||||||
|
|
@ -11,12 +11,20 @@ using Microsoft.Extensions.Logging;
|
||||||
using Sozsoft.Platform.Data.Seeds;
|
using Sozsoft.Platform.Data.Seeds;
|
||||||
using Sozsoft.Platform.DeveloperKit;
|
using Sozsoft.Platform.DeveloperKit;
|
||||||
using Sozsoft.Platform.Entities;
|
using Sozsoft.Platform.Entities;
|
||||||
|
using static Sozsoft.Platform.PlatformConsts;
|
||||||
using Volo.Abp.Application.Dtos;
|
using Volo.Abp.Application.Dtos;
|
||||||
using Volo.Abp.Application.Services;
|
using Volo.Abp.Application.Services;
|
||||||
using Volo.Abp.Domain.Repositories;
|
using Volo.Abp.Domain.Repositories;
|
||||||
|
|
||||||
namespace Platform.Api.Application;
|
namespace Platform.Api.Application;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Custom component yonetimi. Sinif seviyesindeki <see cref="AuthorizeAttribute"/> yalnizca
|
||||||
|
/// kimlik dogrulamasi ister; ekrani gorme ve CRUD haklari asagida ABP policy adlarina baglanir.
|
||||||
|
/// <c>App.DeveloperKit.Components</c> okuma, <c>.Create</c>/<c>.Update</c>/<c>.Delete</c> aksiyon
|
||||||
|
/// yetkileridir. <see cref="GetActiveComponentsAsync"/> calisma zamani okuma yoludur ve yalnizca
|
||||||
|
/// oturum acmis olmayi ister.
|
||||||
|
/// </summary>
|
||||||
[Authorize]
|
[Authorize]
|
||||||
public class CustomComponentAppService : CrudAppService<
|
public class CustomComponentAppService : CrudAppService<
|
||||||
CustomComponent,
|
CustomComponent,
|
||||||
|
|
@ -47,6 +55,12 @@ public class CustomComponentAppService : CrudAppService<
|
||||||
{
|
{
|
||||||
_crudEndpointRepository = crudEndpointRepository;
|
_crudEndpointRepository = crudEndpointRepository;
|
||||||
_configuration = configuration;
|
_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<CustomComponentDto> CreateAsync(CreateUpdateCustomComponentDto input)
|
public override async Task<CustomComponentDto> CreateAsync(CreateUpdateCustomComponentDto input)
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.IO.Compression;
|
using System.IO.Compression;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
@ -31,6 +32,10 @@ namespace Sozsoft.Platform.ListForms;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Wizard, global menu/permission/language kayitlari ve seed dosyalari uretir.
|
/// Wizard, global menu/permission/language kayitlari ve seed dosyalari uretir.
|
||||||
/// Bu nedenle sadece host tarafinda tanimli olan <c>App.Listforms.Wizard</c> yetkisi ile calisir.
|
/// Bu nedenle sadece host tarafinda tanimli olan <c>App.Listforms.Wizard</c> yetkisi ile calisir.
|
||||||
|
/// <para>
|
||||||
|
/// Sinif seviyesindeki yetki ekrani gorme (okuma) hakkidir; aksiyonlar ayrica kendi alt
|
||||||
|
/// yetkilerini ister: <c>.Create</c>, <c>.Update</c>, <c>.Delete</c>, <c>.Export</c>, <c>.Import</c>.
|
||||||
|
/// </para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Authorize(AppCodes.Listforms.Wizard)]
|
[Authorize(AppCodes.Listforms.Wizard)]
|
||||||
public class ListFormWizardAppService(
|
public class ListFormWizardAppService(
|
||||||
|
|
@ -76,6 +81,19 @@ public class ListFormWizardAppService(
|
||||||
{
|
{
|
||||||
Check.NotNull(input, nameof(input));
|
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();
|
var wizardName = input.WizardName?.Trim();
|
||||||
if (string.IsNullOrWhiteSpace(wizardName))
|
if (string.IsNullOrWhiteSpace(wizardName))
|
||||||
throw new UserFriendlyException("Wizard adı zorunludur.");
|
throw new UserFriendlyException("Wizard adı zorunludur.");
|
||||||
|
|
@ -583,6 +601,9 @@ public class ListFormWizardAppService(
|
||||||
var outputPath = ResolveWizardSeedOutputPath();
|
var outputPath = ResolveWizardSeedOutputPath();
|
||||||
Directory.CreateDirectory(outputPath);
|
Directory.CreateDirectory(outputPath);
|
||||||
|
|
||||||
|
// Duzenleme bilgisi yalnizca istege aittir; seed dosyasinda yeri yoktur.
|
||||||
|
input.EditFileName = null;
|
||||||
|
|
||||||
var seedData = new WizardSeedFileDto
|
var seedData = new WizardSeedFileDto
|
||||||
{
|
{
|
||||||
Wizard = input,
|
Wizard = input,
|
||||||
|
|
@ -694,7 +715,17 @@ public class ListFormWizardAppService(
|
||||||
}
|
}
|
||||||
|
|
||||||
[UnitOfWork]
|
[UnitOfWork]
|
||||||
public async Task DeleteFile(string fileName)
|
[Authorize(AppCodes.Listforms.Wizards.Delete)]
|
||||||
|
public Task DeleteFile(string fileName)
|
||||||
|
{
|
||||||
|
return DeleteFileInternalAsync(fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Seed dosyasini ve wizard'in urettigi kayitlari siler. Yetki kontrolu cagiran uca aittir:
|
||||||
|
/// silme yolunda <c>Delete</c>, duzenleme yolunda <c>Update</c> politikasi kullanilir.
|
||||||
|
/// </summary>
|
||||||
|
private async Task DeleteFileInternalAsync(string fileName)
|
||||||
{
|
{
|
||||||
var filePath = ResolveWizardSeedFilePath(fileName);
|
var filePath = ResolveWizardSeedFilePath(fileName);
|
||||||
|
|
||||||
|
|
@ -906,6 +937,7 @@ public class ListFormWizardAppService(
|
||||||
/// <c>{sql|postgres}/{object|execute}/{nesne}.sql</c> ve varsa <c>crud/{nesne}.json</c>.
|
/// <c>{sql|postgres}/{object|execute}/{nesne}.sql</c> ve varsa <c>crud/{nesne}.json</c>.
|
||||||
/// </para>
|
/// </para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[Authorize(AppCodes.Listforms.Wizards.Export)]
|
||||||
[HttpGet("api/app/list-form-wizard/export")]
|
[HttpGet("api/app/list-form-wizard/export")]
|
||||||
public async Task<IRemoteStreamContent> ExportFile(string fileName)
|
public async Task<IRemoteStreamContent> ExportFile(string fileName)
|
||||||
{
|
{
|
||||||
|
|
@ -951,6 +983,404 @@ public class ListFormWizardAppService(
|
||||||
return new RemoteStreamContent(memory, $"{safeName}.zip", "application/zip");
|
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.
|
||||||
|
|
||||||
|
/// <summary>Staging oturumlarinin tutuldugu klasor; seed kokunun altinda nokta ile baslar, seeder taramalarina girmez.</summary>
|
||||||
|
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;
|
||||||
|
|
||||||
|
/// <summary>Kapsam klasorunde import edilmesine izin verilen kok klasorler.</summary>
|
||||||
|
private static readonly string[] ImportAllowedRootFolders =
|
||||||
|
[
|
||||||
|
SeedPathResolver.WizardFolder,
|
||||||
|
SeedPathResolver.CrudFolder,
|
||||||
|
SeedPathResolver.CustomFolder,
|
||||||
|
SeedPathResolver.SqlFolder,
|
||||||
|
SeedPathResolver.PostgresFolder,
|
||||||
|
];
|
||||||
|
|
||||||
|
/// <summary>SQL saglayici klasorlerinin altinda izin verilen script klasorleri.</summary>
|
||||||
|
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<WizardImportAnalysisDto> 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<string>(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<WizardImportApplyResultDto> 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Zip girdisini kapsam klasorune goreli yola cevirir. Zip, kapsam klasorunun icinden
|
||||||
|
/// (<c>wizard/x.json</c>) ya da depo kokunden (<c>configs/seeds/host/wizard/x.json</c>)
|
||||||
|
/// alinmis olabilir; her iki durumda da izin verilen ilk kok klasorden itibaren okunur.
|
||||||
|
/// Kural disi bir yol icin null doner.
|
||||||
|
/// </summary>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Karsilastirmayi satir sonu ve BOM farklarindan bagimsiz hale getirir; ayni icerigin
|
||||||
|
/// farkli platformlarda uretilmis hali catisma olarak gorunmez.
|
||||||
|
/// </summary>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Yarim kalan (tarayici kapatilmis) oturumlarin klasorlerini temizler.</summary>
|
||||||
|
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.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Custom component seed dosyasini, bagimli oldugu component'leri ve bunlarin data source'larinin
|
/// Custom component seed dosyasini, bagimli oldugu component'leri ve bunlarin data source'larinin
|
||||||
/// isaret ettigi crud seed dosyalarini toplar. Bagimlilik zinciri dongu icerse bile
|
/// isaret ettigi crud seed dosyalarini toplar. Bagimlilik zinciri dongu icerse bile
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
{
|
{
|
||||||
"Languages": [
|
"Languages": [
|
||||||
{
|
{
|
||||||
"cultureName": "ar",
|
"cultureName": "ar",
|
||||||
|
|
@ -26033,6 +26033,168 @@
|
||||||
"key": "App.PublicDemo.MailCompanyInfo",
|
"key": "App.PublicDemo.MailCompanyInfo",
|
||||||
"en": "Company Information",
|
"en": "Company Information",
|
||||||
"tr": "Firma Bilgileri"
|
"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ı"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2480,6 +2480,33 @@
|
||||||
"MultiTenancySide": 2,
|
"MultiTenancySide": 2,
|
||||||
"MenuGroup": "Erp|Kurs"
|
"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",
|
"GroupName": "App.Saas",
|
||||||
"Name": "App.DeveloperKit.DynamicServices",
|
"Name": "App.DeveloperKit.DynamicServices",
|
||||||
|
|
@ -2561,6 +2588,51 @@
|
||||||
"MultiTenancySide": 2,
|
"MultiTenancySide": 2,
|
||||||
"MenuGroup": "Erp|Kurs"
|
"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",
|
"GroupName": "App.Administration",
|
||||||
"Name": "App.Setting",
|
"Name": "App.Setting",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
namespace Sozsoft.Platform.Enums;
|
||||||
|
|
||||||
|
/// <summary>Import edilen zip girdisinin hedef kapsam klasorundeki dosyayla karsilastirma sonucu.</summary>
|
||||||
|
public enum WizardImportEntryStatusEnum
|
||||||
|
{
|
||||||
|
/// <summary>Hedefte ayni yolda dosya yok; dogrudan kopyalanir.</summary>
|
||||||
|
New = 0,
|
||||||
|
|
||||||
|
/// <summary>Hedefteki dosya ile icerik ayni; kullaniciya sorulmaz, islem yapilmaz.</summary>
|
||||||
|
Identical = 1,
|
||||||
|
|
||||||
|
/// <summary>Hedefte farkli icerikli bir dosya var; kullanici karsilastirip karar vermelidir.</summary>
|
||||||
|
Conflict = 2,
|
||||||
|
}
|
||||||
|
|
@ -442,6 +442,19 @@ public static class PlatformConsts
|
||||||
public const string Listform = Default + ".Listform";
|
public const string Listform = Default + ".Listform";
|
||||||
public const string ListformField = Default + ".ListformField";
|
public const string ListformField = Default + ".ListformField";
|
||||||
public const string Chart = Default + ".Chart";
|
public const string Chart = Default + ".Chart";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Wizard dosya yoneticisinin aksiyon yetkileri. Hepsi <see cref="Wizard"/> altindadir;
|
||||||
|
/// <see cref="Wizard"/> ekrani gorme (okuma) yetkisidir.
|
||||||
|
/// </summary>
|
||||||
|
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
|
public static class Notifications
|
||||||
|
|
@ -486,6 +499,19 @@ public static class PlatformConsts
|
||||||
public const string Put = CustomEndpoints + ".Put";
|
public const string Put = CustomEndpoints + ".Put";
|
||||||
public const string Remove = CustomEndpoints + ".Remove";
|
public const string Remove = CustomEndpoints + ".Remove";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Custom component yoneticisinin yetkileri. <see cref="Component"/> ekrani gorme
|
||||||
|
/// (okuma) yetkisidir; aksiyonlar onun altindaki alt yetkileri ister.
|
||||||
|
/// </summary>
|
||||||
|
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 static class DynamicServices
|
||||||
{
|
{
|
||||||
public const string DynamicService = Default + ".DynamicServices";
|
public const string DynamicService = Default + ".DynamicServices";
|
||||||
|
|
|
||||||
148
configs/seeds/host/wizard/20260818220914_RoleList.json
Normal file
148
configs/seeds/host/wizard/20260818220914_RoleList.json
Normal file
|
|
@ -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": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -15,3 +15,31 @@ export const PUBLIC_PAGE_DESIGN = {
|
||||||
SERVICES: 'App.Services.Design',
|
SERVICES: 'App.Services.Design',
|
||||||
CONTACT: 'App.Contact.Design',
|
CONTACT: 'App.Contact.Design',
|
||||||
} as const
|
} 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
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,12 @@ export enum WizardComponentKindEnum {
|
||||||
export interface ListFormWizardDto {
|
export interface ListFormWizardDto {
|
||||||
componentKind: WizardComponentKindEnum
|
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
|
wizardName: string
|
||||||
listFormCode: string
|
listFormCode: string
|
||||||
menuCode: string
|
menuCode: string
|
||||||
|
|
@ -149,3 +155,38 @@ export interface WizardSeedFileDto {
|
||||||
isCreatedField: boolean
|
isCreatedField: boolean
|
||||||
insertedRecords: WizardInsertedRecordsDto
|
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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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'
|
import apiService from './api.service'
|
||||||
|
|
||||||
export const postListFormWizard = (input: ListFormWizardDto) =>
|
export const postListFormWizard = (input: ListFormWizardDto) =>
|
||||||
|
|
@ -52,3 +58,45 @@ export const downloadBlob = (blob: Blob, fileName: string) => {
|
||||||
document.body.removeChild(link)
|
document.body.removeChild(link)
|
||||||
window.URL.revokeObjectURL(url)
|
window.URL.revokeObjectURL(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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<WizardImportAnalysisDto>({
|
||||||
|
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<WizardImportApplyResultDto>({
|
||||||
|
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 },
|
||||||
|
})
|
||||||
|
|
|
||||||
|
|
@ -15,31 +15,27 @@ import { getDataSources } from '@/services/data-source.service'
|
||||||
import { sqlObjectManagerService } from '@/services/sql-query-manager.service'
|
import { sqlObjectManagerService } from '@/services/sql-query-manager.service'
|
||||||
import type { SqlObjectExplorerDto, DatabaseColumnDto } from '@/proxy/sql-query-manager/models'
|
import type { SqlObjectExplorerDto, DatabaseColumnDto } from '@/proxy/sql-query-manager/models'
|
||||||
import { MenuItem } from '@/proxy/menus/menu'
|
import { MenuItem } from '@/proxy/menus/menu'
|
||||||
import WizardStep1, {
|
// Adım bileşenleri, sihirbazda göründükleri sırayla.
|
||||||
|
import WizardStepMenu, {
|
||||||
MenuTreeNode,
|
MenuTreeNode,
|
||||||
buildMenuTree,
|
buildMenuTree,
|
||||||
filterNonLinkNodes,
|
filterNonLinkNodes,
|
||||||
findRootCode,
|
findRootCode,
|
||||||
} from './WizardStep1'
|
} from './WizardStepMenu'
|
||||||
import WizardStep2 from './WizardStep2'
|
import WizardStepDataSettings from './WizardStepDataSettings'
|
||||||
import WizardStep3, { WizardGroup, WizardGroupItem } from './WizardStep3'
|
import WizardStepFields, { WizardGroup, WizardGroupItem } from './WizardStepFields'
|
||||||
import WizardStep7 from './WizardStep7'
|
import WizardStepSubForms from './WizardStepSubForms'
|
||||||
import WizardStep8 from './WizardStep8'
|
import WizardStepWidgets from './WizardStepWidgets'
|
||||||
import WizardStep9 from './WizardStep9'
|
import WizardStepWorkflow from './WizardStepWorkflow'
|
||||||
import WizardStep10 from './WizardStep10'
|
import WizardStepTodoLayout from './WizardStepTodoLayout'
|
||||||
import WizardStep11 from './WizardStep11'
|
import WizardStepTreeLayout from './WizardStepTreeLayout'
|
||||||
import WizardStep4 from './WizardStep4'
|
import WizardStepGanttLayout from './WizardStepGanttLayout'
|
||||||
import WizardStep5 from './WizardStep5'
|
import WizardStepSchedulerLayout from './WizardStepSchedulerLayout'
|
||||||
import WizardStep6 from './WizardStep6'
|
import WizardStepDeploy from './WizardStepDeploy'
|
||||||
import { Container } from '@/components/shared'
|
import { Container } from '@/components/shared'
|
||||||
import { sqlDataTypeToDbType } from '../edit/options'
|
import { sqlDataTypeToDbType } from '../edit/options'
|
||||||
import { useStoreActions } from '@/store/store'
|
import { useStoreActions } from '@/store/store'
|
||||||
import {
|
import { getWizardFile, getWizardFiles, postListFormWizard } from '@/services/wizard.service'
|
||||||
deleteWizardFile,
|
|
||||||
getWizardFile,
|
|
||||||
getWizardFiles,
|
|
||||||
postListFormWizard,
|
|
||||||
} from '@/services/wizard.service'
|
|
||||||
import { ListFormWizardDto, WizardComponentKindEnum } from '@/proxy/admin/wizard/models'
|
import { ListFormWizardDto, WizardComponentKindEnum } from '@/proxy/admin/wizard/models'
|
||||||
import WizardStepComponent from './WizardStepComponent'
|
import WizardStepComponent from './WizardStepComponent'
|
||||||
import WizardStepCustomComponent from './WizardStepCustomComponent'
|
import WizardStepCustomComponent from './WizardStepCustomComponent'
|
||||||
|
|
@ -451,8 +447,7 @@ const Wizard = () => {
|
||||||
formikRef.current?.setFieldValue('isBranch', colNames.has('branchid'))
|
formikRef.current?.setFieldValue('isBranch', colNames.has('branchid'))
|
||||||
// Anahtar alan: tabloda Id sutunu varsa her zaman odur; sutun sirasi anahtari
|
// Anahtar alan: tabloda Id sutunu varsa her zaman odur; sutun sirasi anahtari
|
||||||
// belirlemez. Id yoksa son care olarak ilk sutuna dusulur.
|
// belirlemez. Id yoksa son care olarak ilk sutuna dusulur.
|
||||||
const keyColumn =
|
const keyColumn = cols.find((c) => c.columnName.toLowerCase() === 'id') ?? cols[0]
|
||||||
cols.find((c) => c.columnName.toLowerCase() === 'id') ?? cols[0]
|
|
||||||
if (keyColumn) {
|
if (keyColumn) {
|
||||||
formikRef.current?.setFieldValue('keyFieldName', keyColumn.columnName)
|
formikRef.current?.setFieldValue('keyFieldName', keyColumn.columnName)
|
||||||
formikRef.current?.setFieldValue(
|
formikRef.current?.setFieldValue(
|
||||||
|
|
@ -949,16 +944,16 @@ const Wizard = () => {
|
||||||
const values = formik.values
|
const values = formik.values
|
||||||
const isCustom = values.componentKind === WizardComponentKindEnum.Custom
|
const isCustom = values.componentKind === WizardComponentKindEnum.Custom
|
||||||
|
|
||||||
try {
|
// Edit modunda eski dosyanın silinmesini (ve DB kayıtlarının temizlenmesini) sunucu yapar;
|
||||||
// Edit modunda: önce eski dosyayı sil (DB kayıtlarını temizler)
|
// istek `editFileName` ile geldiğinde güncelleme yetkisiyle çalışır.
|
||||||
if (isEditMode && editFileName) {
|
const editing = isEditMode && editFileName ? { editFileName } : {}
|
||||||
await deleteWizardFile(editFileName)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
try {
|
||||||
// Custom yolunda ListForm'a ait hiçbir koleksiyon gönderilmez.
|
// Custom yolunda ListForm'a ait hiçbir koleksiyon gönderilmez.
|
||||||
if (isCustom) {
|
if (isCustom) {
|
||||||
await postListFormWizard({
|
await postListFormWizard({
|
||||||
...values,
|
...values,
|
||||||
|
...editing,
|
||||||
groups: [],
|
groups: [],
|
||||||
subForms: [],
|
subForms: [],
|
||||||
widgets: [],
|
widgets: [],
|
||||||
|
|
@ -979,6 +974,7 @@ const Wizard = () => {
|
||||||
|
|
||||||
await postListFormWizard({
|
await postListFormWizard({
|
||||||
...values,
|
...values,
|
||||||
|
...editing,
|
||||||
groups: editingGroups.map((g) => ({
|
groups: editingGroups.map((g) => ({
|
||||||
caption: g.caption,
|
caption: g.caption,
|
||||||
colCount: g.colCount,
|
colCount: g.colCount,
|
||||||
|
|
@ -1173,7 +1169,7 @@ const Wizard = () => {
|
||||||
|
|
||||||
{/* ─── Step 3: Menu ───────────────────────────────────── */}
|
{/* ─── Step 3: Menu ───────────────────────────────────── */}
|
||||||
{resolvedStep === STEP.menu && (
|
{resolvedStep === STEP.menu && (
|
||||||
<WizardStep1
|
<WizardStepMenu
|
||||||
values={values}
|
values={values}
|
||||||
errors={errors}
|
errors={errors}
|
||||||
touched={touched}
|
touched={touched}
|
||||||
|
|
@ -1209,7 +1205,7 @@ const Wizard = () => {
|
||||||
|
|
||||||
{/* ─── Step 4: Data Settings ───────────────────────────── */}
|
{/* ─── Step 4: Data Settings ───────────────────────────── */}
|
||||||
{resolvedStep === STEP.settings && (
|
{resolvedStep === STEP.settings && (
|
||||||
<WizardStep2
|
<WizardStepDataSettings
|
||||||
values={values}
|
values={values}
|
||||||
errors={errors}
|
errors={errors}
|
||||||
touched={touched}
|
touched={touched}
|
||||||
|
|
@ -1240,7 +1236,7 @@ const Wizard = () => {
|
||||||
|
|
||||||
{/* ─── Step 5: List Form Fields ───────────────────────────── */}
|
{/* ─── Step 5: List Form Fields ───────────────────────────── */}
|
||||||
{resolvedStep === STEP.fields && (
|
{resolvedStep === STEP.fields && (
|
||||||
<WizardStep3
|
<WizardStepFields
|
||||||
selectedColumns={selectedColumns}
|
selectedColumns={selectedColumns}
|
||||||
selectCommandColumns={selectCommandColumns}
|
selectCommandColumns={selectCommandColumns}
|
||||||
groups={editingGroups}
|
groups={editingGroups}
|
||||||
|
|
@ -1255,7 +1251,7 @@ const Wizard = () => {
|
||||||
|
|
||||||
{/* ─── Step 6: Sub Forms ───────────────────────────── */}
|
{/* ─── Step 6: Sub Forms ───────────────────────────── */}
|
||||||
{resolvedStep === STEP.subForms && (
|
{resolvedStep === STEP.subForms && (
|
||||||
<WizardStep4
|
<WizardStepSubForms
|
||||||
subForms={subForms}
|
subForms={subForms}
|
||||||
selectCommandColumns={selectCommandColumns}
|
selectCommandColumns={selectCommandColumns}
|
||||||
translate={translate}
|
translate={translate}
|
||||||
|
|
@ -1267,7 +1263,7 @@ const Wizard = () => {
|
||||||
|
|
||||||
{/* ─── Step 7: Widgets ───────────────────────────── */}
|
{/* ─── Step 7: Widgets ───────────────────────────── */}
|
||||||
{resolvedStep === STEP.widgets && (
|
{resolvedStep === STEP.widgets && (
|
||||||
<WizardStep5
|
<WizardStepWidgets
|
||||||
widgets={widgets}
|
widgets={widgets}
|
||||||
translate={translate}
|
translate={translate}
|
||||||
onChange={setWidgets}
|
onChange={setWidgets}
|
||||||
|
|
@ -1278,7 +1274,7 @@ const Wizard = () => {
|
||||||
|
|
||||||
{/* ─── Step 8: Workflow ───────────────────────────── */}
|
{/* ─── Step 8: Workflow ───────────────────────────── */}
|
||||||
{resolvedStep === STEP.workflow && (
|
{resolvedStep === STEP.workflow && (
|
||||||
<WizardStep6
|
<WizardStepWorkflow
|
||||||
listFormCode={values.listFormCode}
|
listFormCode={values.listFormCode}
|
||||||
workflow={workflow}
|
workflow={workflow}
|
||||||
criteria={criteria}
|
criteria={criteria}
|
||||||
|
|
@ -1293,7 +1289,7 @@ const Wizard = () => {
|
||||||
|
|
||||||
{/* ─── Step 9: Todo / Kanban ────────────────────── */}
|
{/* ─── Step 9: Todo / Kanban ────────────────────── */}
|
||||||
{resolvedStep === STEP.todo && values.todo && (
|
{resolvedStep === STEP.todo && values.todo && (
|
||||||
<WizardStep7
|
<WizardStepTodoLayout
|
||||||
selectedColumns={selectedColumns}
|
selectedColumns={selectedColumns}
|
||||||
selectCommandColumns={selectCommandColumns}
|
selectCommandColumns={selectCommandColumns}
|
||||||
translate={translate}
|
translate={translate}
|
||||||
|
|
@ -1304,7 +1300,7 @@ const Wizard = () => {
|
||||||
|
|
||||||
{/* ─── Step 10: Tree ─────────────────────────────── */}
|
{/* ─── Step 10: Tree ─────────────────────────────── */}
|
||||||
{resolvedStep === STEP.tree && values.tree && (
|
{resolvedStep === STEP.tree && values.tree && (
|
||||||
<WizardStep8
|
<WizardStepTreeLayout
|
||||||
selectCommandColumns={selectCommandColumns}
|
selectCommandColumns={selectCommandColumns}
|
||||||
translate={translate}
|
translate={translate}
|
||||||
onBack={() => goToAdjacentStep(-1)}
|
onBack={() => goToAdjacentStep(-1)}
|
||||||
|
|
@ -1314,7 +1310,7 @@ const Wizard = () => {
|
||||||
|
|
||||||
{/* ─── Step 11: Gantt ────────────────────────────── */}
|
{/* ─── Step 11: Gantt ────────────────────────────── */}
|
||||||
{resolvedStep === STEP.gantt && values.gantt && (
|
{resolvedStep === STEP.gantt && values.gantt && (
|
||||||
<WizardStep9
|
<WizardStepGanttLayout
|
||||||
selectCommandColumns={selectCommandColumns}
|
selectCommandColumns={selectCommandColumns}
|
||||||
translate={translate}
|
translate={translate}
|
||||||
onBack={() => goToAdjacentStep(-1)}
|
onBack={() => goToAdjacentStep(-1)}
|
||||||
|
|
@ -1324,7 +1320,7 @@ const Wizard = () => {
|
||||||
|
|
||||||
{/* ─── Step 12: Scheduler ───────────────────────── */}
|
{/* ─── Step 12: Scheduler ───────────────────────── */}
|
||||||
{resolvedStep === STEP.scheduler && values.scheduler && (
|
{resolvedStep === STEP.scheduler && values.scheduler && (
|
||||||
<WizardStep10
|
<WizardStepSchedulerLayout
|
||||||
selectCommandColumns={selectCommandColumns}
|
selectCommandColumns={selectCommandColumns}
|
||||||
translate={translate}
|
translate={translate}
|
||||||
onBack={() => goToAdjacentStep(-1)}
|
onBack={() => goToAdjacentStep(-1)}
|
||||||
|
|
@ -1334,7 +1330,7 @@ const Wizard = () => {
|
||||||
|
|
||||||
{/* ─── Step 13: Deploy ──────────────────────────── */}
|
{/* ─── Step 13: Deploy ──────────────────────────── */}
|
||||||
{resolvedStep === STEP.deploy && (
|
{resolvedStep === STEP.deploy && (
|
||||||
<WizardStep11
|
<WizardStepDeploy
|
||||||
values={values}
|
values={values}
|
||||||
wizardName={values.wizardName}
|
wizardName={values.wizardName}
|
||||||
selectedColumns={selectedColumns}
|
selectedColumns={selectedColumns}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { useState, useEffect, useCallback, useMemo } from 'react'
|
import { useState, useEffect, useCallback, useMemo, useRef } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import classNames from 'classnames'
|
import classNames from 'classnames'
|
||||||
import { Button, Dialog, Input, Notification, toast } from '@/components/ui'
|
import { Button, Dialog, Input, Notification, toast } from '@/components/ui'
|
||||||
|
|
@ -17,6 +17,7 @@ import {
|
||||||
FaLink,
|
FaLink,
|
||||||
FaExternalLinkAlt,
|
FaExternalLinkAlt,
|
||||||
FaFileExport,
|
FaFileExport,
|
||||||
|
FaFileImport,
|
||||||
} from 'react-icons/fa'
|
} from 'react-icons/fa'
|
||||||
import DbMigrateButton from '@/components/shared/DbMigrateButton'
|
import DbMigrateButton from '@/components/shared/DbMigrateButton'
|
||||||
import {
|
import {
|
||||||
|
|
@ -31,6 +32,9 @@ import { useStoreState } from '@/store/store'
|
||||||
import { ROUTES_ENUM } from '@/routes/route.constant'
|
import { ROUTES_ENUM } from '@/routes/route.constant'
|
||||||
import { WizardComponentKindEnum, WizardFileInfoDto } from '@/proxy/admin/wizard/models'
|
import { WizardComponentKindEnum, WizardFileInfoDto } from '@/proxy/admin/wizard/models'
|
||||||
import { useNavigationIcons } from '@/proxy/menus/navigation-icon.config'
|
import { useNavigationIcons } from '@/proxy/menus/navigation-icon.config'
|
||||||
|
import { usePermission } from '@/utils/hooks/usePermission'
|
||||||
|
import { WIZARD_PERMISSION } from '@/constants/permission.constant'
|
||||||
|
import WizardImportDialog from './WizardImportDialog'
|
||||||
|
|
||||||
interface ConfirmState {
|
interface ConfirmState {
|
||||||
fileName: string
|
fileName: string
|
||||||
|
|
@ -50,6 +54,16 @@ const WizardFileManager = () => {
|
||||||
const [exportingFile, setExportingFile] = useState<string | null>(null)
|
const [exportingFile, setExportingFile] = useState<string | null>(null)
|
||||||
const [confirm, setConfirm] = useState<ConfirmState | null>(null)
|
const [confirm, setConfirm] = useState<ConfirmState | null>(null)
|
||||||
const [search, setSearch] = useState('')
|
const [search, setSearch] = useState('')
|
||||||
|
const [importFile, setImportFile] = useState<File | null>(null)
|
||||||
|
const importInputRef = useRef<HTMLInputElement>(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 filteredFiles = useMemo(() => {
|
||||||
const q = search.trim().toLowerCase()
|
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<HTMLInputElement>) => {
|
||||||
|
const file = e.target.files?.[0]
|
||||||
|
e.target.value = ''
|
||||||
|
if (file) setImportFile(file)
|
||||||
|
}
|
||||||
|
|
||||||
const handleDeleteConfirm = async () => {
|
const handleDeleteConfirm = async () => {
|
||||||
if (!confirm) return
|
if (!confirm) return
|
||||||
setDeletingFile(confirm.fileName)
|
setDeletingFile(confirm.fileName)
|
||||||
|
|
@ -166,18 +188,44 @@ const WizardFileManager = () => {
|
||||||
>
|
>
|
||||||
<FaSync />
|
<FaSync />
|
||||||
</Button>
|
</Button>
|
||||||
|
{canImport && (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="default"
|
||||||
|
type="button"
|
||||||
|
title={translate('::App.ListForm.WizardImportTitle')}
|
||||||
|
className="flex items-center"
|
||||||
|
onClick={() => importInputRef.current?.click()}
|
||||||
|
>
|
||||||
|
<FaFileImport className="sm:mr-1" />
|
||||||
|
<span className="hidden sm:inline">
|
||||||
|
{translate('::App.ListForm.WizardImportButton')}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
<input
|
||||||
|
ref={importInputRef}
|
||||||
|
type="file"
|
||||||
|
accept=".zip,application/zip"
|
||||||
|
className="hidden"
|
||||||
|
onChange={handleImportFileChange}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<DbMigrateButton hideLabelOnMobile />
|
<DbMigrateButton hideLabelOnMobile />
|
||||||
|
{canCreate && (
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="solid"
|
variant="solid"
|
||||||
onClick={() => navigate(ROUTES_ENUM.protected.saas.listFormManagement.wizard)}
|
|
||||||
className="flex items-center"
|
className="flex items-center"
|
||||||
|
onClick={() => navigate(ROUTES_ENUM.protected.saas.listFormManagement.wizard)}
|
||||||
>
|
>
|
||||||
<FaPlus className="mr-1" />
|
<FaPlus className="mr-1" />
|
||||||
<span className="hidden sm:inline">
|
<span className="hidden sm:inline">
|
||||||
{translate('::App.ListFormWizard.AddNewRecord')}
|
{translate('::App.ListFormWizard.AddNewRecord')}
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -280,6 +328,7 @@ const WizardFileManager = () => {
|
||||||
<FaExternalLinkAlt />
|
<FaExternalLinkAlt />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
{canExport && (
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="plain"
|
variant="plain"
|
||||||
|
|
@ -291,6 +340,8 @@ const WizardFileManager = () => {
|
||||||
>
|
>
|
||||||
<FaFileExport />
|
<FaFileExport />
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
|
{canUpdate && (
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="plain"
|
variant="plain"
|
||||||
|
|
@ -305,6 +356,8 @@ const WizardFileManager = () => {
|
||||||
>
|
>
|
||||||
<FaEdit />
|
<FaEdit />
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
|
{canDelete && (
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="plain"
|
variant="plain"
|
||||||
|
|
@ -318,6 +371,7 @@ const WizardFileManager = () => {
|
||||||
>
|
>
|
||||||
<FaTrash />
|
<FaTrash />
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
@ -325,6 +379,14 @@ const WizardFileManager = () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{importFile && (
|
||||||
|
<WizardImportDialog
|
||||||
|
file={importFile}
|
||||||
|
onClose={() => setImportFile(null)}
|
||||||
|
onImported={loadFiles}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Delete Confirm Dialog */}
|
{/* Delete Confirm Dialog */}
|
||||||
<Dialog
|
<Dialog
|
||||||
isOpen={confirm !== null}
|
isOpen={confirm !== null}
|
||||||
|
|
|
||||||
662
ui/src/views/admin/listForm/wizard/WizardImportDialog.tsx
Normal file
662
ui/src/views/admin/listForm/wizard/WizardImportDialog.tsx
Normal file
|
|
@ -0,0 +1,662 @@
|
||||||
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
|
import classNames from 'classnames'
|
||||||
|
import { DiffEditor } from '@monaco-editor/react'
|
||||||
|
import { Button, Dialog, Progress } from '@/components/ui'
|
||||||
|
import {
|
||||||
|
FaCheck,
|
||||||
|
FaCheckCircle,
|
||||||
|
FaCodeBranch,
|
||||||
|
FaExclamationTriangle,
|
||||||
|
FaFileAlt,
|
||||||
|
FaFileImport,
|
||||||
|
FaPlusCircle,
|
||||||
|
FaTimes,
|
||||||
|
FaTimesCircle,
|
||||||
|
FaUndo,
|
||||||
|
} from 'react-icons/fa'
|
||||||
|
import {
|
||||||
|
analyzeWizardImport,
|
||||||
|
applyWizardImport,
|
||||||
|
completeWizardImport,
|
||||||
|
rollbackWizardImport,
|
||||||
|
} from '@/services/wizard.service'
|
||||||
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
|
import { useStoreState } from '@/store/store'
|
||||||
|
import {
|
||||||
|
WizardImportAnalysisDto,
|
||||||
|
WizardImportEntryDto,
|
||||||
|
WizardImportEntryStatusEnum,
|
||||||
|
} from '@/proxy/admin/wizard/models'
|
||||||
|
|
||||||
|
interface WizardImportDialogProps {
|
||||||
|
file: File | null
|
||||||
|
onClose: () => void
|
||||||
|
/** En az bir dosya yazildiginda tetiklenir; liste yenilenir. */
|
||||||
|
onImported: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Kullanicinin bir dosya icin verdigi karar. */
|
||||||
|
interface EntryDecision {
|
||||||
|
/** false ise dosya hedefe yazilmaz. */
|
||||||
|
apply: boolean
|
||||||
|
/** Catisma ekraninda karar verildi mi; verilmeden import baslatilamaz. */
|
||||||
|
resolved: boolean
|
||||||
|
/** Diff ekraninda birlestirilmis icerik; yalnizca catismali dosyalarda dolar. */
|
||||||
|
content?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ImportPhase = 'analyzing' | 'review' | 'running' | 'finished' | 'rolledBack'
|
||||||
|
|
||||||
|
interface AppliedRow {
|
||||||
|
path: string
|
||||||
|
ok: boolean
|
||||||
|
overwritten: boolean
|
||||||
|
message?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const statusOrder: Record<WizardImportEntryStatusEnum, number> = {
|
||||||
|
[WizardImportEntryStatusEnum.Conflict]: 0,
|
||||||
|
[WizardImportEntryStatusEnum.New]: 1,
|
||||||
|
[WizardImportEntryStatusEnum.Identical]: 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
const languageOf = (fileName: string) => (fileName.toLowerCase().endsWith('.sql') ? 'sql' : 'json')
|
||||||
|
|
||||||
|
const formatSize = (size: number) => {
|
||||||
|
if (!size) return '0 B'
|
||||||
|
if (size < 1024) return `${size} B`
|
||||||
|
return `${(size / 1024).toFixed(1)} KB`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wizard export zip'ini kapsam klasorune aktarir.
|
||||||
|
* Akis: analiz → catisma cozumu (diff) → toplu yazma. Yazma sirasinda bir hata olursa
|
||||||
|
* o ana kadar yazilan tum dosyalar sunucudaki yedeklerden geri alinir.
|
||||||
|
*/
|
||||||
|
const WizardImportDialog = ({ file, onClose, onImported }: WizardImportDialogProps) => {
|
||||||
|
const { translate } = useLocalization()
|
||||||
|
const mode = useStoreState((state) => state.theme.mode)
|
||||||
|
|
||||||
|
const [phase, setPhase] = useState<ImportPhase>('analyzing')
|
||||||
|
const [analysis, setAnalysis] = useState<WizardImportAnalysisDto | null>(null)
|
||||||
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
const [decisions, setDecisions] = useState<Record<string, EntryDecision>>({})
|
||||||
|
const [selectedPath, setSelectedPath] = useState<string | null>(null)
|
||||||
|
const [applied, setApplied] = useState<AppliedRow[]>([])
|
||||||
|
const [currentPath, setCurrentPath] = useState<string | null>(null)
|
||||||
|
const [showIdentical, setShowIdentical] = useState(false)
|
||||||
|
const [editorEpoch, setEditorEpoch] = useState(0)
|
||||||
|
|
||||||
|
// Diff editorunde yazilan icerik her tusa basista state'e yazilmaz; karar verilirken okunur.
|
||||||
|
const draftRef = useRef<string>('')
|
||||||
|
|
||||||
|
const entries = useMemo(() => {
|
||||||
|
const list = [...(analysis?.entries ?? [])]
|
||||||
|
list.sort(
|
||||||
|
(a, b) =>
|
||||||
|
statusOrder[a.status] - statusOrder[b.status] ||
|
||||||
|
a.path.localeCompare(b.path, undefined, { sensitivity: 'base' }),
|
||||||
|
)
|
||||||
|
return list
|
||||||
|
}, [analysis])
|
||||||
|
|
||||||
|
const conflicts = useMemo(
|
||||||
|
() => entries.filter((e) => e.status === WizardImportEntryStatusEnum.Conflict),
|
||||||
|
[entries],
|
||||||
|
)
|
||||||
|
const unresolvedCount = useMemo(
|
||||||
|
() => conflicts.filter((e) => !decisions[e.path]?.resolved).length,
|
||||||
|
[conflicts, decisions],
|
||||||
|
)
|
||||||
|
const applyList = useMemo(
|
||||||
|
() => entries.filter((e) => decisions[e.path]?.apply),
|
||||||
|
[entries, decisions],
|
||||||
|
)
|
||||||
|
const visibleEntries = useMemo(
|
||||||
|
() =>
|
||||||
|
showIdentical
|
||||||
|
? entries
|
||||||
|
: entries.filter((e) => e.status !== WizardImportEntryStatusEnum.Identical),
|
||||||
|
[entries, showIdentical],
|
||||||
|
)
|
||||||
|
const identicalCount = useMemo(
|
||||||
|
() => entries.filter((e) => e.status === WizardImportEntryStatusEnum.Identical).length,
|
||||||
|
[entries],
|
||||||
|
)
|
||||||
|
|
||||||
|
const selectedEntry = useMemo(
|
||||||
|
() => entries.find((e) => e.path === selectedPath) ?? null,
|
||||||
|
[entries, selectedPath],
|
||||||
|
)
|
||||||
|
|
||||||
|
// Analiz: zip sunucuda gecici bir oturuma acilir, hedef klasore henuz dokunulmaz.
|
||||||
|
useEffect(() => {
|
||||||
|
let cancelled = false
|
||||||
|
|
||||||
|
const run = async () => {
|
||||||
|
if (!file) return
|
||||||
|
setPhase('analyzing')
|
||||||
|
setError(null)
|
||||||
|
try {
|
||||||
|
const res = await analyzeWizardImport(file)
|
||||||
|
if (cancelled) return
|
||||||
|
|
||||||
|
const data = res.data
|
||||||
|
const initial: Record<string, EntryDecision> = {}
|
||||||
|
data.entries.forEach((entry) => {
|
||||||
|
initial[entry.path] = {
|
||||||
|
// Yeni dosyalar sorulmadan kopyalanir, ayni olanlar atlanir, catismalilar karar bekler.
|
||||||
|
apply: entry.status === WizardImportEntryStatusEnum.New,
|
||||||
|
resolved: entry.status !== WizardImportEntryStatusEnum.Conflict,
|
||||||
|
content:
|
||||||
|
entry.status === WizardImportEntryStatusEnum.Conflict
|
||||||
|
? entry.incomingContent
|
||||||
|
: undefined,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
setAnalysis(data)
|
||||||
|
setDecisions(initial)
|
||||||
|
setPhase('review')
|
||||||
|
|
||||||
|
const firstConflict = data.entries.find(
|
||||||
|
(e) => e.status === WizardImportEntryStatusEnum.Conflict,
|
||||||
|
)
|
||||||
|
if (firstConflict) setSelectedPath(firstConflict.path)
|
||||||
|
} catch (err: any) {
|
||||||
|
if (cancelled) return
|
||||||
|
setError(err?.response?.data?.error?.message ?? err?.message ?? 'Unknown error')
|
||||||
|
setPhase('review')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run()
|
||||||
|
return () => {
|
||||||
|
cancelled = true
|
||||||
|
}
|
||||||
|
}, [file])
|
||||||
|
|
||||||
|
// Diff editorunun tasidigi taslak, secim degistikce ilgili dosyanin son haline doner.
|
||||||
|
useEffect(() => {
|
||||||
|
if (!selectedEntry) {
|
||||||
|
draftRef.current = ''
|
||||||
|
return
|
||||||
|
}
|
||||||
|
draftRef.current = decisions[selectedEntry.path]?.content ?? selectedEntry.incomingContent ?? ''
|
||||||
|
// decisions bagimliligi kasitli olarak disarida: her tus vurusunda editor sifirlanmamalidir.
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [selectedPath])
|
||||||
|
|
||||||
|
const decide = useCallback((path: string, decision: EntryDecision) => {
|
||||||
|
setDecisions((prev) => ({ ...prev, [path]: decision }))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const goToNextConflict = useCallback(
|
||||||
|
(fromPath: string) => {
|
||||||
|
const index = conflicts.findIndex((c) => c.path === fromPath)
|
||||||
|
const next = conflicts.slice(index + 1).find((c) => !decisions[c.path]?.resolved)
|
||||||
|
setSelectedPath(next ? next.path : null)
|
||||||
|
},
|
||||||
|
[conflicts, decisions],
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleTakeIncoming = (entry: WizardImportEntryDto) => {
|
||||||
|
decide(entry.path, { apply: true, resolved: true, content: draftRef.current })
|
||||||
|
goToNextConflict(entry.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKeepExisting = (entry: WizardImportEntryDto) => {
|
||||||
|
decide(entry.path, { apply: false, resolved: true })
|
||||||
|
goToNextConflict(entry.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Editordeki elle yapilan degisiklikleri atip zip'ten gelen icerige doner. */
|
||||||
|
const handleResetDraft = (entry: WizardImportEntryDto) => {
|
||||||
|
draftRef.current = entry.incomingContent ?? ''
|
||||||
|
decide(entry.path, {
|
||||||
|
...(decisions[entry.path] ?? { apply: true, resolved: false }),
|
||||||
|
content: entry.incomingContent,
|
||||||
|
})
|
||||||
|
// Editor icerigi prop ile ayni kalabilecegi icin bilesen key ile yeniden kurulur.
|
||||||
|
setEditorEpoch((v) => v + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Dosyalari sirayla yazar; ilk hatada tum toplu islem geri alinir. */
|
||||||
|
const handleStart = async () => {
|
||||||
|
if (!analysis) return
|
||||||
|
|
||||||
|
setPhase('running')
|
||||||
|
setApplied([])
|
||||||
|
setError(null)
|
||||||
|
|
||||||
|
const rows: AppliedRow[] = []
|
||||||
|
|
||||||
|
for (const entry of applyList) {
|
||||||
|
setCurrentPath(entry.path)
|
||||||
|
try {
|
||||||
|
const res = await applyWizardImport(
|
||||||
|
analysis.importId,
|
||||||
|
entry.path,
|
||||||
|
decisions[entry.path]?.content,
|
||||||
|
)
|
||||||
|
rows.push({ path: entry.path, ok: true, overwritten: res.data.overwritten })
|
||||||
|
setApplied([...rows])
|
||||||
|
} catch (err: any) {
|
||||||
|
const message = err?.response?.data?.error?.message ?? err?.message ?? 'Unknown error'
|
||||||
|
rows.push({ path: entry.path, ok: false, overwritten: false, message })
|
||||||
|
setApplied([...rows])
|
||||||
|
setCurrentPath(null)
|
||||||
|
setError(message)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await rollbackWizardImport(analysis.importId)
|
||||||
|
} catch (rollbackErr: any) {
|
||||||
|
setError(
|
||||||
|
`${message} — ${translate('::App.ListForm.WizardImportRollbackFailed')}: ${
|
||||||
|
rollbackErr?.message ?? 'Unknown error'
|
||||||
|
}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
setPhase('rolledBack')
|
||||||
|
onImported()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setCurrentPath(null)
|
||||||
|
try {
|
||||||
|
await completeWizardImport(analysis.importId)
|
||||||
|
} catch {
|
||||||
|
// Oturum klasoru zaten gecici; temizlenemezse import basarili sayilir.
|
||||||
|
}
|
||||||
|
setPhase('finished')
|
||||||
|
onImported()
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Import baslamadan kapatilirsa sunucudaki staging klasoru birakilmaz. */
|
||||||
|
const handleClose = () => {
|
||||||
|
if (phase === 'running') return
|
||||||
|
if (analysis && phase === 'review') {
|
||||||
|
rollbackWizardImport(analysis.importId).catch(() => undefined)
|
||||||
|
}
|
||||||
|
onClose()
|
||||||
|
}
|
||||||
|
|
||||||
|
const progressPercent = applyList.length
|
||||||
|
? Math.round((applied.length / applyList.length) * 100)
|
||||||
|
: 0
|
||||||
|
|
||||||
|
const renderStatusBadge = (entry: WizardImportEntryDto) => {
|
||||||
|
const decision = decisions[entry.path]
|
||||||
|
|
||||||
|
if (entry.status === WizardImportEntryStatusEnum.Identical)
|
||||||
|
return (
|
||||||
|
<span className="rounded-full bg-gray-200 px-2 py-0.5 text-[11px] text-gray-600 dark:bg-gray-700 dark:text-gray-300">
|
||||||
|
{translate('::App.ListForm.WizardImportIdentical')}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
|
||||||
|
if (entry.status === WizardImportEntryStatusEnum.New)
|
||||||
|
return (
|
||||||
|
<span className="rounded-full bg-emerald-100 px-2 py-0.5 text-[11px] text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300">
|
||||||
|
{translate('::App.ListForm.WizardImportNew')}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!decision?.resolved)
|
||||||
|
return (
|
||||||
|
<span className="rounded-full bg-amber-100 px-2 py-0.5 text-[11px] text-amber-700 dark:bg-amber-900/30 dark:text-amber-300">
|
||||||
|
{translate('::App.ListForm.WizardImportConflict')}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={classNames(
|
||||||
|
'rounded-full px-2 py-0.5 text-[11px]',
|
||||||
|
decision.apply
|
||||||
|
? 'bg-indigo-100 text-indigo-700 dark:bg-indigo-900/30 dark:text-indigo-300'
|
||||||
|
: 'bg-gray-200 text-gray-600 dark:bg-gray-700 dark:text-gray-300',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{translate(
|
||||||
|
decision.apply
|
||||||
|
? '::App.ListForm.WizardImportResolvedApply'
|
||||||
|
: '::App.ListForm.WizardImportResolvedSkip',
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderReview = () => (
|
||||||
|
<div className="flex min-h-0 flex-1 gap-3">
|
||||||
|
{/* Dosya listesi */}
|
||||||
|
<div className="flex w-72 shrink-0 flex-col overflow-hidden rounded-lg border border-gray-200 dark:border-gray-700">
|
||||||
|
<div className="flex items-center justify-between border-b border-gray-200 px-3 py-2 text-xs text-gray-500 dark:border-gray-700">
|
||||||
|
<span className="font-mono truncate">{analysis?.scopeFolder}</span>
|
||||||
|
<span>
|
||||||
|
{applyList.length}/{entries.length}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="min-h-0 flex-1 overflow-auto">
|
||||||
|
{visibleEntries.map((entry) => {
|
||||||
|
const decision = decisions[entry.path]
|
||||||
|
const isConflict = entry.status === WizardImportEntryStatusEnum.Conflict
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={entry.path}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
className={classNames(
|
||||||
|
'flex w-full cursor-pointer items-center gap-2 border-b border-gray-100 px-3 py-2 text-left last:border-b-0 dark:border-gray-800',
|
||||||
|
selectedPath === entry.path
|
||||||
|
? 'bg-indigo-50 dark:bg-indigo-900/20'
|
||||||
|
: 'hover:bg-gray-50 dark:hover:bg-gray-800',
|
||||||
|
)}
|
||||||
|
onClick={() => setSelectedPath(entry.path)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') setSelectedPath(entry.path)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isConflict ? (
|
||||||
|
<FaCodeBranch className="shrink-0 text-amber-500" />
|
||||||
|
) : entry.status === WizardImportEntryStatusEnum.New ? (
|
||||||
|
<FaPlusCircle className="shrink-0 text-emerald-500" />
|
||||||
|
) : (
|
||||||
|
<FaFileAlt className="shrink-0 text-gray-400" />
|
||||||
|
)}
|
||||||
|
<span className="min-w-0 flex-1">
|
||||||
|
<span className="block truncate text-xs font-medium text-gray-800 dark:text-gray-200">
|
||||||
|
{entry.fileName}
|
||||||
|
</span>
|
||||||
|
<span className="block truncate font-mono text-[11px] text-gray-400">
|
||||||
|
{entry.path}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
{renderStatusBadge(entry)}
|
||||||
|
{!isConflict && (
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="shrink-0"
|
||||||
|
checked={!!decision?.apply}
|
||||||
|
title={translate('::App.ListForm.WizardImportInclude')}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
onChange={(e) =>
|
||||||
|
decide(entry.path, {
|
||||||
|
apply: e.target.checked,
|
||||||
|
resolved: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
{identicalCount > 0 && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="border-t border-gray-200 px-3 py-2 text-left text-[11px] text-gray-500 hover:bg-gray-50 dark:border-gray-700 dark:hover:bg-gray-800"
|
||||||
|
onClick={() => setShowIdentical((v) => !v)}
|
||||||
|
>
|
||||||
|
{showIdentical
|
||||||
|
? translate('::App.ListForm.WizardImportHideIdentical')
|
||||||
|
: `${identicalCount} ${translate('::App.ListForm.WizardImportShowIdentical')}`}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Karsilastirma / detay */}
|
||||||
|
<div className="flex min-w-0 flex-1 flex-col overflow-hidden rounded-lg border border-gray-200 dark:border-gray-700">
|
||||||
|
{selectedEntry && selectedEntry.status === WizardImportEntryStatusEnum.Conflict ? (
|
||||||
|
<>
|
||||||
|
<div className="flex flex-wrap items-center gap-2 border-b border-gray-200 px-3 py-2 dark:border-gray-700">
|
||||||
|
<FaCodeBranch className="text-amber-500" />
|
||||||
|
<span className="font-mono text-xs text-gray-700 dark:text-gray-300">
|
||||||
|
{selectedEntry.path}
|
||||||
|
</span>
|
||||||
|
<span className="text-[11px] text-gray-400">
|
||||||
|
{formatSize(selectedEntry.existingSize)} → {formatSize(selectedEntry.incomingSize)}
|
||||||
|
</span>
|
||||||
|
<div className="ml-auto flex gap-2">
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
variant="plain"
|
||||||
|
type="button"
|
||||||
|
icon={<FaUndo />}
|
||||||
|
onClick={() => handleResetDraft(selectedEntry)}
|
||||||
|
>
|
||||||
|
{translate('::App.ListForm.WizardImportResetIncoming')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
variant="default"
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleKeepExisting(selectedEntry)}
|
||||||
|
>
|
||||||
|
{translate('::App.ListForm.WizardImportKeepExisting')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="xs"
|
||||||
|
variant="solid"
|
||||||
|
type="button"
|
||||||
|
onClick={() => handleTakeIncoming(selectedEntry)}
|
||||||
|
>
|
||||||
|
{translate('::App.ListForm.WizardImportUseThis')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 border-b border-gray-200 px-3 py-1 text-[11px] text-gray-400 dark:border-gray-700">
|
||||||
|
<span className="flex-1">{translate('::App.ListForm.WizardImportExistingSide')}</span>
|
||||||
|
<span className="flex-1">{translate('::App.ListForm.WizardImportIncomingSide')}</span>
|
||||||
|
</div>
|
||||||
|
<div className="min-h-0 flex-1">
|
||||||
|
<DiffEditor
|
||||||
|
key={`${selectedEntry.path}#${editorEpoch}`}
|
||||||
|
height="100%"
|
||||||
|
language={languageOf(selectedEntry.fileName)}
|
||||||
|
original={selectedEntry.existingContent ?? ''}
|
||||||
|
modified={
|
||||||
|
decisions[selectedEntry.path]?.content ?? selectedEntry.incomingContent ?? ''
|
||||||
|
}
|
||||||
|
theme={mode === 'dark' ? 'vs-dark' : 'light'}
|
||||||
|
options={{
|
||||||
|
renderSideBySide: true,
|
||||||
|
originalEditable: false,
|
||||||
|
readOnly: false,
|
||||||
|
automaticLayout: true,
|
||||||
|
fontSize: 12,
|
||||||
|
minimap: { enabled: false },
|
||||||
|
scrollBeyondLastLine: false,
|
||||||
|
wordWrap: 'on',
|
||||||
|
}}
|
||||||
|
onMount={(editor) => {
|
||||||
|
const modified = editor.getModifiedEditor()
|
||||||
|
draftRef.current = modified.getValue()
|
||||||
|
modified.onDidChangeModelContent(() => {
|
||||||
|
draftRef.current = modified.getValue()
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-1 flex-col items-center justify-center gap-2 p-6 text-center text-sm text-gray-400">
|
||||||
|
<FaFileImport size={32} />
|
||||||
|
{unresolvedCount > 0 ? (
|
||||||
|
<span>
|
||||||
|
{unresolvedCount} {translate('::App.ListForm.WizardImportUnresolved')}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span>{translate('::App.ListForm.WizardImportReady')}</span>
|
||||||
|
)}
|
||||||
|
{analysis && analysis.warnings.length > 0 && (
|
||||||
|
<div className="mt-3 w-full max-w-xl rounded-lg bg-amber-50 p-3 text-left text-xs text-amber-700 dark:bg-amber-900/20 dark:text-amber-300">
|
||||||
|
<div className="mb-1 flex items-center gap-1 font-medium">
|
||||||
|
<FaExclamationTriangle />
|
||||||
|
{translate('::App.ListForm.WizardImportWarnings')}
|
||||||
|
</div>
|
||||||
|
<ul className="list-disc space-y-0.5 pl-4">
|
||||||
|
{analysis.warnings.map((warning) => (
|
||||||
|
<li key={warning}>{warning}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
const renderProgress = () => (
|
||||||
|
<div className="flex min-h-0 flex-1 flex-col gap-3">
|
||||||
|
<Progress
|
||||||
|
percent={progressPercent}
|
||||||
|
color={phase === 'rolledBack' ? 'red-500' : undefined}
|
||||||
|
customInfo={`${applied.length}/${applyList.length}`}
|
||||||
|
/>
|
||||||
|
{currentPath && (
|
||||||
|
<div className="truncate font-mono text-xs text-gray-500">
|
||||||
|
{translate('::App.ListForm.WizardImportWriting')}: {currentPath}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="min-h-0 flex-1 overflow-auto rounded-lg border border-gray-200 dark:border-gray-700">
|
||||||
|
{applied.map((row) => (
|
||||||
|
<div
|
||||||
|
key={row.path}
|
||||||
|
className="flex items-center gap-2 border-b border-gray-100 px-3 py-1.5 text-xs last:border-b-0 dark:border-gray-800"
|
||||||
|
>
|
||||||
|
{row.ok ? (
|
||||||
|
<FaCheckCircle className="shrink-0 text-emerald-500" />
|
||||||
|
) : (
|
||||||
|
<FaTimesCircle className="shrink-0 text-red-500" />
|
||||||
|
)}
|
||||||
|
<span className="min-w-0 flex-1 truncate font-mono text-gray-700 dark:text-gray-300">
|
||||||
|
{row.path}
|
||||||
|
</span>
|
||||||
|
<span className="shrink-0 text-[11px] text-gray-400">
|
||||||
|
{row.ok
|
||||||
|
? translate(
|
||||||
|
row.overwritten
|
||||||
|
? '::App.ListForm.WizardImportOverwritten'
|
||||||
|
: '::App.ListForm.WizardImportCreated',
|
||||||
|
)
|
||||||
|
: row.message}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{phase === 'rolledBack' && (
|
||||||
|
<div className="rounded-lg bg-red-50 p-3 text-xs text-red-700 dark:bg-red-900/20 dark:text-red-300">
|
||||||
|
<div className="flex items-center gap-1 font-medium">
|
||||||
|
<FaExclamationTriangle />
|
||||||
|
{translate('::App.ListForm.WizardImportRolledBack')}
|
||||||
|
</div>
|
||||||
|
{error && <div className="mt-1 break-all">{error}</div>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{phase === 'finished' && (
|
||||||
|
<div className="rounded-lg bg-emerald-50 p-3 text-xs text-emerald-700 dark:bg-emerald-900/20 dark:text-emerald-300">
|
||||||
|
<div className="flex items-center gap-1 font-medium">
|
||||||
|
<FaCheckCircle />
|
||||||
|
{translate('::App.ListForm.WizardImportSuccess')}
|
||||||
|
</div>
|
||||||
|
<div className="mt-1">{translate('::App.ListForm.WizardImportRunMigrateHint')}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
preventScroll
|
||||||
|
isOpen={file !== null}
|
||||||
|
width={1360}
|
||||||
|
contentClassName="flex flex-col"
|
||||||
|
closable={phase !== 'running'}
|
||||||
|
onClose={handleClose}
|
||||||
|
onRequestClose={handleClose}
|
||||||
|
>
|
||||||
|
<Dialog.Body className="mt-5 flex h-[calc(100vh-14rem)] max-h-[calc(90vh-7rem)] min-h-[360px] !flex-none flex-col gap-3 !overflow-hidden">
|
||||||
|
<div className="flex flex-wrap items-center gap-2 border-b border-gray-200 pb-3 dark:border-gray-700">
|
||||||
|
<FaFileImport className="text-sky-500" />
|
||||||
|
<h5 className="mr-1">{translate('::App.ListForm.WizardImportTitle')}</h5>
|
||||||
|
<span className="truncate font-mono text-xs text-gray-400">{file?.name}</span>
|
||||||
|
{phase === 'review' && analysis && (
|
||||||
|
<div className="ml-auto flex flex-wrap items-center gap-2 text-[11px]">
|
||||||
|
<span className="rounded-full bg-emerald-100 px-2 py-0.5 text-emerald-700 dark:bg-emerald-900/30 dark:text-emerald-300">
|
||||||
|
{entries.filter((e) => e.status === WizardImportEntryStatusEnum.New).length}{' '}
|
||||||
|
{translate('::App.ListForm.WizardImportNew')}
|
||||||
|
</span>
|
||||||
|
<span className="rounded-full bg-amber-100 px-2 py-0.5 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300">
|
||||||
|
{conflicts.length} {translate('::App.ListForm.WizardImportConflict')}
|
||||||
|
</span>
|
||||||
|
<span className="rounded-full bg-gray-200 px-2 py-0.5 text-gray-600 dark:bg-gray-700 dark:text-gray-300">
|
||||||
|
{entries.filter((e) => e.status === WizardImportEntryStatusEnum.Identical).length}{' '}
|
||||||
|
{translate('::App.ListForm.WizardImportIdentical')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{phase === 'analyzing' && (
|
||||||
|
<div className="flex flex-1 items-center justify-center text-sm text-gray-400">
|
||||||
|
<span className="animate-pulse">
|
||||||
|
{translate('::App.ListForm.WizardImportAnalyzing')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{phase === 'review' && error && (
|
||||||
|
<div className="flex flex-1 flex-col items-center justify-center gap-2 text-sm text-red-500">
|
||||||
|
<FaExclamationTriangle size={28} />
|
||||||
|
<span className="break-all text-center">{error}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{phase === 'review' && !error && analysis && renderReview()}
|
||||||
|
|
||||||
|
{(phase === 'running' || phase === 'finished' || phase === 'rolledBack') &&
|
||||||
|
renderProgress()}
|
||||||
|
</Dialog.Body>
|
||||||
|
|
||||||
|
<Dialog.Footer className="mt-1 flex items-center justify-end gap-2 border-t border-gray-200 pt-3 dark:border-gray-700">
|
||||||
|
{phase === 'review' && !error && unresolvedCount > 0 && (
|
||||||
|
<span className="mr-auto text-xs text-amber-600 dark:text-amber-400">
|
||||||
|
{unresolvedCount} {translate('::App.ListForm.WizardImportUnresolved')}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
type="button"
|
||||||
|
variant="plain"
|
||||||
|
icon={<FaTimes />}
|
||||||
|
disabled={phase === 'running'}
|
||||||
|
onClick={handleClose}
|
||||||
|
>
|
||||||
|
{translate(
|
||||||
|
phase === 'finished' || phase === 'rolledBack'
|
||||||
|
? '::App.Platform.Close'
|
||||||
|
: '::App.Platform.Cancel',
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
{phase === 'review' && !error && (
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
type="button"
|
||||||
|
variant="solid"
|
||||||
|
icon={<FaCheck />}
|
||||||
|
disabled={unresolvedCount > 0 || applyList.length === 0}
|
||||||
|
onClick={handleStart}
|
||||||
|
>
|
||||||
|
{translate('::App.ListForm.WizardImportStart')} ({applyList.length})
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Dialog.Footer>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WizardImportDialog
|
||||||
|
|
@ -73,6 +73,7 @@ const WizardStepCustomComponent = ({
|
||||||
<Select
|
<Select
|
||||||
field={field}
|
field={field}
|
||||||
form={form}
|
form={form}
|
||||||
|
size="md"
|
||||||
isClearable={true}
|
isClearable={true}
|
||||||
options={componentOptions}
|
options={componentOptions}
|
||||||
placeholder={translate('::App.WizardStepComponent.SelectComponent')}
|
placeholder={translate('::App.WizardStepComponent.SelectComponent')}
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ import { usePermission } from '@/utils/hooks/usePermission'
|
||||||
|
|
||||||
// ─── Props ────────────────────────────────────────────────────────────────────
|
// ─── Props ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export interface WizardStep2Props {
|
export interface WizardStepDataSettingsProps {
|
||||||
values: ListFormWizardDto
|
values: ListFormWizardDto
|
||||||
errors: FormikErrors<ListFormWizardDto>
|
errors: FormikErrors<ListFormWizardDto>
|
||||||
touched: FormikTouched<ListFormWizardDto>
|
touched: FormikTouched<ListFormWizardDto>
|
||||||
|
|
@ -50,9 +50,14 @@ export interface WizardStep2Props {
|
||||||
onNext: () => void
|
onNext: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── WizardStep2 ──────────────────────────────────────────────────────────────
|
// ─── WizardStepDataSettings ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const WizardStep2 = ({
|
/**
|
||||||
|
* Veri ayarları adımı: ListForm kodu, veri kaynağı, select komutu (tablo/view/sp/query) ve
|
||||||
|
* listenin izin verdiği işlemler (ekleme/güncelleme/silme/detay) ile layout seçenekleri.
|
||||||
|
* Sonraki alan adımının okuduğu kolon listesi buradaki select komutundan çözülür.
|
||||||
|
*/
|
||||||
|
const WizardStepDataSettings = ({
|
||||||
values,
|
values,
|
||||||
errors,
|
errors,
|
||||||
touched,
|
touched,
|
||||||
|
|
@ -75,7 +80,7 @@ const WizardStep2 = ({
|
||||||
translate,
|
translate,
|
||||||
onBack,
|
onBack,
|
||||||
onNext,
|
onNext,
|
||||||
}: WizardStep2Props) => {
|
}: WizardStepDataSettingsProps) => {
|
||||||
const [showTableDesignerDialog, setShowTableDesignerDialog] = useState(false)
|
const [showTableDesignerDialog, setShowTableDesignerDialog] = useState(false)
|
||||||
const [designTableData, setDesignTableData] = useState<{
|
const [designTableData, setDesignTableData] = useState<{
|
||||||
schemaName: string
|
schemaName: string
|
||||||
|
|
@ -826,4 +831,4 @@ const WizardStep2 = ({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default WizardStep2
|
export default WizardStepDataSettings
|
||||||
|
|
@ -11,7 +11,7 @@ import {
|
||||||
FaRocket,
|
FaRocket,
|
||||||
FaSpinner,
|
FaSpinner,
|
||||||
} from 'react-icons/fa'
|
} from 'react-icons/fa'
|
||||||
import { WizardGroup } from './WizardStep3'
|
import { WizardGroup } from './WizardStepFields'
|
||||||
import { dbSourceTypeOptions, selectCommandTypeOptions } from '../edit/options'
|
import { dbSourceTypeOptions, selectCommandTypeOptions } from '../edit/options'
|
||||||
import { ListFormWizardDto, WizardComponentKindEnum } from '@/proxy/admin/wizard/models'
|
import { ListFormWizardDto, WizardComponentKindEnum } from '@/proxy/admin/wizard/models'
|
||||||
import {
|
import {
|
||||||
|
|
@ -24,7 +24,7 @@ import { formatWorkflowApprovers } from '@/utils/workflow/workflowHelpers'
|
||||||
|
|
||||||
// ─── Types ────────────────────────────────────────────────────────────────────
|
// ─── Types ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export interface WizardStep11Props {
|
export interface WizardStepDeployProps {
|
||||||
values: ListFormWizardDto
|
values: ListFormWizardDto
|
||||||
wizardName: string
|
wizardName: string
|
||||||
selectedColumns: Set<string>
|
selectedColumns: Set<string>
|
||||||
|
|
@ -201,8 +201,13 @@ function Row({ label, value }: { label: string; value?: string | number }) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── WizardStep11 ─────────────────────────────────────────────────────────────
|
// ─── WizardStepDeploy ─────────────────────────────────────────────────────────────
|
||||||
const WizardStep11 = ({
|
/**
|
||||||
|
* Deploy adımı: önceki adımlarda toplanan her şeyin özetini gösterir ve onay sonrası
|
||||||
|
* wizard'ı sunucuya gönderir. Sunucu bu istekten menü, izin, dil, ListForm ve seed
|
||||||
|
* dosyasını üretir; özet ekranında bağlantı cümlesi maskelenerek gösterilir.
|
||||||
|
*/
|
||||||
|
const WizardStepDeploy = ({
|
||||||
values,
|
values,
|
||||||
wizardName,
|
wizardName,
|
||||||
selectedColumns,
|
selectedColumns,
|
||||||
|
|
@ -215,7 +220,7 @@ const WizardStep11 = ({
|
||||||
translate,
|
translate,
|
||||||
onBack,
|
onBack,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
}: WizardStep11Props) => {
|
}: WizardStepDeployProps) => {
|
||||||
const [logs, setLogs] = useState<LogEntry[]>([])
|
const [logs, setLogs] = useState<LogEntry[]>([])
|
||||||
const [isDeploying, setIsDeploying] = useState(false)
|
const [isDeploying, setIsDeploying] = useState(false)
|
||||||
const [isDone, setIsDone] = useState(false)
|
const [isDone, setIsDone] = useState(false)
|
||||||
|
|
@ -829,4 +834,4 @@ const WizardStep11 = ({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default WizardStep11
|
export default WizardStepDeploy
|
||||||
|
|
@ -62,7 +62,7 @@ export interface WizardGroup {
|
||||||
items: WizardGroupItem[]
|
items: WizardGroupItem[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WizardStep3Props {
|
export interface WizardStepFieldsProps {
|
||||||
selectedColumns: Set<string>
|
selectedColumns: Set<string>
|
||||||
selectCommandColumns: DatabaseColumnDto[]
|
selectCommandColumns: DatabaseColumnDto[]
|
||||||
groups: WizardGroup[]
|
groups: WizardGroup[]
|
||||||
|
|
@ -641,9 +641,7 @@ function SortableItem({
|
||||||
{/* Bottom row: ColSpan + Editing Form + Required */}
|
{/* Bottom row: ColSpan + Editing Form + Required */}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<span className="text-[10px] text-gray-400">
|
<span className="text-[10px] text-gray-400">{translate('::App.WizardStep3.Span')}</span>
|
||||||
{translate('::App.WizardStep3.Span')}
|
|
||||||
</span>
|
|
||||||
<select
|
<select
|
||||||
value={item.colSpan}
|
value={item.colSpan}
|
||||||
onChange={(e) => onColSpanChange(Number(e.target.value))}
|
onChange={(e) => onColSpanChange(Number(e.target.value))}
|
||||||
|
|
@ -739,9 +737,7 @@ function GroupCard({
|
||||||
/>
|
/>
|
||||||
{/* ColCount */}
|
{/* ColCount */}
|
||||||
<div className="flex items-center gap-1 shrink-0">
|
<div className="flex items-center gap-1 shrink-0">
|
||||||
<span className="text-xs text-gray-400">
|
<span className="text-xs text-gray-400">{translate('::App.WizardStep3.Cols')}</span>
|
||||||
{translate('::App.WizardStep3.Cols')}
|
|
||||||
</span>
|
|
||||||
{[1, 2, 3].map((n) => (
|
{[1, 2, 3].map((n) => (
|
||||||
<Button
|
<Button
|
||||||
key={n}
|
key={n}
|
||||||
|
|
@ -839,8 +835,13 @@ function GroupCard({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── WizardStep3 ──────────────────────────────────────────────────────────────
|
// ─── WizardStepFields ──────────────────────────────────────────────────────────────
|
||||||
const WizardStep3 = ({
|
/**
|
||||||
|
* Alan adımı: select komutundan gelen kolonların ListForm alanlarına dönüştürüldüğü yer.
|
||||||
|
* Kolon grupları, editör tipi/seçenekleri, zorunluluk, lookup ve TR/EN başlıklar burada
|
||||||
|
* düzenlenir; üretilen gruplar deploy'da ListFormField kayıtlarına çevrilir.
|
||||||
|
*/
|
||||||
|
const WizardStepFields = ({
|
||||||
selectedColumns,
|
selectedColumns,
|
||||||
selectCommandColumns,
|
selectCommandColumns,
|
||||||
groups,
|
groups,
|
||||||
|
|
@ -850,7 +851,7 @@ const WizardStep3 = ({
|
||||||
translate,
|
translate,
|
||||||
onBack,
|
onBack,
|
||||||
onNext,
|
onNext,
|
||||||
}: WizardStep3Props) => {
|
}: WizardStepFieldsProps) => {
|
||||||
const [activeId, setActiveId] = useState<string | null>(null)
|
const [activeId, setActiveId] = useState<string | null>(null)
|
||||||
const [overGroupId, setOverGroupId] = useState<string | null>(null)
|
const [overGroupId, setOverGroupId] = useState<string | null>(null)
|
||||||
const [isHelperOpen, setIsHelperOpen] = useState(false)
|
const [isHelperOpen, setIsHelperOpen] = useState(false)
|
||||||
|
|
@ -1266,4 +1267,4 @@ const WizardStep3 = ({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default WizardStep3
|
export default WizardStepFields
|
||||||
|
|
@ -7,14 +7,24 @@ import { useMemo } from 'react'
|
||||||
import { FaArrowLeft, FaArrowRight } from 'react-icons/fa'
|
import { FaArrowLeft, FaArrowRight } from 'react-icons/fa'
|
||||||
import { gantScaleOptions } from '../edit/options'
|
import { gantScaleOptions } from '../edit/options'
|
||||||
|
|
||||||
interface WizardStep9Props {
|
interface WizardStepGanttLayoutProps {
|
||||||
selectCommandColumns: DatabaseColumnDto[]
|
selectCommandColumns: DatabaseColumnDto[]
|
||||||
translate: (key: string) => string
|
translate: (key: string) => string
|
||||||
onBack: () => void
|
onBack: () => void
|
||||||
onNext: () => void
|
onNext: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const WizardStep9 = ({ selectCommandColumns, translate, onBack, onNext }: WizardStep9Props) => {
|
/**
|
||||||
|
* Gantt layout adımı: görev başlığı, başlangıç/bitiş tarihi, ilerleme ve hiyerarşi
|
||||||
|
* (anahtar / üst anahtar) kolonlarının eşleştirilmesi ile ölçek ve genişletme davranışı.
|
||||||
|
* Yalnızca Gantt layout'u seçiliyken gösterilir.
|
||||||
|
*/
|
||||||
|
const WizardStepGanttLayout = ({
|
||||||
|
selectCommandColumns,
|
||||||
|
translate,
|
||||||
|
onBack,
|
||||||
|
onNext,
|
||||||
|
}: WizardStepGanttLayoutProps) => {
|
||||||
const { values } = useFormikContext<ListFormWizardDto>()
|
const { values } = useFormikContext<ListFormWizardDto>()
|
||||||
const fieldOptions = useMemo<SelectBoxOption[]>(
|
const fieldOptions = useMemo<SelectBoxOption[]>(
|
||||||
() =>
|
() =>
|
||||||
|
|
@ -125,4 +135,4 @@ const WizardStep9 = ({ selectCommandColumns, translate, onBack, onNext }: Wizard
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default WizardStep9
|
export default WizardStepGanttLayout
|
||||||
|
|
@ -352,9 +352,9 @@ function MenuTreeInline({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── WizardStep1 ──────────────────────────────────────────────────────────────
|
// ─── WizardStepMenu ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export interface WizardStep1Props {
|
export interface WizardStepMenuProps {
|
||||||
values: ListFormWizardDto
|
values: ListFormWizardDto
|
||||||
errors: FormikErrors<ListFormWizardDto>
|
errors: FormikErrors<ListFormWizardDto>
|
||||||
touched: FormikTouched<ListFormWizardDto>
|
touched: FormikTouched<ListFormWizardDto>
|
||||||
|
|
@ -383,7 +383,12 @@ export interface WizardStep1Props {
|
||||||
translate: (key: string) => string
|
translate: (key: string) => string
|
||||||
}
|
}
|
||||||
|
|
||||||
const WizardStep1 = ({
|
/**
|
||||||
|
* Menü adımı: wizard adı, menü kodu/sırası, üst menü, ikon ve izin grubu burada belirlenir.
|
||||||
|
* Menü ağacını kurup ad/kod çakışmalarını adım geçilmeden önce doğrular; ürettiği menü
|
||||||
|
* kaydı hem List hem Custom yolunda deploy'un çıktısıdır.
|
||||||
|
*/
|
||||||
|
const WizardStepMenu = ({
|
||||||
values,
|
values,
|
||||||
errors,
|
errors,
|
||||||
touched,
|
touched,
|
||||||
|
|
@ -402,7 +407,7 @@ const WizardStep1 = ({
|
||||||
onBack,
|
onBack,
|
||||||
onNext,
|
onNext,
|
||||||
translate,
|
translate,
|
||||||
}: WizardStep1Props) => {
|
}: WizardStepMenuProps) => {
|
||||||
const [menuDialogOpen, setMenuDialogOpen] = useState(false)
|
const [menuDialogOpen, setMenuDialogOpen] = useState(false)
|
||||||
const [menuDialogParentCode, setMenuDialogParentCode] = useState('')
|
const [menuDialogParentCode, setMenuDialogParentCode] = useState('')
|
||||||
// Dolu ise dialog düzenleme modunda açılır.
|
// Dolu ise dialog düzenleme modunda açılır.
|
||||||
|
|
@ -745,4 +750,4 @@ const WizardStep1 = ({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default WizardStep1
|
export default WizardStepMenu
|
||||||
|
|
@ -7,14 +7,23 @@ import { useMemo } from 'react'
|
||||||
import { FaArrowLeft, FaArrowRight } from 'react-icons/fa'
|
import { FaArrowLeft, FaArrowRight } from 'react-icons/fa'
|
||||||
import { firstDayOfWeekOptions, schedulerViewOptions } from '../edit/options'
|
import { firstDayOfWeekOptions, schedulerViewOptions } from '../edit/options'
|
||||||
|
|
||||||
interface WizardStep10Props {
|
interface WizardStepSchedulerLayoutProps {
|
||||||
selectCommandColumns: DatabaseColumnDto[]
|
selectCommandColumns: DatabaseColumnDto[]
|
||||||
translate: (key: string) => string
|
translate: (key: string) => string
|
||||||
onBack: () => void
|
onBack: () => void
|
||||||
onNext: () => void
|
onNext: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const WizardStep10 = ({ selectCommandColumns, translate, onBack, onNext }: WizardStep10Props) => {
|
/**
|
||||||
|
* Takvim layout adımı: randevu başlığı, başlangıç/bitiş ve tüm gün gibi Scheduler
|
||||||
|
* görünümünün kolon eşleşmeleri. Yalnızca Scheduler layout'u seçiliyken gösterilir.
|
||||||
|
*/
|
||||||
|
const WizardStepSchedulerLayout = ({
|
||||||
|
selectCommandColumns,
|
||||||
|
translate,
|
||||||
|
onBack,
|
||||||
|
onNext,
|
||||||
|
}: WizardStepSchedulerLayoutProps) => {
|
||||||
const { values } = useFormikContext<ListFormWizardDto>()
|
const { values } = useFormikContext<ListFormWizardDto>()
|
||||||
const fieldOptions = useMemo<SelectBoxOption[]>(
|
const fieldOptions = useMemo<SelectBoxOption[]>(
|
||||||
() =>
|
() =>
|
||||||
|
|
@ -160,4 +169,4 @@ const WizardStep10 = ({ selectCommandColumns, translate, onBack, onNext }: Wizar
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default WizardStep10
|
export default WizardStepSchedulerLayout
|
||||||
|
|
@ -51,7 +51,12 @@ const createEmptySubForm = (): SubFormDto => ({
|
||||||
searchParams: new URLSearchParams(),
|
searchParams: new URLSearchParams(),
|
||||||
})
|
})
|
||||||
|
|
||||||
function WizardStep4({
|
/**
|
||||||
|
* Alt form adımı: ana kayda bağlı sekmelerde açılacak ListForm'ların (master-detail)
|
||||||
|
* tanımlandığı yer. Her satır bir sekme başlığı, bağlanacak ListForm kodu ve yenileme
|
||||||
|
* davranışı taşır.
|
||||||
|
*/
|
||||||
|
function WizardStepSubForms({
|
||||||
subForms,
|
subForms,
|
||||||
selectCommandColumns,
|
selectCommandColumns,
|
||||||
translate,
|
translate,
|
||||||
|
|
@ -481,4 +486,4 @@ function WizardStep4({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default WizardStep4
|
export default WizardStepSubForms
|
||||||
|
|
@ -7,7 +7,7 @@ import { Field, type FieldProps, useFormikContext } from 'formik'
|
||||||
import { useMemo, useState } from 'react'
|
import { useMemo, useState } from 'react'
|
||||||
import { FaArrowLeft, FaArrowRight } from 'react-icons/fa'
|
import { FaArrowLeft, FaArrowRight } from 'react-icons/fa'
|
||||||
|
|
||||||
interface WizardStep7Props {
|
interface WizardStepTodoLayoutProps {
|
||||||
selectedColumns: Set<string>
|
selectedColumns: Set<string>
|
||||||
selectCommandColumns: DatabaseColumnDto[]
|
selectCommandColumns: DatabaseColumnDto[]
|
||||||
translate: (key: string) => string
|
translate: (key: string) => string
|
||||||
|
|
@ -54,13 +54,17 @@ const mappings: Array<{
|
||||||
{ name: 'orderExpr', translationKey: '::App.ListFormFieldEdit.TodoOrderExpr' },
|
{ name: 'orderExpr', translationKey: '::App.ListFormFieldEdit.TodoOrderExpr' },
|
||||||
]
|
]
|
||||||
|
|
||||||
const WizardStep7 = ({
|
/**
|
||||||
|
* Todo / Kanban layout adımı: kart başlığı, durum kolonu, sıra alanı gibi Kanban görünümünün
|
||||||
|
* ihtiyaç duyduğu kolon eşleşmeleri. Yalnızca Todo layout'u seçiliyken gösterilir.
|
||||||
|
*/
|
||||||
|
const WizardStepTodoLayout = ({
|
||||||
selectedColumns,
|
selectedColumns,
|
||||||
selectCommandColumns,
|
selectCommandColumns,
|
||||||
translate,
|
translate,
|
||||||
onBack,
|
onBack,
|
||||||
onNext,
|
onNext,
|
||||||
}: WizardStep7Props) => {
|
}: WizardStepTodoLayoutProps) => {
|
||||||
const { values, setFieldValue } = useFormikContext<ListFormWizardDto>()
|
const { values, setFieldValue } = useFormikContext<ListFormWizardDto>()
|
||||||
const [validationError, setValidationError] = useState('')
|
const [validationError, setValidationError] = useState('')
|
||||||
const fieldOptions = useMemo<SelectBoxOption[]>(
|
const fieldOptions = useMemo<SelectBoxOption[]>(
|
||||||
|
|
@ -160,4 +164,4 @@ const WizardStep7 = ({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default WizardStep7
|
export default WizardStepTodoLayout
|
||||||
|
|
@ -6,14 +6,23 @@ import { Field, type FieldProps, useFormikContext } from 'formik'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { FaArrowLeft, FaArrowRight } from 'react-icons/fa'
|
import { FaArrowLeft, FaArrowRight } from 'react-icons/fa'
|
||||||
|
|
||||||
interface WizardStep8Props {
|
interface WizardStepTreeLayoutProps {
|
||||||
selectCommandColumns: DatabaseColumnDto[]
|
selectCommandColumns: DatabaseColumnDto[]
|
||||||
translate: (key: string) => string
|
translate: (key: string) => string
|
||||||
onBack: () => void
|
onBack: () => void
|
||||||
onNext: () => void
|
onNext: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const WizardStep8 = ({ selectCommandColumns, translate, onBack, onNext }: WizardStep8Props) => {
|
/**
|
||||||
|
* Ağaç layout adımı: kayıtları hiyerarşik gösterebilmek için anahtar ve üst anahtar
|
||||||
|
* kolonlarının eşleştirilmesi. Yalnızca Tree layout'u seçiliyken gösterilir.
|
||||||
|
*/
|
||||||
|
const WizardStepTreeLayout = ({
|
||||||
|
selectCommandColumns,
|
||||||
|
translate,
|
||||||
|
onBack,
|
||||||
|
onNext,
|
||||||
|
}: WizardStepTreeLayoutProps) => {
|
||||||
const { values } = useFormikContext<ListFormWizardDto>()
|
const { values } = useFormikContext<ListFormWizardDto>()
|
||||||
const fieldOptions = useMemo<SelectBoxOption[]>(
|
const fieldOptions = useMemo<SelectBoxOption[]>(
|
||||||
() =>
|
() =>
|
||||||
|
|
@ -85,4 +94,4 @@ const WizardStep8 = ({ selectCommandColumns, translate, onBack, onNext }: Wizard
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default WizardStep8
|
export default WizardStepTreeLayout
|
||||||
|
|
@ -54,7 +54,11 @@ const emptyWidget: WidgetEditDto = {
|
||||||
isActive: true,
|
isActive: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
function WizardStep5({ widgets, translate, onChange, onBack, onNext }: Props) {
|
/**
|
||||||
|
* Widget adımı: listenin üstünde gösterilecek özet kartlarının (SQL sorgusu, kolon genişliği,
|
||||||
|
* stil sınıfı) tanımlandığı yer.
|
||||||
|
*/
|
||||||
|
function WizardStepWidgets({ widgets, translate, onChange, onBack, onNext }: Props) {
|
||||||
const [dialogIndex, setDialogIndex] = useState<number | null>(null)
|
const [dialogIndex, setDialogIndex] = useState<number | null>(null)
|
||||||
const [deleteIndex, setDeleteIndex] = useState<number | null>(null)
|
const [deleteIndex, setDeleteIndex] = useState<number | null>(null)
|
||||||
|
|
||||||
|
|
@ -352,4 +356,4 @@ function WizardStep5({ widgets, translate, onChange, onBack, onNext }: Props) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default WizardStep5
|
export default WizardStepWidgets
|
||||||
|
|
@ -49,7 +49,11 @@ const toDesignerCriteria = (items: ListFormWorkflowCriteriaDto[]): WorkflowCrite
|
||||||
const toWizardCriteria = (items: WorkflowCriteriaDto[]): ListFormWorkflowCriteriaDto[] =>
|
const toWizardCriteria = (items: WorkflowCriteriaDto[]): ListFormWorkflowCriteriaDto[] =>
|
||||||
items.map(({ ...item }) => item)
|
items.map(({ ...item }) => item)
|
||||||
|
|
||||||
function WizardStep6({
|
/**
|
||||||
|
* Onay akışı adımı: kaydın hangi durumlarda hangi role/kullanıcıya düşeceğini belirleyen
|
||||||
|
* workflow düğümleri ve kriterleri burada kurgulanır.
|
||||||
|
*/
|
||||||
|
function WizardStepWorkflow({
|
||||||
listFormCode,
|
listFormCode,
|
||||||
workflow,
|
workflow,
|
||||||
criteria,
|
criteria,
|
||||||
|
|
@ -302,18 +306,12 @@ function WizardStep6({
|
||||||
<FormContainer>
|
<FormContainer>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-6 gap-4">
|
<div className="grid grid-cols-1 md:grid-cols-6 gap-4">
|
||||||
{[
|
{[
|
||||||
[
|
['approvalUserFieldName', '::App.ListFormEditWorkflow.ApprovalUserFieldName'],
|
||||||
'approvalUserFieldName',
|
|
||||||
'::App.ListFormEditWorkflow.ApprovalUserFieldName',
|
|
||||||
],
|
|
||||||
[
|
[
|
||||||
'approvalStatusFieldName',
|
'approvalStatusFieldName',
|
||||||
'::App.ListFormEditWorkflow.ApprovalStatusFieldName',
|
'::App.ListFormEditWorkflow.ApprovalStatusFieldName',
|
||||||
],
|
],
|
||||||
[
|
['approvalDateFieldName', '::App.ListFormEditWorkflow.ApprovalDateFieldName'],
|
||||||
'approvalDateFieldName',
|
|
||||||
'::App.ListFormEditWorkflow.ApprovalDateFieldName',
|
|
||||||
],
|
|
||||||
[
|
[
|
||||||
'approvalDescriptionFieldName',
|
'approvalDescriptionFieldName',
|
||||||
'::App.ListFormEditWorkflow.ApprovalDescriptionFieldName',
|
'::App.ListFormEditWorkflow.ApprovalDescriptionFieldName',
|
||||||
|
|
@ -360,9 +358,7 @@ function WizardStep6({
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
<FormItem
|
<FormItem
|
||||||
label={translate(
|
label={translate('::App.ListFormEditWorkflow.ApprovalIsFilterUserName')}
|
||||||
'::App.ListFormEditWorkflow.ApprovalIsFilterUserName',
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<Field name="approvalIsFilterUserName">
|
<Field name="approvalIsFilterUserName">
|
||||||
{({ field, form }: FieldProps<boolean>) => (
|
{({ field, form }: FieldProps<boolean>) => (
|
||||||
|
|
@ -489,4 +485,4 @@ function WizardStep6({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default WizardStep6
|
export default WizardStepWorkflow
|
||||||
|
|
@ -5,6 +5,8 @@ import { useLocalization } from '@/utils/hooks/useLocalization'
|
||||||
import { Formik, Form, Field } from 'formik'
|
import { Formik, Form, Field } from 'formik'
|
||||||
import * as Yup from 'yup'
|
import * as Yup from 'yup'
|
||||||
import { Button, Checkbox, FormContainer, FormItem, Input } from '@/components/ui'
|
import { Button, Checkbox, FormContainer, FormItem, Input } from '@/components/ui'
|
||||||
|
import { usePermission } from '@/utils/hooks/usePermission'
|
||||||
|
import { COMPONENT_PERMISSION } from '@/constants/permission.constant'
|
||||||
import Dialog from '@/components/ui/Dialog'
|
import Dialog from '@/components/ui/Dialog'
|
||||||
import { generateDesignerCode } from '@/components/visualDesigner/codeGenerator'
|
import { generateDesignerCode } from '@/components/visualDesigner/codeGenerator'
|
||||||
import {
|
import {
|
||||||
|
|
@ -42,6 +44,12 @@ const ComponentEditor: React.FC<ComponentEditorProps> = ({
|
||||||
|
|
||||||
const isEditing = !!id
|
const isEditing = !!id
|
||||||
|
|
||||||
|
// Kaydetme hakki moda gore degisir: mevcut kayit guncelleme, yeni kayit olusturma yetkisi ister.
|
||||||
|
const { checkPermission } = usePermission()
|
||||||
|
const canSave = checkPermission(
|
||||||
|
isEditing ? COMPONENT_PERMISSION.UPDATE : COMPONENT_PERMISSION.CREATE,
|
||||||
|
)
|
||||||
|
|
||||||
// Initial values for Formik
|
// Initial values for Formik
|
||||||
const [initialValues, setInitialValues] = useState({
|
const [initialValues, setInitialValues] = useState({
|
||||||
name: '',
|
name: '',
|
||||||
|
|
@ -270,7 +278,7 @@ const ComponentEditor: React.FC<ComponentEditorProps> = ({
|
||||||
type="button"
|
type="button"
|
||||||
variant="solid"
|
variant="solid"
|
||||||
size="sm"
|
size="sm"
|
||||||
disabled={isSubmitting || !values.name.trim() || !isValid}
|
disabled={!canSave || isSubmitting || !values.name.trim() || !isValid}
|
||||||
className="flex items-center gap-2"
|
className="flex items-center gap-2"
|
||||||
onClick={submitForm}
|
onClick={submitForm}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,8 @@ import { Button } from '@/components/ui'
|
||||||
import Dialog from '@/components/ui/Dialog'
|
import Dialog from '@/components/ui/Dialog'
|
||||||
import ComponentEditor from './ComponentEditor'
|
import ComponentEditor from './ComponentEditor'
|
||||||
import { parseComponentDependencies } from '@/contexts/componentRuntime'
|
import { parseComponentDependencies } from '@/contexts/componentRuntime'
|
||||||
|
import { usePermission } from '@/utils/hooks/usePermission'
|
||||||
|
import { COMPONENT_PERMISSION } from '@/constants/permission.constant'
|
||||||
|
|
||||||
const ComponentManager: React.FC = () => {
|
const ComponentManager: React.FC = () => {
|
||||||
const {
|
const {
|
||||||
|
|
@ -42,6 +44,10 @@ const ComponentManager: React.FC = () => {
|
||||||
const activeComponents = components?.filter((c) => c.isActive).length || 0
|
const activeComponents = components?.filter((c) => c.isActive).length || 0
|
||||||
const inactiveComponents = totalComponents - activeComponents
|
const inactiveComponents = totalComponents - activeComponents
|
||||||
const { translate } = useLocalization()
|
const { translate } = useLocalization()
|
||||||
|
const { checkPermission } = usePermission()
|
||||||
|
const canCreate = checkPermission(COMPONENT_PERMISSION.CREATE)
|
||||||
|
const canUpdate = checkPermission(COMPONENT_PERMISSION.UPDATE)
|
||||||
|
const canDelete = checkPermission(COMPONENT_PERMISSION.DELETE)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
void refreshComponents()
|
void refreshComponents()
|
||||||
|
|
@ -159,6 +165,7 @@ const ComponentManager: React.FC = () => {
|
||||||
<FaSyncAlt className={isRefreshing ? 'animate-spin' : ''} />
|
<FaSyncAlt className={isRefreshing ? 'animate-spin' : ''} />
|
||||||
{translate('::App.Platform.Refresh')}
|
{translate('::App.Platform.Refresh')}
|
||||||
</Button>
|
</Button>
|
||||||
|
{canCreate && (
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
@ -170,6 +177,7 @@ const ComponentManager: React.FC = () => {
|
||||||
{translate('::App.DeveloperKitComponent.New')}
|
{translate('::App.DeveloperKitComponent.New')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Components List */}
|
{/* Components List */}
|
||||||
|
|
@ -244,6 +252,7 @@ const ComponentManager: React.FC = () => {
|
||||||
type="button"
|
type="button"
|
||||||
variant="plain"
|
variant="plain"
|
||||||
shape="circle"
|
shape="circle"
|
||||||
|
disabled={!canUpdate}
|
||||||
className={`!inline-flex !h-auto items-center gap-1 rounded !px-2 py-1 text-xs font-medium transition-colors`}
|
className={`!inline-flex !h-auto items-center gap-1 rounded !px-2 py-1 text-xs font-medium transition-colors`}
|
||||||
onClick={() => handleToggleActive(component.id, !component.isActive)}
|
onClick={() => handleToggleActive(component.id, !component.isActive)}
|
||||||
>
|
>
|
||||||
|
|
@ -261,6 +270,7 @@ const ComponentManager: React.FC = () => {
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
|
{canUpdate && (
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="solid"
|
variant="solid"
|
||||||
|
|
@ -278,6 +288,7 @@ const ComponentManager: React.FC = () => {
|
||||||
>
|
>
|
||||||
<FaRegEdit className="w-4 h-4" />
|
<FaRegEdit className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="solid"
|
variant="solid"
|
||||||
|
|
@ -302,6 +313,7 @@ const ComponentManager: React.FC = () => {
|
||||||
>
|
>
|
||||||
<FaCog className="w-4 h-4" />
|
<FaCog className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
{canDelete && (
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="solid"
|
variant="solid"
|
||||||
|
|
@ -311,6 +323,7 @@ const ComponentManager: React.FC = () => {
|
||||||
>
|
>
|
||||||
<FaTrashAlt className="w-4 h-4" />
|
<FaTrashAlt className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -333,7 +346,7 @@ const ComponentManager: React.FC = () => {
|
||||||
? translate('::App.DeveloperKit.EmptyFilteredDescription')
|
? translate('::App.DeveloperKit.EmptyFilteredDescription')
|
||||||
: translate('::App.EmptyInitial.InitialDescription')}
|
: translate('::App.EmptyInitial.InitialDescription')}
|
||||||
</p>
|
</p>
|
||||||
{!searchTerm && filterActive === 'all' && (
|
{canCreate && !searchTerm && filterActive === 'all' && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="inline-flex items-center gap-2 bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors"
|
className="inline-flex items-center gap-2 bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors"
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ import {
|
||||||
MenuTreeNode,
|
MenuTreeNode,
|
||||||
buildMenuTree,
|
buildMenuTree,
|
||||||
filterNonLinkNodes,
|
filterNonLinkNodes,
|
||||||
} from '@/views/admin/listForm/wizard/WizardStep1'
|
} from '@/views/admin/listForm/wizard/WizardStepMenu'
|
||||||
import { MenuAddDialog } from '../shared/MenuAddDialog'
|
import { MenuAddDialog } from '../shared/MenuAddDialog'
|
||||||
import { useStoreActions, useStoreState } from '@/store'
|
import { useStoreActions, useStoreState } from '@/store'
|
||||||
import { usePermission } from '@/utils/hooks/usePermission'
|
import { usePermission } from '@/utils/hooks/usePermission'
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue