Wizard File Manager içerisine Menü ve Bağımlılıklar eklendi
This commit is contained in:
parent
b5f411e684
commit
488a3274e1
6 changed files with 414 additions and 47 deletions
8
.github/instructions/lowcode.instructions.md
vendored
8
.github/instructions/lowcode.instructions.md
vendored
|
|
@ -3322,6 +3322,14 @@ Wizard Manager'ın kendi aksiyonları: liste/kart görünümü, arama, düzenlem
|
||||||
silme, DB Migrate, Export (zip), Import (diff + rollback). Yetkiler
|
silme, DB Migrate, Export (zip), Import (diff + rollback). Yetkiler
|
||||||
`App.Listforms.Wizard{,.Create,.Update,.Delete,.Export,.Import}`.
|
`App.Listforms.Wizard{,.Create,.Update,.Delete,.Export,.Import}`.
|
||||||
|
|
||||||
|
Listede her ekranın üst menü zinciri (`WizardFileInfoDto.MenuPath`, Menu tablosundan; kayıt yoksa
|
||||||
|
wizard'ın `MenuParentCode` cevabı) görünür. Bağlı wizard'lar üst ekranın altında gruplanır
|
||||||
|
(`Dependencies`): List yolunda alt formlar, Custom yolunda component `Dependencies` zinciri —
|
||||||
|
export kapanışıyla aynı kural; wizard'ı olmayan ara component atlanır. Kök, hiçbir wizard'ın
|
||||||
|
bağımlılığı olmayan dosyadır; ortak bir alt form her üst ekranın altında tekrar görünür.
|
||||||
|
Gruplar `+`/`−` ile açılıp kapanır (varsayılan açık); arama sırasında eşleşme gizli kalmasın diye
|
||||||
|
hepsi açık gösterilir.
|
||||||
|
|
||||||
### 9.9 Numaratör — `Sequence` (`App.Definitions.Sequence`)
|
### 9.9 Numaratör — `Sequence` (`App.Definitions.Sequence`)
|
||||||
|
|
||||||
"Sipariş numarası otomatik gelsin", "talep numarası her yıl 1'den başlasın" istekleri için kod
|
"Sipariş numarası otomatik gelsin", "talep numarası her yıl 1'den başlasın" istekleri için kod
|
||||||
|
|
|
||||||
|
|
@ -381,4 +381,16 @@ public class WizardFileInfoDto
|
||||||
|
|
||||||
/// <summary>Menu kaydina yazilan URL; Custom yolunda bilesenin rota yoludur.</summary>
|
/// <summary>Menu kaydina yazilan URL; Custom yolunda bilesenin rota yoludur.</summary>
|
||||||
public string MenuUrl { get; set; }
|
public string MenuUrl { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ekranin menu agacindaki ust menuleri, kokten baslayarak; her oge menunun dil anahtaridir.
|
||||||
|
/// Menusuz wizard'da bostur.
|
||||||
|
/// </summary>
|
||||||
|
public List<string> MenuPath { get; set; } = [];
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bu wizard'in dogrudan bagli oldugu diger wizard dosyalari: List yolunda alt formlar, Custom
|
||||||
|
/// yolunda component bagimliliklari. Yalnizca kapsamda wizard dosyasi olanlar listelenir.
|
||||||
|
/// </summary>
|
||||||
|
public List<string> Dependencies { get; set; } = [];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -593,6 +593,12 @@ public class ListFormWizardAppService(
|
||||||
if (!Directory.Exists(outputPath))
|
if (!Directory.Exists(outputPath))
|
||||||
return result;
|
return result;
|
||||||
|
|
||||||
|
var seeds = new Dictionary<string, WizardSeedFileDto>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
var menus = (await repoMenu.GetListAsync())
|
||||||
|
.Where(m => !string.IsNullOrWhiteSpace(m.Code))
|
||||||
|
.GroupBy(m => m.Code, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToDictionary(g => g.Key, g => g.First(), StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
foreach (var file in Directory.GetFiles(outputPath, "*.json").OrderBy(Path.GetFileName))
|
foreach (var file in Directory.GetFiles(outputPath, "*.json").OrderBy(Path.GetFileName))
|
||||||
{
|
{
|
||||||
var fileName = Path.GetFileName(file);
|
var fileName = Path.GetFileName(file);
|
||||||
|
|
@ -600,6 +606,9 @@ public class ListFormWizardAppService(
|
||||||
{
|
{
|
||||||
var json = await File.ReadAllTextAsync(file);
|
var json = await File.ReadAllTextAsync(file);
|
||||||
var seed = WizardSeedFileDto.FromSeedJson(json);
|
var seed = WizardSeedFileDto.FromSeedJson(json);
|
||||||
|
if (seed?.Wizard != null)
|
||||||
|
seeds[fileName] = seed;
|
||||||
|
|
||||||
result.Add(new WizardFileInfoDto
|
result.Add(new WizardFileInfoDto
|
||||||
{
|
{
|
||||||
FileName = fileName,
|
FileName = fileName,
|
||||||
|
|
@ -611,6 +620,7 @@ public class ListFormWizardAppService(
|
||||||
CustomComponentName = seed?.Wizard?.CustomComponentName ?? string.Empty,
|
CustomComponentName = seed?.Wizard?.CustomComponentName ?? string.Empty,
|
||||||
MenuUrl = seed?.Wizard?.MenuUrl ?? string.Empty,
|
MenuUrl = seed?.Wizard?.MenuUrl ?? string.Empty,
|
||||||
CreatedAt = seed?.Wizard?.SeededAt.ToString("s") ?? string.Empty,
|
CreatedAt = seed?.Wizard?.SeededAt.ToString("s") ?? string.Empty,
|
||||||
|
MenuPath = seed?.Wizard != null ? ResolveMenuPath(seed.Wizard, menus) : [],
|
||||||
HasInsertedRecords = seed?.InsertedRecords != null &&
|
HasInsertedRecords = seed?.InsertedRecords != null &&
|
||||||
(seed.InsertedRecords.LanguageKeys.Count > 0 ||
|
(seed.InsertedRecords.LanguageKeys.Count > 0 ||
|
||||||
seed.InsertedRecords.PermissionGroupNames.Count > 0 ||
|
seed.InsertedRecords.PermissionGroupNames.Count > 0 ||
|
||||||
|
|
@ -626,12 +636,138 @@ public class ListFormWizardAppService(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await FillWizardDependenciesAsync(result, seeds);
|
||||||
|
|
||||||
// CreatedAt sortable ("s") bicimindedir; metin karsilastirmasi da kronolojiktir.
|
// CreatedAt sortable ("s") bicimindedir; metin karsilastirmasi da kronolojiktir.
|
||||||
return [.. result
|
return [.. result
|
||||||
.OrderByDescending(x => x.CreatedAt, StringComparer.Ordinal)
|
.OrderByDescending(x => x.CreatedAt, StringComparer.Ordinal)
|
||||||
.ThenBy(x => x.FileName, StringComparer.OrdinalIgnoreCase)];
|
.ThenBy(x => x.FileName, StringComparer.OrdinalIgnoreCase)];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Ekranin ust menu zinciri, kokten baslayarak. Menu tablosu esastir — ekran sonradan baska bir
|
||||||
|
/// menuye tasinmis olabilir; menu kaydi hic yoksa wizard'in ust menu cevabina duser.
|
||||||
|
/// </summary>
|
||||||
|
private static List<string> ResolveMenuPath(ListFormWizardDto wizard, Dictionary<string, Menu> menus)
|
||||||
|
{
|
||||||
|
if (!wizard.CreateMenu)
|
||||||
|
return [];
|
||||||
|
|
||||||
|
if (!menus.TryGetValue(wizard.ResolveMenuCode(), out var menu))
|
||||||
|
{
|
||||||
|
return string.IsNullOrWhiteSpace(wizard.MenuParentCode) ? [] : [wizard.MenuParentCode.Trim()];
|
||||||
|
}
|
||||||
|
|
||||||
|
var path = new List<string>();
|
||||||
|
var visited = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { menu.Code };
|
||||||
|
|
||||||
|
// Bozuk bir ParentCode dongusu listeyi kilitlemesin diye ziyaret edilenler takip edilir.
|
||||||
|
while (!string.IsNullOrWhiteSpace(menu.ParentCode)
|
||||||
|
&& visited.Add(menu.ParentCode)
|
||||||
|
&& menus.TryGetValue(menu.ParentCode, out var parent))
|
||||||
|
{
|
||||||
|
path.Insert(0, string.IsNullOrWhiteSpace(parent.DisplayName) ? parent.Code : parent.DisplayName);
|
||||||
|
menu = parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Her wizard'in dogrudan bagli oldugu diger wizard dosyalarini doldurur. Bagimlilik kurallari
|
||||||
|
/// export kapanisiyla aynidir (<see cref="WizardExportCollector"/>): List yolunda alt formlar,
|
||||||
|
/// Custom yolunda component zinciri. Custom zincirinde wizard'i olmayan ara component'ler
|
||||||
|
/// atlanarak altlarindaki wizard'lara ulasilir; aksi halde grup ara halkada kopardi.
|
||||||
|
/// </summary>
|
||||||
|
private async Task FillWizardDependenciesAsync(List<WizardFileInfoDto> files, Dictionary<string, WizardSeedFileDto> seeds)
|
||||||
|
{
|
||||||
|
var fileByListFormCode = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
var fileByComponent = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
foreach (var (fileName, seed) in seeds)
|
||||||
|
{
|
||||||
|
if (seed.Wizard.ComponentKind == WizardComponentKindEnum.Custom)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(seed.Wizard.CustomComponentName))
|
||||||
|
fileByComponent.TryAdd(seed.Wizard.CustomComponentName.Trim(), fileName);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
fileByListFormCode.TryAdd(seed.Wizard.ResolveListFormCode(), fileName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var componentDependencies = fileByComponent.Count > 0
|
||||||
|
? await new WizardExportCollector(SeedPathResolver.GetScopePath(_configuration, CurrentTenant.Id))
|
||||||
|
.ReadCustomComponentDependenciesAsync()
|
||||||
|
: [];
|
||||||
|
|
||||||
|
foreach (var file in files)
|
||||||
|
{
|
||||||
|
if (!seeds.TryGetValue(file.FileName, out var seed))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var dependencies = seed.Wizard.ComponentKind == WizardComponentKindEnum.Custom
|
||||||
|
? ResolveComponentDependencyFiles(seed.Wizard.CustomComponentName, componentDependencies, fileByComponent)
|
||||||
|
: ReadSubFormCodes(seed)
|
||||||
|
.Select(code => fileByListFormCode.GetValueOrDefault(code))
|
||||||
|
.OfType<string>();
|
||||||
|
|
||||||
|
file.Dependencies = [.. dependencies
|
||||||
|
.Where(d => !string.Equals(d, file.FileName, StringComparison.OrdinalIgnoreCase))
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<string> ReadSubFormCodes(WizardSeedFileDto seed)
|
||||||
|
{
|
||||||
|
SubFormDto[] subForms;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
subForms = seed.ListForm?.SubFormsDto ?? [.. seed.Wizard.SubForms ?? []];
|
||||||
|
}
|
||||||
|
catch (JsonException)
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return subForms
|
||||||
|
.Select(s => s?.Code?.Trim())
|
||||||
|
.Where(code => !string.IsNullOrWhiteSpace(code))
|
||||||
|
.OfType<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<string> ResolveComponentDependencyFiles(
|
||||||
|
string componentName,
|
||||||
|
Dictionary<string, List<string>> componentDependencies,
|
||||||
|
Dictionary<string, string> fileByComponent)
|
||||||
|
{
|
||||||
|
var result = new List<string>();
|
||||||
|
if (string.IsNullOrWhiteSpace(componentName))
|
||||||
|
return result;
|
||||||
|
|
||||||
|
var visited = new HashSet<string>(StringComparer.OrdinalIgnoreCase) { componentName.Trim() };
|
||||||
|
var pending = new Queue<string>(componentDependencies.GetValueOrDefault(componentName.Trim()) ?? []);
|
||||||
|
|
||||||
|
while (pending.Count > 0)
|
||||||
|
{
|
||||||
|
var name = pending.Dequeue();
|
||||||
|
if (!visited.Add(name))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (fileByComponent.TryGetValue(name, out var fileName))
|
||||||
|
{
|
||||||
|
result.Add(fileName);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var next in componentDependencies.GetValueOrDefault(name) ?? [])
|
||||||
|
pending.Enqueue(next);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<WizardSeedFileDto> GetFile(string fileName)
|
public async Task<WizardSeedFileDto> GetFile(string fileName)
|
||||||
{
|
{
|
||||||
var filePath = ResolveWizardSeedFilePath(fileName);
|
var filePath = ResolveWizardSeedFilePath(fileName);
|
||||||
|
|
|
||||||
|
|
@ -262,6 +262,25 @@ public sealed class WizardExportCollector(string scopeRoot)
|
||||||
items[relativePath] = new WizardExportItem(relativePath, "Custom component tanımı", null, content);
|
items[relativePath] = new WizardExportItem(relativePath, "Custom component tanımı", null, content);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Component adi -> dogrudan bagimliliklari. Wizard listesi gruplamayi export'un izledigi
|
||||||
|
/// zincirle ayni kaynaktan kurar; boylece listede birlikte gorunenler arsive de birlikte girer.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<Dictionary<string, List<string>>> ReadCustomComponentDependenciesAsync()
|
||||||
|
{
|
||||||
|
var rows = await ReadCustomComponentRowsAsync();
|
||||||
|
if (rows == null)
|
||||||
|
return new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
return rows.ToDictionary(
|
||||||
|
x => x.Key,
|
||||||
|
x => ParseSeedRowList<string>(GetRowText(x.Value, "Dependencies"))
|
||||||
|
.Where(d => !string.IsNullOrWhiteSpace(d))
|
||||||
|
.Select(d => d.Trim())
|
||||||
|
.ToList(),
|
||||||
|
StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<Dictionary<string, Dictionary<string, object>>?> ReadCustomComponentRowsAsync()
|
private async Task<Dictionary<string, Dictionary<string, object>>?> ReadCustomComponentRowsAsync()
|
||||||
{
|
{
|
||||||
var seedFile = await ReadCustomComponentFileAsync();
|
var seedFile = await ReadCustomComponentFileAsync();
|
||||||
|
|
|
||||||
|
|
@ -135,6 +135,10 @@ export interface WizardFileInfoDto {
|
||||||
componentKind: WizardComponentKindEnum
|
componentKind: WizardComponentKindEnum
|
||||||
customComponentName: string
|
customComponentName: string
|
||||||
menuUrl: string
|
menuUrl: string
|
||||||
|
/** Üst menü zinciri (kökten), menülerin dil anahtarları. */
|
||||||
|
menuPath: string[]
|
||||||
|
/** Doğrudan bağlı diğer wizard dosyaları: alt formlar / component bağımlılıkları. */
|
||||||
|
dependencies: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WizardInsertedRecordsDto {
|
export interface WizardInsertedRecordsDto {
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,10 @@ import {
|
||||||
FaFileImport,
|
FaFileImport,
|
||||||
FaTh,
|
FaTh,
|
||||||
FaList,
|
FaList,
|
||||||
|
FaSitemap,
|
||||||
|
FaLevelUpAlt,
|
||||||
|
FaPlusSquare,
|
||||||
|
FaMinusSquare,
|
||||||
} from 'react-icons/fa'
|
} from 'react-icons/fa'
|
||||||
import DbMigrateButton from '@/components/shared/DbMigrateButton'
|
import DbMigrateButton from '@/components/shared/DbMigrateButton'
|
||||||
import {
|
import {
|
||||||
|
|
@ -55,6 +59,77 @@ interface ConfirmState {
|
||||||
wizardName: string
|
wizardName: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface WizardTreeNode {
|
||||||
|
file: WizardFileInfoDto
|
||||||
|
children: WizardTreeNode[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bağlı wizard'ları üst ekranın altında toplar. Kök, hiçbir wizard'ın bağımlılığı olmayan
|
||||||
|
* dosyadır; birden fazla ekranın kullandığı bir alt form her birinin altında görünür. Döngüdeki
|
||||||
|
* dosyaların kökü olmadığı için hiç çizilmeyenler sonda kök olarak eklenir.
|
||||||
|
*/
|
||||||
|
const buildWizardTree = (files: WizardFileInfoDto[]): WizardTreeNode[] => {
|
||||||
|
const byName = new Map(files.map((f) => [f.fileName.toLowerCase(), f]))
|
||||||
|
const childNames = new Set(
|
||||||
|
files.flatMap((f) => (f.dependencies ?? []).map((d) => d.toLowerCase())),
|
||||||
|
)
|
||||||
|
const rendered = new Set<string>()
|
||||||
|
|
||||||
|
const build = (file: WizardFileInfoDto, path: Set<string>): WizardTreeNode => {
|
||||||
|
const key = file.fileName.toLowerCase()
|
||||||
|
rendered.add(key)
|
||||||
|
const nextPath = new Set(path).add(key)
|
||||||
|
const children = (file.dependencies ?? [])
|
||||||
|
.map((d) => byName.get(d.toLowerCase()))
|
||||||
|
.filter((c): c is WizardFileInfoDto => !!c && !nextPath.has(c.fileName.toLowerCase()))
|
||||||
|
.map((c) => build(c, nextPath))
|
||||||
|
return { file, children }
|
||||||
|
}
|
||||||
|
|
||||||
|
const roots = files
|
||||||
|
.filter((f) => !childNames.has(f.fileName.toLowerCase()))
|
||||||
|
.map((f) => build(f, new Set()))
|
||||||
|
// Döngünün bir üyesi çizildiğinde diğerleri de çizilir; kontrol her adımda tekrarlanır.
|
||||||
|
for (const f of files) {
|
||||||
|
if (!rendered.has(f.fileName.toLowerCase())) roots.push(build(f, new Set()))
|
||||||
|
}
|
||||||
|
return roots
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Aramada bir düğüm, kendisi ya da altındakilerden biri eşleşiyorsa kalır; grup bağlamı korunur. */
|
||||||
|
const filterWizardTree = (
|
||||||
|
nodes: WizardTreeNode[],
|
||||||
|
matches: (f: WizardFileInfoDto) => boolean,
|
||||||
|
): WizardTreeNode[] =>
|
||||||
|
nodes.flatMap((node) => {
|
||||||
|
const children = filterWizardTree(node.children, matches)
|
||||||
|
return matches(node.file) || children.length > 0 ? [{ ...node, children }] : []
|
||||||
|
})
|
||||||
|
|
||||||
|
interface WizardTreeRow {
|
||||||
|
file: WizardFileInfoDto
|
||||||
|
depth: number
|
||||||
|
/** Kökten gelen yol; ortak alt form her üst ekranın altında ayrı açılıp kapanabilsin diye. */
|
||||||
|
key: string
|
||||||
|
hasChildren: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Kapalı grupların altı satıra dökülmez. */
|
||||||
|
const flattenWizardTree = (
|
||||||
|
nodes: WizardTreeNode[],
|
||||||
|
collapsed: Set<string>,
|
||||||
|
depth = 0,
|
||||||
|
prefix = '',
|
||||||
|
): WizardTreeRow[] =>
|
||||||
|
nodes.flatMap((node) => {
|
||||||
|
const key = `${prefix}${node.file.fileName}`
|
||||||
|
const row = { file: node.file, depth, key, hasChildren: node.children.length > 0 }
|
||||||
|
return collapsed.has(key)
|
||||||
|
? [row]
|
||||||
|
: [row, ...flattenWizardTree(node.children, collapsed, depth + 1, `${key}/`)]
|
||||||
|
})
|
||||||
|
|
||||||
const WizardFileManager = () => {
|
const WizardFileManager = () => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { translate } = useLocalization()
|
const { translate } = useLocalization()
|
||||||
|
|
@ -91,15 +166,42 @@ const WizardFileManager = () => {
|
||||||
const canExport = checkPermission(WIZARD_PERMISSION.EXPORT)
|
const canExport = checkPermission(WIZARD_PERMISSION.EXPORT)
|
||||||
const canImport = checkPermission(WIZARD_PERMISSION.IMPORT)
|
const canImport = checkPermission(WIZARD_PERMISSION.IMPORT)
|
||||||
|
|
||||||
const filteredFiles = useMemo(() => {
|
const menuPathText = useCallback(
|
||||||
|
(f: WizardFileInfoDto) => (f.menuPath ?? []).map((key) => translate('::' + key)).join(' › '),
|
||||||
|
[translate],
|
||||||
|
)
|
||||||
|
|
||||||
|
const tree = useMemo(() => {
|
||||||
|
const nodes = buildWizardTree(files)
|
||||||
const q = search.trim().toLowerCase()
|
const q = search.trim().toLowerCase()
|
||||||
if (!q) return files
|
if (!q) return nodes
|
||||||
return files.filter(
|
return filterWizardTree(
|
||||||
|
nodes,
|
||||||
(f) =>
|
(f) =>
|
||||||
(f.wizardName ?? '').toLowerCase().includes(q) ||
|
(f.wizardName ?? '').toLowerCase().includes(q) ||
|
||||||
(f.listFormCode ?? '').toLowerCase().includes(q),
|
(f.listFormCode ?? '').toLowerCase().includes(q) ||
|
||||||
|
(f.customComponentName ?? '').toLowerCase().includes(q) ||
|
||||||
|
menuPathText(f).toLowerCase().includes(q),
|
||||||
)
|
)
|
||||||
}, [files, search])
|
}, [files, search, menuPathText])
|
||||||
|
|
||||||
|
const [collapsed, setCollapsed] = useState<Set<string>>(new Set())
|
||||||
|
// Arama sırasında eşleşme kapalı bir grubun içinde kalmasın diye gruplar açık gösterilir.
|
||||||
|
const effectiveCollapsed = useMemo(
|
||||||
|
() => (search.trim() ? new Set<string>() : collapsed),
|
||||||
|
[collapsed, search],
|
||||||
|
)
|
||||||
|
const toggleGroup = (key: string) =>
|
||||||
|
setCollapsed((prev) => {
|
||||||
|
const next = new Set(prev)
|
||||||
|
if (!next.delete(key)) next.add(key)
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
|
||||||
|
const tableRows = useMemo(
|
||||||
|
() => flattenWizardTree(tree, effectiveCollapsed),
|
||||||
|
[tree, effectiveCollapsed],
|
||||||
|
)
|
||||||
|
|
||||||
const loadFiles = useCallback(async () => {
|
const loadFiles = useCallback(async () => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
|
|
@ -281,6 +383,67 @@ const WizardFileManager = () => {
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const renderKindBadge = (f: WizardFileInfoDto) => {
|
||||||
|
const isCustom = f.componentKind === WizardComponentKindEnum.Custom
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={classNames(
|
||||||
|
'inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium whitespace-nowrap',
|
||||||
|
isCustom
|
||||||
|
? 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-300'
|
||||||
|
: 'bg-indigo-100 text-indigo-700 dark:bg-indigo-900/30 dark:text-indigo-300',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{isCustom ? <FaCube /> : <FaListUl />}
|
||||||
|
{translate(
|
||||||
|
isCustom ? '::App.WizardStepComponent.CustomComponent' : '::App.Listforms.Listform',
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderMenuPath = (f: WizardFileInfoDto, className?: string) => {
|
||||||
|
const text = menuPathText(f)
|
||||||
|
if (!text) return null
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={classNames('inline-flex min-w-0 max-w-full items-center gap-1', className)}
|
||||||
|
title={`${translate('::App.Platform.MenuParent')}: ${text}`}
|
||||||
|
>
|
||||||
|
<FaSitemap className="shrink-0 text-gray-400" />
|
||||||
|
<span className="truncate">{text}</span>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const componentName = (f: WizardFileInfoDto) =>
|
||||||
|
f.componentKind === WizardComponentKindEnum.Custom ? f.customComponentName : f.listFormCode
|
||||||
|
|
||||||
|
/** Alt satırlardaki bağlantı işareti; derinlik kadar içeri itilir. */
|
||||||
|
const renderDepthMarker = (depth: number) =>
|
||||||
|
depth > 0 && (
|
||||||
|
<FaLevelUpAlt
|
||||||
|
className="shrink-0 rotate-90 text-gray-400"
|
||||||
|
style={{ marginLeft: (depth - 1) * 16 }}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
/** Alt ekranı olan satırın aç/kapa düğmesi; olmayanlarda hizayı koruyan boşluk. */
|
||||||
|
const renderGroupToggle = (row: Pick<WizardTreeRow, 'key' | 'hasChildren'>) => {
|
||||||
|
if (!row.hasChildren) return <span className="w-3.5 shrink-0" />
|
||||||
|
const isCollapsed = effectiveCollapsed.has(row.key)
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="shrink-0 text-gray-400 hover:text-indigo-500"
|
||||||
|
title={translate(isCollapsed ? '::App.Platform.ExpandAll' : '::App.Platform.CollapseAll')}
|
||||||
|
onClick={() => toggleGroup(row.key)}
|
||||||
|
>
|
||||||
|
{isCollapsed ? <FaPlusSquare /> : <FaMinusSquare />}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<PageTitle title={translate('::App.Listforms.WizardManager')} />
|
<PageTitle title={translate('::App.Listforms.WizardManager')} />
|
||||||
|
|
@ -384,7 +547,7 @@ const WizardFileManager = () => {
|
||||||
loading={loading}
|
loading={loading}
|
||||||
className={classNames('mt-4', loading && 'min-h-[240px]')}
|
className={classNames('mt-4', loading && 'min-h-[240px]')}
|
||||||
>
|
>
|
||||||
{filteredFiles.length === 0 && !loading && (
|
{tree.length === 0 && !loading && (
|
||||||
<div className="text-center py-12">
|
<div className="text-center py-12">
|
||||||
<FaCalendarAlt size={48} className="mx-auto text-gray-400 mb-4" />
|
<FaCalendarAlt size={48} className="mx-auto text-gray-400 mb-4" />
|
||||||
<p className="text-sm text-gray-400 text-center py-4">
|
<p className="text-sm text-gray-400 text-center py-4">
|
||||||
|
|
@ -395,9 +558,15 @@ const WizardFileManager = () => {
|
||||||
|
|
||||||
{viewMode === 'card' ? (
|
{viewMode === 'card' ? (
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3">
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3">
|
||||||
{filteredFiles.map((f) => {
|
{tree.map((node) => {
|
||||||
|
const f = node.file
|
||||||
const Icon = navigationIcon[f.menuIcon] ?? FaDatabase
|
const Icon = navigationIcon[f.menuIcon] ?? FaDatabase
|
||||||
const isCustom = f.componentKind === WizardComponentKindEnum.Custom
|
const isCustom = f.componentKind === WizardComponentKindEnum.Custom
|
||||||
|
const prefix = `${f.fileName}/`
|
||||||
|
const dependencyCount = flattenWizardTree(node.children, new Set(), 1, prefix).length
|
||||||
|
const children = effectiveCollapsed.has(f.fileName)
|
||||||
|
? []
|
||||||
|
: flattenWizardTree(node.children, effectiveCollapsed, 1, prefix)
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={f.fileName}
|
key={f.fileName}
|
||||||
|
|
@ -419,28 +588,15 @@ const WizardFileManager = () => {
|
||||||
{f.fileName}
|
{f.fileName}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 3. satır: yol ayrımı (Custom / List), layout ve menü adresi */}
|
{/* 3. satır: yol ayrımı (Custom / List) ve bileşen kodu */}
|
||||||
<div className="flex flex-wrap pt-1 items-center gap-1.5 text-xs">
|
<div className="flex flex-wrap pt-1 items-center gap-1.5 text-xs">
|
||||||
<span
|
{renderKindBadge(f)}
|
||||||
className={classNames(
|
<div className="text-xs text-gray-400 truncate">{componentName(f)}</div>
|
||||||
'inline-flex items-center gap-1 rounded-full px-2 py-0.5 font-medium',
|
|
||||||
isCustom
|
|
||||||
? 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-300'
|
|
||||||
: 'bg-indigo-100 text-indigo-700 dark:bg-indigo-900/30 dark:text-indigo-300',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{isCustom ? <FaCube /> : <FaListUl />}
|
|
||||||
{translate(
|
|
||||||
isCustom
|
|
||||||
? '::App.WizardStepComponent.CustomComponent'
|
|
||||||
: '::App.Listforms.Listform',
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
<div className="text-xs text-gray-400 truncate">
|
|
||||||
{isCustom ? f.customComponentName : f.listFormCode}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* 4. satır: ekranın bulunduğu menü */}
|
||||||
|
{renderMenuPath(f, 'pt-1 text-xs text-gray-600 dark:text-gray-300')}
|
||||||
|
|
||||||
<div className="flex flex-wrap pt-1 items-center gap-1.5 text-xs">
|
<div className="flex flex-wrap pt-1 items-center gap-1.5 text-xs">
|
||||||
{/* Custom yolunda layout kavramı yoktur. */}
|
{/* Custom yolunda layout kavramı yoktur. */}
|
||||||
{!isCustom && f.defaultLayout && (
|
{!isCustom && f.defaultLayout && (
|
||||||
|
|
@ -466,6 +622,40 @@ const WizardFileManager = () => {
|
||||||
f,
|
f,
|
||||||
'flex items-center justify-end gap-1 pt-1 border-t border-gray-200 dark:border-gray-700',
|
'flex items-center justify-end gap-1 pt-1 border-t border-gray-200 dark:border-gray-700',
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Bağlı wizard'lar (alt formlar / component bağımlılıkları) aynı kartta kalır. */}
|
||||||
|
{dependencyCount > 0 && (
|
||||||
|
<div className="space-y-1 border-t border-gray-200 pt-2 dark:border-gray-700">
|
||||||
|
<div className="flex items-center gap-1.5 text-xs font-medium text-gray-500">
|
||||||
|
{renderGroupToggle({ key: f.fileName, hasChildren: true })}
|
||||||
|
{translate('::App.Platform.Dependencies')} ({dependencyCount})
|
||||||
|
</div>
|
||||||
|
{children.map((row) => {
|
||||||
|
const { file: child, depth, key } = row
|
||||||
|
const ChildIcon = navigationIcon[child.menuIcon] ?? FaDatabase
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={key}
|
||||||
|
className="flex items-center gap-1.5 rounded border border-gray-200 bg-white px-2 py-1 dark:border-gray-700 dark:bg-gray-900"
|
||||||
|
>
|
||||||
|
{renderDepthMarker(depth)}
|
||||||
|
{renderGroupToggle(row)}
|
||||||
|
<ChildIcon className="shrink-0 text-indigo-400" />
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<div
|
||||||
|
className="truncate text-xs font-medium text-gray-800 dark:text-gray-200"
|
||||||
|
title={child.wizardName || child.fileName}
|
||||||
|
>
|
||||||
|
{child.wizardName || child.fileName}
|
||||||
|
</div>
|
||||||
|
{renderMenuPath(child, 'text-[11px] text-gray-500')}
|
||||||
|
</div>
|
||||||
|
{renderActions(child, 'flex shrink-0 items-center gap-0.5')}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|
@ -487,6 +677,9 @@ const WizardFileManager = () => {
|
||||||
<th className="text-left font-medium px-4 py-2 whitespace-nowrap">
|
<th className="text-left font-medium px-4 py-2 whitespace-nowrap">
|
||||||
{translate('::App.WizardStepComponent.ComponentType')}
|
{translate('::App.WizardStepComponent.ComponentType')}
|
||||||
</th>
|
</th>
|
||||||
|
<th className="text-left font-medium px-4 py-2 whitespace-nowrap">
|
||||||
|
{translate('::App.Platform.MenuParent')}
|
||||||
|
</th>
|
||||||
<th className="text-left font-medium px-4 py-2">
|
<th className="text-left font-medium px-4 py-2">
|
||||||
{translate('::App.WizardStepComponent.MenuUrl')}
|
{translate('::App.WizardStepComponent.MenuUrl')}
|
||||||
</th>
|
</th>
|
||||||
|
|
@ -496,16 +689,24 @@ const WizardFileManager = () => {
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{filteredFiles.map((f) => {
|
{tableRows.map((row) => {
|
||||||
|
const { file: f, depth, key } = row
|
||||||
const Icon = navigationIcon[f.menuIcon] ?? FaDatabase
|
const Icon = navigationIcon[f.menuIcon] ?? FaDatabase
|
||||||
const isCustom = f.componentKind === WizardComponentKindEnum.Custom
|
|
||||||
return (
|
return (
|
||||||
<tr
|
<tr
|
||||||
key={f.fileName}
|
key={key}
|
||||||
className="border-t border-gray-100 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-800"
|
className={classNames(
|
||||||
|
'hover:bg-gray-50 dark:hover:bg-gray-800',
|
||||||
|
// Grup başı çizgiyle ayrılır; alt satırlar üst ekranına yapışık kalır.
|
||||||
|
depth === 0
|
||||||
|
? 'border-t border-gray-200 dark:border-gray-700'
|
||||||
|
: 'bg-gray-50/50 dark:bg-gray-800/40',
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<td className="px-4 py-2">
|
<td className="px-4 py-2">
|
||||||
<div className="flex items-center gap-2 min-w-0">
|
<div className="flex items-center gap-2 min-w-0">
|
||||||
|
{renderDepthMarker(depth)}
|
||||||
|
{renderGroupToggle(row)}
|
||||||
<Icon className="text-indigo-400 shrink-0 text-lg" />
|
<Icon className="text-indigo-400 shrink-0 text-lg" />
|
||||||
<span
|
<span
|
||||||
className="font-medium text-gray-800 dark:text-gray-200 truncate"
|
className="font-medium text-gray-800 dark:text-gray-200 truncate"
|
||||||
|
|
@ -522,24 +723,11 @@ const WizardFileManager = () => {
|
||||||
{f.fileName}
|
{f.fileName}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-2 text-gray-600 dark:text-gray-300">
|
<td className="px-4 py-2 text-gray-600 dark:text-gray-300">
|
||||||
{isCustom ? f.customComponentName : f.listFormCode}
|
{componentName(f)}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-2 whitespace-nowrap">
|
<td className="px-4 py-2 whitespace-nowrap">{renderKindBadge(f)}</td>
|
||||||
<span
|
<td className="px-4 py-2 text-gray-600 dark:text-gray-300 whitespace-nowrap">
|
||||||
className={classNames(
|
{renderMenuPath(f)}
|
||||||
'inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium',
|
|
||||||
isCustom
|
|
||||||
? 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-300'
|
|
||||||
: 'bg-indigo-100 text-indigo-700 dark:bg-indigo-900/30 dark:text-indigo-300',
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{isCustom ? <FaCube /> : <FaListUl />}
|
|
||||||
{translate(
|
|
||||||
isCustom
|
|
||||||
? '::App.WizardStepComponent.CustomComponent'
|
|
||||||
: '::App.Listforms.Listform',
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</td>
|
</td>
|
||||||
<td
|
<td
|
||||||
className="px-4 py-2 font-mono text-gray-600 dark:text-gray-300"
|
className="px-4 py-2 font-mono text-gray-600 dark:text-gray-300"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue