diff --git a/frontend/src/lib/lemon-ui/VerticalNestedDND/VerticalNestedDND.stories.tsx b/frontend/src/lib/lemon-ui/VerticalNestedDND/VerticalNestedDND.stories.tsx index 747b2e47d3d..fa285a71a3d 100644 --- a/frontend/src/lib/lemon-ui/VerticalNestedDND/VerticalNestedDND.stories.tsx +++ b/frontend/src/lib/lemon-ui/VerticalNestedDND/VerticalNestedDND.stories.tsx @@ -2,7 +2,6 @@ import { UniqueIdentifier } from '@dnd-kit/core' import { Meta, StoryFn, StoryObj } from '@storybook/react' import { VerticalNestedDND, VerticalNestedDNDProps } from './VerticalNestedDND' - type Story = StoryObj const meta: Meta = { title: 'Lemon UI/VerticalNestedDND', @@ -23,6 +22,7 @@ interface ExampleItem { id: UniqueIdentifier items: ExampleSubItem[] } +let counter = 0 const Template: StoryFn = (props: VerticalNestedDNDProps) => { const starterData: ExampleItem[] = [ @@ -56,7 +56,29 @@ const Template: StoryFn = (props: VerticalNestedDNDPro }, ] - return + const createNewChildItem = (): ExampleSubItem => { + return { + id: `new-${counter++}`, + } + } + + const createNewContainerItem = (): ExampleItem => { + return { + id: `new-${counter++}`, + items: [], + } + } + + return ( + console.log('onChange', items)} + initialItems={starterData} + /> + ) } export const Base: Story = Template.bind({}) diff --git a/frontend/src/lib/lemon-ui/VerticalNestedDND/VerticalNestedDND.tsx b/frontend/src/lib/lemon-ui/VerticalNestedDND/VerticalNestedDND.tsx index a37a1d41c64..c4093bde31d 100644 --- a/frontend/src/lib/lemon-ui/VerticalNestedDND/VerticalNestedDND.tsx +++ b/frontend/src/lib/lemon-ui/VerticalNestedDND/VerticalNestedDND.tsx @@ -41,14 +41,23 @@ export interface VNDNDContainerItem { id: UniqueIdentifier } -export interface VerticalNestedDNDProps> { +export interface VerticalNestedDNDProps> { initialItems: Item[] + renderContainerItem: (item: Item) => JSX.Element | null + renderChildItem: (item: ChildItem) => JSX.Element | null + createNewContainerItem(): Item + createNewChildItem(): ChildItem + onChange?(items: Item[]): void } -const PLACEHOLDER_ID = 'placeholder' -export function VerticalNestedDND>({ +export function VerticalNestedDND>({ initialItems, -}: VerticalNestedDNDProps): JSX.Element { + renderContainerItem, + renderChildItem, + createNewChildItem, + createNewContainerItem, + onChange, +}: VerticalNestedDNDProps): JSX.Element { const [items, setItems] = useState(() => { const items: Record = {} initialItems.forEach((item) => { @@ -67,6 +76,11 @@ export function VerticalNestedDND { + const newItemsArray = containers.map((containerId) => items[containerId]) + onChange?.(newItemsArray) + }, [containers, items, onChange]) + const collisionDetectionStrategy: CollisionDetection = useCallback( (args) => { if (activeId && activeId in items) { @@ -97,7 +111,7 @@ export function VerticalNestedDND container.id !== overId && - containerItems.some((subItem) => subItem.id === container.id) + containerItems.some((ChildItem) => ChildItem.id === container.id) ), })[0]?.id } @@ -129,6 +143,15 @@ export function VerticalNestedDND items[key].items.some((item) => item.id === id)) } + const findChildItem = (id: UniqueIdentifier): ChildItem | undefined => { + for (const containerId in items) { + const item = items[containerId].items.find((item) => item.id === id) + if (item) { + return item + } + } + } + const getIndex = (id: UniqueIdentifier): number => { const container = findContainer(id) @@ -136,9 +159,7 @@ export function VerticalNestedDND subItem.id === id) - - return index + return items[container].items.findIndex((ChildItem) => ChildItem.id === id) } const onDragCancel = (): void => { @@ -206,8 +227,8 @@ export function VerticalNestedDND { const activeItems = items[activeContainerId].items const overItems = items[overContainerId].items - const overIndex = overItems.findIndex((subItem) => subItem.id === overId) - const activeIndex = activeItems.findIndex((subItem) => subItem.id === active.id) + const overIndex = overItems.findIndex((ChildItem) => ChildItem.id === overId) + const activeIndex = activeItems.findIndex((ChildItem) => ChildItem.id === active.id) let newIndex: number if (overId in items) { @@ -247,8 +268,8 @@ export function VerticalNestedDND { const overItems = items[overContainerId].items - const overIndex = overItems.findIndex((subItem) => subItem.id === overId) - const activeIndex = overItems.findIndex((subItem) => subItem.id === active.id) + const overIndex = overItems.findIndex((ChildItem) => ChildItem.id === overId) + const activeIndex = overItems.findIndex((ChildItem) => ChildItem.id === active.id) const isBelowOverItem = over && @@ -297,8 +318,10 @@ export function VerticalNestedDND subItem.id === active.id) - const overIndex = items[overContainerId].items.findIndex((subItem) => subItem.id === overId) + const activeIndex = items[activeContainerId].items.findIndex( + (ChildItem) => ChildItem.id === active.id + ) + const overIndex = items[overContainerId].items.findIndex((ChildItem) => ChildItem.id === overId) if (activeIndex !== overIndex) { setItems((items) => { @@ -318,14 +341,17 @@ export function VerticalNestedDND -
+
{containers.map((containerId) => ( handleRemove(containerId)} + renderContainerItem={renderContainerItem} + containerItemId={containerId} + item={items[containerId]} + onAddChild={handleAddChild} > {items[containerId].items.map((value, index) => { @@ -338,6 +364,8 @@ export function VerticalNestedDND ) })} @@ -345,6 +373,11 @@ export function VerticalNestedDND ))} +
+ + Add container + +
{createPortal( @@ -359,22 +392,29 @@ export function VerticalNestedDND ) - function renderSortableItemDragOverlay(id: UniqueIdentifier): JSX.Element { - return + function renderSortableItemDragOverlay(id: UniqueIdentifier): JSX.Element | null { + const item = findChildItem(id) + if (!item) { + return null + } + return } - function renderContainerDragOverlay(containerId: UniqueIdentifier): JSX.Element { + function renderContainerDragOverlay(containerId: UniqueIdentifier): JSX.Element | null { + const item = items[containerId] + if (!item) { + return null + } return ( {}} > - {items[containerId].items.map((item, index) => ( - + {items[containerId].items.map((item) => ( + ))} ) @@ -385,26 +425,30 @@ export function VerticalNestedDND { - setContainers((containers) => [...containers, newContainerId]) - const newItem: Item = { - id: newContainerId, - items: [], - } as any + setContainers((containers) => [...containers, newItem.id]) setItems((items) => ({ ...items, - [newContainerId]: newItem, + [newItem.id]: newItem, })) }) } - function getNextContainerId(): string { - const containerIds = Object.keys(items) - const lastContainerId = containerIds[containerIds.length - 1] + function handleAddChild(containerId: UniqueIdentifier): void { + const newChild = createNewChildItem() - return String.fromCharCode(lastContainerId.charCodeAt(0) + 1) + setItems((items) => { + const container = items[containerId] + return { + ...items, + [containerId]: { + ...container, + items: [...container.items, newChild], + }, + } + }) } } @@ -419,37 +463,36 @@ const dropAnimation: DropAnimation = { } const animateLayoutChanges: AnimateLayoutChanges = (args) => defaultAnimateLayoutChanges({ ...args, wasDragging: true }) -interface SortableItemProps { +interface SortableItemProps { containerId: UniqueIdentifier id: UniqueIdentifier index: number handle: boolean disabled?: boolean getIndex(id: UniqueIdentifier): number + renderChildItem(item: Item): JSX.Element | null + item: Item } -function SortableItem({ disabled, id, index, handle }: SortableItemProps): JSX.Element { - const { - setNodeRef, - setActivatorNodeRef, - listeners, - isDragging, - isSorting, - over, - overIndex, - transform, - transition, - } = useSortable({ +function SortableItem({ + disabled, + id, + index, + handle, + renderChildItem, + item, +}: SortableItemProps): JSX.Element { + const { setNodeRef, setActivatorNodeRef, listeners, isDragging, isSorting, transform, transition } = useSortable({ id, }) const mounted = useMountStatus() const mountedWhileDragging = isDragging && !mounted return ( - ) } @@ -473,22 +518,21 @@ function useMountStatus(): boolean { return isMounted } -function DroppableContainer({ +function DroppableContainer>({ children, columns = 1, disabled, - id, items, style, + containerItemId, ...props -}: ContainerProps & { +}: ContainerProps & { disabled?: boolean - id: UniqueIdentifier - items: SubItem[] + items: ChildItem[] style?: React.CSSProperties }): JSX.Element { const { active, attributes, isDragging, listeners, over, setNodeRef, transition, transform } = useSortable({ - id, + id: containerItemId, data: { type: 'container', children: items, @@ -496,7 +540,8 @@ function DroppableContainer({ animateLayoutChanges, }) const isOverContainer = over - ? (id === over.id && active?.data.current?.type !== 'container') || items.some((item) => item.id === over.id) + ? (containerItemId === over.id && active?.data.current?.type !== 'container') || + items.some((item) => item.id === over.id) : false return ( @@ -511,7 +556,7 @@ function DroppableContainer({ ...listeners, }} columns={columns} - label={`Column ${id}`} + containerItemId={containerItemId} {...props} > {children} @@ -519,10 +564,10 @@ function DroppableContainer({ ) } -export interface ContainerProps { +export interface ContainerProps> { children: React.ReactNode columns?: number - label?: string + containerItemId: UniqueIdentifier style?: React.CSSProperties horizontal?: boolean hover?: boolean @@ -533,12 +578,15 @@ export interface ContainerProps { unstyled?: boolean onClick?(): void onRemove?(): void + onAddChild(containerId: UniqueIdentifier): void isDragging?: boolean transition?: string transform?: string + renderContainerItem(item: Item): JSX.Element | null + item: Item } -export const Container = forwardRef(function Container_( +export const Container = forwardRef(function Container_>( { children, handleProps, @@ -546,7 +594,8 @@ export const Container = forwardRef(function Con hover, onClick, onRemove, - label, + onAddChild, + containerItemId, placeholder, style, scrollable, @@ -555,16 +604,20 @@ export const Container = forwardRef(function Con isDragging, transform, transition, + renderContainerItem, + item, ...props - }: ContainerProps, - ref + }: ContainerProps, + ref: React.ForwardedRef ) { const Component = onClick ? 'button' : 'div' return ( (function Con onClick={onClick} tabIndex={onClick ? 0 : undefined} > -
- {label ? {label} : null} - - - - +
+ +
{renderContainerItem ? renderContainerItem(item) : Container {containerItemId}}
+
+ +
+ {placeholder ? children :
    {children}
} +
+ onAddChild(item.id) : undefined} + fullWidth={false} + type="secondary" + > + Add child +
- {placeholder ? children :
    {children}
} ) }) -export interface ItemProps { +export interface ChildItemProps { dragOverlay?: boolean color?: string disabled?: boolean - dragging?: boolean + isDragging?: boolean handleProps?: any height?: number index?: number @@ -601,71 +662,76 @@ export interface ItemProps { style?: React.CSSProperties transition?: string | null wrapperStyle?: React.CSSProperties - value: UniqueIdentifier + childItemId: UniqueIdentifier + item: Item onRemove?(): void + renderChildItem(item: Item): JSX.Element | null } -export const Item = React.memo( - React.forwardRef( - ( - { - color, - dragOverlay, - dragging, - disabled, - fadeIn, - handleProps, - height, - index, - listeners, - onRemove, - sorting, - style, - transition, - transform, - value, - wrapperStyle, - ...props - }, - ref - ) => { - const handle = true - useEffect(() => { - if (!dragOverlay) { - return - } +export const ChildItem = React.memo( + React.forwardRef>(function ChildItem_( + { + color, + dragOverlay, + isDragging, + disabled, + fadeIn, + handleProps, + height, + index, + listeners, + onRemove, + sorting, + style, + transition, + transform, + childItemId, + wrapperStyle, + renderChildItem, + ...props + }, + ref + ) { + const handle = true + useEffect(() => { + if (!dragOverlay) { + return + } - document.body.style.cursor = 'grabbing' + document.body.style.cursor = 'grabbing' - return () => { - document.body.style.cursor = '' - } - }, [dragOverlay]) + return () => { + document.body.style.cursor = '' + } + }, [dragOverlay]) - return ( -
  • -
    - Item {value} - - {onRemove ? : null} - {handle ? : null} - -
    -
  • - ) - } - ) + return ( +
  • +
    + +
    {renderChildItem ? renderChildItem(childItemId) : Item {childItemId}}
    +
    + +
    +
  • + ) + }) ) export function Remove(props: LemonButtonProps): JSX.Element { return ( - + )