fix(dashboard): Replace react-nestable with new SortableTree component (#8599)
**What** - Removes `react-nestable` dependency in favour of our own solution based on `@dnd-kit/core` Resolves CC-217
This commit is contained in:
1 parent
66c39ef876
commit
8c784a8b30
15 files changed
+1235
-302
No files matched your search
@@ -58,7 +58,6 @@
|
|||||||
"react-hook-form": "7.49.1",
|
"react-hook-form": "7.49.1",
|
||||||
"react-i18next": "13.5.0",
|
"react-i18next": "13.5.0",
|
||||||
"react-jwt": "^1.2.0",
|
"react-jwt": "^1.2.0",
|
||||||
"react-nestable": "^3.0.2",
|
|
||||||
"react-router-dom": "6.20.1",
|
"react-router-dom": "6.20.1",
|
||||||
"zod": "3.22.4"
|
"zod": "3.22.4"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./sortable-tree"
|
||||||
+156
@@ -0,0 +1,156 @@
|
|||||||
|
import {
|
||||||
|
DroppableContainer,
|
||||||
|
KeyboardCode,
|
||||||
|
KeyboardCoordinateGetter,
|
||||||
|
closestCorners,
|
||||||
|
getFirstCollision,
|
||||||
|
} from "@dnd-kit/core"
|
||||||
|
|
||||||
|
import type { SensorContext } from "./types"
|
||||||
|
import { getProjection } from "./utils"
|
||||||
|
|
||||||
|
const directions: string[] = [
|
||||||
|
KeyboardCode.Down,
|
||||||
|
KeyboardCode.Right,
|
||||||
|
KeyboardCode.Up,
|
||||||
|
KeyboardCode.Left,
|
||||||
|
]
|
||||||
|
|
||||||
|
const horizontal: string[] = [KeyboardCode.Left, KeyboardCode.Right]
|
||||||
|
|
||||||
|
export const sortableTreeKeyboardCoordinates: (
|
||||||
|
context: SensorContext,
|
||||||
|
indentationWidth: number
|
||||||
|
) => KeyboardCoordinateGetter =
|
||||||
|
(context, indentationWidth) =>
|
||||||
|
(
|
||||||
|
event,
|
||||||
|
{
|
||||||
|
currentCoordinates,
|
||||||
|
context: {
|
||||||
|
active,
|
||||||
|
over,
|
||||||
|
collisionRect,
|
||||||
|
droppableRects,
|
||||||
|
droppableContainers,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
if (directions.includes(event.code)) {
|
||||||
|
if (!active || !collisionRect) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault()
|
||||||
|
|
||||||
|
const {
|
||||||
|
current: { items, offset },
|
||||||
|
} = context
|
||||||
|
|
||||||
|
if (horizontal.includes(event.code) && over?.id) {
|
||||||
|
const { depth, maxDepth, minDepth } = getProjection(
|
||||||
|
items,
|
||||||
|
active.id,
|
||||||
|
over.id,
|
||||||
|
offset,
|
||||||
|
indentationWidth
|
||||||
|
)
|
||||||
|
|
||||||
|
switch (event.code) {
|
||||||
|
case KeyboardCode.Left:
|
||||||
|
if (depth > minDepth) {
|
||||||
|
return {
|
||||||
|
...currentCoordinates,
|
||||||
|
x: currentCoordinates.x - indentationWidth,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case KeyboardCode.Right:
|
||||||
|
if (depth < maxDepth) {
|
||||||
|
return {
|
||||||
|
...currentCoordinates,
|
||||||
|
x: currentCoordinates.x + indentationWidth,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const containers: DroppableContainer[] = []
|
||||||
|
|
||||||
|
droppableContainers.forEach((container) => {
|
||||||
|
if (container?.disabled || container.id === over?.id) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const rect = droppableRects.get(container.id)
|
||||||
|
|
||||||
|
if (!rect) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (event.code) {
|
||||||
|
case KeyboardCode.Down:
|
||||||
|
if (collisionRect.top < rect.top) {
|
||||||
|
containers.push(container)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
case KeyboardCode.Up:
|
||||||
|
if (collisionRect.top > rect.top) {
|
||||||
|
containers.push(container)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const collisions = closestCorners({
|
||||||
|
active,
|
||||||
|
collisionRect,
|
||||||
|
pointerCoordinates: null,
|
||||||
|
droppableRects,
|
||||||
|
droppableContainers: containers,
|
||||||
|
})
|
||||||
|
let closestId = getFirstCollision(collisions, "id")
|
||||||
|
|
||||||
|
if (closestId === over?.id && collisions.length > 1) {
|
||||||
|
closestId = collisions[1].id
|
||||||
|
}
|
||||||
|
|
||||||
|
if (closestId && over?.id) {
|
||||||
|
const activeRect = droppableRects.get(active.id)
|
||||||
|
const newRect = droppableRects.get(closestId)
|
||||||
|
const newDroppable = droppableContainers.get(closestId)
|
||||||
|
|
||||||
|
if (activeRect && newRect && newDroppable) {
|
||||||
|
const newIndex = items.findIndex(({ id }) => id === closestId)
|
||||||
|
const newItem = items[newIndex]
|
||||||
|
const activeIndex = items.findIndex(({ id }) => id === active.id)
|
||||||
|
const activeItem = items[activeIndex]
|
||||||
|
|
||||||
|
if (newItem && activeItem) {
|
||||||
|
const { depth } = getProjection(
|
||||||
|
items,
|
||||||
|
active.id,
|
||||||
|
closestId,
|
||||||
|
(newItem.depth - activeItem.depth) * indentationWidth,
|
||||||
|
indentationWidth
|
||||||
|
)
|
||||||
|
const isBelow = newIndex > activeIndex
|
||||||
|
const modifier = isBelow ? 1 : -1
|
||||||
|
const offset = 0
|
||||||
|
|
||||||
|
const newCoordinates = {
|
||||||
|
x: newRect.left + depth * indentationWidth,
|
||||||
|
y: newRect.top + modifier * offset,
|
||||||
|
}
|
||||||
|
|
||||||
|
return newCoordinates
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
+62
@@ -0,0 +1,62 @@
|
|||||||
|
import type { UniqueIdentifier } from "@dnd-kit/core"
|
||||||
|
import { AnimateLayoutChanges, useSortable } from "@dnd-kit/sortable"
|
||||||
|
import { CSS } from "@dnd-kit/utilities"
|
||||||
|
import { CSSProperties } from "react"
|
||||||
|
|
||||||
|
import { TreeItem, TreeItemProps } from "./tree-item"
|
||||||
|
import { iOS } from "./utils"
|
||||||
|
|
||||||
|
interface SortableTreeItemProps extends TreeItemProps {
|
||||||
|
id: UniqueIdentifier
|
||||||
|
}
|
||||||
|
|
||||||
|
const animateLayoutChanges: AnimateLayoutChanges = ({
|
||||||
|
isSorting,
|
||||||
|
wasDragging,
|
||||||
|
}) => {
|
||||||
|
return isSorting || wasDragging ? false : true
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SortableTreeItem({
|
||||||
|
id,
|
||||||
|
depth,
|
||||||
|
disabled,
|
||||||
|
...props
|
||||||
|
}: SortableTreeItemProps) {
|
||||||
|
const {
|
||||||
|
attributes,
|
||||||
|
isDragging,
|
||||||
|
isSorting,
|
||||||
|
listeners,
|
||||||
|
setDraggableNodeRef,
|
||||||
|
setDroppableNodeRef,
|
||||||
|
transform,
|
||||||
|
transition,
|
||||||
|
} = useSortable({
|
||||||
|
id,
|
||||||
|
animateLayoutChanges,
|
||||||
|
disabled,
|
||||||
|
})
|
||||||
|
const style: CSSProperties = {
|
||||||
|
transform: CSS.Translate.toString(transform),
|
||||||
|
transition,
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TreeItem
|
||||||
|
ref={setDraggableNodeRef}
|
||||||
|
wrapperRef={setDroppableNodeRef}
|
||||||
|
style={style}
|
||||||
|
depth={depth}
|
||||||
|
ghost={isDragging}
|
||||||
|
disableSelection={iOS}
|
||||||
|
disableInteraction={isSorting}
|
||||||
|
disabled={disabled}
|
||||||
|
handleProps={{
|
||||||
|
listeners,
|
||||||
|
attributes,
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,379 @@
|
|||||||
|
import {
|
||||||
|
Announcements,
|
||||||
|
DndContext,
|
||||||
|
DragEndEvent,
|
||||||
|
DragMoveEvent,
|
||||||
|
DragOverEvent,
|
||||||
|
DragOverlay,
|
||||||
|
DragStartEvent,
|
||||||
|
DropAnimation,
|
||||||
|
KeyboardSensor,
|
||||||
|
MeasuringStrategy,
|
||||||
|
PointerSensor,
|
||||||
|
UniqueIdentifier,
|
||||||
|
closestCenter,
|
||||||
|
defaultDropAnimation,
|
||||||
|
useSensor,
|
||||||
|
useSensors,
|
||||||
|
} from "@dnd-kit/core"
|
||||||
|
import {
|
||||||
|
SortableContext,
|
||||||
|
arrayMove,
|
||||||
|
verticalListSortingStrategy,
|
||||||
|
} from "@dnd-kit/sortable"
|
||||||
|
import { CSS } from "@dnd-kit/utilities"
|
||||||
|
import { ReactNode, useEffect, useMemo, useRef, useState } from "react"
|
||||||
|
import { createPortal } from "react-dom"
|
||||||
|
|
||||||
|
import { sortableTreeKeyboardCoordinates } from "./keyboard-coordinates"
|
||||||
|
import { SortableTreeItem } from "./sortable-tree-item"
|
||||||
|
import type { FlattenedItem, SensorContext, TreeItem } from "./types"
|
||||||
|
import {
|
||||||
|
buildTree,
|
||||||
|
flattenTree,
|
||||||
|
getChildCount,
|
||||||
|
getProjection,
|
||||||
|
removeChildrenOf,
|
||||||
|
} from "./utils"
|
||||||
|
|
||||||
|
const measuring = {
|
||||||
|
droppable: {
|
||||||
|
strategy: MeasuringStrategy.Always,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const dropAnimationConfig: DropAnimation = {
|
||||||
|
keyframes({ transform }) {
|
||||||
|
return [
|
||||||
|
{ opacity: 1, transform: CSS.Transform.toString(transform.initial) },
|
||||||
|
{
|
||||||
|
opacity: 0,
|
||||||
|
transform: CSS.Transform.toString({
|
||||||
|
...transform.final,
|
||||||
|
x: transform.final.x + 5,
|
||||||
|
y: transform.final.y + 5,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
easing: "ease-out",
|
||||||
|
sideEffects({ active }) {
|
||||||
|
active.node.animate([{ opacity: 0 }, { opacity: 1 }], {
|
||||||
|
duration: defaultDropAnimation.duration,
|
||||||
|
easing: defaultDropAnimation.easing,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props<T extends TreeItem> {
|
||||||
|
collapsible?: boolean
|
||||||
|
childrenProp?: string
|
||||||
|
items: T[]
|
||||||
|
indentationWidth?: number
|
||||||
|
/**
|
||||||
|
* Enable drag for all items or provide a function to enable drag for specific items.
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
enableDrag?: boolean | ((item: T) => boolean)
|
||||||
|
onChange: (
|
||||||
|
updatedItem: {
|
||||||
|
id: UniqueIdentifier
|
||||||
|
parentId: UniqueIdentifier | null
|
||||||
|
index: number
|
||||||
|
},
|
||||||
|
items: T[]
|
||||||
|
) => void
|
||||||
|
renderValue: (item: T) => ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SortableTree<T extends TreeItem>({
|
||||||
|
collapsible = true,
|
||||||
|
childrenProp = "children", // "children" is the default children prop name
|
||||||
|
enableDrag = true,
|
||||||
|
items = [],
|
||||||
|
indentationWidth = 40,
|
||||||
|
onChange,
|
||||||
|
renderValue,
|
||||||
|
}: Props<T>) {
|
||||||
|
const [collapsedState, setCollapsedState] = useState<
|
||||||
|
Record<UniqueIdentifier, boolean>
|
||||||
|
>({})
|
||||||
|
|
||||||
|
const [activeId, setActiveId] = useState<UniqueIdentifier | null>(null)
|
||||||
|
const [overId, setOverId] = useState<UniqueIdentifier | null>(null)
|
||||||
|
const [offsetLeft, setOffsetLeft] = useState(0)
|
||||||
|
const [currentPosition, setCurrentPosition] = useState<{
|
||||||
|
parentId: UniqueIdentifier | null
|
||||||
|
overId: UniqueIdentifier
|
||||||
|
} | null>(null)
|
||||||
|
|
||||||
|
const flattenedItems = useMemo(() => {
|
||||||
|
const flattenedTree = flattenTree(items, childrenProp)
|
||||||
|
const collapsedItems = flattenedTree.reduce<UniqueIdentifier[]>(
|
||||||
|
(acc, item) => {
|
||||||
|
const { id } = item
|
||||||
|
const children = (item[childrenProp] || []) as FlattenedItem[]
|
||||||
|
const collapsed = collapsedState[id]
|
||||||
|
|
||||||
|
return collapsed && children.length ? [...acc, id] : acc
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
)
|
||||||
|
|
||||||
|
return removeChildrenOf(
|
||||||
|
flattenedTree,
|
||||||
|
activeId ? [activeId, ...collapsedItems] : collapsedItems,
|
||||||
|
childrenProp
|
||||||
|
)
|
||||||
|
}, [activeId, items, childrenProp, collapsedState])
|
||||||
|
|
||||||
|
const projected =
|
||||||
|
activeId && overId
|
||||||
|
? getProjection(
|
||||||
|
flattenedItems,
|
||||||
|
activeId,
|
||||||
|
overId,
|
||||||
|
offsetLeft,
|
||||||
|
indentationWidth
|
||||||
|
)
|
||||||
|
: null
|
||||||
|
|
||||||
|
const sensorContext: SensorContext = useRef({
|
||||||
|
items: flattenedItems,
|
||||||
|
offset: offsetLeft,
|
||||||
|
})
|
||||||
|
|
||||||
|
const [coordinateGetter] = useState(() =>
|
||||||
|
sortableTreeKeyboardCoordinates(sensorContext, indentationWidth)
|
||||||
|
)
|
||||||
|
|
||||||
|
const sensors = useSensors(
|
||||||
|
useSensor(PointerSensor),
|
||||||
|
useSensor(KeyboardSensor, {
|
||||||
|
coordinateGetter,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const sortedIds = useMemo(
|
||||||
|
() => flattenedItems.map(({ id }) => id),
|
||||||
|
[flattenedItems]
|
||||||
|
)
|
||||||
|
|
||||||
|
const activeItem = activeId
|
||||||
|
? flattenedItems.find(({ id }) => id === activeId)
|
||||||
|
: null
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
sensorContext.current = {
|
||||||
|
items: flattenedItems,
|
||||||
|
offset: offsetLeft,
|
||||||
|
}
|
||||||
|
}, [flattenedItems, offsetLeft])
|
||||||
|
|
||||||
|
function handleDragStart({ active: { id: activeId } }: DragStartEvent) {
|
||||||
|
setActiveId(activeId)
|
||||||
|
setOverId(activeId)
|
||||||
|
|
||||||
|
const activeItem = flattenedItems.find(({ id }) => id === activeId)
|
||||||
|
|
||||||
|
if (activeItem) {
|
||||||
|
setCurrentPosition({
|
||||||
|
parentId: activeItem.parentId,
|
||||||
|
overId: activeId,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
document.body.style.setProperty("cursor", "grabbing")
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDragMove({ delta }: DragMoveEvent) {
|
||||||
|
setOffsetLeft(delta.x)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDragOver({ over }: DragOverEvent) {
|
||||||
|
setOverId(over?.id ?? null)
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDragEnd({ active, over }: DragEndEvent) {
|
||||||
|
resetState()
|
||||||
|
|
||||||
|
if (projected && over) {
|
||||||
|
const { depth, parentId } = projected
|
||||||
|
const clonedItems: FlattenedItem[] = JSON.parse(
|
||||||
|
JSON.stringify(flattenTree(items, childrenProp))
|
||||||
|
)
|
||||||
|
const overIndex = clonedItems.findIndex(({ id }) => id === over.id)
|
||||||
|
|
||||||
|
const activeIndex = clonedItems.findIndex(({ id }) => id === active.id)
|
||||||
|
const activeTreeItem = clonedItems[activeIndex]
|
||||||
|
|
||||||
|
clonedItems[activeIndex] = { ...activeTreeItem, depth, parentId }
|
||||||
|
|
||||||
|
const sortedItems = arrayMove(clonedItems, activeIndex, overIndex)
|
||||||
|
|
||||||
|
const { items: newItems, update } = buildTree<T>(
|
||||||
|
sortedItems,
|
||||||
|
overIndex,
|
||||||
|
childrenProp
|
||||||
|
)
|
||||||
|
|
||||||
|
onChange(update, newItems)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDragCancel() {
|
||||||
|
resetState()
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetState() {
|
||||||
|
setOverId(null)
|
||||||
|
setActiveId(null)
|
||||||
|
setOffsetLeft(0)
|
||||||
|
setCurrentPosition(null)
|
||||||
|
|
||||||
|
document.body.style.setProperty("cursor", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleCollapse(id: UniqueIdentifier) {
|
||||||
|
setCollapsedState((state) => ({
|
||||||
|
...state,
|
||||||
|
[id]: state[id] ? false : true,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMovementAnnouncement(
|
||||||
|
eventName: string,
|
||||||
|
activeId: UniqueIdentifier,
|
||||||
|
overId?: UniqueIdentifier
|
||||||
|
) {
|
||||||
|
if (overId && projected) {
|
||||||
|
if (eventName !== "onDragEnd") {
|
||||||
|
if (
|
||||||
|
currentPosition &&
|
||||||
|
projected.parentId === currentPosition.parentId &&
|
||||||
|
overId === currentPosition.overId
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
setCurrentPosition({
|
||||||
|
parentId: projected.parentId,
|
||||||
|
overId,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const clonedItems: FlattenedItem[] = JSON.parse(
|
||||||
|
JSON.stringify(flattenTree(items, childrenProp))
|
||||||
|
)
|
||||||
|
const overIndex = clonedItems.findIndex(({ id }) => id === overId)
|
||||||
|
const activeIndex = clonedItems.findIndex(({ id }) => id === activeId)
|
||||||
|
const sortedItems = arrayMove(clonedItems, activeIndex, overIndex)
|
||||||
|
|
||||||
|
const previousItem = sortedItems[overIndex - 1]
|
||||||
|
|
||||||
|
let announcement
|
||||||
|
const movedVerb = eventName === "onDragEnd" ? "dropped" : "moved"
|
||||||
|
const nestedVerb = eventName === "onDragEnd" ? "dropped" : "nested"
|
||||||
|
|
||||||
|
if (!previousItem) {
|
||||||
|
const nextItem = sortedItems[overIndex + 1]
|
||||||
|
announcement = `${activeId} was ${movedVerb} before ${nextItem.id}.`
|
||||||
|
} else {
|
||||||
|
if (projected.depth > previousItem.depth) {
|
||||||
|
announcement = `${activeId} was ${nestedVerb} under ${previousItem.id}.`
|
||||||
|
} else {
|
||||||
|
let previousSibling: FlattenedItem | undefined = previousItem
|
||||||
|
while (previousSibling && projected.depth < previousSibling.depth) {
|
||||||
|
const parentId: UniqueIdentifier | null = previousSibling.parentId
|
||||||
|
previousSibling = sortedItems.find(({ id }) => id === parentId)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (previousSibling) {
|
||||||
|
announcement = `${activeId} was ${movedVerb} after ${previousSibling.id}.`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return announcement
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const announcements: Announcements = {
|
||||||
|
onDragStart({ active }) {
|
||||||
|
return `Picked up ${active.id}.`
|
||||||
|
},
|
||||||
|
onDragMove({ active, over }) {
|
||||||
|
return getMovementAnnouncement("onDragMove", active.id, over?.id)
|
||||||
|
},
|
||||||
|
onDragOver({ active, over }) {
|
||||||
|
return getMovementAnnouncement("onDragOver", active.id, over?.id)
|
||||||
|
},
|
||||||
|
onDragEnd({ active, over }) {
|
||||||
|
return getMovementAnnouncement("onDragEnd", active.id, over?.id)
|
||||||
|
},
|
||||||
|
onDragCancel({ active }) {
|
||||||
|
return `Moving was cancelled. ${active.id} was dropped in its original position.`
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DndContext
|
||||||
|
accessibility={{ announcements }}
|
||||||
|
sensors={sensors}
|
||||||
|
collisionDetection={closestCenter}
|
||||||
|
measuring={measuring}
|
||||||
|
onDragStart={handleDragStart}
|
||||||
|
onDragMove={handleDragMove}
|
||||||
|
onDragOver={handleDragOver}
|
||||||
|
onDragEnd={handleDragEnd}
|
||||||
|
onDragCancel={handleDragCancel}
|
||||||
|
>
|
||||||
|
<SortableContext items={sortedIds} strategy={verticalListSortingStrategy}>
|
||||||
|
{flattenedItems.map((item) => {
|
||||||
|
const { id, depth } = item
|
||||||
|
const children = (item[childrenProp] || []) as FlattenedItem[]
|
||||||
|
|
||||||
|
const disabled =
|
||||||
|
typeof enableDrag === "function"
|
||||||
|
? !enableDrag(item as unknown as T)
|
||||||
|
: !enableDrag
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SortableTreeItem
|
||||||
|
key={id}
|
||||||
|
id={id}
|
||||||
|
value={renderValue(item as unknown as T)}
|
||||||
|
disabled={disabled}
|
||||||
|
depth={id === activeId && projected ? projected.depth : depth}
|
||||||
|
indentationWidth={indentationWidth}
|
||||||
|
collapsed={Boolean(collapsedState[id] && children.length)}
|
||||||
|
childCount={children.length}
|
||||||
|
onCollapse={
|
||||||
|
collapsible && children.length
|
||||||
|
? () => handleCollapse(id)
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
{createPortal(
|
||||||
|
<DragOverlay dropAnimation={dropAnimationConfig}>
|
||||||
|
{activeId && activeItem ? (
|
||||||
|
<SortableTreeItem
|
||||||
|
id={activeId}
|
||||||
|
depth={activeItem.depth}
|
||||||
|
clone
|
||||||
|
childCount={getChildCount(items, activeId, childrenProp) + 1}
|
||||||
|
value={renderValue(activeItem as unknown as T)}
|
||||||
|
indentationWidth={0}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</DragOverlay>,
|
||||||
|
document.body
|
||||||
|
)}
|
||||||
|
</SortableContext>
|
||||||
|
</DndContext>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,207 @@
|
|||||||
|
import React, { forwardRef, HTMLAttributes, ReactNode } from "react"
|
||||||
|
|
||||||
|
import {
|
||||||
|
DotsSix,
|
||||||
|
FolderIllustration,
|
||||||
|
FolderOpenIllustration,
|
||||||
|
Swatch,
|
||||||
|
TriangleRightMini,
|
||||||
|
} from "@medusajs/icons"
|
||||||
|
import { Badge, clx, IconButton } from "@medusajs/ui"
|
||||||
|
import { HandleProps } from "./types"
|
||||||
|
|
||||||
|
export interface TreeItemProps
|
||||||
|
extends Omit<HTMLAttributes<HTMLLIElement>, "id"> {
|
||||||
|
childCount?: number
|
||||||
|
clone?: boolean
|
||||||
|
collapsed?: boolean
|
||||||
|
depth: number
|
||||||
|
disableInteraction?: boolean
|
||||||
|
disableSelection?: boolean
|
||||||
|
ghost?: boolean
|
||||||
|
handleProps?: HandleProps
|
||||||
|
indentationWidth: number
|
||||||
|
value: ReactNode
|
||||||
|
disabled?: boolean
|
||||||
|
onCollapse?(): void
|
||||||
|
wrapperRef?(node: HTMLLIElement): void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TreeItem = forwardRef<HTMLDivElement, TreeItemProps>(
|
||||||
|
(
|
||||||
|
{
|
||||||
|
childCount,
|
||||||
|
clone,
|
||||||
|
depth,
|
||||||
|
disableSelection,
|
||||||
|
disableInteraction,
|
||||||
|
ghost,
|
||||||
|
handleProps,
|
||||||
|
indentationWidth,
|
||||||
|
collapsed,
|
||||||
|
onCollapse,
|
||||||
|
style,
|
||||||
|
value,
|
||||||
|
disabled,
|
||||||
|
wrapperRef,
|
||||||
|
...props
|
||||||
|
},
|
||||||
|
ref
|
||||||
|
) => {
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
ref={wrapperRef}
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
paddingLeft: `${indentationWidth * depth}px`,
|
||||||
|
} as React.CSSProperties
|
||||||
|
}
|
||||||
|
className={clx("-mb-px list-none", {
|
||||||
|
"pointer-events-none": disableInteraction,
|
||||||
|
"select-none": disableSelection,
|
||||||
|
"[&:first-of-type>div]:border-t-0": !clone,
|
||||||
|
})}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
style={style}
|
||||||
|
className={clx(
|
||||||
|
"bg-ui-bg-base transition-fg relative flex items-center gap-x-3 border-y px-6 py-2.5",
|
||||||
|
{
|
||||||
|
"border-l": depth > 0,
|
||||||
|
"shadow-elevation-flyout bg-ui-bg-base w-fit rounded-lg border-none pr-6 opacity-80":
|
||||||
|
clone,
|
||||||
|
"bg-ui-bg-base-hover z-[1] opacity-50": ghost,
|
||||||
|
"bg-ui-bg-disabled": disabled,
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Handle {...handleProps} disabled={disabled} />
|
||||||
|
<Collapse
|
||||||
|
collapsed={collapsed}
|
||||||
|
onCollapse={onCollapse}
|
||||||
|
clone={clone}
|
||||||
|
/>
|
||||||
|
<Icon
|
||||||
|
childrenCount={childCount}
|
||||||
|
collapsed={collapsed}
|
||||||
|
clone={clone}
|
||||||
|
/>
|
||||||
|
<Value value={value} />
|
||||||
|
<ChildrenCount clone={clone} childrenCount={childCount} />
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
TreeItem.displayName = "TreeItem"
|
||||||
|
|
||||||
|
const Handle = ({
|
||||||
|
listeners,
|
||||||
|
attributes,
|
||||||
|
disabled,
|
||||||
|
}: HandleProps & { disabled?: boolean }) => {
|
||||||
|
return (
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
variant="transparent"
|
||||||
|
type="button"
|
||||||
|
className={clx("cursor-grab", { "cursor-not-allowed": disabled })}
|
||||||
|
disabled={disabled}
|
||||||
|
{...attributes}
|
||||||
|
{...listeners}
|
||||||
|
>
|
||||||
|
<DotsSix />
|
||||||
|
</IconButton>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type IconProps = {
|
||||||
|
childrenCount?: number
|
||||||
|
collapsed?: boolean
|
||||||
|
clone?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const Icon = ({ childrenCount, collapsed, clone }: IconProps) => {
|
||||||
|
const isBranch = clone ? childrenCount && childrenCount > 1 : childrenCount
|
||||||
|
const isOpen = clone ? false : !collapsed
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex size-7 items-center justify-center">
|
||||||
|
{isBranch ? (
|
||||||
|
isOpen ? (
|
||||||
|
<FolderOpenIllustration />
|
||||||
|
) : (
|
||||||
|
<FolderIllustration />
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<Swatch className="text-ui-fg-muted" />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type CollapseProps = {
|
||||||
|
collapsed?: boolean
|
||||||
|
onCollapse?: () => void
|
||||||
|
clone?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const Collapse = ({ collapsed, onCollapse, clone }: CollapseProps) => {
|
||||||
|
if (clone) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!onCollapse) {
|
||||||
|
return <div className="size-7" role="presentation" />
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
variant="transparent"
|
||||||
|
onClick={onCollapse}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<TriangleRightMini
|
||||||
|
className={clx("text-ui-fg-subtle transition-transform", {
|
||||||
|
"rotate-90": !collapsed,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ValueProps = {
|
||||||
|
value: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
const Value = ({ value }: ValueProps) => {
|
||||||
|
return (
|
||||||
|
<div className="txt-compact-small text-ui-fg-subtle flex-grow truncate">
|
||||||
|
{value}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChildrenCountProps = {
|
||||||
|
clone?: boolean
|
||||||
|
childrenCount?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const ChildrenCount = ({ clone, childrenCount }: ChildrenCountProps) => {
|
||||||
|
if (!clone || !childrenCount) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (clone && childrenCount <= 1) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Badge size="2xsmall" color="blue" className="absolute -right-2 -top-2">
|
||||||
|
{childrenCount}
|
||||||
|
</Badge>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import type { DraggableAttributes, UniqueIdentifier } from "@dnd-kit/core"
|
||||||
|
import { SyntheticListenerMap } from "@dnd-kit/core/dist/hooks/utilities"
|
||||||
|
import type { MutableRefObject } from "react"
|
||||||
|
|
||||||
|
export interface TreeItem extends Record<string, unknown> {
|
||||||
|
id: UniqueIdentifier
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FlattenedItem extends TreeItem {
|
||||||
|
parentId: UniqueIdentifier | null
|
||||||
|
depth: number
|
||||||
|
index: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SensorContext = MutableRefObject<{
|
||||||
|
items: FlattenedItem[]
|
||||||
|
offset: number
|
||||||
|
}>
|
||||||
|
|
||||||
|
export type HandleProps = {
|
||||||
|
attributes?: DraggableAttributes | undefined
|
||||||
|
listeners?: SyntheticListenerMap | undefined
|
||||||
|
}
|
||||||
@@ -0,0 +1,299 @@
|
|||||||
|
import type { UniqueIdentifier } from "@dnd-kit/core"
|
||||||
|
import { arrayMove } from "@dnd-kit/sortable"
|
||||||
|
|
||||||
|
import type { FlattenedItem, TreeItem } from "./types"
|
||||||
|
|
||||||
|
export const iOS = /iPad|iPhone|iPod/.test(navigator.platform)
|
||||||
|
|
||||||
|
function getDragDepth(offset: number, indentationWidth: number) {
|
||||||
|
return Math.round(offset / indentationWidth)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getProjection(
|
||||||
|
items: FlattenedItem[],
|
||||||
|
activeId: UniqueIdentifier,
|
||||||
|
overId: UniqueIdentifier,
|
||||||
|
dragOffset: number,
|
||||||
|
indentationWidth: number
|
||||||
|
) {
|
||||||
|
const overItemIndex = items.findIndex(({ id }) => id === overId)
|
||||||
|
const activeItemIndex = items.findIndex(({ id }) => id === activeId)
|
||||||
|
const activeItem = items[activeItemIndex]
|
||||||
|
const newItems = arrayMove(items, activeItemIndex, overItemIndex)
|
||||||
|
const previousItem = newItems[overItemIndex - 1]
|
||||||
|
const nextItem = newItems[overItemIndex + 1]
|
||||||
|
const dragDepth = getDragDepth(dragOffset, indentationWidth)
|
||||||
|
const projectedDepth = activeItem.depth + dragDepth
|
||||||
|
const maxDepth = getMaxDepth({
|
||||||
|
previousItem,
|
||||||
|
})
|
||||||
|
const minDepth = getMinDepth({ nextItem })
|
||||||
|
let depth = projectedDepth
|
||||||
|
|
||||||
|
if (projectedDepth >= maxDepth) {
|
||||||
|
depth = maxDepth
|
||||||
|
} else if (projectedDepth < minDepth) {
|
||||||
|
depth = minDepth
|
||||||
|
}
|
||||||
|
|
||||||
|
return { depth, maxDepth, minDepth, parentId: getParentId() }
|
||||||
|
|
||||||
|
function getParentId() {
|
||||||
|
if (depth === 0 || !previousItem) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
if (depth === previousItem.depth) {
|
||||||
|
return previousItem.parentId
|
||||||
|
}
|
||||||
|
|
||||||
|
if (depth > previousItem.depth) {
|
||||||
|
return previousItem.id
|
||||||
|
}
|
||||||
|
|
||||||
|
const newParent = newItems
|
||||||
|
.slice(0, overItemIndex)
|
||||||
|
.reverse()
|
||||||
|
.find((item) => item.depth === depth)?.parentId
|
||||||
|
|
||||||
|
return newParent ?? null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMaxDepth({ previousItem }: { previousItem: FlattenedItem }) {
|
||||||
|
if (previousItem) {
|
||||||
|
return previousItem.depth + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMinDepth({ nextItem }: { nextItem: FlattenedItem }) {
|
||||||
|
if (nextItem) {
|
||||||
|
return nextItem.depth
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function flatten<T extends TreeItem>(
|
||||||
|
items: T[],
|
||||||
|
parentId: UniqueIdentifier | null = null,
|
||||||
|
depth = 0,
|
||||||
|
childrenProp: string
|
||||||
|
): FlattenedItem[] {
|
||||||
|
return items.reduce<FlattenedItem[]>((acc, item, index) => {
|
||||||
|
const children = (item[childrenProp] || []) as T[]
|
||||||
|
|
||||||
|
return [
|
||||||
|
...acc,
|
||||||
|
{ ...item, parentId, depth, index },
|
||||||
|
...flatten(children, item.id, depth + 1, childrenProp),
|
||||||
|
]
|
||||||
|
}, [])
|
||||||
|
}
|
||||||
|
|
||||||
|
export function flattenTree<T extends TreeItem>(
|
||||||
|
items: T[],
|
||||||
|
childrenProp: string
|
||||||
|
): FlattenedItem[] {
|
||||||
|
return flatten(items, undefined, undefined, childrenProp)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ItemUpdate = {
|
||||||
|
id: UniqueIdentifier
|
||||||
|
parentId: UniqueIdentifier | null
|
||||||
|
index: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildTree<T extends TreeItem>(
|
||||||
|
flattenedItems: FlattenedItem[],
|
||||||
|
newIndex: number,
|
||||||
|
childrenProp: string
|
||||||
|
): { items: T[]; update: ItemUpdate } {
|
||||||
|
const root = { id: "root", [childrenProp]: [] } as T
|
||||||
|
const nodes: Record<string, T> = { [root.id]: root }
|
||||||
|
const items = flattenedItems.map((item) => ({ ...item, [childrenProp]: [] }))
|
||||||
|
|
||||||
|
let update: {
|
||||||
|
id: UniqueIdentifier | null
|
||||||
|
parentId: UniqueIdentifier | null
|
||||||
|
index: number
|
||||||
|
} = {
|
||||||
|
id: null,
|
||||||
|
parentId: null,
|
||||||
|
index: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
items.forEach((item, index) => {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
index: _index,
|
||||||
|
depth: _depth,
|
||||||
|
parentId: _parentId,
|
||||||
|
...rest
|
||||||
|
} = item
|
||||||
|
const children = (item[childrenProp] || []) as T[]
|
||||||
|
|
||||||
|
const parentId = _parentId ?? root.id
|
||||||
|
const parent = nodes[parentId] ?? findItem(items, parentId)
|
||||||
|
|
||||||
|
nodes[id] = { id, [childrenProp]: children } as T
|
||||||
|
;(parent[childrenProp] as T[]).push({
|
||||||
|
id,
|
||||||
|
...rest,
|
||||||
|
[childrenProp]: children,
|
||||||
|
} as T)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the information for them item that was moved to the `newIndex`.
|
||||||
|
*/
|
||||||
|
if (index === newIndex) {
|
||||||
|
const parentChildren = parent[childrenProp] as FlattenedItem[]
|
||||||
|
|
||||||
|
update = {
|
||||||
|
id: item.id,
|
||||||
|
parentId: parent.id === "root" ? null : parent.id,
|
||||||
|
index: parentChildren.length - 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!update.id) {
|
||||||
|
throw new Error("Could not find item")
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
items: root[childrenProp] as T[],
|
||||||
|
update: update as ItemUpdate,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function findItem<T extends TreeItem>(
|
||||||
|
items: T[],
|
||||||
|
itemId: UniqueIdentifier
|
||||||
|
) {
|
||||||
|
return items.find(({ id }) => id === itemId)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function findItemDeep<T extends TreeItem>(
|
||||||
|
items: T[],
|
||||||
|
itemId: UniqueIdentifier,
|
||||||
|
childrenProp: string
|
||||||
|
): TreeItem | undefined {
|
||||||
|
for (const item of items) {
|
||||||
|
const { id } = item
|
||||||
|
const children = (item[childrenProp] || []) as T[]
|
||||||
|
|
||||||
|
if (id === itemId) {
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
|
if (children.length) {
|
||||||
|
const child = findItemDeep(children, itemId, childrenProp)
|
||||||
|
|
||||||
|
if (child) {
|
||||||
|
return child
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setProperty<TItem extends TreeItem, T extends keyof TItem>(
|
||||||
|
items: TItem[],
|
||||||
|
id: UniqueIdentifier,
|
||||||
|
property: T,
|
||||||
|
childrenProp: keyof TItem, // Make childrenProp a key of TItem
|
||||||
|
setter: (value: TItem[T]) => TItem[T]
|
||||||
|
): TItem[] {
|
||||||
|
return items.map((item) => {
|
||||||
|
if (item.id === id) {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
[property]: setter(item[property]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const children = item[childrenProp] as TItem[] | undefined
|
||||||
|
|
||||||
|
if (children && children.length) {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
[childrenProp]: setProperty(
|
||||||
|
children,
|
||||||
|
id,
|
||||||
|
property,
|
||||||
|
childrenProp,
|
||||||
|
setter
|
||||||
|
),
|
||||||
|
} as TItem // Explicitly cast to TItem
|
||||||
|
}
|
||||||
|
|
||||||
|
return item
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function countChildren<T extends TreeItem>(
|
||||||
|
items: T[],
|
||||||
|
count = 0,
|
||||||
|
childrenProp: string
|
||||||
|
): number {
|
||||||
|
return items.reduce((acc, item) => {
|
||||||
|
const children = (item[childrenProp] || []) as T[]
|
||||||
|
|
||||||
|
if (children.length) {
|
||||||
|
return countChildren(children, acc + 1, childrenProp)
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc + 1
|
||||||
|
}, count)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getChildCount<T extends TreeItem>(
|
||||||
|
items: T[],
|
||||||
|
id: UniqueIdentifier,
|
||||||
|
childrenProp: string
|
||||||
|
) {
|
||||||
|
const item = findItemDeep(items, id, childrenProp)
|
||||||
|
|
||||||
|
const children = (item?.[childrenProp] || []) as T[]
|
||||||
|
|
||||||
|
return item ? countChildren(children, 0, childrenProp) : 0
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeChildrenOf(
|
||||||
|
items: FlattenedItem[],
|
||||||
|
ids: UniqueIdentifier[],
|
||||||
|
childrenProp: string
|
||||||
|
) {
|
||||||
|
const excludeParentIds = [...ids]
|
||||||
|
|
||||||
|
return items.filter((item) => {
|
||||||
|
if (item.parentId && excludeParentIds.includes(item.parentId)) {
|
||||||
|
const children = (item[childrenProp] || []) as FlattenedItem[]
|
||||||
|
|
||||||
|
if (children.length) {
|
||||||
|
excludeParentIds.push(item.id)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function listItemsWithChildren<T extends TreeItem>(
|
||||||
|
items: T[],
|
||||||
|
childrenProp: string
|
||||||
|
): T[] {
|
||||||
|
return items.map((item) => {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
[childrenProp]: item[childrenProp]
|
||||||
|
? listItemsWithChildren(item[childrenProp] as TreeItem[], childrenProp)
|
||||||
|
: [],
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
-1
@@ -732,7 +732,6 @@ export const DataGridRoot = <
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (e.key === "z" && (e.metaKey || e.ctrlKey)) {
|
if (e.key === "z" && (e.metaKey || e.ctrlKey)) {
|
||||||
console.log("Undo/Redo")
|
|
||||||
handleUndo(e)
|
handleUndo(e)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
+27
-12
@@ -121,11 +121,15 @@ export const CreateCategoryForm = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<RouteFocusModal.Form form={form}>
|
<RouteFocusModal.Form form={form}>
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
className="flex size-full flex-col overflow-hidden"
|
||||||
|
>
|
||||||
<ProgressTabs
|
<ProgressTabs
|
||||||
value={activeTab}
|
value={activeTab}
|
||||||
onValueChange={(tab) => handleTabChange(tab as Tab)}
|
onValueChange={(tab) => handleTabChange(tab as Tab)}
|
||||||
|
className="flex size-full flex-col"
|
||||||
>
|
>
|
||||||
<form onSubmit={handleSubmit}>
|
|
||||||
<RouteFocusModal.Header>
|
<RouteFocusModal.Header>
|
||||||
<div className="flex w-full items-center justify-between">
|
<div className="flex w-full items-center justify-between">
|
||||||
<div className="-my-2 w-fit border-l">
|
<div className="-my-2 w-fit border-l">
|
||||||
@@ -133,17 +137,37 @@ export const CreateCategoryForm = ({
|
|||||||
<ProgressTabs.Trigger
|
<ProgressTabs.Trigger
|
||||||
value={Tab.DETAILS}
|
value={Tab.DETAILS}
|
||||||
status={detailsStatus}
|
status={detailsStatus}
|
||||||
|
className="w-full min-w-0 max-w-[200px] overflow-hidden"
|
||||||
>
|
>
|
||||||
|
<span className="truncate">
|
||||||
{t("categories.create.tabs.details")}
|
{t("categories.create.tabs.details")}
|
||||||
|
</span>
|
||||||
</ProgressTabs.Trigger>
|
</ProgressTabs.Trigger>
|
||||||
<ProgressTabs.Trigger
|
<ProgressTabs.Trigger
|
||||||
value={Tab.ORGANIZE}
|
value={Tab.ORGANIZE}
|
||||||
status={nestingStatus}
|
status={nestingStatus}
|
||||||
|
className="w-full min-w-0 max-w-[200px] overflow-hidden"
|
||||||
>
|
>
|
||||||
|
<span className="truncate">
|
||||||
{t("categories.create.tabs.organize")}
|
{t("categories.create.tabs.organize")}
|
||||||
|
</span>
|
||||||
</ProgressTabs.Trigger>
|
</ProgressTabs.Trigger>
|
||||||
</ProgressTabs.List>
|
</ProgressTabs.List>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</RouteFocusModal.Header>
|
||||||
|
<RouteFocusModal.Body className="flex size-full flex-col overflow-auto">
|
||||||
|
<ProgressTabs.Content value={Tab.DETAILS}>
|
||||||
|
<CreateCategoryDetails form={form} />
|
||||||
|
</ProgressTabs.Content>
|
||||||
|
<ProgressTabs.Content
|
||||||
|
value={Tab.ORGANIZE}
|
||||||
|
className="bg-ui-bg-subtle flex-1"
|
||||||
|
>
|
||||||
|
<CreateCategoryNesting form={form} shouldFreeze={shouldFreeze} />
|
||||||
|
</ProgressTabs.Content>
|
||||||
|
</RouteFocusModal.Body>
|
||||||
|
<RouteFocusModal.Footer>
|
||||||
<div className="flex items-center justify-end gap-x-2">
|
<div className="flex items-center justify-end gap-x-2">
|
||||||
<RouteFocusModal.Close asChild>
|
<RouteFocusModal.Close asChild>
|
||||||
<Button size="small" variant="secondary">
|
<Button size="small" variant="secondary">
|
||||||
@@ -172,18 +196,9 @@ export const CreateCategoryForm = ({
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</RouteFocusModal.Footer>
|
||||||
</RouteFocusModal.Header>
|
|
||||||
<RouteFocusModal.Body>
|
|
||||||
<ProgressTabs.Content value={Tab.DETAILS}>
|
|
||||||
<CreateCategoryDetails form={form} />
|
|
||||||
</ProgressTabs.Content>
|
|
||||||
<ProgressTabs.Content value={Tab.ORGANIZE}>
|
|
||||||
<CreateCategoryNesting form={form} shouldFreeze={shouldFreeze} />
|
|
||||||
</ProgressTabs.Content>
|
|
||||||
</RouteFocusModal.Body>
|
|
||||||
</form>
|
|
||||||
</ProgressTabs>
|
</ProgressTabs>
|
||||||
|
</form>
|
||||||
</RouteFocusModal.Form>
|
</RouteFocusModal.Form>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
+36
-15
@@ -1,5 +1,9 @@
|
|||||||
|
import { UniqueIdentifier } from "@dnd-kit/core"
|
||||||
|
import { Badge } from "@medusajs/ui"
|
||||||
import { useMemo, useState } from "react"
|
import { useMemo, useState } from "react"
|
||||||
import { UseFormReturn, useWatch } from "react-hook-form"
|
import { UseFormReturn, useWatch } from "react-hook-form"
|
||||||
|
|
||||||
|
import { useTranslation } from "react-i18next"
|
||||||
import { useProductCategories } from "../../../../../hooks/api/categories"
|
import { useProductCategories } from "../../../../../hooks/api/categories"
|
||||||
import { CategoryTree } from "../../../common/components/category-tree"
|
import { CategoryTree } from "../../../common/components/category-tree"
|
||||||
import { CategoryTreeItem } from "../../../common/types"
|
import { CategoryTreeItem } from "../../../common/types"
|
||||||
@@ -17,20 +21,16 @@ export const CreateCategoryNesting = ({
|
|||||||
form,
|
form,
|
||||||
shouldFreeze,
|
shouldFreeze,
|
||||||
}: CreateCategoryNestingProps) => {
|
}: CreateCategoryNestingProps) => {
|
||||||
|
const { t } = useTranslation()
|
||||||
const [snapshot, setSnapshot] = useState<CategoryTreeItem[]>([])
|
const [snapshot, setSnapshot] = useState<CategoryTreeItem[]>([])
|
||||||
|
|
||||||
const { product_categories, isPending, isError, error } =
|
const { product_categories, isPending, isError, error } =
|
||||||
useProductCategories(
|
useProductCategories({
|
||||||
{
|
|
||||||
parent_category_id: "null",
|
parent_category_id: "null",
|
||||||
limit: 9999,
|
limit: 9999,
|
||||||
fields: "id,name,parent_category_id,rank,category_children",
|
fields: "id,name,parent_category_id,rank,category_children,rank",
|
||||||
include_descendants_tree: true,
|
include_descendants_tree: true,
|
||||||
},
|
})
|
||||||
{
|
|
||||||
refetchInterval: Infinity, // Once the data is loaded we don't need to refetch
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
const parentCategoryId = useWatch({
|
const parentCategoryId = useWatch({
|
||||||
control: form.control,
|
control: form.control,
|
||||||
@@ -60,15 +60,22 @@ export const CreateCategoryNesting = ({
|
|||||||
}, [product_categories, watchedName, parentCategoryId, watchedRank])
|
}, [product_categories, watchedName, parentCategoryId, watchedRank])
|
||||||
|
|
||||||
const handleChange = (
|
const handleChange = (
|
||||||
{ parent_category_id, rank }: CategoryTreeItem,
|
{
|
||||||
|
parentId,
|
||||||
|
index,
|
||||||
|
}: {
|
||||||
|
id: UniqueIdentifier
|
||||||
|
parentId: UniqueIdentifier | null
|
||||||
|
index: number
|
||||||
|
},
|
||||||
list: CategoryTreeItem[]
|
list: CategoryTreeItem[]
|
||||||
) => {
|
) => {
|
||||||
form.setValue("parent_category_id", parent_category_id, {
|
form.setValue("parent_category_id", parentId as string | null, {
|
||||||
shouldDirty: true,
|
shouldDirty: true,
|
||||||
shouldTouch: true,
|
shouldTouch: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
form.setValue("rank", rank, {
|
form.setValue("rank", index, {
|
||||||
shouldDirty: true,
|
shouldDirty: true,
|
||||||
shouldTouch: true,
|
shouldTouch: true,
|
||||||
})
|
})
|
||||||
@@ -80,14 +87,28 @@ export const CreateCategoryNesting = ({
|
|||||||
throw error
|
throw error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ready = !isPending && !!product_categories
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CategoryTree
|
<CategoryTree
|
||||||
// When we submit the form we want to freeze the rendered tree to prevent flickering during the exit animation
|
|
||||||
value={shouldFreeze ? snapshot : value}
|
value={shouldFreeze ? snapshot : value}
|
||||||
|
enableDrag={(item) => item.id === ID}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
enableDrag={(i) => i.id === ID}
|
renderValue={(item) => {
|
||||||
showBadge={(i) => i.id === ID}
|
if (item.id === ID) {
|
||||||
isLoading={isPending || !product_categories}
|
return (
|
||||||
|
<div className="flex items-center gap-x-3">
|
||||||
|
<span>{item.name}</span>
|
||||||
|
<Badge size="2xsmall" color="blue">
|
||||||
|
{t("categories.fields.new.label")}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return item.name
|
||||||
|
}}
|
||||||
|
isLoading={!ready}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
+27
-9
@@ -1,9 +1,11 @@
|
|||||||
import { useMutation } from "@tanstack/react-query"
|
import { useMutation } from "@tanstack/react-query"
|
||||||
|
|
||||||
|
import { UniqueIdentifier } from "@dnd-kit/core"
|
||||||
import { Spinner } from "@medusajs/icons"
|
import { Spinner } from "@medusajs/icons"
|
||||||
import { FetchError } from "@medusajs/js-sdk"
|
import { FetchError } from "@medusajs/js-sdk"
|
||||||
import { HttpTypes } from "@medusajs/types"
|
import { HttpTypes } from "@medusajs/types"
|
||||||
import { toast } from "@medusajs/ui"
|
import { toast } from "@medusajs/ui"
|
||||||
|
import { useState } from "react"
|
||||||
import { RouteFocusModal } from "../../../../../components/modals"
|
import { RouteFocusModal } from "../../../../../components/modals"
|
||||||
import {
|
import {
|
||||||
categoriesQueryKeys,
|
categoriesQueryKeys,
|
||||||
@@ -29,11 +31,17 @@ export const OrganizeCategoryForm = () => {
|
|||||||
error: fetchError,
|
error: fetchError,
|
||||||
} = useProductCategories(QUERY)
|
} = useProductCategories(QUERY)
|
||||||
|
|
||||||
|
const [snapshot, setSnapshot] = useState<CategoryTreeItem[]>([])
|
||||||
|
|
||||||
const { mutateAsync, isPending: isMutating } = useMutation({
|
const { mutateAsync, isPending: isMutating } = useMutation({
|
||||||
mutationFn: async ({
|
mutationFn: async ({
|
||||||
value,
|
value,
|
||||||
}: {
|
}: {
|
||||||
value: CategoryTreeItem
|
value: {
|
||||||
|
id: string
|
||||||
|
parent_category_id: string | null
|
||||||
|
rank: number | null
|
||||||
|
}
|
||||||
arr: CategoryTreeItem[]
|
arr: CategoryTreeItem[]
|
||||||
}) => {
|
}) => {
|
||||||
await sdk.admin.productCategory.update(value.id, {
|
await sdk.admin.productCategory.update(value.id, {
|
||||||
@@ -57,6 +65,7 @@ export const OrganizeCategoryForm = () => {
|
|||||||
product_categories: update.arr,
|
product_categories: update.arr,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Optimistically update to the new value
|
||||||
queryClient.setQueryData(categoriesQueryKeys.list(QUERY), nextValue)
|
queryClient.setQueryData(categoriesQueryKeys.list(QUERY), nextValue)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -72,21 +81,29 @@ export const OrganizeCategoryForm = () => {
|
|||||||
|
|
||||||
toast.error(error.message)
|
toast.error(error.message)
|
||||||
},
|
},
|
||||||
onSettled: async (_data, _error, variables) => {
|
onSettled: async () => {
|
||||||
await queryClient.invalidateQueries({
|
await queryClient.invalidateQueries({
|
||||||
queryKey: categoriesQueryKeys.lists(),
|
queryKey: categoriesQueryKeys.all,
|
||||||
})
|
|
||||||
await queryClient.invalidateQueries({
|
|
||||||
queryKey: categoriesQueryKeys.detail(variables.value.id),
|
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleRankChange = async (
|
const handleRankChange = async (
|
||||||
value: CategoryTreeItem,
|
value: {
|
||||||
|
id: UniqueIdentifier
|
||||||
|
parentId: UniqueIdentifier | null
|
||||||
|
index: number
|
||||||
|
},
|
||||||
arr: CategoryTreeItem[]
|
arr: CategoryTreeItem[]
|
||||||
) => {
|
) => {
|
||||||
await mutateAsync({ value, arr })
|
const val = {
|
||||||
|
id: value.id as string,
|
||||||
|
parent_category_id: value.parentId as string | null,
|
||||||
|
rank: value.index,
|
||||||
|
}
|
||||||
|
|
||||||
|
setSnapshot(arr)
|
||||||
|
await mutateAsync({ value: val, arr })
|
||||||
}
|
}
|
||||||
|
|
||||||
const loading = isPending || isMutating
|
const loading = isPending || isMutating
|
||||||
@@ -104,7 +121,8 @@ export const OrganizeCategoryForm = () => {
|
|||||||
</RouteFocusModal.Header>
|
</RouteFocusModal.Header>
|
||||||
<RouteFocusModal.Body className="bg-ui-bg-subtle flex flex-1 flex-col overflow-y-auto">
|
<RouteFocusModal.Body className="bg-ui-bg-subtle flex flex-1 flex-col overflow-y-auto">
|
||||||
<CategoryTree
|
<CategoryTree
|
||||||
value={product_categories || []}
|
renderValue={(item) => item.name}
|
||||||
|
value={loading ? snapshot : product_categories || []}
|
||||||
onChange={handleRankChange}
|
onChange={handleRankChange}
|
||||||
/>
|
/>
|
||||||
</RouteFocusModal.Body>
|
</RouteFocusModal.Body>
|
||||||
|
|||||||
+18
-182
@@ -1,90 +1,31 @@
|
|||||||
import {
|
import { UniqueIdentifier } from "@dnd-kit/core"
|
||||||
DotsSix,
|
|
||||||
FolderIllustration,
|
|
||||||
FolderOpenIllustration,
|
|
||||||
Swatch,
|
|
||||||
TriangleRightMini,
|
|
||||||
} from "@medusajs/icons"
|
|
||||||
import { Badge, IconButton, Text, clx } from "@medusajs/ui"
|
|
||||||
import dropRight from "lodash/dropRight"
|
|
||||||
import flatMap from "lodash/flatMap"
|
|
||||||
import get from "lodash/get"
|
|
||||||
import { ReactNode } from "react"
|
import { ReactNode } from "react"
|
||||||
import Nestable from "react-nestable"
|
|
||||||
|
|
||||||
import { useTranslation } from "react-i18next"
|
import { SortableTree } from "../../../../../components/common/sortable-tree"
|
||||||
import "react-nestable/dist/styles/index.css"
|
|
||||||
import { CategoryTreeItem } from "../../types"
|
import { CategoryTreeItem } from "../../types"
|
||||||
import "./styles.css"
|
|
||||||
|
|
||||||
type CategoryTreeProps = {
|
type CategoryTreeProps = {
|
||||||
value: CategoryTreeItem[]
|
value: CategoryTreeItem[]
|
||||||
onChange: (value: CategoryTreeItem, items: CategoryTreeItem[]) => void
|
onChange: (
|
||||||
|
value: {
|
||||||
|
id: UniqueIdentifier
|
||||||
|
parentId: UniqueIdentifier | null
|
||||||
|
index: number
|
||||||
|
},
|
||||||
|
items: CategoryTreeItem[]
|
||||||
|
) => void
|
||||||
|
renderValue: (item: CategoryTreeItem) => ReactNode
|
||||||
enableDrag?: boolean | ((item: CategoryTreeItem) => boolean)
|
enableDrag?: boolean | ((item: CategoryTreeItem) => boolean)
|
||||||
showBadge?: (item: CategoryTreeItem) => boolean
|
|
||||||
isLoading?: boolean
|
isLoading?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CategoryTree = ({
|
export const CategoryTree = ({
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
|
renderValue,
|
||||||
enableDrag = true,
|
enableDrag = true,
|
||||||
showBadge,
|
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
}: CategoryTreeProps) => {
|
}: CategoryTreeProps) => {
|
||||||
const handleDrag = ({
|
|
||||||
dragItem,
|
|
||||||
items,
|
|
||||||
targetPath,
|
|
||||||
}: {
|
|
||||||
dragItem: CategoryTreeItem
|
|
||||||
items: CategoryTreeItem[]
|
|
||||||
targetPath: number[]
|
|
||||||
}) => {
|
|
||||||
let parentId = null
|
|
||||||
const [rank] = targetPath.slice(-1)
|
|
||||||
|
|
||||||
if (targetPath.length > 1) {
|
|
||||||
const path = dropRight(
|
|
||||||
flatMap(targetPath.slice(0, -1), (item) => [item, "category_children"])
|
|
||||||
)
|
|
||||||
|
|
||||||
const newParent = get(items, path) as CategoryTreeItem
|
|
||||||
parentId = newParent.id
|
|
||||||
}
|
|
||||||
|
|
||||||
onChange(
|
|
||||||
{
|
|
||||||
...dragItem,
|
|
||||||
parent_category_id: parentId,
|
|
||||||
rank,
|
|
||||||
},
|
|
||||||
items
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
...dragItem,
|
|
||||||
parent_category_id: parentId,
|
|
||||||
rank,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getIsEnabled = (item: CategoryTreeItem) => {
|
|
||||||
if (typeof enableDrag === "function") {
|
|
||||||
return enableDrag(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
return enableDrag
|
|
||||||
}
|
|
||||||
|
|
||||||
const getShowBadge = (item: CategoryTreeItem) => {
|
|
||||||
if (typeof showBadge === "function") {
|
|
||||||
return showBadge(item)
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="txt-compact-small relative flex-1 overflow-y-auto">
|
<div className="txt-compact-small relative flex-1 overflow-y-auto">
|
||||||
@@ -96,124 +37,19 @@ export const CategoryTree = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="txt-compact-small relative flex-1 overflow-y-auto">
|
<SortableTree
|
||||||
<Nestable
|
|
||||||
items={value}
|
items={value}
|
||||||
childrenProp="category_children"
|
childrenProp="category_children"
|
||||||
onChange={({ dragItem, items, targetPath }) =>
|
collapsible
|
||||||
handleDrag({
|
enableDrag={enableDrag}
|
||||||
dragItem: dragItem as CategoryTreeItem,
|
onChange={onChange}
|
||||||
items: items as CategoryTreeItem[],
|
renderValue={renderValue}
|
||||||
targetPath,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
disableDrag={({ item }) => getIsEnabled(item as CategoryTreeItem)}
|
|
||||||
renderItem={({ index, item, ...props }) => {
|
|
||||||
return (
|
|
||||||
<CategoryBranch
|
|
||||||
key={index}
|
|
||||||
item={item as CategoryTreeItem}
|
|
||||||
isEnabled={getIsEnabled(item as CategoryTreeItem)}
|
|
||||||
isNew={getShowBadge(item as CategoryTreeItem)}
|
|
||||||
{...props}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}}
|
|
||||||
renderCollapseIcon={({ isCollapsed }) => {
|
|
||||||
return <CollapseHandler isCollapsed={isCollapsed} />
|
|
||||||
}}
|
|
||||||
threshold={10}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const CollapseHandler = ({ isCollapsed }: { isCollapsed: boolean }) => {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center gap-x-3">
|
|
||||||
<IconButton size="small" variant="transparent" type="button">
|
|
||||||
<TriangleRightMini
|
|
||||||
className={clx({
|
|
||||||
"rotate-90 transform transition-transform": !isCollapsed,
|
|
||||||
})}
|
|
||||||
/>
|
|
||||||
</IconButton>
|
|
||||||
<div
|
|
||||||
className="text-ui-fg-muted flex h-7 w-7 items-center justify-center"
|
|
||||||
role="presentation"
|
|
||||||
>
|
|
||||||
{isCollapsed ? <FolderIllustration /> : <FolderOpenIllustration />}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
type CategoryBranchProps = {
|
|
||||||
item: CategoryTreeItem
|
|
||||||
depth: number
|
|
||||||
isEnabled: boolean
|
|
||||||
isNew?: boolean
|
|
||||||
collapseIcon: ReactNode
|
|
||||||
handler?: ReactNode
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CategoryBranch = ({
|
|
||||||
item,
|
|
||||||
depth,
|
|
||||||
isEnabled,
|
|
||||||
isNew = false,
|
|
||||||
collapseIcon,
|
|
||||||
}: CategoryBranchProps) => {
|
|
||||||
const { t } = useTranslation()
|
|
||||||
|
|
||||||
const isLeaf = !collapseIcon
|
|
||||||
|
|
||||||
const Component = (
|
|
||||||
<div
|
|
||||||
onDrag={() => console.log("dragging")}
|
|
||||||
data-disabled={!isEnabled}
|
|
||||||
className={clx(
|
|
||||||
"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 cursor-not-allowed":
|
|
||||||
!isEnabled,
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="flex h-7 w-7 items-center justify-center">
|
|
||||||
<DotsSix className="text-ui-fg-subtle" />
|
|
||||||
</div>
|
|
||||||
{Array.from({ length: depth }).map((_, i) => (
|
|
||||||
<div key={`offset_${i}`} role="presentation" className="h-7 w-7" />
|
|
||||||
))}
|
|
||||||
<div>{collapseIcon}</div>
|
|
||||||
{isLeaf && (
|
|
||||||
<div role="presentation" className="flex items-center">
|
|
||||||
<div className="size-7" />
|
|
||||||
<div className="text-ui-fg-muted flex h-7 w-7 items-center justify-center">
|
|
||||||
<Swatch />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="flex items-center gap-x-3">
|
|
||||||
<Text size="small" leading="compact">
|
|
||||||
{item.name}
|
|
||||||
</Text>
|
|
||||||
{isNew && (
|
|
||||||
<Badge size="2xsmall" color="blue">
|
|
||||||
{t("categories.fields.new.label")}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
return Component
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 -mb-px flex h-12 animate-pulse items-center border-y px-6 py-2.5" />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
-50
@@ -1,50 +0,0 @@
|
|||||||
.nestable-item {
|
|
||||||
margin: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nestable-item.is-dragging:before {
|
|
||||||
content: "" !important;
|
|
||||||
position: absolute !important;
|
|
||||||
top: 0 !important;
|
|
||||||
left: 0 !important;
|
|
||||||
right: 0 !important;
|
|
||||||
bottom: 0 !important;
|
|
||||||
background: var(--bg-base-hover) !important;
|
|
||||||
border-radius: 0 !important;
|
|
||||||
|
|
||||||
transition: 0.3s all !important;
|
|
||||||
border: 0px !important;
|
|
||||||
border-bottom: 1px solid var(--border-base) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nestable-item.is-dragging * {
|
|
||||||
height: 48px !important;
|
|
||||||
min-height: 48px !important;
|
|
||||||
max-height: 48px !important;
|
|
||||||
overflow: hidden !important;
|
|
||||||
background: var(--bg-base) !important;
|
|
||||||
box-shadow: var(--elevation-card-hover) !important;
|
|
||||||
z-index: 1000 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nestable-drag-layer > .nestable-list {
|
|
||||||
box-shadow: var(--elevation-flyout) !important;
|
|
||||||
border-radius: 6px !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 {
|
|
||||||
padding: 0 !important;
|
|
||||||
margin: 0 !important;
|
|
||||||
}
|
|
||||||
@@ -4508,7 +4508,6 @@ __metadata:
|
|||||||
react-hook-form: 7.49.1
|
react-hook-form: 7.49.1
|
||||||
react-i18next: 13.5.0
|
react-i18next: 13.5.0
|
||||||
react-jwt: ^1.2.0
|
react-jwt: ^1.2.0
|
||||||
react-nestable: ^3.0.2
|
|
||||||
react-router-dom: 6.20.1
|
react-router-dom: 6.20.1
|
||||||
tailwindcss: ^3.4.1
|
tailwindcss: ^3.4.1
|
||||||
tsup: ^8.0.2
|
tsup: ^8.0.2
|
||||||
@@ -25965,24 +25964,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"react-addons-shallow-compare@npm:^15.6.3":
|
|
||||||
version: 15.6.3
|
|
||||||
resolution: "react-addons-shallow-compare@npm:15.6.3"
|
|
||||||
dependencies:
|
|
||||||
object-assign: ^4.1.0
|
|
||||||
checksum: ad1a2ef7adf1a307b55de58a99ccf40998a1f7c84dcaadb1a2dd40c0e21e911b84fe04ab5f3494a1118846fc7537043287d54bcaffc2daf819b6c45eef5635ce
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"react-addons-update@npm:^15.6.3":
|
|
||||||
version: 15.6.3
|
|
||||||
resolution: "react-addons-update@npm:15.6.3"
|
|
||||||
dependencies:
|
|
||||||
object-assign: ^4.1.0
|
|
||||||
checksum: b6d98b459eb37393b8103309090b36649c0a0e95c7dbe010e692616eb025baf62b53cb187925ecce0c3ed0377357ce03c3a45a8ec2d91e6886f8d2eb55b0dfd1
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"react-aria@npm:^3.33.1":
|
"react-aria@npm:^3.33.1":
|
||||||
version: 3.33.1
|
version: 3.33.1
|
||||||
resolution: "react-aria@npm:3.33.1"
|
resolution: "react-aria@npm:3.33.1"
|
||||||
@@ -26194,19 +26175,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"react-nestable@npm:^3.0.2":
|
|
||||||
version: 3.0.2
|
|
||||||
resolution: "react-nestable@npm:3.0.2"
|
|
||||||
dependencies:
|
|
||||||
classnames: ^2.3.2
|
|
||||||
react: ^18.2.0
|
|
||||||
react-addons-shallow-compare: ^15.6.3
|
|
||||||
react-addons-update: ^15.6.3
|
|
||||||
react-dom: ^18.2.0
|
|
||||||
checksum: e2a7947382ca28e12048c534a2ad6e544dff60a56e712b7a1b00838434c5b5444b4c50c1fd652e032710b567674b4eff36ee4c3a5021c02f933d11c7b88ce7b9
|
|
||||||
languageName: node
|
|
||||||
linkType: hard
|
|
||||||
|
|
||||||
"react-refresh@npm:^0.14.0":
|
"react-refresh@npm:^0.14.0":
|
||||||
version: 0.14.2
|
version: 0.14.2
|
||||||
resolution: "react-refresh@npm:0.14.2"
|
resolution: "react-refresh@npm:0.14.2"
|
||||||
|
|||||||
Reference in new issue
Block a user