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:
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user