Typesafe routing and CM line height issue

This commit is contained in:
Gregory Schier
2023-03-20 16:47:36 -07:00
parent c5ca3daab3
commit f855d8ab16
20 changed files with 202 additions and 77 deletions

View File

@@ -10,12 +10,13 @@ import { matchPath } from 'react-router-dom';
import { keyValueQueryKey } from '../hooks/useKeyValue';
import { requestsQueryKey } from '../hooks/useRequests';
import { responsesQueryKey } from '../hooks/useResponses';
import { routePaths } from '../hooks/useRoutes';
import { workspacesQueryKey } from '../hooks/useWorkspaces';
import { DEFAULT_FONT_SIZE } from '../lib/constants';
import { extractKeyValue } from '../lib/keyValueStore';
import type { HttpRequest, HttpResponse, KeyValue, Workspace } from '../lib/models';
import { convertDates } from '../lib/models';
import { AppRouter, WORKSPACE_REQUEST_PATH } from './AppRouter';
import { AppRouter } from './AppRouter';
const queryClient = new QueryClient();
@@ -45,12 +46,6 @@ await listen('updated_request', ({ payload: request }: { payload: HttpRequest })
);
});
await listen('deleted_request', ({ payload: request }: { payload: HttpRequest }) => {
queryClient.setQueryData(requestsQueryKey(request.workspaceId), (requests: HttpRequest[] = []) =>
requests.filter((r) => r.id !== request.id),
);
});
await listen('updated_response', ({ payload: response }: { payload: HttpResponse }) => {
queryClient.setQueryData(
responsesQueryKey(response.requestId),
@@ -92,8 +87,27 @@ await listen('updated_workspace', ({ payload: workspace }: { payload: Workspace
});
});
await listen(
'deleted_model',
({ payload: model }: { payload: Workspace | HttpRequest | HttpResponse | KeyValue }) => {
function removeById<T extends { id: string }>(model: T) {
return (entries: T[] | undefined) => entries?.filter((e) => e.id !== model.id);
}
if (model.model === 'workspace') {
queryClient.setQueryData(workspacesQueryKey(), removeById<Workspace>(model));
} else if (model.model === 'http_request') {
queryClient.setQueryData(requestsQueryKey(model.workspaceId), removeById<HttpRequest>(model));
} else if (model.model === 'http_response') {
queryClient.setQueryData(responsesQueryKey(model.requestId), removeById<HttpResponse>(model));
} else if (model.model === 'key_value') {
queryClient.setQueryData(keyValueQueryKey(model), undefined);
}
},
);
await listen('send_request', async () => {
const params = matchPath(WORKSPACE_REQUEST_PATH, window.location.pathname);
const params = matchPath(routePaths.request(), window.location.pathname);
const requestId = params?.params.requestId;
if (typeof requestId !== 'string') {
return;

View File

@@ -1,13 +1,11 @@
import { lazy, Suspense } from 'react';
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
import { createBrowserRouter, Navigate, RouterProvider } from 'react-router-dom';
import { routePaths } from '../hooks/useRoutes';
const Workspaces = lazy(() => import('./Workspaces'));
const Workspace = lazy(() => import('./Workspace'));
const RouteError = lazy(() => import('./RouteError'));
export const WORKSPACE_PATH = '/workspaces/:workspaceId';
export const WORKSPACE_REQUEST_PATH = '/workspaces/:workspaceId/requests/:requestId';
const router = createBrowserRouter([
{
path: '/',
@@ -15,14 +13,21 @@ const router = createBrowserRouter([
children: [
{
path: '/',
element: <Navigate to={routePaths.workspaces()} replace={true} />,
},
{
path: routePaths.workspaces(),
element: <Workspaces />,
},
{
path: WORKSPACE_PATH,
path: routePaths.workspace({ workspaceId: ':workspaceId' }),
element: <Workspace />,
},
{
path: WORKSPACE_REQUEST_PATH,
path: routePaths.request({
workspaceId: ':workspaceId',
requestId: ':requestId',
}),
element: <Workspace />,
},
],

View File

@@ -2,7 +2,7 @@ import classnames from 'classnames';
import { memo, useEffect, useMemo, useState } from 'react';
import { useActiveRequestId } from '../hooks/useActiveRequestId';
import { useDeleteResponses } from '../hooks/useDeleteResponses';
import { useDeleteResponse } from '../hooks/useResponseDelete';
import { useDeleteResponse } from '../hooks/useDeleteResponse';
import { useResponses } from '../hooks/useResponses';
import { useResponseViewMode } from '../hooks/useResponseViewMode';
import { tryFormatJson } from '../lib/formatters';
@@ -21,18 +21,18 @@ interface Props {
}
export const ResponsePane = memo(function ResponsePane({ className }: Props) {
const [activeResponseId, setActiveResponseId] = useState<string | null>(null);
const [pinnedResponseId, setPinnedResponseId] = useState<string | null>(null);
const activeRequestId = useActiveRequestId();
const responses = useResponses(activeRequestId);
const activeResponse: HttpResponse | null = activeResponseId
? responses.find((r) => r.id === activeResponseId) ?? null
const activeResponse: HttpResponse | null = pinnedResponseId
? responses.find((r) => r.id === pinnedResponseId) ?? null
: responses[responses.length - 1] ?? null;
const [viewMode, toggleViewMode] = useResponseViewMode(activeResponse?.requestId);
const deleteResponse = useDeleteResponse(activeResponse);
const deleteResponse = useDeleteResponse(activeResponse?.id ?? null);
const deleteAllResponses = useDeleteResponses(activeResponse?.requestId);
useEffect(() => {
setActiveResponseId(null);
setPinnedResponseId(null);
}, [responses.length]);
const contentType = useMemo(
@@ -92,7 +92,7 @@ export const ResponsePane = memo(function ResponsePane({ className }: Props) {
...responses.slice(0, 10).map((r) => ({
label: r.status + ' - ' + r.elapsed + ' ms',
leftSlot: activeResponse?.id === r.id ? <Icon icon="check" /> : <></>,
onSelect: () => setActiveResponseId(r.id),
onSelect: () => setPinnedResponseId(r.id),
})),
]}
>

View File

@@ -29,7 +29,7 @@ export const UrlBar = memo(function UrlBar({ id: requestId, url, method, classNa
);
return (
<form onSubmit={handleSubmit} className={classnames(className, 'w-full flex items-center')}>
<form onSubmit={handleSubmit} className={className}>
<Input
key={requestId}
hideLabel

View File

@@ -1,9 +1,10 @@
import classnames from 'classnames';
import { memo, useMemo } from 'react';
import { useNavigate } from 'react-router-dom';
import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
import { useActiveWorkspaceId } from '../hooks/useActiveWorkspaceId';
import { useCreateWorkspace } from '../hooks/useCreateWorkspace';
import { useDeleteWorkspace } from '../hooks/useDeleteWorkspace';
import { useRoutes } from '../hooks/useRoutes';
import { useWorkspaces } from '../hooks/useWorkspaces';
import { Button } from './core/Button';
import type { DropdownItem } from './core/Dropdown';
@@ -15,11 +16,12 @@ type Props = {
};
export const WorkspaceDropdown = memo(function WorkspaceDropdown({ className }: Props) {
const navigate = useNavigate();
const routes = useRoutes();
const workspaces = useWorkspaces();
const activeWorkspace = useActiveWorkspace();
const activeWorkspaceId = useActiveWorkspaceId();
const createWorkspace = useCreateWorkspace({ navigateAfter: true });
const deleteWorkspace = useDeleteWorkspace(activeWorkspaceId);
const items: DropdownItem[] = useMemo(() => {
const workspaceItems = workspaces.map((w) => ({
@@ -27,7 +29,7 @@ export const WorkspaceDropdown = memo(function WorkspaceDropdown({ className }:
leftSlot: activeWorkspaceId === w.id ? <Icon icon="check" /> : <Icon icon="empty" />,
onSelect: () => {
if (w.id === activeWorkspaceId) return;
navigate(`/workspaces/${w.id}`);
routes.navigate('workspace', { workspaceId: w.id });
},
}));
@@ -36,10 +38,14 @@ export const WorkspaceDropdown = memo(function WorkspaceDropdown({ className }:
'-----',
{
label: 'New Workspace',
value: 'new',
leftSlot: <Icon icon="plus" />,
onSelect: () => createWorkspace.mutate({ name: 'New Workspace' }),
},
{
label: 'Delete Workspace',
leftSlot: <Icon icon="trash" />,
onSelect: () => deleteWorkspace.mutate(),
},
];
}, [workspaces, activeWorkspaceId]);

View File

@@ -20,10 +20,9 @@ export type DropdownItem =
export interface DropdownProps {
children: ReactElement<HTMLAttributes<HTMLButtonElement>>;
items: DropdownItem[];
ignoreClick?: boolean;
}
export function Dropdown({ children, items, ignoreClick }: DropdownProps) {
export function Dropdown({ children, items }: DropdownProps) {
const [open, setOpen] = useState<boolean>(false);
const ref = useRef<HTMLButtonElement>(null);
const child = useMemo(() => {
@@ -36,7 +35,6 @@ export function Dropdown({ children, items, ignoreClick }: DropdownProps) {
onClick:
existingChild.props?.onClick ??
((e: MouseEvent<HTMLButtonElement>) => {
console.log('CLICK INSIDE');
e.preventDefault();
e.stopPropagation();
setOpen((o) => !o);

View File

@@ -24,6 +24,11 @@
@apply text-placeholder;
}
.cm-scroller {
/* Inherit line-height from outside */
line-height: inherit;
}
/* Don't show selection on blurred input */
.cm-selectionBackground {
@apply bg-transparent;
@@ -54,17 +59,15 @@
&.cm-singleline {
.cm-editor {
@apply h-full w-full;
@apply w-full h-auto;
}
.cm-scroller {
@apply font-mono flex text-[0.8rem];
align-items: center !important;
overflow: hidden !important;
@apply font-mono text-[0.8rem] overflow-hidden;
}
.cm-line {
@apply px-0;
@apply px-2 overflow-hidden;
}
}
@@ -96,11 +99,12 @@
}
.cm-editor .cm-gutterElement {
@apply flex items-center;
transition: color var(--transition-duration);
}
.cm-editor .fold-gutter-icon {
@apply pt-[0.3em] pl-[0.4em] px-[0.4em] h-4 cursor-pointer rounded;
@apply pt-[0.25em] pl-[0.4em] px-[0.4em] h-4 cursor-pointer rounded;
}
.cm-editor .fold-gutter-icon::after {
@@ -109,7 +113,7 @@
}
.cm-editor .fold-gutter-icon[data-open] {
@apply pt-[0.4em] pl-[0.3em];
@apply pt-[0.38em] pl-[0.3em];
}
.cm-editor .fold-gutter-icon[data-open]::after {

View File

@@ -46,7 +46,7 @@ export function Input({
const id = `input-${name}`;
const inputClassName = classnames(
className,
'!bg-transparent pl-3 pr-2 min-w-0 h-full w-full focus:outline-none placeholder:text-placeholder',
'!bg-transparent min-w-0 h-full w-full focus:outline-none placeholder:text-placeholder',
!!leftSlot && '!pl-0.5',
!!rightSlot && '!pr-0.5',
);
@@ -81,8 +81,8 @@ export function Input({
'relative w-full rounded-md text-gray-900',
'border border-gray-200 focus-within:border-focus',
!isValid && '!border-invalid',
size === 'md' && 'h-md',
size === 'sm' && 'h-sm',
size === 'md' && 'h-md leading-md',
size === 'sm' && 'h-sm leading-sm',
)}
>
{leftSlot}