feat(medusa, admin-ui): Improvements to product categories (#3416)
This commit is contained in:
1 parent
8ed67d2d7d
commit
478d1af8d0
8 files changed
+122
-17
No files matched your search
@@ -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
|
||||||
+13
-10
@@ -1,4 +1,4 @@
|
|||||||
import React, { useCallback, useMemo, useState } from "react"
|
import React, { useCallback, useMemo } from "react"
|
||||||
import Nestable from "react-nestable"
|
import Nestable from "react-nestable"
|
||||||
import { dropRight, get, flatMap } from "lodash"
|
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 ReorderIcon from "../../../components/fundamentals/icons/reorder-icon"
|
||||||
import { useQueryClient } from "@tanstack/react-query"
|
import { useQueryClient } from "@tanstack/react-query"
|
||||||
import useNotification from "../../../hooks/use-notification"
|
import useNotification from "../../../hooks/use-notification"
|
||||||
|
import useToggleState from "../../../hooks/use-toggle-state"
|
||||||
|
|
||||||
type ProductCategoriesListProps = {
|
type ProductCategoriesListProps = {
|
||||||
categories: ProductCategory[]
|
categories: ProductCategory[]
|
||||||
@@ -25,8 +26,8 @@ function ProductCategoriesList(props: ProductCategoriesListProps) {
|
|||||||
const { client } = useMedusa()
|
const { client } = useMedusa()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const notification = useNotification()
|
const notification = useNotification()
|
||||||
const [isUpdating, setIsUpdating] = useState(false)
|
const [isUpdating, enableUpdating, disableUpdating] = useToggleState(false)
|
||||||
|
const [isError, enableError, disableError] = useToggleState(false)
|
||||||
const { categories } = props
|
const { categories } = props
|
||||||
|
|
||||||
const onItemDrop = useCallback(
|
const onItemDrop = useCallback(
|
||||||
@@ -35,7 +36,7 @@ function ProductCategoriesList(props: ProductCategoriesListProps) {
|
|||||||
items: ProductCategory[]
|
items: ProductCategory[]
|
||||||
path: number[]
|
path: number[]
|
||||||
}) => {
|
}) => {
|
||||||
setIsUpdating(true)
|
enableUpdating()
|
||||||
let parentId = null
|
let parentId = null
|
||||||
const { dragItem, items, targetPath } = params
|
const { dragItem, items, targetPath } = params
|
||||||
const [rank] = targetPath.slice(-1)
|
const [rank] = targetPath.slice(-1)
|
||||||
@@ -53,16 +54,19 @@ function ProductCategoriesList(props: ProductCategoriesListProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
disableError()
|
||||||
|
|
||||||
await client.admin.productCategories.update(dragItem.id, {
|
await client.admin.productCategories.update(dragItem.id, {
|
||||||
parent_category_id: parentId,
|
parent_category_id: parentId,
|
||||||
rank,
|
rank,
|
||||||
})
|
})
|
||||||
notification("Success", "New order saved", "success")
|
notification("Success", "Successfully updated category tree", "success")
|
||||||
await queryClient.invalidateQueries(adminProductCategoryKeys.lists())
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
notification("Error", "Failed to save new order", "error")
|
notification("Error", "Failed to update category tree", "error")
|
||||||
|
enableError()
|
||||||
} finally {
|
} finally {
|
||||||
setIsUpdating(false)
|
await queryClient.invalidateQueries(adminProductCategoryKeys.lists())
|
||||||
|
disableUpdating()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[]
|
[]
|
||||||
@@ -71,7 +75,6 @@ function ProductCategoriesList(props: ProductCategoriesListProps) {
|
|||||||
const NestableList = useMemo(
|
const NestableList = useMemo(
|
||||||
() => (
|
() => (
|
||||||
<Nestable
|
<Nestable
|
||||||
collapsed
|
|
||||||
items={categories}
|
items={categories}
|
||||||
onChange={onItemDrop}
|
onChange={onItemDrop}
|
||||||
childrenProp="category_children"
|
childrenProp="category_children"
|
||||||
@@ -98,7 +101,7 @@ function ProductCategoriesList(props: ProductCategoriesListProps) {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
[categories]
|
[categories, isError]
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
+32
-2
@@ -8,11 +8,14 @@ import { ProductCategoriesContext } from "../pages"
|
|||||||
import Tooltip from "../../../components/atoms/tooltip"
|
import Tooltip from "../../../components/atoms/tooltip"
|
||||||
import Button from "../../../components/fundamentals/button"
|
import Button from "../../../components/fundamentals/button"
|
||||||
import Actionables from "../../../components/molecules/actionables"
|
import Actionables from "../../../components/molecules/actionables"
|
||||||
|
import TooltipIcon from "../../../components/molecules/tooltip-icon"
|
||||||
import TrashIcon from "../../../components/fundamentals/icons/trash-icon"
|
import TrashIcon from "../../../components/fundamentals/icons/trash-icon"
|
||||||
import EditIcon from "../../../components/fundamentals/icons/edit-icon"
|
import EditIcon from "../../../components/fundamentals/icons/edit-icon"
|
||||||
import PlusIcon from "../../../components/fundamentals/icons/plus-icon"
|
import PlusIcon from "../../../components/fundamentals/icons/plus-icon"
|
||||||
import FolderOpenIcon from "../../../components/fundamentals/icons/folder-open-icon"
|
import FolderOpenIcon from "../../../components/fundamentals/icons/folder-open-icon"
|
||||||
import TagIcon from "../../../components/fundamentals/icons/tag-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 MoreHorizontalIcon from "../../../components/fundamentals/icons/more-horizontal-icon"
|
||||||
import useNotification from "../../../hooks/use-notification"
|
import useNotification from "../../../hooks/use-notification"
|
||||||
|
|
||||||
@@ -70,11 +73,11 @@ function ProductCategoryListItemDetails(
|
|||||||
<div className="flex w-full items-center justify-between">
|
<div className="flex w-full items-center justify-between">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
{hasChildren && (
|
{hasChildren && (
|
||||||
<div className="flex w-[32px] items-center justify-center">
|
<div className="absolute flex w-[20px] cursor-pointer items-center justify-center">
|
||||||
{props.collapseIcon}
|
{props.collapseIcon}
|
||||||
</div>
|
</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 && <FolderOpenIcon color="#889096" size={18} />}
|
||||||
{!hasChildren && <TagIcon color="#889096" size={18} />}
|
{!hasChildren && <TagIcon color="#889096" size={18} />}
|
||||||
</div>
|
</div>
|
||||||
@@ -85,6 +88,33 @@ function ProductCategoryListItemDetails(
|
|||||||
>
|
>
|
||||||
{item.name}
|
{item.name}
|
||||||
</span>
|
</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>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<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
|
// TODO: temporary here, investigate why `useAdminCreateProductCategory` doesn't invalidate this
|
||||||
await queryClient.invalidateQueries(adminProductCategoryKeys.lists())
|
await queryClient.invalidateQueries(adminProductCategoryKeys.lists())
|
||||||
closeModal()
|
closeModal()
|
||||||
notification("Success", "Created a new product category", "success")
|
notification("Success", "Successfully created a category", "success")
|
||||||
} catch (e) {
|
} 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">
|
<div className="mb-8 flex justify-between gap-6">
|
||||||
<InputField
|
<InputField
|
||||||
|
required
|
||||||
label="Name"
|
label="Name"
|
||||||
type="string"
|
type="string"
|
||||||
name="name"
|
name="name"
|
||||||
|
|||||||
@@ -66,11 +66,13 @@ function EditProductCategoriesSideModal(
|
|||||||
is_internal: !isPublic,
|
is_internal: !isPublic,
|
||||||
})
|
})
|
||||||
|
|
||||||
notification("Success", "Product category updated", "success")
|
notification("Success", "Successfully updated the category", "success")
|
||||||
|
close()
|
||||||
} catch (e) {
|
} 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 = () => {
|
const onClose = () => {
|
||||||
@@ -98,6 +100,7 @@ function EditProductCategoriesSideModal(
|
|||||||
|
|
||||||
<div className="flex-grow">
|
<div className="flex-grow">
|
||||||
<InputField
|
<InputField
|
||||||
|
required
|
||||||
label="Name"
|
label="Name"
|
||||||
type="string"
|
type="string"
|
||||||
name="name"
|
name="name"
|
||||||
@@ -108,6 +111,7 @@ function EditProductCategoriesSideModal(
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<InputField
|
<InputField
|
||||||
|
required
|
||||||
label="Handle"
|
label="Handle"
|
||||||
type="string"
|
type="string"
|
||||||
name="handle"
|
name="handle"
|
||||||
|
|||||||
@@ -125,6 +125,11 @@ export class AdminPostProductCategoriesCategoryReq extends AdminProductCategorie
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
name?: string
|
name?: string
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
@IsNotEmpty()
|
||||||
|
handle?: string
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsInt()
|
@IsInt()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
|
|||||||
Reference in new issue
Block a user