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 { AppRouter } from './AppRouter';
import { DialogProvider } from './DialogContext';
import { TauriListeners } from './TauriListeners';
const queryClient = new QueryClient({
logger: undefined,
@@ -28,7 +27,6 @@ export function App() {
<DialogProvider>
<Suspense>
<AppRouter />
<TauriListeners />
{/*<ReactQueryDevtools initialIsOpen={false} />*/}
</Suspense>
</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 RouteError from './RouteError';
import { TauriListeners } from './TauriListeners';
import Workspace from './Workspace';
import Workspaces from './Workspaces';
@@ -8,6 +9,12 @@ const router = createBrowserRouter([
{
path: '/',
errorElement: <RouteError />,
element: (
<>
<Outlet />
<TauriListeners />
</>
),
children: [
{
path: '/',

View File

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

View File

@@ -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

View File

@@ -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<HTMLDivElement>(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}
>
<SidebarItems activeRequestId={activeRequest?.id} requests={requests} />
<SidebarItems requests={requests} />
</VStack>
</div>
</div>
);
});
function SidebarItems({
requests,
activeRequestId,
}: {
requests: HttpRequest[];
activeRequestId?: string;
}) {
function SidebarItems({ requests }: { requests: HttpRequest[] }) {
const [hoveredIndex, setHoveredIndex] = useState<number | null>(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<HTMLLIElement>,
) {
const latestResponse = useLatestResponse(requestId);
const updateRequest = useUpdateRequest(requestId);
const deleteRequest = useDeleteRequest(requestId);
const [editing, setEditing] = useState<boolean>(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<HTMLElement>) => {
// 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<HTMLInputElement>) => {
handleSubmitNameEdit(e.currentTarget).catch(console.error);
},
[handleSubmitNameEdit],
);
return (
<li ref={ref} className={classnames(className, 'block group/item px-2 pb-0.5')}>
<div className="relative">
<Button
<NavLink
tabIndex={0}
color="custom"
size="xs"
to={`/workspaces/${workspaceId}/requests/${requestId}`}
draggable={false} // Item should drag, not the link
onDoubleClick={() => setEditing(true)}
justify="start"
onDoubleClick={handleStartEditing}
onKeyDown={handleKeyDown}
className={classnames(
'flex items-center text-sm h-xs px-2 rounded-md',
editing && 'ring-1 focus-within:ring-focus',
active
isActive
? 'bg-highlight text-gray-900'
: 'text-gray-600 group-hover/item:text-gray-800 active:bg-highlightSecondary',
)}
@@ -209,7 +210,7 @@ const _SidebarItem = forwardRef(function SidebarItem(
ref={handleFocus}
defaultValue={requestName}
className="bg-transparent outline-none w-full"
onBlur={(e) => handleSubmitNameEdit(e.currentTarget)}
onBlur={handleBlur}
onKeyDown={handleInputKeyDown}
/>
) : (
@@ -226,7 +227,7 @@ const _SidebarItem = forwardRef(function SidebarItem(
)}
</div>
)}
</Button>
</NavLink>
</div>
</li>
);
@@ -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}
/>
);
});

View File

@@ -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 (
<>

View File

@@ -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<Model>('created_model', ({ payload, windowLabel }) => {
if (shouldIgnoreEvent(payload, windowLabel)) return;

View File

@@ -1,15 +1,21 @@
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 (
<span
className={classnames(
'bg-highlightSecondary bg-opacity-20 px-1.5 py-0.5 rounded text-sm',
'font-mono text-gray-500 tracking-widest',
)}
>
{children}
<span className={classnames('text-sm text-gray-600')}>
{keys[modifier]}
{keyName}
</span>
);
}

View File

@@ -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<HTMLButtonElement, Props>(function IconButton(
export const IconButton = forwardRef<HTMLButtonElement, Props>(function IconButton(
{
showConfirm,
icon,
@@ -69,5 +69,3 @@ const _IconButton = forwardRef<HTMLButtonElement, Props>(function IconButton(
</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 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<HttpRequest | null, string>({
@@ -39,9 +35,6 @@ export function useDeleteRequest(id: string | null) {
queryClient.setQueryData<HttpRequest[]>(requestsQueryKey({ workspaceId }), (requests) =>
(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] {
const v = useKeyValue<string>({
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];
}

View File

@@ -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<T extends keyof typeof routePaths>(
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<T extends keyof typeof routePaths>(
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],
);
}

View File

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

View File

@@ -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 {