Añadir funcionalidad de arrastre y gestión de filas en QuoteItemsSortable, incluyendo pruebas unitarias y ajustes en el formulario.
This commit is contained in:
parent
463c0662ed
commit
3eb0396cbd
@ -0,0 +1,77 @@
|
|||||||
|
import { act } from "react";
|
||||||
|
import { createRoot } from "react-dom/client";
|
||||||
|
import { QuoteItemsSortableDragHandleCell } from "./QuoteItemsSortableDragHandleCell";
|
||||||
|
import {
|
||||||
|
QuoteItemsSortableTableRowContext,
|
||||||
|
QuoteItemsSortableTableRowContextValue,
|
||||||
|
} from "./QuoteItemsSortableTableRow";
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
var IS_REACT_ACT_ENVIRONMENT: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
||||||
|
|
||||||
|
jest.mock("i18next", () => ({ t: (key: string) => key }));
|
||||||
|
jest.mock("@/ui", () => {
|
||||||
|
const React = require("react") as typeof import("react");
|
||||||
|
|
||||||
|
return {
|
||||||
|
Button: React.forwardRef<HTMLButtonElement, React.ButtonHTMLAttributes<HTMLButtonElement>>(
|
||||||
|
(props, ref) => <button ref={ref} {...props} />
|
||||||
|
),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("QuoteItemsSortableDragHandleCell", () => {
|
||||||
|
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("uses the row sortable context as the drag activator", () => {
|
||||||
|
const listener = jest.fn();
|
||||||
|
const activatorRef = jest.fn();
|
||||||
|
const contextValue: QuoteItemsSortableTableRowContextValue = {
|
||||||
|
attributes: {
|
||||||
|
role: "button",
|
||||||
|
tabIndex: 0,
|
||||||
|
"aria-disabled": false,
|
||||||
|
"aria-pressed": undefined,
|
||||||
|
"aria-roledescription": "sortable",
|
||||||
|
"aria-describedby": "row-1",
|
||||||
|
},
|
||||||
|
listeners: { onPointerDown: listener },
|
||||||
|
ref: activatorRef,
|
||||||
|
isDragging: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
root.render(
|
||||||
|
<QuoteItemsSortableTableRowContext.Provider value={contextValue}>
|
||||||
|
<QuoteItemsSortableDragHandleCell />
|
||||||
|
</QuoteItemsSortableTableRowContext.Provider>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const button = container.querySelector("button") as HTMLButtonElement;
|
||||||
|
|
||||||
|
expect(button.getAttribute("aria-describedby")).toBe("row-1");
|
||||||
|
expect(activatorRef).toHaveBeenCalledWith(button);
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
button.dispatchEvent(new MouseEvent("pointerdown", { bubbles: true }));
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(listener).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -0,0 +1,42 @@
|
|||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { Button } from "@/ui";
|
||||||
|
import { t } from "i18next";
|
||||||
|
import { GripVerticalIcon } from "lucide-react";
|
||||||
|
import { useContext } from "react";
|
||||||
|
import { QuoteItemsSortableTableRowContext } from "./QuoteItemsSortableTableRow";
|
||||||
|
|
||||||
|
export interface QuoteItemsSortableDragHandleCellProps {
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const QuoteItemsSortableDragHandleCell = ({
|
||||||
|
className,
|
||||||
|
}: QuoteItemsSortableDragHandleCellProps) => {
|
||||||
|
const {
|
||||||
|
attributes,
|
||||||
|
isDragging,
|
||||||
|
listeners,
|
||||||
|
ref: setActivatorNodeRef,
|
||||||
|
} = useContext(QuoteItemsSortableTableRowContext);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
ref={setActivatorNodeRef}
|
||||||
|
onClick={(event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
}}
|
||||||
|
size='icon'
|
||||||
|
variant='link'
|
||||||
|
className={cn(
|
||||||
|
isDragging ? "cursor-grabbing" : "cursor-grab",
|
||||||
|
"w-4 h-4 mt-2 text-ring hover:text-muted-foreground",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...attributes}
|
||||||
|
{...listeners}
|
||||||
|
>
|
||||||
|
<GripVerticalIcon className='w-4 h-4' />
|
||||||
|
<span className='sr-only'>{t("common.move_row")}</span>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
@ -1,20 +1,29 @@
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { TableRow } from "@/ui/table";
|
import { TableRow } from "@/ui/table";
|
||||||
import { DraggableSyntheticListeners } from "@dnd-kit/core";
|
import { DraggableAttributes, DraggableSyntheticListeners } from "@dnd-kit/core";
|
||||||
import { defaultAnimateLayoutChanges, useSortable } from "@dnd-kit/sortable";
|
import { defaultAnimateLayoutChanges, useSortable } from "@dnd-kit/sortable";
|
||||||
import { CSS } from "@dnd-kit/utilities";
|
import { CSS } from "@dnd-kit/utilities";
|
||||||
import { CSSProperties, PropsWithChildren, createContext, useMemo } from "react";
|
import { CSSProperties, PropsWithChildren, createContext, useMemo } from "react";
|
||||||
import { QuoteItemsSortableProps } from "./QuoteItemsSortableDataTable";
|
import { QuoteItemsSortableProps } from "./QuoteItemsSortableDataTable";
|
||||||
|
|
||||||
interface Context {
|
export interface QuoteItemsSortableTableRowContextValue {
|
||||||
attributes: Record<string, any>;
|
attributes: DraggableAttributes;
|
||||||
listeners: DraggableSyntheticListeners;
|
listeners: DraggableSyntheticListeners;
|
||||||
ref(node: HTMLElement | null): void;
|
ref(node: HTMLElement | null): void;
|
||||||
|
isDragging: boolean;
|
||||||
}
|
}
|
||||||
export const QuoteItemsSortableTableRowContext = createContext<Context>({
|
export const QuoteItemsSortableTableRowContext = createContext<QuoteItemsSortableTableRowContextValue>({
|
||||||
attributes: {},
|
attributes: {
|
||||||
|
role: "button",
|
||||||
|
tabIndex: 0,
|
||||||
|
"aria-disabled": false,
|
||||||
|
"aria-pressed": undefined,
|
||||||
|
"aria-roledescription": "sortable",
|
||||||
|
"aria-describedby": "",
|
||||||
|
},
|
||||||
listeners: undefined,
|
listeners: undefined,
|
||||||
ref() {},
|
ref() {},
|
||||||
|
isDragging: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
function animateLayoutChanges(args: any) {
|
function animateLayoutChanges(args: any) {
|
||||||
@ -52,8 +61,9 @@ export function QuoteItemsSortableTableRow({
|
|||||||
attributes,
|
attributes,
|
||||||
listeners,
|
listeners,
|
||||||
ref: setActivatorNodeRef,
|
ref: setActivatorNodeRef,
|
||||||
|
isDragging,
|
||||||
}),
|
}),
|
||||||
[attributes, listeners, setActivatorNodeRef]
|
[attributes, isDragging, listeners, setActivatorNodeRef]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
import {
|
import {
|
||||||
DataTablaRowActionFunction,
|
DataTablaRowActionFunction,
|
||||||
DataTableRowActions,
|
DataTableRowActions,
|
||||||
DataTableRowDragHandleCell,
|
|
||||||
} from "@/components";
|
} from "@/components";
|
||||||
import { Checkbox } from "@/ui";
|
import { Checkbox } from "@/ui";
|
||||||
import { ColumnDef, Row } from "@tanstack/react-table";
|
import { ColumnDef, Row } from "@tanstack/react-table";
|
||||||
|
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
import { QuoteItemsSortableDragHandleCell } from "../components/QuoteItemsSortableDragHandleCell";
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
export function useDetailColumns<TData = unknown, TValue = unknown>(
|
export function useDetailColumns<TData = unknown, TValue = unknown>(
|
||||||
@ -35,7 +35,7 @@ export function useDetailColumns<TData = unknown, TValue = unknown>(
|
|||||||
<UnfoldVertical aria-label='Mover fila' className='items-center justify-center w-4 h-4' />
|
<UnfoldVertical aria-label='Mover fila' className='items-center justify-center w-4 h-4' />
|
||||||
),*/
|
),*/
|
||||||
header: () => null,
|
header: () => null,
|
||||||
cell: (info) => <DataTableRowDragHandleCell rowId={info.row.id} />,
|
cell: () => <QuoteItemsSortableDragHandleCell />,
|
||||||
enableSorting: false,
|
enableSorting: false,
|
||||||
enableHiding: false,
|
enableHiding: false,
|
||||||
size: 40,
|
size: 40,
|
||||||
|
|||||||
101
client/src/ui/form.test.tsx
Normal file
101
client/src/ui/form.test.tsx
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
import { act, useEffect, useRef } from "react";
|
||||||
|
import { createRoot } from "react-dom/client";
|
||||||
|
import { FormProvider, UseFormReturn, useForm } from "react-hook-form";
|
||||||
|
import { FormField } from "./form";
|
||||||
|
|
||||||
|
type TestFormValues = {
|
||||||
|
items: Array<{ quantity: string }>;
|
||||||
|
};
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
var IS_REACT_ACT_ENVIRONMENT: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
||||||
|
|
||||||
|
type DynamicQuantityFieldProps = {
|
||||||
|
name: "items.0.quantity" | "items.1.quantity";
|
||||||
|
onMount: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DynamicQuantityField = ({ name, onMount }: DynamicQuantityFieldProps) => {
|
||||||
|
const onMountRef = useRef(onMount);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onMountRef.current();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormField
|
||||||
|
name={name}
|
||||||
|
render={({ field }) => <input aria-label='quantity' {...field} />}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type FormHarnessProps = DynamicQuantityFieldProps & {
|
||||||
|
onFormReady: (form: UseFormReturn<TestFormValues>) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const FormHarness = ({ name, onMount, onFormReady }: FormHarnessProps) => {
|
||||||
|
const form = useForm<TestFormValues>({
|
||||||
|
defaultValues: {
|
||||||
|
items: [{ quantity: "1" }, { quantity: "2" }],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onFormReady(form);
|
||||||
|
}, [form, onFormReady]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormProvider {...form}>
|
||||||
|
<DynamicQuantityField name={name} onMount={onMount} />
|
||||||
|
</FormProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("FormField", () => {
|
||||||
|
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("resubscribes a Controller when its name changes without remounting its parent", () => {
|
||||||
|
const onMount = jest.fn();
|
||||||
|
let form: UseFormReturn<TestFormValues> | undefined;
|
||||||
|
|
||||||
|
const renderHarness = (name: DynamicQuantityFieldProps["name"]) => {
|
||||||
|
root.render(
|
||||||
|
<FormHarness name={name} onMount={onMount} onFormReady={(value) => (form = value)} />
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
act(() => renderHarness("items.0.quantity"));
|
||||||
|
|
||||||
|
const inputBeforeRename = container.querySelector("input[aria-label='quantity']") as HTMLInputElement;
|
||||||
|
expect(inputBeforeRename.value).toBe("1");
|
||||||
|
expect(onMount).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
act(() => renderHarness("items.1.quantity"));
|
||||||
|
|
||||||
|
expect(onMount).toHaveBeenCalledTimes(1);
|
||||||
|
expect((container.querySelector("input[aria-label='quantity']") as HTMLInputElement).value).toBe("2");
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
form?.setValue("items.1.quantity", "5");
|
||||||
|
});
|
||||||
|
|
||||||
|
expect((container.querySelector("input[aria-label='quantity']") as HTMLInputElement).value).toBe("5");
|
||||||
|
expect(form?.getValues("items.1.quantity")).toBe("5");
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -34,7 +34,7 @@ const FormField = <
|
|||||||
}: ControllerProps<TFieldValues, TName>) => {
|
}: ControllerProps<TFieldValues, TName>) => {
|
||||||
return (
|
return (
|
||||||
<FormFieldContext.Provider value={{ name: props.name }}>
|
<FormFieldContext.Provider value={{ name: props.name }}>
|
||||||
<Controller {...props} />
|
<Controller key={props.name} {...props} />
|
||||||
</FormFieldContext.Provider>
|
</FormFieldContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user