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
co-authored by Oli Juhl
parent eb376eb4cf
commit 6d8e4acdc7
7 changed files with 127 additions and 129 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@medusajs/dashboard": patch
---
chore(dashboard): migrate inventory location levels table
@@ -176,7 +176,10 @@ export const useInventoryItemLevels = (
) => { ) => {
const { data, ...rest } = useQuery({ const { data, ...rest } = useQuery({
queryFn: () => sdk.admin.inventoryItem.listLevels(inventoryItemId, query), queryFn: () => sdk.admin.inventoryItem.listLevels(inventoryItemId, query),
queryKey: inventoryItemLevelsQueryKeys.detail(inventoryItemId), queryKey: inventoryItemLevelsQueryKeys.list({
...(query || {}),
inventoryItemId,
}),
...options, ...options,
}) })
@@ -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,
},
],
},
]}
/>
)
}
@@ -1,18 +1,19 @@
import { _DataTable } from "../../../../../components/table/data-table" import { DataTable } from "../../../../../components/data-table"
import { useInventoryItemLevels } from "../../../../../hooks/api/inventory" import { useInventoryItemLevels } from "../../../../../hooks/api/inventory"
import { useDataTable } from "../../../../../hooks/use-data-table"
import { useLocationListTableColumns } from "./use-location-list-table-columns" import { useLocationListTableColumns } from "./use-location-list-table-columns"
import { useLocationLevelTableQuery } from "./use-location-list-table-query" import { useLocationLevelTableQuery } from "./use-location-list-table-query"
const PAGE_SIZE = 20 const PAGE_SIZE = 20
const PREFIX = "invlvl"
export const ItemLocationListTable = ({ export const ItemLocationListTable = ({
inventory_item_id, inventory_item_id,
}: { }: {
inventory_item_id: string inventory_item_id: string
}) => { }) => {
const { searchParams, raw } = useLocationLevelTableQuery({ const searchParams = useLocationLevelTableQuery({
pageSize: PAGE_SIZE, pageSize: PAGE_SIZE,
prefix: PREFIX,
}) })
const { const {
@@ -23,33 +24,26 @@ export const ItemLocationListTable = ({
error, error,
} = useInventoryItemLevels(inventory_item_id, { } = useInventoryItemLevels(inventory_item_id, {
...searchParams, ...searchParams,
fields: "*stock_locations", fields: "+stock_locations.id,+stock_locations.name",
}) })
const columns = useLocationListTableColumns() const columns = useLocationListTableColumns()
const { table } = useDataTable({
data: inventory_levels ?? [],
columns,
count,
enablePagination: true,
getRowId: (row) => row.id,
pageSize: PAGE_SIZE,
})
if (isError) { if (isError) {
throw error throw error
} }
return ( return (
<_DataTable <DataTable
table={table} data={inventory_levels ?? []}
columns={columns} columns={columns}
rowCount={count}
pageSize={PAGE_SIZE} pageSize={PAGE_SIZE}
count={count} getRowId={(row) => row.id}
isLoading={isLoading} isLoading={isLoading}
pagination prefix={PREFIX}
queryObject={raw} layout="fill"
enableSearch={false}
/> />
) )
} }
@@ -1,10 +1,17 @@
import { InventoryTypes, StockLocationDTO } from "@medusajs/types" import { InventoryTypes, StockLocationDTO } from "@medusajs/types"
import { PencilSquare, Trash } from "@medusajs/icons"
import { createColumnHelper } from "@tanstack/react-table"
import { useMemo } from "react" import { useMemo } from "react"
import { createDataTableColumnHelper, toast, usePrompt } from "@medusajs/ui"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { PlaceholderCell } from "../../../../../components/table/table-cells/common/placeholder-cell" 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. * Adds missing properties to the InventoryLevelDTO type.
@@ -16,10 +23,45 @@ interface ExtendedLocationLevel extends InventoryTypes.InventoryLevelDTO {
available_quantity: number available_quantity: number
} }
const columnHelper = createColumnHelper<ExtendedLocationLevel>() const columnHelper = createDataTableColumnHelper<ExtendedLocationLevel>()
export const useLocationListTableColumns = () => { export const useLocationListTableColumns = () => {
const { t } = useTranslation() 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( return useMemo(
() => [ () => [
@@ -54,6 +96,7 @@ export const useLocationListTableColumns = () => {
</div> </div>
) )
}, },
enableSorting: true,
}), }),
columnHelper.accessor("stocked_quantity", { columnHelper.accessor("stocked_quantity", {
header: t("fields.inStock"), header: t("fields.inStock"),
@@ -70,6 +113,7 @@ export const useLocationListTableColumns = () => {
</div> </div>
) )
}, },
enableSorting: true,
}), }),
columnHelper.accessor("available_quantity", { columnHelper.accessor("available_quantity", {
header: t("inventory.available"), header: t("inventory.available"),
@@ -87,9 +131,31 @@ export const useLocationListTableColumns = () => {
) )
}, },
}), }),
columnHelper.display({ columnHelper.action({
id: "actions", actions: (ctx) => {
cell: ({ row }) => <LocationActions level={row.original} />, 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] [t]
@@ -1,3 +1,4 @@
import { HttpTypes } from "@medusajs/types"
import { useQueryParams } from "../../../../../hooks/use-query-params" import { useQueryParams } from "../../../../../hooks/use-query-params"
export const useLocationLevelTableQuery = ({ export const useLocationLevelTableQuery = ({
@@ -7,38 +8,25 @@ export const useLocationLevelTableQuery = ({
pageSize?: number pageSize?: number
prefix?: string prefix?: string
}) => { }) => {
const raw = useQueryParams( const queryObject = useQueryParams(
[ [
"id", "order",
"offset",
"location_id", "location_id",
"stocked_quantity", "stocked_quantity",
"reserved_quantity", "reserved_quantity",
"incoming_quantity", "incoming_quantity",
"available_quantity",
"*stock_locations",
], ],
prefix prefix
) )
const { reserved_quantity, stocked_quantity, available_quantity, ...params } = const { offset, ...rest } = queryObject
raw
const searchParams = { const searchParams: HttpTypes.AdminInventoryLevelFilters = {
limit: pageSize, limit: pageSize,
reserved_quantity: reserved_quantity offset: offset ? Number(offset) : 0,
? JSON.parse(reserved_quantity) ...rest,
: undefined,
stocked_quantity: stocked_quantity
? JSON.parse(stocked_quantity)
: undefined,
available_quantity: available_quantity
? JSON.parse(available_quantity)
: undefined,
...params,
} }
return { return searchParams
searchParams,
raw,
}
} }
@@ -15,7 +15,6 @@ import {
} from "../../../../../components/modals" } from "../../../../../components/modals"
import { KeyboundForm } from "../../../../../components/utilities/keybound-form" import { KeyboundForm } from "../../../../../components/utilities/keybound-form"
import { useCreateOrderFulfillment } from "../../../../../hooks/api/orders" import { useCreateOrderFulfillment } from "../../../../../hooks/api/orders"
import { useStockLocations } from "../../../../../hooks/api/stock-locations"
import { getFulfillableQuantity } from "../../../../../lib/order-item" import { getFulfillableQuantity } from "../../../../../lib/order-item"
import { CreateFulfillmentSchema } from "./constants" import { CreateFulfillmentSchema } from "./constants"
import { OrderCreateFulfillmentItem } from "./order-create-fulfillment-item" import { OrderCreateFulfillmentItem } from "./order-create-fulfillment-item"
@@ -24,6 +23,9 @@ import {
useShippingOptions, useShippingOptions,
} from "../../../../../hooks/api" } from "../../../../../hooks/api"
import { getReservationsLimitCount } from "../../../../../lib/orders" 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 = { type OrderCreateFulfillmentFormProps = {
order: AdminOrder order: AdminOrder
@@ -45,6 +47,16 @@ export function OrderCreateFulfillmentForm({
limit: getReservationsLimitCount(order), 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( const itemReservedQuantitiesMap = useMemo(
() => () =>
new Map((reservations || []).map((r) => [r.line_item_id, r.quantity])), new Map((reservations || []).map((r) => [r.line_item_id, r.quantity])),
@@ -78,8 +90,6 @@ export function OrderCreateFulfillmentForm({
control: form.control, control: form.control,
}) })
const { stock_locations = [] } = useStockLocations()
const { shipping_options = [], isLoading: isShippingOptionsLoading } = const { shipping_options = [], isLoading: isShippingOptionsLoading } =
useShippingOptions({ useShippingOptions({
stock_location_id: selectedLocationId, stock_location_id: selectedLocationId,
@@ -155,7 +165,7 @@ export function OrderCreateFulfillmentForm({
}) })
useEffect(() => { useEffect(() => {
if (stock_locations?.length && shipping_options?.length) { if (shipping_options?.length) {
const initialShippingOptionId = const initialShippingOptionId =
order.shipping_methods?.[0]?.shipping_option_id order.shipping_methods?.[0]?.shipping_option_id
@@ -176,7 +186,7 @@ export function OrderCreateFulfillmentForm({
} // else -> TODO: what if original shipping option is deleted? } // else -> TODO: what if original shipping option is deleted?
} }
} }
}, [stock_locations?.length, shipping_options?.length]) }, [shipping_options])
const fulfilledQuantityArray = (order.items || []).map( const fulfilledQuantityArray = (order.items || []).map(
(item) => (item) =>
@@ -234,7 +244,7 @@ export function OrderCreateFulfillmentForm({
<Form.Field <Form.Field
control={form.control} control={form.control}
name="location_id" name="location_id"
render={({ field: { onChange, ref, ...field } }) => { render={({ field: { ...field } }) => {
return ( return (
<Form.Item> <Form.Item>
<div className="flex flex-col gap-2 xl:flex-row xl:items-center"> <div className="flex flex-col gap-2 xl:flex-row xl:items-center">
@@ -246,21 +256,15 @@ export function OrderCreateFulfillmentForm({
</div> </div>
<div className="flex-1"> <div className="flex-1">
<Form.Control> <Form.Control>
<Select onValueChange={onChange} {...field}> <Combobox
<Select.Trigger {...field}
className="bg-ui-bg-base" options={stockLocations.options}
ref={ref} searchValue={stockLocations.searchValue}
> onSearchValueChange={
<Select.Value /> stockLocations.onSearchValueChange
</Select.Trigger> }
<Select.Content> disabled={stockLocations.disabled}
{stock_locations.map((l) => ( />
<Select.Item key={l.id} value={l.id}>
{l.name}
</Select.Item>
))}
</Select.Content>
</Select>
</Form.Control> </Form.Control>
</div> </div>
</div> </div>