From 563b725ed67c940e5e1bd407226186be1e66f961 Mon Sep 17 00:00:00 2001 From: Shahed Nasser Date: Wed, 4 Dec 2024 12:19:42 +0200 Subject: [PATCH 1/7] docs: revise brand API route (#10352) * docs: revise brand API route * address feedback * add directory images * change sidebar title * address comments --- .../custom-features/api-route/page.mdx | 161 +++++++++++++++--- .../custom-features/module/page.mdx | 10 +- .../customization/custom-features/page.mdx | 6 +- .../custom-features/workflow/page.mdx | 23 ++- www/apps/book/generated/edit-dates.mjs | 2 +- www/apps/book/sidebar.mjs | 2 +- 6 files changed, 170 insertions(+), 34 deletions(-) diff --git a/www/apps/book/app/learn/customization/custom-features/api-route/page.mdx b/www/apps/book/app/learn/customization/custom-features/api-route/page.mdx index 462629566288d..f4d424556be2e 100644 --- a/www/apps/book/app/learn/customization/custom-features/api-route/page.mdx +++ b/www/apps/book/app/learn/customization/custom-features/api-route/page.mdx @@ -1,16 +1,16 @@ import { Prerequisites } from "docs-ui" export const metadata = { - title: `${pageNumber} Create Brand API Route`, + title: `${pageNumber} Guide: Create Brand API Route`, } # {metadata.title} - +In the previous two chapters, you created a [Brand Module](../module/page.mdx) that added the concepts of brands to your application, then created a [workflow to create a brand](../workflow/page.mdx). In this chapter, you'll expose an API route that allows admin users to create a brand using the workflow from the previous chapter. -This chapter covers how to define an API route that creates a brand as the last step of the ["Build Custom Features" chapter](../page.mdx). +An API Route is an endpoint that acts as an entry point for other clients to interact with your Medusa customizations, such as the admin dashboard, storefronts, or third-party systems. - +The Medusa core application provides a set of [admin](!api!/admin) and [store](!api!/store) API routes out-of-the-box. You can also create custom API routes to expose your custom functionalities. -Create the file `src/api/admin/brands/route.ts` with the following content: +## 1. Create the API Route + +You create an API route in a `route.{ts,js}` file under a sub-directory of the `src/api` directory. The file exports API Route handler functions for at least one HTTP method (`GET`, `POST`, `DELETE`, etc…). + + + +Learn more about API routes [in this guide](../../../basics/api-routes/page.mdx). + + + +The route's path is the path of `route.{ts,js}` relative to `src/api`. So, to create the API route at `/admin/brands`, create the file `src/api/admin/brands/route.ts` with the following content: -```ts title="src/api/admin/brands/route.ts" collapsibleLines="1-9" expandButtonLabel="Show Imports" +![Directory structure of the Medusa application after adding the route](https://res.cloudinary.com/dza7lstvk/image/upload/v1732869882/Medusa%20Book/brand-route-dir-overview-2_hjqlnf.jpg) + +```ts title="src/api/admin/brands/route.ts" import { MedusaRequest, MedusaResponse, } from "@medusajs/framework/http" import { - CreateBrandInput, createBrandWorkflow, } from "../../../workflows/create-brand" +type PostAdminCreateBrandType = { + name: string +} + export const POST = async ( - req: MedusaRequest, + req: MedusaRequest, res: MedusaResponse ) => { const { result } = await createBrandWorkflow(req.scope) .run({ - input: req.body, + input: req.validatedBody, }) res.json({ brand: result }) } ``` -This adds a `POST` API route at `/admin/brands`. In the API route's handler, you execute the `createBrandWorkflow`, passing it the request body as input. +You export a route handler function with its name (`POST`) being the HTTP method of the API route you're exposing. + +The function receives two parameters: a `MedusaRequest` object to access request details, and `MedusaResponse` object to return or manipulate the response. The `MedusaRequest` object's `scope` property is the [Medusa container](../../../basics/medusa-container/page.mdx) that holds framework tools and custom and core modules' services. + + + +`MedusaRequest` accepts the request body's type as a type argument. + + -You return in the response the created brand. +In the API route's handler, you execute the `createBrandWorkflow` by invoking it and passing the Medusa container `req.scope` as a parameter, then invoking its `run` method. You pass the workflow's input in the `input` property of the `run` method's parameter. You pass the request body's parameters using the `validatedBody` property of `MedusaRequest`. + +You return a JSON response with the created brand using the `res.json` method. + +--- + +## 2. Create Validation Schema + +The API route you created accepts the brand's name in the request body. So, you'll create a schema used to validate incoming request body parameters. + +Medusa uses [Zod](https://zod.dev/) to create validation schemas. These schemas are then used to validate incoming request bodies or query parameters. -Learn more about API routes [in this guide](../../../basics/api-routes/page.mdx). +Learn more about API route validation in [this chapter](../../../advanced-development/api-routes/validation/page.mdx). + + + +You create a validation schema in a TypeScript or JavaScript file under a sub-directory of the `src/api` directory. So, create the file `src/api/admin/brands/validators.ts` with the following content: + +![Directory structure of Medusa application after adding validators file](https://res.cloudinary.com/dza7lstvk/image/upload/v1732869806/Medusa%20Book/brand-route-dir-overview-1_yfyjss.jpg) + +```ts title="src/api/admin/brands/validators.ts" +import { z } from "zod" + +export const PostAdminCreateBrand = z.object({ + name: z.string() +}) +``` + +You export a validation schema that expects in the request body an object having a `name` property whose value is a string. + +You can then replace `PostAdminCreateBrandType` in `src/api/admin/brands/route.ts` with the following: + +```ts title="src/api/admin/brands/route.ts" +// ... +import { z } from "zod" +import { PostAdminCreateBrand } from "./validators" + +type PostAdminCreateBrandType = z.infer + +// ... +``` + +--- + +## 3. Add Validation Middleware + +A middleware is a function executed before the route handler when a request is sent to an API Route. It's useful to guard API routes, parse custom request body types, and apply validation on an API route. + + + +Learn more about middlewares in [this chapter](../../../advanced-development/api-routes/middlewares/page.mdx). +Medusa provides a `validateAndTransformBody` middleware that accepts a Zod validation schema and returns a response error if a request is sent with body parameters that don't satisfy the validation schema. + +Middlewares are defined in the special file `src/api/middlewares.ts`. So, to add the validation middleware on the API route you created in the previous step, create the file `src/api/middlewares.ts` with the following content: + +![Directory structure of the Medusa application after adding the middleware](https://res.cloudinary.com/dza7lstvk/image/upload/v1732869977/Medusa%20Book/brand-route-dir-overview-3_kcx511.jpg) + +```ts title="src/api/middlewares.ts" +import { + defineMiddlewares, + validateAndTransformBody, +} from "@medusajs/framework/http" +import { PostAdminCreateBrand } from "./admin/brands/validators" + +export default defineMiddlewares({ + routes: [ + { + matcher: "/admin/brands", + method: "POST", + middlewares: [ + validateAndTransformBody(PostAdminCreateBrand), + ], + }, + ], +}) +``` + +You define the middlewares using the `defineMiddlewares` function and export its returned value. The function accepts an object having a `routes` property, which is an array of middleware objects. + +In the middleware object, you define three properties: + +- `matcher`: a string or regular expression indicating the API route path to apply the middleware on. You pass the create brand's route `/admin/brand`. +- `method`: The HTTP method to restrict the middleware to, which is `POST`. +- `middlewares`: An array of middlewares to apply on the route. You pass the `validateAndTransformBody` middleware, passing it the Zod schema you created earlier. + +The Medusa application will now validate the body parameters of `POST` requests sent to `/admin/brands` to ensure they match the Zod validation schema. If not, an error is returned in the response specifying the issues to fix in the request body. + --- ## Test API Route -To test it out, first, retrieve an authenticated token of your admin user by sending a `POST` request to the `/auth/user/emailpass` API Route: +To test out the API route, start the Medusa application with the following command: + +```bash npm2yarn +npm run dev +``` + +Since the `/admin/brands` API route has a `/admin` prefix, it's only accessible by authenticated admin users. + +So, to retrieve an authenticated token of your admin user, send a `POST` request to the `/auth/user/emailpass` API Route: ```bash curl -X POST 'http://localhost:9000/auth/user/emailpass' \ @@ -71,7 +186,13 @@ curl -X POST 'http://localhost:9000/auth/user/emailpass' \ }' ``` -Make sure to replace the email and password with your user's credentials. +Make sure to replace the email and password with your admin user's credentials. + + + +Don't have an admin user? Refer to [this guide](../../../installation/page.mdx#create-medusa-admin-user). + + Then, send a `POST` request to `/admin/brands`, passing the token received from the previous request in the `Authorization` header: @@ -101,14 +222,16 @@ This returns the created brand in the response: ## Summary -By following the previous example chapters, you implemented a custom feature that allows admin users to create a brand by: +By following the previous example chapters, you implemented a custom feature that allows admin users to create a brand. You did that by: -1. Creating a module that defines and manages the `Brand` data model. -2. Creating a workflow that uses the module's main service to create a brand record, and implements the compensation logic to delete that brand in case an error occurs. +1. Creating a module that defines and manages a `brand` table in the database. +2. Creating a workflow that uses the module's service to create a brand record, and implements the compensation logic to delete that brand in case an error occurs. 3. Creating an API route that allows admin users to create a brand. --- -## Next Steps +## Next Steps: Associate Brand with Product + +Now that you have brands in your Medusa application, you want to associate a brand with a product, which is defined in the [Product Module](!resources!/commerce-modules/product). -In the next chapters, you'll learn how to extend data models and associate the brand with a product. +In the next chapters, you'll learn how to build associations between data models defined in different modules. diff --git a/www/apps/book/app/learn/customization/custom-features/module/page.mdx b/www/apps/book/app/learn/customization/custom-features/module/page.mdx index 1828510dced44..07cd352b312d1 100644 --- a/www/apps/book/app/learn/customization/custom-features/module/page.mdx +++ b/www/apps/book/app/learn/customization/custom-features/module/page.mdx @@ -8,7 +8,7 @@ In this chapter, you'll build a Brand Module that adds a `brand` table to the da A module is a reusable package of functionalities related to a single domain or integration. Medusa comes with multiple pre-built modules for core commerce needs, such as the [Cart Module](!resources!/commerce-modules/cart) that holds the data models and business logic for cart operations. -You create in a module new tables in the database, and expose a class that provides data-management methods on those tables. In the next chapters, you'll see how you use the module's functionalities to expose commerce features. +In a module, you create data models and business logic to manage them. In the next chapters, you'll see how you use the module to build commerce features. @@ -20,6 +20,8 @@ Learn more about modules in [this chapter](../../../basics/modules/page.mdx). Modules are created in a sub-directory of `src/modules`. So, start by creating the directory `src/modules/brand` that will hold the Brand Module's files. +![Directory structure in Medusa project after adding the brand directory](https://res.cloudinary.com/dza7lstvk/image/upload/v1732868844/Medusa%20Book/brand-dir-overview-1_hxwvgx.jpg) + --- ## 2. Create Data Model @@ -34,6 +36,8 @@ Learn more about data models in [this chapter](../../../basics/modules/page.mdx# You create a data model in a TypeScript or JavaScript file under the `models` directory of a module. So, to create a data model that represents a new `brand` table in the database, create the file `src/modules/brand/models/brand.ts` with the following content: +![Directory structure in module after adding the brand data model](https://res.cloudinary.com/dza7lstvk/image/upload/v1732868920/Medusa%20Book/brand-dir-overview-2_lexhdl.jpg) + ```ts title="src/modules/brand/models/brand.ts" import { model } from "@medusajs/framework/utils" @@ -72,6 +76,8 @@ Learn more about services in [this chapter](../../../basics/modules/page.mdx#2-c You define a service in a `service.ts` or `service.js` file at the root of your module's directory. So, create the file `src/modules/brand/service.ts` with the following content: +![Directory structure in module after adding the service](https://res.cloudinary.com/dza7lstvk/image/upload/v1732868984/Medusa%20Book/brand-dir-overview-3_jo7baj.jpg) + export const serviceHighlights = [ ["4", "MedusaService", "A service factory that generates data-management methods."] ] @@ -109,6 +115,8 @@ A module must export a definition that tells Medusa the name of the module and i So, to export the Brand Module's definition, create the file `src/modules/brand/index.ts` with the following content: +![Directory structure in module after adding the definition file](https://res.cloudinary.com/dza7lstvk/image/upload/v1732869045/Medusa%20Book/brand-dir-overview-4_nf8ymw.jpg) + ```ts title="src/modules/brand/index.ts" import { Module } from "@medusajs/framework/utils" import BrandModuleService from "./service" diff --git a/www/apps/book/app/learn/customization/custom-features/page.mdx b/www/apps/book/app/learn/customization/custom-features/page.mdx index 47622a66c9360..3b3a1268c6662 100644 --- a/www/apps/book/app/learn/customization/custom-features/page.mdx +++ b/www/apps/book/app/learn/customization/custom-features/page.mdx @@ -8,10 +8,10 @@ In the upcoming chapters, you'll follow step-by-step guides to build custom feat By following these guides, you'll add brands to the Medusa application that you can associate with products. -To build a custom feature in Medusa, you need three main ingredients: +To build a custom feature in Medusa, you need three main tools: -- [Module](../../basics/modules/page.mdx): a re-usable package that defines commerce functionalities for a single domain. It defines new tables to add to the database, and a class of methods to manage these tables. -- [Workflow](../../basics/workflows/page.mdx): a special function that performs a task in a series of steps with advanced features like roll-back mechanism and retry configurations. The steps of a workflow use functionalities implemented by modules. +- [Module](../../basics/modules/page.mdx): a package with commerce logic for a single domain. It defines new tables to add to the database, and a class of methods to manage these tables. +- [Workflow](../../basics/workflows/page.mdx): a tool to perform an operation comprising multiple steps with built-in rollback and retry mechanisms. - [API route](../../basics/api-routes/page.mdx): a REST endpoint that exposes commerce features to clients, such as the admin dashboard or a storefront. The API route executes a workflow that implements the commerce feature using modules. ![Diagram showcasing the flow of a custom developed feature](https://res.cloudinary.com/dza7lstvk/image/upload/v1725867628/Medusa%20Book/custom-development_nofvp6.jpg) diff --git a/www/apps/book/app/learn/customization/custom-features/workflow/page.mdx b/www/apps/book/app/learn/customization/custom-features/workflow/page.mdx index 79f0d68a3bd4a..3d5b40bd3f4ae 100644 --- a/www/apps/book/app/learn/customization/custom-features/workflow/page.mdx +++ b/www/apps/book/app/learn/customization/custom-features/workflow/page.mdx @@ -6,9 +6,9 @@ export const metadata = { # {metadata.title} -This chapter is a follow-up to the [previous one](../module/page.mdx) where you created a Brand Module. In this chapter, you'll create a workflow that creates a brand. +This chapter builds on the work from the [previous chapter](../module/page.mdx) where you created a Brand Module. -You implement commerce features within workflows. A workflow is a series of queries and actions, called steps, that complete a task. You construct a workflow similar to a regular function, but it's a special function that allows you to define roll-back logic, retry configurations, and more advanced features. +After adding custom brands to your application, you build commerce features around them using workflows. A workflow is a series of queries and actions, called steps, that complete a task spanning across modules. You construct a workflow similar to a regular function, but it's a special function that allows you to define roll-back logic, retry configurations, and more advanced features. The workflow you'll create in this chapter will use the Brand Module's service to implement the feature of creating a brand. In the [next chapter](../api-route/page.mdx), you'll expose an API route that allows admin users to create a brand, and you'll use this workflow in the route's implementation. @@ -33,9 +33,11 @@ Learn more about workflows in [this chapter](../../../basics/workflows/page.mdx) A workflow consists of a series of steps, each step created in a TypeScript or JavaScript file under the `src/workflows` directory. A step is defined using the `createStep` utility function imported from `@medusajs/framework/workflows-sdk`. -The workflow you're creating in this guide has one step to create the brand. So, create the file `src/workflows/create-brand/steps/create-brand.ts` with the following content: +The workflow you're creating in this guide has one step to create the brand. So, create the file `src/workflows/create-brand.ts` with the following content: -```ts title="src/workflows/create-brand/steps/create-brand.ts" +![Directory structure in the Medusa project after adding the file for createBrandStep](https://res.cloudinary.com/dza7lstvk/image/upload/v1732869184/Medusa%20Book/brand-workflow-dir-overview-1_fjvf5j.jpg) + +```ts title="src/workflows/create-brand.ts" import { createStep, StepResponse, @@ -89,7 +91,7 @@ Learn more about the compensation function in [this chapter](../../../advanced-d To add a compensation function to the `createBrandStep`, pass it as a third parameter to `createStep`: -```ts title="src/workflows/create-brand/steps/create-brand.ts" +```ts title="src/workflows/create-brand.ts" export const createBrandStep = createStep( // ... async (id: string, { container }) => { @@ -120,22 +122,25 @@ So, if an error occurs during the workflow's execution, the brand that was creat You can now create the workflow that runs the `createBrandStep`. A workflow is created in a TypeScript or JavaScript file under the `src/workflows` directory. In the file, you use the `createWorkflow` function imported from `@medusajs/framework/workflows-sdk` to create the workflow. -So, create the file `src/workflows/create-brand/index.ts` with the following content: +Add the following content in the same `src/workflows/create-brand.ts` file: ```ts +// other imports... import { + // ... createWorkflow, WorkflowResponse, } from "@medusajs/framework/workflows-sdk" -import { createBrandStep } from "./steps/create-brand" -type CreateBrandInput = { +// ... + +type CreateBrandWorkflowInput = { name: string } export const createBrandWorkflow = createWorkflow( "create-brand", - (input: CreateBrandInput) => { + (input: CreateBrandWorkflowInput) => { const brand = createBrandStep(input) return new WorkflowResponse(brand) diff --git a/www/apps/book/generated/edit-dates.mjs b/www/apps/book/generated/edit-dates.mjs index 1924dae459282..d536e4c167549 100644 --- a/www/apps/book/generated/edit-dates.mjs +++ b/www/apps/book/generated/edit-dates.mjs @@ -89,7 +89,7 @@ export const generatedEditDates = { "app/learn/advanced-development/api-routes/additional-data/page.mdx": "2024-09-30T08:43:53.120Z", "app/learn/advanced-development/workflows/page.mdx": "2024-09-18T08:00:57.364Z", "app/learn/advanced-development/workflows/variable-manipulation/page.mdx": "2024-11-14T16:11:24.538Z", - "app/learn/customization/custom-features/api-route/page.mdx": "2024-09-12T12:42:34.201Z", + "app/learn/customization/custom-features/api-route/page.mdx": "2024-11-28T13:12:10.521Z", "app/learn/customization/custom-features/module/page.mdx": "2024-11-28T09:25:29.098Z", "app/learn/customization/custom-features/workflow/page.mdx": "2024-11-28T10:47:28.084Z", "app/learn/customization/extend-models/create-links/page.mdx": "2024-09-30T08:43:53.133Z", diff --git a/www/apps/book/sidebar.mjs b/www/apps/book/sidebar.mjs index 2351f362627fe..fd0616167d538 100644 --- a/www/apps/book/sidebar.mjs +++ b/www/apps/book/sidebar.mjs @@ -108,7 +108,7 @@ export const sidebar = numberSidebarItems( }, { type: "link", - title: "Create Brand API Route", + title: "Brand API Route", path: "/learn/customization/custom-features/api-route", }, ], From 1f3754a75e65dff62b66b0077c36af59925b2cf7 Mon Sep 17 00:00:00 2001 From: "Carlos R. L. Rodrigues" <37986729+carlos-r-l-rodrigues@users.noreply.github.com> Date: Wed, 4 Dec 2024 07:32:03 -0300 Subject: [PATCH 2/7] chore(workflows-sdk): add unit test (#10419) --- .../src/utils/composer/__tests__/compose.ts | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/packages/core/workflows-sdk/src/utils/composer/__tests__/compose.ts b/packages/core/workflows-sdk/src/utils/composer/__tests__/compose.ts index db58a02064232..7e46d2d164925 100644 --- a/packages/core/workflows-sdk/src/utils/composer/__tests__/compose.ts +++ b/packages/core/workflows-sdk/src/utils/composer/__tests__/compose.ts @@ -2596,4 +2596,42 @@ describe("Workflow composer", function () { }), }) }) + + it("should compose the workflow passing nested references to objects", async () => { + const mockStep1Fn = jest.fn().mockImplementation(() => { + return [1, 2, 3, 4, { obj: "return from 1" }] + }) + const mockStep2Fn = jest.fn().mockImplementation((inp) => { + return { + a: { + b: { + c: [ + 0, + [ + { + inp, + }, + ], + ], + }, + }, + } + }) + + const step1 = createStep("step1", mockStep1Fn) as any + const step2 = createStep("step2", mockStep2Fn) as any + + const workflow = createWorkflow("workflow1", function () { + const returnStep1 = step1() + const ret2 = step2(returnStep1[4]) + return new WorkflowResponse(ret2.a.b.c[1][0].inp.obj) + }) + + const workflowInput = { test: "payload1" } + const { result: workflowResult } = await workflow().run({ + input: workflowInput, + }) + + expect(workflowResult).toEqual("return from 1") + }) }) From 582f52b31d822d2f0f3e7af334b5aba7cd12dd65 Mon Sep 17 00:00:00 2001 From: Shahed Nasser Date: Wed, 4 Dec 2024 12:54:19 +0200 Subject: [PATCH 3/7] docs-util: add user, pricing, and auth as modules with DML (#10423) --- .../src/constants/references-details.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/www/utils/packages/typedoc-generate-references/src/constants/references-details.ts b/www/utils/packages/typedoc-generate-references/src/constants/references-details.ts index 5334048f83ef0..7b702b19e16ab 100644 --- a/www/utils/packages/typedoc-generate-references/src/constants/references-details.ts +++ b/www/utils/packages/typedoc-generate-references/src/constants/references-details.ts @@ -35,4 +35,11 @@ export const customModulesOptions: Record> = { } // a list of modules that now support DML -export const dmlModules = ["currency", "region", "product"] +export const dmlModules = [ + "currency", + "region", + "product", + "user", + "auth", + "pricing", +] From a5263083fae89d6885b291b9cb06de8d74aec074 Mon Sep 17 00:00:00 2001 From: Kasper Fabricius Kristensen <45367945+kasperkristensen@users.noreply.github.com> Date: Wed, 4 Dec 2024 14:49:43 +0100 Subject: [PATCH 4/7] fix(framework): Ensure that CORS and Auth middleware is applied for routes only defined in middlewares.ts (#10339) --- .changeset/nice-tools-sell.md | 5 + packages/core/framework/package.json | 1 + packages/core/framework/src/http/router.ts | 150 +++++++++++++++------ yarn.lock | 10 ++ 4 files changed, 125 insertions(+), 41 deletions(-) create mode 100644 .changeset/nice-tools-sell.md diff --git a/.changeset/nice-tools-sell.md b/.changeset/nice-tools-sell.md new file mode 100644 index 0000000000000..a0bc59b2503f1 --- /dev/null +++ b/.changeset/nice-tools-sell.md @@ -0,0 +1,5 @@ +--- +"@medusajs/framework": patch +--- + +fix(framework): Apply CORS and auth middleware for global middleware that is not already applied by routes diff --git a/packages/core/framework/package.json b/packages/core/framework/package.json index ca1e715712732..47338c9ab59eb 100644 --- a/packages/core/framework/package.json +++ b/packages/core/framework/package.json @@ -59,6 +59,7 @@ "@mikro-orm/postgresql": "5.9.7", "@swc/core": "^1.7.28", "@swc/jest": "^0.2.36", + "@types/cors": "^2.8.17", "@types/jsonwebtoken": "^8.5.9", "awilix": "^8.0.1", "ioredis": "^5.4.1", diff --git a/packages/core/framework/src/http/router.ts b/packages/core/framework/src/http/router.ts index 90124df53df8b..ed375548e9712 100644 --- a/packages/core/framework/src/http/router.ts +++ b/packages/core/framework/src/http/router.ts @@ -183,6 +183,44 @@ function getBodyParserMiddleware(args?: ParserConfigArgs) { ] } +function createCorsOptions(origin: string): cors.CorsOptions { + return { + origin: parseCorsOrigins(origin), + credentials: true, + } +} + +function applyCors( + router: Router, + route: string | RegExp, + corsConfig: cors.CorsOptions +) { + router.use(route, cors(corsConfig)) +} + +function getRouteContext( + path: string | RegExp +): "admin" | "store" | "auth" | null { + /** + * We cannot reliably guess the route context from a regex, so we skip it. + */ + if (path instanceof RegExp) { + return null + } + + if (path.startsWith("/admin")) { + return "admin" + } + if (path.startsWith("/store")) { + return "store" + } + if (path.startsWith("/auth")) { + return "auth" + } + + return null +} + // TODO this router would need a proper rework, but it is out of scope right now export class ApiRoutesLoader { @@ -589,13 +627,15 @@ export class ApiRoutesLoader { /** * Applies middleware that checks if a valid publishable key is set on store request */ - applyStorePublishableKeyMiddleware(route: string) { + applyStorePublishableKeyMiddleware(route: string | RegExp) { let middleware = ensurePublishableApiKeyMiddleware as unknown as | RequestHandler | MiddlewareFunction if (ApiRoutesLoader.traceMiddleware) { - middleware = ApiRoutesLoader.traceMiddleware(middleware, { route: route }) + middleware = ApiRoutesLoader.traceMiddleware(middleware, { + route: String(route), + }) } this.#router.use(route, middleware as RequestHandler) @@ -606,7 +646,7 @@ export class ApiRoutesLoader { * needed to pass the middleware via the trace calls */ applyAuthMiddleware( - route: string, + route: string | RegExp, actorType: string | string[], authType: AuthType | AuthType[], options?: { allowUnauthenticated?: boolean; allowUnregistered?: boolean } @@ -617,7 +657,7 @@ export class ApiRoutesLoader { if (ApiRoutesLoader.traceMiddleware) { authenticateMiddleware = ApiRoutesLoader.traceMiddleware( authenticateMiddleware, - { route: route } + { route: String(route) } ) } @@ -632,6 +672,14 @@ export class ApiRoutesLoader { */ applyRouteSpecificMiddlewares(): void { const prioritizedRoutes = prioritize([...this.#routesMap.values()]) + const handledPaths = new Set() + const middlewarePaths = new Set() + + const globalRoutes = this.#globalMiddlewaresDescriptor?.config?.routes ?? [] + + for (const route of globalRoutes) { + middlewarePaths.add(route.matcher) + } for (const descriptor of prioritizedRoutes) { if (!descriptor.config?.routes?.length) { @@ -639,58 +687,33 @@ export class ApiRoutesLoader { } const config = descriptor.config - const routes = descriptor.config.routes - - /** - * Apply default store and admin middlewares if - * not opted out of. - */ + handledPaths.add(descriptor.route) if (config.shouldAppendAdminCors) { - /** - * Apply the admin cors - */ - this.#router.use( + applyCors( + this.#router, descriptor.route, - cors({ - origin: parseCorsOrigins( - configManager.config.projectConfig.http.adminCors - ), - credentials: true, - }) + createCorsOptions(configManager.config.projectConfig.http.adminCors) ) } if (config.shouldAppendAuthCors) { - /** - * Apply the auth cors - */ - this.#router.use( + applyCors( + this.#router, descriptor.route, - cors({ - origin: parseCorsOrigins( - configManager.config.projectConfig.http.authCors - ), - credentials: true, - }) + createCorsOptions(configManager.config.projectConfig.http.authCors) ) } if (config.shouldAppendStoreCors) { - /** - * Apply the store cors - */ - this.#router.use( + applyCors( + this.#router, descriptor.route, - cors({ - origin: parseCorsOrigins( - configManager.config.projectConfig.http.storeCors - ), - credentials: true, - }) + createCorsOptions(configManager.config.projectConfig.http.storeCors) ) } + // Apply other middlewares if (config.routeType === "store") { this.applyStorePublishableKeyMiddleware(descriptor.route) } @@ -715,7 +738,7 @@ export class ApiRoutesLoader { ]) } - for (const route of routes) { + for (const route of descriptor.config.routes) { /** * Apply the body parser middleware if the route * has not opted out of it. @@ -723,6 +746,51 @@ export class ApiRoutesLoader { this.applyBodyParserMiddleware(descriptor.route, route.method!) } } + + /** + * Apply CORS and auth middleware for paths defined in global middleware but not already handled by routes. + */ + for (const path of middlewarePaths) { + if (typeof path === "string" && handledPaths.has(path)) { + continue + } + + const context = getRouteContext(path) + + if (!context) { + continue + } + + switch (context) { + case "admin": + applyCors( + this.#router, + path, + createCorsOptions(configManager.config.projectConfig.http.adminCors) + ) + this.applyAuthMiddleware(path, "user", [ + "bearer", + "session", + "api-key", + ]) + break + case "store": + applyCors( + this.#router, + path, + createCorsOptions(configManager.config.projectConfig.http.storeCors) + ) + this.applyStorePublishableKeyMiddleware(path) + break + case "auth": + applyCors( + this.#router, + path, + createCorsOptions(configManager.config.projectConfig.http.authCors) + ) + break + } + } } /** diff --git a/yarn.lock b/yarn.lock index 3c41ebc51b273..0e2a5efeda90a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5754,6 +5754,7 @@ __metadata: "@opentelemetry/api": ^1.9.0 "@swc/core": ^1.7.28 "@swc/jest": ^0.2.36 + "@types/cors": ^2.8.17 "@types/express": ^4.17.17 "@types/jsonwebtoken": ^8.5.9 awilix: ^8.0.1 @@ -13414,6 +13415,15 @@ __metadata: languageName: node linkType: hard +"@types/cors@npm:^2.8.17": + version: 2.8.17 + resolution: "@types/cors@npm:2.8.17" + dependencies: + "@types/node": "*" + checksum: 457364c28c89f3d9ed34800e1de5c6eaaf344d1bb39af122f013322a50bc606eb2aa6f63de4e41a7a08ba7ef454473926c94a830636723da45bf786df032696d + languageName: node + linkType: hard + "@types/doctrine@npm:^0.0.9": version: 0.0.9 resolution: "@types/doctrine@npm:0.0.9" From 340769595a8813674a500d4883c03881a394df32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Frane=20Poli=C4=87?= <16856471+fPolic@users.noreply.github.com> Date: Wed, 4 Dec 2024 14:56:40 +0100 Subject: [PATCH 5/7] feat(admin, js-sdk, types): update order forms (#10418) * feat: edit shipping, billing and email forms * feat: timeline history, update change model, update tests * fix: address comments * fix: invalidation, translation schema, update label * fix: old/new --- .../http/__tests__/order/admin/order.spec.ts | 91 ++++++-- .../components/common/user-link/user-link.tsx | 11 + .../admin/dashboard/src/hooks/api/orders.tsx | 31 +++ .../src/i18n/translations/$schema.json | 97 +++++++- .../dashboard/src/i18n/translations/en.json | 24 +- .../providers/router-provider/route-map.tsx | 14 ++ .../change-details-tooltip.tsx | 74 ++++++ .../order-activity-section/order-timeline.tsx | 89 +++++++- .../edit-order-billing-address-form.tsx | 216 ++++++++++++++++++ .../edit-order-billing-address-form/index.ts | 1 + .../order-edit-billing-address/index.ts | 1 + .../order-edit-billing-address.tsx | 31 +++ .../edit-order-email-form.tsx | 95 ++++++++ .../components/edit-order-email-form/index.ts | 1 + .../routes/orders/order-edit-email/index.ts | 1 + .../order-edit-email/order-edit-email.tsx | 31 +++ .../edit-order-shipping-address-form.tsx | 215 +++++++++++++++++ .../edit-order-shipping-address-form/index.ts | 1 + .../order-edit-shipping-address/index.ts | 1 + .../order-edit-shipping-address.tsx | 31 +++ .../src/order/workflows/update-order.ts | 81 ++++--- packages/core/js-sdk/src/admin/order.ts | 41 ++++ .../types/src/http/order/admin/payload.ts | 72 ++++++ packages/core/types/src/order/mutations.ts | 9 +- .../types/src/workflow/order/update-order.ts | 1 + .../medusa/src/api/admin/orders/[id]/route.ts | 1 + .../src/services/order-module-service.ts | 4 +- 27 files changed, 1202 insertions(+), 63 deletions(-) create mode 100644 packages/admin/dashboard/src/routes/orders/order-detail/components/order-activity-section/change-details-tooltip.tsx create mode 100644 packages/admin/dashboard/src/routes/orders/order-edit-billing-address/components/edit-order-billing-address-form/edit-order-billing-address-form.tsx create mode 100644 packages/admin/dashboard/src/routes/orders/order-edit-billing-address/components/edit-order-billing-address-form/index.ts create mode 100644 packages/admin/dashboard/src/routes/orders/order-edit-billing-address/index.ts create mode 100644 packages/admin/dashboard/src/routes/orders/order-edit-billing-address/order-edit-billing-address.tsx create mode 100644 packages/admin/dashboard/src/routes/orders/order-edit-email/components/edit-order-email-form/edit-order-email-form.tsx create mode 100644 packages/admin/dashboard/src/routes/orders/order-edit-email/components/edit-order-email-form/index.ts create mode 100644 packages/admin/dashboard/src/routes/orders/order-edit-email/index.ts create mode 100644 packages/admin/dashboard/src/routes/orders/order-edit-email/order-edit-email.tsx create mode 100644 packages/admin/dashboard/src/routes/orders/order-edit-shipping-address/components/edit-order-shipping-address-form/edit-order-shipping-address-form.tsx create mode 100644 packages/admin/dashboard/src/routes/orders/order-edit-shipping-address/components/edit-order-shipping-address-form/index.ts create mode 100644 packages/admin/dashboard/src/routes/orders/order-edit-shipping-address/index.ts create mode 100644 packages/admin/dashboard/src/routes/orders/order-edit-shipping-address/order-edit-shipping-address.tsx diff --git a/integration-tests/http/__tests__/order/admin/order.spec.ts b/integration-tests/http/__tests__/order/admin/order.spec.ts index 8d6de95a28573..1bb66dd8f5a42 100644 --- a/integration-tests/http/__tests__/order/admin/order.spec.ts +++ b/integration-tests/http/__tests__/order/admin/order.spec.ts @@ -76,18 +76,41 @@ medusaIntegrationTestRunner({ version: 1, change_type: "update_order", status: "confirmed", + created_by: expect.any(String), + confirmed_by: expect.any(String), confirmed_at: expect.any(String), actions: expect.arrayContaining([ expect.objectContaining({ version: 1, applied: true, - reference_id: addressBefore.id, - reference: "shipping_address", action: "UPDATE_ORDER_PROPERTIES", - details: { - city: "New New York", - address_1: "New Main street 123", - }, + details: expect.objectContaining({ + type: "shipping_address", + old: expect.objectContaining({ + address_1: addressBefore.address_1, + city: addressBefore.city, + country_code: addressBefore.country_code, + province: addressBefore.province, + postal_code: addressBefore.postal_code, + phone: addressBefore.phone, + company: addressBefore.company, + first_name: addressBefore.first_name, + last_name: addressBefore.last_name, + address_2: addressBefore.address_2, + }), + new: expect.objectContaining({ + address_1: "New Main street 123", + city: "New New York", + country_code: addressBefore.country_code, + province: addressBefore.province, + postal_code: addressBefore.postal_code, + phone: addressBefore.phone, + company: addressBefore.company, + first_name: addressBefore.first_name, + last_name: addressBefore.last_name, + address_2: addressBefore.address_2, + }), + }), }), ]), }) @@ -162,18 +185,33 @@ medusaIntegrationTestRunner({ version: 1, change_type: "update_order", status: "confirmed", + created_by: expect.any(String), + confirmed_by: expect.any(String), confirmed_at: expect.any(String), actions: expect.arrayContaining([ expect.objectContaining({ version: 1, applied: true, - reference_id: addressBefore.id, - reference: "billing_address", action: "UPDATE_ORDER_PROPERTIES", - details: { - city: "New New York", - address_1: "New Main street 123", - }, + details: expect.objectContaining({ + type: "billing_address", + old: expect.objectContaining({ + address_1: addressBefore.address_1, + city: addressBefore.city, + country_code: addressBefore.country_code, + province: addressBefore.province, + postal_code: addressBefore.postal_code, + phone: addressBefore.phone, + }), + new: expect.objectContaining({ + address_1: "New Main street 123", + city: "New New York", + country_code: addressBefore.country_code, + province: addressBefore.province, + postal_code: addressBefore.postal_code, + phone: addressBefore.phone, + }), + }), }), ]), }) @@ -239,16 +277,23 @@ medusaIntegrationTestRunner({ change_type: "update_order", status: "confirmed", confirmed_at: expect.any(String), + created_by: expect.any(String), + confirmed_by: expect.any(String), actions: expect.arrayContaining([ expect.objectContaining({ version: 1, applied: true, - reference_id: order.shipping_address.id, - reference: "shipping_address", action: "UPDATE_ORDER_PROPERTIES", - details: { - address_1: "New Main street 123", - }, + details: expect.objectContaining({ + type: "shipping_address", + old: expect.objectContaining({ + address_1: order.shipping_address.address_1, + city: order.shipping_address.city, + }), + new: expect.objectContaining({ + address_1: "New Main street 123", + }), + }), }), ]), }), @@ -257,16 +302,18 @@ medusaIntegrationTestRunner({ change_type: "update_order", status: "confirmed", confirmed_at: expect.any(String), + created_by: expect.any(String), + confirmed_by: expect.any(String), actions: expect.arrayContaining([ expect.objectContaining({ version: 1, applied: true, - reference_id: order.email, - reference: "email", action: "UPDATE_ORDER_PROPERTIES", - details: { - email: "new-email@example.com", - }, + details: expect.objectContaining({ + type: "email", + old: order.email, + new: "new-email@example.com", + }), }), ]), }), diff --git a/packages/admin/dashboard/src/components/common/user-link/user-link.tsx b/packages/admin/dashboard/src/components/common/user-link/user-link.tsx index 1a23adcf617ab..239abb8b085a8 100644 --- a/packages/admin/dashboard/src/components/common/user-link/user-link.tsx +++ b/packages/admin/dashboard/src/components/common/user-link/user-link.tsx @@ -1,5 +1,6 @@ import { Avatar, Text } from "@medusajs/ui" import { Link } from "react-router-dom" +import { useUser } from "../../../hooks/api/users" type UserLinkProps = { id: string @@ -32,3 +33,13 @@ export const UserLink = ({ ) } + +export const By = ({ id }: { id: string }) => { + const { user } = useUser(id) // todo: extend to support customers + + if (!user) { + return null + } + + return +} diff --git a/packages/admin/dashboard/src/hooks/api/orders.tsx b/packages/admin/dashboard/src/hooks/api/orders.tsx index 4e092a4b70132..4440bb7942e15 100644 --- a/packages/admin/dashboard/src/hooks/api/orders.tsx +++ b/packages/admin/dashboard/src/hooks/api/orders.tsx @@ -51,6 +51,37 @@ export const useOrder = ( return { ...data, ...rest } } +export const useUpdateOrder = ( + id: string, + options?: UseMutationOptions< + HttpTypes.AdminOrderResponse, + FetchError, + HttpTypes.AdminUpdateOrder + > +) => { + return useMutation({ + mutationFn: (payload: HttpTypes.AdminUpdateOrder) => + sdk.admin.order.update(id, payload), + onSuccess: (data: any, variables: any, context: any) => { + queryClient.invalidateQueries({ + queryKey: ordersQueryKeys.detail(id), + }) + + queryClient.invalidateQueries({ + queryKey: ordersQueryKeys.changes(id), + }) + + // TODO: enable when needed + // queryClient.invalidateQueries({ + // queryKey: ordersQueryKeys.lists(), + // }) + + options?.onSuccess?.(data, variables, context) + }, + ...options, + }) +} + export const useOrderPreview = ( id: string, query?: HttpTypes.AdminOrderFilters, diff --git a/packages/admin/dashboard/src/i18n/translations/$schema.json b/packages/admin/dashboard/src/i18n/translations/$schema.json index b4c6e4c51dc7a..ff3a6d5af3d06 100644 --- a/packages/admin/dashboard/src/i18n/translations/$schema.json +++ b/packages/admin/dashboard/src/i18n/translations/$schema.json @@ -4146,6 +4146,65 @@ ], "additionalProperties": false }, + "edit": { + "type": "object", + "properties": { + "email": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "requestSuccess": { + "type": "string" + } + }, + "required": [ + "title", + "requestSuccess" + ], + "additionalProperties": false + }, + "shippingAddress": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "requestSuccess": { + "type": "string" + } + }, + "required": [ + "title", + "requestSuccess" + ], + "additionalProperties": false + }, + "billingAddress": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "requestSuccess": { + "type": "string" + } + }, + "required": [ + "title", + "requestSuccess" + ], + "additionalProperties": false + } + }, + "required": [ + "email", + "shippingAddress", + "billingAddress" + ], + "additionalProperties": false + }, "returns": { "type": "object", "properties": { @@ -5364,6 +5423,26 @@ "declined" ], "additionalProperties": false + }, + "update_order": { + "type": "object", + "properties": { + "shipping_address": { + "type": "string" + }, + "billing_address": { + "type": "string" + }, + "email": { + "type": "string" + } + }, + "required": [ + "shipping_address", + "billing_address", + "email" + ], + "additionalProperties": false } }, "required": [ @@ -5377,7 +5456,8 @@ "claim", "exchange", "edit", - "transfer" + "transfer", + "update_order" ], "additionalProperties": false } @@ -5426,6 +5506,7 @@ "transfer", "payment", "edits", + "edit", "returns", "claims", "exchanges", @@ -10849,6 +10930,12 @@ }, "removed": { "type": "string" + }, + "from": { + "type": "string" + }, + "to": { + "type": "string" } }, "required": [ @@ -10857,7 +10944,9 @@ "available", "inStock", "added", - "removed" + "removed", + "from", + "to" ], "additionalProperties": false }, @@ -10951,6 +11040,9 @@ "subtitle": { "type": "string" }, + "by": { + "type": "string" + }, "item": { "type": "string" }, @@ -11360,6 +11452,7 @@ "discountable", "handle", "subtitle", + "by", "item", "qty", "limit", diff --git a/packages/admin/dashboard/src/i18n/translations/en.json b/packages/admin/dashboard/src/i18n/translations/en.json index feaf8f03810b0..5677801063e6d 100644 --- a/packages/admin/dashboard/src/i18n/translations/en.json +++ b/packages/admin/dashboard/src/i18n/translations/en.json @@ -1002,6 +1002,20 @@ "quantityLowerThanFulfillment": "Cannot set quantity to be less then or equal to fulfilled quantity" } }, + "edit": { + "email": { + "title": "Edit email", + "requestSuccess": "Order email updated to {{email}}." + }, + "shippingAddress": { + "title": "Edit shipping address", + "requestSuccess": "Order shipping address updated." + }, + "billingAddress": { + "title": "Edit billing address", + "requestSuccess": "Order billing address updated." + } + }, "returns": { "create": "Create Return", "confirm": "Confirm Return", @@ -1301,6 +1315,11 @@ "requested": "Order transfer #{{transferId}} requested", "confirmed": "Order transfer #{{transferId}} confirmed", "declined": "Order transfer #{{transferId}} declined" + }, + "update_order": { + "shipping_address": "Shipping address updated", + "billing_address": "Billing address updated", + "email": "Email updated" } } }, @@ -2598,7 +2617,9 @@ "available": "Available", "inStock": "In stock", "added": "Added", - "removed": "Removed" + "removed": "Removed", + "from": "From", + "to": "To" }, "fields": { "amount": "Amount", @@ -2630,6 +2651,7 @@ "discountable": "Discountable", "handle": "Handle", "subtitle": "Subtitle", + "by": "By", "item": "Item", "qty": "qty.", "limit": "Limit", diff --git a/packages/admin/dashboard/src/providers/router-provider/route-map.tsx b/packages/admin/dashboard/src/providers/router-provider/route-map.tsx index 2ee8c9d14f79d..e9fc3ca912471 100644 --- a/packages/admin/dashboard/src/providers/router-provider/route-map.tsx +++ b/packages/admin/dashboard/src/providers/router-provider/route-map.tsx @@ -329,6 +329,20 @@ export const RouteMap: RouteObject[] = [ lazy: () => import("../../routes/orders/order-request-transfer"), }, + { + path: "email", + lazy: () => import("../../routes/orders/order-edit-email"), + }, + { + path: "shipping-address", + lazy: () => + import("../../routes/orders/order-edit-shipping-address"), + }, + { + path: "billing-address", + lazy: () => + import("../../routes/orders/order-edit-billing-address"), + }, ], }, ], diff --git a/packages/admin/dashboard/src/routes/orders/order-detail/components/order-activity-section/change-details-tooltip.tsx b/packages/admin/dashboard/src/routes/orders/order-detail/components/order-activity-section/change-details-tooltip.tsx new file mode 100644 index 0000000000000..25ac34b4e8dac --- /dev/null +++ b/packages/admin/dashboard/src/routes/orders/order-detail/components/order-activity-section/change-details-tooltip.tsx @@ -0,0 +1,74 @@ +import { Popover, Text } from "@medusajs/ui" +import { ReactNode, useState } from "react" +import { useTranslation } from "react-i18next" + +type ChangeDetailsTooltipProps = { + previous: ReactNode + next: ReactNode + title: string +} + +function ChangeDetailsTooltip(props: ChangeDetailsTooltipProps) { + const { t } = useTranslation() + const [open, setOpen] = useState(false) + const previous = props.previous + const next = props.next + const title = props.title + + const handleMouseEnter = () => { + setOpen(true) + } + + const handleMouseLeave = () => { + setOpen(false) + } + + if (!previous && !next) { + return null + } + + return ( + + + + {title} + + + + +
+ {!!previous && ( +
+
+ {t("labels.from")} +
+ +

{previous}

+
+ )} + + {!!next && ( +
+
+ {t("labels.to")} +
+ +

{next}

+
+ )} +
+
+
+ ) +} + +export default ChangeDetailsTooltip diff --git a/packages/admin/dashboard/src/routes/orders/order-detail/components/order-activity-section/order-timeline.tsx b/packages/admin/dashboard/src/routes/orders/order-detail/components/order-activity-section/order-timeline.tsx index 5542246d258f9..6563fb6b9b1f0 100644 --- a/packages/admin/dashboard/src/routes/orders/order-detail/components/order-activity-section/order-timeline.tsx +++ b/packages/admin/dashboard/src/routes/orders/order-detail/components/order-activity-section/order-timeline.tsx @@ -32,6 +32,9 @@ import { useDate } from "../../../../../hooks/use-date" import { getStylizedAmount } from "../../../../../lib/money-amount-helpers" import { getPaymentsFromOrder } from "../order-payment-section" import ActivityItems from "./activity-items" +import { By, UserLink } from "../../../../../components/common/user-link" +import ChangeDetailsTooltip from "./change-details-tooltip" +import { getFormattedAddress } from "../../../../../lib/addresses" type OrderTimelineProps = { order: AdminOrder @@ -42,6 +45,11 @@ type OrderTimelineProps = { */ const NOTE_LIMIT = 9999 +/** + * Order Changes that are not related to RMA flows + */ +const NON_RMA_CHANGE_TYPES = ["transfer", "update_order"] + export const OrderTimeline = ({ order }: OrderTimelineProps) => { const items = useActivityItems(order) @@ -118,10 +126,19 @@ const useActivityItems = (order: AdminOrder): Activity[] => { const { t } = useTranslation() const { order_changes: orderChanges = [] } = useOrderChanges(order.id, { - change_type: ["edit", "claim", "exchange", "return", "transfer"], + change_type: [ + "edit", + "claim", + "exchange", + "return", + "transfer", + "update_order", + ], }) - const rmaChanges = orderChanges.filter((oc) => oc.change_type !== "transfer") + const rmaChanges = orderChanges.filter( + (oc) => !NON_RMA_CHANGE_TYPES.includes(oc.change_type) + ) const missingLineItemIds = getMissingLineItemIds(order, rmaChanges) const { order_items: removedLineItems = [] } = useOrderLineItems( @@ -418,6 +435,74 @@ const useActivityItems = (order: AdminOrder): Activity[] => { } } + for (const update of orderChanges.filter( + (oc) => oc.change_type === "update_order" + )) { + const updateType = update.actions[0]?.details?.type + + if (updateType === "shipping_address") { + items.push({ + title: ( + + ), + timestamp: update.created_at, + children: ( +
+ {t("fields.by")} +
+ ), + }) + } + + if (updateType === "billing_address") { + items.push({ + title: ( + + ), + timestamp: update.created_at, + children: ( +
+ {t("fields.by")} +
+ ), + }) + } + + if (updateType === "email") { + items.push({ + title: ( + + ), + timestamp: update.created_at, + children: ( +
+ {t("fields.by")} +
+ ), + }) + } + } + // for (const note of notes || []) { // items.push({ // title: t("orders.activity.events.note.comment"), diff --git a/packages/admin/dashboard/src/routes/orders/order-edit-billing-address/components/edit-order-billing-address-form/edit-order-billing-address-form.tsx b/packages/admin/dashboard/src/routes/orders/order-edit-billing-address/components/edit-order-billing-address-form/edit-order-billing-address-form.tsx new file mode 100644 index 0000000000000..b20e97b395c5d --- /dev/null +++ b/packages/admin/dashboard/src/routes/orders/order-edit-billing-address/components/edit-order-billing-address-form/edit-order-billing-address-form.tsx @@ -0,0 +1,216 @@ +import * as zod from "zod" +import { zodResolver } from "@hookform/resolvers/zod" +import { HttpTypes } from "@medusajs/types" +import { Button, Input, toast } from "@medusajs/ui" +import { useForm } from "react-hook-form" +import { useTranslation } from "react-i18next" + +import { Form } from "../../../../../components/common/form" +import { RouteDrawer, useRouteModal } from "../../../../../components/modals" +import { KeyboundForm } from "../../../../../components/utilities/keybound-form" +import { useUpdateOrder } from "../../../../../hooks/api/orders" +import { CountrySelect } from "../../../../../components/inputs/country-select" + +type EditOrderBillingAddressFormProps = { + order: HttpTypes.AdminOrder +} + +const EditOrderBillingAddressSchema = zod.object({ + address_1: zod.string().min(1), + address_2: zod.string().optional(), + country_code: zod.string().min(2).max(2), + city: zod.string().optional(), + postal_code: zod.string().optional(), + province: zod.string().optional(), + company: zod.string().optional(), + phone: zod.string().optional(), +}) + +export function EditOrderBillingAddressForm({ + order, +}: EditOrderBillingAddressFormProps) { + const { t } = useTranslation() + const { handleSuccess } = useRouteModal() + + const form = useForm>({ + defaultValues: { + address_1: order.billing_address?.address_1 || "", + address_2: order.billing_address?.address_2 || "", + city: order.billing_address?.city || "", + company: order.billing_address?.company || "", + country_code: order.billing_address?.country_code || "", + phone: order.billing_address?.phone || "", + postal_code: order.billing_address?.postal_code || "", + province: order.billing_address?.province || "", + }, + resolver: zodResolver(EditOrderBillingAddressSchema), + }) + + const { mutateAsync, isPending } = useUpdateOrder(order.id) + + const handleSubmit = form.handleSubmit(async (data) => { + try { + await mutateAsync({ + billing_address: data, + }) + toast.success(t("orders.edit.billingAddress.requestSuccess")) + handleSuccess() + } catch (error) { + toast.error((error as Error).message) + } + }) + + return ( + + + +
+ { + return ( + + {t("fields.address")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.address2")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.postalCode")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.city")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.country")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.state")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.company")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.phone")} + + + + + + ) + }} + /> +
+
+ + +
+ + + + + +
+
+
+
+ ) +} diff --git a/packages/admin/dashboard/src/routes/orders/order-edit-billing-address/components/edit-order-billing-address-form/index.ts b/packages/admin/dashboard/src/routes/orders/order-edit-billing-address/components/edit-order-billing-address-form/index.ts new file mode 100644 index 0000000000000..1c1b9f5a33e4a --- /dev/null +++ b/packages/admin/dashboard/src/routes/orders/order-edit-billing-address/components/edit-order-billing-address-form/index.ts @@ -0,0 +1 @@ +export * from "./edit-order-billing-address-form" diff --git a/packages/admin/dashboard/src/routes/orders/order-edit-billing-address/index.ts b/packages/admin/dashboard/src/routes/orders/order-edit-billing-address/index.ts new file mode 100644 index 0000000000000..dab858990a4be --- /dev/null +++ b/packages/admin/dashboard/src/routes/orders/order-edit-billing-address/index.ts @@ -0,0 +1 @@ +export { OrderEditBillingAddress as Component } from "./order-edit-billing-address" diff --git a/packages/admin/dashboard/src/routes/orders/order-edit-billing-address/order-edit-billing-address.tsx b/packages/admin/dashboard/src/routes/orders/order-edit-billing-address/order-edit-billing-address.tsx new file mode 100644 index 0000000000000..88327223bf3cb --- /dev/null +++ b/packages/admin/dashboard/src/routes/orders/order-edit-billing-address/order-edit-billing-address.tsx @@ -0,0 +1,31 @@ +import { Heading } from "@medusajs/ui" +import { useTranslation } from "react-i18next" +import { useParams } from "react-router-dom" + +import { RouteDrawer } from "../../../components/modals" +import { useOrder } from "../../../hooks/api" +import { DEFAULT_FIELDS } from "../order-detail/constants" +import { EditOrderBillingAddressForm } from "./components/edit-order-billing-address-form" + +export const OrderEditBillingAddress = () => { + const { t } = useTranslation() + const params = useParams() + + const { order, isPending, isError, error } = useOrder(params.id!, { + fields: DEFAULT_FIELDS, + }) + + if (!isPending && isError) { + throw error + } + + return ( + + + {t("orders.edit.billingAddress.title")} + + + {order && } + + ) +} diff --git a/packages/admin/dashboard/src/routes/orders/order-edit-email/components/edit-order-email-form/edit-order-email-form.tsx b/packages/admin/dashboard/src/routes/orders/order-edit-email/components/edit-order-email-form/edit-order-email-form.tsx new file mode 100644 index 0000000000000..db86181971c42 --- /dev/null +++ b/packages/admin/dashboard/src/routes/orders/order-edit-email/components/edit-order-email-form/edit-order-email-form.tsx @@ -0,0 +1,95 @@ +import * as zod from "zod" +import { zodResolver } from "@hookform/resolvers/zod" +import { HttpTypes } from "@medusajs/types" +import { Button, Input, toast } from "@medusajs/ui" +import { useForm } from "react-hook-form" +import { useTranslation } from "react-i18next" + +import { Form } from "../../../../../components/common/form" +import { RouteDrawer, useRouteModal } from "../../../../../components/modals" +import { KeyboundForm } from "../../../../../components/utilities/keybound-form" +import { useUpdateOrder } from "../../../../../hooks/api/orders" + +type EditOrderEmailFormProps = { + order: HttpTypes.AdminOrder +} + +const EditOrderEmailSchema = zod.object({ + email: zod.string().email(), +}) + +export function EditOrderEmailForm({ order }: EditOrderEmailFormProps) { + const { t } = useTranslation() + const { handleSuccess } = useRouteModal() + + const form = useForm>({ + defaultValues: { + email: order.email || "", + }, + resolver: zodResolver(EditOrderEmailSchema), + }) + + const { mutateAsync, isPending } = useUpdateOrder(order.id) + + const handleSubmit = form.handleSubmit(async (data) => { + try { + await mutateAsync({ + email: data.email, + }) + toast.success( + t("orders.edit.email.requestSuccess", { email: data.email }) + ) + handleSuccess() + } catch (error) { + toast.error((error as Error).message) + } + }) + + return ( + + + + { + return ( + + {t("fields.email")} + + + + + + + + ) + }} + /> + + + +
+ + + + + +
+
+
+
+ ) +} diff --git a/packages/admin/dashboard/src/routes/orders/order-edit-email/components/edit-order-email-form/index.ts b/packages/admin/dashboard/src/routes/orders/order-edit-email/components/edit-order-email-form/index.ts new file mode 100644 index 0000000000000..55443133a7598 --- /dev/null +++ b/packages/admin/dashboard/src/routes/orders/order-edit-email/components/edit-order-email-form/index.ts @@ -0,0 +1 @@ +export * from "./edit-order-email-form" diff --git a/packages/admin/dashboard/src/routes/orders/order-edit-email/index.ts b/packages/admin/dashboard/src/routes/orders/order-edit-email/index.ts new file mode 100644 index 0000000000000..014a490fc1ee4 --- /dev/null +++ b/packages/admin/dashboard/src/routes/orders/order-edit-email/index.ts @@ -0,0 +1 @@ +export { OrderEditEmail as Component } from "./order-edit-email" diff --git a/packages/admin/dashboard/src/routes/orders/order-edit-email/order-edit-email.tsx b/packages/admin/dashboard/src/routes/orders/order-edit-email/order-edit-email.tsx new file mode 100644 index 0000000000000..3411a91ac73fe --- /dev/null +++ b/packages/admin/dashboard/src/routes/orders/order-edit-email/order-edit-email.tsx @@ -0,0 +1,31 @@ +import { Heading } from "@medusajs/ui" +import { useTranslation } from "react-i18next" +import { useParams } from "react-router-dom" + +import { RouteDrawer } from "../../../components/modals" +import { useOrder } from "../../../hooks/api" +import { DEFAULT_FIELDS } from "../order-detail/constants" +import { EditOrderEmailForm } from "./components/edit-order-email-form" + +export const OrderEditEmail = () => { + const { t } = useTranslation() + const params = useParams() + + const { order, isPending, isError, error } = useOrder(params.id!, { + fields: DEFAULT_FIELDS, + }) + + if (!isPending && isError) { + throw error + } + + return ( + + + {t("orders.edit.email.title")} + + + {order && } + + ) +} diff --git a/packages/admin/dashboard/src/routes/orders/order-edit-shipping-address/components/edit-order-shipping-address-form/edit-order-shipping-address-form.tsx b/packages/admin/dashboard/src/routes/orders/order-edit-shipping-address/components/edit-order-shipping-address-form/edit-order-shipping-address-form.tsx new file mode 100644 index 0000000000000..f6be220cb42b2 --- /dev/null +++ b/packages/admin/dashboard/src/routes/orders/order-edit-shipping-address/components/edit-order-shipping-address-form/edit-order-shipping-address-form.tsx @@ -0,0 +1,215 @@ +import * as zod from "zod" +import { zodResolver } from "@hookform/resolvers/zod" +import { HttpTypes } from "@medusajs/types" +import { Button, Input, toast } from "@medusajs/ui" +import { useForm } from "react-hook-form" +import { useTranslation } from "react-i18next" + +import { Form } from "../../../../../components/common/form" +import { RouteDrawer, useRouteModal } from "../../../../../components/modals" +import { KeyboundForm } from "../../../../../components/utilities/keybound-form" +import { useUpdateOrder } from "../../../../../hooks/api/orders" +import { CountrySelect } from "../../../../../components/inputs/country-select" + +type EditOrderShippingAddressFormProps = { + order: HttpTypes.AdminOrder +} + +const EditOrderShippingAddressSchema = zod.object({ + address_1: zod.string().min(1), + address_2: zod.string().optional(), + country_code: zod.string().min(2).max(2), + city: zod.string().optional(), + postal_code: zod.string().optional(), + province: zod.string().optional(), + company: zod.string().optional(), + phone: zod.string().optional(), +}) + +export function EditOrderShippingAddressForm({ + order, +}: EditOrderShippingAddressFormProps) { + const { t } = useTranslation() + const { handleSuccess } = useRouteModal() + + const form = useForm>({ + defaultValues: { + address_1: order.shipping_address?.address_1 || "", + address_2: order.shipping_address?.address_2 || "", + city: order.shipping_address?.city || "", + company: order.shipping_address?.company || "", + country_code: order.shipping_address?.country_code || "", + phone: order.shipping_address?.phone || "", + postal_code: order.shipping_address?.postal_code || "", + province: order.shipping_address?.province || "", + }, + resolver: zodResolver(EditOrderShippingAddressSchema), + }) + + const { mutateAsync, isPending } = useUpdateOrder(order.id) + + const handleSubmit = form.handleSubmit(async (data) => { + try { + await mutateAsync({ + shipping_address: data, + }) + toast.success(t("orders.edit.shippingAddress.requestSuccess")) + handleSuccess() + } catch (error) { + toast.error((error as Error).message) + } + }) + + return ( + + + +
+ { + return ( + + {t("fields.address")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.address2")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.postalCode")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.city")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.country")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.state")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.company")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.phone")} + + + + + + ) + }} + /> +
+
+ + +
+ + + + + +
+
+
+
+ ) +} diff --git a/packages/admin/dashboard/src/routes/orders/order-edit-shipping-address/components/edit-order-shipping-address-form/index.ts b/packages/admin/dashboard/src/routes/orders/order-edit-shipping-address/components/edit-order-shipping-address-form/index.ts new file mode 100644 index 0000000000000..b317ef3184ea6 --- /dev/null +++ b/packages/admin/dashboard/src/routes/orders/order-edit-shipping-address/components/edit-order-shipping-address-form/index.ts @@ -0,0 +1 @@ +export * from "./edit-order-shipping-address-form" diff --git a/packages/admin/dashboard/src/routes/orders/order-edit-shipping-address/index.ts b/packages/admin/dashboard/src/routes/orders/order-edit-shipping-address/index.ts new file mode 100644 index 0000000000000..0dca73c20ecad --- /dev/null +++ b/packages/admin/dashboard/src/routes/orders/order-edit-shipping-address/index.ts @@ -0,0 +1 @@ +export { OrderEditShippingAddress as Component } from "./order-edit-shipping-address" diff --git a/packages/admin/dashboard/src/routes/orders/order-edit-shipping-address/order-edit-shipping-address.tsx b/packages/admin/dashboard/src/routes/orders/order-edit-shipping-address/order-edit-shipping-address.tsx new file mode 100644 index 0000000000000..dbb982a073fc5 --- /dev/null +++ b/packages/admin/dashboard/src/routes/orders/order-edit-shipping-address/order-edit-shipping-address.tsx @@ -0,0 +1,31 @@ +import { Heading } from "@medusajs/ui" +import { useTranslation } from "react-i18next" +import { useParams } from "react-router-dom" + +import { RouteDrawer } from "../../../components/modals" +import { useOrder } from "../../../hooks/api" +import { DEFAULT_FIELDS } from "../order-detail/constants" +import { EditOrderShippingAddressForm } from "./components/edit-order-shipping-address-form" + +export const OrderEditShippingAddress = () => { + const { t } = useTranslation() + const params = useParams() + + const { order, isPending, isError } = useOrder(params.id!, { + fields: DEFAULT_FIELDS, + }) + + if (!isPending && isError) { + throw new Error("Order not found") + } + + return ( + + + {t("orders.edit.shippingAddress.title")} + + + {order && } + + ) +} diff --git a/packages/core/core-flows/src/order/workflows/update-order.ts b/packages/core/core-flows/src/order/workflows/update-order.ts index 54a265cebcc6a..b78ac81e9d487 100644 --- a/packages/core/core-flows/src/order/workflows/update-order.ts +++ b/packages/core/core-flows/src/order/workflows/update-order.ts @@ -121,45 +121,62 @@ export const updateOrderWorkflow = createWorkflow( return { ...input, ...update } }) - updateOrdersStep({ + const updatedOrders = updateOrdersStep({ selector: { id: input.id }, update: updateInput, }) - const orderChangeInput = transform({ input, order }, ({ input, order }) => { - const changes: RegisterOrderChangeDTO[] = [] - if (input.shipping_address) { - changes.push({ - change_type: "update_order" as const, - order_id: input.id, - reference: "shipping_address", - reference_id: order.shipping_address?.id, // save previous address id as reference - details: input.shipping_address as Record, // save what changed on the address - }) - } + const orderChangeInput = transform( + { input, updatedOrders, order }, + ({ input, updatedOrders, order }) => { + const updatedOrder = updatedOrders[0] + + const changes: RegisterOrderChangeDTO[] = [] + if (input.shipping_address) { + changes.push({ + change_type: "update_order" as const, + order_id: input.id, + created_by: input.user_id, + confirmed_by: input.user_id, + details: { + type: "shipping_address", + old: order.shipping_address, + new: updatedOrder.shipping_address, + }, + }) + } - if (input.billing_address) { - changes.push({ - change_type: "update_order" as const, - order_id: input.id, - reference: "billing_address", - reference_id: order.billing_address?.id, - details: input.billing_address as Record, - }) - } + if (input.billing_address) { + changes.push({ + change_type: "update_order" as const, + order_id: input.id, + created_by: input.user_id, + confirmed_by: input.user_id, + details: { + type: "billing_address", + old: order.billing_address, + new: updatedOrder.billing_address, + }, + }) + } - if (input.email) { - changes.push({ - change_type: "update_order" as const, - order_id: input.id, - reference: "email", - reference_id: order.email, - details: { email: input.email }, - }) - } + if (input.email) { + changes.push({ + change_type: "update_order" as const, + order_id: input.id, + created_by: input.user_id, + confirmed_by: input.user_id, + details: { + type: "email", + old: order.email, + new: input.email, + }, + }) + } - return changes - }) + return changes + } + ) registerOrderChangesStep(orderChangeInput) diff --git a/packages/core/js-sdk/src/admin/order.ts b/packages/core/js-sdk/src/admin/order.ts index 550126a5ee531..096a31ef490b8 100644 --- a/packages/core/js-sdk/src/admin/order.ts +++ b/packages/core/js-sdk/src/admin/order.ts @@ -64,6 +64,47 @@ export class Order { ) } + /** + * This method updates an order. It sends a request to the + * [Update Order Email](https://docs.medusajs.com/api/admin#orders_postordersid) + * API route. + * + * @param id - The order's ID. + * @param body - The update details. + * @param headers - Headers to pass in the request + * @returns The order's details. + * + * @example + * sdk.admin.order.update( + * "order_123", + * { + * email: "new_email@example.com", + * shipping_address: { + * first_name: "John", + * last_name: "Doe", + * address_1: "123 Main St", + * } + * } + * ) + * .then(({ order }) => { + * console.log(order) + * }) + */ + async update( + id: string, + body: HttpTypes.AdminUpdateOrder, + headers?: ClientHeaders + ) { + return await this.client.fetch( + `/admin/orders/${id}`, + { + method: "POST", + headers, + body, + } + ) + } + /** * This method retrieves the preview of an order based on its last associated change. It sends a request to the * [Get Order Preview](https://docs.medusajs.com/api/admin#orders_getordersidpreview) API route. diff --git a/packages/core/types/src/http/order/admin/payload.ts b/packages/core/types/src/http/order/admin/payload.ts index e46edf2c7a491..d0f601318abcb 100644 --- a/packages/core/types/src/http/order/admin/payload.ts +++ b/packages/core/types/src/http/order/admin/payload.ts @@ -1,3 +1,18 @@ +export interface AdminUpdateOrder { + /** + * The order's email. + */ + email?: string + /** + * The order's shipping address. + */ + shipping_address?: OrderAddress + /** + * The order's billing address. + */ + billing_address?: OrderAddress +} + export interface AdminCreateOrderFulfillment { /** * The items to add to the fulfillment. @@ -82,3 +97,60 @@ export interface AdminRequestOrderTransfer { internal_note?: string description?: string } + +export interface OrderAddress { + /** + * The first name of the address. + */ + first_name?: string + + /** + * The last name of the address. + */ + last_name?: string + + /** + * The phone number of the address. + */ + phone?: string + + /** + * The company of the address. + */ + company?: string + + /** + * The first address line of the address. + */ + address_1?: string + + /** + * The second address line of the address. + */ + address_2?: string + + /** + * The city of the address. + */ + city?: string + + /** + * The country code of the address. + */ + country_code?: string + + /** + * The province/state of the address. + */ + province?: string + + /** + * The postal code of the address. + */ + postal_code?: string + + /** + * Holds custom data in key-value pairs. + */ + metadata?: Record | null +} diff --git a/packages/core/types/src/order/mutations.ts b/packages/core/types/src/order/mutations.ts index 358aefa56b37d..f8c6b6bd2ab90 100644 --- a/packages/core/types/src/order/mutations.ts +++ b/packages/core/types/src/order/mutations.ts @@ -1077,9 +1077,14 @@ export interface RegisterOrderChangeDTO { internal_note?: string | null /** - * The user or customer that requested the order change. + * The user that created the order change. */ - requested_by?: string + created_by?: string + + /** + * The user or customer that confirmed the order change. + */ + confirmed_by?: string /** * Holds custom data in key-value pairs. diff --git a/packages/core/types/src/workflow/order/update-order.ts b/packages/core/types/src/workflow/order/update-order.ts index 32cdd47bc2a5a..e9c0b0357f96d 100644 --- a/packages/core/types/src/workflow/order/update-order.ts +++ b/packages/core/types/src/workflow/order/update-order.ts @@ -2,6 +2,7 @@ import { UpsertOrderAddressDTO } from "../../order" export type UpdateOrderWorkflowInput = { id: string + user_id: string shipping_address?: UpsertOrderAddressDTO billing_address?: UpsertOrderAddressDTO email?: string diff --git a/packages/medusa/src/api/admin/orders/[id]/route.ts b/packages/medusa/src/api/admin/orders/[id]/route.ts index 715b097197355..4fce9e3f29c2e 100644 --- a/packages/medusa/src/api/admin/orders/[id]/route.ts +++ b/packages/medusa/src/api/admin/orders/[id]/route.ts @@ -38,6 +38,7 @@ export const POST = async ( await updateOrderWorkflow(req.scope).run({ input: { ...req.validatedBody, + user_id: req.auth_context.actor_id, id: req.params.id, }, }) diff --git a/packages/modules/order/src/services/order-module-service.ts b/packages/modules/order/src/services/order-module-service.ts index 149d4e77e27fd..3a15c3cd7fe54 100644 --- a/packages/modules/order/src/services/order-module-service.ts +++ b/packages/modules/order/src/services/order-module-service.ts @@ -2268,14 +2268,14 @@ export default class OrderModuleService< description: d.description, metadata: d.metadata, confirmed_at: new Date(), + created_by: d.created_by, + confirmed_by: d.confirmed_by, status: OrderChangeStatus.CONFIRMED, version: orderVersionsMap.get(d.order_id)!, actions: [ { action: ChangeActionType.UPDATE_ORDER_PROPERTIES, details: d.details, - reference: d.reference, - reference_id: d.reference_id, version: orderVersionsMap.get(d.order_id)!, applied: true, }, From 665eea8e755d61d8593c36e102649a19e114d5b4 Mon Sep 17 00:00:00 2001 From: Shahed Nasser Date: Wed, 4 Dec 2024 17:31:20 +0200 Subject: [PATCH 6/7] docs: fixes and improvements to Sanity guide (#10414) --- .../app/integrations/guides/sanity/page.mdx | 84 +++++++++---------- www/apps/resources/generated/edit-dates.mjs | 2 +- 2 files changed, 39 insertions(+), 47 deletions(-) diff --git a/www/apps/resources/app/integrations/guides/sanity/page.mdx b/www/apps/resources/app/integrations/guides/sanity/page.mdx index 24efda6cd7a99..53cf6b9f49ff5 100644 --- a/www/apps/resources/app/integrations/guides/sanity/page.mdx +++ b/www/apps/resources/app/integrations/guides/sanity/page.mdx @@ -680,6 +680,7 @@ import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" import { ProductDTO } from "@medusajs/framework/types" import { ContainerRegistrationKeys, + promiseAll, } from "@medusajs/framework/utils" import SanityModuleService from "../../../modules/sanity/service" import { SANITY_MODULE } from "../../../modules/sanity" @@ -694,15 +695,15 @@ export const syncStep = createStep( const sanityModule: SanityModuleService = container.resolve(SANITY_MODULE) const query = container.resolve(ContainerRegistrationKeys.QUERY) - const total = 0 + let total = 0; const upsertMap: { before: any after: any }[] = [] - const batchSize = 200 - const hasMore = true - const offset = 0 + const batchSize = 200; + let hasMore = true; + let offset = 0; let filters = input.product_ids ? { id: input.product_ids } : {} @@ -760,54 +761,39 @@ Notice that you pass `sanity_product.*` in the `fields` array. Medusa will retri } ``` -Next, you want to sync the retrieved products. So, replace the `TODO` with the following: +Next, you want to sync the retrieved products. So, replace the `TODO` in the `while` loop with the following: ```ts title="src/workflows/sanity-sync-products/steps/sync.ts" -// other imports... -import { +while (hasMore) { // ... - promiseAll, -} from "@medusajs/framework/utils" - -export const syncStep = createStep( - { name: "sync-step", async: true }, - async (input: SyncStepInput, { container }) => { - // ... - - while (hasMore) { - // ... - try { - await promiseAll( - products.map(async (prod) => { - const after = await sanityModule.upsertSyncDocument( - "product", - prod as ProductDTO - ); - - upsertMap.push({ - // @ts-ignore - before: prod.sanity_product, - after - }) - - return after - }), - ) - } catch (e) { - return StepResponse.permanentFailure( - `An error occurred while syncing documents: ${e}`, - upsertMap - ) - } + try { + await promiseAll( + products.map(async (prod) => { + const after = await sanityModule.upsertSyncDocument( + "product", + prod as ProductDTO + ); - offset += batchSize - hasMore = offset < count - total += products.length - } + upsertMap.push({ + // @ts-ignore + before: prod.sanity_product, + after + }) - return new StepResponse({ total }, upsertMap) + return after + }), + ) + } catch (e) { + return StepResponse.permanentFailure( + `An error occurred while syncing documents: ${e}`, + upsertMap + ) } -) + + offset += batchSize + hasMore = offset < count + total += products.length +} ``` In the `while` loop, you loop over the array of products to sync them to Sanity. You use the `promiseAll` Medusa utility that loops over an array of promises and ensures that all transactions within these promises are rolled back in case an error occurs. @@ -816,6 +802,12 @@ For each product, you upsert it into Sanity, then push its document before and a You also wrap the `promiseAll` function within a try-catch block. In the catch block, you invoke and return `StepResponse.permanentFailure` which indicates that the step has failed but still invokes the rollback mechanism that you'll implement in a bit. The first parameter of `permanentFailure` is the error message, and the second is the data to use in the rollback mechanism. +Finally, after the `while` loop and at the end of the step, add the following return statement: + +```ts title="src/workflows/sanity-sync-products/steps/sync.ts" +return new StepResponse({ total }, upsertMap); +``` + If no errors occur, the step returns an instance of `StepResponse`, which must be returned by any step. It accepts as a first parameter the data to return to the workflow that executed this step. #### Add Compensation Function diff --git a/www/apps/resources/generated/edit-dates.mjs b/www/apps/resources/generated/edit-dates.mjs index 60c7e33837f0e..f564325ea09eb 100644 --- a/www/apps/resources/generated/edit-dates.mjs +++ b/www/apps/resources/generated/edit-dates.mjs @@ -3245,7 +3245,7 @@ export const generatedEditDates = { "references/types/HttpTypes/interfaces/types.HttpTypes.AdminBatchProductVariantRequest/page.mdx": "2024-11-25T17:49:26.851Z", "references/types/WorkflowTypes/ProductWorkflow/interfaces/types.WorkflowTypes.ProductWorkflow.ExportProductsDTO/page.mdx": "2024-11-12T09:36:24.232Z", "app/contribution-guidelines/admin-translations/page.mdx": "2024-11-14T08:54:15.369Z", - "app/integrations/guides/sanity/page.mdx": "2024-11-27T10:10:12.100Z", + "app/integrations/guides/sanity/page.mdx": "2024-12-03T14:14:11.347Z", "references/api_key/types/api_key.FindConfigOrder/page.mdx": "2024-11-25T17:49:28.715Z", "references/auth/types/auth.FindConfigOrder/page.mdx": "2024-11-25T17:49:28.887Z", "references/cart/types/cart.FindConfigOrder/page.mdx": "2024-11-25T17:49:29.455Z", From c6f955f0b51a795b4f6a1aeaa1de36205d2d32e5 Mon Sep 17 00:00:00 2001 From: Kasper Fabricius Kristensen <45367945+kasperkristensen@users.noreply.github.com> Date: Wed, 4 Dec 2024 17:09:44 +0100 Subject: [PATCH 7/7] fix(dashboard): Add Shipping Profile metadata route (#10430) --- .changeset/rotten-spoons-brake.md | 5 +++ .../src/hooks/api/shipping-profiles.tsx | 26 ++++++++++++++++ .../providers/router-provider/route-map.tsx | 9 ++++++ .../shipping-profile-metadata/index.ts | 1 + .../shipping-profile-metadata.tsx | 31 +++++++++++++++++++ 5 files changed, 72 insertions(+) create mode 100644 .changeset/rotten-spoons-brake.md create mode 100644 packages/admin/dashboard/src/routes/shipping-profiles/shipping-profile-metadata/index.ts create mode 100644 packages/admin/dashboard/src/routes/shipping-profiles/shipping-profile-metadata/shipping-profile-metadata.tsx diff --git a/.changeset/rotten-spoons-brake.md b/.changeset/rotten-spoons-brake.md new file mode 100644 index 0000000000000..8ace1708e5466 --- /dev/null +++ b/.changeset/rotten-spoons-brake.md @@ -0,0 +1,5 @@ +--- +"@medusajs/dashboard": patch +--- + +fix(dashboard): Add Shipping Profile metadata route diff --git a/packages/admin/dashboard/src/hooks/api/shipping-profiles.tsx b/packages/admin/dashboard/src/hooks/api/shipping-profiles.tsx index c7834e37e20f4..571577ed7aea9 100644 --- a/packages/admin/dashboard/src/hooks/api/shipping-profiles.tsx +++ b/packages/admin/dashboard/src/hooks/api/shipping-profiles.tsx @@ -80,6 +80,32 @@ export const useShippingProfiles = ( return { ...data, ...rest } } +export const useUpdateShippingProfile = ( + id: string, + options?: UseMutationOptions< + HttpTypes.AdminShippingProfileResponse, + FetchError, + HttpTypes.AdminUpdateShippingProfile + > +) => { + const { data, ...rest } = useMutation({ + mutationFn: (payload) => sdk.admin.shippingProfile.update(id, payload), + onSuccess: (data, variables, context) => { + queryClient.invalidateQueries({ + queryKey: shippingProfileQueryKeys.detail(id), + }) + queryClient.invalidateQueries({ + queryKey: shippingProfileQueryKeys.lists(), + }) + + options?.onSuccess?.(data, variables, context) + }, + ...options, + }) + + return { ...data, ...rest } +} + export const useDeleteShippingProfile = ( id: string, options?: UseMutationOptions< diff --git a/packages/admin/dashboard/src/providers/router-provider/route-map.tsx b/packages/admin/dashboard/src/providers/router-provider/route-map.tsx index e9fc3ca912471..16f1573b0e528 100644 --- a/packages/admin/dashboard/src/providers/router-provider/route-map.tsx +++ b/packages/admin/dashboard/src/providers/router-provider/route-map.tsx @@ -1095,6 +1095,15 @@ export const RouteMap: RouteObject[] = [ }, } }, + children: [ + { + path: "metadata/edit", + lazy: () => + import( + "../../routes/shipping-profiles/shipping-profile-metadata" + ), + }, + ], }, ], }, diff --git a/packages/admin/dashboard/src/routes/shipping-profiles/shipping-profile-metadata/index.ts b/packages/admin/dashboard/src/routes/shipping-profiles/shipping-profile-metadata/index.ts new file mode 100644 index 0000000000000..37d8504383c96 --- /dev/null +++ b/packages/admin/dashboard/src/routes/shipping-profiles/shipping-profile-metadata/index.ts @@ -0,0 +1 @@ +export { ShippingProfileMetadata as Component } from "./shipping-profile-metadata" diff --git a/packages/admin/dashboard/src/routes/shipping-profiles/shipping-profile-metadata/shipping-profile-metadata.tsx b/packages/admin/dashboard/src/routes/shipping-profiles/shipping-profile-metadata/shipping-profile-metadata.tsx new file mode 100644 index 0000000000000..35a4bd528994d --- /dev/null +++ b/packages/admin/dashboard/src/routes/shipping-profiles/shipping-profile-metadata/shipping-profile-metadata.tsx @@ -0,0 +1,31 @@ +import { useParams } from "react-router-dom" +import { MetadataForm } from "../../../components/forms/metadata-form/metadata-form" +import { + useShippingProfile, + useUpdateShippingProfile, +} from "../../../hooks/api" + +export const ShippingProfileMetadata = () => { + const { shipping_profile_id } = useParams() + + const { shipping_profile, isPending, isError, error } = useShippingProfile( + shipping_profile_id! + ) + + const { mutateAsync, isPending: isMutating } = useUpdateShippingProfile( + shipping_profile?.id! + ) + + if (isError) { + throw error + } + + return ( + + ) +}