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
+4
View File
@@ -21,17 +21,21 @@ Thank you for considering contributing to Medusa! This document will outline how
### Branches ### Branches
All changes should be part of a branch and submitted as a pull request - your branches should be prefixed with one of: All changes should be part of a branch and submitted as a pull request - your branches should be prefixed with one of:
- `fix/` for bug fixes - `fix/` for bug fixes
- `feat/` for features - `feat/` for features
- `docs/` for documentation changes - `docs/` for documentation changes
### Commits ### Commits
Strive towards keeping your commits small and isolated - this helps the reviewer understand what is going on and makes it easier to process your requests. Strive towards keeping your commits small and isolated - this helps the reviewer understand what is going on and makes it easier to process your requests.
### Pull Requests ### Pull Requests
Once your changes are ready you must submit your branch as a pull request. Your pull request should be opened against the `develop` branch in the main Medusa repo. Once your changes are ready you must submit your branch as a pull request. Your pull request should be opened against the `develop` branch in the main Medusa repo.
In your PR's description you should follow the structure: In your PR's description you should follow the structure:
- **What** - what changes are in this PR - **What** - what changes are in this PR
- **Why** - why are these changes relevant - **Why** - why are these changes relevant
- **How** - how have the changes been implemented - **How** - how have the changes been implemented
+2
View File
@@ -67,6 +67,7 @@ After these four steps and only a couple of minutes, you now have a complete com
Write-ups for all features will be made available in [Github discussions](https://github.com/medusajs/medusa/discussions) prior to starting the implementation process. Write-ups for all features will be made available in [Github discussions](https://github.com/medusajs/medusa/discussions) prior to starting the implementation process.
### Q1 ### Q1
- [x] Admin revamp - [x] Admin revamp
- [x] Tax API - [x] Tax API
- [x] Strategy pattern - [x] Strategy pattern
@@ -74,6 +75,7 @@ Write-ups for all features will be made available in [Github discussions](https:
- [ ] Bulk import / export - [ ] Bulk import / export
### Q2 ### Q2
- [ ] Extended Product API (custom fields, price lists, publishing control, and more) - [ ] Extended Product API (custom fields, price lists, publishing control, and more)
- [ ] Extended Order API (managing placed orders, improved inventory control, and more) - [ ] Extended Order API (managing placed orders, improved inventory control, and more)
- [ ] Sales Channel API - [ ] Sales Channel API
+216 -216
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -120,8 +120,8 @@ yarn global add create-strapi-app@3.6.8
create-strapi-app strapi-medusa --template https://github.com/Deathwish98/strapi-medusa-template.git create-strapi-app strapi-medusa --template https://github.com/Deathwish98/strapi-medusa-template.git
``` ```
> Note: The plugin expects node version to be '>= 10.16.0 and <=14.x.x', otherwise it will throw an error.
> Note: The plugin expects node version to be '>= 10.16.0 and <=14.x.x', otherwise it will throw an error.
After running the command, you have a full Strapi project configured to synchronize with Medusa. Upon the initial start of the Strapi server, all the required models will be created. They will correlate with models from Medusa to allow for two-way synchronization. After running the command, you have a full Strapi project configured to synchronize with Medusa. Upon the initial start of the Strapi server, all the required models will be created. They will correlate with models from Medusa to allow for two-way synchronization.
-1
View File
@@ -8,7 +8,6 @@
</video> </video>
</div> </div>
### Introduction ### Introduction
Handling payments is at the core of every commerce system; it allows us to run our businesses. Consequently, a vast landscape of payment providers has developed, each with varying cost models, implementational specifications, and analytical capabilities. Handling payments is at the core of every commerce system; it allows us to run our businesses. Consequently, a vast landscape of payment providers has developed, each with varying cost models, implementational specifications, and analytical capabilities.
@@ -24,7 +24,7 @@ export default () => {
router.get("/admin/hello", (req, res) => { router.get("/admin/hello", (req, res) => {
res.json({ res.json({
message: "Welcome to Your Store!" message: "Welcome to Your Store!",
}) })
}) })
@@ -53,8 +53,8 @@ Then, create an object that will hold the CORS configurations:
```js ```js
const corsOptions = { const corsOptions = {
origin: projectConfig.admin_cors.split(","), origin: projectConfig.admin_cors.split(","),
credentials: true, credentials: true,
} }
``` ```
@@ -64,7 +64,7 @@ Finally, for each route you add, create an `OPTIONS` request:
router.options("/admin/hello", cors(corsOptions)) router.options("/admin/hello", cors(corsOptions))
router.get("/admin/hello", (req, res) => { router.get("/admin/hello", (req, res) => {
//... //...
}); })
``` ```
## Multiple Endpoints ## Multiple Endpoints
@@ -76,13 +76,13 @@ You can add more than one endpoints in `src/api/index.js`:
```js ```js
router.get("/admin/hello", (req, res) => { router.get("/admin/hello", (req, res) => {
res.json({ res.json({
message: "Welcome to Your Store!" message: "Welcome to Your Store!",
}) })
}) })
router.get("/admin/bye", (req, res) => { router.get("/admin/bye", (req, res) => {
res.json({ res.json({
message: "Come back again!" message: "Come back again!",
}) })
}) })
``` ```
@@ -97,7 +97,7 @@ To do that with the previous example, first, create the file `src/api/hello.js`
export default (router) => { export default (router) => {
router.get("/admin/hello", (req, res) => { router.get("/admin/hello", (req, res) => {
res.json({ res.json({
message: "Welcome to Your Store!" message: "Welcome to Your Store!",
}) })
}) })
} }
@@ -111,7 +111,7 @@ Next, create the file `src/api/bye.js` with the following content:
export default (router) => { export default (router) => {
router.get("/admin/bye", (req, res) => { router.get("/admin/bye", (req, res) => {
res.json({ res.json({
message: "Come back again!" message: "Come back again!",
}) })
}) })
} }
@@ -148,17 +148,15 @@ You can retrieve any registered service in your endpoint using `req.scope.resolv
Heres an example of an endpoint that retrieves the count of products in your store: Heres an example of an endpoint that retrieves the count of products in your store:
```js ```js
router.get('/admin/products/count', (req, res) => { router.get("/admin/products/count", (req, res) => {
const productService = req.scope.resolve('productService') const productService = req.scope.resolve("productService")
productService productService.count().then((count) => {
.count() res.json({
.then((count) => { count,
res.json({ })
count
})
})
}) })
})
``` ```
The `productService` has a `count` method that returns a Promise. This Promise resolves to the count of the products. You return a JSON of the product count. The `productService` has a `count` method that returns a Promise. This Promise resolves to the count of the products. You return a JSON of the product count.
@@ -170,13 +168,13 @@ Protected routes are routes that should be accessible by logged-in users only.
To make a route protected, first, import the `authenticate` middleware: To make a route protected, first, import the `authenticate` middleware:
```js ```js
import authenticate from '@medusajs/medusa/dist/api/middlewares/authenticate' import authenticate from "@medusajs/medusa/dist/api/middlewares/authenticate"
``` ```
Then, add the middleware to your route: Then, add the middleware to your route:
```js ```js
router.get('/store/products/count', authenticate(), (req, res) => { router.get("/store/products/count", authenticate(), (req, res) => {
//... //...
}) })
``` ```
@@ -24,7 +24,7 @@ export default () => {
router.get("/store/hello", (req, res) => { router.get("/store/hello", (req, res) => {
res.json({ res.json({
message: "Welcome to My Store!" message: "Welcome to My Store!",
}) })
}) })
@@ -47,13 +47,13 @@ You can add more than one endpoints in `src/api/index.js`:
```js ```js
router.get("/store/hello", (req, res) => { router.get("/store/hello", (req, res) => {
res.json({ res.json({
message: "Welcome to My Store!" message: "Welcome to My Store!",
}) })
}) })
router.get("/store/bye", (req, res) => { router.get("/store/bye", (req, res) => {
res.json({ res.json({
message: "Come back again!" message: "Come back again!",
}) })
}) })
``` ```
@@ -68,7 +68,7 @@ To do that with the previous example, first, create the file `src/api/hello.js`
export default (router) => { export default (router) => {
router.get("/store/hello", (req, res) => { router.get("/store/hello", (req, res) => {
res.json({ res.json({
message: "Welcome to My Store!" message: "Welcome to My Store!",
}) })
}) })
} }
@@ -82,7 +82,7 @@ Next, create the file `src/api/bye.js` with the following content:
export default (router) => { export default (router) => {
router.get("/store/bye", (req, res) => { router.get("/store/bye", (req, res) => {
res.json({ res.json({
message: "Come back again!" message: "Come back again!",
}) })
}) })
} }
@@ -119,17 +119,15 @@ You can retrieve any registered service in your endpoint using `req.scope.resolv
Heres an example of an endpoint that retrieves the count of products in your store: Heres an example of an endpoint that retrieves the count of products in your store:
```js ```js
router.get('/store/products/count', (req, res) => { router.get("/store/products/count", (req, res) => {
const productService = req.scope.resolve('productService') const productService = req.scope.resolve("productService")
productService productService.count().then((count) => {
.count() res.json({
.then((count) => { count,
res.json({ })
count
})
})
}) })
})
``` ```
The `productService` has a `count` method that returns a Promise. This Promise resolves to the count of the products. You return a JSON of the product count. The `productService` has a `count` method that returns a Promise. This Promise resolves to the count of the products. You return a JSON of the product count.
@@ -141,13 +139,13 @@ Protected routes are routes that should be accessible by logged-in customers onl
To make a route protected, first, import the `authenticate` middleware: To make a route protected, first, import the `authenticate` middleware:
```js ```js
import authenticate from '@medusajs/medusa/dist/api/middlewares/authenticate' import authenticate from "@medusajs/medusa/dist/api/middlewares/authenticate"
``` ```
Then, add the middleware to your route: Then, add the middleware to your route:
```jsx ```jsx
router.get('/store/products/count', authenticate(), (req, res) => { router.get("/store/products/count", authenticate(), (req, res) => {
//... //...
}) })
``` ```
@@ -1,7 +1,5 @@
--- ---
title: Create a Service title: Create a Service
--- ---
# Create a Service # Create a Service
@@ -31,7 +29,7 @@ To create a service, you should create a JavaScript file in `src/services` to ho
For example, if you want to create a service `helloService`, create the file `hello.js` in `src/services` with the following content: For example, if you want to create a service `helloService`, create the file `hello.js` in `src/services` with the following content:
```js ```js
import { BaseService } from "medusa-interfaces"; import { BaseService } from "medusa-interfaces"
class HelloService extends BaseService { class HelloService extends BaseService {
getMessage() { getMessage() {
@@ -39,7 +37,7 @@ class HelloService extends BaseService {
} }
} }
export default HelloService; export default HelloService
``` ```
## Service Constructor ## Service Constructor
@@ -85,10 +83,10 @@ constructor({ helloService }) {
To use your custom service in an endpoint, you can use `req.scope.resolve` passing it the services registration name: To use your custom service in an endpoint, you can use `req.scope.resolve` passing it the services registration name:
```js ```js
const helloService = req.scope.resolve('helloService'); const helloService = req.scope.resolve("helloService")
res.json({ res.json({
message: helloService.getMessage() message: helloService.getMessage(),
}) })
``` ```
+1 -3
View File
@@ -1,7 +1,5 @@
--- ---
title: Introduction title: Introduction
--- ---
## Architecture overview ## Architecture overview
@@ -22,7 +20,7 @@ The admin dashboard is accessible by store operators. Store operators can use th
Medusa provides a beautiful [admin dashboard](https://demo.medusajs.com) that you can use right off the bat. Our admin dashboard provides a lot of functionalities to manage your store including Order management, product management, user management and more. Medusa provides a beautiful [admin dashboard](https://demo.medusajs.com) that you can use right off the bat. Our admin dashboard provides a lot of functionalities to manage your store including Order management, product management, user management and more.
You can also create your own admin dashboard by utilizing the [Admin REST APIs](https://docs.medusajs.com/api/admin/auth). You can also create your own admin dashboard by utilizing the [Admin REST APIs](https://docs.medusajs.com/api/admin/auth).
### Storefront ### Storefront
@@ -24,6 +24,7 @@ import AdminRegionsResource from "./regions"
import AdminNotificationsResource from "./notifications" import AdminNotificationsResource from "./notifications"
import AdminUploadsResource from "./uploads" import AdminUploadsResource from "./uploads"
import AdminProductTagsResource from "./product-tags" import AdminProductTagsResource from "./product-tags"
import AdminPriceListResource from "./price-lists"
class Admin extends BaseResource { class Admin extends BaseResource {
public auth = new AdminAuthResource(this.client) public auth = new AdminAuthResource(this.client)
@@ -35,6 +36,7 @@ class Admin extends BaseResource {
public giftCards = new AdminGiftCardsResource(this.client) public giftCards = new AdminGiftCardsResource(this.client)
public invites = new AdminInvitesResource(this.client) public invites = new AdminInvitesResource(this.client)
public notes = new AdminNotesResource(this.client) public notes = new AdminNotesResource(this.client)
public priceLists = new AdminPriceListResource(this.client)
public products = new AdminProductsResource(this.client) public products = new AdminProductsResource(this.client)
public productTags = new AdminProductTagsResource(this.client) public productTags = new AdminProductTagsResource(this.client)
public productTypes = new AdminProductTypesResource(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 * @param customHeaders
* @return { ResponsePromise<StoreCustomersRes>} * @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` const path = `/store/customers`
return this.client.request("POST", path, payload, {}, customHeaders) return this.client.request("POST", path, payload, {}, customHeaders)
} }
@@ -32,7 +35,9 @@ class CustomerResource extends BaseResource {
* @param customHeaders * @param customHeaders
* @return {ResponsePromise<StoreCustomersRes>} * @return {ResponsePromise<StoreCustomersRes>}
*/ */
retrieve(customHeaders: Record<string, any> = {}): ResponsePromise<StoreCustomersRes> { retrieve(
customHeaders: Record<string, any> = {}
): ResponsePromise<StoreCustomersRes> {
const path = `/store/customers/me` const path = `/store/customers/me`
return this.client.request("GET", path, {}, {}, customHeaders) return this.client.request("GET", path, {}, {}, customHeaders)
} }
@@ -45,7 +50,8 @@ class CustomerResource extends BaseResource {
*/ */
update( update(
payload: StorePostCustomersCustomerReq, payload: StorePostCustomersCustomerReq,
customHeaders: Record<string, any> = {}): ResponsePromise<StoreCustomersRes> { customHeaders: Record<string, any> = {}
): ResponsePromise<StoreCustomersRes> {
const path = `/store/customers/me` const path = `/store/customers/me`
return this.client.request("POST", path, payload, {}, customHeaders) return this.client.request("POST", path, payload, {}, customHeaders)
} }
@@ -58,7 +64,8 @@ class CustomerResource extends BaseResource {
*/ */
listOrders( listOrders(
params?: StoreGetCustomersCustomerOrdersParams, params?: StoreGetCustomersCustomerOrdersParams,
customHeaders: Record<string, any> = {}): ResponsePromise<StoreCustomersListOrdersRes> { customHeaders: Record<string, any> = {}
): ResponsePromise<StoreCustomersListOrdersRes> {
let path = `/store/customers/me/orders` let path = `/store/customers/me/orders`
if (params) { if (params) {
const query = qs.stringify(params) const query = qs.stringify(params)
@@ -77,7 +84,8 @@ class CustomerResource extends BaseResource {
*/ */
resetPassword( resetPassword(
payload: StorePostCustomersCustomerPasswordTokenReq, payload: StorePostCustomersCustomerPasswordTokenReq,
customHeaders: Record<string, any> = {}): ResponsePromise<StoreCustomersRes> { customHeaders: Record<string, any> = {}
): ResponsePromise<StoreCustomersRes> {
const path = `/store/customers/password-reset` const path = `/store/customers/password-reset`
return this.client.request("POST", path, payload, {}, customHeaders) return this.client.request("POST", path, payload, {}, customHeaders)
} }
@@ -1076,6 +1076,18 @@
"deleted_at": null, "deleted_at": null,
"metadata": 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": { "note": {
"id": "note_01F0ZXDBAB9KVZWJVW6115VEM7", "id": "note_01F0ZXDBAB9KVZWJVW6115VEM7",
"value": "customer contacted", "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) => { rest.post("/admin/return-reasons/", (req, res, ctx) => {
const body = req.body as Record<string, any> const body = req.body as Record<string, any>
return res( return res(
@@ -10,6 +10,7 @@ export * from "./orders"
export * from "./products" export * from "./products"
export * from "./product-tags" export * from "./product-tags"
export * from "./product-types" export * from "./product-types"
export * from "./price-lists"
export * from "./return-reasons" export * from "./return-reasons"
export * from "./regions" export * from "./regions"
export * from "./shipping-options" 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/regions"
export * from "./routes/admin/product-tags" export * from "./routes/admin/product-tags"
export * from "./routes/admin/product-types" export * from "./routes/admin/product-types"
export * from "./routes/admin/price-lists"
// Store // Store
export * from "./routes/store/auth" export * from "./routes/store/auth"
@@ -58,6 +58,12 @@ export type AdminPriceListRes = {
price_list: PriceList price_list: PriceList
} }
export type AdminPriceListDeleteBatchRes = {
ids: string[]
deleted: boolean
object: string
}
export type AdminPriceListDeleteRes = DeleteResponse export type AdminPriceListDeleteRes = DeleteResponse
export type AdminPriceListsListRes = PaginatedResponse & { export type AdminPriceListsListRes = PaginatedResponse & {
@@ -70,4 +76,5 @@ export * from "./delete-price-list"
export * from "./get-price-list" export * from "./get-price-list"
export * from "./list-price-lists" export * from "./list-price-lists"
export * from "./update-price-list" export * from "./update-price-list"
export * from "./delete-prices-batch"
export * from "./list-price-list-products" export * from "./list-price-list-products"
@@ -9,7 +9,6 @@ import {
ValidateNested, ValidateNested,
} from "class-validator" } from "class-validator"
import { omit } from "lodash" import { omit } from "lodash"
import { Product } from "../../../../models/product" import { Product } from "../../../../models/product"
import { DateComparisonOperator } from "../../../../types/common" import { DateComparisonOperator } from "../../../../types/common"
import { import {
@@ -5,7 +5,6 @@ import { MedusaError } from "medusa-core-utils"
import { Product } from "../../models/product" import { Product } from "../../models/product"
import { ProductService } from "../../services" import { ProductService } from "../../services"
import { getListConfig } from "../../utils/get-query-config" import { getListConfig } from "../../utils/get-query-config"
import { FindConfig } from "../../types/common"
import { FilterableProductProps } from "../../types/product" import { FilterableProductProps } from "../../types/product"
type ListContext = { type ListContext = {