WizardFileManager CustomEndpoints silme prosedürüne alındı
This commit is contained in:
parent
02b8d3abb4
commit
135a85e682
10 changed files with 273 additions and 10 deletions
5
.github/instructions/lowcode.instructions.md
vendored
5
.github/instructions/lowcode.instructions.md
vendored
|
|
@ -1986,6 +1986,11 @@ işlemde siler. Onay diyaloğu sırayı ve tabloları gösterir:
|
|||
- **Tablolar en sonda** düşürülür: bütün ekranlar gittikten sonra kullanım yeniden ölçülür; aynı
|
||||
tabloyu paylaşan alt formlar (`Hr_T_HealthCard` gibi) tabloyu bir kez listeler. Bağlantılar veri
|
||||
kaynağı kaydı silinmeden önce çözülür. Tablo seçmek yine `App.SqlQueryManager` ister.
|
||||
- **Talebin Custom Endpoint'leri de gider:** tablo talebe aitse (`create_table`/tasarımcı scripti var,
|
||||
talep dışında ekran kullanmıyor) onu sorgulayan Custom Endpoint'ler tabloyu tutmaz; veritabanından
|
||||
ve `data/App.DeveloperKit.CustomEndpoints.json`'dan (migrate edilmemiş satır dahil) silinir —
|
||||
tablo seçilmese de (`WizardDeleteTableDto.CustomEndpoints`, `WizardTableCleaner.DeleteCustomEndpointsAsync`).
|
||||
Diyalog bunları tablonun altında listeler. Tek wizard silmede endpoint yine tabloyu "kullanımda" tutar.
|
||||
- Rapor tek wizard silmeyle aynı sözleşmedir (`WizardDeleteResultDto`, bütün wizard'ların kayıtları
|
||||
tek listede).
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||
using System.Data.Common;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Sozsoft.Mcp.Dtos;
|
||||
|
|
@ -30,7 +31,7 @@ namespace Sozsoft.Mcp;
|
|||
/// <see cref="IMcpQueryExecutor"/> icindedir.
|
||||
/// </summary>
|
||||
[Authorize(PlatformConsts.AppCodes.DeveloperKits.McpServers.Invoke)]
|
||||
public class McpQueryAppService(
|
||||
public partial class McpQueryAppService(
|
||||
IMcpServerConfigProvider serverConfigProvider,
|
||||
IMcpSqlGuard sqlGuard,
|
||||
IMcpQueryExecutor queryExecutor,
|
||||
|
|
@ -115,10 +116,67 @@ public class McpQueryAppService(
|
|||
}
|
||||
catch (DbException ex)
|
||||
{
|
||||
throw new UserFriendlyException(L[McpErrorCodes.SelectFailed, ex.Message]);
|
||||
var columns = IsUnknownColumnError(ex) ? await DescribeTableColumnsAsync(context, sql) : null;
|
||||
throw new UserFriendlyException(columns is null
|
||||
? L[McpErrorCodes.SelectFailed, ex.Message]
|
||||
: L[McpErrorCodes.SelectFailedColumns, ex.Message, columns]);
|
||||
}
|
||||
}
|
||||
|
||||
private const int MaxDescribedTables = 5;
|
||||
|
||||
/// <summary>SQL Server 207 ("Invalid column name"), PostgreSQL 42703 (undefined_column).</summary>
|
||||
private static bool IsUnknownColumnError(DbException ex) =>
|
||||
ex.SqlState == "42703" || ex.Message.Contains("Invalid column name", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
/// Sorgunun FROM/JOIN ile andigi tablolarin gercek kolonlari (<c>Tablo(A, B, C); …</c>). Model
|
||||
/// kolonu tahmin ettiyse dogru listeyi ayni cevapta gorur ve ayrica sema araci cagirmadan duzeltir.
|
||||
/// Kolonlar okunamazsa null doner; asil hata yine iletilir.
|
||||
/// </summary>
|
||||
private async Task<string?> DescribeTableColumnsAsync(McpDataSourceContext context, string sql)
|
||||
{
|
||||
var tables = TableReferenceRegex().Matches(sql)
|
||||
.Select(match => match.Groups["table"].Value)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.Take(MaxDescribedTables)
|
||||
.ToList();
|
||||
if (tables.Count == 0)
|
||||
return null;
|
||||
|
||||
var parameters = tables.Select((table, index) => (Name: $"t{index}", Table: table))
|
||||
.ToDictionary(p => p.Name, p => (object)p.Table);
|
||||
|
||||
try
|
||||
{
|
||||
var rows = await queryExecutor.ExecuteAsync(
|
||||
context,
|
||||
$"SELECT TABLE_NAME, COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_NAME IN ({string.Join(", ", parameters.Keys.Select(name => "@" + name))}) ORDER BY TABLE_NAME, ORDINAL_POSITION",
|
||||
parameters,
|
||||
int.MaxValue);
|
||||
|
||||
var described = rows.Rows
|
||||
.Select(row => (Table: ReadIgnoreCase(row, "TABLE_NAME"), Column: ReadIgnoreCase(row, "COLUMN_NAME")))
|
||||
.Where(item => item.Table is not null && item.Column is not null)
|
||||
.GroupBy(item => item.Table!, StringComparer.OrdinalIgnoreCase)
|
||||
.Select(group => $"{group.Key}({string.Join(", ", group.Select(item => item.Column))})")
|
||||
.ToList();
|
||||
|
||||
return described.Count == 0 ? null : string.Join("; ", described);
|
||||
}
|
||||
catch (DbException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static string? ReadIgnoreCase(Dictionary<string, object?> row, string key) =>
|
||||
row.FirstOrDefault(pair => string.Equals(pair.Key, key, StringComparison.OrdinalIgnoreCase)).Value?.ToString();
|
||||
|
||||
/// <summary>FROM/JOIN'den sonraki ad; sema ve kose parantezi/tirnak atilir (<c>[dbo].[Tablo]</c> → <c>Tablo</c>).</summary>
|
||||
[GeneratedRegex(@"\b(?:FROM|JOIN)\s+(?:[\[""]?\w+[\]""]?\.)?[\[""]?(?<table>\w+)[\]""]?", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant)]
|
||||
private static partial Regex TableReferenceRegex();
|
||||
|
||||
public async Task<List<McpSavedQueryDto>> GetSavedQueriesAsync(string? search = null)
|
||||
{
|
||||
await EnsureToolAllowedAsync(McpToolNames.ListQueries);
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ public static class McpErrorCodes
|
|||
public const string VerifySelectPassed = Prefix + "VerifySelectPassed";
|
||||
public const string VerifySelectFailed = Prefix + "VerifySelectFailed";
|
||||
public const string SelectFailed = Prefix + "SelectFailed";
|
||||
public const string SelectFailedColumns = Prefix + "SelectFailedColumns";
|
||||
public const string VerifyComponentInactive = Prefix + "VerifyComponentInactive";
|
||||
public const string VerifyComponentRouteMismatch = Prefix + "VerifyComponentRouteMismatch";
|
||||
public const string VerifySeedClosureIncomplete = Prefix + "VerifySeedClosureIncomplete";
|
||||
|
|
|
|||
|
|
@ -513,6 +513,12 @@ public class WizardDeleteTableDto
|
|||
|
||||
/// <summary>Tabloyla birlikte silinecek seed dosyalari, seed klasorune gore goreli.</summary>
|
||||
public List<string> Files { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Talep silmesinde talepten kalkan Custom Endpoint'ler (ad): tablo talebe aittir (scripti var,
|
||||
/// talep disinda ekran kullanmiyor) ve endpoint onu sorgular. Tablo secilmese de silinir.
|
||||
/// </summary>
|
||||
public List<string> CustomEndpoints { get; set; } = [];
|
||||
}
|
||||
|
||||
/// <summary>Silmeden once onay diyaloguna gelen analiz.</summary>
|
||||
|
|
|
|||
|
|
@ -315,6 +315,8 @@ public class ListFormWizardAppService(
|
|||
: tableColumns.ContainsKey("IsDeleted");
|
||||
isCreated = tableColumns.ContainsKey("CreatorId");
|
||||
ValidateImageColumnLengths(input, tableColumns);
|
||||
foreach (var item in input.Groups.SelectMany(g => g.Items))
|
||||
item.CaptionName = await ResolveLanguageKeyCasingAsync(rows, item.CaptionName);
|
||||
listFormDto = input.BuildListForm(isDeleted, isCreated);
|
||||
fieldDtos = input.BuildFields();
|
||||
|
||||
|
|
@ -429,6 +431,24 @@ public class ListFormWizardAppService(
|
|||
inserted.LanguageKeys.Add(key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Anahtar yalnizca harf buyuklugu farkiyla zaten varsa onun yazimini dondurur. Anahtar
|
||||
/// <c>(ResourceName, Key)</c> uzerinde harf duyarsiz tekildir, metin sozlugu ise harf duyarlidir:
|
||||
/// farkli yazimla birakilan alan basligi ikinci bir anahtar acamaz ve ekranda anahtar olarak kalir.
|
||||
/// </summary>
|
||||
private async Task<string?> ResolveLanguageKeyCasingAsync(WizardSeedRowStore rows, string? key)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key))
|
||||
return key;
|
||||
|
||||
var fileKey = WizardSeedRowStore.Text(rows.Find(WizardSeedRowStore.LanguageTextFile, nameof(LanguageText.Key), key), nameof(LanguageText.Key));
|
||||
if (fileKey != null)
|
||||
return fileKey;
|
||||
|
||||
var res = PlatformConsts.AppName;
|
||||
return (await repoLangKey.FirstOrDefaultAsync(a => a.ResourceName == res && a.Key == key))?.Key ?? key;
|
||||
}
|
||||
|
||||
private void UpsertLanguageRows(WizardSeedRowStore rows, string res, string key, string? textEn, string? textTr)
|
||||
{
|
||||
foreach (var (culture, value) in new[] { (cultureNameDefault, textEn), (LanguageCodes.Tr, textTr) })
|
||||
|
|
@ -1194,6 +1214,10 @@ public class ListFormWizardAppService(
|
|||
}
|
||||
}
|
||||
|
||||
// Endpoint'ler tablodan once gider; tablo secilmese de talebin parcasi olarak silinir.
|
||||
await _tableCleaner.DeleteCustomEndpointsAsync(
|
||||
[.. tables.SelectMany(t => t.CustomEndpoints).Distinct(StringComparer.OrdinalIgnoreCase)], result.Items);
|
||||
|
||||
if (tables.Count > 0)
|
||||
await _tableCleaner.DropAsync(tables, dropTables, connections, result.Items);
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
|
|
@ -35,6 +36,7 @@ public class WizardTableCleaner(
|
|||
IRepository<CustomEndpoint, Guid> repoCustomEndpoint,
|
||||
IRepository<CustomComponent, Guid> repoCustomComponent,
|
||||
IRepository<CrudEndpoint, Guid> repoCrudEndpoint,
|
||||
IListFormSeedDataSynchronizer seedDataSynchronizer,
|
||||
IDynamicDataManager dynamicDataManager,
|
||||
IConfiguration configuration,
|
||||
ILogger<WizardTableCleaner> logger) : ITransientDependency
|
||||
|
|
@ -79,6 +81,18 @@ public class WizardTableCleaner(
|
|||
.Where(u => u.RecordType != WizardRecordTypeEnum.ListForm ||
|
||||
batchListForms?.Contains(u.Name, StringComparer.OrdinalIgnoreCase) != true)
|
||||
.ToList();
|
||||
|
||||
// Talep silmesinde talebin tablosunu sorgulayan Custom Endpoint'ler talebin parcasidir; tabloyu
|
||||
// tutmaz, taleple birlikte silinir. Migrate edilmemis endpoint yalnizca seed dosyasindadir.
|
||||
if (batchListForms != null && HasCreateTableScript(tableName) &&
|
||||
usedBy.All(u => u.RecordType == WizardRecordTypeEnum.CustomEndpoint))
|
||||
{
|
||||
table.CustomEndpoints = [.. usedBy.Select(u => u.Name)
|
||||
.Concat(await FindSeedEndpointsAsync(table))
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)];
|
||||
usedBy.Clear();
|
||||
}
|
||||
|
||||
if (usedBy.Count > 0)
|
||||
{
|
||||
table.Reason = WizardDeleteReasonEnum.InUse;
|
||||
|
|
@ -167,8 +181,53 @@ public class WizardTableCleaner(
|
|||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Talep silmesinde talebin tablolarini sorgulayan Custom Endpoint'leri veritabanindan ve
|
||||
/// <c>data/App.DeveloperKit.CustomEndpoints.json</c> dosyasindan siler.
|
||||
/// </summary>
|
||||
public async Task DeleteCustomEndpointsAsync(IReadOnlyCollection<string> names, List<WizardDeleteLogItemDto> report)
|
||||
{
|
||||
if (names.Count == 0)
|
||||
return;
|
||||
|
||||
var endpoints = await repoCustomEndpoint.GetListAsync(a => names.Contains(a.Name));
|
||||
if (endpoints.Count > 0)
|
||||
await repoCustomEndpoint.DeleteManyAsync(endpoints, autoSave: true);
|
||||
|
||||
await seedDataSynchronizer.RemoveRowsByFieldAsync(CustomEndpointsListFormCode, nameof(CustomEndpoint.Name), names);
|
||||
|
||||
report.AddRange(names.Select(name => new WizardDeleteLogItemDto
|
||||
{
|
||||
RecordType = WizardRecordTypeEnum.CustomEndpoint,
|
||||
Name = name,
|
||||
Status = WizardDeleteStatusEnum.Deleted,
|
||||
}));
|
||||
}
|
||||
|
||||
// ── Analiz ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
private const string CustomEndpointsListFormCode = PlatformConsts.AppCodes.DeveloperKits.CustomEndpoints;
|
||||
|
||||
/// <summary>Henuz migrate edilmemis, yalnizca seed dosyasinda duran ve tabloyu sorgulayan endpoint'ler.</summary>
|
||||
private async Task<List<string>> FindSeedEndpointsAsync(WizardDeleteTableDto table)
|
||||
{
|
||||
var path = ScopeFile(SeedPathResolver.GetDataFilePath(CustomEndpointsListFormCode));
|
||||
if (!File.Exists(path))
|
||||
return [];
|
||||
|
||||
var file = JsonSerializer.Deserialize<ListFormSeedDataFileDto>(await File.ReadAllTextAsync(path), ListFormSeedDataFileDto.JsonOptions);
|
||||
var pattern = TablePattern(table.TableName);
|
||||
|
||||
string? Text(Dictionary<string, object> row, string field) =>
|
||||
row.FirstOrDefault(kv => string.Equals(kv.Key, field, StringComparison.OrdinalIgnoreCase)).Value?.ToString();
|
||||
|
||||
return [.. (file?.Rows ?? [])
|
||||
.Where(row => string.Equals(Text(row, nameof(CustomEndpoint.DataSourceCode)), table.DataSourceCode, StringComparison.OrdinalIgnoreCase) &&
|
||||
Text(row, nameof(CustomEndpoint.Sql)) is { } sql && pattern.IsMatch(sql))
|
||||
.Select(row => Text(row, nameof(CustomEndpoint.Name)))
|
||||
.OfType<string>()];
|
||||
}
|
||||
|
||||
private async Task ReadTableStateAsync(WizardDeleteTableDto table)
|
||||
{
|
||||
try
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -39933,17 +39933,17 @@
|
|||
"Value": "Ağırlık"
|
||||
},
|
||||
{
|
||||
"Id": "bbcfd955-c535-5c01-b9bb-c7321dd61d89",
|
||||
"Id": "5961b6ba-f1d4-510a-a9cd-ac723dbad6a8",
|
||||
"CultureName": "en",
|
||||
"ResourceName": "Platform",
|
||||
"Key": "App.Listform.ListformField.WorkcenterId",
|
||||
"Value": "Workcenter"
|
||||
"Key": "App.Listform.ListformField.WorkCenterId",
|
||||
"Value": "Work Center"
|
||||
},
|
||||
{
|
||||
"Id": "994c5396-1eea-5a2f-a4dc-11d1c3b1bb8b",
|
||||
"Id": "db209c22-79f7-53aa-b125-06be98097e3e",
|
||||
"CultureName": "tr",
|
||||
"ResourceName": "Platform",
|
||||
"Key": "App.Listform.ListformField.WorkcenterId",
|
||||
"Key": "App.Listform.ListformField.WorkCenterId",
|
||||
"Value": "İş Merkezi"
|
||||
},
|
||||
{
|
||||
|
|
@ -40702,6 +40702,20 @@
|
|||
"Key": "App.ListForm.WizardDeleteTableNoPermission",
|
||||
"Value": "Tablo silmek için SQL Query Manager yetkisi gerekir."
|
||||
},
|
||||
{
|
||||
"Id": "2f3d5aa3-35d7-5d2b-bc3d-ba0363eb838b",
|
||||
"CultureName": "en",
|
||||
"ResourceName": "Platform",
|
||||
"Key": "App.ListForm.WizardDeleteCustomEndpoints",
|
||||
"Value": "Custom Endpoints querying this table are deleted with the request:"
|
||||
},
|
||||
{
|
||||
"Id": "5c30228d-7a64-509d-92cb-5218d0cacfc5",
|
||||
"CultureName": "tr",
|
||||
"ResourceName": "Platform",
|
||||
"Key": "App.ListForm.WizardDeleteCustomEndpoints",
|
||||
"Value": "Bu tabloyu sorgulayan Custom Endpoint'ler taleple birlikte silinir:"
|
||||
},
|
||||
{
|
||||
"Id": "50b48483-4900-53d7-8264-e1a31c899097",
|
||||
"CultureName": "en",
|
||||
|
|
@ -69290,6 +69304,20 @@
|
|||
"Key": "App.Mcp.Error.SelectFailed",
|
||||
"Value": "Sorgu çalışmadı: {0} Bir kolonun var olduğunu varsayma (IsDeleted, TenantId, IsActive her tabloda yoktur); tablonun kolonlarını search_schema ya da INFORMATION_SCHEMA.COLUMNS üzerinde bir SELECT ile kontrol edip sorguyu düzelt."
|
||||
},
|
||||
{
|
||||
"Id": "f78c1319-0925-5e25-af53-eed5f6039eb1",
|
||||
"CultureName": "en",
|
||||
"ResourceName": "Platform",
|
||||
"Key": "App.Mcp.Error.SelectFailedColumns",
|
||||
"Value": "The query failed: {0} Actual columns of the tables in the query: {1}. Rewrite the query using only these columns; do not guess column names."
|
||||
},
|
||||
{
|
||||
"Id": "834dbec7-04c1-57c7-9044-a9bc1e348828",
|
||||
"CultureName": "tr",
|
||||
"ResourceName": "Platform",
|
||||
"Key": "App.Mcp.Error.SelectFailedColumns",
|
||||
"Value": "Sorgu çalışmadı: {0} Sorgudaki tabloların gerçek kolonları: {1}. Sorguyu yalnızca bu kolonlarla yeniden yaz; kolon adı tahmin etme."
|
||||
},
|
||||
{
|
||||
"Id": "aac27480-88f4-5230-9820-f9c7472ef178",
|
||||
"CultureName": "en",
|
||||
|
|
@ -86929,6 +86957,76 @@
|
|||
"ResourceName": "Platform",
|
||||
"Key": "App.Listform.ListformField.Longitude",
|
||||
"Value": "Boylam"
|
||||
},
|
||||
{
|
||||
"Id": "f62001da-76fc-59e7-a302-39ca902d471e",
|
||||
"CultureName": "en",
|
||||
"ResourceName": "Platform",
|
||||
"Key": "App.Listform.ListformField.InternalReference",
|
||||
"Value": "Internal Reference"
|
||||
},
|
||||
{
|
||||
"Id": "c7fa6c98-b2fd-563c-a83f-87639440f864",
|
||||
"CultureName": "tr",
|
||||
"ResourceName": "Platform",
|
||||
"Key": "App.Listform.ListformField.InternalReference",
|
||||
"Value": "İç Referans"
|
||||
},
|
||||
{
|
||||
"Id": "e5c139da-74ef-51d9-923e-c9d3de7e109b",
|
||||
"CultureName": "en",
|
||||
"ResourceName": "Platform",
|
||||
"Key": "App.Listform.ListformField.Cost",
|
||||
"Value": "Cost"
|
||||
},
|
||||
{
|
||||
"Id": "74014c98-a5f8-53de-b16e-d77c86f987b4",
|
||||
"CultureName": "tr",
|
||||
"ResourceName": "Platform",
|
||||
"Key": "App.Listform.ListformField.Cost",
|
||||
"Value": "Maliyet"
|
||||
},
|
||||
{
|
||||
"Id": "de319c46-4357-5025-86c4-1700e71cd1c5",
|
||||
"CultureName": "en",
|
||||
"ResourceName": "Platform",
|
||||
"Key": "App.Listform.ListformField.SetupTimeMin",
|
||||
"Value": "Setup Time (min)"
|
||||
},
|
||||
{
|
||||
"Id": "440df770-7b20-5180-8aff-bd8ef821657d",
|
||||
"CultureName": "tr",
|
||||
"ResourceName": "Platform",
|
||||
"Key": "App.Listform.ListformField.SetupTimeMin",
|
||||
"Value": "Kurulum Süresi (dk)"
|
||||
},
|
||||
{
|
||||
"Id": "e8104b4f-8b74-56f7-a96d-3126fca31472",
|
||||
"CultureName": "en",
|
||||
"ResourceName": "Platform",
|
||||
"Key": "App.Listform.ListformField.CleanupTimeMin",
|
||||
"Value": "Cleanup Time (min)"
|
||||
},
|
||||
{
|
||||
"Id": "cf803733-a414-5abd-a659-70e86431f51b",
|
||||
"CultureName": "tr",
|
||||
"ResourceName": "Platform",
|
||||
"Key": "App.Listform.ListformField.CleanupTimeMin",
|
||||
"Value": "Temizlik Süresi (dk)"
|
||||
},
|
||||
{
|
||||
"Id": "8c9c9180-00ec-5ca0-9d34-a96c6b6a68df",
|
||||
"CultureName": "en",
|
||||
"ResourceName": "Platform",
|
||||
"Key": "App.Listform.ListformField.ExpenseAccount",
|
||||
"Value": "Expense Account"
|
||||
},
|
||||
{
|
||||
"Id": "415a25cd-c5cf-561b-a827-55259b8108e6",
|
||||
"CultureName": "tr",
|
||||
"ResourceName": "Platform",
|
||||
"Key": "App.Listform.ListformField.ExpenseAccount",
|
||||
"Value": "Gider Hesabı"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -234,6 +234,8 @@ export interface WizardDeleteTableDto {
|
|||
usedBy: WizardRecordRefDto[]
|
||||
/** Tabloyla birlikte silinecek seed dosyaları (kapsam klasörüne göre). */
|
||||
files: string[]
|
||||
/** Talep silmesinde tabloyu sorgulayan, taleple birlikte silinecek Custom Endpoint'ler. */
|
||||
customEndpoints?: string[]
|
||||
}
|
||||
|
||||
export interface WizardDeleteAnalysisDto {
|
||||
|
|
|
|||
|
|
@ -596,6 +596,16 @@ const WizardFileManager = () => {
|
|||
{translate('::App.ListForm.WizardDeleteTableNoPermission')}
|
||||
</div>
|
||||
)}
|
||||
{!!table.customEndpoints?.length && (
|
||||
<div className="ml-6 mt-1 text-xs text-gray-500">
|
||||
{translate('::App.ListForm.WizardDeleteCustomEndpoints')}
|
||||
{table.customEndpoints.map((name) => (
|
||||
<div key={name} className="break-all font-mono">
|
||||
{name}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue