Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(inventory,dashboard,types,core-flows,js-sdk,medusa): Improve inventory UX #10630

Merged
merged 28 commits into from
Jan 13, 2025
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
8bf191a
feat(dashboard): Add UI for bulk editing inventory stock (#10556)
kasperkristensen Dec 13, 2024
ee2dc97
progress
kasperkristensen Dec 17, 2024
3bfd46d
Merge branch 'develop' into feat/inventory-ux-feature
kasperkristensen Dec 17, 2024
6ef581c
cleanup types
kasperkristensen Dec 17, 2024
2a0de9f
add changeset
kasperkristensen Dec 17, 2024
e796c2c
fix 0 values
kasperkristensen Dec 17, 2024
e158c9f
format schema
kasperkristensen Dec 17, 2024
5a26dfe
Merge branch 'develop' into feat/inventory-ux-feature
kasperkristensen Dec 17, 2024
b9cb43e
add delete event and allow copy/pasting enabled for some fields
kasperkristensen Dec 17, 2024
60e0fd3
add response types
kasperkristensen Dec 17, 2024
49468f4
add tests
kasperkristensen Dec 17, 2024
686f3a3
work on fixing setValue behaviour
kasperkristensen Dec 18, 2024
219e906
cleanup toggle logic
kasperkristensen Dec 18, 2024
89a9e82
add loading state
kasperkristensen Dec 20, 2024
af946b8
format schema
kasperkristensen Dec 20, 2024
238b588
Merge branch 'develop' into feat/inventory-ux-feature
kasperkristensen Dec 20, 2024
3689e7f
Merge branch 'develop' into feat/inventory-ux-feature
kasperkristensen Jan 6, 2025
4d6e39e
add support for bidirectional actions in DataGrid and update Checkbox…
kasperkristensen Jan 7, 2025
1cf5a71
update lock
kasperkristensen Jan 7, 2025
7f7dfeb
Merge branch 'develop' into feat/inventory-ux-feature
kasperkristensen Jan 7, 2025
f46a3db
lint
kasperkristensen Jan 7, 2025
a060dad
Merge branch 'feat/inventory-ux-feature' of https://github.com/medusa…
kasperkristensen Jan 7, 2025
2638460
fix 404
kasperkristensen Jan 8, 2025
149e4d5
Merge branch 'develop' into feat/inventory-ux-feature
kasperkristensen Jan 10, 2025
7059698
address feedback
kasperkristensen Jan 10, 2025
940ba48
update cursor on bidirectional select
kasperkristensen Jan 10, 2025
27f9649
Merge branch 'develop' into feat/inventory-ux-feature
kasperkristensen Jan 10, 2025
18e8bef
Merge branch 'develop' into feat/inventory-ux-feature
kasperkristensen Jan 10, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/violet-weeks-pretend.md
Original file line number Diff line number Diff line change
@@ -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
162 changes: 156 additions & 6 deletions integration-tests/http/__tests__/inventory/admin/inventory.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ medusaIntegrationTestRunner({
let inventoryItem2
let stockLocation1
let stockLocation2

let stockLocation3
beforeEach(async () => {
await createAdminUser(dbConnection, adminHeaders, getContainer())

Expand All @@ -24,6 +24,10 @@ medusaIntegrationTestRunner({
await api.post(`/admin/stock-locations`, { name: "loc2" }, adminHeaders)
).data.stock_location

stockLocation3 = (
await api.post(`/admin/stock-locations`, { name: "loc3" }, adminHeaders)
).data.stock_location

inventoryItem1 = (
await api.post(
`/admin/inventory-items`,
Expand Down Expand Up @@ -122,24 +126,170 @@ medusaIntegrationTestRunner({
})
})

describe("POST /admin/inventory-items/location-levels/batch", () => {
let locationLevel1
let locationLevel2

beforeEach(async () => {
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 () => {
await api.post(
const seed = await api.post(
`/admin/inventory-items/${inventoryItem1.id}/location-levels`,
{
location_id: stockLocation1.id,
stocked_quantity: 0,
},
adminHeaders
)

locationLevel1 = seed.data.inventory_item.location_levels[0]
})

it("should delete an inventory location level and create a new one", async () => {
const result = await api.post(
`/admin/inventory-items/${inventoryItem1.id}/location-levels/batch`,
{
create: [{ location_id: "location_2" }],
delete: [stockLocation1.id],
delete: [locationLevel1.id],
force: true,
},
adminHeaders
)
Expand All @@ -154,7 +304,7 @@ medusaIntegrationTestRunner({
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(
`/admin/inventory-items/${inventoryItem1.id}/location-levels/${stockLocation1.id}`,
{ stocked_quantity: 10 },
Expand All @@ -164,15 +314,15 @@ medusaIntegrationTestRunner({
const { response } = await api
.post(
`/admin/inventory-items/${inventoryItem1.id}/location-levels/batch`,
{ delete: [stockLocation1.id] },
{ delete: [locationLevel1.id] },
adminHeaders
)
.catch((e) => e)

expect(response.status).toEqual(400)
expect(response.data).toEqual({
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.`,
})
})

Expand Down
2 changes: 1 addition & 1 deletion packages/admin/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,12 @@
"@uiw/react-json-view": "^2.0.0-alpha.17",
"cmdk": "^0.2.0",
"date-fns": "^3.6.0",
"framer-motion": "^11.0.3",
"i18next": "23.7.11",
"i18next-browser-languagedetector": "7.2.0",
"i18next-http-backend": "2.4.2",
"lodash": "^4.17.21",
"match-sorter": "^6.3.4",
"motion": "^11.15.0",
"qs": "^6.12.0",
"react": "^18.2.0",
"react-country-flag": "^3.1.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { motion } from "framer-motion"
import { motion } from "motion/react"

import { IconAvatar } from "../icon-avatar"

export default function AvatarBox({ checked }: { checked?: boolean }) {
return (
<IconAvatar
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 && (
<motion.div
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { clx } from "@medusajs/ui"
import { Transition, motion } from "framer-motion"
import { Transition, motion } from "motion/react"

type LogoBoxProps = {
className?: string
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./progress-bar"
Original file line number Diff line number Diff line change
@@ -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" },
}}
/>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export const Thumbnail = ({ src, alt, size = "base" }: ThumbnailProps) => {
return (
<div
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-5 w-4": size === "small",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export const DataGridCellContainer = ({
<div
{...overlayProps}
data-cell-overlay="true"
className="absolute inset-0"
className="absolute inset-0 z-[2]"
/>
)}
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -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>
)
}
Original file line number Diff line number Diff line change
@@ -1,24 +1,32 @@
import { PropsWithChildren } from "react"

import { clx } from "@medusajs/ui"
import { useDataGridCellError } from "../hooks"
import { DataGridCellProps } from "../types"
import { DataGridRowErrorIndicator } from "./data-grid-row-error-indicator"

type DataGridReadonlyCellProps<TData, TValue = any> = DataGridCellProps<
TData,
TValue
> &
PropsWithChildren
type DataGridReadonlyCellProps<TData, TValue = any> = PropsWithChildren<
DataGridCellProps<TData, TValue>
> & {
color?: "muted" | "normal"
}

export const DataGridReadonlyCell = <TData, TValue = any>({
context,
color = "muted",
children,
}: DataGridReadonlyCellProps<TData, TValue>) => {
const { rowErrors } = useDataGridCellError({ context })

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">
<span className="truncate">{children}</span>
<div
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} />
</div>
)
Expand Down
Loading
Loading