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
|
||||
`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`)
|
||||
|
||||
"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>
|
||||
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))
|
||||
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))
|
||||
{
|
||||
var fileName = Path.GetFileName(file);
|
||||
|
|
@ -600,6 +606,9 @@ public class ListFormWizardAppService(
|
|||
{
|
||||
var json = await File.ReadAllTextAsync(file);
|
||||
var seed = WizardSeedFileDto.FromSeedJson(json);
|
||||
if (seed?.Wizard != null)
|
||||
seeds[fileName] = seed;
|
||||
|
||||
result.Add(new WizardFileInfoDto
|
||||
{
|
||||
FileName = fileName,
|
||||
|
|
@ -611,6 +620,7 @@ public class ListFormWizardAppService(
|
|||
CustomComponentName = seed?.Wizard?.CustomComponentName ?? string.Empty,
|
||||
MenuUrl = seed?.Wizard?.MenuUrl ?? string.Empty,
|
||||
CreatedAt = seed?.Wizard?.SeededAt.ToString("s") ?? string.Empty,
|
||||
MenuPath = seed?.Wizard != null ? ResolveMenuPath(seed.Wizard, menus) : [],
|
||||
HasInsertedRecords = seed?.InsertedRecords != null &&
|
||||
(seed.InsertedRecords.LanguageKeys.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.
|
||||
return [.. result
|
||||
.OrderByDescending(x => x.CreatedAt, StringComparer.Ordinal)
|
||||
.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)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
/// <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()
|
||||
{
|
||||
var seedFile = await ReadCustomComponentFileAsync();
|
||||
|
|
|
|||
|
|
@ -135,6 +135,10 @@ export interface WizardFileInfoDto {
|
|||
componentKind: WizardComponentKindEnum
|
||||
customComponentName: 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 {
|
||||
|
|
|
|||
|
|
@ -22,6 +22,10 @@ import {
|
|||
FaFileImport,
|
||||
FaTh,
|
||||
FaList,
|
||||
FaSitemap,
|
||||
FaLevelUpAlt,
|
||||
FaPlusSquare,
|
||||
FaMinusSquare,
|
||||
} from 'react-icons/fa'
|
||||
import DbMigrateButton from '@/components/shared/DbMigrateButton'
|
||||
import {
|
||||
|
|
@ -55,6 +59,77 @@ interface ConfirmState {
|
|||
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 navigate = useNavigate()
|
||||
const { translate } = useLocalization()
|
||||
|
|
@ -91,15 +166,42 @@ const WizardFileManager = () => {
|
|||
const canExport = checkPermission(WIZARD_PERMISSION.EXPORT)
|
||||
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()
|
||||
if (!q) return files
|
||||
return files.filter(
|
||||
if (!q) return nodes
|
||||
return filterWizardTree(
|
||||
nodes,
|
||||
(f) =>
|
||||
(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 () => {
|
||||
setLoading(true)
|
||||
|
|
@ -281,6 +383,67 @@ const WizardFileManager = () => {
|
|||
</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 (
|
||||
<Container>
|
||||
<PageTitle title={translate('::App.Listforms.WizardManager')} />
|
||||
|
|
@ -384,7 +547,7 @@ const WizardFileManager = () => {
|
|||
loading={loading}
|
||||
className={classNames('mt-4', loading && 'min-h-[240px]')}
|
||||
>
|
||||
{filteredFiles.length === 0 && !loading && (
|
||||
{tree.length === 0 && !loading && (
|
||||
<div className="text-center py-12">
|
||||
<FaCalendarAlt size={48} className="mx-auto text-gray-400 mb-4" />
|
||||
<p className="text-sm text-gray-400 text-center py-4">
|
||||
|
|
@ -395,9 +558,15 @@ const WizardFileManager = () => {
|
|||
|
||||
{viewMode === 'card' ? (
|
||||
<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 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 (
|
||||
<div
|
||||
key={f.fileName}
|
||||
|
|
@ -419,28 +588,15 @@ const WizardFileManager = () => {
|
|||
{f.fileName}
|
||||
</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">
|
||||
<span
|
||||
className={classNames(
|
||||
'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>
|
||||
{renderKindBadge(f)}
|
||||
<div className="text-xs text-gray-400 truncate">{componentName(f)}</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">
|
||||
{/* Custom yolunda layout kavramı yoktur. */}
|
||||
{!isCustom && f.defaultLayout && (
|
||||
|
|
@ -466,6 +622,40 @@ const WizardFileManager = () => {
|
|||
f,
|
||||
'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>
|
||||
)
|
||||
})}
|
||||
|
|
@ -487,6 +677,9 @@ const WizardFileManager = () => {
|
|||
<th className="text-left font-medium px-4 py-2 whitespace-nowrap">
|
||||
{translate('::App.WizardStepComponent.ComponentType')}
|
||||
</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">
|
||||
{translate('::App.WizardStepComponent.MenuUrl')}
|
||||
</th>
|
||||
|
|
@ -496,16 +689,24 @@ const WizardFileManager = () => {
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{filteredFiles.map((f) => {
|
||||
{tableRows.map((row) => {
|
||||
const { file: f, depth, key } = row
|
||||
const Icon = navigationIcon[f.menuIcon] ?? FaDatabase
|
||||
const isCustom = f.componentKind === WizardComponentKindEnum.Custom
|
||||
return (
|
||||
<tr
|
||||
key={f.fileName}
|
||||
className="border-t border-gray-100 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-800"
|
||||
key={key}
|
||||
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">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
{renderDepthMarker(depth)}
|
||||
{renderGroupToggle(row)}
|
||||
<Icon className="text-indigo-400 shrink-0 text-lg" />
|
||||
<span
|
||||
className="font-medium text-gray-800 dark:text-gray-200 truncate"
|
||||
|
|
@ -522,24 +723,11 @@ const WizardFileManager = () => {
|
|||
{f.fileName}
|
||||
</td>
|
||||
<td className="px-4 py-2 text-gray-600 dark:text-gray-300">
|
||||
{isCustom ? f.customComponentName : f.listFormCode}
|
||||
{componentName(f)}
|
||||
</td>
|
||||
<td className="px-4 py-2 whitespace-nowrap">
|
||||
<span
|
||||
className={classNames(
|
||||
'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 className="px-4 py-2 whitespace-nowrap">{renderKindBadge(f)}</td>
|
||||
<td className="px-4 py-2 text-gray-600 dark:text-gray-300 whitespace-nowrap">
|
||||
{renderMenuPath(f)}
|
||||
</td>
|
||||
<td
|
||||
className="px-4 py-2 font-mono text-gray-600 dark:text-gray-300"
|
||||
|
|
|
|||
Loading…
Reference in a new issue