From c28ae573e50b51bd3ef18de7c0e37b37963c423e Mon Sep 17 00:00:00 2001 From: Kasper Fabricius Kristensen <45367945+kasperkristensen@users.noreply.github.com> Date: Mon, 3 Mar 2025 15:18:19 +0100 Subject: [PATCH] fix(dashboard): Use derived state in DataTable (#11487) **What** - Uses derived state in DataTable, to prevent the state in the URL and component from going out of sync. - Introduces a way for RouteModals to restore URL params on close. Resolves CMRC-936 --- .changeset/breezy-bananas-kick.md | 5 ++ .../src/components/data-table/data-table.tsx | 36 +++++++------- .../modals/hooks/use-state-aware-to.tsx | 26 ++++++++++ .../modals/route-drawer/route-drawer.tsx | 11 +++-- .../route-focus-modal/route-focus-modal.tsx | 11 +++-- .../route-modal-provider/route-provider.tsx | 4 +- .../product-variant-section.tsx | 49 ++++++++++++++----- 7 files changed, 103 insertions(+), 39 deletions(-) create mode 100644 .changeset/breezy-bananas-kick.md create mode 100644 packages/admin/dashboard/src/components/modals/hooks/use-state-aware-to.tsx diff --git a/.changeset/breezy-bananas-kick.md b/.changeset/breezy-bananas-kick.md new file mode 100644 index 0000000000000..ac0967be62f55 --- /dev/null +++ b/.changeset/breezy-bananas-kick.md @@ -0,0 +1,5 @@ +--- +"@medusajs/dashboard": patch +--- + +fix(dashboard): Use derrived state in DataTable diff --git a/packages/admin/dashboard/src/components/data-table/data-table.tsx b/packages/admin/dashboard/src/components/data-table/data-table.tsx index 13fe49c5f1a25..da207ae0ec181 100644 --- a/packages/admin/dashboard/src/components/data-table/data-table.tsx +++ b/packages/admin/dashboard/src/components/data-table/data-table.tsx @@ -15,7 +15,7 @@ import { Text, useDataTable, } from "@medusajs/ui" -import React, { ReactNode, useCallback, useState } from "react" +import React, { ReactNode, useCallback, useMemo } from "react" import { useTranslation } from "react-i18next" import { Link, useNavigate, useSearchParams } from "react-router-dom" @@ -110,7 +110,7 @@ export const DataTable = ({ const enableCommands = commands && commands.length > 0 const enableSorting = columns.some((column) => column.enableSorting) - const filterIds = filters?.map((f) => f.id) ?? [] + const filterIds = useMemo(() => filters?.map((f) => f.id) ?? [], [filters]) const prefixedFilterIds = filterIds.map((id) => getQueryParamKey(id, prefix)) const { offset, order, q, ...filterParams } = useQueryParams( @@ -124,9 +124,11 @@ export const DataTable = ({ ) const [_, setSearchParams] = useSearchParams() - const [search, setSearch] = useState(q ?? "") + const search = useMemo(() => { + return q ?? "" + }, [q]) + const handleSearchChange = (value: string) => { - setSearch(value) setSearchParams((prev) => { if (value) { prev.set(getQueryParamKey("q", prefix), value) @@ -138,11 +140,13 @@ export const DataTable = ({ }) } - const [pagination, setPagination] = useState( - offset ? parsePaginationState(offset, pageSize) : { pageIndex: 0, pageSize } - ) + const pagination: DataTablePaginationState = useMemo(() => { + return offset + ? parsePaginationState(offset, pageSize) + : { pageIndex: 0, pageSize } + }, [offset, pageSize]) + const handlePaginationChange = (value: DataTablePaginationState) => { - setPagination(value) setSearchParams((prev) => { if (value.pageIndex === 0) { prev.delete(getQueryParamKey("offset", prefix)) @@ -152,18 +156,16 @@ export const DataTable = ({ transformPaginationState(value).toString() ) } - return prev }) } - const [filtering, setFiltering] = useState( - parseFilterState(filterIds, filterParams) + const filtering: DataTableFilteringState = useMemo( + () => parseFilterState(filterIds, filterParams), + [filterIds, filterParams] ) const handleFilteringChange = (value: DataTableFilteringState) => { - setFiltering(value) - setSearchParams((prev) => { Array.from(prev.keys()).forEach((key) => { if (prefixedFilterIds.includes(key) && !(key in value)) { @@ -184,11 +186,11 @@ export const DataTable = ({ }) } - const [sorting, setSorting] = useState( - order ? parseSortingState(order) : null - ) + const sorting: DataTableSortingState | null = useMemo(() => { + return order ? parseSortingState(order) : null + }, [order]) + const handleSortingChange = (value: DataTableSortingState) => { - setSorting(value) setSearchParams((prev) => { if (value) { const valueToStore = transformSortingState(value) diff --git a/packages/admin/dashboard/src/components/modals/hooks/use-state-aware-to.tsx b/packages/admin/dashboard/src/components/modals/hooks/use-state-aware-to.tsx new file mode 100644 index 0000000000000..53c6914293ef2 --- /dev/null +++ b/packages/admin/dashboard/src/components/modals/hooks/use-state-aware-to.tsx @@ -0,0 +1,26 @@ +import { useMemo } from "react" +import { Path, useLocation } from "react-router-dom" + +/** + * Checks if the current location has a restore_params property. + * If it does, it will return a new path with the params added to it. + * Otherwise, it will return the previous path. + * + * This is useful if the modal needs to return to the original path, with + * the params that were present when the modal was opened. + */ +export const useStateAwareTo = (prev: string | Partial) => { + const location = useLocation() + + const to = useMemo(() => { + const params = location.state?.restore_params + + if (!params) { + return prev + } + + return `${prev}?${params.toString()}` + }, [location.state, prev]) + + return to +} diff --git a/packages/admin/dashboard/src/components/modals/route-drawer/route-drawer.tsx b/packages/admin/dashboard/src/components/modals/route-drawer/route-drawer.tsx index 645ab137d3fe1..3de85342b0a45 100644 --- a/packages/admin/dashboard/src/components/modals/route-drawer/route-drawer.tsx +++ b/packages/admin/dashboard/src/components/modals/route-drawer/route-drawer.tsx @@ -1,12 +1,13 @@ import { Drawer, clx } from "@medusajs/ui" import { PropsWithChildren, useEffect, useState } from "react" -import { useNavigate } from "react-router-dom" +import { Path, useNavigate } from "react-router-dom" +import { useStateAwareTo } from "../hooks/use-state-aware-to" import { RouteModalForm } from "../route-modal-form" import { RouteModalProvider } from "../route-modal-provider/route-provider" import { StackedModalProvider } from "../stacked-modal-provider" type RouteDrawerProps = PropsWithChildren<{ - prev?: string + prev?: string | Partial }> const Root = ({ prev = "..", children }: RouteDrawerProps) => { @@ -14,6 +15,8 @@ const Root = ({ prev = "..", children }: RouteDrawerProps) => { const [open, setOpen] = useState(false) const [stackedModalOpen, onStackedModalOpen] = useState(false) + const to = useStateAwareTo(prev) + /** * Open the modal when the component mounts. This * ensures that the entry animation is played. @@ -30,7 +33,7 @@ const Root = ({ prev = "..", children }: RouteDrawerProps) => { const handleOpenChange = (open: boolean) => { if (!open) { document.body.style.pointerEvents = "auto" - navigate(prev, { replace: true }) + navigate(to, { replace: true }) return } @@ -39,7 +42,7 @@ const Root = ({ prev = "..", children }: RouteDrawerProps) => { return ( - + }> const Root = ({ prev = "..", children }: RouteFocusModalProps) => { @@ -15,6 +16,8 @@ const Root = ({ prev = "..", children }: RouteFocusModalProps) => { const [open, setOpen] = useState(false) const [stackedModalOpen, onStackedModalOpen] = useState(false) + const to = useStateAwareTo(prev) + /** * Open the modal when the component mounts. This * ensures that the entry animation is played. @@ -31,7 +34,7 @@ const Root = ({ prev = "..", children }: RouteFocusModalProps) => { const handleOpenChange = (open: boolean) => { if (!open) { document.body.style.pointerEvents = "auto" - navigate(prev, { replace: true }) + navigate(to, { replace: true }) return } @@ -40,7 +43,7 @@ const Root = ({ prev = "..", children }: RouteFocusModalProps) => { return ( - + {children} diff --git a/packages/admin/dashboard/src/components/modals/route-modal-provider/route-provider.tsx b/packages/admin/dashboard/src/components/modals/route-modal-provider/route-provider.tsx index 39da2eb03f1e5..44e51398bc8dd 100644 --- a/packages/admin/dashboard/src/components/modals/route-modal-provider/route-provider.tsx +++ b/packages/admin/dashboard/src/components/modals/route-modal-provider/route-provider.tsx @@ -1,9 +1,9 @@ import { PropsWithChildren, useCallback, useMemo, useState } from "react" -import { useNavigate } from "react-router-dom" +import { Path, useNavigate } from "react-router-dom" import { RouteModalProviderContext } from "./route-modal-context" type RouteModalProviderProps = PropsWithChildren<{ - prev: string + prev: string | Partial }> export const RouteModalProvider = ({ diff --git a/packages/admin/dashboard/src/routes/products/product-detail/components/product-variant-section/product-variant-section.tsx b/packages/admin/dashboard/src/routes/products/product-detail/components/product-variant-section/product-variant-section.tsx index 6ed32a7d79213..670b0420d9a41 100644 --- a/packages/admin/dashboard/src/routes/products/product-detail/components/product-variant-section/product-variant-section.tsx +++ b/packages/admin/dashboard/src/routes/products/product-detail/components/product-variant-section/product-variant-section.tsx @@ -16,7 +16,7 @@ import { useCallback, useMemo } from "react" import { useTranslation } from "react-i18next" import { CellContext } from "@tanstack/react-table" -import { useNavigate } from "react-router-dom" +import { useNavigate, useSearchParams } from "react-router-dom" import { DataTable } from "../../../../../components/data-table" import { useDataTableDateColumns } from "../../../../../components/data-table/helpers/general/use-data-table-date-columns" import { useDataTableDateFilters } from "../../../../../components/data-table/helpers/general/use-data-table-date-filters" @@ -32,6 +32,7 @@ type ProductVariantSectionProps = { } const PAGE_SIZE = 10 +const PREFIX = "pv" export const ProductVariantSection = ({ product, @@ -46,15 +47,18 @@ export const ProductVariantSection = ({ manage_inventory, created_at, updated_at, - } = useQueryParams([ - "q", - "order", - "offset", - "manage_inventory", - "allow_backorder", - "created_at", - "updated_at", - ]) + } = useQueryParams( + [ + "q", + "order", + "offset", + "manage_inventory", + "allow_backorder", + "created_at", + "updated_at", + ], + PREFIX + ) const columns = useColumns(product) const filters = useFilters() @@ -132,6 +136,7 @@ export const ProductVariantSection = ({ ], }} commands={commands} + prefix={PREFIX} /> ) @@ -145,6 +150,17 @@ const useColumns = (product: HttpTypes.AdminProduct) => { const navigate = useNavigate() const { mutateAsync } = useDeleteVariantLazy(product.id) const prompt = usePrompt() + const [searchParams] = useSearchParams() + + const tableSearchParams = useMemo(() => { + const filtered = new URLSearchParams() + for (const [key, value] of searchParams.entries()) { + if (key.startsWith(`${PREFIX}_`)) { + filtered.append(key, value) + } + } + return filtered + }, [searchParams]) const dateColumns = useDataTableDateColumns() @@ -215,7 +231,16 @@ const useColumns = (product: HttpTypes.AdminProduct) => { icon: , label: t("actions.edit"), onClick: (row) => { - navigate(`edit-variant?variant_id=${row.row.original.id}`) + navigate( + `edit-variant?variant_id=${ + row.row.original.id + }&${tableSearchParams.toString()}`, + { + state: { + restore_params: tableSearchParams.toString(), + }, + } + ) }, }, ] @@ -271,7 +296,7 @@ const useColumns = (product: HttpTypes.AdminProduct) => { return [mainActions, secondaryActions] }, - [handleDelete, navigate, t] + [handleDelete, navigate, t, tableSearchParams] ) const getInventory = useCallback(