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:
committed by
GitHub
parent
9bd2d30595
commit
e2fabc1c05
@@ -84,7 +84,9 @@
|
||||
"lastThirtyDays": "Last 30 days",
|
||||
"lastNinetyDays": "Last 90 days",
|
||||
"lastTwelveMonths": "Last 12 months",
|
||||
"custom": "Custom"
|
||||
"custom": "Custom",
|
||||
"from": "From",
|
||||
"to": "To"
|
||||
},
|
||||
"compare": {
|
||||
"lessThan": "Less than",
|
||||
@@ -673,10 +675,19 @@
|
||||
},
|
||||
"shippingProfile": {
|
||||
"domain": "Shipping Profiles",
|
||||
"title": "Create a shipping profile",
|
||||
"detailsHint": "Specify the details of the shipping profile",
|
||||
"deleteWaring": "You are about to delete the profile: {{name}}. This action cannot be undone.",
|
||||
"typeHint": "Enter shipping profile type, for example: Express, Freight, etc."
|
||||
"create": {
|
||||
"header": "Create Shipping Profile",
|
||||
"hint": "Create a new shipping profile to group products with similar shipping requirements.",
|
||||
"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": {
|
||||
"domain": "Discounts",
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Text } from "@medusajs/ui"
|
||||
import { Text, clx } from "@medusajs/ui"
|
||||
import { ReactNode } from "react"
|
||||
|
||||
export type SectionRowProps = {
|
||||
title: string
|
||||
value?: React.ReactNode | string | null
|
||||
actions?: React.ReactNode
|
||||
value?: ReactNode | string | null
|
||||
actions?: ReactNode
|
||||
}
|
||||
|
||||
export const SectionRow = ({ title, value, actions }: SectionRowProps) => {
|
||||
@@ -11,9 +12,12 @@ export const SectionRow = ({ title, value, actions }: SectionRowProps) => {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`text-ui-fg-subtle grid ${
|
||||
!!actions ? "grid-cols-[1fr_1fr_28px]" : "grid-cols-2"
|
||||
} items-center px-6 py-4`}
|
||||
className={clx(
|
||||
`text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4`,
|
||||
{
|
||||
"grid-cols-[1fr_1fr_28px]": !!actions,
|
||||
}
|
||||
)}
|
||||
>
|
||||
<Text size="small" weight="plus" leading="compact">
|
||||
{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 = {
|
||||
className?: string
|
||||
style?: CSSProperties
|
||||
}
|
||||
|
||||
export const Skeleton = ({ className }: SkeletonProps) => {
|
||||
export const Skeleton = ({ className, style }: SkeletonProps) => {
|
||||
return (
|
||||
<div
|
||||
aria-hidden
|
||||
className={clx(
|
||||
"bg-ui-bg-component h-3 w-3 animate-pulse rounded-[4px]",
|
||||
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 />,
|
||||
label: t("shipping.domain"),
|
||||
to: "/shipping",
|
||||
items: [
|
||||
{
|
||||
label: t("shippingProfile.domain"),
|
||||
to: "/shipping-profiles",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
@@ -53,6 +53,10 @@ const useSettingRoutes = (): NavItemProps[] => {
|
||||
label: t("salesChannels.domain"),
|
||||
to: "/settings/sales-channels",
|
||||
},
|
||||
{
|
||||
label: t("shippingProfile.domain"),
|
||||
to: "/settings/shipping-profiles",
|
||||
},
|
||||
],
|
||||
[t]
|
||||
)
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { EllipseMiniSolid, XMarkMini } from "@medusajs/icons"
|
||||
import { DatePicker, Text, clx } from "@medusajs/ui"
|
||||
import * as Popover from "@radix-ui/react-popover"
|
||||
import { format } from "date-fns"
|
||||
import isEqual from "lodash/isEqual"
|
||||
import { MouseEvent, useMemo, useState } from "react"
|
||||
|
||||
import { t } from "i18next"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { useDate } from "../../../../hooks/use-date"
|
||||
import { useSelectedParams } from "../hooks"
|
||||
import { useDataTableFilterContext } from "./context"
|
||||
import { IFilter } from "./types"
|
||||
@@ -17,19 +17,19 @@ type DateComparisonOperator = {
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
lte?: string
|
||||
$lte?: string
|
||||
/**
|
||||
* The filtered date must be less than this value.
|
||||
*/
|
||||
lt?: string
|
||||
$lt?: string
|
||||
/**
|
||||
* The filtered date must be greater than this value.
|
||||
*/
|
||||
gt?: string
|
||||
$gt?: string
|
||||
}
|
||||
|
||||
export const DateFilter = ({
|
||||
@@ -40,6 +40,8 @@ export const DateFilter = ({
|
||||
const [open, setOpen] = useState(openOnMount)
|
||||
const [showCustom, setShowCustom] = useState(false)
|
||||
|
||||
const { getFullDate } = useDate()
|
||||
|
||||
const { key, label } = filter
|
||||
|
||||
const { removeFilter } = useDataTableFilterContext()
|
||||
@@ -60,14 +62,14 @@ export const DateFilter = ({
|
||||
const currentValue = selectedParams.get()
|
||||
|
||||
const currentDateComparison = parseDateComparison(currentValue)
|
||||
const customStartValue = getDateFromComparison(currentDateComparison, "gte")
|
||||
const customEndValue = getDateFromComparison(currentDateComparison, "lte")
|
||||
const customStartValue = getDateFromComparison(currentDateComparison, "$gte")
|
||||
const customEndValue = getDateFromComparison(currentDateComparison, "$lte")
|
||||
|
||||
const handleCustomDateChange = (
|
||||
value: Date | undefined,
|
||||
pos: "start" | "end"
|
||||
) => {
|
||||
const key = pos === "start" ? "gte" : "lte"
|
||||
const key = pos === "start" ? "$gte" : "$lte"
|
||||
const dateValue = value ? value.toISOString() : undefined
|
||||
|
||||
selectedParams.add(
|
||||
@@ -84,7 +86,7 @@ export const DateFilter = ({
|
||||
}
|
||||
|
||||
const formatCustomDate = (date: Date | undefined) => {
|
||||
return date ? format(date, "dd MMM, yyyy") : undefined
|
||||
return date ? getFullDate({ date: date }) : undefined
|
||||
}
|
||||
|
||||
const getCustomDisplayValue = () => {
|
||||
@@ -194,12 +196,12 @@ export const DateFilter = ({
|
||||
<div>
|
||||
<div className="px-2 py-1">
|
||||
<Text size="xsmall" leading="compact" weight="plus">
|
||||
Starting
|
||||
{t("filters.date.from")}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="px-2 py-1">
|
||||
<DatePicker
|
||||
placeholder="MM/DD/YYYY"
|
||||
// placeholder="MM/DD/YYYY" TODO: Fix DatePicker component not working with placeholder
|
||||
toDate={customEndValue}
|
||||
value={customStartValue}
|
||||
onChange={(d) => handleCustomDateChange(d, "start")}
|
||||
@@ -209,12 +211,12 @@ export const DateFilter = ({
|
||||
<div>
|
||||
<div className="px-2 py-1">
|
||||
<Text size="xsmall" leading="compact" weight="plus">
|
||||
Ending
|
||||
{t("filters.date.to")}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="px-2 py-1">
|
||||
<DatePicker
|
||||
placeholder="MM/DD/YYYY"
|
||||
// placeholder="MM/DD/YYYY"
|
||||
fromDate={customStartValue}
|
||||
value={customEndValue || undefined}
|
||||
onChange={(d) => {
|
||||
@@ -301,13 +303,13 @@ const usePresets = () => {
|
||||
{
|
||||
label: t("filters.date.today"),
|
||||
value: {
|
||||
gte: today.toISOString(),
|
||||
$gte: today.toISOString(),
|
||||
},
|
||||
},
|
||||
{
|
||||
label: t("filters.date.lastSevenDays"),
|
||||
value: {
|
||||
gte: new Date(
|
||||
$gte: new Date(
|
||||
today.getTime() - 7 * 24 * 60 * 60 * 1000
|
||||
).toISOString(), // 7 days ago
|
||||
},
|
||||
@@ -315,7 +317,7 @@ const usePresets = () => {
|
||||
{
|
||||
label: t("filters.date.lastThirtyDays"),
|
||||
value: {
|
||||
gte: new Date(
|
||||
$gte: new Date(
|
||||
today.getTime() - 30 * 24 * 60 * 60 * 1000
|
||||
).toISOString(), // 30 days ago
|
||||
},
|
||||
@@ -323,7 +325,7 @@ const usePresets = () => {
|
||||
{
|
||||
label: t("filters.date.lastNinetyDays"),
|
||||
value: {
|
||||
gte: new Date(
|
||||
$gte: new Date(
|
||||
today.getTime() - 90 * 24 * 60 * 60 * 1000
|
||||
).toISOString(), // 90 days ago
|
||||
},
|
||||
@@ -331,7 +333,7 @@ const usePresets = () => {
|
||||
{
|
||||
label: t("filters.date.lastTwelveMonths"),
|
||||
value: {
|
||||
gte: new Date(
|
||||
$gte: new Date(
|
||||
today.getTime() - 365 * 24 * 60 * 60 * 1000
|
||||
).toISOString(), // 365 days ago
|
||||
},
|
||||
@@ -349,7 +351,7 @@ const parseDateComparison = (value: string[]) => {
|
||||
|
||||
const getDateFromComparison = (
|
||||
comparison: DateComparisonOperator | null,
|
||||
key: "gte" | "lte"
|
||||
key: "$gte" | "$lte"
|
||||
) => {
|
||||
return comparison?.[key] ? new Date(comparison[key] as string) : undefined
|
||||
}
|
||||
|
||||
@@ -7,11 +7,11 @@ import {
|
||||
} from "@tanstack/react-query"
|
||||
import { CreateShippingProfileReq } from "../../types/api-payloads"
|
||||
import {
|
||||
ShippingProfileDeleteRes,
|
||||
ShippingProfileListRes,
|
||||
ShippingProfileRes,
|
||||
} from "../../types/api-responses"
|
||||
|
||||
import { DeleteResponse } from "@medusajs/types"
|
||||
import { client } from "../../lib/client"
|
||||
import { queryClient } from "../../lib/medusa"
|
||||
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 = (
|
||||
query?: Record<string, any>,
|
||||
options?: Omit<
|
||||
@@ -64,11 +83,11 @@ export const useShippingProfiles = (
|
||||
|
||||
export const useDeleteShippingProfile = (
|
||||
profileId: string,
|
||||
options?: UseMutationOptions<ShippingProfileDeleteRes, Error, void>
|
||||
options?: UseMutationOptions<DeleteResponse, Error, void>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: () => client.shippingProfiles.delete(profileId),
|
||||
onSuccess: (data: any, variables: any, context: any) => {
|
||||
onSuccess: (data, variables, context) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: shippingProfileQueryKeys.lists(),
|
||||
})
|
||||
|
||||
@@ -1,27 +1,35 @@
|
||||
import { deleteRequest, getRequest, postRequest } from "./common"
|
||||
import { CreateShippingProfileReq } from "../../types/api-payloads"
|
||||
import {
|
||||
ShippingProfileDeleteRes,
|
||||
ShippingProfileListRes,
|
||||
ShippingProfileRes,
|
||||
} from "../../types/api-responses"
|
||||
import { deleteRequest, getRequest, postRequest } from "./common"
|
||||
|
||||
async function createShippingProfile(payload: CreateShippingProfileReq) {
|
||||
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>) {
|
||||
return getRequest<ShippingProfileListRes>(`/admin/shipping-profiles`, query)
|
||||
}
|
||||
|
||||
async function deleteShippingProfile(profileId: string) {
|
||||
async function deleteShippingProfile(id: string) {
|
||||
return deleteRequest<ShippingProfileDeleteRes>(
|
||||
`/admin/shipping-profiles/${profileId}`
|
||||
`/admin/shipping-profiles/${id}`
|
||||
)
|
||||
}
|
||||
|
||||
export const shippingProfiles = {
|
||||
create: createShippingProfile,
|
||||
retrieve: retrieveShippingProfile,
|
||||
list: listShippingProfiles,
|
||||
create: createShippingProfile,
|
||||
delete: deleteShippingProfile,
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import type {
|
||||
AdminCustomerGroupsRes,
|
||||
AdminCustomersRes,
|
||||
AdminDiscountsRes,
|
||||
AdminDraftOrdersRes,
|
||||
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",
|
||||
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",
|
||||
element: <Outlet />,
|
||||
|
||||
@@ -3,12 +3,12 @@ import { RegionDTO } from "@medusajs/types"
|
||||
import { Badge, Container, Heading, Text, usePrompt } from "@medusajs/ui"
|
||||
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 { 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 = {
|
||||
region: RegionDTO
|
||||
@@ -23,7 +23,7 @@ export const RegionGeneralSection = ({ region }: RegionGeneralSectionProps) => {
|
||||
<Heading>{region.name}</Heading>
|
||||
<RegionActions region={region} />
|
||||
</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">
|
||||
{t("fields.currency")}
|
||||
</Text>
|
||||
@@ -36,7 +36,7 @@ export const RegionGeneralSection = ({ region }: RegionGeneralSectionProps) => {
|
||||
</Text>
|
||||
</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">
|
||||
{t("fields.paymentProviders")}
|
||||
</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 { Button, Heading, Input, Text, toast } from "@medusajs/ui"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import * as zod from "zod"
|
||||
|
||||
import { Form } from "../../../../../components/common/form"
|
||||
import {
|
||||
RouteFocusModal,
|
||||
useRouteModal,
|
||||
} from "../../../../../components/route-modal"
|
||||
import { Form } from "../../../../../components/common/form"
|
||||
import { useCreateShippingProfile } from "../../../../../hooks/api/shipping-profiles"
|
||||
|
||||
const CreateShippingOptionsSchema = zod.object({
|
||||
@@ -31,11 +31,30 @@ export function CreateShippingProfileForm() {
|
||||
const { mutateAsync, isPending } = useCreateShippingProfile()
|
||||
|
||||
const handleSubmit = form.handleSubmit(async (values) => {
|
||||
await mutateAsync({
|
||||
name: values.name,
|
||||
type: values.type,
|
||||
})
|
||||
handleSuccess("/shipping-profiles")
|
||||
await mutateAsync(
|
||||
{
|
||||
name: values.name,
|
||||
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(`/settings/shipping-profiles/${shipping_profile.id}`)
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(t("general.error"), {
|
||||
description: error.message,
|
||||
dismissLabel: t("actions.close"),
|
||||
})
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
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>
|
||||
<Heading className="capitalize">
|
||||
{t("shippingProfile.title")}
|
||||
{t("shippingProfile.create.header")}
|
||||
</Heading>
|
||||
<Text size="small" className="text-ui-fg-subtle">
|
||||
{t("shippingProfile.detailsHint")}
|
||||
{t("shippingProfile.create.hint")}
|
||||
</Text>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
@@ -89,7 +108,7 @@ export function CreateShippingProfileForm() {
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label tooltip={t("shippingProfile.typeHint")}>
|
||||
<Form.Label tooltip={t("shippingProfile.tooltip.type")}>
|
||||
{t("fields.type")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./shipping-profile-general-section"
|
||||
@@ -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 { usePrompt } from "@medusajs/ui"
|
||||
import { AdminShippingProfileResponse } from "@medusajs/types"
|
||||
import { toast, usePrompt } from "@medusajs/ui"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { ShippingProfileDTO } from "@medusajs/types"
|
||||
|
||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||
import { useDeleteShippingProfile } from "../../../../../hooks/api/shipping-profiles"
|
||||
@@ -9,17 +9,17 @@ import { useDeleteShippingProfile } from "../../../../../hooks/api/shipping-prof
|
||||
export const ShippingOptionsRowActions = ({
|
||||
profile,
|
||||
}: {
|
||||
profile: ShippingProfileDTO
|
||||
profile: AdminShippingProfileResponse["shipping_profile"]
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const prompt = usePrompt()
|
||||
// TODO: MISSING ENDPOINT
|
||||
|
||||
const { mutateAsync } = useDeleteShippingProfile(profile.id)
|
||||
|
||||
const handleDelete = async () => {
|
||||
const res = await prompt({
|
||||
title: t("general.areYouSure"),
|
||||
description: t("shippingProfile.deleteWaring", {
|
||||
title: t("shippingProfile.delete.title"),
|
||||
description: t("shippingProfile.delete.description", {
|
||||
name: profile.name,
|
||||
}),
|
||||
verificationText: profile.name,
|
||||
@@ -32,7 +32,22 @@ export const ShippingOptionsRowActions = ({
|
||||
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 (
|
||||
@@ -1,29 +1,31 @@
|
||||
import { Button, Container, Heading } from "@medusajs/ui"
|
||||
import { Link } from "react-router-dom"
|
||||
|
||||
import { keepPreviousData } from "@tanstack/react-query"
|
||||
import { useTranslation } from "react-i18next"
|
||||
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 { 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
|
||||
|
||||
export const ShippingProfileListTable = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { raw, searchParams } = useShippingProfilesTableQuery({
|
||||
const { raw, searchParams } = useShippingProfileTableQuery({
|
||||
pageSize: PAGE_SIZE,
|
||||
})
|
||||
|
||||
const { shipping_profiles, count, isLoading, isError, error } =
|
||||
useShippingProfiles({
|
||||
...searchParams,
|
||||
useShippingProfiles(searchParams, {
|
||||
placeholderData: keepPreviousData,
|
||||
})
|
||||
|
||||
const columns = useShippingProfilesTableColumns()
|
||||
const columns = useShippingProfileTableColumns()
|
||||
const filters = useShippingProfileTableFilters()
|
||||
|
||||
const { table } = useDataTable({
|
||||
data: shipping_profiles,
|
||||
@@ -38,8 +40,6 @@ export const ShippingProfileListTable = () => {
|
||||
throw error
|
||||
}
|
||||
|
||||
const noData = !isLoading && !shipping_profiles.length
|
||||
|
||||
return (
|
||||
<Container className="divide-y p-0">
|
||||
<div className="flex items-center justify-between px-6 py-4">
|
||||
@@ -50,19 +50,19 @@ export const ShippingProfileListTable = () => {
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
{noData ? (
|
||||
<NoRecords className="h-[180px]" title={t("general.noRecordsFound")} />
|
||||
) : (
|
||||
<DataTable
|
||||
table={table}
|
||||
pageSize={PAGE_SIZE}
|
||||
count={count || 1}
|
||||
columns={columns}
|
||||
isLoading={isLoading}
|
||||
queryObject={raw}
|
||||
pagination
|
||||
/>
|
||||
)}
|
||||
<DataTable
|
||||
table={table}
|
||||
pageSize={PAGE_SIZE}
|
||||
count={count}
|
||||
columns={columns}
|
||||
filters={filters}
|
||||
orderBy={["name", "type", "created_at", "updated_at"]}
|
||||
isLoading={isLoading}
|
||||
navigateTo={(row) => row.id}
|
||||
queryObject={raw}
|
||||
search
|
||||
pagination
|
||||
/>
|
||||
</Container>
|
||||
)
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
import { AdminShippingProfileResponse } from "@medusajs/types"
|
||||
import { createColumnHelper } from "@tanstack/react-table"
|
||||
import { useMemo } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { ShippingProfileDTO } from "@medusajs/types"
|
||||
|
||||
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()
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
DALUtils,
|
||||
generateEntityId,
|
||||
Searchable,
|
||||
} from "@medusajs/utils"
|
||||
|
||||
import { DAL } from "@medusajs/types"
|
||||
@@ -41,10 +42,12 @@ export default class ShippingProfile {
|
||||
@PrimaryKey({ columnType: "text" })
|
||||
id: string
|
||||
|
||||
@Searchable()
|
||||
@Property({ columnType: "text" })
|
||||
@ShippingProfileTypeIndex.MikroORMIndex()
|
||||
name: string
|
||||
|
||||
@Searchable()
|
||||
@Property({ columnType: "text" })
|
||||
type: string
|
||||
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { z } from "zod"
|
||||
import { createFindParams, createSelectParams } from "../../utils/validators"
|
||||
import {
|
||||
createFindParams,
|
||||
createOperatorMap,
|
||||
createSelectParams,
|
||||
} from "../../utils/validators"
|
||||
|
||||
export type AdminGetShippingProfileParamsType = z.infer<
|
||||
typeof AdminGetShippingProfileParams
|
||||
@@ -14,8 +18,14 @@ export const AdminGetShippingProfilesParams = createFindParams({
|
||||
offset: 0,
|
||||
}).merge(
|
||||
z.object({
|
||||
q: z.string().optional(),
|
||||
type: 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(),
|
||||
})
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user