fix(dashboard,medusa,fulfillment): Move Shipping Profiles to settings (#7090)

**What**
- Moves Shipping Profiles to settings
- Adds `q` and filters to shipping profile list endpoint
- Adds new details page for profiles
This commit is contained in:
Kasper Fabricius Kristensen authored and GitHub committed 2024-04-19 14:11:32 +00:00
1 parent 9bd2d30595
commit e2fabc1c05
31 files changed
+525 -125

No files matched your search

@@ -84,7 +84,9 @@
"lastThirtyDays": "Last 30 days", "lastThirtyDays": "Last 30 days",
"lastNinetyDays": "Last 90 days", "lastNinetyDays": "Last 90 days",
"lastTwelveMonths": "Last 12 months", "lastTwelveMonths": "Last 12 months",
"custom": "Custom" "custom": "Custom",
"from": "From",
"to": "To"
}, },
"compare": { "compare": {
"lessThan": "Less than", "lessThan": "Less than",
@@ -673,10 +675,19 @@
}, },
"shippingProfile": { "shippingProfile": {
"domain": "Shipping Profiles", "domain": "Shipping Profiles",
"title": "Create a shipping profile", "create": {
"detailsHint": "Specify the details of the shipping profile", "header": "Create Shipping Profile",
"deleteWaring": "You are about to delete the profile: {{name}}. This action cannot be undone.", "hint": "Create a new shipping profile to group products with similar shipping requirements.",
"typeHint": "Enter shipping profile type, for example: Express, Freight, etc." "successToast": "Shipping profile {{name}} was successfully created."
},
"delete": {
"title": "Delete Shipping Profile",
"description": "You are about to delete the shipping profile {{name}}. This action cannot be undone.",
"successToast": "Shipping profile {{name}} was successfully deleted."
},
"tooltip": {
"type": "Enter shipping profile type, for example: Express, Freight, etc."
}
}, },
"discounts": { "discounts": {
"domain": "Discounts", "domain": "Discounts",
@@ -1,9 +1,10 @@
import { Text } from "@medusajs/ui" import { Text, clx } from "@medusajs/ui"
import { ReactNode } from "react"
export type SectionRowProps = { export type SectionRowProps = {
title: string title: string
value?: React.ReactNode | string | null value?: ReactNode | string | null
actions?: React.ReactNode actions?: ReactNode
} }
export const SectionRow = ({ title, value, actions }: SectionRowProps) => { export const SectionRow = ({ title, value, actions }: SectionRowProps) => {
@@ -11,9 +12,12 @@ export const SectionRow = ({ title, value, actions }: SectionRowProps) => {
return ( return (
<div <div
className={`text-ui-fg-subtle grid ${ className={clx(
!!actions ? "grid-cols-[1fr_1fr_28px]" : "grid-cols-2" `text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4`,
} items-center px-6 py-4`} {
"grid-cols-[1fr_1fr_28px]": !!actions,
}
)}
> >
<Text size="small" weight="plus" leading="compact"> <Text size="small" weight="plus" leading="compact">
{title} {title}
@@ -1,16 +1,151 @@
import { clx } from "@medusajs/ui" import { Container, Heading, Text, clx } from "@medusajs/ui"
import { CSSProperties, ComponentPropsWithoutRef } from "react"
type SkeletonProps = { type SkeletonProps = {
className?: string className?: string
style?: CSSProperties
} }
export const Skeleton = ({ className }: SkeletonProps) => { export const Skeleton = ({ className, style }: SkeletonProps) => {
return ( return (
<div <div
aria-hidden
className={clx( className={clx(
"bg-ui-bg-component h-3 w-3 animate-pulse rounded-[4px]", "bg-ui-bg-component h-3 w-3 animate-pulse rounded-[4px]",
className className
)} )}
style={style}
/> />
) )
} }
type TextSkeletonProps = {
size?: ComponentPropsWithoutRef<typeof Text>["size"]
leading?: ComponentPropsWithoutRef<typeof Text>["leading"]
characters?: number
}
type HeadingSkeletonProps = {
level?: ComponentPropsWithoutRef<typeof Heading>["level"]
characters?: number
}
export const HeadingSkeleton = ({
level = "h1",
characters = 10,
}: HeadingSkeletonProps) => {
let charWidth = 9
switch (level) {
case "h1":
charWidth = 11
break
case "h2":
charWidth = 10
break
case "h3":
charWidth = 9
break
}
return (
<Skeleton
className={clx({
"h-7": level === "h1",
"h-6": level === "h2",
"h-5": level === "h3",
})}
style={{
width: `${charWidth * characters}px`,
}}
/>
)
}
export const TextSkeleton = ({
size = "small",
leading = "compact",
characters = 10,
}: TextSkeletonProps) => {
let charWidth = 9
switch (size) {
case "xlarge":
charWidth = 13
break
case "large":
charWidth = 11
break
case "base":
charWidth = 10
break
case "small":
charWidth = 9
break
case "xsmall":
charWidth = 8
break
}
return (
<Skeleton
className={clx({
"h-5": size === "xsmall",
"h-6": size === "small",
"h-7": size === "base",
"h-8": size === "xlarge",
"!h-5": leading === "compact",
})}
style={{
width: `${charWidth * characters}px`,
}}
/>
)
}
export const IconButtonSkeleton = () => {
return <Skeleton className="h-7 w-7 rounded-md" />
}
type GeneralSectionSkeletonProps = {
rowCount?: number
}
export const GeneralSectionSkeleton = ({
rowCount,
}: GeneralSectionSkeletonProps) => {
const rows = Array.from({ length: rowCount ?? 0 }, (_, i) => i)
return (
<Container className="divide-y p-0" aria-hidden>
<div className="flex items-center justify-between px-6 py-4">
<HeadingSkeleton characters={16} />
<IconButtonSkeleton />
</div>
{rows.map((row) => (
<div
key={row}
className="grid grid-cols-2 items-center px-6 py-4"
aria-hidden
>
<TextSkeleton size="small" leading="compact" characters={12} />
<TextSkeleton size="small" leading="compact" characters={24} />
</div>
))}
</Container>
)
}
export const JsonViewSectionSkeleton = () => {
return (
<Container className="divide-y p-0" aria-hidden>
<div className="flex items-center justify-between px-6 py-4" aria-hidden>
<div aria-hidden className="flex items-center gap-x-4">
<HeadingSkeleton level="h2" characters={16} />
<Skeleton className="h-5 w-12 rounded-md" />
</div>
<IconButtonSkeleton />
</div>
</Container>
)
}
@@ -148,12 +148,6 @@ const useCoreRoutes = (): Omit<NavItemProps, "pathname">[] => {
icon: <Envelope />, icon: <Envelope />,
label: t("shipping.domain"), label: t("shipping.domain"),
to: "/shipping", to: "/shipping",
items: [
{
label: t("shippingProfile.domain"),
to: "/shipping-profiles",
},
],
}, },
] ]
} }
@@ -53,6 +53,10 @@ const useSettingRoutes = (): NavItemProps[] => {
label: t("salesChannels.domain"), label: t("salesChannels.domain"),
to: "/settings/sales-channels", to: "/settings/sales-channels",
}, },
{
label: t("shippingProfile.domain"),
to: "/settings/shipping-profiles",
},
], ],
[t] [t]
) )
@@ -1,12 +1,12 @@
import { EllipseMiniSolid, XMarkMini } from "@medusajs/icons" import { EllipseMiniSolid, XMarkMini } from "@medusajs/icons"
import { DatePicker, Text, clx } from "@medusajs/ui" import { DatePicker, Text, clx } from "@medusajs/ui"
import * as Popover from "@radix-ui/react-popover" import * as Popover from "@radix-ui/react-popover"
import { format } from "date-fns"
import isEqual from "lodash/isEqual" import isEqual from "lodash/isEqual"
import { MouseEvent, useMemo, useState } from "react" import { MouseEvent, useMemo, useState } from "react"
import { t } from "i18next" import { t } from "i18next"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { useDate } from "../../../../hooks/use-date"
import { useSelectedParams } from "../hooks" import { useSelectedParams } from "../hooks"
import { useDataTableFilterContext } from "./context" import { useDataTableFilterContext } from "./context"
import { IFilter } from "./types" import { IFilter } from "./types"
@@ -17,19 +17,19 @@ type DateComparisonOperator = {
/** /**
* The filtered date must be greater than or equal to this value. * The filtered date must be greater than or equal to this value.
*/ */
gte?: string $gte?: string
/** /**
* The filtered date must be less than or equal to this value. * The filtered date must be less than or equal to this value.
*/ */
lte?: string $lte?: string
/** /**
* The filtered date must be less than this value. * The filtered date must be less than this value.
*/ */
lt?: string $lt?: string
/** /**
* The filtered date must be greater than this value. * The filtered date must be greater than this value.
*/ */
gt?: string $gt?: string
} }
export const DateFilter = ({ export const DateFilter = ({
@@ -40,6 +40,8 @@ export const DateFilter = ({
const [open, setOpen] = useState(openOnMount) const [open, setOpen] = useState(openOnMount)
const [showCustom, setShowCustom] = useState(false) const [showCustom, setShowCustom] = useState(false)
const { getFullDate } = useDate()
const { key, label } = filter const { key, label } = filter
const { removeFilter } = useDataTableFilterContext() const { removeFilter } = useDataTableFilterContext()
@@ -60,14 +62,14 @@ export const DateFilter = ({
const currentValue = selectedParams.get() const currentValue = selectedParams.get()
const currentDateComparison = parseDateComparison(currentValue) const currentDateComparison = parseDateComparison(currentValue)
const customStartValue = getDateFromComparison(currentDateComparison, "gte") const customStartValue = getDateFromComparison(currentDateComparison, "$gte")
const customEndValue = getDateFromComparison(currentDateComparison, "lte") const customEndValue = getDateFromComparison(currentDateComparison, "$lte")
const handleCustomDateChange = ( const handleCustomDateChange = (
value: Date | undefined, value: Date | undefined,
pos: "start" | "end" pos: "start" | "end"
) => { ) => {
const key = pos === "start" ? "gte" : "lte" const key = pos === "start" ? "$gte" : "$lte"
const dateValue = value ? value.toISOString() : undefined const dateValue = value ? value.toISOString() : undefined
selectedParams.add( selectedParams.add(
@@ -84,7 +86,7 @@ export const DateFilter = ({
} }
const formatCustomDate = (date: Date | undefined) => { const formatCustomDate = (date: Date | undefined) => {
return date ? format(date, "dd MMM, yyyy") : undefined return date ? getFullDate({ date: date }) : undefined
} }
const getCustomDisplayValue = () => { const getCustomDisplayValue = () => {
@@ -194,12 +196,12 @@ export const DateFilter = ({
<div> <div>
<div className="px-2 py-1"> <div className="px-2 py-1">
<Text size="xsmall" leading="compact" weight="plus"> <Text size="xsmall" leading="compact" weight="plus">
Starting {t("filters.date.from")}
</Text> </Text>
</div> </div>
<div className="px-2 py-1"> <div className="px-2 py-1">
<DatePicker <DatePicker
placeholder="MM/DD/YYYY" // placeholder="MM/DD/YYYY" TODO: Fix DatePicker component not working with placeholder
toDate={customEndValue} toDate={customEndValue}
value={customStartValue} value={customStartValue}
onChange={(d) => handleCustomDateChange(d, "start")} onChange={(d) => handleCustomDateChange(d, "start")}
@@ -209,12 +211,12 @@ export const DateFilter = ({
<div> <div>
<div className="px-2 py-1"> <div className="px-2 py-1">
<Text size="xsmall" leading="compact" weight="plus"> <Text size="xsmall" leading="compact" weight="plus">
Ending {t("filters.date.to")}
</Text> </Text>
</div> </div>
<div className="px-2 py-1"> <div className="px-2 py-1">
<DatePicker <DatePicker
placeholder="MM/DD/YYYY" // placeholder="MM/DD/YYYY"
fromDate={customStartValue} fromDate={customStartValue}
value={customEndValue || undefined} value={customEndValue || undefined}
onChange={(d) => { onChange={(d) => {
@@ -301,13 +303,13 @@ const usePresets = () => {
{ {
label: t("filters.date.today"), label: t("filters.date.today"),
value: { value: {
gte: today.toISOString(), $gte: today.toISOString(),
}, },
}, },
{ {
label: t("filters.date.lastSevenDays"), label: t("filters.date.lastSevenDays"),
value: { value: {
gte: new Date( $gte: new Date(
today.getTime() - 7 * 24 * 60 * 60 * 1000 today.getTime() - 7 * 24 * 60 * 60 * 1000
).toISOString(), // 7 days ago ).toISOString(), // 7 days ago
}, },
@@ -315,7 +317,7 @@ const usePresets = () => {
{ {
label: t("filters.date.lastThirtyDays"), label: t("filters.date.lastThirtyDays"),
value: { value: {
gte: new Date( $gte: new Date(
today.getTime() - 30 * 24 * 60 * 60 * 1000 today.getTime() - 30 * 24 * 60 * 60 * 1000
).toISOString(), // 30 days ago ).toISOString(), // 30 days ago
}, },
@@ -323,7 +325,7 @@ const usePresets = () => {
{ {
label: t("filters.date.lastNinetyDays"), label: t("filters.date.lastNinetyDays"),
value: { value: {
gte: new Date( $gte: new Date(
today.getTime() - 90 * 24 * 60 * 60 * 1000 today.getTime() - 90 * 24 * 60 * 60 * 1000
).toISOString(), // 90 days ago ).toISOString(), // 90 days ago
}, },
@@ -331,7 +333,7 @@ const usePresets = () => {
{ {
label: t("filters.date.lastTwelveMonths"), label: t("filters.date.lastTwelveMonths"),
value: { value: {
gte: new Date( $gte: new Date(
today.getTime() - 365 * 24 * 60 * 60 * 1000 today.getTime() - 365 * 24 * 60 * 60 * 1000
).toISOString(), // 365 days ago ).toISOString(), // 365 days ago
}, },
@@ -349,7 +351,7 @@ const parseDateComparison = (value: string[]) => {
const getDateFromComparison = ( const getDateFromComparison = (
comparison: DateComparisonOperator | null, comparison: DateComparisonOperator | null,
key: "gte" | "lte" key: "$gte" | "$lte"
) => { ) => {
return comparison?.[key] ? new Date(comparison[key] as string) : undefined return comparison?.[key] ? new Date(comparison[key] as string) : undefined
} }
@@ -7,11 +7,11 @@ import {
} from "@tanstack/react-query" } from "@tanstack/react-query"
import { CreateShippingProfileReq } from "../../types/api-payloads" import { CreateShippingProfileReq } from "../../types/api-payloads"
import { import {
ShippingProfileDeleteRes,
ShippingProfileListRes, ShippingProfileListRes,
ShippingProfileRes, ShippingProfileRes,
} from "../../types/api-responses" } from "../../types/api-responses"
import { DeleteResponse } from "@medusajs/types"
import { client } from "../../lib/client" import { client } from "../../lib/client"
import { queryClient } from "../../lib/medusa" import { queryClient } from "../../lib/medusa"
import { queryKeysFactory } from "../../lib/query-key-factory" import { queryKeysFactory } from "../../lib/query-key-factory"
@@ -41,6 +41,25 @@ export const useCreateShippingProfile = (
}) })
} }
export const useShippingProfile = (
id: string,
query?: Record<string, any>,
options?: UseQueryOptions<
ShippingProfileRes,
Error,
ShippingProfileRes,
QueryKey
>
) => {
const { data, ...rest } = useQuery({
queryFn: () => client.shippingProfiles.retrieve(id, query),
queryKey: shippingProfileQueryKeys.detail(id, query),
...options,
})
return { ...data, ...rest }
}
export const useShippingProfiles = ( export const useShippingProfiles = (
query?: Record<string, any>, query?: Record<string, any>,
options?: Omit< options?: Omit<
@@ -64,11 +83,11 @@ export const useShippingProfiles = (
export const useDeleteShippingProfile = ( export const useDeleteShippingProfile = (
profileId: string, profileId: string,
options?: UseMutationOptions<ShippingProfileDeleteRes, Error, void> options?: UseMutationOptions<DeleteResponse, Error, void>
) => { ) => {
return useMutation({ return useMutation({
mutationFn: () => client.shippingProfiles.delete(profileId), mutationFn: () => client.shippingProfiles.delete(profileId),
onSuccess: (data: any, variables: any, context: any) => { onSuccess: (data, variables, context) => {
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: shippingProfileQueryKeys.lists(), queryKey: shippingProfileQueryKeys.lists(),
}) })
@@ -1,27 +1,35 @@
import { deleteRequest, getRequest, postRequest } from "./common"
import { CreateShippingProfileReq } from "../../types/api-payloads" import { CreateShippingProfileReq } from "../../types/api-payloads"
import { import {
ShippingProfileDeleteRes, ShippingProfileDeleteRes,
ShippingProfileListRes, ShippingProfileListRes,
ShippingProfileRes, ShippingProfileRes,
} from "../../types/api-responses" } from "../../types/api-responses"
import { deleteRequest, getRequest, postRequest } from "./common"
async function createShippingProfile(payload: CreateShippingProfileReq) { async function createShippingProfile(payload: CreateShippingProfileReq) {
return postRequest<ShippingProfileRes>(`/admin/shipping-profiles`, payload) return postRequest<ShippingProfileRes>(`/admin/shipping-profiles`, payload)
} }
async function retrieveShippingProfile(
id: string,
query?: Record<string, any>
) {
return getRequest<ShippingProfileRes>(`/admin/shipping-profiles/${id}`, query)
}
async function listShippingProfiles(query?: Record<string, any>) { async function listShippingProfiles(query?: Record<string, any>) {
return getRequest<ShippingProfileListRes>(`/admin/shipping-profiles`, query) return getRequest<ShippingProfileListRes>(`/admin/shipping-profiles`, query)
} }
async function deleteShippingProfile(profileId: string) { async function deleteShippingProfile(id: string) {
return deleteRequest<ShippingProfileDeleteRes>( return deleteRequest<ShippingProfileDeleteRes>(
`/admin/shipping-profiles/${profileId}` `/admin/shipping-profiles/${id}`
) )
} }
export const shippingProfiles = { export const shippingProfiles = {
create: createShippingProfile, retrieve: retrieveShippingProfile,
list: listShippingProfiles, list: listShippingProfiles,
create: createShippingProfile,
delete: deleteShippingProfile, delete: deleteShippingProfile,
} }
@@ -1,6 +1,4 @@
import type { import type {
AdminCustomerGroupsRes,
AdminCustomersRes,
AdminDiscountsRes, AdminDiscountsRes,
AdminDraftOrdersRes, AdminDraftOrdersRes,
AdminGiftCardsRes, AdminGiftCardsRes,
@@ -329,21 +329,6 @@ export const v2Routes: RouteObject[] = [
}, },
], ],
}, },
{
path: "shipping-profiles",
lazy: () =>
import("../../v2-routes/shipping/shipping-profiles-list"),
handle: {
crumb: () => "Shipping Profiles",
},
children: [
{
path: "create",
lazy: () =>
import("../../v2-routes/shipping/shipping-profile-create"),
},
],
},
{ {
path: "/customers", path: "/customers",
handle: { handle: {
@@ -727,6 +712,38 @@ export const v2Routes: RouteObject[] = [
}, },
], ],
}, },
{
path: "shipping-profiles",
element: <Outlet />,
handle: {
crumb: () => "Shipping Profiles",
},
children: [
{
path: "",
lazy: () =>
import(
"../../v2-routes/shipping-profiles/shipping-profiles-list"
),
children: [
{
path: "create",
lazy: () =>
import(
"../../v2-routes/shipping-profiles/shipping-profile-create"
),
},
],
},
{
path: ":id",
lazy: () =>
import(
"../../v2-routes/shipping-profiles/shipping-profile-detail"
),
},
],
},
{ {
path: "api-key-management", path: "api-key-management",
element: <Outlet />, element: <Outlet />,
@@ -3,12 +3,12 @@ import { RegionDTO } from "@medusajs/types"
import { Badge, Container, Heading, Text, usePrompt } from "@medusajs/ui" import { Badge, Container, Heading, Text, usePrompt } from "@medusajs/ui"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { ActionMenu } from "../../../../../components/common/action-menu"
import { formatProvider } from "../../../../../lib/format-provider"
import { currencies } from "../../../../../lib/currencies"
import { useDeleteRegion } from "../../../../../hooks/api/regions.tsx"
import { ListSummary } from "../../../../../components/common/list-summary"
import { useNavigate } from "react-router-dom" import { useNavigate } from "react-router-dom"
import { ActionMenu } from "../../../../../components/common/action-menu"
import { ListSummary } from "../../../../../components/common/list-summary"
import { useDeleteRegion } from "../../../../../hooks/api/regions.tsx"
import { currencies } from "../../../../../lib/currencies"
import { formatProvider } from "../../../../../lib/format-provider"
type RegionGeneralSectionProps = { type RegionGeneralSectionProps = {
region: RegionDTO region: RegionDTO
@@ -23,7 +23,7 @@ export const RegionGeneralSection = ({ region }: RegionGeneralSectionProps) => {
<Heading>{region.name}</Heading> <Heading>{region.name}</Heading>
<RegionActions region={region} /> <RegionActions region={region} />
</div> </div>
<div className="grid grid-cols-2 items-center px-6 py-4"> <div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
<Text size="small" leading="compact" weight="plus"> <Text size="small" leading="compact" weight="plus">
{t("fields.currency")} {t("fields.currency")}
</Text> </Text>
@@ -36,7 +36,7 @@ export const RegionGeneralSection = ({ region }: RegionGeneralSectionProps) => {
</Text> </Text>
</div> </div>
</div> </div>
<div className="grid grid-cols-2 items-center px-6 py-4"> <div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
<Text size="small" leading="compact" weight="plus"> <Text size="small" leading="compact" weight="plus">
{t("fields.paymentProviders")} {t("fields.paymentProviders")}
</Text> </Text>
@@ -1,14 +1,14 @@
import { Button, Heading, Input, Text } from "@medusajs/ui"
import { useForm } from "react-hook-form"
import * as zod from "zod"
import { zodResolver } from "@hookform/resolvers/zod" 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 { useTranslation } from "react-i18next"
import * as zod from "zod"
import { Form } from "../../../../../components/common/form"
import { import {
RouteFocusModal, RouteFocusModal,
useRouteModal, useRouteModal,
} from "../../../../../components/route-modal" } from "../../../../../components/route-modal"
import { Form } from "../../../../../components/common/form"
import { useCreateShippingProfile } from "../../../../../hooks/api/shipping-profiles" import { useCreateShippingProfile } from "../../../../../hooks/api/shipping-profiles"
const CreateShippingOptionsSchema = zod.object({ const CreateShippingOptionsSchema = zod.object({
@@ -31,11 +31,30 @@ export function CreateShippingProfileForm() {
const { mutateAsync, isPending } = useCreateShippingProfile() const { mutateAsync, isPending } = useCreateShippingProfile()
const handleSubmit = form.handleSubmit(async (values) => { const handleSubmit = form.handleSubmit(async (values) => {
await mutateAsync({ await mutateAsync(
{
name: values.name, name: values.name,
type: values.type, type: values.type,
},
{
onSuccess: ({ shipping_profile }) => {
toast.success(t("general.success"), {
description: t("shippingProfile.create.successToast", {
name: shipping_profile.name,
}),
dismissLabel: t("actions.close"),
}) })
handleSuccess("/shipping-profiles")
handleSuccess(`/settings/shipping-profiles/${shipping_profile.id}`)
},
onError: (error) => {
toast.error(t("general.error"), {
description: error.message,
dismissLabel: t("actions.close"),
})
},
}
)
}) })
return ( return (
@@ -61,10 +80,10 @@ export function CreateShippingProfileForm() {
<div className="mx-auto flex w-full max-w-[720px] flex-col gap-y-8 px-2 py-16"> <div className="mx-auto flex w-full max-w-[720px] flex-col gap-y-8 px-2 py-16">
<div> <div>
<Heading className="capitalize"> <Heading className="capitalize">
{t("shippingProfile.title")} {t("shippingProfile.create.header")}
</Heading> </Heading>
<Text size="small" className="text-ui-fg-subtle"> <Text size="small" className="text-ui-fg-subtle">
{t("shippingProfile.detailsHint")} {t("shippingProfile.create.hint")}
</Text> </Text>
</div> </div>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
@@ -89,7 +108,7 @@ export function CreateShippingProfileForm() {
render={({ field }) => { render={({ field }) => {
return ( return (
<Form.Item> <Form.Item>
<Form.Label tooltip={t("shippingProfile.typeHint")}> <Form.Label tooltip={t("shippingProfile.tooltip.type")}>
{t("fields.type")} {t("fields.type")}
</Form.Label> </Form.Label>
<Form.Control> <Form.Control>
@@ -0,0 +1,80 @@
import { Trash } from "@medusajs/icons"
import { AdminShippingProfileResponse } from "@medusajs/types"
import { Container, Heading, toast, usePrompt } from "@medusajs/ui"
import { useTranslation } from "react-i18next"
import { useNavigate } from "react-router-dom"
import { ActionMenu } from "../../../../../components/common/action-menu"
import { SectionRow } from "../../../../../components/common/section"
import { useDeleteShippingProfile } from "../../../../../hooks/api/shipping-profiles"
type ShippingProfileGeneralSectionProps = {
profile: AdminShippingProfileResponse["shipping_profile"]
}
export const ShippingProfileGeneralSection = ({
profile,
}: ShippingProfileGeneralSectionProps) => {
const { t } = useTranslation()
const prompt = usePrompt()
const navigate = useNavigate()
const { mutateAsync } = useDeleteShippingProfile(profile.id)
const handleDelete = async () => {
const res = await prompt({
title: t("shippingProfile.delete.title"),
description: t("shippingProfile.delete.description", {
name: profile.name,
}),
verificationText: profile.name,
verificationInstruction: t("general.typeToConfirm"),
confirmText: t("actions.delete"),
cancelText: t("actions.cancel"),
})
if (!res) {
return
}
await mutateAsync(undefined, {
onSuccess: () => {
toast.success(t("general.success"), {
description: t("shippingProfile.delete.successToast", {
name: profile.name,
}),
dismissLabel: t("actions.close"),
})
navigate("/settings/shipping-profiles", { replace: true })
},
onError: (error) => {
toast.error(t("general.error"), {
description: error.message,
dismissLabel: t("actions.close"),
})
},
})
}
return (
<Container className="divide-y p-0">
<div className="flex items-center justify-between px-6 py-4">
<Heading>{profile.name}</Heading>
<ActionMenu
groups={[
{
actions: [
{
icon: <Trash />,
label: t("actions.delete"),
onClick: handleDelete,
},
],
},
]}
/>
</div>
<SectionRow title={t("fields.type")} value={profile.type} />
</Container>
)
}
@@ -0,0 +1 @@
export { ShippingProfileDetail as Component } from "./shipping-profile-detail"
@@ -0,0 +1,36 @@
import { useParams } from "react-router-dom"
import { JsonViewSection } from "../../../components/common/json-view-section"
import {
GeneralSectionSkeleton,
JsonViewSectionSkeleton,
} from "../../../components/common/skeleton"
import { useShippingProfile } from "../../../hooks/api/shipping-profiles"
import { ShippingProfileGeneralSection } from "./components/shipping-profile-general-section"
export const ShippingProfileDetail = () => {
const { id } = useParams()
const { shipping_profile, isLoading, isError, error } = useShippingProfile(
id!
)
if (isLoading || !shipping_profile) {
return (
<div className="flex flex-col gap-y-2">
<GeneralSectionSkeleton rowCount={1} />
<JsonViewSectionSkeleton />
</div>
)
}
if (isError) {
throw error
}
return (
<div className="flex flex-col gap-y-2">
<ShippingProfileGeneralSection profile={shipping_profile} />
<JsonViewSection data={shipping_profile} />
</div>
)
}
@@ -1,7 +1,7 @@
import { Trash } from "@medusajs/icons" import { Trash } from "@medusajs/icons"
import { usePrompt } from "@medusajs/ui" import { AdminShippingProfileResponse } from "@medusajs/types"
import { toast, usePrompt } from "@medusajs/ui"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { ShippingProfileDTO } from "@medusajs/types"
import { ActionMenu } from "../../../../../components/common/action-menu" import { ActionMenu } from "../../../../../components/common/action-menu"
import { useDeleteShippingProfile } from "../../../../../hooks/api/shipping-profiles" import { useDeleteShippingProfile } from "../../../../../hooks/api/shipping-profiles"
@@ -9,17 +9,17 @@ import { useDeleteShippingProfile } from "../../../../../hooks/api/shipping-prof
export const ShippingOptionsRowActions = ({ export const ShippingOptionsRowActions = ({
profile, profile,
}: { }: {
profile: ShippingProfileDTO profile: AdminShippingProfileResponse["shipping_profile"]
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
const prompt = usePrompt() const prompt = usePrompt()
// TODO: MISSING ENDPOINT
const { mutateAsync } = useDeleteShippingProfile(profile.id) const { mutateAsync } = useDeleteShippingProfile(profile.id)
const handleDelete = async () => { const handleDelete = async () => {
const res = await prompt({ const res = await prompt({
title: t("general.areYouSure"), title: t("shippingProfile.delete.title"),
description: t("shippingProfile.deleteWaring", { description: t("shippingProfile.delete.description", {
name: profile.name, name: profile.name,
}), }),
verificationText: profile.name, verificationText: profile.name,
@@ -32,7 +32,22 @@ export const ShippingOptionsRowActions = ({
return return
} }
await mutateAsync() await mutateAsync(undefined, {
onSuccess: () => {
toast.success(t("general.success"), {
description: t("shippingProfile.delete.successToast", {
name: profile.name,
}),
dismissLabel: t("actions.close"),
})
},
onError: (error) => {
toast.error(t("general.error"), {
description: error.message,
dismissLabel: t("actions.close"),
})
},
})
} }
return ( return (
@@ -1,29 +1,31 @@
import { Button, Container, Heading } from "@medusajs/ui" import { Button, Container, Heading } from "@medusajs/ui"
import { Link } from "react-router-dom" import { Link } from "react-router-dom"
import { keepPreviousData } from "@tanstack/react-query"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { DataTable } from "../../../../../components/table/data-table" import { DataTable } from "../../../../../components/table/data-table"
import { useDataTable } from "../../../../../hooks/use-data-table"
import { useShippingProfilesTableColumns } from "./use-shipping-profiles-table-columns"
import { useShippingProfilesTableQuery } from "./use-shipping-profiles-table-query"
import { NoRecords } from "../../../../../components/common/empty-table-content"
import { useShippingProfiles } from "../../../../../hooks/api/shipping-profiles" import { useShippingProfiles } from "../../../../../hooks/api/shipping-profiles"
import { useDataTable } from "../../../../../hooks/use-data-table"
import { useShippingProfileTableColumns } from "./use-shipping-profile-table-columns"
import { useShippingProfileTableFilters } from "./use-shipping-profile-table-filters"
import { useShippingProfileTableQuery } from "./use-shipping-profile-table-query"
const PAGE_SIZE = 20 const PAGE_SIZE = 20
export const ShippingProfileListTable = () => { export const ShippingProfileListTable = () => {
const { t } = useTranslation() const { t } = useTranslation()
const { raw, searchParams } = useShippingProfilesTableQuery({ const { raw, searchParams } = useShippingProfileTableQuery({
pageSize: PAGE_SIZE, pageSize: PAGE_SIZE,
}) })
const { shipping_profiles, count, isLoading, isError, error } = const { shipping_profiles, count, isLoading, isError, error } =
useShippingProfiles({ useShippingProfiles(searchParams, {
...searchParams, placeholderData: keepPreviousData,
}) })
const columns = useShippingProfilesTableColumns() const columns = useShippingProfileTableColumns()
const filters = useShippingProfileTableFilters()
const { table } = useDataTable({ const { table } = useDataTable({
data: shipping_profiles, data: shipping_profiles,
@@ -38,8 +40,6 @@ export const ShippingProfileListTable = () => {
throw error throw error
} }
const noData = !isLoading && !shipping_profiles.length
return ( return (
<Container className="divide-y p-0"> <Container className="divide-y p-0">
<div className="flex items-center justify-between px-6 py-4"> <div className="flex items-center justify-between px-6 py-4">
@@ -50,19 +50,19 @@ export const ShippingProfileListTable = () => {
</Button> </Button>
</div> </div>
</div> </div>
{noData ? (
<NoRecords className="h-[180px]" title={t("general.noRecordsFound")} />
) : (
<DataTable <DataTable
table={table} table={table}
pageSize={PAGE_SIZE} pageSize={PAGE_SIZE}
count={count || 1} count={count}
columns={columns} columns={columns}
filters={filters}
orderBy={["name", "type", "created_at", "updated_at"]}
isLoading={isLoading} isLoading={isLoading}
navigateTo={(row) => row.id}
queryObject={raw} queryObject={raw}
search
pagination pagination
/> />
)}
</Container> </Container>
) )
} }
@@ -1,13 +1,14 @@
import { AdminShippingProfileResponse } from "@medusajs/types"
import { createColumnHelper } from "@tanstack/react-table" import { createColumnHelper } from "@tanstack/react-table"
import { useMemo } from "react" import { useMemo } from "react"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { ShippingProfileDTO } from "@medusajs/types"
import { ShippingOptionsRowActions } from "./shipping-options-row-actions" import { ShippingOptionsRowActions } from "./shipping-options-row-actions"
const columnHelper = createColumnHelper<ShippingProfileDTO>() const columnHelper =
createColumnHelper<AdminShippingProfileResponse["shipping_profile"]>()
export const useShippingProfilesTableColumns = () => { export const useShippingProfileTableColumns = () => {
const { t } = useTranslation() const { t } = useTranslation()
return useMemo( return useMemo(
@@ -0,0 +1,33 @@
import { useTranslation } from "react-i18next"
import { Filter } from "../../../../../components/table/data-table"
export const useShippingProfileTableFilters = () => {
const { t } = useTranslation()
let filters: Filter[] = []
filters.push({
key: "name",
label: t("fields.name"),
type: "string",
})
filters.push({
key: "type",
label: t("fields.type"),
type: "string",
})
const dateFilters: Filter[] = [
{ label: t("fields.createdAt"), key: "created_at" },
{ label: t("fields.updatedAt"), key: "updated_at" },
].map((f) => ({
key: f.key,
label: f.label,
type: "date",
}))
filters = [...filters, ...dateFilters]
return filters
}
@@ -0,0 +1,30 @@
import { useQueryParams } from "../../../../../hooks/use-query-params"
export const useShippingProfileTableQuery = ({
pageSize = 20,
prefix,
}: {
pageSize?: number
prefix?: string
}) => {
const raw = useQueryParams(
["offset", "q", "order", "created_at", "updated_at", "name", "type"],
prefix
)
const searchParams = {
limit: pageSize,
offset: raw.offset ? parseInt(raw.offset) : 0,
q: raw.q,
order: raw.order,
created_at: raw.created_at ? JSON.parse(raw.created_at) : undefined,
updated_at: raw.updated_at ? JSON.parse(raw.updated_at) : undefined,
name: raw.name,
type: raw.type,
}
return {
searchParams,
raw,
}
}
Whitespace-only changes.
@@ -1,21 +0,0 @@
import { useQueryParams } from "../../../../../hooks/use-query-params"
export const useShippingProfilesTableQuery = ({
pageSize = 20,
prefix,
}: {
pageSize?: number
prefix?: string
}) => {
const raw = useQueryParams(["offset"], prefix)
const searchParams = {
limit: pageSize,
offset: raw.offset,
}
return {
searchParams,
raw,
}
}
@@ -2,6 +2,7 @@ import {
createPsqlIndexStatementHelper, createPsqlIndexStatementHelper,
DALUtils, DALUtils,
generateEntityId, generateEntityId,
Searchable,
} from "@medusajs/utils" } from "@medusajs/utils"
import { DAL } from "@medusajs/types" import { DAL } from "@medusajs/types"
@@ -41,10 +42,12 @@ export default class ShippingProfile {
@PrimaryKey({ columnType: "text" }) @PrimaryKey({ columnType: "text" })
id: string id: string
@Searchable()
@Property({ columnType: "text" }) @Property({ columnType: "text" })
@ShippingProfileTypeIndex.MikroORMIndex() @ShippingProfileTypeIndex.MikroORMIndex()
name: string name: string
@Searchable()
@Property({ columnType: "text" }) @Property({ columnType: "text" })
type: string type: string
@@ -1,5 +1,9 @@
import { z } from "zod" import { z } from "zod"
import { createFindParams, createSelectParams } from "../../utils/validators" import {
createFindParams,
createOperatorMap,
createSelectParams,
} from "../../utils/validators"
export type AdminGetShippingProfileParamsType = z.infer< export type AdminGetShippingProfileParamsType = z.infer<
typeof AdminGetShippingProfileParams typeof AdminGetShippingProfileParams
@@ -14,8 +18,14 @@ export const AdminGetShippingProfilesParams = createFindParams({
offset: 0, offset: 0,
}).merge( }).merge(
z.object({ z.object({
q: z.string().optional(),
type: z.string().optional(), type: z.string().optional(),
name: z.string().optional(), name: z.string().optional(),
created_at: createOperatorMap().optional(),
updated_at: createOperatorMap().optional(),
deleted_at: createOperatorMap().optional(),
$and: z.lazy(() => AdminGetShippingProfilesParams.array()).optional(),
$or: z.lazy(() => AdminGetShippingProfilesParams.array()).optional(),
}) })
) )