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 { 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 { CSS } from "@dnd-kit/utilities";
|
||||
import { CSSProperties, PropsWithChildren, createContext, useMemo } from "react";
|
||||
import { QuoteItemsSortableProps } from "./QuoteItemsSortableDataTable";
|
||||
|
||||
interface Context {
|
||||
attributes: Record<string, any>;
|
||||
export interface QuoteItemsSortableTableRowContextValue {
|
||||
attributes: DraggableAttributes;
|
||||
listeners: DraggableSyntheticListeners;
|
||||
ref(node: HTMLElement | null): void;
|
||||
isDragging: boolean;
|
||||
}
|
||||
export const QuoteItemsSortableTableRowContext = createContext<Context>({
|
||||
attributes: {},
|
||||
export const QuoteItemsSortableTableRowContext = createContext<QuoteItemsSortableTableRowContextValue>({
|
||||
attributes: {
|
||||
role: "button",
|
||||
tabIndex: 0,
|
||||
"aria-disabled": false,
|
||||
"aria-pressed": undefined,
|
||||
"aria-roledescription": "sortable",
|
||||
"aria-describedby": "",
|
||||
},
|
||||
listeners: undefined,
|
||||
ref() {},
|
||||
isDragging: false,
|
||||
});
|
||||
|
||||
function animateLayoutChanges(args: any) {
|
||||
@ -52,8 +61,9 @@ export function QuoteItemsSortableTableRow({
|
||||
attributes,
|
||||
listeners,
|
||||
ref: setActivatorNodeRef,
|
||||
isDragging,
|
||||
}),
|
||||
[attributes, listeners, setActivatorNodeRef]
|
||||
[attributes, isDragging, listeners, setActivatorNodeRef]
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@ -1,12 +1,12 @@
|
||||
import {
|
||||
DataTablaRowActionFunction,
|
||||
DataTableRowActions,
|
||||
DataTableRowDragHandleCell,
|
||||
} from "@/components";
|
||||
import { Checkbox } from "@/ui";
|
||||
import { ColumnDef, Row } from "@tanstack/react-table";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { QuoteItemsSortableDragHandleCell } from "../components/QuoteItemsSortableDragHandleCell";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
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' />
|
||||
),*/
|
||||
header: () => null,
|
||||
cell: (info) => <DataTableRowDragHandleCell rowId={info.row.id} />,
|
||||
cell: () => <QuoteItemsSortableDragHandleCell />,
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
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>) => {
|
||||
return (
|
||||
<FormFieldContext.Provider value={{ name: props.name }}>
|
||||
<Controller {...props} />
|
||||
<Controller key={props.name} {...props} />
|
||||
</FormFieldContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user