From 8bf191abe0d77f7f424aa60e473be08577c7b6a5 Mon Sep 17 00:00:00 2001
From: Kasper Fabricius Kristensen
<45367945+kasperkristensen@users.noreply.github.com>
Date: Fri, 13 Dec 2024 15:55:34 +0100
Subject: [PATCH 01/19] feat(dashboard): Add UI for bulk editing inventory
stock (#10556)
---
.../inventory/admin/inventory.spec.ts | 69 +++++-
.../components/common/thumbnail/thumbnail.tsx | 2 +-
.../components/data-grid-cell-container.tsx | 2 +-
.../components/data-grid-duplicate-cell.tsx | 23 ++
.../components/data-grid-readonly-cell.tsx | 22 +-
.../data-grid/components/data-grid-root.tsx | 6 +-
.../data-grid-toggleable-number-cell.tsx | 163 +++++++++++++
.../hooks/use-data-grid-cell-snapshot.tsx | 18 +-
.../data-grid/hooks/use-data-grid-cell.tsx | 18 +-
.../hooks/use-data-grid-form-handlers.tsx | 44 ++--
.../hooks/use-data-grid-keydown-event.tsx | 47 +++-
.../src/components/data-grid/types.ts | 8 +-
.../dashboard/src/hooks/api/inventory.tsx | 30 ++-
.../dashboard/src/hooks/api/products.tsx | 9 +-
.../providers/router-provider/route-map.tsx | 10 +
.../src/routes/inventory/common/constants.ts | 1 +
.../inventory-item-location-levels.tsx | 8 +-
.../inventory-item-reservations.tsx | 6 +-
.../use-location-list-table-columns.tsx | 4 +-
.../components/inventory-list-table.tsx | 26 ++-
.../use-inventory-table-columns.tsx | 33 ++-
.../components/inventory-stock-form/index.ts | 1 +
.../inventory-stock-form.tsx | 92 ++++++++
.../hooks/use-inventory-stock-columns.tsx | 66 ++++++
.../routes/inventory/inventory-stock/index.ts | 1 +
.../inventory-stock/inventory-stock.tsx | 50 ++++
.../inventory/inventory-stock/schema.ts | 21 ++
.../shipping-option-price-cell.tsx | 2 +-
.../hooks/use-price-list-grid-columns.tsx | 4 +-
.../src/routes/products/common/constants.ts | 1 +
.../product-variant-section.tsx | 32 ++-
.../use-variant-table-columns.tsx | 30 ++-
.../product-inventory-form/index.ts | 1 +
.../product-inventory-form.tsx | 176 ++++++++++++++
.../hooks/use-product-inventory-columns.tsx | 215 ++++++++++++++++++
.../hooks/use-product-inventory-data.tsx | 38 ++++
.../products/product-inventory/index.ts | 1 +
.../product-inventory/product-inventory.tsx | 26 +++
.../products/product-inventory/schema.ts | 32 +++
.../products/product-inventory/types.ts | 9 +
.../products/product-inventory/utils.ts | 48 ++++
.../steps/create-inventory-levels.ts | 1 -
.../workflows/batch-inventory-item-levels.ts | 57 +++++
.../workflows/bulk-create-delete-levels.ts | 2 +
.../workflows/delete-inventory-levels.ts | 51 ++++-
.../src/inventory/workflows/index.ts | 7 +-
.../core/js-sdk/src/admin/inventory-item.ts | 142 ++++++++----
.../src/http/inventory/admin/payloads.ts | 13 ++
.../inventory/mutations/inventory-level.ts | 8 +
.../location-levels/batch/route.ts | 20 ++
.../api/admin/inventory-items/middlewares.ts | 17 ++
.../api/admin/inventory-items/validators.ts | 17 ++
.../src/services/inventory-module.ts | 17 +-
53 files changed, 1611 insertions(+), 136 deletions(-)
create mode 100644 packages/admin/dashboard/src/components/data-grid/components/data-grid-duplicate-cell.tsx
create mode 100644 packages/admin/dashboard/src/components/data-grid/components/data-grid-toggleable-number-cell.tsx
create mode 100644 packages/admin/dashboard/src/routes/inventory/common/constants.ts
create mode 100644 packages/admin/dashboard/src/routes/inventory/inventory-stock/components/inventory-stock-form/index.ts
create mode 100644 packages/admin/dashboard/src/routes/inventory/inventory-stock/components/inventory-stock-form/inventory-stock-form.tsx
create mode 100644 packages/admin/dashboard/src/routes/inventory/inventory-stock/hooks/use-inventory-stock-columns.tsx
create mode 100644 packages/admin/dashboard/src/routes/inventory/inventory-stock/index.ts
create mode 100644 packages/admin/dashboard/src/routes/inventory/inventory-stock/inventory-stock.tsx
create mode 100644 packages/admin/dashboard/src/routes/inventory/inventory-stock/schema.ts
create mode 100644 packages/admin/dashboard/src/routes/products/common/constants.ts
create mode 100644 packages/admin/dashboard/src/routes/products/product-inventory/components/product-inventory-form/index.ts
create mode 100644 packages/admin/dashboard/src/routes/products/product-inventory/components/product-inventory-form/product-inventory-form.tsx
create mode 100644 packages/admin/dashboard/src/routes/products/product-inventory/hooks/use-product-inventory-columns.tsx
create mode 100644 packages/admin/dashboard/src/routes/products/product-inventory/hooks/use-product-inventory-data.tsx
create mode 100644 packages/admin/dashboard/src/routes/products/product-inventory/index.ts
create mode 100644 packages/admin/dashboard/src/routes/products/product-inventory/product-inventory.tsx
create mode 100644 packages/admin/dashboard/src/routes/products/product-inventory/schema.ts
create mode 100644 packages/admin/dashboard/src/routes/products/product-inventory/types.ts
create mode 100644 packages/admin/dashboard/src/routes/products/product-inventory/utils.ts
create mode 100644 packages/core/core-flows/src/inventory/workflows/batch-inventory-item-levels.ts
create mode 100644 packages/medusa/src/api/admin/inventory-items/location-levels/batch/route.ts
diff --git a/integration-tests/http/__tests__/inventory/admin/inventory.spec.ts b/integration-tests/http/__tests__/inventory/admin/inventory.spec.ts
index 5661b45e2bc59..392d42489d0ed 100644
--- a/integration-tests/http/__tests__/inventory/admin/inventory.spec.ts
+++ b/integration-tests/http/__tests__/inventory/admin/inventory.spec.ts
@@ -12,7 +12,7 @@ medusaIntegrationTestRunner({
let inventoryItem2
let stockLocation1
let stockLocation2
-
+ let stockLocation3
beforeEach(async () => {
await createAdminUser(dbConnection, adminHeaders, getContainer())
@@ -24,6 +24,10 @@ medusaIntegrationTestRunner({
await api.post(`/admin/stock-locations`, { name: "loc2" }, adminHeaders)
).data.stock_location
+ stockLocation3 = (
+ await api.post(`/admin/stock-locations`, { name: "loc3" }, adminHeaders)
+ ).data.stock_location
+
inventoryItem1 = (
await api.post(
`/admin/inventory-items`,
@@ -122,6 +126,69 @@ medusaIntegrationTestRunner({
})
})
+ describe("POST /admin/inventory-items/location-levels/batch", () => {
+ beforeEach(async () => {
+ await api.post(
+ `/admin/inventory-items/${inventoryItem1.id}/location-levels`,
+ {
+ location_id: stockLocation1.id,
+ stocked_quantity: 0,
+ },
+ adminHeaders
+ )
+
+ await api.post(
+ `/admin/inventory-items/${inventoryItem1.id}/location-levels`,
+ {
+ location_id: stockLocation2.id,
+ stocked_quantity: 10,
+ },
+ adminHeaders
+ )
+ })
+
+ it("should batch update the inventory levels", async () => {
+ const result = await api.post(
+ `/admin/inventory-items/location-levels/batch`,
+ {
+ update: [
+ {
+ location_id: stockLocation1.id,
+ inventory_item_id: inventoryItem1.id,
+ stocked_quantity: 10,
+ },
+ {
+ location_id: stockLocation2.id,
+ inventory_item_id: inventoryItem1.id,
+ stocked_quantity: 20,
+ },
+ ],
+ },
+ adminHeaders
+ )
+
+ expect(result.status).toEqual(200)
+ })
+
+ it("should batch create the inventory levels", async () => {
+ const result = await api.post(
+ `/admin/inventory-items/location-levels/batch`,
+ {
+ create: [
+ {
+ location_id: stockLocation3.id,
+ inventory_item_id: inventoryItem1.id,
+ stocked_quantity: 10,
+ },
+ ],
+ },
+ adminHeaders
+ )
+
+ expect(result.status).toEqual(200)
+ })
+ })
+
describe("POST /admin/inventory-items/:id/location-levels/batch", () => {
beforeEach(async () => {
await api.post(
diff --git a/packages/admin/dashboard/src/components/common/thumbnail/thumbnail.tsx b/packages/admin/dashboard/src/components/common/thumbnail/thumbnail.tsx
index ffd0002f12a7d..5e32de1cda5d3 100644
--- a/packages/admin/dashboard/src/components/common/thumbnail/thumbnail.tsx
+++ b/packages/admin/dashboard/src/components/common/thumbnail/thumbnail.tsx
@@ -11,7 +11,7 @@ export const Thumbnail = ({ src, alt, size = "base" }: ThumbnailProps) => {
return (
)}
diff --git a/packages/admin/dashboard/src/components/data-grid/components/data-grid-duplicate-cell.tsx b/packages/admin/dashboard/src/components/data-grid/components/data-grid-duplicate-cell.tsx
new file mode 100644
index 0000000000000..fc5577cd48ab0
--- /dev/null
+++ b/packages/admin/dashboard/src/components/data-grid/components/data-grid-duplicate-cell.tsx
@@ -0,0 +1,23 @@
+import { ReactNode } from "react"
+import { useWatch } from "react-hook-form"
+import { useDataGridCell } from "../hooks"
+import { DataGridCellProps } from "../types"
+
+export const DataGridDuplicateCell = ({
+ context,
+ children,
+}: DataGridCellProps & {
+ children?: ReactNode | ((props: { value: TValue }) => ReactNode)
+}) => {
+ const { field, control } = useDataGridCell({
+ context,
+ })
+
+ const value = useWatch({ control, name: field })
+
+ return (
+
+ {typeof children === "function" ? children({ value: value }) : children}
+
+ )
+}
diff --git a/packages/admin/dashboard/src/components/data-grid/components/data-grid-readonly-cell.tsx b/packages/admin/dashboard/src/components/data-grid/components/data-grid-readonly-cell.tsx
index bc00e80f899b9..cf842684ae0cc 100644
--- a/packages/admin/dashboard/src/components/data-grid/components/data-grid-readonly-cell.tsx
+++ b/packages/admin/dashboard/src/components/data-grid/components/data-grid-readonly-cell.tsx
@@ -1,24 +1,32 @@
import { PropsWithChildren } from "react"
+import { clx } from "@medusajs/ui"
import { useDataGridCellError } from "../hooks"
import { DataGridCellProps } from "../types"
import { DataGridRowErrorIndicator } from "./data-grid-row-error-indicator"
-type DataGridReadonlyCellProps = DataGridCellProps<
- TData,
- TValue
-> &
- PropsWithChildren
+type DataGridReadonlyCellProps = PropsWithChildren<
+ DataGridCellProps
+> & {
+ color?: "muted" | "normal"
+}
export const DataGridReadonlyCell = ({
context,
+ color = "muted",
children,
}: DataGridReadonlyCellProps) => {
const { rowErrors } = useDataGridCellError({ context })
return (
-
-
{children}
+
)
diff --git a/packages/admin/dashboard/src/components/data-grid/components/data-grid-root.tsx b/packages/admin/dashboard/src/components/data-grid/components/data-grid-root.tsx
index d8d62536bb717..cd2195441a4eb 100644
--- a/packages/admin/dashboard/src/components/data-grid/components/data-grid-root.tsx
+++ b/packages/admin/dashboard/src/components/data-grid/components/data-grid-root.tsx
@@ -50,7 +50,7 @@ import { isCellMatch, isSpecialFocusKey } from "../utils"
import { DataGridKeyboardShortcutModal } from "./data-grid-keyboard-shortcut-modal"
export interface DataGridRootProps<
TData,
- TFieldValues extends FieldValues = FieldValues,
+ TFieldValues extends FieldValues = FieldValues
> {
data?: TData[]
columns: ColumnDef
[]
@@ -90,13 +90,12 @@ const getCommonPinningStyles = (
/**
* TODO:
- * - [Minor] Add shortcuts overview modal.
* - [Minor] Extend the commands to also support modifying the anchor and rangeEnd, to restore the previous focus after undo/redo.
*/
export const DataGridRoot = <
TData,
- TFieldValues extends FieldValues = FieldValues,
+ TFieldValues extends FieldValues = FieldValues
>({
data = [],
columns,
@@ -333,6 +332,7 @@ export const DataGridRoot = <
setSelectionValues,
onEditingChangeHandler,
restoreSnapshot,
+ createSnapshot,
setSingleRange,
scrollToCoordinates,
execute,
diff --git a/packages/admin/dashboard/src/components/data-grid/components/data-grid-toggleable-number-cell.tsx b/packages/admin/dashboard/src/components/data-grid/components/data-grid-toggleable-number-cell.tsx
new file mode 100644
index 0000000000000..d131fbc01b0ed
--- /dev/null
+++ b/packages/admin/dashboard/src/components/data-grid/components/data-grid-toggleable-number-cell.tsx
@@ -0,0 +1,163 @@
+import { Switch } from "@medusajs/ui"
+import { useEffect, useRef, useState } from "react"
+import CurrencyInput, { CurrencyInputProps } from "react-currency-input-field"
+import { Controller, ControllerRenderProps } from "react-hook-form"
+import { useCombinedRefs } from "../../../hooks/use-combined-refs"
+import { useDataGridCell, useDataGridCellError } from "../hooks"
+import { DataGridCellProps, InputProps } from "../types"
+import { DataGridCellContainer } from "./data-grid-cell-container"
+
+export const DataGridTogglableNumberCell = ({
+ context,
+ ...rest
+}: DataGridCellProps & {
+ min?: number
+ max?: number
+ placeholder?: string
+}) => {
+ const { field, control, renderProps } = useDataGridCell({
+ context,
+ })
+ const errorProps = useDataGridCellError({ context })
+
+ const { container, input } = renderProps
+
+ return (
+ {
+ return (
+
+ }
+ >
+
+
+ )
+ }}
+ />
+ )
+}
+
+const OuterComponent = ({
+ field,
+ inputProps,
+ isAnchor,
+}: {
+ field: ControllerRenderProps
+ inputProps: InputProps
+ isAnchor: boolean
+}) => {
+ const buttonRef = useRef(null)
+ const { value } = field
+ const { onChange } = inputProps
+
+ const [localValue, setLocalValue] = useState(value)
+
+ useEffect(() => {
+ setLocalValue(value)
+ }, [value])
+
+ const handleCheckedChange = (update: boolean) => {
+ const newValue = { ...localValue, checked: update }
+ setLocalValue(newValue)
+ onChange(newValue, value)
+ }
+
+ useEffect(() => {
+ const handleKeyDown = (e: KeyboardEvent) => {
+ if (isAnchor && e.key.toLowerCase() === "x") {
+ e.preventDefault()
+ buttonRef.current?.click()
+ }
+ }
+
+ document.addEventListener("keydown", handleKeyDown)
+ return () => document.removeEventListener("keydown", handleKeyDown)
+ }, [isAnchor])
+
+ return (
+
+
+
+ )
+}
+
+const Inner = ({
+ field,
+ inputProps,
+ ...props
+}: {
+ field: ControllerRenderProps
+ inputProps: InputProps
+ min?: number
+ max?: number
+ placeholder?: string
+}) => {
+ const { ref, value, onChange: _, onBlur, ...fieldProps } = field
+ const {
+ ref: inputRef,
+ onChange,
+ onBlur: onInputBlur,
+ onFocus,
+ ...attributes
+ } = inputProps
+
+ const [localValue, setLocalValue] = useState(value)
+
+ useEffect(() => {
+ setLocalValue(value)
+ }, [value])
+
+ const combinedRefs = useCombinedRefs(inputRef, ref)
+
+ const handleInputChange: CurrencyInputProps["onValueChange"] = (
+ value,
+ _name,
+ _values
+ ) => {
+ const ensuredValue = value || ""
+
+ const newValue = { ...localValue, quantity: ensuredValue }
+ setLocalValue(newValue)
+ }
+
+ return (
+
+ {
+ onBlur()
+ onInputBlur()
+
+ onChange(localValue, value)
+ }}
+ onFocus={onFocus}
+ decimalsLimit={0}
+ autoComplete="off"
+ tabIndex={-1}
+ />
+
+ )
+}
diff --git a/packages/admin/dashboard/src/components/data-grid/hooks/use-data-grid-cell-snapshot.tsx b/packages/admin/dashboard/src/components/data-grid/hooks/use-data-grid-cell-snapshot.tsx
index ffc3dcbf1a5c1..9f73327cc5509 100644
--- a/packages/admin/dashboard/src/components/data-grid/hooks/use-data-grid-cell-snapshot.tsx
+++ b/packages/admin/dashboard/src/components/data-grid/hooks/use-data-grid-cell-snapshot.tsx
@@ -15,9 +15,8 @@ export const useDataGridCellSnapshot = <
matrix,
form,
}: UseDataGridCellSnapshotOptions) => {
- const [snapshot, setSnapshot] = useState | null>(
- null
- )
+ const [snapshot, setSnapshot] =
+ useState | null>(null)
const { getValues, setValue } = form
@@ -38,7 +37,18 @@ export const useDataGridCellSnapshot = <
const value = getValues(field as Path)
- setSnapshot({ field, value })
+ setSnapshot((curr) => {
+ /**
+ * If there already exists a snapshot for this field, we don't want to create a new one.
+ * A case where this happens is when the user presses the space key on a field. In that case
+ * we create a snapshot of the value before its destroyed by the space key.
+ */
+ if (curr?.field === field) {
+ return curr
+ }
+
+ return { field, value }
+ })
},
[getValues, matrix]
)
diff --git a/packages/admin/dashboard/src/components/data-grid/hooks/use-data-grid-cell.tsx b/packages/admin/dashboard/src/components/data-grid/hooks/use-data-grid-cell.tsx
index 3f6ee47b2e2ad..1fa693f2e68c9 100644
--- a/packages/admin/dashboard/src/components/data-grid/hooks/use-data-grid-cell.tsx
+++ b/packages/admin/dashboard/src/components/data-grid/hooks/use-data-grid-cell.tsx
@@ -123,16 +123,16 @@ export const useDataGridCell = ({
const validateKeyStroke = useCallback(
(key: string) => {
- if (type === "number") {
- return numberCharacterRegex.test(key)
+ switch (type) {
+ case "togglable-number":
+ case "number":
+ return numberCharacterRegex.test(key)
+ case "text":
+ return textCharacterRegex.test(key)
+ default:
+ // KeyboardEvents should not be forwareded to other types of cells
+ return false
}
-
- if (type === "text") {
- return textCharacterRegex.test(key)
- }
-
- // KeyboardEvents should not be forwareded to other types of cells
- return false
},
[type]
)
diff --git a/packages/admin/dashboard/src/components/data-grid/hooks/use-data-grid-form-handlers.tsx b/packages/admin/dashboard/src/components/data-grid/hooks/use-data-grid-form-handlers.tsx
index bee8c62bac50b..f1224420cfb1a 100644
--- a/packages/admin/dashboard/src/components/data-grid/hooks/use-data-grid-form-handlers.tsx
+++ b/packages/admin/dashboard/src/components/data-grid/hooks/use-data-grid-form-handlers.tsx
@@ -1,3 +1,4 @@
+import set from "lodash/set"
import { useCallback } from "react"
import { FieldValues, Path, PathValue, UseFormReturn } from "react-hook-form"
@@ -12,13 +13,13 @@ type UseDataGridFormHandlersOptions = {
export const useDataGridFormHandlers = <
TData,
- TFieldValues extends FieldValues,
+ TFieldValues extends FieldValues
>({
matrix,
form,
anchor,
}: UseDataGridFormHandlersOptions) => {
- const { getValues, setValue } = form
+ const { getValues, reset } = form
const getSelectionValues = useCallback(
(fields: string[]): PathValue>[] => {
@@ -26,9 +27,10 @@ export const useDataGridFormHandlers = <
return []
}
+ const allValues = getValues()
return fields.map((field) => {
- return getValues(field as Path)
- })
+ return field.split(".").reduce((obj, key) => obj?.[key], allValues)
+ }) as PathValue>[]
},
[getValues]
)
@@ -40,12 +42,12 @@ export const useDataGridFormHandlers = <
}
const type = matrix.getCellType(anchor)
-
if (!type) {
return
}
const convertedValues = convertArrayToPrimitive(values, type)
+ const currentValues = getValues()
fields.forEach((field, index) => {
if (!field) {
@@ -53,18 +55,18 @@ export const useDataGridFormHandlers = <
}
const valueIndex = index % values.length
- const value = convertedValues[valueIndex] as PathValue<
- TFieldValues,
- Path
- >
-
- setValue(field as Path, value, {
- shouldDirty: true,
- shouldTouch: true,
- })
+ const value = convertedValues[valueIndex]
+
+ set(currentValues, field, value)
+ })
+
+ reset(currentValues, {
+ keepDirty: true,
+ keepTouched: true,
+ keepDefaultValues: true,
})
},
- [matrix, anchor, setValue]
+ [matrix, anchor, getValues, reset]
)
return {
@@ -119,7 +121,17 @@ export function convertArrayToPrimitive(
): any[] {
switch (type) {
case "number":
- return values.map((v) => (v === "" ? v : convertToNumber(v)))
+ return values.map((v) => {
+ if (v === "") {
+ return v
+ }
+
+ if (v == null) {
+ return ""
+ }
+
+ return convertToNumber(v)
+ })
case "boolean":
return values.map(convertToBoolean)
case "text":
diff --git a/packages/admin/dashboard/src/components/data-grid/hooks/use-data-grid-keydown-event.tsx b/packages/admin/dashboard/src/components/data-grid/hooks/use-data-grid-keydown-event.tsx
index 3e7da7b1a2a3e..8e7030291b1a6 100644
--- a/packages/admin/dashboard/src/components/data-grid/hooks/use-data-grid-keydown-event.tsx
+++ b/packages/admin/dashboard/src/components/data-grid/hooks/use-data-grid-keydown-event.tsx
@@ -1,4 +1,4 @@
-import { useCallback } from "react"
+import React, { useCallback } from "react"
import type {
FieldValues,
Path,
@@ -39,6 +39,7 @@ type UseDataGridKeydownEventOptions = {
) => PathValue>[]
setSelectionValues: (fields: string[], values: string[]) => void
restoreSnapshot: () => void
+ createSnapshot: (coords: DataGridCoordinates) => void
}
const ARROW_KEYS = ["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"]
@@ -67,6 +68,7 @@ export const useDataGridKeydownEvent = <
getSelectionValues,
setSelectionValues,
restoreSnapshot,
+ createSnapshot,
}: UseDataGridKeydownEventOptions) => {
const handleKeyboardNavigation = useCallback(
(e: KeyboardEvent) => {
@@ -218,6 +220,8 @@ export const useDataGridKeydownEvent = <
return
}
+ createSnapshot(anchor)
+
const current = getValues(field as Path)
const next = ""
@@ -236,7 +240,39 @@ export const useDataGridKeydownEvent = <
input.focus()
},
- [matrix, queryTool, getValues, execute, setValue]
+ [matrix, queryTool, getValues, execute, setValue, createSnapshot]
+ )
+
+ const handleSpaceKeyTogglableNumber = useCallback(
+ (anchor: DataGridCoordinates) => {
+ const field = matrix.getCellField(anchor)
+ const input = queryTool?.getInput(anchor)
+
+ if (!field || !input) {
+ return
+ }
+
+ createSnapshot(anchor)
+
+ const current = getValues(field as Path)
+ const next = { ...current, quantity: "" }
+
+ const command = new DataGridUpdateCommand({
+ next,
+ prev: current,
+ setter: (value) => {
+ setValue(field as Path, value, {
+ shouldDirty: true,
+ shouldTouch: true,
+ })
+ },
+ })
+
+ execute(command)
+
+ input.focus()
+ },
+ [matrix, queryTool, getValues, execute, setValue, createSnapshot]
)
const handleSpaceKey = useCallback(
@@ -257,6 +293,9 @@ export const useDataGridKeydownEvent = <
case "boolean":
handleSpaceKeyBoolean(anchor)
break
+ case "togglable-number":
+ handleSpaceKeyTogglableNumber(anchor)
+ break
case "number":
case "text":
handleSpaceKeyTextOrNumber(anchor)
@@ -269,6 +308,7 @@ export const useDataGridKeydownEvent = <
matrix,
handleSpaceKeyBoolean,
handleSpaceKeyTextOrNumber,
+ handleSpaceKeyTogglableNumber,
]
)
@@ -390,6 +430,7 @@ export const useDataGridKeydownEvent = <
const type = matrix.getCellType(anchor)
switch (type) {
+ case "togglable-number":
case "text":
case "number":
handleEnterKeyTextOrNumber(e, anchor)
@@ -518,7 +559,7 @@ export const useDataGridKeydownEvent = <
break
}
},
- [anchor, isEditing, setTrapActive, containerRef]
+ [isEditing, setTrapActive, containerRef]
)
const handleKeyDownEvent = useCallback(
diff --git a/packages/admin/dashboard/src/components/data-grid/types.ts b/packages/admin/dashboard/src/components/data-grid/types.ts
index 3f59ecbb70f45..857396584ea8a 100644
--- a/packages/admin/dashboard/src/components/data-grid/types.ts
+++ b/packages/admin/dashboard/src/components/data-grid/types.ts
@@ -14,7 +14,11 @@ import {
PathValue,
} from "react-hook-form"
-export type DataGridColumnType = "text" | "number" | "boolean"
+export type DataGridColumnType =
+ | "text"
+ | "number"
+ | "boolean"
+ | "togglable-number"
export type DataGridCoordinates = {
row: number
@@ -100,7 +104,7 @@ export interface DataGridCellContainerProps extends PropsWithChildren<{}> {
}
export type DataGridCellSnapshot<
- TFieldValues extends FieldValues = FieldValues
+ TFieldValues extends FieldValues = FieldValues,
> = {
field: string
value: PathValue>
diff --git a/packages/admin/dashboard/src/hooks/api/inventory.tsx b/packages/admin/dashboard/src/hooks/api/inventory.tsx
index 93f7c867c8c23..8a73f4c42ebbc 100644
--- a/packages/admin/dashboard/src/hooks/api/inventory.tsx
+++ b/packages/admin/dashboard/src/hooks/api/inventory.tsx
@@ -1,3 +1,4 @@
+import { FetchError } from "@medusajs/js-sdk"
import { HttpTypes } from "@medusajs/types"
import {
QueryKey,
@@ -6,11 +7,11 @@ import {
useMutation,
useQuery,
} from "@tanstack/react-query"
-import { FetchError } from "@medusajs/js-sdk"
import { sdk } from "../../lib/client"
import { queryClient } from "../../lib/query-client"
import { queryKeysFactory } from "../../lib/query-key-factory"
+import { variantsQueryKeys } from "./products"
const INVENTORY_ITEMS_QUERY_KEY = "inventory_items" as const
export const inventoryItemsQueryKeys = queryKeysFactory(
@@ -23,7 +24,7 @@ export const inventoryItemLevelsQueryKeys = queryKeysFactory(
)
export const useInventoryItems = (
- query?: Record,
+ query?: HttpTypes.AdminInventoryItemParams,
options?: Omit<
UseQueryOptions<
HttpTypes.AdminInventoryItemListResponse,
@@ -136,7 +137,7 @@ export const useDeleteInventoryItemLevel = (
inventoryItemId: string,
locationId: string,
options?: UseMutationOptions<
- HttpTypes.AdminInventoryItemDeleteResponse,
+ HttpTypes.AdminInventoryLevelDeleteResponse,
FetchError,
void
>
@@ -236,3 +237,26 @@ export const useBatchUpdateInventoryLevels = (
...options,
})
}
+
+export const useBatchInventoryLevels = (
+ options?: UseMutationOptions<
+ HttpTypes.AdminInventoryItemResponse,
+ FetchError,
+ HttpTypes.AdminBatchInventoryItemLevels
+ >
+) => {
+ return useMutation({
+ mutationFn: (payload: HttpTypes.AdminBatchInventoryItemLevels) =>
+ sdk.admin.inventoryItem._batchUpdateLevels(payload),
+ onSuccess: (data, variables, context) => {
+ queryClient.invalidateQueries({
+ queryKey: inventoryItemsQueryKeys.all,
+ })
+ queryClient.invalidateQueries({
+ queryKey: variantsQueryKeys.lists(),
+ })
+ options?.onSuccess?.(data, variables, context)
+ },
+ ...options,
+ })
+}
diff --git a/packages/admin/dashboard/src/hooks/api/products.tsx b/packages/admin/dashboard/src/hooks/api/products.tsx
index edb4a68b38135..eee11f83d89c1 100644
--- a/packages/admin/dashboard/src/hooks/api/products.tsx
+++ b/packages/admin/dashboard/src/hooks/api/products.tsx
@@ -110,9 +110,14 @@ export const useProductVariant = (
export const useProductVariants = (
productId: string,
- query?: Record,
+ query?: HttpTypes.AdminProductVariantParams,
options?: Omit<
- UseQueryOptions,
+ UseQueryOptions<
+ HttpTypes.AdminProductVariantListResponse,
+ FetchError,
+ HttpTypes.AdminProductVariantListResponse,
+ QueryKey
+ >,
"queryFn" | "queryKey"
>
) => {
diff --git a/packages/admin/dashboard/src/providers/router-provider/route-map.tsx b/packages/admin/dashboard/src/providers/router-provider/route-map.tsx
index 0862cb3371199..f622d165812e3 100644
--- a/packages/admin/dashboard/src/providers/router-provider/route-map.tsx
+++ b/packages/admin/dashboard/src/providers/router-provider/route-map.tsx
@@ -129,6 +129,11 @@ export const RouteMap: RouteObject[] = [
"../../routes/products/product-create-variant"
),
},
+ {
+ path: "inventory",
+ lazy: () =>
+ import("../../routes/products/product-inventory"),
+ },
{
path: "metadata/edit",
lazy: () =>
@@ -764,6 +769,11 @@ export const RouteMap: RouteObject[] = [
lazy: () =>
import("../../routes/inventory/inventory-create"),
},
+ {
+ path: "stock",
+ lazy: () =>
+ import("../../routes/inventory/inventory-stock"),
+ },
],
},
{
diff --git a/packages/admin/dashboard/src/routes/inventory/common/constants.ts b/packages/admin/dashboard/src/routes/inventory/common/constants.ts
new file mode 100644
index 0000000000000..81cf300172386
--- /dev/null
+++ b/packages/admin/dashboard/src/routes/inventory/common/constants.ts
@@ -0,0 +1 @@
+export const INVENTORY_ITEM_IDS_KEY = "inventory_item_ids"
diff --git a/packages/admin/dashboard/src/routes/inventory/inventory-detail/components/inventory-item-location-levels.tsx b/packages/admin/dashboard/src/routes/inventory/inventory-detail/components/inventory-item-location-levels.tsx
index 3ca7071f6586b..8904d645bbfff 100644
--- a/packages/admin/dashboard/src/routes/inventory/inventory-detail/components/inventory-item-location-levels.tsx
+++ b/packages/admin/dashboard/src/routes/inventory/inventory-detail/components/inventory-item-location-levels.tsx
@@ -1,8 +1,8 @@
+import { HttpTypes } from "@medusajs/types"
import { Button, Container, Heading } from "@medusajs/ui"
-import { ItemLocationListTable } from "./location-levels-table/location-list-table"
-import { Link } from "react-router-dom"
import { useTranslation } from "react-i18next"
-import { HttpTypes } from "@medusajs/types"
+import { Link } from "react-router-dom"
+import { ItemLocationListTable } from "./location-levels-table/location-list-table"
type InventoryItemLocationLevelsSectionProps = {
inventoryItem: HttpTypes.AdminInventoryItemResponse["inventory_item"]
@@ -13,7 +13,7 @@ export const InventoryItemLocationLevelsSection = ({
const { t } = useTranslation()
return (
-
+
{t("inventory.locationLevels")}