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:
Kasper Fabricius Kristensen
2025-01-12 19:07:14 -05:00
committed by GitHub
parent c5915451b8
commit bc22b81cdf
82 changed files with 2720 additions and 289 deletions
@@ -11,7 +11,6 @@ export const createInventoryLevelsStep = createStep(
createInventoryLevelsStepId,
async (data: InventoryTypes.CreateInventoryLevelInput[], { container }) => {
const service = container.resolve<IInventoryService>(Modules.INVENTORY)
const inventoryLevels = await service.createInventoryLevels(data)
return new StepResponse(
inventoryLevels,
@@ -13,10 +13,7 @@ export const updateInventoryLevelsStepId = "update-inventory-levels-step"
*/
export const updateInventoryLevelsStep = createStep(
updateInventoryLevelsStepId,
async (
input: InventoryTypes.BulkUpdateInventoryLevelInput[],
{ container }
) => {
async (input: InventoryTypes.UpdateInventoryLevelInput[], { container }) => {
const inventoryService: IInventoryService = container.resolve(
Modules.INVENTORY
)
@@ -54,7 +51,7 @@ export const updateInventoryLevelsStep = createStep(
await inventoryService.updateInventoryLevels(
dataBeforeUpdate.map((data) =>
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 {
createWorkflow,
@@ -17,6 +19,8 @@ export const bulkCreateDeleteLevelsWorkflowId =
"bulk-create-delete-levels-workflow"
/**
* This workflow creates and deletes inventory levels.
*
* @deprecated Use `batchInventoryItemLevels` instead.
*/
export const bulkCreateDeleteLevelsWorkflow = createWorkflow(
bulkCreateDeleteLevelsWorkflowId,
@@ -6,7 +6,10 @@ import {
WorkflowResponse,
} 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 { useRemoteQueryStep } from "../../common"
import { deleteEntitiesStep } from "../../common/steps/delete-entities"
@@ -16,24 +19,52 @@ import { deleteEntitiesStep } from "../../common/steps/delete-entities"
*/
export const validateInventoryLevelsDelete = createStep(
"validate-inventory-levels-delete",
async function ({ inventoryLevels }: { inventoryLevels: any[] }) {
const undeleteableItems = inventoryLevels.filter(
(i) => i.reserved_quantity > 0 || i.stocked_quantity > 0
async function ({
inventoryLevels,
force,
}: {
inventoryLevels: InventoryLevelDTO[]
force?: boolean
}) {
const undeleteableDueToReservation = inventoryLevels.filter(
(i) => i.reserved_quantity > 0 || i.incoming_quantity > 0
)
if (undeleteableItems.length) {
const stockLocationIds = deduplicate(
undeleteableItems.map((item) => item.location_id)
if (undeleteableDueToReservation.length) {
const locationIds = deduplicate(
undeleteableDueToReservation.map((item) => item.location_id)
)
throw new MedusaError(
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 =
"delete-inventory-levels-workflow"
/**
@@ -41,16 +72,25 @@ export const deleteInventoryLevelsWorkflowId =
*/
export const deleteInventoryLevelsWorkflow = createWorkflow(
deleteInventoryLevelsWorkflowId,
(input: WorkflowData<FilterableInventoryLevelProps>) => {
(input: WorkflowData<DeleteInventoryLevelsWorkflowInput>) => {
const { filters, force } = transform(input, (data) => {
const { force, ...filters } = data
return {
filters,
force,
}
})
const inventoryLevels = useRemoteQueryStep({
entry_point: "inventory_levels",
fields: ["id", "stocked_quantity", "reserved_quantity", "location_id"],
variables: {
filters: input,
filters: filters,
},
})
validateInventoryLevelsDelete({ inventoryLevels })
validateInventoryLevelsDelete({ inventoryLevels, force })
const idsToDelete = transform({ inventoryLevels }, ({ inventoryLevels }) =>
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-levels"
export * from "./update-inventory-items"
export * from "./delete-inventory-items"
export * from "./delete-inventory-levels"
export * from "./update-inventory-items"
export * from "./update-inventory-levels"
export * from "./bulk-create-delete-levels"
@@ -8,7 +8,7 @@ import {
import { updateInventoryLevelsStep } from "../steps/update-inventory-levels"
export interface UpdateInventoryLevelsWorkflowInput {
updates: InventoryTypes.BulkUpdateInventoryLevelInput[]
updates: InventoryTypes.UpdateInventoryLevelInput[]
}
export const updateInventoryLevelsWorkflowId =
"update-inventory-levels-workflow"