From f5217675b5b03888f3330d9de9d7e9c7d6937e90 Mon Sep 17 00:00:00 2001 From: David Arranz Date: Mon, 19 Oct 2026 18:05:03 +0200 Subject: [PATCH] refactor: simplify quote items table with reusable components and hooks - Refactored QuoteItemsSortableDataTable to reduce lines from 593 to 88, maintaining API and re-exporting types. - Introduced QuoteItemsAddActions for centralized row/article/block addition buttons. - Created QuoteItemsEmptyState to handle empty states with title, description, icon, and actions. - Separated components for table header, body, and drag overlay for better modularity. - Implemented useQuoteItemsSortableTable hook to manage row selection, drag-and-drop functionality, and item manipulation. - Added tests for the new hook to ensure functionality for duplication, deletion, and drag-and-drop operations. - Updated documentation to reflect changes and improvements in the quote items table structure. --- .../components/QuoteChaptersDataTable.tsx | 35 +- .../components/QuoteItemsAddActions.tsx | 23 + .../components/QuoteItemsDragOverlay.tsx | 49 ++ .../components/QuoteItemsEmptyState.tsx | 32 + .../QuoteItemsSortableDataTable.tsx | 617 ++---------------- .../QuoteItemsSortableDataTable.types.ts | 48 ++ ...uoteItemsSortableDataTableToolbar.test.tsx | 5 +- .../QuoteItemsSortableTableBody.tsx | 59 ++ .../components/QuoteItemsSortableTableRow.tsx | 2 +- .../components/QuoteItemsTableHeader.tsx | 23 + client/src/app/quotes/components/index.ts | 2 + client/src/app/quotes/hooks/index.ts | 1 + .../hooks/useQuoteItemsSortableTable.test.tsx | 90 +++ .../hooks/useQuoteItemsSortableTable.ts | 216 ++++++ doc/capitulos-cotizaciones.md | 12 + 15 files changed, 626 insertions(+), 588 deletions(-) create mode 100644 client/src/app/quotes/components/QuoteItemsAddActions.tsx create mode 100644 client/src/app/quotes/components/QuoteItemsDragOverlay.tsx create mode 100644 client/src/app/quotes/components/QuoteItemsEmptyState.tsx create mode 100644 client/src/app/quotes/components/QuoteItemsSortableDataTable.types.ts create mode 100644 client/src/app/quotes/components/QuoteItemsSortableTableBody.tsx create mode 100644 client/src/app/quotes/components/QuoteItemsTableHeader.tsx create mode 100644 client/src/app/quotes/hooks/useQuoteItemsSortableTable.test.tsx create mode 100644 client/src/app/quotes/hooks/useQuoteItemsSortableTable.ts diff --git a/client/src/app/quotes/components/QuoteChaptersDataTable.tsx b/client/src/app/quotes/components/QuoteChaptersDataTable.tsx index cd71e5f..4cb6a85 100644 --- a/client/src/app/quotes/components/QuoteChaptersDataTable.tsx +++ b/client/src/app/quotes/components/QuoteChaptersDataTable.tsx @@ -11,12 +11,6 @@ import { DialogFooter, DialogHeader, DialogTitle, - Empty, - EmptyContent, - EmptyDescription, - EmptyHeader, - EmptyMedia, - EmptyTitle, Label, Select, SelectContent, @@ -73,14 +67,11 @@ import { import { useChapterCollapse } from "../hooks/useChapterCollapse"; import { ChapterEditorController } from "../hooks/useChapterEditor"; import { QuoteItemsInlineRowActions } from "./QuoteItemsInlineRowActions"; -import { RowIdData } from "./QuoteItemsSortableDataTable"; +import type { RowIdData } from "./QuoteItemsSortableDataTable.types"; +import { QuoteItemsEmptyState } from "./QuoteItemsEmptyState"; import { QuoteItemsSortableDataTableToolbar } from "./QuoteItemsSortableDataTableToolbar"; import { QuoteItemsSortableTableRow } from "./QuoteItemsSortableTableRow"; -import { AppendBlockRowButton } from "./AppendBlockRowButton"; -import { AppendCatalogArticleRowButton } from "./AppendCatalogArticleRowButton"; -import { AppendEmptyRowButton } from "./AppendEmptyRowButton"; - type Selection = { chapters: string[]; indexes: number[] }; const isChapter = (id: string | number) => String(id).startsWith("chapter:"); @@ -146,20 +137,14 @@ function EmptyChapterTarget({ > - - - - - {t("quotes.chapters.empty_title")} - {t("quotes.chapters.empty")} - - - - - - - + diff --git a/client/src/app/quotes/components/QuoteItemsAddActions.tsx b/client/src/app/quotes/components/QuoteItemsAddActions.tsx new file mode 100644 index 0000000..b972ee7 --- /dev/null +++ b/client/src/app/quotes/components/QuoteItemsAddActions.tsx @@ -0,0 +1,23 @@ +import type { ButtonProps } from "@/ui"; +import { AppendEmptyRowButton } from "./AppendEmptyRowButton"; +import { AppendCatalogArticleRowButton } from "./AppendCatalogArticleRowButton"; +import { AppendBlockRowButton } from "./AppendBlockRowButton"; + +export interface QuoteItemsAddActionsProps { + onAddRow: () => void; + onAddArticle: () => void; + onAddBlock: () => void; + size?: ButtonProps["size"]; +} + +export function QuoteItemsAddActions({ + onAddRow, onAddArticle, onAddBlock, size, +}: QuoteItemsAddActionsProps) { + return ( + <> + + + + + ); +} diff --git a/client/src/app/quotes/components/QuoteItemsDragOverlay.tsx b/client/src/app/quotes/components/QuoteItemsDragOverlay.tsx new file mode 100644 index 0000000..5dce586 --- /dev/null +++ b/client/src/app/quotes/components/QuoteItemsDragOverlay.tsx @@ -0,0 +1,49 @@ +import { Badge } from "@/ui"; +import { DragOverlay, defaultDropAnimation } from "@dnd-kit/core"; +import type { DropAnimation, UniqueIdentifier } from "@dnd-kit/core"; +import { CSS } from "@dnd-kit/utilities"; +import { createPortal } from "react-dom"; + +const dropAnimationConfig: DropAnimation = { + keyframes({ transform }) { + return [ + { opacity: 1, transform: CSS.Transform.toString(transform.initial) }, + { + opacity: 0, + transform: CSS.Transform.toString({ + ...transform.final, + x: transform.final.x + 5, + y: transform.final.y + 5, + }), + }, + ]; + }, + easing: "ease-out", + sideEffects({ active }) { + active.node.animate([{ opacity: 0 }, { opacity: 1 }], { + duration: defaultDropAnimation.duration, + easing: defaultDropAnimation.easing, + }); + }, +}; + + +export function QuoteItemsDragOverlay({ activeId, selectedCount }: { + activeId: UniqueIdentifier | null | undefined; + selectedCount: number; +}) { + return createPortal( + + {activeId && ( +
+ {selectedCount > 0 && ( + + {selectedCount} + + )} +
+ )} +
, + document.body + ); +} diff --git a/client/src/app/quotes/components/QuoteItemsEmptyState.tsx b/client/src/app/quotes/components/QuoteItemsEmptyState.tsx new file mode 100644 index 0000000..26715a9 --- /dev/null +++ b/client/src/app/quotes/components/QuoteItemsEmptyState.tsx @@ -0,0 +1,32 @@ +import { cn } from "@/lib/utils"; +import { + Empty, EmptyHeader, EmptyMedia, EmptyTitle, EmptyDescription, EmptyContent, +} from "@/ui"; +import { ListPlusIcon } from "lucide-react"; +import type { ReactNode } from "react"; +import { QuoteItemsAddActions } from "./QuoteItemsAddActions"; +import type { QuoteItemsAddActionsProps } from "./QuoteItemsAddActions"; + +interface QuoteItemsEmptyStateProps extends Omit { + title: ReactNode; + description: ReactNode; + icon?: ReactNode; + className?: string; +} + +export function QuoteItemsEmptyState({ + title, description, icon =