feat: Improvements to the products details page in the admin (#6994)
This commit is contained in:
@@ -260,6 +260,9 @@
|
||||
"edit": {
|
||||
"header": "Edit Variant"
|
||||
},
|
||||
"create": {
|
||||
"header": "Create Variant"
|
||||
},
|
||||
"inventory": {
|
||||
"header": "Stock & Inventory",
|
||||
"manageInventoryLabel": "Manage inventory",
|
||||
|
||||
@@ -28,6 +28,9 @@ export const useCreateProductOption = (
|
||||
client.products.createOption(productId, payload),
|
||||
onSuccess: (data: any, variables: any, context: any) => {
|
||||
queryClient.invalidateQueries({ queryKey: optionsQueryKeys.lists() })
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: productsQueryKeys.detail(productId),
|
||||
})
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
@@ -47,6 +50,9 @@ export const useUpdateProductOption = (
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: optionsQueryKeys.detail(optionId),
|
||||
})
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: productsQueryKeys.detail(productId),
|
||||
})
|
||||
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
@@ -62,9 +68,12 @@ export const useDeleteProductOption = (
|
||||
return useMutation({
|
||||
mutationFn: () => client.products.deleteOption(productId, optionId),
|
||||
onSuccess: (data: any, variables: any, context: any) => {
|
||||
queryClient.invalidateQueries({ queryKey: productsQueryKeys.lists() })
|
||||
queryClient.invalidateQueries({ queryKey: optionsQueryKeys.lists() })
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: productsQueryKeys.detail(optionId),
|
||||
queryKey: optionsQueryKeys.detail(optionId),
|
||||
})
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: productsQueryKeys.detail(productId),
|
||||
})
|
||||
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
@@ -108,6 +117,24 @@ export const useProductVariants = (
|
||||
return { ...data, ...rest }
|
||||
}
|
||||
|
||||
export const useCreateProductVariant = (
|
||||
productId: string,
|
||||
options?: UseMutationOptions<any, Error, any>
|
||||
) => {
|
||||
return useMutation({
|
||||
mutationFn: (payload: any) =>
|
||||
client.products.createVariant(productId, payload),
|
||||
onSuccess: (data: any, variables: any, context: any) => {
|
||||
queryClient.invalidateQueries({ queryKey: variantsQueryKeys.lists() })
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: productsQueryKeys.detail(productId),
|
||||
})
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
...options,
|
||||
})
|
||||
}
|
||||
|
||||
export const useUpdateProductVariant = (
|
||||
productId: string,
|
||||
variantId: string,
|
||||
@@ -121,6 +148,9 @@ export const useUpdateProductVariant = (
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: variantsQueryKeys.detail(variantId),
|
||||
})
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: productsQueryKeys.detail(productId),
|
||||
})
|
||||
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
@@ -140,6 +170,9 @@ export const useDeleteVariant = (
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: variantsQueryKeys.detail(variantId),
|
||||
})
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: productsQueryKeys.detail(productId),
|
||||
})
|
||||
|
||||
options?.onSuccess?.(data, variables, context)
|
||||
},
|
||||
|
||||
@@ -36,6 +36,10 @@ async function listVariants(productId: string, query?: Record<string, any>) {
|
||||
return getRequest<any>(`/admin/products/${productId}/variants`, query)
|
||||
}
|
||||
|
||||
async function createVariant(productId: string, payload: any) {
|
||||
return postRequest<any>(`/admin/products/${productId}/variants`, payload)
|
||||
}
|
||||
|
||||
async function updateVariant(
|
||||
productId: string,
|
||||
variantId: string,
|
||||
@@ -76,6 +80,7 @@ export const products = {
|
||||
delete: deleteProduct,
|
||||
retrieveVariant,
|
||||
listVariants,
|
||||
createVariant,
|
||||
updateVariant,
|
||||
deleteVariant,
|
||||
createOption,
|
||||
|
||||
@@ -104,6 +104,16 @@ export const v2Routes: RouteObject[] = [
|
||||
lazy: () =>
|
||||
import("../../v2-routes/products/product-attributes"),
|
||||
},
|
||||
{
|
||||
path: "organization",
|
||||
lazy: () =>
|
||||
import("../../v2-routes/products/product-organization"),
|
||||
},
|
||||
{
|
||||
path: "media",
|
||||
lazy: () =>
|
||||
import("../../v2-routes/products/product-media"),
|
||||
},
|
||||
{
|
||||
path: "options/create",
|
||||
lazy: () =>
|
||||
@@ -115,9 +125,9 @@ export const v2Routes: RouteObject[] = [
|
||||
import("../../v2-routes/products/product-edit-option"),
|
||||
},
|
||||
{
|
||||
path: "media",
|
||||
path: "variants/create",
|
||||
lazy: () =>
|
||||
import("../../v2-routes/products/product-media"),
|
||||
import("../../v2-routes/products/product-create-variant"),
|
||||
},
|
||||
{
|
||||
path: "variants/:variant_id/edit",
|
||||
|
||||
@@ -17,6 +17,7 @@ type EditProductOptionsFormProps = {
|
||||
|
||||
const CreateProductOptionSchema = z.object({
|
||||
title: z.string().min(1),
|
||||
values: z.array(z.string()).optional(),
|
||||
})
|
||||
|
||||
export const CreateProductOptionForm = ({
|
||||
@@ -28,6 +29,7 @@ export const CreateProductOptionForm = ({
|
||||
const form = useForm<z.infer<typeof CreateProductOptionSchema>>({
|
||||
defaultValues: {
|
||||
title: "",
|
||||
values: [],
|
||||
},
|
||||
resolver: zodResolver(CreateProductOptionSchema),
|
||||
})
|
||||
@@ -55,7 +57,9 @@ export const CreateProductOptionForm = ({
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>{t("fields.title")}</Form.Label>
|
||||
<Form.Label>
|
||||
{t("products.fields.options.optionTitle")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
<Input {...field} />
|
||||
</Form.Control>
|
||||
@@ -64,6 +68,30 @@ export const CreateProductOptionForm = ({
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="values"
|
||||
render={({ field: { value, onChange, ...field } }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>
|
||||
{t("products.fields.options.variations")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
<Input
|
||||
{...field}
|
||||
value={(value ?? []).join(",")}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value
|
||||
onChange(val.split(",").map((v) => v.trim()))
|
||||
}}
|
||||
/>
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</RouteDrawer.Body>
|
||||
<RouteDrawer.Footer>
|
||||
<div className="flex items-center justify-end gap-x-2">
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { Product } from "@medusajs/medusa"
|
||||
import { Button, Input } from "@medusajs/ui"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { z } from "zod"
|
||||
import { Form } from "../../../../../components/common/form"
|
||||
import {
|
||||
RouteDrawer,
|
||||
useRouteModal,
|
||||
} from "../../../../../components/route-modal"
|
||||
import { useCreateProductVariant } from "../../../../../hooks/api/products"
|
||||
|
||||
type EditProductVariantsFormProps = {
|
||||
product: Product
|
||||
}
|
||||
|
||||
const CreateProductVariantSchema = z.object({
|
||||
title: z.string().min(1),
|
||||
values: z.array(z.string()).optional(),
|
||||
})
|
||||
|
||||
export const CreateProductVariantForm = ({
|
||||
product,
|
||||
}: EditProductVariantsFormProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { handleSuccess } = useRouteModal()
|
||||
|
||||
const form = useForm<z.infer<typeof CreateProductVariantSchema>>({
|
||||
defaultValues: {
|
||||
title: "",
|
||||
values: [],
|
||||
},
|
||||
resolver: zodResolver(CreateProductVariantSchema),
|
||||
})
|
||||
|
||||
const { mutateAsync, isLoading } = useCreateProductVariant(product.id)
|
||||
|
||||
const handleSubmit = form.handleSubmit(async (values) => {
|
||||
mutateAsync(values, {
|
||||
onSuccess: () => {
|
||||
handleSuccess()
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
return (
|
||||
<RouteDrawer.Form form={form}>
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="flex flex-1 flex-col overflow-hidden"
|
||||
>
|
||||
<RouteDrawer.Body className="flex flex-1 flex-col gap-y-8 overflow-auto">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="title"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>title</Form.Label>
|
||||
<Form.Control>
|
||||
<Input {...field} />
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="values"
|
||||
render={({ field: { value, onChange, ...field } }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>value</Form.Label>
|
||||
<Form.Control>
|
||||
<Input
|
||||
{...field}
|
||||
value={(value ?? []).join(",")}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value
|
||||
onChange(val.split(",").map((v) => v.trim()))
|
||||
}}
|
||||
/>
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</RouteDrawer.Body>
|
||||
<RouteDrawer.Footer>
|
||||
<div className="flex items-center justify-end gap-x-2">
|
||||
<RouteDrawer.Close asChild>
|
||||
<Button variant="secondary" size="small">
|
||||
{t("actions.cancel")}
|
||||
</Button>
|
||||
</RouteDrawer.Close>
|
||||
<Button type="submit" size="small" isLoading={isLoading}>
|
||||
{t("actions.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</RouteDrawer.Footer>
|
||||
</form>
|
||||
</RouteDrawer.Form>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./create-product-variant-form"
|
||||
@@ -0,0 +1 @@
|
||||
export { ProductCreateVariant as Component } from "./product-create-variant"
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Heading } from "@medusajs/ui"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { useParams } from "react-router-dom"
|
||||
import { RouteDrawer } from "../../../components/route-modal"
|
||||
import { useProduct } from "../../../hooks/api/products"
|
||||
import { CreateProductVariantForm } from "./components/create-product-variant-form"
|
||||
|
||||
export const ProductCreateVariant = () => {
|
||||
const { id } = useParams()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { product, isLoading, isError, error } = useProduct(id!)
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return (
|
||||
<RouteDrawer>
|
||||
<RouteDrawer.Header>
|
||||
<Heading>{t("products.variant.create.header")}</Heading>
|
||||
</RouteDrawer.Header>
|
||||
{!isLoading && product && <CreateProductVariantForm product={product} />}
|
||||
</RouteDrawer>
|
||||
)
|
||||
}
|
||||
@@ -17,6 +17,7 @@ type EditProductOptionFormProps = {
|
||||
|
||||
const CreateProductOptionSchema = z.object({
|
||||
title: z.string().min(1),
|
||||
values: z.array(z.string()).optional(),
|
||||
})
|
||||
|
||||
export const CreateProductOptionForm = ({
|
||||
@@ -28,6 +29,7 @@ export const CreateProductOptionForm = ({
|
||||
const form = useForm<z.infer<typeof CreateProductOptionSchema>>({
|
||||
defaultValues: {
|
||||
title: option.title,
|
||||
values: option.values.map((v: any) => v.value),
|
||||
},
|
||||
resolver: zodResolver(CreateProductOptionSchema),
|
||||
})
|
||||
@@ -40,7 +42,7 @@ export const CreateProductOptionForm = ({
|
||||
const handleSubmit = form.handleSubmit(async (values) => {
|
||||
mutateAsync(
|
||||
{
|
||||
option_id: option.id,
|
||||
id: option.id,
|
||||
...values,
|
||||
},
|
||||
{
|
||||
@@ -64,7 +66,9 @@ export const CreateProductOptionForm = ({
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>{t("fields.title")}</Form.Label>
|
||||
<Form.Label>
|
||||
{t("products.fields.options.optionTitle")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
<Input {...field} />
|
||||
</Form.Control>
|
||||
@@ -73,6 +77,30 @@ export const CreateProductOptionForm = ({
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="values"
|
||||
render={({ field: { value, onChange, ...field } }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label>
|
||||
{t("products.fields.options.variations")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
<Input
|
||||
{...field}
|
||||
value={(value ?? []).join(",")}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value
|
||||
onChange(val.split(",").map((v) => v.trim()))
|
||||
}}
|
||||
/>
|
||||
</Form.Control>
|
||||
<Form.ErrorMessage />
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</RouteDrawer.Body>
|
||||
<RouteDrawer.Footer>
|
||||
<div className="flex items-center justify-end gap-x-2">
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./product-organization-form"
|
||||
@@ -0,0 +1,205 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod"
|
||||
import { Product } from "@medusajs/medusa"
|
||||
import { Button, Input, Select } from "@medusajs/ui"
|
||||
import { useForm } from "react-hook-form"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import * as zod from "zod"
|
||||
import { CountrySelect } from "../../../../../components/common/country-select"
|
||||
import { Form } from "../../../../../components/common/form"
|
||||
import {
|
||||
RouteDrawer,
|
||||
useRouteModal,
|
||||
} from "../../../../../components/route-modal"
|
||||
import { useUpdateProduct } from "../../../../../hooks/api/products"
|
||||
import { Combobox } from "../../../../../components/common/combobox"
|
||||
import { useProductTypes } from "../../../../../hooks/api/product-types"
|
||||
import { useTags } from "../../../../../hooks/api/tags"
|
||||
import { useCollections } from "../../../../../hooks/api/collections"
|
||||
import { useCategories } from "../../../../../hooks/api/categories"
|
||||
|
||||
type ProductOrganizationFormProps = {
|
||||
product: Product
|
||||
}
|
||||
|
||||
const ProductOrganizationSchema = zod.object({
|
||||
type_id: zod.string().optional(),
|
||||
collection_id: zod.string().optional(),
|
||||
category_ids: zod.array(zod.string()).optional(),
|
||||
tags: zod.array(zod.string()).optional(),
|
||||
})
|
||||
|
||||
export const ProductOrganizationForm = ({
|
||||
product,
|
||||
}: ProductOrganizationFormProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { handleSuccess } = useRouteModal()
|
||||
const { product_types, isLoading: isLoadingTypes } = useProductTypes()
|
||||
const { product_tags, isLoading: isLoadingTags } = useTags()
|
||||
const { collections, isLoading: isLoadingCollections } = useCollections()
|
||||
const { product_categories, isLoading: isLoadingCategories } = useCategories()
|
||||
|
||||
const form = useForm<zod.infer<typeof ProductOrganizationSchema>>({
|
||||
defaultValues: {
|
||||
type_id: product.type_id || undefined,
|
||||
collection_id: product.collection_id || undefined,
|
||||
category_ids: product.categories?.map((c) => c.id) || undefined,
|
||||
tags: product.tags?.map((t) => t.id) || undefined,
|
||||
},
|
||||
resolver: zodResolver(ProductOrganizationSchema),
|
||||
})
|
||||
|
||||
const { mutateAsync, isLoading } = useUpdateProduct(product.id)
|
||||
|
||||
const handleSubmit = form.handleSubmit(async (data) => {
|
||||
await mutateAsync(
|
||||
{
|
||||
type_id: data.type_id || undefined,
|
||||
collection_id: data.collection_id || undefined,
|
||||
category_ids: data.category_ids || undefined,
|
||||
tags:
|
||||
data.tags?.map((t) => {
|
||||
id: t
|
||||
}) || undefined,
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
handleSuccess()
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
return (
|
||||
<RouteDrawer.Form form={form}>
|
||||
<form onSubmit={handleSubmit} className="flex h-full flex-col">
|
||||
<RouteDrawer.Body>
|
||||
<div className="flex h-full flex-col gap-y-8">
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="type_id"
|
||||
render={({ field: { onChange, ...field } }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label optional>
|
||||
{t("products.fields.type.label")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
<Select
|
||||
disabled={isLoadingTypes}
|
||||
{...field}
|
||||
onValueChange={onChange}
|
||||
>
|
||||
<Select.Trigger ref={field.ref}>
|
||||
<Select.Value />
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{(product_types ?? []).map((type) => (
|
||||
<Select.Item key={type.id} value={type.id}>
|
||||
{type.value}
|
||||
</Select.Item>
|
||||
))}
|
||||
</Select.Content>
|
||||
</Select>
|
||||
</Form.Control>
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="collection_id"
|
||||
render={({ field: { onChange, ...field } }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label optional>
|
||||
{t("products.fields.collection.label")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
<Select
|
||||
disabled={isLoadingCollections}
|
||||
{...field}
|
||||
onValueChange={onChange}
|
||||
>
|
||||
<Select.Trigger ref={field.ref}>
|
||||
<Select.Value />
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{(collections ?? []).map((collection) => (
|
||||
<Select.Item
|
||||
key={collection.id}
|
||||
value={collection.id}
|
||||
>
|
||||
{collection.title}
|
||||
</Select.Item>
|
||||
))}
|
||||
</Select.Content>
|
||||
</Select>
|
||||
</Form.Control>
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="category_ids"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label optional>
|
||||
{t("products.fields.categories.label")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
<Combobox
|
||||
disabled={isLoadingCategories}
|
||||
options={(product_categories ?? []).map((category) => ({
|
||||
label: category.name,
|
||||
value: category.id,
|
||||
}))}
|
||||
{...field}
|
||||
/>
|
||||
</Form.Control>
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<Form.Field
|
||||
control={form.control}
|
||||
name="tags"
|
||||
render={({ field }) => {
|
||||
return (
|
||||
<Form.Item>
|
||||
<Form.Label optional>
|
||||
{t("products.fields.tags.label")}
|
||||
</Form.Label>
|
||||
<Form.Control>
|
||||
<Combobox
|
||||
disabled={isLoadingTags}
|
||||
options={(product_tags ?? []).map((tag) => ({
|
||||
label: tag.value,
|
||||
value: tag.id,
|
||||
}))}
|
||||
{...field}
|
||||
/>
|
||||
</Form.Control>
|
||||
</Form.Item>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</RouteDrawer.Body>
|
||||
<RouteDrawer.Footer>
|
||||
<div className="flex items-center justify-end gap-x-2">
|
||||
<RouteDrawer.Close asChild>
|
||||
<Button size="small" variant="secondary">
|
||||
{t("actions.cancel")}
|
||||
</Button>
|
||||
</RouteDrawer.Close>
|
||||
<Button size="small" type="submit" isLoading={isLoading}>
|
||||
{t("actions.save")}
|
||||
</Button>
|
||||
</div>
|
||||
</RouteDrawer.Footer>
|
||||
</form>
|
||||
</RouteDrawer.Form>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { ProductOrganization as Component } from "./product-organization"
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Heading } from "@medusajs/ui"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { useParams } from "react-router-dom"
|
||||
|
||||
import { RouteDrawer } from "../../../components/route-modal"
|
||||
import { useProduct } from "../../../hooks/api/products"
|
||||
import { ProductOrganizationForm } from "./components/product-organization-form"
|
||||
|
||||
export const ProductOrganization = () => {
|
||||
const { id } = useParams()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { product, isLoading, isError, error } = useProduct(id!)
|
||||
|
||||
if (isError) {
|
||||
throw error
|
||||
}
|
||||
|
||||
return (
|
||||
<RouteDrawer>
|
||||
<RouteDrawer.Header>
|
||||
<Heading>{t("products.editOrganization")}</Heading>
|
||||
</RouteDrawer.Header>
|
||||
{!isLoading && product && <ProductOrganizationForm product={product} />}
|
||||
</RouteDrawer>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user