Typesafe routing and CM line height issue

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

View File

@@ -313,14 +313,13 @@ async fn delete_request(
app_handle: AppHandle<Wry>, app_handle: AppHandle<Wry>,
db_instance: State<'_, Mutex<Pool<Sqlite>>>, db_instance: State<'_, Mutex<Pool<Sqlite>>>,
request_id: &str, request_id: &str,
) -> Result<models::HttpRequest, String> { ) -> Result<(), String> {
let pool = &*db_instance.lock().await; let pool = &*db_instance.lock().await;
let req = models::delete_request(request_id, pool) let req = models::delete_request(request_id, pool)
.await .await
.expect("Failed to delete request"); .expect("Failed to delete request");
app_handle.emit_all("deleted_request", request_id).unwrap(); app_handle.emit_all("deleted_model", req).unwrap();
Ok(())
Ok(req)
} }
#[tauri::command] #[tauri::command]
@@ -357,12 +356,15 @@ async fn responses(
#[tauri::command] #[tauri::command]
async fn delete_response( async fn delete_response(
id: &str, id: &str,
app_handle: AppHandle<Wry>,
db_instance: State<'_, Mutex<Pool<Sqlite>>>, db_instance: State<'_, Mutex<Pool<Sqlite>>>,
) -> Result<(), String> { ) -> Result<(), String> {
let pool = &*db_instance.lock().await; let pool = &*db_instance.lock().await;
models::delete_response(id, pool) let response = models::delete_response(id, pool)
.await .await
.map_err(|e| e.to_string()) .expect("Failed to delete response");
app_handle.emit_all("deleted_model", response).unwrap();
Ok(())
} }
#[tauri::command] #[tauri::command]
@@ -394,6 +396,20 @@ async fn workspaces(
} }
} }
#[tauri::command]
async fn delete_workspace(
app_handle: AppHandle<Wry>,
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
id: &str,
) -> Result<(), String> {
let pool = &*db_instance.lock().await;
let workspace = models::delete_workspace(id, pool)
.await
.expect("Failed to delete workspace");
app_handle.emit_all("deleted_model", workspace).unwrap();
Ok(())
}
#[tauri::command] #[tauri::command]
fn greet(name: &str) -> String { fn greet(name: &str) -> String {
format!("Hello, {}! You've been greeted from Rust!", name) format!("Hello, {}! You've been greeted from Rust!", name)
@@ -511,6 +527,7 @@ fn main() {
send_request, send_request,
create_request, create_request,
create_workspace, create_workspace,
delete_workspace,
update_request, update_request,
delete_request, delete_request,
responses, responses,

View File

@@ -142,6 +142,22 @@ pub async fn get_workspace(id: &str, pool: &Pool<Sqlite>) -> Result<Workspace, s
.await .await
} }
pub async fn delete_workspace(id: &str, pool: &Pool<Sqlite>) -> Result<Workspace, sqlx::Error> {
let workspace = get_workspace(id, pool)
.await
.expect("Failed to get request to delete");
let _ = sqlx::query!(
r#"
DELETE FROM http_requests
WHERE id = ?
"#,
id,
)
.execute(pool)
.await;
Ok(workspace)
}
pub async fn create_workspace( pub async fn create_workspace(
name: &str, name: &str,
description: &str, description: &str,
@@ -395,7 +411,11 @@ pub async fn find_responses(
.await .await
} }
pub async fn delete_response(id: &str, pool: &Pool<Sqlite>) -> Result<(), sqlx::Error> { pub async fn delete_response(id: &str, pool: &Pool<Sqlite>) -> Result<HttpResponse, sqlx::Error> {
let resp = get_response(id, pool)
.await
.expect("Failed to get response to delete");
let _ = sqlx::query!( let _ = sqlx::query!(
r#" r#"
DELETE FROM http_responses DELETE FROM http_responses
@@ -406,7 +426,7 @@ pub async fn delete_response(id: &str, pool: &Pool<Sqlite>) -> Result<(), sqlx::
.execute(pool) .execute(pool)
.await; .await;
Ok(()) Ok(resp)
} }
pub async fn delete_all_responses( pub async fn delete_all_responses(

View File

@@ -10,12 +10,13 @@ import { matchPath } from 'react-router-dom';
import { keyValueQueryKey } from '../hooks/useKeyValue'; import { keyValueQueryKey } from '../hooks/useKeyValue';
import { requestsQueryKey } from '../hooks/useRequests'; import { requestsQueryKey } from '../hooks/useRequests';
import { responsesQueryKey } from '../hooks/useResponses'; import { responsesQueryKey } from '../hooks/useResponses';
import { routePaths } from '../hooks/useRoutes';
import { workspacesQueryKey } from '../hooks/useWorkspaces'; import { workspacesQueryKey } from '../hooks/useWorkspaces';
import { DEFAULT_FONT_SIZE } from '../lib/constants'; import { DEFAULT_FONT_SIZE } from '../lib/constants';
import { extractKeyValue } from '../lib/keyValueStore'; import { extractKeyValue } from '../lib/keyValueStore';
import type { HttpRequest, HttpResponse, KeyValue, Workspace } from '../lib/models'; import type { HttpRequest, HttpResponse, KeyValue, Workspace } from '../lib/models';
import { convertDates } from '../lib/models'; import { convertDates } from '../lib/models';
import { AppRouter, WORKSPACE_REQUEST_PATH } from './AppRouter'; import { AppRouter } from './AppRouter';
const queryClient = new QueryClient(); 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 }) => { await listen('updated_response', ({ payload: response }: { payload: HttpResponse }) => {
queryClient.setQueryData( queryClient.setQueryData(
responsesQueryKey(response.requestId), 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 () => { 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; const requestId = params?.params.requestId;
if (typeof requestId !== 'string') { if (typeof requestId !== 'string') {
return; return;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -46,7 +46,7 @@ export function Input({
const id = `input-${name}`; const id = `input-${name}`;
const inputClassName = classnames( const inputClassName = classnames(
className, 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', !!leftSlot && '!pl-0.5',
!!rightSlot && '!pr-0.5', !!rightSlot && '!pr-0.5',
); );
@@ -81,8 +81,8 @@ export function Input({
'relative w-full rounded-md text-gray-900', 'relative w-full rounded-md text-gray-900',
'border border-gray-200 focus-within:border-focus', 'border border-gray-200 focus-within:border-focus',
!isValid && '!border-invalid', !isValid && '!border-invalid',
size === 'md' && 'h-md', size === 'md' && 'h-md leading-md',
size === 'sm' && 'h-sm', size === 'sm' && 'h-sm leading-sm',
)} )}
> >
{leftSlot} {leftSlot}

View File

@@ -1,6 +1,7 @@
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import type { RouteParamsRequest } from './useRoutes';
export function useActiveRequestId(): string | null { export function useActiveRequestId(): string | null {
const { requestId } = useParams<{ requestId?: string }>(); const { requestId } = useParams<RouteParamsRequest>();
return requestId ?? null; return requestId ?? null;
} }

View File

@@ -1,6 +1,7 @@
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import type { RouteParamsWorkspace } from './useRoutes';
export function useActiveWorkspaceId(): string | null { export function useActiveWorkspaceId(): string | null {
const { workspaceId } = useParams<{ workspaceId?: string }>(); const { workspaceId } = useParams<RouteParamsWorkspace>();
return workspaceId ?? null; return workspaceId ?? null;
} }

View File

@@ -1,17 +1,17 @@
import { useMutation } from '@tanstack/react-query'; import { useMutation } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api'; import { invoke } from '@tauri-apps/api';
import { useNavigate } from 'react-router-dom';
import type { Workspace } from '../lib/models'; import type { Workspace } from '../lib/models';
import { useRoutes } from './useRoutes';
export function useCreateWorkspace({ navigateAfter }: { navigateAfter: boolean }) { export function useCreateWorkspace({ navigateAfter }: { navigateAfter: boolean }) {
const navigate = useNavigate(); const routes = useRoutes();
return useMutation<string, unknown, Pick<Workspace, 'name'>>({ return useMutation<string, unknown, Pick<Workspace, 'name'>>({
mutationFn: (patch) => { mutationFn: (patch) => {
return invoke('create_workspace', patch); return invoke('create_workspace', patch);
}, },
onSuccess: async (workspaceId) => { onSuccess: async (workspaceId) => {
if (navigateAfter) { if (navigateAfter) {
navigate(`/workspaces/${workspaceId}`); routes.navigate('workspace', { workspaceId });
} }
}, },
}); });

View File

@@ -0,0 +1,11 @@
import { useMutation } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api';
export function useDeleteResponse(id: string | null) {
return useMutation({
mutationFn: async () => {
if (id === null) return;
await invoke('delete_response', { id: id });
},
});
}

View File

@@ -9,9 +9,9 @@ export function useDeleteResponses(requestId?: string) {
if (!requestId) return; if (!requestId) return;
await invoke('delete_all_responses', { requestId }); await invoke('delete_all_responses', { requestId });
}, },
onSuccess: () => { onSuccess: async () => {
if (!requestId) return; if (!requestId) return;
queryClient.setQueryData(responsesQueryKey(requestId), []); await queryClient.invalidateQueries(responsesQueryKey(requestId));
}, },
}); });
} }

View File

@@ -0,0 +1,25 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api';
import { useNavigate } from 'react-router-dom';
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
import { useRoutes } from './useRoutes';
import { workspacesQueryKey } from './useWorkspaces';
export function useDeleteWorkspace(id: string | null) {
const queryClient = useQueryClient();
const activeWorkspaceId = useActiveWorkspaceId();
const routes = useRoutes();
return useMutation<void, string>({
mutationFn: async () => {
if (id === null) return;
await invoke('delete_workspace', { id });
},
onSuccess: async () => {
if (id === null) return;
await queryClient.invalidateQueries(workspacesQueryKey());
if (id === activeWorkspaceId) {
routes.navigate('workspace', { workspaceId: id });
}
},
});
}

View File

@@ -1,21 +0,0 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api';
import type { HttpResponse } from '../lib/models';
import { responsesQueryKey } from './useResponses';
export function useDeleteResponse(response: HttpResponse | null) {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async () => {
if (response === null) return;
await invoke('delete_response', { id: response.id });
},
onSuccess: () => {
if (response === null) return;
queryClient.setQueryData(
responsesQueryKey(response.requestId),
(responses: HttpResponse[] = []) => responses.filter((r) => r.id !== response.id),
);
},
});
}

View File

@@ -0,0 +1,39 @@
export type RouteParamsWorkspace = {
workspaceId: string;
};
export type RouteParamsRequest = RouteParamsWorkspace & {
requestId: string;
};
export const routePaths = {
workspaces() {
return '/workspaces';
},
workspace({ workspaceId } = { workspaceId: ':workspaceId' } as RouteParamsWorkspace) {
return `/workspaces/${workspaceId}`;
},
request(
{ workspaceId, requestId } = {
workspaceId: ':workspaceId',
requestId: ':requestId',
} as RouteParamsRequest,
) {
return `${this.workspace({ workspaceId })}/requests/${requestId}`;
},
};
export function useRoutes() {
return {
navigate<T extends keyof typeof routePaths>(
path: T,
params: Parameters<(typeof routePaths)[T]>[0],
) {
// 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
routePaths[path](params as any);
},
paths: routePaths,
};
}

View File

@@ -1,6 +1,5 @@
export interface BaseModel { export interface BaseModel {
readonly id: string; readonly id: string;
readonly workspaceId: string;
readonly createdAt: Date; readonly createdAt: Date;
readonly updatedAt: Date; readonly updatedAt: Date;
} }
@@ -18,6 +17,7 @@ export interface HttpHeader {
} }
export interface HttpRequest extends BaseModel { export interface HttpRequest extends BaseModel {
readonly workspaceId: string;
readonly model: 'http_request'; readonly model: 'http_request';
sortPriority: number; sortPriority: number;
name: string; name: string;
@@ -36,6 +36,7 @@ export interface KeyValue extends Omit<BaseModel, 'id'> {
} }
export interface HttpResponse extends BaseModel { export interface HttpResponse extends BaseModel {
readonly workspaceId: string;
readonly model: 'http_response'; readonly model: 'http_response';
readonly requestId: string; readonly requestId: string;
readonly body: string; readonly body: string;

View File

@@ -13,7 +13,11 @@ module.exports = {
height: { height: {
'sm': '2rem', 'sm': '2rem',
'md': '2.5rem', 'md': '2.5rem',
} },
lineHeight: {
'sm': '2rem',
'md': '2.5rem',
},
}, },
fontFamily: { fontFamily: {
"mono": ["JetBrains Mono", "Menlo", "monospace"], "mono": ["JetBrains Mono", "Menlo", "monospace"],