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,
|
getCoreRowModel,
|
||||||
useReactTable,
|
useReactTable,
|
||||||
} from "@tanstack/react-table";
|
} from "@tanstack/react-table";
|
||||||
import { useMemo, useState } from "react";
|
import { Fragment, useMemo, useState } from "react";
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
import { FieldValues, UseFieldArrayReturn } from "react-hook-form";
|
import { FieldValues, UseFieldArrayReturn } from "react-hook-form";
|
||||||
import { AppendBlockRowButton } from "./AppendBlockRowButton";
|
import { AppendBlockRowButton } from "./AppendBlockRowButton";
|
||||||
import { AppendCatalogArticleRowButton } from "./AppendCatalogArticleRowButton";
|
import { AppendCatalogArticleRowButton } from "./AppendCatalogArticleRowButton";
|
||||||
import { AppendEmptyRowButton } from "./AppendEmptyRowButton";
|
import { AppendEmptyRowButton } from "./AppendEmptyRowButton";
|
||||||
import { QuoteItemsSortableDataTableToolbar } from "./QuoteItemsSortableDataTableToolbar";
|
import { QuoteItemsSortableDataTableToolbar } from "./QuoteItemsSortableDataTableToolbar";
|
||||||
|
import { QuoteItemsInlineRowActions } from "./QuoteItemsInlineRowActions";
|
||||||
import { QuoteItemsSortableTableRow } from "./QuoteItemsSortableTableRow";
|
import { QuoteItemsSortableTableRow } from "./QuoteItemsSortableTableRow";
|
||||||
|
|
||||||
export type RowIdData = { [x: string]: any };
|
export type RowIdData = { [x: string]: any };
|
||||||
@ -120,6 +121,7 @@ export function QuoteItemsSortableDataTable<TData extends RowData & RowIdData, T
|
|||||||
}: QuoteItemsSortableDataTableProps<TData, TValue>) {
|
}: QuoteItemsSortableDataTableProps<TData, TValue>) {
|
||||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||||
const [activeId, setActiveId] = useState<UniqueIdentifier | null>();
|
const [activeId, setActiveId] = useState<UniqueIdentifier | null>();
|
||||||
|
const [activeRowId, setActiveRowId] = useState<string | null>(null);
|
||||||
|
|
||||||
const sorteableRowIds = useMemo(() => data.map((item) => item.id), [data]);
|
const sorteableRowIds = useMemo(() => data.map((item) => item.id), [data]);
|
||||||
|
|
||||||
@ -337,24 +339,46 @@ export function QuoteItemsSortableDataTable<TData extends RowData & RowIdData, T
|
|||||||
items={filterItems(sorteableRowIds)}
|
items={filterItems(sorteableRowIds)}
|
||||||
strategy={verticalListSortingStrategy}
|
strategy={verticalListSortingStrategy}
|
||||||
>
|
>
|
||||||
{filterItems(table.getRowModel().rows).map((row) => (
|
{filterItems(table.getRowModel().rows).map((row) => {
|
||||||
<QuoteItemsSortableTableRow key={(row as Row<any>).id} id={(row as Row<any>).id}>
|
const typedRow = row as Row<TData>;
|
||||||
{(row as Row<any>).getVisibleCells().map((cell) => (
|
const isActive = typedRow.id === activeRowId;
|
||||||
<TableCell
|
|
||||||
key={cell.id}
|
return (
|
||||||
className='px-2 py-2 align-top'
|
<Fragment key={typedRow.id}>
|
||||||
style={{
|
<QuoteItemsSortableTableRow
|
||||||
width:
|
id={typedRow.id}
|
||||||
cell.column.getSize() === Number.MAX_SAFE_INTEGER
|
isActive={isActive}
|
||||||
? "auto"
|
onActivate={() => setActiveRowId(typedRow.id)}
|
||||||
: cell.column.getSize(),
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
{typedRow.getVisibleCells().map((cell) => (
|
||||||
</TableCell>
|
<TableCell
|
||||||
))}
|
key={cell.id}
|
||||||
</QuoteItemsSortableTableRow>
|
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>
|
</SortableContext>
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
|
|||||||
@ -12,6 +12,11 @@ export interface QuoteItemsSortableTableRowContextValue {
|
|||||||
ref(node: HTMLElement | null): void;
|
ref(node: HTMLElement | null): void;
|
||||||
isDragging: boolean;
|
isDragging: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface QuoteItemsSortableTableRowProps extends QuoteItemsSortableProps {
|
||||||
|
isActive?: boolean;
|
||||||
|
onActivate?: () => void;
|
||||||
|
}
|
||||||
export const QuoteItemsSortableTableRowContext = createContext<QuoteItemsSortableTableRowContextValue>({
|
export const QuoteItemsSortableTableRowContext = createContext<QuoteItemsSortableTableRowContextValue>({
|
||||||
attributes: {
|
attributes: {
|
||||||
role: "button",
|
role: "button",
|
||||||
@ -37,7 +42,9 @@ function animateLayoutChanges(args: any) {
|
|||||||
export function QuoteItemsSortableTableRow({
|
export function QuoteItemsSortableTableRow({
|
||||||
id,
|
id,
|
||||||
children,
|
children,
|
||||||
}: PropsWithChildren<QuoteItemsSortableProps>) {
|
isActive = false,
|
||||||
|
onActivate,
|
||||||
|
}: PropsWithChildren<QuoteItemsSortableTableRowProps>) {
|
||||||
const {
|
const {
|
||||||
attributes,
|
attributes,
|
||||||
isDragging,
|
isDragging,
|
||||||
@ -73,10 +80,15 @@ export function QuoteItemsSortableTableRow({
|
|||||||
id={String(id)}
|
id={String(id)}
|
||||||
className={cn(
|
className={cn(
|
||||||
isDragging ? "opacity-60" : "opacity-100",
|
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}
|
ref={setNodeRef}
|
||||||
style={style}
|
style={style}
|
||||||
|
onClick={onActivate}
|
||||||
|
onFocusCapture={onActivate}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
|||||||
@ -12,7 +12,6 @@ import { useToast } from "@/ui/use-toast";
|
|||||||
import { CurrencyData, Language, Quantity, UnitPrice } from "@shared/contexts";
|
import { CurrencyData, Language, Quantity, UnitPrice } from "@shared/contexts";
|
||||||
import { ColumnDef } from "@tanstack/react-table";
|
import { ColumnDef } from "@tanstack/react-table";
|
||||||
import { t } from "i18next";
|
import { t } from "i18next";
|
||||||
import { ChevronDownIcon, ChevronUpIcon, CopyIcon, Trash2Icon } from "lucide-react";
|
|
||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import { useFieldArray, useFormContext } from "react-hook-form";
|
import { useFieldArray, useFormContext } from "react-hook-form";
|
||||||
import { useDetailColumns } from "../../hooks";
|
import { useDetailColumns } from "../../hooks";
|
||||||
@ -192,39 +191,6 @@ export const QuoteDetailsCardEditor = ({
|
|||||||
{
|
{
|
||||||
enableDragHandleColumn: true,
|
enableDragHandleColumn: true,
|
||||||
enableSelectionColumn: 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