frontend_nyla/src/components/FormGenerator/GroupingManager/GroupRow.tsx
2026-04-29 18:32:50 +02:00

293 lines
9.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 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;
}
// ---------------------------------------------------------------------------
// GroupFolderRow
// ---------------------------------------------------------------------------
interface GroupFolderRowProps {
node: TableGroupNode;
depth: number;
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,
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<HTMLInputElement>(null);
const totalCount = node.itemIds.length;
useEffect(() => {
if (isEditing && inputRef.current) {
inputRef.current.focus();
inputRef.current.select();
}
}, [isEditing]);
const indentPx = depth * 20;
const _rowClass = [
styles.groupFolderRow,
isDragOver ? styles.dragOver : '',
isDragOverFromGroup ? styles.dragOverGroup : '',
isDraggingOut ? styles.draggingOut : '',
].filter(Boolean).join(' ');
return (
<>
{typeof document !== 'undefined' && ReactDOM.createPortal(<ConfirmDialog />, document.body)}
<tr
className={_rowClass}
style={{ '--group-indent': `${indentPx}px`, display: hidden ? 'none' : undefined } as React.CSSProperties}
draggable={!isEditing}
onDragStart={onGroupDragStart}
onDrag={onGroupDrag}
onDragEnd={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()}
>
<td colSpan={colSpan} className={styles.folderCell}>
<div className={styles.folderInner}>
{/* Indent */}
{indentPx > 0 && <span className={styles.indent} style={{ width: indentPx }} />}
{/* Chevron */}
<button
className={`${styles.chevronBtn} ${isExpanded ? styles.chevronOpen : ''}`}
onClick={(e) => { e.stopPropagation(); onToggle(); }}
title={isExpanded ? t('Zuklappen') : t('Aufklappen')}
tabIndex={-1}
>
<span className={styles.chevronArrow} />
</button>
{/* Folder icon */}
<span className={styles.folderIcon}>
{isExpanded ? <FaFolderOpen /> : <FaFolder />}
</span>
{/* Name / inline input */}
{isEditing ? (
<input
ref={inputRef}
defaultValue={node.name}
className={styles.nameInput}
placeholder={t('Gruppenname…')}
onKeyDown={(e) => {
if (e.key === 'Enter') onEditCommit(e.currentTarget.value);
if (e.key === 'Escape') onEditCancel();
}}
onBlur={(e) => onEditCommit(e.target.value)}
/>
) : (
<span className={styles.groupName} onClick={(e) => { e.stopPropagation(); onToggle(); }}>
{node.name || <em className={styles.unnamed}>{t('(Unbenannt)')}</em>}
</span>
)}
{/* Item count badge */}
{!isEditing && (
<span className={styles.badge}>
{visibleCount < totalCount && totalCount > 0
? `${visibleCount} / ${totalCount}`
: String(totalCount)}
</span>
)}
{/* Drop hint */}
{(isDragOver || isDragOverFromGroup) && (
<span className={styles.dropHint}>
{isDragOverFromGroup ? t('Als Untergruppe ablegen') : t('Hierher ziehen')}
</span>
)}
{/* ── Bulk actions (delete all, custom batch) right after badge ── */}
{!isEditing && bulkActions.length > 0 && (
<>
<span className={styles.separator} />
<span className={styles.actions}>
{bulkActions.map((action, i) => (
<button
key={i}
className={`${styles.actionBtn} ${action.variant === 'danger' ? styles.actionBtnDanger : ''}`}
title={action.title}
disabled={!!action.disabled}
onClick={(e) => { e.stopPropagation(); if (!action.disabled) action.onClick(); }}
>
{action.icon}
</button>
))}
</span>
</>
)}
{/* ── Group management: rename / add-subgroup ── */}
{!isEditing && (
<span className={styles.mgmtActions}>
<button onClick={(e) => { e.stopPropagation(); onRename(); }} title={t('Umbenennen')} className={styles.mgmtBtn}><FaPen /></button>
<button onClick={(e) => { e.stopPropagation(); onAddSub(); }} title={t('Untergruppe erstellen')} className={styles.mgmtBtn}><FaPlus /></button>
</span>
)}
<span style={{ flex: 1 }} />
</div>
</td>
</tr>
</>
);
}
// ---------------------------------------------------------------------------
// BreadcrumbRow
// ---------------------------------------------------------------------------
interface BreadcrumbRowProps {
groupName: string;
totalItems: number;
colSpan: number;
onBack: () => void;
}
export function BreadcrumbRow({ groupName, totalItems, colSpan, onBack }: BreadcrumbRowProps) {
const { t } = useLanguage();
return (
<tr className={styles.breadcrumbRow}>
<td colSpan={colSpan} className={styles.breadcrumbCell}>
<div className={styles.breadcrumbInner}>
<button className={styles.backButton} onClick={onBack}>
{t('Alle anzeigen')}
</button>
<span className={styles.breadcrumbSep}></span>
<span className={styles.breadcrumbCurrent}>{groupName}</span>
{totalItems > 0 && (
<span style={{ color: 'var(--color-text-secondary, #94a3b8)', fontSize: '11px' }}>
({totalItems} {t('Einträge')})
</span>
)}
</div>
</td>
</tr>
);
}
// ---------------------------------------------------------------------------
// 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 (
<tr
className={`${styles.ungroupedRow} ${isDragOver ? styles.ungroupedDragOver : ''}`}
onDragOver={onDragOver}
onDrop={onDrop}
onDragLeave={onDragLeave}
onDragEnter={(e) => e.preventDefault()}
>
<td colSpan={colSpan} className={styles.ungroupedCell}>
<span className={styles.folderIcon}><FaList /></span>
{t('Nicht zugeordnet')}
<span className={styles.badge}>{count}</span>
{isDragOver && <span className={styles.dropHint}>{t('Aus Gruppe entfernen')}</span>}
</td>
</tr>
);
}