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')}}
|
);
}
|