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/
|
├── configs/
|
||||||
│ ├── docker/ # Yerel geliştirme için veri katmanı compose dosyaları ve scriptler
|
│ ├── docker/ # Yerel geliştirme için veri katmanı compose dosyaları ve scriptler
|
||||||
│ ├── deployment/ # Dev/Prod compose dosyaları, nginx/redis/turn konfigürasyonu, deploy scriptleri
|
│ ├── 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ı)
|
│ └── ai/ # AI entegrasyonu (n8n akış tanımları)
|
||||||
├── .github/instructions/ # ai.instructions.md (bağlayıcı platform kuralları), list.instructions.md
|
├── .github/instructions/ # ai.instructions.md (bağlayıcı platform kuralları), list.instructions.md
|
||||||
├── CLAUDE.md # Claude Code için operasyon kuralları
|
├── 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,
|
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
|
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
|
### 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:
|
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/ListFormSeeder_Administration.cs`
|
||||||
- `.../Migrations/MenusData.json`
|
- `.../Migrations/MenusData.json`
|
||||||
- `.../Migrations/PermissionsData.json`
|
- `.../Migrations/PermissionsData.json`
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Volo.Abp.Application.Services;
|
using Volo.Abp.Application.Services;
|
||||||
|
|
||||||
|
|
@ -30,13 +30,15 @@ public interface ISqlObjectManagerAppService : IApplicationService
|
||||||
Task<string> GetNativeObjectDefinitionAsync(string dataSourceCode, string schemaName, string objectName);
|
Task<string> GetNativeObjectDefinitionAsync(string dataSourceCode, string schemaName, string objectName);
|
||||||
|
|
||||||
/// <summary>
|
/// <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.
|
/// Called automatically after a successful SqlTableDesigner deploy so the script can be re-seeded.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task SaveTableScriptAsync(SaveTableScriptDto input);
|
Task SaveTableScriptAsync(SaveTableScriptDto input);
|
||||||
|
|
||||||
/// <summary>
|
/// <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.
|
/// Non-existing files are ignored.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
Task DeleteSqlDataFilesAsync(DeleteSqlDataFilesDto input);
|
Task DeleteSqlDataFilesAsync(DeleteSqlDataFilesDto input);
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ public class SqlQueryExecutionResultDto
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
public class SaveTableScriptDto
|
public class SaveTableScriptDto
|
||||||
{
|
{
|
||||||
|
|
@ -36,7 +36,7 @@ public class SaveTableScriptDto
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Input for deleting seed files from DbMigrator Seeds/SqlData.
|
/// Input for deleting seed files from the seeds folder ({App:SeedsPath}/SqlData).
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class DeleteSqlDataFilesDto
|
public class DeleteSqlDataFilesDto
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -8,13 +8,15 @@ using Sozsoft.SqlQueryManager.Domain.Services;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Volo.Abp.Application.Services;
|
using Volo.Abp.Application.Services;
|
||||||
using Volo.Abp.MultiTenancy;
|
using Volo.Abp.MultiTenancy;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
using Sozsoft.Platform.Data.Seeds;
|
||||||
using Sozsoft.Platform.Enums;
|
using Sozsoft.Platform.Enums;
|
||||||
using Sozsoft.Platform.Queries;
|
using Sozsoft.Platform.Queries;
|
||||||
|
using Sozsoft.Settings;
|
||||||
|
|
||||||
namespace Sozsoft.SqlQueryManager.Application;
|
namespace Sozsoft.SqlQueryManager.Application;
|
||||||
|
|
||||||
|
|
@ -48,7 +50,7 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA
|
||||||
private readonly IDataSourceManager _dataSourceManager;
|
private readonly IDataSourceManager _dataSourceManager;
|
||||||
private readonly ICurrentTenant _currentTenant;
|
private readonly ICurrentTenant _currentTenant;
|
||||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||||
private readonly IHostEnvironment _hostEnvironment;
|
private readonly IConfiguration _configuration;
|
||||||
private readonly ILogger<SqlObjectManagerAppService> _logger;
|
private readonly ILogger<SqlObjectManagerAppService> _logger;
|
||||||
|
|
||||||
public SqlObjectManagerAppService(
|
public SqlObjectManagerAppService(
|
||||||
|
|
@ -57,7 +59,7 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA
|
||||||
IDataSourceManager dataSourceManager,
|
IDataSourceManager dataSourceManager,
|
||||||
ICurrentTenant currentTenant,
|
ICurrentTenant currentTenant,
|
||||||
IHttpContextAccessor httpContextAccessor,
|
IHttpContextAccessor httpContextAccessor,
|
||||||
IHostEnvironment hostEnvironment,
|
IConfiguration configuration,
|
||||||
ILogger<SqlObjectManagerAppService> logger)
|
ILogger<SqlObjectManagerAppService> logger)
|
||||||
{
|
{
|
||||||
_sqlExecutorService = sqlExecutorService;
|
_sqlExecutorService = sqlExecutorService;
|
||||||
|
|
@ -65,7 +67,7 @@ public class SqlObjectManagerAppService : ApplicationService, ISqlObjectManagerA
|
||||||
_dataSourceManager = dataSourceManager;
|
_dataSourceManager = dataSourceManager;
|
||||||
_currentTenant = currentTenant;
|
_currentTenant = currentTenant;
|
||||||
_httpContextAccessor = httpContextAccessor;
|
_httpContextAccessor = httpContextAccessor;
|
||||||
_hostEnvironment = hostEnvironment;
|
_configuration = configuration;
|
||||||
_logger = logger;
|
_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()
|
private string ResolveSqlDataOutputPath()
|
||||||
{
|
{
|
||||||
return ResolveSqlDataOutputPath("SqlData");
|
var dataDirectoryName = SettingsConsts.DefaultDatabaseProvider == SettingsConsts.DatabaseProvider.PostgreSql
|
||||||
}
|
? SeedPathResolver.PostgresDataFolder
|
||||||
|
: SeedPathResolver.SqlDataFolder;
|
||||||
|
|
||||||
private string ResolveSqlDataOutputPath(string dataDirectoryName)
|
var scopeDirectoryName = _currentTenant.Id == null
|
||||||
{
|
? SeedPathResolver.HostDataFolder
|
||||||
const string dbMigratorName = "Sozsoft.Platform.DbMigrator";
|
: SeedPathResolver.TenantDataFolder;
|
||||||
var safeDirectoryName = NormalizeSqlDataDirectoryName(dataDirectoryName);
|
|
||||||
var dir = new DirectoryInfo(_hostEnvironment.ContentRootPath);
|
|
||||||
|
|
||||||
while (dir != null)
|
return SeedPathResolver.GetPath(
|
||||||
{
|
_configuration,
|
||||||
var candidate = Path.Combine(dir.FullName, "src", dbMigratorName, "Seeds");
|
dataDirectoryName,
|
||||||
if (Directory.Exists(candidate))
|
scopeDirectoryName,
|
||||||
return Path.Combine(candidate, safeDirectoryName);
|
SeedPathResolver.ObjectDataFolder);
|
||||||
|
|
||||||
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";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Sozsoft.Platform.BlobStoring;
|
using Sozsoft.Platform.BlobStoring;
|
||||||
|
using Sozsoft.Platform.Data.Seeds;
|
||||||
using Sozsoft.Platform.Permissions;
|
using Sozsoft.Platform.Permissions;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
|
|
@ -32,13 +33,11 @@ public class FileManagementAppService : ApplicationService, IFileManagementAppSe
|
||||||
private const string IndexFileName = "index.json";
|
private const string IndexFileName = "index.json";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// CDN altinda fiziksel karsiligi olmayan, DbMigrator projesinin Seeds klasorune baglanan
|
/// CDN altinda fiziksel karsiligi olmayan, mount edilen seed klasorune baglanan sanal kok
|
||||||
/// sanal kok klasor. Sadece host baglaminda ve <see cref="PlatformPermissions.Setup.Migrate"/>
|
/// klasor. Sadece host baglaminda ve <see cref="PlatformPermissions.Setup.Migrate"/>
|
||||||
/// yetkisi olan kullaniciya gorunur.
|
/// yetkisi olan kullaniciya gorunur.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private const string SeedsFolderName = "Seeds";
|
private const string SeedsFolderName = "seeds";
|
||||||
|
|
||||||
private const string DbMigratorProjectName = "Sozsoft.Platform.DbMigrator";
|
|
||||||
|
|
||||||
private static readonly HashSet<string> ProtectedFolders = new(StringComparer.OrdinalIgnoreCase)
|
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);
|
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)
|
private async Task<bool> CanAccessSeedsAsync(string tenantId)
|
||||||
{
|
{
|
||||||
return IsHostTenant(tenantId) &&
|
return IsHostTenant(tenantId) &&
|
||||||
await AuthorizationService.IsGrantedAsync(PlatformPermissions.Setup.Migrate);
|
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()
|
private string ResolveSeedsRootPath()
|
||||||
{
|
{
|
||||||
var configuredPath = _configuration["App:DbMigratorSeedsPath"];
|
return SeedPathResolver.GetRootPath(_configuration);
|
||||||
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");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <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)
|
private void AddSeedsFolderItem(List<FileMetadata> items, string tenantId)
|
||||||
{
|
{
|
||||||
var seedsRootPath = ResolveSeedsRootPath();
|
var seedsRootPath = ResolveSeedsRootPath();
|
||||||
if (!Directory.Exists(seedsRootPath))
|
if (!Directory.Exists(seedsRootPath))
|
||||||
{
|
{
|
||||||
Logger.LogWarning("DbMigrator seeds directory was not found: {SeedsPath}", seedsRootPath);
|
Logger.LogWarning("Seeds directory was not found: {SeedsPath}", seedsRootPath);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,10 @@ using Volo.Abp.Domain.Repositories;
|
||||||
using Volo.Abp.MultiTenancy;
|
using Volo.Abp.MultiTenancy;
|
||||||
using Volo.Abp.PermissionManagement;
|
using Volo.Abp.PermissionManagement;
|
||||||
using Volo.Abp.Uow;
|
using Volo.Abp.Uow;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Sozsoft.Languages;
|
using Sozsoft.Languages;
|
||||||
|
using Sozsoft.Platform.Data.Seeds;
|
||||||
using Sozsoft.Platform.DynamicData;
|
using Sozsoft.Platform.DynamicData;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
@ -39,6 +41,7 @@ public class ListFormWizardAppService(
|
||||||
IRepository<Menu, Guid> repoMenu,
|
IRepository<Menu, Guid> repoMenu,
|
||||||
IPermissionGrantRepository permissionGrantRepository,
|
IPermissionGrantRepository permissionGrantRepository,
|
||||||
IHostEnvironment hostEnvironment,
|
IHostEnvironment hostEnvironment,
|
||||||
|
IConfiguration configuration,
|
||||||
LanguageTextAppService languageTextAppService,
|
LanguageTextAppService languageTextAppService,
|
||||||
IDynamicDataManager dynamicDataManager,
|
IDynamicDataManager dynamicDataManager,
|
||||||
ILogger<ListFormWizardAppService> logger
|
ILogger<ListFormWizardAppService> logger
|
||||||
|
|
@ -55,6 +58,7 @@ public class ListFormWizardAppService(
|
||||||
private readonly IRepository<Menu, Guid> repoMenu = repoMenu;
|
private readonly IRepository<Menu, Guid> repoMenu = repoMenu;
|
||||||
private readonly IPermissionGrantRepository permissionGrantRepository = permissionGrantRepository;
|
private readonly IPermissionGrantRepository permissionGrantRepository = permissionGrantRepository;
|
||||||
private readonly IHostEnvironment _hostEnvironment = hostEnvironment;
|
private readonly IHostEnvironment _hostEnvironment = hostEnvironment;
|
||||||
|
private readonly IConfiguration _configuration = configuration;
|
||||||
private readonly LanguageTextAppService _languageTextAppService = languageTextAppService;
|
private readonly LanguageTextAppService _languageTextAppService = languageTextAppService;
|
||||||
private readonly IDynamicDataManager _dynamicDataManager = dynamicDataManager;
|
private readonly IDynamicDataManager _dynamicDataManager = dynamicDataManager;
|
||||||
private readonly ILogger<ListFormWizardAppService> logger = logger;
|
private readonly ILogger<ListFormWizardAppService> logger = logger;
|
||||||
|
|
@ -531,8 +535,7 @@ public class ListFormWizardAppService(
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Wizard konfigürasyonunu JSON dosyası olarak kaydeder.
|
/// Wizard konfigürasyonunu JSON dosyası olarak kaydeder.
|
||||||
/// Önce ContentRootPath'ten yukarı çıkarak Sozsoft.Platform.DbMigrator/Seeds/WizardData dizinini arar.
|
/// Dosya, seeder'lar ve File Manager ile ortak olan seed klasörünün WizardData dizinine yazılır.
|
||||||
/// Bulamazsa ContentRootPath/Seeds/WizardData altına yazar.
|
|
||||||
/// Veritabanı silinip yeniden oluşturulduğunda WizardDataSeeder bu dosyaları okuyarak konfigürasyonu geri yükler.
|
/// Veritabanı silinip yeniden oluşturulduğunda WizardDataSeeder bu dosyaları okuyarak konfigürasyonu geri yükler.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task SaveWizardSeedFileAsync(ListFormWizardDto input, bool isDeletedField, bool isCreatedField, WizardInsertedRecordsDto inserted)
|
private async Task SaveWizardSeedFileAsync(ListFormWizardDto input, bool isDeletedField, bool isCreatedField, WizardInsertedRecordsDto inserted)
|
||||||
|
|
@ -755,31 +758,12 @@ public class ListFormWizardAppService(
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// DbMigrator projesinin Seeds/WizardData dizinini ContentRootPath'ten yukarı traversal ile bulur.
|
/// Wizard seed dosyalarinin yazilacagi WizardData dizinini dondurur.
|
||||||
/// Tüm işletim sistemlerinde Path.Combine kullanır, separator karakteri içermez.
|
/// Yol cozumu <see cref="SeedPathResolver"/> uzerinden seeder'lar ve File Manager ile ortaktir.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private string ResolveWizardSeedOutputPath()
|
private string ResolveWizardSeedOutputPath()
|
||||||
{
|
{
|
||||||
const string dbMigratorName = "Sozsoft.Platform.DbMigrator";
|
return SeedPathResolver.GetPath(_configuration, "WizardData");
|
||||||
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");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<HashSet<string>> GetTableColumnNamesAsync(string dataSourceCode, SelectCommandTypeEnum commandType, string selectCommand)
|
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")
|
var start = context.Criteria.FirstOrDefault(x => x.Kind == "Start")
|
||||||
?? throw new UserFriendlyException("Workflow başlangıç adımı bulunamadı.");
|
?? 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);
|
var result = await RunUntilWaitAsync(context, start);
|
||||||
await InsertWorkflowNoteAsync(
|
await InsertWorkflowNoteAsync(
|
||||||
context,
|
context,
|
||||||
$"Workflow Started: {start.Title}",
|
$"{Localize("App.WorkflowNote.SubjectStarted", "Workflow Started")}: {LocalizeNodeTitle(start.Title)}",
|
||||||
BuildWorkflowNoteContent(context.WorkflowNoteRows));
|
BuildWorkflowNoteContent(context.WorkflowNoteRows));
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|
@ -362,16 +362,16 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
|
||||||
await UpdateRowAsync(context, clear);
|
await UpdateRowAsync(context, clear);
|
||||||
MergeRowValues(context.Row, clear);
|
MergeRowValues(context.Row, clear);
|
||||||
|
|
||||||
context.WorkflowNoteRows.Add(("Reset By: ", ResolveCurrentUserDisplayName()));
|
context.WorkflowNoteRows.Add((NoteLabel("App.WorkflowNote.ResetBy", "Reset By"), ResolveCurrentUserDisplayName()));
|
||||||
if (!previousStatus.IsNullOrWhiteSpace())
|
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);
|
var result = await RunUntilWaitAsync(context, start);
|
||||||
await InsertWorkflowNoteAsync(
|
await InsertWorkflowNoteAsync(
|
||||||
context,
|
context,
|
||||||
$"Workflow Reset: {start.Title}",
|
$"{Localize("App.WorkflowNote.SubjectReset", "Workflow Reset")}: {LocalizeNodeTitle(start.Title)}",
|
||||||
BuildWorkflowNoteContent(context.WorkflowNoteRows));
|
BuildWorkflowNoteContent(context.WorkflowNoteRows));
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|
@ -443,12 +443,15 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
|
||||||
}
|
}
|
||||||
|
|
||||||
var next = FindNextCriteria(context.Criteria, input.Approved ? current.NextOnApprove : current.NextOnReject);
|
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);
|
AddWorkflowDecisionRows(context, current, input.Approved, input.Note ?? string.Empty);
|
||||||
var result = await RunUntilWaitAsync(context, next);
|
var result = await RunUntilWaitAsync(context, next);
|
||||||
|
var decisionSubject = input.Approved
|
||||||
|
? Localize("App.WorkflowNote.SubjectApproved", "Workflow Approved")
|
||||||
|
: Localize("App.WorkflowNote.SubjectRejected", "Workflow Rejected");
|
||||||
await InsertWorkflowNoteAsync(
|
await InsertWorkflowNoteAsync(
|
||||||
context,
|
context,
|
||||||
$"Workflow {(input.Approved ? "Approved" : "Rejected")}: {current.Title}",
|
$"{decisionSubject}: {LocalizeNodeTitle(current.Title)}",
|
||||||
BuildWorkflowNoteContent(context.WorkflowNoteRows));
|
BuildWorkflowNoteContent(context.WorkflowNoteRows));
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
|
@ -790,36 +793,41 @@ public class ListFormWorkflowAppService : PlatformAppService, IListFormWorkflowA
|
||||||
?? "System";
|
?? "System";
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void AddWorkflowDecisionRows(
|
private void AddWorkflowDecisionRows(
|
||||||
WorkflowRunContext context,
|
WorkflowRunContext context,
|
||||||
ListFormWorkflow current,
|
ListFormWorkflow current,
|
||||||
bool approved,
|
bool approved,
|
||||||
string description)
|
string description)
|
||||||
{
|
{
|
||||||
var action = approved ? "Approved" : "Rejected";
|
var action = approved
|
||||||
context.WorkflowNoteRows.Add(("Decision: ", $"{action}: {FormatNode(current)}"));
|
? Localize("App.WorkflowNote.Approved", "Approved")
|
||||||
context.WorkflowNoteRows.Add(("Description: ", description ?? string.Empty));
|
: 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,
|
WorkflowRunContext context,
|
||||||
ListFormWorkflow node)
|
ListFormWorkflow node)
|
||||||
{
|
{
|
||||||
var action = node.Kind switch
|
var action = node.Kind switch
|
||||||
{
|
{
|
||||||
"Start" => "Started",
|
"Start" => Localize("App.WorkflowNote.Started", "Started"),
|
||||||
"Compare" => "Evaluated",
|
"Compare" => Localize("App.WorkflowNote.Evaluated", "Evaluated"),
|
||||||
"Approval" => "Waiting Approval",
|
"Approval" => Localize("App.WorkflowNote.WaitingApproval", "Waiting Approval"),
|
||||||
"Inform" => "Informed",
|
"Inform" => Localize("App.WorkflowNote.Informed", "Informed"),
|
||||||
"End" => "Completed",
|
"End" => Localize("App.WorkflowNote.Completed", "Completed"),
|
||||||
_ => "Processed"
|
_ => Localize("App.WorkflowNote.Processed", "Processed")
|
||||||
};
|
};
|
||||||
|
|
||||||
context.WorkflowNoteRows.Add(($"{action}: ", $"{FormatNode(node)}"));
|
context.WorkflowNoteRows.Add(($"{action}: ", $"{FormatNode(node)}"));
|
||||||
|
|
||||||
if (!node.Approver.IsNullOrWhiteSpace())
|
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>";
|
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)
|
if (node == null)
|
||||||
{
|
{
|
||||||
return string.Empty;
|
return string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
var title = node.Title ?? string.Empty;
|
var title = LocalizeNodeTitle(node.Title);
|
||||||
var kind = node.Kind ?? string.Empty;
|
var kind = node.Kind.IsNullOrWhiteSpace()
|
||||||
return $"{title} ({kind} - {node.Id})";
|
? 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)
|
private static string Encode(string value)
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Sozsoft.Platform.EntityFrameworkCore;
|
using Sozsoft.Platform.EntityFrameworkCore;
|
||||||
using Volo.Abp.DependencyInjection;
|
using Volo.Abp.DependencyInjection;
|
||||||
|
|
@ -26,28 +27,31 @@ namespace Sozsoft.Platform.Data.Seeds;
|
||||||
public class AfterAllMigrationsSqlExecutor : IAfterAllMigrationsSqlExecutor, ITransientDependency
|
public class AfterAllMigrationsSqlExecutor : IAfterAllMigrationsSqlExecutor, ITransientDependency
|
||||||
{
|
{
|
||||||
/// <summary>HostData/TenantData altındaki, migration sonrası da çalıştırılacak script klasörünün adı.</summary>
|
/// <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>
|
/// <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>
|
/// <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 =
|
private static readonly Regex SafeIdentifierRegex =
|
||||||
new(@"^[A-Za-z_][A-Za-z0-9_]{0,126}$", RegexOptions.Compiled);
|
new(@"^[A-Za-z_][A-Za-z0-9_]{0,126}$", RegexOptions.Compiled);
|
||||||
|
|
||||||
private readonly IDbContextProvider<PlatformDbContext> _dbContextProvider;
|
private readonly IDbContextProvider<PlatformDbContext> _dbContextProvider;
|
||||||
private readonly IUnitOfWorkManager _unitOfWorkManager;
|
private readonly IUnitOfWorkManager _unitOfWorkManager;
|
||||||
|
private readonly IConfiguration _configuration;
|
||||||
private readonly ILogger<AfterAllMigrationsSqlExecutor> _logger;
|
private readonly ILogger<AfterAllMigrationsSqlExecutor> _logger;
|
||||||
|
|
||||||
public AfterAllMigrationsSqlExecutor(
|
public AfterAllMigrationsSqlExecutor(
|
||||||
IDbContextProvider<PlatformDbContext> dbContextProvider,
|
IDbContextProvider<PlatformDbContext> dbContextProvider,
|
||||||
IUnitOfWorkManager unitOfWorkManager,
|
IUnitOfWorkManager unitOfWorkManager,
|
||||||
|
IConfiguration configuration,
|
||||||
ILogger<AfterAllMigrationsSqlExecutor> logger)
|
ILogger<AfterAllMigrationsSqlExecutor> logger)
|
||||||
{
|
{
|
||||||
_dbContextProvider = dbContextProvider;
|
_dbContextProvider = dbContextProvider;
|
||||||
_unitOfWorkManager = unitOfWorkManager;
|
_unitOfWorkManager = unitOfWorkManager;
|
||||||
|
_configuration = configuration;
|
||||||
_logger = logger;
|
_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
|
var dataDirectoryName = DefaultDatabaseProvider == DatabaseProvider.PostgreSql
|
||||||
? "PostgresData"
|
? SeedPathResolver.PostgresDataFolder
|
||||||
: "SqlData";
|
: SeedPathResolver.SqlDataFolder;
|
||||||
|
|
||||||
return Path.Combine(Directory.GetCurrentDirectory(), "Seeds", dataDirectoryName, scopeDirectoryName, DirectoryName);
|
return SeedPathResolver.GetPath(_configuration, dataDirectoryName, scopeDirectoryName, DirectoryName);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|
|
||||||
|
|
@ -3588,6 +3588,12 @@
|
||||||
"en": "Workflow has already been started for the selected record",
|
"en": "Workflow has already been started for the selected record",
|
||||||
"tr": "Seçili kayıt için iş akışı zaten başlamış."
|
"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",
|
"resourceName": "Platform",
|
||||||
"key": "App.ListForm.WorkflowReset",
|
"key": "App.ListForm.WorkflowReset",
|
||||||
|
|
@ -6852,6 +6858,12 @@
|
||||||
"en": "Search blog posts...",
|
"en": "Search blog posts...",
|
||||||
"tr": "Blog yazıları ara..."
|
"tr": "Blog yazıları ara..."
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"resourceName": "Platform",
|
||||||
|
"key": "App.PublicBlog.Empty",
|
||||||
|
"en": "No blog posts found.",
|
||||||
|
"tr": "Blog yazısı bulunamadı."
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"resourceName": "Platform",
|
"resourceName": "Platform",
|
||||||
"key": "App.BlogSubscribe.SubscribeDesc",
|
"key": "App.BlogSubscribe.SubscribeDesc",
|
||||||
|
|
@ -18276,6 +18288,126 @@
|
||||||
"en": "Undefined",
|
"en": "Undefined",
|
||||||
"tr": "Tanımsız"
|
"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",
|
"resourceName": "Platform",
|
||||||
"key": "App.MessengerWidget.Title",
|
"key": "App.MessengerWidget.Title",
|
||||||
|
|
@ -25050,6 +25182,12 @@
|
||||||
"en": "Approver",
|
"en": "Approver",
|
||||||
"tr": "Onaylanacak kişi"
|
"tr": "Onaylanacak kişi"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"resourceName": "Platform",
|
||||||
|
"key": "App.WorkflowKind.Compare",
|
||||||
|
"en": "Comparison",
|
||||||
|
"tr": "Karşılaştırma"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"resourceName": "Platform",
|
"resourceName": "Platform",
|
||||||
"key": "App.WorkflowKind.End",
|
"key": "App.WorkflowKind.End",
|
||||||
|
|
@ -25062,6 +25200,12 @@
|
||||||
"en": "Inform",
|
"en": "Inform",
|
||||||
"tr": "Bilgilendirme"
|
"tr": "Bilgilendirme"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"resourceName": "Platform",
|
||||||
|
"key": "App.WorkflowKind.Start",
|
||||||
|
"en": "Start",
|
||||||
|
"tr": "Başlangıç"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"resourceName": "Platform",
|
"resourceName": "Platform",
|
||||||
"key": "App.WorkflowKind.Step",
|
"key": "App.WorkflowKind.Step",
|
||||||
|
|
@ -25074,6 +25218,12 @@
|
||||||
"en": "No rule",
|
"en": "No rule",
|
||||||
"tr": "Kural yok"
|
"tr": "Kural yok"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"resourceName": "Platform",
|
||||||
|
"key": "App.WorkflowOutcome.Next",
|
||||||
|
"en": "Next",
|
||||||
|
"tr": "Sonraki"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"resourceName": "Platform",
|
"resourceName": "Platform",
|
||||||
"key": "App.WorkflowOutcome.CompareState",
|
"key": "App.WorkflowOutcome.CompareState",
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Sozsoft.Platform.EntityFrameworkCore;
|
using Sozsoft.Platform.EntityFrameworkCore;
|
||||||
using Volo.Abp.Data;
|
using Volo.Abp.Data;
|
||||||
|
|
@ -14,27 +16,50 @@ using static Sozsoft.Settings.SettingsConsts;
|
||||||
namespace Sozsoft.Platform.Data.Seeds;
|
namespace Sozsoft.Platform.Data.Seeds;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// SqlTableDesigner üzerinden deploy edilen tabloları Seeds/SqlData/*.sql dosyalarından okuyarak veritabanına uygular.
|
/// SqlTableDesigner ve SqlQueryManager üzerinden deploy edilen nesneleri
|
||||||
/// Her dosya, tek bir tabloya (veya ilgili ALTER/INDEX script'lerine) ait IF OBJECT_ID kontrolü içeren T-SQL batch'leri içermelidir.
|
/// <c>{SqlData|PostgresData}</c> klasöründeki .sql dosyalarından okuyarak veritabanına uygular.
|
||||||
/// Veritabanı silinip yeniden oluşturulduğunda bu seeder tüm tablo scriptlerini yeniden çalıştırır.
|
/// 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>
|
/// </summary>
|
||||||
public class SqlDataSeeder : IDataSeedContributor, ITransientDependency
|
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 IDbContextProvider<PlatformDbContext> _dbContextProvider;
|
||||||
|
private readonly IConfiguration _configuration;
|
||||||
private readonly ILogger<SqlDataSeeder> _logger;
|
private readonly ILogger<SqlDataSeeder> _logger;
|
||||||
|
|
||||||
public SqlDataSeeder(
|
public SqlDataSeeder(
|
||||||
IDbContextProvider<PlatformDbContext> dbContextProvider,
|
IDbContextProvider<PlatformDbContext> dbContextProvider,
|
||||||
|
IConfiguration configuration,
|
||||||
ILogger<SqlDataSeeder> logger)
|
ILogger<SqlDataSeeder> logger)
|
||||||
{
|
{
|
||||||
_dbContextProvider = dbContextProvider;
|
_dbContextProvider = dbContextProvider;
|
||||||
|
_configuration = configuration;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SeedAsync(DataSeedContext context)
|
public async Task SeedAsync(DataSeedContext context)
|
||||||
{
|
{
|
||||||
var dataDirectoryName = GetDataDirectoryName();
|
var dataDirectoryName = GetDataDirectoryName();
|
||||||
var sqlDataPath = Path.Combine(Directory.GetCurrentDirectory(), "Seeds", dataDirectoryName);
|
var sqlDataPath = SeedPathResolver.GetPath(_configuration, dataDirectoryName);
|
||||||
if (!Directory.Exists(sqlDataPath))
|
if (!Directory.Exists(sqlDataPath))
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Seeds/{DirectoryName} directory not found, skipping SqlDataSeeder.", dataDirectoryName);
|
_logger.LogInformation("Seeds/{DirectoryName} directory not found, skipping SqlDataSeeder.", dataDirectoryName);
|
||||||
|
|
@ -107,15 +132,16 @@ public class SqlDataSeeder : IDataSeedContributor, ITransientDependency
|
||||||
private static string GetDataDirectoryName()
|
private static string GetDataDirectoryName()
|
||||||
{
|
{
|
||||||
return DefaultDatabaseProvider == DatabaseProvider.PostgreSql
|
return DefaultDatabaseProvider == DatabaseProvider.PostgreSql
|
||||||
? "PostgresData"
|
? SeedPathResolver.PostgresDataFolder
|
||||||
: "SqlData";
|
: SeedPathResolver.SqlDataFolder;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string[] GetSqlFiles(string dataDirectoryPath, bool isHostSeed)
|
private string[] GetSqlFiles(string dataDirectoryPath, bool isHostSeed)
|
||||||
{
|
{
|
||||||
var sqlFiles = Directory.GetFiles(dataDirectoryPath, "*.sql")
|
var sqlFiles = new List<string>();
|
||||||
.OrderBy(f => Path.GetFileName(f))
|
|
||||||
.ToList();
|
// Klasor kokunde dosya beklenmez; eskiden kalanlar uyari ile islenir.
|
||||||
|
sqlFiles.AddRange(GetLegacyRootSqlFiles(dataDirectoryPath));
|
||||||
|
|
||||||
var scopeDirectoryName = isHostSeed
|
var scopeDirectoryName = isHostSeed
|
||||||
? AfterAllMigrationsSqlExecutor.HostDataDirectoryName
|
? AfterAllMigrationsSqlExecutor.HostDataDirectoryName
|
||||||
|
|
@ -124,22 +150,44 @@ public class SqlDataSeeder : IDataSeedContributor, ITransientDependency
|
||||||
var scopePath = Path.Combine(dataDirectoryPath, scopeDirectoryName);
|
var scopePath = Path.Combine(dataDirectoryPath, scopeDirectoryName);
|
||||||
if (Directory.Exists(scopePath))
|
if (Directory.Exists(scopePath))
|
||||||
{
|
{
|
||||||
sqlFiles.AddRange(Directory.GetFiles(scopePath, "*.sql")
|
sqlFiles.AddRange(GetOrderedSqlFiles(Path.Combine(scopePath, ObjectDataDirectoryName)));
|
||||||
.OrderBy(f => Path.GetFileName(f)));
|
sqlFiles.AddRange(GetLegacyRootSqlFiles(scopePath));
|
||||||
|
|
||||||
// {HostData|TenantData}/ExecuteData: nesneler burada da oluşturulur, ayrıca tüm
|
// {HostData|TenantData}/ExecuteData: nesneler burada da oluşturulur, ayrıca tüm
|
||||||
// migration bittikten sonra AfterAllMigrationsSqlExecutor dosya adı üzerinden çalıştırır.
|
// migration bittikten sonra AfterAllMigrationsSqlExecutor dosya adı üzerinden çalıştırır.
|
||||||
var executeDataPath = Path.Combine(scopePath, AfterAllMigrationsSqlExecutor.DirectoryName);
|
sqlFiles.AddRange(GetOrderedSqlFiles(
|
||||||
if (Directory.Exists(executeDataPath))
|
Path.Combine(scopePath, AfterAllMigrationsSqlExecutor.DirectoryName)));
|
||||||
{
|
|
||||||
sqlFiles.AddRange(Directory.GetFiles(executeDataPath, "*.sql")
|
|
||||||
.OrderBy(f => Path.GetFileName(f)));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return sqlFiles.ToArray();
|
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)
|
private static (string Action, string? ObjectName, string? ObjectType) ExtractSqlInfo(string sql)
|
||||||
{
|
{
|
||||||
var patterns = new[]
|
var patterns = new[]
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Data;
|
using System.Data;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Sozsoft.Languages.Entities;
|
using Sozsoft.Languages.Entities;
|
||||||
using Sozsoft.Languages.Languages;
|
using Sozsoft.Languages.Languages;
|
||||||
|
|
@ -36,6 +37,7 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
|
||||||
private readonly IRepository<ListForm, Guid> _repoListForm;
|
private readonly IRepository<ListForm, Guid> _repoListForm;
|
||||||
private readonly IRepository<ListFormField, Guid> _repoListFormField;
|
private readonly IRepository<ListFormField, Guid> _repoListFormField;
|
||||||
private readonly IRepository<ListFormWorkflow, string> _repoListFormWorkflow;
|
private readonly IRepository<ListFormWorkflow, string> _repoListFormWorkflow;
|
||||||
|
private readonly IConfiguration _configuration;
|
||||||
private readonly ILogger<WizardDataSeeder> _logger;
|
private readonly ILogger<WizardDataSeeder> _logger;
|
||||||
|
|
||||||
private readonly string _cultureNameDefault = PlatformConsts.DefaultLanguage;
|
private readonly string _cultureNameDefault = PlatformConsts.DefaultLanguage;
|
||||||
|
|
@ -51,6 +53,7 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
|
||||||
IRepository<ListForm, Guid> repoListForm,
|
IRepository<ListForm, Guid> repoListForm,
|
||||||
IRepository<ListFormField, Guid> repoListFormField,
|
IRepository<ListFormField, Guid> repoListFormField,
|
||||||
IRepository<ListFormWorkflow, string> repoListFormWorkflow,
|
IRepository<ListFormWorkflow, string> repoListFormWorkflow,
|
||||||
|
IConfiguration configuration,
|
||||||
ILogger<WizardDataSeeder> logger)
|
ILogger<WizardDataSeeder> logger)
|
||||||
{
|
{
|
||||||
_repoLangKey = repoLangKey;
|
_repoLangKey = repoLangKey;
|
||||||
|
|
@ -62,12 +65,13 @@ public class WizardDataSeeder : IDataSeedContributor, ITransientDependency
|
||||||
_repoListForm = repoListForm;
|
_repoListForm = repoListForm;
|
||||||
_repoListFormField = repoListFormField;
|
_repoListFormField = repoListFormField;
|
||||||
_repoListFormWorkflow = repoListFormWorkflow;
|
_repoListFormWorkflow = repoListFormWorkflow;
|
||||||
|
_configuration = configuration;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task SeedAsync(DataSeedContext context)
|
public async Task SeedAsync(DataSeedContext context)
|
||||||
{
|
{
|
||||||
var wizardDataPath = Path.Combine(Directory.GetCurrentDirectory(), "Seeds", "WizardData");
|
var wizardDataPath = SeedPathResolver.GetPath(_configuration, "WizardData");
|
||||||
if (!Directory.Exists(wizardDataPath))
|
if (!Directory.Exists(wizardDataPath))
|
||||||
{
|
{
|
||||||
_logger.LogInformation("WizardData directory not found, skipping.");
|
_logger.LogInformation("WizardData directory not found, skipping.");
|
||||||
|
|
|
||||||
|
|
@ -77,50 +77,6 @@
|
||||||
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
<CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory>
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
</Content>
|
</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>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
{
|
{
|
||||||
"App": {
|
"App": {
|
||||||
|
"SeedsPath": "/etc/api/seeds",
|
||||||
"CdnUrl": "https://cdn-api.sozsoft.com"
|
"CdnUrl": "https://cdn-api.sozsoft.com"
|
||||||
},
|
},
|
||||||
"ConnectionStrings": {
|
"ConnectionStrings": {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
{
|
{
|
||||||
"App": {
|
"App": {
|
||||||
|
"SeedsPath": "/etc/api/seeds",
|
||||||
"CdnUrl": "https://cdn.sozsoft.com"
|
"CdnUrl": "https://cdn.sozsoft.com"
|
||||||
},
|
},
|
||||||
"ConnectionStrings": {
|
"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.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Sozsoft.Platform.Data.Seeds;
|
||||||
|
|
||||||
namespace Sozsoft.Platform.DbStartup;
|
namespace Sozsoft.Platform.DbStartup;
|
||||||
|
|
||||||
|
|
@ -33,6 +34,9 @@ public static class SetupMigrationRunner
|
||||||
|
|
||||||
private const int DefaultTimeoutMinutes = 30;
|
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">
|
/// <param name="processCancellationToken">
|
||||||
/// Sürecin yaşam süresini kontrol eden token. Bu <b>istemci isteğinin</b> token'ı DEĞİL,
|
/// 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
|
/// 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();
|
process.Start();
|
||||||
|
|
||||||
async Task PumpAsync(StreamReader reader, string level)
|
async Task PumpAsync(StreamReader reader, string level)
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
{
|
{
|
||||||
"App": {
|
"App": {
|
||||||
"SelfUrl": "https://dev-api.sozsoft.com",
|
"SelfUrl": "https://dev-api.sozsoft.com",
|
||||||
"ClientUrl": "https://dev.sozsoft.com",
|
"ClientUrl": "https://dev.sozsoft.com",
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
"AttachmentsPath": "/etc/api/mail-queue/attachments",
|
"AttachmentsPath": "/etc/api/mail-queue/attachments",
|
||||||
"CdnUrl": "https://dev-cdn.sozsoft.com",
|
"CdnUrl": "https://dev-cdn.sozsoft.com",
|
||||||
"CdnPath": "/etc/api/cdn",
|
"CdnPath": "/etc/api/cdn",
|
||||||
"DbMigratorSeedsPath": "/etc/api/seeds",
|
"SeedsPath": "/etc/api/seeds",
|
||||||
"BaseDomain": "sozsoft.com",
|
"BaseDomain": "sozsoft.com",
|
||||||
"BackupPath": "/var/opt/mssql/backup"
|
"BackupPath": "/var/opt/mssql/backup"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
{
|
{
|
||||||
"App": {
|
"App": {
|
||||||
"SelfUrl": "https://api.sozsoft.com",
|
"SelfUrl": "https://api.sozsoft.com",
|
||||||
"ClientUrl": "https://sozsoft.com",
|
"ClientUrl": "https://sozsoft.com",
|
||||||
|
|
@ -7,7 +7,7 @@
|
||||||
"AttachmentsPath": "/etc/api/mail-queue/attachments",
|
"AttachmentsPath": "/etc/api/mail-queue/attachments",
|
||||||
"CdnUrl": "https://cdn.sozsoft.com",
|
"CdnUrl": "https://cdn.sozsoft.com",
|
||||||
"CdnPath": "/etc/api/cdn",
|
"CdnPath": "/etc/api/cdn",
|
||||||
"DbMigratorSeedsPath": "/etc/api/seeds",
|
"SeedsPath": "/etc/api/seeds",
|
||||||
"BaseDomain": "sozsoft.com",
|
"BaseDomain": "sozsoft.com",
|
||||||
"BackupPath": "/var/opt/mssql/backup"
|
"BackupPath": "/var/opt/mssql/backup"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,6 @@ networks:
|
||||||
volumes:
|
volumes:
|
||||||
cdn:
|
cdn:
|
||||||
api-keys:
|
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:
|
sql-backups:
|
||||||
external: true
|
external: true
|
||||||
name: sozsoft-platform-data_sql-backups
|
name: sozsoft-platform-data_sql-backups
|
||||||
|
|
@ -25,7 +22,10 @@ services:
|
||||||
- ASPNETCORE_ENVIRONMENT=Dev
|
- ASPNETCORE_ENVIRONMENT=Dev
|
||||||
- SEED=${SEED}
|
- SEED=${SEED}
|
||||||
volumes:
|
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:
|
networks:
|
||||||
- db
|
- db
|
||||||
|
|
||||||
|
|
@ -40,7 +40,9 @@ services:
|
||||||
volumes:
|
volumes:
|
||||||
- cdn:/etc/api/cdn
|
- cdn:/etc/api/cdn
|
||||||
- sql-backups:/etc/api/cdn/host/backup
|
- 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
|
- api-keys:/root/.aspnet/DataProtection-Keys
|
||||||
# Surum notlari imaja gomulmez; deploy sirasinda git tag'lerinden uretilip
|
# Surum notlari imaja gomulmez; deploy sirasinda git tag'lerinden uretilip
|
||||||
# buradan okunur (ReleaseNoteStartupService). Boylece yeni tag API imajini
|
# buradan okunur (ReleaseNoteStartupService). Boylece yeni tag API imajini
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { FiBell, FiCheck, FiGitBranch, FiPlay, FiSlash } from 'react-icons/fi'
|
import { FiBell, FiCheck, FiGitBranch, FiPlay, FiSlash } from 'react-icons/fi'
|
||||||
|
|
||||||
export const kindOptions = [
|
export const kindOptions = [
|
||||||
{ value: 'Start', label: 'App.VideoRoom.Start' },
|
{ value: 'Start', label: 'App.WorkflowKind.Start' },
|
||||||
{ value: 'Compare', label: 'App.ScriptBuilder.Comparison' },
|
{ value: 'Compare', label: 'App.WorkflowKind.Compare' },
|
||||||
{ value: 'Approval', label: 'App.WorkflowKind.Approval' },
|
{ value: 'Approval', label: 'App.WorkflowKind.Approval' },
|
||||||
{ value: 'Inform', label: 'App.WorkflowKind.Inform' },
|
{ value: 'Inform', label: 'App.WorkflowKind.Inform' },
|
||||||
{ value: 'End', label: 'App.WorkflowKind.End' },
|
{ 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. */
|
/** Çıkış etiketleri lokalizasyon anahtarı olarak taşınır; ekranda `translate` ile çözülür. */
|
||||||
export const OUTCOME_KEYS = {
|
export const OUTCOME_KEYS = {
|
||||||
nextOnStart: 'App.Platform.Next2',
|
nextOnStart: 'App.WorkflowOutcome.Next',
|
||||||
nextOnTrue: 'App.WorkflowOutcome.True',
|
nextOnTrue: 'App.WorkflowOutcome.True',
|
||||||
nextOnFalse: 'App.WorkflowOutcome.False',
|
nextOnFalse: 'App.WorkflowOutcome.False',
|
||||||
nextOnApprove: 'App.ListFormWorkflow.Approve',
|
nextOnApprove: 'App.ListFormWorkflow.Approve',
|
||||||
|
|
@ -476,8 +476,8 @@ export function normalizeCriteria(item: WorkflowCriteriaForm): SaveCriteriaInput
|
||||||
export function defaultTitle(kind: string) {
|
export function defaultTitle(kind: string) {
|
||||||
return (
|
return (
|
||||||
{
|
{
|
||||||
Start: 'App.VideoRoom.Start',
|
Start: 'App.WorkflowKind.Start',
|
||||||
Compare: 'App.ScriptBuilder.Comparison',
|
Compare: 'App.WorkflowKind.Compare',
|
||||||
Approval: 'App.WorkflowKind.Approval',
|
Approval: 'App.WorkflowKind.Approval',
|
||||||
Inform: 'App.WorkflowKind.Inform',
|
Inform: 'App.WorkflowKind.Inform',
|
||||||
End: 'App.WorkflowKind.End',
|
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 {
|
export function emptyCompareOutcome1(label = 'Durum', compareColumn = 'Price'): CompareOutcomeDto {
|
||||||
return {
|
return {
|
||||||
label,
|
label,
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,7 @@ const buildDownloadUrl = (item: FileItemType, fallbackTenantId?: string): string
|
||||||
return `${VITE_CDN_URL}/${tenantSegment}/${segments.map(encodeURIComponent).join('/')}`
|
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 isSeedsItem = (item: FileItemType): boolean => {
|
||||||
const relativePath = item.path || item.id.replace(/\|/g, '/')
|
const relativePath = item.path || item.id.replace(/\|/g, '/')
|
||||||
return relativePath.split('/')[0]?.toLowerCase() === 'seeds'
|
return relativePath.split('/')[0]?.toLowerCase() === 'seeds'
|
||||||
|
|
@ -419,7 +419,7 @@ const FileManager = () => {
|
||||||
|
|
||||||
const handleDownload = useCallback(
|
const handleDownload = useCallback(
|
||||||
async (item: FileItemType) => {
|
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)) {
|
if (isSeedsItem(item)) {
|
||||||
try {
|
try {
|
||||||
const blob = await fileManagementService.downloadFile(item.id, currentTenantId)
|
const blob = await fileManagementService.downloadFile(item.id, currentTenantId)
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import {
|
||||||
collectLinks,
|
collectLinks,
|
||||||
getNodeOutcomes,
|
getNodeOutcomes,
|
||||||
outcomeLabel,
|
outcomeLabel,
|
||||||
|
translateWorkflowLabel,
|
||||||
OUTCOME_KEYS,
|
OUTCOME_KEYS,
|
||||||
type WorkflowLink,
|
type WorkflowLink,
|
||||||
type WorkflowOutcome,
|
type WorkflowOutcome,
|
||||||
|
|
@ -342,7 +343,7 @@ function FlowNode({
|
||||||
{translate('::' + kindOptions.find((option) => option.value === item.kind)?.label)}
|
{translate('::' + kindOptions.find((option) => option.value === item.kind)?.label)}
|
||||||
</span>
|
</span>
|
||||||
<strong className="break-words text-sm leading-tight [overflow-wrap:anywhere]">
|
<strong className="break-words text-sm leading-tight [overflow-wrap:anywhere]">
|
||||||
{item.title}
|
{translateWorkflowLabel(item.title, translate)}
|
||||||
</strong>
|
</strong>
|
||||||
<small
|
<small
|
||||||
className={
|
className={
|
||||||
|
|
@ -386,9 +387,7 @@ function FlowNode({
|
||||||
onBeginLink(item.id, outcome.field)
|
onBeginLink(item.id, outcome.field)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{outcome.label.startsWith('ListForms.')
|
{translateWorkflowLabel(outcome.label, translate)}
|
||||||
? translate('::' + outcome.label)
|
|
||||||
: outcome.label}
|
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -505,12 +504,15 @@ function Arrow({
|
||||||
}
|
}
|
||||||
|
|
||||||
function ArrowLabel({ link, pendingLink }: { link: WorkflowLink; pendingLink: PendingLink }) {
|
function ArrowLabel({ link, pendingLink }: { link: WorkflowLink; pendingLink: PendingLink }) {
|
||||||
|
const { translate } = useLocalization()
|
||||||
|
|
||||||
if (!link.label) return null
|
if (!link.label) return null
|
||||||
|
|
||||||
const route = buildArrowRoute(link.source, link.target, link.sourcePort)
|
const route = buildArrowRoute(link.source, link.target, link.sourcePort)
|
||||||
const labelPoint = route.labelPoint
|
const labelPoint = route.labelPoint
|
||||||
const tone = linkTone(link)
|
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 =
|
const isActive =
|
||||||
pendingLink?.sourceId === link.source.id && pendingLink?.outcome === link.sourcePort?.field
|
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}
|
y={labelPoint.y}
|
||||||
textAnchor="middle"
|
textAnchor="middle"
|
||||||
>
|
>
|
||||||
{link.label}
|
{label}
|
||||||
</text>
|
</text>
|
||||||
</g>
|
</g>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -306,7 +306,7 @@ const SqlObjectExplorer = ({
|
||||||
dataSourceCode: dataSource,
|
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 {
|
try {
|
||||||
const fileNames = [...new Set(getSqlDataFileCandidates(dropConfirm.node).filter(Boolean))]
|
const fileNames = [...new Set(getSqlDataFileCandidates(dropConfirm.node).filter(Boolean))]
|
||||||
if (fileNames.length > 0) {
|
if (fileNames.length > 0) {
|
||||||
|
|
|
||||||
|
|
@ -958,7 +958,7 @@ GO`,
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
<DbMigrateButton />
|
<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">
|
<Link to={ROUTES_ENUM.protected.admin.files} target="_blank">
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|
|
||||||
|
|
@ -1952,7 +1952,7 @@ const SqlTableDesignerDialog = ({
|
||||||
</Notification>,
|
</Notification>,
|
||||||
{ placement: 'bottom-end' },
|
{ 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
|
// 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.
|
// can recreate the table from scratch when the database is wiped.
|
||||||
try {
|
try {
|
||||||
|
|
|
||||||
|
|
@ -457,7 +457,16 @@ export const NoteList: React.FC<NoteListProps> = ({
|
||||||
{note.subject && (
|
{note.subject && (
|
||||||
<h4 className="text-sm font-bold text-gray-900 mb-1">{note.subject}</h4>
|
<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>
|
</div>
|
||||||
|
|
||||||
{/* Files */}
|
{/* Files */}
|
||||||
|
|
@ -579,7 +588,10 @@ export const NoteList: React.FC<NoteListProps> = ({
|
||||||
<Badge className={opClass} content={opLabel} />
|
<Badge className={opClass} content={opLabel} />
|
||||||
{rowLabel ? <Badge className="bg-gray-600" content={rowLabel} /> : null}
|
{rowLabel ? <Badge className="bg-gray-600" content={rowLabel} /> : null}
|
||||||
</div>
|
</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 || '')}
|
{(log.httpMethod || 'HTTP') + ' ' + (log.url || '')}
|
||||||
</div>
|
</div>
|
||||||
{changeLines.length > 0 && (
|
{changeLines.length > 0 && (
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ import {
|
||||||
openImageInNewTab,
|
openImageInNewTab,
|
||||||
showImageHoverPreview,
|
showImageHoverPreview,
|
||||||
} from '../form/editors/imageHoverPreview'
|
} from '../form/editors/imageHoverPreview'
|
||||||
|
import { translateWorkflowLabel } from '@/utils/workflow/workflowHelpers'
|
||||||
|
|
||||||
const cellTemplateMultiValue = (
|
const cellTemplateMultiValue = (
|
||||||
cellElement: HTMLElement,
|
cellElement: HTMLElement,
|
||||||
|
|
@ -598,6 +599,9 @@ const useListFormColumns = ({
|
||||||
if (!gridDto || !columnFormats) {
|
if (!gridDto || !columnFormats) {
|
||||||
return columns
|
return columns
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const workflowApprovalStatusField =
|
||||||
|
gridDto.gridOptions?.workflowDto?.approvalStatusFieldName?.toLowerCase()
|
||||||
columnFormats.forEach((colData) => {
|
columnFormats.forEach((colData) => {
|
||||||
if (!colData.canRead || !colData.isActive) {
|
if (!colData.canRead || !colData.isActive) {
|
||||||
return
|
return
|
||||||
|
|
@ -608,6 +612,16 @@ const useListFormColumns = ({
|
||||||
column.dataField = colData.fieldName
|
column.dataField = colData.fieldName
|
||||||
if (colData.dataType) column.dataType = colData.dataType as DataType
|
if (colData.dataType) column.dataType = colData.dataType as DataType
|
||||||
if (colData.captionName) column.caption = translate('::' + colData.captionName)
|
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
|
if (colData.width > 0) column.width = colData.width
|
||||||
column.visible = colData.visible
|
column.visible = colData.visible
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ import { layoutTypes, ListViewLayoutType } from '../admin/listForm/edit/types'
|
||||||
import { useStoreState } from '@/store'
|
import { useStoreState } from '@/store'
|
||||||
import { workflowService } from '@/services/workflow.service'
|
import { workflowService } from '@/services/workflow.service'
|
||||||
import type { WorkflowRunResultDto } 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 = {
|
type ToolbarModalData = {
|
||||||
open: boolean
|
open: boolean
|
||||||
|
|
@ -149,6 +149,15 @@ const useToolbar = ({
|
||||||
const workflowOptions = grdOpt.workflowDto
|
const workflowOptions = grdOpt.workflowDto
|
||||||
const approvalCriteria =
|
const approvalCriteria =
|
||||||
workflowOptions?.criteria?.filter((item) => item.kind === 'Approval') ?? []
|
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 (
|
if (
|
||||||
workflowOptions?.approvalStatusFieldName &&
|
workflowOptions?.approvalStatusFieldName &&
|
||||||
approvalCriteria.length > 0 &&
|
approvalCriteria.length > 0 &&
|
||||||
|
|
@ -166,7 +175,7 @@ const useToolbar = ({
|
||||||
text: toToolbarText(translate('::App.ListForm.WorkflowReset')),
|
text: toToolbarText(translate('::App.ListForm.WorkflowReset')),
|
||||||
hint: translate('::App.ListForm.WorkflowReset'),
|
hint: translate('::App.ListForm.WorkflowReset'),
|
||||||
visible: true,
|
visible: true,
|
||||||
disabled: true,
|
disabled: workflowDisabled('workflowReset'),
|
||||||
onClick: async () => {
|
onClick: async () => {
|
||||||
const selectedRowKeys = (await Promise.resolve(
|
const selectedRowKeys = (await Promise.resolve(
|
||||||
getSelectedRowKeys() as any,
|
getSelectedRowKeys() as any,
|
||||||
|
|
@ -241,10 +250,10 @@ const useToolbar = ({
|
||||||
showText: 'always',
|
showText: 'always',
|
||||||
options: {
|
options: {
|
||||||
icon: 'play',
|
icon: 'play',
|
||||||
text: toToolbarText('Workflow Start'),
|
text: toToolbarText(translate('::App.ListForm.WorkflowStart')),
|
||||||
hint: 'Workflow Start',
|
hint: translate('::App.ListForm.WorkflowStart'),
|
||||||
visible: true,
|
visible: true,
|
||||||
disabled: true,
|
disabled: workflowDisabled('workflowStart'),
|
||||||
onClick: async () => {
|
onClick: async () => {
|
||||||
const selectedRowKeys = (await Promise.resolve(
|
const selectedRowKeys = (await Promise.resolve(
|
||||||
getSelectedRowKeys() as any,
|
getSelectedRowKeys() as any,
|
||||||
|
|
@ -320,10 +329,10 @@ const useToolbar = ({
|
||||||
showText: 'always',
|
showText: 'always',
|
||||||
options: {
|
options: {
|
||||||
icon: 'check',
|
icon: 'check',
|
||||||
text: toToolbarText(criteria.title),
|
text: toToolbarText(translateWorkflowLabel(criteria.title, translate)),
|
||||||
hint: criteria.title,
|
hint: translateWorkflowLabel(criteria.title, translate),
|
||||||
visible: true,
|
visible: true,
|
||||||
disabled: true,
|
disabled: workflowDisabled(`workflowApproval_${criteria.id}`),
|
||||||
onClick: async () => {
|
onClick: async () => {
|
||||||
const selectedRowKeys = (await Promise.resolve(
|
const selectedRowKeys = (await Promise.resolve(
|
||||||
getSelectedRowKeys() as any,
|
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(
|
function isWorkflowApprovalCriteriaActive(
|
||||||
row: Record<string, unknown>,
|
row: Record<string, unknown>,
|
||||||
workflowOptions: WorkflowDto,
|
workflowOptions: WorkflowDto,
|
||||||
|
|
@ -1012,7 +1027,7 @@ function WorkflowApprovalDecisionDialog({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<h5 className="mb-4">{criteriaTitle}</h5>
|
<h5 className="mb-4">{translateWorkflowLabel(criteriaTitle, translate)}</h5>
|
||||||
<p className="mb-4">
|
<p className="mb-4">
|
||||||
{translate('::App.Listform.ListformField.WorkflowDecisionMessage', {
|
{translate('::App.Listform.ListformField.WorkflowDecisionMessage', {
|
||||||
0: keys.length,
|
0: keys.length,
|
||||||
|
|
|
||||||
|
|
@ -135,7 +135,7 @@ const Blog: React.FC = () => {
|
||||||
<input
|
<input
|
||||||
type="search"
|
type="search"
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
placeholder={translateLabel(translate, 'Public.blog.SearchPlaceholder')}
|
placeholder={translateLabel(translate, 'App.PublicBlog.SearchPlaceholder')}
|
||||||
aria-label={translate('::App.Platform.Search3')}
|
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"
|
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)}
|
onChange={(event) => setSearchQuery(event.target.value)}
|
||||||
|
|
@ -162,7 +162,7 @@ const Blog: React.FC = () => {
|
||||||
{posts.length === 0 ? (
|
{posts.length === 0 ? (
|
||||||
<div className="py-12 text-center">
|
<div className="py-12 text-center">
|
||||||
<p className="text-lg text-gray-600 dark:text-gray-300">
|
<p className="text-lg text-gray-600 dark:text-gray-300">
|
||||||
{translateLabel(translate, 'Public.blog.empty')}
|
{translateLabel(translate, 'App.PublicBlog.Empty')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue