Seeds klasörü path değiştirildi.

This commit is contained in:
Sedat ÖZTÜRK 2026-08-17 08:56:52 +03:00
parent 39ad5cab12
commit c993b77b5d
42 changed files with 621 additions and 235 deletions

View file

@ -178,6 +178,8 @@ sozsoft-platform/
├── configs/
│ ├── docker/ # Yerel geliştirme için veri katmanı compose dosyaları ve scriptler
│ ├── deployment/ # Dev/Prod compose dosyaları, nginx/redis/turn konfigürasyonu, deploy scriptleri
│ ├── seeds/ # Runtime'da düzenlenebilen seed'ler: SqlData, PostgresData, WizardData
│ │ # {SqlData|PostgresData}/{HostData|TenantData}/{ObjectData|ExecuteData}
│ └── ai/ # AI entegrasyonu (n8n akış tanımları)
├── .github/instructions/ # ai.instructions.md (bağlayıcı platform kuralları), list.instructions.md
├── CLAUDE.md # Claude Code için operasyon kuralları
@ -233,7 +235,20 @@ dotnet run --project api/src/Sozsoft.Platform.DbMigrator
DbMigrator; şema migration'larını uygular ve ardından menü, route, permission, dil,
host ayarları, ülke/şehir/ilçe verileri ile seed edilmiş ListForm ekranlarını yükler
(`api/src/Sozsoft.Platform.DbMigrator/Seeds/`).
(kodla versiyonlanan sabit seed'ler `api/src/Sozsoft.Platform.DbMigrator/Migrations/`,
runtime'da düzenlenebilen seed'ler `configs/seeds/` altındadır — bkz. `App:SeedsPath`).
`configs/seeds/{SqlData|PostgresData}` altındaki her `.sql` dosyası, önce veritabanı
kapsamına (`HostData` / `TenantData`), sonra amacına göre bir klasörde durur; klasör
köklerinde dosya bulunmaz:
| Klasör | Ne zaman çalışır |
| --- | --- |
| `{HostData\|TenantData}/ObjectData` | Seed sırasında nesneyi oluşturur/günceller. SqlTableDesigner ve SqlQueryManager, deploy ettiği script'i çalıştığı kapsamın bu klasörüne yazar (host bağlamı`HostData`, tenant seçiliyken → `TenantData`). |
| `{HostData\|TenantData}/ExecuteData` | Nesneyi oluşturur; ayrıca tüm migration ve seed'ler bittikten sonra `AfterAllMigrationsSqlExecutor` dosya adından türettiği procedure'ü çalıştırır (dosya adı = procedure adı, `schema.Procedure.sql` de olur). |
Klasör kökünde kalmış eski `.sql` dosyaları geriye dönük uyumluluk için hâlâ işlenir,
fakat `SqlDataSeeder` bunları uyarı ile loglar — ilgili klasöre taşınmaları beklenir.
### 5.4. Frontend
@ -829,7 +844,7 @@ Yeni ABP modülü eklemek için:
Yeni bir modülün nasıl kurulacağını öğrenmek için başvurulacak dosyalar:
- `api/src/Sozsoft.Platform.DbMigrator/Seeds/ListFormSeeder_Saas.cs`
- `api/src/Sozsoft.Platform.DbMigrator/Migrations/ListFormSeeder_Saas.cs`
- `.../Migrations/ListFormSeeder_Administration.cs`
- `.../Migrations/MenusData.json`
- `.../Migrations/PermissionsData.json`

View file

@ -1,4 +1,4 @@
using System.Collections.Generic;
using System.Collections.Generic;
using System.Threading.Tasks;
using Volo.Abp.Application.Services;
@ -30,13 +30,15 @@ public interface ISqlObjectManagerAppService : IApplicationService
Task<string> GetNativeObjectDefinitionAsync(string dataSourceCode, string schemaName, string objectName);
/// <summary>
/// Saves the T-SQL script to Seeds/SqlData/{fileName}.sql in the DbMigrator project.
/// Saves the T-SQL script to {SeedsRoot}/{SqlData|PostgresData}/{HostData|TenantData}/ObjectData/{fileName}.sql
/// (repo: configs/seeds). The scope folder follows the current tenant: host context writes to HostData.
/// Called automatically after a successful SqlTableDesigner deploy so the script can be re-seeded.
/// </summary>
Task SaveTableScriptAsync(SaveTableScriptDto input);
/// <summary>
/// Deletes matching SQL seed files from Seeds/SqlData when objects are dropped from the UI.
/// Deletes matching SQL seed files from the current scope's ObjectData folder when objects are
/// dropped from the UI.
/// Non-existing files are ignored.
/// </summary>
Task DeleteSqlDataFilesAsync(DeleteSqlDataFilesDto input);

View file

@ -20,7 +20,7 @@ public class SqlQueryExecutionResultDto
}
/// <summary>
/// Input for saving a T-SQL script file to DbMigrator Seeds/SqlData.
/// Input for saving a T-SQL script file to the seeds folder ({App:SeedsPath}/SqlData).
/// </summary>
public class SaveTableScriptDto
{
@ -36,7 +36,7 @@ public class SaveTableScriptDto
}
/// <summary>
/// Input for deleting seed files from DbMigrator Seeds/SqlData.
/// Input for deleting seed files from the seeds folder ({App:SeedsPath}/SqlData).
/// </summary>
public class DeleteSqlDataFilesDto
{

View file

@ -8,13 +8,15 @@ using Sozsoft.SqlQueryManager.Domain.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Volo.Abp.Application.Services;
using Volo.Abp.MultiTenancy;
using System.Text.RegularExpressions;
using Sozsoft.Platform.Data.Seeds;
using Sozsoft.Platform.Enums;
using Sozsoft.Platform.Queries;
using Sozsoft.Settings;
namespace Sozsoft.SqlQueryManager.Application;
@ -48,7 +50,7 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA
private readonly IDataSourceManager _dataSourceManager;
private readonly ICurrentTenant _currentTenant;
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly IHostEnvironment _hostEnvironment;
private readonly IConfiguration _configuration;
private readonly ILogger<SqlObjectManagerAppService> _logger;
public SqlObjectManagerAppService(
@ -57,7 +59,7 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA
IDataSourceManager dataSourceManager,
ICurrentTenant currentTenant,
IHttpContextAccessor httpContextAccessor,
IHostEnvironment hostEnvironment,
IConfiguration configuration,
ILogger<SqlObjectManagerAppService> logger)
{
_sqlExecutorService = sqlExecutorService;
@ -65,7 +67,7 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA
_dataSourceManager = dataSourceManager;
_currentTenant = currentTenant;
_httpContextAccessor = httpContextAccessor;
_hostEnvironment = hostEnvironment;
_configuration = configuration;
_logger = logger;
}
@ -955,38 +957,31 @@ FROM (
}
/// <summary>
/// Script dosyalarinin yazilacagi klasor. SqlDataSeeder ile ayni cozumleyiciyi kullanir
/// (App:SeedsPath -> configs/seeds), aksi halde deploy edilen nesne seed olarak geri okunmaz.
/// <para>
/// Yol, islemin yapildigi veritabani kapsamini izler: host (varsayilan) baglaminda
/// <c>{SqlData|PostgresData}/HostData/ObjectData</c>, bir tenant secilmisken
/// <c>{SqlData|PostgresData}/TenantData/ObjectData</c>. Boylece nesne, seed sirasinda
/// olusturuldugu veritabaninda yeniden olusturulur.
/// </para>
/// </summary>
private string ResolveSqlDataOutputPath()
{
return ResolveSqlDataOutputPath("SqlData");
}
var dataDirectoryName = SettingsConsts.DefaultDatabaseProvider == SettingsConsts.DatabaseProvider.PostgreSql
? SeedPathResolver.PostgresDataFolder
: SeedPathResolver.SqlDataFolder;
private string ResolveSqlDataOutputPath(string dataDirectoryName)
{
const string dbMigratorName = "Sozsoft.Platform.DbMigrator";
var safeDirectoryName = NormalizeSqlDataDirectoryName(dataDirectoryName);
var dir = new DirectoryInfo(_hostEnvironment.ContentRootPath);
var scopeDirectoryName = _currentTenant.Id == null
? SeedPathResolver.HostDataFolder
: SeedPathResolver.TenantDataFolder;
while (dir != null)
{
var candidate = Path.Combine(dir.FullName, "src", dbMigratorName, "Seeds");
if (Directory.Exists(candidate))
return Path.Combine(candidate, safeDirectoryName);
candidate = Path.Combine(dir.FullName, dbMigratorName, "Seeds");
if (Directory.Exists(candidate))
return Path.Combine(candidate, safeDirectoryName);
dir = dir.Parent;
}
return Path.Combine(_hostEnvironment.ContentRootPath, "Seeds", safeDirectoryName);
}
private static string NormalizeSqlDataDirectoryName(string dataDirectoryName)
{
return string.Equals(dataDirectoryName, "PostgresData", StringComparison.OrdinalIgnoreCase)
? "PostgresData"
: "SqlData";
return SeedPathResolver.GetPath(
_configuration,
dataDirectoryName,
scopeDirectoryName,
SeedPathResolver.ObjectDataFolder);
}
}

View file

@ -8,6 +8,7 @@ using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using Sozsoft.Platform.BlobStoring;
using Sozsoft.Platform.Data.Seeds;
using Sozsoft.Platform.Permissions;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
@ -32,13 +33,11 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
private const string IndexFileName = "index.json";
/// <summary>
/// CDN altinda fiziksel karsiligi olmayan, DbMigrator projesinin Seeds klasorune baglanan
/// sanal kok klasor. Sadece host baglaminda ve <see cref="PlatformPermissions.Setup.Migrate"/>
/// CDN altinda fiziksel karsiligi olmayan, mount edilen seed klasorune baglanan sanal kok
/// klasor. Sadece host baglaminda ve <see cref="PlatformPermissions.Setup.Migrate"/>
/// yetkisi olan kullaniciya gorunur.
/// </summary>
private const string SeedsFolderName = "Seeds";
private const string DbMigratorProjectName = "Sozsoft.Platform.DbMigrator";
private const string SeedsFolderName = "seeds";
private static readonly HashSet<string> ProtectedFolders = new(StringComparer.OrdinalIgnoreCase)
{
@ -93,42 +92,20 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
return string.Equals(firstPart, SeedsFolderName, StringComparison.OrdinalIgnoreCase);
}
/// <summary>Host + migration yetkisi olmadan DbMigrator Seeds klasoru ne listelenir ne de degistirilir.</summary>
/// <summary>Host + migration yetkisi olmadan seed klasoru ne listelenir ne de degistirilir.</summary>
private async Task<bool> CanAccessSeedsAsync(string tenantId)
{
return IsHostTenant(tenantId) &&
await AuthorizationService.IsGrantedAsync(PlatformPermissions.Setup.Migrate);
}
/// <summary>DbMigrator projesinin Seeds klasorunu bulur (config ile override edilebilir).</summary>
/// <summary>
/// Seed klasorunu bulur. Yol cozumu seeder'lar ile ortaktir; boylece File Manager'dan
/// duzenlenen dosyayi migrate isleminde ayni klasorden okuruz.
/// </summary>
private string ResolveSeedsRootPath()
{
var configuredPath = _configuration["App:DbMigratorSeedsPath"];
if (!string.IsNullOrWhiteSpace(configuredPath))
{
return Path.GetFullPath(configuredPath);
}
var dir = new DirectoryInfo(_hostEnvironment.ContentRootPath);
while (dir != null)
{
var candidate = Path.Combine(dir.FullName, "src", DbMigratorProjectName, "Seeds");
if (Directory.Exists(candidate))
{
return candidate;
}
candidate = Path.Combine(dir.FullName, DbMigratorProjectName, "Seeds");
if (Directory.Exists(candidate))
{
return candidate;
}
dir = dir.Parent;
}
return Path.Combine(_hostEnvironment.ContentRootPath, "Seeds");
return SeedPathResolver.GetRootPath(_configuration);
}
/// <summary>
@ -394,13 +371,13 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
}
}
/// <summary>Kok listeye DbMigrator Seeds klasorunu sanal klasor olarak ekler.</summary>
/// <summary>Kok listeye seed klasorunu sanal klasor olarak ekler.</summary>
private void AddSeedsFolderItem(List<FileMetadata> items, string tenantId)
{
var seedsRootPath = ResolveSeedsRootPath();
if (!Directory.Exists(seedsRootPath))
{
Logger.LogWarning("DbMigrator seeds directory was not found: {SeedsPath}", seedsRootPath);
Logger.LogWarning("Seeds directory was not found: {SeedsPath}", seedsRootPath);
return;
}

View file

@ -13,8 +13,10 @@ using Volo.Abp.Domain.Repositories;
using Volo.Abp.MultiTenancy;
using Volo.Abp.PermissionManagement;
using Volo.Abp.Uow;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Sozsoft.Languages;
using Sozsoft.Platform.Data.Seeds;
using Sozsoft.Platform.DynamicData;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Logging;
@ -39,6 +41,7 @@ public class ListFormWizardAppService(
IRepository<Menu, Guid> repoMenu,
IPermissionGrantRepository permissionGrantRepository,
IHostEnvironment hostEnvironment,
IConfiguration configuration,
LanguageTextAppService languageTextAppService,
IDynamicDataManager dynamicDataManager,
ILogger<ListFormWizardAppService> logger
@ -55,6 +58,7 @@ public class ListFormWizardAppService(
private readonly IRepository<Menu, Guid> repoMenu = repoMenu;
private readonly IPermissionGrantRepository permissionGrantRepository = permissionGrantRepository;
private readonly IHostEnvironment _hostEnvironment = hostEnvironment;
private readonly IConfiguration _configuration = configuration;
private readonly LanguageTextAppService _languageTextAppService = languageTextAppService;
private readonly IDynamicDataManager _dynamicDataManager = dynamicDataManager;
private readonly ILogger<ListFormWizardAppService> logger = logger;
@ -531,8 +535,7 @@ public class ListFormWizardAppService(
/// <summary>
/// Wizard konfigürasyonunu JSON dosyası olarak kaydeder.
/// Önce ContentRootPath'ten yukarı çıkarak Sozsoft.Platform.DbMigrator/Seeds/WizardData dizinini arar.
/// Bulamazsa ContentRootPath/Seeds/WizardData altına yazar.
/// Dosya, seeder'lar ve File Manager ile ortak olan seed klasörünün WizardData dizinine yazılır.
/// Veritabanı silinip yeniden oluşturulduğunda WizardDataSeeder bu dosyaları okuyarak konfigürasyonu geri yükler.
/// </summary>
private async Task SaveWizardSeedFileAsync(ListFormWizardDto input, bool isDeletedField, bool isCreatedField, WizardInsertedRecordsDto inserted)
@ -755,31 +758,12 @@ public class ListFormWizardAppService(
}
/// <summary>
/// DbMigrator projesinin Seeds/WizardData dizinini ContentRootPath'ten yukarı traversal ile bulur.
/// Tüm işletim sistemlerinde Path.Combine kullanır, separator karakteri içermez.
/// Wizard seed dosyalarinin yazilacagi WizardData dizinini dondurur.
/// Yol cozumu <see cref="SeedPathResolver"/> uzerinden seeder'lar ve File Manager ile ortaktir.
/// </summary>
private string ResolveWizardSeedOutputPath()
{
const string dbMigratorName = "Sozsoft.Platform.DbMigrator";
var dir = new DirectoryInfo(_hostEnvironment.ContentRootPath);
while (dir != null)
{
// src/Sozsoft.Platform.DbMigrator/Seeds altında ara
var candidate = Path.Combine(dir.FullName, "src", dbMigratorName, "Seeds");
if (Directory.Exists(candidate))
return Path.Combine(candidate, "WizardData");
// Sozsoft.Platform.DbMigrator/Seeds doğrudan altında ara
candidate = Path.Combine(dir.FullName, dbMigratorName, "Seeds");
if (Directory.Exists(candidate))
return Path.Combine(candidate, "WizardData");
dir = dir.Parent;
}
// Fallback: çalışan API'nin yanında Seeds/WizardData
return Path.Combine(_hostEnvironment.ContentRootPath, "Seeds", "WizardData");
return SeedPathResolver.GetPath(_configuration, "WizardData");
}
private async Task<HashSet<string>> GetTableColumnNamesAsync(string dataSourceCode, SelectCommandTypeEnum commandType, string selectCommand)

View file

@ -308,11 +308,11 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
var start = context.Criteria.FirstOrDefault(x => x.Kind == "Start")
?? throw new UserFriendlyException("Workflow başlangıç adımı bulunamadı.");
context.WorkflowNoteRows.Add(("Started By: ", ResolveCurrentUserDisplayName()));
context.WorkflowNoteRows.Add((NoteLabel("App.WorkflowNote.StartedBy", "Started By"), ResolveCurrentUserDisplayName()));
var result = await RunUntilWaitAsync(context, start);
await InsertWorkflowNoteAsync(
context,
$"Workflow Started: {start.Title}",
$"{Localize("App.WorkflowNote.SubjectStarted", "Workflow Started")}: {LocalizeNodeTitle(start.Title)}",
BuildWorkflowNoteContent(context.WorkflowNoteRows));
return result;
@ -362,16 +362,16 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
await UpdateRowAsync(context, clear);
MergeRowValues(context.Row, clear);
context.WorkflowNoteRows.Add(("Reset By: ", ResolveCurrentUserDisplayName()));
context.WorkflowNoteRows.Add((NoteLabel("App.WorkflowNote.ResetBy", "Reset By"), ResolveCurrentUserDisplayName()));
if (!previousStatus.IsNullOrWhiteSpace())
{
context.WorkflowNoteRows.Add(("Reset Step: ", previousStatus));
context.WorkflowNoteRows.Add((NoteLabel("App.WorkflowNote.ResetStep", "Reset Step"), LocalizeNodeTitle(previousStatus)));
}
var result = await RunUntilWaitAsync(context, start);
await InsertWorkflowNoteAsync(
context,
$"Workflow Reset: {start.Title}",
$"{Localize("App.WorkflowNote.SubjectReset", "Workflow Reset")}: {LocalizeNodeTitle(start.Title)}",
BuildWorkflowNoteContent(context.WorkflowNoteRows));
return result;
@ -443,12 +443,15 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
}
var next = FindNextCriteria(context.Criteria, input.Approved ? current.NextOnApprove : current.NextOnReject);
context.WorkflowNoteRows.Add(("Decision By: ", ResolveCurrentUserDisplayName()));
context.WorkflowNoteRows.Add((NoteLabel("App.WorkflowNote.DecisionBy", "Decision By"), ResolveCurrentUserDisplayName()));
AddWorkflowDecisionRows(context, current, input.Approved, input.Note ?? string.Empty);
var result = await RunUntilWaitAsync(context, next);
var decisionSubject = input.Approved
? Localize("App.WorkflowNote.SubjectApproved", "Workflow Approved")
: Localize("App.WorkflowNote.SubjectRejected", "Workflow Rejected");
await InsertWorkflowNoteAsync(
context,
$"Workflow {(input.Approved ? "Approved" : "Rejected")}: {current.Title}",
$"{decisionSubject}: {LocalizeNodeTitle(current.Title)}",
BuildWorkflowNoteContent(context.WorkflowNoteRows));
return result;
@ -790,36 +793,41 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
?? "System";
}
private static void AddWorkflowDecisionRows(
private void AddWorkflowDecisionRows(
WorkflowRunContext context,
ListFormWorkflow current,
bool approved,
string description)
{
var action = approved ? "Approved" : "Rejected";
context.WorkflowNoteRows.Add(("Decision: ", $"{action}: {FormatNode(current)}"));
context.WorkflowNoteRows.Add(("Description: ", description ?? string.Empty));
var action = approved
? Localize("App.WorkflowNote.Approved", "Approved")
: Localize("App.WorkflowNote.Rejected", "Rejected");
context.WorkflowNoteRows.Add((NoteLabel("App.WorkflowNote.Decision", "Decision"), $"{action}: {FormatNode(current)}"));
context.WorkflowNoteRows.Add((NoteLabel("App.WorkflowNote.Description", "Description"), description ?? string.Empty));
}
private static void AddWorkflowNodeRows(
private void AddWorkflowNodeRows(
WorkflowRunContext context,
ListFormWorkflow node)
{
var action = node.Kind switch
{
"Start" => "Started",
"Compare" => "Evaluated",
"Approval" => "Waiting Approval",
"Inform" => "Informed",
"End" => "Completed",
_ => "Processed"
"Start" => Localize("App.WorkflowNote.Started", "Started"),
"Compare" => Localize("App.WorkflowNote.Evaluated", "Evaluated"),
"Approval" => Localize("App.WorkflowNote.WaitingApproval", "Waiting Approval"),
"Inform" => Localize("App.WorkflowNote.Informed", "Informed"),
"End" => Localize("App.WorkflowNote.Completed", "Completed"),
_ => Localize("App.WorkflowNote.Processed", "Processed")
};
context.WorkflowNoteRows.Add(($"{action}: ", $"{FormatNode(node)}"));
if (!node.Approver.IsNullOrWhiteSpace())
{
context.WorkflowNoteRows.Add((node.Kind == "Inform" ? "Inform: " : "Approver: ", FormatWorkflowApprovers(node.Approver)));
var approverLabel = node.Kind == "Inform"
? NoteLabel("App.WorkflowNote.Inform", "Inform")
: NoteLabel("App.WorkflowNote.Approver", "Approver");
context.WorkflowNoteRows.Add((approverLabel, FormatWorkflowApprovers(node.Approver)));
}
}
@ -955,16 +963,71 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
return $"<table class=\"workflow-note-log\">{string.Join(string.Empty, tableRows)}</table>";
}
private static string FormatNode(ListFormWorkflow node)
private string FormatNode(ListFormWorkflow node)
{
if (node == null)
{
return string.Empty;
}
var title = node.Title ?? string.Empty;
var kind = node.Kind ?? string.Empty;
return $"{title} ({kind} - {node.Id})";
var title = LocalizeNodeTitle(node.Title);
var kind = node.Kind.IsNullOrWhiteSpace()
? string.Empty
: Localize($"App.WorkflowKind.{node.Kind.Trim()}", node.Kind.Trim());
return kind.IsNullOrWhiteSpace() ? title : $"{title} ({kind})";
}
private string Localize(string key, string fallback)
{
if (key.IsNullOrWhiteSpace())
{
return fallback;
}
var value = localizer[key];
return value.ResourceNotFound ? fallback : value.Value;
}
private string NoteLabel(string key, string fallback)
{
return $"{Localize(key, fallback)}: ";
}
/// <summary>
/// Workflow adım başlıkları localization key olarak saklanabilir ("App.WorkflowKind.Approval")
/// ve aynı formda tekrar ederse sonuna sıra numarası eklenir ("App.WorkflowKind.Approval1").
/// </summary>
private string LocalizeNodeTitle(string title)
{
if (title.IsNullOrWhiteSpace())
{
return string.Empty;
}
var text = title.Trim();
var direct = localizer[text];
if (!direct.ResourceNotFound)
{
return direct.Value;
}
var digitCount = 0;
while (digitCount < text.Length && char.IsDigit(text[text.Length - 1 - digitCount]))
{
digitCount++;
}
if (digitCount == 0 || digitCount == text.Length)
{
return text;
}
var baseKey = text[..^digitCount];
var baseValue = localizer[baseKey];
return baseValue.ResourceNotFound
? text
: $"{baseValue.Value} {text[^digitCount..]}";
}
private static string Encode(string value)

View file

@ -1,10 +1,11 @@
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Sozsoft.Platform.EntityFrameworkCore;
using Volo.Abp.DependencyInjection;
@ -26,28 +27,31 @@ namespace Sozsoft.Platform.Data.Seeds;
public class AfterAllMigrationsSqlExecutor : IAfterAllMigrationsSqlExecutor, ITransientDependency
{
/// <summary>HostData/TenantData altındaki, migration sonrası da çalıştırılacak script klasörünün adı.</summary>
public const string DirectoryName = "ExecuteData";
public const string DirectoryName = SeedPathResolver.ExecuteDataFolder;
/// <summary>Host kapsamındaki script klasörü.</summary>
public const string HostDataDirectoryName = "HostData";
public const string HostDataDirectoryName = SeedPathResolver.HostDataFolder;
/// <summary>Tenant kapsamındaki script klasörü (henüz kullanılmıyorsa sessizce atlanır).</summary>
public const string TenantDataDirectoryName = "TenantData";
public const string TenantDataDirectoryName = SeedPathResolver.TenantDataFolder;
private static readonly Regex SafeIdentifierRegex =
new(@"^[A-Za-z_][A-Za-z0-9_]{0,126}$", RegexOptions.Compiled);
private readonly IDbContextProvider<PlatformDbContext> _dbContextProvider;
private readonly IUnitOfWorkManager _unitOfWorkManager;
private readonly IConfiguration _configuration;
private readonly ILogger<AfterAllMigrationsSqlExecutor> _logger;
public AfterAllMigrationsSqlExecutor(
IDbContextProvider<PlatformDbContext> dbContextProvider,
IUnitOfWorkManager unitOfWorkManager,
IConfiguration configuration,
ILogger<AfterAllMigrationsSqlExecutor> logger)
{
_dbContextProvider = dbContextProvider;
_unitOfWorkManager = unitOfWorkManager;
_configuration = configuration;
_logger = logger;
}
@ -131,13 +135,13 @@ public class AfterAllMigrationsSqlExecutor : IAfterAllMigrationsSqlExecutor, ITr
}
}
private static string GetScriptDirectoryPath(string scopeDirectoryName)
private string GetScriptDirectoryPath(string scopeDirectoryName)
{
var dataDirectoryName = DefaultDatabaseProvider == DatabaseProvider.PostgreSql
? "PostgresData"
: "SqlData";
? SeedPathResolver.PostgresDataFolder
: SeedPathResolver.SqlDataFolder;
return Path.Combine(Directory.GetCurrentDirectory(), "Seeds", dataDirectoryName, scopeDirectoryName, DirectoryName);
return SeedPathResolver.GetPath(_configuration, dataDirectoryName, scopeDirectoryName, DirectoryName);
}
/// <summary>

View file

@ -3588,6 +3588,12 @@
"en": "Workflow has already been started for the selected record",
"tr": "Seçili kayıt için iş akışı zaten başlamış."
},
{
"resourceName": "Platform",
"key": "App.ListForm.WorkflowStart",
"en": "Workflow Start",
"tr": "İş Akışı Başlat"
},
{
"resourceName": "Platform",
"key": "App.ListForm.WorkflowReset",
@ -6852,6 +6858,12 @@
"en": "Search blog posts...",
"tr": "Blog yazıları ara..."
},
{
"resourceName": "Platform",
"key": "App.PublicBlog.Empty",
"en": "No blog posts found.",
"tr": "Blog yazısı bulunamadı."
},
{
"resourceName": "Platform",
"key": "App.BlogSubscribe.SubscribeDesc",
@ -18276,6 +18288,126 @@
"en": "Undefined",
"tr": "Tanımsız"
},
{
"resourceName": "Platform",
"key": "App.WorkflowNote.SubjectStarted",
"en": "Workflow Started",
"tr": "Workflow Başlatıldı"
},
{
"resourceName": "Platform",
"key": "App.WorkflowNote.SubjectReset",
"en": "Workflow Reset",
"tr": "Workflow Sıfırlandı"
},
{
"resourceName": "Platform",
"key": "App.WorkflowNote.SubjectApproved",
"en": "Workflow Approved",
"tr": "Workflow Onaylandı"
},
{
"resourceName": "Platform",
"key": "App.WorkflowNote.SubjectRejected",
"en": "Workflow Rejected",
"tr": "Workflow Reddedildi"
},
{
"resourceName": "Platform",
"key": "App.WorkflowNote.StartedBy",
"en": "Started By",
"tr": "Başlatan"
},
{
"resourceName": "Platform",
"key": "App.WorkflowNote.ResetBy",
"en": "Reset By",
"tr": "Sıfırlayan"
},
{
"resourceName": "Platform",
"key": "App.WorkflowNote.ResetStep",
"en": "Reset Step",
"tr": "Sıfırlanan Adım"
},
{
"resourceName": "Platform",
"key": "App.WorkflowNote.DecisionBy",
"en": "Decision By",
"tr": "Karar Veren"
},
{
"resourceName": "Platform",
"key": "App.WorkflowNote.Decision",
"en": "Decision",
"tr": "Karar"
},
{
"resourceName": "Platform",
"key": "App.WorkflowNote.Description",
"en": "Description",
"tr": "Açıklama"
},
{
"resourceName": "Platform",
"key": "App.WorkflowNote.Approver",
"en": "Approver",
"tr": "Onaylayacak"
},
{
"resourceName": "Platform",
"key": "App.WorkflowNote.Inform",
"en": "Inform",
"tr": "Bilgilendirilen"
},
{
"resourceName": "Platform",
"key": "App.WorkflowNote.Approved",
"en": "Approved",
"tr": "Onaylandı"
},
{
"resourceName": "Platform",
"key": "App.WorkflowNote.Rejected",
"en": "Rejected",
"tr": "Reddedildi"
},
{
"resourceName": "Platform",
"key": "App.WorkflowNote.Started",
"en": "Started",
"tr": "Başlatıldı"
},
{
"resourceName": "Platform",
"key": "App.WorkflowNote.Evaluated",
"en": "Evaluated",
"tr": "Değerlendirildi"
},
{
"resourceName": "Platform",
"key": "App.WorkflowNote.WaitingApproval",
"en": "Waiting Approval",
"tr": "Onay Bekliyor"
},
{
"resourceName": "Platform",
"key": "App.WorkflowNote.Informed",
"en": "Informed",
"tr": "Bilgilendirildi"
},
{
"resourceName": "Platform",
"key": "App.WorkflowNote.Completed",
"en": "Completed",
"tr": "Tamamlandı"
},
{
"resourceName": "Platform",
"key": "App.WorkflowNote.Processed",
"en": "Processed",
"tr": "İşlendi"
},
{
"resourceName": "Platform",
"key": "App.MessengerWidget.Title",
@ -25050,6 +25182,12 @@
"en": "Approver",
"tr": "Onaylanacak kişi"
},
{
"resourceName": "Platform",
"key": "App.WorkflowKind.Compare",
"en": "Comparison",
"tr": "Karşılaştırma"
},
{
"resourceName": "Platform",
"key": "App.WorkflowKind.End",
@ -25062,6 +25200,12 @@
"en": "Inform",
"tr": "Bilgilendirme"
},
{
"resourceName": "Platform",
"key": "App.WorkflowKind.Start",
"en": "Start",
"tr": "Başlangıç"
},
{
"resourceName": "Platform",
"key": "App.WorkflowKind.Step",
@ -25074,6 +25218,12 @@
"en": "No rule",
"tr": "Kural yok"
},
{
"resourceName": "Platform",
"key": "App.WorkflowOutcome.Next",
"en": "Next",
"tr": "Sonraki"
},
{
"resourceName": "Platform",
"key": "App.WorkflowOutcome.CompareState",

View file

@ -1,9 +1,11 @@
using System;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Sozsoft.Platform.EntityFrameworkCore;
using Volo.Abp.Data;
@ -14,27 +16,50 @@ using static Sozsoft.Settings.SettingsConsts;
namespace Sozsoft.Platform.Data.Seeds;
/// <summary>
/// SqlTableDesigner üzerinden deploy edilen tabloları Seeds/SqlData/*.sql dosyalarından okuyarak veritabanına uygular.
/// Her dosya, tek bir tabloya (veya ilgili ALTER/INDEX script'lerine) ait IF OBJECT_ID kontrolü içeren T-SQL batch'leri içermelidir.
/// Veritabanı silinip yeniden oluşturulduğunda bu seeder tüm tablo scriptlerini yeniden çalıştırır.
/// SqlTableDesigner ve SqlQueryManager üzerinden deploy edilen nesneleri
/// <c>{SqlData|PostgresData}</c> klasöründeki .sql dosyalarından okuyarak veritabanına uygular.
/// Her dosya, tek bir nesneye (ve ilgili ALTER/INDEX script'lerine) ait varlık kontrolü içeren
/// batch'lerden oluşmalıdır. Veritabanı silinip yeniden oluşturulduğunda tüm scriptler yeniden çalışır.
/// <para>
/// Klasör düzeni — her .sql dosyası amacını belirten bir klasörün altındadır, kökte dosya durmaz:
/// <code>
/// {SqlData|PostgresData}/
/// HostData/ → host (varsayılan) veritabanı kapsamı
/// ObjectData/ → nesne oluşturulur/güncellenir. SqlTableDesigner ve
/// SqlQueryManager, host bağlamındayken buraya yazar.
/// ExecuteData/ → nesne oluşturulur, ayrıca tüm migration'lar bittikten sonra
/// <see cref="AfterAllMigrationsSqlExecutor"/> tarafından çalıştırılır.
/// TenantData/ → tenant veritabanı kapsamı (bir tenant seçiliyken üretilenler)
/// ObjectData/
/// ExecuteData/
/// </code>
/// Geriye dönük uyumluluk: klasör köklerinde kalmış eski .sql dosyaları da işlenir, ancak
/// uyarı ile loglanır — taşınmaları beklenir.
/// </para>
/// </summary>
public class SqlDataSeeder : IDataSeedContributor, ITransientDependency
{
/// <summary>Yalnızca oluşturulan/güncellenen nesne scriptlerinin klasörü.</summary>
public const string ObjectDataDirectoryName = SeedPathResolver.ObjectDataFolder;
private readonly IDbContextProvider<PlatformDbContext> _dbContextProvider;
private readonly IConfiguration _configuration;
private readonly ILogger<SqlDataSeeder> _logger;
public SqlDataSeeder(
IDbContextProvider<PlatformDbContext> dbContextProvider,
IConfiguration configuration,
ILogger<SqlDataSeeder> logger)
{
_dbContextProvider = dbContextProvider;
_configuration = configuration;
_logger = logger;
}
public async Task SeedAsync(DataSeedContext context)
{
var dataDirectoryName = GetDataDirectoryName();
var sqlDataPath = Path.Combine(Directory.GetCurrentDirectory(), "Seeds", dataDirectoryName);
var sqlDataPath = SeedPathResolver.GetPath(_configuration, dataDirectoryName);
if (!Directory.Exists(sqlDataPath))
{
_logger.LogInformation("Seeds/{DirectoryName} directory not found, skipping SqlDataSeeder.", dataDirectoryName);
@ -107,15 +132,16 @@ public class SqlDataSeeder : IDataSeedContributor, ITransientDependency
private static string GetDataDirectoryName()
{
return DefaultDatabaseProvider == DatabaseProvider.PostgreSql
? "PostgresData"
: "SqlData";
? SeedPathResolver.PostgresDataFolder
: SeedPathResolver.SqlDataFolder;
}
private static string[] GetSqlFiles(string dataDirectoryPath, bool isHostSeed)
private string[] GetSqlFiles(string dataDirectoryPath, bool isHostSeed)
{
var sqlFiles = Directory.GetFiles(dataDirectoryPath, "*.sql")
.OrderBy(f => Path.GetFileName(f))
.ToList();
var sqlFiles = new List<string>();
// Klasor kokunde dosya beklenmez; eskiden kalanlar uyari ile islenir.
sqlFiles.AddRange(GetLegacyRootSqlFiles(dataDirectoryPath));
var scopeDirectoryName = isHostSeed
? AfterAllMigrationsSqlExecutor.HostDataDirectoryName
@ -124,22 +150,44 @@ public class SqlDataSeeder : IDataSeedContributor, ITransientDependency
var scopePath = Path.Combine(dataDirectoryPath, scopeDirectoryName);
if (Directory.Exists(scopePath))
{
sqlFiles.AddRange(Directory.GetFiles(scopePath, "*.sql")
.OrderBy(f => Path.GetFileName(f)));
sqlFiles.AddRange(GetOrderedSqlFiles(Path.Combine(scopePath, ObjectDataDirectoryName)));
sqlFiles.AddRange(GetLegacyRootSqlFiles(scopePath));
// {HostData|TenantData}/ExecuteData: nesneler burada da oluşturulur, ayrıca tüm
// migration bittikten sonra AfterAllMigrationsSqlExecutor dosya adı üzerinden çalıştırır.
var executeDataPath = Path.Combine(scopePath, AfterAllMigrationsSqlExecutor.DirectoryName);
if (Directory.Exists(executeDataPath))
{
sqlFiles.AddRange(Directory.GetFiles(executeDataPath, "*.sql")
.OrderBy(f => Path.GetFileName(f)));
}
sqlFiles.AddRange(GetOrderedSqlFiles(
Path.Combine(scopePath, AfterAllMigrationsSqlExecutor.DirectoryName)));
}
return sqlFiles.ToArray();
}
private static string[] GetOrderedSqlFiles(string directoryPath)
{
return Directory.Exists(directoryPath)
? Directory.GetFiles(directoryPath, "*.sql").OrderBy(Path.GetFileName).ToArray()
: [];
}
/// <summary>
/// Klasor kokunde kalmis eski scriptler. Artik her .sql bir amac klasorunun altinda
/// olmali; calistirmaya devam edilir ancak tasinmalari icin uyarilir.
/// </summary>
private string[] GetLegacyRootSqlFiles(string directoryPath)
{
var files = GetOrderedSqlFiles(directoryPath);
if (files.Length > 0)
{
_logger.LogWarning(
"{Count} .sql file(s) found directly under '{Directory}'. Move them into the '{ObjectData}' folder; root-level files are deprecated.",
files.Length,
directoryPath,
ObjectDataDirectoryName);
}
return files;
}
private static (string Action, string? ObjectName, string? ObjectType) ExtractSqlInfo(string sql)
{
var patterns = new[]

View file

@ -1,10 +1,11 @@
using System;
using System;
using System.Collections.Generic;
using System.Data;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Sozsoft.Languages.Entities;
using Sozsoft.Languages.Languages;
@ -36,6 +37,7 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
private readonly IRepository<ListForm, Guid> _repoListForm;
private readonly IRepository<ListFormField, Guid> _repoListFormField;
private readonly IRepository<ListFormWorkflow, string> _repoListFormWorkflow;
private readonly IConfiguration _configuration;
private readonly ILogger<WizardDataSeeder> _logger;
private readonly string _cultureNameDefault = PlatformConsts.DefaultLanguage;
@ -51,6 +53,7 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
IRepository<ListForm, Guid> repoListForm,
IRepository<ListFormField, Guid> repoListFormField,
IRepository<ListFormWorkflow, string> repoListFormWorkflow,
IConfiguration configuration,
ILogger<WizardDataSeeder> logger)
{
_repoLangKey = repoLangKey;
@ -62,12 +65,13 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
_repoListForm = repoListForm;
_repoListFormField = repoListFormField;
_repoListFormWorkflow = repoListFormWorkflow;
_configuration = configuration;
_logger = logger;
}
public async Task SeedAsync(DataSeedContext context)
{
var wizardDataPath = Path.Combine(Directory.GetCurrentDirectory(), "Seeds", "WizardData");
var wizardDataPath = SeedPathResolver.GetPath(_configuration, "WizardData");
if (!Directory.Exists(wizardDataPath))
{
_logger.LogInformation("WizardData directory not found, skipping.");

View file

@ -77,50 +77,6 @@
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Seeds\WizardData\*.json">
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Seeds\SqlData\*.sql">
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Seeds\SqlData\HostData\*.sql">
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Seeds\SqlData\HostData\ExecuteData\*.sql">
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Seeds\SqlData\TenantData\*.sql">
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Seeds\SqlData\TenantData\ExecuteData\*.sql">
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Seeds\PostgresData\*.sql">
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Seeds\PostgresData\HostData\*.sql">
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Seeds\PostgresData\HostData\ExecuteData\*.sql">
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Seeds\PostgresData\TenantData\*.sql">
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
<Content Include="Seeds\PostgresData\TenantData\ExecuteData\*.sql">
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>
<ItemGroup>

View file

@ -1,5 +1,6 @@
{
{
"App": {
"SeedsPath": "/etc/api/seeds",
"CdnUrl": "https://cdn-api.sozsoft.com"
},
"ConnectionStrings": {

View file

@ -1,5 +1,6 @@
{
{
"App": {
"SeedsPath": "/etc/api/seeds",
"CdnUrl": "https://cdn.sozsoft.com"
},
"ConnectionStrings": {

View file

@ -0,0 +1,104 @@
using System;
using System.IO;
using Microsoft.Extensions.Configuration;
namespace Sozsoft.Platform.Data.Seeds;
/// <summary>
/// Runtime'da duzenlenebilen seed dosyalarinin (SqlData / PostgresData / WizardData) klasorunu cozer.
/// <para>
/// Kod ile birlikte versiyonlanan sabit seed'ler (ornegin TenantDataSeeder'in okudugu
/// TenantData.json) buraya dahil degildir; onlar kendi projelerinin ciktisinda kalir.
/// </para>
/// <para>
/// Klasor bilincli olarak hicbir projenin icinde degildir: depoda <c>configs/seeds</c> altinda durur ve
/// konteynere bind mount ile baglanir. Boylece bir seed dosyasi degistiginde ne api ne de migrator
/// imaji yeniden derlenir; deploy sirasindaki <c>git pull</c> yeterlidir. Ayni klasor File Manager
/// uzerinden de duzenlenebildigi icin yapilan degisiklik dogrudan host diskinde kalici olur.
/// </para>
/// <para>
/// Cozum sirasi:
/// 1) <c>App:SeedsPath</c> ayari (konteynerde mount noktasi),
/// 2) calisma dizininden yukari dogru <c>configs/seeds</c> aramasi (local gelistirme),
/// 3) calisma dizininin altindaki <c>Seeds</c> klasoru (son care).
/// </para>
/// </summary>
public static class SeedPathResolver
{
public const string ConfigurationKey = "App:SeedsPath";
/// <summary>SQL Server saglayicisinin script klasoru.</summary>
public const string SqlDataFolder = "SqlData";
/// <summary>PostgreSQL saglayicisinin script klasoru.</summary>
public const string PostgresDataFolder = "PostgresData";
/// <summary>Host veritabani kapsamindaki scriptler.</summary>
public const string HostDataFolder = "HostData";
/// <summary>Tenant veritabani kapsamindaki scriptler.</summary>
public const string TenantDataFolder = "TenantData";
/// <summary>Yalnizca olusturulan/guncellenen nesne scriptleri.</summary>
public const string ObjectDataFolder = "ObjectData";
/// <summary>Olusturulan, ayrica tum migration'lar bittikten sonra calistirilan scriptler.</summary>
public const string ExecuteDataFolder = "ExecuteData";
/// <summary>Wizard'in urettigi konfigurasyon dosyalari.</summary>
public const string WizardDataFolder = "WizardData";
private const string RepositorySeedsFolder = "seeds";
private const string RepositoryConfigsFolder = "configs";
private const string FallbackFolderName = "Seeds";
/// <summary>Seed kok klasorunun tam yolunu dondurur. Klasorun var olmasi garanti edilmez.</summary>
public static string GetRootPath(IConfiguration? configuration)
{
var configuredPath = configuration?[ConfigurationKey];
if (!string.IsNullOrWhiteSpace(configuredPath))
{
return Path.GetFullPath(configuredPath);
}
var repositoryPath = FindRepositorySeedsPath();
if (repositoryPath != null)
{
return repositoryPath;
}
return Path.Combine(Directory.GetCurrentDirectory(), FallbackFolderName);
}
/// <summary>Seed kok klasorunun altindaki bir alt yolu dondurur.</summary>
public static string GetPath(IConfiguration? configuration, params string[] subPaths)
{
var path = GetRootPath(configuration);
foreach (var subPath in subPaths)
{
path = Path.Combine(path, subPath);
}
return path;
}
/// <summary>Calisma dizininden yukari dogru cikarak depodaki configs/seeds klasorunu arar.</summary>
private static string? FindRepositorySeedsPath()
{
var dir = new DirectoryInfo(Directory.GetCurrentDirectory());
while (dir != null)
{
var candidate = Path.Combine(dir.FullName, RepositoryConfigsFolder, RepositorySeedsFolder);
if (Directory.Exists(candidate))
{
return candidate;
}
dir = dir.Parent;
}
return null;
}
}

View file

@ -1,9 +1,10 @@
using System;
using System;
using System.Diagnostics;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Sozsoft.Platform.Data.Seeds;
namespace Sozsoft.Platform.DbStartup;
@ -33,6 +34,9 @@ public static class SetupMigrationRunner
private const int DefaultTimeoutMinutes = 30;
/// <summary>Alt surece seed klasorunu bildiren ortam degiskeni (App:SeedsPath karsiligi).</summary>
private const string SeedsPathEnvironmentVariable = "App__SeedsPath";
/// <param name="processCancellationToken">
/// Sürecin yaşam süresini kontrol eden token. Bu <b>istemci isteğinin</b> token'ı DEĞİL,
/// uygulamanın <c>ApplicationStopping</c> token'ı olmalıdır. Aksi halde kullanıcı sekmeyi
@ -102,6 +106,12 @@ public static class SetupMigrationRunner
}
};
// Seed klasoru artik migrator publish ciktisinin icinde degil, mount edilen ortak
// klasorde. Alt surec API'nin cozdugu yolu birebir kullansin ki uygulamadan
// tetiklenen migrate, File Manager'da gorunen dosyalari islesin.
process.StartInfo.Environment[SeedsPathEnvironmentVariable] =
SeedPathResolver.GetRootPath(configuration);
process.Start();
async Task PumpAsync(StreamReader reader, string level)

View file

@ -1,4 +1,4 @@
{
{
"App": {
"SelfUrl": "https://dev-api.sozsoft.com",
"ClientUrl": "https://dev.sozsoft.com",
@ -7,7 +7,7 @@
"AttachmentsPath": "/etc/api/mail-queue/attachments",
"CdnUrl": "https://dev-cdn.sozsoft.com",
"CdnPath": "/etc/api/cdn",
"DbMigratorSeedsPath": "/etc/api/seeds",
"SeedsPath": "/etc/api/seeds",
"BaseDomain": "sozsoft.com",
"BackupPath": "/var/opt/mssql/backup"
},

View file

@ -1,4 +1,4 @@
{
{
"App": {
"SelfUrl": "https://api.sozsoft.com",
"ClientUrl": "https://sozsoft.com",
@ -7,7 +7,7 @@
"AttachmentsPath": "/etc/api/mail-queue/attachments",
"CdnUrl": "https://cdn.sozsoft.com",
"CdnPath": "/etc/api/cdn",
"DbMigratorSeedsPath": "/etc/api/seeds",
"SeedsPath": "/etc/api/seeds",
"BaseDomain": "sozsoft.com",
"BackupPath": "/var/opt/mssql/backup"
},

View file

@ -8,9 +8,6 @@ networks:
volumes:
cdn:
api-keys:
# DbMigrator imajindaki Seeds klasoru ilk calistirmada bu volume'e kopyalanir;
# api konteyneri ayni volume'u /etc/api/seeds altinda okuyup yazar.
seeds:
sql-backups:
external: true
name: sozsoft-platform-data_sql-backups
@ -25,7 +22,10 @@ services:
- ASPNETCORE_ENVIRONMENT=Dev
- SEED=${SEED}
volumes:
- seeds:/srv/app/Seeds
# Seed dosyalari imaja gomulmez; depodaki configs/seeds klasorunden mount edilir
# (App:SeedsPath). Boylece bir seed degistiginde imaj build etmek gerekmez,
# deploy sirasindaki git pull yeterlidir.
- ../seeds:/etc/api/seeds
networks:
- db
@ -40,7 +40,9 @@ services:
volumes:
- cdn:/etc/api/cdn
- sql-backups:/etc/api/cdn/host/backup
- seeds:/etc/api/seeds
# Seed dosyalari: migrator ile ayni depo klasoru. File Manager uzerinden yapilan
# duzenleme dogrudan host diskine yazilir ve deploy'lar arasinda kalici olur.
- ../seeds:/etc/api/seeds
- api-keys:/root/.aspnet/DataProtection-Keys
# Surum notlari imaja gomulmez; deploy sirasinda git tag'lerinden uretilip
# buradan okunur (ReleaseNoteStartupService). Boylece yeni tag API imajini

View file

@ -1,8 +1,8 @@
import { FiBell, FiCheck, FiGitBranch, FiPlay, FiSlash } from 'react-icons/fi'
export const kindOptions = [
{ value: 'Start', label: 'App.VideoRoom.Start' },
{ value: 'Compare', label: 'App.ScriptBuilder.Comparison' },
{ value: 'Start', label: 'App.WorkflowKind.Start' },
{ value: 'Compare', label: 'App.WorkflowKind.Compare' },
{ value: 'Approval', label: 'App.WorkflowKind.Approval' },
{ value: 'Inform', label: 'App.WorkflowKind.Inform' },
{ value: 'End', label: 'App.WorkflowKind.End' },

View file

@ -244,7 +244,7 @@ function sideToward(from: WorkflowCriteriaDto, to: WorkflowCriteriaDto) {
/** Çıkış etiketleri lokalizasyon anahtarı olarak taşınır; ekranda `translate` ile çözülür. */
export const OUTCOME_KEYS = {
nextOnStart: 'App.Platform.Next2',
nextOnStart: 'App.WorkflowOutcome.Next',
nextOnTrue: 'App.WorkflowOutcome.True',
nextOnFalse: 'App.WorkflowOutcome.False',
nextOnApprove: 'App.ListFormWorkflow.Approve',
@ -476,8 +476,8 @@ export function normalizeCriteria(item: WorkflowCriteriaForm): SaveCriteriaInput
export function defaultTitle(kind: string) {
return (
{
Start: 'App.VideoRoom.Start',
Compare: 'App.ScriptBuilder.Comparison',
Start: 'App.WorkflowKind.Start',
Compare: 'App.WorkflowKind.Compare',
Approval: 'App.WorkflowKind.Approval',
Inform: 'App.WorkflowKind.Inform',
End: 'App.WorkflowKind.End',
@ -485,6 +485,33 @@ export function defaultTitle(kind: string) {
)
}
/**
* Node basliklari lokalizasyon anahtari olarak uretilir ve benzersizlik icin
* sonuna sira numarasi eklenir (`App.WorkflowKind.Approval1`). Bu yuzden ham
* baslik dogrudan `translate` edilemez; once numara ayrilir, anahtar cevrilir,
* numara tekrar eklenir. Anahtar bulunamazsa ham baslik gosterilir.
*/
const LOCALIZATION_KEY_PATTERN = /^[A-Za-z0-9_]+(?:\.[A-Za-z0-9_]+)+$/u
export function translateWorkflowLabel(
title: string | null | undefined,
translate: (key: string) => string,
) {
const raw = String(title ?? '').trim()
if (!raw) return ''
const match = /^(.*?)(\d*)$/u.exec(raw)
const base = (match?.[1] || raw).trim()
const suffix = match?.[2] || ''
if (!LOCALIZATION_KEY_PATTERN.test(base)) return raw
const translated = translate('::' + base)
if (!translated || translated === base) return raw
return `${translated}${suffix}`
}
export function emptyCompareOutcome1(label = 'Durum', compareColumn = 'Price'): CompareOutcomeDto {
return {
label,

View file

@ -72,7 +72,7 @@ const buildDownloadUrl = (item: FileItemType, fallbackTenantId?: string): string
return `${VITE_CDN_URL}/${tenantSegment}/${segments.map(encodeURIComponent).join('/')}`
}
/** DbMigrator Seeds klasoru CDN altinda degil, backend sanal kok olarak sunar. */
/** seeds klasoru CDN altinda degil, backend sanal kok olarak sunar (configs/seeds). */
const isSeedsItem = (item: FileItemType): boolean => {
const relativePath = item.path || item.id.replace(/\|/g, '/')
return relativePath.split('/')[0]?.toLowerCase() === 'seeds'
@ -419,7 +419,7 @@ const FileManager = () => {
const handleDownload = useCallback(
async (item: FileItemType) => {
// Sanal Seeds klasoru CDN altinda olmadigi icin dosya API uzerinden stream edilir.
// Sanal seeds klasoru CDN altinda olmadigi icin dosya API uzerinden stream edilir.
if (isSeedsItem(item)) {
try {
const blob = await fileManagementService.downloadFile(item.id, currentTenantId)

View file

@ -7,6 +7,7 @@ import {
collectLinks,
getNodeOutcomes,
outcomeLabel,
translateWorkflowLabel,
OUTCOME_KEYS,
type WorkflowLink,
type WorkflowOutcome,
@ -342,7 +343,7 @@ function FlowNode({
{translate('::' + kindOptions.find((option) => option.value === item.kind)?.label)}
</span>
<strong className="break-words text-sm leading-tight [overflow-wrap:anywhere]">
{item.title}
{translateWorkflowLabel(item.title, translate)}
</strong>
<small
className={
@ -386,9 +387,7 @@ function FlowNode({
onBeginLink(item.id, outcome.field)
}}
>
{outcome.label.startsWith('ListForms.')
? translate('::' + outcome.label)
: outcome.label}
{translateWorkflowLabel(outcome.label, translate)}
</span>
))}
</div>
@ -505,12 +504,15 @@ function Arrow({
}
function ArrowLabel({ link, pendingLink }: { link: WorkflowLink; pendingLink: PendingLink }) {
const { translate } = useLocalization()
if (!link.label) return null
const route = buildArrowRoute(link.source, link.target, link.sourcePort)
const labelPoint = route.labelPoint
const tone = linkTone(link)
const labelWidth = Math.max(38, (link.label || '').length * 6 + 14)
const label = translateWorkflowLabel(link.label, translate)
const labelWidth = Math.max(38, label.length * 6 + 14)
const isActive =
pendingLink?.sourceId === link.source.id && pendingLink?.outcome === link.sourcePort?.field
@ -536,7 +538,7 @@ function ArrowLabel({ link, pendingLink }: { link: WorkflowLink; pendingLink: Pe
y={labelPoint.y}
textAnchor="middle"
>
{link.label}
{label}
</text>
</g>
)

View file

@ -306,7 +306,7 @@ const SqlObjectExplorer = ({
dataSourceCode: dataSource,
})
// If a matching seed file exists under DbMigrator/Seeds/SqlData, delete it too.
// If a matching seed file exists under the seeds folder (configs/seeds/SqlData), delete it too.
try {
const fileNames = [...new Set(getSqlDataFileCandidates(dropConfirm.node).filter(Boolean))]
if (fileNames.length > 0) {

View file

@ -958,7 +958,7 @@ GO`,
))}
</select>
<DbMigrateButton />
{/* Seed dosyalari (DbMigrator/Seeds) File Manager uzerinden yonetilir. */}
{/* Seed dosyalari (configs/seeds) File Manager uzerinden yonetilir. */}
<Link to={ROUTES_ENUM.protected.admin.files} target="_blank">
<Button
size="sm"

View file

@ -1952,7 +1952,7 @@ const SqlTableDesignerDialog = ({
</Notification>,
{ placement: 'bottom-end' },
)
// Save seed file to DbMigrator/Seeds/SqlData after successful deploy.
// Save seed file to the seeds folder (configs/seeds/SqlData) after successful deploy.
// Always use the full CREATE TABLE script (not the ALTER diff) so the seed file
// can recreate the table from scratch when the database is wiped.
try {

View file

@ -457,7 +457,16 @@ export const NoteList: React.FC<NoteListProps> = ({
{note.subject && (
<h4 className="text-sm font-bold text-gray-900 mb-1">{note.subject}</h4>
)}
{note.content && <div dangerouslySetInnerHTML={{ __html: note.content }} />}
{note.content && (
<div
className={
note.type === 'workflow'
? 'text-[11px] leading-snug text-gray-600 dark:text-gray-300 [&_table]:w-full [&_table]:table-fixed [&_td]:align-top [&_td]:py-0.5 [&_td:first-child]:w-[38%] [&_td:first-child]:pr-2 [&_td:first-child]:text-gray-400 [&_td:last-child]:break-words'
: undefined
}
dangerouslySetInnerHTML={{ __html: note.content }}
/>
)}
</div>
{/* Files */}
@ -579,7 +588,10 @@ export const NoteList: React.FC<NoteListProps> = ({
<Badge className={opClass} content={opLabel} />
{rowLabel ? <Badge className="bg-gray-600" content={rowLabel} /> : null}
</div>
<div className="text-xs text-gray-500 mt-1 truncate">
<div
className="text-xs text-gray-500 mt-1 break-all whitespace-normal"
title={(log.httpMethod || 'HTTP') + ' ' + (log.url || '')}
>
{(log.httpMethod || 'HTTP') + ' ' + (log.url || '')}
</div>
{changeLines.length > 0 && (

View file

@ -29,6 +29,7 @@ import {
openImageInNewTab,
showImageHoverPreview,
} from '../form/editors/imageHoverPreview'
import { translateWorkflowLabel } from '@/utils/workflow/workflowHelpers'
const cellTemplateMultiValue = (
cellElement: HTMLElement,
@ -598,6 +599,9 @@ const useListFormColumns = ({
if (!gridDto || !columnFormats) {
return columns
}
const workflowApprovalStatusField =
gridDto.gridOptions?.workflowDto?.approvalStatusFieldName?.toLowerCase()
columnFormats.forEach((colData) => {
if (!colData.canRead || !colData.isActive) {
return
@ -608,6 +612,16 @@ const useListFormColumns = ({
column.dataField = colData.fieldName
if (colData.dataType) column.dataType = colData.dataType as DataType
if (colData.captionName) column.caption = translate('::' + colData.captionName)
// Workflow onay durumu alani, kriter basligini (lokalizasyon anahtari +
// sira no) ham degeriyle saklar; ekranda cevrilerek gosterilir.
if (
workflowApprovalStatusField &&
colData.fieldName?.toLowerCase() === workflowApprovalStatusField
) {
column.customizeText = (cellInfo: any) =>
translateWorkflowLabel(cellInfo?.value, translate)
}
if (colData.width > 0) column.width = colData.width
column.visible = colData.visible

View file

@ -13,7 +13,7 @@ import { layoutTypes, ListViewLayoutType } from '../admin/listForm/edit/types'
import { useStoreState } from '@/store'
import { workflowService } from '@/services/workflow.service'
import type { WorkflowRunResultDto } from '@/services/workflow.service'
import { splitWorkflowApprovers } from '@/utils/workflow/workflowHelpers'
import { splitWorkflowApprovers, translateWorkflowLabel } from '@/utils/workflow/workflowHelpers'
type ToolbarModalData = {
open: boolean
@ -149,6 +149,15 @@ const useToolbar = ({
const workflowOptions = grdOpt.workflowDto
const approvalCriteria =
workflowOptions?.criteria?.filter((item) => item.kind === 'Approval') ?? []
// Toolbar declarative olarak basiliyor (`<Item {...item}/>`), aktiflik ise
// `updateWorkflowApprovalToolbarItems` ile imperative yaziliyor. Item'lar
// sabit `disabled: true` ile kurulursa, devextreme-react option guncellemesini
// secim degisiminden sonra uygulayip butonu tekrar kapatiyor. Bu yuzden
// baslangic degeri de mevcut secimden hesaplanir.
const currentSelection = resolveSelectedRowsData(getSelectedRowsData)
const workflowDisabled = (itemName: string) =>
isWorkflowToolbarItemDisabled(itemName, workflowOptions, currentSelection, currentUser) ?? true
if (
workflowOptions?.approvalStatusFieldName &&
approvalCriteria.length > 0 &&
@ -166,7 +175,7 @@ const useToolbar = ({
text: toToolbarText(translate('::App.ListForm.WorkflowReset')),
hint: translate('::App.ListForm.WorkflowReset'),
visible: true,
disabled: true,
disabled: workflowDisabled('workflowReset'),
onClick: async () => {
const selectedRowKeys = (await Promise.resolve(
getSelectedRowKeys() as any,
@ -241,10 +250,10 @@ const useToolbar = ({
showText: 'always',
options: {
icon: 'play',
text: toToolbarText('Workflow Start'),
hint: 'Workflow Start',
text: toToolbarText(translate('::App.ListForm.WorkflowStart')),
hint: translate('::App.ListForm.WorkflowStart'),
visible: true,
disabled: true,
disabled: workflowDisabled('workflowStart'),
onClick: async () => {
const selectedRowKeys = (await Promise.resolve(
getSelectedRowKeys() as any,
@ -320,10 +329,10 @@ const useToolbar = ({
showText: 'always',
options: {
icon: 'check',
text: toToolbarText(criteria.title),
hint: criteria.title,
text: toToolbarText(translateWorkflowLabel(criteria.title, translate)),
hint: translateWorkflowLabel(criteria.title, translate),
visible: true,
disabled: true,
disabled: workflowDisabled(`workflowApproval_${criteria.id}`),
onClick: async () => {
const selectedRowKeys = (await Promise.resolve(
getSelectedRowKeys() as any,
@ -725,6 +734,12 @@ const useToolbar = ({
}
}
/** Secili satirlari senkron okur; kaynak promise donerse bos kabul edilir. */
function resolveSelectedRowsData(getSelectedRowsData: () => any) {
const data = getSelectedRowsData?.()
return Array.isArray(data) ? (data as Record<string, unknown>[]) : []
}
function isWorkflowApprovalCriteriaActive(
row: Record<string, unknown>,
workflowOptions: WorkflowDto,
@ -1012,7 +1027,7 @@ function WorkflowApprovalDecisionDialog({
return (
<>
<h5 className="mb-4">{criteriaTitle}</h5>
<h5 className="mb-4">{translateWorkflowLabel(criteriaTitle, translate)}</h5>
<p className="mb-4">
{translate('::App.Listform.ListformField.WorkflowDecisionMessage', {
0: keys.length,

View file

@ -135,7 +135,7 @@ const Blog: React.FC = () => {
<input
type="search"
value={searchQuery}
placeholder={translateLabel(translate, 'Public.blog.SearchPlaceholder')}
placeholder={translateLabel(translate, 'App.PublicBlog.SearchPlaceholder')}
aria-label={translate('::App.Platform.Search3')}
className="w-full rounded-lg border border-gray-300 bg-white py-2 pl-10 text-gray-900 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 dark:placeholder-gray-400"
onChange={(event) => setSearchQuery(event.target.value)}
@ -162,7 +162,7 @@ const Blog: React.FC = () => {
{posts.length === 0 ? (
<div className="py-12 text-center">
<p className="text-lg text-gray-600 dark:text-gray-300">
{translateLabel(translate, 'Public.blog.empty')}
{translateLabel(translate, 'App.PublicBlog.Empty')}
</p>
</div>
) : (