From 21b0e0c26b62de281556cf5b6f9c2252bb72188f Mon Sep 17 00:00:00 2001 From: Shahed Nasser Date: Fri, 6 Dec 2024 16:34:02 +0200 Subject: [PATCH 01/11] docs: revise admin customization pages (#10466) * docs: revise admin customization pages * fix prerequisites link * apply lint --- .../data-models/check-constraints/page.mdx | 12 +- .../data-models/relationships/page.mdx | 2 +- .../workflows/compensation-function/page.mdx | 2 +- .../custom-features/api-route/page.mdx | 2 +- .../customization/customize-admin/page.mdx | 21 +- .../customize-admin/route/page.mdx | 486 ++++++++++++++---- .../customize-admin/widget/page.mdx | 169 ++++-- www/apps/book/generated/edit-dates.mjs | 6 +- www/apps/book/sidebar.mjs | 2 +- 9 files changed, 538 insertions(+), 164 deletions(-) diff --git a/www/apps/book/app/learn/advanced-development/data-models/check-constraints/page.mdx b/www/apps/book/app/learn/advanced-development/data-models/check-constraints/page.mdx index 59c11996e6349..fc6b1dd15df64 100644 --- a/www/apps/book/app/learn/advanced-development/data-models/check-constraints/page.mdx +++ b/www/apps/book/app/learn/advanced-development/data-models/check-constraints/page.mdx @@ -29,12 +29,12 @@ export const checks1Highlights = [ ```ts highlights={checks1Highlights} import { model } from "@medusajs/framework/utils" -const CustomProduct = model.define('custom_product', { +const CustomProduct = model.define("custom_product", { // ... price: model.bigNumber(), }) .checks([ - (columns) => `${columns.price} >= 0` + (columns) => `${columns.price} >= 0`, ]) ``` @@ -53,15 +53,15 @@ export const checks2Highlights = [ ```ts highlights={checks2Highlights} import { model } from "@medusajs/framework/utils" -const CustomProduct = model.define('custom_product', { +const CustomProduct = model.define("custom_product", { // ... price: model.bigNumber(), }) .checks([ { - name: 'custom_product_price_check', - expression: (columns) => `${columns.price} >= 0` - } + name: "custom_product_price_check", + expression: (columns) => `${columns.price} >= 0`, + }, ]) ``` diff --git a/www/apps/book/app/learn/advanced-development/data-models/relationships/page.mdx b/www/apps/book/app/learn/advanced-development/data-models/relationships/page.mdx index 8ecc950a274bc..fcd5366ae2805 100644 --- a/www/apps/book/app/learn/advanced-development/data-models/relationships/page.mdx +++ b/www/apps/book/app/learn/advanced-development/data-models/relationships/page.mdx @@ -152,7 +152,7 @@ const Order = model.define("order", { mappedBy: "orders", pivotTable: "order_product", joinColumn: "order_id", - inverseJoinColumn: "product_id" + inverseJoinColumn: "product_id", }), }) diff --git a/www/apps/book/app/learn/advanced-development/workflows/compensation-function/page.mdx b/www/apps/book/app/learn/advanced-development/workflows/compensation-function/page.mdx index 0dd06ce533aa0..ce4e551d349c0 100644 --- a/www/apps/book/app/learn/advanced-development/workflows/compensation-function/page.mdx +++ b/www/apps/book/app/learn/advanced-development/workflows/compensation-function/page.mdx @@ -236,7 +236,7 @@ const step1 = createStep( ) return new StepResponse(ids, prevData) - }, + } ) ``` 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 f4d424556be2e..979a0e5a962d4 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 @@ -97,7 +97,7 @@ You create a validation schema in a TypeScript or JavaScript file under a sub-di import { z } from "zod" export const PostAdminCreateBrand = z.object({ - name: z.string() + name: z.string(), }) ``` diff --git a/www/apps/book/app/learn/customization/customize-admin/page.mdx b/www/apps/book/app/learn/customization/customize-admin/page.mdx index e23488a45388f..806a4c78e4c9b 100644 --- a/www/apps/book/app/learn/customization/customize-admin/page.mdx +++ b/www/apps/book/app/learn/customization/customize-admin/page.mdx @@ -1,10 +1,23 @@ export const metadata = { - title: `${pageNumber} Customize Admin to Add Brands`, + title: `${pageNumber} Customize Medusa Admin Dashboard`, } # {metadata.title} -In the next chapters, you'll continue with the brands example to learn how to customize the Medusa Admin to: +In the previous chapters, you've customized your Medusa application to [add brands](../custom-features/module/page.mdx), [expose an API route to create brands](../custom-features/api-route/page.mdx), and [linked brands to products](../extend-features/define-link/page.mdx). -- Show a product's brand on its details page using a widget. -- Add a page showing the list of brands in your application using a UI route. +After customizing and extending your application with new features, you may need to provide an interface for admin users to utilize these features. The Medusa Admin dashboard is extendable, allowing you to: + +- Insert components, called [widgets](../../advanced-development/admin/widgets/page.mdx), on existing pages. +- Add new pages, called [UI Routes](../../advanced-development/admin/ui-routes/page.mdx). + +From these customizations, you can send requests to custom API routes, allowing admin users to manage custom resources on the dashboard + +--- + +## Next Chapters: View Brands in Dashboard + +In the next chapters, you'll continue with the brands example to: + +- Add a new section to the product details page that shows the product's brand. +- Add a new page in the dashboard that shows all brands in the store. diff --git a/www/apps/book/app/learn/customization/customize-admin/route/page.mdx b/www/apps/book/app/learn/customization/customize-admin/route/page.mdx index 049cb0b6edb96..37b5eacc96b94 100644 --- a/www/apps/book/app/learn/customization/customize-admin/route/page.mdx +++ b/www/apps/book/app/learn/customization/customize-admin/route/page.mdx @@ -1,196 +1,480 @@ import { Prerequisites } from "docs-ui" export const metadata = { - title: `${pageNumber} Create Brands List UI Route in Admin`, + title: `${pageNumber} Create Brands UI Route in Admin`, } # {metadata.title} - - -This chapter covers how to create a UI route (or page) that shows your brands as a step of the ["Customize Admin" chapter](../page.mdx). - - - -## What is a UI Route? - -A UI route is a React Component that adds a new page to your admin dashboard. - -The UI Route can be shown in the sidebar or added as a nested page. - ---- - -## Prerequisite: Add Retrieve Brand API Route +In this chapter, you'll add a UI route to the admin dashboard that shows the all [brands](../../custom-features/module/page.mdx) in a new page. You'll retrieve the brands from the server and display them in a table with pagination. -Before adding the UI route, you need an API route that retrieves all brands. +## 1. Get Brands API Route + +In a [previous chapter](../../extend-features/query-linked-records/page.mdx), you learned how to add an API route that retrieves brands and their products using [Query](../../../advanced-development/module-links/query/page.mdx). You'll expand that API route to support pagination, so that on the admin dashboard you can show the brands in a paginated table. -Create the file `src/api/admin/brands/route.ts` with the following content: +Replace or create the `GET` API route at `src/api/admin/brands/route.ts` with the following: + +export const apiRouteHighlights = [ + ["15", "metadata", "Pagination details, such as the total count or how many items were skipped."], + ["18", "remoteQueryConfig", "Query configurations parsed from the request."], +] -```ts title="src/api/admin/brands/route.ts" collapsibleLines="1-7" expandMoreButton="Show Imports" +```ts title="src/api/admin/brands/route.ts" highlights={apiRouteHighlights} +// other imports... import { MedusaRequest, MedusaResponse, } from "@medusajs/framework/http" -import { BRAND_MODULE } from "../../../modules/brand" -import BrandModuleService from "../../../modules/brand/service" export const GET = async ( req: MedusaRequest, res: MedusaResponse ) => { - const brandModuleService: BrandModuleService = req.scope.resolve( - BRAND_MODULE - ) - - const limit = req.query.limit || 15 - const offset = req.query.offset || 0 - - const [brands, count] = await brandModuleService.listAndCountBrands({}, { - skip: offset as number, - take: limit as number, + const query = req.scope.resolve("query") + + const { + data: brands, + metadata: { count, take, skip }, + } = await query.graph({ + entity: "brand", + ...req.remoteQueryConfig, }) - res.json({ + res.json({ brands, count, - limit, - offset, + limit: take, + offset: skip, }) } ``` -This adds a `GET` API route at `/admin/brands`. +In the API route, you use Query's `graph` method to retrieve the brands. In the method's object parameter, you spread the `remoteQueryConfig` property of the request object. This property holds configurations for pagination and retrieved fields. + +The query configurations are combined from default configurations, which you'll add next, and the request's query parameters: + +- `fields`: The fields to retrieve in the brands. +- `limit`: The maximum number of items to retrieve. +- `offset`: The number of items to skip before retrieving the returned items. + +When you pass pagination configurations to the `graph` method, the returned object has the pagination's details in a `metadata` property, whose value is an object having the following properties: + +- `count`: The total count of items. +- `take`: The maximum number of items returned in the `data` array. +- `skip`: The number of items skipped before retrieving the returned items. + +You return in the response the retrieved brands and the pagination configurations. + + + +Learn more about pagination with Query in [this chapter](../../../advanced-development/module-links/query/page.mdx#apply-pagination). + + + +--- + +## 2. Add Default Query Configurations + +Next, you'll set the default query configurations of the above API route and allow passing query parameters to change the configurations. + +Medusa provides a `validateAndTransformQuery` middleware that validates the accepted query parameters for a request and sets the default Query configuration. So, in `src/api/middlewares.ts`, add a new middleware configuration object: + +```ts title="src/api/middlewares.ts" +import { + defineMiddlewares, + validateAndTransformQuery, +} from "@medusajs/framework/http" +import { createFindParams } from "@medusajs/medusa/api/utils/validators" +// other imports... + +export const GetBrandsSchema = createFindParams() + +export default defineMiddlewares({ + routes: [ + // ... + { + matcher: "/admin/brands", + method: "GET", + middlewares: [ + validateAndTransformQuery( + GetBrandsSchema, + { + defaults: [ + "id", + "name", + "products.*", + ], + isList: true, + } + ), + ], + }, + + ], +}) +``` + +You apply the `validateAndTransformQuery` middleware on the `GET /admin/brands` API route. The middleware accepts two parameters: -In the API route, you resolve the Brand Module's main service and use its `listAndCountBrands` method to retrieve the list of brands with their total count. +- A [Zod](https://zod.dev/) schema that a request's query parameters must satisfy. Medusa provides a `createFindParams` utility that generates a Zod schema with the following properties: + - `fields`: A comma-separated string indicating the fields to retrieve. + - `limit`: The maximum number of items to retrieve. + - `offset`: The number of items to skip before retrieving the returned items. + - `order`: The name of the field to sort the items by. Learn more about sorting in [the API reference](!api!/admin#sort-order) +- An object of Query configurations having the following properties: + - `defaults`: An array of default fields and relations to retrieve. + - `isList`: Whether the API route returns a list of items. -This method accepts as a first parameter filters to apply on the retrieved data, and as a second parameter configurations for pagination. +By applying the above middleware, you can pass pagination configurations to `GET /admin/brands`, which will return a paginated list of brands. You'll see how it works when you create the UI route. -Learn more about the `listAndCount` method and its parameters in [this reference](!resources!/service-factory-reference/methods/listAndCount). +Learn more about using the `validateAndTransformQuery` middleware to configure Query in [this chapter](../../../advanced-development/module-links/query/page.mdx#request-query-configurations). --- -## Add a UI Route to Show Brands +## 3. Initialize JS SDK + +In your custom UI route, you'll retrieve the brands by sending a request to the Medusa server. Medusa has a [JS SDK](!resources!/js-sdk) that simplifies sending requests to the core API route. + +If you didn't follow the [previous chapter](../widget/page.mdx), create the file `src/admin/lib/sdk.ts` with the following content: -A UI route is created in a file named `page.tsx` under subdirectories of the `src/admin/routes` directory. The file’s default export must be the UI route’s React component. +![The directory structure of the Medusa application after adding the file](https://res.cloudinary.com/dza7lstvk/image/upload/v1733414606/Medusa%20Book/brands-admin-dir-overview-1_jleg0t.jpg) + +```ts title="src/admin/lib/sdk.ts" +import Medusa from "@medusajs/js-sdk" + +export const sdk = new Medusa({ + baseUrl: "http://localhost:9000", + debug: process.env.NODE_ENV === "development", + auth: { + type: "session", + }, +}) +``` -To create a UI route that shows the list of brands, create the file `src/admin/routes/brands/page.tsx` with the following content: +You initialize the SDK passing it the following options: + +- `baseUrl`: The URL to the Medusa server. +- `debug`: Whether to enable logging debug messages. This should only be enabled in development. +- `auth.type`: The authentication method used in the client application, which is `session` in the Medusa Admin dashboard. + +You can now use the SDK to send requests to the Medusa server. + + + +Learn more about the JS SDK and its options in [this reference](!resources!/js-sdk). + + + +--- + +## 4. Add a UI Route to Show Brands + +You'll now add the UI route that shows the paginated list of brands. A UI route is a React component created in a `page.tsx` file under a sub-directory of `src/admin/routes`. The file's path relative to src/admin/routes determines its path in the dashboard. + + + +Learn more about UI routes in [this chapter](../../../advanced-development/admin/ui-routes/page.mdx). + + + +So, to add the UI route at the `localhost:9000/app/brands` path, create the file `src/admin/routes/brands/page.tsx` with the following content: + +![Directory structure of the Medusa application after adding the UI route.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733472011/Medusa%20Book/brands-admin-dir-overview-3_syytld.jpg) export const uiRouteHighlights = [ - ["7", "brands", "State variable to store the brands."], - ["12", "fetch", "Retrieve the brands from the custom API route."] + ["8", "BrandsPage", "The UI route that displays a new page."], + ["23", "defineRouteConfig", "Export config to add a link for the UI route in the sidebar."], + ["24", "label", "The sidebar item's label."], + ["25", "icon", "The sidebar item's icon."] ] ```tsx title="src/admin/routes/brands/page.tsx" highlights={uiRouteHighlights} -import { Table, Container, Heading } from "@medusajs/ui" -import { useEffect, useState } from "react" import { defineRouteConfig } from "@medusajs/admin-sdk" import { TagSolid } from "@medusajs/icons" +import { Container, Heading } from "@medusajs/ui" +import { useQuery } from "@tanstack/react-query" +import { sdk } from "../../lib/sdk" +import { useMemo, useState } from "react" const BrandsPage = () => { - const [brands, setBrands] = useState< - Record[] - >([]) - - useEffect(() => { - fetch(`/admin/brands`, { - credentials: "include", - }) - .then((res) => res.json()) - .then(({ brands: brandsData }) => { - setBrands(brandsData) - }) - }, []) - + // TODO retrieve brands return (
- Brands -
-
- - - - ID - Name - - - - {brands.map((brand) => ( - - {brand.id} - {brand.name} - - ))} - -
+
+ Brands +
+ {/* TODO show brands */}
) } +export const config = defineRouteConfig({ + label: "Brands", + icon: TagSolid, +}) + export default BrandsPage +``` + +A route's file must export the React component that will be rendered in the new page. It must be the default export of the file. You can also export configurations that add a link in the sidebar for the UI route. You create these configurations using `defineRouteConfig` from the Admin Extension SDK. + +So far, you only show a "Brands" header. In admin customizations, use components from the [Medusa UI package](!ui!) to maintain a consistent user interface and design in the dashboard. + +### Add Table Component -// TODO export configuration +To show the brands with pagination functionalities, you'll create a new `Table` component that uses the UI package's [Table](!ui!/components/table) component with some alterations to match the design of the Medusa Admin. This new component is taken from the [Admin Components guide](!resources!/admin-components/components/table). + +Create the `Table` component in the file `src/admin/components/table.tsx`: + +![Directory structure of the Medusa application after adding the table component.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733472527/Medusa%20Book/brands-admin-dir-overview-4_avosrf.jpg) + +```tsx title="src/admin/components/table.tsx" +import { useMemo } from "react" +import { Table as UiTable } from "@medusajs/ui" + +export type TableProps = { + columns: { + key: string + label?: string + render?: (value: unknown) => React.ReactNode + }[] + data: Record[] + pageSize: number + count: number + currentPage: number + setCurrentPage: (value: number) => void +} + +export const Table = ({ + columns, + data, + pageSize, + count, + currentPage, + setCurrentPage, +}: TableProps) => { + const pageCount = useMemo(() => { + return Math.ceil(count / pageSize) + }, [count, pageSize]) + + const canNextPage = useMemo(() => { + return currentPage < pageCount - 1 + }, [currentPage, pageCount]) + const canPreviousPage = useMemo(() => { + return currentPage - 1 >= 0 + }, [currentPage]) + + const nextPage = () => { + if (canNextPage) { + setCurrentPage(currentPage + 1) + } + } + + const previousPage = () => { + if (canPreviousPage) { + setCurrentPage(currentPage - 1) + } + } + + console.log(pageCount, canNextPage, canPreviousPage, currentPage) + + return ( +
+ + + + {columns.map((column, index) => ( + + {column.label || column.key} + + ))} + + + + {data.map((item, index) => { + const rowIndex = "id" in item ? item.id as string : index + return ( + + {columns.map((column, index) => ( + + <> + {column.render && column.render(item[column.key])} + {!column.render && ( + <>{item[column.key] as string} + )} + + + ))} + + ) + })} + + + +
+ ) +} ``` -This adds a new page in the admin at `http://localhost:9000/app/brands`. +This component accepts the following props: -In the UI route's component, you retrieve the brands from the `/admin/brands` API route. You show the brands in a table. +- `columns`: An array of the table's columns. +- `data`: The rows in the table. +- `pageSize`: The maximum number of items shown in a page. +- `count`: The total number of items. +- `currentPage`: A zero-based index of the current page. +- `setCurrentPage`: A function to change the current page. - +In the component, you use the UI package's [Table](!ui!/components/table) component to display the data received as a prop in a table that supports pagination. -Admin customizations can use the [Medusa UI package](!ui!) to align your customizations with the admin's design. Also, [this guide](!resources!/admin-components) includes examples of common components in the Medusa Admin. +You can learn more about this component's implementation and how it works in the [Admin Components guide](!resources!/admin-components), which provides more examples of how to build common components in the Medusa Admin dashboard. - +### Retrieve Brands From API Route -### Add UI Route to the Sidebar +You'll now update the UI route to retrieve the brands from the API route you added earlier. -To add the UI route to the sidebar, replace the `TODO` at the end of the file with the following: +First, add the following type in `src/admin/routes/brands/page.tsx`: -```ts title="src/admin/routes/brands/page.tsx" -export const config = defineRouteConfig({ - label: "Brands", - icon: TagSolid, +```tsx title="src/admin/routes/brands/page.tsx" +type BrandsResponse = { + brands: { + id: string + name: string + }[] + count: number + limit: number + offset: number +} +``` + +This is the type of expected response from the `GET /admin/brands` API route. + +Then, replace the `// TODO retrieve brands` in the component with the following: + +export const queryHighlights = [ + ["1", "currentPage", "A zero-based index of the current page of items."], + ["2", "limit", "The maximum number of items per page."], + ["3", "offset", "The number of items to skip before retrieving the page's items."], + ["7", "useQuery", "Retrieve brands using Tanstack Query"], + ["8", "fetch", "Send a request to a custom API route."], + ["8", "`/admin/brands`", "The API route's path."], + ["9", "query", "Query parameters to pass in the request"] +] + +```tsx title="src/admin/routes/brands/page.tsx" highlights={queryHighlights} +const [currentPage, setCurrentPage] = useState(0) +const limit = 15 +const offset = useMemo(() => { + return currentPage * limit +}, [currentPage]) + +const { data } = useQuery({ + queryFn: () => sdk.client.fetch(`/admin/brands`, { + query: { + limit, + offset, + }, + }), + queryKey: [["brands", limit, offset]], }) ``` -You export a `config` variable defined using the `defineRouteConfig` utility. +You first define pagination-related variables: + +- `currentPage`: A zero-based index of the current page of items. +- `limit`: The maximum number of items per page. +- `offset`: The number of items to skip before retrieving the page's items. This is calculated from the `currentPage` and `limit` variables. + +Then, you use `useQuery` from [Tanstack (React) Query](https://tanstack.com/query/latest) to query the Medusa server. Tanstack Query provides features like asynchronous state management and optimized caching. + +In the `queryFn` function that executes the query, you use the JS SDK's `client.fetch` method to send a request to your custom API route. The first parameter is the route's path, and the second is an object of request configuration and data. You pass the query parameters in the `query` property. + +This sends a request to the [Get Brands API route](#1-get-brands-api-route), passing the pagination query parameters. Whenever `currentPage` is updated, the `offset` is also updated, which will send a new request to retrieve the brands for the current page. + +### Display Brands Table + +Finally, you'll display the brands in a table using the component you created earlier. Import the component at the top of `src/admin/routes/brands/page.tsx`: + +```tsx title="src/admin/routes/brands/page.tsx" +import { Table } from "../../components/table" +``` + +Then, replace the `{/* TODO show brands */}` in the return statement with the following: -This indicates that a new item should be added to the sidebar with the title `Brands` and an icon from the [Medusa Icons package](!ui!/icons/overview). +```tsx title="src/admin/routes/brands/page.tsx" + +``` + +This renders a table that shows the ID and name of the brands. --- ## Test it Out -To test it out, start the Medusa application and login into the Medusa Admin. +To test out the UI route, start the Medusa application: -You'll find a new "Brands" sidebar item. If you click on it, a new page opens showing the list of brands in your store. +```bash npm2yarn +npm run dev +``` + +Then, open the admin dashboard at `http://localhost:9000/app`. After you log in, you'll find a new "Brands" sidebar item. Click on it to see the brands in your store. You can also go to `http://localhost:9000/app/brands` to see the page. + +![A new sidebar item is added for the new brands UI route. The UI route shows the table of brands with pagination.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733421074/Medusa%20Book/Screenshot_2024-12-05_at_7.46.52_PM_slcdqd.png) --- ## Summary -By following the examples of the previous chapters, you: +By following the previous chapters, you: -- Created a widget that showed the brand of a product in the Medusa Admin. -- Created a UI route that showed the list of brands in the Medusa Admin. +- Injected a widget into the product details page to show the product's brand. +- Created a UI route in the Medusa Admin that shows the list of brands. --- -## Next Steps +## Next Steps: Integrate Third-Party Systems + +Your customizations often span across systems, where you need to retrieve data or perform operations in a third-party system. -In the next chapters, you'll learn how to integrate third-party systems into your Medusa application to sync brands. +In the next chapters, you'll learn about the concepts that facilitate integrating third-party systems in your application. You'll integrate a dummy third-party system and sync the brands between it and the Medusa application. diff --git a/www/apps/book/app/learn/customization/customize-admin/widget/page.mdx b/www/apps/book/app/learn/customization/customize-admin/widget/page.mdx index 537627544804d..a78b64474ec37 100644 --- a/www/apps/book/app/learn/customization/customize-admin/widget/page.mdx +++ b/www/apps/book/app/learn/customization/customize-admin/widget/page.mdx @@ -1,64 +1,132 @@ import { Prerequisites } from "docs-ui" export const metadata = { - title: `${pageNumber} Show Brand of Product in Admin`, + title: `${pageNumber} Guide: Add Product's Brand Widget in Admin`, } # {metadata.title} - +In this chapter, you'll customize the product details page of the Medusa Admin dashboard to show the product's [brand](../../custom-features/module/page.mdx). You'll create a widget that is injected into a pre-defined zone in the page, and in the widget you'll retrieve the product's brand from the server and display it. -This chapter covers how to show the brand of a product in the Medusa Admin using a widget as a step of the ["Customize Admin" chapter](../page.mdx). + + +## 1. Initialize JS SDK + +In your custom widget, you'll retrieve the product's brand by sending a request to the Medusa server. Medusa has a [JS SDK](!resources!/js-sdk) that simplifies sending requests to the server's API routes. + +So, you'll start by configuring the JS SDK. Create the file `src/admin/lib/sdk.ts` with the following content: + +![The directory structure of the Medusa application after adding the file](https://res.cloudinary.com/dza7lstvk/image/upload/v1733414606/Medusa%20Book/brands-admin-dir-overview-1_jleg0t.jpg) + +```ts title="src/admin/lib/sdk.ts" +import Medusa from "@medusajs/js-sdk" + +export const sdk = new Medusa({ + baseUrl: "http://localhost:9000", + debug: process.env.NODE_ENV === "development", + auth: { + type: "session", + }, +}) +``` + +You initialize the SDK passing it the following options: + +- `baseUrl`: The URL to the Medusa server. +- `debug`: Whether to enable logging debug messages. This should only be enabled in development. +- `auth.type`: The authentication method used in the client application, which is `session` in the Medusa Admin dashboard. + +You can now use the SDK to send requests to the Medusa server. + + + +Learn more about the JS SDK and its options in [this reference](!resources!/js-sdk). -## Widget to Show Brand in Product Details +--- + +## 2. Add Widget to Product Details Page + +You'll now add a widget to the product-details page. A widget is a React component that's injected into pre-defined zones in the Medusa Admin dashboard. It's created in a `.tsx` file under the `src/admin/widgets` directory. + + + +Learn more about widgets in [this documentation](../../../advanced-development/admin/widgets/page.mdx). + + To create a widget that shows a product's brand in its details page, create the file `src/admin/widgets/product-brand.tsx` with the following content: +![Directory structure of the Medusa application after adding the widget](https://res.cloudinary.com/dza7lstvk/image/upload/v1733414684/Medusa%20Book/brands-admin-dir-overview-2_eq5xhi.jpg) + export const highlights = [ - ["7", "data", "Receive the product's details as a prop"], - ["9", "brand", "A state variable to store the brand"], - ["19", "fetch", "Retrieve the brand of a product using the custom API route"], - ["41", "zone", "Show the widget at the top of the product details page."] + ["14", "ProductBrandWidget", "Widget to inject into the page"], + ["15", "data", "Receive the product's details as a prop"], + ["17", "useQuery", "Use Tanstack Query to send the request to the server with the JS SDK."], + ["18", "sdk", "Send the request to retrieve the product with the JS SDK."], + ["19", "fields", "Specify the product's brand to be retrieved."], + ["23", "brandName", "Get brand name from the query request."], + ["53", "defineWidgetConfig", "Export the widget's configurations"], + ["54", "zone", "Show the widget at the top of the product details page."] ] ```tsx title="src/admin/widgets/product-brand.tsx" highlights={highlights} import { defineWidgetConfig } from "@medusajs/admin-sdk" import { DetailWidgetProps, AdminProduct } from "@medusajs/framework/types" -import { useEffect, useState } from "react" -import { Container, Heading } from "@medusajs/ui" +import { clx, Container, Heading, Text } from "@medusajs/ui" +import { useQuery } from "@tanstack/react-query" +import { sdk } from "../lib/sdk" + +type AdminProductBrand = AdminProduct & { + brand?: { + id: string + name: string + } +} const ProductBrandWidget = ({ - data, + data: product, }: DetailWidgetProps) => { - const [brand, setBrand] = useState< - Record | undefined - >() - const [loading, setLoading] = useState(true) - - useEffect(() => { - if (!loading) { - return - } + const { data: queryResult } = useQuery({ + queryFn: () => sdk.admin.product.retrieve(product.id, { + fields: "+brand.*", + }), + queryKey: [["product", product.id]], + }) + const brandName = (queryResult?.product as AdminProductBrand)?.brand?.name - fetch(`/admin/products/${data.id}?fields=+brand.*`, { - credentials: "include", - }) - .then((res) => res.json()) - .then(({ product }) => { - setBrand(product.brand) - setLoading(false) - }) - }, [loading]) - return (
- Brand +
+ Brand +
+
+
+ + Name + + + + {brandName || "-"} +
- {loading && Loading...} - {brand && Name: {brand.name}}
) } @@ -70,32 +138,41 @@ export const config = defineWidgetConfig({ export default ProductBrandWidget ``` -This adds a widget at the top of the product's details page. +A widget's file must export: - +- A React component to be rendered in the specified injection zone. The component must be the file's default export. +- A configuration object created with `defineWidgetConfig` from the Admin Extension SDK. The function receives an object as a parameter that has a `zone` property, whose value is the zone to inject the widget to. -Learn more about widgets [in this guide](../../../basics/admin-customizations/page.mdx). +Since the widget is injected at the top of the product details page, the widget receives the product's details as a parameter. - +In the widget, you use [Tanstack (React) Query](https://tanstack.com/query/latest) to query the Medusa server. Tanstack Query provides features like asynchronous state management and optimized caching. In the `queryFn` function that executes the query, you use the JS SDK to send a request to the [Get Product API Route](!api!/admin#products_getproductsid), passing `+brand.*` in the `fields` query parameter to retrieve the product's brand. -Widgets created in a details page receive the targetted item in a `data` prop. So, the `ProductBrandWidget` receives the product's details in the `data` prop. +You then render a section that shows the brand's name. In admin customizations, use components from the [Medusa UI package](!ui!) to maintain a consistent user interface and design in the dashboard. -In the widget, you fetch the product's brand using the [Get Product API route](!api!/admin#products_getproductsid), passing it the query parameter `fields=+brand.*` to retrieve the product's brand. +--- - +## Test it Out -Admin customizations can use the [Medusa UI package](!ui!) to align your customizations with the admin's design. Also, [this guide](!resources!/admin-components) includes examples of common components in the Medusa Admin. +To test out your widget, start the Medusa application: - +```bash npm2yarn +npm run dev +``` + +Then, open the admin dashboard at `http://localhost:9000/app`. After you log in, open the page of a product that has a brand. You'll see a new section at the top showing the brand's name. + +![The widget is added as the first section of the product details page.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733414415/Medusa%20Book/Screenshot_2024-12-05_at_5.59.25_PM_y85m14.png) --- -## Test it Out +## Admin Components Guides + +When building your widget, you may need more complicated components. For example, you may add a form to the above widget to set the product's brand. -Start your Medusa application and go to a product's details page in the Medusa Admin, you'll find a new block at the top of the page showing the product's brand. +The [Admin Components guides](!resources!/admin-components) show you how to build and use common components in the Medusa Admin, such as forms, tables, JSON data viewer, and more. The components in the guides also follow the Medusa Admin's design convention. --- -## Next Chapter: Add List of Brands Page +## Next Chapter: Add UI Route for Brands -In the next chapter, you'll add a new page or UI route that displays the list of brands in your application. +In the next chapter, you'll add a UI route that displays the list of brands in your application and allows admin users. diff --git a/www/apps/book/generated/edit-dates.mjs b/www/apps/book/generated/edit-dates.mjs index 43c3150d58874..ec5da6af6327e 100644 --- a/www/apps/book/generated/edit-dates.mjs +++ b/www/apps/book/generated/edit-dates.mjs @@ -94,9 +94,9 @@ export const generatedEditDates = { "app/learn/customization/custom-features/workflow/page.mdx": "2024-11-28T10:47:28.084Z", "app/learn/customization/extend-features/extend-create-product/page.mdx": "2024-12-05T09:26:15.796Z", "app/learn/customization/custom-features/page.mdx": "2024-11-28T08:21:55.207Z", - "app/learn/customization/customize-admin/page.mdx": "2024-09-12T12:25:29.853Z", - "app/learn/customization/customize-admin/route/page.mdx": "2024-10-07T12:43:11.335Z", - "app/learn/customization/customize-admin/widget/page.mdx": "2024-12-05T10:40:56.611Z", + "app/learn/customization/customize-admin/page.mdx": "2024-12-06T07:21:02.303Z", + "app/learn/customization/customize-admin/route/page.mdx": "2024-12-06T08:29:57.834Z", + "app/learn/customization/customize-admin/widget/page.mdx": "2024-12-06T08:15:11.426Z", "app/learn/customization/extend-features/define-link/page.mdx": "2024-12-04T17:15:16.004Z", "app/learn/customization/extend-features/page.mdx": "2024-09-12T12:38:57.394Z", "app/learn/customization/extend-features/query-linked-records/page.mdx": "2024-12-05T10:36:32.357Z", diff --git a/www/apps/book/sidebar.mjs b/www/apps/book/sidebar.mjs index f61486a6b5961..382db13feb7cd 100644 --- a/www/apps/book/sidebar.mjs +++ b/www/apps/book/sidebar.mjs @@ -142,7 +142,7 @@ export const sidebar = numberSidebarItems( children: [ { type: "link", - title: "Add Widgets", + title: "Add Widget", path: "/learn/customization/customize-admin/widget", }, { From a76b5336044b9ee367a5ff80a7706a2891a8eecf Mon Sep 17 00:00:00 2001 From: Shahed Nasser Date: Fri, 6 Dec 2024 17:56:27 +0200 Subject: [PATCH 02/11] docs: revise last chapters of customizations (#10480) --- .../custom-features/workflow/page.mdx | 2 +- .../integrate-systems/handle-event/page.mdx | 286 +++++++++++++----- .../customization/integrate-systems/page.mdx | 25 +- .../integrate-systems/schedule-task/page.mdx | 250 ++++++++------- .../integrate-systems/service/page.mdx | 178 +++++------ .../learn/customization/next-steps/page.mdx | 24 +- www/apps/book/generated/edit-dates.mjs | 12 +- www/apps/book/sidebar.mjs | 6 +- 8 files changed, 447 insertions(+), 336 deletions(-) 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 3d5b40bd3f4ae..aa4f20d261c1d 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 @@ -124,7 +124,7 @@ You can now create the workflow that runs the `createBrandStep`. A workflow is c Add the following content in the same `src/workflows/create-brand.ts` file: -```ts +```ts title="src/workflows/create-brand.ts" // other imports... import { // ... diff --git a/www/apps/book/app/learn/customization/integrate-systems/handle-event/page.mdx b/www/apps/book/app/learn/customization/integrate-systems/handle-event/page.mdx index d0cad26da6354..dc9f7e51ad619 100644 --- a/www/apps/book/app/learn/customization/integrate-systems/handle-event/page.mdx +++ b/www/apps/book/app/learn/customization/integrate-systems/handle-event/page.mdx @@ -1,31 +1,43 @@ import { Prerequisites } from "docs-ui" export const metadata = { - title: `${pageNumber} Brand Example: Handle Event to Sync Third-Party System`, + title: `${pageNumber} Guide: Sync Brands from Medusa to CMS`, } # {metadata.title} - +In the [previous chapter](../service/page.mdx), you created a CMS Module that integrates a dummy third-party system. You can now perform actions using that module within your custom flows. -This chapter covers how to emit an event when a brand is created, listen to that event in a subscriber, and create the brand in the third-party system as a step of the ["Integrate Systems" chapter](../page.mdx). +In another previous chapter, you [added a workflow](../../custom-features/workflow/page.mdx) that creates a brand. After integrating the CMS, you want to sync that brand to the third-party system as well. + +Medusa has an event system that emits events when an operation is performed. It allows you to listen to those events and perform an asynchronous action in a function called a [subscriber](../../../basics/events-and-subscribers/page.mdx). This is useful to perform actions that aren't integral to the original flow, such as syncing data to a third-party system. + + + +Learn more about Medusa's event system and subscribers in [this chapter](../../../basics/events-and-subscribers/page.mdx). -## 1. Emit Custom Event for Brand Creation +In this chapter, you'll modify the `createBrandWorkflow` you created before to emit a custom event that indicates a brand was created. Then, you'll listen to that event in a subscriber to sync the brand to the third-party CMS. You'll implement the sync logic within a workflow that you execute in the subscriber. -To handle brand-creation event, you'll emit a custom event when a brand is created. +## 1. Emit Event in createBrandWorkflow -In the `createBrandWorkflow` defined in `src/workflows/create-brand/index.ts`, use the `emitEventStep` helper step imported from `@medusajs/medusa/core-flows` after the `createBrandStep`: +Since syncing the brand to the third-party system isn't integral to creating a brand, you'll emit a custom event indicating that a brand was created. + +Medusa provides an `emitEventStep` that allows you to emit an event in your workflows. So, in the `createBrandWorkflow` defined in `src/workflows/create-brand.ts`, use the `emitEventStep` helper step after the `createBrandStep`: export const eventHighlights = [ ["13", "emitEventStep", "Emit an event."], @@ -58,126 +70,183 @@ export const createBrandWorkflow = createWorkflow( ) ``` -The `emitEventStep` accepts as a parameter an object having two properties: +The `emitEventStep` accepts an object parameter having two properties: + +- `eventName`: The name of the event to emit. You'll use this name later to listen to the event in a subscriber. +- `data`: The data payload to emit with the event. This data is passed to subscribers that listen to the event. You add the brand's ID to the data payload, informing the subscribers which brand was created. -- `eventName`: The name of the event to emit. -- `data`: The data payload to emit with the event. This is useful for subscribers to access the created brand. +You'll learn how to handle this event in a later step. --- ## 2. Create Sync to Third-Party System Workflow -Next, you'll create the workflow that syncs the created brand to the third-party system. +The subscriber that will listen to the `brand.created` event will sync the created brand to the third-party CMS. So, you'll implement the syncing logic in a workflow, then execute the workflow in the subscriber. -Create the file `src/workflows/sync-brand-to-system/index.ts` with the following content: +Workflows have a built-in durable execution engine that helps you complete tasks spanning multiple systems. Also, their rollback mechanism ensures that data is consistent across systems even when errors occur during execution. -```ts title="src/workflows/sync-brand-to-system/index.ts" -import { - createWorkflow, - WorkflowResponse, -} from "@medusajs/framework/workflows-sdk" + -export type SyncBrandToSystemInput = { - id: string -} +Learn more about workflows in [this chapter](../../../basics/workflows/page.mdx). -export const syncBrandToSystemWorkflow = createWorkflow( - "sync-brand-to-system", - (input: SyncBrandToSystemInput) => { - // ... - } -) -``` + + +You'll create a `syncBrandToSystemWorkflow` that has two steps: + +- `useQueryGraphStep`: a step that Medusa provides to retrieve data using [Query](../../../advanced-development/module-links/query/page.mdx). You'll use this to retrieve the brand's details using its ID. +- `syncBrandToCmsStep`: a step that you'll create to sync the brand to the CMS. -This defines an empty workflow and its expected input. +### syncBrandToCmsStep -### Create createBrandInSystemStep +To implement the step that syncs the brand to the CMS, create the file `src/workflows/sync-brands-to-cms.ts` with the following content: -Next, create the step that syncs the brand in the file `src/workflows/sync-brand-to-system/steps/create-brand-in-system.ts`: +![Directory structure of the Medusa application after adding the file](https://res.cloudinary.com/dza7lstvk/image/upload/v1733493547/Medusa%20Book/cms-dir-overview-4_u5t0ug.jpg) -export const stepHighlights = [ - ["18", "createBrand", "Create a brand in the third-party system."], - ["27", "deleteBrand", "Delete the brand in the third-party system if an error occurs."] +export const syncStepHighlights = [ + ["8", "InferTypeOf", "Get the `Brand` data model as a type."], + ["14", "cmsModuleService", "Resolve the CMS Module's service from the container."], + ["16", "createBrand", "Create the brand in the third-party CMS."], + ["18", "brand.id", "Pass the brand's ID to the compensation function."], + ["27", "deleteBrand", "Delete the brand in the third-party CMS if an error occurs."] ] -```ts title="src/workflows/sync-brand-to-system/steps/create-brand-in-system.ts" highlights={stepHighlights} collapsibleLines="1-8" expandButtonLabel="Show Imports" -import { - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" -import { SyncBrandToSystemInput } from ".." -import BrandModuleService from "../../../modules/brand/service" -import { BRAND_MODULE } from "../../../modules/brand" +```ts title="src/workflows/sync-brands-to-cms.ts" highlights={syncStepHighlights} collapsibleLines="1-6" expandButtonLabel="Show Imports" +import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" +import { InferTypeOf } from "@medusajs/framework/types" +import { Brand } from "../modules/brand/models/brand" +import { CMS_MODULE } from "../modules/cms" +import CmsModuleService from "../modules/cms/service" + +type SyncBrandToCmsStepInput = { + brand: InferTypeOf +} -export const createBrandInSystemStep = createStep( - "create-brand-in-system", - async ({ id }: SyncBrandToSystemInput, { container }) => { - const brandModuleService: BrandModuleService = container.resolve( - BRAND_MODULE - ) +const syncBrandToCmsStep = createStep( + "sync-brand-to-cms", + async ({ brand }: SyncBrandToCmsStepInput, { container }) => { + const cmsModuleService: CmsModuleService = container.resolve(CMS_MODULE) - const brand = await brandModuleService.retrieveBrand(id) - - await brandModuleService.client.createBrand(brand) + await cmsModuleService.createBrand(brand) return new StepResponse(null, brand.id) }, async (id, { container }) => { - const brandModuleService: BrandModuleService = container.resolve( - BRAND_MODULE - ) + if (!id) { + return + } + + const cmsModuleService: CmsModuleService = container.resolve(CMS_MODULE) - await brandModuleService.client.deleteBrand(id) + await cmsModuleService.deleteBrand(id) } ) ``` -This step resolves the Brand Module's main service and uses its `client` property to access its internal service that integrates the third-party system. +You create the `syncBrandToCmsStep` that accepts a brand as an input. In the step, you resolve the CMS Module's service from the [Medusa container](../../../basics/medusa-container/page.mdx) and use its `createBrand` method. This method will create the brand in the third-party CMS. + +You also pass the brand's ID to the step's compensation function. In this function, you delete the brand in the third-party CMS if an error occurs during the workflow's execution. + + + +Learn more about compensation functions in [this chapter](../../../advanced-development/workflows/compensation-function/page.mdx). -In the step, you use the `createBrand` method of the client to create the brand in the third-party system. + -In the compensation function, you undo the step's action using the `deleteBrand` method of the client. +### Create Workflow -### Add Step to Workflow +You can now create the workflow that uses the above step. Add the workflow to the same `src/workflows/sync-brands-to-cms.ts` file: -Finally, add this step to the `syncBrandToSystemWorkflow` in `src/workflows/sync-brand-to-system/index.ts`: +export const syncWorkflowHighlights = [ + ["19", "useQueryGraphStep", "Retrieve the brand's details."], + ["23", "id", "Filter by the brand's ID."], + ["26", "throwIfKeyNotFound", "Throw an error if a brand with the specified ID doesn't exist."], + ["30", "syncBrandToCmsStep", "Create the brand in the third-party CMS."] +] -```ts title="src/workflows/sync-brand-to-system/index.ts" +```ts title="src/workflows/sync-brands-to-cms.ts" highlights={syncWorkflowHighlights} // other imports... -import { createBrandInSystemStep } from "./steps/create-brand-in-system" +import { + // ... + createWorkflow, + WorkflowResponse, +} from "@medusajs/framework/workflows-sdk" +import { useQueryGraphStep } from "@medusajs/medusa/core-flows" // ... -export const syncBrandToSystemWorkflow = createWorkflow( - "sync-brand-to-system", - (input: SyncBrandToSystemInput) => { - createBrandInSystemStep(input) +type SyncBrandToCmsWorkflowInput = { + id: string +} - return new WorkflowResponse(undefined) +export const syncBrandToCmsWorkflow = createWorkflow( + "sync-brand-to-cms", + (input: SyncBrandToCmsWorkflowInput) => { + // @ts-ignore + const { data: brands } = useQueryGraphStep({ + entity: "brand", + fields: ["*"], + filters: { + id: input.id, + }, + options: { + throwIfKeyNotFound: true, + }, + }) + + syncBrandToCmsStep({ + brand: brands[0], + } as SyncBrandToCmsStepInput) + + return new WorkflowResponse({}) } ) ``` -The workflow now calls the step and returns an `undefined` result. +You create a `syncBrandToCmsWorkflow` that accepts the brand's ID as input. The workflow has the following steps: + +- `useQueryGraphStep`: Retrieve the brand's details using Query. You pass the brand's ID as a filter, and set the `throwIfKeyNotFound` option to true so that the step throws an error if a brand with the specified ID doesn't exist. +- `syncBrandToCmsStep`: Create the brand in the third-party CMS. + +You'll execute this workflow in the subscriber next. + + + +Learn more about `useQueryGraphStep` in [this reference](!resources!/references/helper-steps/useQueryGraphStep). + + --- ## 3. Handle brand.created Event -To handle the `brand.created` event, create a subscriber at `src/subscribers/brand-created.ts` with the following content: +You now have a workflow with the logic to sync a brand to the CMS. You need to execute this workflow whenever the `brand.created` event is emitted. So, you'll create a subscriber that listens to and handle the event. + +Subscribers are created in a TypeScript or JavaScript file under the `src/subscribers` directory. So, create the file `src/subscribers/brand-created.ts` with the following content: + +![Directory structure of the Medusa application after adding the subscriber](https://res.cloudinary.com/dza7lstvk/image/upload/v1733493774/Medusa%20Book/cms-dir-overview-5_iqqwvg.jpg) + +export const subscriberHighlights = [ + ["7", "brandCreatedHandler", "The function to execute when the event is emitted."], + ["8", "data", "The event's data payload."], + ["9", "container", "The Medusa container used to resolve resources."], + ["10", "id: string", "The expected data payload's type."], + ["11", "syncBrandToCmsWorkflow", "Execute the workflow to sync the brand to the CMS."], + ["16", "config", "Export the subscriber's configurations."], + ["17", "event", "The event that the subscriber is listening to."] +] -```ts title="src/subscribers/brand-created.ts" +```ts title="src/subscribers/brand-created.ts" highlights={subscriberHighlights} import type { SubscriberConfig, SubscriberArgs, } from "@medusajs/framework" -import { syncBrandToSystemWorkflow } from "../workflows/sync-brand-to-system" +import { syncBrandToCmsWorkflow } from "../workflows/sync-brands-to-cms" export default async function brandCreatedHandler({ event: { data }, container, }: SubscriberArgs<{ id: string }>) { - await syncBrandToSystemWorkflow(container).run({ + await syncBrandToCmsWorkflow(container).run({ input: data, }) } @@ -187,27 +256,84 @@ export const config: SubscriberConfig = { } ``` -The subscriber handler accesses the event payload in the `event.data` property of its object parameter. +A subscriber file must export: + +- The asynchronous function that's executed when the event is emitted. This must be the file's default export. +- An object that holds the subscriber's configurations. It has an `event` property that indicates the name of the event that the subscriber is listening to. + +The subscriber function accepts an object parameter that has two properties: + +- `event`: An object of event details. Its `data` property holds the event's data payload, which is the brand's ID. +- `container`: The Medusa container used to resolve framework and commerce tools. + +In the function, you execute the `syncBrandToCmsWorkflow`, passing it the data payload as an input. So, everytime a brand is created, Medusa will execute this function, which in turn executes the workflow to sync the brand to the CMS. -Learn more about subscribers [in this guide](../../../basics/events-and-subscribers/page.mdx). +Learn more about subscribers in [this chapter](../../../basics/events-and-subscribers/page.mdx). -It then executes the `syncBrandToSystemWorkflow`, passing it the ID of the brand to create in the third-party system. - --- ## Test it Out -To test it out, start the Medusa application and create a brand using the API route created in a [previous chapter](../../custom-features/api-route/page.mdx#test-api-route). +To test the subscriber and workflow out, you'll use the [Create Brand API route](../../custom-features/api-route/page.mdx) you created in a previous chapter. -If you check the logs, you'll find the `brand.created` event was emitted, and that the request to the third-party system was simulated. +First, start the Medusa application: ---- +```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' \ +-H 'Content-Type: application/json' \ +--data-raw '{ + "email": "admin@medusa-test.com", + "password": "supersecret" +}' +``` + +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). -## Next Chapter: Sync Brand from Third-Party System to Medusa + + +Then, send a `POST` request to `/admin/brands`, passing the token received from the previous request in the `Authorization` header: + +```bash +curl -X POST 'http://localhost:9000/admin/brands' \ +-H 'Content-Type: application/json' \ +-H 'Authorization: Bearer {token}' \ +--data '{ + "name": "Acme" +}' +``` + +This request returns the created brand. If you check the logs, you'll find the `brand.created` event was emitted, and that the request to the third-party system was simulated: + +```plain +info: Processing brand.created which has 1 subscribers +http: POST /admin/brands ← - (200) - 16.418 ms +info: Sending a POST request to /brands. +info: Request Data: { + "id": "01JEDWENYD361P664WRQPMC3J8", + "name": "Acme", + "created_at": "2024-12-06T11:42:32.909Z", + "updated_at": "2024-12-06T11:42:32.909Z", + "deleted_at": null +} +info: API Key: "123" +``` + +--- -In the next chapter, you'll learn how to sync brands in the third-party system into Medusa using a workflow and a scheduled job. +## Next Chapter: Sync Brand from Third-Party CMS to Medusa +You can also automate syncing data from a third-party system to Medusa at a regular interval. In the next chapter, you'll learn how to sync brands from the third-party CMS to Medusa once a day. diff --git a/www/apps/book/app/learn/customization/integrate-systems/page.mdx b/www/apps/book/app/learn/customization/integrate-systems/page.mdx index 588ffbabd7090..ae4b51a07dd0a 100644 --- a/www/apps/book/app/learn/customization/integrate-systems/page.mdx +++ b/www/apps/book/app/learn/customization/integrate-systems/page.mdx @@ -4,25 +4,22 @@ export const metadata = { # {metadata.title} -In this chapter, you'll learn how to integrate a third-party system into Medusa. +Commerce applications often connect to third-party systems that provide additional or specialized features. For example, you may integrate a Content-Management System (CMS) for rich content features, a payment provider to process credit-card payments, and a notification service to send emails. -## How to Integrate a Third-Party System? +Medusa's framework facilitates integrating these systems and orchestrating operations across them, saving you the effort of managing them yourself. You won't find those capabilities in other commerce platforms that in these scenarios become a bottleneck to building customizations and iterating quickly. -To integrate a third-party system into Medusa, you: +In Medusa, you integrate a third-party system by: -1. Implement the methods to interact with the system in a service. It can either be the main module's service, or an internal service in the module that's used by the main one. -2. Implement in workflows custom features around the integration, such as sending data to the third-party system. - - Workflows roll-back mechanism ensures data consistency. This is essential as you integrate multiple systems into your application. -3. Use the workflow in other resources to expose or utilize the custom functionality. +1. Creating a module whose service provides the methods to connect to and perform operations in the third-party system. +2. Building workflows that complete tasks spanning across systems. You use the module that integrates a third-party system in the workflow's steps. +3. Executing the workflows you built in an [API route](../../basics/api-routes/page.mdx), at a scheduled time, or when an event is emitted. --- -## Next Chapters: Syncing Brands Example +## Next Chapters: Sync Brands Example -In the next chapters, you'll implement an example of syncing brands with a third-party system, such as a Content Management System (CMS). +In the previous chapters, you've [added brands](../custom-features/module/page.mdx) to your Medusa application. In the next chapters, you will: -That requires: - -1. Implementing the service that integrates the third-party system. -2. Creating a brand in the third-party system when a brand is created in Medusa. -2. Retrieving the brands from the third-party system to sync them with Medusa's brands at a scheduled interval. +1. Integrate a dummy third-party CMS in the Brand Module. +2. Sync brands to the CMS when a brand is created. +2. Sync brands from the CMS at a daily schedule. diff --git a/www/apps/book/app/learn/customization/integrate-systems/schedule-task/page.mdx b/www/apps/book/app/learn/customization/integrate-systems/schedule-task/page.mdx index ba195c5cec52f..5c21a584edbb2 100644 --- a/www/apps/book/app/learn/customization/integrate-systems/schedule-task/page.mdx +++ b/www/apps/book/app/learn/customization/integrate-systems/schedule-task/page.mdx @@ -1,87 +1,106 @@ import { Prerequisites } from "docs-ui" export const metadata = { - title: `${pageNumber} Schedule Syncing Brands from Third-Party System`, + title: `${pageNumber} Guide: Schedule Syncing Brands from CMS`, } # {metadata.title} - +In the previous chapters, you've [integrated a third-party CMS](../service/page.mdx) and implemented the logic to [sync created brands](../handle-event/page.mdx) from Medusa to the CMS. -This chapter covers how to use workflows and scheduled jobs to sync brands from the third-party system as the last step of the ["Integrate Systems" chapter](../page.mdx). +However, when you integrate a third-party system, you want the data to be in sync between the Medusa application and the system. One way to do so is by automatically syncing the data once a day. + +You can create an action to be automatically executed at a specified interval using scheduled jobs. A scheduled job is an asynchronous function with a specified schedule of when the Medusa application should run it. Scheduled jobs are useful to automate repeated tasks. + + + +Learn more about scheduled jobs in [this chapter](../../../basics/scheduled-jobs/page.mdx). -## 1. Implement Syncing Workflow +In this chapter, you'll create a scheduled job that triggers syncing the brands from the third-party CMS to Medusa once a day. You'll implement the syncing logic in a workflow, and execute that workflow in the scheduled job. -Start by defining the workflow that syncs the brand from the third-party system. +--- + +## 1. Implement Syncing Workflow + +You'll start by implementing the syncing logic in a workflow, then execute the workflow later in the scheduled job. + +Workflows have a built-in durable execution engine that helps you complete tasks spanning multiple systems. Also, their rollback mechanism ensures that data is consistent across systems even when errors occur during execution. + + + +Learn more about workflows in [this chapter](../../../basics/workflows/page.mdx). + + -The workflow has the following steps: +This workflow will have three steps: -1. Retrieve brands from the third-party system. -2. Create new brands in Medusa. -3. Update existing brands in Medusa. +1. `retrieveBrandsFromCmsStep` to retrieve the brands from the CMS. +2. `createBrandsStep` to create the brands retrieved in the first step that don't exist in Medusa. +3. `updateBrandsStep` to update the brands retrieved in the first step that exist in Medusa. -### Retrieve Brands Step +### retrieveBrandsFromCmsStep -To create the step that retrieves the brands from the third-party service, create the file `src/workflows/sync-brands-from-system/steps/retrieve-brands-from-system.ts` with the following content: +To create the step that retrieves the brands from the third-party CMS, create the file `src/workflows/sync-brands-from-cms.ts` with the following content: -```ts title="src/workflows/sync-brands-from-system/steps/retrieve-brands-from-system.ts" collapsibleLines="1-7" expandButtonLabel="Show Imports" +![Directory structure of the Medusa application after creating the file.](https://res.cloudinary.com/dza7lstvk/image/upload/v1733494196/Medusa%20Book/cms-dir-overview-6_z1omsi.jpg) + +```ts title="src/workflows/sync-brands-from-cms.ts" collapsibleLines="1-7" expandButtonLabel="Show Imports" import { createStep, StepResponse, } from "@medusajs/framework/workflows-sdk" -import BrandModuleService from "../../../modules/brand/service" -import { BRAND_MODULE } from "../../../modules/brand" +import CmsModuleService from "../modules/cms/service" +import { CMS_MODULE } from "../modules/cms" -export const retrieveBrandsFromSystemStep = createStep( - "retrieve-brands-from-system", +const retrieveBrandsFromCmsStep = createStep( + "retrieve-brands-from-cms", async (_, { container }) => { - const brandModuleService: BrandModuleService = container.resolve( - BRAND_MODULE + const cmsModuleService: CmsModuleService = container.resolve( + CMS_MODULE ) - const brands = await brandModuleService.client.retrieveBrands() + const brands = await cmsModuleService.retrieveBrands() return new StepResponse(brands) } ) ``` -In this step, you resolve the Brand Module's main service from the container, and use its client service to retrieve the brands from the third-party system. - -The step returns the retrieved brands. +You create a `retrieveBrandsFromSystemStep` that resolves the CMS Module's service and uses its `retrieveBrands` method to retrieve the brands in the CMS. You return those brands in the step's response. -### Create Brands Step +### createBrandsStep -Next, create the step that creates new brands in Medusa in the file `src/workflows/sync-brands-from-system/steps/create-brands.ts`: +The brands retrieved in the first step may have brands that don't exist in Medusa. So, you'll create a step that creates those brands. Add the step to the same `src/workflows/sync-brands-from-cms.ts` file: export const createBrandsHighlights = [ - ["21", "createBrands", "Create the brands in Medusa"], - ["30", "deleteBrands", "Delete the brands from Medusa"] + ["22", "createBrands", "Create the brands in Medusa"], + ["35", "deleteBrands", "Delete the brands from Medusa"] ] -```ts title="src/workflows/sync-brands-from-system/steps/create-brands.ts" highlights={createBrandsHighlights} collapsibleLines="1-9" expandButtonLabel="Show Imports" -import { - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" -import { InferTypeOf } from "@medusajs/framework/types" -import BrandModuleService from "../../../modules/brand/service" -import { BRAND_MODULE } from "../../../modules/brand" -import { Brand } from "../../../modules/brand/models/brand" +```ts title="src/workflows/sync-brands-from-cms.ts" highlights={createBrandsHighlights} collapsibleLines="1-8" expandButtonLabel="Show Imports" +// other imports... +import BrandModuleService from "../modules/brand/service" +import { BRAND_MODULE } from "../modules/brand" + +// ... + +type CreateBrand = { + name: string +} type CreateBrandsInput = { - brands: InferTypeOf[] + brands: CreateBrand[] } export const createBrandsStep = createStep( @@ -93,52 +112,52 @@ export const createBrandsStep = createStep( const brands = await brandModuleService.createBrands(input.brands) - return new StepResponse(brands, brands.map((brand) => brand.id)) + return new StepResponse(brands, brands) }, - async (ids: string[], { container }) => { + async (brands, { container }) => { + if (!brands) { + return + } + const brandModuleService: BrandModuleService = container.resolve( BRAND_MODULE ) - await brandModuleService.deleteBrands(ids) + await brandModuleService.deleteBrands(brands.map((brand) => brand.id)) } ) ``` -This step receives the brands to create as input. - - +The `createBrandsStep` accepts the brands to create as an input. It resolves the [Brand Module](../../custom-features/module/page.mdx)'s service and uses the generated `createBrands` method to create the brands. -Since a data model is a variable, use the `InferTypeOf` utility imported from `@medusajs/framework/types` to infer its type. +The step passes the created brands to the compensation function, which deletes those brands if an error occurs during the workflow's execution. - + -In the step, you resolve the Brand Module's main service and uses its `createBrands` method to create the brands. +Learn more about compensation functions in [this chapter](../../../advanced-development/workflows/compensation-function/page.mdx). -You return the created brands and pass their IDs to the compensation function, which deletes the brands if an error occurs. + ### Update Brands Step -To create the step that updates existing brands in Medusa, create the file `src/workflows/sync-brands-from-system/steps/update-brands.ts` with the following content: +The brands retrieved in the first step may also have brands that exist in Medusa. So, you'll create a step that updates their details to match that of the CMS. Add the step to the same `src/workflows/sync-brands-from-cms.ts` file: export const updateBrandsHighlights = [ - ["21", "prevUpdatedBrands", "Retrieve the data of the brands before the update."], - ["25", "updateBrands", "Update the brands in Medusa."], - ["34", "updateBrands", "Revert the update by reverting the brands' to before the update."] + ["19", "prevUpdatedBrands", "Retrieve the data of the brands before the update."], + ["23", "updateBrands", "Update the brands in Medusa."], + ["36", "updateBrands", "Revert the update by reverting the brands' details to before the update."] ] -```ts title="src/workflows/sync-brands-from-system/steps/update-brands.ts" highlights={updateBrandsHighlights} collapsibleLines="1-9" expandButtonLabel="Show Imports" -import { - createStep, - StepResponse, -} from "@medusajs/framework/workflows-sdk" -import { InferTypeOf } from "@medusajs/framework/types" -import BrandModuleService from "../../../modules/brand/service" -import { BRAND_MODULE } from "../../../modules/brand" -import { Brand } from "../../../modules/brand/models/brand" +```ts title="src/workflows/sync-brands-from-cms.ts" highlights={updateBrandsHighlights} +// ... + +type UpdateBrand = { + id: string + name: string +} type UpdateBrandsInput = { - brands: InferTypeOf[] + brands: UpdateBrand[] } export const updateBrandsStep = createStep( @@ -157,6 +176,10 @@ export const updateBrandsStep = createStep( return new StepResponse(updatedBrands, prevUpdatedBrands) }, async (prevUpdatedBrands, { container }) => { + if (!prevUpdatedBrands) { + return + } + const brandModuleService: BrandModuleService = container.resolve( BRAND_MODULE ) @@ -166,27 +189,24 @@ export const updateBrandsStep = createStep( ) ``` -This step receives the brands to update as input. - -In the step, you retrieve the brands first to pass them later to the compensation function, then update and return the brands. +The `updateBrandsStep` receives the brands to update in Medusa. In the step, you retrieve the brand's details in Medusa before the update to pass them to the compensation function. You then update the brands using the Brand Module's `updateBrands` generated method. -In the compensation function, you update the brands are again but to their data before the update made by the step. +In the compensation function, which receives the brand's old data, you revert the update using the same `updateBrands` method. ### Create Workflow -Finally, create the workflow in the file `src/workflows/sync-brands-from-system/index.ts` with the following content: +Finally, you'll create the workflow that uses the above steps to sync the brands from the CMS to Medusa. Add to the same `src/workflows/sync-brands-from-cms.ts` file the following: -```ts title="src/workflows/sync-brands-from-system/index.ts" +```ts title="src/workflows/sync-brands-from-cms.ts" +// other imports... import { + // ... createWorkflow, - WorkflowResponse, transform, + WorkflowResponse, } from "@medusajs/framework/workflows-sdk" -import { InferTypeOf } from "@medusajs/framework/types" -import { retrieveBrandsFromSystemStep } from "./steps/retrieve-brands-from-system" -import { createBrandsStep } from "./steps/create-brands" -import { updateBrandsStep } from "./steps/update-brands" -import { Brand } from "../../modules/brand/models/brand" + +// ... export const syncBrandsFromSystemWorkflow = createWorkflow( "sync-brands-from-system", @@ -198,35 +218,37 @@ export const syncBrandsFromSystemWorkflow = createWorkflow( ) ``` -For now, you only add the `retrieveBrandsFromSystemStep` to the workflow that retrieves the brands from the third-party system. +In the workflow, you only use the `retrieveBrandsFromSystemStep` for now, which retrieves the brands from the third-party CMS. -### Identify Brands to Create or Update in Workflow +Next, you need to identify which brands must be created or updated. Since workflows are constructed internally and are only evaluated during execution, you can't access values to perform data manipulation directly. Instead, use [transform](../../../advanced-development/workflows/variable-manipulation/page.mdx) from the Workflows SDK that gives you access to the real-time values of the data, allowing you to create new variables using those values. -Next, you need to identify which brands must be created or updated. + -Since workflows are constructed internally and are only evaluated during execution, you can't access any data's value to perform data manipulation or checks. +Learn more about data manipulation using `transform` in [this chapter](../../../advanced-development/workflows/variable-manipulation/page.mdx). -Instead, use the `transform` utility function imported from `@medusajs/framework/workflows-sdk`, which gives you access to the real-time values of the data to perform actions on them. + So, replace the `TODO` with the following: -```ts title="src/workflows/sync-brands-from-system/index.ts" +```ts title="src/workflows/sync-brands-from-cms.ts" const { toCreate, toUpdate } = transform( { brands, }, (data) => { - const toCreate: InferTypeOf[] = [] - const toUpdate: InferTypeOf[] = [] + const toCreate: CreateBrand[] = [] + const toUpdate: UpdateBrand[] = [] data.brands.forEach((brand) => { if (brand.external_id) { toUpdate.push({ - ...brand, - id: brand.external_id, + id: brand.external_id as string, + name: brand.name as string, }) } else { - toCreate.push(brand) + toCreate.push({ + name: brand.name as string, + }) } }) @@ -242,19 +264,11 @@ const { toCreate, toUpdate } = transform( 1. The data to be passed to the function in the second parameter. 2. A function to execute only when the workflow is executed. Its return value can be consumed by the rest of the workflow. -In the function, you sort the brands as to be created or to be updated based on whether they have an `external_id` property. - - +In `transform`'s function, you loop over the brands array to check which should be created or updated. This logic assumes that a brand in the CMS has an `external_id` property whose value is the brand's ID in Medusa. -This approach assumes that the third-party system stores the ID of the brand in Medusa in `external_id`. - - +You now have the list of brands to create and update. So, replace the new `TODO` with the following: -### Create and Update the Brands - -Finally, replace the new `TODO` with the following: - -```ts title="src/workflows/sync-brands-from-system/index.ts" +```ts title="src/workflows/sync-brands-from-cms.ts" const created = createBrandsStep({ brands: toCreate }) const updated = updateBrandsStep({ brands: toUpdate }) @@ -264,24 +278,28 @@ return new WorkflowResponse({ }) ``` -You pass the brands to be created to the `createBrandsStep`, and the brands to be updated to the `updateBrandsStep`. +You first run the `createBrandsStep` to create the brands that don't exist in Medusa, then the `updateBrandsStep` to update the brands that exist in Medusa. You pass the arrays returned by `transform` as the inputs for the steps. -Then, you return the created and updated brands. +Finally, you return an object of the created and updated brands. You'll execute this workflow in the scheduled job next. --- ## 2. Schedule Syncing Task -To schedule a task that syncs brands from the third-party system, create a scheduled job at `src/jobs/sync-brands-from-system.ts`: +You now have the workflow to sync the brands from the CMS to Medusa. Next, you'll create a scheduled job that runs this workflow once a day to ensure the data between Medusa and the CMS are always in sync. + +A scheduled job is created in a TypeScript or JavaScript file under the `src/jobs` directory. So, create the file `src/jobs/sync-brands-from-cms.ts` with the following content: + +![Directory structure of the Medusa application after adding the scheduled job](https://res.cloudinary.com/dza7lstvk/image/upload/v1733494592/Medusa%20Book/cms-dir-overview-7_dkjb9s.jpg) ```ts title="src/jobs/sync-brands-from-system.ts" import { MedusaContainer } from "@medusajs/framework/types" -import { syncBrandsFromSystemWorkflow } from "../workflows/sync-brands-from-system" +import { syncBrandsFromCmsWorkflow } from "../workflows/sync-brands-from-cms" export default async function (container: MedusaContainer) { const logger = container.resolve("logger") - const { result } = await syncBrandsFromSystemWorkflow(container).run() + const { result } = await syncBrandsFromCmsWorkflow(container).run() logger.info( `Synced brands from third-party system: ${ @@ -291,31 +309,37 @@ export default async function (container: MedusaContainer) { export const config = { name: "sync-brands-from-system", - schedule: "* * * * *", + schedule: "0 0 * * *", // change to * * * * * for debugging } ``` -This defines a scheduled job that runs every minute (for testing purposes). - - +A scheduled job file must export: -Learn more about scheduled jobs [in this guide](../../../basics/scheduled-jobs/page.mdx). +- An asynchronous function that will be executed at the specified schedule. This function must be the file's default export. +- An object of scheduled jobs configuration. It has two properties: + - `name`: A unique name for the scheduled job. + - `schedule`: A string that holds a [cron expression](https://crontab.guru/) indicating the schedule to run the job. - +The scheduled job function accepts as a parameter the [Medusa container](../../../basics/medusa-container/page.mdx) used to resolve framework and commerce tools. You then execute the `syncBrandsFromCmsWorkflow` and use its result to log how many brands were created or updated. -The scheduled job executes the `syncBrandsFromSystemWorkflow` and prints how many brands were created and updated. +Based on the cron expression specified in `config.schedule`, Medusa will run the scheduled job every day at midnight. You can also change it to `* * * * *` to run it every minute for easier debugging. --- ## Test it Out -To test it out, start the Medusa application. In a minute, the scheduled job will run and you'll see a logged message indicating how many brands were created or updated. +To test out the scheduled job, start the Medusa application: + +```bash npm2yarn +npm run dev +``` + +If you set the schedule to `* * * * *` for debugging, the scheduled job will run in a minute. You'll see in the logs how many brands were created or updated. --- ## Summary -In the previous chapters, you: +By following the previous chapters, you utilized Medusa's framework and orchestration tools to perform and automate tasks that span across systems. -- Created a service that acts as a client integrating a third-party system. -- Implemented two-way sync of brands between the third-party system and Medusa using a subscriber and a scheduled job. +With Medusa, you can integrate any service from your commerce ecosystem with ease. You don't have to set up separate applications to manage your different customizations, or worry about data inconsistency across systems. Your efforts only go into implementing the business logic that ties your systems together. diff --git a/www/apps/book/app/learn/customization/integrate-systems/service/page.mdx b/www/apps/book/app/learn/customization/integrate-systems/service/page.mdx index 82b96e44a10d7..daf0fb1121bab 100644 --- a/www/apps/book/app/learn/customization/integrate-systems/service/page.mdx +++ b/www/apps/book/app/learn/customization/integrate-systems/service/page.mdx @@ -1,41 +1,46 @@ import { Prerequisites } from "docs-ui" export const metadata = { - title: `${pageNumber} Integrate Third-Party Brand System in a Service`, + title: `${pageNumber} Guide: Integrate CMS Brand System`, } # {metadata.title} - +In the previous chapters, you've created a [Brand Module](../../custom-features/module/page.mdx) that adds brands to your application. In this chapter, you'll integrate a dummy Content-Management System (CMS) in a new module. The module's service will provide methods to retrieve and manage brands in the CMS. You'll later use this service to sync data from and to the CMS. -This chapter covers how to integrate a dummy third-party system in a service as a step of the ["Integrate Systems" chapter](../page.mdx). + + +Learn more about modules in [this chapter](../../../basics/modules/page.mdx). -## 1. Create Service +## 1. Create Module Directory - +You'll integrate the third-party system in a new CMS Module. So, create the directory `src/modules/cms` that will hold the module's resources. + +![Directory structure after adding the directory for the CMS Module](https://res.cloudinary.com/dza7lstvk/image/upload/v1733492447/Medusa%20Book/cms-dir-overview-1_gasguk.jpg) + +--- -Start by creating the file `src/modules/brand/services/client.ts` with the following content: +## 2. Create Module Service + +Next, you'll create the module's service. It will provide methods to connect and perform actions with the third-party system. + +Create the CMS Module's service at `src/modules/cms/service.ts` with the following content: + +![Directory structure after adding the CMS Module's service](https://res.cloudinary.com/dza7lstvk/image/upload/v1733492583/Medusa%20Book/cms-dir-overview-2_zwcwh3.jpg) export const serviceHighlights = [ - ["4", "BrandClientOptions", "Define the options that the Brand Module receives necessary for the integration."], - ["8", "InjectedDependencies", "Define the dependencies injected into the service."], - ["20", "moduleDef", "Retrieve the module's configuration."] + ["3", "ModuleOptions", "The options that the CMS Module receives."], + ["7", "InjectedDependencies", "The dependencies injected into the service from the module's container."], + ["16", "logger", "Dependencies injected from the module's container"], + ["16", "options", "Options passed to the module in the configurations."] ] -```ts title="src/modules/brand/services/client.ts" highlights={serviceHighlights} +```ts title="src/modules/cms/service.ts" highlights={serviceHighlights} import { Logger, ConfigModule } from "@medusajs/framework/types" -import { BRAND_MODULE } from ".." -export type BrandClientOptions = { +export type ModuleOptions = { apiKey: string } @@ -44,68 +49,54 @@ type InjectedDependencies = { configModule: ConfigModule } -export class BrandClient { - private options_: BrandClientOptions +class CmsModuleService { + private options_: ModuleOptions private logger_: Logger - constructor({ logger, configModule }: InjectedDependencies) { + constructor({ logger }: InjectedDependencies, options: ModuleOptions) { this.logger_ = logger + this.options_ = options - const moduleDef = configModule.modules[BRAND_MODULE] - if (typeof moduleDef !== "boolean") { - this.options_ = moduleDef.options as BrandClientOptions - } + // TODO initialize SDK } } -``` -This creates a `BrandClient` service. Using dependency injection, you resolve the `logger` and `configModule` from the Module's container. - -`logger` is useful to log messages, and `configModule` has configurations exported in `medusa-config.ts`. - -You also define an `options_` property in your service to store the module's options. - -The `configModule`'s `modules` property is an object whose keys are registered module names and values are the module's configuration. +export default CmsModuleService +``` -If the module's configuration isn't a boolean, it has an `options` property that holds the module's options. You use it to set the `options_` property's value. +You create a `CmsModuleService` that will hold the methods to connect to the third-party CMS. A service's constructor accepts two parameters: - +1. The module's container. Since a module is [isolated](../../../advanced-development/modules/isolation/page.mdx), it has a [local container](../../../advanced-development/modules/container/page.mdx) different than the Medusa container you use in other customizations. This container holds framework tools like the [Logger utility](../../../debugging-and-testing/logging/page.mdx) and resources within the module. +2. Options passed to the module when it's later added in Medusa's configurations. These options are useful to pass secret keys or configurations that ensure your module is re-usable across applications. For the CMS Module, you accept the API key to connect to the dummy CMS as an option. -If the service integrating the third-party system was a main service, it receives the module's options as a second parameter. - - +When integrating a third-party system that has a Node.js SDK or client, you can initialize that client in the constructor to be used in the service's methods. ### Integration Methods -Next, add the following methods to simulate sending requests to the third-party system: +Next, you'll add methods that simulate sending requests to a third-party CMS. You'll use these methods later to sync brands from and to the CMS. + +Add the following methods in the `CmsModuleService`: export const methodsHighlights = [ - ["10", "sendRequest", "Since the third-party system isn't real, this method only logs a message."], - ["19", "createBrand", "A method that creates a brand in the third-party system."], - ["23", "deleteBrand", "A method that deletes a brand in the third-party system."], - ["27", "retrieveBrands", "A method that retrieves a brand from a third-party system."] + ["6", "sendRequest", "Since the third-party system isn't real, this method only logs a message."], + ["12", "createBrand", "A method that creates a brand in the third-party system."], + ["16", "deleteBrand", "A method that deletes a brand in the third-party system."], + ["20", "retrieveBrands", "A method that retrieves a brand from a third-party system."] ] ```ts title="src/modules/brand/services/client.ts" highlights={methodsHighlights} -// other imports... -import { InferTypeOf } from "@medusajs/framework/types" -import { Brand } from "../models/brand" - export class BrandClient { // ... // a dummy method to simulate sending a request, // in a realistic scenario, you'd use an SDK, fetch, or axios clients private async sendRequest(url: string, method: string, data?: any) { - this.logger_.info(`Sending a ${ - method - } request to ${url}. data: ${JSON.stringify(data, null, 2)}`) - this.logger_.info(`Client Options: ${ - JSON.stringify(this.options_, null, 2) - }`) + this.logger_.info(`Sending a ${method} request to ${url}.`) + this.logger_.info(`Request Data: ${JSON.stringify(data, null, 2)}`) + this.logger_.info(`API Key: ${JSON.stringify(this.options_.apiKey, null, 2)}`) } - async createBrand(brand: InferTypeOf) { + async createBrand(brand: Record) { await this.sendRequest("/brands", "POST", brand) } @@ -113,7 +104,7 @@ export class BrandClient { await this.sendRequest(`/brands/${id}`, "DELETE") } - async retrieveBrands() { + async retrieveBrands(): Promise[]> { await this.sendRequest("/brands", "GET") return [] @@ -121,85 +112,70 @@ export class BrandClient { } ``` -The `sendRequest` method is a dummy method to simulate sending a request to a third-party system. +The `sendRequest` method sends requests to the third-party CMS. Since this guide isn't using a real CMS, it only simulates the sending by logging messages in the terminal. You also add three methods that use the `sendRequest` method: -- `createBrand` that creates a brand in the third-party system. To reference a brand's type, you use the `InferTypeOf` utility imported from `@medusajs/framework/types`. This transforms a data model, which is a variable, to its equivalent type. +- `createBrand` that creates a brand in the third-party system. - `deleteBrand` that deletes the brand in the third-party system. - `retrieveBrands` to retrieve a brand from the third-party system. --- -## 2. Export Service +## 3. Export Module Definition -If the service integrating the third-party system is the module's main service, you only need to export it in the module definition. +After creating the module's service, you'll export the module definition indicating the module's name and service. -However, since this service is an internal service in the Brand Module, you must export it in a `src/modules/brand/services/index.ts` file: +Create the file `src/modules/cms/index.ts` with the following content: -```ts title="src/modules/brand/services/index.ts" -export * from "./client" -``` +![Directory structure of the Medusa application after adding the module definition file](https://res.cloudinary.com/dza7lstvk/image/upload/v1733492991/Medusa%20Book/cms-dir-overview-3_b0byks.jpg) -This registers the service in the module's container, allowing you to access it in the module's main service. +```ts title="src/modules/cms/index.ts" +import { Module } from "@medusajs/framework/utils" +import CmsModuleService from "./service" ---- - -## 3. Add Internal Service in Main Service - -In the main service at `src/modules/brand/service.ts`, add the following imports and types at the top of the file: - -```ts title="src/modules/brand/service.ts" -// other imports... -import { BrandClient, BrandClientOptions } from "./services" - -type InjectedDependencies = { - brandClient: BrandClient -} -``` - -Then, add the following in the `BrandModuleService` class: +export const CMS_MODULE = "cms" -```ts title="src/modules/brand/service.ts" -class BrandModuleService extends MedusaService({ - Brand, -}) { - public client: BrandClient - - constructor({ brandClient }: InjectedDependencies) { - super(...arguments) - - this.client = brandClient - } -} +export default Module(CMS_MODULE, { + service: CmsModuleService, +}) ``` -In the main module service, you first resolve through dependency injection the `brandClient` from the container and set it in a public property `client`. +You use `Module` from the Modules SDK to export the module's defintion, indicating that the module's name is `cms` and its service is `CmsModuleService`. --- -## 4. Pass Options to the Module +## 4. Add Module to Medusa's Configurations -To pass options in the module, change its configurations in `medusa-config.ts`: +Finally, add the module to the Medusa configurations at `medusa-config.ts`: ```ts title="medusa-config.ts" module.exports = defineConfig({ // ... modules: [ + // ... { - resolve: "./src/modules/brand", + resolve: "./src/modules/cms", options: { - apiKey: process.env.BRAND_API_KEY || "temp", + apiKey: process.env.CMS_API_KEY, }, }, ], }) ``` -A module's configuration accepts an `options` property, which can hold any options to pass to the module. +The object passed in `modules` accept an `options` property, whose value is an object of options to pass to the module. These are the options you receive in the `CmsModuleService`'s constructor. + +You can add the `CMS_API_KEY` environment variable to `.env`: + +```bash +CMS_API_KEY=123 +``` --- -## Next Steps: Sync Brand From Medusa to Third-Party System +## Next Steps: Sync Brand From Medusa to CMS + +You can now use the CMS Module's service to perform actions on the third-party CMS. -In the next chapter, you'll learn how to sync brands created in Medusa to the third-party system using a workflow and a scheduled job. +In the next chapter, you'll learn how to emit an event when a brand is created, then handle that event to sync the brand from Medusa to the third-party service. diff --git a/www/apps/book/app/learn/customization/next-steps/page.mdx b/www/apps/book/app/learn/customization/next-steps/page.mdx index 4dc5e068540cc..d5d355adb91ca 100644 --- a/www/apps/book/app/learn/customization/next-steps/page.mdx +++ b/www/apps/book/app/learn/customization/next-steps/page.mdx @@ -1,32 +1,20 @@ export const metadata = { - title: `${pageNumber} Customizations Next Steps`, + title: `${pageNumber} Customizations Next Steps: Learn the Fundamentals`, } # {metadata.title} -The previous examples under the Customization chapter explained more about customizing Medusa for a realistic use case. +The previous guides introduced Medusa's different concepts and how you can use them to customize Medusa for a realistic use case, You added brands to your application, linked them to products, customized the admin dashboard, and integrated a third-party CMS. -Your learning journey doesn't end here, and this only presents some of Medusa's powerful features. - -This chapter guides you into how to continue your learning journey, and what resources will be helpful for you during your development. - -## Follow the Rest of this Documentation - -The next chapters of the documentation provide more in-depth uses of the different concepts you learned about. - -While you can start playing around with Medusa and customize it, it's highly recommended to continue the rest of this documentation to learn about what more you can do with each concept. - ---- +The next chapters will cover each of these concepts in depth, with the different ways you can use them, their options or configurations, and more advanced features that weren't covered in the previous guides. While you can start building with Medusa, it's highly recommended to follow the next chapters for a better understanding of Medusa's fundamentals. ## Helpful Resources Guides -The [Development Resources](!resources!) documentation provides more helpful guides and references for your development journey. - -Some of these guides and references are: +The [Development Resources](!resources!) documentation provides more helpful guides and references for your development journey. Some of these guides and references include: -1. [Service Factory Reference](!resources!/service-factory-reference): Learn about the methods generated by the service factory with examples. -2. [Workflows Reference](!resources!/medusa-workflows-reference): Browse the list of workflows and their hooks. 3. [Commerce Modules](!resources!/commerce-modules): Browse the list of commerce modules in Medusa and their references to learn how to use them. +1. [Service Factory Reference](!resources!/service-factory-reference): Learn about the methods generated by `MedusaService` with examples. +2. [Workflows Reference](!resources!/medusa-workflows-reference): Browse the list of core workflows and their hooks that are useful for your customizations. 4. [Admin Injection Zones](!resources!/admin-widget-injection-zones): Browse the injection zones in the Medusa Admin to learn where you can inject widgets. --- diff --git a/www/apps/book/generated/edit-dates.mjs b/www/apps/book/generated/edit-dates.mjs index ec5da6af6327e..106bd650c190a 100644 --- a/www/apps/book/generated/edit-dates.mjs +++ b/www/apps/book/generated/edit-dates.mjs @@ -91,7 +91,7 @@ export const generatedEditDates = { "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-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/custom-features/workflow/page.mdx": "2024-12-06T14:34:53.354Z", "app/learn/customization/extend-features/extend-create-product/page.mdx": "2024-12-05T09:26:15.796Z", "app/learn/customization/custom-features/page.mdx": "2024-11-28T08:21:55.207Z", "app/learn/customization/customize-admin/page.mdx": "2024-12-06T07:21:02.303Z", @@ -100,11 +100,11 @@ export const generatedEditDates = { "app/learn/customization/extend-features/define-link/page.mdx": "2024-12-04T17:15:16.004Z", "app/learn/customization/extend-features/page.mdx": "2024-09-12T12:38:57.394Z", "app/learn/customization/extend-features/query-linked-records/page.mdx": "2024-12-05T10:36:32.357Z", - "app/learn/customization/integrate-systems/handle-event/page.mdx": "2024-09-30T08:43:53.135Z", - "app/learn/customization/integrate-systems/page.mdx": "2024-09-12T12:33:29.827Z", - "app/learn/customization/integrate-systems/schedule-task/page.mdx": "2024-09-30T08:43:53.135Z", - "app/learn/customization/integrate-systems/service/page.mdx": "2024-10-16T08:49:50.899Z", - "app/learn/customization/next-steps/page.mdx": "2024-09-12T10:50:04.873Z", + "app/learn/customization/integrate-systems/handle-event/page.mdx": "2024-12-06T14:34:53.355Z", + "app/learn/customization/integrate-systems/page.mdx": "2024-12-06T14:34:53.355Z", + "app/learn/customization/integrate-systems/schedule-task/page.mdx": "2024-12-06T14:34:53.355Z", + "app/learn/customization/integrate-systems/service/page.mdx": "2024-12-06T14:34:53.356Z", + "app/learn/customization/next-steps/page.mdx": "2024-12-06T14:34:53.356Z", "app/learn/customization/page.mdx": "2024-09-12T11:16:18.504Z", "app/learn/more-resources/cheatsheet/page.mdx": "2024-07-11T16:11:26.480Z", "app/learn/architecture/architectural-modules/page.mdx": "2024-09-23T12:51:04.520Z", diff --git a/www/apps/book/sidebar.mjs b/www/apps/book/sidebar.mjs index 382db13feb7cd..b764aa8f2d628 100644 --- a/www/apps/book/sidebar.mjs +++ b/www/apps/book/sidebar.mjs @@ -159,17 +159,17 @@ export const sidebar = numberSidebarItems( children: [ { type: "link", - title: "Integrate with Service", + title: "CMS Module", path: "/learn/customization/integrate-systems/service", }, { type: "link", - title: "Handle Event", + title: "Sync to CMS", path: "/learn/customization/integrate-systems/handle-event", }, { type: "link", - title: "Schedule Task", + title: "Schedule Syncing", path: "/learn/customization/integrate-systems/schedule-task", }, ], From e7e36f39fbb0eb58c364facf456721e753b96940 Mon Sep 17 00:00:00 2001 From: Shahed Nasser Date: Fri, 6 Dec 2024 19:54:46 +0200 Subject: [PATCH 03/11] docs: support detecting broken link cross-projects (#10483) * docs: support detecting broken link cross-projects * remove double separators --- www/apps/api-reference/markdown/admin.mdx | 2 +- .../markdown/client-libraries.mdx | 4 +- www/apps/api-reference/markdown/store.mdx | 2 +- www/apps/api-reference/next.config.mjs | 47 +++ www/apps/api-reference/package.json | 1 + .../book/app/learn/basics/workflows/page.mdx | 2 +- .../query-linked-records/page.mdx | 2 +- www/apps/book/next.config.mjs | 17 +- .../components/forms/page.mdx | 2 +- .../payment/payment-flow/page.mdx | 2 +- .../product/guides/price-with-taxes/page.mdx | 2 +- .../promotion/extend/page.mdx | 14 +- www/apps/resources/app/recipes/b2b/page.mdx | 2 +- .../subscriptions/examples/standard/page.mdx | 2 +- www/apps/resources/generated/sidebar.mjs | 3 - www/apps/resources/next.config.mjs | 16 +- www/apps/resources/sidebar.mjs | 3 - www/apps/resources/utils/get-slugs.mjs | 4 +- .../src/broken-link-checker.ts | 284 ++++++++++++++++-- .../remark-rehype-plugins/src/constants.ts | 1 + .../src/cross-project-links.ts | 123 +++----- .../remark-rehype-plugins/src/types/index.ts | 10 + .../src/utils/component-link-fixer.ts | 57 +--- .../src/utils/cross-project-link-utils.ts | 21 ++ .../src/utils/perform-action-on-literal.ts | 28 ++ www/packages/types/src/build-scripts.ts | 5 + www/packages/types/src/index.ts | 1 + www/yarn.lock | 1 + 28 files changed, 492 insertions(+), 166 deletions(-) create mode 100644 www/packages/remark-rehype-plugins/src/constants.ts create mode 100644 www/packages/remark-rehype-plugins/src/utils/cross-project-link-utils.ts create mode 100644 www/packages/remark-rehype-plugins/src/utils/perform-action-on-literal.ts create mode 100644 www/packages/types/src/build-scripts.ts diff --git a/www/apps/api-reference/markdown/admin.mdx b/www/apps/api-reference/markdown/admin.mdx index 64a1f360fa785..e2eee7359eadd 100644 --- a/www/apps/api-reference/markdown/admin.mdx +++ b/www/apps/api-reference/markdown/admin.mdx @@ -832,7 +832,7 @@ If you click on the workflow, you'll view a reference of that workflow, includin This is useful if you want to extend an API route and pass additional data or perform custom actions. -Refer to [this guide](https://docs.medusajs.com/learn/customization/extend-features/extend-create-product) to find an example of extending an API route. +Refer to [this guide](!docs!/learn/customization/extend-features/extend-create-product) to find an example of extending an API route. Just Getting Started? -Check out the [Medusa v2 Documentation](https://docs.medusajs.com). +Check out the [Medusa v2 Documentation](!docs!). @@ -16,7 +16,7 @@ To use Medusa's JS SDK library, install the following packages in your project ( npm install @medusajs/js-sdk@latest @medusajs/types@latest ``` -Learn more about the JS SDK in [this documentation](https://docs.medusajs.com/resources/js-sdk). +Learn more about the JS SDK in [this documentation](!resources!/js-sdk). ### Download Full Reference diff --git a/www/apps/api-reference/markdown/store.mdx b/www/apps/api-reference/markdown/store.mdx index 04776d0207574..a68dc51cc718f 100644 --- a/www/apps/api-reference/markdown/store.mdx +++ b/www/apps/api-reference/markdown/store.mdx @@ -831,7 +831,7 @@ If you click on the workflow, you'll view a reference of that workflow, includin This is useful if you want to extend an API route and pass additional data or perform custom actions. -Refer to [this guide](https://docs.medusajs.com/learn/customization/extend-features/extend-create-product) to find an example of extending an API route. +Refer to [this guide](!docs!/learn/customization/extend-features/extend-create-product) to find an example of extending an API route. -Find a full list of the registered resources in the Medusa container and their registration key in [this reference](!resources!/resources/medusa-container-resources). You can use these resources in your custom workflows. +Find a full list of the registered resources in the Medusa container and their registration key in [this reference](!resources!/medusa-container-resources). You can use these resources in your custom workflows. \ No newline at end of file diff --git a/www/apps/book/app/learn/customization/extend-features/query-linked-records/page.mdx b/www/apps/book/app/learn/customization/extend-features/query-linked-records/page.mdx index 6bdeb92f1a36c..2449a7e5a54f1 100644 --- a/www/apps/book/app/learn/customization/extend-features/query-linked-records/page.mdx +++ b/www/apps/book/app/learn/customization/extend-features/query-linked-records/page.mdx @@ -6,7 +6,7 @@ export const metadata = { # {metadata.title} -In the previous chapters, you [defined a link](../define-link/page.mdx) between the [custom Brand Module](../../custom-features/module/page.mdx) and Medusa's [Product Module](!resources!/comerce-modules/product), then [extended the create-product flow](../extend-create-product/page.mdx) to link a product to a brand. +In the previous chapters, you [defined a link](../define-link/page.mdx) between the [custom Brand Module](../../custom-features/module/page.mdx) and Medusa's [Product Module](!resources!/commerce-modules/product), then [extended the create-product flow](../extend-create-product/page.mdx) to link a product to a brand. In this chapter, you'll learn how to retrieve a product's brand (and vice-versa) in two ways: Using Medusa's existing API route, or in customizations, such as a custom API route. diff --git a/www/apps/book/next.config.mjs b/www/apps/book/next.config.mjs index 390cead21ebd4..aa49639579e80 100644 --- a/www/apps/book/next.config.mjs +++ b/www/apps/book/next.config.mjs @@ -9,11 +9,27 @@ import { crossProjectLinksPlugin, } from "remark-rehype-plugins" import { sidebar } from "./sidebar.mjs" +import path from "path" const withMDX = mdx({ extension: /\.mdx?$/, options: { rehypePlugins: [ + [ + brokenLinkCheckerPlugin, + { + crossProjects: { + resources: { + projectPath: path.resolve("..", "resources"), + hasGeneratedSlugs: true, + }, + ui: { + projectPath: path.resolve("..", "ui"), + contentPath: "src/content/docs", + }, + }, + }, + ], [ crossProjectLinksPlugin, { @@ -37,7 +53,6 @@ const withMDX = mdx({ process.env.VERCEL_ENV === "production", }, ], - [brokenLinkCheckerPlugin], [localLinksRehypePlugin], [ rehypeMdxCodeProps, diff --git a/www/apps/resources/app/admin-components/components/forms/page.mdx b/www/apps/resources/app/admin-components/components/forms/page.mdx index 5051232fe2f5f..64a24647a5b89 100644 --- a/www/apps/resources/app/admin-components/components/forms/page.mdx +++ b/www/apps/resources/app/admin-components/components/forms/page.mdx @@ -11,7 +11,7 @@ export const metadata = { The Medusa Admin has two types of forms: 1. Create forms, created using the [FocusModal UI component](!ui!/components/focus-modal). -2. Edit or update forms, created using the [Drawer UI component](!ui!/ui/components/drawer). +2. Edit or update forms, created using the [Drawer UI component](!ui!/components/drawer). This guide explains how to create these two form types following the Medusa Admin's conventions. diff --git a/www/apps/resources/app/commerce-modules/payment/payment-flow/page.mdx b/www/apps/resources/app/commerce-modules/payment/payment-flow/page.mdx index 93f5cc3807a77..10742536de320 100644 --- a/www/apps/resources/app/commerce-modules/payment/payment-flow/page.mdx +++ b/www/apps/resources/app/commerce-modules/payment/payment-flow/page.mdx @@ -66,7 +66,7 @@ remoteLink.create({ -Learn more about the remote link in [this documentation](!docs!/advanced-development/module-links/remote-link). +Learn more about the remote link in [this documentation](!docs!/learn/advanced-development/module-links/remote-link). diff --git a/www/apps/resources/app/commerce-modules/product/guides/price-with-taxes/page.mdx b/www/apps/resources/app/commerce-modules/product/guides/price-with-taxes/page.mdx index d4903742cc27c..60c2d9701613d 100644 --- a/www/apps/resources/app/commerce-modules/product/guides/price-with-taxes/page.mdx +++ b/www/apps/resources/app/commerce-modules/product/guides/price-with-taxes/page.mdx @@ -14,7 +14,7 @@ In this document, you'll learn how to calculate a product variant's price with t You'll need the following resources for the taxes calculation: -1. [Query](!docs!/advanced-development/module-links/query) to retrieve the product's variants' prices for a context. Learn more about that in [this guide](../price/page.mdx). +1. [Query](!docs!/learn/advanced-development/module-links/query) to retrieve the product's variants' prices for a context. Learn more about that in [this guide](../price/page.mdx). 2. The Tax Module's main service to get the tax lines for each product. ```ts diff --git a/www/apps/resources/app/commerce-modules/promotion/extend/page.mdx b/www/apps/resources/app/commerce-modules/promotion/extend/page.mdx index 56b7489e75c25..cb7e5f4b00324 100644 --- a/www/apps/resources/app/commerce-modules/promotion/extend/page.mdx +++ b/www/apps/resources/app/commerce-modules/promotion/extend/page.mdx @@ -121,7 +121,7 @@ To do that, you'll consume the [promotionsCreated](/references/medusa-workflows/ -Learn more about workflow hooks in [this guide](!docs!/advanced-development/workflows/workflow-hooks). +Learn more about workflow hooks in [this guide](!docs!/learn/advanced-development/workflows/workflow-hooks). @@ -156,7 +156,7 @@ In the snippet above, you add a validation rule indicating that `custom_name` is -Learn more about additional data validation in [this guide](!docs!/advanced-development/api-routes/additional-data). +Learn more about additional data validation in [this guide](!docs!/learn/advanced-development/api-routes/additional-data). @@ -208,7 +208,7 @@ In the compensation function that undoes the step's actions in case of an error, -Learn more about compensation functions in [this guide](!docs!/advanced-development/workflows/compensation-function). +Learn more about compensation functions in [this guide](!docs!/learn/advanced-development/workflows/compensation-function). @@ -266,9 +266,9 @@ The workflow accepts as an input the created promotion and the `additional_data` In the workflow, you: -1. Use the `transform` utility to get the value of `custom_name` based on whether it's set in `additional_data`. Learn more about why you can't use conditional operators in a workflow without using `transform` in [this guide](!docs!/advanced-development/workflows/conditions#why-if-conditions-arent-allowed-in-workflows). +1. Use the `transform` utility to get the value of `custom_name` based on whether it's set in `additional_data`. Learn more about why you can't use conditional operators in a workflow without using `transform` in [this guide](!docs!/learn/advanced-development/workflows/conditions#why-if-conditions-arent-allowed-in-workflows). 2. Create the `Custom` record using the `createCustomStep`. -3. Use the `when-then` utility to link the promotion to the `Custom` record if it was created. Learn more about why you can't use if-then conditions in a workflow without using `when-then` in [this guide](!docs!/advanced-development/workflows/conditions#why-if-conditions-arent-allowed-in-workflows). +3. Use the `when-then` utility to link the promotion to the `Custom` record if it was created. Learn more about why you can't use if-then conditions in a workflow without using `when-then` in [this guide](!docs!/learn/advanced-development/workflows/conditions#why-if-conditions-arent-allowed-in-workflows). You'll next execute the workflow in the hook handler. @@ -379,7 +379,7 @@ Among the returned `promotion` object, you'll find a `custom` property which hol ### Retrieve using Query -You can also retrieve the `Custom` record linked to a promotion in your code using [Query](!docs!/advanced-development/module-links/query). +You can also retrieve the `Custom` record linked to a promotion in your code using [Query](!docs!/learn/advanced-development/module-links/query). For example: @@ -393,7 +393,7 @@ const { data: [promotion] } = await query.graph({ }) ``` -Learn more about how to use Query in [this guide](!docs!/advanced-development/module-links/query). +Learn more about how to use Query in [this guide](!docs!/learn/advanced-development/module-links/query). --- diff --git a/www/apps/resources/app/recipes/b2b/page.mdx b/www/apps/resources/app/recipes/b2b/page.mdx index 5a47fa580a03c..5b06d915a0d21 100644 --- a/www/apps/resources/app/recipes/b2b/page.mdx +++ b/www/apps/resources/app/recipes/b2b/page.mdx @@ -736,7 +736,7 @@ The Medusa Admin plugin can be extended to add widgets, new pages, and setting p icon: AcademicCapSolid, }, { - href: "!docs!/learn/advanced-development/admin/setting-pages", + href: "!docs!/learn/advanced-development/admin/ui-routes#create-settings-page", title: "Create Admin Setting Page", text: "Learn how to add new page to the Medusa Admin settings.", icon: AcademicCapSolid, diff --git a/www/apps/resources/app/recipes/subscriptions/examples/standard/page.mdx b/www/apps/resources/app/recipes/subscriptions/examples/standard/page.mdx index 7bd662c235c06..d78d629919cbc 100644 --- a/www/apps/resources/app/recipes/subscriptions/examples/standard/page.mdx +++ b/www/apps/resources/app/recipes/subscriptions/examples/standard/page.mdx @@ -2008,7 +2008,7 @@ This loops over the returned subscriptions and executes the `createSubscriptionO ### Further Reads -- [How to Create a Scheduled Job](!docs!/learn/basics/scheeduled-jobs) +- [How to Create a Scheduled Job](!docs!/learn/basics/scheduled-jobs) --- diff --git a/www/apps/resources/generated/sidebar.mjs b/www/apps/resources/generated/sidebar.mjs index c51bf4af4f279..d8899e0f635f8 100644 --- a/www/apps/resources/generated/sidebar.mjs +++ b/www/apps/resources/generated/sidebar.mjs @@ -9118,9 +9118,6 @@ export const generatedSidebar = [ } ] }, - { - "type": "separator" - }, { "loaded": true, "isPathHref": true, diff --git a/www/apps/resources/next.config.mjs b/www/apps/resources/next.config.mjs index 95a2a6dc49c2f..5065b8f3ff228 100644 --- a/www/apps/resources/next.config.mjs +++ b/www/apps/resources/next.config.mjs @@ -7,13 +7,27 @@ import { workflowDiagramLinkFixerPlugin, } from "remark-rehype-plugins" import mdxPluginOptions from "./mdx-options.mjs" +import path from "node:path" const withMDX = mdx({ extension: /\.mdx?$/, options: { rehypePlugins: [ + [ + brokenLinkCheckerPlugin, + { + crossProjects: { + docs: { + projectPath: path.resolve("..", "book"), + }, + ui: { + projectPath: path.resolve("..", "ui"), + contentPath: "src/content/docs", + }, + }, + }, + ], ...mdxPluginOptions.options.rehypePlugins, - [brokenLinkCheckerPlugin], [localLinksRehypePlugin], [typeListLinkFixerPlugin], [ diff --git a/www/apps/resources/sidebar.mjs b/www/apps/resources/sidebar.mjs index 7befd8c476ab9..7e7ec55154da8 100644 --- a/www/apps/resources/sidebar.mjs +++ b/www/apps/resources/sidebar.mjs @@ -2131,9 +2131,6 @@ export const sidebar = sidebarAttachHrefCommonOptions([ }, ], }, - { - type: "separator", - }, { type: "category", title: "General", diff --git a/www/apps/resources/utils/get-slugs.mjs b/www/apps/resources/utils/get-slugs.mjs index b3a6ef066fba5..b3e247b5bb65d 100644 --- a/www/apps/resources/utils/get-slugs.mjs +++ b/www/apps/resources/utils/get-slugs.mjs @@ -7,7 +7,7 @@ const monoRepoPath = path.resolve("..", "..", "..") /** * * @param {string} dir - The directory to search in - * @returns {Promise<{ origSlug: string; newSlug: string }[]>} + * @returns {Promise} */ export default async function getSlugs(options = {}) { let { dir, basePath = path.resolve("app"), baseSlug = basePath } = options @@ -15,7 +15,7 @@ export default async function getSlugs(options = {}) { dir = basePath } /** - * @type {{ origSlug: string; newSlug: string }[]} + * @type {import("types").SlugChange[]} */ const slugs = [] diff --git a/www/packages/remark-rehype-plugins/src/broken-link-checker.ts b/www/packages/remark-rehype-plugins/src/broken-link-checker.ts index b6c074ae4dc17..4d9eb542d0a4f 100644 --- a/www/packages/remark-rehype-plugins/src/broken-link-checker.ts +++ b/www/packages/remark-rehype-plugins/src/broken-link-checker.ts @@ -1,9 +1,253 @@ -import { existsSync } from "fs" +import { existsSync, readdirSync, readFileSync } from "fs" import path from "path" import type { Transformer } from "unified" -import type { UnistNode, UnistTree } from "./types/index.js" +import type { + BrokenLinkCheckerOptions, + UnistNode, + UnistNodeWithData, + UnistTree, +} from "./types/index.js" +import type { VFile } from "vfile" +import { parseCrossProjectLink } from "./utils/cross-project-link-utils.js" +import { SlugChange } from "types" +import getAttribute from "./utils/get-attribute.js" +import { estreeToJs } from "./utils/estree-to-js.js" +import { performActionOnLiteral } from "./utils/perform-action-on-literal.js" +import { MD_LINK_REGEX } from "./constants.js" -export function brokenLinkCheckerPlugin(): Transformer { +function getErrorMessage({ + link, + file, +}: { + link: string + file: VFile +}): string { + return `Broken link found! ${link} linked in ${file.history[0]}` +} + +function checkLocalLinkExists({ + link, + file, + currentPageFilePath, +}: { + link: string + file: VFile + currentPageFilePath: string +}) { + // get absolute path of the URL + const linkedFilePath = path + .resolve(currentPageFilePath, link) + .replace(/#.*$/, "") + // check if the file exists + if (!existsSync(linkedFilePath)) { + throw new Error( + getErrorMessage({ + link, + file, + }) + ) + } +} + +function mdxPageExists(pagePath: string): boolean { + if (!existsSync(pagePath)) { + // for projects that use a convention other than mdx + // check if an mdx file exists with the same name + if (existsSync(`${pagePath}.mdx`)) { + return true + } + return false + } + + if (existsSync(path.join(pagePath, "page.mdx"))) { + return true + } + + // for projects that use a convention other than mdx + // check if an mdx file exists with the same name + return readdirSync(pagePath).some((fileName) => fileName.endsWith(".mdx")) +} + +function componentChecker({ + node, + ...rest +}: { + node: UnistNodeWithData + file: VFile + currentPageFilePath: string + options: BrokenLinkCheckerOptions +}) { + if (!node.name) { + return + } + + let attributeName: string | undefined + + const maybeCheckAttribute = () => { + if (!attributeName) { + return + } + + const attribute = getAttribute(node, attributeName) + + if ( + !attribute || + typeof attribute.value === "string" || + !attribute.value.data?.estree + ) { + return + } + + const itemJsVar = estreeToJs(attribute.value.data.estree) + + if (!itemJsVar) { + return + } + + performActionOnLiteral(itemJsVar, (item) => { + checkLink({ + link: item.original.value as string, + ...rest, + }) + }) + } + + switch (node.name) { + case "Prerequisites": + case "CardList": + attributeName = "items" + break + case "Card": + attributeName = "href" + break + case "WorkflowDiagram": + attributeName = "workflow" + break + case "TypeList": + attributeName = "types" + break + } + + maybeCheckAttribute() +} + +function checkLink({ + link, + file, + currentPageFilePath, + options, +}: { + link: unknown | undefined + file: VFile + currentPageFilePath: string + options: BrokenLinkCheckerOptions +}) { + if (!link || typeof link !== "string") { + return + } + // try to remove hash + const hashIndex = link.lastIndexOf("#") + const likeWithoutHash = hashIndex !== -1 ? link.substring(0, hashIndex) : link + if (likeWithoutHash.match(/page\.mdx?$/)) { + checkLocalLinkExists({ + link: likeWithoutHash, + file, + currentPageFilePath, + }) + return + } + + const parsedLink = parseCrossProjectLink(likeWithoutHash) + + if (!parsedLink || !Object.hasOwn(options.crossProjects, parsedLink.area)) { + if (MD_LINK_REGEX.test(link)) { + // try fixing MDX links + let linkMatches + let tempLink = link + MD_LINK_REGEX.lastIndex = 0 + + while ((linkMatches = MD_LINK_REGEX.exec(tempLink)) !== null) { + if (!linkMatches.groups?.link) { + return + } + + checkLink({ + link: linkMatches.groups.link, + file, + currentPageFilePath, + options, + }) + + tempLink = tempLink.replace(linkMatches.groups.link, "") + // reset regex + MD_LINK_REGEX.lastIndex = 0 + } + } + return + } + + const projectOptions = options.crossProjects[parsedLink.area] + + const isReferenceLink = parsedLink.path.startsWith("/references") + const baseDir = isReferenceLink + ? "references" + : projectOptions.contentPath || "app" + const pagePath = isReferenceLink + ? parsedLink.path.replace(/^\/references/, "") + : parsedLink.path + // check if the file exists + if (mdxPageExists(path.join(projectOptions.projectPath, baseDir, pagePath))) { + return + } + + // file doesn't exist, check if slugs are enabled and generated + const generatedSlugsPath = path.join( + projectOptions.projectPath, + "generated", + "slug-changes.mjs" + ) + if (!projectOptions.hasGeneratedSlugs || !existsSync(generatedSlugsPath)) { + throw new Error( + getErrorMessage({ + link, + file, + }) + ) + } + + // get slugs from file + const generatedSlugContent = readFileSync(generatedSlugsPath, "utf-8") + const slugChanges: SlugChange[] = JSON.parse( + generatedSlugContent.substring(generatedSlugContent.indexOf("[")) + ) + const slugChange = slugChanges.find( + (change) => change.newSlug === parsedLink.path + ) + + if ( + !slugChange || + !mdxPageExists(path.join(projectOptions.projectPath, slugChange.origSlug)) + ) { + throw new Error( + getErrorMessage({ + link, + file, + }) + ) + } +} + +const allowedComponentNames = [ + "Card", + "CardList", + "Prerequisites", + "WorkflowDiagram", + "TypeList", +] + +export function brokenLinkCheckerPlugin( + options: BrokenLinkCheckerOptions +): Transformer { return async (tree, file) => { const { visit } = await import("unist-util-visit") @@ -12,20 +256,26 @@ export function brokenLinkCheckerPlugin(): Transformer { "" ) - visit(tree as UnistTree, "element", (node: UnistNode) => { - if (node.tagName !== "a" || !node.properties?.href?.match(/page\.mdx?/)) { - return + visit( + tree as UnistTree, + ["element", "mdxJsxFlowElement"], + (node: UnistNode) => { + if (node.tagName === "a" && node.properties?.href) { + checkLink({ + link: node.properties.href, + file, + currentPageFilePath, + options, + }) + } else if (node.name && allowedComponentNames.includes(node.name)) { + componentChecker({ + node: node as UnistNodeWithData, + file, + currentPageFilePath, + options, + }) + } } - // get absolute path of the URL - const linkedFilePath = path - .resolve(currentPageFilePath, node.properties.href) - .replace(/#.*$/, "") - // check if the file exists - if (!existsSync(linkedFilePath)) { - throw new Error( - `Broken link found! ${node.properties.href} linked in ${file.history[0]}` - ) - } - }) + ) } } diff --git a/www/packages/remark-rehype-plugins/src/constants.ts b/www/packages/remark-rehype-plugins/src/constants.ts new file mode 100644 index 0000000000000..934cda4618ed3 --- /dev/null +++ b/www/packages/remark-rehype-plugins/src/constants.ts @@ -0,0 +1 @@ +export const MD_LINK_REGEX = /\[(.*?)\]\((?(![a-z]+!|\.).*?)\)/gm diff --git a/www/packages/remark-rehype-plugins/src/cross-project-links.ts b/www/packages/remark-rehype-plugins/src/cross-project-links.ts index 407dda58fcff0..3fe859358c3a3 100644 --- a/www/packages/remark-rehype-plugins/src/cross-project-links.ts +++ b/www/packages/remark-rehype-plugins/src/cross-project-links.ts @@ -1,18 +1,13 @@ -/* eslint-disable no-case-declarations */ import type { Transformer } from "unified" import type { CrossProjectLinksOptions, - ExpressionJsVar, UnistNode, UnistNodeWithData, UnistTree, } from "./types/index.js" import { estreeToJs } from "./utils/estree-to-js.js" import getAttribute from "./utils/get-attribute.js" -import { - isExpressionJsVarLiteral, - isExpressionJsVarObj, -} from "./utils/expression-is-utils.js" +import { performActionOnLiteral } from "./utils/perform-action-on-literal.js" const PROJECT_REGEX = /^!(?[\w-]+)!/ @@ -61,89 +56,65 @@ function componentFixer( return } - const fixProperty = (item: ExpressionJsVar) => { - if (!isExpressionJsVarObj(item)) { + let attributeName: string | undefined + + const maybeCheckAttribute = () => { + if (!attributeName) { return } - Object.entries(item).forEach(([key, value]) => { - if ( - (key !== "href" && key !== "link") || - !isExpressionJsVarLiteral(value) - ) { - return - } + const attribute = getAttribute(node, attributeName) + + if ( + !attribute || + typeof attribute.value === "string" || + !attribute.value.data?.estree + ) { + return + } + + const itemJsVar = estreeToJs(attribute.value.data.estree) + + if (!itemJsVar) { + return + } - value.original.value = matchAndFixLinks( - value.original.value as string, + performActionOnLiteral(itemJsVar, (item) => { + item.original.value = matchAndFixLinks( + item.original.value as string, options ) - value.original.raw = JSON.stringify(value.original.value) + item.original.raw = JSON.stringify(item.original.value) }) } switch (node.name) { case "CardList": - const itemsAttribute = getAttribute(node, "items") - - if ( - !itemsAttribute?.value || - typeof itemsAttribute.value === "string" || - !itemsAttribute.value.data?.estree - ) { - return - } - - const jsVar = estreeToJs(itemsAttribute.value.data.estree) - - if (!jsVar) { - return - } - - if (Array.isArray(jsVar)) { - jsVar.forEach(fixProperty) - } else { - fixProperty(jsVar) - } - return - case "Card": - const hrefAttribute = getAttribute(node, "href") - - if (!hrefAttribute?.value || typeof hrefAttribute.value !== "string") { - return - } - - hrefAttribute.value = matchAndFixLinks(hrefAttribute.value, options) - - return case "Prerequisites": - const prerequisitesItemsAttribute = getAttribute(node, "items") - - if ( - !prerequisitesItemsAttribute?.value || - typeof prerequisitesItemsAttribute.value === "string" || - !prerequisitesItemsAttribute.value.data?.estree - ) { - return - } - - const prerequisitesJsVar = estreeToJs( - prerequisitesItemsAttribute.value.data.estree - ) - - if (!prerequisitesJsVar) { - return - } - - if (Array.isArray(prerequisitesJsVar)) { - prerequisitesJsVar.forEach(fixProperty) - } else { - fixProperty(prerequisitesJsVar) - } - return + attributeName = "items" + break + case "Card": + attributeName = "href" + break + case "WorkflowDiagram": + attributeName = "workflow" + break + case "TypeList": + attributeName = "types" + break } + + maybeCheckAttribute() } +const allowedComponentNames = [ + "Card", + "CardList", + "Prerequisites", + "WorkflowDiagram", + "TypeList", +] + export function crossProjectLinksPlugin( options: CrossProjectLinksOptions ): Transformer { @@ -155,9 +126,7 @@ export function crossProjectLinksPlugin( ["element", "mdxJsxFlowElement"], (node: UnistNode) => { const isComponent = - node.name === "Card" || - node.name === "CardList" || - node.name === "Prerequisites" + node.name && allowedComponentNames.includes(node.name) const isLink = node.tagName === "a" && node.properties?.href if (!isComponent && !isLink) { return diff --git a/www/packages/remark-rehype-plugins/src/types/index.ts b/www/packages/remark-rehype-plugins/src/types/index.ts index 5cffb878ac8ce..faaad3a18c14a 100644 --- a/www/packages/remark-rehype-plugins/src/types/index.ts +++ b/www/packages/remark-rehype-plugins/src/types/index.ts @@ -118,6 +118,16 @@ export declare type CrossProjectLinksOptions = { useBaseUrl?: boolean } +export declare type BrokenLinkCheckerOptions = { + crossProjects: { + [k: string]: { + projectPath: string + contentPath?: string + hasGeneratedSlugs?: boolean + } + } +} + export declare type ComponentLinkFixerLinkType = "md" | "value" export declare type ComponentLinkFixerOptions = { diff --git a/www/packages/remark-rehype-plugins/src/utils/component-link-fixer.ts b/www/packages/remark-rehype-plugins/src/utils/component-link-fixer.ts index fff48ec155ef7..085190c7f8bcc 100644 --- a/www/packages/remark-rehype-plugins/src/utils/component-link-fixer.ts +++ b/www/packages/remark-rehype-plugins/src/utils/component-link-fixer.ts @@ -1,21 +1,13 @@ import path from "path" import { Transformer } from "unified" -import { - ComponentLinkFixerLinkType, - ExpressionJsVar, - UnistNodeWithData, - UnistTree, -} from "../types/index.js" +import { UnistNodeWithData, UnistTree } from "../types/index.js" import { FixLinkOptions, fixLinkUtil } from "../index.js" import getAttribute from "../utils/get-attribute.js" import { estreeToJs } from "../utils/estree-to-js.js" -import { - isExpressionJsVarLiteral, - isExpressionJsVarObj, -} from "../utils/expression-is-utils.js" import { ComponentLinkFixerOptions } from "../types/index.js" +import { performActionOnLiteral } from "./perform-action-on-literal.js" +import { MD_LINK_REGEX } from "../constants.js" -const MD_LINK_REGEX = /\[(.*?)\]\((?(![a-z]+!|\.).*?)\)/gm const VALUE_LINK_REGEX = /^(![a-z]+!|\.)/gm function matchMdLinks( @@ -59,33 +51,6 @@ function matchValueLink( }) } -function traverseJsVar( - item: ExpressionJsVar[] | ExpressionJsVar, - linkOptions: Omit, - checkLinksType: ComponentLinkFixerLinkType -) { - const linkFn = checkLinksType === "md" ? matchMdLinks : matchValueLink - if (Array.isArray(item)) { - item.forEach((item) => traverseJsVar(item, linkOptions, checkLinksType)) - } else if (isExpressionJsVarLiteral(item)) { - item.original.value = linkFn(item.original.value as string, linkOptions) - item.original.raw = JSON.stringify(item.original.value) - } else { - Object.values(item).forEach((value) => { - if (Array.isArray(value) || isExpressionJsVarObj(value)) { - return traverseJsVar(value, linkOptions, checkLinksType) - } - - if (!isExpressionJsVarLiteral(value)) { - return - } - - value.original.value = linkFn(value.original.value as string, linkOptions) - value.original.raw = JSON.stringify(value.original.value) - }) - } -} - export function componentLinkFixer( componentName: string, attributeName: string, @@ -117,12 +82,12 @@ export function componentLinkFixer( return } - const workflowAttribute = getAttribute(node, attributeName) + const attribute = getAttribute(node, attributeName) if ( - !workflowAttribute || - typeof workflowAttribute.value === "string" || - !workflowAttribute.value.data?.estree + !attribute || + typeof attribute.value === "string" || + !attribute.value.data?.estree ) { return } @@ -132,13 +97,17 @@ export function componentLinkFixer( appsPath, } - const itemJsVar = estreeToJs(workflowAttribute.value.data.estree) + const itemJsVar = estreeToJs(attribute.value.data.estree) if (!itemJsVar) { return } - traverseJsVar(itemJsVar, linkOptions, checkLinksType) + const linkFn = checkLinksType === "md" ? matchMdLinks : matchValueLink + performActionOnLiteral(itemJsVar, (item) => { + item.original.value = linkFn(item.original.value as string, linkOptions) + item.original.raw = JSON.stringify(item.original.value) + }) }) } } diff --git a/www/packages/remark-rehype-plugins/src/utils/cross-project-link-utils.ts b/www/packages/remark-rehype-plugins/src/utils/cross-project-link-utils.ts new file mode 100644 index 0000000000000..b75cdc5de1938 --- /dev/null +++ b/www/packages/remark-rehype-plugins/src/utils/cross-project-link-utils.ts @@ -0,0 +1,21 @@ +const PROJECT_REGEX = /^!(?[\w-]+)!/ + +export const parseCrossProjectLink = ( + link: string +): + | { + area: string + path: string + } + | undefined => { + const projectArea = PROJECT_REGEX.exec(link) + + if (!projectArea?.groups?.area) { + return undefined + } + + return { + area: projectArea.groups.area, + path: link.replace(PROJECT_REGEX, ""), + } +} diff --git a/www/packages/remark-rehype-plugins/src/utils/perform-action-on-literal.ts b/www/packages/remark-rehype-plugins/src/utils/perform-action-on-literal.ts new file mode 100644 index 0000000000000..80a47c5fd55ed --- /dev/null +++ b/www/packages/remark-rehype-plugins/src/utils/perform-action-on-literal.ts @@ -0,0 +1,28 @@ +import { ExpressionJsVar, ExpressionJsVarLiteral } from "../types/index.js" +import { + isExpressionJsVarLiteral, + isExpressionJsVarObj, +} from "./expression-is-utils.js" + +export const performActionOnLiteral = ( + item: ExpressionJsVar[] | ExpressionJsVar, + action: (item: ExpressionJsVarLiteral) => void +) => { + if (Array.isArray(item)) { + item.forEach((i) => performActionOnLiteral(i, action)) + } else if (isExpressionJsVarLiteral(item)) { + action(item) + } else { + Object.values(item).forEach((value) => { + if (Array.isArray(value) || isExpressionJsVarObj(value)) { + return performActionOnLiteral(value, action) + } + + if (!isExpressionJsVarLiteral(value)) { + return + } + + action(value) + }) + } +} diff --git a/www/packages/types/src/build-scripts.ts b/www/packages/types/src/build-scripts.ts new file mode 100644 index 0000000000000..2344ca8e06d85 --- /dev/null +++ b/www/packages/types/src/build-scripts.ts @@ -0,0 +1,5 @@ +export type SlugChange = { + origSlug: string + newSlug: string + filePath: string +} diff --git a/www/packages/types/src/index.ts b/www/packages/types/src/index.ts index 8bc8df0ebfe2c..67a85c06d02fe 100644 --- a/www/packages/types/src/index.ts +++ b/www/packages/types/src/index.ts @@ -1,4 +1,5 @@ export * from "./api-testing.js" +export * from "./build-scripts.js" export * from "./config.js" export * from "./general.js" export * from "./menu.js" diff --git a/www/yarn.lock b/www/yarn.lock index fcc0d66ee1f6b..257e5a40daf97 100644 --- a/www/yarn.lock +++ b/www/yarn.lock @@ -6805,6 +6805,7 @@ __metadata: react-transition-group: ^4.4.5 rehype-mdx-code-props: ^3.0.1 rehype-slug: ^6.0.0 + remark-rehype-plugins: "*" slugify: ^1.6.6 swr: ^2.2.0 tailwind: "*" From c247f5bca16ec559eb8e484b262bba1c6d13aa88 Mon Sep 17 00:00:00 2001 From: Shahed Nasser Date: Sat, 7 Dec 2024 00:10:52 +0200 Subject: [PATCH 04/11] docs: fix self hosting deployments not showing (#10486) --- www/apps/resources/app/deployment/page.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/www/apps/resources/app/deployment/page.mdx b/www/apps/resources/app/deployment/page.mdx index 6a79b69667496..c96aa53478f74 100644 --- a/www/apps/resources/app/deployment/page.mdx +++ b/www/apps/resources/app/deployment/page.mdx @@ -32,7 +32,7 @@ With Medusa Cloud, you maintain full customization control as you deploy your ow To host and maintain Medusa on your own, check out the following guides. - + --- From 55f5ce4690db29c8253f2206d3929b40cd9b723f Mon Sep 17 00:00:00 2001 From: Hirotaka Mizutani <52546+hirotaka@users.noreply.github.com> Date: Sat, 7 Dec 2024 22:52:15 +0900 Subject: [PATCH 05/11] feat: added Japanese translation (#10379) * feat: add Japanese language support * feat: add missing required keys for Japanese translation --- .../admin/dashboard/src/i18n/languages.ts | 8 +- .../dashboard/src/i18n/translations/index.ts | 4 + .../dashboard/src/i18n/translations/ja.json | 2801 +++++++++++++++++ 3 files changed, 2812 insertions(+), 1 deletion(-) create mode 100644 packages/admin/dashboard/src/i18n/translations/ja.json diff --git a/packages/admin/dashboard/src/i18n/languages.ts b/packages/admin/dashboard/src/i18n/languages.ts index b82e3d6976c29..1926d8a24f6f5 100644 --- a/packages/admin/dashboard/src/i18n/languages.ts +++ b/packages/admin/dashboard/src/i18n/languages.ts @@ -1,4 +1,4 @@ -import { de, enUS, es, fr, it, pl, ptBR, th, tr } from "date-fns/locale" +import { de, enUS, es, fr, it, ja, pl, ptBR, th, tr } from "date-fns/locale" import { Language } from "./types" export const languages: Language[] = [ @@ -32,6 +32,12 @@ export const languages: Language[] = [ ltr: true, date_locale: it, }, + { + code: "ja", + display_name: "日本語", + ltr: true, + date_locale: ja, + }, { code: "pl", display_name: "Polski", diff --git a/packages/admin/dashboard/src/i18n/translations/index.ts b/packages/admin/dashboard/src/i18n/translations/index.ts index cca0a445277b7..00ceee957d08e 100644 --- a/packages/admin/dashboard/src/i18n/translations/index.ts +++ b/packages/admin/dashboard/src/i18n/translations/index.ts @@ -3,6 +3,7 @@ import en from "./en.json" import es from "./es.json" import fr from "./fr.json" import it from "./it.json" +import ja from "./ja.json" import pl from "./pl.json" import ptBR from "./ptBR.json" import th from "./th.json" @@ -24,6 +25,9 @@ export default { it: { translation: it, }, + ja: { + translation: ja, + }, pl: { translation: pl, }, diff --git a/packages/admin/dashboard/src/i18n/translations/ja.json b/packages/admin/dashboard/src/i18n/translations/ja.json new file mode 100644 index 0000000000000..b2c3d13f8de80 --- /dev/null +++ b/packages/admin/dashboard/src/i18n/translations/ja.json @@ -0,0 +1,2801 @@ +{ + "$schema": "./$schema.json", + "general": { + "ascending": "昇順", + "descending": "降順", + "add": "追加", + "start": "開始", + "end": "終了", + "open": "開く", + "close": "閉じる", + "apply": "適用", + "range": "範囲", + "search": "検索", + "of": "の", + "results": "結果", + "pages": "ページ", + "next": "次へ", + "prev": "前へ", + "is": "は", + "timeline": "タイムライン", + "success": "成功", + "warning": "警告", + "tip": "ヒント", + "error": "エラー", + "select": "選択", + "selected": "選択済み", + "enabled": "有効", + "disabled": "無効", + "expired": "期限切れ", + "active": "アクティブ", + "revoked": "取り消し済み", + "new": "新規", + "modified": "変更済み", + "added": "追加済み", + "removed": "削除済み", + "admin": "管理者", + "store": "ストア", + "details": "詳細", + "items_one": "{{count}}個のアイテム", + "items_other": "{{count}}個のアイテム", + "countSelected": "{{count}}個選択済み", + "countOfTotalSelected": "{{total}}個中{{count}}個選択済み", + "plusCount": "+ {{count}}", + "plusCountMore": "+ さらに{{count}}個", + "areYouSure": "本当によろしいですか?", + "noRecordsFound": "レコードが見つかりません", + "typeToConfirm": "確認のために{val}と入力してください:", + "noResultsTitle": "結果なし", + "noResultsMessage": "フィルターまたは検索クエリを変更してみてください", + "noSearchResults": "検索結果なし", + "noSearchResultsFor": "<0>'{{query}}'の検索結果はありません", + "noRecordsTitle": "レコードなし", + "noRecordsMessage": "表示するレコードがありません", + "unsavedChangesTitle": "このフォームから離れてもよろしいですか?", + "unsavedChangesDescription": "保存されていない変更があります。このフォームを終了すると、これらの変更は失われます。", + "includesTaxTooltip": "この列の価格は税込みです。", + "excludesTaxTooltip": "この列の価格は税抜きです。", + "noMoreData": "これ以上データはありません" + }, + "json": { + "header": "JSON", + "numberOfKeys_one": "{{count}}個のキー", + "numberOfKeys_other": "{{count}}個のキー", + "drawer": { + "header_one": "JSON <0>· {{count}}個のキー", + "header_other": "JSON <0>· {{count}}個のキー", + "description": "このオブジェクトのJSONデータを表示します。" + } + }, + "metadata": { + "header": "メタデータ", + "numberOfKeys_one": "{{count}}個のキー", + "numberOfKeys_other": "{{count}}個のキー", + "edit": { + "header": "メタデータを編集", + "description": "このオブジェクトのメタデータを編集します。", + "successToast": "メタデータが正常に更新されました。", + "actions": { + "insertRowAbove": "上に行を挿入", + "insertRowBelow": "下に行を挿入", + "deleteRow": "行を削除" + }, + "labels": { + "key": "キー", + "value": "値" + }, + "complexRow": { + "label": "一部の行が無効になっています", + "description": "このオブジェクトには、配列やオブジェクトなどのプリミティブでないメタデータが含まれており、ここでは編集できません。無効になっている行を編集するには、APIを直接使用してください。", + "tooltip": "この行はプリミティブでないデータを含むため無効になっています。" + } + } + }, + "validation": { + "mustBeInt": "値は数値でなければなりません。", + "mustBePositive": "値は正の数値でなければなりません。" + }, + "actions": { + "save": "保存", + "saveAsDraft": "下書きとして保存", + "copy": "コピー", + "copied": "コピーしました", + "duplicate": "複製", + "publish": "公開", + "create": "作成", + "delete": "削除", + "remove": "除去", + "revoke": "無効", + "cancel": "キャンセル", + "forceConfirm": "強制確認", + "continueEdit": "編集を続ける", + "enable": "有効化", + "disable": "無効化", + "undo": "元に戻す", + "complete": "完了", + "viewDetails": "詳細を表示", + "back": "戻る", + "close": "閉じる", + "showMore": "もっと見る", + "continue": "続ける", + "continueWithEmail": "メールで続ける", + "idCopiedToClipboard": "IDをクリップボードにコピーしました", + "addReason": "理由を追加", + "addNote": "メモを追加", + "reset": "リセット", + "confirm": "確認", + "edit": "編集", + "addItems": "項目を追加", + "download": "ダウンロード", + "clear": "クリア", + "clearAll": "すべてクリア", + "apply": "適用", + "add": "追加", + "select": "選択", + "browse": "参照", + "logout": "ログアウト", + "hide": "非表示", + "export": "エクスポート", + "import": "インポート", + "cannotUndo": "この操作は元に戻せません" + }, + "operators": { + "in": "含む" + }, + "app": { + "search": { + "label": "検索", + "title": "検索", + "description": "注文、商品、顧客など、ストア全体を検索します。", + "allAreas": "すべての領域", + "navigation": "ナビゲーション", + "openResult": "結果を開く", + "showMore": "もっと見る", + "placeholder": "何でも検索...", + "noResultsTitle": "結果が見つかりません", + "noResultsMessage": "検索に一致するものが見つかりませんでした。", + "emptySearchTitle": "検索するには入力してください", + "emptySearchMessage": "キーワードまたはフレーズを入力して探索してください。", + "loadMore": "さらに{{count}}件読み込む", + "groups": { + "all": "すべての領域", + "customer": "顧客", + "customerGroup": "顧客グループ", + "product": "商品", + "productVariant": "商品バリエーション", + "inventory": "在庫", + "reservation": "予約", + "category": "カテゴリ", + "collection": "コレクション", + "order": "注文", + "promotion": "プロモーション", + "campaign": "キャンペーン", + "priceList": "価格リスト", + "user": "ユーザー", + "region": "地域", + "taxRegion": "税地域", + "returnReason": "返品理由", + "salesChannel": "販売チャネル", + "productType": "商品タイプ", + "productTag": "商品タグ", + "location": "拠点", + "shippingProfile": "配送プロファイル", + "publishableApiKey": "公開可能なAPIキー", + "secretApiKey": "秘密のAPIキー", + "command": "コマンド", + "navigation": "ナビゲーション" + } + }, + "keyboardShortcuts": { + "pageShortcut": "飛び先", + "settingShortcut": "設定", + "commandShortcut": "コマンド", + "then": "次に", + "navigation": { + "goToOrders": "注文", + "goToProducts": "商品", + "goToCollections": "コレクション", + "goToCategories": "カテゴリ", + "goToCustomers": "顧客", + "goToCustomerGroups": "顧客グループ", + "goToInventory": "在庫", + "goToReservations": "予約", + "goToPriceLists": "価格リスト", + "goToPromotions": "プロモーション", + "goToCampaigns": "キャンペーン" + }, + "settings": { + "goToSettings": "設定", + "goToStore": "ストア", + "goToUsers": "ユーザー", + "goToRegions": "地域", + "goToTaxRegions": "税地域", + "goToSalesChannels": "販売チャネル", + "goToProductTypes": "商品タイプ", + "goToLocations": "拠点", + "goToPublishableApiKeys": "公開可能なAPIキー", + "goToSecretApiKeys": "秘密のAPIキー", + "goToWorkflows": "ワークフロー", + "goToProfile": "プロファイル", + "goToReturnReasons": "返品理由" + } + }, + "menus": { + "user": { + "documentation": "ドキュメント", + "changelog": "変更履歴", + "shortcuts": "ショートカット", + "profileSettings": "プロファイル設定", + "theme": { + "label": "テーマ", + "dark": "ダーク", + "light": "ライト", + "system": "システム" + } + }, + "store": { + "label": "ストア", + "storeSettings": "ストア設定" + }, + "actions": { + "logout": "ログアウト" + } + }, + "nav": { + "accessibility": { + "title": "ナビゲーション", + "description": "ダッシュボードのナビゲーションメニュー。" + }, + "common": { + "extensions": "拡張機能" + }, + "main": { + "store": "ストア", + "storeSettings": "ストア設定" + }, + "settings": { + "header": "設定", + "general": "一般", + "developer": "開発者", + "myAccount": "マイアカウント" + } + } + }, + "dataGrid": { + "columns": { + "view": "表示", + "resetToDefault": "デフォルトにリセット", + "disabled": "表示列の変更は無効になっています。" + }, + "shortcuts": { + "label": "ショートカット", + "commands": { + "undo": "元に戻す", + "redo": "やり直し", + "copy": "コピー", + "paste": "貼り付け", + "edit": "編集", + "delete": "削除", + "clear": "クリア", + "moveUp": "上に移動", + "moveDown": "下に移動", + "moveLeft": "左に移動", + "moveRight": "右に移動", + "moveTop": "一番上に移動", + "moveBottom": "一番下に移動", + "selectDown": "下を選択", + "selectUp": "上を選択", + "selectColumnDown": "下の列を選択", + "selectColumnUp": "上の列を選択", + "focusToolbar": "ツールバーにフォーカス", + "focusCancel": "キャンセルにフォーカス" + } + }, + "errors": { + "fixError": "エラーを修正", + "count_one": "{{count}}個のエラー", + "count_other": "{{count}}個のエラー" + } + }, + "filters": { + "date": { + "today": "今日", + "lastSevenDays": "過去7日間", + "lastThirtyDays": "過去30日間", + "lastNinetyDays": "過去90日間", + "lastTwelveMonths": "過去12ヶ月", + "custom": "カスタム", + "from": "開始日", + "to": "終了日" + }, + "compare": { + "lessThan": "未満", + "greaterThan": "以上", + "exact": "一致", + "range": "範囲", + "lessThanLabel": "{{value}}未満", + "greaterThanLabel": "{{value}}以上", + "andLabel": "かつ" + }, + "addFilter": "フィルター追加" + }, + "errorBoundary": { + "badRequestTitle": "400 - 不正なリクエスト", + "badRequestMessage": "構文が不正なため、サーバーがリクエストを理解できませんでした。", + "notFoundTitle": "404 - このアドレスにページが存在しません", + "notFoundMessage": "URLを確認して再試行するか、検索バーを使用して探しているものを見つけてください。", + "internalServerErrorTitle": "500 - 内部サーバーエラー", + "internalServerErrorMessage": "サーバーで予期せぬエラーが発生しました。後ほど再試行してください。", + "defaultTitle": "エラーが発生しました", + "defaultMessage": "このページの表示中に予期せぬエラーが発生しました。", + "noMatchMessage": "お探しのページは存在しません。", + "backToDashboard": "ダッシュボードに戻る" + }, + "addresses": { + "shippingAddress": { + "header": "配送先住所", + "editHeader": "配送先住所の編集", + "editLabel": "配送先住所", + "label": "配送先住所" + }, + "billingAddress": { + "header": "請求先住所", + "editHeader": "請求先住所の編集", + "editLabel": "請求先住所", + "label": "請求先住所", + "sameAsShipping": "配送先住所と同じ" + }, + "contactHeading": "連絡先", + "locationHeading": "所在地" + }, + "email": { + "editHeader": "Eメールを編集", + "editLabel": "Eメール", + "label": "Eメール" + }, + "transferOwnership": { + "header": "所有権の移転", + "label": "所有権の移転", + "details": { + "order": "注文詳細", + "draft": "下書き詳細" + }, + "currentOwner": { + "label": "現在の所有者", + "hint": "注文の現在の所有者です。" + }, + "newOwner": { + "label": "新しい所有者", + "hint": "注文を移転する新しい所有者です。" + }, + "validation": { + "mustBeDifferent": "新しい所有者は現在の所有者と異なる必要があります。", + "required": "新しい所有者は必須です。" + } + }, + "sales_channels": { + "availableIn": "<1>{{y}}の販売チャネルのうち<0>{{x}}で利用可能" + }, + "products": { + "domain": "商品", + "list": { + "noRecordsMessage": "最初の商品を作成して販売を開始しましょう。" + }, + "edit": { + "header": "商品を編集", + "description": "商品の詳細を編集します。", + "successToast": "商品「{{title}}」が正常に更新されました。" + }, + "create": { + "title": "商品を作成", + "description": "新しい商品を作成します。", + "header": "一般", + "tabs": { + "details": "詳細", + "organize": "整理", + "variants": "バリエーション", + "inventory": "在庫キット" + }, + "errors": { + "variants": "少なくとも1つのバリエーションを選択してください。", + "options": "少なくとも1つのオプションを作成してください。", + "uniqueSku": "SKUは一意である必要があります。" + }, + "inventory": { + "heading": "在庫キット", + "label": "バリエーションの在庫キットに在庫アイテムを追加します。", + "itemPlaceholder": "在庫アイテムを選択", + "quantityPlaceholder": "このキットに必要な数量は?" + }, + "variants": { + "header": "バリエーション", + "subHeadingTitle": "はい、これはバリエーションのある商品です", + "subHeadingDescription": "チェックを外すと、デフォルトのバリエーションを作成します", + "optionTitle": { + "placeholder": "サイズ" + }, + "optionValues": { + "placeholder": "小、中、大" + }, + "productVariants": { + "label": "商品バリエーション", + "hint": "このランキングはストアフロントでのバリエーションの順序に影響します。", + "alert": "バリエーションを作成するにはオプションを追加してください。", + "tip": "チェックをしていないバリエーションは作成されません。後でバリエーションを作成・編集できますが、このリストは商品オプションのバリエーションに適合します。" + }, + "productOptions": { + "label": "商品オプション", + "hint": "色、サイズなど、商品のオプションを定義します。" + } + }, + "successToast": "商品「{{title}}」が正常に作成されました。" + }, + "export": { + "header": "商品リストをエクスポート", + "description": "商品リストをCSVファイルにエクスポートします。", + "success": { + "title": "エクスポートを処理中です", + "description": "データのエクスポートには数分かかる場合があります。完了時にお知らせします。" + }, + "filters": { + "title": "フィルター", + "description": "テーブル概要でフィルターを適用してこのビューを調整します" + }, + "columns": { + "title": "列", + "description": "特定のニーズに合わせてエクスポートするデータをカスタマイズします" + } + }, + "import": { + "header": "商品リストをインポート", + "uploadLabel": "商品をインポート", + "uploadHint": "CSVファイルをドラッグ&ドロップするか、クリックしてアップロードします", + "description": "事前に定義されたフォーマットのCSVファイルを提供して商品をインポートします", + "template": { + "title": "リストの配置方法が不明ですか?", + "description": "正しいフォーマットに従っていることを確認するために、以下のテンプレートをダウンロードしてください。" + }, + "upload": { + "title": "CSVファイルをアップロード", + "description": "インポートにより商品を追加または更新できます。既存の商品を更新するには、既存のハンドルとIDを使用する必要があります。既存のバリエーションを更新するには、既存のIDを使用する必要があります。商品をインポートする前に確認を求められます。", + "preprocessing": "前処理中...", + "productsToCreate": "作成される商品", + "productsToUpdate": "更新される商品" + }, + "success": { + "title": "インポートを処理中です", + "description": "データのインポートには時間がかかる場合があります。完了時にお知らせします。" + } + }, + "deleteWarning": "商品「{{title}}」を削除しようとしています。この操作は元に戻せません。", + "variants": "バリエーション", + "attributes": "属性", + "editAttributes": "属性を編集", + "editOptions": "オプションを編集", + "editPrices": "価格を編集", + "media": { + "label": "メディア", + "editHint": "ストアフロントで商品を展示するためにメディアを追加します。", + "makeThumbnail": "サムネイルを作成", + "uploadImagesLabel": "画像をアップロード", + "uploadImagesHint": "ここに画像をドラッグ&ドロップするか、クリックしてアップロードします。", + "invalidFileType": "「{{name}}」はサポートされていないファイルタイプです。サポートされているファイルタイプは次のとおりです:{{types}}。", + "failedToUpload": "追加されたメディアのアップロードに失敗しました。もう一度お試しください。", + "deleteWarning_one": "{{count}}枚の画像を削除しようとしています。この操作は元に戻せません。", + "deleteWarning_other": "{{count}}枚の画像を削除しようとしています。この操作は元に戻せません。", + "deleteWarningWithThumbnail_one": "サムネイルを含む{{count}}枚の画像を削除しようとしています。この操作は元に戻せません。", + "deleteWarningWithThumbnail_other": "サムネイルを含む{{count}}枚の画像を削除しようとしています。この操作は元に戻せません。", + "thumbnailTooltip": "サムネイル", + "galleryLabel": "ギャラリー", + "downloadImageLabel": "現在の画像をダウンロード", + "deleteImageLabel": "現在の画像を削除", + "emptyState": { + "header": "まだメディアはありません", + "description": "ストアフロントで商品を展示するためにメディアを追加します。", + "action": "メディアを追加" + }, + "successToast": "メディアは正常に更新されました。" + }, + "discountableHint": "チェックを外すと、この商品に割引は適用されません。", + "noSalesChannels": "どの販売チャネルでも利用できません", + "variantCount_one": "{{count}}個のバリエーション", + "variantCount_other": "{{count}}個のバリエーション", + "deleteVariantWarning": "バリエーション「{{title}}」を削除しようとしています。この操作は元に戻せません。", + "productStatus": { + "draft": "下書き", + "published": "公開済み", + "proposed": "提案済み", + "rejected": "却下" + }, + "fields": { + "title": { + "label": "タイトル", + "hint": "商品に短く明確なタイトルを付けてください。<0/>検索エンジンには50〜60文字が推奨されています。" + }, + "subtitle": { + "label": "サブタイトル" + }, + "handle": { + "label": "ハンドル", + "tooltip": "ハンドルはストアフロントで商品を参照するために使用されます。指定しない場合、商品タイトルからハンドルが生成されます。" + }, + "description": { + "label": "説明", + "hint": "商品に短く明確な説明を付けてください。<0/>検索エンジンには120〜160文字が推奨されています。" + }, + "discountable": { + "label": "割引可能", + "hint": "チェックを外すと、この商品に割引は適用されません" + }, + "type": { + "label": "タイプ" + }, + "collection": { + "label": "コレクション" + }, + "categories": { + "label": "カテゴリー" + }, + "tags": { + "label": "タグ" + }, + "sales_channels": { + "label": "販売チャネル", + "hint": "未選択の場合、この商品はデフォルトの販売チャネルでのみ利用可能になります。" + }, + "countryOrigin": { + "label": "原産国" + }, + "material": { + "label": "素材" + }, + "width": { + "label": "幅" + }, + "length": { + "label": "長さ" + }, + "height": { + "label": "高さ" + }, + "weight": { + "label": "重さ" + }, + "options": { + "label": "商品オプション", + "hint": "オプションは商品の色、サイズなどを定義するために使用されます", + "add": "オプションを追加", + "optionTitle": "オプションタイトル", + "optionTitlePlaceholder": "色", + "variations": "バリエーション(カンマ区切り)", + "variantionsPlaceholder": "赤,青,緑" + }, + "variants": { + "label": "商品バリエーション", + "hint": "チェックを外したバリエーションは作成されません。このランキングはフロントエンドでのバリエーションのランク付けに影響します。" + }, + "mid_code": { + "label": "中分類コード" + }, + "hs_code": { + "label": "HSコード" + } + }, + "variant": { + "edit": { + "header": "バリエーションを編集", + "success": "商品バリエーションが正常に編集されました" + }, + "create": { + "header": "バリエーション詳細" + }, + "deleteWarning": "このバリエーションを削除してもよろしいですか?", + "pricesPagination": "1 - {{total}}件中{{current}}件の価格", + "tableItemAvailable": "{{availableCount}}個利用可能", + "tableItem_one": "{{locationCount}}の拠点で{{availableCount}}個利用可能", + "tableItem_other": "{{locationCount}}の拠点で{{availableCount}}個利用可能", + "inventory": { + "notManaged": "管理されていません", + "manageItems": "在庫アイテムを管理", + "notManagedDesc": "このバリエーションの在庫は管理されていません。「在庫を管理」をオンにしてバリエーションの在庫を追跡します。", + "manageKit": "在庫キットを管理", + "navigateToItem": "在庫アイテムに移動", + "actions": { + "inventoryItems": "在庫アイテムに移動", + "inventoryKit": "在庫アイテムを表示" + }, + "inventoryKit": "在庫キット", + "inventoryKitHint": "このバリエーションは複数の在庫アイテムで構成されていますか?", + "validation": { + "itemId": "在庫アイテムを選択してください。", + "quantity": "数量は必須です。正の数値を入力してください。" + }, + "header": "在庫と在庫管理", + "editItemDetails": "アイテム詳細を編集", + "manageInventoryLabel": "在庫を管理", + "manageInventoryHint": "有効にすると、注文や返品が作成されたときに在庫数量を変更します。", + "allowBackordersLabel": "取り寄せを許可", + "allowBackordersHint": "有効にすると、利用可能な数量がない場合でも顧客がバリエーションを購入できます。", + "toast": { + "levelsBatch": "在庫レベルが更新されました。", + "update": "在庫アイテムが正常に更新されました。", + "updateLevel": "在庫レベルが正常に更新されました。", + "itemsManageSuccess": "在庫アイテムが正常に更新されました。" + } + } + }, + "options": { + "header": "オプション", + "edit": { + "header": "オプションを編集", + "successToast": "オプション「{{title}}」が正常に更新されました。" + }, + "create": { + "header": "オプションを作成", + "successToast": "オプション「{{title}}」が正常に作成されました。" + }, + "deleteWarning": "商品オプション「{{title}}」を削除しようとしています。この操作は元に戻せません。" + }, + "organization": { + "header": "整理", + "edit": { + "header": "整理を編集", + "toasts": { + "success": "「{{title}}」の整理が正常に更新されました。" + } + } + }, + "toasts": { + "delete": { + "success": { + "header": "商品が削除されました", + "description": "「{{title}}」が正常に削除されました。" + }, + "error": { + "header": "商品の削除に失敗しました" + } + } + } + }, + "collections": { + "domain": "コレクション", + "subtitle": "商品をコレクションに整理します。", + "createCollection": "コレクションを作成", + "createCollectionHint": "商品を整理するための新しいコレクションを作成します。", + "createSuccess": "コレクションが正常に作成されました。", + "editCollection": "コレクションを編集", + "handleTooltip": "ハンドルはストアフロントでコレクションを参照するために使用されます。指定しない場合、コレクションのタイトルからハンドルが生成されます。", + "deleteWarning": "コレクション「{{title}}」を削除しようとしています。この操作は元に戻せません。", + "removeSingleProductWarning": "商品「{{title}}」をコレクションから削除しようとしています。この操作は元に戻せません。", + "removeProductsWarning_one": "{{count}}個の商品をコレクションから削除しようとしています。この操作は元に戻せません。", + "removeProductsWarning_other": "{{count}}個の商品をコレクションから削除しようとしています。この操作は元に戻せません。", + "products": { + "list": { + "noRecordsMessage": "コレクションに商品がありません。" + }, + "add": { + "successToast_one": "商品がコレクションに正常に追加されました。", + "successToast_other": "商品がコレクションに正常に追加されました。" + }, + "remove": { + "successToast_one": "商品がコレクションから正常に削除されました。", + "successToast_other": "商品がコレクションから正常に削除されました。" + } + } + }, + "categories": { + "domain": "カテゴリー", + "subtitle": "商品をカテゴリーに整理し、それらのカテゴリーのランキングと階層を管理します。", + "create": { + "header": "カテゴリーを作成", + "hint": "商品を整理するための新しいカテゴリーを作成します。", + "tabs": { + "details": "詳細", + "organize": "ランキングを整理" + }, + "successToast": "カテゴリー「{{name}}」が正常に作成されました。" + }, + "edit": { + "header": "カテゴリーを編集", + "description": "カテゴリーを編集して詳細を更新します。", + "successToast": "カテゴリーが正常に更新されました。" + }, + "delete": { + "confirmation": "カテゴリー「{{name}}」を削除しようとしています。この操作は元に戻せません。", + "successToast": "カテゴリー「{{name}}」が正常に削除されました。" + }, + "products": { + "add": { + "disabledTooltip": "この商品は既にこのカテゴリーに含まれています。", + "successToast_one": "{{count}}個の商品をカテゴリーに追加しました。", + "successToast_other": "{{count}}個の商品をカテゴリーに追加しました。" + }, + "remove": { + "confirmation_one": "{{count}}個の商品をカテゴリーから削除しようとしています。この操作は元に戻せません。", + "confirmation_other": "{{count}}個の商品をカテゴリーから削除しようとしています。この操作は元に戻せません。", + "successToast_one": "{{count}}個の商品をカテゴリーから削除しました。", + "successToast_other": "{{count}}個の商品をカテゴリーから削除しました。" + }, + "list": { + "noRecordsMessage": "このカテゴリーに商品はありません。" + } + }, + "organize": { + "header": "整理", + "action": "ランキングを編集" + }, + "fields": { + "visibility": { + "label": "可視性", + "internal": "内部", + "public": "公開" + }, + "status": { + "label": "ステータス", + "active": "アクティブ", + "inactive": "非アクティブ" + }, + "path": { + "label": "パス", + "tooltip": "カテゴリーの完全なパスを表示します。" + }, + "children": { + "label": "子カテゴリー" + }, + "new": { + "label": "新規" + } + } + }, + "inventory": { + "domain": "在庫", + "subtitle": "在庫アイテムを管理する", + "reserved": "予約済み", + "available": "利用可能", + "locationLevels": "拠点", + "associatedVariants": "関連バリエーション", + "manageLocations": "拠点を管理", + "deleteWarning": "在庫アイテムを削除しようとしています。この操作は元に戻せません。", + "editItemDetails": "アイテム詳細を編集", + "create": { + "title": "在庫アイテムを作成", + "details": "詳細", + "availability": "在庫状況", + "locations": "拠点", + "attributes": "属性", + "requiresShipping": "配送が必要", + "requiresShippingHint": "この在庫アイテムは配送が必要ですか?", + "successToast": "在庫アイテムが正常に作成されました。" + }, + "reservation": { + "header": "{{itemName}}の予約", + "editItemDetails": "予約を編集", + "lineItemId": "ラインアイテムID", + "orderID": "注文ID", + "description": "説明", + "location": "拠点", + "inStockAtLocation": "この拠点の在庫", + "availableAtLocation": "この拠点で利用可能", + "reservedAtLocation": "この拠点で予約済み", + "reservedAmount": "予約数量", + "create": "予約を作成", + "itemToReserve": "予約するアイテム", + "quantityPlaceholder": "予約する数量を入力してください", + "descriptionPlaceholder": "どのタイプの予約ですか?", + "successToast": "予約が正常に作成されました。", + "updateSuccessToast": "予約が正常に更新されました。", + "deleteSuccessToast": "予約が正常に削除されました。", + "errors": { + "noAvaliableQuantity": "在庫ロケーションに利用可能な数量がありません。", + "quantityOutOfRange": "最小数量は1、最大数量は{{max}}です" + } + }, + "adjustInventory": { + "errors": { + "stockedQuantity": "在庫数量を予約数量{{quantity}}より少ない数に更新することはできません。" + } + }, + "toast": { + "updateLocations": "拠点が正常に更新されました。", + "updateLevel": "在庫レベルが正常に更新されました。", + "updateItem": "在庫アイテムが正常に更新されました。" + } + }, + "giftCards": { + "domain": "ギフトカード", + "editGiftCard": "ギフトカードを編集", + "createGiftCard": "ギフトカードを作成", + "createGiftCardHint": "ストアで支払い方法として使用できるギフトカードを手動で作成します。", + "selectRegionFirst": "まず地域を選択してください", + "deleteGiftCardWarning": "ギフトカード「{{code}}」を削除しようとしています。この操作は元に戻せません。", + "balanceHigherThanValue": "残高は元の金額を超えることはできません。", + "balanceLowerThanZero": "残高をマイナスにすることはできません。", + "expiryDateHint": "ギフトカードの有効期限に関する法律は国によって異なります。有効期限を設定する前に、現地の法令を確認してください。", + "regionHint": "ギフトカードの地域を変更すると、通貨も変更され、金銭的価値に影響を与える可能性があります。", + "enabledHint": "ギフトカードを有効にするか無効にするかを指定します。", + "balance": "残高", + "currentBalance": "現在の残高", + "initialBalance": "初期残高", + "personalMessage": "個人メッセージ", + "recipient": "受取人" + }, + "customers": { + "domain": "顧客", + "list": { + "noRecordsMessage": "顧客情報がここに表示されます。" + }, + "create": { + "header": "顧客を作成", + "hint": "新しい顧客を作成し、その詳細を管理します。", + "successToast": "顧客「{{email}}」が正常に作成されました。" + }, + "groups": { + "label": "顧客グループ", + "remove": "顧客を「{{name}}」グループから削除してもよろしいですか?", + "removeMany": "顧客を以下の顧客グループから削除してもよろしいですか:{{groups}}?", + "alreadyAddedTooltip": "顧客は既にこの顧客グループに所属しています。", + "list": { + "noRecordsMessage": "この顧客はどのグループにも所属していません。" + }, + "add": { + "success": "顧客を追加しました:{{groups}}", + "list": { + "noRecordsMessage": "まず顧客グループを作成してください。" + } + }, + "removed": { + "success": "顧客を削除しました:{{groups}}", + "list": { + "noRecordsMessage": "まず顧客グループを作成してください。" + } + } + }, + "edit": { + "header": "顧客を編集", + "emailDisabledTooltip": "登録済み顧客のメールアドレスは変更できません。", + "successToast": "顧客「{{email}}」が正常に更新されました。" + }, + "delete": { + "title": "顧客を削除", + "description": "顧客「{{email}}」を削除しようとしています。この操作は元に戻せません。", + "successToast": "顧客「{{email}}」が正常に削除されました。" + }, + "fields": { + "guest": "ゲスト", + "registered": "登録済み", + "groups": "グループ" + }, + "registered": "登録済み", + "guest": "ゲスト", + "hasAccount": "アカウントあり" + }, + "customerGroups": { + "domain": "顧客グループ", + "subtitle": "顧客をグループに整理します。グループごとに異なるプロモーションや価格を設定できます。", + "create": { + "header": "顧客グループを作成", + "hint": "顧客をセグメント化するための新しい顧客グループを作成します。", + "successToast": "顧客グループ「{{name}}」が正常に作成されました。" + }, + "edit": { + "header": "顧客グループを編集", + "successToast": "顧客グループ「{{name}}」が正常に更新されました。" + }, + "delete": { + "title": "顧客グループを削除", + "description": "顧客グループ「{{name}}」を削除しようとしています。この操作は元に戻せません。", + "successToast": "顧客グループ「{{name}}」が正常に削除されました。" + }, + "customers": { + "alreadyAddedTooltip": "この顧客は既にグループに追加されています。", + "add": { + "successToast_one": "顧客がグループに正常に追加されました。", + "successToast_other": "顧客がグループに正常に追加されました。", + "list": { + "noRecordsMessage": "まず顧客を作成してください。" + } + }, + "remove": { + "title_one": "顧客を削除", + "title_other": "顧客を削除", + "description_one": "{{count}}人の顧客を顧客グループから削除しようとしています。この操作は元に戻せません。", + "description_other": "{{count}}人の顧客を顧客グループから削除しようとしています。この操作は元に戻せません。" + }, + "list": { + "noRecordsMessage": "このグループには顧客がいません。" + } + } + }, + "orders": { + "domain": "注文", + "claim": "クレーム", + "exchange": "交換", + "return": "返品", + "cancelWarning": "注文{{id}}をキャンセルしようとしています。この操作は元に戻せません。", + "onDateFromSalesChannel": "{{salesChannel}}から{{date}}", + "list": { + "noRecordsMessage": "あなたの注文がここに表示されます。" + }, + "summary": { + "requestReturn": "返品をリクエスト", + "allocateItems": "アイテムを割り当てる", + "editOrder": "注文を編集", + "editOrderContinue": "注文編集を続ける", + "inventoryKit": "{{count}}個の在庫アイテムで構成", + "itemTotal": "アイテム合計", + "shippingTotal": "配送合計", + "discountTotal": "割引合計", + "taxTotalIncl": "税金合計(税込)", + "itemSubtotal": "アイテム小計", + "shippingSubtotal": "配送小計", + "discountSubtotal": "割引小計", + "taxTotal": "税金合計" + }, + "transfer": { + "title": "所有権の移転", + "requestSuccess": "注文移転リクエストが{{email}}に送信されました。", + "currentOwner": "現在の所有者", + "newOwner": "新しい所有者", + "currentOwnerDescription": "この注文に現在関連付けられている顧客。", + "newOwnerDescription": "この注文を移転する顧客。" + }, + "payment": { + "title": "支払い", + "isReadyToBeCaptured": "支払い<0/>が確定しました。", + "totalPaidByCustomer": "顧客による支払い合計", + "capture": "支払いが確定", + "capture_short": "確定済み", + "refund": "返金", + "markAsPaid": "支払い済みとしてマーク", + "statusLabel": "支払いステータス", + "statusTitle": "支払いステータス", + "status": { + "notPaid": "未払い", + "authorized": "承認済み", + "partiallyAuthorized": "一部承認", + "awaiting": "待機中", + "captured": "確定済み", + "partiallyRefunded": "一部返金", + "partiallyCaptured": "一部支払い済み", + "refunded": "返金済み", + "canceled": "キャンセル済み", + "requiresAction": "対応が必要" + }, + "capturePayment": "{{amount}}の支払いが確定されます。", + "capturePaymentSuccess": "{{amount}}の支払いが正常に確定されました", + "markAsPaidPayment": "{{amount}}の支払いが支払い済みとしてマークされます。", + "markAsPaidPaymentSuccess": "{{amount}}の支払いが正常に支払い済みとしてマークされました", + "createRefund": "返金を作成", + "refundPaymentSuccess": "{{amount}}の返金が成功しました", + "createRefundWrongQuantity": "数量は1から{{number}}の間の数字である必要があります", + "refundAmount": "{{ amount }}を返金", + "paymentLink": "{{ amount }}の支払いリンクをコピー", + "selectPaymentToRefund": "返金する支払いを選択" + }, + "edits": { + "title": "注文を編集", + "confirm": "編集を確認", + "confirmText": "注文編集を確認しようとしています。この操作は元に戻せません。", + "cancel": "編集をキャンセル", + "currentItems": "現在のアイテム", + "currentItemsDescription": "アイテムの数量を調整または削除。", + "addItemsDescription": "注文に新しいアイテムを追加できます。", + "addItems": "アイテムを追加", + "amountPaid": "支払い済み金額", + "newTotal": "新しい合計", + "differenceDue": "差額", + "create": "注文を編集", + "currentTotal": "現在の合計", + "noteHint": "編集の内部メモを追加", + "cancelSuccessToast": "注文編集がキャンセルされました", + "createSuccessToast": "注文編集リクエストが作成されました", + "activeChangeError": "注文にはすでにアクティブな注文変更(返品、クレーム、交換など)があります。注文を編集する前に変更を完了またはキャンセルしてください。", + "panel": { + "title": "注文編集がリクエストされました", + "titlePending": "注文編集が保留中です" + }, + "toast": { + "canceledSuccessfully": "注文編集がキャンセルされました", + "confirmedSuccessfully": "注文編集が確認されました" + }, + "validation": { + "quantityLowerThanFulfillment": "数量を出荷済みの数量以下に設定することはできません" + } + }, + "edit": { + "email": { + "title": "Eメールを編集", + "requestSuccess": "注文のEメールが「{{email}}」に更新されました。" + }, + "shippingAddress": { + "title": "配送先住所を編集", + "requestSuccess": "注文の配送先住所が更新されました。" + }, + "billingAddress": { + "title": "請求先住所を編集", + "requestSuccess": "注文の請求先住所が更新されました。" + } + }, + "returns": { + "create": "返品を作成", + "confirm": "返品を確認", + "confirmText": "返品を確認しようとしています。この操作は元に戻せません。", + "inbound": "入庫", + "outbound": "出庫", + "sendNotification": "通知を送信", + "sendNotificationHint": "顧客に返品について通知します。", + "returnTotal": "返品合計", + "inboundTotal": "入庫合計", + "refundAmount": "返金額", + "outstandingAmount": "未払い金額", + "reason": "理由", + "reasonHint": "顧客がアイテムを返品したい理由を選択してください。", + "note": "メモ", + "noInventoryLevel": "在庫レベルなし", + "noInventoryLevelDesc": "選択した拠点には選択したアイテムの在庫レベルがありません。返品をリクエストできますが、選択した拠点の在庫レベルが作成されるまで受け取ることはできません。", + "noteHint": "何か指定したい場合は自由に入力できます。", + "location": "拠点", + "locationHint": "アイテムを返品する拠点を選択してください。", + "inboundShipping": "返品配送", + "inboundShippingHint": "使用する方法を選択してください。", + "returnableQuantityLabel": "返品可能数量", + "refundableAmountLabel": "返金可能金額", + "returnRequestedInfo": "{{requestedItemsCount}}個のアイテムの返品がリクエストされました", + "returnReceivedInfo": "{{requestedItemsCount}}個のアイテムの返品を受け取りました", + "itemReceived": "アイテムを受け取りました", + "returnRequested": "返品がリクエストされました", + "damagedItemReceived": "破損したアイテムを受け取りました", + "damagedItemsReturned": "{{quantity}}個の破損したアイテムが返品されました", + "activeChangeError": "この注文には進行中のアクティブな注文変更があります。まず変更を完了または破棄してください。", + "cancel": { + "title": "返品をキャンセル", + "description": "返品リクエストをキャンセルしてもよろしいですか?" + }, + "placeholders": { + "noReturnShippingOptions": { + "title": "返品配送オプションが見つかりません", + "hint": "この拠点の返品配送オプションが作成されていません。拠点と配送で作成できます。" + }, + "outboundShippingOptions": { + "title": "出庫配送オプションが見つかりません", + "hint": "この拠点の出庫配送オプションが作成されていません。拠点と配送で作成できます。" + } + }, + "receive": { + "action": "アイテムを受け取る", + "receiveItems": "{{ returnType }} {{ id }}", + "restockAll": "すべてのアイテムを再入庫", + "itemsLabel": "受け取ったアイテム", + "title": "#{{returnId}}のアイテムを受け取る", + "sendNotificationHint": "顧客に返品の受け取りについて通知します。", + "inventoryWarning": "上記の入力に基づいて自動的に在庫レベルを調整することに注意してください。", + "writeOffInputLabel": "アイテムのうち何個が損傷していますか?", + "toast": { + "success": "返品が正常に受け取られました。", + "errorLargeValue": "数量がリクエストされたアイテム数量を超えています。", + "errorNegativeValue": "数量は負の値にはできません。", + "errorLargeDamagedValue": "損傷したアイテムの数量 + 損傷していない受け取ったアイテムの数量が返品の総アイテム数量を超えています。損傷していないアイテムの数量を減らしてください。" + } + }, + "toast": { + "canceledSuccessfully": "返品が正常にキャンセルされました", + "confirmedSuccessfully": "返品が正常に確認されました" + }, + "panel": { + "title": "返品が開始されました", + "description": "完了する必要のある未処理の返品リクエストがあります" + } + }, + "claims": { + "create": "クレームを作成", + "confirm": "クレームを確認", + "confirmText": "クレームを確認しようとしています。この操作は元に戻せません。", + "manage": "クレームを管理", + "outbound": "出庫", + "outboundItemAdded": "クレームを通じて{{itemsCount}}個追加されました", + "outboundTotal": "出庫合計", + "outboundShipping": "出庫配送", + "outboundShippingHint": "使用する方法を選択してください。", + "refundAmount": "見積もり差額", + "activeChangeError": "この注文にはアクティブな注文変更があります。前の変更を完了または破棄してください。", + "actions": { + "cancelClaim": { + "successToast": "クレームが正常にキャンセルされました。" + } + }, + "cancel": { + "title": "クレームをキャンセル", + "description": "クレームをキャンセルしてもよろしいですか?" + }, + "tooltips": { + "onlyReturnShippingOptions": "このリストには返品配送オプションのみが含まれます。" + }, + "toast": { + "canceledSuccessfully": "クレームが正常にキャンセルされました", + "confirmedSuccessfully": "クレームが正常に確認されました" + }, + "panel": { + "title": "クレームが開始されました", + "description": "完了する必要のある未処理のクレームリクエストがあります" + } + }, + "exchanges": { + "create": "交換を作成", + "manage": "交換を管理", + "confirm": "交換を確認", + "confirmText": "交換を確認しようとしています。この操作は元に戻せません。", + "outbound": "出庫", + "outboundItemAdded": "交換を通じて{{itemsCount}}個追加されました", + "outboundTotal": "出庫合計", + "outboundShipping": "出庫配送", + "outboundShippingHint": "使用する方法を選択してください。", + "refundAmount": "見積もり差額", + "activeChangeError": "この注文にはアクティブな注文変更があります。前の変更を完了または破棄してください。", + "actions": { + "cancelExchange": { + "successToast": "交換が正常にキャンセルされました。" + } + }, + "cancel": { + "title": "交換をキャンセル", + "description": "交換をキャンセルしてもよろしいですか?" + }, + "tooltips": { + "onlyReturnShippingOptions": "このリストには返品配送オプションのみが含まれます。" + }, + "toast": { + "canceledSuccessfully": "交換が正常にキャンセルされました", + "confirmedSuccessfully": "交換が正常に確認されました" + }, + "panel": { + "title": "交換が開始されました", + "description": "完了する必要のある未処理の交換リクエストがあります" + } + }, + "reservations": { + "allocatedLabel": "割り当て済み", + "notAllocatedLabel": "未割り当て" + }, + "allocateItems": { + "action": "アイテムを割り当てる", + "title": "注文アイテムを割り当てる", + "locationDescription": "割り当てる拠点を選択してください。", + "itemsToAllocate": "割り当てるアイテム", + "itemsToAllocateDesc": "割り当てたいアイテムの数を選択してください", + "search": "アイテムを検索", + "consistsOf": "{{num}}個の在庫アイテムで構成", + "requires": "バリエーションごとに{{num}}個必要", + "toast": { + "created": "アイテムが正常に割り当てられました" + }, + "error": { + "quantityNotAllocated": "割り当てられていないアイテムがあります。" + } + }, + "shipment": { + "title": "出荷済みとしてマーク", + "trackingNumber": "追跡番号", + "addTracking": "追跡番号を追加", + "sendNotification": "通知を送信", + "sendNotificationHint": "この出荷について顧客に通知します。", + "toastCreated": "出荷が正常に作成されました。" + }, + "fulfillment": { + "cancelWarning": "出荷をキャンセルしようとしています。この操作は元に戻せません。", + "markAsDeliveredWarning": "出荷を配達済みとしてマークしようとしています。この操作は元に戻せません。", + "unfulfilledItems": "未出荷アイテム", + "statusLabel": "出荷ステータス", + "statusTitle": "出荷ステータス", + "fulfillItems": "アイテムを出荷", + "awaitingFulfillmentBadge": "出荷待ち", + "requiresShipping": "配送が必要", + "number": "出荷 #{{number}}", + "itemsToFulfill": "出荷するアイテム", + "create": "出荷を作成", + "available": "利用可能", + "inStock": "在庫あり", + "markAsShipped": "配送済みとしてマーク", + "markAsDelivered": "配達済みとしてマーク", + "itemsToFulfillDesc": "出荷するアイテムと数量を選択してください", + "locationDescription": "アイテムを出荷する拠点を選択してください。", + "sendNotificationHint": "作成された出荷について顧客に通知します。", + "methodDescription": "顧客が選択した配送方法とは異なる方法を選択してください", + "error": { + "wrongQuantity": "出荷可能なアイテムは1つのみです", + "wrongQuantity_other": "数量は1から{{number}}の間の数字である必要があります", + "noItems": "出荷するアイテムがありません。" + }, + "status": { + "notFulfilled": "未出荷", + "partiallyFulfilled": "一部出荷", + "fulfilled": "出荷済み", + "partiallyShipped": "一部配送", + "shipped": "配送済み", + "delivered": "配達済み", + "partiallyDelivered": "一部配達", + "partiallyReturned": "一部返品", + "returned": "返品済み", + "canceled": "キャンセル済み", + "requiresAction": "対応が必要" + }, + "toast": { + "created": "出荷が正常に作成されました", + "canceled": "出荷が正常にキャンセルされました", + "fulfillmentShipped": "すでに配送された出荷をキャンセルすることはできません", + "fulfillmentDelivered": "出荷が正常に配達済みとしてマークされました" + }, + "trackingLabel": "追跡", + "shippingFromLabel": "発送元", + "itemsLabel": "アイテム" + }, + "refund": { + "title": "返金を作成", + "sendNotificationHint": "作成された返金について顧客に通知します。", + "systemPayment": "システム支払い", + "systemPaymentDesc": "1つ以上の支払いがシステム支払いです。このような支払いの取り込みと返金はMedusaでは処理されないことに注意してください。", + "error": { + "amountToLarge": "元の注文金額を超えて返金することはできません。", + "amountNegative": "返金額は正の数でなければなりません。", + "reasonRequired": "返金理由を選択してください。" + } + }, + "customer": { + "contactLabel": "連絡先", + "editEmail": "Eメールを編集", + "transferOwnership": "所有権を移転", + "editBillingAddress": "請求先住所を編集", + "editShippingAddress": "配送先住所を編集" + }, + "activity": { + "header": "アクティビティ", + "showMoreActivities_one": "さらに{{count}}件のアクティビティを表示", + "showMoreActivities_other": "さらに{{count}}件のアクティビティを表示", + "comment": { + "label": "コメント", + "placeholder": "コメントを残す", + "addButtonText": "コメントを追加", + "deleteButtonText": "コメントを削除" + }, + "from": "から", + "to": "まで", + "events": { + "common": { + "toReturn": "返品する", + "toSend": "送信する" + }, + "placed": { + "title": "注文が完了しました", + "fromSalesChannel": "{{salesChannel}}から" + }, + "canceled": { + "title": "注文がキャンセルされました" + }, + "payment": { + "awaiting": "支払い待ち", + "captured": "支払いが確定されました", + "canceled": "支払いがキャンセルされました", + "refunded": "支払いが返金されました" + }, + "fulfillment": { + "created": "アイテムが出荷されました", + "canceled": "出荷がキャンセルされました", + "shipped": "アイテムが配送されました", + "delivered": "アイテムが配達されました", + "items_one": "{{count}}個のアイテム", + "items_other": "{{count}}個のアイテム" + }, + "return": { + "created": "返品#{{returnId}}がリクエストされました", + "canceled": "返品#{{returnId}}がキャンセルされました", + "received": "返品#{{returnId}}を受け取りました", + "items_one": "{{count}}個のアイテムが返品されました", + "items_other": "{{count}}個のアイテムが返品されました" + }, + "note": { + "comment": "コメント", + "byLine": "{{author}}による" + }, + "claim": { + "created": "クレーム#{{claimId}}がリクエストされました", + "canceled": "クレーム#{{claimId}}がキャンセルされました", + "itemsInbound": "{{count}}個のアイテムを返品", + "itemsOutbound": "{{count}}個のアイテムを送信" + }, + "exchange": { + "created": "交換#{{exchangeId}}がリクエストされました", + "canceled": "交換#{{exchangeId}}がキャンセルされました", + "itemsInbound": "{{count}}個のアイテムを返品", + "itemsOutbound": "{{count}}個のアイテムを送信" + }, + "edit": { + "requested": "注文編集#{{editId}}がリクエストされました", + "confirmed": "注文編集#{{editId}}が確認されました" + }, + "transfer": { + "requested": "注文移転#{{transferId}}がリクエストされました", + "confirmed": "注文移転#{{transferId}}が確認されました", + "declined": "注文移転#{{transferId}}が拒否されました" + }, + "update_order": { + "shipping_address": "配送先住所が更新されました", + "billing_address": "請求先住所が更新されました", + "email": "Eメールが更新されました" + } + } + }, + "fields": { + "displayId": "表示ID", + "refundableAmount": "返金可能金額", + "returnableQuantity": "返品可能数量" + } + }, + "draftOrders": { + "domain": "下書き注文", + "deleteWarning": "下書き注文{{id}}を削除しようとしています。この操作は取り消せません。", + "paymentLinkLabel": "支払いリンク", + "cartIdLabel": "カートID", + "markAsPaid": { + "label": "支払い済みとしてマーク", + "warningTitle": "支払い済みとしてマーク", + "warningDescription": "下書き注文を支払い済みとしてマークしようとしています。この操作は取り消せず、後で支払いを受け取ることはできなくなります。" + }, + "status": { + "open": "未完了", + "completed": "完了済み" + }, + "create": { + "createDraftOrder": "下書き注文を作成", + "createDraftOrderHint": "注文が確定する前に詳細を管理するための新しい下書き注文を作成します。", + "chooseRegionHint": "地域を選択", + "existingItemsLabel": "既存の商品", + "existingItemsHint": "下書き注文に既存の商品を追加します。", + "customItemsLabel": "カスタム商品", + "customItemsHint": "下書き注文にカスタム商品を追加します。", + "addExistingItemsAction": "既存の商品を追加", + "addCustomItemAction": "カスタム商品を追加", + "noCustomItemsAddedLabel": "まだカスタム商品が追加されていません", + "noExistingItemsAddedLabel": "まだ既存の商品が追加されていません", + "chooseRegionTooltip": "まず地域を選択してください", + "useExistingCustomerLabel": "既存の顧客を使用", + "addShippingMethodsAction": "配送方法を追加", + "unitPriceOverrideLabel": "単価の上書き", + "shippingOptionLabel": "配送オプション", + "shippingOptionHint": "下書き注文の配送オプションを選択してください。", + "shippingPriceOverrideLabel": "配送料金の上書き", + "shippingPriceOverrideHint": "下書き注文の配送料金を上書きします。", + "sendNotificationLabel": "通知を送信", + "sendNotificationHint": "下書き注文が作成されたときに顧客に通知を送信します。" + }, + "validation": { + "requiredEmailOrCustomer": "Eメールまたは顧客情報が必要です。", + "requiredItems": "少なくとも1つの商品が必要です。", + "invalidEmail": "有効なEメールを入力してください。" + } + }, + "stockLocations": { + "domain": "在庫拠点と配送", + "list": { + "description": "店舗の在庫拠点と配送オプションを管理します。" + }, + "create": { + "header": "在庫拠点を作成", + "hint": "在庫拠点は、商品が保管され出荷される物理的な拠点です。", + "successToast": "拠点「{{name}}」が正常に作成されました。" + }, + "edit": { + "header": "在庫拠点を編集", + "viewInventory": "在庫を表示", + "successToast": "拠点「{{name}}」が正常に更新されました。" + }, + "delete": { + "confirmation": "在庫拠点「{{name}}」を削除しようとしています。この操作は取り消せません。" + }, + "fulfillmentProviders": { + "header": "フルフィルメントプロバイダー", + "shippingOptionsTooltip": "このドロップダウンには、この拠点で有効化されたプロバイダーのみが含まれます。ドロップダウンが無効の場合は、拠点にプロバイダーを追加してください。", + "label": "接続済みフルフィルメントプロバイダー", + "connectedTo": "{{total}}個中{{count}}個のフルフィルメントプロバイダーに接続済み", + "noProviders": "この在庫拠点はフルフィルメントプロバイダーに接続されていません。", + "action": "プロバイダーを接続", + "successToast": "在庫拠点のフルフィルメントプロバイダーが正常に更新されました。" + }, + "fulfillmentSets": { + "pickup": { + "header": "ピックアップ" + }, + "shipping": { + "header": "配送" + }, + "disable": { + "confirmation": "「{{name}}」を無効にしてもよろしいですか?関連するすべてのサービスゾーンと配送オプションが削除され、この操作は取り消せません。", + "pickup": "プックアップが正常に無効化されました。", + "shipping": "配送が正常に無効化されました。" + }, + "enable": { + "pickup": "ピックアップが正常に有効化されました。", + "shipping": "配送が正常に有効化されました。" + } + }, + "sidebar": { + "header": "配送設定", + "shippingProfiles": { + "label": "配送プロファイル", + "description": "配送要件ごとに商品をグループ化" + } + }, + "salesChannels": { + "header": "販売チャネル", + "label": "接続済み販売チャネル", + "connectedTo": "{{total}}個中{{count}}個の販売チャネルに接続済み", + "noChannels": "この拠点は販売チャネルに接続されていません。", + "action": "販売チャネルを接続", + "successToast": "販売チャネルが正常に更新されました。" + }, + "shippingOptions": { + "create": { + "shipping": { + "header": "「{{zone}}」の配送オプションを作成", + "hint": "この拠点からの商品の配送方法を定義する新しい配送オプションを作成します。", + "label": "配送オプション", + "successToast": "配送オプション「{{name}}」が正常に作成されました。" + }, + "returns": { + "header": "「{{zone}}」の返品オプションを作成", + "hint": "この拠点への商品の返品方法を定義する新しい返品オプションを作成します。", + "label": "返品オプション", + "successToast": "返品オプション「{{name}}」が正常に作成されました。" + }, + "tabs": { + "details": "詳細", + "prices": "価格" + }, + "action": "オプションを作成" + }, + "delete": { + "confirmation": "配送オプション「{{name}}」を削除しようとしています。この操作は取り消せません。", + "successToast": "配送オプション「{{name}}」が正常に削除されました。" + }, + "edit": { + "header": "配送オプションを編集", + "action": "オプションを編集", + "successToast": "配送オプション「{{name}}」が正常に更新されました。" + }, + "pricing": { + "action": "価格を編集" + }, + "fields": { + "count": { + "shipping_one": "{{count}}個の配送オプション", + "shipping_other": "{{count}}個の配送オプション", + "returns_one": "{{count}}個の返品オプション", + "returns_other": "{{count}}個の返品オプション" + }, + "priceType": { + "label": "価格タイプ", + "options": { + "fixed": { + "label": "固定", + "hint": "配送オプションの価格は固定で、注文内容に基づいて変更されません。" + }, + "calculated": { + "label": "計算式", + "hint": "配送オプションの価格は、チェックアウト時にフルフィルメントプロバイダーによって計算されます。" + } + } + }, + "enableInStore": { + "label": "ストアで有効化", + "hint": "顧客がチェックアウト時にこのオプションを使用できるかどうか。" + }, + "provider": "フルフィルメントプロバイダー", + "profile": "配送プロファイル" + } + }, + "serviceZones": { + "create": { + "headerPickup": "{{location}}からピックアップのサービスゾーンを作成", + "headerShipping": "{{location}}から配送のサービスゾーンを作成", + "action": "サービスゾーンを作成", + "successToast": "サービスゾーン「{{name}}」が正常に作成されました。" + }, + "edit": { + "header": "サービスゾーンを編集", + "successToast": "サービスゾーン「{{name}}」が正常に更新されました。" + }, + "delete": { + "confirmation": "サービスゾーン「{{name}}」を削除しようとしています。この操作は取り消せません。", + "successToast": "サービスゾーン「{{name}}」が正常に削除されました。" + }, + "manageAreas": { + "header": "{{name}}のエリアを管理", + "action": "エリアを管理", + "label": "エリア", + "hint": "サービスゾーンがカバーする地理的エリアを選択してください。", + "successToast": "「{{name}}」のエリアが正常に更新されました。" + }, + "fields": { + "noRecords": "配送オプションを追加するサービスゾーンがありません。", + "tip": "サービスゾーンは地理的ゾーンまたはエリアの集合です。定義された一連の拠点に利用可能な配送オプションを制限するために使用されます。" + } + } + }, + "shippingProfile": { + "domain": "配送プロファイル", + "subtitle": "類似の配送要件を持つ商品をプロファイルにグループ化します。", + "create": { + "header": "配送プロファイルを作成", + "hint": "類似の配送要件を持つ商品をグループ化するための新しい配送プロファイルを作成します。", + "successToast": "配送プロファイル「{{name}}」が正常に作成されました。" + }, + "delete": { + "title": "配送プロファイルを削除", + "description": "配送プロファイル「{{name}}」を削除しようとしています。この操作は取り消せません。", + "successToast": "配送プロファイル「{{name}}」が正常に削除されました。" + }, + "tooltip": { + "type": "配送プロファイルのタイプを入力してください。例:重い、特大、貨物専用など。" + } + }, + "taxRegions": { + "domain": "税地域", + "list": { + "hint": "顧客が異なる国や地域から購入する際の課金方法を管理します。" + }, + "delete": { + "confirmation": "税地域を削除しようとしています。この操作は取り消せません。", + "successToast": "税地域が正常に削除されました。" + }, + "create": { + "header": "税地域を作成", + "hint": "特定の国の税率を定義するための新しい税地域を作成します。", + "errors": { + "rateIsRequired": "デフォルトの税率を作成する際は税率が必要です。", + "nameIsRequired": "デフォルトの税率を作成する際は名前が必要です。" + }, + "successToast": "税地域が正常に作成されました。" + }, + "province": { + "header": "省", + "create": { + "header": "省の税地域を作成", + "hint": "特定の省の税率を定義するための新しい税地域を作成します。" + } + }, + "state": { + "header": "州", + "create": { + "header": "州の税地域を作成", + "hint": "特定の州の税率を定義するための新しい税地域を作成します。" + } + }, + "stateOrTerritory": { + "header": "州または準州", + "create": { + "header": "州/準州の税地域を作成", + "hint": "特定の州/準州の税率を定義するための新しい税地域を作成します。" + } + }, + "county": { + "header": "郡", + "create": { + "header": "郡の税地域を作成", + "hint": "特定の郡の税率を定義するための新しい税地域を作成します。" + } + }, + "region": { + "header": "地域", + "create": { + "header": "地域の税地域を作成", + "hint": "特定の地域の税率を定義するための新しい税地域を作成します。" + } + }, + "department": { + "header": "部", + "create": { + "header": "部の税地域を作成", + "hint": "特定の部の税率を定義するための新しい税地域を作成します。" + } + }, + "territory": { + "header": "準州", + "create": { + "header": "準州の税地域を作成", + "hint": "特定の準州の税率を定義するための新しい税地域を作成します。" + } + }, + "prefecture": { + "header": "都道府県", + "create": { + "header": "都道府県の税地域を作成", + "hint": "特定の都道府県の税率を定義するための新しい税地域を作成します。" + } + }, + "district": { + "header": "地区", + "create": { + "header": "地区の税地域を作成", + "hint": "特定の地区の税率を定義するための新しい税地域を作成します。" + } + }, + "governorate": { + "header": "行政区", + "create": { + "header": "行政区の税地域を作成", + "hint": "特定の行政区の税率を定義するための新しい税地域を作成します。" + } + }, + "canton": { + "header": "カントン", + "create": { + "header": "カントンの税地域を作成", + "hint": "特定のカントンの税率を定義するための新しい税地域を作成します。" + } + }, + "emirate": { + "header": "首長国", + "create": { + "header": "首長国の税地域を作成", + "hint": "特定の首長国の税率を定義するための新しい税地域を作成します。" + } + }, + "sublevel": { + "header": "サブレベル", + "create": { + "header": "サブレベルの税地域を作成", + "hint": "特定のサブレベルの税率を定義するための新しい税地域を作成します。" + } + }, + "taxOverrides": { + "header": "上書き", + "create": { + "header": "上書きを作成", + "hint": "選択した条件に対してデフォルトの税率を上書きする税率を作成します。" + }, + "edit": { + "header": "上書きを編集", + "hint": "選択した条件に対してデフォルトの税率を上書きする税率を編集します。" + } + }, + "taxRates": { + "create": { + "header": "税率を作成", + "hint": "地域の税率を定義するための新しい税率を作成します。", + "successToast": "税率が正常に作成されました。" + }, + "edit": { + "header": "税率を編集", + "hint": "地域の税率を定義するための税率を編集します。", + "successToast": "税率が正常に更新されました。" + }, + "delete": { + "confirmation": "税率「{{name}}」を削除しようとしています。この操作は取り消せません。", + "successToast": "税率が正常に削除されました。" + } + }, + "fields": { + "isCombinable": { + "label": "組み合わせ可能", + "hint": "この税率を税地域のデフォルト税率と組み合わせることができるかどうか。", + "true": "組み合わせ可能", + "false": "組み合わせ不可" + }, + "defaultTaxRate": { + "label": "デフォルト税率", + "tooltip": "この地域のデフォルト税率。例えば、国や地域の標準VAT税率など。", + "action": "デフォルト税率を作成" + }, + "taxRate": "税率", + "taxCode": "税コード", + "targets": { + "label": "対象", + "hint": "この税率が適用される対象を選択してください。", + "options": { + "product": "商品", + "productCollection": "商品コレクション", + "productTag": "商品タグ", + "productType": "商品タイプ", + "customerGroup": "顧客グループ" + }, + "operators": { + "in": "に含まれる", + "on": "に対して", + "and": "および" + }, + "placeholders": { + "product": "商品を検索", + "productCollection": "商品コレクションを検索", + "productTag": "商品タグを検索", + "productType": "商品タイプを検索", + "customerGroup": "顧客グループを検索" + }, + "tags": { + "product": "商品", + "productCollection": "商品コレクション", + "productTag": "商品タグ", + "productType": "商品タイプ", + "customerGroup": "顧客グループ" + }, + "modal": { + "header": "対象を追加" + }, + "values_one": "{{count}}個の値", + "values_other": "{{count}}個の値", + "numberOfTargets_one": "{{count}}個の対象", + "numberOfTargets_other": "{{count}}個の対象", + "additionalValues_one": "さらに{{count}}個の値", + "additionalValues_other": "さらに{{count}}個の値", + "action": "対象を追加" + }, + "sublevels": { + "labels": { + "province": "省", + "state": "州", + "region": "地域", + "stateOrTerritory": "州/準州", + "department": "部", + "county": "郡", + "territory": "準州", + "prefecture": "都道府県", + "district": "地区", + "governorate": "行政区", + "emirate": "首長国", + "canton": "カントン", + "sublevel": "サブレベルコード" + }, + "placeholders": { + "province": "省を選択", + "state": "州を選択", + "region": "地域を選択", + "stateOrTerritory": "州/準州を選択", + "department": "行政区を選択", + "county": "郡を選択", + "territory": "準州を選択", + "prefecture": "都道府県を選択", + "district": "地区を選択", + "governorate": "行政区を選択", + "emirate": "首長国を選択", + "canton": "カントンを選択" + }, + "tooltips": { + "sublevel": "サブレベルの税地域のISO 3166-2コードを入力してください。", + "notPartOfCountry": "{{province}}は{{country}}の一部ではないようです。これが正しいかどうか確認してください。" + }, + "alert": { + "header": "この税地域ではサブレベル地域が無効になっています", + "description": "この地域ではデフォルトでサブレベル地域が無効になっています。有効にすると、州、県、準州などのサブレベル地域を作成できます。", + "action": "サブレベル地域を有効にする" + } + }, + "noDefaultRate": { + "label": "デフォルト税率なし", + "tooltip": "この税地域にはデフォルトの税率がありません。国のVATなど標準税率がある場合は、この地域に追加してください。" + } + } + }, + "promotions": { + "domain": "プロモーション", + "sections": { + "details": "プロモーションの詳細" + }, + "tabs": { + "template": "タイプ", + "details": "詳細", + "campaign": "キャンペーン" + }, + "fields": { + "type": "タイプ", + "value_type": "値のタイプ", + "value": "値", + "campaign": "キャンペーン", + "method": "方法", + "allocation": "配分", + "addCondition": "条件を追加", + "clearAll": "すべてクリア", + "amount": { + "tooltip": "金額を設定するには通貨コードを選択してください" + }, + "conditions": { + "rules": { + "title": "誰がこのコードを使用できますか?", + "description": "どの顧客がプロモーションコードを使用できるか?何も変更しなければ、すべての顧客が使用できます。" + }, + "target-rules": { + "title": "プロモーションはどの商品に適用されますか?", + "description": "プロモーションは、次の条件に一致する商品に適用されます。" + }, + "buy-rules": { + "title": "プロモーションを有効にするためにカートに必要なものは?", + "description": "これらの条件が一致した場合、ターゲット商品に対してプロモーションが有効になります。" + } + } + }, + "tooltips": { + "campaignType": "支出予算を設定するには、プロモーションで通貨コードを選択する必要があります。" + }, + "errors": { + "requiredField": "必須項目です", + "promotionTabError": "進む前にプロモーションタブのエラーを修正してください" + }, + "toasts": { + "promotionCreateSuccess": "プロモーション「{{code}}」が正常に作成されました。" + }, + "create": {}, + "edit": { + "title": "プロモーションの詳細を編集", + "rules": { + "title": "使用条件を編集" + }, + "target-rules": { + "title": "商品条件を編集" + }, + "buy-rules": { + "title": "購入条件を編集" + } + }, + "campaign": { + "header": "キャンペーン", + "edit": { + "header": "キャンペーンを編集", + "successToast": "プロモーションのキャンペーンが正常に更新されました。" + }, + "actions": { + "goToCampaign": "キャンペーンに移動" + } + }, + "campaign_currency": { + "tooltip": "これはプロモーションの通貨です。詳細タブから変更してください。" + }, + "form": { + "required": "必須", + "and": "と", + "selectAttribute": "属性を選択", + "campaign": { + "existing": { + "title": "既存のキャンペーン", + "description": "既存のキャンペーンにプロモーションを追加します。", + "placeholder": { + "title": "既存のキャンペーンはありません", + "desc": "複数のプロモーションを追跡し、予算制限を設定するために作成できます。" + } + }, + "new": { + "title": "新しいキャンペーン", + "description": "このプロモーションのために新しいキャンペーンを作成します。" + }, + "none": { + "title": "キャンペーンなし", + "description": "プロモーションをキャンペーンに関連付けずに進めます。" + } + }, + "status": { + "title": "ステータス" + }, + "method": { + "label": "方法", + "code": { + "title": "プロモーションコード", + "description": "顧客はチェックアウト時にこのコードを入力する必要があります。" + }, + "automatic": { + "title": "自動", + "description": "顧客はチェックアウト時にこのプロモーションを見ることができます。" + } + }, + "max_quantity": { + "title": "最大数量", + "description": "このプロモーションが適用される商品の最大数量。" + }, + "type": { + "standard": { + "title": "標準", + "description": "標準的なプロモーション" + }, + "buyget": { + "title": "購入して取得", + "description": "Xを購入してYを取得するプロモーション" + } + }, + "allocation": { + "each": { + "title": "各々", + "description": "各アイテムに値を適用します。" + }, + "across": { + "title": "全体", + "description": "アイテム全体に値を適用します。" + } + }, + "code": { + "title": "コード", + "description": "顧客がチェックアウト時に入力するコードです。" + }, + "value": { + "title": "プロモーション値" + }, + "value_type": { + "fixed": { + "title": "プロモーション値", + "description": "割引される金額。例:100" + }, + "percentage": { + "title": "プロモーション値", + "description": "金額から割引されるパーセンテージ。例:8%" + } + } + }, + "deleteWarning": "プロモーション「{{code}}」を削除しようとしています。この操作は取り消せません。", + "createPromotionTitle": "プロモーションを作成", + "type": "プロモーションタイプ", + "conditions": { + "add": "条件を追加", + "list": { + "noRecordsMessage": "プロモーションが適用されるアイテムを制限する条件を追加してください。" + } + } + }, + "campaigns": { + "domain": "キャンペーン", + "details": "キャンペーン詳細", + "status": { + "active": "アクティブ", + "expired": "期限切れ", + "scheduled": "スケジュール済み" + }, + "delete": { + "title": "本当ですか?", + "description": "キャンペーン「{{name}}」を削除しようとしています。この操作は取り消せません。", + "successToast": "キャンペーン「{{name}}」が正常に作成されました。" + }, + "edit": { + "header": "キャンペーンを編集", + "description": "キャンペーンの詳細を編集します。", + "successToast": "キャンペーン「{{name}}」が正常に更新されました。" + }, + "configuration": { + "header": "設定", + "edit": { + "header": "キャンペーン設定を編集", + "description": "キャンペーンの設定を編集します。", + "successToast": "キャンペーン設定が正常に更新されました。" + } + }, + "create": { + "title": "キャンペーンを作成", + "description": "プロモーションキャンペーンを作成します。", + "hint": "プロモーションキャンペーンを作成します。", + "header": "キャンペーンを作成", + "successToast": "キャンペーン「{{name}}」が正常に作成されました。" + }, + "fields": { + "name": "名前", + "identifier": "識別子", + "start_date": "開始日", + "end_date": "終了日", + "total_spend": "予算支出", + "total_used": "使用済み予算", + "budget_limit": "予算制限", + "campaign_id": { + "hint": "プロモーションと同じ通貨コードのキャンペーンのみがこのリストに表示されます。" + } + }, + "budget": { + "create": { + "hint": "キャンペーンの予算を作成します。", + "header": "キャンペーン予算" + }, + "details": "キャンペーン予算", + "fields": { + "type": "タイプ", + "currency": "通貨", + "limit": "制限", + "used": "使用済み" + }, + "type": { + "spend": { + "title": "支出", + "description": "すべてのプロモーション使用の合計割引額に制限を設定します。" + }, + "usage": { + "title": "使用", + "description": "プロモーションが使用できる回数に制限を設定します。" + } + }, + "edit": { + "header": "キャンペーン予算を編集" + } + }, + "promotions": { + "remove": { + "title": "キャンペーンからプロモーションを削除", + "description": "キャンペーンから{{count}}個のプロモーションを削除しようとしています。この操作は取り消せません。" + }, + "alreadyAdded": "このプロモーションはすでにキャンペーンに追加されています。", + "alreadyAddedDiffCampaign": "このプロモーションは別のキャンペーン({{name}})にすでに追加されています。", + "currencyMismatch": "プロモーションとキャンペーンの通貨が一致しません。", + "toast": { + "success": "{{count}}個のプロモーションが正常にキャンペーンに追加されました。" + }, + "add": { + "list": { + "noRecordsMessage": "最初にプロモーションを作成してください。" + } + }, + "list": { + "noRecordsMessage": "このキャンペーンにはプロモーションがありません。" + } + }, + "deleteCampaignWarning": "キャンペーン「{{name}}」を削除しようとしています。この操作は取り消せません。", + "totalSpend": "<0>{{amount}} <1>{{currency}}" + }, + "priceLists": { + "domain": "価格リスト", + "subtitle": "特定の条件に対して販売価格を作成または上書きします。", + "delete": { + "confirmation": "価格リスト「{{title}}」を削除しようとしています。この操作は取り消せません。", + "successToast": "価格リスト「{{title}}」が正常に削除されました。" + }, + "create": { + "header": "価格リストを作成", + "subheader": "商品の価格を管理するための新しい価格リストを作成します。", + "tabs": { + "details": "詳細", + "products": "商品", + "prices": "価格" + }, + "successToast": "価格リスト「{{title}}」が正常に作成されました。", + "products": { + "list": { + "noRecordsMessage": "最初に商品を作成してください。" + } + } + }, + "edit": { + "header": "価格リストを編集", + "successToast": "価格リスト「{{title}}」が正常に更新されました。" + }, + "configuration": { + "header": "設定", + "edit": { + "header": "価格リストの設定を編集", + "description": "価格リストの設定を編集します。", + "successToast": "価格リストの設定が正常に更新されました。" + } + }, + "products": { + "header": "商品", + "actions": { + "addProducts": "商品を追加", + "editPrices": "価格を編集" + }, + "delete": { + "confirmation_one": "{{count}}個の商品に対する価格をこの価格リストから削除しようとしています。この操作は取り消せません。", + "confirmation_other": "{{count}}個の商品に対する価格をこの価格リストから削除しようとしています。この操作は取り消せません。", + "successToast_one": "{{count}}個の商品に対する価格が正常に削除されました。", + "successToast_other": "{{count}}個の商品に対する価格が正常に削除されました。" + }, + "add": { + "successToast": "価格が正常に価格リストに追加されました。" + }, + "edit": { + "successToast": "価格が正常に更新されました。" + } + }, + "fields": { + "priceOverrides": { + "label": "価格の上書き", + "header": "価格の上書き" + }, + "status": { + "label": "ステータス", + "options": { + "active": "アクティブ", + "draft": "ドラフト", + "expired": "期限切れ", + "scheduled": "スケジュール済み" + } + }, + "type": { + "label": "タイプ", + "hint": "作成したい価格リストのタイプを選択します。", + "options": { + "sale": { + "label": "セール", + "description": "セール価格は商品の一時的な価格変更です。" + }, + "override": { + "label": "上書き", + "description": "上書きは通常、顧客特有の価格を作成するために使用されます。" + } + } + }, + "startsAt": { + "label": "価格リストには開始日がありますか?", + "hint": "将来有効になるように、価格リストをスケジュールします。" + }, + "endsAt": { + "label": "価格リストには終了日がありますか?", + "hint": "将来無効になるように、価格リストをスケジュールします。" + }, + "customerAvailability": { + "header": "顧客グループを選択", + "label": "顧客の可用性", + "hint": "この価格リストが適用される顧客グループを選択します。", + "placeholder": "顧客グループを検索", + "attribute": "顧客グループ" + } + } + }, + "profile": { + "domain": "プロフィール", + "manageYourProfileDetails": "プロフィールの詳細を管理します。", + "fields": { + "languageLabel": "言語", + "usageInsightsLabel": "使用状況のインサイト" + }, + "edit": { + "header": "プロフィールを編集", + "languageHint": "管理ダッシュボードで使用する言語です。これにより、ストアの言語は変更されません。", + "languagePlaceholder": "言語を選択", + "usageInsightsHint": "使用状況のインサイトを共有し、Medusaの改善にご協力ください。収集内容やその使用方法については、<0>ドキュメントをご覧ください。" + }, + "toast": { + "edit": "プロフィールの変更が保存されました" + } + }, + "users": { + "domain": "ユーザー", + "editUser": "ユーザーを編集", + "inviteUser": "ユーザーを招待", + "inviteUserHint": "新しいユーザーをストアに招待します。", + "sendInvite": "招待を送信", + "pendingInvites": "保留中の招待", + "deleteInviteWarning": "{{email}}への招待を削除しようとしています 。この操作は取り消せません。", + "resendInvite": "招待を再送信", + "copyInviteLink": "招待リンクをコピー", + "expiredOnDate": "{{date}}に期限切れ", + "validFromUntil": "<0>{{from}} - <1>{{until}} の間有効", + "acceptedOnDate": "{{date}}に承認されました", + "inviteStatus": { + "accepted": "承認済み", + "pending": "保留中", + "expired": "期限切れ" + }, + "roles": { + "admin": "管理者", + "developer": "開発者", + "member": "メンバー" + }, + "deleteUserWarning": "{{name}}を削除しようとしています。この操作は取り消せません。", + "invite": "招待" + }, + "store": { + "domain": "ストア", + "manageYourStoresDetails": "ストアの詳細を管理します。", + "editStore": "ストアを編集", + "defaultCurrency": "デフォルト通貨", + "defaultRegion": "デフォルト地域", + "swapLinkTemplate": "リンクテンプレートを交換", + "paymentLinkTemplate": "支払いリンクテンプレート", + "inviteLinkTemplate": "招待リンクテンプレート", + "currencies": "通貨", + "addCurrencies": "通貨を追加", + "enableTaxInclusivePricing": "税込価格を有効にする", + "disableTaxInclusivePricing": "税込価格を無効にする", + "removeCurrencyWarning_one": "{{count}}個の通貨をストアから削除しようとしています。進む前に、その通貨を使用しているすべての価格が削除されていることを確認してください。", + "removeCurrencyWarning_other": "{{count}}個の通貨をストアから削除しようとしています。進む前に、それらの通貨を使用しているすべての価格が削除されていることを確認してください。", + "currencyAlreadyAdded": "この通貨はすでにストアに追加されています。", + "edit": { + "header": "ストアを編集" + }, + "toast": { + "update": "ストアが正常に更新されました。", + "currenciesUpdated": "通貨が正常に更新されました。", + "currenciesRemoved": "ストアから通貨が正常に削除されました。", + "updatedTaxInclusivitySuccessfully": "税込価格が正常に更新されました。" + } + }, + "regions": { + "domain": "地域", + "subtitle": "地域は、商品を販売するエリアです。複数の国をカバーでき、異なる税率、プロバイダー、通貨を持ちます。", + "createRegion": "地域を作成", + "createRegionHint": "一連の国の税率とプロバイダーを管理します。", + "addCountries": "国を追加", + "editRegion": "地域を編集", + "countriesHint": "この地域に含まれる国を追加します。", + "deleteRegionWarning": "地域「{{name}}」を削除しようとしています。この操作は取り消せません。", + "removeCountriesWarning_one": "{{count}}か国を地域から削除しようとしています。この操作は取り消せません。", + "removeCountriesWarning_other": "{{count}}か国を地域から削除しようとしています。この操作は取り消せません。", + "removeCountryWarning": "国「{{name}}」を地域から削除しようとしています。この操作は取り消せません。", + "automaticTaxesHint": "有効にすると、税金は配送先住所に基づいてチェックアウト時にのみ計算されます。", + "taxInclusiveHint": "有効にすると、この地域の価格は税込みになります。", + "providersHint": "この地域で利用可能な決済プロバイダーを追加します。", + "shippingOptions": "配送オプション", + "deleteShippingOptionWarning": "配送オプション「{{name}}」を削除しようとしています。この操作は取り消せません。", + "return": "返品", + "outbound": "発送", + "priceType": "価格タイプ", + "flatRate": "定額", + "calculated": "計算式", + "list": { + "noRecordsMessage": "販売地域の地域を作成してください。" + }, + "toast": { + "delete": "地域が正常に削除されました", + "edit": "地域の編集が保存されました", + "create": "地域が正常に作成されました", + "countries": "地域の国が正常に更新されました" + }, + "shippingOption": { + "createShippingOption": "配送オプションを作成", + "createShippingOptionHint": "地域の新しい配送オプションを作成します。", + "editShippingOption": "配送オプションを編集", + "fulfillmentMethod": "出荷方法", + "type": { + "outbound": "発送", + "outboundHint": "顧客に商品を送る配送オプションを作成する場合に使用します。", + "return": "返品", + "returnHint": "顧客が商品を返品するための配送オプションを作成する場合に使用します。" + }, + "priceType": { + "label": "価格タイプ", + "flatRate": "定額", + "calculated": "計算式" + }, + "availability": { + "adminOnly": "管理者のみ", + "adminOnlyHint": "有効にすると、配送オプションは管理ダッシュボードでのみ利用可能になり、ストアフロントでは利用できません。" + }, + "taxInclusiveHint": "有効にすると、配送オプションの価格は税込みになります。", + "requirements": { + "label": "要件", + "hint": "配送オプションの要件を指定します。" + } + } + }, + "taxes": { + "domain": "税地域", + "domainDescription": "税地域を管理する", + "countries": { + "taxCountriesHint": "税設定は表示されている国に適用されます。" + }, + "settings": { + "editTaxSettings": "税設定を編集", + "taxProviderLabel": "税プロバイダー", + "systemTaxProviderLabel": "システム税プロバイダー", + "calculateTaxesAutomaticallyLabel": "税金を自動計算する", + "calculateTaxesAutomaticallyHint": "有効にすると、税率が自動的に計算され、カートに適用されます。無効の場合、税金はチェックアウト時に手動で計算する必要があります。サードパーティの税プロバイダーを使用する場合は、手動での税金計算が推奨されます。", + "applyTaxesOnGiftCardsLabel": "ギフトカードに税金を適用する", + "applyTaxesOnGiftCardsHint": "有効にすると、チェックアウト時にギフトカードに税金が適用されます。一部の国では、税法により購入時にギフトカードへの課税が必要とされています。", + "defaultTaxRateLabel": "デフォルト税率", + "defaultTaxCodeLabel": "デフォルト税コード" + }, + "defaultRate": { + "sectionTitle": "デフォルト税率" + }, + "taxRate": { + "sectionTitle": "税率", + "createTaxRate": "税率を作成", + "createTaxRateHint": "地域の新しい税率を作成します。", + "deleteRateDescription": "税率「{{name}}」を削除しようとしています。この操作は取り消せません。", + "editTaxRate": "税率を編集", + "editRateAction": "税率を編集", + "editOverridesAction": "上書きを編集", + "editOverridesTitle": "税率の上書きを編集", + "editOverridesHint": "税率の上書きを指定します。", + "deleteTaxRateWarning": "税率「{{name}}」を削除しようとしています。この操作は取り消せません。", + "productOverridesLabel": "商品の上書き", + "productOverridesHint": "税率の商品上書きを指定します。", + "addProductOverridesAction": "商品の上書きを追加", + "productTypeOverridesLabel": "商品タイプの上書き", + "productTypeOverridesHint": "税率の商品タイプ上書きを指定します。", + "addProductTypeOverridesAction": "商品タイプの上書きを追加", + "shippingOptionOverridesLabel": "配送オプションの上書き", + "shippingOptionOverridesHint": "税率の配送オプション上書きを指定します。", + "addShippingOptionOverridesAction": "配送オプションの上書きを追加", + "productOverridesHeader": "商品", + "productTypeOverridesHeader": "商品タイプ", + "shippingOptionOverridesHeader": "配送オプション" + } + }, + "locations": { + "domain": "拠点", + "editLocation": "拠点を編集", + "addSalesChannels": "販売チャネルを追加", + "noLocationsFound": "拠点が見つかりません", + "selectLocations": "商品を在庫する拠点を選択してください。", + "deleteLocationWarning": "拠点「{{name}}」を削除しようとしています。この操作は取り消せません。", + "removeSalesChannelsWarning_one": "{{count}}個の販売チャネルを拠点から削除しようとしています。", + "removeSalesChannelsWarning_other": "{{count}}個の販売チャネルを拠点から削除しようとしています。", + "toast": { + "create": "拠点が正常に作成されました", + "update": "拠点が正常に更新されました", + "removeChannel": "販売チャネルが正常に削除されました" + } + }, + "reservations": { + "domain": "予約", + "subtitle": "在庫アイテムの予約数量を管理します。", + "deleteWarning": "予約を削除しようとしています。この操作は取り消せません。" + }, + "salesChannels": { + "domain": "販売チャネル", + "subtitle": "商品を販売するオンラインおよびオフラインのチャネルを管理します。", + "createSalesChannel": "販売チャネルを作成", + "createSalesChannelHint": "商品を販売するための新しい販売チャネルを作成します。", + "enabledHint": "販売チャネルが有効かどうかを指定します。", + "removeProductsWarning_one": "{{sales_channel}}から{{count}}個の商品を削除しようとしています。", + "removeProductsWarning_other": "{{sales_channel}}から{{count}}個の商品を削除しようとしています。", + "addProducts": "商品を追加", + "editSalesChannel": "販売チャネルを編集", + "productAlreadyAdded": "この商品はすでに販売チャネルに追加されています。", + "deleteSalesChannelWarning": "販売チャネル{{name}}を削除しようとしています。この操作は取り消せません。", + "toast": { + "create": "販売チャネルが正常に作成されました", + "update": "販売チャネルが正常に更新されました", + "delete": "販売チャネルが正常に削除されました" + }, + "tooltip": { + "cannotDeleteDefault": "デフォルトの販売チャネルは削除できません" + }, + "products": { + "list": { + "noRecordsMessage": "この販売チャネルには商品がありません。" + }, + "add": { + "list": { + "noRecordsMessage": "最初に商品を作成してください。" + } + } + } + }, + "apiKeyManagement": { + "domain": { + "publishable": "公開可能なAPIキー", + "secret": "秘密のAPIキー" + }, + "subtitle": { + "publishable": "ストアフロントで使用されるAPIキーを管理し、特定の販売チャネルへのリクエストの範囲を制限します。", + "secret": "管理者アプリケーションで管理者ユーザーを認証するために使用されるAPIキーを管理します。" + }, + "status": { + "active": "アクティブ", + "revoked": "無効" + }, + "type": { + "publishable": "公開可能", + "secret": "秘密" + }, + "create": { + "createPublishableHeader": "公開可能なAPIキーを作成", + "createPublishableHint": "特定の販売チャネルへのリクエストの範囲を制限するための新しい公開可能なAPIキーを作成します。", + "createSecretHeader": "秘密のAPIキーを作成", + "createSecretHint": "認証された管理者ユーザーとしてMedusa APIにアクセスするための新しい秘密のAPIキーを作成します。", + "secretKeyCreatedHeader": "秘密のキーが作成されました", + "secretKeyCreatedHint": "新しい秘密のキーが生成されました。今すぐコピーして安全に保管してください。これが表示される唯一の機会です。", + "copySecretTokenSuccess": "秘密のキーがクリップボードにコピーされました。", + "copySecretTokenFailure": "秘密のキーをクリップボードにコピーできませんでした。", + "successToast": "APIキーが正常に作成されました。" + }, + "edit": { + "header": "APIキーを編集", + "description": "APIキーのタイトルを編集します。", + "successToast": "APIキー「{{title}}」が正常に更新されました。" + }, + "salesChannels": { + "title": "販売チャネルを追加", + "description": "APIキーが制限される販売チャネルを追加します。", + "successToast_one": "{{count}}個の販売チャネルがAPIキーに正常に追加されました。", + "successToast_other": "{{count}}個の販売チャネルがAPIキーに正常に追加されました。", + "alreadyAddedTooltip": "この販売チャネルはすでにAPIキーに追加されています。", + "list": { + "noRecordsMessage": "公開可能なAPIキーの範囲内に販売チャネルがありません。" + } + }, + "delete": { + "warning": "APIキー「{{title}}」を削除しようとしています。この操作は取り消せません。", + "successToast": "APIキー「{{title}}」が正常に削除されました。" + }, + "revoke": { + "warning": "APIキー「{{title}}」を無効化しようとしています。この操作は取り消せません。", + "successToast": "APIキー「{{title}}」が正常に無効化されました。" + }, + "addSalesChannels": { + "list": { + "noRecordsMessage": "最初に販売チャネルを作成してください。" + } + }, + "removeSalesChannel": { + "warning": "販売チャネル「{{name}}」をAPIキーから削除しようとしています。この操作は取り消せません。", + "warningBatch_one": "{{count}}個の販売チャネルをAPIキーから削除しようとしています。この操作は取り消せません。", + "warningBatch_other": "{{count}}個の販売チャネルをAPIキーから削除しようとしています。この操作は取り消せません。", + "successToast": "販売チャネルがAPIキーから正常に削除されました。", + "successToastBatch_one": "{{count}}個の販売チャネルがAPIキーから正常に削除されました。", + "successToastBatch_other": "{{count}}個の販売チャネルがAPIキーから正常に削除されました。" + }, + "actions": { + "revoke": "APIキーを無効化", + "copy": "APIキーをコピー", + "copySuccessToast": "APIキーがクリップボードにコピーされました。" + }, + "table": { + "lastUsedAtHeader": "最終使用日時", + "createdAtHeader": "無効化日時" + }, + "fields": { + "lastUsedAtLabel": "最終使用日時", + "revokedByLabel": "無効化者", + "revokedAtLabel": "無効化日時", + "createdByLabel": "作成者" + } + }, + "returnReasons": { + "domain": "返品理由", + "subtitle": "返品された商品の理由を管理します。", + "calloutHint": "返品を分類するための理由を管理します。", + "editReason": "返品理由を編集", + "create": { + "header": "返品理由を追加", + "subtitle": "最も一般的な返品理由を指定します。", + "hint": "返品を分類するための新しい返品理由を作成します。", + "successToast": "返品理由「{{label}}」が正常に作成されました。" + }, + "edit": { + "header": "返品理由を編集", + "subtitle": "返品理由の値を編集します。", + "successToast": "返品理由「{{label}}」が正常に更新されました。" + }, + "delete": { + "confirmation": "返品理由「{{label}}」を削除しようとしています。この操作は取り消せません。", + "successToast": "返品理由「{{label}}」が正常に削除されました。" + }, + "fields": { + "value": { + "label": "値", + "placeholder": "wrong_size", + "tooltip": "値は返品理由の一意の識別子である必要があります。" + }, + "label": { "label": "ラベル", "placeholder": "サイズが合わない" }, + "description": { + "label": "説明", + "placeholder": "顧客が間違ったサイズを受け取った" + } + } + }, + "login": { + "forgotPassword": "パスワードをお忘れですか? - <0>リセット", + "title": "Medusaへようこそ", + "hint": "アカウントエリアにアクセスするにはサインインしてください" + }, + "invite": { + "title": "Medusaへようこそ", + "hint": "以下でアカウントを作成してください", + "backToLogin": "ログインに戻る", + "createAccount": "アカウントを作成", + "alreadyHaveAccount": "すでにアカウントをお持ちですか? - <0>ログイン", + "emailTooltip": "Eメールは変更できません。別のEメールを使用したい場合は、新しい招待を送信する必要があります。", + "invalidInvite": "招待が無効であるか、期限が切れています。", + "successTitle": "アカウントが登録されました", + "successHint": "Medusa Adminをすぐに始めましょう。", + "successAction": "Medusa Adminを開始", + "invalidTokenTitle": "招待トークンが無効です", + "invalidTokenHint": "新しい招待リンクをリクエストしてみてください。", + "passwordMismatch": "パスワードが一致しません", + "toast": { + "accepted": "招待が正常に受け入れられました" + } + }, + "resetPassword": { + "title": "パスワードをリセット", + "hint": "以下にEメールを入力してください。パスワードのリセット方法についての説明をお送りします。", + "email": "Eメール", + "sendResetInstructions": "リセット手順を送信", + "backToLogin": "<0>ログインに戻る", + "newPasswordHint": "以下に新しいパスワードを選択してください。", + "invalidTokenTitle": "リセットトークンが無効です", + "invalidTokenHint": "新しいリセットリンクをリクエストしてみてください。", + "expiredTokenTitle": "リセットトークンの有効期限が切れています", + "goToResetPassword": "パスワードリセットページへ", + "resetPassword": "パスワードをリセット", + "newPassword": "新しいパスワード", + "repeatNewPassword": "新しいパスワードを再入力", + "tokenExpiresIn": "トークンの有効期限は残り<0>{{time}}分です", + "successfulRequestTitle": "Eメールを正常に送信しました", + "successfulRequest": "パスワードをリセットするためのEメールを送信しました。数分経っても受信していない場合は、迷惑フォルダをご確認ください。", + "successfulResetTitle": "パスワードのリセットに成功しました", + "successfulReset": "ログインページからログインしてください。", + "passwordMismatch": "パスワードが一致しません", + "invalidLinkTitle": "リセットリンクが無効です", + "invalidLinkHint": "もう一度パスワードのリセットを試みてください。" + }, + "workflowExecutions": { + "domain": "ワークフロー", + "subtitle": "Medusaアプリケーションでのワークフロー実行を表示し、追跡します。", + "transactionIdLabel": "トランザクションID", + "workflowIdLabel": "ワークフローID", + "progressLabel": "進捗", + "stepsCompletedLabel_one": "{{count}}ステップ中{{completed}}ステップ完了", + "stepsCompletedLabel_other": "{{count}}ステップ中{{completed}}ステップ完了", + "list": { + "noRecordsMessage": "まだワークフローが実行されていません。" + }, + "history": { + "sectionTitle": "履歴", + "runningState": "実行中...", + "awaitingState": "待機中", + "failedState": "失敗", + "skippedState": "スキップ", + "skippedFailureState": "スキップ(失敗)", + "definitionLabel": "定義", + "outputLabel": "出力", + "compensateInputLabel": "補償処理入力", + "revertedLabel": "復元済み", + "errorLabel": "エラー" + }, + "state": { + "done": "完了", + "failed": "失敗", + "reverted": "復元済み", + "invoking": "呼び出し中", + "compensating": "補償処理中", + "notStarted": "未開始" + }, + "transaction": { + "state": { + "waitingToCompensate": "補償処理待ち" + } + }, + "step": { + "state": { + "skipped": "スキップ", + "skippedFailure": "スキップ(失敗)", + "dormant": "休止中", + "timeout": "タイムアウト" + } + } + }, + "productTypes": { + "domain": "商品タイプ", + "subtitle": "商品をタイプ別に整理します。", + "create": { + "header": "商品タイプを作成", + "hint": "商品を分類するための新しい商品タイプを作成します。", + "successToast": "商品タイプ「{{value}}」が正常に作成されました。" + }, + "edit": { + "header": "商品タイプを編集", + "successToast": "商品タイプ「{{value}}」が正常に更新されました。" + }, + "delete": { + "confirmation": "商品タイプ「{{value}}」を削除しようとしています。この操作は取り消せません。", + "successToast": "商品タイプ「{{value}}」が正常に削除されました。" + }, + "fields": { + "value": "値" + } + }, + "productTags": { + "domain": "商品タグ", + "create": { + "header": "商品タグを作成", + "subtitle": "商品を分類するための新しい商品タグを作成します。", + "successToast": "商品タグ「{{value}}」が正常に作成されました。" + }, + "edit": { + "header": "商品タグを編集", + "subtitle": "商品タグの値を編集します。", + "successToast": "商品タグ「{{value}}」が正常に更新されました。" + }, + "delete": { + "confirmation": "商品タグ「{{value}}」を削除しようとしています。この操作は取り消せません。", + "successToast": "商品タグ「{{value}}」が正常に削除されました。" + }, + "fields": { + "value": "値" + } + }, + "notifications": { + "domain": "通知", + "emptyState": { + "title": "通知はありません", + "description": "現在通知はありませんが、通知が届くとここに表示されます。" + }, + "accessibility": { + "description": "Medusaの活動に関する通知がここに表示されます。" + } + }, + "errors": { + "serverError": "サーバーエラー - 後でもう一度お試しください。", + "invalidCredentials": "Eメールまたはパスワードが間違っています" + }, + "statuses": { + "scheduled": "スケジュール済み", + "expired": "期限切れ", + "active": "アクティブ", + "enabled": "有効", + "disabled": "無効" + }, + "labels": { + "productVariant": "商品バリエーション", + "prices": "価格", + "available": "利用可能", + "inStock": "在庫あり", + "added": "追加済み", + "removed": "削除済み", + "from": "から", + "to": "へ" + }, + "fields": { + "amount": "金額", + "refundAmount": "返金額", + "name": "名前", + "default": "デフォルト", + "lastName": "姓", + "firstName": "名", + "title": "タイトル", + "customTitle": "カスタムタイトル", + "manageInventory": "在庫管理", + "inventoryKit": "在庫キットあり", + "inventoryItems": "在庫アイテム", + "inventoryItem": "在庫アイテム", + "requiredQuantity": "必要数量", + "description": "説明", + "email": "Eメール", + "password": "パスワード", + "repeatPassword": "パスワードを再入力", + "confirmPassword": "パスワードを確認", + "newPassword": "新しいパスワード", + "repeatNewPassword": "新しいパスワードを再入力", + "categories": "カテゴリー", + "shippingMethod": "配送方法", + "configurations": "設定", + "conditions": "条件", + "category": "カテゴリー", + "collection": "コレクション", + "discountable": "割引可能", + "handle": "ハンドル", + "subtitle": "サブタイトル", + "by": "作成者", + "item": "アイテム", + "qty": "数量", + "limit": "制限", + "tags": "タグ", + "type": "タイプ", + "reason": "理由", + "none": "なし", + "all": "すべて", + "search": "検索", + "percentage": "割合", + "sales_channels": "販売チャネル", + "customer_groups": "顧客グループ", + "product_tags": "商品タグ", + "product_types": "商品タイプ", + "product_collections": "商品コレクション", + "status": "ステータス", + "code": "コード", + "value": "値", + "disabled": "無効", + "dynamic": "動的", + "normal": "通常", + "years": "年", + "months": "月", + "days": "日", + "hours": "時間", + "minutes": "分", + "totalRedemptions": "総償還数", + "countries": "国", + "paymentProviders": "決済プロバイダー", + "refundReason": "返金理由", + "fulfillmentProviders": "フルフィルメントプロバイダー", + "fulfillmentProvider": "フルフィルメントプロバイダー", + "providers": "プロバイダー", + "availability": "利用可能性", + "inventory": "在庫", + "optional": "任意", + "note": "メモ", + "automaticTaxes": "自動税計算", + "taxInclusivePricing": "税込価格", + "currency": "通貨", + "address": "住所", + "address2": "アパート、部屋番号など", + "city": "市区町村", + "postalCode": "郵便番号", + "country": "国", + "state": "州", + "province": "都道府県", + "company": "会社", + "phone": "電話番号", + "metadata": "メタデータ", + "selectCountry": "国を選択", + "products": "商品", + "variants": "バリエーション", + "orders": "注文", + "account": "アカウント", + "total": "注文合計", + "paidTotal": "支払済み合計", + "totalExclTax": "税抜合計", + "subtotal": "小計", + "shipping": "配送", + "outboundShipping": "発送配送", + "returnShipping": "返品配送", + "tax": "税金", + "created": "作成日", + "key": "キー", + "customer": "顧客", + "date": "日付", + "order": "注文", + "fulfillment": "フルフィルメント", + "provider": "プロバイダー", + "payment": "支払い", + "items": "アイテム", + "salesChannel": "販売チャネル", + "region": "地域", + "discount": "割引", + "role": "役割", + "sent": "送信済み", + "salesChannels": "販売チャネル", + "product": "商品", + "createdAt": "作成日時", + "updatedAt": "更新日時", + "revokedAt": "無効化日時", + "true": "True", + "false": "False", + "giftCard": "ギフトカード", + "tag": "タグ", + "dateIssued": "発行日", + "issuedDate": "発行日", + "expiryDate": "有効期限", + "price": "価格", + "priceTemplate": "価格 {{regionOrCurrency}}", + "height": "高さ", + "width": "幅", + "length": "長さ", + "weight": "重量", + "midCode": "MIDコード", + "hsCode": "HSコード", + "ean": "EAN", + "upc": "UPC", + "inventoryQuantity": "在庫数量", + "barcode": "バーコード", + "countryOfOrigin": "原産国", + "material": "素材", + "thumbnail": "サムネイル", + "sku": "SKU", + "managedInventory": "管理在庫", + "allowBackorder": "バックオーダー許可", + "inStock": "在庫あり", + "location": "拠点", + "quantity": "数量", + "variant": "バリエーション", + "id": "ID", + "parent": "親", + "minSubtotal": "最小小計", + "maxSubtotal": "最大小計", + "shippingProfile": "配送プロファイル", + "summary": "概要", + "details": "詳細", + "label": "ラベル", + "rate": "料率", + "requiresShipping": "配送必要", + "unitPrice": "単価", + "startDate": "開始日", + "endDate": "終了日", + "draft": "下書き", + "values": "値" + }, + "dateTime": { + "years_one": "年", + "years_other": "年", + "months_one": "月", + "months_other": "月", + "weeks_one": "週", + "weeks_other": "週", + "days_one": "日", + "days_other": "日", + "hours_one": "時", + "hours_other": "時", + "minutes_one": "分", + "minutes_other": "分", + "seconds_one": "秒", + "seconds_other": "秒" + } +} From 864f53011b892e1ed0abee2e241b662eccef7e6d Mon Sep 17 00:00:00 2001 From: Kasper Fabricius Kristensen <45367945+kasperkristensen@users.noreply.github.com> Date: Sun, 8 Dec 2024 12:51:13 +0100 Subject: [PATCH 06/11] fix(dashboard,types): Fix TS errors (#10457) **What** - Fixes TS erros in dashboard project - Updates incorrect HTTP Invite types - Fixes incorrectly formatted dates in dashboard --- .changeset/stale-eyes-float.md | 6 ++ .../common/action-menu/action-menu.tsx | 20 ++-- .../common/button-menu/button-menu.tsx | 96 ------------------- .../components/common/button-menu/index.ts | 1 - .../src/components/common/date/index.ts | 11 --- .../forms/address-form/address-form.tsx | 28 ++++-- .../forms/metadata-form/metadata-form.tsx | 3 +- .../created-at-cell/created-at-cell.tsx | 22 ++--- .../order/customer-cell/customer-cell.tsx | 2 +- .../payment-status-cell.tsx | 3 +- .../sales-channel-cell/sales-channel-cell.tsx | 2 +- .../region/countries-cell/countries-cell.tsx | 20 ++-- .../dashboard-extension-provider/types.ts | 32 ------- .../admin/dashboard/src/hooks/api/auth.tsx | 9 +- .../admin/dashboard/src/hooks/api/claims.tsx | 5 +- .../dashboard/src/hooks/api/collections.tsx | 8 +- .../admin/dashboard/src/hooks/api/invites.tsx | 26 ++--- .../dashboard/src/hooks/api/price-lists.tsx | 2 +- .../table/columns/use-order-table-columns.tsx | 2 +- packages/admin/dashboard/src/index.ts | 1 - .../order-edit-items-section.tsx | 19 ++-- .../order-summary-section.tsx | 5 +- .../return-info-popover.tsx | 15 ++- .../src/routes/tax-regions/common/hooks.ts | 12 +-- .../edit-user-form/edit-user-form.tsx | 4 +- .../invite-user-form/invite-user-form.tsx | 8 +- .../users/user-metadata/user-metadata.tsx | 4 +- .../types/src/http/invite/admin/entities.ts | 6 +- 28 files changed, 120 insertions(+), 252 deletions(-) create mode 100644 .changeset/stale-eyes-float.md delete mode 100644 packages/admin/dashboard/src/components/common/button-menu/button-menu.tsx delete mode 100644 packages/admin/dashboard/src/components/common/button-menu/index.ts delete mode 100644 packages/admin/dashboard/src/components/common/date/index.ts delete mode 100644 packages/admin/dashboard/src/extensions/dashboard-extension-provider/types.ts delete mode 100644 packages/admin/dashboard/src/index.ts diff --git a/.changeset/stale-eyes-float.md b/.changeset/stale-eyes-float.md new file mode 100644 index 0000000000000..2d6c828aff7d8 --- /dev/null +++ b/.changeset/stale-eyes-float.md @@ -0,0 +1,6 @@ +--- +"@medusajs/dashboard": patch +"@medusajs/types": patch +--- + +fix(dashboard,types): Fix TS errors diff --git a/packages/admin/dashboard/src/components/common/action-menu/action-menu.tsx b/packages/admin/dashboard/src/components/common/action-menu/action-menu.tsx index 2ef6ca956d889..cd918763c2591 100644 --- a/packages/admin/dashboard/src/components/common/action-menu/action-menu.tsx +++ b/packages/admin/dashboard/src/components/common/action-menu/action-menu.tsx @@ -1,7 +1,7 @@ import { DropdownMenu, IconButton, clx } from "@medusajs/ui" import { EllipsisHorizontal } from "@medusajs/icons" -import { ReactNode } from "react" +import { PropsWithChildren, ReactNode } from "react" import { Link } from "react-router-dom" import { ConditionalTooltip } from "../conditional-tooltip" @@ -28,18 +28,20 @@ export type ActionGroup = { actions: Action[] } -type ActionMenuProps = { +type ActionMenuProps = PropsWithChildren<{ groups: ActionGroup[] -} +}> + +export const ActionMenu = ({ groups, children }: ActionMenuProps) => { + const inner = children ?? ( + + + + ) -export const ActionMenu = ({ groups }: ActionMenuProps) => { return ( - - - - - + {inner} {groups.map((group, index) => { if (!group.actions.length) { diff --git a/packages/admin/dashboard/src/components/common/button-menu/button-menu.tsx b/packages/admin/dashboard/src/components/common/button-menu/button-menu.tsx deleted file mode 100644 index 54ba5a476ec9c..0000000000000 --- a/packages/admin/dashboard/src/components/common/button-menu/button-menu.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import { DropdownMenu, clx } from "@medusajs/ui" - -import { PropsWithChildren, ReactNode } from "react" -import { Link } from "react-router-dom" - -type Action = { - icon: ReactNode - label: string - disabled?: boolean -} & ( - | { - to: string - onClick?: never - } - | { - onClick: () => void - to?: never - } -) - -type ActionGroup = { - actions: Action[] -} - -type ActionMenuProps = { - groups: ActionGroup[] -} - -export const ButtonMenu = ({ - groups, - children, -}: PropsWithChildren) => { - return ( - - {children} - - {groups.map((group, index) => { - if (!group.actions.length) { - return null - } - - const isLast = index === groups.length - 1 - - return ( - - {group.actions.map((action, index) => { - if (action.onClick) { - return ( - { - e.stopPropagation() - action.onClick() - }} - className={clx( - "[&_svg]:text-ui-fg-subtle flex items-center gap-x-2", - { - "[&_svg]:text-ui-fg-disabled": action.disabled, - } - )} - > - {action.icon} - {action.label} - - ) - } - - return ( -
- - e.stopPropagation()}> - {action.icon} - {action.label} - - -
- ) - })} - {!isLast && } -
- ) - })} -
-
- ) -} diff --git a/packages/admin/dashboard/src/components/common/button-menu/index.ts b/packages/admin/dashboard/src/components/common/button-menu/index.ts deleted file mode 100644 index a3a827d6173da..0000000000000 --- a/packages/admin/dashboard/src/components/common/button-menu/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./action-menu" diff --git a/packages/admin/dashboard/src/components/common/date/index.ts b/packages/admin/dashboard/src/components/common/date/index.ts deleted file mode 100644 index 98cb13cafc947..0000000000000 --- a/packages/admin/dashboard/src/components/common/date/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -import format from "date-fns/format" - -export function formatDate(date: string | Date) { - const value = new Date(date) - value.setMinutes(value.getMinutes() - value.getTimezoneOffset()) - - const hour12 = Intl.DateTimeFormat().resolvedOptions().hour12 - const timestampFormat = hour12 ? "dd MMM yyyy hh:MM a" : "dd MMM yyyy HH:MM" - - return format(value, timestampFormat) -} diff --git a/packages/admin/dashboard/src/components/forms/address-form/address-form.tsx b/packages/admin/dashboard/src/components/forms/address-form/address-form.tsx index 5485882289143..2dd74890e9bd7 100644 --- a/packages/admin/dashboard/src/components/forms/address-form/address-form.tsx +++ b/packages/admin/dashboard/src/components/forms/address-form/address-form.tsx @@ -2,11 +2,11 @@ import { Heading, Input, Select, clx } from "@medusajs/ui" import { useTranslation } from "react-i18next" import { z } from "zod" +import { HttpTypes } from "@medusajs/types" import { Control } from "react-hook-form" import { AddressSchema } from "../../../lib/schemas" import { Form } from "../../common/form" import { CountrySelect } from "../../inputs/country-select" -import { HttpTypes } from "@medusajs/types" type AddressFieldValues = z.infer @@ -187,14 +187,24 @@ export const AddressForm = ({ - {countries.map((country) => ( - - {country.display_name} - - ))} + {countries.map((country) => { + /** + * If a country does not have an ISO 2 code, it is not + * a valid country and should not be selectable. + */ + if (!country.iso_2) { + return null + } + + return ( + + {country.display_name} + + ) + })} ) : ( diff --git a/packages/admin/dashboard/src/components/forms/metadata-form/metadata-form.tsx b/packages/admin/dashboard/src/components/forms/metadata-form/metadata-form.tsx index c59374f055a13..19bee56d37339 100644 --- a/packages/admin/dashboard/src/components/forms/metadata-form/metadata-form.tsx +++ b/packages/admin/dashboard/src/components/forms/metadata-form/metadata-form.tsx @@ -18,7 +18,7 @@ import { Trash, } from "@medusajs/icons" import { FetchError } from "@medusajs/js-sdk" -import { ComponentPropsWithoutRef, forwardRef, useRef } from "react" +import { ComponentPropsWithoutRef, forwardRef } from "react" import { ConditionalTooltip } from "../../common/conditional-tooltip" import { Form } from "../../common/form" import { InlineTip } from "../../common/inline-tip" @@ -78,7 +78,6 @@ const InnerForm = ({ const { t } = useTranslation() const { handleSuccess } = useRouteModal() - const deletedOriginalRows = useRef([]) const hasUneditableRows = getHasUneditableRows(metadata) const form = useForm>({ diff --git a/packages/admin/dashboard/src/components/table/table-cells/common/created-at-cell/created-at-cell.tsx b/packages/admin/dashboard/src/components/table/table-cells/common/created-at-cell/created-at-cell.tsx index 50b480961b5b4..3b8309055c4d6 100644 --- a/packages/admin/dashboard/src/components/table/table-cells/common/created-at-cell/created-at-cell.tsx +++ b/packages/admin/dashboard/src/components/table/table-cells/common/created-at-cell/created-at-cell.tsx @@ -1,6 +1,6 @@ import { Tooltip } from "@medusajs/ui" -import format from "date-fns/format" import { useTranslation } from "react-i18next" +import { useDate } from "../../../../../hooks/use-date" import { PlaceholderCell } from "../placeholder-cell" type DateCellProps = { @@ -8,28 +8,26 @@ type DateCellProps = { } export const CreatedAtCell = ({ date }: DateCellProps) => { + const { getFullDate } = useDate() + if (!date) { return } - const value = new Date(date) - value.setMinutes(value.getMinutes() - value.getTimezoneOffset()) - - const hour12 = Intl.DateTimeFormat().resolvedOptions().hour12 - const timestampFormat = hour12 ? "dd MMM yyyy hh:MM a" : "dd MMM yyyy HH:MM" - return (
{`${format( - value, - timestampFormat - )}`} + {`${getFullDate({ + date, + includeTime: true, + })}`} } > - {format(value, "dd MMM yyyy")} + + {getFullDate({ date, includeTime: true })} +
) diff --git a/packages/admin/dashboard/src/components/table/table-cells/order/customer-cell/customer-cell.tsx b/packages/admin/dashboard/src/components/table/table-cells/order/customer-cell/customer-cell.tsx index 050aa744b3369..39d5045ac3767 100644 --- a/packages/admin/dashboard/src/components/table/table-cells/order/customer-cell/customer-cell.tsx +++ b/packages/admin/dashboard/src/components/table/table-cells/order/customer-cell/customer-cell.tsx @@ -4,7 +4,7 @@ import { useTranslation } from "react-i18next" export const CustomerCell = ({ customer, }: { - customer: HttpTypes.AdminCustomer | null + customer?: HttpTypes.AdminCustomer | null }) => { if (!customer) { return - diff --git a/packages/admin/dashboard/src/components/table/table-cells/order/payment-status-cell/payment-status-cell.tsx b/packages/admin/dashboard/src/components/table/table-cells/order/payment-status-cell/payment-status-cell.tsx index 0aacd0ba09e33..01382e098a3e4 100644 --- a/packages/admin/dashboard/src/components/table/table-cells/order/payment-status-cell/payment-status-cell.tsx +++ b/packages/admin/dashboard/src/components/table/table-cells/order/payment-status-cell/payment-status-cell.tsx @@ -1,9 +1,10 @@ +import { HttpTypes } from "@medusajs/types" import { useTranslation } from "react-i18next" import { getOrderPaymentStatus } from "../../../../../lib/order-helpers" import { StatusCell } from "../../common/status-cell" type PaymentStatusCellProps = { - status: PaymentStatus + status: HttpTypes.AdminOrder["payment_status"] } export const PaymentStatusCell = ({ status }: PaymentStatusCellProps) => { diff --git a/packages/admin/dashboard/src/components/table/table-cells/order/sales-channel-cell/sales-channel-cell.tsx b/packages/admin/dashboard/src/components/table/table-cells/order/sales-channel-cell/sales-channel-cell.tsx index c451cb12b27b9..50a3e0f35575e 100644 --- a/packages/admin/dashboard/src/components/table/table-cells/order/sales-channel-cell/sales-channel-cell.tsx +++ b/packages/admin/dashboard/src/components/table/table-cells/order/sales-channel-cell/sales-channel-cell.tsx @@ -4,7 +4,7 @@ import { useTranslation } from "react-i18next" export const SalesChannelCell = ({ channel, }: { - channel: HttpTypes.AdminSalesChannel | null + channel?: HttpTypes.AdminSalesChannel | null }) => { if (!channel) { return - diff --git a/packages/admin/dashboard/src/components/table/table-cells/region/countries-cell/countries-cell.tsx b/packages/admin/dashboard/src/components/table/table-cells/region/countries-cell/countries-cell.tsx index 22727be4850a8..085f18de819a5 100644 --- a/packages/admin/dashboard/src/components/table/table-cells/region/countries-cell/countries-cell.tsx +++ b/packages/admin/dashboard/src/components/table/table-cells/region/countries-cell/countries-cell.tsx @@ -1,4 +1,4 @@ -import { RegionCountryDTO } from "@medusajs/types" +import { HttpTypes } from "@medusajs/types" import { useTranslation } from "react-i18next" import { countries as COUNTRIES } from "../../../../../lib/data/countries" @@ -6,24 +6,24 @@ import { ListSummary } from "../../../../common/list-summary" import { PlaceholderCell } from "../../common/placeholder-cell" type CountriesCellProps = { - countries?: RegionCountryDTO[] | null + countries?: HttpTypes.AdminRegionCountry[] | null } export const CountriesCell = ({ countries }: CountriesCellProps) => { - const { t } = useTranslation() - if (!countries || countries.length === 0) { return } + const list = countries + .map( + (country) => + COUNTRIES.find((c) => c.iso_2 === country.iso_2)?.display_name + ) + .filter(Boolean) as string[] + return (
- - COUNTRIES.find((c) => c.iso_2 === country.iso_2)!.display_name - )} - /> +
) } diff --git a/packages/admin/dashboard/src/extensions/dashboard-extension-provider/types.ts b/packages/admin/dashboard/src/extensions/dashboard-extension-provider/types.ts deleted file mode 100644 index bb4fc6a93baad..0000000000000 --- a/packages/admin/dashboard/src/extensions/dashboard-extension-provider/types.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { InjectionZone } from "@medusajs/admin-shared" -import { ComponentType } from "react" -import { LoaderFunction } from "react-router-dom" -import { CustomFieldConfiguration } from "../../extensions/custom-field-registry/types" - -export type RouteExtension = { - Component: ComponentType - loader?: LoaderFunction - path: string -} - -export type MenuItemExtension = { - label: string - path: string - icon?: ComponentType -} - -export type WidgetExtension = { - Component: ComponentType - zone: InjectionZone[] -} - -export type RoutingExtensionConfig = { - routes: RouteExtension[] - menuItems: MenuItemExtension[] -} - -export type DashboardExtensionConfig = { - customFields?: CustomFieldConfiguration - menuItems?: MenuItemExtension[] - widgets?: WidgetExtension[] -} diff --git a/packages/admin/dashboard/src/hooks/api/auth.tsx b/packages/admin/dashboard/src/hooks/api/auth.tsx index 6d053e87d0887..170bfc5b70a19 100644 --- a/packages/admin/dashboard/src/hooks/api/auth.tsx +++ b/packages/admin/dashboard/src/hooks/api/auth.tsx @@ -1,11 +1,14 @@ -import { UseMutationOptions, useMutation } from "@tanstack/react-query" import { FetchError } from "@medusajs/js-sdk" -import { sdk } from "../../lib/client" import { HttpTypes } from "@medusajs/types" +import { UseMutationOptions, useMutation } from "@tanstack/react-query" +import { sdk } from "../../lib/client" export const useSignInWithEmailPass = ( options?: UseMutationOptions< - string, + | string + | { + location: string + }, FetchError, HttpTypes.AdminSignUpWithEmailPassword > diff --git a/packages/admin/dashboard/src/hooks/api/claims.tsx b/packages/admin/dashboard/src/hooks/api/claims.tsx index a191f459d108a..4fb5bbebcc924 100644 --- a/packages/admin/dashboard/src/hooks/api/claims.tsx +++ b/packages/admin/dashboard/src/hooks/api/claims.tsx @@ -208,14 +208,13 @@ export const useAddClaimInboundItems = ( id: string, orderId: string, options?: UseMutationOptions< - HttpTypes.AdminClaimResponse, + HttpTypes.AdminClaimReturnPreviewResponse, FetchError, HttpTypes.AdminAddClaimInboundItems > ) => { return useMutation({ - mutationFn: (payload: HttpTypes.AdminAddClaimInboundItems) => - sdk.admin.claim.addInboundItems(id, payload), + mutationFn: (payload) => sdk.admin.claim.addInboundItems(id, payload), onSuccess: (data: any, variables: any, context: any) => { queryClient.invalidateQueries({ queryKey: ordersQueryKeys.details(), diff --git a/packages/admin/dashboard/src/hooks/api/collections.tsx b/packages/admin/dashboard/src/hooks/api/collections.tsx index 9a6fbded6d65a..49e8aee34836c 100644 --- a/packages/admin/dashboard/src/hooks/api/collections.tsx +++ b/packages/admin/dashboard/src/hooks/api/collections.tsx @@ -37,7 +37,7 @@ export const useCollection = ( } export const useCollections = ( - query?: FindParams & HttpTypes.AdminCollectionFilters, + query?: FindParams & HttpTypes.AdminCollectionListParams, options?: Omit< UseQueryOptions< PaginatedResponse<{ collections: HttpTypes.AdminCollection[] }>, @@ -60,7 +60,7 @@ export const useCollections = ( export const useUpdateCollection = ( id: string, options?: UseMutationOptions< - { collection: HttpTypes.AdminCollection }, + HttpTypes.AdminCollectionResponse, FetchError, HttpTypes.AdminUpdateCollection > @@ -82,7 +82,7 @@ export const useUpdateCollection = ( export const useUpdateCollectionProducts = ( id: string, options?: UseMutationOptions< - { collection: HttpTypes.AdminCollection }, + HttpTypes.AdminCollectionResponse, FetchError, HttpTypes.AdminUpdateCollectionProducts > @@ -110,7 +110,7 @@ export const useUpdateCollectionProducts = ( export const useCreateCollection = ( options?: UseMutationOptions< - { collection: HttpTypes.AdminCollection }, + HttpTypes.AdminCollectionResponse, FetchError, HttpTypes.AdminCreateCollection > diff --git a/packages/admin/dashboard/src/hooks/api/invites.tsx b/packages/admin/dashboard/src/hooks/api/invites.tsx index 17a5f4e135ed6..3edfd86eb72a8 100644 --- a/packages/admin/dashboard/src/hooks/api/invites.tsx +++ b/packages/admin/dashboard/src/hooks/api/invites.tsx @@ -1,8 +1,5 @@ -import { - AdminInviteResponse, - HttpTypes, - PaginatedResponse, -} from "@medusajs/types" +import { FetchError } from "@medusajs/js-sdk" +import { HttpTypes } from "@medusajs/types" import { QueryKey, UseMutationOptions, @@ -13,7 +10,6 @@ import { import { sdk } from "../../lib/client" import { queryClient } from "../../lib/query-client" import { queryKeysFactory } from "../../lib/query-key-factory" -import { FetchError } from "@medusajs/js-sdk" const INVITES_QUERY_KEY = "invites" as const const invitesQueryKeys = queryKeysFactory(INVITES_QUERY_KEY) @@ -22,9 +18,9 @@ export const useInvite = ( id: string, options?: Omit< UseQueryOptions< - { invite: HttpTypes.AdminInviteResponse }, + HttpTypes.AdminInviteResponse, FetchError, - { invite: HttpTypes.AdminInviteResponse }, + HttpTypes.AdminInviteResponse, QueryKey >, "queryFn" | "queryKey" @@ -43,9 +39,9 @@ export const useInvites = ( query?: Record, options?: Omit< UseQueryOptions< - PaginatedResponse<{ invites: HttpTypes.AdminInviteResponse[] }>, + HttpTypes.AdminInviteListResponse, FetchError, - PaginatedResponse<{ invites: HttpTypes.AdminInviteResponse[] }>, + HttpTypes.AdminInviteListResponse, QueryKey >, "queryFn" | "queryKey" @@ -62,7 +58,7 @@ export const useInvites = ( export const useCreateInvite = ( options?: UseMutationOptions< - { invite: AdminInviteResponse }, + HttpTypes.AdminInviteResponse, FetchError, HttpTypes.AdminCreateInvite > @@ -79,11 +75,7 @@ export const useCreateInvite = ( export const useResendInvite = ( id: string, - options?: UseMutationOptions< - { invite: AdminInviteResponse }, - FetchError, - void - > + options?: UseMutationOptions ) => { return useMutation({ mutationFn: () => sdk.admin.invite.resend(id), @@ -118,7 +110,7 @@ export const useDeleteInvite = ( export const useAcceptInvite = ( inviteToken: string, options?: UseMutationOptions< - { user: HttpTypes.AdminUserResponse }, + HttpTypes.AdminAcceptInviteResponse, FetchError, HttpTypes.AdminAcceptInvite & { auth_token: string } > diff --git a/packages/admin/dashboard/src/hooks/api/price-lists.tsx b/packages/admin/dashboard/src/hooks/api/price-lists.tsx index 7c3fd1d43d039..1a4e2ac4fc166 100644 --- a/packages/admin/dashboard/src/hooks/api/price-lists.tsx +++ b/packages/admin/dashboard/src/hooks/api/price-lists.tsx @@ -128,7 +128,7 @@ export const useBatchPriceListPrices = ( id: string, query?: HttpTypes.AdminPriceListParams, options?: UseMutationOptions< - HttpTypes.AdminPriceListResponse, + HttpTypes.AdminPriceListBatchResponse, FetchError, HttpTypes.AdminBatchPriceListPrice > diff --git a/packages/admin/dashboard/src/hooks/table/columns/use-order-table-columns.tsx b/packages/admin/dashboard/src/hooks/table/columns/use-order-table-columns.tsx index e28ced6cab6e4..f26abbc35b8f2 100644 --- a/packages/admin/dashboard/src/hooks/table/columns/use-order-table-columns.tsx +++ b/packages/admin/dashboard/src/hooks/table/columns/use-order-table-columns.tsx @@ -1,3 +1,4 @@ +import { HttpTypes } from "@medusajs/types" import { ColumnDef, ColumnDefBase, @@ -33,7 +34,6 @@ import { TotalCell, TotalHeader, } from "../../../components/table/table-cells/order/total-cell" -import { HttpTypes } from "@medusajs/types" // We have to use any here, as the type of Order is so complex that it lags the TS server const columnHelper = createColumnHelper() diff --git a/packages/admin/dashboard/src/index.ts b/packages/admin/dashboard/src/index.ts deleted file mode 100644 index 18000e720893e..0000000000000 --- a/packages/admin/dashboard/src/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./render" diff --git a/packages/admin/dashboard/src/routes/orders/order-create-edit/components/order-edit-create-form/order-edit-items-section.tsx b/packages/admin/dashboard/src/routes/orders/order-create-edit/components/order-edit-create-form/order-edit-items-section.tsx index 1f212308174bb..e2a253475d67c 100644 --- a/packages/admin/dashboard/src/routes/orders/order-create-edit/components/order-edit-create-form/order-edit-items-section.tsx +++ b/packages/admin/dashboard/src/routes/orders/order-create-edit/components/order-edit-create-form/order-edit-items-section.tsx @@ -1,15 +1,15 @@ -import { useMemo, useState } from "react" import { AdminOrder, AdminOrderPreview } from "@medusajs/types" import { Button, Heading, Input, toast } from "@medusajs/ui" +import { useMemo, useState } from "react" import { useTranslation } from "react-i18next" import { RouteFocusModal, StackedFocusModal, useStackedModal, } from "../../../../../components/modals" +import { useAddOrderEditItems } from "../../../../../hooks/api/order-edits" import { AddOrderEditItemsTable } from "../add-order-edit-items-table" import { OrderEditItem } from "./order-edit-item" -import { useAddOrderEditItems } from "../../../../../hooks/api/order-edits" type ExchangeInboundSectionProps = { order: AdminOrder @@ -39,16 +39,19 @@ export const OrderEditItemsSection = ({ * CALLBACKS */ const onItemsSelected = async () => { - try { - await addItems({ + await addItems( + { items: addedVariants.map((i) => ({ variant_id: i, quantity: 1, })), - }) - } catch (e) { - toast.error(e.message) - } + }, + { + onError: (e) => { + toast.error(e.message) + }, + } + ) setIsOpen("inbound-items", false) } diff --git a/packages/admin/dashboard/src/routes/orders/order-detail/components/order-summary-section/order-summary-section.tsx b/packages/admin/dashboard/src/routes/orders/order-detail/components/order-summary-section/order-summary-section.tsx index 06432bcd3dbd7..c48a14611fbc0 100644 --- a/packages/admin/dashboard/src/routes/orders/order-detail/components/order-summary-section/order-summary-section.tsx +++ b/packages/admin/dashboard/src/routes/orders/order-detail/components/order-summary-section/order-summary-section.tsx @@ -38,7 +38,6 @@ import { import { AdminReservation } from "@medusajs/types/src/http" import { AdminPaymentCollection } from "../../../../../../../../core/types/dist/http/payment/admin/entities" import { ActionMenu } from "../../../../../components/common/action-menu" -import { ButtonMenu } from "../../../../../components/common/button-menu/button-menu" import { Thumbnail } from "../../../../../components/common/thumbnail" import { useClaims } from "../../../../../hooks/api/claims" import { useExchanges } from "../../../../../hooks/api/exchanges" @@ -193,7 +192,7 @@ export const OrderSummarySection = ({ order }: OrderSummarySectionProps) => { {t("orders.returns.receive.action")} ) : ( - { @@ -225,7 +224,7 @@ export const OrderSummarySection = ({ order }: OrderSummarySectionProps) => { - + ))} {showAllocateButton && ( diff --git a/packages/admin/dashboard/src/routes/orders/order-detail/components/order-summary-section/return-info-popover.tsx b/packages/admin/dashboard/src/routes/orders/order-detail/components/order-summary-section/return-info-popover.tsx index c73b4107bbf0e..92dc670ceb69f 100644 --- a/packages/admin/dashboard/src/routes/orders/order-detail/components/order-summary-section/return-info-popover.tsx +++ b/packages/admin/dashboard/src/routes/orders/order-detail/components/order-summary-section/return-info-popover.tsx @@ -3,7 +3,7 @@ import { AdminReturn } from "@medusajs/types" import { Badge, Popover, Text } from "@medusajs/ui" import { useState } from "react" import { useTranslation } from "react-i18next" -import { formatDate } from "../../../../../components/common/date" +import { useDate } from "../../../../../hooks/use-date" type ReturnInfoPopoverProps = { orderReturn: AdminReturn @@ -13,6 +13,8 @@ function ReturnInfoPopover({ orderReturn }: ReturnInfoPopoverProps) { const { t } = useTranslation() const [open, setOpen] = useState(false) + const { getFullDate } = useDate() + const handleMouseEnter = () => { setOpen(true) } @@ -44,7 +46,7 @@ function ReturnInfoPopover({ orderReturn }: ReturnInfoPopoverProps) { onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} autoFocus={false} - className="focus-visible:outline-none align-sub" + className="align-sub focus-visible:outline-none" > @@ -52,7 +54,7 @@ function ReturnInfoPopover({ orderReturn }: ReturnInfoPopoverProps) {
@@ -64,7 +66,7 @@ function ReturnInfoPopover({ orderReturn }: ReturnInfoPopoverProps) { {t(`orders.returns.returnRequested`)} {" · "} - {formatDate(orderReturn.requested_at)} + {getFullDate({ date: orderReturn.requested_at, includeTime: true })} @@ -73,7 +75,10 @@ function ReturnInfoPopover({ orderReturn }: ReturnInfoPopoverProps) { {" · "} {orderReturn.received_at - ? formatDate(orderReturn.received_at) + ? getFullDate({ + date: orderReturn.received_at, + includeTime: true, + }) : "-"}
diff --git a/packages/admin/dashboard/src/routes/tax-regions/common/hooks.ts b/packages/admin/dashboard/src/routes/tax-regions/common/hooks.ts index 0f519a09041ce..1caaa514f8b36 100644 --- a/packages/admin/dashboard/src/routes/tax-regions/common/hooks.ts +++ b/packages/admin/dashboard/src/routes/tax-regions/common/hooks.ts @@ -32,20 +32,12 @@ export const useDeleteTaxRegionAction = ({ await mutateAsync(undefined, { onSuccess: () => { - toast.success(t("general.success"), { - description: t("taxRegions.delete.successToast"), - dismissable: true, - dismissLabel: t("actions.close"), - }) + toast.success(t("taxRegions.delete.successToast")) navigate(to, { replace: true }) }, onError: (e) => { - toast.error(t("general.error"), { - description: e.message, - dismissable: true, - dismissLabel: t("actions.close"), - }) + toast.error(e.message) }, }) } diff --git a/packages/admin/dashboard/src/routes/users/user-edit/components/edit-user-form/edit-user-form.tsx b/packages/admin/dashboard/src/routes/users/user-edit/components/edit-user-form/edit-user-form.tsx index 7ee591bc17bd1..d448fe6e29900 100644 --- a/packages/admin/dashboard/src/routes/users/user-edit/components/edit-user-form/edit-user-form.tsx +++ b/packages/admin/dashboard/src/routes/users/user-edit/components/edit-user-form/edit-user-form.tsx @@ -4,14 +4,14 @@ import { useForm } from "react-hook-form" import { useTranslation } from "react-i18next" import * as zod from "zod" -import { UserDTO } from "@medusajs/types" +import { HttpTypes } from "@medusajs/types" import { Form } from "../../../../../components/common/form" import { RouteDrawer, useRouteModal } from "../../../../../components/modals" import { KeyboundForm } from "../../../../../components/utilities/keybound-form" import { useUpdateUser } from "../../../../../hooks/api/users" type EditUserFormProps = { - user: UserDTO + user: HttpTypes.AdminUser } const EditUserFormSchema = zod.object({ diff --git a/packages/admin/dashboard/src/routes/users/user-invite/components/invite-user-form/invite-user-form.tsx b/packages/admin/dashboard/src/routes/users/user-invite/components/invite-user-form/invite-user-form.tsx index 61622ec318b8a..136d5b420f6b9 100644 --- a/packages/admin/dashboard/src/routes/users/user-invite/components/invite-user-form/invite-user-form.tsx +++ b/packages/admin/dashboard/src/routes/users/user-invite/components/invite-user-form/invite-user-form.tsx @@ -1,6 +1,6 @@ import { zodResolver } from "@hookform/resolvers/zod" import { ArrowPath, Link, Trash } from "@medusajs/icons" -import { InviteDTO } from "@medusajs/types" +import { HttpTypes } from "@medusajs/types" import { Alert, Button, @@ -70,7 +70,7 @@ export const InviteUserForm = () => { const columns = useColumns() const { table } = useDataTable({ - data: (invites ?? []) as InviteDTO[], + data: invites ?? [], columns, count, enablePagination: true, @@ -185,7 +185,7 @@ export const InviteUserForm = () => { ) } -const InviteActions = ({ invite }: { invite: InviteDTO }) => { +const InviteActions = ({ invite }: { invite: HttpTypes.AdminInvite }) => { const { mutateAsync: revokeAsync } = useDeleteInvite(invite.id) const { mutateAsync: resendAsync } = useResendInvite(invite.id) @@ -253,7 +253,7 @@ const InviteActions = ({ invite }: { invite: InviteDTO }) => { ) } -const columnHelper = createColumnHelper() +const columnHelper = createColumnHelper() const useColumns = () => { const { t } = useTranslation() diff --git a/packages/admin/dashboard/src/routes/users/user-metadata/user-metadata.tsx b/packages/admin/dashboard/src/routes/users/user-metadata/user-metadata.tsx index 6c64330f1ea13..e88f49e798898 100644 --- a/packages/admin/dashboard/src/routes/users/user-metadata/user-metadata.tsx +++ b/packages/admin/dashboard/src/routes/users/user-metadata/user-metadata.tsx @@ -7,8 +7,8 @@ import { useUpdateUser, useUser } from "../../../hooks/api" export const UserMetadata = () => { const { id } = useParams() - const { user, isPending, isError, error } = useUser(id) - const { mutateAsync, isPending: isMutating } = useUpdateUser(id) + const { user, isPending, isError, error } = useUser(id!) + const { mutateAsync, isPending: isMutating } = useUpdateUser(id!) if (isError) { throw error diff --git a/packages/core/types/src/http/invite/admin/entities.ts b/packages/core/types/src/http/invite/admin/entities.ts index 10dc03dfcc1e0..35cbba36b29a1 100644 --- a/packages/core/types/src/http/invite/admin/entities.ts +++ b/packages/core/types/src/http/invite/admin/entities.ts @@ -18,7 +18,7 @@ export interface AdminInvite { /** * The date the invite expires. */ - expires_at?: Date + expires_at: string /** * Key-value pairs of custom data. */ @@ -26,9 +26,9 @@ export interface AdminInvite { /** * The date that the invite was created. */ - created_at?: Date + created_at: string /** * The date that the invite was updated. */ - updated_at?: Date + updated_at: string } From f95c4e240c4a5ca0fb88a09636c3d8a2266de279 Mon Sep 17 00:00:00 2001 From: Riqwan Thamir Date: Sun, 8 Dec 2024 13:13:31 +0100 Subject: [PATCH 07/11] fix(promotion, core-flows): updating cart with removed promotion removes adjustments (#10489) --- .changeset/empty-trees-walk.md | 6 ++ .../http/__tests__/cart/store/cart.spec.ts | 46 ++++++++++++++ .../src/cart/workflows/refresh-cart-items.ts | 15 +++-- .../promotion-module/compute-actions.spec.ts | 62 ------------------- .../src/services/promotion-module.ts | 9 +-- 5 files changed, 62 insertions(+), 76 deletions(-) create mode 100644 .changeset/empty-trees-walk.md diff --git a/.changeset/empty-trees-walk.md b/.changeset/empty-trees-walk.md new file mode 100644 index 0000000000000..3345800c5cee6 --- /dev/null +++ b/.changeset/empty-trees-walk.md @@ -0,0 +1,6 @@ +--- +"@medusajs/promotion": patch +"@medusajs/core-flows": patch +--- + +fix(promotion, core-flows): updating cart with removed promotion removes adjustments diff --git a/integration-tests/http/__tests__/cart/store/cart.spec.ts b/integration-tests/http/__tests__/cart/store/cart.spec.ts index c82ebaaaff5b5..0a5d36472ba00 100644 --- a/integration-tests/http/__tests__/cart/store/cart.spec.ts +++ b/integration-tests/http/__tests__/cart/store/cart.spec.ts @@ -1344,6 +1344,52 @@ medusaIntegrationTestRunner({ }) ) }) + + it("should remove promotion adjustments when promotion is deleted", async () => { + let cartBeforeRemovingPromotion = ( + await api.get(`/store/carts/${cart.id}`, storeHeaders) + ).data.cart + + expect(cartBeforeRemovingPromotion).toEqual( + expect.objectContaining({ + id: cart.id, + items: expect.arrayContaining([ + expect.objectContaining({ + adjustments: [ + { + id: expect.any(String), + code: "PROMOTION_APPLIED", + promotion_id: promotion.id, + amount: 100, + }, + ], + }), + ]), + }) + ) + + await api.delete(`/admin/promotions/${promotion.id}`, adminHeaders) + + let response = await api.post( + `/store/carts/${cart.id}`, + { + email: "test@test.com", + }, + storeHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.cart).toEqual( + expect.objectContaining({ + id: cart.id, + items: expect.arrayContaining([ + expect.objectContaining({ + adjustments: [], + }), + ]), + }) + ) + }) }) describe("POST /store/carts/:id/customer", () => { diff --git a/packages/core/core-flows/src/cart/workflows/refresh-cart-items.ts b/packages/core/core-flows/src/cart/workflows/refresh-cart-items.ts index 6a90feb1eca9c..93c4eadbe02a8 100644 --- a/packages/core/core-flows/src/cart/workflows/refresh-cart-items.ts +++ b/packages/core/core-flows/src/cart/workflows/refresh-cart-items.ts @@ -106,13 +106,16 @@ export const refreshCartItemsWorkflow = createWorkflow( input: { cart_id: cart.id }, }) - const cartPromoCodes = transform({ cart, input }, ({ cart, input }) => { - if (isDefined(input.promo_codes)) { - return input.promo_codes - } else { - return cart.promotions.map((p) => p.code) + const cartPromoCodes = transform( + { refetchedCart, input }, + ({ refetchedCart, input }) => { + if (isDefined(input.promo_codes)) { + return input.promo_codes + } else { + return refetchedCart.promotions.map((p) => p?.code).filter(Boolean) + } } - }) + ) updateCartPromotionsWorkflow.runAsStep({ input: { diff --git a/packages/modules/promotion/integration-tests/__tests__/services/promotion-module/compute-actions.spec.ts b/packages/modules/promotion/integration-tests/__tests__/services/promotion-module/compute-actions.spec.ts index 63f57081d3738..d839968794146 100644 --- a/packages/modules/promotion/integration-tests/__tests__/services/promotion-module/compute-actions.spec.ts +++ b/packages/modules/promotion/integration-tests/__tests__/services/promotion-module/compute-actions.spec.ts @@ -57,68 +57,6 @@ moduleIntegrationTestRunner({ expect(response).toEqual([]) }) - - it("should throw error when code in items adjustment does not exist", async () => { - await createDefaultPromotion(service, {}) - - const error = await service - .computeActions(["PROMOTION_TEST"], { - items: [ - { - id: "item_cotton_tshirt", - quantity: 1, - subtotal: 100, - adjustments: [ - { - id: "test-adjustment", - code: "DOES_NOT_EXIST", - }, - ], - }, - { - id: "item_cotton_sweater", - quantity: 5, - subtotal: 750, - }, - ], - }) - .catch((e) => e) - - expect(error.message).toContain( - "Applied Promotion for code (DOES_NOT_EXIST) not found" - ) - }) - - it("should throw error when code in shipping adjustment does not exist", async () => { - await createDefaultPromotion(service, {}) - - const error = await service - .computeActions(["PROMOTION_TEST"], { - items: [ - { - id: "item_cotton_tshirt", - quantity: 1, - subtotal: 100, - }, - { - id: "item_cotton_sweater", - quantity: 5, - subtotal: 750, - adjustments: [ - { - id: "test-adjustment", - code: "DOES_NOT_EXIST", - }, - ], - }, - ], - }) - .catch((e) => e) - - expect(error.message).toContain( - "Applied Promotion for code (DOES_NOT_EXIST) not found" - ) - }) }) describe("when promotion is for items and allocation is each", () => { diff --git a/packages/modules/promotion/src/services/promotion-module.ts b/packages/modules/promotion/src/services/promotion-module.ts index 5d3afdf86a50c..8ca859f3080cd 100644 --- a/packages/modules/promotion/src/services/promotion-module.ts +++ b/packages/modules/promotion/src/services/promotion-module.ts @@ -397,6 +397,7 @@ export default class PromotionModuleService ], }, { + take: null, relations: [ "application_method", "application_method.target_rules", @@ -421,19 +422,11 @@ export default class PromotionModuleService const appliedCodes = [...appliedShippingCodes, ...appliedItemCodes] for (const appliedCode of appliedCodes) { - const promotion = existingPromotionsMap.get(appliedCode) const adjustments = codeAdjustmentMap.get(appliedCode) || [] const action = appliedShippingCodes.includes(appliedCode) ? ComputedActions.REMOVE_SHIPPING_METHOD_ADJUSTMENT : ComputedActions.REMOVE_ITEM_ADJUSTMENT - if (!promotion) { - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - `Applied Promotion for code (${appliedCode}) not found` - ) - } - adjustments.forEach((adjustment) => computedActions.push({ action, From 9e797dc3d2d14fec9b878ab8ca38d8337a778b4a Mon Sep 17 00:00:00 2001 From: Riqwan Thamir Date: Sun, 8 Dec 2024 14:06:50 +0100 Subject: [PATCH 08/11] feat(core-flows, types): update shipping methods upon cart ops (#10382) * feat(core-flows,framework,medusa): list shipping options pass in cart as pricing context * chore: add test for shipping options returning free shipping * feat(core-flows, types): update shipping methods upon cart ops * chore: fix specs * chore: fix bugs + specs * Update update-shipping-methods.ts Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com> * Update mutations.ts Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com> * chore: undo refresh changes * chore: merge with latest * chore: address PR comments * chore: fix conflicts * chore: fix specs * chore: address reviews --------- Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com> --- .../http/__tests__/cart/store/cart.spec.ts | 411 ++++++++ .../admin/shipping-option.spec.ts | 20 +- .../store/shipping-option.spec.ts | 131 ++- .../cart/store/cart.workflows.spec.ts | 931 +++++++----------- .../__tests__/cart/store/carts.spec.ts | 312 ------ .../core/core-flows/src/cart/steps/index.ts | 2 - .../steps/refresh-cart-shipping-methods.ts | 73 -- .../steps/remove-shipping-method-from-cart.ts | 6 +- .../src/cart/steps/update-shipping-methods.ts | 43 + .../steps/validate-shipping-options-price.ts | 37 + .../core/core-flows/src/cart/utils/fields.ts | 1 + .../workflows/add-shipping-method-to-cart.ts | 32 +- .../src/cart/workflows/add-to-cart.ts | 54 +- .../list-shipping-options-for-cart.ts | 121 +-- .../src/cart/workflows/refresh-cart-items.ts | 7 +- .../refresh-cart-shipping-methods.ts | 125 +++ .../workflows/update-line-item-in-cart.ts | 47 +- packages/core/types/src/cart/mutations.ts | 5 + packages/core/types/src/cart/service.ts | 41 + .../api/admin/shipping-options/validators.ts | 2 +- .../shipping-option.spec.ts | 2 +- 21 files changed, 1164 insertions(+), 1239 deletions(-) delete mode 100644 packages/core/core-flows/src/cart/steps/refresh-cart-shipping-methods.ts create mode 100644 packages/core/core-flows/src/cart/steps/update-shipping-methods.ts create mode 100644 packages/core/core-flows/src/cart/steps/validate-shipping-options-price.ts create mode 100644 packages/core/core-flows/src/cart/workflows/refresh-cart-shipping-methods.ts diff --git a/integration-tests/http/__tests__/cart/store/cart.spec.ts b/integration-tests/http/__tests__/cart/store/cart.spec.ts index 0a5d36472ba00..6005a7f6d5de1 100644 --- a/integration-tests/http/__tests__/cart/store/cart.spec.ts +++ b/integration-tests/http/__tests__/cart/store/cart.spec.ts @@ -302,7 +302,107 @@ medusaIntegrationTestRunner({ }) describe("POST /store/carts/:id/line-items", () => { + let shippingOption + beforeEach(async () => { + const stockLocation = ( + await api.post( + `/admin/stock-locations`, + { name: "test location" }, + adminHeaders + ) + ).data.stock_location + + await api.post( + `/admin/stock-locations/${stockLocation.id}/sales-channels`, + { add: [salesChannel.id] }, + adminHeaders + ) + + const shippingProfile = ( + await api.post( + `/admin/shipping-profiles`, + { name: `test-${stockLocation.id}`, type: "default" }, + adminHeaders + ) + ).data.shipping_profile + + const fulfillmentSets = ( + await api.post( + `/admin/stock-locations/${stockLocation.id}/fulfillment-sets?fields=*fulfillment_sets`, + { + name: `Test-${shippingProfile.id}`, + type: "test-type", + }, + adminHeaders + ) + ).data.stock_location.fulfillment_sets + + const fulfillmentSet = ( + await api.post( + `/admin/fulfillment-sets/${fulfillmentSets[0].id}/service-zones`, + { + name: `Test-${shippingProfile.id}`, + geo_zones: [ + { type: "country", country_code: "it" }, + { type: "country", country_code: "us" }, + ], + }, + adminHeaders + ) + ).data.fulfillment_set + + await api.post( + `/admin/stock-locations/${stockLocation.id}/fulfillment-providers`, + { add: ["manual_test-provider"] }, + adminHeaders + ) + + shippingOption = ( + await api.post( + `/admin/shipping-options`, + { + name: `Shipping`, + service_zone_id: fulfillmentSet.service_zones[0].id, + shipping_profile_id: shippingProfile.id, + provider_id: "manual_test-provider", + price_type: "flat", + type: { + label: "Test type", + description: "Test description", + code: "test-code", + }, + prices: [ + { currency_code: "usd", amount: 1000 }, + { + currency_code: "usd", + amount: 0, + rules: [ + { + attribute: "item_total", + operator: "gt", + value: 5000, + }, + ], + }, + ], + rules: [ + { + attribute: "enabled_in_store", + value: '"true"', + operator: "eq", + }, + { + attribute: "is_return", + value: "false", + operator: "eq", + }, + ], + }, + adminHeaders + ) + ).data.shipping_option + cart = ( await api.post( `/store/carts`, @@ -362,6 +462,101 @@ medusaIntegrationTestRunner({ ) }) + describe("with custom shipping options prices", () => { + beforeEach(async () => { + cart = ( + await api.post( + `/store/carts`, + { + currency_code: "usd", + sales_channel_id: salesChannel.id, + region_id: region.id, + items: [{ variant_id: product.variants[0].id, quantity: 1 }], + promo_codes: [promotion.code], + }, + storeHeadersWithCustomer + ) + ).data.cart + }) + + it("should update shipping method amount when cart totals change", async () => { + let response = await api.post( + `/store/carts/${cart.id}/shipping-methods`, + { option_id: shippingOption.id }, + storeHeaders + ) + + expect(response.data.cart).toEqual( + expect.objectContaining({ + id: cart.id, + shipping_methods: expect.arrayContaining([ + expect.objectContaining({ + shipping_option_id: shippingOption.id, + amount: 1000, + is_tax_inclusive: true, + }), + ]), + }) + ) + + response = await api.post( + `/store/carts/${cart.id}/line-items`, + { + variant_id: product.variants[0].id, + quantity: 100, + }, + storeHeaders + ) + + expect(response.data.cart).toEqual( + expect.objectContaining({ + id: cart.id, + shipping_methods: expect.arrayContaining([ + expect.objectContaining({ + shipping_option_id: shippingOption.id, + amount: 0, + is_tax_inclusive: true, + }), + ]), + }) + ) + }) + + it("should remove shipping methods when they are no longer valid for the cart", async () => { + let response = await api.post( + `/store/carts/${cart.id}/shipping-methods`, + { option_id: shippingOption.id }, + storeHeaders + ) + + expect(response.data.cart).toEqual( + expect.objectContaining({ + id: cart.id, + shipping_methods: expect.arrayContaining([ + expect.objectContaining({ + shipping_option_id: shippingOption.id, + amount: 1000, + is_tax_inclusive: true, + }), + ]), + }) + ) + + response = await api.post( + `/store/carts/${cart.id}`, + { region_id: noAutomaticRegion.id }, + storeHeaders + ) + + expect(response.data.cart).toEqual( + expect.objectContaining({ + id: cart.id, + shipping_methods: expect.arrayContaining([]), + }) + ) + }) + }) + it("should add item to cart with tax lines multiple times", async () => { let response = await api.post( `/store/carts/${cart.id}/line-items`, @@ -1529,6 +1724,222 @@ medusaIntegrationTestRunner({ ) }) }) + + describe("POST /store/carts/:id/shipping-methods", () => { + let shippingOption + + beforeEach(async () => { + const stockLocation = ( + await api.post( + `/admin/stock-locations`, + { name: "test location" }, + adminHeaders + ) + ).data.stock_location + + await api.post( + `/admin/stock-locations/${stockLocation.id}/sales-channels`, + { add: [salesChannel.id] }, + adminHeaders + ) + + const shippingProfile = ( + await api.post( + `/admin/shipping-profiles`, + { name: `test-${stockLocation.id}`, type: "default" }, + adminHeaders + ) + ).data.shipping_profile + + const fulfillmentSets = ( + await api.post( + `/admin/stock-locations/${stockLocation.id}/fulfillment-sets?fields=*fulfillment_sets`, + { + name: `Test-${shippingProfile.id}`, + type: "test-type", + }, + adminHeaders + ) + ).data.stock_location.fulfillment_sets + + const fulfillmentSet = ( + await api.post( + `/admin/fulfillment-sets/${fulfillmentSets[0].id}/service-zones`, + { + name: `Test-${shippingProfile.id}`, + geo_zones: [ + { type: "country", country_code: "it" }, + { type: "country", country_code: "us" }, + ], + }, + adminHeaders + ) + ).data.fulfillment_set + + await api.post( + `/admin/stock-locations/${stockLocation.id}/fulfillment-providers`, + { add: ["manual_test-provider"] }, + adminHeaders + ) + + shippingOption = ( + await api.post( + `/admin/shipping-options`, + { + name: `Test shipping option ${fulfillmentSet.id}`, + service_zone_id: fulfillmentSet.service_zones[0].id, + shipping_profile_id: shippingProfile.id, + provider_id: "manual_test-provider", + price_type: "flat", + type: { + label: "Test type", + description: "Test description", + code: "test-code", + }, + prices: [ + { currency_code: "usd", amount: 1000 }, + { + currency_code: "usd", + amount: 500, + rules: [ + { + attribute: "item_total", + operator: "gt", + value: 3000, + }, + ], + }, + ], + rules: [ + { + attribute: "enabled_in_store", + value: '"true"', + operator: "eq", + }, + { + attribute: "is_return", + value: "false", + operator: "eq", + }, + ], + }, + adminHeaders + ) + ).data.shipping_option + + cart = ( + await api.post( + `/store/carts?fields=+total`, + { + currency_code: "usd", + sales_channel_id: salesChannel.id, + region_id: region.id, + items: [{ variant_id: product.variants[0].id, quantity: 1 }], + }, + storeHeadersWithCustomer + ) + ).data.cart + }) + + it("should add shipping method to cart", async () => { + let response = await api.post( + `/store/carts/${cart.id}/shipping-methods`, + { option_id: shippingOption.id }, + storeHeaders + ) + + expect(response.status).toEqual(200) + expect(response.data.cart).toEqual( + expect.objectContaining({ + id: cart.id, + shipping_methods: expect.arrayContaining([ + expect.objectContaining({ + shipping_option_id: shippingOption.id, + amount: 1000, + is_tax_inclusive: true, + }), + ]), + }) + ) + + // Total is over the amount 3000 to enable the second pricing rule + const cart2 = ( + await api.post( + `/store/carts?fields=+total`, + { + currency_code: "usd", + sales_channel_id: salesChannel.id, + region_id: region.id, + items: [{ variant_id: product.variants[0].id, quantity: 5 }], + }, + storeHeadersWithCustomer + ) + ).data.cart + + response = await api.post( + `/store/carts/${cart2.id}/shipping-methods`, + { option_id: shippingOption.id }, + storeHeaders + ) + + expect(response.data.cart).toEqual( + expect.objectContaining({ + id: cart2.id, + shipping_methods: expect.arrayContaining([ + expect.objectContaining({ + shipping_option_id: shippingOption.id, + amount: 500, + is_tax_inclusive: true, + }), + ]), + }) + ) + }) + + it("should throw when prices are not setup for shipping option", async () => { + cart = ( + await api.post( + `/store/carts?fields=+total`, + { + currency_code: "eur", + sales_channel_id: salesChannel.id, + region_id: region.id, + items: [{ variant_id: product.variants[0].id, quantity: 5 }], + }, + storeHeadersWithCustomer + ) + ).data.cart + + let { response } = await api + .post( + `/store/carts/${cart.id}/shipping-methods`, + { option_id: shippingOption.id }, + storeHeaders + ) + .catch((e) => e) + + expect(response.data).toEqual({ + type: "invalid_data", + message: `Shipping options with IDs ${shippingOption.id} do not have a price`, + }) + }) + + it("should throw when shipping option id is not found", async () => { + let { response } = await api + .post( + `/store/carts/${cart.id}/shipping-methods`, + { option_id: "does-not-exist" }, + storeHeaders + ) + .catch((e) => e) + + expect(response.status).toEqual(400) + expect(response.data).toEqual({ + type: "invalid_data", + message: "Shipping Options are invalid for cart.", + }) + }) + }) }) }, }) diff --git a/integration-tests/http/__tests__/shipping-option/admin/shipping-option.spec.ts b/integration-tests/http/__tests__/shipping-option/admin/shipping-option.spec.ts index 19644d9e8de89..8907c3563ec1a 100644 --- a/integration-tests/http/__tests__/shipping-option/admin/shipping-option.spec.ts +++ b/integration-tests/http/__tests__/shipping-option/admin/shipping-option.spec.ts @@ -152,12 +152,12 @@ medusaIntegrationTestRunner({ amount: 500, rules: [ { - attribute: "total", + attribute: "item_total", operator: "gte", value: 100, }, { - attribute: "total", + attribute: "item_total", operator: "lte", value: 200, }, @@ -220,12 +220,12 @@ medusaIntegrationTestRunner({ rules_count: 2, price_rules: expect.arrayContaining([ expect.objectContaining({ - attribute: "total", + attribute: "item_total", operator: "gte", value: "100", }), expect.objectContaining({ - attribute: "total", + attribute: "item_total", operator: "lte", value: "200", }), @@ -329,7 +329,7 @@ medusaIntegrationTestRunner({ amount: 500, rules: [ { - attribute: "total", + attribute: "item_total", operator: "gt", value: 200, }, @@ -380,7 +380,7 @@ medusaIntegrationTestRunner({ rules_count: 2, price_rules: expect.arrayContaining([ expect.objectContaining({ - attribute: "total", + attribute: "item_total", operator: "gt", value: "200", }), @@ -460,7 +460,7 @@ medusaIntegrationTestRunner({ amount: 500, rules: [ { - attribute: "total", + attribute: "item_total", operator: "not_whitelisted", value: 100, }, @@ -498,7 +498,7 @@ medusaIntegrationTestRunner({ amount: 500, rules: [ { - attribute: "total", + attribute: "item_total", operator: "gt", value: "string", }, @@ -628,7 +628,7 @@ medusaIntegrationTestRunner({ amount: 5, rules: [ { - attribute: "total", + attribute: "item_total", operator: "gt", value: 200, }, @@ -704,7 +704,7 @@ medusaIntegrationTestRunner({ amount: 5, price_rules: [ expect.objectContaining({ - attribute: "total", + attribute: "item_total", operator: "gt", value: "200", }), diff --git a/integration-tests/http/__tests__/shipping-option/store/shipping-option.spec.ts b/integration-tests/http/__tests__/shipping-option/store/shipping-option.spec.ts index 5c6e49a39417a..9261db2b2d224 100644 --- a/integration-tests/http/__tests__/shipping-option/store/shipping-option.spec.ts +++ b/integration-tests/http/__tests__/shipping-option/store/shipping-option.spec.ts @@ -1,9 +1,4 @@ import { medusaIntegrationTestRunner } from "@medusajs/test-utils" -import { - IFulfillmentModuleService, - IRegionModuleService, -} from "@medusajs/types" -import { ContainerRegistrationKeys, Modules } from "@medusajs/utils" import { createAdminUser, generatePublishableKey, @@ -20,9 +15,6 @@ medusaIntegrationTestRunner({ testSuite: ({ dbConnection, getContainer, api }) => { describe("Store: Shipping Option API", () => { let appContainer - let fulfillmentModule: IFulfillmentModuleService - let regionService: IRegionModuleService - let salesChannel let region let regionTwo @@ -36,8 +28,6 @@ medusaIntegrationTestRunner({ beforeAll(async () => { appContainer = getContainer() - fulfillmentModule = appContainer.resolve(Modules.FULFILLMENT) - regionService = appContainer.resolve(Modules.REGION) }) beforeEach(async () => { @@ -45,31 +35,27 @@ medusaIntegrationTestRunner({ storeHeaders = generateStoreHeaders({ publishableKey }) await createAdminUser(dbConnection, adminHeaders, appContainer) - const remoteLinkService = appContainer.resolve( - ContainerRegistrationKeys.REMOTE_LINK - ) - region = await regionService.createRegions({ - name: "Test region", - countries: ["US"], - currency_code: "usd", - }) - - regionTwo = await regionService.createRegions({ - name: "Test region two", - countries: ["DK"], - currency_code: "dkk", - }) + region = ( + await api.post( + "/admin/regions", + { name: "US", currency_code: "usd", countries: ["US"] }, + adminHeaders + ) + ).data.region - await api.post( - "/admin/price-preferences", - { - attribute: "region_id", - value: regionTwo.id, - is_tax_inclusive: true, - }, - adminHeaders - ) + regionTwo = ( + await api.post( + "/admin/regions", + { + name: "Test region two", + currency_code: "dkk", + countries: ["DK"], + is_tax_inclusive: true, + }, + adminHeaders + ) + ).data.region salesChannel = ( await api.post( @@ -116,22 +102,39 @@ medusaIntegrationTestRunner({ stockLocation = ( await api.post( `/admin/stock-locations`, - { - name: "test location", - }, + { name: "test location" }, adminHeaders ) ).data.stock_location - shippingProfile = await fulfillmentModule.createShippingProfiles({ - name: "Test", - type: "default", - }) + await api.post( + `/admin/stock-locations/${stockLocation.id}/sales-channels`, + { add: [salesChannel.id] }, + adminHeaders + ) - fulfillmentSet = await fulfillmentModule.createFulfillmentSets({ - name: "Test", - type: "test-type", - service_zones: [ + shippingProfile = ( + await api.post( + `/admin/shipping-profiles`, + { name: "Test", type: "default" }, + adminHeaders + ) + ).data.shipping_profile + + const fulfillmentSets = ( + await api.post( + `/admin/stock-locations/${stockLocation.id}/fulfillment-sets?fields=*fulfillment_sets`, + { + name: "Test", + type: "test-type", + }, + adminHeaders + ) + ).data.stock_location.fulfillment_sets + + fulfillmentSet = ( + await api.post( + `/admin/fulfillment-sets/${fulfillmentSets[0].id}/service-zones`, { name: "Test", geo_zones: [ @@ -139,27 +142,9 @@ medusaIntegrationTestRunner({ { type: "country", country_code: "dk" }, ], }, - ], - }) - - await remoteLinkService.create([ - { - [Modules.SALES_CHANNEL]: { - sales_channel_id: salesChannel.id, - }, - [Modules.STOCK_LOCATION]: { - stock_location_id: stockLocation.id, - }, - }, - { - [Modules.STOCK_LOCATION]: { - stock_location_id: stockLocation.id, - }, - [Modules.FULFILLMENT]: { - fulfillment_set_id: fulfillmentSet.id, - }, - }, - ]) + adminHeaders + ) + ).data.fulfillment_set await api.post( `/admin/stock-locations/${stockLocation.id}/fulfillment-providers`, @@ -196,7 +181,7 @@ medusaIntegrationTestRunner({ rules: [ { operator: "gt", - attribute: "total", + attribute: "item_total", value: 2000, }, ], @@ -246,8 +231,11 @@ medusaIntegrationTestRunner({ expect.objectContaining({ id: shippingOption.id, name: "Test shipping option", - amount: 1100, price_type: "flat", + amount: 1100, + calculated_price: expect.objectContaining({ + calculated_amount: 1100, + }), }) ) @@ -272,8 +260,12 @@ medusaIntegrationTestRunner({ id: shippingOption.id, name: "Test shipping option", amount: 500, - price_type: "flat", is_tax_inclusive: true, + calculated_price: expect.objectContaining({ + calculated_amount: 500, + is_calculated_price_tax_inclusive: true, + }), + price_type: "flat", }) ) }) @@ -313,6 +305,9 @@ medusaIntegrationTestRunner({ name: "Test shipping option", // Free shipping due to cart total being greater than 2000 amount: 0, + calculated_price: expect.objectContaining({ + calculated_amount: 0, + }), price_type: "flat", }) ) diff --git a/integration-tests/modules/__tests__/cart/store/cart.workflows.spec.ts b/integration-tests/modules/__tests__/cart/store/cart.workflows.spec.ts index 78f66698d01bc..f9a3ebb560e4b 100644 --- a/integration-tests/modules/__tests__/cart/store/cart.workflows.spec.ts +++ b/integration-tests/modules/__tests__/cart/store/cart.workflows.spec.ts @@ -34,8 +34,11 @@ import { import { adminHeaders, createAdminUser, + generatePublishableKey, + generateStoreHeaders, } from "../../../../helpers/create-admin-user" import { seedStorefrontDefaults } from "../../../../helpers/seed-storefront-defaults" +import { createAuthenticatedCustomer } from "../../../helpers/create-authenticated-customer" jest.setTimeout(200000) @@ -56,9 +59,10 @@ medusaIntegrationTestRunner({ let stockLocationModule: IStockLocationService let inventoryModule: IInventoryService let fulfillmentModule: IFulfillmentModuleService - let remoteLink, remoteQuery - + let remoteLink, remoteQuery, storeHeaders + let salesChannel let defaultRegion + let customer, storeHeadersWithCustomer beforeAll(async () => { appContainer = getContainer() @@ -69,9 +73,9 @@ medusaIntegrationTestRunner({ productModule = appContainer.resolve(Modules.PRODUCT) pricingModule = appContainer.resolve(Modules.PRICING) paymentModule = appContainer.resolve(Modules.PAYMENT) + fulfillmentModule = appContainer.resolve(Modules.FULFILLMENT) inventoryModule = appContainer.resolve(Modules.INVENTORY) stockLocationModule = appContainer.resolve(Modules.STOCK_LOCATION) - fulfillmentModule = appContainer.resolve(Modules.FULFILLMENT) remoteLink = appContainer.resolve(ContainerRegistrationKeys.REMOTE_LINK) remoteQuery = appContainer.resolve( ContainerRegistrationKeys.REMOTE_QUERY @@ -79,11 +83,35 @@ medusaIntegrationTestRunner({ }) beforeEach(async () => { + const publishableKey = await generatePublishableKey(appContainer) + storeHeaders = generateStoreHeaders({ publishableKey }) await createAdminUser(dbConnection, adminHeaders, appContainer) + const result = await createAuthenticatedCustomer(api, storeHeaders, { + first_name: "tony", + last_name: "stark", + email: "tony@test-industries.com", + }) + + customer = result.customer + storeHeadersWithCustomer = { + headers: { + ...storeHeaders.headers, + authorization: `Bearer ${result.jwt}`, + }, + } + const { region } = await seedStorefrontDefaults(appContainer, "dkk") defaultRegion = region + + salesChannel = ( + await api.post( + "/admin/sales-channels", + { name: "test sales channel", description: "channel" }, + adminHeaders + ) + ).data.sales_channel }) describe("CreateCartWorkflow", () => { @@ -896,123 +924,6 @@ medusaIntegrationTestRunner({ }) describe("updateLineItemInCartWorkflow", () => { - it("should update item in cart", async () => { - const salesChannel = await scModuleService.createSalesChannels({ - name: "Webshop", - }) - - const location = await stockLocationModule.createStockLocations({ - name: "Warehouse", - }) - - const [product] = await productModule.createProducts([ - { - title: "Test product", - variants: [ - { - title: "Test variant", - }, - ], - }, - ]) - - const inventoryItem = await inventoryModule.createInventoryItems({ - sku: "inv-1234", - }) - - await inventoryModule.createInventoryLevels([ - { - inventory_item_id: inventoryItem.id, - location_id: location.id, - stocked_quantity: 2, - reserved_quantity: 0, - }, - ]) - - const priceSet = await pricingModule.createPriceSets({ - prices: [ - { - amount: 3000, - currency_code: "usd", - }, - ], - }) - - await remoteLink.create([ - { - [Modules.PRODUCT]: { - variant_id: product.variants[0].id, - }, - [Modules.PRICING]: { - price_set_id: priceSet.id, - }, - }, - { - [Modules.SALES_CHANNEL]: { - sales_channel_id: salesChannel.id, - }, - [Modules.STOCK_LOCATION]: { - stock_location_id: location.id, - }, - }, - { - [Modules.PRODUCT]: { - variant_id: product.variants[0].id, - }, - [Modules.INVENTORY]: { - inventory_item_id: inventoryItem.id, - }, - }, - ]) - - let cart = await cartModuleService.createCarts({ - currency_code: "usd", - sales_channel_id: salesChannel.id, - items: [ - { - variant_id: product.variants[0].id, - quantity: 1, - unit_price: 5000, - title: "Test item", - }, - ], - }) - - cart = await cartModuleService.retrieveCart(cart.id, { - select: ["id", "region_id", "currency_code"], - relations: ["items", "items.variant_id", "items.metadata"], - }) - - const item = cart.items?.[0]! - - const { errors } = await updateLineItemInCartWorkflow( - appContainer - ).run({ - input: { - cart, - item, - update: { - metadata: { - foo: "bar", - }, - quantity: 2, - }, - }, - throwOnError: false, - }) - - const updatedItem = await cartModuleService.retrieveLineItem(item.id) - - expect(updatedItem).toEqual( - expect.objectContaining({ - id: item.id, - unit_price: 3000, - quantity: 2, - title: "Test item", - }) - ) - }) - describe("compensation", () => { it("should revert line item update to original state", async () => { expect.assertions(2) @@ -1544,45 +1455,23 @@ medusaIntegrationTestRunner({ let shippingProfile let fulfillmentSet let priceSet + let region + let stockLocation beforeEach(async () => { - cart = await cartModuleService.createCarts({ - currency_code: "usd", - shipping_address: { - country_code: "us", - province: "ny", - }, - }) - - shippingProfile = await fulfillmentModule.createShippingProfiles({ - name: "Test", - type: "default", - }) - - fulfillmentSet = await fulfillmentModule.createFulfillmentSets({ - name: "Test", - type: "test-type", - service_zones: [ + region = ( + await api.post( + "/admin/regions", { - name: "Test", - geo_zones: [{ type: "country", country_code: "us" }], + name: "test-region", + currency_code: "usd", + countries: ["us"], }, - ], - }) - - priceSet = await pricingModule.createPriceSets({ - prices: [{ amount: 3000, currency_code: "usd" }], - }) - - await pricingModule.createPricePreferences({ - attribute: "currency_code", - value: "usd", - is_tax_inclusive: true, - }) - }) + adminHeaders + ) + ).data.region - it("should add shipping method to cart", async () => { - const stockLocation = ( + stockLocation = ( await api.post( `/admin/stock-locations`, { name: "test location" }, @@ -1590,16 +1479,41 @@ medusaIntegrationTestRunner({ ) ).data.stock_location - await remoteLink.create([ - { - [Modules.STOCK_LOCATION]: { - stock_location_id: stockLocation.id, + await api.post( + `/admin/stock-locations/${stockLocation.id}/sales-channels`, + { add: [salesChannel.id] }, + adminHeaders + ) + + shippingProfile = ( + await api.post( + `/admin/shipping-profiles`, + { name: "Test", type: "default" }, + adminHeaders + ) + ).data.shipping_profile + + const fulfillmentSets = ( + await api.post( + `/admin/stock-locations/${stockLocation.id}/fulfillment-sets?fields=*fulfillment_sets`, + { + name: "Test", + type: "test-type", }, - [Modules.FULFILLMENT]: { - fulfillment_set_id: fulfillmentSet.id, + adminHeaders + ) + ).data.stock_location.fulfillment_sets + + fulfillmentSet = ( + await api.post( + `/admin/fulfillment-sets/${fulfillmentSets[0].id}/service-zones`, + { + name: "Test", + geo_zones: [{ type: "country", country_code: "us" }], }, - }, - ]) + adminHeaders + ) + ).data.fulfillment_set await api.post( `/admin/stock-locations/${stockLocation.id}/fulfillment-providers`, @@ -1607,37 +1521,61 @@ medusaIntegrationTestRunner({ adminHeaders ) - const shippingOption = await fulfillmentModule.createShippingOptions({ - name: "Test shipping option", - service_zone_id: fulfillmentSet.service_zones[0].id, - shipping_profile_id: shippingProfile.id, - provider_id: "manual_test-provider", - price_type: "flat", - type: { - label: "Test type", - description: "Test description", - code: "test-code", - }, - rules: [ - { - operator: RuleOperator.EQ, - attribute: "is_return", - value: "false", - }, + cart = ( + await api.post( + `/store/carts`, { - operator: RuleOperator.EQ, - attribute: "enabled_in_store", - value: "true", + currency_code: "usd", + region_id: region.id, + sales_channel_id: salesChannel.id, }, - ], - }) + storeHeaders + ) + ).data.cart - await remoteLink.create([ + await api.post( + "/admin/price-preferences", { - [Modules.FULFILLMENT]: { shipping_option_id: shippingOption.id }, - [Modules.PRICING]: { price_set_id: priceSet.id }, + attribute: "currency_code", + value: "usd", + is_tax_inclusive: true, }, - ]) + adminHeaders + ) + }) + + it("should add shipping method to cart", async () => { + const shippingOption = ( + await api.post( + `/admin/shipping-options`, + { + name: "Test shipping option", + service_zone_id: fulfillmentSet.service_zones[0].id, + shipping_profile_id: shippingProfile.id, + provider_id: "manual_test-provider", + price_type: "flat", + type: { + label: "Test type", + description: "Test description", + code: "test-code", + }, + prices: [{ amount: 3000, currency_code: "usd" }], + rules: [ + { + operator: RuleOperator.EQ, + attribute: "is_return", + value: "false", + }, + { + operator: RuleOperator.EQ, + attribute: "enabled_in_store", + value: "true", + }, + ], + }, + adminHeaders + ) + ).data.shipping_option await addShippingMethodToCartWorkflow(appContainer).run({ input: { @@ -1646,9 +1584,8 @@ medusaIntegrationTestRunner({ }, }) - cart = await cartModuleService.retrieveCart(cart.id, { - relations: ["shipping_methods"], - }) + cart = (await api.get(`/store/carts/${cart.id}`, storeHeaders)).data + .cart expect(cart).toEqual( expect.objectContaining({ @@ -1658,7 +1595,6 @@ medusaIntegrationTestRunner({ expect.objectContaining({ amount: 3000, is_tax_inclusive: true, - name: "Test shipping option", }), ], }) @@ -1666,40 +1602,37 @@ medusaIntegrationTestRunner({ }) it("should throw error when shipping option is not valid", async () => { - const shippingOption = await fulfillmentModule.createShippingOptions({ - name: "Test shipping option", - service_zone_id: fulfillmentSet.service_zones[0].id, - shipping_profile_id: shippingProfile.id, - provider_id: "manual_test-provider", - price_type: "flat", - type: { - label: "Test type", - description: "Test description", - code: "test-code", - }, - rules: [ + const shippingOption = ( + await api.post( + `/admin/shipping-options`, { - operator: RuleOperator.EQ, - attribute: "shipping_address.city", - value: "sf", + name: "Test shipping option", + service_zone_id: fulfillmentSet.service_zones[0].id, + shipping_profile_id: shippingProfile.id, + provider_id: "manual_test-provider", + price_type: "flat", + type: { + label: "Test type", + description: "Test description", + code: "test-code", + }, + rules: [ + { + operator: RuleOperator.EQ, + attribute: "shipping_address.city", + value: "sf", + }, + ], + prices: [{ amount: 3000, currency_code: "usd" }], }, - ], - }) - - await remoteLink.create([ - { - [Modules.FULFILLMENT]: { shipping_option_id: shippingOption.id }, - [Modules.PRICING]: { price_set_id: priceSet.id }, - }, - ]) + adminHeaders + ) + ).data.shipping_option const { errors } = await addShippingMethodToCartWorkflow( appContainer ).run({ - input: { - options: [{ id: shippingOption.id }], - cart_id: cart.id, - }, + input: { options: [{ id: shippingOption.id }], cart_id: cart.id }, throwOnError: false, }) @@ -1737,97 +1670,62 @@ medusaIntegrationTestRunner({ }) it("should add shipping method with custom data", async () => { - const stockLocation = ( + const shippingOption = ( await api.post( - `/admin/stock-locations`, - { name: "test location" }, + `/admin/shipping-options`, + { + name: "Test shipping option", + service_zone_id: fulfillmentSet.service_zones[0].id, + shipping_profile_id: shippingProfile.id, + provider_id: "manual_test-provider", + price_type: "flat", + type: { + label: "Test type", + description: "Test description", + code: "test-code", + }, + rules: [ + { + operator: RuleOperator.EQ, + attribute: "is_return", + value: "false", + }, + { + operator: RuleOperator.EQ, + attribute: "enabled_in_store", + value: "true", + }, + ], + prices: [{ amount: 3000, currency_code: "usd" }], + }, adminHeaders ) - ).data.stock_location - - await remoteLink.create([ - { - [Modules.STOCK_LOCATION]: { - stock_location_id: stockLocation.id, - }, - [Modules.FULFILLMENT]: { - fulfillment_set_id: fulfillmentSet.id, - }, - }, - ]) - - await api.post( - `/admin/stock-locations/${stockLocation.id}/fulfillment-providers`, - { add: ["manual_test-provider"] }, - adminHeaders - ) + ).data.shipping_option - const shippingOption = await fulfillmentModule.createShippingOptions({ - name: "Test shipping option", - service_zone_id: fulfillmentSet.service_zones[0].id, - shipping_profile_id: shippingProfile.id, - provider_id: "manual_test-provider", - price_type: "flat", - type: { - label: "Test type", - description: "Test description", - code: "test-code", + await addShippingMethodToCartWorkflow(appContainer).run({ + input: { + options: [{ id: shippingOption.id, data: { test: "test" } }], + cart_id: cart.id, }, - rules: [ - { - operator: RuleOperator.EQ, - attribute: "is_return", - value: "false", - }, - { - operator: RuleOperator.EQ, - attribute: "enabled_in_store", - value: "true", - }, - ], }) - await remoteLink.create([ - { - [Modules.FULFILLMENT]: { shipping_option_id: shippingOption.id }, - [Modules.PRICING]: { price_set_id: priceSet.id }, - }, - ]) - - await addShippingMethodToCartWorkflow(appContainer).run({ - input: { - options: [{ id: shippingOption.id, data: { test: "test" } }], - cart_id: cart.id, - }, - }) - - cart = await cartModuleService.retrieveCart(cart.id, { - select: ["id"], - relations: ["shipping_methods"], - }) + cart = ( + await api.get( + `/store/carts/${cart.id}?fields=+shipping_methods.data`, + storeHeaders + ) + ).data.cart expect(cart).toEqual( expect.objectContaining({ id: cart.id, shipping_methods: [ - { - id: expect.any(String), - cart_id: cart.id, - description: null, + expect.objectContaining({ amount: 3000, - raw_amount: { - value: "3000", - precision: 20, - }, - metadata: null, is_tax_inclusive: true, - name: "Test shipping option", data: { test: "test" }, shipping_option_id: shippingOption.id, - deleted_at: null, - updated_at: expect.any(Date), - created_at: expect.any(Date), - }, + }), ], }) ) @@ -1835,123 +1733,146 @@ medusaIntegrationTestRunner({ }) describe("listShippingOptionsForCartWorkflow", () => { + let cart + let shippingProfile + let fulfillmentSet let region + let stockLocation beforeEach(async () => { - region = await regionModuleService.createRegions({ - name: "US", - currency_code: "usd", - }) - }) + region = ( + await api.post( + "/admin/regions", + { + name: "test-region", + currency_code: "usd", + countries: ["us"], + }, + adminHeaders + ) + ).data.region - it("should list shipping options for cart", async () => { - const salesChannel = await scModuleService.createSalesChannels({ - name: "Webshop", - }) + stockLocation = ( + await api.post( + `/admin/stock-locations`, + { name: "test location" }, + adminHeaders + ) + ).data.stock_location - const location = await stockLocationModule.createStockLocations({ - name: "Europe", - }) + await api.post( + `/admin/stock-locations/${stockLocation.id}/sales-channels`, + { add: [salesChannel.id] }, + adminHeaders + ) - let cart = await cartModuleService.createCarts({ - currency_code: "usd", - region_id: region.id, - sales_channel_id: salesChannel.id, - shipping_address: { - city: "CPH", - province: "Sjaelland", - country_code: "dk", - }, - }) + shippingProfile = ( + await api.post( + `/admin/shipping-profiles`, + { name: "Test", type: "default" }, + adminHeaders + ) + ).data.shipping_profile - const shippingProfile = - await fulfillmentModule.createShippingProfiles({ - name: "Test", - type: "default", - }) + const fulfillmentSets = ( + await api.post( + `/admin/stock-locations/${stockLocation.id}/fulfillment-sets?fields=*fulfillment_sets`, + { + name: "Test", + type: "test-type", + }, + adminHeaders + ) + ).data.stock_location.fulfillment_sets - const fulfillmentSet = await fulfillmentModule.createFulfillmentSets({ - name: "Test", - type: "test-type", - service_zones: [ + fulfillmentSet = ( + await api.post( + `/admin/fulfillment-sets/${fulfillmentSets[0].id}/service-zones`, { name: "Test", - geo_zones: [ - { - type: "country", - country_code: "dk", - }, - ], + geo_zones: [{ type: "country", country_code: "us" }], }, - ], - }) + adminHeaders + ) + ).data.fulfillment_set - const shippingOption = await fulfillmentModule.createShippingOptions({ - name: "Test shipping option", - service_zone_id: fulfillmentSet.service_zones[0].id, - shipping_profile_id: shippingProfile.id, - provider_id: "manual_test-provider", - price_type: "flat", - type: { - label: "Test type", - description: "Test description", - code: "test-code", - }, - }) + await api.post( + `/admin/stock-locations/${stockLocation.id}/fulfillment-providers`, + { add: ["manual_test-provider"] }, + adminHeaders + ) - const priceSet = await pricingModule.createPriceSets({ - prices: [ + cart = ( + await api.post( + `/store/carts`, { - amount: 3000, currency_code: "usd", - }, - ], - }) - - await remoteLink.create([ - { - [Modules.SALES_CHANNEL]: { + region_id: region.id, sales_channel_id: salesChannel.id, }, - [Modules.STOCK_LOCATION]: { - stock_location_id: location.id, - }, - }, + storeHeaders + ) + ).data.cart + + await api.post( + "/admin/price-preferences", { - [Modules.STOCK_LOCATION]: { - stock_location_id: location.id, - }, - [Modules.FULFILLMENT]: { - fulfillment_set_id: fulfillmentSet.id, - }, + attribute: "currency_code", + value: "usd", + is_tax_inclusive: true, }, - { - [Modules.FULFILLMENT]: { - shipping_option_id: shippingOption.id, - }, - [Modules.PRICING]: { - price_set_id: priceSet.id, + adminHeaders + ) + }) + + it("should list shipping options for cart", async () => { + const shippingOption = ( + await api.post( + `/admin/shipping-options`, + { + name: "Test shipping option", + service_zone_id: fulfillmentSet.service_zones[0].id, + shipping_profile_id: shippingProfile.id, + provider_id: "manual_test-provider", + price_type: "flat", + type: { + label: "Test type", + description: "Test description", + code: "test-code", + }, + prices: [ + { + amount: 3000, + currency_code: "usd", + }, + ], + rules: [ + { + operator: RuleOperator.EQ, + attribute: "is_return", + value: "false", + }, + { + operator: RuleOperator.EQ, + attribute: "enabled_in_store", + value: "true", + }, + ], }, - }, - ]) + adminHeaders + ) + ).data.shipping_option - cart = await cartModuleService.retrieveCart(cart.id, { - select: ["id"], - relations: ["shipping_address"], - }) + cart = (await api.get(`/store/carts/${cart.id}`, storeHeaders)).data + .cart const { result } = await listShippingOptionsForCartWorkflow( appContainer - ).run({ - input: { - cart_id: cart.id, - }, - }) + ).run({ input: { cart_id: cart.id } }) expect(result).toEqual([ expect.objectContaining({ amount: 3000, - name: "Test shipping option", id: shippingOption.id, }), ]) @@ -1962,43 +1883,15 @@ medusaIntegrationTestRunner({ name: "Webshop", }) - const location = await stockLocationModule.createStockLocations({ - name: "Europe", - }) - let cart = await cartModuleService.createCarts({ currency_code: "usd", region_id: region.id, sales_channel_id: salesChannel.id, shipping_address: { - city: "CPH", - province: "Sjaelland", - country_code: "dk", + country_code: "us", }, }) - const shippingProfile = - await fulfillmentModule.createShippingProfiles({ - name: "Test", - type: "default", - }) - - const fulfillmentSet = await fulfillmentModule.createFulfillmentSets({ - name: "Test", - type: "test-type", - service_zones: [ - { - name: "Test", - geo_zones: [ - { - type: "country", - country_code: "dk", - }, - ], - }, - ], - }) - const shippingOption = await fulfillmentModule.createShippingOptions([ { name: "Return shipping option", @@ -2057,12 +1950,12 @@ medusaIntegrationTestRunner({ sales_channel_id: salesChannel.id, }, [Modules.STOCK_LOCATION]: { - stock_location_id: location.id, + stock_location_id: stockLocation.id, }, }, { [Modules.STOCK_LOCATION]: { - stock_location_id: location.id, + stock_location_id: stockLocation.id, }, [Modules.FULFILLMENT]: { fulfillment_set_id: fulfillmentSet.id, @@ -2116,88 +2009,49 @@ medusaIntegrationTestRunner({ name: "Webshop", }) - const location = await stockLocationModule.createStockLocations({ - name: "Europe", - }) - - let cart = await cartModuleService.createCarts({ - currency_code: "usd", - region_id: region.id, - sales_channel_id: salesChannel.id, - shipping_address: { - city: "CPH", - province: "Sjaelland", - country_code: "dk", - }, - }) - - const shippingProfile = - await fulfillmentModule.createShippingProfiles({ - name: "Test", - type: "default", - }) - - const fulfillmentSet = await fulfillmentModule.createFulfillmentSets({ - name: "Test", - type: "test-type", - service_zones: [ - { - name: "Test", - geo_zones: [ - { - type: "country", - country_code: "us", - }, - ], - }, - ], - }) - - const shippingOption = await fulfillmentModule.createShippingOptions({ - name: "Test shipping option", - service_zone_id: fulfillmentSet.service_zones[0].id, - shipping_profile_id: shippingProfile.id, - provider_id: "manual_test-provider", - price_type: "flat", - type: { - label: "Test type", - description: "Test description", - code: "test-code", - }, - }) - - const priceSet = await pricingModule.createPriceSets({ - prices: [ - { - amount: 3000, - currency_code: "usd", - }, - ], - }) + await api.post( + `/store/carts/${cart.id}`, + { sales_channel_id: salesChannel.id }, + storeHeaders + ) - await remoteLink.create([ - { - [Modules.STOCK_LOCATION]: { - stock_location_id: location.id, - }, - [Modules.FULFILLMENT]: { - fulfillment_set_id: fulfillmentSet.id, - }, - }, + await api.post( + `/admin/shipping-options`, { - [Modules.FULFILLMENT]: { - shipping_option_id: shippingOption.id, - }, - [Modules.PRICING]: { - price_set_id: priceSet.id, + name: "Test shipping option", + service_zone_id: fulfillmentSet.service_zones[0].id, + shipping_profile_id: shippingProfile.id, + provider_id: "manual_test-provider", + price_type: "flat", + type: { + label: "Test type", + description: "Test description", + code: "test-code", }, + prices: [ + { + amount: 3000, + currency_code: "usd", + }, + ], + rules: [ + { + operator: RuleOperator.EQ, + attribute: "is_return", + value: "false", + }, + { + operator: RuleOperator.EQ, + attribute: "enabled_in_store", + value: "true", + }, + ], }, - ]) + adminHeaders + ) - cart = await cartModuleService.retrieveCart(cart.id, { - select: ["id"], - relations: ["shipping_address"], - }) + cart = (await api.get(`/store/carts/${cart.id}`, storeHeaders)).data + .cart const { result } = await listShippingOptionsForCartWorkflow( appContainer @@ -2207,99 +2061,6 @@ medusaIntegrationTestRunner({ expect(result).toEqual([]) }) - - it("should throw when shipping options are missing prices", async () => { - const salesChannel = await scModuleService.createSalesChannels({ - name: "Webshop", - }) - - const location = await stockLocationModule.createStockLocations({ - name: "Europe", - }) - - let cart = await cartModuleService.createCarts({ - currency_code: "usd", - region_id: region.id, - sales_channel_id: salesChannel.id, - shipping_address: { - city: "CPH", - province: "Sjaelland", - country_code: "dk", - }, - }) - - const shippingProfile = - await fulfillmentModule.createShippingProfiles({ - name: "Test", - type: "default", - }) - - const fulfillmentSet = await fulfillmentModule.createFulfillmentSets({ - name: "Test", - type: "test-type", - service_zones: [ - { - name: "Test", - geo_zones: [ - { - type: "country", - country_code: "dk", - }, - ], - }, - ], - }) - - const shippingOption = await fulfillmentModule.createShippingOptions({ - name: "Test shipping option", - service_zone_id: fulfillmentSet.service_zones[0].id, - shipping_profile_id: shippingProfile.id, - provider_id: "manual_test-provider", - price_type: "flat", - type: { - label: "Test type", - description: "Test description", - code: "test-code", - }, - }) - - await remoteLink.create([ - { - [Modules.SALES_CHANNEL]: { - sales_channel_id: salesChannel.id, - }, - [Modules.STOCK_LOCATION]: { - stock_location_id: location.id, - }, - }, - { - [Modules.STOCK_LOCATION]: { - stock_location_id: location.id, - }, - [Modules.FULFILLMENT]: { - fulfillment_set_id: fulfillmentSet.id, - }, - }, - ]) - - cart = await cartModuleService.retrieveCart(cart.id, { - select: ["id"], - relations: ["shipping_address"], - }) - - const { errors } = await listShippingOptionsForCartWorkflow( - appContainer - ).run({ - input: { cart_id: cart.id }, - throwOnError: false, - }) - - expect(errors).toEqual([ - expect.objectContaining({ - message: `Shipping options with IDs ${shippingOption.id} do not have a price`, - }), - ]) - }) }) describe("updateTaxLinesWorkflow", () => { diff --git a/integration-tests/modules/__tests__/cart/store/carts.spec.ts b/integration-tests/modules/__tests__/cart/store/carts.spec.ts index 42f3d21e228c7..b268a1ff80d47 100644 --- a/integration-tests/modules/__tests__/cart/store/carts.spec.ts +++ b/integration-tests/modules/__tests__/cart/store/carts.spec.ts @@ -20,7 +20,6 @@ import { Modules, ProductStatus, PromotionType, - RuleOperator, } from "@medusajs/utils" import { createAdminUser, @@ -715,221 +714,6 @@ medusaIntegrationTestRunner({ }) }) - it("should add item to cart", async () => { - const customer = await customerModule.createCustomers({ - email: "tony@stark-industries.com", - }) - - const salesChannel = await scModule.createSalesChannels({ - name: "Webshop", - }) - - const [productWithSpecialTax] = await productModule.createProducts([ - { - // This product ID is setup in the tax structure fixture (setupTaxStructure) - id: "product_id_1", - title: "Test product", - variants: [{ title: "Test variant", manage_inventory: false }], - } as any, - ]) - - const [productWithDefaultTax] = await productModule.createProducts([ - { - title: "Test product default tax", - variants: [ - { title: "Test variant default tax", manage_inventory: false }, - ], - }, - ]) - - await api.post( - "/admin/price-preferences", - { - attribute: "currency_code", - value: "usd", - is_tax_inclusive: true, - }, - adminHeaders - ) - - const cart = await cartModule.createCarts({ - currency_code: "usd", - customer_id: customer.id, - sales_channel_id: salesChannel.id, - region_id: region.id, - shipping_address: { - customer_id: customer.id, - address_1: "test address 1", - address_2: "test address 2", - city: "SF", - country_code: "US", - province: "CA", - postal_code: "94016", - }, - items: [ - { - id: "item-1", - unit_price: 2000, - quantity: 1, - title: "Test item", - product_id: "prod_mat", - } as any, - ], - }) - - const appliedPromotion = await promotionModule.createPromotions({ - code: "PROMOTION_APPLIED", - type: PromotionType.STANDARD, - application_method: { - type: "fixed", - target_type: "items", - allocation: "across", - value: 300, - apply_to_quantity: 2, - currency_code: "usd", - target_rules: [ - { - attribute: "product_id", - operator: "in", - values: ["prod_mat", productWithSpecialTax.id], - }, - ], - }, - }) - - const [lineItemAdjustment] = await cartModule.addLineItemAdjustments([ - { - code: appliedPromotion.code!, - amount: 300, - item_id: "item-1", - promotion_id: appliedPromotion.id, - }, - ]) - - const [priceSet, priceSetDefaultTax] = - await pricingModule.createPriceSets([ - { - prices: [{ amount: 3000, currency_code: "usd" }], - }, - { - prices: [{ amount: 2000, currency_code: "usd" }], - }, - ]) - - await remoteLink.create([ - { - [Modules.PRODUCT]: { - variant_id: productWithSpecialTax.variants[0].id, - }, - [Modules.PRICING]: { price_set_id: priceSet.id }, - }, - { - [Modules.PRODUCT]: { - variant_id: productWithDefaultTax.variants[0].id, - }, - [Modules.PRICING]: { price_set_id: priceSetDefaultTax.id }, - }, - { - [Modules.CART]: { cart_id: cart.id }, - [Modules.PROMOTION]: { promotion_id: appliedPromotion.id }, - }, - ]) - - let response = await api.post( - `/store/carts/${cart.id}/line-items`, - { - variant_id: productWithSpecialTax.variants[0].id, - quantity: 1, - }, - storeHeaders - ) - - expect(response.status).toEqual(200) - - expect(response.data.cart).toEqual( - expect.objectContaining({ - id: cart.id, - currency_code: "usd", - items: expect.arrayContaining([ - expect.objectContaining({ - unit_price: 3000, - is_tax_inclusive: true, - quantity: 1, - title: "Test variant", - tax_lines: [ - expect.objectContaining({ - description: "CA Reduced Rate for Products", - code: "CAREDUCE_PROD", - rate: 3, - provider_id: "system", - }), - ], - adjustments: [ - expect.objectContaining({ - code: "PROMOTION_APPLIED", - amount: 177.86561264822134, - }), - ], - }), - expect.objectContaining({ - unit_price: 2000, - is_tax_inclusive: false, - quantity: 1, - title: "Test item", - tax_lines: [ - expect.objectContaining({ - code: "CADEFAULT", - description: "CA Default Rate", - provider_id: "system", - rate: 5, - }), - ], - adjustments: [ - expect.objectContaining({ - id: expect.not.stringContaining(lineItemAdjustment.id), - code: "PROMOTION_APPLIED", - amount: 122.13438735177866, - }), - ], - }), - ]), - }) - ) - - response = await api.post( - `/store/carts/${cart.id}/line-items`, - { - variant_id: productWithDefaultTax.variants[0].id, - quantity: 1, - }, - storeHeaders - ) - - expect(response.data.cart).toEqual( - expect.objectContaining({ - id: cart.id, - currency_code: "usd", - items: expect.arrayContaining([ - expect.objectContaining({ - unit_price: 2000, - is_tax_inclusive: true, - quantity: 1, - title: "Test variant default tax", - tax_lines: [ - // Uses the california default rate - expect.objectContaining({ - description: "CA Default Rate", - code: "CADEFAULT", - rate: 5, - provider_id: "system", - }), - ], - }), - ]), - }) - ) - }) - it("adding an existing variant should update or create line item depending on metadata", async () => { const product = ( await api.post(`/admin/products`, productData, adminHeaders) @@ -1304,102 +1088,6 @@ medusaIntegrationTestRunner({ }) }) - describe("POST /store/carts/:id/shipping-methods", () => { - it("should add a shipping methods to a cart", async () => { - const cart = await cartModule.createCarts({ - currency_code: "usd", - shipping_address: { country_code: "us" }, - items: [], - }) - - const shippingProfile = - await fulfillmentModule.createShippingProfiles({ - name: "Test", - type: "default", - }) - - const fulfillmentSet = await fulfillmentModule.createFulfillmentSets({ - name: "Test", - type: "test-type", - service_zones: [ - { - name: "Test", - geo_zones: [{ type: "country", country_code: "us" }], - }, - ], - }) - - await api.post( - "/admin/price-preferences", - { - attribute: "currency_code", - value: "usd", - is_tax_inclusive: true, - }, - adminHeaders - ) - - const priceSet = await pricingModule.createPriceSets({ - prices: [{ amount: 3000, currency_code: "usd" }], - }) - - const shippingOption = await fulfillmentModule.createShippingOptions({ - name: "Test shipping option", - service_zone_id: fulfillmentSet.service_zones[0].id, - shipping_profile_id: shippingProfile.id, - provider_id: "manual_test-provider", - price_type: "flat", - type: { - label: "Test type", - description: "Test description", - code: "test-code", - }, - rules: [ - { - operator: RuleOperator.EQ, - attribute: "is_return", - value: "false", - }, - { - operator: RuleOperator.EQ, - attribute: "enabled_in_store", - value: "true", - }, - ], - }) - - await remoteLink.create([ - { - [Modules.FULFILLMENT]: { shipping_option_id: shippingOption.id }, - [Modules.PRICING]: { price_set_id: priceSet.id }, - }, - ]) - - let response = await api.post( - `/store/carts/${cart.id}/shipping-methods`, - { option_id: shippingOption.id }, - storeHeaders - ) - - expect(response.status).toEqual(200) - expect(response.data.cart).toEqual( - expect.objectContaining({ - id: cart.id, - shipping_methods: [ - { - shipping_option_id: shippingOption.id, - amount: 3000, - is_tax_inclusive: true, - id: expect.any(String), - tax_lines: [], - adjustments: [], - }, - ], - }) - ) - }) - }) - describe("POST /store/carts/:id/complete", () => { let salesChannel let product diff --git a/packages/core/core-flows/src/cart/steps/index.ts b/packages/core/core-flows/src/cart/steps/index.ts index 5f9cddd02f526..de29ab97c2f77 100644 --- a/packages/core/core-flows/src/cart/steps/index.ts +++ b/packages/core/core-flows/src/cart/steps/index.ts @@ -14,7 +14,6 @@ export * from "./get-promotion-codes-to-apply" export * from "./get-variant-price-sets" export * from "./get-variants" export * from "./prepare-adjustments-from-promotion-actions" -export * from "./refresh-cart-shipping-methods" export * from "./remove-line-item-adjustments" export * from "./remove-shipping-method-adjustments" export * from "./remove-shipping-method-from-cart" @@ -27,4 +26,3 @@ export * from "./update-line-items" export * from "./validate-cart-payments" export * from "./validate-cart-shipping-options" export * from "./validate-variant-prices" - diff --git a/packages/core/core-flows/src/cart/steps/refresh-cart-shipping-methods.ts b/packages/core/core-flows/src/cart/steps/refresh-cart-shipping-methods.ts deleted file mode 100644 index 5c3830838ab32..0000000000000 --- a/packages/core/core-flows/src/cart/steps/refresh-cart-shipping-methods.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { - CartDTO, - ICartModuleService, - IFulfillmentModuleService, -} from "@medusajs/framework/types" -import { Modules, arrayDifference } from "@medusajs/framework/utils" -import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk" - -export interface RefreshCartShippingMethodsStepInput { - cart: CartDTO -} - -export const refreshCartShippingMethodsStepId = "refresh-cart-shipping-methods" -/** - * This step refreshes the shipping methods of a cart. - */ -export const refreshCartShippingMethodsStep = createStep( - refreshCartShippingMethodsStepId, - async (data: RefreshCartShippingMethodsStepInput, { container }) => { - const { cart } = data - const { shipping_methods: shippingMethods = [] } = cart - - if (!shippingMethods?.length) { - return new StepResponse(void 0, []) - } - - const fulfillmentModule = container.resolve( - Modules.FULFILLMENT - ) - - const cartModule = container.resolve(Modules.CART) - - const shippingOptionIds: string[] = shippingMethods.map( - (sm) => sm.shipping_option_id! - ) - - const validShippingOptions = - await fulfillmentModule.listShippingOptionsForContext( - { - id: shippingOptionIds, - context: { ...cart, is_return: "false", enabled_in_store: "true" }, - address: { - country_code: cart.shipping_address?.country_code, - province_code: cart.shipping_address?.province, - city: cart.shipping_address?.city, - postal_expression: cart.shipping_address?.postal_code, - }, - }, - { relations: ["rules"] } - ) - - const validShippingOptionIds = validShippingOptions.map((o) => o.id) - const invalidShippingOptionIds = arrayDifference( - shippingOptionIds, - validShippingOptionIds - ) - - const shippingMethodsToDelete = shippingMethods - .filter((sm) => invalidShippingOptionIds.includes(sm.shipping_option_id!)) - .map((sm) => sm.id) - - await cartModule.softDeleteShippingMethods(shippingMethodsToDelete) - - return new StepResponse(void 0, shippingMethodsToDelete) - }, - async (shippingMethodsToRestore, { container }) => { - if (shippingMethodsToRestore?.length) { - const cartModule = container.resolve(Modules.CART) - - await cartModule.restoreShippingMethods(shippingMethodsToRestore) - } - } -) diff --git a/packages/core/core-flows/src/cart/steps/remove-shipping-method-from-cart.ts b/packages/core/core-flows/src/cart/steps/remove-shipping-method-from-cart.ts index d9c739abd05cc..3706c8b02c0d1 100644 --- a/packages/core/core-flows/src/cart/steps/remove-shipping-method-from-cart.ts +++ b/packages/core/core-flows/src/cart/steps/remove-shipping-method-from-cart.ts @@ -16,6 +16,10 @@ export const removeShippingMethodFromCartStep = createStep( async (data: RemoveShippingMethodFromCartStepInput, { container }) => { const cartService = container.resolve(Modules.CART) + if (!data?.shipping_method_ids?.length) { + return new StepResponse(null, []) + } + const methods = await cartService.softDeleteShippingMethods( data.shipping_method_ids ) @@ -23,7 +27,7 @@ export const removeShippingMethodFromCartStep = createStep( return new StepResponse(methods, data.shipping_method_ids) }, async (ids, { container }) => { - if (!ids) { + if (!ids?.length) { return } diff --git a/packages/core/core-flows/src/cart/steps/update-shipping-methods.ts b/packages/core/core-flows/src/cart/steps/update-shipping-methods.ts new file mode 100644 index 0000000000000..b53b2f981a7d6 --- /dev/null +++ b/packages/core/core-flows/src/cart/steps/update-shipping-methods.ts @@ -0,0 +1,43 @@ +import { + ICartModuleService, + UpdateShippingMethodDTO, +} from "@medusajs/framework/types" +import { + Modules, + getSelectsAndRelationsFromObjectArray, +} from "@medusajs/framework/utils" +import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk" + +export const updateShippingMethodsStepId = "update-shipping-methods-step" +/** + * This step updates a cart's shipping methods. + */ +export const updateShippingMethodsStep = createStep( + updateShippingMethodsStepId, + async (data: UpdateShippingMethodDTO[], { container }) => { + if (!data?.length) { + return new StepResponse([], []) + } + + const cartModule = container.resolve(Modules.CART) + const { selects, relations } = getSelectsAndRelationsFromObjectArray(data) + + const dataBeforeUpdate = await cartModule.listShippingMethods( + { id: data.map((d) => d.id!) }, + { select: selects, relations } + ) + + const updatedItems = await cartModule.updateShippingMethods(data) + + return new StepResponse(updatedItems, dataBeforeUpdate) + }, + async (dataBeforeUpdate, { container }) => { + if (!dataBeforeUpdate?.length) { + return + } + + const cartModule: ICartModuleService = container.resolve(Modules.CART) + + await cartModule.updateShippingMethods(dataBeforeUpdate) + } +) diff --git a/packages/core/core-flows/src/cart/steps/validate-shipping-options-price.ts b/packages/core/core-flows/src/cart/steps/validate-shipping-options-price.ts new file mode 100644 index 0000000000000..51784e93cdb10 --- /dev/null +++ b/packages/core/core-flows/src/cart/steps/validate-shipping-options-price.ts @@ -0,0 +1,37 @@ +import { isDefined, MedusaError } from "@medusajs/framework/utils" +import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk" + +export const validateCartShippingOptionsStepId = + "validate-cart-shipping-options" +/** + * This step validates shipping options to ensure they have a price. + */ +export const validateCartShippingOptionsPriceStep = createStep( + "validate-cart-shipping-options-price", + async (data: { shippingOptions: any[] }, { container }) => { + const { shippingOptions = [] } = data + const optionsMissingPrices: string[] = [] + + for (const shippingOption of shippingOptions) { + const { calculated_price, ...options } = shippingOption + + if ( + shippingOption?.id && + !isDefined(calculated_price?.calculated_amount) + ) { + optionsMissingPrices.push(options.id) + } + } + + if (optionsMissingPrices.length) { + const ids = optionsMissingPrices.join(", ") + + throw new MedusaError( + MedusaError.Types.INVALID_DATA, + `Shipping options with IDs ${ids} do not have a price` + ) + } + + return new StepResponse(void 0) + } +) diff --git a/packages/core/core-flows/src/cart/utils/fields.ts b/packages/core/core-flows/src/cart/utils/fields.ts index 9d58454953fe6..f3d6f7e69aa78 100644 --- a/packages/core/core-flows/src/cart/utils/fields.ts +++ b/packages/core/core-flows/src/cart/utils/fields.ts @@ -3,6 +3,7 @@ export const cartFieldsForRefreshSteps = [ "currency_code", "quantity", "subtotal", + "total", "item_subtotal", "shipping_subtotal", "region_id", diff --git a/packages/core/core-flows/src/cart/workflows/add-shipping-method-to-cart.ts b/packages/core/core-flows/src/cart/workflows/add-shipping-method-to-cart.ts index 86a2cf16b0799..d097137586e85 100644 --- a/packages/core/core-flows/src/cart/workflows/add-shipping-method-to-cart.ts +++ b/packages/core/core-flows/src/cart/workflows/add-shipping-method-to-cart.ts @@ -14,7 +14,9 @@ import { } from "../steps" import { validateCartStep } from "../steps/validate-cart" import { validateAndReturnShippingMethodsDataStep } from "../steps/validate-shipping-methods-data" +import { validateCartShippingOptionsPriceStep } from "../steps/validate-shipping-options-price" import { cartFieldsForRefreshSteps } from "../utils/fields" +import { listShippingOptionsForCartWorkflow } from "./list-shipping-options-for-cart" import { updateCartPromotionsWorkflow } from "./update-cart-promotions" import { updateTaxLinesWorkflow } from "./update-tax-lines" @@ -54,30 +56,24 @@ export const addShippingMethodToCartWorkflow = createWorkflow( shippingOptionsContext: { is_return: "false", enabled_in_store: "true" }, }) - const shippingOptions = useRemoteQueryStep({ - entry_point: "shipping_option", - fields: [ - "id", - "name", - "calculated_price.calculated_amount", - "calculated_price.is_calculated_price_tax_inclusive", - "provider_id", - ], - variables: { - id: optionIds, - calculated_price: { - context: { currency_code: cart.currency_code }, - }, + const shippingOptions = listShippingOptionsForCartWorkflow.runAsStep({ + input: { + option_ids: optionIds, + cart_id: cart.id, + is_return: false, }, - }).config({ name: "fetch-shipping-option" }) + }) + + validateCartShippingOptionsPriceStep({ shippingOptions }) const validateShippingMethodsDataInput = transform( { input, shippingOptions }, - (data) => { - return data.input.options.map((inputOption) => { - const shippingOption = data.shippingOptions.find( + ({ input, shippingOptions }) => { + return input.options.map((inputOption) => { + const shippingOption = shippingOptions.find( (so) => so.id === inputOption.id ) + return { id: inputOption.id, provider_id: shippingOption?.provider_id, diff --git a/packages/core/core-flows/src/cart/workflows/add-to-cart.ts b/packages/core/core-flows/src/cart/workflows/add-to-cart.ts index 90a129cf5df48..7050c005adc00 100644 --- a/packages/core/core-flows/src/cart/workflows/add-to-cart.ts +++ b/packages/core/core-flows/src/cart/workflows/add-to-cart.ts @@ -8,27 +8,20 @@ import { parallelize, transform, WorkflowData, - WorkflowResponse, } from "@medusajs/framework/workflows-sdk" import { emitEventStep } from "../../common/steps/emit-event" import { useRemoteQueryStep } from "../../common/steps/use-remote-query" import { createLineItemsStep, getLineItemActionsStep, - refreshCartShippingMethodsStep, updateLineItemsStep, } from "../steps" import { validateCartStep } from "../steps/validate-cart" import { validateVariantPricesStep } from "../steps/validate-variant-prices" -import { - cartFieldsForRefreshSteps, - productVariantsFields, -} from "../utils/fields" +import { productVariantsFields } from "../utils/fields" import { prepareLineItemData } from "../utils/prepare-line-item-data" import { confirmVariantInventoryWorkflow } from "./confirm-variant-inventory" -import { refreshPaymentCollectionForCartWorkflow } from "./refresh-payment-collection" -import { updateCartPromotionsWorkflow } from "./update-cart-promotions" -import { updateTaxLinesWorkflow } from "./update-tax-lines" +import { refreshCartItemsWorkflow } from "./refresh-cart-items" export const addToCartWorkflowId = "add-to-cart" /** @@ -44,6 +37,7 @@ export const addToCartWorkflow = createWorkflow( }) // TODO: This is on par with the context used in v1.*, but we can be more flexible. + // TODO: create a common workflow to fetch variants and its prices const pricingContext = transform({ cart: input.cart }, (data) => { return { currency_code: data.cart.currency_code, @@ -100,7 +94,7 @@ export const addToCartWorkflow = createWorkflow( }, }) - const [createdItems, updatedItems] = parallelize( + parallelize( createLineItemsStep({ id: input.cart.id, items: itemsToCreate, @@ -111,43 +105,13 @@ export const addToCartWorkflow = createWorkflow( }) ) - const items = transform({ createdItems, updatedItems }, (data) => { - return [...(data.createdItems || []), ...(data.updatedItems || [])] - }) - - const cart = useRemoteQueryStep({ - entry_point: "cart", - fields: cartFieldsForRefreshSteps, - variables: { id: input.cart.id }, - list: false, - }).config({ name: "refetch–cart" }) - - parallelize( - refreshCartShippingMethodsStep({ cart }), - emitEventStep({ - eventName: CartWorkflowEvents.UPDATED, - data: { id: input.cart.id }, - }) - ) - - updateTaxLinesWorkflow.runAsStep({ - input: { - cart_id: input.cart.id, - }, + refreshCartItemsWorkflow.runAsStep({ + input: { cart_id: input.cart.id }, }) - updateCartPromotionsWorkflow.runAsStep({ - input: { - cart_id: input.cart.id, - }, + emitEventStep({ + eventName: CartWorkflowEvents.UPDATED, + data: { id: input.cart.id }, }) - - refreshPaymentCollectionForCartWorkflow.runAsStep({ - input: { - cart_id: input.cart.id, - }, - }) - - return new WorkflowResponse(items) } ) diff --git a/packages/core/core-flows/src/cart/workflows/list-shipping-options-for-cart.ts b/packages/core/core-flows/src/cart/workflows/list-shipping-options-for-cart.ts index d1069c0e3f809..c2a72e172e3fb 100644 --- a/packages/core/core-flows/src/cart/workflows/list-shipping-options-for-cart.ts +++ b/packages/core/core-flows/src/cart/workflows/list-shipping-options-for-cart.ts @@ -1,8 +1,7 @@ -import { deepFlatMap, isPresent, MedusaError } from "@medusajs/framework/utils" +import { deepFlatMap } from "@medusajs/framework/utils" import { createWorkflow, transform, - when, WorkflowData, WorkflowResponse, } from "@medusajs/framework/workflows-sdk" @@ -16,7 +15,14 @@ export const listShippingOptionsForCartWorkflowId = */ export const listShippingOptionsForCartWorkflow = createWorkflow( listShippingOptionsForCartWorkflowId, - (input: WorkflowData<{ cart_id: string; is_return?: boolean }>) => { + ( + input: WorkflowData<{ + cart_id: string + option_ids?: string[] + is_return?: boolean + enabled_in_store?: boolean + }> + ) => { const cartQuery = useQueryGraphStep({ entity: "cart", filters: { id: input.cart_id }, @@ -28,6 +34,8 @@ export const listShippingOptionsForCartWorkflow = createWorkflow( "shipping_address.city", "shipping_address.country_code", "shipping_address.province", + "shipping_address.postal_code", + "item_total", "total", ], options: { throwIfKeyNotFound: true }, @@ -70,42 +78,31 @@ export const listShippingOptionsForCartWorkflow = createWorkflow( } ) - const customerGroupIds = when( - "get-customer-group", - { cart }, - ({ cart }) => { - return !!cart.id - } - ).then(() => { - const customerQuery = useQueryGraphStep({ - entity: "customer", - filters: { id: cart.customer_id }, - fields: ["groups.id"], - }).config({ name: "get-customer" }) - - return transform({ customerQuery }, ({ customerQuery }) => { - const customer = customerQuery.data[0] + const queryVariables = transform( + { input, fulfillmentSetIds, cart }, + ({ input, fulfillmentSetIds, cart }) => ({ + id: input.option_ids, - if (!isPresent(customer)) { - return [] - } + context: { + is_return: input.is_return ?? false, + enabled_in_store: input.enabled_in_store ?? true, + }, - const { groups = [] } = customer + filters: { + fulfillment_set_id: fulfillmentSetIds, - return groups.map((group) => group.id) - }) - }) + address: { + country_code: cart.shipping_address?.country_code, + province_code: cart.shipping_address?.province, + city: cart.shipping_address?.city, + postal_expression: cart.shipping_address?.postal_code, + }, + }, - const pricingContext = transform( - { cart, customerGroupIds }, - ({ cart, customerGroupIds }) => ({ - ...cart, - customer_group_id: customerGroupIds, + calculated_price: { context: cart }, }) ) - const isReturn = transform({ input }, ({ input }) => !!input.is_return) - const shippingOptions = useRemoteQueryStep({ entry_point: "shipping_options", fields: [ @@ -116,7 +113,6 @@ export const listShippingOptionsForCartWorkflow = createWorkflow( "shipping_profile_id", "provider_id", "data", - "amount", "type.id", "type.label", @@ -132,55 +128,22 @@ export const listShippingOptionsForCartWorkflow = createWorkflow( "calculated_price.*", ], - variables: { - context: { - is_return: isReturn, - enabled_in_store: "true", - }, - filters: { - fulfillment_set_id: fulfillmentSetIds, - address: { - city: cart.shipping_address?.city, - country_code: cart.shipping_address?.country_code, - province_code: cart.shipping_address?.province, - }, - }, - - calculated_price: { - context: pricingContext, - }, - }, + variables: queryVariables, }).config({ name: "shipping-options-query" }) - const shippingOptionsWithPrice = transform({ shippingOptions }, (data) => { - const optionsMissingPrices: string[] = [] - - const options = data.shippingOptions.map((shippingOption) => { - const { calculated_price, ...options } = shippingOption ?? {} - - if (options?.id && !isPresent(calculated_price?.calculated_amount)) { - optionsMissingPrices.push(options.id) - } + const shippingOptionsWithPrice = transform( + { shippingOptions }, + ({ shippingOptions }) => + shippingOptions.map((shippingOption) => { + const price = shippingOption.calculated_price - return { - ...options, - amount: calculated_price?.calculated_amount, - is_tax_inclusive: - !!calculated_price?.is_calculated_price_tax_inclusive, - } - }) - - if (optionsMissingPrices.length) { - const ids = optionsMissingPrices.join(", ") - - throw new MedusaError( - MedusaError.Types.INVALID_DATA, - `Shipping options with IDs ${ids} do not have a price` - ) - } - - return options - }) + return { + ...shippingOption, + amount: price?.calculated_amount, + is_tax_inclusive: !!price?.is_calculated_price_tax_inclusive, + } + }) + ) return new WorkflowResponse(shippingOptionsWithPrice) } diff --git a/packages/core/core-flows/src/cart/workflows/refresh-cart-items.ts b/packages/core/core-flows/src/cart/workflows/refresh-cart-items.ts index 93c4eadbe02a8..46e1194d86213 100644 --- a/packages/core/core-flows/src/cart/workflows/refresh-cart-items.ts +++ b/packages/core/core-flows/src/cart/workflows/refresh-cart-items.ts @@ -6,13 +6,14 @@ import { WorkflowResponse, } from "@medusajs/framework/workflows-sdk" import { useRemoteQueryStep } from "../../common/steps/use-remote-query" -import { refreshCartShippingMethodsStep, updateLineItemsStep } from "../steps" +import { updateLineItemsStep } from "../steps" import { validateVariantPricesStep } from "../steps/validate-variant-prices" import { cartFieldsForRefreshSteps, productVariantsFields, } from "../utils/fields" import { prepareLineItemData } from "../utils/prepare-line-item-data" +import { refreshCartShippingMethodsWorkflow } from "./refresh-cart-shipping-methods" import { refreshPaymentCollectionForCartWorkflow } from "./refresh-payment-collection" import { updateCartPromotionsWorkflow } from "./update-cart-promotions" import { updateTaxLinesWorkflow } from "./update-tax-lines" @@ -100,7 +101,9 @@ export const refreshCartItemsWorkflow = createWorkflow( list: false, }).config({ name: "refetch–cart" }) - refreshCartShippingMethodsStep({ cart: refetchedCart }) + refreshCartShippingMethodsWorkflow.runAsStep({ + input: { cart_id: cart.id }, + }) updateTaxLinesWorkflow.runAsStep({ input: { cart_id: cart.id }, diff --git a/packages/core/core-flows/src/cart/workflows/refresh-cart-shipping-methods.ts b/packages/core/core-flows/src/cart/workflows/refresh-cart-shipping-methods.ts new file mode 100644 index 0000000000000..9d8850686809c --- /dev/null +++ b/packages/core/core-flows/src/cart/workflows/refresh-cart-shipping-methods.ts @@ -0,0 +1,125 @@ +import { isDefined, isPresent } from "@medusajs/framework/utils" +import { + createWorkflow, + parallelize, + transform, + when, + WorkflowData, +} from "@medusajs/framework/workflows-sdk" +import { useQueryGraphStep } from "../../common" +import { removeShippingMethodFromCartStep } from "../steps" +import { updateShippingMethodsStep } from "../steps/update-shipping-methods" +import { listShippingOptionsForCartWorkflow } from "./list-shipping-options-for-cart" + +export const refreshCartShippingMethodsWorkflowId = + "refresh-cart-shipping-methods" +/** + * This workflow refreshes a cart's shipping methods + */ +export const refreshCartShippingMethodsWorkflow = createWorkflow( + refreshCartShippingMethodsWorkflowId, + (input: WorkflowData<{ cart_id: string }>) => { + const cartQuery = useQueryGraphStep({ + entity: "cart", + filters: { id: input.cart_id }, + fields: [ + "id", + "sales_channel_id", + "currency_code", + "region_id", + "shipping_methods.*", + "shipping_address.city", + "shipping_address.country_code", + "shipping_address.province", + "shipping_methods.shipping_option_id", + "total", + ], + options: { throwIfKeyNotFound: true }, + }).config({ name: "get-cart" }) + + const cart = transform({ cartQuery }, ({ cartQuery }) => cartQuery.data[0]) + const shippingOptionIds: string[] = transform({ cart }, ({ cart }) => + (cart.shipping_methods || []) + .map((shippingMethod) => shippingMethod.shipping_option_id) + .filter(Boolean) + ) + + when({ shippingOptionIds }, ({ shippingOptionIds }) => { + return !!shippingOptionIds?.length + }).then(() => { + const shippingOptions = listShippingOptionsForCartWorkflow.runAsStep({ + input: { + option_ids: shippingOptionIds, + cart_id: cart.id, + is_return: false, + }, + }) + + // Creates an object on which shipping methods to remove or update depending + // on the validity of the shipping options for the cart + const shippingMethodsData = transform( + { cart, shippingOptions }, + ({ cart, shippingOptions }) => { + const { shipping_methods: shippingMethods = [] } = cart + + const validShippingMethods = shippingMethods.filter( + (shippingMethod) => { + // Fetch the available shipping options for the cart context and find the one associated + // with the current shipping method + const shippingOption = shippingOptions.find( + (shippingOption) => + shippingOption.id === shippingMethod.shipping_option_id + ) + + const shippingOptionPrice = + shippingOption?.calculated_price?.calculated_amount + + // The shipping method is only valid if both the shipping option and the price is found + // for the context of the cart. The invalid options will lead to a deleted shipping method + if (isPresent(shippingOption) && isDefined(shippingOptionPrice)) { + return true + } + + return false + } + ) + + const shippingMethodIds = shippingMethods.map((sm) => sm.id) + const validShippingMethodIds = validShippingMethods.map((sm) => sm.id) + const invalidShippingMethodIds = shippingMethodIds.filter( + (id) => !validShippingMethodIds.includes(id) + ) + + const shippingMethodsToUpdate = validShippingMethods.map( + (shippingMethod) => { + const shippingOption = shippingOptions.find( + (s) => s.id === shippingMethod.shipping_option_id + )! + + return { + id: shippingMethod.id, + shipping_option_id: shippingOption.id, + amount: shippingOption.calculated_price.calculated_amount, + is_tax_inclusive: + shippingOption.calculated_price + .is_calculated_price_tax_inclusive, + } + } + ) + + return { + shippingMethodsToRemove: invalidShippingMethodIds, + shippingMethodsToUpdate, + } + } + ) + + parallelize( + removeShippingMethodFromCartStep({ + shipping_method_ids: shippingMethodsData.shippingMethodsToRemove, + }), + updateShippingMethodsStep(shippingMethodsData.shippingMethodsToUpdate) + ) + }) + } +) diff --git a/packages/core/core-flows/src/cart/workflows/update-line-item-in-cart.ts b/packages/core/core-flows/src/cart/workflows/update-line-item-in-cart.ts index edfe4320338ec..528c4b01a4376 100644 --- a/packages/core/core-flows/src/cart/workflows/update-line-item-in-cart.ts +++ b/packages/core/core-flows/src/cart/workflows/update-line-item-in-cart.ts @@ -1,28 +1,16 @@ import { UpdateLineItemInCartWorkflowInputDTO } from "@medusajs/framework/types" -import { CartWorkflowEvents } from "@medusajs/framework/utils" import { WorkflowData, - WorkflowResponse, createWorkflow, - parallelize, transform, } from "@medusajs/framework/workflows-sdk" -import { emitEventStep } from "../../common/steps/emit-event" import { useRemoteQueryStep } from "../../common/steps/use-remote-query" import { updateLineItemsStepWithSelector } from "../../line-item/steps" -import { refreshCartShippingMethodsStep } from "../steps" import { validateCartStep } from "../steps/validate-cart" import { validateVariantPricesStep } from "../steps/validate-variant-prices" -import { - cartFieldsForRefreshSteps, - productVariantsFields, -} from "../utils/fields" +import { productVariantsFields } from "../utils/fields" import { confirmVariantInventoryWorkflow } from "./confirm-variant-inventory" -import { refreshPaymentCollectionForCartWorkflow } from "./refresh-payment-collection" -import { updateCartPromotionsWorkflow } from "./update-cart-promotions" - -// TODO: The UpdateLineItemsWorkflow are missing the following steps: -// - Validate shipping methods for new items (fulfillment module) +import { refreshCartItemsWorkflow } from "./refresh-cart-items" export const updateLineItemInCartWorkflowId = "update-line-item-in-cart" /** @@ -89,35 +77,10 @@ export const updateLineItemInCartWorkflow = createWorkflow( } }) - const result = updateLineItemsStepWithSelector(lineItemUpdate) - - const cart = useRemoteQueryStep({ - entry_point: "cart", - fields: cartFieldsForRefreshSteps, - variables: { id: input.cart.id }, - list: false, - }).config({ name: "refetch–cart" }) + updateLineItemsStepWithSelector(lineItemUpdate) - refreshCartShippingMethodsStep({ cart }) - - updateCartPromotionsWorkflow.runAsStep({ - input: { - cart_id: input.cart.id, - }, + refreshCartItemsWorkflow.runAsStep({ + input: { cart_id: input.cart.id }, }) - - parallelize( - refreshPaymentCollectionForCartWorkflow.runAsStep({ - input: { cart_id: input.cart.id }, - }), - emitEventStep({ - eventName: CartWorkflowEvents.UPDATED, - data: { id: input.cart.id }, - }) - ) - - const updatedItem = transform({ result }, (data) => data.result?.[0]) - - return new WorkflowResponse(updatedItem) } ) diff --git a/packages/core/types/src/cart/mutations.ts b/packages/core/types/src/cart/mutations.ts index a8e13860c8ea5..3d51cd8cb6051 100644 --- a/packages/core/types/src/cart/mutations.ts +++ b/packages/core/types/src/cart/mutations.ts @@ -767,6 +767,11 @@ export interface UpdateShippingMethodDTO { */ amount?: BigNumberInput + /** + * The tax inclusivity setting of the shipping method. + */ + is_tax_inclusive?: boolean + /** * The data of the shipping method. */ diff --git a/packages/core/types/src/cart/service.ts b/packages/core/types/src/cart/service.ts index cc2f31532618e..f9f42d41abc7d 100644 --- a/packages/core/types/src/cart/service.ts +++ b/packages/core/types/src/cart/service.ts @@ -38,6 +38,7 @@ import { UpdateLineItemTaxLineDTO, UpdateLineItemWithSelectorDTO, UpdateShippingMethodAdjustmentDTO, + UpdateShippingMethodDTO, UpdateShippingMethodTaxLineDTO, UpsertLineItemAdjustmentDTO, } from "./mutations" @@ -822,6 +823,46 @@ export interface ICartModuleService extends IModuleService { sharedContext?: Context ): Promise + /** + * This method updates existing shipping methods. + * + * @param {UpdateShippingMethodDTO[]} data - A list of shipping methods to update + * @returns {Promise} The updated shipping methods. + * + * @example + * const shippingMethods = await cartModuleService.updateShippingMethods([ + * { + * id: "casm_123", + * amount: 2, + * }, + * ]) + */ + updateShippingMethods( + data: UpdateShippingMethodDTO[] + ): Promise + + /** + * This method updates an existing shipping method. + * + * @param {string} shippingMethodId - The shipping methods's ID. + * @param {Partial} data - The attributes to update in the shipping method. + * @param {Context} sharedContext - A context used to share resources, such as transaction manager, between the application and the module. + * @returns {Promise} The updated shipping method. + * + * @example + * const lineItem = await cartModuleService.updateShippingMethods( + * "casm_123", + * { + * amount: 3000, + * } + * ) + */ + updateShippingMethods( + shippingMethodId: string, + data: Partial, + sharedContext?: Context + ): Promise + /** * This method retrieves a paginated list of line item adjustments based on optional filters and configuration. * diff --git a/packages/medusa/src/api/admin/shipping-options/validators.ts b/packages/medusa/src/api/admin/shipping-options/validators.ts index e63e6215abe95..61a8b59dc495e 100644 --- a/packages/medusa/src/api/admin/shipping-options/validators.ts +++ b/packages/medusa/src/api/admin/shipping-options/validators.ts @@ -86,7 +86,7 @@ export const AdminCreateShippingOptionTypeObject = z const AdminPriceRules = z.array( z.object({ - attribute: z.literal("total"), + attribute: z.literal("item_total"), operator: z.nativeEnum(PricingRuleOperator), value: z.number(), }) diff --git a/packages/modules/fulfillment/integration-tests/__tests__/fulfillment-module-service/shipping-option.spec.ts b/packages/modules/fulfillment/integration-tests/__tests__/fulfillment-module-service/shipping-option.spec.ts index 72f73687f529a..109c23d606148 100644 --- a/packages/modules/fulfillment/integration-tests/__tests__/fulfillment-module-service/shipping-option.spec.ts +++ b/packages/modules/fulfillment/integration-tests/__tests__/fulfillment-module-service/shipping-option.spec.ts @@ -8,11 +8,11 @@ import { GeoZoneType, Modules, } from "@medusajs/framework/utils" -import { FulfillmentProviderService } from "@services" import { MockEventBusService, moduleIntegrationTestRunner, } from "@medusajs/test-utils" +import { FulfillmentProviderService } from "@services" import { resolve } from "path" import { buildExpectedEventMessageShape, From a1a1e0e789424546443ce195b95f652d081d7b3b Mon Sep 17 00:00:00 2001 From: Kasper Fabricius Kristensen <45367945+kasperkristensen@users.noreply.github.com> Date: Mon, 9 Dec 2024 09:44:11 +0100 Subject: [PATCH 09/11] feat(dashboard): SO cart item total rules UI (#10386) --- .changeset/dry-cheetahs-wait.md | 6 + .../components/common/tax-badge/tax-badge.tsx | 12 +- .../components/data-grid-cell-container.tsx | 84 ++- .../data-grid/components/data-grid-root.tsx | 8 +- .../hooks/use-data-grid-keydown-event.tsx | 6 +- .../src/components/data-grid/types.ts | 1 + .../stacked-focus-modal.tsx | 20 +- .../src/i18n/translations/$schema.json | 147 +++- .../dashboard/src/i18n/translations/en.json | 33 + .../conditional-price-form.tsx | 696 ++++++++++++++++++ .../conditional-price-form/index.ts | 1 + .../shipping-option-price-cell/index.ts | 1 + .../shipping-option-price-cell.tsx | 261 +++++++ .../shipping-option-price-provider/index.ts | 2 + .../shipping-option-price-context.tsx | 10 + .../shipping-option-price-provider.tsx | 23 + .../use-shipping-option-price.tsx | 14 + .../src/routes/locations/common/constants.ts | 5 + .../use-shipping-option-price-columns.tsx | 123 +++- .../src/routes/locations/common/schema.ts | 90 +++ .../src/routes/locations/common/types.ts | 12 + ...custom-shipping-option-price-field-info.ts | 19 + .../common/utils/price-rule-helpers.ts | 41 ++ ...location-fulfillment-providers-section.tsx | 5 +- .../create-service-zone-form.tsx | 3 - .../create-shipping-option-details-form.tsx | 2 +- .../create-shipping-options-form.tsx | 95 ++- .../create-shipping-options-prices-form.tsx | 67 +- .../create-shipping-options-form/schema.ts | 21 + .../edit-shipping-options-pricing-form.tsx | 287 +++++--- ...n-service-zone-shipping-option-pricing.tsx | 11 +- .../http/shipping-option/admin/entities.ts | 9 +- .../http/shipping-option/admin/payloads.ts | 22 +- 33 files changed, 1947 insertions(+), 190 deletions(-) create mode 100644 .changeset/dry-cheetahs-wait.md create mode 100644 packages/admin/dashboard/src/routes/locations/common/components/conditional-price-form/conditional-price-form.tsx create mode 100644 packages/admin/dashboard/src/routes/locations/common/components/conditional-price-form/index.ts create mode 100644 packages/admin/dashboard/src/routes/locations/common/components/shipping-option-price-cell/index.ts create mode 100644 packages/admin/dashboard/src/routes/locations/common/components/shipping-option-price-cell/shipping-option-price-cell.tsx create mode 100644 packages/admin/dashboard/src/routes/locations/common/components/shipping-option-price-provider/index.ts create mode 100644 packages/admin/dashboard/src/routes/locations/common/components/shipping-option-price-provider/shipping-option-price-context.tsx create mode 100644 packages/admin/dashboard/src/routes/locations/common/components/shipping-option-price-provider/shipping-option-price-provider.tsx create mode 100644 packages/admin/dashboard/src/routes/locations/common/components/shipping-option-price-provider/use-shipping-option-price.tsx create mode 100644 packages/admin/dashboard/src/routes/locations/common/schema.ts create mode 100644 packages/admin/dashboard/src/routes/locations/common/types.ts create mode 100644 packages/admin/dashboard/src/routes/locations/common/utils/get-custom-shipping-option-price-field-info.ts create mode 100644 packages/admin/dashboard/src/routes/locations/common/utils/price-rule-helpers.ts diff --git a/.changeset/dry-cheetahs-wait.md b/.changeset/dry-cheetahs-wait.md new file mode 100644 index 0000000000000..5da63fb7c3298 --- /dev/null +++ b/.changeset/dry-cheetahs-wait.md @@ -0,0 +1,6 @@ +--- +"@medusajs/dashboard": patch +"@medusajs/types": patch +--- + +feat(dashboard,types): Add UI to manage conditional SO prices diff --git a/packages/admin/dashboard/src/components/common/tax-badge/tax-badge.tsx b/packages/admin/dashboard/src/components/common/tax-badge/tax-badge.tsx index 60801b9ccaef9..c8ca429586255 100644 --- a/packages/admin/dashboard/src/components/common/tax-badge/tax-badge.tsx +++ b/packages/admin/dashboard/src/components/common/tax-badge/tax-badge.tsx @@ -1,5 +1,5 @@ -import { BuildingTax } from "@medusajs/icons" -import { Tooltip, clx } from "@medusajs/ui" +import { TaxExclusive, TaxInclusive } from "@medusajs/icons" +import { Tooltip } from "@medusajs/ui" import { useTranslation } from "react-i18next" type IncludesTaxTooltipProps = { @@ -20,9 +20,11 @@ export const IncludesTaxTooltip = ({ : t("general.excludesTaxTooltip") } > - + {includesTax ? ( + + ) : ( + + )} ) } diff --git a/packages/admin/dashboard/src/components/data-grid/components/data-grid-cell-container.tsx b/packages/admin/dashboard/src/components/data-grid/components/data-grid-cell-container.tsx index 56a0861556e6a..f9e2908d32114 100644 --- a/packages/admin/dashboard/src/components/data-grid/components/data-grid-cell-container.tsx +++ b/packages/admin/dashboard/src/components/data-grid/components/data-grid-cell-container.tsx @@ -19,52 +19,56 @@ export const DataGridCellContainer = ({ children, errors, rowErrors, + outerComponent, }: DataGridCellContainerProps & DataGridErrorRenderProps) => { const error = get(errors, field) const hasError = !!error return ( -
- { - return ( -
- - - -
- ) - }} - /> -
- - {children} - -
- - {showOverlay && ( -
+
+ { + return ( +
+ + + +
+ ) + }} /> - )} +
+ + {children} + +
+ + {showOverlay && ( +
+ )} +
+ {outerComponent}
) } diff --git a/packages/admin/dashboard/src/components/data-grid/components/data-grid-root.tsx b/packages/admin/dashboard/src/components/data-grid/components/data-grid-root.tsx index 63001cd8a05b8..d8d62536bb717 100644 --- a/packages/admin/dashboard/src/components/data-grid/components/data-grid-root.tsx +++ b/packages/admin/dashboard/src/components/data-grid/components/data-grid-root.tsx @@ -57,6 +57,7 @@ export interface DataGridRootProps< state: UseFormReturn getSubRows?: (row: TData) => TData[] | undefined onEditingChange?: (isEditing: boolean) => void + disableInteractions?: boolean } const ROW_HEIGHT = 40 @@ -102,6 +103,7 @@ export const DataGridRoot = < state, getSubRows, onEditingChange, + disableInteractions, }: DataGridRootProps) => { const containerRef = useRef(null) @@ -114,7 +116,9 @@ export const DataGridRoot = < formState: { errors }, } = state - const [trapActive, setTrapActive] = useState(true) + const [internalTrapActive, setTrapActive] = useState(true) + + const trapActive = !disableInteractions && internalTrapActive const [anchor, setAnchor] = useState(null) const [rangeEnd, setRangeEnd] = useState(null) @@ -533,7 +537,7 @@ export const DataGridRoot = < queryTool?.getContainer(anchor)?.focus() }) } - }, [anchor, trapActive, queryTool]) + }, [anchor, trapActive, setSingleRange, scrollToCoordinates, queryTool]) return ( diff --git a/packages/admin/dashboard/src/components/data-grid/hooks/use-data-grid-keydown-event.tsx b/packages/admin/dashboard/src/components/data-grid/hooks/use-data-grid-keydown-event.tsx index 86e2a7f890d66..3e7da7b1a2a3e 100644 --- a/packages/admin/dashboard/src/components/data-grid/hooks/use-data-grid-keydown-event.tsx +++ b/packages/admin/dashboard/src/components/data-grid/hooks/use-data-grid-keydown-event.tsx @@ -46,7 +46,7 @@ const VERTICAL_KEYS = ["ArrowUp", "ArrowDown"] export const useDataGridKeydownEvent = < TData, - TFieldValues extends FieldValues, + TFieldValues extends FieldValues >({ containerRef, matrix, @@ -108,8 +108,8 @@ export const useDataGridKeydownEvent = < direction === "horizontal" ? setSingleRange : e.shiftKey - ? setRangeEnd - : setSingleRange + ? setRangeEnd + : setSingleRange if (!basis) { return diff --git a/packages/admin/dashboard/src/components/data-grid/types.ts b/packages/admin/dashboard/src/components/data-grid/types.ts index 5286a7a06df8b..3f59ecbb70f45 100644 --- a/packages/admin/dashboard/src/components/data-grid/types.ts +++ b/packages/admin/dashboard/src/components/data-grid/types.ts @@ -96,6 +96,7 @@ export interface DataGridCellContainerProps extends PropsWithChildren<{}> { isDragSelected: boolean placeholder?: ReactNode showOverlay: boolean + outerComponent?: ReactNode } export type DataGridCellSnapshot< diff --git a/packages/admin/dashboard/src/components/modals/stacked-focus-modal/stacked-focus-modal.tsx b/packages/admin/dashboard/src/components/modals/stacked-focus-modal/stacked-focus-modal.tsx index 599b14861da3e..92764504270a8 100644 --- a/packages/admin/dashboard/src/components/modals/stacked-focus-modal/stacked-focus-modal.tsx +++ b/packages/admin/dashboard/src/components/modals/stacked-focus-modal/stacked-focus-modal.tsx @@ -13,12 +13,20 @@ type StackedFocusModalProps = PropsWithChildren<{ * when multiple stacked modals are registered to the same parent modal. */ id: string + /** + * An optional callback that is called when the modal is opened or closed. + */ + onOpenChangeCallback?: (open: boolean) => void }> /** * A stacked modal that can be rendered above a parent modal. */ -export const Root = ({ id, children }: StackedFocusModalProps) => { +export const Root = ({ + id, + onOpenChangeCallback, + children, +}: StackedFocusModalProps) => { const { register, unregister, getIsOpen, setIsOpen } = useStackedModal() useEffect(() => { @@ -28,11 +36,13 @@ export const Root = ({ id, children }: StackedFocusModalProps) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, []) + const handleOpenChange = (open: boolean) => { + setIsOpen(id, open) + onOpenChangeCallback?.(open) + } + return ( - setIsOpen(id, open)} - > + {children} ) diff --git a/packages/admin/dashboard/src/i18n/translations/$schema.json b/packages/admin/dashboard/src/i18n/translations/$schema.json index be9d4f399ab35..9e3667c732043 100644 --- a/packages/admin/dashboard/src/i18n/translations/$schema.json +++ b/packages/admin/dashboard/src/i18n/translations/$schema.json @@ -5544,6 +5544,144 @@ "required": ["action"], "additionalProperties": false }, + "conditionalPrices": { + "type": "object", + "properties": { + "header": { + "type": "string" + }, + "description": { + "type": "string" + }, + "attributes": { + "type": "object", + "properties": { + "cartItemTotal": { + "type": "string" + } + }, + "required": [ + "cartItemTotal" + ], + "additionalProperties": false + }, + "summaries": { + "type": "object", + "properties": { + "range": { + "type": "string" + }, + "greaterThan": { + "type": "string" + }, + "lessThan": { + "type": "string" + } + }, + "required": [ + "range", + "greaterThan", + "lessThan" + ], + "additionalProperties": false + }, + "actions": { + "type": "object", + "properties": { + "addPrice": { + "type": "string" + }, + "manageConditionalPrices": { + "type": "string" + } + }, + "required": [ + "addPrice", + "manageConditionalPrices" + ], + "additionalProperties": false + }, + "rules": { + "type": "object", + "properties": { + "amount": { + "type": "string" + }, + "gte": { + "type": "string" + }, + "lte": { + "type": "string" + } + }, + "required": [ + "amount", + "gte", + "lte" + ], + "additionalProperties": false + }, + "customRules": { + "type": "object", + "properties": { + "label": { + "type": "string" + }, + "tooltip": { + "type": "string" + }, + "eq": { + "type": "string" + }, + "gt": { + "type": "string" + }, + "lt": { + "type": "string" + } + }, + "required": [ + "label", + "tooltip", + "eq", + "gt", + "lt" + ], + "additionalProperties": false + }, + "errors": { + "type": "object", + "properties": { + "amountRequired": { + "type": "string" + }, + "minOrMaxRequired": { + "type": "string" + }, + "minGreaterThanMax": { + "type": "string" + } + }, + "required": [ + "amountRequired", + "minOrMaxRequired", + "minGreaterThanMax" + ], + "additionalProperties": false + } + }, + "required": [ + "header", + "description", + "attributes", + "summaries", + "actions", + "rules", + "customRules", + "errors" + ], + "additionalProperties": false + }, "fields": { "type": "object", "properties": { @@ -5644,7 +5782,14 @@ "additionalProperties": false } }, - "required": ["create", "delete", "edit", "pricing", "fields"], + "required": [ + "create", + "delete", + "edit", + "pricing", + "conditionalPrices", + "fields" + ], "additionalProperties": false }, "serviceZones": { diff --git a/packages/admin/dashboard/src/i18n/translations/en.json b/packages/admin/dashboard/src/i18n/translations/en.json index 4b46b2556e7d8..056492c9f1bc3 100644 --- a/packages/admin/dashboard/src/i18n/translations/en.json +++ b/packages/admin/dashboard/src/i18n/translations/en.json @@ -1463,6 +1463,39 @@ "pricing": { "action": "Edit prices" }, + "conditionalPrices": { + "header": "Conditional Prices for {{name}}", + "description": "Manage the conditional prices for this shipping option based on the cart item total.", + "attributes": { + "cartItemTotal": "Cart item total" + }, + "summaries": { + "range": "If <0>{{attribute}} is between <1>{{gte}} and <2>{{lte}}", + "greaterThan": "If <0>{{attribute}} ≥ <1>{{gte}}", + "lessThan": "If <0>{{attribute}} ≤ <1>{{lte}}" + }, + "actions": { + "addPrice": "Add price", + "manageConditionalPrices": "Manage conditional prices" + }, + "rules": { + "amount": "Shipping option price", + "gte": "Minimum cart item total", + "lte": "Maximum cart item total" + }, + "customRules": { + "label": "Custom rules", + "tooltip": "This conditional price has rules that cannot be managed in the dashboard.", + "eq": "Cart item total must equal", + "gt": "Cart item total must be greater than", + "lt": "Cart item total must be less than" + }, + "errors": { + "amountRequired": "Shipping option price is required", + "minOrMaxRequired": "At least one of minimum or maximum cart item total must be provided", + "minGreaterThanMax": "Minimum cart item total must be less than or equal to maximum cart item total" + } + }, "fields": { "count": { "shipping_one": "{{count}} shipping option", diff --git a/packages/admin/dashboard/src/routes/locations/common/components/conditional-price-form/conditional-price-form.tsx b/packages/admin/dashboard/src/routes/locations/common/components/conditional-price-form/conditional-price-form.tsx new file mode 100644 index 0000000000000..d58764defe46f --- /dev/null +++ b/packages/admin/dashboard/src/routes/locations/common/components/conditional-price-form/conditional-price-form.tsx @@ -0,0 +1,696 @@ +import { zodResolver } from "@hookform/resolvers/zod" +import { + InformationCircleSolid, + Plus, + TriangleDownMini, + XMark, + XMarkMini, +} from "@medusajs/icons" +import { + Badge, + Button, + CurrencyInput, + Heading, + IconButton, + Label, + Text, + Tooltip, +} from "@medusajs/ui" +import * as Accordion from "@radix-ui/react-accordion" +import React, { Fragment, ReactNode, useRef, useState } from "react" +import { + Control, + ControllerRenderProps, + useFieldArray, + useForm, + useFormContext, + useWatch, +} from "react-hook-form" +import { Trans, useTranslation } from "react-i18next" + +import { formatValue } from "react-currency-input-field" +import { Divider } from "../../../../../components/common/divider" +import { Form } from "../../../../../components/common/form" +import { StackedFocusModal } from "../../../../../components/modals" +import { KeyboundForm } from "../../../../../components/utilities/keybound-form" +import { useCombinedRefs } from "../../../../../hooks/use-combined-refs" +import { castNumber } from "../../../../../lib/cast-number" +import { CurrencyInfo } from "../../../../../lib/data/currencies" +import { getLocaleAmount } from "../../../../../lib/money-amount-helpers" +import { CreateShippingOptionSchemaType } from "../../../location-service-zone-shipping-option-create/components/create-shipping-options-form/schema" +import { + CondtionalPriceRuleSchema, + CondtionalPriceRuleSchemaType, + UpdateConditionalPriceRuleSchema, + UpdateConditionalPriceRuleSchemaType, +} from "../../schema" +import { ConditionalPriceInfo } from "../../types" +import { getCustomShippingOptionPriceFieldName } from "../../utils/get-custom-shipping-option-price-field-info" +import { useShippingOptionPrice } from "../shipping-option-price-provider" + +const RULE_ITEM_PREFIX = "rule-item" + +const getRuleValue = (index: number) => `${RULE_ITEM_PREFIX}-${index}` + +interface ConditionalPriceFormProps { + info: ConditionalPriceInfo + variant: "create" | "update" +} + +export const ConditionalPriceForm = ({ + info, + variant, +}: ConditionalPriceFormProps) => { + const { t } = useTranslation() + const { getValues, setValue: setFormValue } = + useFormContext() + const { onCloseConditionalPricesModal } = useShippingOptionPrice() + + const [value, setValue] = useState([getRuleValue(0)]) + + const { field, type, currency, name: header } = info + + const name = getCustomShippingOptionPriceFieldName(field, type) + + const conditionalPriceForm = useForm< + CondtionalPriceRuleSchemaType | UpdateConditionalPriceRuleSchemaType + >({ + defaultValues: { + prices: getValues(name) || [ + { + amount: "", + gte: "", + lte: null, + }, + ], + }, + resolver: zodResolver( + variant === "create" + ? CondtionalPriceRuleSchema + : UpdateConditionalPriceRuleSchema + ), + }) + + const { fields, append, remove } = useFieldArray({ + control: conditionalPriceForm.control, + name: "prices", + }) + + const handleAdd = () => { + append({ + amount: "", + gte: "", + lte: null, + }) + + setValue([...value, getRuleValue(fields.length)]) + } + + const handleRemove = (index: number) => { + remove(index) + } + + const handleOnSubmit = conditionalPriceForm.handleSubmit((values) => { + setFormValue(name, values.prices, { + shouldDirty: true, + shouldValidate: true, + shouldTouch: true, + }) + onCloseConditionalPricesModal() + }) + + // Intercept the Cmd + Enter key to only save the inner form. + const handleOnKeyDown = (event: React.KeyboardEvent) => { + if (event.key === "Enter" && (event.metaKey || event.ctrlKey)) { + console.log("Fired") + + event.preventDefault() + event.stopPropagation() + + handleOnSubmit() + } + } + + return ( +
+ + + + +
+
+
+
+ + + {t( + "stockLocations.shippingOptions.conditionalPrices.header", + { + name: header, + } + )} + + + + + {t( + "stockLocations.shippingOptions.conditionalPrices.description" + )} + + +
+ + {fields.map((field, index) => ( + + ))} + +
+ +
+
+
+
+
+ +
+ + + + +
+
+
+
+ + ) +} + +interface ConditionalPriceListProps { + children?: ReactNode + value: string[] + onValueChange: (value: string[]) => void +} + +const ConditionalPriceList = ({ + children, + value, + onValueChange, +}: ConditionalPriceListProps) => { + return ( + + {children} + + ) +} + +interface ConditionalPriceItemProps { + index: number + currency: CurrencyInfo + onRemove: (index: number) => void + control: Control +} + +const ConditionalPriceItem = ({ + index, + currency, + onRemove, + control, +}: ConditionalPriceItemProps) => { + const { t } = useTranslation() + + const handleRemove = (e: React.MouseEvent) => { + e.stopPropagation() + onRemove(index) + } + + return ( + + +
+
+
+ +
+
+ +
+
+
+ + + + + + +
+
+
+ + + { + return ( + +
+
+ + {t( + "stockLocations.shippingOptions.conditionalPrices.rules.amount" + )} + +
+
+ + + onChange(values?.value ? values?.value : "") + } + autoFocus + {...props} + /> + + +
+
+
+ ) + }} + /> + + { + return ( + + ) + }} + /> + + { + return ( + + ) + }} + /> + +
+
+ ) +} + +interface OperatorInputProps { + currency: CurrencyInfo + placeholder: string + label: string + field: ControllerRenderProps< + CondtionalPriceRuleSchemaType, + `prices.${number}.lte` | `prices.${number}.gte` + > +} + +const OperatorInput = ({ + field, + label, + currency, + placeholder, +}: OperatorInputProps) => { + const innerRef = useRef(null) + + const { value, onChange, ref, ...props } = field + + const refs = useCombinedRefs(innerRef, ref) + + const action = () => { + if (value === null) { + onChange("") + + requestAnimationFrame(() => { + innerRef.current?.focus() + }) + + return + } + + onChange(null) + } + + const isNull = value === null + + return ( + +
+
+ + {isNull ? : } + + {label} +
+ {!isNull && ( +
+ + + onChange(values?.value ? values?.value : "") + } + {...props} + /> + + +
+ )} +
+
+ ) +} + +const ReadOnlyConditions = ({ + index, + control, + currency, +}: { + index: number + control: Control + currency: CurrencyInfo +}) => { + const { t } = useTranslation() + + const item = useWatch({ + control, + name: `prices.${index}`, + }) + + if (item.eq == null && item.gt == null && item.lt == null) { + return null + } + + return ( +
+ +
+ + {t( + "stockLocations.shippingOptions.conditionalPrices.customRules.label" + )} + + + + +
+
+ {item.eq != null && ( +
+
+ +
+ +
+ )} + {item.gt != null && ( + + +
+
+ +
+ +
+
+ )} + {item.lt != null && ( + + +
+
+ +
+ +
+
+ )} +
+
+ ) +} + +const AmountDisplay = ({ + index, + currency, + control, +}: { + index: number + currency: CurrencyInfo + control: Control +}) => { + const amount = useWatch({ + control, + name: `prices.${index}.amount`, + }) + + if (amount === "" || amount === undefined) { + return ( + + - + + ) + } + + const castAmount = castNumber(amount) + + return ( + + {getLocaleAmount(castAmount, currency.code)} + + ) +} + +const ConditionContainer = ({ children }: { children: ReactNode }) => ( +
+ {children} +
+) + +const ConditionDisplay = ({ + index, + currency, + control, +}: { + index: number + currency: CurrencyInfo + control: Control +}) => { + const { t, i18n } = useTranslation() + + const gte = useWatch({ + control, + name: `prices.${index}.gte`, + }) + + const lte = useWatch({ + control, + name: `prices.${index}.lte`, + }) + + const renderCondition = () => { + const castGte = gte ? castNumber(gte) : undefined + const castLte = lte ? castNumber(lte) : undefined + + if (!castGte && !castLte) { + return null + } + + if (castGte && !castLte) { + return ( + + , + , + ]} + values={{ + attribute: t( + "stockLocations.shippingOptions.conditionalPrices.attributes.cartItemTotal" + ), + gte: getLocaleAmount(castGte, currency.code), + }} + /> + + ) + } + + if (!castGte && castLte) { + return ( + + , + , + ]} + values={{ + attribute: t( + "stockLocations.shippingOptions.conditionalPrices.attributes.cartItemTotal" + ), + lte: getLocaleAmount(castLte, currency.code), + }} + /> + + ) + } + + if (castGte && castLte) { + return ( + + , + , + , + ]} + values={{ + attribute: t( + "stockLocations.shippingOptions.conditionalPrices.attributes.cartItemTotal" + ), + gte: getLocaleAmount(castGte, currency.code), + lte: getLocaleAmount(castLte, currency.code), + }} + /> + + ) + } + + return null + } + + return renderCondition() +} diff --git a/packages/admin/dashboard/src/routes/locations/common/components/conditional-price-form/index.ts b/packages/admin/dashboard/src/routes/locations/common/components/conditional-price-form/index.ts new file mode 100644 index 0000000000000..b184d947d1c4d --- /dev/null +++ b/packages/admin/dashboard/src/routes/locations/common/components/conditional-price-form/index.ts @@ -0,0 +1 @@ +export * from "./conditional-price-form" diff --git a/packages/admin/dashboard/src/routes/locations/common/components/shipping-option-price-cell/index.ts b/packages/admin/dashboard/src/routes/locations/common/components/shipping-option-price-cell/index.ts new file mode 100644 index 0000000000000..df3d6951d3f7c --- /dev/null +++ b/packages/admin/dashboard/src/routes/locations/common/components/shipping-option-price-cell/index.ts @@ -0,0 +1 @@ +export * from "./shipping-option-price-cell" diff --git a/packages/admin/dashboard/src/routes/locations/common/components/shipping-option-price-cell/shipping-option-price-cell.tsx b/packages/admin/dashboard/src/routes/locations/common/components/shipping-option-price-cell/shipping-option-price-cell.tsx new file mode 100644 index 0000000000000..856e4ad93957b --- /dev/null +++ b/packages/admin/dashboard/src/routes/locations/common/components/shipping-option-price-cell/shipping-option-price-cell.tsx @@ -0,0 +1,261 @@ +import { ArrowsPointingOut, CircleSliders } from "@medusajs/icons" +import { clx } from "@medusajs/ui" +import { useCallback, useEffect, useRef, useState } from "react" +import CurrencyInput, { + CurrencyInputProps, + formatValue, +} from "react-currency-input-field" +import { + Control, + Controller, + ControllerRenderProps, + useWatch, +} from "react-hook-form" +import { DataGridCellContainer } from "../../../../../components/data-grid/components/data-grid-cell-container" +import { + useDataGridCell, + useDataGridCellError, +} from "../../../../../components/data-grid/hooks" +import { + DataGridCellProps, + InputProps, +} from "../../../../../components/data-grid/types" +import { useCombinedRefs } from "../../../../../hooks/use-combined-refs" +import { currencies, CurrencyInfo } from "../../../../../lib/data/currencies" +import { getCustomShippingOptionPriceFieldName } from "../../utils/get-custom-shipping-option-price-field-info" +import { useShippingOptionPrice } from "../shipping-option-price-provider" + +interface ShippingOptionPriceCellProps + extends DataGridCellProps { + code: string + header: string + type: "currency" | "region" +} + +export const ShippingOptionPriceCell = ({ + context, + code, + header, + type, +}: ShippingOptionPriceCellProps) => { + const [symbolWidth, setSymbolWidth] = useState(0) + + const measuredRef = useCallback((node: HTMLSpanElement) => { + if (node) { + const width = node.offsetWidth + setSymbolWidth(width) + } + }, []) + + const { field, control, renderProps } = useDataGridCell({ + context, + }) + + const errorProps = useDataGridCellError({ context }) + + const { container, input } = renderProps + const { isAnchor } = container + + const currency = currencies[code.toUpperCase()] + + return ( + { + return ( + + } + > + + + ) + }} + /> + ) +} + +const OuterComponent = ({ + isAnchor, + header, + field, + control, + symbolWidth, + type, + currency, +}: { + isAnchor: boolean + header: string + field: string + control: Control + symbolWidth: number + type: "currency" | "region" + currency: CurrencyInfo +}) => { + const { onOpenConditionalPricesModal } = useShippingOptionPrice() + + const buttonRef = useRef(null) + + const name = getCustomShippingOptionPriceFieldName(field, type) + const price = useWatch({ control, name }) + + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (isAnchor && (e.metaKey || e.ctrlKey) && e.key.toLowerCase() === "b") { + e.preventDefault() + buttonRef.current?.click() + } + } + + document.addEventListener("keydown", handleKeyDown) + return () => document.removeEventListener("keydown", handleKeyDown) + }, [isAnchor]) + + return ( +
+ {price?.length > 0 && !isAnchor && ( +
+ +
+ )} + +
+ ) +} + +const Inner = ({ + field, + onMeasureSymbol, + inputProps, + currencyInfo, +}: { + field: ControllerRenderProps + onMeasureSymbol: (node: HTMLSpanElement) => void + inputProps: InputProps + currencyInfo: CurrencyInfo +}) => { + const { value, onChange: _, onBlur, ref, ...rest } = field + const { + ref: inputRef, + onBlur: onInputBlur, + onFocus, + onChange, + ...attributes + } = inputProps + + const formatter = useCallback( + (value?: string | number) => { + const ensuredValue = + typeof value === "number" ? value.toString() : value || "" + + return formatValue({ + value: ensuredValue, + decimalScale: currencyInfo.decimal_digits, + disableGroupSeparators: true, + decimalSeparator: ".", + }) + }, + [currencyInfo] + ) + + const [localValue, setLocalValue] = useState(value || "") + + const handleValueChange: CurrencyInputProps["onValueChange"] = ( + value, + _name, + _values + ) => { + if (!value) { + setLocalValue("") + return + } + + setLocalValue(value) + } + + useEffect(() => { + let update = value + + // The component we use is a bit fidly when the value is updated externally + // so we need to ensure a format that will result in the cell being formatted correctly + // according to the users locale on the next render. + if (!isNaN(Number(value))) { + update = formatter(update) + } + + setLocalValue(update) + }, [value, formatter]) + + const combinedRed = useCombinedRefs(inputRef, ref) + + return ( +
+ + {currencyInfo.symbol_native} + + { + onBlur() + onInputBlur() + + onChange(localValue, value) + }} + onFocus={onFocus} + decimalScale={currencyInfo.decimal_digits} + decimalsLimit={currencyInfo.decimal_digits} + autoComplete="off" + tabIndex={-1} + /> +
+ ) +} diff --git a/packages/admin/dashboard/src/routes/locations/common/components/shipping-option-price-provider/index.ts b/packages/admin/dashboard/src/routes/locations/common/components/shipping-option-price-provider/index.ts new file mode 100644 index 0000000000000..2d0cdea4ce397 --- /dev/null +++ b/packages/admin/dashboard/src/routes/locations/common/components/shipping-option-price-provider/index.ts @@ -0,0 +1,2 @@ +export * from "./shipping-option-price-provider" +export * from "./use-shipping-option-price" diff --git a/packages/admin/dashboard/src/routes/locations/common/components/shipping-option-price-provider/shipping-option-price-context.tsx b/packages/admin/dashboard/src/routes/locations/common/components/shipping-option-price-provider/shipping-option-price-context.tsx new file mode 100644 index 0000000000000..8c80fc7632932 --- /dev/null +++ b/packages/admin/dashboard/src/routes/locations/common/components/shipping-option-price-provider/shipping-option-price-context.tsx @@ -0,0 +1,10 @@ +import { createContext } from "react" +import { ConditionalPriceInfo } from "../../types" + +type ShippingOptionPriceContextType = { + onOpenConditionalPricesModal: (info: ConditionalPriceInfo) => void + onCloseConditionalPricesModal: () => void +} + +export const ShippingOptionPriceContext = + createContext(null) diff --git a/packages/admin/dashboard/src/routes/locations/common/components/shipping-option-price-provider/shipping-option-price-provider.tsx b/packages/admin/dashboard/src/routes/locations/common/components/shipping-option-price-provider/shipping-option-price-provider.tsx new file mode 100644 index 0000000000000..88d26abf888b7 --- /dev/null +++ b/packages/admin/dashboard/src/routes/locations/common/components/shipping-option-price-provider/shipping-option-price-provider.tsx @@ -0,0 +1,23 @@ +import { ShippingOptionPriceContext } from "./shipping-option-price-context" + +import { PropsWithChildren } from "react" +import { ConditionalPriceInfo } from "../../types" + +type ShippingOptionPriceProviderProps = PropsWithChildren<{ + onOpenConditionalPricesModal: (info: ConditionalPriceInfo) => void + onCloseConditionalPricesModal: () => void +}> + +export const ShippingOptionPriceProvider = ({ + children, + onOpenConditionalPricesModal, + onCloseConditionalPricesModal, +}: ShippingOptionPriceProviderProps) => { + return ( + + {children} + + ) +} diff --git a/packages/admin/dashboard/src/routes/locations/common/components/shipping-option-price-provider/use-shipping-option-price.tsx b/packages/admin/dashboard/src/routes/locations/common/components/shipping-option-price-provider/use-shipping-option-price.tsx new file mode 100644 index 0000000000000..214ba4d89308a --- /dev/null +++ b/packages/admin/dashboard/src/routes/locations/common/components/shipping-option-price-provider/use-shipping-option-price.tsx @@ -0,0 +1,14 @@ +import { useContext } from "react" +import { ShippingOptionPriceContext } from "./shipping-option-price-context" + +export const useShippingOptionPrice = () => { + const context = useContext(ShippingOptionPriceContext) + + if (!context) { + throw new Error( + "useShippingOptionPrice must be used within a ShippingOptionPriceProvider" + ) + } + + return context +} diff --git a/packages/admin/dashboard/src/routes/locations/common/constants.ts b/packages/admin/dashboard/src/routes/locations/common/constants.ts index 687386f572074..3d5e2e30e1407 100644 --- a/packages/admin/dashboard/src/routes/locations/common/constants.ts +++ b/packages/admin/dashboard/src/routes/locations/common/constants.ts @@ -9,3 +9,8 @@ export enum ShippingOptionPriceType { } export const GEO_ZONE_STACKED_MODAL_ID = "geo-zone" + +export const CONDITIONAL_PRICES_STACKED_MODAL_ID = "conditional-prices" + +export const ITEM_TOTAL_ATTRIBUTE = "item_total" +export const REGION_ID_ATTRIBUTE = "region_id" diff --git a/packages/admin/dashboard/src/routes/locations/common/hooks/use-shipping-option-price-columns.tsx b/packages/admin/dashboard/src/routes/locations/common/hooks/use-shipping-option-price-columns.tsx index 95d2c6096ee5e..ad51cc2ffe263 100644 --- a/packages/admin/dashboard/src/routes/locations/common/hooks/use-shipping-option-price-columns.tsx +++ b/packages/admin/dashboard/src/routes/locations/common/hooks/use-shipping-option-price-columns.tsx @@ -1,11 +1,16 @@ import { HttpTypes } from "@medusajs/types" +import { ColumnDef } from "@tanstack/react-table" +import { TFunction } from "i18next" import { useMemo } from "react" +import { FieldPath, FieldValues } from "react-hook-form" import { useTranslation } from "react-i18next" +import { IncludesTaxTooltip } from "../../../../components/common/tax-badge/tax-badge" import { createDataGridHelper, DataGrid, } from "../../../../components/data-grid" -import { createDataGridPriceColumns } from "../../../../components/data-grid/helpers/create-data-grid-price-columns" +import { FieldContext } from "../../../../components/data-grid/types" +import { ShippingOptionPriceCell } from "../components/shipping-option-price-cell" const columnHelper = createDataGridHelper() @@ -26,6 +31,8 @@ export const useShippingOptionPriceColumns = ({ return [ columnHelper.column({ id: "name", + name: t("fields.name"), + disableHiding: true, header: t("fields.name"), cell: (context) => { return ( @@ -51,3 +58,117 @@ export const useShippingOptionPriceColumns = ({ ] }, [t, currencies, regions, pricePreferences, name]) } + +type CreateDataGridPriceColumnsProps< + TData, + TFieldValues extends FieldValues, +> = { + currencies?: string[] + regions?: HttpTypes.AdminRegion[] + pricePreferences?: HttpTypes.AdminPricePreference[] + getFieldName: ( + context: FieldContext, + value: string + ) => FieldPath | null + t: TFunction +} + +export const createDataGridPriceColumns = < + TData, + TFieldValues extends FieldValues, +>({ + currencies, + regions, + pricePreferences, + getFieldName, + t, +}: CreateDataGridPriceColumnsProps): ColumnDef< + TData, + unknown +>[] => { + const columnHelper = createDataGridHelper() + + return [ + ...(currencies?.map((currency) => { + const preference = pricePreferences?.find( + (p) => p.attribute === "currency_code" && p.value === currency + ) + + const translatedCurrencyName = t("fields.priceTemplate", { + regionOrCurrency: currency.toUpperCase(), + }) + + return columnHelper.column({ + id: `currency_prices.${currency}`, + name: t("fields.priceTemplate", { + regionOrCurrency: currency.toUpperCase(), + }), + field: (context) => { + return getFieldName(context, currency) + }, + type: "number", + header: () => ( +
+ + {translatedCurrencyName} + + +
+ ), + cell: (context) => { + return ( + + ) + }, + }) + }) ?? []), + ...(regions?.map((region) => { + const preference = pricePreferences?.find( + (p) => p.attribute === "region_id" && p.value === region.id + ) + + const translatedRegionName = t("fields.priceTemplate", { + regionOrCurrency: region.name, + }) + + return columnHelper.column({ + id: `region_prices.${region.id}`, + name: t("fields.priceTemplate", { + regionOrCurrency: region.name, + }), + field: (context) => { + return getFieldName(context, region.id) + }, + type: "number", + header: () => ( +
+ + {translatedRegionName} + + +
+ ), + cell: (context) => { + const currency = currencies?.find((c) => c === region.currency_code) + if (!currency) { + return null + } + + return ( + + ) + }, + }) + }) ?? []), + ] +} diff --git a/packages/admin/dashboard/src/routes/locations/common/schema.ts b/packages/admin/dashboard/src/routes/locations/common/schema.ts new file mode 100644 index 0000000000000..3548d75774e0f --- /dev/null +++ b/packages/admin/dashboard/src/routes/locations/common/schema.ts @@ -0,0 +1,90 @@ +import { t } from "i18next" +import { z } from "zod" +import { castNumber } from "../../../lib/cast-number" + +export const ConditionalPriceSchema = z + .object({ + amount: z.union([z.string(), z.number()]), + gte: z.union([z.string(), z.number()]).nullish(), + lte: z.union([z.string(), z.number()]).nullish(), + lt: z.number().nullish(), + gt: z.number().nullish(), + eq: z.number().nullish(), + }) + .refine((data) => data.amount !== "", { + message: t( + "stockLocations.shippingOptions.conditionalPrices.errors.amountRequired" + ), + path: ["amount"], + }) + .refine( + (data) => { + const hasEqLtGt = + data.eq !== undefined || data.lt !== undefined || data.gt !== undefined + + // The rule has operators that can only be managed using the API, so we should not validate this. + if (hasEqLtGt) { + return true + } + + return ( + (data.gte !== undefined && data.gte !== "") || + (data.lte !== undefined && data.lte !== "") + ) + }, + { + message: t( + "stockLocations.shippingOptions.conditionalPrices.errors.minOrMaxRequired" + ), + path: ["gte"], + } + ) + .refine( + (data) => { + if ( + data.gte != null && + data.gte !== "" && + data.lte != null && + data.lte !== "" + ) { + const gte = castNumber(data.gte) + const lte = castNumber(data.lte) + return gte <= lte + } + return true + }, + { + message: t( + "stockLocations.shippingOptions.conditionalPrices.errors.minGreaterThanMax" + ), + path: ["gte"], + } + ) + +export type ConditionalPrice = z.infer + +export const UpdateConditionalPriceSchema = ConditionalPriceSchema.and( + z.object({ + id: z.string().optional(), + }) +) + +export type UpdateConditionalPrice = z.infer< + typeof UpdateConditionalPriceSchema +> + +export const CondtionalPriceRuleSchema = z.object({ + prices: z.array(ConditionalPriceSchema), +}) + +export type CondtionalPriceRuleSchemaType = z.infer< + typeof CondtionalPriceRuleSchema +> + +export const UpdateConditionalPriceRuleSchema = z.object({ + prices: z.array(UpdateConditionalPriceSchema), +}) + +export type UpdateConditionalPriceRuleSchemaType = z.infer< + typeof UpdateConditionalPriceRuleSchema +> diff --git a/packages/admin/dashboard/src/routes/locations/common/types.ts b/packages/admin/dashboard/src/routes/locations/common/types.ts new file mode 100644 index 0000000000000..354c72b099d31 --- /dev/null +++ b/packages/admin/dashboard/src/routes/locations/common/types.ts @@ -0,0 +1,12 @@ +import { CurrencyInfo } from "../../../lib/data/currencies" + +export type ConditionalShippingOptionPriceAccessor = + | `conditional_region_prices.${string}` + | `conditional_currency_prices.${string}` + +export type ConditionalPriceInfo = { + type: "currency" | "region" + field: string + name: string + currency: CurrencyInfo +} diff --git a/packages/admin/dashboard/src/routes/locations/common/utils/get-custom-shipping-option-price-field-info.ts b/packages/admin/dashboard/src/routes/locations/common/utils/get-custom-shipping-option-price-field-info.ts new file mode 100644 index 0000000000000..1b2232fb8b5cc --- /dev/null +++ b/packages/admin/dashboard/src/routes/locations/common/utils/get-custom-shipping-option-price-field-info.ts @@ -0,0 +1,19 @@ +import { ConditionalShippingOptionPriceAccessor } from "../types" + +export const getCustomShippingOptionPriceFieldName = ( + field: string, + type: "region" | "currency" +): ConditionalShippingOptionPriceAccessor => { + const prefix = type === "region" ? "region_prices" : "currency_prices" + const customPrefix = + type === "region" + ? "conditional_region_prices" + : "conditional_currency_prices" + + const name = field.replace( + prefix, + customPrefix + ) as ConditionalShippingOptionPriceAccessor + + return name +} diff --git a/packages/admin/dashboard/src/routes/locations/common/utils/price-rule-helpers.ts b/packages/admin/dashboard/src/routes/locations/common/utils/price-rule-helpers.ts new file mode 100644 index 0000000000000..a47493f0a9aa1 --- /dev/null +++ b/packages/admin/dashboard/src/routes/locations/common/utils/price-rule-helpers.ts @@ -0,0 +1,41 @@ +import { castNumber } from "../../../../lib/cast-number" +import { ITEM_TOTAL_ATTRIBUTE } from "../constants" + +const createPriceRule = ( + attribute: string, + operator: string, + value: string | number +) => { + const rule = { + attribute, + operator, + value: castNumber(value), + } + + return rule +} + +export const buildShippingOptionPriceRules = (rule: { + gte?: string | number | null + lte?: string | number | null + gt?: string | number | null + lt?: string | number | null + eq?: string | number | null +}) => { + const conditions = [ + { value: rule.gte, operator: "gte" }, + { value: rule.lte, operator: "lte" }, + { value: rule.gt, operator: "gt" }, + { value: rule.lt, operator: "lt" }, + { value: rule.eq, operator: "eq" }, + ] + + const conditionsWithValues = conditions.filter(({ value }) => value) as { + value: string | number + operator: string + }[] + + return conditionsWithValues.map(({ operator, value }) => + createPriceRule(ITEM_TOTAL_ATTRIBUTE, operator, value) + ) +} diff --git a/packages/admin/dashboard/src/routes/locations/location-detail/components/location-fulfillment-providers-section/location-fulfillment-providers-section.tsx b/packages/admin/dashboard/src/routes/locations/location-detail/components/location-fulfillment-providers-section/location-fulfillment-providers-section.tsx index cc68879a0de92..0a94bb2504dea 100644 --- a/packages/admin/dashboard/src/routes/locations/location-detail/components/location-fulfillment-providers-section/location-fulfillment-providers-section.tsx +++ b/packages/admin/dashboard/src/routes/locations/location-detail/components/location-fulfillment-providers-section/location-fulfillment-providers-section.tsx @@ -1,6 +1,7 @@ import { HandTruck, PencilSquare } from "@medusajs/icons" import { HttpTypes } from "@medusajs/types" import { Container, Heading } from "@medusajs/ui" +import { Fragment } from "react" import { useTranslation } from "react-i18next" import { ActionMenu } from "../../../../../components/common/action-menu" @@ -50,7 +51,7 @@ function LocationsFulfillmentProvidersSection({
{fulfillment_providers?.map((fulfillmentProvider) => { return ( - <> + @@ -58,7 +59,7 @@ function LocationsFulfillmentProvidersSection({
{formatProvider(fulfillmentProvider.id)}
- +
) })}
diff --git a/packages/admin/dashboard/src/routes/locations/location-service-zone-create/components/create-service-zone-form/create-service-zone-form.tsx b/packages/admin/dashboard/src/routes/locations/location-service-zone-create/components/create-service-zone-form/create-service-zone-form.tsx index 9da23bb7f3f53..5d4688025ff3e 100644 --- a/packages/admin/dashboard/src/routes/locations/location-service-zone-create/components/create-service-zone-form/create-service-zone-form.tsx +++ b/packages/admin/dashboard/src/routes/locations/location-service-zone-create/components/create-service-zone-form/create-service-zone-form.tsx @@ -1,7 +1,6 @@ import { zodResolver } from "@hookform/resolvers/zod" import { HttpTypes } from "@medusajs/types" import { Button, Heading, Input, toast } from "@medusajs/ui" -import { useState } from "react" import { useForm } from "react-hook-form" import { useTranslation } from "react-i18next" import { z } from "zod" @@ -42,8 +41,6 @@ export function CreateServiceZoneForm({ const { t } = useTranslation() const { handleSuccess } = useRouteModal() - const [open, setOpen] = useState(false) - const form = useForm>({ defaultValues: { name: "", diff --git a/packages/admin/dashboard/src/routes/locations/location-service-zone-shipping-option-create/components/create-shipping-options-form/create-shipping-option-details-form.tsx b/packages/admin/dashboard/src/routes/locations/location-service-zone-shipping-option-create/components/create-shipping-options-form/create-shipping-option-details-form.tsx index dac4f1678e5d8..dd95fb5f1c6f4 100644 --- a/packages/admin/dashboard/src/routes/locations/location-service-zone-shipping-option-create/components/create-shipping-options-form/create-shipping-option-details-form.tsx +++ b/packages/admin/dashboard/src/routes/locations/location-service-zone-shipping-option-create/components/create-shipping-options-form/create-shipping-option-details-form.tsx @@ -54,7 +54,7 @@ export const CreateShippingOptionDetailsForm = ({ return (
-
+
{t( diff --git a/packages/admin/dashboard/src/routes/locations/location-service-zone-shipping-option-create/components/create-shipping-options-form/create-shipping-options-form.tsx b/packages/admin/dashboard/src/routes/locations/location-service-zone-shipping-option-create/components/create-shipping-options-form/create-shipping-options-form.tsx index 1cf638d2248ea..1ed449e6c8533 100644 --- a/packages/admin/dashboard/src/routes/locations/location-service-zone-shipping-option-create/components/create-shipping-options-form/create-shipping-options-form.tsx +++ b/packages/admin/dashboard/src/routes/locations/location-service-zone-shipping-option-create/components/create-shipping-options-form/create-shipping-options-form.tsx @@ -13,6 +13,7 @@ import { KeyboundForm } from "../../../../../components/utilities/keybound-form" import { useCreateShippingOptions } from "../../../../../hooks/api/shipping-options" import { castNumber } from "../../../../../lib/cast-number" import { ShippingOptionPriceType } from "../../../common/constants" +import { buildShippingOptionPriceRules } from "../../../common/utils/price-rule-helpers" import { CreateShippingOptionDetailsForm } from "./create-shipping-option-details-form" import { CreateShippingOptionsPricesForm } from "./create-shipping-options-prices-form" import { @@ -51,6 +52,8 @@ export function CreateShippingOptionsForm({ provider_id: "", region_prices: {}, currency_prices: {}, + conditional_region_prices: {}, + conditional_currency_prices: {}, }, resolver: zodResolver(CreateShippingOptionSchema), }) @@ -63,7 +66,7 @@ export function CreateShippingOptionsForm({ const handleSubmit = form.handleSubmit(async (data) => { const currencyPrices = Object.entries(data.currency_prices) .map(([code, value]) => { - if (value === "" || value === undefined) { + if (!value) { return undefined } @@ -72,11 +75,11 @@ export function CreateShippingOptionsForm({ amount: castNumber(value), } }) - .filter((o) => !!o) as { currency_code: string; amount: number }[] + .filter((p): p is { currency_code: string; amount: number } => !!p) const regionPrices = Object.entries(data.region_prices) .map(([region_id, value]) => { - if (value === "" || value === undefined) { + if (!value) { return undefined } @@ -85,7 +88,40 @@ export function CreateShippingOptionsForm({ amount: castNumber(value), } }) - .filter((o) => !!o) as { region_id: string; amount: number }[] + .filter((p): p is { region_id: string; amount: number } => !!p) + + const conditionalRegionPrices = Object.entries( + data.conditional_region_prices + ).flatMap(([region_id, value]) => { + const prices: HttpTypes.AdminCreateShippingOptionPriceWithRegion[] = + value?.map((rule) => ({ + region_id: region_id, + amount: castNumber(rule.amount), + rules: buildShippingOptionPriceRules(rule), + })) || [] + + return prices?.filter(Boolean) + }) + + const conditionalCurrencyPrices = Object.entries( + data.conditional_currency_prices + ).flatMap(([currency_code, value]) => { + const prices: HttpTypes.AdminCreateShippingOptionPriceWithCurrency[] = + value?.map((rule) => ({ + currency_code, + amount: castNumber(rule.amount), + rules: buildShippingOptionPriceRules(rule), + })) || [] + + return prices?.filter(Boolean) + }) + + const allPrices = [ + ...currencyPrices, + ...conditionalCurrencyPrices, + ...regionPrices, + ...conditionalRegionPrices, + ] await mutateAsync( { @@ -94,17 +130,17 @@ export function CreateShippingOptionsForm({ service_zone_id: zone.id, shipping_profile_id: data.shipping_profile_id, provider_id: data.provider_id, - prices: [...currencyPrices, ...regionPrices], + prices: allPrices, rules: [ { // eslint-disable-next-line - value: isReturn ? '"true"' : '"false"', // we want JSONB saved as string + value: isReturn ? '"true"' : '"false"', attribute: "is_return", operator: "eq", }, { // eslint-disable-next-line - value: data.enabled_in_store ? '"true"' : '"false"', // we want JSONB saved as string + value: data.enabled_in_store ? '"true"' : '"false"', attribute: "enabled_in_store", operator: "eq", }, @@ -123,12 +159,9 @@ export function CreateShippingOptionsForm({ `stockLocations.shippingOptions.create.${ isReturn ? "returns" : "shipping" }.successToast`, - { - name: shipping_option.name, - } + { name: shipping_option.name } ) ) - handleSuccess(`/settings/locations/${locationId}`) }, onError: (e) => { @@ -193,12 +226,38 @@ export function CreateShippingOptionsForm({ return ( - onTabChange(tab as Tab)} + { + const isEnterKey = e.key === "Enter" + const isModifierPressed = e.metaKey || e.ctrlKey + const shouldContinueToPricing = + activeTab !== Tab.PRICING && !isCalculatedPriceType + + if (!isEnterKey) { + return + } + e.preventDefault() + + if (!isModifierPressed) { + return + } + + if (shouldContinueToPricing) { + e.stopPropagation() + onTabChange(Tab.PRICING) + return + } + + handleSubmit() + }} > - + onTabChange(tab as Tab)} + > - - + + ) } diff --git a/packages/admin/dashboard/src/routes/locations/location-service-zone-shipping-option-create/components/create-shipping-options-form/create-shipping-options-prices-form.tsx b/packages/admin/dashboard/src/routes/locations/location-service-zone-shipping-option-create/components/create-shipping-options-form/create-shipping-options-prices-form.tsx index f0048538449e0..6d00a42c422df 100644 --- a/packages/admin/dashboard/src/routes/locations/location-service-zone-shipping-option-create/components/create-shipping-options-form/create-shipping-options-prices-form.tsx +++ b/packages/admin/dashboard/src/routes/locations/location-service-zone-shipping-option-create/components/create-shipping-options-form/create-shipping-options-prices-form.tsx @@ -1,12 +1,20 @@ -import { useMemo } from "react" -import { UseFormReturn } from "react-hook-form" +import { useMemo, useState } from "react" +import { UseFormReturn, useWatch } from "react-hook-form" import { DataGrid } from "../../../../../components/data-grid" -import { useRouteModal } from "../../../../../components/modals" +import { + StackedFocusModal, + useRouteModal, + useStackedModal, +} from "../../../../../components/modals" import { usePricePreferences } from "../../../../../hooks/api/price-preferences" import { useRegions } from "../../../../../hooks/api/regions" import { useStore } from "../../../../../hooks/api/store" +import { ConditionalPriceForm } from "../../../common/components/conditional-price-form" +import { ShippingOptionPriceProvider } from "../../../common/components/shipping-option-price-provider" +import { CONDITIONAL_PRICES_STACKED_MODAL_ID } from "../../../common/constants" import { useShippingOptionPriceColumns } from "../../../common/hooks/use-shipping-option-price-columns" +import { ConditionalPriceInfo } from "../../../common/types" import { CreateShippingOptionSchema } from "./schema" type PricingPricesFormProps = { @@ -16,6 +24,20 @@ type PricingPricesFormProps = { export const CreateShippingOptionsPricesForm = ({ form, }: PricingPricesFormProps) => { + const { getIsOpen, setIsOpen } = useStackedModal() + const [selectedPrice, setSelectedPrice] = + useState(null) + + const onOpenConditionalPricesModal = (info: ConditionalPriceInfo) => { + setIsOpen(CONDITIONAL_PRICES_STACKED_MODAL_ID, true) + setSelectedPrice(info) + } + + const onCloseConditionalPricesModal = () => { + setIsOpen(CONDITIONAL_PRICES_STACKED_MODAL_ID, false) + setSelectedPrice(null) + } + const { store, isLoading: isStoreLoading, @@ -42,7 +64,10 @@ export const CreateShippingOptionsPricesForm = ({ const { setCloseOnEscape } = useRouteModal() + const name = useWatch({ control: form.control, name: "name" }) + const columns = useShippingOptionPriceColumns({ + name, currencies, regions, pricePreferences, @@ -64,14 +89,32 @@ export const CreateShippingOptionsPricesForm = ({ } return ( -
- setCloseOnEscape(!editing)} - /> -
+ { + if (!open) { + setSelectedPrice(null) + } + }} + > + +
+ setCloseOnEscape(!editing)} + disableInteractions={getIsOpen(CONDITIONAL_PRICES_STACKED_MODAL_ID)} + /> + {selectedPrice && ( + + )} +
+
+
) } diff --git a/packages/admin/dashboard/src/routes/locations/location-service-zone-shipping-option-create/components/create-shipping-options-form/schema.ts b/packages/admin/dashboard/src/routes/locations/location-service-zone-shipping-option-create/components/create-shipping-options-form/schema.ts index a9553e504ee1a..442e2920dc1ef 100644 --- a/packages/admin/dashboard/src/routes/locations/location-service-zone-shipping-option-create/components/create-shipping-options-form/schema.ts +++ b/packages/admin/dashboard/src/routes/locations/location-service-zone-shipping-option-create/components/create-shipping-options-form/schema.ts @@ -1,5 +1,6 @@ import { z } from "zod" import { ShippingOptionPriceType } from "../../../common/constants" +import { ConditionalPriceSchema } from "../../../common/schema" export type CreateShippingOptionSchema = z.infer< typeof CreateShippingOptionSchema @@ -13,9 +14,29 @@ export const CreateShippingOptionDetailsSchema = z.object({ provider_id: z.string().min(1), }) +export const ShippingOptionConditionalPriceSchema = z.object({ + conditional_region_prices: z.record( + z.string(), + z.array(ConditionalPriceSchema).optional() + ), + conditional_currency_prices: z.record( + z.string(), + z.array(ConditionalPriceSchema).optional() + ), +}) + +export type ShippingOptionConditionalPriceSchemaType = z.infer< + typeof ShippingOptionConditionalPriceSchema +> + export const CreateShippingOptionSchema = z .object({ region_prices: z.record(z.string(), z.string().optional()), currency_prices: z.record(z.string(), z.string().optional()), }) .merge(CreateShippingOptionDetailsSchema) + .merge(ShippingOptionConditionalPriceSchema) + +export type CreateShippingOptionSchemaType = z.infer< + typeof CreateShippingOptionSchema +> diff --git a/packages/admin/dashboard/src/routes/locations/location-service-zone-shipping-option-pricing/components/create-shipping-options-form/edit-shipping-options-pricing-form.tsx b/packages/admin/dashboard/src/routes/locations/location-service-zone-shipping-option-pricing/components/create-shipping-options-form/edit-shipping-options-pricing-form.tsx index ba3eacdafddc6..8b2659b251b76 100644 --- a/packages/admin/dashboard/src/routes/locations/location-service-zone-shipping-option-pricing/components/create-shipping-options-form/edit-shipping-options-pricing-form.tsx +++ b/packages/admin/dashboard/src/routes/locations/location-service-zone-shipping-option-pricing/components/create-shipping-options-form/edit-shipping-options-pricing-form.tsx @@ -1,5 +1,5 @@ import { zodResolver } from "@hookform/resolvers/zod" -import { useMemo } from "react" +import { useMemo, useState } from "react" import { useForm } from "react-hook-form" import * as zod from "zod" @@ -10,7 +10,9 @@ import { useTranslation } from "react-i18next" import { DataGrid } from "../../../../../components/data-grid" import { RouteFocusModal, + StackedFocusModal, useRouteModal, + useStackedModal, } from "../../../../../components/modals/index" import { KeyboundForm } from "../../../../../components/utilities/keybound-form" import { usePricePreferences } from "../../../../../hooks/api/price-preferences" @@ -18,35 +20,20 @@ import { useRegions } from "../../../../../hooks/api/regions" import { useUpdateShippingOptions } from "../../../../../hooks/api/shipping-options" import { useStore } from "../../../../../hooks/api/store" import { castNumber } from "../../../../../lib/cast-number" +import { ConditionalPriceForm } from "../../../common/components/conditional-price-form" +import { ShippingOptionPriceProvider } from "../../../common/components/shipping-option-price-provider" +import { + CONDITIONAL_PRICES_STACKED_MODAL_ID, + ITEM_TOTAL_ATTRIBUTE, + REGION_ID_ATTRIBUTE, +} from "../../../common/constants" import { useShippingOptionPriceColumns } from "../../../common/hooks/use-shipping-option-price-columns" - -const getInitialCurrencyPrices = ( - prices: HttpTypes.AdminShippingOptionPrice[] -) => { - const ret: Record = {} - prices.forEach((p) => { - if (p.price_rules!.length) { - // this is a region price - return - } - ret[p.currency_code!] = p.amount - }) - return ret -} - -const getInitialRegionPrices = ( - prices: HttpTypes.AdminShippingOptionPrice[] -) => { - const ret: Record = {} - prices.forEach((p) => { - if (p.price_rules!.length) { - const regionId = p.price_rules![0].value - ret[regionId] = p.amount - } - }) - - return ret -} +import { + UpdateConditionalPrice, + UpdateConditionalPriceSchema, +} from "../../../common/schema" +import { ConditionalPriceInfo } from "../../../common/types" +import { buildShippingOptionPriceRules } from "../../../common/utils/price-rule-helpers" type PriceRecord = { id?: string @@ -64,6 +51,14 @@ const EditShippingOptionPricingSchema = zod.object({ zod.string(), zod.string().or(zod.number()).optional() ), + conditional_region_prices: zod.record( + zod.string(), + zod.array(UpdateConditionalPriceSchema) + ), + conditional_currency_prices: zod.record( + zod.string(), + zod.array(UpdateConditionalPriceSchema) + ), }) type EditShippingOptionPricingFormProps = { @@ -75,12 +70,22 @@ export function EditShippingOptionsPricingForm({ }: EditShippingOptionPricingFormProps) { const { t } = useTranslation() const { handleSuccess } = useRouteModal() + const { getIsOpen, setIsOpen } = useStackedModal() + const [selectedPrice, setSelectedPrice] = + useState(null) + + const onOpenConditionalPricesModal = (info: ConditionalPriceInfo) => { + setIsOpen(CONDITIONAL_PRICES_STACKED_MODAL_ID, true) + setSelectedPrice(info) + } + + const onCloseConditionalPricesModal = () => { + setIsOpen(CONDITIONAL_PRICES_STACKED_MODAL_ID, false) + setSelectedPrice(null) + } const form = useForm>({ - defaultValues: { - region_prices: getInitialRegionPrices(shippingOption.prices), - currency_prices: getInitialCurrencyPrices(shippingOption.prices), - }, + defaultValues: getDefaultValues(shippingOption.prices), resolver: zodResolver(EditShippingOptionPricingSchema), }) @@ -127,80 +132,80 @@ export function EditShippingOptionsPricingForm({ const handleSubmit = form.handleSubmit(async (data) => { const currencyPrices = Object.entries(data.currency_prices) .map(([code, value]) => { - if (value === "" || value === undefined) { + if ( + !value || + !currencies.some((c) => c.toLowerCase() === code.toLowerCase()) + ) { return undefined } - const currencyExists = currencies.some( - (currencyCode) => currencyCode.toLowerCase() == code.toLowerCase() - ) - if (!currencyExists) { - return undefined - } - - const amount = castNumber(value) - const priceRecord: PriceRecord = { currency_code: code, - amount: amount, + amount: castNumber(value), } - const price = shippingOption.prices.find( + const existingPrice = shippingOption.prices.find( (p) => p.currency_code === code && !p.price_rules!.length ) - // if that currency price is already defined for the SO, we will do an update - if (price) { - priceRecord["id"] = price.id + if (existingPrice) { + priceRecord.id = existingPrice.id } return priceRecord }) - .filter((p) => !!p) as PriceRecord[] + .filter((p): p is PriceRecord => !!p) + + const conditionalCurrencyPrices = Object.entries( + data.conditional_currency_prices + ).flatMap(([currency_code, value]) => + value?.map((rule) => ({ + id: rule.id, + currency_code, + amount: castNumber(rule.amount), + rules: buildShippingOptionPriceRules(rule), + })) + ) + /** + * TODO: If we try to update an existing region price the API throws an error. + * Instead we re-create region prices. + */ const regionPrices = Object.entries(data.region_prices) .map(([region_id, value]) => { - if (value === "" || value === undefined) { - return undefined - } - - // Check if the region_id exists in the regions array to avoid - // sending updates of region prices where the region has been - // deleted - const regionExists = regions?.some((region) => region.id === region_id) - if (!regionExists) { + if (!value || !regions?.some((region) => region.id === region_id)) { return undefined } - const amount = castNumber(value) - const priceRecord: PriceRecord = { region_id, - amount: amount, + amount: castNumber(value), } - /** - * HACK - when trying to update prices which already have a region price - * we get error: `Price rule with price_id: , rule_type_id: already exist`, - * so for now, we recreate region prices. - */ - - // const price = shippingOption.prices.find( - // (p) => p.price_rules?.[0]?.value === region_id - // ) - - // if (price) { - // priceRecord["id"] = price.id - // } - return priceRecord }) - .filter((p) => !!p) as PriceRecord[] + .filter((p): p is PriceRecord => !!p) + + const conditionalRegionPrices = Object.entries( + data.conditional_region_prices + ).flatMap(([region_id, value]) => + value?.map((rule) => ({ + id: rule.id, + region_id, + amount: castNumber(rule.amount), + rules: buildShippingOptionPriceRules(rule), + })) + ) + + const allPrices = [ + ...currencyPrices, + ...conditionalCurrencyPrices, + ...regionPrices, + ...conditionalRegionPrices, + ] await mutateAsync( - { - prices: [...currencyPrices, ...regionPrices], - }, + { prices: allPrices }, { onSuccess: () => { toast.success(t("general.success")) @@ -233,15 +238,35 @@ export function EditShippingOptionsPricingForm({ -
- setCloseOnEscape(!editing)} - /> -
+ { + if (!open) { + setSelectedPrice(null) + } + }} + > + +
+ setCloseOnEscape(!editing)} + disableInteractions={getIsOpen( + CONDITIONAL_PRICES_STACKED_MODAL_ID + )} + /> +
+ {selectedPrice && ( + + )} +
+
@@ -265,3 +290,89 @@ export function EditShippingOptionsPricingForm({ ) } + +const findRuleValue = ( + rules: HttpTypes.AdminShippingOptionPriceRule[], + operator: string +) => { + const fallbackValue = ["eq", "gt", "lt"].includes(operator) ? undefined : null + + return ( + rules?.find( + (r) => r.attribute === ITEM_TOTAL_ATTRIBUTE && r.operator === operator + )?.value || fallbackValue + ) +} + +const mapToConditionalPrice = ( + price: HttpTypes.AdminShippingOptionPrice +): UpdateConditionalPrice => { + const rules = price.price_rules || [] + + return { + id: price.id, + amount: price.amount, + gte: findRuleValue(rules, "gte"), + lte: findRuleValue(rules, "lte"), + gt: findRuleValue(rules, "gt") as undefined | null, + lt: findRuleValue(rules, "lt") as undefined | null, + eq: findRuleValue(rules, "eq") as undefined | null, + } +} + +const getDefaultValues = (prices: HttpTypes.AdminShippingOptionPrice[]) => { + const hasAttributes = ( + price: HttpTypes.AdminShippingOptionPrice, + required: string[], + forbidden: string[] = [] + ) => { + const attributes = price.price_rules?.map((r) => r.attribute) || [] + return ( + required.every((attr) => attributes.includes(attr)) && + !forbidden.some((attr) => attributes.includes(attr)) + ) + } + + const currency_prices: Record = {} + const conditional_currency_prices: Record = + {} + const region_prices: Record = {} + const conditional_region_prices: Record = {} + + prices.forEach((price) => { + if (!price.price_rules?.length) { + currency_prices[price.currency_code!] = price.amount + return + } + + if (hasAttributes(price, [ITEM_TOTAL_ATTRIBUTE], [REGION_ID_ATTRIBUTE])) { + const code = price.currency_code! + if (!conditional_currency_prices[code]) { + conditional_currency_prices[code] = [] + } + conditional_currency_prices[code].push(mapToConditionalPrice(price)) + return + } + + if (hasAttributes(price, [REGION_ID_ATTRIBUTE], [ITEM_TOTAL_ATTRIBUTE])) { + const regionId = price.price_rules[0].value + region_prices[regionId] = price.amount + return + } + + if (hasAttributes(price, [REGION_ID_ATTRIBUTE, ITEM_TOTAL_ATTRIBUTE])) { + const regionId = price.price_rules[0].value + if (!conditional_region_prices[regionId]) { + conditional_region_prices[regionId] = [] + } + conditional_region_prices[regionId].push(mapToConditionalPrice(price)) + } + }) + + return { + currency_prices, + conditional_currency_prices, + region_prices, + conditional_region_prices, + } +} diff --git a/packages/admin/dashboard/src/routes/locations/location-service-zone-shipping-option-pricing/location-service-zone-shipping-option-pricing.tsx b/packages/admin/dashboard/src/routes/locations/location-service-zone-shipping-option-pricing/location-service-zone-shipping-option-pricing.tsx index 9cd94ca5687cd..066940c15f7cb 100644 --- a/packages/admin/dashboard/src/routes/locations/location-service-zone-shipping-option-pricing/location-service-zone-shipping-option-pricing.tsx +++ b/packages/admin/dashboard/src/routes/locations/location-service-zone-shipping-option-pricing/location-service-zone-shipping-option-pricing.tsx @@ -14,10 +14,13 @@ export function LocationServiceZoneShippingOptionPricing() { }) } - const { shipping_option: shippingOption, isError, error } = - useShippingOption(so_id, { - fields: "*prices,*prices.price_rules", - }) + const { + shipping_option: shippingOption, + isError, + error, + } = useShippingOption(so_id, { + fields: "*prices,*prices.price_rules", + }) if (isError) { throw error diff --git a/packages/core/types/src/http/shipping-option/admin/entities.ts b/packages/core/types/src/http/shipping-option/admin/entities.ts index d22eac5c52e0e..cab9ad9429c27 100644 --- a/packages/core/types/src/http/shipping-option/admin/entities.ts +++ b/packages/core/types/src/http/shipping-option/admin/entities.ts @@ -30,7 +30,14 @@ export interface AdminShippingOptionRule { // TODO: This type is complete, but it's not clear what the `rules` field is supposed to return in all cases. export interface AdminShippingOptionPriceRule { id: string - value: string + value: string | number + operator: RuleOperatorType + attribute: string + price_id: string + priority: number + created_at: string + updated_at: string + deleted_at: string | null } export interface AdminShippingOptionPrice extends AdminPrice { diff --git a/packages/core/types/src/http/shipping-option/admin/payloads.ts b/packages/core/types/src/http/shipping-option/admin/payloads.ts index c0297dde6b794..514226f795494 100644 --- a/packages/core/types/src/http/shipping-option/admin/payloads.ts +++ b/packages/core/types/src/http/shipping-option/admin/payloads.ts @@ -13,12 +13,24 @@ export interface AdminCreateShippingOptionType { code: string } -export interface AdminCreateShippingOptionPriceWithCurrency { +interface AdminShippingOptionPriceRulePayload { + operator: string + attribute: string + value: string | string[] | number +} + +interface AdminShippingOptionPriceWithRules { + rules?: AdminShippingOptionPriceRulePayload[] +} + +export interface AdminCreateShippingOptionPriceWithCurrency + extends AdminShippingOptionPriceWithRules { currency_code: string amount: number } -export interface AdminCreateShippingOptionPriceWithRegion { +export interface AdminCreateShippingOptionPriceWithRegion + extends AdminShippingOptionPriceWithRules { region_id: string amount: number } @@ -43,13 +55,15 @@ export interface AdminUpdateShippingOptionRule id: string } -export interface AdminUpdateShippingOptionPriceWithCurrency { +export interface AdminUpdateShippingOptionPriceWithCurrency + extends AdminShippingOptionPriceWithRules { id?: string currency_code?: string amount?: number } -export interface AdminUpdateShippingOptionPriceWithRegion { +export interface AdminUpdateShippingOptionPriceWithRegion + extends AdminShippingOptionPriceWithRules { id?: string region_id?: string amount?: number From c8cb9b5c1addb5dcd30ca559813b96e5fba1d6a3 Mon Sep 17 00:00:00 2001 From: Shahed Nasser Date: Mon, 9 Dec 2024 11:43:52 +0200 Subject: [PATCH 10/11] fix(medusa): add query type argument to RequestWithContext (#10469) --- .changeset/empty-snails-admire.md | 5 +++++ packages/medusa/src/api/store/products/helpers.ts | 13 +++++++------ 2 files changed, 12 insertions(+), 6 deletions(-) create mode 100644 .changeset/empty-snails-admire.md diff --git a/.changeset/empty-snails-admire.md b/.changeset/empty-snails-admire.md new file mode 100644 index 0000000000000..bef41ee805a9c --- /dev/null +++ b/.changeset/empty-snails-admire.md @@ -0,0 +1,5 @@ +--- +"@medusajs/medusa": patch +--- + +fix(medusa): add query type argument to RequestWithContext diff --git a/packages/medusa/src/api/store/products/helpers.ts b/packages/medusa/src/api/store/products/helpers.ts index 12c0d13678db1..6f845b968b159 100644 --- a/packages/medusa/src/api/store/products/helpers.ts +++ b/packages/medusa/src/api/store/products/helpers.ts @@ -9,14 +9,15 @@ import { import { calculateAmountsWithTax, Modules } from "@medusajs/framework/utils" import { TaxModuleService } from "@medusajs/tax/dist/services" -export type RequestWithContext = MedusaStoreRequest & { - taxContext: { - taxLineContext?: TaxCalculationContext - taxInclusivityContext?: { - automaticTaxes: boolean +export type RequestWithContext> = + MedusaStoreRequest & { + taxContext: { + taxLineContext?: TaxCalculationContext + taxInclusivityContext?: { + automaticTaxes: boolean + } } } -} export const refetchProduct = async ( idOrFilter: string | object, From 0ae98c51eba613fbd1101da5cc28e1794b252798 Mon Sep 17 00:00:00 2001 From: Shahed Nasser Date: Mon, 9 Dec 2024 13:54:42 +0200 Subject: [PATCH 11/11] docs: revise main docs outline (#10502) --- .github/workflows/docs-test.yml | 4 +- .../advanced-development/api-routes/page.mdx | 14 - .../advanced-development/modules/page.mdx | 15 - .../app/learn/advanced-development/page.mdx | 15 - .../advanced-development/workflows/page.mdx | 14 - .../basics/admin-customizations/page.mdx | 64 ---- www/apps/book/app/learn/basics/page.mdx | 18 - .../basics/project-directories-files/page.mdx | 26 -- www/apps/book/app/learn/build/page.mdx | 2 +- .../custom-features/api-route/page.mdx | 8 +- .../custom-features/module/page.mdx | 10 +- .../customization/custom-features/page.mdx | 6 +- .../custom-features/workflow/page.mdx | 6 +- .../customization/customize-admin/page.mdx | 4 +- .../customize-admin/route/page.mdx | 8 +- .../customize-admin/widget/page.mdx | 2 +- .../extend-features/define-link/page.mdx | 4 +- .../extend-create-product/page.mdx | 10 +- .../customization/extend-features/page.mdx | 6 +- .../query-linked-records/page.mdx | 2 +- .../integrate-systems/handle-event/page.mdx | 14 +- .../customization/integrate-systems/page.mdx | 2 +- .../integrate-systems/schedule-task/page.mdx | 12 +- .../integrate-systems/service/page.mdx | 4 +- .../book/app/learn/customization/page.mdx | 14 - .../app/learn/first-customizations/page.mdx | 67 ---- .../admin/constraints/page.mdx | 0 .../admin/page.mdx | 0 .../admin/tips/page.mdx | 0 .../admin/ui-routes/page.mdx | 0 .../admin/widgets/page.mdx | 0 .../api-routes/additional-data/page.mdx | 2 +- .../api-routes/cors/page.mdx | 0 .../api-routes/errors/page.mdx | 0 .../api-routes/http-methods/page.mdx | 0 .../api-routes/middlewares/page.mdx | 0 .../api-routes/page.mdx | 0 .../api-routes/parameters/page.mdx | 0 .../api-routes/protected-routes/page.mdx | 0 .../api-routes/responses/page.mdx | 0 .../api-routes/validation/page.mdx | 2 +- .../custom-cli-scripts/page.mdx | 0 .../custom-cli-scripts/seed-data/page.mdx | 0 .../data-models/check-constraints/page.mdx | 0 .../data-models/configure-properties/page.mdx | 0 .../data-models/default-properties/page.mdx | 0 .../data-models/index/page.mdx | 0 .../data-models/infer-type/page.mdx | 0 .../data-models/manage-relationships/page.mdx | 0 .../data-models/page.mdx | 4 +- .../data-models/primary-key/page.mdx | 0 .../data-models/property-types/page.mdx | 0 .../data-models/relationships/page.mdx | 0 .../data-models/searchable-property/page.mdx | 0 .../data-models/write-migration/page.mdx | 0 .../environment-variables/page.mdx | 0 .../data-payload/page.mdx | 0 .../emit-event/page.mdx | 0 .../events-and-subscribers/page.mdx | 4 +- .../medusa-container/page.mdx | 14 +- .../module-links/custom-columns/page.mdx | 0 .../module-links/directions/page.mdx | 0 .../module-links/page.mdx | 0 .../module-links/query/page.mdx | 4 +- .../module-links/remote-link/page.mdx | 0 .../modules}/architectural-modules/page.mdx | 0 .../modules}/commerce-modules/page.mdx | 6 +- .../modules/container/page.mdx | 0 .../modules/db-operations/page.mdx | 0 .../modules/isolation/page.mdx | 4 +- .../modules}/loaders/page.mdx | 10 +- .../modules-directory-structure/page.mdx | 4 +- .../modules/multiple-services/page.mdx | 0 .../modules/options/page.mdx | 0 .../{basics => fundamentals}/modules/page.mdx | 12 +- .../modules/service-constraints/page.mdx | 0 .../modules/service-factory/page.mdx | 0 .../scheduled-jobs/execution-number/page.mdx | 0 .../scheduled-jobs/page.mdx | 2 +- .../workflows/access-workflow-errors/page.mdx | 0 .../workflows/add-workflow-hook/page.mdx | 0 .../workflows/compensation-function/page.mdx | 0 .../workflows/conditions/page.mdx | 0 .../constructor-constraints/page.mdx | 0 .../execute-another-workflow/page.mdx | 0 .../workflows/long-running-workflow/page.mdx | 0 .../workflows/multiple-step-usage/page.mdx | 0 .../workflows/page.mdx | 2 +- .../workflows/parallel-steps/page.mdx | 0 .../workflows/retry-failed-steps/page.mdx | 0 .../workflows/variable-manipulation/page.mdx | 0 .../workflows/workflow-hooks/page.mdx | 0 .../workflows/workflow-timeout/page.mdx | 0 www/apps/book/app/learn/installation/page.mdx | 28 +- .../architecture}/page.mdx | 0 .../learn/more-resources/cheatsheet/page.mdx | 160 --------- .../components/Homepage/CodeTabs/index.tsx | 16 +- .../Homepage/LinksSection/index.tsx | 4 +- www/apps/book/generated/edit-dates.mjs | 193 +++++----- www/apps/book/next.config.mjs | 37 +- www/apps/book/sidebar.mjs | 340 ++++++++---------- www/apps/book/utils/number-sidebar-items.mjs | 32 +- www/apps/book/utils/redirects.mjs | 109 ++++++ .../components/forms/page.mdx | 4 +- .../auth/create-actor-type/page.mdx | 2 +- .../app/commerce-modules/cart/extend/page.mdx | 22 +- .../commerce-modules/customer/extend/page.mdx | 22 +- .../payment/payment-flow/page.mdx | 2 +- .../commerce-modules/product/extend/page.mdx | 22 +- .../product/guides/price-with-taxes/page.mdx | 2 +- .../product/guides/price/page.mdx | 2 +- .../promotion/extend/page.mdx | 22 +- www/apps/resources/app/examples/page.mdx | 178 ++++----- .../app/integrations/guides/resend/page.mdx | 18 +- .../app/integrations/guides/sanity/page.mdx | 34 +- .../app/medusa-cli/commands/exec/page.mdx | 2 +- .../app/medusa-container-resources/page.mdx | 4 +- www/apps/resources/app/recipes/b2b/page.mdx | 16 +- .../app/recipes/commerce-automation/page.mdx | 24 +- .../examples/standard/page.mdx | 30 +- .../app/recipes/digital-products/page.mdx | 20 +- .../integrate-ecommerce-stack/page.mdx | 8 +- .../examples/restaurant-delivery/page.mdx | 26 +- .../marketplace/examples/vendors/page.mdx | 26 +- .../app/recipes/marketplace/page.mdx | 12 +- .../app/recipes/multi-region-store/page.mdx | 2 +- .../app/recipes/omnichannel/page.mdx | 6 +- www/apps/resources/app/recipes/oms/page.mdx | 10 +- .../recipes/personalized-products/page.mdx | 8 +- www/apps/resources/app/recipes/pos/page.mdx | 4 +- .../subscriptions/examples/standard/page.mdx | 20 +- .../app/recipes/subscriptions/page.mdx | 14 +- .../methods/list/page.mdx | 2 +- .../methods/listAndCount/page.mdx | 2 +- .../methods/retrieve/page.mdx | 2 +- .../app/service-factory-reference/page.mdx | 2 +- .../medusa-admin/no-widget-route/page.mdx | 4 +- .../functions/workflows.createHook/page.mdx | 2 +- .../page.mdx | 2 +- www/apps/ui/contentlayer.config.ts | 8 + www/apps/ui/package.json | 3 +- 141 files changed, 781 insertions(+), 1148 deletions(-) delete mode 100644 www/apps/book/app/learn/advanced-development/api-routes/page.mdx delete mode 100644 www/apps/book/app/learn/advanced-development/modules/page.mdx delete mode 100644 www/apps/book/app/learn/advanced-development/page.mdx delete mode 100644 www/apps/book/app/learn/advanced-development/workflows/page.mdx delete mode 100644 www/apps/book/app/learn/basics/admin-customizations/page.mdx delete mode 100644 www/apps/book/app/learn/basics/page.mdx delete mode 100644 www/apps/book/app/learn/basics/project-directories-files/page.mdx delete mode 100644 www/apps/book/app/learn/customization/page.mdx delete mode 100644 www/apps/book/app/learn/first-customizations/page.mdx rename www/apps/book/app/learn/{advanced-development => fundamentals}/admin/constraints/page.mdx (100%) rename www/apps/book/app/learn/{advanced-development => fundamentals}/admin/page.mdx (100%) rename www/apps/book/app/learn/{advanced-development => fundamentals}/admin/tips/page.mdx (100%) rename www/apps/book/app/learn/{advanced-development => fundamentals}/admin/ui-routes/page.mdx (100%) rename www/apps/book/app/learn/{advanced-development => fundamentals}/admin/widgets/page.mdx (100%) rename www/apps/book/app/learn/{advanced-development => fundamentals}/api-routes/additional-data/page.mdx (98%) rename www/apps/book/app/learn/{advanced-development => fundamentals}/api-routes/cors/page.mdx (100%) rename www/apps/book/app/learn/{advanced-development => fundamentals}/api-routes/errors/page.mdx (100%) rename www/apps/book/app/learn/{advanced-development => fundamentals}/api-routes/http-methods/page.mdx (100%) rename www/apps/book/app/learn/{advanced-development => fundamentals}/api-routes/middlewares/page.mdx (100%) rename www/apps/book/app/learn/{basics => fundamentals}/api-routes/page.mdx (100%) rename www/apps/book/app/learn/{advanced-development => fundamentals}/api-routes/parameters/page.mdx (100%) rename www/apps/book/app/learn/{advanced-development => fundamentals}/api-routes/protected-routes/page.mdx (100%) rename www/apps/book/app/learn/{advanced-development => fundamentals}/api-routes/responses/page.mdx (100%) rename www/apps/book/app/learn/{advanced-development => fundamentals}/api-routes/validation/page.mdx (99%) rename www/apps/book/app/learn/{advanced-development => fundamentals}/custom-cli-scripts/page.mdx (100%) rename www/apps/book/app/learn/{advanced-development => fundamentals}/custom-cli-scripts/seed-data/page.mdx (100%) rename www/apps/book/app/learn/{advanced-development => fundamentals}/data-models/check-constraints/page.mdx (100%) rename www/apps/book/app/learn/{advanced-development => fundamentals}/data-models/configure-properties/page.mdx (100%) rename www/apps/book/app/learn/{advanced-development => fundamentals}/data-models/default-properties/page.mdx (100%) rename www/apps/book/app/learn/{advanced-development => fundamentals}/data-models/index/page.mdx (100%) rename www/apps/book/app/learn/{advanced-development => fundamentals}/data-models/infer-type/page.mdx (100%) rename www/apps/book/app/learn/{advanced-development => fundamentals}/data-models/manage-relationships/page.mdx (100%) rename www/apps/book/app/learn/{advanced-development => fundamentals}/data-models/page.mdx (57%) rename www/apps/book/app/learn/{advanced-development => fundamentals}/data-models/primary-key/page.mdx (100%) rename www/apps/book/app/learn/{advanced-development => fundamentals}/data-models/property-types/page.mdx (100%) rename www/apps/book/app/learn/{advanced-development => fundamentals}/data-models/relationships/page.mdx (100%) rename www/apps/book/app/learn/{advanced-development => fundamentals}/data-models/searchable-property/page.mdx (100%) rename www/apps/book/app/learn/{advanced-development => fundamentals}/data-models/write-migration/page.mdx (100%) rename www/apps/book/app/learn/{advanced-development => fundamentals}/environment-variables/page.mdx (100%) rename www/apps/book/app/learn/{advanced-development => fundamentals}/events-and-subscribers/data-payload/page.mdx (100%) rename www/apps/book/app/learn/{advanced-development => fundamentals}/events-and-subscribers/emit-event/page.mdx (100%) rename www/apps/book/app/learn/{basics => fundamentals}/events-and-subscribers/page.mdx (92%) rename www/apps/book/app/learn/{basics => fundamentals}/medusa-container/page.mdx (78%) rename www/apps/book/app/learn/{advanced-development => fundamentals}/module-links/custom-columns/page.mdx (100%) rename www/apps/book/app/learn/{advanced-development => fundamentals}/module-links/directions/page.mdx (100%) rename www/apps/book/app/learn/{advanced-development => fundamentals}/module-links/page.mdx (100%) rename www/apps/book/app/learn/{advanced-development => fundamentals}/module-links/query/page.mdx (98%) rename www/apps/book/app/learn/{advanced-development => fundamentals}/module-links/remote-link/page.mdx (100%) rename www/apps/book/app/learn/{advanced-development/architecture => fundamentals/modules}/architectural-modules/page.mdx (100%) rename www/apps/book/app/learn/{basics => fundamentals/modules}/commerce-modules/page.mdx (70%) rename www/apps/book/app/learn/{advanced-development => fundamentals}/modules/container/page.mdx (100%) rename www/apps/book/app/learn/{advanced-development => fundamentals}/modules/db-operations/page.mdx (100%) rename www/apps/book/app/learn/{advanced-development => fundamentals}/modules/isolation/page.mdx (94%) rename www/apps/book/app/learn/{basics => fundamentals/modules}/loaders/page.mdx (90%) rename www/apps/book/app/learn/{basics => fundamentals/modules}/modules-directory-structure/page.mdx (91%) rename www/apps/book/app/learn/{advanced-development => fundamentals}/modules/multiple-services/page.mdx (100%) rename www/apps/book/app/learn/{advanced-development => fundamentals}/modules/options/page.mdx (100%) rename www/apps/book/app/learn/{basics => fundamentals}/modules/page.mdx (93%) rename www/apps/book/app/learn/{advanced-development => fundamentals}/modules/service-constraints/page.mdx (100%) rename www/apps/book/app/learn/{advanced-development => fundamentals}/modules/service-factory/page.mdx (100%) rename www/apps/book/app/learn/{advanced-development => fundamentals}/scheduled-jobs/execution-number/page.mdx (100%) rename www/apps/book/app/learn/{basics => fundamentals}/scheduled-jobs/page.mdx (98%) rename www/apps/book/app/learn/{advanced-development => fundamentals}/workflows/access-workflow-errors/page.mdx (100%) rename www/apps/book/app/learn/{advanced-development => fundamentals}/workflows/add-workflow-hook/page.mdx (100%) rename www/apps/book/app/learn/{advanced-development => fundamentals}/workflows/compensation-function/page.mdx (100%) rename www/apps/book/app/learn/{advanced-development => fundamentals}/workflows/conditions/page.mdx (100%) rename www/apps/book/app/learn/{advanced-development => fundamentals}/workflows/constructor-constraints/page.mdx (100%) rename www/apps/book/app/learn/{advanced-development => fundamentals}/workflows/execute-another-workflow/page.mdx (100%) rename www/apps/book/app/learn/{advanced-development => fundamentals}/workflows/long-running-workflow/page.mdx (100%) rename www/apps/book/app/learn/{advanced-development => fundamentals}/workflows/multiple-step-usage/page.mdx (100%) rename www/apps/book/app/learn/{basics => fundamentals}/workflows/page.mdx (98%) rename www/apps/book/app/learn/{advanced-development => fundamentals}/workflows/parallel-steps/page.mdx (100%) rename www/apps/book/app/learn/{advanced-development => fundamentals}/workflows/retry-failed-steps/page.mdx (100%) rename www/apps/book/app/learn/{advanced-development => fundamentals}/workflows/variable-manipulation/page.mdx (100%) rename www/apps/book/app/learn/{advanced-development => fundamentals}/workflows/workflow-hooks/page.mdx (100%) rename www/apps/book/app/learn/{advanced-development => fundamentals}/workflows/workflow-timeout/page.mdx (100%) rename www/apps/book/app/learn/{advanced-development/architecture/overview => introduction/architecture}/page.mdx (100%) delete mode 100644 www/apps/book/app/learn/more-resources/cheatsheet/page.mdx create mode 100644 www/apps/book/utils/redirects.mjs diff --git a/.github/workflows/docs-test.yml b/.github/workflows/docs-test.yml index 5482a64686bec..dcc4c293f163f 100644 --- a/.github/workflows/docs-test.yml +++ b/.github/workflows/docs-test.yml @@ -41,9 +41,9 @@ jobs: NEXT_PUBLIC_BASE_URL: "http://localhost:3000" NEXT_PUBLIC_BASE_PATH: /api NEXT_PUBLIC_DOCS_URL: "https://medusa-docs.vercel.app" - NEXT_PUBLIC_UI_URL: "https://docs-ui.vercel.app" + NODE_ENV: production + NEXT_PUBLIC_RESOURCES_URL: "http://medusa-types-nine.vercel.app" # TODO change once we have actual URLs - NEXT_PUBLIC_RESOURCES_URL: "http://example.com" NEXT_PUBLIC_USER_GUIDE_URL: "http://example.com" vale-book: diff --git a/www/apps/book/app/learn/advanced-development/api-routes/page.mdx b/www/apps/book/app/learn/advanced-development/api-routes/page.mdx deleted file mode 100644 index 3b66757ea4b32..0000000000000 --- a/www/apps/book/app/learn/advanced-development/api-routes/page.mdx +++ /dev/null @@ -1,14 +0,0 @@ -export const metadata = { - title: `${pageNumber} API Routes Advanced Guides`, -} - -# {metadata.title} - -In the next chapters, you'll focus more on API routes to learn about topics such as: - -- Creating API routes for different HTTP methods. -- Accepting parameters in your API routes. -- Formatting response data and headers. -- Applying middlewares on API routes. -- Validating request body parameters. -- Protecting API routes by requiring user authentication. diff --git a/www/apps/book/app/learn/advanced-development/modules/page.mdx b/www/apps/book/app/learn/advanced-development/modules/page.mdx deleted file mode 100644 index 8811f4b9d4959..0000000000000 --- a/www/apps/book/app/learn/advanced-development/modules/page.mdx +++ /dev/null @@ -1,15 +0,0 @@ -export const metadata = { - title: `${pageNumber} Modules Advanced Guides`, -} - -# {metadata.title} - -In the next chapters, you'll learn more about developing modules and related resources. - -By the end of this chapter, you'll know more about: - -1. A module's container and how a module is isolated. -2. Passing options to a module. -3. The service factory and the methods it generates. -4. Using a module's service to query and perform actions on the database. -5. Using multiple services in a module. diff --git a/www/apps/book/app/learn/advanced-development/page.mdx b/www/apps/book/app/learn/advanced-development/page.mdx deleted file mode 100644 index 581d4161d0eac..0000000000000 --- a/www/apps/book/app/learn/advanced-development/page.mdx +++ /dev/null @@ -1,15 +0,0 @@ -export const metadata = { - title: `${pageNumber} Advanced Development`, -} - -# {metadata.title} - -In the previous chapters, you got a brief introduction to Medusa’s basic concepts. However, to build a custom commerce application, you need a deeper understanding of how you utilize these concepts for your business use case. - -The next chapters dive deeper into each concept, and explore Medusa's architecture. By the end of these chapters, you’ll be able to: - -- Expose API routes with control over authentication. -- Build sophisticated business logic in modules and manage links between them. -- Create advanced workflows and configure retries and timeout. -- Add new pages to the Medusa Admin. -- Do more with subscribers, scheduled jobs, and other tools. diff --git a/www/apps/book/app/learn/advanced-development/workflows/page.mdx b/www/apps/book/app/learn/advanced-development/workflows/page.mdx deleted file mode 100644 index 8b73c9a2e033b..0000000000000 --- a/www/apps/book/app/learn/advanced-development/workflows/page.mdx +++ /dev/null @@ -1,14 +0,0 @@ -export const metadata = { - title: `${pageNumber} Workflows Advanced Development`, -} - -# {metadata.title} - -In the next chapters, you'll learn about workflows in-depth and how to use them in your custom development. - -By the end of these chapters, you'll learn about: - -- Constructing a workflow and its constraints. -- Using a compensation function to undo a step's action when errors occur. -- Hooks and how to consume and expose them. -- Configurations to retry workflows or run them in the background. diff --git a/www/apps/book/app/learn/basics/admin-customizations/page.mdx b/www/apps/book/app/learn/basics/admin-customizations/page.mdx deleted file mode 100644 index aa604422d9b64..0000000000000 --- a/www/apps/book/app/learn/basics/admin-customizations/page.mdx +++ /dev/null @@ -1,64 +0,0 @@ -export const metadata = { - title: `${pageNumber} Admin Customizations`, -} - -# {metadata.title} - -In this chapter, you’ll learn how to customize the Medusa Admin dashboard. - -## What is the Medusa Admin? - -The Medusa Admin is an admin dashboard that merchants use to manage their store's data. - -You can extend the Medusa Admin to add widgets and new pages. In your customizations, you interact with API routes to provide merchants with custom functionalities. - -The Medusa Admin is installed in your Medusa application and runs at `http://localhost:9000/app` when you start the application. - ---- - -## Example: Create a Widget - -A widget is a React component that can be injected into an existing page in the admin dashboard. - -For example, create the file `src/admin/widgets/product-widget.tsx` with the following content: - -```tsx title="src/admin/widgets/product-widget.tsx" -import { defineWidgetConfig } from "@medusajs/admin-sdk" -import { Container, Heading } from "@medusajs/ui" - -const ProductWidget = () => { - return ( - -
- Product Widget -
-
- ) -} - -export const config = defineWidgetConfig({ - zone: "product.details.before", -}) - -export default ProductWidget -``` - -This inserts a widget with the text “Product Widget” at the beginning of a product’s details page. - -In your widget, use custom components from the [Medusa UI package](https://docs.medusajs.com/ui). - -### Test the Widget - -To test out the widget, start the Medusa application: - -```bash npm2yarn -npm run dev -``` - -Then, open a product’s details page in the Medusa Admin. You’ll find your custom widget at the top of the page. - ---- - -## Admin Components List - -To build admin customizations that match the Medusa Admin's designs and layouts, refer to [this guide](!resources!/admin-components) to find common components. diff --git a/www/apps/book/app/learn/basics/page.mdx b/www/apps/book/app/learn/basics/page.mdx deleted file mode 100644 index 39f031737da64..0000000000000 --- a/www/apps/book/app/learn/basics/page.mdx +++ /dev/null @@ -1,18 +0,0 @@ -export const metadata = { - title: `${pageNumber} The Basics`, -} - -# {metadata.title} - -In the next chapters, you’ll learn about the basic concepts of Medusa that are central to your development. - -By the end of these chapters, you’ll be able to: - -- Expose your custom functionalities through endpoints. -- Create custom modules that define custom business logic. -- Create custom tables in the database through data models. -- Execute scripts when the Medusa application starts. -- Perform asynchronous actions when an event occurs. -- Run tasks at a specified time or pattern during the Medusa application's runtime. -- Create custom flows as a series of steps involving multiple services. -- Customize the admin dashboard to inject components on existing pages or add new pages. diff --git a/www/apps/book/app/learn/basics/project-directories-files/page.mdx b/www/apps/book/app/learn/basics/project-directories-files/page.mdx deleted file mode 100644 index d883a574f69cc..0000000000000 --- a/www/apps/book/app/learn/basics/project-directories-files/page.mdx +++ /dev/null @@ -1,26 +0,0 @@ -export const metadata = { - title: `${pageNumber} Project File Conventions`, -} - -# {metadata.title} - -In this chapter, you’ll learn about important directories and files in your Medusa application's project. - -![A diagram of the directories overview](https://res.cloudinary.com/dza7lstvk/image/upload/v1732803813/Medusa%20Book/medusa-dir-overview_v7ks0j.jpg) - -## src - -This directory is the central place for your custom development. It includes the following sub-directories: - -- `admin`: Holds your admin dashboard's custom [widgets](../../advanced-development/admin/widgets/page.mdx) and [UI routes](../../advanced-development/admin/ui-routes/page.mdx). -- `api`: Holds your custom [API routes](../api-routes/page.mdx) that are added as endpoints in your Medusa application. -- `jobs`: Holds your [scheduled jobs](../scheduled-jobs/page.mdx) that run at a specified interval during your Medusa application's runtime. -- `links`: Holds you [module links](../../advanced-development/module-links/page.mdx) that build associations between data models of different modules. -- `modules`: Holds your custom [modules](../modules/page.mdx) that implement custom business logic. -- `scripts`: Holds your custom [scripts](../../advanced-development/custom-cli-scripts/page.mdx) to be executed using Medusa's CLI tool. -- `subscribers`: Holds your [event listeners](../events-and-subscribers/page.mdx) that are executed asynchronously whenever an event is emitted. -- `workflows`: Holds your custom [flows](../workflows/page.mdx) that can be executed from anywhere in your application. - -## medusa-config.ts - -This file holds your [Medusa configurations](!resources!/references/medusa-config), such as your PostgreSQL database configurations. diff --git a/www/apps/book/app/learn/build/page.mdx b/www/apps/book/app/learn/build/page.mdx index d0cc43bee76e6..033a3df6979c1 100644 --- a/www/apps/book/app/learn/build/page.mdx +++ b/www/apps/book/app/learn/build/page.mdx @@ -55,7 +55,7 @@ cp ../../.env .env.production -When `NODE_ENV=production`, the Medusa application loads the environment variables from `.env.production`. Learn more about environment variables in [this guide](../advanced-development/environment-variables/page.mdx). +When `NODE_ENV=production`, the Medusa application loads the environment variables from `.env.production`. Learn more about environment variables in [this guide](../fundamentals/environment-variables/page.mdx). 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 979a0e5a962d4..dc416aa76d1ec 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 @@ -27,7 +27,7 @@ You create an API route in a `route.{ts,js}` file under a sub-directory of the ` -Learn more about API routes [in this guide](../../../basics/api-routes/page.mdx). +Learn more about API routes [in this guide](../../../fundamentals/api-routes/page.mdx). @@ -63,7 +63,7 @@ export const POST = async ( 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. +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](../../../fundamentals/medusa-container/page.mdx) that holds framework tools and custom and core modules' services. @@ -85,7 +85,7 @@ Medusa uses [Zod](https://zod.dev/) to create validation schemas. These schemas -Learn more about API route validation in [this chapter](../../../advanced-development/api-routes/validation/page.mdx). +Learn more about API route validation in [this chapter](../../../fundamentals/api-routes/validation/page.mdx). @@ -123,7 +123,7 @@ A middleware is a function executed before the route handler when a request is s -Learn more about middlewares in [this chapter](../../../advanced-development/api-routes/middlewares/page.mdx). +Learn more about middlewares in [this chapter](../../../fundamentals/api-routes/middlewares/page.mdx). 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 07cd352b312d1..f49bd34622c47 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 @@ -12,7 +12,7 @@ In a module, you create data models and business logic to manage them. In the ne -Learn more about modules in [this chapter](../../../basics/modules/page.mdx). +Learn more about modules in [this chapter](../../../fundamentals/modules/page.mdx). @@ -30,7 +30,7 @@ A data model represents a table in the database. You create data models using Me -Learn more about data models in [this chapter](../../../basics/modules/page.mdx#1-create-data-model). +Learn more about data models in [this chapter](../../../fundamentals/modules/page.mdx#1-create-data-model). @@ -56,7 +56,7 @@ You define the data model using the `define` method of the `model` utility impor -Learn about other property types in [this chapter](../../../advanced-development/data-models/property-types/page.mdx). +Learn about other property types in [this chapter](../../../fundamentals/data-models/property-types/page.mdx). @@ -70,7 +70,7 @@ In this step, you'll create the Brand Module's service that provides methods to -Learn more about services in [this chapter](../../../basics/modules/page.mdx#2-create-service). +Learn more about services in [this chapter](../../../fundamentals/modules/page.mdx#2-create-service). @@ -168,7 +168,7 @@ A migration is a TypeScript or JavaScript file that defines database changes mad -Learn more about migrations in [this chapter](../../../basics/modules/page.mdx#5-generate-migrations). +Learn more about migrations in [this chapter](../../../fundamentals/modules/page.mdx#5-generate-migrations). 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 3b3a1268c6662..204dfc459576c 100644 --- a/www/apps/book/app/learn/customization/custom-features/page.mdx +++ b/www/apps/book/app/learn/customization/custom-features/page.mdx @@ -10,9 +10,9 @@ By following these guides, you'll add brands to the Medusa application that you To build a custom feature in Medusa, you need three main tools: -- [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. +- [Module](../../fundamentals/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](../../fundamentals/workflows/page.mdx): a tool to perform an operation comprising multiple steps with built-in rollback and retry mechanisms. +- [API route](../../fundamentals/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 aa4f20d261c1d..c76eae0929a05 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 @@ -14,7 +14,7 @@ The workflow you'll create in this chapter will use the Brand Module's service t -Learn more about workflows in [this chapter](../../../basics/workflows/page.mdx). +Learn more about workflows in [this chapter](../../../fundamentals/workflows/page.mdx). @@ -67,7 +67,7 @@ You create a `createBrandStep` using the `createStep` function. It accepts the s The step function receives two parameters: input passed to the step when it's invoked, and an object of general context and configurations. This object has a `container` property, which is the Medusa container. -The [Medusa container](../../../basics/medusa-container/page.mdx) is a registry of framework and commerce tools accessible in your customizations, such as a workflow's step. The Medusa application registers the services of core and custom modules in the container, allowing you to resolve and use them. +The [Medusa container](../../../fundamentals/medusa-container/page.mdx) is a registry of framework and commerce tools accessible in your customizations, such as a workflow's step. The Medusa application registers the services of core and custom modules in the container, allowing you to resolve and use them. So, In the step function, you use the Medusa container to resolve the Brand Module's service and use its generated `createBrands` method, which accepts an object of brands to create. @@ -85,7 +85,7 @@ You define for each step a compensation function that's executed when an error o -Learn more about the compensation function in [this chapter](../../../advanced-development/workflows/compensation-function/page.mdx). +Learn more about the compensation function in [this chapter](../../../fundamentals/workflows/compensation-function/page.mdx). diff --git a/www/apps/book/app/learn/customization/customize-admin/page.mdx b/www/apps/book/app/learn/customization/customize-admin/page.mdx index 806a4c78e4c9b..44b44001e2e97 100644 --- a/www/apps/book/app/learn/customization/customize-admin/page.mdx +++ b/www/apps/book/app/learn/customization/customize-admin/page.mdx @@ -8,8 +8,8 @@ In the previous chapters, you've customized your Medusa application to [add bran After customizing and extending your application with new features, you may need to provide an interface for admin users to utilize these features. The Medusa Admin dashboard is extendable, allowing you to: -- Insert components, called [widgets](../../advanced-development/admin/widgets/page.mdx), on existing pages. -- Add new pages, called [UI Routes](../../advanced-development/admin/ui-routes/page.mdx). +- Insert components, called [widgets](../../fundamentals/admin/widgets/page.mdx), on existing pages. +- Add new pages, called [UI Routes](../../fundamentals/admin/ui-routes/page.mdx). From these customizations, you can send requests to custom API routes, allowing admin users to manage custom resources on the dashboard diff --git a/www/apps/book/app/learn/customization/customize-admin/route/page.mdx b/www/apps/book/app/learn/customization/customize-admin/route/page.mdx index 37b5eacc96b94..ff5b9885127dd 100644 --- a/www/apps/book/app/learn/customization/customize-admin/route/page.mdx +++ b/www/apps/book/app/learn/customization/customize-admin/route/page.mdx @@ -19,7 +19,7 @@ In this chapter, you'll add a UI route to the admin dashboard that shows the all ## 1. Get Brands API Route -In a [previous chapter](../../extend-features/query-linked-records/page.mdx), you learned how to add an API route that retrieves brands and their products using [Query](../../../advanced-development/module-links/query/page.mdx). You'll expand that API route to support pagination, so that on the admin dashboard you can show the brands in a paginated table. +In a [previous chapter](../../extend-features/query-linked-records/page.mdx), you learned how to add an API route that retrieves brands and their products using [Query](../../../fundamentals/module-links/query/page.mdx). You'll expand that API route to support pagination, so that on the admin dashboard you can show the brands in a paginated table. Replace or create the `GET` API route at `src/api/admin/brands/route.ts` with the following: @@ -76,7 +76,7 @@ You return in the response the retrieved brands and the pagination configuration -Learn more about pagination with Query in [this chapter](../../../advanced-development/module-links/query/page.mdx#apply-pagination). +Learn more about pagination with Query in [this chapter](../../../fundamentals/module-links/query/page.mdx#apply-pagination). @@ -138,7 +138,7 @@ By applying the above middleware, you can pass pagination configurations to `GET -Learn more about using the `validateAndTransformQuery` middleware to configure Query in [this chapter](../../../advanced-development/module-links/query/page.mdx#request-query-configurations). +Learn more about using the `validateAndTransformQuery` middleware to configure Query in [this chapter](../../../fundamentals/module-links/query/page.mdx#request-query-configurations). @@ -186,7 +186,7 @@ You'll now add the UI route that shows the paginated list of brands. A UI route -Learn more about UI routes in [this chapter](../../../advanced-development/admin/ui-routes/page.mdx). +Learn more about UI routes in [this chapter](../../../fundamentals/admin/ui-routes/page.mdx). diff --git a/www/apps/book/app/learn/customization/customize-admin/widget/page.mdx b/www/apps/book/app/learn/customization/customize-admin/widget/page.mdx index a78b64474ec37..13ac3187e9bb5 100644 --- a/www/apps/book/app/learn/customization/customize-admin/widget/page.mdx +++ b/www/apps/book/app/learn/customization/customize-admin/widget/page.mdx @@ -59,7 +59,7 @@ You'll now add a widget to the product-details page. A widget is a React compone -Learn more about widgets in [this documentation](../../../advanced-development/admin/widgets/page.mdx). +Learn more about widgets in [this documentation](../../../fundamentals/admin/widgets/page.mdx). diff --git a/www/apps/book/app/learn/customization/extend-features/define-link/page.mdx b/www/apps/book/app/learn/customization/extend-features/define-link/page.mdx index c671d0cfbc32c..3687c4134beb5 100644 --- a/www/apps/book/app/learn/customization/extend-features/define-link/page.mdx +++ b/www/apps/book/app/learn/customization/extend-features/define-link/page.mdx @@ -8,7 +8,7 @@ export const metadata = { In this chapter, you'll learn how to define a module link between a brand defined in the [custom Brand Module](../../custom-features/module/page.mdx), and a product defined in the [Product Module](!resources!/commerce-modules/product) that's available in your Medusa application out-of-the-box. -Modules are [isolated](../../../advanced-development/modules/isolation/page.mdx) from other resources, ensuring that they're integrated into the Medusa application without side effects. However, you may need to associate data models of different modules, or you're trying to extend data models from commerce modules with custom properties. To do that, you define module links. +Modules are [isolated](../../../fundamentals/modules/isolation/page.mdx) from other resources, ensuring that they're integrated into the Medusa application without side effects. However, you may need to associate data models of different modules, or you're trying to extend data models from commerce modules with custom properties. To do that, you define module links. A module link forms an association between two data models of different modules while maintaining module isolation. You can then manage and query linked records of the data models using Medusa's Modules SDK. @@ -16,7 +16,7 @@ In this chapter, you'll define a module link between the `Brand` data model of t -Learn more about module links in [this chapters](../../../advanced-development/module-links/page.mdx). +Learn more about module links in [this chapters](../../../fundamentals/module-links/page.mdx). diff --git a/www/apps/book/app/learn/customization/extend-features/extend-create-product/page.mdx b/www/apps/book/app/learn/customization/extend-features/extend-create-product/page.mdx index 8db53721fe149..512744fdd440f 100644 --- a/www/apps/book/app/learn/customization/extend-features/extend-create-product/page.mdx +++ b/www/apps/book/app/learn/customization/extend-features/extend-create-product/page.mdx @@ -8,7 +8,7 @@ export const metadata = { After linking the [custom Brand data model](../../custom-features/module/page.mdx) and Medusa's [Product Module](!resources!/commerce-modules/product) in the [previous chapter](../define-link/page.mdx), you'll extend the create product workflow and API route to allow associating a brand with a product. -Some API routes, including the [Create Product API route](!api!/admin#products_postproducts), accept an `additional_data` request body parameter. This parameter can hold custom data that's passed to the [hooks](../../../advanced-development/workflows/workflow-hooks/page.mdx) of the workflow executed in the API route, allowing you to consume those hooks and perform actions with the custom data. +Some API routes, including the [Create Product API route](!api!/admin#products_postproducts), accept an `additional_data` request body parameter. This parameter can hold custom data that's passed to the [hooks](../../../fundamentals/workflows/workflow-hooks/page.mdx) of the workflow executed in the API route, allowing you to consume those hooks and perform actions with the custom data. So, in this chapter, to extend the create product flow and associate a brand with a product, you will: @@ -17,7 +17,7 @@ So, in this chapter, to extend the create product flow and associate a brand wit -To learn more about the `additional_data` property and the API routes that accept additional data, refer to [this chapter](../../../advanced-development/api-routes/additional-data/page.mdx). +To learn more about the `additional_data` property and the API routes that accept additional data, refer to [this chapter](../../../fundamentals/api-routes/additional-data/page.mdx). @@ -42,7 +42,7 @@ A workflow hook is a point in a workflow where you can inject a step to perform -Learn more about the workflow hooks in [this chapter](../../../advanced-development/workflows/workflow-hooks/page.mdx). +Learn more about the workflow hooks in [this chapter](../../../fundamentals/workflows/workflow-hooks/page.mdx). @@ -87,7 +87,7 @@ createProductsWorkflow.hooks.productsCreated( Workflows have a special `hooks` property to access its hooks and consume them. Each hook, such as `productCreated`, accepts a step function as a parameter. The step function accepts the following parameters: 1. An object having an `additional_data` property, which is the custom data passed in the request body under `additional_data`. The object will also have properties passed from the workflow to the hook, which in this case is the `products` property that holds an array of the created products. -2. An object of properties related to the step's context. It has a `container` property whose value is the [Medusa container](../../../basics/medusa-container/page.mdx) to resolve framework and commerce tools. +2. An object of properties related to the step's context. It has a `container` property whose value is the [Medusa container](../../../fundamentals/medusa-container/page.mdx) to resolve framework and commerce tools. In the step, if a brand ID is passed in `additional_data`, you resolve the Brand Module's service and use its generated `retrieveBrand` method to retrieve the brand by its ID. The `retrieveBrand` method will throw an error if the brand doesn't exist. @@ -97,7 +97,7 @@ Next, you want to create a link between the created products and the brand. To d -Learn more about the remote link in [this chapter](../../../advanced-development/module-links/remote-link/page.mdx). +Learn more about the remote link in [this chapter](../../../fundamentals/module-links/remote-link/page.mdx). diff --git a/www/apps/book/app/learn/customization/extend-features/page.mdx b/www/apps/book/app/learn/customization/extend-features/page.mdx index 8cea08345e77b..2f0e066aac5ca 100644 --- a/www/apps/book/app/learn/customization/extend-features/page.mdx +++ b/www/apps/book/app/learn/customization/extend-features/page.mdx @@ -10,9 +10,9 @@ In other commerce platforms, you extend core features and models through hacky w Medusa's framework and orchestration tools mitigate these issues while supporting all your customization needs: -- [Module Links](../../advanced-development/module-links/page.mdx): Link data models of different modules without building direct dependencies, ensuring that the Medusa application integrates your modules without side effects. -- [Workflow Hooks](../../advanced-development/workflows/workflow-hooks/page.mdx): inject custom functionalities into a workflow at predefined points, called hooks. This allows you to perform custom actions as a part of a core workflow without hacky workarounds. -- [Additional Data in API Routes](../../advanced-development/api-routes/additional-data/page.mdx): Configure core API routes to accept request parameters relevant to your customizations. These parameters are passed to the underlying workflow's hooks, where you can manage your custom data as part of an existing flow. +- [Module Links](../../fundamentals/module-links/page.mdx): Link data models of different modules without building direct dependencies, ensuring that the Medusa application integrates your modules without side effects. +- [Workflow Hooks](../../fundamentals/workflows/workflow-hooks/page.mdx): inject custom functionalities into a workflow at predefined points, called hooks. This allows you to perform custom actions as a part of a core workflow without hacky workarounds. +- [Additional Data in API Routes](../../fundamentals/api-routes/additional-data/page.mdx): Configure core API routes to accept request parameters relevant to your customizations. These parameters are passed to the underlying workflow's hooks, where you can manage your custom data as part of an existing flow. --- diff --git a/www/apps/book/app/learn/customization/extend-features/query-linked-records/page.mdx b/www/apps/book/app/learn/customization/extend-features/query-linked-records/page.mdx index 2449a7e5a54f1..06c755ff91488 100644 --- a/www/apps/book/app/learn/customization/extend-features/query-linked-records/page.mdx +++ b/www/apps/book/app/learn/customization/extend-features/query-linked-records/page.mdx @@ -74,7 +74,7 @@ You can also retrieve linked records using Query. Query allows you to retrieve d -Learn more about Query in [this chapter](../../../advanced-development/module-links/query/page.mdx). +Learn more about Query in [this chapter](../../../fundamentals/module-links/query/page.mdx). diff --git a/www/apps/book/app/learn/customization/integrate-systems/handle-event/page.mdx b/www/apps/book/app/learn/customization/integrate-systems/handle-event/page.mdx index dc9f7e51ad619..11cf42a3678d4 100644 --- a/www/apps/book/app/learn/customization/integrate-systems/handle-event/page.mdx +++ b/www/apps/book/app/learn/customization/integrate-systems/handle-event/page.mdx @@ -10,11 +10,11 @@ In the [previous chapter](../service/page.mdx), you created a CMS Module that in In another previous chapter, you [added a workflow](../../custom-features/workflow/page.mdx) that creates a brand. After integrating the CMS, you want to sync that brand to the third-party system as well. -Medusa has an event system that emits events when an operation is performed. It allows you to listen to those events and perform an asynchronous action in a function called a [subscriber](../../../basics/events-and-subscribers/page.mdx). This is useful to perform actions that aren't integral to the original flow, such as syncing data to a third-party system. +Medusa has an event system that emits events when an operation is performed. It allows you to listen to those events and perform an asynchronous action in a function called a [subscriber](../../../fundamentals/events-and-subscribers/page.mdx). This is useful to perform actions that aren't integral to the original flow, such as syncing data to a third-party system. -Learn more about Medusa's event system and subscribers in [this chapter](../../../basics/events-and-subscribers/page.mdx). +Learn more about Medusa's event system and subscribers in [this chapter](../../../fundamentals/events-and-subscribers/page.mdx). @@ -87,13 +87,13 @@ Workflows have a built-in durable execution engine that helps you complete tasks -Learn more about workflows in [this chapter](../../../basics/workflows/page.mdx). +Learn more about workflows in [this chapter](../../../fundamentals/workflows/page.mdx). You'll create a `syncBrandToSystemWorkflow` that has two steps: -- `useQueryGraphStep`: a step that Medusa provides to retrieve data using [Query](../../../advanced-development/module-links/query/page.mdx). You'll use this to retrieve the brand's details using its ID. +- `useQueryGraphStep`: a step that Medusa provides to retrieve data using [Query](../../../fundamentals/module-links/query/page.mdx). You'll use this to retrieve the brand's details using its ID. - `syncBrandToCmsStep`: a step that you'll create to sync the brand to the CMS. ### syncBrandToCmsStep @@ -142,13 +142,13 @@ const syncBrandToCmsStep = createStep( ) ``` -You create the `syncBrandToCmsStep` that accepts a brand as an input. In the step, you resolve the CMS Module's service from the [Medusa container](../../../basics/medusa-container/page.mdx) and use its `createBrand` method. This method will create the brand in the third-party CMS. +You create the `syncBrandToCmsStep` that accepts a brand as an input. In the step, you resolve the CMS Module's service from the [Medusa container](../../../fundamentals/medusa-container/page.mdx) and use its `createBrand` method. This method will create the brand in the third-party CMS. You also pass the brand's ID to the step's compensation function. In this function, you delete the brand in the third-party CMS if an error occurs during the workflow's execution. -Learn more about compensation functions in [this chapter](../../../advanced-development/workflows/compensation-function/page.mdx). +Learn more about compensation functions in [this chapter](../../../fundamentals/workflows/compensation-function/page.mdx). @@ -270,7 +270,7 @@ In the function, you execute the `syncBrandToCmsWorkflow`, passing it the data p -Learn more about subscribers in [this chapter](../../../basics/events-and-subscribers/page.mdx). +Learn more about subscribers in [this chapter](../../../fundamentals/events-and-subscribers/page.mdx). diff --git a/www/apps/book/app/learn/customization/integrate-systems/page.mdx b/www/apps/book/app/learn/customization/integrate-systems/page.mdx index ae4b51a07dd0a..c09ef24a2f528 100644 --- a/www/apps/book/app/learn/customization/integrate-systems/page.mdx +++ b/www/apps/book/app/learn/customization/integrate-systems/page.mdx @@ -12,7 +12,7 @@ In Medusa, you integrate a third-party system by: 1. Creating a module whose service provides the methods to connect to and perform operations in the third-party system. 2. Building workflows that complete tasks spanning across systems. You use the module that integrates a third-party system in the workflow's steps. -3. Executing the workflows you built in an [API route](../../basics/api-routes/page.mdx), at a scheduled time, or when an event is emitted. +3. Executing the workflows you built in an [API route](../../fundamentals/api-routes/page.mdx), at a scheduled time, or when an event is emitted. --- diff --git a/www/apps/book/app/learn/customization/integrate-systems/schedule-task/page.mdx b/www/apps/book/app/learn/customization/integrate-systems/schedule-task/page.mdx index 5c21a584edbb2..4686c08f05fcd 100644 --- a/www/apps/book/app/learn/customization/integrate-systems/schedule-task/page.mdx +++ b/www/apps/book/app/learn/customization/integrate-systems/schedule-task/page.mdx @@ -14,7 +14,7 @@ You can create an action to be automatically executed at a specified interval us -Learn more about scheduled jobs in [this chapter](../../../basics/scheduled-jobs/page.mdx). +Learn more about scheduled jobs in [this chapter](../../../fundamentals/scheduled-jobs/page.mdx). @@ -39,7 +39,7 @@ Workflows have a built-in durable execution engine that helps you complete tasks -Learn more about workflows in [this chapter](../../../basics/workflows/page.mdx). +Learn more about workflows in [this chapter](../../../fundamentals/workflows/page.mdx). @@ -134,7 +134,7 @@ The step passes the created brands to the compensation function, which deletes t -Learn more about compensation functions in [this chapter](../../../advanced-development/workflows/compensation-function/page.mdx). +Learn more about compensation functions in [this chapter](../../../fundamentals/workflows/compensation-function/page.mdx). @@ -220,11 +220,11 @@ export const syncBrandsFromSystemWorkflow = createWorkflow( In the workflow, you only use the `retrieveBrandsFromSystemStep` for now, which retrieves the brands from the third-party CMS. -Next, you need to identify which brands must be created or updated. Since workflows are constructed internally and are only evaluated during execution, you can't access values to perform data manipulation directly. Instead, use [transform](../../../advanced-development/workflows/variable-manipulation/page.mdx) from the Workflows SDK that gives you access to the real-time values of the data, allowing you to create new variables using those values. +Next, you need to identify which brands must be created or updated. Since workflows are constructed internally and are only evaluated during execution, you can't access values to perform data manipulation directly. Instead, use [transform](../../../fundamentals/workflows/variable-manipulation/page.mdx) from the Workflows SDK that gives you access to the real-time values of the data, allowing you to create new variables using those values. -Learn more about data manipulation using `transform` in [this chapter](../../../advanced-development/workflows/variable-manipulation/page.mdx). +Learn more about data manipulation using `transform` in [this chapter](../../../fundamentals/workflows/variable-manipulation/page.mdx). @@ -320,7 +320,7 @@ A scheduled job file must export: - `name`: A unique name for the scheduled job. - `schedule`: A string that holds a [cron expression](https://crontab.guru/) indicating the schedule to run the job. -The scheduled job function accepts as a parameter the [Medusa container](../../../basics/medusa-container/page.mdx) used to resolve framework and commerce tools. You then execute the `syncBrandsFromCmsWorkflow` and use its result to log how many brands were created or updated. +The scheduled job function accepts as a parameter the [Medusa container](../../../fundamentals/medusa-container/page.mdx) used to resolve framework and commerce tools. You then execute the `syncBrandsFromCmsWorkflow` and use its result to log how many brands were created or updated. Based on the cron expression specified in `config.schedule`, Medusa will run the scheduled job every day at midnight. You can also change it to `* * * * *` to run it every minute for easier debugging. diff --git a/www/apps/book/app/learn/customization/integrate-systems/service/page.mdx b/www/apps/book/app/learn/customization/integrate-systems/service/page.mdx index daf0fb1121bab..29ee23c0fabf8 100644 --- a/www/apps/book/app/learn/customization/integrate-systems/service/page.mdx +++ b/www/apps/book/app/learn/customization/integrate-systems/service/page.mdx @@ -10,7 +10,7 @@ In the previous chapters, you've created a [Brand Module](../../custom-features/ -Learn more about modules in [this chapter](../../../basics/modules/page.mdx). +Learn more about modules in [this chapter](../../../fundamentals/modules/page.mdx). @@ -66,7 +66,7 @@ export default CmsModuleService You create a `CmsModuleService` that will hold the methods to connect to the third-party CMS. A service's constructor accepts two parameters: -1. The module's container. Since a module is [isolated](../../../advanced-development/modules/isolation/page.mdx), it has a [local container](../../../advanced-development/modules/container/page.mdx) different than the Medusa container you use in other customizations. This container holds framework tools like the [Logger utility](../../../debugging-and-testing/logging/page.mdx) and resources within the module. +1. The module's container. Since a module is [isolated](../../../fundamentals/modules/isolation/page.mdx), it has a [local container](../../../fundamentals/modules/container/page.mdx) different than the Medusa container you use in other customizations. This container holds framework tools like the [Logger utility](../../../debugging-and-testing/logging/page.mdx) and resources within the module. 2. Options passed to the module when it's later added in Medusa's configurations. These options are useful to pass secret keys or configurations that ensure your module is re-usable across applications. For the CMS Module, you accept the API key to connect to the dummy CMS as an option. When integrating a third-party system that has a Node.js SDK or client, you can initialize that client in the constructor to be used in the service's methods. diff --git a/www/apps/book/app/learn/customization/page.mdx b/www/apps/book/app/learn/customization/page.mdx deleted file mode 100644 index 9e794cdcf700b..0000000000000 --- a/www/apps/book/app/learn/customization/page.mdx +++ /dev/null @@ -1,14 +0,0 @@ -export const metadata = { - title: `${pageNumber} Custom Development`, -} - -# {metadata.title} - -In the next chapters, you’ll customize Medusa using the concepts you learned. - -You'll follow an example of building a Brands Module that leads you through: - -1. The main concepts you need to build custom features. -2. How to extend data models that are in the Commerce Modules. -3. How to use your custom features in pages and widgets in the admin dashboard. -4. How to integrate third-party systems into your Medusa application. \ No newline at end of file diff --git a/www/apps/book/app/learn/first-customizations/page.mdx b/www/apps/book/app/learn/first-customizations/page.mdx deleted file mode 100644 index 2eac7314456c5..0000000000000 --- a/www/apps/book/app/learn/first-customizations/page.mdx +++ /dev/null @@ -1,67 +0,0 @@ -export const metadata = { - title: `${pageNumber} Your First Customizations`, -} - -# {metadata.title} - -In this chapter, you’ll build your first customizations with Medusa. - -You'll create a `GET` API route at the path `/hello-world`. An API route exposes your application's commerce features to frontend clients, such as the admin dashboard or a storefront. - -Medusa has [Store](!api!/store) and [Admin](!api!/admin) API routes, and you can create custom API routes for your custom features. - -## 1. Create API Route - -API Routes expose business logic through REST APIs. You can create custom API routes to expose custom business logic. - -To create an API route, create the file `src/api/hello-world/route.ts` with the following content: - -```ts title="src/api/hello-world/route.ts" -import type { - MedusaRequest, - MedusaResponse, -} from "@medusajs/framework/http" - -export const GET = ( - req: MedusaRequest, - res: MedusaResponse -) => { - res.json({ - message: "Hello, world!", - }) -} -``` - ---- - -## 2. Test Customizations - -To test out your customizations: - -1. Start the Medusa application: - -```bash npm2yarn -npm run dev -``` - -2. Send a `GET` request to `/hello-world`: - -```bash -curl http://localhost:9000/hello-world -``` - -In the response, you’ll receive the following object: - -```json -{ - "message": "Hello, world!" -} -``` - ---- - -## Next Steps - -Congratulations, you’ve made your first customization with Medusa! - -You can now start your in-depth learning journey to become an expert. diff --git a/www/apps/book/app/learn/advanced-development/admin/constraints/page.mdx b/www/apps/book/app/learn/fundamentals/admin/constraints/page.mdx similarity index 100% rename from www/apps/book/app/learn/advanced-development/admin/constraints/page.mdx rename to www/apps/book/app/learn/fundamentals/admin/constraints/page.mdx diff --git a/www/apps/book/app/learn/advanced-development/admin/page.mdx b/www/apps/book/app/learn/fundamentals/admin/page.mdx similarity index 100% rename from www/apps/book/app/learn/advanced-development/admin/page.mdx rename to www/apps/book/app/learn/fundamentals/admin/page.mdx diff --git a/www/apps/book/app/learn/advanced-development/admin/tips/page.mdx b/www/apps/book/app/learn/fundamentals/admin/tips/page.mdx similarity index 100% rename from www/apps/book/app/learn/advanced-development/admin/tips/page.mdx rename to www/apps/book/app/learn/fundamentals/admin/tips/page.mdx diff --git a/www/apps/book/app/learn/advanced-development/admin/ui-routes/page.mdx b/www/apps/book/app/learn/fundamentals/admin/ui-routes/page.mdx similarity index 100% rename from www/apps/book/app/learn/advanced-development/admin/ui-routes/page.mdx rename to www/apps/book/app/learn/fundamentals/admin/ui-routes/page.mdx diff --git a/www/apps/book/app/learn/advanced-development/admin/widgets/page.mdx b/www/apps/book/app/learn/fundamentals/admin/widgets/page.mdx similarity index 100% rename from www/apps/book/app/learn/advanced-development/admin/widgets/page.mdx rename to www/apps/book/app/learn/fundamentals/admin/widgets/page.mdx diff --git a/www/apps/book/app/learn/advanced-development/api-routes/additional-data/page.mdx b/www/apps/book/app/learn/fundamentals/api-routes/additional-data/page.mdx similarity index 98% rename from www/apps/book/app/learn/advanced-development/api-routes/additional-data/page.mdx rename to www/apps/book/app/learn/fundamentals/api-routes/additional-data/page.mdx index e275fe20ae750..86d9c4cf0b651 100644 --- a/www/apps/book/app/learn/advanced-development/api-routes/additional-data/page.mdx +++ b/www/apps/book/app/learn/fundamentals/api-routes/additional-data/page.mdx @@ -130,7 +130,7 @@ The `additional_data` is then passed to hooks in the `createProductsWorkflow` us -Learn about workflow hooks in [this guide](../../workflows/workflow-hooks/page.mdx). +Learn about workflow hooks in [this guide](../../../fundamentals/workflows/workflow-hooks/page.mdx). diff --git a/www/apps/book/app/learn/advanced-development/api-routes/cors/page.mdx b/www/apps/book/app/learn/fundamentals/api-routes/cors/page.mdx similarity index 100% rename from www/apps/book/app/learn/advanced-development/api-routes/cors/page.mdx rename to www/apps/book/app/learn/fundamentals/api-routes/cors/page.mdx diff --git a/www/apps/book/app/learn/advanced-development/api-routes/errors/page.mdx b/www/apps/book/app/learn/fundamentals/api-routes/errors/page.mdx similarity index 100% rename from www/apps/book/app/learn/advanced-development/api-routes/errors/page.mdx rename to www/apps/book/app/learn/fundamentals/api-routes/errors/page.mdx diff --git a/www/apps/book/app/learn/advanced-development/api-routes/http-methods/page.mdx b/www/apps/book/app/learn/fundamentals/api-routes/http-methods/page.mdx similarity index 100% rename from www/apps/book/app/learn/advanced-development/api-routes/http-methods/page.mdx rename to www/apps/book/app/learn/fundamentals/api-routes/http-methods/page.mdx diff --git a/www/apps/book/app/learn/advanced-development/api-routes/middlewares/page.mdx b/www/apps/book/app/learn/fundamentals/api-routes/middlewares/page.mdx similarity index 100% rename from www/apps/book/app/learn/advanced-development/api-routes/middlewares/page.mdx rename to www/apps/book/app/learn/fundamentals/api-routes/middlewares/page.mdx diff --git a/www/apps/book/app/learn/basics/api-routes/page.mdx b/www/apps/book/app/learn/fundamentals/api-routes/page.mdx similarity index 100% rename from www/apps/book/app/learn/basics/api-routes/page.mdx rename to www/apps/book/app/learn/fundamentals/api-routes/page.mdx diff --git a/www/apps/book/app/learn/advanced-development/api-routes/parameters/page.mdx b/www/apps/book/app/learn/fundamentals/api-routes/parameters/page.mdx similarity index 100% rename from www/apps/book/app/learn/advanced-development/api-routes/parameters/page.mdx rename to www/apps/book/app/learn/fundamentals/api-routes/parameters/page.mdx diff --git a/www/apps/book/app/learn/advanced-development/api-routes/protected-routes/page.mdx b/www/apps/book/app/learn/fundamentals/api-routes/protected-routes/page.mdx similarity index 100% rename from www/apps/book/app/learn/advanced-development/api-routes/protected-routes/page.mdx rename to www/apps/book/app/learn/fundamentals/api-routes/protected-routes/page.mdx diff --git a/www/apps/book/app/learn/advanced-development/api-routes/responses/page.mdx b/www/apps/book/app/learn/fundamentals/api-routes/responses/page.mdx similarity index 100% rename from www/apps/book/app/learn/advanced-development/api-routes/responses/page.mdx rename to www/apps/book/app/learn/fundamentals/api-routes/responses/page.mdx diff --git a/www/apps/book/app/learn/advanced-development/api-routes/validation/page.mdx b/www/apps/book/app/learn/fundamentals/api-routes/validation/page.mdx similarity index 99% rename from www/apps/book/app/learn/advanced-development/api-routes/validation/page.mdx rename to www/apps/book/app/learn/fundamentals/api-routes/validation/page.mdx index d31df8df1b759..ca6203b2a09c7 100644 --- a/www/apps/book/app/learn/advanced-development/api-routes/validation/page.mdx +++ b/www/apps/book/app/learn/fundamentals/api-routes/validation/page.mdx @@ -208,7 +208,7 @@ export default defineMiddlewares({ The `validateAndTransformQuery` accepts two parameters: - The first one is the Zod schema to validate the query parameters against. -- The second one is an object of options for retrieving data using Query, which you can learn more about in [this chapter](../../module-links/query/page.mdx). +- The second one is an object of options for retrieving data using Query, which you can learn more about in [this chapter](../../../fundamentals/module-links/query/page.mdx). #### How the Validation Works diff --git a/www/apps/book/app/learn/advanced-development/custom-cli-scripts/page.mdx b/www/apps/book/app/learn/fundamentals/custom-cli-scripts/page.mdx similarity index 100% rename from www/apps/book/app/learn/advanced-development/custom-cli-scripts/page.mdx rename to www/apps/book/app/learn/fundamentals/custom-cli-scripts/page.mdx diff --git a/www/apps/book/app/learn/advanced-development/custom-cli-scripts/seed-data/page.mdx b/www/apps/book/app/learn/fundamentals/custom-cli-scripts/seed-data/page.mdx similarity index 100% rename from www/apps/book/app/learn/advanced-development/custom-cli-scripts/seed-data/page.mdx rename to www/apps/book/app/learn/fundamentals/custom-cli-scripts/seed-data/page.mdx diff --git a/www/apps/book/app/learn/advanced-development/data-models/check-constraints/page.mdx b/www/apps/book/app/learn/fundamentals/data-models/check-constraints/page.mdx similarity index 100% rename from www/apps/book/app/learn/advanced-development/data-models/check-constraints/page.mdx rename to www/apps/book/app/learn/fundamentals/data-models/check-constraints/page.mdx diff --git a/www/apps/book/app/learn/advanced-development/data-models/configure-properties/page.mdx b/www/apps/book/app/learn/fundamentals/data-models/configure-properties/page.mdx similarity index 100% rename from www/apps/book/app/learn/advanced-development/data-models/configure-properties/page.mdx rename to www/apps/book/app/learn/fundamentals/data-models/configure-properties/page.mdx diff --git a/www/apps/book/app/learn/advanced-development/data-models/default-properties/page.mdx b/www/apps/book/app/learn/fundamentals/data-models/default-properties/page.mdx similarity index 100% rename from www/apps/book/app/learn/advanced-development/data-models/default-properties/page.mdx rename to www/apps/book/app/learn/fundamentals/data-models/default-properties/page.mdx diff --git a/www/apps/book/app/learn/advanced-development/data-models/index/page.mdx b/www/apps/book/app/learn/fundamentals/data-models/index/page.mdx similarity index 100% rename from www/apps/book/app/learn/advanced-development/data-models/index/page.mdx rename to www/apps/book/app/learn/fundamentals/data-models/index/page.mdx diff --git a/www/apps/book/app/learn/advanced-development/data-models/infer-type/page.mdx b/www/apps/book/app/learn/fundamentals/data-models/infer-type/page.mdx similarity index 100% rename from www/apps/book/app/learn/advanced-development/data-models/infer-type/page.mdx rename to www/apps/book/app/learn/fundamentals/data-models/infer-type/page.mdx diff --git a/www/apps/book/app/learn/advanced-development/data-models/manage-relationships/page.mdx b/www/apps/book/app/learn/fundamentals/data-models/manage-relationships/page.mdx similarity index 100% rename from www/apps/book/app/learn/advanced-development/data-models/manage-relationships/page.mdx rename to www/apps/book/app/learn/fundamentals/data-models/manage-relationships/page.mdx diff --git a/www/apps/book/app/learn/advanced-development/data-models/page.mdx b/www/apps/book/app/learn/fundamentals/data-models/page.mdx similarity index 57% rename from www/apps/book/app/learn/advanced-development/data-models/page.mdx rename to www/apps/book/app/learn/fundamentals/data-models/page.mdx index 501244bdc6a48..8c7e6a6569ad1 100644 --- a/www/apps/book/app/learn/advanced-development/data-models/page.mdx +++ b/www/apps/book/app/learn/fundamentals/data-models/page.mdx @@ -4,9 +4,9 @@ export const metadata = { # {metadata.title} -In the next chapters, you'll learn more about defining data models. +Data models are created and managed in a module. To learn how to create a data model in a custom module, refer to the [Modules chapter](../modules/page.mdx). -You'll learn about: +In the next chapters, you'll learn about defining data models in more details. You'll learn about: - The different property types available. - How to set a property as a primary key. diff --git a/www/apps/book/app/learn/advanced-development/data-models/primary-key/page.mdx b/www/apps/book/app/learn/fundamentals/data-models/primary-key/page.mdx similarity index 100% rename from www/apps/book/app/learn/advanced-development/data-models/primary-key/page.mdx rename to www/apps/book/app/learn/fundamentals/data-models/primary-key/page.mdx diff --git a/www/apps/book/app/learn/advanced-development/data-models/property-types/page.mdx b/www/apps/book/app/learn/fundamentals/data-models/property-types/page.mdx similarity index 100% rename from www/apps/book/app/learn/advanced-development/data-models/property-types/page.mdx rename to www/apps/book/app/learn/fundamentals/data-models/property-types/page.mdx diff --git a/www/apps/book/app/learn/advanced-development/data-models/relationships/page.mdx b/www/apps/book/app/learn/fundamentals/data-models/relationships/page.mdx similarity index 100% rename from www/apps/book/app/learn/advanced-development/data-models/relationships/page.mdx rename to www/apps/book/app/learn/fundamentals/data-models/relationships/page.mdx diff --git a/www/apps/book/app/learn/advanced-development/data-models/searchable-property/page.mdx b/www/apps/book/app/learn/fundamentals/data-models/searchable-property/page.mdx similarity index 100% rename from www/apps/book/app/learn/advanced-development/data-models/searchable-property/page.mdx rename to www/apps/book/app/learn/fundamentals/data-models/searchable-property/page.mdx diff --git a/www/apps/book/app/learn/advanced-development/data-models/write-migration/page.mdx b/www/apps/book/app/learn/fundamentals/data-models/write-migration/page.mdx similarity index 100% rename from www/apps/book/app/learn/advanced-development/data-models/write-migration/page.mdx rename to www/apps/book/app/learn/fundamentals/data-models/write-migration/page.mdx diff --git a/www/apps/book/app/learn/advanced-development/environment-variables/page.mdx b/www/apps/book/app/learn/fundamentals/environment-variables/page.mdx similarity index 100% rename from www/apps/book/app/learn/advanced-development/environment-variables/page.mdx rename to www/apps/book/app/learn/fundamentals/environment-variables/page.mdx diff --git a/www/apps/book/app/learn/advanced-development/events-and-subscribers/data-payload/page.mdx b/www/apps/book/app/learn/fundamentals/events-and-subscribers/data-payload/page.mdx similarity index 100% rename from www/apps/book/app/learn/advanced-development/events-and-subscribers/data-payload/page.mdx rename to www/apps/book/app/learn/fundamentals/events-and-subscribers/data-payload/page.mdx diff --git a/www/apps/book/app/learn/advanced-development/events-and-subscribers/emit-event/page.mdx b/www/apps/book/app/learn/fundamentals/events-and-subscribers/emit-event/page.mdx similarity index 100% rename from www/apps/book/app/learn/advanced-development/events-and-subscribers/emit-event/page.mdx rename to www/apps/book/app/learn/fundamentals/events-and-subscribers/emit-event/page.mdx diff --git a/www/apps/book/app/learn/basics/events-and-subscribers/page.mdx b/www/apps/book/app/learn/fundamentals/events-and-subscribers/page.mdx similarity index 92% rename from www/apps/book/app/learn/basics/events-and-subscribers/page.mdx rename to www/apps/book/app/learn/fundamentals/events-and-subscribers/page.mdx index 61e08861a8e71..89457a41d9c3b 100644 --- a/www/apps/book/app/learn/basics/events-and-subscribers/page.mdx +++ b/www/apps/book/app/learn/fundamentals/events-and-subscribers/page.mdx @@ -20,7 +20,7 @@ Subscribers are useful to perform actions that aren't integral to the original f -If the action you're performing is integral to the main flow of the core commerce feature, use [workflow hooks](../../advanced-development/workflows/workflow-hooks/page.mdx) instead. +If the action you're performing is integral to the main flow of the core commerce feature, use [workflow hooks](../workflows/workflow-hooks/page.mdx) instead. @@ -105,4 +105,4 @@ Medusa provides two Event Modules out of the box: - [Local Event Module](!resources!/architectural-modules/event/local), used by default. It's useful for development, as you don't need additional setup to use it. - [Redis Event Module](!resources!/architectural-modules/event/redis), which is useful in production. It uses [Redis](https://redis.io/) to implement Medusa's pub/sub events system. -Medusa's [architecture](../../advanced-development/architecture/overview/page.mdx) also allows you to build a custom Event Module that uses a different service or logic to implement the pub/sub system. Learn how to build an Event Module in [this guide](!resources!/architectural-modules/event/create). +Medusa's [architecture](../../introduction/architecture/page.mdx) also allows you to build a custom Event Module that uses a different service or logic to implement the pub/sub system. Learn how to build an Event Module in [this guide](!resources!/architectural-modules/event/create). diff --git a/www/apps/book/app/learn/basics/medusa-container/page.mdx b/www/apps/book/app/learn/fundamentals/medusa-container/page.mdx similarity index 78% rename from www/apps/book/app/learn/basics/medusa-container/page.mdx rename to www/apps/book/app/learn/fundamentals/medusa-container/page.mdx index e1c883efe2d0a..27828531ac418 100644 --- a/www/apps/book/app/learn/basics/medusa-container/page.mdx +++ b/www/apps/book/app/learn/fundamentals/medusa-container/page.mdx @@ -14,7 +14,7 @@ In other platforms, if you have a resource A (for example, a class) that depends Medusa simplifies this process by giving you access to the container, with the tools or resources already registered, at all times in your customizations. When you reach a point in your code where you need a tool, you resolve it from the container and use it. -For example, consider you're creating an API route that retrieves products based on filters using [Query](../../advanced-development/module-links/query/page.mdx), a tool that fetches data across the application. In the API route's function, you can resolve Query from the container passed to the API route and use it: +For example, consider you're creating an API route that retrieves products based on filters using [Query](../../fundamentals/module-links/query/page.mdx), a tool that fetches data across the application. In the API route's function, you can resolve Query from the container passed to the API route and use it: export const highlights = [ ["8", "resolve", "A method that resolves resources from the container."], @@ -53,7 +53,7 @@ The API route accepts as a first parameter a request object that has a `scope` p -You can learn more about how Query works in [this chapter](../../advanced-development/module-links/query/page.mdx). +You can learn more about how Query works in [this chapter](../../fundamentals/module-links/query/page.mdx). @@ -71,7 +71,7 @@ This section gives quick examples of how to resolve resources from the Medusa co ### Subscriber -A [subscriber](../events-and-subscribers/page.mdx) function, which is executed when an event is emitted, accepts as a parameter an object with a `container` property, whose value is the Medusa container. Use its `resolve` method to resolve a resource by its registration key: +A [subscriber](../../fundamentals/events-and-subscribers/page.mdx) function, which is executed when an event is emitted, accepts as a parameter an object with a `container` property, whose value is the Medusa container. Use its `resolve` method to resolve a resource by its registration key: export const subscriberHighlights = [ ["6", "container", "Receive the Medusa container as a parameter"], @@ -106,7 +106,7 @@ export const config: SubscriberConfig = { ### Scheduled Job -A [scheduled job](../scheduled-jobs/page.mdx) function, which is executed at a specified interval, accepts the Medusa container as a parameter. Use its `resolve` method to resolve a resource by its registration key: +A [scheduled job](../../fundamentals/scheduled-jobs/page.mdx) function, which is executed at a specified interval, accepts the Medusa container as a parameter. Use its `resolve` method to resolve a resource by its registration key: export const scheduledJobHighlights = [ ["5", "container", "Receive the Medusa container as a parameter"], @@ -144,7 +144,7 @@ export const config = { ### Workflow Step -A [step in a workflow](../workflows/page.mdx), which is a special function where you build durable execution logic across multiple modules, accepts in its second parameter a `container` property, whose value is the Medusa container. Use its `resolve` method to resolve a resource by its registration key: +A [step in a workflow](../../fundamentals/workflows/page.mdx), which is a special function where you build durable execution logic across multiple modules, accepts in its second parameter a `container` property, whose value is the Medusa container. Use its `resolve` method to resolve a resource by its registration key: export const workflowStepsHighlight = [ ["7", "container", "Receive the Medusa container as a parameter"], @@ -175,6 +175,6 @@ const step1 = createStep("step-1", async (_, { container }) => { ### Module Services and Loaders -A [module](../modules/page.mdx), which is a package of functionalities for a single feature or domain, has its own container, so it can't resolve resources from the Medusa container. +A [module](../../fundamentals/modules/page.mdx), which is a package of functionalities for a single feature or domain, has its own container, so it can't resolve resources from the Medusa container. -Learn more about the module's container in [this chapter](../../advanced-development/modules/container/page.mdx). +Learn more about the module's container in [this chapter](../../fundamentals/modules/container/page.mdx). diff --git a/www/apps/book/app/learn/advanced-development/module-links/custom-columns/page.mdx b/www/apps/book/app/learn/fundamentals/module-links/custom-columns/page.mdx similarity index 100% rename from www/apps/book/app/learn/advanced-development/module-links/custom-columns/page.mdx rename to www/apps/book/app/learn/fundamentals/module-links/custom-columns/page.mdx diff --git a/www/apps/book/app/learn/advanced-development/module-links/directions/page.mdx b/www/apps/book/app/learn/fundamentals/module-links/directions/page.mdx similarity index 100% rename from www/apps/book/app/learn/advanced-development/module-links/directions/page.mdx rename to www/apps/book/app/learn/fundamentals/module-links/directions/page.mdx diff --git a/www/apps/book/app/learn/advanced-development/module-links/page.mdx b/www/apps/book/app/learn/fundamentals/module-links/page.mdx similarity index 100% rename from www/apps/book/app/learn/advanced-development/module-links/page.mdx rename to www/apps/book/app/learn/fundamentals/module-links/page.mdx diff --git a/www/apps/book/app/learn/advanced-development/module-links/query/page.mdx b/www/apps/book/app/learn/fundamentals/module-links/query/page.mdx similarity index 98% rename from www/apps/book/app/learn/advanced-development/module-links/query/page.mdx rename to www/apps/book/app/learn/fundamentals/module-links/query/page.mdx index 140239e614534..df971ce06e13f 100644 --- a/www/apps/book/app/learn/advanced-development/module-links/query/page.mdx +++ b/www/apps/book/app/learn/fundamentals/module-links/query/page.mdx @@ -288,7 +288,7 @@ The `order` property is an object whose keys are property names, and values are For API routes that retrieve a single or list of resources, Medusa provides a `validateAndTransformQuery` middleware that: -- Validates accepted query parameters, as explained in [this documentation](../../api-routes/validation/page.mdx). +- Validates accepted query parameters, as explained in [this documentation](../../../fundamentals/api-routes/validation/page.mdx). - Parses configurations that are received as query parameters to be passed to Query. Using this middleware allows you to have default configurations for retrieved fields and relations or pagination, while allowing clients to customize them per request. @@ -331,7 +331,7 @@ export default defineMiddlewares({ The `validateAndTransformQuery` accepts two parameters: -1. A Zod validation schema for the query parameters, which you can learn more about in the [API Route Validation documentation](../../api-routes/validation/page.mdx). Medusa has a `createFindParams` utility that generates a Zod schema that accepts four query parameters: +1. A Zod validation schema for the query parameters, which you can learn more about in the [API Route Validation documentation](../../../fundamentals/api-routes/validation/page.mdx). Medusa has a `createFindParams` utility that generates a Zod schema that accepts four query parameters: 1. `fields`: The fields and relations to retrieve in the returned resources. 2. `offset`: The number of items to skip before retrieving the returned items. 3. `limit`: The maximum number of items to return. diff --git a/www/apps/book/app/learn/advanced-development/module-links/remote-link/page.mdx b/www/apps/book/app/learn/fundamentals/module-links/remote-link/page.mdx similarity index 100% rename from www/apps/book/app/learn/advanced-development/module-links/remote-link/page.mdx rename to www/apps/book/app/learn/fundamentals/module-links/remote-link/page.mdx diff --git a/www/apps/book/app/learn/advanced-development/architecture/architectural-modules/page.mdx b/www/apps/book/app/learn/fundamentals/modules/architectural-modules/page.mdx similarity index 100% rename from www/apps/book/app/learn/advanced-development/architecture/architectural-modules/page.mdx rename to www/apps/book/app/learn/fundamentals/modules/architectural-modules/page.mdx diff --git a/www/apps/book/app/learn/basics/commerce-modules/page.mdx b/www/apps/book/app/learn/fundamentals/modules/commerce-modules/page.mdx similarity index 70% rename from www/apps/book/app/learn/basics/commerce-modules/page.mdx rename to www/apps/book/app/learn/fundamentals/modules/commerce-modules/page.mdx index 4c4922b5d04ec..1862f1514fc84 100644 --- a/www/apps/book/app/learn/basics/commerce-modules/page.mdx +++ b/www/apps/book/app/learn/fundamentals/modules/commerce-modules/page.mdx @@ -8,7 +8,7 @@ In this chapter, you'll learn about Medusa's commerce modules. ## What is a Commerce Module? -Commerce modules are built-in [modules](../modules/page.mdx) of Medusa that provide core commerce logic specific to domains like Products, Orders, Customers, Fulfillment, and much more. +Commerce modules are built-in [modules](../page.mdx) of Medusa that provide core commerce logic specific to domains like Products, Orders, Customers, Fulfillment, and much more. Medusa's commerce modules are used to form Medusa's default [workflows](!resources!/medusa-workflows-reference) and [APIs](!api!/store). For example, when you call the add to cart endpoint. the add to cart workflow runs which uses the Product Module to check if the product exists, the Inventory Module to ensure the product is available in the inventory, and the Cart Module to finally add the product to the cart. @@ -28,9 +28,9 @@ Refer to [this reference](!resources!/commerce-modules) for a full list of comme ## Use Commerce Modules in Custom Flows -Similar to your [custom modules](../modules/page.mdx), the Medusa application registers a commerce module's service in the [container](../medusa-container/page.mdx). So, you can resolve it in your custom flows. This is useful as you build unique requirements extending core commerce features. +Similar to your [custom modules](../page.mdx), the Medusa application registers a commerce module's service in the [container](../../medusa-container/page.mdx). So, you can resolve it in your custom flows. This is useful as you build unique requirements extending core commerce features. -For example, consider you have a [workflow](../workflows/page.mdx) (a special function that performs a task in a series of steps with rollback mechanism) that needs a step to retrieve the total number of products. You can create a step in the workflow that resolves the Product Module's service from the container to use its methods: +For example, consider you have a [workflow](../../../fundamentals/workflows/page.mdx) (a special function that performs a task in a series of steps with rollback mechanism) that needs a step to retrieve the total number of products. You can create a step in the workflow that resolves the Product Module's service from the container to use its methods: export const highlights = [ ["6", `"product"`, "Resolve the Product Module's service from the container."], diff --git a/www/apps/book/app/learn/advanced-development/modules/container/page.mdx b/www/apps/book/app/learn/fundamentals/modules/container/page.mdx similarity index 100% rename from www/apps/book/app/learn/advanced-development/modules/container/page.mdx rename to www/apps/book/app/learn/fundamentals/modules/container/page.mdx diff --git a/www/apps/book/app/learn/advanced-development/modules/db-operations/page.mdx b/www/apps/book/app/learn/fundamentals/modules/db-operations/page.mdx similarity index 100% rename from www/apps/book/app/learn/advanced-development/modules/db-operations/page.mdx rename to www/apps/book/app/learn/fundamentals/modules/db-operations/page.mdx diff --git a/www/apps/book/app/learn/advanced-development/modules/isolation/page.mdx b/www/apps/book/app/learn/fundamentals/modules/isolation/page.mdx similarity index 94% rename from www/apps/book/app/learn/advanced-development/modules/isolation/page.mdx rename to www/apps/book/app/learn/fundamentals/modules/isolation/page.mdx index 46cc9cfc9fb22..f4601d1792f3d 100644 --- a/www/apps/book/app/learn/advanced-development/modules/isolation/page.mdx +++ b/www/apps/book/app/learn/fundamentals/modules/isolation/page.mdx @@ -9,7 +9,7 @@ In this chapter, you'll learn how modules are isolated, and what that means for - Modules can't access resources, such as services or data models, from other modules. -- Use Medusa's linking concepts, as explained in the [Module Links chapters](../../module-links/page.mdx), to extend a module's data models and retrieve data across modules. +- Use Medusa's linking concepts, as explained in the [Module Links chapters](../../../fundamentals/module-links/page.mdx), to extend a module's data models and retrieve data across modules. @@ -33,7 +33,7 @@ Some of the module isolation's benefits include: ## How to Extend Data Model of Another Module? -To extend the data model of another module, such as the `product` data model of the Product Module, use Medusa's linking concepts as explained in the [Module Links chapters](../../module-links/page.mdx). +To extend the data model of another module, such as the `product` data model of the Product Module, use Medusa's linking concepts as explained in the [Module Links chapters](../../../fundamentals/module-links/page.mdx). --- diff --git a/www/apps/book/app/learn/basics/loaders/page.mdx b/www/apps/book/app/learn/fundamentals/modules/loaders/page.mdx similarity index 90% rename from www/apps/book/app/learn/basics/loaders/page.mdx rename to www/apps/book/app/learn/fundamentals/modules/loaders/page.mdx index a4c060160907f..609ff9862f871 100644 --- a/www/apps/book/app/learn/basics/loaders/page.mdx +++ b/www/apps/book/app/learn/fundamentals/modules/loaders/page.mdx @@ -12,13 +12,13 @@ In this chapter, you’ll learn about loaders and how to use them. When building a commerce application, you'll often need to execute an action the first time the application starts. For example, if your application needs to connect to databases other than Medusa's PostgreSQL database, you might need to establish a connection on application startup. -In Medusa, you can execute an action when the application starts using a loader. A loader is a function exported by a [module](../modules/page.mdx), which is a package of business logic for a single domain. When the Medusa application starts, it executes all loaders exported by configured modules. +In Medusa, you can execute an action when the application starts using a loader. A loader is a function exported by a [module](../page.mdx), which is a package of business logic for a single domain. When the Medusa application starts, it executes all loaders exported by configured modules. -Loaders are useful to register custom resources, such as database connections, in the [module's container](../../advanced-development/modules/container/page.mdx), which is similar to the [Medusa container](../medusa-container/page.mdx) but includes only [resources available to the module](!resources!/medusa-container-resources#module-container-resources). Modules are isolated, so they can't access resources outside of them, such as a service in another module. +Loaders are useful to register custom resources, such as database connections, in the [module's container](../container/page.mdx), which is similar to the [Medusa container](../../medusa-container/page.mdx) but includes only [resources available to the module](!resources!/medusa-container-resources#module-container-resources). Modules are isolated, so they can't access resources outside of them, such as a service in another module. -Medusa isolates modules to ensure that they're re-usable across applications, aren't tightly coupled to other resources, and don't have implications when integrated into the Medusa application. Learn more about why modules are isolated in [this chapter](../../advanced-development/modules/isolation/page.mdx), and check out [this reference for the list of resources in the module's container](!resources!/medusa-container-resources#module-container-resources). +Medusa isolates modules to ensure that they're re-usable across applications, aren't tightly coupled to other resources, and don't have implications when integrated into the Medusa application. Learn more about why modules are isolated in [this chapter](../isolation/page.mdx), and check out [this reference for the list of resources in the module's container](!resources!/medusa-container-resources#module-container-resources). @@ -36,7 +36,7 @@ For example, consider you have a `hello` module, you can create a loader at `src -Learn how to create a module in [this chapter](../modules/page.mdx). +Learn how to create a module in [this chapter](../page.mdx). @@ -84,7 +84,7 @@ The second parameter of the `Module` function accepts a `loaders` property whose ### Test the Loader -Assuming your module is [added to Medusa's configuration](../modules/page.mdx#4-add-module-to-medusas-configurations), you can test the loader by starting the Medusa application: +Assuming your module is [added to Medusa's configuration](../page.mdx#4-add-module-to-medusas-configurations), you can test the loader by starting the Medusa application: ```bash npm2yarn npm run dev diff --git a/www/apps/book/app/learn/basics/modules-directory-structure/page.mdx b/www/apps/book/app/learn/fundamentals/modules/modules-directory-structure/page.mdx similarity index 91% rename from www/apps/book/app/learn/basics/modules-directory-structure/page.mdx rename to www/apps/book/app/learn/fundamentals/modules/modules-directory-structure/page.mdx index 4f89f30a80251..c49a998a5b900 100644 --- a/www/apps/book/app/learn/basics/modules-directory-structure/page.mdx +++ b/www/apps/book/app/learn/fundamentals/modules/modules-directory-structure/page.mdx @@ -10,13 +10,13 @@ In this document, you'll learn about the expected files and directories in your ## index.ts -The `index.ts` file in the root of your module's directory is the only required file. It must export the module's definition as explained in a [previous chapter](../modules/page.mdx). +The `index.ts` file in the root of your module's directory is the only required file. It must export the module's definition as explained in a [previous chapter](../page.mdx). --- ## service.ts -A module must have a main service. It's created in the `service.ts` file at the root of your module directory as explained in a [previous chapter](../modules/page.mdx). +A module must have a main service. It's created in the `service.ts` file at the root of your module directory as explained in a [previous chapter](../page.mdx). --- diff --git a/www/apps/book/app/learn/advanced-development/modules/multiple-services/page.mdx b/www/apps/book/app/learn/fundamentals/modules/multiple-services/page.mdx similarity index 100% rename from www/apps/book/app/learn/advanced-development/modules/multiple-services/page.mdx rename to www/apps/book/app/learn/fundamentals/modules/multiple-services/page.mdx diff --git a/www/apps/book/app/learn/advanced-development/modules/options/page.mdx b/www/apps/book/app/learn/fundamentals/modules/options/page.mdx similarity index 100% rename from www/apps/book/app/learn/advanced-development/modules/options/page.mdx rename to www/apps/book/app/learn/fundamentals/modules/options/page.mdx diff --git a/www/apps/book/app/learn/basics/modules/page.mdx b/www/apps/book/app/learn/fundamentals/modules/page.mdx similarity index 93% rename from www/apps/book/app/learn/basics/modules/page.mdx rename to www/apps/book/app/learn/fundamentals/modules/page.mdx index 47933135b4fff..2ff7fe97b079d 100644 --- a/www/apps/book/app/learn/basics/modules/page.mdx +++ b/www/apps/book/app/learn/fundamentals/modules/page.mdx @@ -20,7 +20,7 @@ As you learn more about Medusa, you will see that modules are central to customi ## How to Create a Module? -In a module, you define data models that represent new tables in the database, and you manage these models in a class called a service. Then, the Medusa application registers the module's service in the [Medusa container](../medusa-container/page.mdx) so that you can build commerce flows and features around the functionalities provided by the module. +In a module, you define data models that represent new tables in the database, and you manage these models in a class called a service. Then, the Medusa application registers the module's service in the [Medusa container](../../fundamentals/medusa-container/page.mdx) so that you can build commerce flows and features around the functionalities provided by the module. In this section, you'll build a Blog Module that has a `Post` data model and a service to manage that data model, you'll expose an API endpoint to create a blog post. @@ -53,7 +53,7 @@ You define the data model using the `define` method of the `model` utility impor -Learn about other property types in [this chapter](../../advanced-development/data-models/property-types/page.mdx). +Learn about other property types in [this chapter](../../fundamentals/data-models/property-types/page.mdx). @@ -61,7 +61,7 @@ The code snippet above defines a `Post` data model with `id` and `title` propert ### 2. Create Service -You perform database operations on your data models in a service, which is a class exported by the module and acts like an interface to its functionalities. Medusa registers the service in its [container](../medusa-container/page.mdx), allowing you to resolve and use it when building custom commerce flows. +You perform database operations on your data models in a service, which is a class exported by the module and acts like an interface to its functionalities. Medusa registers the service in its [container](../../fundamentals/medusa-container/page.mdx), allowing you to resolve and use it when building custom commerce flows. You define a service in a `service.ts` or `service.js` file at the root of your module's directory. So, to create the Blog Module's service, create the file `src/modules/blog/service.ts` with the following content: @@ -205,11 +205,11 @@ This creates the `post` table in the database. Since the module's main service is registered in the Medusa container, you can resolve it in other customizations to use its methods. -To test out the Blog Module, you'll add the functionality to create a post in a [workflow](../workflows/page.mdx), which is a special function that performs a task in a series of steps with rollback logic. Then, you'll expose an [API route](../api-routes/page.mdx) that creates a blog post by executing the workflow. +To test out the Blog Module, you'll add the functionality to create a post in a [workflow](../../fundamentals/workflows/page.mdx), which is a special function that performs a task in a series of steps with rollback logic. Then, you'll expose an [API route](../api-routes/page.mdx) that creates a blog post by executing the workflow. -By building a commerce feature in a workflow, you can execute it in other customizations while ensuring data consistency across systems. If an error occurs during execution, every step has its own rollback logic to undo its actions. Workflows have other special features which you can learn about in [this chapter](../workflows/page.mdx). +By building a commerce feature in a workflow, you can execute it in other customizations while ensuring data consistency across systems. If an error occurs during execution, every step has its own rollback logic to undo its actions. Workflows have other special features which you can learn about in [this chapter](../../fundamentals/workflows/page.mdx). @@ -326,4 +326,4 @@ This will create a post and return it in the response: } ``` -You can also execute the workflow from a [subscriber](../events-and-subscribers/page.mdx) when an event occurs, or from a [scheduled job](../scheduled-jobs/page.mdx) to run it at a specified interval. +You can also execute the workflow from a [subscriber](../../fundamentals/events-and-subscribers/page.mdx) when an event occurs, or from a [scheduled job](../../fundamentals/scheduled-jobs/page.mdx) to run it at a specified interval. diff --git a/www/apps/book/app/learn/advanced-development/modules/service-constraints/page.mdx b/www/apps/book/app/learn/fundamentals/modules/service-constraints/page.mdx similarity index 100% rename from www/apps/book/app/learn/advanced-development/modules/service-constraints/page.mdx rename to www/apps/book/app/learn/fundamentals/modules/service-constraints/page.mdx diff --git a/www/apps/book/app/learn/advanced-development/modules/service-factory/page.mdx b/www/apps/book/app/learn/fundamentals/modules/service-factory/page.mdx similarity index 100% rename from www/apps/book/app/learn/advanced-development/modules/service-factory/page.mdx rename to www/apps/book/app/learn/fundamentals/modules/service-factory/page.mdx diff --git a/www/apps/book/app/learn/advanced-development/scheduled-jobs/execution-number/page.mdx b/www/apps/book/app/learn/fundamentals/scheduled-jobs/execution-number/page.mdx similarity index 100% rename from www/apps/book/app/learn/advanced-development/scheduled-jobs/execution-number/page.mdx rename to www/apps/book/app/learn/fundamentals/scheduled-jobs/execution-number/page.mdx diff --git a/www/apps/book/app/learn/basics/scheduled-jobs/page.mdx b/www/apps/book/app/learn/fundamentals/scheduled-jobs/page.mdx similarity index 98% rename from www/apps/book/app/learn/basics/scheduled-jobs/page.mdx rename to www/apps/book/app/learn/fundamentals/scheduled-jobs/page.mdx index 21119d5386584..71e80857d0f8a 100644 --- a/www/apps/book/app/learn/basics/scheduled-jobs/page.mdx +++ b/www/apps/book/app/learn/fundamentals/scheduled-jobs/page.mdx @@ -17,7 +17,7 @@ Medusa removes this overhead by supporting this feature natively with scheduled - You want the action to execute at a specified schedule while the Medusa application **isn't** running. Instead, use the operating system's equivalent of a cron job. -- You want to execute the action once when the application loads. Use [loaders](../loaders/page.mdx) instead. +- You want to execute the action once when the application loads. Use [loaders](../modules/loaders/page.mdx) instead. - You want to execute the action if an event occurs. Use [subscribers](../events-and-subscribers/page.mdx) instead. diff --git a/www/apps/book/app/learn/advanced-development/workflows/access-workflow-errors/page.mdx b/www/apps/book/app/learn/fundamentals/workflows/access-workflow-errors/page.mdx similarity index 100% rename from www/apps/book/app/learn/advanced-development/workflows/access-workflow-errors/page.mdx rename to www/apps/book/app/learn/fundamentals/workflows/access-workflow-errors/page.mdx diff --git a/www/apps/book/app/learn/advanced-development/workflows/add-workflow-hook/page.mdx b/www/apps/book/app/learn/fundamentals/workflows/add-workflow-hook/page.mdx similarity index 100% rename from www/apps/book/app/learn/advanced-development/workflows/add-workflow-hook/page.mdx rename to www/apps/book/app/learn/fundamentals/workflows/add-workflow-hook/page.mdx diff --git a/www/apps/book/app/learn/advanced-development/workflows/compensation-function/page.mdx b/www/apps/book/app/learn/fundamentals/workflows/compensation-function/page.mdx similarity index 100% rename from www/apps/book/app/learn/advanced-development/workflows/compensation-function/page.mdx rename to www/apps/book/app/learn/fundamentals/workflows/compensation-function/page.mdx diff --git a/www/apps/book/app/learn/advanced-development/workflows/conditions/page.mdx b/www/apps/book/app/learn/fundamentals/workflows/conditions/page.mdx similarity index 100% rename from www/apps/book/app/learn/advanced-development/workflows/conditions/page.mdx rename to www/apps/book/app/learn/fundamentals/workflows/conditions/page.mdx diff --git a/www/apps/book/app/learn/advanced-development/workflows/constructor-constraints/page.mdx b/www/apps/book/app/learn/fundamentals/workflows/constructor-constraints/page.mdx similarity index 100% rename from www/apps/book/app/learn/advanced-development/workflows/constructor-constraints/page.mdx rename to www/apps/book/app/learn/fundamentals/workflows/constructor-constraints/page.mdx diff --git a/www/apps/book/app/learn/advanced-development/workflows/execute-another-workflow/page.mdx b/www/apps/book/app/learn/fundamentals/workflows/execute-another-workflow/page.mdx similarity index 100% rename from www/apps/book/app/learn/advanced-development/workflows/execute-another-workflow/page.mdx rename to www/apps/book/app/learn/fundamentals/workflows/execute-another-workflow/page.mdx diff --git a/www/apps/book/app/learn/advanced-development/workflows/long-running-workflow/page.mdx b/www/apps/book/app/learn/fundamentals/workflows/long-running-workflow/page.mdx similarity index 100% rename from www/apps/book/app/learn/advanced-development/workflows/long-running-workflow/page.mdx rename to www/apps/book/app/learn/fundamentals/workflows/long-running-workflow/page.mdx diff --git a/www/apps/book/app/learn/advanced-development/workflows/multiple-step-usage/page.mdx b/www/apps/book/app/learn/fundamentals/workflows/multiple-step-usage/page.mdx similarity index 100% rename from www/apps/book/app/learn/advanced-development/workflows/multiple-step-usage/page.mdx rename to www/apps/book/app/learn/fundamentals/workflows/multiple-step-usage/page.mdx diff --git a/www/apps/book/app/learn/basics/workflows/page.mdx b/www/apps/book/app/learn/fundamentals/workflows/page.mdx similarity index 98% rename from www/apps/book/app/learn/basics/workflows/page.mdx rename to www/apps/book/app/learn/fundamentals/workflows/page.mdx index a551712044070..b22aa309a3c6d 100644 --- a/www/apps/book/app/learn/basics/workflows/page.mdx +++ b/www/apps/book/app/learn/fundamentals/workflows/page.mdx @@ -22,7 +22,7 @@ However, unlike regular functions, workflows: - Support defining roll-back logic for each step, so that you can handle errors gracefully and your data remain consistent across systems. - Perform long actions asynchronously, giving you control over when a step starts and finishes. -You implement all custom flows within workflows, then execute them from [API routes](../api-routes/page.mdx), [subscribers](../events-and-subscribers/page.mdx), and [scheduled jobs](../scheduled-jobs/page.mdx). +You implement all custom flows within workflows, then execute them from [API routes](../api-routes/page.mdx), [subscribers](../../fundamentals/events-and-subscribers/page.mdx), and [scheduled jobs](../../fundamentals/scheduled-jobs/page.mdx). --- diff --git a/www/apps/book/app/learn/advanced-development/workflows/parallel-steps/page.mdx b/www/apps/book/app/learn/fundamentals/workflows/parallel-steps/page.mdx similarity index 100% rename from www/apps/book/app/learn/advanced-development/workflows/parallel-steps/page.mdx rename to www/apps/book/app/learn/fundamentals/workflows/parallel-steps/page.mdx diff --git a/www/apps/book/app/learn/advanced-development/workflows/retry-failed-steps/page.mdx b/www/apps/book/app/learn/fundamentals/workflows/retry-failed-steps/page.mdx similarity index 100% rename from www/apps/book/app/learn/advanced-development/workflows/retry-failed-steps/page.mdx rename to www/apps/book/app/learn/fundamentals/workflows/retry-failed-steps/page.mdx diff --git a/www/apps/book/app/learn/advanced-development/workflows/variable-manipulation/page.mdx b/www/apps/book/app/learn/fundamentals/workflows/variable-manipulation/page.mdx similarity index 100% rename from www/apps/book/app/learn/advanced-development/workflows/variable-manipulation/page.mdx rename to www/apps/book/app/learn/fundamentals/workflows/variable-manipulation/page.mdx diff --git a/www/apps/book/app/learn/advanced-development/workflows/workflow-hooks/page.mdx b/www/apps/book/app/learn/fundamentals/workflows/workflow-hooks/page.mdx similarity index 100% rename from www/apps/book/app/learn/advanced-development/workflows/workflow-hooks/page.mdx rename to www/apps/book/app/learn/fundamentals/workflows/workflow-hooks/page.mdx diff --git a/www/apps/book/app/learn/advanced-development/workflows/workflow-timeout/page.mdx b/www/apps/book/app/learn/fundamentals/workflows/workflow-timeout/page.mdx similarity index 100% rename from www/apps/book/app/learn/advanced-development/workflows/workflow-timeout/page.mdx rename to www/apps/book/app/learn/fundamentals/workflows/workflow-timeout/page.mdx diff --git a/www/apps/book/app/learn/installation/page.mdx b/www/apps/book/app/learn/installation/page.mdx index 6976ebe1cfbf7..fb0c7c3f21cc3 100644 --- a/www/apps/book/app/learn/installation/page.mdx +++ b/www/apps/book/app/learn/installation/page.mdx @@ -103,6 +103,32 @@ Replace `admin@medusajs.com` and `supersecret` with the user's email and passwor You can then use the user's credentials to log into the Medusa Admin application. +--- + +## Project Files + +Your Medusa application's project will have the following files and directories: + +![A diagram of the directories overview](https://res.cloudinary.com/dza7lstvk/image/upload/v1732803813/Medusa%20Book/medusa-dir-overview_v7ks0j.jpg) + +### src + +This directory is the central place for your custom development. It includes the following sub-directories: + +- `admin`: Holds your admin dashboard's custom [widgets](../fundamentals/admin/widgets/page.mdx) and [UI routes](../fundamentals/admin/ui-routes/page.mdx). +- `api`: Holds your custom [API routes](../fundamentals/api-routes/page.mdx) that are added as endpoints in your Medusa application. +- `jobs`: Holds your [scheduled jobs](../fundamentals/scheduled-jobs/page.mdx) that run at a specified interval during your Medusa application's runtime. +- `links`: Holds you [module links](../fundamentals/module-links/page.mdx) that build associations between data models of different modules. +- `modules`: Holds your custom [modules](../fundamentals/modules/page.mdx) that implement custom business logic. +- `scripts`: Holds your custom [scripts](../fundamentals/custom-cli-scripts/page.mdx) to be executed using Medusa's CLI tool. +- `subscribers`: Holds your [event listeners](../fundamentals/events-and-subscribers/page.mdx) that are executed asynchronously whenever an event is emitted. +- `workflows`: Holds your custom [flows](../fundamentals/workflows/page.mdx) that can be executed from anywhere in your application. + +### medusa-config.ts + +This file holds your [Medusa configurations](!resources!/references/medusa-config), such as your PostgreSQL database configurations. + + --- ## Configure Medusa Application @@ -115,4 +141,4 @@ If you run into issues with configurations, such as CORS configurations, or need ## Next Steps -In the next documentation pages, you'll start customizing your Medusa application, learn about the project's directory structure, and learn about different basic concepts. +In the next chapters, you'll learn about the architecture of your Medusa application, then learn how to customize your application to build custom features. diff --git a/www/apps/book/app/learn/advanced-development/architecture/overview/page.mdx b/www/apps/book/app/learn/introduction/architecture/page.mdx similarity index 100% rename from www/apps/book/app/learn/advanced-development/architecture/overview/page.mdx rename to www/apps/book/app/learn/introduction/architecture/page.mdx diff --git a/www/apps/book/app/learn/more-resources/cheatsheet/page.mdx b/www/apps/book/app/learn/more-resources/cheatsheet/page.mdx deleted file mode 100644 index d269c9d8a52b2..0000000000000 --- a/www/apps/book/app/learn/more-resources/cheatsheet/page.mdx +++ /dev/null @@ -1,160 +0,0 @@ -import { Table } from "docs-ui" - -export const metadata = { - title: `${pageNumber} Cheat sheet`, -} - -# {metadata.title} - -This chapter provides a cheat sheet for Medusa's resources on when to use or not use them. - -
- - - Name - Use if - Don't use if - - - - - API Routes - - - - You're exposing custom functionality to be used in the admin dashboard or an external application, such as a storefront. - - - - - \- - - - - - Modules - - - - You're implementing a custom commerce feature. For example, you're implementing digital products. - - You want to extend data models in other commerce modules, such as adding a field or a relation to the Product model. - - You want to re-use your custom commerce functionalities across Medusa applications or use them in other environments, such as Edge functions and Next.js apps. - - - - - \- - - - - - Module Links - - - - You want to create a relation between data models from different modules. - - - - - - You want to create a relationship between data models in the same module. Use data model relationships instead. - - - - - Data Models - - - - You want to store data related to your customization in the database. - - - - - - You want to store simple key-value pairs related to a Medusa data model. Instead, use the `metadata` field that models have, which is an object of custom key-value pairs. - - - - - Data Model Relationships - - - - You want to create a relation between data models in the same module. - - - - - - You want to create a relationship between data models in different modules. Use module links instead. - - - - - Loaders - - - - You're performing an action at application start-up. - - You're establishing a one-time connection with an external system. - - - - - - You want to perform an action continuously or at a set time pattern in the application. Use scheduled jobs instead. - - - - - Subscribers - - - - You want to perform an action everytime a specific event is emitted in the Medusa application. - - - - - \- - - - - - Scheduled Jobs - - - - You're executing an action at a specified time interval during application runtime. - - The action must be executed automatically. - - - - - - You want the action to execute at a specified time interval while the Medusa application **isn't** running. Instead, use the operating system's equivalent of a cron job. - - You want to execute the action once. Use loaders instead. - - You want to execute the action if an event occurs. Use subscribers instead. - - - - - Workflows - - - - You're defining a flow with interactions across multiple systems and services. - - You're defining flows to be used across different resources. For example, if you want to invoke the flow manually through an API Router, but also want to automate its running through a scheduled job. - - You want to define how the series of actions are rolled-back when an error occurs. - - - - - \- - - - - - Middlewares - - - - You want to protect API routes by a custom condition. - - You're modifying the request body. - - - - - \- - - - - -
\ No newline at end of file diff --git a/www/apps/book/components/Homepage/CodeTabs/index.tsx b/www/apps/book/components/Homepage/CodeTabs/index.tsx index e7c00da4cf3e8..375a7e0c7938b 100644 --- a/www/apps/book/components/Homepage/CodeTabs/index.tsx +++ b/www/apps/book/components/Homepage/CodeTabs/index.tsx @@ -24,7 +24,7 @@ const HomepageCodeTabs = () => { "Expose custom features with REST API routes, then consume them from your client applications.", link: { title: "API Routes", - link: "/learn/basics/api-routes", + link: "/learn/fundamentals/api-routes", }, }, code: { @@ -64,7 +64,7 @@ const HomepageCodeTabs = () => { "Build flows as a series of steps, with retry mechanisms and tracking of each steps' status.", link: { title: "Workflows", - link: "/learn/basics/workflows", + link: "/learn/fundamentals/workflows", }, }, code: { @@ -105,7 +105,7 @@ const HomepageCodeTabs = () => { "Create data models that represent tables in the database using Medusa's Data Model Language.", link: { title: "DML", - link: "/learn/basics/modules#1-create-data-model", + link: "/learn/fundamentals/modules#1-create-data-model", }, }, code: { @@ -141,7 +141,7 @@ const HomepageCodeTabs = () => { "Build custom modules with commerce or architectural features and use them in API routes or workflows.", link: { title: "Modules", - link: "/learn/basics/modules", + link: "/learn/fundamentals/modules", }, }, code: { @@ -193,7 +193,7 @@ export async function POST( "Add custom properties to Medusa's data models using module links to build custom use cases.", link: { title: "Module Links", - link: "/learn/advanced-development/module-links", + link: "/learn/fundamentals/module-links", }, }, code: { @@ -223,7 +223,7 @@ export default defineLink( "Handle events emitted by the Medusa application to perform custom actions.", link: { title: "Subscribers", - link: "/learn/basics/events-and-subscribers", + link: "/learn/fundamentals/events-and-subscribers", }, }, code: { @@ -271,8 +271,8 @@ export const config: SubscriberConfig = { content: "Inject widgets into predefined zones in the Medusa Admin, or add new pages.", link: { - title: "Admin Customizations", - link: "/learn/basics/admin-customizations", + title: "Admin Widgets", + link: "/learn/fundamentals/admin/widgets", }, }, code: { diff --git a/www/apps/book/components/Homepage/LinksSection/index.tsx b/www/apps/book/components/Homepage/LinksSection/index.tsx index dc0dce5f561e3..49ef155b88fac 100644 --- a/www/apps/book/components/Homepage/LinksSection/index.tsx +++ b/www/apps/book/components/Homepage/LinksSection/index.tsx @@ -24,11 +24,11 @@ const HomepageLinksSection = () => { title: "Admin Development", links: [ { - href: "/learn/basics/admin-customizations", + href: "/learn/fundamentals/admin/widgets", text: "Build a UI Widget", }, { - href: "/learn/advanced-development/admin/ui-routes", + href: "/learn/fundamentals/admin/ui-routes", text: "Add a UI Route", }, { diff --git a/www/apps/book/generated/edit-dates.mjs b/www/apps/book/generated/edit-dates.mjs index 106bd650c190a..de0b8b671288e 100644 --- a/www/apps/book/generated/edit-dates.mjs +++ b/www/apps/book/generated/edit-dates.mjs @@ -1,121 +1,112 @@ export const generatedEditDates = { - "app/learn/basics/scheduled-jobs/page.mdx": "2024-11-29T07:47:23.809Z", - "app/learn/basics/workflows/page.mdx": "2024-11-29T08:00:47.637Z", + "app/learn/fundamentals/scheduled-jobs/page.mdx": "2024-12-09T10:51:40.570Z", + "app/learn/fundamentals/workflows/page.mdx": "2024-12-09T10:52:19.379Z", "app/learn/deployment/page.mdx": "2024-11-25T14:32:44.949Z", "app/learn/page.mdx": "2024-11-28T14:17:53.023Z", - "app/learn/basics/commerce-modules/page.mdx": "2024-11-28T15:50:01.284Z", - "app/learn/advanced-development/workflows/retry-failed-steps/page.mdx": "2024-09-30T08:43:53.130Z", - "app/learn/advanced-development/workflows/workflow-hooks/page.mdx": "2024-09-30T08:43:53.131Z", - "app/learn/cheatsheet/page.mdx": "2024-07-11T13:53:40+03:00", + "app/learn/fundamentals/modules/commerce-modules/page.mdx": "2024-12-09T10:46:29.339Z", + "app/learn/fundamentals/workflows/retry-failed-steps/page.mdx": "2024-12-04T07:37:59.823Z", + "app/learn/fundamentals/workflows/workflow-hooks/page.mdx": "2024-12-09T10:44:33.781Z", "app/learn/debugging-and-testing/logging/page.mdx": "2024-09-30T08:43:53.135Z", "app/learn/more-resources/page.mdx": "2024-07-04T17:26:03+03:00", "app/learn/storefront-development/page.mdx": "2024-09-11T10:58:59.290Z", "app/learn/storefront-development/nextjs-starter/page.mdx": "2024-07-04T17:26:03+03:00", - "app/learn/basics/page.mdx": "2024-09-03T07:11:06.879Z", - "app/learn/basics/admin-customizations/page.mdx": "2024-10-07T12:41:39.218Z", - "app/learn/advanced-development/workflows/workflow-timeout/page.mdx": "2024-09-30T08:43:53.131Z", - "app/learn/advanced-development/workflows/parallel-steps/page.mdx": "2024-09-30T08:43:53.130Z", - "app/learn/advanced-development/page.mdx": "2024-07-04T17:26:03+03:00", - "app/learn/first-customizations/page.mdx": "2024-09-11T10:48:42.374Z", + "app/learn/fundamentals/page.mdx": "2024-07-04T17:26:03+03:00", + "app/learn/fundamentals/admin-customizations/page.mdx": "2024-10-07T12:41:39.218Z", + "app/learn/fundamentals/workflows/workflow-timeout/page.mdx": "2024-10-21T13:30:21.372Z", + "app/learn/fundamentals/workflows/parallel-steps/page.mdx": "2024-10-21T13:30:21.372Z", "app/learn/debugging-and-testing/page.mdx": "2024-05-03T17:36:38+03:00", - "app/learn/basics/medusa-container/page.mdx": "2024-11-28T14:28:30.248Z", - "app/learn/basics/project-directories-files/page.mdx": "2024-11-28T14:27:01.290Z", - "app/learn/basics/api-routes/page.mdx": "2024-11-28T15:44:30.413Z", - "app/learn/basics/modules-directory-structure/page.mdx": "2024-10-03T13:03:35.957Z", - "app/learn/advanced-development/workflows/access-workflow-errors/page.mdx": "2024-09-18T12:54:04.695Z", - "app/learn/basics/events-and-subscribers/page.mdx": "2024-11-29T07:44:23.499Z", - "app/learn/advanced-development/modules/container/page.mdx": "2024-11-21T08:59:18.707Z", - "app/learn/advanced-development/workflows/execute-another-workflow/page.mdx": "2024-09-30T08:43:53.129Z", - "app/learn/basics/loaders/page.mdx": "2024-11-29T07:36:00.800Z", - "app/learn/advanced-development/admin/widgets/page.mdx": "2024-11-29T07:59:37.919Z", - "app/learn/advanced-development/data-models/page.mdx": "2024-09-19T07:26:43.535Z", - "app/learn/advanced-development/modules/remote-link/page.mdx": "2024-09-30T08:43:53.127Z", - "app/learn/advanced-development/api-routes/protected-routes/page.mdx": "2024-09-30T08:43:53.121Z", - "app/learn/advanced-development/workflows/add-workflow-hook/page.mdx": "2024-09-30T08:43:53.128Z", - "app/learn/advanced-development/events-and-subscribers/data-payload/page.mdx": "2024-07-16T17:12:05+01:00", - "app/learn/advanced-development/data-models/default-properties/page.mdx": "2024-09-19T07:32:06.118Z", - "app/learn/advanced-development/workflows/advanced-example/page.mdx": "2024-09-11T10:46:59.975Z", - "app/learn/advanced-development/events-and-subscribers/emit-event/page.mdx": "2024-11-11T15:17:58.045Z", - "app/learn/advanced-development/workflows/conditions/page.mdx": "2024-09-30T08:43:53.128Z", - "app/learn/advanced-development/modules/module-link-directions/page.mdx": "2024-07-24T09:16:01+02:00", - "app/learn/advanced-development/admin/page.mdx": "2024-10-07T12:39:13.178Z", - "app/learn/advanced-development/workflows/long-running-workflow/page.mdx": "2024-09-30T08:43:53.129Z", - "app/learn/advanced-development/workflows/constructor-constraints/page.mdx": "2024-11-14T16:13:19.234Z", - "app/learn/advanced-development/data-models/write-migration/page.mdx": "2024-07-15T17:46:10+02:00", - "app/learn/advanced-development/data-models/manage-relationships/page.mdx": "2024-09-10T11:39:51.167Z", - "app/learn/advanced-development/modules/remote-query/page.mdx": "2024-07-21T21:20:24+02:00", - "app/learn/advanced-development/modules/options/page.mdx": "2024-10-16T08:49:27.162Z", - "app/learn/advanced-development/data-models/relationships/page.mdx": "2024-12-04T10:52:32.992Z", - "app/learn/advanced-development/workflows/compensation-function/page.mdx": "2024-11-28T14:05:29.691Z", - "app/learn/advanced-development/modules/service-factory/page.mdx": "2024-09-30T08:43:53.127Z", - "app/learn/advanced-development/data-models/primary-key/page.mdx": "2024-09-30T08:43:53.123Z", - "app/learn/advanced-development/modules/module-links/page.mdx": "2024-09-30T08:43:53.126Z", - "app/learn/advanced-development/data-models/searchable-property/page.mdx": "2024-09-30T08:43:53.125Z", - "app/learn/advanced-development/scheduled-jobs/execution-number/page.mdx": "2024-07-02T09:41:15+00:00", - "app/learn/advanced-development/api-routes/parameters/page.mdx": "2024-11-12T13:35:09.393Z", - "app/learn/advanced-development/api-routes/http-methods/page.mdx": "2024-09-11T10:43:33.169Z", - "app/learn/advanced-development/admin/tips/page.mdx": "2024-10-07T12:50:36.335Z", - "app/learn/advanced-development/api-routes/cors/page.mdx": "2024-09-30T08:43:53.121Z", - "app/learn/advanced-development/admin/ui-routes/page.mdx": "2024-11-29T08:10:52.377Z", - "app/learn/advanced-development/api-routes/middlewares/page.mdx": "2024-09-11T10:45:31.861Z", - "app/learn/advanced-development/modules/isolation/page.mdx": "2024-07-04T17:26:03+03:00", - "app/learn/advanced-development/data-models/configure-properties/page.mdx": "2024-09-30T08:43:53.122Z", - "app/learn/advanced-development/data-models/index/page.mdx": "2024-09-30T08:43:53.122Z", - "app/learn/advanced-development/custom-cli-scripts/page.mdx": "2024-09-30T08:43:53.122Z", - "app/learn/advanced-development/data-models/property-types/page.mdx": "2024-09-30T08:43:53.124Z", + "app/learn/fundamentals/medusa-container/page.mdx": "2024-12-09T11:02:38.225Z", + "app/learn/fundamentals/api-routes/page.mdx": "2024-12-04T11:02:57.134Z", + "app/learn/fundamentals/modules/modules-directory-structure/page.mdx": "2024-12-09T10:32:46.839Z", + "app/learn/fundamentals/workflows/access-workflow-errors/page.mdx": "2024-10-21T13:30:21.371Z", + "app/learn/fundamentals/events-and-subscribers/page.mdx": "2024-12-09T10:48:09.285Z", + "app/learn/fundamentals/modules/container/page.mdx": "2024-11-21T09:22:27.898Z", + "app/learn/fundamentals/workflows/execute-another-workflow/page.mdx": "2024-10-23T07:08:55.900Z", + "app/learn/fundamentals/modules/loaders/page.mdx": "2024-12-09T10:32:29.221Z", + "app/learn/fundamentals/admin/widgets/page.mdx": "2024-12-04T11:02:57.133Z", + "app/learn/fundamentals/data-models/page.mdx": "2024-12-09T11:34:51.065Z", + "app/learn/fundamentals/modules/remote-link/page.mdx": "2024-09-30T08:43:53.127Z", + "app/learn/fundamentals/api-routes/protected-routes/page.mdx": "2024-11-13T10:44:04.225Z", + "app/learn/fundamentals/workflows/add-workflow-hook/page.mdx": "2024-10-21T13:30:21.371Z", + "app/learn/fundamentals/events-and-subscribers/data-payload/page.mdx": "2024-10-21T13:30:21.369Z", + "app/learn/fundamentals/data-models/default-properties/page.mdx": "2024-10-21T13:30:21.368Z", + "app/learn/fundamentals/workflows/advanced-example/page.mdx": "2024-09-11T10:46:59.975Z", + "app/learn/fundamentals/events-and-subscribers/emit-event/page.mdx": "2024-11-25T16:19:32.168Z", + "app/learn/fundamentals/workflows/conditions/page.mdx": "2024-10-21T13:30:21.371Z", + "app/learn/fundamentals/modules/module-link-directions/page.mdx": "2024-07-24T09:16:01+02:00", + "app/learn/fundamentals/admin/page.mdx": "2024-10-23T07:08:55.898Z", + "app/learn/fundamentals/workflows/long-running-workflow/page.mdx": "2024-12-04T07:37:59.822Z", + "app/learn/fundamentals/workflows/constructor-constraints/page.mdx": "2024-11-25T16:19:32.168Z", + "app/learn/fundamentals/data-models/write-migration/page.mdx": "2024-11-11T15:27:59.794Z", + "app/learn/fundamentals/data-models/manage-relationships/page.mdx": "2024-10-28T04:22:21.328Z", + "app/learn/fundamentals/modules/remote-query/page.mdx": "2024-07-21T21:20:24+02:00", + "app/learn/fundamentals/modules/options/page.mdx": "2024-11-19T16:37:47.253Z", + "app/learn/fundamentals/data-models/relationships/page.mdx": "2024-12-06T14:34:50.384Z", + "app/learn/fundamentals/workflows/compensation-function/page.mdx": "2024-12-06T14:34:50.384Z", + "app/learn/fundamentals/modules/service-factory/page.mdx": "2024-10-21T13:30:21.371Z", + "app/learn/fundamentals/data-models/primary-key/page.mdx": "2024-10-21T13:30:21.368Z", + "app/learn/fundamentals/modules/module-links/page.mdx": "2024-09-30T08:43:53.126Z", + "app/learn/fundamentals/data-models/searchable-property/page.mdx": "2024-10-21T13:30:21.369Z", + "app/learn/fundamentals/scheduled-jobs/execution-number/page.mdx": "2024-10-21T13:30:21.371Z", + "app/learn/fundamentals/api-routes/parameters/page.mdx": "2024-11-19T16:37:47.251Z", + "app/learn/fundamentals/api-routes/http-methods/page.mdx": "2024-10-21T13:30:21.367Z", + "app/learn/fundamentals/admin/tips/page.mdx": "2024-11-19T16:43:01.662Z", + "app/learn/fundamentals/api-routes/cors/page.mdx": "2024-10-21T13:30:21.366Z", + "app/learn/fundamentals/admin/ui-routes/page.mdx": "2024-12-04T11:02:57.133Z", + "app/learn/fundamentals/api-routes/middlewares/page.mdx": "2024-10-21T13:30:21.367Z", + "app/learn/fundamentals/modules/isolation/page.mdx": "2024-12-09T11:02:38.087Z", + "app/learn/fundamentals/data-models/configure-properties/page.mdx": "2024-10-21T13:30:21.368Z", + "app/learn/fundamentals/data-models/index/page.mdx": "2024-10-21T13:30:21.368Z", + "app/learn/fundamentals/custom-cli-scripts/page.mdx": "2024-10-23T07:08:55.898Z", + "app/learn/fundamentals/data-models/property-types/page.mdx": "2024-10-28T04:22:27.540Z", "app/learn/debugging-and-testing/testing-tools/integration-tests/api-routes/page.mdx": "2024-09-30T08:43:53.136Z", "app/learn/debugging-and-testing/testing-tools/integration-tests/page.mdx": "2024-09-10T11:39:51.170Z", "app/learn/debugging-and-testing/testing-tools/integration-tests/workflows/page.mdx": "2024-09-30T08:43:53.139Z", "app/learn/debugging-and-testing/testing-tools/page.mdx": "2024-09-30T08:43:53.139Z", "app/learn/debugging-and-testing/testing-tools/unit-tests/module-example/page.mdx": "2024-09-02T11:04:27.232Z", "app/learn/debugging-and-testing/testing-tools/unit-tests/page.mdx": "2024-09-02T11:03:26.997Z", - "app/learn/advanced-development/modules/service-constraints/page.mdx": "2024-09-30T08:43:53.127Z", - "app/learn/advanced-development/api-routes/page.mdx": "2024-09-04T09:36:33.961Z", - "app/learn/advanced-development/api-routes/responses/page.mdx": "2024-09-11T10:44:37.016Z", - "app/learn/advanced-development/api-routes/validation/page.mdx": "2024-11-12T13:32:32.484Z", - "app/learn/advanced-development/api-routes/errors/page.mdx": "2024-09-30T08:43:53.121Z", - "app/learn/advanced-development/admin/constraints/page.mdx": "2024-09-10T11:39:51.165Z", + "app/learn/fundamentals/modules/service-constraints/page.mdx": "2024-11-19T16:37:47.253Z", + "app/learn/fundamentals/api-routes/responses/page.mdx": "2024-10-21T13:30:21.367Z", + "app/learn/fundamentals/api-routes/validation/page.mdx": "2024-12-09T11:02:38.397Z", + "app/learn/fundamentals/api-routes/errors/page.mdx": "2024-10-21T13:30:21.367Z", + "app/learn/fundamentals/admin/constraints/page.mdx": "2024-10-21T13:30:21.366Z", "app/learn/debugging-and-testing/testing-tools/modules-tests/module-example/page.mdx": "2024-10-16T08:50:03.061Z", "app/learn/debugging-and-testing/testing-tools/modules-tests/page.mdx": "2024-10-16T08:50:23.232Z", - "app/learn/advanced-development/module-links/custom-columns/page.mdx": "2024-11-19T15:05:59.471Z", - "app/learn/advanced-development/module-links/directions/page.mdx": "2024-09-16T15:37:51.441Z", - "app/learn/advanced-development/module-links/page.mdx": "2024-09-16T15:36:48.190Z", - "app/learn/advanced-development/module-links/query/page.mdx": "2024-11-19T15:25:01.149Z", - "app/learn/advanced-development/module-links/remote-link/page.mdx": "2024-09-16T12:42:27.581Z", - "app/learn/advanced-development/modules/db-operations/page.mdx": "2024-09-16T14:38:29.150Z", - "app/learn/advanced-development/modules/multiple-services/page.mdx": "2024-09-16T14:41:32.975Z", - "app/learn/advanced-development/modules/page.mdx": "2024-09-16T14:33:48.217Z", + "app/learn/fundamentals/module-links/custom-columns/page.mdx": "2024-11-20T14:32:09.764Z", + "app/learn/fundamentals/module-links/directions/page.mdx": "2024-10-21T13:30:21.369Z", + "app/learn/fundamentals/module-links/page.mdx": "2024-10-28T04:22:27.541Z", + "app/learn/fundamentals/module-links/query/page.mdx": "2024-12-09T10:38:41.169Z", + "app/learn/fundamentals/module-links/remote-link/page.mdx": "2024-10-28T04:22:21.328Z", + "app/learn/fundamentals/modules/db-operations/page.mdx": "2024-12-04T11:02:57.134Z", + "app/learn/fundamentals/modules/multiple-services/page.mdx": "2024-10-21T13:30:21.370Z", + "app/learn/fundamentals/modules/page.mdx": "2024-12-09T11:02:37.696Z", "app/learn/debugging-and-testing/instrumentation/page.mdx": "2024-09-17T08:53:15.910Z", - "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-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-12-06T14:34:53.354Z", - "app/learn/customization/extend-features/extend-create-product/page.mdx": "2024-12-05T09:26:15.796Z", - "app/learn/customization/custom-features/page.mdx": "2024-11-28T08:21:55.207Z", - "app/learn/customization/customize-admin/page.mdx": "2024-12-06T07:21:02.303Z", - "app/learn/customization/customize-admin/route/page.mdx": "2024-12-06T08:29:57.834Z", - "app/learn/customization/customize-admin/widget/page.mdx": "2024-12-06T08:15:11.426Z", - "app/learn/customization/extend-features/define-link/page.mdx": "2024-12-04T17:15:16.004Z", - "app/learn/customization/extend-features/page.mdx": "2024-09-12T12:38:57.394Z", - "app/learn/customization/extend-features/query-linked-records/page.mdx": "2024-12-05T10:36:32.357Z", - "app/learn/customization/integrate-systems/handle-event/page.mdx": "2024-12-06T14:34:53.355Z", - "app/learn/customization/integrate-systems/page.mdx": "2024-12-06T14:34:53.355Z", - "app/learn/customization/integrate-systems/schedule-task/page.mdx": "2024-12-06T14:34:53.355Z", - "app/learn/customization/integrate-systems/service/page.mdx": "2024-12-06T14:34:53.356Z", + "app/learn/fundamentals/api-routes/additional-data/page.mdx": "2024-12-09T10:46:28.589Z", + "app/learn/fundamentals/workflows/variable-manipulation/page.mdx": "2024-11-19T16:43:01.663Z", + "app/learn/customization/custom-features/api-route/page.mdx": "2024-12-09T10:39:30.046Z", + "app/learn/customization/custom-features/module/page.mdx": "2024-12-09T11:02:39.826Z", + "app/learn/customization/custom-features/workflow/page.mdx": "2024-12-09T10:46:47.051Z", + "app/learn/customization/extend-features/extend-create-product/page.mdx": "2024-12-09T11:02:39.423Z", + "app/learn/customization/custom-features/page.mdx": "2024-12-09T10:46:28.593Z", + "app/learn/customization/customize-admin/page.mdx": "2024-12-09T11:02:38.801Z", + "app/learn/customization/customize-admin/route/page.mdx": "2024-12-09T11:02:38.969Z", + "app/learn/customization/customize-admin/widget/page.mdx": "2024-12-09T11:02:39.108Z", + "app/learn/customization/extend-features/define-link/page.mdx": "2024-12-09T11:02:39.346Z", + "app/learn/customization/extend-features/page.mdx": "2024-12-09T11:02:39.244Z", + "app/learn/customization/extend-features/query-linked-records/page.mdx": "2024-12-09T11:02:39.519Z", + "app/learn/customization/integrate-systems/handle-event/page.mdx": "2024-12-09T11:02:39.744Z", + "app/learn/customization/integrate-systems/page.mdx": "2024-12-09T10:40:08.528Z", + "app/learn/customization/integrate-systems/schedule-task/page.mdx": "2024-12-09T10:52:19.379Z", + "app/learn/customization/integrate-systems/service/page.mdx": "2024-12-09T11:02:39.594Z", "app/learn/customization/next-steps/page.mdx": "2024-12-06T14:34:53.356Z", - "app/learn/customization/page.mdx": "2024-09-12T11:16:18.504Z", - "app/learn/more-resources/cheatsheet/page.mdx": "2024-07-11T16:11:26.480Z", - "app/learn/architecture/architectural-modules/page.mdx": "2024-09-23T12:51:04.520Z", - "app/learn/architecture/overview/page.mdx": "2024-09-23T12:55:01.339Z", - "app/learn/advanced-development/data-models/infer-type/page.mdx": "2024-09-30T08:43:53.123Z", - "app/learn/advanced-development/custom-cli-scripts/seed-data/page.mdx": "2024-10-03T11:11:07.181Z", - "app/learn/basics/modules/page.mdx": "2024-11-28T15:41:58.354Z", - "app/learn/advanced-development/environment-variables/page.mdx": "2024-10-25T14:59:07.831Z", - "app/learn/build/page.mdx": "2024-11-11T11:08:41.832Z", + "app/learn/fundamentals/modules/architectural-modules/page.mdx": "2024-10-21T13:30:21.367Z", + "app/learn/introduction/architecture/page.mdx": "2024-10-21T13:30:21.368Z", + "app/learn/fundamentals/data-models/infer-type/page.mdx": "2024-10-21T13:30:21.368Z", + "app/learn/fundamentals/custom-cli-scripts/seed-data/page.mdx": "2024-10-21T13:30:21.368Z", + "app/learn/fundamentals/environment-variables/page.mdx": "2024-12-09T11:00:57.428Z", + "app/learn/build/page.mdx": "2024-12-09T11:05:17.383Z", "app/learn/deployment/general/page.mdx": "2024-11-25T14:33:50.439Z", - "app/learn/advanced-development/workflows/multiple-step-usage/page.mdx": "2024-11-12T11:11:49.191Z", - "app/learn/installation/page.mdx": "2024-11-28T14:18:21.905Z", - "app/learn/advanced-development/data-models/check-constraints/page.mdx": "2024-12-04T10:39:01.764Z" + "app/learn/fundamentals/workflows/multiple-step-usage/page.mdx": "2024-11-25T16:19:32.169Z", + "app/learn/installation/page.mdx": "2024-12-09T11:22:27.919Z", + "app/learn/fundamentals/data-models/check-constraints/page.mdx": "2024-12-06T14:34:50.384Z" } \ No newline at end of file diff --git a/www/apps/book/next.config.mjs b/www/apps/book/next.config.mjs index aa49639579e80..a91473df28eda 100644 --- a/www/apps/book/next.config.mjs +++ b/www/apps/book/next.config.mjs @@ -10,6 +10,7 @@ import { } from "remark-rehype-plugins" import { sidebar } from "./sidebar.mjs" import path from "path" +import redirects from "./utils/redirects.mjs" const withMDX = mdx({ extension: /\.mdx?$/, @@ -165,41 +166,7 @@ const nextConfig = { ], } }, - async redirects() { - return [ - { - source: "/v2/:path*", - destination: "/:path*", - permanent: true, - }, - { - source: "/recipes/:path*", - destination: "/resources/recipes", - permanent: true, - }, - { - source: "/plugins/:path*", - destination: "/v1/plugins/:path*", - permanent: true, - }, - { - source: "/medusa-react/:path*", - destination: "/v1/medusa-react/:path*", - permanent: true, - }, - { - source: "/learn/customization/extend-models/:path*", - destination: "/learn/customization/extend-features/:path*", - permanent: true, - }, - { - source: "/learn/customization/extend-features/create-links", - destination: - "/learn/customization/extend-features/extend-create-product", - permanent: true, - }, - ] - }, + redirects, } export default withMDX(nextConfig) diff --git a/www/apps/book/sidebar.mjs b/www/apps/book/sidebar.mjs index b764aa8f2d628..e188f3a7eef54 100644 --- a/www/apps/book/sidebar.mjs +++ b/www/apps/book/sidebar.mjs @@ -16,80 +16,14 @@ export const sidebar = numberSidebarItems( }, { type: "link", - path: "/learn/first-customizations", - title: "Your First Customizations", - chapterTitle: "First Customization", - }, - ], - }, - { - type: "link", - path: "/learn/basics", - title: "The Basics", - chapterTitle: "Basics", - children: [ - { - type: "link", - path: "/learn/basics/project-directories-files", - title: "Project Conventions", - }, - { - type: "link", - path: "/learn/basics/medusa-container", - title: "Medusa Container", - }, - { - type: "link", - path: "/learn/basics/modules", - title: "Modules", - }, - { - type: "link", - path: "/learn/basics/api-routes", - title: "API Routes", - }, - { - type: "link", - path: "/learn/basics/commerce-modules", - title: "Commerce Modules", - }, - { - type: "link", - path: "/learn/basics/modules-directory-structure", - title: "Modules Directory Structure", - }, - { - type: "link", - path: "/learn/basics/loaders", - title: "Loaders", - }, - { - type: "link", - path: "/learn/basics/events-and-subscribers", - title: "Events and Subscribers", - }, - { - type: "link", - path: "/learn/basics/scheduled-jobs", - title: "Scheduled Jobs", - }, - { - type: "link", - path: "/learn/basics/workflows", - title: "Workflows", - }, - { - type: "link", - path: "/learn/basics/admin-customizations", - title: "Admin Customizations", + title: "Architecture", + path: "/learn/introduction/architecture", }, ], }, { - type: "link", - path: "/learn/customization", - title: "Custom Development", - chapterTitle: "Customization", + type: "category", + title: "Customize", children: [ { type: "link", @@ -182,348 +116,361 @@ export const sidebar = numberSidebarItems( ], }, { - type: "link", - path: "/learn/advanced-development", - title: "Advanced Development", - chapterTitle: "Advanced", + type: "category", + title: "Fundamentals", children: [ { type: "link", - title: "Architecture", - path: "/learn/advanced-development/architecture/overview", - children: [ - { - type: "link", - path: "/learn/advanced-development/architecture/architectural-modules", - title: "Architectural Modules", - }, - ], + path: "/learn/fundamentals/medusa-container", + title: "Medusa Container", }, { type: "link", - title: "API Routes", - path: "/learn/advanced-development/api-routes", + path: "/learn/fundamentals/modules", + title: "Modules", children: [ { type: "link", - path: "/learn/advanced-development/api-routes/http-methods", - title: "HTTP Methods", + path: "/learn/fundamentals/modules/modules-directory-structure", + title: "Modules Directory Structure", }, { type: "link", - path: "/learn/advanced-development/api-routes/parameters", - title: "Parameters", + path: "/learn/fundamentals/modules/loaders", + title: "Loaders", }, { type: "link", - path: "/learn/advanced-development/api-routes/responses", - title: "Response", - }, - { - type: "link", - path: "/learn/advanced-development/api-routes/middlewares", - title: "Middlewares", - }, - { - type: "link", - path: "/learn/advanced-development/api-routes/validation", - title: "Validation", - }, - { - type: "link", - path: "/learn/advanced-development/api-routes/protected-routes", - title: "Protected Routes", - }, - { - type: "link", - path: "/learn/advanced-development/api-routes/errors", - title: "Errors", - }, - { - type: "link", - path: "/learn/advanced-development/api-routes/cors", - title: "Handling CORS", - }, - { - type: "link", - path: "/learn/advanced-development/api-routes/additional-data", - title: "Additional Data", - }, - ], - }, - { - type: "link", - path: "/learn/advanced-development/modules", - title: "Modules", - children: [ - { - type: "link", - path: "/learn/advanced-development/modules/isolation", + path: "/learn/fundamentals/modules/isolation", title: "Module Isolation", }, { type: "link", - path: "/learn/advanced-development/modules/container", + path: "/learn/fundamentals/modules/container", title: "Module Container", }, { type: "link", - path: "/learn/advanced-development/modules/options", + path: "/learn/fundamentals/modules/options", title: "Module Options", }, { type: "link", - path: "/learn/advanced-development/modules/service-factory", + path: "/learn/fundamentals/modules/service-factory", title: "Service Factory", }, { type: "link", - path: "/learn/advanced-development/modules/service-constraints", + path: "/learn/fundamentals/modules/service-constraints", title: "Service Constraints", }, { type: "link", - path: "/learn/advanced-development/modules/db-operations", + path: "/learn/fundamentals/modules/db-operations", title: "Database Operations", }, { type: "link", - path: "/learn/advanced-development/modules/multiple-services", + path: "/learn/fundamentals/modules/multiple-services", title: "Multiple Services", }, + { + type: "link", + path: "/learn/fundamentals/modules/commerce-modules", + title: "Commerce Modules", + }, + { + type: "link", + path: "/learn/fundamentals/modules/architectural-modules", + title: "Architectural Modules", + }, ], }, { type: "link", - path: "/learn/advanced-development/module-links", + path: "/learn/fundamentals/module-links", title: "Module Links", children: [ { type: "link", - path: "/learn/advanced-development/module-links/directions", + path: "/learn/fundamentals/module-links/directions", title: "Module Link Direction", }, { type: "link", - path: "/learn/advanced-development/module-links/remote-link", + path: "/learn/fundamentals/module-links/remote-link", title: "Remote Link", }, { type: "link", - path: "/learn/advanced-development/module-links/query", + path: "/learn/fundamentals/module-links/query", title: "Query", }, { type: "link", - path: "/learn/advanced-development/module-links/custom-columns", + path: "/learn/fundamentals/module-links/custom-columns", title: "Custom Columns", }, ], }, { type: "link", - path: "/learn/advanced-development/data-models", + path: "/learn/fundamentals/data-models", title: "Data Models", children: [ { type: "link", - path: "/learn/advanced-development/data-models/infer-type", + path: "/learn/fundamentals/data-models/infer-type", title: "Infer Type", }, { type: "link", - path: "/learn/advanced-development/data-models/property-types", + path: "/learn/fundamentals/data-models/property-types", title: "Property Types", }, { type: "link", - path: "/learn/advanced-development/data-models/primary-key", + path: "/learn/fundamentals/data-models/primary-key", title: "Primary Key", }, { type: "link", - path: "/learn/advanced-development/data-models/default-properties", + path: "/learn/fundamentals/data-models/default-properties", title: "Default Properties", }, { type: "link", - path: "/learn/advanced-development/data-models/configure-properties", + path: "/learn/fundamentals/data-models/configure-properties", title: "Configure Properties", }, { type: "link", - path: "/learn/advanced-development/data-models/relationships", + path: "/learn/fundamentals/data-models/relationships", title: "Relationships", }, { type: "link", - path: "/learn/advanced-development/data-models/manage-relationships", + path: "/learn/fundamentals/data-models/manage-relationships", title: "Manage Relationships", }, { type: "link", - path: "/learn/advanced-development/data-models/index", + path: "/learn/fundamentals/data-models/index", title: "Define Index", }, { type: "link", - path: "/learn/advanced-development/data-models/check-constraints", + path: "/learn/fundamentals/data-models/check-constraints", title: "Check Constraints", }, { type: "link", - path: "/learn/advanced-development/data-models/searchable-property", + path: "/learn/fundamentals/data-models/searchable-property", title: "Searchable Property", }, { type: "link", - path: "/learn/advanced-development/data-models/write-migration", + path: "/learn/fundamentals/data-models/write-migration", title: "Write Migration", }, ], }, { - type: "sub-category", - title: "Events and Subscribers", + type: "link", + title: "API Routes", + path: "/learn/fundamentals/api-routes", children: [ { type: "link", - path: "/learn/advanced-development/events-and-subscribers/data-payload", - title: "Events Data Payload", + path: "/learn/fundamentals/api-routes/http-methods", + title: "HTTP Methods", }, { type: "link", - path: "/learn/advanced-development/events-and-subscribers/emit-event", - title: "Emit Event", + path: "/learn/fundamentals/api-routes/parameters", + title: "Parameters", }, - ], - }, - { - type: "sub-category", - title: "Scheduled Jobs", - children: [ { type: "link", - path: "/learn/advanced-development/scheduled-jobs/execution-number", - title: "Execution Number", + path: "/learn/fundamentals/api-routes/responses", + title: "Response", + }, + { + type: "link", + path: "/learn/fundamentals/api-routes/middlewares", + title: "Middlewares", + }, + { + type: "link", + path: "/learn/fundamentals/api-routes/validation", + title: "Validation", + }, + { + type: "link", + path: "/learn/fundamentals/api-routes/protected-routes", + title: "Protected Routes", + }, + { + type: "link", + path: "/learn/fundamentals/api-routes/errors", + title: "Errors", + }, + { + type: "link", + path: "/learn/fundamentals/api-routes/cors", + title: "Handling CORS", + }, + { + type: "link", + path: "/learn/fundamentals/api-routes/additional-data", + title: "Additional Data", }, ], }, { type: "link", - path: "/learn/advanced-development/workflows", + path: "/learn/fundamentals/workflows", title: "Workflows", children: [ { type: "link", - path: "/learn/advanced-development/workflows/variable-manipulation", + path: "/learn/fundamentals/workflows/variable-manipulation", title: "Transform Variables", }, { type: "link", - path: "/learn/advanced-development/workflows/conditions", + path: "/learn/fundamentals/workflows/conditions", title: "When-Then Conditions", }, { type: "link", - path: "/learn/advanced-development/workflows/constructor-constraints", + path: "/learn/fundamentals/workflows/constructor-constraints", title: "Constructor Constraints", }, { type: "link", - path: "/learn/advanced-development/workflows/compensation-function", + path: "/learn/fundamentals/workflows/compensation-function", title: "Compensation Function", }, { type: "link", - path: "/learn/advanced-development/workflows/workflow-hooks", + path: "/learn/fundamentals/workflows/workflow-hooks", title: "Workflow Hooks", }, { type: "link", - path: "/learn/advanced-development/workflows/add-workflow-hook", + path: "/learn/fundamentals/workflows/add-workflow-hook", title: "Expose a Hook", }, { type: "link", - path: "/learn/advanced-development/workflows/access-workflow-errors", + path: "/learn/fundamentals/workflows/access-workflow-errors", title: "Access Workflow Errors", }, { type: "link", - path: "/learn/advanced-development/workflows/retry-failed-steps", + path: "/learn/fundamentals/workflows/retry-failed-steps", title: "Retry Failed Steps", }, { type: "link", - path: "/learn/advanced-development/workflows/parallel-steps", + path: "/learn/fundamentals/workflows/parallel-steps", title: "Run Steps in Parallel", }, { type: "link", - path: "/learn/advanced-development/workflows/workflow-timeout", + path: "/learn/fundamentals/workflows/workflow-timeout", title: "Workflow Timeout", }, { type: "link", - path: "/learn/advanced-development/workflows/long-running-workflow", + path: "/learn/fundamentals/workflows/long-running-workflow", title: "Long-Running Workflow", }, { type: "link", - path: "/learn/advanced-development/workflows/execute-another-workflow", + path: "/learn/fundamentals/workflows/execute-another-workflow", title: "Execute Another Workflow", }, { type: "link", - path: "/learn/advanced-development/workflows/multiple-step-usage", + path: "/learn/fundamentals/workflows/multiple-step-usage", title: "Multiple Step Usage", }, ], }, { type: "link", - path: "/learn/advanced-development/custom-cli-scripts", - title: "Custom CLI Scripts", + path: "/learn/fundamentals/events-and-subscribers", + title: "Events and Subscribers", children: [ { type: "link", - path: "/learn/advanced-development/custom-cli-scripts/seed-data", - title: "Seed Data", + path: "/learn/fundamentals/events-and-subscribers/data-payload", + title: "Events Data Payload", + }, + { + type: "link", + path: "/learn/fundamentals/events-and-subscribers/emit-event", + title: "Emit Event", + }, + ], + }, + { + type: "link", + path: "/learn/fundamentals/scheduled-jobs", + title: "Scheduled Jobs", + children: [ + { + type: "link", + path: "/learn/fundamentals/scheduled-jobs/execution-number", + title: "Execution Number", }, ], }, { type: "link", - path: "/learn/advanced-development/admin", + path: "/learn/fundamentals/admin", title: "Admin Development", children: [ { type: "link", - path: "/learn/advanced-development/admin/widgets", + path: "/learn/fundamentals/admin/widgets", title: "Admin Widgets", }, { type: "link", - path: "/learn/advanced-development/admin/ui-routes", + path: "/learn/fundamentals/admin/ui-routes", title: "Admin UI Routes", }, { type: "link", - path: "/learn/advanced-development/admin/constraints", + path: "/learn/fundamentals/admin/constraints", title: "Constraints", }, { type: "link", - path: "/learn/advanced-development/admin/tips", + path: "/learn/fundamentals/admin/tips", title: "Tips", }, ], }, + { + type: "link", + path: "/learn/fundamentals/custom-cli-scripts", + title: "Custom CLI Scripts", + children: [ + { + type: "link", + path: "/learn/fundamentals/custom-cli-scripts/seed-data", + title: "Seed Data", + }, + ], + }, { type: "link", title: "Environment Variables", - path: "/learn/advanced-development/environment-variables", + path: "/learn/fundamentals/environment-variables", }, ], }, @@ -614,13 +561,6 @@ export const sidebar = numberSidebarItems( type: "link", path: "/learn/more-resources", title: "More Resources", - children: [ - { - type: "link", - path: "/learn/more-resources/cheatsheet", - title: "Cheat Sheet", - }, - ], }, ]) ) diff --git a/www/apps/book/utils/number-sidebar-items.mjs b/www/apps/book/utils/number-sidebar-items.mjs index e341e09387a7e..d6e5b4aab1697 100644 --- a/www/apps/book/utils/number-sidebar-items.mjs +++ b/www/apps/book/utils/number-sidebar-items.mjs @@ -16,6 +16,7 @@ export default function numberSidebarItems(sidebarItems, numbering = [1]) { sidebarItems.forEach((item, index) => { if (item.type === "separator") { ;(parentItem?.children || numberedItems).push(item) + return } // append current number to the item's title @@ -27,13 +28,17 @@ export default function numberSidebarItems(sidebarItems, numbering = [1]) { if (isTopItems) { // Add chapter category - numberedItems.push({ - type: "category", - title: item.chapterTitle, - children: [], - loaded: true, - initialOpen: false, - }) + numberedItems.push( + item.type === "category" + ? item + : { + type: "category", + title: item.chapterTitle, + children: [], + loaded: true, + initialOpen: false, + } + ) parentItem = numberedItems[numberedItems.length - 1] } @@ -42,19 +47,12 @@ export default function numberSidebarItems(sidebarItems, numbering = [1]) { item.children = numberSidebarItems(item.children, [...numbering, 1]) } - ;(parentItem?.children || numberedItems).push(item) + if (item.type !== "category" || !isTopItems) { + ;(parentItem?.children || numberedItems).push(item) + } numbering[numbering.length - 1]++ }) return numberedItems } - -function padNumber(number) { - number = number.toString() - if (number.length < 2) { - number = `0` + number - } - - return number -} diff --git a/www/apps/book/utils/redirects.mjs b/www/apps/book/utils/redirects.mjs new file mode 100644 index 0000000000000..53416e6c68897 --- /dev/null +++ b/www/apps/book/utils/redirects.mjs @@ -0,0 +1,109 @@ +/** + * @returns {Promise} + */ +const redirects = async () => { + return [ + { + source: "/v2/:path*", + destination: "/:path*", + permanent: true, + }, + { + source: "/recipes/:path*", + destination: "/resources/recipes", + permanent: true, + }, + { + source: "/plugins/:path*", + destination: "/v1/plugins/:path*", + permanent: true, + }, + { + source: "/medusa-react/:path*", + destination: "/v1/medusa-react/:path*", + permanent: true, + }, + { + source: "/learn/customization/extend-models/:path*", + destination: "/learn/customization/extend-features/:path*", + permanent: true, + }, + { + source: "/learn/advanced-development/architecture/overview", + destination: "/learn/introduction/architecture", + permanent: true, + }, + { + source: "/learn/first-customizations", + destination: "/learn/customization", + permanent: true, + }, + { + source: "/learn/basics/medusa-container", + destination: "/learn/fundamentals/medusa-container", + permanent: true, + }, + { + source: "/learn/basics/modules", + destination: "/learn/fundamentals/modules", + permanent: true, + }, + { + source: "/learn/basics/modules-directory-structure", + destination: "/learn/fundamentals/modules/modules-directory-structure", + permanent: true, + }, + { + source: "/learn/basics/loaders", + destination: "/learn/fundamentals/modules/loaders", + permanent: true, + }, + { + source: "/learn/basics/commerce-modules", + destination: "/learn/fundamentals/modules/commerce-modules", + permanent: true, + }, + { + source: "/learn/advanced-development/architecture/architectural-modules", + destination: "/learn/fundamentals/modules/architectural-modules", + permanent: true, + }, + { + source: "/learn/basics/api-routes", + destination: "/learn/fundamentals/api-routes", + permanent: true, + }, + { + source: "/learn/basics/workflows", + destination: "/learn/fundamentals/workflows", + permanent: true, + }, + { + source: "/learn/basics/events-and-subscribers", + destination: "/learn/fundamentals/events-and-subscribers", + permanent: true, + }, + { + source: "/learn/basics/scheduled-jobs", + destination: "/learn/fundamentals/scheduled-jobs", + permanent: true, + }, + { + source: "/learn/basics/project-directories-files", + destination: "/learn/installation#project-files", + permanent: true, + }, + { + source: "/learn/basics/admin-customizations", + destination: "/learn/fundamentals/admin/widgets", + permanent: true, + }, + { + source: "/learn/advanced-development/:path*", + destination: "/learn/fundamentals/:path*", + permanent: true, + }, + ] +} + +export default redirects diff --git a/www/apps/resources/app/admin-components/components/forms/page.mdx b/www/apps/resources/app/admin-components/components/forms/page.mdx index 64a24647a5b89..7689ba6ba01a3 100644 --- a/www/apps/resources/app/admin-components/components/forms/page.mdx +++ b/www/apps/resources/app/admin-components/components/forms/page.mdx @@ -175,7 +175,7 @@ You create the `CreateForm` component. For now, it uses `useForm` from `react-ho You also define a `handleSubmit` function to perform an action when the form is submitted. -You can replace the content of the function with sending a request to Medusa's routes. Refer to [this guide](!docs!/learn/advanced-development/admin/tips#send-requests-to-api-routes) for more details on how to do that. +You can replace the content of the function with sending a request to Medusa's routes. Refer to [this guide](!docs!/learn/fundamentals/admin/tips#send-requests-to-api-routes) for more details on how to do that. ### Render Components @@ -454,7 +454,7 @@ You create the `EditForm` component. For now, it uses `useForm` from `react-hook You also define a `handleSubmit` function to perform an action when the form is submitted. -You can replace the content of the function with sending a request to Medusa's routes. Refer to [this guide](!docs!/learn/advanced-development/admin/tips#send-requests-to-api-routes) for more details on how to do that. +You can replace the content of the function with sending a request to Medusa's routes. Refer to [this guide](!docs!/learn/fundamentals/admin/tips#send-requests-to-api-routes) for more details on how to do that. ### Render Components diff --git a/www/apps/resources/app/commerce-modules/auth/create-actor-type/page.mdx b/www/apps/resources/app/commerce-modules/auth/create-actor-type/page.mdx index 2642c3d55fb11..978e51adebd4a 100644 --- a/www/apps/resources/app/commerce-modules/auth/create-actor-type/page.mdx +++ b/www/apps/resources/app/commerce-modules/auth/create-actor-type/page.mdx @@ -12,7 +12,7 @@ Before creating an actor type, you must have a module with a data model represen -Learn how to create a module in [this guide](!docs!/learn/basics/modules). +Learn how to create a module in [this guide](!docs!/learn/fundamentals/modules). diff --git a/www/apps/resources/app/commerce-modules/cart/extend/page.mdx b/www/apps/resources/app/commerce-modules/cart/extend/page.mdx index d9ccafc782795..4f1e117252cc3 100644 --- a/www/apps/resources/app/commerce-modules/cart/extend/page.mdx +++ b/www/apps/resources/app/commerce-modules/cart/extend/page.mdx @@ -22,7 +22,7 @@ Consider you have a Hello Module defined in the `/src/modules/hello` directory. -If you don't have a module, follow [this guide](!docs!/learn/basics/modules) to create one. +If you don't have a module, follow [this guide](!docs!/learn/fundamentals/modules) to create one. @@ -43,7 +43,7 @@ This creates a `Custom` data model that has the `id` and `custom_name` propertie -Learn more about data models in [this guide](!docs!/learn/basics/modules#1-create-data-model). +Learn more about data models in [this guide](!docs!/learn/fundamentals/modules#1-create-data-model). @@ -55,7 +55,7 @@ Next, you'll define a module link between the `Custom` and `Cart` data model. A -Learn more about module links in [this guide](!docs!/learn/advanced-development/module-links). +Learn more about module links in [this guide](!docs!/learn/fundamentals/module-links). @@ -82,7 +82,7 @@ This defines a link between the `Cart` and `Custom` data models. Using this link items={[ { text: "Module must be registered in medusa-config.ts", - link: "!docs!/learn/basics/modules#4-add-module-to-configurations" + link: "!docs!/learn/fundamentals/modules#4-add-module-to-configurations" } ]} /> @@ -115,7 +115,7 @@ To do that, you'll consume the [cartCreated](/references/medusa-workflows/create -Learn more about workflow hooks in [this guide](!docs!/learn/advanced-development/workflows/workflow-hooks). +Learn more about workflow hooks in [this guide](!docs!/learn/fundamentals/workflows/workflow-hooks). @@ -150,7 +150,7 @@ In the snippet above, you add a validation rule indicating that `custom_name` is -Learn more about additional data validation in [this guide](!docs!/learn/advanced-development/api-routes/additional-data). +Learn more about additional data validation in [this guide](!docs!/learn/fundamentals/api-routes/additional-data). @@ -202,7 +202,7 @@ In the compensation function that undoes the step's actions in case of an error, -Learn more about compensation functions in [this guide](!docs!/learn/advanced-development/workflows/compensation-function). +Learn more about compensation functions in [this guide](!docs!/learn/fundamentals/workflows/compensation-function). @@ -260,9 +260,9 @@ The workflow accepts as an input the created cart and the `additional_data` para In the workflow, you: -1. Use the `transform` utility to get the value of `custom_name` based on whether it's set in `additional_data`. Learn more about why you can't use conditional operators in a workflow without using `transform` in [this guide](!docs!/learn/advanced-development/workflows/conditions#why-if-conditions-arent-allowed-in-workflows). +1. Use the `transform` utility to get the value of `custom_name` based on whether it's set in `additional_data`. Learn more about why you can't use conditional operators in a workflow without using `transform` in [this guide](!docs!/learn/fundamentals/workflows/conditions#why-if-conditions-arent-allowed-in-workflows). 2. Create the `Custom` record using the `createCustomStep`. -3. Use the `when-then` utility to link the cart to the `Custom` record if it was created. Learn more about why you can't use if-then conditions in a workflow without using `when-then` in [this guide](!docs!/learn/advanced-development/workflows/conditions#why-if-conditions-arent-allowed-in-workflows). +3. Use the `when-then` utility to link the cart to the `Custom` record if it was created. Learn more about why you can't use if-then conditions in a workflow without using `when-then` in [this guide](!docs!/learn/fundamentals/workflows/conditions#why-if-conditions-arent-allowed-in-workflows). You'll next call the workflow in the hook handler. @@ -355,7 +355,7 @@ Among the returned `cart` object, you'll find a `custom` property which holds th ### Retrieve using Query -You can also retrieve the `Custom` record linked to a cart in your code using [Query](!docs!/learn/advanced-development/module-links/query). +You can also retrieve the `Custom` record linked to a cart in your code using [Query](!docs!/learn/fundamentals/module-links/query). For example: @@ -369,7 +369,7 @@ const { data: [cart] } = await query.graph({ }) ``` -Learn more about how to use Query in [this guide](!docs!/learn/advanced-development/module-links/query). +Learn more about how to use Query in [this guide](!docs!/learn/fundamentals/module-links/query). --- diff --git a/www/apps/resources/app/commerce-modules/customer/extend/page.mdx b/www/apps/resources/app/commerce-modules/customer/extend/page.mdx index 352c3882100b9..d94fdb6ee5202 100644 --- a/www/apps/resources/app/commerce-modules/customer/extend/page.mdx +++ b/www/apps/resources/app/commerce-modules/customer/extend/page.mdx @@ -28,7 +28,7 @@ Consider you have a Hello Module defined in the `/src/modules/hello` directory. -If you don't have a module, follow [this guide](!docs!/learn/basics/modules) to create one. +If you don't have a module, follow [this guide](!docs!/learn/fundamentals/modules) to create one. @@ -49,7 +49,7 @@ This creates a `Custom` data model that has the `id` and `custom_name` propertie -Learn more about data models in [this guide](!docs!/learn/basics/modules#1-create-data-model). +Learn more about data models in [this guide](!docs!/learn/fundamentals/modules#1-create-data-model). @@ -61,7 +61,7 @@ Next, you'll define a module link between the `Custom` and `Customer` data model -Learn more about module links in [this guide](!docs!/learn/advanced-development/module-links). +Learn more about module links in [this guide](!docs!/learn/fundamentals/module-links). @@ -88,7 +88,7 @@ This defines a link between the `Customer` and `Custom` data models. Using this items={[ { text: "Module must be registered in medusa-config.ts", - link: "!docs!/learn/basics/modules#4-add-module-to-configurations" + link: "!docs!/learn/fundamentals/modules#4-add-module-to-configurations" } ]} /> @@ -121,7 +121,7 @@ To do that, you'll consume the [customersCreated](/resources/references/medusa-w -Learn more about workflow hooks in [this guide](!docs!/learn/advanced-development/workflows/workflow-hooks). +Learn more about workflow hooks in [this guide](!docs!/learn/fundamentals/workflows/workflow-hooks). @@ -156,7 +156,7 @@ In the snippet above, you add a validation rule indicating that `custom_name` is -Learn more about additional data validation in [this guide](!docs!/learn/advanced-development/api-routes/additional-data). +Learn more about additional data validation in [this guide](!docs!/learn/fundamentals/api-routes/additional-data). @@ -208,7 +208,7 @@ In the compensation function that undoes the step's actions in case of an error, -Learn more about compensation functions in [this guide](!docs!/learn/advanced-development/workflows/compensation-function). +Learn more about compensation functions in [this guide](!docs!/learn/fundamentals/workflows/compensation-function). @@ -266,9 +266,9 @@ The workflow accepts as an input the created customer and the `additional_data` In the workflow, you: -1. Use the `transform` utility to get the value of `custom_name` based on whether it's set in `additional_data`. Learn more about why you can't use conditional operators in a workflow without using `transform` in [this guide](!docs!/learn/advanced-development/workflows/conditions#why-if-conditions-arent-allowed-in-workflows). +1. Use the `transform` utility to get the value of `custom_name` based on whether it's set in `additional_data`. Learn more about why you can't use conditional operators in a workflow without using `transform` in [this guide](!docs!/learn/fundamentals/workflows/conditions#why-if-conditions-arent-allowed-in-workflows). 2. Create the `Custom` record using the `createCustomStep`. -3. Use the `when-then` utility to link the customer to the `Custom` record if it was created. Learn more about why you can't use if-then conditions in a workflow without using `when-then` in [this guide](!docs!/learn/advanced-development/workflows/conditions#why-if-conditions-arent-allowed-in-workflows). +3. Use the `when-then` utility to link the customer to the `Custom` record if it was created. Learn more about why you can't use if-then conditions in a workflow without using `when-then` in [this guide](!docs!/learn/fundamentals/workflows/conditions#why-if-conditions-arent-allowed-in-workflows). You'll next execute the workflow in the hook handler. @@ -367,7 +367,7 @@ Among the returned `customer` object, you'll find a `custom` property which hold ### Retrieve using Query -You can also retrieve the `Custom` record linked to a customer in your code using [Query](!docs!/learn/advanced-development/module-links/query). +You can also retrieve the `Custom` record linked to a customer in your code using [Query](!docs!/learn/fundamentals/module-links/query). For example: @@ -381,7 +381,7 @@ const { data: [customer] } = await query.graph({ }) ``` -Learn more about how to use Query in [this guide](!docs!/learn/advanced-development/module-links/query). +Learn more about how to use Query in [this guide](!docs!/learn/fundamentals/module-links/query). --- diff --git a/www/apps/resources/app/commerce-modules/payment/payment-flow/page.mdx b/www/apps/resources/app/commerce-modules/payment/payment-flow/page.mdx index 10742536de320..68d6c460d8d40 100644 --- a/www/apps/resources/app/commerce-modules/payment/payment-flow/page.mdx +++ b/www/apps/resources/app/commerce-modules/payment/payment-flow/page.mdx @@ -66,7 +66,7 @@ remoteLink.create({ -Learn more about the remote link in [this documentation](!docs!/learn/advanced-development/module-links/remote-link). +Learn more about the remote link in [this documentation](!docs!/learn/fundamentals/module-links/remote-link). diff --git a/www/apps/resources/app/commerce-modules/product/extend/page.mdx b/www/apps/resources/app/commerce-modules/product/extend/page.mdx index decd74503559e..ba9c52021d4d0 100644 --- a/www/apps/resources/app/commerce-modules/product/extend/page.mdx +++ b/www/apps/resources/app/commerce-modules/product/extend/page.mdx @@ -28,7 +28,7 @@ Consider you have a Hello Module defined in the `/src/modules/hello` directory. -If you don't have a module, follow [this guide](!docs!/learn/basics/modules) to create one. +If you don't have a module, follow [this guide](!docs!/learn/fundamentals/modules) to create one. @@ -49,7 +49,7 @@ This creates a `Custom` data model that has the `id` and `custom_name` propertie -Learn more about data models in [this guide](!docs!/learn/basics/modules#1-create-data-model). +Learn more about data models in [this guide](!docs!/learn/fundamentals/modules#1-create-data-model). @@ -61,7 +61,7 @@ Next, you'll define a module link between the `Custom` and `Product` data model. -Learn more about module links in [this guide](!docs!/learn/advanced-development/module-links). +Learn more about module links in [this guide](!docs!/learn/fundamentals/module-links). @@ -88,7 +88,7 @@ This defines a link between the `Product` and `Custom` data models. Using this l items={[ { text: "Module must be registered in medusa-config.js", - link: "!docs!/learn/basics/modules#4-add-module-to-configurations" + link: "!docs!/learn/fundamentals/modules#4-add-module-to-configurations" } ]} /> @@ -121,7 +121,7 @@ To do that, you'll consume the [productsCreated](/references/medusa-workflows/cr -Learn more about workflow hooks in [this guide](!docs!/learn/advanced-development/workflows/workflow-hooks). +Learn more about workflow hooks in [this guide](!docs!/learn/fundamentals/workflows/workflow-hooks). @@ -156,7 +156,7 @@ In the snippet above, you add a validation rule indicating that `custom_name` is -Learn more about additional data validation in [this guide](!docs!/learn/advanced-development/api-routes/additional-data). +Learn more about additional data validation in [this guide](!docs!/learn/fundamentals/api-routes/additional-data). @@ -208,7 +208,7 @@ In the compensation function that undoes the step's actions in case of an error, -Learn more about compensation functions in [this guide](!docs!/learn/advanced-development/workflows/compensation-function). +Learn more about compensation functions in [this guide](!docs!/learn/fundamentals/workflows/compensation-function). @@ -266,9 +266,9 @@ The workflow accepts as an input the created product and the `additional_data` p In the workflow, you: -1. Use the `transform` utility to get the value of `custom_name` based on whether it's set in `additional_data`. Learn more about why you can't use conditional operators in a workflow without using `transform` in [this guide](!docs!/learn/advanced-development/workflows/conditions#why-if-conditions-arent-allowed-in-workflows). +1. Use the `transform` utility to get the value of `custom_name` based on whether it's set in `additional_data`. Learn more about why you can't use conditional operators in a workflow without using `transform` in [this guide](!docs!/learn/fundamentals/workflows/conditions#why-if-conditions-arent-allowed-in-workflows). 2. Create the `Custom` record using the `createCustomStep`. -3. Use the `when-then` utility to link the product to the `Custom` record if it was created. Learn more about why you can't use if-then conditions in a workflow without using `when-then` in [this guide](!docs!/learn/advanced-development/workflows/conditions#why-if-conditions-arent-allowed-in-workflows). +3. Use the `when-then` utility to link the product to the `Custom` record if it was created. Learn more about why you can't use if-then conditions in a workflow without using `when-then` in [this guide](!docs!/learn/fundamentals/workflows/conditions#why-if-conditions-arent-allowed-in-workflows). You'll next execute the workflow in the hook handler. @@ -373,7 +373,7 @@ Among the returned `product` object, you'll find a `custom` property which holds ### Retrieve using Query -You can also retrieve the `Custom` record linked to a product in your code using [Query](!docs!/learn/advanced-development/module-links/query). +You can also retrieve the `Custom` record linked to a product in your code using [Query](!docs!/learn/fundamentals/module-links/query). For example: @@ -387,7 +387,7 @@ const { data: [product] } = await query.graph({ }) ``` -Learn more about how to use Query in [this guide](!docs!/learn/advanced-development/module-links/query). +Learn more about how to use Query in [this guide](!docs!/learn/fundamentals/module-links/query). --- diff --git a/www/apps/resources/app/commerce-modules/product/guides/price-with-taxes/page.mdx b/www/apps/resources/app/commerce-modules/product/guides/price-with-taxes/page.mdx index 60c2d9701613d..f22897512be5c 100644 --- a/www/apps/resources/app/commerce-modules/product/guides/price-with-taxes/page.mdx +++ b/www/apps/resources/app/commerce-modules/product/guides/price-with-taxes/page.mdx @@ -14,7 +14,7 @@ In this document, you'll learn how to calculate a product variant's price with t You'll need the following resources for the taxes calculation: -1. [Query](!docs!/learn/advanced-development/module-links/query) to retrieve the product's variants' prices for a context. Learn more about that in [this guide](../price/page.mdx). +1. [Query](!docs!/learn/fundamentals/module-links/query) to retrieve the product's variants' prices for a context. Learn more about that in [this guide](../price/page.mdx). 2. The Tax Module's main service to get the tax lines for each product. ```ts diff --git a/www/apps/resources/app/commerce-modules/product/guides/price/page.mdx b/www/apps/resources/app/commerce-modules/product/guides/price/page.mdx index 78e2a80bf7fb5..b154b233685c6 100644 --- a/www/apps/resources/app/commerce-modules/product/guides/price/page.mdx +++ b/www/apps/resources/app/commerce-modules/product/guides/price/page.mdx @@ -8,7 +8,7 @@ export const metadata = { # {metadata.title} -In this document, you'll learn how to retrieve product variant prices in the Medusa application using [Query](!docs!/learn/advanced-development/module-links/query). +In this document, you'll learn how to retrieve product variant prices in the Medusa application using [Query](!docs!/learn/fundamentals/module-links/query). diff --git a/www/apps/resources/app/commerce-modules/promotion/extend/page.mdx b/www/apps/resources/app/commerce-modules/promotion/extend/page.mdx index cb7e5f4b00324..0486882101ef6 100644 --- a/www/apps/resources/app/commerce-modules/promotion/extend/page.mdx +++ b/www/apps/resources/app/commerce-modules/promotion/extend/page.mdx @@ -28,7 +28,7 @@ Consider you have a Hello Module defined in the `/src/modules/hello` directory. -If you don't have a module, follow [this guide](!docs!/learn/basics/modules) to create one. +If you don't have a module, follow [this guide](!docs!/learn/fundamentals/modules) to create one. @@ -49,7 +49,7 @@ This creates a `Custom` data model that has the `id` and `custom_name` propertie -Learn more about data models in [this guide](!docs!/learn/basics/modules#1-create-data-model). +Learn more about data models in [this guide](!docs!/learn/fundamentals/modules#1-create-data-model). @@ -61,7 +61,7 @@ Next, you'll define a module link between the `Custom` and `Promotion` data mode -Learn more about module links in [this guide](!docs!/learn/advanced-development/module-links). +Learn more about module links in [this guide](!docs!/learn/fundamentals/module-links). @@ -88,7 +88,7 @@ This defines a link between the `Promotion` and `Custom` data models. Using this items={[ { text: "Module must be registered in medusa-config.js", - link: "!docs!/learn/basics/modules#4-add-module-to-configurations" + link: "!docs!/learn/fundamentals/modules#4-add-module-to-configurations" } ]} /> @@ -121,7 +121,7 @@ To do that, you'll consume the [promotionsCreated](/references/medusa-workflows/ -Learn more about workflow hooks in [this guide](!docs!/learn/advanced-development/workflows/workflow-hooks). +Learn more about workflow hooks in [this guide](!docs!/learn/fundamentals/workflows/workflow-hooks). @@ -156,7 +156,7 @@ In the snippet above, you add a validation rule indicating that `custom_name` is -Learn more about additional data validation in [this guide](!docs!/learn/advanced-development/api-routes/additional-data). +Learn more about additional data validation in [this guide](!docs!/learn/fundamentals/api-routes/additional-data). @@ -208,7 +208,7 @@ In the compensation function that undoes the step's actions in case of an error, -Learn more about compensation functions in [this guide](!docs!/learn/advanced-development/workflows/compensation-function). +Learn more about compensation functions in [this guide](!docs!/learn/fundamentals/workflows/compensation-function). @@ -266,9 +266,9 @@ The workflow accepts as an input the created promotion and the `additional_data` In the workflow, you: -1. Use the `transform` utility to get the value of `custom_name` based on whether it's set in `additional_data`. Learn more about why you can't use conditional operators in a workflow without using `transform` in [this guide](!docs!/learn/advanced-development/workflows/conditions#why-if-conditions-arent-allowed-in-workflows). +1. Use the `transform` utility to get the value of `custom_name` based on whether it's set in `additional_data`. Learn more about why you can't use conditional operators in a workflow without using `transform` in [this guide](!docs!/learn/fundamentals/workflows/conditions#why-if-conditions-arent-allowed-in-workflows). 2. Create the `Custom` record using the `createCustomStep`. -3. Use the `when-then` utility to link the promotion to the `Custom` record if it was created. Learn more about why you can't use if-then conditions in a workflow without using `when-then` in [this guide](!docs!/learn/advanced-development/workflows/conditions#why-if-conditions-arent-allowed-in-workflows). +3. Use the `when-then` utility to link the promotion to the `Custom` record if it was created. Learn more about why you can't use if-then conditions in a workflow without using `when-then` in [this guide](!docs!/learn/fundamentals/workflows/conditions#why-if-conditions-arent-allowed-in-workflows). You'll next execute the workflow in the hook handler. @@ -379,7 +379,7 @@ Among the returned `promotion` object, you'll find a `custom` property which hol ### Retrieve using Query -You can also retrieve the `Custom` record linked to a promotion in your code using [Query](!docs!/learn/advanced-development/module-links/query). +You can also retrieve the `Custom` record linked to a promotion in your code using [Query](!docs!/learn/fundamentals/module-links/query). For example: @@ -393,7 +393,7 @@ const { data: [promotion] } = await query.graph({ }) ``` -Learn more about how to use Query in [this guide](!docs!/learn/advanced-development/module-links/query). +Learn more about how to use Query in [this guide](!docs!/learn/fundamentals/module-links/query). --- diff --git a/www/apps/resources/app/examples/page.mdx b/www/apps/resources/app/examples/page.mdx index d82e19ff17db6..3c5d490524daa 100644 --- a/www/apps/resources/app/examples/page.mdx +++ b/www/apps/resources/app/examples/page.mdx @@ -36,7 +36,7 @@ export const GET = ( This creates a `GET` API route at `/hello-world`. -Learn more in [this documentation](!docs!/learn/basics/api-routes). +Learn more in [this documentation](!docs!/learn/fundamentals/api-routes). ### Resolve Resources in API Route @@ -65,7 +65,7 @@ export const GET = async ( This resolves the Product Module's main service. -Learn more in [this documentation](!docs!/learn/basics/medusa-container). +Learn more in [this documentation](!docs!/learn/fundamentals/medusa-container). ### Use Path Parameters @@ -93,7 +93,7 @@ export const GET = async ( } ``` -Learn more about path parameters in [this documentation](!docs!/learn/advanced-development/api-routes/parameters#path-parameters). +Learn more about path parameters in [this documentation](!docs!/learn/fundamentals/api-routes/parameters#path-parameters). ### Use Query Parameters @@ -119,7 +119,7 @@ export const GET = async ( } ``` -Learn more about query parameters in [this documentation](!docs!/learn/advanced-development/api-routes/parameters#query-parameters). +Learn more about query parameters in [this documentation](!docs!/learn/fundamentals/api-routes/parameters#query-parameters). ### Use Body Parameters @@ -150,7 +150,7 @@ export const POST = async ( } ``` -Learn more about request body parameters in [this documentation](!docs!/learn/advanced-development/api-routes/parameters#request-body-parameters). +Learn more about request body parameters in [this documentation](!docs!/learn/fundamentals/api-routes/parameters#request-body-parameters). ### Set Response Code @@ -169,7 +169,7 @@ export const GET = async ( } ``` -Learn more about setting the response code in [this documentation](!docs!/learn/advanced-development/api-routes/responses#set-response-status-code). +Learn more about setting the response code in [this documentation](!docs!/learn/fundamentals/api-routes/responses#set-response-status-code). ### Execute a Workflow in an API Route @@ -197,7 +197,7 @@ export async function GET( } ``` -Learn more in [this documentation](!docs!/learn/basics/workflows#3-execute-the-workflow). +Learn more in [this documentation](!docs!/learn/fundamentals/workflows#3-execute-the-workflow). ### Change Response Content Type @@ -235,7 +235,7 @@ export const GET = async ( This changes the response type to return an event stream. -Learn more in [this documentation](!docs!/learn/advanced-development/api-routes/responses#change-response-content-type). +Learn more in [this documentation](!docs!/learn/fundamentals/api-routes/responses#change-response-content-type). ### Create Middleware @@ -285,7 +285,7 @@ export default defineMiddlewares({ }) ``` -Learn more about middlewares in [this documentation](!docs!/learn/advanced-development/api-routes/middlewares). +Learn more about middlewares in [this documentation](!docs!/learn/fundamentals/api-routes/middlewares). ### Restrict HTTP Methods in Middleware @@ -370,7 +370,7 @@ export const POST = async ( } ``` -Learn more about request body validation in [this documentation](!docs!/learn/advanced-development/api-routes/validation). +Learn more about request body validation in [this documentation](!docs!/learn/fundamentals/api-routes/validation). ### Pass Additional Data to API Route @@ -403,7 +403,7 @@ export default defineMiddlewares({ -Learn more about additional data in [this documentation](!docs!/learn/advanced-development/api-routes/additional-data). +Learn more about additional data in [this documentation](!docs!/learn/fundamentals/api-routes/additional-data). @@ -430,7 +430,7 @@ createProductsWorkflow.hooks.productsCreated( -Learn more about workflow hooks in [this documentation](!docs!/learn/advanced-development/workflows/workflow-hooks). +Learn more about workflow hooks in [this documentation](!docs!/learn/fundamentals/workflows/workflow-hooks). @@ -461,7 +461,7 @@ export default defineMiddlewares({ }) ``` -Learn more in [this documentation](!docs!/learn/advanced-development/api-routes/protected-routes). +Learn more in [this documentation](!docs!/learn/fundamentals/api-routes/protected-routes). ### Restrict an API Route to Logged-In Customers @@ -487,7 +487,7 @@ export default defineMiddlewares({ }) ``` -Learn more in [this documentation](!docs!/learn/advanced-development/api-routes/protected-routes). +Learn more in [this documentation](!docs!/learn/fundamentals/api-routes/protected-routes). ### Retrieve Logged-In Admin User @@ -522,7 +522,7 @@ export const GET = async ( } ``` -Learn more in [this documentation](!docs!/learn/advanced-development/api-routes/protected-routes#retrieve-logged-in-admin-users-details). +Learn more in [this documentation](!docs!/learn/fundamentals/api-routes/protected-routes#retrieve-logged-in-admin-users-details). ### Retrieve Logged-In Customer @@ -560,7 +560,7 @@ export const GET = async ( } ``` -Learn more in [this documentation](!docs!/learn/advanced-development/api-routes/protected-routes#retrieve-logged-in-customers-details). +Learn more in [this documentation](!docs!/learn/fundamentals/api-routes/protected-routes#retrieve-logged-in-customers-details). ### Throw Errors in API Route @@ -585,7 +585,7 @@ export const GET = async ( } ``` -Learn more in [this documentation](!docs!/learn/advanced-development/api-routes/errors). +Learn more in [this documentation](!docs!/learn/fundamentals/api-routes/errors). ### Override Error Handler of API Routes @@ -614,7 +614,7 @@ export default defineMiddlewares({ }) ``` -Learn more in [this documentation](!docs!/learn/advanced-development/api-routes/errors#override-error-handler), +Learn more in [this documentation](!docs!/learn/fundamentals/api-routes/errors#override-error-handler), ### Setting up CORS for Custom API Routes @@ -707,7 +707,7 @@ A module is a package of reusable commerce or architectural functionalities. The -Find this example explained in details in [this documentation](!docs!/learn/basics/modules). +Find this example explained in details in [this documentation](!docs!/learn/fundamentals/modules). @@ -843,7 +843,7 @@ class HelloModuleService extends MedusaService({ export default HelloModuleService ``` -Learn more in [this documentation](!docs!/learn/advanced-development/modules/multiple-services). +Learn more in [this documentation](!docs!/learn/fundamentals/modules/multiple-services). ### Accept Module Options @@ -895,7 +895,7 @@ export default class HelloModuleService extends MedusaService({ } ``` -Learn more in [this documentation](!docs!/learn/advanced-development/modules/options). +Learn more in [this documentation](!docs!/learn/fundamentals/modules/options). ### Integrate Third-Party System in Module @@ -974,7 +974,7 @@ npx medusa db:generate helloModuleService npx medusa db:migrate ``` -Learn more in [this documentation](!docs!/learn/basics/modules#1-create-data-model). +Learn more in [this documentation](!docs!/learn/fundamentals/modules#1-create-data-model). ### Data Model Property Types @@ -1061,7 +1061,7 @@ const MyCustom = model.define("my_custom", { }) ``` -Learn more in [this documentation](!docs!/learn/advanced-development/data-models/property-types). +Learn more in [this documentation](!docs!/learn/fundamentals/data-models/property-types). ### Set Primary Key @@ -1104,7 +1104,7 @@ const MyCustom = model.define("my_custom", { export default MyCustom ``` -Learn more in [this documentation](!docs!/learn/advanced-development/data-models/primary-key). +Learn more in [this documentation](!docs!/learn/fundamentals/data-models/primary-key). ### Default Property Value @@ -1126,7 +1126,7 @@ const MyCustom = model.define("my_custom", { export default MyCustom ``` -Learn more in [this documentation](!docs!/learn/advanced-development/data-models/configure-properties). +Learn more in [this documentation](!docs!/learn/fundamentals/data-models/configure-properties). ### Nullable Property @@ -1143,7 +1143,7 @@ const MyCustom = model.define("my_custom", { export default MyCustom ``` -Learn more in [this documentation](!docs!/learn/advanced-development/data-models/configure-properties#nullable-property). +Learn more in [this documentation](!docs!/learn/fundamentals/data-models/configure-properties#nullable-property). ### Unique Property @@ -1160,7 +1160,7 @@ const User = model.define("user", { export default User ``` -Learn more in [this documentation](!docs!/learn/advanced-development/data-models/configure-properties#unique-property). +Learn more in [this documentation](!docs!/learn/fundamentals/data-models/configure-properties#unique-property). ### Define Database Index on Property @@ -1179,7 +1179,7 @@ const MyCustom = model.define("my_custom", { export default MyCustom ``` -Learn more in [this documentation](!docs!/learn/advanced-development/data-models/index#define-database-index-on-property). +Learn more in [this documentation](!docs!/learn/fundamentals/data-models/index#define-database-index-on-property). ### Define Composite Index on Data Model @@ -1206,7 +1206,7 @@ const MyCustom = model.define("my_custom", { export default MyCustom ``` -Learn more in [this documentation](!docs!/learn/advanced-development/data-models/index#define-database-index-on-data-model). +Learn more in [this documentation](!docs!/learn/fundamentals/data-models/index#define-database-index-on-data-model). ### Make a Property Searchable @@ -1237,7 +1237,7 @@ const myCustoms = await helloModuleService.listMyCustoms({ }) ``` -Learn more in [this documentation](!docs!/learn/advanced-development/data-models/searchable-property). +Learn more in [this documentation](!docs!/learn/fundamentals/data-models/searchable-property). ### Create One-to-One Relationship @@ -1259,7 +1259,7 @@ const Email = model.define("email", { }) ``` -Learn more in [this documentation](!docs!/learn/advanced-development/data-models/relationships#one-to-one-relationship). +Learn more in [this documentation](!docs!/learn/fundamentals/data-models/relationships#one-to-one-relationship). ### Create One-to-Many Relationship @@ -1281,7 +1281,7 @@ const Product = model.define("product", { }) ``` -Learn more in [this documentation](!docs!/learn/advanced-development/data-models/relationships#one-to-many-relationship). +Learn more in [this documentation](!docs!/learn/fundamentals/data-models/relationships#one-to-many-relationship). ### Create Many-to-Many Relationship @@ -1305,7 +1305,7 @@ const Product = model.define("product", { }) ``` -Learn more in [this documentation](!docs!/learn/advanced-development/data-models/relationships#many-to-many-relationship). +Learn more in [this documentation](!docs!/learn/fundamentals/data-models/relationships#many-to-many-relationship). ### Configure Cascades of Data Model @@ -1326,7 +1326,7 @@ const Store = model.define("store", { This configures the delete cascade on the `Store` data model so that, when a store is delete, its products are also deleted. -Learn more in [this documentation](!docs!/learn/advanced-development/data-models/relationships#cascades). +Learn more in [this documentation](!docs!/learn/fundamentals/data-models/relationships#cascades). ### Manage One-to-One Relationship @@ -1373,7 +1373,7 @@ const user = await helloModuleService.updateUsers({ }) ``` -Learn more in [this documentation](!docs!/learn/advanced-development/data-models/manage-relationships#manage-one-to-one-relationship). +Learn more in [this documentation](!docs!/learn/fundamentals/data-models/manage-relationships#manage-one-to-one-relationship). ### Manage One-to-Many Relationship @@ -1402,7 +1402,7 @@ const product = await helloModuleService.updateProducts({ }) ``` -Learn more in [this documentation](!docs!/learn/advanced-development/data-models/manage-relationships#manage-one-to-many-relationship) +Learn more in [this documentation](!docs!/learn/fundamentals/data-models/manage-relationships#manage-one-to-many-relationship) ### Manage Many-to-Many Relationship @@ -1443,7 +1443,7 @@ const updatedProduct = await helloModuleService.updateProducts({ }) ``` -Learn more in [this documentation](!docs!/learn/advanced-development/data-models/manage-relationships#manage-many-to-many-relationship). +Learn more in [this documentation](!docs!/learn/fundamentals/data-models/manage-relationships#manage-many-to-many-relationship). ### Retrieve Related Records @@ -1464,7 +1464,7 @@ const product = await helloModuleService.retrieveProducts( ) ``` -Learn more in [this documentation](!docs!/learn/advanced-development/data-models/manage-relationships#retrieve-records-of-relation). +Learn more in [this documentation](!docs!/learn/fundamentals/data-models/manage-relationships#retrieve-records-of-relation). --- @@ -1495,7 +1495,7 @@ The `HelloModuleService` will now have data-management methods for `MyCustom`. Refer to [this reference](../service-factory-reference/page.mdx) for details on the generated methods. -Learn more about the service factory in [this documentation](!docs!/learn/advanced-development/modules/service-factory). +Learn more about the service factory in [this documentation](!docs!/learn/fundamentals/modules/service-factory). ### Resolve Resources in the Service @@ -1557,7 +1557,7 @@ export default class HelloModuleService { -Learn more in [this documentation](!docs!/learn/advanced-development/modules/container). +Learn more in [this documentation](!docs!/learn/fundamentals/modules/container). ### Access Module Options in Service @@ -1589,7 +1589,7 @@ export default class HelloModuleService extends MedusaService({ } ``` -Learn more in [this documentation](!docs!/learn/advanced-development/modules/options). +Learn more in [this documentation](!docs!/learn/fundamentals/modules/options). ### Run Database Query in Service @@ -1625,7 +1625,7 @@ class HelloModuleService { } ``` -Learn more in [this documentation](!docs!/learn/advanced-development/modules/db-operations#run-queries) +Learn more in [this documentation](!docs!/learn/fundamentals/modules/db-operations#run-queries) ### Execute Database Operations in Transactions @@ -1682,7 +1682,7 @@ class HelloModuleService { } ``` -Learn more in [this documentation](!docs!/learn/advanced-development/modules/db-operations#execute-operations-in-transactions). +Learn more in [this documentation](!docs!/learn/fundamentals/modules/db-operations#execute-operations-in-transactions). --- @@ -1713,7 +1713,7 @@ export default defineLink( npx medusa db:migrate ``` -Learn more in [this documentation](!docs!/learn/advanced-development/module-links). +Learn more in [this documentation](!docs!/learn/fundamentals/module-links). ### Define a List Link @@ -1733,7 +1733,7 @@ export default defineLink( ) ``` -Learn more about list links in [this documentation](!docs!/learn/advanced-development/module-links#define-a-list-link). +Learn more about list links in [this documentation](!docs!/learn/fundamentals/module-links#define-a-list-link). ### Set Delete Cascade on Link Definition @@ -1753,7 +1753,7 @@ export default defineLink( ) ``` -Learn more in [this documentation](!docs!/learn/advanced-development/module-links#define-a-list-link). +Learn more in [this documentation](!docs!/learn/fundamentals/module-links#define-a-list-link). ### Add Custom Columns to Module Link @@ -1813,7 +1813,7 @@ const { data } = await query.graph({ }) ``` -Learn more in [this documentation](!docs!/learn/advanced-development/module-links/custom-columns). +Learn more in [this documentation](!docs!/learn/fundamentals/module-links/custom-columns). ### Create Link Between Records @@ -1835,7 +1835,7 @@ await remoteLink.create({ }) ``` -Learn more in [this documentation](!docs!/learn/advanced-development/module-links/remote-link#create-link). +Learn more in [this documentation](!docs!/learn/fundamentals/module-links/remote-link#create-link). ### Dismiss Link Between Records @@ -1857,7 +1857,7 @@ await remoteLink.dismiss({ }) ``` -Learn more in [this documentation](!docs!/learn/advanced-development/module-links/remote-link#dismiss-link). +Learn more in [this documentation](!docs!/learn/fundamentals/module-links/remote-link#dismiss-link). ### Cascade Delete Linked Records @@ -1877,7 +1877,7 @@ await remoteLink.delete({ }) ``` -Learn more in [this documentation](!docs!/learn/advanced-development/module-links/remote-link#cascade-delete-linked-records). +Learn more in [this documentation](!docs!/learn/fundamentals/module-links/remote-link#cascade-delete-linked-records). ### Restore Linked Records @@ -1897,7 +1897,7 @@ await remoteLink.restore({ }) ``` -Learn more in [this documentation](!docs!/learn/advanced-development/module-links/remote-link#restore-linked-records). +Learn more in [this documentation](!docs!/learn/fundamentals/module-links/remote-link#restore-linked-records). --- @@ -1933,7 +1933,7 @@ export const GET = async ( } ``` -Learn more in [this documentation](!docs!/learn/advanced-development/module-links/query). +Learn more in [this documentation](!docs!/learn/fundamentals/module-links/query). ### Retrieve Linked Records of Data Model @@ -1967,7 +1967,7 @@ export const GET = async ( } ``` -Learn more in [this documentation](!docs!/learn/advanced-development/module-links/query#retrieve-linked-records). +Learn more in [this documentation](!docs!/learn/fundamentals/module-links/query#retrieve-linked-records). ### Apply Filters to Retrieved Records @@ -2003,7 +2003,7 @@ export const GET = async ( } ``` -Learn more in [this documentation](!docs!/learn/advanced-development/module-links/query#apply-filters). +Learn more in [this documentation](!docs!/learn/fundamentals/module-links/query#apply-filters). ### Apply Pagination and Sort Records @@ -2048,7 +2048,7 @@ export const GET = async ( } ``` -Learn more in [this documentation](!docs!/learn/advanced-development/module-links/query#sort-records). +Learn more in [this documentation](!docs!/learn/fundamentals/module-links/query#sort-records). --- @@ -2115,7 +2115,7 @@ const myWorkflow = createWorkflow( export default myWorkflow ``` -Learn more in [this documentation](!docs!/learn/basics/workflows). +Learn more in [this documentation](!docs!/learn/fundamentals/workflows). ### Execute a Workflow @@ -2211,7 +2211,7 @@ Learn more in [this documentation](!docs!/learn/basics/workflows). -Learn more in [this documentation](!docs!/learn/basics/workflows#3-execute-the-workflow). +Learn more in [this documentation](!docs!/learn/fundamentals/workflows#3-execute-the-workflow). ### Step with a Compensation Function @@ -2238,7 +2238,7 @@ const step1 = createStep( ) ``` -Learn more in [this documentation](!docs!/learn/advanced-development/workflows/compensation-function). +Learn more in [this documentation](!docs!/learn/fundamentals/workflows/compensation-function). ### Manipulate Variables in Workflow @@ -2268,7 +2268,7 @@ const myWorkflow = createWorkflow( ) ``` -Learn more in [this documentation](!docs!/learn/advanced-development/workflows/variable-manipulation) +Learn more in [this documentation](!docs!/learn/fundamentals/workflows/variable-manipulation) ### Using Conditions in Workflow @@ -2336,7 +2336,7 @@ const workflow = createWorkflow( ) ``` -Learn more in [this documentation](!docs!/learn/advanced-development/workflows/execute-another-workflow). +Learn more in [this documentation](!docs!/learn/fundamentals/workflows/execute-another-workflow). ### Consume a Workflow Hook @@ -2357,7 +2357,7 @@ createProductsWorkflow.hooks.productsCreated( This executes a custom step at the hook's designated point in the workflow. -Learn more in [this documentation](!docs!/learn/advanced-development/workflows/workflow-hooks). +Learn more in [this documentation](!docs!/learn/fundamentals/workflows/workflow-hooks). ### Expose a Hook @@ -2388,7 +2388,7 @@ export const myWorkflow = createWorkflow( ) ``` -Learn more in [this documentation](!book!/learn/advanced-development/workflows/add-workflow-hook). +Learn more in [this documentation](!book!/learn/fundamentals/workflows/add-workflow-hook). ### Retry Steps @@ -2412,7 +2412,7 @@ export const step1 = createStep( ) ``` -Learn more in [this documentation](!docs!/learn/advanced-development/workflows/retry-failed-steps). +Learn more in [this documentation](!docs!/learn/fundamentals/workflows/retry-failed-steps). ### Run Steps in Parallel @@ -2453,7 +2453,7 @@ const myWorkflow = createWorkflow( ) ``` -Learn more in [this documentation](!docs!/learn/advanced-development/workflows/parallel-steps). +Learn more in [this documentation](!docs!/learn/fundamentals/workflows/parallel-steps). ### Configure Workflow Timeout @@ -2481,7 +2481,7 @@ const myWorkflow = createWorkflow({ export default myWorkflow ``` -Learn more in [this documentation](!docs!/learn/advanced-development/workflows/workflow-timeout). +Learn more in [this documentation](!docs!/learn/fundamentals/workflows/workflow-timeout). ### Configure Step Timeout @@ -2499,7 +2499,7 @@ const step1 = createStep( ) ``` -Learn more in [this documentation](!docs!/learn/advanced-development/workflows/workflow-timeout#configure-step-timeout). +Learn more in [this documentation](!docs!/learn/fundamentals/workflows/workflow-timeout#configure-step-timeout). ### Long-Running Workflow @@ -2519,7 +2519,7 @@ const step2 = createStep( ) ``` -Learn more in [this documentation](!docs!/learn/advanced-development/workflows/long-running-workflow). +Learn more in [this documentation](!docs!/learn/fundamentals/workflows/long-running-workflow). ### Change Step Status in Long-Running Workflow @@ -2588,7 +2588,7 @@ await workflowEngineService.setStepFailure({ }) ``` -Learn more in [this documentation](!docs!/learn/advanced-development/workflows/long-running-workflow). +Learn more in [this documentation](!docs!/learn/fundamentals/workflows/long-running-workflow). ### Access Long-Running Workflow's Result @@ -2634,7 +2634,7 @@ export async function GET(req: MedusaRequest, res: MedusaResponse) { } ``` -Learn more in [this documentation](!docs!/learn/advanced-development/workflows/long-running-workflow#access-long-running-workflow-status-and-result). +Learn more in [this documentation](!docs!/learn/fundamentals/workflows/long-running-workflow#access-long-running-workflow-status-and-result). --- @@ -2664,7 +2664,7 @@ export const config: SubscriberConfig = { } ``` -Learn more in [this documentation](!docs!/learn/basics/events-and-subscribers). +Learn more in [this documentation](!docs!/learn/fundamentals/events-and-subscribers). ### Resolve Resources in Subscriber @@ -2694,7 +2694,7 @@ export const config: SubscriberConfig = { } ``` -Learn more in [this documentation](!docs!/learn/basics/events-and-subscribers#resolve-resources). +Learn more in [this documentation](!docs!/learn/fundamentals/events-and-subscribers#resolve-resources). ### Send a Notification to Reset Password @@ -2779,7 +2779,7 @@ export const config: SubscriberConfig = { } ``` -Learn more in [this documentation](!docs!/learn/basics/workflows#3-execute-the-workflow) +Learn more in [this documentation](!docs!/learn/fundamentals/workflows#3-execute-the-workflow) --- @@ -2805,7 +2805,7 @@ export const config = { } ``` -Learn more in [this documentation](!docs!/learn/basics/scheduled-jobs). +Learn more in [this documentation](!docs!/learn/fundamentals/scheduled-jobs). ### Resolve Resources in Scheduled Job @@ -2834,7 +2834,7 @@ export const config = { } ``` -Learn more in [this documentation](!docs!/learn/basics/scheduled-jobs#resolve-resources) +Learn more in [this documentation](!docs!/learn/fundamentals/scheduled-jobs#resolve-resources) ### Specify a Job's Execution Number @@ -2853,7 +2853,7 @@ export const config = { } ``` -Learn more in [this documentation](!docs!/learn/advanced-development/scheduled-jobs/execution-number). +Learn more in [this documentation](!docs!/learn/fundamentals/scheduled-jobs/execution-number). ### Execute a Workflow in a Scheduled Job @@ -2882,7 +2882,7 @@ export const config = { } ``` -Learn more in [this documentation](!docs!/learn/basics/workflows#3-execute-the-workflow) +Learn more in [this documentation](!docs!/learn/fundamentals/workflows#3-execute-the-workflow) --- @@ -2904,7 +2904,7 @@ export default async function helloWorldLoader() { } ``` -Learn more in [this documentation](!docs!/learn/basics/loaders). +Learn more in [this documentation](!docs!/learn/fundamentals/modules/loaders). ### Resolve Resources in Loader @@ -2927,7 +2927,7 @@ export default async function helloWorldLoader({ } ``` -Learn more in [this documentation](!docs!/learn/advanced-development/modules/container). +Learn more in [this documentation](!docs!/learn/fundamentals/modules/container). ### Access Module Options @@ -2954,7 +2954,7 @@ export default async function helloWorldLoader({ } ``` -Learn more in [this documentation](!docs!/learn/advanced-development/modules/options). +Learn more in [this documentation](!docs!/learn/fundamentals/modules/options). ### Register Resources in the Module's Container @@ -3019,7 +3019,7 @@ export const config = defineWidgetConfig({ export default ProductWidget ``` -Learn more about widgets in [this documentation](!docs!/learn/advanced-development/admin/widgets). +Learn more about widgets in [this documentation](!docs!/learn/fundamentals/admin/widgets). ### Receive Details Props in Widgets @@ -3056,7 +3056,7 @@ export const config = defineWidgetConfig({ export default ProductWidget ``` -Learn more in [this documentation](!docs!/learn/advanced-development/admin/widgets#detail-widget-props). +Learn more in [this documentation](!docs!/learn/fundamentals/admin/widgets#detail-widget-props). ### Create a UI Route @@ -3089,7 +3089,7 @@ export default CustomPage This adds a new page at `localhost:9000/app/custom`. -Learn more in [this documentation](!docs!/learn/advanced-development/admin/ui-routes). +Learn more in [this documentation](!docs!/learn/fundamentals/admin/ui-routes). ### Create Settings Page @@ -3120,7 +3120,7 @@ export default CustomSettingPage This adds a setting page at `localhost:9000/app/settings/custom`. -Learn more in [this documentation](!docs!/learn/advanced-development/admin/ui-routes#create-settings-page) +Learn more in [this documentation](!docs!/learn/fundamentals/admin/ui-routes#create-settings-page) ### Accept Path Parameters in UI Routes @@ -3149,7 +3149,7 @@ export default CustomPage This creates a UI route at `localhost:9000/app/custom/:id`, where `:id` is a path parameter. -Learn more in [this documentation](!docs!/learn/advanced-development/admin/ui-routes#path-parameters) +Learn more in [this documentation](!docs!/learn/fundamentals/admin/ui-routes#path-parameters) ### Send Request to API Route @@ -3196,7 +3196,7 @@ export const config = defineWidgetConfig({ export default ProductWidget ``` -Learn more in [this documentation](!docs!/learn/advanced-development/admin/tips#send-requests-to-api-routes) +Learn more in [this documentation](!docs!/learn/fundamentals/admin/tips#send-requests-to-api-routes) ### Add Link to Another Page @@ -3224,7 +3224,7 @@ export const config = defineWidgetConfig({ export default ProductWidget ``` -Learn more in [this documentation](!docs!/learn/advanced-development/admin/tips#routing-functionalities). +Learn more in [this documentation](!docs!/learn/fundamentals/admin/tips#routing-functionalities). --- @@ -3750,7 +3750,7 @@ Learn more in [this documentation](../commerce-modules/payment/payment-flow/page ### Get Variant's Prices for Region and Currency -To get prices of a product variant for a region and currency using [Query](!docs!/learn/advanced-development/module-links/query): +To get prices of a product variant for a region and currency using [Query](!docs!/learn/fundamentals/module-links/query): ```ts import { QueryContext } from "@medusajs/framework/utils" @@ -3782,7 +3782,7 @@ Learn more in [this documentation](../commerce-modules/product/guides/price/page ### Get All Variant's Prices -To get all prices of a product variant using [Query](!docs!/learn/advanced-development/module-links/query): +To get all prices of a product variant using [Query](!docs!/learn/fundamentals/module-links/query): ```ts const { data: products } = await query.graph({ @@ -3804,7 +3804,7 @@ Learn more in [this documentation](../commerce-modules/product/guides/price/page ### Get Variant Prices with Taxes -To get a variant's prices with taxes using [Query](!docs!/learn/advanced-development/module-links/query) and the [Tax Module](../commerce-modules/tax/page.mdx) +To get a variant's prices with taxes using [Query](!docs!/learn/fundamentals/module-links/query) and the [Tax Module](../commerce-modules/tax/page.mdx) ```ts import { diff --git a/www/apps/resources/app/integrations/guides/resend/page.mdx b/www/apps/resources/app/integrations/guides/resend/page.mdx index 954ec61f1ffcd..836437089bfd1 100644 --- a/www/apps/resources/app/integrations/guides/resend/page.mdx +++ b/www/apps/resources/app/integrations/guides/resend/page.mdx @@ -81,7 +81,7 @@ Afterwards, the installation process will start, which will install the Medusa a -The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](!docs!/learn/basics/api-routes). Learn more about Medusa's architecture in [this documentation](!docs!/learn/advanced-development/architecture/overview). +The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](!docs!/learn/fundamentals/api-routes). Learn more about Medusa's architecture in [this documentation](!docs!/learn/introduction/architecture). @@ -154,7 +154,7 @@ Medusa's Notification Module delegates sending notifications to other modules, c -Learn more about modules in [this documentation](!docs!/learn/basics/modules). +Learn more about modules in [this documentation](!docs!/learn/fundamentals/modules). @@ -248,7 +248,7 @@ class ResendNotificationProviderService extends AbstractNotificationProviderServ A module's service accepts two parameters: -1. Dependencies resolved from the [Module's container](!docs!/learn/advanced-development/modules/container), which is the module's local registry that the Medusa application adds framework tools to. In this service, you resolve the [Logger utility](!docs!/learn/debugging-and-testing/logging) from the module's container. +1. Dependencies resolved from the [Module's container](!docs!/learn/fundamentals/modules/container), which is the module's local registry that the Medusa application adds framework tools to. In this service, you resolve the [Logger utility](!docs!/learn/debugging-and-testing/logging) from the module's container. 2. The module's options that are passed to the module in Medusa's configuration as you'll see in a later section. Using the API key passed in the module's options, you initialize the Resend client. You also set the `options` and `logger` properties. @@ -627,7 +627,7 @@ Instead, you'll create a subscriber that listens to the `order.placed` event and -Learn more about Medusa's event system in [this documentation](!docs!/learn/basics/events-and-subscribers). +Learn more about Medusa's event system in [this documentation](!docs!/learn/fundamentals/events-and-subscribers). @@ -639,7 +639,7 @@ A workflow is a series of queries and actions, called steps, that complete a tas -Learn more about workflows in [this documentation](!docs!/learn/basics/workflows) +Learn more about workflows in [this documentation](!docs!/learn/fundamentals/workflows) @@ -669,7 +669,7 @@ You define the `sendNotificationStep` using the `createStep` function that accep - A string indicating the step's unique name. - The step's function definition as a second parameter. It accepts the step's input as a first parameter, and an object of options as a second. -The `container` property in the second parameter is an instance of the [Medusa container](!docs!/learn/basics/medusa-container), which is a registry of framework and commerce tools, such a module's service, that you can resolve to utilize their functionalities. +The `container` property in the second parameter is an instance of the [Medusa container](!docs!/learn/fundamentals/medusa-container), which is a registry of framework and commerce tools, such a module's service, that you can resolve to utilize their functionalities. @@ -741,7 +741,7 @@ You create a workflow using the `createWorkflow` function imported from `@medusa It accepts as a second parameter a constructor function, which is the workflow's implementation. The workflow has the following steps: -1. `useQueryGraphStep`, which is a step implemented by Medusa that uses [Query](!docs!/learn/advanced-development/module-links/query), a tool that allows you to retrieve data across modules. You use it to retrieve the order's details. +1. `useQueryGraphStep`, which is a step implemented by Medusa that uses [Query](!docs!/learn/fundamentals/module-links/query), a tool that allows you to retrieve data across modules. You use it to retrieve the order's details. 2. `sendNotificationStep` which is the step you implemented. You pass it an array with one object, which is the notification's details having following properties: - `to`: The address to send the email to. You pass the customer's email that is stored in the order. - `channel`: The channel to send the notification through, which is `email`. Since you specified `email` in the Resend Module Provider's `channel` option, the Notification Module will delegate the sending to the Resend Module Provider's service. @@ -750,7 +750,7 @@ It accepts as a second parameter a constructor function, which is the workflow's -A workflow's constructor function has some constraints in implementation. Learn more about them in [this documentation](!docs!/learn/advanced-development/workflows/constructor-constraints). +A workflow's constructor function has some constraints in implementation. Learn more about them in [this documentation](!docs!/learn/fundamentals/workflows/constructor-constraints). @@ -802,7 +802,7 @@ A subscriber file exports: - An asynchronous function that's executed whenever the associated event is emitted, which is the `order.placed` event. - A configuration object with an `event` property indicating the event the subscriber is listening to. -The subscriber function accepts the event's details as a first paramter which has a `data` property that holds the data payload of the event. For example, Medusa emits the `order.placed` event with the order's ID in the data payload. The function also accepts as a second parameter the [Medusa container](!docs!/learn/basics/medusa-container). +The subscriber function accepts the event's details as a first paramter which has a `data` property that holds the data payload of the event. For example, Medusa emits the `order.placed` event with the order's ID in the data payload. The function also accepts as a second parameter the [Medusa container](!docs!/learn/fundamentals/medusa-container). In the function, you execute the `sendOrderConfirmationWorkflow` by invoking it, passing it the `container`, then using its `run` method. The `run` method accepts an object having an `input` property, which is the input to pass to the workflow. You pass the ID of the placed order as received in the event's data payload. diff --git a/www/apps/resources/app/integrations/guides/sanity/page.mdx b/www/apps/resources/app/integrations/guides/sanity/page.mdx index 53cf6b9f49ff5..9b67050485a30 100644 --- a/www/apps/resources/app/integrations/guides/sanity/page.mdx +++ b/www/apps/resources/app/integrations/guides/sanity/page.mdx @@ -82,7 +82,7 @@ Afterwards, the installation process will start, which will install the Medusa a -The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](!docs!/learn/basics/api-routes). Learn more about Medusa's architecture in [this documentation](!docs!/learn/advanced-development/architecture/overview). +The Medusa application is composed of a headless Node.js server and an admin dashboard. The storefront is installed or custom-built separately and connects to the Medusa application through its REST endpoints, called [API routes](!docs!/learn/fundamentals/api-routes). Learn more about Medusa's architecture in [this documentation](!docs!/learn/introduction/architecture). @@ -138,7 +138,7 @@ In this step, you'll create a Sanity Module that provides the interface to conne -Learn more about modules in [this documentation](!docs!/learn/basics/modules). +Learn more about modules in [this documentation](!docs!/learn/fundamentals/modules). @@ -150,11 +150,11 @@ A module is created under the `src/modules` directory of your Medusa application You define a module's functionalities in a service. A service is a TypeScript or JavaScript class that the module exports. In the service's methods, you can connect to the database, which is useful if your module defines tables in the database, or connect to a third-party service. -Medusa registers the module's service in the [Medusa container](!docs!/learn/basics/medusa-container), allowing you to easily resolve the service from other customizations and use its methods. +Medusa registers the module's service in the [Medusa container](!docs!/learn/fundamentals/medusa-container), allowing you to easily resolve the service from other customizations and use its methods. -The Medusa application registers resources, such as a module's service or the [logging tool](!docs!/learn/debugging-and-testing/logging), in the Medusa container so that you can resolve them from other customizations, as you'll see in later sections. Learn more about it in [this documentation](!docs!/learn/basics/medusa-container). +The Medusa application registers resources, such as a module's service or the [logging tool](!docs!/learn/debugging-and-testing/logging), in the Medusa container so that you can resolve them from other customizations, as you'll see in later sections. Learn more about it in [this documentation](!docs!/learn/fundamentals/medusa-container). @@ -258,7 +258,7 @@ class SanityModuleService { The service's constructor accepts two parameters: -1. Resources to resolve from the Module's container. A module has a different container than the Medusa application, which you can learn more about it in [this documentation](!docs!/learn/advanced-development/modules/container). +1. Resources to resolve from the Module's container. A module has a different container than the Medusa application, which you can learn more about it in [this documentation](!docs!/learn/fundamentals/modules/container). 2. The options passed to the module. In the constructor, you create a Sanity client using the `createClient` function imported from `@sanity/client`. You pass it the options that the module receives. @@ -603,7 +603,7 @@ In the next steps, you'll create a link between the Product and Sanity modules t Since a product has a document in Sanity, you want to build an association between the [Product](../../../commerce-modules/product/page.mdx) and Sanity modules so that when you retrieve a product, you also retrieve its associated Sanity document. -However, modules are [isolated](!docs!/learn/advanced-development/modules/isolation) to ensure they're re-usable and don't have side effects when integrated into the Medusa application. So, to build associations between modules, you define [module links](!docs!/learn/advanced-development/module-links). +However, modules are [isolated](!docs!/learn/fundamentals/modules/isolation) to ensure they're re-usable and don't have side effects when integrated into the Medusa application. So, to build associations between modules, you define [module links](!docs!/learn/fundamentals/module-links). A Module Link associates two modules' data models while maintaining module isolation. A data model can be a table in the database or a virtual model from an external systems. @@ -657,7 +657,7 @@ Within a workflow's steps, you resolve modules to use their service's functional -Learn more about workflows in [this documentation](!docs!/learn/basics/workflows) +Learn more about workflows in [this documentation](!docs!/learn/fundamentals/workflows) @@ -743,7 +743,7 @@ You define the `syncStep` using the `createStep` function, which accepts two par The step function accepts the step's input as a first parameter, and an object of options as a second. The object of options has a `container` property, which is an instance of the Medusa container that you can use to resolve resources. -In the step, you resolve from the Medusa container Sanity Module's service and [Query](!docs!/learn/advanced-development/module-links/query), which is a tool that allows you to retrieve data across modules and links. +In the step, you resolve from the Medusa container Sanity Module's service and [Query](!docs!/learn/fundamentals/module-links/query), which is a tool that allows you to retrieve data across modules and links. You use Query's `graph` method to retrieve products, filtering them by their IDs and applying pagination configurations. The `graph` method accepts a `fields` property in its object parameter, which indicates the product data model's fields and relations to retrieve. @@ -816,7 +816,7 @@ If no errors occur, the step returns an instance of `StepResponse`, which must b -Learn more about compensation functions in [this documentation](!docs!/learn/advanced-development/workflows/compensation-function). +Learn more about compensation functions in [this documentation](!docs!/learn/fundamentals/workflows/compensation-function). @@ -895,7 +895,7 @@ The `retentionTime` property indicates how long should the workflow's progress b -A workflow's constructor function has some constraints in implementation. Learn more about them in [this documentation](!docs!/learn/advanced-development/workflows/constructor-constraints). +A workflow's constructor function has some constraints in implementation. Learn more about them in [this documentation](!docs!/learn/fundamentals/workflows/constructor-constraints). @@ -915,7 +915,7 @@ Subscribers are useful when you want to perform an action that isn't an integral -Learn more about events and subscribers in [this documentation](!docs!/learn/basics/events-and-subscribers). You can also find the list of emitted events in [this reference](../../../events-reference/page.mdx). +Learn more about events and subscribers in [this documentation](!docs!/learn/fundamentals/events-and-subscribers). You can also find the list of emitted events in [this reference](../../../events-reference/page.mdx). @@ -1316,7 +1316,7 @@ You can now manage the product's content in Sanity, add more fields, and customi There are cases where you need to trigger the syncing of products manually, such as when an error occurs or you have products from before creating this integration. -The Medusa Admin dashboard is customizable, allowing you to either inject components, called [widgets](!docs!/learn/advanced-development/admin/widgets), into existing pages, or adding new pages, called [UI routes](!docs!/learn/advanced-development/admin/ui-routes). In these customizations, you can send requests to the Medusa application to perform custom operations. +The Medusa Admin dashboard is customizable, allowing you to either inject components, called [widgets](!docs!/learn/fundamentals/admin/widgets), into existing pages, or adding new pages, called [UI routes](!docs!/learn/fundamentals/admin/ui-routes). In these customizations, you can send requests to the Medusa application to perform custom operations. In this step, you'll add a widget to the product's details page. In that page, you'll show whether a product is synced with Sanity, and allow the admin user to trigger syncing it manually. @@ -1326,7 +1326,7 @@ Before you do that, however, you need two new API routes in your Medusa applicat -An API route is a REST API endpoint that exposes commerce features to the admin dashboard or other frontend clients. Learn more about API routes in [this documentation](!docs!/learn/basics/api-routes). +An API route is a REST API endpoint that exposes commerce features to the admin dashboard or other frontend clients. Learn more about API routes in [this documentation](!docs!/learn/fundamentals/api-routes). @@ -1395,7 +1395,7 @@ You'll test out this route in a later section. -Since the API route is added under the `/admin` prefix, only authenticated admin users can access it. Learn more about protected routes in [this documentation](!docs!/learn/advanced-development/api-routes/protected-routes). +Since the API route is added under the `/admin` prefix, only authenticated admin users can access it. Learn more about protected routes in [this documentation](!docs!/learn/fundamentals/api-routes/protected-routes). @@ -1654,7 +1654,7 @@ To render a widget that matches the rest of the admin dashboard's design, you us -Learn more about widgets in [this documentation](/learn/advanced-development/admin/widgets). +Learn more about widgets in [this documentation](/learn/fundamentals/admin/widgets). @@ -1676,7 +1676,7 @@ Earlier in this guide when introducing workflows, you learned that you can track Medusa has a [workflow engine](../../../architectural-modules/workflow-engine/page.mdx) that manages workflow executions, roll-backs, and other functionalities under the hood. -The workflow engine is an [architectural module](!docs!/learn/advanced-development/architecture/architectural-modules), which can be replaced with a [Redis Workflow Engine](../../../architectural-modules/workflow-engine/redis/page.mdx), or a custom one of your choice, allowing you to take ownership of your application's tooling. +The workflow engine is an [architectural module](!docs!/learn/fundamentals/modules/architectural-modules), which can be replaced with a [Redis Workflow Engine](../../../architectural-modules/workflow-engine/redis/page.mdx), or a custom one of your choice, allowing you to take ownership of your application's tooling. In your customizations, you can resolve the workflow engine from the container and manage executions of a workflow, such as retrieve them and check their progress. @@ -1921,7 +1921,7 @@ To display components that match the design of the Medusa Admin, you use compone -Learn more about UI routes in [this documentation](!docs!/learn/advanced-development/admin/ui-routes). +Learn more about UI routes in [this documentation](!docs!/learn/fundamentals/admin/ui-routes). diff --git a/www/apps/resources/app/medusa-cli/commands/exec/page.mdx b/www/apps/resources/app/medusa-cli/commands/exec/page.mdx index ec86d4bac9cbf..985eb3dc2570a 100644 --- a/www/apps/resources/app/medusa-cli/commands/exec/page.mdx +++ b/www/apps/resources/app/medusa-cli/commands/exec/page.mdx @@ -11,7 +11,7 @@ export const metadata = { # {metadata.title} -Run a custom CLI script. Learn more about it in [this guide](!docs!/learn/advanced-development/custom-cli-scripts). +Run a custom CLI script. Learn more about it in [this guide](!docs!/learn/fundamentals/custom-cli-scripts). ```bash npx medusa exec [file] [args...] diff --git a/www/apps/resources/app/medusa-container-resources/page.mdx b/www/apps/resources/app/medusa-container-resources/page.mdx index 30f249d6a7e1c..534c0c81747aa 100644 --- a/www/apps/resources/app/medusa-container-resources/page.mdx +++ b/www/apps/resources/app/medusa-container-resources/page.mdx @@ -14,7 +14,7 @@ The following table has a list of all resources that you can resolve in your cus -Learn more about the Medusa Container in [this documentation](!docs!/learn/basics/medusa-container). +Learn more about the Medusa Container in [this documentation](!docs!/learn/fundamentals/medusa-container). @@ -147,7 +147,7 @@ The following table has a list of all resources that you can resolve in a module -Learn more about the Module Container in [this documentation](!docs!/learn/advanced-development/modules/container). +Learn more about the Module Container in [this documentation](!docs!/learn/fundamentals/modules/container). diff --git a/www/apps/resources/app/recipes/b2b/page.mdx b/www/apps/resources/app/recipes/b2b/page.mdx index 5b06d915a0d21..66ae087b5640f 100644 --- a/www/apps/resources/app/recipes/b2b/page.mdx +++ b/www/apps/resources/app/recipes/b2b/page.mdx @@ -173,13 +173,13 @@ You can create a B2B module that adds necessary data models to represent a B2B c {/* - You can store the product's ID in the external system using the `metadata` property of the `Product` data model in the Product Module. Alternatively, you can create a [data model](!docs!/learn/basics/modules#1-create-data-model) in your module to store data related to the external system. + You can store the product's ID in the external system using the `metadata` property of the `Product` data model in the Product Module. Alternatively, you can create a [data model](!docs!/learn/fundamentals/modules#1-create-data-model) in your module to store data related to the external system. @@ -150,7 +150,7 @@ Workflows can be executed from anywhere. So, taking the workflow described above Workflows guarantee data consistency through their compensation feature. You can provide a compensation function to steps that roll back the actions of that step. Then, if an error occurs in any step, the actions of previous steps are rolled back using their compensation function. -If you’re calling this API route from a frontend client, make sure to set the [CORS middleware](!docs!/learn/advanced-development/api-routes/cors) on it since it’s not under the `/store` or `/admin` route prefixes. +If you’re calling this API route from a frontend client, make sure to set the [CORS middleware](!docs!/learn/fundamentals/api-routes/cors) on it since it’s not under the `/store` or `/admin` route prefixes. ### Further Reads -- [How to Create a Workflow](!docs!/learn/basics/workflows) -- [What is a Compensation Function](!docs!/learn/advanced-development/workflows/compensation-function) -- [How to Create an API route](!docs!/learn/basics/api-routes) +- [How to Create a Workflow](!docs!/learn/fundamentals/workflows) +- [What is a Compensation Function](!docs!/learn/fundamentals/workflows/compensation-function) +- [How to Create an API route](!docs!/learn/fundamentals/api-routes) --- @@ -704,7 +704,7 @@ This returns the list of restaurants in the response. ### Further Reads -- [What is and how to use it](!docs!/learn/advanced-development/module-links/query) +- [What is and how to use it](!docs!/learn/fundamentals/module-links/query) - [How to Retrieve Prices for Product Variants](../../../../commerce-modules/product/guides/price/page.mdx) --- @@ -1561,7 +1561,7 @@ In this step, you’ll create the workflow that handles the different stages of For example, when a restaurant finishes preparing the order’s items, this workflow creates a fulfillment for the order. -This workflow will be a [long-running workflow](!docs!/learn/advanced-development/workflows/long-running-workflow) that runs asynchronously in the background. Its async steps only succeed once an outside action sets its status, allowing the workflow to move to the next step. +This workflow will be a [long-running workflow](!docs!/learn/fundamentals/workflows/long-running-workflow) that runs asynchronously in the background. Its async steps only succeed once an outside action sets its status, allowing the workflow to move to the next step. API routes that perform actions related to the delivery, which you’ll create later, will trigger the workflow to move to the next step. @@ -2082,7 +2082,7 @@ In the next steps, you’ll execute the workflow and see it in action as you add ### Further Reads -- [Long-Running Workflows](!docs!/learn/advanced-development/workflows/long-running-workflow) +- [Long-Running Workflows](!docs!/learn/fundamentals/workflows/long-running-workflow) --- @@ -3478,7 +3478,7 @@ The next steps of this example depend on your use case. This section provides so ### Admin Development -The Medusa Admin is extendable, allowing you to add widgets to existing pages or create new pages. Learn more about it in [this documentation](!docs!/learn/advanced-development/admin). +The Medusa Admin is extendable, allowing you to add widgets to existing pages or create new pages. Learn more about it in [this documentation](!docs!/learn/fundamentals/admin). ### Storefront Development diff --git a/www/apps/resources/app/recipes/marketplace/examples/vendors/page.mdx b/www/apps/resources/app/recipes/marketplace/examples/vendors/page.mdx index eda36bbf40119..37daae80e5bca 100644 --- a/www/apps/resources/app/recipes/marketplace/examples/vendors/page.mdx +++ b/www/apps/resources/app/recipes/marketplace/examples/vendors/page.mdx @@ -120,7 +120,7 @@ class MarketplaceModuleService extends MedusaService({ export default MarketplaceModuleService ``` -The service extends the [service factory](!docs!/learn/advanced-development/modules/service-factory), which provides basic data-management features. +The service extends the [service factory](!docs!/learn/fundamentals/modules/service-factory), which provides basic data-management features. ### Create Module Definition @@ -154,8 +154,8 @@ module.exports = defineConfig({ ### Further Reads -- [How to Create a Module](!docs!/learn/basics/modules) -- [How to Create Data Models](!docs!/learn/basics/modules#1-create-data-model) +- [How to Create a Module](!docs!/learn/fundamentals/modules) +- [How to Create Data Models](!docs!/learn/fundamentals/modules#1-create-data-model) --- @@ -213,7 +213,7 @@ This adds a list link between the `Vendor` and `Order` data models, indicating t ### Further Read -- [How to Define Module Links](!docs!/learn/advanced-development/module-links) +- [How to Define Module Links](!docs!/learn/fundamentals/module-links) --- @@ -350,10 +350,10 @@ You return the created vendor admin. ### Further Read -- [How to Create a Workflow](!docs!/learn/basics/workflows) +- [How to Create a Workflow](!docs!/learn/fundamentals/workflows) - [What is an Actor Type](../../../../commerce-modules/auth/auth-identity-and-actor-types/page.mdx) - [How to Create an Actor Type](../../../../commerce-modules/auth/create-actor-type/page.mdx) -- [What is a Compensation Function](!docs!/learn/advanced-development/workflows/compensation-function) +- [What is a Compensation Function](!docs!/learn/fundamentals/workflows/compensation-function) --- @@ -527,7 +527,7 @@ This route is available because you created the `vendor` actor type previously. -Don't include a trailing slash at the end of the URL. Learn more [here](!docs!/learn/advanced-development/api-routes/middlewares). +Don't include a trailing slash at the end of the URL. Learn more [here](!docs!/learn/fundamentals/api-routes/middlewares). @@ -563,8 +563,8 @@ Use this token in the header of later requests that require authentication. ### Further Reads -- [How to Create an API route](!docs!/learn/basics/api-routes) -- [How to Create a Middleware](!docs!/learn/advanced-development/api-routes/middlewares) +- [How to Create an API route](!docs!/learn/fundamentals/api-routes) +- [How to Create a Middleware](!docs!/learn/fundamentals/api-routes/middlewares) - [Learn more about the /auth route](../../../../commerce-modules/auth/authentication-route/page.mdx) --- @@ -792,8 +792,8 @@ curl 'http://localhost:9000/vendors/products' \ ### Further Reads -- [How to use Query](!docs!/learn/advanced-development/module-links/query) -- [How to use the Remote Link](!docs!/learn/advanced-development/module-links/remote-link) +- [How to use Query](!docs!/learn/fundamentals/module-links/query) +- [How to use the Remote Link](!docs!/learn/fundamentals/module-links/remote-link) --- @@ -1392,7 +1392,7 @@ For example, you can link sales channels to vendors or other settings. -[Learn more about module links](!docs!/learn/advanced-development/module-links). +[Learn more about module links](!docs!/learn/fundamentals/module-links). @@ -1404,6 +1404,6 @@ You can also create a custom storefront. Check out the [Storefront Development]( ### Admin Development -The Medusa Admin is extendable, allowing you to add widgets to existing pages or create new pages. Learn more about it in [this documentation](!docs!/learn/advanced-development/admin). +The Medusa Admin is extendable, allowing you to add widgets to existing pages or create new pages. Learn more about it in [this documentation](!docs!/learn/fundamentals/admin). If your use case requires bigger customizations to the admin, such as showing different products and orders based on the logged-in vendor, use the [admin API routes](!api!/admin) to build a custom admin. diff --git a/www/apps/resources/app/recipes/marketplace/page.mdx b/www/apps/resources/app/recipes/marketplace/page.mdx index 332c426aa0962..ebace6df4f1e2 100644 --- a/www/apps/resources/app/recipes/marketplace/page.mdx +++ b/www/apps/resources/app/recipes/marketplace/page.mdx @@ -35,13 +35,13 @@ You can create a marketplace module that implements data models for vendors, the -This applies to relations between data models of the same module. To retrieve linked records of different modules, use [Query](!docs!/learn/advanced-development/module-links/query). +This applies to relations between data models of the same module. To retrieve linked records of different modules, use [Query](!docs!/learn/fundamentals/module-links/query). diff --git a/www/apps/resources/app/service-factory-reference/methods/listAndCount/page.mdx b/www/apps/resources/app/service-factory-reference/methods/listAndCount/page.mdx index 33aac00fa951b..b46f5d41a5584 100644 --- a/www/apps/resources/app/service-factory-reference/methods/listAndCount/page.mdx +++ b/www/apps/resources/app/service-factory-reference/methods/listAndCount/page.mdx @@ -54,7 +54,7 @@ The method returns an array with two items: -This applies to relations between data models of the same module. To retrieve linked records of different modules, use [Query](!docs!/learn/advanced-development/module-links/query). +This applies to relations between data models of the same module. To retrieve linked records of different modules, use [Query](!docs!/learn/fundamentals/module-links/query). diff --git a/www/apps/resources/app/service-factory-reference/methods/retrieve/page.mdx b/www/apps/resources/app/service-factory-reference/methods/retrieve/page.mdx index cc09b4c4ac42b..958edf28568fd 100644 --- a/www/apps/resources/app/service-factory-reference/methods/retrieve/page.mdx +++ b/www/apps/resources/app/service-factory-reference/methods/retrieve/page.mdx @@ -30,7 +30,7 @@ The method returns the record as an object. -This applies to relations between data models of the same module. To retrieve linked records of different modules, use [Query](!docs!/learn/advanced-development/module-links/query). +This applies to relations between data models of the same module. To retrieve linked records of different modules, use [Query](!docs!/learn/fundamentals/module-links/query). diff --git a/www/apps/resources/app/service-factory-reference/page.mdx b/www/apps/resources/app/service-factory-reference/page.mdx index c145165551066..d6c9fc88b7399 100644 --- a/www/apps/resources/app/service-factory-reference/page.mdx +++ b/www/apps/resources/app/service-factory-reference/page.mdx @@ -10,7 +10,7 @@ This section of the documentation provides a reference of the methods generated -Learn more about the service factory in [this documentation](!docs!/learn/advanced-development/modules/service-factory). +Learn more about the service factory in [this documentation](!docs!/learn/fundamentals/modules/service-factory). diff --git a/www/apps/resources/app/troubleshooting/medusa-admin/no-widget-route/page.mdx b/www/apps/resources/app/troubleshooting/medusa-admin/no-widget-route/page.mdx index e3d26b715d28a..c4089a1ce8992 100644 --- a/www/apps/resources/app/troubleshooting/medusa-admin/no-widget-route/page.mdx +++ b/www/apps/resources/app/troubleshooting/medusa-admin/no-widget-route/page.mdx @@ -18,7 +18,7 @@ Refer to the [Admin Widget Injection Zones list](../../../admin-widget-injection Widget and UI routes must be defined as arrow functions. Any other type of declaration isn't accepted. -Refer to the [Admin Development Constraints](!docs!/learn/advanced-development/admin/constraints) documentation for more details. +Refer to the [Admin Development Constraints](!docs!/learn/fundamentals/admin/constraints) documentation for more details. --- @@ -34,7 +34,7 @@ export const config = defineWidgetConfig({ Any other usage leads to the widget not being shown. -Refer to the [Admin Development Constraints](!docs!/learn/advanced-development/admin/constraints) documentation for more details. +Refer to the [Admin Development Constraints](!docs!/learn/fundamentals/admin/constraints) documentation for more details. --- diff --git a/www/apps/resources/references/workflows/functions/workflows.createHook/page.mdx b/www/apps/resources/references/workflows/functions/workflows.createHook/page.mdx index a6ae04d5c4517..1723cf2c8c014 100644 --- a/www/apps/resources/references/workflows/functions/workflows.createHook/page.mdx +++ b/www/apps/resources/references/workflows/functions/workflows.createHook/page.mdx @@ -13,7 +13,7 @@ Expose a hook in your workflow where you can inject custom functionality as a st A handler hook can later be registered to consume the hook and perform custom functionality. -Learn more in [this documentation](https://docs.medusajs.com/advanced-development/workflows/add-workflow-hook). +Learn more in [this documentation](https://docs.medusajs.com/fundamentals/workflows/add-workflow-hook). ## Example diff --git a/www/apps/resources/references/workflows/types/workflows.UnwrapWorkflowInputDataType/page.mdx b/www/apps/resources/references/workflows/types/workflows.UnwrapWorkflowInputDataType/page.mdx index 69dfda97b7d60..0eeeac20ae6eb 100644 --- a/www/apps/resources/references/workflows/types/workflows.UnwrapWorkflowInputDataType/page.mdx +++ b/www/apps/resources/references/workflows/types/workflows.UnwrapWorkflowInputDataType/page.mdx @@ -14,4 +14,4 @@ type WorkflowInputData = UnwrapWorkflowInputDataType ## Type Parameters - ReturnType<[StepFunction](../workflows.StepFunction/page.mdx)<TData, TResult>>","description":"","optional":false,"defaultValue":"","expandable":false,"children":[]},{"name":"run","type":"``(...`args`: Parameters<ExportedWorkflow<TData, TResult, TDataOverride, TResultOverride>[\"run\"]>) => ReturnType<ExportedWorkflow<TData, TResult, TDataOverride, TResultOverride>[\"run\"]>","description":"","optional":false,"defaultValue":"","expandable":false,"children":[]},{"name":"getName","type":"() => `string`","description":"This method retrieves the workflow's name.","optional":false,"defaultValue":"","expandable":false,"children":[]},{"name":"config","type":"(`config`: `TransactionModelOptions`) => `void`","description":"This method sets the workflow's configurations.","optional":false,"defaultValue":"","expandable":false,"children":[]},{"name":"hooks","type":"ConvertHooksToFunctions<THooks>","description":"The workflow's exposed hooks, used to register a handler to consume the hook.\n\nLearn more in [this documentation](https://docs.medusajs.com/advanced-development/workflows/add-workflow-hook#how-to-consume-a-hook).","optional":false,"defaultValue":"","expandable":false,"children":[]}]}]} expandUrl="https://docs.medusajs.com/v2/advanced-development/data-models/manage-relationships#retrieve-records-of-relation" sectionTitle="UnwrapWorkflowInputDataType"/> + ReturnType<[StepFunction](../workflows.StepFunction/page.mdx)<TData, TResult>>","description":"","optional":false,"defaultValue":"","expandable":false,"children":[]},{"name":"run","type":"``(...`args`: Parameters<ExportedWorkflow<TData, TResult, TDataOverride, TResultOverride>[\"run\"]>) => ReturnType<ExportedWorkflow<TData, TResult, TDataOverride, TResultOverride>[\"run\"]>","description":"","optional":false,"defaultValue":"","expandable":false,"children":[]},{"name":"getName","type":"() => `string`","description":"This method retrieves the workflow's name.","optional":false,"defaultValue":"","expandable":false,"children":[]},{"name":"config","type":"(`config`: `TransactionModelOptions`) => `void`","description":"This method sets the workflow's configurations.","optional":false,"defaultValue":"","expandable":false,"children":[]},{"name":"hooks","type":"ConvertHooksToFunctions<THooks>","description":"The workflow's exposed hooks, used to register a handler to consume the hook.\n\nLearn more in [this documentation](https://docs.medusajs.com/fundamentals/workflows/add-workflow-hook#how-to-consume-a-hook).","optional":false,"defaultValue":"","expandable":false,"children":[]}]}]} expandUrl="https://docs.medusajs.com/v2/advanced-development/data-models/manage-relationships#retrieve-records-of-relation" sectionTitle="UnwrapWorkflowInputDataType"/> diff --git a/www/apps/ui/contentlayer.config.ts b/www/apps/ui/contentlayer.config.ts index 191a89015ee04..808c558412863 100644 --- a/www/apps/ui/contentlayer.config.ts +++ b/www/apps/ui/contentlayer.config.ts @@ -1,3 +1,5 @@ +import "dotenv/config" + import { defineDocumentType, makeSource } from "contentlayer/source-files" import { rehypeComponent } from "./src/lib/rehype-component" import rehypeSlug from "rehype-slug" @@ -28,5 +30,11 @@ export default makeSource({ documentTypes: [Doc], mdx: { rehypePlugins: [[rehypeComponent], [rehypeSlug]], + mdxOptions: (options) => { + return { + ...options, + development: process.env.NODE_ENV === "development", + } + }, }, }) diff --git a/www/apps/ui/package.json b/www/apps/ui/package.json index 4a111bf3b471e..7e119311795bc 100644 --- a/www/apps/ui/package.json +++ b/www/apps/ui/package.json @@ -5,7 +5,8 @@ "scripts": { "dev": "next dev", "dev:monorepo": "yarn dev -p 3002", - "build": "yarn copy-colors && next build", + "contentlayer:build": "contentlayer build", + "build": "yarn copy-colors && yarn contentlayer:build && next build", "start": "next start", "start:monorepo": "yarn start -p 3002", "lint": "next lint --fix",