feat(medusa, admin-ui): Improvements to product categories (#3416)
This commit is contained in:
11
.changeset/smart-spies-bake.md
Normal file
11
.changeset/smart-spies-bake.md
Normal file
@@ -0,0 +1,11 @@
|
||||
---
|
||||
"@medusajs/admin-ui": patch
|
||||
"@medusajs/medusa": patch
|
||||
---
|
||||
|
||||
feat(medusa, admin-ui): Improvements to product categories
|
||||
- Adds name as required in category create form
|
||||
- Adds name and handle as required in category edit form
|
||||
- Updates message on create/update forms
|
||||
- Adds category indicators for is_internal and is_active fields in the tree list
|
||||
- Fixes bug where tree is not reset when update fails
|
||||
@@ -0,0 +1,29 @@
|
||||
import React from "react"
|
||||
import IconProps from "../types/icon-type"
|
||||
|
||||
type TagDotIconProps = IconProps & {
|
||||
outerColor: string
|
||||
}
|
||||
|
||||
const TagDotIcon: React.FC<TagDotIconProps> = ({
|
||||
size = "24px",
|
||||
color = "#E5484D",
|
||||
outerColor = "transparent",
|
||||
...attributes
|
||||
}) => {
|
||||
return (
|
||||
<svg
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 32 32"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...attributes}
|
||||
>
|
||||
<circle cx="16" cy="16" r="6" fill={outerColor} />
|
||||
<circle cx="16" cy="16" r="3" fill={color} />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export default TagDotIcon
|
||||
@@ -0,0 +1,20 @@
|
||||
import React from "react"
|
||||
import Tooltip, { TooltipProps } from "../../atoms/tooltip"
|
||||
|
||||
type TooltipIconProps = TooltipProps & {
|
||||
icon: React.ReactNode
|
||||
}
|
||||
|
||||
const TooltipIcon: React.FC<TooltipIconProps> = ({
|
||||
content,
|
||||
icon,
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<Tooltip content={content} side="top" {...props}>
|
||||
{icon}
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
export default TooltipIcon
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useMemo, useState } from "react"
|
||||
import React, { useCallback, useMemo } from "react"
|
||||
import Nestable from "react-nestable"
|
||||
import { dropRight, get, flatMap } from "lodash"
|
||||
|
||||
@@ -13,6 +13,7 @@ import ProductCategoryListItemDetails from "./product-category-list-item-details
|
||||
import ReorderIcon from "../../../components/fundamentals/icons/reorder-icon"
|
||||
import { useQueryClient } from "@tanstack/react-query"
|
||||
import useNotification from "../../../hooks/use-notification"
|
||||
import useToggleState from "../../../hooks/use-toggle-state"
|
||||
|
||||
type ProductCategoriesListProps = {
|
||||
categories: ProductCategory[]
|
||||
@@ -25,8 +26,8 @@ function ProductCategoriesList(props: ProductCategoriesListProps) {
|
||||
const { client } = useMedusa()
|
||||
const queryClient = useQueryClient()
|
||||
const notification = useNotification()
|
||||
const [isUpdating, setIsUpdating] = useState(false)
|
||||
|
||||
const [isUpdating, enableUpdating, disableUpdating] = useToggleState(false)
|
||||
const [isError, enableError, disableError] = useToggleState(false)
|
||||
const { categories } = props
|
||||
|
||||
const onItemDrop = useCallback(
|
||||
@@ -35,7 +36,7 @@ function ProductCategoriesList(props: ProductCategoriesListProps) {
|
||||
items: ProductCategory[]
|
||||
path: number[]
|
||||
}) => {
|
||||
setIsUpdating(true)
|
||||
enableUpdating()
|
||||
let parentId = null
|
||||
const { dragItem, items, targetPath } = params
|
||||
const [rank] = targetPath.slice(-1)
|
||||
@@ -53,16 +54,19 @@ function ProductCategoriesList(props: ProductCategoriesListProps) {
|
||||
}
|
||||
|
||||
try {
|
||||
disableError()
|
||||
|
||||
await client.admin.productCategories.update(dragItem.id, {
|
||||
parent_category_id: parentId,
|
||||
rank,
|
||||
})
|
||||
notification("Success", "New order saved", "success")
|
||||
await queryClient.invalidateQueries(adminProductCategoryKeys.lists())
|
||||
notification("Success", "Successfully updated category tree", "success")
|
||||
} catch (e) {
|
||||
notification("Error", "Failed to save new order", "error")
|
||||
notification("Error", "Failed to update category tree", "error")
|
||||
enableError()
|
||||
} finally {
|
||||
setIsUpdating(false)
|
||||
await queryClient.invalidateQueries(adminProductCategoryKeys.lists())
|
||||
disableUpdating()
|
||||
}
|
||||
},
|
||||
[]
|
||||
@@ -71,7 +75,6 @@ function ProductCategoriesList(props: ProductCategoriesListProps) {
|
||||
const NestableList = useMemo(
|
||||
() => (
|
||||
<Nestable
|
||||
collapsed
|
||||
items={categories}
|
||||
onChange={onItemDrop}
|
||||
childrenProp="category_children"
|
||||
@@ -98,7 +101,7 @@ function ProductCategoriesList(props: ProductCategoriesListProps) {
|
||||
)}
|
||||
/>
|
||||
),
|
||||
[categories]
|
||||
[categories, isError]
|
||||
)
|
||||
|
||||
return (
|
||||
|
||||
@@ -8,11 +8,14 @@ import { ProductCategoriesContext } from "../pages"
|
||||
import Tooltip from "../../../components/atoms/tooltip"
|
||||
import Button from "../../../components/fundamentals/button"
|
||||
import Actionables from "../../../components/molecules/actionables"
|
||||
import TooltipIcon from "../../../components/molecules/tooltip-icon"
|
||||
import TrashIcon from "../../../components/fundamentals/icons/trash-icon"
|
||||
import EditIcon from "../../../components/fundamentals/icons/edit-icon"
|
||||
import PlusIcon from "../../../components/fundamentals/icons/plus-icon"
|
||||
import FolderOpenIcon from "../../../components/fundamentals/icons/folder-open-icon"
|
||||
import TagIcon from "../../../components/fundamentals/icons/tag-icon"
|
||||
import TagDotIcon from "../../../components/fundamentals/icons/tag-dot-icon"
|
||||
import EyeOffIcon from "../../../components/fundamentals/icons/eye-off-icon"
|
||||
import MoreHorizontalIcon from "../../../components/fundamentals/icons/more-horizontal-icon"
|
||||
import useNotification from "../../../hooks/use-notification"
|
||||
|
||||
@@ -70,11 +73,11 @@ function ProductCategoryListItemDetails(
|
||||
<div className="flex w-full items-center justify-between">
|
||||
<div className="flex items-center">
|
||||
{hasChildren && (
|
||||
<div className="flex w-[32px] items-center justify-center">
|
||||
<div className="absolute flex w-[20px] cursor-pointer items-center justify-center">
|
||||
{props.collapseIcon}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex w-[32px] items-center justify-center">
|
||||
<div className="ml-[20px] flex w-[32px] items-center justify-center">
|
||||
{hasChildren && <FolderOpenIcon color="#889096" size={18} />}
|
||||
{!hasChildren && <TagIcon color="#889096" size={18} />}
|
||||
</div>
|
||||
@@ -85,6 +88,33 @@ function ProductCategoryListItemDetails(
|
||||
>
|
||||
{item.name}
|
||||
</span>
|
||||
|
||||
<div className="flex w-[64px] items-center justify-center">
|
||||
{!item.is_active && (
|
||||
<TooltipIcon
|
||||
content="Category status is inactive"
|
||||
icon={
|
||||
<TagDotIcon
|
||||
size="32"
|
||||
className="cursor-pointer"
|
||||
outerColor="#FFE5E5"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{item.is_internal && (
|
||||
<TooltipIcon
|
||||
content="Category visibility is private"
|
||||
icon={
|
||||
<EyeOffIcon
|
||||
color="#889096"
|
||||
size={18}
|
||||
className="cursor-pointer"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
|
||||
@@ -59,9 +59,11 @@ function CreateProductCategory(props: CreateProductCategoryProps) {
|
||||
// TODO: temporary here, investigate why `useAdminCreateProductCategory` doesn't invalidate this
|
||||
await queryClient.invalidateQueries(adminProductCategoryKeys.lists())
|
||||
closeModal()
|
||||
notification("Success", "Created a new product category", "success")
|
||||
notification("Success", "Successfully created a category", "success")
|
||||
} catch (e) {
|
||||
notification("Error", "Failed to create a new product category", "error")
|
||||
const errorMessage =
|
||||
e.response?.data?.message || "Failed to create a new category"
|
||||
notification("Error", errorMessage, "error")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,6 +97,7 @@ function CreateProductCategory(props: CreateProductCategoryProps) {
|
||||
|
||||
<div className="mb-8 flex justify-between gap-6">
|
||||
<InputField
|
||||
required
|
||||
label="Name"
|
||||
type="string"
|
||||
name="name"
|
||||
|
||||
@@ -66,11 +66,13 @@ function EditProductCategoriesSideModal(
|
||||
is_internal: !isPublic,
|
||||
})
|
||||
|
||||
notification("Success", "Product category updated", "success")
|
||||
notification("Success", "Successfully updated the category", "success")
|
||||
close()
|
||||
} catch (e) {
|
||||
notification("Error", "Failed to update the category", "error")
|
||||
const errorMessage =
|
||||
e.response?.data?.message || "Failed to update the category"
|
||||
notification("Error", errorMessage, "error")
|
||||
}
|
||||
close()
|
||||
}
|
||||
|
||||
const onClose = () => {
|
||||
@@ -98,6 +100,7 @@ function EditProductCategoriesSideModal(
|
||||
|
||||
<div className="flex-grow">
|
||||
<InputField
|
||||
required
|
||||
label="Name"
|
||||
type="string"
|
||||
name="name"
|
||||
@@ -108,6 +111,7 @@ function EditProductCategoriesSideModal(
|
||||
/>
|
||||
|
||||
<InputField
|
||||
required
|
||||
label="Handle"
|
||||
type="string"
|
||||
name="handle"
|
||||
|
||||
@@ -125,6 +125,11 @@ export class AdminPostProductCategoriesCategoryReq extends AdminProductCategorie
|
||||
@IsOptional()
|
||||
name?: string
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
handle?: string
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@IsNotEmpty()
|
||||
|
||||
Reference in New Issue
Block a user