import React, { useEffect, useRef } from 'react'; import ReactDOM from 'react-dom'; import { useLanguage } from '../../../providers/language/LanguageContext'; import { useConfirm } from '../../../hooks/useConfirm'; import styles from './GroupRow.module.css'; import fgTableCss from '../FormGeneratorTable/FormGeneratorTable.module.css'; import type { TableGroupNode } from '../FormGeneratorTable/FormGeneratorTable'; import { FaFolder, FaFolderOpen, FaList, FaPen, FaPlus } from 'react-icons/fa'; // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- export interface GroupBulkAction { icon?: React.ReactNode; title?: string; variant?: 'default' | 'danger'; onClick: () => void; disabled?: boolean; } /** Horizontal shift per nesting level — keep in sync with item rows (`FormGeneratorTable`). */ export const GROUP_TREE_INDENT_STEP_PX = 20; // --------------------------------------------------------------------------- // GroupFolderRow // --------------------------------------------------------------------------- /** Folder row: optional select column, then one merged cell for folder UI (spans actions + data cols — no blank actions column). */ export interface GroupFolderTableCells { showSelect: boolean; /** `` for folder strip = `detectedColumns.length` + (1 if table has an actions column). */ dataColumnsCount: number; selectClassName: string; selectTdStyle?: React.CSSProperties; } interface GroupFolderRowProps { node: TableGroupNode; depth: number; /** Checkbox for “whole subtree”: select / clear all selectable visible items under this folder. */ subtreeSelect?: { checked: boolean; indeterminate: boolean; disabled: boolean; onToggle: () => void; }; /** When set, use split `` layout; omit single-cell colspan. */ tableCells?: GroupFolderTableCells; /** Legacy single spanning cell — only used when `tableCells` is omitted. */ colSpan?: number; visibleCount: number; isExpanded: boolean; isEditing: boolean; /** True while an ITEM is dragged over this row (drop item into group). */ isDragOver: boolean; /** True while a GROUP is dragged over this row (nest group inside). */ isDragOverFromGroup: boolean; bulkActions?: GroupBulkAction[]; onToggle: () => void; onEditCommit: (name: string) => void; onEditCancel: () => void; onRename: () => void; onAddSub: () => void; // Item drag-drop onItemDragOver: (e: React.DragEvent) => void; onItemDrop: (e: React.DragEvent) => void; onItemDragLeave: () => void; // Group drag (this row is draggable) onGroupDragStart: (e: React.DragEvent) => void; onGroupDragEnd: () => void; onGroupDrag?: (e: React.DragEvent) => void; /** True while this group is being dragged leftward to pop out one level */ isDraggingOut?: boolean; /** Hide this row via display:none (keeps it in DOM so drag operations don't break) */ hidden?: boolean; // Group drop (another group dropped onto this) onGroupDragOver: (e: React.DragEvent) => void; onGroupDrop: (e: React.DragEvent) => void; onGroupDragLeave: () => void; } export function GroupFolderRow({ node, depth, subtreeSelect, tableCells, colSpan, visibleCount, isExpanded, isEditing, isDragOver, isDragOverFromGroup, isDraggingOut, hidden, bulkActions = [], onToggle, onEditCommit, onEditCancel, onRename, onAddSub, onItemDragOver, onItemDrop, onItemDragLeave, onGroupDragStart, onGroupDragEnd, onGroupDrag, onGroupDragOver, onGroupDrop, onGroupDragLeave, }: GroupFolderRowProps) { const { t } = useLanguage(); const { ConfirmDialog } = useConfirm(); const inputRef = useRef(null); const subtreeCbRef = useRef(null); const totalCount = node.itemIds.length; useEffect(() => { const el = subtreeCbRef.current; if (!el || !subtreeSelect) return; el.indeterminate = subtreeSelect.indeterminate; }, [subtreeSelect?.indeterminate, subtreeSelect?.checked, subtreeSelect]); useEffect(() => { if (isEditing && inputRef.current) { inputRef.current.focus(); inputRef.current.select(); } }, [isEditing]); const indentPx = depth * GROUP_TREE_INDENT_STEP_PX; const _rowClass = [ styles.groupFolderRow, tableCells ? fgTableCss.treeRowIndented : '', isDragOver ? styles.dragOver : '', isDragOverFromGroup ? styles.dragOverGroup : '', isDraggingOut ? styles.draggingOut : '', subtreeSelect?.checked && !subtreeSelect?.disabled ? styles.folderRowSubtreeFull : '', subtreeSelect?.indeterminate && !subtreeSelect?.checked ? styles.folderRowSubtreePartial : '', ].filter(Boolean).join(' '); const mergedColSpan = tableCells ? tableCells.dataColumnsCount : (colSpan ?? 1); const folderStripStyle = ({ '--group-indent': `${indentPx}px`, ...(tableCells ? { ['--row-tree-indent' as string]: `${depth * GROUP_TREE_INDENT_STEP_PX}px` } : {}), }) as React.CSSProperties; const guardDragDecor = ( e: React.DragEvent, relay: React.DragEventHandler | undefined, ) => { const el = e.target as HTMLElement; if (el.closest('input, button, textarea, label')) { e.preventDefault(); e.stopPropagation(); return; } relay?.(e); }; const folderCells = ( <> {typeof document !== 'undefined' && ReactDOM.createPortal(, document.body)} guardDragDecor(e, onGroupDragStart)} onDrag={(e) => guardDragDecor(e, onGroupDrag)} onDragEnd={(e) => guardDragDecor(e, onGroupDragEnd)} // item drag-over onDragOver={(e) => { // distinguish item vs group drag via dataTransfer type if (e.dataTransfer.types.includes('application/porta-group')) { onGroupDragOver(e); } else { onItemDragOver(e); } }} onDrop={(e) => { if (e.dataTransfer.types.includes('application/porta-group')) { onGroupDrop(e); } else { onItemDrop(e); } }} onDragLeave={() => { onItemDragLeave(); onGroupDragLeave(); }} onDragEnter={(e) => e.preventDefault()} > {tableCells?.showSelect && ( {subtreeSelect && ( { e.stopPropagation(); subtreeSelect.onToggle(); }} onClick={(e) => e.stopPropagation()} title={node.name ? t('Auswahl unter „{name}“', { name: node.name }) : t('Auswahl dieser Gruppe')} aria-label={node.name ? t('Alle sichtbaren Einträge in „{name}“ auswählen', { name: node.name }) : t('Alle sichtbaren Einträge in dieser Gruppe auswählen')} /> )} )}
{/* Indent */} {indentPx > 0 && } {/* Chevron */} {/* Folder icon */} {isExpanded ? : } {/* Name / inline input */} {isEditing ? ( { if (e.key === 'Enter') onEditCommit(e.currentTarget.value); if (e.key === 'Escape') onEditCancel(); }} onBlur={(e) => onEditCommit(e.target.value)} /> ) : ( { e.stopPropagation(); onToggle(); }}> {node.name || {t('(Unbenannt)')}} )} {/* Item count badge */} {!isEditing && ( {visibleCount < totalCount && totalCount > 0 ? `${visibleCount} / ${totalCount}` : String(totalCount)} )} {/* Drop hint */} {(isDragOver || isDragOverFromGroup) && ( {isDragOverFromGroup ? t('Als Untergruppe ablegen') : t('Hierher ziehen')} )} {/* ── Bulk actions (delete all, custom batch) right after badge ── */} {!isEditing && bulkActions.length > 0 && ( <> {bulkActions.map((action, i) => ( ))} )} {/* ── Group management: rename / add-subgroup ── */} {!isEditing && ( )}
); return folderCells; } // --------------------------------------------------------------------------- // BreadcrumbRow // --------------------------------------------------------------------------- interface BreadcrumbRowProps { groupName: string; totalItems: number; colSpan: number; onBack: () => void; } export function BreadcrumbRow({ groupName, totalItems, colSpan, onBack }: BreadcrumbRowProps) { const { t } = useLanguage(); return (
{groupName} {totalItems > 0 && ( ({totalItems} {t('Einträge')}) )}
); } // --------------------------------------------------------------------------- // UngroupedRow — also a drop zone for removing items/groups from groups // --------------------------------------------------------------------------- interface UngroupedRowProps { count: number; colSpan: number; isDragOver?: boolean; onDragOver?: (e: React.DragEvent) => void; onDrop?: (e: React.DragEvent) => void; onDragLeave?: () => void; } export function UngroupedRow({ count, colSpan, isDragOver, onDragOver, onDrop, onDragLeave }: UngroupedRowProps) { const { t } = useLanguage(); return ( e.preventDefault()} > {t('Nicht zugeordnet')} {count} {isDragOver && {t('Aus Gruppe entfernen')}} ); }