mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-02-16 15:47:47 +01:00
Compare commits
78 Commits
v2023.3.0-
...
v2024.0.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
efce69292d | ||
|
|
0ccc893440 | ||
|
|
1f9756c917 | ||
|
|
be8f0e4521 | ||
|
|
bcdf51d231 | ||
|
|
1a1553eebd | ||
|
|
321c3862fe | ||
|
|
466d412e65 | ||
|
|
86f50b826f | ||
|
|
ac1e646e68 | ||
|
|
33374eefc7 | ||
|
|
7047df4f7e | ||
|
|
c8bd4d0ae0 | ||
|
|
1e79f76701 | ||
|
|
18852dca06 | ||
|
|
408e7e80b7 | ||
|
|
fc185de023 | ||
|
|
bb9d3a42f3 | ||
|
|
baf0f4291d | ||
|
|
536066142c | ||
|
|
04cf16497d | ||
|
|
feb5972090 | ||
|
|
77bf5a58d8 | ||
|
|
3539642491 | ||
|
|
08abea6a6f | ||
|
|
0045b85f00 | ||
|
|
4b34c3d101 | ||
|
|
4af0a15d9f | ||
|
|
3a4a76c58d | ||
|
|
3086d815c1 | ||
|
|
a48a9eab4a | ||
|
|
48664c66e5 | ||
|
|
7aee5176a9 | ||
|
|
0da68ced18 | ||
|
|
39f7d9c113 | ||
|
|
138943bfb6 | ||
|
|
c1c9f882a6 | ||
|
|
1bcf26f656 | ||
|
|
7c2466da5e | ||
|
|
7dc78a1f6f | ||
|
|
88d024023b | ||
|
|
626aacf982 | ||
|
|
d5855c45a6 | ||
|
|
793bff9f27 | ||
|
|
88ea68e72f | ||
|
|
35e40d2c55 | ||
|
|
c472b83409 | ||
|
|
52c26d235c | ||
|
|
ac54729012 | ||
|
|
0586034ef4 | ||
|
|
91790ba708 | ||
|
|
d8ab6c0b50 | ||
|
|
b600a21a2b | ||
|
|
4f9d1278f7 | ||
|
|
15aa93f5f9 | ||
|
|
c7798092d8 | ||
|
|
5560593aaa | ||
|
|
66639e651d | ||
|
|
8e42d5ccdb | ||
|
|
5c62594087 | ||
|
|
26b6c48657 | ||
|
|
0290aba982 | ||
|
|
0bafc4e4f5 | ||
|
|
9a36f94279 | ||
|
|
1d8e66179e | ||
|
|
fda6d16d8e | ||
|
|
c4737916df | ||
|
|
919465cdbb | ||
|
|
de3730fa4f | ||
|
|
aff26fdd46 | ||
|
|
3c0edf06af | ||
|
|
cb8939db88 | ||
|
|
bf4b3213c4 | ||
|
|
633d7c52c4 | ||
|
|
0401cb92aa | ||
|
|
bff6c668a0 | ||
|
|
ee87e65763 | ||
|
|
f165a0b827 |
5
.github/workflows/artifacts.yml
vendored
5
.github/workflows/artifacts.yml
vendored
@@ -17,9 +17,8 @@ jobs:
|
||||
target: x86_64-apple-darwin
|
||||
- os: windows-2022
|
||||
target: x86_64-pc-windows-msvc
|
||||
# # Re-enable Linux when context menu is supported
|
||||
# - os: ubuntu-20.04
|
||||
# target: x86_64-unknown-linux-gnu
|
||||
- os: ubuntu-20.04
|
||||
target: x86_64-unknown-linux-gnu
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
node_modules/
|
||||
dist/
|
||||
out/
|
||||
.prettierrc.cjs
|
||||
|
||||
20
.run/Build Desktop.run.xml
Normal file
20
.run/Build Desktop.run.xml
Normal file
@@ -0,0 +1,20 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Build Desktop" type="ShConfigurationType">
|
||||
<option name="SCRIPT_TEXT" value="npm run tauri build -- --target universal-apple-darwin" />
|
||||
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
|
||||
<option name="SCRIPT_PATH" value="" />
|
||||
<option name="SCRIPT_OPTIONS" value="" />
|
||||
<option name="INDEPENDENT_SCRIPT_WORKING_DIRECTORY" value="true" />
|
||||
<option name="SCRIPT_WORKING_DIRECTORY" value="$PROJECT_DIR$" />
|
||||
<option name="INDEPENDENT_INTERPRETER_PATH" value="true" />
|
||||
<option name="INTERPRETER_PATH" value="/bin/zsh" />
|
||||
<option name="INTERPRETER_OPTIONS" value="" />
|
||||
<option name="EXECUTE_IN_TERMINAL" value="true" />
|
||||
<option name="EXECUTE_SCRIPT_FILE" value="false" />
|
||||
<envs>
|
||||
<env name="TAURI_KEY_PASSWORD" value="fishhook-upstream-wash-assured" />
|
||||
<env name="TAURI_PRIVATE_KEY" value="dW50cnVzdGVkIGNvbW1lbnQ6IHJzaWduIGVuY3J5cHRlZCBzZWNyZXQga2V5ClJXUlRZMEl5OGxWaytTa3dIa2xXVUltQzRGUXIzd2lYQ2NpV0ZhQURSbWJWZ1NrK0tnY0FBQkFBQUFBQUFBQUFBQUlBQUFBQUV2M1VKdVRyVHpHSzhQdGc2ZVFtOVNsMU5tNEVSN280cFNrbXhncW9tdjNXaFJZUTJqUzQ5Q01zWTJWRVhaY1pGNHNjR1NFR3JmcWFRN09NdWdGMXpZVXhzejR4V3lDV1JpZHlnbW5LNS9vMFFtRlZjbUl4YjZSNzhlMmk3ait5SExYcG5QZUkxOFE9Cg==" />
|
||||
</envs>
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
13
.run/Dev Desktop.run.xml
Normal file
13
.run/Dev Desktop.run.xml
Normal file
@@ -0,0 +1,13 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Dev Desktop" type="js.build_tools.npm">
|
||||
<package-json value="$PROJECT_DIR$/package.json" />
|
||||
<command value="run" />
|
||||
<scripts>
|
||||
<script value="start" />
|
||||
</scripts>
|
||||
<node-interpreter value="project" />
|
||||
<envs>
|
||||
</envs>
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
11
index.html
11
index.html
@@ -5,17 +5,6 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Yaak App</title>
|
||||
<!-- <script src="http://localhost:8097"></script>-->
|
||||
<style>
|
||||
body {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background-color: black;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
1696
package-lock.json
generated
1696
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
25
package.json
25
package.json
@@ -5,19 +5,21 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "npm run build:plugins && npm run tauri-dev",
|
||||
"tauri-dev": "YAAK_ENV=development tauri dev --no-watch --config src-tauri/tauri-dev.conf.json",
|
||||
"tauri-dev": "tauri dev --no-watch --config ./src-tauri/tauri-dev.conf.json",
|
||||
"tauri-build": "tauri build",
|
||||
"tauri": "tauri",
|
||||
"build": "npm run build:frontend",
|
||||
"dev": "vite dev",
|
||||
"lint": "tsc && eslint . --ext .ts,.tsx",
|
||||
"build:icon:release": "tauri icon design/icon.png --output src-tauri/icons/release",
|
||||
"build:icon:dev": "tauri icon design/icon-dev.png --output src-tauri/icons/dev",
|
||||
"build:icon:release": "tauri icon design/icon.png --output ./src-tauri/icons/release",
|
||||
"build:icon:dev": "tauri icon design/icon-dev.png --output ./src-tauri/icons/dev",
|
||||
"build:frontend": "vite build",
|
||||
"build:plugins": "run-p build:plugin:importer-insomnia build:plugin:importer-postman build:plugin:importer-yaak",
|
||||
"build:plugin:importer-insomnia": "cd src-tauri/plugins/importer-insomnia && vite build",
|
||||
"build:plugin:importer-postman": "cd src-tauri/plugins/importer-postman && vite build",
|
||||
"build:plugin:importer-yaak": "cd src-tauri/plugins/importer-yaak && vite build",
|
||||
"build:plugins": "run-p build:plugin:*",
|
||||
"build:plugin:importer-insomnia": "cd plugins/importer-insomnia && vite build --emptyOutDir",
|
||||
"build:plugin:importer-postman": "cd plugins/importer-postman && vite build --emptyOutDir",
|
||||
"build:plugin:importer-yaak": "cd plugins/importer-yaak && vite build --emptyOutDir",
|
||||
"build:plugin:filter-jsonpath": "cd plugins/filter-jsonpath && vite build --emptyOutDir",
|
||||
"build:plugin:filter-xpath": "cd plugins/filter-xpath && vite build --emptyOutDir",
|
||||
"test": "vitest",
|
||||
"coverage": "vitest run --coverage",
|
||||
"prepare": "husky install"
|
||||
@@ -32,14 +34,13 @@
|
||||
"@lezer/generator": "^1.2.2",
|
||||
"@lezer/highlight": "^1.1.3",
|
||||
"@lezer/lr": "^1.3.3",
|
||||
"@radix-ui/react-icons": "^1.2.0",
|
||||
"@react-hook/resize-observer": "^1.2.6",
|
||||
"@tailwindcss/container-queries": "^0.1.0",
|
||||
"@tanstack/query-sync-storage-persister": "^4.27.1",
|
||||
"@tanstack/react-query": "^4.28.0",
|
||||
"@tanstack/react-query-devtools": "^4.28.0",
|
||||
"@tanstack/react-query-persist-client": "^4.28.0",
|
||||
"@tauri-apps/api": "^1.5.1",
|
||||
"@tauri-apps/api": "^1.5.3",
|
||||
"buffer": "^6.0.3",
|
||||
"classnames": "^2.3.2",
|
||||
"cm6-graphql": "^0.0.9",
|
||||
@@ -47,6 +48,7 @@
|
||||
"focus-trap-react": "^10.1.1",
|
||||
"format-graphql": "^1.4.0",
|
||||
"framer-motion": "^9.0.4",
|
||||
"lucide-react": "^0.309.0",
|
||||
"papaparse": "^5.4.1",
|
||||
"parse-color": "^1.0.0",
|
||||
"react": "^18.2.0",
|
||||
@@ -56,13 +58,13 @@
|
||||
"react-helmet-async": "^1.3.0",
|
||||
"react-router-dom": "^6.8.1",
|
||||
"react-use": "^17.4.0",
|
||||
"tauri-plugin-context-menu": "^0.5.0",
|
||||
"slugify": "^1.6.6",
|
||||
"tauri-plugin-log-api": "github:tauri-apps/tauri-plugin-log#v1",
|
||||
"uuid": "^9.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/nesting": "^0.0.0-insiders.565cd3e",
|
||||
"@tauri-apps/cli": "^1.5.4",
|
||||
"@tauri-apps/cli": "^1.5.6",
|
||||
"@types/node": "^18.7.10",
|
||||
"@types/papaparse": "^5.3.7",
|
||||
"@types/parse-color": "^1.0.1",
|
||||
@@ -86,6 +88,7 @@
|
||||
"postcss": "^8.4.21",
|
||||
"postcss-nesting": "^11.2.1",
|
||||
"prettier": "^2.8.4",
|
||||
"react-devtools": "^4.28.5",
|
||||
"tailwindcss": "^3.2.7",
|
||||
"typescript": "^5.0.2",
|
||||
"vite": "^4.0.0",
|
||||
|
||||
173
plugins/filter-jsonpath/package-lock.json
generated
Normal file
173
plugins/filter-jsonpath/package-lock.json
generated
Normal file
@@ -0,0 +1,173 @@
|
||||
{
|
||||
"name": "filter-jsonpath",
|
||||
"version": "0.0.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "filter-jsonpath",
|
||||
"version": "0.0.1",
|
||||
"dependencies": {
|
||||
"jsonpath": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/deep-is": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
|
||||
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="
|
||||
},
|
||||
"node_modules/escodegen": {
|
||||
"version": "1.14.3",
|
||||
"resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz",
|
||||
"integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==",
|
||||
"dependencies": {
|
||||
"esprima": "^4.0.1",
|
||||
"estraverse": "^4.2.0",
|
||||
"esutils": "^2.0.2",
|
||||
"optionator": "^0.8.1"
|
||||
},
|
||||
"bin": {
|
||||
"escodegen": "bin/escodegen.js",
|
||||
"esgenerate": "bin/esgenerate.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"source-map": "~0.6.1"
|
||||
}
|
||||
},
|
||||
"node_modules/escodegen/node_modules/esprima": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
|
||||
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
|
||||
"bin": {
|
||||
"esparse": "bin/esparse.js",
|
||||
"esvalidate": "bin/esvalidate.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/esprima": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/esprima/-/esprima-1.2.2.tgz",
|
||||
"integrity": "sha512-+JpPZam9w5DuJ3Q67SqsMGtiHKENSMRVoxvArfJZK01/BfLEObtZ6orJa/MtoGNR/rfMgp5837T41PAmTwAv/A==",
|
||||
"bin": {
|
||||
"esparse": "bin/esparse.js",
|
||||
"esvalidate": "bin/esvalidate.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/estraverse": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz",
|
||||
"integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/esutils": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
|
||||
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-levenshtein": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
|
||||
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="
|
||||
},
|
||||
"node_modules/jsonpath": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/jsonpath/-/jsonpath-1.1.1.tgz",
|
||||
"integrity": "sha512-l6Cg7jRpixfbgoWgkrl77dgEj8RPvND0wMH6TwQmi9Qs4TFfS9u5cUFnbeKTwj5ga5Y3BTGGNI28k117LJ009w==",
|
||||
"dependencies": {
|
||||
"esprima": "1.2.2",
|
||||
"static-eval": "2.0.2",
|
||||
"underscore": "1.12.1"
|
||||
}
|
||||
},
|
||||
"node_modules/levn": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz",
|
||||
"integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==",
|
||||
"dependencies": {
|
||||
"prelude-ls": "~1.1.2",
|
||||
"type-check": "~0.3.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/optionator": {
|
||||
"version": "0.8.3",
|
||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz",
|
||||
"integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==",
|
||||
"dependencies": {
|
||||
"deep-is": "~0.1.3",
|
||||
"fast-levenshtein": "~2.0.6",
|
||||
"levn": "~0.3.0",
|
||||
"prelude-ls": "~1.1.2",
|
||||
"type-check": "~0.3.2",
|
||||
"word-wrap": "~1.2.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prelude-ls": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz",
|
||||
"integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==",
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/static-eval": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/static-eval/-/static-eval-2.0.2.tgz",
|
||||
"integrity": "sha512-N/D219Hcr2bPjLxPiV+TQE++Tsmrady7TqAJugLy7Xk1EumfDWS/f5dtBbkRCGE7wKKXuYockQoj8Rm2/pVKyg==",
|
||||
"dependencies": {
|
||||
"escodegen": "^1.8.1"
|
||||
}
|
||||
},
|
||||
"node_modules/type-check": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz",
|
||||
"integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==",
|
||||
"dependencies": {
|
||||
"prelude-ls": "~1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/underscore": {
|
||||
"version": "1.12.1",
|
||||
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.12.1.tgz",
|
||||
"integrity": "sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw=="
|
||||
},
|
||||
"node_modules/word-wrap": {
|
||||
"version": "1.2.5",
|
||||
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
||||
"integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
7
plugins/filter-jsonpath/package.json
Normal file
7
plugins/filter-jsonpath/package.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "filter-jsonpath",
|
||||
"version": "0.0.1",
|
||||
"dependencies": {
|
||||
"jsonpath": "^1.1.1"
|
||||
}
|
||||
}
|
||||
12
plugins/filter-jsonpath/src/index.js
Normal file
12
plugins/filter-jsonpath/src/index.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import jp from 'jsonpath';
|
||||
|
||||
export function pluginHookResponseFilter(filter, text) {
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(text);
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
const filtered = jp.query(parsed, filter);
|
||||
return { filtered: JSON.stringify(filtered, null, 2) };
|
||||
}
|
||||
13
plugins/filter-jsonpath/vite.config.js
Normal file
13
plugins/filter-jsonpath/vite.config.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import { resolve } from 'path';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
lib: {
|
||||
entry: resolve(__dirname, 'src/index.js'),
|
||||
fileName: 'index',
|
||||
formats: ['es'],
|
||||
},
|
||||
outDir: resolve(__dirname, '../../src-tauri/plugins/filter-jsonpath'),
|
||||
},
|
||||
});
|
||||
32
plugins/filter-xpath/package-lock.json
generated
Normal file
32
plugins/filter-xpath/package-lock.json
generated
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "filter-xpath",
|
||||
"version": "0.0.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "filter-xpath",
|
||||
"version": "0.0.1",
|
||||
"dependencies": {
|
||||
"@xmldom/xmldom": "^0.8.10",
|
||||
"xpath": "^0.0.34"
|
||||
}
|
||||
},
|
||||
"node_modules/@xmldom/xmldom": {
|
||||
"version": "0.8.10",
|
||||
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz",
|
||||
"integrity": "sha512-2WALfTl4xo2SkGCYRt6rDTFfk9R1czmBvUQy12gK2KuRKIpWEhcbbzy8EZXtz/jkRqHX8bFEc6FC1HjX4TUWYw==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/xpath": {
|
||||
"version": "0.0.34",
|
||||
"resolved": "https://registry.npmjs.org/xpath/-/xpath-0.0.34.tgz",
|
||||
"integrity": "sha512-FxF6+rkr1rNSQrhUNYrAFJpRXNzlDoMxeXN5qI84939ylEv3qqPFKa85Oxr6tDaJKqwW6KKyo2v26TSv3k6LeA==",
|
||||
"engines": {
|
||||
"node": ">=0.6.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
8
plugins/filter-xpath/package.json
Normal file
8
plugins/filter-xpath/package.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "filter-xpath",
|
||||
"version": "0.0.1",
|
||||
"dependencies": {
|
||||
"@xmldom/xmldom": "^0.8.10",
|
||||
"xpath": "^0.0.34"
|
||||
}
|
||||
}
|
||||
8
plugins/filter-xpath/src/index.js
Normal file
8
plugins/filter-xpath/src/index.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import xpath from 'xpath';
|
||||
import { DOMParser } from '@xmldom/xmldom';
|
||||
|
||||
export function pluginHookResponseFilter(filter, text) {
|
||||
const doc = new DOMParser().parseFromString(text, 'text/xml');
|
||||
const filtered = `${xpath.select(filter, doc)}`;
|
||||
return { filtered };
|
||||
}
|
||||
@@ -8,6 +8,6 @@ export default defineConfig({
|
||||
fileName: 'index',
|
||||
formats: ['es'],
|
||||
},
|
||||
outDir: resolve(__dirname, 'out'),
|
||||
outDir: resolve(__dirname, '../../src-tauri/plugins/filter-xpath'),
|
||||
},
|
||||
});
|
||||
4
plugins/importer-insomnia/package.json
Normal file
4
plugins/importer-insomnia/package.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "importer-insomnia",
|
||||
"version": "0.0.1"
|
||||
}
|
||||
13
plugins/importer-insomnia/vite.config.js
Normal file
13
plugins/importer-insomnia/vite.config.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import { resolve } from 'path';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
lib: {
|
||||
entry: resolve(__dirname, 'src/index.js'),
|
||||
fileName: 'index',
|
||||
formats: ['es'],
|
||||
},
|
||||
outDir: resolve(__dirname, '../../src-tauri/plugins/importer-insomnia'),
|
||||
},
|
||||
});
|
||||
4
plugins/importer-postman/package.json
Normal file
4
plugins/importer-postman/package.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "importer-postman",
|
||||
"version": "0.0.1"
|
||||
}
|
||||
@@ -32,7 +32,7 @@ export function pluginHookImport(contents: string): { resources: ExportResources
|
||||
|
||||
const workspace: ExportResources['workspaces'][0] = {
|
||||
model: 'workspace',
|
||||
id: 'wrk_0',
|
||||
id: generateId('wk'),
|
||||
name: info.name || 'Postman Import',
|
||||
description: info.description || '',
|
||||
};
|
||||
@@ -43,7 +43,7 @@ export function pluginHookImport(contents: string): { resources: ExportResources
|
||||
const folder: ExportResources['folders'][0] = {
|
||||
model: 'folder',
|
||||
workspaceId: workspace.id,
|
||||
id: `fld_${exportResources.folders.length}`,
|
||||
id: generateId('fl'),
|
||||
name: v.name,
|
||||
folderId,
|
||||
};
|
||||
@@ -57,7 +57,7 @@ export function pluginHookImport(contents: string): { resources: ExportResources
|
||||
const authPatch = importAuth(r.auth);
|
||||
const request: ExportResources['requests'][0] = {
|
||||
model: 'http_request',
|
||||
id: `req_${exportResources.requests.length}`,
|
||||
id: generateId('rq'),
|
||||
workspaceId: workspace.id,
|
||||
folderId,
|
||||
name: v.name,
|
||||
@@ -89,7 +89,7 @@ export function pluginHookImport(contents: string): { resources: ExportResources
|
||||
importItem(item);
|
||||
}
|
||||
|
||||
return { resources: exportResources };
|
||||
return { resources: convertTemplateSyntax(exportResources) };
|
||||
}
|
||||
|
||||
function importAuth(
|
||||
@@ -131,7 +131,7 @@ function importBody(rawBody: any): Pick<HttpRequest, 'body' | 'bodyType' | 'head
|
||||
),
|
||||
},
|
||||
};
|
||||
} else if ('formdata' in body) {
|
||||
} else if ('urlencoded' in body) {
|
||||
return {
|
||||
headers: [
|
||||
{
|
||||
@@ -142,13 +142,39 @@ function importBody(rawBody: any): Pick<HttpRequest, 'body' | 'bodyType' | 'head
|
||||
],
|
||||
bodyType: 'application/x-www-form-urlencoded',
|
||||
body: {
|
||||
form: toArray(body.formdata).map((f) => ({
|
||||
form: toArray(body.urlencoded).map((f) => ({
|
||||
enabled: !f.disabled,
|
||||
name: f.key ?? '',
|
||||
value: f.value ?? '',
|
||||
})),
|
||||
},
|
||||
};
|
||||
} else if ('formdata' in body) {
|
||||
return {
|
||||
headers: [
|
||||
{
|
||||
name: 'Content-Type',
|
||||
value: 'multipart/form-data',
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
bodyType: 'multipart/form-data',
|
||||
body: {
|
||||
form: toArray(body.formdata).map((f) =>
|
||||
f.src != null
|
||||
? {
|
||||
enabled: !f.disabled,
|
||||
name: f.key ?? '',
|
||||
file: f.src ?? '',
|
||||
}
|
||||
: {
|
||||
enabled: !f.disabled,
|
||||
name: f.key ?? '',
|
||||
value: f.value ?? '',
|
||||
},
|
||||
),
|
||||
},
|
||||
};
|
||||
} else {
|
||||
// TODO: support other body types
|
||||
return { headers: [], bodyType: null, body: {} };
|
||||
@@ -171,3 +197,27 @@ function toArray(value: any): any[] {
|
||||
if (Object.prototype.toString.call(value) === '[object Array]') return value;
|
||||
else return [];
|
||||
}
|
||||
|
||||
/** Recursively render all nested object properties */
|
||||
function convertTemplateSyntax<T>(obj: T): T {
|
||||
if (typeof obj === 'string') {
|
||||
return obj.replace(/{{\s*(_\.)?([^}]+)\s*}}/g, '${[$2]}') as T;
|
||||
} else if (Array.isArray(obj) && obj != null) {
|
||||
return obj.map(convertTemplateSyntax) as T;
|
||||
} else if (typeof obj === 'object' && obj != null) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(obj).map(([k, v]) => [k, convertTemplateSyntax(v)]),
|
||||
) as T;
|
||||
} else {
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
|
||||
function generateId(prefix: 'wk' | 'rq' | 'fl'): string {
|
||||
const alphabet = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
let id = `${prefix}_`;
|
||||
for (let i = 0; i < 10; i++) {
|
||||
id += alphabet[Math.floor(Math.random() * alphabet.length)];
|
||||
}
|
||||
return id;
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"lib": [
|
||||
"ES2020"
|
||||
"ESNext",
|
||||
],
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
@@ -18,6 +18,6 @@
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
"./src"
|
||||
]
|
||||
}
|
||||
@@ -8,6 +8,6 @@ export default defineConfig({
|
||||
fileName: 'index',
|
||||
formats: ['es'],
|
||||
},
|
||||
outDir: resolve(__dirname, 'out'),
|
||||
outDir: resolve(__dirname, '../../src-tauri/plugins/importer-postman'),
|
||||
},
|
||||
});
|
||||
4
plugins/importer-yaak/package.json
Normal file
4
plugins/importer-yaak/package.json
Normal file
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"name": "importer-yaak",
|
||||
"version": "0.0.1"
|
||||
}
|
||||
@@ -8,6 +8,6 @@ export default defineConfig({
|
||||
fileName: 'index',
|
||||
formats: ['es'],
|
||||
},
|
||||
outDir: resolve(__dirname, 'out'),
|
||||
outDir: resolve(__dirname, '../../src-tauri/plugins/importer-yaak'),
|
||||
},
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT id, model, workspace_id, request_id, updated_at, created_at, url,\n status, status_reason, content_length, body, body_path, elapsed, error,\n headers AS \"headers!: sqlx::types::Json<Vec<HttpResponseHeader>>\"\n FROM http_responses\n WHERE workspace_id = ?\n ORDER BY created_at DESC\n ",
|
||||
"query": "\n SELECT id, model, workspace_id, request_id, updated_at, created_at, url,\n status, status_reason, content_length, body_path, elapsed, error,\n headers AS \"headers!: sqlx::types::Json<Vec<HttpResponseHeader>>\"\n FROM http_responses\n WHERE request_id = ?\n ORDER BY created_at DESC\n LIMIT ?\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -53,34 +53,29 @@
|
||||
"ordinal": 9,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "body",
|
||||
"ordinal": 10,
|
||||
"type_info": "Blob"
|
||||
},
|
||||
{
|
||||
"name": "body_path",
|
||||
"ordinal": 11,
|
||||
"ordinal": 10,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "elapsed",
|
||||
"ordinal": 12,
|
||||
"ordinal": 11,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "error",
|
||||
"ordinal": 13,
|
||||
"ordinal": 12,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "headers!: sqlx::types::Json<Vec<HttpResponseHeader>>",
|
||||
"ordinal": 14,
|
||||
"ordinal": 13,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
@@ -94,11 +89,10 @@
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "26072725d536c3cfdffd9a681d17c0ee2f246ca98e0459630a2430236d3bbdd2"
|
||||
"hash": "07b0c398efd1d5f8f479652de658716a9e7faef6aba6583dd209a4f290c5edd1"
|
||||
}
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n INSERT INTO http_responses (\n id,\n request_id,\n workspace_id,\n elapsed,\n url,\n status,\n status_reason,\n content_length,\n body,\n body_path,\n headers\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);\n ",
|
||||
"query": "\n INSERT INTO http_responses (\n id,\n request_id,\n workspace_id,\n elapsed,\n url,\n status,\n status_reason,\n content_length,\n body_path,\n headers\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 11
|
||||
"Right": 10
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "8947a2a90478277c42fe9b06bc1fa98197642a4d281a3dbc101be2c9c1fec36c"
|
||||
"hash": "198bd086ccc87d2e6c24cb1c717f486d3ab58c0c958ede850c018fc266eade87"
|
||||
}
|
||||
74
src-tauri/.sqlx/query-20cf0f8b71e600bc40ee204b60a12b2f3728178f3b8788d2e5cc92821b74d470.json
generated
Normal file
74
src-tauri/.sqlx/query-20cf0f8b71e600bc40ee204b60a12b2f3728178f3b8788d2e5cc92821b74d470.json
generated
Normal file
@@ -0,0 +1,74 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT\n id,\n model,\n created_at,\n updated_at,\n name,\n description,\n setting_request_timeout,\n setting_follow_redirects,\n setting_validate_certificates,\n variables AS \"variables!: sqlx::types::Json<Vec<EnvironmentVariable>>\"\n FROM workspaces\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "model",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "created_at",
|
||||
"ordinal": 2,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "updated_at",
|
||||
"ordinal": 3,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"ordinal": 4,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "description",
|
||||
"ordinal": 5,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "setting_request_timeout",
|
||||
"ordinal": 6,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "setting_follow_redirects",
|
||||
"ordinal": 7,
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"name": "setting_validate_certificates",
|
||||
"ordinal": 8,
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"name": "variables!: sqlx::types::Json<Vec<EnvironmentVariable>>",
|
||||
"ordinal": 9,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 0
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "20cf0f8b71e600bc40ee204b60a12b2f3728178f3b8788d2e5cc92821b74d470"
|
||||
}
|
||||
12
src-tauri/.sqlx/query-294cbe19f9ddd9519ace3558df4308948082ec0ce7096855aa7d8fba519b8b4f.json
generated
Normal file
12
src-tauri/.sqlx/query-294cbe19f9ddd9519ace3558df4308948082ec0ce7096855aa7d8fba519b8b4f.json
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n UPDATE http_responses SET (\n elapsed,\n url,\n status,\n status_reason,\n content_length,\n body_path,\n error,\n headers,\n updated_at\n ) = (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) WHERE id = ?;\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 9
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "294cbe19f9ddd9519ace3558df4308948082ec0ce7096855aa7d8fba519b8b4f"
|
||||
}
|
||||
12
src-tauri/.sqlx/query-2c181a4dc13efc52fe6a5a68291c5678a9624020df4ea744e78396f6926d5c88.json
generated
Normal file
12
src-tauri/.sqlx/query-2c181a4dc13efc52fe6a5a68291c5678a9624020df4ea744e78396f6926d5c88.json
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n INSERT INTO settings (id)\n VALUES ('default')\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 0
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "2c181a4dc13efc52fe6a5a68291c5678a9624020df4ea744e78396f6926d5c88"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT id, model, created_at, updated_at, name, description,\n variables AS \"variables!: sqlx::types::Json<Vec<EnvironmentVariable>>\"\n FROM workspaces\n ",
|
||||
"query": "\n SELECT\n id,\n model,\n created_at,\n updated_at,\n theme,\n appearance,\n update_channel\n FROM settings\n WHERE id = 'default'\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -24,17 +24,17 @@
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"name": "theme",
|
||||
"ordinal": 4,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "description",
|
||||
"name": "appearance",
|
||||
"ordinal": 5,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "variables!: sqlx::types::Json<Vec<EnvironmentVariable>>",
|
||||
"name": "update_channel",
|
||||
"ordinal": 6,
|
||||
"type_info": "Text"
|
||||
}
|
||||
@@ -52,5 +52,5 @@
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "5588db23df7f30dc75857e05395ebbcf2384e2ac0d7cb87f76d74c6d50781d7b"
|
||||
"hash": "3b3fb6271340c6ec21a10b4f1b20502c86c425e0b53ac07692f8a4ed0be09335"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT id, model, workspace_id, request_id, updated_at, created_at, url,\n status, status_reason, content_length, body, body_path, elapsed, error,\n headers AS \"headers!: sqlx::types::Json<Vec<HttpResponseHeader>>\"\n FROM http_responses\n WHERE id = ?\n ",
|
||||
"query": "\n SELECT id, model, workspace_id, request_id, updated_at, created_at, url,\n status, status_reason, content_length, body_path, elapsed, error,\n headers AS \"headers!: sqlx::types::Json<Vec<HttpResponseHeader>>\"\n FROM http_responses\n WHERE workspace_id = ?\n ORDER BY created_at DESC\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -53,29 +53,24 @@
|
||||
"ordinal": 9,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "body",
|
||||
"ordinal": 10,
|
||||
"type_info": "Blob"
|
||||
},
|
||||
{
|
||||
"name": "body_path",
|
||||
"ordinal": 11,
|
||||
"ordinal": 10,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "elapsed",
|
||||
"ordinal": 12,
|
||||
"ordinal": 11,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "error",
|
||||
"ordinal": 13,
|
||||
"ordinal": 12,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "headers!: sqlx::types::Json<Vec<HttpResponseHeader>>",
|
||||
"ordinal": 14,
|
||||
"ordinal": 13,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
@@ -94,11 +89,10 @@
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "c23c61b05a4c9e04ab0c1fc2c579d6f2a82a37aeed8addf9861b4985f2a5422e"
|
||||
"hash": "3d199d371be948211f4a50c869b307f5df60784293c52397d77a187633a406dd"
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n INSERT INTO workspaces (id, name, description, variables)\n VALUES (?, ?, ?, ?)\n ON CONFLICT (id) DO UPDATE SET\n updated_at = CURRENT_TIMESTAMP,\n name = excluded.name,\n description = excluded.description,\n variables = excluded.variables\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 4
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "610223ad10b6e25926d486ba775a74b55625fcc4e6637d8a805d44ec3f3b9532"
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n UPDATE http_responses SET (\n elapsed,\n url,\n status,\n status_reason,\n content_length,\n body,\n body_path,\n error,\n headers,\n updated_at\n ) = (?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) WHERE id = ?;\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 10
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "62475fd9483fb5eda01c937949da2ef66ac7005b4be06b87aa6210d462348aca"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT id, model, workspace_id, request_id, updated_at, created_at, url,\n status, status_reason, content_length, body, body_path, elapsed, error,\n headers AS \"headers!: sqlx::types::Json<Vec<HttpResponseHeader>>\"\n FROM http_responses\n WHERE request_id = ?\n ORDER BY created_at DESC\n ",
|
||||
"query": "\n SELECT id, model, workspace_id, request_id, updated_at, created_at, url,\n status, status_reason, content_length, body_path, elapsed, error,\n headers AS \"headers!: sqlx::types::Json<Vec<HttpResponseHeader>>\"\n FROM http_responses\n WHERE id = ?\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -53,29 +53,24 @@
|
||||
"ordinal": 9,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "body",
|
||||
"ordinal": 10,
|
||||
"type_info": "Blob"
|
||||
},
|
||||
{
|
||||
"name": "body_path",
|
||||
"ordinal": 11,
|
||||
"ordinal": 10,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "elapsed",
|
||||
"ordinal": 12,
|
||||
"ordinal": 11,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "error",
|
||||
"ordinal": 13,
|
||||
"ordinal": 12,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "headers!: sqlx::types::Json<Vec<HttpResponseHeader>>",
|
||||
"ordinal": 14,
|
||||
"ordinal": 13,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
@@ -94,11 +89,10 @@
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "5aa070e61995f8b1724efaa94c5f0cef5a4be6efda5d70354ad449d7d4b5aee4"
|
||||
"hash": "679a519475adeb50abf046114d3c0d1e48e103f2bb11ef47637d7f0b00ed241f"
|
||||
}
|
||||
12
src-tauri/.sqlx/query-86a9d12d7b00217f3143671908c31c2c6a3c24774a505280dcba169eb5b6b0fb.json
generated
Normal file
12
src-tauri/.sqlx/query-86a9d12d7b00217f3143671908c31c2c6a3c24774a505280dcba169eb5b6b0fb.json
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n UPDATE settings SET (\n theme,\n appearance,\n update_channel\n ) = (?, ?, ?) WHERE id = 'default';\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 3
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "86a9d12d7b00217f3143671908c31c2c6a3c24774a505280dcba169eb5b6b0fb"
|
||||
}
|
||||
12
src-tauri/.sqlx/query-cae4e905515ddea1ec2cd685020241f06b49719085a695b897ef8ad409d2cef2.json
generated
Normal file
12
src-tauri/.sqlx/query-cae4e905515ddea1ec2cd685020241f06b49719085a695b897ef8ad409d2cef2.json
generated
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n INSERT INTO workspaces (\n id,\n name,\n description,\n variables,\n setting_request_timeout,\n setting_follow_redirects,\n setting_validate_certificates\n )\n VALUES (?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT (id) DO UPDATE SET\n updated_at = CURRENT_TIMESTAMP,\n name = excluded.name,\n description = excluded.description,\n variables = excluded.variables,\n setting_request_timeout = excluded.setting_request_timeout,\n setting_follow_redirects = excluded.setting_follow_redirects,\n setting_validate_certificates = excluded.setting_validate_certificates\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 7
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "cae4e905515ddea1ec2cd685020241f06b49719085a695b897ef8ad409d2cef2"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n SELECT id, model, created_at, updated_at, name, description,\n variables AS \"variables!: sqlx::types::Json<Vec<EnvironmentVariable>>\"\n FROM workspaces WHERE id = ?\n ",
|
||||
"query": "\n SELECT\n id,\n model,\n created_at,\n updated_at,\n name,\n description,\n setting_request_timeout,\n setting_follow_redirects,\n setting_validate_certificates,\n variables AS \"variables!: sqlx::types::Json<Vec<EnvironmentVariable>>\"\n FROM workspaces WHERE id = ?\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -34,8 +34,23 @@
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "variables!: sqlx::types::Json<Vec<EnvironmentVariable>>",
|
||||
"name": "setting_request_timeout",
|
||||
"ordinal": 6,
|
||||
"type_info": "Int64"
|
||||
},
|
||||
{
|
||||
"name": "setting_follow_redirects",
|
||||
"ordinal": 7,
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"name": "setting_validate_certificates",
|
||||
"ordinal": 8,
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"name": "variables!: sqlx::types::Json<Vec<EnvironmentVariable>>",
|
||||
"ordinal": 9,
|
||||
"type_info": "Null"
|
||||
}
|
||||
],
|
||||
@@ -49,8 +64,11 @@
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "dbe457087a7bccbca4c1d673aa8e547df04530a7f860a6ccd4e20126a7cdfa4f"
|
||||
"hash": "e08fa4f9b2929f20a01d1dc43d6847a309d3e8c5b324df2d039d1c6e07e6eb2f"
|
||||
}
|
||||
518
src-tauri/Cargo.lock
generated
518
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -17,39 +17,46 @@ tauri-build = { version = "1.2", features = [] }
|
||||
objc = "0.2.7"
|
||||
cocoa = "0.25.0"
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
openssl-sys = { version = "0.9", features = ["vendored"] } # For Ubuntu installation to work
|
||||
|
||||
[dependencies]
|
||||
base64 = "0.21.0"
|
||||
boa_engine = "0.17.3"
|
||||
boa_runtime = "0.17.3"
|
||||
chrono = { version = "0.4.23", features = ["serde"] }
|
||||
boa_engine = { version = "0.17.3", features = ["annex-b"] }
|
||||
boa_runtime = { version = "0.17.3" }
|
||||
chrono = { version = "0.4.31", features = ["serde"] }
|
||||
futures = "0.3.26"
|
||||
http = "0.2.8"
|
||||
rand = "0.8.5"
|
||||
reqwest = { version = "0.11.14", features = ["json", "multipart"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = { version = "1.0", features = ["raw_value"] }
|
||||
reqwest = { version = "0.11.14", features = ["json", "multipart", "gzip", "brotli", "deflate"] }
|
||||
serde = { version = "1.0.195", features = ["derive"] }
|
||||
serde_json = { version = "1.0.111", features = ["raw_value"] }
|
||||
sqlx = { version = "0.7.2", features = ["sqlite", "runtime-tokio-rustls", "json", "chrono", "time"] }
|
||||
tauri = { version = "1.3", features = [
|
||||
"cli",
|
||||
tauri = { version = "1.5.2", features = [
|
||||
"config-toml",
|
||||
"devtools",
|
||||
"dialog-open",
|
||||
"dialog-save",
|
||||
"fs-read-file",
|
||||
"os-all",
|
||||
"protocol-asset",
|
||||
"shell-open",
|
||||
"updater",
|
||||
"window-start-dragging",
|
||||
"dialog-open",
|
||||
"dialog-save",
|
||||
"window-close",
|
||||
"window-maximize",
|
||||
"window-minimize",
|
||||
"window-set-decorations",
|
||||
"window-set-title",
|
||||
"window-start-dragging",
|
||||
"window-unmaximize",
|
||||
] }
|
||||
tauri-plugin-window-state = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
|
||||
tauri-plugin-log = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1", features = ["colored"] }
|
||||
tokio = { version = "1.25.0", features = ["sync"] }
|
||||
uuid = "1.3.0"
|
||||
log = "0.4.20"
|
||||
tauri-plugin-context-menu = "0.5.0"
|
||||
datetime = "0.5.2"
|
||||
window-shadows = "0.2.2"
|
||||
|
||||
[features]
|
||||
# by default Tauri runs in production mode
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
|
||||
<true/>
|
||||
<!-- 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>
|
||||
|
||||
1
src-tauri/migrations/20231122055216_remove_body.sql
Normal file
1
src-tauri/migrations/20231122055216_remove_body.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE http_responses DROP COLUMN body;
|
||||
13
src-tauri/migrations/20240111221224_settings.sql
Normal file
13
src-tauri/migrations/20240111221224_settings.sql
Normal file
@@ -0,0 +1,13 @@
|
||||
CREATE TABLE settings
|
||||
(
|
||||
id TEXT NOT NULL
|
||||
PRIMARY KEY,
|
||||
model TEXT DEFAULT 'settings' NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
follow_redirects BOOLEAN DEFAULT TRUE NOT NULL,
|
||||
validate_certificates BOOLEAN DEFAULT TRUE NOT NULL,
|
||||
request_timeout INTEGER DEFAULT 0 NOT NULL,
|
||||
theme TEXT DEFAULT 'default' NOT NULL,
|
||||
appearance TEXT DEFAULT 'system' NOT NULL
|
||||
);
|
||||
@@ -0,0 +1,9 @@
|
||||
-- Add existing request-related settings to workspace
|
||||
ALTER TABLE workspaces ADD COLUMN setting_request_timeout INTEGER DEFAULT '0' NOT NULL;
|
||||
ALTER TABLE workspaces ADD COLUMN setting_validate_certificates BOOLEAN DEFAULT TRUE NOT NULL;
|
||||
ALTER TABLE workspaces ADD COLUMN setting_follow_redirects BOOLEAN DEFAULT TRUE NOT NULL;
|
||||
|
||||
-- Remove old settings that used to be global
|
||||
ALTER TABLE settings DROP COLUMN request_timeout;
|
||||
ALTER TABLE settings DROP COLUMN follow_redirects;
|
||||
ALTER TABLE settings DROP COLUMN validate_certificates;
|
||||
1
src-tauri/migrations/20240118181105_channel_setting.sql
Normal file
1
src-tauri/migrations/20240118181105_channel_setting.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE settings ADD COLUMN update_channel TEXT DEFAULT 'stable' NOT NULL;
|
||||
3191
src-tauri/plugins/filter-jsonpath/index.mjs
Normal file
3191
src-tauri/plugins/filter-jsonpath/index.mjs
Normal file
File diff suppressed because it is too large
Load Diff
6125
src-tauri/plugins/filter-xpath/index.mjs
Normal file
6125
src-tauri/plugins/filter-xpath/index.mjs
Normal file
File diff suppressed because it is too large
Load Diff
134
src-tauri/plugins/importer-insomnia/index.mjs
Normal file
134
src-tauri/plugins/importer-insomnia/index.mjs
Normal file
@@ -0,0 +1,134 @@
|
||||
function S(e, t) {
|
||||
return console.log("IMPORTING Environment", e._id, e.name, JSON.stringify(e, null, 2)), {
|
||||
id: e._id,
|
||||
createdAt: new Date(e.created ?? Date.now()).toISOString().replace("Z", ""),
|
||||
updatedAt: new Date(e.updated ?? Date.now()).toISOString().replace("Z", ""),
|
||||
workspaceId: t,
|
||||
model: "environment",
|
||||
name: e.name,
|
||||
variables: Object.entries(e.data).map(([n, a]) => ({
|
||||
enabled: !0,
|
||||
name: n,
|
||||
value: `${a}`
|
||||
}))
|
||||
};
|
||||
}
|
||||
function I(e) {
|
||||
return m(e) && e._type === "workspace";
|
||||
}
|
||||
function y(e) {
|
||||
return m(e) && e._type === "request_group";
|
||||
}
|
||||
function g(e) {
|
||||
return m(e) && e._type === "request";
|
||||
}
|
||||
function f(e) {
|
||||
return m(e) && e._type === "environment";
|
||||
}
|
||||
function m(e) {
|
||||
return Object.prototype.toString.call(e) === "[object Object]";
|
||||
}
|
||||
function w(e) {
|
||||
return Object.prototype.toString.call(e) === "[object String]";
|
||||
}
|
||||
function O(e) {
|
||||
return Object.entries(e).map(([t, n]) => ({
|
||||
enabled: !0,
|
||||
name: t,
|
||||
value: `${n}`
|
||||
}));
|
||||
}
|
||||
function l(e) {
|
||||
return w(e) ? e.replaceAll(/{{\s*(_\.)?([^}]+)\s*}}/g, "${[$2]}") : e;
|
||||
}
|
||||
function h(e, t, n = 0) {
|
||||
var c, o;
|
||||
console.log("IMPORTING REQUEST", e._id, e.name, JSON.stringify(e, null, 2));
|
||||
let a = null, r = null;
|
||||
((c = e.body) == null ? void 0 : c.mimeType) === "application/graphql" ? (a = "graphql", r = l(e.body.text)) : ((o = e.body) == null ? void 0 : o.mimeType) === "application/json" && (a = "application/json", r = l(e.body.text));
|
||||
let i = null, u = {};
|
||||
return e.authentication.type === "bearer" ? (i = "bearer", u = {
|
||||
token: l(e.authentication.token)
|
||||
}) : e.authentication.type === "basic" && (i = "basic", u = {
|
||||
username: l(e.authentication.username),
|
||||
password: l(e.authentication.password)
|
||||
}), {
|
||||
id: e._id,
|
||||
createdAt: new Date(e.created ?? Date.now()).toISOString().replace("Z", ""),
|
||||
updatedAt: new Date(e.updated ?? Date.now()).toISOString().replace("Z", ""),
|
||||
workspaceId: t,
|
||||
folderId: e.parentId === t ? null : e.parentId,
|
||||
model: "http_request",
|
||||
sortPriority: n,
|
||||
name: e.name,
|
||||
url: l(e.url),
|
||||
body: r,
|
||||
bodyType: a,
|
||||
authentication: u,
|
||||
authenticationType: i,
|
||||
method: e.method,
|
||||
headers: (e.headers ?? []).map(({ name: d, value: p, disabled: s }) => ({
|
||||
enabled: !s,
|
||||
name: d,
|
||||
value: p
|
||||
})).filter(({ name: d, value: p }) => d !== "" || p !== "")
|
||||
};
|
||||
}
|
||||
function _(e, t) {
|
||||
return console.log("IMPORTING FOLDER", e._id, e.name, JSON.stringify(e, null, 2)), {
|
||||
id: e._id,
|
||||
createdAt: new Date(e.created ?? Date.now()).toISOString().replace("Z", ""),
|
||||
updatedAt: new Date(e.updated ?? Date.now()).toISOString().replace("Z", ""),
|
||||
folderId: e.parentId === t ? null : e.parentId,
|
||||
workspaceId: t,
|
||||
model: "folder",
|
||||
name: e.name
|
||||
};
|
||||
}
|
||||
function b(e) {
|
||||
console.log("RUNNING INSOMNIA");
|
||||
let t;
|
||||
try {
|
||||
t = JSON.parse(e);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
if (!m(t) || !Array.isArray(t.resources))
|
||||
return;
|
||||
const n = {
|
||||
workspaces: [],
|
||||
requests: [],
|
||||
environments: [],
|
||||
folders: []
|
||||
}, a = t.resources.filter(I);
|
||||
for (const r of a) {
|
||||
const i = t.resources.find(
|
||||
(o) => f(o) && o.parentId === r._id
|
||||
);
|
||||
n.workspaces.push({
|
||||
id: r._id,
|
||||
createdAt: new Date(a.created ?? Date.now()).toISOString().replace("Z", ""),
|
||||
updatedAt: new Date(a.updated ?? Date.now()).toISOString().replace("Z", ""),
|
||||
model: "workspace",
|
||||
name: r.name,
|
||||
variables: i ? O(i.data) : []
|
||||
});
|
||||
const u = t.resources.filter(
|
||||
(o) => f(o) && o.parentId === (i == null ? void 0 : i._id)
|
||||
);
|
||||
n.environments.push(
|
||||
...u.map((o) => S(o, r._id))
|
||||
);
|
||||
const c = (o) => {
|
||||
const d = t.resources.filter((s) => s.parentId === o);
|
||||
let p = 0;
|
||||
for (const s of d)
|
||||
y(s) ? (n.folders.push(_(s, r._id)), c(s._id)) : g(s) && n.requests.push(h(s, r._id, p++));
|
||||
};
|
||||
c(r._id);
|
||||
}
|
||||
return n.requests = n.requests.filter(Boolean), n.environments = n.environments.filter(Boolean), n.workspaces = n.workspaces.filter(Boolean), { resources: n };
|
||||
}
|
||||
export {
|
||||
b as pluginHookImport
|
||||
};
|
||||
@@ -1,152 +0,0 @@
|
||||
function S(e, t) {
|
||||
return (
|
||||
console.log('IMPORTING Environment', e._id, e.name, JSON.stringify(e, null, 2)),
|
||||
{
|
||||
id: e._id,
|
||||
createdAt: new Date(e.created ?? Date.now()).toISOString().replace('Z', ''),
|
||||
updatedAt: new Date(e.updated ?? Date.now()).toISOString().replace('Z', ''),
|
||||
workspaceId: t,
|
||||
model: 'environment',
|
||||
name: e.name,
|
||||
variables: Object.entries(e.data).map(([n, a]) => ({
|
||||
enabled: !0,
|
||||
name: n,
|
||||
value: `${a}`,
|
||||
})),
|
||||
}
|
||||
);
|
||||
}
|
||||
function I(e) {
|
||||
return m(e) && e._type === 'workspace';
|
||||
}
|
||||
function y(e) {
|
||||
return m(e) && e._type === 'request_group';
|
||||
}
|
||||
function g(e) {
|
||||
return m(e) && e._type === 'request';
|
||||
}
|
||||
function f(e) {
|
||||
return m(e) && e._type === 'environment';
|
||||
}
|
||||
function m(e) {
|
||||
return Object.prototype.toString.call(e) === '[object Object]';
|
||||
}
|
||||
function w(e) {
|
||||
return Object.prototype.toString.call(e) === '[object String]';
|
||||
}
|
||||
function O(e) {
|
||||
return Object.entries(e).map(([t, n]) => ({
|
||||
enabled: !0,
|
||||
name: t,
|
||||
value: `${n}`,
|
||||
}));
|
||||
}
|
||||
function l(e) {
|
||||
return w(e) ? e.replaceAll(/{{\s*(_\.)?([^}]+)\s*}}/g, '${[$2]}') : e;
|
||||
}
|
||||
function h(e, t, n = 0) {
|
||||
var c, o;
|
||||
console.log('IMPORTING REQUEST', e._id, e.name, JSON.stringify(e, null, 2));
|
||||
let a = null,
|
||||
r = null;
|
||||
((c = e.body) == null ? void 0 : c.mimeType) === 'application/graphql'
|
||||
? ((a = 'graphql'), (r = l(e.body.text)))
|
||||
: ((o = e.body) == null ? void 0 : o.mimeType) === 'application/json' &&
|
||||
((a = 'application/json'), (r = l(e.body.text)));
|
||||
let i = null,
|
||||
u = {};
|
||||
return (
|
||||
e.authentication.type === 'bearer'
|
||||
? ((i = 'bearer'),
|
||||
(u = {
|
||||
token: l(e.authentication.token),
|
||||
}))
|
||||
: e.authentication.type === 'basic' &&
|
||||
((i = 'basic'),
|
||||
(u = {
|
||||
username: l(e.authentication.username),
|
||||
password: l(e.authentication.password),
|
||||
})),
|
||||
{
|
||||
id: e._id,
|
||||
createdAt: new Date(e.created ?? Date.now()).toISOString().replace('Z', ''),
|
||||
updatedAt: new Date(e.updated ?? Date.now()).toISOString().replace('Z', ''),
|
||||
workspaceId: t,
|
||||
folderId: e.parentId === t ? null : e.parentId,
|
||||
model: 'http_request',
|
||||
sortPriority: n,
|
||||
name: e.name,
|
||||
url: l(e.url),
|
||||
body: r,
|
||||
bodyType: a,
|
||||
authentication: u,
|
||||
authenticationType: i,
|
||||
method: e.method,
|
||||
headers: (e.headers ?? [])
|
||||
.map(({ name: d, value: p, disabled: s }) => ({
|
||||
enabled: !s,
|
||||
name: d,
|
||||
value: p,
|
||||
}))
|
||||
.filter(({ name: d, value: p }) => d !== '' || p !== ''),
|
||||
}
|
||||
);
|
||||
}
|
||||
function _(e, t) {
|
||||
return (
|
||||
console.log('IMPORTING FOLDER', e._id, e.name, JSON.stringify(e, null, 2)),
|
||||
{
|
||||
id: e._id,
|
||||
createdAt: new Date(e.created ?? Date.now()).toISOString().replace('Z', ''),
|
||||
updatedAt: new Date(e.updated ?? Date.now()).toISOString().replace('Z', ''),
|
||||
folderId: e.parentId === t ? null : e.parentId,
|
||||
workspaceId: t,
|
||||
model: 'folder',
|
||||
name: e.name,
|
||||
}
|
||||
);
|
||||
}
|
||||
function b(e) {
|
||||
console.log('RUNNING INSOMNIA');
|
||||
let t;
|
||||
try {
|
||||
t = JSON.parse(e);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
if (!m(t) || !Array.isArray(t.resources)) return;
|
||||
const n = {
|
||||
workspaces: [],
|
||||
requests: [],
|
||||
environments: [],
|
||||
folders: [],
|
||||
},
|
||||
a = t.resources.filter(I);
|
||||
for (const r of a) {
|
||||
const i = t.resources.find((o) => f(o) && o.parentId === r._id);
|
||||
n.workspaces.push({
|
||||
id: r._id,
|
||||
createdAt: new Date(a.created ?? Date.now()).toISOString().replace('Z', ''),
|
||||
updatedAt: new Date(a.updated ?? Date.now()).toISOString().replace('Z', ''),
|
||||
model: 'workspace',
|
||||
name: r.name,
|
||||
variables: i ? O(i.data) : [],
|
||||
});
|
||||
const u = t.resources.filter((o) => f(o) && o.parentId === (i == null ? void 0 : i._id));
|
||||
n.environments.push(...u.map((o) => S(o, r._id)));
|
||||
const c = (o) => {
|
||||
const d = t.resources.filter((s) => s.parentId === o);
|
||||
let p = 0;
|
||||
for (const s of d)
|
||||
y(s) ? (n.folders.push(_(s, r._id)), c(s._id)) : g(s) && n.requests.push(h(s, r._id, p++));
|
||||
};
|
||||
c(r._id);
|
||||
}
|
||||
return (
|
||||
(n.requests = n.requests.filter(Boolean)),
|
||||
(n.environments = n.environments.filter(Boolean)),
|
||||
(n.workspaces = n.workspaces.filter(Boolean)),
|
||||
{ resources: n }
|
||||
);
|
||||
}
|
||||
export { b as pluginHookImport };
|
||||
160
src-tauri/plugins/importer-postman/index.mjs
Normal file
160
src-tauri/plugins/importer-postman/index.mjs
Normal file
@@ -0,0 +1,160 @@
|
||||
const T = "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", w = "https://schema.getpostman.com/json/collection/v2.0.0/collection.json", A = [w, T];
|
||||
function q(e) {
|
||||
const t = b(e);
|
||||
if (t == null)
|
||||
return;
|
||||
const n = a(t.info);
|
||||
if (!A.includes(n.schema) || !Array.isArray(t.item))
|
||||
return;
|
||||
const i = {
|
||||
workspaces: [],
|
||||
environments: [],
|
||||
requests: [],
|
||||
folders: []
|
||||
}, c = {
|
||||
model: "workspace",
|
||||
id: m("wk"),
|
||||
name: n.name || "Postman Import",
|
||||
description: n.description || ""
|
||||
};
|
||||
i.workspaces.push(c);
|
||||
const f = (r, u = null) => {
|
||||
if (typeof r.name == "string" && Array.isArray(r.item)) {
|
||||
const o = {
|
||||
model: "folder",
|
||||
workspaceId: c.id,
|
||||
id: m("fl"),
|
||||
name: r.name,
|
||||
folderId: u
|
||||
};
|
||||
i.folders.push(o);
|
||||
for (const s of r.item)
|
||||
f(s, o.id);
|
||||
} else if (typeof r.name == "string" && "request" in r) {
|
||||
const o = a(r.request), s = k(o.body), d = S(o.auth), g = {
|
||||
model: "http_request",
|
||||
id: m("rq"),
|
||||
workspaceId: c.id,
|
||||
folderId: u,
|
||||
name: r.name,
|
||||
method: o.method || "GET",
|
||||
url: typeof o.url == "string" ? o.url : a(o.url).raw,
|
||||
body: s.body,
|
||||
bodyType: s.bodyType,
|
||||
authentication: d.authentication,
|
||||
authenticationType: d.authenticationType,
|
||||
headers: [
|
||||
...s.headers,
|
||||
...d.headers,
|
||||
...y(o.header).map((p) => ({
|
||||
name: p.key,
|
||||
value: p.value,
|
||||
enabled: !p.disabled
|
||||
}))
|
||||
]
|
||||
};
|
||||
i.requests.push(g);
|
||||
} else
|
||||
console.log("Unknown item", r, u);
|
||||
};
|
||||
for (const r of t.item)
|
||||
f(r);
|
||||
return { resources: h(i) };
|
||||
}
|
||||
function S(e) {
|
||||
const t = a(e);
|
||||
return "basic" in t ? {
|
||||
headers: [],
|
||||
authenticationType: "basic",
|
||||
authentication: {
|
||||
username: t.basic.username || "",
|
||||
password: t.basic.password || ""
|
||||
}
|
||||
} : { headers: [], authenticationType: null, authentication: {} };
|
||||
}
|
||||
function k(e) {
|
||||
const t = a(e);
|
||||
return "graphql" in t ? {
|
||||
headers: [
|
||||
{
|
||||
name: "Content-Type",
|
||||
value: "application/json",
|
||||
enabled: !0
|
||||
}
|
||||
],
|
||||
bodyType: "graphql",
|
||||
body: {
|
||||
text: JSON.stringify(
|
||||
{ query: t.graphql.query, variables: b(t.graphql.variables) },
|
||||
null,
|
||||
2
|
||||
)
|
||||
}
|
||||
} : "urlencoded" in t ? {
|
||||
headers: [
|
||||
{
|
||||
name: "Content-Type",
|
||||
value: "application/x-www-form-urlencoded",
|
||||
enabled: !0
|
||||
}
|
||||
],
|
||||
bodyType: "application/x-www-form-urlencoded",
|
||||
body: {
|
||||
form: y(t.urlencoded).map((n) => ({
|
||||
enabled: !n.disabled,
|
||||
name: n.key ?? "",
|
||||
value: n.value ?? ""
|
||||
}))
|
||||
}
|
||||
} : "formdata" in t ? {
|
||||
headers: [
|
||||
{
|
||||
name: "Content-Type",
|
||||
value: "multipart/form-data",
|
||||
enabled: !0
|
||||
}
|
||||
],
|
||||
bodyType: "multipart/form-data",
|
||||
body: {
|
||||
form: y(t.formdata).map(
|
||||
(n) => n.src != null ? {
|
||||
enabled: !n.disabled,
|
||||
name: n.key ?? "",
|
||||
file: n.src ?? ""
|
||||
} : {
|
||||
enabled: !n.disabled,
|
||||
name: n.key ?? "",
|
||||
value: n.value ?? ""
|
||||
}
|
||||
)
|
||||
}
|
||||
} : { headers: [], bodyType: null, body: {} };
|
||||
}
|
||||
function b(e) {
|
||||
try {
|
||||
return a(JSON.parse(e));
|
||||
} catch {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function a(e) {
|
||||
return Object.prototype.toString.call(e) === "[object Object]" ? e : {};
|
||||
}
|
||||
function y(e) {
|
||||
return Object.prototype.toString.call(e) === "[object Array]" ? e : [];
|
||||
}
|
||||
function h(e) {
|
||||
return typeof e == "string" ? e.replace(/{{\s*(_\.)?([^}]+)\s*}}/g, "${[$2]}") : Array.isArray(e) && e != null ? e.map(h) : typeof e == "object" && e != null ? Object.fromEntries(
|
||||
Object.entries(e).map(([t, n]) => [t, h(n)])
|
||||
) : e;
|
||||
}
|
||||
function m(e) {
|
||||
const t = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
let n = `${e}_`;
|
||||
for (let l = 0; l < 10; l++)
|
||||
n += t[Math.floor(Math.random() * t.length)];
|
||||
return n;
|
||||
}
|
||||
export {
|
||||
q as pluginHookImport
|
||||
};
|
||||
@@ -1,130 +0,0 @@
|
||||
const f = 'https://schema.getpostman.com/json/collection/v2.1.0/collection.json',
|
||||
b = 'https://schema.getpostman.com/json/collection/v2.0.0/collection.json',
|
||||
w = [b, f];
|
||||
function A(t) {
|
||||
const e = m(t);
|
||||
if (e == null) return;
|
||||
const r = s(e.info);
|
||||
if (!w.includes(r.schema) || !Array.isArray(e.item)) return;
|
||||
const a = {
|
||||
workspaces: [],
|
||||
environments: [],
|
||||
requests: [],
|
||||
folders: [],
|
||||
},
|
||||
c = {
|
||||
model: 'workspace',
|
||||
id: 'wrk_0',
|
||||
name: r.name || 'Postman Import',
|
||||
description: r.description || '',
|
||||
};
|
||||
a.workspaces.push(c);
|
||||
const p = (o, l = null) => {
|
||||
if (typeof o.name == 'string' && Array.isArray(o.item)) {
|
||||
const n = {
|
||||
model: 'folder',
|
||||
workspaceId: c.id,
|
||||
id: `fld_${a.folders.length}`,
|
||||
name: o.name,
|
||||
folderId: l,
|
||||
};
|
||||
a.folders.push(n);
|
||||
for (const i of o.item) p(i, n.id);
|
||||
} else if (typeof o.name == 'string' && 'request' in o) {
|
||||
const n = s(o.request),
|
||||
i = T(n.body),
|
||||
u = g(n.auth),
|
||||
y = {
|
||||
model: 'http_request',
|
||||
id: `req_${a.requests.length}`,
|
||||
workspaceId: c.id,
|
||||
folderId: l,
|
||||
name: o.name,
|
||||
method: n.method || 'GET',
|
||||
url: typeof n.url == 'string' ? n.url : s(n.url).raw,
|
||||
body: i.body,
|
||||
bodyType: i.bodyType,
|
||||
authentication: u.authentication,
|
||||
authenticationType: u.authenticationType,
|
||||
headers: [
|
||||
...i.headers,
|
||||
...u.headers,
|
||||
...h(n.header).map((d) => ({
|
||||
name: d.key,
|
||||
value: d.value,
|
||||
enabled: !d.disabled,
|
||||
})),
|
||||
],
|
||||
};
|
||||
a.requests.push(y);
|
||||
} else console.log('Unknown item', o, l);
|
||||
};
|
||||
for (const o of e.item) p(o);
|
||||
return { resources: a };
|
||||
}
|
||||
function g(t) {
|
||||
const e = s(t);
|
||||
return 'basic' in e
|
||||
? {
|
||||
headers: [],
|
||||
authenticationType: 'basic',
|
||||
authentication: {
|
||||
username: e.basic.username || '',
|
||||
password: e.basic.password || '',
|
||||
},
|
||||
}
|
||||
: { headers: [], authenticationType: null, authentication: {} };
|
||||
}
|
||||
function T(t) {
|
||||
const e = s(t);
|
||||
return 'graphql' in e
|
||||
? {
|
||||
headers: [
|
||||
{
|
||||
name: 'Content-Type',
|
||||
value: 'application/json',
|
||||
enabled: !0,
|
||||
},
|
||||
],
|
||||
bodyType: 'graphql',
|
||||
body: {
|
||||
text: JSON.stringify(
|
||||
{ query: e.graphql.query, variables: m(e.graphql.variables) },
|
||||
null,
|
||||
2,
|
||||
),
|
||||
},
|
||||
}
|
||||
: 'formdata' in e
|
||||
? {
|
||||
headers: [
|
||||
{
|
||||
name: 'Content-Type',
|
||||
value: 'application/x-www-form-urlencoded',
|
||||
enabled: !0,
|
||||
},
|
||||
],
|
||||
bodyType: 'application/x-www-form-urlencoded',
|
||||
body: {
|
||||
form: h(e.formdata).map((r) => ({
|
||||
enabled: !r.disabled,
|
||||
name: r.key ?? '',
|
||||
value: r.value ?? '',
|
||||
})),
|
||||
},
|
||||
}
|
||||
: { headers: [], bodyType: null, body: {} };
|
||||
}
|
||||
function m(t) {
|
||||
try {
|
||||
return s(JSON.parse(t));
|
||||
} catch {}
|
||||
return null;
|
||||
}
|
||||
function s(t) {
|
||||
return Object.prototype.toString.call(t) === '[object Object]' ? t : {};
|
||||
}
|
||||
function h(t) {
|
||||
return Object.prototype.toString.call(t) === '[object Array]' ? t : [];
|
||||
}
|
||||
export { A as pluginHookImport };
|
||||
File diff suppressed because it is too large
Load Diff
17
src-tauri/plugins/importer-yaak/index.mjs
Normal file
17
src-tauri/plugins/importer-yaak/index.mjs
Normal file
@@ -0,0 +1,17 @@
|
||||
function u(r) {
|
||||
let e;
|
||||
try {
|
||||
e = JSON.parse(r);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
if (t(e) && e.yaakSchema === 1)
|
||||
return { resources: e.resources };
|
||||
}
|
||||
function t(r) {
|
||||
return Object.prototype.toString.call(r) === "[object Object]";
|
||||
}
|
||||
export {
|
||||
t as isJSObject,
|
||||
u as pluginHookImport
|
||||
};
|
||||
@@ -1,13 +0,0 @@
|
||||
function u(r) {
|
||||
let e;
|
||||
try {
|
||||
e = JSON.parse(r);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
if (t(e) && e.yaakSchema === 1) return { resources: e.resources };
|
||||
}
|
||||
function t(r) {
|
||||
return Object.prototype.toString.call(r) === '[object Object]';
|
||||
}
|
||||
export { t as isJSObject, u as pluginHookImport };
|
||||
@@ -1,89 +1,216 @@
|
||||
use log::{debug, warn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use sqlx::{Pool, Sqlite};
|
||||
use sqlx::types::JsonValue;
|
||||
use tauri::{async_runtime, AppHandle, Manager};
|
||||
use tauri::{AppHandle, Manager, State};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use crate::is_dev;
|
||||
use crate::{is_dev, models};
|
||||
|
||||
// serializable
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub enum AnalyticsResource {
|
||||
App,
|
||||
// Workspace,
|
||||
// Environment,
|
||||
// Folder,
|
||||
// HttpRequest,
|
||||
// HttpResponse,
|
||||
Sidebar,
|
||||
Workspace,
|
||||
Environment,
|
||||
Folder,
|
||||
HttpRequest,
|
||||
HttpResponse,
|
||||
KeyValue,
|
||||
}
|
||||
|
||||
impl AnalyticsResource {
|
||||
pub fn from_str(s: &str) -> Option<AnalyticsResource> {
|
||||
match s {
|
||||
"App" => Some(AnalyticsResource::App),
|
||||
"Sidebar" => Some(AnalyticsResource::Sidebar),
|
||||
"Workspace" => Some(AnalyticsResource::Workspace),
|
||||
"Environment" => Some(AnalyticsResource::Environment),
|
||||
"Folder" => Some(AnalyticsResource::Folder),
|
||||
"HttpRequest" => Some(AnalyticsResource::HttpRequest),
|
||||
"HttpResponse" => Some(AnalyticsResource::HttpResponse),
|
||||
"KeyValue" => Some(AnalyticsResource::KeyValue),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub enum AnalyticsAction {
|
||||
Launch,
|
||||
// Create,
|
||||
// Update,
|
||||
// Upsert,
|
||||
// Delete,
|
||||
// Send,
|
||||
// Duplicate,
|
||||
LaunchFirst,
|
||||
LaunchUpdate,
|
||||
Create,
|
||||
Update,
|
||||
Upsert,
|
||||
Delete,
|
||||
DeleteMany,
|
||||
Send,
|
||||
Toggle,
|
||||
Duplicate,
|
||||
}
|
||||
|
||||
impl AnalyticsAction {
|
||||
pub fn from_str(s: &str) -> Option<AnalyticsAction> {
|
||||
match s {
|
||||
"Launch" => Some(AnalyticsAction::Launch),
|
||||
"LaunchFirst" => Some(AnalyticsAction::LaunchFirst),
|
||||
"LaunchUpdate" => Some(AnalyticsAction::LaunchUpdate),
|
||||
"Create" => Some(AnalyticsAction::Create),
|
||||
"Update" => Some(AnalyticsAction::Update),
|
||||
"Upsert" => Some(AnalyticsAction::Upsert),
|
||||
"Delete" => Some(AnalyticsAction::Delete),
|
||||
"DeleteMany" => Some(AnalyticsAction::DeleteMany),
|
||||
"Send" => Some(AnalyticsAction::Send),
|
||||
"Duplicate" => Some(AnalyticsAction::Duplicate),
|
||||
"Toggle" => Some(AnalyticsAction::Toggle),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn resource_name(resource: AnalyticsResource) -> &'static str {
|
||||
match resource {
|
||||
AnalyticsResource::App => "app",
|
||||
// AnalyticsResource::Workspace => "workspace",
|
||||
// AnalyticsResource::Environment => "environment",
|
||||
// AnalyticsResource::Folder => "folder",
|
||||
// AnalyticsResource::HttpRequest => "http_request",
|
||||
// AnalyticsResource::HttpResponse => "http_response",
|
||||
AnalyticsResource::Sidebar => "sidebar",
|
||||
AnalyticsResource::Workspace => "workspace",
|
||||
AnalyticsResource::Environment => "environment",
|
||||
AnalyticsResource::Folder => "folder",
|
||||
AnalyticsResource::HttpRequest => "http_request",
|
||||
AnalyticsResource::HttpResponse => "http_response",
|
||||
AnalyticsResource::KeyValue => "key_value",
|
||||
}
|
||||
}
|
||||
|
||||
fn action_name(action: AnalyticsAction) -> &'static str {
|
||||
match action {
|
||||
AnalyticsAction::Launch => "launch",
|
||||
// AnalyticsAction::Create => "create",
|
||||
// AnalyticsAction::Update => "update",
|
||||
// AnalyticsAction::Upsert => "upsert",
|
||||
// AnalyticsAction::Delete => "delete",
|
||||
// AnalyticsAction::Send => "send",
|
||||
// AnalyticsAction::Duplicate => "duplicate",
|
||||
AnalyticsAction::LaunchFirst => "launch_first",
|
||||
AnalyticsAction::LaunchUpdate => "launch_update",
|
||||
AnalyticsAction::Create => "create",
|
||||
AnalyticsAction::Update => "update",
|
||||
AnalyticsAction::Upsert => "upsert",
|
||||
AnalyticsAction::Delete => "delete",
|
||||
AnalyticsAction::DeleteMany => "delete_many",
|
||||
AnalyticsAction::Send => "send",
|
||||
AnalyticsAction::Duplicate => "duplicate",
|
||||
AnalyticsAction::Toggle => "toggle",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn track_event(
|
||||
#[derive(Default, Debug)]
|
||||
pub struct LaunchEventInfo {
|
||||
pub current_version: String,
|
||||
pub previous_version: String,
|
||||
pub launched_after_update: bool,
|
||||
pub num_launches: i32,
|
||||
}
|
||||
|
||||
pub async fn track_launch_event(app_handle: &AppHandle) -> LaunchEventInfo {
|
||||
let namespace = "analytics";
|
||||
let last_tracked_version_key = "last_tracked_version";
|
||||
let db_instance: State<'_, Mutex<Pool<Sqlite>>> = app_handle.state();
|
||||
let pool = &*db_instance.lock().await;
|
||||
|
||||
let mut info = LaunchEventInfo::default();
|
||||
|
||||
info.num_launches = models::get_key_value_int(namespace, "num_launches", 0, pool).await + 1;
|
||||
info.previous_version =
|
||||
models::get_key_value_string(namespace, last_tracked_version_key, "", pool).await;
|
||||
info.current_version = app_handle.package_info().version.to_string();
|
||||
|
||||
if info.previous_version.is_empty() {
|
||||
track_event(
|
||||
app_handle,
|
||||
AnalyticsResource::App,
|
||||
AnalyticsAction::LaunchFirst,
|
||||
None,
|
||||
)
|
||||
.await;
|
||||
} else {
|
||||
info.launched_after_update = info.current_version != info.previous_version;
|
||||
if info.launched_after_update {
|
||||
track_event(
|
||||
app_handle,
|
||||
AnalyticsResource::App,
|
||||
AnalyticsAction::LaunchUpdate,
|
||||
Some(json!({ "num_launches": info.num_launches })),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
};
|
||||
|
||||
// Track a launch event in all cases
|
||||
track_event(
|
||||
app_handle,
|
||||
AnalyticsResource::App,
|
||||
AnalyticsAction::Launch,
|
||||
Some(json!({ "num_launches": info.num_launches })),
|
||||
)
|
||||
.await;
|
||||
|
||||
|
||||
// Update key values
|
||||
|
||||
models::set_key_value_string(
|
||||
namespace,
|
||||
last_tracked_version_key,
|
||||
info.current_version.as_str(),
|
||||
pool,
|
||||
)
|
||||
.await;
|
||||
models::set_key_value_int(namespace, "num_launches", info.num_launches, pool).await;
|
||||
|
||||
info
|
||||
}
|
||||
|
||||
pub async fn track_event(
|
||||
app_handle: &AppHandle,
|
||||
resource: AnalyticsResource,
|
||||
action: AnalyticsAction,
|
||||
attributes: Option<JsonValue>,
|
||||
) {
|
||||
async_runtime::block_on(async move {
|
||||
let event = format!("{}.{}", resource_name(resource), action_name(action));
|
||||
let attributes_json = attributes.unwrap_or("{}".to_string().into()).to_string();
|
||||
let info = app_handle.package_info();
|
||||
let tz = datetime::sys_timezone().unwrap_or("unknown".to_string());
|
||||
let params = vec![
|
||||
("e", event.clone()),
|
||||
("a", attributes_json.clone()),
|
||||
("id", "site_zOK0d7jeBy2TLxFCnZ".to_string()),
|
||||
("v", info.version.clone().to_string()),
|
||||
("os", get_os().to_string()),
|
||||
("tz", tz),
|
||||
("xy", get_window_size(app_handle)),
|
||||
];
|
||||
let url = "https://t.yaak.app/t/e".to_string();
|
||||
let req = reqwest::Client::builder()
|
||||
.build()
|
||||
.unwrap()
|
||||
.get(&url)
|
||||
.query(¶ms);
|
||||
let event = format!("{}.{}", resource_name(resource), action_name(action));
|
||||
let attributes_json = attributes.unwrap_or("{}".to_string().into()).to_string();
|
||||
let info = app_handle.package_info();
|
||||
let tz = datetime::sys_timezone().unwrap_or("unknown".to_string());
|
||||
let site = match is_dev() {
|
||||
true => "site_TkHWjoXwZPq3HfhERb",
|
||||
false => "site_zOK0d7jeBy2TLxFCnZ",
|
||||
};
|
||||
let base_url = match is_dev() {
|
||||
true => "http://localhost:7194",
|
||||
false => "https://t.yaak.app",
|
||||
};
|
||||
let params = vec![
|
||||
("e", event.clone()),
|
||||
("a", attributes_json.clone()),
|
||||
("id", site.to_string()),
|
||||
("v", info.version.clone().to_string()),
|
||||
("os", get_os().to_string()),
|
||||
("tz", tz),
|
||||
("xy", get_window_size(app_handle)),
|
||||
];
|
||||
let req = reqwest::Client::builder()
|
||||
.build()
|
||||
.unwrap()
|
||||
.get(format!("{base_url}/t/e"))
|
||||
.query(¶ms);
|
||||
|
||||
if is_dev() {
|
||||
debug!("Send event (dev): {}", event);
|
||||
} else if let Err(e) = req.send().await {
|
||||
warn!(
|
||||
"Error sending analytics event: {} {} {:?}",
|
||||
e, event, params
|
||||
);
|
||||
} else {
|
||||
debug!("Send event: {}: {:?}", event, params);
|
||||
}
|
||||
});
|
||||
// Disable analytics actual sending in dev
|
||||
if is_dev() {
|
||||
debug!("track: {} {}", event, attributes_json);
|
||||
return;
|
||||
}
|
||||
|
||||
if let Err(e) = req.send().await {
|
||||
warn!(
|
||||
"Error sending analytics event: {} {} {} {:?}",
|
||||
e, event, attributes_json, params,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn get_os() -> &'static str {
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
use std::fs;
|
||||
use std::fs::{create_dir_all, File};
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
use base64::Engine;
|
||||
use http::{HeaderMap, HeaderName, HeaderValue, Method};
|
||||
use http::header::{ACCEPT, USER_AGENT};
|
||||
use log::warn;
|
||||
use log::{error, info, warn};
|
||||
use reqwest::multipart;
|
||||
use reqwest::redirect::Policy;
|
||||
use sqlx::{Pool, Sqlite};
|
||||
@@ -14,12 +16,13 @@ use tauri::{AppHandle, Wry};
|
||||
|
||||
use crate::{emit_side_effect, models, render, response_err};
|
||||
|
||||
pub async fn actually_send_request(
|
||||
pub async fn send_http_request(
|
||||
request: models::HttpRequest,
|
||||
response: &models::HttpResponse,
|
||||
environment_id: &str,
|
||||
app_handle: &AppHandle<Wry>,
|
||||
pool: &Pool<Sqlite>,
|
||||
download_path: Option<PathBuf>,
|
||||
) -> Result<models::HttpResponse, String> {
|
||||
let start = std::time::Instant::now();
|
||||
let environment = models::get_environment(environment_id, pool).await.ok();
|
||||
@@ -34,11 +37,26 @@ pub async fn actually_send_request(
|
||||
url_string = format!("http://{}", url_string);
|
||||
}
|
||||
|
||||
let client = reqwest::Client::builder()
|
||||
.redirect(Policy::none())
|
||||
// .danger_accept_invalid_certs(true)
|
||||
.build()
|
||||
.expect("Failed to build client");
|
||||
let mut client_builder = reqwest::Client::builder()
|
||||
.redirect(match workspace.setting_follow_redirects {
|
||||
true => Policy::limited(10), // TODO: Handle redirects natively
|
||||
false => Policy::none(),
|
||||
})
|
||||
.gzip(true)
|
||||
.brotli(true)
|
||||
.deflate(true)
|
||||
.referer(false)
|
||||
.danger_accept_invalid_certs(!workspace.setting_validate_certificates)
|
||||
.tls_info(true);
|
||||
|
||||
if workspace.setting_request_timeout > 0 {
|
||||
client_builder = client_builder.timeout(Duration::from_millis(
|
||||
workspace.setting_request_timeout.unsigned_abs(),
|
||||
));
|
||||
}
|
||||
|
||||
// .use_rustls_tls() // TODO: Make this configurable (maybe)
|
||||
let client = client_builder.build().expect("Failed to build client");
|
||||
|
||||
let m = Method::from_bytes(request.method.to_uppercase().as_bytes())
|
||||
.expect("Failed to create method");
|
||||
@@ -63,14 +81,14 @@ pub async fn actually_send_request(
|
||||
let header_name = match HeaderName::from_bytes(name.as_bytes()) {
|
||||
Ok(n) => n,
|
||||
Err(e) => {
|
||||
eprintln!("Failed to create header name: {}", e);
|
||||
error!("Failed to create header name: {}", e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let header_value = match HeaderValue::from_str(value.as_str()) {
|
||||
Ok(n) => n,
|
||||
Err(e) => {
|
||||
eprintln!("Failed to create header value: {}", e);
|
||||
error!("Failed to create header value: {}", e);
|
||||
continue;
|
||||
}
|
||||
};
|
||||
@@ -114,7 +132,9 @@ pub async fn actually_send_request(
|
||||
|
||||
let mut query_params = Vec::new();
|
||||
for p in request.url_parameters.0 {
|
||||
if !p.enabled || p.name.is_empty() { continue; }
|
||||
if !p.enabled || p.name.is_empty() {
|
||||
continue;
|
||||
}
|
||||
query_params.push((
|
||||
render::render(&p.name, &workspace, environment_ref),
|
||||
render::render(&p.value, &workspace, environment_ref),
|
||||
@@ -128,18 +148,38 @@ pub async fn actually_send_request(
|
||||
let request_body = request.body.0;
|
||||
|
||||
if request_body.contains_key("text") {
|
||||
let raw_text = request_body.get("text").unwrap_or(empty_string).as_str().unwrap_or("");
|
||||
let raw_text = request_body
|
||||
.get("text")
|
||||
.unwrap_or(empty_string)
|
||||
.as_str()
|
||||
.unwrap_or("");
|
||||
let body = render::render(raw_text, &workspace, environment_ref);
|
||||
request_builder = request_builder.body(body);
|
||||
} else if body_type == "application/x-www-form-urlencoded" && request_body.contains_key("form") {
|
||||
} else if body_type == "application/x-www-form-urlencoded"
|
||||
&& request_body.contains_key("form")
|
||||
{
|
||||
let mut form_params = Vec::new();
|
||||
let form = request_body.get("form");
|
||||
if let Some(f) = form {
|
||||
for p in f.as_array().unwrap_or(&Vec::new()) {
|
||||
let enabled = p.get("enabled").unwrap_or(empty_bool).as_bool().unwrap_or(false);
|
||||
let name = p.get("name").unwrap_or(empty_string).as_str().unwrap_or_default();
|
||||
if !enabled || name.is_empty() { continue; }
|
||||
let value = p.get("value").unwrap_or(empty_string).as_str().unwrap_or_default();
|
||||
let enabled = p
|
||||
.get("enabled")
|
||||
.unwrap_or(empty_bool)
|
||||
.as_bool()
|
||||
.unwrap_or(false);
|
||||
let name = p
|
||||
.get("name")
|
||||
.unwrap_or(empty_string)
|
||||
.as_str()
|
||||
.unwrap_or_default();
|
||||
if !enabled || name.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let value = p
|
||||
.get("value")
|
||||
.unwrap_or(empty_string)
|
||||
.as_str()
|
||||
.unwrap_or_default();
|
||||
form_params.push((
|
||||
render::render(name, &workspace, environment_ref),
|
||||
render::render(value, &workspace, environment_ref),
|
||||
@@ -151,17 +191,41 @@ pub async fn actually_send_request(
|
||||
let mut multipart_form = multipart::Form::new();
|
||||
if let Some(form_definition) = request_body.get("form") {
|
||||
for p in form_definition.as_array().unwrap_or(&Vec::new()) {
|
||||
let enabled = p.get("enabled").unwrap_or(empty_bool).as_bool().unwrap_or(false);
|
||||
let name = p.get("name").unwrap_or(empty_string).as_str().unwrap_or_default();
|
||||
if !enabled || name.is_empty() { continue; }
|
||||
let enabled = p
|
||||
.get("enabled")
|
||||
.unwrap_or(empty_bool)
|
||||
.as_bool()
|
||||
.unwrap_or(false);
|
||||
let name = p
|
||||
.get("name")
|
||||
.unwrap_or(empty_string)
|
||||
.as_str()
|
||||
.unwrap_or_default();
|
||||
if !enabled || name.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let file = p.get("file").unwrap_or(empty_string).as_str().unwrap_or_default();
|
||||
let value = p.get("value").unwrap_or(empty_string).as_str().unwrap_or_default();
|
||||
let file = p
|
||||
.get("file")
|
||||
.unwrap_or(empty_string)
|
||||
.as_str()
|
||||
.unwrap_or_default();
|
||||
let value = p
|
||||
.get("value")
|
||||
.unwrap_or(empty_string)
|
||||
.as_str()
|
||||
.unwrap_or_default();
|
||||
multipart_form = multipart_form.part(
|
||||
render::render(name, &workspace, environment_ref),
|
||||
match !file.is_empty() {
|
||||
true => multipart::Part::bytes(fs::read(file).map_err(|e| e.to_string())?),
|
||||
false => multipart::Part::text(render::render(value, &workspace, environment_ref)),
|
||||
true => {
|
||||
multipart::Part::bytes(fs::read(file).map_err(|e| e.to_string())?)
|
||||
}
|
||||
false => multipart::Part::text(render::render(
|
||||
value,
|
||||
&workspace,
|
||||
environment_ref,
|
||||
)),
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -228,11 +292,6 @@ pub async fn actually_send_request(
|
||||
);
|
||||
}
|
||||
|
||||
// Also store body directly on the model, if small enough
|
||||
if body_bytes.len() < 100_000 {
|
||||
response.body = Some(body_bytes);
|
||||
}
|
||||
|
||||
response.elapsed = start.elapsed().as_millis() as i64;
|
||||
response = models::update_response_if_id(&response, pool)
|
||||
.await
|
||||
@@ -240,8 +299,20 @@ pub async fn actually_send_request(
|
||||
if !request.id.is_empty() {
|
||||
emit_side_effect(app_handle, "updated_model", &response);
|
||||
}
|
||||
|
||||
// Copy response to download path, if specified
|
||||
match (download_path, response.body_path.clone()) {
|
||||
(Some(dl_path), Some(body_path)) => {
|
||||
info!("Downloading response body to {}", dl_path.display());
|
||||
fs::copy(body_path, dl_path).expect("Failed to copy file for response download");
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
|
||||
Ok(response)
|
||||
}
|
||||
Err(e) => response_err(response, e.to_string(), app_handle, pool).await,
|
||||
Err(e) => {
|
||||
response_err(response, e.to_string(), app_handle, pool).await
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,36 +9,39 @@ extern crate objc;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::env::current_dir;
|
||||
use std::fs::{create_dir_all, File};
|
||||
use std::fs::{create_dir_all, read_to_string, File};
|
||||
use std::process::exit;
|
||||
|
||||
use fern::colors::ColoredLevelConfig;
|
||||
use log::{debug, error, info};
|
||||
use log::{debug, error, info, warn};
|
||||
use rand::random;
|
||||
use serde::Serialize;
|
||||
use sqlx::{Pool, Sqlite, SqlitePool};
|
||||
use serde_json::Value;
|
||||
use sqlx::migrate::Migrator;
|
||||
use sqlx::types::Json;
|
||||
use tauri::{AppHandle, Menu, RunEvent, State, Submenu, Window, WindowUrl, Wry};
|
||||
use tauri::{CustomMenuItem, Manager, WindowEvent};
|
||||
use sqlx::{Pool, Sqlite, SqlitePool};
|
||||
#[cfg(target_os = "macos")]
|
||||
use tauri::TitleBarStyle;
|
||||
use tauri::{AppHandle, RunEvent, State, Window, WindowUrl, Wry};
|
||||
use tauri::{Manager, WindowEvent};
|
||||
use tauri_plugin_log::{fern, LogTarget};
|
||||
use tauri_plugin_window_state::{StateFlags, WindowExt};
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::time::sleep;
|
||||
use window_shadows::set_shadow;
|
||||
|
||||
use window_ext::TrafficLightWindowExt;
|
||||
|
||||
use crate::analytics::{AnalyticsAction, AnalyticsResource, track_event};
|
||||
use crate::analytics::{AnalyticsAction, AnalyticsResource};
|
||||
use crate::http::send_http_request;
|
||||
use crate::plugin::{ImportResources, ImportResult};
|
||||
use crate::send::actually_send_request;
|
||||
use crate::updates::{update_mode_from_str, UpdateMode, YaakUpdater};
|
||||
|
||||
mod analytics;
|
||||
mod http;
|
||||
mod models;
|
||||
mod plugin;
|
||||
mod render;
|
||||
mod send;
|
||||
mod updates;
|
||||
mod window_ext;
|
||||
mod window_menu;
|
||||
@@ -67,7 +70,7 @@ async fn migrate_db(
|
||||
info!("Running migrations at {}", p.to_string_lossy());
|
||||
let m = Migrator::new(p).await.expect("Failed to load migrations");
|
||||
m.run(pool).await.expect("Failed to run migrations");
|
||||
info!("Migrations complete");
|
||||
info!("Migrations complete!");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -82,7 +85,53 @@ async fn send_ephemeral_request(
|
||||
let response = models::HttpResponse::new();
|
||||
let environment_id2 = environment_id.unwrap_or("n/a").to_string();
|
||||
request.id = "".to_string();
|
||||
actually_send_request(request, &response, &environment_id2, &app_handle, pool).await
|
||||
send_http_request(
|
||||
request,
|
||||
&response,
|
||||
&environment_id2,
|
||||
&app_handle,
|
||||
pool,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn filter_response(
|
||||
window: Window<Wry>,
|
||||
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
|
||||
response_id: &str,
|
||||
filter: &str,
|
||||
) -> Result<String, String> {
|
||||
let pool = &*db_instance.lock().await;
|
||||
let response = models::get_response(response_id, pool)
|
||||
.await
|
||||
.expect("Failed to get response");
|
||||
|
||||
if let None = response.body_path {
|
||||
return Err("Response body not found".to_string());
|
||||
}
|
||||
|
||||
let mut content_type = "".to_string();
|
||||
for header in response.headers.iter() {
|
||||
if header.name.to_lowercase() == "content-type" {
|
||||
content_type = header.value.to_string().to_lowercase();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Have plugins register their own content type (regex?)
|
||||
let plugin_name = if content_type.contains("json") {
|
||||
"filter-jsonpath"
|
||||
} else {
|
||||
"filter-xpath"
|
||||
};
|
||||
|
||||
let body = read_to_string(response.body_path.unwrap()).unwrap();
|
||||
let filter_result = plugin::run_plugin_filter(&window.app_handle(), plugin_name, filter, &body)
|
||||
.await
|
||||
.expect("Failed to run filter");
|
||||
Ok(filter_result.filtered)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -179,6 +228,7 @@ async fn send_request(
|
||||
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
|
||||
request_id: &str,
|
||||
environment_id: Option<&str>,
|
||||
download_dir: Option<&str>,
|
||||
) -> Result<models::HttpResponse, String> {
|
||||
let pool = &*db_instance.lock().await;
|
||||
|
||||
@@ -186,7 +236,7 @@ async fn send_request(
|
||||
.await
|
||||
.expect("Failed to get request");
|
||||
|
||||
let response = models::create_response(&req.id, 0, "", 0, None, None, None, None, vec![], pool)
|
||||
let response = models::create_response(&req.id, 0, "", 0, None, None, None, vec![], pool)
|
||||
.await
|
||||
.expect("Failed to create response");
|
||||
|
||||
@@ -195,9 +245,22 @@ async fn send_request(
|
||||
let app_handle2 = window.app_handle().clone();
|
||||
let pool2 = pool.clone();
|
||||
|
||||
let download_path = if let Some(p) = download_dir {
|
||||
Some(std::path::Path::new(p).to_path_buf())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) =
|
||||
actually_send_request(req, &response2, &environment_id2, &app_handle2, &pool2).await
|
||||
if let Err(e) = send_http_request(
|
||||
req,
|
||||
&response2,
|
||||
&environment_id2,
|
||||
&app_handle2,
|
||||
&pool2,
|
||||
download_path,
|
||||
)
|
||||
.await
|
||||
{
|
||||
response_err(&response2, e, &app_handle2, &pool2)
|
||||
.await
|
||||
@@ -224,6 +287,28 @@ async fn response_err(
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn track_event(
|
||||
window: Window<Wry>,
|
||||
resource: &str,
|
||||
action: &str,
|
||||
attributes: Option<Value>,
|
||||
) -> Result<(), String> {
|
||||
match (
|
||||
AnalyticsResource::from_str(resource),
|
||||
AnalyticsAction::from_str(action),
|
||||
) {
|
||||
(Some(resource), Some(action)) => {
|
||||
analytics::track_event(&window.app_handle(), resource, action, attributes).await;
|
||||
}
|
||||
_ => {
|
||||
error!("Invalid action/resource for track_event: {action} {resource}");
|
||||
return Err("Invalid event".to_string());
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn set_update_mode(
|
||||
update_mode: &str,
|
||||
@@ -240,7 +325,7 @@ async fn get_key_value(
|
||||
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
|
||||
) -> Result<Option<models::KeyValue>, ()> {
|
||||
let pool = &*db_instance.lock().await;
|
||||
let result = models::get_key_value(namespace, key, pool).await;
|
||||
let result = models::get_key_value_raw(namespace, key, pool).await;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
@@ -253,7 +338,7 @@ async fn set_key_value(
|
||||
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
|
||||
) -> Result<models::KeyValue, String> {
|
||||
let pool = &*db_instance.lock().await;
|
||||
let (key_value, created) = models::set_key_value(namespace, key, value, pool).await;
|
||||
let (key_value, created) = models::set_key_value_raw(namespace, key, value, pool).await;
|
||||
|
||||
if created {
|
||||
emit_and_return(&window, "created_model", key_value)
|
||||
@@ -269,15 +354,10 @@ async fn create_workspace(
|
||||
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
|
||||
) -> Result<models::Workspace, String> {
|
||||
let pool = &*db_instance.lock().await;
|
||||
let created_workspace = models::upsert_workspace(
|
||||
pool,
|
||||
models::Workspace {
|
||||
name: name.to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.expect("Failed to create Workspace");
|
||||
let created_workspace =
|
||||
models::upsert_workspace(pool, models::Workspace::new(name.to_string()))
|
||||
.await
|
||||
.expect("Failed to create Workspace");
|
||||
|
||||
emit_and_return(&window, "created_model", created_workspace)
|
||||
}
|
||||
@@ -504,6 +584,27 @@ async fn list_environments(
|
||||
Ok(environments)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn get_settings(db_instance: State<'_, Mutex<Pool<Sqlite>>>) -> Result<models::Settings, ()> {
|
||||
let pool = &*db_instance.lock().await;
|
||||
Ok(models::get_or_create_settings(pool).await)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn update_settings(
|
||||
settings: models::Settings,
|
||||
window: Window<Wry>,
|
||||
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
|
||||
) -> Result<models::Settings, String> {
|
||||
let pool = &*db_instance.lock().await;
|
||||
|
||||
let updated_settings = models::update_settings(pool, settings)
|
||||
.await
|
||||
.expect("Failed to update settings");
|
||||
|
||||
emit_and_return(&window, "updated_model", updated_settings)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn get_folder(
|
||||
id: &str,
|
||||
@@ -551,10 +652,11 @@ async fn get_workspace(
|
||||
#[tauri::command]
|
||||
async fn list_responses(
|
||||
request_id: &str,
|
||||
limit: Option<i64>,
|
||||
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
|
||||
) -> Result<Vec<models::HttpResponse>, String> {
|
||||
let pool = &*db_instance.lock().await;
|
||||
models::find_responses(request_id, pool)
|
||||
models::find_responses(request_id, limit, pool)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
@@ -651,24 +753,37 @@ fn main() {
|
||||
.level_for("sqlx", log::LevelFilter::Warn)
|
||||
.level_for("hyper", log::LevelFilter::Info)
|
||||
.level_for("tracing", log::LevelFilter::Info)
|
||||
.level_for("reqwest", log::LevelFilter::Debug)
|
||||
.level_for("reqwest", log::LevelFilter::Info)
|
||||
.level_for("tokio_util", log::LevelFilter::Info)
|
||||
.with_colors(ColoredLevelConfig::default())
|
||||
.level(log::LevelFilter::Trace)
|
||||
.build(),
|
||||
)
|
||||
.plugin(tauri_plugin_window_state::Builder::default().build())
|
||||
.plugin(tauri_plugin_context_menu::init())
|
||||
.setup(|app| {
|
||||
let app_data_dir = app.path_resolver().app_data_dir().unwrap();
|
||||
let app_config_dir = app.path_resolver().app_config_dir().unwrap();
|
||||
info!(
|
||||
"App Config Dir: {}",
|
||||
app_config_dir.as_path().to_string_lossy(),
|
||||
);
|
||||
info!("App Data Dir: {}", app_data_dir.as_path().to_string_lossy(),);
|
||||
let dir = match is_dev() {
|
||||
true => current_dir().unwrap(),
|
||||
false => app.path_resolver().app_data_dir().unwrap(),
|
||||
false => app_data_dir,
|
||||
};
|
||||
|
||||
create_dir_all(dir.clone()).expect("Problem creating App directory!");
|
||||
let p = dir.join("db.sqlite");
|
||||
File::options()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.open(&p)
|
||||
.expect("Problem creating database file!");
|
||||
|
||||
let p_string = p.to_string_lossy().replace(' ', "%20");
|
||||
let url = format!("sqlite://{}?mode=rwc", p_string);
|
||||
println!("Connecting to database at {}", url);
|
||||
info!("Connecting to database at {}", url);
|
||||
|
||||
tauri::async_runtime::block_on(async move {
|
||||
let pool = SqlitePool::connect(p.to_str().unwrap())
|
||||
@@ -704,10 +819,12 @@ fn main() {
|
||||
delete_workspace,
|
||||
duplicate_request,
|
||||
export_data,
|
||||
filter_response,
|
||||
get_key_value,
|
||||
get_environment,
|
||||
get_folder,
|
||||
get_request,
|
||||
get_settings,
|
||||
get_workspace,
|
||||
import_data,
|
||||
list_environments,
|
||||
@@ -720,9 +837,11 @@ fn main() {
|
||||
send_request,
|
||||
set_key_value,
|
||||
set_update_mode,
|
||||
track_event,
|
||||
update_environment,
|
||||
update_folder,
|
||||
update_request,
|
||||
update_settings,
|
||||
update_workspace,
|
||||
])
|
||||
.build(tauri::generate_context!())
|
||||
@@ -747,21 +866,27 @@ fn main() {
|
||||
debug!("Updater downloaded");
|
||||
}
|
||||
tauri::UpdaterEvent::Error(e) => {
|
||||
error!("Updater error: {:?}", e);
|
||||
warn!("Updater received error: {:?}", e);
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
RunEvent::Ready => {
|
||||
let w = create_window(app_handle, None);
|
||||
w.restore_state(StateFlags::all())
|
||||
.expect("Failed to restore window state");
|
||||
if let Err(e) = w.restore_state(StateFlags::all()) {
|
||||
error!("Failed to restore window state {}", e);
|
||||
}
|
||||
|
||||
track_event(
|
||||
app_handle,
|
||||
AnalyticsResource::App,
|
||||
AnalyticsAction::Launch,
|
||||
None,
|
||||
);
|
||||
let h = app_handle.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let info = analytics::track_launch_event(&h).await;
|
||||
info!("Launched Yaak {:?}", info);
|
||||
|
||||
// Wait for window render and give a chance for the user to notice
|
||||
if info.launched_after_update && info.num_launches > 1 {
|
||||
sleep(std::time::Duration::from_secs(5)).await;
|
||||
let _ = w.emit("show_changelog", true);
|
||||
}
|
||||
});
|
||||
}
|
||||
RunEvent::WindowEvent {
|
||||
label: _label,
|
||||
@@ -784,28 +909,18 @@ fn main() {
|
||||
}
|
||||
|
||||
fn is_dev() -> bool {
|
||||
let env = option_env!("YAAK_ENV");
|
||||
env.unwrap_or("production") != "production"
|
||||
#[cfg(dev)]
|
||||
{
|
||||
return true;
|
||||
}
|
||||
#[cfg(not(dev))]
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
fn create_window(handle: &AppHandle<Wry>, url: Option<&str>) -> Window<Wry> {
|
||||
let mut app_menu = window_menu::os_default("Yaak".to_string().as_str());
|
||||
if is_dev() {
|
||||
let submenu = Submenu::new(
|
||||
"Developer",
|
||||
Menu::new()
|
||||
.add_item(
|
||||
CustomMenuItem::new("refresh".to_string(), "Refresh")
|
||||
.accelerator("CmdOrCtrl + Shift + r"),
|
||||
)
|
||||
.add_item(
|
||||
CustomMenuItem::new("toggle_devtools".to_string(), "Open Devtools")
|
||||
.accelerator("CmdOrCtrl + Option + i"),
|
||||
),
|
||||
);
|
||||
app_menu = app_menu.add_submenu(submenu);
|
||||
}
|
||||
|
||||
let app_menu = window_menu::os_default("Yaak".to_string().as_str());
|
||||
let window_num = handle.windows().len();
|
||||
let window_id = format!("wnd_{}", window_num);
|
||||
let mut win_builder = tauri::WindowBuilder::new(
|
||||
@@ -813,7 +928,6 @@ fn create_window(handle: &AppHandle<Wry>, url: Option<&str>) -> Window<Wry> {
|
||||
window_id,
|
||||
WindowUrl::App(url.unwrap_or_default().into()),
|
||||
)
|
||||
.menu(app_menu)
|
||||
.fullscreen(false)
|
||||
.resizable(true)
|
||||
.inner_size(1100.0, 600.0)
|
||||
@@ -828,12 +942,24 @@ fn create_window(handle: &AppHandle<Wry>, url: Option<&str>) -> Window<Wry> {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
win_builder = win_builder
|
||||
.menu(app_menu)
|
||||
.hidden_title(true)
|
||||
.title_bar_style(TitleBarStyle::Overlay);
|
||||
}
|
||||
|
||||
// Add non-MacOS things
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
// Doesn't seem to work from Rust, here, so we do it in JS
|
||||
win_builder = win_builder.decorations(false);
|
||||
}
|
||||
|
||||
let win = win_builder.build().expect("failed to build window");
|
||||
|
||||
// Tauri doesn't support shadows when hiding decorations, so we add our own
|
||||
#[cfg(any(windows, target_os = "macos"))]
|
||||
set_shadow(&win, true).unwrap();
|
||||
|
||||
let win2 = win.clone();
|
||||
let handle2 = handle.clone();
|
||||
win.on_menu_event(move |event| match event.menu_item_id() {
|
||||
@@ -873,7 +999,6 @@ fn create_window(handle: &AppHandle<Wry>, url: Option<&str>) -> Window<Wry> {
|
||||
WindowEvent::Focused(..) => apply_offset(),
|
||||
WindowEvent::ScaleFactorChanged { .. } => apply_offset(),
|
||||
WindowEvent::CloseRequested { .. } => {
|
||||
println!("CLOSE REQUESTED");
|
||||
// api.prevent_close();
|
||||
}
|
||||
_ => {}
|
||||
@@ -900,10 +1025,6 @@ fn emit_side_effect<S: Serialize + Clone>(app_handle: &AppHandle<Wry>, event: &s
|
||||
}
|
||||
|
||||
async fn get_update_mode(pool: &Pool<Sqlite>) -> UpdateMode {
|
||||
let mode = models::get_key_value_string("app", "update_mode", pool)
|
||||
.await;
|
||||
match mode {
|
||||
Some(mode) => update_mode_from_str(&mode),
|
||||
None => UpdateMode::Stable,
|
||||
}
|
||||
let settings = models::get_or_create_settings(pool).await;
|
||||
update_mode_from_str(settings.update_channel.as_str())
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::collections::HashMap;
|
||||
use std::fs;
|
||||
use log::error;
|
||||
|
||||
use rand::distributions::{Alphanumeric, DistString};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -8,6 +9,22 @@ use sqlx::types::{Json, JsonValue};
|
||||
use sqlx::types::chrono::NaiveDateTime;
|
||||
use tauri::AppHandle;
|
||||
|
||||
fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Default)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct Settings {
|
||||
pub id: String,
|
||||
pub model: String,
|
||||
pub created_at: NaiveDateTime,
|
||||
pub updated_at: NaiveDateTime,
|
||||
pub theme: String,
|
||||
pub appearance: String,
|
||||
pub update_channel: String,
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Default)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct Workspace {
|
||||
@@ -18,6 +35,26 @@ pub struct Workspace {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub variables: Json<Vec<EnvironmentVariable>>,
|
||||
|
||||
// Settings
|
||||
#[serde(default = "default_true")]
|
||||
pub setting_validate_certificates: bool,
|
||||
#[serde(default = "default_true")]
|
||||
pub setting_follow_redirects: bool,
|
||||
pub setting_request_timeout: i64,
|
||||
}
|
||||
|
||||
// Implement default for Workspace
|
||||
impl Workspace {
|
||||
pub(crate) fn new(name: String) -> Self {
|
||||
Self {
|
||||
name,
|
||||
model: "workspace".to_string(),
|
||||
setting_validate_certificates: true,
|
||||
setting_follow_redirects: true,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Default)]
|
||||
@@ -32,14 +69,10 @@ pub struct Environment {
|
||||
pub variables: Json<Vec<EnvironmentVariable>>,
|
||||
}
|
||||
|
||||
fn default_enabled() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct EnvironmentVariable {
|
||||
#[serde(default = "default_enabled")]
|
||||
#[serde(default = "default_true")]
|
||||
pub enabled: bool,
|
||||
pub name: String,
|
||||
pub value: String,
|
||||
@@ -48,7 +81,7 @@ pub struct EnvironmentVariable {
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct HttpRequestHeader {
|
||||
#[serde(default = "default_enabled")]
|
||||
#[serde(default = "default_true")]
|
||||
pub enabled: bool,
|
||||
pub name: String,
|
||||
pub value: String,
|
||||
@@ -57,7 +90,7 @@ pub struct HttpRequestHeader {
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct HttpUrlParameter {
|
||||
#[serde(default = "default_enabled")]
|
||||
#[serde(default = "default_true")]
|
||||
pub enabled: bool,
|
||||
pub name: String,
|
||||
pub value: String,
|
||||
@@ -124,7 +157,6 @@ pub struct HttpResponse {
|
||||
pub elapsed: i64,
|
||||
pub status: i64,
|
||||
pub status_reason: Option<String>,
|
||||
pub body: Option<Vec<u8>>,
|
||||
pub body_path: Option<String>,
|
||||
pub headers: Json<Vec<HttpResponseHeader>>,
|
||||
}
|
||||
@@ -149,13 +181,75 @@ pub struct KeyValue {
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
pub async fn set_key_value(
|
||||
pub async fn set_key_value_string(
|
||||
namespace: &str,
|
||||
key: &str,
|
||||
value: &str,
|
||||
pool: &Pool<Sqlite>,
|
||||
) -> (KeyValue, bool) {
|
||||
let existing = get_key_value(namespace, key, pool).await;
|
||||
let encoded = serde_json::to_string(value);
|
||||
set_key_value_raw(namespace, key, &encoded.unwrap(), pool).await
|
||||
}
|
||||
|
||||
pub async fn set_key_value_int(
|
||||
namespace: &str,
|
||||
key: &str,
|
||||
value: i32,
|
||||
pool: &Pool<Sqlite>,
|
||||
) -> (KeyValue, bool) {
|
||||
let encoded = serde_json::to_string(&value);
|
||||
set_key_value_raw(namespace, key, &encoded.unwrap(), pool).await
|
||||
}
|
||||
|
||||
pub async fn get_key_value_string(
|
||||
namespace: &str,
|
||||
key: &str,
|
||||
default: &str,
|
||||
pool: &Pool<Sqlite>,
|
||||
) -> String {
|
||||
match get_key_value_raw(namespace, key, pool).await {
|
||||
None => default.to_string(),
|
||||
Some(v) => {
|
||||
let result = serde_json::from_str(&v.value);
|
||||
match result {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
error!("Failed to parse string key value: {}", e);
|
||||
default.to_string()
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_key_value_int(
|
||||
namespace: &str,
|
||||
key: &str,
|
||||
default: i32,
|
||||
pool: &Pool<Sqlite>,
|
||||
) -> i32 {
|
||||
match get_key_value_raw(namespace, key, pool).await {
|
||||
None => default.clone(),
|
||||
Some(v) => {
|
||||
let result = serde_json::from_str(&v.value);
|
||||
match result {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
error!("Failed to parse int key value: {}", e);
|
||||
default.clone()
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn set_key_value_raw(
|
||||
namespace: &str,
|
||||
key: &str,
|
||||
value: &str,
|
||||
pool: &Pool<Sqlite>,
|
||||
) -> (KeyValue, bool) {
|
||||
let existing = get_key_value_raw(namespace, key, pool).await;
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO key_values (namespace, key, value)
|
||||
@@ -171,13 +265,13 @@ pub async fn set_key_value(
|
||||
.await
|
||||
.expect("Failed to insert key value");
|
||||
|
||||
let kv = get_key_value(namespace, key, pool)
|
||||
let kv = get_key_value_raw(namespace, key, pool)
|
||||
.await
|
||||
.expect("Failed to get key value");
|
||||
(kv, existing.is_none())
|
||||
}
|
||||
|
||||
pub async fn get_key_value(namespace: &str, key: &str, pool: &Pool<Sqlite>) -> Option<KeyValue> {
|
||||
pub async fn get_key_value_raw(namespace: &str, key: &str, pool: &Pool<Sqlite>) -> Option<KeyValue> {
|
||||
sqlx::query_as!(
|
||||
KeyValue,
|
||||
r#"
|
||||
@@ -193,23 +287,20 @@ pub async fn get_key_value(namespace: &str, key: &str, pool: &Pool<Sqlite>) -> O
|
||||
.ok()
|
||||
}
|
||||
|
||||
pub async fn get_key_value_string(namespace: &str, key: &str, pool: &Pool<Sqlite>) -> Option<String> {
|
||||
let kv = get_key_value(namespace, key, pool).await?;
|
||||
let result = serde_json::from_str(&kv.value);
|
||||
match result {
|
||||
Ok(v) => Some(v),
|
||||
Err(e) => {
|
||||
println!("Failed to parse key value: {}", e);
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn find_workspaces(pool: &Pool<Sqlite>) -> Result<Vec<Workspace>, sqlx::Error> {
|
||||
sqlx::query_as!(
|
||||
Workspace,
|
||||
r#"
|
||||
SELECT id, model, created_at, updated_at, name, description,
|
||||
SELECT
|
||||
id,
|
||||
model,
|
||||
created_at,
|
||||
updated_at,
|
||||
name,
|
||||
description,
|
||||
setting_request_timeout,
|
||||
setting_follow_redirects,
|
||||
setting_validate_certificates,
|
||||
variables AS "variables!: sqlx::types::Json<Vec<EnvironmentVariable>>"
|
||||
FROM workspaces
|
||||
"#,
|
||||
@@ -222,7 +313,16 @@ pub async fn get_workspace(id: &str, pool: &Pool<Sqlite>) -> Result<Workspace, s
|
||||
sqlx::query_as!(
|
||||
Workspace,
|
||||
r#"
|
||||
SELECT id, model, created_at, updated_at, name, description,
|
||||
SELECT
|
||||
id,
|
||||
model,
|
||||
created_at,
|
||||
updated_at,
|
||||
name,
|
||||
description,
|
||||
setting_request_timeout,
|
||||
setting_follow_redirects,
|
||||
setting_validate_certificates,
|
||||
variables AS "variables!: sqlx::types::Json<Vec<EnvironmentVariable>>"
|
||||
FROM workspaces WHERE id = ?
|
||||
"#,
|
||||
@@ -284,6 +384,63 @@ pub async fn delete_environment(id: &str, pool: &Pool<Sqlite>) -> Result<Environ
|
||||
Ok(env)
|
||||
}
|
||||
|
||||
async fn get_settings(pool: &Pool<Sqlite>) -> Result<Settings, sqlx::Error> {
|
||||
sqlx::query_as!(
|
||||
Settings,
|
||||
r#"
|
||||
SELECT
|
||||
id,
|
||||
model,
|
||||
created_at,
|
||||
updated_at,
|
||||
theme,
|
||||
appearance,
|
||||
update_channel
|
||||
FROM settings
|
||||
WHERE id = 'default'
|
||||
"#,
|
||||
)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_or_create_settings(pool: &Pool<Sqlite>) -> Settings {
|
||||
if let Ok(settings) = get_settings(pool).await {
|
||||
settings
|
||||
} else {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO settings (id)
|
||||
VALUES ('default')
|
||||
"#,
|
||||
)
|
||||
.execute(pool)
|
||||
.await.expect("Failed to insert settings");
|
||||
get_settings(pool).await.expect("Failed to get settings")
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update_settings(
|
||||
pool: &Pool<Sqlite>,
|
||||
settings: Settings,
|
||||
) -> Result<Settings, sqlx::Error> {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
UPDATE settings SET (
|
||||
theme,
|
||||
appearance,
|
||||
update_channel
|
||||
) = (?, ?, ?) WHERE id = 'default';
|
||||
"#,
|
||||
settings.theme,
|
||||
settings.appearance,
|
||||
settings.update_channel
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
get_settings(pool).await
|
||||
}
|
||||
|
||||
pub async fn upsert_environment(
|
||||
pool: &Pool<Sqlite>,
|
||||
environment: Environment,
|
||||
@@ -594,7 +751,6 @@ pub async fn create_response(
|
||||
status: i64,
|
||||
status_reason: Option<&str>,
|
||||
content_length: Option<i64>,
|
||||
body: Option<Vec<u8>>,
|
||||
body_path: Option<&str>,
|
||||
headers: Vec<HttpResponseHeader>,
|
||||
pool: &Pool<Sqlite>,
|
||||
@@ -613,11 +769,10 @@ pub async fn create_response(
|
||||
status,
|
||||
status_reason,
|
||||
content_length,
|
||||
body,
|
||||
body_path,
|
||||
headers
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
|
||||
"#,
|
||||
id,
|
||||
request_id,
|
||||
@@ -627,7 +782,6 @@ pub async fn create_response(
|
||||
status,
|
||||
status_reason,
|
||||
content_length,
|
||||
body,
|
||||
body_path,
|
||||
headers_json,
|
||||
)
|
||||
@@ -672,18 +826,32 @@ pub async fn upsert_workspace(
|
||||
let trimmed_name = workspace.name.trim();
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO workspaces (id, name, description, variables)
|
||||
VALUES (?, ?, ?, ?)
|
||||
INSERT INTO workspaces (
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
variables,
|
||||
setting_request_timeout,
|
||||
setting_follow_redirects,
|
||||
setting_validate_certificates
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
updated_at = CURRENT_TIMESTAMP,
|
||||
name = excluded.name,
|
||||
description = excluded.description,
|
||||
variables = excluded.variables
|
||||
variables = excluded.variables,
|
||||
setting_request_timeout = excluded.setting_request_timeout,
|
||||
setting_follow_redirects = excluded.setting_follow_redirects,
|
||||
setting_validate_certificates = excluded.setting_validate_certificates
|
||||
"#,
|
||||
id,
|
||||
trimmed_name,
|
||||
workspace.description,
|
||||
workspace.variables,
|
||||
workspace.setting_request_timeout,
|
||||
workspace.setting_follow_redirects,
|
||||
workspace.setting_validate_certificates,
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
@@ -704,19 +872,17 @@ pub async fn update_response(
|
||||
status,
|
||||
status_reason,
|
||||
content_length,
|
||||
body,
|
||||
body_path,
|
||||
error,
|
||||
headers,
|
||||
updated_at
|
||||
) = (?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) WHERE id = ?;
|
||||
) = (?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP) WHERE id = ?;
|
||||
"#,
|
||||
response.elapsed,
|
||||
response.url,
|
||||
response.status,
|
||||
response.status_reason,
|
||||
response.content_length,
|
||||
response.body,
|
||||
response.body_path,
|
||||
response.error,
|
||||
headers_json,
|
||||
@@ -732,7 +898,7 @@ pub async fn get_response(id: &str, pool: &Pool<Sqlite>) -> Result<HttpResponse,
|
||||
HttpResponse,
|
||||
r#"
|
||||
SELECT id, model, workspace_id, request_id, updated_at, created_at, url,
|
||||
status, status_reason, content_length, body, body_path, elapsed, error,
|
||||
status, status_reason, content_length, body_path, elapsed, error,
|
||||
headers AS "headers!: sqlx::types::Json<Vec<HttpResponseHeader>>"
|
||||
FROM http_responses
|
||||
WHERE id = ?
|
||||
@@ -745,19 +911,26 @@ pub async fn get_response(id: &str, pool: &Pool<Sqlite>) -> Result<HttpResponse,
|
||||
|
||||
pub async fn find_responses(
|
||||
request_id: &str,
|
||||
limit: Option<i64>,
|
||||
pool: &Pool<Sqlite>,
|
||||
) -> Result<Vec<HttpResponse>, sqlx::Error> {
|
||||
let limit_unwrapped = match limit {
|
||||
Some(l) => l,
|
||||
None => i64::MAX,
|
||||
};
|
||||
sqlx::query_as!(
|
||||
HttpResponse,
|
||||
r#"
|
||||
SELECT id, model, workspace_id, request_id, updated_at, created_at, url,
|
||||
status, status_reason, content_length, body, body_path, elapsed, error,
|
||||
status, status_reason, content_length, body_path, elapsed, error,
|
||||
headers AS "headers!: sqlx::types::Json<Vec<HttpResponseHeader>>"
|
||||
FROM http_responses
|
||||
WHERE request_id = ?
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ?
|
||||
"#,
|
||||
request_id,
|
||||
limit_unwrapped,
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
@@ -771,7 +944,7 @@ pub async fn find_responses_by_workspace_id(
|
||||
HttpResponse,
|
||||
r#"
|
||||
SELECT id, model, workspace_id, request_id, updated_at, created_at, url,
|
||||
status, status_reason, content_length, body, body_path, elapsed, error,
|
||||
status, status_reason, content_length, body_path, elapsed, error,
|
||||
headers AS "headers!: sqlx::types::Json<Vec<HttpResponseHeader>>"
|
||||
FROM http_responses
|
||||
WHERE workspace_id = ?
|
||||
@@ -789,7 +962,7 @@ pub async fn delete_response(id: &str, pool: &Pool<Sqlite>) -> Result<HttpRespon
|
||||
// Delete the body file if it exists
|
||||
if let Some(p) = resp.body_path.clone() {
|
||||
if let Err(e) = fs::remove_file(p) {
|
||||
println!("Failed to delete body file: {}", e);
|
||||
error!("Failed to delete body file: {}", e);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -810,7 +983,7 @@ pub async fn delete_all_responses(
|
||||
request_id: &str,
|
||||
pool: &Pool<Sqlite>,
|
||||
) -> Result<(), sqlx::Error> {
|
||||
for r in find_responses(request_id, pool).await? {
|
||||
for r in find_responses(request_id, None, pool).await? {
|
||||
delete_response(&r.id, pool).await?;
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
use std::fs;
|
||||
|
||||
use boa_engine::builtins::promise::PromiseState;
|
||||
use boa_engine::{
|
||||
js_string,
|
||||
module::{ModuleLoader, SimpleModuleLoader},
|
||||
property::Attribute,
|
||||
Context, JsArgs, JsNativeError, JsValue, Module, NativeFunction, Source,
|
||||
Context, js_string, JsNativeError, JsValue, Module, module::SimpleModuleLoader,
|
||||
property::Attribute, Source,
|
||||
};
|
||||
use boa_engine::builtins::promise::PromiseState;
|
||||
use boa_engine::module::ModuleLoader;
|
||||
use boa_runtime::Console;
|
||||
use log::debug;
|
||||
use log::{debug, error};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use tauri::AppHandle;
|
||||
|
||||
use crate::models::{Environment, Folder, HttpRequest, Workspace};
|
||||
|
||||
#[derive(Default, Debug, Deserialize, Serialize)]
|
||||
pub struct FilterResult {
|
||||
pub filtered: String,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Deserialize, Serialize)]
|
||||
pub struct ImportResult {
|
||||
pub resources: ImportResources,
|
||||
@@ -28,6 +32,29 @@ pub struct ImportResources {
|
||||
pub requests: Vec<HttpRequest>,
|
||||
}
|
||||
|
||||
pub async fn run_plugin_filter(
|
||||
app_handle: &AppHandle,
|
||||
plugin_name: &str,
|
||||
response_body: &str,
|
||||
filter: &str,
|
||||
) -> Option<FilterResult> {
|
||||
let result_json = run_plugin(
|
||||
app_handle,
|
||||
plugin_name,
|
||||
"pluginHookResponseFilter",
|
||||
&[js_string!(response_body).into(), js_string!(filter).into()],
|
||||
);
|
||||
|
||||
if result_json.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");
|
||||
Some(resources)
|
||||
}
|
||||
|
||||
pub async fn run_plugin_import(
|
||||
app_handle: &AppHandle,
|
||||
plugin_name: &str,
|
||||
@@ -63,7 +90,7 @@ fn run_plugin(
|
||||
.resolve_resource("plugins")
|
||||
.expect("failed to resolve plugin directory resource")
|
||||
.join(plugin_name);
|
||||
let plugin_index_file = plugin_dir.join("out/index.js");
|
||||
let plugin_index_file = plugin_dir.join("index.mjs");
|
||||
|
||||
debug!(
|
||||
"Running plugin dir={:?} file={:?}",
|
||||
@@ -80,7 +107,6 @@ fn run_plugin(
|
||||
.expect("failed to create context");
|
||||
|
||||
add_runtime(context);
|
||||
add_globals(context);
|
||||
|
||||
let source = Source::from_filepath(&plugin_index_file).expect("Error opening file");
|
||||
|
||||
@@ -88,7 +114,6 @@ fn run_plugin(
|
||||
let module = Module::parse(source, None, context).expect("failed to parse module");
|
||||
|
||||
// Insert parsed entrypoint into the module loader
|
||||
// TODO: Is this needed if loaded from file already?
|
||||
loader.insert(plugin_index_file, module.clone());
|
||||
|
||||
let promise_result = module
|
||||
@@ -131,26 +156,9 @@ fn run_plugin(
|
||||
}
|
||||
}
|
||||
|
||||
fn add_runtime(context: &mut Context<'_>) {
|
||||
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");
|
||||
}
|
||||
|
||||
fn add_globals(context: &mut Context<'_>) {
|
||||
context
|
||||
.register_global_builtin_callable(
|
||||
"sayHello",
|
||||
1,
|
||||
NativeFunction::from_fn_ptr(|_, args, context| {
|
||||
let value: String = args
|
||||
.get_or_undefined(0)
|
||||
.try_js_into(context)
|
||||
.expect("failed to convert arg");
|
||||
println!("Hello {}!", value);
|
||||
Ok(value.into())
|
||||
}),
|
||||
)
|
||||
.expect("failed to register global");
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ use log::info;
|
||||
use tauri::{AppHandle, updater, Window, Wry};
|
||||
use tauri::api::dialog;
|
||||
|
||||
use crate::is_dev;
|
||||
|
||||
// Check for updates every 3 hours
|
||||
const MAX_UPDATE_CHECK_SECONDS: u64 = 60 * 60 * 3;
|
||||
|
||||
@@ -29,8 +31,15 @@ impl YaakUpdater {
|
||||
mode: UpdateMode,
|
||||
) -> Result<(), updater::Error> {
|
||||
self.last_update_check = SystemTime::now();
|
||||
|
||||
let update_mode = get_update_mode_str(mode);
|
||||
info!("Checking for updates mode={}", update_mode);
|
||||
let enabled = !is_dev();
|
||||
info!("Checking for updates mode={} enabled={}", update_mode, enabled);
|
||||
|
||||
if !enabled {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
match app_handle
|
||||
.updater()
|
||||
.header("X-Update-Mode", update_mode)?
|
||||
@@ -38,13 +47,40 @@ impl YaakUpdater {
|
||||
.await
|
||||
{
|
||||
Ok(update) => {
|
||||
if dialog::blocking::ask(
|
||||
let h = app_handle.clone();
|
||||
dialog::ask(
|
||||
None::<&Window>,
|
||||
"Update available",
|
||||
format!("{} is available. Would you like to download and install it now?", update.latest_version()),
|
||||
) {
|
||||
_ = update.download_and_install().await;
|
||||
}
|
||||
"Update Available",
|
||||
format!(
|
||||
"{} is available. Would you like to download and install it now?",
|
||||
update.latest_version()
|
||||
),
|
||||
|confirmed| {
|
||||
if !confirmed {
|
||||
return;
|
||||
}
|
||||
tauri::async_runtime::spawn(async move {
|
||||
match update.download_and_install().await {
|
||||
Ok(_) => {
|
||||
if dialog::blocking::ask(
|
||||
None::<&Window>,
|
||||
"Update Installed",
|
||||
"Would you like to restart the app?",
|
||||
) {
|
||||
h.restart();
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
dialog::message(
|
||||
None::<&Window>,
|
||||
"Update Failed",
|
||||
format!("The update failed to install: {}", e),
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
Err(updater::Error::UpToDate) => Ok(()),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use tauri::{AboutMetadata, CustomMenuItem, Menu, MenuItem, Submenu};
|
||||
use crate::is_dev;
|
||||
|
||||
pub fn os_default(#[allow(unused)] app_name: &str) -> Menu {
|
||||
let mut menu = Menu::new();
|
||||
@@ -12,6 +13,11 @@ pub fn os_default(#[allow(unused)] app_name: &str) -> Menu {
|
||||
AboutMetadata::default(),
|
||||
))
|
||||
.add_native_item(MenuItem::Separator)
|
||||
.add_item(
|
||||
CustomMenuItem::new("toggle_settings".to_string(), "Settings")
|
||||
.accelerator("CmdOrCtrl+,"),
|
||||
)
|
||||
.add_native_item(MenuItem::Separator)
|
||||
.add_native_item(MenuItem::Services)
|
||||
.add_native_item(MenuItem::Separator)
|
||||
.add_native_item(MenuItem::Hide)
|
||||
@@ -69,23 +75,23 @@ pub fn os_default(#[allow(unused)] app_name: &str) -> Menu {
|
||||
)
|
||||
.add_item(
|
||||
CustomMenuItem::new("zoom_out".to_string(), "Zoom Out").accelerator("CmdOrCtrl+-"),
|
||||
)
|
||||
.add_native_item(MenuItem::Separator)
|
||||
.add_item(
|
||||
CustomMenuItem::new("toggle_sidebar".to_string(), "Toggle Sidebar")
|
||||
.accelerator("CmdOrCtrl+b"),
|
||||
)
|
||||
.add_item(
|
||||
CustomMenuItem::new("focus_sidebar".to_string(), "Focus Sidebar")
|
||||
.accelerator("CmdOrCtrl+1"),
|
||||
)
|
||||
.add_item(
|
||||
CustomMenuItem::new("toggle_settings".to_string(), "Toggle Settings")
|
||||
.accelerator("CmdOrCtrl+,"),
|
||||
)
|
||||
.add_item(
|
||||
CustomMenuItem::new("focus_url".to_string(), "Focus URL").accelerator("CmdOrCtrl+l"),
|
||||
);
|
||||
// .add_native_item(MenuItem::Separator)
|
||||
// .add_item(
|
||||
// CustomMenuItem::new("toggle_sidebar".to_string(), "Toggle Sidebar")
|
||||
// .accelerator("CmdOrCtrl+b"),
|
||||
// )
|
||||
// .add_item(
|
||||
// CustomMenuItem::new("focus_sidebar".to_string(), "Focus Sidebar")
|
||||
// .accelerator("CmdOrCtrl+1"),
|
||||
// )
|
||||
// .add_item(
|
||||
// CustomMenuItem::new("toggle_settings".to_string(), "Toggle Settings")
|
||||
// .accelerator("CmdOrCtrl+,"),
|
||||
// )
|
||||
// .add_item(
|
||||
// CustomMenuItem::new("focus_url".to_string(), "Focus URL").accelerator("CmdOrCtrl+l"),
|
||||
// );
|
||||
menu = menu.add_submenu(Submenu::new("View", view_menu));
|
||||
|
||||
let mut window_menu = Menu::new();
|
||||
@@ -98,22 +104,37 @@ pub fn os_default(#[allow(unused)] app_name: &str) -> Menu {
|
||||
window_menu = window_menu.add_native_item(MenuItem::CloseWindow);
|
||||
menu = menu.add_submenu(Submenu::new("Window", window_menu));
|
||||
|
||||
menu = menu.add_submenu(Submenu::new(
|
||||
"Workspace",
|
||||
Menu::new()
|
||||
.add_item(
|
||||
CustomMenuItem::new("send_request".to_string(), "Send Request")
|
||||
.accelerator("CmdOrCtrl+r"),
|
||||
)
|
||||
.add_item(
|
||||
CustomMenuItem::new("new_request".to_string(), "New Request")
|
||||
.accelerator("CmdOrCtrl+n"),
|
||||
)
|
||||
.add_item(
|
||||
CustomMenuItem::new("duplicate_request".to_string(), "Duplicate Request")
|
||||
.accelerator("CmdOrCtrl+d"),
|
||||
),
|
||||
));
|
||||
// menu = menu.add_submenu(Submenu::new(
|
||||
// "Workspace",
|
||||
// Menu::new()
|
||||
// .add_item(
|
||||
// CustomMenuItem::new("send_request".to_string(), "Send Request")
|
||||
// .accelerator("CmdOrCtrl+r"),
|
||||
// )
|
||||
// .add_item(
|
||||
// CustomMenuItem::new("new_request".to_string(), "New Request")
|
||||
// .accelerator("CmdOrCtrl+n"),
|
||||
// )
|
||||
// .add_item(
|
||||
// CustomMenuItem::new("duplicate_request".to_string(), "Duplicate Request")
|
||||
// .accelerator("CmdOrCtrl+d"),
|
||||
// ),
|
||||
// ));
|
||||
|
||||
if is_dev() {
|
||||
menu = menu.add_submenu(Submenu::new(
|
||||
"Developer",
|
||||
Menu::new()
|
||||
.add_item(
|
||||
CustomMenuItem::new("refresh".to_string(), "Refresh")
|
||||
.accelerator("CmdOrCtrl + Shift + r"),
|
||||
)
|
||||
.add_item(
|
||||
CustomMenuItem::new("toggle_devtools".to_string(), "Open Devtools")
|
||||
.accelerator("CmdOrCtrl + Option + i"),
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
menu
|
||||
}
|
||||
|
||||
@@ -8,26 +8,10 @@
|
||||
},
|
||||
"package": {
|
||||
"productName": "Yaak",
|
||||
"version": "2023.3.0-beta.2"
|
||||
"version": "2024.0.1"
|
||||
},
|
||||
"tauri": {
|
||||
"windows": [],
|
||||
"cli": {
|
||||
"description": "Yaak CLI",
|
||||
"longDescription": "This is the Yaak CLI, yo",
|
||||
"beforeHelp": "u can use it to build, develop and manage your Yaak application.",
|
||||
"afterHelp": "Have fun!",
|
||||
"args": [],
|
||||
"subcommands": {
|
||||
"import": {
|
||||
"args": [{
|
||||
"name": "file",
|
||||
"short": "f",
|
||||
"takesValue": true
|
||||
}]
|
||||
}
|
||||
}
|
||||
},
|
||||
"allowlist": {
|
||||
"all": false,
|
||||
"os": {
|
||||
@@ -51,8 +35,13 @@
|
||||
"open": true
|
||||
},
|
||||
"window": {
|
||||
"close": true,
|
||||
"maximize": true,
|
||||
"minimize": true,
|
||||
"setDecorations": true,
|
||||
"setTitle": true,
|
||||
"startDragging": true,
|
||||
"setTitle": true
|
||||
"unmaximize": true
|
||||
},
|
||||
"dialog": {
|
||||
"all": false,
|
||||
@@ -72,7 +61,7 @@
|
||||
"icons/release/icon.icns",
|
||||
"icons/release/icon.ico"
|
||||
],
|
||||
"identifier": "co.schier.yaak",
|
||||
"identifier": "app.yaak.desktop",
|
||||
"longDescription": "The best cross-platform visual API client",
|
||||
"resources": [
|
||||
"migrations/*",
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
<svg width="100%" height="100%" viewBox="0 0 15 15" version="1.1" xmlns="http://www.w3.org/2000/svg"
|
||||
style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<path fill="currentColor"
|
||||
d="M2.5,1C1.672,1 1,1.672 1,2.5L1,12.5C1,13.328 1.672,14 2.5,14L12.5,14C13.328,14 14,13.328 14,12.5L14,2.5C14,1.672 13.328,1 12.5,1L2.5,1ZM12.5,13C12.776,13 13,12.776 13,12.5L13,2.5C13,2.224 12.776,2 12.5,2L6,2L6,13L12.5,13ZM2.5,2L5,2L5,13L2.5,13C2.224,13 2,12.776 2,12.5L2,2.5C2,2.224 2.224,2 2.5,2Z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 553 B |
@@ -1,6 +0,0 @@
|
||||
<svg width="100%" height="100%" viewBox="0 0 15 15" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<rect x="0" y="0" width="15" height="15" style="fill:none;"/>
|
||||
<g transform="matrix(1,0,0,1,-16,-8.88178e-16)">
|
||||
<path fill="currentColor" d="M18.5,1C17.672,1 17,1.672 17,2.5L17,12.5C17,13.328 17.672,14 18.5,14L28.5,14C29.328,14 30,13.328 30,12.5L30,2.5C30,1.672 29.328,1 28.5,1L18.5,1ZM28.5,13C28.776,13 29,12.776 29,12.5L29,2.5C29,2.224 28.776,2 28.5,2L22,2L22,13L28.5,13ZM18,11.535L21,12.285L21,13L18.5,13C18.224,13 18,12.776 18,12.5L18,11.535ZM18,10.504L21,11.254L21,9.81L18,9.06L18,10.504ZM18,8.029L21,8.779L21,7.327L18,6.577L18,8.029ZM18,5.546L21,6.296L21,4.833L18,4.083L18,5.546ZM21,3.802L18,3.052L18,2.5C18,2.224 18.224,2 18.5,2L21,2L21,3.802Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1006 B |
@@ -1,5 +1,4 @@
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||
import { MotionConfig } from 'framer-motion';
|
||||
import { Suspense } from 'react';
|
||||
import { DndProvider } from 'react-dnd';
|
||||
@@ -26,7 +25,7 @@ export function App() {
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
<Suspense>
|
||||
<AppRouter />
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
{/*<ReactQueryDevtools initialIsOpen={false} />*/}
|
||||
</Suspense>
|
||||
</DndProvider>
|
||||
</HelmetProvider>
|
||||
|
||||
@@ -16,6 +16,7 @@ interface State {
|
||||
|
||||
interface Actions {
|
||||
show: (d: DialogEntryOptionalId) => void;
|
||||
toggle: (d: DialogEntry) => void;
|
||||
hide: (id: string) => void;
|
||||
}
|
||||
|
||||
@@ -26,15 +27,20 @@ export const DialogProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const [dialogs, setDialogs] = useState<State['dialogs']>([]);
|
||||
const actions = useMemo<Actions>(
|
||||
() => ({
|
||||
show: ({ id: oid, ...props }: DialogEntryOptionalId) => {
|
||||
show({ id: oid, ...props }: DialogEntryOptionalId) {
|
||||
const id = oid ?? Math.random().toString(36).slice(2);
|
||||
setDialogs((a) => [...a.filter((d) => d.id !== id), { id, ...props }]);
|
||||
},
|
||||
toggle({ id: oid, ...props }: DialogEntryOptionalId) {
|
||||
const id = oid ?? Math.random().toString(36).slice(2);
|
||||
if (dialogs.some((d) => d.id === id)) this.hide(id);
|
||||
else this.show({ id, ...props });
|
||||
},
|
||||
hide: (id: string) => {
|
||||
setDialogs((a) => a.filter((d) => d.id !== id));
|
||||
},
|
||||
}),
|
||||
[],
|
||||
[dialogs],
|
||||
);
|
||||
|
||||
const state: State = {
|
||||
|
||||
@@ -2,6 +2,7 @@ import classNames from 'classnames';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useActiveEnvironment } from '../hooks/useActiveEnvironment';
|
||||
import { useAppRoutes } from '../hooks/useAppRoutes';
|
||||
import { useCreateEnvironment } from '../hooks/useCreateEnvironment';
|
||||
import { useEnvironments } from '../hooks/useEnvironments';
|
||||
import type { ButtonProps } from './core/Button';
|
||||
import { Button } from './core/Button';
|
||||
@@ -21,11 +22,13 @@ export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdo
|
||||
}: Props) {
|
||||
const environments = useEnvironments();
|
||||
const activeEnvironment = useActiveEnvironment();
|
||||
const createEnvironment = useCreateEnvironment();
|
||||
const dialog = useDialog();
|
||||
const routes = useAppRoutes();
|
||||
|
||||
const showEnvironmentDialog = useCallback(() => {
|
||||
dialog.show({
|
||||
dialog.toggle({
|
||||
id: 'environment-editor',
|
||||
title: 'Manage Environments',
|
||||
render: () => <EnvironmentEditDialog initialEnvironment={activeEnvironment} />,
|
||||
});
|
||||
@@ -51,14 +54,25 @@ export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdo
|
||||
...((environments.length > 0
|
||||
? [{ type: 'separator', label: 'Environments' }]
|
||||
: []) as DropdownItem[]),
|
||||
{
|
||||
key: 'edit',
|
||||
label: 'Manage Environments',
|
||||
leftSlot: <Icon icon="gear" />,
|
||||
onSelect: showEnvironmentDialog,
|
||||
},
|
||||
environments.length
|
||||
? {
|
||||
key: 'edit',
|
||||
label: 'Manage Environments',
|
||||
hotKeyAction: 'environmentEditor.toggle',
|
||||
leftSlot: <Icon icon="box" />,
|
||||
onSelect: showEnvironmentDialog,
|
||||
}
|
||||
: {
|
||||
key: 'new',
|
||||
label: 'New Environment',
|
||||
leftSlot: <Icon icon="plus" />,
|
||||
onSelect: async () => {
|
||||
await createEnvironment.mutateAsync();
|
||||
showEnvironmentDialog();
|
||||
},
|
||||
},
|
||||
],
|
||||
[activeEnvironment, environments, routes, showEnvironmentDialog],
|
||||
[activeEnvironment?.id, createEnvironment, environments, routes, showEnvironmentDialog],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -43,6 +43,11 @@ export const EnvironmentEditDialog = function ({ initialEnvironment }: Props) {
|
||||
[environments, selectedEnvironmentId],
|
||||
);
|
||||
|
||||
const handleCreateEnvironment = async () => {
|
||||
const e = await createEnvironment.mutateAsync();
|
||||
setSelectedEnvironmentId(e.id);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
@@ -53,30 +58,22 @@ export const EnvironmentEditDialog = function ({ initialEnvironment }: Props) {
|
||||
{showSidebar && (
|
||||
<aside className="grid grid-rows-[minmax(0,1fr)_auto] gap-y-0.5 h-full max-w-[250px] pr-3 border-r border-gray-100 -ml-2">
|
||||
<div className="min-w-0 h-full w-full overflow-y-scroll">
|
||||
<SidebarButton
|
||||
active={selectedEnvironment == null}
|
||||
onClick={() => setSelectedEnvironmentId(null)}
|
||||
>
|
||||
Base Environment
|
||||
</SidebarButton>
|
||||
<div className="ml-3 pl-2 border-l border-highlight">
|
||||
{environments.map((e) => (
|
||||
<SidebarButton
|
||||
key={e.id}
|
||||
active={selectedEnvironment?.id === e.id}
|
||||
onClick={() => setSelectedEnvironmentId(e.id)}
|
||||
>
|
||||
{e.name}
|
||||
</SidebarButton>
|
||||
))}
|
||||
</div>
|
||||
{environments.map((e) => (
|
||||
<SidebarButton
|
||||
key={e.id}
|
||||
active={selectedEnvironment?.id === e.id}
|
||||
onClick={() => setSelectedEnvironmentId(e.id)}
|
||||
>
|
||||
{e.name}
|
||||
</SidebarButton>
|
||||
))}
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
className="w-full text-center"
|
||||
color="gray"
|
||||
justify="center"
|
||||
onClick={() => createEnvironment.mutate()}
|
||||
onClick={handleCreateEnvironment}
|
||||
>
|
||||
New Environment
|
||||
</Button>
|
||||
@@ -191,7 +188,12 @@ const EnvironmentEditor = function ({
|
||||
<h1 className="text-xl">{environment?.name ?? 'Base Environment'}</h1>
|
||||
{items != null && (
|
||||
<Dropdown items={items}>
|
||||
<IconButton icon="gear" title="Environment Actions" size="sm" className="!h-auto w-8" />
|
||||
<IconButton
|
||||
icon="moreVertical"
|
||||
title="Environment Actions"
|
||||
size="sm"
|
||||
className="!h-auto w-8"
|
||||
/>
|
||||
</Dropdown>
|
||||
)}
|
||||
</HStack>
|
||||
@@ -199,7 +201,6 @@ const EnvironmentEditor = function ({
|
||||
nameAutocomplete={nameAutocomplete}
|
||||
nameAutocompleteVariables={false}
|
||||
namePlaceholder="VAR_NAME"
|
||||
valuePlaceholder="variable value"
|
||||
nameValidate={validateName}
|
||||
valueAutocompleteVariables={false}
|
||||
forceUpdateKey={environment?.id ?? workspace?.id ?? 'n/a'}
|
||||
|
||||
@@ -2,7 +2,6 @@ import { useQueryClient } from '@tanstack/react-query';
|
||||
import { appWindow } from '@tauri-apps/api/window';
|
||||
import { useEffect } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useEffectOnce } from 'react-use';
|
||||
import { keyValueQueryKey } from '../hooks/useKeyValue';
|
||||
import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
|
||||
import { useRecentEnvironments } from '../hooks/useRecentEnvironments';
|
||||
@@ -11,15 +10,17 @@ import { useRecentWorkspaces } from '../hooks/useRecentWorkspaces';
|
||||
import { requestsQueryKey } from '../hooks/useRequests';
|
||||
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
|
||||
import { responsesQueryKey } from '../hooks/useResponses';
|
||||
import { settingsQueryKey } from '../hooks/useSettings';
|
||||
import { useSyncWindowTitle } from '../hooks/useSyncWindowTitle';
|
||||
import { useSyncAppearance } from '../hooks/useSyncAppearance';
|
||||
import { workspacesQueryKey } from '../hooks/useWorkspaces';
|
||||
import { trackPage } from '../lib/analytics';
|
||||
import { DEFAULT_FONT_SIZE } from '../lib/constants';
|
||||
import { NAMESPACE_NO_SYNC } from '../lib/keyValueStore';
|
||||
import type { HttpRequest, HttpResponse, Model, Workspace } from '../lib/models';
|
||||
import { modelsEq } from '../lib/models';
|
||||
import { setPathname } from '../lib/persistPathname';
|
||||
|
||||
const DEFAULT_FONT_SIZE = 16;
|
||||
|
||||
export function GlobalHooks() {
|
||||
// Include here so they always update, even
|
||||
// if no component references them
|
||||
@@ -27,6 +28,8 @@ export function GlobalHooks() {
|
||||
useRecentEnvironments();
|
||||
useRecentRequests();
|
||||
|
||||
useSyncAppearance();
|
||||
|
||||
useSyncWindowTitle();
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
@@ -38,10 +41,6 @@ export function GlobalHooks() {
|
||||
setPathname(location.pathname).catch(console.error);
|
||||
}, [location.pathname]);
|
||||
|
||||
useEffectOnce(() => {
|
||||
trackPage('/');
|
||||
});
|
||||
|
||||
useListenToTauriEvent<Model>('created_model', ({ payload, windowLabel }) => {
|
||||
if (shouldIgnoreEvent(payload, windowLabel)) return;
|
||||
|
||||
@@ -54,6 +53,8 @@ export function GlobalHooks() {
|
||||
? workspacesQueryKey(payload)
|
||||
: payload.model === 'key_value'
|
||||
? keyValueQueryKey(payload)
|
||||
: payload.model === 'settings'
|
||||
? settingsQueryKey()
|
||||
: null;
|
||||
|
||||
if (queryKey === null) {
|
||||
@@ -79,6 +80,8 @@ export function GlobalHooks() {
|
||||
? workspacesQueryKey(payload)
|
||||
: payload.model === 'key_value'
|
||||
? keyValueQueryKey(payload)
|
||||
: payload.model === 'settings'
|
||||
? settingsQueryKey()
|
||||
: null;
|
||||
|
||||
if (queryKey === null) {
|
||||
@@ -91,9 +94,11 @@ export function GlobalHooks() {
|
||||
}
|
||||
|
||||
if (!shouldIgnoreModel(payload)) {
|
||||
console.time('set query date');
|
||||
queryClient.setQueryData<Model[]>(queryKey, (values) =>
|
||||
values?.map((v) => (modelsEq(v, payload) ? payload : v)),
|
||||
);
|
||||
console.timeEnd('set query date');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -110,6 +115,8 @@ export function GlobalHooks() {
|
||||
queryClient.setQueryData<HttpResponse[]>(responsesQueryKey(payload), removeById(payload));
|
||||
} else if (payload.model === 'key_value') {
|
||||
queryClient.setQueryData(keyValueQueryKey(payload), undefined);
|
||||
} else if (payload.model === 'settings') {
|
||||
queryClient.setQueryData(settingsQueryKey(), undefined);
|
||||
}
|
||||
});
|
||||
useListenToTauriEvent<number>('zoom', ({ payload: zoomDelta, windowLabel }) => {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { updateSchema } from 'cm6-graphql';
|
||||
import type { EditorView } from 'codemirror';
|
||||
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { useIntrospectGraphQL } from '../hooks/useIntrospectGraphQL';
|
||||
import { tryFormatJson } from '../lib/formatters';
|
||||
import type { HttpRequest } from '../lib/models';
|
||||
import { Button } from './core/Button';
|
||||
import type { EditorProps } from './core/Editor';
|
||||
@@ -81,46 +82,50 @@ export function GraphQLEditor({ defaultValue, onChange, baseRequest, ...extraEdi
|
||||
placeholder="..."
|
||||
ref={editorViewRef}
|
||||
actions={
|
||||
(error || isLoading) && (
|
||||
<Button
|
||||
size="xs"
|
||||
color={error ? 'danger' : 'gray'}
|
||||
isLoading={isLoading}
|
||||
onClick={() => {
|
||||
dialog.show({
|
||||
title: 'Introspection Failed',
|
||||
size: 'sm',
|
||||
id: 'introspection-failed',
|
||||
render: () => (
|
||||
<div className="whitespace-pre-wrap">
|
||||
<FormattedError>{error ?? 'unknown'}</FormattedError>
|
||||
<div className="w-full mt-3">
|
||||
<Button
|
||||
onClick={() => {
|
||||
dialog.hide('introspection-failed');
|
||||
refetch();
|
||||
}}
|
||||
className="ml-auto"
|
||||
color="secondary"
|
||||
size="sm"
|
||||
>
|
||||
Try Again
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
});
|
||||
}}
|
||||
>
|
||||
{error ? 'Introspection Failed' : 'Introspecting'}
|
||||
</Button>
|
||||
)
|
||||
error || isLoading
|
||||
? [
|
||||
<Button
|
||||
key="introspection"
|
||||
size="xs"
|
||||
color={error ? 'danger' : 'gray'}
|
||||
isLoading={isLoading}
|
||||
onClick={() => {
|
||||
dialog.show({
|
||||
title: 'Introspection Failed',
|
||||
size: 'dynamic',
|
||||
id: 'introspection-failed',
|
||||
render: () => (
|
||||
<>
|
||||
<FormattedError>{error ?? 'unknown'}</FormattedError>
|
||||
<div className="w-full mt-3">
|
||||
<Button
|
||||
onClick={() => {
|
||||
dialog.hide('introspection-failed');
|
||||
refetch();
|
||||
}}
|
||||
className="ml-auto"
|
||||
color="secondary"
|
||||
size="sm"
|
||||
>
|
||||
Try Again
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
),
|
||||
});
|
||||
}}
|
||||
>
|
||||
{error ? 'Introspection Failed' : 'Introspecting'}
|
||||
</Button>,
|
||||
]
|
||||
: []
|
||||
}
|
||||
{...extraEditorProps}
|
||||
/>
|
||||
<Separator variant="primary" />
|
||||
<p className="pt-1 text-gray-500 text-sm">Variables</p>
|
||||
<Editor
|
||||
format={tryFormatJson}
|
||||
contentType="application/json"
|
||||
defaultValue={JSON.stringify(variables, null, 2)}
|
||||
heightMode="auto"
|
||||
|
||||
10
src-web/components/KeyboardShortcutsDialog.tsx
Normal file
10
src-web/components/KeyboardShortcutsDialog.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { hotkeyActions } from '../hooks/useHotKey';
|
||||
import { HotKeyList } from './core/HotKeyList';
|
||||
|
||||
export const KeyboardShortcutsDialog = () => {
|
||||
return (
|
||||
<div className="h-full w-full">
|
||||
<HotKeyList hotkeys={hotkeyActions} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -32,7 +32,7 @@ export function Overlay({
|
||||
return (
|
||||
<Portal name={portalName}>
|
||||
{open && (
|
||||
<FocusTrap>
|
||||
<FocusTrap>
|
||||
<motion.div
|
||||
className={classNames('fixed inset-0', zIndexes[zIndex])}
|
||||
initial={{ opacity: 0 }}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useActiveEnvironmentId } from '../hooks/useActiveEnvironmentId';
|
||||
import { useActiveRequest } from '../hooks/useActiveRequest';
|
||||
import { useActiveWorkspaceId } from '../hooks/useActiveWorkspaceId';
|
||||
import { useAppRoutes } from '../hooks/useAppRoutes';
|
||||
import { useHotKey } from '../hooks/useHotKey';
|
||||
import { useRecentRequests } from '../hooks/useRecentRequests';
|
||||
import { useRequests } from '../hooks/useRequests';
|
||||
import { fallbackRequestName } from '../lib/fallbackRequestName';
|
||||
@@ -33,25 +34,19 @@ export function RecentRequestsDropdown({ className }: Pick<ButtonProps, 'classNa
|
||||
|
||||
// Handle key-up
|
||||
useKeyPressEvent('Control', undefined, () => {
|
||||
if (!dropdownRef.current?.isOpen) return;
|
||||
dropdownRef.current?.select?.();
|
||||
});
|
||||
|
||||
useKey(
|
||||
'Tab',
|
||||
(e) => {
|
||||
if (!e.ctrlKey || recentRequestIds.length === 0) return;
|
||||
useHotKey('requestSwitcher.prev', () => {
|
||||
if (!dropdownRef.current?.isOpen) dropdownRef.current?.open(1);
|
||||
dropdownRef.current?.next?.();
|
||||
});
|
||||
|
||||
if (!dropdownRef.current?.isOpen) {
|
||||
dropdownRef.current?.open(e.shiftKey ? -1 : 1);
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.shiftKey) dropdownRef.current?.prev?.();
|
||||
else dropdownRef.current?.next?.();
|
||||
},
|
||||
undefined,
|
||||
[recentRequestIds.length],
|
||||
);
|
||||
useHotKey('requestSwitcher.next', () => {
|
||||
if (!dropdownRef.current?.isOpen) dropdownRef.current?.open(-1);
|
||||
dropdownRef.current?.prev?.();
|
||||
});
|
||||
|
||||
const items = useMemo<DropdownItem[]>(() => {
|
||||
if (activeWorkspaceId === null) return [];
|
||||
|
||||
@@ -54,7 +54,7 @@ export const RecentResponsesDropdown = function ResponsePane({
|
||||
>
|
||||
<IconButton
|
||||
title="Show response history"
|
||||
icon="triangleDown"
|
||||
icon="chevronDown"
|
||||
className="ml-auto"
|
||||
size="sm"
|
||||
iconSize="md"
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
import { useRef } from 'react';
|
||||
import { useDeleteRequest } from '../hooks/useDeleteRequest';
|
||||
import { useDuplicateRequest } from '../hooks/useDuplicateRequest';
|
||||
import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
|
||||
import type { DropdownProps, DropdownRef } from './core/Dropdown';
|
||||
import { Dropdown } from './core/Dropdown';
|
||||
import { HotKey } from './core/HotKey';
|
||||
import { Icon } from './core/Icon';
|
||||
|
||||
interface Props {
|
||||
requestId: string | null;
|
||||
children: DropdownProps['children'];
|
||||
}
|
||||
|
||||
export function RequestActionsDropdown({ requestId, children }: Props) {
|
||||
const deleteRequest = useDeleteRequest(requestId);
|
||||
const duplicateRequest = useDuplicateRequest({ id: requestId, navigateAfter: true });
|
||||
const dropdownRef = useRef<DropdownRef>(null);
|
||||
|
||||
useListenToTauriEvent('toggle_settings', () => {
|
||||
dropdownRef.current?.toggle();
|
||||
});
|
||||
|
||||
// TODO: Put this somewhere better
|
||||
useListenToTauriEvent('duplicate_request', () => {
|
||||
duplicateRequest.mutate();
|
||||
});
|
||||
|
||||
if (requestId == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
ref={dropdownRef}
|
||||
items={[
|
||||
{
|
||||
key: 'duplicate',
|
||||
label: 'Duplicate',
|
||||
onSelect: duplicateRequest.mutate,
|
||||
leftSlot: <Icon icon="copy" />,
|
||||
rightSlot: <HotKey modifier="Meta" keyName="D" />,
|
||||
},
|
||||
{
|
||||
key: 'delete',
|
||||
label: 'Delete',
|
||||
onSelect: deleteRequest.mutate,
|
||||
variant: 'danger',
|
||||
leftSlot: <Icon icon="trash" />,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{children}
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,9 @@
|
||||
import { memo } from 'react';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { usePrompt } from '../hooks/usePrompt';
|
||||
import { Button } from './core/Button';
|
||||
import type { DropdownItem } from './core/Dropdown';
|
||||
import { Icon } from './core/Icon';
|
||||
import type { RadioDropdownItem } from './core/RadioDropdown';
|
||||
import { RadioDropdown } from './core/RadioDropdown';
|
||||
|
||||
type Props = {
|
||||
@@ -8,7 +12,15 @@ type Props = {
|
||||
onChange: (method: string) => void;
|
||||
};
|
||||
|
||||
const methodItems = ['GET', 'PUT', 'POST', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD'].map((m) => ({
|
||||
const radioItems: RadioDropdownItem<string>[] = [
|
||||
'GET',
|
||||
'PUT',
|
||||
'POST',
|
||||
'PATCH',
|
||||
'DELETE',
|
||||
'OPTIONS',
|
||||
'HEAD',
|
||||
].map((m) => ({
|
||||
value: m,
|
||||
label: m,
|
||||
}));
|
||||
@@ -18,8 +30,32 @@ export const RequestMethodDropdown = memo(function RequestMethodDropdown({
|
||||
onChange,
|
||||
className,
|
||||
}: Props) {
|
||||
const prompt = usePrompt();
|
||||
const extraItems = useMemo<DropdownItem[]>(
|
||||
() => [
|
||||
{ type: 'separator' },
|
||||
{
|
||||
key: 'custom',
|
||||
label: 'CUSTOM',
|
||||
leftSlot: <Icon icon="sparkles" />,
|
||||
onSelect: async () => {
|
||||
const newMethod = await prompt({
|
||||
label: 'Http Method',
|
||||
name: 'httpMethod',
|
||||
defaultValue: '',
|
||||
title: 'Custom Method',
|
||||
description: 'Enter a custom method name',
|
||||
placeholder: 'CUSTOM',
|
||||
});
|
||||
onChange(newMethod);
|
||||
},
|
||||
},
|
||||
],
|
||||
[onChange, prompt],
|
||||
);
|
||||
|
||||
return (
|
||||
<RadioDropdown value={method} items={methodItems} onChange={onChange}>
|
||||
<RadioDropdown value={method} items={radioItems} extraItems={extraItems} onChange={onChange}>
|
||||
<Button size="xs" className={className}>
|
||||
{method.toUpperCase()}
|
||||
</Button>
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { appWindow } from '@tauri-apps/api/window';
|
||||
import classNames from 'classnames';
|
||||
import type { CSSProperties } from 'react';
|
||||
import { memo, useCallback, useMemo, useState } from 'react';
|
||||
import { createGlobalState } from 'react-use';
|
||||
import { useActiveEnvironmentId } from '../hooks/useActiveEnvironmentId';
|
||||
import { useActiveRequest } from '../hooks/useActiveRequest';
|
||||
import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
|
||||
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
|
||||
import { useSettings } from '../hooks/useSettings';
|
||||
import { useUpdateRequest } from '../hooks/useUpdateRequest';
|
||||
import { useUpdateSettings } from '../hooks/useUpdateSettings';
|
||||
import { tryFormatJson } from '../lib/formatters';
|
||||
import type { HttpHeader, HttpRequest, HttpUrlParameter } from '../lib/models';
|
||||
import {
|
||||
@@ -24,10 +22,13 @@ import {
|
||||
} from '../lib/models';
|
||||
import { BasicAuth } from './BasicAuth';
|
||||
import { BearerAuth } from './BearerAuth';
|
||||
import { Checkbox } from './core/Checkbox';
|
||||
import { CountBadge } from './core/CountBadge';
|
||||
import { Editor } from './core/Editor';
|
||||
import type { TabItem } from './core/Tabs/Tabs';
|
||||
import { Input } from './core/Input';
|
||||
import { VStack } from './core/Stacks';
|
||||
import { TabContent, Tabs } from './core/Tabs/Tabs';
|
||||
import type { TabItem } from './core/Tabs/Tabs';
|
||||
import { EmptyStateText } from './EmptyStateText';
|
||||
import { FormMultipartEditor } from './FormMultipartEditor';
|
||||
import { FormUrlencodedEditor } from './FormUrlencodedEditor';
|
||||
@@ -47,7 +48,6 @@ const useActiveTab = createGlobalState<string>('body');
|
||||
export const RequestPane = memo(function RequestPane({ style, fullHeight, className }: Props) {
|
||||
const activeRequest = useActiveRequest();
|
||||
const activeRequestId = activeRequest?.id ?? null;
|
||||
const activeEnvironmentId = useActiveEnvironmentId();
|
||||
const updateRequest = useUpdateRequest(activeRequestId);
|
||||
const [activeTab, setActiveTab] = useActiveTab();
|
||||
const [forceUpdateHeaderEditorKey, setForceUpdateHeaderEditorKey] = useState<number>(0);
|
||||
@@ -183,18 +183,6 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN
|
||||
[updateRequest],
|
||||
);
|
||||
|
||||
useListenToTauriEvent(
|
||||
'send_request',
|
||||
async ({ windowLabel }) => {
|
||||
if (windowLabel !== appWindow.label) return;
|
||||
await invoke('send_request', {
|
||||
requestId: activeRequestId,
|
||||
environmentId: activeEnvironmentId,
|
||||
});
|
||||
},
|
||||
[activeRequestId, activeEnvironmentId],
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={style}
|
||||
@@ -202,7 +190,12 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN
|
||||
>
|
||||
{activeRequest && (
|
||||
<>
|
||||
<UrlBar id={activeRequest.id} url={activeRequest.url} method={activeRequest.method} />
|
||||
<UrlBar
|
||||
key={activeRequest.id} // Force-reset the url bar when the active request changes
|
||||
id={activeRequest.id}
|
||||
url={activeRequest.url}
|
||||
method={activeRequest.method}
|
||||
/>
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
label="Request"
|
||||
@@ -255,7 +248,7 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN
|
||||
defaultValue={`${activeRequest?.body?.text ?? ''}`}
|
||||
contentType="application/json"
|
||||
onChange={handleBodyTextChange}
|
||||
format={(v) => tryFormatJson(v)}
|
||||
format={tryFormatJson}
|
||||
/>
|
||||
) : activeRequest.bodyType === BODY_TYPE_XML ? (
|
||||
<Editor
|
||||
|
||||
@@ -3,8 +3,10 @@ import classNames from 'classnames';
|
||||
import type { CSSProperties, MouseEvent as ReactMouseEvent } from 'react';
|
||||
import React, { memo, useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { useLocalStorage } from 'react-use';
|
||||
import { useActiveRequest } from '../hooks/useActiveRequest';
|
||||
import { useActiveWorkspaceId } from '../hooks/useActiveWorkspaceId';
|
||||
import { clamp } from '../lib/clamp';
|
||||
import { HotKeyList } from './core/HotKeyList';
|
||||
import { RequestPane } from './RequestPane';
|
||||
import { ResizeHandle } from './ResizeHandle';
|
||||
import { ResponsePane } from './ResponsePane';
|
||||
@@ -20,10 +22,11 @@ const drag = { gridArea: 'drag' };
|
||||
const DEFAULT = 0.5;
|
||||
const MIN_WIDTH_PX = 10;
|
||||
const MIN_HEIGHT_PX = 30;
|
||||
const STACK_VERTICAL_WIDTH = 650;
|
||||
const STACK_VERTICAL_WIDTH = 700;
|
||||
|
||||
export const RequestResponse = memo(function RequestResponse({ style }: Props) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const activeRequest = useActiveRequest();
|
||||
const [vertical, setVertical] = useState<boolean>(false);
|
||||
const [widthRaw, setWidth] = useLocalStorage<number>(`body_width::${useActiveWorkspaceId()}`);
|
||||
const [heightRaw, setHeight] = useLocalStorage<number>(`body_height::${useActiveWorkspaceId()}`);
|
||||
@@ -34,7 +37,7 @@ export const RequestResponse = memo(function RequestResponse({ style }: Props) {
|
||||
null,
|
||||
);
|
||||
|
||||
useResizeObserver(containerRef, ({ contentRect }) => {
|
||||
useResizeObserver(containerRef.current, ({ contentRect }) => {
|
||||
setVertical(contentRect.width < STACK_VERTICAL_WIDTH);
|
||||
});
|
||||
|
||||
@@ -114,6 +117,10 @@ export const RequestResponse = memo(function RequestResponse({ style }: Props) {
|
||||
[width, height, vertical, setHeight, setWidth],
|
||||
);
|
||||
|
||||
if (activeRequest === null) {
|
||||
return <HotKeyList hotkeys={['request.create', 'sidebar.toggle']} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="grid gap-1.5 w-full h-full p-3" style={styles}>
|
||||
<RequestPane style={rqst} fullHeight={!vertical} />
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import classNames from 'classnames';
|
||||
import type { CSSProperties } from 'react';
|
||||
import { useCallback, memo, useEffect, useMemo, useState } from 'react';
|
||||
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { createGlobalState } from 'react-use';
|
||||
import { useActiveRequestId } from '../hooks/useActiveRequestId';
|
||||
import { useActiveRequest } from '../hooks/useActiveRequest';
|
||||
import { useLatestResponse } from '../hooks/useLatestResponse';
|
||||
import { useResponseContentType } from '../hooks/useResponseContentType';
|
||||
import { useResponses } from '../hooks/useResponses';
|
||||
@@ -12,18 +12,19 @@ import { isResponseLoading } from '../lib/models';
|
||||
import { Banner } from './core/Banner';
|
||||
import { CountBadge } from './core/CountBadge';
|
||||
import { DurationTag } from './core/DurationTag';
|
||||
import { HotKeyList } from './core/HotKeyList';
|
||||
import { SizeTag } from './core/SizeTag';
|
||||
import { HStack } from './core/Stacks';
|
||||
import { StatusTag } from './core/StatusTag';
|
||||
import type { TabItem } from './core/Tabs/Tabs';
|
||||
import { TabContent, Tabs } from './core/Tabs/Tabs';
|
||||
import { EmptyStateText } from './EmptyStateText';
|
||||
import { RecentResponsesDropdown } from './RecentResponsesDropdown';
|
||||
import { ResponseHeaders } from './ResponseHeaders';
|
||||
import { CsvViewer } from './responseViewers/CsvViewer';
|
||||
import { ImageViewer } from './responseViewers/ImageViewer';
|
||||
import { TextViewer } from './responseViewers/TextViewer';
|
||||
import { WebPageViewer } from './responseViewers/WebPageViewer';
|
||||
import { RecentResponsesDropdown } from './RecentResponsesDropdown';
|
||||
|
||||
interface Props {
|
||||
style?: CSSProperties;
|
||||
@@ -34,9 +35,9 @@ const useActiveTab = createGlobalState<string>('body');
|
||||
|
||||
export const ResponsePane = memo(function ResponsePane({ style, className }: Props) {
|
||||
const [pinnedResponseId, setPinnedResponseId] = useState<string | null>(null);
|
||||
const activeRequestId = useActiveRequestId();
|
||||
const latestResponse = useLatestResponse(activeRequestId);
|
||||
const responses = useResponses(activeRequestId);
|
||||
const activeRequest = useActiveRequest();
|
||||
const latestResponse = useLatestResponse(activeRequest?.id ?? null);
|
||||
const responses = useResponses(activeRequest?.id ?? null);
|
||||
const activeResponse: HttpResponse | null = pinnedResponseId
|
||||
? responses.find((r) => r.id === pinnedResponseId) ?? null
|
||||
: latestResponse ?? null;
|
||||
@@ -48,11 +49,14 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro
|
||||
|
||||
const contentType = useResponseContentType(activeResponse);
|
||||
|
||||
const handlePinnedResponse = useCallback((r: HttpResponse) => {
|
||||
setPinnedResponseId(r.id);
|
||||
}, [setPinnedResponseId])
|
||||
const handlePinnedResponse = useCallback(
|
||||
(r: HttpResponse) => {
|
||||
setPinnedResponseId(r.id);
|
||||
},
|
||||
[setPinnedResponseId],
|
||||
);
|
||||
|
||||
const tabs: TabItem[] = useMemo(
|
||||
const tabs = useMemo<TabItem[]>(
|
||||
() => [
|
||||
{
|
||||
value: 'body',
|
||||
@@ -62,7 +66,7 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro
|
||||
onChange: setViewMode,
|
||||
items: [
|
||||
{ label: 'Pretty', value: 'pretty' },
|
||||
{ label: 'Raw', value: 'raw' },
|
||||
...(contentType?.startsWith('image') ? [] : [{ label: 'Raw', value: 'raw' }]),
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -78,20 +82,32 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro
|
||||
value: 'headers',
|
||||
},
|
||||
],
|
||||
[activeResponse?.headers, setViewMode, viewMode],
|
||||
[activeResponse?.headers, contentType, setViewMode, viewMode],
|
||||
);
|
||||
|
||||
if (activeRequest === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={style}
|
||||
className={classNames(
|
||||
className,
|
||||
'bg-gray-50 max-h-full h-full grid grid-rows-[auto_minmax(0,1fr)] grid-cols-1',
|
||||
'dark:bg-gray-100 rounded-md border border-highlight',
|
||||
'max-h-full h-full grid grid-rows-[auto_minmax(0,1fr)] grid-cols-1',
|
||||
'bg-gray-50 dark:bg-gray-100 rounded-md border border-highlight',
|
||||
'shadow shadow-gray-100 dark:shadow-gray-0 relative',
|
||||
)}
|
||||
>
|
||||
{activeResponse?.error && <Banner className="m-2">{activeResponse.error}</Banner>}
|
||||
{!activeResponse && (
|
||||
<>
|
||||
<span />
|
||||
<HotKeyList
|
||||
hotkeys={['request.send', 'request.create', 'sidebar.toggle', 'urlBar.focus']}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{activeResponse && !activeResponse.error && !isResponseLoading(activeResponse) && (
|
||||
<>
|
||||
<HStack
|
||||
@@ -145,10 +161,14 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro
|
||||
<TabContent value="body">
|
||||
{!activeResponse.contentLength ? (
|
||||
<EmptyStateText>Empty Body</EmptyStateText>
|
||||
) : viewMode === 'pretty' && contentType?.includes('html') ? (
|
||||
<WebPageViewer response={activeResponse} />
|
||||
) : contentType?.startsWith('image') ? (
|
||||
<ImageViewer className="pb-2" response={activeResponse} />
|
||||
) : activeResponse.contentLength > 2 * 1000 * 1000 ? (
|
||||
<div className="text-sm italic text-gray-500">
|
||||
Cannot preview text responses larger than 2MB
|
||||
</div>
|
||||
) : viewMode === 'pretty' && contentType?.includes('html') ? (
|
||||
<WebPageViewer response={activeResponse} />
|
||||
) : contentType?.match(/csv|tab-separated/) ? (
|
||||
<CsvViewer className="pb-2" response={activeResponse} />
|
||||
) : (
|
||||
|
||||
@@ -7,7 +7,7 @@ import { VStack } from './core/Stacks';
|
||||
|
||||
export default function RouteError() {
|
||||
const error = useRouteError();
|
||||
console.log("Error", error);
|
||||
console.log('Error', error);
|
||||
const stringified = JSON.stringify(error);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const message = (error as any).message ?? stringified;
|
||||
|
||||
92
src-web/components/SettingsDialog.tsx
Normal file
92
src-web/components/SettingsDialog.tsx
Normal file
@@ -0,0 +1,92 @@
|
||||
import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
|
||||
import { useSettings } from '../hooks/useSettings';
|
||||
import { useUpdateSettings } from '../hooks/useUpdateSettings';
|
||||
import { useUpdateWorkspace } from '../hooks/useUpdateWorkspace';
|
||||
import { Checkbox } from './core/Checkbox';
|
||||
import { Heading } from './core/Heading';
|
||||
import { Input } from './core/Input';
|
||||
import { Select } from './core/Select';
|
||||
import { Separator } from './core/Separator';
|
||||
import { VStack } from './core/Stacks';
|
||||
|
||||
export const SettingsDialog = () => {
|
||||
const workspace = useActiveWorkspace();
|
||||
const updateWorkspace = useUpdateWorkspace(workspace?.id ?? null);
|
||||
const settings = useSettings();
|
||||
const updateSettings = useUpdateSettings();
|
||||
|
||||
if (settings == null || workspace == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<VStack space={2} className="mb-2">
|
||||
<Select
|
||||
name="appearance"
|
||||
label="Appearance"
|
||||
labelPosition="left"
|
||||
labelClassName="w-1/3"
|
||||
size="sm"
|
||||
value={settings.appearance}
|
||||
onChange={(appearance) => updateSettings.mutateAsync({ ...settings, appearance })}
|
||||
options={{
|
||||
system: 'System',
|
||||
light: 'Light',
|
||||
dark: 'Dark',
|
||||
}}
|
||||
/>
|
||||
|
||||
<Select
|
||||
name="updateChannel"
|
||||
label="Update Channel"
|
||||
labelPosition="left"
|
||||
labelClassName="w-1/3"
|
||||
size="sm"
|
||||
value={settings.updateChannel}
|
||||
onChange={(updateChannel) => updateSettings.mutateAsync({ ...settings, updateChannel })}
|
||||
options={{
|
||||
stable: 'Release',
|
||||
beta: 'Early Bird (Beta)',
|
||||
}}
|
||||
/>
|
||||
|
||||
<Separator className="my-4" />
|
||||
|
||||
<Heading size={2}>
|
||||
Workspace{' '}
|
||||
<div className="inline-block ml-1 bg-gray-500 dark:bg-gray-300 px-2 py-0.5 text-sm rounded text-white dark:text-gray-900">
|
||||
{workspace.name}
|
||||
</div>
|
||||
</Heading>
|
||||
<VStack className="mt-1 w-full" space={3}>
|
||||
<Input
|
||||
size="sm"
|
||||
name="requestTimeout"
|
||||
label="Request Timeout (ms)"
|
||||
labelPosition="left"
|
||||
labelClassName="w-1/3"
|
||||
containerClassName="col-span-2"
|
||||
defaultValue={`${workspace.settingRequestTimeout}`}
|
||||
validate={(value) => parseInt(value) >= 0}
|
||||
onChange={(v) => updateWorkspace.mutateAsync({ settingRequestTimeout: parseInt(v) || 0 })}
|
||||
/>
|
||||
|
||||
<Checkbox
|
||||
checked={workspace.settingValidateCertificates}
|
||||
title="Validate TLS Certificates"
|
||||
onChange={(settingValidateCertificates) =>
|
||||
updateWorkspace.mutateAsync({ settingValidateCertificates })
|
||||
}
|
||||
/>
|
||||
|
||||
<Checkbox
|
||||
checked={workspace.settingFollowRedirects}
|
||||
title="Follow Redirects"
|
||||
onChange={(settingFollowRedirects) =>
|
||||
updateWorkspace.mutateAsync({ settingFollowRedirects })
|
||||
}
|
||||
/>
|
||||
</VStack>
|
||||
</VStack>
|
||||
);
|
||||
};
|
||||
129
src-web/components/SettingsDropdown.tsx
Normal file
129
src-web/components/SettingsDropdown.tsx
Normal file
@@ -0,0 +1,129 @@
|
||||
import { invoke, shell } from '@tauri-apps/api';
|
||||
import { useRef, useState } from 'react';
|
||||
import { useAppVersion } from '../hooks/useAppVersion';
|
||||
import { useExportData } from '../hooks/useExportData';
|
||||
import { useImportData } from '../hooks/useImportData';
|
||||
import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
|
||||
import { Button } from './core/Button';
|
||||
import type { DropdownRef } from './core/Dropdown';
|
||||
import { Dropdown } from './core/Dropdown';
|
||||
import { Icon } from './core/Icon';
|
||||
import { IconButton } from './core/IconButton';
|
||||
import { VStack } from './core/Stacks';
|
||||
import { useDialog } from './DialogContext';
|
||||
import { KeyboardShortcutsDialog } from './KeyboardShortcutsDialog';
|
||||
import { SettingsDialog } from './SettingsDialog';
|
||||
|
||||
export function SettingsDropdown() {
|
||||
const importData = useImportData();
|
||||
const exportData = useExportData();
|
||||
const appVersion = useAppVersion();
|
||||
const dropdownRef = useRef<DropdownRef>(null);
|
||||
const dialog = useDialog();
|
||||
const [showChangelog, setShowChangelog] = useState<boolean>(false);
|
||||
|
||||
useListenToTauriEvent('show_changelog', () => {
|
||||
setShowChangelog(true);
|
||||
});
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
ref={dropdownRef}
|
||||
onClose={() => setShowChangelog(false)}
|
||||
items={[
|
||||
{
|
||||
key: 'settings',
|
||||
label: 'Settings',
|
||||
hotKeyAction: 'settings.show',
|
||||
leftSlot: <Icon icon="settings" />,
|
||||
onSelect: () => {
|
||||
dialog.show({
|
||||
id: 'settings',
|
||||
size: 'md',
|
||||
title: 'Settings',
|
||||
render: () => <SettingsDialog />,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'hotkeys',
|
||||
label: 'Keyboard shortcuts',
|
||||
hotKeyAction: 'hotkeys.showHelp',
|
||||
leftSlot: <Icon icon="keyboard" />,
|
||||
onSelect: () => {
|
||||
dialog.show({
|
||||
id: 'hotkey-help',
|
||||
title: 'Keyboard Shortcuts',
|
||||
size: 'sm',
|
||||
render: () => <KeyboardShortcutsDialog />,
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'import-data',
|
||||
label: 'Import Data',
|
||||
leftSlot: <Icon icon="folderInput" />,
|
||||
onSelect: () => {
|
||||
dialog.show({
|
||||
title: 'Import Data',
|
||||
size: 'sm',
|
||||
render: ({ hide }) => {
|
||||
return (
|
||||
<VStack space={3}>
|
||||
<p>Insomnia or Postman Collection v2/v2.1 formats are supported</p>
|
||||
<Button
|
||||
size="sm"
|
||||
color="primary"
|
||||
onClick={async () => {
|
||||
await importData.mutateAsync();
|
||||
hide();
|
||||
}}
|
||||
>
|
||||
Select File
|
||||
</Button>
|
||||
</VStack>
|
||||
);
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'export-data',
|
||||
label: 'Export Data',
|
||||
leftSlot: <Icon icon="folderOutput" />,
|
||||
onSelect: () => exportData.mutate(),
|
||||
},
|
||||
{ type: 'separator', label: `Yaak v${appVersion.data}` },
|
||||
{
|
||||
key: 'update-check',
|
||||
label: 'Check for Updates',
|
||||
leftSlot: <Icon icon="update" />,
|
||||
onSelect: () => invoke('check_for_updates'),
|
||||
},
|
||||
{
|
||||
key: 'feedback',
|
||||
label: 'Feedback',
|
||||
leftSlot: <Icon icon="chat" />,
|
||||
rightSlot: <Icon icon="externalLink" />,
|
||||
onSelect: () => shell.open('https://yaak.canny.io'),
|
||||
},
|
||||
{
|
||||
key: 'changelog',
|
||||
label: 'Changelog',
|
||||
variant: showChangelog ? 'notify' : 'default',
|
||||
leftSlot: <Icon icon="cake" />,
|
||||
rightSlot: <Icon icon="externalLink" />,
|
||||
onSelect: () => shell.open(`https://yaak.app/changelog/${appVersion.data}`),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<IconButton
|
||||
size="sm"
|
||||
title="Main Menu"
|
||||
icon="settings"
|
||||
className="pointer-events-auto"
|
||||
showBadge={showChangelog}
|
||||
/>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
@@ -5,8 +5,8 @@ import type { XYCoord } from 'react-dnd';
|
||||
import { useDrag, useDrop } from 'react-dnd';
|
||||
import { useKey, useKeyPressEvent } from 'react-use';
|
||||
|
||||
import { showMenu } from 'tauri-plugin-context-menu';
|
||||
import { useActiveEnvironmentId } from '../hooks/useActiveEnvironmentId';
|
||||
import { useActiveRequest } from '../hooks/useActiveRequest';
|
||||
import { useActiveRequestId } from '../hooks/useActiveRequestId';
|
||||
import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
|
||||
import { useAppRoutes } from '../hooks/useAppRoutes';
|
||||
@@ -15,10 +15,11 @@ import { useCreateRequest } from '../hooks/useCreateRequest';
|
||||
import { useDeleteAnyRequest } from '../hooks/useDeleteAnyRequest';
|
||||
import { useDeleteFolder } from '../hooks/useDeleteFolder';
|
||||
import { useDeleteRequest } from '../hooks/useDeleteRequest';
|
||||
import { useDuplicateRequest } from '../hooks/useDuplicateRequest';
|
||||
import { useFolders } from '../hooks/useFolders';
|
||||
import { useHotKey } from '../hooks/useHotKey';
|
||||
import { useKeyValue } from '../hooks/useKeyValue';
|
||||
import { useLatestResponse } from '../hooks/useLatestResponse';
|
||||
import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
|
||||
import { usePrompt } from '../hooks/usePrompt';
|
||||
import { useRequests } from '../hooks/useRequests';
|
||||
import { useSendManyRequests } from '../hooks/useSendFolder';
|
||||
@@ -31,9 +32,8 @@ import { fallbackRequestName } from '../lib/fallbackRequestName';
|
||||
import { NAMESPACE_NO_SYNC } from '../lib/keyValueStore';
|
||||
import type { Folder, HttpRequest, Workspace } from '../lib/models';
|
||||
import { isResponseLoading } from '../lib/models';
|
||||
import { Dropdown } from './core/Dropdown';
|
||||
import { ContextMenu } from './core/Dropdown';
|
||||
import { Icon } from './core/Icon';
|
||||
import { IconButton } from './core/IconButton';
|
||||
import { InlineCode } from './core/InlineCode';
|
||||
import { VStack } from './core/Stacks';
|
||||
import { StatusTag } from './core/StatusTag';
|
||||
@@ -55,7 +55,6 @@ interface TreeNode {
|
||||
|
||||
export function Sidebar({ className }: Props) {
|
||||
const { hidden } = useSidebarHidden();
|
||||
const createRequest = useCreateRequest();
|
||||
const sidebarRef = useRef<HTMLLIElement>(null);
|
||||
const activeRequestId = useActiveRequestId();
|
||||
const activeEnvironmentId = useActiveEnvironmentId();
|
||||
@@ -63,6 +62,7 @@ export function Sidebar({ className }: Props) {
|
||||
const folders = useFolders();
|
||||
const deleteAnyRequest = useDeleteAnyRequest();
|
||||
const activeWorkspace = useActiveWorkspace();
|
||||
const duplicateRequest = useDuplicateRequest({ id: activeRequestId, navigateAfter: true });
|
||||
const routes = useAppRoutes();
|
||||
const [hasFocus, setHasFocus] = useState<boolean>(false);
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
@@ -78,13 +78,30 @@ export function Sidebar({ className }: Props) {
|
||||
namespace: NAMESPACE_NO_SYNC,
|
||||
});
|
||||
|
||||
useHotKey('request.duplicate', () => {
|
||||
duplicateRequest.mutate();
|
||||
});
|
||||
|
||||
const isCollapsed = useCallback(
|
||||
(id: string) => collapsed.value?.[id] ?? false,
|
||||
[collapsed.value],
|
||||
);
|
||||
|
||||
const { tree, treeParentMap, selectableRequests } = useMemo<{
|
||||
tree: TreeNode | null;
|
||||
treeParentMap: Record<string, TreeNode>;
|
||||
selectableRequests: { id: string; index: number; tree: TreeNode }[];
|
||||
selectableRequests: {
|
||||
id: string;
|
||||
index: number;
|
||||
tree: TreeNode;
|
||||
}[];
|
||||
}>(() => {
|
||||
const treeParentMap: Record<string, TreeNode> = {};
|
||||
const selectableRequests: { id: string; index: number; tree: TreeNode }[] = [];
|
||||
const selectableRequests: {
|
||||
id: string;
|
||||
index: number;
|
||||
tree: TreeNode;
|
||||
}[] = [];
|
||||
if (activeWorkspace == null) {
|
||||
return { tree: null, treeParentMap, selectableRequests };
|
||||
}
|
||||
@@ -114,11 +131,16 @@ export function Sidebar({ className }: Props) {
|
||||
return { tree, treeParentMap, selectableRequests };
|
||||
}, [activeWorkspace, requests, folders]);
|
||||
|
||||
// TODO: Move these listeners to a central place
|
||||
useListenToTauriEvent('new_request', async () => createRequest.mutate({}));
|
||||
|
||||
const focusActiveRequest = useCallback(
|
||||
(args: { forced?: { id: string; tree: TreeNode }; noFocusSidebar?: boolean } = {}) => {
|
||||
(
|
||||
args: {
|
||||
forced?: {
|
||||
id: string;
|
||||
tree: TreeNode;
|
||||
};
|
||||
noFocusSidebar?: boolean;
|
||||
} = {},
|
||||
) => {
|
||||
const { forced, noFocusSidebar } = args;
|
||||
const tree = forced?.tree ?? treeParentMap[activeRequestId ?? 'n/a'] ?? null;
|
||||
const children = tree?.children ?? [];
|
||||
@@ -191,19 +213,15 @@ export function Sidebar({ className }: Props) {
|
||||
useKeyPressEvent('Backspace', handleDeleteKey);
|
||||
useKeyPressEvent('Delete', handleDeleteKey);
|
||||
|
||||
useListenToTauriEvent(
|
||||
'focus_sidebar',
|
||||
() => {
|
||||
if (hidden || hasFocus) return;
|
||||
// Select 0 index on focus if none selected
|
||||
focusActiveRequest(
|
||||
selectedTree != null && selectedId != null
|
||||
? { forced: { id: selectedId, tree: selectedTree } }
|
||||
: undefined,
|
||||
);
|
||||
},
|
||||
[focusActiveRequest, hidden, activeRequestId],
|
||||
);
|
||||
useHotKey('sidebar.focus', () => {
|
||||
if (hidden || hasFocus) return;
|
||||
// Select 0 index on focus if none selected
|
||||
focusActiveRequest(
|
||||
selectedTree != null && selectedId != null
|
||||
? { forced: { id: selectedId, tree: selectedTree } }
|
||||
: undefined,
|
||||
);
|
||||
});
|
||||
|
||||
useKeyPressEvent('Enter', (e) => {
|
||||
if (!hasFocus) return;
|
||||
@@ -258,13 +276,21 @@ export function Sidebar({ className }: Props) {
|
||||
|
||||
const handleMove = useCallback<DraggableSidebarItemProps['onMove']>(
|
||||
(id, side) => {
|
||||
const hoveredTree = treeParentMap[id] ?? null;
|
||||
let hoveredTree = treeParentMap[id] ?? null;
|
||||
const dragIndex = hoveredTree?.children.findIndex((n) => n.item.id === id) ?? -99;
|
||||
const hoveredIndex = dragIndex + (side === 'above' ? 0 : 1);
|
||||
const hoveredItem = hoveredTree?.children[dragIndex]?.item ?? null;
|
||||
let hoveredIndex = dragIndex + (side === 'above' ? 0 : 1);
|
||||
|
||||
if (hoveredItem?.model === 'folder' && side === 'below' && !isCollapsed(hoveredItem.id)) {
|
||||
// Move into folder if it's open and we're moving below it
|
||||
hoveredTree = hoveredTree?.children.find((n) => n.item.id === id) ?? null;
|
||||
hoveredIndex = 0;
|
||||
}
|
||||
|
||||
setHoveredTree(hoveredTree);
|
||||
setHoveredIndex(hoveredIndex);
|
||||
},
|
||||
[treeParentMap],
|
||||
[isCollapsed, treeParentMap],
|
||||
);
|
||||
|
||||
const handleDragStart = useCallback<DraggableSidebarItemProps['onDragStart']>((id: string) => {
|
||||
@@ -297,8 +323,9 @@ export function Sidebar({ className }: Props) {
|
||||
newChildren.splice(hoveredIndex - 1, 0, child);
|
||||
}
|
||||
|
||||
const prev = newChildren[hoveredIndex - 1]?.item;
|
||||
const next = newChildren[hoveredIndex + 1]?.item;
|
||||
const insertedIndex = newChildren.findIndex((c) => c.item === child.item);
|
||||
const prev = newChildren[insertedIndex - 1]?.item;
|
||||
const next = newChildren[insertedIndex + 1]?.item;
|
||||
const beforePriority = prev == null || prev.model === 'workspace' ? 0 : prev.sortPriority;
|
||||
const afterPriority = next == null || next.model === 'workspace' ? 0 : next.sortPriority;
|
||||
|
||||
@@ -340,11 +367,8 @@ export function Sidebar({ className }: Props) {
|
||||
],
|
||||
);
|
||||
|
||||
if (tree == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (collapsed.value == null) {
|
||||
// Not ready to render yet
|
||||
if (tree == null || collapsed.value == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -364,7 +388,7 @@ export function Sidebar({ className }: Props) {
|
||||
treeParentMap={treeParentMap}
|
||||
selectedId={selectedId}
|
||||
selectedTree={selectedTree}
|
||||
collapsed={collapsed.value}
|
||||
isCollapsed={isCollapsed}
|
||||
tree={tree}
|
||||
focused={hasFocus}
|
||||
draggingId={draggingId}
|
||||
@@ -392,7 +416,7 @@ interface SidebarItemsProps {
|
||||
handleEnd: (id: string) => void;
|
||||
handleDragStart: (id: string) => void;
|
||||
onSelect: (requestId: string) => void;
|
||||
collapsed: Record<string, boolean>;
|
||||
isCollapsed: (id: string) => boolean;
|
||||
}
|
||||
|
||||
function SidebarItems({
|
||||
@@ -403,7 +427,7 @@ function SidebarItems({
|
||||
draggingId,
|
||||
onSelect,
|
||||
treeParentMap,
|
||||
collapsed,
|
||||
isCollapsed,
|
||||
hoveredTree,
|
||||
hoveredIndex,
|
||||
handleEnd,
|
||||
@@ -438,16 +462,16 @@ function SidebarItems({
|
||||
onSelect={onSelect}
|
||||
onDragStart={handleDragStart}
|
||||
useProminentStyles={focused}
|
||||
collapsed={collapsed}
|
||||
isCollapsed={isCollapsed}
|
||||
child={child}
|
||||
>
|
||||
{child.item.model === 'folder' &&
|
||||
!collapsed[child.item.id] &&
|
||||
!isCollapsed(child.item.id) &&
|
||||
draggingId !== child.item.id && (
|
||||
<SidebarItems
|
||||
treeParentMap={treeParentMap}
|
||||
tree={child}
|
||||
collapsed={collapsed}
|
||||
isCollapsed={isCollapsed}
|
||||
draggingId={draggingId}
|
||||
hoveredTree={hoveredTree}
|
||||
hoveredIndex={hoveredIndex}
|
||||
@@ -478,12 +502,10 @@ type SidebarItemProps = {
|
||||
itemModel: string;
|
||||
useProminentStyles?: boolean;
|
||||
selected?: boolean;
|
||||
onSelect: (id: string) => void;
|
||||
draggable?: boolean;
|
||||
children?: ReactNode;
|
||||
collapsed: Record<string, boolean>;
|
||||
child: TreeNode;
|
||||
};
|
||||
} & Pick<SidebarItemsProps, 'isCollapsed' | 'onSelect'>;
|
||||
|
||||
const SidebarItem = forwardRef(function SidebarItem(
|
||||
{
|
||||
@@ -496,24 +518,26 @@ const SidebarItem = forwardRef(function SidebarItem(
|
||||
useProminentStyles,
|
||||
selected,
|
||||
onSelect,
|
||||
collapsed,
|
||||
isCollapsed,
|
||||
child,
|
||||
}: SidebarItemProps,
|
||||
ref: ForwardedRef<HTMLLIElement>,
|
||||
) {
|
||||
const activeRequest = useActiveRequest();
|
||||
const createRequest = useCreateRequest();
|
||||
const createFolder = useCreateFolder();
|
||||
const deleteFolder = useDeleteFolder(itemId);
|
||||
const sendRequest = useSendRequest(itemId);
|
||||
const sendManyRequests = useSendManyRequests();
|
||||
const deleteRequest = useDeleteRequest(itemId);
|
||||
const duplicateRequest = useDuplicateRequest({ id: itemId, navigateAfter: true });
|
||||
const sendRequest = useSendRequest(itemId);
|
||||
const sendAndDownloadRequest = useSendRequest(itemId, { download: true });
|
||||
const sendManyRequests = useSendManyRequests();
|
||||
const latestResponse = useLatestResponse(itemId);
|
||||
const updateRequest = useUpdateRequest(itemId);
|
||||
const updateAnyFolder = useUpdateAnyFolder();
|
||||
const prompt = usePrompt();
|
||||
const [editing, setEditing] = useState<boolean>(false);
|
||||
const activeRequestId = useActiveRequestId();
|
||||
const isActive = activeRequestId === itemId;
|
||||
const isActive = activeRequest?.id === itemId;
|
||||
|
||||
const handleSubmitNameEdit = useCallback(
|
||||
(el: HTMLInputElement) => {
|
||||
@@ -557,105 +581,103 @@ const SidebarItem = forwardRef(function SidebarItem(
|
||||
[handleSubmitNameEdit],
|
||||
);
|
||||
|
||||
const handleContextMenu = useCallback(
|
||||
(e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
showMenu({
|
||||
pos: { x: e.clientX, y: e.clientY },
|
||||
items:
|
||||
itemModel === 'http_request'
|
||||
? [
|
||||
{
|
||||
label: 'Send Request',
|
||||
event: () => sendRequest.mutate(),
|
||||
},
|
||||
{
|
||||
label: 'Delete Request',
|
||||
event: () => deleteRequest.mutate(),
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
label: 'Send All',
|
||||
event: () => sendManyRequests.mutate(child.children.map((c) => c.item.id)),
|
||||
},
|
||||
{
|
||||
label: 'Delete Folder',
|
||||
event: () => deleteFolder.mutate(),
|
||||
},
|
||||
],
|
||||
})
|
||||
.then((r) => console.log(r))
|
||||
.catch((e) => console.log(e));
|
||||
},
|
||||
[itemModel, sendRequest, deleteRequest, sendManyRequests, child.children, deleteFolder],
|
||||
);
|
||||
|
||||
const handleSelect = useCallback(() => onSelect(itemId), [onSelect, itemId]);
|
||||
const [showContextMenu, setShowContextMenu] = useState<{
|
||||
x: number;
|
||||
y: number;
|
||||
} | null>(null);
|
||||
|
||||
const handleContextMenu = useCallback((e: React.MouseEvent<HTMLElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setShowContextMenu({ x: e.clientX, y: e.clientY });
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<li ref={ref}>
|
||||
<div className={classNames(className, 'block relative group/item px-2 pb-0.5')}>
|
||||
{itemModel === 'folder' && (
|
||||
<Dropdown
|
||||
items={[
|
||||
{
|
||||
key: 'sendAll',
|
||||
label: 'Send All',
|
||||
leftSlot: <Icon icon="paperPlane" />,
|
||||
onSelect: () => sendManyRequests.mutate(child.children.map((c) => c.item.id)),
|
||||
},
|
||||
{ type: 'separator', label: itemName },
|
||||
{
|
||||
key: 'rename',
|
||||
label: 'Rename',
|
||||
leftSlot: <Icon icon="pencil" />,
|
||||
onSelect: async () => {
|
||||
const name = await prompt({
|
||||
title: 'Rename Folder',
|
||||
description: (
|
||||
<>
|
||||
Enter a new name for <InlineCode>{itemName}</InlineCode>
|
||||
</>
|
||||
),
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
defaultValue: itemName,
|
||||
});
|
||||
updateAnyFolder.mutate({ id: itemId, update: (f) => ({ ...f, name }) });
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'deleteFolder',
|
||||
label: 'Delete',
|
||||
variant: 'danger',
|
||||
leftSlot: <Icon icon="trash" />,
|
||||
onSelect: () => deleteFolder.mutate(),
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
key: 'createRequest',
|
||||
label: 'New Request',
|
||||
leftSlot: <Icon icon="plus" />,
|
||||
onSelect: () => createRequest.mutate({ folderId: itemId, sortPriority: -1 }),
|
||||
},
|
||||
{
|
||||
key: 'createFolder',
|
||||
label: 'New Folder',
|
||||
leftSlot: <Icon icon="plus" />,
|
||||
onSelect: () => createFolder.mutate({ folderId: itemId, sortPriority: -1 }),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<IconButton
|
||||
title="Folder options"
|
||||
size="xs"
|
||||
icon="dotsV"
|
||||
className="ml-auto !bg-transparent absolute right-2 opacity-0 group-hover/item:opacity-70 transition-opacity"
|
||||
/>
|
||||
</Dropdown>
|
||||
)}
|
||||
<ContextMenu
|
||||
show={showContextMenu}
|
||||
items={
|
||||
itemModel === 'folder'
|
||||
? [
|
||||
{
|
||||
key: 'sendAll',
|
||||
label: 'Send All',
|
||||
leftSlot: <Icon icon="sendHorizontal" />,
|
||||
onSelect: () => sendManyRequests.mutate(child.children.map((c) => c.item.id)),
|
||||
},
|
||||
{
|
||||
key: 'rename',
|
||||
label: 'Rename',
|
||||
leftSlot: <Icon icon="pencil" />,
|
||||
onSelect: async () => {
|
||||
const name = await prompt({
|
||||
title: 'Rename Folder',
|
||||
description: (
|
||||
<>
|
||||
Enter a new name for <InlineCode>{itemName}</InlineCode>
|
||||
</>
|
||||
),
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
defaultValue: itemName,
|
||||
});
|
||||
updateAnyFolder.mutate({ id: itemId, update: (f) => ({ ...f, name }) });
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'deleteFolder',
|
||||
label: 'Delete',
|
||||
variant: 'danger',
|
||||
leftSlot: <Icon icon="trash" />,
|
||||
onSelect: () => deleteFolder.mutate(),
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
key: 'createRequest',
|
||||
label: 'New Request',
|
||||
leftSlot: <Icon icon="plus" />,
|
||||
onSelect: () => createRequest.mutate({ folderId: itemId }),
|
||||
},
|
||||
{
|
||||
key: 'createFolder',
|
||||
label: 'New Folder',
|
||||
leftSlot: <Icon icon="plus" />,
|
||||
onSelect: () => createFolder.mutate({ folderId: itemId, sortPriority: -1 }),
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
key: 'sendRequest',
|
||||
label: 'Send',
|
||||
hotKeyAction: 'request.send',
|
||||
hotKeyLabelOnly: true, // Already bound in URL bar
|
||||
leftSlot: <Icon icon="sendHorizontal" />,
|
||||
onSelect: () => sendRequest.mutate(),
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
key: 'duplicateRequest',
|
||||
label: 'Duplicate',
|
||||
hotKeyAction: 'request.duplicate',
|
||||
hotKeyLabelOnly: true, // Would trigger for every request (bad)
|
||||
leftSlot: <Icon icon="copy" />,
|
||||
onSelect: () => {
|
||||
duplicateRequest.mutate();
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'deleteRequest',
|
||||
variant: 'danger',
|
||||
label: 'Delete',
|
||||
leftSlot: <Icon icon="trash" />,
|
||||
onSelect: () => deleteRequest.mutate(),
|
||||
},
|
||||
]
|
||||
}
|
||||
onClose={() => setShowContextMenu(null)}
|
||||
/>
|
||||
<button
|
||||
// tabIndex={-1} // Will prevent drag-n-drop
|
||||
disabled={editing}
|
||||
@@ -679,7 +701,7 @@ const SidebarItem = forwardRef(function SidebarItem(
|
||||
icon="chevronRight"
|
||||
className={classNames(
|
||||
'-ml-0.5 mr-2 transition-transform',
|
||||
!collapsed[itemId] && 'transform rotate-90',
|
||||
!isCollapsed(itemId) && 'transform rotate-90',
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
@@ -750,7 +772,13 @@ function DraggableSidebarItem({
|
||||
[onMove],
|
||||
);
|
||||
|
||||
const [{ isDragging }, connectDrag] = useDrag<DragItem, unknown, { isDragging: boolean }>(
|
||||
const [{ isDragging }, connectDrag] = useDrag<
|
||||
DragItem,
|
||||
unknown,
|
||||
{
|
||||
isDragging: boolean;
|
||||
}
|
||||
>(
|
||||
() => ({
|
||||
type: ItemTypes.REQUEST,
|
||||
item: () => {
|
||||
|
||||
@@ -2,8 +2,8 @@ import { memo } from 'react';
|
||||
import { useCreateFolder } from '../hooks/useCreateFolder';
|
||||
import { useCreateRequest } from '../hooks/useCreateRequest';
|
||||
import { useSidebarHidden } from '../hooks/useSidebarHidden';
|
||||
import { trackEvent } from '../lib/analytics';
|
||||
import { Dropdown } from './core/Dropdown';
|
||||
import { Icon } from './core/Icon';
|
||||
import { IconButton } from './core/IconButton';
|
||||
import { HStack } from './core/Stacks';
|
||||
|
||||
@@ -15,10 +15,14 @@ export const SidebarActions = memo(function SidebarActions() {
|
||||
return (
|
||||
<HStack>
|
||||
<IconButton
|
||||
onClick={toggle}
|
||||
onClick={() => {
|
||||
trackEvent('Sidebar', 'Toggle');
|
||||
toggle();
|
||||
}}
|
||||
className="pointer-events-auto"
|
||||
size="sm"
|
||||
title="Show sidebar"
|
||||
hotkeyAction="sidebar.toggle"
|
||||
icon={hidden ? 'leftPanelHidden' : 'leftPanelVisible'}
|
||||
/>
|
||||
<Dropdown
|
||||
@@ -26,13 +30,12 @@ export const SidebarActions = memo(function SidebarActions() {
|
||||
{
|
||||
key: 'create-request',
|
||||
label: 'New Request',
|
||||
leftSlot: <Icon icon="plus" />,
|
||||
hotKeyAction: 'request.create',
|
||||
onSelect: () => createRequest.mutate({}),
|
||||
},
|
||||
{
|
||||
key: 'create-folder',
|
||||
label: 'New Folder',
|
||||
leftSlot: <Icon icon="plus" />,
|
||||
onSelect: () => createFolder.mutate({}),
|
||||
},
|
||||
]}
|
||||
|
||||
@@ -2,8 +2,8 @@ import classNames from 'classnames';
|
||||
import type { EditorView } from 'codemirror';
|
||||
import type { FormEvent } from 'react';
|
||||
import { memo, useCallback, useRef, useState } from 'react';
|
||||
import { useHotKey } from '../hooks/useHotKey';
|
||||
import { useIsResponseLoading } from '../hooks/useIsResponseLoading';
|
||||
import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
|
||||
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
|
||||
import { useSendRequest } from '../hooks/useSendRequest';
|
||||
import { useUpdateRequest } from '../hooks/useUpdateRequest';
|
||||
@@ -40,7 +40,11 @@ export const UrlBar = memo(function UrlBar({ id: requestId, url, method, classNa
|
||||
[sendRequest],
|
||||
);
|
||||
|
||||
useListenToTauriEvent('focus_url', () => {
|
||||
useHotKey('urlBar.focus', () => {
|
||||
const head = inputRef.current?.state.doc.length ?? 0;
|
||||
inputRef.current?.dispatch({
|
||||
selection: { anchor: 0, head },
|
||||
});
|
||||
inputRef.current?.focus();
|
||||
});
|
||||
|
||||
@@ -67,18 +71,19 @@ export const UrlBar = memo(function UrlBar({ id: requestId, url, method, classNa
|
||||
<RequestMethodDropdown
|
||||
method={method}
|
||||
onChange={handleMethodChange}
|
||||
className="!h-auto mx-0.5 my-0.5"
|
||||
className="mx-0.5 my-0.5"
|
||||
/>
|
||||
}
|
||||
rightSlot={
|
||||
<IconButton
|
||||
size="xs"
|
||||
iconSize="sm"
|
||||
iconSize="md"
|
||||
title="Send Request"
|
||||
type="submit"
|
||||
className="!h-auto w-8 mr-0.5 my-0.5"
|
||||
icon={loading ? 'update' : 'paperPlane'}
|
||||
className="w-8 mr-0.5 my-0.5"
|
||||
icon={loading ? 'update' : 'sendHorizontal'}
|
||||
spin={loading}
|
||||
hotkeyAction="request.send"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -8,11 +8,10 @@ import type {
|
||||
} from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useWindowSize } from 'react-use';
|
||||
import { useIsFullscreen } from '../hooks/useIsFullscreen';
|
||||
import { useOsInfo } from '../hooks/useOsInfo';
|
||||
import { useSidebarHidden } from '../hooks/useSidebarHidden';
|
||||
import { useSidebarWidth } from '../hooks/useSidebarWidth';
|
||||
import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
|
||||
import { WINDOW_FLOATING_SIDEBAR_WIDTH } from '../lib/constants';
|
||||
import { Button } from './core/Button';
|
||||
import { HStack } from './core/Stacks';
|
||||
import { Overlay } from './Overlay';
|
||||
@@ -27,9 +26,11 @@ const head = { gridArea: 'head' };
|
||||
const body = { gridArea: 'body' };
|
||||
const drag = { gridArea: 'drag' };
|
||||
|
||||
const WINDOW_FLOATING_SIDEBAR_WIDTH = 600;
|
||||
|
||||
export default function Workspace() {
|
||||
const { setWidth, width, resetWidth } = useSidebarWidth();
|
||||
const { hide, show, hidden, toggle } = useSidebarHidden();
|
||||
const { hide, show, hidden } = useSidebarHidden();
|
||||
|
||||
const windowSize = useWindowSize();
|
||||
const [floating, setFloating] = useState<boolean>(false);
|
||||
@@ -38,14 +39,16 @@ export default function Workspace() {
|
||||
null,
|
||||
);
|
||||
|
||||
useListenToTauriEvent('toggle_sidebar', toggle);
|
||||
|
||||
// float/un-float sidebar on window resize
|
||||
useEffect(() => {
|
||||
const shouldHide = windowSize.width <= WINDOW_FLOATING_SIDEBAR_WIDTH;
|
||||
if (shouldHide) setFloating(true);
|
||||
else if (!shouldHide) setFloating(false);
|
||||
}, [windowSize.width]);
|
||||
if (shouldHide && !floating) {
|
||||
setFloating(true);
|
||||
hide();
|
||||
} else if (!shouldHide && floating) {
|
||||
setFloating(false);
|
||||
}
|
||||
}, [floating, hide, windowSize.width]);
|
||||
|
||||
const unsub = () => {
|
||||
if (moveState.current !== null) {
|
||||
@@ -66,9 +69,9 @@ export default function Workspace() {
|
||||
e.preventDefault(); // Prevent text selection and things
|
||||
const newWidth = startWidth + (e.clientX - mouseStartX);
|
||||
if (newWidth < 100) {
|
||||
hide();
|
||||
hide();
|
||||
resetWidth();
|
||||
} else {
|
||||
} else {
|
||||
show();
|
||||
setWidth(newWidth);
|
||||
}
|
||||
@@ -121,9 +124,9 @@ export default function Workspace() {
|
||||
)}
|
||||
>
|
||||
{floating ? (
|
||||
<Overlay open={!hidden} portalName="sidebar" onClose={hide} zIndex={10}>
|
||||
<Overlay open={!hidden} portalName="sidebar" onClose={hide}>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -10 }}
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
className={classNames(
|
||||
'absolute top-0 left-0 bottom-0 bg-gray-100 border-r border-highlight w-[14rem]',
|
||||
@@ -171,12 +174,14 @@ interface HeaderSizeProps extends HTMLAttributes<HTMLDivElement> {
|
||||
|
||||
function HeaderSize({ className, ...props }: HeaderSizeProps) {
|
||||
const platform = useOsInfo();
|
||||
const fullscreen = useIsFullscreen();
|
||||
const stoplightsVisible = platform?.osType === 'Darwin' && !fullscreen;
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
className,
|
||||
'h-md pt-[1px] flex items-center w-full pr-3 pl-20 border-b',
|
||||
platform?.osType === 'Darwin' && 'pl-20',
|
||||
'h-md pt-[1px] flex items-center w-full border-b',
|
||||
stoplightsVisible ? 'pl-20 pr-1' : 'pl-1',
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
@@ -3,15 +3,10 @@ import classNames from 'classnames';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
|
||||
import { useAppRoutes } from '../hooks/useAppRoutes';
|
||||
import { useAppVersion } from '../hooks/useAppVersion';
|
||||
import { useCreateWorkspace } from '../hooks/useCreateWorkspace';
|
||||
import { useDeleteWorkspace } from '../hooks/useDeleteWorkspace';
|
||||
import { useExportData } from '../hooks/useExportData';
|
||||
import { useImportData } from '../hooks/useImportData';
|
||||
import { usePrompt } from '../hooks/usePrompt';
|
||||
import { getRecentEnvironments } from '../hooks/useRecentEnvironments';
|
||||
import { useTheme } from '../hooks/useTheme';
|
||||
import { useUpdateMode } from '../hooks/useUpdateMode';
|
||||
import { useUpdateWorkspace } from '../hooks/useUpdateWorkspace';
|
||||
import { useWorkspaces } from '../hooks/useWorkspaces';
|
||||
import type { ButtonProps } from './core/Button';
|
||||
@@ -32,17 +27,12 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
|
||||
const workspaces = useWorkspaces();
|
||||
const activeWorkspace = useActiveWorkspace();
|
||||
const activeWorkspaceId = activeWorkspace?.id ?? null;
|
||||
const createWorkspace = useCreateWorkspace({ navigateAfter: true });
|
||||
const updateWorkspace = useUpdateWorkspace(activeWorkspaceId);
|
||||
const deleteWorkspace = useDeleteWorkspace(activeWorkspace);
|
||||
const importData = useImportData();
|
||||
const exportData = useExportData();
|
||||
const { appearance, toggleAppearance } = useTheme();
|
||||
const createWorkspace = useCreateWorkspace({ navigateAfter: true });
|
||||
const dialog = useDialog();
|
||||
const prompt = usePrompt();
|
||||
const routes = useAppRoutes();
|
||||
const appVersion = useAppVersion();
|
||||
const [updateMode, setUpdateMode] = useUpdateMode();
|
||||
|
||||
const items: DropdownItem[] = useMemo(() => {
|
||||
const workspaceItems: DropdownItem[] = workspaces.map((w) => ({
|
||||
@@ -65,7 +55,7 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
|
||||
<Button
|
||||
className="focus"
|
||||
color="gray"
|
||||
rightSlot={<Icon icon="openNewWindow" />}
|
||||
rightSlot={<Icon icon="externalLink" />}
|
||||
onClick={async () => {
|
||||
hide();
|
||||
const environmentId = (await getRecentEnvironments(w.id))[0];
|
||||
@@ -149,52 +139,14 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
|
||||
createWorkspace.mutate({ name });
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'import-data',
|
||||
label: 'Import Data',
|
||||
leftSlot: <Icon icon="download" />,
|
||||
onSelect: () => importData.mutate(),
|
||||
},
|
||||
{
|
||||
key: 'export-data',
|
||||
label: 'Export Data',
|
||||
leftSlot: <Icon icon="upload" />,
|
||||
onSelect: () => exportData.mutate(),
|
||||
},
|
||||
{ type: 'separator', label: `v${appVersion.data}` },
|
||||
{
|
||||
key: 'appearance',
|
||||
label: 'Toggle Theme',
|
||||
onSelect: toggleAppearance,
|
||||
leftSlot: <Icon icon={appearance === 'dark' ? 'sun' : 'moon'} />,
|
||||
},
|
||||
{
|
||||
key: 'update-mode',
|
||||
label: updateMode === 'stable' ? 'Enable Beta' : 'Disable Beta',
|
||||
onSelect: () => setUpdateMode(updateMode === 'stable' ? 'beta' : 'stable'),
|
||||
leftSlot: <Icon icon="camera" />,
|
||||
},
|
||||
{
|
||||
key: 'update-check',
|
||||
label: 'Check for Updates',
|
||||
onSelect: () => invoke('check_for_updates'),
|
||||
leftSlot: <Icon icon="update" />,
|
||||
},
|
||||
];
|
||||
}, [
|
||||
activeWorkspace?.name,
|
||||
activeWorkspaceId,
|
||||
appearance,
|
||||
createWorkspace,
|
||||
deleteWorkspace.mutate,
|
||||
dialog,
|
||||
exportData,
|
||||
importData,
|
||||
prompt,
|
||||
routes,
|
||||
setUpdateMode,
|
||||
toggleAppearance,
|
||||
updateMode,
|
||||
updateWorkspace,
|
||||
workspaces,
|
||||
]);
|
||||
|
||||
@@ -1,24 +1,23 @@
|
||||
import classNames from 'classnames';
|
||||
import React, { memo } from 'react';
|
||||
import { useActiveRequest } from '../hooks/useActiveRequest';
|
||||
import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
|
||||
import React, { memo, useState } from 'react';
|
||||
import { Icon } from './core/Icon';
|
||||
import { IconButton } from './core/IconButton';
|
||||
import { HStack } from './core/Stacks';
|
||||
import { EnvironmentActionsDropdown } from './EnvironmentActionsDropdown';
|
||||
import { RecentRequestsDropdown } from './RecentRequestsDropdown';
|
||||
import { RequestActionsDropdown } from './RequestActionsDropdown';
|
||||
import { SettingsDropdown } from './SettingsDropdown';
|
||||
import { SidebarActions } from './SidebarActions';
|
||||
import { WorkspaceActionsDropdown } from './WorkspaceActionsDropdown';
|
||||
import { useOsInfo } from '../hooks/useOsInfo';
|
||||
import { Button } from './core/Button';
|
||||
import { appWindow } from '@tauri-apps/api/window';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const WorkspaceHeader = memo(function WorkspaceHeader({ className }: Props) {
|
||||
const activeRequest = useActiveRequest();
|
||||
const activeWorkspace = useActiveWorkspace();
|
||||
|
||||
const osInfo = useOsInfo();
|
||||
const [maximized, setMaximized] = useState<boolean>(false);
|
||||
return (
|
||||
<HStack
|
||||
space={2}
|
||||
@@ -29,13 +28,7 @@ export const WorkspaceHeader = memo(function WorkspaceHeader({ className }: Prop
|
||||
<HStack space={0.5} className="flex-1 pointer-events-none" alignItems="center">
|
||||
<SidebarActions />
|
||||
<HStack alignItems="center">
|
||||
<WorkspaceActionsDropdown
|
||||
leftSlot={
|
||||
<div className="w-4 h-4 leading-4 rounded text-[0.8em] bg-[#1B88DE] bg-opacity-80 text-white mr-1">
|
||||
{activeWorkspace?.name[0]?.toUpperCase()}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<WorkspaceActionsDropdown />
|
||||
<Icon icon="chevronRight" className="text-gray-900 text-opacity-disabled" />
|
||||
<EnvironmentActionsDropdown className="w-auto pointer-events-auto" />
|
||||
</HStack>
|
||||
@@ -43,15 +36,54 @@ export const WorkspaceHeader = memo(function WorkspaceHeader({ className }: Prop
|
||||
<div className="pointer-events-none">
|
||||
<RecentRequestsDropdown />
|
||||
</div>
|
||||
<div className="flex-1 flex justify-end -mr-2 pointer-events-none">
|
||||
<RequestActionsDropdown requestId={activeRequest?.id ?? null}>
|
||||
<IconButton
|
||||
size="sm"
|
||||
title="Request Options"
|
||||
icon="gear"
|
||||
className="pointer-events-auto"
|
||||
/>
|
||||
</RequestActionsDropdown>
|
||||
<div className="flex-1 flex items-center h-full justify-end pointer-events-none">
|
||||
<SettingsDropdown />
|
||||
{(osInfo?.osType === 'Linux' || osInfo?.osType === 'Windows_NT') && (
|
||||
<HStack className="ml-4" alignItems="center">
|
||||
<Button
|
||||
className="px-4 !text-gray-600 rounded-none"
|
||||
onClick={() => appWindow.minimize()}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<path fill="currentColor" d="M14 8v1H3V8z" />
|
||||
</svg>
|
||||
</Button>
|
||||
<Button
|
||||
className="px-4 !text-gray-600 rounded-none"
|
||||
onClick={async () => {
|
||||
await appWindow.toggleMaximize();
|
||||
setMaximized(await appWindow.isMaximized());
|
||||
}}
|
||||
>
|
||||
{maximized ? (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<g fill="currentColor">
|
||||
<path d="M3 5v9h9V5zm8 8H4V6h7z" />
|
||||
<path fillRule="evenodd" d="M5 5h1V4h7v7h-1v1h2V3H5z" clipRule="evenodd" />
|
||||
</g>
|
||||
</svg>
|
||||
) : (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<path fill="currentColor" d="M3 3v10h10V3zm9 9H4V4h8z" />
|
||||
</svg>
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
color="custom"
|
||||
className="px-4 text-gray-600 rounded-none hocus:bg-red-200 hocus:text-gray-800"
|
||||
onClick={() => appWindow.close()}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<path
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
d="m7.116 8l-4.558 4.558l.884.884L8 8.884l4.558 4.558l.884-.884L8.884 8l4.558-4.558l-.884-.884L8 7.116L3.442 2.558l-.884.884z"
|
||||
clipRule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</Button>
|
||||
</HStack>
|
||||
)}
|
||||
</div>
|
||||
</HStack>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import classNames from 'classnames';
|
||||
import type { HTMLAttributes, ReactNode } from 'react';
|
||||
import { forwardRef, memo, useMemo } from 'react';
|
||||
import { forwardRef, useImperativeHandle, useMemo, useRef } from 'react';
|
||||
import type { HotkeyAction } from '../../hooks/useHotKey';
|
||||
import { useFormattedHotkey, useHotKey } from '../../hooks/useHotKey';
|
||||
import { Icon } from './Icon';
|
||||
|
||||
const colorStyles = {
|
||||
@@ -26,10 +28,10 @@ export type ButtonProps = HTMLAttributes<HTMLButtonElement> & {
|
||||
title?: string;
|
||||
leftSlot?: ReactNode;
|
||||
rightSlot?: ReactNode;
|
||||
hotkeyAction?: HotkeyAction;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const _Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
|
||||
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
|
||||
{
|
||||
isLoading,
|
||||
className,
|
||||
@@ -43,10 +45,16 @@ const _Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
|
||||
leftSlot,
|
||||
rightSlot,
|
||||
disabled,
|
||||
hotkeyAction,
|
||||
title,
|
||||
onClick,
|
||||
...props
|
||||
}: ButtonProps,
|
||||
ref,
|
||||
) {
|
||||
const hotkeyTrigger = useFormattedHotkey(hotkeyAction ?? null)?.join('');
|
||||
const fullTitle = hotkeyTrigger ? `${title} ${hotkeyTrigger}` : title;
|
||||
|
||||
const classes = useMemo(
|
||||
() =>
|
||||
classNames(
|
||||
@@ -66,8 +74,26 @@ const _Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
|
||||
[className, disabled, color, justify, size],
|
||||
);
|
||||
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
useImperativeHandle<HTMLButtonElement | null, HTMLButtonElement | null>(
|
||||
ref,
|
||||
() => buttonRef.current,
|
||||
);
|
||||
|
||||
useHotKey(hotkeyAction ?? null, () => {
|
||||
buttonRef.current?.click();
|
||||
});
|
||||
|
||||
return (
|
||||
<button ref={ref} type={type} className={classes} disabled={disabled} {...props}>
|
||||
<button
|
||||
ref={buttonRef}
|
||||
type={type}
|
||||
className={classes}
|
||||
disabled={disabled}
|
||||
onClick={onClick}
|
||||
title={fullTitle}
|
||||
{...props}
|
||||
>
|
||||
{isLoading ? (
|
||||
<Icon icon="update" size={size} className="animate-spin mr-1" />
|
||||
) : leftSlot ? (
|
||||
@@ -87,5 +113,3 @@ const _Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
|
||||
</button>
|
||||
);
|
||||
});
|
||||
|
||||
export const Button = memo(_Button);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import classNames from 'classnames';
|
||||
import { useCallback } from 'react';
|
||||
import { Icon } from './Icon';
|
||||
import { HStack } from './Stacks';
|
||||
|
||||
interface Props {
|
||||
checked: boolean;
|
||||
@@ -8,33 +8,47 @@ interface Props {
|
||||
onChange: (checked: boolean) => void;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
hideLabel?: boolean;
|
||||
}
|
||||
|
||||
export function Checkbox({ checked, onChange, className, disabled, title }: Props) {
|
||||
const handleClick = useCallback(() => {
|
||||
onChange(!checked);
|
||||
}, [onChange, checked]);
|
||||
|
||||
export function Checkbox({ checked, onChange, className, disabled, title, hideLabel }: Props) {
|
||||
return (
|
||||
<button
|
||||
role="checkbox"
|
||||
aria-checked={checked ? 'true' : 'false'}
|
||||
disabled={disabled}
|
||||
onClick={handleClick}
|
||||
title={title}
|
||||
className={classNames(
|
||||
className,
|
||||
'flex-shrink-0 w-4 h-4 border border-gray-200 rounded',
|
||||
'focus:border-focus',
|
||||
'disabled:opacity-disabled',
|
||||
checked && 'bg-gray-200/10',
|
||||
// Remove focus style
|
||||
'outline-none',
|
||||
)}
|
||||
<HStack
|
||||
as="label"
|
||||
space={2}
|
||||
alignItems="center"
|
||||
className={classNames(className, 'text-gray-900 text-sm', disabled && 'opacity-disabled')}
|
||||
>
|
||||
<div className="flex items-center justify-center">
|
||||
<Icon size="sm" icon={checked ? 'check' : 'empty'} />
|
||||
<div className="relative flex">
|
||||
<input
|
||||
aria-hidden
|
||||
className="appearance-none w-4 h-4 flex-shrink-0 border border-gray-200 rounded focus:border-focus outline-none ring-0"
|
||||
type="checkbox"
|
||||
disabled={disabled}
|
||||
onChange={() => onChange(!checked)}
|
||||
/>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<Icon size="sm" icon={checked ? 'check' : 'empty'} />
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
{/*<button*/}
|
||||
{/* role="checkbox"*/}
|
||||
{/* aria-checked={checked ? 'true' : 'false'}*/}
|
||||
{/* disabled={disabled}*/}
|
||||
{/* onClick={handleClick}*/}
|
||||
{/* title={title}*/}
|
||||
{/* className={classNames(*/}
|
||||
{/* className,*/}
|
||||
{/* 'flex-shrink-0 w-4 h-4 border border-gray-200 rounded',*/}
|
||||
{/* 'focus:border-focus',*/}
|
||||
{/* 'disabled:opacity-disabled',*/}
|
||||
{/* checked && 'bg-gray-200/10',*/}
|
||||
{/* // Remove focus style*/}
|
||||
{/* 'outline-none',*/}
|
||||
{/* )}*/}
|
||||
{/*>*/}
|
||||
{/*</button>*/}
|
||||
{!hideLabel && title}
|
||||
</HStack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -65,24 +65,27 @@ export function Dialog({
|
||||
)}
|
||||
>
|
||||
{title ? (
|
||||
<Heading className="text-xl font-semibold w-full" id={titleId}>
|
||||
{title}
|
||||
<Heading size={1} id={titleId}>
|
||||
{' '}
|
||||
{title}{' '}
|
||||
</Heading>
|
||||
) : (
|
||||
<span />
|
||||
)}
|
||||
{description && <p id={descriptionId}>{description}</p>}
|
||||
<div className="h-full w-full">{children}</div>
|
||||
<div className="h-full w-full grid grid-cols-[minmax(0,1fr)]">{children}</div>
|
||||
|
||||
{/*Put close at the end so that it's the last thing to be tabbed to*/}
|
||||
{!hideX && (
|
||||
<IconButton
|
||||
onClick={onClose}
|
||||
title="Close dialog"
|
||||
aria-label="Close"
|
||||
icon="x"
|
||||
size="sm"
|
||||
className="ml-auto absolute right-1 top-1"
|
||||
/>
|
||||
<div className="ml-auto absolute right-1 top-1">
|
||||
<IconButton
|
||||
onClick={onClose}
|
||||
title="Close dialog"
|
||||
aria-label="Close"
|
||||
size="sm"
|
||||
icon="x"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
MouseEvent,
|
||||
ReactElement,
|
||||
ReactNode,
|
||||
SetStateAction,
|
||||
} from 'react';
|
||||
import React, {
|
||||
Children,
|
||||
@@ -20,8 +21,11 @@ import React, {
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useKey, useKeyPressEvent, useWindowSize } from 'react-use';
|
||||
import type { HotkeyAction } from '../../hooks/useHotKey';
|
||||
import { useHotKey } from '../../hooks/useHotKey';
|
||||
import { Overlay } from '../Overlay';
|
||||
import { Button } from './Button';
|
||||
import { HotKey } from './HotKey';
|
||||
import { Separator } from './Separator';
|
||||
import { VStack } from './Stacks';
|
||||
|
||||
@@ -30,23 +34,28 @@ export type DropdownItemSeparator = {
|
||||
label?: string;
|
||||
};
|
||||
|
||||
export type DropdownItem =
|
||||
| {
|
||||
key: string;
|
||||
type?: 'default';
|
||||
label: ReactNode;
|
||||
variant?: 'danger';
|
||||
disabled?: boolean;
|
||||
hidden?: boolean;
|
||||
leftSlot?: ReactNode;
|
||||
rightSlot?: ReactNode;
|
||||
onSelect?: () => void;
|
||||
}
|
||||
| DropdownItemSeparator;
|
||||
export type DropdownItemDefault = {
|
||||
key: string;
|
||||
type?: 'default';
|
||||
label: ReactNode;
|
||||
hotKeyAction?: HotkeyAction;
|
||||
hotKeyLabelOnly?: boolean;
|
||||
variant?: 'default' | 'danger' | 'notify';
|
||||
disabled?: boolean;
|
||||
hidden?: boolean;
|
||||
leftSlot?: ReactNode;
|
||||
rightSlot?: ReactNode;
|
||||
onSelect?: () => void;
|
||||
};
|
||||
|
||||
export type DropdownItem = DropdownItemDefault | DropdownItemSeparator;
|
||||
|
||||
export interface DropdownProps {
|
||||
children: ReactElement<HTMLAttributes<HTMLButtonElement>>;
|
||||
items: DropdownItem[];
|
||||
openOnHotKeyAction?: HotkeyAction;
|
||||
onOpen?: () => void;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export interface DropdownRef {
|
||||
@@ -60,20 +69,33 @@ export interface DropdownRef {
|
||||
}
|
||||
|
||||
export const Dropdown = forwardRef<DropdownRef, DropdownProps>(function Dropdown(
|
||||
{ children, items }: DropdownProps,
|
||||
{ children, items, openOnHotKeyAction, onOpen, onClose }: DropdownProps,
|
||||
ref,
|
||||
) {
|
||||
const [open, setOpen] = useState<boolean>(false);
|
||||
const [isOpen, _setIsOpen] = useState<boolean>(false);
|
||||
const [defaultSelectedIndex, setDefaultSelectedIndex] = useState<number>();
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
const menuRef = useRef<Omit<DropdownRef, 'open'>>(null);
|
||||
|
||||
const setIsOpen = useCallback(
|
||||
(o: SetStateAction<boolean>) => {
|
||||
_setIsOpen(o);
|
||||
if (o) onOpen?.();
|
||||
else onClose?.();
|
||||
},
|
||||
[onClose, onOpen],
|
||||
);
|
||||
|
||||
useHotKey(openOnHotKeyAction ?? null, () => {
|
||||
setIsOpen(true);
|
||||
});
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
...menuRef.current,
|
||||
isOpen: open,
|
||||
isOpen: isOpen,
|
||||
toggle(activeIndex?: number) {
|
||||
if (!open) this.open(activeIndex);
|
||||
else setOpen(false);
|
||||
if (!isOpen) this.open(activeIndex);
|
||||
else setIsOpen(false);
|
||||
},
|
||||
open(activeIndex?: number) {
|
||||
if (activeIndex === undefined) {
|
||||
@@ -81,7 +103,7 @@ export const Dropdown = forwardRef<DropdownRef, DropdownProps>(function Dropdown
|
||||
} else {
|
||||
setDefaultSelectedIndex(activeIndex >= 0 ? activeIndex : items.length + activeIndex);
|
||||
}
|
||||
setOpen(true);
|
||||
setIsOpen(true);
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -98,54 +120,97 @@ export const Dropdown = forwardRef<DropdownRef, DropdownProps>(function Dropdown
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setDefaultSelectedIndex(undefined);
|
||||
setOpen((o) => !o);
|
||||
setIsOpen((o) => !o);
|
||||
}),
|
||||
};
|
||||
return cloneElement(existingChild, props);
|
||||
}, [children]);
|
||||
}, [children, setIsOpen]);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
setOpen(false);
|
||||
setIsOpen(false);
|
||||
buttonRef.current?.focus();
|
||||
}, []);
|
||||
}, [setIsOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
buttonRef.current?.setAttribute('aria-expanded', open.toString());
|
||||
}, [open]);
|
||||
buttonRef.current?.setAttribute('aria-expanded', isOpen.toString());
|
||||
}, [isOpen]);
|
||||
|
||||
const windowSize = useWindowSize();
|
||||
const triggerRect = useMemo(() => {
|
||||
if (!windowSize) return null; // No-op to TS happy with this dep
|
||||
if (!open) return null;
|
||||
if (!isOpen) return null;
|
||||
return buttonRef.current?.getBoundingClientRect();
|
||||
}, [open, windowSize]);
|
||||
}, [isOpen, windowSize]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{child}
|
||||
{open && triggerRect && (
|
||||
<Menu
|
||||
ref={menuRef}
|
||||
defaultSelectedIndex={defaultSelectedIndex}
|
||||
items={items}
|
||||
triggerRect={triggerRect}
|
||||
onClose={handleClose}
|
||||
/>
|
||||
)}
|
||||
<Menu
|
||||
ref={menuRef}
|
||||
showTriangle
|
||||
defaultSelectedIndex={defaultSelectedIndex}
|
||||
items={items}
|
||||
triggerShape={triggerRect ?? null}
|
||||
onClose={handleClose}
|
||||
isOpen={isOpen}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
interface ContextMenuProps {
|
||||
show: { x: number; y: number } | null;
|
||||
className?: string;
|
||||
items: DropdownProps['items'];
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const ContextMenu = forwardRef<DropdownRef, ContextMenuProps>(function ContextMenu(
|
||||
{ show, className, items, onClose },
|
||||
ref,
|
||||
) {
|
||||
const triggerShape = useMemo(
|
||||
() => ({
|
||||
top: show?.y ?? 0,
|
||||
bottom: show?.y ?? 0,
|
||||
left: show?.x ?? 0,
|
||||
right: show?.x ?? 0,
|
||||
}),
|
||||
[show],
|
||||
);
|
||||
|
||||
return (
|
||||
<Menu
|
||||
className={className}
|
||||
ref={ref}
|
||||
items={items}
|
||||
isOpen={show != null}
|
||||
onClose={onClose}
|
||||
triggerShape={triggerShape}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
interface MenuProps {
|
||||
className?: string;
|
||||
defaultSelectedIndex?: number;
|
||||
items: DropdownProps['items'];
|
||||
triggerRect: DOMRect;
|
||||
triggerShape: Pick<DOMRect, 'top' | 'bottom' | 'left' | 'right'> | null;
|
||||
onClose: () => void;
|
||||
showTriangle?: boolean;
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle'>, MenuProps>(function Menu(
|
||||
{ className, items, onClose, triggerRect, defaultSelectedIndex }: MenuProps,
|
||||
{
|
||||
className,
|
||||
isOpen,
|
||||
items,
|
||||
onClose,
|
||||
triggerShape,
|
||||
defaultSelectedIndex,
|
||||
showTriangle,
|
||||
}: MenuProps,
|
||||
ref,
|
||||
) {
|
||||
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||
@@ -248,21 +313,30 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle'>, MenuPro
|
||||
|
||||
const { containerStyles, triangleStyles } = useMemo<{
|
||||
containerStyles: CSSProperties;
|
||||
triangleStyles: CSSProperties;
|
||||
triangleStyles: CSSProperties | null;
|
||||
}>(() => {
|
||||
const docWidth = document.documentElement.getBoundingClientRect().width;
|
||||
const spaceRemaining = docWidth - triggerRect.left;
|
||||
const top = triggerRect?.bottom + 5;
|
||||
const onRight = spaceRemaining < 200;
|
||||
const containerStyles = onRight
|
||||
? { top, right: docWidth - triggerRect?.right }
|
||||
: { top, left: triggerRect?.left };
|
||||
if (triggerShape == null) return { containerStyles: {}, triangleStyles: null };
|
||||
|
||||
const docRect = document.documentElement.getBoundingClientRect();
|
||||
const width = triggerShape.right - triggerShape.left;
|
||||
const heightAbove = triggerShape.top;
|
||||
const heightBelow = docRect.height - triggerShape.bottom;
|
||||
const hSpaceRemaining = docRect.width - triggerShape.left;
|
||||
const top = triggerShape?.bottom + 5;
|
||||
const onRight = hSpaceRemaining < 200;
|
||||
const upsideDown = heightAbove > heightBelow && heightBelow < 200;
|
||||
const containerStyles = {
|
||||
top: !upsideDown ? top : undefined,
|
||||
bottom: upsideDown ? docRect.height - top : undefined,
|
||||
right: onRight ? docRect.width - triggerShape?.right : undefined,
|
||||
left: !onRight ? triggerShape?.left : undefined,
|
||||
};
|
||||
const size = { top: '-0.2rem', width: '0.4rem', height: '0.4rem' };
|
||||
const triangleStyles = onRight
|
||||
? { right: triggerRect.width / 2, marginRight: '-0.2rem', ...size }
|
||||
: { left: triggerRect.width / 2, marginLeft: '-0.2rem', ...size };
|
||||
? { right: width / 2, marginRight: '-0.2rem', ...size }
|
||||
: { left: width / 2, marginLeft: '-0.2rem', ...size };
|
||||
return { containerStyles, triangleStyles };
|
||||
}, [triggerRect]);
|
||||
}, [triggerShape]);
|
||||
|
||||
const handleFocus = useCallback(
|
||||
(i: DropdownItem) => {
|
||||
@@ -275,67 +349,89 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle'>, MenuPro
|
||||
if (items.length === 0) return null;
|
||||
|
||||
return (
|
||||
<Overlay open variant="transparent" portalName="dropdown" zIndex={50}>
|
||||
<div>
|
||||
<div tabIndex={-1} aria-hidden className="fixed inset-0 z-30" onClick={onClose} />
|
||||
<motion.div
|
||||
tabIndex={0}
|
||||
onKeyDown={handleMenuKeyDown}
|
||||
initial={{ opacity: 0, y: -5, scale: 0.98 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
role="menu"
|
||||
aria-orientation="vertical"
|
||||
dir="ltr"
|
||||
ref={containerRef}
|
||||
style={containerStyles}
|
||||
className={classNames(className, 'outline-none mt-1 pointer-events-auto fixed z-50')}
|
||||
>
|
||||
<span
|
||||
aria-hidden
|
||||
style={triangleStyles}
|
||||
className="bg-gray-50 absolute rotate-45 border-gray-200 border-t border-l"
|
||||
/>
|
||||
{containerStyles && (
|
||||
<VStack
|
||||
space={0.5}
|
||||
ref={initMenu}
|
||||
style={menuStyles}
|
||||
className={classNames(
|
||||
className,
|
||||
'h-auto bg-gray-50 rounded-md shadow-lg dark:shadow-gray-0 py-1.5 border',
|
||||
'border-gray-200 overflow-auto mb-1 mx-0.5',
|
||||
)}
|
||||
<>
|
||||
{items.map(
|
||||
(item) =>
|
||||
item.type !== 'separator' &&
|
||||
!item.hotKeyLabelOnly && (
|
||||
<MenuItemHotKey
|
||||
key={item.key}
|
||||
onSelect={handleSelect}
|
||||
item={item}
|
||||
action={item.hotKeyAction}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
{isOpen && (
|
||||
<Overlay open variant="transparent" portalName="dropdown" zIndex={50}>
|
||||
<div>
|
||||
<div tabIndex={-1} aria-hidden className="fixed inset-0 z-30" onClick={onClose} />
|
||||
<motion.div
|
||||
tabIndex={0}
|
||||
onKeyDown={handleMenuKeyDown}
|
||||
initial={{ opacity: 0, y: -5, scale: 0.98 }}
|
||||
animate={{ opacity: 1, y: 0, scale: 1 }}
|
||||
role="menu"
|
||||
aria-orientation="vertical"
|
||||
dir="ltr"
|
||||
ref={containerRef}
|
||||
style={containerStyles}
|
||||
className={classNames(className, 'outline-none my-1 pointer-events-auto fixed z-50')}
|
||||
>
|
||||
{items.map((item, i) => {
|
||||
if (item.type === 'separator') {
|
||||
return <Separator key={i} className="my-1.5" label={item.label} />;
|
||||
}
|
||||
if (item.hidden) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<MenuItem
|
||||
focused={i === selectedIndex}
|
||||
onFocus={handleFocus}
|
||||
onSelect={handleSelect}
|
||||
key={item.key}
|
||||
item={item}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</VStack>
|
||||
)}
|
||||
</motion.div>
|
||||
</div>
|
||||
</Overlay>
|
||||
{triangleStyles && showTriangle && (
|
||||
<span
|
||||
aria-hidden
|
||||
style={triangleStyles}
|
||||
className="bg-gray-50 absolute rotate-45 border-gray-200 border-t border-l"
|
||||
/>
|
||||
)}
|
||||
{containerStyles && (
|
||||
<VStack
|
||||
space={0.5}
|
||||
ref={initMenu}
|
||||
style={menuStyles}
|
||||
className={classNames(
|
||||
className,
|
||||
'h-auto bg-gray-50 rounded-md shadow-lg dark:shadow-gray-0 py-1.5 border',
|
||||
'border-gray-200 overflow-auto mb-1 mx-0.5',
|
||||
)}
|
||||
>
|
||||
{items.map((item, i) => {
|
||||
if (item.type === 'separator') {
|
||||
return (
|
||||
<Separator key={i} className="my-1.5">
|
||||
{item.label}
|
||||
</Separator>
|
||||
);
|
||||
}
|
||||
if (item.hidden) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<MenuItem
|
||||
focused={i === selectedIndex}
|
||||
onFocus={handleFocus}
|
||||
onSelect={handleSelect}
|
||||
key={item.key}
|
||||
item={item}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</VStack>
|
||||
)}
|
||||
</motion.div>
|
||||
</div>
|
||||
</Overlay>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
interface MenuItemProps {
|
||||
className?: string;
|
||||
item: DropdownItem;
|
||||
onSelect: (item: DropdownItem) => void;
|
||||
onFocus: (item: DropdownItem) => void;
|
||||
item: DropdownItemDefault;
|
||||
onSelect: (item: DropdownItemDefault) => void;
|
||||
onFocus: (item: DropdownItemDefault) => void;
|
||||
focused: boolean;
|
||||
}
|
||||
|
||||
@@ -359,7 +455,7 @@ function MenuItem({ className, focused, onFocus, item, onSelect, ...props }: Men
|
||||
[focused],
|
||||
);
|
||||
|
||||
if (item.type === 'separator') return <Separator className="my-1.5" />;
|
||||
const rightSlot = item.rightSlot ?? <HotKey action={item.hotKeyAction ?? null} />;
|
||||
|
||||
return (
|
||||
<Button
|
||||
@@ -373,12 +469,13 @@ function MenuItem({ className, focused, onFocus, item, onSelect, ...props }: Men
|
||||
onClick={handleClick}
|
||||
justify="start"
|
||||
leftSlot={item.leftSlot && <div className="pr-2 flex justify-start">{item.leftSlot}</div>}
|
||||
rightSlot={item.rightSlot && <div className="ml-auto pl-3">{item.rightSlot}</div>}
|
||||
rightSlot={rightSlot && <div className="ml-auto pl-3">{rightSlot}</div>}
|
||||
className={classNames(
|
||||
className,
|
||||
'min-w-[8rem] outline-none px-2 mx-1.5 flex text-sm text-gray-700 whitespace-nowrap',
|
||||
'focus:bg-highlight focus:text-gray-900 rounded',
|
||||
item.variant === 'danger' && 'text-red-600',
|
||||
item.variant === 'notify' && 'text-pink-600',
|
||||
)}
|
||||
innerClassName="!text-left"
|
||||
{...props}
|
||||
@@ -394,3 +491,14 @@ function MenuItem({ className, focused, onFocus, item, onSelect, ...props }: Men
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
interface MenuItemHotKeyProps {
|
||||
action: HotkeyAction | undefined;
|
||||
onSelect: MenuItemProps['onSelect'];
|
||||
item: MenuItemProps['item'];
|
||||
}
|
||||
|
||||
function MenuItemHotKey({ action, onSelect, item }: MenuItemHotKeyProps) {
|
||||
useHotKey(action ?? null, () => onSelect(item));
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@
|
||||
}
|
||||
|
||||
.cm-scroller {
|
||||
@apply font-mono text-[0.8rem] overflow-hidden;
|
||||
@apply font-mono text-xs overflow-hidden;
|
||||
}
|
||||
|
||||
.cm-line {
|
||||
@@ -215,6 +215,28 @@
|
||||
}
|
||||
}
|
||||
|
||||
.cm-editor .cm-panels {
|
||||
@apply bg-transparent border-0 text-gray-800 z-50;
|
||||
|
||||
input,
|
||||
button {
|
||||
@apply rounded-sm outline-none;
|
||||
}
|
||||
|
||||
button {
|
||||
@apply appearance-none bg-none bg-gray-800 text-gray-100 focus:bg-gray-900 cursor-default;
|
||||
}
|
||||
|
||||
input {
|
||||
@apply bg-gray-50 border border-highlight focus:border-focus outline-none;
|
||||
}
|
||||
|
||||
/* Hide the "All" button */
|
||||
button[name='select'] {
|
||||
@apply hidden;
|
||||
}
|
||||
}
|
||||
|
||||
/* Add default icon. Needs low priority so it can be overwritten */
|
||||
.cm-completionIcon::after {
|
||||
content: '𝑥';
|
||||
|
||||
@@ -5,7 +5,18 @@ import { keymap, placeholder as placeholderExt, tooltips } from '@codemirror/vie
|
||||
import classNames from 'classnames';
|
||||
import { EditorView } from 'codemirror';
|
||||
import type { MutableRefObject, ReactNode } from 'react';
|
||||
import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useRef } from 'react';
|
||||
import {
|
||||
Children,
|
||||
cloneElement,
|
||||
forwardRef,
|
||||
isValidElement,
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
useMemo,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import { useActiveEnvironment } from '../../../hooks/useActiveEnvironment';
|
||||
import { useActiveWorkspace } from '../../../hooks/useActiveWorkspace';
|
||||
import { IconButton } from '../IconButton';
|
||||
@@ -145,6 +156,12 @@ const _Editor = forwardRef<EditorView | undefined, EditorProps>(function Editor(
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [forceUpdateKey]);
|
||||
|
||||
const classList = className?.split(/\s+/) ?? [];
|
||||
const bgClassList = classList
|
||||
.filter((c) => c.match(/(^|:)?bg-.+/)) // Find bg-* classes
|
||||
.map((c) => c.replace(/^bg-/, '!bg-')) // !important
|
||||
.map((c) => c.replace(/^dark:bg-/, 'dark:!bg-')); // !important
|
||||
|
||||
// Initialize the editor when ref mounts
|
||||
const initEditorRef = useCallback((container: HTMLDivElement | null) => {
|
||||
if (container === null) {
|
||||
@@ -184,7 +201,7 @@ const _Editor = forwardRef<EditorView | undefined, EditorProps>(function Editor(
|
||||
|
||||
view = new EditorView({ state, parent: container });
|
||||
cm.current = { view, languageCompartment };
|
||||
syncGutterBg({ parent: container, className });
|
||||
syncGutterBg({ parent: container, bgClassList });
|
||||
if (autoFocus) {
|
||||
view.focus();
|
||||
}
|
||||
@@ -198,6 +215,50 @@ const _Editor = forwardRef<EditorView | undefined, EditorProps>(function Editor(
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
// Add bg classes to actions, so they appear over the text
|
||||
const decoratedActions = useMemo(() => {
|
||||
const results = [];
|
||||
const actionClassName = classNames(
|
||||
'transition-opacity opacity-0 group-hover:opacity-50 hover:!opacity-100 shadow',
|
||||
bgClassList,
|
||||
);
|
||||
|
||||
if (format) {
|
||||
results.push(
|
||||
<IconButton
|
||||
showConfirm
|
||||
key="format"
|
||||
size="sm"
|
||||
title="Reformat contents"
|
||||
icon="magicWand"
|
||||
className={classNames(actionClassName)}
|
||||
onClick={() => {
|
||||
if (cm.current === null) return;
|
||||
const { doc } = cm.current.view.state;
|
||||
const formatted = format(doc.toString());
|
||||
// Update editor and blur because the cursor will reset anyway
|
||||
cm.current.view.dispatch({
|
||||
changes: { from: 0, to: doc.length, insert: formatted },
|
||||
});
|
||||
cm.current.view.contentDOM.blur();
|
||||
// Fire change event
|
||||
onChange?.(formatted);
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
}
|
||||
results.push(
|
||||
Children.map(actions, (existingChild) => {
|
||||
if (!isValidElement(existingChild)) return null;
|
||||
return cloneElement(existingChild, {
|
||||
...existingChild.props,
|
||||
className: classNames(existingChild.props.className, actionClassName),
|
||||
});
|
||||
}),
|
||||
);
|
||||
return results;
|
||||
}, [actions, bgClassList, format, onChange]);
|
||||
|
||||
const cmContainer = (
|
||||
<div
|
||||
ref={initEditorRef}
|
||||
@@ -219,28 +280,17 @@ const _Editor = forwardRef<EditorView | undefined, EditorProps>(function Editor(
|
||||
return (
|
||||
<div className="group relative h-full w-full">
|
||||
{cmContainer}
|
||||
{format && (
|
||||
<HStack space={0.5} alignItems="center" className="absolute bottom-2 right-0 ">
|
||||
{actions}
|
||||
<IconButton
|
||||
showConfirm
|
||||
size="sm"
|
||||
title="Reformat contents"
|
||||
icon="magicWand"
|
||||
className="transition-opacity opacity-0 group-hover:opacity-70"
|
||||
onClick={() => {
|
||||
if (cm.current === null) return;
|
||||
const { doc } = cm.current.view.state;
|
||||
const formatted = format(doc.toString());
|
||||
// Update editor and blur because the cursor will reset anyway
|
||||
cm.current.view.dispatch({
|
||||
changes: { from: 0, to: doc.length, insert: formatted },
|
||||
});
|
||||
cm.current.view.contentDOM.blur();
|
||||
// Fire change event
|
||||
onChange?.(formatted);
|
||||
}}
|
||||
/>
|
||||
{decoratedActions && (
|
||||
<HStack
|
||||
space={1}
|
||||
alignItems="center"
|
||||
justifyContent="end"
|
||||
className={classNames(
|
||||
'absolute bottom-2 left-0 right-0',
|
||||
'pointer-events-none', // No pointer events so we don't block the editor
|
||||
)}
|
||||
>
|
||||
{decoratedActions}
|
||||
</HStack>
|
||||
)}
|
||||
</div>
|
||||
@@ -290,6 +340,8 @@ function getExtensions({
|
||||
|
||||
// Handle onChange
|
||||
EditorView.updateListener.of((update) => {
|
||||
// Only fire onChange if the document changed and the update was from user input. This prevents firing onChange when the document is updated when
|
||||
// changing pages (one request to another in header editor)
|
||||
if (onChange && update.docChanged && isViewUpdateFromUserInput(update)) {
|
||||
onChange.current?.(update.state.doc.toString());
|
||||
}
|
||||
@@ -313,19 +365,14 @@ function isViewUpdateFromUserInput(viewUpdate: ViewUpdate) {
|
||||
|
||||
const syncGutterBg = ({
|
||||
parent,
|
||||
className = '',
|
||||
bgClassList,
|
||||
}: {
|
||||
parent: HTMLDivElement;
|
||||
className?: string;
|
||||
bgClassList: string[];
|
||||
}) => {
|
||||
const gutterEl = parent.querySelector<HTMLDivElement>('.cm-gutters');
|
||||
const classList = className?.split(/\s+/) ?? [];
|
||||
const bgClasses = classList
|
||||
.filter((c) => c.match(/(^|:)?bg-.+/)) // Find bg-* classes
|
||||
.map((c) => c.replace(/^bg-/, '!bg-')) // !important
|
||||
.map((c) => c.replace(/^dark:bg-/, 'dark:!bg-')); // !important
|
||||
if (gutterEl) {
|
||||
gutterEl?.classList.add(...bgClasses);
|
||||
gutterEl?.classList.add(...bgClassList);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -5,11 +5,12 @@ interface Props {
|
||||
}
|
||||
|
||||
export function FormattedError({ children }: Props) {
|
||||
console.log('ERROR', children);
|
||||
return (
|
||||
<pre
|
||||
className={classNames(
|
||||
'text-sm select-auto cursor-text bg-gray-100 p-3 rounded',
|
||||
'whitespace-normal border border-red-500 border-dashed',
|
||||
'w-full text-sm select-auto cursor-text bg-gray-100 p-3 rounded',
|
||||
'whitespace-pre border border-red-500 border-dashed overflow-x-auto',
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user