feat(dashboard): refactor location list UI to use data table (#13571)
* wip: convert location list to a table * chore: changeset * fix: rm search bluring on loading change * feat: translations and palceholders, cleanup, make content more compact * fix: delete message * chore: optimise use memo * fix: update toast * feat: make stock location address searchable * fix: search input blur on load finish
This commit is contained in:
5
.changeset/weak-candles-melt.md
Normal file
5
.changeset/weak-candles-melt.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@medusajs/dashboard": patch
|
||||
---
|
||||
|
||||
feat(dashboard): refactor location list UI to use data table
|
||||
@@ -31,26 +31,26 @@ type DataTableActionProps = {
|
||||
label: string
|
||||
disabled?: boolean
|
||||
} & (
|
||||
| {
|
||||
| {
|
||||
to: string
|
||||
}
|
||||
| {
|
||||
| {
|
||||
onClick: () => void
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
type DataTableActionMenuActionProps = {
|
||||
label: string
|
||||
icon: ReactNode
|
||||
disabled?: boolean
|
||||
} & (
|
||||
| {
|
||||
| {
|
||||
to: string
|
||||
}
|
||||
| {
|
||||
| {
|
||||
onClick: () => void
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
type DataTableActionMenuGroupProps = {
|
||||
actions: DataTableActionMenuActionProps[]
|
||||
@@ -138,15 +138,18 @@ export const DataTable = <TData,>({
|
||||
const isViewConfigEnabled = useFeatureFlag("view_configurations")
|
||||
|
||||
// If view config is disabled, don't use column visibility features
|
||||
const effectiveEnableColumnVisibility = isViewConfigEnabled && enableColumnVisibility
|
||||
const effectiveEnableColumnVisibility =
|
||||
isViewConfigEnabled && enableColumnVisibility
|
||||
const effectiveEnableViewSelector = isViewConfigEnabled && enableViewSelector
|
||||
|
||||
const enableFiltering = filters && filters.length > 0
|
||||
const showFilterMenu = enableFilterMenu !== undefined ? enableFilterMenu : enableFiltering
|
||||
const showFilterMenu =
|
||||
enableFilterMenu !== undefined ? enableFilterMenu : enableFiltering
|
||||
const enableCommands = commands && commands.length > 0
|
||||
const enableSorting = columns.some((column) => column.enableSorting)
|
||||
|
||||
const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>(initialColumnVisibility)
|
||||
const [columnVisibility, setColumnVisibility] =
|
||||
React.useState<VisibilityState>(initialColumnVisibility)
|
||||
|
||||
// Update column visibility when initial visibility changes
|
||||
React.useEffect(() => {
|
||||
@@ -154,9 +157,12 @@ export const DataTable = <TData,>({
|
||||
const currentKeys = Object.keys(columnVisibility).sort()
|
||||
const newKeys = Object.keys(initialColumnVisibility).sort()
|
||||
|
||||
const hasChanged = currentKeys.length !== newKeys.length ||
|
||||
const hasChanged =
|
||||
currentKeys.length !== newKeys.length ||
|
||||
currentKeys.some((key, index) => key !== newKeys[index]) ||
|
||||
Object.entries(initialColumnVisibility).some(([key, value]) => columnVisibility[key] !== value)
|
||||
Object.entries(initialColumnVisibility).some(
|
||||
([key, value]) => columnVisibility[key] !== value
|
||||
)
|
||||
|
||||
if (hasChanged) {
|
||||
setColumnVisibility(initialColumnVisibility)
|
||||
@@ -164,10 +170,13 @@ export const DataTable = <TData,>({
|
||||
}, [initialColumnVisibility])
|
||||
|
||||
// Wrapper function to handle column visibility changes
|
||||
const handleColumnVisibilityChange = React.useCallback((visibility: VisibilityState) => {
|
||||
setColumnVisibility(visibility)
|
||||
onColumnVisibilityChange?.(visibility)
|
||||
}, [onColumnVisibilityChange])
|
||||
const handleColumnVisibilityChange = React.useCallback(
|
||||
(visibility: VisibilityState) => {
|
||||
setColumnVisibility(visibility)
|
||||
onColumnVisibilityChange?.(visibility)
|
||||
},
|
||||
[onColumnVisibilityChange]
|
||||
)
|
||||
|
||||
// Extract filter IDs for query param management
|
||||
const filterIds = useMemo(() => filters?.map((f) => f.id) ?? [], [filters])
|
||||
@@ -231,7 +240,7 @@ export const DataTable = <TData,>({
|
||||
Array.from(prev.keys()).forEach((key) => {
|
||||
if (prefixedFilterIds.includes(key)) {
|
||||
// Extract the unprefixed key
|
||||
const unprefixedKey = prefix ? key.replace(`${prefix}_`, '') : key
|
||||
const unprefixedKey = prefix ? key.replace(`${prefix}_`, "") : key
|
||||
if (!(unprefixedKey in value)) {
|
||||
prev.delete(key)
|
||||
}
|
||||
@@ -257,11 +266,14 @@ export const DataTable = <TData,>({
|
||||
}, [order])
|
||||
|
||||
// Memoize current configuration to prevent infinite loops
|
||||
const currentConfiguration = useMemo(() => ({
|
||||
filters: filtering,
|
||||
sorting: sorting,
|
||||
search: search,
|
||||
}), [filtering, sorting, search])
|
||||
const currentConfiguration = useMemo(
|
||||
() => ({
|
||||
filters: filtering,
|
||||
sorting: sorting,
|
||||
search: search,
|
||||
}),
|
||||
[filtering, sorting, search]
|
||||
)
|
||||
|
||||
const handleSortingChange = (value: DataTableSortingState) => {
|
||||
setSearchParams((prev) => {
|
||||
@@ -315,42 +327,43 @@ export const DataTable = <TData,>({
|
||||
onRowClick: rowHref ? onRowClick : undefined,
|
||||
pagination: enablePagination
|
||||
? {
|
||||
state: pagination,
|
||||
onPaginationChange: handlePaginationChange,
|
||||
}
|
||||
state: pagination,
|
||||
onPaginationChange: handlePaginationChange,
|
||||
}
|
||||
: undefined,
|
||||
filtering: enableFiltering
|
||||
? {
|
||||
state: filtering,
|
||||
onFilteringChange: handleFilteringChange,
|
||||
}
|
||||
state: filtering,
|
||||
onFilteringChange: handleFilteringChange,
|
||||
}
|
||||
: undefined,
|
||||
sorting: enableSorting
|
||||
? {
|
||||
state: sorting,
|
||||
onSortingChange: handleSortingChange,
|
||||
}
|
||||
state: sorting,
|
||||
onSortingChange: handleSortingChange,
|
||||
}
|
||||
: undefined,
|
||||
search: enableSearch
|
||||
? {
|
||||
state: search,
|
||||
onSearchChange: handleSearchChange,
|
||||
}
|
||||
state: search,
|
||||
onSearchChange: handleSearchChange,
|
||||
}
|
||||
: undefined,
|
||||
rowSelection,
|
||||
isLoading,
|
||||
columnVisibility: effectiveEnableColumnVisibility
|
||||
? {
|
||||
state: columnVisibility,
|
||||
onColumnVisibilityChange: handleColumnVisibilityChange,
|
||||
}
|
||||
: undefined,
|
||||
columnOrder: effectiveEnableColumnVisibility && columnOrder && onColumnOrderChange
|
||||
? {
|
||||
state: columnOrder,
|
||||
onColumnOrderChange: onColumnOrderChange,
|
||||
}
|
||||
state: columnVisibility,
|
||||
onColumnVisibilityChange: handleColumnVisibilityChange,
|
||||
}
|
||||
: undefined,
|
||||
columnOrder:
|
||||
effectiveEnableColumnVisibility && columnOrder && onColumnOrderChange
|
||||
? {
|
||||
state: columnOrder,
|
||||
onColumnOrderChange: onColumnOrderChange,
|
||||
}
|
||||
: undefined,
|
||||
})
|
||||
|
||||
const shouldRenderHeading = heading || subHeading
|
||||
@@ -358,7 +371,9 @@ export const DataTable = <TData,>({
|
||||
return (
|
||||
<UiDataTable
|
||||
instance={instance}
|
||||
className={layout === "fill" ? "h-full [&_tr]:last-of-type:!border-b" : undefined}
|
||||
className={
|
||||
layout === "fill" ? "h-full [&_tr]:last-of-type:!border-b" : undefined
|
||||
}
|
||||
>
|
||||
<UiDataTable.Toolbar
|
||||
className="flex flex-col items-start justify-between gap-2 md:flex-row md:items-center"
|
||||
@@ -397,7 +412,9 @@ export const DataTable = <TData,>({
|
||||
</div>
|
||||
)}
|
||||
{actionMenu && <ActionMenu variant="primary" {...actionMenu} />}
|
||||
{actions && actions.length > 0 && <DataTableActions actions={actions} />}
|
||||
{actions && actions.length > 0 && (
|
||||
<DataTableActions actions={actions} />
|
||||
)}
|
||||
{!actions && action && <DataTableAction {...action} />}
|
||||
</div>
|
||||
</div>
|
||||
@@ -407,7 +424,9 @@ export const DataTable = <TData,>({
|
||||
<UiDataTable.Pagination translations={paginationTranslations} />
|
||||
)}
|
||||
{enableCommands && (
|
||||
<UiDataTable.CommandBar selectedLabel={(count) => `${count} selected`} />
|
||||
<UiDataTable.CommandBar
|
||||
selectedLabel={(count) => `${count} selected`}
|
||||
/>
|
||||
)}
|
||||
</UiDataTable>
|
||||
)
|
||||
@@ -520,4 +539,3 @@ const DataTableActions = ({ actions }: { actions: DataTableActionProps[] }) => {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -5834,9 +5834,23 @@
|
||||
"properties": {
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"noRecordsMessage": {
|
||||
"type": "string"
|
||||
},
|
||||
"noRecordsMessageEmpty": {
|
||||
"type": "string"
|
||||
},
|
||||
"noRecordsMessageFiltered": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["description"],
|
||||
"required": [
|
||||
"description",
|
||||
"noRecordsMessage",
|
||||
"noRecordsMessageEmpty",
|
||||
"noRecordsMessageFiltered"
|
||||
],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"create": {
|
||||
@@ -5876,9 +5890,12 @@
|
||||
"properties": {
|
||||
"confirmation": {
|
||||
"type": "string"
|
||||
},
|
||||
"successToast": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["confirmation"],
|
||||
"required": ["confirmation", "successToast"],
|
||||
"additionalProperties": false
|
||||
},
|
||||
"fulfillmentProviders": {
|
||||
|
||||
@@ -1535,7 +1535,10 @@
|
||||
"stockLocations": {
|
||||
"domain": "Locations & Shipping",
|
||||
"list": {
|
||||
"description": "Manage your store's stock locations and shipping options."
|
||||
"description": "Manage your store's stock locations and shipping options.",
|
||||
"noRecordsMessage": "No records",
|
||||
"noRecordsMessageEmpty": "No locations found",
|
||||
"noRecordsMessageFiltered": "No locations found matching the filters"
|
||||
},
|
||||
"create": {
|
||||
"header": "Create Stock Location",
|
||||
@@ -1548,7 +1551,8 @@
|
||||
"successToast": "Location {{name}} was successfully updated."
|
||||
},
|
||||
"delete": {
|
||||
"confirmation": "You are about to delete the stock location \"{{name}}\". This action cannot be undone."
|
||||
"confirmation": "You are about to delete the stock location \"{{name}}\". This action cannot be undone.",
|
||||
"successToast": "Location \"{{name}}\" was successfully deleted."
|
||||
},
|
||||
"fulfillmentProviders": {
|
||||
"header": "Fulfillment Providers",
|
||||
|
||||
@@ -62,7 +62,7 @@ export const EditLocationForm = ({ location }: EditLocationFormProps) => {
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
toast.success(t("stockLocations.edit.successToast"))
|
||||
toast.success(t("stockLocations.edit.successToast", { name: name }))
|
||||
handleSuccess()
|
||||
},
|
||||
onError: (e) => {
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./location-list-header"
|
||||
@@ -1,21 +0,0 @@
|
||||
import { Button, Container, Heading, Text } from "@medusajs/ui"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { Link } from "react-router-dom"
|
||||
|
||||
export const LocationListHeader = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<Container className="flex h-fit items-center justify-between gap-x-4 px-6 py-4">
|
||||
<div>
|
||||
<Heading>{t("stockLocations.domain")}</Heading>
|
||||
<Text className="text-ui-fg-subtle txt-small">
|
||||
{t("stockLocations.list.description")}
|
||||
</Text>
|
||||
</div>
|
||||
<Button size="small" className="shrink-0" variant="secondary" asChild>
|
||||
<Link to="create">{t("actions.create")}</Link>
|
||||
</Button>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
@@ -1,2 +1 @@
|
||||
export { shippingListLoader as loader } from "./loader"
|
||||
export { LocationList as Component } from "./location-list"
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import { FetchError } from "@medusajs/js-sdk"
|
||||
import { LoaderFunctionArgs, redirect } from "react-router-dom"
|
||||
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { stockLocationsQueryKeys } from "../../../hooks/api/stock-locations"
|
||||
import { sdk } from "../../../lib/client"
|
||||
import { queryClient } from "../../../lib/query-client"
|
||||
import { LOCATION_LIST_FIELDS } from "./constants"
|
||||
|
||||
const shippingListQuery = () => ({
|
||||
queryKey: stockLocationsQueryKeys.lists(),
|
||||
queryFn: async () => {
|
||||
return await sdk.admin.stockLocation
|
||||
.list({
|
||||
// TODO: change this when RQ is fixed
|
||||
fields: LOCATION_LIST_FIELDS,
|
||||
})
|
||||
.catch((error: FetchError) => {
|
||||
if (error.status === 401) {
|
||||
throw redirect("/login")
|
||||
}
|
||||
|
||||
throw error
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
export const shippingListLoader = async (_: LoaderFunctionArgs) => {
|
||||
const query = shippingListQuery()
|
||||
|
||||
return (
|
||||
queryClient.getQueryData<HttpTypes.AdminStockLocationListResponse>(
|
||||
query.queryKey
|
||||
) ?? (await queryClient.fetchQuery(query))
|
||||
)
|
||||
}
|
||||
@@ -1,34 +1,46 @@
|
||||
import { ShoppingBag, TruckFast } from "@medusajs/icons"
|
||||
import { Container, Heading } from "@medusajs/ui"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { useLoaderData } from "react-router-dom"
|
||||
|
||||
import { useStockLocations } from "../../../hooks/api/stock-locations"
|
||||
import LocationListItem from "./components/location-list-item/location-list-item"
|
||||
import { LOCATION_LIST_FIELDS } from "./constants"
|
||||
import { shippingListLoader } from "./loader"
|
||||
import { useLocationListTableColumns } from "./use-location-list-table-columns"
|
||||
import { useLocationListTableQuery } from "./use-location-list-table-query"
|
||||
|
||||
import { DataTable } from "../../../components/data-table"
|
||||
import { SidebarLink } from "../../../components/common/sidebar-link/sidebar-link"
|
||||
import { TwoColumnPage } from "../../../components/layout/pages"
|
||||
import { useExtension } from "../../../providers/extension-provider"
|
||||
import { LocationListHeader } from "./components/location-list-header"
|
||||
import { keepPreviousData } from "@tanstack/react-query"
|
||||
|
||||
const PAGE_SIZE = 20
|
||||
const PREFIX = "loc"
|
||||
|
||||
export function LocationList() {
|
||||
const initialData = useLoaderData() as Awaited<
|
||||
ReturnType<typeof shippingListLoader>
|
||||
>
|
||||
const { t } = useTranslation()
|
||||
|
||||
const searchParams = useLocationListTableQuery({
|
||||
pageSize: PAGE_SIZE,
|
||||
prefix: PREFIX,
|
||||
})
|
||||
|
||||
const {
|
||||
stock_locations: stockLocations = [],
|
||||
count,
|
||||
isError,
|
||||
error,
|
||||
isLoading,
|
||||
} = useStockLocations(
|
||||
{
|
||||
fields: LOCATION_LIST_FIELDS,
|
||||
...searchParams,
|
||||
},
|
||||
{ initialData }
|
||||
{
|
||||
placeholderData: keepPreviousData,
|
||||
}
|
||||
)
|
||||
|
||||
const columns = useLocationListTableColumns()
|
||||
const { getWidgets } = useExtension()
|
||||
|
||||
if (isError) {
|
||||
@@ -46,12 +58,38 @@ export function LocationList() {
|
||||
showJSON
|
||||
>
|
||||
<TwoColumnPage.Main>
|
||||
<LocationListHeader />
|
||||
<div className="flex flex-col gap-3 lg:col-span-2">
|
||||
{stockLocations.map((location) => (
|
||||
<LocationListItem key={location.id} location={location} />
|
||||
))}
|
||||
</div>
|
||||
<Container className="flex flex-col divide-y p-0">
|
||||
<DataTable
|
||||
data={stockLocations}
|
||||
columns={columns}
|
||||
rowCount={count}
|
||||
pageSize={PAGE_SIZE}
|
||||
getRowId={(row) => row.id}
|
||||
heading={t("stockLocations.domain")}
|
||||
subHeading={t("stockLocations.list.description")}
|
||||
emptyState={{
|
||||
empty: {
|
||||
heading: t("stockLocations.list.noRecordsMessage"),
|
||||
description: t("stockLocations.list.noRecordsMessageEmpty"),
|
||||
},
|
||||
filtered: {
|
||||
heading: t("stockLocations.list.noRecordsMessage"),
|
||||
description: t("stockLocations.list.noRecordsMessageFiltered"),
|
||||
},
|
||||
}}
|
||||
actions={[
|
||||
{
|
||||
label: t("actions.create"),
|
||||
to: "create",
|
||||
},
|
||||
]}
|
||||
isLoading={isLoading}
|
||||
rowHref={(row) => `/settings/locations/${row.id}`}
|
||||
enableSearch={true}
|
||||
prefix={PREFIX}
|
||||
layout="fill"
|
||||
/>
|
||||
</Container>
|
||||
</TwoColumnPage.Main>
|
||||
<TwoColumnPage.Sidebar>
|
||||
<LinksSection />
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { PencilSquare, Trash } from "@medusajs/icons"
|
||||
import {
|
||||
createDataTableColumnHelper,
|
||||
StatusBadge,
|
||||
toast,
|
||||
usePrompt,
|
||||
} from "@medusajs/ui"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { useMemo } from "react"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import { FetchError } from "@medusajs/js-sdk"
|
||||
|
||||
import { PlaceholderCell } from "../../../components/table/table-cells/common/placeholder-cell"
|
||||
import { getFormattedAddress } from "../../../lib/addresses"
|
||||
import { FulfillmentSetType } from "../common/constants"
|
||||
import { queryClient } from "../../../lib/query-client"
|
||||
import { stockLocationsQueryKeys } from "../../../hooks/api/stock-locations"
|
||||
import { ListSummary } from "../../../components/common/list-summary"
|
||||
import { sdk } from "../../../lib/client"
|
||||
|
||||
const columnHelper = createDataTableColumnHelper<HttpTypes.AdminStockLocation>()
|
||||
|
||||
export const useLocationListTableColumns = () => {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const prompt = usePrompt()
|
||||
|
||||
const handleDelete = async (location: HttpTypes.AdminStockLocation) => {
|
||||
const result = await prompt({
|
||||
title: t("general.areYouSure"),
|
||||
description: t("stockLocations.delete.confirmation", {
|
||||
name: location.name,
|
||||
}),
|
||||
confirmText: t("actions.remove"),
|
||||
cancelText: t("actions.cancel"),
|
||||
})
|
||||
|
||||
if (!result) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await sdk.admin.stockLocation.delete(location.id)
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: stockLocationsQueryKeys.lists(),
|
||||
})
|
||||
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: stockLocationsQueryKeys.detail(location.id),
|
||||
})
|
||||
|
||||
toast.success(
|
||||
t("stockLocations.delete.successToast", {
|
||||
name: location.name,
|
||||
})
|
||||
)
|
||||
} catch (e) {
|
||||
toast.error((e as FetchError).message)
|
||||
}
|
||||
}
|
||||
|
||||
return useMemo(
|
||||
() => [
|
||||
columnHelper.accessor("name", {
|
||||
header: t("fields.name"),
|
||||
cell: ({ getValue }) => {
|
||||
const name = getValue()
|
||||
if (!name) {
|
||||
return <PlaceholderCell />
|
||||
}
|
||||
|
||||
return (
|
||||
<span className="text-ui-fg-subtle text-small truncate">
|
||||
{name}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("address", {
|
||||
header: t("fields.address"),
|
||||
cell: ({ getValue, row }) => {
|
||||
const address = getValue()
|
||||
const location = row.original
|
||||
|
||||
if (!address) {
|
||||
return <PlaceholderCell />
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<span className="text-ui-fg-subtle text-small truncate">
|
||||
{getFormattedAddress({
|
||||
address: location.address as HttpTypes.AdminOrderAddress,
|
||||
}).join(", ")}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("fulfillment_sets", {
|
||||
id: "shipping_fulfillment",
|
||||
header: t("stockLocations.fulfillmentSets.shipping.header"),
|
||||
cell: ({ getValue }) => {
|
||||
const fulfillmentSets = getValue()
|
||||
const shippingSet = fulfillmentSets?.find(
|
||||
(f) => f.type === FulfillmentSetType.Shipping
|
||||
)
|
||||
const fulfillmentSetExists = !!shippingSet
|
||||
|
||||
return (
|
||||
<StatusBadge color={fulfillmentSetExists ? "green" : "grey"}>
|
||||
{t(
|
||||
fulfillmentSetExists ? "statuses.enabled" : "statuses.disabled"
|
||||
)}
|
||||
</StatusBadge>
|
||||
)
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("fulfillment_sets", {
|
||||
id: "pickup_fulfillment",
|
||||
header: t("stockLocations.fulfillmentSets.pickup.header"),
|
||||
cell: ({ getValue }) => {
|
||||
const fulfillmentSets = getValue()
|
||||
const pickupSet = fulfillmentSets?.find(
|
||||
(f) => f.type === FulfillmentSetType.Pickup
|
||||
)
|
||||
const fulfillmentSetExists = !!pickupSet
|
||||
|
||||
return (
|
||||
<StatusBadge color={fulfillmentSetExists ? "green" : "grey"}>
|
||||
{t(
|
||||
fulfillmentSetExists ? "statuses.enabled" : "statuses.disabled"
|
||||
)}
|
||||
</StatusBadge>
|
||||
)
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor("sales_channels", {
|
||||
header: t("stockLocations.salesChannels.label"),
|
||||
cell: ({ getValue }) => {
|
||||
const salesChannels = getValue()
|
||||
|
||||
if (!salesChannels?.length) {
|
||||
return <PlaceholderCell />
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<ListSummary
|
||||
inline
|
||||
n={1}
|
||||
list={salesChannels.map((s) => s.name)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}),
|
||||
columnHelper.action({
|
||||
actions: (ctx) => {
|
||||
const location = ctx.row.original
|
||||
return [
|
||||
[
|
||||
{
|
||||
icon: <PencilSquare />,
|
||||
label: t("actions.edit"),
|
||||
onClick: () => {
|
||||
navigate(`/settings/locations/${location.id}/edit`)
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
icon: <Trash />,
|
||||
label: t("actions.delete"),
|
||||
onClick: () => handleDelete(location),
|
||||
},
|
||||
],
|
||||
]
|
||||
},
|
||||
}),
|
||||
],
|
||||
[]
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { useQueryParams } from "../../../hooks/use-query-params"
|
||||
|
||||
export const useLocationListTableQuery = ({
|
||||
pageSize = 20,
|
||||
prefix,
|
||||
}: {
|
||||
pageSize?: number
|
||||
prefix?: string
|
||||
}) => {
|
||||
const queryObject = useQueryParams(["order", "offset", "q"], prefix)
|
||||
|
||||
const { offset, ...rest } = queryObject
|
||||
|
||||
const searchParams: HttpTypes.AdminStockLocationListParams = {
|
||||
limit: pageSize,
|
||||
offset: offset ? Number(offset) : 0,
|
||||
...rest,
|
||||
}
|
||||
|
||||
return searchParams
|
||||
}
|
||||
@@ -1,12 +1,20 @@
|
||||
import { PencilSquare, Trash } from "@medusajs/icons"
|
||||
import { HttpTypes } from "@medusajs/types"
|
||||
import { Container, createDataTableColumnHelper, toast, usePrompt, } from "@medusajs/ui"
|
||||
import {
|
||||
Container,
|
||||
createDataTableColumnHelper,
|
||||
toast,
|
||||
usePrompt,
|
||||
} from "@medusajs/ui"
|
||||
import { keepPreviousData } from "@tanstack/react-query"
|
||||
import { useCallback, useMemo } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { useNavigate } from "react-router-dom"
|
||||
import { DataTable } from "../../../../../components/data-table"
|
||||
import { useDeleteRefundReasonLazy, useRefundReasons, } from "../../../../../hooks/api"
|
||||
import {
|
||||
useDeleteRefundReasonLazy,
|
||||
useRefundReasons,
|
||||
} from "../../../../../hooks/api"
|
||||
import { useRefundReasonTableColumns } from "../../../../../hooks/table/columns"
|
||||
import { useRefundReasonTableQuery } from "../../../../../hooks/table/query"
|
||||
|
||||
@@ -18,7 +26,7 @@ export const RefundReasonListTable = () => {
|
||||
pageSize: PAGE_SIZE,
|
||||
})
|
||||
|
||||
const { refund_reasons, count, isPending, isError, error } = useRefundReasons(
|
||||
const { refund_reasons, count, isLoading, isError, error } = useRefundReasons(
|
||||
searchParams,
|
||||
{
|
||||
placeholderData: keepPreviousData,
|
||||
@@ -56,7 +64,7 @@ export const RefundReasonListTable = () => {
|
||||
to: "create",
|
||||
},
|
||||
]}
|
||||
isLoading={isPending}
|
||||
isLoading={isLoading}
|
||||
enableSearch={true}
|
||||
/>
|
||||
</Container>
|
||||
|
||||
@@ -4,14 +4,14 @@ import { StockLocation } from "@models"
|
||||
const StockLocationAddress = model
|
||||
.define("StockLocationAddress", {
|
||||
id: model.id({ prefix: "laddr" }).primaryKey(),
|
||||
address_1: model.text(),
|
||||
address_2: model.text().nullable(),
|
||||
address_1: model.text().searchable(),
|
||||
address_2: model.text().searchable().nullable(),
|
||||
company: model.text().nullable(),
|
||||
city: model.text().nullable(),
|
||||
country_code: model.text(),
|
||||
city: model.text().searchable().nullable(),
|
||||
country_code: model.text().searchable(),
|
||||
phone: model.text().nullable(),
|
||||
province: model.text().nullable(),
|
||||
postal_code: model.text().nullable(),
|
||||
province: model.text().searchable().nullable(),
|
||||
postal_code: model.text().searchable().nullable(),
|
||||
metadata: model.json().nullable(),
|
||||
stock_locations: model.hasOne(() => StockLocation, {
|
||||
mappedBy: "address",
|
||||
|
||||
@@ -9,6 +9,7 @@ const StockLocation = model.define("StockLocation", {
|
||||
.belongsTo(() => StockLocationAddress, {
|
||||
mappedBy: "stock_locations",
|
||||
})
|
||||
.searchable()
|
||||
.nullable(),
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user