import classnames from 'classnames'; import type { Identifier } from 'dnd-core'; import type { CSSProperties } from 'react'; import React, { useCallback, useEffect, useRef, useState } from 'react'; import type { XYCoord } from 'react-dnd'; import { DndProvider, useDrag, useDrop } from 'react-dnd'; import { HTML5Backend } from 'react-dnd-html5-backend'; import { useActiveRequest } from '../hooks/useActiveRequest'; import { useCreateRequest } from '../hooks/useCreateRequest'; import { useDeleteRequest } from '../hooks/useDeleteRequest'; import { useKeyValue } from '../hooks/useKeyValue'; import { useRequests } from '../hooks/useRequests'; import { useTheme } from '../hooks/useTheme'; import { useUpdateRequest } from '../hooks/useUpdateRequest'; import { clamp } from '../lib/clamp'; import type { HttpRequest } from '../lib/models'; import { Button } from './core/Button'; import { Dropdown, DropdownMenuTrigger } from './core/Dropdown'; import { Icon } from './core/Icon'; import { IconButton } from './core/IconButton'; import { HStack, VStack } from './core/Stacks'; import { WindowDragRegion } from './core/WindowDragRegion'; interface Props { className?: string; } const MIN_WIDTH = 110; const INITIAL_WIDTH = 200; const MAX_WIDTH = 500; enum ItemTypes { REQUEST = 'request', } export function Sidebar({ className }: Props) { return ( ); } export function Container({ className }: Props) { const [isResizing, setIsRisizing] = useState(false); const width = useKeyValue({ key: 'sidebar_width', initialValue: INITIAL_WIDTH }); const sidebarRef = useRef(null); const requests = useRequests(); const activeRequest = useActiveRequest(); const createRequest = useCreateRequest({ navigateAfter: true }); const { appearance, toggleAppearance } = useTheme(); const [items, setItems] = useState(requests.map((r) => ({ request: r, left: 0, top: 0 }))); useEffect(() => { setItems(requests.map((r) => ({ request: r, left: 0, top: 0 }))); }, [requests.length]); const moveState = useRef<{ move: (e: MouseEvent) => void; up: () => void } | null>(null); const unsub = () => { if (moveState.current !== null) { document.documentElement.removeEventListener('mousemove', moveState.current.move); document.documentElement.removeEventListener('mouseup', moveState.current.up); } }; const handleResizeReset = () => { width.set(INITIAL_WIDTH); }; const handleResizeStart = (e: React.MouseEvent) => { unsub(); const mouseStartX = e.clientX; const startWidth = width.value; moveState.current = { move: (e: MouseEvent) => { const newWidth = clamp(startWidth + (e.clientX - mouseStartX), MIN_WIDTH, MAX_WIDTH); width.set(newWidth); }, up: () => { unsub(); setIsRisizing(false); }, }; document.documentElement.addEventListener('mousemove', moveState.current.move); document.documentElement.addEventListener('mouseup', moveState.current.up); setIsRisizing(true); }; const sidebarWidth = sidebarRef.current?.clientWidth ?? 0; const handleMove = useCallback((dragIndex: number, hoverIndex: number) => { setItems((oldItems) => { const newItems = [...oldItems]; const b = newItems[hoverIndex]!; newItems[hoverIndex] = newItems[dragIndex]!; newItems[dragIndex] = b; return newItems; }); }, []); return (
{/* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions */}
{ await createRequest.mutate({ name: 'Test Request' }); }} /> {items.map(({ request, left, top }, i) => ( ))}
); } interface SidebarItemProps { request: HttpRequest; sidebarWidth: number; active?: boolean; isDragging?: boolean; } function SidebarItem({ request, active, sidebarWidth, isDragging }: SidebarItemProps) { const deleteRequest = useDeleteRequest(request); const updateRequest = useUpdateRequest(request); const [editing, setEditing] = useState(false); const handleSubmitNameEdit = async (el: HTMLInputElement) => { await updateRequest.mutate({ name: el.value }); setEditing(false); }; const handleFocus = (el: HTMLInputElement | null) => { el?.focus(); el?.select(); }; return (
  • , }, ]} >
  • ); } type DraggableSidebarItemProps = SidebarItemProps & { left: number; top: number; index: number; onMove: (dragIndex: number, hoverIndex: number) => void; }; type DragItem = { request: HttpRequest; index: number; top: number; left: number; }; function getStyles(left: number, top: number, width: number): CSSProperties { const transform = `translate3d(${left}px, ${top}px, 0)`; return { transform, WebkitTransform: transform, width, }; } function DraggableSidebarItem({ index, left, top, request, active, sidebarWidth, onMove, }: DraggableSidebarItemProps) { const ref = useRef(null); const [, drop] = useDrop({ accept: ItemTypes.REQUEST, hover: (item, monitor) => { if (!ref.current) return; const dragIndex = item.index; const hoverIndex = index; // Don't replace items with themselves if (dragIndex === hoverIndex) return; const hoverBoundingRect = ref.current?.getBoundingClientRect(); const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2; const clientOffset = monitor.getClientOffset(); const hoverClientY = (clientOffset as XYCoord).y - hoverBoundingRect.top; // Only perform the move when the mouse has crossed half of the items height // When dragging downwards, only move when the cursor is below 50% // When dragging upwards, only move when the cursor is above 50% if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) return; if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) return; onMove(dragIndex, hoverIndex); // Note: we're mutating the monitor item here! Generally it's better to // avoid mutations, but it's good here for the sake of performance to // avoid expensive index searches. item.index = hoverIndex; }, }); const [monitor, drag] = useDrag(() => ({ type: ItemTypes.REQUEST, item: () => ({ request, left, top, index }), isDragging: (monitor) => monitor.getItem().request.id === request.id, collect: (monitor) => ({ isDragging: monitor.isDragging() }), })); const isDragging = monitor?.isDragging; drag(drop(ref)); return (
    ); }