chore(dashboard): migrate location levels table (#12624)

* chore: migrate location levels table

* fix: page size

* fix: disable sorting by available quantity

* wip: migrate fulfillment location combobox

---------

Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com>
This commit is contained in:
Frane Polić
2025-08-18 14:50:52 +02:00
committed by GitHub
parent eb376eb4cf
commit 6d8e4acdc7
7 changed files with 127 additions and 129 deletions

View File

@@ -176,7 +176,10 @@ export const useInventoryItemLevels = (
) => {
const { data, ...rest } = useQuery({
queryFn: () => sdk.admin.inventoryItem.listLevels(inventoryItemId, query),
queryKey: inventoryItemLevelsQueryKeys.detail(inventoryItemId),
queryKey: inventoryItemLevelsQueryKeys.list({
...(query || {}),
inventoryItemId,
}),
...options,
})

View File

@@ -1,62 +0,0 @@
import { PencilSquare, Trash } from "@medusajs/icons"
import { InventoryTypes } from "@medusajs/types"
import { usePrompt } from "@medusajs/ui"
import { useTranslation } from "react-i18next"
import { ActionMenu } from "../../../../../components/common/action-menu"
import { useDeleteInventoryItemLevel } from "../../../../../hooks/api/inventory"
export const LocationActions = ({
level,
}: {
level: InventoryTypes.InventoryLevelDTO
}) => {
const { t } = useTranslation()
const prompt = usePrompt()
const { mutateAsync } = useDeleteInventoryItemLevel(
level.inventory_item_id,
level.location_id
)
const handleDelete = async () => {
const res = await prompt({
title: t("general.areYouSure"),
description: t("inventory.deleteWarning"),
confirmText: t("actions.delete"),
cancelText: t("actions.cancel"),
})
if (!res) {
return
}
await mutateAsync()
}
return (
<ActionMenu
groups={[
{
actions: [
{
icon: <PencilSquare />,
label: t("actions.edit"),
to: `locations/${level.location_id}`,
},
],
},
{
actions: [
{
icon: <Trash />,
label: t("actions.delete"),
onClick: handleDelete,
disabled:
level.reserved_quantity > 0 || level.stocked_quantity > 0,
},
],
},
]}
/>
)
}

View File

@@ -1,18 +1,19 @@
import { _DataTable } from "../../../../../components/table/data-table"
import { DataTable } from "../../../../../components/data-table"
import { useInventoryItemLevels } from "../../../../../hooks/api/inventory"
import { useDataTable } from "../../../../../hooks/use-data-table"
import { useLocationListTableColumns } from "./use-location-list-table-columns"
import { useLocationLevelTableQuery } from "./use-location-list-table-query"
const PAGE_SIZE = 20
const PREFIX = "invlvl"
export const ItemLocationListTable = ({
inventory_item_id,
}: {
inventory_item_id: string
}) => {
const { searchParams, raw } = useLocationLevelTableQuery({
const searchParams = useLocationLevelTableQuery({
pageSize: PAGE_SIZE,
prefix: PREFIX,
})
const {
@@ -23,33 +24,26 @@ export const ItemLocationListTable = ({
error,
} = useInventoryItemLevels(inventory_item_id, {
...searchParams,
fields: "*stock_locations",
fields: "+stock_locations.id,+stock_locations.name",
})
const columns = useLocationListTableColumns()
const { table } = useDataTable({
data: inventory_levels ?? [],
columns,
count,
enablePagination: true,
getRowId: (row) => row.id,
pageSize: PAGE_SIZE,
})
if (isError) {
throw error
}
return (
<_DataTable
table={table}
<DataTable
data={inventory_levels ?? []}
columns={columns}
rowCount={count}
pageSize={PAGE_SIZE}
count={count}
getRowId={(row) => row.id}
isLoading={isLoading}
pagination
queryObject={raw}
prefix={PREFIX}
layout="fill"
enableSearch={false}
/>
)
}

View File

@@ -1,10 +1,17 @@
import { InventoryTypes, StockLocationDTO } from "@medusajs/types"
import { PencilSquare, Trash } from "@medusajs/icons"
import { createColumnHelper } from "@tanstack/react-table"
import { useMemo } from "react"
import { createDataTableColumnHelper, toast, usePrompt } from "@medusajs/ui"
import { useTranslation } from "react-i18next"
import { PlaceholderCell } from "../../../../../components/table/table-cells/common/placeholder-cell"
import { LocationActions } from "./location-actions"
import {
inventoryItemLevelsQueryKeys,
inventoryItemsQueryKeys,
} from "../../../../../hooks/api"
import { sdk } from "../../../../../lib/client"
import { queryClient } from "../../../../../lib/query-client"
import { useNavigate } from "react-router-dom"
/**
* Adds missing properties to the InventoryLevelDTO type.
@@ -16,10 +23,45 @@ interface ExtendedLocationLevel extends InventoryTypes.InventoryLevelDTO {
available_quantity: number
}
const columnHelper = createColumnHelper<ExtendedLocationLevel>()
const columnHelper = createDataTableColumnHelper<ExtendedLocationLevel>()
export const useLocationListTableColumns = () => {
const { t } = useTranslation()
const navigate = useNavigate()
const prompt = usePrompt()
const handleDelete = async (level: ExtendedLocationLevel) => {
const res = await prompt({
title: t("general.areYouSure"),
description: t("inventory.deleteWarning"),
confirmText: t("actions.delete"),
cancelText: t("actions.cancel"),
})
if (!res) {
return
}
try {
await sdk.admin.inventoryItem.deleteLevel(
level.inventory_item_id,
level.location_id
)
queryClient.invalidateQueries({
queryKey: inventoryItemsQueryKeys.lists(),
})
queryClient.invalidateQueries({
queryKey: inventoryItemsQueryKeys.detail(level.inventory_item_id),
})
queryClient.invalidateQueries({
queryKey: inventoryItemLevelsQueryKeys.detail(level.inventory_item_id),
})
} catch (e) {
toast.error(e.message)
}
}
return useMemo(
() => [
@@ -54,6 +96,7 @@ export const useLocationListTableColumns = () => {
</div>
)
},
enableSorting: true,
}),
columnHelper.accessor("stocked_quantity", {
header: t("fields.inStock"),
@@ -70,6 +113,7 @@ export const useLocationListTableColumns = () => {
</div>
)
},
enableSorting: true,
}),
columnHelper.accessor("available_quantity", {
header: t("inventory.available"),
@@ -87,9 +131,31 @@ export const useLocationListTableColumns = () => {
)
},
}),
columnHelper.display({
id: "actions",
cell: ({ row }) => <LocationActions level={row.original} />,
columnHelper.action({
actions: (ctx) => {
const level = ctx.row.original
return [
[
{
icon: <PencilSquare />,
label: t("actions.edit"),
onClick: (row) => {
navigate(`locations/${level.location_id}`)
},
},
],
[
{
icon: <Trash />,
label: t("actions.delete"),
onClick: () => handleDelete(level),
disabled:
level.reserved_quantity > 0 || level.stocked_quantity > 0,
},
],
]
},
}),
],
[t]

View File

@@ -1,3 +1,4 @@
import { HttpTypes } from "@medusajs/types"
import { useQueryParams } from "../../../../../hooks/use-query-params"
export const useLocationLevelTableQuery = ({
@@ -7,38 +8,25 @@ export const useLocationLevelTableQuery = ({
pageSize?: number
prefix?: string
}) => {
const raw = useQueryParams(
const queryObject = useQueryParams(
[
"id",
"order",
"offset",
"location_id",
"stocked_quantity",
"reserved_quantity",
"incoming_quantity",
"available_quantity",
"*stock_locations",
],
prefix
)
const { reserved_quantity, stocked_quantity, available_quantity, ...params } =
raw
const { offset, ...rest } = queryObject
const searchParams = {
const searchParams: HttpTypes.AdminInventoryLevelFilters = {
limit: pageSize,
reserved_quantity: reserved_quantity
? JSON.parse(reserved_quantity)
: undefined,
stocked_quantity: stocked_quantity
? JSON.parse(stocked_quantity)
: undefined,
available_quantity: available_quantity
? JSON.parse(available_quantity)
: undefined,
...params,
offset: offset ? Number(offset) : 0,
...rest,
}
return {
searchParams,
raw,
}
return searchParams
}

View File

@@ -15,7 +15,6 @@ import {
} from "../../../../../components/modals"
import { KeyboundForm } from "../../../../../components/utilities/keybound-form"
import { useCreateOrderFulfillment } from "../../../../../hooks/api/orders"
import { useStockLocations } from "../../../../../hooks/api/stock-locations"
import { getFulfillableQuantity } from "../../../../../lib/order-item"
import { CreateFulfillmentSchema } from "./constants"
import { OrderCreateFulfillmentItem } from "./order-create-fulfillment-item"
@@ -24,6 +23,9 @@ import {
useShippingOptions,
} from "../../../../../hooks/api"
import { getReservationsLimitCount } from "../../../../../lib/orders"
import { sdk } from "../../../../../lib/client"
import { useComboboxData } from "../../../../../hooks/use-combobox-data"
import { Combobox } from "../../../../../components/inputs/combobox"
type OrderCreateFulfillmentFormProps = {
order: AdminOrder
@@ -45,6 +47,16 @@ export function OrderCreateFulfillmentForm({
limit: getReservationsLimitCount(order),
})
const stockLocations = useComboboxData({
queryFn: (params) => sdk.admin.stockLocation.list(params),
queryKey: ["stock_locations"],
getOptions: (data) =>
data.stock_locations.map((location) => ({
label: location.name,
value: location.id,
})),
})
const itemReservedQuantitiesMap = useMemo(
() =>
new Map((reservations || []).map((r) => [r.line_item_id, r.quantity])),
@@ -78,8 +90,6 @@ export function OrderCreateFulfillmentForm({
control: form.control,
})
const { stock_locations = [] } = useStockLocations()
const { shipping_options = [], isLoading: isShippingOptionsLoading } =
useShippingOptions({
stock_location_id: selectedLocationId,
@@ -155,7 +165,7 @@ export function OrderCreateFulfillmentForm({
})
useEffect(() => {
if (stock_locations?.length && shipping_options?.length) {
if (shipping_options?.length) {
const initialShippingOptionId =
order.shipping_methods?.[0]?.shipping_option_id
@@ -176,7 +186,7 @@ export function OrderCreateFulfillmentForm({
} // else -> TODO: what if original shipping option is deleted?
}
}
}, [stock_locations?.length, shipping_options?.length])
}, [shipping_options])
const fulfilledQuantityArray = (order.items || []).map(
(item) =>
@@ -234,7 +244,7 @@ export function OrderCreateFulfillmentForm({
<Form.Field
control={form.control}
name="location_id"
render={({ field: { onChange, ref, ...field } }) => {
render={({ field: { ...field } }) => {
return (
<Form.Item>
<div className="flex flex-col gap-2 xl:flex-row xl:items-center">
@@ -246,21 +256,15 @@ export function OrderCreateFulfillmentForm({
</div>
<div className="flex-1">
<Form.Control>
<Select onValueChange={onChange} {...field}>
<Select.Trigger
className="bg-ui-bg-base"
ref={ref}
>
<Select.Value />
</Select.Trigger>
<Select.Content>
{stock_locations.map((l) => (
<Select.Item key={l.id} value={l.id}>
{l.name}
</Select.Item>
))}
</Select.Content>
</Select>
<Combobox
{...field}
options={stockLocations.options}
searchValue={stockLocations.searchValue}
onSearchValueChange={
stockLocations.onSearchValueChange
}
disabled={stockLocations.disabled}
/>
</Form.Control>
</div>
</div>