Seeds klasörü path değiştirildi.
This commit is contained in:
parent
39ad5cab12
commit
c993b77b5d
42 changed files with 621 additions and 235 deletions
19
README.md
19
README.md
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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[]
|
||||
|
|
|
|||
|
|
@ -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.");
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
{
|
||||
"App": {
|
||||
"SeedsPath": "/etc/api/seeds",
|
||||
"CdnUrl": "https://cdn-api.sozsoft.com"
|
||||
},
|
||||
"ConnectionStrings": {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
{
|
||||
"App": {
|
||||
"SeedsPath": "/etc/api/seeds",
|
||||
"CdnUrl": "https://cdn.sozsoft.com"
|
||||
},
|
||||
"ConnectionStrings": {
|
||||
|
|
|
|||
104
api/src/Sozsoft.Platform.Domain/Data/SeedPathResolver.cs
Normal file
104
api/src/Sozsoft.Platform.Domain/Data/SeedPathResolver.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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' },
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 && (
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
) : (
|
||||
|
|
|
|||
Loading…
Reference in a new issue