fix(dashboard): inventory kit combobox state (#12371)
* fix(dashboard): inventory kit combobox state * chore: changeset
This commit is contained in:
5
.changeset/large-clocks-decide.md
Normal file
5
.changeset/large-clocks-decide.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@medusajs/dashboard": patch
|
||||
---
|
||||
|
||||
fix(dashboard): inventory kit combobox state
|
||||
@@ -13,6 +13,7 @@ type ComboboxExternalData = {
|
||||
}
|
||||
|
||||
type ComboboxQueryParams = {
|
||||
id?: string
|
||||
q?: string
|
||||
offset?: number
|
||||
limit?: number
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user