A bunch of tweaks

This commit is contained in:
Gregory Schier
2023-04-06 16:05:25 -07:00
parent 0b3bd6313f
commit b2524c1de0
16 changed files with 110 additions and 101 deletions

Binary file not shown.

Binary file not shown.

View File

@@ -6,7 +6,6 @@ import { HTML5Backend } from 'react-dnd-html5-backend';
import { HelmetProvider } from 'react-helmet-async'; import { HelmetProvider } from 'react-helmet-async';
import { AppRouter } from './AppRouter'; import { AppRouter } from './AppRouter';
import { DialogProvider } from './DialogContext'; import { DialogProvider } from './DialogContext';
import { TauriListeners } from './TauriListeners';
const queryClient = new QueryClient({ const queryClient = new QueryClient({
logger: undefined, logger: undefined,
@@ -28,7 +27,6 @@ export function App() {
<DialogProvider> <DialogProvider>
<Suspense> <Suspense>
<AppRouter /> <AppRouter />
<TauriListeners />
{/*<ReactQueryDevtools initialIsOpen={false} />*/} {/*<ReactQueryDevtools initialIsOpen={false} />*/}
</Suspense> </Suspense>
</DialogProvider> </DialogProvider>

View File

@@ -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 { routePaths } from '../hooks/useRoutes';
import RouteError from './RouteError'; import RouteError from './RouteError';
import { TauriListeners } from './TauriListeners';
import Workspace from './Workspace'; import Workspace from './Workspace';
import Workspaces from './Workspaces'; import Workspaces from './Workspaces';
@@ -8,6 +9,12 @@ const router = createBrowserRouter([
{ {
path: '/', path: '/',
errorElement: <RouteError />, errorElement: <RouteError />,
element: (
<>
<Outlet />
<TauriListeners />
</>
),
children: [ children: [
{ {
path: '/', path: '/',

View File

@@ -24,7 +24,7 @@ export function RequestActionsDropdown({ requestId, children }: Props) {
label: 'Duplicate', label: 'Duplicate',
onSelect: duplicateRequest.mutate, onSelect: duplicateRequest.mutate,
leftSlot: <Icon icon="copy" />, leftSlot: <Icon icon="copy" />,
rightSlot: <HotKey>D</HotKey>, rightSlot: <HotKey modifier="Meta" keyName="D" />,
}, },
{ {
label: 'Delete', label: 'Delete',

View File

@@ -40,7 +40,7 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro
const activeResponse: HttpResponse | null = pinnedResponseId const activeResponse: HttpResponse | null = pinnedResponseId
? responses.find((r) => r.id === pinnedResponseId) ?? null ? responses.find((r) => r.id === pinnedResponseId) ?? null
: responses[responses.length - 1] ?? 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 deleteResponse = useDeleteResponse(activeResponse?.id ?? null);
const deleteAllResponses = useDeleteResponses(activeResponse?.requestId); const deleteAllResponses = useDeleteResponses(activeResponse?.requestId);
const [activeTab, setActiveTab] = useActiveTab(); const [activeTab, setActiveTab] = useActiveTab();
@@ -62,7 +62,7 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro
label: 'Preview', label: 'Preview',
options: { options: {
value: viewMode, value: viewMode,
onChange: toggleViewMode, onChange: setViewMode,
items: [ items: [
{ label: 'Pretty', value: 'pretty' }, { label: 'Pretty', value: 'pretty' },
{ label: 'Raw', value: 'raw' }, { label: 'Raw', value: 'raw' },
@@ -81,7 +81,7 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro
value: 'headers', value: 'headers',
}, },
], ],
[activeResponse?.headers, toggleViewMode, viewMode], [activeResponse?.headers, setViewMode, viewMode],
); );
// Don't render until we know the view mode // Don't render until we know the view mode

View File

@@ -3,7 +3,8 @@ import type { ForwardedRef, KeyboardEvent } from 'react';
import React, { forwardRef, Fragment, memo, useCallback, useMemo, useRef, useState } from 'react'; import React, { forwardRef, Fragment, memo, useCallback, useMemo, useRef, useState } from 'react';
import type { XYCoord } from 'react-dnd'; import type { XYCoord } from 'react-dnd';
import { useDrag, useDrop } 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 { useDeleteRequest } from '../hooks/useDeleteRequest';
import { useLatestResponse } from '../hooks/useLatestResponse'; import { useLatestResponse } from '../hooks/useLatestResponse';
import { useRequests } from '../hooks/useRequests'; import { useRequests } from '../hooks/useRequests';
@@ -11,7 +12,6 @@ import { useUpdateAnyRequest } from '../hooks/useUpdateAnyRequest';
import { useUpdateRequest } from '../hooks/useUpdateRequest'; import { useUpdateRequest } from '../hooks/useUpdateRequest';
import type { HttpRequest } from '../lib/models'; import type { HttpRequest } from '../lib/models';
import { isResponseLoading } from '../lib/models'; import { isResponseLoading } from '../lib/models';
import { Button } from './core/Button';
import { Icon } from './core/Icon'; import { Icon } from './core/Icon';
import { VStack } from './core/Stacks'; import { VStack } from './core/Stacks';
import { StatusTag } from './core/StatusTag'; import { StatusTag } from './core/StatusTag';
@@ -28,7 +28,6 @@ enum ItemTypes {
export const Sidebar = memo(function Sidebar({ className }: Props) { export const Sidebar = memo(function Sidebar({ className }: Props) {
const sidebarRef = useRef<HTMLDivElement>(null); const sidebarRef = useRef<HTMLDivElement>(null);
const unorderedRequests = useRequests(); const unorderedRequests = useRequests();
const activeRequest = useActiveRequest();
const requests = useMemo( const requests = useMemo(
() => [...unorderedRequests].sort((a, b) => a.sortPriority - b.sortPriority), () => [...unorderedRequests].sort((a, b) => a.sortPriority - b.sortPriority),
[unorderedRequests], [unorderedRequests],
@@ -45,20 +44,14 @@ export const Sidebar = memo(function Sidebar({ className }: Props) {
className="relative py-3 overflow-y-auto overflow-x-visible" className="relative py-3 overflow-y-auto overflow-x-visible"
draggable={false} draggable={false}
> >
<SidebarItems activeRequestId={activeRequest?.id} requests={requests} /> <SidebarItems requests={requests} />
</VStack> </VStack>
</div> </div>
</div> </div>
); );
}); });
function SidebarItems({ function SidebarItems({ requests }: { requests: HttpRequest[] }) {
requests,
activeRequestId,
}: {
requests: HttpRequest[];
activeRequestId?: string;
}) {
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null); const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
const updateRequest = useUpdateAnyRequest(); const updateRequest = useUpdateAnyRequest();
@@ -112,7 +105,6 @@ function SidebarItems({
requestId={r.id} requestId={r.id}
requestName={r.name} requestName={r.name}
workspaceId={r.workspaceId} workspaceId={r.workspaceId}
active={r.id === activeRequestId}
onMove={handleMove} onMove={handleMove}
onEnd={handleEnd} onEnd={handleEnd}
/> />
@@ -128,17 +120,18 @@ type SidebarItemProps = {
requestId: string; requestId: string;
requestName: string; requestName: string;
workspaceId: string; workspaceId: string;
active?: boolean;
}; };
const _SidebarItem = forwardRef(function SidebarItem( const _SidebarItem = forwardRef(function SidebarItem(
{ className, requestName, requestId, workspaceId, active }: SidebarItemProps, { className, requestName, requestId, workspaceId }: SidebarItemProps,
ref: ForwardedRef<HTMLLIElement>, ref: ForwardedRef<HTMLLIElement>,
) { ) {
const latestResponse = useLatestResponse(requestId); const latestResponse = useLatestResponse(requestId);
const updateRequest = useUpdateRequest(requestId); const updateRequest = useUpdateRequest(requestId);
const deleteRequest = useDeleteRequest(requestId); const deleteRequest = useDeleteRequest(requestId);
const [editing, setEditing] = useState<boolean>(false); const [editing, setEditing] = useState<boolean>(false);
const activeRequestId = useActiveRequestId();
const isActive = activeRequestId === requestId;
const handleSubmitNameEdit = useCallback( const handleSubmitNameEdit = useCallback(
async (el: HTMLInputElement) => { async (el: HTMLInputElement) => {
@@ -156,16 +149,16 @@ const _SidebarItem = forwardRef(function SidebarItem(
const handleKeyDown = useCallback( const handleKeyDown = useCallback(
(e: KeyboardEvent<HTMLElement>) => { (e: KeyboardEvent<HTMLElement>) => {
// Hitting enter on active request during keyboard nav will start edit // Hitting enter on active request during keyboard nav will start edit
if (active && e.key === 'Enter') { if (isActive && e.key === 'Enter') {
e.preventDefault(); e.preventDefault();
setEditing(true); setEditing(true);
} }
if (active && (e.key === 'Backspace' || e.key === 'Delete')) { if (isActive && (e.key === 'Backspace' || e.key === 'Delete')) {
e.preventDefault(); e.preventDefault();
deleteRequest.mutate(); deleteRequest.mutate();
} }
}, },
[active, deleteRequest], [isActive, deleteRequest],
); );
const handleInputKeyDown = useCallback( const handleInputKeyDown = useCallback(
@@ -185,21 +178,29 @@ const _SidebarItem = forwardRef(function SidebarItem(
[handleSubmitNameEdit], [handleSubmitNameEdit],
); );
const handleStartEditing = useCallback(() => setEditing(true), [setEditing]);
const handleBlur = useCallback(
(e: React.FocusEvent<HTMLInputElement>) => {
handleSubmitNameEdit(e.currentTarget).catch(console.error);
},
[handleSubmitNameEdit],
);
return ( return (
<li ref={ref} className={classnames(className, 'block group/item px-2 pb-0.5')}> <li ref={ref} className={classnames(className, 'block group/item px-2 pb-0.5')}>
<div className="relative"> <div className="relative">
<Button <NavLink
tabIndex={0} tabIndex={0}
color="custom" color="custom"
size="xs"
to={`/workspaces/${workspaceId}/requests/${requestId}`} to={`/workspaces/${workspaceId}/requests/${requestId}`}
draggable={false} // Item should drag, not the link draggable={false} // Item should drag, not the link
onDoubleClick={() => setEditing(true)} onDoubleClick={handleStartEditing}
justify="start"
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
className={classnames( className={classnames(
'flex items-center text-sm h-xs px-2 rounded-md',
editing && 'ring-1 focus-within:ring-focus', editing && 'ring-1 focus-within:ring-focus',
active isActive
? 'bg-highlight text-gray-900' ? 'bg-highlight text-gray-900'
: 'text-gray-600 group-hover/item:text-gray-800 active:bg-highlightSecondary', : 'text-gray-600 group-hover/item:text-gray-800 active:bg-highlightSecondary',
)} )}
@@ -209,7 +210,7 @@ const _SidebarItem = forwardRef(function SidebarItem(
ref={handleFocus} ref={handleFocus}
defaultValue={requestName} defaultValue={requestName}
className="bg-transparent outline-none w-full" className="bg-transparent outline-none w-full"
onBlur={(e) => handleSubmitNameEdit(e.currentTarget)} onBlur={handleBlur}
onKeyDown={handleInputKeyDown} onKeyDown={handleInputKeyDown}
/> />
) : ( ) : (
@@ -226,7 +227,7 @@ const _SidebarItem = forwardRef(function SidebarItem(
)} )}
</div> </div>
)} )}
</Button> </NavLink>
</div> </div>
</li> </li>
); );
@@ -248,7 +249,6 @@ const DraggableSidebarItem = memo(function DraggableSidebarItem({
requestName, requestName,
requestId, requestId,
workspaceId, workspaceId,
active,
onMove, onMove,
onEnd, onEnd,
}: DraggableSidebarItemProps) { }: DraggableSidebarItemProps) {
@@ -290,7 +290,6 @@ const DraggableSidebarItem = memo(function DraggableSidebarItem({
requestName={requestName} requestName={requestName}
requestId={requestId} requestId={requestId}
workspaceId={workspaceId} workspaceId={workspaceId}
active={active}
/> />
); );
}); });

View File

@@ -1,26 +1,20 @@
import { memo, useCallback } from 'react'; import { memo, useCallback } from 'react';
import { useActiveRequestId } from '../hooks/useActiveRequestId';
import { useCreateRequest } from '../hooks/useCreateRequest'; import { useCreateRequest } from '../hooks/useCreateRequest';
import { useDuplicateRequest } from '../hooks/useDuplicateRequest';
import { useSidebarHidden } from '../hooks/useSidebarHidden'; import { useSidebarHidden } from '../hooks/useSidebarHidden';
import { useTauriEvent } from '../hooks/useTauriEvent'; import { useTauriEvent } from '../hooks/useTauriEvent';
import { IconButton } from './core/IconButton'; import { IconButton } from './core/IconButton';
export const SidebarActions = memo(function SidebarDisplayToggle() { export const SidebarActions = memo(function SidebarActions() {
const { hidden, toggle } = useSidebarHidden(); const { hidden, toggle } = useSidebarHidden();
const activeRequestId = useActiveRequestId();
const createRequest = useCreateRequest({ navigateAfter: true }); const createRequest = useCreateRequest({ navigateAfter: true });
const duplicateRequest = useDuplicateRequest({ id: activeRequestId, navigateAfter: true });
const handleCreateRequest = useCallback(() => { const handleCreateRequest = useCallback(() => {
createRequest.mutate({}); createRequest.mutate({});
}, [createRequest]); }, [createRequest]);
useTauriEvent('new_request', () => { useTauriEvent('new_request', () => {
createRequest.mutate({}); createRequest.mutate({});
}); });
// TODO: Put this somewhere better
useTauriEvent('duplicate_request', () => {
duplicateRequest.mutate();
});
return ( return (
<> <>

View File

@@ -1,5 +1,7 @@
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import { appWindow } from '@tauri-apps/api/window'; import { appWindow } from '@tauri-apps/api/window';
import { useActiveRequestId } from '../hooks/useActiveRequestId';
import { useDuplicateRequest } from '../hooks/useDuplicateRequest';
import { keyValueQueryKey } from '../hooks/useKeyValue'; import { keyValueQueryKey } from '../hooks/useKeyValue';
import { requestsQueryKey } from '../hooks/useRequests'; import { requestsQueryKey } from '../hooks/useRequests';
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey'; import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
@@ -15,6 +17,14 @@ export function TauriListeners() {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const { wasUpdatedExternally } = useRequestUpdateKey(null); 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<Model>('created_model', ({ payload, windowLabel }) => { useTauriEvent<Model>('created_model', ({ payload, windowLabel }) => {
if (shouldIgnoreEvent(payload, windowLabel)) return; if (shouldIgnoreEvent(payload, windowLabel)) return;

View File

@@ -1,15 +1,21 @@
import classnames from 'classnames'; import classnames from 'classnames';
import type { HTMLAttributes } from 'react';
export function HotKey({ children }: HTMLAttributes<HTMLSpanElement>) { interface Props {
modifier: 'Meta' | 'Control' | 'Shift';
keyName: string;
}
const keys: Record<Props['modifier'], string> = {
Control: '⌃',
Meta: '⌘',
Shift: '⇧',
};
export function HotKey({ modifier, keyName }: Props) {
return ( return (
<span <span className={classnames('text-sm text-gray-600')}>
className={classnames( {keys[modifier]}
'bg-highlightSecondary bg-opacity-20 px-1.5 py-0.5 rounded text-sm', {keyName}
'font-mono text-gray-500 tracking-widest',
)}
>
{children}
</span> </span>
); );
} }

View File

@@ -1,6 +1,6 @@
import classnames from 'classnames'; import classnames from 'classnames';
import type { MouseEvent } from 'react'; import type { MouseEvent } from 'react';
import { forwardRef, memo, useCallback } from 'react'; import { forwardRef, useCallback } from 'react';
import { useTimedBoolean } from '../../hooks/useTimedBoolean'; import { useTimedBoolean } from '../../hooks/useTimedBoolean';
import type { ButtonProps } from './Button'; import type { ButtonProps } from './Button';
import { Button } from './Button'; import { Button } from './Button';
@@ -15,7 +15,7 @@ type Props = IconProps &
title: string; title: string;
}; };
const _IconButton = forwardRef<HTMLButtonElement, Props>(function IconButton( export const IconButton = forwardRef<HTMLButtonElement, Props>(function IconButton(
{ {
showConfirm, showConfirm,
icon, icon,
@@ -69,5 +69,3 @@ const _IconButton = forwardRef<HTMLButtonElement, Props>(function IconButton(
</Button> </Button>
); );
}); });
export const IconButton = memo(_IconButton);

View File

@@ -3,16 +3,12 @@ import { invoke } from '@tauri-apps/api';
import { InlineCode } from '../components/core/InlineCode'; import { InlineCode } from '../components/core/InlineCode';
import type { HttpRequest } from '../lib/models'; import type { HttpRequest } from '../lib/models';
import { getRequest } from '../lib/store'; import { getRequest } from '../lib/store';
import { useActiveRequestId } from './useActiveRequestId';
import { useConfirm } from './useConfirm'; import { useConfirm } from './useConfirm';
import { requestsQueryKey } from './useRequests'; import { requestsQueryKey } from './useRequests';
import { responsesQueryKey } from './useResponses'; import { responsesQueryKey } from './useResponses';
import { useRoutes } from './useRoutes';
export function useDeleteRequest(id: string | null) { export function useDeleteRequest(id: string | null) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const activeRequestId = useActiveRequestId();
const routes = useRoutes();
const confirm = useConfirm(); const confirm = useConfirm();
return useMutation<HttpRequest | null, string>({ return useMutation<HttpRequest | null, string>({
@@ -39,9 +35,6 @@ export function useDeleteRequest(id: string | null) {
queryClient.setQueryData<HttpRequest[]>(requestsQueryKey({ workspaceId }), (requests) => queryClient.setQueryData<HttpRequest[]>(requestsQueryKey({ workspaceId }), (requests) =>
(requests ?? []).filter((r) => r.id !== requestId), (requests ?? []).filter((r) => r.id !== requestId),
); );
if (activeRequestId === requestId) {
routes.navigate('workspace', { workspaceId });
}
}, },
}); });
} }

View File

@@ -1,15 +1,11 @@
import { useKeyValue } from './useKeyValue'; import { useLocalStorage } from 'react-use';
export function useResponseViewMode(requestId?: string): [string | undefined, () => void] { export function useResponseViewMode(
const v = useKeyValue<string>({ requestId?: string,
namespace: 'app', ): [string | undefined, (m: 'pretty' | 'raw') => void] {
key: ['response_view_mode', requestId ?? 'n/a'], const [value, setValue] = useLocalStorage<'pretty' | 'raw'>(
defaultValue: 'pretty', `response_view_mode::${requestId}`,
}); 'pretty',
);
const toggle = () => { return [value, setValue];
v.set(v.value === 'pretty' ? 'raw' : 'pretty');
};
return [v.value, toggle];
} }

View File

@@ -1,3 +1,4 @@
import { useMemo } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
export type RouteParamsWorkspace = { export type RouteParamsWorkspace = {
@@ -27,17 +28,20 @@ export const routePaths = {
export function useRoutes() { export function useRoutes() {
const navigate = useNavigate(); const navigate = useNavigate();
return { return useMemo(
navigate<T extends keyof typeof routePaths>( () => ({
path: T, navigate<T extends keyof typeof routePaths>(
...params: Parameters<(typeof routePaths)[T]> path: T,
) { ...params: Parameters<(typeof routePaths)[T]>
// Not sure how to make TS work here, but it's good from the ) {
// outside caller perspective. // Not sure how to make TS work here, but it's good from the
// eslint-disable-next-line @typescript-eslint/no-explicit-any // outside caller perspective.
const resolvedPath = routePaths[path](...(params as any)); // eslint-disable-next-line @typescript-eslint/no-explicit-any
navigate(resolvedPath); const resolvedPath = routePaths[path](...(params as any));
}, navigate(resolvedPath);
paths: routePaths, },
}; paths: routePaths,
}),
[navigate],
);
} }

View File

@@ -5,6 +5,6 @@ export function useSidebarWidth() {
return useKeyValue<number>({ return useKeyValue<number>({
namespace: NAMESPACE_NO_SYNC, namespace: NAMESPACE_NO_SYNC,
key: 'sidebar_width', key: 'sidebar_width',
defaultValue: 200, defaultValue: 220,
}); });
} }

View File

@@ -33,20 +33,24 @@
} }
/* Style the scrollbars */ /* Style the scrollbars */
::-webkit-scrollbar-corner, * {
::-webkit-scrollbar { ::-webkit-scrollbar-corner,
@apply w-1.5 h-1.5; ::-webkit-scrollbar {
} @apply w-1.5 h-1.5;
}
.scrollbar-track, .scrollbar-track,
::-webkit-scrollbar-corner, ::-webkit-scrollbar-corner,
::-webkit-scrollbar { ::-webkit-scrollbar {
@apply bg-transparent; @apply bg-transparent;
} }
.scrollbar-thumb, &:hover {
::-webkit-scrollbar-thumb { &.scrollbar-thumb,
@apply bg-gray-500/30 hover:bg-gray-500/50 rounded-full; &::-webkit-scrollbar-thumb {
@apply bg-gray-500/30 hover:bg-gray-500/50 rounded-full;
}
}
} }
iframe { iframe {