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
This commit is contained in:
1 parent
51b0af193c
commit
c28ae573e5
7 files changed
+103
-39
No files matched your search
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/dashboard": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix(dashboard): Use derrived state in DataTable
|
||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
useDataTable,
|
useDataTable,
|
||||||
} from "@medusajs/ui"
|
} from "@medusajs/ui"
|
||||||
import React, { ReactNode, useCallback, useState } from "react"
|
import React, { ReactNode, useCallback, useMemo } from "react"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { Link, useNavigate, useSearchParams } from "react-router-dom"
|
import { Link, useNavigate, useSearchParams } from "react-router-dom"
|
||||||
|
|
||||||
@@ -110,7 +110,7 @@ export const DataTable = <TData,>({
|
|||||||
const enableCommands = commands && commands.length > 0
|
const enableCommands = commands && commands.length > 0
|
||||||
const enableSorting = columns.some((column) => column.enableSorting)
|
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 prefixedFilterIds = filterIds.map((id) => getQueryParamKey(id, prefix))
|
||||||
|
|
||||||
const { offset, order, q, ...filterParams } = useQueryParams(
|
const { offset, order, q, ...filterParams } = useQueryParams(
|
||||||
@@ -124,9 +124,11 @@ export const DataTable = <TData,>({
|
|||||||
)
|
)
|
||||||
const [_, setSearchParams] = useSearchParams()
|
const [_, setSearchParams] = useSearchParams()
|
||||||
|
|
||||||
const [search, setSearch] = useState<string>(q ?? "")
|
const search = useMemo(() => {
|
||||||
|
return q ?? ""
|
||||||
|
}, [q])
|
||||||
|
|
||||||
const handleSearchChange = (value: string) => {
|
const handleSearchChange = (value: string) => {
|
||||||
setSearch(value)
|
|
||||||
setSearchParams((prev) => {
|
setSearchParams((prev) => {
|
||||||
if (value) {
|
if (value) {
|
||||||
prev.set(getQueryParamKey("q", prefix), value)
|
prev.set(getQueryParamKey("q", prefix), value)
|
||||||
@@ -138,11 +140,13 @@ export const DataTable = <TData,>({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const [pagination, setPagination] = useState<DataTablePaginationState>(
|
const pagination: DataTablePaginationState = useMemo(() => {
|
||||||
offset ? parsePaginationState(offset, pageSize) : { pageIndex: 0, pageSize }
|
return offset
|
||||||
)
|
? parsePaginationState(offset, pageSize)
|
||||||
|
: { pageIndex: 0, pageSize }
|
||||||
|
}, [offset, pageSize])
|
||||||
|
|
||||||
const handlePaginationChange = (value: DataTablePaginationState) => {
|
const handlePaginationChange = (value: DataTablePaginationState) => {
|
||||||
setPagination(value)
|
|
||||||
setSearchParams((prev) => {
|
setSearchParams((prev) => {
|
||||||
if (value.pageIndex === 0) {
|
if (value.pageIndex === 0) {
|
||||||
prev.delete(getQueryParamKey("offset", prefix))
|
prev.delete(getQueryParamKey("offset", prefix))
|
||||||
@@ -152,18 +156,16 @@ export const DataTable = <TData,>({
|
|||||||
transformPaginationState(value).toString()
|
transformPaginationState(value).toString()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return prev
|
return prev
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const [filtering, setFiltering] = useState<DataTableFilteringState>(
|
const filtering: DataTableFilteringState = useMemo(
|
||||||
parseFilterState(filterIds, filterParams)
|
() => parseFilterState(filterIds, filterParams),
|
||||||
|
[filterIds, filterParams]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleFilteringChange = (value: DataTableFilteringState) => {
|
const handleFilteringChange = (value: DataTableFilteringState) => {
|
||||||
setFiltering(value)
|
|
||||||
|
|
||||||
setSearchParams((prev) => {
|
setSearchParams((prev) => {
|
||||||
Array.from(prev.keys()).forEach((key) => {
|
Array.from(prev.keys()).forEach((key) => {
|
||||||
if (prefixedFilterIds.includes(key) && !(key in value)) {
|
if (prefixedFilterIds.includes(key) && !(key in value)) {
|
||||||
@@ -184,11 +186,11 @@ export const DataTable = <TData,>({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const [sorting, setSorting] = useState<DataTableSortingState | null>(
|
const sorting: DataTableSortingState | null = useMemo(() => {
|
||||||
order ? parseSortingState(order) : null
|
return order ? parseSortingState(order) : null
|
||||||
)
|
}, [order])
|
||||||
|
|
||||||
const handleSortingChange = (value: DataTableSortingState) => {
|
const handleSortingChange = (value: DataTableSortingState) => {
|
||||||
setSorting(value)
|
|
||||||
setSearchParams((prev) => {
|
setSearchParams((prev) => {
|
||||||
if (value) {
|
if (value) {
|
||||||
const valueToStore = transformSortingState(value)
|
const valueToStore = transformSortingState(value)
|
||||||
|
|||||||
@@ -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<Path>) => {
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
import { Drawer, clx } from "@medusajs/ui"
|
import { Drawer, clx } from "@medusajs/ui"
|
||||||
import { PropsWithChildren, useEffect, useState } from "react"
|
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 { RouteModalForm } from "../route-modal-form"
|
||||||
import { RouteModalProvider } from "../route-modal-provider/route-provider"
|
import { RouteModalProvider } from "../route-modal-provider/route-provider"
|
||||||
import { StackedModalProvider } from "../stacked-modal-provider"
|
import { StackedModalProvider } from "../stacked-modal-provider"
|
||||||
|
|
||||||
type RouteDrawerProps = PropsWithChildren<{
|
type RouteDrawerProps = PropsWithChildren<{
|
||||||
prev?: string
|
prev?: string | Partial<Path>
|
||||||
}>
|
}>
|
||||||
|
|
||||||
const Root = ({ prev = "..", children }: RouteDrawerProps) => {
|
const Root = ({ prev = "..", children }: RouteDrawerProps) => {
|
||||||
@@ -14,6 +15,8 @@ const Root = ({ prev = "..", children }: RouteDrawerProps) => {
|
|||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
const [stackedModalOpen, onStackedModalOpen] = useState(false)
|
const [stackedModalOpen, onStackedModalOpen] = useState(false)
|
||||||
|
|
||||||
|
const to = useStateAwareTo(prev)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Open the modal when the component mounts. This
|
* Open the modal when the component mounts. This
|
||||||
* ensures that the entry animation is played.
|
* ensures that the entry animation is played.
|
||||||
@@ -30,7 +33,7 @@ const Root = ({ prev = "..", children }: RouteDrawerProps) => {
|
|||||||
const handleOpenChange = (open: boolean) => {
|
const handleOpenChange = (open: boolean) => {
|
||||||
if (!open) {
|
if (!open) {
|
||||||
document.body.style.pointerEvents = "auto"
|
document.body.style.pointerEvents = "auto"
|
||||||
navigate(prev, { replace: true })
|
navigate(to, { replace: true })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,7 +42,7 @@ const Root = ({ prev = "..", children }: RouteDrawerProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer open={open} onOpenChange={handleOpenChange}>
|
<Drawer open={open} onOpenChange={handleOpenChange}>
|
||||||
<RouteModalProvider prev={prev}>
|
<RouteModalProvider prev={to}>
|
||||||
<StackedModalProvider onOpenChange={onStackedModalOpen}>
|
<StackedModalProvider onOpenChange={onStackedModalOpen}>
|
||||||
<Drawer.Content
|
<Drawer.Content
|
||||||
aria-describedby={undefined}
|
aria-describedby={undefined}
|
||||||
|
|||||||
+7
-4
@@ -1,13 +1,14 @@
|
|||||||
import { FocusModal, clx } from "@medusajs/ui"
|
import { FocusModal, clx } from "@medusajs/ui"
|
||||||
import { PropsWithChildren, useEffect, useState } from "react"
|
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 { RouteModalForm } from "../route-modal-form"
|
||||||
import { useRouteModal } from "../route-modal-provider"
|
import { useRouteModal } from "../route-modal-provider"
|
||||||
import { RouteModalProvider } from "../route-modal-provider/route-provider"
|
import { RouteModalProvider } from "../route-modal-provider/route-provider"
|
||||||
import { StackedModalProvider } from "../stacked-modal-provider"
|
import { StackedModalProvider } from "../stacked-modal-provider"
|
||||||
|
|
||||||
type RouteFocusModalProps = PropsWithChildren<{
|
type RouteFocusModalProps = PropsWithChildren<{
|
||||||
prev?: string
|
prev?: string | Partial<Path>
|
||||||
}>
|
}>
|
||||||
|
|
||||||
const Root = ({ prev = "..", children }: RouteFocusModalProps) => {
|
const Root = ({ prev = "..", children }: RouteFocusModalProps) => {
|
||||||
@@ -15,6 +16,8 @@ const Root = ({ prev = "..", children }: RouteFocusModalProps) => {
|
|||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
const [stackedModalOpen, onStackedModalOpen] = useState(false)
|
const [stackedModalOpen, onStackedModalOpen] = useState(false)
|
||||||
|
|
||||||
|
const to = useStateAwareTo(prev)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Open the modal when the component mounts. This
|
* Open the modal when the component mounts. This
|
||||||
* ensures that the entry animation is played.
|
* ensures that the entry animation is played.
|
||||||
@@ -31,7 +34,7 @@ const Root = ({ prev = "..", children }: RouteFocusModalProps) => {
|
|||||||
const handleOpenChange = (open: boolean) => {
|
const handleOpenChange = (open: boolean) => {
|
||||||
if (!open) {
|
if (!open) {
|
||||||
document.body.style.pointerEvents = "auto"
|
document.body.style.pointerEvents = "auto"
|
||||||
navigate(prev, { replace: true })
|
navigate(to, { replace: true })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,7 +43,7 @@ const Root = ({ prev = "..", children }: RouteFocusModalProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<FocusModal open={open} onOpenChange={handleOpenChange}>
|
<FocusModal open={open} onOpenChange={handleOpenChange}>
|
||||||
<RouteModalProvider prev={prev}>
|
<RouteModalProvider prev={to}>
|
||||||
<StackedModalProvider onOpenChange={onStackedModalOpen}>
|
<StackedModalProvider onOpenChange={onStackedModalOpen}>
|
||||||
<Content stackedModalOpen={stackedModalOpen}>{children}</Content>
|
<Content stackedModalOpen={stackedModalOpen}>{children}</Content>
|
||||||
</StackedModalProvider>
|
</StackedModalProvider>
|
||||||
|
|||||||
+2
-2
@@ -1,9 +1,9 @@
|
|||||||
import { PropsWithChildren, useCallback, useMemo, useState } from "react"
|
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"
|
import { RouteModalProviderContext } from "./route-modal-context"
|
||||||
|
|
||||||
type RouteModalProviderProps = PropsWithChildren<{
|
type RouteModalProviderProps = PropsWithChildren<{
|
||||||
prev: string
|
prev: string | Partial<Path>
|
||||||
}>
|
}>
|
||||||
|
|
||||||
export const RouteModalProvider = ({
|
export const RouteModalProvider = ({
|
||||||
|
|||||||
+37
-12
@@ -16,7 +16,7 @@ import { useCallback, useMemo } from "react"
|
|||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
import { CellContext } from "@tanstack/react-table"
|
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 { DataTable } from "../../../../../components/data-table"
|
||||||
import { useDataTableDateColumns } from "../../../../../components/data-table/helpers/general/use-data-table-date-columns"
|
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"
|
import { useDataTableDateFilters } from "../../../../../components/data-table/helpers/general/use-data-table-date-filters"
|
||||||
@@ -32,6 +32,7 @@ type ProductVariantSectionProps = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const PAGE_SIZE = 10
|
const PAGE_SIZE = 10
|
||||||
|
const PREFIX = "pv"
|
||||||
|
|
||||||
export const ProductVariantSection = ({
|
export const ProductVariantSection = ({
|
||||||
product,
|
product,
|
||||||
@@ -46,15 +47,18 @@ export const ProductVariantSection = ({
|
|||||||
manage_inventory,
|
manage_inventory,
|
||||||
created_at,
|
created_at,
|
||||||
updated_at,
|
updated_at,
|
||||||
} = useQueryParams([
|
} = useQueryParams(
|
||||||
"q",
|
[
|
||||||
"order",
|
"q",
|
||||||
"offset",
|
"order",
|
||||||
"manage_inventory",
|
"offset",
|
||||||
"allow_backorder",
|
"manage_inventory",
|
||||||
"created_at",
|
"allow_backorder",
|
||||||
"updated_at",
|
"created_at",
|
||||||
])
|
"updated_at",
|
||||||
|
],
|
||||||
|
PREFIX
|
||||||
|
)
|
||||||
|
|
||||||
const columns = useColumns(product)
|
const columns = useColumns(product)
|
||||||
const filters = useFilters()
|
const filters = useFilters()
|
||||||
@@ -132,6 +136,7 @@ export const ProductVariantSection = ({
|
|||||||
],
|
],
|
||||||
}}
|
}}
|
||||||
commands={commands}
|
commands={commands}
|
||||||
|
prefix={PREFIX}
|
||||||
/>
|
/>
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
@@ -145,6 +150,17 @@ const useColumns = (product: HttpTypes.AdminProduct) => {
|
|||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { mutateAsync } = useDeleteVariantLazy(product.id)
|
const { mutateAsync } = useDeleteVariantLazy(product.id)
|
||||||
const prompt = usePrompt()
|
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<HttpTypes.AdminProductVariant>()
|
const dateColumns = useDataTableDateColumns<HttpTypes.AdminProductVariant>()
|
||||||
|
|
||||||
@@ -215,7 +231,16 @@ const useColumns = (product: HttpTypes.AdminProduct) => {
|
|||||||
icon: <PencilSquare />,
|
icon: <PencilSquare />,
|
||||||
label: t("actions.edit"),
|
label: t("actions.edit"),
|
||||||
onClick: (row) => {
|
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]
|
return [mainActions, secondaryActions]
|
||||||
},
|
},
|
||||||
[handleDelete, navigate, t]
|
[handleDelete, navigate, t, tableSearchParams]
|
||||||
)
|
)
|
||||||
|
|
||||||
const getInventory = useCallback(
|
const getInventory = useCallback(
|
||||||
|
|||||||
Reference in new issue
Block a user