diff --git a/src-tauri/icons/icon.afdesign b/src-tauri/icons/icon.afdesign deleted file mode 100644 index 3ea2e157..00000000 Binary files a/src-tauri/icons/icon.afdesign and /dev/null differ diff --git a/src-web/assets/icons/Icons.afdesign b/src-web/assets/icons/Icons.afdesign deleted file mode 100644 index 5561d0fb..00000000 Binary files a/src-web/assets/icons/Icons.afdesign and /dev/null differ diff --git a/src-web/components/App.tsx b/src-web/components/App.tsx index 200b2297..fc6f84f1 100644 --- a/src-web/components/App.tsx +++ b/src-web/components/App.tsx @@ -6,7 +6,6 @@ import { HTML5Backend } from 'react-dnd-html5-backend'; import { HelmetProvider } from 'react-helmet-async'; import { AppRouter } from './AppRouter'; import { DialogProvider } from './DialogContext'; -import { TauriListeners } from './TauriListeners'; const queryClient = new QueryClient({ logger: undefined, @@ -28,7 +27,6 @@ export function App() { - {/**/} diff --git a/src-web/components/AppRouter.tsx b/src-web/components/AppRouter.tsx index 448e2d96..1e08ebf1 100644 --- a/src-web/components/AppRouter.tsx +++ b/src-web/components/AppRouter.tsx @@ -1,6 +1,7 @@ -import { createBrowserRouter, Navigate, RouterProvider } from 'react-router-dom'; +import { createBrowserRouter, Navigate, Outlet, RouterProvider } from 'react-router-dom'; import { routePaths } from '../hooks/useRoutes'; import RouteError from './RouteError'; +import { TauriListeners } from './TauriListeners'; import Workspace from './Workspace'; import Workspaces from './Workspaces'; @@ -8,6 +9,12 @@ const router = createBrowserRouter([ { path: '/', errorElement: , + element: ( + <> + + + + ), children: [ { path: '/', diff --git a/src-web/components/RequestActionsDropdown.tsx b/src-web/components/RequestActionsDropdown.tsx index 94606d82..865f3546 100644 --- a/src-web/components/RequestActionsDropdown.tsx +++ b/src-web/components/RequestActionsDropdown.tsx @@ -24,7 +24,7 @@ export function RequestActionsDropdown({ requestId, children }: Props) { label: 'Duplicate', onSelect: duplicateRequest.mutate, leftSlot: , - rightSlot: ⌘D, + rightSlot: , }, { label: 'Delete', diff --git a/src-web/components/ResponsePane.tsx b/src-web/components/ResponsePane.tsx index 1598f309..f4cc6cb5 100644 --- a/src-web/components/ResponsePane.tsx +++ b/src-web/components/ResponsePane.tsx @@ -40,7 +40,7 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro const activeResponse: HttpResponse | null = pinnedResponseId ? responses.find((r) => r.id === pinnedResponseId) ?? null : responses[responses.length - 1] ?? null; - const [viewMode, toggleViewMode] = useResponseViewMode(activeResponse?.requestId); + const [viewMode, setViewMode] = useResponseViewMode(activeResponse?.requestId); const deleteResponse = useDeleteResponse(activeResponse?.id ?? null); const deleteAllResponses = useDeleteResponses(activeResponse?.requestId); const [activeTab, setActiveTab] = useActiveTab(); @@ -62,7 +62,7 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro label: 'Preview', options: { value: viewMode, - onChange: toggleViewMode, + onChange: setViewMode, items: [ { label: 'Pretty', value: 'pretty' }, { label: 'Raw', value: 'raw' }, @@ -81,7 +81,7 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro value: 'headers', }, ], - [activeResponse?.headers, toggleViewMode, viewMode], + [activeResponse?.headers, setViewMode, viewMode], ); // Don't render until we know the view mode diff --git a/src-web/components/Sidebar.tsx b/src-web/components/Sidebar.tsx index c93906f3..f94e020c 100644 --- a/src-web/components/Sidebar.tsx +++ b/src-web/components/Sidebar.tsx @@ -3,7 +3,8 @@ 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 { NavLink } from 'react-router-dom'; +import { useActiveRequestId } from '../hooks/useActiveRequestId'; import { useDeleteRequest } from '../hooks/useDeleteRequest'; import { useLatestResponse } from '../hooks/useLatestResponse'; import { useRequests } from '../hooks/useRequests'; @@ -11,7 +12,6 @@ import { useUpdateAnyRequest } from '../hooks/useUpdateAnyRequest'; import { useUpdateRequest } from '../hooks/useUpdateRequest'; import type { HttpRequest } from '../lib/models'; import { isResponseLoading } from '../lib/models'; -import { Button } from './core/Button'; import { Icon } from './core/Icon'; import { VStack } from './core/Stacks'; import { StatusTag } from './core/StatusTag'; @@ -28,7 +28,6 @@ enum ItemTypes { 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], @@ -45,20 +44,14 @@ export const Sidebar = memo(function Sidebar({ className }: Props) { className="relative py-3 overflow-y-auto overflow-x-visible" draggable={false} > - + ); }); -function SidebarItems({ - requests, - activeRequestId, -}: { - requests: HttpRequest[]; - activeRequestId?: string; -}) { +function SidebarItems({ requests }: { requests: HttpRequest[] }) { const [hoveredIndex, setHoveredIndex] = useState(null); const updateRequest = useUpdateAnyRequest(); @@ -112,7 +105,6 @@ function SidebarItems({ requestId={r.id} requestName={r.name} workspaceId={r.workspaceId} - active={r.id === activeRequestId} onMove={handleMove} onEnd={handleEnd} /> @@ -128,17 +120,18 @@ type SidebarItemProps = { requestId: string; requestName: string; workspaceId: string; - active?: boolean; }; const _SidebarItem = forwardRef(function SidebarItem( - { className, requestName, requestId, workspaceId, active }: SidebarItemProps, + { className, requestName, requestId, workspaceId }: SidebarItemProps, ref: ForwardedRef, ) { const latestResponse = useLatestResponse(requestId); const updateRequest = useUpdateRequest(requestId); const deleteRequest = useDeleteRequest(requestId); const [editing, setEditing] = useState(false); + const activeRequestId = useActiveRequestId(); + const isActive = activeRequestId === requestId; const handleSubmitNameEdit = useCallback( async (el: HTMLInputElement) => { @@ -156,16 +149,16 @@ const _SidebarItem = forwardRef(function SidebarItem( const handleKeyDown = useCallback( (e: KeyboardEvent) => { // Hitting enter on active request during keyboard nav will start edit - if (active && e.key === 'Enter') { + if (isActive && e.key === 'Enter') { e.preventDefault(); setEditing(true); } - if (active && (e.key === 'Backspace' || e.key === 'Delete')) { + if (isActive && (e.key === 'Backspace' || e.key === 'Delete')) { e.preventDefault(); deleteRequest.mutate(); } }, - [active, deleteRequest], + [isActive, deleteRequest], ); const handleInputKeyDown = useCallback( @@ -185,21 +178,29 @@ const _SidebarItem = forwardRef(function SidebarItem( [handleSubmitNameEdit], ); + const handleStartEditing = useCallback(() => setEditing(true), [setEditing]); + + const handleBlur = useCallback( + (e: React.FocusEvent) => { + handleSubmitNameEdit(e.currentTarget).catch(console.error); + }, + [handleSubmitNameEdit], + ); + return (
  • -
    )} - +
  • ); @@ -248,7 +249,6 @@ const DraggableSidebarItem = memo(function DraggableSidebarItem({ requestName, requestId, workspaceId, - active, onMove, onEnd, }: DraggableSidebarItemProps) { @@ -290,7 +290,6 @@ const DraggableSidebarItem = memo(function DraggableSidebarItem({ requestName={requestName} requestId={requestId} workspaceId={workspaceId} - active={active} /> ); }); diff --git a/src-web/components/SidebarActions.tsx b/src-web/components/SidebarActions.tsx index 3a5d556a..347c5d41 100644 --- a/src-web/components/SidebarActions.tsx +++ b/src-web/components/SidebarActions.tsx @@ -1,26 +1,20 @@ import { memo, useCallback } from 'react'; -import { useActiveRequestId } from '../hooks/useActiveRequestId'; import { useCreateRequest } from '../hooks/useCreateRequest'; -import { useDuplicateRequest } from '../hooks/useDuplicateRequest'; import { useSidebarHidden } from '../hooks/useSidebarHidden'; import { useTauriEvent } from '../hooks/useTauriEvent'; import { IconButton } from './core/IconButton'; -export const SidebarActions = memo(function SidebarDisplayToggle() { +export const SidebarActions = memo(function SidebarActions() { const { hidden, toggle } = useSidebarHidden(); - const activeRequestId = useActiveRequestId(); const createRequest = useCreateRequest({ navigateAfter: true }); - const duplicateRequest = useDuplicateRequest({ id: activeRequestId, navigateAfter: true }); + const handleCreateRequest = useCallback(() => { createRequest.mutate({}); }, [createRequest]); + useTauriEvent('new_request', () => { createRequest.mutate({}); }); - // TODO: Put this somewhere better - useTauriEvent('duplicate_request', () => { - duplicateRequest.mutate(); - }); return ( <> diff --git a/src-web/components/TauriListeners.tsx b/src-web/components/TauriListeners.tsx index f48831a4..980d867d 100644 --- a/src-web/components/TauriListeners.tsx +++ b/src-web/components/TauriListeners.tsx @@ -1,5 +1,7 @@ import { useQueryClient } from '@tanstack/react-query'; import { appWindow } from '@tauri-apps/api/window'; +import { useActiveRequestId } from '../hooks/useActiveRequestId'; +import { useDuplicateRequest } from '../hooks/useDuplicateRequest'; import { keyValueQueryKey } from '../hooks/useKeyValue'; import { requestsQueryKey } from '../hooks/useRequests'; import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey'; @@ -15,6 +17,14 @@ export function TauriListeners() { const queryClient = useQueryClient(); const { wasUpdatedExternally } = useRequestUpdateKey(null); + const activeRequestId = useActiveRequestId(); + const duplicateRequest = useDuplicateRequest({ id: activeRequestId, navigateAfter: true }); + + // TODO: Put this somewhere better + useTauriEvent('duplicate_request', () => { + duplicateRequest.mutate(); + }); + useTauriEvent('created_model', ({ payload, windowLabel }) => { if (shouldIgnoreEvent(payload, windowLabel)) return; diff --git a/src-web/components/core/HotKey.tsx b/src-web/components/core/HotKey.tsx index 90519a26..6d7b3d55 100644 --- a/src-web/components/core/HotKey.tsx +++ b/src-web/components/core/HotKey.tsx @@ -1,15 +1,21 @@ import classnames from 'classnames'; -import type { HTMLAttributes } from 'react'; -export function HotKey({ children }: HTMLAttributes) { +interface Props { + modifier: 'Meta' | 'Control' | 'Shift'; + keyName: string; +} + +const keys: Record = { + Control: '⌃', + Meta: '⌘', + Shift: '⇧', +}; + +export function HotKey({ modifier, keyName }: Props) { return ( - - {children} + + {keys[modifier]} + {keyName} ); } diff --git a/src-web/components/core/IconButton.tsx b/src-web/components/core/IconButton.tsx index 26f4fd2d..04a928e7 100644 --- a/src-web/components/core/IconButton.tsx +++ b/src-web/components/core/IconButton.tsx @@ -1,6 +1,6 @@ import classnames from 'classnames'; import type { MouseEvent } from 'react'; -import { forwardRef, memo, useCallback } from 'react'; +import { forwardRef, useCallback } from 'react'; import { useTimedBoolean } from '../../hooks/useTimedBoolean'; import type { ButtonProps } from './Button'; import { Button } from './Button'; @@ -15,7 +15,7 @@ type Props = IconProps & title: string; }; -const _IconButton = forwardRef(function IconButton( +export const IconButton = forwardRef(function IconButton( { showConfirm, icon, @@ -69,5 +69,3 @@ const _IconButton = forwardRef(function IconButton( ); }); - -export const IconButton = memo(_IconButton); diff --git a/src-web/hooks/useDeleteRequest.tsx b/src-web/hooks/useDeleteRequest.tsx index 38a7d64a..05b6aae0 100644 --- a/src-web/hooks/useDeleteRequest.tsx +++ b/src-web/hooks/useDeleteRequest.tsx @@ -3,16 +3,12 @@ import { invoke } from '@tauri-apps/api'; import { InlineCode } from '../components/core/InlineCode'; import type { HttpRequest } from '../lib/models'; import { getRequest } from '../lib/store'; -import { useActiveRequestId } from './useActiveRequestId'; import { useConfirm } from './useConfirm'; import { requestsQueryKey } from './useRequests'; import { responsesQueryKey } from './useResponses'; -import { useRoutes } from './useRoutes'; export function useDeleteRequest(id: string | null) { const queryClient = useQueryClient(); - const activeRequestId = useActiveRequestId(); - const routes = useRoutes(); const confirm = useConfirm(); return useMutation({ @@ -39,9 +35,6 @@ export function useDeleteRequest(id: string | null) { queryClient.setQueryData(requestsQueryKey({ workspaceId }), (requests) => (requests ?? []).filter((r) => r.id !== requestId), ); - if (activeRequestId === requestId) { - routes.navigate('workspace', { workspaceId }); - } }, }); } diff --git a/src-web/hooks/useResponseViewMode.ts b/src-web/hooks/useResponseViewMode.ts index ff405cdb..eb3017d5 100644 --- a/src-web/hooks/useResponseViewMode.ts +++ b/src-web/hooks/useResponseViewMode.ts @@ -1,15 +1,11 @@ -import { useKeyValue } from './useKeyValue'; +import { useLocalStorage } from 'react-use'; -export function useResponseViewMode(requestId?: string): [string | undefined, () => void] { - const v = useKeyValue({ - namespace: 'app', - key: ['response_view_mode', requestId ?? 'n/a'], - defaultValue: 'pretty', - }); - - const toggle = () => { - v.set(v.value === 'pretty' ? 'raw' : 'pretty'); - }; - - return [v.value, toggle]; +export function useResponseViewMode( + requestId?: string, +): [string | undefined, (m: 'pretty' | 'raw') => void] { + const [value, setValue] = useLocalStorage<'pretty' | 'raw'>( + `response_view_mode::${requestId}`, + 'pretty', + ); + return [value, setValue]; } diff --git a/src-web/hooks/useRoutes.ts b/src-web/hooks/useRoutes.ts index c982c05b..73a0432a 100644 --- a/src-web/hooks/useRoutes.ts +++ b/src-web/hooks/useRoutes.ts @@ -1,3 +1,4 @@ +import { useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; export type RouteParamsWorkspace = { @@ -27,17 +28,20 @@ export const routePaths = { export function useRoutes() { const navigate = useNavigate(); - return { - navigate( - path: T, - ...params: Parameters<(typeof routePaths)[T]> - ) { - // Not sure how to make TS work here, but it's good from the - // outside caller perspective. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const resolvedPath = routePaths[path](...(params as any)); - navigate(resolvedPath); - }, - paths: routePaths, - }; + return useMemo( + () => ({ + navigate( + path: T, + ...params: Parameters<(typeof routePaths)[T]> + ) { + // Not sure how to make TS work here, but it's good from the + // outside caller perspective. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const resolvedPath = routePaths[path](...(params as any)); + navigate(resolvedPath); + }, + paths: routePaths, + }), + [navigate], + ); } diff --git a/src-web/hooks/useSidebarWidth.ts b/src-web/hooks/useSidebarWidth.ts index 9c54f6c1..362ff92e 100644 --- a/src-web/hooks/useSidebarWidth.ts +++ b/src-web/hooks/useSidebarWidth.ts @@ -5,6 +5,6 @@ export function useSidebarWidth() { return useKeyValue({ namespace: NAMESPACE_NO_SYNC, key: 'sidebar_width', - defaultValue: 200, + defaultValue: 220, }); } diff --git a/src-web/main.css b/src-web/main.css index 075d7a71..3557e35e 100644 --- a/src-web/main.css +++ b/src-web/main.css @@ -33,20 +33,24 @@ } /* Style the scrollbars */ - ::-webkit-scrollbar-corner, - ::-webkit-scrollbar { - @apply w-1.5 h-1.5; - } + * { + ::-webkit-scrollbar-corner, + ::-webkit-scrollbar { + @apply w-1.5 h-1.5; + } - .scrollbar-track, - ::-webkit-scrollbar-corner, - ::-webkit-scrollbar { - @apply bg-transparent; - } + .scrollbar-track, + ::-webkit-scrollbar-corner, + ::-webkit-scrollbar { + @apply bg-transparent; + } - .scrollbar-thumb, - ::-webkit-scrollbar-thumb { - @apply bg-gray-500/30 hover:bg-gray-500/50 rounded-full; + &:hover { + &.scrollbar-thumb, + &::-webkit-scrollbar-thumb { + @apply bg-gray-500/30 hover:bg-gray-500/50 rounded-full; + } + } } iframe {