feat(inventory,dashboard,types,core-flows,js-sdk,medusa): Improve inventory UX (#10630)
* feat(dashboard): Add UI for bulk editing inventory stock (#10556) * progress * cleanup types * add changeset * fix 0 values * format schema * add delete event and allow copy/pasting enabled for some fields * add response types * add tests * work on fixing setValue behaviour * cleanup toggle logic * add loading state * format schema * add support for bidirectional actions in DataGrid and update Checkbox and RadioGroup * update lock * lint * fix 404 * address feedback * update cursor on bidirectional select
This commit is contained in:
1 parent
c5915451b8
commit
bc22b81cdf
82 files changed
+2667
-236
No files matched your search
@@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/inventory": patch
|
||||||
|
"@medusajs/dashboard": patch
|
||||||
|
"@medusajs/core-flows": patch
|
||||||
|
"@medusajs/js-sdk": patch
|
||||||
|
"@medusajs/types": patch
|
||||||
|
"@medusajs/medusa": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
feat(inventory,dashboard,core-flows,js-sdk,types,medusa): Improve inventory management UX
|
||||||
@@ -12,7 +12,7 @@ medusaIntegrationTestRunner({
|
|||||||
let inventoryItem2
|
let inventoryItem2
|
||||||
let stockLocation1
|
let stockLocation1
|
||||||
let stockLocation2
|
let stockLocation2
|
||||||
|
let stockLocation3
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await createAdminUser(dbConnection, adminHeaders, getContainer())
|
await createAdminUser(dbConnection, adminHeaders, getContainer())
|
||||||
|
|
||||||
@@ -24,6 +24,10 @@ medusaIntegrationTestRunner({
|
|||||||
await api.post(`/admin/stock-locations`, { name: "loc2" }, adminHeaders)
|
await api.post(`/admin/stock-locations`, { name: "loc2" }, adminHeaders)
|
||||||
).data.stock_location
|
).data.stock_location
|
||||||
|
|
||||||
|
stockLocation3 = (
|
||||||
|
await api.post(`/admin/stock-locations`, { name: "loc3" }, adminHeaders)
|
||||||
|
).data.stock_location
|
||||||
|
|
||||||
inventoryItem1 = (
|
inventoryItem1 = (
|
||||||
await api.post(
|
await api.post(
|
||||||
`/admin/inventory-items`,
|
`/admin/inventory-items`,
|
||||||
@@ -122,9 +126,152 @@ medusaIntegrationTestRunner({
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("POST /admin/inventory-items/:id/location-levels/batch", () => {
|
describe("POST /admin/inventory-items/location-levels/batch", () => {
|
||||||
|
let locationLevel1
|
||||||
|
let locationLevel2
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await api.post(
|
const seed = await api.post(
|
||||||
|
`/admin/inventory-items/${inventoryItem1.id}/location-levels/batch`,
|
||||||
|
{
|
||||||
|
create: [
|
||||||
|
{
|
||||||
|
location_id: stockLocation1.id,
|
||||||
|
stocked_quantity: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
location_id: stockLocation2.id,
|
||||||
|
stocked_quantity: 10,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
locationLevel1 = seed.data.created[0]
|
||||||
|
locationLevel2 = seed.data.created[1]
|
||||||
|
})
|
||||||
|
|
||||||
|
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)
|
||||||
|
expect(result.data).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
updated: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
location_id: stockLocation1.id,
|
||||||
|
inventory_item_id: inventoryItem1.id,
|
||||||
|
stocked_quantity: 10,
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
location_id: stockLocation2.id,
|
||||||
|
inventory_item_id: inventoryItem1.id,
|
||||||
|
stocked_quantity: 20,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
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)
|
||||||
|
expect(result.data).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
created: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
location_id: stockLocation3.id,
|
||||||
|
inventory_item_id: inventoryItem1.id,
|
||||||
|
stocked_quantity: 10,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should batch delete the inventory levels when stocked quantity is 0 and force is false", async () => {
|
||||||
|
const result = await api.post(
|
||||||
|
`/admin/inventory-items/location-levels/batch`,
|
||||||
|
{ delete: [locationLevel1.id] },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result.status).toEqual(200)
|
||||||
|
expect(result.data).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
deleted: [locationLevel1.id],
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should not delete the inventory levels when stocked quantity is greater than 0 and force is false", async () => {
|
||||||
|
const error = await api
|
||||||
|
.post(
|
||||||
|
`/admin/inventory-items/location-levels/batch`,
|
||||||
|
{ delete: [locationLevel2.id] },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
.catch((e) => e)
|
||||||
|
|
||||||
|
expect(error.response.status).toEqual(400)
|
||||||
|
expect(error.response.data).toEqual({
|
||||||
|
type: "not_allowed",
|
||||||
|
message: `Cannot remove Inventory Levels for ${stockLocation2.id} because there are stocked items at the locations. Use force flag to delete anyway.`,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should delete the inventory levels when stocked quantity is greater than 0 and force is true", async () => {
|
||||||
|
const result = await api.post(
|
||||||
|
`/admin/inventory-items/location-levels/batch`,
|
||||||
|
{ delete: [locationLevel2.id], force: true },
|
||||||
|
adminHeaders
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(result.status).toEqual(200)
|
||||||
|
expect(result.data).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
deleted: [locationLevel2.id],
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("POST /admin/inventory-items/:id/location-levels/batch", () => {
|
||||||
|
let locationLevel1
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const seed = await api.post(
|
||||||
`/admin/inventory-items/${inventoryItem1.id}/location-levels`,
|
`/admin/inventory-items/${inventoryItem1.id}/location-levels`,
|
||||||
{
|
{
|
||||||
location_id: stockLocation1.id,
|
location_id: stockLocation1.id,
|
||||||
@@ -132,6 +279,8 @@ medusaIntegrationTestRunner({
|
|||||||
},
|
},
|
||||||
adminHeaders
|
adminHeaders
|
||||||
)
|
)
|
||||||
|
|
||||||
|
locationLevel1 = seed.data.inventory_item.location_levels[0]
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should delete an inventory location level and create a new one", async () => {
|
it("should delete an inventory location level and create a new one", async () => {
|
||||||
@@ -139,7 +288,8 @@ medusaIntegrationTestRunner({
|
|||||||
`/admin/inventory-items/${inventoryItem1.id}/location-levels/batch`,
|
`/admin/inventory-items/${inventoryItem1.id}/location-levels/batch`,
|
||||||
{
|
{
|
||||||
create: [{ location_id: "location_2" }],
|
create: [{ location_id: "location_2" }],
|
||||||
delete: [stockLocation1.id],
|
delete: [locationLevel1.id],
|
||||||
|
force: true,
|
||||||
},
|
},
|
||||||
adminHeaders
|
adminHeaders
|
||||||
)
|
)
|
||||||
@@ -154,7 +304,7 @@ medusaIntegrationTestRunner({
|
|||||||
expect(levelsListResult.data.inventory_levels).toHaveLength(1)
|
expect(levelsListResult.data.inventory_levels).toHaveLength(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should not delete an inventory location level when there is stocked items", async () => {
|
it("should not delete an inventory location level when there is stocked items without force", async () => {
|
||||||
await api.post(
|
await api.post(
|
||||||
`/admin/inventory-items/${inventoryItem1.id}/location-levels/${stockLocation1.id}`,
|
`/admin/inventory-items/${inventoryItem1.id}/location-levels/${stockLocation1.id}`,
|
||||||
{ stocked_quantity: 10 },
|
{ stocked_quantity: 10 },
|
||||||
@@ -164,7 +314,7 @@ medusaIntegrationTestRunner({
|
|||||||
const { response } = await api
|
const { response } = await api
|
||||||
.post(
|
.post(
|
||||||
`/admin/inventory-items/${inventoryItem1.id}/location-levels/batch`,
|
`/admin/inventory-items/${inventoryItem1.id}/location-levels/batch`,
|
||||||
{ delete: [stockLocation1.id] },
|
{ delete: [locationLevel1.id] },
|
||||||
adminHeaders
|
adminHeaders
|
||||||
)
|
)
|
||||||
.catch((e) => e)
|
.catch((e) => e)
|
||||||
@@ -172,7 +322,7 @@ medusaIntegrationTestRunner({
|
|||||||
expect(response.status).toEqual(400)
|
expect(response.status).toEqual(400)
|
||||||
expect(response.data).toEqual({
|
expect(response.data).toEqual({
|
||||||
type: "not_allowed",
|
type: "not_allowed",
|
||||||
message: `Cannot remove Inventory Levels for ${stockLocation1.id} because there are stocked or reserved items at the locations`,
|
message: `Cannot remove Inventory Levels for ${stockLocation1.id} because there are stocked items at the locations. Use force flag to delete anyway.`,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -56,12 +56,12 @@
|
|||||||
"@uiw/react-json-view": "^2.0.0-alpha.17",
|
"@uiw/react-json-view": "^2.0.0-alpha.17",
|
||||||
"cmdk": "^0.2.0",
|
"cmdk": "^0.2.0",
|
||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
"framer-motion": "^11.0.3",
|
|
||||||
"i18next": "23.7.11",
|
"i18next": "23.7.11",
|
||||||
"i18next-browser-languagedetector": "7.2.0",
|
"i18next-browser-languagedetector": "7.2.0",
|
||||||
"i18next-http-backend": "2.4.2",
|
"i18next-http-backend": "2.4.2",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"match-sorter": "^6.3.4",
|
"match-sorter": "^6.3.4",
|
||||||
|
"motion": "^11.15.0",
|
||||||
"qs": "^6.12.0",
|
"qs": "^6.12.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-country-flag": "^3.1.0",
|
"react-country-flag": "^3.1.0",
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { motion } from "framer-motion"
|
import { motion } from "motion/react"
|
||||||
|
|
||||||
import { IconAvatar } from "../icon-avatar"
|
import { IconAvatar } from "../icon-avatar"
|
||||||
|
|
||||||
@@ -6,7 +6,7 @@ export default function AvatarBox({ checked }: { checked?: boolean }) {
|
|||||||
return (
|
return (
|
||||||
<IconAvatar
|
<IconAvatar
|
||||||
size="xlarge"
|
size="xlarge"
|
||||||
className="bg-ui-button-neutral shadow-buttons-neutral after:button-neutral-gradient relative mb-4 flex items-center justify-center rounded-xl after:inset-0 after:content-[''] w-[52px] h-[52px]"
|
className="bg-ui-button-neutral shadow-buttons-neutral after:button-neutral-gradient relative mb-4 flex h-[52px] w-[52px] items-center justify-center rounded-xl after:inset-0 after:content-['']"
|
||||||
>
|
>
|
||||||
{checked && (
|
{checked && (
|
||||||
<motion.div
|
<motion.div
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { clx } from "@medusajs/ui"
|
import { clx } from "@medusajs/ui"
|
||||||
import { Transition, motion } from "framer-motion"
|
import { Transition, motion } from "motion/react"
|
||||||
|
|
||||||
type LogoBoxProps = {
|
type LogoBoxProps = {
|
||||||
className?: string
|
className?: string
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./progress-bar"
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { motion } from "motion/react"
|
||||||
|
|
||||||
|
interface ProgressBarProps {
|
||||||
|
/**
|
||||||
|
* The duration of the animation in seconds.
|
||||||
|
*
|
||||||
|
* @default 2
|
||||||
|
*/
|
||||||
|
duration?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProgressBar = ({ duration = 2 }: ProgressBarProps) => {
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
className="bg-ui-fg-subtle size-full"
|
||||||
|
initial={{
|
||||||
|
width: "0%",
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
delay: 0.2,
|
||||||
|
duration,
|
||||||
|
ease: "linear",
|
||||||
|
}}
|
||||||
|
animate={{
|
||||||
|
width: "90%",
|
||||||
|
}}
|
||||||
|
exit={{
|
||||||
|
width: "100%",
|
||||||
|
transition: { duration: 0.2, ease: "linear" },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -11,7 +11,7 @@ export const Thumbnail = ({ src, alt, size = "base" }: ThumbnailProps) => {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clx(
|
className={clx(
|
||||||
"bg-ui-bg-component flex items-center justify-center overflow-hidden rounded-[4px]",
|
"bg-ui-bg-component border-ui-border-base flex items-center justify-center overflow-hidden rounded border",
|
||||||
{
|
{
|
||||||
"h-8 w-6": size === "base",
|
"h-8 w-6": size === "base",
|
||||||
"h-5 w-4": size === "small",
|
"h-5 w-4": size === "small",
|
||||||
|
|||||||
+1
-1
@@ -64,7 +64,7 @@ export const DataGridCellContainer = ({
|
|||||||
<div
|
<div
|
||||||
{...overlayProps}
|
{...overlayProps}
|
||||||
data-cell-overlay="true"
|
data-cell-overlay="true"
|
||||||
className="absolute inset-0"
|
className="absolute inset-0 z-[2]"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
import { ReactNode } from "react"
|
||||||
|
import { useDataGridDuplicateCell } from "../hooks"
|
||||||
|
|
||||||
|
interface DataGridDuplicateCellProps<TValue> {
|
||||||
|
duplicateOf: string
|
||||||
|
children?: ReactNode | ((props: { value: TValue }) => ReactNode)
|
||||||
|
}
|
||||||
|
export const DataGridDuplicateCell = <TValue,>({
|
||||||
|
duplicateOf,
|
||||||
|
children,
|
||||||
|
}: DataGridDuplicateCellProps<TValue>) => {
|
||||||
|
const { watchedValue } = useDataGridDuplicateCell({ duplicateOf })
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-ui-bg-base txt-compact-small text-ui-fg-subtle flex size-full cursor-not-allowed items-center justify-between overflow-hidden px-4 py-2.5 outline-none">
|
||||||
|
{typeof children === "function"
|
||||||
|
? children({ value: watchedValue })
|
||||||
|
: children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
+15
-7
@@ -1,24 +1,32 @@
|
|||||||
import { PropsWithChildren } from "react"
|
import { PropsWithChildren } from "react"
|
||||||
|
|
||||||
|
import { clx } from "@medusajs/ui"
|
||||||
import { useDataGridCellError } from "../hooks"
|
import { useDataGridCellError } from "../hooks"
|
||||||
import { DataGridCellProps } from "../types"
|
import { DataGridCellProps } from "../types"
|
||||||
import { DataGridRowErrorIndicator } from "./data-grid-row-error-indicator"
|
import { DataGridRowErrorIndicator } from "./data-grid-row-error-indicator"
|
||||||
|
|
||||||
type DataGridReadonlyCellProps<TData, TValue = any> = DataGridCellProps<
|
type DataGridReadonlyCellProps<TData, TValue = any> = PropsWithChildren<
|
||||||
TData,
|
DataGridCellProps<TData, TValue>
|
||||||
TValue
|
> & {
|
||||||
> &
|
color?: "muted" | "normal"
|
||||||
PropsWithChildren
|
}
|
||||||
|
|
||||||
export const DataGridReadonlyCell = <TData, TValue = any>({
|
export const DataGridReadonlyCell = <TData, TValue = any>({
|
||||||
context,
|
context,
|
||||||
|
color = "muted",
|
||||||
children,
|
children,
|
||||||
}: DataGridReadonlyCellProps<TData, TValue>) => {
|
}: DataGridReadonlyCellProps<TData, TValue>) => {
|
||||||
const { rowErrors } = useDataGridCellError({ context })
|
const { rowErrors } = useDataGridCellError({ context })
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-ui-bg-subtle txt-compact-small text-ui-fg-subtle flex size-full cursor-not-allowed items-center justify-between overflow-hidden px-4 py-2.5 outline-none">
|
<div
|
||||||
<span className="truncate">{children}</span>
|
className={clx(
|
||||||
|
"txt-compact-small text-ui-fg-subtle flex size-full cursor-not-allowed items-center justify-between overflow-hidden px-4 py-2.5 outline-none",
|
||||||
|
color === "muted" && "bg-ui-bg-subtle",
|
||||||
|
color === "normal" && "bg-ui-bg-base"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex-1 truncate">{children}</div>
|
||||||
<DataGridRowErrorIndicator rowErrors={rowErrors} />
|
<DataGridRowErrorIndicator rowErrors={rowErrors} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ import { isCellMatch, isSpecialFocusKey } from "../utils"
|
|||||||
import { DataGridKeyboardShortcutModal } from "./data-grid-keyboard-shortcut-modal"
|
import { DataGridKeyboardShortcutModal } from "./data-grid-keyboard-shortcut-modal"
|
||||||
export interface DataGridRootProps<
|
export interface DataGridRootProps<
|
||||||
TData,
|
TData,
|
||||||
TFieldValues extends FieldValues = FieldValues,
|
TFieldValues extends FieldValues = FieldValues
|
||||||
> {
|
> {
|
||||||
data?: TData[]
|
data?: TData[]
|
||||||
columns: ColumnDef<TData>[]
|
columns: ColumnDef<TData>[]
|
||||||
@@ -58,6 +58,7 @@ export interface DataGridRootProps<
|
|||||||
getSubRows?: (row: TData) => TData[] | undefined
|
getSubRows?: (row: TData) => TData[] | undefined
|
||||||
onEditingChange?: (isEditing: boolean) => void
|
onEditingChange?: (isEditing: boolean) => void
|
||||||
disableInteractions?: boolean
|
disableInteractions?: boolean
|
||||||
|
multiColumnSelection?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const ROW_HEIGHT = 40
|
const ROW_HEIGHT = 40
|
||||||
@@ -90,13 +91,12 @@ const getCommonPinningStyles = <TData,>(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* TODO:
|
* 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.
|
* - [Minor] Extend the commands to also support modifying the anchor and rangeEnd, to restore the previous focus after undo/redo.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const DataGridRoot = <
|
export const DataGridRoot = <
|
||||||
TData,
|
TData,
|
||||||
TFieldValues extends FieldValues = FieldValues,
|
TFieldValues extends FieldValues = FieldValues
|
||||||
>({
|
>({
|
||||||
data = [],
|
data = [],
|
||||||
columns,
|
columns,
|
||||||
@@ -104,6 +104,7 @@ export const DataGridRoot = <
|
|||||||
getSubRows,
|
getSubRows,
|
||||||
onEditingChange,
|
onEditingChange,
|
||||||
disableInteractions,
|
disableInteractions,
|
||||||
|
multiColumnSelection = false,
|
||||||
}: DataGridRootProps<TData, TFieldValues>) => {
|
}: DataGridRootProps<TData, TFieldValues>) => {
|
||||||
const containerRef = useRef<HTMLDivElement>(null)
|
const containerRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
@@ -231,8 +232,13 @@ export const DataGridRoot = <
|
|||||||
}
|
}
|
||||||
|
|
||||||
const matrix = useMemo(
|
const matrix = useMemo(
|
||||||
() => new DataGridMatrix<TData, TFieldValues>(flatRows, columns),
|
() =>
|
||||||
[flatRows, columns]
|
new DataGridMatrix<TData, TFieldValues>(
|
||||||
|
flatRows,
|
||||||
|
columns,
|
||||||
|
multiColumnSelection
|
||||||
|
),
|
||||||
|
[flatRows, columns, multiColumnSelection]
|
||||||
)
|
)
|
||||||
const queryTool = useDataGridQueryTool(containerRef)
|
const queryTool = useDataGridQueryTool(containerRef)
|
||||||
|
|
||||||
@@ -333,6 +339,7 @@ export const DataGridRoot = <
|
|||||||
setSelectionValues,
|
setSelectionValues,
|
||||||
onEditingChangeHandler,
|
onEditingChangeHandler,
|
||||||
restoreSnapshot,
|
restoreSnapshot,
|
||||||
|
createSnapshot,
|
||||||
setSingleRange,
|
setSingleRange,
|
||||||
scrollToCoordinates,
|
scrollToCoordinates,
|
||||||
execute,
|
execute,
|
||||||
@@ -390,6 +397,7 @@ export const DataGridRoot = <
|
|||||||
setDragEnd,
|
setDragEnd,
|
||||||
setValue,
|
setValue,
|
||||||
execute,
|
execute,
|
||||||
|
multiColumnSelection,
|
||||||
})
|
})
|
||||||
|
|
||||||
const { getCellErrorMetadata, getCellMetadata } = useDataGridCellMetadata<
|
const { getCellErrorMetadata, getCellMetadata } = useDataGridCellMetadata<
|
||||||
@@ -655,6 +663,7 @@ export const DataGridRoot = <
|
|||||||
virtualPaddingLeft={virtualPaddingLeft}
|
virtualPaddingLeft={virtualPaddingLeft}
|
||||||
virtualPaddingRight={virtualPaddingRight}
|
virtualPaddingRight={virtualPaddingRight}
|
||||||
onDragToFillStart={onDragToFillStart}
|
onDragToFillStart={onDragToFillStart}
|
||||||
|
multiColumnSelection={multiColumnSelection}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
@@ -787,6 +796,7 @@ type DataGridCellProps<TData> = {
|
|||||||
rowIndex: number
|
rowIndex: number
|
||||||
anchor: DataGridCoordinates | null
|
anchor: DataGridCoordinates | null
|
||||||
onDragToFillStart: (e: React.MouseEvent<HTMLElement>) => void
|
onDragToFillStart: (e: React.MouseEvent<HTMLElement>) => void
|
||||||
|
multiColumnSelection: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const DataGridCell = <TData,>({
|
const DataGridCell = <TData,>({
|
||||||
@@ -795,6 +805,7 @@ const DataGridCell = <TData,>({
|
|||||||
rowIndex,
|
rowIndex,
|
||||||
anchor,
|
anchor,
|
||||||
onDragToFillStart,
|
onDragToFillStart,
|
||||||
|
multiColumnSelection,
|
||||||
}: DataGridCellProps<TData>) => {
|
}: DataGridCellProps<TData>) => {
|
||||||
const coords: DataGridCoordinates = {
|
const coords: DataGridCoordinates = {
|
||||||
row: rowIndex,
|
row: rowIndex,
|
||||||
@@ -828,7 +839,12 @@ const DataGridCell = <TData,>({
|
|||||||
{isAnchor && (
|
{isAnchor && (
|
||||||
<div
|
<div
|
||||||
onMouseDown={onDragToFillStart}
|
onMouseDown={onDragToFillStart}
|
||||||
className="bg-ui-fg-interactive absolute bottom-0 right-0 z-[3] size-1.5 cursor-ns-resize"
|
className={clx(
|
||||||
|
"bg-ui-fg-interactive absolute bottom-0 right-0 z-[3] size-1.5 cursor-ns-resize",
|
||||||
|
{
|
||||||
|
"cursor-nwse-resize": multiColumnSelection,
|
||||||
|
}
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -846,6 +862,7 @@ type DataGridRowProps<TData> = {
|
|||||||
flatColumns: Column<TData, unknown>[]
|
flatColumns: Column<TData, unknown>[]
|
||||||
anchor: DataGridCoordinates | null
|
anchor: DataGridCoordinates | null
|
||||||
onDragToFillStart: (e: React.MouseEvent<HTMLElement>) => void
|
onDragToFillStart: (e: React.MouseEvent<HTMLElement>) => void
|
||||||
|
multiColumnSelection: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const DataGridRow = <TData,>({
|
const DataGridRow = <TData,>({
|
||||||
@@ -858,6 +875,7 @@ const DataGridRow = <TData,>({
|
|||||||
flatColumns,
|
flatColumns,
|
||||||
anchor,
|
anchor,
|
||||||
onDragToFillStart,
|
onDragToFillStart,
|
||||||
|
multiColumnSelection,
|
||||||
}: DataGridRowProps<TData>) => {
|
}: DataGridRowProps<TData>) => {
|
||||||
const visibleCells = row.getVisibleCells()
|
const visibleCells = row.getVisibleCells()
|
||||||
|
|
||||||
@@ -904,6 +922,7 @@ const DataGridRow = <TData,>({
|
|||||||
rowIndex={rowIndex}
|
rowIndex={rowIndex}
|
||||||
anchor={anchor}
|
anchor={anchor}
|
||||||
onDragToFillStart={onDragToFillStart}
|
onDragToFillStart={onDragToFillStart}
|
||||||
|
multiColumnSelection={multiColumnSelection}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+205
@@ -0,0 +1,205 @@
|
|||||||
|
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 { ConditionalTooltip } from "../../common/conditional-tooltip"
|
||||||
|
import { useDataGridCell, useDataGridCellError } from "../hooks"
|
||||||
|
import { DataGridCellProps, InputProps } from "../types"
|
||||||
|
import { DataGridCellContainer } from "./data-grid-cell-container"
|
||||||
|
|
||||||
|
export const DataGridTogglableNumberCell = <TData, TValue = any>({
|
||||||
|
context,
|
||||||
|
disabledToggleTooltip,
|
||||||
|
...rest
|
||||||
|
}: DataGridCellProps<TData, TValue> & {
|
||||||
|
min?: number
|
||||||
|
max?: number
|
||||||
|
placeholder?: string
|
||||||
|
disabledToggleTooltip?: string
|
||||||
|
}) => {
|
||||||
|
const { field, control, renderProps } = useDataGridCell({
|
||||||
|
context,
|
||||||
|
})
|
||||||
|
const errorProps = useDataGridCellError({ context })
|
||||||
|
|
||||||
|
const { container, input } = renderProps
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name={field}
|
||||||
|
render={({ field }) => {
|
||||||
|
return (
|
||||||
|
<DataGridCellContainer
|
||||||
|
{...container}
|
||||||
|
{...errorProps}
|
||||||
|
outerComponent={
|
||||||
|
<OuterComponent
|
||||||
|
field={field}
|
||||||
|
inputProps={input}
|
||||||
|
isAnchor={container.isAnchor}
|
||||||
|
tooltip={disabledToggleTooltip}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Inner field={field} inputProps={input} {...rest} />
|
||||||
|
</DataGridCellContainer>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const OuterComponent = ({
|
||||||
|
field,
|
||||||
|
inputProps,
|
||||||
|
isAnchor,
|
||||||
|
tooltip,
|
||||||
|
}: {
|
||||||
|
field: ControllerRenderProps<any, string>
|
||||||
|
inputProps: InputProps
|
||||||
|
isAnchor: boolean
|
||||||
|
tooltip?: string
|
||||||
|
}) => {
|
||||||
|
const buttonRef = useRef<HTMLButtonElement>(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 }
|
||||||
|
|
||||||
|
if (!update && !newValue.disabledToggle) {
|
||||||
|
newValue.quantity = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if (update && newValue.quantity === "") {
|
||||||
|
newValue.quantity = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<ConditionalTooltip
|
||||||
|
showTooltip={localValue.disabledToggle && tooltip}
|
||||||
|
content={tooltip}
|
||||||
|
>
|
||||||
|
<div className="absolute inset-y-0 left-4 z-[3] flex w-fit items-center justify-center">
|
||||||
|
<Switch
|
||||||
|
ref={buttonRef}
|
||||||
|
size="small"
|
||||||
|
className="shrink-0"
|
||||||
|
checked={localValue.checked}
|
||||||
|
disabled={localValue.disabledToggle}
|
||||||
|
onCheckedChange={handleCheckedChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</ConditionalTooltip>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const Inner = ({
|
||||||
|
field,
|
||||||
|
inputProps,
|
||||||
|
placeholder,
|
||||||
|
...props
|
||||||
|
}: {
|
||||||
|
field: ControllerRenderProps<any, string>
|
||||||
|
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"] = (
|
||||||
|
updatedValue,
|
||||||
|
_name,
|
||||||
|
_values
|
||||||
|
) => {
|
||||||
|
const ensuredValue = updatedValue !== undefined ? updatedValue : ""
|
||||||
|
const newValue = { ...localValue, quantity: ensuredValue }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the value is not empty, then the location should be enabled.
|
||||||
|
*
|
||||||
|
* Else, if the value is empty and the location is enabled, then the
|
||||||
|
* location should be disabled, unless toggling the location is disabled.
|
||||||
|
*/
|
||||||
|
if (ensuredValue !== "") {
|
||||||
|
newValue.checked = true
|
||||||
|
} else if (newValue.checked && newValue.disabledToggle === false) {
|
||||||
|
newValue.checked = false
|
||||||
|
}
|
||||||
|
|
||||||
|
setLocalValue(newValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleOnChange = () => {
|
||||||
|
if (localValue.disabledToggle && localValue.quantity === "") {
|
||||||
|
localValue.quantity = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
onChange(localValue, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex size-full items-center gap-x-2">
|
||||||
|
<CurrencyInput
|
||||||
|
{...fieldProps}
|
||||||
|
{...attributes}
|
||||||
|
{...props}
|
||||||
|
ref={combinedRefs}
|
||||||
|
className="txt-compact-small w-full flex-1 cursor-default appearance-none bg-transparent pl-8 text-right outline-none"
|
||||||
|
value={localValue?.quantity}
|
||||||
|
onValueChange={handleInputChange}
|
||||||
|
formatValueOnBlur
|
||||||
|
onBlur={() => {
|
||||||
|
onBlur()
|
||||||
|
onInputBlur()
|
||||||
|
handleOnChange()
|
||||||
|
}}
|
||||||
|
onFocus={onFocus}
|
||||||
|
decimalsLimit={0}
|
||||||
|
autoComplete="off"
|
||||||
|
tabIndex={-1}
|
||||||
|
placeholder={!localValue.checked ? placeholder : undefined}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ export * from "./use-data-grid-cell-metadata"
|
|||||||
export * from "./use-data-grid-cell-snapshot"
|
export * from "./use-data-grid-cell-snapshot"
|
||||||
export * from "./use-data-grid-clipboard-events"
|
export * from "./use-data-grid-clipboard-events"
|
||||||
export * from "./use-data-grid-column-visibility"
|
export * from "./use-data-grid-column-visibility"
|
||||||
|
export * from "./use-data-grid-duplicate-cell"
|
||||||
export * from "./use-data-grid-error-highlighting"
|
export * from "./use-data-grid-error-highlighting"
|
||||||
export * from "./use-data-grid-form-handlers"
|
export * from "./use-data-grid-form-handlers"
|
||||||
export * from "./use-data-grid-keydown-event"
|
export * from "./use-data-grid-keydown-event"
|
||||||
|
|||||||
+12
-3
@@ -17,6 +17,7 @@ type UseDataGridCellHandlersOptions<TData, TFieldValues extends FieldValues> = {
|
|||||||
setDragEnd: (coords: DataGridCoordinates | null) => void
|
setDragEnd: (coords: DataGridCoordinates | null) => void
|
||||||
setValue: UseFormSetValue<TFieldValues>
|
setValue: UseFormSetValue<TFieldValues>
|
||||||
execute: (command: DataGridUpdateCommand) => void
|
execute: (command: DataGridUpdateCommand) => void
|
||||||
|
multiColumnSelection?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useDataGridCellHandlers = <
|
export const useDataGridCellHandlers = <
|
||||||
@@ -36,6 +37,7 @@ export const useDataGridCellHandlers = <
|
|||||||
setDragEnd,
|
setDragEnd,
|
||||||
setValue,
|
setValue,
|
||||||
execute,
|
execute,
|
||||||
|
multiColumnSelection,
|
||||||
}: UseDataGridCellHandlersOptions<TData, TFieldValues>) => {
|
}: UseDataGridCellHandlersOptions<TData, TFieldValues>) => {
|
||||||
const getWrapperFocusHandler = useCallback(
|
const getWrapperFocusHandler = useCallback(
|
||||||
(coords: DataGridCoordinates) => {
|
(coords: DataGridCoordinates) => {
|
||||||
@@ -74,9 +76,9 @@ export const useDataGridCellHandlers = <
|
|||||||
return (_e: MouseEvent<HTMLElement>) => {
|
return (_e: MouseEvent<HTMLElement>) => {
|
||||||
/**
|
/**
|
||||||
* If the column is not the same as the anchor col,
|
* If the column is not the same as the anchor col,
|
||||||
* we don't want to select the cell.
|
* we don't want to select the cell. Unless multiColumnSelection is true.
|
||||||
*/
|
*/
|
||||||
if (anchor?.col !== coords.col) {
|
if (anchor?.col !== coords.col && !multiColumnSelection) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,7 +89,14 @@ export const useDataGridCellHandlers = <
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[anchor?.col, isDragging, isSelecting, setDragEnd, setRangeEnd]
|
[
|
||||||
|
anchor?.col,
|
||||||
|
isDragging,
|
||||||
|
isSelecting,
|
||||||
|
setDragEnd,
|
||||||
|
setRangeEnd,
|
||||||
|
multiColumnSelection,
|
||||||
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
const getInputChangeHandler = useCallback(
|
const getInputChangeHandler = useCallback(
|
||||||
|
|||||||
+14
-4
@@ -15,9 +15,8 @@ export const useDataGridCellSnapshot = <
|
|||||||
matrix,
|
matrix,
|
||||||
form,
|
form,
|
||||||
}: UseDataGridCellSnapshotOptions<TData, TFieldValues>) => {
|
}: UseDataGridCellSnapshotOptions<TData, TFieldValues>) => {
|
||||||
const [snapshot, setSnapshot] = useState<DataGridCellSnapshot<TFieldValues> | null>(
|
const [snapshot, setSnapshot] =
|
||||||
null
|
useState<DataGridCellSnapshot<TFieldValues> | null>(null)
|
||||||
)
|
|
||||||
|
|
||||||
const { getValues, setValue } = form
|
const { getValues, setValue } = form
|
||||||
|
|
||||||
@@ -38,7 +37,18 @@ export const useDataGridCellSnapshot = <
|
|||||||
|
|
||||||
const value = getValues(field as Path<TFieldValues>)
|
const value = getValues(field as Path<TFieldValues>)
|
||||||
|
|
||||||
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]
|
[getValues, matrix]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -123,16 +123,16 @@ export const useDataGridCell = <TData, TValue>({
|
|||||||
|
|
||||||
const validateKeyStroke = useCallback(
|
const validateKeyStroke = useCallback(
|
||||||
(key: string) => {
|
(key: string) => {
|
||||||
if (type === "number") {
|
switch (type) {
|
||||||
|
case "togglable-number":
|
||||||
|
case "number":
|
||||||
return numberCharacterRegex.test(key)
|
return numberCharacterRegex.test(key)
|
||||||
}
|
case "text":
|
||||||
|
|
||||||
if (type === "text") {
|
|
||||||
return textCharacterRegex.test(key)
|
return textCharacterRegex.test(key)
|
||||||
}
|
default:
|
||||||
|
|
||||||
// KeyboardEvents should not be forwareded to other types of cells
|
// KeyboardEvents should not be forwareded to other types of cells
|
||||||
return false
|
return false
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[type]
|
[type]
|
||||||
)
|
)
|
||||||
|
|||||||
+8
-1
@@ -45,7 +45,14 @@ export const useDataGridClipboardEvents = <
|
|||||||
const fields = matrix.getFieldsInSelection(anchor, rangeEnd)
|
const fields = matrix.getFieldsInSelection(anchor, rangeEnd)
|
||||||
const values = getSelectionValues(fields)
|
const values = getSelectionValues(fields)
|
||||||
|
|
||||||
const text = values.map((value) => `${value}` ?? "").join("\t")
|
const text = values
|
||||||
|
.map((value) => {
|
||||||
|
if (typeof value === "object" && value !== null) {
|
||||||
|
return JSON.stringify(value)
|
||||||
|
}
|
||||||
|
return `${value}` ?? ""
|
||||||
|
})
|
||||||
|
.join("\t")
|
||||||
|
|
||||||
e.clipboardData?.setData("text/plain", text)
|
e.clipboardData?.setData("text/plain", text)
|
||||||
},
|
},
|
||||||
|
|||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
import { useWatch } from "react-hook-form"
|
||||||
|
import { useDataGridContext } from "../context"
|
||||||
|
|
||||||
|
interface UseDataGridDuplicateCellOptions {
|
||||||
|
duplicateOf: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useDataGridDuplicateCell = ({
|
||||||
|
duplicateOf,
|
||||||
|
}: UseDataGridDuplicateCellOptions) => {
|
||||||
|
const { control } = useDataGridContext()
|
||||||
|
|
||||||
|
const watchedValue = useWatch({ control, name: duplicateOf })
|
||||||
|
|
||||||
|
return {
|
||||||
|
watchedValue,
|
||||||
|
}
|
||||||
|
}
|
||||||
+108
-16
@@ -1,8 +1,14 @@
|
|||||||
|
import get from "lodash/get"
|
||||||
|
import set from "lodash/set"
|
||||||
import { useCallback } from "react"
|
import { useCallback } from "react"
|
||||||
import { FieldValues, Path, PathValue, UseFormReturn } from "react-hook-form"
|
import { FieldValues, Path, PathValue, UseFormReturn } from "react-hook-form"
|
||||||
|
|
||||||
import { DataGridMatrix } from "../models"
|
import { DataGridMatrix } from "../models"
|
||||||
import { DataGridColumnType, DataGridCoordinates } from "../types"
|
import {
|
||||||
|
DataGridColumnType,
|
||||||
|
DataGridCoordinates,
|
||||||
|
DataGridToggleableNumber,
|
||||||
|
} from "../types"
|
||||||
|
|
||||||
type UseDataGridFormHandlersOptions<TData, TFieldValues extends FieldValues> = {
|
type UseDataGridFormHandlersOptions<TData, TFieldValues extends FieldValues> = {
|
||||||
matrix: DataGridMatrix<TData, TFieldValues>
|
matrix: DataGridMatrix<TData, TFieldValues>
|
||||||
@@ -12,13 +18,13 @@ type UseDataGridFormHandlersOptions<TData, TFieldValues extends FieldValues> = {
|
|||||||
|
|
||||||
export const useDataGridFormHandlers = <
|
export const useDataGridFormHandlers = <
|
||||||
TData,
|
TData,
|
||||||
TFieldValues extends FieldValues,
|
TFieldValues extends FieldValues
|
||||||
>({
|
>({
|
||||||
matrix,
|
matrix,
|
||||||
form,
|
form,
|
||||||
anchor,
|
anchor,
|
||||||
}: UseDataGridFormHandlersOptions<TData, TFieldValues>) => {
|
}: UseDataGridFormHandlersOptions<TData, TFieldValues>) => {
|
||||||
const { getValues, setValue } = form
|
const { getValues, reset } = form
|
||||||
|
|
||||||
const getSelectionValues = useCallback(
|
const getSelectionValues = useCallback(
|
||||||
(fields: string[]): PathValue<TFieldValues, Path<TFieldValues>>[] => {
|
(fields: string[]): PathValue<TFieldValues, Path<TFieldValues>>[] => {
|
||||||
@@ -26,26 +32,28 @@ export const useDataGridFormHandlers = <
|
|||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const allValues = getValues()
|
||||||
|
|
||||||
return fields.map((field) => {
|
return fields.map((field) => {
|
||||||
return getValues(field as Path<TFieldValues>)
|
return field.split(".").reduce((obj, key) => obj?.[key], allValues)
|
||||||
})
|
}) as PathValue<TFieldValues, Path<TFieldValues>>[]
|
||||||
},
|
},
|
||||||
[getValues]
|
[getValues]
|
||||||
)
|
)
|
||||||
|
|
||||||
const setSelectionValues = useCallback(
|
const setSelectionValues = useCallback(
|
||||||
async (fields: string[], values: string[]) => {
|
async (fields: string[], values: string[], isHistory?: boolean) => {
|
||||||
if (!fields.length || !anchor) {
|
if (!fields.length || !anchor) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const type = matrix.getCellType(anchor)
|
const type = matrix.getCellType(anchor)
|
||||||
|
|
||||||
if (!type) {
|
if (!type) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const convertedValues = convertArrayToPrimitive(values, type)
|
const convertedValues = convertArrayToPrimitive(values, type)
|
||||||
|
const currentValues = getValues()
|
||||||
|
|
||||||
fields.forEach((field, index) => {
|
fields.forEach((field, index) => {
|
||||||
if (!field) {
|
if (!field) {
|
||||||
@@ -53,18 +61,18 @@ export const useDataGridFormHandlers = <
|
|||||||
}
|
}
|
||||||
|
|
||||||
const valueIndex = index % values.length
|
const valueIndex = index % values.length
|
||||||
const value = convertedValues[valueIndex] as PathValue<
|
const newValue = convertedValues[valueIndex]
|
||||||
TFieldValues,
|
|
||||||
Path<TFieldValues>
|
|
||||||
>
|
|
||||||
|
|
||||||
setValue(field as Path<TFieldValues>, value, {
|
setValue(currentValues, field, newValue, type, isHistory)
|
||||||
shouldDirty: true,
|
|
||||||
shouldTouch: true,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
reset(currentValues, {
|
||||||
|
keepDirty: true,
|
||||||
|
keepTouched: true,
|
||||||
|
keepDefaultValues: true,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
[matrix, anchor, setValue]
|
[matrix, anchor, getValues, reset]
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -113,13 +121,97 @@ function covertToString(value: any): string {
|
|||||||
return String(value)
|
return String(value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function convertToggleableNumber(value: any): {
|
||||||
|
quantity: number
|
||||||
|
checked: boolean
|
||||||
|
disabledToggle: boolean
|
||||||
|
} {
|
||||||
|
let obj = value
|
||||||
|
|
||||||
|
if (typeof obj === "string") {
|
||||||
|
try {
|
||||||
|
obj = JSON.parse(obj)
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`String "${value}" cannot be converted to object.`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
|
||||||
|
function setValue<
|
||||||
|
T extends DataGridToggleableNumber = DataGridToggleableNumber
|
||||||
|
>(
|
||||||
|
currentValues: any,
|
||||||
|
field: string,
|
||||||
|
newValue: T,
|
||||||
|
type: string,
|
||||||
|
isHistory?: boolean
|
||||||
|
) {
|
||||||
|
if (type !== "togglable-number") {
|
||||||
|
set(currentValues, field, newValue)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
setValueToggleableNumber(currentValues, field, newValue, isHistory)
|
||||||
|
}
|
||||||
|
|
||||||
|
function setValueToggleableNumber(
|
||||||
|
currentValues: any,
|
||||||
|
field: string,
|
||||||
|
newValue: DataGridToggleableNumber,
|
||||||
|
isHistory?: boolean
|
||||||
|
) {
|
||||||
|
const currentValue = get(currentValues, field)
|
||||||
|
const { disabledToggle } = currentValue
|
||||||
|
|
||||||
|
const normalizeQuantity = (value: number | string | null | undefined) => {
|
||||||
|
if (disabledToggle && value === "") {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
const determineChecked = (quantity: number | string | null | undefined) => {
|
||||||
|
if (disabledToggle) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return quantity !== "" && quantity != null
|
||||||
|
}
|
||||||
|
|
||||||
|
const quantity = normalizeQuantity(newValue.quantity)
|
||||||
|
const checked = isHistory
|
||||||
|
? disabledToggle
|
||||||
|
? true
|
||||||
|
: newValue.checked
|
||||||
|
: determineChecked(quantity)
|
||||||
|
|
||||||
|
set(currentValues, field, {
|
||||||
|
...currentValue,
|
||||||
|
quantity,
|
||||||
|
checked,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export function convertArrayToPrimitive(
|
export function convertArrayToPrimitive(
|
||||||
values: any[],
|
values: any[],
|
||||||
type: DataGridColumnType
|
type: DataGridColumnType
|
||||||
): any[] {
|
): any[] {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "number":
|
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 "togglable-number":
|
||||||
|
return values.map(convertToggleableNumber)
|
||||||
case "boolean":
|
case "boolean":
|
||||||
return values.map(convertToBoolean)
|
return values.map(convertToBoolean)
|
||||||
case "text":
|
case "text":
|
||||||
|
|||||||
+78
-3
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback } from "react"
|
import React, { useCallback } from "react"
|
||||||
import type {
|
import type {
|
||||||
FieldValues,
|
FieldValues,
|
||||||
Path,
|
Path,
|
||||||
@@ -39,6 +39,7 @@ type UseDataGridKeydownEventOptions<TData, TFieldValues extends FieldValues> = {
|
|||||||
) => PathValue<TFieldValues, Path<TFieldValues>>[]
|
) => PathValue<TFieldValues, Path<TFieldValues>>[]
|
||||||
setSelectionValues: (fields: string[], values: string[]) => void
|
setSelectionValues: (fields: string[], values: string[]) => void
|
||||||
restoreSnapshot: () => void
|
restoreSnapshot: () => void
|
||||||
|
createSnapshot: (coords: DataGridCoordinates) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
const ARROW_KEYS = ["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"]
|
const ARROW_KEYS = ["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"]
|
||||||
@@ -67,6 +68,7 @@ export const useDataGridKeydownEvent = <
|
|||||||
getSelectionValues,
|
getSelectionValues,
|
||||||
setSelectionValues,
|
setSelectionValues,
|
||||||
restoreSnapshot,
|
restoreSnapshot,
|
||||||
|
createSnapshot,
|
||||||
}: UseDataGridKeydownEventOptions<TData, TFieldValues>) => {
|
}: UseDataGridKeydownEventOptions<TData, TFieldValues>) => {
|
||||||
const handleKeyboardNavigation = useCallback(
|
const handleKeyboardNavigation = useCallback(
|
||||||
(e: KeyboardEvent) => {
|
(e: KeyboardEvent) => {
|
||||||
@@ -218,6 +220,8 @@ export const useDataGridKeydownEvent = <
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
createSnapshot(anchor)
|
||||||
|
|
||||||
const current = getValues(field as Path<TFieldValues>)
|
const current = getValues(field as Path<TFieldValues>)
|
||||||
const next = ""
|
const next = ""
|
||||||
|
|
||||||
@@ -236,7 +240,46 @@ export const useDataGridKeydownEvent = <
|
|||||||
|
|
||||||
input.focus()
|
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<TFieldValues>)
|
||||||
|
let checked = current.checked
|
||||||
|
|
||||||
|
// If the toggle is not disabled, then we want to uncheck the toggle.
|
||||||
|
if (!current.disabledToggle) {
|
||||||
|
checked = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const next = { ...current, quantity: "", checked }
|
||||||
|
|
||||||
|
const command = new DataGridUpdateCommand({
|
||||||
|
next,
|
||||||
|
prev: current,
|
||||||
|
setter: (value) => {
|
||||||
|
setValue(field as Path<TFieldValues>, value, {
|
||||||
|
shouldDirty: true,
|
||||||
|
shouldTouch: true,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
execute(command)
|
||||||
|
|
||||||
|
input.focus()
|
||||||
|
},
|
||||||
|
[matrix, queryTool, getValues, execute, setValue, createSnapshot]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleSpaceKey = useCallback(
|
const handleSpaceKey = useCallback(
|
||||||
@@ -257,6 +300,9 @@ export const useDataGridKeydownEvent = <
|
|||||||
case "boolean":
|
case "boolean":
|
||||||
handleSpaceKeyBoolean(anchor)
|
handleSpaceKeyBoolean(anchor)
|
||||||
break
|
break
|
||||||
|
case "togglable-number":
|
||||||
|
handleSpaceKeyTogglableNumber(anchor)
|
||||||
|
break
|
||||||
case "number":
|
case "number":
|
||||||
case "text":
|
case "text":
|
||||||
handleSpaceKeyTextOrNumber(anchor)
|
handleSpaceKeyTextOrNumber(anchor)
|
||||||
@@ -269,6 +315,7 @@ export const useDataGridKeydownEvent = <
|
|||||||
matrix,
|
matrix,
|
||||||
handleSpaceKeyBoolean,
|
handleSpaceKeyBoolean,
|
||||||
handleSpaceKeyTextOrNumber,
|
handleSpaceKeyTextOrNumber,
|
||||||
|
handleSpaceKeyTogglableNumber,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -390,6 +437,7 @@ export const useDataGridKeydownEvent = <
|
|||||||
const type = matrix.getCellType(anchor)
|
const type = matrix.getCellType(anchor)
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
|
case "togglable-number":
|
||||||
case "text":
|
case "text":
|
||||||
case "number":
|
case "number":
|
||||||
handleEnterKeyTextOrNumber(e, anchor)
|
handleEnterKeyTextOrNumber(e, anchor)
|
||||||
@@ -403,6 +451,29 @@ export const useDataGridKeydownEvent = <
|
|||||||
[anchor, matrix, handleEnterKeyTextOrNumber, handleEnterKeyBoolean]
|
[anchor, matrix, handleEnterKeyTextOrNumber, handleEnterKeyBoolean]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const handleDeleteKeyTogglableNumber = useCallback(
|
||||||
|
(anchor: DataGridCoordinates, rangeEnd: DataGridCoordinates) => {
|
||||||
|
const fields = matrix.getFieldsInSelection(anchor, rangeEnd)
|
||||||
|
const prev = getSelectionValues(fields)
|
||||||
|
|
||||||
|
const next = prev.map((value) => ({
|
||||||
|
...value,
|
||||||
|
quantity: "",
|
||||||
|
checked: value.disableToggle ? value.checked : false,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const command = new DataGridBulkUpdateCommand({
|
||||||
|
fields,
|
||||||
|
next,
|
||||||
|
prev,
|
||||||
|
setter: setSelectionValues,
|
||||||
|
})
|
||||||
|
|
||||||
|
execute(command)
|
||||||
|
},
|
||||||
|
[matrix, getSelectionValues, setSelectionValues, execute]
|
||||||
|
)
|
||||||
|
|
||||||
const handleDeleteKeyTextOrNumber = useCallback(
|
const handleDeleteKeyTextOrNumber = useCallback(
|
||||||
(anchor: DataGridCoordinates, rangeEnd: DataGridCoordinates) => {
|
(anchor: DataGridCoordinates, rangeEnd: DataGridCoordinates) => {
|
||||||
const fields = matrix.getFieldsInSelection(anchor, rangeEnd)
|
const fields = matrix.getFieldsInSelection(anchor, rangeEnd)
|
||||||
@@ -461,6 +532,9 @@ export const useDataGridKeydownEvent = <
|
|||||||
case "boolean":
|
case "boolean":
|
||||||
handleDeleteKeyBoolean(anchor, rangeEnd)
|
handleDeleteKeyBoolean(anchor, rangeEnd)
|
||||||
break
|
break
|
||||||
|
case "togglable-number":
|
||||||
|
handleDeleteKeyTogglableNumber(anchor, rangeEnd)
|
||||||
|
break
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
@@ -470,6 +544,7 @@ export const useDataGridKeydownEvent = <
|
|||||||
matrix,
|
matrix,
|
||||||
handleDeleteKeyTextOrNumber,
|
handleDeleteKeyTextOrNumber,
|
||||||
handleDeleteKeyBoolean,
|
handleDeleteKeyBoolean,
|
||||||
|
handleDeleteKeyTogglableNumber,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -518,7 +593,7 @@ export const useDataGridKeydownEvent = <
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[anchor, isEditing, setTrapActive, containerRef]
|
[isEditing, setTrapActive, containerRef]
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleKeyDownEvent = useCallback(
|
const handleKeyDownEvent = useCallback(
|
||||||
|
|||||||
+10
-6
@@ -4,7 +4,7 @@ export type DataGridBulkUpdateCommandArgs = {
|
|||||||
fields: string[]
|
fields: string[]
|
||||||
next: any[]
|
next: any[]
|
||||||
prev: any[]
|
prev: any[]
|
||||||
setter: (fields: string[], values: any[]) => void
|
setter: (fields: string[], values: any[], isHistory?: boolean) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DataGridBulkUpdateCommand implements Command {
|
export class DataGridBulkUpdateCommand implements Command {
|
||||||
@@ -13,7 +13,11 @@ export class DataGridBulkUpdateCommand implements Command {
|
|||||||
private _prev: any[]
|
private _prev: any[]
|
||||||
private _next: any[]
|
private _next: any[]
|
||||||
|
|
||||||
private _setter: (fields: string[], any: string[]) => void
|
private _setter: (
|
||||||
|
fields: string[],
|
||||||
|
values: any[],
|
||||||
|
isHistory?: boolean
|
||||||
|
) => void
|
||||||
|
|
||||||
constructor({ fields, prev, next, setter }: DataGridBulkUpdateCommandArgs) {
|
constructor({ fields, prev, next, setter }: DataGridBulkUpdateCommandArgs) {
|
||||||
this._fields = fields
|
this._fields = fields
|
||||||
@@ -22,13 +26,13 @@ export class DataGridBulkUpdateCommand implements Command {
|
|||||||
this._setter = setter
|
this._setter = setter
|
||||||
}
|
}
|
||||||
|
|
||||||
execute(): void {
|
execute(redo = false): void {
|
||||||
this._setter(this._fields, this._next)
|
this._setter(this._fields, this._next, redo)
|
||||||
}
|
}
|
||||||
undo(): void {
|
undo(): void {
|
||||||
this._setter(this._fields, this._prev)
|
this._setter(this._fields, this._prev, true)
|
||||||
}
|
}
|
||||||
redo(): void {
|
redo(): void {
|
||||||
this.execute()
|
this.execute(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,25 @@
|
|||||||
import { ColumnDef, Row } from "@tanstack/react-table"
|
import { ColumnDef, Row } from "@tanstack/react-table"
|
||||||
import { FieldValues } from "react-hook-form"
|
import { FieldValues } from "react-hook-form"
|
||||||
import { DataGridColumnType, DataGridCoordinates, Grid, GridCell, InternalColumnMeta } from "../types"
|
import {
|
||||||
|
DataGridColumnType,
|
||||||
|
DataGridCoordinates,
|
||||||
|
Grid,
|
||||||
|
GridCell,
|
||||||
|
InternalColumnMeta,
|
||||||
|
} from "../types"
|
||||||
|
|
||||||
export class DataGridMatrix<TData, TFieldValues extends FieldValues> {
|
export class DataGridMatrix<TData, TFieldValues extends FieldValues> {
|
||||||
|
private multiColumnSelection: boolean
|
||||||
private cells: Grid<TFieldValues>
|
private cells: Grid<TFieldValues>
|
||||||
public rowAccessors: (string | null)[] = []
|
public rowAccessors: (string | null)[] = []
|
||||||
public columnAccessors: (string | null)[] = []
|
public columnAccessors: (string | null)[] = []
|
||||||
|
|
||||||
constructor(data: Row<TData>[], columns: ColumnDef<TData>[]) {
|
constructor(
|
||||||
|
data: Row<TData>[],
|
||||||
|
columns: ColumnDef<TData>[],
|
||||||
|
multiColumnSelection: boolean = false
|
||||||
|
) {
|
||||||
|
this.multiColumnSelection = multiColumnSelection
|
||||||
this.cells = this._populateCells(data, columns)
|
this.cells = this._populateCells(data, columns)
|
||||||
|
|
||||||
this.rowAccessors = this._computeRowAccessors()
|
this.rowAccessors = this._computeRowAccessors()
|
||||||
@@ -64,19 +76,28 @@ export class DataGridMatrix<TData, TFieldValues extends FieldValues> {
|
|||||||
return keys
|
return keys
|
||||||
}
|
}
|
||||||
|
|
||||||
if (start.col !== end.col) {
|
if (!this.multiColumnSelection && start.col !== end.col) {
|
||||||
throw new Error("Selection must be in the same column")
|
throw new Error(
|
||||||
|
"Selection must be in the same column when multiColumnSelection is disabled"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const startRow = Math.min(start.row, end.row)
|
const startRow = Math.min(start.row, end.row)
|
||||||
const endRow = Math.max(start.row, end.row)
|
const endRow = Math.max(start.row, end.row)
|
||||||
const col = start.col
|
const startCol = this.multiColumnSelection
|
||||||
|
? Math.min(start.col, end.col)
|
||||||
|
: start.col
|
||||||
|
const endCol = this.multiColumnSelection
|
||||||
|
? Math.max(start.col, end.col)
|
||||||
|
: start.col
|
||||||
|
|
||||||
for (let row = startRow; row <= endRow; row++) {
|
for (let row = startRow; row <= endRow; row++) {
|
||||||
|
for (let col = startCol; col <= endCol; col++) {
|
||||||
if (this._isValidPosition(row, col) && this.cells[row][col] !== null) {
|
if (this._isValidPosition(row, col) && this.cells[row][col] !== null) {
|
||||||
keys.push(this.cells[row][col]?.field as string)
|
keys.push(this.cells[row][col]?.field as string)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return keys
|
return keys
|
||||||
}
|
}
|
||||||
@@ -106,15 +127,27 @@ export class DataGridMatrix<TData, TFieldValues extends FieldValues> {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (start.col !== end.col) {
|
if (!this.multiColumnSelection && start.col !== end.col) {
|
||||||
throw new Error("Selection must be in the same column")
|
throw new Error(
|
||||||
|
"Selection must be in the same column when multiColumnSelection is disabled"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const startRow = Math.min(start.row, end.row)
|
const startRow = Math.min(start.row, end.row)
|
||||||
const endRow = Math.max(start.row, end.row)
|
const endRow = Math.max(start.row, end.row)
|
||||||
const col = start.col
|
const startCol = this.multiColumnSelection
|
||||||
|
? Math.min(start.col, end.col)
|
||||||
|
: start.col
|
||||||
|
const endCol = this.multiColumnSelection
|
||||||
|
? Math.max(start.col, end.col)
|
||||||
|
: start.col
|
||||||
|
|
||||||
return cell.col === col && cell.row >= startRow && cell.row <= endRow
|
return (
|
||||||
|
cell.row >= startRow &&
|
||||||
|
cell.row <= endRow &&
|
||||||
|
cell.col >= startCol &&
|
||||||
|
cell.col <= endCol
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleColumn(col: number, enabled: boolean) {
|
toggleColumn(col: number, enabled: boolean) {
|
||||||
|
|||||||
@@ -14,7 +14,11 @@ import {
|
|||||||
PathValue,
|
PathValue,
|
||||||
} from "react-hook-form"
|
} from "react-hook-form"
|
||||||
|
|
||||||
export type DataGridColumnType = "text" | "number" | "boolean"
|
export type DataGridColumnType =
|
||||||
|
| "text"
|
||||||
|
| "number"
|
||||||
|
| "boolean"
|
||||||
|
| "togglable-number"
|
||||||
|
|
||||||
export type DataGridCoordinates = {
|
export type DataGridCoordinates = {
|
||||||
row: number
|
row: number
|
||||||
@@ -100,7 +104,7 @@ export interface DataGridCellContainerProps extends PropsWithChildren<{}> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type DataGridCellSnapshot<
|
export type DataGridCellSnapshot<
|
||||||
TFieldValues extends FieldValues = FieldValues
|
TFieldValues extends FieldValues = FieldValues,
|
||||||
> = {
|
> = {
|
||||||
field: string
|
field: string
|
||||||
value: PathValue<TFieldValues, Path<TFieldValues>>
|
value: PathValue<TFieldValues, Path<TFieldValues>>
|
||||||
@@ -160,3 +164,9 @@ export type GridColumnOption = {
|
|||||||
checked: boolean
|
checked: boolean
|
||||||
disabled: boolean
|
disabled: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type DataGridToggleableNumber = {
|
||||||
|
quantity: number | string
|
||||||
|
checked: boolean
|
||||||
|
disabledToggle: boolean
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { XMarkMini } from "@medusajs/icons"
|
import { XMarkMini } from "@medusajs/icons"
|
||||||
import { Badge, clx } from "@medusajs/ui"
|
import { Badge, clx } from "@medusajs/ui"
|
||||||
import { AnimatePresence, motion } from "framer-motion"
|
import { AnimatePresence, motion } from "motion/react"
|
||||||
import {
|
import {
|
||||||
FocusEvent,
|
FocusEvent,
|
||||||
KeyboardEvent,
|
KeyboardEvent,
|
||||||
|
|||||||
@@ -2,28 +2,47 @@ import * as Dialog from "@radix-ui/react-dialog"
|
|||||||
|
|
||||||
import { SidebarLeft, TriangleRightMini, XMark } from "@medusajs/icons"
|
import { SidebarLeft, TriangleRightMini, XMark } from "@medusajs/icons"
|
||||||
import { IconButton, clx } from "@medusajs/ui"
|
import { IconButton, clx } from "@medusajs/ui"
|
||||||
import { PropsWithChildren, ReactNode } from "react"
|
import { AnimatePresence } from "motion/react"
|
||||||
|
import { PropsWithChildren, ReactNode, useEffect, useState } from "react"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { Link, Outlet, UIMatch, useMatches } from "react-router-dom"
|
import {
|
||||||
|
Link,
|
||||||
|
Outlet,
|
||||||
|
UIMatch,
|
||||||
|
useMatches,
|
||||||
|
useNavigation,
|
||||||
|
} from "react-router-dom"
|
||||||
|
|
||||||
import { KeybindProvider } from "../../../providers/keybind-provider"
|
import { KeybindProvider } from "../../../providers/keybind-provider"
|
||||||
import { useGlobalShortcuts } from "../../../providers/keybind-provider/hooks"
|
import { useGlobalShortcuts } from "../../../providers/keybind-provider/hooks"
|
||||||
import { useSidebar } from "../../../providers/sidebar-provider"
|
import { useSidebar } from "../../../providers/sidebar-provider"
|
||||||
|
import { ProgressBar } from "../../common/progress-bar"
|
||||||
import { Notifications } from "../notifications"
|
import { Notifications } from "../notifications"
|
||||||
|
|
||||||
export const Shell = ({ children }: PropsWithChildren) => {
|
export const Shell = ({ children }: PropsWithChildren) => {
|
||||||
const globalShortcuts = useGlobalShortcuts()
|
const globalShortcuts = useGlobalShortcuts()
|
||||||
|
const navigation = useNavigation()
|
||||||
|
|
||||||
|
const loading = navigation.state === "loading"
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<KeybindProvider shortcuts={globalShortcuts}>
|
<KeybindProvider shortcuts={globalShortcuts}>
|
||||||
<div className="flex h-screen flex-col items-start overflow-hidden lg:flex-row">
|
<div className="relative flex h-screen flex-col items-start overflow-hidden lg:flex-row">
|
||||||
|
<NavigationBar loading={loading} />
|
||||||
<div>
|
<div>
|
||||||
<MobileSidebarContainer>{children}</MobileSidebarContainer>
|
<MobileSidebarContainer>{children}</MobileSidebarContainer>
|
||||||
<DesktopSidebarContainer>{children}</DesktopSidebarContainer>
|
<DesktopSidebarContainer>{children}</DesktopSidebarContainer>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex h-screen w-full flex-col overflow-auto">
|
<div className="flex h-screen w-full flex-col overflow-auto">
|
||||||
<Topbar />
|
<Topbar />
|
||||||
<main className="flex h-full w-full flex-col items-center overflow-y-auto">
|
<main
|
||||||
|
className={clx(
|
||||||
|
"flex h-full w-full flex-col items-center overflow-y-auto transition-opacity delay-200 duration-200",
|
||||||
|
{
|
||||||
|
"opacity-25": loading,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
>
|
||||||
<Gutter>
|
<Gutter>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</Gutter>
|
</Gutter>
|
||||||
@@ -34,6 +53,36 @@ export const Shell = ({ children }: PropsWithChildren) => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const NavigationBar = ({ loading }: { loading: boolean }) => {
|
||||||
|
const [showBar, setShowBar] = useState(false)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the loading state is true, we want to show the bar after a short delay.
|
||||||
|
* The delay is used to prevent the bar from flashing on quick navigations.
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
let timeout: ReturnType<typeof setTimeout>
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
timeout = setTimeout(() => {
|
||||||
|
setShowBar(true)
|
||||||
|
}, 200)
|
||||||
|
} else {
|
||||||
|
setShowBar(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearTimeout(timeout)
|
||||||
|
}
|
||||||
|
}, [loading])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="fixed inset-x-0 top-0 z-50 h-1">
|
||||||
|
<AnimatePresence>{showBar ? <ProgressBar /> : null}</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const Gutter = ({ children }: PropsWithChildren) => {
|
const Gutter = ({ children }: PropsWithChildren) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full max-w-[1600px] flex-col gap-y-2 p-3">
|
<div className="flex w-full max-w-[1600px] flex-col gap-y-2 p-3">
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { FetchError } from "@medusajs/js-sdk"
|
||||||
import { HttpTypes } from "@medusajs/types"
|
import { HttpTypes } from "@medusajs/types"
|
||||||
import {
|
import {
|
||||||
QueryKey,
|
QueryKey,
|
||||||
@@ -6,11 +7,11 @@ import {
|
|||||||
useMutation,
|
useMutation,
|
||||||
useQuery,
|
useQuery,
|
||||||
} from "@tanstack/react-query"
|
} from "@tanstack/react-query"
|
||||||
import { FetchError } from "@medusajs/js-sdk"
|
|
||||||
|
|
||||||
import { sdk } from "../../lib/client"
|
import { sdk } from "../../lib/client"
|
||||||
import { queryClient } from "../../lib/query-client"
|
import { queryClient } from "../../lib/query-client"
|
||||||
import { queryKeysFactory } from "../../lib/query-key-factory"
|
import { queryKeysFactory } from "../../lib/query-key-factory"
|
||||||
|
import { variantsQueryKeys } from "./products"
|
||||||
|
|
||||||
const INVENTORY_ITEMS_QUERY_KEY = "inventory_items" as const
|
const INVENTORY_ITEMS_QUERY_KEY = "inventory_items" as const
|
||||||
export const inventoryItemsQueryKeys = queryKeysFactory(
|
export const inventoryItemsQueryKeys = queryKeysFactory(
|
||||||
@@ -23,7 +24,7 @@ export const inventoryItemLevelsQueryKeys = queryKeysFactory(
|
|||||||
)
|
)
|
||||||
|
|
||||||
export const useInventoryItems = (
|
export const useInventoryItems = (
|
||||||
query?: Record<string, any>,
|
query?: HttpTypes.AdminInventoryItemParams,
|
||||||
options?: Omit<
|
options?: Omit<
|
||||||
UseQueryOptions<
|
UseQueryOptions<
|
||||||
HttpTypes.AdminInventoryItemListResponse,
|
HttpTypes.AdminInventoryItemListResponse,
|
||||||
@@ -136,7 +137,7 @@ export const useDeleteInventoryItemLevel = (
|
|||||||
inventoryItemId: string,
|
inventoryItemId: string,
|
||||||
locationId: string,
|
locationId: string,
|
||||||
options?: UseMutationOptions<
|
options?: UseMutationOptions<
|
||||||
HttpTypes.AdminInventoryItemDeleteResponse,
|
HttpTypes.AdminInventoryLevelDeleteResponse,
|
||||||
FetchError,
|
FetchError,
|
||||||
void
|
void
|
||||||
>
|
>
|
||||||
@@ -210,17 +211,20 @@ export const useUpdateInventoryLevel = (
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useBatchUpdateInventoryLevels = (
|
export const useBatchInventoryItemLocationLevels = (
|
||||||
inventoryItemId: string,
|
inventoryItemId: string,
|
||||||
options?: UseMutationOptions<
|
options?: UseMutationOptions<
|
||||||
HttpTypes.AdminInventoryItemResponse,
|
HttpTypes.AdminBatchInventoryItemLocationLevelsResponse,
|
||||||
FetchError,
|
FetchError,
|
||||||
HttpTypes.AdminBatchUpdateInventoryLevelLocation
|
HttpTypes.AdminBatchInventoryItemLocationLevels
|
||||||
>
|
>
|
||||||
) => {
|
) => {
|
||||||
return useMutation({
|
return useMutation({
|
||||||
mutationFn: (payload: HttpTypes.AdminBatchUpdateInventoryLevelLocation) =>
|
mutationFn: (payload) =>
|
||||||
sdk.admin.inventoryItem.batchUpdateLevels(inventoryItemId, payload),
|
sdk.admin.inventoryItem.batchInventoryItemLocationLevels(
|
||||||
|
inventoryItemId,
|
||||||
|
payload
|
||||||
|
),
|
||||||
onSuccess: (data, variables, context) => {
|
onSuccess: (data, variables, context) => {
|
||||||
queryClient.invalidateQueries({
|
queryClient.invalidateQueries({
|
||||||
queryKey: inventoryItemsQueryKeys.lists(),
|
queryKey: inventoryItemsQueryKeys.lists(),
|
||||||
@@ -236,3 +240,26 @@ export const useBatchUpdateInventoryLevels = (
|
|||||||
...options,
|
...options,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const useBatchInventoryItemsLocationLevels = (
|
||||||
|
options?: UseMutationOptions<
|
||||||
|
HttpTypes.AdminBatchInventoryItemsLocationLevelsResponse,
|
||||||
|
FetchError,
|
||||||
|
HttpTypes.AdminBatchInventoryItemsLocationLevels
|
||||||
|
>
|
||||||
|
) => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (payload) =>
|
||||||
|
sdk.admin.inventoryItem.batchInventoryItemsLocationLevels(payload),
|
||||||
|
onSuccess: (data, variables, context) => {
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: inventoryItemsQueryKeys.all,
|
||||||
|
})
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: variantsQueryKeys.lists(),
|
||||||
|
})
|
||||||
|
options?.onSuccess?.(data, variables, context)
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -110,9 +110,14 @@ export const useProductVariant = (
|
|||||||
|
|
||||||
export const useProductVariants = (
|
export const useProductVariants = (
|
||||||
productId: string,
|
productId: string,
|
||||||
query?: Record<string, any>,
|
query?: HttpTypes.AdminProductVariantParams,
|
||||||
options?: Omit<
|
options?: Omit<
|
||||||
UseQueryOptions<any, FetchError, any, QueryKey>,
|
UseQueryOptions<
|
||||||
|
HttpTypes.AdminProductVariantListResponse,
|
||||||
|
FetchError,
|
||||||
|
HttpTypes.AdminProductVariantListResponse,
|
||||||
|
QueryKey
|
||||||
|
>,
|
||||||
"queryFn" | "queryKey"
|
"queryFn" | "queryKey"
|
||||||
>
|
>
|
||||||
) => {
|
) => {
|
||||||
|
|||||||
@@ -2649,6 +2649,35 @@
|
|||||||
],
|
],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
|
"stock": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"heading": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"loading": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"tooltips": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"alreadyManaged": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"alreadyManagedWithSku": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["alreadyManaged", "alreadyManagedWithSku"],
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["heading", "description", "loading", "tooltips"],
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
"toasts": {
|
"toasts": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -2721,6 +2750,7 @@
|
|||||||
"variant",
|
"variant",
|
||||||
"options",
|
"options",
|
||||||
"organization",
|
"organization",
|
||||||
|
"stock",
|
||||||
"toasts"
|
"toasts"
|
||||||
],
|
],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
@@ -3307,6 +3337,46 @@
|
|||||||
"updateItem"
|
"updateItem"
|
||||||
],
|
],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"stock": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"title": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"action": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"placeholder": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"disablePrompt_one": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"disablePrompt_other": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"disabledToggleTooltip": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"successToast": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"title",
|
||||||
|
"description",
|
||||||
|
"action",
|
||||||
|
"placeholder",
|
||||||
|
"disablePrompt_one",
|
||||||
|
"disablePrompt_other",
|
||||||
|
"disabledToggleTooltip",
|
||||||
|
"successToast"
|
||||||
|
],
|
||||||
|
"additionalProperties": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@@ -3322,7 +3392,8 @@
|
|||||||
"create",
|
"create",
|
||||||
"reservation",
|
"reservation",
|
||||||
"adjustInventory",
|
"adjustInventory",
|
||||||
"toast"
|
"toast",
|
||||||
|
"stock"
|
||||||
],
|
],
|
||||||
"additionalProperties": false
|
"additionalProperties": false
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -645,6 +645,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"stock": {
|
||||||
|
"heading": "Manage product stock levels and locations",
|
||||||
|
"description": "Update the stocked inventory levels for all of the product's variants.",
|
||||||
|
"loading": "Hang on this may take a moment...",
|
||||||
|
"tooltips": {
|
||||||
|
"alreadyManaged": "This inventory item is already editable under {{title}}.",
|
||||||
|
"alreadyManagedWithSku": "This inventory item is already editable under {{title}} ({{sku}})."
|
||||||
|
}
|
||||||
|
},
|
||||||
"toasts": {
|
"toasts": {
|
||||||
"delete": {
|
"delete": {
|
||||||
"success": {
|
"success": {
|
||||||
@@ -799,6 +808,16 @@
|
|||||||
"updateLocations": "Locations updated successfully.",
|
"updateLocations": "Locations updated successfully.",
|
||||||
"updateLevel": "Inventory level updated successfully.",
|
"updateLevel": "Inventory level updated successfully.",
|
||||||
"updateItem": "Inventory item updated successfully."
|
"updateItem": "Inventory item updated successfully."
|
||||||
|
},
|
||||||
|
"stock": {
|
||||||
|
"title": "Update inventory levels",
|
||||||
|
"description": "Update the stocked inventory levels for the selected inventory items.",
|
||||||
|
"action": "Edit stock levels",
|
||||||
|
"placeholder": "Not enabled",
|
||||||
|
"disablePrompt_one": "You are about to disable {{count}} location level. This action cannot be undone.",
|
||||||
|
"disablePrompt_other": "You are about to disable {{count}} location levels. This action cannot be undone.",
|
||||||
|
"disabledToggleTooltip": "Cannot disable: clear incoming and/or reserved quantity before disabling.",
|
||||||
|
"successToast": "Inventory levels updated successfully."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"giftCards": {
|
"giftCards": {
|
||||||
|
|||||||
@@ -129,6 +129,11 @@ export const RouteMap: RouteObject[] = [
|
|||||||
"../../routes/products/product-create-variant"
|
"../../routes/products/product-create-variant"
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "stock",
|
||||||
|
lazy: () =>
|
||||||
|
import("../../routes/products/product-stock"),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "metadata/edit",
|
path: "metadata/edit",
|
||||||
lazy: () =>
|
lazy: () =>
|
||||||
@@ -764,6 +769,11 @@ export const RouteMap: RouteObject[] = [
|
|||||||
lazy: () =>
|
lazy: () =>
|
||||||
import("../../routes/inventory/inventory-create"),
|
import("../../routes/inventory/inventory-create"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "stock",
|
||||||
|
lazy: () =>
|
||||||
|
import("../../routes/inventory/inventory-stock"),
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import { useDashboardExtension } from "../../../extensions"
|
|||||||
import { useCustomer } from "../../../hooks/api/customers"
|
import { useCustomer } from "../../../hooks/api/customers"
|
||||||
import { CustomerGeneralSection } from "./components/customer-general-section"
|
import { CustomerGeneralSection } from "./components/customer-general-section"
|
||||||
import { CustomerGroupSection } from "./components/customer-group-section"
|
import { CustomerGroupSection } from "./components/customer-group-section"
|
||||||
import { customerLoader } from "./loader"
|
|
||||||
import { CustomerOrderSection } from "./components/customer-order-section"
|
import { CustomerOrderSection } from "./components/customer-order-section"
|
||||||
|
import { customerLoader } from "./loader"
|
||||||
|
|
||||||
export const CustomerDetail = () => {
|
export const CustomerDetail = () => {
|
||||||
const { id } = useParams()
|
const { id } = useParams()
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export const INVENTORY_ITEM_IDS_KEY = "inventory_item_ids"
|
||||||
+5
-5
@@ -1,8 +1,8 @@
|
|||||||
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 { HttpTypes } from "@medusajs/types"
|
||||||
|
import { Button, Container, Heading } from "@medusajs/ui"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { Link } from "react-router-dom"
|
||||||
|
import { ItemLocationListTable } from "./location-levels-table/location-list-table"
|
||||||
|
|
||||||
type InventoryItemLocationLevelsSectionProps = {
|
type InventoryItemLocationLevelsSectionProps = {
|
||||||
inventoryItem: HttpTypes.AdminInventoryItemResponse["inventory_item"]
|
inventoryItem: HttpTypes.AdminInventoryItemResponse["inventory_item"]
|
||||||
@@ -13,7 +13,7 @@ export const InventoryItemLocationLevelsSection = ({
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container className="p-0">
|
<Container className="divide-y p-0">
|
||||||
<div className="flex items-center justify-between px-6 py-4">
|
<div className="flex items-center justify-between px-6 py-4">
|
||||||
<Heading>{t("inventory.locationLevels")}</Heading>
|
<Heading>{t("inventory.locationLevels")}</Heading>
|
||||||
<Button size="small" variant="secondary" asChild>
|
<Button size="small" variant="secondary" asChild>
|
||||||
|
|||||||
+3
-3
@@ -1,8 +1,8 @@
|
|||||||
|
import { HttpTypes } from "@medusajs/types"
|
||||||
import { Button, Container, Heading } from "@medusajs/ui"
|
import { Button, Container, Heading } from "@medusajs/ui"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
import { Link } from "react-router-dom"
|
import { Link } from "react-router-dom"
|
||||||
import { ReservationItemTable } from "./reservations-table/reservation-list-table"
|
import { ReservationItemTable } from "./reservations-table/reservation-list-table"
|
||||||
import { useTranslation } from "react-i18next"
|
|
||||||
import { HttpTypes } from "@medusajs/types"
|
|
||||||
|
|
||||||
type InventoryItemLocationLevelsSectionProps = {
|
type InventoryItemLocationLevelsSectionProps = {
|
||||||
inventoryItem: HttpTypes.AdminInventoryItemResponse["inventory_item"]
|
inventoryItem: HttpTypes.AdminInventoryItemResponse["inventory_item"]
|
||||||
@@ -13,7 +13,7 @@ export const InventoryItemReservationsSection = ({
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container className="p-0">
|
<Container className="divide-y p-0">
|
||||||
<div className="flex items-center justify-between px-6 py-4">
|
<div className="flex items-center justify-between px-6 py-4">
|
||||||
<Heading>{t("reservations.domain")}</Heading>
|
<Heading>{t("reservations.domain")}</Heading>
|
||||||
<Button size="small" variant="secondary" asChild>
|
<Button size="small" variant="secondary" asChild>
|
||||||
|
|||||||
+2
-2
@@ -1,10 +1,10 @@
|
|||||||
import { InventoryTypes, StockLocationDTO } from "@medusajs/types"
|
import { InventoryTypes, StockLocationDTO } from "@medusajs/types"
|
||||||
|
|
||||||
import { LocationActions } from "./location-actions"
|
|
||||||
import { PlaceholderCell } from "../../../../../components/table/table-cells/common/placeholder-cell"
|
|
||||||
import { createColumnHelper } from "@tanstack/react-table"
|
import { createColumnHelper } from "@tanstack/react-table"
|
||||||
import { useMemo } from "react"
|
import { useMemo } from "react"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { PlaceholderCell } from "../../../../../components/table/table-cells/common/placeholder-cell"
|
||||||
|
import { LocationActions } from "./location-actions"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds missing properties to the InventoryLevelDTO type.
|
* Adds missing properties to the InventoryLevelDTO type.
|
||||||
|
|||||||
+2
-2
@@ -7,7 +7,7 @@ import { useFieldArray, useForm } from "react-hook-form"
|
|||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
import { RouteDrawer, useRouteModal } from "../../../../../../components/modals"
|
import { RouteDrawer, useRouteModal } from "../../../../../../components/modals"
|
||||||
import { useBatchUpdateInventoryLevels } from "../../../../../../hooks/api/inventory"
|
import { useBatchInventoryItemLocationLevels } from "../../../../../../hooks/api/inventory"
|
||||||
|
|
||||||
import { useEffect, useMemo } from "react"
|
import { useEffect, useMemo } from "react"
|
||||||
import { KeyboundForm } from "../../../../../../components/utilities/keybound-form"
|
import { KeyboundForm } from "../../../../../../components/utilities/keybound-form"
|
||||||
@@ -70,7 +70,7 @@ export const ManageLocationsForm = ({
|
|||||||
)
|
)
|
||||||
}, [existingLocationLevels, locations])
|
}, [existingLocationLevels, locations])
|
||||||
|
|
||||||
const { mutateAsync } = useBatchUpdateInventoryLevels(item.id)
|
const { mutateAsync } = useBatchInventoryItemLocationLevels(item.id)
|
||||||
|
|
||||||
const handleSubmit = form.handleSubmit(async ({ locations }) => {
|
const handleSubmit = form.handleSubmit(async ({ locations }) => {
|
||||||
// Changes in selected locations
|
// Changes in selected locations
|
||||||
|
|||||||
+25
-1
@@ -1,11 +1,14 @@
|
|||||||
import { InventoryTypes } from "@medusajs/types"
|
import { InventoryTypes } from "@medusajs/types"
|
||||||
import { Button, Container, Heading, Text } from "@medusajs/ui"
|
import { Button, Container, Heading, Text } from "@medusajs/ui"
|
||||||
|
|
||||||
|
import { RowSelectionState } from "@tanstack/react-table"
|
||||||
|
import { useState } from "react"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { Link } from "react-router-dom"
|
import { Link, useNavigate } from "react-router-dom"
|
||||||
import { DataTable } from "../../../../components/table/data-table"
|
import { DataTable } from "../../../../components/table/data-table"
|
||||||
import { useInventoryItems } from "../../../../hooks/api/inventory"
|
import { useInventoryItems } from "../../../../hooks/api/inventory"
|
||||||
import { useDataTable } from "../../../../hooks/use-data-table"
|
import { useDataTable } from "../../../../hooks/use-data-table"
|
||||||
|
import { INVENTORY_ITEM_IDS_KEY } from "../../common/constants"
|
||||||
import { useInventoryTableColumns } from "./use-inventory-table-columns"
|
import { useInventoryTableColumns } from "./use-inventory-table-columns"
|
||||||
import { useInventoryTableFilters } from "./use-inventory-table-filters"
|
import { useInventoryTableFilters } from "./use-inventory-table-filters"
|
||||||
import { useInventoryTableQuery } from "./use-inventory-table-query"
|
import { useInventoryTableQuery } from "./use-inventory-table-query"
|
||||||
@@ -14,6 +17,9 @@ const PAGE_SIZE = 20
|
|||||||
|
|
||||||
export const InventoryListTable = () => {
|
export const InventoryListTable = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
const [selection, setSelection] = useState<RowSelectionState>({})
|
||||||
|
|
||||||
const { searchParams, raw } = useInventoryTableQuery({
|
const { searchParams, raw } = useInventoryTableQuery({
|
||||||
pageSize: PAGE_SIZE,
|
pageSize: PAGE_SIZE,
|
||||||
@@ -39,6 +45,11 @@ export const InventoryListTable = () => {
|
|||||||
enablePagination: true,
|
enablePagination: true,
|
||||||
getRowId: (row) => row.id,
|
getRowId: (row) => row.id,
|
||||||
pageSize: PAGE_SIZE,
|
pageSize: PAGE_SIZE,
|
||||||
|
enableRowSelection: true,
|
||||||
|
rowSelection: {
|
||||||
|
state: selection,
|
||||||
|
updater: setSelection,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
if (isError) {
|
if (isError) {
|
||||||
@@ -75,6 +86,19 @@ export const InventoryListTable = () => {
|
|||||||
{ key: "reserved_quantity", label: t("inventory.reserved") },
|
{ key: "reserved_quantity", label: t("inventory.reserved") },
|
||||||
]}
|
]}
|
||||||
navigateTo={(row) => `${row.id}`}
|
navigateTo={(row) => `${row.id}`}
|
||||||
|
commands={[
|
||||||
|
{
|
||||||
|
action: async (selection) => {
|
||||||
|
navigate(
|
||||||
|
`stock?${INVENTORY_ITEM_IDS_KEY}=${Object.keys(selection).join(
|
||||||
|
","
|
||||||
|
)}`
|
||||||
|
)
|
||||||
|
},
|
||||||
|
label: t("inventory.stock.action"),
|
||||||
|
shortcut: "i",
|
||||||
|
},
|
||||||
|
]}
|
||||||
/>
|
/>
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
|
|||||||
+31
-2
@@ -1,10 +1,11 @@
|
|||||||
import { InventoryTypes, ProductVariantDTO } from "@medusajs/types"
|
import { InventoryTypes, ProductVariantDTO } from "@medusajs/types"
|
||||||
|
|
||||||
import { InventoryActions } from "./inventory-actions"
|
import { Checkbox } from "@medusajs/ui"
|
||||||
import { PlaceholderCell } from "../../../../components/table/table-cells/common/placeholder-cell"
|
|
||||||
import { createColumnHelper } from "@tanstack/react-table"
|
import { createColumnHelper } from "@tanstack/react-table"
|
||||||
import { useMemo } from "react"
|
import { useMemo } from "react"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { PlaceholderCell } from "../../../../components/table/table-cells/common/placeholder-cell"
|
||||||
|
import { InventoryActions } from "./inventory-actions"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds missing properties to the InventoryItemDTO type.
|
* Adds missing properties to the InventoryItemDTO type.
|
||||||
@@ -22,6 +23,34 @@ export const useInventoryTableColumns = () => {
|
|||||||
|
|
||||||
return useMemo(
|
return useMemo(
|
||||||
() => [
|
() => [
|
||||||
|
columnHelper.display({
|
||||||
|
id: "select",
|
||||||
|
header: ({ table }) => {
|
||||||
|
return (
|
||||||
|
<Checkbox
|
||||||
|
checked={
|
||||||
|
table.getIsSomePageRowsSelected()
|
||||||
|
? "indeterminate"
|
||||||
|
: table.getIsAllPageRowsSelected()
|
||||||
|
}
|
||||||
|
onCheckedChange={(value) =>
|
||||||
|
table.toggleAllPageRowsSelected(!!value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
cell: ({ row }) => {
|
||||||
|
return (
|
||||||
|
<Checkbox
|
||||||
|
checked={row.getIsSelected()}
|
||||||
|
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}),
|
||||||
columnHelper.accessor("title", {
|
columnHelper.accessor("title", {
|
||||||
header: t("fields.title"),
|
header: t("fields.title"),
|
||||||
cell: ({ getValue }) => {
|
cell: ({ getValue }) => {
|
||||||
|
|||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./inventory-stock-form"
|
||||||
+164
@@ -0,0 +1,164 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
|
import { HttpTypes } from "@medusajs/types"
|
||||||
|
import { Button, toast } from "@medusajs/ui"
|
||||||
|
import { useRef } from "react"
|
||||||
|
import { DefaultValues, useForm } from "react-hook-form"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { DataGrid } from "../../../../../components/data-grid"
|
||||||
|
import {
|
||||||
|
RouteFocusModal,
|
||||||
|
useRouteModal,
|
||||||
|
} from "../../../../../components/modals"
|
||||||
|
import { KeyboundForm } from "../../../../../components/utilities/keybound-form"
|
||||||
|
import { useBatchInventoryItemsLocationLevels } from "../../../../../hooks/api"
|
||||||
|
import { castNumber } from "../../../../../lib/cast-number"
|
||||||
|
import { useInventoryStockColumns } from "../../hooks/use-inventory-stock-columns"
|
||||||
|
import {
|
||||||
|
InventoryItemSchema,
|
||||||
|
InventoryLocationsSchema,
|
||||||
|
InventoryStockSchema,
|
||||||
|
} from "../../schema"
|
||||||
|
|
||||||
|
type InventoryStockFormProps = {
|
||||||
|
items: HttpTypes.AdminInventoryItem[]
|
||||||
|
locations: HttpTypes.AdminStockLocation[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const InventoryStockForm = ({
|
||||||
|
items,
|
||||||
|
locations,
|
||||||
|
}: InventoryStockFormProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { setCloseOnEscape, handleSuccess } = useRouteModal()
|
||||||
|
|
||||||
|
const initialValues = useRef(getDefaultValues(items, locations))
|
||||||
|
console.log("initialValues", initialValues.current)
|
||||||
|
|
||||||
|
const form = useForm<InventoryStockSchema>({
|
||||||
|
defaultValues: getDefaultValues(items, locations),
|
||||||
|
resolver: zodResolver(InventoryStockSchema),
|
||||||
|
})
|
||||||
|
|
||||||
|
const columns = useInventoryStockColumns(locations)
|
||||||
|
|
||||||
|
const { mutateAsync, isPending } = useBatchInventoryItemsLocationLevels()
|
||||||
|
|
||||||
|
const onSubmit = form.handleSubmit(async (data) => {
|
||||||
|
const payload: HttpTypes.AdminBatchInventoryItemsLocationLevels = {
|
||||||
|
create: [],
|
||||||
|
update: [],
|
||||||
|
delete: [],
|
||||||
|
force: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [inventory_item_id, item] of Object.entries(
|
||||||
|
data.inventory_items
|
||||||
|
)) {
|
||||||
|
for (const [location_id, level] of Object.entries(item.locations)) {
|
||||||
|
if (level.id) {
|
||||||
|
const wasChecked =
|
||||||
|
initialValues.current?.inventory_items?.[inventory_item_id]
|
||||||
|
?.locations?.[location_id]?.checked
|
||||||
|
|
||||||
|
if (wasChecked && !level.checked) {
|
||||||
|
payload.delete.push(level.id)
|
||||||
|
} else {
|
||||||
|
const newQuantity =
|
||||||
|
level.quantity !== "" ? castNumber(level.quantity) : 0
|
||||||
|
const originalQuantity =
|
||||||
|
initialValues.current?.inventory_items?.[inventory_item_id]
|
||||||
|
?.locations?.[location_id]?.quantity
|
||||||
|
|
||||||
|
if (newQuantity !== originalQuantity) {
|
||||||
|
payload.update.push({
|
||||||
|
id: level.id,
|
||||||
|
inventory_item_id,
|
||||||
|
location_id,
|
||||||
|
stocked_quantity: newQuantity,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!level.id && level.quantity !== "") {
|
||||||
|
payload.create.push({
|
||||||
|
inventory_item_id,
|
||||||
|
location_id,
|
||||||
|
stocked_quantity: castNumber(level.quantity),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await mutateAsync(payload, {
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success(t("inventory.stock.successToast"))
|
||||||
|
handleSuccess()
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(error.message)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RouteFocusModal.Form form={form}>
|
||||||
|
<KeyboundForm onSubmit={onSubmit} className="flex size-full flex-col">
|
||||||
|
<RouteFocusModal.Header />
|
||||||
|
<RouteFocusModal.Body className="size-full flex-1 overflow-y-auto">
|
||||||
|
<DataGrid
|
||||||
|
columns={columns}
|
||||||
|
data={items}
|
||||||
|
state={form}
|
||||||
|
onEditingChange={(editing) => {
|
||||||
|
setCloseOnEscape(!editing)
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</RouteFocusModal.Body>
|
||||||
|
<RouteFocusModal.Footer>
|
||||||
|
<div className="flex items-center justify-end gap-2">
|
||||||
|
<RouteFocusModal.Close asChild>
|
||||||
|
<Button variant="secondary" size="small" type="button">
|
||||||
|
{t("actions.cancel")}
|
||||||
|
</Button>
|
||||||
|
</RouteFocusModal.Close>
|
||||||
|
<Button type="submit" size="small" isLoading={isPending}>
|
||||||
|
{t("actions.save")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</RouteFocusModal.Footer>
|
||||||
|
</KeyboundForm>
|
||||||
|
</RouteFocusModal.Form>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDefaultValues(
|
||||||
|
items: HttpTypes.AdminInventoryItem[],
|
||||||
|
locations: HttpTypes.AdminStockLocation[]
|
||||||
|
): DefaultValues<InventoryStockSchema> {
|
||||||
|
return {
|
||||||
|
inventory_items: items.reduce((acc, item) => {
|
||||||
|
const locationsMap = locations.reduce((locationAcc, location) => {
|
||||||
|
const level = item.location_levels?.find(
|
||||||
|
(level) => level.location_id === location.id
|
||||||
|
)
|
||||||
|
|
||||||
|
locationAcc[location.id] = {
|
||||||
|
id: level?.id,
|
||||||
|
quantity:
|
||||||
|
typeof level?.stocked_quantity === "number"
|
||||||
|
? level?.stocked_quantity
|
||||||
|
: "",
|
||||||
|
checked: !!level,
|
||||||
|
disabledToggle:
|
||||||
|
(level?.incoming_quantity || 0) > 0 ||
|
||||||
|
(level?.reserved_quantity || 0) > 0,
|
||||||
|
}
|
||||||
|
return locationAcc
|
||||||
|
}, {} as InventoryLocationsSchema)
|
||||||
|
|
||||||
|
acc[item.id] = { locations: locationsMap }
|
||||||
|
return acc
|
||||||
|
}, {} as Record<string, InventoryItemSchema>),
|
||||||
|
}
|
||||||
|
}
|
||||||
+76
@@ -0,0 +1,76 @@
|
|||||||
|
import { HttpTypes } from "@medusajs/types"
|
||||||
|
import { useMemo } from "react"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { createDataGridHelper } from "../../../../components/data-grid"
|
||||||
|
import { DataGridReadOnlyCell } from "../../../../components/data-grid/components"
|
||||||
|
import { DataGridTogglableNumberCell } from "../../../../components/data-grid/components/data-grid-toggleable-number-cell"
|
||||||
|
import { InventoryStockSchema } from "../schema"
|
||||||
|
|
||||||
|
const helper = createDataGridHelper<
|
||||||
|
HttpTypes.AdminInventoryItem,
|
||||||
|
InventoryStockSchema
|
||||||
|
>()
|
||||||
|
|
||||||
|
export const useInventoryStockColumns = (
|
||||||
|
locations: HttpTypes.AdminStockLocation[] = []
|
||||||
|
) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() => [
|
||||||
|
helper.column({
|
||||||
|
id: "title",
|
||||||
|
name: "Title",
|
||||||
|
header: "Title",
|
||||||
|
cell: (context) => {
|
||||||
|
const item = context.row.original
|
||||||
|
return (
|
||||||
|
<DataGridReadOnlyCell context={context} color="normal">
|
||||||
|
<span title={item.title || undefined}>{item.title || "-"}</span>
|
||||||
|
</DataGridReadOnlyCell>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
disableHiding: true,
|
||||||
|
}),
|
||||||
|
helper.column({
|
||||||
|
id: "sku",
|
||||||
|
name: "SKU",
|
||||||
|
header: "SKU",
|
||||||
|
cell: (context) => {
|
||||||
|
const item = context.row.original
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DataGridReadOnlyCell context={context} color="normal">
|
||||||
|
<span title={item.sku || undefined}>{item.sku || "-"}</span>
|
||||||
|
</DataGridReadOnlyCell>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
disableHiding: true,
|
||||||
|
}),
|
||||||
|
...locations.map((location) =>
|
||||||
|
helper.column({
|
||||||
|
id: `location_${location.id}`,
|
||||||
|
name: location.name,
|
||||||
|
header: location.name,
|
||||||
|
field: (context) => {
|
||||||
|
const item = context.row.original
|
||||||
|
|
||||||
|
return `inventory_items.${item.id}.locations.${location.id}` as const
|
||||||
|
},
|
||||||
|
type: "togglable-number",
|
||||||
|
cell: (context) => {
|
||||||
|
return (
|
||||||
|
<DataGridTogglableNumberCell
|
||||||
|
context={context}
|
||||||
|
disabledToggleTooltip={t(
|
||||||
|
"inventory.stock.disabledToggleTooltip"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
),
|
||||||
|
],
|
||||||
|
[locations, t]
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { InventoryStock as Component } from "./inventory-stock"
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { useSearchParams } from "react-router-dom"
|
||||||
|
import { RouteFocusModal } from "../../../components/modals"
|
||||||
|
import { useInventoryItems, useStockLocations } from "../../../hooks/api"
|
||||||
|
import { INVENTORY_ITEM_IDS_KEY } from "../common/constants"
|
||||||
|
import { InventoryStockForm } from "./components/inventory-stock-form"
|
||||||
|
|
||||||
|
export const InventoryStock = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const [searchParams] = useSearchParams()
|
||||||
|
const inventoryItemIds =
|
||||||
|
searchParams.get(INVENTORY_ITEM_IDS_KEY)?.split(",") || undefined
|
||||||
|
|
||||||
|
const { inventory_items, isPending, isError, error } = useInventoryItems({
|
||||||
|
id: inventoryItemIds,
|
||||||
|
})
|
||||||
|
|
||||||
|
const {
|
||||||
|
stock_locations,
|
||||||
|
isPending: isPendingStockLocations,
|
||||||
|
isError: isErrorStockLocations,
|
||||||
|
error: errorStockLocations,
|
||||||
|
} = useStockLocations({
|
||||||
|
limit: 9999,
|
||||||
|
fields: "id,name",
|
||||||
|
})
|
||||||
|
|
||||||
|
const ready =
|
||||||
|
!isPending &&
|
||||||
|
!!inventory_items &&
|
||||||
|
!isPendingStockLocations &&
|
||||||
|
!!stock_locations
|
||||||
|
|
||||||
|
if (isError) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isErrorStockLocations) {
|
||||||
|
throw errorStockLocations
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RouteFocusModal>
|
||||||
|
<RouteFocusModal.Title asChild>
|
||||||
|
<span className="sr-only">{t("inventory.stock.title")}</span>
|
||||||
|
</RouteFocusModal.Title>
|
||||||
|
<RouteFocusModal.Description asChild>
|
||||||
|
<span className="sr-only">{t("inventory.stock.description")}</span>
|
||||||
|
</RouteFocusModal.Description>
|
||||||
|
{ready && (
|
||||||
|
<InventoryStockForm
|
||||||
|
items={inventory_items}
|
||||||
|
locations={stock_locations}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</RouteFocusModal>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { z } from "zod"
|
||||||
|
|
||||||
|
const LocationQuantitySchema = z.object({
|
||||||
|
id: z.string().optional(),
|
||||||
|
quantity: z.union([z.number(), z.string()]),
|
||||||
|
checked: z.boolean(),
|
||||||
|
disabledToggle: z.boolean(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const InventoryLocationsSchema = z.record(LocationQuantitySchema)
|
||||||
|
|
||||||
|
const InventoryItemSchema = z.object({
|
||||||
|
locations: InventoryLocationsSchema,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const InventoryStockSchema = z.object({
|
||||||
|
inventory_items: z.record(InventoryItemSchema),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type InventoryLocationsSchema = z.infer<typeof InventoryLocationsSchema>
|
||||||
|
export type InventoryItemSchema = z.infer<typeof InventoryItemSchema>
|
||||||
|
export type InventoryStockSchema = z.infer<typeof InventoryStockSchema>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import { Alert, Button, Heading, Hint, Input, Text, toast } from "@medusajs/ui"
|
import { Alert, Button, Heading, Hint, Input, Text, toast } from "@medusajs/ui"
|
||||||
import { AnimatePresence, motion } from "framer-motion"
|
|
||||||
import i18n from "i18next"
|
import i18n from "i18next"
|
||||||
|
import { AnimatePresence, motion } from "motion/react"
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
import { useForm } from "react-hook-form"
|
import { useForm } from "react-hook-form"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
|
|||||||
+1
-1
@@ -130,7 +130,7 @@ const OuterComponent = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="absolute inset-y-0 z-[2] flex w-fit items-center justify-center"
|
className="absolute inset-y-0 z-[3] flex w-fit items-center justify-center"
|
||||||
style={{
|
style={{
|
||||||
left: symbolWidth ? `${symbolWidth + 16 + 4}px` : undefined,
|
left: symbolWidth ? `${symbolWidth + 16 + 4}px` : undefined,
|
||||||
}}
|
}}
|
||||||
|
|||||||
+2
-2
@@ -41,7 +41,7 @@ export const usePriceListGridColumns = ({
|
|||||||
return (
|
return (
|
||||||
<DataGrid.ReadonlyCell context={context}>
|
<DataGrid.ReadonlyCell context={context}>
|
||||||
<div className="flex h-full w-full items-center gap-x-2 overflow-hidden">
|
<div className="flex h-full w-full items-center gap-x-2 overflow-hidden">
|
||||||
<Thumbnail src={entity.thumbnail} />
|
<Thumbnail src={entity.thumbnail} size="small" />
|
||||||
<span className="truncate">{entity.title}</span>
|
<span className="truncate">{entity.title}</span>
|
||||||
</div>
|
</div>
|
||||||
</DataGrid.ReadonlyCell>
|
</DataGrid.ReadonlyCell>
|
||||||
@@ -49,7 +49,7 @@ export const usePriceListGridColumns = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DataGrid.ReadonlyCell context={context}>
|
<DataGrid.ReadonlyCell context={context} color="normal">
|
||||||
<div className="flex h-full w-full items-center gap-x-2 overflow-hidden">
|
<div className="flex h-full w-full items-center gap-x-2 overflow-hidden">
|
||||||
<span className="truncate">{entity.title}</span>
|
<span className="truncate">{entity.title}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export const PRODUCT_VARIANT_IDS_KEY = "product_variant_ids"
|
||||||
+31
-1
@@ -1,13 +1,17 @@
|
|||||||
import { PencilSquare, Plus } from "@medusajs/icons"
|
import { Buildings, PencilSquare, Plus } from "@medusajs/icons"
|
||||||
import { HttpTypes } from "@medusajs/types"
|
import { HttpTypes } from "@medusajs/types"
|
||||||
import { Container, Heading } from "@medusajs/ui"
|
import { Container, Heading } from "@medusajs/ui"
|
||||||
import { keepPreviousData } from "@tanstack/react-query"
|
import { keepPreviousData } from "@tanstack/react-query"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
|
import { RowSelectionState } from "@tanstack/react-table"
|
||||||
|
import { useState } from "react"
|
||||||
|
import { useNavigate } from "react-router-dom"
|
||||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||||
import { DataTable } from "../../../../../components/table/data-table"
|
import { DataTable } from "../../../../../components/table/data-table"
|
||||||
import { useProductVariants } from "../../../../../hooks/api/products"
|
import { useProductVariants } from "../../../../../hooks/api/products"
|
||||||
import { useDataTable } from "../../../../../hooks/use-data-table"
|
import { useDataTable } from "../../../../../hooks/use-data-table"
|
||||||
|
import { PRODUCT_VARIANT_IDS_KEY } from "../../../common/constants"
|
||||||
import { useProductVariantTableColumns } from "./use-variant-table-columns"
|
import { useProductVariantTableColumns } from "./use-variant-table-columns"
|
||||||
import { useProductVariantTableFilters } from "./use-variant-table-filters"
|
import { useProductVariantTableFilters } from "./use-variant-table-filters"
|
||||||
import { useProductVariantTableQuery } from "./use-variant-table-query"
|
import { useProductVariantTableQuery } from "./use-variant-table-query"
|
||||||
@@ -22,6 +26,7 @@ export const ProductVariantSection = ({
|
|||||||
product,
|
product,
|
||||||
}: ProductVariantSectionProps) => {
|
}: ProductVariantSectionProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
const { searchParams, raw } = useProductVariantTableQuery({
|
const { searchParams, raw } = useProductVariantTableQuery({
|
||||||
pageSize: PAGE_SIZE,
|
pageSize: PAGE_SIZE,
|
||||||
@@ -37,6 +42,8 @@ export const ProductVariantSection = ({
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const [selection, setSelection] = useState<RowSelectionState>({})
|
||||||
|
|
||||||
const filters = useProductVariantTableFilters()
|
const filters = useProductVariantTableFilters()
|
||||||
const columns = useProductVariantTableColumns(product)
|
const columns = useProductVariantTableColumns(product)
|
||||||
|
|
||||||
@@ -47,6 +54,11 @@ export const ProductVariantSection = ({
|
|||||||
enablePagination: true,
|
enablePagination: true,
|
||||||
getRowId: (row) => row.id,
|
getRowId: (row) => row.id,
|
||||||
pageSize: PAGE_SIZE,
|
pageSize: PAGE_SIZE,
|
||||||
|
enableRowSelection: true,
|
||||||
|
rowSelection: {
|
||||||
|
state: selection,
|
||||||
|
updater: setSelection,
|
||||||
|
},
|
||||||
meta: {
|
meta: {
|
||||||
product,
|
product,
|
||||||
},
|
},
|
||||||
@@ -74,6 +86,11 @@ export const ProductVariantSection = ({
|
|||||||
to: `prices`,
|
to: `prices`,
|
||||||
icon: <PencilSquare />,
|
icon: <PencilSquare />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: t("inventory.stock.action"),
|
||||||
|
to: `stock`,
|
||||||
|
icon: <Buildings />,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
@@ -97,6 +114,19 @@ export const ProductVariantSection = ({
|
|||||||
pagination
|
pagination
|
||||||
search
|
search
|
||||||
queryObject={raw}
|
queryObject={raw}
|
||||||
|
commands={[
|
||||||
|
{
|
||||||
|
action: async (selection) => {
|
||||||
|
navigate(
|
||||||
|
`stock?${PRODUCT_VARIANT_IDS_KEY}=${Object.keys(selection).join(
|
||||||
|
","
|
||||||
|
)}`
|
||||||
|
)
|
||||||
|
},
|
||||||
|
label: t("inventory.stock.action"),
|
||||||
|
shortcut: "i",
|
||||||
|
},
|
||||||
|
]}
|
||||||
/>
|
/>
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
|
|||||||
+38
-6
@@ -1,6 +1,6 @@
|
|||||||
import { Buildings, Component, PencilSquare, Trash } from "@medusajs/icons"
|
import { Buildings, Component, PencilSquare, Trash } from "@medusajs/icons"
|
||||||
import { HttpTypes, InventoryItemDTO } from "@medusajs/types"
|
import { HttpTypes, InventoryItemDTO } from "@medusajs/types"
|
||||||
import { Badge, clx, usePrompt } from "@medusajs/ui"
|
import { Badge, Checkbox, clx, usePrompt } from "@medusajs/ui"
|
||||||
import { createColumnHelper } from "@tanstack/react-table"
|
import { createColumnHelper } from "@tanstack/react-table"
|
||||||
import { useMemo } from "react"
|
import { useMemo } from "react"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
@@ -73,11 +73,6 @@ const VariantActions = ({
|
|||||||
to: `edit-variant?variant_id=${variant.id}`,
|
to: `edit-variant?variant_id=${variant.id}`,
|
||||||
icon: <PencilSquare />,
|
icon: <PencilSquare />,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: t("actions.delete"),
|
|
||||||
onClick: handleDelete,
|
|
||||||
icon: <Trash />,
|
|
||||||
},
|
|
||||||
hasInventoryItem
|
hasInventoryItem
|
||||||
? {
|
? {
|
||||||
label: t("products.variant.inventory.actions.inventoryItems"),
|
label: t("products.variant.inventory.actions.inventoryItems"),
|
||||||
@@ -94,6 +89,15 @@ const VariantActions = ({
|
|||||||
: false,
|
: false,
|
||||||
].filter(Boolean) as Action[],
|
].filter(Boolean) as Action[],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
label: t("actions.delete"),
|
||||||
|
onClick: handleDelete,
|
||||||
|
icon: <Trash />,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@@ -145,6 +149,34 @@ export const useProductVariantTableColumns = (
|
|||||||
|
|
||||||
return useMemo(
|
return useMemo(
|
||||||
() => [
|
() => [
|
||||||
|
columnHelper.display({
|
||||||
|
id: "select",
|
||||||
|
header: ({ table }) => {
|
||||||
|
return (
|
||||||
|
<Checkbox
|
||||||
|
checked={
|
||||||
|
table.getIsSomePageRowsSelected()
|
||||||
|
? "indeterminate"
|
||||||
|
: table.getIsAllPageRowsSelected()
|
||||||
|
}
|
||||||
|
onCheckedChange={(value) =>
|
||||||
|
table.toggleAllPageRowsSelected(!!value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
cell: ({ row }) => {
|
||||||
|
return (
|
||||||
|
<Checkbox
|
||||||
|
checked={row.getIsSelected()}
|
||||||
|
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}),
|
||||||
columnHelper.accessor("title", {
|
columnHelper.accessor("title", {
|
||||||
header: () => (
|
header: () => (
|
||||||
<div className="flex h-full w-full items-center">
|
<div className="flex h-full w-full items-center">
|
||||||
|
|||||||
+1
@@ -0,0 +1 @@
|
|||||||
|
export * from "./product-stock-form"
|
||||||
+220
@@ -0,0 +1,220 @@
|
|||||||
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
|
import { HttpTypes } from "@medusajs/types"
|
||||||
|
import { Button, toast, usePrompt } from "@medusajs/ui"
|
||||||
|
import { useEffect, useMemo, useRef, useState } from "react"
|
||||||
|
import { DefaultValues, useForm } from "react-hook-form"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
|
||||||
|
import { DataGrid } from "../../../../../components/data-grid"
|
||||||
|
import {
|
||||||
|
RouteFocusModal,
|
||||||
|
useRouteModal,
|
||||||
|
} from "../../../../../components/modals"
|
||||||
|
import { KeyboundForm } from "../../../../../components/utilities/keybound-form"
|
||||||
|
import { useBatchInventoryItemsLocationLevels } from "../../../../../hooks/api"
|
||||||
|
import { castNumber } from "../../../../../lib/cast-number"
|
||||||
|
import { useProductStockColumns } from "../../hooks/use-product-stock-columns"
|
||||||
|
import {
|
||||||
|
ProductStockInventoryItemSchema,
|
||||||
|
ProductStockLocationSchema,
|
||||||
|
ProductStockSchema,
|
||||||
|
ProductStockVariantSchema,
|
||||||
|
} from "../../schema"
|
||||||
|
import {
|
||||||
|
getDisabledInventoryRows,
|
||||||
|
isProductVariantWithInventoryPivot,
|
||||||
|
} from "../../utils"
|
||||||
|
|
||||||
|
type ProductStockFormProps = {
|
||||||
|
variants: HttpTypes.AdminProductVariant[]
|
||||||
|
locations: HttpTypes.AdminStockLocation[]
|
||||||
|
onLoaded: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProductStockForm = ({
|
||||||
|
variants,
|
||||||
|
locations,
|
||||||
|
onLoaded,
|
||||||
|
}: ProductStockFormProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { handleSuccess, setCloseOnEscape } = useRouteModal()
|
||||||
|
const prompt = usePrompt()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onLoaded()
|
||||||
|
}, [onLoaded])
|
||||||
|
|
||||||
|
const [isPromptOpen, setIsPromptOpen] = useState(false)
|
||||||
|
|
||||||
|
const form = useForm<ProductStockSchema>({
|
||||||
|
defaultValues: getDefaultValue(variants, locations),
|
||||||
|
resolver: zodResolver(ProductStockSchema),
|
||||||
|
})
|
||||||
|
|
||||||
|
const initialValues = useRef(getDefaultValue(variants, locations))
|
||||||
|
|
||||||
|
const disabled = useMemo(() => getDisabledInventoryRows(variants), [variants])
|
||||||
|
const columns = useProductStockColumns(locations, disabled)
|
||||||
|
|
||||||
|
const { mutateAsync, isPending } = useBatchInventoryItemsLocationLevels()
|
||||||
|
|
||||||
|
const onSubmit = form.handleSubmit(async (data) => {
|
||||||
|
const payload: HttpTypes.AdminBatchInventoryItemsLocationLevels = {
|
||||||
|
create: [],
|
||||||
|
update: [],
|
||||||
|
delete: [],
|
||||||
|
force: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [variantId, variant] of Object.entries(data.variants)) {
|
||||||
|
for (const [inventory_item_id, item] of Object.entries(
|
||||||
|
variant.inventory_items
|
||||||
|
)) {
|
||||||
|
for (const [location_id, level] of Object.entries(item.locations)) {
|
||||||
|
if (level.id) {
|
||||||
|
const wasChecked =
|
||||||
|
initialValues.current?.variants?.[variantId]?.inventory_items?.[
|
||||||
|
inventory_item_id
|
||||||
|
]?.locations?.[location_id]?.checked
|
||||||
|
|
||||||
|
if (wasChecked && !level.checked) {
|
||||||
|
payload.delete.push(level.id)
|
||||||
|
} else {
|
||||||
|
const newQuantity =
|
||||||
|
level.quantity !== "" ? castNumber(level.quantity) : 0
|
||||||
|
const originalQuantity =
|
||||||
|
initialValues.current?.variants?.[variantId]?.inventory_items?.[
|
||||||
|
inventory_item_id
|
||||||
|
]?.locations?.[location_id]?.quantity
|
||||||
|
|
||||||
|
if (newQuantity !== originalQuantity) {
|
||||||
|
payload.update.push({
|
||||||
|
inventory_item_id,
|
||||||
|
location_id,
|
||||||
|
stocked_quantity: newQuantity,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!level.id && level.quantity !== "") {
|
||||||
|
payload.create.push({
|
||||||
|
inventory_item_id,
|
||||||
|
location_id,
|
||||||
|
stocked_quantity: castNumber(level.quantity),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payload.delete.length > 0) {
|
||||||
|
setIsPromptOpen(true)
|
||||||
|
const confirm = await prompt({
|
||||||
|
title: t("general.areYouSure"),
|
||||||
|
description: t("inventory.stock.disablePrompt", {
|
||||||
|
count: payload.delete.length,
|
||||||
|
}),
|
||||||
|
confirmText: t("actions.continue"),
|
||||||
|
cancelText: t("actions.cancel"),
|
||||||
|
variant: "confirmation",
|
||||||
|
})
|
||||||
|
|
||||||
|
setIsPromptOpen(false)
|
||||||
|
|
||||||
|
if (!confirm) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await mutateAsync(payload, {
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success(t("inventory.stock.successToast"))
|
||||||
|
handleSuccess()
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(error.message)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RouteFocusModal.Form form={form}>
|
||||||
|
<KeyboundForm onSubmit={onSubmit} className="flex h-full flex-col">
|
||||||
|
<RouteFocusModal.Header />
|
||||||
|
<RouteFocusModal.Body className="flex-1">
|
||||||
|
<DataGrid
|
||||||
|
state={form}
|
||||||
|
columns={columns}
|
||||||
|
data={variants}
|
||||||
|
getSubRows={getSubRows}
|
||||||
|
onEditingChange={(editing) => setCloseOnEscape(!editing)}
|
||||||
|
disableInteractions={isPending || isPromptOpen}
|
||||||
|
multiColumnSelection={true}
|
||||||
|
/>
|
||||||
|
</RouteFocusModal.Body>
|
||||||
|
<RouteFocusModal.Footer>
|
||||||
|
<div className="flex items-center justify-end gap-2">
|
||||||
|
<RouteFocusModal.Close asChild>
|
||||||
|
<Button variant="secondary" size="small" type="button">
|
||||||
|
{t("actions.cancel")}
|
||||||
|
</Button>
|
||||||
|
</RouteFocusModal.Close>
|
||||||
|
<Button type="submit" size="small" isLoading={isPending}>
|
||||||
|
{t("actions.save")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</RouteFocusModal.Footer>
|
||||||
|
</KeyboundForm>
|
||||||
|
</RouteFocusModal.Form>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSubRows(
|
||||||
|
row:
|
||||||
|
| HttpTypes.AdminProductVariant
|
||||||
|
| HttpTypes.AdminProductVariantInventoryItemLink
|
||||||
|
): HttpTypes.AdminProductVariantInventoryItemLink[] | undefined {
|
||||||
|
if (isProductVariantWithInventoryPivot(row)) {
|
||||||
|
return row.inventory_items
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDefaultValue(
|
||||||
|
variants: HttpTypes.AdminProductVariant[],
|
||||||
|
locations: HttpTypes.AdminStockLocation[]
|
||||||
|
): DefaultValues<ProductStockSchema> {
|
||||||
|
return {
|
||||||
|
variants: variants.reduce((variantAcc, variant) => {
|
||||||
|
const inventoryItems = variant.inventory_items?.reduce(
|
||||||
|
(itemAcc, item) => {
|
||||||
|
const locationsMap = locations.reduce((locationAcc, location) => {
|
||||||
|
const level = item.inventory?.location_levels?.find(
|
||||||
|
(level) => level.location_id === location.id
|
||||||
|
)
|
||||||
|
|
||||||
|
locationAcc[location.id] = {
|
||||||
|
id: level?.id,
|
||||||
|
quantity:
|
||||||
|
level?.stocked_quantity !== undefined
|
||||||
|
? level?.stocked_quantity
|
||||||
|
: "",
|
||||||
|
checked: !!level,
|
||||||
|
disabledToggle:
|
||||||
|
(level?.incoming_quantity || 0) > 0 ||
|
||||||
|
(level?.reserved_quantity || 0) > 0,
|
||||||
|
}
|
||||||
|
return locationAcc
|
||||||
|
}, {} as ProductStockLocationSchema)
|
||||||
|
|
||||||
|
itemAcc[item.inventory_item_id] = { locations: locationsMap }
|
||||||
|
return itemAcc
|
||||||
|
},
|
||||||
|
{} as Record<string, ProductStockInventoryItemSchema>
|
||||||
|
)
|
||||||
|
|
||||||
|
variantAcc[variant.id] = { inventory_items: inventoryItems || {} }
|
||||||
|
return variantAcc
|
||||||
|
}, {} as Record<string, ProductStockVariantSchema>),
|
||||||
|
}
|
||||||
|
}
|
||||||
+226
@@ -0,0 +1,226 @@
|
|||||||
|
import { InformationCircle } from "@medusajs/icons"
|
||||||
|
import { HttpTypes } from "@medusajs/types"
|
||||||
|
import { Switch, Tooltip } from "@medusajs/ui"
|
||||||
|
import { useCallback, useMemo } from "react"
|
||||||
|
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { Thumbnail } from "../../../../components/common/thumbnail"
|
||||||
|
import { createDataGridHelper } from "../../../../components/data-grid"
|
||||||
|
import { DataGridReadOnlyCell } from "../../../../components/data-grid/components"
|
||||||
|
import { DataGridDuplicateCell } from "../../../../components/data-grid/components/data-grid-duplicate-cell"
|
||||||
|
import { DataGridTogglableNumberCell } from "../../../../components/data-grid/components/data-grid-toggleable-number-cell"
|
||||||
|
import { ProductStockSchema } from "../schema"
|
||||||
|
import { isProductVariant } from "../utils"
|
||||||
|
|
||||||
|
const helper = createDataGridHelper<
|
||||||
|
| HttpTypes.AdminProductVariant
|
||||||
|
| HttpTypes.AdminProductVariantInventoryItemLink,
|
||||||
|
ProductStockSchema
|
||||||
|
>()
|
||||||
|
|
||||||
|
type DisabledItem = { id: string; title: string; sku: string }
|
||||||
|
type DisabledResult =
|
||||||
|
| {
|
||||||
|
isDisabled: true
|
||||||
|
item: DisabledItem
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
isDisabled: false
|
||||||
|
item: undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useProductStockColumns = (
|
||||||
|
locations: HttpTypes.AdminStockLocation[] = [],
|
||||||
|
disabled: Record<string, DisabledItem> = {}
|
||||||
|
) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const getIsDisabled = useCallback(
|
||||||
|
(item: HttpTypes.AdminProductVariantInventoryItemLink): DisabledResult => {
|
||||||
|
const disabledItem = disabled[item.inventory_item_id]
|
||||||
|
const isDisabled = !!disabledItem && disabledItem.id !== item.variant_id
|
||||||
|
|
||||||
|
if (!isDisabled) {
|
||||||
|
return {
|
||||||
|
isDisabled: false,
|
||||||
|
item: undefined,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isDisabled,
|
||||||
|
item: disabledItem,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[disabled]
|
||||||
|
)
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() => [
|
||||||
|
helper.column({
|
||||||
|
id: "title",
|
||||||
|
name: "Title",
|
||||||
|
header: "Title",
|
||||||
|
cell: (context) => {
|
||||||
|
const item = context.row.original
|
||||||
|
|
||||||
|
if (isProductVariant(item)) {
|
||||||
|
return (
|
||||||
|
<DataGridReadOnlyCell context={context}>
|
||||||
|
<div className="flex items-center gap-x-2">
|
||||||
|
<Thumbnail size="small" src={item.product?.thumbnail} />
|
||||||
|
<span>{item.title || "-"}</span>
|
||||||
|
</div>
|
||||||
|
</DataGridReadOnlyCell>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const { isDisabled, item: disabledItem } = getIsDisabled(item)
|
||||||
|
|
||||||
|
if (isDisabled) {
|
||||||
|
return (
|
||||||
|
<DataGridReadOnlyCell context={context} color="normal">
|
||||||
|
<div className="flex size-full items-center justify-between gap-x-2">
|
||||||
|
<span
|
||||||
|
title={item.inventory?.title || undefined}
|
||||||
|
className="text-ui-fg-disabled"
|
||||||
|
>
|
||||||
|
{item.inventory?.title || "-"}
|
||||||
|
</span>
|
||||||
|
<Tooltip
|
||||||
|
content={
|
||||||
|
disabledItem.sku
|
||||||
|
? t("products.stock.tooltips.alreadyManagedWithSku", {
|
||||||
|
title: disabledItem.title,
|
||||||
|
sku: disabledItem.sku,
|
||||||
|
})
|
||||||
|
: t("products.stock.tooltips.alreadyManaged", {
|
||||||
|
title: disabledItem.title,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<InformationCircle />
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</DataGridReadOnlyCell>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DataGridReadOnlyCell context={context} color="normal">
|
||||||
|
{item.inventory?.title || "-"}
|
||||||
|
</DataGridReadOnlyCell>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
disableHiding: true,
|
||||||
|
}),
|
||||||
|
helper.column({
|
||||||
|
id: "sku",
|
||||||
|
name: "SKU",
|
||||||
|
header: "SKU",
|
||||||
|
cell: (context) => {
|
||||||
|
const item = context.row.original
|
||||||
|
|
||||||
|
if (isProductVariant(item)) {
|
||||||
|
return (
|
||||||
|
<DataGridReadOnlyCell context={context}>
|
||||||
|
{item.sku || "-"}
|
||||||
|
</DataGridReadOnlyCell>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const { isDisabled } = getIsDisabled(item)
|
||||||
|
|
||||||
|
if (isDisabled) {
|
||||||
|
return (
|
||||||
|
<DataGridReadOnlyCell context={context} color="normal">
|
||||||
|
<span className="text-ui-fg-disabled">
|
||||||
|
{item.inventory?.sku || "-"}
|
||||||
|
</span>
|
||||||
|
</DataGridReadOnlyCell>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DataGridReadOnlyCell context={context} color="normal">
|
||||||
|
{item.inventory?.sku || "-"}
|
||||||
|
</DataGridReadOnlyCell>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
disableHiding: true,
|
||||||
|
}),
|
||||||
|
...locations.map((location) =>
|
||||||
|
helper.column({
|
||||||
|
id: `location_${location.id}`,
|
||||||
|
name: location.name,
|
||||||
|
header: location.name,
|
||||||
|
field: (context) => {
|
||||||
|
const item = context.row.original
|
||||||
|
|
||||||
|
if (isProductVariant(item)) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const { isDisabled } = getIsDisabled(item)
|
||||||
|
|
||||||
|
if (isDisabled) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return `variants.${item.variant_id}.inventory_items.${item.inventory_item_id}.locations.${location.id}` as const
|
||||||
|
},
|
||||||
|
type: "togglable-number",
|
||||||
|
cell: (context) => {
|
||||||
|
const item = context.row.original
|
||||||
|
|
||||||
|
if (isProductVariant(item)) {
|
||||||
|
return <DataGridReadOnlyCell context={context} />
|
||||||
|
}
|
||||||
|
|
||||||
|
const { isDisabled, item: disabledItem } = getIsDisabled(item)
|
||||||
|
|
||||||
|
if (isDisabled) {
|
||||||
|
return (
|
||||||
|
<DataGridDuplicateCell
|
||||||
|
duplicateOf={`variants.${disabledItem.id}.inventory_items.${item.inventory_item_id}.locations.${location.id}`}
|
||||||
|
>
|
||||||
|
{({ value }) => {
|
||||||
|
const { checked, quantity } = value as {
|
||||||
|
checked: boolean
|
||||||
|
quantity: number | string
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex size-full items-center gap-x-2">
|
||||||
|
<Switch
|
||||||
|
className="shrink-0 cursor-not-allowed"
|
||||||
|
tabIndex={-1}
|
||||||
|
size="small"
|
||||||
|
checked={checked}
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
<span className="text-ui-fg-disabled flex size-full items-center justify-end">
|
||||||
|
{quantity}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</DataGridDuplicateCell>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DataGridTogglableNumberCell
|
||||||
|
context={context}
|
||||||
|
disabledToggleTooltip={t(
|
||||||
|
"inventory.stock.disabledToggleTooltip"
|
||||||
|
)}
|
||||||
|
placeholder={t("inventory.stock.placeholder")}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
),
|
||||||
|
],
|
||||||
|
[locations, getIsDisabled, t]
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { productStockLoader as loader } from "./loader"
|
||||||
|
export { ProductStock as Component } from "./product-stock"
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { HttpTypes } from "@medusajs/types"
|
||||||
|
import { defer, LoaderFunctionArgs } from "react-router-dom"
|
||||||
|
import { sdk } from "../../../lib/client"
|
||||||
|
import { PRODUCT_VARIANT_IDS_KEY } from "../common/constants"
|
||||||
|
|
||||||
|
async function getProductStockData(id: string, productVariantIds?: string[]) {
|
||||||
|
const CHUNK_SIZE = 20
|
||||||
|
let offset = 0
|
||||||
|
let totalCount = 0
|
||||||
|
|
||||||
|
let allVariants: HttpTypes.AdminProductVariant[] = []
|
||||||
|
|
||||||
|
do {
|
||||||
|
const { variants: chunk, count } = await sdk.admin.product.listVariants(
|
||||||
|
id,
|
||||||
|
{
|
||||||
|
id: productVariantIds,
|
||||||
|
offset,
|
||||||
|
limit: CHUNK_SIZE,
|
||||||
|
fields:
|
||||||
|
"id,title,sku,inventory_items,inventory_items.*,inventory_items.inventory,inventory_items.inventory.id,inventory_items.inventory.title,inventory_items.inventory.sku,*inventory_items.inventory.location_levels,product.thumbnail",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
allVariants = [...allVariants, ...chunk]
|
||||||
|
totalCount = count
|
||||||
|
offset += CHUNK_SIZE
|
||||||
|
} while (allVariants.length < totalCount)
|
||||||
|
|
||||||
|
const { stock_locations } = await sdk.admin.stockLocation.list({
|
||||||
|
limit: 9999,
|
||||||
|
fields: "id,name",
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
variants: allVariants,
|
||||||
|
locations: stock_locations,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const productStockLoader = async ({
|
||||||
|
params,
|
||||||
|
request,
|
||||||
|
}: LoaderFunctionArgs) => {
|
||||||
|
const id = params.id!
|
||||||
|
const searchParams = new URLSearchParams(request.url)
|
||||||
|
const productVariantIds =
|
||||||
|
searchParams.get(PRODUCT_VARIANT_IDS_KEY)?.split(",") || undefined
|
||||||
|
|
||||||
|
const dataPromise = getProductStockData(id, productVariantIds)
|
||||||
|
|
||||||
|
return defer({
|
||||||
|
data: dataPromise,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
import { HttpTypes } from "@medusajs/types"
|
||||||
|
import { AnimatePresence } from "motion/react"
|
||||||
|
import { Suspense, useEffect, useRef, useState } from "react"
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
|
import { Await, useLoaderData } from "react-router-dom"
|
||||||
|
|
||||||
|
import { ColumnDef } from "@tanstack/react-table"
|
||||||
|
import { ProgressBar } from "../../../components/common/progress-bar"
|
||||||
|
import { Skeleton } from "../../../components/common/skeleton"
|
||||||
|
import { DataGridSkeleton } from "../../../components/data-grid/components"
|
||||||
|
import { RouteFocusModal } from "../../../components/modals"
|
||||||
|
import { ProductStockForm } from "./components/product-stock-form"
|
||||||
|
import { productStockLoader } from "./loader"
|
||||||
|
|
||||||
|
export const ProductStock = () => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const data = useLoaderData() as Awaited<ReturnType<typeof productStockLoader>>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We render a local ProgressBar, as we cannot rely on the global NavigationBar.
|
||||||
|
* This is because we are deferring the data, meaning that the navigation is
|
||||||
|
* instant, and the data is loaded in parallel with the navigation, but may resolve
|
||||||
|
* after the navigation has completed. This will result in the data loading after the
|
||||||
|
* navigation has completed most of the time for this route, as we chunk the data into
|
||||||
|
* multiple queries.
|
||||||
|
*
|
||||||
|
* Here we instead render a local ProgressBar, which is animated, and exit
|
||||||
|
* the animation when the data is loaded, and the form is rendered.
|
||||||
|
*/
|
||||||
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
const timeoutRef = useRef<ReturnType<typeof setTimeout>>()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
timeoutRef.current = setTimeout(() => {
|
||||||
|
setIsLoading(true)
|
||||||
|
}, 200)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (timeoutRef.current) {
|
||||||
|
clearTimeout(timeoutRef.current)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const onLoaded = () => {
|
||||||
|
if (timeoutRef.current) {
|
||||||
|
clearTimeout(timeoutRef.current)
|
||||||
|
}
|
||||||
|
setIsLoading(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="fixed inset-x-0 top-0 z-50 h-1">
|
||||||
|
<AnimatePresence>
|
||||||
|
{isLoading ? <ProgressBar duration={5} /> : null}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
<RouteFocusModal>
|
||||||
|
<RouteFocusModal.Title asChild>
|
||||||
|
<span className="sr-only">{t("products.stock.heading")}</span>
|
||||||
|
</RouteFocusModal.Title>
|
||||||
|
<RouteFocusModal.Description asChild>
|
||||||
|
<span className="sr-only">{t("products.stock.description")}</span>
|
||||||
|
</RouteFocusModal.Description>
|
||||||
|
<Suspense fallback={<ProductStockFallback />}>
|
||||||
|
<Await resolve={data.data}>
|
||||||
|
{(data: {
|
||||||
|
variants: HttpTypes.AdminProductVariant[]
|
||||||
|
locations: HttpTypes.AdminStockLocation[]
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<ProductStockForm
|
||||||
|
variants={data.variants}
|
||||||
|
locations={data.locations}
|
||||||
|
onLoaded={onLoaded}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</Await>
|
||||||
|
</Suspense>
|
||||||
|
</RouteFocusModal>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const ProductStockFallback = () => {
|
||||||
|
return (
|
||||||
|
<div className="relative flex size-full flex-col items-center justify-center divide-y">
|
||||||
|
<div className="flex size-full flex-col divide-y">
|
||||||
|
<div className="px-4 py-2">
|
||||||
|
<Skeleton className="h-7 w-7" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 overflow-auto">
|
||||||
|
<DataGridSkeleton
|
||||||
|
columns={Array.from({ length: 10 }) as ColumnDef<any>[]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="bg-ui-bg-base flex items-center justify-end gap-x-2 p-4">
|
||||||
|
<Skeleton className="h-7 w-[59px]" />
|
||||||
|
<Skeleton className="h-7 w-[46px]" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { z } from "zod"
|
||||||
|
|
||||||
|
const LocationQuantitySchema = z.object({
|
||||||
|
id: z.string().optional(),
|
||||||
|
quantity: z.union([z.number(), z.string()]),
|
||||||
|
checked: z.boolean(),
|
||||||
|
disabledToggle: z.boolean(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const ProductStockLocationsSchema = z.record(LocationQuantitySchema)
|
||||||
|
|
||||||
|
const ProductStockInventoryItemSchema = z.object({
|
||||||
|
locations: ProductStockLocationsSchema,
|
||||||
|
})
|
||||||
|
|
||||||
|
const ProductStockVariantSchema = z.object({
|
||||||
|
inventory_items: z.record(ProductStockInventoryItemSchema),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const ProductStockSchema = z.object({
|
||||||
|
variants: z.record(ProductStockVariantSchema),
|
||||||
|
})
|
||||||
|
|
||||||
|
export type ProductStockLocationSchema = z.infer<
|
||||||
|
typeof ProductStockLocationsSchema
|
||||||
|
>
|
||||||
|
export type ProductStockInventoryItemSchema = z.infer<
|
||||||
|
typeof ProductStockInventoryItemSchema
|
||||||
|
>
|
||||||
|
export type ProductStockVariantSchema = z.infer<
|
||||||
|
typeof ProductStockVariantSchema
|
||||||
|
>
|
||||||
|
export type ProductStockSchema = z.infer<typeof ProductStockSchema>
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { HttpTypes } from "@medusajs/types"
|
||||||
|
|
||||||
|
export function isProductVariant(
|
||||||
|
row:
|
||||||
|
| HttpTypes.AdminProductVariant
|
||||||
|
| HttpTypes.AdminProductVariantInventoryItemLink
|
||||||
|
): row is HttpTypes.AdminProductVariant {
|
||||||
|
return row.id.startsWith("variant_")
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isProductVariantWithInventoryPivot(
|
||||||
|
row:
|
||||||
|
| HttpTypes.AdminProductVariant
|
||||||
|
| HttpTypes.AdminProductVariantInventoryItemLink
|
||||||
|
): row is HttpTypes.AdminProductVariant & {
|
||||||
|
inventory_items: HttpTypes.AdminProductVariantInventoryItemLink[]
|
||||||
|
} {
|
||||||
|
return (row as any).inventory_items && (row as any).inventory_items.length > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDisabledInventoryRows(
|
||||||
|
variants: HttpTypes.AdminProductVariant[]
|
||||||
|
) {
|
||||||
|
const seen: Record<string, HttpTypes.AdminProductVariant> = {}
|
||||||
|
const disabled: Record<string, { id: string; title: string; sku: string }> =
|
||||||
|
{}
|
||||||
|
|
||||||
|
variants.forEach((variant) => {
|
||||||
|
const inventoryItems = variant.inventory_items
|
||||||
|
|
||||||
|
if (!inventoryItems) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
inventoryItems.forEach((item) => {
|
||||||
|
const existing = seen[item.inventory_item_id]
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
disabled[item.inventory_item_id] = {
|
||||||
|
id: existing.id,
|
||||||
|
title: existing.title || "",
|
||||||
|
sku: existing.sku || "",
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
seen[item.inventory_item_id] = variant
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
return disabled
|
||||||
|
}
|
||||||
+2
-2
@@ -5,11 +5,12 @@ import {
|
|||||||
useAnimationControls,
|
useAnimationControls,
|
||||||
useDragControls,
|
useDragControls,
|
||||||
useMotionValue,
|
useMotionValue,
|
||||||
} from "framer-motion"
|
} from "motion/react"
|
||||||
import { useEffect, useRef, useState } from "react"
|
import { useEffect, useRef, useState } from "react"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { Link } from "react-router-dom"
|
import { Link } from "react-router-dom"
|
||||||
|
|
||||||
|
import { HttpTypes } from "@medusajs/types"
|
||||||
import {
|
import {
|
||||||
STEP_ERROR_STATES,
|
STEP_ERROR_STATES,
|
||||||
STEP_INACTIVE_STATES,
|
STEP_INACTIVE_STATES,
|
||||||
@@ -17,7 +18,6 @@ import {
|
|||||||
STEP_OK_STATES,
|
STEP_OK_STATES,
|
||||||
STEP_SKIPPED_STATES,
|
STEP_SKIPPED_STATES,
|
||||||
} from "../../../constants"
|
} from "../../../constants"
|
||||||
import { HttpTypes } from "@medusajs/types"
|
|
||||||
|
|
||||||
type WorkflowExecutionTimelineSectionProps = {
|
type WorkflowExecutionTimelineSectionProps = {
|
||||||
execution: HttpTypes.AdminWorkflowExecutionResponse["workflow_execution"]
|
execution: HttpTypes.AdminWorkflowExecutionResponse["workflow_execution"]
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ export const createInventoryLevelsStep = createStep(
|
|||||||
createInventoryLevelsStepId,
|
createInventoryLevelsStepId,
|
||||||
async (data: InventoryTypes.CreateInventoryLevelInput[], { container }) => {
|
async (data: InventoryTypes.CreateInventoryLevelInput[], { container }) => {
|
||||||
const service = container.resolve<IInventoryService>(Modules.INVENTORY)
|
const service = container.resolve<IInventoryService>(Modules.INVENTORY)
|
||||||
|
|
||||||
const inventoryLevels = await service.createInventoryLevels(data)
|
const inventoryLevels = await service.createInventoryLevels(data)
|
||||||
return new StepResponse(
|
return new StepResponse(
|
||||||
inventoryLevels,
|
inventoryLevels,
|
||||||
|
|||||||
@@ -13,10 +13,7 @@ export const updateInventoryLevelsStepId = "update-inventory-levels-step"
|
|||||||
*/
|
*/
|
||||||
export const updateInventoryLevelsStep = createStep(
|
export const updateInventoryLevelsStep = createStep(
|
||||||
updateInventoryLevelsStepId,
|
updateInventoryLevelsStepId,
|
||||||
async (
|
async (input: InventoryTypes.UpdateInventoryLevelInput[], { container }) => {
|
||||||
input: InventoryTypes.BulkUpdateInventoryLevelInput[],
|
|
||||||
{ container }
|
|
||||||
) => {
|
|
||||||
const inventoryService: IInventoryService = container.resolve(
|
const inventoryService: IInventoryService = container.resolve(
|
||||||
Modules.INVENTORY
|
Modules.INVENTORY
|
||||||
)
|
)
|
||||||
@@ -54,7 +51,7 @@ export const updateInventoryLevelsStep = createStep(
|
|||||||
await inventoryService.updateInventoryLevels(
|
await inventoryService.updateInventoryLevels(
|
||||||
dataBeforeUpdate.map((data) =>
|
dataBeforeUpdate.map((data) =>
|
||||||
convertItemResponseToUpdateRequest(data, selects, relations)
|
convertItemResponseToUpdateRequest(data, selects, relations)
|
||||||
) as InventoryTypes.BulkUpdateInventoryLevelInput[]
|
) as InventoryTypes.UpdateInventoryLevelInput[]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import {
|
||||||
|
createWorkflow,
|
||||||
|
parallelize,
|
||||||
|
transform,
|
||||||
|
WorkflowData,
|
||||||
|
WorkflowResponse,
|
||||||
|
} from "@medusajs/framework/workflows-sdk"
|
||||||
|
import { BatchWorkflowInput, InventoryTypes } from "@medusajs/types"
|
||||||
|
import { createInventoryLevelsStep, updateInventoryLevelsStep } from "../steps"
|
||||||
|
import { deleteInventoryLevelsWorkflow } from "./delete-inventory-levels"
|
||||||
|
|
||||||
|
export interface BatchInventoryItemLevelsWorkflowInput
|
||||||
|
extends BatchWorkflowInput<
|
||||||
|
InventoryTypes.CreateInventoryLevelInput,
|
||||||
|
InventoryTypes.UpdateInventoryLevelInput
|
||||||
|
> {
|
||||||
|
/**
|
||||||
|
* If true, the workflow will force deletion of the inventory levels, even
|
||||||
|
* if they have a non-zero stocked quantity. It false, the workflow will
|
||||||
|
* not delete the inventory levels if they have a non-zero stocked quantity.
|
||||||
|
*
|
||||||
|
* Inventory levels that have reserved or incoming items at the location
|
||||||
|
* will not be deleted even if the force flag is set to true.
|
||||||
|
*
|
||||||
|
* @default false
|
||||||
|
*/
|
||||||
|
force?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const batchInventoryItemLevelsWorkflowId =
|
||||||
|
"batch-inventory-item-levels-workflow"
|
||||||
|
|
||||||
|
export const batchInventoryItemLevelsWorkflow = createWorkflow(
|
||||||
|
batchInventoryItemLevelsWorkflowId,
|
||||||
|
(input: WorkflowData<BatchInventoryItemLevelsWorkflowInput>) => {
|
||||||
|
const { createInput, updateInput, deleteInput } = transform(
|
||||||
|
input,
|
||||||
|
(data) => {
|
||||||
|
return {
|
||||||
|
createInput: data.create || [],
|
||||||
|
updateInput: data.update || [],
|
||||||
|
deleteInput: {
|
||||||
|
id: data.delete || [],
|
||||||
|
force: data.force ?? false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
const res = parallelize(
|
||||||
|
createInventoryLevelsStep(createInput),
|
||||||
|
updateInventoryLevelsStep(updateInput),
|
||||||
|
deleteInventoryLevelsWorkflow.runAsStep({
|
||||||
|
input: deleteInput,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
return new WorkflowResponse(
|
||||||
|
transform({ res, input }, (data) => {
|
||||||
|
return {
|
||||||
|
created: data.res[0],
|
||||||
|
updated: data.res[1],
|
||||||
|
deleted: data.input.delete,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// TODO: Remove this workflow in a future release.
|
||||||
|
|
||||||
import { InventoryLevelDTO, InventoryTypes } from "@medusajs/framework/types"
|
import { InventoryLevelDTO, InventoryTypes } from "@medusajs/framework/types"
|
||||||
import {
|
import {
|
||||||
createWorkflow,
|
createWorkflow,
|
||||||
@@ -17,6 +19,8 @@ export const bulkCreateDeleteLevelsWorkflowId =
|
|||||||
"bulk-create-delete-levels-workflow"
|
"bulk-create-delete-levels-workflow"
|
||||||
/**
|
/**
|
||||||
* This workflow creates and deletes inventory levels.
|
* This workflow creates and deletes inventory levels.
|
||||||
|
*
|
||||||
|
* @deprecated Use `batchInventoryItemLevels` instead.
|
||||||
*/
|
*/
|
||||||
export const bulkCreateDeleteLevelsWorkflow = createWorkflow(
|
export const bulkCreateDeleteLevelsWorkflow = createWorkflow(
|
||||||
bulkCreateDeleteLevelsWorkflowId,
|
bulkCreateDeleteLevelsWorkflowId,
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ import {
|
|||||||
WorkflowResponse,
|
WorkflowResponse,
|
||||||
} from "@medusajs/framework/workflows-sdk"
|
} from "@medusajs/framework/workflows-sdk"
|
||||||
|
|
||||||
import { FilterableInventoryLevelProps } from "@medusajs/framework/types"
|
import {
|
||||||
|
FilterableInventoryLevelProps,
|
||||||
|
InventoryLevelDTO,
|
||||||
|
} from "@medusajs/framework/types"
|
||||||
import { deduplicate, MedusaError, Modules } from "@medusajs/framework/utils"
|
import { deduplicate, MedusaError, Modules } from "@medusajs/framework/utils"
|
||||||
import { useRemoteQueryStep } from "../../common"
|
import { useRemoteQueryStep } from "../../common"
|
||||||
import { deleteEntitiesStep } from "../../common/steps/delete-entities"
|
import { deleteEntitiesStep } from "../../common/steps/delete-entities"
|
||||||
@@ -16,24 +19,52 @@ import { deleteEntitiesStep } from "../../common/steps/delete-entities"
|
|||||||
*/
|
*/
|
||||||
export const validateInventoryLevelsDelete = createStep(
|
export const validateInventoryLevelsDelete = createStep(
|
||||||
"validate-inventory-levels-delete",
|
"validate-inventory-levels-delete",
|
||||||
async function ({ inventoryLevels }: { inventoryLevels: any[] }) {
|
async function ({
|
||||||
const undeleteableItems = inventoryLevels.filter(
|
inventoryLevels,
|
||||||
(i) => i.reserved_quantity > 0 || i.stocked_quantity > 0
|
force,
|
||||||
|
}: {
|
||||||
|
inventoryLevels: InventoryLevelDTO[]
|
||||||
|
force?: boolean
|
||||||
|
}) {
|
||||||
|
const undeleteableDueToReservation = inventoryLevels.filter(
|
||||||
|
(i) => i.reserved_quantity > 0 || i.incoming_quantity > 0
|
||||||
)
|
)
|
||||||
|
|
||||||
if (undeleteableItems.length) {
|
if (undeleteableDueToReservation.length) {
|
||||||
const stockLocationIds = deduplicate(
|
const locationIds = deduplicate(
|
||||||
undeleteableItems.map((item) => item.location_id)
|
undeleteableDueToReservation.map((item) => item.location_id)
|
||||||
)
|
)
|
||||||
|
|
||||||
throw new MedusaError(
|
throw new MedusaError(
|
||||||
MedusaError.Types.NOT_ALLOWED,
|
MedusaError.Types.NOT_ALLOWED,
|
||||||
`Cannot remove Inventory Levels for ${stockLocationIds} because there are stocked or reserved items at the locations`
|
`Cannot remove Inventory Levels for ${locationIds.join(
|
||||||
|
", "
|
||||||
|
)} because there are reserved or incoming items at the locations`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const undeleteableDueToStock = inventoryLevels.filter(
|
||||||
|
(i) => !force && i.stocked_quantity > 0
|
||||||
|
)
|
||||||
|
|
||||||
|
if (undeleteableDueToStock.length) {
|
||||||
|
const locationIds = deduplicate(
|
||||||
|
undeleteableDueToStock.map((item) => item.location_id)
|
||||||
|
)
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.NOT_ALLOWED,
|
||||||
|
`Cannot remove Inventory Levels for ${locationIds.join(
|
||||||
|
", "
|
||||||
|
)} because there are stocked items at the locations. Use force flag to delete anyway.`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
export interface DeleteInventoryLevelsWorkflowInput
|
||||||
|
extends FilterableInventoryLevelProps {
|
||||||
|
force?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export const deleteInventoryLevelsWorkflowId =
|
export const deleteInventoryLevelsWorkflowId =
|
||||||
"delete-inventory-levels-workflow"
|
"delete-inventory-levels-workflow"
|
||||||
/**
|
/**
|
||||||
@@ -41,16 +72,25 @@ export const deleteInventoryLevelsWorkflowId =
|
|||||||
*/
|
*/
|
||||||
export const deleteInventoryLevelsWorkflow = createWorkflow(
|
export const deleteInventoryLevelsWorkflow = createWorkflow(
|
||||||
deleteInventoryLevelsWorkflowId,
|
deleteInventoryLevelsWorkflowId,
|
||||||
(input: WorkflowData<FilterableInventoryLevelProps>) => {
|
(input: WorkflowData<DeleteInventoryLevelsWorkflowInput>) => {
|
||||||
|
const { filters, force } = transform(input, (data) => {
|
||||||
|
const { force, ...filters } = data
|
||||||
|
|
||||||
|
return {
|
||||||
|
filters,
|
||||||
|
force,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const inventoryLevels = useRemoteQueryStep({
|
const inventoryLevels = useRemoteQueryStep({
|
||||||
entry_point: "inventory_levels",
|
entry_point: "inventory_levels",
|
||||||
fields: ["id", "stocked_quantity", "reserved_quantity", "location_id"],
|
fields: ["id", "stocked_quantity", "reserved_quantity", "location_id"],
|
||||||
variables: {
|
variables: {
|
||||||
filters: input,
|
filters: filters,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
validateInventoryLevelsDelete({ inventoryLevels })
|
validateInventoryLevelsDelete({ inventoryLevels, force })
|
||||||
|
|
||||||
const idsToDelete = transform({ inventoryLevels }, ({ inventoryLevels }) =>
|
const idsToDelete = transform({ inventoryLevels }, ({ inventoryLevels }) =>
|
||||||
inventoryLevels.map((il) => il.id)
|
inventoryLevels.map((il) => il.id)
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
export * from "./delete-inventory-items"
|
export * from "./batch-inventory-item-levels"
|
||||||
|
export * from "./bulk-create-delete-levels"
|
||||||
export * from "./create-inventory-items"
|
export * from "./create-inventory-items"
|
||||||
export * from "./create-inventory-levels"
|
export * from "./create-inventory-levels"
|
||||||
export * from "./update-inventory-items"
|
export * from "./delete-inventory-items"
|
||||||
export * from "./delete-inventory-levels"
|
export * from "./delete-inventory-levels"
|
||||||
|
export * from "./update-inventory-items"
|
||||||
export * from "./update-inventory-levels"
|
export * from "./update-inventory-levels"
|
||||||
export * from "./bulk-create-delete-levels"
|
|
||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
import { updateInventoryLevelsStep } from "../steps/update-inventory-levels"
|
import { updateInventoryLevelsStep } from "../steps/update-inventory-levels"
|
||||||
|
|
||||||
export interface UpdateInventoryLevelsWorkflowInput {
|
export interface UpdateInventoryLevelsWorkflowInput {
|
||||||
updates: InventoryTypes.BulkUpdateInventoryLevelInput[]
|
updates: InventoryTypes.UpdateInventoryLevelInput[]
|
||||||
}
|
}
|
||||||
export const updateInventoryLevelsWorkflowId =
|
export const updateInventoryLevelsWorkflowId =
|
||||||
"update-inventory-levels-workflow"
|
"update-inventory-levels-workflow"
|
||||||
|
|||||||
@@ -355,6 +355,8 @@ export class InventoryItem {
|
|||||||
* [Manage Inventory Levels](https://docs.medusajs.com/api/admin#inventory-items_postinventoryitemsidlocationlevelsbatch)
|
* [Manage Inventory Levels](https://docs.medusajs.com/api/admin#inventory-items_postinventoryitemsidlocationlevelsbatch)
|
||||||
* API route.
|
* API route.
|
||||||
*
|
*
|
||||||
|
* @deprecated Use `batchInventoryItemLocationLevels` instead.
|
||||||
|
*
|
||||||
* @param id - The inventory item's ID.
|
* @param id - The inventory item's ID.
|
||||||
* @param body - The inventory levels to create or delete.
|
* @param body - The inventory levels to create or delete.
|
||||||
* @param query - Configure the fields to retrieve in the inventory item.
|
* @param query - Configure the fields to retrieve in the inventory item.
|
||||||
@@ -367,19 +369,19 @@ export class InventoryItem {
|
|||||||
* location_id: "sloc_123",
|
* location_id: "sloc_123",
|
||||||
* stocked_quantity: 10
|
* stocked_quantity: 10
|
||||||
* }],
|
* }],
|
||||||
* delete: ["sloc_123"]
|
* delete: ["ilvl_123"]
|
||||||
* })
|
* })
|
||||||
* .then(({ inventory_item }) => {
|
* .then(({ created, updated, deleted }) => {
|
||||||
* console.log(inventory_item)
|
* console.log(created, updated, deleted)
|
||||||
* })
|
* })
|
||||||
*/
|
*/
|
||||||
async batchUpdateLevels(
|
async batchUpdateLevels(
|
||||||
id: string,
|
id: string,
|
||||||
body: HttpTypes.AdminBatchUpdateInventoryLevelLocation,
|
body: HttpTypes.AdminBatchInventoryItemLocationLevels,
|
||||||
query?: SelectParams,
|
query?: SelectParams,
|
||||||
headers?: ClientHeaders
|
headers?: ClientHeaders
|
||||||
) {
|
) {
|
||||||
return await this.client.fetch<HttpTypes.AdminInventoryItemResponse>(
|
return await this.client.fetch<HttpTypes.AdminBatchInventoryItemLocationLevelsResponse>(
|
||||||
`/admin/inventory-items/${id}/location-levels/batch`,
|
`/admin/inventory-items/${id}/location-levels/batch`,
|
||||||
{
|
{
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -389,4 +391,75 @@ export class InventoryItem {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method manages the inventory levels of an inventory item. It sends a request to the
|
||||||
|
* [Manage Inventory Levels](https://docs.medusajs.com/api/admin#inventory-items_postinventoryitemsidlocationlevelsbatch)
|
||||||
|
* API route.
|
||||||
|
*
|
||||||
|
* @param id - The inventory item's ID.
|
||||||
|
* @param body - The inventory levels to create, update or delete, and an optional `force` flag.
|
||||||
|
* @param headers - Headers to pass in the request
|
||||||
|
* @returns The inventory item's details.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* sdk.admin.inventoryItem.batchInventoryItemLocationLevels("iitem_123", {
|
||||||
|
* create: [{
|
||||||
|
* location_id: "sloc_123",
|
||||||
|
* stocked_quantity: 10
|
||||||
|
* }],
|
||||||
|
* delete: ["ilvl_123"]
|
||||||
|
* })
|
||||||
|
* .then(({ created, updated, deleted }) => {
|
||||||
|
* console.log(created, updated, deleted)
|
||||||
|
* })
|
||||||
|
*/
|
||||||
|
async batchInventoryItemLocationLevels(
|
||||||
|
id: string,
|
||||||
|
body: HttpTypes.AdminBatchInventoryItemLocationLevels,
|
||||||
|
headers?: ClientHeaders
|
||||||
|
) {
|
||||||
|
return await this.client.fetch<HttpTypes.AdminBatchInventoryItemLocationLevelsResponse>(
|
||||||
|
`/admin/inventory-items/${id}/location-levels/batch`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers,
|
||||||
|
body,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method manages the inventory levels of multiple inventory items.
|
||||||
|
*
|
||||||
|
* @param body - The inventory levels to create, update or delete, and an optional `force` flag.
|
||||||
|
* @param headers - Headers to pass in the request
|
||||||
|
* @returns The inventory item's details.
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* sdk.admin.inventoryItem.batchInventoryItemsLocationLevels({
|
||||||
|
* create: [{
|
||||||
|
* inventory_item_id: "iitem_123",
|
||||||
|
* location_id: "sloc_123",
|
||||||
|
* stocked_quantity: 10
|
||||||
|
* }],
|
||||||
|
* delete: ["ilvl_123"]
|
||||||
|
* })
|
||||||
|
* .then(({ created, updated, deleted }) => {
|
||||||
|
* console.log(created, updated, deleted)
|
||||||
|
* })
|
||||||
|
*/
|
||||||
|
async batchInventoryItemsLocationLevels(
|
||||||
|
body: HttpTypes.AdminBatchInventoryItemsLocationLevels,
|
||||||
|
headers?: ClientHeaders
|
||||||
|
) {
|
||||||
|
return await this.client.fetch<HttpTypes.AdminBatchInventoryItemsLocationLevelsResponse>(
|
||||||
|
`/admin/inventory-items/location-levels/batch`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers,
|
||||||
|
body,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -11,7 +11,7 @@ export interface AdminUpdateInventoryLevel {
|
|||||||
incoming_quantity?: number
|
incoming_quantity?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AdminCreateInventoryLevel {
|
export interface AdminBatchCreateInventoryItemLocationLevels {
|
||||||
/**
|
/**
|
||||||
* The ID of the associated stock location.
|
* The ID of the associated stock location.
|
||||||
*/
|
*/
|
||||||
@@ -28,7 +28,40 @@ export interface AdminCreateInventoryLevel {
|
|||||||
incoming_quantity?: number
|
incoming_quantity?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AdminBatchUpdateInventoryLevelLocation {
|
export interface AdminBatchCreateInventoryItemLocationLevels {
|
||||||
|
/**
|
||||||
|
* The ID of the associated stock location.
|
||||||
|
*/
|
||||||
|
location_id: string
|
||||||
|
/**
|
||||||
|
* The associated inventory item's stocked quantity in the
|
||||||
|
* associated stock location.
|
||||||
|
*/
|
||||||
|
stocked_quantity?: number
|
||||||
|
/**
|
||||||
|
* The associated inventory item's incoming quantity in the
|
||||||
|
* associated stock location.
|
||||||
|
*/
|
||||||
|
incoming_quantity?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminBatchUpdateInventoryItemLocationLevels
|
||||||
|
extends AdminBatchCreateInventoryItemLocationLevels {
|
||||||
|
/**
|
||||||
|
* The ID of the inventory level to update.
|
||||||
|
*/
|
||||||
|
id?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminBatchInventoryItemLocationLevels {
|
||||||
|
/**
|
||||||
|
* A list of inventory levels to update.
|
||||||
|
*/
|
||||||
|
update?: AdminBatchUpdateInventoryItemLocationLevels[]
|
||||||
|
/**
|
||||||
|
* A list of inventory levels to create.
|
||||||
|
*/
|
||||||
|
create?: AdminBatchCreateInventoryItemLocationLevels[]
|
||||||
/**
|
/**
|
||||||
* A list of location IDs to
|
* A list of location IDs to
|
||||||
* delete their associated inventory
|
* delete their associated inventory
|
||||||
@@ -36,11 +69,44 @@ export interface AdminBatchUpdateInventoryLevelLocation {
|
|||||||
*/
|
*/
|
||||||
delete?: string[]
|
delete?: string[]
|
||||||
/**
|
/**
|
||||||
* @ignore
|
* Whether to force the deletion of the inventory levels,
|
||||||
|
* even if the the location has stocked quantity.
|
||||||
*/
|
*/
|
||||||
update?: never // TODO - not implemented // AdminUpdateInventoryLevel[]
|
force?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminBatchCreateInventoryItemsLocationLevels {
|
||||||
/**
|
/**
|
||||||
* A list of inventory levels to create.
|
* The ID of the associated stock location.
|
||||||
*/
|
*/
|
||||||
create?: AdminCreateInventoryLevel[]
|
location_id: string
|
||||||
|
/**
|
||||||
|
* The ID of the associated inventory item.
|
||||||
|
*/
|
||||||
|
inventory_item_id: string
|
||||||
|
/**
|
||||||
|
* The associated inventory item's stocked quantity in the
|
||||||
|
* associated stock location.
|
||||||
|
*/
|
||||||
|
stocked_quantity?: number
|
||||||
|
/**
|
||||||
|
* The associated inventory item's incoming quantity in the
|
||||||
|
* associated stock location.
|
||||||
|
*/
|
||||||
|
incoming_quantity?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminBatchUpdateInventoryItemsLocationLevels
|
||||||
|
extends AdminBatchCreateInventoryItemsLocationLevels {
|
||||||
|
/**
|
||||||
|
* The ID of the inventory level to update.
|
||||||
|
*/
|
||||||
|
id?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminBatchInventoryItemsLocationLevels {
|
||||||
|
create: AdminBatchCreateInventoryItemsLocationLevels[]
|
||||||
|
update: AdminBatchUpdateInventoryItemsLocationLevels[]
|
||||||
|
delete: string[]
|
||||||
|
force?: boolean
|
||||||
}
|
}
|
||||||
@@ -14,3 +14,21 @@ export type AdminInventoryLevelListResponse = PaginatedResponse<{
|
|||||||
*/
|
*/
|
||||||
inventory_levels: InventoryLevel[]
|
inventory_levels: InventoryLevel[]
|
||||||
}>
|
}>
|
||||||
|
|
||||||
|
export interface AdminBatchInventoryItemLocationLevelsResponse {
|
||||||
|
/**
|
||||||
|
* The created inventory levels.
|
||||||
|
*/
|
||||||
|
created?: InventoryLevel[]
|
||||||
|
/**
|
||||||
|
* The updated inventory levels.
|
||||||
|
*/
|
||||||
|
updated?: InventoryLevel[]
|
||||||
|
/**
|
||||||
|
* The IDs of the deleted inventory levels.
|
||||||
|
*/
|
||||||
|
deleted?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminBatchInventoryItemsLocationLevelsResponse
|
||||||
|
extends AdminBatchInventoryItemLocationLevelsResponse {}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { AdminCollection } from "../../collection"
|
import { AdminCollection } from "../../collection"
|
||||||
|
import { AdminInventoryItem } from "../../inventory"
|
||||||
import { AdminPrice } from "../../pricing"
|
import { AdminPrice } from "../../pricing"
|
||||||
import { AdminProductCategory } from "../../product-category"
|
import { AdminProductCategory } from "../../product-category"
|
||||||
import { AdminProductTag } from "../../product-tag"
|
import { AdminProductTag } from "../../product-tag"
|
||||||
@@ -13,6 +14,29 @@ import {
|
|||||||
ProductStatus,
|
ProductStatus,
|
||||||
} from "../common"
|
} from "../common"
|
||||||
|
|
||||||
|
export interface AdminProductVariantInventoryItemLink {
|
||||||
|
/**
|
||||||
|
* The ID of the pivot record.
|
||||||
|
*/
|
||||||
|
id: string
|
||||||
|
/**
|
||||||
|
* The ID of the variant.
|
||||||
|
*/
|
||||||
|
variant_id: string
|
||||||
|
/**
|
||||||
|
* The variant that the inventory item is linked to.
|
||||||
|
*/
|
||||||
|
variant?: AdminProductVariant
|
||||||
|
/**
|
||||||
|
* The ID of the inventory item.
|
||||||
|
*/
|
||||||
|
inventory_item_id: string
|
||||||
|
/**
|
||||||
|
* The inventory item that is linked to the variant.
|
||||||
|
*/
|
||||||
|
inventory?: AdminInventoryItem
|
||||||
|
}
|
||||||
|
|
||||||
export interface AdminProductVariant extends BaseProductVariant {
|
export interface AdminProductVariant extends BaseProductVariant {
|
||||||
/**
|
/**
|
||||||
* The product variant's prices.
|
* The product variant's prices.
|
||||||
@@ -26,6 +50,10 @@ export interface AdminProductVariant extends BaseProductVariant {
|
|||||||
* The product that this variant belongs to.
|
* The product that this variant belongs to.
|
||||||
*/
|
*/
|
||||||
product?: AdminProduct | null
|
product?: AdminProduct | null
|
||||||
|
/**
|
||||||
|
* The variant's inventory items.
|
||||||
|
*/
|
||||||
|
inventory_items?: AdminProductVariantInventoryItemLink[] | null
|
||||||
}
|
}
|
||||||
export interface AdminProductOption extends BaseProductOption {
|
export interface AdminProductOption extends BaseProductOption {
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -26,9 +26,17 @@ export interface CreateInventoryLevelInput {
|
|||||||
*/
|
*/
|
||||||
export interface UpdateInventoryLevelInput {
|
export interface UpdateInventoryLevelInput {
|
||||||
/**
|
/**
|
||||||
* id of the inventory level to update
|
* ID of the inventory level to update
|
||||||
*/
|
*/
|
||||||
id?: string
|
id?: string
|
||||||
|
/**
|
||||||
|
* The ID of the associated inventory item.
|
||||||
|
*/
|
||||||
|
inventory_item_id: string
|
||||||
|
/**
|
||||||
|
* The ID of the associated location.
|
||||||
|
*/
|
||||||
|
location_id: string
|
||||||
/**
|
/**
|
||||||
* The stocked quantity of the associated inventory item in the associated location.
|
* The stocked quantity of the associated inventory item in the associated location.
|
||||||
*/
|
*/
|
||||||
@@ -39,22 +47,6 @@ export interface UpdateInventoryLevelInput {
|
|||||||
incoming_quantity?: number
|
incoming_quantity?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @interface
|
|
||||||
*
|
|
||||||
* The attributes to update in an inventory level. The inventory level is identified by the IDs of its associated inventory item and location.
|
|
||||||
*/
|
|
||||||
export type BulkUpdateInventoryLevelInput = {
|
|
||||||
/**
|
|
||||||
* The ID of the associated inventory level.
|
|
||||||
*/
|
|
||||||
inventory_item_id: string
|
|
||||||
/**
|
|
||||||
* The ID of the associated location.
|
|
||||||
*/
|
|
||||||
location_id: string
|
|
||||||
} & UpdateInventoryLevelInput
|
|
||||||
|
|
||||||
export type BulkAdjustInventoryLevelInput = {
|
export type BulkAdjustInventoryLevelInput = {
|
||||||
/**
|
/**
|
||||||
* The ID of the associated inventory level.
|
* The ID of the associated inventory level.
|
||||||
@@ -69,4 +61,4 @@ export type BulkAdjustInventoryLevelInput = {
|
|||||||
* The quantity to adjust the inventory level by.
|
* The quantity to adjust the inventory level by.
|
||||||
*/
|
*/
|
||||||
adjustment: BigNumberInput
|
adjustment: BigNumberInput
|
||||||
} & UpdateInventoryLevelInput
|
}
|
||||||
@@ -13,11 +13,11 @@ import {
|
|||||||
ReservationItemDTO,
|
ReservationItemDTO,
|
||||||
} from "./common"
|
} from "./common"
|
||||||
import {
|
import {
|
||||||
BulkUpdateInventoryLevelInput,
|
|
||||||
CreateInventoryItemInput,
|
CreateInventoryItemInput,
|
||||||
CreateInventoryLevelInput,
|
CreateInventoryLevelInput,
|
||||||
CreateReservationItemInput,
|
CreateReservationItemInput,
|
||||||
UpdateInventoryItemInput,
|
UpdateInventoryItemInput,
|
||||||
|
UpdateInventoryLevelInput,
|
||||||
UpdateReservationItemInput,
|
UpdateReservationItemInput,
|
||||||
} from "./mutations"
|
} from "./mutations"
|
||||||
|
|
||||||
@@ -610,7 +610,7 @@ export interface IInventoryService extends IModuleService {
|
|||||||
/**
|
/**
|
||||||
* This method updates existing inventory levels.
|
* This method updates existing inventory levels.
|
||||||
*
|
*
|
||||||
* @param {BulkUpdateInventoryLevelInput[]} updates - The list of The attributes to update in an inventory level. The inventory level is identified by the IDs of its associated inventory item and location.
|
* @param {UpdateInventoryLevelInput[]} updates - The list of The attributes to update in an inventory level. The inventory level is identified by the IDs of its associated inventory item and location.
|
||||||
* @param {Context} context - A context used to share resources, such as transaction manager, between the application and the module.
|
* @param {Context} context - A context used to share resources, such as transaction manager, between the application and the module.
|
||||||
* @returns {Promise<InventoryLevelDTO[]>} The updated inventory levels.
|
* @returns {Promise<InventoryLevelDTO[]>} The updated inventory levels.
|
||||||
*
|
*
|
||||||
@@ -626,14 +626,14 @@ export interface IInventoryService extends IModuleService {
|
|||||||
* ])
|
* ])
|
||||||
*/
|
*/
|
||||||
updateInventoryLevels(
|
updateInventoryLevels(
|
||||||
updates: BulkUpdateInventoryLevelInput[],
|
updates: UpdateInventoryLevelInput[],
|
||||||
context?: Context
|
context?: Context
|
||||||
): Promise<InventoryLevelDTO[]>
|
): Promise<InventoryLevelDTO[]>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This method updates an existing inventory level.
|
* This method updates an existing inventory level.
|
||||||
*
|
*
|
||||||
* @param {BulkUpdateInventoryLevelInput} updates - The attributes to update in an inventory level. The inventory level is identified by the IDs of its associated inventory item and location.
|
* @param {UpdateInventoryLevelInput} updates - The attributes to update in an inventory level. The inventory level is identified by the IDs of its associated inventory item and location.
|
||||||
* @param {Context} context - A context used to share resources, such as transaction manager, between the application and the module.
|
* @param {Context} context - A context used to share resources, such as transaction manager, between the application and the module.
|
||||||
* @returns {Promise<InventoryLevelDTO>} The updated inventory level.
|
* @returns {Promise<InventoryLevelDTO>} The updated inventory level.
|
||||||
*
|
*
|
||||||
@@ -646,7 +646,7 @@ export interface IInventoryService extends IModuleService {
|
|||||||
* })
|
* })
|
||||||
*/
|
*/
|
||||||
updateInventoryLevels(
|
updateInventoryLevels(
|
||||||
updates: BulkUpdateInventoryLevelInput,
|
updates: UpdateInventoryLevelInput,
|
||||||
context?: Context
|
context?: Context
|
||||||
): Promise<InventoryLevelDTO>
|
): Promise<InventoryLevelDTO>
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,16 @@ const Checkbox = React.forwardRef<
|
|||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="text-ui-fg-on-inverted bg-ui-bg-base shadow-borders-base group-hover:bg-ui-bg-base-hover group-focus-visible:!shadow-borders-interactive-with-focus group-data-[state=checked]:bg-ui-bg-interactive group-data-[state=checked]:shadow-borders-interactive-with-shadow group-data-[state=indeterminate]:bg-ui-bg-interactive group-data-[state=indeterminate]:shadow-borders-interactive-with-shadow [&_path]:shadow-details-contrast-on-bg-interactive group-disabled:text-ui-fg-disabled group-disabled:!bg-ui-bg-disabled group-disabled:!shadow-borders-base transition-fg h-[14px] w-[14px] rounded-[3px]">
|
<div
|
||||||
|
className={clx(
|
||||||
|
"text-ui-fg-on-inverted bg-ui-bg-base shadow-borders-base [&_path]:shadow-details-contrast-on-bg-interactive transition-fg h-[14px] w-[14px] rounded-[3px]",
|
||||||
|
"group-disabled:cursor-not-allowed group-disabled:opacity-50",
|
||||||
|
"group-focus-visible:!shadow-borders-interactive-with-focus",
|
||||||
|
"group-hover:group-enabled:group-data-[state=unchecked]:bg-ui-bg-base-hover",
|
||||||
|
"group-data-[state=checked]:bg-ui-bg-interactive group-data-[state=checked]:shadow-borders-interactive-with-shadow",
|
||||||
|
"group-data-[state=indeterminate]:bg-ui-bg-interactive group-data-[state=indeterminate]:shadow-borders-interactive-with-shadow"
|
||||||
|
)}
|
||||||
|
>
|
||||||
<Primitives.Indicator className="absolute inset-0 flex items-center justify-center">
|
<Primitives.Indicator className="absolute inset-0 flex items-center justify-center">
|
||||||
{checked === "indeterminate" ? <MinusMini /> : <CheckMini />}
|
{checked === "indeterminate" ? <MinusMini /> : <CheckMini />}
|
||||||
</Primitives.Indicator>
|
</Primitives.Indicator>
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ const Indicator = React.forwardRef<
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={clx(
|
className={clx(
|
||||||
"bg-ui-bg-base shadow-details-contrast-on-bg-interactive group-disabled:bg-ui-fg-disabled h-1.5 w-1.5 rounded-full group-disabled:shadow-none"
|
"bg-ui-bg-base shadow-details-contrast-on-bg-interactive h-1.5 w-1.5 rounded-full"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</Primitives.Indicator>
|
</Primitives.Indicator>
|
||||||
@@ -60,10 +60,10 @@ const Item = React.forwardRef<
|
|||||||
<div
|
<div
|
||||||
className={clx(
|
className={clx(
|
||||||
"shadow-borders-base bg-ui-bg-base transition-fg flex h-[14px] w-[14px] items-center justify-center rounded-full",
|
"shadow-borders-base bg-ui-bg-base transition-fg flex h-[14px] w-[14px] items-center justify-center rounded-full",
|
||||||
"group-hover:bg-ui-bg-base-hover",
|
"group-hover:group-enabled:group-data-[state=unchecked]:bg-ui-bg-base-hover",
|
||||||
"group-data-[state=checked]:bg-ui-bg-interactive group-data-[state=checked]:shadow-borders-interactive-with-shadow",
|
"group-data-[state=checked]:bg-ui-bg-interactive group-data-[state=checked]:shadow-borders-interactive-with-shadow",
|
||||||
"group-focus-visible:!shadow-borders-interactive-with-focus",
|
"group-focus-visible:!shadow-borders-interactive-with-focus",
|
||||||
"group-disabled:!bg-ui-bg-disabled group-disabled:!shadow-borders-base"
|
"group-disabled:cursor-not-allowed group-disabled:opacity-50"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Indicator />
|
<Indicator />
|
||||||
@@ -95,7 +95,10 @@ const ChoiceBox = React.forwardRef<
|
|||||||
<Primitives.Item
|
<Primitives.Item
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={clx(
|
className={clx(
|
||||||
"shadow-borders-base bg-ui-bg-base focus-visible:shadow-borders-interactive-with-focus outline-none transition-fg disabled:bg-ui-bg-disabled group flex items-start gap-x-2 rounded-lg p-3 disabled:cursor-not-allowed data-[state=checked]:shadow-borders-interactive-with-shadow",
|
"shadow-borders-base bg-ui-bg-base focus-visible:shadow-borders-interactive-with-focus transition-fg group flex items-start gap-x-2 rounded-lg p-3 outline-none",
|
||||||
|
"hover:enabled:bg-ui-bg-base-hover",
|
||||||
|
"data-[state=checked]:shadow-borders-interactive-with-shadow",
|
||||||
|
"group-disabled:cursor-not-allowed group-disabled:opacity-50",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -103,7 +106,12 @@ const ChoiceBox = React.forwardRef<
|
|||||||
aria-describedby={descriptionId}
|
aria-describedby={descriptionId}
|
||||||
>
|
>
|
||||||
<div className="flex h-5 w-5 items-center justify-center">
|
<div className="flex h-5 w-5 items-center justify-center">
|
||||||
<div className="shadow-borders-base bg-ui-bg-base group-data-[state=checked]:bg-ui-bg-interactive group-data-[state=checked]:shadow-borders-interactive-with-shadow transition-fg group-disabled:!bg-ui-bg-disabled group-hover:bg-ui-bg-base-hover group-disabled:!shadow-borders-base flex h-3.5 w-3.5 items-center justify-center rounded-full">
|
<div
|
||||||
|
className={clx(
|
||||||
|
"shadow-borders-base bg-ui-bg-base group-data-[state=checked]:bg-ui-bg-interactive group-data-[state=checked]:shadow-borders-interactive-with-shadow transition-fg flex h-3.5 w-3.5 items-center justify-center rounded-full",
|
||||||
|
"group-hover:group-enabled:group-data-[state=unchecked]:bg-ui-bg-base-hover"
|
||||||
|
)}
|
||||||
|
>
|
||||||
<Indicator />
|
<Indicator />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -117,7 +125,7 @@ const ChoiceBox = React.forwardRef<
|
|||||||
{label}
|
{label}
|
||||||
</Label>
|
</Label>
|
||||||
<Hint
|
<Hint
|
||||||
className="txt-compact-medium text-ui-fg-subtle group-disabled:text-ui-fg-disabled text-left"
|
className="txt-small text-ui-fg-subtle group-disabled:text-ui-fg-disabled text-left"
|
||||||
id={descriptionId}
|
id={descriptionId}
|
||||||
>
|
>
|
||||||
{description}
|
{description}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import * as React from "react"
|
|||||||
import { clx } from "@/utils/clx"
|
import { clx } from "@/utils/clx"
|
||||||
|
|
||||||
const switchVariants = cva({
|
const switchVariants = cva({
|
||||||
base: "bg-ui-bg-switch-off hover:bg-ui-bg-switch-off-hover data-[state=unchecked]:hover:after:bg-switch-off-hover-gradient before:shadow-details-switch-background focus-visible:shadow-details-switch-background-focus data-[state=checked]:bg-ui-bg-interactive disabled:!bg-ui-bg-disabled group relative inline-flex items-center rounded-full outline-none transition-all before:absolute before:inset-0 before:rounded-full before:content-[''] after:absolute after:inset-0 after:rounded-full after:content-[''] disabled:cursor-not-allowed",
|
base: "bg-ui-bg-switch-off hover:bg-ui-bg-switch-off-hover data-[state=unchecked]:hover:after:bg-switch-off-hover-gradient before:shadow-details-switch-background focus-visible:shadow-details-switch-background-focus data-[state=checked]:bg-ui-bg-interactive disabled:opacity-50 group relative inline-flex items-center rounded-full outline-none transition-all before:absolute before:inset-0 before:rounded-full before:content-[''] after:absolute after:inset-0 after:rounded-full after:content-[''] disabled:cursor-not-allowed",
|
||||||
variants: {
|
variants: {
|
||||||
size: {
|
size: {
|
||||||
small: "h-[16px] w-[28px]",
|
small: "h-[16px] w-[28px]",
|
||||||
@@ -20,7 +20,7 @@ const switchVariants = cva({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const thumbVariants = cva({
|
const thumbVariants = cva({
|
||||||
base: "bg-ui-fg-on-color shadow-details-switch-handle group-disabled:bg-ui-fg-disabled pointer-events-none h-[14px] w-[14px] rounded-full transition-all group-disabled:shadow-none",
|
base: "bg-ui-fg-on-color shadow-details-switch-handle pointer-events-none h-[14px] w-[14px] rounded-full transition-all",
|
||||||
variants: {
|
variants: {
|
||||||
size: {
|
size: {
|
||||||
small:
|
small:
|
||||||
|
|||||||
@@ -1,39 +1,35 @@
|
|||||||
import {
|
|
||||||
AdminCreateInventoryLocationLevelType,
|
|
||||||
AdminUpdateInventoryLocationLevelType,
|
|
||||||
} from "../../../validators"
|
|
||||||
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
|
import { MedusaRequest, MedusaResponse } from "@medusajs/framework/http"
|
||||||
|
import { AdminBatchInventoryItemLocationsLevelType } from "../../../validators"
|
||||||
|
|
||||||
import { bulkCreateDeleteLevelsWorkflow } from "@medusajs/core-flows"
|
import { batchInventoryItemLevelsWorkflow } from "@medusajs/core-flows"
|
||||||
import { BatchMethodRequest } from "@medusajs/framework/types"
|
|
||||||
|
|
||||||
export const POST = async (
|
export const POST = async (
|
||||||
req: MedusaRequest<
|
req: MedusaRequest<AdminBatchInventoryItemLocationsLevelType>,
|
||||||
BatchMethodRequest<
|
res: MedusaResponse
|
||||||
AdminCreateInventoryLocationLevelType,
|
|
||||||
AdminUpdateInventoryLocationLevelType
|
|
||||||
>
|
|
||||||
>,
|
|
||||||
res: MedusaResponse<{ inventory_item: {} }>
|
|
||||||
) => {
|
) => {
|
||||||
const { id } = req.params
|
const { id } = req.params
|
||||||
|
|
||||||
// TODO: Normalize workflow and response, and add support for updates
|
const workflow = batchInventoryItemLevelsWorkflow(req.scope)
|
||||||
const workflow = bulkCreateDeleteLevelsWorkflow(req.scope)
|
const output = await workflow.run({
|
||||||
await workflow.run({
|
|
||||||
input: {
|
input: {
|
||||||
deletes:
|
delete: req.validatedBody.delete ?? [],
|
||||||
req.validatedBody.delete?.map((location_id) => ({
|
create:
|
||||||
location_id,
|
|
||||||
inventory_item_id: id,
|
|
||||||
})) ?? [],
|
|
||||||
creates:
|
|
||||||
req.validatedBody.create?.map((c) => ({
|
req.validatedBody.create?.map((c) => ({
|
||||||
...c,
|
...c,
|
||||||
inventory_item_id: id,
|
inventory_item_id: id,
|
||||||
})) ?? [],
|
})) ?? [],
|
||||||
|
update:
|
||||||
|
req.validatedBody.update?.map((u) => ({
|
||||||
|
...u,
|
||||||
|
inventory_item_id: id,
|
||||||
|
})) ?? [],
|
||||||
|
force: req.validatedBody.force ?? false,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
res.status(200).json({ inventory_item: {} })
|
res.status(200).json({
|
||||||
|
created: output.result.created,
|
||||||
|
updated: output.result.updated,
|
||||||
|
deleted: output.result.deleted,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { batchInventoryItemLevelsWorkflow } from "@medusajs/core-flows"
|
||||||
|
import { MedusaRequest, MedusaResponse } from "@medusajs/framework"
|
||||||
|
import { AdminBatchInventoryItemLevelsType } from "../../validators"
|
||||||
|
|
||||||
|
export const POST = async (
|
||||||
|
req: MedusaRequest<AdminBatchInventoryItemLevelsType>,
|
||||||
|
res: MedusaResponse
|
||||||
|
) => {
|
||||||
|
const body = req.validatedBody
|
||||||
|
|
||||||
|
const output = await batchInventoryItemLevelsWorkflow(req.scope).run({
|
||||||
|
input: body,
|
||||||
|
})
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
created: output.result.created,
|
||||||
|
updated: output.result.updated,
|
||||||
|
deleted: output.result.deleted,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -4,9 +4,10 @@ import {
|
|||||||
} from "@medusajs/framework"
|
} from "@medusajs/framework"
|
||||||
import { MiddlewareRoute, unlessPath } from "@medusajs/framework/http"
|
import { MiddlewareRoute, unlessPath } from "@medusajs/framework/http"
|
||||||
import { DEFAULT_BATCH_ENDPOINTS_SIZE_LIMIT } from "../../../utils/middlewares"
|
import { DEFAULT_BATCH_ENDPOINTS_SIZE_LIMIT } from "../../../utils/middlewares"
|
||||||
import { createBatchBody } from "../../utils/validators"
|
|
||||||
import * as QueryConfig from "./query-config"
|
import * as QueryConfig from "./query-config"
|
||||||
import {
|
import {
|
||||||
|
AdminBatchInventoryItemLevels,
|
||||||
|
AdminBatchInventoryItemLocationsLevel,
|
||||||
AdminCreateInventoryItem,
|
AdminCreateInventoryItem,
|
||||||
AdminCreateInventoryLocationLevel,
|
AdminCreateInventoryLocationLevel,
|
||||||
AdminGetInventoryItemParams,
|
AdminGetInventoryItemParams,
|
||||||
@@ -49,6 +50,22 @@ export const adminInventoryRoutesMiddlewares: MiddlewareRoute[] = [
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
method: ["POST"],
|
||||||
|
matcher: "/admin/inventory-items/batch",
|
||||||
|
bodyParser: {
|
||||||
|
sizeLimit: DEFAULT_BATCH_ENDPOINTS_SIZE_LIMIT,
|
||||||
|
},
|
||||||
|
middlewares: [validateAndTransformBody(AdminBatchInventoryItemLevels)],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
method: ["POST"],
|
||||||
|
matcher: "/admin/inventory-items/location-levels/batch",
|
||||||
|
bodyParser: {
|
||||||
|
sizeLimit: DEFAULT_BATCH_ENDPOINTS_SIZE_LIMIT,
|
||||||
|
},
|
||||||
|
middlewares: [validateAndTransformBody(AdminBatchInventoryItemLevels)],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
method: ["POST"],
|
method: ["POST"],
|
||||||
matcher: "/admin/inventory-items/:id",
|
matcher: "/admin/inventory-items/:id",
|
||||||
@@ -88,12 +105,7 @@ export const adminInventoryRoutesMiddlewares: MiddlewareRoute[] = [
|
|||||||
sizeLimit: DEFAULT_BATCH_ENDPOINTS_SIZE_LIMIT,
|
sizeLimit: DEFAULT_BATCH_ENDPOINTS_SIZE_LIMIT,
|
||||||
},
|
},
|
||||||
middlewares: [
|
middlewares: [
|
||||||
validateAndTransformBody(
|
validateAndTransformBody(AdminBatchInventoryItemLocationsLevel),
|
||||||
createBatchBody(
|
|
||||||
AdminCreateInventoryLocationLevel,
|
|
||||||
AdminUpdateInventoryLocationLevel
|
|
||||||
)
|
|
||||||
),
|
|
||||||
validateAndTransformQuery(
|
validateAndTransformQuery(
|
||||||
AdminGetInventoryLocationLevelParams,
|
AdminGetInventoryLocationLevelParams,
|
||||||
QueryConfig.retrieveLocationLevelsTransformQueryConfig
|
QueryConfig.retrieveLocationLevelsTransformQueryConfig
|
||||||
|
|||||||
@@ -75,6 +75,30 @@ export const AdminCreateInventoryLocationLevel = z
|
|||||||
})
|
})
|
||||||
.strict()
|
.strict()
|
||||||
|
|
||||||
|
export type AdminUpdateInventoryLocationLevelBatchType = z.infer<
|
||||||
|
typeof AdminUpdateInventoryLocationLevelBatch
|
||||||
|
>
|
||||||
|
|
||||||
|
export const AdminUpdateInventoryLocationLevelBatch = z
|
||||||
|
.object({
|
||||||
|
id: z.string().optional(),
|
||||||
|
location_id: z.string(),
|
||||||
|
stocked_quantity: z.number().min(0).optional(),
|
||||||
|
incoming_quantity: z.number().min(0).optional(),
|
||||||
|
})
|
||||||
|
.strict()
|
||||||
|
|
||||||
|
export type AdminBatchInventoryItemLocationsLevelType = z.infer<
|
||||||
|
typeof AdminBatchInventoryItemLocationsLevel
|
||||||
|
>
|
||||||
|
|
||||||
|
export const AdminBatchInventoryItemLocationsLevel = z.object({
|
||||||
|
create: z.array(AdminCreateInventoryLocationLevel).optional(),
|
||||||
|
update: z.array(AdminUpdateInventoryLocationLevelBatch).optional(),
|
||||||
|
delete: z.array(z.string()).optional(),
|
||||||
|
force: z.boolean().optional(),
|
||||||
|
})
|
||||||
|
|
||||||
export type AdminUpdateInventoryLocationLevelType = z.infer<
|
export type AdminUpdateInventoryLocationLevelType = z.infer<
|
||||||
typeof AdminUpdateInventoryLocationLevel
|
typeof AdminUpdateInventoryLocationLevel
|
||||||
>
|
>
|
||||||
@@ -129,3 +153,23 @@ export const AdminUpdateInventoryItem = z
|
|||||||
metadata: z.record(z.unknown()).nullish(),
|
metadata: z.record(z.unknown()).nullish(),
|
||||||
})
|
})
|
||||||
.strict()
|
.strict()
|
||||||
|
|
||||||
|
const AdminBatchInventoryLocationLevel = z.object({
|
||||||
|
inventory_item_id: z.string(),
|
||||||
|
location_id: z.string(),
|
||||||
|
stocked_quantity: z.number().min(0).optional(),
|
||||||
|
incoming_quantity: z.number().min(0).optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const AdminBatchInventoryItemLevels = z
|
||||||
|
.object({
|
||||||
|
create: z.array(AdminBatchInventoryLocationLevel).optional(),
|
||||||
|
update: z.array(AdminBatchInventoryLocationLevel).optional(),
|
||||||
|
delete: z.array(z.string()).optional(),
|
||||||
|
force: z.boolean().optional(),
|
||||||
|
})
|
||||||
|
.strict()
|
||||||
|
|
||||||
|
export type AdminBatchInventoryItemLevelsType = z.infer<
|
||||||
|
typeof AdminBatchInventoryItemLevels
|
||||||
|
>
|
||||||
@@ -546,11 +546,11 @@ export default class InventoryModuleService
|
|||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
async updateInventoryLevels(
|
async updateInventoryLevels(
|
||||||
updates: InventoryTypes.BulkUpdateInventoryLevelInput[],
|
updates: InventoryTypes.UpdateInventoryLevelInput[],
|
||||||
context?: Context
|
context?: Context
|
||||||
): Promise<InventoryTypes.InventoryLevelDTO[]>
|
): Promise<InventoryTypes.InventoryLevelDTO[]>
|
||||||
async updateInventoryLevels(
|
async updateInventoryLevels(
|
||||||
updates: InventoryTypes.BulkUpdateInventoryLevelInput,
|
updates: InventoryTypes.UpdateInventoryLevelInput,
|
||||||
context?: Context
|
context?: Context
|
||||||
): Promise<InventoryTypes.InventoryLevelDTO>
|
): Promise<InventoryTypes.InventoryLevelDTO>
|
||||||
|
|
||||||
@@ -558,8 +558,8 @@ export default class InventoryModuleService
|
|||||||
@EmitEvents()
|
@EmitEvents()
|
||||||
async updateInventoryLevels(
|
async updateInventoryLevels(
|
||||||
updates:
|
updates:
|
||||||
| InventoryTypes.BulkUpdateInventoryLevelInput[]
|
| InventoryTypes.UpdateInventoryLevelInput[]
|
||||||
| InventoryTypes.BulkUpdateInventoryLevelInput,
|
| InventoryTypes.UpdateInventoryLevelInput,
|
||||||
@MedusaContext() context: Context = {}
|
@MedusaContext() context: Context = {}
|
||||||
): Promise<
|
): Promise<
|
||||||
InventoryTypes.InventoryLevelDTO | InventoryTypes.InventoryLevelDTO[]
|
InventoryTypes.InventoryLevelDTO | InventoryTypes.InventoryLevelDTO[]
|
||||||
@@ -592,7 +592,7 @@ export default class InventoryModuleService
|
|||||||
|
|
||||||
@InjectTransactionManager()
|
@InjectTransactionManager()
|
||||||
async updateInventoryLevels_(
|
async updateInventoryLevels_(
|
||||||
updates: InventoryTypes.BulkUpdateInventoryLevelInput[],
|
updates: InventoryTypes.UpdateInventoryLevelInput[],
|
||||||
@MedusaContext() context: Context = {}
|
@MedusaContext() context: Context = {}
|
||||||
) {
|
) {
|
||||||
const inventoryLevels = await this.ensureInventoryLevels(
|
const inventoryLevels = await this.ensureInventoryLevels(
|
||||||
@@ -611,16 +611,13 @@ export default class InventoryModuleService
|
|||||||
return acc
|
return acc
|
||||||
}, new Map())
|
}, new Map())
|
||||||
|
|
||||||
return await this.inventoryLevelService_.update(
|
const updatesWithIds = updates.map((update) => {
|
||||||
updates.map((update) => {
|
const id = levelMap.get(update.inventory_item_id).get(update.location_id)
|
||||||
const id = levelMap
|
|
||||||
.get(update.inventory_item_id)
|
|
||||||
.get(update.location_id)
|
|
||||||
|
|
||||||
return { id, ...update }
|
return { id, ...update }
|
||||||
}),
|
})
|
||||||
context
|
|
||||||
)
|
return await this.inventoryLevelService_.update(updatesWithIds, context)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -5617,12 +5617,12 @@ __metadata:
|
|||||||
autoprefixer: ^10.4.17
|
autoprefixer: ^10.4.17
|
||||||
cmdk: ^0.2.0
|
cmdk: ^0.2.0
|
||||||
date-fns: ^3.6.0
|
date-fns: ^3.6.0
|
||||||
framer-motion: ^11.0.3
|
|
||||||
i18next: 23.7.11
|
i18next: 23.7.11
|
||||||
i18next-browser-languagedetector: 7.2.0
|
i18next-browser-languagedetector: 7.2.0
|
||||||
i18next-http-backend: 2.4.2
|
i18next-http-backend: 2.4.2
|
||||||
lodash: ^4.17.21
|
lodash: ^4.17.21
|
||||||
match-sorter: ^6.3.4
|
match-sorter: ^6.3.4
|
||||||
|
motion: ^11.15.0
|
||||||
postcss: ^8.4.33
|
postcss: ^8.4.33
|
||||||
prettier: ^3.1.1
|
prettier: ^3.1.1
|
||||||
qs: ^6.12.0
|
qs: ^6.12.0
|
||||||
@@ -20712,15 +20712,17 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"framer-motion@npm:^11.0.3":
|
"framer-motion@npm:^11.15.0":
|
||||||
version: 11.1.8
|
version: 11.15.0
|
||||||
resolution: "framer-motion@npm:11.1.8"
|
resolution: "framer-motion@npm:11.15.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
motion-dom: ^11.14.3
|
||||||
|
motion-utils: ^11.14.3
|
||||||
tslib: ^2.4.0
|
tslib: ^2.4.0
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
"@emotion/is-prop-valid": "*"
|
"@emotion/is-prop-valid": "*"
|
||||||
react: ^18.0.0
|
react: ^18.0.0 || ^19.0.0
|
||||||
react-dom: ^18.0.0
|
react-dom: ^18.0.0 || ^19.0.0
|
||||||
peerDependenciesMeta:
|
peerDependenciesMeta:
|
||||||
"@emotion/is-prop-valid":
|
"@emotion/is-prop-valid":
|
||||||
optional: true
|
optional: true
|
||||||
@@ -20728,7 +20730,7 @@ __metadata:
|
|||||||
optional: true
|
optional: true
|
||||||
react-dom:
|
react-dom:
|
||||||
optional: true
|
optional: true
|
||||||
checksum: ff7073ca011936fcf0e4af54ff93824609cf03b556def5a69d21f44674c44ec0429a450b419bb4ae89d5b7af9d7b0e88ed57d3eff1ff668ed2a52b42a60375dd
|
checksum: 59f1c1eea09a5cbda346624a7d700bdb1ccff8a8528ed145009db974283064c3a4e55ca9eaaf4950494f254f6233c37634735b9bd8463b25ffeef624030894d6
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@@ -25290,6 +25292,41 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"motion-dom@npm:^11.14.3":
|
||||||
|
version: 11.14.3
|
||||||
|
resolution: "motion-dom@npm:11.14.3"
|
||||||
|
checksum: 14989aba2981dcf618dc77d202ac35325366e645fd9e57c6942d88d0696263bbe7d0680da2e5f84e93339a67255bdbfebb8a4994a46584a661dd9a1e136fa7a1
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"motion-utils@npm:^11.14.3":
|
||||||
|
version: 11.14.3
|
||||||
|
resolution: "motion-utils@npm:11.14.3"
|
||||||
|
checksum: 7459bcb27311b72b416b2618cbfd56bad7d0fbec27736529e3f45a561fa78c43bf82e05338d9d9b765649b57d1c693821e83b30c6ba449d6f7f66c5245f072fb
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"motion@npm:^11.15.0":
|
||||||
|
version: 11.15.0
|
||||||
|
resolution: "motion@npm:11.15.0"
|
||||||
|
dependencies:
|
||||||
|
framer-motion: ^11.15.0
|
||||||
|
tslib: ^2.4.0
|
||||||
|
peerDependencies:
|
||||||
|
"@emotion/is-prop-valid": "*"
|
||||||
|
react: ^18.0.0 || ^19.0.0
|
||||||
|
react-dom: ^18.0.0 || ^19.0.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
"@emotion/is-prop-valid":
|
||||||
|
optional: true
|
||||||
|
react:
|
||||||
|
optional: true
|
||||||
|
react-dom:
|
||||||
|
optional: true
|
||||||
|
checksum: fe10db69ad3ca7cb3fd2896d4cd6a79ca8080de16f5fdfcf82a6decd474423f4207b7a924dc7bfb405cffb36d7de0e13780f1a623287be354ced65c78a612c99
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"mri@npm:^1.1.0":
|
"mri@npm:^1.1.0":
|
||||||
version: 1.2.0
|
version: 1.2.0
|
||||||
resolution: "mri@npm:1.2.0"
|
resolution: "mri@npm:1.2.0"
|
||||||
|
|||||||
Reference in new issue
Block a user