mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-01-18 09:46:44 +01:00
Compare commits
16 Commits
v2024.4.0-
...
v2024.4.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb5f9a4671 | ||
|
|
ad1a4eadd9 | ||
|
|
69a151bfe5 | ||
|
|
d8d2f44723 | ||
|
|
d5ea03ce91 | ||
|
|
816bc543d7 | ||
|
|
36d8c56872 | ||
|
|
a7bb5605ab | ||
|
|
31147475f3 | ||
|
|
5a2d510d07 | ||
|
|
4a70c5415b | ||
|
|
ad796275b6 | ||
|
|
31f5163ee3 | ||
|
|
1a6a75ca13 | ||
|
|
edad9e2d68 | ||
|
|
d5d0edb0b0 |
@@ -1,6 +1,6 @@
|
||||
<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="SCRIPT_TEXT" value="npm run tauri build" />
|
||||
<option name="INDEPENDENT_SCRIPT_PATH" value="true" />
|
||||
<option name="SCRIPT_PATH" value="" />
|
||||
<option name="SCRIPT_OPTIONS" value="" />
|
||||
@@ -9,12 +9,9 @@
|
||||
<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_IN_TERMINAL" value="false" />
|
||||
<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>
|
||||
<envs />
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
@@ -80,6 +80,26 @@ describe('exporter-curl', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('Exports multi-line JSON body', () => {
|
||||
expect(
|
||||
pluginHookExport({
|
||||
url: 'https://yaak.app',
|
||||
method: 'POST',
|
||||
bodyType: 'application/json',
|
||||
body: {
|
||||
text: `{"foo":"bar",\n"baz":"qux"}`,
|
||||
},
|
||||
headers: [{ name: 'Content-Type', value: 'application/json' }],
|
||||
}),
|
||||
).toEqual(
|
||||
[
|
||||
`curl -X POST 'https://yaak.app'`,
|
||||
`--header 'Content-Type: application/json'`,
|
||||
`--data-raw $'{"foo":"bar",\n"baz":"qux"}'`,
|
||||
].join(` \\\n `),
|
||||
);
|
||||
});
|
||||
|
||||
test('Exports headers', () => {
|
||||
expect(
|
||||
pluginHookExport({
|
||||
|
||||
@@ -52,7 +52,7 @@ export function pluginHookImport(rawData: string) {
|
||||
|
||||
// Replace non-escaped newlines with semicolons to make parsing easier
|
||||
// NOTE: This is really slow in debug build but fast in release mode
|
||||
const normalizedData = rawData.replace(/([^\\])\n/g, '$1; ');
|
||||
const normalizedData = rawData.replace(/\ncurl/g, '; curl');
|
||||
|
||||
let currentCommand: ParseEntry[] = [];
|
||||
|
||||
@@ -137,8 +137,6 @@ export function importCommand(parseEntries: ParseEntry[], workspaceId: string) {
|
||||
// Start at 1 so we can skip the ^curl part
|
||||
for (let i = 1; i < parseEntries.length; i++) {
|
||||
let parseEntry = parseEntries[i];
|
||||
// trim leading spaces between parsed entries
|
||||
// regex won't match otherwise (e.g. -H 'Content-Type: application/json')
|
||||
if (typeof parseEntry === 'string') {
|
||||
parseEntry = parseEntry.trim();
|
||||
}
|
||||
@@ -186,7 +184,7 @@ export function importCommand(parseEntries: ParseEntry[], workspaceId: string) {
|
||||
urlParameters =
|
||||
search?.split('&').map((p) => {
|
||||
const v = splitOnce(p, '=');
|
||||
return { name: v[0] ?? '', value: v[1] ?? '' };
|
||||
return { name: v[0] ?? '', value: v[1] ?? '', enabled: true };
|
||||
}) ?? [];
|
||||
|
||||
url = baseUrl ?? urlArg;
|
||||
@@ -214,11 +212,13 @@ export function importCommand(parseEntries: ParseEntry[], workspaceId: string) {
|
||||
return {
|
||||
name: (name ?? '').trim().replace(/;$/, ''),
|
||||
value: '',
|
||||
enabled: true,
|
||||
};
|
||||
}
|
||||
return {
|
||||
name: (name ?? '').trim(),
|
||||
value: value.trim(),
|
||||
enabled: true,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -245,6 +245,7 @@ export function importCommand(parseEntries: ParseEntry[], workspaceId: string) {
|
||||
headers.push({
|
||||
name: 'Cookie',
|
||||
value: cookieHeaderValue,
|
||||
enabled: true,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -288,12 +289,17 @@ export function importCommand(parseEntries: ParseEntry[], workspaceId: string) {
|
||||
) {
|
||||
bodyType = mimeType ?? 'application/x-www-form-urlencoded';
|
||||
body = {
|
||||
params: dataParameters.map((parameter) => ({
|
||||
form: dataParameters.map((parameter) => ({
|
||||
...parameter,
|
||||
name: decodeURIComponent(parameter.name || ''),
|
||||
value: decodeURIComponent(parameter.value || ''),
|
||||
})),
|
||||
};
|
||||
headers.push({
|
||||
name: 'Content-Type',
|
||||
value: 'application/x-www-form-urlencoded',
|
||||
enabled: true,
|
||||
});
|
||||
} else if (dataParameters.length > 0) {
|
||||
bodyType =
|
||||
mimeType === 'application/json' || mimeType === 'text/xml' || mimeType === 'text/plain'
|
||||
@@ -309,13 +315,20 @@ export function importCommand(parseEntries: ParseEntry[], workspaceId: string) {
|
||||
body = {
|
||||
form: formDataParams,
|
||||
};
|
||||
if (mimeType == null) {
|
||||
headers.push({
|
||||
name: 'Content-Type',
|
||||
value: 'multipart/form-data',
|
||||
enabled: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Method
|
||||
let method = getPairValue(pairsByName, '', ['X', 'request']).toUpperCase();
|
||||
|
||||
if (method === '' && body) {
|
||||
method = 'text' in body || 'params' in body ? 'POST' : 'GET';
|
||||
method = 'text' in body || 'form' in body ? 'POST' : 'GET';
|
||||
}
|
||||
|
||||
const request: ExportResources['httpRequests'][0] = {
|
||||
@@ -344,6 +357,7 @@ const pairsToDataParameters = (keyedPairs: PairsByName) => {
|
||||
value: string;
|
||||
contentType?: string;
|
||||
filePath?: string;
|
||||
enabled?: boolean;
|
||||
}[] = [];
|
||||
|
||||
for (const flagName of DATA_FLAGS) {
|
||||
@@ -363,11 +377,13 @@ const pairsToDataParameters = (keyedPairs: PairsByName) => {
|
||||
name: name ?? '',
|
||||
value: '',
|
||||
filePath: p.slice(1),
|
||||
enabled: true,
|
||||
});
|
||||
} else {
|
||||
dataParameters.push({
|
||||
name: name ?? '',
|
||||
value: flagName === 'data-urlencode' ? encodeURIComponent(value ?? '') : value ?? '',
|
||||
enabled: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,6 +122,13 @@ describe('importer-curl', () => {
|
||||
baseRequest({
|
||||
method: 'POST',
|
||||
url: 'https://yaak.app',
|
||||
headers: [
|
||||
{
|
||||
name: 'Content-Type',
|
||||
value: 'multipart/form-data',
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
bodyType: 'multipart/form-data',
|
||||
body: {
|
||||
form: [
|
||||
@@ -145,11 +152,18 @@ describe('importer-curl', () => {
|
||||
method: 'POST',
|
||||
url: 'https://yaak.app',
|
||||
bodyType: 'application/x-www-form-urlencoded',
|
||||
headers: [
|
||||
{
|
||||
name: 'Content-Type',
|
||||
value: 'application/x-www-form-urlencoded',
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
body: {
|
||||
params: [
|
||||
{ name: 'a', value: '' },
|
||||
{ name: 'b', value: '' },
|
||||
{ name: 'c', value: 'ccc' },
|
||||
form: [
|
||||
{ name: 'a', value: '', enabled: true },
|
||||
{ name: 'b', value: '', enabled: true },
|
||||
{ name: 'c', value: 'ccc', enabled: true },
|
||||
],
|
||||
},
|
||||
}),
|
||||
@@ -168,7 +182,7 @@ describe('importer-curl', () => {
|
||||
baseRequest({
|
||||
method: 'POST',
|
||||
url: 'https://yaak.app',
|
||||
headers: [{ name: 'Content-Type', value: 'text/plain' }],
|
||||
headers: [{ name: 'Content-Type', value: 'text/plain', enabled: true }],
|
||||
bodyType: 'text/plain',
|
||||
body: { text: 'a&b&c=ccc' },
|
||||
}),
|
||||
@@ -177,6 +191,27 @@ describe('importer-curl', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('Imports multi-line JSON', () => {
|
||||
expect(
|
||||
pluginHookImport(
|
||||
`curl -H Content-Type:application/json -d $'{\n "foo":"bar"\n}' https://yaak.app`,
|
||||
),
|
||||
).toEqual({
|
||||
resources: {
|
||||
workspaces: [baseWorkspace()],
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
method: 'POST',
|
||||
url: 'https://yaak.app',
|
||||
headers: [{ name: 'Content-Type', value: 'application/json', enabled: true }],
|
||||
bodyType: 'application/json',
|
||||
body: { text: '{\n "foo":"bar"\n}' },
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('Imports multiple headers', () => {
|
||||
expect(
|
||||
pluginHookImport('curl -H Foo:bar --header Name -H AAA:bbb -H :ccc https://yaak.app'),
|
||||
@@ -187,10 +222,10 @@ describe('importer-curl', () => {
|
||||
baseRequest({
|
||||
url: 'https://yaak.app',
|
||||
headers: [
|
||||
{ name: 'Name', value: '' },
|
||||
{ name: 'Foo', value: 'bar' },
|
||||
{ name: 'AAA', value: 'bbb' },
|
||||
{ name: '', value: 'ccc' },
|
||||
{ name: 'Name', value: '', enabled: true },
|
||||
{ name: 'Foo', value: 'bar', enabled: true },
|
||||
{ name: 'AAA', value: 'bbb', enabled: true },
|
||||
{ name: '', value: 'ccc', enabled: true },
|
||||
],
|
||||
}),
|
||||
],
|
||||
@@ -241,7 +276,7 @@ describe('importer-curl', () => {
|
||||
httpRequests: [
|
||||
baseRequest({
|
||||
url: 'https://yaak.app',
|
||||
headers: [{ name: 'Cookie', value: 'foo=bar' }],
|
||||
headers: [{ name: 'Cookie', value: 'foo=bar', enabled: true }],
|
||||
}),
|
||||
],
|
||||
},
|
||||
@@ -256,8 +291,8 @@ describe('importer-curl', () => {
|
||||
baseRequest({
|
||||
url: 'https://yaak.app',
|
||||
urlParameters: [
|
||||
{ name: 'foo', value: 'bar' },
|
||||
{ name: 'baz', value: 'a%20a' },
|
||||
{ name: 'foo', value: 'bar', enabled: true },
|
||||
{ name: 'baz', value: 'a%20a', enabled: true },
|
||||
],
|
||||
}),
|
||||
],
|
||||
|
||||
182
src-tauri/Cargo.lock
generated
182
src-tauri/Cargo.lock
generated
@@ -1051,16 +1051,6 @@ dependencies = [
|
||||
"version_check 0.9.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cookie"
|
||||
version = "0.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3cd91cf61412820176e137621345ee43b3f4423e589e7ae4e50d601d93e35ef8"
|
||||
dependencies = [
|
||||
"time",
|
||||
"version_check 0.9.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cookie_store"
|
||||
version = "0.16.2"
|
||||
@@ -1830,26 +1820,11 @@ dependencies = [
|
||||
"new_debug_unreachable",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.3.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "da0290714b38af9b4a7b094b8a37086d1b4e61f2df9122c3cad2577669145335"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-executor",
|
||||
"futures-io",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.29"
|
||||
version = "0.3.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb"
|
||||
checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
@@ -1857,15 +1832,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.29"
|
||||
version = "0.3.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c"
|
||||
checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
|
||||
|
||||
[[package]]
|
||||
name = "futures-executor"
|
||||
version = "0.3.29"
|
||||
version = "0.3.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0f4fb8693db0cf099eadcca0efe2a5a22e4550f98ed16aba6c48700da29597bc"
|
||||
checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
@@ -1885,9 +1860,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-io"
|
||||
version = "0.3.29"
|
||||
version = "0.3.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa"
|
||||
checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
|
||||
|
||||
[[package]]
|
||||
name = "futures-lite"
|
||||
@@ -1904,9 +1879,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.29"
|
||||
version = "0.3.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53b153fd91e4b0147f4aced87be237c98248656bb01050b96bf3ee89220a8ddb"
|
||||
checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -1915,23 +1890,22 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-sink"
|
||||
version = "0.3.29"
|
||||
version = "0.3.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817"
|
||||
checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.29"
|
||||
version = "0.3.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2"
|
||||
checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.29"
|
||||
version = "0.3.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104"
|
||||
checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-macro",
|
||||
@@ -2226,11 +2200,9 @@ dependencies = [
|
||||
"hyper 0.14.27",
|
||||
"hyper-rustls 0.24.2",
|
||||
"log",
|
||||
"once_cell",
|
||||
"prost",
|
||||
"prost-reflect",
|
||||
"prost-types",
|
||||
"protoc-bin-vendored",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tauri",
|
||||
@@ -2475,6 +2447,12 @@ dependencies = [
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "http-range"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21dec9db110f5f872ed9699c3ecf50cf16f423502706ba5c72462e28d3157573"
|
||||
|
||||
[[package]]
|
||||
name = "httparse"
|
||||
version = "1.8.0"
|
||||
@@ -4321,56 +4299,6 @@ dependencies = [
|
||||
"prost",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "protoc-bin-vendored"
|
||||
version = "3.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "005ca8623e5633e298ad1f917d8be0a44bcf406bf3cde3b80e63003e49a3f27d"
|
||||
dependencies = [
|
||||
"protoc-bin-vendored-linux-aarch_64",
|
||||
"protoc-bin-vendored-linux-ppcle_64",
|
||||
"protoc-bin-vendored-linux-x86_32",
|
||||
"protoc-bin-vendored-linux-x86_64",
|
||||
"protoc-bin-vendored-macos-x86_64",
|
||||
"protoc-bin-vendored-win32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "protoc-bin-vendored-linux-aarch_64"
|
||||
version = "3.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fb9fc9cce84c8694b6ea01cc6296617b288b703719b725b8c9c65f7c5874435"
|
||||
|
||||
[[package]]
|
||||
name = "protoc-bin-vendored-linux-ppcle_64"
|
||||
version = "3.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02d2a07dcf7173a04d49974930ccbfb7fd4d74df30ecfc8762cf2f895a094516"
|
||||
|
||||
[[package]]
|
||||
name = "protoc-bin-vendored-linux-x86_32"
|
||||
version = "3.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d54fef0b04fcacba64d1d80eed74a20356d96847da8497a59b0a0a436c9165b0"
|
||||
|
||||
[[package]]
|
||||
name = "protoc-bin-vendored-linux-x86_64"
|
||||
version = "3.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8782f2ce7d43a9a5c74ea4936f001e9e8442205c244f7a3d4286bd4c37bc924"
|
||||
|
||||
[[package]]
|
||||
name = "protoc-bin-vendored-macos-x86_64"
|
||||
version = "3.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b5de656c7ee83f08e0ae5b81792ccfdc1d04e7876b1d9a38e6876a9e09e02537"
|
||||
|
||||
[[package]]
|
||||
name = "protoc-bin-vendored-win32"
|
||||
version = "3.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9653c3ed92974e34c5a6e0a510864dab979760481714c172e0a34e437cb98804"
|
||||
|
||||
[[package]]
|
||||
name = "psl-types"
|
||||
version = "2.0.11"
|
||||
@@ -5014,9 +4942,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "schemars"
|
||||
version = "0.8.16"
|
||||
version = "0.8.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "45a28f4c49489add4ce10783f7911893516f15afe45d015608d41faca6bc4d29"
|
||||
checksum = "fc6e7ed6919cb46507fb01ff1654309219f62b4d603822501b0b80d42f6f21ef"
|
||||
dependencies = [
|
||||
"dyn-clone",
|
||||
"indexmap 1.9.3",
|
||||
@@ -5028,14 +4956,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "schemars_derive"
|
||||
version = "0.8.16"
|
||||
version = "0.8.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c767fd6fa65d9ccf9cf026122c1b555f2ef9a4f0cea69da4d7dbc3e258d30967"
|
||||
checksum = "185f2b7aa7e02d418e453790dde16890256bbd2bcd04b7dc5348811052b53f49"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde_derive_internals",
|
||||
"syn 1.0.109",
|
||||
"syn 2.0.58",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5150,13 +5078,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive_internals"
|
||||
version = "0.26.0"
|
||||
version = "0.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85bf8229e7920a9f636479437026331ce11aa132b4dde37d121944a44d6e5f3c"
|
||||
checksum = "330f01ce65a3a5fe59a60c82f3c9a024b573b8a6e875bd233fe5f934e71d54e3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
"syn 2.0.58",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5849,9 +5777,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tao"
|
||||
version = "0.27.1"
|
||||
version = "0.28.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92bcf8885e147b56d6e26751263b45876284f32ca404703f6d3b8f80d16ff4dd"
|
||||
checksum = "12a8121bd5721ebbbe0889f8286d5824673beeb04071519b68916fbed04f3093"
|
||||
dependencies = [
|
||||
"bitflags 2.5.0",
|
||||
"cocoa",
|
||||
@@ -5922,9 +5850,9 @@ checksum = "14c39fd04924ca3a864207c66fc2cd7d22d7c016007f9ce846cbb9326331930a"
|
||||
|
||||
[[package]]
|
||||
name = "tauri"
|
||||
version = "2.0.0-beta.17"
|
||||
version = "2.0.0-beta.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5fedd5490eddf117253945f0baedafded43474c971cba546a818f527d5c26266"
|
||||
checksum = "6f8e5bc2e4f5eb7496d1a3e5f4d272f69f1333db5f8efed28d79d7f93334fe95"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
@@ -5938,6 +5866,7 @@ dependencies = [
|
||||
"gtk",
|
||||
"heck 0.5.0",
|
||||
"http 1.1.0",
|
||||
"http-range",
|
||||
"jni",
|
||||
"libc",
|
||||
"log",
|
||||
@@ -5971,9 +5900,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-build"
|
||||
version = "2.0.0-beta.13"
|
||||
version = "2.0.0-beta.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "abcf98a9b4527567c3e5ca9723431d121e001c2145651b3fa044d22b5e025a7e"
|
||||
checksum = "8aa28eebafcda490fa7097a6e3a4d07f65967614d35dd88b2aaa19dbb49241cd"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cargo_toml",
|
||||
@@ -5993,9 +5922,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-codegen"
|
||||
version = "2.0.0-beta.13"
|
||||
version = "2.0.0-beta.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b383f341efb803852b0235a2f330ca90c4c113f422dd6d646b888685b372cace"
|
||||
checksum = "727d13a28e9ec895f537d90a09acb0aa3593f703a715fe8a77f87269d3245b52"
|
||||
dependencies = [
|
||||
"base64 0.22.0",
|
||||
"brotli",
|
||||
@@ -6020,11 +5949,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-macros"
|
||||
version = "2.0.0-beta.13"
|
||||
version = "2.0.0-beta.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "71be71718cfe48b149507157bfbad0e2ba0e98ea51658be26c7c677eb188fb0c"
|
||||
checksum = "258667612ad901d256e04ace71ac54d4b3dd8fb1e5baa24403b50991cade4365"
|
||||
dependencies = [
|
||||
"heck 0.4.1",
|
||||
"heck 0.5.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.58",
|
||||
@@ -6199,9 +6128,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-runtime"
|
||||
version = "2.0.0-beta.14"
|
||||
version = "2.0.0-beta.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "148b6e6aff8e63fe5d4ae1d50159d50cfc0b4309abdeca64833c887c6b5631ef"
|
||||
checksum = "574f3d59cbe6c76b6d849bc35aa3a9e8061ff8f75f557dc33f38c0e43cf55a41"
|
||||
dependencies = [
|
||||
"dpi",
|
||||
"gtk",
|
||||
@@ -6218,9 +6147,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-runtime-wry"
|
||||
version = "2.0.0-beta.14"
|
||||
version = "2.0.0-beta.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "398d065c6e0fbf3c4304583759b6e153bc1e0daeb033bede6834ebe4df371fc3"
|
||||
checksum = "d6d1f223de1d674aaa561c900ac650b3160f11520e9b191a3574f6c493fc77fa"
|
||||
dependencies = [
|
||||
"cocoa",
|
||||
"gtk",
|
||||
@@ -6242,9 +6171,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tauri-utils"
|
||||
version = "2.0.0-beta.13"
|
||||
version = "2.0.0-beta.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4709765385f035338ecc330f3fba753b8ee283c659c235da9768949cdb25469"
|
||||
checksum = "2b4251529d92b5c611ccaa611f8a31cb41b1aa00db8bcc0a49efe5d966bfa911"
|
||||
dependencies = [
|
||||
"brotli",
|
||||
"cargo_metadata",
|
||||
@@ -7251,18 +7180,6 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "window-shadows"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67ff424735b1ac21293b0492b069394b0a189c8a463fb015a16dea7c2e221c08"
|
||||
dependencies = [
|
||||
"cocoa",
|
||||
"objc",
|
||||
"raw-window-handle 0.5.2",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "window-vibrancy"
|
||||
version = "0.5.0"
|
||||
@@ -7731,9 +7648,7 @@ dependencies = [
|
||||
"boa_runtime",
|
||||
"chrono",
|
||||
"cocoa",
|
||||
"cookie 0.18.0",
|
||||
"datetime",
|
||||
"futures",
|
||||
"grpc",
|
||||
"http 0.2.10",
|
||||
"log",
|
||||
@@ -7759,7 +7674,6 @@ dependencies = [
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"uuid",
|
||||
"window-shadows",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -3,10 +3,6 @@ workspace = { members = ["grpc"] }
|
||||
[package]
|
||||
name = "yaak-app"
|
||||
version = "0.0.0"
|
||||
description = "A network protocol testing utility app"
|
||||
authors = ["Gregory Schier"]
|
||||
license = "MIT"
|
||||
repository = "https://github.com/gschier/yaak-app"
|
||||
edition = "2021"
|
||||
|
||||
# Produce a library for mobile support
|
||||
@@ -32,18 +28,13 @@ base64 = "0.22.0"
|
||||
boa_engine = { version = "0.18.0", features = ["annex-b"] }
|
||||
boa_runtime = { version = "0.18.0" }
|
||||
chrono = { version = "0.4.31", features = ["serde"] }
|
||||
futures = "0.3.26"
|
||||
http = "0.2.8"
|
||||
http = "0.2.10"
|
||||
rand = "0.8.5"
|
||||
reqwest = { version = "0.11.23", features = ["multipart", "cookies", "gzip", "brotli", "deflate"] }
|
||||
cookie = { version = "0.18.0" }
|
||||
serde = { version = "1.0.195", features = ["derive"] }
|
||||
serde_json = { version = "1.0.111", features = ["raw_value"] }
|
||||
reqwest = { version = "0.11.23", features = ["multipart", "cookies", "gzip", "brotli", "deflate", "json"] }
|
||||
serde = { version = "1.0.198", features = ["derive"] }
|
||||
serde_json = { version = "1.0.116", features = ["raw_value"] }
|
||||
sqlx = { version = "0.7.4", features = ["sqlite", "runtime-tokio-rustls", "json", "chrono", "time"] }
|
||||
tauri = { version = "2.0.0-beta.17", features = [
|
||||
"config-toml",
|
||||
"devtools",
|
||||
] }
|
||||
tauri = { version = "2.0.0-beta.19", features = ["config-toml", "devtools", "protocol-asset"] }
|
||||
tauri-plugin-clipboard-manager = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||
tauri-plugin-dialog = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||
tauri-plugin-log = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2", features = ["colored"] }
|
||||
@@ -53,10 +44,9 @@ tauri-plugin-updater = { git = "https://github.com/tauri-apps/plugins-workspace"
|
||||
tauri-plugin-window-state = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||
tauri-plugin-fs = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||
tokio = { version = "1.36.0", features = ["sync"] }
|
||||
uuid = "1.3.0"
|
||||
log = "0.4.20"
|
||||
uuid = "1.7.0"
|
||||
log = "0.4.21"
|
||||
datetime = "0.5.2"
|
||||
window-shadows = "0.2.2"
|
||||
reqwest_cookie_store = "0.6.0"
|
||||
grpc = { path = "./grpc" }
|
||||
tokio-stream = "0.1.15"
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
"window:allow-set-title",
|
||||
"window:allow-start-dragging",
|
||||
"window:allow-unmaximize",
|
||||
"clipboard-manager:default"
|
||||
"clipboard-manager:allow-read-text",
|
||||
"clipboard-manager:allow-write-text"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"main":{"identifier":"main","description":"Main permissions","local":true,"windows":["*"],"permissions":["os:allow-os-type","event:allow-emit","clipboard-manager:allow-write-text","clipboard-manager:allow-read-text","dialog:allow-open","dialog:allow-save","event:allow-listen","event:allow-unlisten","fs:allow-read-file","fs:allow-read-text-file",{"identifier":"fs:scope","allow":[{"path":"$APPDATA"},{"path":"$APPDATA/**"}]},"shell:allow-open",{"identifier":"shell:allow-execute","allow":[{"args":true,"name":"protoc","sidecar":true}]},"window:allow-close","window:allow-is-fullscreen","window:allow-maximize","window:allow-minimize","window:allow-set-decorations","window:allow-set-title","window:allow-start-dragging","window:allow-unmaximize","clipboard-manager:default"]}}
|
||||
{"main":{"identifier":"main","description":"Main permissions","local":true,"windows":["*"],"permissions":["os:allow-os-type","event:allow-emit","clipboard-manager:allow-write-text","clipboard-manager:allow-read-text","dialog:allow-open","dialog:allow-save","event:allow-listen","event:allow-unlisten","fs:allow-read-file","fs:allow-read-text-file",{"identifier":"fs:scope","allow":[{"path":"$APPDATA"},{"path":"$APPDATA/**"}]},"shell:allow-open",{"identifier":"shell:allow-execute","allow":[{"args":true,"name":"protoc","sidecar":true}]},"window:allow-close","window:allow-is-fullscreen","window:allow-maximize","window:allow-minimize","window:allow-set-decorations","window:allow-set-title","window:allow-start-dragging","window:allow-unmaximize","clipboard-manager:allow-read-text","clipboard-manager:allow-write-text"]}}
|
||||
6791
src-tauri/gen/schemas/linux-schema.json
Normal file
6791
src-tauri/gen/schemas/linux-schema.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -14,11 +14,9 @@ serde = { version = "1.0.196", features = ["derive"] }
|
||||
serde_json = "1.0.113"
|
||||
prost-reflect = { version = "0.12.0", features = ["serde", "derive"] }
|
||||
log = "0.4.20"
|
||||
once_cell = { version = "1.19.0", features = [] }
|
||||
anyhow = "1.0.79"
|
||||
hyper = { version = "0.14" }
|
||||
hyper-rustls = { version = "0.24.0", features = ["http2"] }
|
||||
protoc-bin-vendored = "3.0.0"
|
||||
uuid = { version = "1.7.0", features = ["v4"] }
|
||||
tauri = { version = "2.0.0-beta.16" }
|
||||
tauri-plugin-shell = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v2" }
|
||||
|
||||
@@ -9,10 +9,10 @@ var j = "(?:" + [
|
||||
">\\&",
|
||||
"<\\&",
|
||||
"[&;()|<>]"
|
||||
].join("|") + ")", D = new RegExp("^" + j + "$"), q = "|&;()<> \\t", M = '"((\\\\"|[^"])*?)"', Q = "'((\\\\'|[^'])*?)'", V = /^#$/, _ = "'", G = '"', U = "$", R = "", z = 4294967296;
|
||||
].join("|") + ")", D = new RegExp("^" + j + "$"), q = "|&;()<> \\t", M = '"((\\\\"|[^"])*?)"', Q = "'((\\\\'|[^'])*?)'", V = /^#$/, _ = "'", G = '"', U = "$", $ = "", z = 4294967296;
|
||||
for (var L = 0; L < 4; L++)
|
||||
R += (z * Math.random()).toString(16);
|
||||
var J = new RegExp("^" + R);
|
||||
$ += (z * Math.random()).toString(16);
|
||||
var J = new RegExp("^" + $);
|
||||
function X(n, s) {
|
||||
for (var e = s.lastIndex, t = [], c; c = s.exec(n); )
|
||||
t.push(c), s.lastIndex === c.index && (s.lastIndex += 1);
|
||||
@@ -20,7 +20,7 @@ function X(n, s) {
|
||||
}
|
||||
function F(n, s, e) {
|
||||
var t = typeof n == "function" ? n(e) : n[e];
|
||||
return typeof t > "u" && e != "" ? t = "" : typeof t > "u" && (t = "$"), typeof t == "object" ? s + R + JSON.stringify(t) + R : s + t;
|
||||
return typeof t > "u" && e != "" ? t = "" : typeof t > "u" && (t = "$"), typeof t == "object" ? s + $ + JSON.stringify(t) + $ : s + t;
|
||||
}
|
||||
function K(n, s, e) {
|
||||
e || (e = {});
|
||||
@@ -39,30 +39,30 @@ function K(n, s, e) {
|
||||
return;
|
||||
if (D.test(a))
|
||||
return { op: a };
|
||||
var x = !1, O = !1, p = "", A = !1, i;
|
||||
function b() {
|
||||
var x = !1, C = !1, d = "", O = !1, i;
|
||||
function T() {
|
||||
i += 1;
|
||||
var v, d, T = a.charAt(i);
|
||||
if (T === "{") {
|
||||
var v, p, R = a.charAt(i);
|
||||
if (R === "{") {
|
||||
if (i += 1, a.charAt(i) === "}")
|
||||
throw new Error("Bad substitution: " + a.slice(i - 2, i + 1));
|
||||
if (v = a.indexOf("}", i), v < 0)
|
||||
throw new Error("Bad substitution: " + a.slice(i));
|
||||
d = a.slice(i, v), i = v;
|
||||
} else if (/[*@#?$!_-]/.test(T))
|
||||
d = T, i += 1;
|
||||
p = a.slice(i, v), i = v;
|
||||
} else if (/[*@#?$!_-]/.test(R))
|
||||
p = R, i += 1;
|
||||
else {
|
||||
var g = a.slice(i);
|
||||
v = g.match(/[^\w\d_]/), v ? (d = g.slice(0, v.index), i += v.index - 1) : (d = g, i = a.length);
|
||||
v = g.match(/[^\w\d_]/), v ? (p = g.slice(0, v.index), i += v.index - 1) : (p = g, i = a.length);
|
||||
}
|
||||
return F(s, "", d);
|
||||
return F(s, "", p);
|
||||
}
|
||||
for (i = 0; i < a.length; i++) {
|
||||
var u = a.charAt(i);
|
||||
if (A = A || !x && (u === "*" || u === "?"), O)
|
||||
p += u, O = !1;
|
||||
if (O = O || !x && (u === "*" || u === "?"), C)
|
||||
d += u, C = !1;
|
||||
else if (x)
|
||||
u === x ? x = !1 : x == _ ? p += u : u === t ? (i += 1, u = a.charAt(i), u === G || u === t || u === U ? p += u : p += t + u) : u === U ? p += b() : p += u;
|
||||
u === x ? x = !1 : x == _ ? d += u : u === t ? (i += 1, u = a.charAt(i), u === G || u === t || u === U ? d += u : d += t + u) : u === U ? d += T() : d += u;
|
||||
else if (u === G || u === _)
|
||||
x = u;
|
||||
else {
|
||||
@@ -70,13 +70,13 @@ function K(n, s, e) {
|
||||
return { op: a };
|
||||
if (V.test(u)) {
|
||||
w = !0;
|
||||
var E = { comment: n.slice(r.index + i + 1) };
|
||||
return p.length ? [p, E] : [E];
|
||||
var b = { comment: n.slice(r.index + i + 1) };
|
||||
return d.length ? [d, b] : [b];
|
||||
} else
|
||||
u === t ? O = !0 : u === U ? p += b() : p += u;
|
||||
u === t ? C = !0 : u === U ? d += T() : d += u;
|
||||
}
|
||||
}
|
||||
return A ? { op: "glob", pattern: p } : p;
|
||||
return O ? { op: "glob", pattern: d } : d;
|
||||
}).reduce(function(r, a) {
|
||||
return typeof a > "u" ? r : r.concat(a);
|
||||
}, []);
|
||||
@@ -86,9 +86,9 @@ var Y = function(s, e, t) {
|
||||
return typeof e != "function" ? c : c.reduce(function(m, f) {
|
||||
if (typeof f == "object")
|
||||
return m.concat(f);
|
||||
var w = f.split(RegExp("(" + R + ".*?" + R + ")", "g"));
|
||||
var w = f.split(RegExp("(" + $ + ".*?" + $ + ")", "g"));
|
||||
return w.length === 1 ? m.concat(w[0]) : m.concat(w.filter(Boolean).map(function(r) {
|
||||
return J.test(r) ? JSON.parse(r.split(R)[1]) : r;
|
||||
return J.test(r) ? JSON.parse(r.split($)[1]) : r;
|
||||
}));
|
||||
}, []);
|
||||
}, Z = Y;
|
||||
@@ -118,7 +118,7 @@ const ae = "curl", se = "cURL", ie = "cURL command line tool", H = ["d", "data",
|
||||
function oe(n) {
|
||||
if (!n.match(/^\s*curl /))
|
||||
return null;
|
||||
const s = [], e = n.replace(/([^\\])\n/g, "$1; ");
|
||||
const s = [], e = n.replace(/\ncurl/g, "; curl");
|
||||
let t = [];
|
||||
const m = Z(e).flatMap((r) => typeof r == "string" && r.startsWith("-") && !r.startsWith("--") && r.length > 2 ? [r.slice(0, 2), r.slice(2)] : r);
|
||||
for (const r of m) {
|
||||
@@ -158,73 +158,84 @@ function te(n, s) {
|
||||
for (let o = 1; o < n.length; o++) {
|
||||
let l = n[o];
|
||||
if (typeof l == "string" && (l = l.trim()), typeof l == "string" && l.match(/^-{1,2}[\w-]+/)) {
|
||||
const $ = l[0] === "-" && l[1] !== "-";
|
||||
const E = l[0] === "-" && l[1] !== "-";
|
||||
let h = l.replace(/^-{1,2}/, "");
|
||||
if (!ee.includes(h))
|
||||
continue;
|
||||
let y;
|
||||
const S = n[o + 1];
|
||||
$ && h.length > 1 ? (y = h.slice(1), h = h.slice(0, 1)) : typeof S == "string" && !S.startsWith("-") ? (y = S, o++) : y = !0, e[h] = e[h] || [], e[h].push(y);
|
||||
E && h.length > 1 ? (y = h.slice(1), h = h.slice(0, 1)) : typeof S == "string" && !S.startsWith("-") ? (y = S, o++) : y = !0, e[h] = e[h] || [], e[h].push(y);
|
||||
} else
|
||||
l && t.push(l);
|
||||
}
|
||||
let c, m;
|
||||
const f = C(e, t[0] || "", ["url"]), [w, r] = W(f, "?");
|
||||
const f = A(e, t[0] || "", ["url"]), [w, r] = W(f, "?");
|
||||
c = (r == null ? void 0 : r.split("&").map((o) => {
|
||||
const l = W(o, "=");
|
||||
return { name: l[0] ?? "", value: l[1] ?? "" };
|
||||
return { name: l[0] ?? "", value: l[1] ?? "", enabled: !0 };
|
||||
})) ?? [], m = w ?? f;
|
||||
const [a, x] = C(e, "", ["u", "user"]).split(/:(.*)$/), O = C(e, !1, ["digest"]), p = a ? O ? "digest" : "basic" : null, A = a ? {
|
||||
const [a, x] = A(e, "", ["u", "user"]).split(/:(.*)$/), C = A(e, !1, ["digest"]), d = a ? C ? "digest" : "basic" : null, O = a ? {
|
||||
username: a.trim(),
|
||||
password: (x ?? "").trim()
|
||||
} : {}, i = [
|
||||
...e.header || [],
|
||||
...e.H || []
|
||||
].map((o) => {
|
||||
const [l, $] = o.split(/:(.*)$/);
|
||||
return $ ? {
|
||||
const [l, E] = o.split(/:(.*)$/);
|
||||
return E ? {
|
||||
name: (l ?? "").trim(),
|
||||
value: $.trim()
|
||||
value: E.trim(),
|
||||
enabled: !0
|
||||
} : {
|
||||
name: (l ?? "").trim().replace(/;$/, ""),
|
||||
value: ""
|
||||
value: "",
|
||||
enabled: !0
|
||||
};
|
||||
}), b = [
|
||||
}), T = [
|
||||
...e.cookie || [],
|
||||
...e.b || []
|
||||
].map((o) => {
|
||||
const l = o.split("=", 1)[0], $ = o.replace(`${l}=`, "");
|
||||
return `${l}=${$}`;
|
||||
const l = o.split("=", 1)[0], E = o.replace(`${l}=`, "");
|
||||
return `${l}=${E}`;
|
||||
}).join("; "), u = i.find((o) => o.name.toLowerCase() === "cookie");
|
||||
b && u ? u.value += `; ${b}` : b && i.push({
|
||||
T && u ? u.value += `; ${T}` : T && i.push({
|
||||
name: "Cookie",
|
||||
value: b
|
||||
value: T,
|
||||
enabled: !0
|
||||
});
|
||||
const E = ne(e), v = i.find((o) => o.name.toLowerCase() === "content-type"), d = v ? v.value.split(";")[0] : null, T = [
|
||||
const b = ne(e), v = i.find((o) => o.name.toLowerCase() === "content-type"), p = v ? v.value.split(";")[0] : null, R = [
|
||||
...e.form || [],
|
||||
...e.F || []
|
||||
].map((o) => {
|
||||
const l = o.split("="), $ = l[0] ?? "", h = l[1] ?? "", y = {
|
||||
name: $,
|
||||
const l = o.split("="), E = l[0] ?? "", h = l[1] ?? "", y = {
|
||||
name: E,
|
||||
enabled: !0
|
||||
};
|
||||
return h.indexOf("@") === 0 ? y.file = h.slice(1) : y.value = h, y;
|
||||
});
|
||||
let g = {}, I = null;
|
||||
const B = C(e, !1, ["G", "get"]);
|
||||
E.length > 0 && B ? c.push(...E) : E.length > 0 && (d == null || d === "application/x-www-form-urlencoded") ? (I = d ?? "application/x-www-form-urlencoded", g = {
|
||||
params: E.map((o) => ({
|
||||
const B = A(e, !1, ["G", "get"]);
|
||||
b.length > 0 && B ? c.push(...b) : b.length > 0 && (p == null || p === "application/x-www-form-urlencoded") ? (I = p ?? "application/x-www-form-urlencoded", g = {
|
||||
form: b.map((o) => ({
|
||||
...o,
|
||||
name: decodeURIComponent(o.name || ""),
|
||||
value: decodeURIComponent(o.value || "")
|
||||
}))
|
||||
}) : E.length > 0 ? (I = d === "application/json" || d === "text/xml" || d === "text/plain" ? d : "other", g = {
|
||||
text: E.map(({ name: o, value: l }) => o && l ? `${o}=${l}` : o || l).join("&")
|
||||
}) : T.length && (I = d ?? "multipart/form-data", g = {
|
||||
form: T
|
||||
});
|
||||
let P = C(e, "", ["X", "request"]).toUpperCase();
|
||||
return P === "" && g && (P = "text" in g || "params" in g ? "POST" : "GET"), {
|
||||
}, i.push({
|
||||
name: "Content-Type",
|
||||
value: "application/x-www-form-urlencoded",
|
||||
enabled: !0
|
||||
})) : b.length > 0 ? (I = p === "application/json" || p === "text/xml" || p === "text/plain" ? p : "other", g = {
|
||||
text: b.map(({ name: o, value: l }) => o && l ? `${o}=${l}` : o || l).join("&")
|
||||
}) : R.length && (I = p ?? "multipart/form-data", g = {
|
||||
form: R
|
||||
}, p == null && i.push({
|
||||
name: "Content-Type",
|
||||
value: "multipart/form-data",
|
||||
enabled: !0
|
||||
}));
|
||||
let P = A(e, "", ["X", "request"]).toUpperCase();
|
||||
return P === "" && g && (P = "text" in g || "form" in g ? "POST" : "GET"), {
|
||||
id: N("http_request"),
|
||||
model: "http_request",
|
||||
workspaceId: s,
|
||||
@@ -233,8 +244,8 @@ function te(n, s) {
|
||||
url: m,
|
||||
method: P,
|
||||
headers: i,
|
||||
authentication: A,
|
||||
authenticationType: p,
|
||||
authentication: O,
|
||||
authenticationType: d,
|
||||
body: g,
|
||||
bodyType: I,
|
||||
folderId: null,
|
||||
@@ -253,15 +264,17 @@ const ne = (n) => {
|
||||
c.startsWith("@") ? s.push({
|
||||
name: m ?? "",
|
||||
value: "",
|
||||
filePath: c.slice(1)
|
||||
filePath: c.slice(1),
|
||||
enabled: !0
|
||||
}) : s.push({
|
||||
name: m ?? "",
|
||||
value: e === "data-urlencode" ? encodeURIComponent(f ?? "") : f ?? ""
|
||||
value: e === "data-urlencode" ? encodeURIComponent(f ?? "") : f ?? "",
|
||||
enabled: !0
|
||||
});
|
||||
}
|
||||
}
|
||||
return s;
|
||||
}, C = (n, s, e) => {
|
||||
}, A = (n, s, e) => {
|
||||
for (const t of e)
|
||||
if (n[t] && n[t].length)
|
||||
return n[t][0];
|
||||
|
||||
@@ -10,6 +10,7 @@ use std::fs::{create_dir_all, File, read_to_string};
|
||||
use std::path::PathBuf;
|
||||
use std::process::exit;
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
|
||||
use ::http::Uri;
|
||||
use ::http::uri::InvalidUri;
|
||||
@@ -30,7 +31,6 @@ use tauri::TitleBarStyle;
|
||||
use tauri_plugin_log::{fern, Target, TargetKind};
|
||||
use tauri_plugin_shell::ShellExt;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::time::sleep;
|
||||
|
||||
use ::grpc::{Code, deserialize_message, serialize_message, ServiceDefinition};
|
||||
use ::grpc::manager::{DynamicMessage, GrpcHandle};
|
||||
@@ -48,22 +48,24 @@ use crate::models::{
|
||||
get_cookie_jar, get_environment, get_folder, get_grpc_connection,
|
||||
get_grpc_request, get_http_request, get_http_response, get_key_value_raw,
|
||||
get_or_create_settings, get_workspace, get_workspace_export_resources, GrpcConnection, GrpcEvent,
|
||||
GrpcEventType, GrpcRequest, HttpRequest, HttpRequestHeader, HttpResponse,
|
||||
GrpcEventType, GrpcRequest, HttpRequest, HttpResponse,
|
||||
KeyValue, list_cookie_jars, list_environments, list_folders, list_grpc_connections,
|
||||
list_grpc_events, list_grpc_requests, list_http_requests, list_responses, list_workspaces,
|
||||
ModelType, set_key_value_raw, Settings, update_response_if_id, update_settings, upsert_cookie_jar,
|
||||
upsert_environment, upsert_folder, upsert_grpc_connection, upsert_grpc_event, upsert_grpc_request, upsert_http_request, upsert_workspace,
|
||||
Workspace, WorkspaceExportResources,
|
||||
};
|
||||
use crate::notifications::YaakNotifier;
|
||||
use crate::plugin::{ImportResult, run_plugin_export_curl, run_plugin_import};
|
||||
use crate::render::render_request;
|
||||
use crate::updates::{update_mode_from_str, UpdateMode, YaakUpdater};
|
||||
use crate::updates::{UpdateMode, YaakUpdater};
|
||||
use crate::window_menu::app_menu;
|
||||
|
||||
mod analytics;
|
||||
mod grpc;
|
||||
mod http;
|
||||
mod models;
|
||||
mod notifications;
|
||||
mod plugin;
|
||||
mod render;
|
||||
mod updates;
|
||||
@@ -107,6 +109,16 @@ async fn cmd_metadata(app_handle: AppHandle) -> Result<AppMetaData, ()> {
|
||||
});
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn cmd_dismiss_notification(
|
||||
app: AppHandle,
|
||||
notification_id: &str,
|
||||
yaak_notifier: State<'_, Mutex<YaakNotifier>>,
|
||||
) -> Result<(), String> {
|
||||
info!("SEEN? {notification_id}");
|
||||
yaak_notifier.lock().await.seen(&app, notification_id).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn cmd_grpc_reflect(
|
||||
request_id: &str,
|
||||
@@ -1172,30 +1184,12 @@ async fn cmd_duplicate_grpc_request(id: &str, w: WebviewWindow) -> Result<GrpcRe
|
||||
|
||||
#[tauri::command]
|
||||
async fn cmd_create_http_request(
|
||||
workspace_id: &str,
|
||||
name: &str,
|
||||
sort_priority: f64,
|
||||
folder_id: Option<&str>,
|
||||
method: Option<&str>,
|
||||
headers: Option<Vec<HttpRequestHeader>>,
|
||||
body_type: Option<&str>,
|
||||
request: HttpRequest,
|
||||
w: WebviewWindow,
|
||||
) -> Result<HttpRequest, String> {
|
||||
upsert_http_request(
|
||||
&w,
|
||||
HttpRequest {
|
||||
workspace_id: workspace_id.to_string(),
|
||||
name: name.to_string(),
|
||||
folder_id: folder_id.map(|s| s.to_string()),
|
||||
body_type: body_type.map(|s| s.to_string()),
|
||||
method: method.map(|s| s.to_string()).unwrap_or("GET".to_string()),
|
||||
headers: Json(headers.unwrap_or_default()),
|
||||
sort_priority,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
upsert_http_request(&w, request)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -1588,6 +1582,10 @@ pub fn run() {
|
||||
let yaak_updater = YaakUpdater::new();
|
||||
app.manage(Mutex::new(yaak_updater));
|
||||
|
||||
// Add notifier
|
||||
let yaak_notifier = YaakNotifier::new();
|
||||
app.manage(Mutex::new(yaak_notifier));
|
||||
|
||||
// Add GRPC manager
|
||||
let grpc_handle = GrpcHandle::new(&app.app_handle());
|
||||
app.manage(Mutex::new(grpc_handle));
|
||||
@@ -1656,6 +1654,7 @@ pub fn run() {
|
||||
cmd_metadata,
|
||||
cmd_new_window,
|
||||
cmd_request_to_curl,
|
||||
cmd_dismiss_notification,
|
||||
cmd_send_ephemeral_request,
|
||||
cmd_send_http_request,
|
||||
cmd_set_key_value,
|
||||
@@ -1674,21 +1673,11 @@ pub fn run() {
|
||||
.run(|app_handle, event| {
|
||||
match event {
|
||||
RunEvent::Ready => {
|
||||
let w = create_window(app_handle, None);
|
||||
// if let Err(e) = w.restore_state(StateFlags::all()) {
|
||||
// error!("Failed to restore window state {}", e);
|
||||
// }
|
||||
|
||||
create_window(app_handle, None);
|
||||
let h = app_handle.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
let info = analytics::track_launch_event(&h).await;
|
||||
debug!("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 {
|
||||
@@ -1703,6 +1692,16 @@ pub fn run() {
|
||||
let update_mode = get_update_mode(&h).await;
|
||||
_ = val.lock().await.check(&h, update_mode).await;
|
||||
});
|
||||
|
||||
let h = app_handle.clone();
|
||||
tauri::async_runtime::spawn(async move {
|
||||
tokio::time::sleep(Duration::from_millis(4000)).await;
|
||||
let val: State<'_, Mutex<YaakNotifier>> = h.state();
|
||||
let mut n = val.lock().await;
|
||||
if let Err(e) = n.check(&h).await {
|
||||
warn!("Failed to check for notifications {}", e)
|
||||
}
|
||||
});
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
@@ -1722,6 +1721,9 @@ fn is_dev() -> bool {
|
||||
|
||||
fn create_window(handle: &AppHandle, url: Option<&str>) -> WebviewWindow {
|
||||
let menu = app_menu(handle).unwrap();
|
||||
|
||||
// This causes the window to not be clickable (in AppImage), so disable on Linux
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
handle.set_menu(menu).expect("Failed to set app menu");
|
||||
|
||||
let window_num = handle.webview_windows().len();
|
||||
@@ -1820,7 +1822,7 @@ fn create_window(handle: &AppHandle, url: Option<&str>) -> WebviewWindow {
|
||||
|
||||
async fn get_update_mode(h: &AppHandle) -> UpdateMode {
|
||||
let settings = get_or_create_settings(h).await;
|
||||
update_mode_from_str(settings.update_channel.as_str())
|
||||
UpdateMode::new(settings.update_channel.as_str())
|
||||
}
|
||||
|
||||
fn safe_uri(endpoint: &str) -> Result<Uri, InvalidUri> {
|
||||
|
||||
94
src-tauri/src/notifications.rs
Normal file
94
src-tauri/src/notifications.rs
Normal file
@@ -0,0 +1,94 @@
|
||||
use std::time::SystemTime;
|
||||
|
||||
use chrono::{Duration, NaiveDateTime, Utc};
|
||||
use http::Method;
|
||||
use log::debug;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tauri::{AppHandle, Manager};
|
||||
|
||||
use crate::models::{get_key_value_raw, set_key_value_raw};
|
||||
|
||||
// Check for updates every hour
|
||||
const MAX_UPDATE_CHECK_SECONDS: u64 = 60 * 60;
|
||||
|
||||
const KV_NAMESPACE: &str = "notifications";
|
||||
const KV_KEY: &str = "seen";
|
||||
|
||||
// Create updater struct
|
||||
pub struct YaakNotifier {
|
||||
last_check: SystemTime,
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Default)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct YaakNotification {
|
||||
timestamp: NaiveDateTime,
|
||||
id: String,
|
||||
message: String,
|
||||
action: Option<YaakNotificationAction>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct YaakNotificationAction {
|
||||
label: String,
|
||||
url: String,
|
||||
}
|
||||
|
||||
impl YaakNotifier {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
last_check: SystemTime::UNIX_EPOCH,
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn seen(&mut self, app: &AppHandle, id: &str) -> Result<(), String> {
|
||||
let mut seen = get_kv(app).await?;
|
||||
seen.push(id.to_string());
|
||||
debug!("Marked notification as seen {}", id);
|
||||
let seen_json = serde_json::to_string(&seen).map_err(|e| e.to_string())?;
|
||||
set_key_value_raw(app, KV_NAMESPACE, KV_KEY, seen_json.as_str()).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn check(&mut self, app: &AppHandle) -> Result<(), String> {
|
||||
let ignore_check = self.last_check.elapsed().unwrap().as_secs() < MAX_UPDATE_CHECK_SECONDS;
|
||||
|
||||
if ignore_check {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.last_check = SystemTime::now();
|
||||
|
||||
let info = app.package_info().clone();
|
||||
let req = reqwest::Client::default()
|
||||
.request(Method::GET, "https://notify.yaak.app/notifications")
|
||||
.query(&[("version", info.version)]);
|
||||
let resp = req.send().await.map_err(|e| e.to_string())?;
|
||||
let notification = resp
|
||||
.json::<YaakNotification>()
|
||||
.await
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
let age = notification
|
||||
.timestamp
|
||||
.signed_duration_since(Utc::now().naive_utc());
|
||||
let seen = get_kv(app).await?;
|
||||
if seen.contains(¬ification.id) || (age > Duration::days(1)) {
|
||||
debug!("Already seen notification {}", notification.id);
|
||||
return Ok(());
|
||||
}
|
||||
debug!("Got notification {:?}", notification);
|
||||
|
||||
let _ = app.emit("notification", notification.clone());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_kv(app: &AppHandle) -> Result<Vec<String>, String> {
|
||||
match get_key_value_raw(app, "notifications", "seen").await {
|
||||
None => Ok(Vec::new()),
|
||||
Some(v) => serde_json::from_str(&v.value).map_err(|e| e.to_string()),
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::time::SystemTime;
|
||||
|
||||
use log::info;
|
||||
@@ -20,6 +21,25 @@ pub enum UpdateMode {
|
||||
Beta,
|
||||
}
|
||||
|
||||
impl Display for UpdateMode {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||
let s = match self {
|
||||
UpdateMode::Stable => "stable",
|
||||
UpdateMode::Beta => "beta",
|
||||
};
|
||||
write!(f, "{}", s)
|
||||
}
|
||||
}
|
||||
|
||||
impl UpdateMode {
|
||||
pub fn new(mode: &str) -> UpdateMode {
|
||||
match mode {
|
||||
"beta" => UpdateMode::Beta,
|
||||
_ => UpdateMode::Stable,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl YaakUpdater {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
@@ -33,23 +53,21 @@ impl YaakUpdater {
|
||||
) -> Result<bool, tauri_plugin_updater::Error> {
|
||||
self.last_update_check = SystemTime::now();
|
||||
|
||||
let update_mode = get_update_mode_str(mode);
|
||||
let enabled = !is_dev();
|
||||
info!(
|
||||
"Checking for updates mode={} enabled={}",
|
||||
update_mode, enabled
|
||||
);
|
||||
let enabled = is_dev();
|
||||
info!("Checking for updates mode={} enabled={}", mode, enabled);
|
||||
|
||||
if !enabled {
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
match app_handle
|
||||
|
||||
let update_check_result = app_handle
|
||||
.updater_builder()
|
||||
.header("X-Update-Mode", update_mode)?
|
||||
.header("X-Update-Mode", mode.to_string())?
|
||||
.build()?
|
||||
.check()
|
||||
.await
|
||||
.await;
|
||||
|
||||
match update_check_result
|
||||
{
|
||||
Ok(Some(update)) => {
|
||||
let h = app_handle.clone();
|
||||
@@ -59,6 +77,8 @@ impl YaakUpdater {
|
||||
"{} is available. Would you like to download and install it now?",
|
||||
update.version
|
||||
))
|
||||
.ok_button_label("Download")
|
||||
.cancel_button_label("Later")
|
||||
.title("Update Available")
|
||||
.show(|confirmed| {
|
||||
if !confirmed {
|
||||
@@ -70,6 +90,8 @@ impl YaakUpdater {
|
||||
if h.dialog()
|
||||
.message("Would you like to restart the app?")
|
||||
.title("Update Installed")
|
||||
.ok_button_label("Restart")
|
||||
.cancel_button_label("Later")
|
||||
.blocking_show()
|
||||
{
|
||||
h.restart();
|
||||
@@ -102,17 +124,3 @@ impl YaakUpdater {
|
||||
self.force_check(app_handle, mode).await
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_mode_from_str(mode: &str) -> UpdateMode {
|
||||
match mode {
|
||||
"beta" => UpdateMode::Beta,
|
||||
_ => UpdateMode::Stable,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_update_mode_str(mode: UpdateMode) -> &'static str {
|
||||
match mode {
|
||||
UpdateMode::Stable => "stable",
|
||||
UpdateMode::Beta => "beta",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,15 +6,18 @@
|
||||
"frontendDist": "../dist"
|
||||
},
|
||||
"productName": "Yaak",
|
||||
"version": "2024.4.0-beta.2",
|
||||
"version": "2024.4.0",
|
||||
"identifier": "app.yaak.desktop",
|
||||
"app": {
|
||||
"withGlobalTauri": false,
|
||||
"security": {
|
||||
"assetProtocol": {
|
||||
"scope": [
|
||||
"$APPDATA/responses/*"
|
||||
]
|
||||
"enable": true,
|
||||
"scope": {
|
||||
"allow": [
|
||||
"$APPDATA/responses/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -24,48 +27,6 @@
|
||||
"https://update.yaak.app/check/{{target}}/{{arch}}/{{current_version}}"
|
||||
],
|
||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IEMxRDJFREQ1MjExQjdGN0IKUldSN2Z4c2gxZTNTd1FHNCtmYnFXMHVVQzhuNkJOM1cwOFBodmdLall3ckhKenpKUytHSTR1MlkK"
|
||||
},
|
||||
"allowlist": {
|
||||
"all": false,
|
||||
"os": {
|
||||
"allow-os-type": true
|
||||
},
|
||||
"fs": {
|
||||
"readFile": true,
|
||||
"scope": [
|
||||
"$RESOURCE/*",
|
||||
"$APPDATA/responses/*"
|
||||
]
|
||||
},
|
||||
"shell": {
|
||||
"all": false,
|
||||
"open": true,
|
||||
"sidecar": true,
|
||||
"scope": [
|
||||
{
|
||||
"name": "protoc",
|
||||
"sidecar": true,
|
||||
"args": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"window": {
|
||||
"close": true,
|
||||
"maximize": true,
|
||||
"minimize": true,
|
||||
"setDecorations": true,
|
||||
"setTitle": true,
|
||||
"startDragging": true,
|
||||
"unmaximize": true
|
||||
},
|
||||
"dialog": {
|
||||
"all": false,
|
||||
"open": true,
|
||||
"save": true
|
||||
},
|
||||
"path": {
|
||||
"all": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
|
||||
@@ -30,15 +30,14 @@ export function BinaryFileEditor({
|
||||
|
||||
const handleClick = async () => {
|
||||
await ignoreContentType.set(false);
|
||||
const path = await open({
|
||||
const selected = await open({
|
||||
title: 'Select File',
|
||||
multiple: false,
|
||||
});
|
||||
if (path) {
|
||||
onChange({ filePath: path });
|
||||
} else {
|
||||
onChange({ filePath: undefined });
|
||||
if (selected == null) {
|
||||
return;
|
||||
}
|
||||
onChange({ filePath: selected.path });
|
||||
};
|
||||
|
||||
const filePath = typeof body.filePath === 'string' ? body.filePath : undefined;
|
||||
|
||||
@@ -2,12 +2,15 @@ import { useCreateDropdownItems } from '../hooks/useCreateDropdownItems';
|
||||
import type { DropdownProps } from './core/Dropdown';
|
||||
import { Dropdown } from './core/Dropdown';
|
||||
|
||||
interface Props {
|
||||
interface Props extends Omit<DropdownProps, 'items'> {
|
||||
hideFolder?: boolean;
|
||||
children: DropdownProps['children'];
|
||||
}
|
||||
|
||||
export function CreateDropdown({ hideFolder, children }: Props) {
|
||||
export function CreateDropdown({ hideFolder, children, ...props }: Props) {
|
||||
const items = useCreateDropdownItems({ hideFolder, hideIcons: true });
|
||||
return <Dropdown items={items}>{children}</Dropdown>;
|
||||
return (
|
||||
<Dropdown items={items} {...props}>
|
||||
{children}
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { save } from '@tauri-apps/plugin-dialog';
|
||||
import { useState } from 'react';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import slugify from 'slugify';
|
||||
import type { Workspace } from '../lib/models';
|
||||
import { count } from '../lib/pluralize';
|
||||
@@ -10,16 +10,26 @@ import { HStack, VStack } from './core/Stacks';
|
||||
|
||||
interface Props {
|
||||
onHide: () => void;
|
||||
onSuccess: (path: string) => void;
|
||||
activeWorkspace: Workspace;
|
||||
workspaces: Workspace[];
|
||||
}
|
||||
|
||||
export function ExportDataDialog({ onHide, activeWorkspace, workspaces: allWorkspaces }: Props) {
|
||||
export function ExportDataDialog({
|
||||
onHide,
|
||||
onSuccess,
|
||||
activeWorkspace,
|
||||
workspaces: allWorkspaces,
|
||||
}: Props) {
|
||||
const [selectedWorkspaces, setSelectedWorkspaces] = useState<Record<string, boolean>>({
|
||||
[activeWorkspace.id]: true,
|
||||
});
|
||||
|
||||
const workspaces = [activeWorkspace, ...allWorkspaces.filter((w) => w.id !== activeWorkspace.id)];
|
||||
// Put active workspace first
|
||||
const workspaces = useMemo(
|
||||
() => [activeWorkspace, ...allWorkspaces.filter((w) => w.id !== activeWorkspace.id)],
|
||||
[activeWorkspace, allWorkspaces],
|
||||
);
|
||||
|
||||
const handleToggleAll = () => {
|
||||
setSelectedWorkspaces(
|
||||
@@ -27,7 +37,7 @@ export function ExportDataDialog({ onHide, activeWorkspace, workspaces: allWorks
|
||||
);
|
||||
};
|
||||
|
||||
const handleExport = async () => {
|
||||
const handleExport = useCallback(async () => {
|
||||
const ids = Object.keys(selectedWorkspaces).filter((k) => selectedWorkspaces[k]);
|
||||
const workspace = ids.length === 1 ? workspaces.find((w) => w.id === ids[0]) : undefined;
|
||||
const slug = workspace ? slugify(workspace.name, { lower: true }) : 'workspaces';
|
||||
@@ -41,7 +51,8 @@ export function ExportDataDialog({ onHide, activeWorkspace, workspaces: allWorks
|
||||
|
||||
await invoke('cmd_export_data', { workspaceIds: ids, exportPath });
|
||||
onHide();
|
||||
};
|
||||
onSuccess(exportPath);
|
||||
}, [onHide, onSuccess, selectedWorkspaces, workspaces]);
|
||||
|
||||
const allSelected = workspaces.every((w) => selectedWorkspaces[w.id]);
|
||||
const numSelected = Object.values(selectedWorkspaces).filter(Boolean).length;
|
||||
@@ -97,7 +108,7 @@ export function ExportDataDialog({ onHide, activeWorkspace, workspaces: allWorks
|
||||
className="focus"
|
||||
color="primary"
|
||||
disabled={noneSelected}
|
||||
onClick={handleExport}
|
||||
onClick={() => handleExport()}
|
||||
>
|
||||
Export {count('Workspace', numSelected, { omitSingle: true, noneWord: 'Nothing' })}
|
||||
</Button>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { getCurrent } from '@tauri-apps/api/webviewWindow';
|
||||
import { useEffect } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useCommandPalette } from '../hooks/useCommandPalette';
|
||||
import { cookieJarsQueryKey } from '../hooks/useCookieJars';
|
||||
@@ -24,12 +24,12 @@ import { workspacesQueryKey } from '../hooks/useWorkspaces';
|
||||
import type { Model } from '../lib/models';
|
||||
import { modelsEq } from '../lib/models';
|
||||
import { setPathname } from '../lib/persistPathname';
|
||||
import { useNotificationToast } from '../hooks/useNotificationToast';
|
||||
|
||||
const DEFAULT_FONT_SIZE = 16;
|
||||
|
||||
export function GlobalHooks() {
|
||||
// Include here so they always update, even
|
||||
// if no component references them
|
||||
// Include here so they always update, even if no component references them
|
||||
useRecentWorkspaces();
|
||||
useRecentEnvironments();
|
||||
useRecentRequests();
|
||||
@@ -38,6 +38,7 @@ export function GlobalHooks() {
|
||||
useSyncWindowTitle();
|
||||
useGlobalCommands();
|
||||
useCommandPalette();
|
||||
useNotificationToast();
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const { wasUpdatedExternally } = useRequestUpdateKey(null);
|
||||
|
||||
@@ -10,9 +10,11 @@ import { JsonAttributeTree } from './core/JsonAttributeTree';
|
||||
import { KeyValueRow, KeyValueRows } from './core/KeyValueRow';
|
||||
import { Separator } from './core/Separator';
|
||||
import { SplitLayout } from './core/SplitLayout';
|
||||
import { HStack } from './core/Stacks';
|
||||
import { HStack, VStack } from './core/Stacks';
|
||||
import { EmptyStateText } from './EmptyStateText';
|
||||
import { RecentConnectionsDropdown } from './RecentConnectionsDropdown';
|
||||
import { Button } from './core/Button';
|
||||
import { useStateWithDeps } from '../hooks/useStateWithDeps';
|
||||
|
||||
interface Props {
|
||||
style?: CSSProperties;
|
||||
@@ -32,6 +34,8 @@ export function GrpcConnectionMessagesPane({ style, methodType, activeRequest }:
|
||||
const connections = useGrpcConnections(activeRequest.id ?? null);
|
||||
const activeConnection = connections[0] ?? null;
|
||||
const events = useGrpcEvents(activeConnection?.id ?? null);
|
||||
const [showLarge, setShowLarge] = useStateWithDeps<boolean>(false, [activeRequest.id]);
|
||||
const [showingLarge, setShowingLarge] = useState<boolean>(false);
|
||||
|
||||
const activeEvent = useMemo(
|
||||
() => events.find((m) => m.id === activeEventId) ?? null,
|
||||
@@ -102,7 +106,30 @@ export function GrpcConnectionMessagesPane({ style, methodType, activeRequest }:
|
||||
<div className="mb-2 select-text cursor-text font-semibold">
|
||||
Message {activeEvent.eventType === 'client_message' ? 'Sent' : 'Received'}
|
||||
</div>
|
||||
<JsonAttributeTree attrValue={JSON.parse(activeEvent?.content ?? '{}')} />
|
||||
{!showLarge && activeEvent.content.length > 1000 * 1000 ? (
|
||||
<VStack space={2} className="text-sm italic text-gray-500">
|
||||
Message previews larger than 1MB are hidden
|
||||
<div>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setShowingLarge(true);
|
||||
setTimeout(() => {
|
||||
setShowLarge(true);
|
||||
setShowingLarge(false);
|
||||
}, 500);
|
||||
}}
|
||||
isLoading={showingLarge}
|
||||
color="gray"
|
||||
variant="border"
|
||||
size="xs"
|
||||
>
|
||||
Try Showing
|
||||
</Button>
|
||||
</div>
|
||||
</VStack>
|
||||
) : (
|
||||
<JsonAttributeTree attrValue={JSON.parse(activeEvent?.content ?? '{}')} />
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className="h-full grid grid-rows-[auto_minmax(0,1fr)]">
|
||||
@@ -111,7 +138,7 @@ export function GrpcConnectionMessagesPane({ style, methodType, activeRequest }:
|
||||
{activeEvent.content}
|
||||
</div>
|
||||
{activeEvent.error && (
|
||||
<div className="text-xs font-mono py-1 text-orange-700">
|
||||
<div className="select-text cursor-text text-xs font-mono py-1 text-orange-700">
|
||||
{activeEvent.error}
|
||||
</div>
|
||||
)}
|
||||
@@ -196,12 +223,8 @@ function EventRow({
|
||||
}
|
||||
/>
|
||||
<div className={classNames('w-full truncate text-2xs')}>
|
||||
{content}
|
||||
{error && (
|
||||
<>
|
||||
<span className="text-orange-600"> ({error})</span>
|
||||
</>
|
||||
)}
|
||||
{content.slice(0, 1000)}
|
||||
{error && <span className="text-orange-600"> ({error})</span>}
|
||||
</div>
|
||||
<div className={classNames('opacity-50 text-2xs')}>
|
||||
{format(createdAt + 'Z', 'HH:mm:ss.SSS')}
|
||||
|
||||
@@ -42,15 +42,15 @@ export function GrpcProtoSelection({ requestId }: Props) {
|
||||
color="primary"
|
||||
size="sm"
|
||||
onClick={async () => {
|
||||
const files = await open({
|
||||
const selected = await open({
|
||||
title: 'Select Proto Files',
|
||||
multiple: true,
|
||||
filters: [{ name: 'Proto Files', extensions: ['proto'] }],
|
||||
});
|
||||
if (files == null) {
|
||||
if (selected == null) {
|
||||
return;
|
||||
}
|
||||
const newFiles = files.map((f) => f.path).filter((p) => !protoFiles.includes(p));
|
||||
const newFiles = selected.map((f) => f.path).filter((p) => !protoFiles.includes(p));
|
||||
await protoFilesKv.set([...protoFiles, ...newFiles]);
|
||||
await grpc.reflect.refetch();
|
||||
}}
|
||||
|
||||
43
src-web/components/ImportCurlButton.tsx
Normal file
43
src-web/components/ImportCurlButton.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Button } from './core/Button';
|
||||
import { useClipboardText } from '../hooks/useClipboardText';
|
||||
import { useImportCurl } from '../hooks/useImportCurl';
|
||||
import { Icon } from './core/Icon';
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
export function ImportCurlButton() {
|
||||
const [clipboardText] = useClipboardText();
|
||||
const importCurl = useImportCurl({ clearClipboard: true });
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
if (!clipboardText?.trim().startsWith('curl ')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ delay: 0.5 }}
|
||||
>
|
||||
<Button
|
||||
size="xs"
|
||||
variant="border"
|
||||
color="secondary"
|
||||
leftSlot={<Icon icon="paste" size="sm" />}
|
||||
isLoading={isLoading}
|
||||
onClick={() => {
|
||||
setIsLoading(true);
|
||||
importCurl
|
||||
.mutateAsync({
|
||||
requestId: null, // Create request
|
||||
command: clipboardText,
|
||||
})
|
||||
.finally(() => setIsLoading(false));
|
||||
}}
|
||||
>
|
||||
Import Curl
|
||||
</Button>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
@@ -38,9 +38,9 @@ import { GraphQLEditor } from './GraphQLEditor';
|
||||
import { HeadersEditor } from './HeadersEditor';
|
||||
import { UrlBar } from './UrlBar';
|
||||
import { UrlParametersEditor } from './UrlParameterEditor';
|
||||
import { useCurlToRequest } from '../hooks/useCurlToRequest';
|
||||
import { useToast } from './ToastContext';
|
||||
import { Icon } from './core/Icon';
|
||||
import { useImportCurl } from '../hooks/useImportCurl';
|
||||
import { useRequests } from '../hooks/useRequests';
|
||||
import type { GenericCompletionOption } from './core/Editor/genericCompletion';
|
||||
|
||||
interface Props {
|
||||
style: CSSProperties;
|
||||
@@ -57,6 +57,7 @@ export const RequestPane = memo(function RequestPane({
|
||||
className,
|
||||
activeRequest,
|
||||
}: Props) {
|
||||
const requests = useRequests();
|
||||
const activeRequestId = activeRequest.id;
|
||||
const updateRequest = useUpdateHttpRequest(activeRequestId);
|
||||
const [activeTab, setActiveTab] = useActiveTab();
|
||||
@@ -230,11 +231,9 @@ export const RequestPane = memo(function RequestPane({
|
||||
[updateRequest],
|
||||
);
|
||||
|
||||
const importCurl = useCurlToRequest();
|
||||
const toast = useToast();
|
||||
|
||||
const isLoading = useIsResponseLoading(activeRequestId ?? null);
|
||||
const { updateKey } = useRequestUpdateKey(activeRequestId ?? null);
|
||||
const importCurl = useImportCurl({ clearClipboard: true });
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -244,6 +243,7 @@ export const RequestPane = memo(function RequestPane({
|
||||
{activeRequest && (
|
||||
<>
|
||||
<UrlBar
|
||||
key={forceUpdateKey}
|
||||
url={activeRequest.url}
|
||||
method={activeRequest.method}
|
||||
placeholder="https://example.com"
|
||||
@@ -252,14 +252,24 @@ export const RequestPane = memo(function RequestPane({
|
||||
return;
|
||||
}
|
||||
importCurl.mutate({ requestId: activeRequestId, command });
|
||||
toast.show({
|
||||
render: () => [
|
||||
<>
|
||||
<Icon icon="info" />
|
||||
<span>Curl command imported</span>
|
||||
</>,
|
||||
],
|
||||
});
|
||||
}}
|
||||
autocomplete={{
|
||||
minMatch: 3,
|
||||
options:
|
||||
requests.length > 0
|
||||
? [
|
||||
...requests.map(
|
||||
(r) =>
|
||||
({
|
||||
type: 'constant',
|
||||
label: r.url,
|
||||
} as GenericCompletionOption),
|
||||
),
|
||||
]
|
||||
: [
|
||||
{ label: 'http://', type: 'constant' },
|
||||
{ label: 'https://', type: 'constant' },
|
||||
],
|
||||
}}
|
||||
onSend={handleSend}
|
||||
onCancel={handleCancel}
|
||||
@@ -326,17 +336,6 @@ export const RequestPane = memo(function RequestPane({
|
||||
contentType="text/xml"
|
||||
onChange={handleBodyTextChange}
|
||||
/>
|
||||
) : activeRequest.bodyType === BODY_TYPE_OTHER ? (
|
||||
<Editor
|
||||
forceUpdateKey={forceUpdateKey}
|
||||
useTemplating
|
||||
autocompleteVariables
|
||||
placeholder="..."
|
||||
className="!bg-gray-50"
|
||||
heightMode={fullHeight ? 'full' : 'auto'}
|
||||
defaultValue={`${activeRequest.body?.text ?? ''}`}
|
||||
onChange={handleBodyTextChange}
|
||||
/>
|
||||
) : activeRequest.bodyType === BODY_TYPE_GRAPHQL ? (
|
||||
<GraphQLEditor
|
||||
forceUpdateKey={forceUpdateKey}
|
||||
@@ -365,6 +364,17 @@ export const RequestPane = memo(function RequestPane({
|
||||
onChange={handleBinaryFileChange}
|
||||
onChangeContentType={handleContentTypeChange}
|
||||
/>
|
||||
) : typeof activeRequest.bodyType === 'string' ? (
|
||||
<Editor
|
||||
forceUpdateKey={forceUpdateKey}
|
||||
useTemplating
|
||||
autocompleteVariables
|
||||
placeholder="..."
|
||||
className="!bg-gray-50"
|
||||
heightMode={fullHeight ? 'full' : 'auto'}
|
||||
defaultValue={`${activeRequest.body?.text ?? ''}`}
|
||||
onChange={handleBodyTextChange}
|
||||
/>
|
||||
) : (
|
||||
<EmptyStateText>Empty Body</EmptyStateText>
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { open } from '@tauri-apps/plugin-shell';
|
||||
import { useRef, useState } from 'react';
|
||||
import { useRef } from 'react';
|
||||
import { useAppInfo } from '../hooks/useAppInfo';
|
||||
import { useCheckForUpdates } from '../hooks/useCheckForUpdates';
|
||||
import { useExportData } from '../hooks/useExportData';
|
||||
@@ -20,11 +20,6 @@ export function SettingsDropdown() {
|
||||
const dropdownRef = useRef<DropdownRef>(null);
|
||||
const dialog = useDialog();
|
||||
const checkForUpdates = useCheckForUpdates();
|
||||
const [showChangelog, setShowChangelog] = useState<boolean>(false);
|
||||
|
||||
useListenToTauriEvent('show_changelog', () => {
|
||||
setShowChangelog(true);
|
||||
});
|
||||
|
||||
const showSettings = () => {
|
||||
dialog.show({
|
||||
@@ -40,7 +35,6 @@ export function SettingsDropdown() {
|
||||
return (
|
||||
<Dropdown
|
||||
ref={dropdownRef}
|
||||
onClose={() => setShowChangelog(false)}
|
||||
items={[
|
||||
{
|
||||
key: 'settings',
|
||||
@@ -92,20 +86,13 @@ export function SettingsDropdown() {
|
||||
{
|
||||
key: 'changelog',
|
||||
label: 'Changelog',
|
||||
variant: showChangelog ? 'notify' : 'default',
|
||||
leftSlot: <Icon icon="cake" />,
|
||||
rightSlot: <Icon icon="externalLink" />,
|
||||
onSelect: () => open(`https://yaak.app/changelog/${appInfo.data?.version}`),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<IconButton
|
||||
size="sm"
|
||||
title="Main Menu"
|
||||
icon="settings"
|
||||
className="pointer-events-auto"
|
||||
showBadge={showChangelog}
|
||||
/>
|
||||
<IconButton size="sm" title="Main Menu" icon="settings" className="pointer-events-auto" />
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -608,7 +608,7 @@ const SidebarItem = forwardRef(function SidebarItem(
|
||||
const deleteRequest = useDeleteRequest(activeRequest ?? null);
|
||||
const duplicateHttpRequest = useDuplicateHttpRequest({ id: itemId, navigateAfter: true });
|
||||
const duplicateGrpcRequest = useDuplicateGrpcRequest({ id: itemId, navigateAfter: true });
|
||||
const [copyAsCurl] = useCopyAsCurl(itemId);
|
||||
const [, copyAsCurl] = useCopyAsCurl(itemId);
|
||||
const sendRequest = useSendRequest(itemId);
|
||||
const sendManyRequests = useSendManyRequests();
|
||||
const latestHttpResponse = useLatestHttpResponse(itemId);
|
||||
|
||||
@@ -34,13 +34,8 @@ export function SidebarActions() {
|
||||
hotkeyAction="sidebar.toggle"
|
||||
icon={hidden ? 'leftPanelHidden' : 'leftPanelVisible'}
|
||||
/>
|
||||
<CreateDropdown>
|
||||
<IconButton
|
||||
size="sm"
|
||||
icon="plusCircle"
|
||||
title="Add Resource"
|
||||
hotkeyAction="http_request.create"
|
||||
/>
|
||||
<CreateDropdown hotKeyAction="http_request.create">
|
||||
<IconButton size="sm" icon="plusCircle" title="Add Resource" />
|
||||
</CreateDropdown>
|
||||
</HStack>
|
||||
);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import React, { createContext, useContext, useMemo, useRef, useState } from 'react';
|
||||
import type { ToastProps } from './core/Toast';
|
||||
import { Toast } from './core/Toast';
|
||||
@@ -6,13 +7,15 @@ import { Portal } from './Portal';
|
||||
import { AnimatePresence } from 'framer-motion';
|
||||
|
||||
type ToastEntry = {
|
||||
render: ({ hide }: { hide: () => void }) => React.ReactNode;
|
||||
timeout?: number;
|
||||
id?: string;
|
||||
message: ReactNode;
|
||||
timeout?: number | null;
|
||||
onClose?: ToastProps['onClose'];
|
||||
} & Omit<ToastProps, 'onClose' | 'open' | 'children' | 'timeout'>;
|
||||
|
||||
type PrivateToastEntry = ToastEntry & {
|
||||
id: string;
|
||||
timeout: number;
|
||||
timeout: number | null;
|
||||
};
|
||||
|
||||
interface State {
|
||||
@@ -33,16 +36,26 @@ export const ToastProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const timeoutRef = useRef<NodeJS.Timeout>();
|
||||
const actions = useMemo<Actions>(
|
||||
() => ({
|
||||
show({ timeout = 4000, ...props }: ToastEntry) {
|
||||
const id = generateId();
|
||||
timeoutRef.current = setTimeout(() => {
|
||||
this.hide(id);
|
||||
}, timeout);
|
||||
setToasts((a) => [...a.filter((d) => d.id !== id), { id, timeout, ...props }]);
|
||||
show({ id, timeout = 4000, ...props }: ToastEntry) {
|
||||
id = id ?? generateId();
|
||||
if (timeout != null) {
|
||||
timeoutRef.current = setTimeout(() => this.hide(id), timeout);
|
||||
}
|
||||
setToasts((a) => {
|
||||
if (a.some((v) => v.id === id)) {
|
||||
// It's already visible with this id
|
||||
return a;
|
||||
}
|
||||
return [...a, { id, timeout, ...props }];
|
||||
});
|
||||
return id;
|
||||
},
|
||||
hide: (id: string) => {
|
||||
setToasts((a) => a.filter((d) => d.id !== id));
|
||||
setToasts((all) => {
|
||||
const t = all.find((t) => t.id === id);
|
||||
t?.onClose?.();
|
||||
return all.filter((t) => t.id !== id);
|
||||
});
|
||||
},
|
||||
}),
|
||||
[],
|
||||
@@ -66,12 +79,18 @@ export const ToastProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
);
|
||||
};
|
||||
|
||||
function ToastInstance({ id, render, timeout, ...props }: PrivateToastEntry) {
|
||||
function ToastInstance({ id, message, timeout, ...props }: PrivateToastEntry) {
|
||||
const { actions } = useContext(ToastContext);
|
||||
const children = render({ hide: () => actions.hide(id) });
|
||||
return (
|
||||
<Toast open timeout={timeout} onClose={() => actions.hide(id)} {...props}>
|
||||
{children}
|
||||
<Toast
|
||||
open
|
||||
timeout={timeout}
|
||||
{...props}
|
||||
// We call onClose inside actions.hide instead of passing to toast so that
|
||||
// it gets called from external close calls as well
|
||||
onClose={() => actions.hide(id)}
|
||||
>
|
||||
{message}
|
||||
</Toast>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import type { EditorView } from 'codemirror';
|
||||
import type { FormEvent } from 'react';
|
||||
import type { FormEvent, ReactNode } from 'react';
|
||||
import { memo, useRef, useState } from 'react';
|
||||
import { useHotKey } from '../hooks/useHotKey';
|
||||
import type { HttpRequest } from '../lib/models';
|
||||
import type { IconProps } from './core/Icon';
|
||||
import { IconButton } from './core/IconButton';
|
||||
import type { InputProps } from './core/Input';
|
||||
import { Input } from './core/Input';
|
||||
import { RequestMethodDropdown } from './RequestMethodDropdown';
|
||||
|
||||
@@ -20,6 +21,8 @@ type Props = Pick<HttpRequest, 'url'> & {
|
||||
onMethodChange?: (method: string) => void;
|
||||
isLoading: boolean;
|
||||
forceUpdateKey: string;
|
||||
rightSlot?: ReactNode;
|
||||
autocomplete?: InputProps['autocomplete'];
|
||||
};
|
||||
|
||||
export const UrlBar = memo(function UrlBar({
|
||||
@@ -34,6 +37,8 @@ export const UrlBar = memo(function UrlBar({
|
||||
onMethodChange,
|
||||
onPaste,
|
||||
submitIcon = 'sendHorizontal',
|
||||
autocomplete,
|
||||
rightSlot,
|
||||
isLoading,
|
||||
}: Props) {
|
||||
const inputRef = useRef<EditorView>(null);
|
||||
@@ -65,6 +70,7 @@ export const UrlBar = memo(function UrlBar({
|
||||
className="pl-0 pr-1.5 py-0.5"
|
||||
name="url"
|
||||
label="Enter URL"
|
||||
autocomplete={autocomplete}
|
||||
forceUpdateKey={forceUpdateKey}
|
||||
onFocus={() => setIsFocused(true)}
|
||||
onBlur={() => setIsFocused(false)}
|
||||
@@ -84,17 +90,20 @@ export const UrlBar = memo(function UrlBar({
|
||||
)
|
||||
}
|
||||
rightSlot={
|
||||
submitIcon !== null && (
|
||||
<IconButton
|
||||
size="xs"
|
||||
iconSize="md"
|
||||
title="Send Request"
|
||||
type="submit"
|
||||
className="w-8 my-0.5 mr-0.5"
|
||||
icon={isLoading ? 'x' : submitIcon}
|
||||
hotkeyAction="http_request.send"
|
||||
/>
|
||||
)
|
||||
<>
|
||||
{rightSlot}
|
||||
{submitIcon !== null && (
|
||||
<IconButton
|
||||
size="xs"
|
||||
iconSize="md"
|
||||
title="Send Request"
|
||||
type="submit"
|
||||
className="w-8 my-0.5 mr-0.5"
|
||||
icon={isLoading ? 'x' : submitIcon}
|
||||
hotkeyAction="http_request.send"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</form>
|
||||
|
||||
@@ -6,7 +6,7 @@ import type {
|
||||
MouseEvent as ReactMouseEvent,
|
||||
ReactNode,
|
||||
} from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { useWindowSize } from 'react-use';
|
||||
import { useActiveRequest } from '../hooks/useActiveRequest';
|
||||
import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
|
||||
@@ -33,8 +33,6 @@ import { ResizeHandle } from './ResizeHandle';
|
||||
import { Sidebar } from './Sidebar';
|
||||
import { SidebarActions } from './SidebarActions';
|
||||
import { WorkspaceHeader } from './WorkspaceHeader';
|
||||
import { useClipboardText } from '../hooks/useClipboardText';
|
||||
import { useToast } from './ToastContext';
|
||||
|
||||
const side = { gridArea: 'side' };
|
||||
const head = { gridArea: 'head' };
|
||||
@@ -56,24 +54,6 @@ export default function Workspace() {
|
||||
const moveState = useRef<{ move: (e: MouseEvent) => void; up: (e: MouseEvent) => void } | null>(
|
||||
null,
|
||||
);
|
||||
const clipboardText = useClipboardText();
|
||||
const toast = useToast();
|
||||
|
||||
useEffect(() => {
|
||||
const isCurlInClipboard = clipboardText?.startsWith('curl ');
|
||||
if (!isCurlInClipboard) {
|
||||
return;
|
||||
}
|
||||
|
||||
toast.show({
|
||||
render: () => (
|
||||
<div>
|
||||
<p>Curl command detected?</p>
|
||||
<Button color="primary">Import</Button>
|
||||
</div>
|
||||
),
|
||||
});
|
||||
}, [clipboardText, toast]);
|
||||
|
||||
const unsub = () => {
|
||||
if (moveState.current !== null) {
|
||||
|
||||
@@ -11,6 +11,7 @@ import { RecentRequestsDropdown } from './RecentRequestsDropdown';
|
||||
import { SettingsDropdown } from './SettingsDropdown';
|
||||
import { SidebarActions } from './SidebarActions';
|
||||
import { WorkspaceActionsDropdown } from './WorkspaceActionsDropdown';
|
||||
import { ImportCurlButton } from './ImportCurlButton';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
@@ -19,6 +20,7 @@ interface Props {
|
||||
export const WorkspaceHeader = memo(function WorkspaceHeader({ className }: Props) {
|
||||
const osInfo = useOsInfo();
|
||||
const [maximized, setMaximized] = useState<boolean>(false);
|
||||
|
||||
return (
|
||||
<HStack
|
||||
space={2}
|
||||
@@ -38,7 +40,8 @@ export const WorkspaceHeader = memo(function WorkspaceHeader({ className }: Prop
|
||||
<div className="pointer-events-none">
|
||||
<RecentRequestsDropdown />
|
||||
</div>
|
||||
<div className="flex-1 flex items-center h-full justify-end pointer-events-none">
|
||||
<div className="flex-1 flex gap-1 items-center h-full justify-end pointer-events-none">
|
||||
<ImportCurlButton />
|
||||
<SettingsDropdown />
|
||||
{(osInfo?.osType === 'linux' || osInfo?.osType === 'windows') && (
|
||||
<HStack className="ml-4" alignItems="center">
|
||||
|
||||
@@ -10,7 +10,7 @@ export type ButtonProps = Omit<HTMLAttributes<HTMLButtonElement>, 'color'> & {
|
||||
color?: 'custom' | 'default' | 'gray' | 'primary' | 'secondary' | 'warning' | 'danger';
|
||||
variant?: 'border' | 'solid';
|
||||
isLoading?: boolean;
|
||||
size?: 'sm' | 'md' | 'xs';
|
||||
size?: 'xs' | 'sm' | 'md';
|
||||
justify?: 'start' | 'center';
|
||||
type?: 'button' | 'submit';
|
||||
forDropdown?: boolean;
|
||||
|
||||
@@ -30,6 +30,7 @@ import { HotKey } from './HotKey';
|
||||
import { Separator } from './Separator';
|
||||
import { HStack, VStack } from './Stacks';
|
||||
import { Icon } from './Icon';
|
||||
import { useStateWithDeps } from '../../hooks/useStateWithDeps';
|
||||
|
||||
export type DropdownItemSeparator = {
|
||||
type: 'separator';
|
||||
@@ -58,6 +59,7 @@ export interface DropdownProps {
|
||||
items: DropdownItem[];
|
||||
onOpen?: () => void;
|
||||
onClose?: () => void;
|
||||
hotKeyAction?: HotkeyAction;
|
||||
}
|
||||
|
||||
export interface DropdownRef {
|
||||
@@ -71,7 +73,7 @@ export interface DropdownRef {
|
||||
}
|
||||
|
||||
export const Dropdown = forwardRef<DropdownRef, DropdownProps>(function Dropdown(
|
||||
{ children, items, onOpen, onClose }: DropdownProps,
|
||||
{ children, items, onOpen, onClose, hotKeyAction }: DropdownProps,
|
||||
ref,
|
||||
) {
|
||||
const [isOpen, _setIsOpen] = useState<boolean>(false);
|
||||
@@ -88,18 +90,33 @@ export const Dropdown = forwardRef<DropdownRef, DropdownProps>(function Dropdown
|
||||
[onClose, onOpen],
|
||||
);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
setIsOpen(false);
|
||||
buttonRef.current?.focus();
|
||||
// Reset so it triggers a render if opening sets to 0, for example
|
||||
setDefaultSelectedIndex(undefined);
|
||||
}, [setIsOpen]);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
...menuRef.current,
|
||||
isOpen: isOpen,
|
||||
toggle() {
|
||||
if (!isOpen) this.open();
|
||||
else setIsOpen(false);
|
||||
else this.close();
|
||||
},
|
||||
open() {
|
||||
setIsOpen(true);
|
||||
},
|
||||
close() {
|
||||
handleClose();
|
||||
},
|
||||
}));
|
||||
|
||||
useHotKey(hotKeyAction ?? null, () => {
|
||||
setDefaultSelectedIndex(0);
|
||||
setIsOpen(true);
|
||||
});
|
||||
|
||||
const child = useMemo(() => {
|
||||
const existingChild = Children.only(children);
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@@ -119,11 +136,6 @@ export const Dropdown = forwardRef<DropdownRef, DropdownProps>(function Dropdown
|
||||
return cloneElement(existingChild, props);
|
||||
}, [children, setIsOpen]);
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
setIsOpen(false);
|
||||
buttonRef.current?.focus();
|
||||
}, [setIsOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
buttonRef.current?.setAttribute('aria-expanded', isOpen.toString());
|
||||
}, [isOpen]);
|
||||
@@ -206,7 +218,10 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle'>, MenuPro
|
||||
}: MenuProps,
|
||||
ref,
|
||||
) {
|
||||
const [selectedIndex, setSelectedIndex] = useState<number | null>(defaultSelectedIndex ?? null);
|
||||
const [selectedIndex, setSelectedIndex] = useStateWithDeps<number | null>(
|
||||
defaultSelectedIndex ?? null,
|
||||
[defaultSelectedIndex],
|
||||
);
|
||||
const [menuStyles, setMenuStyles] = useState<CSSProperties>({});
|
||||
const [filter, setFilter] = useState<string>('');
|
||||
const [containerWidth, setContainerWidth] = useState<number | null>(null);
|
||||
@@ -223,7 +238,7 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle'>, MenuPro
|
||||
onClose();
|
||||
setSelectedIndex(null);
|
||||
setFilter('');
|
||||
}, [onClose]);
|
||||
}, [onClose, setSelectedIndex]);
|
||||
|
||||
// Close menu on space bar
|
||||
const handleMenuKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
@@ -265,7 +280,7 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle'>, MenuPro
|
||||
}
|
||||
return nextIndex;
|
||||
});
|
||||
}, [items]);
|
||||
}, [items, setSelectedIndex]);
|
||||
|
||||
const handleNext = useCallback(() => {
|
||||
setSelectedIndex((currIndex) => {
|
||||
@@ -282,7 +297,7 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle'>, MenuPro
|
||||
}
|
||||
return nextIndex;
|
||||
});
|
||||
}, [items]);
|
||||
}, [items, setSelectedIndex]);
|
||||
|
||||
useKey(
|
||||
'ArrowUp',
|
||||
@@ -312,11 +327,11 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle'>, MenuPro
|
||||
handleClose();
|
||||
}
|
||||
setSelectedIndex(null);
|
||||
if (i.type !== 'separator') {
|
||||
i.onSelect?.();
|
||||
if (i.type !== 'separator' && typeof i.onSelect === 'function') {
|
||||
i.onSelect();
|
||||
}
|
||||
},
|
||||
[handleClose],
|
||||
[handleClose, setSelectedIndex],
|
||||
);
|
||||
|
||||
useImperativeHandle(
|
||||
@@ -377,7 +392,7 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle'>, MenuPro
|
||||
const index = filteredItems.findIndex((item) => item === i) ?? null;
|
||||
setSelectedIndex(index);
|
||||
},
|
||||
[filteredItems],
|
||||
[filteredItems, setSelectedIndex],
|
||||
);
|
||||
|
||||
if (items.length === 0) return null;
|
||||
@@ -506,7 +521,7 @@ function MenuItem({ className, focused, onFocus, item, onSelect, ...props }: Men
|
||||
return (
|
||||
<Button
|
||||
ref={initRef}
|
||||
size="xs"
|
||||
size="sm"
|
||||
tabIndex={-1}
|
||||
onMouseEnter={(e) => e.currentTarget.focus()}
|
||||
onMouseLeave={(e) => e.currentTarget.blur()}
|
||||
@@ -518,6 +533,7 @@ function MenuItem({ className, focused, onFocus, item, onSelect, ...props }: Men
|
||||
rightSlot={rightSlot && <div className="ml-auto pl-3">{rightSlot}</div>}
|
||||
className={classNames(
|
||||
className,
|
||||
'h-xs', // More compact
|
||||
'min-w-[8rem] outline-none px-2 mx-1.5 flex text-sm text-gray-700 whitespace-nowrap',
|
||||
'focus:bg-highlight focus:text-gray-800 rounded',
|
||||
item.variant === 'danger' && 'text-red-600',
|
||||
|
||||
@@ -17,9 +17,12 @@ export interface GenericCompletionConfig {
|
||||
options: GenericCompletionOption[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Complete options, always matching until the start of the line
|
||||
*/
|
||||
export function genericCompletion({ options, minMatch = 1 }: GenericCompletionConfig) {
|
||||
return function completions(context: CompletionContext) {
|
||||
const toMatch = context.matchBefore(/\w*/);
|
||||
const toMatch = context.matchBefore(/.*/);
|
||||
|
||||
// Only match if we're at the start of the line
|
||||
if (toMatch === null || toMatch.from > 0) return null;
|
||||
|
||||
@@ -21,20 +21,12 @@ export function twig(
|
||||
const completions = twigCompletion({ options: variables });
|
||||
|
||||
const language = mixLanguage(base);
|
||||
const completion = language.data.of({ autocomplete: completions });
|
||||
const completionBase = base.language.data.of({ autocomplete: completions });
|
||||
const additionalCompletion = autocomplete
|
||||
? [language.data.of({ autocomplete: genericCompletion(autocomplete) })]
|
||||
? [base.language.data.of({ autocomplete: genericCompletion(autocomplete) })]
|
||||
: [];
|
||||
|
||||
return [
|
||||
language,
|
||||
completion,
|
||||
completionBase,
|
||||
base.support,
|
||||
placeholders(variables),
|
||||
...additionalCompletion,
|
||||
];
|
||||
return [language, completionBase, base.support, placeholders(variables), ...additionalCompletion];
|
||||
}
|
||||
|
||||
function mixLanguage(base: LanguageSupport): LRLanguage {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { LanguageSupport, LRLanguage } from '@codemirror/language';
|
||||
import { completions } from './completion';
|
||||
import { parser } from './url';
|
||||
|
||||
const urlLanguage = LRLanguage.define({
|
||||
@@ -7,8 +6,6 @@ const urlLanguage = LRLanguage.define({
|
||||
languageData: {},
|
||||
});
|
||||
|
||||
const completion = urlLanguage.data.of({ autocomplete: completions });
|
||||
|
||||
export function url() {
|
||||
return new LanguageSupport(urlLanguage, [completion]);
|
||||
return new LanguageSupport(urlLanguage, []);
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ const icons = {
|
||||
cake: lucide.CakeIcon,
|
||||
chat: lucide.MessageSquare,
|
||||
check: lucide.CheckIcon,
|
||||
checkCircle: lucide.CheckCircleIcon,
|
||||
chevronDown: lucide.ChevronDownIcon,
|
||||
chevronRight: lucide.ChevronRightIcon,
|
||||
code: lucide.CodeIcon,
|
||||
@@ -41,6 +42,7 @@ const icons = {
|
||||
magicWand: lucide.Wand2Icon,
|
||||
minus: lucide.MinusIcon,
|
||||
moreVertical: lucide.MoreVerticalIcon,
|
||||
paste: lucide.ClipboardPasteIcon,
|
||||
pencil: lucide.PencilIcon,
|
||||
plug: lucide.Plug,
|
||||
plus: lucide.PlusIcon,
|
||||
|
||||
@@ -2,7 +2,7 @@ import classNames from 'classnames';
|
||||
import type { EditorView } from 'codemirror';
|
||||
import type { HTMLAttributes, ReactNode } from 'react';
|
||||
import { forwardRef, useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { useStateSyncDefault } from '../../hooks/useStateSyncDefault';
|
||||
import { useStateWithDeps } from '../../hooks/useStateWithDeps';
|
||||
import type { EditorProps } from './Editor';
|
||||
import { Editor } from './Editor';
|
||||
import { IconButton } from './IconButton';
|
||||
@@ -72,7 +72,7 @@ export const Input = forwardRef<EditorView | undefined, InputProps>(function Inp
|
||||
}: InputProps,
|
||||
ref,
|
||||
) {
|
||||
const [obscured, setObscured] = useStateSyncDefault(type === 'password');
|
||||
const [obscured, setObscured] = useStateWithDeps(type === 'password', [type]);
|
||||
const [currentValue, setCurrentValue] = useState(defaultValue ?? '');
|
||||
const [focused, setFocused] = useState(false);
|
||||
|
||||
|
||||
@@ -392,11 +392,15 @@ function PairEditorRow({
|
||||
className="font-mono text-xs"
|
||||
onClick={async (e) => {
|
||||
e.preventDefault();
|
||||
const file = await open({
|
||||
const selected = await open({
|
||||
title: 'Select file',
|
||||
multiple: false,
|
||||
});
|
||||
handleChangeValueFile((Array.isArray(file) ? file[0] : file) ?? '');
|
||||
if (selected == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
handleChangeValueFile(selected.path);
|
||||
}}
|
||||
>
|
||||
{getFileName(pairContainer.pair.value) || 'Select File'}
|
||||
@@ -491,7 +495,8 @@ const newPairContainer = (initialPair?: Pair): PairContainer => {
|
||||
return { id, pair };
|
||||
};
|
||||
|
||||
const getFileName = (path: string): string => {
|
||||
const getFileName = (path: string | null | undefined): string => {
|
||||
if (typeof path !== 'string') return '';
|
||||
const parts = path.split(/[\\/]/);
|
||||
return parts[parts.length - 1] ?? '';
|
||||
};
|
||||
|
||||
@@ -1,23 +1,39 @@
|
||||
import classNames from 'classnames';
|
||||
import { motion } from 'framer-motion';
|
||||
import type { ReactNode } from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import React from 'react';
|
||||
import { useKey } from 'react-use';
|
||||
import { Heading } from './Heading';
|
||||
import { IconButton } from './IconButton';
|
||||
import type { IconProps } from './Icon';
|
||||
import { Icon } from './Icon';
|
||||
|
||||
export interface ToastProps {
|
||||
children: ReactNode;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
title?: ReactNode;
|
||||
className?: string;
|
||||
timeout: number;
|
||||
timeout: number | null;
|
||||
action?: ReactNode;
|
||||
variant?: 'copied' | 'success' | 'info' | 'warning' | 'error';
|
||||
}
|
||||
|
||||
export function Toast({ children, className, open, onClose, title, timeout }: ToastProps) {
|
||||
const titleId = useMemo(() => Math.random().toString(36).slice(2), []);
|
||||
const ICONS: Record<NonNullable<ToastProps['variant']>, IconProps['icon']> = {
|
||||
copied: 'copyCheck',
|
||||
warning: 'alert',
|
||||
error: 'alert',
|
||||
info: 'info',
|
||||
success: 'checkCircle',
|
||||
};
|
||||
|
||||
export function Toast({
|
||||
children,
|
||||
className,
|
||||
open,
|
||||
onClose,
|
||||
timeout,
|
||||
action,
|
||||
variant,
|
||||
}: ToastProps) {
|
||||
useKey(
|
||||
'Escape',
|
||||
() => {
|
||||
@@ -46,14 +62,22 @@ export function Toast({ children, className, open, onClose, title, timeout }: To
|
||||
'text-gray-700',
|
||||
)}
|
||||
>
|
||||
<div className="px-3 py-2">
|
||||
{title && (
|
||||
<Heading size={3} id={titleId}>
|
||||
{title}
|
||||
</Heading>
|
||||
<div className="px-3 py-2 flex items-center gap-2">
|
||||
{variant != null && (
|
||||
<Icon
|
||||
icon={ICONS[variant]}
|
||||
className={classNames(
|
||||
variant === 'success' && 'text-green-500',
|
||||
variant === 'warning' && 'text-orange-500',
|
||||
variant === 'error' && 'text-red-500',
|
||||
variant === 'copied' && 'text-violet-500',
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2">{children}</div>
|
||||
<div className="flex flex-col gap-1 w-full">
|
||||
<div>{children}</div>
|
||||
{action}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<IconButton
|
||||
@@ -63,14 +87,17 @@ export function Toast({ children, className, open, onClose, title, timeout }: To
|
||||
icon="x"
|
||||
onClick={onClose}
|
||||
/>
|
||||
<div className="w-full absolute bottom-0 left-0 right-0">
|
||||
<motion.div
|
||||
className="bg-highlight h-0.5"
|
||||
initial={{ width: '100%' }}
|
||||
animate={{ width: '0%', opacity: 0.2 }}
|
||||
transition={{ duration: timeout / 1000, ease: 'linear' }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{timeout != null && (
|
||||
<div className="w-full absolute bottom-0 left-0 right-0">
|
||||
<motion.div
|
||||
className="bg-highlight h-0.5"
|
||||
initial={{ width: '100%' }}
|
||||
animate={{ width: '0%', opacity: 0.2 }}
|
||||
transition={{ duration: timeout / 1000, ease: 'linear' }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,25 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { readText } from '@tauri-apps/plugin-clipboard-manager';
|
||||
import { readText, writeText } from '@tauri-apps/plugin-clipboard-manager';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
import { useWindowFocus } from './useWindowFocus';
|
||||
import { createGlobalState } from 'react-use';
|
||||
|
||||
const useClipboardTextState = createGlobalState<string>('');
|
||||
|
||||
export function useClipboardText() {
|
||||
return useQuery({
|
||||
queryKey: [],
|
||||
queryFn: () => readText(),
|
||||
}).data;
|
||||
const [value, setValue] = useClipboardTextState();
|
||||
const focused = useWindowFocus();
|
||||
|
||||
useEffect(() => {
|
||||
readText().then(setValue);
|
||||
}, [focused, setValue]);
|
||||
|
||||
const setText = useCallback(
|
||||
(text: string) => {
|
||||
writeText(text).catch(console.error);
|
||||
setValue(text);
|
||||
},
|
||||
[setValue],
|
||||
);
|
||||
|
||||
return [value, setText] as const;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { writeText } from '@tauri-apps/plugin-clipboard-manager';
|
||||
import React, { useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useToast } from '../components/ToastContext';
|
||||
import { Icon } from '../components/core/Icon';
|
||||
|
||||
export function useCopyAsCurl(requestId: string) {
|
||||
const [checked, setChecked] = useState<boolean>(false);
|
||||
@@ -15,12 +14,8 @@ export function useCopyAsCurl(requestId: string) {
|
||||
setChecked(true);
|
||||
setTimeout(() => setChecked(false), 800);
|
||||
toast.show({
|
||||
render: () => [
|
||||
<>
|
||||
<Icon icon="copyCheck" />
|
||||
<span>Command copied to clipboard</span>
|
||||
</>,
|
||||
],
|
||||
variant: 'copied',
|
||||
message: 'Curl copied to clipboard',
|
||||
});
|
||||
return cmd;
|
||||
},
|
||||
|
||||
@@ -13,13 +13,7 @@ export function useCreateHttpRequest() {
|
||||
const activeRequest = useActiveRequest();
|
||||
const routes = useAppRoutes();
|
||||
|
||||
return useMutation<
|
||||
HttpRequest,
|
||||
unknown,
|
||||
Partial<
|
||||
Pick<HttpRequest, 'name' | 'sortPriority' | 'folderId' | 'bodyType' | 'method' | 'headers'>
|
||||
>
|
||||
>({
|
||||
return useMutation<HttpRequest, unknown, Partial<HttpRequest>>({
|
||||
mutationFn: (patch) => {
|
||||
if (workspaceId === null) {
|
||||
throw new Error("Cannot create request when there's no active workspace");
|
||||
@@ -34,7 +28,8 @@ export function useCreateHttpRequest() {
|
||||
}
|
||||
}
|
||||
patch.folderId = patch.folderId || activeRequest?.folderId;
|
||||
return invoke('cmd_create_http_request', { workspaceId, name: '', ...patch });
|
||||
console.log('PATCH', patch);
|
||||
return invoke('cmd_create_http_request', { request: { workspaceId, ...patch } });
|
||||
},
|
||||
onSettled: () => trackEvent('http_request', 'create'),
|
||||
onSuccess: async (request) => {
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
|
||||
import { useRequestUpdateKey } from './useRequestUpdateKey';
|
||||
import { useUpdateAnyHttpRequest } from './useUpdateAnyHttpRequest';
|
||||
|
||||
export function useCurlToRequest() {
|
||||
const workspaceId = useActiveWorkspaceId();
|
||||
const updateRequest = useUpdateAnyHttpRequest();
|
||||
const { wasUpdatedExternally } = useRequestUpdateKey(null);
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({ requestId, command }: { requestId: string; command: string }) => {
|
||||
const request: Record<string, unknown> = await invoke('cmd_curl_to_request', {
|
||||
command,
|
||||
workspaceId,
|
||||
});
|
||||
delete request.id;
|
||||
await updateRequest.mutateAsync({ id: requestId, update: request });
|
||||
wasUpdatedExternally(requestId);
|
||||
console.log('FOO', request);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -4,12 +4,14 @@ import { ExportDataDialog } from '../components/ExportDataDialog';
|
||||
import { useActiveWorkspace } from './useActiveWorkspace';
|
||||
import { useAlert } from './useAlert';
|
||||
import { useWorkspaces } from './useWorkspaces';
|
||||
import { useToast } from '../components/ToastContext';
|
||||
|
||||
export function useExportData() {
|
||||
const workspaces = useWorkspaces();
|
||||
const activeWorkspace = useActiveWorkspace();
|
||||
const alert = useAlert();
|
||||
const dialog = useDialog();
|
||||
const toast = useToast();
|
||||
|
||||
return useMutation({
|
||||
onError: (err: string) => {
|
||||
@@ -28,6 +30,12 @@ export function useExportData() {
|
||||
onHide={hide}
|
||||
workspaces={workspaces}
|
||||
activeWorkspace={activeWorkspace}
|
||||
onSuccess={() => {
|
||||
toast.show({
|
||||
variant: 'success',
|
||||
message: 'Data export successful',
|
||||
});
|
||||
}}
|
||||
/>
|
||||
),
|
||||
});
|
||||
|
||||
46
src-web/hooks/useImportCurl.ts
Normal file
46
src-web/hooks/useImportCurl.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
|
||||
import { useRequestUpdateKey } from './useRequestUpdateKey';
|
||||
import { useUpdateAnyHttpRequest } from './useUpdateAnyHttpRequest';
|
||||
import { useToast } from '../components/ToastContext';
|
||||
import { useCreateHttpRequest } from './useCreateHttpRequest';
|
||||
import { useClipboardText } from './useClipboardText';
|
||||
|
||||
export function useImportCurl({ clearClipboard }: { clearClipboard?: boolean } = {}) {
|
||||
const workspaceId = useActiveWorkspaceId();
|
||||
const updateRequest = useUpdateAnyHttpRequest();
|
||||
const createRequest = useCreateHttpRequest();
|
||||
const { wasUpdatedExternally } = useRequestUpdateKey(null);
|
||||
const toast = useToast();
|
||||
const [, setClipboardText] = useClipboardText();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({ requestId, command }: { requestId: string | null; command: string }) => {
|
||||
const request: Record<string, unknown> = await invoke('cmd_curl_to_request', {
|
||||
command,
|
||||
workspaceId,
|
||||
});
|
||||
delete request.id;
|
||||
|
||||
let verb;
|
||||
if (requestId == null) {
|
||||
verb = 'Created';
|
||||
await createRequest.mutateAsync(request);
|
||||
} else {
|
||||
verb = 'Updated';
|
||||
await updateRequest.mutateAsync({ id: requestId, update: request });
|
||||
setTimeout(() => wasUpdatedExternally(requestId), 100);
|
||||
}
|
||||
|
||||
toast.show({
|
||||
variant: 'success',
|
||||
message: `${verb} request from Curl`,
|
||||
});
|
||||
|
||||
if (clearClipboard) {
|
||||
setClipboardText('');
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
46
src-web/hooks/useNotificationToast.tsx
Normal file
46
src-web/hooks/useNotificationToast.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
import { useToast } from '../components/ToastContext';
|
||||
import { useListenToTauriEvent } from './useListenToTauriEvent';
|
||||
import { Button } from '../components/core/Button';
|
||||
import { open } from '@tauri-apps/plugin-shell';
|
||||
import { invoke } from '@tauri-apps/api/core';
|
||||
|
||||
export function useNotificationToast() {
|
||||
const toast = useToast();
|
||||
|
||||
const markRead = (id: string) => {
|
||||
invoke('cmd_dismiss_notification', { notificationId: id }).catch(console.error);
|
||||
};
|
||||
|
||||
useListenToTauriEvent<{
|
||||
id: string;
|
||||
timestamp: string;
|
||||
message: string;
|
||||
action?: null | {
|
||||
url: string;
|
||||
label: string;
|
||||
};
|
||||
}>('notification', ({ payload }) => {
|
||||
const actionUrl = payload.action?.url;
|
||||
const actionLabel = payload.action?.label;
|
||||
toast.show({
|
||||
id: payload.id,
|
||||
timeout: null,
|
||||
message: payload.message,
|
||||
onClose: () => markRead(payload.id),
|
||||
action:
|
||||
actionLabel && actionUrl ? (
|
||||
<Button
|
||||
size="xs"
|
||||
color="gray"
|
||||
className="mr-auto min-w-[5rem]"
|
||||
onClick={() => {
|
||||
toast.hide(payload.id);
|
||||
return open(actionUrl);
|
||||
}}
|
||||
>
|
||||
{actionLabel}
|
||||
</Button>
|
||||
) : null,
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,12 +1,14 @@
|
||||
import type { DependencyList } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
/**
|
||||
* Like useState, except it will update the value when the default value changes
|
||||
*/
|
||||
export function useStateSyncDefault<T>(defaultValue: T) {
|
||||
export function useStateWithDeps<T>(defaultValue: T, deps: DependencyList) {
|
||||
const [value, setValue] = useState(defaultValue);
|
||||
useEffect(() => {
|
||||
setValue(defaultValue);
|
||||
}, [defaultValue]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, deps);
|
||||
return [value, setValue] as const;
|
||||
}
|
||||
Reference in New Issue
Block a user