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:
5
.changeset/calm-bananas-roll.md
Normal file
5
.changeset/calm-bananas-roll.md
Normal file
@@ -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 InputField from "../../../components/molecules/input"
|
||||
import Select from "../../../components/molecules/select"
|
||||
import TreeCrumbs from "../components/tree-crumbs"
|
||||
import { useQueryClient } from "@tanstack/react-query"
|
||||
|
||||
const visibilityOptions = [
|
||||
@@ -36,7 +37,7 @@ type CreateProductCategoryProps = {
|
||||
* Focus modal container for creating Publishable Keys.
|
||||
*/
|
||||
function CreateProductCategory(props: CreateProductCategoryProps) {
|
||||
const { closeModal, parentCategory } = props
|
||||
const { closeModal, parentCategory, categories } = props
|
||||
const notification = useNotification()
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
@@ -90,9 +91,21 @@ function CreateProductCategory(props: CreateProductCategoryProps) {
|
||||
|
||||
<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]">
|
||||
<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}`}
|
||||
</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>
|
||||
|
||||
<div className="mb-8 flex justify-between gap-6">
|
||||
|
||||
@@ -9,6 +9,7 @@ import CrossIcon from "../../../components/fundamentals/icons/cross-icon"
|
||||
import InputField from "../../../components/molecules/input"
|
||||
import Select from "../../../components/molecules/select"
|
||||
import useNotification from "../../../hooks/use-notification"
|
||||
import TreeCrumbs from "../components/tree-crumbs"
|
||||
|
||||
const visibilityOptions = [
|
||||
{
|
||||
@@ -35,7 +36,7 @@ type EditProductCategoriesSideModalProps = {
|
||||
function EditProductCategoriesSideModal(
|
||||
props: EditProductCategoriesSideModalProps
|
||||
) {
|
||||
const { isVisible, close, activeCategory } = props
|
||||
const { isVisible, close, activeCategory, categories } = props
|
||||
|
||||
const [name, setName] = useState("")
|
||||
const [handle, setHandle] = useState("")
|
||||
@@ -81,10 +82,9 @@ function EditProductCategoriesSideModal(
|
||||
|
||||
return (
|
||||
<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 === */}
|
||||
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center justify-between p-6">
|
||||
<h3 className="inter-large-semibold flex items-center gap-2 text-xl text-gray-900">
|
||||
Edit product category
|
||||
</h3>
|
||||
@@ -96,9 +96,17 @@ function EditProductCategoriesSideModal(
|
||||
<CrossIcon size={20} className="text-grey-50" />
|
||||
</Button>
|
||||
</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
|
||||
required
|
||||
label="Name"
|
||||
@@ -138,15 +146,12 @@ function EditProductCategoriesSideModal(
|
||||
onChange={(o) => setIsPublic(o.value === "public")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* === DIVIDER === */}
|
||||
<div className="block h-[1px] bg-gray-200" />
|
||||
|
||||
<div
|
||||
className="block h-[1px] bg-gray-200"
|
||||
style={{ margin: "24px -24px" }}
|
||||
/>
|
||||
{/* === FOOTER === */}
|
||||
|
||||
<div className="flex justify-end gap-2">
|
||||
<div className="flex justify-end gap-2 p-3">
|
||||
<Button size="small" variant="ghost" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
|
||||
@@ -8,6 +8,7 @@ import BodyCard from "../../../components/organisms/body-card"
|
||||
import CreateProductCategory from "../modals/add-product-category"
|
||||
import ProductCategoriesList from "../components/product-categories-list"
|
||||
import EditProductCategoriesSideModal from "../modals/edit-product-category"
|
||||
import { flattenCategoryTree } from "../utils"
|
||||
|
||||
/**
|
||||
* Product categories empty state placeholder.
|
||||
@@ -46,7 +47,7 @@ function ProductCategoryPage() {
|
||||
|
||||
const [activeCategory, setActiveCategory] = useState<ProductCategory>()
|
||||
|
||||
const { product_categories: categories, isLoading } =
|
||||
const { product_categories: categories = [], isLoading } =
|
||||
useAdminProductCategories({
|
||||
parent_category_id: "null",
|
||||
include_descendants_tree: true,
|
||||
@@ -59,7 +60,7 @@ function ProductCategoryPage() {
|
||||
},
|
||||
]
|
||||
|
||||
const showPlaceholder = !isLoading && !categories?.length
|
||||
const showPlaceholder = !isLoading && !categories.length
|
||||
|
||||
const editCategory = (category: ProductCategory) => {
|
||||
setActiveCategory(category)
|
||||
@@ -67,10 +68,14 @@ function ProductCategoryPage() {
|
||||
}
|
||||
|
||||
const createSubCategory = (category: ProductCategory) => {
|
||||
if (isLoading) {
|
||||
return
|
||||
}
|
||||
setActiveCategory(category)
|
||||
showCreateModal()
|
||||
}
|
||||
|
||||
const flattenedCategories = flattenCategoryTree(categories)
|
||||
const context = {
|
||||
editCategory,
|
||||
createSubCategory,
|
||||
@@ -97,6 +102,7 @@ function ProductCategoryPage() {
|
||||
{isCreateModalVisible && (
|
||||
<CreateProductCategory
|
||||
parentCategory={activeCategory}
|
||||
categories={flattenedCategories}
|
||||
closeModal={() => {
|
||||
hideCreateModal()
|
||||
setActiveCategory(undefined)
|
||||
@@ -108,6 +114,7 @@ function ProductCategoryPage() {
|
||||
close={hideEditModal}
|
||||
activeCategory={activeCategory}
|
||||
isVisible={!!activeCategory && isEditModalVisible}
|
||||
categories={flattenedCategories}
|
||||
/>
|
||||
</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