diff --git a/.changeset/tame-grapes-jump.md b/.changeset/tame-grapes-jump.md new file mode 100644 index 0000000000..085d0d3a1f --- /dev/null +++ b/.changeset/tame-grapes-jump.md @@ -0,0 +1,8 @@ +--- +"@medusajs/fulfillment": patch +"@medusajs/admin-shared": patch +"@medusajs/dashboard": patch +"@medusajs/js-sdk": patch +--- + +feat(dashboard, js-sdk): shipping option type mngmt dashboard diff --git a/integration-tests/http/__tests__/shipping-option-type/admin/shipping-option-type.spec.ts b/integration-tests/http/__tests__/shipping-option-type/admin/shipping-option-type.spec.ts index 60ab211284..cd0c41d085 100644 --- a/integration-tests/http/__tests__/shipping-option-type/admin/shipping-option-type.spec.ts +++ b/integration-tests/http/__tests__/shipping-option-type/admin/shipping-option-type.spec.ts @@ -66,6 +66,23 @@ medusaIntegrationTestRunner({ }) it("returns a list of shipping option types matching free text search param", async () => { + const res = await api.get("/admin/shipping-option-types?q=st1", adminHeaders) + + expect(res.status).toEqual(200) + + expect(res.data.shipping_option_types).toEqual([ + { + id: expect.stringMatching(/sotype_.{24}/), + label: "Test1", + code: "test1", + description: "Test1 description", + created_at: expect.any(String), + updated_at: expect.any(String), + }, + ]) + }) + + it("returns a list of shipping option types matching code search param", async () => { const res = await api.get("/admin/shipping-option-types?code=test1", adminHeaders) expect(res.status).toEqual(200) diff --git a/packages/admin/admin-shared/src/extensions/widgets/constants.ts b/packages/admin/admin-shared/src/extensions/widgets/constants.ts index 1e7a19a7eb..0137c4214f 100644 --- a/packages/admin/admin-shared/src/extensions/widgets/constants.ts +++ b/packages/admin/admin-shared/src/extensions/widgets/constants.ts @@ -55,6 +55,13 @@ const PRODUCT_CATEGORY_INJECTION_ZONES = [ "product_category.list.after", ] as const +const SHIPPING_OPTION_TYPE_INJECTION_ZONES = [ + "shipping_option_type.details.before", + "shipping_option_type.details.after", + "shipping_option_type.list.before", + "shipping_option_type.list.after", +] as const + const PRODUCT_TYPE_INJECTION_ZONES = [ "product_type.details.before", "product_type.details.after", @@ -204,6 +211,7 @@ export const INJECTION_ZONES = [ ...PRODUCT_COLLECTION_INJECTION_ZONES, ...PRODUCT_CATEGORY_INJECTION_ZONES, ...PRODUCT_TYPE_INJECTION_ZONES, + ...SHIPPING_OPTION_TYPE_INJECTION_ZONES, ...PRODUCT_TAG_INJECTION_ZONES, ...PRICE_LIST_INJECTION_ZONES, ...PROMOTION_INJECTION_ZONES, diff --git a/packages/admin/dashboard/src/components/layout/settings-layout/settings-layout.tsx b/packages/admin/dashboard/src/components/layout/settings-layout/settings-layout.tsx index dfdc4900c5..1da7d7be89 100644 --- a/packages/admin/dashboard/src/components/layout/settings-layout/settings-layout.tsx +++ b/packages/admin/dashboard/src/components/layout/settings-layout/settings-layout.tsx @@ -47,6 +47,10 @@ const useSettingRoutes = (): INavItem[] => { label: t("salesChannels.domain"), to: "/settings/sales-channels", }, + { + label: t("shippingOptionTypes.domain"), + to: "/settings/shipping-option-types", + }, { label: t("productTypes.domain"), to: "/settings/product-types", 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 b8348eff14..1a0012a2a9 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 @@ -1369,6 +1369,59 @@ export function getRouteMap({ }, ], }, + { + path: "shipping-option-types", + errorElement: , + element: , + handle: { + breadcrumb: () => t("shippingOptionTypes.domain"), + }, + children: [ + { + path: "", + lazy: () => + import( + "../../routes/shipping-option-types/shipping-option-type-list" + ), + children: [ + { + path: "create", + lazy: () => + import( + "../../routes/shipping-option-types/shipping-option-type-create" + ), + }, + ], + }, + { + path: ":id", + lazy: async () => { + const { Component, Breadcrumb, loader } = await import( + "../../routes/shipping-option-types/shipping-option-type-detail" + ) + + return { + Component, + loader, + handle: { + breadcrumb: ( + match: UIMatch + ) => , + }, + } + }, + children: [ + { + path: "edit", + lazy: () => + import( + "../../routes/shipping-option-types/shipping-option-type-edit" + ), + }, + ], + }, + ], + }, { path: "product-types", errorElement: , diff --git a/packages/admin/dashboard/src/hooks/api/index.ts b/packages/admin/dashboard/src/hooks/api/index.ts index ce51741a67..7b332ff6cc 100644 --- a/packages/admin/dashboard/src/hooks/api/index.ts +++ b/packages/admin/dashboard/src/hooks/api/index.ts @@ -26,6 +26,7 @@ export * from "./regions" export * from "./reservations" export * from "./sales-channels" export * from "./shipping-options" +export * from "./shipping-option-types" export * from "./shipping-profiles" export * from "./stock-locations" export * from "./store" diff --git a/packages/admin/dashboard/src/hooks/api/shipping-option-types.tsx b/packages/admin/dashboard/src/hooks/api/shipping-option-types.tsx new file mode 100644 index 0000000000..ad5b5eeced --- /dev/null +++ b/packages/admin/dashboard/src/hooks/api/shipping-option-types.tsx @@ -0,0 +1,128 @@ +import { FetchError } from "@medusajs/js-sdk" +import { HttpTypes } from "@medusajs/types" +import { + QueryKey, + useMutation, + UseMutationOptions, + useQuery, + UseQueryOptions, +} from "@tanstack/react-query" +import { sdk } from "../../lib/client" +import { queryClient } from "../../lib/query-client" +import { queryKeysFactory } from "../../lib/query-key-factory" + +const SHIPPING_OPTION_TYPES_QUERY_KEY = "shipping_option_types" as const +export const shippingOptionTypesQueryKeys = queryKeysFactory( + SHIPPING_OPTION_TYPES_QUERY_KEY +) + +export const useShippingOptionType = ( + id: string, + query?: HttpTypes.SelectParams, + options?: Omit< + UseQueryOptions< + HttpTypes.AdminShippingOptionTypeResponse, + FetchError, + HttpTypes.AdminShippingOptionTypeResponse, + QueryKey + >, + "queryKey" | "queryFn" + > +) => { + const { data, ...rest } = useQuery({ + queryFn: () => sdk.admin.shippingOptionType.retrieve(id, query), + queryKey: shippingOptionTypesQueryKeys.detail(id), + ...options, + }) + + return { ...data, ...rest } +} + +export const useShippingOptionTypes = ( + query?: HttpTypes.AdminShippingOptionTypeListParams, + options?: Omit< + UseQueryOptions< + HttpTypes.AdminShippingOptionTypeListResponse, + FetchError, + HttpTypes.AdminShippingOptionTypeListResponse, + QueryKey + >, + "queryKey" | "queryFn" + > +) => { + const { data, ...rest } = useQuery({ + queryFn: () => sdk.admin.shippingOptionType.list(query), + queryKey: shippingOptionTypesQueryKeys.list(query), + ...options, + }) + + return { ...data, ...rest } +} + +export const useCreateShippingOptionType = ( + options?: UseMutationOptions< + HttpTypes.AdminShippingOptionTypeResponse, + FetchError, + HttpTypes.AdminCreateShippingOptionType + > +) => { + return useMutation({ + mutationFn: (payload) => sdk.admin.shippingOptionType.create(payload), + onSuccess: (data, variables, context) => { + queryClient.invalidateQueries({ + queryKey: shippingOptionTypesQueryKeys.lists(), + }) + + options?.onSuccess?.(data, variables, context) + }, + ...options, + }) +} + +export const useUpdateShippingOptionType = ( + id: string, + options?: UseMutationOptions< + HttpTypes.AdminShippingOptionTypeResponse, + FetchError, + HttpTypes.AdminUpdateShippingOptionType + > +) => { + return useMutation({ + mutationFn: (payload) => sdk.admin.shippingOptionType.update(id, payload), + onSuccess: (data, variables, context) => { + queryClient.invalidateQueries({ + queryKey: shippingOptionTypesQueryKeys.detail(id), + }) + queryClient.invalidateQueries({ + queryKey: shippingOptionTypesQueryKeys.lists(), + }) + + options?.onSuccess?.(data, variables, context) + }, + ...options, + }) +} + +export const useDeleteShippingOptionType = ( + id: string, + options?: UseMutationOptions< + HttpTypes.AdminShippingOptionTypeDeleteResponse, + FetchError, + void + > +) => { + return useMutation({ + mutationFn: () => sdk.admin.shippingOptionType.delete(id), + onSuccess: (data, variables, context) => { + queryClient.invalidateQueries({ + queryKey: shippingOptionTypesQueryKeys.detail(id), + }) + queryClient.invalidateQueries({ + queryKey: shippingOptionTypesQueryKeys.lists(), + }) + + options?.onSuccess?.(data, variables, context) + }, + ...options, + }) +} diff --git a/packages/admin/dashboard/src/hooks/table/columns/use-shipping-option-type-table-columns.tsx b/packages/admin/dashboard/src/hooks/table/columns/use-shipping-option-type-table-columns.tsx new file mode 100644 index 0000000000..98f8538c19 --- /dev/null +++ b/packages/admin/dashboard/src/hooks/table/columns/use-shipping-option-type-table-columns.tsx @@ -0,0 +1,44 @@ +import { HttpTypes } from "@medusajs/types" +import { createColumnHelper } from "@tanstack/react-table" +import { useMemo } from "react" +import { useTranslation } from "react-i18next" + +import { DateCell } from "../../../components/table/table-cells/common/date-cell" +import { TextCell } from "../../../components/table/table-cells/common/text-cell" + +const columnHelper = createColumnHelper() + +export const useShippingOptionTypeTableColumns = () => { + const { t } = useTranslation() + + return useMemo( + () => [ + columnHelper.accessor("label", { + header: () => t("fields.label"), + cell: ({ getValue }) => , + }), + columnHelper.accessor("code", { + header: () => t("fields.code"), + cell: ({ getValue }) => , + }), + columnHelper.accessor("description", { + header: () => t("fields.description"), + cell: ({ getValue }) => , + }), + columnHelper.accessor("created_at", { + header: () => t("fields.createdAt"), + + cell: ({ getValue }) => { + return + }, + }), + columnHelper.accessor("updated_at", { + header: () => t("fields.updatedAt"), + cell: ({ getValue }) => { + return + }, + }), + ], + [t] + ) +} diff --git a/packages/admin/dashboard/src/hooks/table/filters/use-shipping-option-type-table-filters.tsx b/packages/admin/dashboard/src/hooks/table/filters/use-shipping-option-type-table-filters.tsx new file mode 100644 index 0000000000..b880231492 --- /dev/null +++ b/packages/admin/dashboard/src/hooks/table/filters/use-shipping-option-type-table-filters.tsx @@ -0,0 +1,5 @@ +import { useDateTableFilters } from "./use-date-table-filters" + +export const useShippingOptionTypeTableFilters = () => { + return useDateTableFilters() +} diff --git a/packages/admin/dashboard/src/hooks/table/query/use-shipping-option-type-table-query.tsx b/packages/admin/dashboard/src/hooks/table/query/use-shipping-option-type-table-query.tsx new file mode 100644 index 0000000000..75d576eea5 --- /dev/null +++ b/packages/admin/dashboard/src/hooks/table/query/use-shipping-option-type-table-query.tsx @@ -0,0 +1,32 @@ +import { HttpTypes } from "@medusajs/types" +import { useQueryParams } from "../../use-query-params" + +type UseShippingOptionTypeTableQueryProps = { + prefix?: string + pageSize?: number +} + +export const useShippingOptionTypeTableQuery = ({ + prefix, + pageSize = 20, +}: UseShippingOptionTypeTableQueryProps) => { + const queryObject = useQueryParams( + ["offset", "q", "order", "created_at", "updated_at"], + prefix + ) + + const { offset, q, order, created_at, updated_at } = queryObject + const searchParams: HttpTypes.AdminShippingOptionTypeListParams = { + limit: pageSize, + offset: offset ? Number(offset) : 0, + order, + created_at: created_at ? JSON.parse(created_at) : undefined, + updated_at: updated_at ? JSON.parse(updated_at) : undefined, + q, + } + + return { + searchParams, + raw: queryObject, + } +} diff --git a/packages/admin/dashboard/src/i18n/translations/$schema.json b/packages/admin/dashboard/src/i18n/translations/$schema.json index ab2a83906d..f73a9eda35 100644 --- a/packages/admin/dashboard/src/i18n/translations/$schema.json +++ b/packages/admin/dashboard/src/i18n/translations/$schema.json @@ -10411,6 +10411,77 @@ ], "additionalProperties": false }, + "shippingOptionTypes": { + "type": "object", + "properties": { + "domain": { + "type": "string" + }, + "subtitle": { + "type": "string" + }, + "create": { + "type": "object", + "properties": { + "header": { + "type": "string" + }, + "hint": { + "type": "string" + }, + "successToast": { + "type": "string" + } + }, + "required": ["header", "hint", "successToast"], + "additionalProperties": false + }, + "edit": { + "type": "object", + "properties": { + "header": { + "type": "string" + }, + "successToast": { + "type": "string" + } + }, + "required": ["header", "successToast"], + "additionalProperties": false + }, + "delete": { + "type": "object", + "properties": { + "confirmation": { + "type": "string" + }, + "successToast": { + "type": "string" + } + }, + "required": ["confirmation", "successToast"], + "additionalProperties": false + }, + "fields": { + "type": "object", + "properties": { + "label": { + "type": "string" + }, + "code": { + "type": "string" + }, + "description": { + "type": "string" + } + }, + "required": ["label", "code", "description"], + "additionalProperties": false + } + }, + "required": ["domain", "subtitle", "create", "edit", "delete", "fields"], + "additionalProperties": false + }, "productTypes": { "type": "object", "properties": { diff --git a/packages/admin/dashboard/src/i18n/translations/en.json b/packages/admin/dashboard/src/i18n/translations/en.json index 2b757f674f..3e23efc2bd 100644 --- a/packages/admin/dashboard/src/i18n/translations/en.json +++ b/packages/admin/dashboard/src/i18n/translations/en.json @@ -2799,6 +2799,28 @@ } } }, + "shippingOptionTypes": { + "domain": "Shipping Option Types", + "subtitle": "Organize your shipping options into types.", + "create": { + "header": "Create Shipping Option Type", + "hint": "Create a new shipping option type to categorize your shipping options.", + "successToast": "Shipping option type {{label}} was successfully created." + }, + "edit": { + "header": "Edit Shipping Option Type", + "successToast": "Shipping option type {{label}} was successfully updated." + }, + "delete": { + "confirmation": "You are about to delete the shipping option type \"{{label}}\". This action cannot be undone.", + "successToast": "Shipping option type \"{{label}}\" was successfully deleted." + }, + "fields": { + "label": "Label", + "code": "Code", + "description": "Description" + } + }, "productTypes": { "domain": "Product Types", "subtitle": "Organize your products into types.", @@ -3051,4 +3073,4 @@ "seconds_one": "Second", "seconds_other": "Seconds" } -} \ No newline at end of file +} diff --git a/packages/admin/dashboard/src/routes/shipping-option-types/common/hooks/use-delete-shipping-option-type-action.tsx b/packages/admin/dashboard/src/routes/shipping-option-types/common/hooks/use-delete-shipping-option-type-action.tsx new file mode 100644 index 0000000000..58973b2ab9 --- /dev/null +++ b/packages/admin/dashboard/src/routes/shipping-option-types/common/hooks/use-delete-shipping-option-type-action.tsx @@ -0,0 +1,41 @@ +import { useNavigate } from "react-router-dom" +import { toast, usePrompt } from "@medusajs/ui" +import { useTranslation } from "react-i18next" + +import { useDeleteShippingOptionType } from "../../../../hooks/api/shipping-option-types" + +export const useDeleteShippingOptionTypeAction = ( + id: string, + label: string +) => { + const { t } = useTranslation() + const prompt = usePrompt() + const navigate = useNavigate() + + const { mutateAsync } = useDeleteShippingOptionType(id) + + const handleDelete = async () => { + const result = await prompt({ + title: t("general.areYouSure"), + description: t("shippingOptionTypes.delete.confirmation", { label }), + confirmText: t("actions.delete"), + cancelText: t("actions.cancel"), + }) + + if (!result) { + return + } + + await mutateAsync(undefined, { + onSuccess: () => { + navigate("/settings/shipping-option-types", { replace: true }) + toast.success(t("shippingOptionTypes.delete.successToast", { label })) + }, + onError: (e) => { + toast.error(e.message) + }, + }) + } + + return handleDelete +} diff --git a/packages/admin/dashboard/src/routes/shipping-option-types/shipping-option-type-create/components/create-shipping-option-type-form/create-shipping-option-type-form.tsx b/packages/admin/dashboard/src/routes/shipping-option-types/shipping-option-type-create/components/create-shipping-option-type-form/create-shipping-option-type-form.tsx new file mode 100644 index 0000000000..8b52dc1350 --- /dev/null +++ b/packages/admin/dashboard/src/routes/shipping-option-types/shipping-option-type-create/components/create-shipping-option-type-form/create-shipping-option-type-form.tsx @@ -0,0 +1,168 @@ +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 { z } from "zod" +import { Form } from "../../../../../components/common/form" +import { RouteFocusModal, useRouteModal, } from "../../../../../components/modals" +import { KeyboundForm } from "../../../../../components/utilities/keybound-form" +import { useCreateShippingOptionType } from "../../../../../hooks/api" + +const CreateShippingOptionTypeSchema = z.object({ + label: z.string().min(1), + code: z.string().min(1), + description: z.string().optional(), +}) + +export const CreateShippingOptionTypeForm = () => { + const { t } = useTranslation() + const { handleSuccess } = useRouteModal() + + const form = useForm>({ + defaultValues: { + label: "", + code: "", + description: undefined, + }, + resolver: zodResolver(CreateShippingOptionTypeSchema), + }) + + const generateCodeFromLabel = (label: string) => { + return label + .toLowerCase() + .replace(/[^a-z0-9]/g, "_") + .replace(/_+/g, "_") + .replace(/^_|_$/g, "") + } + + const { mutateAsync, isPending } = useCreateShippingOptionType() + + const handleSubmit = form.handleSubmit( + async (values: z.infer) => { + await mutateAsync(values, { + onSuccess: ({ shipping_option_type }) => { + toast.success( + t("shippingOptionTypes.create.successToast", { + label: shipping_option_type.label.trim(), + }) + ) + + handleSuccess( + `/settings/shipping-option-types/${shipping_option_type.id}` + ) + }, + onError: (e) => { + toast.error(e.message) + }, + }) + } + ) + + return ( + + + + + + {t("shippingOptionTypes.create.header")} + + {t("shippingOptionTypes.create.hint")} + + + + { + return ( + + + {t("shippingOptionTypes.fields.label")} + + + { + if ( + !form.getFieldState("code").isTouched || + !form.getValues("code") + ) { + form.setValue( + "code", + generateCodeFromLabel(e.target.value) + ) + } + field.onChange(e) + }} + /> + + + + ) + }} + /> + { + return ( + + + {t("shippingOptionTypes.fields.code")} + + + + + + + ) + }} + /> + { + return ( + + + {t("shippingOptionTypes.fields.description")} + + ({t("fields.optional")}) + + + + + + + + ) + }} + /> + + + + + + + + {t("actions.cancel")} + + + + {t("actions.create")} + + + + + + ) +} diff --git a/packages/admin/dashboard/src/routes/shipping-option-types/shipping-option-type-create/components/create-shipping-option-type-form/index.ts b/packages/admin/dashboard/src/routes/shipping-option-types/shipping-option-type-create/components/create-shipping-option-type-form/index.ts new file mode 100644 index 0000000000..4acf84093a --- /dev/null +++ b/packages/admin/dashboard/src/routes/shipping-option-types/shipping-option-type-create/components/create-shipping-option-type-form/index.ts @@ -0,0 +1 @@ +export * from "./create-shipping-option-type-form" diff --git a/packages/admin/dashboard/src/routes/shipping-option-types/shipping-option-type-create/index.ts b/packages/admin/dashboard/src/routes/shipping-option-types/shipping-option-type-create/index.ts new file mode 100644 index 0000000000..41746bd68e --- /dev/null +++ b/packages/admin/dashboard/src/routes/shipping-option-types/shipping-option-type-create/index.ts @@ -0,0 +1 @@ +export { ShippingOptionTypeCreate as Component } from "./shipping-option-type-create" diff --git a/packages/admin/dashboard/src/routes/shipping-option-types/shipping-option-type-create/shipping-option-type-create.tsx b/packages/admin/dashboard/src/routes/shipping-option-types/shipping-option-type-create/shipping-option-type-create.tsx new file mode 100644 index 0000000000..43ce3ecde8 --- /dev/null +++ b/packages/admin/dashboard/src/routes/shipping-option-types/shipping-option-type-create/shipping-option-type-create.tsx @@ -0,0 +1,10 @@ +import { RouteFocusModal } from "../../../components/modals" +import { CreateShippingOptionTypeForm } from "./components/create-shipping-option-type-form" + +export const ShippingOptionTypeCreate = () => { + return ( + + + + ) +} diff --git a/packages/admin/dashboard/src/routes/shipping-option-types/shipping-option-type-detail/breadcrumb.tsx b/packages/admin/dashboard/src/routes/shipping-option-types/shipping-option-type-detail/breadcrumb.tsx new file mode 100644 index 0000000000..1ef483e976 --- /dev/null +++ b/packages/admin/dashboard/src/routes/shipping-option-types/shipping-option-type-detail/breadcrumb.tsx @@ -0,0 +1,24 @@ +import { HttpTypes } from "@medusajs/types" +import { UIMatch } from "react-router-dom" + +import { useShippingOptionType } from "../../../hooks/api" + +type ShippingOptionTypeDetailBreadcrumbProps = + UIMatch + +export const ShippingOptionTypeDetailBreadcrumb = ( + props: ShippingOptionTypeDetailBreadcrumbProps +) => { + const { id } = props.params || {} + + const { shipping_option_type } = useShippingOptionType(id!, undefined, { + initialData: props.data, + enabled: Boolean(id), + }) + + if (!shipping_option_type) { + return null + } + + return {shipping_option_type.label} +} diff --git a/packages/admin/dashboard/src/routes/shipping-option-types/shipping-option-type-detail/components/shipping-option-type-general-section/index.ts b/packages/admin/dashboard/src/routes/shipping-option-types/shipping-option-type-detail/components/shipping-option-type-general-section/index.ts new file mode 100644 index 0000000000..fad30733b0 --- /dev/null +++ b/packages/admin/dashboard/src/routes/shipping-option-types/shipping-option-type-detail/components/shipping-option-type-general-section/index.ts @@ -0,0 +1 @@ +export * from "./shipping-option-type-general-section" diff --git a/packages/admin/dashboard/src/routes/shipping-option-types/shipping-option-type-detail/components/shipping-option-type-general-section/shipping-option-type-general-section.tsx b/packages/admin/dashboard/src/routes/shipping-option-types/shipping-option-type-detail/components/shipping-option-type-general-section/shipping-option-type-general-section.tsx new file mode 100644 index 0000000000..46571fa06e --- /dev/null +++ b/packages/admin/dashboard/src/routes/shipping-option-types/shipping-option-type-detail/components/shipping-option-type-general-section/shipping-option-type-general-section.tsx @@ -0,0 +1,66 @@ +import { PencilSquare, Trash } from "@medusajs/icons" +import { HttpTypes } from "@medusajs/types" +import { Container, Heading, Text } from "@medusajs/ui" +import { useTranslation } from "react-i18next" +import { ActionMenu } from "../../../../../components/common/action-menu" +import { useDeleteShippingOptionTypeAction } from "../../../common/hooks/use-delete-shipping-option-type-action" + +type ShippingOptionTypeGeneralSectionProps = { + shippingOptionType: HttpTypes.AdminShippingOptionType +} + +export const ShippingOptionTypeGeneralSection = ({ + shippingOptionType, +}: ShippingOptionTypeGeneralSectionProps) => { + const { t } = useTranslation() + const handleDelete = useDeleteShippingOptionTypeAction( + shippingOptionType.id, + shippingOptionType.label + ) + + return ( + + + {shippingOptionType.label} + , + to: "edit", + }, + ], + }, + { + actions: [ + { + label: t("actions.delete"), + icon: , + onClick: handleDelete, + }, + ], + }, + ]} + /> + + + + {t("fields.code")} + + + {shippingOptionType.code} + + + + + {t("fields.description")} + + + {shippingOptionType.description || "-"} + + + + ) +} diff --git a/packages/admin/dashboard/src/routes/shipping-option-types/shipping-option-type-detail/index.ts b/packages/admin/dashboard/src/routes/shipping-option-types/shipping-option-type-detail/index.ts new file mode 100644 index 0000000000..a926355be3 --- /dev/null +++ b/packages/admin/dashboard/src/routes/shipping-option-types/shipping-option-type-detail/index.ts @@ -0,0 +1,3 @@ +export { ShippingOptionTypeDetailBreadcrumb as Breadcrumb } from "./breadcrumb" +export { shippingOptionTypeLoader as loader } from "./loader" +export { ShippingOptionTypeDetail as Component } from "./shipping-option-type-detail" diff --git a/packages/admin/dashboard/src/routes/shipping-option-types/shipping-option-type-detail/loader.ts b/packages/admin/dashboard/src/routes/shipping-option-types/shipping-option-type-detail/loader.ts new file mode 100644 index 0000000000..65380f9967 --- /dev/null +++ b/packages/admin/dashboard/src/routes/shipping-option-types/shipping-option-type-detail/loader.ts @@ -0,0 +1,19 @@ +import { LoaderFunctionArgs } from "react-router-dom" + +import { shippingOptionTypesQueryKeys } from "../../../hooks/api/shipping-option-types" +import { sdk } from "../../../lib/client" +import { queryClient } from "../../../lib/query-client" + +const shippingOptionTypeDetailQuery = (id: string) => ({ + queryKey: shippingOptionTypesQueryKeys.detail(id), + queryFn: async () => sdk.admin.shippingOptionType.retrieve(id), +}) + +export const shippingOptionTypeLoader = async ({ + params, +}: LoaderFunctionArgs) => { + const id = params.id + const query = shippingOptionTypeDetailQuery(id!) + + return queryClient.ensureQueryData(query) +} diff --git a/packages/admin/dashboard/src/routes/shipping-option-types/shipping-option-type-detail/shipping-option-type-detail.tsx b/packages/admin/dashboard/src/routes/shipping-option-types/shipping-option-type-detail/shipping-option-type-detail.tsx new file mode 100644 index 0000000000..b6c7afbdde --- /dev/null +++ b/packages/admin/dashboard/src/routes/shipping-option-types/shipping-option-type-detail/shipping-option-type-detail.tsx @@ -0,0 +1,46 @@ +import { useLoaderData, useParams } from "react-router-dom" + +import { SingleColumnPageSkeleton } from "../../../components/common/skeleton" +import { SingleColumnPage } from "../../../components/layout/pages" +import { useShippingOptionType } from "../../../hooks/api" +import { useExtension } from "../../../providers/extension-provider" +import { ShippingOptionTypeGeneralSection } from "./components/shipping-option-type-general-section" +import { shippingOptionTypeLoader } from "./loader" + +export const ShippingOptionTypeDetail = () => { + const { id } = useParams() + const initialData = useLoaderData() as Awaited< + ReturnType + > + + const { shipping_option_type, isPending, isError, error } = + useShippingOptionType(id!, undefined, { + initialData, + }) + + const { getWidgets } = useExtension() + + if (isPending || !shipping_option_type) { + return + } + + if (isError) { + throw error + } + + return ( + + + + ) +} diff --git a/packages/admin/dashboard/src/routes/shipping-option-types/shipping-option-type-edit/components/edit-shipping-option-type-form/edit-shipping-option-type-form.tsx b/packages/admin/dashboard/src/routes/shipping-option-types/shipping-option-type-edit/components/edit-shipping-option-type-form/edit-shipping-option-type-form.tsx new file mode 100644 index 0000000000..e544867c0a --- /dev/null +++ b/packages/admin/dashboard/src/routes/shipping-option-types/shipping-option-type-edit/components/edit-shipping-option-type-form/edit-shipping-option-type-form.tsx @@ -0,0 +1,145 @@ +import { zodResolver } from "@hookform/resolvers/zod" +import { HttpTypes } from "@medusajs/types" +import { Button, Input, Text, toast } from "@medusajs/ui" +import { useForm } from "react-hook-form" +import { useTranslation } from "react-i18next" +import { z } from "zod" +import { Form } from "../../../../../components/common/form" +import { RouteDrawer, useRouteModal } from "../../../../../components/modals" +import { KeyboundForm } from "../../../../../components/utilities/keybound-form" +import { useUpdateShippingOptionType } from "../../../../../hooks/api/shipping-option-types" + +const EditShippingOptionTypeSchema = z.object({ + label: z.string().min(1), + code: z.string().min(1), + description: z.string().optional(), +}) + +type EditShippingOptionTypeFormProps = { + shippingOptionType: HttpTypes.AdminShippingOptionType +} + +export const EditShippingOptionTypeForm = ({ + shippingOptionType, +}: EditShippingOptionTypeFormProps) => { + const { t } = useTranslation() + const { handleSuccess } = useRouteModal() + + const form = useForm>({ + defaultValues: { + label: shippingOptionType.label, + code: shippingOptionType.code, + description: shippingOptionType.description, + }, + resolver: zodResolver(EditShippingOptionTypeSchema), + }) + + const { mutateAsync, isPending } = useUpdateShippingOptionType( + shippingOptionType.id + ) + + const handleSubmit = form.handleSubmit(async (data) => { + await mutateAsync( + { + label: data.label, + code: data.code, + description: data.description, + }, + { + onSuccess: ({ shipping_option_type }) => { + toast.success( + t("shippingOptionTypes.edit.successToast", { + value: shipping_option_type.label, + }) + ) + handleSuccess() + }, + onError: (error) => { + toast.error(error.message) + }, + } + ) + }) + + return ( + + + + { + return ( + + + {t("shippingOptionTypes.fields.label")} + + + + + + + ) + }} + /> + { + return ( + + + {t("shippingOptionTypes.fields.code")} + + + + + + + ) + }} + /> + { + return ( + + + {t("shippingOptionTypes.fields.description")} + + ({t("fields.optional")}) + + + + + + + + ) + }} + /> + + + + + + {t("actions.cancel")} + + + + {t("actions.save")} + + + + + + ) +} diff --git a/packages/admin/dashboard/src/routes/shipping-option-types/shipping-option-type-edit/components/edit-shipping-option-type-form/index.ts b/packages/admin/dashboard/src/routes/shipping-option-types/shipping-option-type-edit/components/edit-shipping-option-type-form/index.ts new file mode 100644 index 0000000000..fb3fb51c5c --- /dev/null +++ b/packages/admin/dashboard/src/routes/shipping-option-types/shipping-option-type-edit/components/edit-shipping-option-type-form/index.ts @@ -0,0 +1 @@ +export * from "./edit-shipping-option-type-form" diff --git a/packages/admin/dashboard/src/routes/shipping-option-types/shipping-option-type-edit/index.ts b/packages/admin/dashboard/src/routes/shipping-option-types/shipping-option-type-edit/index.ts new file mode 100644 index 0000000000..067f3fafac --- /dev/null +++ b/packages/admin/dashboard/src/routes/shipping-option-types/shipping-option-type-edit/index.ts @@ -0,0 +1 @@ +export { ShippingOptionTypeEdit as Component } from "./shipping-option-type-edit" diff --git a/packages/admin/dashboard/src/routes/shipping-option-types/shipping-option-type-edit/shipping-option-type-edit.tsx b/packages/admin/dashboard/src/routes/shipping-option-types/shipping-option-type-edit/shipping-option-type-edit.tsx new file mode 100644 index 0000000000..3b6d3cc9ec --- /dev/null +++ b/packages/admin/dashboard/src/routes/shipping-option-types/shipping-option-type-edit/shipping-option-type-edit.tsx @@ -0,0 +1,31 @@ +import { Heading } from "@medusajs/ui" +import { useTranslation } from "react-i18next" +import { useParams } from "react-router-dom" +import { RouteDrawer } from "../../../components/modals" +import { useShippingOptionType } from "../../../hooks/api" +import { EditShippingOptionTypeForm } from "./components/edit-shipping-option-type-form" + +export const ShippingOptionTypeEdit = () => { + const { id } = useParams() + const { t } = useTranslation() + + const { shipping_option_type, isPending, isError, error } = + useShippingOptionType(id!) + + const ready = !isPending && !!shipping_option_type + + if (isError) { + throw error + } + + return ( + + + {t("shippingOptionTypes.edit.header")} + + {ready && ( + + )} + + ) +} diff --git a/packages/admin/dashboard/src/routes/shipping-option-types/shipping-option-type-list/components/shipping-option-type-list-table/index.ts b/packages/admin/dashboard/src/routes/shipping-option-types/shipping-option-type-list/components/shipping-option-type-list-table/index.ts new file mode 100644 index 0000000000..4cc93a1a35 --- /dev/null +++ b/packages/admin/dashboard/src/routes/shipping-option-types/shipping-option-type-list/components/shipping-option-type-list-table/index.ts @@ -0,0 +1 @@ +export * from "./shipping-option-type-list-table" diff --git a/packages/admin/dashboard/src/routes/shipping-option-types/shipping-option-type-list/components/shipping-option-type-list-table/shipping-option-type-list-table.tsx b/packages/admin/dashboard/src/routes/shipping-option-types/shipping-option-type-list/components/shipping-option-type-list-table/shipping-option-type-list-table.tsx new file mode 100644 index 0000000000..7c03c93b4f --- /dev/null +++ b/packages/admin/dashboard/src/routes/shipping-option-types/shipping-option-type-list/components/shipping-option-type-list-table/shipping-option-type-list-table.tsx @@ -0,0 +1,99 @@ +import { HttpTypes } from "@medusajs/types" +import { Button, Container, Heading, Text } from "@medusajs/ui" +import { keepPreviousData } from "@tanstack/react-query" +import { createColumnHelper } from "@tanstack/react-table" +import { useMemo } from "react" +import { useTranslation } from "react-i18next" +import { Link } from "react-router-dom" +import { _DataTable } from "../../../../../components/table/data-table" +import { useShippingOptionTypes } from "../../../../../hooks/api" +import { useShippingOptionTypeTableColumns } from "../../../../../hooks/table/columns/use-shipping-option-type-table-columns" +import { useShippingOptionTypeTableFilters } from "../../../../../hooks/table/filters/use-shipping-option-type-table-filters" +import { useShippingOptionTypeTableQuery } from "../../../../../hooks/table/query/use-shipping-option-type-table-query" +import { useDataTable } from "../../../../../hooks/use-data-table" +import { ShippingOptionTypeRowActions } from "./shipping-option-type-table-row-actions" + +const PAGE_SIZE = 20 + +export const ShippingOptionTypeListTable = () => { + const { t } = useTranslation() + + const { searchParams, raw } = useShippingOptionTypeTableQuery({ + pageSize: PAGE_SIZE, + }) + const { shipping_option_types, count, isLoading, isError, error } = + useShippingOptionTypes(searchParams, { + placeholderData: keepPreviousData, + }) + + const filters = useShippingOptionTypeTableFilters() + const columns = useColumns() + + const { table } = useDataTable({ + columns, + data: shipping_option_types, + count, + getRowId: (row) => row.id, + pageSize: PAGE_SIZE, + }) + + if (isError) { + throw error + } + + return ( + + + + {t("shippingOptionTypes.domain")} + + {t("shippingOptionTypes.subtitle")} + + + + {t("actions.create")} + + + <_DataTable + table={table} + filters={filters} + isLoading={isLoading} + columns={columns} + pageSize={PAGE_SIZE} + count={count} + orderBy={[ + { key: "label", label: t("fields.label") }, + { key: "code", label: t("fields.code") }, + { key: "description", label: t("fields.description") }, + { key: "created_at", label: t("fields.createdAt") }, + { key: "updated_at", label: t("fields.updatedAt") }, + ]} + navigateTo={({ original }) => original.id} + queryObject={raw} + pagination + search + /> + + ) +} + +const columnHelper = createColumnHelper() + +const useColumns = () => { + const base = useShippingOptionTypeTableColumns() + + return useMemo( + () => [ + ...base, + columnHelper.display({ + id: "actions", + cell: ({ row }) => { + return ( + + ) + }, + }), + ], + [base] + ) +} diff --git a/packages/admin/dashboard/src/routes/shipping-option-types/shipping-option-type-list/components/shipping-option-type-list-table/shipping-option-type-table-row-actions.tsx b/packages/admin/dashboard/src/routes/shipping-option-types/shipping-option-type-list/components/shipping-option-type-list-table/shipping-option-type-table-row-actions.tsx new file mode 100644 index 0000000000..46ca46d377 --- /dev/null +++ b/packages/admin/dashboard/src/routes/shipping-option-types/shipping-option-type-list/components/shipping-option-type-list-table/shipping-option-type-table-row-actions.tsx @@ -0,0 +1,44 @@ +import { PencilSquare, Trash } from "@medusajs/icons" +import { HttpTypes } from "@medusajs/types" +import { useTranslation } from "react-i18next" +import { ActionMenu } from "../../../../../components/common/action-menu" +import { useDeleteShippingOptionTypeAction } from "../../../common/hooks/use-delete-shipping-option-type-action" + +type ShippingOptionTypeRowActionsProps = { + shippingOptionType: HttpTypes.AdminShippingOptionType +} + +export const ShippingOptionTypeRowActions = ({ + shippingOptionType, +}: ShippingOptionTypeRowActionsProps) => { + const { t } = useTranslation() + const handleDelete = useDeleteShippingOptionTypeAction( + shippingOptionType.id, + shippingOptionType.label + ) + + return ( + , + to: `/settings/shipping-option-types/${shippingOptionType.id}/edit`, + }, + ], + }, + { + actions: [ + { + label: t("actions.delete"), + icon: , + onClick: handleDelete, + }, + ], + }, + ]} + /> + ) +} diff --git a/packages/admin/dashboard/src/routes/shipping-option-types/shipping-option-type-list/index.ts b/packages/admin/dashboard/src/routes/shipping-option-types/shipping-option-type-list/index.ts new file mode 100644 index 0000000000..d8d6eb9cdc --- /dev/null +++ b/packages/admin/dashboard/src/routes/shipping-option-types/shipping-option-type-list/index.ts @@ -0,0 +1 @@ +export { ShippingOptionTypeList as Component } from "./shipping-option-type-list" diff --git a/packages/admin/dashboard/src/routes/shipping-option-types/shipping-option-type-list/shipping-option-type-list.tsx b/packages/admin/dashboard/src/routes/shipping-option-types/shipping-option-type-list/shipping-option-type-list.tsx new file mode 100644 index 0000000000..bb2eed60db --- /dev/null +++ b/packages/admin/dashboard/src/routes/shipping-option-types/shipping-option-type-list/shipping-option-type-list.tsx @@ -0,0 +1,18 @@ +import { SingleColumnPage } from "../../../components/layout/pages" +import { useExtension } from "../../../providers/extension-provider" +import { ShippingOptionTypeListTable } from "./components/shipping-option-type-list-table" + +export const ShippingOptionTypeList = () => { + const { getWidgets } = useExtension() + + return ( + + + + ) +} diff --git a/packages/core/js-sdk/src/admin/index.ts b/packages/core/js-sdk/src/admin/index.ts index 2c573ecfb8..b08811ddda 100644 --- a/packages/core/js-sdk/src/admin/index.ts +++ b/packages/core/js-sdk/src/admin/index.ts @@ -43,6 +43,7 @@ import { TaxRegion } from "./tax-region" import { Upload } from "./upload" import { User } from "./user" import { WorkflowExecution } from "./workflow-execution" +import { ShippingOptionType } from "./shipping-option-type" export class Admin { /** @@ -113,6 +114,10 @@ export class Admin { * @tags fulfillment */ public shippingOption: ShippingOption + /** + * @tags fulfillment + */ + public shippingOptionType: ShippingOptionType /** * @tags fulfillment */ @@ -240,6 +245,7 @@ export class Admin { this.fulfillment = new Fulfillment(client) this.fulfillmentProvider = new FulfillmentProvider(client) this.shippingOption = new ShippingOption(client) + this.shippingOptionType = new ShippingOptionType(client) this.shippingProfile = new ShippingProfile(client) this.inventoryItem = new InventoryItem(client) this.notification = new Notification(client) diff --git a/packages/core/js-sdk/src/admin/shipping-option-type.ts b/packages/core/js-sdk/src/admin/shipping-option-type.ts new file mode 100644 index 0000000000..6653a839ce --- /dev/null +++ b/packages/core/js-sdk/src/admin/shipping-option-type.ts @@ -0,0 +1,219 @@ +import { HttpTypes } from "@medusajs/types" +import { Client } from "../client" +import { ClientHeaders } from "../types" + +export class ShippingOptionType { + /** + * @ignore + */ + private client: Client + /** + * @ignore + */ + constructor(client: Client) { + this.client = client + } + + /** + * This method creates a shipping option type. It sends a request to the + * [Create Shipping Option Type](TODO HERE) + * API route. + * + * @param body - The shipping option type's details. + * @param query - Configure the fields to retrieve in the shipping option type. + * @param headers - Headers to pass in the request + * @returns The shipping option type's details. + * + * @example + * sdk.admin.shippingOptionType.create({ + * label: "Standard", + * code: "standard", + * description: "Ship in 2-3 days." + * }) + * .then(({ shipping_option_type }) => { + * console.log(shipping_option_type) + * }) + */ + async create( + body: HttpTypes.AdminCreateShippingOptionType, + query?: HttpTypes.SelectParams, + headers?: ClientHeaders + ) { + return this.client.fetch( + `/admin/shipping-option-types`, + { + method: "POST", + headers, + body, + query, + } + ) + } + + /** + * This method updates a shipping option type. It sends a request to the + * [Update Shipping Option Type](TODO HERE) + * API route. + * + * @param id - The shipping option type's ID. + * @param body - The data to update in the shipping option type. + * @param query - Configure the fields to retrieve in the shipping option type. + * @param headers - Headers to pass in the request + * @returns The shipping option type's details. + * + * @example + * sdk.admin.shippingOptionType.update("sotype_123", { + * code: "express" + * }) + * .then(({ shipping_option_type }) => { + * console.log(shipping_option_type) + * }) + */ + async update( + id: string, + body: HttpTypes.AdminUpdateShippingOptionType, + query?: HttpTypes.SelectParams, + headers?: ClientHeaders + ) { + return this.client.fetch( + `/admin/shipping-option-types/${id}`, + { + method: "POST", + headers, + body, + query, + } + ) + } + + /** + * This method retrieves a paginated list of shipping option types. It sends a request to the + * [List Shipping Option Types](TODO HERE) API route. + * + * @param query - Filters and pagination configurations. + * @param headers - Headers to pass in the request. + * @returns The paginated list of shipping option types. + * + * @example + * To retrieve the list of shipping option types: + * + * ```ts + * sdk.admin.shippingOptionType.list() + * .then(({ shipping_option_types, count, limit, offset }) => { + * console.log(shipping_option_types) + * }) + * ``` + * + * 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.shippingOptionType.list({ + * limit: 10, + * offset: 10 + * }) + * .then(({ shipping_option_types, count, limit, offset }) => { + * console.log(shipping_option_types) + * }) + * ``` + * + * Using the `fields` query parameter, you can specify the fields and relations to retrieve + * in each shipping option type: + * + * ```ts + * sdk.admin.shippingOptionType.list({ + * fields: "id,*shippingOptions" + * }) + * .then(({ shipping_option_types, count, limit, offset }) => { + * console.log(shipping_option_types) + * }) + * ``` + * + * Learn more about the `fields` property in the [API reference](https://docs.medusajs.com/api/store#select-fields-and-relations). + */ + async list( + query?: HttpTypes.AdminShippingOptionTypeListParams, + headers?: ClientHeaders + ) { + return this.client.fetch( + `/admin/shipping-option-types`, + { + headers, + query: query, + } + ) + } + + /** + * This method retrieves a shipping option type by its ID. It sends a request to the + * [Get Shipping Option Type](TODO HERE) + * API route. + * + * @param id - The shipping option type's ID. + * @param query - Configure the fields to retrieve in the shipping option type. + * @param headers - Headers to pass in the request + * @returns The shipping option type's details. + * + * @example + * To retrieve a shipping option type by its ID: + * + * ```ts + * sdk.admin.shippingOptionType.retrieve("sotype_123") + * .then(({ shipping_option_type }) => { + * console.log(shipping_option_type) + * }) + * ``` + * + * To specify the fields and relations to retrieve: + * + * ```ts + * sdk.admin.shippingOptionType.retrieve("sotype_123", { + * fields: "id,*shippingOptions" + * }) + * .then(({ shipping_option_type }) => { + * console.log(shipping_option_type) + * }) + * ``` + * + * 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?: HttpTypes.SelectParams, + headers?: ClientHeaders + ) { + return this.client.fetch( + `/admin/shipping-option-types/${id}`, + { + query, + headers, + } + ) + } + + /** + * This method deletes a shipping option type. It sends a request to the + * [Delete Shipping Option Type](TODO HERE) + * API route. + * + * @param id - The shipping option type's ID. + * @param headers - Headers to pass in the request + * @returns The shipping option type's details. + * + * @example + * sdk.admin.shippingOptionType.delete("sotype_123") + * .then(({ deleted }) => { + * console.log(deleted) + * }) + */ + async delete(id: string, headers?: ClientHeaders) { + return this.client.fetch( + `/admin/shipping-option-types/${id}`, + { + method: "DELETE", + headers, + } + ) + } +} diff --git a/packages/modules/fulfillment/src/models/shipping-option-type.ts b/packages/modules/fulfillment/src/models/shipping-option-type.ts index 8de32d5b02..065a8b10ed 100644 --- a/packages/modules/fulfillment/src/models/shipping-option-type.ts +++ b/packages/modules/fulfillment/src/models/shipping-option-type.ts @@ -4,9 +4,9 @@ import { ShippingOption } from "./shipping-option" export const ShippingOptionType = model.define("shipping_option_type", { id: model.id({ prefix: "sotype" }).primaryKey(), - label: model.text(), - description: model.text().nullable(), - code: model.text(), + label: model.text().searchable(), + description: model.text().searchable().nullable(), + code: model.text().searchable(), shipping_option: model.hasOne(() => ShippingOption, { mappedBy: "type", }), diff --git a/packages/modules/fulfillment/src/models/shipping-option.ts b/packages/modules/fulfillment/src/models/shipping-option.ts index ee02078ef6..0327cbb000 100644 --- a/packages/modules/fulfillment/src/models/shipping-option.ts +++ b/packages/modules/fulfillment/src/models/shipping-option.ts @@ -10,7 +10,7 @@ import { ShippingProfile } from "./shipping-profile" export const ShippingOption = model .define("shipping_option", { id: model.id({ prefix: "so" }).primaryKey(), - name: model.text(), + name: model.text().searchable(), price_type: model .enum(ShippingOptionPriceType) .default(ShippingOptionPriceType.FLAT),