Merge pull request #77 from valueonag/feat/demo-system-readieness

merge fixes
This commit is contained in:
Patrick Motsch 2026-05-08 00:43:43 +02:00 committed by GitHub
commit 77f4693f4b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 90 additions and 210 deletions

View file

@ -81,6 +81,15 @@ import api from '../../../api';
import { PeriodPicker } from '../../PeriodPicker';
import type { PeriodValue } from '../../PeriodPicker';
import { TableViewsBar, groupLevelsToApiPayload, type GroupByLevelSpec } from '../TableViewsBar';
import {
listTableViews,
getTableView,
createTableView,
updateTableView,
deleteTableView,
type TableListViewRow,
type TableViewConfig,
} from '../../../api/tableViewApi';
function groupLevelsFromViewConfig(raw: unknown): GroupByLevelSpec[] {
if (!Array.isArray(raw)) return [];
@ -91,15 +100,6 @@ function groupLevelsFromViewConfig(raw: unknown): GroupByLevelSpec[] {
}))
.filter((l) => l.field);
}
import {
listTableViews,
getTableView,
createTableView,
updateTableView,
deleteTableView,
type TableListViewRow,
type TableViewConfig,
} from '../../../api/tableViewApi';
function collapseLocalStorageKey(contextKey: string) {
return `porta_table_collapse_${contextKey}`;
@ -2650,6 +2650,81 @@ export function FormGeneratorTable<T extends Record<string, any>>({
batchActions.length > 0 ||
(selectable && selectedIds.size > 0);
const _renderDataRow = (row: T, index: number) => {
const dataAttributes = getRowDataAttributes ? getRowDataAttributes(row, index) : {};
const rowId = _getRowId(row);
return (
<tr
key={rowId || index}
className={`${styles.tr} ${selectedIds.has(rowId) ? styles.selected : ''} ${onRowClick ? styles.clickable : ''}`}
onClick={() => onRowClick?.(row, index)}
draggable={rowDraggable}
onDragStart={(e) => {
if (rowDraggable && onRowDragStart) onRowDragStart(e, row);
}}
onDragEnd={() => {}}
{...Object.fromEntries(Object.entries(dataAttributes).map(([k, v]) => [`data-${k}`, v]))}
>
{selectable && (
<td className={styles.selectColumn} style={{ width: '40px', minWidth: '40px', maxWidth: '40px' }}>
<input type="checkbox"
checked={selectedIds.has(rowId)}
onChange={() => handleRowSelect(row)}
onClick={(e) => e.stopPropagation()}
disabled={isRowSelectable && !isRowSelectable(row)}
title={isRowSelectable && !isRowSelectable(row) ? t('Dieses Element kann nicht ausgewählt werden') : t('Dieses Element auswählen')}
style={{ opacity: isRowSelectable && !isRowSelectable(row) ? 0.4 : 1, cursor: isRowSelectable && !isRowSelectable(row) ? 'not-allowed' : 'pointer' }}
/>
</td>
)}
{hasActionColumn && (
<td className={styles.actionsColumn} style={compact ? undefined : { width: `${currentActionsWidth}px`, minWidth: `${defaultActionsWidth}px` }}>
<div ref={(el) => { if (el) actionButtonsRefs.current.set(index, el); else actionButtonsRefs.current.delete(index); }}
className={`${styles.actionButtons} ${shouldWrapActionButtons ? styles.actionButtonsWrap : ''}`}>
{actionButtons.map((ab, ai) => {
if (ab.visible && !ab.visible(row, hookData)) return null;
const abTitle = typeof ab.title === 'function' ? ab.title(row) : ab.title;
let dis: boolean | { disabled: boolean; message?: string } = false;
if (ab.disabled) { dis = ab.disabled(row, hookData); }
else if (row._permissions) {
if (ab.type === 'edit' && row._permissions.canUpdate === false) dis = true;
else if (ab.type === 'delete' && row._permissions.canDelete === false) dis = true;
}
const isLd = ab.loading ? ab.loading(row) : false;
const isProc = ab.isProcessing ? ab.isProcessing(row) : false;
const bp = { row, disabled: dis, loading: isLd, className: [compact ? actionBtnStyles.compact : '', ab.className ?? ''].filter(Boolean).join(' '), title: abTitle, idField: ab.idField ?? 'id', nameField: ab.nameField ?? 'name', typeField: ab.typeField ?? 'type', contentField: ab.contentField ?? 'content', operationName: ab.operationName, loadingStateName: ab.loadingStateName };
switch (ab.type) {
case 'edit': return <EditActionButton key={`a-${ai}`} {...bp} onEdit={ab.onAction} hookData={hookData} />;
case 'delete': return <DeleteActionButton key={`a-${ai}`} {...bp} containerRef={{ current: actionButtonsRefs.current.get(index) || null }} hookData={hookData} />;
case 'view': return <ViewActionButton key={`a-${ai}`} {...bp} onView={ab.onAction || (() => {})} isViewing={isProc} hookData={hookData} />;
case 'copy': return <CopyActionButton key={`a-${ai}`} {...bp} onCopy={ab.onAction} isCopying={isProc} contentField={ab.contentField} />;
default: return null;
}
})}
{customActions.map((ca) => (
<CustomActionButton key={`ca-${ca.id}`} row={row} id={ca.id} icon={ca.icon}
onClick={ca.onClick} visible={ca.visible} disabled={ca.disabled}
loading={ca.loading} title={ca.title} className={ca.className}
hookData={hookData} idField={ca.idField ?? 'id'} />
))}
</div>
</td>
)}
{detectedColumns.map((col) => {
const cv = row[col.key];
const cCls = col.cellClassName ? col.cellClassName(cv, row) : '';
const aStyle = _columnAlignStyle(col);
return (
<td 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)}
</td>
);
})}
</tr>
);
};
return (
<div
className={`${styles.formGeneratorTable} ${compact ? styles.compactMode : ''} ${useSectionsGroupLayout ? styles.formGeneratorTableSectionsRoot : ''} ${className}`}
@ -3157,116 +3232,9 @@ export function FormGeneratorTable<T extends Record<string, any>>({
);
})}
</tr>
{isExpanded && groupRows.map((row, rowIndex) => {
{isExpanded && groupRows.map((row) => {
const globalIndex = displayData.indexOf(row);
const dataAttributes = getRowDataAttributes ? getRowDataAttributes(row, globalIndex) : {};
return (
<tr
key={`${groupKey}-row-${rowIndex}`}
className={`${styles.tr} ${selectedIds.has(_getRowId(row)) ? styles.selected : ''} ${onRowClick ? styles.clickable : ''}`}
onClick={() => onRowClick?.(row, globalIndex)}
draggable={rowDraggable}
onDragStart={rowDraggable && onRowDragStart ? (e) => onRowDragStart(e, row) : undefined}
{...Object.fromEntries(
Object.entries(dataAttributes).map(([key, value]) => [`data-${key}`, value])
)}
>
{selectable && (
<td className={styles.selectColumn} style={{ width: '40px', minWidth: '40px', maxWidth: '40px' }}>
<input
type="checkbox"
checked={selectedIds.has(_getRowId(row))}
onChange={() => handleRowSelect(row)}
onClick={(e) => e.stopPropagation()}
disabled={isRowSelectable && !isRowSelectable(row)}
title={
isRowSelectable && !isRowSelectable(row)
? t('Dieses Element kann nicht ausgewählt werden')
: t('Dieses Element auswählen')
}
style={{
opacity: isRowSelectable && !isRowSelectable(row) ? 0.4 : 1,
cursor: isRowSelectable && !isRowSelectable(row) ? 'not-allowed' : 'pointer'
}}
/>
</td>
)}
{hasActionColumn && (
<td
className={styles.actionsColumn}
style={compact ? undefined : { width: `${currentActionsWidth}px`, minWidth: `${defaultActionsWidth}px` }}
>
<div
ref={(el) => {
if (el) {
actionButtonsRefs.current.set(globalIndex, el);
} else {
actionButtonsRefs.current.delete(globalIndex);
}
}}
className={`${styles.actionButtons} ${shouldWrapActionButtons ? styles.actionButtonsWrap : ''}`}
>
{actionButtons.map((actionButton, actionIndex) => {
if (actionButton.visible && !actionButton.visible(row, hookData)) return null;
const actionTitle = typeof actionButton.title === 'function'
? actionButton.title(row)
: actionButton.title;
let disabledResult: boolean | { disabled: boolean; message?: string } = false;
if (actionButton.disabled) {
disabledResult = actionButton.disabled(row, hookData);
} else if (row._permissions) {
if (actionButton.type === 'edit' && row._permissions.canUpdate === false) {
disabledResult = true;
} else if (actionButton.type === 'delete' && row._permissions.canDelete === false) {
disabledResult = true;
}
}
const isLoading = actionButton.loading ? actionButton.loading(row) : false;
const isProcessing = actionButton.isProcessing ? actionButton.isProcessing(row) : false;
const baseProps = {
row, disabled: disabledResult, loading: isLoading,
className: [compact ? actionBtnStyles.compact : '', actionButton.className ?? ''].filter(Boolean).join(' '),
title: actionTitle,
idField: actionButton.idField ?? 'id', nameField: actionButton.nameField ?? 'name',
typeField: actionButton.typeField ?? 'type', contentField: actionButton.contentField ?? 'content',
operationName: actionButton.operationName, loadingStateName: actionButton.loadingStateName
};
switch (actionButton.type) {
case 'edit':
return <EditActionButton key={`action-${actionIndex}`} {...baseProps} onEdit={actionButton.onAction} hookData={hookData} />;
case 'delete':
return <DeleteActionButton key={`action-${actionIndex}`} {...baseProps} containerRef={{ current: actionButtonsRefs.current.get(globalIndex) || null }} hookData={hookData} />;
case 'view':
return <ViewActionButton key={`action-${actionIndex}`} {...baseProps} onView={actionButton.onAction || (() => {})} isViewing={isProcessing} hookData={hookData} />;
case 'copy':
return <CopyActionButton key={`action-${actionIndex}`} {...baseProps} onCopy={actionButton.onAction} isCopying={isProcessing} contentField={actionButton.contentField} />;
default:
return null;
}
})}
{customActions.map((customAction) => (
<CustomActionButton key={`custom-${customAction.id}`} row={row} id={customAction.id} icon={customAction.icon}
onClick={customAction.onClick} visible={customAction.visible} disabled={customAction.disabled}
loading={customAction.loading} title={customAction.title} className={customAction.className}
hookData={hookData} idField={customAction.idField ?? 'id'} />
))}
</div>
</td>
)}
{detectedColumns.map(column => {
const cellValue = row[column.key];
const customClassName = column.cellClassName ? column.cellClassName(cellValue, row) : '';
const combinedClassName = `${styles.td} ${customClassName}`.trim();
const alignStyle = _columnAlignStyle(column);
return (
<td key={column.key} className={combinedClassName}
style={{ width: columnWidths[column.key] || column.width || 150, minWidth: columnWidths[column.key] || column.width || 150, maxWidth: columnWidths[column.key] || column.width || 150, ...alignStyle }}>
{formatCellValue(cellValue, column, row)}
</td>
);
})}
</tr>
);
return _renderDataRow(row, globalIndex);
})}
</React.Fragment>
);
@ -3276,97 +3244,6 @@ export function FormGeneratorTable<T extends Record<string, any>>({
// Total colspan for group/breadcrumb/ungrouped rows
const _totalColSpan = (selectable ? 1 : 0) + (hasActionColumn ? 1 : 0) + detectedColumns.length;
// ── Helper: render a single data row ──────────────────
const _renderDataRow = (row: T, index: number) => {
const dataAttributes = getRowDataAttributes ? getRowDataAttributes(row, index) : {};
const rowId = _getRowId(row);
return (
<tr
key={rowId || index}
className={`${styles.tr} ${selectedIds.has(rowId) ? styles.selected : ''} ${onRowClick ? styles.clickable : ''}`}
onClick={() => onRowClick?.(row, index)}
draggable={rowDraggable}
onDragStart={(e) => {
if (rowDraggable && onRowDragStart) onRowDragStart(e, row);
}}
onDragEnd={() => {}}
{...Object.fromEntries(Object.entries(dataAttributes).map(([k, v]) => [`data-${k}`, v]))}
>
{selectable && (
<td
className={styles.selectColumn}
style={{ width: '40px', minWidth: '40px', maxWidth: '40px' }}
>
<input type="checkbox"
checked={selectedIds.has(_getRowId(row))}
onChange={() => handleRowSelect(row)}
onClick={(e) => e.stopPropagation()}
disabled={isRowSelectable && !isRowSelectable(row)}
title={isRowSelectable && !isRowSelectable(row) ? t('Dieses Element kann nicht ausgewählt werden') : t('Dieses Element auswählen')}
style={{ opacity: isRowSelectable && !isRowSelectable(row) ? 0.4 : 1, cursor: isRowSelectable && !isRowSelectable(row) ? 'not-allowed' : 'pointer' }}
/>
</td>
)}
{hasActionColumn && (
<td
className={styles.actionsColumn}
style={compact ? undefined : { width: `${currentActionsWidth}px`, minWidth: `${defaultActionsWidth}px` }}
>
<div ref={(el) => { if (el) actionButtonsRefs.current.set(index, el); else actionButtonsRefs.current.delete(index); }}
className={`${styles.actionButtons} ${shouldWrapActionButtons ? styles.actionButtonsWrap : ''}`}>
{actionButtons.map((ab, ai) => {
if (ab.visible && !ab.visible(row, hookData)) return null;
const abTitle = typeof ab.title === 'function' ? ab.title(row) : ab.title;
let dis: boolean | { disabled: boolean; message?: string } = false;
if (ab.disabled) { dis = ab.disabled(row, hookData); }
else if (row._permissions) {
if (ab.type === 'edit' && row._permissions.canUpdate === false) dis = true;
else if (ab.type === 'delete' && row._permissions.canDelete === false) dis = true;
}
const isLd = ab.loading ? ab.loading(row) : false;
const isProc = ab.isProcessing ? ab.isProcessing(row) : false;
const bp = { row, disabled: dis, loading: isLd, className: [compact ? actionBtnStyles.compact : '', ab.className ?? ''].filter(Boolean).join(' '), title: abTitle, idField: ab.idField ?? 'id', nameField: ab.nameField ?? 'name', typeField: ab.typeField ?? 'type', contentField: ab.contentField ?? 'content', operationName: ab.operationName, loadingStateName: ab.loadingStateName };
switch (ab.type) {
case 'edit': return <EditActionButton key={`a-${ai}`} {...bp} onEdit={ab.onAction} hookData={hookData} />;
case 'delete': return <DeleteActionButton key={`a-${ai}`} {...bp} containerRef={{ current: actionButtonsRefs.current.get(index) || null }} hookData={hookData} />;
case 'view': return <ViewActionButton key={`a-${ai}`} {...bp} onView={ab.onAction || (() => {})} isViewing={isProc} hookData={hookData} />;
case 'copy': return <CopyActionButton key={`a-${ai}`} {...bp} onCopy={ab.onAction} isCopying={isProc} contentField={ab.contentField} />;
default: return null;
}
})}
{customActions.map((ca) => (
<CustomActionButton key={`ca-${ca.id}`} row={row} id={ca.id} icon={ca.icon}
onClick={ca.onClick} visible={ca.visible} disabled={ca.disabled}
loading={ca.loading} title={ca.title} className={ca.className}
hookData={hookData} idField={ca.idField ?? 'id'} />
))}
</div>
</td>
)}
{detectedColumns.map((col) => {
const cv = row[col.key];
const cCls = col.cellClassName ? col.cellClassName(cv, row) : '';
const aStyle = _columnAlignStyle(col);
return (
<td
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)}
</td>
);
})}
</tr>
);
};
// ── Strategy B view grouping: insert collapsible group headers ──
if (effectiveGroupLayout && effectiveGroupLayout.bands.length > 0) {
const rows: React.ReactNode[] = [];

View file

@ -405,10 +405,12 @@ export function FormGeneratorTree<T = any>({
onSelectionChange,
onRefresh,
onSendToChat,
allowCreateFolder = true,
className,
}: FormGeneratorTreeProps<T>) {
const { t } = useLanguage();
const { confirm, ConfirmDialog } = useConfirm();
const { confirm } = useConfirm();
const { prompt, PromptDialog } = usePrompt();
const [nodes, setNodes] = useState<TreeNode<T>[]>([]);
const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set());
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
@ -695,6 +697,7 @@ export function FormGeneratorTree<T = any>({
if (ownership === 'shared') return;
if (draggingIds.size === 0) return;
if (draggingIds.has(node.id)) return;
if (node.type !== 'folder') return;
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
setDragOverId(node.id);