From 19ecccc2a8c9b0d89e1d22242bc40cd685dfc192 Mon Sep 17 00:00:00 2001 From: Riqwan Thamir Date: Wed, 8 Feb 2023 12:33:59 +0100 Subject: [PATCH 01/28] chore: empty state categories list page --- .../fundamentals/icons/swatch-icon/index.tsx | 29 +++++++ src/components/organisms/body-card.tsx | 78 +++++++++++-------- src/components/organisms/sidebar/index.tsx | 12 ++- src/domain/product-categories/index.tsx | 43 ++++++++++ src/pages/a.jsx | 5 ++ 5 files changed, 134 insertions(+), 33 deletions(-) create mode 100644 src/components/fundamentals/icons/swatch-icon/index.tsx create mode 100644 src/domain/product-categories/index.tsx diff --git a/src/components/fundamentals/icons/swatch-icon/index.tsx b/src/components/fundamentals/icons/swatch-icon/index.tsx new file mode 100644 index 0000000000..ac89324556 --- /dev/null +++ b/src/components/fundamentals/icons/swatch-icon/index.tsx @@ -0,0 +1,29 @@ +import React from "react" +import IconProps from "../types/icon-type" + +const SwatchIcon: React.FC = ({ + size = "24px", + color = "currentColor", + ...attributes +}) => { + return ( + + + + ) +} + +export default SwatchIcon diff --git a/src/components/organisms/body-card.tsx b/src/components/organisms/body-card.tsx index fddb7843e2..51f925a52a 100644 --- a/src/components/organisms/body-card.tsx +++ b/src/components/organisms/body-card.tsx @@ -32,6 +32,8 @@ const BodyCard: React.FC = ({ className, children, compact = false, + setBorders = false, + footerMinHeight = 24, ...rest }) => { const { isScrolled, scrollListener } = useScroll({ threshold: 16 }) @@ -50,41 +52,55 @@ const BodyCard: React.FC = ({ )}
-
- {customHeader ? ( -
{customHeader}
- ) : title ? ( -

{title}

- ) : ( -
- )} +
+
+
+ {customHeader ? ( +
{customHeader}
+ ) : title ? ( +

{title}

+ ) : ( +
+ )} + + {subtitle && ( +

+ {subtitle} +

+ )} +
-
- {status && status} - +
+ {status && status} + +
- {subtitle && ( -

- {subtitle} -

- )} - {children && ( -
- {children} -
- )} + +
+ {children && ( +
+ {children} +
+ )} +
{events && events.length > 0 ? (
@@ -106,7 +122,7 @@ const BodyCard: React.FC = ({
) : ( -
+
)}
) diff --git a/src/components/organisms/sidebar/index.tsx b/src/components/organisms/sidebar/index.tsx index 4f6c5c2e6a..d0e6c34afc 100644 --- a/src/components/organisms/sidebar/index.tsx +++ b/src/components/organisms/sidebar/index.tsx @@ -8,6 +8,7 @@ import GearIcon from "../../fundamentals/icons/gear-icon" import GiftIcon from "../../fundamentals/icons/gift-icon" import SaleIcon from "../../fundamentals/icons/sale-icon" import TagIcon from "../../fundamentals/icons/tag-icon" +import SwatchIcon from "../../fundamentals/icons/swatch-icon" import UsersIcon from "../../fundamentals/icons/users-icon" import SidebarMenuItem from "../../molecules/sidebar-menu-item" import UserMenu from "../../molecules/user-menu" @@ -17,6 +18,7 @@ const ICON_SIZE = 20 const Sidebar: React.FC = () => { const [currentlyOpen, setCurrentlyOpen] = useState(-1) + const { isFeatureEnabled } = useFeatureFlag() const { store } = useAdminStore() const triggerHandler = () => { @@ -30,8 +32,6 @@ const Sidebar: React.FC = () => { // infinite updates, and we do not want the variable to be free floating. triggerHandler.id = 0 - const { isFeatureEnabled } = useFeatureFlag() - const inventoryEnabled = isFeatureEnabled("inventoryService") && isFeatureEnabled("stockLocationService") @@ -63,6 +63,14 @@ const Sidebar: React.FC = () => { text={"Products"} triggerHandler={triggerHandler} /> + {isFeatureEnabled("product_categories") && ( + } + text={"Product Categories"} + triggerHandler={triggerHandler} + /> + )} } diff --git a/src/domain/product-categories/index.tsx b/src/domain/product-categories/index.tsx new file mode 100644 index 0000000000..d10155f600 --- /dev/null +++ b/src/domain/product-categories/index.tsx @@ -0,0 +1,43 @@ +import BodyCard from "../../components/organisms/body-card" +import { Route, Routes } from "react-router-dom" + +const ProductCategoryIndex = () => { + const actions = [ + { + label: "Add category", + onClick: () => {}, + }, + ] + + return ( +
+
+ +
+

+ No product categories yet, use the above button to create your + first category. +

+
+
+
+
+ ) +} + +const ProductCategories = () => { + return ( + + } /> + + ) +} + +export default ProductCategories diff --git a/src/pages/a.jsx b/src/pages/a.jsx index 8ca9f383e3..32b74d77ce 100644 --- a/src/pages/a.jsx +++ b/src/pages/a.jsx @@ -17,6 +17,7 @@ import Orders from "../domain/orders" import DraftOrders from "../domain/orders/draft-orders" import Pricing from "../domain/pricing" import ProductsRoute from "../domain/products" +import ProductCategories from "../domain/product-categories" import PublishableApiKeys from "../domain/publishable-api-keys" import SalesChannels from "../domain/sales-channels" import Settings from "../domain/settings" @@ -42,6 +43,10 @@ const DashboardRoutes = () => { } /> } /> + } + /> } /> } /> } /> From c2a0b4eb0d761c455a67be984eeda8aa65a0680d Mon Sep 17 00:00:00 2001 From: Riqwan Thamir Date: Wed, 8 Feb 2023 12:33:59 +0100 Subject: [PATCH 02/28] chore: add empty page state for product categories --- .../fundamentals/icons/swatch-icon/index.tsx | 29 +++++++ src/components/organisms/body-card.tsx | 78 +++++++++++-------- src/components/organisms/sidebar/index.tsx | 11 +++ src/domain/product-categories/index.tsx | 43 ++++++++++ src/pages/a.jsx | 5 ++ 5 files changed, 135 insertions(+), 31 deletions(-) create mode 100644 src/components/fundamentals/icons/swatch-icon/index.tsx create mode 100644 src/domain/product-categories/index.tsx diff --git a/src/components/fundamentals/icons/swatch-icon/index.tsx b/src/components/fundamentals/icons/swatch-icon/index.tsx new file mode 100644 index 0000000000..ac89324556 --- /dev/null +++ b/src/components/fundamentals/icons/swatch-icon/index.tsx @@ -0,0 +1,29 @@ +import React from "react" +import IconProps from "../types/icon-type" + +const SwatchIcon: React.FC = ({ + size = "24px", + color = "currentColor", + ...attributes +}) => { + return ( + + + + ) +} + +export default SwatchIcon diff --git a/src/components/organisms/body-card.tsx b/src/components/organisms/body-card.tsx index fddb7843e2..51f925a52a 100644 --- a/src/components/organisms/body-card.tsx +++ b/src/components/organisms/body-card.tsx @@ -32,6 +32,8 @@ const BodyCard: React.FC = ({ className, children, compact = false, + setBorders = false, + footerMinHeight = 24, ...rest }) => { const { isScrolled, scrollListener } = useScroll({ threshold: 16 }) @@ -50,41 +52,55 @@ const BodyCard: React.FC = ({ )}
-
- {customHeader ? ( -
{customHeader}
- ) : title ? ( -

{title}

- ) : ( -
- )} +
+
+
+ {customHeader ? ( +
{customHeader}
+ ) : title ? ( +

{title}

+ ) : ( +
+ )} + + {subtitle && ( +

+ {subtitle} +

+ )} +
-
- {status && status} - +
+ {status && status} + +
- {subtitle && ( -

- {subtitle} -

- )} - {children && ( -
- {children} -
- )} + +
+ {children && ( +
+ {children} +
+ )} +
{events && events.length > 0 ? (
@@ -106,7 +122,7 @@ const BodyCard: React.FC = ({
) : ( -
+
)}
) diff --git a/src/components/organisms/sidebar/index.tsx b/src/components/organisms/sidebar/index.tsx index 4f6c5c2e6a..162d7ab89d 100644 --- a/src/components/organisms/sidebar/index.tsx +++ b/src/components/organisms/sidebar/index.tsx @@ -8,15 +8,18 @@ import GearIcon from "../../fundamentals/icons/gear-icon" import GiftIcon from "../../fundamentals/icons/gift-icon" import SaleIcon from "../../fundamentals/icons/sale-icon" import TagIcon from "../../fundamentals/icons/tag-icon" +import SwatchIcon from "../../fundamentals/icons/swatch-icon" import UsersIcon from "../../fundamentals/icons/users-icon" import SidebarMenuItem from "../../molecules/sidebar-menu-item" import UserMenu from "../../molecules/user-menu" +import { useFeatureFlag } from "../../../context/feature-flag" const ICON_SIZE = 20 const Sidebar: React.FC = () => { const [currentlyOpen, setCurrentlyOpen] = useState(-1) + const { isFeatureEnabled } = useFeatureFlag() const { store } = useAdminStore() const triggerHandler = () => { @@ -63,6 +66,14 @@ const Sidebar: React.FC = () => { text={"Products"} triggerHandler={triggerHandler} /> + {isFeatureEnabled("product_categories") && ( + } + text={"Product Categories"} + triggerHandler={triggerHandler} + /> + )} } diff --git a/src/domain/product-categories/index.tsx b/src/domain/product-categories/index.tsx new file mode 100644 index 0000000000..d10155f600 --- /dev/null +++ b/src/domain/product-categories/index.tsx @@ -0,0 +1,43 @@ +import BodyCard from "../../components/organisms/body-card" +import { Route, Routes } from "react-router-dom" + +const ProductCategoryIndex = () => { + const actions = [ + { + label: "Add category", + onClick: () => {}, + }, + ] + + return ( +
+
+ +
+

+ No product categories yet, use the above button to create your + first category. +

+
+
+
+
+ ) +} + +const ProductCategories = () => { + return ( + + } /> + + ) +} + +export default ProductCategories diff --git a/src/pages/a.jsx b/src/pages/a.jsx index 8ca9f383e3..3ba2ade071 100644 --- a/src/pages/a.jsx +++ b/src/pages/a.jsx @@ -18,6 +18,7 @@ import DraftOrders from "../domain/orders/draft-orders" import Pricing from "../domain/pricing" import ProductsRoute from "../domain/products" import PublishableApiKeys from "../domain/publishable-api-keys" +import ProductCategories from "../domain/product-categories" import SalesChannels from "../domain/sales-channels" import Settings from "../domain/settings" @@ -42,6 +43,10 @@ const DashboardRoutes = () => { } /> } /> + } + /> } /> } /> } /> From f76f16f161d3d36fe5b11eb93960b5694451c29f Mon Sep 17 00:00:00 2001 From: fPolic Date: Wed, 15 Feb 2023 16:29:00 +0100 Subject: [PATCH 03/28] feat: create category flow --- src/domain/product-categories/index.tsx | 32 +---- .../modals/add-product-category.tsx | 133 ++++++++++++++++++ src/domain/product-categories/pages/index.tsx | 45 ++++++ .../publishable-api-keys/pages/index.tsx | 1 - 4 files changed, 179 insertions(+), 32 deletions(-) create mode 100644 src/domain/product-categories/modals/add-product-category.tsx create mode 100644 src/domain/product-categories/pages/index.tsx diff --git a/src/domain/product-categories/index.tsx b/src/domain/product-categories/index.tsx index d10155f600..a9da57f318 100644 --- a/src/domain/product-categories/index.tsx +++ b/src/domain/product-categories/index.tsx @@ -1,36 +1,6 @@ -import BodyCard from "../../components/organisms/body-card" import { Route, Routes } from "react-router-dom" -const ProductCategoryIndex = () => { - const actions = [ - { - label: "Add category", - onClick: () => {}, - }, - ] - - return ( -
-
- -
-

- No product categories yet, use the above button to create your - first category. -

-
-
-
-
- ) -} +import ProductCategoryIndex from "./pages" const ProductCategories = () => { return ( diff --git a/src/domain/product-categories/modals/add-product-category.tsx b/src/domain/product-categories/modals/add-product-category.tsx new file mode 100644 index 0000000000..45ba055d11 --- /dev/null +++ b/src/domain/product-categories/modals/add-product-category.tsx @@ -0,0 +1,133 @@ +import React, { useState } from "react" + +import useNotification from "../../../hooks/use-notification" +import { useAdminCreateProductCategory } from "../../../../../medusa/packages/medusa-react" +import FocusModal from "../../../components/molecules/modal/focus-modal" +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" + +const visibilityOptions = [ + { + label: "Public", + value: "public", + }, + { label: "Private", value: "private" }, +] + +const statusOptions = [ + { label: "Active", value: "active" }, + { label: "Inactive", value: "inactive" }, +] + +type CreateProductCategoryProps = { + closeModal: () => void + parentCategory?: string +} + +/** + * Focus modal container for creating Publishable Keys. + */ +function CreateProductCategory(props: CreateProductCategoryProps) { + const { closeModal, parentCategory } = props + const notification = useNotification() + + const [name, setName] = useState("") + const [handle, setHandle] = useState("") + const [isActive, setIsActive] = useState(true) + const [isPublic, setIsPublic] = useState(true) + + const { mutateAsync: createProductCategory } = useAdminCreateProductCategory() + + const onSubmit = async () => { + try { + await createProductCategory({ + name, + is_active: isActive, + is_internal: !isPublic, + parent_category_id: parentCategory ?? null, + }) + closeModal() + notification("Success", "Created a new product category", "success") + } catch (e) { + notification("Error", "Failed to create a new API key", "error") + } + } + + return ( + + +
+ +
+ +
+
+
+ + +
+

+ Add category +

+

Details

+ +
+ setName(ev.target.value)} + /> + + setHandle(ev.target.value)} + /> +
+ +
+
+ setIsPublic(o.value === "public")} + /> +
+
+
+
+
+ ) +} + +export default CreateProductCategory diff --git a/src/domain/product-categories/pages/index.tsx b/src/domain/product-categories/pages/index.tsx new file mode 100644 index 0000000000..7bb5ea8439 --- /dev/null +++ b/src/domain/product-categories/pages/index.tsx @@ -0,0 +1,45 @@ +import BodyCard from "../../../components/organisms/body-card" +import useToggleState from "../../../hooks/use-toggle-state" +import CreateProductCategory from "../modals/add-product-category" + +function ProductCategory() { + const { + state: isCreateModalVisible, + open: showCreateModal, + close: hideCreateModal, + } = useToggleState() + + const actions = [ + { + label: "Add category", + onClick: showCreateModal, + }, + ] + + return ( +
+
+ +
+

+ No product categories yet, use the above button to create your + first category. +

+
+
+ {isCreateModalVisible && ( + + )} +
+
+ ) +} + +export default ProductCategory diff --git a/src/domain/publishable-api-keys/pages/index.tsx b/src/domain/publishable-api-keys/pages/index.tsx index 16908bc1db..45aa06fd24 100644 --- a/src/domain/publishable-api-keys/pages/index.tsx +++ b/src/domain/publishable-api-keys/pages/index.tsx @@ -4,7 +4,6 @@ import { PublishableApiKey, SalesChannel } from "@medusajs/medusa" import { useAdminAddPublishableKeySalesChannelsBatch, useAdminCreatePublishableApiKey, - useAdminPublishableApiKeySalesChannels, } from "medusa-react" import Breadcrumb from "../../../components/molecules/breadcrumb" From f299c716caf2b8debb35a423c26a35a3c489f638 Mon Sep 17 00:00:00 2001 From: fPolic Date: Thu, 16 Feb 2023 13:59:46 +0100 Subject: [PATCH 04/28] wip: draggable categories list + icons --- .../icons/folder-open-icon/index.tsx | 29 ++++++++ .../fundamentals/icons/reorder-icon/index.tsx | 64 ++++++++++++++++ .../icons/triangle-mini-icon/index.tsx | 28 +++++++ .../components/product-categories-list.tsx | 73 +++++++++++++++++++ src/domain/product-categories/pages/index.tsx | 44 ++++++++--- .../utils/use-raised-shadow.tsx | 28 +++++++ 6 files changed, 257 insertions(+), 9 deletions(-) create mode 100644 src/components/fundamentals/icons/folder-open-icon/index.tsx create mode 100644 src/components/fundamentals/icons/reorder-icon/index.tsx create mode 100644 src/components/fundamentals/icons/triangle-mini-icon/index.tsx create mode 100644 src/domain/product-categories/components/product-categories-list.tsx create mode 100644 src/domain/product-categories/utils/use-raised-shadow.tsx diff --git a/src/components/fundamentals/icons/folder-open-icon/index.tsx b/src/components/fundamentals/icons/folder-open-icon/index.tsx new file mode 100644 index 0000000000..74e8d97ad1 --- /dev/null +++ b/src/components/fundamentals/icons/folder-open-icon/index.tsx @@ -0,0 +1,29 @@ +import React from "react" +import IconProps from "../types/icon-type" + +const FolderOpenIcon: React.FC = ({ + size = "24px", + color = "currentColor", + ...attributes +}) => { + return ( + + + + ) +} + +export default FolderOpenIcon diff --git a/src/components/fundamentals/icons/reorder-icon/index.tsx b/src/components/fundamentals/icons/reorder-icon/index.tsx new file mode 100644 index 0000000000..b2eda246f4 --- /dev/null +++ b/src/components/fundamentals/icons/reorder-icon/index.tsx @@ -0,0 +1,64 @@ +import React from "react" +import IconProps from "../types/icon-type" + +const ReorderIcon: React.FC = ({ + size = "24px", + color = "currentColor", + ...attributes +}) => { + return ( + + + + + + + + + ) +} + +export default ReorderIcon diff --git a/src/components/fundamentals/icons/triangle-mini-icon/index.tsx b/src/components/fundamentals/icons/triangle-mini-icon/index.tsx new file mode 100644 index 0000000000..f0a4c50908 --- /dev/null +++ b/src/components/fundamentals/icons/triangle-mini-icon/index.tsx @@ -0,0 +1,28 @@ +import React from "react" +import IconProps from "../types/icon-type" + +const TriangleDownIcon: React.FC = ({ + size = "20", + color = "currentColor", + ...attributes +}) => { + return ( + + + + ) +} + +export default TriangleDownIcon diff --git a/src/domain/product-categories/components/product-categories-list.tsx b/src/domain/product-categories/components/product-categories-list.tsx new file mode 100644 index 0000000000..883634ed5a --- /dev/null +++ b/src/domain/product-categories/components/product-categories-list.tsx @@ -0,0 +1,73 @@ +import { ProductCategory } from "@medusajs/medusa" +import { Reorder, useDragControls, useMotionValue } from "framer-motion" +import { useRaisedShadow } from "../utils/use-raised-shadow" +import ReorderIcon from "../../../components/fundamentals/icons/reorder-icon" +import { useState } from "react" +import { useAdminProductCategories } from "../../../../../medusa/packages/medusa-react" + +type ProductCategoriesListItemProps = { + item: ProductCategory + depth: number +} + +function ProductCategoriesListItem(props: ProductCategoriesListItemProps) { + const { depth, item } = props + + const [showChildren, setShowChildren] = useState(false) + + const y = useMotionValue(0) + const boxShadow = useRaisedShadow(y) + const dragControls = useDragControls() + + const { product_categories: childCategories } = useAdminProductCategories( + { parent_category_id: item.id }, + { keepPreviousData: true, enabled: showChildren } + ) + + return ( + <> + +
+ dragControls.start(event)} /> + {item.name} +
+
+ {childCategories?.length && + childCategories.map((c) => ( + + ))} + + ) +} + +type ProductCategoriesListProps = { + categories: ProductCategory[] +} + +/** + * Draggable list that renders product categories tree view. + */ +function ProductCategoriesList(props: ProductCategoriesListProps) { + const [categories, setCategories] = useState(props.categories) + + return ( + + {categories.map((category) => ( + + ))} + + ) +} + +export default ProductCategoriesList diff --git a/src/domain/product-categories/pages/index.tsx b/src/domain/product-categories/pages/index.tsx index 7bb5ea8439..e37ebcbda9 100644 --- a/src/domain/product-categories/pages/index.tsx +++ b/src/domain/product-categories/pages/index.tsx @@ -1,14 +1,39 @@ -import BodyCard from "../../../components/organisms/body-card" import useToggleState from "../../../hooks/use-toggle-state" +import BodyCard from "../../../components/organisms/body-card" import CreateProductCategory from "../modals/add-product-category" +import { useAdminProductCategories } from "../../../../../medusa/packages/medusa-react" +import ProductCategoriesList from "../components/product-categories-list" -function ProductCategory() { +/** + * Product categories empty state placeholder. + */ +function ProductCategoriesEmptyState() { + return ( +
+

+ No product categories yet, use the above button to create your first + category. +

+
+ ) +} + +/** + * Product category index page container. + */ +function ProductCategoryPage() { const { state: isCreateModalVisible, open: showCreateModal, close: hideCreateModal, } = useToggleState() + const { product_categories: categories, isLoading } = + useAdminProductCategories({ + // TODO: doesn't work + // parent_category_id: null, + }) + const actions = [ { label: "Add category", @@ -16,6 +41,8 @@ function ProductCategory() { }, ] + const showList = !isLoading && categories?.length + return (
@@ -27,12 +54,11 @@ function ProductCategory() { footerMinHeight={40} setBorders > -
-

- No product categories yet, use the above button to create your - first category. -

-
+ {showList ? ( + + ) : ( + + )} {isCreateModalVisible && ( @@ -42,4 +68,4 @@ function ProductCategory() { ) } -export default ProductCategory +export default ProductCategoryPage diff --git a/src/domain/product-categories/utils/use-raised-shadow.tsx b/src/domain/product-categories/utils/use-raised-shadow.tsx new file mode 100644 index 0000000000..59c3a9572d --- /dev/null +++ b/src/domain/product-categories/utils/use-raised-shadow.tsx @@ -0,0 +1,28 @@ +import { animate, MotionValue, useMotionValue } from "framer-motion" +import { useEffect } from "react" + +const inactiveShadow = "0px 0px 0px rgba(0,0,0,0.8)" + +export function useRaisedShadow(value: MotionValue) { + const boxShadow = useMotionValue(inactiveShadow) + + useEffect(() => { + let isActive = false + value.onChange((latest) => { + const wasActive = isActive + if (latest !== 0) { + isActive = true + if (isActive !== wasActive) { + animate(boxShadow, "0px 1px 4px rgba(0,0,0,0.3)") + } + } else { + isActive = false + if (isActive !== wasActive) { + animate(boxShadow, inactiveShadow) + } + } + }) + }, [value, boxShadow]) + + return boxShadow +} From 111d3c2162795953c1b7181da5e4860628e91129 Mon Sep 17 00:00:00 2001 From: fPolic Date: Thu, 16 Feb 2023 19:16:16 +0100 Subject: [PATCH 05/28] wip: category list item, edit category modal wip, logic for rendering draggable, nested list --- .../fundamentals/icons/reorder-icon/index.tsx | 2 +- .../components/product-categories-list.tsx | 55 ++++++++---- .../product-category-list-item-details.tsx | 81 +++++++++++++++++ .../modals/add-product-category.tsx | 7 +- .../modals/edit-product-category.tsx | 78 +++++++++++++++++ src/domain/product-categories/pages/index.tsx | 87 ++++++++++++++----- 6 files changed, 271 insertions(+), 39 deletions(-) create mode 100644 src/domain/product-categories/components/product-category-list-item-details.tsx create mode 100644 src/domain/product-categories/modals/edit-product-category.tsx diff --git a/src/components/fundamentals/icons/reorder-icon/index.tsx b/src/components/fundamentals/icons/reorder-icon/index.tsx index b2eda246f4..6c28116825 100644 --- a/src/components/fundamentals/icons/reorder-icon/index.tsx +++ b/src/components/fundamentals/icons/reorder-icon/index.tsx @@ -10,7 +10,7 @@ const ReorderIcon: React.FC = ({
dragControls.start(event)} /> - {item.name} +
- {childCategories?.length && - childCategories.map((c) => ( - + {!!item.category_children?.length && + item.category_children.map((c) => ( + ))} ) @@ -56,10 +53,38 @@ type ProductCategoriesListProps = { */ function ProductCategoriesList(props: ProductCategoriesListProps) { const [categories, setCategories] = useState(props.categories) + const { flatCategoriesList, rootCategories } = useMemo(() => { + const res = [] + const allCategories = {} + + categories.forEach((c) => (allCategories[c.id] = c)) + + const topLevelCategories = categories.filter((c) => !c.parent_category_id) + + const go = (active) => { + const node = allCategories[active.id] + if (!node) { + return + } + node.category_children?.forEach((ch) => + Object.assign(ch, allCategories[ch.id]) + ) + res.push(node) + node.category_children?.forEach(go) + } + + topLevelCategories.forEach(go) + + return { flatCategoriesList: res, rootCategories: topLevelCategories } + }, [categories]) return ( - - {categories.map((category) => ( + + {rootCategories.map((category) => ( productCategoriesPageContext.editCategory(category), + icon: , + }, + { + label: "Delete", + variant: "danger", + onClick: deleteCategory, + icon: , + }, + ] + + return ( +
+
+ {category.name} +
+ +
+ + Add category item to{" "} + + "{category.name}" + + + } + > + + + +
+
+ ) +} + +export default ProductCategoryListItemDetails diff --git a/src/domain/product-categories/modals/add-product-category.tsx b/src/domain/product-categories/modals/add-product-category.tsx index 45ba055d11..b731e29e0d 100644 --- a/src/domain/product-categories/modals/add-product-category.tsx +++ b/src/domain/product-categories/modals/add-product-category.tsx @@ -7,6 +7,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 { ProductCategory } from "@medusajs/medusa" const visibilityOptions = [ { @@ -23,7 +24,7 @@ const statusOptions = [ type CreateProductCategoryProps = { closeModal: () => void - parentCategory?: string + parentCategory?: ProductCategory } /** @@ -46,7 +47,7 @@ function CreateProductCategory(props: CreateProductCategoryProps) { name, is_active: isActive, is_internal: !isPublic, - parent_category_id: parentCategory ?? null, + parent_category_id: parentCategory?.id ?? null, }) closeModal() notification("Success", "Created a new product category", "success") @@ -79,7 +80,7 @@ function CreateProductCategory(props: CreateProductCategoryProps) {

- Add category + Add category {parentCategory && `to ${parentCategory.name}`}

Details

diff --git a/src/domain/product-categories/modals/edit-product-category.tsx b/src/domain/product-categories/modals/edit-product-category.tsx new file mode 100644 index 0000000000..7ce1e8d0f0 --- /dev/null +++ b/src/domain/product-categories/modals/edit-product-category.tsx @@ -0,0 +1,78 @@ +import React, { useRef, useState } from "react" +import { useAdminSalesChannels } from "medusa-react" +import { SalesChannel } from "@medusajs/medusa" + +import SideModal from "../../../components/molecules/modal/side-modal" +import Button from "../../../components/fundamentals/button" +import CrossIcon from "../../../components/fundamentals/icons/cross-icon" + +type EditProductCategoriesSideModalProps = { + close: () => void + isVisible: boolean +} + +/** + * Modal for editing product categories + */ +function EditProductCategoriesSideModal( + props: EditProductCategoriesSideModalProps +) { + const { isVisible, close } = props + + const onSave = () => { + close() + } + + const onClose = () => { + close() + } + + return ( + +
+ {/* === HEADER === */} + +
+

+ Edit product category +

+ +
+ {/* === DIVIDER === */} + +
+
+
+ {/* === DIVIDER === */} + +
+ {/* === FOOTER === */} + +
+ + +
+
+ + ) +} + +export default EditProductCategoriesSideModal diff --git a/src/domain/product-categories/pages/index.tsx b/src/domain/product-categories/pages/index.tsx index e37ebcbda9..e8f46140ac 100644 --- a/src/domain/product-categories/pages/index.tsx +++ b/src/domain/product-categories/pages/index.tsx @@ -1,8 +1,13 @@ +import { createContext, useState } from "react" + +import { ProductCategory } from "@medusajs/medusa" + import useToggleState from "../../../hooks/use-toggle-state" import BodyCard from "../../../components/organisms/body-card" import CreateProductCategory from "../modals/add-product-category" -import { useAdminProductCategories } from "../../../../../medusa/packages/medusa-react" import ProductCategoriesList from "../components/product-categories-list" +import { useAdminProductCategories } from "../../../../../medusa/packages/medusa-react" +import EditProductCategoriesSideModal from "../modals/edit-product-category" /** * Product categories empty state placeholder. @@ -18,6 +23,11 @@ function ProductCategoriesEmptyState() { ) } +export const ProductCategoriesContext = createContext<{ + editCategory: (category: ProductCategory) => void + createSubCategory: (category: ProductCategory) => void +}>({} as any) + /** * Product category index page container. */ @@ -28,8 +38,17 @@ function ProductCategoryPage() { close: hideCreateModal, } = useToggleState() + const { + state: isEditModalVisible, + open: showEditModal, + close: hideEditModal, + } = useToggleState() + + const [activeCategory, setActiveCategory] = useState() + const { product_categories: categories, isLoading } = useAdminProductCategories({ + expand: "category_children", // TODO: doesn't work // parent_category_id: null, }) @@ -43,28 +62,56 @@ function ProductCategoryPage() { const showList = !isLoading && categories?.length + const editCategory = (category: ProductCategory) => { + setActiveCategory(category) + showEditModal() + } + + const createSubCategory = (category: ProductCategory) => { + setActiveCategory(category) + showCreateModal() + } + + const context = { + editCategory, + createSubCategory, + } + return ( -
-
- - {showList ? ( - - ) : ( - + +
+
+ + {showList ? ( + + ) : ( + + )} + + {isCreateModalVisible && ( + { + hideCreateModal() + setActiveCategory(undefined) + }} + /> )} - - {isCreateModalVisible && ( - - )} + + +
-
+ ) } From f35bde6c899ebbc60ae1e019d887b981ddd1ce39 Mon Sep 17 00:00:00 2001 From: fPolic Date: Sat, 18 Feb 2023 16:22:08 +0100 Subject: [PATCH 06/28] wip: draggable list & collapsable items --- .../icons/folder-open-icon/index.tsx | 4 +- .../components/product-categories-list.tsx | 114 ++++++++++++------ .../product-category-list-item-details.tsx | 18 ++- 3 files changed, 93 insertions(+), 43 deletions(-) diff --git a/src/components/fundamentals/icons/folder-open-icon/index.tsx b/src/components/fundamentals/icons/folder-open-icon/index.tsx index 74e8d97ad1..59703296d1 100644 --- a/src/components/fundamentals/icons/folder-open-icon/index.tsx +++ b/src/components/fundamentals/icons/folder-open-icon/index.tsx @@ -2,7 +2,7 @@ import React from "react" import IconProps from "../types/icon-type" const FolderOpenIcon: React.FC = ({ - size = "24px", + size = "20px", color = "currentColor", ...attributes }) => { @@ -10,7 +10,7 @@ const FolderOpenIcon: React.FC = ({ void +} - const [showChildren, setShowChildren] = useState(true) +function ProductCategoriesListItem(props: ProductCategoriesListItemProps) { + const { item, isOpen, toggleCategory } = props const y = useMotionValue(0) const boxShadow = useRaisedShadow(y) @@ -24,8 +28,8 @@ function ProductCategoriesListItem(props: ProductCategoriesListItemProps) { return ( <>
dragControls.start(event)} /> - +
- {!!item.category_children?.length && - item.category_children.map((c) => ( - - ))} ) } @@ -52,43 +56,75 @@ type ProductCategoriesListProps = { * Draggable list that renders product categories tree view. */ function ProductCategoriesList(props: ProductCategoriesListProps) { - const [categories, setCategories] = useState(props.categories) - const { flatCategoriesList, rootCategories } = useMemo(() => { - const res = [] - const allCategories = {} + const flatCategoriesList = useMemo(() => { + const categoriesMap = {} + const flatCategoriesList: DraggableListItem[] = [] - categories.forEach((c) => (allCategories[c.id] = c)) + props.categories.forEach((c) => (categoriesMap[c.id] = c)) - const topLevelCategories = categories.filter((c) => !c.parent_category_id) + const visit = (active, depth) => { + const node = categoriesMap[active.id] - const go = (active) => { - const node = allCategories[active.id] - if (!node) { - return - } node.category_children?.forEach((ch) => - Object.assign(ch, allCategories[ch.id]) + Object.assign(ch, categoriesMap[ch.id]) ) - res.push(node) - node.category_children?.forEach(go) - } - topLevelCategories.forEach(go) + flatCategoriesList.push({ depth, category: node }) + + node.category_children?.forEach((c) => visit(c, depth + 1)) + } - return { flatCategoriesList: res, rootCategories: topLevelCategories } - }, [categories]) + props.categories + .filter((c) => !c.parent_category_id) + .forEach((c) => visit(c, 0)) + + return flatCategoriesList + }, [props.categories]) + + const [openCategories, setOpenCategories] = useState({}) + + const [items, _setItems] = useState(flatCategoriesList) + + useEffect(() => { + setItems((ii) => + flatCategoriesList + .sort( + (a, b) => + ii.map((i) => i.category.id).indexOf(b) - + ii.map((i) => i.category.id).indexOf(a) + ) + .filter( + (c) => + c.category.parent_category_id in openCategories || + !c.category.parent_category_id + ) + ) + }, [flatCategoriesList, openCategories]) + + const setItems = (newItems) => { + // flatCategoriesList.sort((a, b) => newItems.indexOf(a) - newItems.indexOf(b)) + _setItems(newItems) + // TODO: set new order of `ìtems` to `flatCategoriesList` + } + + const toggleCategory = (categoryId: string) => { + const temp = { ...openCategories } + if (temp[categoryId]) { + delete temp[categoryId] + } else { + temp[categoryId] = true + } + setOpenCategories(temp) + } return ( - - {rootCategories.map((category) => ( + + {items.map((item) => ( toggleCategory(item.category.id)} /> ))} diff --git a/src/domain/product-categories/components/product-category-list-item-details.tsx b/src/domain/product-categories/components/product-category-list-item-details.tsx index 858f34c990..24f9bedbc6 100644 --- a/src/domain/product-categories/components/product-category-list-item-details.tsx +++ b/src/domain/product-categories/components/product-category-list-item-details.tsx @@ -10,16 +10,22 @@ 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 { useAdminDeleteProductCategory } from "../../../../../medusa/packages/medusa-react" +import FolderOpenIcon from "../../../components/fundamentals/icons/folder-open-icon" +import TriangleMiniIcon from "../../../components/fundamentals/icons/triangle-mini-icon" type ProductCategoryListItemDetailsProps = { depth: number + isOpen: boolean category: ProductCategory + toggleCategory: () => void } function ProductCategoryListItemDetails( props: ProductCategoryListItemDetailsProps ) { - const { category, depth } = props + const { category, depth, isOpen, toggleCategory } = props + + const hasChildren = !!category.category_children?.length const productCategoriesPageContext = useContext(ProductCategoriesContext) @@ -38,6 +44,7 @@ function ProductCategoryListItemDetails( variant: "danger", onClick: deleteCategory, icon: , + disabled: !!category.category_children?.length, }, ] @@ -46,7 +53,14 @@ function ProductCategoryListItemDetails( style={{ paddingLeft: depth * 32 }} className="flex justify-between items-center w-full" > -
+
+ {hasChildren && ( + + )} + {hasChildren && } {category.name}
From fce407826121e480631730135f227f3f162d848e Mon Sep 17 00:00:00 2001 From: fPolic Date: Sun, 19 Feb 2023 12:06:33 +0100 Subject: [PATCH 07/28] wip: edit categories UI --- .../modals/add-product-category.tsx | 2 +- .../modals/edit-product-category.tsx | 98 +++++++++++++++++-- src/domain/product-categories/pages/index.tsx | 3 +- 3 files changed, 95 insertions(+), 8 deletions(-) diff --git a/src/domain/product-categories/modals/add-product-category.tsx b/src/domain/product-categories/modals/add-product-category.tsx index b731e29e0d..70c8def814 100644 --- a/src/domain/product-categories/modals/add-product-category.tsx +++ b/src/domain/product-categories/modals/add-product-category.tsx @@ -52,7 +52,7 @@ function CreateProductCategory(props: CreateProductCategoryProps) { closeModal() notification("Success", "Created a new product category", "success") } catch (e) { - notification("Error", "Failed to create a new API key", "error") + notification("Error", "Failed to create a new product category", "error") } } diff --git a/src/domain/product-categories/modals/edit-product-category.tsx b/src/domain/product-categories/modals/edit-product-category.tsx index 7ce1e8d0f0..ce1f62adcd 100644 --- a/src/domain/product-categories/modals/edit-product-category.tsx +++ b/src/domain/product-categories/modals/edit-product-category.tsx @@ -1,12 +1,30 @@ -import React, { useRef, useState } from "react" -import { useAdminSalesChannels } from "medusa-react" -import { SalesChannel } from "@medusajs/medusa" +import React, { useEffect, useState } from "react" + +import { ProductCategory } from "@medusajs/medusa" import SideModal from "../../../components/molecules/modal/side-modal" import Button from "../../../components/fundamentals/button" import CrossIcon from "../../../components/fundamentals/icons/cross-icon" +import { useAdminUpdateProductCategory } from "../../../../../medusa/packages/medusa-react" +import InputField from "../../../components/molecules/input" +import Select from "../../../components/molecules/select" +import useNotification from "../../../hooks/use-notification" + +const visibilityOptions = [ + { + label: "Public", + value: "public", + }, + { label: "Private", value: "private" }, +] + +const statusOptions = [ + { label: "Active", value: "active" }, + { label: "Inactive", value: "inactive" }, +] type EditProductCategoriesSideModalProps = { + activeCategory: ProductCategory close: () => void isVisible: boolean } @@ -17,9 +35,43 @@ type EditProductCategoriesSideModalProps = { function EditProductCategoriesSideModal( props: EditProductCategoriesSideModalProps ) { - const { isVisible, close } = props + const { isVisible, close, activeCategory } = props + + const [name, setName] = useState("") + const [handle, setHandle] = useState("") + const [isActive, setIsActive] = useState(true) + const [isPublic, setIsPublic] = useState(true) + + const notification = useNotification() + + const { mutateAsync: updateCategory } = useAdminUpdateProductCategory( + activeCategory?.id + ) - const onSave = () => { + useEffect(() => { + if (activeCategory) { + setName(activeCategory.name) + setHandle(activeCategory.handle) + setIsActive(activeCategory.is_active) + setIsPublic(!activeCategory.is_internal) + } + }, [activeCategory]) + + const onSave = async () => { + try { + await updateCategory({ + name, + handle, + is_active: isActive, + is_internal: !isPublic, + }) + + // TODO: check on the BD, when we send update partial children of the category are lost + + notification("Success", "Product category updated", "success") + } catch (e) { + notification("Error", "Failed to update the category", "error") + } close() } @@ -47,7 +99,41 @@ function EditProductCategoriesSideModal( {/* === DIVIDER === */}
-
+
+ setName(ev.target.value)} + /> + + setHandle(ev.target.value)} + /> + + setIsPublic(o.value === "public")} + />
{/* === DIVIDER === */} diff --git a/src/domain/product-categories/pages/index.tsx b/src/domain/product-categories/pages/index.tsx index e8f46140ac..2aa2aea443 100644 --- a/src/domain/product-categories/pages/index.tsx +++ b/src/domain/product-categories/pages/index.tsx @@ -106,8 +106,9 @@ function ProductCategoryPage() { )}
From 5972a5e1821f9a4d4c7f2fd222fdc9f58bbc0805 Mon Sep 17 00:00:00 2001 From: fPolic Date: Sun, 19 Feb 2023 12:07:01 +0100 Subject: [PATCH 08/28] fix: fields width --- src/domain/product-categories/modals/edit-product-category.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/domain/product-categories/modals/edit-product-category.tsx b/src/domain/product-categories/modals/edit-product-category.tsx index ce1f62adcd..5268c0fae3 100644 --- a/src/domain/product-categories/modals/edit-product-category.tsx +++ b/src/domain/product-categories/modals/edit-product-category.tsx @@ -105,7 +105,6 @@ function EditProductCategoriesSideModal( type="string" name="name" value={name} - className="w-[338px]" placeholder="Give this category a name" onChange={(ev) => setName(ev.target.value)} /> @@ -115,7 +114,6 @@ function EditProductCategoriesSideModal( type="string" name="handle" value={handle} - className="w-[338px]" placeholder="Custom handle" onChange={(ev) => setHandle(ev.target.value)} /> From 79092cf991e1b60652cd6f52b9b05a8a2a9a40d9 Mon Sep 17 00:00:00 2001 From: fPolic Date: Mon, 20 Feb 2023 10:56:20 +0100 Subject: [PATCH 09/28] feat: toggle category on reorder start, hide all descendants on toggle --- .../components/product-categories-list.tsx | 48 +++++++++++++------ 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/src/domain/product-categories/components/product-categories-list.tsx b/src/domain/product-categories/components/product-categories-list.tsx index d8b30ab417..b955ca391f 100644 --- a/src/domain/product-categories/components/product-categories-list.tsx +++ b/src/domain/product-categories/components/product-categories-list.tsx @@ -25,14 +25,24 @@ function ProductCategoriesListItem(props: ProductCategoriesListItemProps) { const boxShadow = useRaisedShadow(y) const dragControls = useDragControls() + /** + * When reorder starts close dragged category. + */ + const onDragStart = () => { + if (isOpen) { + toggleCategory() + } + } + return ( <>
@@ -86,31 +96,39 @@ function ProductCategoriesList(props: ProductCategoriesListProps) { const [items, _setItems] = useState(flatCategoriesList) useEffect(() => { - setItems((ii) => - flatCategoriesList - .sort( - (a, b) => - ii.map((i) => i.category.id).indexOf(b) - - ii.map((i) => i.category.id).indexOf(a) - ) - .filter( - (c) => - c.category.parent_category_id in openCategories || - !c.category.parent_category_id - ) + setItems( + flatCategoriesList.filter( + (c) => + c.category.parent_category_id in openCategories || + !c.category.parent_category_id + ) ) - }, [flatCategoriesList, openCategories]) + }, [openCategories]) const setItems = (newItems) => { // flatCategoriesList.sort((a, b) => newItems.indexOf(a) - newItems.indexOf(b)) _setItems(newItems) - // TODO: set new order of `ìtems` to `flatCategoriesList` + // TODO: set new order of `items` to `flatCategoriesList` } + /** + * Toggle display of subcategories in the list + */ const toggleCategory = (categoryId: string) => { const temp = { ...openCategories } if (temp[categoryId]) { delete temp[categoryId] + + const visit = (node) => { + delete temp[node.id] + + node.category_children?.forEach(visit) + } + const rootNode = flatCategoriesList.find( + (c) => c.category.id === categoryId + )!.category + + visit(rootNode) } else { temp[categoryId] = true } From 85a8e2e6bfd5800a15c7b7aab468ff8c57a90a83 Mon Sep 17 00:00:00 2001 From: fPolic Date: Tue, 21 Feb 2023 11:05:19 +0100 Subject: [PATCH 10/28] feat: polish line item details --- .../components/product-categories-list.tsx | 5 +- .../product-category-list-item-details.tsx | 49 +++++++++++++++---- 2 files changed, 43 insertions(+), 11 deletions(-) diff --git a/src/domain/product-categories/components/product-categories-list.tsx b/src/domain/product-categories/components/product-categories-list.tsx index b955ca391f..deda5d73b5 100644 --- a/src/domain/product-categories/components/product-categories-list.tsx +++ b/src/domain/product-categories/components/product-categories-list.tsx @@ -46,7 +46,10 @@ function ProductCategoriesListItem(props: ProductCategoriesListItemProps) { className="relative bg-white" >
- dragControls.start(event)} /> + dragControls.start(event)} + /> -
+
{hasChildren && ( - +
+ +
)} - {hasChildren && } - {category.name} +
+ {hasChildren && } + {!hasChildren && } +
+ + {category.name} +
@@ -83,10 +100,22 @@ function ProductCategoryListItemDetails( productCategoriesPageContext.createSubCategory(category) } > - + - + + + + } + />
) From 12f8e72b38a41d691321ea5812fb4bd8b8640429 Mon Sep 17 00:00:00 2001 From: fPolic Date: Wed, 22 Feb 2023 11:56:40 +0100 Subject: [PATCH 11/28] wip: setup react nestable and render list --- package.json | 1 + .../components/product-categories-list.tsx | 116 ++++-------------- .../product-category-list-item-details.tsx | 23 ++-- .../styles/product-categories.css | 21 ++++ .../utils/use-raised-shadow.tsx | 28 ----- 5 files changed, 53 insertions(+), 136 deletions(-) create mode 100644 src/domain/product-categories/styles/product-categories.css delete mode 100644 src/domain/product-categories/utils/use-raised-shadow.tsx diff --git a/package.json b/package.json index 2bea3683c6..78de3d6135 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "react-hotkeys-hook": "^3.4.7", "react-json-tree": "^0.17.0", "react-jwt": "^1.1.4", + "react-nestable": "^2.0.0", "react-router-dom": "^6.4.2", "react-select": "^5.5.4", "react-table": "^7.7.0", diff --git a/src/domain/product-categories/components/product-categories-list.tsx b/src/domain/product-categories/components/product-categories-list.tsx index deda5d73b5..2671078856 100644 --- a/src/domain/product-categories/components/product-categories-list.tsx +++ b/src/domain/product-categories/components/product-categories-list.tsx @@ -1,15 +1,18 @@ -import { useEffect, useMemo, useState } from "react" -import { Reorder, useDragControls, useMotionValue } from "framer-motion" +import { useMemo } from "react" +import Nestable from "react-nestable" + +import "react-nestable/dist/styles/index.css" +import "../styles/product-categories.css" import { ProductCategory } from "@medusajs/medusa" -import { useRaisedShadow } from "../utils/use-raised-shadow" import ReorderIcon from "../../../components/fundamentals/icons/reorder-icon" import ProductCategoryListItemDetails from "./product-category-list-item-details" -type DraggableListItem = { - depth: number +export type DraggableListItem = { + id: string category: ProductCategory + children: DraggableListItem[] } type ProductCategoriesListItemProps = { @@ -21,42 +24,18 @@ type ProductCategoriesListItemProps = { function ProductCategoriesListItem(props: ProductCategoriesListItemProps) { const { item, isOpen, toggleCategory } = props - const y = useMotionValue(0) - const boxShadow = useRaisedShadow(y) - const dragControls = useDragControls() - - /** - * When reorder starts close dragged category. - */ - const onDragStart = () => { - if (isOpen) { - toggleCategory() - } - } - return ( <> - +
- dragControls.start(event)} - /> +
- +
) } @@ -71,84 +50,31 @@ type ProductCategoriesListProps = { function ProductCategoriesList(props: ProductCategoriesListProps) { const flatCategoriesList = useMemo(() => { const categoriesMap = {} - const flatCategoriesList: DraggableListItem[] = [] props.categories.forEach((c) => (categoriesMap[c.id] = c)) - const visit = (active, depth) => { + const visit = (active) => { const node = categoriesMap[active.id] node.category_children?.forEach((ch) => Object.assign(ch, categoriesMap[ch.id]) ) - flatCategoriesList.push({ depth, category: node }) + const children = node.category_children?.map((c) => visit(c)) - node.category_children?.forEach((c) => visit(c, depth + 1)) + return { id: node.id, category: node, children } } - props.categories + return props.categories .filter((c) => !c.parent_category_id) - .forEach((c) => visit(c, 0)) - - return flatCategoriesList + .map((c) => visit(c)) }, [props.categories]) - const [openCategories, setOpenCategories] = useState({}) - - const [items, _setItems] = useState(flatCategoriesList) - - useEffect(() => { - setItems( - flatCategoriesList.filter( - (c) => - c.category.parent_category_id in openCategories || - !c.category.parent_category_id - ) - ) - }, [openCategories]) - - const setItems = (newItems) => { - // flatCategoriesList.sort((a, b) => newItems.indexOf(a) - newItems.indexOf(b)) - _setItems(newItems) - // TODO: set new order of `items` to `flatCategoriesList` - } - - /** - * Toggle display of subcategories in the list - */ - const toggleCategory = (categoryId: string) => { - const temp = { ...openCategories } - if (temp[categoryId]) { - delete temp[categoryId] - - const visit = (node) => { - delete temp[node.id] - - node.category_children?.forEach(visit) - } - const rootNode = flatCategoriesList.find( - (c) => c.category.id === categoryId - )!.category - - visit(rootNode) - } else { - temp[categoryId] = true - } - setOpenCategories(temp) - } - return ( - - {items.map((item) => ( - toggleCategory(item.category.id)} - /> - ))} - + ) } diff --git a/src/domain/product-categories/components/product-category-list-item-details.tsx b/src/domain/product-categories/components/product-category-list-item-details.tsx index 1be969cfb6..7967638246 100644 --- a/src/domain/product-categories/components/product-category-list-item-details.tsx +++ b/src/domain/product-categories/components/product-category-list-item-details.tsx @@ -1,8 +1,6 @@ import React, { useContext } from "react" import clsx from "clsx" -import { ProductCategory } from "@medusajs/medusa" - import { ProductCategoriesContext } from "../pages" import Tooltip from "../../../components/atoms/tooltip" import Button from "../../../components/fundamentals/button" @@ -15,33 +13,32 @@ import FolderOpenIcon from "../../../components/fundamentals/icons/folder-open-i import TriangleMiniIcon from "../../../components/fundamentals/icons/triangle-mini-icon" import TagIcon from "../../../components/fundamentals/icons/tag-icon" import MoreHorizontalIcon from "../../../components/fundamentals/icons/more-horizontal-icon" +import { DraggableListItem } from "./product-categories-list" type ProductCategoryListItemDetailsProps = { depth: number isOpen: boolean - category: ProductCategory + item: DraggableListItem toggleCategory: () => void } function ProductCategoryListItemDetails( props: ProductCategoryListItemDetailsProps ) { - const { category, depth, isOpen, toggleCategory } = props + const { item, depth, isOpen, toggleCategory } = props - const hasChildren = !!category.category_children?.length + const hasChildren = !!item.children?.length const productCategoriesPageContext = useContext(ProductCategoriesContext) - const { mutateAsync: deleteCategory } = useAdminDeleteProductCategory( - category.id - ) + const { mutateAsync: deleteCategory } = useAdminDeleteProductCategory(item.id) const paddingLeft = hasChildren ? depth * 32 : (depth - 1) * 32 + 64 const actions = [ { label: "Edit", - onClick: () => productCategoriesPageContext.editCategory(category), + onClick: () => productCategoriesPageContext.editCategory(item), icon: , }, { @@ -49,7 +46,7 @@ function ProductCategoryListItemDetails( variant: "danger", onClick: deleteCategory, icon: , - disabled: !!category.category_children?.length, + disabled: !!item.category?.length, }, ] @@ -77,7 +74,7 @@ function ProductCategoryListItemDetails( "text-gray-400 font-normal": !hasChildren, })} > - {category.name} + {item.category.name}
@@ -88,7 +85,7 @@ function ProductCategoryListItemDetails( <> Add category item to{" "} - "{category.name}" + "{item.category.name}" } @@ -97,7 +94,7 @@ function ProductCategoryListItemDetails( size="small" variant="ghost" onClick={() => - productCategoriesPageContext.createSubCategory(category) + productCategoriesPageContext.createSubCategory(item.category) } > diff --git a/src/domain/product-categories/styles/product-categories.css b/src/domain/product-categories/styles/product-categories.css new file mode 100644 index 0000000000..f5c091743b --- /dev/null +++ b/src/domain/product-categories/styles/product-categories.css @@ -0,0 +1,21 @@ + + +.nestable-item.is-dragging:before { + content: ' '; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: white; + + transition: .3s all; + + /*border: none;*/ + /*border-radius: 0;*/ + /*border-bottom: 1px dashed #4682b4;*/ +} + +.nestable-item.is-dragging * { + /*height: 20px;*/ +} diff --git a/src/domain/product-categories/utils/use-raised-shadow.tsx b/src/domain/product-categories/utils/use-raised-shadow.tsx deleted file mode 100644 index 59c3a9572d..0000000000 --- a/src/domain/product-categories/utils/use-raised-shadow.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { animate, MotionValue, useMotionValue } from "framer-motion" -import { useEffect } from "react" - -const inactiveShadow = "0px 0px 0px rgba(0,0,0,0.8)" - -export function useRaisedShadow(value: MotionValue) { - const boxShadow = useMotionValue(inactiveShadow) - - useEffect(() => { - let isActive = false - value.onChange((latest) => { - const wasActive = isActive - if (latest !== 0) { - isActive = true - if (isActive !== wasActive) { - animate(boxShadow, "0px 1px 4px rgba(0,0,0,0.3)") - } - } else { - isActive = false - if (isActive !== wasActive) { - animate(boxShadow, inactiveShadow) - } - } - }) - }, [value, boxShadow]) - - return boxShadow -} From c6463fccdd68e6722618b84c84af20ed2cc7ab26 Mon Sep 17 00:00:00 2001 From: fPolic Date: Wed, 22 Feb 2023 14:42:14 +0100 Subject: [PATCH 12/28] feat: restructure rendering, cleanup components, connect target elements --- .../components/product-categories-list.tsx | 72 +++++----- .../product-category-list-item-details.tsx | 136 +++++++++--------- 2 files changed, 98 insertions(+), 110 deletions(-) diff --git a/src/domain/product-categories/components/product-categories-list.tsx b/src/domain/product-categories/components/product-categories-list.tsx index 2671078856..b5c9fcb163 100644 --- a/src/domain/product-categories/components/product-categories-list.tsx +++ b/src/domain/product-categories/components/product-categories-list.tsx @@ -1,4 +1,4 @@ -import { useMemo } from "react" +import React, { useMemo } from "react" import Nestable from "react-nestable" import "react-nestable/dist/styles/index.css" @@ -6,39 +6,9 @@ import "../styles/product-categories.css" import { ProductCategory } from "@medusajs/medusa" -import ReorderIcon from "../../../components/fundamentals/icons/reorder-icon" +import TriangleMiniIcon from "../../../components/fundamentals/icons/triangle-mini-icon" import ProductCategoryListItemDetails from "./product-category-list-item-details" - -export type DraggableListItem = { - id: string - category: ProductCategory - children: DraggableListItem[] -} - -type ProductCategoriesListItemProps = { - isOpen: boolean - item: DraggableListItem - toggleCategory: () => void -} - -function ProductCategoriesListItem(props: ProductCategoriesListItemProps) { - const { item, isOpen, toggleCategory } = props - - return ( - <> -
-
- - -
-
- - ) -} +import ReorderIcon from "../../../components/fundamentals/icons/reorder-icon" type ProductCategoriesListProps = { categories: ProductCategory[] @@ -48,9 +18,11 @@ type ProductCategoriesListProps = { * Draggable list that renders product categories tree view. */ function ProductCategoriesList(props: ProductCategoriesListProps) { - const flatCategoriesList = useMemo(() => { + const categories = useMemo(() => { + /** + * HACK - for now to properly reference nested children + */ const categoriesMap = {} - props.categories.forEach((c) => (categoriesMap[c.id] = c)) const visit = (active) => { @@ -60,9 +32,7 @@ function ProductCategoriesList(props: ProductCategoriesListProps) { Object.assign(ch, categoriesMap[ch.id]) ) - const children = node.category_children?.map((c) => visit(c)) - - return { id: node.id, category: node, children } + return node } return props.categories @@ -72,8 +42,30 @@ function ProductCategoriesList(props: ProductCategoriesListProps) { return ( ( + + )} + handler={} + renderCollapseIcon={({ isCollapsed }) => ( + + )} /> ) } diff --git a/src/domain/product-categories/components/product-category-list-item-details.tsx b/src/domain/product-categories/components/product-category-list-item-details.tsx index 7967638246..3013baad14 100644 --- a/src/domain/product-categories/components/product-category-list-item-details.tsx +++ b/src/domain/product-categories/components/product-category-list-item-details.tsx @@ -1,6 +1,8 @@ import React, { useContext } from "react" import clsx from "clsx" +import { ProductCategory } from "@medusajs/medusa" + import { ProductCategoriesContext } from "../pages" import Tooltip from "../../../components/atoms/tooltip" import Button from "../../../components/fundamentals/button" @@ -10,31 +12,26 @@ import EditIcon from "../../../components/fundamentals/icons/edit-icon" import PlusIcon from "../../../components/fundamentals/icons/plus-icon" import { useAdminDeleteProductCategory } from "../../../../../medusa/packages/medusa-react" import FolderOpenIcon from "../../../components/fundamentals/icons/folder-open-icon" -import TriangleMiniIcon from "../../../components/fundamentals/icons/triangle-mini-icon" import TagIcon from "../../../components/fundamentals/icons/tag-icon" import MoreHorizontalIcon from "../../../components/fundamentals/icons/more-horizontal-icon" -import { DraggableListItem } from "./product-categories-list" type ProductCategoryListItemDetailsProps = { - depth: number - isOpen: boolean - item: DraggableListItem - toggleCategory: () => void + item: ProductCategory + handler: React.ReactNode + collapseIcon: React.ReactNode } function ProductCategoryListItemDetails( props: ProductCategoryListItemDetailsProps ) { - const { item, depth, isOpen, toggleCategory } = props + const { item } = props - const hasChildren = !!item.children?.length + const hasChildren = !!item.category_children?.length const productCategoriesPageContext = useContext(ProductCategoriesContext) const { mutateAsync: deleteCategory } = useAdminDeleteProductCategory(item.id) - const paddingLeft = hasChildren ? depth * 32 : (depth - 1) * 32 + 64 - const actions = [ { label: "Edit", @@ -46,73 +43,72 @@ function ProductCategoryListItemDetails( variant: "danger", onClick: deleteCategory, icon: , - disabled: !!item.category?.length, + disabled: !!item.category_children.length, }, ] return ( -
-
- {hasChildren && ( -
- +
+
+ {props.handler} + +
+
+ {hasChildren && ( +
+ {props.collapseIcon} +
+ )} +
+ {hasChildren && } + {!hasChildren && } +
+ + {item.name} +
- )} -
- {hasChildren && } - {!hasChildren && } -
- - {item.category.name} - -
-
- - Add category item to{" "} - - "{item.category.name}" - - - } - > - - - + + Add category item to{" "} + + "{item.name}" + + + } > - - - } - /> + + + + + + } + /> +
+
) From 4f07c2a3e83dd64e2581e111a4af080262b69723 Mon Sep 17 00:00:00 2001 From: fPolic Date: Thu, 23 Feb 2023 13:19:41 +0100 Subject: [PATCH 13/28] feat: tree levels padding --- .../components/product-category-list-item-details.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/domain/product-categories/components/product-category-list-item-details.tsx b/src/domain/product-categories/components/product-category-list-item-details.tsx index 3013baad14..8b677b5d54 100644 --- a/src/domain/product-categories/components/product-category-list-item-details.tsx +++ b/src/domain/product-categories/components/product-category-list-item-details.tsx @@ -16,6 +16,7 @@ import TagIcon from "../../../components/fundamentals/icons/tag-icon" import MoreHorizontalIcon from "../../../components/fundamentals/icons/more-horizontal-icon" type ProductCategoryListItemDetailsProps = { + depth: number item: ProductCategory handler: React.ReactNode collapseIcon: React.ReactNode @@ -49,8 +50,13 @@ function ProductCategoryListItemDetails( return (
-
- {props.handler} +
+
+ {props.handler} +
From b7b705838b264e197e4bc5e3e28c7a25401b3b4a Mon Sep 17 00:00:00 2001 From: fPolic Date: Thu, 23 Feb 2023 15:52:51 +0100 Subject: [PATCH 14/28] wip: saving changes to BD, fix medusa react imports, placeholder sizing --- src/components/organisms/sidebar/index.tsx | 4 +- .../components/product-categories-list.tsx | 43 +++++++++++++++++-- .../product-category-list-item-details.tsx | 2 +- .../modals/add-product-category.tsx | 5 ++- .../modals/edit-product-category.tsx | 2 +- src/domain/product-categories/pages/index.tsx | 2 +- .../styles/product-categories.css | 14 +++--- 7 files changed, 56 insertions(+), 16 deletions(-) diff --git a/src/components/organisms/sidebar/index.tsx b/src/components/organisms/sidebar/index.tsx index 162d7ab89d..83f085cf78 100644 --- a/src/components/organisms/sidebar/index.tsx +++ b/src/components/organisms/sidebar/index.tsx @@ -1,5 +1,6 @@ import { useAdminStore } from "medusa-react" import React, { useState } from "react" + import { useFeatureFlag } from "../../../context/feature-flag" import BuildingsIcon from "../../fundamentals/icons/buildings-icon" import CartIcon from "../../fundamentals/icons/cart-icon" @@ -12,7 +13,6 @@ import SwatchIcon from "../../fundamentals/icons/swatch-icon" import UsersIcon from "../../fundamentals/icons/users-icon" import SidebarMenuItem from "../../molecules/sidebar-menu-item" import UserMenu from "../../molecules/user-menu" -import { useFeatureFlag } from "../../../context/feature-flag" const ICON_SIZE = 20 @@ -33,8 +33,6 @@ const Sidebar: React.FC = () => { // infinite updates, and we do not want the variable to be free floating. triggerHandler.id = 0 - const { isFeatureEnabled } = useFeatureFlag() - const inventoryEnabled = isFeatureEnabled("inventoryService") && isFeatureEnabled("stockLocationService") diff --git a/src/domain/product-categories/components/product-categories-list.tsx b/src/domain/product-categories/components/product-categories-list.tsx index b5c9fcb163..28b680f940 100644 --- a/src/domain/product-categories/components/product-categories-list.tsx +++ b/src/domain/product-categories/components/product-categories-list.tsx @@ -1,14 +1,17 @@ import React, { useMemo } from "react" import Nestable from "react-nestable" +import { dropRight, get, flatMap } from "lodash" import "react-nestable/dist/styles/index.css" import "../styles/product-categories.css" import { ProductCategory } from "@medusajs/medusa" +import { adminProductCategoryKeys, useMedusa } from "medusa-react" import TriangleMiniIcon from "../../../components/fundamentals/icons/triangle-mini-icon" import ProductCategoryListItemDetails from "./product-category-list-item-details" import ReorderIcon from "../../../components/fundamentals/icons/reorder-icon" +import { useQueryClient } from "@tanstack/react-query" type ProductCategoriesListProps = { categories: ProductCategory[] @@ -18,6 +21,9 @@ type ProductCategoriesListProps = { * Draggable list that renders product categories tree view. */ function ProductCategoriesList(props: ProductCategoriesListProps) { + const { client } = useMedusa() + const queryClient = useQueryClient() + const categories = useMemo(() => { /** * HACK - for now to properly reference nested children @@ -28,9 +34,9 @@ function ProductCategoriesList(props: ProductCategoriesListProps) { const visit = (active) => { const node = categoriesMap[active.id] - node.category_children?.forEach((ch) => - Object.assign(ch, categoriesMap[ch.id]) - ) + node.category_children = node.category_children + .map((ch) => Object.assign(ch, categoriesMap[ch.id])) + .sort((a, b) => a.name.localeCompare(b.name)) return node } @@ -40,10 +46,41 @@ function ProductCategoriesList(props: ProductCategoriesListProps) { .map((c) => visit(c)) }, [props.categories]) + const onItemDrop = async (params: { + item: ProductCategory + items: ProductCategory[] + path: number[] + }) => { + const { dragItem, items, targetPath } = params + + if (targetPath.length === 1) { + await client.admin.productCategories.update(dragItem.id, { + parent_category_id: null, + }) + } else { + const newParent = get( + items, + dropRight( + flatMap(targetPath.slice(0, -1), (item) => [ + item, + "category_children", + ]) + ) + ) + + await client.admin.productCategories.update(dragItem.id, { + parent_category_id: newParent.id, + }) + } + + await queryClient.invalidateQueries(adminProductCategoryKeys.lists()) + } + return ( ( Date: Thu, 23 Feb 2023 17:36:46 +0100 Subject: [PATCH 15/28] fix: nested children association --- .../components/product-categories-list.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/domain/product-categories/components/product-categories-list.tsx b/src/domain/product-categories/components/product-categories-list.tsx index 28b680f940..658cb99786 100644 --- a/src/domain/product-categories/components/product-categories-list.tsx +++ b/src/domain/product-categories/components/product-categories-list.tsx @@ -35,8 +35,11 @@ function ProductCategoriesList(props: ProductCategoriesListProps) { const node = categoriesMap[active.id] node.category_children = node.category_children - .map((ch) => Object.assign(ch, categoriesMap[ch.id])) .sort((a, b) => a.name.localeCompare(b.name)) + .map((ch) => { + visit(ch) + return Object.assign(ch, categoriesMap[ch.id]) + }) return node } From 0d7fe24e1ff54a666755f6fe0d72231377d20603 Mon Sep 17 00:00:00 2001 From: fPolic Date: Thu, 23 Feb 2023 17:48:02 +0100 Subject: [PATCH 16/28] fix: edit modal spacing, fix z index layers for select --- src/components/molecules/modal/side-modal.tsx | 2 +- src/components/molecules/select/index.tsx | 7 ++- .../modals/edit-product-category.tsx | 55 ++++++++++--------- 3 files changed, 36 insertions(+), 28 deletions(-) diff --git a/src/components/molecules/modal/side-modal.tsx b/src/components/molecules/modal/side-modal.tsx index 14f4933403..f0fd25462a 100644 --- a/src/components/molecules/modal/side-modal.tsx +++ b/src/components/molecules/modal/side-modal.tsx @@ -43,7 +43,7 @@ function SideModal(props: SideModalProps) { background: "white", right: 0, top: 0, - zIndex: 9999, + zIndex: 200, }} className="rounded border overflow-hidden" animate={{ right: 0 }} diff --git a/src/components/molecules/select/index.tsx b/src/components/molecules/select/index.tsx index 85cea7fbef..3e7f59a084 100644 --- a/src/components/molecules/select/index.tsx +++ b/src/components/molecules/select/index.tsx @@ -1,5 +1,6 @@ import clsx from "clsx" import React, { + CSSProperties, useContext, useEffect, useImperativeHandle, @@ -31,6 +32,7 @@ type MultiSelectProps = InputHeaderProps & { placeholder?: string isMultiSelect?: boolean labelledBy?: string + menuPortalStyles?: CSSProperties options: { label: string; value: string | null; disabled?: boolean }[] value: | { label: string; value: string }[] @@ -69,6 +71,7 @@ const SSelect = React.forwardRef( placeholder = "Search...", options, onCreateOption, + menuPortalStyles = {}, }: MultiSelectProps, ref ) => { @@ -176,7 +179,9 @@ const SSelect = React.forwardRef( }} closeMenuOnSelect={!isMultiSelect} blurInputOnSelect={!isMultiSelect} - styles={{ menuPortal: (base) => ({ ...base, zIndex: 60 }) }} + styles={{ + menuPortal: (base) => ({ ...base, ...menuPortalStyles }), + }} hideSelectedOptions={false} menuPortalTarget={portalRef?.current?.lastChild || document.body} menuPlacement="auto" diff --git a/src/domain/product-categories/modals/edit-product-category.tsx b/src/domain/product-categories/modals/edit-product-category.tsx index 8424e677ad..166d38ad6d 100644 --- a/src/domain/product-categories/modals/edit-product-category.tsx +++ b/src/domain/product-categories/modals/edit-product-category.tsx @@ -99,36 +99,39 @@ function EditProductCategoriesSideModal( {/* === DIVIDER === */}
-
- setName(ev.target.value)} - /> - - setHandle(ev.target.value)} - /> - - setIsActive(o.value === "active")} + /> setIsActive(o.value === "active")} /> @@ -121,6 +122,7 @@ function CreateProductCategory(props: CreateProductCategoryProps) { Date: Wed, 1 Mar 2023 18:24:32 +0100 Subject: [PATCH 22/28] chore: added ranking to the tree --- .../components/product-categories-list.tsx | 29 ++----------------- src/domain/product-categories/pages/index.tsx | 5 ++-- 2 files changed, 5 insertions(+), 29 deletions(-) diff --git a/src/domain/product-categories/components/product-categories-list.tsx b/src/domain/product-categories/components/product-categories-list.tsx index cb069f867a..ccef343f2b 100644 --- a/src/domain/product-categories/components/product-categories-list.tsx +++ b/src/domain/product-categories/components/product-categories-list.tsx @@ -25,32 +25,7 @@ function ProductCategoriesList(props: ProductCategoriesListProps) { const { client } = useMedusa() const queryClient = useQueryClient() const notification = useNotification() - - const categories = useMemo(() => { - /** - * HACK - for now to properly reference nested children - */ - const categoriesMap = {} - props.categories.forEach((c) => (categoriesMap[c.id] = c)) - - const visit = (active) => { - const node = categoriesMap[active.id] - - node.category_children = node.category_children - .sort((a, b) => a.name.localeCompare(b.name)) - .map((ch) => { - visit(ch) - return Object.assign(ch, categoriesMap[ch.id]) - }) - - return node - } - - return props.categories - .filter((c) => !c.parent_category_id) - .sort((a, b) => a.name.localeCompare(b.name)) - .map((c) => visit(c)) - }, [props.categories]) + const { categories } = props const onItemDrop = async (params: { item: ProductCategory @@ -59,6 +34,7 @@ function ProductCategoriesList(props: ProductCategoriesListProps) { }) => { let parentId = null const { dragItem, items, targetPath } = params + const [position] = targetPath.slice(-1) if (targetPath.length > 1) { const path = dropRight( @@ -72,6 +48,7 @@ function ProductCategoriesList(props: ProductCategoriesListProps) { try { await client.admin.productCategories.update(dragItem.id, { parent_category_id: parentId, + position, }) notification("Success", "New order saved", "success") await queryClient.invalidateQueries(adminProductCategoryKeys.lists()) diff --git a/src/domain/product-categories/pages/index.tsx b/src/domain/product-categories/pages/index.tsx index f1ecbc5e70..b4433cd094 100644 --- a/src/domain/product-categories/pages/index.tsx +++ b/src/domain/product-categories/pages/index.tsx @@ -48,9 +48,8 @@ function ProductCategoryPage() { const { product_categories: categories, isLoading } = useAdminProductCategories({ - expand: "category_children", - // TODO: doesn't work - // parent_category_id: null, + parent_category_id: "null", + include_descendants_tree: true, }) const actions = [ From 1b1ee27dc2a43d8930dfa457574b82977948b1c8 Mon Sep 17 00:00:00 2001 From: Riqwan Thamir Date: Thu, 2 Mar 2023 22:02:09 +0100 Subject: [PATCH 23/28] chore: remove duplicate import --- src/pages/a.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/a.jsx b/src/pages/a.jsx index 4d004e6d42..32b74d77ce 100644 --- a/src/pages/a.jsx +++ b/src/pages/a.jsx @@ -19,7 +19,6 @@ import Pricing from "../domain/pricing" import ProductsRoute from "../domain/products" import ProductCategories from "../domain/product-categories" import PublishableApiKeys from "../domain/publishable-api-keys" -import ProductCategories from "../domain/product-categories" import SalesChannels from "../domain/sales-channels" import Settings from "../domain/settings" From d5f007de5358bd0b5873b8e47578c7f864c5661a Mon Sep 17 00:00:00 2001 From: Riqwan Thamir Date: Fri, 3 Mar 2023 12:25:38 +0100 Subject: [PATCH 24/28] chore: rename rank to position --- .../product-categories/components/product-categories-list.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/domain/product-categories/components/product-categories-list.tsx b/src/domain/product-categories/components/product-categories-list.tsx index ccef343f2b..145e5c3de0 100644 --- a/src/domain/product-categories/components/product-categories-list.tsx +++ b/src/domain/product-categories/components/product-categories-list.tsx @@ -34,7 +34,7 @@ function ProductCategoriesList(props: ProductCategoriesListProps) { }) => { let parentId = null const { dragItem, items, targetPath } = params - const [position] = targetPath.slice(-1) + const [rank] = targetPath.slice(-1) if (targetPath.length > 1) { const path = dropRight( @@ -48,7 +48,7 @@ function ProductCategoriesList(props: ProductCategoriesListProps) { try { await client.admin.productCategories.update(dragItem.id, { parent_category_id: parentId, - position, + rank, }) notification("Success", "New order saved", "success") await queryClient.invalidateQueries(adminProductCategoryKeys.lists()) From 65e046e5f16888c402df8b48b22743cf8be64a06 Mon Sep 17 00:00:00 2001 From: Riqwan Thamir Date: Fri, 3 Mar 2023 12:31:12 +0100 Subject: [PATCH 25/28] added handle to create --- src/domain/product-categories/modals/add-product-category.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/domain/product-categories/modals/add-product-category.tsx b/src/domain/product-categories/modals/add-product-category.tsx index e6ec8090c8..3267478851 100644 --- a/src/domain/product-categories/modals/add-product-category.tsx +++ b/src/domain/product-categories/modals/add-product-category.tsx @@ -51,6 +51,7 @@ function CreateProductCategory(props: CreateProductCategoryProps) { try { await createProductCategory({ name, + handle, is_active: isActive, is_internal: !isPublic, parent_category_id: parentCategory?.id ?? null, From c4926ccbb3c5ba5382e768acdbd6218dbef0963a Mon Sep 17 00:00:00 2001 From: fPolic Date: Mon, 6 Mar 2023 13:25:12 +0100 Subject: [PATCH 26/28] fix: prevent list flicker on reorder --- .../components/product-categories-list.tsx | 4 ++-- src/domain/product-categories/pages/index.tsx | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/domain/product-categories/components/product-categories-list.tsx b/src/domain/product-categories/components/product-categories-list.tsx index 145e5c3de0..bfb01df3cb 100644 --- a/src/domain/product-categories/components/product-categories-list.tsx +++ b/src/domain/product-categories/components/product-categories-list.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from "react" +import React from "react" import Nestable from "react-nestable" import { dropRight, get, flatMap } from "lodash" @@ -88,4 +88,4 @@ function ProductCategoriesList(props: ProductCategoriesListProps) { ) } -export default ProductCategoriesList +export default React.memo(ProductCategoriesList) // Memo prevents list flicker on reorder diff --git a/src/domain/product-categories/pages/index.tsx b/src/domain/product-categories/pages/index.tsx index b4433cd094..2fa5fcc59d 100644 --- a/src/domain/product-categories/pages/index.tsx +++ b/src/domain/product-categories/pages/index.tsx @@ -14,7 +14,7 @@ import EditProductCategoriesSideModal from "../modals/edit-product-category" */ function ProductCategoriesEmptyState() { return ( -
+

No product categories yet, use the above button to create your first category. @@ -78,8 +78,8 @@ function ProductCategoryPage() { return ( -

-
+
+
Date: Mon, 6 Mar 2023 16:46:19 +0100 Subject: [PATCH 27/28] feat: disable reorder while update is running, memo the list --- .../components/product-categories-list.tsx | 136 +++++++++++------- 1 file changed, 86 insertions(+), 50 deletions(-) diff --git a/src/domain/product-categories/components/product-categories-list.tsx b/src/domain/product-categories/components/product-categories-list.tsx index bfb01df3cb..18c513e6e7 100644 --- a/src/domain/product-categories/components/product-categories-list.tsx +++ b/src/domain/product-categories/components/product-categories-list.tsx @@ -1,4 +1,4 @@ -import React from "react" +import React, { useCallback, useMemo, useState } from "react" import Nestable from "react-nestable" import { dropRight, get, flatMap } from "lodash" @@ -25,66 +25,102 @@ function ProductCategoriesList(props: ProductCategoriesListProps) { const { client } = useMedusa() const queryClient = useQueryClient() const notification = useNotification() + const [isUpdating, setIsUpdating] = useState(false) + const { categories } = props - const onItemDrop = async (params: { - item: ProductCategory - items: ProductCategory[] - path: number[] - }) => { - let parentId = null - const { dragItem, items, targetPath } = params - const [rank] = targetPath.slice(-1) + const onItemDrop = useCallback( + async (params: { + item: ProductCategory + items: ProductCategory[] + path: number[] + }) => { + setIsUpdating(true) + let parentId = null + const { dragItem, items, targetPath } = params + const [rank] = targetPath.slice(-1) + + if (targetPath.length > 1) { + const path = dropRight( + flatMap(targetPath.slice(0, -1), (item) => [ + item, + "category_children", + ]) + ) - if (targetPath.length > 1) { - const path = dropRight( - flatMap(targetPath.slice(0, -1), (item) => [item, "category_children"]) - ) + const newParent = get(items, path) + parentId = newParent.id + } - const newParent = get(items, path) - parentId = newParent.id - } + try { + await client.admin.productCategories.update(dragItem.id, { + parent_category_id: parentId, + rank, + }) + notification("Success", "New order saved", "success") + await queryClient.invalidateQueries(adminProductCategoryKeys.lists()) + } catch (e) { + notification("Error", "Failed to save new order", "error") + } finally { + setIsUpdating(false) + } + }, + [] + ) - try { - await client.admin.productCategories.update(dragItem.id, { - parent_category_id: parentId, - rank, - }) - notification("Success", "New order saved", "success") - await queryClient.invalidateQueries(adminProductCategoryKeys.lists()) - } catch (e) { - notification("Error", "Failed to save new order", "error") - } - } + const NestableList = useMemo( + () => ( + ( + + )} + handler={} + renderCollapseIcon={({ isCollapsed }) => ( + + )} + /> + ), + [categories] + ) return ( - ( - - )} - handler={} - renderCollapseIcon={({ isCollapsed }) => ( - + {NestableList} + {isUpdating && ( +
)} - /> +
) } From 47f3376376099060c7c883af6ebae9f15bb70b3c Mon Sep 17 00:00:00 2001 From: fPolic Date: Mon, 6 Mar 2023 16:52:38 +0100 Subject: [PATCH 28/28] fix: display notification on category deletion --- .../product-category-list-item-details.tsx | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/src/domain/product-categories/components/product-category-list-item-details.tsx b/src/domain/product-categories/components/product-category-list-item-details.tsx index ff64a0131a..b8fc2bc715 100644 --- a/src/domain/product-categories/components/product-category-list-item-details.tsx +++ b/src/domain/product-categories/components/product-category-list-item-details.tsx @@ -14,6 +14,7 @@ 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 MoreHorizontalIcon from "../../../components/fundamentals/icons/more-horizontal-icon" +import useNotification from "../../../hooks/use-notification" type ProductCategoryListItemDetailsProps = { depth: number @@ -26,6 +27,7 @@ function ProductCategoryListItemDetails( props: ProductCategoryListItemDetailsProps ) { const { item } = props + const notification = useNotification() const hasChildren = !!item.category_children?.length @@ -42,7 +44,14 @@ function ProductCategoryListItemDetails( { label: "Delete", variant: "danger", - onClick: deleteCategory, + onClick: async () => { + try { + await deleteCategory() + notification("Success", "Category deleted", "success") + } catch (e) { + notification("Error", "Category deletion failed", "error") + } + }, icon: , disabled: !!item.category_children.length, }, @@ -52,33 +61,33 @@ function ProductCategoryListItemDetails(
-
+
{props.handler}
-
+
{hasChildren && ( -
+
{props.collapseIcon}
)} -
+
{hasChildren && } {!hasChildren && }
{item.name}
-
+