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
parent fb33dbaca3
commit b164977a19
24 changed files with 838 additions and 276 deletions

View File

@@ -10,7 +10,7 @@ Thank you for considering contributing to Medusa! This document will outline how
## Issues before PRs ## Issues before PRs
1. Before you start working on a change please make sure that there is an issue for what you will be working on. You can either find and [existing issue](https://github.com/medusajs/medusa/issues) or [open a new issue](https://github.com/medusajs/medusa/issues/new) if none exists. Doing this makes sure that others can contribute with thoughts or suggest alternatives, ultimately making sure that we only add changes that make 1. Before you start working on a change please make sure that there is an issue for what you will be working on. You can either find and [existing issue](https://github.com/medusajs/medusa/issues) or [open a new issue](https://github.com/medusajs/medusa/issues/new) if none exists. Doing this makes sure that others can contribute with thoughts or suggest alternatives, ultimately making sure that we only add changes that make
2. When you are ready to start working on a change you should first [fork the Medusa repo](https://help.github.com/en/github/getting-started-with-github/fork-a-repo) and [branch out](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-and-deleting-branches-within-your-repository) from the `develop` branch. 2. When you are ready to start working on a change you should first [fork the Medusa repo](https://help.github.com/en/github/getting-started-with-github/fork-a-repo) and [branch out](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/creating-and-deleting-branches-within-your-repository) from the `develop` branch.
3. Make your changes. 3. Make your changes.
@@ -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
@@ -52,8 +56,8 @@ All PRs should include tests for the changes that are included. We have two type
### Documentation ### Documentation
- We generally encourage to document your changes through comments in your code. - We generally encourage to document your changes through comments in your code.
- If you alter user-facing behaviour you must provide documentation for such changes. - If you alter user-facing behaviour you must provide documentation for such changes.
- All methods and endpoints should be documented using [JSDoc](https://jsdoc.app/) and [`swagger-inline`](https://www.npmjs.com/package/swagger-inline) - All methods and endpoints should be documented using [JSDoc](https://jsdoc.app/) and [`swagger-inline`](https://www.npmjs.com/package/swagger-inline)
### Release ### Release

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,12 +75,13 @@ 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
- [ ] Multi-warehouse support - [ ] Multi-warehouse support
- [ ] GraphQL API - [ ] GraphQL API
## Setting up a storefront for your Medusa project ## Setting up a storefront for your Medusa project
Medusa is a headless commerce engine which means that it can be used for any type of digital commerce experience - you may use it as the backend for an app, a voice application, social commerce experiences or a traditional e-commerce website, you may even want to integrate Medusa into your own software to enable commerce functionality. All of these are use cases that Medusa supports - to learn more read the documentation or reach out. Medusa is a headless commerce engine which means that it can be used for any type of digital commerce experience - you may use it as the backend for an app, a voice application, social commerce experiences or a traditional e-commerce website, you may even want to integrate Medusa into your own software to enable commerce functionality. All of these are use cases that Medusa supports - to learn more read the documentation or reach out.

File diff suppressed because it is too large Load Diff

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.

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.

View File

@@ -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) => {
//... //...
}) })
``` ```

View File

@@ -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) => {
//... //...
}) })
``` ```

View File

@@ -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(),
}) })
``` ```

View File

@@ -1,7 +1,5 @@
--- ---
title: Introduction title: Introduction
--- ---
## Architecture overview ## Architecture overview
@@ -18,11 +16,11 @@ Your Medusa server will include all functionalities related to your stores ch
### Admin Dashboard ### Admin Dashboard
The admin dashboard is accessible by store operators. Store operators can use the admin dashboard to view, create, and modify data such as orders and products. The admin dashboard is accessible by store operators. Store operators can use the admin dashboard to view, create, and modify data such as orders and products.
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

View File

@@ -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)

View File

@@ -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

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)
} }

View File

@@ -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",

View File

@@ -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(

View File

@@ -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"

View File

@@ -0,0 +1,2 @@
export * from "./queries"
export * from "./mutations"

View File

@@ -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
)
)
}

View File

@@ -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
}

View File

@@ -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"),
},
})
)
})
})

View File

@@ -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)
})
})

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"

View File

@@ -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"

View File

@@ -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 {

View File

@@ -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 = {