From 5ab15a29889870411b719ebad3fb94786baee45e Mon Sep 17 00:00:00 2001 From: Riqwan Thamir Date: Mon, 17 Mar 2025 17:16:27 +0100 Subject: [PATCH] feat(dashboard,js-sdk,admin-shared): add customer addresses + layout change (#11871) what: - changes customer layout from 1 layout to 2 - adds ability to create and delete customer addresses - adds 2 customer widget locations - adds is_giftcard=false by default to products list Screenshot 2025-03-08 at 21 34 02 --- .changeset/cuddly-monkeys-grow.md | 7 + .changeset/violet-trainers-sleep.md | 7 + .../src/extensions/widgets/constants.ts | 2 + .../empty-table-content.tsx | 7 +- .../src/components/common/listicle/index.ts | 1 + .../components/common/listicle/listicle.tsx | 34 +++ .../common/sidebar-link/sidebar-link.tsx | 4 +- .../dashboard-app/routes/get-route.map.tsx | 7 + .../dashboard/src/hooks/api/customers.tsx | 113 +++++++ .../src/i18n/translations/$schema.json | 84 ++++++ .../__tests__/validate-translations.spec.ts | 1 + .../dashboard/src/i18n/translations/en.json | 25 +- .../create-customer-address-form.tsx | 282 ++++++++++++++++++ .../create-customer-address-form/index.ts | 1 + .../customer-create-address.tsx | 10 + .../customer-create-address/index.ts | 1 + .../customer-address-section.tsx | 104 +++++++ .../customer-address-section/index.ts | 1 + .../customer-detail/customer-detail.tsx | 28 +- .../customers/customer-detail/loader.ts | 5 +- .../product-list-table/product-list-table.tsx | 1 + .../routes/products/product-list/loader.ts | 9 +- packages/core/js-sdk/src/admin/customer.ts | 234 ++++++++++++--- .../integration-tests/__tests__/race.spec.ts | 3 +- 24 files changed, 912 insertions(+), 59 deletions(-) create mode 100644 .changeset/cuddly-monkeys-grow.md create mode 100644 .changeset/violet-trainers-sleep.md create mode 100644 packages/admin/dashboard/src/components/common/listicle/index.ts create mode 100644 packages/admin/dashboard/src/components/common/listicle/listicle.tsx create mode 100644 packages/admin/dashboard/src/routes/customers/customer-create-address/components/create-customer-address-form/create-customer-address-form.tsx create mode 100644 packages/admin/dashboard/src/routes/customers/customer-create-address/components/create-customer-address-form/index.ts create mode 100644 packages/admin/dashboard/src/routes/customers/customer-create-address/customer-create-address.tsx create mode 100644 packages/admin/dashboard/src/routes/customers/customer-create-address/index.ts create mode 100644 packages/admin/dashboard/src/routes/customers/customer-detail/components/customer-address-section/customer-address-section.tsx create mode 100644 packages/admin/dashboard/src/routes/customers/customer-detail/components/customer-address-section/index.ts diff --git a/.changeset/cuddly-monkeys-grow.md b/.changeset/cuddly-monkeys-grow.md new file mode 100644 index 0000000000..fdbf82d75f --- /dev/null +++ b/.changeset/cuddly-monkeys-grow.md @@ -0,0 +1,7 @@ +--- +"@medusajs/admin-shared": patch +"@medusajs/dashboard": patch +"@medusajs/js-sdk": patch +--- + +feat(dashboard,js-sdk,admin-shared): add customer addresses + layout change diff --git a/.changeset/violet-trainers-sleep.md b/.changeset/violet-trainers-sleep.md new file mode 100644 index 0000000000..fdbf82d75f --- /dev/null +++ b/.changeset/violet-trainers-sleep.md @@ -0,0 +1,7 @@ +--- +"@medusajs/admin-shared": patch +"@medusajs/dashboard": patch +"@medusajs/js-sdk": patch +--- + +feat(dashboard,js-sdk,admin-shared): add customer addresses + layout change diff --git a/packages/admin/admin-shared/src/extensions/widgets/constants.ts b/packages/admin/admin-shared/src/extensions/widgets/constants.ts index b4a83fc1a9..e45f38458b 100644 --- a/packages/admin/admin-shared/src/extensions/widgets/constants.ts +++ b/packages/admin/admin-shared/src/extensions/widgets/constants.ts @@ -10,6 +10,8 @@ const ORDER_INJECTION_ZONES = [ const CUSTOMER_INJECTION_ZONES = [ "customer.details.before", "customer.details.after", + "customer.details.side.before", + "customer.details.side.after", "customer.list.before", "customer.list.after", ] as const diff --git a/packages/admin/dashboard/src/components/common/empty-table-content/empty-table-content.tsx b/packages/admin/dashboard/src/components/common/empty-table-content/empty-table-content.tsx index 25fab01877..6c824263fd 100644 --- a/packages/admin/dashboard/src/components/common/empty-table-content/empty-table-content.tsx +++ b/packages/admin/dashboard/src/components/common/empty-table-content/empty-table-content.tsx @@ -1,5 +1,6 @@ import { ExclamationCircle, MagnifyingGlass, PlusMini } from "@medusajs/icons" import { Button, Text, clx } from "@medusajs/ui" +import React from "react" import { useTranslation } from "react-i18next" import { Link } from "react-router-dom" @@ -44,6 +45,7 @@ type NoRecordsProps = { message?: string className?: string buttonVariant?: string + icon?: React.ReactNode } & ActionProps const DefaultButton = ({ action }: ActionProps) => @@ -70,18 +72,19 @@ export const NoRecords = ({ action, className, buttonVariant = "default", + icon = , }: NoRecordsProps) => { const { t } = useTranslation() return (
- + {icon}
diff --git a/packages/admin/dashboard/src/components/common/listicle/index.ts b/packages/admin/dashboard/src/components/common/listicle/index.ts new file mode 100644 index 0000000000..49b9a3a3ee --- /dev/null +++ b/packages/admin/dashboard/src/components/common/listicle/index.ts @@ -0,0 +1 @@ +export * from "./listicle" diff --git a/packages/admin/dashboard/src/components/common/listicle/listicle.tsx b/packages/admin/dashboard/src/components/common/listicle/listicle.tsx new file mode 100644 index 0000000000..05b7836d3f --- /dev/null +++ b/packages/admin/dashboard/src/components/common/listicle/listicle.tsx @@ -0,0 +1,34 @@ +import { Text } from "@medusajs/ui" +import { ReactNode } from "react" + +export interface ListicleProps { + labelKey: string + descriptionKey: string + children?: ReactNode +} + +export const Listicle = ({ + labelKey, + descriptionKey, + children, +}: ListicleProps) => { + return ( +
+
+
+
+ + {labelKey} + + + {descriptionKey} + +
+
+ {children} +
+
+
+
+ ) +} diff --git a/packages/admin/dashboard/src/components/common/sidebar-link/sidebar-link.tsx b/packages/admin/dashboard/src/components/common/sidebar-link/sidebar-link.tsx index cb2f9ffba6..5da28dc2ec 100644 --- a/packages/admin/dashboard/src/components/common/sidebar-link/sidebar-link.tsx +++ b/packages/admin/dashboard/src/components/common/sidebar-link/sidebar-link.tsx @@ -1,9 +1,9 @@ import { ReactNode } from "react" import { Link } from "react-router-dom" -import { IconAvatar } from "../icon-avatar" -import { Text } from "@medusajs/ui" import { TriangleRightMini } from "@medusajs/icons" +import { Text } from "@medusajs/ui" +import { IconAvatar } from "../icon-avatar" export interface SidebarLinkProps { to: string diff --git a/packages/admin/dashboard/src/dashboard-app/routes/get-route.map.tsx b/packages/admin/dashboard/src/dashboard-app/routes/get-route.map.tsx index ee3de99151..2c1302c44c 100644 --- a/packages/admin/dashboard/src/dashboard-app/routes/get-route.map.tsx +++ b/packages/admin/dashboard/src/dashboard-app/routes/get-route.map.tsx @@ -665,6 +665,13 @@ export function getRouteMap({ lazy: () => import("../../routes/customers/customer-edit"), }, + { + path: "create-address", + lazy: () => + import( + "../../routes/customers/customer-create-address" + ), + }, { path: "add-customer-groups", lazy: () => diff --git a/packages/admin/dashboard/src/hooks/api/customers.tsx b/packages/admin/dashboard/src/hooks/api/customers.tsx index 5f2ef7b66a..88cd2a482e 100644 --- a/packages/admin/dashboard/src/hooks/api/customers.tsx +++ b/packages/admin/dashboard/src/hooks/api/customers.tsx @@ -14,6 +14,9 @@ import { customerGroupsQueryKeys } from "./customer-groups" const CUSTOMERS_QUERY_KEY = "customers" as const export const customersQueryKeys = queryKeysFactory(CUSTOMERS_QUERY_KEY) +export const customerAddressesQueryKeys = queryKeysFactory( + `${CUSTOMERS_QUERY_KEY}-addresses` +) export const useCustomer = ( id: string, @@ -148,3 +151,113 @@ export const useBatchCustomerCustomerGroups = ( ...options, }) } + +export const useCreateCustomerAddress = ( + id: string, + options?: UseMutationOptions< + HttpTypes.AdminCustomerResponse, + FetchError, + HttpTypes.AdminCreateCustomerAddress + > +) => { + return useMutation({ + mutationFn: (payload) => sdk.admin.customer.createAddress(id, payload), + onSuccess: (data, variables, context) => { + queryClient.invalidateQueries({ queryKey: customersQueryKeys.lists() }) + queryClient.invalidateQueries({ queryKey: customersQueryKeys.detail(id) }) + queryClient.invalidateQueries({ + queryKey: customerAddressesQueryKeys.list(id), + }) + + options?.onSuccess?.(data, variables, context) + }, + ...options, + }) +} + +export const useUpdateCustomerAddress = ( + id: string, + addressId: string, + options?: UseMutationOptions< + HttpTypes.AdminCustomerResponse, + FetchError, + HttpTypes.AdminUpdateCustomerAddress + > +) => { + return useMutation({ + mutationFn: (payload) => + sdk.admin.customer.updateAddress(id, addressId, payload), + onSuccess: (data, variables, context) => { + queryClient.invalidateQueries({ queryKey: customersQueryKeys.lists() }) + queryClient.invalidateQueries({ queryKey: customersQueryKeys.detail(id) }) + queryClient.invalidateQueries({ + queryKey: customerAddressesQueryKeys.list(id), + }) + + options?.onSuccess?.(data, variables, context) + }, + ...options, + }) +} + +export const useDeleteCustomerAddress = ( + id: string, + options?: UseMutationOptions< + HttpTypes.AdminCustomerResponse, + FetchError, + string + > +) => { + return useMutation({ + mutationFn: (addressId: string) => + sdk.admin.customer.deleteAddress(id, addressId), + onSuccess: (data, variables, context) => { + queryClient.invalidateQueries({ queryKey: customersQueryKeys.lists() }) + queryClient.invalidateQueries({ queryKey: customersQueryKeys.detail(id) }) + queryClient.invalidateQueries({ + queryKey: customerAddressesQueryKeys.list(id), + }) + + options?.onSuccess?.(data, variables, context) + }, + ...options, + }) +} + +export const useListCustomerAddresses = ( + id: string, + query?: Record, + options?: UseQueryOptions< + HttpTypes.AdminCustomerResponse, + FetchError, + HttpTypes.AdminCustomerResponse, + QueryKey + > +) => { + const { data, ...rest } = useQuery({ + queryFn: () => sdk.admin.customer.listAddresses(id, query), + queryKey: customerAddressesQueryKeys.list(id), + ...options, + }) + + return { ...data, ...rest } +} + +export const useCustomerAddress = ( + id: string, + addressId: string, + options?: UseQueryOptions< + HttpTypes.AdminCustomerResponse, + FetchError, + HttpTypes.AdminCustomerResponse, + QueryKey + > +) => { + const { data, ...rest } = useQuery({ + queryFn: () => sdk.admin.customer.retrieveAddress(id, addressId), + queryKey: customerAddressesQueryKeys.detail(id), + ...options, + }) + + return { ...data, ...rest } +} diff --git a/packages/admin/dashboard/src/i18n/translations/$schema.json b/packages/admin/dashboard/src/i18n/translations/$schema.json index c5e9498a8c..8747b8ffd8 100644 --- a/packages/admin/dashboard/src/i18n/translations/$schema.json +++ b/packages/admin/dashboard/src/i18n/translations/$schema.json @@ -134,6 +134,9 @@ "areYouSure": { "type": "string" }, + "areYouSureDescription": { + "type": "string" + }, "noRecordsFound": { "type": "string" }, @@ -1346,6 +1349,9 @@ "addresses": { "type": "object", "properties": { + "title": { + "type": "string" + }, "shippingAddress": { "type": "object", "properties": { @@ -3491,6 +3497,84 @@ }, "hasAccount": { "type": "string" + }, + "addresses": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "fields": { + "type": "object", + "properties": { + "addressName": { + "type": "string" + }, + "address1": { + "type": "string" + }, + "address2": { + "type": "string" + }, + "city": { + "type": "string" + }, + "province": { + "type": "string" + }, + "postalCode": { + "type": "string" + }, + "country": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "company": { + "type": "string" + }, + "countryCode": { + "type": "string" + }, + "provinceCode": { + "type": "string" + } + }, + "required": [ + "addressName", + "address1", + "address2", + "city", + "province", + "postalCode", + "country", + "phone", + "company", + "countryCode", + "provinceCode" + ], + "additionalProperties": false + }, + "create": { + "type": "object", + "properties": { + "header": { + "type": "string" + }, + "hint": { + "type": "string" + }, + "successToast": { + "type": "string" + } + }, + "required": ["header", "hint", "successToast"], + "additionalProperties": false + } + }, + "required": ["title", "create"], + "additionalProperties": false } }, "required": [ diff --git a/packages/admin/dashboard/src/i18n/translations/__tests__/validate-translations.spec.ts b/packages/admin/dashboard/src/i18n/translations/__tests__/validate-translations.spec.ts index f2814923f7..85d69f38a9 100644 --- a/packages/admin/dashboard/src/i18n/translations/__tests__/validate-translations.spec.ts +++ b/packages/admin/dashboard/src/i18n/translations/__tests__/validate-translations.spec.ts @@ -56,6 +56,7 @@ describe("translation schema validation", () => { if (missingInTranslations.length > 0) { console.error("\nMissing keys in en.json:", missingInTranslations) } + if (extraInTranslations.length > 0) { console.error("\nExtra keys in en.json:", extraInTranslations) } diff --git a/packages/admin/dashboard/src/i18n/translations/en.json b/packages/admin/dashboard/src/i18n/translations/en.json index 716c5fea25..d4dd541740 100644 --- a/packages/admin/dashboard/src/i18n/translations/en.json +++ b/packages/admin/dashboard/src/i18n/translations/en.json @@ -43,6 +43,7 @@ "plusCount": "+ {{count}}", "plusCountMore": "+ {{count}} more", "areYouSure": "Are you sure?", + "areYouSureDescription": "You are about to delete the {{entity}} {{title}}. This action cannot be undone.", "noRecordsFound": "No records found", "typeToConfirm": "Please type {val} to confirm:", "noResultsTitle": "No results", @@ -349,6 +350,7 @@ "backToDashboard": "Back to dashboard" }, "addresses": { + "title": "Addresses", "shippingAddress": { "header": "Shipping Address", "editHeader": "Edit Shipping Address", @@ -931,7 +933,28 @@ }, "registered": "Registered", "guest": "Guest", - "hasAccount": "Has account" + "hasAccount": "Has account", + "addresses": { + "title": "Addresses", + "fields": { + "addressName": "Address name", + "address1": "Address 1", + "address2": "Address 2", + "city": "City", + "province": "Province", + "postalCode": "Postal code", + "country": "Country", + "phone": "Phone", + "company": "Company", + "countryCode": "Country code", + "provinceCode": "Province code" + }, + "create": { + "header": "Create Address", + "hint": "Create a new address for the customer.", + "successToast": "Address was successfully created." + } + } }, "customerGroups": { "domain": "Customer Groups", diff --git a/packages/admin/dashboard/src/routes/customers/customer-create-address/components/create-customer-address-form/create-customer-address-form.tsx b/packages/admin/dashboard/src/routes/customers/customer-create-address/components/create-customer-address-form/create-customer-address-form.tsx new file mode 100644 index 0000000000..86ce3693a1 --- /dev/null +++ b/packages/admin/dashboard/src/routes/customers/customer-create-address/components/create-customer-address-form/create-customer-address-form.tsx @@ -0,0 +1,282 @@ +import { zodResolver } from "@hookform/resolvers/zod" +import { Button, Heading, Input, Text, toast } from "@medusajs/ui" +import { useForm } from "react-hook-form" +import { useTranslation } from "react-i18next" +import { useParams } from "react-router-dom" +import * as zod from "zod" +import { Form } from "../../../../../components/common/form" +import { CountrySelect } from "../../../../../components/inputs/country-select" +import { + RouteFocusModal, + useRouteModal, +} from "../../../../../components/modals" +import { KeyboundForm } from "../../../../../components/utilities/keybound-form" +import { useCreateCustomerAddress } from "../../../../../hooks/api/customers" + +const CreateCustomerAddressSchema = zod.object({ + address_name: zod.string().min(1), + address_1: zod.string().min(1), + address_2: zod.string().optional(), + country_code: zod.string().min(2).max(2), + city: zod.string().optional(), + postal_code: zod.string().optional(), + province: zod.string().optional(), + company: zod.string().optional(), + phone: zod.string().optional(), +}) + +export const CreateCustomerAddressForm = () => { + const { t } = useTranslation() + const { id } = useParams() + const { handleSuccess } = useRouteModal() + + const form = useForm>({ + defaultValues: { + address_name: "", + address_1: "", + address_2: "", + city: "", + company: "", + country_code: "", + phone: "", + postal_code: "", + province: "", + }, + resolver: zodResolver(CreateCustomerAddressSchema), + }) + + const { mutateAsync, isPending } = useCreateCustomerAddress(id!) + + const handleSubmit = form.handleSubmit(async (values) => { + await mutateAsync( + { + address_name: values.address_name, + address_1: values.address_1, + address_2: values.address_2, + country_code: values.country_code, + city: values.city, + postal_code: values.postal_code, + province: values.province, + company: values.company, + phone: values.phone, + }, + { + onSuccess: () => { + toast.success(t("customers.addresses.create.successToast")) + + handleSuccess(`/customers/${id}`) + }, + onError: (e) => { + toast.error(e.message) + }, + } + ) + }) + + return ( + + + + +
+
+
+ + {t("customers.addresses.create.header")} + + + {t("customers.addresses.create.hint")} + +
+
+ { + return ( + + + {t("customers.addresses.fields.addressName")} + + + + + + + ) + }} + /> +
+
+ { + return ( + + {t("fields.address")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.address2")} + + + + + + ) + }} + /> + { + return ( + + + {t("fields.postalCode")} + + + + + + + ) + }} + /> + { + return ( + + {t("fields.city")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.country")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.state")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.company")} + + + + + + ) + }} + /> + { + return ( + + {t("fields.phone")} + + + + + + ) + }} + /> +
+
+
+
+ +
+ + + + +
+
+
+
+ ) +} diff --git a/packages/admin/dashboard/src/routes/customers/customer-create-address/components/create-customer-address-form/index.ts b/packages/admin/dashboard/src/routes/customers/customer-create-address/components/create-customer-address-form/index.ts new file mode 100644 index 0000000000..d7d23558b4 --- /dev/null +++ b/packages/admin/dashboard/src/routes/customers/customer-create-address/components/create-customer-address-form/index.ts @@ -0,0 +1 @@ +export * from "./create-customer-address-form" diff --git a/packages/admin/dashboard/src/routes/customers/customer-create-address/customer-create-address.tsx b/packages/admin/dashboard/src/routes/customers/customer-create-address/customer-create-address.tsx new file mode 100644 index 0000000000..d8e3f43ce4 --- /dev/null +++ b/packages/admin/dashboard/src/routes/customers/customer-create-address/customer-create-address.tsx @@ -0,0 +1,10 @@ +import { RouteFocusModal } from "../../../components/modals" +import { CreateCustomerAddressForm } from "./components/create-customer-address-form" + +export const CustomerCreateAddress = () => { + return ( + + + + ) +} diff --git a/packages/admin/dashboard/src/routes/customers/customer-create-address/index.ts b/packages/admin/dashboard/src/routes/customers/customer-create-address/index.ts new file mode 100644 index 0000000000..06c21ad3c2 --- /dev/null +++ b/packages/admin/dashboard/src/routes/customers/customer-create-address/index.ts @@ -0,0 +1 @@ +export { CustomerCreateAddress as Component } from "./customer-create-address" diff --git a/packages/admin/dashboard/src/routes/customers/customer-detail/components/customer-address-section/customer-address-section.tsx b/packages/admin/dashboard/src/routes/customers/customer-detail/components/customer-address-section/customer-address-section.tsx new file mode 100644 index 0000000000..ebd6f8e5cf --- /dev/null +++ b/packages/admin/dashboard/src/routes/customers/customer-detail/components/customer-address-section/customer-address-section.tsx @@ -0,0 +1,104 @@ +import { HttpTypes } from "@medusajs/types" +import { clx, Container, Heading, toast, usePrompt } from "@medusajs/ui" +import { useTranslation } from "react-i18next" + +import { Trash } from "@medusajs/icons" +import { Link, useNavigate } from "react-router-dom" +import { ActionMenu } from "../../../../../components/common/action-menu" +import { NoRecords } from "../../../../../components/common/empty-table-content" +import { Listicle } from "../../../../../components/common/listicle" +import { useDeleteCustomerAddress } from "../../../../../hooks/api/customers" + +type CustomerAddressSectionProps = { + customer: HttpTypes.AdminCustomer +} + +export const CustomerAddressSection = ({ + customer, +}: CustomerAddressSectionProps) => { + const { t } = useTranslation() + const prompt = usePrompt() + const navigate = useNavigate() + const { mutateAsync: deleteAddress } = useDeleteCustomerAddress(customer.id) + + const addresses = customer.addresses ?? [] + + const handleDelete = async (address: HttpTypes.AdminCustomerAddress) => { + const confirm = await prompt({ + title: t("general.areYouSure"), + description: t("general.areYouSureDescription", { + entity: t("fields.address"), + title: address.address_name ?? "n/a", + }), + verificationInstruction: t("general.typeToConfirm"), + verificationText: address.address_name ?? "address", + confirmText: t("actions.delete"), + cancelText: t("actions.cancel"), + }) + + if (!confirm) { + return + } + + await deleteAddress(address.id, { + onSuccess: () => { + toast.success( + t("general.success", { name: address.address_name ?? "address" }) + ) + + navigate(`/customers/${customer.id}`, { replace: true }) + }, + onError: (e) => { + toast.error(e.message) + }, + }) + } + + return ( + +
+ {t("addresses.title")} + + Add + +
+ + {addresses.length === 0 && ( + + )} + + {addresses.map((address) => { + return ( + + , + label: t("actions.delete"), + onClick: async () => { + await handleDelete(address) + }, + }, + ], + }, + ]} + /> + + ) + })} +
+ ) +} diff --git a/packages/admin/dashboard/src/routes/customers/customer-detail/components/customer-address-section/index.ts b/packages/admin/dashboard/src/routes/customers/customer-detail/components/customer-address-section/index.ts new file mode 100644 index 0000000000..d3e4870893 --- /dev/null +++ b/packages/admin/dashboard/src/routes/customers/customer-detail/components/customer-address-section/index.ts @@ -0,0 +1 @@ +export * from "./customer-address-section" diff --git a/packages/admin/dashboard/src/routes/customers/customer-detail/customer-detail.tsx b/packages/admin/dashboard/src/routes/customers/customer-detail/customer-detail.tsx index 8c94430ffa..7d48897ab8 100644 --- a/packages/admin/dashboard/src/routes/customers/customer-detail/customer-detail.tsx +++ b/packages/admin/dashboard/src/routes/customers/customer-detail/customer-detail.tsx @@ -1,9 +1,10 @@ import { useLoaderData, useParams } from "react-router-dom" import { SingleColumnPageSkeleton } from "../../../components/common/skeleton" -import { SingleColumnPage } from "../../../components/layout/pages" +import { TwoColumnPage } from "../../../components/layout/pages" import { useCustomer } from "../../../hooks/api/customers" import { useExtension } from "../../../providers/extension-provider" +import { CustomerAddressSection } from "./components/customer-address-section/customer-address-section" import { CustomerGeneralSection } from "./components/customer-general-section" import { CustomerGroupSection } from "./components/customer-group-section" import { CustomerOrderSection } from "./components/customer-order-section" @@ -15,9 +16,11 @@ export const CustomerDetail = () => { const initialData = useLoaderData() as Awaited< ReturnType > - const { customer, isLoading, isError, error } = useCustomer(id!, undefined, { - initialData, - }) + const { customer, isLoading, isError, error } = useCustomer( + id!, + { fields: "+*addresses" }, + { initialData } + ) const { getWidgets } = useExtension() @@ -30,19 +33,26 @@ export const CustomerDetail = () => { } return ( - - - - - + + + + + + + + + ) } diff --git a/packages/admin/dashboard/src/routes/customers/customer-detail/loader.ts b/packages/admin/dashboard/src/routes/customers/customer-detail/loader.ts index eebfaa84f9..301e21403e 100644 --- a/packages/admin/dashboard/src/routes/customers/customer-detail/loader.ts +++ b/packages/admin/dashboard/src/routes/customers/customer-detail/loader.ts @@ -5,7 +5,10 @@ import { queryClient } from "../../../lib/query-client" const customerDetailQuery = (id: string) => ({ queryKey: productsQueryKeys.detail(id), - queryFn: async () => sdk.admin.customer.retrieve(id), + queryFn: async () => + sdk.admin.customer.retrieve(id, { + fields: "+*addresses", + }), }) export const customerLoader = async ({ params }: LoaderFunctionArgs) => { diff --git a/packages/admin/dashboard/src/routes/products/product-list/components/product-list-table/product-list-table.tsx b/packages/admin/dashboard/src/routes/products/product-list/components/product-list-table/product-list-table.tsx index 6cb2ae04bc..533b6763a5 100644 --- a/packages/admin/dashboard/src/routes/products/product-list/components/product-list-table/product-list-table.tsx +++ b/packages/admin/dashboard/src/routes/products/product-list/components/product-list-table/product-list-table.tsx @@ -33,6 +33,7 @@ export const ProductListTable = () => { const { products, count, isLoading, isError, error } = useProducts( { ...searchParams, + is_giftcard: false, }, { initialData, diff --git a/packages/admin/dashboard/src/routes/products/product-list/loader.ts b/packages/admin/dashboard/src/routes/products/product-list/loader.ts index a75c08c9d9..60eb724f92 100644 --- a/packages/admin/dashboard/src/routes/products/product-list/loader.ts +++ b/packages/admin/dashboard/src/routes/products/product-list/loader.ts @@ -6,8 +6,13 @@ import { sdk } from "../../../lib/client" import { queryClient } from "../../../lib/query-client" const productsListQuery = () => ({ - queryKey: productsQueryKeys.list({ limit: 20, offset: 0 }), - queryFn: async () => sdk.admin.product.list({ limit: 20, offset: 0 }), + queryKey: productsQueryKeys.list({ + limit: 20, + offset: 0, + is_giftcard: false, + }), + queryFn: async () => + sdk.admin.product.list({ limit: 20, offset: 0, is_giftcard: false }), }) export const productsLoader = (client: QueryClient) => { diff --git a/packages/core/js-sdk/src/admin/customer.ts b/packages/core/js-sdk/src/admin/customer.ts index 4ce71c078f..76a9729add 100644 --- a/packages/core/js-sdk/src/admin/customer.ts +++ b/packages/core/js-sdk/src/admin/customer.ts @@ -1,7 +1,4 @@ -import { - HttpTypes, - SelectParams, -} from "@medusajs/types" +import { HttpTypes, SelectParams } from "@medusajs/types" import { Client } from "../client" import { ClientHeaders } from "../types" @@ -20,12 +17,12 @@ export class Customer { /** * This method creates a customer. It sends a request to the * [Create Customer](https://docs.medusajs.com/api/admin#customers_postcustomers) API route. - * + * * @param body - The customer's details. * @param query - Configure the fields to retrieve in the customer. * @param headers - Headers to pass in the request. * @returns The customer's details. - * + * * @example * sdk.admin.customer.create({ * email: "customer@gmail.com" @@ -39,26 +36,27 @@ export class Customer { query?: SelectParams, headers?: ClientHeaders ) { - return this.client.fetch< - HttpTypes.AdminCustomerResponse - >(`/admin/customers`, { - method: "POST", - headers, - body, - query, - }) + return this.client.fetch( + `/admin/customers`, + { + method: "POST", + headers, + body, + query, + } + ) } /** * This method updates a customer's details. It sends a request to the * [Update Customer](https://docs.medusajs.com/api/admin#customers_postcustomersid) API route. - * + * * @param id - The customer's ID. * @param body - The details to update of the customer. * @param query - Configure the fields to retrieve in the customer. * @param headers - Headers to pass in the request. * @returns The customer's details. - * + * * @example * sdk.admin.customer.update("cus_123", { * first_name: "John" @@ -88,25 +86,25 @@ export class Customer { * This method retrieves a paginated list of customers. It sends a request to the * [List Customers](https://docs.medusajs.com/api/admin#customers_getcustomers) * API route. - * + * * @param queryParams - Filters and pagination configurations. * @param headers - Headers to pass in the request. * @returns The paginated list of customers. - * + * * @example * To retrieve the list of customers: - * + * * ```ts * sdk.admin.customer.list() * .then(({ customers, count, limit, offset }) => { * console.log(customers) * }) * ``` - * + * * To configure the pagination, pass the `limit` and `offset` query parameters. - * + * * For example, to retrieve only 10 items and skip 10 items: - * + * * ```ts * sdk.admin.customer.list({ * limit: 10, @@ -116,10 +114,10 @@ export class Customer { * console.log(customers) * }) * ``` - * + * * Using the `fields` query parameter, you can specify the fields and relations to retrieve * in each customer: - * + * * ```ts * sdk.admin.customer.list({ * fields: "id,*groups" @@ -128,43 +126,44 @@ export class Customer { * console.log(customers) * }) * ``` - * + * * Learn more about the `fields` property in the [API reference](https://docs.medusajs.com/api/store#select-fields-and-relations). */ async list( queryParams?: HttpTypes.AdminCustomerFilters, headers?: ClientHeaders ) { - return this.client.fetch< - HttpTypes.AdminCustomerListResponse - >(`/admin/customers`, { - headers, - query: queryParams, - }) + return this.client.fetch( + `/admin/customers`, + { + headers, + query: queryParams, + } + ) } /** - * This method retrieves a customer by its ID. It sends a request to the + * This method retrieves a customer by its ID. It sends a request to the * [Get Customer](https://docs.medusajs.com/api/admin#customers_getcustomersid) * API route. - * + * * @param id - The customer's ID. * @param query - Configure the fields to retrieve in the customer. * @param headers - Headers to pass in the request. * @returns The customer's details. - * + * * @example * To retrieve a customer by its ID: - * + * * ```ts * sdk.admin.customer.retrieve("cus_123") * .then(({ customer }) => { * console.log(customer) * }) * ``` - * + * * To specify the fields and relations to retrieve: - * + * * ```ts * sdk.admin.customer.retrieve("cus_123", { * fields: "id,*groups" @@ -173,7 +172,7 @@ export class Customer { * console.log(customer) * }) * ``` - * + * * Learn more about the `fields` property in the [API reference](https://docs.medusajs.com/api/store#select-fields-and-relations). */ async retrieve(id: string, query?: SelectParams, headers?: ClientHeaders) { @@ -187,14 +186,14 @@ export class Customer { } /** - * This method deletes a customer by its ID. It sends a request to the + * This method deletes a customer by its ID. It sends a request to the * [Delete Customer](https://docs.medusajs.com/api/admin#customers_deletecustomersid) * API route. - * + * * @param id - The customer's ID. * @param headers - Headers to pass in the request. * @returns The deletion's details. - * + * * @example * sdk.admin.customer.delete("cus_123") * .then(({ deleted }) => { @@ -244,4 +243,157 @@ export class Customer { } ) } + + /** + * This method creates a customer address. It sends a request to the + * [Create Customer Address](https://docs.medusajs.com/api/admin#customers_postcustomersidaddresses) + * API route. + * + * @param id - The customer's ID. + * @param body - The customer address's details. + * @param headers - Headers to pass in the request. + * @returns The customer address's details. + * + * @example + * sdk.admin.customer.createAddress("cus_123", { + * address_1: "123 Main St", + * city: "Anytown", + * country_code: "US", + * postal_code: "12345" + * }) + * .then(({ customer }) => { + * console.log(customer) + * }) + */ + async createAddress( + id: string, + body: HttpTypes.AdminCreateCustomerAddress, + headers?: ClientHeaders + ) { + return await this.client.fetch( + `/admin/customers/${id}/addresses`, + { + method: "POST", + headers, + body, + } + ) + } + + /** + * This method updates a customer address. It sends a request to the + * [Update Customer Address](https://docs.medusajs.com/api/admin#customers_postcustomersidaddressesaddressid) + * API route. + * + * @param id - The customer's ID. + * @param addressId - The customer address's ID. + * @param body - The customer address's details. + * @param headers - Headers to pass in the request. + * @returns The customer address's details. + * + * @example + * sdk.admin.customer.updateAddress("cus_123", "cus_addr_123", { + * address_1: "123 Main St", + * city: "Anytown", + * country_code: "US", + * postal_code: "12345" + * }) + * .then(({ customer }) => { + * console.log(customer) + * }) + */ + async updateAddress( + id: string, + addressId: string, + body: HttpTypes.AdminUpdateCustomerAddress, + headers?: ClientHeaders + ) { + return await this.client.fetch( + `/admin/customers/${id}/addresses/${addressId}`, + { + method: "POST", + headers, + body, + } + ) + } + + /** + * This method deletes a customer address. It sends a request to the + * [Delete Customer Address](https://docs.medusajs.com/api/admin#customers_deletecustomersidaddressesaddressid) + * API route. + * + * @param id - The customer's ID. + * @param addressId - The customer address's ID. + * @param headers - Headers to pass in the request. + * @returns The customer address's details. + * + * @example + * sdk.admin.customer.deleteAddress("cus_123", "cus_addr_123") + * .then(({ customer }) => { + * console.log(customer) + * }) + */ + async deleteAddress(id: string, addressId: string, headers?: ClientHeaders) { + return await this.client.fetch( + `/admin/customers/${id}/addresses/${addressId}`, + { + method: "DELETE", + headers, + } + ) + } + + /** + * This method retrieves a customer address by its ID. It sends a request to the + * [Get Customer Address](https://docs.medusajs.com/api/admin#customers_getcustomersidaddressesaddressid) + * API route. + * + * @param id - The customer's ID. + * @param addressId - The customer address's ID. + * @param headers - Headers to pass in the request. + * @returns The customer address's details. + * + * @example + * sdk.admin.customer.retrieveAddress("cus_123", "cus_addr_123") + * .then(({ customer }) => { + * console.log(customer) + * }) + */ + async retrieveAddress( + id: string, + addressId: string, + headers?: ClientHeaders + ) { + return await this.client.fetch( + `/admin/customers/${id}/addresses/${addressId}`, + { + headers, + } + ) + } + + /** + * This method retrieves a list of customer addresses. It sends a request to the + * [List Customer Addresses](https://docs.medusajs.com/api/admin#customers_getcustomersidaddresses) + * API route. + * + * @param id - The customer's ID. + * @param headers - Headers to pass in the request. + * @returns The list of customer addresses. + * + * @example + * sdk.admin.customer.listAddresses("cus_123") + * .then(({ addresses }) => { + * console.log(addresses) + * }) + */ + async listAddresses(id: string, headers?: ClientHeaders) { + return await this.client.fetch( + `/admin/customers/${id}/addresses`, + { + headers, + } + ) + } } diff --git a/packages/modules/workflow-engine-inmemory/integration-tests/__tests__/race.spec.ts b/packages/modules/workflow-engine-inmemory/integration-tests/__tests__/race.spec.ts index 2b87bdfab7..ff8b18dd52 100644 --- a/packages/modules/workflow-engine-inmemory/integration-tests/__tests__/race.spec.ts +++ b/packages/modules/workflow-engine-inmemory/integration-tests/__tests__/race.spec.ts @@ -28,7 +28,8 @@ moduleIntegrationTestRunner({ moduleName: Modules.WORKFLOW_ENGINE, resolve: __dirname + "/../..", testSuite: ({ service: workflowOrcModule, medusaApp }) => { - describe("Testing race condition of the workflow during retry", () => { + // TODO: Debug the issue with this test https://github.com/medusajs/medusa/actions/runs/13900190144/job/38897122803#step:5:5616 + describe.skip("Testing race condition of the workflow during retry", () => { it("should prevent race continuation of the workflow during retryIntervalAwaiting in background execution", (done) => { const step0InvokeMock = jest.fn() const step1InvokeMock = jest.fn()