mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-02-16 20:57:49 +01:00
Compare commits
37 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aeda72f13e | ||
|
|
83aa9041cb | ||
|
|
d51913509d | ||
|
|
5106f28ba5 | ||
|
|
0c55c6eaab | ||
|
|
b0edbd19c8 | ||
|
|
7630db79b7 | ||
|
|
55a7b82567 | ||
|
|
b5cb46918a | ||
|
|
a793ece1a5 | ||
|
|
0f6e4b641a | ||
|
|
5ac5fab0c6 | ||
|
|
8030a8a235 | ||
|
|
d98426cad3 | ||
|
|
06034a8fc4 | ||
|
|
1ee9f9bb51 | ||
|
|
4b99d1405e | ||
|
|
8480e52195 | ||
|
|
243e65a992 | ||
|
|
b82304a233 | ||
|
|
f7a4ea9735 | ||
|
|
33d1a84ecd | ||
|
|
f4a071ee05 | ||
|
|
e26ba0f9d0 | ||
|
|
b4e2a12375 | ||
|
|
5e7aacd31a | ||
|
|
00718df49e | ||
|
|
bb9025ab07 | ||
|
|
867f3908ed | ||
|
|
30e1ecac39 | ||
|
|
7eb2abe9b2 | ||
|
|
a5ac8fa035 | ||
|
|
dd705de155 | ||
|
|
b15cdec701 | ||
|
|
a99a36b5cc | ||
|
|
e0b0e3d781 | ||
|
|
98a4834d4f |
@@ -12,7 +12,7 @@ module.exports = {
|
||||
parserOptions: {
|
||||
project: ["./tsconfig.json"]
|
||||
},
|
||||
ignorePatterns: ["src-tauri/**/*"],
|
||||
ignorePatterns: ["src-tauri/**/*", "plugins/**/*"],
|
||||
settings: {
|
||||
react: {
|
||||
version: "detect"
|
||||
|
||||
2
.github/workflows/artifacts.yml
vendored
2
.github/workflows/artifacts.yml
vendored
@@ -3,6 +3,8 @@ on:
|
||||
push:
|
||||
tags: [ v* ]
|
||||
|
||||
permissions: write-all
|
||||
|
||||
jobs:
|
||||
build-artifacts:
|
||||
strategy:
|
||||
|
||||
8
Makefile
8
Makefile
@@ -1,7 +1,13 @@
|
||||
.PHONY: sqlx-prepare, dev
|
||||
.PHONY: sqlx-prepare, dev, migrate, build
|
||||
|
||||
sqlx-prepare:
|
||||
cd src-tauri && cargo sqlx prepare --database-url 'sqlite://db.sqlite'
|
||||
|
||||
dev:
|
||||
npm run tauri-dev
|
||||
|
||||
migrate:
|
||||
cd src-tauri && cargo sqlx migrate run --database-url 'sqlite://db.sqlite?mode=rw'
|
||||
|
||||
build:
|
||||
./node_modules/.bin/tauri build
|
||||
|
||||
Binary file not shown.
68
src-tauri/Cargo.lock
generated
68
src-tauri/Cargo.lock
generated
@@ -133,6 +133,17 @@ dependencies = [
|
||||
"critical-section",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atty"
|
||||
version = "0.2.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
|
||||
dependencies = [
|
||||
"hermit-abi 0.1.19",
|
||||
"libc",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.1.0"
|
||||
@@ -509,6 +520,30 @@ dependencies = [
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "3.2.25"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123"
|
||||
dependencies = [
|
||||
"atty",
|
||||
"bitflags 1.3.2",
|
||||
"clap_lex",
|
||||
"indexmap 1.9.3",
|
||||
"strsim",
|
||||
"termcolor",
|
||||
"textwrap",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5"
|
||||
dependencies = [
|
||||
"os_str_bytes",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cobs"
|
||||
version = "0.2.3"
|
||||
@@ -1577,6 +1612,15 @@ dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.1.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hermit-abi"
|
||||
version = "0.3.3"
|
||||
@@ -2392,7 +2436,7 @@ version = "1.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
|
||||
dependencies = [
|
||||
"hermit-abi",
|
||||
"hermit-abi 0.3.3",
|
||||
"libc",
|
||||
]
|
||||
|
||||
@@ -2561,6 +2605,12 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "os_str_bytes"
|
||||
version = "6.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1"
|
||||
|
||||
[[package]]
|
||||
name = "overload"
|
||||
version = "0.1.1"
|
||||
@@ -4012,6 +4062,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"base64 0.21.5",
|
||||
"bytes",
|
||||
"clap",
|
||||
"cocoa 0.24.1",
|
||||
"dirs-next",
|
||||
"embed_plist",
|
||||
@@ -4238,6 +4289,21 @@ dependencies = [
|
||||
"utf-8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "termcolor"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6093bad37da69aab9d123a8091e4be0aa4a03e4d601ec641c327398315f62b64"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "textwrap"
|
||||
version = "0.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d"
|
||||
|
||||
[[package]]
|
||||
name = "thin-slice"
|
||||
version = "0.1.1"
|
||||
|
||||
@@ -29,7 +29,19 @@ reqwest = { version = "0.11.14", features = ["json"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = { version = "1.0", features = ["raw_value"] }
|
||||
sqlx = { version = "0.6.2", features = ["sqlite", "runtime-tokio-rustls", "json", "chrono", "time", "offline"] }
|
||||
tauri = { version = "1.3", features = ["config-toml", "devtools", "fs-read-file", "os-all", "protocol-asset", "shell-open", "system-tray", "updater", "window-start-dragging"] }
|
||||
tauri = { version = "1.3", features = [
|
||||
"cli",
|
||||
"config-toml",
|
||||
"devtools",
|
||||
"fs-read-file",
|
||||
"os-all",
|
||||
"protocol-asset",
|
||||
"shell-open",
|
||||
"system-tray",
|
||||
"updater",
|
||||
"window-start-dragging",
|
||||
"dialog-open",
|
||||
] }
|
||||
tauri-plugin-window-state = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
|
||||
tokio = { version = "1.25.0", features = ["sync"] }
|
||||
uuid = "1.3.0"
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE workspaces ADD COLUMN variables DEFAULT '[]' NOT NULL;
|
||||
19
src-tauri/migrations/20231103142807_folders.sql
Normal file
19
src-tauri/migrations/20231103142807_folders.sql
Normal file
@@ -0,0 +1,19 @@
|
||||
CREATE TABLE folders
|
||||
(
|
||||
id TEXT NOT NULL
|
||||
PRIMARY KEY,
|
||||
model TEXT DEFAULT 'folder' NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
deleted_at DATETIME,
|
||||
workspace_id TEXT NOT NULL
|
||||
REFERENCES workspaces
|
||||
ON DELETE CASCADE,
|
||||
folder_id TEXT NULL
|
||||
REFERENCES folders
|
||||
ON DELETE CASCADE,
|
||||
name TEXT NOT NULL,
|
||||
sort_priority REAL DEFAULT 0 NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE http_requests ADD COLUMN folder_id TEXT REFERENCES folders(id) ON DELETE CASCADE;
|
||||
4
src-tauri/plugins/hello-world/greet.js
Normal file
4
src-tauri/plugins/hello-world/greet.js
Normal file
@@ -0,0 +1,4 @@
|
||||
export function greet() {
|
||||
// Call Rust-provided fn!
|
||||
sayHello('Plugin');
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export function hello() {
|
||||
sayHello('Plugin');
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { hello } from './hello.js';
|
||||
import { greet } from './greet.js';
|
||||
|
||||
export function entrypoint() {
|
||||
hello();
|
||||
export function hello() {
|
||||
greet();
|
||||
console.log('Try JSON parse', JSON.parse(`{ "hello": 123 }`).hello);
|
||||
console.log('Try RegExp', '123'.match(/[\d]+/));
|
||||
}
|
||||
|
||||
156
src-tauri/plugins/insomnia-importer/out/index.js
Normal file
156
src-tauri/plugins/insomnia-importer/out/index.js
Normal file
@@ -0,0 +1,156 @@
|
||||
function O(e, t) {
|
||||
return (
|
||||
console.log('IMPORTING Environment', e._id, e.name, JSON.stringify(e, null, 2)),
|
||||
{
|
||||
id: e._id,
|
||||
createdAt: new Date(e.created ?? Date.now()).toISOString().replace('Z', ''),
|
||||
updatedAt: new Date(e.updated ?? Date.now()).toISOString().replace('Z', ''),
|
||||
workspaceId: t,
|
||||
model: 'environment',
|
||||
name: e.name,
|
||||
variables: Object.entries(e.data).map(([i, s]) => ({
|
||||
enabled: !0,
|
||||
name: i,
|
||||
value: `${s}`,
|
||||
})),
|
||||
}
|
||||
);
|
||||
}
|
||||
function g(e) {
|
||||
return d(e) && e._type === 'workspace';
|
||||
}
|
||||
function y(e) {
|
||||
return d(e) && e._type === 'request_group';
|
||||
}
|
||||
function _(e) {
|
||||
return d(e) && e._type === 'request';
|
||||
}
|
||||
function I(e) {
|
||||
return d(e) && e._type === 'environment';
|
||||
}
|
||||
function d(e) {
|
||||
return Object.prototype.toString.call(e) === '[object Object]';
|
||||
}
|
||||
function h(e) {
|
||||
return Object.prototype.toString.call(e) === '[object String]';
|
||||
}
|
||||
function N(e) {
|
||||
return Object.entries(e).map(([t, i]) => ({
|
||||
enabled: !0,
|
||||
name: t,
|
||||
value: `${i}`,
|
||||
}));
|
||||
}
|
||||
function c(e) {
|
||||
return h(e) ? e.replaceAll(/{{\s*(_\.)?([^}]+)\s*}}/g, '${[$2]}') : e;
|
||||
}
|
||||
function D(e, t, i = 0) {
|
||||
var a, u;
|
||||
console.log('IMPORTING REQUEST', e._id, e.name, JSON.stringify(e, null, 2));
|
||||
let s = null,
|
||||
n = null;
|
||||
((a = e.body) == null ? void 0 : a.mimeType) === 'application/graphql'
|
||||
? ((s = 'graphql'), (n = c(e.body.text)))
|
||||
: ((u = e.body) == null ? void 0 : u.mimeType) === 'application/json' &&
|
||||
((s = 'application/json'), (n = c(e.body.text)));
|
||||
let p = null,
|
||||
o = {};
|
||||
return (
|
||||
e.authentication.type === 'bearer'
|
||||
? ((p = 'bearer'),
|
||||
(o = {
|
||||
token: c(e.authentication.token),
|
||||
}))
|
||||
: e.authentication.type === 'basic' &&
|
||||
((p = 'basic'),
|
||||
(o = {
|
||||
username: c(e.authentication.username),
|
||||
password: c(e.authentication.password),
|
||||
})),
|
||||
{
|
||||
id: e._id,
|
||||
createdAt: new Date(e.created ?? Date.now()).toISOString().replace('Z', ''),
|
||||
updatedAt: new Date(e.updated ?? Date.now()).toISOString().replace('Z', ''),
|
||||
workspaceId: t,
|
||||
folderId: e.parentId === t ? null : e.parentId,
|
||||
model: 'http_request',
|
||||
sortPriority: i,
|
||||
name: e.name,
|
||||
url: c(e.url),
|
||||
body: n,
|
||||
bodyType: s,
|
||||
authentication: o,
|
||||
authenticationType: p,
|
||||
method: e.method,
|
||||
headers: (e.headers ?? []).map(({ name: m, value: r, disabled: f }) => ({
|
||||
enabled: !f,
|
||||
name: m,
|
||||
value: r,
|
||||
})),
|
||||
}
|
||||
);
|
||||
}
|
||||
function w(e, t) {
|
||||
return (
|
||||
console.log('IMPORTING Workspace', e._id, e.name, JSON.stringify(e, null, 2)),
|
||||
{
|
||||
id: e._id,
|
||||
createdAt: new Date(e.created ?? Date.now()).toISOString().replace('Z', ''),
|
||||
updatedAt: new Date(e.updated ?? Date.now()).toISOString().replace('Z', ''),
|
||||
model: 'workspace',
|
||||
name: e.name,
|
||||
variables: t,
|
||||
}
|
||||
);
|
||||
}
|
||||
function b(e, t) {
|
||||
return (
|
||||
console.log('IMPORTING FOLDER', e._id, e.name, JSON.stringify(e, null, 2)),
|
||||
{
|
||||
id: e._id,
|
||||
createdAt: new Date(e.created ?? Date.now()).toISOString().replace('Z', ''),
|
||||
updatedAt: new Date(e.updated ?? Date.now()).toISOString().replace('Z', ''),
|
||||
folderId: e.parentId === t ? null : e.parentId,
|
||||
workspaceId: t,
|
||||
model: 'folder',
|
||||
name: e.name,
|
||||
}
|
||||
);
|
||||
}
|
||||
function T(e) {
|
||||
const t = JSON.parse(e);
|
||||
if (!d(t)) return;
|
||||
const { _type: i, __export_format: s } = t;
|
||||
if (i !== 'export' || s !== 4 || !Array.isArray(t.resources)) return;
|
||||
const n = {
|
||||
workspaces: [],
|
||||
requests: [],
|
||||
environments: [],
|
||||
folders: [],
|
||||
},
|
||||
p = t.resources.filter(g);
|
||||
for (const o of p) {
|
||||
console.log('IMPORTING WORKSPACE', o.name);
|
||||
const a = t.resources.find((r) => I(r) && r.parentId === o._id);
|
||||
console.log('FOUND BASE ENV', a.name),
|
||||
n.workspaces.push(w(o, a ? N(a.data) : [])),
|
||||
console.log('IMPORTING ENVIRONMENTS', a.name);
|
||||
const u = t.resources.filter((r) => I(r) && r.parentId === (a == null ? void 0 : a._id));
|
||||
console.log('FOUND', u.length, 'ENVIRONMENTS'),
|
||||
n.environments.push(...u.map((r) => O(r, o._id)));
|
||||
const m = (r) => {
|
||||
const f = t.resources.filter((l) => l.parentId === r);
|
||||
let S = 0;
|
||||
for (const l of f)
|
||||
y(l) ? (n.folders.push(b(l, o._id)), m(l._id)) : _(l) && n.requests.push(D(l, o._id, S++));
|
||||
};
|
||||
m(o._id);
|
||||
}
|
||||
return (
|
||||
(n.requests = n.requests.filter(Boolean)),
|
||||
(n.environments = n.environments.filter(Boolean)),
|
||||
(n.workspaces = n.workspaces.filter(Boolean)),
|
||||
n
|
||||
);
|
||||
}
|
||||
export { T as pluginHookImport };
|
||||
23
src-tauri/plugins/insomnia-importer/src/helpers/types.js
Normal file
23
src-tauri/plugins/insomnia-importer/src/helpers/types.js
Normal file
@@ -0,0 +1,23 @@
|
||||
export function isWorkspace(obj) {
|
||||
return isJSObject(obj) && obj._type === 'workspace';
|
||||
}
|
||||
|
||||
export function isRequestGroup(obj) {
|
||||
return isJSObject(obj) && obj._type === 'request_group';
|
||||
}
|
||||
|
||||
export function isRequest(obj) {
|
||||
return isJSObject(obj) && obj._type === 'request';
|
||||
}
|
||||
|
||||
export function isEnvironment(obj) {
|
||||
return isJSObject(obj) && obj._type === 'environment';
|
||||
}
|
||||
|
||||
export function isJSObject(obj) {
|
||||
return Object.prototype.toString.call(obj) === '[object Object]';
|
||||
}
|
||||
|
||||
export function isJSString(obj) {
|
||||
return Object.prototype.toString.call(obj) === '[object String]';
|
||||
}
|
||||
18
src-tauri/plugins/insomnia-importer/src/helpers/variables.js
Normal file
18
src-tauri/plugins/insomnia-importer/src/helpers/variables.js
Normal file
@@ -0,0 +1,18 @@
|
||||
import { isJSString } from './types.js';
|
||||
|
||||
export function parseVariables(data) {
|
||||
return Object.entries(data).map(([name, value]) => ({
|
||||
enabled: true,
|
||||
name,
|
||||
value: `${value}`,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Insomnia syntax to Yaak syntax
|
||||
* @param {string} variable - Text to convert
|
||||
*/
|
||||
export function convertSyntax(variable) {
|
||||
if (!isJSString(variable)) return variable;
|
||||
return variable.replaceAll(/{{\s*(_\.)?([^}]+)\s*}}/g, '${[$2]}');
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/**
|
||||
* Import an Insomnia environment object.
|
||||
* @param {Object} e - The environment object to import.
|
||||
* @param workspaceId - Workspace to import into.
|
||||
*/
|
||||
export function importEnvironment(e, workspaceId) {
|
||||
console.log('IMPORTING Environment', e._id, e.name, JSON.stringify(e, null, 2));
|
||||
return {
|
||||
id: e._id,
|
||||
createdAt: new Date(e.created ?? Date.now()).toISOString().replace('Z', ''),
|
||||
updatedAt: new Date(e.updated ?? Date.now()).toISOString().replace('Z', ''),
|
||||
workspaceId,
|
||||
model: 'environment',
|
||||
name: e.name,
|
||||
variables: Object.entries(e.data).map(([name, value]) => ({
|
||||
enabled: true,
|
||||
name,
|
||||
value: `${value}`,
|
||||
})),
|
||||
};
|
||||
}
|
||||
17
src-tauri/plugins/insomnia-importer/src/importers/folder.js
Normal file
17
src-tauri/plugins/insomnia-importer/src/importers/folder.js
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Import an Insomnia folder object.
|
||||
* @param {Object} f - The environment object to import.
|
||||
* @param workspaceId - Workspace to import into.
|
||||
*/
|
||||
export function importFolder(f, workspaceId) {
|
||||
console.log('IMPORTING FOLDER', f._id, f.name, JSON.stringify(f, null, 2));
|
||||
return {
|
||||
id: f._id,
|
||||
createdAt: new Date(f.created ?? Date.now()).toISOString().replace('Z', ''),
|
||||
updatedAt: new Date(f.updated ?? Date.now()).toISOString().replace('Z', ''),
|
||||
folderId: f.parentId === workspaceId ? null : f.parentId,
|
||||
workspaceId,
|
||||
model: 'folder',
|
||||
name: f.name,
|
||||
};
|
||||
}
|
||||
58
src-tauri/plugins/insomnia-importer/src/importers/request.js
Normal file
58
src-tauri/plugins/insomnia-importer/src/importers/request.js
Normal file
@@ -0,0 +1,58 @@
|
||||
import { convertSyntax } from '../helpers/variables.js';
|
||||
|
||||
/**
|
||||
* Import an Insomnia request object.
|
||||
* @param {Object} r - The request object to import.
|
||||
* @param workspaceId - The workspace ID to use for the request.
|
||||
* @param {number} sortPriority - The sort priority to use for the request.
|
||||
*/
|
||||
export function importRequest(r, workspaceId, sortPriority = 0) {
|
||||
console.log('IMPORTING REQUEST', r._id, r.name, JSON.stringify(r, null, 2));
|
||||
|
||||
let bodyType = null;
|
||||
let body = null;
|
||||
if (r.body?.mimeType === 'application/graphql') {
|
||||
bodyType = 'graphql';
|
||||
body = convertSyntax(r.body.text);
|
||||
} else if (r.body?.mimeType === 'application/json') {
|
||||
bodyType = 'application/json';
|
||||
body = convertSyntax(r.body.text);
|
||||
}
|
||||
|
||||
let authenticationType = null;
|
||||
let authentication = {};
|
||||
if (r.authentication.type === 'bearer') {
|
||||
authenticationType = 'bearer';
|
||||
authentication = {
|
||||
token: convertSyntax(r.authentication.token),
|
||||
};
|
||||
} else if (r.authentication.type === 'basic') {
|
||||
authenticationType = 'basic';
|
||||
authentication = {
|
||||
username: convertSyntax(r.authentication.username),
|
||||
password: convertSyntax(r.authentication.password),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
id: r._id,
|
||||
createdAt: new Date(r.created ?? Date.now()).toISOString().replace('Z', ''),
|
||||
updatedAt: new Date(r.updated ?? Date.now()).toISOString().replace('Z', ''),
|
||||
workspaceId,
|
||||
folderId: r.parentId === workspaceId ? null : r.parentId,
|
||||
model: 'http_request',
|
||||
sortPriority,
|
||||
name: r.name,
|
||||
url: convertSyntax(r.url),
|
||||
body,
|
||||
bodyType,
|
||||
authentication,
|
||||
authenticationType,
|
||||
method: r.method,
|
||||
headers: (r.headers ?? []).map(({ name, value, disabled }) => ({
|
||||
enabled: !disabled,
|
||||
name,
|
||||
value,
|
||||
})),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Import an Insomnia workspace object.
|
||||
* @param {Object} w - The workspace object to import.
|
||||
*/
|
||||
export function importWorkspace(w, variables) {
|
||||
console.log('IMPORTING Workspace', w._id, w.name, JSON.stringify(w, null, 2));
|
||||
return {
|
||||
id: w._id,
|
||||
createdAt: new Date(w.created ?? Date.now()).toISOString().replace('Z', ''),
|
||||
updatedAt: new Date(w.updated ?? Date.now()).toISOString().replace('Z', ''),
|
||||
model: 'workspace',
|
||||
name: w.name,
|
||||
variables,
|
||||
};
|
||||
}
|
||||
78
src-tauri/plugins/insomnia-importer/src/index.js
Normal file
78
src-tauri/plugins/insomnia-importer/src/index.js
Normal file
@@ -0,0 +1,78 @@
|
||||
import { importEnvironment } from './importers/environment.js';
|
||||
import { importRequest } from './importers/request.js';
|
||||
import { importWorkspace } from './importers/workspace.js';
|
||||
import {
|
||||
isEnvironment,
|
||||
isJSObject,
|
||||
isRequest,
|
||||
isRequestGroup,
|
||||
isWorkspace,
|
||||
} from './helpers/types.js';
|
||||
import { parseVariables } from './helpers/variables.js';
|
||||
import { importFolder } from './importers/folder.js';
|
||||
|
||||
export function pluginHookImport(contents) {
|
||||
const parsed = JSON.parse(contents);
|
||||
if (!isJSObject(parsed)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { _type, __export_format } = parsed;
|
||||
if (_type !== 'export' || __export_format !== 4 || !Array.isArray(parsed.resources)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const resources = {
|
||||
workspaces: [],
|
||||
requests: [],
|
||||
environments: [],
|
||||
folders: [],
|
||||
};
|
||||
|
||||
// Import workspaces
|
||||
const workspacesToImport = parsed.resources.filter(isWorkspace);
|
||||
for (const workspaceToImport of workspacesToImport) {
|
||||
console.log('IMPORTING WORKSPACE', workspaceToImport.name);
|
||||
const baseEnvironment = parsed.resources.find(
|
||||
(r) => isEnvironment(r) && r.parentId === workspaceToImport._id,
|
||||
);
|
||||
console.log('FOUND BASE ENV', baseEnvironment.name);
|
||||
resources.workspaces.push(
|
||||
importWorkspace(
|
||||
workspaceToImport,
|
||||
baseEnvironment ? parseVariables(baseEnvironment.data) : [],
|
||||
),
|
||||
);
|
||||
console.log('IMPORTING ENVIRONMENTS', baseEnvironment.name);
|
||||
const environmentsToImport = parsed.resources.filter(
|
||||
(r) => isEnvironment(r) && r.parentId === baseEnvironment?._id,
|
||||
);
|
||||
console.log('FOUND', environmentsToImport.length, 'ENVIRONMENTS');
|
||||
resources.environments.push(
|
||||
...environmentsToImport.map((r) => importEnvironment(r, workspaceToImport._id)),
|
||||
);
|
||||
|
||||
const nextFolder = (parentId) => {
|
||||
const children = parsed.resources.filter((r) => r.parentId === parentId);
|
||||
let sortPriority = 0;
|
||||
for (const child of children) {
|
||||
if (isRequestGroup(child)) {
|
||||
resources.folders.push(importFolder(child, workspaceToImport._id));
|
||||
nextFolder(child._id);
|
||||
} else if (isRequest(child)) {
|
||||
resources.requests.push(importRequest(child, workspaceToImport._id, sortPriority++));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Import folders
|
||||
nextFolder(workspaceToImport._id);
|
||||
}
|
||||
|
||||
// Filter out any `null` values
|
||||
resources.requests = resources.requests.filter(Boolean);
|
||||
resources.environments = resources.environments.filter(Boolean);
|
||||
resources.workspaces = resources.workspaces.filter(Boolean);
|
||||
|
||||
return resources;
|
||||
}
|
||||
13
src-tauri/plugins/insomnia-importer/vite.config.js
Normal file
13
src-tauri/plugins/insomnia-importer/vite.config.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import { resolve } from 'path';
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
lib: {
|
||||
entry: resolve(__dirname, 'src/index.js'),
|
||||
fileName: 'index',
|
||||
formats: ['es'],
|
||||
},
|
||||
outDir: resolve(__dirname, 'out'),
|
||||
},
|
||||
});
|
||||
@@ -1,5 +1,15 @@
|
||||
{
|
||||
"db": "SQLite",
|
||||
"02506ad41cc94cd937422ef1977a97174431f008a9fb4ce39667d587a858b876": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Right": 5
|
||||
}
|
||||
},
|
||||
"query": "\n INSERT INTO folders (\n id,\n workspace_id,\n folder_id,\n name,\n sort_priority\n )\n VALUES (?, ?, ?, ?, ?)\n ON CONFLICT (id) DO UPDATE SET\n updated_at = CURRENT_TIMESTAMP,\n name = excluded.name,\n folder_id = excluded.folder_id,\n sort_priority = excluded.sort_priority\n "
|
||||
},
|
||||
"06aaf8f4a17566f1d25da2a60f0baf4b5fc28c3cf0c001a84e25edf9eab3c7e3": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
@@ -58,6 +68,126 @@
|
||||
},
|
||||
"query": "\n DELETE FROM http_responses\n WHERE id = ?\n "
|
||||
},
|
||||
"1428d25b6aa3d6ec55742a968571fa951da0406d7bb32408883c584eae7dd53c": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "model",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "workspace_id",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "created_at",
|
||||
"ordinal": 3,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "updated_at",
|
||||
"ordinal": 4,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "folder_id",
|
||||
"ordinal": 5,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"ordinal": 6,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "sort_priority",
|
||||
"ordinal": 7,
|
||||
"type_info": "Float"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
}
|
||||
},
|
||||
"query": "\n SELECT\n id,\n model,\n workspace_id,\n created_at,\n updated_at,\n folder_id,\n name,\n sort_priority\n FROM folders\n WHERE id = ?\n "
|
||||
},
|
||||
"1517b0f86c841b5f1247bd40c3a9b38ab001d846a410b6e3cd36f9e844d50ddb": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "model",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "workspace_id",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "created_at",
|
||||
"ordinal": 3,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "updated_at",
|
||||
"ordinal": 4,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "folder_id",
|
||||
"ordinal": 5,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"ordinal": 6,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "sort_priority",
|
||||
"ordinal": 7,
|
||||
"type_info": "Float"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
}
|
||||
},
|
||||
"query": "\n SELECT\n id,\n model,\n workspace_id,\n created_at,\n updated_at,\n folder_id,\n name,\n sort_priority\n FROM folders\n WHERE workspace_id = ?\n "
|
||||
},
|
||||
"26072725d536c3cfdffd9a681d17c0ee2f246ca98e0459630a2430236d3bbdd2": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
@@ -160,16 +290,6 @@
|
||||
},
|
||||
"query": "\n SELECT id, model, workspace_id, request_id, updated_at, created_at, url,\n status, status_reason, content_length, body, body_path, elapsed, error,\n headers AS \"headers!: sqlx::types::Json<Vec<HttpResponseHeader>>\"\n FROM http_responses\n WHERE workspace_id = ?\n ORDER BY created_at DESC\n "
|
||||
},
|
||||
"3ec4710d28a7f38608c96798d971217ac97788bcb639089d0c5750c0d339bc9a": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Right": 3
|
||||
}
|
||||
},
|
||||
"query": "\n UPDATE environments\n SET (name, variables, updated_at) = (?, ?, CURRENT_TIMESTAMP)\n WHERE id = ?;\n "
|
||||
},
|
||||
"448a1d1f1866ab42c0f81fcf8eb2930bf21dfdd43ca4831bc1a198cf45ac3732": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
@@ -180,6 +300,60 @@
|
||||
},
|
||||
"query": "\n DELETE FROM http_requests\n WHERE id = ?\n "
|
||||
},
|
||||
"5588db23df7f30dc75857e05395ebbcf2384e2ac0d7cb87f76d74c6d50781d7b": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "model",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "created_at",
|
||||
"ordinal": 2,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "updated_at",
|
||||
"ordinal": 3,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"ordinal": 4,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "description",
|
||||
"ordinal": 5,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "variables!: sqlx::types::Json<Vec<EnvironmentVariable>>",
|
||||
"ordinal": 6,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 0
|
||||
}
|
||||
},
|
||||
"query": "\n SELECT id, model, created_at, updated_at, name, description,\n variables AS \"variables!: sqlx::types::Json<Vec<EnvironmentVariable>>\"\n FROM workspaces\n "
|
||||
},
|
||||
"5aa070e61995f8b1724efaa94c5f0cef5a4be6efda5d70354ad449d7d4b5aee4": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
@@ -282,6 +456,16 @@
|
||||
},
|
||||
"query": "\n SELECT id, model, workspace_id, request_id, updated_at, created_at, url,\n status, status_reason, content_length, body, body_path, elapsed, error,\n headers AS \"headers!: sqlx::types::Json<Vec<HttpResponseHeader>>\"\n FROM http_responses\n WHERE request_id = ?\n ORDER BY created_at DESC\n "
|
||||
},
|
||||
"610223ad10b6e25926d486ba775a74b55625fcc4e6637d8a805d44ec3f3b9532": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Right": 4
|
||||
}
|
||||
},
|
||||
"query": "\n INSERT INTO workspaces (id, name, description, variables)\n VALUES (?, ?, ?, ?)\n ON CONFLICT (id) DO UPDATE SET\n updated_at = CURRENT_TIMESTAMP,\n name = excluded.name,\n description = excluded.description,\n variables = excluded.variables\n "
|
||||
},
|
||||
"62475fd9483fb5eda01c937949da2ef66ac7005b4be06b87aa6210d462348aca": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
@@ -346,7 +530,17 @@
|
||||
},
|
||||
"query": "\n SELECT\n id,\n model,\n workspace_id,\n created_at,\n updated_at,\n name,\n variables AS \"variables!: sqlx::types::Json<Vec<EnvironmentVariable>>\"\n FROM environments\n WHERE id = ?\n "
|
||||
},
|
||||
"6f0cb5a6d1e8dbc8cdfcc3c7e7944b2c83c22cb795b9d6b98fe067dabec9680b": {
|
||||
"84be2b954870ab181738656ecd4d03fca2ff21012947014c79626abfce8e999b": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
}
|
||||
},
|
||||
"query": "\n DELETE FROM workspaces\n WHERE id = ?\n "
|
||||
},
|
||||
"854536c80af3f86bb9a63b8ce059ad724374b545cb23481bb3b2ce07d7414220": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -365,58 +559,63 @@
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "created_at",
|
||||
"name": "folder_id",
|
||||
"ordinal": 3,
|
||||
"type_info": "Datetime"
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "updated_at",
|
||||
"name": "created_at",
|
||||
"ordinal": 4,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"name": "updated_at",
|
||||
"ordinal": 5,
|
||||
"type_info": "Text"
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "url",
|
||||
"name": "name",
|
||||
"ordinal": 6,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "method",
|
||||
"name": "url",
|
||||
"ordinal": 7,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "body",
|
||||
"name": "method",
|
||||
"ordinal": 8,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "body_type",
|
||||
"name": "body",
|
||||
"ordinal": 9,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "authentication!: Json<HashMap<String, JsonValue>>",
|
||||
"name": "body_type",
|
||||
"ordinal": 10,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "authentication_type",
|
||||
"name": "authentication!: Json<HashMap<String, JsonValue>>",
|
||||
"ordinal": 11,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "sort_priority",
|
||||
"name": "authentication_type",
|
||||
"ordinal": 12,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "sort_priority",
|
||||
"ordinal": 13,
|
||||
"type_info": "Float"
|
||||
},
|
||||
{
|
||||
"name": "headers!: sqlx::types::Json<Vec<HttpRequestHeader>>",
|
||||
"ordinal": 13,
|
||||
"ordinal": 14,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
@@ -424,6 +623,7 @@
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
@@ -440,27 +640,7 @@
|
||||
"Right": 1
|
||||
}
|
||||
},
|
||||
"query": "\n SELECT\n id,\n model,\n workspace_id,\n created_at,\n updated_at,\n name,\n url,\n method,\n body,\n body_type,\n authentication AS \"authentication!: Json<HashMap<String, JsonValue>>\",\n authentication_type,\n sort_priority,\n headers AS \"headers!: sqlx::types::Json<Vec<HttpRequestHeader>>\"\n FROM http_requests\n WHERE workspace_id = ?\n "
|
||||
},
|
||||
"84be2b954870ab181738656ecd4d03fca2ff21012947014c79626abfce8e999b": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
}
|
||||
},
|
||||
"query": "\n DELETE FROM workspaces\n WHERE id = ?\n "
|
||||
},
|
||||
"86e32d6a6fadf35436f19b577a659c203a8d143cb3a8d6122951c5bf54a0888d": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Right": 4
|
||||
}
|
||||
},
|
||||
"query": "\n INSERT INTO environments (id, workspace_id, name, variables)\n VALUES (?, ?, ?, ?)\n "
|
||||
"query": "\n SELECT\n id,\n model,\n workspace_id,\n folder_id,\n created_at,\n updated_at,\n name,\n url,\n method,\n body,\n body_type,\n authentication AS \"authentication!: Json<HashMap<String, JsonValue>>\",\n authentication_type,\n sort_priority,\n headers AS \"headers!: sqlx::types::Json<Vec<HttpRequestHeader>>\"\n FROM http_requests\n WHERE id = ?\n "
|
||||
},
|
||||
"8947a2a90478277c42fe9b06bc1fa98197642a4d281a3dbc101be2c9c1fec36c": {
|
||||
"describe": {
|
||||
@@ -472,6 +652,128 @@
|
||||
},
|
||||
"query": "\n INSERT INTO http_responses (\n id,\n request_id,\n workspace_id,\n elapsed,\n url,\n status,\n status_reason,\n content_length,\n body,\n body_path,\n headers\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);\n "
|
||||
},
|
||||
"93aea3881dffb70a82325263740a0bb6477e78f27991ce7456b394e84383acb6": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
}
|
||||
},
|
||||
"query": "\n DELETE FROM folders\n WHERE id = ?\n "
|
||||
},
|
||||
"a558e182f40286fe52bed5f03b2dc367b7229ab6bd9cda0a7ce219a438ccd5fd": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "model",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "workspace_id",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "folder_id",
|
||||
"ordinal": 3,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "created_at",
|
||||
"ordinal": 4,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "updated_at",
|
||||
"ordinal": 5,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"ordinal": 6,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "url",
|
||||
"ordinal": 7,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "method",
|
||||
"ordinal": 8,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "body",
|
||||
"ordinal": 9,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "body_type",
|
||||
"ordinal": 10,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "authentication!: Json<HashMap<String, JsonValue>>",
|
||||
"ordinal": 11,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "authentication_type",
|
||||
"ordinal": 12,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "sort_priority",
|
||||
"ordinal": 13,
|
||||
"type_info": "Float"
|
||||
},
|
||||
{
|
||||
"name": "headers!: sqlx::types::Json<Vec<HttpRequestHeader>>",
|
||||
"ordinal": 14,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
}
|
||||
},
|
||||
"query": "\n SELECT\n id,\n model,\n workspace_id,\n folder_id,\n created_at,\n updated_at,\n name,\n url,\n method,\n body,\n body_type,\n authentication AS \"authentication!: Json<HashMap<String, JsonValue>>\",\n authentication_type,\n sort_priority,\n headers AS \"headers!: sqlx::types::Json<Vec<HttpRequestHeader>>\"\n FROM http_requests\n WHERE workspace_id = ?\n "
|
||||
},
|
||||
"ac1b4ffbd98b67f0a1a74e3525387d679dd6f44c561d55c7bbea747053e53671": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Right": 0
|
||||
}
|
||||
},
|
||||
"query": "\n UPDATE http_responses\n SET (elapsed, status_reason) = (-1, 'Cancelled')\n WHERE elapsed = 0;\n "
|
||||
},
|
||||
"aeb0712785a9964d516dc8939bc54aa8206ad852e608b362d014b67a0f21b0ed": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
@@ -482,16 +784,6 @@
|
||||
},
|
||||
"query": "\n DELETE FROM environments\n WHERE id = ?\n "
|
||||
},
|
||||
"b19c275180909a39342b13c3cdcf993781636913ae590967f5508c46a56dc961": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Right": 11
|
||||
}
|
||||
},
|
||||
"query": "\n INSERT INTO http_requests (\n id,\n workspace_id,\n name,\n url,\n method,\n body,\n body_type,\n authentication,\n authentication_type,\n headers,\n sort_priority\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT (id) DO UPDATE SET\n updated_at = CURRENT_TIMESTAMP,\n name = excluded.name,\n method = excluded.method,\n headers = excluded.headers,\n body = excluded.body,\n body_type = excluded.body_type,\n authentication = excluded.authentication,\n authentication_type = excluded.authentication_type,\n url = excluded.url,\n sort_priority = excluded.sort_priority\n "
|
||||
},
|
||||
"ba2b34a77723f24f86e4c3c45274dbfec6ca130e16e592f948844c037bdc0593": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
@@ -648,198 +940,6 @@
|
||||
},
|
||||
"query": "\n SELECT id, model, workspace_id, request_id, updated_at, created_at, url,\n status, status_reason, content_length, body, body_path, elapsed, error,\n headers AS \"headers!: sqlx::types::Json<Vec<HttpResponseHeader>>\"\n FROM http_responses\n WHERE id = ?\n "
|
||||
},
|
||||
"caf3f21bf291dfbd36446592066e96c1f83abe96f6ea9211a3e049eb9c58a8c8": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "model",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "created_at",
|
||||
"ordinal": 2,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "updated_at",
|
||||
"ordinal": 3,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"ordinal": 4,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "description",
|
||||
"ordinal": 5,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
}
|
||||
},
|
||||
"query": "\n SELECT id, model, created_at, updated_at, name, description\n FROM workspaces WHERE id = ?\n "
|
||||
},
|
||||
"cea4cae52f16ec78aca9a47b17117422d4f165e5a3b308c70fd1a180382475ea": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "model",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "created_at",
|
||||
"ordinal": 2,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "updated_at",
|
||||
"ordinal": 3,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"ordinal": 4,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "description",
|
||||
"ordinal": 5,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 0
|
||||
}
|
||||
},
|
||||
"query": "\n SELECT id, model, created_at, updated_at, name, description\n FROM workspaces\n "
|
||||
},
|
||||
"ced098adb79c0ee64e223b6e02371ef253920a2c342275de0fa9c181529a4adc": {
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "model",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "workspace_id",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "created_at",
|
||||
"ordinal": 3,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "updated_at",
|
||||
"ordinal": 4,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"ordinal": 5,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "url",
|
||||
"ordinal": 6,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "method",
|
||||
"ordinal": 7,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "body",
|
||||
"ordinal": 8,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "body_type",
|
||||
"ordinal": 9,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "authentication!: Json<HashMap<String, JsonValue>>",
|
||||
"ordinal": 10,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "authentication_type",
|
||||
"ordinal": 11,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "sort_priority",
|
||||
"ordinal": 12,
|
||||
"type_info": "Float"
|
||||
},
|
||||
{
|
||||
"name": "headers!: sqlx::types::Json<Vec<HttpRequestHeader>>",
|
||||
"ordinal": 13,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
false
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
}
|
||||
},
|
||||
"query": "\n SELECT\n id,\n model,\n workspace_id,\n created_at,\n updated_at,\n name,\n url,\n method,\n body,\n body_type,\n authentication AS \"authentication!: Json<HashMap<String, JsonValue>>\",\n authentication_type,\n sort_priority,\n headers AS \"headers!: sqlx::types::Json<Vec<HttpRequestHeader>>\"\n FROM http_requests\n WHERE id = ?\n "
|
||||
},
|
||||
"d80c09497771e3641022e73ec6c6a87e73a551f88a948a5445d754922b82b50b": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
@@ -850,24 +950,78 @@
|
||||
},
|
||||
"query": "\n INSERT INTO key_values (namespace, key, value)\n VALUES (?, ?, ?) ON CONFLICT DO UPDATE SET\n updated_at = CURRENT_TIMESTAMP,\n value = excluded.value\n "
|
||||
},
|
||||
"e0f41023d877d94b7609ce910a71bd89c4827a558654b8ae14d85e6ba86990cf": {
|
||||
"dbe457087a7bccbca4c1d673aa8e547df04530a7f860a6ccd4e20126a7cdfa4f": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"columns": [
|
||||
{
|
||||
"name": "id",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "model",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "created_at",
|
||||
"ordinal": 2,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "updated_at",
|
||||
"ordinal": 3,
|
||||
"type_info": "Datetime"
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
"ordinal": 4,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "description",
|
||||
"ordinal": 5,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "variables!: sqlx::types::Json<Vec<EnvironmentVariable>>",
|
||||
"ordinal": 6,
|
||||
"type_info": "Null"
|
||||
}
|
||||
],
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
"Right": 1
|
||||
}
|
||||
},
|
||||
"query": "\n UPDATE workspaces SET (name, updated_at) =\n (?, CURRENT_TIMESTAMP) WHERE id = ?;\n "
|
||||
"query": "\n SELECT id, model, created_at, updated_at, name, description,\n variables AS \"variables!: sqlx::types::Json<Vec<EnvironmentVariable>>\"\n FROM workspaces WHERE id = ?\n "
|
||||
},
|
||||
"f116d8cf9aad828135bb8c3a4c8b8e6b857ae13303989e9133a33b2d1cf20e96": {
|
||||
"dcc2f405f8e29d0599d86bcde509187e9cc5fc647067eaa5c738cb24e2f081e5": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Right": 3
|
||||
"Right": 4
|
||||
}
|
||||
},
|
||||
"query": "\n INSERT INTO workspaces (id, name, description)\n VALUES (?, ?, ?)\n "
|
||||
"query": "\n INSERT INTO environments (\n id,\n workspace_id,\n name,\n variables\n )\n VALUES (?, ?, ?, ?)\n ON CONFLICT (id) DO UPDATE SET\n updated_at = CURRENT_TIMESTAMP,\n name = excluded.name,\n variables = excluded.variables\n "
|
||||
},
|
||||
"e5b410442b00ee354bb58eb0e8fb2af06e9dea4bb24763d717c72a840450a759": {
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"nullable": [],
|
||||
"parameters": {
|
||||
"Right": 12
|
||||
}
|
||||
},
|
||||
"query": "\n INSERT INTO http_requests (\n id,\n workspace_id,\n folder_id,\n name,\n url,\n method,\n body,\n body_type,\n authentication,\n authentication_type,\n headers,\n sort_priority\n )\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT (id) DO UPDATE SET\n updated_at = CURRENT_TIMESTAMP,\n name = excluded.name,\n folder_id = excluded.folder_id,\n method = excluded.method,\n headers = excluded.headers,\n body = excluded.body,\n body_type = excluded.body_type,\n authentication = excluded.authentication,\n authentication_type = excluded.authentication_type,\n url = excluded.url,\n sort_priority = excluded.sort_priority\n "
|
||||
}
|
||||
}
|
||||
118
src-tauri/src/analytics.rs
Normal file
118
src-tauri/src/analytics.rs
Normal file
@@ -0,0 +1,118 @@
|
||||
use sqlx::types::JsonValue;
|
||||
use tauri::{async_runtime, AppHandle, Manager};
|
||||
|
||||
use crate::is_dev;
|
||||
|
||||
pub enum AnalyticsResource {
|
||||
App,
|
||||
// Workspace,
|
||||
// Environment,
|
||||
// Folder,
|
||||
// HttpRequest,
|
||||
// HttpResponse,
|
||||
}
|
||||
|
||||
pub enum AnalyticsAction {
|
||||
Launch,
|
||||
// Create,
|
||||
// Update,
|
||||
// Upsert,
|
||||
// Delete,
|
||||
// Send,
|
||||
// Duplicate,
|
||||
}
|
||||
|
||||
fn resource_name(resource: AnalyticsResource) -> &'static str {
|
||||
match resource {
|
||||
AnalyticsResource::App => "app",
|
||||
// AnalyticsResource::Workspace => "workspace",
|
||||
// AnalyticsResource::Environment => "environment",
|
||||
// AnalyticsResource::Folder => "folder",
|
||||
// AnalyticsResource::HttpRequest => "http_request",
|
||||
// AnalyticsResource::HttpResponse => "http_response",
|
||||
}
|
||||
}
|
||||
|
||||
fn action_name(action: AnalyticsAction) -> &'static str {
|
||||
match action {
|
||||
AnalyticsAction::Launch => "launch",
|
||||
// AnalyticsAction::Create => "create",
|
||||
// AnalyticsAction::Update => "update",
|
||||
// AnalyticsAction::Upsert => "upsert",
|
||||
// AnalyticsAction::Delete => "delete",
|
||||
// AnalyticsAction::Send => "send",
|
||||
// AnalyticsAction::Duplicate => "duplicate",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn track_event(
|
||||
app_handle: &AppHandle,
|
||||
resource: AnalyticsResource,
|
||||
action: AnalyticsAction,
|
||||
attributes: Option<JsonValue>,
|
||||
) {
|
||||
async_runtime::block_on(async move {
|
||||
let event = format!("{}.{}", resource_name(resource), action_name(action));
|
||||
let attributes_json = attributes.unwrap_or("{}".to_string().into()).to_string();
|
||||
let info = app_handle.package_info();
|
||||
let params = vec![
|
||||
("e", event.clone()),
|
||||
("a", attributes_json.clone()),
|
||||
("id", "site_zOK0d7jeBy2TLxFCnZ".to_string()),
|
||||
("v", info.version.clone().to_string()),
|
||||
("os", get_os().to_string()),
|
||||
("xy", get_window_size(app_handle)),
|
||||
];
|
||||
let url = format!("https://t.yaak.app/t/e");
|
||||
let req = reqwest::Client::builder()
|
||||
.build()
|
||||
.unwrap()
|
||||
.get(&url)
|
||||
.query(¶ms);
|
||||
|
||||
if is_dev() {
|
||||
println!("Ignore dev analytics event: {} {:?}", event, params);
|
||||
} else {
|
||||
if let Err(e) = req.send().await {
|
||||
println!("Error sending analytics event: {}", e);
|
||||
} else {
|
||||
println!("Sent analytics event: {}", event);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn get_os() -> &'static str {
|
||||
if cfg!(target_os = "windows") {
|
||||
"windows"
|
||||
} else if cfg!(target_os = "macos") {
|
||||
"macos"
|
||||
} else if cfg!(target_os = "linux") {
|
||||
"linux"
|
||||
} else {
|
||||
"unknown"
|
||||
}
|
||||
}
|
||||
|
||||
fn get_window_size(app_handle: &AppHandle) -> String {
|
||||
let window = match app_handle.windows().into_values().next() {
|
||||
Some(w) => w,
|
||||
None => return "unknown".to_string(),
|
||||
};
|
||||
|
||||
let current_monitor = match window.current_monitor() {
|
||||
Ok(Some(m)) => m,
|
||||
_ => return "unknown".to_string(),
|
||||
};
|
||||
|
||||
let scale_factor = current_monitor.scale_factor();
|
||||
let size = current_monitor.size();
|
||||
let width: f64 = size.width as f64 / scale_factor;
|
||||
let height: f64 = size.height as f64 / scale_factor;
|
||||
|
||||
format!(
|
||||
"{}x{}",
|
||||
(width / 100.0).round() * 100.0,
|
||||
(height / 100.0).round() * 100.0
|
||||
)
|
||||
}
|
||||
@@ -7,6 +7,12 @@
|
||||
#[macro_use]
|
||||
extern crate objc;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::env::current_dir;
|
||||
use std::fs::{create_dir_all, File};
|
||||
use std::io::Write;
|
||||
use std::process::exit;
|
||||
|
||||
use base64::Engine;
|
||||
use http::header::{HeaderName, ACCEPT, USER_AGENT};
|
||||
use http::{HeaderMap, HeaderValue, Method};
|
||||
@@ -17,22 +23,23 @@ use sqlx::migrate::Migrator;
|
||||
use sqlx::sqlite::SqlitePoolOptions;
|
||||
use sqlx::types::Json;
|
||||
use sqlx::{Pool, Sqlite};
|
||||
use std::collections::HashMap;
|
||||
use std::env::current_dir;
|
||||
use std::fs::{create_dir_all, File};
|
||||
use std::io::Write;
|
||||
#[cfg(target_os = "macos")]
|
||||
use tauri::TitleBarStyle;
|
||||
use tauri::{AppHandle, Menu, MenuItem, RunEvent, State, Submenu, Window, WindowUrl, Wry};
|
||||
use tauri::{AppHandle, Menu, RunEvent, State, Submenu, Window, WindowUrl, Wry};
|
||||
use tauri::{CustomMenuItem, Manager, WindowEvent};
|
||||
use tauri_plugin_window_state::{StateFlags, WindowExt};
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
use window_ext::TrafficLightWindowExt;
|
||||
|
||||
use crate::analytics::{track_event, AnalyticsAction, AnalyticsResource};
|
||||
|
||||
mod analytics;
|
||||
mod models;
|
||||
mod plugin;
|
||||
mod render;
|
||||
mod window_ext;
|
||||
mod plugin;
|
||||
mod window_menu;
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
pub struct CustomResponse {
|
||||
@@ -64,19 +71,19 @@ async fn migrate_db(
|
||||
|
||||
#[tauri::command]
|
||||
async fn send_ephemeral_request(
|
||||
request: models::HttpRequest,
|
||||
mut request: models::HttpRequest,
|
||||
environment_id: Option<&str>,
|
||||
app_handle: AppHandle<Wry>,
|
||||
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
|
||||
) -> Result<models::HttpResponse, String> {
|
||||
let pool = &*db_instance.lock().await;
|
||||
let response = models::HttpResponse::default();
|
||||
let response = models::HttpResponse::new();
|
||||
let environment_id2 = environment_id.unwrap_or("n/a").to_string();
|
||||
return actually_send_ephemeral_request(request, &response, &environment_id2, &app_handle, pool)
|
||||
.await;
|
||||
request.id = "".to_string();
|
||||
return actually_send_request(request, &response, &environment_id2, &app_handle, pool).await;
|
||||
}
|
||||
|
||||
async fn actually_send_ephemeral_request(
|
||||
async fn actually_send_request(
|
||||
request: models::HttpRequest,
|
||||
response: &models::HttpResponse,
|
||||
environment_id: &str,
|
||||
@@ -86,8 +93,11 @@ async fn actually_send_ephemeral_request(
|
||||
let start = std::time::Instant::now();
|
||||
let environment = models::get_environment(environment_id, pool).await.ok();
|
||||
let environment_ref = environment.as_ref();
|
||||
let workspace = models::get_workspace(&request.workspace_id, pool)
|
||||
.await
|
||||
.expect("Failed to get Workspace");
|
||||
|
||||
let mut url_string = render::render(&request.url, environment.as_ref());
|
||||
let mut url_string = render::render(&request.url, &workspace, environment.as_ref());
|
||||
|
||||
if !url_string.starts_with("http://") && !url_string.starts_with("https://") {
|
||||
url_string = format!("http://{}", url_string);
|
||||
@@ -112,8 +122,8 @@ async fn actually_send_ephemeral_request(
|
||||
continue;
|
||||
}
|
||||
|
||||
let name = render::render(&h.name, environment_ref);
|
||||
let value = render::render(&h.value, environment_ref);
|
||||
let name = render::render(&h.name, &workspace, environment_ref);
|
||||
let value = render::render(&h.value, &workspace, environment_ref);
|
||||
|
||||
let header_name = match HeaderName::from_bytes(name.as_bytes()) {
|
||||
Ok(n) => n,
|
||||
@@ -148,8 +158,8 @@ async fn actually_send_ephemeral_request(
|
||||
.unwrap_or(empty_value)
|
||||
.as_str()
|
||||
.unwrap_or("");
|
||||
let username = render::render(raw_username, environment_ref);
|
||||
let password = render::render(raw_password, environment_ref);
|
||||
let username = render::render(raw_username, &workspace, environment_ref);
|
||||
let password = render::render(raw_password, &workspace, environment_ref);
|
||||
|
||||
let auth = format!("{username}:{password}");
|
||||
let encoded = base64::engine::general_purpose::STANDARD_NO_PAD.encode(auth);
|
||||
@@ -159,7 +169,7 @@ async fn actually_send_ephemeral_request(
|
||||
);
|
||||
} else if b == "bearer" {
|
||||
let raw_token = a.get("token").unwrap_or(empty_value).as_str().unwrap_or("");
|
||||
let token = render::render(raw_token, environment_ref);
|
||||
let token = render::render(raw_token, &workspace, environment_ref);
|
||||
headers.insert(
|
||||
"Authorization",
|
||||
HeaderValue::from_str(&format!("Bearer {token}")).unwrap(),
|
||||
@@ -173,7 +183,7 @@ async fn actually_send_ephemeral_request(
|
||||
|
||||
let sendable_req_result = match (request.body, request.body_type) {
|
||||
(Some(raw_body), Some(_)) => {
|
||||
let body = render::render(&raw_body, environment_ref);
|
||||
let body = render::render(&raw_body, &workspace, environment_ref);
|
||||
builder.body(body).build()
|
||||
}
|
||||
_ => builder.build(),
|
||||
@@ -248,6 +258,22 @@ async fn actually_send_ephemeral_request(
|
||||
Err(e) => response_err(response, e.to_string(), app_handle, pool).await,
|
||||
}
|
||||
}
|
||||
#[tauri::command]
|
||||
async fn import_data(
|
||||
window: Window<Wry>,
|
||||
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
|
||||
file_paths: Vec<&str>,
|
||||
) -> Result<plugin::ImportedResources, String> {
|
||||
let pool = &*db_instance.lock().await;
|
||||
let imported = plugin::run_plugin_import(
|
||||
&window.app_handle(),
|
||||
pool,
|
||||
"insomnia-importer",
|
||||
file_paths.first().unwrap(),
|
||||
)
|
||||
.await;
|
||||
Ok(imported)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn send_request(
|
||||
@@ -272,7 +298,7 @@ async fn send_request(
|
||||
let pool2 = pool.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
actually_send_ephemeral_request(req, &response2, &environment_id2, &app_handle2, &pool2)
|
||||
actually_send_request(req, &response2, &environment_id2, &app_handle2, &pool2)
|
||||
.await
|
||||
.expect("Failed to send request");
|
||||
});
|
||||
@@ -287,6 +313,7 @@ async fn response_err(
|
||||
pool: &Pool<Sqlite>,
|
||||
) -> Result<models::HttpResponse, String> {
|
||||
let mut response = response.clone();
|
||||
response.elapsed = -1;
|
||||
response.error = Some(error.clone());
|
||||
response = models::update_response_if_id(&response, pool)
|
||||
.await
|
||||
@@ -331,9 +358,15 @@ async fn create_workspace(
|
||||
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
|
||||
) -> Result<models::Workspace, String> {
|
||||
let pool = &*db_instance.lock().await;
|
||||
let created_workspace = models::create_workspace(name, "", pool)
|
||||
.await
|
||||
.expect("Failed to create workspace");
|
||||
let created_workspace = models::upsert_workspace(
|
||||
pool,
|
||||
models::Workspace {
|
||||
name: name.to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.expect("Failed to create Workspace");
|
||||
|
||||
emit_and_return(&window, "created_model", created_workspace)
|
||||
}
|
||||
@@ -347,9 +380,17 @@ async fn create_environment(
|
||||
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
|
||||
) -> Result<models::Environment, String> {
|
||||
let pool = &*db_instance.lock().await;
|
||||
let created_environment = models::create_environment(workspace_id, name, variables, pool)
|
||||
.await
|
||||
.expect("Failed to create environment");
|
||||
let created_environment = models::upsert_environment(
|
||||
pool,
|
||||
models::Environment {
|
||||
workspace_id: workspace_id.to_string(),
|
||||
name: name.to_string(),
|
||||
variables: Json(variables),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.expect("Failed to create environment");
|
||||
|
||||
emit_and_return(&window, "created_model", created_environment)
|
||||
}
|
||||
@@ -359,24 +400,21 @@ async fn create_request(
|
||||
workspace_id: &str,
|
||||
name: &str,
|
||||
sort_priority: f64,
|
||||
folder_id: Option<&str>,
|
||||
window: Window<Wry>,
|
||||
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
|
||||
) -> Result<models::HttpRequest, String> {
|
||||
let pool = &*db_instance.lock().await;
|
||||
let headers = Vec::new();
|
||||
let created_request = models::upsert_request(
|
||||
None,
|
||||
workspace_id,
|
||||
name,
|
||||
"GET",
|
||||
None,
|
||||
None,
|
||||
HashMap::new(),
|
||||
None,
|
||||
"",
|
||||
headers,
|
||||
sort_priority,
|
||||
pool,
|
||||
models::HttpRequest {
|
||||
workspace_id: workspace_id.to_string(),
|
||||
name: name.to_string(),
|
||||
method: "GET".to_string(),
|
||||
folder_id: folder_id.map(|s| s.to_string()),
|
||||
sort_priority,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.expect("Failed to create request");
|
||||
@@ -405,7 +443,7 @@ async fn update_workspace(
|
||||
) -> Result<models::Workspace, String> {
|
||||
let pool = &*db_instance.lock().await;
|
||||
|
||||
let updated_workspace = models::update_workspace(workspace, pool)
|
||||
let updated_workspace = models::upsert_workspace(pool, workspace)
|
||||
.await
|
||||
.expect("Failed to update request");
|
||||
|
||||
@@ -420,14 +458,9 @@ async fn update_environment(
|
||||
) -> Result<models::Environment, String> {
|
||||
let pool = &*db_instance.lock().await;
|
||||
|
||||
let updated_environment = models::update_environment(
|
||||
environment.id.as_str(),
|
||||
environment.name.as_str(),
|
||||
environment.variables.0,
|
||||
pool,
|
||||
)
|
||||
.await
|
||||
.expect("Failed to update request");
|
||||
let updated_environment = models::upsert_environment(pool, environment)
|
||||
.await
|
||||
.expect("Failed to update environment");
|
||||
|
||||
emit_and_return(&window, "updated_model", updated_environment)
|
||||
}
|
||||
@@ -439,35 +472,9 @@ async fn update_request(
|
||||
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
|
||||
) -> Result<models::HttpRequest, String> {
|
||||
let pool = &*db_instance.lock().await;
|
||||
|
||||
// TODO: Figure out how to make this better
|
||||
let b2;
|
||||
let body = match request.body {
|
||||
Some(b) => {
|
||||
b2 = b;
|
||||
Some(b2.as_str())
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
// TODO: Figure out how to make this better
|
||||
let updated_request = models::upsert_request(
|
||||
Some(request.id.as_str()),
|
||||
request.workspace_id.as_str(),
|
||||
request.name.as_str(),
|
||||
request.method.as_str(),
|
||||
body,
|
||||
request.body_type,
|
||||
request.authentication.0,
|
||||
request.authentication_type,
|
||||
request.url.as_str(),
|
||||
request.headers.0,
|
||||
request.sort_priority,
|
||||
pool,
|
||||
)
|
||||
.await
|
||||
.expect("Failed to update request");
|
||||
|
||||
let updated_request = models::upsert_request(pool, request)
|
||||
.await
|
||||
.expect("Failed to update request");
|
||||
emit_and_return(&window, "updated_model", updated_request)
|
||||
}
|
||||
|
||||
@@ -484,6 +491,69 @@ async fn delete_request(
|
||||
emit_and_return(&window, "deleted_model", req)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn list_folders(
|
||||
workspace_id: &str,
|
||||
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
|
||||
) -> Result<Vec<models::Folder>, String> {
|
||||
let pool = &*db_instance.lock().await;
|
||||
models::find_folders(workspace_id, pool)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn create_folder(
|
||||
workspace_id: &str,
|
||||
name: &str,
|
||||
sort_priority: f64,
|
||||
folder_id: Option<&str>,
|
||||
window: Window<Wry>,
|
||||
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
|
||||
) -> Result<models::Folder, String> {
|
||||
let pool = &*db_instance.lock().await;
|
||||
let created_request = models::upsert_folder(
|
||||
pool,
|
||||
models::Folder {
|
||||
workspace_id: workspace_id.to_string(),
|
||||
name: name.to_string(),
|
||||
folder_id: folder_id.map(|s| s.to_string()),
|
||||
sort_priority,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.expect("Failed to create folder");
|
||||
|
||||
emit_and_return(&window, "created_model", created_request)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn update_folder(
|
||||
folder: models::Folder,
|
||||
window: Window<Wry>,
|
||||
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
|
||||
) -> Result<models::Folder, String> {
|
||||
let pool = &*db_instance.lock().await;
|
||||
let updated_folder = models::upsert_folder(pool, folder)
|
||||
.await
|
||||
.expect("Failed to update request");
|
||||
emit_and_return(&window, "updated_model", updated_folder)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn delete_folder(
|
||||
window: Window<Wry>,
|
||||
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
|
||||
folder_id: &str,
|
||||
) -> Result<models::Folder, String> {
|
||||
let pool = &*db_instance.lock().await;
|
||||
let req = models::delete_folder(folder_id, pool)
|
||||
.await
|
||||
.expect("Failed to delete folder");
|
||||
emit_and_return(&window, "deleted_model", req)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn delete_environment(
|
||||
window: Window<Wry>,
|
||||
@@ -521,6 +591,17 @@ async fn list_environments(
|
||||
Ok(environments)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn get_folder(
|
||||
id: &str,
|
||||
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
|
||||
) -> Result<models::Folder, String> {
|
||||
let pool = &*db_instance.lock().await;
|
||||
models::get_folder(id, pool)
|
||||
.await
|
||||
.map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn get_request(
|
||||
id: &str,
|
||||
@@ -598,10 +679,15 @@ async fn list_workspaces(
|
||||
.await
|
||||
.expect("Failed to find workspaces");
|
||||
if workspaces.is_empty() {
|
||||
let workspace =
|
||||
models::create_workspace("My Project", "This is the default workspace", pool)
|
||||
.await
|
||||
.expect("Failed to create workspace");
|
||||
let workspace = models::upsert_workspace(
|
||||
pool,
|
||||
models::Workspace {
|
||||
name: "My Project".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await
|
||||
.expect("Failed to create Workspace");
|
||||
Ok(vec![workspace])
|
||||
} else {
|
||||
Ok(workspaces)
|
||||
@@ -623,7 +709,7 @@ async fn delete_workspace(
|
||||
let pool = &*db_instance.lock().await;
|
||||
let workspace = models::delete_workspace(workspace_id, pool)
|
||||
.await
|
||||
.expect("Failed to delete workspace");
|
||||
.expect("Failed to delete Workspace");
|
||||
emit_and_return(&window, "deleted_model", workspace)
|
||||
}
|
||||
|
||||
@@ -641,6 +727,7 @@ fn main() {
|
||||
let p_string = p.to_string_lossy().replace(' ', "%20");
|
||||
let url = format!("sqlite://{}?mode=rwc", p_string);
|
||||
println!("Connecting to database at {}", url);
|
||||
|
||||
tauri::async_runtime::block_on(async move {
|
||||
let pool = SqlitePoolOptions::new()
|
||||
.connect(url.as_str())
|
||||
@@ -648,30 +735,68 @@ fn main() {
|
||||
.expect("Failed to connect to database");
|
||||
|
||||
// Setup the DB handle
|
||||
let m = Mutex::new(pool);
|
||||
let m = Mutex::new(pool.clone());
|
||||
migrate_db(app.handle(), &m)
|
||||
.await
|
||||
.expect("Failed to migrate database");
|
||||
app.manage(m);
|
||||
|
||||
let _ = models::cancel_pending_responses(&pool).await;
|
||||
|
||||
// TODO: Move this somewhere better
|
||||
match app.get_cli_matches() {
|
||||
Ok(matches) => {
|
||||
let cmd = matches.subcommand.unwrap_or_default();
|
||||
if cmd.name == "import" {
|
||||
let arg_file = cmd
|
||||
.matches
|
||||
.args
|
||||
.get("file")
|
||||
.unwrap()
|
||||
.value
|
||||
.as_str()
|
||||
.unwrap();
|
||||
plugin::run_plugin_import(
|
||||
&app.handle(),
|
||||
&pool,
|
||||
"insomnia-importer",
|
||||
arg_file,
|
||||
)
|
||||
.await;
|
||||
exit(0);
|
||||
} else if cmd.name == "hello" {
|
||||
plugin::run_plugin_hello(&app.handle(), "hello-world");
|
||||
exit(0);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("Nothing found: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
create_environment,
|
||||
create_folder,
|
||||
create_request,
|
||||
create_workspace,
|
||||
delete_all_responses,
|
||||
delete_environment,
|
||||
delete_folder,
|
||||
delete_request,
|
||||
delete_response,
|
||||
delete_workspace,
|
||||
duplicate_request,
|
||||
get_key_value,
|
||||
get_environment,
|
||||
get_folder,
|
||||
get_request,
|
||||
get_workspace,
|
||||
import_data,
|
||||
list_environments,
|
||||
list_folders,
|
||||
list_requests,
|
||||
list_responses,
|
||||
list_workspaces,
|
||||
@@ -680,6 +805,7 @@ fn main() {
|
||||
send_request,
|
||||
set_key_value,
|
||||
update_environment,
|
||||
update_folder,
|
||||
update_request,
|
||||
update_workspace,
|
||||
])
|
||||
@@ -690,7 +816,13 @@ fn main() {
|
||||
let w = create_window(app_handle, None);
|
||||
w.restore_state(StateFlags::all())
|
||||
.expect("Failed to restore window state");
|
||||
plugin::test_plugins(&app_handle);
|
||||
|
||||
track_event(
|
||||
app_handle,
|
||||
AnalyticsResource::App,
|
||||
AnalyticsAction::Launch,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
// ExitRequested { api, .. } => {
|
||||
@@ -705,70 +837,31 @@ fn is_dev() -> bool {
|
||||
}
|
||||
|
||||
fn create_window(handle: &AppHandle<Wry>, url: Option<&str>) -> Window<Wry> {
|
||||
let default_menu = Menu::os_default("Yaak".to_string().as_str());
|
||||
let mut test_menu = Menu::new()
|
||||
.add_item(
|
||||
CustomMenuItem::new("send_request".to_string(), "Send Request")
|
||||
.accelerator("CmdOrCtrl+r"),
|
||||
)
|
||||
.add_item(
|
||||
CustomMenuItem::new("zoom_reset".to_string(), "Zoom to Actual Size")
|
||||
.accelerator("CmdOrCtrl+0"),
|
||||
)
|
||||
.add_item(
|
||||
CustomMenuItem::new("zoom_in".to_string(), "Zoom In").accelerator("CmdOrCtrl+Plus"),
|
||||
)
|
||||
.add_item(
|
||||
CustomMenuItem::new("zoom_out".to_string(), "Zoom Out").accelerator("CmdOrCtrl+-"),
|
||||
)
|
||||
.add_item(
|
||||
CustomMenuItem::new("toggle_sidebar".to_string(), "Toggle Sidebar")
|
||||
.accelerator("CmdOrCtrl+b"),
|
||||
)
|
||||
.add_item(
|
||||
CustomMenuItem::new("focus_url".to_string(), "Focus URL").accelerator("CmdOrCtrl+l"),
|
||||
)
|
||||
.add_item(
|
||||
CustomMenuItem::new("new_request".to_string(), "New Request")
|
||||
.accelerator("CmdOrCtrl+n"),
|
||||
)
|
||||
.add_item(
|
||||
CustomMenuItem::new("toggle_settings".to_string(), "Toggle Settings")
|
||||
.accelerator("CmdOrCtrl+,"),
|
||||
)
|
||||
.add_item(
|
||||
CustomMenuItem::new("duplicate_request".to_string(), "Duplicate Request")
|
||||
.accelerator("CmdOrCtrl+d"),
|
||||
)
|
||||
.add_item(
|
||||
CustomMenuItem::new("focus_sidebar".to_string(), "Focus Sidebar")
|
||||
.accelerator("CmdOrCtrl+1"),
|
||||
)
|
||||
.add_item(CustomMenuItem::new("new_window".to_string(), "New Window"));
|
||||
let mut app_menu = window_menu::os_default("Yaak".to_string().as_str());
|
||||
if is_dev() {
|
||||
test_menu = test_menu
|
||||
.add_native_item(MenuItem::Separator)
|
||||
.add_item(
|
||||
CustomMenuItem::new("refresh".to_string(), "Refresh")
|
||||
.accelerator("CmdOrCtrl + Shift + r"),
|
||||
)
|
||||
.add_item(
|
||||
CustomMenuItem::new("toggle_devtools".to_string(), "Open Devtools")
|
||||
.accelerator("CmdOrCtrl + Option + i"),
|
||||
);
|
||||
let submenu = Submenu::new(
|
||||
"Developer",
|
||||
Menu::new()
|
||||
.add_item(
|
||||
CustomMenuItem::new("refresh".to_string(), "Refresh")
|
||||
.accelerator("CmdOrCtrl + Shift + r"),
|
||||
)
|
||||
.add_item(
|
||||
CustomMenuItem::new("toggle_devtools".to_string(), "Open Devtools")
|
||||
.accelerator("CmdOrCtrl + Option + i"),
|
||||
),
|
||||
);
|
||||
app_menu = app_menu.add_submenu(submenu);
|
||||
}
|
||||
|
||||
let submenu = Submenu::new("Test Menu", test_menu);
|
||||
|
||||
let window_num = handle.windows().len();
|
||||
let window_id = format!("wnd_{}", window_num);
|
||||
let menu = default_menu.add_submenu(submenu);
|
||||
let mut win_builder = tauri::WindowBuilder::new(
|
||||
handle,
|
||||
window_id,
|
||||
WindowUrl::App(url.unwrap_or_default().into()),
|
||||
)
|
||||
.menu(menu)
|
||||
.menu(app_menu)
|
||||
.fullscreen(false)
|
||||
.resizable(true)
|
||||
.inner_size(1100.0, 600.0)
|
||||
@@ -795,7 +888,7 @@ fn create_window(handle: &AppHandle<Wry>, url: Option<&str>) -> Window<Wry> {
|
||||
let win2 = win.clone();
|
||||
let handle2 = handle.clone();
|
||||
win.on_menu_event(move |event| match event.menu_item_id() {
|
||||
"quit" => std::process::exit(0),
|
||||
"quit" => exit(0),
|
||||
"close" => win2.close().unwrap(),
|
||||
"zoom_reset" => win2.emit("zoom", 0).unwrap(),
|
||||
"zoom_in" => win2.emit("zoom", 1).unwrap(),
|
||||
|
||||
@@ -7,8 +7,8 @@ use sqlx::types::chrono::NaiveDateTime;
|
||||
use sqlx::types::{Json, JsonValue};
|
||||
use sqlx::{Pool, Sqlite};
|
||||
|
||||
#[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Default)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct Workspace {
|
||||
pub id: String,
|
||||
pub model: String,
|
||||
@@ -16,10 +16,11 @@ pub struct Workspace {
|
||||
pub updated_at: NaiveDateTime,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub variables: Json<Vec<EnvironmentVariable>>,
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Default)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct Environment {
|
||||
pub id: String,
|
||||
pub workspace_id: String,
|
||||
@@ -30,35 +31,45 @@ pub struct Environment {
|
||||
pub variables: Json<Vec<EnvironmentVariable>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
fn default_enabled() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct EnvironmentVariable {
|
||||
#[serde(default)]
|
||||
#[serde(default = "default_enabled")]
|
||||
pub enabled: bool,
|
||||
pub name: String,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct HttpRequestHeader {
|
||||
#[serde(default)]
|
||||
#[serde(default = "default_enabled")]
|
||||
pub enabled: bool,
|
||||
pub name: String,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
fn default_http_request_method() -> String {
|
||||
"GET".to_string()
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Default)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct HttpRequest {
|
||||
pub id: String,
|
||||
pub workspace_id: String,
|
||||
pub model: String,
|
||||
pub created_at: NaiveDateTime,
|
||||
pub updated_at: NaiveDateTime,
|
||||
pub id: String,
|
||||
pub workspace_id: String,
|
||||
pub folder_id: Option<String>,
|
||||
pub model: String,
|
||||
pub sort_priority: f64,
|
||||
pub name: String,
|
||||
pub url: String,
|
||||
#[serde(default = "default_http_request_method")]
|
||||
pub method: String,
|
||||
pub body: Option<String>,
|
||||
pub body_type: Option<String>,
|
||||
@@ -67,15 +78,28 @@ pub struct HttpRequest {
|
||||
pub headers: Json<Vec<HttpRequestHeader>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Default)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct Folder {
|
||||
pub created_at: NaiveDateTime,
|
||||
pub updated_at: NaiveDateTime,
|
||||
pub id: String,
|
||||
pub workspace_id: String,
|
||||
pub folder_id: Option<String>,
|
||||
pub model: String,
|
||||
pub name: String,
|
||||
pub sort_priority: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct HttpResponseHeader {
|
||||
pub name: String,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Default)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct HttpResponse {
|
||||
pub id: String,
|
||||
pub model: String,
|
||||
@@ -94,8 +118,17 @@ pub struct HttpResponse {
|
||||
pub headers: Json<Vec<HttpResponseHeader>>,
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
impl HttpResponse {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self {
|
||||
model: "http_response".to_string(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(sqlx::FromRow, Debug, Clone, Serialize, Deserialize, Default)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct KeyValue {
|
||||
pub model: String,
|
||||
pub created_at: NaiveDateTime,
|
||||
@@ -153,7 +186,8 @@ pub async fn find_workspaces(pool: &Pool<Sqlite>) -> Result<Vec<Workspace>, sqlx
|
||||
sqlx::query_as!(
|
||||
Workspace,
|
||||
r#"
|
||||
SELECT id, model, created_at, updated_at, name, description
|
||||
SELECT id, model, created_at, updated_at, name, description,
|
||||
variables AS "variables!: sqlx::types::Json<Vec<EnvironmentVariable>>"
|
||||
FROM workspaces
|
||||
"#,
|
||||
)
|
||||
@@ -165,7 +199,8 @@ pub async fn get_workspace(id: &str, pool: &Pool<Sqlite>) -> Result<Workspace, s
|
||||
sqlx::query_as!(
|
||||
Workspace,
|
||||
r#"
|
||||
SELECT id, model, created_at, updated_at, name, description
|
||||
SELECT id, model, created_at, updated_at, name, description,
|
||||
variables AS "variables!: sqlx::types::Json<Vec<EnvironmentVariable>>"
|
||||
FROM workspaces WHERE id = ?
|
||||
"#,
|
||||
id,
|
||||
@@ -193,27 +228,6 @@ pub async fn delete_workspace(id: &str, pool: &Pool<Sqlite>) -> Result<Workspace
|
||||
Ok(workspace)
|
||||
}
|
||||
|
||||
pub async fn create_workspace(
|
||||
name: &str,
|
||||
description: &str,
|
||||
pool: &Pool<Sqlite>,
|
||||
) -> Result<Workspace, sqlx::Error> {
|
||||
let id = generate_id(Some("wk"));
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO workspaces (id, name, description)
|
||||
VALUES (?, ?, ?)
|
||||
"#,
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
get_workspace(&id, pool).await
|
||||
}
|
||||
|
||||
pub async fn find_environments(
|
||||
workspace_id: &str,
|
||||
pool: &Pool<Sqlite>,
|
||||
@@ -232,30 +246,6 @@ pub async fn find_environments(
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn create_environment(
|
||||
workspace_id: &str,
|
||||
name: &str,
|
||||
variables: Vec<EnvironmentVariable>,
|
||||
pool: &Pool<Sqlite>,
|
||||
) -> Result<Environment, sqlx::Error> {
|
||||
let id = generate_id(Some("en"));
|
||||
let trimmed_name = name.trim();
|
||||
let variables_json = Json(variables);
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO environments (id, workspace_id, name, variables)
|
||||
VALUES (?, ?, ?, ?)
|
||||
"#,
|
||||
id,
|
||||
workspace_id,
|
||||
trimmed_name,
|
||||
variables_json,
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
get_environment(&id, pool).await
|
||||
}
|
||||
|
||||
pub async fn delete_environment(id: &str, pool: &Pool<Sqlite>) -> Result<Environment, sqlx::Error> {
|
||||
let env = get_environment(id, pool).await?;
|
||||
let _ = sqlx::query!(
|
||||
@@ -271,26 +261,37 @@ pub async fn delete_environment(id: &str, pool: &Pool<Sqlite>) -> Result<Environ
|
||||
Ok(env)
|
||||
}
|
||||
|
||||
pub async fn update_environment(
|
||||
id: &str,
|
||||
name: &str,
|
||||
variables: Vec<EnvironmentVariable>,
|
||||
pub async fn upsert_environment(
|
||||
pool: &Pool<Sqlite>,
|
||||
environment: Environment,
|
||||
) -> Result<Environment, sqlx::Error> {
|
||||
let variables_json = Json(variables);
|
||||
let id = match environment.id.as_str() {
|
||||
"" => generate_id(Some("ev")),
|
||||
_ => environment.id.to_string(),
|
||||
};
|
||||
let trimmed_name = environment.name.trim();
|
||||
sqlx::query!(
|
||||
r#"
|
||||
UPDATE environments
|
||||
SET (name, variables, updated_at) = (?, ?, CURRENT_TIMESTAMP)
|
||||
WHERE id = ?;
|
||||
INSERT INTO environments (
|
||||
id,
|
||||
workspace_id,
|
||||
name,
|
||||
variables
|
||||
)
|
||||
VALUES (?, ?, ?, ?)
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
updated_at = CURRENT_TIMESTAMP,
|
||||
name = excluded.name,
|
||||
variables = excluded.variables
|
||||
"#,
|
||||
name,
|
||||
variables_json,
|
||||
id,
|
||||
environment.workspace_id,
|
||||
trimmed_name,
|
||||
environment.variables,
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
get_environment(id, pool).await
|
||||
get_environment(&id, pool).await
|
||||
}
|
||||
|
||||
pub async fn get_environment(id: &str, pool: &Pool<Sqlite>) -> Result<Environment, sqlx::Error> {
|
||||
@@ -314,66 +315,127 @@ pub async fn get_environment(id: &str, pool: &Pool<Sqlite>) -> Result<Environmen
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn duplicate_request(id: &str, pool: &Pool<Sqlite>) -> Result<HttpRequest, sqlx::Error> {
|
||||
let existing = get_request(id, pool).await?;
|
||||
|
||||
// TODO: Figure out how to make this better
|
||||
let b2;
|
||||
let body = match existing.body {
|
||||
Some(b) => {
|
||||
b2 = b;
|
||||
Some(b2.as_str())
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
|
||||
upsert_request(
|
||||
None,
|
||||
existing.workspace_id.as_str(),
|
||||
existing.name.as_str(),
|
||||
existing.method.as_str(),
|
||||
body,
|
||||
existing.body_type,
|
||||
existing.authentication.0,
|
||||
existing.authentication_type,
|
||||
existing.url.as_str(),
|
||||
existing.headers.0,
|
||||
existing.sort_priority + 0.001,
|
||||
pool,
|
||||
pub async fn get_folder(id: &str, pool: &Pool<Sqlite>) -> Result<Folder, sqlx::Error> {
|
||||
sqlx::query_as!(
|
||||
Folder,
|
||||
r#"
|
||||
SELECT
|
||||
id,
|
||||
model,
|
||||
workspace_id,
|
||||
created_at,
|
||||
updated_at,
|
||||
folder_id,
|
||||
name,
|
||||
sort_priority
|
||||
FROM folders
|
||||
WHERE id = ?
|
||||
"#,
|
||||
id,
|
||||
)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn upsert_request(
|
||||
id: Option<&str>,
|
||||
pub async fn find_folders(
|
||||
workspace_id: &str,
|
||||
name: &str,
|
||||
method: &str,
|
||||
body: Option<&str>,
|
||||
body_type: Option<String>,
|
||||
authentication: HashMap<String, JsonValue>,
|
||||
authentication_type: Option<String>,
|
||||
url: &str,
|
||||
headers: Vec<HttpRequestHeader>,
|
||||
sort_priority: f64,
|
||||
pool: &Pool<Sqlite>,
|
||||
) -> Result<HttpRequest, sqlx::Error> {
|
||||
let generated_id;
|
||||
let id = match id {
|
||||
Some(v) => v,
|
||||
None => {
|
||||
generated_id = generate_id(Some("rq"));
|
||||
generated_id.as_str()
|
||||
}
|
||||
) -> Result<Vec<Folder>, sqlx::Error> {
|
||||
sqlx::query_as!(
|
||||
Folder,
|
||||
r#"
|
||||
SELECT
|
||||
id,
|
||||
model,
|
||||
workspace_id,
|
||||
created_at,
|
||||
updated_at,
|
||||
folder_id,
|
||||
name,
|
||||
sort_priority
|
||||
FROM folders
|
||||
WHERE workspace_id = ?
|
||||
"#,
|
||||
workspace_id,
|
||||
)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn delete_folder(id: &str, pool: &Pool<Sqlite>) -> Result<Folder, sqlx::Error> {
|
||||
let env = get_folder(id, pool).await?;
|
||||
let _ = sqlx::query!(
|
||||
r#"
|
||||
DELETE FROM folders
|
||||
WHERE id = ?
|
||||
"#,
|
||||
id,
|
||||
)
|
||||
.execute(pool)
|
||||
.await;
|
||||
|
||||
Ok(env)
|
||||
}
|
||||
|
||||
pub async fn upsert_folder(pool: &Pool<Sqlite>, r: Folder) -> Result<Folder, sqlx::Error> {
|
||||
let id = match r.id.as_str() {
|
||||
"" => generate_id(Some("fl")),
|
||||
_ => r.id.to_string(),
|
||||
};
|
||||
let headers_json = Json(headers);
|
||||
let auth_json = Json(authentication);
|
||||
let trimmed_name = name.trim();
|
||||
let trimmed_name = r.name.trim();
|
||||
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO folders (
|
||||
id,
|
||||
workspace_id,
|
||||
folder_id,
|
||||
name,
|
||||
sort_priority
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
updated_at = CURRENT_TIMESTAMP,
|
||||
name = excluded.name,
|
||||
folder_id = excluded.folder_id,
|
||||
sort_priority = excluded.sort_priority
|
||||
"#,
|
||||
id,
|
||||
r.workspace_id,
|
||||
r.folder_id,
|
||||
trimmed_name,
|
||||
r.sort_priority,
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
get_folder(&id, pool).await
|
||||
}
|
||||
|
||||
pub async fn duplicate_request(id: &str, pool: &Pool<Sqlite>) -> Result<HttpRequest, sqlx::Error> {
|
||||
let mut request = get_request(id, pool).await?.clone();
|
||||
request.id = "".to_string();
|
||||
upsert_request(pool, request).await
|
||||
}
|
||||
|
||||
pub async fn upsert_request(
|
||||
pool: &Pool<Sqlite>,
|
||||
r: HttpRequest,
|
||||
) -> Result<HttpRequest, sqlx::Error> {
|
||||
let id = match r.id.as_str() {
|
||||
"" => generate_id(Some("rq")),
|
||||
_ => r.id.to_string(),
|
||||
};
|
||||
let headers_json = Json(r.headers);
|
||||
let auth_json = Json(r.authentication);
|
||||
let trimmed_name = r.name.trim();
|
||||
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO http_requests (
|
||||
id,
|
||||
workspace_id,
|
||||
folder_id,
|
||||
name,
|
||||
url,
|
||||
method,
|
||||
@@ -384,10 +446,11 @@ pub async fn upsert_request(
|
||||
headers,
|
||||
sort_priority
|
||||
)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
updated_at = CURRENT_TIMESTAMP,
|
||||
name = excluded.name,
|
||||
folder_id = excluded.folder_id,
|
||||
method = excluded.method,
|
||||
headers = excluded.headers,
|
||||
body = excluded.body,
|
||||
@@ -398,20 +461,22 @@ pub async fn upsert_request(
|
||||
sort_priority = excluded.sort_priority
|
||||
"#,
|
||||
id,
|
||||
workspace_id,
|
||||
r.workspace_id,
|
||||
r.folder_id,
|
||||
trimmed_name,
|
||||
url,
|
||||
method,
|
||||
body,
|
||||
body_type,
|
||||
r.url,
|
||||
r.method,
|
||||
r.body,
|
||||
r.body_type,
|
||||
auth_json,
|
||||
authentication_type,
|
||||
r.authentication_type,
|
||||
headers_json,
|
||||
sort_priority,
|
||||
r.sort_priority,
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
get_request(id, pool).await
|
||||
|
||||
get_request(&id, pool).await
|
||||
}
|
||||
|
||||
pub async fn find_requests(
|
||||
@@ -425,6 +490,7 @@ pub async fn find_requests(
|
||||
id,
|
||||
model,
|
||||
workspace_id,
|
||||
folder_id,
|
||||
created_at,
|
||||
updated_at,
|
||||
name,
|
||||
@@ -453,6 +519,7 @@ pub async fn get_request(id: &str, pool: &Pool<Sqlite>) -> Result<HttpRequest, s
|
||||
id,
|
||||
model,
|
||||
workspace_id,
|
||||
folder_id,
|
||||
created_at,
|
||||
updated_at,
|
||||
name,
|
||||
@@ -542,6 +609,19 @@ pub async fn create_response(
|
||||
get_response(&id, pool).await
|
||||
}
|
||||
|
||||
pub async fn cancel_pending_responses(pool: &Pool<Sqlite>) -> Result<(), sqlx::Error> {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
UPDATE http_responses
|
||||
SET (elapsed, status_reason) = (-1, 'Cancelled')
|
||||
WHERE elapsed = 0;
|
||||
"#,
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn update_response_if_id(
|
||||
response: &HttpResponse,
|
||||
pool: &Pool<Sqlite>,
|
||||
@@ -552,23 +632,34 @@ pub async fn update_response_if_id(
|
||||
return update_response(response, pool).await;
|
||||
}
|
||||
|
||||
pub async fn update_workspace(
|
||||
workspace: Workspace,
|
||||
pub async fn upsert_workspace(
|
||||
pool: &Pool<Sqlite>,
|
||||
workspace: Workspace,
|
||||
) -> Result<Workspace, sqlx::Error> {
|
||||
let id = match workspace.id.as_str() {
|
||||
"" => generate_id(Some("wk")),
|
||||
_ => workspace.id.to_string(),
|
||||
};
|
||||
let trimmed_name = workspace.name.trim();
|
||||
sqlx::query!(
|
||||
r#"
|
||||
UPDATE workspaces SET (name, updated_at) =
|
||||
(?, CURRENT_TIMESTAMP) WHERE id = ?;
|
||||
INSERT INTO workspaces (id, name, description, variables)
|
||||
VALUES (?, ?, ?, ?)
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
updated_at = CURRENT_TIMESTAMP,
|
||||
name = excluded.name,
|
||||
description = excluded.description,
|
||||
variables = excluded.variables
|
||||
"#,
|
||||
id,
|
||||
trimmed_name,
|
||||
workspace.id,
|
||||
workspace.description,
|
||||
workspace.variables,
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
get_workspace(&workspace.id, pool).await
|
||||
get_workspace(&id, pool).await
|
||||
}
|
||||
|
||||
pub async fn update_response(
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
use std::fs;
|
||||
|
||||
use boa_engine::builtins::promise::PromiseState;
|
||||
use boa_engine::{
|
||||
js_string,
|
||||
module::{ModuleLoader, SimpleModuleLoader},
|
||||
@@ -5,17 +8,98 @@ use boa_engine::{
|
||||
Context, JsArgs, JsNativeError, JsValue, Module, NativeFunction, Source,
|
||||
};
|
||||
use boa_runtime::Console;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use sqlx::{Pool, Sqlite};
|
||||
use tauri::AppHandle;
|
||||
|
||||
pub fn test_plugins(app_handle: &AppHandle) {
|
||||
use crate::models::{self, Environment, Folder, HttpRequest, Workspace};
|
||||
|
||||
pub fn run_plugin_hello(app_handle: &AppHandle, plugin_name: &str) {
|
||||
run_plugin(app_handle, plugin_name, "hello", &[]);
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Deserialize, Serialize)]
|
||||
pub struct ImportedResources {
|
||||
workspaces: Vec<Workspace>,
|
||||
environments: Vec<Environment>,
|
||||
folders: Vec<Folder>,
|
||||
requests: Vec<HttpRequest>,
|
||||
}
|
||||
|
||||
pub async fn run_plugin_import(
|
||||
app_handle: &AppHandle,
|
||||
pool: &Pool<Sqlite>,
|
||||
plugin_name: &str,
|
||||
file_path: &str,
|
||||
) -> ImportedResources {
|
||||
let file = fs::read_to_string(file_path)
|
||||
.expect(format!("Unable to read file {}", file_path.to_string()).as_str());
|
||||
let file_contents = file.as_str();
|
||||
let result_json = run_plugin(
|
||||
app_handle,
|
||||
plugin_name,
|
||||
"pluginHookImport",
|
||||
&[js_string!(file_contents).into()],
|
||||
);
|
||||
let resources: ImportedResources =
|
||||
serde_json::from_value(result_json).expect("failed to parse result json");
|
||||
let mut imported_resources = ImportedResources::default();
|
||||
|
||||
println!("Importing resources");
|
||||
for w in resources.workspaces {
|
||||
println!("Importing workspace: {:?}", w);
|
||||
let x = models::upsert_workspace(&pool, w)
|
||||
.await
|
||||
.expect("Failed to create workspace");
|
||||
imported_resources.workspaces.push(x.clone());
|
||||
println!("Imported workspace: {}", x.name);
|
||||
}
|
||||
|
||||
for e in resources.environments {
|
||||
println!("Importing environment: {:?}", e);
|
||||
let x = models::upsert_environment(&pool, e)
|
||||
.await
|
||||
.expect("Failed to create environment");
|
||||
imported_resources.environments.push(x.clone());
|
||||
println!("Imported environment: {}", x.name);
|
||||
}
|
||||
|
||||
for f in resources.folders {
|
||||
println!("Importing folder: {:?}", f);
|
||||
let x = models::upsert_folder(&pool, f)
|
||||
.await
|
||||
.expect("Failed to create folder");
|
||||
imported_resources.folders.push(x.clone());
|
||||
println!("Imported folder: {}", x.name);
|
||||
}
|
||||
|
||||
for r in resources.requests {
|
||||
println!("Importing request: {:?}", r);
|
||||
let x = models::upsert_request(&pool, r)
|
||||
.await
|
||||
.expect("Failed to create request");
|
||||
imported_resources.requests.push(x.clone());
|
||||
println!("Imported request: {}", x.name);
|
||||
}
|
||||
|
||||
imported_resources
|
||||
}
|
||||
|
||||
fn run_plugin(
|
||||
app_handle: &AppHandle,
|
||||
plugin_name: &str,
|
||||
entrypoint: &str,
|
||||
js_args: &[JsValue],
|
||||
) -> serde_json::Value {
|
||||
let plugin_dir = app_handle
|
||||
.path_resolver()
|
||||
.resolve_resource("plugins/hello-world")
|
||||
.expect("failed to resolve plugin directory resource");
|
||||
let plugin_entry_file = app_handle
|
||||
.path_resolver()
|
||||
.resolve_resource("plugins/hello-world/index.js")
|
||||
.expect("failed to resolve plugin entry point resource");
|
||||
.resolve_resource("plugins")
|
||||
.expect("failed to resolve plugin directory resource")
|
||||
.join(plugin_name);
|
||||
let plugin_index_file = plugin_dir.join("out/index.js");
|
||||
|
||||
println!("Plugin dir={:?} file={:?}", plugin_dir, plugin_index_file);
|
||||
|
||||
// Module loader for the specific plugin
|
||||
let loader = &SimpleModuleLoader::new(plugin_dir).expect("failed to create module loader");
|
||||
@@ -29,47 +113,53 @@ pub fn test_plugins(app_handle: &AppHandle) {
|
||||
add_runtime(context);
|
||||
add_globals(context);
|
||||
|
||||
let source = Source::from_filepath(&plugin_entry_file).expect("Error opening file");
|
||||
let source = Source::from_filepath(&plugin_index_file).expect("Error opening file");
|
||||
|
||||
// Can also pass a `Some(realm)` if you need to execute the module in another realm.
|
||||
let module = Module::parse(source, None, context).expect("failed to parse module");
|
||||
|
||||
// Insert parsed entrypoint into the module loader
|
||||
// TODO: Is this needed if loaded from file already?
|
||||
loader.insert(plugin_entry_file, module.clone());
|
||||
loader.insert(plugin_index_file, module.clone());
|
||||
|
||||
let _promise_result = module
|
||||
let promise_result = module
|
||||
.load_link_evaluate(context)
|
||||
.expect("failed to evaluate module");
|
||||
|
||||
// Very important to push forward the job queue after queueing promises.
|
||||
context.run_jobs();
|
||||
|
||||
// // Checking if the final promise didn't return an error.
|
||||
// match promise_result.state() {
|
||||
// PromiseState::Pending => return Err("module didn't execute!".into()),
|
||||
// PromiseState::Fulfilled(v) => {
|
||||
// assert_eq!(v, JsValue::undefined())
|
||||
// }
|
||||
// PromiseState::Rejected(err) => {
|
||||
// return Err(JsError::from_opaque(err).try_native(context)?.into())
|
||||
// }
|
||||
// }
|
||||
// Checking if the final promise didn't return an error.
|
||||
match promise_result.state().expect("failed to get promise state") {
|
||||
PromiseState::Pending => {
|
||||
panic!("Promise was pending");
|
||||
}
|
||||
PromiseState::Fulfilled(v) => {
|
||||
assert_eq!(v, JsValue::undefined())
|
||||
}
|
||||
PromiseState::Rejected(err) => {
|
||||
panic!("Failed to link: {}", err.display());
|
||||
}
|
||||
}
|
||||
|
||||
let namespace = module.namespace(context);
|
||||
|
||||
let entrypoint_fn = namespace
|
||||
.get(js_string!("entrypoint"), context)
|
||||
let result = namespace
|
||||
.get(js_string!(entrypoint), context)
|
||||
.expect("failed to get entrypoint")
|
||||
.as_callable()
|
||||
.cloned()
|
||||
.ok_or_else(|| JsNativeError::typ().with_message("export wasn't a function!"))
|
||||
.expect("Failed to get entrypoint");
|
||||
|
||||
// Actually call the entrypoint function
|
||||
let _result = entrypoint_fn
|
||||
.call(&JsValue::undefined(), &[], context)
|
||||
.expect("Failed to get entrypoint")
|
||||
.call(&JsValue::undefined(), js_args, context)
|
||||
.expect("Failed to call entrypoint");
|
||||
|
||||
match result.is_undefined() {
|
||||
true => json!(null), // to_json doesn't work with undefined (yet)
|
||||
false => result
|
||||
.to_json(context)
|
||||
.expect("failed to convert result to json"),
|
||||
}
|
||||
}
|
||||
|
||||
fn add_runtime(context: &mut Context<'_>) {
|
||||
|
||||
@@ -1,24 +1,27 @@
|
||||
use crate::models::Environment;
|
||||
use crate::models::{Environment, Workspace};
|
||||
use std::collections::HashMap;
|
||||
use tauri::regex::Regex;
|
||||
|
||||
pub fn render(template: &str, environment: Option<&Environment>) -> String {
|
||||
match environment {
|
||||
Some(environment) => render_with_environment(template, environment),
|
||||
None => template.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn render_with_environment(template: &str, environment: &Environment) -> String {
|
||||
pub fn render(template: &str, workspace: &Workspace, environment: Option<&Environment>) -> String {
|
||||
let mut map = HashMap::new();
|
||||
let variables = &environment.variables.0;
|
||||
for variable in variables {
|
||||
if !variable.enabled {
|
||||
let workspace_variables = &workspace.variables.0;
|
||||
for variable in workspace_variables {
|
||||
if !variable.enabled || variable.value.is_empty() {
|
||||
continue;
|
||||
}
|
||||
map.insert(variable.name.as_str(), variable.value.as_str());
|
||||
}
|
||||
|
||||
if let Some(e) = environment {
|
||||
let environment_variables = &e.variables.0;
|
||||
for variable in environment_variables {
|
||||
if !variable.enabled || variable.value.is_empty() {
|
||||
continue;
|
||||
}
|
||||
map.insert(variable.name.as_str(), variable.value.as_str());
|
||||
}
|
||||
}
|
||||
|
||||
Regex::new(r"\$\{\[\s*([^]\s]+)\s*]}")
|
||||
.expect("Failed to create regex")
|
||||
.replace_all(template, |caps: &tauri::regex::Captures| {
|
||||
|
||||
119
src-tauri/src/window_menu.rs
Normal file
119
src-tauri/src/window_menu.rs
Normal file
@@ -0,0 +1,119 @@
|
||||
use tauri::{AboutMetadata, CustomMenuItem, Menu, MenuItem, Submenu};
|
||||
|
||||
pub fn os_default(#[allow(unused)] app_name: &str) -> Menu {
|
||||
let mut menu = Menu::new();
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
menu = menu.add_submenu(Submenu::new(
|
||||
app_name,
|
||||
Menu::new()
|
||||
.add_native_item(MenuItem::About(
|
||||
app_name.to_string(),
|
||||
AboutMetadata::default(),
|
||||
))
|
||||
.add_native_item(MenuItem::Separator)
|
||||
.add_native_item(MenuItem::Services)
|
||||
.add_native_item(MenuItem::Separator)
|
||||
.add_native_item(MenuItem::Hide)
|
||||
.add_native_item(MenuItem::HideOthers)
|
||||
.add_native_item(MenuItem::ShowAll)
|
||||
.add_native_item(MenuItem::Separator)
|
||||
.add_native_item(MenuItem::Quit),
|
||||
));
|
||||
}
|
||||
|
||||
let mut file_menu = Menu::new();
|
||||
file_menu = file_menu.add_native_item(MenuItem::CloseWindow);
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
file_menu = file_menu.add_native_item(MenuItem::Quit);
|
||||
}
|
||||
menu = menu.add_submenu(Submenu::new("File", file_menu));
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
let mut edit_menu = Menu::new();
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
edit_menu = edit_menu.add_native_item(MenuItem::Undo);
|
||||
edit_menu = edit_menu.add_native_item(MenuItem::Redo);
|
||||
edit_menu = edit_menu.add_native_item(MenuItem::Separator);
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
edit_menu = edit_menu.add_native_item(MenuItem::Cut);
|
||||
edit_menu = edit_menu.add_native_item(MenuItem::Copy);
|
||||
edit_menu = edit_menu.add_native_item(MenuItem::Paste);
|
||||
}
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
edit_menu = edit_menu.add_native_item(MenuItem::SelectAll);
|
||||
}
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
{
|
||||
menu = menu.add_submenu(Submenu::new("Edit", edit_menu));
|
||||
}
|
||||
let mut view_menu = Menu::new();
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
view_menu = view_menu
|
||||
.add_native_item(MenuItem::EnterFullScreen)
|
||||
.add_native_item(MenuItem::Separator);
|
||||
}
|
||||
view_menu = view_menu
|
||||
.add_item(
|
||||
CustomMenuItem::new("zoom_reset".to_string(), "Zoom to Actual Size")
|
||||
.accelerator("CmdOrCtrl+0"),
|
||||
)
|
||||
.add_item(
|
||||
CustomMenuItem::new("zoom_in".to_string(), "Zoom In").accelerator("CmdOrCtrl+Plus"),
|
||||
)
|
||||
.add_item(
|
||||
CustomMenuItem::new("zoom_out".to_string(), "Zoom Out").accelerator("CmdOrCtrl+-"),
|
||||
)
|
||||
.add_native_item(MenuItem::Separator)
|
||||
.add_item(
|
||||
CustomMenuItem::new("toggle_sidebar".to_string(), "Toggle Sidebar")
|
||||
.accelerator("CmdOrCtrl+b"),
|
||||
)
|
||||
.add_item(
|
||||
CustomMenuItem::new("focus_sidebar".to_string(), "Focus Sidebar")
|
||||
.accelerator("CmdOrCtrl+1"),
|
||||
)
|
||||
.add_item(
|
||||
CustomMenuItem::new("toggle_settings".to_string(), "Toggle Settings")
|
||||
.accelerator("CmdOrCtrl+,"),
|
||||
)
|
||||
.add_item(
|
||||
CustomMenuItem::new("focus_url".to_string(), "Focus URL").accelerator("CmdOrCtrl+l"),
|
||||
);
|
||||
menu = menu.add_submenu(Submenu::new("View", view_menu));
|
||||
|
||||
let mut window_menu = Menu::new();
|
||||
window_menu = window_menu.add_native_item(MenuItem::Minimize);
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
window_menu = window_menu.add_native_item(MenuItem::Zoom);
|
||||
window_menu = window_menu.add_native_item(MenuItem::Separator);
|
||||
}
|
||||
window_menu = window_menu.add_native_item(MenuItem::CloseWindow);
|
||||
menu = menu.add_submenu(Submenu::new("Window", window_menu));
|
||||
|
||||
menu = menu.add_submenu(Submenu::new(
|
||||
"Workspace",
|
||||
Menu::new()
|
||||
.add_item(
|
||||
CustomMenuItem::new("send_request".to_string(), "Send Request")
|
||||
.accelerator("CmdOrCtrl+r"),
|
||||
)
|
||||
.add_item(
|
||||
CustomMenuItem::new("new_request".to_string(), "New Request")
|
||||
.accelerator("CmdOrCtrl+n"),
|
||||
)
|
||||
.add_item(
|
||||
CustomMenuItem::new("duplicate_request".to_string(), "Duplicate Request")
|
||||
.accelerator("CmdOrCtrl+d"),
|
||||
),
|
||||
));
|
||||
|
||||
menu
|
||||
}
|
||||
@@ -8,10 +8,27 @@
|
||||
},
|
||||
"package": {
|
||||
"productName": "Yaak",
|
||||
"version": "2023.1.7"
|
||||
"version": "2023.2.0"
|
||||
},
|
||||
"tauri": {
|
||||
"windows": [],
|
||||
"cli": {
|
||||
"description": "Yaak CLI",
|
||||
"longDescription": "This is the Yaak CLI, yo",
|
||||
"beforeHelp": "u can use it to build, develop and manage your Yaak application.",
|
||||
"afterHelp": "Have fun!",
|
||||
"args": [],
|
||||
"subcommands": {
|
||||
"import": {
|
||||
"args": [{
|
||||
"name": "file",
|
||||
"short": "f",
|
||||
"takesValue": true
|
||||
}]
|
||||
},
|
||||
"hello": {}
|
||||
}
|
||||
},
|
||||
"allowlist": {
|
||||
"all": false,
|
||||
"os": {
|
||||
@@ -36,6 +53,10 @@
|
||||
},
|
||||
"window": {
|
||||
"startDragging": true
|
||||
},
|
||||
"dialog": {
|
||||
"all": false,
|
||||
"open": true
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
|
||||
@@ -1,85 +1,76 @@
|
||||
import classNames from 'classnames';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useActiveEnvironment } from '../hooks/useActiveEnvironment';
|
||||
import { useAppRoutes } from '../hooks/useAppRoutes';
|
||||
import { useEnvironments } from '../hooks/useEnvironments';
|
||||
import type { ButtonProps } from './core/Button';
|
||||
import { Button } from './core/Button';
|
||||
import type { DropdownItem } from './core/Dropdown';
|
||||
import { Dropdown } from './core/Dropdown';
|
||||
import { Icon } from './core/Icon';
|
||||
import { useEnvironments } from '../hooks/useEnvironments';
|
||||
import { useActiveEnvironment } from '../hooks/useActiveEnvironment';
|
||||
import { useDialog } from './DialogContext';
|
||||
import { EnvironmentEditDialog } from './EnvironmentEditDialog';
|
||||
import { useAppRoutes } from '../hooks/useAppRoutes';
|
||||
import { useCreateEnvironment } from '../hooks/useCreateEnvironment';
|
||||
import { usePrompt } from '../hooks/usePrompt';
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
};
|
||||
} & Pick<ButtonProps, 'forDropdown' | 'leftSlot'>;
|
||||
|
||||
export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdown({
|
||||
className,
|
||||
...buttonProps
|
||||
}: Props) {
|
||||
const environments = useEnvironments();
|
||||
const activeEnvironment = useActiveEnvironment();
|
||||
const createEnvironment = useCreateEnvironment();
|
||||
const dialog = useDialog();
|
||||
const prompt = usePrompt();
|
||||
const routes = useAppRoutes();
|
||||
|
||||
const showEnvironmentDialog = useCallback(() => {
|
||||
dialog.show({
|
||||
title: 'Manage Environments',
|
||||
render: () => <EnvironmentEditDialog />,
|
||||
render: () => <EnvironmentEditDialog initialEnvironment={activeEnvironment} />,
|
||||
});
|
||||
}, [dialog]);
|
||||
}, [dialog, activeEnvironment]);
|
||||
|
||||
const items: DropdownItem[] = useMemo(
|
||||
() =>
|
||||
environments.length === 0
|
||||
? [
|
||||
{
|
||||
key: 'create',
|
||||
label: 'Create Environment',
|
||||
leftSlot: <Icon icon="plusCircle" />,
|
||||
onSelect: async () => {
|
||||
await createEnvironment.mutateAsync();
|
||||
showEnvironmentDialog();
|
||||
},
|
||||
},
|
||||
]
|
||||
: [
|
||||
...environments.map(
|
||||
(e) => ({
|
||||
key: e.id,
|
||||
label: e.name,
|
||||
rightSlot: e.id === activeEnvironment?.id ? <Icon icon="check" /> : undefined,
|
||||
onSelect: async () => {
|
||||
routes.setEnvironment(e);
|
||||
},
|
||||
}),
|
||||
[activeEnvironment?.id],
|
||||
),
|
||||
{ type: 'separator', label: 'Environments' },
|
||||
{
|
||||
key: 'edit',
|
||||
label: 'Manage Environments',
|
||||
leftSlot: <Icon icon="gear" />,
|
||||
onSelect: showEnvironmentDialog,
|
||||
},
|
||||
],
|
||||
[activeEnvironment, environments, routes, createEnvironment, showEnvironmentDialog],
|
||||
() => [
|
||||
...environments.map(
|
||||
(e) => ({
|
||||
key: e.id,
|
||||
label: e.name,
|
||||
rightSlot: e.id === activeEnvironment?.id ? <Icon icon="check" /> : undefined,
|
||||
onSelect: async () => {
|
||||
if (e.id !== activeEnvironment?.id) {
|
||||
routes.setEnvironment(e);
|
||||
} else {
|
||||
routes.setEnvironment(null);
|
||||
}
|
||||
},
|
||||
}),
|
||||
[activeEnvironment?.id],
|
||||
),
|
||||
...((environments.length > 0
|
||||
? [{ type: 'separator', label: 'Environments' }]
|
||||
: []) as DropdownItem[]),
|
||||
{
|
||||
key: 'edit',
|
||||
label: 'Manage Environments',
|
||||
leftSlot: <Icon icon="gear" />,
|
||||
onSelect: showEnvironmentDialog,
|
||||
},
|
||||
],
|
||||
[activeEnvironment, environments, routes, showEnvironmentDialog],
|
||||
);
|
||||
|
||||
return (
|
||||
<Dropdown items={items}>
|
||||
<Button
|
||||
forDropdown
|
||||
size="sm"
|
||||
className={classNames(
|
||||
className,
|
||||
'text-gray-800 !px-2 truncate',
|
||||
activeEnvironment == null && 'text-opacity-disabled italic',
|
||||
)}
|
||||
{...buttonProps}
|
||||
>
|
||||
{activeEnvironment?.name ?? 'No Environment'}
|
||||
</Button>
|
||||
|
||||
@@ -1,34 +1,48 @@
|
||||
import { useCreateEnvironment } from '../hooks/useCreateEnvironment';
|
||||
import { useEnvironments } from '../hooks/useEnvironments';
|
||||
import type { Environment } from '../lib/models';
|
||||
import { Button } from './core/Button';
|
||||
import classNames from 'classnames';
|
||||
import { useActiveEnvironment } from '../hooks/useActiveEnvironment';
|
||||
import { useAppRoutes } from '../hooks/useAppRoutes';
|
||||
import { PairEditor } from './core/PairEditor';
|
||||
import type { PairEditorProps } from './core/PairEditor';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useUpdateEnvironment } from '../hooks/useUpdateEnvironment';
|
||||
import { HStack, VStack } from './core/Stacks';
|
||||
import { IconButton } from './core/IconButton';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useWindowSize } from 'react-use';
|
||||
import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
|
||||
import { useCreateEnvironment } from '../hooks/useCreateEnvironment';
|
||||
import { useDeleteEnvironment } from '../hooks/useDeleteEnvironment';
|
||||
import { useEnvironments } from '../hooks/useEnvironments';
|
||||
import { usePrompt } from '../hooks/usePrompt';
|
||||
import { useUpdateEnvironment } from '../hooks/useUpdateEnvironment';
|
||||
import { useUpdateWorkspace } from '../hooks/useUpdateWorkspace';
|
||||
import type { Environment, Workspace } from '../lib/models';
|
||||
import { Button } from './core/Button';
|
||||
import type { DropdownItem } from './core/Dropdown';
|
||||
import { Dropdown } from './core/Dropdown';
|
||||
import type {
|
||||
GenericCompletionConfig,
|
||||
GenericCompletionOption,
|
||||
} from './core/Editor/genericCompletion';
|
||||
import { Icon } from './core/Icon';
|
||||
import { usePrompt } from '../hooks/usePrompt';
|
||||
import { IconButton } from './core/IconButton';
|
||||
import { InlineCode } from './core/InlineCode';
|
||||
import { useWindowSize } from 'react-use';
|
||||
import type { GenericCompletionConfig } from './core/Editor/genericCompletion';
|
||||
import type { PairEditorProps } from './core/PairEditor';
|
||||
import { PairEditor } from './core/PairEditor';
|
||||
import { HStack, VStack } from './core/Stacks';
|
||||
|
||||
export const EnvironmentEditDialog = function () {
|
||||
const routes = useAppRoutes();
|
||||
interface Props {
|
||||
initialEnvironment: Environment | null;
|
||||
}
|
||||
|
||||
export const EnvironmentEditDialog = function ({ initialEnvironment }: Props) {
|
||||
const [selectedEnvironmentId, setSelectedEnvironmentId] = useState<string | null>(
|
||||
initialEnvironment?.id ?? null,
|
||||
);
|
||||
const environments = useEnvironments();
|
||||
const createEnvironment = useCreateEnvironment();
|
||||
const activeEnvironment = useActiveEnvironment();
|
||||
const activeWorkspace = useActiveWorkspace();
|
||||
|
||||
const windowSize = useWindowSize();
|
||||
const showSidebar = windowSize.width > 500;
|
||||
|
||||
const selectedEnvironment = useMemo(
|
||||
() => environments.find((e) => e.id === selectedEnvironmentId) ?? null,
|
||||
[environments, selectedEnvironmentId],
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
@@ -37,39 +51,39 @@ export const EnvironmentEditDialog = function () {
|
||||
)}
|
||||
>
|
||||
{showSidebar && (
|
||||
<aside className="grid grid-rows-[minmax(0,1fr)_auto] gap-y-0.5 h-full max-w-[250px] pr-4 border-r border-gray-100">
|
||||
<aside className="grid grid-rows-[minmax(0,1fr)_auto] gap-y-0.5 h-full max-w-[250px] pr-3 border-r border-gray-100 -ml-2">
|
||||
<div className="min-w-0 h-full w-full overflow-y-scroll">
|
||||
{environments.map((e) => (
|
||||
<Button
|
||||
size="xs"
|
||||
color="custom"
|
||||
className={classNames(
|
||||
'w-full',
|
||||
'text-gray-600 hocus:text-gray-800',
|
||||
activeEnvironment?.id === e.id && 'bg-highlightSecondary !text-gray-900',
|
||||
)}
|
||||
justify="start"
|
||||
key={e.id}
|
||||
onClick={() => {
|
||||
routes.setEnvironment(e);
|
||||
}}
|
||||
>
|
||||
{e.name}
|
||||
</Button>
|
||||
))}
|
||||
<SidebarButton
|
||||
active={selectedEnvironment == null}
|
||||
onClick={() => setSelectedEnvironmentId(null)}
|
||||
>
|
||||
Base Environment
|
||||
</SidebarButton>
|
||||
<div className="ml-3 pl-2 border-l border-highlight">
|
||||
{environments.map((e) => (
|
||||
<SidebarButton
|
||||
key={e.id}
|
||||
active={selectedEnvironment?.id === e.id}
|
||||
onClick={() => setSelectedEnvironmentId(e.id)}
|
||||
>
|
||||
{e.name}
|
||||
</SidebarButton>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
className="w-full"
|
||||
className="w-full text-center"
|
||||
color="gray"
|
||||
justify="center"
|
||||
onClick={() => createEnvironment.mutate()}
|
||||
>
|
||||
New Environment
|
||||
</Button>
|
||||
</aside>
|
||||
)}
|
||||
{activeEnvironment != null ? (
|
||||
<EnvironmentEditor environment={activeEnvironment} />
|
||||
{activeWorkspace != null ? (
|
||||
<EnvironmentEditor environment={selectedEnvironment} workspace={activeWorkspace} />
|
||||
) : (
|
||||
<div className="flex w-full h-full items-center justify-center text-gray-400 italic">
|
||||
select an environment
|
||||
@@ -79,57 +93,90 @@ export const EnvironmentEditDialog = function () {
|
||||
);
|
||||
};
|
||||
|
||||
const EnvironmentEditor = function ({ environment }: { environment: Environment }) {
|
||||
const EnvironmentEditor = function ({
|
||||
environment,
|
||||
workspace,
|
||||
}: {
|
||||
environment: Environment | null;
|
||||
workspace: Workspace;
|
||||
}) {
|
||||
const environments = useEnvironments();
|
||||
const updateEnvironment = useUpdateEnvironment(environment.id);
|
||||
const updateEnvironment = useUpdateEnvironment(environment?.id ?? 'n/a');
|
||||
const updateWorkspace = useUpdateWorkspace(workspace.id);
|
||||
const deleteEnvironment = useDeleteEnvironment(environment);
|
||||
const variables = environment == null ? workspace.variables : environment.variables;
|
||||
const handleChange = useCallback<PairEditorProps['onChange']>(
|
||||
(variables) => {
|
||||
updateEnvironment.mutate({ variables });
|
||||
if (environment != null) {
|
||||
updateEnvironment.mutate({ variables });
|
||||
} else {
|
||||
updateWorkspace.mutate({ variables });
|
||||
}
|
||||
},
|
||||
[updateEnvironment],
|
||||
[updateWorkspace, updateEnvironment, environment],
|
||||
);
|
||||
|
||||
// Gather a list of env names from other environments, to help the user get them aligned
|
||||
const nameAutocomplete = useMemo<GenericCompletionConfig>(() => {
|
||||
const allVariableNames = environments.flatMap((e) => e.variables.map((v) => v.name));
|
||||
// Filter out empty strings and variables that already exist in the active environment
|
||||
const otherEnvironments = environments.filter((e) => e.id !== environment?.id);
|
||||
const allVariableNames =
|
||||
environment == null
|
||||
? [
|
||||
// Nothing to autocomplete if we're in the base environment
|
||||
]
|
||||
: [
|
||||
...workspace.variables.map((v) => v.name),
|
||||
...otherEnvironments.flatMap((e) => e.variables.map((v) => v.name)),
|
||||
];
|
||||
|
||||
// Filter out empty strings and variables that already exist
|
||||
const variableNames = allVariableNames.filter(
|
||||
(name) => name != '' && !environment.variables.find((v) => v.name === name),
|
||||
(name) => name != '' && !variables.find((v) => v.name === name),
|
||||
);
|
||||
return { options: variableNames.map((name) => ({ label: name, type: 'constant' })) };
|
||||
}, [environments, environment.variables]);
|
||||
const uniqueVariableNames = [...new Set(variableNames)];
|
||||
const options = uniqueVariableNames.map(
|
||||
(name): GenericCompletionOption => ({
|
||||
label: name,
|
||||
type: 'constant',
|
||||
}),
|
||||
);
|
||||
return { options };
|
||||
}, [environments, variables, workspace, environment]);
|
||||
|
||||
const prompt = usePrompt();
|
||||
const items = useMemo<DropdownItem[]>(
|
||||
() => [
|
||||
{
|
||||
key: 'rename',
|
||||
label: 'Rename',
|
||||
leftSlot: <Icon icon="pencil" size="sm" />,
|
||||
onSelect: async () => {
|
||||
const name = await prompt({
|
||||
title: 'Rename Environment',
|
||||
description: (
|
||||
<>
|
||||
Enter a new name for <InlineCode>{environment.name}</InlineCode>
|
||||
</>
|
||||
),
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
defaultValue: environment.name,
|
||||
});
|
||||
updateEnvironment.mutate({ name });
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'delete',
|
||||
variant: 'danger',
|
||||
label: 'Delete',
|
||||
leftSlot: <Icon icon="trash" size="sm" />,
|
||||
onSelect: () => deleteEnvironment.mutate(),
|
||||
},
|
||||
],
|
||||
[deleteEnvironment, updateEnvironment, environment.name, prompt],
|
||||
const items = useMemo<DropdownItem[] | null>(
|
||||
() =>
|
||||
environment == null
|
||||
? null
|
||||
: [
|
||||
{
|
||||
key: 'rename',
|
||||
label: 'Rename',
|
||||
leftSlot: <Icon icon="pencil" size="sm" />,
|
||||
onSelect: async () => {
|
||||
const name = await prompt({
|
||||
title: 'Rename Environment',
|
||||
description: (
|
||||
<>
|
||||
Enter a new name for <InlineCode>{environment.name}</InlineCode>
|
||||
</>
|
||||
),
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
defaultValue: environment.name,
|
||||
});
|
||||
updateEnvironment.mutate({ name });
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'delete',
|
||||
variant: 'danger',
|
||||
label: 'Delete',
|
||||
leftSlot: <Icon icon="trash" size="sm" />,
|
||||
onSelect: () => deleteEnvironment.mutate(),
|
||||
},
|
||||
],
|
||||
[deleteEnvironment, updateEnvironment, prompt, environment],
|
||||
);
|
||||
|
||||
const validateName = useCallback((name: string) => {
|
||||
@@ -141,10 +188,12 @@ const EnvironmentEditor = function ({ environment }: { environment: Environment
|
||||
return (
|
||||
<VStack space={2}>
|
||||
<HStack space={2} className="justify-between">
|
||||
<h1 className="text-xl">{environment.name}</h1>
|
||||
<Dropdown items={items}>
|
||||
<IconButton icon="gear" title="Environment Actions" size="sm" className="!h-auto w-8" />
|
||||
</Dropdown>
|
||||
<h1 className="text-xl">{environment?.name ?? 'Base Environment'}</h1>
|
||||
{items != null && (
|
||||
<Dropdown items={items}>
|
||||
<IconButton icon="gear" title="Environment Actions" size="sm" className="!h-auto w-8" />
|
||||
</Dropdown>
|
||||
)}
|
||||
</HStack>
|
||||
<PairEditor
|
||||
nameAutocomplete={nameAutocomplete}
|
||||
@@ -153,10 +202,37 @@ const EnvironmentEditor = function ({ environment }: { environment: Environment
|
||||
valuePlaceholder="variable value"
|
||||
nameValidate={validateName}
|
||||
valueAutocompleteVariables={false}
|
||||
forceUpdateKey={environment.id}
|
||||
pairs={environment.variables}
|
||||
forceUpdateKey={environment?.id ?? workspace?.id ?? 'n/a'}
|
||||
pairs={variables}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</VStack>
|
||||
);
|
||||
};
|
||||
|
||||
function SidebarButton({
|
||||
children,
|
||||
className,
|
||||
active,
|
||||
onClick,
|
||||
}: {
|
||||
className?: string;
|
||||
children: string;
|
||||
active: boolean;
|
||||
onClick: () => void;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
tabIndex={active ? 0 : -1}
|
||||
onClick={onClick}
|
||||
className={classNames(
|
||||
className,
|
||||
'flex items-center text-sm text-left w-full mb-1 h-xs rounded px-2',
|
||||
'text-gray-600 hocus:text-gray-800 focus:bg-highlightSecondary outline-none',
|
||||
active && '!text-gray-900',
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { appWindow } from '@tauri-apps/api/window';
|
||||
import { useEffect } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useEffectOnce } from 'react-use';
|
||||
import { keyValueQueryKey } from '../hooks/useKeyValue';
|
||||
import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
|
||||
import { useRecentEnvironments } from '../hooks/useRecentEnvironments';
|
||||
import { useRecentRequests } from '../hooks/useRecentRequests';
|
||||
import { useRecentWorkspaces } from '../hooks/useRecentWorkspaces';
|
||||
import { requestsQueryKey } from '../hooks/useRequests';
|
||||
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
|
||||
import { responsesQueryKey } from '../hooks/useResponses';
|
||||
import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
|
||||
import { workspacesQueryKey } from '../hooks/useWorkspaces';
|
||||
import { trackPage } from '../lib/analytics';
|
||||
import { DEFAULT_FONT_SIZE } from '../lib/constants';
|
||||
import { NAMESPACE_NO_SYNC } from '../lib/keyValueStore';
|
||||
import type { HttpRequest, HttpResponse, Model, Workspace } from '../lib/models';
|
||||
import { modelsEq } from '../lib/models';
|
||||
import { useRecentRequests } from '../hooks/useRecentRequests';
|
||||
import { useRecentWorkspaces } from '../hooks/useRecentWorkspaces';
|
||||
import { useRecentEnvironments } from '../hooks/useRecentEnvironments';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useEffect } from 'react';
|
||||
import { setPathname } from '../lib/persistPathname';
|
||||
|
||||
export function GlobalHooks() {
|
||||
@@ -30,9 +32,13 @@ export function GlobalHooks() {
|
||||
// Listen for location changes and update the pathname
|
||||
const location = useLocation();
|
||||
useEffect(() => {
|
||||
setPathname(location.pathname);
|
||||
setPathname(location.pathname).catch(console.error);
|
||||
}, [location.pathname]);
|
||||
|
||||
useEffectOnce(() => {
|
||||
trackPage('/');
|
||||
});
|
||||
|
||||
useListenToTauriEvent<Model>('created_model', ({ payload, windowLabel }) => {
|
||||
if (shouldIgnoreEvent(payload, windowLabel)) return;
|
||||
|
||||
@@ -82,9 +88,8 @@ export function GlobalHooks() {
|
||||
}
|
||||
|
||||
if (!shouldIgnoreModel(payload)) {
|
||||
queryClient.setQueryData<Model[]>(
|
||||
queryKey,
|
||||
(values) => values?.map((v) => (modelsEq(v, payload) ? payload : v)),
|
||||
queryClient.setQueryData<Model[]>(queryKey, (values) =>
|
||||
values?.map((v) => (modelsEq(v, payload) ? payload : v)),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -32,7 +32,7 @@ export function Overlay({
|
||||
return (
|
||||
<Portal name={portalName}>
|
||||
{open && (
|
||||
<FocusTrap>
|
||||
<FocusTrap>
|
||||
<motion.div
|
||||
className={classNames('fixed inset-0', zIndexes[zIndex])}
|
||||
initial={{ opacity: 0 }}
|
||||
@@ -46,10 +46,6 @@ export function Overlay({
|
||||
variant === 'default' && 'bg-gray-600/30 dark:bg-black/30 backdrop-blur-sm',
|
||||
)}
|
||||
/>
|
||||
{/* Add region to still be able to drag the window */}
|
||||
{variant !== 'transparent' && (
|
||||
<div data-tauri-drag-region className="absolute top-0 left-0 right-0 h-md" />
|
||||
)}
|
||||
<div className="bg-red-100">{children}</div>
|
||||
</motion.div>
|
||||
</FocusTrap>
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import classNames from 'classnames';
|
||||
import { useMemo, useRef } from 'react';
|
||||
import { useKey, useKeyPressEvent } from 'react-use';
|
||||
import { useActiveEnvironmentId } from '../hooks/useActiveEnvironmentId';
|
||||
import { useActiveRequest } from '../hooks/useActiveRequest';
|
||||
import { useActiveWorkspaceId } from '../hooks/useActiveWorkspaceId';
|
||||
import { useAppRoutes } from '../hooks/useAppRoutes';
|
||||
import { useRecentRequests } from '../hooks/useRecentRequests';
|
||||
import { useRequests } from '../hooks/useRequests';
|
||||
import type { ButtonProps } from './core/Button';
|
||||
import { Button } from './core/Button';
|
||||
import { CountBadge } from './core/CountBadge';
|
||||
import type { DropdownItem, DropdownRef } from './core/Dropdown';
|
||||
import { Dropdown } from './core/Dropdown';
|
||||
import classNames from 'classnames';
|
||||
import { useActiveEnvironmentId } from '../hooks/useActiveEnvironmentId';
|
||||
|
||||
export function RecentRequestsDropdown() {
|
||||
export function RecentRequestsDropdown({ className }: Pick<ButtonProps, 'className'>) {
|
||||
const dropdownRef = useRef<DropdownRef>(null);
|
||||
const activeRequest = useActiveRequest();
|
||||
const activeWorkspaceId = useActiveWorkspaceId();
|
||||
@@ -63,7 +63,7 @@ export function RecentRequestsDropdown() {
|
||||
recentRequestItems.push({
|
||||
key: request.id,
|
||||
label: request.name,
|
||||
leftSlot: <CountBadge className="!ml-0 px-0 w-5" count={recentRequestItems.length + 1} />,
|
||||
// leftSlot: <CountBadge className="!ml-0 px-0 w-5" count={recentRequestItems.length + 1} />,
|
||||
onSelect: () => {
|
||||
routes.navigate('request', {
|
||||
requestId: request.id,
|
||||
@@ -76,7 +76,12 @@ export function RecentRequestsDropdown() {
|
||||
|
||||
// No recent requests to show
|
||||
if (recentRequestItems.length === 0) {
|
||||
return [];
|
||||
return [
|
||||
{
|
||||
label: 'No recent requests',
|
||||
disabled: true,
|
||||
},
|
||||
] as DropdownItem[];
|
||||
}
|
||||
|
||||
return recentRequestItems.slice(0, 20);
|
||||
@@ -85,9 +90,11 @@ export function RecentRequestsDropdown() {
|
||||
return (
|
||||
<Dropdown ref={dropdownRef} items={items}>
|
||||
<Button
|
||||
data-tauri-drag-region
|
||||
size="sm"
|
||||
className={classNames(
|
||||
'flex-[2] text-center text-gray-800 text-sm truncate pointer-events-none',
|
||||
className,
|
||||
'text-gray-800 text-sm truncate pointer-events-auto',
|
||||
activeRequest === null && 'text-opacity-disabled italic',
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -1,24 +1,21 @@
|
||||
import type { HTMLAttributes, ReactElement } from 'react';
|
||||
import React, { useRef } from 'react';
|
||||
import { useRef } from 'react';
|
||||
import { useDeleteRequest } from '../hooks/useDeleteRequest';
|
||||
import { useDuplicateRequest } from '../hooks/useDuplicateRequest';
|
||||
import { useTheme } from '../hooks/useTheme';
|
||||
import type { DropdownRef } from './core/Dropdown';
|
||||
import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
|
||||
import type { DropdownProps, DropdownRef } from './core/Dropdown';
|
||||
import { Dropdown } from './core/Dropdown';
|
||||
import { HotKey } from './core/HotKey';
|
||||
import { Icon } from './core/Icon';
|
||||
import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
|
||||
|
||||
interface Props {
|
||||
requestId: string;
|
||||
children: ReactElement<HTMLAttributes<HTMLButtonElement>>;
|
||||
requestId: string | null;
|
||||
children: DropdownProps['children'];
|
||||
}
|
||||
|
||||
export function RequestActionsDropdown({ requestId, children }: Props) {
|
||||
const deleteRequest = useDeleteRequest(requestId);
|
||||
const duplicateRequest = useDuplicateRequest({ id: requestId, navigateAfter: true });
|
||||
const dropdownRef = useRef<DropdownRef>(null);
|
||||
const { appearance, toggleAppearance } = useTheme();
|
||||
|
||||
useListenToTauriEvent('toggle_settings', () => {
|
||||
dropdownRef.current?.toggle();
|
||||
@@ -29,6 +26,10 @@ export function RequestActionsDropdown({ requestId, children }: Props) {
|
||||
duplicateRequest.mutate();
|
||||
});
|
||||
|
||||
if (requestId == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
ref={dropdownRef}
|
||||
@@ -47,13 +48,6 @@ export function RequestActionsDropdown({ requestId, children }: Props) {
|
||||
variant: 'danger',
|
||||
leftSlot: <Icon icon="trash" />,
|
||||
},
|
||||
{ type: 'separator', label: 'Yaak Settings' },
|
||||
{
|
||||
key: 'appearance',
|
||||
label: appearance === 'dark' ? 'Light Theme' : 'Dark Theme',
|
||||
onSelect: toggleAppearance,
|
||||
leftSlot: <Icon icon={appearance === 'dark' ? 'sun' : 'moon'} />,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -4,9 +4,10 @@ import classNames from 'classnames';
|
||||
import type { CSSProperties } from 'react';
|
||||
import { memo, useCallback, useMemo, useState } from 'react';
|
||||
import { createGlobalState } from 'react-use';
|
||||
import { useActiveEnvironmentId } from '../hooks/useActiveEnvironmentId';
|
||||
import { useActiveRequest } from '../hooks/useActiveRequest';
|
||||
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
|
||||
import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
|
||||
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
|
||||
import { useUpdateRequest } from '../hooks/useUpdateRequest';
|
||||
import { tryFormatJson } from '../lib/formatters';
|
||||
import type { HttpHeader, HttpRequest } from '../lib/models';
|
||||
@@ -30,7 +31,6 @@ import { GraphQLEditor } from './GraphQLEditor';
|
||||
import { HeaderEditor } from './HeaderEditor';
|
||||
import { ParametersEditor } from './ParameterEditor';
|
||||
import { UrlBar } from './UrlBar';
|
||||
import { useActiveEnvironmentId } from '../hooks/useActiveEnvironmentId';
|
||||
|
||||
interface Props {
|
||||
style?: CSSProperties;
|
||||
@@ -88,7 +88,7 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN
|
||||
// Force update header editor so any changed headers are reflected
|
||||
setTimeout(() => setForceUpdateHeaderEditorKey((u) => u + 1), 100);
|
||||
|
||||
await updateRequest.mutate(patch);
|
||||
updateRequest.mutate(patch);
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -125,7 +125,7 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN
|
||||
token: authentication.token ?? '',
|
||||
};
|
||||
}
|
||||
await updateRequest.mutate({ authenticationType, authentication });
|
||||
updateRequest.mutate({ authenticationType, authentication });
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -146,7 +146,10 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN
|
||||
'send_request',
|
||||
async ({ windowLabel }) => {
|
||||
if (windowLabel !== appWindow.label) return;
|
||||
await invoke('send_request', { requestId: activeRequestId, environmentId: activeEnvironmentId });
|
||||
await invoke('send_request', {
|
||||
requestId: activeRequestId,
|
||||
environmentId: activeEnvironmentId,
|
||||
});
|
||||
},
|
||||
[activeRequestId, activeEnvironmentId],
|
||||
);
|
||||
|
||||
@@ -1,26 +1,38 @@
|
||||
import classNames from 'classnames';
|
||||
import type { ForwardedRef } from 'react';
|
||||
import React, { forwardRef, Fragment, memo, useCallback, useMemo, useRef, useState } from 'react';
|
||||
import type { ForwardedRef, ReactNode } from 'react';
|
||||
import React, { forwardRef, Fragment, useCallback, useMemo, useRef, useState } from 'react';
|
||||
import type { XYCoord } from 'react-dnd';
|
||||
import { useDrag, useDrop } from 'react-dnd';
|
||||
import { useKey, useKeyPressEvent } from 'react-use';
|
||||
import { useActiveEnvironmentId } from '../hooks/useActiveEnvironmentId';
|
||||
import { useActiveRequestId } from '../hooks/useActiveRequestId';
|
||||
import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
|
||||
import { useAppRoutes } from '../hooks/useAppRoutes';
|
||||
import { useCreateFolder } from '../hooks/useCreateFolder';
|
||||
import { useCreateRequest } from '../hooks/useCreateRequest';
|
||||
import { useDeleteAnyRequest } from '../hooks/useDeleteAnyRequest';
|
||||
import { useDeleteFolder } from '../hooks/useDeleteFolder';
|
||||
import { useFolders } from '../hooks/useFolders';
|
||||
import { useKeyValue } from '../hooks/useKeyValue';
|
||||
import { useLatestResponse } from '../hooks/useLatestResponse';
|
||||
import { useRequests } from '../hooks/useRequests';
|
||||
import { useSidebarHidden } from '../hooks/useSidebarHidden';
|
||||
import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
|
||||
import { usePrompt } from '../hooks/usePrompt';
|
||||
import { useRequests } from '../hooks/useRequests';
|
||||
import { useSendAnyRequest } from '../hooks/useSendAnyRequest';
|
||||
import { useSidebarHidden } from '../hooks/useSidebarHidden';
|
||||
import { useUpdateAnyFolder } from '../hooks/useUpdateAnyFolder';
|
||||
import { useUpdateAnyRequest } from '../hooks/useUpdateAnyRequest';
|
||||
import { useUpdateRequest } from '../hooks/useUpdateRequest';
|
||||
import type { HttpRequest } from '../lib/models';
|
||||
import { NAMESPACE_NO_SYNC } from '../lib/keyValueStore';
|
||||
import type { Folder, HttpRequest, Workspace } from '../lib/models';
|
||||
import { isResponseLoading } from '../lib/models';
|
||||
import { Dropdown } from './core/Dropdown';
|
||||
import { Icon } from './core/Icon';
|
||||
import { IconButton } from './core/IconButton';
|
||||
import { InlineCode } from './core/InlineCode';
|
||||
import { VStack } from './core/Stacks';
|
||||
import { StatusTag } from './core/StatusTag';
|
||||
import { DropMarker } from './DropMarker';
|
||||
import { useActiveEnvironmentId } from '../hooks/useActiveEnvironmentId';
|
||||
import { useCreateRequest } from '../hooks/useCreateRequest';
|
||||
import { VStack } from './core/Stacks';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
@@ -30,59 +42,131 @@ enum ItemTypes {
|
||||
REQUEST = 'request',
|
||||
}
|
||||
|
||||
export const Sidebar = memo(function Sidebar({ className }: Props) {
|
||||
interface TreeNode {
|
||||
item: Workspace | Folder | HttpRequest;
|
||||
children: TreeNode[];
|
||||
depth: number;
|
||||
}
|
||||
|
||||
export function Sidebar({ className }: Props) {
|
||||
const { hidden } = useSidebarHidden();
|
||||
const createRequest = useCreateRequest();
|
||||
const sidebarRef = useRef<HTMLDivElement>(null);
|
||||
const sidebarRef = useRef<HTMLLIElement>(null);
|
||||
const activeRequestId = useActiveRequestId();
|
||||
const activeEnvironmentId = useActiveEnvironmentId();
|
||||
const unorderedRequests = useRequests();
|
||||
const requests = useRequests();
|
||||
const folders = useFolders();
|
||||
const deleteAnyRequest = useDeleteAnyRequest();
|
||||
const activeWorkspace = useActiveWorkspace();
|
||||
const routes = useAppRoutes();
|
||||
const requests = useMemo(
|
||||
() => [...unorderedRequests].sort((a, b) => a.sortPriority - b.sortPriority),
|
||||
[unorderedRequests],
|
||||
);
|
||||
const [hasFocus, setHasFocus] = useState<boolean>(false);
|
||||
const [selectedIndex, setSelectedIndex] = useState<number>();
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
const [selectedTree, setSelectedTree] = useState<TreeNode | null>(null);
|
||||
const updateAnyRequest = useUpdateAnyRequest();
|
||||
const updateAnyFolder = useUpdateAnyFolder();
|
||||
const [draggingId, setDraggingId] = useState<string | null>(null);
|
||||
const [hoveredTree, setHoveredTree] = useState<TreeNode | null>(null);
|
||||
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
|
||||
const collapsed = useKeyValue<Record<string, boolean>>({
|
||||
key: ['sidebar_collapsed', activeWorkspace?.id ?? 'n/a'],
|
||||
defaultValue: {},
|
||||
namespace: NAMESPACE_NO_SYNC,
|
||||
});
|
||||
|
||||
const { tree, treeParentMap, selectableRequests } = useMemo<{
|
||||
tree: TreeNode | null;
|
||||
treeParentMap: Record<string, TreeNode>;
|
||||
selectableRequests: { id: string; index: number; tree: TreeNode }[];
|
||||
}>(() => {
|
||||
const treeParentMap: Record<string, TreeNode> = {};
|
||||
const selectableRequests: { id: string; index: number; tree: TreeNode }[] = [];
|
||||
if (activeWorkspace == null) {
|
||||
return { tree: null, treeParentMap, selectableRequests };
|
||||
}
|
||||
|
||||
let selectableRequestIndex = 0;
|
||||
|
||||
// Put requests and folders into a tree structure
|
||||
const next = (node: TreeNode): TreeNode => {
|
||||
const childItems = [...requests, ...folders].filter((f) =>
|
||||
node.item.model === 'workspace' ? f.folderId == null : f.folderId === node.item.id,
|
||||
);
|
||||
|
||||
childItems.sort((a, b) => a.sortPriority - b.sortPriority);
|
||||
const depth = node.depth + 1;
|
||||
for (const item of childItems) {
|
||||
treeParentMap[item.id] = node;
|
||||
node.children.push(next({ item, children: [], depth }));
|
||||
if (item.model === 'http_request') {
|
||||
selectableRequests.push({ id: item.id, index: selectableRequestIndex++, tree: node });
|
||||
}
|
||||
}
|
||||
return node;
|
||||
};
|
||||
|
||||
const tree = next({ item: activeWorkspace, children: [], depth: 0 });
|
||||
|
||||
return { tree, treeParentMap, selectableRequests };
|
||||
}, [activeWorkspace, requests, folders]);
|
||||
|
||||
// TODO: Move these listeners to a central place
|
||||
useListenToTauriEvent('new_request', async () => createRequest.mutate({}));
|
||||
|
||||
const focusActiveRequest = useCallback(
|
||||
(forcedIndex?: number) => {
|
||||
const index = forcedIndex ?? requests.findIndex((r) => r.id === activeRequestId);
|
||||
if (index < 0) return;
|
||||
setSelectedIndex(index >= 0 ? index : undefined);
|
||||
(args: { forced?: { id: string; tree: TreeNode }; noFocusSidebar?: boolean } = {}) => {
|
||||
const { forced, noFocusSidebar } = args;
|
||||
const tree = forced?.tree ?? treeParentMap[activeRequestId ?? 'n/a'] ?? null;
|
||||
const children = tree?.children ?? [];
|
||||
const id = forced?.id ?? children.find((m) => m.item.id === activeRequestId)?.item.id ?? null;
|
||||
if (id == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
setSelectedId(id);
|
||||
setSelectedTree(tree);
|
||||
setHasFocus(true);
|
||||
sidebarRef.current?.focus();
|
||||
if (!noFocusSidebar) {
|
||||
sidebarRef.current?.focus();
|
||||
}
|
||||
},
|
||||
[activeRequestId, requests],
|
||||
[activeRequestId, treeParentMap],
|
||||
);
|
||||
|
||||
const handleSelect = useCallback(
|
||||
(requestId: string) => {
|
||||
const index = requests.findIndex((r) => r.id === requestId);
|
||||
const request = requests[index];
|
||||
if (!request) return;
|
||||
routes.navigate('request', {
|
||||
requestId,
|
||||
workspaceId: request.workspaceId,
|
||||
environmentId: activeEnvironmentId ?? undefined,
|
||||
});
|
||||
setSelectedIndex(index);
|
||||
focusActiveRequest(index);
|
||||
(id: string) => {
|
||||
const tree = treeParentMap[id ?? 'n/a'] ?? null;
|
||||
const children = tree?.children ?? [];
|
||||
const node = children.find((m) => m.item.id === id) ?? null;
|
||||
if (node == null || tree == null || node.item.model === 'workspace') {
|
||||
return;
|
||||
}
|
||||
|
||||
const { item } = node;
|
||||
|
||||
if (item.model === 'folder') {
|
||||
collapsed.set((c) => ({ ...c, [item.id]: !c[item.id] }));
|
||||
} else {
|
||||
routes.navigate('request', {
|
||||
requestId: id,
|
||||
workspaceId: item.workspaceId,
|
||||
environmentId: activeEnvironmentId ?? undefined,
|
||||
});
|
||||
setSelectedId(id);
|
||||
setSelectedTree(tree);
|
||||
focusActiveRequest({ forced: { id, tree } });
|
||||
}
|
||||
},
|
||||
[focusActiveRequest, requests, routes, activeEnvironmentId],
|
||||
[treeParentMap, collapsed, routes, activeEnvironmentId, focusActiveRequest],
|
||||
);
|
||||
|
||||
const handleClearSelected = useCallback(() => {
|
||||
setSelectedIndex(undefined);
|
||||
}, [setSelectedIndex]);
|
||||
setSelectedId(null);
|
||||
setSelectedTree(null);
|
||||
}, []);
|
||||
|
||||
const handleFocus = useCallback(() => {
|
||||
if (hasFocus) return;
|
||||
focusActiveRequest();
|
||||
focusActiveRequest({ noFocusSidebar: true });
|
||||
}, [focusActiveRequest, hasFocus]);
|
||||
|
||||
const handleBlur = useCallback(() => setHasFocus(false), []);
|
||||
@@ -92,11 +176,11 @@ export const Sidebar = memo(function Sidebar({ className }: Props) {
|
||||
if (!hasFocus) return;
|
||||
e.preventDefault();
|
||||
|
||||
const selectedRequest = requests[selectedIndex ?? -1];
|
||||
if (selectedRequest === undefined) return;
|
||||
deleteAnyRequest.mutate(selectedRequest.id);
|
||||
const selected = selectableRequests.find((r) => r.id === selectedId);
|
||||
if (selected == null) return;
|
||||
deleteAnyRequest.mutate(selected.id);
|
||||
},
|
||||
[deleteAnyRequest, hasFocus, requests, selectedIndex],
|
||||
[deleteAnyRequest, hasFocus, selectableRequests, selectedId],
|
||||
);
|
||||
|
||||
useKeyPressEvent('Backspace', handleDeleteKey);
|
||||
@@ -107,19 +191,26 @@ export const Sidebar = memo(function Sidebar({ className }: Props) {
|
||||
() => {
|
||||
if (hidden || hasFocus) return;
|
||||
// Select 0 index on focus if none selected
|
||||
focusActiveRequest(selectedIndex ?? 0);
|
||||
focusActiveRequest(
|
||||
selectedTree != null && selectedId != null
|
||||
? { forced: { id: selectedId, tree: selectedTree } }
|
||||
: undefined,
|
||||
);
|
||||
},
|
||||
[focusActiveRequest, hidden, activeRequestId],
|
||||
);
|
||||
|
||||
useKeyPressEvent('Enter', (e) => {
|
||||
if (!hasFocus) return;
|
||||
const request = requests[selectedIndex ?? -1];
|
||||
if (!request || request.id === activeRequestId) return;
|
||||
const selected = selectableRequests.find((r) => r.id === selectedId);
|
||||
if (!selected || selected.id === activeRequestId || activeWorkspace == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
routes.navigate('request', {
|
||||
requestId: request.id,
|
||||
workspaceId: request.workspaceId,
|
||||
requestId: selected.id,
|
||||
workspaceId: activeWorkspace?.id,
|
||||
environmentId: activeEnvironmentId ?? undefined,
|
||||
});
|
||||
});
|
||||
@@ -128,160 +219,287 @@ export const Sidebar = memo(function Sidebar({ className }: Props) {
|
||||
'ArrowUp',
|
||||
() => {
|
||||
if (!hasFocus) return;
|
||||
let newIndex = (selectedIndex ?? requests.length) - 1;
|
||||
if (newIndex < 0) {
|
||||
newIndex = requests.length - 1;
|
||||
const i = selectableRequests.findIndex((r) => r.id === selectedId);
|
||||
const newSelectable = selectableRequests[i - 1];
|
||||
if (newSelectable == null) {
|
||||
return;
|
||||
}
|
||||
setSelectedIndex(newIndex);
|
||||
|
||||
setSelectedId(newSelectable.id);
|
||||
setSelectedTree(newSelectable.tree);
|
||||
},
|
||||
undefined,
|
||||
[hasFocus, requests, selectedIndex],
|
||||
[hasFocus, selectableRequests, selectedId, setSelectedId, setSelectedTree],
|
||||
);
|
||||
|
||||
useKey(
|
||||
'ArrowDown',
|
||||
() => {
|
||||
if (!hasFocus) return;
|
||||
let newIndex = (selectedIndex ?? -1) + 1;
|
||||
if (newIndex > requests.length - 1) {
|
||||
newIndex = 0;
|
||||
const i = selectableRequests.findIndex((r) => r.id === selectedId);
|
||||
const newSelectable = selectableRequests[i + 1];
|
||||
if (newSelectable == null) {
|
||||
return;
|
||||
}
|
||||
setSelectedIndex(newIndex);
|
||||
|
||||
setSelectedId(newSelectable.id);
|
||||
setSelectedTree(newSelectable.tree);
|
||||
},
|
||||
undefined,
|
||||
[hasFocus, requests, selectedIndex],
|
||||
[hasFocus, selectableRequests, selectedId, setSelectedId, setSelectedTree],
|
||||
);
|
||||
|
||||
return (
|
||||
<div aria-hidden={hidden} className="h-full">
|
||||
<VStack
|
||||
as="ul"
|
||||
role="menu"
|
||||
aria-orientation="vertical"
|
||||
dir="ltr"
|
||||
ref={sidebarRef}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
tabIndex={hidden ? -1 : 0}
|
||||
className={classNames(
|
||||
className,
|
||||
'h-full pb-3 overflow-y-scroll overflow-x-visible hide-scrollbars pt-2',
|
||||
)}
|
||||
>
|
||||
<SidebarItems
|
||||
selectedIndex={selectedIndex}
|
||||
requests={requests}
|
||||
focused={hasFocus}
|
||||
onSelect={handleSelect}
|
||||
onClearSelected={handleClearSelected}
|
||||
/>
|
||||
</VStack>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
interface SidebarItemsProps {
|
||||
requests: HttpRequest[];
|
||||
focused: boolean;
|
||||
selectedIndex?: number;
|
||||
onSelect: (requestId: string) => void;
|
||||
onClearSelected: () => void;
|
||||
}
|
||||
|
||||
function SidebarItems({
|
||||
requests,
|
||||
focused,
|
||||
selectedIndex,
|
||||
onSelect,
|
||||
onClearSelected,
|
||||
}: SidebarItemsProps) {
|
||||
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
|
||||
const updateRequest = useUpdateAnyRequest();
|
||||
|
||||
const handleMove = useCallback<DraggableSidebarItemProps['onMove']>(
|
||||
(id, side) => {
|
||||
const dragIndex = requests.findIndex((r) => r.id === id);
|
||||
setHoveredIndex(side === 'above' ? dragIndex : dragIndex + 1);
|
||||
const hoveredTree = treeParentMap[id] ?? null;
|
||||
const dragIndex = hoveredTree?.children.findIndex((n) => n.item.id === id) ?? -99;
|
||||
const hoveredIndex = dragIndex + (side === 'above' ? 0 : 1);
|
||||
setHoveredTree(hoveredTree);
|
||||
setHoveredIndex(hoveredIndex);
|
||||
},
|
||||
[requests],
|
||||
[treeParentMap],
|
||||
);
|
||||
|
||||
const handleDragStart = useCallback<DraggableSidebarItemProps['onDragStart']>((id: string) => {
|
||||
setDraggingId(id);
|
||||
}, []);
|
||||
|
||||
const handleEnd = useCallback<DraggableSidebarItemProps['onEnd']>(
|
||||
(requestId) => {
|
||||
if (hoveredIndex === null) return;
|
||||
setHoveredIndex(null);
|
||||
onClearSelected();
|
||||
async (itemId) => {
|
||||
setHoveredTree(null);
|
||||
handleClearSelected();
|
||||
|
||||
const index = requests.findIndex((r) => r.id === requestId);
|
||||
const request = requests[index];
|
||||
if (request === undefined) return;
|
||||
if (hoveredTree == null || hoveredIndex == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newRequests = requests.filter((r) => r.id !== requestId);
|
||||
if (hoveredIndex > index) newRequests.splice(hoveredIndex - 1, 0, request);
|
||||
else newRequests.splice(hoveredIndex, 0, request);
|
||||
const parentTree = treeParentMap[itemId] ?? null;
|
||||
const index = parentTree?.children.findIndex((n) => n.item.id === itemId) ?? -1;
|
||||
const child = parentTree?.children[index ?? -1];
|
||||
if (child == null || parentTree == null) return;
|
||||
|
||||
// Do a simple find because the math is too hard
|
||||
const newIndex = newRequests.findIndex((r) => r.id === requestId) ?? 0;
|
||||
const beforePriority = newRequests[newIndex - 1]?.sortPriority ?? 0;
|
||||
const afterPriority = newRequests[newIndex + 1]?.sortPriority ?? 0;
|
||||
const movedToDifferentTree = hoveredTree.item.id !== parentTree.item.id;
|
||||
const movedUpInSameTree = !movedToDifferentTree && hoveredIndex < index;
|
||||
|
||||
const newChildren = hoveredTree.children.filter((c) => c.item.id !== itemId);
|
||||
if (movedToDifferentTree || movedUpInSameTree) {
|
||||
// Moving up or into a new tree is simply inserting before the hovered item
|
||||
newChildren.splice(hoveredIndex, 0, child);
|
||||
} else {
|
||||
// Moving down has to account for the fact that the original item will be removed
|
||||
newChildren.splice(hoveredIndex - 1, 0, child);
|
||||
}
|
||||
|
||||
const prev = newChildren[hoveredIndex - 1]?.item;
|
||||
const next = newChildren[hoveredIndex + 1]?.item;
|
||||
const beforePriority = prev == null || prev.model === 'workspace' ? 0 : prev.sortPriority;
|
||||
const afterPriority = next == null || next.model === 'workspace' ? 0 : next.sortPriority;
|
||||
|
||||
const folderId = hoveredTree.item.model === 'folder' ? hoveredTree.item.id : null;
|
||||
const shouldUpdateAll = afterPriority - beforePriority < 1;
|
||||
|
||||
if (shouldUpdateAll) {
|
||||
newRequests.forEach(({ id }, i) => {
|
||||
const sortPriority = i * 1000;
|
||||
const update = (r: HttpRequest) => ({ ...r, sortPriority });
|
||||
updateRequest.mutate({ id, update });
|
||||
});
|
||||
await Promise.all(
|
||||
newChildren.map((child, i) => {
|
||||
const sortPriority = i * 1000;
|
||||
if (child.item.model === 'folder') {
|
||||
const updateFolder = (f: Folder) => ({ ...f, sortPriority, folderId });
|
||||
return updateAnyFolder.mutateAsync({ id: child.item.id, update: updateFolder });
|
||||
} else if (child.item.model === 'http_request') {
|
||||
const updateRequest = (r: HttpRequest) => ({ ...r, sortPriority, folderId });
|
||||
return updateAnyRequest.mutateAsync({ id: child.item.id, update: updateRequest });
|
||||
}
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
const sortPriority = afterPriority - (afterPriority - beforePriority) / 2;
|
||||
const update = (r: HttpRequest) => ({ ...r, sortPriority });
|
||||
updateRequest.mutate({ id: requestId, update });
|
||||
if (child.item.model === 'folder') {
|
||||
const updateFolder = (f: Folder) => ({ ...f, sortPriority, folderId });
|
||||
await updateAnyFolder.mutateAsync({ id: child.item.id, update: updateFolder });
|
||||
} else if (child.item.model === 'http_request') {
|
||||
const updateRequest = (r: HttpRequest) => ({ ...r, sortPriority, folderId });
|
||||
await updateAnyRequest.mutateAsync({ id: child.item.id, update: updateRequest });
|
||||
}
|
||||
}
|
||||
setDraggingId(null);
|
||||
},
|
||||
[hoveredIndex, requests, updateRequest, onClearSelected],
|
||||
[
|
||||
hoveredIndex,
|
||||
hoveredTree,
|
||||
handleClearSelected,
|
||||
treeParentMap,
|
||||
updateAnyFolder,
|
||||
updateAnyRequest,
|
||||
],
|
||||
);
|
||||
|
||||
if (tree == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (collapsed.value == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{requests.map((r, i) => (
|
||||
<Fragment key={r.id}>
|
||||
{hoveredIndex === i && <DropMarker />}
|
||||
<aside
|
||||
aria-hidden={hidden}
|
||||
ref={sidebarRef}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
tabIndex={hidden ? -1 : 0}
|
||||
className={classNames(
|
||||
className,
|
||||
'h-full pb-3 overflow-y-scroll overflow-x-visible hide-scrollbars pt-2',
|
||||
)}
|
||||
>
|
||||
<SidebarItems
|
||||
treeParentMap={treeParentMap}
|
||||
selectedId={selectedId}
|
||||
selectedTree={selectedTree}
|
||||
collapsed={collapsed.value}
|
||||
tree={tree}
|
||||
focused={hasFocus}
|
||||
draggingId={draggingId}
|
||||
onSelect={handleSelect}
|
||||
hoveredIndex={hoveredIndex}
|
||||
hoveredTree={hoveredTree}
|
||||
handleMove={handleMove}
|
||||
handleEnd={handleEnd}
|
||||
handleDragStart={handleDragStart}
|
||||
/>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
interface SidebarItemsProps {
|
||||
tree: TreeNode;
|
||||
focused: boolean;
|
||||
draggingId: string | null;
|
||||
selectedId: string | null;
|
||||
selectedTree: TreeNode | null;
|
||||
treeParentMap: Record<string, TreeNode>;
|
||||
hoveredTree: TreeNode | null;
|
||||
hoveredIndex: number | null;
|
||||
handleMove: (id: string, side: 'above' | 'below') => void;
|
||||
handleEnd: (id: string) => void;
|
||||
handleDragStart: (id: string) => void;
|
||||
onSelect: (requestId: string) => void;
|
||||
collapsed: Record<string, boolean>;
|
||||
}
|
||||
|
||||
function SidebarItems({
|
||||
tree,
|
||||
focused,
|
||||
selectedId,
|
||||
selectedTree,
|
||||
draggingId,
|
||||
onSelect,
|
||||
treeParentMap,
|
||||
collapsed,
|
||||
hoveredTree,
|
||||
hoveredIndex,
|
||||
handleEnd,
|
||||
handleMove,
|
||||
handleDragStart,
|
||||
}: SidebarItemsProps) {
|
||||
return (
|
||||
<VStack
|
||||
as="ul"
|
||||
role="menu"
|
||||
aria-orientation="vertical"
|
||||
dir="ltr"
|
||||
className={classNames(
|
||||
tree.depth > 0 && 'border-l border-highlight',
|
||||
tree.depth === 0 && 'ml-0',
|
||||
tree.depth >= 1 && 'ml-[1.3em]',
|
||||
)}
|
||||
>
|
||||
{tree.children.map((child, i) => (
|
||||
<Fragment key={child.item.id}>
|
||||
{hoveredIndex === i && hoveredTree?.item.id === tree.item.id && <DropMarker />}
|
||||
<DraggableSidebarItem
|
||||
key={r.id}
|
||||
selected={selectedIndex === i}
|
||||
requestId={r.id}
|
||||
requestName={r.name}
|
||||
selected={selectedId === child.item.id}
|
||||
itemId={child.item.id}
|
||||
itemName={child.item.name}
|
||||
itemModel={child.item.model}
|
||||
onMove={handleMove}
|
||||
onEnd={handleEnd}
|
||||
useProminentStyles={focused}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
onDragStart={handleDragStart}
|
||||
useProminentStyles={focused}
|
||||
collapsed={collapsed}
|
||||
child={child}
|
||||
>
|
||||
{child.item.model === 'folder' &&
|
||||
!collapsed[child.item.id] &&
|
||||
draggingId !== child.item.id && (
|
||||
<SidebarItems
|
||||
treeParentMap={treeParentMap}
|
||||
tree={child}
|
||||
collapsed={collapsed}
|
||||
draggingId={draggingId}
|
||||
hoveredTree={hoveredTree}
|
||||
hoveredIndex={hoveredIndex}
|
||||
focused={focused}
|
||||
selectedId={selectedId}
|
||||
selectedTree={selectedTree}
|
||||
onSelect={onSelect}
|
||||
handleMove={handleMove}
|
||||
handleEnd={handleEnd}
|
||||
handleDragStart={handleDragStart}
|
||||
/>
|
||||
)}
|
||||
</DraggableSidebarItem>
|
||||
</Fragment>
|
||||
))}
|
||||
{hoveredIndex === requests.length && <DropMarker />}
|
||||
</>
|
||||
{hoveredIndex === tree.children.length && hoveredTree?.item.id === tree.item.id && (
|
||||
<DropMarker />
|
||||
)}
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
|
||||
type SidebarItemProps = {
|
||||
className?: string;
|
||||
requestId: string;
|
||||
requestName: string;
|
||||
itemId: string;
|
||||
itemName: string;
|
||||
itemModel: string;
|
||||
useProminentStyles?: boolean;
|
||||
selected?: boolean;
|
||||
onSelect: (requestId: string) => void;
|
||||
onSelect: (id: string) => void;
|
||||
draggable?: boolean;
|
||||
children?: ReactNode;
|
||||
collapsed: Record<string, boolean>;
|
||||
child: TreeNode;
|
||||
};
|
||||
|
||||
const _SidebarItem = forwardRef(function SidebarItem(
|
||||
{ className, requestName, requestId, useProminentStyles, selected, onSelect }: SidebarItemProps,
|
||||
const SidebarItem = forwardRef(function SidebarItem(
|
||||
{
|
||||
children,
|
||||
className,
|
||||
itemName,
|
||||
itemId,
|
||||
itemModel,
|
||||
useProminentStyles,
|
||||
selected,
|
||||
onSelect,
|
||||
collapsed,
|
||||
child,
|
||||
}: SidebarItemProps,
|
||||
ref: ForwardedRef<HTMLLIElement>,
|
||||
) {
|
||||
const latestResponse = useLatestResponse(requestId);
|
||||
const updateRequest = useUpdateRequest(requestId);
|
||||
const sendAnyRequest = useSendAnyRequest();
|
||||
const createRequest = useCreateRequest();
|
||||
const createFolder = useCreateFolder();
|
||||
const deleteRequest = useDeleteFolder(itemId);
|
||||
const latestResponse = useLatestResponse(itemId);
|
||||
const updateRequest = useUpdateRequest(itemId);
|
||||
const updateAnyFolder = useUpdateAnyFolder();
|
||||
const prompt = usePrompt();
|
||||
const [editing, setEditing] = useState<boolean>(false);
|
||||
const activeRequestId = useActiveRequestId();
|
||||
const isActive = activeRequestId === requestId;
|
||||
const isActive = activeRequestId === itemId;
|
||||
|
||||
const handleSubmitNameEdit = useCallback(
|
||||
(el: HTMLInputElement) => {
|
||||
@@ -313,7 +531,10 @@ const _SidebarItem = forwardRef(function SidebarItem(
|
||||
[handleSubmitNameEdit],
|
||||
);
|
||||
|
||||
const handleStartEditing = useCallback(() => setEditing(true), [setEditing]);
|
||||
const handleStartEditing = useCallback(() => {
|
||||
if (itemModel !== 'http_request') return;
|
||||
setEditing(true);
|
||||
}, [setEditing, itemModel]);
|
||||
|
||||
const handleBlur = useCallback(
|
||||
(e: React.FocusEvent<HTMLInputElement>) => {
|
||||
@@ -323,70 +544,153 @@ const _SidebarItem = forwardRef(function SidebarItem(
|
||||
);
|
||||
|
||||
const handleSelect = useCallback(() => {
|
||||
onSelect(requestId);
|
||||
}, [onSelect, requestId]);
|
||||
onSelect(itemId);
|
||||
}, [onSelect, itemId]);
|
||||
|
||||
return (
|
||||
<li ref={ref} className={classNames(className, 'block group/item px-2 pb-0.5')}>
|
||||
<button
|
||||
// tabIndex={-1} // Will prevent drag-n-drop
|
||||
onClick={handleSelect}
|
||||
disabled={editing}
|
||||
onDoubleClick={handleStartEditing}
|
||||
data-active={isActive}
|
||||
data-selected={selected}
|
||||
className={classNames(
|
||||
'w-full flex items-center text-sm h-xs px-2 rounded-md transition-colors',
|
||||
editing && 'ring-1 focus-within:ring-focus',
|
||||
isActive && 'bg-highlight text-gray-800',
|
||||
!isActive && 'text-gray-600 group-hover/item:text-gray-800 active:bg-highlightSecondary',
|
||||
selected && useProminentStyles && '!bg-violet-500/20 text-gray-900',
|
||||
<li ref={ref}>
|
||||
<div className={classNames(className, 'block relative group/item px-2 pb-0.5')}>
|
||||
{itemModel === 'folder' && (
|
||||
<Dropdown
|
||||
items={[
|
||||
{
|
||||
key: 'sendAll',
|
||||
label: 'Send All',
|
||||
leftSlot: <Icon icon="paperPlane" />,
|
||||
onSelect: () => {
|
||||
for (const { item } of child.children) {
|
||||
if (item.model === 'http_request') {
|
||||
sendAnyRequest.mutate(item.id);
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
{ type: 'separator', label: itemName },
|
||||
{
|
||||
key: 'rename',
|
||||
label: 'Rename',
|
||||
leftSlot: <Icon icon="pencil" />,
|
||||
onSelect: async () => {
|
||||
const name = await prompt({
|
||||
title: 'Rename Folder',
|
||||
description: (
|
||||
<>
|
||||
Enter a new name for <InlineCode>{itemName}</InlineCode>
|
||||
</>
|
||||
),
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
defaultValue: itemName,
|
||||
});
|
||||
updateAnyFolder.mutate({ id: itemId, update: (f) => ({ ...f, name }) });
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'deleteFolder',
|
||||
label: 'Delete',
|
||||
variant: 'danger',
|
||||
leftSlot: <Icon icon="trash" />,
|
||||
onSelect: () => deleteRequest.mutate(),
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
key: 'createRequest',
|
||||
label: 'New Request',
|
||||
leftSlot: <Icon icon="plus" />,
|
||||
onSelect: () => createRequest.mutate({ folderId: itemId, sortPriority: -1 }),
|
||||
},
|
||||
{
|
||||
key: 'createFolder',
|
||||
label: 'New Folder',
|
||||
leftSlot: <Icon icon="plus" />,
|
||||
onSelect: () => createFolder.mutate({ folderId: itemId, sortPriority: -1 }),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<IconButton
|
||||
title="Folder options"
|
||||
size="xs"
|
||||
icon="dotsV"
|
||||
className="ml-auto !bg-transparent absolute right-2 opacity-0 group-hover/item:opacity-70 transition-opacity"
|
||||
/>
|
||||
</Dropdown>
|
||||
)}
|
||||
>
|
||||
{editing ? (
|
||||
<input
|
||||
ref={handleFocus}
|
||||
defaultValue={requestName}
|
||||
className="bg-transparent outline-none w-full"
|
||||
onBlur={handleBlur}
|
||||
onKeyDown={handleInputKeyDown}
|
||||
/>
|
||||
) : (
|
||||
<span className={classNames('truncate', !requestName && 'text-gray-400 italic')}>
|
||||
{requestName || 'New Request'}
|
||||
</span>
|
||||
)}
|
||||
{latestResponse && (
|
||||
<div className="ml-auto">
|
||||
{isResponseLoading(latestResponse) ? (
|
||||
<Icon spin size="sm" icon="update" />
|
||||
) : (
|
||||
<StatusTag className="text-2xs dark:opacity-80" response={latestResponse} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
// tabIndex={-1} // Will prevent drag-n-drop
|
||||
onClick={handleSelect}
|
||||
disabled={editing}
|
||||
onDoubleClick={handleStartEditing}
|
||||
data-active={isActive}
|
||||
data-selected={selected}
|
||||
className={classNames(
|
||||
'w-full flex items-center text-sm h-xs px-2 rounded-md transition-colors',
|
||||
editing && 'ring-1 focus-within:ring-focus',
|
||||
isActive && 'bg-highlightSecondary text-gray-800',
|
||||
!isActive &&
|
||||
'text-gray-600 group-hover/item:text-gray-800 active:bg-highlightSecondary',
|
||||
selected && useProminentStyles && '!bg-violet-400/20 text-gray-950',
|
||||
)}
|
||||
>
|
||||
{itemModel === 'folder' && (
|
||||
<Icon
|
||||
size="sm"
|
||||
icon="chevronRight"
|
||||
className={classNames(
|
||||
'-ml-0.5 mr-2 transition-transform',
|
||||
!collapsed[itemId] && 'transform rotate-90',
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{editing ? (
|
||||
<input
|
||||
ref={handleFocus}
|
||||
defaultValue={itemName}
|
||||
className="bg-transparent outline-none w-full"
|
||||
onBlur={handleBlur}
|
||||
onKeyDown={handleInputKeyDown}
|
||||
/>
|
||||
) : (
|
||||
<span className={classNames('truncate', !itemName && 'text-gray-400 italic')}>
|
||||
{itemName || 'New Request'}
|
||||
</span>
|
||||
)}
|
||||
{latestResponse && (
|
||||
<div className="ml-auto">
|
||||
{isResponseLoading(latestResponse) ? (
|
||||
<Icon spin size="sm" icon="update" />
|
||||
) : (
|
||||
<StatusTag className="text-2xs dark:opacity-80" response={latestResponse} />
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
{children}
|
||||
</li>
|
||||
);
|
||||
});
|
||||
|
||||
const SidebarItem = memo(_SidebarItem);
|
||||
|
||||
type DraggableSidebarItemProps = SidebarItemProps & {
|
||||
onMove: (id: string, side: 'above' | 'below') => void;
|
||||
onEnd: (id: string) => void;
|
||||
onDragStart: (id: string) => void;
|
||||
children?: ReactNode;
|
||||
child?: TreeNode;
|
||||
};
|
||||
|
||||
type DragItem = {
|
||||
id: string;
|
||||
requestName: string;
|
||||
itemName: string;
|
||||
};
|
||||
|
||||
const DraggableSidebarItem = memo(function DraggableSidebarItem({
|
||||
requestName,
|
||||
requestId,
|
||||
function DraggableSidebarItem({
|
||||
itemName,
|
||||
itemId,
|
||||
itemModel,
|
||||
child,
|
||||
onMove,
|
||||
onEnd,
|
||||
onDragStart,
|
||||
...props
|
||||
}: DraggableSidebarItemProps) {
|
||||
const ref = useRef<HTMLLIElement>(null);
|
||||
@@ -400,7 +704,7 @@ const DraggableSidebarItem = memo(function DraggableSidebarItem({
|
||||
const hoverMiddleY = (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
|
||||
const clientOffset = monitor.getClientOffset();
|
||||
const hoverClientY = (clientOffset as XYCoord).y - hoverBoundingRect.top;
|
||||
onMove(requestId, hoverClientY < hoverMiddleY ? 'above' : 'below');
|
||||
onMove(itemId, hoverClientY < hoverMiddleY ? 'above' : 'below');
|
||||
},
|
||||
},
|
||||
[onMove],
|
||||
@@ -409,25 +713,29 @@ const DraggableSidebarItem = memo(function DraggableSidebarItem({
|
||||
const [{ isDragging }, connectDrag] = useDrag<DragItem, unknown, { isDragging: boolean }>(
|
||||
() => ({
|
||||
type: ItemTypes.REQUEST,
|
||||
item: () => ({ id: requestId, requestName }),
|
||||
item: () => {
|
||||
onDragStart(itemId);
|
||||
return { id: itemId, itemName };
|
||||
},
|
||||
collect: (m) => ({ isDragging: m.isDragging() }),
|
||||
options: { dropEffect: 'move' },
|
||||
end: () => onEnd(requestId),
|
||||
end: () => onEnd(itemId),
|
||||
}),
|
||||
[onEnd],
|
||||
);
|
||||
|
||||
connectDrag(ref);
|
||||
connectDrop(ref);
|
||||
connectDrag(connectDrop(ref));
|
||||
|
||||
return (
|
||||
<SidebarItem
|
||||
ref={ref}
|
||||
draggable
|
||||
className={classNames(isDragging && 'opacity-20')}
|
||||
requestName={requestName}
|
||||
requestId={requestId}
|
||||
itemName={itemName}
|
||||
itemId={itemId}
|
||||
itemModel={itemModel}
|
||||
child={child}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { memo } from 'react';
|
||||
import { useSidebarHidden } from '../hooks/useSidebarHidden';
|
||||
import { IconButton } from './core/IconButton';
|
||||
import { useCreateFolder } from '../hooks/useCreateFolder';
|
||||
import { useCreateRequest } from '../hooks/useCreateRequest';
|
||||
import { useSidebarHidden } from '../hooks/useSidebarHidden';
|
||||
import { Dropdown } from './core/Dropdown';
|
||||
import { IconButton } from './core/IconButton';
|
||||
import { HStack } from './core/Stacks';
|
||||
|
||||
export const SidebarActions = memo(function SidebarActions() {
|
||||
const createRequest = useCreateRequest();
|
||||
const createFolder = useCreateFolder();
|
||||
const { hidden, toggle } = useSidebarHidden();
|
||||
|
||||
return (
|
||||
@@ -19,12 +22,22 @@ export const SidebarActions = memo(function SidebarActions() {
|
||||
icon={hidden ? 'leftPanelHidden' : 'leftPanelVisible'}
|
||||
/>
|
||||
)}
|
||||
<IconButton
|
||||
size="sm"
|
||||
icon="plusCircle"
|
||||
title="Create Request"
|
||||
onClick={() => createRequest.mutate({})}
|
||||
/>
|
||||
<Dropdown
|
||||
items={[
|
||||
{
|
||||
key: 'create-request',
|
||||
label: 'Create Request',
|
||||
onSelect: () => createRequest.mutate({}),
|
||||
},
|
||||
{
|
||||
key: 'create-folder',
|
||||
label: 'Create Folder',
|
||||
onSelect: () => createFolder.mutate({}),
|
||||
},
|
||||
]}
|
||||
>
|
||||
<IconButton size="sm" icon="plusCircle" title="Add Resource" />
|
||||
</Dropdown>
|
||||
</HStack>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -3,9 +3,9 @@ import type { EditorView } from 'codemirror';
|
||||
import type { FormEvent } from 'react';
|
||||
import { memo, useCallback, useRef, useState } from 'react';
|
||||
import { useIsResponseLoading } from '../hooks/useIsResponseLoading';
|
||||
import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
|
||||
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
|
||||
import { useSendRequest } from '../hooks/useSendRequest';
|
||||
import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent';
|
||||
import { useUpdateRequest } from '../hooks/useUpdateRequest';
|
||||
import type { HttpRequest } from '../lib/models';
|
||||
import { IconButton } from './core/IconButton';
|
||||
@@ -35,7 +35,7 @@ export const UrlBar = memo(function UrlBar({ id: requestId, url, method, classNa
|
||||
const handleSubmit = useCallback(
|
||||
async (e: FormEvent) => {
|
||||
e.preventDefault();
|
||||
sendRequest();
|
||||
sendRequest.mutate();
|
||||
},
|
||||
[sendRequest],
|
||||
);
|
||||
|
||||
@@ -1,24 +1,28 @@
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { open } from '@tauri-apps/api/dialog';
|
||||
import classNames from 'classnames';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
|
||||
import { useAppRoutes } from '../hooks/useAppRoutes';
|
||||
import { useCreateWorkspace } from '../hooks/useCreateWorkspace';
|
||||
import { useDeleteWorkspace } from '../hooks/useDeleteWorkspace';
|
||||
import { usePrompt } from '../hooks/usePrompt';
|
||||
import { getRecentEnvironments } from '../hooks/useRecentEnvironments';
|
||||
import { useTheme } from '../hooks/useTheme';
|
||||
import { useUpdateWorkspace } from '../hooks/useUpdateWorkspace';
|
||||
import { useWorkspaces } from '../hooks/useWorkspaces';
|
||||
import { Button } from './core/Button';
|
||||
import type { Environment, Folder, HttpRequest, Workspace } from '../lib/models';
|
||||
import { pluralize } from '../lib/pluralize';
|
||||
import type { ButtonProps } from './core/Button';
|
||||
import { Button } from './core/Button';
|
||||
import type { DropdownItem } from './core/Dropdown';
|
||||
import { Dropdown } from './core/Dropdown';
|
||||
import { Icon } from './core/Icon';
|
||||
import { InlineCode } from './core/InlineCode';
|
||||
import { HStack } from './core/Stacks';
|
||||
import { HStack, VStack } from './core/Stacks';
|
||||
import { useDialog } from './DialogContext';
|
||||
import { getRecentEnvironments } from '../hooks/useRecentEnvironments';
|
||||
|
||||
type Props = Pick<ButtonProps, 'className' | 'justify' | 'forDropdown'>;
|
||||
type Props = Pick<ButtonProps, 'className' | 'justify' | 'forDropdown' | 'leftSlot'>;
|
||||
|
||||
export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
|
||||
className,
|
||||
@@ -30,10 +34,72 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
|
||||
const createWorkspace = useCreateWorkspace({ navigateAfter: true });
|
||||
const updateWorkspace = useUpdateWorkspace(activeWorkspaceId);
|
||||
const deleteWorkspace = useDeleteWorkspace(activeWorkspace);
|
||||
const { appearance, toggleAppearance } = useTheme();
|
||||
const dialog = useDialog();
|
||||
const prompt = usePrompt();
|
||||
const routes = useAppRoutes();
|
||||
|
||||
const importData = useCallback(async () => {
|
||||
const selected = await open({
|
||||
multiple: true,
|
||||
filters: [
|
||||
{
|
||||
name: 'Export File',
|
||||
extensions: ['json'],
|
||||
},
|
||||
],
|
||||
});
|
||||
if (selected == null || selected.length === 0) return;
|
||||
const imported: {
|
||||
workspaces: Workspace[];
|
||||
environments: Environment[];
|
||||
folders: Folder[];
|
||||
requests: HttpRequest[];
|
||||
} = await invoke('import_data', {
|
||||
filePaths: selected,
|
||||
});
|
||||
const importedWorkspace = imported.workspaces[0];
|
||||
|
||||
dialog.show({
|
||||
title: 'Import Complete',
|
||||
size: 'dynamic',
|
||||
hideX: true,
|
||||
render: ({ hide }) => {
|
||||
const { workspaces, environments, folders, requests } = imported;
|
||||
return (
|
||||
<VStack space={3}>
|
||||
<ul className="list-disc pl-6">
|
||||
<li>
|
||||
{workspaces.length} {pluralize('Workspace', workspaces.length)}
|
||||
</li>
|
||||
<li>
|
||||
{environments.length} {pluralize('Environment', environments.length)}
|
||||
</li>
|
||||
<li>
|
||||
{folders.length} {pluralize('Folder', folders.length)}
|
||||
</li>
|
||||
<li>
|
||||
{requests.length} {pluralize('Request', requests.length)}
|
||||
</li>
|
||||
</ul>
|
||||
<div>
|
||||
<Button className="ml-auto" onClick={hide} color="primary">
|
||||
Done
|
||||
</Button>
|
||||
</div>
|
||||
</VStack>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
if (importedWorkspace != null) {
|
||||
routes.navigate('workspace', {
|
||||
workspaceId: importedWorkspace.id,
|
||||
environmentId: imported.environments[0]?.id,
|
||||
});
|
||||
}
|
||||
}, [routes, dialog]);
|
||||
|
||||
const items: DropdownItem[] = useMemo(() => {
|
||||
const workspaceItems: DropdownItem[] = workspaces.map((w) => ({
|
||||
key: w.id,
|
||||
@@ -51,7 +117,7 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
|
||||
),
|
||||
render: ({ hide }) => {
|
||||
return (
|
||||
<HStack space={2} justifyContent="end" className="mt-6">
|
||||
<HStack space={2} justifyContent="end" alignItems="center" className="mt-6">
|
||||
<Button
|
||||
className="focus"
|
||||
color="gray"
|
||||
@@ -133,22 +199,36 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
|
||||
const name = await prompt({
|
||||
name: 'name',
|
||||
label: 'Name',
|
||||
defaultValue: '',
|
||||
description: 'Enter a name for the new workspace',
|
||||
defaultValue: 'My Workspace',
|
||||
title: 'Create Workspace',
|
||||
});
|
||||
createWorkspace.mutate({ name });
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'import',
|
||||
label: 'Import Data',
|
||||
onSelect: importData,
|
||||
leftSlot: <Icon icon="download" />,
|
||||
},
|
||||
{
|
||||
key: 'appearance',
|
||||
label: 'Toggle Theme',
|
||||
onSelect: toggleAppearance,
|
||||
leftSlot: <Icon icon={appearance === 'dark' ? 'sun' : 'moon'} />,
|
||||
},
|
||||
];
|
||||
}, [
|
||||
activeWorkspace?.name,
|
||||
activeWorkspaceId,
|
||||
appearance,
|
||||
createWorkspace,
|
||||
deleteWorkspace.mutate,
|
||||
dialog,
|
||||
importData,
|
||||
prompt,
|
||||
routes,
|
||||
toggleAppearance,
|
||||
updateWorkspace,
|
||||
workspaces,
|
||||
]);
|
||||
@@ -156,7 +236,6 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
|
||||
return (
|
||||
<Dropdown items={items}>
|
||||
<Button
|
||||
forDropdown
|
||||
size="sm"
|
||||
className={classNames(className, 'text-gray-800 !px-2 truncate')}
|
||||
{...buttonProps}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import classNames from 'classnames';
|
||||
import { memo } from 'react';
|
||||
import React, { memo } from 'react';
|
||||
import { useActiveRequest } from '../hooks/useActiveRequest';
|
||||
import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
|
||||
import { Icon } from './core/Icon';
|
||||
import { IconButton } from './core/IconButton';
|
||||
import { HStack } from './core/Stacks';
|
||||
import { EnvironmentActionsDropdown } from './EnvironmentActionsDropdown';
|
||||
import { RecentRequestsDropdown } from './RecentRequestsDropdown';
|
||||
import { RequestActionsDropdown } from './RequestActionsDropdown';
|
||||
import { SidebarActions } from './SidebarActions';
|
||||
import { EnvironmentActionsDropdown } from './EnvironmentActionsDropdown';
|
||||
import { WorkspaceActionsDropdown } from './WorkspaceActionsDropdown';
|
||||
|
||||
interface Props {
|
||||
@@ -15,6 +17,7 @@ interface Props {
|
||||
|
||||
export const WorkspaceHeader = memo(function WorkspaceHeader({ className }: Props) {
|
||||
const activeRequest = useActiveRequest();
|
||||
const activeWorkspace = useActiveWorkspace();
|
||||
|
||||
return (
|
||||
<HStack
|
||||
@@ -24,23 +27,30 @@ export const WorkspaceHeader = memo(function WorkspaceHeader({ className }: Prop
|
||||
>
|
||||
<HStack space={0.5} className="flex-1 pointer-events-none" alignItems="center">
|
||||
<SidebarActions />
|
||||
<WorkspaceActionsDropdown />
|
||||
<EnvironmentActionsDropdown className="pointer-events-auto" />
|
||||
<HStack alignItems="center">
|
||||
<WorkspaceActionsDropdown
|
||||
leftSlot={
|
||||
<div className="w-4 h-4 leading-4 rounded text-[0.8em] bg-[#1B88DE] bg-opacity-80 text-white mr-1">
|
||||
{activeWorkspace?.name[0]?.toUpperCase()}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<Icon icon="chevronRight" className="text-gray-900 text-opacity-disabled" />
|
||||
<EnvironmentActionsDropdown className="w-auto pointer-events-auto" />
|
||||
</HStack>
|
||||
</HStack>
|
||||
<div className="pointer-events-none">
|
||||
<RecentRequestsDropdown />
|
||||
</div>
|
||||
<div className="flex-1 flex justify-end -mr-2 pointer-events-none">
|
||||
{activeRequest && (
|
||||
<RequestActionsDropdown requestId={activeRequest?.id}>
|
||||
<IconButton
|
||||
size="sm"
|
||||
title="Request Options"
|
||||
icon="gear"
|
||||
className="pointer-events-auto"
|
||||
/>
|
||||
</RequestActionsDropdown>
|
||||
)}
|
||||
<RequestActionsDropdown requestId={activeRequest?.id ?? null}>
|
||||
<IconButton
|
||||
size="sm"
|
||||
title="Request Options"
|
||||
icon="gear"
|
||||
className="pointer-events-auto"
|
||||
/>
|
||||
</RequestActionsDropdown>
|
||||
</div>
|
||||
</HStack>
|
||||
);
|
||||
|
||||
@@ -51,6 +51,7 @@ const _Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
|
||||
() =>
|
||||
classNames(
|
||||
className,
|
||||
'max-w-full min-w-0', // Help with truncation
|
||||
'whitespace-nowrap outline-none',
|
||||
'flex-shrink-0 flex items-center',
|
||||
'focus-visible-or-class:ring rounded-md',
|
||||
@@ -72,7 +73,15 @@ const _Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button(
|
||||
) : leftSlot ? (
|
||||
<div className="mr-1">{leftSlot}</div>
|
||||
) : null}
|
||||
<div className={classNames('max-w-[15em] truncate w-full text-left', innerClassName)}>{children}</div>
|
||||
<div
|
||||
className={classNames(
|
||||
'max-w-[15em] truncate w-full',
|
||||
justify === 'start' ? 'text-left' : 'text-center',
|
||||
innerClassName,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
{rightSlot && <div className="ml-1">{rightSlot}</div>}
|
||||
{forDropdown && <Icon icon="chevronDown" size={size} className="ml-1 -mr-1" />}
|
||||
</button>
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import classNames from 'classnames';
|
||||
import { motion } from 'framer-motion';
|
||||
import type { CSSProperties, HTMLAttributes, MouseEvent, ReactElement, ReactNode } from 'react';
|
||||
import type {
|
||||
CSSProperties,
|
||||
FocusEvent as ReactFocusEvent,
|
||||
HTMLAttributes,
|
||||
MouseEvent,
|
||||
ReactElement,
|
||||
ReactNode,
|
||||
} from 'react';
|
||||
import React, {
|
||||
Children,
|
||||
cloneElement,
|
||||
@@ -13,10 +20,10 @@ import React, {
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useKey, useKeyPressEvent, useWindowSize } from 'react-use';
|
||||
import { Overlay } from '../Overlay';
|
||||
import { Button } from './Button';
|
||||
import { Separator } from './Separator';
|
||||
import { VStack } from './Stacks';
|
||||
import { Overlay } from '../Overlay';
|
||||
|
||||
export type DropdownItemSeparator = {
|
||||
type: 'separator';
|
||||
@@ -108,7 +115,7 @@ export const Dropdown = forwardRef<DropdownRef, DropdownProps>(function Dropdown
|
||||
|
||||
const windowSize = useWindowSize();
|
||||
const triggerRect = useMemo(() => {
|
||||
windowSize; // Make TS happy with this dep
|
||||
if (!windowSize) return null; // No-op to TS happy with this dep
|
||||
if (!open) return null;
|
||||
return buttonRef.current?.getBoundingClientRect();
|
||||
}, [open, windowSize]);
|
||||
@@ -334,7 +341,13 @@ interface MenuItemProps {
|
||||
|
||||
function MenuItem({ className, focused, onFocus, item, onSelect, ...props }: MenuItemProps) {
|
||||
const handleClick = useCallback(() => onSelect?.(item), [item, onSelect]);
|
||||
const handleFocus = useCallback(() => onFocus?.(item), [item, onFocus]);
|
||||
const handleFocus = useCallback(
|
||||
(e: ReactFocusEvent<HTMLButtonElement>) => {
|
||||
e.stopPropagation(); // Don't trigger focus on any parents
|
||||
return onFocus?.(item);
|
||||
},
|
||||
[item, onFocus],
|
||||
);
|
||||
|
||||
const initRef = useCallback(
|
||||
(el: HTMLButtonElement | null) => {
|
||||
@@ -355,6 +368,7 @@ function MenuItem({ className, focused, onFocus, item, onSelect, ...props }: Men
|
||||
tabIndex={-1}
|
||||
onMouseEnter={(e) => e.currentTarget.focus()}
|
||||
onMouseLeave={(e) => e.currentTarget.blur()}
|
||||
disabled={item.disabled}
|
||||
onFocus={handleFocus}
|
||||
onClick={handleClick}
|
||||
justify="start"
|
||||
@@ -366,6 +380,7 @@ function MenuItem({ className, focused, onFocus, item, onSelect, ...props }: Men
|
||||
'focus:bg-highlight focus:text-gray-900 rounded',
|
||||
item.variant === 'danger' && 'text-red-600',
|
||||
)}
|
||||
innerClassName="!text-left"
|
||||
{...props}
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -6,6 +6,11 @@
|
||||
|
||||
* {
|
||||
@apply cursor-text;
|
||||
@apply caret-transparent !important;
|
||||
}
|
||||
|
||||
.cm-cursor {
|
||||
@apply border-gray-800 !important;
|
||||
}
|
||||
|
||||
&.cm-focused {
|
||||
@@ -17,7 +22,7 @@
|
||||
}
|
||||
|
||||
.cm-line {
|
||||
@apply text-gray-800 caret-gray-800 pl-1 pr-1.5;
|
||||
@apply text-gray-800 pl-1 pr-1.5;
|
||||
}
|
||||
|
||||
.cm-placeholder {
|
||||
@@ -58,7 +63,7 @@
|
||||
-webkit-text-security: none;
|
||||
|
||||
&.placeholder-widget-error {
|
||||
@apply bg-red-300/40 border-red-300 border-opacity-40;
|
||||
@apply bg-red-300/40 border-red-300/80 border-opacity-40 hover:border-red-300 hover:bg-red-300/50;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -159,7 +164,7 @@
|
||||
@apply h-full flex items-center;
|
||||
|
||||
/* Break characters on line wrapping mode, useful for URL field.
|
||||
* We can make this dynamic if we need it to be configurable later
|
||||
* We can make this dynamic if we need it to be configurable later
|
||||
*/
|
||||
&.cm-lineWrapping {
|
||||
@apply break-all;
|
||||
|
||||
@@ -6,13 +6,14 @@ import classNames from 'classnames';
|
||||
import { EditorView } from 'codemirror';
|
||||
import type { MutableRefObject, ReactNode } from 'react';
|
||||
import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useRef } from 'react';
|
||||
import { useActiveEnvironment } from '../../../hooks/useActiveEnvironment';
|
||||
import { useActiveWorkspace } from '../../../hooks/useActiveWorkspace';
|
||||
import { IconButton } from '../IconButton';
|
||||
import { HStack } from '../Stacks';
|
||||
import './Editor.css';
|
||||
import { baseExtensions, getLanguageExtension, multiLineExtensions } from './extensions';
|
||||
import type { GenericCompletionConfig } from './genericCompletion';
|
||||
import { singleLineExt } from './singleLine';
|
||||
import { useActiveEnvironment } from '../../../hooks/useActiveEnvironment';
|
||||
|
||||
// Export some things so all the code-split parts are in this file
|
||||
export { buildClientSchema, getIntrospectionQuery } from 'graphql/utilities';
|
||||
@@ -72,7 +73,9 @@ const _Editor = forwardRef<EditorView | undefined, EditorProps>(function Editor(
|
||||
ref,
|
||||
) {
|
||||
const e = useActiveEnvironment();
|
||||
const w = useActiveWorkspace();
|
||||
const environment = autocompleteVariables ? e : null;
|
||||
const workspace = autocompleteVariables ? w : null;
|
||||
|
||||
const cm = useRef<{ view: EditorView; languageCompartment: Compartment } | null>(null);
|
||||
useImperativeHandle(ref, () => cm.current?.view);
|
||||
@@ -124,9 +127,15 @@ const _Editor = forwardRef<EditorView | undefined, EditorProps>(function Editor(
|
||||
useEffect(() => {
|
||||
if (cm.current === null) return;
|
||||
const { view, languageCompartment } = cm.current;
|
||||
const ext = getLanguageExtension({ contentType, environment, useTemplating, autocomplete });
|
||||
const ext = getLanguageExtension({
|
||||
contentType,
|
||||
environment,
|
||||
workspace,
|
||||
useTemplating,
|
||||
autocomplete,
|
||||
});
|
||||
view.dispatch({ effects: languageCompartment.reconfigure(ext) });
|
||||
}, [contentType, autocomplete, useTemplating, environment]);
|
||||
}, [contentType, autocomplete, useTemplating, environment, workspace]);
|
||||
|
||||
useEffect(() => {
|
||||
if (cm.current === null) return;
|
||||
@@ -152,6 +161,7 @@ const _Editor = forwardRef<EditorView | undefined, EditorProps>(function Editor(
|
||||
useTemplating,
|
||||
autocomplete,
|
||||
environment,
|
||||
workspace,
|
||||
});
|
||||
|
||||
const state = EditorState.create({
|
||||
@@ -221,10 +231,14 @@ const _Editor = forwardRef<EditorView | undefined, EditorProps>(function Editor(
|
||||
onClick={() => {
|
||||
if (cm.current === null) return;
|
||||
const { doc } = cm.current.view.state;
|
||||
const insert = format(doc.toString());
|
||||
const formatted = format(doc.toString());
|
||||
// Update editor and blur because the cursor will reset anyway
|
||||
cm.current.view.dispatch({ changes: { from: 0, to: doc.length, insert } });
|
||||
cm.current.view.dispatch({
|
||||
changes: { from: 0, to: doc.length, insert: formatted },
|
||||
});
|
||||
cm.current.view.contentDOM.blur();
|
||||
// Fire change event
|
||||
onChange?.(formatted);
|
||||
}}
|
||||
/>
|
||||
</HStack>
|
||||
@@ -271,7 +285,7 @@ function getExtensions({
|
||||
EditorView.domEventHandlers({
|
||||
focus: () => onFocus.current?.(),
|
||||
blur: () => onBlur.current?.(),
|
||||
keydown: e => onKeyDown.current?.(e),
|
||||
keydown: (e) => onKeyDown.current?.(e),
|
||||
}),
|
||||
|
||||
// Handle onChange
|
||||
|
||||
@@ -32,11 +32,11 @@ import {
|
||||
} from '@codemirror/view';
|
||||
import { tags as t } from '@lezer/highlight';
|
||||
import { graphql, graphqlLanguageSupport } from 'cm6-graphql';
|
||||
import type { Environment, Workspace } from '../../../lib/models';
|
||||
import type { EditorProps } from './index';
|
||||
import { text } from './text/extension';
|
||||
import { twig } from './twig/extension';
|
||||
import { url } from './url/extension';
|
||||
import type { Environment } from '../../../lib/models';
|
||||
|
||||
export const myHighlightStyle = HighlightStyle.define([
|
||||
{
|
||||
@@ -96,8 +96,9 @@ export function getLanguageExtension({
|
||||
contentType,
|
||||
useTemplating = false,
|
||||
environment,
|
||||
workspace,
|
||||
autocomplete,
|
||||
}: { environment: Environment | null } & Pick<
|
||||
}: { environment: Environment | null; workspace: Workspace | null } & Pick<
|
||||
EditorProps,
|
||||
'contentType' | 'useTemplating' | 'autocomplete'
|
||||
>) {
|
||||
@@ -110,20 +111,20 @@ export function getLanguageExtension({
|
||||
return base;
|
||||
}
|
||||
|
||||
return twig(base, environment, autocomplete);
|
||||
return twig(base, environment, workspace, autocomplete);
|
||||
}
|
||||
|
||||
export const baseExtensions = [
|
||||
highlightSpecialChars(),
|
||||
history(),
|
||||
dropCursor(),
|
||||
drawSelection(),
|
||||
bracketMatching(),
|
||||
// TODO: Figure out how to debounce showing of autocomplete in a good way
|
||||
// debouncedAutocompletionDisplay({ millis: 1000 }),
|
||||
// autocompletion({ closeOnBlur: true, interactionDelay: 200, activateOnTyping: false }),
|
||||
autocompletion({
|
||||
// closeOnBlur: false,
|
||||
interactionDelay: 200,
|
||||
compareCompletions: (a, b) => {
|
||||
// Don't sort completions at all, only on boost
|
||||
return (a.boost ?? 0) - (b.boost ?? 0);
|
||||
@@ -147,7 +148,6 @@ export const multiLineExtensions = [
|
||||
},
|
||||
}),
|
||||
EditorState.allowMultipleSelections.of(true),
|
||||
drawSelection(),
|
||||
indentOnInput(),
|
||||
closeBrackets(),
|
||||
rectangularSelection(),
|
||||
|
||||
@@ -7,14 +7,17 @@ import { placeholders } from './placeholder';
|
||||
import { textLanguageName } from '../text/extension';
|
||||
import { twigCompletion } from './completion';
|
||||
import { parser as twigParser } from './twig';
|
||||
import type { Environment } from '../../../../lib/models';
|
||||
import type { Environment, Workspace } from '../../../../lib/models';
|
||||
|
||||
export function twig(
|
||||
base: LanguageSupport,
|
||||
environment: Environment | null,
|
||||
workspace: Workspace | null,
|
||||
autocomplete?: GenericCompletionConfig,
|
||||
) {
|
||||
const variables = environment?.variables.filter(v => v.enabled) ?? [];
|
||||
const variables =
|
||||
[...(workspace?.variables ?? []), ...(environment?.variables ?? [])].filter((v) => v.enabled) ??
|
||||
[];
|
||||
const completions = twigCompletion({ options: variables });
|
||||
|
||||
const language = mixLanguage(base);
|
||||
|
||||
@@ -16,6 +16,7 @@ class PlaceholderWidget extends WidgetType {
|
||||
elt.className = `placeholder-widget ${
|
||||
!this.isExistingVariable ? 'placeholder-widget-error' : ''
|
||||
}`;
|
||||
elt.title = !this.isExistingVariable ? 'Variable not found in active environment' : '';
|
||||
elt.textContent = this.name;
|
||||
return elt;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
CheckboxIcon,
|
||||
CheckIcon,
|
||||
ChevronDownIcon,
|
||||
ChevronRightIcon,
|
||||
ClockIcon,
|
||||
CodeIcon,
|
||||
ColorWheelIcon,
|
||||
@@ -12,6 +13,7 @@ import {
|
||||
DividerHorizontalIcon,
|
||||
DotsHorizontalIcon,
|
||||
DotsVerticalIcon,
|
||||
DownloadIcon,
|
||||
DragHandleDots2Icon,
|
||||
EyeClosedIcon,
|
||||
EyeOpenIcon,
|
||||
@@ -49,12 +51,14 @@ const icons = {
|
||||
checkbox: CheckboxIcon,
|
||||
clock: ClockIcon,
|
||||
chevronDown: ChevronDownIcon,
|
||||
chevronRight: ChevronRightIcon,
|
||||
code: CodeIcon,
|
||||
colorWheel: ColorWheelIcon,
|
||||
copy: CopyIcon,
|
||||
dividerH: DividerHorizontalIcon,
|
||||
dotsH: DotsHorizontalIcon,
|
||||
dotsV: DotsVerticalIcon,
|
||||
download: DownloadIcon,
|
||||
drag: DragHandleDots2Icon,
|
||||
eye: EyeOpenIcon,
|
||||
eyeClosed: EyeClosedIcon,
|
||||
|
||||
@@ -8,8 +8,8 @@ interface Props {
|
||||
}
|
||||
|
||||
export function StatusTag({ response, className, showReason }: Props) {
|
||||
const { status, error } = response;
|
||||
const label = error ? 'ERR' : status;
|
||||
const { status } = response;
|
||||
const label = status < 100 ? 'ERR' : status;
|
||||
return (
|
||||
<span
|
||||
className={classNames(
|
||||
|
||||
@@ -53,18 +53,18 @@ export function useAppRoutes() {
|
||||
}, [nav]);
|
||||
|
||||
const setEnvironment = useCallback(
|
||||
({ id: environmentId }: Environment) => {
|
||||
(environment: Environment | null) => {
|
||||
if (workspaceId == null) {
|
||||
navigate('workspaces');
|
||||
} else if (requestId == null) {
|
||||
navigate('workspace', {
|
||||
workspaceId: workspaceId,
|
||||
environmentId: environmentId ?? null,
|
||||
environmentId: environment == null ? undefined : environment.id,
|
||||
});
|
||||
} else {
|
||||
navigate('request', {
|
||||
workspaceId,
|
||||
environmentId: environmentId ?? null,
|
||||
environmentId: environment == null ? undefined : environment.id,
|
||||
requestId,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { trackEvent } from '../lib/analytics';
|
||||
import type { Environment } from '../lib/models';
|
||||
import { environmentsQueryKey, useEnvironments } from './useEnvironments';
|
||||
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
|
||||
import { useAppRoutes } from './useAppRoutes';
|
||||
import { environmentsQueryKey, useEnvironments } from './useEnvironments';
|
||||
import { usePrompt } from './usePrompt';
|
||||
import { useWorkspaces } from './useWorkspaces';
|
||||
|
||||
@@ -20,7 +21,6 @@ export function useCreateEnvironment() {
|
||||
const name = await prompt({
|
||||
name: 'name',
|
||||
title: 'Create Environment',
|
||||
description: 'Enter a name for the new environment',
|
||||
label: 'Name',
|
||||
defaultValue: 'My Environment',
|
||||
});
|
||||
@@ -30,6 +30,7 @@ export function useCreateEnvironment() {
|
||||
: [];
|
||||
return invoke('create_environment', { name, variables, workspaceId });
|
||||
},
|
||||
onSettled: () => trackEvent('environment', 'create'),
|
||||
onSuccess: async (environment) => {
|
||||
if (workspaceId == null) return;
|
||||
routes.setEnvironment(environment);
|
||||
|
||||
26
src-web/hooks/useCreateFolder.ts
Normal file
26
src-web/hooks/useCreateFolder.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { trackEvent } from '../lib/analytics';
|
||||
import type { Folder } from '../lib/models';
|
||||
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
|
||||
import { foldersQueryKey } from './useFolders';
|
||||
|
||||
export function useCreateFolder() {
|
||||
const workspaceId = useActiveWorkspaceId();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<Folder, unknown, Partial<Pick<Folder, 'name' | 'sortPriority' | 'folderId'>>>({
|
||||
mutationFn: (patch) => {
|
||||
if (workspaceId === null) {
|
||||
throw new Error("Cannot create folder when there's no active workspace");
|
||||
}
|
||||
patch.name = patch.name || 'New Folder';
|
||||
patch.sortPriority = patch.sortPriority || Date.now();
|
||||
return invoke('create_folder', { workspaceId, ...patch });
|
||||
},
|
||||
onSettled: () => trackEvent('folder', 'create'),
|
||||
onSuccess: async (request) => {
|
||||
await queryClient.invalidateQueries(foldersQueryKey({ workspaceId: request.workspaceId }));
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { trackEvent } from '../lib/analytics';
|
||||
import type { HttpRequest } from '../lib/models';
|
||||
import { useActiveEnvironmentId } from './useActiveEnvironmentId';
|
||||
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
|
||||
import { useAppRoutes } from './useAppRoutes';
|
||||
import { requestsQueryKey, useRequests } from './useRequests';
|
||||
import { useActiveEnvironmentId } from './useActiveEnvironmentId';
|
||||
|
||||
export function useCreateRequest() {
|
||||
const workspaceId = useActiveWorkspaceId();
|
||||
@@ -13,7 +14,11 @@ export function useCreateRequest() {
|
||||
const requests = useRequests();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<HttpRequest, unknown, Partial<Pick<HttpRequest, 'name' | 'sortPriority'>>>({
|
||||
return useMutation<
|
||||
HttpRequest,
|
||||
unknown,
|
||||
Partial<Pick<HttpRequest, 'name' | 'sortPriority' | 'folderId'>>
|
||||
>({
|
||||
mutationFn: (patch) => {
|
||||
if (workspaceId === null) {
|
||||
throw new Error("Cannot create request when there's no active workspace");
|
||||
@@ -22,6 +27,7 @@ export function useCreateRequest() {
|
||||
patch.sortPriority = patch.sortPriority || maxSortPriority(requests) + 1000;
|
||||
return invoke('create_request', { workspaceId, ...patch });
|
||||
},
|
||||
onSettled: () => trackEvent('http_request', 'create'),
|
||||
onSuccess: async (request) => {
|
||||
queryClient.setQueryData<HttpRequest[]>(
|
||||
requestsQueryKey({ workspaceId: request.workspaceId }),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { trackEvent } from '../lib/analytics';
|
||||
import type { Workspace } from '../lib/models';
|
||||
import { useAppRoutes } from './useAppRoutes';
|
||||
import { workspacesQueryKey } from './useWorkspaces';
|
||||
@@ -11,6 +12,7 @@ export function useCreateWorkspace({ navigateAfter }: { navigateAfter: boolean }
|
||||
mutationFn: (patch) => {
|
||||
return invoke('create_workspace', patch);
|
||||
},
|
||||
onSettled: () => trackEvent('workspace', 'create'),
|
||||
onSuccess: async (workspace) => {
|
||||
queryClient.setQueryData<Workspace[]>(workspacesQueryKey({}), (workspaces) => [
|
||||
...(workspaces ?? []),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { InlineCode } from '../components/core/InlineCode';
|
||||
import { trackEvent } from '../lib/analytics';
|
||||
import type { HttpRequest } from '../lib/models';
|
||||
import { getRequest } from '../lib/store';
|
||||
import { useConfirm } from './useConfirm';
|
||||
@@ -26,6 +27,7 @@ export function useDeleteAnyRequest() {
|
||||
if (!confirmed) return null;
|
||||
return invoke('delete_request', { requestId: id });
|
||||
},
|
||||
onSettled: () => trackEvent('http_request', 'delete'),
|
||||
onSuccess: async (request) => {
|
||||
// Was it cancelled?
|
||||
if (request === null) return;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { InlineCode } from '../components/core/InlineCode';
|
||||
import { trackEvent } from '../lib/analytics';
|
||||
import type { Environment, Workspace } from '../lib/models';
|
||||
import { useConfirm } from './useConfirm';
|
||||
import { environmentsQueryKey } from './useEnvironments';
|
||||
@@ -23,13 +24,13 @@ export function useDeleteEnvironment(environment: Environment | null) {
|
||||
if (!confirmed) return null;
|
||||
return invoke('delete_environment', { environmentId: environment?.id });
|
||||
},
|
||||
onSettled: () => trackEvent('environment', 'delete'),
|
||||
onSuccess: async (environment) => {
|
||||
if (environment === null) return;
|
||||
|
||||
const { id: environmentId, workspaceId } = environment;
|
||||
queryClient.setQueryData<Workspace[]>(
|
||||
environmentsQueryKey({ workspaceId }),
|
||||
(environments) => environments?.filter((e) => e.id !== environmentId),
|
||||
queryClient.setQueryData<Workspace[]>(environmentsQueryKey({ workspaceId }), (environments) =>
|
||||
environments?.filter((e) => e.id !== environmentId),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
42
src-web/hooks/useDeleteFolder.tsx
Normal file
42
src-web/hooks/useDeleteFolder.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { InlineCode } from '../components/core/InlineCode';
|
||||
import { trackEvent } from '../lib/analytics';
|
||||
import type { Folder } from '../lib/models';
|
||||
import { getFolder } from '../lib/store';
|
||||
import { useConfirm } from './useConfirm';
|
||||
import { foldersQueryKey } from './useFolders';
|
||||
import { requestsQueryKey } from './useRequests';
|
||||
|
||||
export function useDeleteFolder(id: string | null) {
|
||||
const queryClient = useQueryClient();
|
||||
const confirm = useConfirm();
|
||||
|
||||
return useMutation<Folder | null, string>({
|
||||
mutationFn: async () => {
|
||||
const folder = await getFolder(id);
|
||||
const confirmed = await confirm({
|
||||
title: 'Delete Folder',
|
||||
variant: 'delete',
|
||||
description: (
|
||||
<>
|
||||
Permanently delete <InlineCode>{folder?.name}</InlineCode> and everything in it?
|
||||
</>
|
||||
),
|
||||
});
|
||||
if (!confirmed) return null;
|
||||
return invoke('delete_folder', { folderId: id });
|
||||
},
|
||||
onSettled: () => trackEvent('folder', 'delete'),
|
||||
onSuccess: async (folder) => {
|
||||
// Was it cancelled?
|
||||
if (folder === null) return;
|
||||
|
||||
const { workspaceId } = folder;
|
||||
|
||||
// Nesting makes it hard to clean things up, so just clear everything that could have been deleted
|
||||
await queryClient.invalidateQueries(requestsQueryKey({ workspaceId }));
|
||||
await queryClient.invalidateQueries(foldersQueryKey({ workspaceId }));
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,40 +1,11 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { InlineCode } from '../components/core/InlineCode';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import type { HttpRequest } from '../lib/models';
|
||||
import { getRequest } from '../lib/store';
|
||||
import { useConfirm } from './useConfirm';
|
||||
import { requestsQueryKey } from './useRequests';
|
||||
import { responsesQueryKey } from './useResponses';
|
||||
import { useDeleteAnyRequest } from './useDeleteAnyRequest';
|
||||
|
||||
export function useDeleteRequest(id: string | null) {
|
||||
const queryClient = useQueryClient();
|
||||
const confirm = useConfirm();
|
||||
const deleteAnyRequest = useDeleteAnyRequest();
|
||||
|
||||
return useMutation<HttpRequest | null, string>({
|
||||
mutationFn: async () => {
|
||||
const request = await getRequest(id);
|
||||
const confirmed = await confirm({
|
||||
title: 'Delete Request',
|
||||
variant: 'delete',
|
||||
description: (
|
||||
<>
|
||||
Permanently delete <InlineCode>{request?.name}</InlineCode>?
|
||||
</>
|
||||
),
|
||||
});
|
||||
if (!confirmed) return null;
|
||||
return invoke('delete_request', { requestId: id });
|
||||
},
|
||||
onSuccess: async (request) => {
|
||||
// Was it cancelled?
|
||||
if (request === null) return;
|
||||
|
||||
const { workspaceId, id: requestId } = request;
|
||||
queryClient.setQueryData(responsesQueryKey({ requestId }), []); // Responses were deleted
|
||||
queryClient.setQueryData<HttpRequest[]>(requestsQueryKey({ workspaceId }), (requests) =>
|
||||
(requests ?? []).filter((r) => r.id !== requestId),
|
||||
);
|
||||
},
|
||||
mutationFn: () => deleteAnyRequest.mutateAsync(id ?? 'n/a'),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { trackEvent } from '../lib/analytics';
|
||||
import type { HttpResponse } from '../lib/models';
|
||||
import { responsesQueryKey } from './useResponses';
|
||||
|
||||
@@ -9,6 +10,7 @@ export function useDeleteResponse(id: string | null) {
|
||||
mutationFn: async () => {
|
||||
return await invoke('delete_response', { id: id });
|
||||
},
|
||||
onSettled: () => trackEvent('http_response', 'delete'),
|
||||
onSuccess: ({ requestId, id: responseId }) => {
|
||||
queryClient.setQueryData<HttpResponse[]>(responsesQueryKey({ requestId }), (responses) =>
|
||||
(responses ?? []).filter((response) => response.id !== responseId),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { trackEvent } from '../lib/analytics';
|
||||
import { responsesQueryKey } from './useResponses';
|
||||
|
||||
export function useDeleteResponses(requestId?: string) {
|
||||
@@ -9,6 +10,7 @@ export function useDeleteResponses(requestId?: string) {
|
||||
if (requestId === undefined) return;
|
||||
await invoke('delete_all_responses', { requestId });
|
||||
},
|
||||
onSettled: () => trackEvent('http_response', 'delete_many'),
|
||||
onSuccess: async () => {
|
||||
if (requestId === undefined) return;
|
||||
queryClient.setQueryData(responsesQueryKey({ requestId }), []);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { InlineCode } from '../components/core/InlineCode';
|
||||
import { trackEvent } from '../lib/analytics';
|
||||
import type { Workspace } from '../lib/models';
|
||||
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
|
||||
import { useAppRoutes } from './useAppRoutes';
|
||||
@@ -28,6 +29,7 @@ export function useDeleteWorkspace(workspace: Workspace | null) {
|
||||
if (!confirmed) return null;
|
||||
return invoke('delete_workspace', { workspaceId: workspace?.id });
|
||||
},
|
||||
onSettled: () => trackEvent('workspace', 'delete'),
|
||||
onSuccess: async (workspace) => {
|
||||
if (workspace === null) return;
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { trackEvent } from '../lib/analytics';
|
||||
import type { HttpRequest } from '../lib/models';
|
||||
import { useActiveEnvironmentId } from './useActiveEnvironmentId';
|
||||
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
|
||||
import { useAppRoutes } from './useAppRoutes';
|
||||
import { requestsQueryKey } from './useRequests';
|
||||
import { useActiveEnvironmentId } from './useActiveEnvironmentId';
|
||||
|
||||
export function useDuplicateRequest({
|
||||
id,
|
||||
@@ -22,6 +23,7 @@ export function useDuplicateRequest({
|
||||
if (id === null) throw new Error("Can't duplicate a null request");
|
||||
return invoke('duplicate_request', { id });
|
||||
},
|
||||
onSettled: () => trackEvent('http_request', 'duplicate'),
|
||||
onSuccess: async (request) => {
|
||||
queryClient.setQueryData<HttpRequest[]>(
|
||||
requestsQueryKey({ workspaceId: request.workspaceId }),
|
||||
|
||||
22
src-web/hooks/useFolders.ts
Normal file
22
src-web/hooks/useFolders.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import type { Folder, HttpRequest } from '../lib/models';
|
||||
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
|
||||
|
||||
export function foldersQueryKey({ workspaceId }: { workspaceId: string }) {
|
||||
return ['folders', { workspaceId }];
|
||||
}
|
||||
|
||||
export function useFolders() {
|
||||
const workspaceId = useActiveWorkspaceId();
|
||||
return (
|
||||
useQuery({
|
||||
enabled: workspaceId != null,
|
||||
queryKey: foldersQueryKey({ workspaceId: workspaceId ?? 'n/a' }),
|
||||
queryFn: async () => {
|
||||
if (workspaceId == null) return [];
|
||||
return (await invoke('list_folders', { workspaceId })) as Folder[];
|
||||
},
|
||||
}).data ?? []
|
||||
);
|
||||
}
|
||||
@@ -6,8 +6,8 @@ import { minPromiseMillis } from '../lib/minPromiseMillis';
|
||||
import type { HttpRequest } from '../lib/models';
|
||||
import { getResponseBodyText } from '../lib/responseBody';
|
||||
import { sendEphemeralRequest } from '../lib/sendEphemeralRequest';
|
||||
import { useDebouncedValue } from './useDebouncedValue';
|
||||
import { useActiveEnvironmentId } from './useActiveEnvironmentId';
|
||||
import { useDebouncedValue } from './useDebouncedValue';
|
||||
|
||||
const introspectionRequestBody = JSON.stringify({
|
||||
query: getIntrospectionQuery(),
|
||||
@@ -66,7 +66,7 @@ export function useIntrospectGraphQL(baseRequest: HttpRequest) {
|
||||
runIntrospection(); // Run immediately
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [request.id, request.url, request.method, refetchKey]);
|
||||
}, [request.id, request.url, request.method, refetchKey, activeEnvironmentId]);
|
||||
|
||||
const refetch = useCallback(() => {
|
||||
setRefetchKey((k) => k + 1);
|
||||
|
||||
13
src-web/hooks/useSendAnyRequest.ts
Normal file
13
src-web/hooks/useSendAnyRequest.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { trackEvent } from '../lib/analytics';
|
||||
import type { HttpResponse } from '../lib/models';
|
||||
import { useActiveEnvironmentId } from './useActiveEnvironmentId';
|
||||
|
||||
export function useSendAnyRequest() {
|
||||
const environmentId = useActiveEnvironmentId();
|
||||
return useMutation<HttpResponse, string, string | null>({
|
||||
mutationFn: (id) => invoke('send_request', { requestId: id, environmentId }),
|
||||
onSettled: () => trackEvent('http_request', 'send'),
|
||||
});
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import type { HttpResponse } from '../lib/models';
|
||||
import { useActiveEnvironmentId } from './useActiveEnvironmentId';
|
||||
import { useSendAnyRequest } from './useSendAnyRequest';
|
||||
|
||||
export function useSendRequest(id: string | null) {
|
||||
const environmentId = useActiveEnvironmentId();
|
||||
const sendAnyRequest = useSendAnyRequest();
|
||||
return useMutation<HttpResponse, string>({
|
||||
mutationFn: () => invoke('send_request', { requestId: id, environmentId }),
|
||||
}).mutate;
|
||||
mutationFn: () => sendAnyRequest.mutateAsync(id),
|
||||
});
|
||||
}
|
||||
|
||||
28
src-web/hooks/useUpdateAnyFolder.ts
Normal file
28
src-web/hooks/useUpdateAnyFolder.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import type { Folder, HttpRequest } from '../lib/models';
|
||||
import { getFolder, getRequest } from '../lib/store';
|
||||
import { requestsQueryKey } from './useRequests';
|
||||
import { foldersQueryKey } from './useFolders';
|
||||
|
||||
export function useUpdateAnyFolder() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<void, unknown, { id: string; update: (r: Folder) => Folder }>({
|
||||
mutationFn: async ({ id, update }) => {
|
||||
const folder = await getFolder(id);
|
||||
if (folder === null) {
|
||||
throw new Error("Can't update a null folder");
|
||||
}
|
||||
|
||||
await invoke('update_folder', { folder: update(folder) });
|
||||
},
|
||||
onMutate: async ({ id, update }) => {
|
||||
const folder = await getFolder(id);
|
||||
if (folder === null) return;
|
||||
queryClient.setQueryData<Folder[]>(foldersQueryKey(folder), (folders) =>
|
||||
(folders ?? []).map((f) => (f.id === folder.id ? update(f) : f)),
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -7,20 +7,28 @@ import { requestsQueryKey } from './useRequests';
|
||||
export function useUpdateAnyRequest() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<void, unknown, { id: string; update: (r: HttpRequest) => HttpRequest }>({
|
||||
return useMutation<
|
||||
void,
|
||||
unknown,
|
||||
{ id: string; update: Partial<HttpRequest> | ((r: HttpRequest) => HttpRequest) }
|
||||
>({
|
||||
mutationFn: async ({ id, update }) => {
|
||||
const request = await getRequest(id);
|
||||
if (request === null) {
|
||||
throw new Error("Can't update a null request");
|
||||
}
|
||||
|
||||
await invoke('update_request', { request: update(request) });
|
||||
const patchedRequest =
|
||||
typeof update === 'function' ? update(request) : { ...request, ...update };
|
||||
await invoke('update_request', { request: patchedRequest });
|
||||
},
|
||||
onMutate: async ({ id, update }) => {
|
||||
const request = await getRequest(id);
|
||||
if (request === null) return;
|
||||
const patchedRequest =
|
||||
typeof update === 'function' ? update(request) : { ...request, ...update };
|
||||
queryClient.setQueryData<HttpRequest[]>(requestsQueryKey(request), (requests) =>
|
||||
(requests ?? []).map((r) => (r.id === request.id ? update(r) : r)),
|
||||
(requests ?? []).map((r) => (r.id === patchedRequest.id ? patchedRequest : r)),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,29 +1,10 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import type { HttpRequest } from '../lib/models';
|
||||
import { getRequest } from '../lib/store';
|
||||
import { requestsQueryKey } from './useRequests';
|
||||
import { useUpdateAnyRequest } from './useUpdateAnyRequest';
|
||||
|
||||
export function useUpdateRequest(id: string | null) {
|
||||
const queryClient = useQueryClient();
|
||||
const updateAnyRequest = useUpdateAnyRequest();
|
||||
return useMutation<void, unknown, Partial<HttpRequest> | ((r: HttpRequest) => HttpRequest)>({
|
||||
mutationFn: async (v) => {
|
||||
const request = await getRequest(id);
|
||||
if (request == null) {
|
||||
throw new Error("Can't update a null request");
|
||||
}
|
||||
|
||||
const newRequest = typeof v === 'function' ? v(request) : { ...request, ...v };
|
||||
await invoke('update_request', { request: newRequest });
|
||||
},
|
||||
onMutate: async (v) => {
|
||||
const request = await getRequest(id);
|
||||
if (request === null) return;
|
||||
|
||||
const patchedRequest = typeof v === 'function' ? v(request) : { ...request, ...v };
|
||||
queryClient.setQueryData<HttpRequest[]>(requestsQueryKey(request), (requests) =>
|
||||
(requests ?? []).map((r) => (r.id === patchedRequest.id ? patchedRequest : r)),
|
||||
);
|
||||
},
|
||||
mutationFn: async (update) => updateAnyRequest.mutateAsync({ id: id ?? 'n/a', update }),
|
||||
});
|
||||
}
|
||||
|
||||
58
src-web/lib/analytics.ts
Normal file
58
src-web/lib/analytics.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { getVersion } from '@tauri-apps/api/app';
|
||||
import type { Environment, Folder, HttpRequest, HttpResponse, KeyValue, Workspace } from './models';
|
||||
|
||||
const appVersion = await getVersion();
|
||||
|
||||
export function trackEvent(
|
||||
resource:
|
||||
| Workspace['model']
|
||||
| Environment['model']
|
||||
| Folder['model']
|
||||
| HttpRequest['model']
|
||||
| HttpResponse['model']
|
||||
| KeyValue['model'],
|
||||
event: 'create' | 'update' | 'delete' | 'delete_many' | 'send' | 'duplicate',
|
||||
attributes: Record<string, string | number> = {},
|
||||
) {
|
||||
send('/e', [
|
||||
{ name: 'e', value: `${resource}.${event}` },
|
||||
{ name: 'a', value: JSON.stringify({ ...attributes, version: appVersion }) },
|
||||
]);
|
||||
}
|
||||
|
||||
export function trackPage(pathname: string) {
|
||||
if (pathname === sessionStorage.lastPathName) {
|
||||
return;
|
||||
}
|
||||
|
||||
sessionStorage.lastPathName = pathname;
|
||||
send('/p', [
|
||||
{
|
||||
name: 'h',
|
||||
value: 'desktop.yaak.app',
|
||||
},
|
||||
{ name: 'p', value: pathname },
|
||||
]);
|
||||
}
|
||||
|
||||
function send(path: string, params: { name: string; value: string | number }[]) {
|
||||
if (localStorage.disableAnalytics === 'true') {
|
||||
console.log('Analytics disabled', path, params);
|
||||
}
|
||||
|
||||
params.push({ name: 'id', value: 'site_zOK0d7jeBy2TLxFCnZ' });
|
||||
params.push({
|
||||
name: 'tz',
|
||||
value: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
});
|
||||
params.push({ name: 'xy', value: screensize() });
|
||||
const qs = params.map((v) => `${v.name}=${encodeURIComponent(v.value)}`).join('&');
|
||||
const url = `https://t.yaak.app/t${path}?${qs}`;
|
||||
fetch(url, { mode: 'no-cors' }).catch((err) => console.log('Error:', err));
|
||||
}
|
||||
|
||||
function screensize() {
|
||||
const w = window.screen.width;
|
||||
const h = window.screen.height;
|
||||
return `${Math.round(w / 100) * 100}x${Math.round(h / 100) * 100}`;
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
export const BODY_TYPE_NONE = null;
|
||||
export const BODY_TYPE_GRAPHQL = 'graphql';
|
||||
export const BODY_TYPE_JSON = 'application/json';
|
||||
@@ -20,6 +19,7 @@ export interface Workspace extends BaseModel {
|
||||
readonly model: 'workspace';
|
||||
name: string;
|
||||
description: string;
|
||||
variables: EnvironmentVariable[];
|
||||
}
|
||||
|
||||
export interface EnvironmentVariable {
|
||||
@@ -28,6 +28,14 @@ export interface EnvironmentVariable {
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
export interface Folder extends BaseModel {
|
||||
readonly workspaceId: string;
|
||||
readonly model: 'folder';
|
||||
folderId: string | null;
|
||||
sortPriority: number;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface Environment extends BaseModel {
|
||||
readonly workspaceId: string;
|
||||
readonly model: 'environment';
|
||||
@@ -44,6 +52,7 @@ export interface HttpHeader {
|
||||
export interface HttpRequest extends BaseModel {
|
||||
readonly workspaceId: string;
|
||||
readonly model: 'http_request';
|
||||
folderId: string | null;
|
||||
sortPriority: number;
|
||||
name: string;
|
||||
url: string;
|
||||
@@ -51,8 +60,6 @@ export interface HttpRequest extends BaseModel {
|
||||
bodyType: string | null;
|
||||
authentication: Record<string, string | number | boolean | null | undefined>;
|
||||
authenticationType: string | null;
|
||||
auth: Record<string, string | number | null>;
|
||||
authType: string | null;
|
||||
method: string;
|
||||
headers: HttpHeader[];
|
||||
}
|
||||
@@ -80,7 +87,7 @@ export interface HttpResponse extends BaseModel {
|
||||
}
|
||||
|
||||
export function isResponseLoading(response: HttpResponse): boolean {
|
||||
return !(response.body || response.status || response.error);
|
||||
return response.elapsed === 0;
|
||||
}
|
||||
|
||||
export function modelsEq(a: Model, b: Model) {
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import type { HttpRequest, HttpResponse } from './models';
|
||||
|
||||
export async function sendEphemeralRequest(request: HttpRequest, environmentId: string | null): Promise<HttpResponse> {
|
||||
export async function sendEphemeralRequest(
|
||||
request: HttpRequest,
|
||||
environmentId: string | null,
|
||||
): Promise<HttpResponse> {
|
||||
// Remove some things that we don't want to associate
|
||||
const newRequest = { ...request, id: '', requestId: '', workspaceId: '' };
|
||||
const newRequest = { ...request };
|
||||
return invoke('send_ephemeral_request', { request: newRequest, environmentId });
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import type { Environment, HttpRequest, Workspace } from './models';
|
||||
import type { Environment, Folder, HttpRequest, Workspace } from './models';
|
||||
|
||||
export async function getRequest(id: string | null): Promise<HttpRequest | null> {
|
||||
if (id === null) return null;
|
||||
@@ -19,6 +19,15 @@ export async function getEnvironment(id: string | null): Promise<Environment | n
|
||||
return environment;
|
||||
}
|
||||
|
||||
export async function getFolder(id: string | null): Promise<Folder | null> {
|
||||
if (id === null) return null;
|
||||
const folder: Folder = (await invoke('get_folder', { id })) ?? null;
|
||||
if (folder == null) {
|
||||
return null;
|
||||
}
|
||||
return folder;
|
||||
}
|
||||
|
||||
export async function getWorkspace(id: string | null): Promise<Workspace | null> {
|
||||
if (id === null) return null;
|
||||
const workspace: Workspace = (await invoke('get_workspace', { id })) ?? null;
|
||||
|
||||
@@ -40,9 +40,10 @@ export const appThemeVariants: AppThemeColorVariant[] = [
|
||||
];
|
||||
|
||||
export type AppThemeLayer = 'root' | 'sidebar' | 'titlebar' | 'content' | 'above';
|
||||
export type AppThemeColors = Record<AppThemeColor, string>;
|
||||
|
||||
export interface AppThemeLayerStyle {
|
||||
colors: Record<AppThemeColor, string>;
|
||||
colors: AppThemeColors;
|
||||
blackPoint?: number;
|
||||
whitePoint?: number;
|
||||
}
|
||||
|
||||
@@ -1,24 +1,43 @@
|
||||
import type { AppTheme } from './theme';
|
||||
import type { AppTheme, AppThemeColors } from './theme';
|
||||
import { generateCSS, toTailwindVariable } from './theme';
|
||||
|
||||
export type Appearance = 'dark' | 'light';
|
||||
|
||||
enum Theme {
|
||||
yaak = 'yaak',
|
||||
catppuccin = 'catppuccin',
|
||||
}
|
||||
|
||||
const themes: Record<Theme, AppThemeColors> = {
|
||||
yaak: {
|
||||
gray: 'hsl(245, 23%, 45%)',
|
||||
red: 'hsl(342,100%, 63%)',
|
||||
orange: 'hsl(32, 98%, 54%)',
|
||||
yellow: 'hsl(52, 79%, 58%)',
|
||||
green: 'hsl(136, 62%, 54%)',
|
||||
blue: 'hsl(206, 100%, 56%)',
|
||||
pink: 'hsl(300, 100%, 71%)',
|
||||
violet: 'hsl(266, 100%, 73%)',
|
||||
},
|
||||
catppuccin: {
|
||||
gray: 'hsl(240, 23%, 47%)',
|
||||
red: 'hsl(343, 91%, 74%)',
|
||||
orange: 'hsl(23, 92%, 74%)',
|
||||
yellow: 'hsl(41, 86%, 72%)',
|
||||
green: 'hsl(115, 54%, 65%)',
|
||||
blue: 'hsl(217, 92%, 65%)',
|
||||
pink: 'hsl(316, 72%, 75%)',
|
||||
violet: 'hsl(267, 84%, 70%)',
|
||||
},
|
||||
};
|
||||
|
||||
const darkTheme: AppTheme = {
|
||||
name: 'Default Dark',
|
||||
appearance: 'dark',
|
||||
layers: {
|
||||
root: {
|
||||
blackPoint: 0.2,
|
||||
colors: {
|
||||
gray: '#6b5b98',
|
||||
red: '#ff417b',
|
||||
orange: '#fd9014',
|
||||
yellow: '#e8d13f',
|
||||
green: '#3fd265',
|
||||
blue: '#219dff',
|
||||
pink: '#ff6dff',
|
||||
violet: '#b176ff',
|
||||
},
|
||||
colors: themes.yaak,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -58,7 +58,7 @@ module.exports = {
|
||||
'5xl': '3.052rem',
|
||||
},
|
||||
colors: {
|
||||
selection: 'hsl(var(--color-violet-500) / 0.4)',
|
||||
selection: 'hsl(var(--color-violet-500) / 0.3)',
|
||||
focus: 'hsl(var(--color-blue-500) / 0.6)',
|
||||
invalid: 'hsl(var(--color-red-500))',
|
||||
highlight: 'hsl(var(--color-gray-300) / 0.35)',
|
||||
|
||||
Reference in New Issue
Block a user