import classnames from 'classnames'; import type { ForwardedRef, KeyboardEvent } from 'react'; import React, { forwardRef, Fragment, memo, useCallback, useMemo, useRef, useState } from 'react'; import type { XYCoord } from 'react-dnd'; import { useDrag, useDrop } from 'react-dnd'; import { useActiveRequest } from '../hooks/useActiveRequest'; import { useRequests } from '../hooks/useRequests'; import { useUpdateAnyRequest } from '../hooks/useUpdateAnyRequest'; import { useUpdateRequest } from '../hooks/useUpdateRequest'; import type { HttpRequest } from '../lib/models'; import { Button } from './core/Button'; import { IconButton } from './core/IconButton'; import { HStack, VStack } from './core/Stacks'; import { DropMarker } from './DropMarker'; import { RequestActionsDropdown } from './RequestActionsDropdown'; import { ToggleThemeButton } from './ToggleThemeButton'; interface Props { className?: string; } enum ItemTypes { REQUEST = 'request', } export const Sidebar = memo(function Sidebar({ className }: Props) { const sidebarRef = useRef(null); const unorderedRequests = useRequests(); const activeRequest = useActiveRequest(); const requests = useMemo( () => [...unorderedRequests].sort((a, b) => a.sortPriority - b.sortPriority), [unorderedRequests], ); return (
); }); function SidebarItems({ requests, activeRequestId, }: { requests: HttpRequest[]; activeRequestId?: string; }) { const [hoveredIndex, setHoveredIndex] = useState(null); const updateRequest = useUpdateAnyRequest(); const handleMove = useCallback( (id, side) => { const dragIndex = requests.findIndex((r) => r.id === id); setHoveredIndex(side === 'above' ? dragIndex : dragIndex + 1); }, [requests], ); const handleEnd = useCallback( (requestId) => { if (hoveredIndex === null) return; setHoveredIndex(null); const index = requests.findIndex((r) => r.id === requestId); const request = requests[index]; if (request === undefined) return; const newRequests = requests.filter((r) => r.id !== requestId); if (hoveredIndex > index) newRequests.splice(hoveredIndex - 1, 0, request); else newRequests.splice(hoveredIndex, 0, request); const beforePriority = newRequests[hoveredIndex - 1]?.sortPriority ?? 0; const afterPriority = newRequests[hoveredIndex + 1]?.sortPriority ?? 0; const shouldUpdateAll = afterPriority - beforePriority < 1; if (shouldUpdateAll) { newRequests.forEach(({ id }, i) => { const sortPriority = i * 1000; const update = (r: HttpRequest) => ({ ...r, sortPriority }); updateRequest.mutate({ id, update }); }); } else { const sortPriority = afterPriority - (afterPriority - beforePriority) / 2; const update = (r: HttpRequest) => ({ ...r, sortPriority }); updateRequest.mutate({ id: requestId, update }); } }, [hoveredIndex, requests], ); return ( <> {requests.map((r, i) => ( {hoveredIndex === i && } ))} {hoveredIndex === requests.length && } ); } type SidebarItemProps = { className?: string; requestId: string; requestName: string; workspaceId: string; active?: boolean; }; const _SidebarItem = forwardRef(function SidebarItem( { className, requestName, requestId, workspaceId, active }: SidebarItemProps, ref: ForwardedRef, ) { const updateRequest = useUpdateRequest(requestId); const [editing, setEditing] = useState(false); const handleSubmitNameEdit = useCallback(async (el: HTMLInputElement) => { await updateRequest.mutate((r) => ({ ...r, name: el.value })); setEditing(false); }, []); const handleFocus = useCallback((el: HTMLInputElement | null) => { el?.focus(); el?.select(); }, []); const handleKeyDown = useCallback( (e: KeyboardEvent) => { // Hitting enter on active request during keyboard nav will start edit if (active && e.key === 'Enter') { e.preventDefault(); setEditing(true); } }, [active], ); const handleInputKeyDown = useCallback( async (e: KeyboardEvent) => { switch (e.key) { case 'Enter': await handleSubmitNameEdit(e.currentTarget); break; case 'Escape': setEditing(false); break; } }, [active], ); return (
  • ); }); const SidebarItem = memo(_SidebarItem); type DraggableSidebarItemProps = SidebarItemProps & { onMove: (id: string, side: 'above' | 'below') => void; onEnd: (id: string) => void; }; type DragItem = { id: string; workspaceId: string; requestName: string; }; const DraggableSidebarItem = memo(function DraggableSidebarItem({ requestName, requestId, workspaceId, active, onMove, onEnd, }: DraggableSidebarItemProps) { const ref = useRef(null); const [, connectDrop] = useDrop( { accept: ItemTypes.REQUEST, hover: (item, monitor) => { if (!ref.current) 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; onMove(requestId, hoverClientY < hoverMiddleY ? 'above' : 'below'); }, }, [onMove], ); const [{ isDragging }, connectDrag] = useDrag( () => ({ type: ItemTypes.REQUEST, item: () => ({ id: requestId, requestName, workspaceId }), collect: (m) => ({ isDragging: m.isDragging() }), options: { dropEffect: 'move' }, end: () => onEnd(requestId), }), [onEnd], ); connectDrag(ref); connectDrop(ref); return ( ); });