feat: Revamp product details page and several product fixes and cleanups (#6988)
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
export * from "./section-row"
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { Text } from "@medusajs/ui"
|
||||||
|
|
||||||
|
export type SectionRowProps = {
|
||||||
|
title: string
|
||||||
|
value?: React.ReactNode | string | null
|
||||||
|
actions?: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SectionRow = ({ title, value, actions }: SectionRowProps) => {
|
||||||
|
const isValueString = typeof value === "string" || !value
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`text-ui-fg-subtle grid ${
|
||||||
|
!!actions ? "grid-cols-[1fr_1fr_28px]" : "grid-cols-2"
|
||||||
|
} items-center px-6 py-4`}
|
||||||
|
>
|
||||||
|
<Text size="small" weight="plus" leading="compact">
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{isValueString ? (
|
||||||
|
<Text size="small" leading="compact" className="text-pretty">
|
||||||
|
{value ?? "-"}
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-wrap gap-1">{value}</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{actions && <div>{actions}</div>}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -7,11 +7,7 @@ import {
|
|||||||
} from "@tanstack/react-query"
|
} from "@tanstack/react-query"
|
||||||
import { client } from "../../lib/client"
|
import { client } from "../../lib/client"
|
||||||
import { queryKeysFactory } from "../../lib/query-key-factory"
|
import { queryKeysFactory } from "../../lib/query-key-factory"
|
||||||
import {
|
import { ProductDeleteRes, ProductRes } from "../../types/api-responses"
|
||||||
ProductDeleteRes,
|
|
||||||
ProductListRes,
|
|
||||||
ProductRes,
|
|
||||||
} from "../../types/api-responses"
|
|
||||||
import { queryClient } from "../../lib/medusa"
|
import { queryClient } from "../../lib/medusa"
|
||||||
|
|
||||||
const PRODUCTS_QUERY_KEY = "products" as const
|
const PRODUCTS_QUERY_KEY = "products" as const
|
||||||
@@ -20,6 +16,63 @@ export const productsQueryKeys = queryKeysFactory(PRODUCTS_QUERY_KEY)
|
|||||||
const VARIANTS_QUERY_KEY = "product_variants" as const
|
const VARIANTS_QUERY_KEY = "product_variants" as const
|
||||||
export const variantsQueryKeys = queryKeysFactory(VARIANTS_QUERY_KEY)
|
export const variantsQueryKeys = queryKeysFactory(VARIANTS_QUERY_KEY)
|
||||||
|
|
||||||
|
const OPTIONS_QUERY_KEY = "product_options" as const
|
||||||
|
export const optionsQueryKeys = queryKeysFactory(OPTIONS_QUERY_KEY)
|
||||||
|
|
||||||
|
export const useCreateProductOption = (
|
||||||
|
productId: string,
|
||||||
|
options?: UseMutationOptions<any, Error, any>
|
||||||
|
) => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (payload: any) =>
|
||||||
|
client.products.createOption(productId, payload),
|
||||||
|
onSuccess: (data: any, variables: any, context: any) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: optionsQueryKeys.lists() })
|
||||||
|
options?.onSuccess?.(data, variables, context)
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useUpdateProductOption = (
|
||||||
|
productId: string,
|
||||||
|
optionId: string,
|
||||||
|
options?: UseMutationOptions<any, Error, any>
|
||||||
|
) => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (payload: any) =>
|
||||||
|
client.products.updateOption(productId, optionId, payload),
|
||||||
|
onSuccess: (data: any, variables: any, context: any) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: optionsQueryKeys.lists() })
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: optionsQueryKeys.detail(optionId),
|
||||||
|
})
|
||||||
|
|
||||||
|
options?.onSuccess?.(data, variables, context)
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useDeleteProductOption = (
|
||||||
|
productId: string,
|
||||||
|
optionId: string,
|
||||||
|
options?: UseMutationOptions<any, Error, void>
|
||||||
|
) => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: () => client.products.deleteOption(productId, optionId),
|
||||||
|
onSuccess: (data: any, variables: any, context: any) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: productsQueryKeys.lists() })
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: productsQueryKeys.detail(optionId),
|
||||||
|
})
|
||||||
|
|
||||||
|
options?.onSuccess?.(data, variables, context)
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export const useProductVariant = (
|
export const useProductVariant = (
|
||||||
productId: string,
|
productId: string,
|
||||||
variantId: string,
|
variantId: string,
|
||||||
@@ -55,6 +108,26 @@ export const useProductVariants = (
|
|||||||
return { ...data, ...rest }
|
return { ...data, ...rest }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const useUpdateProductVariant = (
|
||||||
|
productId: string,
|
||||||
|
variantId: string,
|
||||||
|
options?: UseMutationOptions<any, Error, any>
|
||||||
|
) => {
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (payload: any) =>
|
||||||
|
client.products.updateVariant(productId, variantId, payload),
|
||||||
|
onSuccess: (data: any, variables: any, context: any) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: variantsQueryKeys.lists() })
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: variantsQueryKeys.detail(variantId),
|
||||||
|
})
|
||||||
|
|
||||||
|
options?.onSuccess?.(data, variables, context)
|
||||||
|
},
|
||||||
|
...options,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export const useDeleteVariant = (
|
export const useDeleteVariant = (
|
||||||
productId: string,
|
productId: string,
|
||||||
variantId: string,
|
variantId: string,
|
||||||
@@ -78,7 +151,7 @@ export const useProduct = (
|
|||||||
id: string,
|
id: string,
|
||||||
query?: Record<string, any>,
|
query?: Record<string, any>,
|
||||||
options?: Omit<
|
options?: Omit<
|
||||||
UseQueryOptions<ProductRes, Error, ProductRes, QueryKey>,
|
UseQueryOptions<any, Error, any, QueryKey>,
|
||||||
"queryFn" | "queryKey"
|
"queryFn" | "queryKey"
|
||||||
>
|
>
|
||||||
) => {
|
) => {
|
||||||
@@ -94,7 +167,7 @@ export const useProduct = (
|
|||||||
export const useProducts = (
|
export const useProducts = (
|
||||||
query?: Record<string, any>,
|
query?: Record<string, any>,
|
||||||
options?: Omit<
|
options?: Omit<
|
||||||
UseQueryOptions<ProductListRes, Error, ProductListRes, QueryKey>,
|
UseQueryOptions<any, Error, any, QueryKey>,
|
||||||
"queryFn" | "queryKey"
|
"queryFn" | "queryKey"
|
||||||
>
|
>
|
||||||
) => {
|
) => {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Address } from "@medusajs/medusa"
|
import { Address } from "@medusajs/medusa"
|
||||||
|
import { countries } from "./countries"
|
||||||
|
|
||||||
export const isSameAddress = (a: Address | null, b: Address | null) => {
|
export const isSameAddress = (a: Address | null, b: Address | null) => {
|
||||||
if (!a || !b) {
|
if (!a || !b) {
|
||||||
@@ -75,3 +76,12 @@ export const getFormattedAddress = ({
|
|||||||
|
|
||||||
return formattedAddress
|
return formattedAddress
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getFormattedCountry = (countryCode: string | null | undefined) => {
|
||||||
|
if (!countryCode) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
const country = countries.find((c) => c.iso_2 === countryCode)
|
||||||
|
return country ? country.display_name : countryCode
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,16 +1,12 @@
|
|||||||
import {
|
import { ProductDeleteRes, ProductListRes } from "../../types/api-responses"
|
||||||
ProductDeleteRes,
|
|
||||||
ProductListRes,
|
|
||||||
ProductRes,
|
|
||||||
} from "../../types/api-responses"
|
|
||||||
import { deleteRequest, getRequest, postRequest } from "./common"
|
import { deleteRequest, getRequest, postRequest } from "./common"
|
||||||
|
|
||||||
async function retrieveProduct(id: string, query?: Record<string, any>) {
|
async function retrieveProduct(id: string, query?: Record<string, any>) {
|
||||||
return getRequest<ProductRes>(`/admin/products/${id}`, query)
|
return getRequest<any>(`/admin/products/${id}`, query)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createProduct(payload: any) {
|
async function createProduct(payload: any) {
|
||||||
return postRequest<ProductRes>(`/admin/products`, payload)
|
return postRequest<any>(`/admin/products`, payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function listProducts(query?: Record<string, any>) {
|
async function listProducts(query?: Record<string, any>) {
|
||||||
@@ -18,7 +14,7 @@ async function listProducts(query?: Record<string, any>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function updateProduct(id: string, payload: any) {
|
async function updateProduct(id: string, payload: any) {
|
||||||
return postRequest<ProductRes>(`/admin/products/${id}`, payload)
|
return postRequest<any>(`/admin/products/${id}`, payload)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteProduct(id: string) {
|
async function deleteProduct(id: string) {
|
||||||
@@ -40,12 +36,38 @@ async function listVariants(productId: string, query?: Record<string, any>) {
|
|||||||
return getRequest<any>(`/admin/products/${productId}/variants`, query)
|
return getRequest<any>(`/admin/products/${productId}/variants`, query)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function updateVariant(
|
||||||
|
productId: string,
|
||||||
|
variantId: string,
|
||||||
|
payload: any
|
||||||
|
) {
|
||||||
|
return postRequest<any>(
|
||||||
|
`/admin/products/${productId}/variants/${variantId}`,
|
||||||
|
payload
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
async function deleteVariant(productId: string, variantId: string) {
|
async function deleteVariant(productId: string, variantId: string) {
|
||||||
return deleteRequest<any>(
|
return deleteRequest<any>(
|
||||||
`/admin/products/${productId}/variants/${variantId}`
|
`/admin/products/${productId}/variants/${variantId}`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function createOption(productId: string, payload: any) {
|
||||||
|
return postRequest<any>(`/admin/products/${productId}/options`, payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateOption(productId: string, optionId: string, payload: any) {
|
||||||
|
return postRequest<any>(
|
||||||
|
`/admin/products/${productId}/options/${optionId}`,
|
||||||
|
payload
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteOption(productId: string, optionId: string) {
|
||||||
|
return deleteRequest<any>(`/admin/products/${productId}/options/${optionId}`)
|
||||||
|
}
|
||||||
|
|
||||||
export const products = {
|
export const products = {
|
||||||
retrieve: retrieveProduct,
|
retrieve: retrieveProduct,
|
||||||
list: listProducts,
|
list: listProducts,
|
||||||
@@ -54,5 +76,9 @@ export const products = {
|
|||||||
delete: deleteProduct,
|
delete: deleteProduct,
|
||||||
retrieveVariant,
|
retrieveVariant,
|
||||||
listVariants,
|
listVariants,
|
||||||
|
updateVariant,
|
||||||
deleteVariant,
|
deleteVariant,
|
||||||
|
createOption,
|
||||||
|
updateOption,
|
||||||
|
deleteOption,
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -1,7 +1,6 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import { Product } from "@medusajs/medusa"
|
import { Product } from "@medusajs/medusa"
|
||||||
import { Button, Input } from "@medusajs/ui"
|
import { Button, Input } from "@medusajs/ui"
|
||||||
import { useAdminCreateProductOption } from "medusa-react"
|
|
||||||
import { useForm } from "react-hook-form"
|
import { useForm } from "react-hook-form"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
@@ -10,6 +9,7 @@ import {
|
|||||||
RouteDrawer,
|
RouteDrawer,
|
||||||
useRouteModal,
|
useRouteModal,
|
||||||
} from "../../../../../components/route-modal"
|
} from "../../../../../components/route-modal"
|
||||||
|
import { useCreateProductOption } from "../../../../../hooks/api/products"
|
||||||
|
|
||||||
type EditProductOptionsFormProps = {
|
type EditProductOptionsFormProps = {
|
||||||
product: Product
|
product: Product
|
||||||
@@ -32,7 +32,7 @@ export const CreateProductOptionForm = ({
|
|||||||
resolver: zodResolver(CreateProductOptionSchema),
|
resolver: zodResolver(CreateProductOptionSchema),
|
||||||
})
|
})
|
||||||
|
|
||||||
const { mutateAsync, isLoading } = useAdminCreateProductOption(product.id)
|
const { mutateAsync, isLoading } = useCreateProductOption(product.id)
|
||||||
|
|
||||||
const handleSubmit = form.handleSubmit(async (values) => {
|
const handleSubmit = form.handleSubmit(async (values) => {
|
||||||
mutateAsync(values, {
|
mutateAsync(values, {
|
||||||
|
|||||||
+5
-3
@@ -87,7 +87,7 @@ export const CreateProductDetails = ({ form }: CreateProductPropsProps) => {
|
|||||||
const { product_categories, isLoading: isLoadingCategories } = useCategories()
|
const { product_categories, isLoading: isLoadingCategories } = useCategories()
|
||||||
|
|
||||||
const options = form.watch("options")
|
const options = form.watch("options")
|
||||||
const optionPermutations = permutations(options)
|
const optionPermutations = permutations(options ?? [])
|
||||||
|
|
||||||
// const { append } = useFieldArray({
|
// const { append } = useFieldArray({
|
||||||
// name: "images",
|
// name: "images",
|
||||||
@@ -396,7 +396,7 @@ export const CreateProductDetails = ({ form }: CreateProductPropsProps) => {
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name="options"
|
name="options"
|
||||||
render={({ field: { onChange, value } }) => {
|
render={({ field: { onChange, value } }) => {
|
||||||
const normalizedValue = value.map((v) => {
|
const normalizedValue = (value ?? []).map((v) => {
|
||||||
return {
|
return {
|
||||||
key: v.title,
|
key: v.title,
|
||||||
value: v.values.join(","),
|
value: v.values.join(","),
|
||||||
@@ -443,7 +443,9 @@ export const CreateProductDetails = ({ form }: CreateProductPropsProps) => {
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name="variants"
|
name="variants"
|
||||||
render={({ field: { value, onChange, ...field } }) => {
|
render={({ field: { value, onChange, ...field } }) => {
|
||||||
const selectedOptions = value.map((v) => v.options)
|
const selectedOptions = (value ?? []).map(
|
||||||
|
(v) => v.options
|
||||||
|
)
|
||||||
return (
|
return (
|
||||||
<Form.Item>
|
<Form.Item>
|
||||||
<Form.Label optional>
|
<Form.Label optional>
|
||||||
|
|||||||
+4
-7
@@ -56,10 +56,6 @@ export const CreateProductForm = () => {
|
|||||||
|
|
||||||
const form = useForm<Schema>({
|
const form = useForm<Schema>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
title: "",
|
|
||||||
subtitle: "",
|
|
||||||
handle: "",
|
|
||||||
description: "",
|
|
||||||
discountable: true,
|
discountable: true,
|
||||||
tags: [],
|
tags: [],
|
||||||
sales_channels: [],
|
sales_channels: [],
|
||||||
@@ -83,11 +79,12 @@ export const CreateProductForm = () => {
|
|||||||
length: values.length ? parseFloat(values.length) : undefined,
|
length: values.length ? parseFloat(values.length) : undefined,
|
||||||
height: values.height ? parseFloat(values.height) : undefined,
|
height: values.height ? parseFloat(values.height) : undefined,
|
||||||
weight: values.weight ? parseFloat(values.weight) : undefined,
|
weight: values.weight ? parseFloat(values.weight) : undefined,
|
||||||
variants: values.variants,
|
variants: values.variants.map((variant) => ({
|
||||||
|
...variant,
|
||||||
|
prices: [],
|
||||||
|
})),
|
||||||
} as any
|
} as any
|
||||||
|
|
||||||
delete reqData.sales_channels
|
|
||||||
|
|
||||||
await mutateAsync(reqData, {
|
await mutateAsync(reqData, {
|
||||||
onSuccess: ({ product }) => {
|
onSuccess: ({ product }) => {
|
||||||
handleSuccess(`../${product.id}`)
|
handleSuccess(`../${product.id}`)
|
||||||
|
|||||||
+12
-56
@@ -3,6 +3,8 @@ import { Product } from "@medusajs/medusa"
|
|||||||
import { Container, Heading, Text } from "@medusajs/ui"
|
import { Container, Heading, Text } from "@medusajs/ui"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||||
|
import { SectionRow } from "../../../../../components/common/section"
|
||||||
|
import { getFormattedCountry } from "../../../../../lib/addresses"
|
||||||
|
|
||||||
type ProductAttributeSectionProps = {
|
type ProductAttributeSectionProps = {
|
||||||
product: Product
|
product: Product
|
||||||
@@ -31,62 +33,16 @@ export const ProductAttributeSection = ({
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
|
<SectionRow title={t("fields.height")} value={product.height} />
|
||||||
<Text size="small" weight="plus" leading="compact">
|
<SectionRow title={t("fields.width")} value={product.width} />
|
||||||
{t("fields.height")}
|
<SectionRow title={t("fields.length")} value={product.length} />
|
||||||
</Text>
|
<SectionRow title={t("fields.weight")} value={product.weight} />
|
||||||
<Text size="small" leading="compact" className="text-pretty">
|
<SectionRow title={t("fields.midCode")} value={product.mid_code} />
|
||||||
{product.height ?? "-"}
|
<SectionRow title={t("fields.hsCode")} value={product.hs_code} />
|
||||||
</Text>
|
<SectionRow
|
||||||
</div>
|
title={t("fields.countryOfOrigin")}
|
||||||
<div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
|
value={getFormattedCountry(product.origin_country)}
|
||||||
<Text size="small" weight="plus" leading="compact">
|
/>
|
||||||
{t("fields.width")}
|
|
||||||
</Text>
|
|
||||||
<Text size="small" leading="compact">
|
|
||||||
{product.width ?? "-"}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
<div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
|
|
||||||
<Text size="small" weight="plus" leading="compact">
|
|
||||||
{t("fields.length")}
|
|
||||||
</Text>
|
|
||||||
<Text size="small" leading="compact">
|
|
||||||
{product.length ?? "-"}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
<div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
|
|
||||||
<Text size="small" weight="plus" leading="compact">
|
|
||||||
{t("fields.weight")}
|
|
||||||
</Text>
|
|
||||||
<Text size="small" leading="compact">
|
|
||||||
{product.weight ?? "-"}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
<div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
|
|
||||||
<Text size="small" weight="plus" leading="compact">
|
|
||||||
{t("fields.midCode")}
|
|
||||||
</Text>
|
|
||||||
<Text size="small" leading="compact">
|
|
||||||
{product.mid_code ?? "-"}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
<div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
|
|
||||||
<Text size="small" weight="plus" leading="compact">
|
|
||||||
{t("fields.hsCode")}
|
|
||||||
</Text>
|
|
||||||
<Text size="small" leading="compact">
|
|
||||||
{product.hs_code ?? "-"}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
<div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
|
|
||||||
<Text size="small" weight="plus" leading="compact">
|
|
||||||
{t("fields.countryOfOrigin")}
|
|
||||||
</Text>
|
|
||||||
<Text size="small" leading="compact">
|
|
||||||
{product.origin_country ?? "-"}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+27
-33
@@ -5,6 +5,22 @@ import { useTranslation } from "react-i18next"
|
|||||||
import { useNavigate } from "react-router-dom"
|
import { useNavigate } from "react-router-dom"
|
||||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||||
import { useDeleteProduct } from "../../../../../hooks/api/products"
|
import { useDeleteProduct } from "../../../../../hooks/api/products"
|
||||||
|
import { SectionRow } from "../../../../../components/common/section"
|
||||||
|
|
||||||
|
const productStatusColor = (status: string) => {
|
||||||
|
switch (status) {
|
||||||
|
case "draft":
|
||||||
|
return "purple"
|
||||||
|
case "proposed":
|
||||||
|
return "orange"
|
||||||
|
case "published":
|
||||||
|
return "green"
|
||||||
|
case "rejected":
|
||||||
|
return "red"
|
||||||
|
default:
|
||||||
|
return "purple"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type ProductGeneralSectionProps = {
|
type ProductGeneralSectionProps = {
|
||||||
product: Product
|
product: Product
|
||||||
@@ -45,7 +61,9 @@ export const ProductGeneralSection = ({
|
|||||||
<div className="flex items-center justify-between px-6 py-4">
|
<div className="flex items-center justify-between px-6 py-4">
|
||||||
<Heading>{product.title}</Heading>
|
<Heading>{product.title}</Heading>
|
||||||
<div className="flex items-center gap-x-4">
|
<div className="flex items-center gap-x-4">
|
||||||
<StatusBadge color="green">Published</StatusBadge>
|
<StatusBadge color={productStatusColor(product.status)}>
|
||||||
|
{t(`products.productStatus.${product.status}`)}
|
||||||
|
</StatusBadge>
|
||||||
<ActionMenu
|
<ActionMenu
|
||||||
groups={[
|
groups={[
|
||||||
{
|
{
|
||||||
@@ -70,38 +88,14 @@ export const ProductGeneralSection = ({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-ui-fg-subtle grid grid-cols-2 items-start px-6 py-4">
|
|
||||||
<Text size="small" weight="plus" leading="compact">
|
<SectionRow title={t("fields.description")} value={product.description} />
|
||||||
{t("fields.description")}
|
<SectionRow title={t("fields.subtitle")} value={product.subtitle} />
|
||||||
</Text>
|
<SectionRow title={t("fields.handle")} value={`/${product.handle}`} />
|
||||||
<Text size="small" leading="compact" className="text-pretty">
|
<SectionRow
|
||||||
{product.description}
|
title={t("fields.discountable")}
|
||||||
</Text>
|
value={product.discountable ? t("fields.true") : t("fields.false")}
|
||||||
</div>
|
/>
|
||||||
<div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
|
|
||||||
<Text size="small" weight="plus" leading="compact">
|
|
||||||
{t("fields.subtitle")}
|
|
||||||
</Text>
|
|
||||||
<Text size="small" leading="compact" className="text-pretty">
|
|
||||||
{product.subtitle ?? "-"}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
<div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
|
|
||||||
<Text size="small" weight="plus" leading="compact">
|
|
||||||
{t("fields.handle")}
|
|
||||||
</Text>
|
|
||||||
<Text size="small" leading="compact" className="text-pretty">
|
|
||||||
/{product.handle}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
<div className="text-ui-fg-subtle grid grid-cols-2 items-center px-6 py-4">
|
|
||||||
<Text size="small" weight="plus" leading="compact">
|
|
||||||
{t("fields.discountable")}
|
|
||||||
</Text>
|
|
||||||
<Text size="small" leading="compact" className="text-pretty">
|
|
||||||
{product.discountable ? t("fields.true") : t("fields.false")}
|
|
||||||
</Text>
|
|
||||||
</div>
|
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+75
-42
@@ -1,8 +1,64 @@
|
|||||||
import { PencilSquare, Plus } from "@medusajs/icons"
|
import { PencilSquare, Plus, Trash } from "@medusajs/icons"
|
||||||
import { Product, ProductOption } from "@medusajs/medusa"
|
import { Product } from "@medusajs/medusa"
|
||||||
import { Badge, Container, Heading, Text } from "@medusajs/ui"
|
import { Badge, Container, Heading, usePrompt } from "@medusajs/ui"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||||
|
import { SectionRow } from "../../../../../components/common/section"
|
||||||
|
import { useDeleteProductOption } from "../../../../../hooks/api/products"
|
||||||
|
|
||||||
|
const OptionActions = ({
|
||||||
|
product,
|
||||||
|
option,
|
||||||
|
}: {
|
||||||
|
product: Product
|
||||||
|
option: any
|
||||||
|
}) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { mutateAsync } = useDeleteProductOption(product.id, option.id)
|
||||||
|
const prompt = usePrompt()
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
const res = await prompt({
|
||||||
|
title: t("general.areYouSure"),
|
||||||
|
description: t("products.deleteWarning", {
|
||||||
|
title: product.title,
|
||||||
|
}),
|
||||||
|
confirmText: t("actions.delete"),
|
||||||
|
cancelText: t("actions.cancel"),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!res) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
await mutateAsync()
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ActionMenu
|
||||||
|
groups={[
|
||||||
|
{
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
label: t("actions.edit"),
|
||||||
|
to: `options/${option.id}/edit`,
|
||||||
|
icon: <PencilSquare />,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
label: t("actions.delete"),
|
||||||
|
onClick: handleDelete,
|
||||||
|
icon: <Trash />,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
type ProductOptionSectionProps = {
|
type ProductOptionSectionProps = {
|
||||||
product: Product
|
product: Product
|
||||||
@@ -31,50 +87,27 @@ export const ProductOptionSection = ({
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{product.options.map((option) => {
|
{product.options.map((option) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<SectionRow
|
||||||
|
title={option.title}
|
||||||
key={option.id}
|
key={option.id}
|
||||||
className="text-ui-fg-subtle grid grid-cols-[1fr_1fr_28px] items-start gap-4 px-6 py-4"
|
value={option.values.map((val) => {
|
||||||
>
|
return (
|
||||||
<Text size="small" leading="compact" weight="plus">
|
<Badge
|
||||||
{option.title}
|
key={val.value}
|
||||||
</Text>
|
size="2xsmall"
|
||||||
<div className="flex flex-wrap gap-1">
|
className="flex min-w-[20px] items-center justify-center"
|
||||||
{getUnqiueValues(option).map((value) => {
|
>
|
||||||
return (
|
{val.value}
|
||||||
<Badge
|
</Badge>
|
||||||
key={value}
|
)
|
||||||
size="2xsmall"
|
})}
|
||||||
className="flex min-w-[20px] items-center justify-center"
|
actions={<OptionActions product={product} option={option} />}
|
||||||
>
|
/>
|
||||||
{value}
|
|
||||||
</Badge>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
<ActionMenu
|
|
||||||
groups={[
|
|
||||||
{
|
|
||||||
actions: [
|
|
||||||
{
|
|
||||||
label: t("actions.edit"),
|
|
||||||
to: `options/${option.id}/edit`,
|
|
||||||
icon: <PencilSquare />,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const getUnqiueValues = (option: ProductOption) => {
|
|
||||||
const values = option.values.map((v) => v.value)
|
|
||||||
|
|
||||||
return Array.from(new Set(values))
|
|
||||||
}
|
|
||||||
|
|||||||
+43
-51
@@ -1,9 +1,10 @@
|
|||||||
import { PencilSquare } from "@medusajs/icons"
|
import { PencilSquare } from "@medusajs/icons"
|
||||||
import { Product } from "@medusajs/medusa"
|
import { Product } from "@medusajs/medusa"
|
||||||
import { Badge, Container, Heading, Text } from "@medusajs/ui"
|
import { Badge, Container, Heading } from "@medusajs/ui"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { Link } from "react-router-dom"
|
import { Link } from "react-router-dom"
|
||||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||||
|
import { SectionRow } from "../../../../../components/common/section"
|
||||||
|
|
||||||
type ProductOrganizationSectionProps = {
|
type ProductOrganizationSectionProps = {
|
||||||
product: Product
|
product: Product
|
||||||
@@ -32,58 +33,49 @@ export const ProductOrganizationSection = ({
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-ui-fg-subtle grid grid-cols-2 items-start px-6 py-4">
|
|
||||||
<Text size="small" weight="plus" leading="compact">
|
<SectionRow
|
||||||
{t("fields.tags")}
|
title={t("fields.tags")}
|
||||||
</Text>
|
value={
|
||||||
<div className="flex flex-wrap items-center gap-1">
|
product.tags.length > 0
|
||||||
{product.tags.length > 0
|
|
||||||
? product.tags.map((tag) => (
|
? product.tags.map((tag) => (
|
||||||
<Badge key={tag.id} className="w-fit" size="2xsmall" asChild>
|
<Badge key={tag.id} className="w-fit" size="2xsmall" asChild>
|
||||||
<Link to={`/products?tags=${tag.id}`}>{tag.value}</Link>
|
<Link to={`/products?tags=${tag.id}`}>{tag.value}</Link>
|
||||||
</Badge>
|
</Badge>
|
||||||
))
|
))
|
||||||
: "-"}
|
: undefined
|
||||||
</div>
|
}
|
||||||
</div>
|
/>
|
||||||
<div className="text-ui-fg-subtle grid grid-cols-2 items-start px-6 py-4">
|
<SectionRow
|
||||||
<Text size="small" weight="plus" leading="compact">
|
title={t("fields.type")}
|
||||||
{t("fields.type")}
|
value={
|
||||||
</Text>
|
product.type ? (
|
||||||
{product.type ? (
|
<Badge size="2xsmall" className="w-fit" asChild>
|
||||||
<Badge size="2xsmall" className="w-fit" asChild>
|
<Link to={`/products?type_id=${product.type_id}`}>
|
||||||
<Link to={`/products?type_id=${product.type_id}`}>
|
{product.type.value}
|
||||||
{product.type.value}
|
</Link>
|
||||||
</Link>
|
</Badge>
|
||||||
</Badge>
|
) : undefined
|
||||||
) : (
|
}
|
||||||
<Text size="small" leading="compact">
|
/>
|
||||||
-
|
|
||||||
</Text>
|
<SectionRow
|
||||||
)}
|
title={t("fields.collection")}
|
||||||
</div>
|
value={
|
||||||
<div className="text-ui-fg-subtle grid grid-cols-2 items-start px-6 py-4">
|
product.collection ? (
|
||||||
<Text size="small" weight="plus" leading="compact">
|
<Badge size="2xsmall" color="blue" className="w-fit" asChild>
|
||||||
{t("fields.collection")}
|
<Link to={`/collections/${product.collection.id}`}>
|
||||||
</Text>
|
{product.collection.title}
|
||||||
{product.collection ? (
|
</Link>
|
||||||
<Badge size="2xsmall" color="blue" className="w-fit" asChild>
|
</Badge>
|
||||||
<Link to={`/collections/${product.collection.id}`}>
|
) : undefined
|
||||||
{product.collection.title}
|
}
|
||||||
</Link>
|
/>
|
||||||
</Badge>
|
|
||||||
) : (
|
<SectionRow
|
||||||
<Text size="small" leading="compact">
|
title={t("fields.categories")}
|
||||||
-
|
value={
|
||||||
</Text>
|
product.categories?.length > 0
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="text-ui-fg-subtle grid grid-cols-2 items-start px-6 py-4">
|
|
||||||
<Text size="small" weight="plus" leading="compact">
|
|
||||||
{t("fields.categories")}
|
|
||||||
</Text>
|
|
||||||
<div className="flex flex-wrap items-center gap-1">
|
|
||||||
{product.categories?.length > 0
|
|
||||||
? product.categories.map((pcat) => (
|
? product.categories.map((pcat) => (
|
||||||
<Badge
|
<Badge
|
||||||
key={pcat.id}
|
key={pcat.id}
|
||||||
@@ -95,9 +87,9 @@ export const ProductOrganizationSection = ({
|
|||||||
<Link to={`/categories/${pcat.id}`}>{pcat.name}</Link>
|
<Link to={`/categories/${pcat.id}`}>{pcat.name}</Link>
|
||||||
</Badge>
|
</Badge>
|
||||||
))
|
))
|
||||||
: "-"}
|
: undefined
|
||||||
</div>
|
}
|
||||||
</div>
|
/>
|
||||||
</Container>
|
</Container>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -1,19 +1,19 @@
|
|||||||
import { Channels, PencilSquare } from "@medusajs/icons"
|
import { Channels, PencilSquare } from "@medusajs/icons"
|
||||||
import { Product } from "@medusajs/medusa"
|
import { Product } from "@medusajs/medusa"
|
||||||
import { Container, Heading, Text, Tooltip } from "@medusajs/ui"
|
import { Container, Heading, Text, Tooltip } from "@medusajs/ui"
|
||||||
// import { useAdminSalesChannels } from "medusa-react"
|
|
||||||
import { Trans, useTranslation } from "react-i18next"
|
import { Trans, useTranslation } from "react-i18next"
|
||||||
import { ActionMenu } from "../../../../../components/common/action-menu"
|
import { ActionMenu } from "../../../../../components/common/action-menu"
|
||||||
|
import { useSalesChannels } from "../../../../../hooks/api/sales-channels"
|
||||||
|
|
||||||
type ProductSalesChannelSectionProps = {
|
type ProductSalesChannelSectionProps = {
|
||||||
product: Product
|
product: Product
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: The fetched sales channel doesn't contain all necessary info
|
||||||
export const ProductSalesChannelSection = ({
|
export const ProductSalesChannelSection = ({
|
||||||
product,
|
product,
|
||||||
}: ProductSalesChannelSectionProps) => {
|
}: ProductSalesChannelSectionProps) => {
|
||||||
// const { count } = useAdminSalesChannels()
|
const { count } = useSalesChannels()
|
||||||
const count = 0
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const availableInSalesChannels =
|
const availableInSalesChannels =
|
||||||
|
|||||||
+3
@@ -29,6 +29,9 @@ export const ProductVariantSection = ({
|
|||||||
product.id,
|
product.id,
|
||||||
{
|
{
|
||||||
...searchParams,
|
...searchParams,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keepPreviousData: true,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
+31
-31
@@ -69,38 +69,38 @@ export const useProductVariantTableColumns = (product?: Product) => {
|
|||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
const optionColumns = useMemo(() => {
|
const optionColumns = useMemo(() => {
|
||||||
return product
|
if (!product) {
|
||||||
? product.options?.map((o) => {
|
return []
|
||||||
return columnHelper.display({
|
}
|
||||||
id: o.id,
|
return product.options?.map((option) => {
|
||||||
header: () => (
|
return columnHelper.display({
|
||||||
<div className="flex h-full w-full items-center">
|
id: option.id,
|
||||||
<span className="truncate">{o.title}</span>
|
header: () => (
|
||||||
</div>
|
<div className="flex h-full w-full items-center">
|
||||||
),
|
<span className="truncate">{option.title}</span>
|
||||||
cell: ({ row }) => {
|
</div>
|
||||||
const value = row.original.options.find(
|
),
|
||||||
(op) => op.option_id === o.id
|
cell: ({ row }) => {
|
||||||
)
|
const variantOpt: any = row.original.options.find(
|
||||||
|
(opt: any) => opt.option_value.option_id === option.id
|
||||||
|
)
|
||||||
|
if (!variantOpt) {
|
||||||
|
return <PlaceholderCell />
|
||||||
|
}
|
||||||
|
|
||||||
if (!value) {
|
return (
|
||||||
return <PlaceholderCell />
|
<div className="flex h-full w-full items-center overflow-hidden">
|
||||||
}
|
<Badge
|
||||||
|
size="2xsmall"
|
||||||
return (
|
className="flex min-w-[20px] items-center justify-center"
|
||||||
<div className="flex h-full w-full items-center overflow-hidden">
|
>
|
||||||
<Badge
|
{variantOpt.option_value.value}
|
||||||
size="2xsmall"
|
</Badge>
|
||||||
className="flex min-w-[20px] items-center justify-center"
|
</div>
|
||||||
>
|
)
|
||||||
{value.value}
|
},
|
||||||
</Badge>
|
})
|
||||||
</div>
|
})
|
||||||
)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
: []
|
|
||||||
}, [product])
|
}, [product])
|
||||||
|
|
||||||
return useMemo(
|
return useMemo(
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import { AdminProductsRes } from "@medusajs/medusa"
|
import { AdminProductsRes } from "@medusajs/medusa"
|
||||||
import { Response } from "@medusajs/medusa-js"
|
import { Response } from "@medusajs/medusa-js"
|
||||||
import { adminProductKeys } from "medusa-react"
|
|
||||||
import { LoaderFunctionArgs } from "react-router-dom"
|
import { LoaderFunctionArgs } from "react-router-dom"
|
||||||
|
|
||||||
import { medusa, queryClient } from "../../../lib/medusa"
|
import { queryClient } from "../../../lib/medusa"
|
||||||
|
import { productsQueryKeys } from "../../../hooks/api/products"
|
||||||
|
import { client } from "../../../lib/client"
|
||||||
|
|
||||||
const productDetailQuery = (id: string) => ({
|
const productDetailQuery = (id: string) => ({
|
||||||
queryKey: adminProductKeys.detail(id),
|
queryKey: productsQueryKeys.detail(id),
|
||||||
queryFn: async () => medusa.admin.products.retrieve(id),
|
queryFn: async () => client.products.retrieve(id),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const productLoader = async ({ params }: LoaderFunctionArgs) => {
|
export const productLoader = async ({ params }: LoaderFunctionArgs) => {
|
||||||
|
|||||||
+13
-23
@@ -16,6 +16,7 @@ import sideBefore from "medusa-admin:widgets/product/details/side/before"
|
|||||||
import { ProductOrganizationSection } from "./components/product-organization-section"
|
import { ProductOrganizationSection } from "./components/product-organization-section"
|
||||||
import { useProduct } from "../../../hooks/api/products"
|
import { useProduct } from "../../../hooks/api/products"
|
||||||
|
|
||||||
|
// TODO: Use product domain translations only
|
||||||
export const ProductDetail = () => {
|
export const ProductDetail = () => {
|
||||||
const initialData = useLoaderData() as Awaited<
|
const initialData = useLoaderData() as Awaited<
|
||||||
ReturnType<typeof productLoader>
|
ReturnType<typeof productLoader>
|
||||||
@@ -43,31 +44,13 @@ export const ProductDetail = () => {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
<div className="flex flex-col gap-x-4 xl:flex-row xl:items-start">
|
|
||||||
<div className="flex flex-col gap-y-2">
|
<div className="flex flex-col gap-x-4 lg:flex-row lg:items-start">
|
||||||
|
<div className="w-full flex flex-col gap-y-2">
|
||||||
<ProductGeneralSection product={product} />
|
<ProductGeneralSection product={product} />
|
||||||
<ProductMediaSection product={product} />
|
<ProductMediaSection product={product} />
|
||||||
<ProductOptionSection product={product} />
|
<ProductOptionSection product={product} />
|
||||||
<ProductVariantSection product={product} />
|
<ProductVariantSection product={product} />
|
||||||
<div className="flex flex-col gap-y-2 xl:hidden">
|
|
||||||
{sideBefore.widgets.map((w, i) => {
|
|
||||||
return (
|
|
||||||
<div key={i}>
|
|
||||||
<w.Component />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
<ProductSalesChannelSection product={product} />
|
|
||||||
<ProductOrganizationSection product={product} />
|
|
||||||
<ProductAttributeSection product={product} />
|
|
||||||
{sideAfter.widgets.map((w, i) => {
|
|
||||||
return (
|
|
||||||
<div key={i}>
|
|
||||||
<w.Component />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
{after.widgets.map((w, i) => {
|
{after.widgets.map((w, i) => {
|
||||||
return (
|
return (
|
||||||
<div key={i}>
|
<div key={i}>
|
||||||
@@ -75,9 +58,12 @@ export const ProductDetail = () => {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
<JsonViewSection data={product} root="product" />
|
|
||||||
|
<div className="hidden lg:block">
|
||||||
|
<JsonViewSection data={product} root="product" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="hidden w-full max-w-[400px] flex-col gap-y-2 xl:flex">
|
<div className="w-full lg:max-w-[400px] max-w-[100%] mt-2 lg:mt-0 flex flex-col gap-y-2">
|
||||||
{sideBefore.widgets.map((w, i) => {
|
{sideBefore.widgets.map((w, i) => {
|
||||||
return (
|
return (
|
||||||
<div key={i}>
|
<div key={i}>
|
||||||
@@ -95,6 +81,10 @@ export const ProductDetail = () => {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|
||||||
|
<div className="lg:hidden">
|
||||||
|
<JsonViewSection data={product} root="product" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
|
|||||||
+4
-3
@@ -1,7 +1,6 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import { ProductOption } from "@medusajs/medusa"
|
import { ProductOption } from "@medusajs/medusa"
|
||||||
import { Button, Input } from "@medusajs/ui"
|
import { Button, Input } from "@medusajs/ui"
|
||||||
import { useAdminUpdateProductOption } from "medusa-react"
|
|
||||||
import { useForm } from "react-hook-form"
|
import { useForm } from "react-hook-form"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
@@ -10,6 +9,7 @@ import {
|
|||||||
RouteDrawer,
|
RouteDrawer,
|
||||||
useRouteModal,
|
useRouteModal,
|
||||||
} from "../../../../../components/route-modal"
|
} from "../../../../../components/route-modal"
|
||||||
|
import { useUpdateProductOption } from "../../../../../hooks/api/products"
|
||||||
|
|
||||||
type EditProductOptionFormProps = {
|
type EditProductOptionFormProps = {
|
||||||
option: ProductOption
|
option: ProductOption
|
||||||
@@ -32,8 +32,9 @@ export const CreateProductOptionForm = ({
|
|||||||
resolver: zodResolver(CreateProductOptionSchema),
|
resolver: zodResolver(CreateProductOptionSchema),
|
||||||
})
|
})
|
||||||
|
|
||||||
const { mutateAsync, isLoading } = useAdminUpdateProductOption(
|
const { mutateAsync, isLoading } = useUpdateProductOption(
|
||||||
option.product_id
|
option.product_id,
|
||||||
|
option.id
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleSubmit = form.handleSubmit(async (values) => {
|
const handleSubmit = form.handleSubmit(async (values) => {
|
||||||
|
|||||||
+5
-2
@@ -1,7 +1,6 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod"
|
import { zodResolver } from "@hookform/resolvers/zod"
|
||||||
import { Product, ProductOption, ProductVariant } from "@medusajs/medusa"
|
import { Product, ProductOption, ProductVariant } from "@medusajs/medusa"
|
||||||
import { Button, Heading, Input, Switch } from "@medusajs/ui"
|
import { Button, Heading, Input, Switch } from "@medusajs/ui"
|
||||||
import { useAdminUpdateVariant } from "medusa-react"
|
|
||||||
import { useForm } from "react-hook-form"
|
import { useForm } from "react-hook-form"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import { z } from "zod"
|
import { z } from "zod"
|
||||||
@@ -17,6 +16,7 @@ import {
|
|||||||
} from "../../../../../components/route-modal"
|
} from "../../../../../components/route-modal"
|
||||||
import { castNumber } from "../../../../../lib/cast-number"
|
import { castNumber } from "../../../../../lib/cast-number"
|
||||||
import { optionalInt } from "../../../../../lib/validation"
|
import { optionalInt } from "../../../../../lib/validation"
|
||||||
|
import { useUpdateProductVariant } from "../../../../../hooks/api/products"
|
||||||
|
|
||||||
type ProductEditVariantFormProps = {
|
type ProductEditVariantFormProps = {
|
||||||
product: Product
|
product: Product
|
||||||
@@ -83,7 +83,10 @@ export const ProductEditVariantForm = ({
|
|||||||
resolver: zodResolver(ProductEditVariantSchema),
|
resolver: zodResolver(ProductEditVariantSchema),
|
||||||
})
|
})
|
||||||
|
|
||||||
const { mutateAsync, isLoading } = useAdminUpdateVariant(product.id)
|
const { mutateAsync, isLoading } = useUpdateProductVariant(
|
||||||
|
product.id,
|
||||||
|
variant.id
|
||||||
|
)
|
||||||
|
|
||||||
const handleSubmit = form.handleSubmit(async (data) => {
|
const handleSubmit = form.handleSubmit(async (data) => {
|
||||||
const parseNumber = (value?: string | number) => {
|
const parseNumber = (value?: string | number) => {
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { adminProductKeys, adminStoreKeys } from "medusa-react"
|
|
||||||
import { LoaderFunctionArgs } from "react-router-dom"
|
import { LoaderFunctionArgs } from "react-router-dom"
|
||||||
|
|
||||||
import { medusa, queryClient } from "../../../lib/medusa"
|
import { medusa, queryClient } from "../../../lib/medusa"
|
||||||
|
import { productsQueryKeys } from "../../../hooks/api/products"
|
||||||
|
|
||||||
const queryKey = (id: string) => {
|
const queryKey = (id: string) => {
|
||||||
return [adminProductKeys.detail(id), adminStoreKeys.details()]
|
return [productsQueryKeys.detail(id)]
|
||||||
}
|
}
|
||||||
|
|
||||||
const queryFn = async (id: string) => {
|
const queryFn = async (id: string) => {
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { AdminProductsListRes } from "@medusajs/medusa"
|
import { AdminProductsListRes } from "@medusajs/medusa"
|
||||||
import { Response } from "@medusajs/medusa-js"
|
import { Response } from "@medusajs/medusa-js"
|
||||||
import { QueryClient } from "@tanstack/react-query"
|
import { QueryClient } from "@tanstack/react-query"
|
||||||
import { adminProductKeys } from "medusa-react"
|
|
||||||
|
|
||||||
import { medusa, queryClient } from "../../../lib/medusa"
|
import { medusa, queryClient } from "../../../lib/medusa"
|
||||||
|
import { productsQueryKeys } from "../../../hooks/api/products"
|
||||||
|
|
||||||
const productsListQuery = () => ({
|
const productsListQuery = () => ({
|
||||||
queryKey: adminProductKeys.list({ limit: 20, offset: 0 }),
|
queryKey: productsQueryKeys.list({ limit: 20, offset: 0 }),
|
||||||
queryFn: async () => medusa.admin.products.list({ limit: 20, offset: 0 }),
|
queryFn: async () => medusa.admin.products.list({ limit: 20, offset: 0 }),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
+16
-12
@@ -3,7 +3,6 @@ import { CheckMini, Spinner, ThumbnailBadge } from "@medusajs/icons"
|
|||||||
import { Image, Product } from "@medusajs/medusa"
|
import { Image, Product } from "@medusajs/medusa"
|
||||||
import { Button, CommandBar, Tooltip, clx } from "@medusajs/ui"
|
import { Button, CommandBar, Tooltip, clx } from "@medusajs/ui"
|
||||||
import { AnimatePresence, motion } from "framer-motion"
|
import { AnimatePresence, motion } from "framer-motion"
|
||||||
import { useAdminUpdateProduct, useMedusa } from "medusa-react"
|
|
||||||
import { Fragment, useCallback, useState } from "react"
|
import { Fragment, useCallback, useState } from "react"
|
||||||
import { useFieldArray, useForm } from "react-hook-form"
|
import { useFieldArray, useForm } from "react-hook-form"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
@@ -19,6 +18,7 @@ import {
|
|||||||
FileType,
|
FileType,
|
||||||
FileUpload,
|
FileUpload,
|
||||||
} from "../../../../../components/common/file-upload"
|
} from "../../../../../components/common/file-upload"
|
||||||
|
import { useUpdateProduct } from "../../../../../hooks/api/products"
|
||||||
|
|
||||||
type ProductMediaViewProps = {
|
type ProductMediaViewProps = {
|
||||||
product: Product
|
product: Product
|
||||||
@@ -73,8 +73,7 @@ export const EditProductMediaForm = ({ product }: ProductMediaViewProps) => {
|
|||||||
keyName: "field_id",
|
keyName: "field_id",
|
||||||
})
|
})
|
||||||
|
|
||||||
const { mutateAsync, isLoading } = useAdminUpdateProduct(product.id)
|
const { mutateAsync, isLoading } = useUpdateProduct(product.id)
|
||||||
const { client } = useMedusa()
|
|
||||||
|
|
||||||
const handleSubmit = form.handleSubmit(async ({ media }) => {
|
const handleSubmit = form.handleSubmit(async ({ media }) => {
|
||||||
const urls = media.map((m) => m.url)
|
const urls = media.map((m) => m.url)
|
||||||
@@ -86,15 +85,20 @@ export const EditProductMediaForm = ({ product }: ProductMediaViewProps) => {
|
|||||||
if (filesToUpload.length) {
|
if (filesToUpload.length) {
|
||||||
const files = filesToUpload.map((m) => m.file) as File[]
|
const files = filesToUpload.map((m) => m.file) as File[]
|
||||||
|
|
||||||
const uploads = await client.admin.uploads
|
// TODO: Implement upload to Medusa
|
||||||
.create(files)
|
// const uploads = await client.admin.uploads
|
||||||
.then((res) => {
|
// .create(files)
|
||||||
return res.uploads
|
// .then((res) => {
|
||||||
})
|
// return res.uploads
|
||||||
.catch((_err) => {
|
// })
|
||||||
// Show error message
|
// .catch((_err) => {
|
||||||
return null
|
// // Show error message
|
||||||
})
|
// return null
|
||||||
|
// })
|
||||||
|
|
||||||
|
const uploads = files.map((file) => ({
|
||||||
|
url: URL.createObjectURL(file),
|
||||||
|
}))
|
||||||
|
|
||||||
if (!uploads) {
|
if (!uploads) {
|
||||||
return
|
return
|
||||||
|
|||||||
+11
-13
@@ -1,7 +1,6 @@
|
|||||||
import { Product, SalesChannel } from "@medusajs/medusa"
|
import { Product, SalesChannel } from "@medusajs/medusa"
|
||||||
import { Button, Checkbox } from "@medusajs/ui"
|
import { Button, Checkbox } from "@medusajs/ui"
|
||||||
import { RowSelectionState, createColumnHelper } from "@tanstack/react-table"
|
import { RowSelectionState, createColumnHelper } from "@tanstack/react-table"
|
||||||
import { useAdminSalesChannels, useAdminUpdateProduct } from "medusa-react"
|
|
||||||
import { useEffect, useMemo, useState } from "react"
|
import { useEffect, useMemo, useState } from "react"
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import * as zod from "zod"
|
import * as zod from "zod"
|
||||||
@@ -17,6 +16,8 @@ import { useSalesChannelTableColumns } from "../../../../../hooks/table/columns/
|
|||||||
import { useSalesChannelTableFilters } from "../../../../../hooks/table/filters/use-sales-channel-table-filters"
|
import { useSalesChannelTableFilters } from "../../../../../hooks/table/filters/use-sales-channel-table-filters"
|
||||||
import { useSalesChannelTableQuery } from "../../../../../hooks/table/query/use-sales-channel-table-query"
|
import { useSalesChannelTableQuery } from "../../../../../hooks/table/query/use-sales-channel-table-query"
|
||||||
import { useDataTable } from "../../../../../hooks/use-data-table"
|
import { useDataTable } from "../../../../../hooks/use-data-table"
|
||||||
|
import { useSalesChannels } from "../../../../../hooks/api/sales-channels"
|
||||||
|
import { useUpdateProduct } from "../../../../../hooks/api/products"
|
||||||
|
|
||||||
type EditSalesChannelsFormProps = {
|
type EditSalesChannelsFormProps = {
|
||||||
product: Product
|
product: Product
|
||||||
@@ -63,15 +64,14 @@ export const EditSalesChannelsForm = ({
|
|||||||
const { searchParams, raw } = useSalesChannelTableQuery({
|
const { searchParams, raw } = useSalesChannelTableQuery({
|
||||||
pageSize: PAGE_SIZE,
|
pageSize: PAGE_SIZE,
|
||||||
})
|
})
|
||||||
const { sales_channels, count, isLoading, isError, error } =
|
const { sales_channels, count, isLoading, isError, error } = useSalesChannels(
|
||||||
useAdminSalesChannels(
|
{
|
||||||
{
|
...searchParams,
|
||||||
...searchParams,
|
},
|
||||||
},
|
{
|
||||||
{
|
keepPreviousData: true,
|
||||||
keepPreviousData: true,
|
}
|
||||||
}
|
)
|
||||||
)
|
|
||||||
|
|
||||||
const filters = useSalesChannelTableFilters()
|
const filters = useSalesChannelTableFilters()
|
||||||
const columns = useColumns()
|
const columns = useColumns()
|
||||||
@@ -90,9 +90,7 @@ export const EditSalesChannelsForm = ({
|
|||||||
pageSize: PAGE_SIZE,
|
pageSize: PAGE_SIZE,
|
||||||
})
|
})
|
||||||
|
|
||||||
const { mutateAsync, isLoading: isMutating } = useAdminUpdateProduct(
|
const { mutateAsync, isLoading: isMutating } = useUpdateProduct(product.id)
|
||||||
product.id
|
|
||||||
)
|
|
||||||
|
|
||||||
const handleSubmit = form.handleSubmit(async (data) => {
|
const handleSubmit = form.handleSubmit(async (data) => {
|
||||||
const arr = data.sales_channels ?? []
|
const arr = data.sales_channels ?? []
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export const defaultAdminProductsVariantFields = [
|
|||||||
"barcode",
|
"barcode",
|
||||||
"*prices",
|
"*prices",
|
||||||
"*options",
|
"*options",
|
||||||
|
"*options.option_value",
|
||||||
]
|
]
|
||||||
|
|
||||||
export const retrieveVariantConfig = {
|
export const retrieveVariantConfig = {
|
||||||
@@ -81,10 +82,10 @@ export const defaultAdminProductFields = [
|
|||||||
"*options.values",
|
"*options.values",
|
||||||
"*tags",
|
"*tags",
|
||||||
"*images",
|
"*images",
|
||||||
"*sales_channels",
|
|
||||||
"*variants",
|
"*variants",
|
||||||
"*variants.prices",
|
"*variants.prices",
|
||||||
"*variants.options",
|
"*variants.options",
|
||||||
|
"*variants.options.option_value",
|
||||||
"*sales_channels",
|
"*sales_channels",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user