Compare commits

..

65 Commits

Author SHA1 Message Date
Gregory Schier
c29b3c6509 Change version 2024-06-17 15:09:44 -07:00
Gregory Schier
7faa423aba Recursive environments 2024-06-17 12:24:06 -07:00
Gregory Schier
5b2162e48d Fix flash loading response viewer 2024-06-17 11:43:45 -07:00
Gregory Schier
ee776143b2 Fix parsing notification timestamp 2024-06-17 11:43:20 -07:00
Gregory Schier
7a18fb29e4 More dynamic plugin access 2024-06-15 22:13:01 -07:00
Gregory Schier
4485cad9e8 Workspace dropdown to RadioDropdown 2024-06-14 17:07:35 -07:00
Gregory Schier
33763b6d2f Fix plugins 2024-06-13 07:03:50 -07:00
Gregory Schier
641fe86cf7 Support nested functions 2024-06-12 23:13:36 -07:00
Gregory Schier
65e7c804d7 Parse window title and theme better 2024-06-12 10:15:14 -07:00
Gregory Schier
23ec8bee8f Move binary detection to TextViewer 2024-06-12 09:47:34 -07:00
Gregory Schier
8aeeaa2e09 Bump version 2024-06-12 00:23:50 -07:00
Gregory Schier
57f01d249e Entitlement for v8/Deno 2024-06-12 00:23:32 -07:00
Gregory Schier
6c5a914db6 Variables in pair editor 2024-06-11 12:33:43 -07:00
Gregory Schier
155e51aa74 Put toast on top 2024-06-11 12:04:46 -07:00
Gregory Schier
012a984456 Fix bulk edit label 2024-06-11 09:02:11 -07:00
Gregory Schier
25800202f2 Bump version 2024-06-11 08:35:29 -07:00
Gregory Schier
a058064f1f Adjust fuzzy threshold 2024-06-10 23:25:57 -07:00
Gregory Schier
9f40804532 Fix window drag above cmd+k 2024-06-10 23:24:03 -07:00
Gregory Schier
26cc467858 Fix delete request in sidebar 2024-06-10 23:17:12 -07:00
Gregory Schier
be1cf7bf65 Don't arrow-nav to hidden dropdown items 2024-06-10 23:16:37 -07:00
Gregory Schier
ea4f104ca7 Bulk edit environments 2024-06-10 23:16:21 -07:00
Gregory Schier
32a28a3170 Fix hotkey react keys 2024-06-10 22:29:06 -07:00
Gregory Schier
6215914212 Cmd Palette Improvements (#50)
- Fuzzy matching
- Show hotkeys
- Add actions
2024-06-10 21:37:41 -07:00
Gregory Schier
a2dbd7f849 Download Active Response (#49)
This PR prompts you to save un-previewable file types and adds an option
to save to the response history.
2024-06-10 16:36:09 -07:00
Gregory Schier
5bb9815f4b Remove jump to request hotkey 2024-06-10 09:00:26 -07:00
Gregory Schier
7cd8ac3b21 Response viewer for PDF (#48)
This PR adds a response viewer for PDF files using `react-pdf`
2024-06-10 08:57:08 -07:00
Gregory Schier
456d3aaf52 Don't focus sidebar on cmd+0 2024-06-09 08:36:12 -07:00
Gregory Schier
113743f7cf Try fix CI 2024-06-09 08:09:47 -07:00
Gregory Schier
01a4d6f4ac Bump version 2024-06-09 07:56:44 -07:00
Gregory Schier
ff5cfe744e Remove CI again 2024-06-09 07:56:09 -07:00
Gregory Schier
29c4b51f54 Add it back 2024-06-09 07:23:28 -07:00
Gregory Schier
2f74bf8db8 Remove CI in release stage 2024-06-09 07:22:49 -07:00
Gregory Schier
b30d784d06 Remove GH Action step names 2024-06-09 07:16:53 -07:00
Gregory Schier
ac0adaf3d8 Back to other 2024-06-09 07:14:07 -07:00
Gregory Schier
c246d3a748 Fix action step 2024-06-09 07:10:44 -07:00
Gregory Schier
ae40728c1e Rust cache release 2024-06-09 07:09:46 -07:00
Gregory Schier
0430ec883b Cmd jump to request 2024-06-09 07:03:16 -07:00
Gregory Schier
eba6f33536 Only CI on PR 2024-06-09 06:47:47 -07:00
Gregory Schier
dae2873376 Fix Rust lint 2024-06-09 06:43:53 -07:00
Gregory Schier
cb2f56d9a1 Remove slow udeps from CI 2024-06-08 21:03:17 -07:00
Gregory Schier
7d82aa70a4 Rust CI to release 2024-06-08 20:51:03 -07:00
Gregory Schier
adea234987 Try again 2024-06-08 20:47:48 -07:00
Gregory Schier
37d0b487b8 Remove cache 2024-06-08 20:47:18 -07:00
Gregory Schier
d507f8c99f Try removing git db 2024-06-08 20:44:58 -07:00
Gregory Schier
60406ac83f Cargo check 2024-06-08 20:36:56 -07:00
Gregory Schier
8fe6f3a335 Remove Cargo fmt 2024-06-08 20:34:24 -07:00
Gregory Schier
69e027c302 Fix tests 2024-06-08 20:30:52 -07:00
Gregory Schier
4232bdd298 Cargo format 2024-06-08 20:28:45 -07:00
Gregory Schier
ef1c5da027 Set max dropdown width 2024-06-08 19:55:25 -07:00
Gregory Schier
e250326868 Bump version 2024-06-08 19:46:32 -07:00
Gregory Schier
125f503cfa Upgrade reqwest and add ALPN for http/2 2024-06-08 19:40:35 -07:00
Gregory Schier
8f086425fe Fix curl exporter 2024-06-08 19:40:11 -07:00
Gregory Schier
ae2da73873 Fix cmd+k filtering 2024-06-07 22:52:23 -07:00
Gregory Schier
3cd6688ffb Remove delete hotkey for request 2024-06-07 22:52:10 -07:00
Gregory Schier
8538da8879 Short method tags and hide active in cmd+k 2024-06-07 22:39:11 -07:00
Gregory Schier
b0e4ece278 Add command palette (#46)
This PR finished the initial PoC command palette. It currently only
supports switching between requests and workspaces, but can easily be
extended for more.
2024-06-07 21:59:57 -07:00
Gregory Schier
5e058af03e Bulk editor (#45)
Bulk editor for all pair editors except multipart/form-data
2024-06-07 13:42:08 -07:00
Gregory Schier
5108bc92f3 Fix rose pine and method tags 2024-06-07 12:01:31 -07:00
Gregory Schier
3c5fdcb18d Generate things 2024-06-07 11:58:08 -07:00
Gregory Schier
4672de4a47 Remove tauri-plugin-deep-link 2024-06-07 11:57:44 -07:00
Gregory Schier
239f6da141 Update CI script 2024-06-07 11:01:21 -07:00
Gregory Schier
ec148d1736 Fix workflow 2024-06-07 10:54:50 -07:00
Gregory Schier
392b549646 Deno plugins (#42)
Switch from BoaJS to Deno core
2024-06-07 10:47:41 -07:00
Gregory Schier
993d4dc65d Open workspace pref (#44)
Ability to remember workspace window opening selection.
2024-06-07 09:04:53 -07:00
Gregory Schier
e326405f4f Templating (#43)
Add new `templating` crate with custom parser/renderer for dealing with
variables
2024-06-07 08:39:12 -07:00
134 changed files with 4716 additions and 6289 deletions

18
.github/workflows/ci-js.yml vendored Normal file
View File

@@ -0,0 +1,18 @@
on:
pull_request:
branches: [develop]
name: CI (JS)
jobs:
test:
name: Lint/Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: lts/*
- run: npm ci
- run: npm run lint
- run: npm test

36
.github/workflows/ci-rust.yml vendored Normal file
View File

@@ -0,0 +1,36 @@
on:
pull_request:
branches: [develop]
paths:
- src-tauri/**
- .github/workflows/**
name: CI (Rust)
defaults:
run:
working-directory: src-tauri
jobs:
test:
name: Check/Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev
- uses: dtolnay/rust-toolchain@stable
- uses: actions/cache@v3
continue-on-error: false
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
restore-keys: ${{ runner.os }}-cargo-
- run: cargo check --all
- run: cargo test --all

View File

@@ -32,6 +32,17 @@ jobs:
with:
# Those targets are only used on macos runners so it's in an `if` to slightly speed up windows and linux builds.
targets: ${{ matrix.platform == 'macos-latest' && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
- uses: actions/cache@v3
continue-on-error: false
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
restore-keys: ${{ runner.os }}-cargo-
- name: install dependencies (ubuntu only)
if: matrix.platform == 'ubuntu-22.04' # This must match the platform value defined above.
run: |

3
.gitignore vendored
View File

@@ -27,4 +27,5 @@ dist-ssr
*.sqlite
*.sqlite-*
.cargo
.cargo
plugins/**/build

Binary file not shown.

696
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
{
"name": "yaak-app",
"name": "yaak",
"private": true,
"version": "0.0.0",
"type": "module",
@@ -52,6 +52,7 @@
"codemirror": "^6.0.1",
"codemirror-json-schema": "^0.6.1",
"date-fns": "^3.3.1",
"fast-fuzzy": "^1.12.0",
"focus-trap-react": "^10.1.1",
"format-graphql": "^1.4.0",
"framer-motion": "^9.0.4",
@@ -64,6 +65,7 @@
"react-dnd-html5-backend": "^16.0.1",
"react-dom": "^18.2.0",
"react-helmet-async": "^1.3.0",
"react-pdf": "^9.0.0",
"react-router-dom": "^6.8.1",
"react-use": "^17.4.0",
"slugify": "^1.6.6",
@@ -103,6 +105,7 @@
"tailwindcss": "^3.2.7",
"typescript": "^5.4.5",
"vite": "^5.0.0",
"vite-plugin-static-copy": "^1.0.5",
"vite-plugin-svgr": "^4.2.0",
"vite-plugin-top-level-await": "^1.4.1",
"vitest": "^1.3.0"

View File

@@ -2,7 +2,7 @@ import { HttpRequest } from '../../../src-web/lib/models';
const NEWLINE = '\\\n ';
export function pluginHookExport(request: Partial<HttpRequest>) {
export function pluginHookExport(_: any, request: Partial<HttpRequest>) {
const xs = ['curl'];
// Add method and URL all on first line

View File

@@ -1,10 +1,12 @@
import { describe, expect, test } from 'vitest';
import { pluginHookExport } from '../src';
const ctx = {};
describe('exporter-curl', () => {
test('Exports GET with params', () => {
expect(
pluginHookExport({
pluginHookExport(ctx, {
url: 'https://yaak.app',
urlParameters: [
{ name: 'a', value: 'aaa' },
@@ -18,7 +20,7 @@ describe('exporter-curl', () => {
});
test('Exports POST with url form data', () => {
expect(
pluginHookExport({
pluginHookExport(ctx, {
url: 'https://yaak.app',
method: 'POST',
bodyType: 'application/x-www-form-urlencoded',
@@ -37,7 +39,7 @@ describe('exporter-curl', () => {
test('Exports PUT with multipart form', () => {
expect(
pluginHookExport({
pluginHookExport(ctx, {
url: 'https://yaak.app',
method: 'PUT',
bodyType: 'multipart/form-data',
@@ -62,7 +64,7 @@ describe('exporter-curl', () => {
test('Exports JSON body', () => {
expect(
pluginHookExport({
pluginHookExport(ctx, {
url: 'https://yaak.app',
method: 'POST',
bodyType: 'application/json',
@@ -82,7 +84,7 @@ describe('exporter-curl', () => {
test('Exports multi-line JSON body', () => {
expect(
pluginHookExport({
pluginHookExport(ctx, {
url: 'https://yaak.app',
method: 'POST',
bodyType: 'application/json',
@@ -102,7 +104,7 @@ describe('exporter-curl', () => {
test('Exports headers', () => {
expect(
pluginHookExport({
pluginHookExport(ctx, {
headers: [
{ name: 'a', value: 'aaa' },
{ name: 'b', value: 'bbb', enabled: true },
@@ -114,7 +116,7 @@ describe('exporter-curl', () => {
test('Basic auth', () => {
expect(
pluginHookExport({
pluginHookExport(ctx, {
url: 'https://yaak.app',
authenticationType: 'basic',
authentication: {
@@ -127,7 +129,7 @@ describe('exporter-curl', () => {
test('Broken basic auth', () => {
expect(
pluginHookExport({
pluginHookExport(ctx, {
url: 'https://yaak.app',
authenticationType: 'basic',
authentication: {},
@@ -137,7 +139,7 @@ describe('exporter-curl', () => {
test('Digest auth', () => {
expect(
pluginHookExport({
pluginHookExport(ctx, {
url: 'https://yaak.app',
authenticationType: 'digest',
authentication: {
@@ -150,7 +152,7 @@ describe('exporter-curl', () => {
test('Bearer auth', () => {
expect(
pluginHookExport({
pluginHookExport(ctx, {
url: 'https://yaak.app',
authenticationType: 'bearer',
authentication: {
@@ -162,7 +164,7 @@ describe('exporter-curl', () => {
test('Broken bearer auth', () => {
expect(
pluginHookExport({
pluginHookExport(ctx, {
url: 'https://yaak.app',
authenticationType: 'bearer',
authentication: {

View File

@@ -8,6 +8,8 @@ export default defineConfig({
fileName: 'index',
formats: ['es'],
},
emptyOutDir: true,
sourcemap: true,
outDir: resolve(__dirname, '../../src-tauri/plugins/exporter-curl'),
},
});

View File

@@ -1,6 +1,6 @@
import jp from 'jsonpath';
export function pluginHookResponseFilter(filter, text) {
export function pluginHookResponseFilter(ctx, filter, text) {
let parsed;
try {
parsed = JSON.parse(text);

View File

@@ -8,6 +8,8 @@ export default defineConfig({
fileName: 'index',
formats: ['es'],
},
emptyOutDir: true,
sourcemap: true,
outDir: resolve(__dirname, '../../src-tauri/plugins/filter-jsonpath'),
},
});

View File

@@ -1,7 +1,7 @@
import xpath from 'xpath';
import { DOMParser } from '@xmldom/xmldom';
export function pluginHookResponseFilter(filter, text) {
export function pluginHookResponseFilter(ctx, filter, text) {
const doc = new DOMParser().parseFromString(text, 'text/xml');
const filtered = `${xpath.select(filter, doc)}`;
return { filtered };

View File

@@ -8,6 +8,8 @@ export default defineConfig({
fileName: 'index',
formats: ['es'],
},
emptyOutDir: true,
sourcemap: true,
outDir: resolve(__dirname, '../../src-tauri/plugins/filter-xpath'),
},
});

View File

@@ -43,7 +43,7 @@ type Pair = string | boolean;
type PairsByName = Record<string, Pair[]>;
export function pluginHookImport(rawData: string) {
export function pluginHookImport(_: any, rawData: string) {
if (!rawData.match(/^\s*curl /)) {
return null;
}

View File

@@ -2,9 +2,11 @@ import { describe, expect, test } from 'vitest';
import { HttpRequest, Model, Workspace } from '../../../src-web/lib/models';
import { pluginHookImport } from '../src';
const ctx = {};
describe('importer-curl', () => {
test('Imports basic GET', () => {
expect(pluginHookImport('curl https://yaak.app')).toEqual({
expect(pluginHookImport(ctx, 'curl https://yaak.app')).toEqual({
resources: {
workspaces: [baseWorkspace()],
httpRequests: [
@@ -17,7 +19,7 @@ describe('importer-curl', () => {
});
test('Explicit URL', () => {
expect(pluginHookImport('curl --url https://yaak.app')).toEqual({
expect(pluginHookImport(ctx, 'curl --url https://yaak.app')).toEqual({
resources: {
workspaces: [baseWorkspace()],
httpRequests: [
@@ -30,7 +32,7 @@ describe('importer-curl', () => {
});
test('Missing URL', () => {
expect(pluginHookImport('curl -X POST')).toEqual({
expect(pluginHookImport(ctx, 'curl -X POST')).toEqual({
resources: {
workspaces: [baseWorkspace()],
httpRequests: [
@@ -43,7 +45,7 @@ describe('importer-curl', () => {
});
test('URL between', () => {
expect(pluginHookImport('curl -v https://yaak.app -X POST')).toEqual({
expect(pluginHookImport(ctx, 'curl -v https://yaak.app -X POST')).toEqual({
resources: {
workspaces: [baseWorkspace()],
httpRequests: [
@@ -57,7 +59,7 @@ describe('importer-curl', () => {
});
test('Random flags', () => {
expect(pluginHookImport('curl --random -Z -Y -S --foo https://yaak.app')).toEqual({
expect(pluginHookImport(ctx, 'curl --random -Z -Y -S --foo https://yaak.app')).toEqual({
resources: {
workspaces: [baseWorkspace()],
httpRequests: [
@@ -70,7 +72,7 @@ describe('importer-curl', () => {
});
test('Imports --request method', () => {
expect(pluginHookImport('curl --request POST https://yaak.app')).toEqual({
expect(pluginHookImport(ctx, 'curl --request POST https://yaak.app')).toEqual({
resources: {
workspaces: [baseWorkspace()],
httpRequests: [
@@ -84,7 +86,7 @@ describe('importer-curl', () => {
});
test('Imports -XPOST method', () => {
expect(pluginHookImport('curl -XPOST --request POST https://yaak.app')).toEqual({
expect(pluginHookImport(ctx, 'curl -XPOST --request POST https://yaak.app')).toEqual({
resources: {
workspaces: [baseWorkspace()],
httpRequests: [
@@ -99,7 +101,10 @@ describe('importer-curl', () => {
test('Imports multiple requests', () => {
expect(
pluginHookImport('curl \\\n https://yaak.app\necho "foo"\ncurl example.com;curl foo.com'),
pluginHookImport(
ctx,
'curl \\\n https://yaak.app\necho "foo"\ncurl example.com;curl foo.com',
),
).toEqual({
resources: {
workspaces: [baseWorkspace()],
@@ -114,7 +119,7 @@ describe('importer-curl', () => {
test('Imports form data', () => {
expect(
pluginHookImport('curl -X POST -F "a=aaa" -F b=bbb" -F f=@filepath https://yaak.app'),
pluginHookImport(ctx, 'curl -X POST -F "a=aaa" -F b=bbb" -F f=@filepath https://yaak.app'),
).toEqual({
resources: {
workspaces: [baseWorkspace()],
@@ -144,7 +149,7 @@ describe('importer-curl', () => {
});
test('Imports data params as form url-encoded', () => {
expect(pluginHookImport('curl -d a -d b -d c=ccc https://yaak.app')).toEqual({
expect(pluginHookImport(ctx, 'curl -d a -d b -d c=ccc https://yaak.app')).toEqual({
resources: {
workspaces: [baseWorkspace()],
httpRequests: [
@@ -174,7 +179,7 @@ describe('importer-curl', () => {
test('Imports data params as text', () => {
expect(
pluginHookImport('curl -H Content-Type:text/plain -d a -d b -d c=ccc https://yaak.app'),
pluginHookImport(ctx, 'curl -H Content-Type:text/plain -d a -d b -d c=ccc https://yaak.app'),
).toEqual({
resources: {
workspaces: [baseWorkspace()],
@@ -194,6 +199,7 @@ describe('importer-curl', () => {
test('Imports multi-line JSON', () => {
expect(
pluginHookImport(
ctx,
`curl -H Content-Type:application/json -d $'{\n "foo":"bar"\n}' https://yaak.app`,
),
).toEqual({
@@ -214,7 +220,7 @@ describe('importer-curl', () => {
test('Imports multiple headers', () => {
expect(
pluginHookImport('curl -H Foo:bar --header Name -H AAA:bbb -H :ccc https://yaak.app'),
pluginHookImport(ctx, 'curl -H Foo:bar --header Name -H AAA:bbb -H :ccc https://yaak.app'),
).toEqual({
resources: {
workspaces: [baseWorkspace()],
@@ -234,7 +240,7 @@ describe('importer-curl', () => {
});
test('Imports basic auth', () => {
expect(pluginHookImport('curl --user user:pass https://yaak.app')).toEqual({
expect(pluginHookImport(ctx, 'curl --user user:pass https://yaak.app')).toEqual({
resources: {
workspaces: [baseWorkspace()],
httpRequests: [
@@ -252,7 +258,7 @@ describe('importer-curl', () => {
});
test('Imports digest auth', () => {
expect(pluginHookImport('curl --digest --user user:pass https://yaak.app')).toEqual({
expect(pluginHookImport(ctx, 'curl --digest --user user:pass https://yaak.app')).toEqual({
resources: {
workspaces: [baseWorkspace()],
httpRequests: [
@@ -270,7 +276,7 @@ describe('importer-curl', () => {
});
test('Imports cookie as header', () => {
expect(pluginHookImport('curl --cookie "foo=bar" https://yaak.app')).toEqual({
expect(pluginHookImport(ctx, 'curl --cookie "foo=bar" https://yaak.app')).toEqual({
resources: {
workspaces: [baseWorkspace()],
httpRequests: [
@@ -284,7 +290,7 @@ describe('importer-curl', () => {
});
test('Imports query params from the URL', () => {
expect(pluginHookImport('curl "https://yaak.app?foo=bar&baz=a%20a"')).toEqual({
expect(pluginHookImport(ctx, 'curl "https://yaak.app?foo=bar&baz=a%20a"')).toEqual({
resources: {
workspaces: [baseWorkspace()],
httpRequests: [

View File

@@ -8,6 +8,8 @@ export default defineConfig({
fileName: 'index',
formats: ['es'],
},
emptyOutDir: true,
sourcemap: true,
outDir: resolve(__dirname, '../../src-tauri/plugins/importer-curl'),
},
});

View File

@@ -5,7 +5,7 @@ import {
HttpRequest,
Workspace,
} from '../../../src-web/lib/models';
import { parse as parseYaml } from 'yaml';
import '../../../src-web/plugin/runtime.d.ts';
type AtLeast<T, K extends keyof T> = Partial<T> & Pick<T, K>;
@@ -17,7 +17,7 @@ export interface ExportResources {
folders: AtLeast<Folder, 'name' | 'id' | 'model' | 'workspaceId'>[];
}
export function pluginHookImport(contents: string) {
export function pluginHookImport(ctx: YaakContext, contents: string) {
let parsed: any;
try {
@@ -25,8 +25,10 @@ export function pluginHookImport(contents: string) {
} catch (e) {}
try {
parsed = parseYaml(contents);
} catch (e) {}
parsed = parsed ?? YAML.parse(contents);
} catch (e) {
console.log('FAILED', e);
}
if (!isJSObject(parsed)) return;
if (!Array.isArray(parsed.resources)) return;

View File

@@ -8,6 +8,8 @@ export default defineConfig({
fileName: 'index',
formats: ['es'],
},
emptyOutDir: true,
sourcemap: true,
outDir: resolve(__dirname, '../../src-tauri/plugins/importer-insomnia'),
},
});

View File

@@ -13,7 +13,11 @@ interface ExportResources {
folders: AtLeast<Folder, 'name' | 'id' | 'model' | 'workspaceId'>[];
}
export function pluginHookImport(contents: string): { resources: ExportResources } | undefined {
export function pluginHookImport(
ctx: any,
contents: string,
): { resources: ExportResources } | undefined {
console.log('CTX', ctx);
const root = parseJSONToRecord(contents);
if (root == null) return;

View File

@@ -23,7 +23,7 @@ describe('importer-postman', () => {
for (const fixture of fixtures) {
test('Imports ' + fixture, () => {
const contents = fs.readFileSync(path.join(p, fixture), 'utf-8');
const imported = pluginHookImport(contents);
const imported = pluginHookImport({}, contents);
const folder0 = newId('folder');
const folder1 = newId('folder');
expect(imported).toEqual({

View File

@@ -8,6 +8,8 @@ export default defineConfig({
fileName: 'index',
formats: ['es'],
},
emptyOutDir: true,
sourcemap: true,
outDir: resolve(__dirname, '../../src-tauri/plugins/importer-postman'),
},
});

View File

@@ -1,4 +1,4 @@
export function pluginHookImport(contents: string) {
export function pluginHookImport(ctx: any, contents: string) {
let parsed;
try {
parsed = JSON.parse(contents);
@@ -18,7 +18,7 @@ export function pluginHookImport(contents: string) {
// Migrate v1 to v2 -- changes requests to httpRequests
if ('requests' in parsed.resources) {
parsed.resources.httpRequests = parsed.resources.requests;
delete parsed.resources.requests;
delete parsed.resources['requests'];
}
return { resources: parsed.resources }; // Should already be in the correct format

View File

@@ -1,15 +1,18 @@
import { describe, expect, test } from 'vitest';
import { pluginHookImport } from '../src';
const ctx = {};
describe('importer-yaak', () => {
test('Skips invalid imports', () => {
expect(pluginHookImport('not JSON')).toBeUndefined();
expect(pluginHookImport('[]')).toBeUndefined();
expect(pluginHookImport(JSON.stringify({ resources: {} }))).toBeUndefined();
expect(pluginHookImport(ctx, 'not JSON')).toBeUndefined();
expect(pluginHookImport(ctx, '[]')).toBeUndefined();
expect(pluginHookImport(ctx, JSON.stringify({ resources: {} }))).toBeUndefined();
});
test('converts schema 1 to 2', () => {
const imported = pluginHookImport(
ctx,
JSON.stringify({
yaakSchema: 1,
resources: {

View File

@@ -8,6 +8,8 @@ export default defineConfig({
fileName: 'index',
formats: ['es'],
},
emptyOutDir: true,
sourcemap: true,
outDir: resolve(__dirname, '../../src-tauri/plugins/importer-yaak'),
},
});

View File

@@ -1,6 +1,6 @@
{
"db_name": "SQLite",
"query": "\n SELECT\n id, model, created_at, updated_at, theme, appearance,\n theme_dark, theme_light, update_channel,\n interface_font_size, interface_scale, editor_font_size, editor_soft_wrap\n FROM settings\n WHERE id = 'default'\n ",
"query": "\n SELECT\n id, model, created_at, updated_at, theme, appearance,\n theme_dark, theme_light, update_channel,\n interface_font_size, interface_scale, editor_font_size, editor_soft_wrap, \n open_workspace_new_window\n FROM settings\n WHERE id = 'default'\n ",
"describe": {
"columns": [
{
@@ -67,6 +67,11 @@
"name": "editor_soft_wrap",
"ordinal": 12,
"type_info": "Bool"
},
{
"name": "open_workspace_new_window",
"ordinal": 13,
"type_info": "Bool"
}
],
"parameters": {
@@ -85,8 +90,9 @@
false,
false,
false,
false
false,
true
]
},
"hash": "ca3485d87b060cd77c4114d2af544adf18f6f15341d9d5db40865e92a80da4e2"
"hash": "05dca7fe15ab1bf03952e94498ef3130e16f752da72782783696eb2cca4736d5"
}

View File

@@ -1,12 +1,12 @@
{
"db_name": "SQLite",
"query": "\n UPDATE settings SET (\n theme, appearance, theme_dark, theme_light, update_channel,\n interface_font_size, interface_scale, editor_font_size, editor_soft_wrap\n ) = (?, ?, ?, ?, ?, ?, ?, ?, ?) WHERE id = 'default';\n ",
"query": "\n UPDATE settings SET (\n theme, appearance, theme_dark, theme_light, update_channel,\n interface_font_size, interface_scale, editor_font_size, editor_soft_wrap,\n open_workspace_new_window\n ) = (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) WHERE id = 'default';\n ",
"describe": {
"columns": [],
"parameters": {
"Right": 9
"Right": 10
},
"nullable": []
},
"hash": "efd8ba41ea909b18dd520c57c1d464c5ae057b720cbbedcaec1513d43535632c"
"hash": "6b5edf45a6799cd7f87c23a3c7f818ad110d58c601f694a619d9345ae9e8e11d"
}

1695
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
workspace = { members = ["grpc"] }
workspace = { members = ["grpc", "templates"] }
[package]
name = "yaak-app"
@@ -24,32 +24,36 @@ cocoa = "0.25.0"
openssl-sys = { version = "0.9", features = ["vendored"] } # For Ubuntu installation to work
[dependencies]
grpc = { path = "./grpc" }
templates = { path = "./templates" }
anyhow = "1.0.86"
base64 = "0.22.0"
boa_engine = { version = "0.18.0", features = ["annex-b"] }
boa_runtime = { version = "0.18.0" }
chrono = { version = "0.4.31", features = ["serde"] }
http = "0.2.10"
datetime = "0.5.2"
deno_ast = { version = "0.39.0", features = ["transpiling"] }
deno_console = "0.155.0"
deno_core = { version = "0.284.0" }
hex_color = "3.0.0"
http = "1"
log = "0.4.21"
rand = "0.8.5"
reqwest = { version = "0.11.23", features = ["multipart", "cookies", "gzip", "brotli", "deflate", "json"] }
regex = "1.10.2"
reqwest = { version = "0.12.4", features = ["multipart", "cookies", "gzip", "brotli", "deflate", "json", "native-tls-alpn"] }
reqwest_cookie_store = "0.8.0"
serde = { version = "1.0.198", features = ["derive"] }
serde_json = { version = "1.0.116", features = ["raw_value"] }
serde_yaml = "0.9.34"
sqlx = { version = "0.7.4", features = ["sqlite", "runtime-tokio-rustls", "json", "chrono", "time"] }
tauri = { version = "2.0.0-beta", features = ["config-toml", "devtools", "protocol-asset"] }
tauri = { version = "2.0.0-beta", features = ["devtools", "protocol-asset"] }
tauri-plugin-clipboard-manager = "2.1.0-beta"
tauri-plugin-dialog = "2.0.0-beta"
tauri-plugin-fs = "2.0.0-beta"
tauri-plugin-log = { version = "2.0.0-beta", features = ["colored"] }
tauri-plugin-shell = "2.0.0-beta"
tauri-plugin-os = "2.0.0-beta"
tauri-plugin-shell = "2.0.0-beta"
tauri-plugin-updater = "2.0.0-beta"
tauri-plugin-window-state = "2.0.0-beta"
tauri-plugin-fs = "2.0.0-beta"
tauri-plugin-deep-link = "2.0.0-beta"
tokio = { version = "1.36.0", features = ["sync"] }
uuid = "1.7.0"
log = "0.4.21"
datetime = "0.5.2"
reqwest_cookie_store = "0.6.0"
grpc = { path = "./grpc" }
tokio-stream = "0.1.15"
regex = "1.10.2"
hex_color = "3.0.0"
uuid = "1.7.0"
thiserror = "1.0.61"

File diff suppressed because one or more lines are too long

View File

@@ -2512,69 +2512,6 @@
"clipboard-manager:deny-write-text"
]
},
{
"description": "deep-link:default -> Allows reading the opened deep link via the get_current command",
"type": "string",
"enum": [
"deep-link:default"
]
},
{
"description": "deep-link:allow-get-current -> Enables the get_current command without any pre-configured scope.",
"type": "string",
"enum": [
"deep-link:allow-get-current"
]
},
{
"description": "deep-link:allow-is-registered -> Enables the is_registered command without any pre-configured scope.",
"type": "string",
"enum": [
"deep-link:allow-is-registered"
]
},
{
"description": "deep-link:allow-register -> Enables the register command without any pre-configured scope.",
"type": "string",
"enum": [
"deep-link:allow-register"
]
},
{
"description": "deep-link:allow-unregister -> Enables the unregister command without any pre-configured scope.",
"type": "string",
"enum": [
"deep-link:allow-unregister"
]
},
{
"description": "deep-link:deny-get-current -> Denies the get_current command without any pre-configured scope.",
"type": "string",
"enum": [
"deep-link:deny-get-current"
]
},
{
"description": "deep-link:deny-is-registered -> Denies the is_registered command without any pre-configured scope.",
"type": "string",
"enum": [
"deep-link:deny-is-registered"
]
},
{
"description": "deep-link:deny-register -> Denies the register command without any pre-configured scope.",
"type": "string",
"enum": [
"deep-link:deny-register"
]
},
{
"description": "deep-link:deny-unregister -> Denies the unregister command without any pre-configured scope.",
"type": "string",
"enum": [
"deep-link:deny-unregister"
]
},
{
"type": "string",
"enum": [

View File

@@ -2512,69 +2512,6 @@
"clipboard-manager:deny-write-text"
]
},
{
"description": "deep-link:default -> Allows reading the opened deep link via the get_current command",
"type": "string",
"enum": [
"deep-link:default"
]
},
{
"description": "deep-link:allow-get-current -> Enables the get_current command without any pre-configured scope.",
"type": "string",
"enum": [
"deep-link:allow-get-current"
]
},
{
"description": "deep-link:allow-is-registered -> Enables the is_registered command without any pre-configured scope.",
"type": "string",
"enum": [
"deep-link:allow-is-registered"
]
},
{
"description": "deep-link:allow-register -> Enables the register command without any pre-configured scope.",
"type": "string",
"enum": [
"deep-link:allow-register"
]
},
{
"description": "deep-link:allow-unregister -> Enables the unregister command without any pre-configured scope.",
"type": "string",
"enum": [
"deep-link:allow-unregister"
]
},
{
"description": "deep-link:deny-get-current -> Denies the get_current command without any pre-configured scope.",
"type": "string",
"enum": [
"deep-link:deny-get-current"
]
},
{
"description": "deep-link:deny-is-registered -> Denies the is_registered command without any pre-configured scope.",
"type": "string",
"enum": [
"deep-link:deny-is-registered"
]
},
{
"description": "deep-link:deny-register -> Denies the register command without any pre-configured scope.",
"type": "string",
"enum": [
"deep-link:deny-register"
]
},
{
"description": "deep-link:deny-unregister -> Denies the unregister command without any pre-configured scope.",
"type": "string",
"enum": [
"deep-link:deny-unregister"
]
},
{
"type": "string",
"enum": [

View File

@@ -185,20 +185,22 @@ impl GrpcHandle {
pub async fn services_from_files(
&mut self,
id: &str,
uri: &Uri,
uri: &str,
paths: Vec<PathBuf>,
) -> Result<Vec<ServiceDefinition>, String> {
let pool = fill_pool_from_files(&self.app_handle, paths).await?;
self.pools.insert(self.get_pool_key(id, uri), pool.clone());
let uri = Uri::from_str(uri).map_err(|e| e.to_string())?;
self.pools.insert(self.get_pool_key(id, &uri), pool.clone());
Ok(self.services_from_pool(&pool))
}
pub async fn services_from_reflection(
&mut self,
id: &str,
uri: &Uri,
uri: &str,
) -> Result<Vec<ServiceDefinition>, String> {
let pool = fill_pool(uri).await?;
self.pools.insert(self.get_pool_key(id, uri), pool.clone());
let uri = Uri::from_str(uri).map_err(|e| e.to_string())?;
let pool = fill_pool(&uri).await?;
self.pools.insert(self.get_pool_key(id, &uri), pool.clone());
Ok(self.services_from_pool(&pool))
}
@@ -234,9 +236,10 @@ impl GrpcHandle {
pub async fn connect(
&mut self,
id: &str,
uri: Uri,
uri: &str,
proto_files: Vec<PathBuf>,
) -> Result<GrpcConnection, String> {
let uri = Uri::from_str(uri).map_err(|e| e.to_string())?;
let pool = match self.pools.get(id) {
Some(p) => p.clone(),
None => match proto_files.len() {

View File

@@ -1,10 +1,14 @@
<?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>
<!-- Re-enable for sandboxing. Currently disabled because auto-updater doesn't work with sandboxing.-->
<!-- <key>com.apple.security.app-sandbox</key> <true/>-->
<!-- <key>com.apple.security.files.user-selected.read-write</key> <true/>-->
<!-- <key>com.apple.security.network.client</key> <true/>-->
</dict>
<dict>
<!-- Enable for v8 execution -->
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<!-- Re-enable for sandboxing. Currently disabled because auto-updater doesn't work with sandboxing.-->
<!-- <key>com.apple.security.app-sandbox</key> <true/>-->
<!-- <key>com.apple.security.files.user-selected.read-write</key> <true/>-->
<!-- <key>com.apple.security.network.client</key> <true/>-->
</dict>
</plist>

View File

@@ -0,0 +1 @@
ALTER TABLE settings ADD COLUMN open_workspace_new_window BOOLEAN NULL DEFAULT NULL;

View File

@@ -1,36 +1,37 @@
const o = `\\
`;
function d(n) {
var h, f, r, u, l, s;
const t = ["curl"];
n.method && t.push("-X", n.method), n.url && t.push(i(n.url)), t.push(o);
for (const a of (n.urlParameters ?? []).filter(p))
t.push("--url-query", i(`${a.name}=${a.value}`)), t.push(o);
for (const a of (n.headers ?? []).filter(p))
t.push("--header", i(`${a.name}: ${a.value}`)), t.push(o);
if (Array.isArray((h = n.body) == null ? void 0 : h.form)) {
const a = n.bodyType === "multipart/form-data" ? "--form" : "--data";
for (const e of (((f = n.body) == null ? void 0 : f.form) ?? []).filter(p)) {
function y(p, t) {
var f, r, u, l, s, c;
const n = ["curl"];
t.method && n.push("-X", t.method), t.url && n.push(i(t.url)), n.push(o);
for (const a of (t.urlParameters ?? []).filter(h))
n.push("--url-query", i(`${a.name}=${a.value}`)), n.push(o);
for (const a of (t.headers ?? []).filter(h))
n.push("--header", i(`${a.name}: ${a.value}`)), n.push(o);
if (Array.isArray((f = t.body) == null ? void 0 : f.form)) {
const a = t.bodyType === "multipart/form-data" ? "--form" : "--data";
for (const e of (((r = t.body) == null ? void 0 : r.form) ?? []).filter(h)) {
if (e.file) {
let c = `${e.name}=@${e.file}`;
c += e.contentType ? `;type=${e.contentType}` : "", t.push(a, c);
let d = `${e.name}=@${e.file}`;
d += e.contentType ? `;type=${e.contentType}` : "", n.push(a, d);
} else
t.push(a, i(`${e.name}=${e.value}`));
t.push(o);
n.push(a, i(`${e.name}=${e.value}`));
n.push(o);
}
} else
typeof ((r = n.body) == null ? void 0 : r.text) == "string" && (t.push("--data-raw", `$${i(n.body.text)}`), t.push(o));
return (n.authenticationType === "basic" || n.authenticationType === "digest") && (n.authenticationType === "digest" && t.push("--digest"), t.push(
typeof ((u = t.body) == null ? void 0 : u.text) == "string" && (n.push("--data-raw", `$${i(t.body.text)}`), n.push(o));
return (t.authenticationType === "basic" || t.authenticationType === "digest") && (t.authenticationType === "digest" && n.push("--digest"), n.push(
"--user",
i(`${((u = n.authentication) == null ? void 0 : u.username) ?? ""}:${((l = n.authentication) == null ? void 0 : l.password) ?? ""}`)
), t.push(o)), n.authenticationType === "bearer" && (t.push("--header", i(`Authorization: Bearer ${((s = n.authentication) == null ? void 0 : s.token) ?? ""}`)), t.push(o)), t[t.length - 1] === o && t.splice(t.length - 1, 1), t.join(" ");
i(`${((l = t.authentication) == null ? void 0 : l.username) ?? ""}:${((s = t.authentication) == null ? void 0 : s.password) ?? ""}`)
), n.push(o)), t.authenticationType === "bearer" && (n.push("--header", i(`Authorization: Bearer ${((c = t.authentication) == null ? void 0 : c.token) ?? ""}`)), n.push(o)), n[n.length - 1] === o && n.splice(n.length - 1, 1), n.join(" ");
}
function i(n) {
return `'${n.replace(/'/g, "\\'")}'`;
function i(p) {
return `'${p.replace(/'/g, "\\'")}'`;
}
function p(n) {
return n.enabled !== !1 && !!n.name;
function h(p) {
return p.enabled !== !1 && !!p.name;
}
export {
d as pluginHookExport
y as pluginHookExport
};
//# sourceMappingURL=index.mjs.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"index.mjs","sources":["../../../plugins/exporter-curl/src/index.ts"],"sourcesContent":["import { HttpRequest } from '../../../src-web/lib/models';\n\nconst NEWLINE = '\\\\\\n ';\n\nexport function pluginHookExport(_: any, request: Partial<HttpRequest>) {\n const xs = ['curl'];\n\n // Add method and URL all on first line\n if (request.method) xs.push('-X', request.method);\n if (request.url) xs.push(quote(request.url));\n\n xs.push(NEWLINE);\n\n // Add URL params\n for (const p of (request.urlParameters ?? []).filter(onlyEnabled)) {\n xs.push('--url-query', quote(`${p.name}=${p.value}`));\n xs.push(NEWLINE);\n }\n\n // Add headers\n for (const h of (request.headers ?? []).filter(onlyEnabled)) {\n xs.push('--header', quote(`${h.name}: ${h.value}`));\n xs.push(NEWLINE);\n }\n\n // Add form params\n if (Array.isArray(request.body?.form)) {\n const flag = request.bodyType === 'multipart/form-data' ? '--form' : '--data';\n for (const p of (request.body?.form ?? []).filter(onlyEnabled)) {\n if (p.file) {\n let v = `${p.name}=@${p.file}`;\n v += p.contentType ? `;type=${p.contentType}` : '';\n xs.push(flag, v);\n } else {\n xs.push(flag, quote(`${p.name}=${p.value}`));\n }\n xs.push(NEWLINE);\n }\n } else if (typeof request.body?.text === 'string') {\n // --data-raw $'...' to do special ANSI C quoting\n xs.push('--data-raw', `$${quote(request.body.text)}`);\n xs.push(NEWLINE);\n }\n\n // Add basic/digest authentication\n if (request.authenticationType === 'basic' || request.authenticationType === 'digest') {\n if (request.authenticationType === 'digest') xs.push('--digest');\n xs.push(\n '--user',\n quote(`${request.authentication?.username ?? ''}:${request.authentication?.password ?? ''}`),\n );\n xs.push(NEWLINE);\n }\n\n // Add bearer authentication\n if (request.authenticationType === 'bearer') {\n xs.push('--header', quote(`Authorization: Bearer ${request.authentication?.token ?? ''}`));\n xs.push(NEWLINE);\n }\n\n // Remove trailing newline\n if (xs[xs.length - 1] === NEWLINE) {\n xs.splice(xs.length - 1, 1);\n }\n\n return xs.join(' ');\n}\n\nfunction quote(arg: string): string {\n const escaped = arg.replace(/'/g, \"\\\\'\");\n return `'${escaped}'`;\n}\n\nfunction onlyEnabled(v: { name?: string; enabled?: boolean }): boolean {\n return v.enabled !== false && !!v.name;\n}\n"],"names":["NEWLINE","pluginHookExport","_","request","_a","_b","_c","_d","_e","_f","xs","quote","p","onlyEnabled","h","flag","v","arg"],"mappings":"AAEA,MAAMA,IAAU;AAAA;AAEA,SAAAC,EAAiBC,GAAQC,GAA+B;AAFxE,MAAAC,GAAAC,GAAAC,GAAAC,GAAAC,GAAAC;AAGQ,QAAAC,IAAK,CAAC,MAAM;AAGlB,EAAIP,EAAQ,UAAWO,EAAA,KAAK,MAAMP,EAAQ,MAAM,GAC5CA,EAAQ,OAAKO,EAAG,KAAKC,EAAMR,EAAQ,GAAG,CAAC,GAE3CO,EAAG,KAAKV,CAAO;AAGf,aAAWY,MAAMT,EAAQ,iBAAiB,IAAI,OAAOU,CAAW;AAC3D,IAAAH,EAAA,KAAK,eAAeC,EAAM,GAAGC,EAAE,IAAI,IAAIA,EAAE,KAAK,EAAE,CAAC,GACpDF,EAAG,KAAKV,CAAO;AAIjB,aAAWc,MAAMX,EAAQ,WAAW,IAAI,OAAOU,CAAW;AACrD,IAAAH,EAAA,KAAK,YAAYC,EAAM,GAAGG,EAAE,IAAI,KAAKA,EAAE,KAAK,EAAE,CAAC,GAClDJ,EAAG,KAAKV,CAAO;AAIjB,MAAI,MAAM,SAAQI,IAAAD,EAAQ,SAAR,gBAAAC,EAAc,IAAI,GAAG;AACrC,UAAMW,IAAOZ,EAAQ,aAAa,wBAAwB,WAAW;AAC1D,eAAAS,QAAMP,IAAAF,EAAQ,SAAR,gBAAAE,EAAc,SAAQ,CAAI,GAAA,OAAOQ,CAAW,GAAG;AAC9D,UAAID,EAAE,MAAM;AACV,YAAII,IAAI,GAAGJ,EAAE,IAAI,KAAKA,EAAE,IAAI;AAC5B,QAAAI,KAAKJ,EAAE,cAAc,SAASA,EAAE,WAAW,KAAK,IAC7CF,EAAA,KAAKK,GAAMC,CAAC;AAAA,MAAA;AAEZ,QAAAN,EAAA,KAAKK,GAAMJ,EAAM,GAAGC,EAAE,IAAI,IAAIA,EAAE,KAAK,EAAE,CAAC;AAE7C,MAAAF,EAAG,KAAKV,CAAO;AAAA,IACjB;AAAA,EACS;AAAA,IAAA,SAAOM,IAAAH,EAAQ,SAAR,gBAAAG,EAAc,SAAS,aAEpCI,EAAA,KAAK,cAAc,IAAIC,EAAMR,EAAQ,KAAK,IAAI,CAAC,EAAE,GACpDO,EAAG,KAAKV,CAAO;AAIjB,UAAIG,EAAQ,uBAAuB,WAAWA,EAAQ,uBAAuB,cACvEA,EAAQ,uBAAuB,YAAUO,EAAG,KAAK,UAAU,GAC5DA,EAAA;AAAA,IACD;AAAA,IACAC,EAAM,KAAGJ,IAAAJ,EAAQ,mBAAR,gBAAAI,EAAwB,aAAY,EAAE,MAAIC,IAAAL,EAAQ,mBAAR,gBAAAK,EAAwB,aAAY,EAAE,EAAE;AAAA,EAAA,GAE7FE,EAAG,KAAKV,CAAO,IAIbG,EAAQ,uBAAuB,aAC9BO,EAAA,KAAK,YAAYC,EAAM,2BAAyBF,IAAAN,EAAQ,mBAAR,gBAAAM,EAAwB,UAAS,EAAE,EAAE,CAAC,GACzFC,EAAG,KAAKV,CAAO,IAIbU,EAAGA,EAAG,SAAS,CAAC,MAAMV,KACxBU,EAAG,OAAOA,EAAG,SAAS,GAAG,CAAC,GAGrBA,EAAG,KAAK,GAAG;AACpB;AAEA,SAASC,EAAMM,GAAqB;AAElC,SAAO,IADSA,EAAI,QAAQ,MAAM,KAAK,CACrB;AACpB;AAEA,SAASJ,EAAYG,GAAkD;AACrE,SAAOA,EAAE,YAAY,MAAS,CAAC,CAACA,EAAE;AACpC;"}

View File

@@ -1,15 +1,15 @@
var Xe = typeof globalThis < "u" ? globalThis : typeof window < "u" ? window : typeof global < "u" ? global : typeof self < "u" ? self : {};
function Nt(le) {
return le && le.__esModule && Object.prototype.hasOwnProperty.call(le, "default") ? le.default : le;
function Nt(ce) {
return ce && ce.__esModule && Object.prototype.hasOwnProperty.call(ce, "default") ? ce.default : ce;
}
function Be(le) {
throw new Error('Could not dynamically require "' + le + '". Please configure the dynamicRequireTargets or/and ignoreDynamicRequires option of @rollup/plugin-commonjs appropriately for this require call to work.');
function Be(ce) {
throw new Error('Could not dynamically require "' + ce + '". Please configure the dynamicRequireTargets or/and ignoreDynamicRequires option of @rollup/plugin-commonjs appropriately for this require call to work.');
}
var Ve = { exports: {} };
/*! jsonpath 1.1.1 */
(function(le, Ne) {
(function(ce, Ne) {
(function(T) {
le.exports = T();
ce.exports = T();
})(function() {
return function T(L, B, v) {
function y(E, d) {
@@ -1131,7 +1131,7 @@ var Ve = { exports: {} };
function Ze() {
var e = [], t;
for (t = i, O("["); !_("]"); )
_(",") ? (N(), e.push(null)) : (e.push(ce()), _("]") || O(","));
_(",") ? (N(), e.push(null)) : (e.push(le()), _("]") || O(","));
return N(), n.markEnd(n.createArrayExpression(e), t);
}
function _e(e, t) {
@@ -1145,11 +1145,11 @@ var Ve = { exports: {} };
function et() {
var e, t, u, l, C, x;
if (e = i, x = i, e.type === y.Identifier)
return u = De(), e.value === "get" && !_(":") ? (t = De(), O("("), O(")"), l = _e([]), n.markEnd(n.createProperty("get", t, l), x)) : e.value === "set" && !_(":") ? (t = De(), O("("), e = i, e.type !== y.Identifier ? (O(")"), G(e, a.UnexpectedToken, e.value), l = _e([])) : (C = [ye()], O(")"), l = _e(C, e)), n.markEnd(n.createProperty("set", t, l), x)) : (O(":"), l = ce(), n.markEnd(n.createProperty("init", u, l), x));
return u = De(), e.value === "get" && !_(":") ? (t = De(), O("("), O(")"), l = _e([]), n.markEnd(n.createProperty("get", t, l), x)) : e.value === "set" && !_(":") ? (t = De(), O("("), e = i, e.type !== y.Identifier ? (O(")"), G(e, a.UnexpectedToken, e.value), l = _e([])) : (C = [ye()], O(")"), l = _e(C, e)), n.markEnd(n.createProperty("set", t, l), x)) : (O(":"), l = le(), n.markEnd(n.createProperty("init", u, l), x));
if (e.type === y.EOF || e.type === y.Punctuator)
pe(e);
else
return t = De(), O(":"), l = ce(), n.markEnd(n.createProperty("init", t, l), x);
return t = De(), O(":"), l = le(), n.markEnd(n.createProperty("init", t, l), x);
}
function tt() {
var e = [], t, u, l, C, x = {}, j = String, W;
@@ -1184,7 +1184,7 @@ var Ve = { exports: {} };
function je() {
var e = [];
if (O("("), !_(")"))
for (; r < h && (e.push(ce()), !_(")")); )
for (; r < h && (e.push(le()), !_(")")); )
O(",");
return O(")"), e;
}
@@ -1300,17 +1300,17 @@ var Ve = { exports: {} };
}
function st() {
var e, t, u, l, C;
return C = i, e = at(), _("?") && (N(), t = p.allowIn, p.allowIn = !0, u = ce(), p.allowIn = t, O(":"), l = ce(), e = n.createConditionalExpression(e, u, l), n.markEnd(e, C)), e;
return C = i, e = at(), _("?") && (N(), t = p.allowIn, p.allowIn = !0, u = le(), p.allowIn = t, O(":"), l = le(), e = n.createConditionalExpression(e, u, l), n.markEnd(e, C)), e;
}
function ce() {
function le() {
var e, t, u, l, C;
return e = i, C = i, l = t = st(), Ye() && (be(t) || G({}, a.InvalidLHSInAssignment), c && t.type === E.Identifier && J(t.name) && G(e, a.StrictLHSAssignment), e = N(), u = ce(), l = n.markEnd(n.createAssignmentExpression(e.value, t, u), C)), l;
return e = i, C = i, l = t = st(), Ye() && (be(t) || G({}, a.InvalidLHSInAssignment), c && t.type === E.Identifier && J(t.name) && G(e, a.StrictLHSAssignment), e = N(), u = le(), l = n.markEnd(n.createAssignmentExpression(e.value, t, u), C)), l;
}
function te() {
var e, t = i;
if (e = ce(), _(",")) {
if (e = le(), _(",")) {
for (e = n.createSequenceExpression([e]); r < h && _(","); )
N(), e.expressions.push(ce());
N(), e.expressions.push(le());
n.markEnd(e, t);
}
return e;
@@ -1330,7 +1330,7 @@ var Ve = { exports: {} };
}
function lt(e) {
var t = null, u, l;
return l = i, u = ye(), c && J(u.name) && G({}, a.StrictVarName), e === "const" ? (O("="), t = ce()) : _("=") && (N(), t = ce()), n.markEnd(n.createVariableDeclarator(u, t), l);
return l = i, u = ye(), c && J(u.name) && G({}, a.StrictVarName), e === "const" ? (O("="), t = le()) : _("=") && (N(), t = le()), n.markEnd(n.createVariableDeclarator(u, t), l);
}
function Te(e) {
var t = [];
@@ -3176,16 +3176,17 @@ Expecting ` + se.join(", ") + ", got '" + (this.terminals_[w] || w) + "'" : ie =
})(Ve);
var Pt = Ve.exports;
const Lt = /* @__PURE__ */ Nt(Pt);
function Rt(le, Ne) {
let T;
function Rt(ce, Ne, T) {
let L;
try {
T = JSON.parse(Ne);
L = JSON.parse(T);
} catch {
return;
}
const L = Lt.query(T, le);
return { filtered: JSON.stringify(L, null, 2) };
const B = Lt.query(L, Ne);
return { filtered: JSON.stringify(B, null, 2) };
}
export {
Rt as pluginHookResponseFilter
};
//# sourceMappingURL=index.mjs.map

File diff suppressed because one or more lines are too long

View File

@@ -6116,10 +6116,11 @@ Tr.__DOMHandler = Dr;
Tr.normalizeLineEndings = Rt;
Tr.DOMParser = _t;
var Cu = Tr.DOMParser;
function yu(t, u) {
const n = new Cu().parseFromString(u, "text/xml");
return { filtered: `${Qt.select(t, n)}` };
function yu(t, u, n) {
const s = new Cu().parseFromString(n, "text/xml");
return { filtered: `${Qt.select(u, s)}` };
}
export {
yu as pluginHookResponseFilter
};
//# sourceMappingURL=index.mjs.map

File diff suppressed because one or more lines are too long

View File

@@ -9,86 +9,86 @@ var j = "(?:" + [
">\\&",
"<\\&",
"[&;()|<>]"
].join("|") + ")", D = new RegExp("^" + j + "$"), q = "|&;()<> \\t", M = '"((\\\\"|[^"])*?)"', Q = "'((\\\\'|[^'])*?)'", V = /^#$/, _ = "'", G = '"', U = "$", $ = "", z = 4294967296;
].join("|") + ")", D = new RegExp("^" + j + "$"), _ = "|&;()<> \\t", M = '"((\\\\"|[^"])*?)"', Q = "'((\\\\'|[^'])*?)'", V = /^#$/, q = "'", G = '"', U = "$", R = "", z = 4294967296;
for (var L = 0; L < 4; L++)
$ += (z * Math.random()).toString(16);
var J = new RegExp("^" + $);
R += (z * Math.random()).toString(16);
var J = new RegExp("^" + R);
function X(n, s) {
for (var e = s.lastIndex, t = [], c; c = s.exec(n); )
t.push(c), s.lastIndex === c.index && (s.lastIndex += 1);
return s.lastIndex = e, t;
for (var t = s.lastIndex, r = [], l; l = s.exec(n); )
r.push(l), s.lastIndex === l.index && (s.lastIndex += 1);
return s.lastIndex = t, r;
}
function F(n, s, e) {
var t = typeof n == "function" ? n(e) : n[e];
return typeof t > "u" && e != "" ? t = "" : typeof t > "u" && (t = "$"), typeof t == "object" ? s + $ + JSON.stringify(t) + $ : s + t;
function F(n, s, t) {
var r = typeof n == "function" ? n(t) : n[t];
return typeof r > "u" && t != "" ? r = "" : typeof r > "u" && (r = "$"), typeof r == "object" ? s + R + JSON.stringify(r) + R : s + r;
}
function K(n, s, e) {
e || (e = {});
var t = e.escape || "\\", c = "(\\" + t + `['"` + q + `]|[^\\s'"` + q + "])+", m = new RegExp([
function K(n, s, t) {
t || (t = {});
var r = t.escape || "\\", l = "(\\" + r + `['"` + _ + `]|[^\\s'"` + _ + "])+", h = new RegExp([
"(" + j + ")",
// control chars
"(" + c + "|" + M + "|" + Q + ")+"
].join("|"), "g"), f = X(n, m);
if (f.length === 0)
"(" + l + "|" + M + "|" + Q + ")+"
].join("|"), "g"), p = X(n, h);
if (p.length === 0)
return [];
s || (s = {});
var w = !1;
return f.map(function(r) {
var a = r[0];
if (!a || w)
var g = !1;
return p.map(function(d) {
var e = d[0];
if (!e || g)
return;
if (D.test(a))
return { op: a };
var x = !1, C = !1, d = "", O = !1, i;
function T() {
i += 1;
var v, p, R = a.charAt(i);
if (R === "{") {
if (i += 1, a.charAt(i) === "}")
throw new Error("Bad substitution: " + a.slice(i - 2, i + 1));
if (v = a.indexOf("}", i), v < 0)
throw new Error("Bad substitution: " + a.slice(i));
p = a.slice(i, v), i = v;
} else if (/[*@#?$!_-]/.test(R))
p = R, i += 1;
if (D.test(e))
return { op: e };
var c = !1, T = !1, m = "", O = !1, a;
function $() {
a += 1;
var x, f, C = e.charAt(a);
if (C === "{") {
if (a += 1, e.charAt(a) === "}")
throw new Error("Bad substitution: " + e.slice(a - 2, a + 1));
if (x = e.indexOf("}", a), x < 0)
throw new Error("Bad substitution: " + e.slice(a));
f = e.slice(a, x), a = x;
} else if (/[*@#?$!_-]/.test(C))
f = C, a += 1;
else {
var g = a.slice(i);
v = g.match(/[^\w\d_]/), v ? (p = g.slice(0, v.index), i += v.index - 1) : (p = g, i = a.length);
var w = e.slice(a);
x = w.match(/[^\w\d_]/), x ? (f = w.slice(0, x.index), a += x.index - 1) : (f = w, a = e.length);
}
return F(s, "", p);
return F(s, "", f);
}
for (i = 0; i < a.length; i++) {
var u = a.charAt(i);
if (O = O || !x && (u === "*" || u === "?"), C)
d += u, C = !1;
else if (x)
u === x ? x = !1 : x == _ ? d += u : u === t ? (i += 1, u = a.charAt(i), u === G || u === t || u === U ? d += u : d += t + u) : u === U ? d += T() : d += u;
else if (u === G || u === _)
x = u;
for (a = 0; a < e.length; a++) {
var u = e.charAt(a);
if (O = O || !c && (u === "*" || u === "?"), T)
m += u, T = !1;
else if (c)
u === c ? c = !1 : c == q ? m += u : u === r ? (a += 1, u = e.charAt(a), u === G || u === r || u === U ? m += u : m += r + u) : u === U ? m += $() : m += u;
else if (u === G || u === q)
c = u;
else {
if (D.test(u))
return { op: a };
return { op: e };
if (V.test(u)) {
w = !0;
var b = { comment: n.slice(r.index + i + 1) };
return d.length ? [d, b] : [b];
g = !0;
var b = { comment: n.slice(d.index + a + 1) };
return m.length ? [m, b] : [b];
} else
u === t ? C = !0 : u === U ? d += T() : d += u;
u === r ? T = !0 : u === U ? m += $() : m += u;
}
}
return O ? { op: "glob", pattern: d } : d;
}).reduce(function(r, a) {
return typeof a > "u" ? r : r.concat(a);
return O ? { op: "glob", pattern: m } : m;
}).reduce(function(d, e) {
return typeof e > "u" ? d : d.concat(e);
}, []);
}
var Y = function(s, e, t) {
var c = K(s, e, t);
return typeof e != "function" ? c : c.reduce(function(m, f) {
if (typeof f == "object")
return m.concat(f);
var w = f.split(RegExp("(" + $ + ".*?" + $ + ")", "g"));
return w.length === 1 ? m.concat(w[0]) : m.concat(w.filter(Boolean).map(function(r) {
return J.test(r) ? JSON.parse(r.split($)[1]) : r;
var Y = function(s, t, r) {
var l = K(s, t, r);
return typeof t != "function" ? l : l.reduce(function(h, p) {
if (typeof p == "object")
return h.concat(p);
var g = p.split(RegExp("(" + R + ".*?" + R + ")", "g"));
return g.length === 1 ? h.concat(g[0]) : h.concat(g.filter(Boolean).map(function(d) {
return J.test(d) ? JSON.parse(d.split(R)[1]) : d;
}));
}, []);
}, Z = Y;
@@ -115,138 +115,138 @@ const ae = "curl", se = "cURL", ie = "cURL command line tool", H = ["d", "data",
// Request method
H
].flatMap((n) => n);
function oe(n) {
if (!n.match(/^\s*curl /))
function oe(n, s) {
if (!s.match(/^\s*curl /))
return null;
const s = [], e = n.replace(/\ncurl/g, "; curl");
let t = [];
const m = Z(e).flatMap((r) => typeof r == "string" && r.startsWith("-") && !r.startsWith("--") && r.length > 2 ? [r.slice(0, 2), r.slice(2)] : r);
for (const r of m) {
if (typeof r == "string") {
r.startsWith("$") ? t.push(r.slice(1)) : t.push(r);
const t = [], r = s.replace(/\ncurl/g, "; curl");
let l = [];
const p = Z(r).flatMap((e) => typeof e == "string" && e.startsWith("-") && !e.startsWith("--") && e.length > 2 ? [e.slice(0, 2), e.slice(2)] : e);
for (const e of p) {
if (typeof e == "string") {
e.startsWith("$") ? l.push(e.slice(1)) : l.push(e);
continue;
}
if ("comment" in r)
if ("comment" in e)
continue;
const { op: a } = r;
if (a === ";") {
s.push(t), t = [];
const { op: c } = e;
if (c === ";") {
t.push(l), l = [];
continue;
}
if (a != null && a.startsWith("$")) {
const x = a.slice(2, a.length - 1).replace(/\\'/g, "'");
t.push(x);
if (c != null && c.startsWith("$")) {
const T = c.slice(2, c.length - 1).replace(/\\'/g, "'");
l.push(T);
continue;
}
a === "glob" && t.push(r.pattern);
c === "glob" && l.push(e.pattern);
}
s.push(t);
const f = {
t.push(l);
const g = {
model: "workspace",
id: N("workspace"),
name: "Curl Import"
};
return {
resources: {
httpRequests: s.filter((r) => r[0] === "curl").map((r) => te(r, f.id)),
workspaces: [f]
httpRequests: t.filter((e) => e[0] === "curl").map((e) => te(e, g.id)),
workspaces: [g]
}
};
}
function te(n, s) {
const e = {}, t = [];
for (let o = 1; o < n.length; o++) {
let l = n[o];
if (typeof l == "string" && (l = l.trim()), typeof l == "string" && l.match(/^-{1,2}[\w-]+/)) {
const E = l[0] === "-" && l[1] !== "-";
let h = l.replace(/^-{1,2}/, "");
if (!ee.includes(h))
const t = {}, r = [];
for (let i = 1; i < n.length; i++) {
let o = n[i];
if (typeof o == "string" && (o = o.trim()), typeof o == "string" && o.match(/^-{1,2}[\w-]+/)) {
const E = o[0] === "-" && o[1] !== "-";
let v = o.replace(/^-{1,2}/, "");
if (!ee.includes(v))
continue;
let y;
const S = n[o + 1];
E && h.length > 1 ? (y = h.slice(1), h = h.slice(0, 1)) : typeof S == "string" && !S.startsWith("-") ? (y = S, o++) : y = !0, e[h] = e[h] || [], e[h].push(y);
const S = n[i + 1];
E && v.length > 1 ? (y = v.slice(1), v = v.slice(0, 1)) : typeof S == "string" && !S.startsWith("-") ? (y = S, i++) : y = !0, t[v] = t[v] || [], t[v].push(y);
} else
l && t.push(l);
o && r.push(o);
}
let c, m;
const f = A(e, t[0] || "", ["url"]), [w, r] = W(f, "?");
c = (r == null ? void 0 : r.split("&").map((o) => {
const l = W(o, "=");
return { name: l[0] ?? "", value: l[1] ?? "", enabled: !0 };
})) ?? [], m = w ?? f;
const [a, x] = A(e, "", ["u", "user"]).split(/:(.*)$/), C = A(e, !1, ["digest"]), d = a ? C ? "digest" : "basic" : null, O = a ? {
username: a.trim(),
password: (x ?? "").trim()
} : {}, i = [
...e.header || [],
...e.H || []
].map((o) => {
const [l, E] = o.split(/:(.*)$/);
let l, h;
const p = A(t, r[0] || "", ["url"]), [g, d] = W(p, "?");
l = (d == null ? void 0 : d.split("&").map((i) => {
const o = W(i, "=");
return { name: o[0] ?? "", value: o[1] ?? "", enabled: !0 };
})) ?? [], h = g ?? p;
const [e, c] = A(t, "", ["u", "user"]).split(/:(.*)$/), T = A(t, !1, ["digest"]), m = e ? T ? "digest" : "basic" : null, O = e ? {
username: e.trim(),
password: (c ?? "").trim()
} : {}, a = [
...t.header || [],
...t.H || []
].map((i) => {
const [o, E] = i.split(/:(.*)$/);
return E ? {
name: (l ?? "").trim(),
name: (o ?? "").trim(),
value: E.trim(),
enabled: !0
} : {
name: (l ?? "").trim().replace(/;$/, ""),
name: (o ?? "").trim().replace(/;$/, ""),
value: "",
enabled: !0
};
}), T = [
...e.cookie || [],
...e.b || []
].map((o) => {
const l = o.split("=", 1)[0], E = o.replace(`${l}=`, "");
return `${l}=${E}`;
}).join("; "), u = i.find((o) => o.name.toLowerCase() === "cookie");
T && u ? u.value += `; ${T}` : T && i.push({
}), $ = [
...t.cookie || [],
...t.b || []
].map((i) => {
const o = i.split("=", 1)[0], E = i.replace(`${o}=`, "");
return `${o}=${E}`;
}).join("; "), u = a.find((i) => i.name.toLowerCase() === "cookie");
$ && u ? u.value += `; ${$}` : $ && a.push({
name: "Cookie",
value: T,
value: $,
enabled: !0
});
const b = ne(e), v = i.find((o) => o.name.toLowerCase() === "content-type"), p = v ? v.value.split(";")[0] : null, R = [
...e.form || [],
...e.F || []
].map((o) => {
const l = o.split("="), E = l[0] ?? "", h = l[1] ?? "", y = {
const b = ne(t), x = a.find((i) => i.name.toLowerCase() === "content-type"), f = x ? x.value.split(";")[0] : null, C = [
...t.form || [],
...t.F || []
].map((i) => {
const o = i.split("="), E = o[0] ?? "", v = o[1] ?? "", y = {
name: E,
enabled: !0
};
return h.indexOf("@") === 0 ? y.file = h.slice(1) : y.value = h, y;
return v.indexOf("@") === 0 ? y.file = v.slice(1) : y.value = v, y;
});
let g = {}, I = null;
const B = A(e, !1, ["G", "get"]);
b.length > 0 && B ? c.push(...b) : b.length > 0 && (p == null || p === "application/x-www-form-urlencoded") ? (I = p ?? "application/x-www-form-urlencoded", g = {
form: b.map((o) => ({
...o,
name: decodeURIComponent(o.name || ""),
value: decodeURIComponent(o.value || "")
let w = {}, I = null;
const B = A(t, !1, ["G", "get"]);
b.length > 0 && B ? l.push(...b) : b.length > 0 && (f == null || f === "application/x-www-form-urlencoded") ? (I = f ?? "application/x-www-form-urlencoded", w = {
form: b.map((i) => ({
...i,
name: decodeURIComponent(i.name || ""),
value: decodeURIComponent(i.value || "")
}))
}, i.push({
}, a.push({
name: "Content-Type",
value: "application/x-www-form-urlencoded",
enabled: !0
})) : b.length > 0 ? (I = p === "application/json" || p === "text/xml" || p === "text/plain" ? p : "other", g = {
text: b.map(({ name: o, value: l }) => o && l ? `${o}=${l}` : o || l).join("&")
}) : R.length && (I = p ?? "multipart/form-data", g = {
form: R
}, p == null && i.push({
})) : b.length > 0 ? (I = f === "application/json" || f === "text/xml" || f === "text/plain" ? f : "other", w = {
text: b.map(({ name: i, value: o }) => i && o ? `${i}=${o}` : i || o).join("&")
}) : C.length && (I = f ?? "multipart/form-data", w = {
form: C
}, f == null && a.push({
name: "Content-Type",
value: "multipart/form-data",
enabled: !0
}));
let P = A(e, "", ["X", "request"]).toUpperCase();
return P === "" && g && (P = "text" in g || "form" in g ? "POST" : "GET"), {
let P = A(t, "", ["X", "request"]).toUpperCase();
return P === "" && w && (P = "text" in w || "form" in w ? "POST" : "GET"), {
id: N("http_request"),
model: "http_request",
workspaceId: s,
name: "",
urlParameters: c,
url: m,
urlParameters: l,
url: h,
method: P,
headers: i,
headers: a,
authentication: O,
authenticationType: d,
body: g,
authenticationType: m,
body: w,
bodyType: I,
folderId: null,
sortPriority: 0
@@ -254,35 +254,35 @@ function te(n, s) {
}
const ne = (n) => {
let s = [];
for (const e of H) {
const t = n[e];
if (!(!t || t.length === 0))
for (const c of t) {
if (typeof c != "string")
for (const t of H) {
const r = n[t];
if (!(!r || r.length === 0))
for (const l of r) {
if (typeof l != "string")
continue;
const [m, f] = c.split("=");
c.startsWith("@") ? s.push({
name: m ?? "",
const [h, p] = l.split("=");
l.startsWith("@") ? s.push({
name: h ?? "",
value: "",
filePath: c.slice(1),
filePath: l.slice(1),
enabled: !0
}) : s.push({
name: m ?? "",
value: e === "data-urlencode" ? encodeURIComponent(f ?? "") : f ?? "",
name: h ?? "",
value: t === "data-urlencode" ? encodeURIComponent(p ?? "") : p ?? "",
enabled: !0
});
}
}
return s;
}, A = (n, s, e) => {
for (const t of e)
if (n[t] && n[t].length)
return n[t][0];
}, A = (n, s, t) => {
for (const r of t)
if (n[r] && n[r].length)
return n[r][0];
return s;
};
function W(n, s) {
const e = n.indexOf(s);
return e > -1 ? [n.slice(0, e), n.slice(e + 1)] : [n];
const t = n.indexOf(s);
return t > -1 ? [n.slice(0, t), n.slice(t + 1)] : [n];
}
const k = {};
function N(n) {
@@ -295,3 +295,4 @@ export {
se as name,
oe as pluginHookImport
};
//# sourceMappingURL=index.mjs.map

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -1,72 +1,73 @@
const S = "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", _ = "https://schema.getpostman.com/json/collection/v2.0.0/collection.json", O = [_, S];
function v(e) {
var g;
const t = k(e);
if (t == null)
const _ = "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", C = "https://schema.getpostman.com/json/collection/v2.0.0/collection.json", O = [C, _];
function v(e, t) {
var w;
console.log("CTX", e);
const a = q(t);
if (a == null)
return;
const o = i(t.info);
if (!O.includes(o.schema) || !Array.isArray(t.item))
const s = i(a.info);
if (!O.includes(s.schema) || !Array.isArray(a.item))
return;
const u = A(t.auth), s = {
const u = k(a.auth), n = {
workspaces: [],
environments: [],
httpRequests: [],
folders: []
}, n = {
}, p = {
model: "workspace",
id: h("workspace"),
name: o.name || "Postman Import",
description: o.description || "",
variables: ((g = t.variable) == null ? void 0 : g.map((r) => ({
id: b("workspace"),
name: s.name || "Postman Import",
description: s.description || "",
variables: ((w = a.variable) == null ? void 0 : w.map((r) => ({
name: r.key,
value: r.value
}))) ?? []
};
s.workspaces.push(n);
const T = (r, p = null) => {
n.workspaces.push(p);
const g = (r, d = null) => {
if (typeof r.name == "string" && Array.isArray(r.item)) {
const a = {
const o = {
model: "folder",
workspaceId: n.id,
id: h("folder"),
workspaceId: p.id,
id: b("folder"),
name: r.name,
folderId: p
folderId: d
};
s.folders.push(a);
n.folders.push(o);
for (const l of r.item)
T(l, a.id);
g(l, o.id);
} else if (typeof r.name == "string" && "request" in r) {
const a = i(r.request), l = j(a.body), w = A(a.auth), d = w.authenticationType == null ? u : w, q = {
const o = i(r.request), l = j(o.body), A = k(o.auth), m = A.authenticationType == null ? u : A, S = {
model: "http_request",
id: h("http_request"),
workspaceId: n.id,
folderId: p,
id: b("http_request"),
workspaceId: p.id,
folderId: d,
name: r.name,
method: a.method || "GET",
url: typeof a.url == "string" ? a.url : i(a.url).raw,
method: o.method || "GET",
url: typeof o.url == "string" ? o.url : i(o.url).raw,
body: l.body,
bodyType: l.bodyType,
authentication: d.authentication,
authenticationType: d.authenticationType,
authentication: m.authentication,
authenticationType: m.authenticationType,
headers: [
...l.headers,
...d.headers,
...b(a.header).map((m) => ({
name: m.key,
value: m.value,
enabled: !m.disabled
...m.headers,
...f(o.header).map((y) => ({
name: y.key,
value: y.value,
enabled: !y.disabled
}))
]
};
s.httpRequests.push(q);
n.httpRequests.push(S);
} else
console.log("Unknown item", r, p);
console.log("Unknown item", r, d);
};
for (const r of t.item)
T(r);
return { resources: f(s) };
for (const r of a.item)
g(r);
return { resources: T(n) };
}
function A(e) {
function k(e) {
const t = i(e);
return "basic" in t ? {
headers: [],
@@ -84,7 +85,7 @@ function A(e) {
} : { headers: [], authenticationType: null, authentication: {} };
}
function j(e) {
var o, c, u, s;
var a, s, c, u;
const t = i(e);
return "graphql" in t ? {
headers: [
@@ -97,7 +98,7 @@ function j(e) {
bodyType: "graphql",
body: {
text: JSON.stringify(
{ query: t.graphql.query, variables: k(t.graphql.variables) },
{ query: t.graphql.query, variables: q(t.graphql.variables) },
null,
2
)
@@ -112,7 +113,7 @@ function j(e) {
],
bodyType: "application/x-www-form-urlencoded",
body: {
form: b(t.urlencoded).map((n) => ({
form: f(t.urlencoded).map((n) => ({
enabled: !n.disabled,
name: n.key ?? "",
value: n.value ?? ""
@@ -128,7 +129,7 @@ function j(e) {
],
bodyType: "multipart/form-data",
body: {
form: b(t.formdata).map(
form: f(t.formdata).map(
(n) => n.src != null ? {
enabled: !n.disabled,
contentType: n.contentType ?? null,
@@ -145,17 +146,17 @@ function j(e) {
headers: [
{
name: "Content-Type",
value: ((c = (o = t.options) == null ? void 0 : o.raw) == null ? void 0 : c.language) === "json" ? "application/json" : "",
value: ((s = (a = t.options) == null ? void 0 : a.raw) == null ? void 0 : s.language) === "json" ? "application/json" : "",
enabled: !0
}
],
bodyType: ((s = (u = t.options) == null ? void 0 : u.raw) == null ? void 0 : s.language) === "json" ? "application/json" : "other",
bodyType: ((u = (c = t.options) == null ? void 0 : c.raw) == null ? void 0 : u.language) === "json" ? "application/json" : "other",
body: {
text: t.raw ?? ""
}
} : { headers: [], bodyType: null, body: {} };
}
function k(e) {
function q(e) {
try {
return i(JSON.parse(e));
} catch {
@@ -165,18 +166,19 @@ function k(e) {
function i(e) {
return Object.prototype.toString.call(e) === "[object Object]" ? e : {};
}
function b(e) {
function f(e) {
return Object.prototype.toString.call(e) === "[object Array]" ? e : [];
}
function f(e) {
return typeof e == "string" ? e.replace(/{{\s*(_\.)?([^}]+)\s*}}/g, "${[$2]}") : Array.isArray(e) && e != null ? e.map(f) : typeof e == "object" && e != null ? Object.fromEntries(
Object.entries(e).map(([t, o]) => [t, f(o)])
function T(e) {
return typeof e == "string" ? e.replace(/{{\s*(_\.)?([^}]+)\s*}}/g, "${[$2]}") : Array.isArray(e) && e != null ? e.map(T) : typeof e == "object" && e != null ? Object.fromEntries(
Object.entries(e).map(([t, a]) => [t, T(a)])
) : e;
}
const y = {};
function h(e) {
return y[e] = (y[e] ?? -1) + 1, `GENERATE_ID::${e.toUpperCase()}_${y[e]}`;
const h = {};
function b(e) {
return h[e] = (h[e] ?? -1) + 1, `GENERATE_ID::${e.toUpperCase()}_${h[e]}`;
}
export {
v as pluginHookImport
};
//# sourceMappingURL=index.mjs.map

File diff suppressed because one or more lines are too long

View File

@@ -1,17 +1,18 @@
function u(r) {
function u(r, t) {
let e;
try {
e = JSON.parse(r);
e = JSON.parse(t);
} catch {
return;
}
if (!(!t(e) || !("yaakSchema" in e)))
if (!(!s(e) || !("yaakSchema" in e)))
return "requests" in e.resources && (e.resources.httpRequests = e.resources.requests, delete e.resources.requests), { resources: e.resources };
}
function t(r) {
function s(r) {
return Object.prototype.toString.call(r) === "[object Object]";
}
export {
t as isJSObject,
s as isJSObject,
u as pluginHookImport
};
//# sourceMappingURL=index.mjs.map

View File

@@ -0,0 +1 @@
{"version":3,"file":"index.mjs","sources":["../../../plugins/importer-yaak/src/index.ts"],"sourcesContent":["export function pluginHookImport(ctx: any, contents: string) {\n let parsed;\n try {\n parsed = JSON.parse(contents);\n } catch (err) {\n return undefined;\n }\n\n if (!isJSObject(parsed)) {\n return undefined;\n }\n\n const isYaakExport = 'yaakSchema' in parsed;\n if (!isYaakExport) {\n return;\n }\n\n // Migrate v1 to v2 -- changes requests to httpRequests\n if ('requests' in parsed.resources) {\n parsed.resources.httpRequests = parsed.resources.requests;\n delete parsed.resources['requests'];\n }\n\n return { resources: parsed.resources }; // Should already be in the correct format\n}\n\nexport function isJSObject(obj: any) {\n return Object.prototype.toString.call(obj) === '[object Object]';\n}\n"],"names":["pluginHookImport","ctx","contents","parsed","isJSObject","obj"],"mappings":"AAAgB,SAAAA,EAAiBC,GAAUC,GAAkB;AACvD,MAAAC;AACA,MAAA;AACO,IAAAA,IAAA,KAAK,MAAMD,CAAQ;AAAA,UAChB;AACL;AAAA,EACT;AAOA,MALI,GAACE,EAAWD,CAAM,KAKlB,EADiB,gBAAgBA;AAMjC,WAAA,cAAcA,EAAO,cAChBA,EAAA,UAAU,eAAeA,EAAO,UAAU,UAC1C,OAAAA,EAAO,UAAU,WAGnB,EAAE,WAAWA,EAAO;AAC7B;AAEO,SAASC,EAAWC,GAAU;AACnC,SAAO,OAAO,UAAU,SAAS,KAAKA,CAAG,MAAM;AACjD;"}

View File

@@ -1,6 +1,6 @@
use std::fmt::Display;
use log::{debug, info, warn};
use log::{debug, info};
use serde::{Deserialize, Serialize};
use serde_json::json;
use sqlx::types::JsonValue;

305
src-tauri/src/deno.rs Normal file
View File

@@ -0,0 +1,305 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
//! This example shows how to use swc to transpile TypeScript and JSX/TSX
//! modules.
//!
//! It will only transpile, not typecheck (like Deno's `--no-check` flag).
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use std::sync::Arc;
use anyhow::anyhow;
use anyhow::bail;
use anyhow::Context;
use anyhow::Error;
use deno_ast::ParseParams;
use deno_ast::{EmitOptions, MediaType, SourceMapOption, TranspileOptions};
use deno_core::error::{AnyError, JsError};
use deno_core::resolve_path;
use deno_core::JsRuntime;
use deno_core::ModuleLoadResponse;
use deno_core::ModuleLoader;
use deno_core::ModuleSource;
use deno_core::ModuleSourceCode;
use deno_core::ModuleSpecifier;
use deno_core::ModuleType;
use deno_core::RequestedModuleType;
use deno_core::ResolutionKind;
use deno_core::RuntimeOptions;
use deno_core::SourceMapGetter;
use deno_core::{resolve_import, v8};
use tokio::task::block_in_place;
use crate::deno_ops::op_yaml_parse;
use crate::plugin::PluginCapability;
#[derive(Clone)]
struct SourceMapStore(Rc<RefCell<HashMap<String, Vec<u8>>>>);
impl SourceMapGetter for SourceMapStore {
fn get_source_map(&self, specifier: &str) -> Option<Vec<u8>> {
self.0.borrow().get(specifier).cloned()
}
fn get_source_line(&self, _file_name: &str, _line_number: usize) -> Option<String> {
None
}
}
struct TypescriptModuleLoader {
source_maps: SourceMapStore,
}
impl ModuleLoader for TypescriptModuleLoader {
fn resolve(
&self,
specifier: &str,
referrer: &str,
_kind: ResolutionKind,
) -> Result<ModuleSpecifier, Error> {
Ok(resolve_import(specifier, referrer)?)
}
fn load(
&self,
module_specifier: &ModuleSpecifier,
_maybe_referrer: Option<&ModuleSpecifier>,
_is_dyn_import: bool,
_requested_module_type: RequestedModuleType,
) -> ModuleLoadResponse {
let source_maps = self.source_maps.clone();
fn load(
source_maps: SourceMapStore,
module_specifier: &ModuleSpecifier,
) -> Result<ModuleSource, AnyError> {
let path = module_specifier
.to_file_path()
.map_err(|_| anyhow!("Only file:// URLs are supported."))?;
let media_type = MediaType::from_path(&path);
let (module_type, should_transpile) = match MediaType::from_path(&path) {
MediaType::JavaScript | MediaType::Mjs | MediaType::Cjs => {
(ModuleType::JavaScript, false)
}
MediaType::Jsx => (ModuleType::JavaScript, true),
MediaType::TypeScript
| MediaType::Mts
| MediaType::Cts
| MediaType::Dts
| MediaType::Dmts
| MediaType::Dcts
| MediaType::Tsx => (ModuleType::JavaScript, true),
MediaType::Json => (ModuleType::Json, false),
_ => bail!("Unknown extension {:?}", path.extension()),
};
let code = std::fs::read_to_string(&path)?;
let code = if should_transpile {
let parsed = deno_ast::parse_module(ParseParams {
specifier: module_specifier.clone(),
text: Arc::from(code),
media_type,
capture_tokens: false,
scope_analysis: false,
maybe_syntax: None,
})?;
let res = parsed.transpile(
&TranspileOptions::default(),
&EmitOptions {
source_map: SourceMapOption::Separate,
inline_sources: true,
..Default::default()
},
)?;
let src = res.into_source();
let source_map = src.source_map.unwrap();
let source = src.source;
source_maps
.0
.borrow_mut()
.insert(module_specifier.to_string(), source_map);
String::from_utf8(source).unwrap()
} else {
code
};
Ok(ModuleSource::new(
module_type,
ModuleSourceCode::String(code.into()),
module_specifier,
None,
))
}
ModuleLoadResponse::Sync(load(source_maps, module_specifier))
}
}
pub fn run_plugin_block(
plugin_index_file: &str,
fn_name: &str,
fn_args: Vec<serde_json::Value>,
) -> Result<serde_json::Value, Error> {
block_in_place(|| {
tauri::async_runtime::block_on(run_plugin(plugin_index_file, fn_name, fn_args))
})
}
deno_core::extension!(
yaak_runtime,
ops = [ op_yaml_parse ],
esm_entry_point = "ext:yaak_runtime/yaml.js",
esm = [dir "src/plugin-runtime", "yaml.js"]
);
async fn run_plugin(
plugin_index_file: &str,
fn_name: &str,
fn_args: Vec<serde_json::Value>,
) -> Result<serde_json::Value, Error> {
let mut js_runtime = load_js_runtime()?;
let module_namespace = load_main_module(&mut js_runtime, plugin_index_file).await?;
let scope = &mut js_runtime.handle_scope();
let module_namespace = v8::Local::<v8::Object>::new(scope, module_namespace);
// Get the exported function we're calling
let func_key = v8::String::new(scope, fn_name).unwrap();
let func = module_namespace.get(scope, func_key.into()).unwrap();
let func = v8::Local::<v8::Function>::try_from(func).unwrap();
let tc_scope = &mut v8::TryCatch::new(scope);
// Create Yaak context object
let null = v8::null(tc_scope).into();
let name = v8::String::new(tc_scope, "foo").unwrap().into();
let value = v8::String::new(tc_scope, "bar").unwrap().into();
let yaak_ctx: v8::Local<v8::Value> =
v8::Object::with_prototype_and_properties(tc_scope, null, &[name], &[value]).into();
// Create the function arguments
let passed_args = &mut fn_args
.iter()
.map(|a| {
let v: v8::Local<v8::Value> = deno_core::serde_v8::to_v8(tc_scope, a).unwrap();
v
})
.collect::<Vec<v8::Local<v8::Value>>>();
let all_args = &mut vec![yaak_ctx];
all_args.append(passed_args);
// Call the function
let func_res = func.call(tc_scope, module_namespace.into(), all_args);
// Catch and return any thrown errors
if tc_scope.has_caught() {
let e = tc_scope.exception().unwrap();
let js_error = JsError::from_v8_exception(tc_scope, e);
return Err(Error::msg(js_error.stack.unwrap_or_default()));
}
// Handle the result
match func_res {
None => Ok(serde_json::Value::Null),
Some(res) => {
if res.is_null() || res.is_undefined() {
Ok(serde_json::Value::Null)
} else {
let value: serde_json::Value = deno_core::serde_v8::from_v8(tc_scope, res).unwrap();
Ok(value)
}
}
}
}
pub fn get_plugin_capabilities_block(plugin_index_file: &str) -> Result<Vec<PluginCapability>, Error> {
block_in_place(|| tauri::async_runtime::block_on(get_plugin_capabilities(plugin_index_file)))
}
pub async fn get_plugin_capabilities(
plugin_index_file: &str,
) -> Result<Vec<PluginCapability>, Error> {
let mut js_runtime = load_js_runtime()?;
let module_namespace = load_main_module(&mut js_runtime, plugin_index_file).await?;
let scope = &mut js_runtime.handle_scope();
let module_namespace = v8::Local::<v8::Object>::new(scope, module_namespace);
let property_names =
match module_namespace.get_own_property_names(scope, v8::GetPropertyNamesArgs::default()) {
None => return Ok(Vec::new()),
Some(names) => names,
};
let mut capabilities: Vec<PluginCapability> = Vec::new();
for i in 0..property_names.length() {
let name = property_names.get_index(scope, i);
let name = match name {
Some(name) => name,
None => return Ok(Vec::new()),
};
match name.to_rust_string_lossy(scope).as_str() {
"pluginHookImport" => _ = capabilities.push(PluginCapability::Import),
"pluginHookExport" => _ = capabilities.push(PluginCapability::Export),
"pluginHookResponseFilter" => _ = capabilities.push(PluginCapability::Filter),
_ => {}
};
}
Ok(capabilities)
}
async fn load_main_module(
js_runtime: &mut JsRuntime,
plugin_index_file: &str,
) -> Result<v8::Global<v8::Object>, Error> {
let main_module = resolve_path(
plugin_index_file,
&std::env::current_dir().context("Unable to get CWD")?,
)?;
// Load the main module so we can do stuff with it
let mod_id = js_runtime.load_main_es_module(&main_module).await?;
let result = js_runtime.mod_evaluate(mod_id);
js_runtime.run_event_loop(Default::default()).await?;
result.await?;
let module_namespace = js_runtime.get_module_namespace(mod_id).unwrap();
Ok(module_namespace)
}
fn load_js_runtime<'s>() -> Result<JsRuntime, Error> {
let source_map_store = SourceMapStore(Rc::new(RefCell::new(HashMap::new())));
let mut ext_console = deno_console::deno_console::init_ops_and_esm();
ext_console.esm_entry_point = Some("ext:deno_console/01_console.js");
let ext_yaak = yaak_runtime::init_ops_and_esm();
let js_runtime = JsRuntime::new(RuntimeOptions {
module_loader: Some(Rc::new(TypescriptModuleLoader {
source_maps: source_map_store.clone(),
})),
source_map_getter: Some(Rc::new(source_map_store)),
extensions: vec![ext_console, ext_yaak],
..Default::default()
});
// let main_module = resolve_path(
// plugin_index_file.to_str().unwrap(),
// &std::env::current_dir().context("Unable to get CWD")?,
// )?;
//
// // Load the main module so we can do stuff with it
// let mod_id = js_runtime.load_main_es_module(&main_module).await?;
// let result = js_runtime.mod_evaluate(mod_id);
// js_runtime.run_event_loop(Default::default()).await?;
// result.await?;
//
// let module_namespace = js_runtime.get_module_namespace(mod_id).unwrap();
// let scope = &mut js_runtime.handle_scope();
// let module_namespace = v8::Local::<v8::Object>::new(scope, module_namespace);
Ok(js_runtime)
}

16
src-tauri/src/deno_ops.rs Normal file
View File

@@ -0,0 +1,16 @@
use deno_core::error::AnyError;
use deno_core::op2;
#[op2]
#[serde]
pub fn op_yaml_parse(#[string] text: String) -> Result<serde_json::Value, AnyError> {
let value = serde_yaml::from_str(&text)?;
Ok(value)
}
#[op2]
#[string]
pub fn op_yaml_stringify(#[serde] value: serde_json::Value) -> Result<String, AnyError> {
let value = serde_yaml::to_string(&value)?;
Ok(value)
}

View File

@@ -8,15 +8,17 @@ use std::time::Duration;
use base64::Engine;
use http::header::{ACCEPT, USER_AGENT};
use http::{HeaderMap, HeaderName, HeaderValue, Method};
use http::{HeaderMap, HeaderName, HeaderValue};
use log::{error, info, warn};
use reqwest::redirect::Policy;
use reqwest::Method;
use reqwest::{multipart, Url};
use sqlx::types::{Json, JsonValue};
use tauri::{Manager, WebviewWindow};
use tokio::sync::oneshot;
use tokio::sync::watch::Receiver;
use crate::render::variables_from_environment;
use crate::{models, render, response_err};
pub async fn send_http_request(
@@ -32,8 +34,9 @@ pub async fn send_http_request(
let workspace = models::get_workspace(window, &request.workspace_id)
.await
.expect("Failed to get Workspace");
let vars = variables_from_environment(&workspace, environment_ref);
let mut url_string = render::render(&request.url, &workspace, environment.as_ref());
let mut url_string = render::render(&request.url, &vars);
url_string = ensure_proto(&url_string);
if !url_string.starts_with("http://") && !url_string.starts_with("https://") {
@@ -45,6 +48,7 @@ pub async fn send_http_request(
true => Policy::limited(10), // TODO: Handle redirects natively
false => Policy::none(),
})
.connection_verbose(true)
.gzip(true)
.brotli(true)
.deflate(true)
@@ -106,7 +110,7 @@ pub async fn send_http_request(
format!("Failed to parse URL \"{}\": {}", url_string, e.to_string()),
window,
)
.await;
.await;
}
};
@@ -142,8 +146,8 @@ pub async fn send_http_request(
continue;
}
let name = render::render(&h.name, &workspace, environment_ref);
let value = render::render(&h.value, &workspace, environment_ref);
let name = render::render(&h.name, &vars);
let value = render::render(&h.value, &vars);
let header_name = match HeaderName::from_bytes(name.as_bytes()) {
Ok(n) => n,
@@ -178,8 +182,8 @@ pub async fn send_http_request(
.unwrap_or(empty_value)
.as_str()
.unwrap_or("");
let username = render::render(raw_username, &workspace, environment_ref);
let password = render::render(raw_password, &workspace, environment_ref);
let username = render::render(raw_username, &vars);
let password = render::render(raw_password, &vars);
let auth = format!("{username}:{password}");
let encoded = base64::engine::general_purpose::STANDARD_NO_PAD.encode(auth);
@@ -189,7 +193,7 @@ pub async fn send_http_request(
);
} else if b == "bearer" {
let raw_token = a.get("token").unwrap_or(empty_value).as_str().unwrap_or("");
let token = render::render(raw_token, &workspace, environment_ref);
let token = render::render(raw_token, &vars);
headers.insert(
"Authorization",
HeaderValue::from_str(&format!("Bearer {token}")).unwrap(),
@@ -203,8 +207,8 @@ pub async fn send_http_request(
continue;
}
query_params.push((
render::render(&p.name, &workspace, environment_ref),
render::render(&p.value, &workspace, environment_ref),
render::render(&p.name, &vars),
render::render(&p.value, &vars),
));
}
request_builder = request_builder.query(&query_params);
@@ -220,7 +224,7 @@ pub async fn send_http_request(
.unwrap_or(empty_string)
.as_str()
.unwrap_or("");
let body = render::render(raw_text, &workspace, environment_ref);
let body = render::render(raw_text, &vars);
request_builder = request_builder.body(body);
} else if body_type == "application/x-www-form-urlencoded"
&& request_body.contains_key("form")
@@ -247,10 +251,7 @@ pub async fn send_http_request(
.unwrap_or(empty_string)
.as_str()
.unwrap_or_default();
form_params.push((
render::render(name, &workspace, environment_ref),
render::render(value, &workspace, environment_ref),
));
form_params.push((render::render(name, &vars), render::render(value, &vars)));
}
}
request_builder = request_builder.form(&form_params);
@@ -299,13 +300,9 @@ pub async fn send_http_request(
.as_str()
.unwrap_or_default();
let name = render::render(name_raw, &workspace, environment_ref);
let name = render::render(name_raw, &vars);
let mut part = if file_path.is_empty() {
multipart::Part::text(render::render(
value_raw,
&workspace,
environment_ref,
))
multipart::Part::text(render::render(value_raw, &vars))
} else {
match fs::read(file_path) {
Ok(f) => multipart::Part::bytes(f),
@@ -322,7 +319,7 @@ pub async fn send_http_request(
.unwrap_or_default();
if !ct_raw.is_empty() {
let content_type = render::render(ct_raw, &workspace, environment_ref);
let content_type = render::render(ct_raw, &vars);
part = part
.mime_str(content_type.as_str())
.map_err(|e| e.to_string())?;
@@ -392,11 +389,11 @@ pub async fn send_http_request(
response.url = v.url().to_string();
response.remote_addr = v.remote_addr().map(|a| a.to_string());
response.version = match v.version() {
http::Version::HTTP_09 => Some("HTTP/0.9".to_string()),
http::Version::HTTP_10 => Some("HTTP/1.0".to_string()),
http::Version::HTTP_11 => Some("HTTP/1.1".to_string()),
http::Version::HTTP_2 => Some("HTTP/2".to_string()),
http::Version::HTTP_3 => Some("HTTP/3".to_string()),
reqwest::Version::HTTP_09 => Some("HTTP/0.9".to_string()),
reqwest::Version::HTTP_10 => Some("HTTP/1.0".to_string()),
reqwest::Version::HTTP_11 => Some("HTTP/1.1".to_string()),
reqwest::Version::HTTP_2 => Some("HTTP/2".to_string()),
reqwest::Version::HTTP_3 => Some("HTTP/3".to_string()),
_ => None,
};

View File

@@ -6,61 +6,64 @@ extern crate objc;
use std::collections::HashMap;
use std::env::current_dir;
use std::fs;
use std::fs::{create_dir_all, read_to_string, File};
use std::fs::{create_dir_all, File, read_to_string};
use std::path::PathBuf;
use std::process::exit;
use std::str::FromStr;
use std::time::Duration;
use ::http::uri::InvalidUri;
use ::http::Uri;
use base64::Engine;
use fern::colors::ColoredLevelConfig;
use log::{debug, error, info, warn};
use rand::random;
use serde_json::{json, Value};
use sqlx::{Pool, Sqlite, SqlitePool};
use sqlx::migrate::Migrator;
use sqlx::sqlite::SqliteConnectOptions;
use sqlx::types::Json;
use sqlx::{Pool, Sqlite, SqlitePool};
use tauri::{AppHandle, LogicalSize, RunEvent, State, WebviewUrl, WebviewWindow};
use tauri::{Manager, WindowEvent};
use tauri::path::BaseDirectory;
#[cfg(target_os = "macos")]
use tauri::TitleBarStyle;
use tauri::{AppHandle, LogicalSize, RunEvent, State, WebviewUrl, WebviewWindow};
use tauri::{Manager, WindowEvent};
use tauri_plugin_log::{fern, Target, TargetKind};
use tauri_plugin_shell::ShellExt;
use tokio::sync::Mutex;
use ::grpc::{Code, deserialize_message, serialize_message, ServiceDefinition};
use ::grpc::manager::{DynamicMessage, GrpcHandle};
use ::grpc::{deserialize_message, serialize_message, Code, ServiceDefinition};
use crate::analytics::{AnalyticsAction, AnalyticsResource};
use crate::grpc::metadata_to_map;
use crate::http_request::send_http_request;
use crate::models::{
cancel_pending_grpc_connections, cancel_pending_responses, create_http_response,
delete_all_grpc_connections, delete_all_http_responses, delete_cookie_jar, delete_environment,
delete_folder, delete_grpc_connection, delete_grpc_request, delete_http_request,
delete_http_response, delete_workspace, duplicate_grpc_request, duplicate_http_request,
generate_model_id, get_cookie_jar, get_environment, get_folder, get_grpc_connection,
cancel_pending_grpc_connections, cancel_pending_responses, CookieJar,
create_http_response, delete_all_grpc_connections, delete_all_http_responses, delete_cookie_jar,
delete_environment, delete_folder, delete_grpc_connection, delete_grpc_request,
delete_http_request, delete_http_response, delete_workspace, duplicate_grpc_request,
duplicate_http_request, Environment, EnvironmentVariable, Folder, generate_model_id,
get_cookie_jar, get_environment, get_folder, get_grpc_connection,
get_grpc_request, get_http_request, get_http_response, get_key_value_raw,
get_or_create_settings, get_workspace, get_workspace_export_resources, list_cookie_jars,
list_environments, list_folders, list_grpc_connections, list_grpc_events, list_grpc_requests,
list_http_requests, list_responses, list_workspaces, set_key_value_raw, update_response_if_id,
update_settings, upsert_cookie_jar, upsert_environment, upsert_folder, upsert_grpc_connection,
upsert_grpc_event, upsert_grpc_request, upsert_http_request, upsert_workspace, CookieJar,
Environment, EnvironmentVariable, Folder, GrpcConnection, GrpcEvent, GrpcEventType,
GrpcRequest, HttpRequest, HttpResponse, KeyValue, ModelType, Settings, Workspace,
get_or_create_settings, get_workspace, get_workspace_export_resources, GrpcConnection, GrpcEvent,
GrpcEventType, GrpcRequest, HttpRequest, HttpResponse, KeyValue,
list_cookie_jars, list_environments, list_folders, list_grpc_connections, list_grpc_events,
list_grpc_requests, list_http_requests, list_responses, list_workspaces, ModelType,
set_key_value_raw, Settings, update_response_if_id, update_settings, upsert_cookie_jar, upsert_environment,
upsert_folder, upsert_grpc_connection, upsert_grpc_event, upsert_grpc_request, upsert_http_request, upsert_workspace, Workspace,
WorkspaceExportResources,
};
use crate::notifications::YaakNotifier;
use crate::plugin::{run_plugin_export_curl, run_plugin_import, ImportResult};
use crate::render::render_request;
use crate::plugin::{
find_plugins, get_plugin, ImportResult, PluginCapability,
run_plugin_export_curl, run_plugin_filter, run_plugin_import,
};
use crate::render::{render_request, variables_from_environment};
use crate::updates::{UpdateMode, YaakUpdater};
use crate::window_menu::app_menu;
mod analytics;
mod deno;
mod deno_ops;
mod grpc;
mod http_request;
mod models;
@@ -118,7 +121,6 @@ async fn cmd_dismiss_notification(
notification_id: &str,
yaak_notifier: State<'_, Mutex<YaakNotifier>>,
) -> Result<(), String> {
info!("SEEN? {notification_id}");
yaak_notifier.lock().await.seen(&app, notification_id).await
}
@@ -132,14 +134,14 @@ async fn cmd_grpc_reflect(
let req = get_grpc_request(&window, request_id)
.await
.map_err(|e| e.to_string())?;
let uri = safe_uri(&req.url).map_err(|e| e.to_string())?;
let uri = safe_uri(req.url.as_str());
if proto_files.len() > 0 {
grpc_handle
.lock()
.await
.services_from_files(
&req.id,
&uri,
uri.as_str(),
proto_files
.iter()
.map(|p| PathBuf::from_str(p).unwrap())
@@ -150,7 +152,7 @@ async fn cmd_grpc_reflect(
grpc_handle
.lock()
.await
.services_from_reflection(&req.id, &uri)
.services_from_reflection(&req.id, uri.as_str())
.await
}
}
@@ -174,6 +176,7 @@ async fn cmd_grpc_go(
.await
.map_err(|e| e.to_string())?;
let mut metadata = HashMap::new();
let vars = variables_from_environment(&workspace, environment.as_ref());
// Add rest of metadata
for h in req.clone().metadata.0 {
@@ -185,15 +188,14 @@ async fn cmd_grpc_go(
continue;
}
let name = render::render(&h.name, &workspace, environment.as_ref());
let value = render::render(&h.value, &workspace, environment.as_ref());
let name = render::render(&h.name, &vars);
let value = render::render(&h.value, &vars);
metadata.insert(name, value);
}
if let Some(b) = &req.authentication_type {
let req = req.clone();
let environment_ref = environment.as_ref();
let empty_value = &serde_json::to_value("").unwrap();
let a = req.authentication.0;
@@ -208,15 +210,15 @@ async fn cmd_grpc_go(
.unwrap_or(empty_value)
.as_str()
.unwrap_or("");
let username = render::render(raw_username, &workspace, environment_ref);
let password = render::render(raw_password, &workspace, environment_ref);
let username = render::render(raw_username, &vars);
let password = render::render(raw_password, &vars);
let auth = format!("{username}:{password}");
let encoded = base64::engine::general_purpose::STANDARD_NO_PAD.encode(auth);
metadata.insert("Authorization".to_string(), format!("Basic {}", encoded));
} else if b == "bearer" {
let raw_token = a.get("token").unwrap_or(empty_value).as_str().unwrap_or("");
let token = render::render(raw_token, &workspace, environment_ref);
let token = render::render(raw_token, &vars);
metadata.insert("Authorization".to_string(), format!("Bearer {token}"));
}
}
@@ -249,7 +251,7 @@ async fn cmd_grpc_go(
let maybe_in_msg_tx = std::sync::Mutex::new(Some(in_msg_tx.clone()));
let (cancelled_tx, mut cancelled_rx) = tokio::sync::watch::channel(false);
let uri = safe_uri(&req.url).map_err(|e| e.to_string())?;
let uri = safe_uri(&req.url);
let in_msg_stream = tokio_stream::wrappers::ReceiverStream::new(in_msg_rx);
@@ -267,7 +269,7 @@ async fn cmd_grpc_go(
.await
.connect(
&req.clone().id,
uri,
uri.as_str(),
proto_files
.iter()
.map(|p| PathBuf::from_str(p).unwrap())
@@ -288,11 +290,10 @@ async fn cmd_grpc_go(
let cb = {
let cancelled_rx = cancelled_rx.clone();
let environment = environment.clone();
let workspace = workspace.clone();
let w = w.clone();
let base_msg = base_msg.clone();
let method_desc = method_desc.clone();
let vars = vars.clone();
move |ev: tauri::Event| {
if *cancelled_rx.borrow() {
@@ -315,9 +316,8 @@ async fn cmd_grpc_go(
Ok(IncomingMsg::Message(raw_msg)) => {
let w = w.clone();
let base_msg = base_msg.clone();
let environment_ref = environment.as_ref();
let method_desc = method_desc.clone();
let msg = render::render(raw_msg.as_str(), &workspace, environment_ref);
let msg = render::render(raw_msg.as_str(), &vars);
let d_msg: DynamicMessage = match deserialize_message(msg.as_str(), method_desc)
{
Ok(d_msg) => d_msg,
@@ -369,14 +369,13 @@ async fn cmd_grpc_go(
let w = w.clone();
let base_event = base_msg.clone();
let req = req.clone();
let workspace = workspace.clone();
let environment = environment.clone();
let vars = vars.clone();
let raw_msg = if req.message.is_empty() {
"{}".to_string()
} else {
req.message
};
let msg = render::render(&raw_msg, &workspace, environment.as_ref());
let msg = render::render(&raw_msg, &vars);
upsert_grpc_event(
&w,
@@ -736,7 +735,11 @@ async fn cmd_filter_response(
};
let body = read_to_string(response.body_path.unwrap()).unwrap();
let filter_result = plugin::run_plugin_filter(&w.app_handle(), plugin_name, filter, &body)
let plugin = match get_plugin(&w.app_handle(), plugin_name).map_err(|e| e.to_string())? {
None => return Err("Failed to get plugin".into()),
Some(p) => p,
};
let filter_result = run_plugin_filter(&plugin, filter, &body)
.await
.expect("Failed to run filter");
Ok(filter_result.filtered)
@@ -749,26 +752,23 @@ async fn cmd_import_data(
_workspace_id: &str,
) -> Result<WorkspaceExportResources, String> {
let mut result: Option<ImportResult> = None;
let plugins = vec![
"importer-postman",
"importer-insomnia",
"importer-yaak",
"importer-curl",
];
let file = read_to_string(file_path)
.unwrap_or_else(|_| panic!("Unable to read file {}", file_path));
let file =
read_to_string(file_path).unwrap_or_else(|_| panic!("Unable to read file {}", file_path));
let file_contents = file.as_str();
for plugin_name in plugins {
let v = run_plugin_import(&w.app_handle(), plugin_name, file_contents)
let plugins = find_plugins(w.app_handle(), &PluginCapability::Import)
.await
.map_err(|e| e.to_string())?;
for plugin in plugins {
let v = run_plugin_import(&plugin, file_contents)
.await
.map_err(|e| e.to_string())?;
if let Some(r) = v {
info!("Imported data using {}", plugin_name);
info!("Imported data using {}", plugin.name);
analytics::track_event(
&w.app_handle(),
AnalyticsResource::App,
AnalyticsAction::Import,
Some(json!({ "plugin": plugin_name })),
Some(json!({ "plugin": plugin.name })),
)
.await;
result = Some(r);
@@ -808,13 +808,15 @@ async fn cmd_import_data(
}
};
info!("Importing resources");
for mut v in r.resources.workspaces {
v.id = maybe_gen_id(v.id.as_str(), ModelType::TypeWorkspace, &mut id_map);
let x = upsert_workspace(&w, v).await.map_err(|e| e.to_string())?;
imported_resources.workspaces.push(x.clone());
info!("Imported workspace: {}", x.name);
}
info!(
"Imported {} workspaces",
imported_resources.workspaces.len()
);
for mut v in r.resources.environments {
v.id = maybe_gen_id(v.id.as_str(), ModelType::TypeEnvironment, &mut id_map);
@@ -825,8 +827,11 @@ async fn cmd_import_data(
);
let x = upsert_environment(&w, v).await.map_err(|e| e.to_string())?;
imported_resources.environments.push(x.clone());
info!("Imported environment: {}", x.name);
}
info!(
"Imported {} environments",
imported_resources.environments.len()
);
for mut v in r.resources.folders {
v.id = maybe_gen_id(v.id.as_str(), ModelType::TypeFolder, &mut id_map);
@@ -838,8 +843,8 @@ async fn cmd_import_data(
v.folder_id = maybe_gen_id_opt(v.folder_id, ModelType::TypeFolder, &mut id_map);
let x = upsert_folder(&w, v).await.map_err(|e| e.to_string())?;
imported_resources.folders.push(x.clone());
info!("Imported folder: {}", x.name);
}
info!("Imported {} folders", imported_resources.folders.len());
for mut v in r.resources.http_requests {
v.id = maybe_gen_id(v.id.as_str(), ModelType::TypeHttpRequest, &mut id_map);
@@ -853,8 +858,11 @@ async fn cmd_import_data(
.await
.map_err(|e| e.to_string())?;
imported_resources.http_requests.push(x.clone());
info!("Imported request: {}", x.name);
}
info!(
"Imported {} http_requests",
imported_resources.http_requests.len()
);
for mut v in r.resources.grpc_requests {
v.id = maybe_gen_id(v.id.as_str(), ModelType::TypeGrpcRequest, &mut id_map);
@@ -868,8 +876,11 @@ async fn cmd_import_data(
.await
.map_err(|e| e.to_string())?;
imported_resources.grpc_requests.push(x.clone());
info!("Imported request: {}", x.name);
}
info!(
"Imported {} grpc_requests",
imported_resources.grpc_requests.len()
);
Ok(imported_resources)
}
@@ -898,13 +909,16 @@ async fn cmd_request_to_curl(
#[tauri::command]
async fn cmd_curl_to_request(
app: AppHandle,
app_handle: AppHandle,
command: &str,
workspace_id: &str,
) -> Result<HttpRequest, String> {
let v = run_plugin_import(&app, "importer-curl", command)
.await
.map_err(|e| e.to_string());
let plugin = match get_plugin(&app_handle, "importer-curl").map_err(|e| e.to_string())? {
None => return Err("Failed to find plugin".into()),
Some(p) => p,
};
let v = run_plugin_import(&plugin, command).await;
match v {
Ok(Some(r)) => r
.resources
@@ -952,6 +966,28 @@ async fn cmd_export_data(
Ok(())
}
#[tauri::command]
async fn cmd_save_response(
window: WebviewWindow,
response_id: &str,
filepath: &str,
) -> Result<(), String> {
let response = get_http_response(&window, response_id)
.await
.map_err(|e| e.to_string())?;
let body_path = match response.body_path {
None => {
return Err("Response does not have a body".to_string());
}
Some(p) => p,
};
fs::copy(body_path, filepath).map_err(|e| e.to_string())?;
Ok(())
}
#[tauri::command]
async fn cmd_send_http_request(
window: WebviewWindow,
@@ -1032,6 +1068,7 @@ async fn response_err(
error: String,
w: &WebviewWindow,
) -> Result<HttpResponse, String> {
warn!("Failed to send request: {}", error);
let mut response = response.clone();
response.elapsed = -1;
response.error = Some(error.clone());
@@ -1581,7 +1618,9 @@ pub fn run() {
.level_for("tokio_util", log::LevelFilter::Info)
.level_for("tonic", log::LevelFilter::Info)
.level_for("tower", log::LevelFilter::Info)
.level_for("tracing", log::LevelFilter::Info)
.level_for("tracing", log::LevelFilter::Warn)
.level_for("swc_ecma_codegen", log::LevelFilter::Off)
.level_for("swc_ecma_transforms_base", log::LevelFilter::Off)
.with_colors(ColoredLevelConfig::default())
.level(if is_dev() {
log::LevelFilter::Trace
@@ -1693,6 +1732,7 @@ pub fn run() {
cmd_new_window,
cmd_request_to_curl,
cmd_dismiss_notification,
cmd_save_response,
cmd_send_ephemeral_request,
cmd_send_http_request,
cmd_set_key_value,
@@ -1894,11 +1934,10 @@ async fn get_update_mode(h: &AppHandle) -> UpdateMode {
UpdateMode::new(settings.update_channel.as_str())
}
fn safe_uri(endpoint: &str) -> Result<Uri, InvalidUri> {
let uri = if endpoint.starts_with("http://") || endpoint.starts_with("https://") {
Uri::from_str(endpoint)?
fn safe_uri(endpoint: &str) -> String {
if endpoint.starts_with("http://") || endpoint.starts_with("https://") {
endpoint.into()
} else {
Uri::from_str(&format!("http://{}", endpoint))?
};
Ok(uri)
format!("http://{}", endpoint)
}
}

View File

@@ -59,6 +59,7 @@ pub struct Settings {
pub interface_scale: i64,
pub editor_font_size: i64,
pub editor_soft_wrap: bool,
pub open_workspace_new_window: Option<bool>,
}
#[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Default)]
@@ -891,7 +892,8 @@ async fn get_settings(mgr: &impl Manager<Wry>) -> Result<Settings, sqlx::Error>
SELECT
id, model, created_at, updated_at, theme, appearance,
theme_dark, theme_light, update_channel,
interface_font_size, interface_scale, editor_font_size, editor_soft_wrap
interface_font_size, interface_scale, editor_font_size, editor_soft_wrap,
open_workspace_new_window
FROM settings
WHERE id = 'default'
"#,
@@ -928,8 +930,9 @@ pub async fn update_settings(
r#"
UPDATE settings SET (
theme, appearance, theme_dark, theme_light, update_channel,
interface_font_size, interface_scale, editor_font_size, editor_soft_wrap
) = (?, ?, ?, ?, ?, ?, ?, ?, ?) WHERE id = 'default';
interface_font_size, interface_scale, editor_font_size, editor_soft_wrap,
open_workspace_new_window
) = (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) WHERE id = 'default';
"#,
settings.theme,
settings.appearance,
@@ -940,6 +943,7 @@ pub async fn update_settings(
settings.interface_scale,
settings.editor_font_size,
settings.editor_soft_wrap,
settings.open_workspace_new_window,
)
.execute(&db)
.await?;

View File

@@ -1,8 +1,8 @@
use std::time::SystemTime;
use chrono::{Duration, NaiveDateTime, Utc};
use http::Method;
use chrono::{DateTime, Duration, Utc};
use log::debug;
use reqwest::Method;
use serde::{Deserialize, Serialize};
use tauri::{AppHandle, Manager};
@@ -23,7 +23,7 @@ pub struct YaakNotifier {
#[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Default)]
#[serde(default, rename_all = "camelCase")]
pub struct YaakNotification {
timestamp: NaiveDateTime,
timestamp: DateTime<Utc>,
id: String,
message: String,
action: Option<YaakNotificationAction>,
@@ -77,7 +77,7 @@ impl YaakNotifier {
let age = notification
.timestamp
.signed_duration_since(Utc::now().naive_utc());
.signed_duration_since(Utc::now());
let seen = get_kv(app).await?;
if seen.contains(&notification.id) || (age > Duration::days(1)) {
debug!("Already seen notification {}", notification.id);

View File

@@ -0,0 +1,7 @@
((globalThis) => {
const core = Deno.core;
globalThis.YAML = {
parse: core.ops.op_yaml_parse,
stringify: core.ops.op_yaml_stringify,
};
})(globalThis);

View File

@@ -1,19 +1,24 @@
use std::rc::Rc;
use std::{fs, io};
use boa_engine::builtins::promise::PromiseState;
use boa_engine::{
js_string, module::SimpleModuleLoader, property::Attribute, Context, JsNativeError, JsValue,
Module, Source,
};
use boa_runtime::Console;
use log::{debug, error};
use log::error;
use serde::{Deserialize, Serialize};
use serde_json::json;
use tauri::path::BaseDirectory;
use tauri::{AppHandle, Manager};
use thiserror::Error;
use crate::deno::{get_plugin_capabilities_block, run_plugin_block};
use crate::models::{HttpRequest, WorkspaceExportResources};
#[derive(Error, Debug)]
pub enum PluginError {
#[error("directory not found")]
DirectoryNotFound(#[from] io::Error),
#[error("anyhow error")]
V8(#[from] anyhow::Error),
// #[error("unknown data store error")]
// Unknown,
}
#[derive(Default, Debug, Deserialize, Serialize)]
pub struct FilterResult {
pub filtered: String,
@@ -24,26 +29,87 @@ pub struct ImportResult {
pub resources: WorkspaceExportResources,
}
pub async fn run_plugin_filter(
#[derive(Eq, PartialEq, Hash, Clone)]
pub enum PluginCapability {
Export,
Import,
Filter,
}
pub struct PluginDef {
pub name: String,
pub path: String,
pub capabilities: Vec<PluginCapability>,
}
pub fn scan_plugins(app_handle: &AppHandle) -> Result<Vec<PluginDef>, PluginError> {
let plugins_dir = app_handle
.path()
.resolve("plugins", BaseDirectory::Resource)
.expect("failed to resolve plugin directory resource");
let plugin_entries = fs::read_dir(plugins_dir)?;
let mut plugins = Vec::new();
for entry in plugin_entries {
let plugin_dir_entry = match entry {
Err(_) => continue,
Ok(entry) => entry,
};
let plugin_index_file = plugin_dir_entry.path().join("index.mjs");
let capabilities = get_plugin_capabilities_block(&plugin_index_file.to_str().unwrap())?;
plugins.push(PluginDef {
name: plugin_dir_entry.file_name().to_string_lossy().to_string(),
path: plugin_index_file.to_string_lossy().to_string(),
capabilities,
});
}
Ok(plugins)
}
pub async fn find_plugins(
app_handle: &AppHandle,
plugin_name: &str,
capability: &PluginCapability,
) -> Result<Vec<PluginDef>, PluginError> {
let plugins = scan_plugins(app_handle)?
.into_iter()
.filter(|p| p.capabilities.contains(capability))
.collect();
Ok(plugins)
}
pub fn get_plugin(app_handle: &AppHandle, name: &str) -> Result<Option<PluginDef>, PluginError> {
Ok(scan_plugins(app_handle)?
.into_iter()
.find(|p| p.name == name))
}
pub async fn run_plugin_filter(
plugin: &PluginDef,
response_body: &str,
filter: &str,
) -> Option<FilterResult> {
let result_json = run_plugin(
app_handle,
plugin_name,
let result = run_plugin_block(
&plugin.path,
"pluginHookResponseFilter",
&[js_string!(response_body).into(), js_string!(filter).into()],
);
vec![
serde_json::to_value(response_body).unwrap(),
serde_json::to_value(filter).unwrap(),
],
)
.map_err(|e| e.to_string())
.expect("Failed to run plugin");
if result_json.is_null() {
error!("Plugin {} failed to run", plugin_name);
if result.is_null() {
error!("Plugin {} failed to run", plugin.name);
return None;
}
let resources: FilterResult =
serde_json::from_value(result_json).expect("failed to parse filter plugin result json");
serde_json::from_value(result).expect("failed to parse filter plugin result json");
Some(resources)
}
@@ -51,114 +117,34 @@ pub fn run_plugin_export_curl(
app_handle: &AppHandle,
request: &HttpRequest,
) -> Result<String, String> {
let mut context = Context::default();
let request_json = serde_json::to_value(request).map_err(|e| e.to_string())?;
let result_json = run_plugin(
app_handle,
"exporter-curl",
"pluginHookExport",
&[JsValue::from_json(&request_json, &mut context).map_err(|e| e.to_string())?],
);
let plugin = match get_plugin(app_handle, "exporter-curl").map_err(|e| e.to_string())? {
None => return Err("Failed to get plugin".into()),
Some(p) => p,
};
let resources: String = serde_json::from_value(result_json).map_err(|e| e.to_string())?;
Ok(resources)
let request_json = serde_json::to_value(request).map_err(|e| e.to_string())?;
let result = run_plugin_block(&plugin.path, "pluginHookExport", vec![request_json])
.map_err(|e| e.to_string())?;
let export_str: String = serde_json::from_value(result).map_err(|e| e.to_string())?;
Ok(export_str)
}
pub async fn run_plugin_import(
app_handle: &AppHandle,
plugin_name: &str,
plugin: &PluginDef,
file_contents: &str,
) -> Result<Option<ImportResult>, String> {
let result_json = run_plugin(
app_handle,
plugin_name,
let result = run_plugin_block(
&plugin.path,
"pluginHookImport",
&[js_string!(file_contents).into()],
);
vec![serde_json::to_value(file_contents).map_err(|e| e.to_string())?],
)
.map_err(|e| e.to_string())?;
if result_json.is_null() {
if result.is_null() {
return Ok(None);
}
let resources: ImportResult = serde_json::from_value(result_json).map_err(|e| e.to_string())?;
let resources: ImportResult = serde_json::from_value(result).map_err(|e| e.to_string())?;
Ok(Some(resources))
}
fn run_plugin(
app_handle: &AppHandle,
plugin_name: &str,
entrypoint: &str,
js_args: &[JsValue],
) -> serde_json::Value {
let plugin_dir = app_handle
.path()
.resolve("plugins", BaseDirectory::Resource)
.expect("failed to resolve plugin directory resource")
.join(plugin_name);
let plugin_index_file = plugin_dir.join("index.mjs");
debug!(
"Running plugin dir={:?} file={:?}",
plugin_dir, plugin_index_file
);
let loader = Rc::new(SimpleModuleLoader::new(plugin_dir).unwrap());
let context = &mut Context::builder()
.module_loader(loader.clone())
.build()
.expect("failed to create context");
add_runtime(context);
let source = Source::from_filepath(&plugin_index_file).expect("Error opening file");
// Can also pass a `Some(realm)` if you need to execute the module in another realm.
let module = Module::parse(source, None, context).expect("failed to parse module");
// Insert parsed entrypoint into the module loader
loader.insert(plugin_index_file, module.clone());
let promise_result = module.load_link_evaluate(context);
// Very important to push forward the job queue after queueing promises.
context.run_jobs();
// Checking if the final promise didn't return an error.
match promise_result.state() {
PromiseState::Pending => {
panic!("Promise was pending");
}
PromiseState::Fulfilled(v) => {
assert_eq!(v, JsValue::undefined())
}
PromiseState::Rejected(err) => {
panic!("Failed to link: {}", err.display());
}
}
let namespace = module.namespace(context);
let result = namespace
.get(js_string!(entrypoint), context)
.expect("failed to get entrypoint")
.as_callable()
.cloned()
.ok_or_else(|| JsNativeError::typ().with_message("export wasn't a function!"))
.expect("Failed to get entrypoint")
.call(&JsValue::undefined(), js_args, context)
.expect("Failed to call entrypoint");
match result.is_undefined() {
true => json!(null), // to_json doesn't work with undefined (yet)
false => result
.to_json(context)
.expect("failed to convert result to json"),
}
}
fn add_runtime(context: &mut Context) {
let console = Console::init(context);
context
.register_global_property(js_string!(Console::NAME), console, Attribute::all())
.expect("the console builtin shouldn't exist");
}

View File

@@ -1,22 +1,26 @@
use std::collections::HashMap;
use regex::Regex;
use sqlx::types::{Json, JsonValue};
use crate::models::{Environment, HttpRequest, HttpRequestHeader, HttpUrlParameter, Workspace};
use crate::models::{
Environment, EnvironmentVariable, HttpRequest, HttpRequestHeader, HttpUrlParameter, Workspace,
};
use templates::parse_and_render;
pub fn render_request(r: &HttpRequest, w: &Workspace, e: Option<&Environment>) -> HttpRequest {
let r = r.clone();
let vars = &variables_from_environment(w, e);
HttpRequest {
url: render(r.url.as_str(), w, e),
url: render(r.url.as_str(), vars),
url_parameters: Json(
r.url_parameters
.0
.iter()
.map(|p| HttpUrlParameter {
enabled: p.enabled,
name: render(p.name.as_str(), w, e),
value: render(p.value.as_str(), w, e),
name: render(p.name.as_str(), vars),
value: render(p.value.as_str(), vars),
})
.collect::<Vec<HttpUrlParameter>>(),
),
@@ -26,8 +30,8 @@ pub fn render_request(r: &HttpRequest, w: &Workspace, e: Option<&Environment>) -
.iter()
.map(|p| HttpRequestHeader {
enabled: p.enabled,
name: render(p.name.as_str(), w, e),
value: render(p.value.as_str(), w, e),
name: render(p.name.as_str(), vars),
value: render(p.value.as_str(), vars),
})
.collect::<Vec<HttpRequestHeader>>(),
),
@@ -37,11 +41,11 @@ pub fn render_request(r: &HttpRequest, w: &Workspace, e: Option<&Environment>) -
.iter()
.map(|(k, v)| {
let v = if v.is_string() {
render(v.as_str().unwrap(), w, e)
render(v.as_str().unwrap(), vars)
} else {
v.to_string()
};
(render(k, w, e), JsonValue::from(v))
(render(k, vars), JsonValue::from(v))
})
.collect::<HashMap<String, JsonValue>>(),
),
@@ -51,11 +55,11 @@ pub fn render_request(r: &HttpRequest, w: &Workspace, e: Option<&Environment>) -
.iter()
.map(|(k, v)| {
let v = if v.is_string() {
render(v.as_str().unwrap(), w, e)
render(v.as_str().unwrap(), vars)
} else {
v.to_string()
};
(render(k, w, e), JsonValue::from(v))
(render(k, vars), JsonValue::from(v))
})
.collect::<HashMap<String, JsonValue>>(),
),
@@ -63,31 +67,58 @@ pub fn render_request(r: &HttpRequest, w: &Workspace, e: Option<&Environment>) -
}
}
pub fn render(template: &str, workspace: &Workspace, environment: Option<&Environment>) -> String {
let mut map = HashMap::new();
let workspace_variables = &workspace.variables.0;
for variable in workspace_variables {
pub fn recursively_render_variables<'s>(
m: &HashMap<String, String>,
render_count: usize,
) -> HashMap<String, String> {
let mut did_render = false;
let mut new_map = m.clone();
for (k, v) in m.clone() {
let rendered = render(v.as_str(), m);
if rendered != v {
did_render = true
}
new_map.insert(k, rendered);
}
if did_render && render_count <= 3 {
new_map = recursively_render_variables(&new_map, render_count + 1);
}
new_map
}
pub fn variables_from_environment(
workspace: &Workspace,
environment: Option<&Environment>,
) -> HashMap<String, String> {
let mut variables = HashMap::new();
variables = add_variable_to_map(variables, &workspace.variables.0);
if let Some(e) = environment {
variables = add_variable_to_map(variables, &e.variables.0);
}
recursively_render_variables(&variables, 0)
}
pub fn render(template: &str, vars: &HashMap<String, String>) -> String {
parse_and_render(template, vars, None)
}
fn add_variable_to_map(
m: HashMap<String, String>,
variables: &Vec<EnvironmentVariable>,
) -> HashMap<String, String> {
let mut map = m.clone();
for variable in variables {
if !variable.enabled || variable.value.is_empty() {
continue;
}
map.insert(variable.name.as_str(), variable.value.as_str());
let name = variable.name.as_str();
let value = variable.value.as_str();
map.insert(name.into(), value.into());
}
if let Some(e) = environment {
let environment_variables = &e.variables.0;
for variable in environment_variables {
if !variable.enabled || variable.value.is_empty() {
continue;
}
map.insert(variable.name.as_str(), variable.value.as_str());
}
}
Regex::new(r"\$\{\[\s*([^]\s]+)\s*]}")
.expect("Failed to create regex")
.replace_all(template, |caps: &regex::Captures| {
let key = caps.get(1).unwrap().as_str();
map.get(key).unwrap_or(&"")
})
.to_string()
map
}

View File

@@ -1,4 +1,5 @@
use hex_color::HexColor;
use log::warn;
use objc::{msg_send, sel, sel_impl};
use rand::{distributions::Alphanumeric, Rng};
use tauri::{
@@ -25,15 +26,35 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
let window_for_theme = window.clone();
let id1 = h.listen("yaak_bg_changed", move |ev| {
let payload = serde_json::from_str::<&str>(ev.payload()).unwrap().trim();
let color = HexColor::parse_rgb(payload).unwrap();
update_window_theme(window_for_theme.clone(), color);
let color_str: String = match serde_json::from_str(ev.payload()) {
Ok(color) => color,
Err(err) => {
warn!("Failed to JSON parse color '{}': {}", ev.payload(), err);
return;
}
};
match HexColor::parse_rgb(color_str.trim()) {
Ok(color) => {
update_window_theme(window_for_theme.clone(), color);
}
Err(err) => {
warn!("Failed to parse background color '{}': {}", color_str, err)
}
}
});
let window_for_title = window.clone();
let id2 = h.listen("yaak_title_changed", move |ev| {
let payload = serde_json::from_str::<&str>(ev.payload()).unwrap().trim();
update_window_title(window_for_title.clone(), payload.to_string());
let title: String = match serde_json::from_str(ev.payload()) {
Ok(title) => title,
Err(err) => {
warn!("Failed to parse window title \"{}\": {}", ev.payload(), err);
return;
}
};
update_window_title(window_for_title.clone(), title);
});
let h = h.clone();

View File

@@ -134,8 +134,11 @@ pub fn app_menu(app_handle: &AppHandle) -> tauri::Result<Menu<Wry>> {
.build(app_handle)?,
&MenuItemBuilder::with_id("dev.reset_size".to_string(), "Reset Size")
.build(app_handle)?,
&MenuItemBuilder::with_id("dev.generate_theme_css".to_string(), "Generate Theme CSS")
.build(app_handle)?,
&MenuItemBuilder::with_id(
"dev.generate_theme_css".to_string(),
"Generate Theme CSS",
)
.build(app_handle)?,
],
)?,
],

View File

@@ -1,6 +1,6 @@
{
"productName": "yaak",
"version": "2024.5.2",
"version": "2024.6.5",
"identifier": "app.yaak.desktop",
"build": {
"beforeBuildCommand": "npm run build",

View File

@@ -0,0 +1,6 @@
[package]
name = "templates"
version = "0.1.0"
edition = "2021"
[dependencies]

View File

@@ -0,0 +1,7 @@
pub mod parser;
pub mod renderer;
pub use parser::*;
pub use renderer::*;
pub fn template_foo() {}

View File

@@ -0,0 +1,413 @@
#[derive(Clone, PartialEq, Debug)]
pub enum Val {
Str(String),
Var(String),
Fn { name: String, args: Vec<Val> },
}
#[derive(Clone, PartialEq, Debug)]
pub enum Token {
Raw(String),
Tag(Val),
Eof,
}
// Template Syntax
//
// ${[ my_var ]}
// ${[ my_fn() ]}
// ${[ my_fn(my_var) ]}
// ${[ my_fn(my_var, "A String") ]}
// default
#[derive(Default)]
pub struct Parser {
tokens: Vec<Token>,
chars: Vec<char>,
pos: usize,
curr_text: String,
}
impl Parser {
pub fn new(text: &str) -> Parser {
Parser {
chars: text.chars().collect(),
..Parser::default()
}
}
pub fn parse(&mut self) -> Vec<Token> {
let start_pos = self.pos;
while self.pos < self.chars.len() {
if self.match_str("${[") {
let start_curr = self.pos;
if let Some(t) = self.parse_tag() {
self.push_token(t);
} else {
self.pos = start_curr;
self.curr_text += "${[";
}
} else {
let ch = self.next_char();
self.curr_text.push(ch);
}
if start_pos == self.pos {
panic!("Parser stuck!");
}
}
self.push_token(Token::Eof);
self.tokens.clone()
}
fn parse_tag(&mut self) -> Option<Token> {
// Parse up to first identifier
// ${[ my_var...
self.skip_whitespace();
let val = match self.parse_value() {
Some(v) => v,
None => return None,
};
// Parse to closing tag
// ${[ my_var(a, b, c) ]}
self.skip_whitespace();
if !self.match_str("]}") {
return None;
}
Some(Token::Tag(val))
}
#[allow(dead_code)]
fn debug_pos(&self, x: &str) {
println!(
r#"Position: {x} -- [{}] = {} --> "{}" --> {:?}"#,
self.pos,
self.chars[self.pos],
self.chars.iter().collect::<String>(),
self.tokens,
);
}
fn parse_value(&mut self) -> Option<Val> {
if let Some((name, args)) = self.parse_fn() {
Some(Val::Fn { name, args })
} else if let Some(v) = self.parse_ident() {
Some(Val::Var(v))
} else if let Some(v) = self.parse_string() {
Some(Val::Str(v))
} else {
None
}
}
fn parse_fn(&mut self) -> Option<(String, Vec<Val>)> {
let start_pos = self.pos;
let name = match self.parse_ident() {
Some(v) => v,
None => {
self.pos = start_pos;
return None;
}
};
let args = match self.parse_fn_args() {
Some(args) => args,
None => {
self.pos = start_pos;
return None;
}
};
Some((name, args))
}
fn parse_fn_args(&mut self) -> Option<Vec<Val>> {
if !self.match_str("(") {
return None;
}
let start_pos = self.pos;
let mut args: Vec<Val> = Vec::new();
while self.pos < self.chars.len() {
self.skip_whitespace();
if let Some(v) = self.parse_value() {
args.push(v);
}
self.skip_whitespace();
if self.match_str(")") {
break;
}
self.skip_whitespace();
// If we don't find a comma, that's bad
if !args.is_empty() && !self.match_str(",") {
self.pos = start_pos;
return None;
}
if start_pos == self.pos {
panic!("Parser stuck!");
}
}
return Some(args);
}
fn parse_ident(&mut self) -> Option<String> {
let start_pos = self.pos;
let mut text = String::new();
while self.pos < self.chars.len() {
let ch = self.peek_char();
if ch.is_alphanumeric() || ch == '_' {
text.push(ch);
self.pos += 1;
} else {
break;
}
if start_pos == self.pos {
panic!("Parser stuck!");
}
}
if text.is_empty() {
self.pos = start_pos;
return None;
}
return Some(text);
}
fn parse_string(&mut self) -> Option<String> {
let start_pos = self.pos;
let mut text = String::new();
if !self.match_str("\"") {
return None;
}
let mut found_closing = false;
while self.pos < self.chars.len() {
let ch = self.next_char();
match ch {
'\\' => {
text.push(self.next_char());
}
'"' => {
found_closing = true;
break;
}
_ => {
text.push(ch);
}
}
if start_pos == self.pos {
panic!("Parser stuck!");
}
}
if !found_closing {
self.pos = start_pos;
return None;
}
return Some(text);
}
fn skip_whitespace(&mut self) {
while self.pos < self.chars.len() {
if self.peek_char().is_whitespace() {
self.pos += 1;
} else {
break;
}
}
}
fn next_char(&mut self) -> char {
let ch = self.peek_char();
self.pos += 1;
ch
}
fn peek_char(&self) -> char {
let ch = self.chars[self.pos];
ch
}
fn push_token(&mut self, token: Token) {
// Push any text we've accumulated
if !self.curr_text.is_empty() {
let text_token = Token::Raw(self.curr_text.clone());
self.tokens.push(text_token);
self.curr_text.clear();
}
self.tokens.push(token);
}
fn match_str(&mut self, value: &str) -> bool {
if self.pos + value.len() > self.chars.len() {
return false;
}
let cmp = self.chars[self.pos..self.pos + value.len()]
.iter()
.collect::<String>();
if cmp == value {
// We have a match, so advance the current index
self.pos += value.len();
true
} else {
false
}
}
}
#[cfg(test)]
mod tests {
use crate::*;
#[test]
fn var_simple() {
let mut p = Parser::new("${[ foo ]}");
assert_eq!(
p.parse(),
vec![Token::Tag(Val::Var("foo".into())), Token::Eof]
);
}
#[test]
fn var_multiple_names_invalid() {
let mut p = Parser::new("${[ foo bar ]}");
assert_eq!(
p.parse(),
vec![Token::Raw("${[ foo bar ]}".into()), Token::Eof]
);
}
#[test]
fn tag_string() {
let mut p = Parser::new(r#"${[ "foo \"bar\" baz" ]}"#);
assert_eq!(
p.parse(),
vec![Token::Tag(Val::Str(r#"foo "bar" baz"#.into())), Token::Eof]
);
}
#[test]
fn var_surrounded() {
let mut p = Parser::new("Hello ${[ foo ]}!");
assert_eq!(
p.parse(),
vec![
Token::Raw("Hello ".to_string()),
Token::Tag(Val::Var("foo".into())),
Token::Raw("!".to_string()),
Token::Eof,
]
);
}
#[test]
fn fn_simple() {
let mut p = Parser::new("${[ foo() ]}");
assert_eq!(
p.parse(),
vec![
Token::Tag(Val::Fn {
name: "foo".into(),
args: Vec::new(),
}),
Token::Eof
]
);
}
#[test]
fn fn_ident_arg() {
let mut p = Parser::new("${[ foo(bar) ]}");
assert_eq!(
p.parse(),
vec![
Token::Tag(Val::Fn {
name: "foo".into(),
args: vec![Val::Var("bar".into())],
}),
Token::Eof
]
);
}
#[test]
fn fn_ident_args() {
let mut p = Parser::new("${[ foo(bar,baz, qux ) ]}");
assert_eq!(
p.parse(),
vec![
Token::Tag(Val::Fn {
name: "foo".into(),
args: vec![
Val::Var("bar".into()),
Val::Var("baz".into()),
Val::Var("qux".into()),
],
}),
Token::Eof
]
);
}
#[test]
fn fn_mixed_args() {
let mut p = Parser::new(r#"${[ foo(bar,"baz \"hi\"", qux ) ]}"#);
assert_eq!(
p.parse(),
vec![
Token::Tag(Val::Fn {
name: "foo".into(),
args: vec![
Val::Var("bar".into()),
Val::Str(r#"baz "hi""#.into()),
Val::Var("qux".into()),
],
}),
Token::Eof
]
);
}
#[test]
fn fn_nested() {
let mut p = Parser::new(r#"${[ outer(inner(foo, "i"), "o") ]}"#);
assert_eq!(
p.parse(),
vec![
Token::Tag(Val::Fn {
name: "outer".into(),
args: vec![
Val::Fn {
name: "inner".into(),
args: vec![Val::Var("foo".into()), Val::Str("i".into()),],
},
Val::Str("o".into())
],
}),
Token::Eof
]
);
}
}

View File

@@ -0,0 +1,132 @@
use std::collections::HashMap;
use crate::{Parser, Token, Val};
type TemplateCallback = fn(name: &str, args: Vec<String>) -> String;
pub fn parse_and_render(
template: &str,
vars: &HashMap<String, String>,
cb: Option<TemplateCallback>,
) -> String {
let mut p = Parser::new(template);
let tokens = p.parse();
render(tokens, vars, cb)
}
pub fn render(
tokens: Vec<Token>,
vars: &HashMap<String, String>,
cb: Option<TemplateCallback>,
) -> String {
let mut doc_str: Vec<String> = Vec::new();
for t in tokens {
match t {
Token::Raw(s) => doc_str.push(s),
Token::Tag(val) => doc_str.push(render_tag(val, &vars, cb)),
Token::Eof => {}
}
}
return doc_str.join("");
}
fn render_tag(
val: Val,
vars: &HashMap<String, String>,
cb: Option<TemplateCallback>,
) -> String {
match val {
Val::Str(s) => s.into(),
Val::Var(name) => match vars.get(name.as_str()) {
Some(v) => v.to_string(),
None => "".into(),
},
Val::Fn { name, args } => {
let empty = "".to_string();
let resolved_args = args
.iter()
.map(|a| match a {
Val::Str(s) => s.to_string(),
Val::Var(i) => vars.get(i.as_str()).unwrap_or(&empty).to_string(),
val => render_tag(val.clone(), vars, cb),
})
.collect::<Vec<String>>();
match cb {
Some(cb) => cb(name.as_str(), resolved_args),
None => "".into(),
}
}
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use crate::*;
#[test]
fn render_empty() {
let template = "";
let vars = HashMap::new();
let result = "";
assert_eq!(parse_and_render(template, &vars, None), result.to_string());
}
#[test]
fn render_text_only() {
let template = "Hello World!";
let vars = HashMap::new();
let result = "Hello World!";
assert_eq!(parse_and_render(template, &vars, None), result.to_string());
}
#[test]
fn render_simple() {
let template = "${[ foo ]}";
let vars = HashMap::from([("foo".to_string(), "bar".to_string())]);
let result = "bar";
assert_eq!(parse_and_render(template, &vars, None), result.to_string());
}
#[test]
fn render_surrounded() {
let template = "hello ${[ word ]} world!";
let vars = HashMap::from([("word".to_string(), "cruel".to_string())]);
let result = "hello cruel world!";
assert_eq!(parse_and_render(template, &vars, None), result.to_string());
}
#[test]
fn render_valid_fn() {
let vars = HashMap::new();
let template = r#"${[ say_hello("John", "Kate") ]}"#;
let result = r#"say_hello: ["John", "Kate"]"#;
fn cb(name: &str, args: Vec<String>) -> String {
format!("{name}: {:?}", args)
}
assert_eq!(parse_and_render(template, &vars, Some(cb)), result);
}
#[test]
fn render_nested_fn() {
let vars = HashMap::new();
let template = r#"${[ upper(secret()) ]}"#;
let result = r#"ABC"#;
fn cb(name: &str, args: Vec<String>) -> String {
match name {
"secret" => "abc".to_string(),
"upper" => args[0].to_string().to_uppercase(),
_ => "".to_string(),
}
}
assert_eq!(
parse_and_render(template, &vars, Some(cb)),
result.to_string()
);
}
}

View File

@@ -1,28 +1,243 @@
import { invoke } from '@tauri-apps/api/core';
import classNames from 'classnames';
import type { ReactNode } from 'react';
import { useCallback, useMemo, useState } from 'react';
import { search } from 'fast-fuzzy';
import type { KeyboardEvent, ReactNode } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useActiveCookieJar } from '../hooks/useActiveCookieJar';
import { useActiveEnvironment } from '../hooks/useActiveEnvironment';
import { useActiveEnvironmentId } from '../hooks/useActiveEnvironmentId';
import { useActiveRequestId } from '../hooks/useActiveRequestId';
import { useActiveWorkspaceId } from '../hooks/useActiveWorkspaceId';
import { useAppRoutes } from '../hooks/useAppRoutes';
import { getRecentEnvironments } from '../hooks/useRecentEnvironments';
import { useCreateEnvironment } from '../hooks/useCreateEnvironment';
import { useCreateGrpcRequest } from '../hooks/useCreateGrpcRequest';
import { useCreateHttpRequest } from '../hooks/useCreateHttpRequest';
import { useCreateWorkspace } from '../hooks/useCreateWorkspace';
import { useDebouncedState } from '../hooks/useDebouncedState';
import { useEnvironments } from '../hooks/useEnvironments';
import type { HotkeyAction } from '../hooks/useHotKey';
import { useHotKey } from '../hooks/useHotKey';
import { useOpenWorkspace } from '../hooks/useOpenWorkspace';
import { useRecentEnvironments } from '../hooks/useRecentEnvironments';
import { useRecentRequests } from '../hooks/useRecentRequests';
import { useRecentWorkspaces } from '../hooks/useRecentWorkspaces';
import { useRequests } from '../hooks/useRequests';
import { useSidebarHidden } from '../hooks/useSidebarHidden';
import { useWorkspaces } from '../hooks/useWorkspaces';
import { fallbackRequestName } from '../lib/fallbackRequestName';
import { Input } from './core/Input';
import { CookieDialog } from './CookieDialog';
import { Button } from './core/Button';
import { Heading } from './core/Heading';
import { HotKey } from './core/HotKey';
import { HttpMethodTag } from './core/HttpMethodTag';
import { Icon } from './core/Icon';
import { PlainInput } from './core/PlainInput';
import { HStack } from './core/Stacks';
import { useDialog } from './DialogContext';
import { EnvironmentEditDialog } from './EnvironmentEditDialog';
interface CommandPaletteGroup {
key: string;
label: ReactNode;
items: CommandPaletteItem[];
}
type CommandPaletteItem = {
key: string;
onSelect: () => void;
action?: HotkeyAction;
} & ({ searchText: string; label: ReactNode } | { label: string });
const MAX_PER_GROUP = 8;
export function CommandPalette({ onClose }: { onClose: () => void }) {
const [selectedIndex, setSelectedIndex] = useState<number>(0);
const [command, setCommand] = useDebouncedState<string>('', 150);
const [selectedItemKey, setSelectedItemKey] = useState<string | null>(null);
const routes = useAppRoutes();
const activeEnvironmentId = useActiveEnvironmentId();
const activeRequestId = useActiveRequestId();
const active = useActiveWorkspaceId();
const workspaces = useWorkspaces();
const environments = useEnvironments();
const recentEnvironments = useRecentEnvironments();
const recentWorkspaces = useRecentWorkspaces();
const requests = useRequests();
const [command, setCommand] = useState<string>('');
const recentRequests = useRecentRequests();
const openWorkspace = useOpenWorkspace();
const createWorkspace = useCreateWorkspace();
const createHttpRequest = useCreateHttpRequest();
const { activeCookieJar } = useActiveCookieJar();
const createGrpcRequest = useCreateGrpcRequest();
const createEnvironment = useCreateEnvironment();
const dialog = useDialog();
const workspaceId = useActiveWorkspaceId();
const activeEnvironment = useActiveEnvironment();
const [, setSidebarHidden] = useSidebarHidden();
const items = useMemo<{ label: string; onSelect: () => void; key: string }[]>(() => {
const items = [];
for (const r of requests) {
items.push({
const workspaceCommands = useMemo<CommandPaletteItem[]>(() => {
const commands: CommandPaletteItem[] = [
{
key: 'settings.open',
label: 'Open Settings',
action: 'settings.show',
onSelect: async () => {
if (workspaceId == null) return;
await invoke('cmd_new_nested_window', {
url: routes.paths.workspaceSettings({ workspaceId }),
label: 'settings',
title: 'Yaak Settings',
});
},
},
{
key: 'app.create',
label: 'Create Workspace',
onSelect: createWorkspace.mutate,
},
{
key: 'http_request.create',
label: 'Create HTTP Request',
onSelect: () => createHttpRequest.mutate({}),
},
{
key: 'cookies.show',
label: 'Show Cookies',
onSelect: async () => {
dialog.show({
id: 'cookies',
title: 'Manage Cookies',
size: 'full',
render: () => <CookieDialog cookieJarId={activeCookieJar?.id ?? null} />,
});
},
},
{
key: 'grpc_request.create',
label: 'Create GRPC Request',
onSelect: () => createGrpcRequest.mutate({}),
},
{
key: 'environment.edit',
label: 'Edit Environment',
action: 'environmentEditor.toggle',
onSelect: () => {
dialog.toggle({
id: 'environment-editor',
noPadding: true,
size: 'lg',
className: 'h-[80vh]',
render: () => <EnvironmentEditDialog initialEnvironment={activeEnvironment} />,
});
},
},
{
key: 'environment.create',
label: 'Create Environment',
onSelect: createEnvironment.mutate,
},
{
key: 'sidebar.toggle',
label: 'Toggle Sidebar',
action: 'sidebar.focus',
onSelect: () => setSidebarHidden((h) => !h),
},
];
return commands.sort((a, b) =>
('searchText' in a ? a.searchText : a.label).localeCompare(
'searchText' in b ? b.searchText : b.label,
),
);
}, [
activeCookieJar,
activeEnvironment,
createEnvironment.mutate,
createGrpcRequest,
createHttpRequest,
createWorkspace.mutate,
dialog,
routes.paths,
setSidebarHidden,
workspaceId,
]);
const sortedRequests = useMemo(() => {
return [...requests].sort((a, b) => {
const aRecentIndex = recentRequests.indexOf(a.id);
const bRecentIndex = recentRequests.indexOf(b.id);
if (aRecentIndex >= 0 && bRecentIndex >= 0) {
return aRecentIndex - bRecentIndex;
} else if (aRecentIndex >= 0 && bRecentIndex === -1) {
return -1;
} else if (aRecentIndex === -1 && bRecentIndex >= 0) {
return 1;
} else {
return a.createdAt.localeCompare(b.createdAt);
}
});
}, [recentRequests, requests]);
const sortedEnvironments = useMemo(() => {
return [...environments].sort((a, b) => {
const aRecentIndex = recentEnvironments.indexOf(a.id);
const bRecentIndex = recentEnvironments.indexOf(b.id);
if (aRecentIndex >= 0 && bRecentIndex >= 0) {
return aRecentIndex - bRecentIndex;
} else if (aRecentIndex >= 0 && bRecentIndex === -1) {
return -1;
} else if (aRecentIndex === -1 && bRecentIndex >= 0) {
return 1;
} else {
return a.createdAt.localeCompare(b.createdAt);
}
});
}, [environments, recentEnvironments]);
const sortedWorkspaces = useMemo(() => {
return [...workspaces].sort((a, b) => {
const aRecentIndex = recentWorkspaces.indexOf(a.id);
const bRecentIndex = recentWorkspaces.indexOf(b.id);
if (aRecentIndex >= 0 && bRecentIndex >= 0) {
return aRecentIndex - bRecentIndex;
} else if (aRecentIndex >= 0 && bRecentIndex === -1) {
return -1;
} else if (aRecentIndex === -1 && bRecentIndex >= 0) {
return 1;
} else {
return a.createdAt.localeCompare(b.createdAt);
}
});
}, [recentWorkspaces, workspaces]);
const groups = useMemo<CommandPaletteGroup[]>(() => {
const actionsGroup: CommandPaletteGroup = {
key: 'actions',
label: 'Actions',
items: workspaceCommands,
};
const requestGroup: CommandPaletteGroup = {
key: 'requests',
label: 'Requests',
items: [],
};
for (const r of sortedRequests) {
if (r.id === activeRequestId) {
continue;
}
requestGroup.items.push({
key: `switch-request-${r.id}`,
label: `Switch Request → ${fallbackRequestName(r)}`,
searchText: fallbackRequestName(r),
label: (
<HStack space={2}>
<HttpMethodTag className="text-fg-subtler" request={r} />
<div className="truncate">{fallbackRequestName(r)}</div>
</HStack>
),
onSelect: () => {
return routes.navigate('request', {
workspaceId: r.workspaceId,
@@ -32,25 +247,79 @@ export function CommandPalette({ onClose }: { onClose: () => void }) {
},
});
}
for (const w of workspaces) {
items.push({
key: `switch-workspace-${w.id}`,
label: `Switch Workspace → ${w.name}`,
onSelect: async () => {
const environmentId = (await getRecentEnvironments(w.id))[0];
return routes.navigate('workspace', {
workspaceId: w.id,
environmentId,
});
},
const environmentGroup: CommandPaletteGroup = {
key: 'environments',
label: 'Environments',
items: [],
};
for (const e of sortedEnvironments) {
if (e.id === activeEnvironment?.id) {
continue;
}
environmentGroup.items.push({
key: `switch-environment-${e.id}`,
label: e.name,
onSelect: () => routes.setEnvironment(e),
});
}
return items;
}, [activeEnvironmentId, requests, routes, workspaces]);
const filteredItems = useMemo(() => {
return items.filter((v) => v.label.toLowerCase().includes(command.toLowerCase()));
}, [command, items]);
const workspaceGroup: CommandPaletteGroup = {
key: 'workspaces',
label: 'Workspaces',
items: [],
};
for (const w of sortedWorkspaces) {
if (w.id === active) {
continue;
}
workspaceGroup.items.push({
key: `switch-workspace-${w.id}`,
label: w.name,
onSelect: () => openWorkspace.mutate({ workspaceId: w.id, inNewWindow: false }),
});
}
return [actionsGroup, requestGroup, environmentGroup, workspaceGroup];
}, [
workspaceCommands,
sortedRequests,
activeRequestId,
routes,
activeEnvironmentId,
sortedEnvironments,
activeEnvironment?.id,
sortedWorkspaces,
active,
openWorkspace,
]);
const allItems = useMemo(() => groups.flatMap((g) => g.items), [groups]);
useEffect(() => {
setSelectedItemKey(null);
}, [command]);
const { filteredGroups, filteredAllItems } = useMemo(() => {
const result = command
? search(command, allItems, {
threshold: 0.5,
keySelector: (v) => ('searchText' in v ? v.searchText : v.label),
})
: allItems;
const filteredGroups = groups
.map((g) => {
g.items = result.filter((i) => g.items.includes(i)).slice(0, MAX_PER_GROUP);
return g;
})
.filter((g) => g.items.length > 0);
const filteredAllItems = filteredGroups.flatMap((g) => g.items);
return { filteredAllItems, filteredGroups };
}, [allItems, command, groups]);
const handleSelectAndClose = useCallback(
(cb: () => void) => {
@@ -60,44 +329,73 @@ export function CommandPalette({ onClose }: { onClose: () => void }) {
[onClose],
);
const selectedItem = useMemo(() => {
let selectedItem = filteredAllItems.find((i) => i.key === selectedItemKey) ?? null;
if (selectedItem == null) {
selectedItem = filteredAllItems[0] ?? null;
}
return selectedItem;
}, [filteredAllItems, selectedItemKey]);
const handleKeyDown = useCallback(
(e: KeyboardEvent) => {
if (e.key === 'ArrowDown') {
setSelectedIndex((prev) => prev + 1);
} else if (e.key === 'ArrowUp') {
setSelectedIndex((prev) => prev - 1);
(e: KeyboardEvent<HTMLInputElement>) => {
const index = filteredAllItems.findIndex((v) => v.key === selectedItem?.key);
if (e.key === 'ArrowDown' || (e.ctrlKey && e.key === 'n')) {
const next = filteredAllItems[index + 1] ?? filteredAllItems[0];
setSelectedItemKey(next?.key ?? null);
} else if (e.key === 'ArrowUp' || (e.ctrlKey && e.key === 'k')) {
const prev = filteredAllItems[index - 1] ?? filteredAllItems[filteredAllItems.length - 1];
setSelectedItemKey(prev?.key ?? null);
} else if (e.key === 'Enter') {
const item = filteredItems[selectedIndex];
if (item) {
handleSelectAndClose(item.onSelect);
const selected = filteredAllItems[index];
setSelectedItemKey(selected?.key ?? null);
if (selected) {
handleSelectAndClose(selected.onSelect);
}
}
},
[filteredItems, handleSelectAndClose, selectedIndex],
[filteredAllItems, handleSelectAndClose, selectedItem?.key],
);
return (
<div className="h-full grid grid-rows-[auto_minmax(0,1fr)]">
<div className="h-full w-[400px] grid grid-rows-[auto_minmax(0,1fr)] overflow-hidden">
<div className="px-2 py-2 w-full">
<Input
<PlainInput
hideLabel
leftSlot={
<div className="h-md w-10 flex justify-center items-center">
<Icon icon="search" className="text-fg-subtle" />
</div>
}
name="command"
label="Command"
placeholder="Type a command"
defaultValue=""
placeholder="Search or type a command"
className="font-sans !text-base"
defaultValue={command}
onChange={setCommand}
onKeyDown={handleKeyDown}
onKeyDownCapture={handleKeyDown}
/>
</div>
<div className="h-full px-1.5 overflow-y-auto">
{filteredItems.map((v, i) => (
<CommandPaletteItem
active={i === selectedIndex}
key={v.key}
onClick={() => handleSelectAndClose(v.onSelect)}
>
{v.label}
</CommandPaletteItem>
<div className="h-full px-1.5 overflow-y-auto pb-1">
{filteredGroups.map((g) => (
<div key={g.key} className="mb-1.5 w-full">
<Heading size={2} className="!text-xs uppercase px-1.5 h-sm flex items-center">
{g.label}
</Heading>
{g.items.map((v) => (
<CommandPaletteItem
active={v.key === selectedItem?.key}
key={v.key}
onClick={() => handleSelectAndClose(v.onSelect)}
rightSlot={
v.action && <CommandPaletteAction action={v.action} onAction={v.onSelect} />
}
>
{v.label}
</CommandPaletteItem>
))}
</div>
))}
</div>
</div>
@@ -108,20 +406,39 @@ function CommandPaletteItem({
children,
active,
onClick,
rightSlot,
}: {
children: ReactNode;
active: boolean;
onClick: () => void;
rightSlot?: ReactNode;
}) {
return (
<button
<Button
onClick={onClick}
tabIndex={active ? undefined : -1}
rightSlot={rightSlot}
color="custom"
justify="start"
className={classNames(
'w-full h-xs flex items-center rounded px-1.5 text-fg-subtle',
'w-full h-sm flex items-center rounded px-1.5',
'hover:text-fg',
active && 'bg-background-highlight-secondary text-fg',
!active && 'text-fg-subtle',
)}
>
{children}
</button>
<span className="truncate">{children}</span>
</Button>
);
}
function CommandPaletteAction({
action,
onAction,
}: {
action: HotkeyAction;
onAction: () => void;
}) {
useHotKey(action, onAction);
return <HotKey className="ml-auto" action={action} />;
}

View File

@@ -21,7 +21,7 @@ import { Icon } from './core/Icon';
import { IconButton } from './core/IconButton';
import { InlineCode } from './core/InlineCode';
import type { PairEditorProps } from './core/PairEditor';
import { PairEditor } from './core/PairEditor';
import { PairOrBulkEditor } from './core/PairOrBulkEditor';
import { Separator } from './core/Separator';
import { SplitLayout } from './core/SplitLayout';
import { HStack, VStack } from './core/Stacks';
@@ -185,18 +185,20 @@ const EnvironmentEditor = function ({
/>
</Heading>
</HStack>
<PairEditor
className="pr-2"
nameAutocomplete={nameAutocomplete}
nameAutocompleteVariables={false}
namePlaceholder="VAR_NAME"
nameValidate={validateName}
valueType={valueVisibility.value ? 'text' : 'password'}
valueAutocompleteVariables={false}
forceUpdateKey={environment?.id ?? workspace?.id ?? 'n/a'}
pairs={variables}
onChange={handleChange}
/>
<div className="h-full pr-2 pb-2">
<PairOrBulkEditor
preferenceName="environment"
nameAutocomplete={nameAutocomplete}
nameAutocompleteVariables={false}
namePlaceholder="VAR_NAME"
nameValidate={validateName}
valueType={valueVisibility.value ? 'text' : 'password'}
valueAutocompleteVariables={true}
forceUpdateKey={environment?.id ?? workspace?.id ?? 'n/a'}
pairs={variables}
onChange={handleChange}
/>
</div>
</VStack>
);
};

View File

@@ -59,7 +59,7 @@ export function ExportDataDialog({
const noneSelected = numSelected === 0;
return (
<VStack space={3} className="w-full mb-3 px-4">
<table className="w-full mb-auto min-w-full max-w-full divide-y">
<table className="w-full mb-auto min-w-full max-w-full divide-y divide-background-highlight-secondary">
<thead>
<tr>
<th className="w-6 min-w-0 py-2 text-left pl-1">
@@ -76,7 +76,7 @@ export function ExportDataDialog({
</th>
</tr>
</thead>
<tbody className="divide-y">
<tbody className="divide-y divide-background-highlight-secondary">
{workspaces.map((w) => (
<tr key={w.id}>
<td className="min-w-0 py-1 pl-1">

View File

@@ -1,7 +1,7 @@
import { useCallback, useMemo } from 'react';
import type { HttpRequest } from '../lib/models';
import type { Pair, PairEditorProps } from './core/PairEditor';
import { PairEditor } from './core/PairEditor';
import { PairOrBulkEditor } from './core/PairOrBulkEditor';
type Props = {
forceUpdateKey: string;
@@ -27,7 +27,8 @@ export function FormUrlencodedEditor({ body, forceUpdateKey, onChange }: Props)
);
return (
<PairEditor
<PairOrBulkEditor
preferenceName="form_urlencoded"
valueAutocompleteVariables
nameAutocompleteVariables
namePlaceholder="entry_name"

View File

@@ -6,7 +6,6 @@ import { useCommandPalette } from '../hooks/useCommandPalette';
import { cookieJarsQueryKey } from '../hooks/useCookieJars';
import { environmentsQueryKey } from '../hooks/useEnvironments';
import { foldersQueryKey } from '../hooks/useFolders';
import { useGlobalCommands } from '../hooks/useGlobalCommands';
import { grpcConnectionsQueryKey } from '../hooks/useGrpcConnections';
import { grpcEventsQueryKey } from '../hooks/useGrpcEvents';
import { grpcRequestsQueryKey } from '../hooks/useGrpcRequests';
@@ -42,7 +41,6 @@ export function GlobalHooks() {
// Other useful things
useSyncThemeToDocument();
useGlobalCommands();
useCommandPalette();
useNotificationToast();

View File

@@ -125,7 +125,7 @@ export function GraphQLEditor({ defaultValue, onChange, baseRequest, ...extraEdi
{...extraEditorProps}
/>
<div className="grid grid-rows-[auto_minmax(0,1fr)] grid-cols-1 min-h-[5rem]">
<Separator variant="primary" className="pb-1">
<Separator dashed className="pb-1">
Variables
</Separator>
<Editor

View File

@@ -111,7 +111,7 @@ export function GrpcConnectionLayout({ style }: Props) {
) : messages.length >= 0 ? (
<GrpcConnectionMessagesPane activeRequest={activeRequest} methodType={methodType} />
) : (
<HotKeyList hotkeys={['grpc_request.send', 'sidebar.toggle', 'urlBar.focus']} />
<HotKeyList hotkeys={['grpc_request.send', 'sidebar.focus', 'urlBar.focus']} />
)}
</div>
)

View File

@@ -6,7 +6,7 @@ import { mimeTypes } from '../lib/data/mimetypes';
import type { HttpRequest } from '../lib/models';
import type { GenericCompletionConfig } from './core/Editor/genericCompletion';
import type { PairEditorProps } from './core/PairEditor';
import { PairEditor } from './core/PairEditor';
import { PairOrBulkEditor } from './core/PairOrBulkEditor';
type Props = {
forceUpdateKey: string;
@@ -16,7 +16,8 @@ type Props = {
export function HeadersEditor({ headers, onChange, forceUpdateKey }: Props) {
return (
<PairEditor
<PairOrBulkEditor
preferenceName="headers"
valueAutocompleteVariables
nameAutocompleteVariables
pairs={headers}

View File

@@ -0,0 +1,64 @@
import { useState } from 'react';
import { useOpenWorkspace } from '../hooks/useOpenWorkspace';
import { useSettings } from '../hooks/useSettings';
import { useUpdateSettings } from '../hooks/useUpdateSettings';
import type { Workspace } from '../lib/models';
import { Button } from './core/Button';
import { Checkbox } from './core/Checkbox';
import { Icon } from './core/Icon';
import { InlineCode } from './core/InlineCode';
import { HStack, VStack } from './core/Stacks';
interface Props {
hide: () => void;
workspace: Workspace;
}
export function OpenWorkspaceDialog({ hide, workspace }: Props) {
const openWorkspace = useOpenWorkspace();
const settings = useSettings();
const updateSettings = useUpdateSettings();
const [remember, setRemember] = useState<boolean>(false);
return (
<VStack space={3}>
<p>
Where would you like to open <InlineCode>{workspace.name}</InlineCode>?
</p>
<HStack space={2} justifyContent="start" className="flex-row-reverse">
<Button
className="focus"
color="primary"
onClick={() => {
hide();
openWorkspace.mutate({ workspaceId: workspace.id, inNewWindow: false });
if (remember) {
updateSettings.mutate({ openWorkspaceNewWindow: false });
}
}}
>
This Window
</Button>
<Button
className="focus"
color="secondary"
rightSlot={<Icon icon="externalLink" />}
onClick={() => {
hide();
openWorkspace.mutate({ workspaceId: workspace.id, inNewWindow: true });
if (remember) {
updateSettings.mutate({ openWorkspaceNewWindow: true });
}
}}
>
New Window
</Button>
</HStack>
{settings && (
<HStack justifyContent="end">
<Checkbox checked={remember} title="Remember my choice" onChange={setRemember} />
</HStack>
)}
</VStack>
);
}

View File

@@ -47,13 +47,13 @@ export function Overlay({
variant === 'default' && 'bg-background-backdrop backdrop-blur-sm',
)}
/>
{children}
{/* Show draggable region at the top */}
{/* TODO: Figure out tauri drag region and also make clickable still */}
{variant === 'default' && (
<div data-tauri-drag-region className="absolute top-0 left-0 h-md right-0" />
)}
{children}
</motion.div>
</FocusTrap>
)}

View File

@@ -53,7 +53,7 @@ export function RecentRequestsDropdown({ className }: Pick<ButtonProps, 'classNa
key: request.id,
label: fallbackRequestName(request),
// leftSlot: <CountBadge className="!ml-0 px-0 w-5" count={recentRequestItems.length} />,
leftSlot: <HttpMethodTag request={request} />,
leftSlot: <HttpMethodTag className="text-right" shortNames request={request} />,
onSelect: () => {
routes.navigate('request', {
requestId: request.id,

View File

@@ -1,6 +1,7 @@
import classNames from 'classnames';
import { useDeleteHttpResponse } from '../hooks/useDeleteHttpResponse';
import { useDeleteHttpResponses } from '../hooks/useDeleteHttpResponses';
import { useSaveResponse } from '../hooks/useSaveResponse';
import type { HttpResponse } from '../lib/models';
import { pluralize } from '../lib/pluralize';
import { Dropdown } from './core/Dropdown';
@@ -25,30 +26,49 @@ export const RecentResponsesDropdown = function ResponsePane({
const deleteResponse = useDeleteHttpResponse(activeResponse?.id ?? null);
const deleteAllResponses = useDeleteHttpResponses(activeResponse?.requestId);
const latestResponseId = responses[0]?.id ?? 'n/a';
const saveResponse = useSaveResponse(activeResponse);
return (
<Dropdown
items={[
{
key: 'save',
label: 'Save to File',
onSelect: saveResponse.mutate,
leftSlot: <Icon icon="save" />,
hidden: responses.length === 0,
disabled: responses.length === 0,
},
{
key: 'clear-single',
label: 'Clear Response',
label: 'Delete',
leftSlot: <Icon icon="trash" />,
onSelect: deleteResponse.mutate,
disabled: responses.length === 0,
},
{
key: 'unpin',
label: 'Unpin Response',
onSelect: () => onPinnedResponseId(activeResponse.id),
leftSlot: <Icon icon="unpin" />,
hidden: latestResponseId === activeResponse.id,
disabled: responses.length === 0,
},
{ type: 'separator', label: 'History' },
{
key: 'clear-all',
label: `Clear ${responses.length} ${pluralize('Response', responses.length)}`,
label: `Delete ${responses.length} ${pluralize('Response', responses.length)}`,
onSelect: deleteAllResponses.mutate,
hidden: responses.length <= 1,
disabled: responses.length === 0,
},
{ type: 'separator', label: 'History' },
{ type: 'separator' },
...responses.slice(0, 20).map((r: HttpResponse) => ({
key: r.id,
label: (
<HStack space={2}>
<StatusTag className="text-sm" response={r} />
<span>&rarr;</span>{' '}
<span className="text-fg-subtle">&rarr;</span>{' '}
<span className="font-mono text-sm">{r.elapsed >= 0 ? `${r.elapsed}ms` : 'n/a'}</span>
</HStack>
),

View File

@@ -262,13 +262,15 @@ export const RequestPane = memo(function RequestPane({
options:
requests.length > 0
? [
...requests.map(
(r) =>
({
type: 'constant',
label: r.url,
} as GenericCompletionOption),
),
...requests
.filter((r) => r.id !== activeRequestId)
.map(
(r) =>
({
type: 'constant',
label: r.url,
} as GenericCompletionOption),
),
]
: [
{ label: 'http://', type: 'constant' },

View File

@@ -23,6 +23,7 @@ import { ResponseHeaders } from './ResponseHeaders';
import { AudioViewer } from './responseViewers/AudioViewer';
import { CsvViewer } from './responseViewers/CsvViewer';
import { ImageViewer } from './responseViewers/ImageViewer';
import { PdfViewer } from './responseViewers/PdfViewer';
import { TextViewer } from './responseViewers/TextViewer';
import { VideoViewer } from './responseViewers/VideoViewer';
import { WebPageViewer } from './responseViewers/WebPageViewer';
@@ -83,7 +84,7 @@ export const ResponsePane = memo(function ResponsePane({ style, className, activ
>
{activeResponse == null ? (
<HotKeyList
hotkeys={['http_request.send', 'http_request.create', 'sidebar.toggle', 'urlBar.focus']}
hotkeys={['http_request.send', 'http_request.create', 'sidebar.focus', 'urlBar.focus']}
/>
) : isResponseLoading(activeResponse) ? (
<div className="h-full w-full flex items-center justify-center">
@@ -158,12 +159,12 @@ export const ResponsePane = memo(function ResponsePane({ style, className, activ
<AudioViewer response={activeResponse} />
) : contentType?.startsWith('video') ? (
<VideoViewer response={activeResponse} />
) : activeResponse.contentLength > 2 * 1000 * 1000 ? (
<EmptyStateText>Cannot preview text responses larger than 2MB</EmptyStateText>
) : viewMode === 'pretty' && contentType?.includes('html') ? (
<WebPageViewer response={activeResponse} />
) : contentType?.match(/pdf/) ? (
<PdfViewer response={activeResponse} />
) : contentType?.match(/csv|tab-separated/) ? (
<CsvViewer className="pb-2" response={activeResponse} />
) : viewMode === 'pretty' && contentType?.includes('html') ? (
<WebPageViewer response={activeResponse} />
) : (
<TextViewer
className="-mr-2" // Pull to the right

View File

@@ -50,6 +50,33 @@ export function SettingsGeneral() {
onClick={() => checkForUpdates.mutateAsync()}
/>
</div>
<Select
name="openWorkspace"
label="Open Workspace"
labelPosition="left"
size="sm"
value={
settings.openWorkspaceNewWindow === true
? 'new'
: settings.openWorkspaceNewWindow === false
? 'current'
: 'ask'
}
onChange={(v) => {
if (v === 'current') {
updateSettings.mutate({ openWorkspaceNewWindow: false });
} else if (v === 'new') {
updateSettings.mutate({ openWorkspaceNewWindow: true });
} else {
updateSettings.mutate({ openWorkspaceNewWindow: null });
}
}}
options={[
{ label: 'Always Ask', value: 'ask' },
{ label: 'Current Window', value: 'current' },
{ label: 'New Window', value: 'new' },
]}
/>
<Separator className="my-4" />
<Heading size={2}>

View File

@@ -101,7 +101,7 @@ export function Sidebar({ className }: Props) {
[collapsed.value],
);
const { tree, treeParentMap, selectableRequests, selectedRequest } = useMemo<{
const { tree, treeParentMap, selectableRequests } = useMemo<{
tree: TreeNode | null;
treeParentMap: Record<string, TreeNode>;
selectedRequest: HttpRequest | GrpcRequest | null;
@@ -159,8 +159,6 @@ export function Sidebar({ className }: Props) {
return { tree, treeParentMap, selectableRequests, selectedRequest };
}, [activeWorkspace, selectedId, requests, folders, collapsed.value]);
const deleteSelectedRequest = useDeleteRequest(selectedRequest);
const focusActiveRequest = useCallback(
(
args: {
@@ -192,7 +190,7 @@ export function Sidebar({ className }: Props) {
);
const handleSelect = useCallback(
async (id: string) => {
async (id: string, opts: { noFocus?: boolean } = {}) => {
const tree = treeParentMap[id ?? 'n/a'] ?? null;
const children = tree?.children ?? [];
const node = children.find((m) => m.item.id === id) ?? null;
@@ -212,7 +210,7 @@ export function Sidebar({ className }: Props) {
});
setSelectedId(id);
setSelectedTree(tree);
focusActiveRequest({ forced: { id, tree } });
if (!opts.noFocus) focusActiveRequest({ forced: { id, tree } });
}
},
[treeParentMap, collapsed, routes, activeEnvironmentId, focusActiveRequest],
@@ -230,21 +228,6 @@ export function Sidebar({ className }: Props) {
const handleBlur = useCallback(() => setHasFocus(false), []);
const handleDeleteKey = useCallback(
async (e: KeyboardEvent) => {
if (!hasFocus) return;
e.preventDefault();
const selected = selectableRequests.find((r) => r.id === selectedId);
if (selected == null) return;
await deleteSelectedRequest.mutateAsync();
},
[hasFocus, selectableRequests, deleteSelectedRequest, selectedId],
);
useKeyPressEvent('Backspace', handleDeleteKey);
useKeyPressEvent('Delete', handleDeleteKey);
useHotKey('sidebar.focus', async () => {
// Hide the sidebar if it's already focused
if (!hidden && hasFocus) {
@@ -620,7 +603,7 @@ const SidebarItem = forwardRef(function SidebarItem(
) {
const activeRequest = useActiveRequest();
const deleteFolder = useDeleteFolder(itemId);
const deleteRequest = useDeleteRequest(activeRequest ?? null);
const deleteRequest = useDeleteRequest(itemId);
const duplicateHttpRequest = useDuplicateHttpRequest({ id: itemId, navigateAfter: true });
const duplicateGrpcRequest = useDuplicateGrpcRequest({ id: itemId, navigateAfter: true });
const copyAsCurl = useCopyAsCurl(itemId);
@@ -839,7 +822,7 @@ const SidebarItem = forwardRef(function SidebarItem(
)}
/>
)}
<div className="flex items-end gap-2 min-w-0">
<div className="flex items-center gap-2 min-w-0">
{itemPrefix}
{editing ? (
<input

View File

@@ -31,7 +31,6 @@ export function SidebarActions() {
className="pointer-events-auto"
size="sm"
title="Show sidebar"
hotkeyAction="sidebar.toggle"
icon={hidden ? 'leftPanelHidden' : 'leftPanelVisible'}
/>
<CreateDropdown hotKeyAction="http_request.create">

View File

@@ -67,7 +67,7 @@ export const ToastProvider = ({ children }: { children: React.ReactNode }) => {
<ToastContext.Provider value={state}>
{children}
<Portal name="toasts">
<div className="absolute right-0 bottom-0">
<div className="absolute right-0 bottom-0 z-10">
<AnimatePresence>
{toasts.map((props: PrivateToastEntry) => (
<ToastInstance key={props.id} {...props} />

View File

@@ -1,5 +1,5 @@
import type { HttpRequest } from '../lib/models';
import { PairEditor } from './core/PairEditor';
import { PairOrBulkEditor } from './core/PairOrBulkEditor';
type Props = {
forceUpdateKey: string;
@@ -9,7 +9,8 @@ type Props = {
export function UrlParametersEditor({ urlParameters, forceUpdateKey, onChange }: Props) {
return (
<PairEditor
<PairOrBulkEditor
preferenceName="url_parameters"
valueAutocompleteVariables
nameAutocompleteVariables
namePlaceholder="param_name"

View File

@@ -186,7 +186,7 @@ export default function Workspace() {
</div>
) : activeRequest == null ? (
<HotKeyList
hotkeys={['http_request.create', 'sidebar.toggle', 'settings.show']}
hotkeys={['http_request.create', 'sidebar.focus', 'settings.show']}
bottomSlot={
<HStack space={1} justifyContent="center" className="mt-3">
<Button variant="border" size="sm" onClick={() => importData.mutate()}>

View File

@@ -1,23 +1,23 @@
import { invoke } from '@tauri-apps/api/core';
import classNames from 'classnames';
import { memo, useMemo } from 'react';
import { memo, useCallback, useMemo } from 'react';
import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
import { useAppRoutes } from '../hooks/useAppRoutes';
import { useCommand } from '../hooks/useCommands';
import { useCreateWorkspace } from '../hooks/useCreateWorkspace';
import { useDeleteWorkspace } from '../hooks/useDeleteWorkspace';
import { useOpenWorkspace } from '../hooks/useOpenWorkspace';
import { usePrompt } from '../hooks/usePrompt';
import { getRecentEnvironments } from '../hooks/useRecentEnvironments';
import { getRecentRequests } from '../hooks/useRecentRequests';
import { useSettings } from '../hooks/useSettings';
import { useUpdateWorkspace } from '../hooks/useUpdateWorkspace';
import { useWorkspaces } from '../hooks/useWorkspaces';
import { getWorkspace } from '../lib/store';
import type { ButtonProps } from './core/Button';
import { Button } from './core/Button';
import type { DropdownItem } from './core/Dropdown';
import { Dropdown } from './core/Dropdown';
import { Icon } from './core/Icon';
import { InlineCode } from './core/InlineCode';
import { HStack } from './core/Stacks';
import type { RadioDropdownItem } from './core/RadioDropdown';
import { RadioDropdown } from './core/RadioDropdown';
import { useDialog } from './DialogContext';
import { OpenWorkspaceDialog } from './OpenWorkspaceDialog';
type Props = Pick<ButtonProps, 'className' | 'justify' | 'forDropdown' | 'leftSlot'>;
@@ -30,86 +30,25 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
const activeWorkspaceId = activeWorkspace?.id ?? null;
const updateWorkspace = useUpdateWorkspace(activeWorkspaceId);
const deleteWorkspace = useDeleteWorkspace(activeWorkspace);
const createWorkspace = useCommand('workspace.create');
const createWorkspace = useCreateWorkspace();
const dialog = useDialog();
const prompt = usePrompt();
const routes = useAppRoutes();
const settings = useSettings();
const openWorkspace = useOpenWorkspace();
const openWorkspaceNewWindow = settings?.openWorkspaceNewWindow ?? null;
const items: DropdownItem[] = useMemo(() => {
const workspaceItems: DropdownItem[] = workspaces.map((w) => ({
const { workspaceItems, extraItems } = useMemo<{
workspaceItems: RadioDropdownItem[];
extraItems: DropdownItem[];
}>(() => {
const workspaceItems: RadioDropdownItem[] = workspaces.map((w) => ({
key: w.id,
label: w.name,
value: w.id,
leftSlot: w.id === activeWorkspaceId ? <Icon icon="check" /> : <Icon icon="empty" />,
onSelect: async () => {
dialog.show({
id: 'open-workspace',
size: 'sm',
title: 'Open Workspace',
description: (
<>
Where would you like to open <InlineCode>{w.name}</InlineCode>?
</>
),
render: ({ hide }) => {
return (
<HStack space={2} justifyContent="start" className="mt-4 mb-6 flex-row-reverse">
<Button
className="focus"
color="primary"
onClick={async () => {
hide();
const environmentId = (await getRecentEnvironments(w.id))[0];
const requestId = (await getRecentRequests(w.id))[0];
if (requestId != null) {
routes.navigate('request', { workspaceId: w.id, environmentId, requestId });
} else {
routes.navigate('workspace', { workspaceId: w.id, environmentId });
}
}}
>
This Window
</Button>
<Button
className="focus"
color="secondary"
rightSlot={<Icon icon="externalLink" />}
onClick={async () => {
hide();
const environmentId = (await getRecentEnvironments(w.id))[0];
const requestId = (await getRecentRequests(w.id))[0];
const path =
requestId != null
? routes.paths.request({
workspaceId: w.id,
environmentId,
requestId,
})
: routes.paths.workspace({ workspaceId: w.id, environmentId });
await invoke('cmd_new_window', { url: path });
}}
>
New Window
</Button>
</HStack>
);
},
});
},
}));
const activeWorkspaceItems: DropdownItem[] =
workspaces.length <= 1
? []
: [
...workspaceItems,
{
type: 'separator',
label: activeWorkspace?.name,
},
];
return [
...activeWorkspaceItems,
const extraItems: DropdownItem[] = [
{
key: 'rename',
label: 'Rename',
@@ -143,23 +82,50 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
key: 'create-workspace',
label: 'New Workspace',
leftSlot: <Icon icon="plus" />,
onSelect: () => createWorkspace.mutate({}),
onSelect: createWorkspace.mutate,
},
];
return { workspaceItems, extraItems };
}, [
activeWorkspace?.name,
activeWorkspaceId,
createWorkspace,
deleteWorkspace.mutate,
dialog,
prompt,
routes,
updateWorkspace,
workspaces,
]);
const handleChange = useCallback(
async (workspaceId: string | null) => {
if (workspaceId == null) return;
if (typeof openWorkspaceNewWindow === 'boolean') {
openWorkspace.mutate({ workspaceId, inNewWindow: openWorkspaceNewWindow });
return;
}
const workspace = await getWorkspace(workspaceId);
if (workspace == null) return;
dialog.show({
id: 'open-workspace',
size: 'sm',
title: 'Open Workspace',
render: ({ hide }) => <OpenWorkspaceDialog workspace={workspace} hide={hide} />,
});
},
[dialog, openWorkspace, openWorkspaceNewWindow],
);
return (
<Dropdown items={items}>
<RadioDropdown
items={workspaceItems}
extraItems={extraItems}
onChange={handleChange}
value={activeWorkspaceId}
>
<Button
size="sm"
className={classNames(
@@ -171,6 +137,6 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
>
{activeWorkspace?.name ?? 'Workspace'}
</Button>
</Dropdown>
</RadioDropdown>
);
});

View File

@@ -0,0 +1,45 @@
import { useCallback, useMemo } from 'react';
import { Editor } from './Editor';
import type { PairEditorProps } from './PairEditor';
type Props = PairEditorProps;
export function BulkPairEditor({ pairs, onChange, namePlaceholder, valuePlaceholder }: Props) {
const pairsText = useMemo(() => {
return pairs
.filter((p) => !(p.name.trim() === '' && p.value.trim() === ''))
.map((p) => `${p.name}: ${p.value}`)
.join('\n');
}, [pairs]);
const handleChange = useCallback(
(text: string) => {
const pairs = text
.split('\n')
.filter((l: string) => l.trim())
.map(lineToPair);
onChange(pairs);
},
[onChange],
);
return (
<Editor
useTemplating
autocompleteVariables
placeholder={`${namePlaceholder ?? 'name'}: ${valuePlaceholder ?? 'value'}`}
defaultValue={pairsText}
contentType="pairs"
onChange={handleChange}
/>
);
}
function lineToPair(l: string): PairEditorProps['pairs'][0] {
const [name, ...values] = l.split(':');
const pair: PairEditorProps['pairs'][0] = {
name: (name ?? '').trim(),
value: values.join(':').trim(),
};
return pair;
}

View File

@@ -18,6 +18,7 @@ export interface DialogProps {
hideX?: boolean;
noPadding?: boolean;
noScroll?: boolean;
vAlign?: 'top' | 'center';
}
export function Dialog({
@@ -31,6 +32,7 @@ export function Dialog({
hideX,
noPadding,
noScroll,
vAlign = 'center',
}: DialogProps) {
const titleId = useMemo(() => Math.random().toString(36).slice(2), []);
const descriptionId = useMemo(
@@ -50,13 +52,14 @@ export function Dialog({
return (
<Overlay open={open} onClose={onClose} portalName="dialog">
<div className="x-theme-dialog absolute inset-0 flex items-center justify-center pointer-events-none">
<div
role="dialog"
aria-labelledby={titleId}
aria-describedby={descriptionId}
className="pointer-events-auto"
>
<div
className={classNames(
'x-theme-dialog absolute inset-0 flex flex-col items-center pointer-events-none',
vAlign === 'top' && 'justify-start',
vAlign === 'center' && 'justify-center',
)}
>
<div role="dialog" aria-labelledby={titleId} aria-describedby={descriptionId}>
<motion.div
initial={{ top: 5, scale: 0.97 }}
animate={{ top: 0, scale: 1 }}
@@ -66,12 +69,12 @@ export function Dialog({
'relative bg-background pointer-events-auto',
'rounded-lg',
'border border-background-highlight-secondary shadow-lg shadow-[rgba(0,0,0,0.1)]',
'max-w-[calc(100vw-5rem)] max-h-[calc(100vh-6rem)]',
size === 'sm' && 'w-[25rem] max-h-[80vh]',
'max-w-[calc(100vw-5rem)] max-h-[calc(100vh-4rem)]',
size === 'sm' && 'w-[28rem] max-h-[80vh]',
size === 'md' && 'w-[45rem] max-h-[80vh]',
size === 'lg' && 'w-[65rem] max-h-[80vh]',
size === 'full' && 'w-[100vw] h-[100vh]',
size === 'dynamic' && 'min-w-[30vw] max-w-[80vw]',
size === 'dynamic' && 'min-w-[20rem] max-w-[100vw] w-full mt-8',
)}
>
{title ? (

View File

@@ -35,6 +35,7 @@ import { HStack, VStack } from './Stacks';
export type DropdownItemSeparator = {
type: 'separator';
label?: string;
hidden?: boolean;
};
export type DropdownItemDefault = {
@@ -273,7 +274,7 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle'>, MenuPro
let nextIndex = (currIndex ?? 0) - 1;
const maxTries = items.length;
for (let i = 0; i < maxTries; i++) {
if (items[nextIndex]?.type === 'separator') {
if (items[nextIndex]?.hidden || items[nextIndex]?.type === 'separator') {
nextIndex--;
} else if (nextIndex < 0) {
nextIndex = items.length - 1;
@@ -290,7 +291,7 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle'>, MenuPro
let nextIndex = (currIndex ?? -1) + 1;
const maxTries = items.length;
for (let i = 0; i < maxTries; i++) {
if (items[nextIndex]?.type === 'separator') {
if (items[nextIndex]?.hidden || items[nextIndex]?.type === 'separator') {
nextIndex++;
} else if (nextIndex >= items.length) {
nextIndex = 0;
@@ -373,6 +374,7 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle'>, MenuPro
right: onRight ? docRect.width - triggerShape.right : undefined,
left: !onRight ? triggerShape.left : undefined,
minWidth: fullWidth ? triggerWidth : undefined,
maxWidth: '40rem',
};
const size = { top: '-0.2rem', width: '0.4rem', height: '0.4rem' };
const triangleStyles = onRight
@@ -455,6 +457,9 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle'>, MenuPro
<span className="text-fg-subtler text-center px-2 py-1">No matches</span>
)}
{filteredItems.map((item, i) => {
if (item.hidden) {
return null;
}
if (item.type === 'separator') {
return (
<Separator key={i} className={classNames('my-1.5', item.label && 'ml-2')}>
@@ -462,9 +467,6 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle'>, MenuPro
</Separator>
);
}
if (item.hidden) {
return null;
}
return (
<MenuItem
focused={i === selectedIndex}
@@ -537,20 +539,12 @@ function MenuItem({ className, focused, onFocus, item, onSelect, ...props }: Men
'h-xs', // More compact
'min-w-[8rem] outline-none px-2 mx-1.5 flex whitespace-nowrap',
'focus:bg-background-highlight focus:text-fg rounded',
item.variant === 'default' && 'text-fg-subtle',
item.variant === 'danger' && 'text-fg-danger',
item.variant === 'notify' && 'text-fg-primary',
item.variant === 'danger' && '!text-fg-danger',
item.variant === 'notify' && '!text-fg-primary',
)}
{...props}
>
<div
className={classNames(
// Add padding on right when no right slot, for some visual balance
!item.rightSlot && 'pr-4',
)}
>
{item.label}
</div>
<div className={classNames('truncate')}>{item.label}</div>
</Button>
);
}

View File

@@ -102,7 +102,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
// Use ref so we can update the handler without re-initializing the editor
const handleChange = useRef<EditorProps['onChange']>(onChange);
useEffect(() => {
handleChange.current = onChange;
handleChange.current = onChange ? onChange : onChange;
}, [onChange]);
// Use ref so we can update the handler without re-initializing the editor
@@ -332,6 +332,20 @@ function getExtensions({
return [
...baseExtensions, // Must be first
EditorView.domEventHandlers({
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),
...(singleLine ? [singleLineExt()] : []),
@@ -349,21 +363,6 @@ function getExtensions({
onChange.current?.(update.state.doc.toString());
}
}),
EditorView.domEventHandlers({
focus: () => {
onFocus.current?.();
},
blur: () => {
onBlur.current?.();
},
keydown: (e) => {
onKeyDown.current?.(e);
},
paste: (e) => {
onPaste.current?.(e.clipboardData?.getData('text/plain') ?? '');
},
}),
];
}

View File

@@ -35,6 +35,7 @@ import { graphql, graphqlLanguageSupport } from 'cm6-graphql';
import { EditorView } from 'codemirror';
import type { Environment, Workspace } from '../../../lib/models';
import type { EditorProps } from './index';
import { pairs } from './pairs/extension';
import { text } from './text/extension';
import { twig } from './twig/extension';
import { url } from './url/extension';
@@ -71,6 +72,7 @@ const syntaxExtensions: Record<string, LanguageSupport> = {
'application/xml': xml(),
'text/xml': xml(),
url: url(),
pairs: pairs(),
};
export function getLanguageExtension({

View File

@@ -0,0 +1,11 @@
import { LanguageSupport, LRLanguage } from '@codemirror/language';
import { parser } from './pairs';
const urlLanguage = LRLanguage.define({
parser,
languageData: {},
});
export function pairs() {
return new LanguageSupport(urlLanguage, []);
}

View File

@@ -0,0 +1,7 @@
import { styleTags, tags as t } from '@lezer/highlight';
export const highlight = styleTags({
Sep: t.bracket,
Key: t.attributeName,
Value: t.string,
});

View File

@@ -0,0 +1,9 @@
@top pairs { (Key? Sep Value)* }
@tokens {
Sep { ":" }
Key { ![:]+ }
Value { ![\n]+ }
}
@external propSource highlight from "./highlight"

View File

@@ -0,0 +1,19 @@
// This file was generated by lezer-generator. You probably shouldn't edit it.
import {LRParser} from "@lezer/lr"
import {highlight} from "./highlight"
export const parser = LRParser.deserialize({
version: 14,
states: "!QQQOPOOOYOQO'#CaO_OPO'#CaQQOPOOOOOO,58{,58{OdOQO,58{OOOO-E6_-E6_OOOO1G.g1G.g",
stateData: "i~OQQORPO~OSSO~ORTO~OSVO~O",
goto: "]UPPPPPVQRORUR",
nodeNames: "⚠ pairs Key Sep Value",
maxTerm: 6,
propSources: [highlight],
skippedNodes: [0],
repeatNodeCount: 1,
tokenData: "#oRRVOYhYZ!UZ![h![!]#[!];'Sh;'S;=`#U<%lOhRoVQPSQOYhYZ!UZ![h![!]!m!];'Sh;'S;=`#U<%lOhP!ZSQPO![!U!];'S!U;'S;=`!g<%lO!UP!jP;=`<%l!UQ!rSSQOY!mZ;'S!m;'S;=`#O<%lO!mQ#RP;=`<%l!mR#XP;=`<%lhR#cSRPSQOY!mZ;'S!m;'S;=`#O<%lO!m",
tokenizers: [0, 1],
topRules: {"pairs":[0,1]},
tokenPrec: 0
})

View File

@@ -9,7 +9,7 @@ export function FormattedError({ children }: Props) {
return (
<pre
className={classNames(
'w-full select-auto cursor-text bg-background-highlight-secondary p-3 rounded',
'font-mono text-sm w-full select-auto cursor-text bg-background-highlight-secondary p-3 rounded',
'whitespace-pre-wrap border border-fg-danger border-dashed overflow-x-auto',
)}
>

View File

@@ -1,11 +1,13 @@
import classNames from 'classnames';
import type { HotkeyAction } from '../../hooks/useHotKey';
import { useHotKeyLabel } from '../../hooks/useHotKey';
interface Props {
action: HotkeyAction;
className?: string;
}
export function HotKeyLabel({ action }: Props) {
export function HotKeyLabel({ action, className }: Props) {
const label = useHotKeyLabel(action);
return <span className="text-fg-subtle whitespace-nowrap">{label}</span>;
return <span className={classNames(className, 'text-fg-subtle whitespace-nowrap')}>{label}</span>;
}

View File

@@ -1,26 +1,27 @@
import React from 'react';
import classNames from 'classnames';
import React, { Fragment } from 'react';
import type { HotkeyAction } from '../../hooks/useHotKey';
import { HotKey } from './HotKey';
import { HotKeyLabel } from './HotKeyLabel';
import { HStack, VStack } from './Stacks';
interface Props {
hotkeys: HotkeyAction[];
bottomSlot?: React.ReactNode;
className?: string;
}
export const HotKeyList = ({ hotkeys, bottomSlot }: Props) => {
export const HotKeyList = ({ hotkeys, bottomSlot, className }: Props) => {
return (
<div className="h-full flex items-center justify-center">
<VStack space={2}>
<div className={classNames(className, 'h-full flex items-center justify-center')}>
<div className="px-4 grid gap-2 grid-cols-[auto_auto]">
{hotkeys.map((hotkey) => (
<HStack key={hotkey} className="grid grid-cols-2">
<HotKeyLabel action={hotkey} />
<HotKey className="ml-auto" action={hotkey} />
</HStack>
<Fragment key={hotkey}>
<HotKeyLabel className="truncate" action={hotkey} />
<HotKey className="ml-4" action={hotkey} />
</Fragment>
))}
{bottomSlot}
</VStack>
</div>
</div>
);
};

View File

@@ -4,9 +4,10 @@ import type { GrpcRequest, HttpRequest } from '../../lib/models';
interface Props {
request: HttpRequest | GrpcRequest;
className?: string;
shortNames?: boolean;
}
const methodMap: Record<string, string> = {
const longMethodMap = {
get: 'GET',
put: 'PUT',
post: 'POST',
@@ -15,9 +16,20 @@ const methodMap: Record<string, string> = {
options: 'OPTIONS',
head: 'HEAD',
grpc: 'GRPC',
} as const;
const shortMethodMap: Record<keyof typeof longMethodMap, string> = {
get: 'GET',
put: 'PUT',
post: 'POST',
patch: 'PTCH',
delete: 'DEL',
options: 'OPTS',
head: 'HEAD',
grpc: 'GRPC',
};
export function HttpMethodTag({ request, className }: Props) {
export function HttpMethodTag({ shortNames, request, className }: Props) {
const method =
request.model === 'http_request' && request.bodyType === 'graphql'
? 'GQL'
@@ -26,9 +38,17 @@ export function HttpMethodTag({ request, className }: Props) {
: request.method;
const m = method.toLowerCase();
const methodMap: Record<string, string> = shortNames ? shortMethodMap : longMethodMap;
return (
<span className={classNames(className, 'text-xs font-mono text-fg-subtle')}>
{methodMap[m] ?? m.slice(0, 3).toUpperCase()}
<span
className={classNames(
className,
'text-xs font-mono text-fg-subtle',
'pt-[0.25em]', // Fix for monospace font not vertically centering
shortNames && 'w-[2.5em]',
)}
>
{methodMap[m] ?? m.slice(0, 4).toUpperCase()}
</span>
);
}

Some files were not shown because too many files have changed in this diff Show More