Feat: Medusa react price list (#1258)

* export everything from price lists in core

* medusa-js price list

* feat: add product list for price lists

* feat: add product list for price lists

* add price list to admin module

* add price list hooks initial

* refactor: product list controller

* fix: add integration test for price list products

* update types

* add tests for price lists

* update medusa react tests

* update update request for price lists

* Apply suggestions from code review

Co-authored-by: Sebastian Rindom <skrindom@gmail.com>

* rename methods

* pr feedback

* list products from price list

* fix errors after merge

* update medusa react with medusa-js method name changes

* redo changes

* update hook names

* fix: routes in msw handler

Co-authored-by: Sebastian Rindom <skrindom@gmail.com>
Co-authored-by: Zakaria El Asri <zakaria.elas@gmail.com>
This commit is contained in:
Philip Korsholm
2022-04-03 20:48:49 +02:00
committed by GitHub
co-authored by Sebastian Rindom Zakaria El Asri
parent fb33dbaca3
commit b164977a19
24 changed files with 838 additions and 276 deletions
@@ -24,6 +24,7 @@ import AdminRegionsResource from "./regions"
import AdminNotificationsResource from "./notifications"
import AdminUploadsResource from "./uploads"
import AdminProductTagsResource from "./product-tags"
import AdminPriceListResource from "./price-lists"
class Admin extends BaseResource {
public auth = new AdminAuthResource(this.client)
@@ -35,6 +36,7 @@ class Admin extends BaseResource {
public giftCards = new AdminGiftCardsResource(this.client)
public invites = new AdminInvitesResource(this.client)
public notes = new AdminNotesResource(this.client)
public priceLists = new AdminPriceListResource(this.client)
public products = new AdminProductsResource(this.client)
public productTags = new AdminProductTagsResource(this.client)
public productTypes = new AdminProductTypesResource(this.client)
@@ -0,0 +1,99 @@
import {
AdminPostPriceListPricesPricesReq,
AdminPostPriceListsPriceListPriceListReq,
AdminPostPriceListsPriceListReq,
AdminPriceListDeleteRes,
AdminPriceListRes,
AdminGetPriceListPaginationParams,
AdminPriceListsListRes,
AdminDeletePriceListPricesPricesReq,
AdminPriceListDeleteBatchRes,
AdminGetPriceListsPriceListProductsParams,
} from "@medusajs/medusa"
import qs from "qs"
import { ResponsePromise } from "../../typings"
import BaseResource from "../base"
class AdminPriceListResource extends BaseResource {
create(
payload: AdminPostPriceListsPriceListReq,
customHeaders: Record<string, any> = {}
): ResponsePromise<AdminPriceListRes> {
const path = `/admin/price-lists`
return this.client.request("POST", path, payload, {}, customHeaders)
}
update(
id: string,
payload: AdminPostPriceListsPriceListPriceListReq,
customHeaders: Record<string, any> = {}
): ResponsePromise<AdminPriceListRes> {
const path = `/admin/price-lists/${id}`
return this.client.request("POST", path, payload, {}, customHeaders)
}
delete(
id: string,
customHeaders: Record<string, any> = {}
): ResponsePromise<AdminPriceListDeleteRes> {
const path = `/admin/price-lists/${id}`
return this.client.request("DELETE", path, {}, {}, customHeaders)
}
retrieve(
id: string,
customHeaders: Record<string, any> = {}
): ResponsePromise<AdminPriceListRes> {
const path = `/admin/price-lists/${id}`
return this.client.request("GET", path, {}, {}, customHeaders)
}
list(
query?: AdminGetPriceListPaginationParams,
customHeaders: Record<string, any> = {}
): ResponsePromise<AdminPriceListsListRes> {
let path = `/admin/price-lists/`
if (query) {
const queryString = qs.stringify(query)
path = `/admin/price-lists?${queryString}`
}
return this.client.request("GET", path, {}, {}, customHeaders)
}
listProducts(
id: string,
query?: AdminGetPriceListsPriceListProductsParams,
customHeaders: Record<string, any> = {}
): ResponsePromise<any> {
let path = `/admin/price-lists/${id}/products`
if (query) {
const queryString = qs.stringify(query)
path = `/admin/price-lists/${id}/products?${queryString}`
}
return this.client.request("GET", path, {}, {}, customHeaders)
}
addPrices(
id: string,
payload: AdminPostPriceListPricesPricesReq,
customHeaders: Record<string, any> = {}
): ResponsePromise<AdminPriceListRes> {
const path = `/admin/price-lists/${id}/prices/batch`
return this.client.request("POST", path, payload, {}, customHeaders)
}
deletePrices(
id: string,
payload: AdminDeletePriceListPricesPricesReq,
customHeaders: Record<string, any> = {}
): ResponsePromise<AdminPriceListDeleteBatchRes> {
const path = `/admin/price-lists/${id}/prices/batch`
return this.client.request("DELETE", path, payload, {}, customHeaders)
}
}
export default AdminPriceListResource
+13 -5
View File
@@ -22,7 +22,10 @@ class CustomerResource extends BaseResource {
* @param customHeaders
* @return { ResponsePromise<StoreCustomersRes>}
*/
create(payload: StorePostCustomersReq, customHeaders: Record<string, any> = {}): ResponsePromise<StoreCustomersRes> {
create(
payload: StorePostCustomersReq,
customHeaders: Record<string, any> = {}
): ResponsePromise<StoreCustomersRes> {
const path = `/store/customers`
return this.client.request("POST", path, payload, {}, customHeaders)
}
@@ -32,7 +35,9 @@ class CustomerResource extends BaseResource {
* @param customHeaders
* @return {ResponsePromise<StoreCustomersRes>}
*/
retrieve(customHeaders: Record<string, any> = {}): ResponsePromise<StoreCustomersRes> {
retrieve(
customHeaders: Record<string, any> = {}
): ResponsePromise<StoreCustomersRes> {
const path = `/store/customers/me`
return this.client.request("GET", path, {}, {}, customHeaders)
}
@@ -45,7 +50,8 @@ class CustomerResource extends BaseResource {
*/
update(
payload: StorePostCustomersCustomerReq,
customHeaders: Record<string, any> = {}): ResponsePromise<StoreCustomersRes> {
customHeaders: Record<string, any> = {}
): ResponsePromise<StoreCustomersRes> {
const path = `/store/customers/me`
return this.client.request("POST", path, payload, {}, customHeaders)
}
@@ -58,7 +64,8 @@ class CustomerResource extends BaseResource {
*/
listOrders(
params?: StoreGetCustomersCustomerOrdersParams,
customHeaders: Record<string, any> = {}): ResponsePromise<StoreCustomersListOrdersRes> {
customHeaders: Record<string, any> = {}
): ResponsePromise<StoreCustomersListOrdersRes> {
let path = `/store/customers/me/orders`
if (params) {
const query = qs.stringify(params)
@@ -77,7 +84,8 @@ class CustomerResource extends BaseResource {
*/
resetPassword(
payload: StorePostCustomersCustomerPasswordTokenReq,
customHeaders: Record<string, any> = {}): ResponsePromise<StoreCustomersRes> {
customHeaders: Record<string, any> = {}
): ResponsePromise<StoreCustomersRes> {
const path = `/store/customers/password-reset`
return this.client.request("POST", path, payload, {}, customHeaders)
}
@@ -1076,6 +1076,18 @@
"deleted_at": null,
"metadata": null
},
"price_list": {
"id": "pl_ZXDBABWJVW6115VEMZXDBAB",
"name": "test price list",
"description": "test price list",
"type": "products",
"status": "active",
"starts_at": null,
"ends_at": null,
"created_at": "2021-03-16T21:24:35.828Z",
"updated_at": "2021-03-16T21:24:35.828Z",
"deleted_at": null
},
"note": {
"id": "note_01F0ZXDBAB9KVZWJVW6115VEM7",
"value": "customer contacted",
@@ -170,6 +170,89 @@ export const adminHandlers = [
)
}),
rest.post("/admin/price-lists/", (req, res, ctx) => {
const body = req.body as Record<string, any>
return res(
ctx.status(200),
ctx.json({
price_list: {
...fixtures.get("price_list"),
...body,
},
})
)
}),
rest.get("/admin/price-lists/", (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
price_lists: fixtures.list("price_list"),
count: 2,
offset: 0,
limit: 10,
})
)
}),
rest.get("/admin/price-lists/:id", (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
price_list: fixtures.get("price_list"),
})
)
}),
rest.post("/admin/price-lists/:id", (req, res, ctx) => {
const body = req.body as Record<string, any>
return res(
ctx.status(200),
ctx.json({
price_list: {
...fixtures.get("price_list"),
...body,
id: req.params.id,
},
})
)
}),
rest.delete("/admin/price-lists/:id", (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
id: req.params.id,
object: "price_list",
deleted: true,
})
)
}),
rest.delete("/admin/price-lists/:id/prices/batch", (req, res, ctx) => {
const body = req.body as Record<string, any>
return res(
ctx.status(200),
ctx.json({
ids: body.price_ids,
object: "money-amount",
deleted: true,
})
)
}),
rest.post("/admin/price-lists/:id/prices/batch", (req, res, ctx) => {
return res(
ctx.status(200),
ctx.json({
price_list: {
...fixtures.get("price_list"),
id: req.params.id,
},
})
)
}),
rest.post("/admin/return-reasons/", (req, res, ctx) => {
const body = req.body as Record<string, any>
return res(
@@ -10,6 +10,7 @@ export * from "./orders"
export * from "./products"
export * from "./product-tags"
export * from "./product-types"
export * from "./price-lists"
export * from "./return-reasons"
export * from "./regions"
export * from "./shipping-options"
@@ -0,0 +1,2 @@
export * from "./queries"
export * from "./mutations"
@@ -0,0 +1,108 @@
import {
AdminPriceListRes,
AdminPostPriceListsPriceListPriceListReq,
AdminPostPriceListsPriceListReq,
AdminPostPriceListPricesPricesReq,
AdminDeletePriceListPricesPricesReq,
AdminPriceListDeleteRes,
AdminPriceListDeleteBatchRes,
} from "@medusajs/medusa"
import { Response } from "@medusajs/medusa-js"
import { useMutation, UseMutationOptions, useQueryClient } from "react-query"
import { useMedusa } from "../../../contexts/medusa"
import { buildOptions } from "../../utils/buildOptions"
import { adminPriceListKeys } from "./queries"
export const useAdminCreatePriceList = (
options?: UseMutationOptions<
Response<AdminPriceListRes>,
Error,
AdminPostPriceListsPriceListReq
>
) => {
const { client } = useMedusa()
const queryClient = useQueryClient()
return useMutation(
(payload: AdminPostPriceListsPriceListReq) =>
client.admin.priceLists.create(payload),
buildOptions(queryClient, adminPriceListKeys.lists(), options)
)
}
export const useAdminUpdatePriceList = (
id: string,
options?: UseMutationOptions<
Response<AdminPriceListRes>,
Error,
AdminPostPriceListsPriceListPriceListReq
>
) => {
const { client } = useMedusa()
const queryClient = useQueryClient()
return useMutation(
(payload: AdminPostPriceListsPriceListPriceListReq) =>
client.admin.priceLists.update(id, payload),
buildOptions(
queryClient,
[adminPriceListKeys.detail(id), adminPriceListKeys.lists()],
options
)
)
}
export const useAdminDeletePriceList = (
id: string,
options?: UseMutationOptions<Response<AdminPriceListDeleteRes>, Error, void>
) => {
const { client } = useMedusa()
const queryClient = useQueryClient()
return useMutation(
() => client.admin.priceLists.delete(id),
buildOptions(
queryClient,
[adminPriceListKeys.detail(id), adminPriceListKeys.lists()],
options
)
)
}
export const useAdminCreatePriceListPrices = (
id: string,
options?: UseMutationOptions<
Response<AdminPriceListRes>,
Error,
AdminPostPriceListPricesPricesReq
>
) => {
const { client } = useMedusa()
const queryClient = useQueryClient()
return useMutation(
(payload: AdminPostPriceListPricesPricesReq) =>
client.admin.priceLists.addPrices(id, payload),
buildOptions(queryClient, adminPriceListKeys.lists(), options)
)
}
export const useAdminDeletePriceListPrices = (
id: string,
options?: UseMutationOptions<
Response<AdminPriceListDeleteBatchRes>,
Error,
AdminDeletePriceListPricesPricesReq
>
) => {
const { client } = useMedusa()
const queryClient = useQueryClient()
return useMutation(
(payload: AdminDeletePriceListPricesPricesReq) =>
client.admin.priceLists.deletePrices(id, payload),
buildOptions(
queryClient,
[adminPriceListKeys.detail(id), adminPriceListKeys.lists()],
options
)
)
}
@@ -0,0 +1,70 @@
import {
AdminGetPriceListsPriceListProductsParams,
AdminGetPriceListPaginationParams,
AdminPriceListsListRes,
AdminPriceListRes,
AdminProductsListRes,
} from "@medusajs/medusa"
import { Response } from "@medusajs/medusa-js"
import { useQuery } from "react-query"
import { useMedusa } from "../../../contexts"
import { UseQueryOptionsWrapper } from "../../../types"
import { queryKeysFactory } from "../../utils/index"
const ADMIN_PRICE_LISTS_QUERY_KEY = `admin_price_lists` as const
export const adminPriceListKeys = queryKeysFactory(ADMIN_PRICE_LISTS_QUERY_KEY)
type PriceListQueryKeys = typeof adminPriceListKeys
export const useAdminPriceLists = (
query?: AdminGetPriceListPaginationParams,
options?: UseQueryOptionsWrapper<
Response<AdminPriceListsListRes>,
Error,
ReturnType<PriceListQueryKeys["list"]>
>
) => {
const { client } = useMedusa()
const { data, ...rest } = useQuery(
adminPriceListKeys.list(query),
() => client.admin.priceLists.list(query),
options
)
return { ...data, ...rest } as const
}
export const useAdminPriceListProducts = (
id: string,
query?: AdminGetPriceListsPriceListProductsParams,
options?: UseQueryOptionsWrapper<
Response<AdminProductsListRes>,
Error,
ReturnType<PriceListQueryKeys["detail"]>
>
) => {
const { client } = useMedusa()
const { data, ...rest } = useQuery(
adminPriceListKeys.detail(id),
() => client.admin.priceLists.listProducts(id, query),
options
)
return { ...data, ...rest } as const
}
export const useAdminPriceList = (
id: string,
options?: UseQueryOptionsWrapper<
Response<AdminPriceListRes>,
Error,
ReturnType<PriceListQueryKeys["detail"]>
>
) => {
const { client } = useMedusa()
const { data, ...rest } = useQuery(
adminPriceListKeys.detail(id),
() => client.admin.priceLists.retrieve(id),
options
)
return { ...data, ...rest } as const
}
@@ -0,0 +1,139 @@
import {
useAdminCreatePriceList,
useAdminCreatePriceListPrices,
useAdminDeletePriceList,
useAdminDeletePriceListPrices,
useAdminUpdatePriceList,
} from "../../../../src"
import { renderHook } from "@testing-library/react-hooks"
import { fixtures } from "../../../../mocks/data"
import { createWrapper } from "../../../utils"
import { PriceListType } from "@medusajs/medusa/dist/types/price-list"
describe("useAdminCreatePriceList hook", () => {
test("creates a price list and returns it", async () => {
const priceList = {
name: "talked to customer",
type: PriceListType.SALE,
description: "test",
prices: [],
}
const { result, waitFor } = renderHook(() => useAdminCreatePriceList(), {
wrapper: createWrapper(),
})
result.current.mutate(priceList)
await waitFor(() => result.current.isSuccess)
expect(result.current.data.response.status).toEqual(200)
expect(result.current.data.price_list).toEqual(
expect.objectContaining({
...fixtures.get("price_list"),
...priceList,
})
)
})
})
describe("useAdminUpdatePriceList hook", () => {
test("updates a price list and returns it", async () => {
const priceList = {
name: "talked to customer",
type: PriceListType.SALE,
prices: [],
customer_groups: [],
}
const { result, waitFor } = renderHook(
() => useAdminUpdatePriceList(fixtures.get("price_list").id),
{
wrapper: createWrapper(),
}
)
result.current.mutate(priceList)
await waitFor(() => result.current.isSuccess)
expect(result.current.data.response.status).toEqual(200)
expect(result.current.data.price_list).toEqual(
expect.objectContaining({
...fixtures.get("price_list"),
...priceList,
})
)
})
})
describe("useAdminDeletePriceList hook", () => {
test("deletes a price list", async () => {
const { result, waitFor } = renderHook(
() => useAdminDeletePriceList(fixtures.get("price_list").id),
{
wrapper: createWrapper(),
}
)
result.current.mutate()
await waitFor(() => result.current.isSuccess)
expect(result.current.data.response.status).toEqual(200)
expect(result.current.data).toEqual(
expect.objectContaining({
id: fixtures.get("price_list").id,
deleted: true,
})
)
})
})
describe("useAdminDeletePriceListBatch hook", () => {
test("deletes a money amounts from price list", async () => {
const { result, waitFor } = renderHook(
() => useAdminDeletePriceListPrices(fixtures.get("price_list").id),
{
wrapper: createWrapper(),
}
)
result.current.mutate({ price_ids: [fixtures.get("money_amount").id] })
await waitFor(() => result.current.isSuccess)
expect(result.current.data.response.status).toEqual(200)
expect(result.current.data).toEqual(
expect.objectContaining({
ids: [fixtures.get("money_amount").id],
deleted: true,
})
)
})
})
describe("useAdminDeletePriceList hook", () => {
test("Adds prices to price list", async () => {
const { result, waitFor } = renderHook(
() => useAdminCreatePriceListPrices(fixtures.get("price_list").id),
{
wrapper: createWrapper(),
}
)
result.current.mutate({ prices: [] })
await waitFor(() => result.current.isSuccess)
expect(result.current.data.response.status).toEqual(200)
expect(result.current.data).toEqual(
expect.objectContaining({
price_list: {
...fixtures.get("price_list"),
},
})
)
})
})
@@ -0,0 +1,35 @@
import { useAdminPriceList, useAdminPriceLists } from "../../../../src"
import { renderHook } from "@testing-library/react-hooks"
import { fixtures } from "../../../../mocks/data"
import { createWrapper } from "../../../utils"
describe("useAdminPriceLists hook", () => {
test("returns a list of price lists", async () => {
const priceLists = fixtures.list("price_list")
const { result, waitFor } = renderHook(() => useAdminPriceLists(), {
wrapper: createWrapper(),
})
await waitFor(() => result.current.isSuccess)
expect(result.current.response.status).toEqual(200)
expect(result.current.price_lists).toEqual(priceLists)
})
})
describe("useAdminPriceList hook", () => {
test("returns a price list", async () => {
const priceList = fixtures.get("price_list")
const { result, waitFor } = renderHook(
() => useAdminPriceList(priceList.id),
{
wrapper: createWrapper(),
}
)
await waitFor(() => result.current.isSuccess)
expect(result.current.response.status).toEqual(200)
expect(result.current.price_list).toEqual(priceList)
})
})
+1
View File
@@ -41,6 +41,7 @@ export * from "./routes/admin/shipping-options"
export * from "./routes/admin/regions"
export * from "./routes/admin/product-tags"
export * from "./routes/admin/product-types"
export * from "./routes/admin/price-lists"
// Store
export * from "./routes/store/auth"
@@ -58,6 +58,12 @@ export type AdminPriceListRes = {
price_list: PriceList
}
export type AdminPriceListDeleteBatchRes = {
ids: string[]
deleted: boolean
object: string
}
export type AdminPriceListDeleteRes = DeleteResponse
export type AdminPriceListsListRes = PaginatedResponse & {
@@ -70,4 +76,5 @@ export * from "./delete-price-list"
export * from "./get-price-list"
export * from "./list-price-lists"
export * from "./update-price-list"
export * from "./delete-prices-batch"
export * from "./list-price-list-products"
@@ -9,7 +9,6 @@ import {
ValidateNested,
} from "class-validator"
import { omit } from "lodash"
import { Product } from "../../../../models/product"
import { DateComparisonOperator } from "../../../../types/common"
import {
@@ -5,7 +5,6 @@ import { MedusaError } from "medusa-core-utils"
import { Product } from "../../models/product"
import { ProductService } from "../../services"
import { getListConfig } from "../../utils/get-query-config"
import { FindConfig } from "../../types/common"
import { FilterableProductProps } from "../../types/product"
type ListContext = {