fix(dashboard): inventory kit combobox state (#12371)

* fix(dashboard): inventory kit combobox state

* chore: changeset
This commit is contained in:
Frane Polić
2025-05-11 18:45:08 +02:00
committed by GitHub
parent fff285f8d2
commit 6aa1ebdee2
4 changed files with 280 additions and 206 deletions

View File

@@ -0,0 +1,5 @@
---
"@medusajs/dashboard": patch
---
fix(dashboard): inventory kit combobox state

View File

@@ -13,6 +13,7 @@ type ComboboxExternalData = {
}
type ComboboxQueryParams = {
id?: string
q?: string
offset?: number
limit?: number

View File

@@ -3,7 +3,7 @@ import { XMarkMini } from "@medusajs/icons"
import { AdminProductVariant, HttpTypes } from "@medusajs/types"
import { Button, Heading, IconButton, Input, Label, toast } from "@medusajs/ui"
import i18next from "i18next"
import { useFieldArray, useForm } from "react-hook-form"
import { useFieldArray, useForm, UseFormReturn } from "react-hook-form"
import { useTranslation } from "react-i18next"
import * as zod from "zod"
@@ -56,6 +56,130 @@ const ManageVariantInventoryItemsSchema = zod.object({
),
})
type InventoryItemFormData = zod.infer<
typeof ManageVariantInventoryItemsSchema
>["inventory"]
type VariantInventoryItemRowProps = {
form: UseFormReturn<InventoryItemFormData>
inventoryIndex: number
inventoryItem: {
id: string
inventory_item_id: string
required_quantity: number
}
onRemove: () => void
}
function VariantInventoryItemRow({
form,
inventoryIndex,
inventoryItem,
onRemove,
}: VariantInventoryItemRowProps) {
const { t } = useTranslation()
const items = useComboboxData({
queryKey: ["inventory_items"],
defaultValueKey: "id",
defaultValue: inventoryItem.inventory_item_id,
queryFn: (params) => sdk.admin.inventoryItem.list(params),
getOptions: (data) =>
data.inventory_items.map((item) => ({
label: `${item.title} ${item.sku ? `(${item.sku})` : ""}`,
value: item.id!,
})),
})
return (
<li
key={inventoryItem.id}
className="bg-ui-bg-component shadow-elevation-card-rest grid grid-cols-[1fr_28px] items-center gap-1.5 rounded-xl p-1.5"
>
<div className="grid grid-cols-[min-content,1fr] items-center gap-1.5">
<div className="flex items-center px-2 py-1.5">
<Label
size="xsmall"
weight="plus"
className="text-ui-fg-subtle"
htmlFor={`inventory.${inventoryIndex}.inventory_item_id`}
>
{t("fields.item")}
</Label>
</div>
<Form.Field
control={form.control}
name={`inventory.${inventoryIndex}.inventory_item_id`}
render={({ field }) => {
return (
<Form.Item>
<Form.Control>
<Combobox
{...field}
options={items.options}
searchValue={items.searchValue}
onSearchValueChange={items.onSearchValueChange}
onBlur={() => items.onSearchValueChange("")}
fetchNextPage={items.fetchNextPage}
className="bg-ui-bg-field-component hover:bg-ui-bg-field-component-hover"
placeholder={t("products.create.inventory.itemPlaceholder")}
/>
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
<div className="flex items-center px-2 py-1.5">
<Label
size="xsmall"
weight="plus"
className="text-ui-fg-subtle"
htmlFor={`inventory.${inventoryIndex}.required_quantity`}
>
{t("fields.quantity")}
</Label>
</div>
<Form.Field
control={form.control}
name={`inventory.${inventoryIndex}.required_quantity`}
render={({ field: { onChange, value, ...field } }) => {
return (
<Form.Item>
<Form.Control>
<Input
type="number"
className="bg-ui-bg-field-component"
min={0}
value={value}
onChange={onChange}
{...field}
placeholder={t(
"products.create.inventory.quantityPlaceholder"
)}
/>
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
</div>
<IconButton
type="button"
size="small"
variant="transparent"
className="text-ui-fg-muted"
onClick={onRemove}
>
<XMarkMini />
</IconButton>
</li>
)
}
export function ManageVariantInventoryItemsForm({
variant,
}: ManageVariantInventoryItemsFormProps) {
@@ -86,17 +210,6 @@ export function ManageVariantInventoryItemsForm({
const hasKit = inventory.fields.length > 1
const items = useComboboxData({
queryKey: ["inventory_items"],
queryFn: (params) => sdk.admin.inventoryItem.list(params),
getOptions: (data) =>
data.inventory_items.map((item) => ({
label: `${item.title} ${item.sku ? `(${item.sku})` : ""}`,
value: item.id!,
})),
defaultValue: variant.inventory_items?.[0]?.inventory_item_id,
})
const { mutateAsync, isPending } = useProductVariantsInventoryItemsBatch(
variant?.product_id!
)
@@ -212,92 +325,13 @@ export function ManageVariantInventoryItemsForm({
</Button>
</div>
{inventory.fields.map((inventoryItem, inventoryIndex) => (
<li
<VariantInventoryItemRow
key={inventoryItem.id}
className="bg-ui-bg-component shadow-elevation-card-rest grid grid-cols-[1fr_28px] items-center gap-1.5 rounded-xl p-1.5"
>
<div className="grid grid-cols-[min-content,1fr] items-center gap-1.5">
<div className="flex items-center px-2 py-1.5">
<Label
size="xsmall"
weight="plus"
className="text-ui-fg-subtle"
htmlFor={`inventory.${inventoryIndex}.inventory_item_id`}
>
{t("fields.item")}
</Label>
</div>
<Form.Field
control={form.control}
name={`inventory.${inventoryIndex}.inventory_item_id`}
render={({ field }) => {
return (
<Form.Item>
<Form.Control>
<Combobox
{...field}
options={items.options}
searchValue={items.searchValue}
onSearchValueChange={items.onSearchValueChange}
fetchNextPage={items.fetchNextPage}
className="bg-ui-bg-field-component hover:bg-ui-bg-field-component-hover"
placeholder={t(
"products.create.inventory.itemPlaceholder"
)}
/>
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
<div className="flex items-center px-2 py-1.5">
<Label
size="xsmall"
weight="plus"
className="text-ui-fg-subtle"
htmlFor={`inventory.${inventoryIndex}.required_quantity`}
>
{t("fields.quantity")}
</Label>
</div>
<Form.Field
control={form.control}
name={`inventory.${inventoryIndex}.required_quantity`}
render={({ field: { onChange, value, ...field } }) => {
return (
<Form.Item>
<Form.Control>
<Input
type="number"
className="bg-ui-bg-field-component"
min={0}
value={value}
onChange={onChange}
{...field}
placeholder={t(
"products.create.inventory.quantityPlaceholder"
)}
/>
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
</div>
<IconButton
type="button"
size="small"
variant="transparent"
className="text-ui-fg-muted"
onClick={() => inventory.remove(inventoryIndex)}
>
<XMarkMini />
</IconButton>
</li>
form={form}
inventoryIndex={inventoryIndex}
inventoryItem={inventoryItem}
onRemove={() => inventory.remove(inventoryIndex)}
/>
))}
</div>
</div>

View File

@@ -9,6 +9,139 @@ import { Combobox } from "../../../../../../../components/inputs/combobox"
import { useComboboxData } from "../../../../../../../hooks/use-combobox-data"
import { sdk } from "../../../../../../../lib/client"
type InventoryItemRowProps = {
form: UseFormReturn<ProductCreateSchemaType>
variantIndex: number
inventoryIndex: number
inventoryItem: any
isItemOptionDisabled: (
option: { value: string },
inventoryIndex: number
) => boolean
onRemove: () => void
}
function InventoryItemRow({
form,
variantIndex,
inventoryIndex,
inventoryItem,
isItemOptionDisabled,
onRemove,
}: InventoryItemRowProps) {
const { t } = useTranslation()
const items = useComboboxData({
queryKey: ["inventory_items"],
defaultValueKey: "id",
defaultValue: inventoryItem.inventory_item_id, // prefetch existing inventory items
queryFn: (params) => sdk.admin.inventoryItem.list(params),
getOptions: (data) =>
data.inventory_items.map((item) => ({
label: `${item.title} ${item.sku ? `(${item.sku})` : ""}`,
value: item.id,
})),
})
return (
<li
key={inventoryItem.id}
className="bg-ui-bg-component shadow-elevation-card-rest grid grid-cols-[1fr_28px] items-center gap-1.5 rounded-xl p-1.5"
>
<div className="grid grid-cols-[min-content,1fr] items-center gap-1.5">
<div className="flex items-center px-2 py-1.5">
<Label
size="xsmall"
weight="plus"
className="text-ui-fg-subtle"
htmlFor={`variants.${variantIndex}.inventory.${inventoryIndex}.inventory_item_id`}
>
{t("fields.item")}
</Label>
</div>
<Form.Field
control={form.control}
name={`variants.${variantIndex}.inventory.${inventoryIndex}.inventory_item_id`}
render={({ field }) => {
return (
<Form.Item>
<Form.Control>
<Combobox
{...field}
options={items.options.map((o) => ({
...o,
disabled: isItemOptionDisabled(o, inventoryIndex),
}))}
searchValue={items.searchValue}
onBlur={() => items.onSearchValueChange("")}
onSearchValueChange={items.onSearchValueChange}
fetchNextPage={items.fetchNextPage}
className="bg-ui-bg-field-component hover:bg-ui-bg-field-component-hover"
placeholder={t("products.create.inventory.itemPlaceholder")}
/>
</Form.Control>
</Form.Item>
)
}}
/>
<div className="flex items-center px-2 py-1.5">
<Label
size="xsmall"
weight="plus"
className="text-ui-fg-subtle"
htmlFor={`variants.${variantIndex}.inventory.${inventoryIndex}.required_quantity`}
>
{t("fields.quantity")}
</Label>
</div>
<Form.Field
control={form.control}
name={`variants.${variantIndex}.inventory.${inventoryIndex}.required_quantity`}
render={({ field: { onChange, value, ...field } }) => {
return (
<Form.Item>
<Form.Control>
<Input
type="number"
className="bg-ui-bg-field-component"
min={0}
value={value}
onChange={(e) => {
const value = e.target.value
if (value === "") {
onChange(null)
} else {
onChange(Number(value))
}
}}
{...field}
placeholder={t(
"products.create.inventory.quantityPlaceholder"
)}
/>
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
</div>
<IconButton
type="button"
size="small"
variant="transparent"
className="text-ui-fg-muted"
onClick={onRemove}
>
<XMarkMini />
</IconButton>
</li>
)
}
type VariantSectionProps = {
form: UseFormReturn<ProductCreateSchemaType>
variant: ProductCreateSchemaType["variants"][0]
@@ -28,26 +161,14 @@ function VariantSection({ form, variant, index }: VariantSectionProps) {
name: `variants.${index}.inventory`,
})
const items = useComboboxData({
queryKey: ["inventory_items"],
queryFn: (params) => sdk.admin.inventoryItem.list(params),
getOptions: (data) =>
data.inventory_items.map((item) => ({
label: `${item.title} ${item.sku ? `(${item.sku})` : ""}`,
value: item.id,
})),
})
/**
* Will mark an option as disabled if another input already selected that option
* @param option
* @param inventoryIndex
*/
const isItemOptionDisabled = (
option: (typeof items.options)[0],
option: { value: string },
inventoryIndex: number
) => {
return inventoryFormData?.some(
return !!inventoryFormData?.some(
(i, index) =>
index != inventoryIndex && i.inventory_item_id === option.value
)
@@ -75,102 +196,15 @@ function VariantSection({ form, variant, index }: VariantSectionProps) {
</Button>
</div>
{inventory.fields.map((inventoryItem, inventoryIndex) => (
<li
<InventoryItemRow
key={inventoryItem.id}
className="bg-ui-bg-component shadow-elevation-card-rest grid grid-cols-[1fr_28px] items-center gap-1.5 rounded-xl p-1.5"
>
<div className="grid grid-cols-[min-content,1fr] items-center gap-1.5">
<div className="flex items-center px-2 py-1.5">
<Label
size="xsmall"
weight="plus"
className="text-ui-fg-subtle"
htmlFor={`variants.${index}.inventory.${inventoryIndex}.inventory_item_id`}
>
{t("fields.item")}
</Label>
</div>
<Form.Field
control={form.control}
name={`variants.${index}.inventory.${inventoryIndex}.inventory_item_id`}
render={({ field }) => {
return (
<Form.Item>
<Form.Control>
<Combobox
{...field}
options={items.options.map((o) => ({
...o,
disabled: isItemOptionDisabled(o, inventoryIndex),
}))}
searchValue={items.searchValue}
onSearchValueChange={items.onSearchValueChange}
fetchNextPage={items.fetchNextPage}
className="bg-ui-bg-field-component hover:bg-ui-bg-field-component-hover"
placeholder={t(
"products.create.inventory.itemPlaceholder"
)}
/>
</Form.Control>
</Form.Item>
)
}}
/>
<div className="flex items-center px-2 py-1.5">
<Label
size="xsmall"
weight="plus"
className="text-ui-fg-subtle"
htmlFor={`variants.${index}.inventory.${inventoryIndex}.required_quantity`}
>
{t("fields.quantity")}
</Label>
</div>
<Form.Field
control={form.control}
name={`variants.${index}.inventory.${inventoryIndex}.required_quantity`}
render={({ field: { onChange, value, ...field } }) => {
return (
<Form.Item>
<Form.Control>
<Input
type="number"
className="bg-ui-bg-field-component"
min={0}
value={value}
onChange={(e) => {
const value = e.target.value
if (value === "") {
onChange(null)
} else {
onChange(Number(value))
}
}}
{...field}
placeholder={t(
"products.create.inventory.quantityPlaceholder"
)}
/>
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
</div>
<IconButton
type="button"
size="small"
variant="transparent"
className="text-ui-fg-muted"
onClick={() => inventory.remove(inventoryIndex)}
>
<XMarkMini />
</IconButton>
</li>
form={form}
variantIndex={index}
inventoryIndex={inventoryIndex}
inventoryItem={inventoryItem}
isItemOptionDisabled={isItemOptionDisabled}
onRemove={() => inventory.remove(inventoryIndex)}
/>
))}
</div>
)