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
2024-04-19 16:11:32 +02:00
committed by GitHub
parent 9bd2d30595
commit e2fabc1c05
31 changed files with 536 additions and 136 deletions

View File

@@ -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",

View File

@@ -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}

View File

@@ -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>
)
}

View File

@@ -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",
},
],
},
]
}

View File

@@ -53,6 +53,10 @@ const useSettingRoutes = (): NavItemProps[] => {
label: t("salesChannels.domain"),
to: "/settings/sales-channels",
},
{
label: t("shippingProfile.domain"),
to: "/settings/shipping-profiles",
},
],
[t]
)

View File

@@ -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
}

View File

@@ -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(),
})

View File

@@ -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,
}

View File

@@ -1,6 +1,4 @@
import type {
AdminCustomerGroupsRes,
AdminCustomersRes,
AdminDiscountsRes,
AdminDraftOrdersRes,
AdminGiftCardsRes,

View File

@@ -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 />,

View File

@@ -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>

View File

@@ -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>

View File

@@ -0,0 +1 @@
export * from "./shipping-profile-general-section"

View File

@@ -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>
)
}

View File

@@ -0,0 +1 @@
export { ShippingProfileDetail as Component } from "./shipping-profile-detail"

View File

@@ -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>
)
}

View File

@@ -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 (

View File

@@ -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>
)
}

View File

@@ -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(

View File

@@ -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
}

View File

@@ -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,
}
}

View File

@@ -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,
}
}

View File

@@ -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

View File

@@ -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(),
})
)