feat(admin-ui): added breadcrumbs for categories on create/edit modal (#3420)
What: - Adds breadcrumbs to create modal - Adds breadcrumbs to edit modal <img width="581" alt="2" src="https://user-images.githubusercontent.com/5105988/223782603-f168d554-65bd-4cfc-bdcd-eabdd9f06b20.png"> <img width="1115" alt="1" src="https://user-images.githubusercontent.com/5105988/223782607-1ae441c9-c9eb-4cb0-9015-2038db55dd64.png"> RESOLVES CORE-1210
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/admin-ui": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
feat(admin-ui): added breadcrumbs for categories on create/edit modal
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import React from "react"
|
||||||
|
import { ProductCategory } from "@medusajs/medusa"
|
||||||
|
import { getAncestors } from "../utils"
|
||||||
|
|
||||||
|
type TreeCrumbsProps = React.HtmlHTMLAttributes<HTMLDivElement> & {
|
||||||
|
nodes: ProductCategory[]
|
||||||
|
currentNode: ProductCategory
|
||||||
|
showPlaceholder: boolean
|
||||||
|
placeholderText: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const TreeCrumbs: React.FC<TreeCrumbsProps> = ({
|
||||||
|
nodes,
|
||||||
|
currentNode,
|
||||||
|
showPlaceholder = false,
|
||||||
|
placeholderText = "",
|
||||||
|
...props
|
||||||
|
}) => {
|
||||||
|
const ancestors = getAncestors(currentNode, nodes)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span {...props}>
|
||||||
|
<span className="text-grey-40">
|
||||||
|
{ancestors.map((ancestor, index) => {
|
||||||
|
const categoryName = ancestor.name
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={ancestor.id} className="inline-block">
|
||||||
|
<span>
|
||||||
|
{categoryName.length > 25
|
||||||
|
? categoryName.substring(0, 25) + "..."
|
||||||
|
: categoryName}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{(showPlaceholder || ancestors.length !== index + 1) && (
|
||||||
|
<span className="mx-2">/</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
|
||||||
|
{showPlaceholder && (
|
||||||
|
<span>
|
||||||
|
<span className="border-grey-40 rounded-[10px] border-[1px] border-dashed px-[8px] py-[4px]">
|
||||||
|
{placeholderText}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TreeCrumbs
|
||||||
@@ -12,6 +12,7 @@ import Button from "../../../components/fundamentals/button"
|
|||||||
import CrossIcon from "../../../components/fundamentals/icons/cross-icon"
|
import CrossIcon from "../../../components/fundamentals/icons/cross-icon"
|
||||||
import InputField from "../../../components/molecules/input"
|
import InputField from "../../../components/molecules/input"
|
||||||
import Select from "../../../components/molecules/select"
|
import Select from "../../../components/molecules/select"
|
||||||
|
import TreeCrumbs from "../components/tree-crumbs"
|
||||||
import { useQueryClient } from "@tanstack/react-query"
|
import { useQueryClient } from "@tanstack/react-query"
|
||||||
|
|
||||||
const visibilityOptions = [
|
const visibilityOptions = [
|
||||||
@@ -36,7 +37,7 @@ type CreateProductCategoryProps = {
|
|||||||
* Focus modal container for creating Publishable Keys.
|
* Focus modal container for creating Publishable Keys.
|
||||||
*/
|
*/
|
||||||
function CreateProductCategory(props: CreateProductCategoryProps) {
|
function CreateProductCategory(props: CreateProductCategoryProps) {
|
||||||
const { closeModal, parentCategory } = props
|
const { closeModal, parentCategory, categories } = props
|
||||||
const notification = useNotification()
|
const notification = useNotification()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
|
||||||
@@ -90,9 +91,21 @@ function CreateProductCategory(props: CreateProductCategoryProps) {
|
|||||||
|
|
||||||
<FocusModal.Main className="no-scrollbar flex w-full justify-center">
|
<FocusModal.Main className="no-scrollbar flex w-full justify-center">
|
||||||
<div className="small:w-4/5 medium:w-7/12 large:w-6/12 my-16 max-w-[700px]">
|
<div className="small:w-4/5 medium:w-7/12 large:w-6/12 my-16 max-w-[700px]">
|
||||||
<h1 className="inter-xlarge-semibold text-grey-90 pb-8">
|
<h1 className="inter-xlarge-semibold text-grey-90 pb-6">
|
||||||
Add category {parentCategory && `to ${parentCategory.name}`}
|
Add category {parentCategory && `to ${parentCategory.name}`}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
|
{parentCategory && (
|
||||||
|
<div className="mb-6">
|
||||||
|
<TreeCrumbs
|
||||||
|
nodes={categories}
|
||||||
|
currentNode={parentCategory}
|
||||||
|
showPlaceholder={true}
|
||||||
|
placeholderText={name || "New"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<h4 className="inter-large-semibold text-grey-90 pb-1">Details</h4>
|
<h4 className="inter-large-semibold text-grey-90 pb-1">Details</h4>
|
||||||
|
|
||||||
<div className="mb-8 flex justify-between gap-6">
|
<div className="mb-8 flex justify-between gap-6">
|
||||||
|
|||||||
+17
-12
@@ -9,6 +9,7 @@ import CrossIcon from "../../../components/fundamentals/icons/cross-icon"
|
|||||||
import InputField from "../../../components/molecules/input"
|
import InputField from "../../../components/molecules/input"
|
||||||
import Select from "../../../components/molecules/select"
|
import Select from "../../../components/molecules/select"
|
||||||
import useNotification from "../../../hooks/use-notification"
|
import useNotification from "../../../hooks/use-notification"
|
||||||
|
import TreeCrumbs from "../components/tree-crumbs"
|
||||||
|
|
||||||
const visibilityOptions = [
|
const visibilityOptions = [
|
||||||
{
|
{
|
||||||
@@ -35,7 +36,7 @@ type EditProductCategoriesSideModalProps = {
|
|||||||
function EditProductCategoriesSideModal(
|
function EditProductCategoriesSideModal(
|
||||||
props: EditProductCategoriesSideModalProps
|
props: EditProductCategoriesSideModalProps
|
||||||
) {
|
) {
|
||||||
const { isVisible, close, activeCategory } = props
|
const { isVisible, close, activeCategory, categories } = props
|
||||||
|
|
||||||
const [name, setName] = useState("")
|
const [name, setName] = useState("")
|
||||||
const [handle, setHandle] = useState("")
|
const [handle, setHandle] = useState("")
|
||||||
@@ -81,10 +82,9 @@ function EditProductCategoriesSideModal(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SideModal close={onClose} isVisible={!!isVisible}>
|
<SideModal close={onClose} isVisible={!!isVisible}>
|
||||||
<div className="flex h-full flex-col justify-between p-6">
|
<div className="flex h-full flex-col justify-between">
|
||||||
{/* === HEADER === */}
|
{/* === HEADER === */}
|
||||||
|
<div className="flex items-center justify-between p-6">
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<h3 className="inter-large-semibold flex items-center gap-2 text-xl text-gray-900">
|
<h3 className="inter-large-semibold flex items-center gap-2 text-xl text-gray-900">
|
||||||
Edit product category
|
Edit product category
|
||||||
</h3>
|
</h3>
|
||||||
@@ -96,9 +96,17 @@ function EditProductCategoriesSideModal(
|
|||||||
<CrossIcon size={20} className="text-grey-50" />
|
<CrossIcon size={20} className="text-grey-50" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{/* === DIVIDER === */}
|
|
||||||
|
|
||||||
<div className="flex-grow">
|
{/* === DIVIDER === */}
|
||||||
|
<div className="block h-[1px] bg-gray-200" />
|
||||||
|
|
||||||
|
{activeCategory && (
|
||||||
|
<div className="mt-[25px] px-6">
|
||||||
|
<TreeCrumbs nodes={categories} currentNode={activeCategory} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex-grow px-6">
|
||||||
<InputField
|
<InputField
|
||||||
required
|
required
|
||||||
label="Name"
|
label="Name"
|
||||||
@@ -138,15 +146,12 @@ function EditProductCategoriesSideModal(
|
|||||||
onChange={(o) => setIsPublic(o.value === "public")}
|
onChange={(o) => setIsPublic(o.value === "public")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* === DIVIDER === */}
|
{/* === DIVIDER === */}
|
||||||
|
<div className="block h-[1px] bg-gray-200" />
|
||||||
|
|
||||||
<div
|
|
||||||
className="block h-[1px] bg-gray-200"
|
|
||||||
style={{ margin: "24px -24px" }}
|
|
||||||
/>
|
|
||||||
{/* === FOOTER === */}
|
{/* === FOOTER === */}
|
||||||
|
<div className="flex justify-end gap-2 p-3">
|
||||||
<div className="flex justify-end gap-2">
|
|
||||||
<Button size="small" variant="ghost" onClick={onClose}>
|
<Button size="small" variant="ghost" onClick={onClose}>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import BodyCard from "../../../components/organisms/body-card"
|
|||||||
import CreateProductCategory from "../modals/add-product-category"
|
import CreateProductCategory from "../modals/add-product-category"
|
||||||
import ProductCategoriesList from "../components/product-categories-list"
|
import ProductCategoriesList from "../components/product-categories-list"
|
||||||
import EditProductCategoriesSideModal from "../modals/edit-product-category"
|
import EditProductCategoriesSideModal from "../modals/edit-product-category"
|
||||||
|
import { flattenCategoryTree } from "../utils"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Product categories empty state placeholder.
|
* Product categories empty state placeholder.
|
||||||
@@ -46,7 +47,7 @@ function ProductCategoryPage() {
|
|||||||
|
|
||||||
const [activeCategory, setActiveCategory] = useState<ProductCategory>()
|
const [activeCategory, setActiveCategory] = useState<ProductCategory>()
|
||||||
|
|
||||||
const { product_categories: categories, isLoading } =
|
const { product_categories: categories = [], isLoading } =
|
||||||
useAdminProductCategories({
|
useAdminProductCategories({
|
||||||
parent_category_id: "null",
|
parent_category_id: "null",
|
||||||
include_descendants_tree: true,
|
include_descendants_tree: true,
|
||||||
@@ -59,7 +60,7 @@ function ProductCategoryPage() {
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const showPlaceholder = !isLoading && !categories?.length
|
const showPlaceholder = !isLoading && !categories.length
|
||||||
|
|
||||||
const editCategory = (category: ProductCategory) => {
|
const editCategory = (category: ProductCategory) => {
|
||||||
setActiveCategory(category)
|
setActiveCategory(category)
|
||||||
@@ -67,10 +68,14 @@ function ProductCategoryPage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const createSubCategory = (category: ProductCategory) => {
|
const createSubCategory = (category: ProductCategory) => {
|
||||||
|
if (isLoading) {
|
||||||
|
return
|
||||||
|
}
|
||||||
setActiveCategory(category)
|
setActiveCategory(category)
|
||||||
showCreateModal()
|
showCreateModal()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const flattenedCategories = flattenCategoryTree(categories)
|
||||||
const context = {
|
const context = {
|
||||||
editCategory,
|
editCategory,
|
||||||
createSubCategory,
|
createSubCategory,
|
||||||
@@ -97,6 +102,7 @@ function ProductCategoryPage() {
|
|||||||
{isCreateModalVisible && (
|
{isCreateModalVisible && (
|
||||||
<CreateProductCategory
|
<CreateProductCategory
|
||||||
parentCategory={activeCategory}
|
parentCategory={activeCategory}
|
||||||
|
categories={flattenedCategories}
|
||||||
closeModal={() => {
|
closeModal={() => {
|
||||||
hideCreateModal()
|
hideCreateModal()
|
||||||
setActiveCategory(undefined)
|
setActiveCategory(undefined)
|
||||||
@@ -108,6 +114,7 @@ function ProductCategoryPage() {
|
|||||||
close={hideEditModal}
|
close={hideEditModal}
|
||||||
activeCategory={activeCategory}
|
activeCategory={activeCategory}
|
||||||
isVisible={!!activeCategory && isEditModalVisible}
|
isVisible={!!activeCategory && isEditModalVisible}
|
||||||
|
categories={flattenedCategories}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
export const flattenCategoryTree = (rootCategories) => {
|
||||||
|
return rootCategories.reduce((acc, category) => {
|
||||||
|
if (category?.category_children.length) {
|
||||||
|
acc = acc
|
||||||
|
.concat(flattenCategoryTree(category.category_children))
|
||||||
|
.concat(category)
|
||||||
|
} else {
|
||||||
|
acc.push(category)
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}, [])
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getAncestors = (targetNode, nodes, acc = []) => {
|
||||||
|
let parentCategory = null
|
||||||
|
|
||||||
|
acc.push(targetNode)
|
||||||
|
|
||||||
|
if (targetNode.parent_category_id) {
|
||||||
|
parentCategory = nodes.find((n) => n.id === targetNode.parent_category_id)
|
||||||
|
|
||||||
|
acc = getAncestors(parentCategory, nodes, acc)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!parentCategory) {
|
||||||
|
return acc.reverse()
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user