Custom Component json dosyasının oluşturulması
This commit is contained in:
parent
7870ebb7bc
commit
3152cd681b
19 changed files with 823 additions and 77 deletions
|
|
@ -265,7 +265,8 @@ Aktif veritabanı sağlayıcısına göre `sql/` (SQL Server) ya da `postgres/`
|
|||
| `{sql\|postgres}/object` | 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ı → `host/`, tenant seçiliyken → `tenants/{tenantId}/`). |
|
||||
| `{sql\|postgres}/execute` | 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). |
|
||||
| `wizard` | ListForm Wizard'ın ürettiği `.json` konfigürasyonları; `WizardDataSeeder` kendi kapsamındaki dosyaları okur. |
|
||||
| `crud` / `custom` | CrudEndpoint ve CustomEndpoint konfigürasyonları için ayrılmıştır. |
|
||||
| `crud` | CrudEndpoint konfigürasyonları; `CrudDataSeeder` kendi kapsamındaki dosyaları okur. |
|
||||
| `custom` | Custom Component konfigürasyonları (`{Name}.json`); Component Manager kaydetme/silme sırasında dosyayı senkronlar, `CustomComponentDataSeeder` kendi kapsamındaki dosyaları okuyup veritabanına uygular. |
|
||||
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -13,6 +13,12 @@ public class CustomComponentDto : FullAuditedEntityDto<Guid>
|
|||
public string? Description { get; set; }
|
||||
public bool IsActive { get; set; } = true;
|
||||
public string? Dependencies { get; set; } // JSON string of component names
|
||||
|
||||
/// <summary>
|
||||
/// Data sekmesinde kullanilan API'ler; JSON dizisi. Props icindeki designer dokumanindan
|
||||
/// kaydetme sirasinda sunucu tarafinda uretilir, istemciden gonderilmez.
|
||||
/// </summary>
|
||||
public string? DataSources { get; set; }
|
||||
}
|
||||
|
||||
public class CreateUpdateCustomComponentDto
|
||||
|
|
|
|||
|
|
@ -1,7 +1,14 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.IO;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Sozsoft.Platform.Data.Seeds;
|
||||
using Sozsoft.Platform.DeveloperKit;
|
||||
using Sozsoft.Platform.Entities;
|
||||
using Volo.Abp.Application.Dtos;
|
||||
|
|
@ -18,21 +25,72 @@ public class CustomComponentAppService : CrudAppService<
|
|||
PagedAndSortedResultRequestDto,
|
||||
CreateUpdateCustomComponentDto>, ICustomComponentAppService
|
||||
{
|
||||
public CustomComponentAppService(IRepository<CustomComponent, Guid> repository) : base(repository)
|
||||
private static readonly JsonSerializerOptions SeedJsonWriteOptions = new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
// Component kodu HTML/JSX karakterleri icerir; kacislar dosyayi okunamaz hale getirir.
|
||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
||||
};
|
||||
|
||||
private static readonly JsonSerializerOptions JsonReadOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly IRepository<CrudEndpoint, Guid> _crudEndpointRepository;
|
||||
|
||||
public CustomComponentAppService(
|
||||
IRepository<CustomComponent, Guid> repository,
|
||||
IRepository<CrudEndpoint, Guid> crudEndpointRepository,
|
||||
IConfiguration configuration) : base(repository)
|
||||
{
|
||||
_crudEndpointRepository = crudEndpointRepository;
|
||||
_configuration = configuration;
|
||||
}
|
||||
|
||||
public override async Task<CustomComponentDto> CreateAsync(CreateUpdateCustomComponentDto input)
|
||||
{
|
||||
var result = await base.CreateAsync(input);
|
||||
await SyncDataSourcesAsync(result.Id);
|
||||
await SyncSeedFileAsync(result.Id);
|
||||
return result;
|
||||
}
|
||||
|
||||
public override async Task<CustomComponentDto> UpdateAsync(
|
||||
Guid id,
|
||||
CreateUpdateCustomComponentDto input)
|
||||
{
|
||||
var existingComponent = await Repository.GetAsync(id);
|
||||
var previousName = existingComponent.Name;
|
||||
|
||||
if (input.Props is null)
|
||||
{
|
||||
var existingComponent = await Repository.GetAsync(id);
|
||||
input.Props = existingComponent.Props;
|
||||
}
|
||||
|
||||
return await base.UpdateAsync(id, input);
|
||||
var result = await base.UpdateAsync(id, input);
|
||||
|
||||
// Ad degistiyse eski dosya artik hicbir component'i temsil etmez.
|
||||
if (!string.Equals(previousName, result.Name, StringComparison.Ordinal))
|
||||
{
|
||||
DeleteSeedFile(previousName);
|
||||
}
|
||||
|
||||
await SyncDataSourcesAsync(id);
|
||||
await SyncSeedFileAsync(id);
|
||||
return result;
|
||||
}
|
||||
|
||||
public override async Task DeleteAsync(Guid id)
|
||||
{
|
||||
var component = await Repository.FindAsync(id);
|
||||
await base.DeleteAsync(id);
|
||||
|
||||
if (component != null)
|
||||
{
|
||||
DeleteSeedFile(component.Name);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<CustomComponentDto>> GetActiveComponentsAsync()
|
||||
|
|
@ -40,5 +98,203 @@ public class CustomComponentAppService : CrudAppService<
|
|||
var components = await Repository.GetListAsync(x => x.IsActive);
|
||||
return await MapToGetListOutputDtosAsync(components);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Seed dosyasini yazar; dosya islemi hatasi CRUD islemini engellemez, yalnizca loglanir.
|
||||
/// </summary>
|
||||
private async Task SyncSeedFileAsync(Guid id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var component = await Repository.FindAsync(id);
|
||||
if (component != null)
|
||||
{
|
||||
await WriteSeedFileAsync(component);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Custom component seed file could not be synchronized for {Id}: {Message}", id, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<string> WriteSeedFileAsync(CustomComponent component)
|
||||
{
|
||||
var safeFileName = GetSafeFileName(component.Name)
|
||||
?? throw new InvalidOperationException($"Invalid component name for a seed file: '{component.Name}'.");
|
||||
|
||||
var outputPath = ResolveCustomSeedOutputPath();
|
||||
Directory.CreateDirectory(outputPath);
|
||||
|
||||
var filePath = Path.Combine(outputPath, $"{safeFileName}.json");
|
||||
|
||||
var seedFile = new CustomComponentSeedFileDto
|
||||
{
|
||||
GeneratedAt = Clock.Now.ToUniversalTime(),
|
||||
CustomComponents =
|
||||
[
|
||||
new CustomComponentSeedItemDto
|
||||
{
|
||||
Name = component.Name,
|
||||
RoutePath = component.RoutePath,
|
||||
Code = component.Code,
|
||||
Props = component.Props,
|
||||
Description = component.Description,
|
||||
IsActive = component.IsActive,
|
||||
Dependencies = ParseDependencies(component.Dependencies),
|
||||
DataSources = ParseDataSources(component.DataSources)
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
await File.WriteAllTextAsync(filePath, JsonSerializer.Serialize(seedFile, SeedJsonWriteOptions));
|
||||
Logger.LogInformation("Custom component seed file saved: {FilePath}", filePath);
|
||||
|
||||
return filePath;
|
||||
}
|
||||
|
||||
private void DeleteSeedFile(string componentName)
|
||||
{
|
||||
try
|
||||
{
|
||||
var safeFileName = GetSafeFileName(componentName);
|
||||
if (safeFileName == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var filePath = Path.Combine(ResolveCustomSeedOutputPath(), $"{safeFileName}.json");
|
||||
if (File.Exists(filePath))
|
||||
{
|
||||
File.Delete(filePath);
|
||||
Logger.LogInformation("Custom component seed file deleted: {FilePath}", filePath);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Custom component seed file could not be deleted for {Name}: {Message}", componentName, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Designer dokumanindaki (Props) Data sekmesi endpoint'lerini cozer, her birini CRUD
|
||||
/// endpoint kataloguyla eslestirir ve sonucu entity'nin <c>DataSources</c> sutununa yazar.
|
||||
/// Boylece component'in hangi <c>crud/{EntityName}.json</c> seed dosyalarina bagli oldugu
|
||||
/// dokuman ayristirmadan sorgulanabilir. Hata CRUD islemini engellemez, yalnizca loglanir.
|
||||
/// </summary>
|
||||
private async Task SyncDataSourcesAsync(Guid id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var component = await Repository.FindAsync(id);
|
||||
if (component == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var sources = await ResolveDataSourcesAsync(component.Props);
|
||||
var serialized = sources.Count == 0 ? null : JsonSerializer.Serialize(sources, SeedJsonWriteOptions);
|
||||
|
||||
if (string.Equals(component.DataSources, serialized, StringComparison.Ordinal))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
component.DataSources = serialized;
|
||||
await Repository.UpdateAsync(component, autoSave: true);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError(ex, "Custom component data sources could not be resolved for {Id}: {Message}", id, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Designer dokumanindaki endpoint'leri veritabanindaki CRUD endpoint kataloguyla eslestirir.
|
||||
/// </summary>
|
||||
private async Task<List<CustomComponentSeedDataSourceDto>> ResolveDataSourcesAsync(string? props)
|
||||
{
|
||||
var endpoints = await _crudEndpointRepository.GetListAsync();
|
||||
var references = endpoints
|
||||
.Select(x => new CustomComponentDataSourceResolver.CrudEndpointReference
|
||||
{
|
||||
EntityName = x.EntityName,
|
||||
Method = x.Method,
|
||||
Path = x.Path,
|
||||
OperationType = x.OperationType
|
||||
})
|
||||
.ToList();
|
||||
|
||||
return CustomComponentDataSourceResolver.Resolve(props, references);
|
||||
}
|
||||
|
||||
/// <summary>Entity'de JSON string olarak tutulan endpoint listesini cozer.</summary>
|
||||
private static List<CustomComponentSeedDataSourceDto> ParseDataSources(string? dataSources)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(dataSources))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return JsonSerializer.Deserialize<List<CustomComponentSeedDataSourceDto>>(dataSources, JsonReadOptions) ?? [];
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Designer dokumanindaki tek bir data source kaydi.</summary>
|
||||
private sealed class DesignerDataSourceDto
|
||||
{
|
||||
public string? Name { get; set; }
|
||||
public string? Method { get; set; }
|
||||
public string? Url { get; set; }
|
||||
public string? ResponsePath { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>Entity'de JSON string olarak tutulan bagimlilik listesini cozer.</summary>
|
||||
private static List<string> ParseDependencies(string? dependencies)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(dependencies))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return JsonSerializer.Deserialize<List<string>>(dependencies, JsonReadOptions) ?? [];
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Custom component seed dosyalarinin yazilacagi klasor; kapsam aktif tenant'i izler
|
||||
/// (host baglaminda <c>host/custom</c>, tenant baglaminda <c>tenants/{tenantId}/custom</c>).
|
||||
/// </summary>
|
||||
private string ResolveCustomSeedOutputPath()
|
||||
{
|
||||
return SeedPathResolver.GetScopePath(_configuration, CurrentTenant.Id, SeedPathResolver.CustomFolder);
|
||||
}
|
||||
|
||||
/// <summary>Component adindan guvenli bir dosya adi uretir; path traversal denemelerini reddeder.</summary>
|
||||
private static string? GetSafeFileName(string componentName)
|
||||
{
|
||||
var trimmed = componentName?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(trimmed) ||
|
||||
trimmed.Contains('/') ||
|
||||
trimmed.Contains('\\') ||
|
||||
trimmed.Contains(".."))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var safeFileName = string.Concat(trimmed.Split(Path.GetInvalidFileNameChars()));
|
||||
return string.IsNullOrWhiteSpace(safeFileName) ? null : safeFileName;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,206 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Sozsoft.Platform.Entities;
|
||||
using Volo.Abp.Data;
|
||||
using Volo.Abp.DependencyInjection;
|
||||
using Volo.Abp.Domain.Repositories;
|
||||
|
||||
namespace Sozsoft.Platform.Data.Seeds;
|
||||
|
||||
/// <summary>
|
||||
/// Component Manager ile olusturulan custom component'leri
|
||||
/// <c>{host|tenants/{tenantId}}/custom/*.json</c> dosyalarindan okuyarak veritabanina uygular
|
||||
/// (kapsam, seed edilen veritabanini izler).
|
||||
/// <para>
|
||||
/// Dosyalar component kaydedildikce/silindikce otomatik yazildigi icin kaynak kabul edilir:
|
||||
/// ayni adli component varsa dosyadaki icerikle guncellenir, yoksa olusturulur.
|
||||
/// Dosyada olmayan component'ler silinmez.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public class CustomComponentDataSeeder : IDataSeedContributor, ITransientDependency
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonReadOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
|
||||
private static readonly JsonSerializerOptions JsonWriteOptions = new()
|
||||
{
|
||||
WriteIndented = true
|
||||
};
|
||||
|
||||
private readonly IRepository<CustomComponent, Guid> _repository;
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly ILogger<CustomComponentDataSeeder> _logger;
|
||||
|
||||
public CustomComponentDataSeeder(
|
||||
IRepository<CustomComponent, Guid> repository,
|
||||
IConfiguration configuration,
|
||||
ILogger<CustomComponentDataSeeder> logger)
|
||||
{
|
||||
_repository = repository;
|
||||
_configuration = configuration;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task SeedAsync(DataSeedContext context)
|
||||
{
|
||||
var scopeFolderName = SeedPathResolver.GetScopeFolderName(context.TenantId);
|
||||
var customDataPath = SeedPathResolver.GetScopePath(_configuration, context.TenantId, SeedPathResolver.CustomFolder);
|
||||
|
||||
if (!Directory.Exists(customDataPath))
|
||||
{
|
||||
_logger.LogInformation("Seeds/{Scope}/custom directory not found, skipping CustomComponentDataSeeder.", scopeFolderName);
|
||||
return;
|
||||
}
|
||||
|
||||
var jsonFiles = Directory.GetFiles(customDataPath, "*.json").OrderBy(Path.GetFileName).ToArray();
|
||||
if (jsonFiles.Length == 0)
|
||||
{
|
||||
_logger.LogInformation("No JSON files found in Seeds/{Scope}/custom directory, skipping CustomComponentDataSeeder.", scopeFolderName);
|
||||
return;
|
||||
}
|
||||
|
||||
// Eski seed dosyalarinda DataSources blogu yoktur; katalog, bloksuz dosyalarda
|
||||
// endpoint listesinin Props'tan turetilebilmesi icin bastan okunur.
|
||||
var crudEndpoints = ReadCrudEndpointCatalog(context.TenantId, scopeFolderName);
|
||||
|
||||
_logger.LogInformation("CustomComponentDataSeeder started. {Count} file(s) to be processed.", jsonFiles.Length);
|
||||
|
||||
foreach (var filePath in jsonFiles)
|
||||
{
|
||||
var fileName = Path.GetFileName(filePath);
|
||||
try
|
||||
{
|
||||
var json = await File.ReadAllTextAsync(filePath);
|
||||
var seedFile = JsonSerializer.Deserialize<CustomComponentSeedFileDto>(json, JsonReadOptions);
|
||||
|
||||
if (seedFile?.CustomComponents == null || seedFile.CustomComponents.Count == 0)
|
||||
{
|
||||
_logger.LogWarning("[{File}] No custom component found, skipped.", fileName);
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var item in seedFile.CustomComponents)
|
||||
{
|
||||
await ApplyAsync(fileName, item, crudEndpoints);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Tek bir dosyanin hatasi diger component'lerin seed edilmesini engellemez.
|
||||
_logger.LogError(ex, "Failed to process custom component seed file: {FileName}", fileName);
|
||||
}
|
||||
}
|
||||
|
||||
_logger.LogInformation("CustomComponentDataSeeder completed. {Count} file(s) processed.", jsonFiles.Length);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Kapsamdaki <c>crud/*.json</c> dosyalarini okuyarak endpoint katalogunu olusturur.
|
||||
/// Veritabani yerine dosyalardan okunur; boylece seed sirasi CrudEndpointDataSeeder'a bagli kalmaz.
|
||||
/// </summary>
|
||||
private List<CustomComponentDataSourceResolver.CrudEndpointReference> ReadCrudEndpointCatalog(
|
||||
Guid? tenantId,
|
||||
string scopeFolderName)
|
||||
{
|
||||
var references = new List<CustomComponentDataSourceResolver.CrudEndpointReference>();
|
||||
var crudDataPath = SeedPathResolver.GetScopePath(_configuration, tenantId, SeedPathResolver.CrudFolder);
|
||||
|
||||
if (!Directory.Exists(crudDataPath))
|
||||
{
|
||||
return references;
|
||||
}
|
||||
|
||||
foreach (var filePath in Directory.GetFiles(crudDataPath, "*.json"))
|
||||
{
|
||||
try
|
||||
{
|
||||
var seedFile = JsonSerializer.Deserialize<CrudEndpointSeedFileDto>(
|
||||
File.ReadAllText(filePath),
|
||||
JsonReadOptions);
|
||||
|
||||
if (seedFile?.Endpoints == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
references.AddRange(seedFile.Endpoints.Select(endpoint =>
|
||||
new CustomComponentDataSourceResolver.CrudEndpointReference
|
||||
{
|
||||
EntityName = seedFile.EntityName,
|
||||
Method = endpoint.Method,
|
||||
Path = endpoint.Path,
|
||||
OperationType = endpoint.OperationType
|
||||
}));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Katalog yardimci bilgidir; okunamayan bir dosya component seed'ini durdurmaz.
|
||||
_logger.LogWarning(ex, "[{Scope}] CRUD seed file could not be read for the data source catalog: {File}",
|
||||
scopeFolderName, Path.GetFileName(filePath));
|
||||
}
|
||||
}
|
||||
|
||||
return references;
|
||||
}
|
||||
|
||||
private async Task ApplyAsync(
|
||||
string fileName,
|
||||
CustomComponentSeedItemDto item,
|
||||
List<CustomComponentDataSourceResolver.CrudEndpointReference> crudEndpoints)
|
||||
{
|
||||
var name = item.Name?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
_logger.LogWarning("[{File}] Component name is empty, skipped.", fileName);
|
||||
return;
|
||||
}
|
||||
|
||||
var dependencies = JsonSerializer.Serialize(item.Dependencies ?? new List<string>());
|
||||
// DataSources blogu olmayan (bu sutundan once yazilmis) dosyalarda liste Props'taki
|
||||
// designer dokumanindan turetilir; boylece sutun her durumda dolar.
|
||||
var dataSourceItems = item.DataSources is { Count: > 0 }
|
||||
? item.DataSources
|
||||
: CustomComponentDataSourceResolver.Resolve(item.Props, crudEndpoints);
|
||||
|
||||
// Bos liste sutunu doldurmaya degmez; component'in hic endpoint'i yoksa null kalir.
|
||||
var dataSources = dataSourceItems.Count > 0
|
||||
? JsonSerializer.Serialize(dataSourceItems, JsonWriteOptions)
|
||||
: null;
|
||||
var current = await _repository.FirstOrDefaultAsync(x => x.Name == name);
|
||||
|
||||
if (current == null)
|
||||
{
|
||||
await _repository.InsertAsync(new CustomComponent(
|
||||
name,
|
||||
item.RoutePath,
|
||||
item.Code,
|
||||
item.Props,
|
||||
item.Description,
|
||||
item.IsActive,
|
||||
dependencies,
|
||||
dataSources), autoSave: true);
|
||||
|
||||
_logger.LogInformation("[{File}] '{Name}' created.", fileName, name);
|
||||
return;
|
||||
}
|
||||
|
||||
current.RoutePath = item.RoutePath;
|
||||
current.Code = item.Code;
|
||||
current.Props = item.Props;
|
||||
current.Description = item.Description;
|
||||
current.IsActive = item.IsActive;
|
||||
current.Dependencies = dependencies;
|
||||
current.DataSources = dataSources;
|
||||
|
||||
await _repository.UpdateAsync(current, autoSave: true);
|
||||
_logger.LogInformation("[{File}] '{Name}' updated.", fileName, name);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
{
|
||||
{
|
||||
"Languages": [
|
||||
{
|
||||
"cultureName": "ar",
|
||||
|
|
@ -8802,6 +8802,30 @@
|
|||
"en": "No dependencies",
|
||||
"tr": "Bağımlılık yok"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.DeveloperKitComponent.SaveToSeed",
|
||||
"en": "Save to Seed",
|
||||
"tr": "Seed'e Kaydet"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.DeveloperKitComponent.SaveToSeedDescription",
|
||||
"en": "Writes all component information as JSON under seeds/{scope}/custom.",
|
||||
"tr": "Bileşenin tüm bilgilerini seeds/{kapsam}/custom altına JSON olarak yazar."
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.DeveloperKitComponent.SeedSaved",
|
||||
"en": "Seed file saved:",
|
||||
"tr": "Seed dosyası kaydedildi:"
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.DeveloperKitComponent.SeedSaveFailed",
|
||||
"en": "Seed file could not be saved.",
|
||||
"tr": "Seed dosyası kaydedilemedi."
|
||||
},
|
||||
{
|
||||
"resourceName": "Platform",
|
||||
"key": "App.ComponentAction.Edit",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,121 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Sozsoft.Platform.Data.Seeds;
|
||||
|
||||
/// <summary>
|
||||
/// Component'in designer dokumanindaki (Props) Data sekmesi endpoint'lerini cozup
|
||||
/// CRUD endpoint katalogu ile eslestiren ortak mantik.
|
||||
/// <para>
|
||||
/// Ayni cozum hem kaydetme sirasinda (CustomComponentAppService, katalog veritabanindan gelir)
|
||||
/// hem de seed sirasinda (CustomComponentDataSeeder, katalog <c>crud/*.json</c> dosyalarindan gelir)
|
||||
/// kullanilir; boylece <c>DataSources</c> sutunu iki yolda da ayni sekilde uretilir.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static class CustomComponentDataSourceResolver
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonReadOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
|
||||
/// <summary>Eslestirmede kullanilan CRUD endpoint tanimi.</summary>
|
||||
public sealed class CrudEndpointReference
|
||||
{
|
||||
public string EntityName { get; set; } = string.Empty;
|
||||
public string Method { get; set; } = string.Empty;
|
||||
public string Path { get; set; } = string.Empty;
|
||||
public string OperationType { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Props icindeki <c>visualDesigner.dataSources</c> listesini okur ve her kaydi method + path
|
||||
/// ile <paramref name="endpoints"/> icinde arar. Eslesmeyen (elle yazilmis ya da custom)
|
||||
/// endpoint'ler de listede kalir; yalnizca entity/seed dosyasi alanlari bos gelir.
|
||||
/// </summary>
|
||||
public static List<CustomComponentSeedDataSourceDto> Resolve(
|
||||
string? props,
|
||||
IReadOnlyCollection<CrudEndpointReference> endpoints)
|
||||
{
|
||||
var documentSources = ParseDocumentDataSources(props);
|
||||
if (documentSources.Count == 0)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var resolved = new List<CustomComponentSeedDataSourceDto>();
|
||||
|
||||
foreach (var source in documentSources)
|
||||
{
|
||||
var method = (source.Method ?? string.Empty).Trim().ToUpperInvariant();
|
||||
var url = (source.Url ?? string.Empty).Trim();
|
||||
if (url.Length == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ayni method + path birden fazla bilesene baglanmis olabilir; tekrarlari eleriz.
|
||||
if (resolved.Exists(x => x.Method == method && string.Equals(x.Url, url, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var endpoint = endpoints.FirstOrDefault(x =>
|
||||
string.Equals(x.Method?.Trim(), method, StringComparison.OrdinalIgnoreCase) &&
|
||||
string.Equals(x.Path?.Trim(), url, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
resolved.Add(new CustomComponentSeedDataSourceDto
|
||||
{
|
||||
Name = (source.Name ?? string.Empty).Trim(),
|
||||
Method = method,
|
||||
Url = url,
|
||||
ResponsePath = string.IsNullOrWhiteSpace(source.ResponsePath) ? null : source.ResponsePath.Trim(),
|
||||
EntityName = endpoint?.EntityName,
|
||||
OperationType = endpoint?.OperationType,
|
||||
SeedFile = endpoint == null
|
||||
? null
|
||||
: $"{SeedPathResolver.CrudFolder}/{endpoint.EntityName}.json"
|
||||
});
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
/// <summary>Props JSON'undaki designer dokumanindan yalnizca dataSources dizisini ayiklar.</summary>
|
||||
private static List<DesignerDataSourceDto> ParseDocumentDataSources(string? props)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(props))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var document = JsonDocument.Parse(props);
|
||||
if (!document.RootElement.TryGetProperty("visualDesigner", out var designer) ||
|
||||
designer.ValueKind != JsonValueKind.Object ||
|
||||
!designer.TryGetProperty("dataSources", out var dataSources) ||
|
||||
dataSources.ValueKind != JsonValueKind.Array)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
return JsonSerializer.Deserialize<List<DesignerDataSourceDto>>(dataSources.GetRawText(), JsonReadOptions) ?? [];
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Designer dokumanindaki tek bir data source kaydi.</summary>
|
||||
private sealed class DesignerDataSourceDto
|
||||
{
|
||||
public string? Name { get; set; }
|
||||
public string? Method { get; set; }
|
||||
public string? Url { get; set; }
|
||||
public string? ResponsePath { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Sozsoft.Platform.Data.Seeds;
|
||||
|
||||
/// <summary>
|
||||
/// Bir custom component'in seed dosyasi.
|
||||
/// Component Manager uzerinden kaydedildiginde/silindiginde
|
||||
/// <c>{host|tenants/{tenantId}}/custom/{Name}.json</c> olarak yazilir; veritabani silinip
|
||||
/// yeniden olusturuldugunda <c>CustomComponentDataSeeder</c> ayni dosyalari okuyarak
|
||||
/// component'leri geri yukler.
|
||||
/// <para>
|
||||
/// Dosya duzeni bilincli olarak TenantData.json'daki <c>CustomComponents</c> bloguyla aynidir;
|
||||
/// boylece eski seed icerigi dogrudan tasinabilir. Liste birden fazla component tasiyabilse de
|
||||
/// senkronizasyon her component icin tek dosya yazar.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public class CustomComponentSeedFileDto
|
||||
{
|
||||
/// <summary>Dosyanin en son yazildigi an (UTC) — yalnizca bilgi amaclidir.</summary>
|
||||
public DateTime GeneratedAt { get; set; }
|
||||
|
||||
public List<CustomComponentSeedItemDto> CustomComponents { get; set; } = [];
|
||||
}
|
||||
|
||||
/// <summary>Component'in seed edilebilir tam tanimi; dosya adi <see cref="Name"/> ile ayni olmasi beklenir.</summary>
|
||||
public class CustomComponentSeedItemDto
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string RoutePath { get; set; } = string.Empty;
|
||||
public string Code { get; set; } = string.Empty;
|
||||
public string? Props { get; set; }
|
||||
public string? Description { get; set; }
|
||||
public bool IsActive { get; set; } = true;
|
||||
|
||||
/// <summary>Bagimli olunan component adlari; veritabaninda JSON string olarak tutulur.</summary>
|
||||
public List<string> Dependencies { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Component'in Data sekmesinde kullandigi API'ler; veritabaninda JSON string olarak tutulur.
|
||||
/// Kaydetme sirasinda designer dokumanindan otomatik uretilir.
|
||||
/// </summary>
|
||||
public List<CustomComponentSeedDataSourceDto> DataSources { get; set; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Component'in kullandigi tek bir API. <see cref="EntityName"/> dolu ise endpoint
|
||||
/// CRUD Endpoint Manager tarafindan uretilmistir ve tanimi <see cref="SeedFile"/>
|
||||
/// (kapsam klasorune gore <c>crud/{EntityName}.json</c>) dosyasindadir.
|
||||
/// </summary>
|
||||
public class CustomComponentSeedDataSourceDto
|
||||
{
|
||||
/// <summary>Designer'daki gorunur ad.</summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
public string Method { get; set; } = string.Empty;
|
||||
|
||||
public string Url { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>Yanit icindeki liste yolu; bos ise yanitin kendisi kullanilir.</summary>
|
||||
public string? ResponsePath { get; set; }
|
||||
|
||||
/// <summary>Endpoint bir CRUD endpoint'ine denk geliyorsa entity adi, aksi halde null.</summary>
|
||||
public string? EntityName { get; set; }
|
||||
|
||||
/// <summary>CRUD endpoint'inin islem turu (GetAll, GetById, Create, Update, Delete).</summary>
|
||||
public string? OperationType { get; set; }
|
||||
|
||||
/// <summary>Endpoint tanimini tasiyan seed dosyasinin kapsam klasorune gore yolu.</summary>
|
||||
public string? SeedFile { get; set; }
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
|
@ -72,7 +72,7 @@ public static class SeedPathResolver
|
|||
/// <summary>Crud endpoint konfigurasyon dosyalari.</summary>
|
||||
public const string CrudFolder = "crud";
|
||||
|
||||
/// <summary>Custom endpoint konfigurasyon dosyalari.</summary>
|
||||
/// <summary>Custom component konfigurasyon dosyalari.</summary>
|
||||
public const string CustomFolder = "custom";
|
||||
|
||||
private const string RepositorySeedsFolder = "seeds";
|
||||
|
|
|
|||
|
|
@ -16,7 +16,15 @@ public class CustomComponent : FullAuditedEntity<Guid>, IMultiTenant
|
|||
public bool IsActive { get; set; } = true;
|
||||
public string? Dependencies { get; set; } // JSON string of component names
|
||||
|
||||
public CustomComponent(string name, string routePath, string code, string? props, string? description, bool isActive, string? dependencies)
|
||||
/// <summary>
|
||||
/// Component'in Data sekmesinde kullandigi API'ler; JSON dizisi olarak tutulur.
|
||||
/// Her kayit endpoint'in method/url bilgisinin yani sira cozumlenebiliyorsa
|
||||
/// kaynak CRUD endpoint'ini (EntityName ve <c>crud/{EntityName}.json</c> seed dosyasi) tasir.
|
||||
/// Props icindeki designer dokumanindan turetilir; kaydetme sirasinda otomatik yazilir.
|
||||
/// </summary>
|
||||
public string? DataSources { get; set; }
|
||||
|
||||
public CustomComponent(string name, string routePath, string code, string? props, string? description, bool isActive, string? dependencies, string? dataSources = null)
|
||||
{
|
||||
Name = name;
|
||||
RoutePath = routePath;
|
||||
|
|
@ -25,6 +33,7 @@ public class CustomComponent : FullAuditedEntity<Guid>, IMultiTenant
|
|||
Description = description;
|
||||
IsActive = isActive;
|
||||
Dependencies = dependencies;
|
||||
DataSources = dataSources;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -670,6 +670,8 @@ public class PlatformDbContext :
|
|||
b.Property(x => x.Props).HasColumnType("nvarchar(max)");
|
||||
b.Property(x => x.Description).HasMaxLength(512);
|
||||
b.Property(x => x.Dependencies).HasMaxLength(2048);
|
||||
// Data sekmesindeki endpoint listesi; adet ve url uzunlugu onceden bilinemez.
|
||||
b.Property(x => x.DataSources).HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasIndex(x => new { x.TenantId, x.Name }).IsUnique().HasFilter("[IsDeleted] = 0");
|
||||
b.HasIndex(x => new { x.TenantId, x.RoutePath }).IsUnique().HasFilter("[IsDeleted] = 0");
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ using Volo.Abp.EntityFrameworkCore;
|
|||
namespace Sozsoft.Platform.Migrations
|
||||
{
|
||||
[DbContext(typeof(PlatformDbContext))]
|
||||
[Migration("20260817083025_Initial")]
|
||||
[Migration("20260818091734_Initial")]
|
||||
partial class Initial
|
||||
{
|
||||
/// <inheritdoc />
|
||||
|
|
@ -1921,6 +1921,9 @@ namespace Sozsoft.Platform.Migrations
|
|||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("CreatorId");
|
||||
|
||||
b.Property<string>("DataSources")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<Guid?>("DeleterId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("DeleterId");
|
||||
|
|
@ -1185,6 +1185,7 @@ namespace Sozsoft.Platform.Migrations
|
|||
Description = table.Column<string>(type: "nvarchar(512)", maxLength: 512, nullable: true),
|
||||
IsActive = table.Column<bool>(type: "bit", nullable: false),
|
||||
Dependencies = table.Column<string>(type: "nvarchar(2048)", maxLength: 2048, nullable: true),
|
||||
DataSources = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
CreationTime = table.Column<DateTime>(type: "datetime2", nullable: false),
|
||||
CreatorId = table.Column<Guid>(type: "uniqueidentifier", nullable: true),
|
||||
LastModificationTime = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
|
|
@ -1918,6 +1918,9 @@ namespace Sozsoft.Platform.Migrations
|
|||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("CreatorId");
|
||||
|
||||
b.Property<string>("DataSources")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<Guid?>("DeleterId")
|
||||
.HasColumnType("uniqueidentifier")
|
||||
.HasColumnName("DeleterId");
|
||||
|
|
|
|||
|
|
@ -346,26 +346,6 @@
|
|||
]
|
||||
}
|
||||
],
|
||||
"CustomComponents": [
|
||||
{
|
||||
"name": "DynamicEntityComponent",
|
||||
"routePath": "/admin/dynamic-entity",
|
||||
"code": "import React, { useEffect, useState } from \"react\";\nimport axios from \"axios\";\n\ninterface DynamicEntityComponentProps {\n title: string;\n}\n\nconst api = axios.create({\n baseURL: \"https://localhost:44344\",\n});\n\nconst DynamicEntityComponent: React.FC<DynamicEntityComponentProps> = ({ title }) => {\n const [data, setData] = useState<Array<{ id: string; name: string }>>([]);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n useEffect(() => {\n const fetchData = async () => {\n setLoading(true);\n setError(null);\n\n try {\n const res = await api.get(`/api/app/crudendpoint/${title}`);\n const raw = Array.isArray(res.data) ? res.data : res.data?.items ?? [];\n\n const filtered = raw.map((item: any) => ({\n id: item.Id ?? item.id,\n name: item.Name ?? item.name,\n }));\n\n setData(filtered);\n } catch (err: any) {\n setError(err.message || \"Failed to fetch data\");\n } finally {\n setLoading(false);\n }\n };\n\n if (title) fetchData();\n }, [title]);\n\n if (loading) return <div>Loading...</div>;\n if (error) return <div className=\"text-red-600 dark:text-red-400\">Error: {error}</div>;\n if (!data.length) return <div>No records found</div>;\n\n const headers = [\"id\", \"name\", \"actions\"];\n\n return (\n <div className=\"overflow-auto\">\n <table className=\"min-w-full bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-700 shadow-sm rounded-lg\">\n <thead className=\"bg-slate-100 dark:bg-slate-800\">\n <tr>\n {headers.map((key) => (\n <th\n key={key}\n className=\"text-left px-4 py-2 border-b border-slate-200 dark:border-slate-700 text-sm font-medium text-slate-700 dark:text-slate-200\"\n >\n {key === \"actions\" ? \"Actions\" : key}\n </th>\n ))}\n </tr>\n </thead>\n <tbody>\n {data.map((item, rowIndex) => (\n <tr key={item.id ?? rowIndex} className=\"hover:bg-slate-50 dark:hover:bg-slate-800\">\n <td className=\"px-4 py-2 border-b border-slate-100 dark:border-slate-800 text-sm text-slate-800 dark:text-slate-100\">\n {item.id}\n </td>\n <td className=\"px-4 py-2 border-b border-slate-100 dark:border-slate-800 text-sm text-slate-800 dark:text-slate-100\">\n {item.name}\n </td>\n <td className=\"px-4 py-2 border-b border-slate-100 dark:border-slate-800\">\n <Button\n type=\"button\"\n color=\"blue-500\"\n onClick={() => alert(item.name)}\n shape=\"round\"\n size=\"xs\"\n variant=\"solid\"\n id=\"c_mrix4c1a_qrybhk\"\n >\n Show Name\n </Button>\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n );\n};\n\nexport default DynamicEntityComponent;",
|
||||
"props": null,
|
||||
"description": null,
|
||||
"isActive": true,
|
||||
"dependencies": []
|
||||
},
|
||||
{
|
||||
"name": "RoleListComponent",
|
||||
"routePath": "/admin/roles-list",
|
||||
"code": "const RoleListComponent = ({\n title = \"AbpRoles\"\n}) => {\n return (\n <DynamicEntityComponent id=\"c_mdljvvmq_fno52v\" title={title} />\n );\n};\n\nexport default RoleListComponent;",
|
||||
"props": null,
|
||||
"description": null,
|
||||
"isActive": true,
|
||||
"dependencies": ["DynamicEntityComponent"]
|
||||
}
|
||||
],
|
||||
"Abouts": [
|
||||
{
|
||||
"stats": [
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ public class TenantSeederDto
|
|||
//Saas
|
||||
public List<GlobalSearchSeedDto> GlobalSearch { get; set; }
|
||||
public List<CustomEndpointSeedDto> CustomEndpoints { get; set; }
|
||||
public List<CustomComponentSeedDto> CustomComponents { get; set; }
|
||||
public List<SequenceSeedDto> Sequences { get; set; }
|
||||
|
||||
//Definitions
|
||||
|
|
@ -340,17 +339,6 @@ public class InstallmentOptionSeedDto
|
|||
public decimal Commission { get; set; }
|
||||
}
|
||||
|
||||
public class CustomComponentSeedDto
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string RoutePath { get; set; }
|
||||
public string Code { get; set; }
|
||||
public string Props { get; set; }
|
||||
public string Description { get; set; }
|
||||
public bool IsActive { get; set; }
|
||||
public List<string> Dependencies { get; set; } = new();
|
||||
}
|
||||
|
||||
public class ReportTemplateSeedDto
|
||||
{
|
||||
public string Name { get; set; }
|
||||
|
|
@ -479,7 +467,6 @@ public class TenantDataSeeder : IDataSeedContributor, ITransientDependency
|
|||
private readonly IRepository<Branch, Guid> _branchRepository;
|
||||
private readonly IRepository<GlobalSearch, int> _globalSearch;
|
||||
private readonly IRepository<CustomEndpoint, Guid> _customEndpointRepository;
|
||||
private readonly IRepository<CustomComponent, Guid> _customComponentRepository;
|
||||
private readonly IRepository<ReportCategory, Guid> _reportCategoriesRepository;
|
||||
private readonly IRepository<ReportTemplate, Guid> _reportTemplatesRepository;
|
||||
private readonly IRepository<Home, Guid> _homeRepository;
|
||||
|
|
@ -535,7 +522,6 @@ public class TenantDataSeeder : IDataSeedContributor, ITransientDependency
|
|||
IRepository<Skill, string> skillRepository,
|
||||
IRepository<SkillLevel, string> skillLevelRepository,
|
||||
IRepository<CustomEndpoint, Guid> customEndpointRepository,
|
||||
IRepository<CustomComponent, Guid> customComponentRepository,
|
||||
IRepository<ReportCategory, Guid> reportCategoriesRepository,
|
||||
IRepository<ReportTemplate, Guid> reportTemplatesRepository,
|
||||
IRepository<Home, Guid> homeRepository,
|
||||
|
|
@ -589,7 +575,6 @@ public class TenantDataSeeder : IDataSeedContributor, ITransientDependency
|
|||
_productRepository = productRepository;
|
||||
_paymentMethodRepository = paymentMethodRepository;
|
||||
_installmentOptionRepository = installmentOptionRepository;
|
||||
_customComponentRepository = customComponentRepository;
|
||||
_reportCategoriesRepository = reportCategoriesRepository;
|
||||
_reportTemplatesRepository = reportTemplatesRepository;
|
||||
_homeRepository = homeRepository;
|
||||
|
|
@ -830,23 +815,6 @@ public class TenantDataSeeder : IDataSeedContributor, ITransientDependency
|
|||
}
|
||||
}
|
||||
|
||||
foreach (var item in items.CustomComponents)
|
||||
{
|
||||
var exists = await _customComponentRepository.AnyAsync(x => x.Name == item.Name);
|
||||
|
||||
if (!exists)
|
||||
{
|
||||
await _customComponentRepository.InsertAsync(new CustomComponent(
|
||||
item.Name,
|
||||
item.RoutePath,
|
||||
item.Code,
|
||||
item.Props,
|
||||
item.Description,
|
||||
item.IsActive,
|
||||
JsonSerializer.Serialize(item.Dependencies)));
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var item in items.ReportCategories)
|
||||
{
|
||||
var exists = await _reportCategoriesRepository.AnyAsync(x => x.Name == item.Name);
|
||||
|
|
|
|||
15
configs/seeds/host/custom/DynamicEntityComponent.json
Normal file
15
configs/seeds/host/custom/DynamicEntityComponent.json
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"GeneratedAt": "2026-08-18T08:40:41.9083616Z",
|
||||
"CustomComponents": [
|
||||
{
|
||||
"Name": "DynamicEntityComponent",
|
||||
"RoutePath": "/admin/dynamic-entity",
|
||||
"Code": "import React, { useEffect, useState } from \"react\";\nimport axios from \"axios\";\n\ninterface DynamicEntityComponentProps {\n title: string;\n}\n\nconst api = axios.create({\n baseURL: \"https://localhost:44344\",\n});\n\nconst DynamicEntityComponent: React.FC<DynamicEntityComponentProps> = ({ title }) => {\n const [data, setData] = useState<Array<{ id: string; name: string }>>([]);\n const [loading, setLoading] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n useEffect(() => {\n const fetchData = async () => {\n setLoading(true);\n setError(null);\n\n try {\n const res = await api.get(`/api/app/crudendpoint/${title}`);\n const raw = Array.isArray(res.data) ? res.data : res.data?.items ?? [];\n\n const filtered = raw.map((item: any) => ({\n id: item.Id ?? item.id,\n name: item.Name ?? item.name,\n }));\n\n setData(filtered);\n } catch (err: any) {\n setError(err.message || \"Failed to fetch data\");\n } finally {\n setLoading(false);\n }\n };\n\n if (title) fetchData();\n }, [title]);\n\n if (loading) return <div>Loading...</div>;\n if (error) return <div className=\"text-red-600 dark:text-red-400\">Error: {error}</div>;\n if (!data.length) return <div>No records found</div>;\n\n const headers = [\"id\", \"name\", \"actions\"];\n\n return (\n <div className=\"overflow-auto\">\n <table className=\"min-w-full bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-700 shadow-sm rounded-lg\">\n <thead className=\"bg-slate-100 dark:bg-slate-800\">\n <tr>\n {headers.map((key) => (\n <th\n key={key}\n className=\"text-left px-4 py-2 border-b border-slate-200 dark:border-slate-700 text-sm font-medium text-slate-700 dark:text-slate-200\"\n >\n {key === \"actions\" ? \"Actions\" : key}\n </th>\n ))}\n </tr>\n </thead>\n <tbody>\n {data.map((item, rowIndex) => (\n <tr key={item.id ?? rowIndex} className=\"hover:bg-slate-50 dark:hover:bg-slate-800\">\n <td className=\"px-4 py-2 border-b border-slate-100 dark:border-slate-800 text-sm text-slate-800 dark:text-slate-100\">\n {item.id}\n </td>\n <td className=\"px-4 py-2 border-b border-slate-100 dark:border-slate-800 text-sm text-slate-800 dark:text-slate-100\">\n {item.name}\n </td>\n <td className=\"px-4 py-2 border-b border-slate-100 dark:border-slate-800\">\n <Button\n type=\"button\"\n color=\"blue-500\"\n onClick={() => alert(item.name)}\n shape=\"round\"\n size=\"xs\"\n variant=\"solid\"\n id=\"c_mrix4c1a_qrybhk\"\n >\n Show Name\n </Button>\n </td>\n </tr>\n ))}\n </tbody>\n </table>\n </div>\n );\n};\n\nexport default DynamicEntityComponent;",
|
||||
"Props": null,
|
||||
"Description": null,
|
||||
"IsActive": true,
|
||||
"Dependencies": [],
|
||||
"DataSources": []
|
||||
}
|
||||
]
|
||||
}
|
||||
61
configs/seeds/host/custom/RoleComponent.json
Normal file
61
configs/seeds/host/custom/RoleComponent.json
Normal file
File diff suppressed because one or more lines are too long
17
configs/seeds/host/custom/RoleListComponent.json
Normal file
17
configs/seeds/host/custom/RoleListComponent.json
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"GeneratedAt": "2026-08-18T00:00:00Z",
|
||||
"CustomComponents": [
|
||||
{
|
||||
"Name": "RoleListComponent",
|
||||
"RoutePath": "/admin/roles-list",
|
||||
"Code": "const RoleListComponent = ({\n title = \"AbpRoles\"\n}) => {\n return (\n <DynamicEntityComponent id=\"c_mdljvvmq_fno52v\" title={title} />\n );\n};\n\nexport default RoleListComponent;",
|
||||
"Props": null,
|
||||
"Description": null,
|
||||
"IsActive": true,
|
||||
"Dependencies": [
|
||||
"DynamicEntityComponent"
|
||||
],
|
||||
"DataSources": []
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -261,21 +261,23 @@ const ComponentEditor: React.FC<ComponentEditorProps> = ({
|
|||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
<Dialog.Footer className="flex items-center justify-end gap-2 border-t border-slate-200 pt-3 dark:border-slate-700">
|
||||
<Button type="button" size="sm" onClick={onClose} disabled={isSubmitting}>
|
||||
{translate('::App.Platform.Cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="solid"
|
||||
size="sm"
|
||||
disabled={isSubmitting || !values.name.trim() || !isValid}
|
||||
className="flex items-center gap-2"
|
||||
onClick={submitForm}
|
||||
>
|
||||
<FaRegSave className="h-4 w-4" />
|
||||
{isSubmitting ? translate('::App.Platform.Saving') : translate('::App.Platform.Save')}
|
||||
</Button>
|
||||
<Dialog.Footer className="flex flex-col items-stretch gap-2 border-t border-slate-200 pt-3 dark:border-slate-700">
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<Button type="button" size="sm" disabled={isSubmitting} onClick={onClose}>
|
||||
{translate('::App.Platform.Cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="solid"
|
||||
size="sm"
|
||||
disabled={isSubmitting || !values.name.trim() || !isValid}
|
||||
className="flex items-center gap-2"
|
||||
onClick={submitForm}
|
||||
>
|
||||
<FaRegSave className="h-4 w-4" />
|
||||
{isSubmitting ? translate('::App.Platform.Saving') : translate('::App.Platform.Save')}
|
||||
</Button>
|
||||
</div>
|
||||
</Dialog.Footer>
|
||||
</>
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in a new issue