UI Verbesserungen Gruppierung und Anwendung auf alle Seiten
This commit is contained in:
parent
aff9dcb7bd
commit
e7a79a3484
8 changed files with 241 additions and 24 deletions
|
|
@ -486,6 +486,56 @@
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Items that live inside a group — subtle tint + left connector */
|
||||||
|
.tr.groupedItem {
|
||||||
|
border-left: 3px solid color-mix(in srgb, var(--color-primary, #4a6fa5) 35%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tr.groupedItem:hover {
|
||||||
|
background: color-mix(in srgb, var(--color-primary, #4a6fa5) 8%, var(--color-bg, #fff));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hierarchy: set `--row-tree-indent` on the <tr> (px). Same row shifts checkbox, actions, and every `.td`.
|
||||||
|
* Folder rows attach this class from GroupRow.tsx; omit padding on `.folderCell` (inner strip uses `--group-indent`).
|
||||||
|
*/
|
||||||
|
.treeRowIndented {
|
||||||
|
--row-tree-indent: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.treeRowIndented > .selectColumn {
|
||||||
|
box-sizing: border-box !important;
|
||||||
|
padding-top: 4px !important;
|
||||||
|
padding-right: 4px !important;
|
||||||
|
padding-bottom: 4px !important;
|
||||||
|
padding-left: calc(4px + var(--row-tree-indent)) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.treeRowIndented > .actionsColumn {
|
||||||
|
box-sizing: border-box !important;
|
||||||
|
padding-top: 4px !important;
|
||||||
|
padding-right: 4px !important;
|
||||||
|
padding-bottom: 4px !important;
|
||||||
|
padding-left: calc(4px + var(--row-tree-indent)) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.treeRowIndented > .td {
|
||||||
|
box-sizing: border-box !important;
|
||||||
|
padding-top: 8px !important;
|
||||||
|
padding-right: 12px !important;
|
||||||
|
padding-bottom: 8px !important;
|
||||||
|
padding-left: calc(12px + var(--row-tree-indent)) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.treeRowIndented > .folderCell:first-child {
|
||||||
|
box-sizing: border-box !important;
|
||||||
|
padding-left: calc(12px + var(--row-tree-indent)) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.treeRowIndented > .selectColumn + .folderCell {
|
||||||
|
padding: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* Selection Column */
|
/* Selection Column */
|
||||||
.selectColumn {
|
.selectColumn {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
|
||||||
|
|
@ -76,11 +76,15 @@ import {
|
||||||
} from '../../../utils/attributeTypeMapper';
|
} from '../../../utils/attributeTypeMapper';
|
||||||
import type { AttributeType } from '../../../utils/attributeTypeMapper';
|
import type { AttributeType } from '../../../utils/attributeTypeMapper';
|
||||||
import { FaFilter, FaTrash } from 'react-icons/fa';
|
import { FaFilter, FaTrash } from 'react-icons/fa';
|
||||||
import type { GroupBulkAction } from '../GroupingManager/GroupRow';
|
|
||||||
import api from '../../../api';
|
import api from '../../../api';
|
||||||
import { PeriodPicker } from '../../PeriodPicker';
|
import { PeriodPicker } from '../../PeriodPicker';
|
||||||
import type { PeriodValue } from '../../PeriodPicker';
|
import type { PeriodValue } from '../../PeriodPicker';
|
||||||
import { GroupFolderRow, BreadcrumbRow } from '../GroupingManager/GroupRow';
|
import {
|
||||||
|
GroupFolderRow,
|
||||||
|
BreadcrumbRow,
|
||||||
|
GROUP_TREE_INDENT_STEP_PX,
|
||||||
|
type GroupBulkAction,
|
||||||
|
} from '../GroupingManager/GroupRow';
|
||||||
|
|
||||||
/** A filter value can be a plain string, null (for empty/missing), or a
|
/** A filter value can be a plain string, null (for empty/missing), or a
|
||||||
* {value, label} object returned by FK-aware filter-values endpoints. */
|
* {value, label} object returned by FK-aware filter-values endpoints. */
|
||||||
|
|
@ -3058,17 +3062,23 @@ export function FormGeneratorTable<T extends Record<string, any>>({
|
||||||
const rowId = _getRowId(row);
|
const rowId = _getRowId(row);
|
||||||
const isDragging = draggedRowId === rowId;
|
const isDragging = draggedRowId === rowId;
|
||||||
const willUngroup = isDragging && dragWillUngroup;
|
const willUngroup = isDragging && dragWillUngroup;
|
||||||
const indentStyle: React.CSSProperties = indentLevel > 0
|
const isGrouped = indentLevel > 0;
|
||||||
? { borderLeft: `3px solid color-mix(in srgb, var(--color-primary, #4a6fa5) ${Math.min(indentLevel * 25, 60)}%, transparent)` }
|
const rowBgStyle: React.CSSProperties = isGrouped
|
||||||
|
? { background: 'color-mix(in srgb, var(--color-primary, #4a6fa5) 4%, transparent)' }
|
||||||
: {};
|
: {};
|
||||||
const ungroupStyle: React.CSSProperties = willUngroup
|
const ungroupStyle: React.CSSProperties = willUngroup
|
||||||
? { borderLeft: '3px solid #d69e2e', opacity: 0.5 }
|
? { outline: '2px solid #d69e2e', opacity: 0.5 }
|
||||||
: {};
|
: {};
|
||||||
|
/** Visual depth inside group tree (= folder depth + 1 for direct children — see `_renderGroup`). */
|
||||||
|
const leadingIndentPx = isGrouped ? indentLevel * GROUP_TREE_INDENT_STEP_PX : 0;
|
||||||
|
const treeIndentCss: React.CSSProperties | undefined =
|
||||||
|
isGrouped ? { ['--row-tree-indent' as string]: `${leadingIndentPx}px` } : undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr
|
<tr
|
||||||
key={rowId || index}
|
key={rowId || index}
|
||||||
className={`${styles.tr} ${selectedIds.has(rowId) ? styles.selected : ''} ${onRowClick ? styles.clickable : ''}`}
|
className={`${styles.tr} ${selectedIds.has(rowId) ? styles.selected : ''} ${onRowClick ? styles.clickable : ''} ${isGrouped ? styles.groupedItem : ''} ${isGrouped ? styles.treeRowIndented : ''}`}
|
||||||
style={{ ...indentStyle, ...ungroupStyle, display: hiddenByDrag ? 'none' : undefined }}
|
style={{ ...rowBgStyle, ...ungroupStyle, ...(treeIndentCss ?? {}), display: hiddenByDrag ? 'none' : undefined }}
|
||||||
onClick={() => onRowClick?.(row, index)}
|
onClick={() => onRowClick?.(row, index)}
|
||||||
draggable={groupingEnabled || rowDraggable}
|
draggable={groupingEnabled || rowDraggable}
|
||||||
onDragStart={(e) => {
|
onDragStart={(e) => {
|
||||||
|
|
@ -3111,7 +3121,10 @@ export function FormGeneratorTable<T extends Record<string, any>>({
|
||||||
{...Object.fromEntries(Object.entries(dataAttributes).map(([k, v]) => [`data-${k}`, v]))}
|
{...Object.fromEntries(Object.entries(dataAttributes).map(([k, v]) => [`data-${k}`, v]))}
|
||||||
>
|
>
|
||||||
{selectable && (
|
{selectable && (
|
||||||
<td className={styles.selectColumn} style={{ width: '40px', minWidth: '40px', maxWidth: '40px' }}>
|
<td
|
||||||
|
className={styles.selectColumn}
|
||||||
|
style={{ width: '40px', minWidth: '40px', maxWidth: '40px' }}
|
||||||
|
>
|
||||||
<input type="checkbox"
|
<input type="checkbox"
|
||||||
checked={selectedIds.has(_getRowId(row))}
|
checked={selectedIds.has(_getRowId(row))}
|
||||||
onChange={() => handleRowSelect(row)}
|
onChange={() => handleRowSelect(row)}
|
||||||
|
|
@ -3123,7 +3136,10 @@ export function FormGeneratorTable<T extends Record<string, any>>({
|
||||||
</td>
|
</td>
|
||||||
)}
|
)}
|
||||||
{hasActionColumn && (
|
{hasActionColumn && (
|
||||||
<td className={styles.actionsColumn} style={{ width: `${currentActionsWidth}px`, minWidth: `${defaultActionsWidth}px` }}>
|
<td
|
||||||
|
className={styles.actionsColumn}
|
||||||
|
style={{ width: `${currentActionsWidth}px`, minWidth: `${defaultActionsWidth}px` }}
|
||||||
|
>
|
||||||
<div ref={(el) => { if (el) actionButtonsRefs.current.set(index, el); else actionButtonsRefs.current.delete(index); }}
|
<div ref={(el) => { if (el) actionButtonsRefs.current.set(index, el); else actionButtonsRefs.current.delete(index); }}
|
||||||
className={`${styles.actionButtons} ${shouldWrapActionButtons ? styles.actionButtonsWrap : ''}`}>
|
className={`${styles.actionButtons} ${shouldWrapActionButtons ? styles.actionButtonsWrap : ''}`}>
|
||||||
{actionButtons.map((ab, ai) => {
|
{actionButtons.map((ab, ai) => {
|
||||||
|
|
@ -3155,13 +3171,21 @@ export function FormGeneratorTable<T extends Record<string, any>>({
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
)}
|
)}
|
||||||
{detectedColumns.map(col => {
|
{detectedColumns.map((col) => {
|
||||||
const cv = row[col.key];
|
const cv = row[col.key];
|
||||||
const cCls = col.cellClassName ? col.cellClassName(cv, row) : '';
|
const cCls = col.cellClassName ? col.cellClassName(cv, row) : '';
|
||||||
const aStyle = _columnAlignStyle(col);
|
const aStyle = _columnAlignStyle(col);
|
||||||
return (
|
return (
|
||||||
<td key={col.key} className={`${styles.td} ${cCls}`.trim()}
|
<td
|
||||||
style={{ width: columnWidths[col.key] || col.width || 150, minWidth: columnWidths[col.key] || col.width || 150, maxWidth: columnWidths[col.key] || col.width || 150, ...aStyle }}>
|
key={col.key}
|
||||||
|
className={`${styles.td} ${cCls}`.trim()}
|
||||||
|
style={{
|
||||||
|
width: columnWidths[col.key] || col.width || 150,
|
||||||
|
minWidth: columnWidths[col.key] || col.width || 150,
|
||||||
|
maxWidth: columnWidths[col.key] || col.width || 150,
|
||||||
|
...aStyle,
|
||||||
|
}}
|
||||||
|
>
|
||||||
{formatCellValue(cv, col, row)}
|
{formatCellValue(cv, col, row)}
|
||||||
</td>
|
</td>
|
||||||
);
|
);
|
||||||
|
|
@ -3181,9 +3205,25 @@ export function FormGeneratorTable<T extends Record<string, any>>({
|
||||||
const _visibleById = new Map<string, T>();
|
const _visibleById = new Map<string, T>();
|
||||||
displayData.forEach(row => _visibleById.set(_getRowId(row), row));
|
displayData.forEach(row => _visibleById.set(_getRowId(row), row));
|
||||||
|
|
||||||
|
const _collectSelectableSubtreeIds = (n: typeof groupTree[0]): string[] => {
|
||||||
|
const acc: string[] = [];
|
||||||
|
for (const id of n.itemIds) {
|
||||||
|
const r = _visibleById.get(id);
|
||||||
|
if (!r) continue;
|
||||||
|
if (isRowSelectable && !isRowSelectable(r)) continue;
|
||||||
|
acc.push(id);
|
||||||
|
}
|
||||||
|
for (const sg of n.subGroups) acc.push(..._collectSelectableSubtreeIds(sg));
|
||||||
|
return acc;
|
||||||
|
};
|
||||||
|
|
||||||
const _renderGroup = (node: typeof groupTree[0], depth: number, inheritedHidden = false): React.ReactNode => {
|
const _renderGroup = (node: typeof groupTree[0], depth: number, inheritedHidden = false): React.ReactNode => {
|
||||||
const visibleIds = node.itemIds.filter(id => _visibleById.has(id));
|
const visibleIds = node.itemIds.filter(id => _visibleById.has(id));
|
||||||
const groupItems = visibleIds.map(id => _visibleById.get(id)!);
|
const groupItems = visibleIds.map(id => _visibleById.get(id)!);
|
||||||
|
const subtreeSelectableIds = _collectSelectableSubtreeIds(node);
|
||||||
|
const subtreeSelectedCount = subtreeSelectableIds.filter(id => selectedIds.has(id)).length;
|
||||||
|
const subtreeAllSelected = subtreeSelectableIds.length > 0 && subtreeSelectedCount === subtreeSelectableIds.length;
|
||||||
|
const subtreePartial = subtreeSelectedCount > 0 && subtreeSelectedCount < subtreeSelectableIds.length;
|
||||||
const userCollapsed = expandedGroups.has(`collapsed-${node.id}`);
|
const userCollapsed = expandedGroups.has(`collapsed-${node.id}`);
|
||||||
|
|
||||||
// Visual expanded state (chevron icon, hover-expand override, drag-collapse override)
|
// Visual expanded state (chevron icon, hover-expand override, drag-collapse override)
|
||||||
|
|
@ -3241,7 +3281,24 @@ export function FormGeneratorTable<T extends Record<string, any>>({
|
||||||
<GroupFolderRow
|
<GroupFolderRow
|
||||||
node={node}
|
node={node}
|
||||||
depth={depth}
|
depth={depth}
|
||||||
colSpan={_totalColSpan}
|
tableCells={{
|
||||||
|
showSelect: selectable,
|
||||||
|
dataColumnsCount: detectedColumns.length + (hasActionColumn ? 1 : 0),
|
||||||
|
selectClassName: styles.selectColumn,
|
||||||
|
selectTdStyle: { width: '40px', minWidth: '40px', maxWidth: '40px' },
|
||||||
|
}}
|
||||||
|
subtreeSelect={selectable ? {
|
||||||
|
checked: subtreeAllSelected,
|
||||||
|
indeterminate: subtreePartial,
|
||||||
|
disabled: subtreeSelectableIds.length === 0,
|
||||||
|
onToggle: () => {
|
||||||
|
if (subtreeSelectableIds.length === 0) return;
|
||||||
|
const next = new Set(selectedIds);
|
||||||
|
if (subtreeAllSelected) subtreeSelectableIds.forEach(id => next.delete(id));
|
||||||
|
else subtreeSelectableIds.forEach(id => next.add(id));
|
||||||
|
_notifySelection(next);
|
||||||
|
},
|
||||||
|
} : undefined}
|
||||||
visibleCount={visibleIds.length}
|
visibleCount={visibleIds.length}
|
||||||
isExpanded={isExp}
|
isExpanded={isExp}
|
||||||
isEditing={editingGroupId === node.id}
|
isEditing={editingGroupId === node.id}
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,17 @@
|
||||||
border-left: 3px solid #d69e2e;
|
border-left: 3px solid #d69e2e;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Folder subtree selection (aligned with tbody .tr.selected) */
|
||||||
|
.groupFolderRow.folderRowSubtreeFull {
|
||||||
|
background: rgba(124, 109, 216, 0.08);
|
||||||
|
background: rgba(var(--color-secondary-rgb), 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.groupFolderRow.folderRowSubtreePartial {
|
||||||
|
background: rgba(124, 109, 216, 0.04);
|
||||||
|
background: rgba(var(--color-secondary-rgb), 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
.folderCell {
|
.folderCell {
|
||||||
padding: 0 !important;
|
padding: 0 !important;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import ReactDOM from 'react-dom';
|
||||||
import { useLanguage } from '../../../providers/language/LanguageContext';
|
import { useLanguage } from '../../../providers/language/LanguageContext';
|
||||||
import { useConfirm } from '../../../hooks/useConfirm';
|
import { useConfirm } from '../../../hooks/useConfirm';
|
||||||
import styles from './GroupRow.module.css';
|
import styles from './GroupRow.module.css';
|
||||||
|
import fgTableCss from '../FormGeneratorTable/FormGeneratorTable.module.css';
|
||||||
import type { TableGroupNode } from '../FormGeneratorTable/FormGeneratorTable';
|
import type { TableGroupNode } from '../FormGeneratorTable/FormGeneratorTable';
|
||||||
import { FaFolder, FaFolderOpen, FaList, FaPen, FaPlus } from 'react-icons/fa';
|
import { FaFolder, FaFolderOpen, FaList, FaPen, FaPlus } from 'react-icons/fa';
|
||||||
|
|
||||||
|
|
@ -18,14 +19,36 @@ export interface GroupBulkAction {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Horizontal shift per nesting level — keep in sync with item rows (`FormGeneratorTable`). */
|
||||||
|
export const GROUP_TREE_INDENT_STEP_PX = 20;
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// GroupFolderRow
|
// 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;
|
||||||
|
/** `<td colSpan>` for folder strip = `detectedColumns.length` + (1 if table has an actions column). */
|
||||||
|
dataColumnsCount: number;
|
||||||
|
selectClassName: string;
|
||||||
|
selectTdStyle?: React.CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
interface GroupFolderRowProps {
|
interface GroupFolderRowProps {
|
||||||
node: TableGroupNode;
|
node: TableGroupNode;
|
||||||
depth: number;
|
depth: number;
|
||||||
colSpan: 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 `<td>` layout; omit single-cell colspan. */
|
||||||
|
tableCells?: GroupFolderTableCells;
|
||||||
|
/** Legacy single spanning cell — only used when `tableCells` is omitted. */
|
||||||
|
colSpan?: number;
|
||||||
visibleCount: number;
|
visibleCount: number;
|
||||||
isExpanded: boolean;
|
isExpanded: boolean;
|
||||||
isEditing: boolean;
|
isEditing: boolean;
|
||||||
|
|
@ -60,6 +83,8 @@ interface GroupFolderRowProps {
|
||||||
export function GroupFolderRow({
|
export function GroupFolderRow({
|
||||||
node,
|
node,
|
||||||
depth,
|
depth,
|
||||||
|
subtreeSelect,
|
||||||
|
tableCells,
|
||||||
colSpan,
|
colSpan,
|
||||||
visibleCount,
|
visibleCount,
|
||||||
isExpanded,
|
isExpanded,
|
||||||
|
|
@ -87,8 +112,15 @@ export function GroupFolderRow({
|
||||||
const { t } = useLanguage();
|
const { t } = useLanguage();
|
||||||
const { ConfirmDialog } = useConfirm();
|
const { ConfirmDialog } = useConfirm();
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const subtreeCbRef = useRef<HTMLInputElement>(null);
|
||||||
const totalCount = node.itemIds.length;
|
const totalCount = node.itemIds.length;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const el = subtreeCbRef.current;
|
||||||
|
if (!el || !subtreeSelect) return;
|
||||||
|
el.indeterminate = subtreeSelect.indeterminate;
|
||||||
|
}, [subtreeSelect?.indeterminate, subtreeSelect?.checked, subtreeSelect]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isEditing && inputRef.current) {
|
if (isEditing && inputRef.current) {
|
||||||
inputRef.current.focus();
|
inputRef.current.focus();
|
||||||
|
|
@ -96,26 +128,55 @@ export function GroupFolderRow({
|
||||||
}
|
}
|
||||||
}, [isEditing]);
|
}, [isEditing]);
|
||||||
|
|
||||||
const indentPx = depth * 20;
|
const indentPx = depth * GROUP_TREE_INDENT_STEP_PX;
|
||||||
|
|
||||||
const _rowClass = [
|
const _rowClass = [
|
||||||
styles.groupFolderRow,
|
styles.groupFolderRow,
|
||||||
|
tableCells ? fgTableCss.treeRowIndented : '',
|
||||||
isDragOver ? styles.dragOver : '',
|
isDragOver ? styles.dragOver : '',
|
||||||
isDragOverFromGroup ? styles.dragOverGroup : '',
|
isDragOverFromGroup ? styles.dragOverGroup : '',
|
||||||
isDraggingOut ? styles.draggingOut : '',
|
isDraggingOut ? styles.draggingOut : '',
|
||||||
|
subtreeSelect?.checked && !subtreeSelect?.disabled ? styles.folderRowSubtreeFull : '',
|
||||||
|
subtreeSelect?.indeterminate && !subtreeSelect?.checked ? styles.folderRowSubtreePartial : '',
|
||||||
].filter(Boolean).join(' ');
|
].filter(Boolean).join(' ');
|
||||||
|
|
||||||
return (
|
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(<ConfirmDialog />, document.body)}
|
{typeof document !== 'undefined' && ReactDOM.createPortal(<ConfirmDialog />, document.body)}
|
||||||
|
|
||||||
<tr
|
<tr
|
||||||
className={_rowClass}
|
className={_rowClass}
|
||||||
style={{ '--group-indent': `${indentPx}px`, display: hidden ? 'none' : undefined } as React.CSSProperties}
|
style={{ ...folderStripStyle, display: hidden ? 'none' : undefined } as React.CSSProperties}
|
||||||
draggable={!isEditing}
|
draggable={!isEditing}
|
||||||
onDragStart={onGroupDragStart}
|
onDragStart={(e) => guardDragDecor(e, onGroupDragStart)}
|
||||||
onDrag={onGroupDrag}
|
onDrag={(e) => guardDragDecor(e, onGroupDrag)}
|
||||||
onDragEnd={onGroupDragEnd}
|
onDragEnd={(e) => guardDragDecor(e, onGroupDragEnd)}
|
||||||
// item drag-over
|
// item drag-over
|
||||||
onDragOver={(e) => {
|
onDragOver={(e) => {
|
||||||
// distinguish item vs group drag via dataTransfer type
|
// distinguish item vs group drag via dataTransfer type
|
||||||
|
|
@ -135,7 +196,23 @@ export function GroupFolderRow({
|
||||||
onDragLeave={() => { onItemDragLeave(); onGroupDragLeave(); }}
|
onDragLeave={() => { onItemDragLeave(); onGroupDragLeave(); }}
|
||||||
onDragEnter={(e) => e.preventDefault()}
|
onDragEnter={(e) => e.preventDefault()}
|
||||||
>
|
>
|
||||||
<td colSpan={colSpan} className={styles.folderCell}>
|
{tableCells?.showSelect && (
|
||||||
|
<td className={tableCells.selectClassName} style={tableCells.selectTdStyle}>
|
||||||
|
{subtreeSelect && (
|
||||||
|
<input
|
||||||
|
ref={subtreeCbRef}
|
||||||
|
type="checkbox"
|
||||||
|
checked={subtreeSelect.checked}
|
||||||
|
disabled={subtreeSelect.disabled}
|
||||||
|
onChange={(e) => { 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')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
<td colSpan={tableCells ? mergedColSpan : (colSpan ?? 1)} className={styles.folderCell}>
|
||||||
<div className={styles.folderInner}>
|
<div className={styles.folderInner}>
|
||||||
{/* Indent */}
|
{/* Indent */}
|
||||||
{indentPx > 0 && <span className={styles.indent} style={{ width: indentPx }} />}
|
{indentPx > 0 && <span className={styles.indent} style={{ width: indentPx }} />}
|
||||||
|
|
@ -143,6 +220,7 @@ export function GroupFolderRow({
|
||||||
{/* Chevron */}
|
{/* Chevron */}
|
||||||
<button
|
<button
|
||||||
className={`${styles.chevronBtn} ${isExpanded ? styles.chevronOpen : ''}`}
|
className={`${styles.chevronBtn} ${isExpanded ? styles.chevronOpen : ''}`}
|
||||||
|
type="button"
|
||||||
onClick={(e) => { e.stopPropagation(); onToggle(); }}
|
onClick={(e) => { e.stopPropagation(); onToggle(); }}
|
||||||
title={isExpanded ? t('Zuklappen') : t('Aufklappen')}
|
title={isExpanded ? t('Zuklappen') : t('Aufklappen')}
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
|
|
@ -198,6 +276,7 @@ export function GroupFolderRow({
|
||||||
{bulkActions.map((action, i) => (
|
{bulkActions.map((action, i) => (
|
||||||
<button
|
<button
|
||||||
key={i}
|
key={i}
|
||||||
|
type="button"
|
||||||
className={`${styles.actionBtn} ${action.variant === 'danger' ? styles.actionBtnDanger : ''}`}
|
className={`${styles.actionBtn} ${action.variant === 'danger' ? styles.actionBtnDanger : ''}`}
|
||||||
title={action.title}
|
title={action.title}
|
||||||
disabled={!!action.disabled}
|
disabled={!!action.disabled}
|
||||||
|
|
@ -213,8 +292,8 @@ export function GroupFolderRow({
|
||||||
{/* ── Group management: rename / add-subgroup ── */}
|
{/* ── Group management: rename / add-subgroup ── */}
|
||||||
{!isEditing && (
|
{!isEditing && (
|
||||||
<span className={styles.mgmtActions}>
|
<span className={styles.mgmtActions}>
|
||||||
<button onClick={(e) => { e.stopPropagation(); onRename(); }} title={t('Umbenennen')} className={styles.mgmtBtn}><FaPen /></button>
|
<button type="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>
|
<button type="button" onClick={(e) => { e.stopPropagation(); onAddSub(); }} title={t('Untergruppe erstellen')} className={styles.mgmtBtn}><FaPlus /></button>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -224,6 +303,8 @@ export function GroupFolderRow({
|
||||||
</tr>
|
</tr>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
return folderCells;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import {
|
||||||
deleteFiles as deleteFilesApi,
|
deleteFiles as deleteFilesApi,
|
||||||
type FolderInfo,
|
type FolderInfo,
|
||||||
} from '../api/fileApi';
|
} from '../api/fileApi';
|
||||||
|
import type { TableGroupNode } from '../api/connectionApi';
|
||||||
|
|
||||||
export interface FilePreviewResult {
|
export interface FilePreviewResult {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
|
|
@ -73,6 +74,7 @@ export function useUserFiles() {
|
||||||
totalItems: number;
|
totalItems: number;
|
||||||
totalPages: number;
|
totalPages: number;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
const [groupTree, setGroupTree] = useState<TableGroupNode[]>([]);
|
||||||
const { request, isLoading: loading, error } = useApiRequest<null, UserFile[]>();
|
const { request, isLoading: loading, error } = useApiRequest<null, UserFile[]>();
|
||||||
const { checkPermission } = usePermissions();
|
const { checkPermission } = usePermissions();
|
||||||
|
|
||||||
|
|
@ -172,6 +174,9 @@ export function useUserFiles() {
|
||||||
if (data.pagination) {
|
if (data.pagination) {
|
||||||
setPagination(data.pagination);
|
setPagination(data.pagination);
|
||||||
}
|
}
|
||||||
|
if (Array.isArray((data as any).groupTree)) {
|
||||||
|
setGroupTree((data as any).groupTree);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Handle non-paginated response (backward compatibility)
|
// Handle non-paginated response (backward compatibility)
|
||||||
console.log('📋 Processing non-paginated response:', {
|
console.log('📋 Processing non-paginated response:', {
|
||||||
|
|
@ -325,6 +330,7 @@ export function useUserFiles() {
|
||||||
attributes,
|
attributes,
|
||||||
permissions,
|
permissions,
|
||||||
pagination,
|
pagination,
|
||||||
|
groupTree,
|
||||||
fetchFileById,
|
fetchFileById,
|
||||||
generateEditFieldsFromAttributes,
|
generateEditFieldsFromAttributes,
|
||||||
ensureAttributesLoaded
|
ensureAttributesLoaded
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import {
|
||||||
type AttributeDefinition,
|
type AttributeDefinition,
|
||||||
type PaginationParams
|
type PaginationParams
|
||||||
} from '../api/promptApi';
|
} from '../api/promptApi';
|
||||||
|
import type { TableGroupNode } from '../api/connectionApi';
|
||||||
|
|
||||||
// Re-export types for backward compatibility
|
// Re-export types for backward compatibility
|
||||||
export type { Prompt, AttributeDefinition, PaginationParams };
|
export type { Prompt, AttributeDefinition, PaginationParams };
|
||||||
|
|
@ -34,6 +35,7 @@ export function usePrompts() {
|
||||||
totalItems: number;
|
totalItems: number;
|
||||||
totalPages: number;
|
totalPages: number;
|
||||||
} | null>(null);
|
} | null>(null);
|
||||||
|
const [groupTree, setGroupTree] = useState<TableGroupNode[]>([]);
|
||||||
const { request, isLoading: loading, error } = useApiRequest<null, Prompt[]>();
|
const { request, isLoading: loading, error } = useApiRequest<null, Prompt[]>();
|
||||||
const { checkPermission } = usePermissions();
|
const { checkPermission } = usePermissions();
|
||||||
|
|
||||||
|
|
@ -99,6 +101,9 @@ export function usePrompts() {
|
||||||
if (data.pagination) {
|
if (data.pagination) {
|
||||||
setPagination(data.pagination);
|
setPagination(data.pagination);
|
||||||
}
|
}
|
||||||
|
if (Array.isArray((data as any).groupTree)) {
|
||||||
|
setGroupTree((data as any).groupTree);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Handle non-paginated response (backward compatibility)
|
// Handle non-paginated response (backward compatibility)
|
||||||
const items = Array.isArray(data) ? data : [];
|
const items = Array.isArray(data) ? data : [];
|
||||||
|
|
@ -454,10 +459,11 @@ export function usePrompts() {
|
||||||
attributes,
|
attributes,
|
||||||
permissions,
|
permissions,
|
||||||
pagination,
|
pagination,
|
||||||
|
groupTree,
|
||||||
fetchPromptById,
|
fetchPromptById,
|
||||||
generateEditFieldsFromAttributes,
|
generateEditFieldsFromAttributes,
|
||||||
generateCreateFieldsFromAttributes,
|
generateCreateFieldsFromAttributes,
|
||||||
ensureAttributesLoaded // Generic function to ensure attributes are loaded
|
ensureAttributesLoaded
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,7 @@ export const FilesPage: React.FC = () => {
|
||||||
error,
|
error,
|
||||||
refetch: tableRefetch,
|
refetch: tableRefetch,
|
||||||
pagination,
|
pagination,
|
||||||
|
groupTree,
|
||||||
fetchFileById,
|
fetchFileById,
|
||||||
updateFileOptimistically,
|
updateFileOptimistically,
|
||||||
} = useUserFiles();
|
} = useUserFiles();
|
||||||
|
|
@ -556,7 +557,9 @@ export const FilesPage: React.FC = () => {
|
||||||
handleInlineUpdate,
|
handleInlineUpdate,
|
||||||
updateOptimistically: updateFileOptimistically,
|
updateOptimistically: updateFileOptimistically,
|
||||||
previewingFiles,
|
previewingFiles,
|
||||||
|
groupTree,
|
||||||
}}
|
}}
|
||||||
|
groupingConfig={{ contextKey: 'files', enabled: true }}
|
||||||
emptyMessage={emptyTableMessage}
|
emptyMessage={emptyTableMessage}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ export const PromptsPage: React.FC = () => {
|
||||||
loading,
|
loading,
|
||||||
error,
|
error,
|
||||||
refetch,
|
refetch,
|
||||||
|
groupTree,
|
||||||
fetchPromptById,
|
fetchPromptById,
|
||||||
updateOptimistically,
|
updateOptimistically,
|
||||||
} = usePrompts();
|
} = usePrompts();
|
||||||
|
|
@ -236,7 +237,9 @@ export const PromptsPage: React.FC = () => {
|
||||||
handleDelete: handlePromptDelete,
|
handleDelete: handlePromptDelete,
|
||||||
handleInlineUpdate,
|
handleInlineUpdate,
|
||||||
updateOptimistically,
|
updateOptimistically,
|
||||||
|
groupTree,
|
||||||
}}
|
}}
|
||||||
|
groupingConfig={{ contextKey: 'prompts', enabled: true }}
|
||||||
emptyMessage={t('Keine Prompts gefunden')}
|
emptyMessage={t('Keine Prompts gefunden')}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue