finished file tree folder selection in file create node
This commit is contained in:
parent
47b3c1ab23
commit
0941b9e0ad
4 changed files with 266 additions and 55 deletions
|
|
@ -1,9 +1,12 @@
|
|||
/**
|
||||
* userFileFolder — same folder tree as Meine Dateien (FormGeneratorTree) inside a collapsible panel.
|
||||
* userFileFolder — FormGeneratorTree embedded: combobox-style trigger + expandable tree.
|
||||
*/
|
||||
|
||||
import React, { useMemo, useCallback, useState } from 'react';
|
||||
import React, { useMemo, useCallback, useState, useEffect } from 'react';
|
||||
import { FaFolderPlus } from 'react-icons/fa';
|
||||
import { useLanguage } from '../../../../providers/language/LanguageContext';
|
||||
import { usePrompt } from '../../../../hooks/usePrompt';
|
||||
import { getFolderTree, createFolder } from '../../../../api/fileApi';
|
||||
import { FormGeneratorTree } from '../../../FormGenerator/FormGeneratorTree';
|
||||
import { createFolderFileProvider } from '../../../FormGenerator/FormGeneratorTree/providers/FolderFileProvider';
|
||||
import type { TreeNode } from '../../../FormGenerator/FormGeneratorTree';
|
||||
|
|
@ -11,22 +14,82 @@ import type { FieldRendererProps } from './index';
|
|||
|
||||
export const UserFileFolderPicker: React.FC<FieldRendererProps> = ({ param, value, onChange, request }) => {
|
||||
const { t } = useLanguage();
|
||||
const [panelOpen, setPanelOpen] = useState(true);
|
||||
const { prompt, PromptDialog } = usePrompt();
|
||||
const [panelOpen, setPanelOpen] = useState(false);
|
||||
/** Remount embedded tree after create/rename elsewhere */
|
||||
const [treeRefreshKey, setTreeRefreshKey] = useState(0);
|
||||
const [creating, setCreating] = useState(false);
|
||||
/** Display name for saved folderId (resolved from API when graph loads). */
|
||||
const [pickedName, setPickedName] = useState<string | null>(null);
|
||||
|
||||
const provider = useMemo(() => createFolderFileProvider({ includeFiles: false }), []);
|
||||
|
||||
const strVal = typeof value === 'string' ? value : '';
|
||||
const rootSelected = strVal === '';
|
||||
|
||||
useEffect(() => {
|
||||
if (!strVal) {
|
||||
setPickedName(null);
|
||||
return;
|
||||
}
|
||||
if (!request) return;
|
||||
let cancelled = false;
|
||||
getFolderTree(request, 'me')
|
||||
.then((folders) => {
|
||||
if (cancelled) return;
|
||||
const f = folders.find((x) => x.id === strVal);
|
||||
setPickedName(f?.name ?? null);
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) setPickedName(null);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [strVal, request]);
|
||||
|
||||
const handleNodeClick = useCallback(
|
||||
(node: TreeNode) => {
|
||||
if (node.type === 'folder') {
|
||||
setPickedName(node.name);
|
||||
onChange(node.id);
|
||||
setPanelOpen(false);
|
||||
}
|
||||
},
|
||||
[onChange],
|
||||
);
|
||||
|
||||
const clearFolder = useCallback(() => {
|
||||
onChange('');
|
||||
setPickedName(null);
|
||||
}, [onChange]);
|
||||
|
||||
const triggerLabel = strVal ? (pickedName ?? '…') : t('Wähle einen Zielordner');
|
||||
|
||||
const handleCreateFolder = useCallback(async () => {
|
||||
if (!request || creating) return;
|
||||
const parentHint = strVal && pickedName ? ` („${pickedName}“)` : strVal ? '' : ' (Stamm)';
|
||||
const entered = await prompt(`Ordnername${parentHint}:`, {
|
||||
title: 'Neuer Ordner',
|
||||
placeholder: 'Ordnername',
|
||||
confirmLabel: t('Anlegen'),
|
||||
});
|
||||
const trimmed = entered?.trim();
|
||||
if (!trimmed) return;
|
||||
setCreating(true);
|
||||
try {
|
||||
const parentId = strVal || null;
|
||||
const folder = await createFolder(request, trimmed, parentId);
|
||||
setPickedName(folder.name);
|
||||
onChange(folder.id);
|
||||
setTreeRefreshKey((k) => k + 1);
|
||||
} catch {
|
||||
// stay silent in minimal UI; devtools / global handler may log
|
||||
} finally {
|
||||
setCreating(false);
|
||||
}
|
||||
}, [request, creating, strVal, pickedName, prompt, onChange, t]);
|
||||
|
||||
return (
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<label style={{ display: 'block', fontSize: 12, marginBottom: 2 }}>{param.description || param.name}</label>
|
||||
|
|
@ -35,32 +98,71 @@ export const UserFileFolderPicker: React.FC<FieldRendererProps> = ({ param, valu
|
|||
)}
|
||||
{request && (
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setPanelOpen((o) => !o)}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
width: '100%',
|
||||
padding: '6px 10px',
|
||||
marginBottom: panelOpen ? 6 : 0,
|
||||
alignItems: 'stretch',
|
||||
borderRadius: 6,
|
||||
border: '1px solid var(--color-border, #cbd5e1)',
|
||||
background: 'var(--table-header-bg, #f1f5f9)',
|
||||
cursor: 'pointer',
|
||||
fontSize: 12,
|
||||
textAlign: 'left',
|
||||
color: 'var(--color-text, #334155)',
|
||||
overflow: 'hidden',
|
||||
marginBottom: panelOpen ? 6 : 0,
|
||||
}}
|
||||
>
|
||||
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
{panelOpen ? t('Ordnerbaum ausblenden') : t('Ordnerbaum einblenden')}
|
||||
</span>
|
||||
<span aria-hidden style={{ marginLeft: 8, flexShrink: 0 }}>
|
||||
{panelOpen ? '▾' : '▸'}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setPanelOpen((o) => !o)}
|
||||
style={{
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: 8,
|
||||
minWidth: 0,
|
||||
padding: '8px 10px',
|
||||
border: 'none',
|
||||
background: 'transparent',
|
||||
cursor: 'pointer',
|
||||
fontSize: 12,
|
||||
textAlign: 'left',
|
||||
color: 'var(--color-text, #334155)',
|
||||
}}
|
||||
>
|
||||
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', flex: 1 }}>
|
||||
{triggerLabel}
|
||||
</span>
|
||||
<span aria-hidden style={{ flexShrink: 0, fontSize: 10, opacity: 0.65 }}>
|
||||
{panelOpen ? '▾' : '▸'}
|
||||
</span>
|
||||
</button>
|
||||
{strVal ? (
|
||||
<button
|
||||
type="button"
|
||||
title={t('Zielordner entfernen (Stamm — Meine Dateien)')}
|
||||
aria-label={t('Zielordner entfernen')}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
clearFolder();
|
||||
}}
|
||||
style={{
|
||||
flexShrink: 0,
|
||||
width: 36,
|
||||
border: 'none',
|
||||
borderLeft: '1px solid var(--color-border, #cbd5e1)',
|
||||
background: 'transparent',
|
||||
cursor: 'pointer',
|
||||
fontSize: 16,
|
||||
lineHeight: 1,
|
||||
color: 'var(--color-text-secondary, #64748b)',
|
||||
padding: 0,
|
||||
}}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{panelOpen && (
|
||||
<div
|
||||
|
|
@ -72,42 +174,92 @@ export const UserFileFolderPicker: React.FC<FieldRendererProps> = ({ param, valu
|
|||
}}
|
||||
>
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => onChange('')}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
onChange('');
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
padding: '8px 12px',
|
||||
fontSize: 12,
|
||||
fontWeight: 600,
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'stretch',
|
||||
borderBottom: '1px solid var(--color-border, #e2e8f0)',
|
||||
background: rootSelected
|
||||
? 'rgba(37, 99, 235, 0.12)'
|
||||
: 'var(--table-header-bg, #f8fafc)',
|
||||
background: 'var(--table-header-bg, #f8fafc)',
|
||||
}}
|
||||
>
|
||||
{t('Stamm — Meine Dateien')}
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => {
|
||||
clearFolder();
|
||||
setPanelOpen(false);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
clearFolder();
|
||||
setPanelOpen(false);
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
flex: 1,
|
||||
padding: '8px 12px',
|
||||
fontSize: 12,
|
||||
fontWeight: 600,
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
minHeight: 36,
|
||||
background: rootSelected ? 'rgba(37, 99, 235, 0.12)' : 'transparent',
|
||||
}}
|
||||
>
|
||||
{t('Stamm — Meine Dateien')}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t('Neuen Ordner erstellen')}
|
||||
title={
|
||||
creating
|
||||
? t('Wird angelegt…')
|
||||
: strVal
|
||||
? `Unterordner von: ${pickedName ?? '…'}`
|
||||
: 'Unter dem Stamm (oberste Ebene)'
|
||||
}
|
||||
disabled={creating}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
void handleCreateFolder();
|
||||
}}
|
||||
style={{
|
||||
flexShrink: 0,
|
||||
width: 40,
|
||||
minHeight: 36,
|
||||
alignSelf: 'stretch',
|
||||
border: 'none',
|
||||
borderLeft: '1px solid var(--color-border, #e2e8f0)',
|
||||
background: 'transparent',
|
||||
cursor: creating ? 'not-allowed' : 'pointer',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: 'var(--primary-color, #2563eb)',
|
||||
opacity: creating ? 0.5 : 1,
|
||||
}}
|
||||
>
|
||||
<FaFolderPlus size={14} aria-hidden />
|
||||
</button>
|
||||
</div>
|
||||
<FormGeneratorTree
|
||||
key={`user-folder-tree-${treeRefreshKey}`}
|
||||
provider={provider}
|
||||
ownership="own"
|
||||
title={t('Ordner')}
|
||||
compact
|
||||
allowCreateFolder
|
||||
allowCreateFolder={false}
|
||||
showFilter={false}
|
||||
emptyMessage={t('Noch keine Ordner')}
|
||||
onNodeClick={handleNodeClick}
|
||||
embedMaxHeight={260}
|
||||
embedMaxHeight={240}
|
||||
hideRowActionButtons
|
||||
hideSectionHeader
|
||||
enableDragDrop
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<PromptDialog />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ function _buildChildMap<T>(nodes: TreeNode<T>[]): Map<string | '__root__', TreeN
|
|||
function _flatten<T>(
|
||||
nodes: TreeNode<T>[],
|
||||
expandedIds: Set<string>,
|
||||
confirmedEmptyFolderIds: Set<string>,
|
||||
): FlatEntry<T>[] {
|
||||
const childMap = _buildChildMap(nodes);
|
||||
const result: FlatEntry<T>[] = [];
|
||||
|
|
@ -70,8 +71,22 @@ function _flatten<T>(
|
|||
const children = childMap.get(parentKey);
|
||||
if (!children) return;
|
||||
for (const node of children) {
|
||||
const nodeChildren = childMap.get(node.id);
|
||||
const hasChildren = (nodeChildren && nodeChildren.length > 0) || node.type === 'folder';
|
||||
const loadedChildren = childMap.get(node.id) ?? [];
|
||||
const hasLoadedKids = loadedChildren.length > 0;
|
||||
let hasChildren = false;
|
||||
|
||||
if (node.type !== 'folder') {
|
||||
hasChildren = hasLoadedKids;
|
||||
} else if (hasLoadedKids) {
|
||||
hasChildren = true;
|
||||
} else if (confirmedEmptyFolderIds.has(node.id)) {
|
||||
hasChildren = false;
|
||||
} else if (node.hasSubfoldersInApiTree === false && node.mayHaveLazyFileChildren === false) {
|
||||
hasChildren = false;
|
||||
} else {
|
||||
hasChildren = true;
|
||||
}
|
||||
|
||||
result.push({ node, depth, hasChildren });
|
||||
if (hasChildren && expandedIds.has(node.id)) {
|
||||
_walk(node.id, depth + 1);
|
||||
|
|
@ -134,6 +149,7 @@ interface TreeNodeRowProps<T = any> {
|
|||
onDragLeave: (e: React.DragEvent) => void;
|
||||
onDrop: (e: React.DragEvent, node: TreeNode<T>) => void;
|
||||
hideRowActionButtons?: boolean;
|
||||
dragDropEnabled?: boolean;
|
||||
}
|
||||
|
||||
const TreeNodeRow = React.memo(function TreeNodeRow<T>({
|
||||
|
|
@ -163,6 +179,7 @@ const TreeNodeRow = React.memo(function TreeNodeRow<T>({
|
|||
onDragLeave,
|
||||
onDrop,
|
||||
hideRowActionButtons = false,
|
||||
dragDropEnabled = true,
|
||||
}: TreeNodeRowProps<T>) {
|
||||
const { node, depth, hasChildren } = entry;
|
||||
const renameRef = useRef<HTMLInputElement>(null);
|
||||
|
|
@ -243,11 +260,11 @@ const TreeNodeRow = React.memo(function TreeNodeRow<T>({
|
|||
className={rowClasses}
|
||||
onClick={_handleRowClick}
|
||||
onDoubleClick={_handleDoubleClick}
|
||||
draggable={!hideRowActionButtons}
|
||||
onDragStart={hideRowActionButtons ? undefined : (e) => onDragStart(e, node)}
|
||||
onDragOver={hideRowActionButtons ? undefined : (e) => onDragOver(e, node)}
|
||||
onDragLeave={hideRowActionButtons ? undefined : onDragLeave}
|
||||
onDrop={hideRowActionButtons ? undefined : (e) => onDrop(e, node)}
|
||||
draggable={dragDropEnabled}
|
||||
onDragStart={dragDropEnabled ? (e) => onDragStart(e, node) : undefined}
|
||||
onDragOver={dragDropEnabled ? (e) => onDragOver(e, node) : undefined}
|
||||
onDragLeave={dragDropEnabled ? onDragLeave : undefined}
|
||||
onDrop={dragDropEnabled ? (e) => onDrop(e, node) : undefined}
|
||||
data-node-id={node.id}
|
||||
title={node.name}
|
||||
role="treeitem"
|
||||
|
|
@ -425,6 +442,8 @@ export function FormGeneratorTree<T = any>({
|
|||
className,
|
||||
embedMaxHeight,
|
||||
hideRowActionButtons = false,
|
||||
hideSectionHeader = false,
|
||||
enableDragDrop,
|
||||
}: FormGeneratorTreeProps<T>) {
|
||||
const { prompt, PromptDialog } = usePrompt();
|
||||
const [nodes, setNodes] = useState<TreeNode<T>[]>([]);
|
||||
|
|
@ -437,12 +456,15 @@ export function FormGeneratorTree<T = any>({
|
|||
const [dragOverId, setDragOverId] = useState<string | null>(null);
|
||||
const [draggingIds, setDraggingIds] = useState<Set<string>>(new Set());
|
||||
const [filterText, setFilterText] = useState('');
|
||||
/** Folders we expanded and confirmed have no visible children → hide chevron like a real leaf */
|
||||
const [confirmedEmptyFolderIds, setConfirmedEmptyFolderIds] = useState(() => new Set<string>());
|
||||
const lastSelectedIdRef = useRef<string | null>(null);
|
||||
const treeContentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const _loadRoot = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
setConfirmedEmptyFolderIds(new Set());
|
||||
const rootNodes = await provider.loadChildren(null, ownership);
|
||||
setNodes(rootNodes);
|
||||
if (defaultCollapsed && rootNodes.length === 0) {
|
||||
|
|
@ -457,7 +479,10 @@ export function FormGeneratorTree<T = any>({
|
|||
_loadRoot();
|
||||
}, [_loadRoot]);
|
||||
|
||||
const flatEntriesRaw = useMemo(() => _flatten(nodes, expandedIds), [nodes, expandedIds]);
|
||||
const flatEntriesRaw = useMemo(
|
||||
() => _flatten(nodes, expandedIds, confirmedEmptyFolderIds),
|
||||
[nodes, expandedIds, confirmedEmptyFolderIds],
|
||||
);
|
||||
|
||||
const flatEntries = useMemo(() => {
|
||||
const term = filterText.trim().toLowerCase();
|
||||
|
|
@ -506,6 +531,13 @@ export function FormGeneratorTree<T = any>({
|
|||
const childNodes = await provider.loadChildren(id, ownership);
|
||||
if (childNodes.length > 0) {
|
||||
setNodes((prev) => [...prev, ...childNodes]);
|
||||
setConfirmedEmptyFolderIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.delete(id);
|
||||
return next;
|
||||
});
|
||||
} else if (node.type === 'folder') {
|
||||
setConfirmedEmptyFolderIds((prev) => new Set(prev).add(id));
|
||||
}
|
||||
}
|
||||
setTimeout(() => {
|
||||
|
|
@ -623,6 +655,11 @@ export function FormGeneratorTree<T = any>({
|
|||
const newNode = await provider.createChild(parentId, trimmed);
|
||||
setNodes((prev) => [...prev, newNode]);
|
||||
if (parentId) {
|
||||
setConfirmedEmptyFolderIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.delete(parentId);
|
||||
return next;
|
||||
});
|
||||
setExpandedIds((prev) => new Set(prev).add(parentId));
|
||||
}
|
||||
} catch {
|
||||
|
|
@ -851,6 +888,8 @@ export function FormGeneratorTree<T = any>({
|
|||
);
|
||||
}, [provider, ownership]);
|
||||
|
||||
const dragDropEnabled = enableDragDrop ?? !hideRowActionButtons;
|
||||
|
||||
const _filteredIdsForAction = useCallback(
|
||||
(action: TreeBatchAction): string[] => {
|
||||
const ids = [...selectedIds];
|
||||
|
|
@ -890,7 +929,7 @@ export function FormGeneratorTree<T = any>({
|
|||
: undefined
|
||||
}
|
||||
>
|
||||
{title && (
|
||||
{title && !hideSectionHeader && (
|
||||
<div
|
||||
className={`${styles.sectionHeader} ${collapsible ? '' : styles.sectionHeaderNonCollapsible}`}
|
||||
onClick={collapsible ? () => setSectionCollapsed((v) => !v) : undefined}
|
||||
|
|
@ -1030,6 +1069,7 @@ export function FormGeneratorTree<T = any>({
|
|||
onDragLeave={_handleDragLeave}
|
||||
onDrop={_handleDrop}
|
||||
hideRowActionButtons={hideRowActionButtons}
|
||||
dragDropEnabled={dragDropEnabled}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,9 @@ interface FileData {
|
|||
sysCreatedBy?: string;
|
||||
}
|
||||
|
||||
function _mapFolderToNode(folder: FolderData, ownership: Ownership): TreeNode {
|
||||
function _mapFolderToNode(folder: FolderData, ownership: Ownership, allFolders: FolderData[], includeFilesInTree: boolean): TreeNode {
|
||||
const hasSubfoldersInApiTree = allFolders.some((f) => (f.parentId ?? null) === folder.id);
|
||||
const mayHaveLazyFileChildren = includeFilesInTree && !hasSubfoldersInApiTree;
|
||||
return {
|
||||
id: folder.id,
|
||||
name: folder.name,
|
||||
|
|
@ -34,6 +36,8 @@ function _mapFolderToNode(folder: FolderData, ownership: Ownership): TreeNode {
|
|||
neutralize: folder.neutralize,
|
||||
contextOrphan: folder.contextOrphan,
|
||||
icon: <FaFolder />,
|
||||
hasSubfoldersInApiTree,
|
||||
mayHaveLazyFileChildren,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -77,7 +81,7 @@ export function createFolderFileProvider(options: { includeFiles?: boolean } = {
|
|||
const foldersRes = await api.get('/api/files/folders/tree', { params: { owner } });
|
||||
const allFolders: FolderData[] = foldersRes.data ?? [];
|
||||
const childFolders = allFolders.filter((f) => (f.parentId ?? null) === parentId);
|
||||
nodes.push(...childFolders.map((f) => _mapFolderToNode(f, ownership)));
|
||||
nodes.push(...childFolders.map((f) => _mapFolderToNode(f, ownership, allFolders, includeFiles)));
|
||||
|
||||
if (includeFiles) {
|
||||
try {
|
||||
|
|
@ -140,7 +144,7 @@ export function createFolderFileProvider(options: { includeFiles?: boolean } = {
|
|||
|
||||
async createChild(parentId, name) {
|
||||
const res = await api.post('/api/files/folders', { name, parentId });
|
||||
const node = _mapFolderToNode(res.data, 'own');
|
||||
const node = _mapFolderToNode(res.data, 'own', [], includeFiles);
|
||||
typeMap.set(node.id, 'folder');
|
||||
return node;
|
||||
},
|
||||
|
|
@ -164,7 +168,7 @@ export function createFolderFileProvider(options: { includeFiles?: boolean } = {
|
|||
await Promise.all(
|
||||
ids.map((id) => {
|
||||
if (_isFile(id)) return api.put(`/api/files/${id}`, { folderId: targetParentId });
|
||||
return api.post(`/api/files/folders/${id}/move`, { targetParentId });
|
||||
return api.post(`/api/files/folders/${id}/move`, { parentId: targetParentId });
|
||||
}),
|
||||
);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -16,6 +16,16 @@ export interface TreeNode<T = any> {
|
|||
isLoading?: boolean;
|
||||
sizeBytes?: number;
|
||||
data?: T;
|
||||
/**
|
||||
* From bulk `/folders/tree` response: another folder references this folder as parent.
|
||||
* When false AND no lazy-file mode, omit expand affordance immediately.
|
||||
*/
|
||||
hasSubfoldersInApiTree?: boolean;
|
||||
/**
|
||||
* Folder tree mixes in files lazily (`includeFiles` in FolderFileProvider). When true but
|
||||
* no subfolders in API snapshot, expand may still reveal files → keep chevron until loaded.
|
||||
*/
|
||||
mayHaveLazyFileChildren?: boolean;
|
||||
}
|
||||
|
||||
export interface TreeBatchAction {
|
||||
|
|
@ -66,7 +76,12 @@ export interface FormGeneratorTreeProps<T = any> {
|
|||
/** Embedded pickers (e.g. automation node config): constrain overall height so the tree scrolls inside. */
|
||||
embedMaxHeight?: number;
|
||||
/**
|
||||
* Hides checkbox, size column, per-row emoji actions, drag-drop, and batch toolbar — saves space in pickers.
|
||||
* Hides checkbox, size column, per-row emoji actions, and batch toolbar — saves space in pickers.
|
||||
* Drag-drop defaults off when hidden; pass `enableDragDrop` to keep moving folders inside the mini tree.
|
||||
*/
|
||||
hideRowActionButtons?: boolean;
|
||||
/** When true, folders remain draggable despite `hideRowActionButtons`. */
|
||||
enableDragDrop?: boolean;
|
||||
/** Hides the titled section header (count, refresh, new folder) — for compact embedded pickers. */
|
||||
hideSectionHeader?: boolean;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue