Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/i18n/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -568,6 +568,12 @@
"categories": {
"label": "Categories"
},
"primaryCategory": {
"label": "Primary Category"
},
"secondaryCategories": {
"label": "Secondary Categories"
},
"tags": {
"label": "Tags"
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { PencilSquare } from "@medusajs/icons"
import { HttpTypes } from "@medusajs/types"
import { Badge, Container, Heading, Tooltip } from "@medusajs/ui"
import { useTranslation } from "react-i18next"
import { Link } from "react-router-dom"
import { ActionMenu } from "../../../../../components/common/action-menu"
import { SectionRow } from "../../../../../components/common/section"
import { useDashboardExtension } from "../../../../../extensions"
import { AdminProductWithAttributes } from "../../../../../types/products"
import { useProductCategories } from "../../../../../hooks/api/categories"

type ProductOrganizationSectionProps = {
product: HttpTypes.AdminProduct
product: AdminProductWithAttributes
}

export const ProductOrganizationSection = ({
Expand All @@ -17,6 +18,20 @@ export const ProductOrganizationSection = ({
const { t } = useTranslation()
const { getDisplays } = useDashboardExtension()

// Fetch ALL categories and then filter to show only secondary ones
const { product_categories: allCategories } = useProductCategories()

const primaryCategory = product.categories?.[0]
const primaryCategoryId = primaryCategory?.id
const secondaryCategoryIds = product.secondary_categories
?.map((sc) => sc.category_id)
.filter((id) => id !== primaryCategoryId) || []

// Filter fetched categories to only include those in secondaryCategoryIds
const secondaryCategories = allCategories?.filter((cat) =>
secondaryCategoryIds.includes(cat.id)
)

return (
<Container className="divide-y p-0">
<div className="flex items-center justify-between px-6 py-4">
Expand Down Expand Up @@ -75,14 +90,26 @@ export const ProductOrganizationSection = ({
/>

<SectionRow
title={t("fields.categories")}
title={t("products.fields.primaryCategory.label")}
value={
primaryCategory ? (
<OrganizationTag
label={primaryCategory.name}
to={`/categories/${primaryCategory.id}`}
/>
) : undefined
}
/>

<SectionRow
title={t("products.fields.secondaryCategories.label")}
value={
product.categories?.length
? product.categories.map((pcat) => (
secondaryCategories?.length
? secondaryCategories.map((category: any) => (
<OrganizationTag
key={pcat.id}
label={pcat.name}
to={`/categories/${pcat.id}`}
key={category.id}
label={category.name}
to={`/categories/${category.id}`}
/>
))
: undefined
Expand Down
2 changes: 1 addition & 1 deletion src/routes/products/product-detail/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const productDetailQuery = (id: string) => ({
fetchQuery(`/vendor/products/${id}`, {
method: "GET",
query: {
fields: "*variants.inventory_items,*categories",
fields: "*variants.inventory_items,*categories,*secondary_categories",
},
}),
})
Expand Down
2 changes: 1 addition & 1 deletion src/routes/products/product-detail/product-detail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { ProductAdditionalAttributesSection } from "./components/product-additio
export const ProductDetail = () => {
const { id } = useParams()
const { product, isLoading, isError, error } = useProduct(id!, {
fields: "*variants.inventory_items,*categories",
fields: "*variants.inventory_items,*categories,*secondary_categories",
})

const { getWidgets } = useDashboardExtension()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { HttpTypes } from "@medusajs/types"
import { Button, toast } from "@medusajs/ui"
import { useTranslation } from "react-i18next"
import { useEffect } from "react"
import * as zod from "zod"

import { Form } from "../../../../../components/common/form"
Expand All @@ -15,16 +15,17 @@ import {
import { useUpdateProduct } from "../../../../../hooks/api/products"
import { useComboboxData } from "../../../../../hooks/use-combobox-data"
import { fetchQuery } from "../../../../../lib/client"
import { AdminProductWithAttributes } from "../../../../../types/products"

type ProductOrganizationFormProps = {
product: HttpTypes.AdminProduct
product: AdminProductWithAttributes
}

const ProductOrganizationSchema = zod.object({
type_id: zod.string().nullable(),
collection_id: zod.string().nullable(),
category_ids: zod.string().nullable(),
// category_ids: zod.array(zod.string()),
primary_category_id: zod.string().nullable(),
secondary_category_ids: zod.array(zod.string()),
tag_ids: zod.array(zod.string()),
})

Expand Down Expand Up @@ -98,7 +99,11 @@ export const ProductOrganizationForm = ({
defaultValues: {
type_id: product.type_id ?? "",
collection_id: product.collection_id ?? "",
category_ids: product.categories?.[0]?.id || "",
primary_category_id: product.categories?.[0]?.id || "",
secondary_category_ids:
product.secondary_categories
?.map((sc) => sc.category_id)
.filter((id) => id !== product.categories?.[0]?.id) || [], // Filter out primary category
tag_ids: product.tags?.map((t) => t.id) || [],
},
schema: ProductOrganizationSchema,
Expand All @@ -108,14 +113,43 @@ export const ProductOrganizationForm = ({

const { mutateAsync, isPending } = useUpdateProduct(product.id)

// Watch for changes in primary category and remove it from secondary categories
const primaryCategoryId = form.watch("primary_category_id")
useEffect(() => {
if (primaryCategoryId) {
const currentSecondaryIds = form.getValues("secondary_category_ids")
if (currentSecondaryIds?.includes(primaryCategoryId)) {
form.setValue(
"secondary_category_ids",
currentSecondaryIds.filter((id) => id !== primaryCategoryId)
)
}
}
}, [primaryCategoryId, form])

const handleSubmit = form.handleSubmit(async (data) => {
// Filter out primary category from secondary categories to avoid duplicates
const filteredSecondaryCategories = (data.secondary_category_ids || []).filter(
(id) => id !== data.primary_category_id
)

await mutateAsync(
{
type_id: data.type_id || null,
collection_id: data.collection_id || null,
categories: [{ id: data.category_ids || "" }],
categories: data.primary_category_id
? [{ id: data.primary_category_id }]
: [],
tags: data.tag_ids?.map((t) => ({ id: t })),
},
additional_data: {
secondary_categories: [
{
product_id: product.id,
secondary_categories_ids: filteredSecondaryCategories,
},
],
},
} as any,
{
onSuccess: ({ product }) => {
toast.success(
Expand Down Expand Up @@ -185,15 +219,14 @@ export const ProductOrganizationForm = ({
/>
<Form.Field
control={form.control}
name="category_ids"
name="primary_category_id"
render={({ field }) => {
return (
<Form.Item>
<Form.Label optional>
{t("products.fields.categories.label")}
{t("products.fields.primaryCategory.label")}
</Form.Label>
<Form.Control>
{/* <CategoryCombobox {...field} /> */}
<Combobox
{...field}
multiple={false}
Expand All @@ -207,6 +240,34 @@ export const ProductOrganizationForm = ({
)
}}
/>
<Form.Field
control={form.control}
name="secondary_category_ids"
render={({ field }) => {
// Filter out primary category from secondary categories options
const filteredOptions = categories.options.filter(
(option) => option.value !== primaryCategoryId
)

return (
<Form.Item>
<Form.Label optional>
{t("products.fields.secondaryCategories.label")}
</Form.Label>
<Form.Control>
<Combobox
{...field}
multiple
options={filteredOptions}
onSearchValueChange={categories.onSearchValueChange}
searchValue={categories.searchValue}
/>
</Form.Control>
<Form.ErrorMessage />
</Form.Item>
)
}}
/>
<Form.Field
control={form.control}
name="tag_ids"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export const ProductOrganization = () => {
const { t } = useTranslation()

const { product, isLoading, isError, error } = useProduct(id!, {
fields: "*categories",
fields: "*categories,*secondary_categories",
})

if (isError) {
Expand Down
50 changes: 50 additions & 0 deletions src/types/products.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { HttpTypes } from "@medusajs/types"

export interface ProductAttributePossibleValue {
id: string
value: string
rank: number
metadata: Record<string, any>
attribute_id: string
created_at: string
updated_at: string
deleted_at: string | null
}

export interface ProductAttributeCategory {
id: string
name: string
}

export interface ProductAttribute {
id: string
name: string
description: string
handle: string
is_filterable: boolean
ui_component: "toggle" | "select" | "text" | "text_area" | "unit"
metadata: Record<string, any>
possible_values: ProductAttributePossibleValue[]
product_categories: ProductAttributeCategory[]
}

export interface ProductAttributesResponse {
attributes: ProductAttribute[]
}

export interface SecondaryCategory {
id: string
category_id: string
created_at: string
updated_at: string
deleted_at: string | null
}

export interface AdminProductWithAttributes extends HttpTypes.AdminProduct {
attribute_values?: {
attribute_id: string
value: string
}[]
secondary_categories?: SecondaryCategory[]
}