mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-22 16:58:28 +02:00
Compare commits
20 Commits
v2023.0.5
...
v2023.0.12
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
10af9b6f99 | ||
|
|
aa8c066f2d | ||
|
|
b913b74449 | ||
|
|
b71adce50b | ||
|
|
0fbb44c701 | ||
|
|
de335e8637 | ||
|
|
2999f63a4c | ||
|
|
2abc5e6f0b | ||
|
|
639de4321e | ||
|
|
b3c461afdd | ||
|
|
7d154800a0 | ||
|
|
b48ed0399e | ||
|
|
c5d6e7d74a | ||
|
|
e82f915363 | ||
|
|
3128e9ce76 | ||
|
|
bc0e86757c | ||
|
|
fec99916c2 | ||
|
|
3b5d059b11 | ||
|
|
c3fe2acc8a | ||
|
|
4d002c412b |
25
.github/workflows/artifacts.yml
vendored
25
.github/workflows/artifacts.yml
vendored
@@ -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 }}'
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
8
src-tauri/macos/entitlements.plist
Normal file
8
src-tauri/macos/entitlements.plist
Normal 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>
|
||||||
@@ -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" => {
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 />;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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 ?? ''}
|
||||||
|
|||||||
9
src-web/components/ImageView.tsx
Normal file
9
src-web/components/ImageView.tsx
Normal 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>;
|
||||||
|
}
|
||||||
@@ -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}
|
||||||
|
|||||||
@@ -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',
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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} />
|
||||||
•
|
{activeResponse.elapsed > 0 && <> • {activeResponse.elapsed}ms</>}
|
||||||
{activeResponse.elapsed}ms •
|
{activeResponse.body.length > 0 && (
|
||||||
{Math.round(activeResponse.body.length / 1000)} KB
|
<> • {(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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -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}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}, [
|
}, [
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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' && (
|
||||||
|
|||||||
@@ -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',
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
28
src-web/components/core/StatusTag.tsx
Normal file
28
src-web/components/core/StatusTag.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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[]>(
|
||||||
|
|||||||
@@ -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),
|
||||||
@@ -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),
|
||||||
);
|
);
|
||||||
@@ -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);
|
|
||||||
}
|
}
|
||||||
|
|||||||
7
src-web/hooks/useLatestResponse.ts
Normal file
7
src-web/hooks/useLatestResponse.ts
Normal 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;
|
||||||
|
}
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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)"
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user