feat: version 4.16.0 -- Equipos: tipo de licencia y software agrupado por fabricante
Clasificador por reglas sin IA, correccion manual (migracion 040) y filas desplegables en ListView. Flyout por encima de popups. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { MoreVertical, GripVertical } from 'lucide-react';
|
||||
import { Fragment, useMemo, useState } from 'react';
|
||||
import { MoreVertical, GripVertical, ChevronRight, ChevronDown } from 'lucide-react';
|
||||
import { useUi } from '../context/ui-context.jsx';
|
||||
import { useDragReorder } from '../hooks/useDragReorder.js';
|
||||
import { Flyout, FlyoutItem } from './Flyout.jsx';
|
||||
@@ -9,15 +9,25 @@ import { Flyout, FlyoutItem } from './Flyout.jsx';
|
||||
// `onReorder(idsEnNuevoOrden)` es opcional: si se pasa, habilita arrastrar filas con el mouse para
|
||||
// definir un orden manual (columna con icono de agarre a la izquierda) — solo mientras no haya un
|
||||
// ordenamiento por columna activo, para no mezclar los dos mecanismos de orden a la vez.
|
||||
export function ListView({ columns, rows, searchValue, getSearchHaystack, onRowClick, rowActions, getRowKey, initialSort, onReorder }) {
|
||||
// `getChildren(fila)` es opcional: si devuelve filas, la fila se pliega/despliega con su boton o con
|
||||
// un clic y las hijas se muestran debajo. Al buscar, solo quedan las hijas que coinciden y se abren.
|
||||
export function ListView({ columns, rows, searchValue, getSearchHaystack, onRowClick, rowActions, getRowKey, initialSort, onReorder, getChildren }) {
|
||||
const { t } = useUi();
|
||||
const [sort, setSort] = useState(initialSort || { key: null, dir: 1 });
|
||||
const [expanded, setExpanded] = useState(() => new Set());
|
||||
const haystack = (r) => (getSearchHaystack ? getSearchHaystack(r) : JSON.stringify(r)).toLowerCase();
|
||||
const toggleRow = (key) => setExpanded((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (!next.delete(key)) next.add(key);
|
||||
return next;
|
||||
});
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
if (!searchValue) return rows;
|
||||
const q = searchValue.toLowerCase();
|
||||
return rows.filter((r) => (getSearchHaystack ? getSearchHaystack(r) : JSON.stringify(r)).toLowerCase().includes(q));
|
||||
}, [rows, searchValue, getSearchHaystack]);
|
||||
return rows.filter((r) => haystack(r).includes(q) || getChildren?.(r)?.some((c) => haystack(c).includes(q)));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [rows, searchValue, getSearchHaystack, getChildren]);
|
||||
|
||||
const sorted = useMemo(() => {
|
||||
if (!sort.key) return filtered;
|
||||
@@ -38,6 +48,55 @@ export function ListView({ columns, rows, searchValue, getSearchHaystack, onRowC
|
||||
setSort((prev) => (prev.key === key ? { key, dir: prev.dir * -1 } : { key, dir: 1 }));
|
||||
}
|
||||
|
||||
function renderRow(row, { isParent, open, onToggle, child } = {}) {
|
||||
const actions = rowActions?.(row) || [];
|
||||
const activate = isParent ? onToggle : onRowClick ? () => onRowClick(row) : null;
|
||||
return (
|
||||
<tr
|
||||
key={getRowKey(row)}
|
||||
onClick={() => activate?.()}
|
||||
onContextMenu={actions.length ? (event) => { event.preventDefault(); event.currentTarget.querySelector('.row-menu-btn')?.click(); } : undefined}
|
||||
tabIndex={activate ? 0 : undefined}
|
||||
onKeyDown={(event) => { if (activate && event.target === event.currentTarget && (event.key === 'Enter' || event.key === ' ')) { event.preventDefault(); activate(); } }}
|
||||
className={dragEnabled && isDragging(getRowKey(row)) ? 'dragging' : ''}
|
||||
{...(dragEnabled ? getRowProps(row) : {})}
|
||||
>
|
||||
{dragEnabled && (
|
||||
<td className="drag-handle-cell" onClick={(e) => e.stopPropagation()} {...getHandleProps(row)}>
|
||||
<GripVertical size={15} />
|
||||
</td>
|
||||
)}
|
||||
{columns.map((col, index) => (
|
||||
<td key={col.key} style={index === 0 && child ? { paddingLeft: 38 } : undefined}>
|
||||
{index === 0 && isParent ? (
|
||||
<span className="list-tree-cell">
|
||||
<button type="button" className="tree-toggle" aria-expanded={open} aria-label={t(open ? 'common_collapse' : 'common_expand')} onClick={(e) => { e.stopPropagation(); onToggle(); }}>
|
||||
{open ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
||||
</button>
|
||||
{col.render ? col.render(row) : row[col.key]}
|
||||
</span>
|
||||
) : (col.render ? col.render(row) : row[col.key])}
|
||||
</td>
|
||||
))}
|
||||
{rowActions && (
|
||||
<td onClick={(e) => e.stopPropagation()}>
|
||||
{actions.length > 0 && (
|
||||
<Flyout trigger={({ onClick }) => (
|
||||
<button className="icon-btn row-menu-btn" aria-label={t('common_actions')} onClick={onClick}><MoreVertical /></button>
|
||||
)}>
|
||||
{actions.map((action, i) => (
|
||||
<FlyoutItem key={i} icon={action.icon} danger={action.danger} onClick={action.onClick}>
|
||||
{action.label}
|
||||
</FlyoutItem>
|
||||
))}
|
||||
</Flyout>
|
||||
)}
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="list-card">
|
||||
<table className="list-table">
|
||||
@@ -53,39 +112,20 @@ export function ListView({ columns, rows, searchValue, getSearchHaystack, onRowC
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{displayRows.map((row) => (
|
||||
<tr
|
||||
key={getRowKey(row)}
|
||||
onClick={() => onRowClick?.(row)}
|
||||
onContextMenu={rowActions ? (event) => { event.preventDefault(); event.currentTarget.querySelector('.row-menu-btn')?.click(); } : undefined}
|
||||
tabIndex={onRowClick ? 0 : undefined}
|
||||
onKeyDown={(event) => { if (onRowClick && event.target === event.currentTarget && (event.key === 'Enter' || event.key === ' ')) { event.preventDefault(); onRowClick(row); } }}
|
||||
className={dragEnabled && isDragging(getRowKey(row)) ? 'dragging' : ''}
|
||||
{...(dragEnabled ? getRowProps(row) : {})}
|
||||
>
|
||||
{dragEnabled && (
|
||||
<td className="drag-handle-cell" onClick={(e) => e.stopPropagation()} {...getHandleProps(row)}>
|
||||
<GripVertical size={15} />
|
||||
</td>
|
||||
)}
|
||||
{columns.map((col) => (
|
||||
<td key={col.key}>{col.render ? col.render(row) : row[col.key]}</td>
|
||||
))}
|
||||
{rowActions && (
|
||||
<td onClick={(e) => e.stopPropagation()}>
|
||||
<Flyout trigger={({ onClick }) => (
|
||||
<button className="icon-btn row-menu-btn" aria-label={t('common_actions')} onClick={onClick}><MoreVertical /></button>
|
||||
)}>
|
||||
{rowActions(row).map((action, i) => (
|
||||
<FlyoutItem key={i} icon={action.icon} danger={action.danger} onClick={action.onClick}>
|
||||
{action.label}
|
||||
</FlyoutItem>
|
||||
))}
|
||||
</Flyout>
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
))}
|
||||
{displayRows.map((row) => {
|
||||
const key = getRowKey(row);
|
||||
const allChildren = getChildren?.(row);
|
||||
const isParent = !!allChildren?.length;
|
||||
const q = searchValue?.toLowerCase();
|
||||
const children = isParent && q ? allChildren.filter((c) => haystack(c).includes(q)) : allChildren;
|
||||
const open = isParent && (expanded.has(key) || (!!q && children.length > 0));
|
||||
return (
|
||||
<Fragment key={key}>
|
||||
{renderRow(row, { isParent, open, onToggle: () => toggleRow(key) })}
|
||||
{open && children.map((child) => renderRow(child, { child: true }))}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
{displayRows.length === 0 && <div className="list-empty">{t('common_empty')}</div>}
|
||||
|
||||
Reference in New Issue
Block a user