From dd5add04daf255b9ead678f04c11bcfa7139d000 Mon Sep 17 00:00:00 2001 From: buddhsen-tripathi Date: Wed, 20 May 2026 04:37:04 -0400 Subject: [PATCH] feat: implement reorder functionality for invoice items --- .../invoiceHelpers/invoice-items-section.tsx | 45 ++++++++++++++++--- 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/apps/web/src/app/(dashboard)/create/invoice/invoiceHelpers/invoice-items-section.tsx b/apps/web/src/app/(dashboard)/create/invoice/invoiceHelpers/invoice-items-section.tsx index beda940..0807ed7 100644 --- a/apps/web/src/app/(dashboard)/create/invoice/invoiceHelpers/invoice-items-section.tsx +++ b/apps/web/src/app/(dashboard)/create/invoice/invoiceHelpers/invoice-items-section.tsx @@ -16,13 +16,14 @@ import { import { createInvoiceItemSchema, ZodCreateInvoiceSchema } from "@/zod-schemas/invoice/create-invoice"; import { useFieldArray, useForm, UseFormReturn } from "react-hook-form"; import { BoxIcon, BoxPlusIcon, TrashIcon } from "@/assets/icons"; +import { Reorder } from "motion/react"; import { FormInput } from "@/components/ui/form/form-input"; import { formatCurrencyText } from "@/constants/currency"; import { zodResolver } from "@hookform/resolvers/zod"; import FormRow from "@/components/ui/form/form-row"; import { Form } from "@/components/ui/form/form"; import { Button } from "@/components/ui/button"; -import { PencilIcon } from "lucide-react"; +import { GripVerticalIcon, PencilIcon } from "lucide-react"; import React, { useState } from "react"; interface InvoiceItemsSectionProps { @@ -31,19 +32,37 @@ interface InvoiceItemsSectionProps { type InvoiceItem = ZodCreateInvoiceSchema["items"][number]; const InvoiceItemsSection: React.FC = ({ form }) => { - const { fields, append, remove, update } = useFieldArray({ + const { fields, append, remove, update, move } = useFieldArray({ control: form.control, name: "items", }); + const onReorder = (newIds: string[]) => { + const oldIds = fields.map((field) => field.id); + const reorder = getSingleMove(oldIds, newIds); + if (reorder) move(reorder.from, reorder.to); + }; + return (
{/* Rendering the items */} {fields.length > 0 && ( -
+ field.id)} + onReorder={onReorder} + className="flex flex-col gap-2" + > {fields.map((field, index) => ( -
+
+
+ +
@@ -88,9 +107,9 @@ const InvoiceItemsSection: React.FC = ({ form }) => {
-
+ ))} - + )} {/* Dialog for adding a new item */} @@ -105,6 +124,20 @@ const InvoiceItemsSection: React.FC = ({ form }) => { export default InvoiceItemsSection; +// Derives a single move(from, to) from the old vs new id ordering produced by a drag. +const getSingleMove = (oldIds: string[], newIds: string[]): { from: number; to: number } | null => { + let start = 0; + while (start < oldIds.length && oldIds[start] === newIds[start]) start++; + + let end = oldIds.length - 1; + while (end >= 0 && oldIds[end] === newIds[end]) end--; + + if (start > end) return null; + + // Forward move if the element at `start` slid down to `end`, otherwise backward. + return oldIds[start] === newIds[end] ? { from: start, to: end } : { from: end, to: start }; +}; + interface AddItemModalProps { type: "add" | "edit"; children: React.ReactNode;