Compare commits

..

20 Commits

Author SHA1 Message Date
Gregory Schier
10af9b6f99 Minor tweaks 2023-04-04 17:21:02 -07:00
Gregory Schier
aa8c066f2d Fix some things 2023-04-04 16:56:45 -07:00
Gregory Schier
b913b74449 Editor line wrapping support (not used yet) 2023-04-04 16:40:37 -07:00
Gregory Schier
b71adce50b remove janky last location tracking 2023-04-04 16:23:08 -07:00
Gregory Schier
0fbb44c701 Fix resize cursor 2023-04-04 16:12:45 -07:00
Gregory Schier
de335e8637 Better button styles 2023-04-04 15:40:25 -07:00
Gregory Schier
2999f63a4c Bump version 2023-04-04 13:56:24 -07:00
Gregory Schier
2abc5e6f0b Some small fixes 2023-04-04 13:56:14 -07:00
Gregory Schier
639de4321e A few fixes 2023-04-04 13:31:48 -07:00
Gregory Schier
b3c461afdd Better status tags and delete request on key 2023-04-04 12:36:30 -07:00
Gregory Schier
7d154800a0 Remove expects from request sending 2023-04-04 08:14:32 -07:00
Gregory Schier
b48ed0399e Fix web view height 2023-04-04 07:51:41 -07:00
Gregory Schier
c5d6e7d74a Fix autocomplete spacing 2023-04-04 07:51:19 -07:00
Gregory Schier
e82f915363 Fix input focus border 2023-04-03 12:19:37 -07:00
Gregory Schier
3128e9ce76 Hot keys and cleanup 2023-04-03 07:59:49 -07:00
Gregory Schier
bc0e86757c Add entitlemet for v8 2023-04-02 20:23:21 -07:00
Gregory Schier
fec99916c2 Debug codesigned build 2023-04-02 19:09:14 -07:00
Gregory Schier
3b5d059b11 Disable code signing 2023-04-02 18:27:14 -07:00
Gregory Schier
c3fe2acc8a Fix tauri script command 2023-04-02 17:25:24 -07:00
Gregory Schier
4d002c412b Fix universal binary 2023-04-02 17:12:20 -07:00
42 changed files with 393 additions and 360 deletions

View File

@@ -5,19 +5,22 @@ on:
jobs: jobs:
build-artifacts: build-artifacts:
runs-on: ${{ matrix.platform }}
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
# platform: [ ubuntu-latest, macos-latest, windows-latest ] include:
platform: [ macos-latest ] - os: macos-latest
target: aarch64-apple-darwin
- os: macos-latest
target: x86_64-apple-darwin
runs-on: ${{ matrix.os }}
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: dtolnay/rust-toolchain@stable - uses: dtolnay/rust-toolchain@stable
with: with:
targets: 'aarch64-apple-darwin,x86_64-apple-darwin' targets: ${{ matrix.target }}
- name: Cache Rust - name: Cache Rust
uses: actions/cache@v2 uses: actions/cache@v2
with: with:
@@ -31,7 +34,7 @@ jobs:
node-version: 18 node-version: 18
cache: 'npm' cache: 'npm'
- name: install dependencies (ubuntu only) - name: install dependencies (ubuntu only)
if: matrix.platform == 'ubuntu-latest' if: matrix.os == 'ubuntu-latest'
run: | run: |
sudo apt-get update sudo apt-get update
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
@@ -39,7 +42,9 @@ jobs:
run: npm ci run: npm ci
- name: Run tests - name: Run tests
run: npm test run: npm test
- uses: tauri-apps/tauri-action@v0 # Pin dev version to get non-default targets
# https://github.com/tauri-apps/tauri-action/issues/356
- uses: tauri-apps/tauri-action@dev
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
@@ -53,7 +58,7 @@ jobs:
with: with:
tagName: 'v__VERSION__' tagName: 'v__VERSION__'
releaseName: 'Release __VERSION__' releaseName: 'Release __VERSION__'
releaseBody: 'See the assets to download this version and install.' releaseBody: '<!-- Release Notes -->'
releaseDraft: false releaseDraft: true
prerelease: false prerelease: false
args: '--target universal-apple-darwin' args: '--target ${{ matrix.target }}'

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Yaak App</title> <title>Yaak App</title>
<script src="http://localhost:8097"></script> <!-- <script src="http://localhost:8097"></script>-->
<style> <style>
body { body {
background-color: white; background-color: white;

View File

@@ -6,6 +6,7 @@
"scripts": { "scripts": {
"tauri-dev": "YAAK_ENV=development tauri dev", "tauri-dev": "YAAK_ENV=development tauri dev",
"tauri-build": "tauri build", "tauri-build": "tauri build",
"tauri": "tauri",
"build": "npm run build:frontend", "build": "npm run build:frontend",
"dev": "vite dev", "dev": "vite dev",
"lint": "tsc && eslint . --ext .ts,.tsx", "lint": "tsc && eslint . --ext .ts,.tsx",

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
</dict>
</plist>

View File

@@ -79,11 +79,8 @@ async fn actually_send_ephemeral_request(
let start = std::time::Instant::now(); let start = std::time::Instant::now();
let mut url_string = request.url.to_string(); let mut url_string = request.url.to_string();
let mut variables = HashMap::new(); let variables: HashMap<&str, &str> = HashMap::new();
variables.insert("PROJECT_ID", "project_123"); // variables.insert("", "");
variables.insert("TOKEN", "s3cret");
variables.insert("DOMAIN", "schier.co");
variables.insert("BASE_URL", "https://schier.co");
let re = Regex::new(r"\$\{\[\s*([^]\s]+)\s*]}").expect("Failed to create regex"); let re = Regex::new(r"\$\{\[\s*([^]\s]+)\s*]}").expect("Failed to create regex");
url_string = re url_string = re
@@ -104,6 +101,7 @@ async fn actually_send_ephemeral_request(
let client = reqwest::Client::builder() let client = reqwest::Client::builder()
.redirect(Policy::none()) .redirect(Policy::none())
// .danger_accept_invalid_certs(true)
.build() .build()
.expect("Failed to build client"); .expect("Failed to build client");
@@ -188,12 +186,23 @@ async fn actually_send_ephemeral_request(
let raw_response = client.execute(sendable_req).await; let raw_response = client.execute(sendable_req).await;
let p = app_handle let plugin_rel_path = "plugins/plugin.ts";
.path_resolver() let plugin_path = match app_handle.path_resolver().resolve_resource(plugin_rel_path) {
.resolve_resource("plugins/plugin.ts") Some(p) => p,
.expect("failed to resolve resource"); None => {
return response_err(
response,
format!("Plugin not found at {}", plugin_rel_path),
&app_handle,
pool,
)
.await;
}
};
runtime::run_plugin_sync(p.to_str().unwrap()).unwrap(); if let Err(e) = runtime::run_plugin_sync(plugin_path.to_str().unwrap()) {
return response_err(response, e.to_string(), &app_handle, pool).await;
}
match raw_response { match raw_response {
Ok(v) => { Ok(v) => {
@@ -608,6 +617,14 @@ fn create_window(handle: &AppHandle<Wry>) -> Window<Wry> {
.add_item( .add_item(
CustomMenuItem::new("focus_url".to_string(), "Focus URL").accelerator("CmdOrCtrl+l"), CustomMenuItem::new("focus_url".to_string(), "Focus URL").accelerator("CmdOrCtrl+l"),
) )
.add_item(
CustomMenuItem::new("new_request".to_string(), "New Request")
.accelerator("CmdOrCtrl+n"),
)
.add_item(
CustomMenuItem::new("duplicate_request".to_string(), "Duplicate Request")
.accelerator("CmdOrCtrl+d"),
)
.add_item(CustomMenuItem::new("new_window".to_string(), "New Window")); .add_item(CustomMenuItem::new("new_window".to_string(), "New Window"));
if is_dev() { if is_dev() {
test_menu = test_menu test_menu = test_menu
@@ -652,6 +669,8 @@ fn create_window(handle: &AppHandle<Wry>) -> Window<Wry> {
"toggle_sidebar" => win2.emit("toggle_sidebar", true).unwrap(), "toggle_sidebar" => win2.emit("toggle_sidebar", true).unwrap(),
"focus_url" => win2.emit("focus_url", true).unwrap(), "focus_url" => win2.emit("focus_url", true).unwrap(),
"send_request" => win2.emit("send_request", true).unwrap(), "send_request" => win2.emit("send_request", true).unwrap(),
"new_request" => _ = win2.emit("new_request", true).unwrap(),
"duplicate_request" => _ = win2.emit("duplicate_request", true).unwrap(),
"refresh" => win2.eval("location.reload()").unwrap(), "refresh" => win2.eval("location.reload()").unwrap(),
"new_window" => _ = create_window(&handle2), "new_window" => _ = create_window(&handle2),
"toggle_devtools" => { "toggle_devtools" => {

View File

@@ -8,7 +8,7 @@
}, },
"package": { "package": {
"productName": "Yaak", "productName": "Yaak",
"version": "2023.0.5" "version": "2023.0.12"
}, },
"tauri": { "tauri": {
"windows": [], "windows": [],
@@ -52,6 +52,7 @@
}, },
"macOS": { "macOS": {
"exceptionDomain": "", "exceptionDomain": "",
"entitlements": "macos/entitlements.plist",
"frameworks": [] "frameworks": []
}, },
"windows": { "windows": {

View File

@@ -1,7 +1,4 @@
import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { persistQueryClient } from '@tanstack/react-query-persist-client';
import { MotionConfig } from 'framer-motion'; import { MotionConfig } from 'framer-motion';
import { Suspense } from 'react'; import { Suspense } from 'react';
import { DndProvider } from 'react-dnd'; import { DndProvider } from 'react-dnd';
@@ -16,26 +13,12 @@ const queryClient = new QueryClient({
defaultOptions: { defaultOptions: {
queries: { queries: {
retry: false, retry: false,
cacheTime: 1000 * 60 * 60 * 24, // 24 hours refetchOnWindowFocus: true,
networkMode: 'offlineFirst', networkMode: 'offlineFirst',
// It's a desktop app, so this isn't necessary
refetchOnWindowFocus: false,
}, },
}, },
}); });
const localStoragePersister = createSyncStoragePersister({
storage: window.localStorage,
throttleTime: 1000, // 1 second
});
persistQueryClient({
queryClient,
persister: localStoragePersister,
maxAge: 1000 * 60 * 60 * 24, // 24 hours
});
export function App() { export function App() {
return ( return (
<QueryClientProvider client={queryClient}> <QueryClientProvider client={queryClient}>
@@ -46,7 +29,7 @@ export function App() {
<Suspense> <Suspense>
<AppRouter /> <AppRouter />
<TauriListeners /> <TauriListeners />
<ReactQueryDevtools initialIsOpen={false} /> {/*<ReactQueryDevtools initialIsOpen={false} />*/}
</Suspense> </Suspense>
</DialogProvider> </DialogProvider>
</DndProvider> </DndProvider>

View File

@@ -1,13 +1,5 @@
import { useEffect } from 'react'; import { createBrowserRouter, Navigate, RouterProvider } from 'react-router-dom';
import {
createBrowserRouter,
Navigate,
Outlet,
RouterProvider,
useLocation,
} from 'react-router-dom';
import { routePaths } from '../hooks/useRoutes'; import { routePaths } from '../hooks/useRoutes';
import { setLastLocation } from '../lib/lastLocation';
import RouteError from './RouteError'; import RouteError from './RouteError';
import Workspace from './Workspace'; import Workspace from './Workspace';
import Workspaces from './Workspaces'; import Workspaces from './Workspaces';
@@ -16,7 +8,6 @@ const router = createBrowserRouter([
{ {
path: '/', path: '/',
errorElement: <RouteError />, errorElement: <RouteError />,
element: <RouterRoot />,
children: [ children: [
{ {
path: '/', path: '/',
@@ -44,11 +35,3 @@ const router = createBrowserRouter([
export function AppRouter() { export function AppRouter() {
return <RouterProvider router={router} />; return <RouterProvider router={router} />;
} }
function RouterRoot() {
const { pathname } = useLocation();
useEffect(() => {
setLastLocation(pathname).catch(console.error);
}, [pathname]);
return <Outlet />;
}

View File

@@ -71,7 +71,7 @@ export function GraphQLEditor({ defaultValue, onChange, baseRequest, ...extraEdi
const dialog = useDialog(); const dialog = useDialog();
return ( return (
<div className="pb-2 h-full grid grid-rows-[minmax(0,100%)_auto_auto_minmax(0,auto)]"> <div className="pb-2 h-full w-full grid grid-cols-1 grid-rows-[minmax(0,100%)_auto_auto_minmax(0,auto)]">
<Editor <Editor
contentType="application/graphql" contentType="application/graphql"
defaultValue={query ?? ''} defaultValue={query ?? ''}

View File

@@ -0,0 +1,9 @@
interface Props {
data: string;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export function ImageView({ data }: Props) {
// const dataUri = `data:image/png;base64,${window.btoa(data)}`;
return <div>Image preview not supported until binary response support is added</div>;
}

View File

@@ -1,11 +1,11 @@
import type { HTMLAttributes, ReactElement } from 'react'; import type { HTMLAttributes, ReactElement } from 'react';
import { useConfirm } from '../hooks/useConfirm'; import React from 'react';
import { useDeleteRequest } from '../hooks/useDeleteRequest'; import { useDeleteRequest } from '../hooks/useDeleteRequest';
import { useDuplicateRequest } from '../hooks/useDuplicateRequest'; import { useDuplicateRequest } from '../hooks/useDuplicateRequest';
import { useRequest } from '../hooks/useRequest'; import { useTheme } from '../hooks/useTheme';
import { Dropdown } from './core/Dropdown'; import { Dropdown } from './core/Dropdown';
import { HotKey } from './core/HotKey';
import { Icon } from './core/Icon'; import { Icon } from './core/Icon';
import { InlineCode } from './core/InlineCode';
interface Props { interface Props {
requestId: string; requestId: string;
@@ -13,10 +13,9 @@ interface Props {
} }
export function RequestActionsDropdown({ requestId, children }: Props) { export function RequestActionsDropdown({ requestId, children }: Props) {
const request = useRequest(requestId ?? null); const deleteRequest = useDeleteRequest(requestId);
const deleteRequest = useDeleteRequest(requestId ?? null);
const duplicateRequest = useDuplicateRequest({ id: requestId, navigateAfter: true }); const duplicateRequest = useDuplicateRequest({ id: requestId, navigateAfter: true });
const confirm = useConfirm(); const { appearance, toggleAppearance } = useTheme();
return ( return (
<Dropdown <Dropdown
@@ -25,25 +24,19 @@ 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>,
}, },
{ {
label: 'Delete', label: 'Delete',
onSelect: async () => { onSelect: deleteRequest.mutate,
const confirmed = await confirm({
title: 'Delete Request',
variant: 'delete',
description: (
<>
Are you sure you want to delete <InlineCode>{request?.name}</InlineCode>?
</>
),
});
if (confirmed) {
deleteRequest.mutate();
}
},
leftSlot: <Icon icon="trash" />, leftSlot: <Icon icon="trash" />,
}, },
{ type: 'separator', label: 'Yaak Settings' },
{
label: appearance === 'dark' ? 'Light Theme' : 'Dark Theme',
onSelect: toggleAppearance,
leftSlot: <Icon icon={appearance === 'dark' ? 'sun' : 'moon'} />,
},
]} ]}
> >
{children} {children}

View File

@@ -30,8 +30,8 @@ export function ResizeHandle({
style={style} style={style}
className={classnames( className={classnames(
className, className,
'group z-10 flex cursor-ew-resize', 'group z-10 flex',
vertical ? 'w-full h-3 cursor-ns-resize' : 'h-full w-3 cursor-ew-resize', vertical ? 'w-full h-3 cursor-row-resize' : 'h-full w-3 cursor-col-resize',
justify === 'center' && 'justify-center', justify === 'center' && 'justify-center',
justify === 'end' && 'justify-end', justify === 'end' && 'justify-end',
justify === 'start' && 'justify-start', justify === 'start' && 'justify-start',
@@ -46,9 +46,9 @@ export function ResizeHandle({
{isResizing && ( {isResizing && (
<div <div
className={classnames( className={classnames(
'fixed -left-20 -right-20 -top-20 -bottom-20 cursor-ew-resize', 'fixed -left-20 -right-20 -top-20 -bottom-20',
vertical && 'cursor-ns-resize', vertical && 'cursor-row-resize',
!vertical && 'cursor-ew-resize', !vertical && 'cursor-col-resize',
)} )}
/> />
)} )}

View File

@@ -8,13 +8,13 @@ interface Props {
export function ResponseHeaders({ headers }: Props) { export function ResponseHeaders({ headers }: Props) {
return ( return (
<dl className="text-xs w-full font-mono"> <dl className="text-xs w-full h-full font-mono overflow-auto">
{headers.map((h, i) => { {headers.map((h, i) => {
return ( return (
<HStack <HStack
space={3} space={3}
key={i} key={i}
className={classnames(i > 0 && 'border-t border-highlightSecondary', 'py-1')} className={classnames(i > 0 ? 'border-t border-highlightSecondary py-1' : 'pb-1')}
> >
<dd className="w-1/3 text-violet-600 select-text cursor-text">{h.name}</dd> <dd className="w-1/3 text-violet-600 select-text cursor-text">{h.name}</dd>
<dt className="w-2/3 select-text cursor-text break-all">{h.value}</dt> <dt className="w-2/3 select-text cursor-text break-all">{h.value}</dt>

View File

@@ -9,6 +9,7 @@ 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';
import type { HttpResponse } from '../lib/models'; import type { HttpResponse } from '../lib/models';
import { isResponseLoading } from '../lib/models';
import { pluralize } from '../lib/pluralize'; import { pluralize } from '../lib/pluralize';
import { Banner } from './core/Banner'; import { Banner } from './core/Banner';
import { CountBadge } from './core/CountBadge'; import { CountBadge } from './core/CountBadge';
@@ -17,10 +18,12 @@ import { Editor } from './core/Editor';
import { Icon } from './core/Icon'; import { Icon } from './core/Icon';
import { IconButton } from './core/IconButton'; import { IconButton } from './core/IconButton';
import { HStack } from './core/Stacks'; import { HStack } from './core/Stacks';
import { StatusColor } from './core/StatusColor'; import { StatusTag } from './core/StatusTag';
import type { TabItem } from './core/Tabs/Tabs';
import { TabContent, Tabs } from './core/Tabs/Tabs'; import { TabContent, Tabs } from './core/Tabs/Tabs';
import { Webview } from './core/Webview'; import { Webview } from './core/Webview';
import { EmptyStateText } from './EmptyStateText'; import { EmptyStateText } from './EmptyStateText';
import { ImageView } from './ImageView';
import { ResponseHeaders } from './ResponseHeaders'; import { ResponseHeaders } from './ResponseHeaders';
interface Props { interface Props {
@@ -52,9 +55,20 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro
[activeResponse], [activeResponse],
); );
const tabs = useMemo( const tabs: TabItem[] = useMemo(
() => [ () => [
{ label: 'Body', value: 'body' }, {
value: 'body',
label: 'Preview',
options: {
value: viewMode,
onChange: toggleViewMode,
items: [
{ label: 'Pretty', value: 'pretty' },
{ label: 'Raw', value: 'raw' },
],
},
},
{ {
label: ( label: (
<div className="flex items-center"> <div className="flex items-center">
@@ -67,9 +81,12 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro
value: 'headers', value: 'headers',
}, },
], ],
[activeResponse?.headers], [activeResponse?.headers, toggleViewMode, viewMode],
); );
// Don't render until we know the view mode
if (viewMode === undefined) return null;
return ( return (
<div <div
style={style} style={style}
@@ -80,105 +97,104 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro
'shadow shadow-gray-100 dark:shadow-gray-0 relative', 'shadow shadow-gray-100 dark:shadow-gray-0 relative',
)} )}
> >
<HStack {activeResponse?.error && <Banner className="m-2">{activeResponse.error}</Banner>}
alignItems="center" {activeResponse && !activeResponse.error && !isResponseLoading(activeResponse) && (
className={classnames( <>
'italic text-gray-700 text-sm w-full flex-shrink-0', <HStack
// Remove a bit of space because the tabs have lots too alignItems="center"
'-mb-1.5', className={classnames(
)} 'italic text-gray-700 text-sm w-full flex-shrink-0',
> // Remove a bit of space because the tabs have lots too
{activeResponse && ( '-mb-1.5',
<> )}
<div className="whitespace-nowrap p-3 py-2"> >
<StatusColor statusCode={activeResponse.status}> {activeResponse && (
{activeResponse.status} <HStack alignItems="center" className="w-full">
{activeResponse.statusReason && ` ${activeResponse.statusReason}`} <div className="whitespace-nowrap px-3">
</StatusColor> <StatusTag response={activeResponse} />
&nbsp;&bull;&nbsp; {activeResponse.elapsed > 0 && <>&nbsp;&bull;&nbsp;{activeResponse.elapsed}ms</>}
{activeResponse.elapsed}ms &nbsp;&bull;&nbsp; {activeResponse.body.length > 0 && (
{Math.round(activeResponse.body.length / 1000)} KB <>&nbsp;&bull;&nbsp;{(activeResponse.body.length / 1000).toFixed(1)} KB</>
</div> )}
</div>
<Dropdown <Dropdown
items={[ items={[
{ {
label: viewMode === 'pretty' ? 'View Raw' : 'View Prettified', label: 'Clear Response',
onSelect: toggleViewMode, onSelect: deleteResponse.mutate,
}, disabled: responses.length === 0,
{ type: 'separator', label: 'Actions' }, },
{ {
label: 'Clear Response', label: `Clear ${responses.length} ${pluralize('Response', responses.length)}`,
onSelect: deleteResponse.mutate, onSelect: deleteAllResponses.mutate,
disabled: responses.length === 0, hidden: responses.length <= 1,
}, disabled: responses.length === 0,
{ },
label: `Clear ${responses.length} ${pluralize('Response', responses.length)}`, { type: 'separator', label: 'History' },
onSelect: deleteAllResponses.mutate, ...responses.slice(0, 10).map((r) => ({
hidden: responses.length <= 1, label: r.status + ' - ' + r.elapsed + ' ms',
disabled: responses.length === 0, leftSlot: activeResponse?.id === r.id ? <Icon icon="check" /> : <></>,
}, onSelect: () => setPinnedResponseId(r.id),
{ type: 'separator', label: 'History' }, })),
...responses.slice(0, 10).map((r) => ({ ]}
label: r.status + ' - ' + r.elapsed + ' ms', >
leftSlot: activeResponse?.id === r.id ? <Icon icon="check" /> : <></>, <IconButton
onSelect: () => setPinnedResponseId(r.id), title="Show response history"
})), icon="triangleDown"
]} className="ml-auto"
size="sm"
iconSize="md"
/>
</Dropdown>
</HStack>
)}
</HStack>
{
<Tabs
value={activeTab}
onChangeValue={setActiveTab}
label="Response"
tabs={tabs}
className="ml-3 mr-1"
tabListClassName="mt-1.5"
> >
<IconButton <TabContent value="headers">
title="Show response history" <ResponseHeaders headers={activeResponse?.headers ?? []} />
icon="triangleDown" </TabContent>
className="ml-auto" <TabContent value="body">
size="sm" {!activeResponse.body ? (
iconSize="md" <EmptyStateText>No Response</EmptyStateText>
/> ) : viewMode === 'pretty' && contentType.includes('html') ? (
</Dropdown> <Webview
</> body={activeResponse.body}
)} contentType={contentType}
</HStack> url={activeResponse.url}
/>
{activeResponse?.error ? ( ) : viewMode === 'pretty' && contentType.includes('json') ? (
<Banner className="m-2">{activeResponse.error}</Banner> <Editor
) : ( readOnly
<Tabs forceUpdateKey={`pretty::${activeResponse.updatedAt}`}
value={activeTab} className="bg-gray-50 dark:!bg-gray-100"
onChangeValue={setActiveTab} defaultValue={tryFormatJson(activeResponse?.body)}
label="Response" contentType={contentType}
className="px-3" />
tabs={tabs} ) : contentType.startsWith('image') ? (
> <ImageView data={activeResponse?.body} />
<TabContent value="body"> ) : activeResponse?.body ? (
{activeResponse === null ? ( <Editor
<EmptyStateText>No Response</EmptyStateText> readOnly
) : viewMode === 'pretty' && contentType.includes('html') ? ( forceUpdateKey={activeResponse.updatedAt}
<Webview className="bg-gray-50 dark:!bg-gray-100"
body={activeResponse.body} defaultValue={activeResponse?.body}
contentType={contentType} contentType={contentType}
url={activeResponse.url} />
/> ) : null}
) : viewMode === 'pretty' && contentType.includes('json') ? ( </TabContent>
<Editor </Tabs>
readOnly }
forceUpdateKey={`pretty::${activeResponse.updatedAt}`} </>
className="bg-gray-50 dark:!bg-gray-100"
defaultValue={tryFormatJson(activeResponse?.body)}
contentType={contentType}
/>
) : activeResponse?.body ? (
<Editor
readOnly
forceUpdateKey={activeResponse.updatedAt}
className="bg-gray-50 dark:!bg-gray-100"
defaultValue={activeResponse?.body}
contentType={contentType}
/>
) : null}
</TabContent>
<TabContent value="headers">
<ResponseHeaders headers={activeResponse?.headers ?? []} />
</TabContent>
</Tabs>
)} )}
</div> </div>
); );

View File

@@ -15,9 +15,14 @@ export default function RouteError() {
<pre className="text-sm select-auto cursor-text bg-gray-100 p-3 rounded whitespace-normal"> <pre className="text-sm select-auto cursor-text bg-gray-100 p-3 rounded whitespace-normal">
{message} {message}
</pre> </pre>
<Button to="/" color="primary"> <VStack space={2}>
Go Home <Button to="/" color="primary">
</Button> Go Home
</Button>
<Button color="secondary" onClick={() => window.location.reload()}>
Refresh
</Button>
</VStack>
</VStack> </VStack>
</div> </div>
); );

View File

@@ -4,16 +4,18 @@ import React, { forwardRef, Fragment, memo, useCallback, useMemo, useRef, useSta
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 { useActiveRequest } from '../hooks/useActiveRequest';
import { useDeleteRequest } from '../hooks/useDeleteRequest';
import { useLatestResponse } from '../hooks/useLatestResponse';
import { useRequests } from '../hooks/useRequests'; import { useRequests } from '../hooks/useRequests';
import { useUpdateAnyRequest } from '../hooks/useUpdateAnyRequest'; 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 { Button } from './core/Button'; import { Button } from './core/Button';
import { IconButton } from './core/IconButton'; import { Icon } from './core/Icon';
import { HStack, VStack } from './core/Stacks'; import { VStack } from './core/Stacks';
import { StatusTag } from './core/StatusTag';
import { DropMarker } from './DropMarker'; import { DropMarker } from './DropMarker';
import { RequestActionsDropdown } from './RequestActionsDropdown';
import { ToggleThemeButton } from './ToggleThemeButton';
interface Props { interface Props {
className?: string; className?: string;
@@ -45,9 +47,6 @@ export const Sidebar = memo(function Sidebar({ className }: Props) {
> >
<SidebarItems activeRequestId={activeRequest?.id} requests={requests} /> <SidebarItems activeRequestId={activeRequest?.id} requests={requests} />
</VStack> </VStack>
<HStack className="mx-1 pb-1" alignItems="center" justifyContent="end">
<ToggleThemeButton />
</HStack>
</div> </div>
</div> </div>
); );
@@ -136,7 +135,9 @@ const _SidebarItem = forwardRef(function SidebarItem(
{ className, requestName, requestId, workspaceId, active }: SidebarItemProps, { className, requestName, requestId, workspaceId, active }: SidebarItemProps,
ref: ForwardedRef<HTMLLIElement>, ref: ForwardedRef<HTMLLIElement>,
) { ) {
const latestResponse = useLatestResponse(requestId);
const updateRequest = useUpdateRequest(requestId); const updateRequest = useUpdateRequest(requestId);
const deleteRequest = useDeleteRequest(requestId);
const [editing, setEditing] = useState<boolean>(false); const [editing, setEditing] = useState<boolean>(false);
const handleSubmitNameEdit = useCallback( const handleSubmitNameEdit = useCallback(
@@ -159,12 +160,17 @@ const _SidebarItem = forwardRef(function SidebarItem(
e.preventDefault(); e.preventDefault();
setEditing(true); setEditing(true);
} }
if (active && (e.key === 'Backspace' || e.key === 'Delete')) {
e.preventDefault();
deleteRequest.mutate();
}
}, },
[active], [active, deleteRequest],
); );
const handleInputKeyDown = useCallback( const handleInputKeyDown = useCallback(
async (e: KeyboardEvent<HTMLInputElement>) => { async (e: KeyboardEvent<HTMLInputElement>) => {
e.stopPropagation();
switch (e.key) { switch (e.key) {
case 'Enter': case 'Enter':
e.preventDefault(); e.preventDefault();
@@ -183,21 +189,19 @@ const _SidebarItem = forwardRef(function SidebarItem(
<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 <Button
tabIndex={0}
color="custom" color="custom"
size="sm" 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={() => setEditing(true)}
onClick={active ? () => setEditing(true) : undefined}
justify="start" justify="start"
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
className={classnames( className={classnames(
editing && 'focus-within:border-focus', editing && 'ring-1 focus-within:ring-focus',
active active
? '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',
// Move out of the way when trash is shown
'group-hover/item:pr-7',
)} )}
> >
{editing ? ( {editing ? (
@@ -213,19 +217,16 @@ const _SidebarItem = forwardRef(function SidebarItem(
{requestName || 'New Request'} {requestName || 'New Request'}
</span> </span>
)} )}
{latestResponse && (
<div className="ml-auto">
{isResponseLoading(latestResponse) ? (
<Icon spin size="sm" icon="update" />
) : (
<StatusTag className="text-2xs dark:opacity-80" response={latestResponse} />
)}
</div>
)}
</Button> </Button>
<RequestActionsDropdown requestId={requestId}>
<IconButton
color="custom"
size="sm"
title="Request Options"
icon="dotsH"
className={classnames(
'absolute right-0 top-0 transition-opacity !opacity-0',
'group-hover/item:!opacity-100 focus-visible:!opacity-100',
)}
/>
</RequestActionsDropdown>
</div> </div>
</li> </li>
); );

View File

@@ -1,14 +1,26 @@
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 { IconButton } from './core/IconButton'; import { IconButton } from './core/IconButton';
export const SidebarActions = memo(function SidebarDisplayToggle() { export const SidebarActions = memo(function SidebarDisplayToggle() {
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({ name: 'New Request' }); createRequest.mutate({});
}, [createRequest]); }, [createRequest]);
useTauriEvent('new_request', () => {
createRequest.mutate({});
});
// TODO: Put this somewhere better
useTauriEvent('duplicate_request', () => {
duplicateRequest.mutate();
});
return ( return (
<> <>

View File

@@ -1,14 +0,0 @@
import React from 'react';
import { useTheme } from '../hooks/useTheme';
import { IconButton } from './core/IconButton';
export function ToggleThemeButton() {
const { appearance, toggleAppearance } = useTheme();
return (
<IconButton
title={appearance === 'dark' ? 'Enable light mode' : 'Enable dark mode'}
icon={appearance === 'dark' ? 'moon' : 'sun'}
onClick={toggleAppearance}
/>
);
}

View File

@@ -1,7 +1,6 @@
import classnames from 'classnames'; import classnames from 'classnames';
import { memo, useMemo } from 'react'; import { memo, useMemo } from 'react';
import { useActiveWorkspace } from '../hooks/useActiveWorkspace'; import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
import { useConfirm } from '../hooks/useConfirm';
import { useCreateWorkspace } from '../hooks/useCreateWorkspace'; import { useCreateWorkspace } from '../hooks/useCreateWorkspace';
import { useDeleteWorkspace } from '../hooks/useDeleteWorkspace'; import { useDeleteWorkspace } from '../hooks/useDeleteWorkspace';
import { useRoutes } from '../hooks/useRoutes'; import { useRoutes } from '../hooks/useRoutes';
@@ -10,7 +9,6 @@ import { Button } from './core/Button';
import type { DropdownItem } from './core/Dropdown'; import type { DropdownItem } from './core/Dropdown';
import { Dropdown } from './core/Dropdown'; import { Dropdown } from './core/Dropdown';
import { Icon } from './core/Icon'; import { Icon } from './core/Icon';
import { InlineCode } from './core/InlineCode';
type Props = { type Props = {
className?: string; className?: string;
@@ -21,9 +19,8 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceDropdown({ classN
const activeWorkspace = useActiveWorkspace(); const activeWorkspace = useActiveWorkspace();
const activeWorkspaceId = activeWorkspace?.id ?? null; const activeWorkspaceId = activeWorkspace?.id ?? null;
const createWorkspace = useCreateWorkspace({ navigateAfter: true }); const createWorkspace = useCreateWorkspace({ navigateAfter: true });
const deleteWorkspace = useDeleteWorkspace(activeWorkspaceId); const deleteWorkspace = useDeleteWorkspace(activeWorkspace);
const routes = useRoutes(); const routes = useRoutes();
const confirm = useConfirm();
const items: DropdownItem[] = useMemo(() => { const items: DropdownItem[] = useMemo(() => {
const workspaceItems = workspaces.map((w) => ({ const workspaceItems = workspaces.map((w) => ({
@@ -49,20 +46,7 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceDropdown({ classN
{ {
label: 'Delete Workspace', label: 'Delete Workspace',
leftSlot: <Icon icon="trash" />, leftSlot: <Icon icon="trash" />,
onSelect: async () => { onSelect: deleteWorkspace.mutate,
const confirmed = await confirm({
title: 'Delete Workspace',
variant: 'delete',
description: (
<>
Are you sure you want to delete <InlineCode>{activeWorkspace?.name}</InlineCode>?
</>
),
});
if (confirmed) {
deleteWorkspace.mutate();
}
},
}, },
]; ];
}, [ }, [

View File

@@ -13,6 +13,7 @@ interface Props {
export const WorkspaceHeader = memo(function WorkspaceHeader({ className }: Props) { export const WorkspaceHeader = memo(function WorkspaceHeader({ className }: Props) {
const activeRequest = useActiveRequest(); const activeRequest = useActiveRequest();
return ( return (
<HStack <HStack
justifyContent="center" justifyContent="center"

View File

@@ -46,7 +46,6 @@ const _Button = forwardRef<any, ButtonProps>(function Button(
() => () =>
classnames( classnames(
className, className,
'opacity-90 hover:opacity-100',
'outline-none whitespace-nowrap', 'outline-none whitespace-nowrap',
'focus-visible-or-class:ring', 'focus-visible-or-class:ring',
'rounded-md flex items-center', 'rounded-md flex items-center',

View File

@@ -266,7 +266,7 @@ function MenuItem({ className, focused, onFocus, item, onSelect, ...props }: Men
onClick={handleClick} onClick={handleClick}
className={classnames( className={classnames(
className, className,
'min-w-[8rem] outline-none px-2 mx-1.5 h-7 flex items-center text-sm text-gray-700 whitespace-nowrap pr-4', 'min-w-[8rem] outline-none px-2 mx-1.5 h-7 flex items-center text-sm text-gray-700 whitespace-nowrap',
'focus:bg-highlight focus:text-gray-900 rounded', 'focus:bg-highlight focus:text-gray-900 rounded',
)} )}
{...props} {...props}

View File

@@ -183,20 +183,16 @@
@apply bg-highlight text-gray-900; @apply bg-highlight text-gray-900;
} }
& > ul > li:hover {
@apply text-gray-800;
}
.cm-completionIcon { .cm-completionIcon {
@apply text-sm flex items-center pb-0.5; @apply text-sm flex items-center pb-0.5 flex-shrink-0;
} }
.cm-completionLabel { .cm-completionLabel {
@apply text-gray-700;
} }
.cm-completionDetail { .cm-completionDetail {
@apply ml-auto; @apply ml-auto pl-6;
} }
} }
} }

View File

@@ -35,6 +35,7 @@ export interface EditorProps {
onFocus?: () => void; onFocus?: () => void;
onBlur?: () => void; onBlur?: () => void;
singleLine?: boolean; singleLine?: boolean;
wrapLines?: boolean;
format?: (v: string) => string; format?: (v: string) => string;
autocomplete?: GenericCompletionConfig; autocomplete?: GenericCompletionConfig;
actions?: ReactNode; actions?: ReactNode;
@@ -59,6 +60,7 @@ const _Editor = forwardRef<EditorView | undefined, EditorProps>(function Editor(
format, format,
autocomplete, autocomplete,
actions, actions,
wrapLines,
}: EditorProps, }: EditorProps,
ref, ref,
) { ) {
@@ -93,6 +95,15 @@ const _Editor = forwardRef<EditorView | undefined, EditorProps>(function Editor(
cm.current?.view.dispatch({ effects: effect }); cm.current?.view.dispatch({ effects: effect });
}, [placeholder]); }, [placeholder]);
// Update wrap lines
const wrapLinesCompartment = useRef(new Compartment());
useEffect(() => {
if (cm.current === null) return;
const ext = wrapLines ? [EditorView.lineWrapping] : [];
const effect = wrapLinesCompartment.current.reconfigure(ext);
cm.current?.view.dispatch({ effects: effect });
}, [wrapLines]);
// Update language extension when contentType changes // Update language extension when contentType changes
useEffect(() => { useEffect(() => {
if (cm.current === null) return; if (cm.current === null) return;
@@ -126,16 +137,15 @@ const _Editor = forwardRef<EditorView | undefined, EditorProps>(function Editor(
doc: `${defaultValue ?? ''}`, doc: `${defaultValue ?? ''}`,
extensions: [ extensions: [
languageCompartment.of(langExt), languageCompartment.of(langExt),
placeholderCompartment.current.of( placeholderCompartment.current.of([]),
placeholderExt(placeholderElFromText(placeholder ?? '')), wrapLinesCompartment.current.of([]),
),
...getExtensions({ ...getExtensions({
container, container,
readOnly,
singleLine,
onChange: handleChange, onChange: handleChange,
onFocus: handleFocus, onFocus: handleFocus,
onBlur: handleBlur, onBlur: handleBlur,
readOnly,
singleLine,
}), }),
], ],
}); });

View File

@@ -3,18 +3,8 @@ import type { CompletionContext } from '@codemirror/autocomplete';
const openTag = '${[ '; const openTag = '${[ ';
const closeTag = ' ]}'; const closeTag = ' ]}';
const variables = [ const variables: { name: string }[] = [
{ name: 'DOMAIN' }, // TODO: Put variables here
{ name: 'BASE_URL' },
{ name: 'CONTENT_THINGY' },
{ name: 'TOKEN' },
{ name: 'PROJECT_ID' },
{ name: 'DUMMY' },
{ name: 'DUMMY_2' },
{ name: 'STRIPE_PUB_KEY' },
{ name: 'RAILWAY_TOKEN' },
{ name: 'SECRET' },
{ name: 'PORT' },
]; ];
const MIN_MATCH_VAR = 2; const MIN_MATCH_VAR = 2;

View File

@@ -5,7 +5,7 @@ export function HotKey({ children }: HTMLAttributes<HTMLSpanElement>) {
return ( return (
<span <span
className={classnames( className={classnames(
'bg-gray-400 bg-opacity-20 px-1.5 py-0.5 rounded text-sm', 'bg-highlightSecondary bg-opacity-20 px-1.5 py-0.5 rounded text-sm',
'font-mono text-gray-500 tracking-widest', 'font-mono text-gray-500 tracking-widest',
)} )}
> >

View File

@@ -57,12 +57,12 @@ export const Input = forwardRef<EditorView | undefined, InputProps>(function Inp
const [currentValue, setCurrentValue] = useState(defaultValue ?? ''); const [currentValue, setCurrentValue] = useState(defaultValue ?? '');
const [focused, setFocused] = useState(false); const [focused, setFocused] = useState(false);
const handleOnFocus = useCallback(() => { const handleFocus = useCallback(() => {
setFocused(true); setFocused(true);
onFocus?.(); onFocus?.();
}, [onFocus]); }, [onFocus]);
const handleOnBlur = useCallback(() => { const handleBlur = useCallback(() => {
setFocused(false); setFocused(false);
onBlur?.(); onBlur?.();
}, [onBlur]); }, [onBlur]);
@@ -107,8 +107,8 @@ export const Input = forwardRef<EditorView | undefined, InputProps>(function Inp
className={classnames( className={classnames(
containerClassName, containerClassName,
'relative w-full rounded-md text-gray-900', 'relative w-full rounded-md text-gray-900',
'border border-highlight', 'border',
focused && 'border-focus', focused ? 'border-focus' : 'border-highlight',
!isValid && '!border-invalid', !isValid && '!border-invalid',
size === 'md' && 'h-md leading-md', size === 'md' && 'h-md leading-md',
size === 'sm' && 'h-sm leading-sm', size === 'sm' && 'h-sm leading-sm',
@@ -125,8 +125,8 @@ export const Input = forwardRef<EditorView | undefined, InputProps>(function Inp
placeholder={placeholder} placeholder={placeholder}
onChange={handleChange} onChange={handleChange}
className={inputClassName} className={inputClassName}
onFocus={handleOnFocus} onFocus={handleFocus}
onBlur={handleOnBlur} onBlur={handleBlur}
{...props} {...props}
/> />
{type === 'password' && ( {type === 'password' && (

View File

@@ -137,7 +137,7 @@ export const PairEditor = memo(function PairEditor({
className={classnames( className={classnames(
className, className,
'@container', '@container',
'pb-2 grid', 'pb-2 grid overflow-auto max-h-full',
// Move over the width of the drag handle // Move over the width of the drag handle
'-ml-3', '-ml-3',
)} )}

View File

@@ -1,23 +0,0 @@
import classnames from 'classnames';
import type { ReactNode } from 'react';
interface Props {
statusCode: number;
children: ReactNode;
}
export function StatusColor({ statusCode, children }: Props) {
return (
<span
className={classnames(
statusCode >= 100 && statusCode < 200 && 'text-green-600',
statusCode >= 200 && statusCode < 300 && 'text-green-600',
statusCode >= 300 && statusCode < 400 && 'text-pink-600',
statusCode >= 400 && statusCode < 500 && 'text-orange-600',
statusCode >= 500 && statusCode < 600 && 'text-red-600',
)}
>
{children}
</span>
);
}

View File

@@ -0,0 +1,28 @@
import classnames from 'classnames';
import type { HttpResponse } from '../../lib/models';
interface Props {
response: Pick<HttpResponse, 'status' | 'error'>;
className?: string;
}
export function StatusTag({ response, className }: Props) {
const { status, error } = response;
const label = error ? 'ERR' : status;
return (
<span
className={classnames(
className,
'font-mono',
status >= 0 && status < 100 && 'text-red-600',
status >= 100 && status < 200 && 'text-green-600',
status >= 200 && status < 300 && 'text-green-600',
status >= 300 && status < 400 && 'text-pink-600',
status >= 400 && status < 500 && 'text-orange-600',
status >= 500 && 'text-red-600',
)}
>
{label}
</span>
);
}

View File

@@ -81,8 +81,10 @@ export function Tabs({
<HStack space={1} className="flex-shrink-0"> <HStack space={1} className="flex-shrink-0">
{tabs.map((t) => { {tabs.map((t) => {
const isActive = t.value === value; const isActive = t.value === value;
// const btnClassName = classnames(isActive ? 'bg-highlightSecondary' : 'text-gray-600'); const btnClassName = classnames(
const btnClassName = classnames(isActive ? '' : 'text-gray-600', '!px-0 mr-4 ml-[1px]'); isActive ? '' : 'text-gray-600 hover:text-gray-800',
'!px-0 mr-4 ml-[1px]',
);
if ('options' in t) { if ('options' in t) {
const option = t.options.items.find( const option = t.options.items.find(
@@ -147,7 +149,7 @@ export const TabContent = memo(function TabContent({
<div <div
tabIndex={-1} tabIndex={-1}
data-tab={value} data-tab={value}
className={classnames(className, 'tab-content', 'overflow-auto hidden w-full h-full')} className={classnames(className, 'tab-content', 'hidden w-full h-full')}
> >
{children} {children}
</div> </div>

View File

@@ -16,12 +16,12 @@ export function Webview({ body, url, contentType }: Props) {
}, [url, body, contentType]); }, [url, body, contentType]);
return ( return (
<div className="px-2 pb-2"> <div className="h-full pb-3">
<iframe <iframe
title="Response preview" title="Response preview"
srcDoc={contentForIframe} srcDoc={contentForIframe}
sandbox="allow-scripts allow-same-origin" sandbox="allow-scripts allow-same-origin"
className="h-full w-full rounded-md border border-gray-100/20" className="h-full w-full rounded border border-highlightSecondary"
/> />
</div> </div>
); );

View File

@@ -16,8 +16,9 @@ export function useCreateRequest({ navigateAfter }: { navigateAfter: boolean })
if (workspaceId === null) { if (workspaceId === null) {
throw new Error("Cannot create request when there's no active workspace"); throw new Error("Cannot create request when there's no active workspace");
} }
const sortPriority = maxSortPriority(requests) + 1000; patch.name = patch.name || 'New Request';
return invoke('create_request', { sortPriority, workspaceId, ...patch }); patch.sortPriority = patch.sortPriority || maxSortPriority(requests) + 1000;
return invoke('create_request', { workspaceId, ...patch });
}, },
onSuccess: async (request) => { onSuccess: async (request) => {
queryClient.setQueryData<HttpRequest[]>( queryClient.setQueryData<HttpRequest[]>(

View File

@@ -1,7 +1,10 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api'; import { invoke } from '@tauri-apps/api';
import { InlineCode } from '../components/core/InlineCode';
import type { HttpRequest } from '../lib/models'; import type { HttpRequest } from '../lib/models';
import { getRequest } from '../lib/store';
import { useActiveRequestId } from './useActiveRequestId'; import { useActiveRequestId } from './useActiveRequestId';
import { useConfirm } from './useConfirm';
import { requestsQueryKey } from './useRequests'; import { requestsQueryKey } from './useRequests';
import { responsesQueryKey } from './useResponses'; import { responsesQueryKey } from './useResponses';
import { useRoutes } from './useRoutes'; import { useRoutes } from './useRoutes';
@@ -10,9 +13,28 @@ export function useDeleteRequest(id: string | null) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const activeRequestId = useActiveRequestId(); const activeRequestId = useActiveRequestId();
const routes = useRoutes(); const routes = useRoutes();
return useMutation<HttpRequest, string>({ const confirm = useConfirm();
mutationFn: async () => invoke('delete_request', { requestId: id }),
onSuccess: async ({ workspaceId, id: requestId }) => { return useMutation<HttpRequest | null, string>({
mutationFn: async () => {
const request = await getRequest(id);
const confirmed = await confirm({
title: 'Delete Request',
variant: 'delete',
description: (
<>
Are you sure you want to delete <InlineCode>{request?.name}</InlineCode>?
</>
),
});
if (!confirmed) return null;
return invoke('delete_request', { requestId: id });
},
onSuccess: async (request) => {
// Was it cancelled?
if (request === null) return;
const { workspaceId, id: requestId } = request;
queryClient.setQueryData(responsesQueryKey({ requestId }), []); // Responses were deleted queryClient.setQueryData(responsesQueryKey({ requestId }), []); // Responses were deleted
queryClient.setQueryData<HttpRequest[]>(requestsQueryKey({ workspaceId }), (requests) => queryClient.setQueryData<HttpRequest[]>(requestsQueryKey({ workspaceId }), (requests) =>
(requests ?? []).filter((r) => r.id !== requestId), (requests ?? []).filter((r) => r.id !== requestId),

View File

@@ -1,20 +1,37 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useMutation, useQueryClient } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api'; import { invoke } from '@tauri-apps/api';
import { InlineCode } from '../components/core/InlineCode';
import type { Workspace } from '../lib/models'; import type { Workspace } from '../lib/models';
import { useActiveWorkspaceId } from './useActiveWorkspaceId'; import { useActiveWorkspaceId } from './useActiveWorkspaceId';
import { useConfirm } from './useConfirm';
import { requestsQueryKey } from './useRequests'; import { requestsQueryKey } from './useRequests';
import { useRoutes } from './useRoutes'; import { useRoutes } from './useRoutes';
import { workspacesQueryKey } from './useWorkspaces'; import { workspacesQueryKey } from './useWorkspaces';
export function useDeleteWorkspace(id: string | null) { export function useDeleteWorkspace(workspace: Workspace | null) {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const activeWorkspaceId = useActiveWorkspaceId(); const activeWorkspaceId = useActiveWorkspaceId();
const routes = useRoutes(); const routes = useRoutes();
return useMutation<Workspace, string>({ const confirm = useConfirm();
mutationFn: () => {
return invoke('delete_workspace', { id }); return useMutation<Workspace | null, string>({
mutationFn: async () => {
const confirmed = await confirm({
title: 'Delete Workspace',
variant: 'delete',
description: (
<>
Are you sure you want to delete <InlineCode>{workspace?.name}</InlineCode>?
</>
),
});
if (!confirmed) return null;
return invoke('delete_workspace', { id: workspace?.id });
}, },
onSuccess: async ({ id: workspaceId }) => { onSuccess: async (workspace) => {
if (workspace === null) return;
const { id: workspaceId } = workspace;
queryClient.setQueryData<Workspace[]>(workspacesQueryKey({}), (workspaces) => queryClient.setQueryData<Workspace[]>(workspacesQueryKey({}), (workspaces) =>
workspaces?.filter((workspace) => workspace.id !== workspaceId), workspaces?.filter((workspace) => workspace.id !== workspaceId),
); );

View File

@@ -1,8 +1,8 @@
import { useResponses } from './useResponses'; import { isResponseLoading } from '../lib/models';
import { useLatestResponse } from './useLatestResponse';
export function useIsResponseLoading(requestId: string | null): boolean { export function useIsResponseLoading(requestId: string | null): boolean {
const responses = useResponses(requestId); const response = useLatestResponse(requestId);
const response = responses[responses.length - 1]; if (response === null) return false;
if (!response) return false; return isResponseLoading(response);
return !(response.body || response.status || response.error);
} }

View File

@@ -0,0 +1,7 @@
import type { HttpResponse } from '../lib/models';
import { useResponses } from './useResponses';
export function useLatestResponse(requestId: string | null): HttpResponse | null {
const responses = useResponses(requestId);
return responses[responses.length - 1] ?? null;
}

View File

@@ -1,17 +0,0 @@
import { getKeyValue, NAMESPACE_NO_SYNC, setKeyValue } from './keyValueStore';
export async function getLastLocation(): Promise<string> {
return getKeyValue({ namespace: NAMESPACE_NO_SYNC, key: 'last_location', fallback: '/' });
}
export async function setLastLocation(pathname: string): Promise<void> {
return setKeyValue({ namespace: NAMESPACE_NO_SYNC, key: 'last_location', value: pathname });
}
export async function syncLastLocation(): Promise<void> {
const lastPathname = await getLastLocation();
if (lastPathname !== window.location.pathname) {
console.log(`Redirecting to last location: ${lastPathname}`);
window.location.assign(lastPathname);
}
}

View File

@@ -72,3 +72,7 @@ export interface HttpResponse extends BaseModel {
readonly url: string; readonly url: string;
readonly headers: HttpHeader[]; readonly headers: HttpHeader[];
} }
export function isResponseLoading(response: HttpResponse): boolean {
return !(response.body || response.status || response.error);
}

View File

@@ -12,11 +12,11 @@ const darkTheme: AppTheme = {
colors: { colors: {
gray: '#6b5b98', gray: '#6b5b98',
red: '#ff417b', red: '#ff417b',
orange: '#ff9411', orange: '#fd9014',
yellow: '#e8d13f', yellow: '#e8d13f',
green: '#43e76f', green: '#3fd265',
blue: '#219dff', blue: '#219dff',
pink: '#f670f6', pink: '#ff6dff',
violet: '#b176ff', violet: '#b176ff',
}, },
}, },
@@ -31,11 +31,11 @@ const lightTheme: AppTheme = {
colors: { colors: {
gray: '#7f8fb0', gray: '#7f8fb0',
red: '#ec3f87', red: '#ec3f87',
orange: '#ff8b00', orange: '#ff8000',
yellow: '#e7cf24', yellow: '#e7cf24',
green: '#00d365', green: '#00d365',
blue: '#0090ff', blue: '#0090ff',
pink: '#f670f6', pink: '#ea6cea',
violet: '#ac6cff', violet: '#ac6cff',
}, },
}, },
@@ -50,14 +50,6 @@ export function getAppearance(): Appearance {
return getPreferredAppearance(); return getPreferredAppearance();
} }
export function toggleAppearance(): Appearance {
const currentTheme =
document.documentElement.getAttribute('data-appearance') ?? getPreferredAppearance();
const newAppearance = currentTheme === 'dark' ? 'light' : 'dark';
setAppearance(newAppearance);
return newAppearance;
}
export function setAppearance(a?: Appearance) { export function setAppearance(a?: Appearance) {
const appearance = a ?? getPreferredAppearance(); const appearance = a ?? getPreferredAppearance();
const theme = appearance === 'dark' ? darkTheme : lightTheme; const theme = appearance === 'dark' ? darkTheme : lightTheme;

View File

@@ -2,12 +2,10 @@ import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client'; import { createRoot } from 'react-dom/client';
import { App } from './components/App'; import { App } from './components/App';
import { getKeyValue } from './lib/keyValueStore'; import { getKeyValue } from './lib/keyValueStore';
import { syncLastLocation } from './lib/lastLocation';
import { getPreferredAppearance, setAppearance } from './lib/theme/window'; import { getPreferredAppearance, setAppearance } from './lib/theme/window';
import './main.css'; import './main.css';
setAppearance(await getKeyValue({ key: 'appearance', fallback: getPreferredAppearance() })); setAppearance(await getKeyValue({ key: 'appearance', fallback: getPreferredAppearance() }));
await syncLastLocation();
// root holds our app's root DOM Element: // root holds our app's root DOM Element:
createRoot(document.getElementById('root') as HTMLElement).render( createRoot(document.getElementById('root') as HTMLElement).render(

View File

@@ -16,13 +16,13 @@ module.exports = {
"xs": "0.8rem" "xs": "0.8rem"
}, },
height: { height: {
"xs": "1.5rem", "xs": "1.75rem",
"sm": "2.0rem", "sm": "2.0rem",
"md": "2.5rem" "md": "2.5rem"
}, },
lineHeight: { lineHeight: {
// HACK: Minus 2 to account for borders inside inputs // HACK: Minus 2 to account for borders inside inputs
"xs": "calc(1.5rem - 2px)", "xs": "calc(1.75rem - 2px)",
"sm": "calc(2.0rem - 2px)", "sm": "calc(2.0rem - 2px)",
"md": "calc(2.5rem - 2px)" "md": "calc(2.5rem - 2px)"
}, },