feat(dashboard): Add Optimistic UI to category rank form + style updates (#7747)
**What** - Makes rank updates optimistic, meaning that we override the local state (ranking) with what we expect the outcome of the request to be. If a request fails then we revert to the last known server state. - Updates the style of dragged items. - Fixes an issue where the tree would flicker when submitting the create form.
This commit is contained in:
1 parent
288e41856b
commit
d5c5628ffc
9 files changed
+209
-158
No files matched your search
+6
-1
@@ -30,6 +30,7 @@ export const CreateCategoryForm = ({
|
|||||||
|
|
||||||
const [activeTab, setActiveTab] = useState<Tab>(Tab.DETAILS)
|
const [activeTab, setActiveTab] = useState<Tab>(Tab.DETAILS)
|
||||||
const [validDetails, setValidDetails] = useState(false)
|
const [validDetails, setValidDetails] = useState(false)
|
||||||
|
const [shouldFreeze, setShouldFreeze] = useState(false)
|
||||||
|
|
||||||
const form = useForm<CreateCategorySchema>({
|
const form = useForm<CreateCategorySchema>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -79,6 +80,8 @@ export const CreateCategoryForm = ({
|
|||||||
const handleSubmit = form.handleSubmit((data) => {
|
const handleSubmit = form.handleSubmit((data) => {
|
||||||
const { visibility, status, parent_category_id, rank, ...rest } = data
|
const { visibility, status, parent_category_id, rank, ...rest } = data
|
||||||
|
|
||||||
|
setShouldFreeze(true)
|
||||||
|
|
||||||
mutateAsync(
|
mutateAsync(
|
||||||
{
|
{
|
||||||
...rest,
|
...rest,
|
||||||
@@ -105,6 +108,8 @@ export const CreateCategoryForm = ({
|
|||||||
dismissable: true,
|
dismissable: true,
|
||||||
dismissLabel: t("actions.close"),
|
dismissLabel: t("actions.close"),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
setShouldFreeze(false)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -181,7 +186,7 @@ export const CreateCategoryForm = ({
|
|||||||
<CreateCategoryDetails form={form} />
|
<CreateCategoryDetails form={form} />
|
||||||
</ProgressTabs.Content>
|
</ProgressTabs.Content>
|
||||||
<ProgressTabs.Content value={Tab.ORGANIZE}>
|
<ProgressTabs.Content value={Tab.ORGANIZE}>
|
||||||
<CreateCategoryNesting form={form} />
|
<CreateCategoryNesting form={form} shouldFreeze={shouldFreeze} />
|
||||||
</ProgressTabs.Content>
|
</ProgressTabs.Content>
|
||||||
</RouteFocusModal.Body>
|
</RouteFocusModal.Body>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
+20
-84
@@ -1,19 +1,24 @@
|
|||||||
import { useMemo } from "react"
|
import { useMemo, useState } from "react"
|
||||||
import { UseFormReturn, useWatch } from "react-hook-form"
|
import { UseFormReturn, useWatch } from "react-hook-form"
|
||||||
import { useProductCategories } from "../../../../../hooks/api/categories"
|
import { useProductCategories } from "../../../../../hooks/api/categories"
|
||||||
import {
|
import { CategoryTree } from "../../../common/components/category-tree"
|
||||||
CategoryTree,
|
import { CategoryTreeItem } from "../../../common/types"
|
||||||
CategoryTreeItem,
|
import { insertCategoryTreeItem } from "../../../common/utils"
|
||||||
} from "../../../common/components/category-tree"
|
|
||||||
import { CreateCategorySchema } from "./schema"
|
import { CreateCategorySchema } from "./schema"
|
||||||
|
|
||||||
type CreateCategoryNestingProps = {
|
type CreateCategoryNestingProps = {
|
||||||
form: UseFormReturn<CreateCategorySchema>
|
form: UseFormReturn<CreateCategorySchema>
|
||||||
|
shouldFreeze?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const ID = "new-item"
|
const ID = "new-item"
|
||||||
|
|
||||||
export const CreateCategoryNesting = ({ form }: CreateCategoryNestingProps) => {
|
export const CreateCategoryNesting = ({
|
||||||
|
form,
|
||||||
|
shouldFreeze,
|
||||||
|
}: CreateCategoryNestingProps) => {
|
||||||
|
const [snapshot, setSnapshot] = useState<CategoryTreeItem[]>([])
|
||||||
|
|
||||||
const { product_categories, isPending, isError, error } =
|
const { product_categories, isPending, isError, error } =
|
||||||
useProductCategories(
|
useProductCategories(
|
||||||
{
|
{
|
||||||
@@ -48,24 +53,27 @@ export const CreateCategoryNesting = ({ form }: CreateCategoryNestingProps) => {
|
|||||||
name: watchedName,
|
name: watchedName,
|
||||||
parent_category_id: parentCategoryId,
|
parent_category_id: parentCategoryId,
|
||||||
rank: watchedRank,
|
rank: watchedRank,
|
||||||
|
category_children: null,
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("inserting", temp)
|
|
||||||
|
|
||||||
return insertCategoryTreeItem(product_categories ?? [], temp)
|
return insertCategoryTreeItem(product_categories ?? [], temp)
|
||||||
}, [product_categories, watchedName, parentCategoryId, watchedRank])
|
}, [product_categories, watchedName, parentCategoryId, watchedRank])
|
||||||
|
|
||||||
const handleChange = ({ parent_category_id, rank }: CategoryTreeItem) => {
|
const handleChange = (
|
||||||
|
{ parent_category_id, rank }: CategoryTreeItem,
|
||||||
|
list: CategoryTreeItem[]
|
||||||
|
) => {
|
||||||
form.setValue("parent_category_id", parent_category_id, {
|
form.setValue("parent_category_id", parent_category_id, {
|
||||||
shouldDirty: true,
|
shouldDirty: true,
|
||||||
shouldTouch: true,
|
shouldTouch: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log("rank", rank)
|
|
||||||
form.setValue("rank", rank, {
|
form.setValue("rank", rank, {
|
||||||
shouldDirty: true,
|
shouldDirty: true,
|
||||||
shouldTouch: true,
|
shouldTouch: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
setSnapshot(list)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isError) {
|
if (isError) {
|
||||||
@@ -74,7 +82,8 @@ export const CreateCategoryNesting = ({ form }: CreateCategoryNestingProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<CategoryTree
|
<CategoryTree
|
||||||
value={value}
|
// When we submit the form we want to freeze the rendered tree to prevent flickering during the exit animation
|
||||||
|
value={shouldFreeze ? snapshot : value}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
enableDrag={(i) => i.id === ID}
|
enableDrag={(i) => i.id === ID}
|
||||||
showBadge={(i) => i.id === ID}
|
showBadge={(i) => i.id === ID}
|
||||||
@@ -82,76 +91,3 @@ export const CreateCategoryNesting = ({ form }: CreateCategoryNestingProps) => {
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Since we allow the user to go back and forth between the two steps of the form,
|
|
||||||
* we need to handle restoring the state of the tree when it re-renders.
|
|
||||||
*/
|
|
||||||
const insertCategoryTreeItem = (
|
|
||||||
categories: CategoryTreeItem[],
|
|
||||||
newItem: Omit<CategoryTreeItem, "category_children">
|
|
||||||
): CategoryTreeItem[] => {
|
|
||||||
const seen = new Set<string>()
|
|
||||||
|
|
||||||
const remove = (
|
|
||||||
items: CategoryTreeItem[],
|
|
||||||
id: string
|
|
||||||
): CategoryTreeItem[] => {
|
|
||||||
const stack = [...items]
|
|
||||||
const result: CategoryTreeItem[] = []
|
|
||||||
|
|
||||||
while (stack.length > 0) {
|
|
||||||
const item = stack.pop()!
|
|
||||||
if (item.id !== id) {
|
|
||||||
if (item.category_children) {
|
|
||||||
item.category_children = remove(item.category_children, id)
|
|
||||||
}
|
|
||||||
result.push(item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
const insert = (items: CategoryTreeItem[]): CategoryTreeItem[] => {
|
|
||||||
const stack = [...items]
|
|
||||||
|
|
||||||
while (stack.length > 0) {
|
|
||||||
const item = stack.pop()!
|
|
||||||
if (seen.has(item.id)) {
|
|
||||||
continue // Prevent revisiting the same node
|
|
||||||
}
|
|
||||||
seen.add(item.id)
|
|
||||||
|
|
||||||
if (item.id === newItem.parent_category_id) {
|
|
||||||
if (!item.category_children) {
|
|
||||||
item.category_children = []
|
|
||||||
}
|
|
||||||
item.category_children.push({ ...newItem, category_children: null })
|
|
||||||
item.category_children.sort((a, b) => (a.rank ?? 0) - (b.rank ?? 0))
|
|
||||||
return categories
|
|
||||||
}
|
|
||||||
if (item.category_children) {
|
|
||||||
stack.push(...item.category_children)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return items
|
|
||||||
}
|
|
||||||
|
|
||||||
categories = remove(categories, newItem.id)
|
|
||||||
|
|
||||||
if (newItem.parent_category_id === null && newItem.rank === null) {
|
|
||||||
categories.unshift({ ...newItem, category_children: null })
|
|
||||||
} else if (newItem.parent_category_id === null && newItem.rank !== null) {
|
|
||||||
categories.splice(newItem.rank, 0, {
|
|
||||||
...newItem,
|
|
||||||
category_children: null,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
categories = insert(categories)
|
|
||||||
}
|
|
||||||
|
|
||||||
categories.sort((a, b) => (a.rank ?? 0) - (b.rank ?? 0))
|
|
||||||
|
|
||||||
return categories
|
|
||||||
}
|
|
||||||
+1
-5
@@ -1,14 +1,10 @@
|
|||||||
import { useParams } from "react-router-dom"
|
|
||||||
import { RouteFocusModal } from "../../../components/route-modal"
|
import { RouteFocusModal } from "../../../components/route-modal"
|
||||||
import { OrganizeCategoryForm } from "./components/organize-category-form/organize-category-form"
|
import { OrganizeCategoryForm } from "./components/organize-category-form/organize-category-form"
|
||||||
|
|
||||||
// TODO: Something around the mpath of categories is bugged out, and using this form breaks your categories. See CORE-2287.
|
|
||||||
export const CategoryOrganize = () => {
|
export const CategoryOrganize = () => {
|
||||||
const { id } = useParams()
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RouteFocusModal>
|
<RouteFocusModal>
|
||||||
<OrganizeCategoryForm categoryId={id} />
|
<OrganizeCategoryForm />
|
||||||
</RouteFocusModal>
|
</RouteFocusModal>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
+70
-45
@@ -1,8 +1,10 @@
|
|||||||
import { keepPreviousData } from "@tanstack/react-query"
|
import { useMutation } from "@tanstack/react-query"
|
||||||
|
|
||||||
import { Spinner } from "@medusajs/icons"
|
import { Spinner } from "@medusajs/icons"
|
||||||
import { FetchError } from "@medusajs/js-sdk"
|
import { FetchError } from "@medusajs/js-sdk"
|
||||||
import { useState } from "react"
|
import { HttpTypes } from "@medusajs/types"
|
||||||
|
import { toast } from "@medusajs/ui"
|
||||||
|
import { t } from "i18next"
|
||||||
import { RouteFocusModal } from "../../../../../components/route-modal"
|
import { RouteFocusModal } from "../../../../../components/route-modal"
|
||||||
import {
|
import {
|
||||||
categoriesQueryKeys,
|
categoriesQueryKeys,
|
||||||
@@ -10,66 +12,89 @@ import {
|
|||||||
} from "../../../../../hooks/api/categories"
|
} from "../../../../../hooks/api/categories"
|
||||||
import { sdk } from "../../../../../lib/client"
|
import { sdk } from "../../../../../lib/client"
|
||||||
import { queryClient } from "../../../../../lib/query-client"
|
import { queryClient } from "../../../../../lib/query-client"
|
||||||
import {
|
import { CategoryTree } from "../../../common/components/category-tree"
|
||||||
CategoryTree,
|
import { CategoryTreeItem } from "../../../common/types"
|
||||||
CategoryTreeItem,
|
|
||||||
} from "../../../common/components/category-tree"
|
|
||||||
|
|
||||||
type OrganizeCategoryFormProps = {
|
const QUERY = {
|
||||||
categoryId?: string
|
fields: "id,name,parent_category_id,rank,*category_children",
|
||||||
|
parent_category_id: "null",
|
||||||
|
include_descendants_tree: true,
|
||||||
|
limit: 9999,
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Add some focus/highlight state if we enter this form from a specific category. Awaiting design.
|
export const OrganizeCategoryForm = () => {
|
||||||
export const OrganizeCategoryForm = ({
|
|
||||||
categoryId,
|
|
||||||
}: OrganizeCategoryFormProps) => {
|
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
|
||||||
// TODO: Display error message to the user, might be in a toast or in the header. Awaiting design.
|
|
||||||
const [error, setError] = useState<FetchError | null>(null)
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
product_categories,
|
product_categories,
|
||||||
isPending,
|
isPending,
|
||||||
isError,
|
isError,
|
||||||
error: fetchError,
|
error: fetchError,
|
||||||
} = useProductCategories(
|
} = useProductCategories(QUERY)
|
||||||
{
|
|
||||||
fields: "id,name,parent_category_id,rank,*category_children",
|
|
||||||
parent_category_id: "null",
|
|
||||||
include_descendants_tree: true,
|
|
||||||
limit: 9999,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
placeholderData: keepPreviousData,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const handleRankChange = async (value: CategoryTreeItem) => {
|
const { mutateAsync, isPending: isMutating } = useMutation({
|
||||||
setIsLoading(true)
|
mutationFn: async ({
|
||||||
setError(null)
|
value,
|
||||||
|
}: {
|
||||||
await sdk.admin.productCategory
|
value: CategoryTreeItem
|
||||||
.update(value.id, {
|
arr: CategoryTreeItem[]
|
||||||
|
}) => {
|
||||||
|
await sdk.admin.productCategory.update(value.id, {
|
||||||
rank: value.rank ?? 0,
|
rank: value.rank ?? 0,
|
||||||
parent_category_id: value.parent_category_id,
|
parent_category_id: value.parent_category_id,
|
||||||
})
|
})
|
||||||
.then(async () => {
|
},
|
||||||
await queryClient.invalidateQueries({
|
onMutate: async (update) => {
|
||||||
queryKey: categoriesQueryKeys.lists(),
|
// Cancel any outgoing refetches (so they don't overwrite our optimistic update)
|
||||||
})
|
await queryClient.cancelQueries({
|
||||||
await queryClient.invalidateQueries({
|
queryKey: categoriesQueryKeys.list(QUERY),
|
||||||
queryKey: categoriesQueryKeys.detail(value.id),
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
|
||||||
setError(error)
|
// Snapshot the previous value
|
||||||
|
const previousValue:
|
||||||
|
| HttpTypes.AdminProductCategoryListResponse
|
||||||
|
| undefined = queryClient.getQueryData(categoriesQueryKeys.list(QUERY))
|
||||||
|
|
||||||
|
const nextValue = {
|
||||||
|
...previousValue,
|
||||||
|
product_categories: update.arr,
|
||||||
|
}
|
||||||
|
|
||||||
|
queryClient.setQueryData(categoriesQueryKeys.list(QUERY), nextValue)
|
||||||
|
|
||||||
|
return {
|
||||||
|
previousValue,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError: (error: FetchError, _newValue, context) => {
|
||||||
|
// Roll back to the previous value
|
||||||
|
queryClient.setQueryData(
|
||||||
|
categoriesQueryKeys.list(QUERY),
|
||||||
|
context?.previousValue
|
||||||
|
)
|
||||||
|
|
||||||
|
toast.error(t("general.error"), {
|
||||||
|
description: error.message,
|
||||||
|
dismissLabel: t("general.close"),
|
||||||
|
dismissable: true,
|
||||||
})
|
})
|
||||||
.finally(() => {
|
},
|
||||||
setIsLoading(false)
|
onSettled: async (_data, _error, variables) => {
|
||||||
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: categoriesQueryKeys.lists(),
|
||||||
})
|
})
|
||||||
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: categoriesQueryKeys.detail(variables.value.id),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleRankChange = async (
|
||||||
|
value: CategoryTreeItem,
|
||||||
|
arr: CategoryTreeItem[]
|
||||||
|
) => {
|
||||||
|
await mutateAsync({ value, arr })
|
||||||
}
|
}
|
||||||
|
|
||||||
const loading = isPending || isLoading
|
const loading = isPending || isMutating
|
||||||
|
|
||||||
if (isError) {
|
if (isError) {
|
||||||
throw fetchError
|
throw fetchError
|
||||||
|
|||||||
+9
-16
@@ -14,8 +14,8 @@ import Nestable from "react-nestable"
|
|||||||
|
|
||||||
import { useTranslation } from "react-i18next"
|
import { useTranslation } from "react-i18next"
|
||||||
import "react-nestable/dist/styles/index.css"
|
import "react-nestable/dist/styles/index.css"
|
||||||
|
import { CategoryTreeItem } from "../../types"
|
||||||
import "./styles.css"
|
import "./styles.css"
|
||||||
import { CategoryTreeItem } from "./types"
|
|
||||||
|
|
||||||
type CategoryTreeProps = {
|
type CategoryTreeProps = {
|
||||||
value: CategoryTreeItem[]
|
value: CategoryTreeItem[]
|
||||||
@@ -119,7 +119,6 @@ export const CategoryTree = ({
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
handler={<DragHandle />}
|
|
||||||
renderCollapseIcon={({ isCollapsed }) => {
|
renderCollapseIcon={({ isCollapsed }) => {
|
||||||
return <CollapseHandler isCollapsed={isCollapsed} />
|
return <CollapseHandler isCollapsed={isCollapsed} />
|
||||||
}}
|
}}
|
||||||
@@ -155,7 +154,7 @@ type CategoryBranchProps = {
|
|||||||
isEnabled: boolean
|
isEnabled: boolean
|
||||||
isNew?: boolean
|
isNew?: boolean
|
||||||
collapseIcon: ReactNode
|
collapseIcon: ReactNode
|
||||||
handler: ReactNode
|
handler?: ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CategoryBranch = ({
|
export const CategoryBranch = ({
|
||||||
@@ -164,7 +163,6 @@ export const CategoryBranch = ({
|
|||||||
isEnabled,
|
isEnabled,
|
||||||
isNew = false,
|
isNew = false,
|
||||||
collapseIcon,
|
collapseIcon,
|
||||||
handler,
|
|
||||||
}: CategoryBranchProps) => {
|
}: CategoryBranchProps) => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
||||||
@@ -172,15 +170,19 @@ export const CategoryBranch = ({
|
|||||||
|
|
||||||
const Component = (
|
const Component = (
|
||||||
<div
|
<div
|
||||||
|
onDrag={() => console.log("dragging")}
|
||||||
data-disabled={!isEnabled}
|
data-disabled={!isEnabled}
|
||||||
className={clx(
|
className={clx(
|
||||||
"bg-ui-bg-base hover:bg-ui-bg-base-hover transition-fg group group flex h-12 items-center gap-x-3 border-b px-6 py-2.5",
|
"bg-ui-bg-base hover:bg-ui-bg-base-hover transition-fg group group flex h-12 cursor-grab items-center gap-x-3 border-b px-6 py-2.5 active:cursor-grabbing",
|
||||||
{
|
{
|
||||||
"bg-ui-bg-subtle hover:bg-ui-bg-subtle": !isEnabled,
|
"bg-ui-bg-subtle hover:bg-ui-bg-subtle cursor-not-allowed":
|
||||||
|
!isEnabled,
|
||||||
}
|
}
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div>{handler}</div>
|
<div className="flex h-7 w-7 items-center justify-center">
|
||||||
|
<DotsSix className="text-ui-fg-subtle" />
|
||||||
|
</div>
|
||||||
{Array.from({ length: depth }).map((_, i) => (
|
{Array.from({ length: depth }).map((_, i) => (
|
||||||
<div key={`offset_${i}`} role="presentation" className="h-7 w-7" />
|
<div key={`offset_${i}`} role="presentation" className="h-7 w-7" />
|
||||||
))}
|
))}
|
||||||
@@ -198,7 +200,6 @@ export const CategoryBranch = ({
|
|||||||
<Text size="small" leading="compact">
|
<Text size="small" leading="compact">
|
||||||
{item.name}
|
{item.name}
|
||||||
</Text>
|
</Text>
|
||||||
<Text>{item.rank}</Text>
|
|
||||||
{isNew && (
|
{isNew && (
|
||||||
<Badge size="2xsmall" color="blue">
|
<Badge size="2xsmall" color="blue">
|
||||||
{t("categories.fields.new.label")}
|
{t("categories.fields.new.label")}
|
||||||
@@ -211,14 +212,6 @@ export const CategoryBranch = ({
|
|||||||
return Component
|
return Component
|
||||||
}
|
}
|
||||||
|
|
||||||
const DragHandle = () => {
|
|
||||||
return (
|
|
||||||
<div className="flex h-7 w-7 cursor-grab items-center justify-center active:cursor-grabbing group-data-[disabled=true]:cursor-not-allowed">
|
|
||||||
<DotsSix className="text-ui-fg-subtle" />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const CategoryLeafPlaceholder = () => {
|
const CategoryLeafPlaceholder = () => {
|
||||||
return (
|
return (
|
||||||
<div className="bg-ui-bg-base flex h-12 animate-pulse items-center border-b px-6 py-2.5" />
|
<div className="bg-ui-bg-base flex h-12 animate-pulse items-center border-b px-6 py-2.5" />
|
||||||
|
|||||||
-1
@@ -1,2 +1 @@
|
|||||||
export * from "./category-tree"
|
export * from "./category-tree"
|
||||||
export * from "./types"
|
|
||||||
+17
-6
@@ -9,28 +9,39 @@
|
|||||||
left: 0 !important;
|
left: 0 !important;
|
||||||
right: 0 !important;
|
right: 0 !important;
|
||||||
bottom: 0 !important;
|
bottom: 0 !important;
|
||||||
background: transparent !important;
|
background: var(--bg-base-hover) !important;
|
||||||
border-radius: 0 !important;
|
border-radius: 0 !important;
|
||||||
|
|
||||||
transition: 0.3s all !important;
|
transition: 0.3s all !important;
|
||||||
border: 1px dashed var(--border-interactive) !important;
|
border: 0px !important;
|
||||||
|
border-bottom: 1px solid var(--border-base) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nestable-item.is-dragging * {
|
.nestable-item.is-dragging * {
|
||||||
height: 48px !important;
|
height: 48px !important;
|
||||||
min-height: 48px !important;
|
min-height: 48px !important;
|
||||||
max-height: 48px !important;
|
max-height: 48px !important;
|
||||||
border-radius: 6 !important;
|
|
||||||
overflow: hidden !important;
|
overflow: hidden !important;
|
||||||
background: var(--bg-base) !important;
|
background: var(--bg-base) !important;
|
||||||
box-shadow: var(--elevation-card-hover) !important;
|
box-shadow: var(--elevation-card-hover) !important;
|
||||||
z-index: 1000 !important;
|
z-index: 1000 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nestable-drag-layer > .nestable-list > .nestable-item-copy-new-item {
|
.nestable-drag-layer > .nestable-list {
|
||||||
box-shadow: var(--elevation-card-hover) !important;
|
box-shadow: var(--elevation-flyout) !important;
|
||||||
border-radius: 6 !important;
|
border-radius: 6px !important;
|
||||||
overflow: hidden !important;
|
overflow: hidden !important;
|
||||||
|
opacity: 0.8 !important;
|
||||||
|
width: fit-content !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nestable-item,
|
||||||
|
.nestable-item-copy {
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nestable-drag-layer > .nestable-list > .nestable-item-copy > div {
|
||||||
|
border-bottom: 0px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nestable-list {
|
.nestable-list {
|
||||||
|
|||||||
File renamed without changes.
@@ -1,6 +1,8 @@
|
|||||||
import { AdminProductCategoryResponse } from "@medusajs/types"
|
import { AdminProductCategoryResponse } from "@medusajs/types"
|
||||||
import { TFunction } from "i18next"
|
import { TFunction } from "i18next"
|
||||||
|
|
||||||
|
import { CategoryTreeItem } from "./types"
|
||||||
|
|
||||||
export function getIsActiveProps(
|
export function getIsActiveProps(
|
||||||
isActive: boolean,
|
isActive: boolean,
|
||||||
t: TFunction
|
t: TFunction
|
||||||
@@ -69,3 +71,87 @@ export function getCategoryChildren(
|
|||||||
name: child.name,
|
name: child.name,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const insertCategoryTreeItem = (
|
||||||
|
categories: CategoryTreeItem[],
|
||||||
|
newItem: CategoryTreeItem
|
||||||
|
): CategoryTreeItem[] => {
|
||||||
|
const seen = new Set<string>()
|
||||||
|
|
||||||
|
const remove = (
|
||||||
|
items: CategoryTreeItem[],
|
||||||
|
id: string
|
||||||
|
): CategoryTreeItem[] => {
|
||||||
|
const stack = [...items]
|
||||||
|
const result: CategoryTreeItem[] = []
|
||||||
|
|
||||||
|
while (stack.length > 0) {
|
||||||
|
const item = stack.pop()!
|
||||||
|
if (item.id !== id) {
|
||||||
|
if (item.category_children) {
|
||||||
|
item.category_children = remove(item.category_children, id)
|
||||||
|
}
|
||||||
|
result.push(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
const insert = (items: CategoryTreeItem[]): CategoryTreeItem[] => {
|
||||||
|
const stack = [...items]
|
||||||
|
|
||||||
|
while (stack.length > 0) {
|
||||||
|
const item = stack.pop()!
|
||||||
|
if (seen.has(item.id)) {
|
||||||
|
continue // Prevent revisiting the same node
|
||||||
|
}
|
||||||
|
seen.add(item.id)
|
||||||
|
|
||||||
|
if (item.id === newItem.parent_category_id) {
|
||||||
|
if (!item.category_children) {
|
||||||
|
item.category_children = []
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newItem.rank === null) {
|
||||||
|
item.category_children.push(newItem)
|
||||||
|
} else {
|
||||||
|
item.category_children.splice(newItem.rank, 0, newItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
item.category_children.forEach((child, index) => {
|
||||||
|
child.rank = index
|
||||||
|
})
|
||||||
|
|
||||||
|
item.category_children.sort((a, b) => (a.rank ?? 0) - (b.rank ?? 0))
|
||||||
|
return categories
|
||||||
|
}
|
||||||
|
if (item.category_children) {
|
||||||
|
stack.push(...item.category_children)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return items
|
||||||
|
}
|
||||||
|
|
||||||
|
categories = remove(categories, newItem.id)
|
||||||
|
|
||||||
|
if (newItem.parent_category_id === null && newItem.rank === null) {
|
||||||
|
categories.unshift(newItem)
|
||||||
|
|
||||||
|
categories.forEach((child, index) => {
|
||||||
|
child.rank = index
|
||||||
|
})
|
||||||
|
} else if (newItem.parent_category_id === null && newItem.rank !== null) {
|
||||||
|
categories.splice(newItem.rank, 0, newItem)
|
||||||
|
|
||||||
|
categories.forEach((child, index) => {
|
||||||
|
child.rank = index
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
categories = insert(categories)
|
||||||
|
}
|
||||||
|
|
||||||
|
categories.sort((a, b) => (a.rank ?? 0) - (b.rank ?? 0))
|
||||||
|
|
||||||
|
return categories
|
||||||
|
}
|
||||||
Reference in new issue
Block a user