Compare commits

...

8 Commits

Author SHA1 Message Date
Gregory Schier
8cf45ba97f Bump version 2024-05-12 12:11:14 -07:00
Gregory Schier
6749fa9348 Add curl banner to import dialog 2024-05-10 13:36:30 -07:00
Gregory Schier
0f739834c8 Change curl import to post-toast 2024-05-10 13:06:40 -07:00
Gregory Schier
2d69b83765 Toast component and use for copy-as-curl 2024-05-10 12:37:04 -07:00
Gregory Schier
fa1765f356 Insomnia YAML and loading state on import 2024-05-10 09:46:20 -07:00
Gregory Schier
d1a0265ea5 Some fixes after upgrading react-query 2024-05-10 09:19:29 -07:00
Gregory Schier
bc191fec95 Update deps 2024-05-10 08:52:06 -07:00
Gregory Schier
43d042ae68 Fix paste handler in Editor.tsx 2024-05-09 23:17:43 -07:00
35 changed files with 4892 additions and 356 deletions

154
package-lock.json generated
View File

@@ -19,10 +19,7 @@
"@lezer/lr": "^1.3.3",
"@react-hook/resize-observer": "^1.2.6",
"@tailwindcss/container-queries": "^0.1.0",
"@tanstack/query-sync-storage-persister": "^4.27.1",
"@tanstack/react-query": "^4.28.0",
"@tanstack/react-query-devtools": "^4.28.0",
"@tanstack/react-query-persist-client": "^4.28.0",
"@tanstack/react-query": "^5.35.5",
"@tauri-apps/api": ">=2.0.0-beta.0",
"@tauri-apps/plugin-clipboard-manager": "^2.1.0-beta.1",
"@tauri-apps/plugin-dialog": ">=2.0.0-beta.0",
@@ -56,6 +53,7 @@
},
"devDependencies": {
"@tailwindcss/nesting": "^0.0.0-insiders.565cd3e",
"@tanstack/react-query-devtools": "^5.35.5",
"@tauri-apps/cli": ">=2.0.0-beta.0",
"@types/node": "^18.7.10",
"@types/papaparse": "^5.3.7",
@@ -2245,112 +2243,55 @@
"postcss": "^8.2.15"
}
},
"node_modules/@tanstack/match-sorter-utils": {
"version": "8.15.1",
"resolved": "https://registry.npmjs.org/@tanstack/match-sorter-utils/-/match-sorter-utils-8.15.1.tgz",
"integrity": "sha512-PnVV3d2poenUM31ZbZi/yXkBu3J7kd5k2u51CGwwNojag451AjTH9N6n41yjXz2fpLeewleyLBmNS6+HcGDlXw==",
"dependencies": {
"remove-accents": "0.5.0"
},
"engines": {
"node": ">=12"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tanstack/query-core": {
"version": "4.36.1",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-4.36.1.tgz",
"integrity": "sha512-DJSilV5+ytBP1FbFcEJovv4rnnm/CokuVvrBEtW/Va9DvuJ3HksbXUJEpI0aV1KtuL4ZoO9AVE6PyNLzF7tLeA==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tanstack/query-persist-client-core": {
"version": "4.36.1",
"resolved": "https://registry.npmjs.org/@tanstack/query-persist-client-core/-/query-persist-client-core-4.36.1.tgz",
"integrity": "sha512-eocgCeI7D7TRv1IUUBMfVwOI0wdSmMkBIbkKhqEdTrnUHUQEeOaYac8oeZk2cumAWJdycu6P/wB+WqGynTnzXg==",
"dependencies": {
"@tanstack/query-core": "4.36.1"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tanstack/query-sync-storage-persister": {
"version": "4.36.1",
"resolved": "https://registry.npmjs.org/@tanstack/query-sync-storage-persister/-/query-sync-storage-persister-4.36.1.tgz",
"integrity": "sha512-yMEt5hWe2+1eclf1agMtXHnPIkxEida0lYWkfdhR8U6KXk/lO4Vca6piJmhKI85t0NHlx3l/z6zX+t/Fn5O9NA==",
"dependencies": {
"@tanstack/query-persist-client-core": "4.36.1"
},
"node_modules/@tanstack/query-devtools": {
"version": "5.32.1",
"resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.32.1.tgz",
"integrity": "sha512-7Xq57Ctopiy/4atpb0uNY5VRuCqRS/1fi/WBCKKX6jHMa6cCgDuV/AQuiwRXcKARbq2OkVAOrW2v4xK9nTbcCA==",
"dev": true,
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tanstack/react-query": {
"version": "4.36.1",
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-4.36.1.tgz",
"integrity": "sha512-y7ySVHFyyQblPl3J3eQBWpXZkliroki3ARnBKsdJchlgt7yJLRDUcf4B8soufgiYt3pEQIkBWBx1N9/ZPIeUWw==",
"version": "5.35.5",
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.35.5.tgz",
"integrity": "sha512-sppX7L+PVn5GBV3In6zzj0zcKfnZRKhXbX1MfIfKo1OjIq2GMaopvAFOP0x1bRYTUk2ikrdYcQYOozX7PWkb8A==",
"dependencies": {
"@tanstack/query-core": "4.36.1",
"use-sync-external-store": "^1.2.0"
"@tanstack/query-core": "5.35.5"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react-native": "*"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
},
"react-native": {
"optional": true
}
"react": "^18.0.0"
}
},
"node_modules/@tanstack/react-query-devtools": {
"version": "4.36.1",
"resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-4.36.1.tgz",
"integrity": "sha512-WYku83CKP3OevnYSG8Y/QO9g0rT75v1om5IvcWUwiUZJ4LanYGLVCZ8TdFG5jfsq4Ej/lu2wwDAULEUnRIMBSw==",
"version": "5.35.5",
"resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.35.5.tgz",
"integrity": "sha512-4Xll14B9uhgEJ+uqZZ5tqZ7G1LDR7wGYgb+NOZHGn11TTABnlV8GWon7zDMqdaHeR5mjjuY1UFo9pbz39kuZKQ==",
"dev": true,
"dependencies": {
"@tanstack/match-sorter-utils": "^8.7.0",
"superjson": "^1.10.0",
"use-sync-external-store": "^1.2.0"
"@tanstack/query-devtools": "5.32.1"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"@tanstack/react-query": "^4.36.1",
"react": "^16.8.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0"
"@tanstack/react-query": "^5.35.5",
"react": "^18.0.0"
}
},
"node_modules/@tanstack/react-query-persist-client": {
"version": "4.36.1",
"resolved": "https://registry.npmjs.org/@tanstack/react-query-persist-client/-/react-query-persist-client-4.36.1.tgz",
"integrity": "sha512-32I5b9aAu4NCiXZ7Te/KEQLfHbYeTNriVPrKYcvEThnZ9tlW01vLcSoxpUIsMYRsembvJUUAkzYBAiZHLOd6pQ==",
"dependencies": {
"@tanstack/query-persist-client-core": "4.36.1"
},
"node_modules/@tanstack/react-query/node_modules/@tanstack/query-core": {
"version": "5.35.5",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.35.5.tgz",
"integrity": "sha512-OMWvlEqG01RfGj+XZb/piDzPp0eZkkHWSDHt2LvE/fd1zWburP/xwm0ghk6Iv8cuPlP+ACFkZviKXK0OVt6lhg==",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"@tanstack/react-query": "^4.36.1"
}
},
"node_modules/@tauri-apps/api": {
@@ -4248,20 +4189,6 @@
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
"dev": true
},
"node_modules/copy-anything": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz",
"integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==",
"dependencies": {
"is-what": "^4.1.8"
},
"engines": {
"node": ">=12.13"
},
"funding": {
"url": "https://github.com/sponsors/mesqueeb"
}
},
"node_modules/copy-to-clipboard": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz",
@@ -7068,17 +6995,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-what": {
"version": "4.1.16",
"resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz",
"integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==",
"engines": {
"node": ">=12.13"
},
"funding": {
"url": "https://github.com/sponsors/mesqueeb"
}
},
"node_modules/isarray": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
@@ -9376,11 +9292,6 @@
"node": ">=0.10.0"
}
},
"node_modules/remove-accents": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.5.0.tgz",
"integrity": "sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A=="
},
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@@ -10392,17 +10303,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/superjson": {
"version": "1.13.3",
"resolved": "https://registry.npmjs.org/superjson/-/superjson-1.13.3.tgz",
"integrity": "sha512-mJiVjfd2vokfDxsQPOwJ/PtanO87LhpYY88ubI5dUB1Ab58Txbyje3+jpm+/83R/fevaq/107NNhtYBLuoTrFg==",
"dependencies": {
"copy-anything": "^3.0.2"
},
"engines": {
"node": ">=10"
}
},
"node_modules/supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
@@ -11047,14 +10947,6 @@
"node": ">=0.10.0"
}
},
"node_modules/use-sync-external-store": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz",
"integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",

View File

@@ -39,10 +39,7 @@
"@lezer/lr": "^1.3.3",
"@react-hook/resize-observer": "^1.2.6",
"@tailwindcss/container-queries": "^0.1.0",
"@tanstack/query-sync-storage-persister": "^4.27.1",
"@tanstack/react-query": "^4.28.0",
"@tanstack/react-query-devtools": "^4.28.0",
"@tanstack/react-query-persist-client": "^4.28.0",
"@tanstack/react-query": "^5.35.5",
"@tauri-apps/api": ">=2.0.0-beta.0",
"@tauri-apps/plugin-clipboard-manager": "^2.1.0-beta.1",
"@tauri-apps/plugin-dialog": ">=2.0.0-beta.0",
@@ -76,6 +73,7 @@
},
"devDependencies": {
"@tailwindcss/nesting": "^0.0.0-insiders.565cd3e",
"@tanstack/react-query-devtools": "^5.35.5",
"@tauri-apps/cli": ">=2.0.0-beta.0",
"@types/node": "^18.7.10",
"@types/papaparse": "^5.3.7",

View File

@@ -6,7 +6,21 @@
"packages": {
"": {
"name": "importer-insomnia",
"version": "0.0.1"
"version": "0.0.1",
"dependencies": {
"yaml": "^2.4.2"
}
},
"node_modules/yaml": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.2.tgz",
"integrity": "sha512-B3VqDZ+JAg1nZpaEmWtTXUlBneoGx6CPM9b0TENK6aoSu5t73dItudwdgmi6tHlIZZId4dZ9skcAQ2UbcyAeVA==",
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14"
}
}
}
}

View File

@@ -1,4 +1,7 @@
{
"name": "importer-insomnia",
"version": "0.0.1"
"version": "0.0.1",
"dependencies": {
"yaml": "^2.4.2"
}
}

View File

@@ -5,6 +5,7 @@ import {
HttpRequest,
Workspace,
} from '../../../src-web/lib/models';
import { parse as parseYaml } from 'yaml';
type AtLeast<T, K extends keyof T> = Partial<T> & Pick<T, K>;
@@ -17,12 +18,15 @@ export interface ExportResources {
}
export function pluginHookImport(contents: string) {
let parsed;
let parsed: any;
try {
parsed = JSON.parse(contents);
} catch (e) {
return;
}
} catch (e) {}
try {
parsed = parseYaml(contents);
} catch (e) {}
if (!isJSObject(parsed)) return;
if (!Array.isArray(parsed.resources)) return;

View File

@@ -10,6 +10,7 @@
"os:allow-os-type",
"event:allow-emit",
"clipboard-manager:allow-write-text",
"clipboard-manager:allow-read-text",
"dialog:allow-open",
"dialog:allow-save",
"event:allow-listen",

View File

@@ -1 +1 @@
{"main":{"identifier":"main","description":"Main permissions","local":true,"windows":["*"],"permissions":["os:allow-os-type","event:allow-emit","clipboard-manager:allow-write-text","dialog:allow-open","dialog:allow-save","event:allow-listen","event:allow-unlisten","fs:allow-read-file","fs:allow-read-text-file",{"identifier":"fs:scope","allow":[{"path":"$APPDATA"},{"path":"$APPDATA/**"}]},"shell:allow-open",{"identifier":"shell:allow-execute","allow":[{"args":true,"name":"protoc","sidecar":true}]},"window:allow-close","window:allow-is-fullscreen","window:allow-maximize","window:allow-minimize","window:allow-set-decorations","window:allow-set-title","window:allow-start-dragging","window:allow-unmaximize","clipboard-manager:default"]}}
{"main":{"identifier":"main","description":"Main permissions","local":true,"windows":["*"],"permissions":["os:allow-os-type","event:allow-emit","clipboard-manager:allow-write-text","clipboard-manager:allow-read-text","dialog:allow-open","dialog:allow-save","event:allow-listen","event:allow-unlisten","fs:allow-read-file","fs:allow-read-text-file",{"identifier":"fs:scope","allow":[{"path":"$APPDATA"},{"path":"$APPDATA/**"}]},"shell:allow-open",{"identifier":"shell:allow-execute","allow":[{"args":true,"name":"protoc","sidecar":true}]},"window:allow-close","window:allow-is-fullscreen","window:allow-maximize","window:allow-minimize","window:allow-set-decorations","window:allow-set-title","window:allow-start-dragging","window:allow-unmaximize","clipboard-manager:default"]}}

File diff suppressed because it is too large Load Diff

View File

@@ -91,16 +91,19 @@ struct AppMetaData {
version: String,
name: String,
app_data_dir: String,
app_log_dir: String,
}
#[tauri::command]
async fn cmd_metadata(app_handle: AppHandle) -> Result<AppMetaData, ()> {
let app_data_dir = app_handle.path().app_data_dir().unwrap();
let app_log_dir = app_handle.path().app_log_dir().unwrap();
return Ok(AppMetaData {
is_dev: is_dev(),
version: app_handle.package_info().version.to_string(),
name: app_handle.package_info().name.to_string(),
app_data_dir: app_data_dir.to_string_lossy().to_string(),
app_log_dir: app_log_dir.to_string_lossy().to_string(),
});
}

View File

@@ -6,7 +6,7 @@
"frontendDist": "../dist"
},
"productName": "Yaak",
"version": "2024.4.0-beta.1",
"version": "2024.4.0-beta.2",
"identifier": "app.yaak.desktop",
"app": {
"withGlobalTauri": false,

View File

@@ -7,7 +7,6 @@ import { HelmetProvider } from 'react-helmet-async';
import { AppRouter } from './AppRouter';
const queryClient = new QueryClient({
logger: undefined,
defaultOptions: {
queries: {
retry: false,

View File

@@ -1,12 +1,15 @@
import { Outlet } from 'react-router-dom';
import { DialogProvider } from './DialogContext';
import { GlobalHooks } from './GlobalHooks';
import { ToastProvider } from './ToastContext';
export function DefaultLayout() {
return (
<DialogProvider>
<Outlet />
<GlobalHooks />
<ToastProvider>
<Outlet />
<GlobalHooks />
</ToastProvider>
</DialogProvider>
);
}

View File

@@ -94,7 +94,7 @@ export function GrpcConnectionLayout({ style }: Props) {
/>
)}
secondSlot={({ style }) =>
!grpc.go.isLoading && (
!grpc.go.isPending && (
<div
style={style}
className={classNames(

View File

@@ -71,7 +71,7 @@ export function GrpcProtoSelection({ requestId }: Props) {
{!serverReflection && services != null && services.length > 0 && (
<Banner className="flex flex-col gap-2">
<p>
Found services
Found services{' '}
{services?.slice(0, 5).map((s, i) => {
return (
<span key={i}>
@@ -124,7 +124,6 @@ export function GrpcProtoSelection({ requestId }: Props) {
className="ml-auto opacity-30 transition-opacity group-hover:opacity-100"
onClick={async () => {
await protoFilesKv.set(protoFiles.filter((p) => p !== f));
grpc.reflect.remove();
}}
/>
</td>

View File

@@ -0,0 +1,42 @@
import { VStack } from './core/Stacks';
import { Button } from './core/Button';
import React, { useState } from 'react';
import { Banner } from './core/Banner';
import { Icon } from './core/Icon';
interface Props {
importData: () => Promise<void>;
}
export function ImportDataDialog({ importData }: Props) {
const [isLoading, setIsLoading] = useState<boolean>(false);
return (
<VStack space={5} className="pb-4">
<VStack space={1}>
<ul className="list-disc pl-5">
<li>Postman Collection v2+</li>
<li>Insomnia v4+</li>
<li>Curl commands</li>
</ul>
<Banner className="mt-3 flex items-center gap-2">
<Icon icon="magicWand" />
Paste any Curl command into URL bar
</Banner>
</VStack>
<Button
color="primary"
isLoading={isLoading}
onClick={async () => {
setIsLoading(true);
try {
await importData();
} finally {
setIsLoading(false);
}
}}
>
{isLoading ? 'Importing' : 'Select File'}
</Button>
</VStack>
);
}

View File

@@ -1,6 +1,6 @@
import classNames from 'classnames';
import type { CSSProperties } from 'react';
import { memo, useCallback, useMemo, useState } from 'react';
import React, { memo, useCallback, useMemo, useState } from 'react';
import { createGlobalState } from 'react-use';
import { useCancelHttpResponse } from '../hooks/useCancelHttpResponse';
import { useIsResponseLoading } from '../hooks/useIsResponseLoading';
@@ -39,7 +39,8 @@ import { HeadersEditor } from './HeadersEditor';
import { UrlBar } from './UrlBar';
import { UrlParametersEditor } from './UrlParameterEditor';
import { useCurlToRequest } from '../hooks/useCurlToRequest';
import { useConfirm } from '../hooks/useConfirm';
import { useToast } from './ToastContext';
import { Icon } from './core/Icon';
interface Props {
style: CSSProperties;
@@ -230,7 +231,7 @@ export const RequestPane = memo(function RequestPane({
);
const importCurl = useCurlToRequest();
const confirm = useConfirm();
const toast = useToast();
const isLoading = useIsResponseLoading(activeRequestId ?? null);
const { updateKey } = useRequestUpdateKey(activeRequestId ?? null);
@@ -250,17 +251,15 @@ export const RequestPane = memo(function RequestPane({
if (!command.startsWith('curl ')) {
return;
}
if (
await confirm({
id: 'paste-curl',
title: 'Import from Curl?',
description:
'Do you want to overwrite the current request with the Curl command?',
confirmText: 'Overwrite',
})
) {
importCurl.mutate({ requestId: activeRequestId, command });
}
importCurl.mutate({ requestId: activeRequestId, command });
toast.show({
render: () => [
<>
<Icon icon="info" />
<span>Curl command imported</span>
</>,
],
});
}}
onSend={handleSend}
onCancel={handleCancel}

View File

@@ -81,7 +81,7 @@ export const SettingsDialog = () => {
size="sm"
title="Check for updates"
icon="refresh"
spin={checkForUpdates.isLoading}
spin={checkForUpdates.isPending}
onClick={() => checkForUpdates.mutateAsync()}
/>
</div>
@@ -135,6 +135,7 @@ export const SettingsDialog = () => {
<KeyValueRows>
<KeyValueRow label="Version" value={appInfo.data?.version} />
<KeyValueRow label="Data Directory" value={appInfo.data?.appDataDir} />
<KeyValueRow label="Logs Directory" value={appInfo.data?.appLogDir} />
</KeyValueRows>
</VStack>
);

View File

@@ -9,7 +9,6 @@ import { useActiveEnvironmentId } from '../hooks/useActiveEnvironmentId';
import { useActiveRequest } from '../hooks/useActiveRequest';
import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
import { useAppRoutes } from '../hooks/useAppRoutes';
import { useCopyAsCurl } from '../hooks/useCopyAsCurl';
import { useCreateDropdownItems } from '../hooks/useCreateDropdownItems';
import { useDeleteFolder } from '../hooks/useDeleteFolder';
import { useDeleteRequest } from '../hooks/useDeleteRequest';
@@ -41,6 +40,7 @@ import { InlineCode } from './core/InlineCode';
import { VStack } from './core/Stacks';
import { StatusTag } from './core/StatusTag';
import { DropMarker } from './DropMarker';
import { useCopyAsCurl } from '../hooks/useCopyAsCurl';
interface Props {
className?: string;
@@ -608,7 +608,7 @@ const SidebarItem = forwardRef(function SidebarItem(
const deleteRequest = useDeleteRequest(activeRequest ?? null);
const duplicateHttpRequest = useDuplicateHttpRequest({ id: itemId, navigateAfter: true });
const duplicateGrpcRequest = useDuplicateGrpcRequest({ id: itemId, navigateAfter: true });
const [isCopied, copyAsCurl] = useCopyAsCurl(itemId);
const [copyAsCurl] = useCopyAsCurl(itemId);
const sendRequest = useSendRequest(itemId);
const sendManyRequests = useSendManyRequests();
const latestHttpResponse = useLatestHttpResponse(itemId);
@@ -739,12 +739,7 @@ const SidebarItem = forwardRef(function SidebarItem(
{
key: 'copyCurl',
label: 'Copy as Curl',
leftSlot: (
<Icon
className={isCopied ? 'text-green-500' : undefined}
icon={isCopied ? 'check' : 'copy'}
/>
),
leftSlot: <Icon icon="copy" />,
onSelect: copyAsCurl,
},
{ type: 'separator' },

View File

@@ -0,0 +1,79 @@
import React, { createContext, useContext, useMemo, useRef, useState } from 'react';
import type { ToastProps } from './core/Toast';
import { Toast } from './core/Toast';
import { generateId } from '../lib/generateId';
import { Portal } from './Portal';
import { AnimatePresence } from 'framer-motion';
type ToastEntry = {
render: ({ hide }: { hide: () => void }) => React.ReactNode;
timeout?: number;
} & Omit<ToastProps, 'onClose' | 'open' | 'children' | 'timeout'>;
type PrivateToastEntry = ToastEntry & {
id: string;
timeout: number;
};
interface State {
toasts: PrivateToastEntry[];
actions: Actions;
}
interface Actions {
show: (d: ToastEntry) => void;
hide: (id: string) => void;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const ToastContext = createContext<State>({} as State);
export const ToastProvider = ({ children }: { children: React.ReactNode }) => {
const [toasts, setToasts] = useState<State['toasts']>([]);
const timeoutRef = useRef<NodeJS.Timeout>();
const actions = useMemo<Actions>(
() => ({
show({ timeout = 4000, ...props }: ToastEntry) {
const id = generateId();
timeoutRef.current = setTimeout(() => {
this.hide(id);
}, timeout);
setToasts((a) => [...a.filter((d) => d.id !== id), { id, timeout, ...props }]);
return id;
},
hide: (id: string) => {
setToasts((a) => a.filter((d) => d.id !== id));
},
}),
[],
);
const state: State = { toasts, actions };
return (
<ToastContext.Provider value={state}>
{children}
<Portal name="toasts">
<div className="absolute right-0 bottom-0">
<AnimatePresence>
{toasts.map((props: PrivateToastEntry) => (
<ToastInstance key={props.id} {...props} />
))}
</AnimatePresence>
</div>
</Portal>
</ToastContext.Provider>
);
};
function ToastInstance({ id, render, timeout, ...props }: PrivateToastEntry) {
const { actions } = useContext(ToastContext);
const children = render({ hide: () => actions.hide(id) });
return (
<Toast open timeout={timeout} onClose={() => actions.hide(id)} {...props}>
{children}
</Toast>
);
}
export const useToast = () => useContext(ToastContext).actions;

View File

@@ -6,7 +6,7 @@ import type {
MouseEvent as ReactMouseEvent,
ReactNode,
} from 'react';
import { useCallback, useMemo, useRef, useState } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useWindowSize } from 'react-use';
import { useActiveRequest } from '../hooks/useActiveRequest';
import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
@@ -33,6 +33,8 @@ import { ResizeHandle } from './ResizeHandle';
import { Sidebar } from './Sidebar';
import { SidebarActions } from './SidebarActions';
import { WorkspaceHeader } from './WorkspaceHeader';
import { useClipboardText } from '../hooks/useClipboardText';
import { useToast } from './ToastContext';
const side = { gridArea: 'side' };
const head = { gridArea: 'head' };
@@ -54,6 +56,24 @@ export default function Workspace() {
const moveState = useRef<{ move: (e: MouseEvent) => void; up: (e: MouseEvent) => void } | null>(
null,
);
const clipboardText = useClipboardText();
const toast = useToast();
useEffect(() => {
const isCurlInClipboard = clipboardText?.startsWith('curl ');
if (!isCurlInClipboard) {
return;
}
toast.show({
render: () => (
<div>
<p>Curl command detected?</p>
<Button color="primary">Import</Button>
</div>
),
});
}, [clipboardText, toast]);
const unsub = () => {
if (moveState.current !== null) {

View File

@@ -60,9 +60,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
size === 'sm' && 'h-sm px-2.5 text-sm',
size === 'xs' && 'h-xs px-2 text-sm',
// Solids
variant === 'solid' &&
color === 'custom' &&
'ring-blue-400 enabled:hocus:bg-highlightSecondary',
variant === 'solid' && color === 'custom' && 'ring-blue-400',
variant === 'solid' &&
color === 'default' &&
'text-gray-700 enabled:hocus:bg-gray-700/10 enabled:hocus:text-gray-800 ring-blue-400',
@@ -118,13 +116,13 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
ref={buttonRef}
type={type}
className={classes}
disabled={disabled}
disabled={disabled || isLoading}
onClick={onClick}
title={fullTitle}
{...props}
>
{isLoading ? (
<Icon icon="update" size={size} className="animate-spin mr-1" />
<Icon icon="refresh" size={size} className="animate-spin mr-1" />
) : leftSlot ? (
<div className="mr-1">{leftSlot}</div>
) : null}

View File

@@ -329,10 +329,18 @@ function getExtensions({
return [
// NOTE: These *must* be anonymous functions so the references update properly
EditorView.domEventHandlers({
focus: () => onFocus.current?.(),
blur: () => onBlur.current?.(),
keydown: (e) => onKeyDown.current?.(e),
paste: (e) => onPaste.current?.(e.clipboardData?.getData('text/plain') ?? ''),
focus: () => {
onFocus.current?.();
},
blur: () => {
onBlur.current?.();
},
keydown: (e) => {
onKeyDown.current?.(e);
},
paste: (e) => {
onPaste.current?.(e.clipboardData?.getData('text/plain') ?? '');
},
}),
tooltips({ parent }),
keymap.of(singleLine ? defaultKeymap.filter((k) => k.key !== 'Enter') : defaultKeymap),

View File

@@ -17,7 +17,6 @@ const icons = {
arrowUpFromDot: lucide.ArrowUpFromDotIcon,
box: lucide.BoxIcon,
cake: lucide.CakeIcon,
minus: lucide.MinusIcon,
chat: lucide.MessageSquare,
check: lucide.CheckIcon,
chevronDown: lucide.ChevronDownIcon,
@@ -25,6 +24,7 @@ const icons = {
code: lucide.CodeIcon,
cookie: lucide.CookieIcon,
copy: lucide.CopyIcon,
copyCheck: lucide.CopyCheck,
download: lucide.DownloadIcon,
externalLink: lucide.ExternalLinkIcon,
eye: lucide.EyeIcon,
@@ -39,6 +39,7 @@ const icons = {
leftPanelHidden: lucide.PanelLeftOpenIcon,
leftPanelVisible: lucide.PanelLeftCloseIcon,
magicWand: lucide.Wand2Icon,
minus: lucide.MinusIcon,
moreVertical: lucide.MoreVerticalIcon,
pencil: lucide.PencilIcon,
plug: lucide.Plug,

View File

@@ -0,0 +1,76 @@
import classNames from 'classnames';
import { motion } from 'framer-motion';
import type { ReactNode } from 'react';
import React, { useMemo } from 'react';
import { useKey } from 'react-use';
import { Heading } from './Heading';
import { IconButton } from './IconButton';
export interface ToastProps {
children: ReactNode;
open: boolean;
onClose: () => void;
title?: ReactNode;
className?: string;
timeout: number;
}
export function Toast({ children, className, open, onClose, title, timeout }: ToastProps) {
const titleId = useMemo(() => Math.random().toString(36).slice(2), []);
useKey(
'Escape',
() => {
if (!open) return;
onClose();
},
{},
[open],
);
return (
<motion.div
initial={{ opacity: 0, right: '-10%' }}
animate={{ opacity: 100, right: 0 }}
exit={{ opacity: 0, right: '-100%' }}
transition={{ duration: 0.2 }}
className={classNames(
className,
'pointer-events-auto',
'relative bg-gray-50 dark:bg-gray-100 pointer-events-auto',
'rounded-lg',
'border border-highlightSecondary dark:border-highlight shadow-xl',
'max-w-[calc(100vw-5rem)] max-h-[calc(100vh-6rem)]',
'w-[22rem] max-h-[80vh]',
'm-2 grid grid-cols-[1fr_auto]',
'text-gray-700',
)}
>
<div className="px-3 py-2">
{title && (
<Heading size={3} id={titleId}>
{title}
</Heading>
)}
<div className="flex items-center gap-2">{children}</div>
</div>
<IconButton
color="custom"
className="opacity-50"
title="Dismiss"
icon="x"
onClick={onClose}
/>
<div className="w-full absolute bottom-0 left-0 right-0">
<motion.div
className="bg-highlight h-0.5"
initial={{ width: '100%' }}
animate={{ width: '0%', opacity: 0.2 }}
transition={{ duration: timeout / 1000, ease: 'linear' }}
/>
</div>
</motion.div>
);
}

View File

@@ -1,13 +1,20 @@
import { useQuery } from '@tanstack/react-query';
import { invoke } from '@tauri-apps/api/core';
export interface AppInfo {
isDev: boolean;
version: string;
name: string;
appDataDir: string;
appLogDir: string;
}
export function useAppInfo() {
return useQuery(['appInfo'], async () => {
return (await invoke('cmd_metadata')) as {
isDev: boolean;
version: string;
name: string;
appDataDir: string;
};
return useQuery({
queryKey: ['appInfo'],
queryFn: async () => {
const metadata = await invoke('cmd_metadata');
return metadata as AppInfo;
},
});
}

View File

@@ -0,0 +1,9 @@
import { useQuery } from '@tanstack/react-query';
import { readText } from '@tauri-apps/plugin-clipboard-manager';
export function useClipboardText() {
return useQuery({
queryKey: [],
queryFn: () => readText(),
}).data;
}

View File

@@ -1,9 +1,12 @@
import { invoke } from '@tauri-apps/api/core';
import { writeText } from '@tauri-apps/plugin-clipboard-manager';
import { useState } from 'react';
import React, { useState } from 'react';
import { useToast } from '../components/ToastContext';
import { Icon } from '../components/core/Icon';
export function useCopyAsCurl(requestId: string) {
const [checked, setChecked] = useState<boolean>(false);
const toast = useToast();
return [
checked,
async () => {
@@ -11,6 +14,14 @@ export function useCopyAsCurl(requestId: string) {
await writeText(cmd);
setChecked(true);
setTimeout(() => setChecked(false), 800);
toast.show({
render: () => [
<>
<Icon icon="copyCheck" />
<span>Command copied to clipboard</span>
</>,
],
});
return cmd;
},
] as const;

View File

@@ -35,7 +35,9 @@ export function useCreateFolder() {
},
onSettled: () => trackEvent('folder', 'create'),
onSuccess: async (request) => {
await queryClient.invalidateQueries(foldersQueryKey({ workspaceId: request.workspaceId }));
await queryClient.invalidateQueries({
queryKey: foldersQueryKey({ workspaceId: request.workspaceId }),
});
},
});
}

View File

@@ -36,8 +36,8 @@ export function useDeleteFolder(id: string | null) {
const { workspaceId } = folder;
// Nesting makes it hard to clean things up, so just clear everything that could have been deleted
await queryClient.invalidateQueries(httpRequestsQueryKey({ workspaceId }));
await queryClient.invalidateQueries(foldersQueryKey({ workspaceId }));
await queryClient.invalidateQueries({ queryKey: httpRequestsQueryKey({ workspaceId }) });
await queryClient.invalidateQueries({ queryKey: foldersQueryKey({ workspaceId }) });
},
});
}

View File

@@ -44,7 +44,7 @@ export function useDeleteWorkspace(workspace: Workspace | null) {
// Also clean up other things that may have been deleted
queryClient.setQueryData(httpRequestsQueryKey({ workspaceId }), []);
await queryClient.invalidateQueries(httpRequestsQueryKey({ workspaceId }));
await queryClient.invalidateQueries({ queryKey: httpRequestsQueryKey({ workspaceId }) });
},
});
}

View File

@@ -46,7 +46,7 @@ export function useGrpc(
const debouncedUrl = useDebouncedValue<string>(req?.url ?? 'n/a', 500);
const reflect = useQuery<ReflectResponseService[], string>({
enabled: req != null,
queryKey: ['grpc_reflect', req?.id ?? 'n/a', debouncedUrl],
queryKey: ['grpc_reflect', req?.id ?? 'n/a', debouncedUrl, protoFiles],
refetchOnWindowFocus: false,
queryFn: async () => {
return (await minPromiseMillis(

View File

@@ -9,6 +9,7 @@ import { count } from '../lib/pluralize';
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
import { useAlert } from './useAlert';
import { useAppRoutes } from './useAppRoutes';
import { ImportDataDialog } from '../components/ImportDataDialog';
export function useImportData() {
const routes = useAppRoutes();
@@ -16,13 +17,13 @@ export function useImportData() {
const alert = useAlert();
const activeWorkspaceId = useActiveWorkspaceId();
const importData = async () => {
const importData = async (): Promise<boolean> => {
const selected = await open({
filters: [{ name: 'Export File', extensions: ['json', 'yaml', 'sh', 'txt'] }],
multiple: false,
});
if (selected == null) {
return;
return false;
}
const imported: {
@@ -35,6 +36,7 @@ export function useImportData() {
filePath: selected.path,
workspaceId: activeWorkspaceId,
});
const importedWorkspace = imported.workspaces[0];
dialog.show({
@@ -69,6 +71,8 @@ export function useImportData() {
environmentId: imported.environments[0]?.id,
});
}
return true;
};
return useMutation({
@@ -82,32 +86,20 @@ export function useImportData() {
title: 'Import Data',
size: 'sm',
render: ({ hide }) => {
return (
<VStack space={5} className="pb-4">
<VStack space={1}>
<p>Supported Formats:</p>
<ul className="list-disc pl-5">
<li>Postman Collection v2/v2.1</li>
<li>Insomnia</li>
</ul>
</VStack>
<Button
size="sm"
color="primary"
onClick={async () => {
try {
await importData();
resolve();
} catch (err) {
reject(err);
}
hide();
}}
>
Select File
</Button>
</VStack>
);
const importAndHide = async () => {
try {
const didImport = await importData();
if (!didImport) {
return;
}
resolve();
} catch (err) {
reject(err);
} finally {
hide();
}
};
return <ImportDataDialog importData={importAndHide} />;
},
});
});

View File

@@ -9,8 +9,12 @@ export function workspacesQueryKey(_?: {}) {
export function useWorkspaces() {
return (
useQuery(workspacesQueryKey(), async () => {
return (await invoke('cmd_list_workspaces')) as Workspace[];
useQuery({
queryKey: workspacesQueryKey(),
queryFn: async () => {
const workspaces = await invoke('cmd_list_workspaces');
return workspaces as Workspace[];
},
}).data ?? []
);
}

View File

@@ -14,6 +14,7 @@ export type TrackResource =
| 'key_value'
| 'setting'
| 'sidebar'
| 'toast'
| 'workspace';
export type TrackAction =

View File

@@ -4,7 +4,7 @@ import { internalIpV4 } from 'internal-ip';
import svgr from 'vite-plugin-svgr';
import topLevelAwait from 'vite-plugin-top-level-await';
const mobile = !!/android|ios/.exec(process.env.TAURI_ENV_PLATFORM);
const mobile = !!/android|ios/.exec(process.env.TAURI_ENV_PLATFORM ?? '');
// https://vitejs.dev/config/
export default defineConfig(async () => ({