Añadir acciones en línea para las filas de cotización y mejorar la gestión de filas activas
This commit is contained in:
parent
6f77eaad97
commit
2527c2c785
@ -0,0 +1,56 @@
|
||||
import { act } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import type { ButtonHTMLAttributes } from "react";
|
||||
import { QuoteItemsInlineRowActions } from "./QuoteItemsInlineRowActions";
|
||||
|
||||
declare global {
|
||||
var IS_REACT_ACT_ENVIRONMENT: boolean;
|
||||
}
|
||||
|
||||
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
||||
|
||||
jest.mock("i18next", () => ({ t: (key: string) => key }));
|
||||
jest.mock("@/ui", () => ({
|
||||
Button: (props: ButtonHTMLAttributes<HTMLButtonElement>) => <button {...props} />,
|
||||
}));
|
||||
|
||||
describe("QuoteItemsInlineRowActions", () => {
|
||||
let container: HTMLDivElement;
|
||||
let root: ReturnType<typeof createRoot>;
|
||||
|
||||
beforeEach(() => {
|
||||
container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
root = createRoot(container);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
act(() => root.unmount());
|
||||
container.remove();
|
||||
});
|
||||
|
||||
it("reuses table meta actions for the active row index", () => {
|
||||
const insertItem = jest.fn();
|
||||
const duplicateItems = jest.fn();
|
||||
const deleteItems = jest.fn();
|
||||
const onDelete = jest.fn();
|
||||
const row = { index: 2 } as any;
|
||||
const table = { options: { meta: { insertItem, duplicateItems, deleteItems } } } as any;
|
||||
|
||||
act(() => {
|
||||
root.render(<QuoteItemsInlineRowActions row={row} table={table} onDelete={onDelete} />);
|
||||
});
|
||||
|
||||
const buttons = Array.from(container.querySelectorAll("button"));
|
||||
act(() => buttons[0].click());
|
||||
act(() => buttons[1].click());
|
||||
act(() => buttons[2].click());
|
||||
act(() => buttons[3].click());
|
||||
|
||||
expect(insertItem).toHaveBeenNthCalledWith(1, 2);
|
||||
expect(insertItem).toHaveBeenNthCalledWith(2, 3);
|
||||
expect(duplicateItems).toHaveBeenCalledWith(2);
|
||||
expect(deleteItems).toHaveBeenCalledWith(2);
|
||||
expect(onDelete).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,46 @@
|
||||
import { Button } from "@/ui";
|
||||
import { Row, RowData, Table } from "@tanstack/react-table";
|
||||
import { t } from "i18next";
|
||||
import { ChevronDownIcon, ChevronUpIcon, CopyIcon, Trash2Icon } from "lucide-react";
|
||||
|
||||
export const QuoteItemsInlineRowActions = <TData extends RowData>({
|
||||
row,
|
||||
table,
|
||||
onDelete,
|
||||
}: {
|
||||
row: Row<TData>;
|
||||
table: Table<TData>;
|
||||
onDelete: () => void;
|
||||
}) => (
|
||||
<div className='flex flex-wrap items-center gap-2'>
|
||||
<Button type='button' variant='outline' size='sm' onClick={() => table.options.meta?.insertItem(row.index)}>
|
||||
<ChevronUpIcon className='w-4 h-4 mr-2' />
|
||||
{t("common.insert_row_above")}
|
||||
</Button>
|
||||
<Button
|
||||
type='button'
|
||||
variant='outline'
|
||||
size='sm'
|
||||
onClick={() => table.options.meta?.insertItem(row.index + 1)}
|
||||
>
|
||||
<ChevronDownIcon className='w-4 h-4 mr-2' />
|
||||
{t("common.insert_row_below")}
|
||||
</Button>
|
||||
<Button type='button' variant='outline' size='sm' onClick={() => table.options.meta?.duplicateItems(row.index)}>
|
||||
<CopyIcon className='w-4 h-4 mr-2' />
|
||||
{t("common.duplicate_row")}
|
||||
</Button>
|
||||
<Button
|
||||
type='button'
|
||||
variant='destructive'
|
||||
size='sm'
|
||||
onClick={() => {
|
||||
table.options.meta?.deleteItems(row.index);
|
||||
onDelete();
|
||||
}}
|
||||
>
|
||||
<Trash2Icon className='w-4 h-4 mr-2' />
|
||||
{t("common.remove_row")}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
@ -30,13 +30,14 @@ import {
|
||||
getCoreRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
import { useMemo, useState } from "react";
|
||||
import { Fragment, useMemo, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { FieldValues, UseFieldArrayReturn } from "react-hook-form";
|
||||
import { AppendBlockRowButton } from "./AppendBlockRowButton";
|
||||
import { AppendCatalogArticleRowButton } from "./AppendCatalogArticleRowButton";
|
||||
import { AppendEmptyRowButton } from "./AppendEmptyRowButton";
|
||||
import { QuoteItemsSortableDataTableToolbar } from "./QuoteItemsSortableDataTableToolbar";
|
||||
import { QuoteItemsInlineRowActions } from "./QuoteItemsInlineRowActions";
|
||||
import { QuoteItemsSortableTableRow } from "./QuoteItemsSortableTableRow";
|
||||
|
||||
export type RowIdData = { [x: string]: any };
|
||||
@ -120,6 +121,7 @@ export function QuoteItemsSortableDataTable<TData extends RowData & RowIdData, T
|
||||
}: QuoteItemsSortableDataTableProps<TData, TValue>) {
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
const [activeId, setActiveId] = useState<UniqueIdentifier | null>();
|
||||
const [activeRowId, setActiveRowId] = useState<string | null>(null);
|
||||
|
||||
const sorteableRowIds = useMemo(() => data.map((item) => item.id), [data]);
|
||||
|
||||
@ -337,24 +339,46 @@ export function QuoteItemsSortableDataTable<TData extends RowData & RowIdData, T
|
||||
items={filterItems(sorteableRowIds)}
|
||||
strategy={verticalListSortingStrategy}
|
||||
>
|
||||
{filterItems(table.getRowModel().rows).map((row) => (
|
||||
<QuoteItemsSortableTableRow key={(row as Row<any>).id} id={(row as Row<any>).id}>
|
||||
{(row as Row<any>).getVisibleCells().map((cell) => (
|
||||
<TableCell
|
||||
key={cell.id}
|
||||
className='px-2 py-2 align-top'
|
||||
style={{
|
||||
width:
|
||||
cell.column.getSize() === Number.MAX_SAFE_INTEGER
|
||||
? "auto"
|
||||
: cell.column.getSize(),
|
||||
}}
|
||||
{filterItems(table.getRowModel().rows).map((row) => {
|
||||
const typedRow = row as Row<TData>;
|
||||
const isActive = typedRow.id === activeRowId;
|
||||
|
||||
return (
|
||||
<Fragment key={typedRow.id}>
|
||||
<QuoteItemsSortableTableRow
|
||||
id={typedRow.id}
|
||||
isActive={isActive}
|
||||
onActivate={() => setActiveRowId(typedRow.id)}
|
||||
>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</QuoteItemsSortableTableRow>
|
||||
))}
|
||||
{typedRow.getVisibleCells().map((cell) => (
|
||||
<TableCell
|
||||
key={cell.id}
|
||||
className='px-2 py-2 align-top'
|
||||
style={{
|
||||
width:
|
||||
cell.column.getSize() === Number.MAX_SAFE_INTEGER
|
||||
? "auto"
|
||||
: cell.column.getSize(),
|
||||
}}
|
||||
>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</QuoteItemsSortableTableRow>
|
||||
{isActive && (
|
||||
<TableRow className='border border-t-0 border-destructive/30 border-l-4 border-l-destructive bg-destructive/5 hover:bg-destructive/5'>
|
||||
<TableCell colSpan={typedRow.getVisibleCells().length} className='px-2 py-2'>
|
||||
<QuoteItemsInlineRowActions
|
||||
row={typedRow}
|
||||
table={table}
|
||||
onDelete={() => setActiveRowId(null)}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</SortableContext>
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
@ -12,6 +12,11 @@ export interface QuoteItemsSortableTableRowContextValue {
|
||||
ref(node: HTMLElement | null): void;
|
||||
isDragging: boolean;
|
||||
}
|
||||
|
||||
export interface QuoteItemsSortableTableRowProps extends QuoteItemsSortableProps {
|
||||
isActive?: boolean;
|
||||
onActivate?: () => void;
|
||||
}
|
||||
export const QuoteItemsSortableTableRowContext = createContext<QuoteItemsSortableTableRowContextValue>({
|
||||
attributes: {
|
||||
role: "button",
|
||||
@ -37,7 +42,9 @@ function animateLayoutChanges(args: any) {
|
||||
export function QuoteItemsSortableTableRow({
|
||||
id,
|
||||
children,
|
||||
}: PropsWithChildren<QuoteItemsSortableProps>) {
|
||||
isActive = false,
|
||||
onActivate,
|
||||
}: PropsWithChildren<QuoteItemsSortableTableRowProps>) {
|
||||
const {
|
||||
attributes,
|
||||
isDragging,
|
||||
@ -73,10 +80,15 @@ export function QuoteItemsSortableTableRow({
|
||||
id={String(id)}
|
||||
className={cn(
|
||||
isDragging ? "opacity-60" : "opacity-100",
|
||||
"m-0 hover:bg-muted hover:focus-within:bg-accent focus-within:bg-accent"
|
||||
isActive
|
||||
? "border border-destructive/30 border-b-0 border-l-4 border-l-destructive bg-destructive/5 hover:bg-destructive/5 focus-within:bg-destructive/5"
|
||||
: "hover:bg-muted hover:focus-within:bg-accent focus-within:bg-accent",
|
||||
"m-0"
|
||||
)}
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
onClick={onActivate}
|
||||
onFocusCapture={onActivate}
|
||||
>
|
||||
{children}
|
||||
</TableRow>
|
||||
|
||||
@ -12,7 +12,6 @@ import { useToast } from "@/ui/use-toast";
|
||||
import { CurrencyData, Language, Quantity, UnitPrice } from "@shared/contexts";
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { t } from "i18next";
|
||||
import { ChevronDownIcon, ChevronUpIcon, CopyIcon, Trash2Icon } from "lucide-react";
|
||||
import { useCallback, useState } from "react";
|
||||
import { useFieldArray, useFormContext } from "react-hook-form";
|
||||
import { useDetailColumns } from "../../hooks";
|
||||
@ -192,39 +191,6 @@ export const QuoteDetailsCardEditor = ({
|
||||
{
|
||||
enableDragHandleColumn: true,
|
||||
enableSelectionColumn: true,
|
||||
enableActionsColumn: true,
|
||||
rowActionFn: (props) => {
|
||||
const { table, row } = props;
|
||||
return [
|
||||
{
|
||||
label: t("common.duplicate_row"),
|
||||
icon: <CopyIcon className='w-4 h-4 mr-2' />,
|
||||
onClick: () => table.options.meta?.duplicateItems(row.index),
|
||||
},
|
||||
{
|
||||
label: t("common.insert_row_above"),
|
||||
icon: <ChevronUpIcon className='w-4 h-4 mr-2' />,
|
||||
onClick: () => table.options.meta?.insertItem(row.index),
|
||||
},
|
||||
{
|
||||
label: t("common.insert_row_below"),
|
||||
icon: <ChevronDownIcon className='w-4 h-4 mr-2' />,
|
||||
onClick: () => table.options.meta?.insertItem(row.index + 1),
|
||||
},
|
||||
|
||||
{
|
||||
label: "-",
|
||||
},
|
||||
{
|
||||
label: t("common.remove_row"),
|
||||
//shortcut: "⌘⌫",
|
||||
icon: <Trash2Icon className='w-4 h-4 mr-2' />,
|
||||
onClick: () => {
|
||||
table.options.meta?.deleteItems(row.index);
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user