mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-01-06 05:19:05 -05:00
Compare commits
49 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db6a7dcabb | ||
|
|
00b1f90074 | ||
|
|
e292235792 | ||
|
|
5f86802d88 | ||
|
|
acb7f2e49b | ||
|
|
e2a15609bf | ||
|
|
aa3bfd78c4 | ||
|
|
23c4971127 | ||
|
|
40669217fb | ||
|
|
8089ea87e8 | ||
|
|
9de24e3a40 | ||
|
|
5afb8e7383 | ||
|
|
b3e3f22211 | ||
|
|
1ff6ff16b3 | ||
|
|
b8a692f1a5 | ||
|
|
5506cdd05f | ||
|
|
4180fecb4b | ||
|
|
fa257fdb18 | ||
|
|
2da141ea16 | ||
|
|
1993361f87 | ||
|
|
a5dd3beb73 | ||
|
|
17423f8c54 | ||
|
|
8f495b9ade | ||
|
|
46b9b758fe | ||
|
|
b0e84aac0c | ||
|
|
20de2aeacc | ||
|
|
7198534640 | ||
|
|
7e8ec36474 | ||
|
|
52d1602d35 | ||
|
|
e5731ceb1f | ||
|
|
3ed5a47a83 | ||
|
|
262a29ca5d | ||
|
|
4a3e599128 | ||
|
|
7ebe844643 | ||
|
|
a49b72eebc | ||
|
|
bba3afa0b7 | ||
|
|
221e768b33 | ||
|
|
c2dc7e0f4a | ||
|
|
9e065c34ee | ||
|
|
2f91d541c5 | ||
|
|
948fd487ab | ||
|
|
ed6a5386a2 | ||
|
|
8a24c48fd3 | ||
|
|
d726a6f5bf | ||
|
|
8d2a2a8532 | ||
|
|
b838a6ffc1 | ||
|
|
2174a91b64 | ||
|
|
083f83ccab | ||
|
|
4f749be2e2 |
15
package-lock.json
generated
15
package-lock.json
generated
@@ -34,6 +34,7 @@
|
||||
"format-graphql": "^1.4.0",
|
||||
"framer-motion": "^9.0.4",
|
||||
"lucide-react": "^0.309.0",
|
||||
"mime": "^4.0.1",
|
||||
"papaparse": "^5.4.1",
|
||||
"parse-color": "^1.0.0",
|
||||
"react": "^18.2.0",
|
||||
@@ -7208,6 +7209,20 @@
|
||||
"node": ">=8.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-4.0.1.tgz",
|
||||
"integrity": "sha512-5lZ5tyrIfliMXzFtkYyekWbtRXObT9OWa8IwQ5uxTBDHucNNwniRqo0yInflj+iYi5CBa6qxadGzGarDfuEOxA==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa"
|
||||
],
|
||||
"bin": {
|
||||
"mime": "bin/cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/mimic-fn": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz",
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
"format-graphql": "^1.4.0",
|
||||
"framer-motion": "^9.0.4",
|
||||
"lucide-react": "^0.309.0",
|
||||
"mime": "^4.0.1",
|
||||
"papaparse": "^5.4.1",
|
||||
"parse-color": "^1.0.0",
|
||||
"react": "^18.2.0",
|
||||
|
||||
@@ -6,10 +6,14 @@ export function isRequestGroup(obj) {
|
||||
return isJSObject(obj) && obj._type === 'request_group';
|
||||
}
|
||||
|
||||
export function isRequest(obj) {
|
||||
export function isHttpRequest(obj) {
|
||||
return isJSObject(obj) && obj._type === 'request';
|
||||
}
|
||||
|
||||
export function isGrpcRequest(obj) {
|
||||
return isJSObject(obj) && obj._type === 'grpc_request';
|
||||
}
|
||||
|
||||
export function isEnvironment(obj) {
|
||||
return isJSObject(obj) && obj._type === 'environment';
|
||||
}
|
||||
|
||||
37
plugins/importer-insomnia/src/importers/grpcRequest.js
Normal file
37
plugins/importer-insomnia/src/importers/grpcRequest.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import { convertSyntax } from '../helpers/variables.js';
|
||||
|
||||
/**
|
||||
* Import an Insomnia GRPC 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 importGrpcRequest(r, workspaceId, sortPriority = 0) {
|
||||
console.log('IMPORTING GRPC REQUEST', r._id, r.name, JSON.stringify(r, null, 2));
|
||||
|
||||
const parts = r.protoMethodName.split('/').filter((p) => p !== '');
|
||||
const service = parts[0] ?? null;
|
||||
const method = parts[1] ?? null;
|
||||
|
||||
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: 'grpc_request',
|
||||
sortPriority,
|
||||
name: r.name,
|
||||
url: convertSyntax(r.url),
|
||||
service,
|
||||
method,
|
||||
message: r.body?.text ?? '',
|
||||
metadata: (r.metadata ?? [])
|
||||
.map(({ name, value, disabled }) => ({
|
||||
enabled: !disabled,
|
||||
name,
|
||||
value,
|
||||
}))
|
||||
.filter(({ name, value }) => name !== '' || value !== ''),
|
||||
};
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import { convertSyntax } from '../helpers/variables.js';
|
||||
* @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) {
|
||||
export function importHttpRequest(r, workspaceId, sortPriority = 0) {
|
||||
console.log('IMPORTING REQUEST', r._id, r.name, JSON.stringify(r, null, 2));
|
||||
|
||||
let bodyType = null;
|
||||
@@ -1,17 +1,18 @@
|
||||
import { importEnvironment } from './importers/environment.js';
|
||||
import { importRequest } from './importers/request.js';
|
||||
import { importEnvironment } from './importers/environment';
|
||||
import { importHttpRequest } from './importers/httpRequest';
|
||||
import {
|
||||
isEnvironment,
|
||||
isJSObject,
|
||||
isRequest,
|
||||
isHttpRequest,
|
||||
isRequestGroup,
|
||||
isWorkspace,
|
||||
isGrpcRequest,
|
||||
} from './helpers/types.js';
|
||||
import { parseVariables } from './helpers/variables.js';
|
||||
import { importFolder } from './importers/folder.js';
|
||||
import { importGrpcRequest } from './importers/grpcRequest';
|
||||
|
||||
export function pluginHookImport(contents) {
|
||||
console.log('RUNNING INSOMNIA');
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(contents);
|
||||
@@ -24,7 +25,8 @@ export function pluginHookImport(contents) {
|
||||
|
||||
const resources = {
|
||||
workspaces: [],
|
||||
requests: [],
|
||||
httpRequests: [],
|
||||
grpcRequests: [],
|
||||
environments: [],
|
||||
folders: [],
|
||||
};
|
||||
@@ -57,8 +59,15 @@ export function pluginHookImport(contents) {
|
||||
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++));
|
||||
} else if (isHttpRequest(child)) {
|
||||
resources.httpRequests.push(
|
||||
importHttpRequest(child, workspaceToImport._id, sortPriority++),
|
||||
);
|
||||
} else if (isGrpcRequest(child)) {
|
||||
console.log('GRPC', JSON.stringify(child, null, 1));
|
||||
resources.grpcRequests.push(
|
||||
importGrpcRequest(child, workspaceToImport._id, sortPriority++),
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -68,7 +77,8 @@ export function pluginHookImport(contents) {
|
||||
}
|
||||
|
||||
// Filter out any `null` values
|
||||
resources.requests = resources.requests.filter(Boolean);
|
||||
resources.httpRequests = resources.httpRequests.filter(Boolean);
|
||||
resources.grpcRequests = resources.grpcRequests.filter(Boolean);
|
||||
resources.environments = resources.environments.filter(Boolean);
|
||||
resources.workspaces = resources.workspaces.filter(Boolean);
|
||||
|
||||
|
||||
1495
plugins/importer-postman/package-lock.json
generated
1495
plugins/importer-postman/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,7 @@
|
||||
{
|
||||
"name": "importer-postman",
|
||||
"version": "0.0.1"
|
||||
"version": "0.0.1",
|
||||
"devDependencies": {
|
||||
"vitest": "^1.4.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ type AtLeast<T, K extends keyof T> = Partial<T> & Pick<T, K>;
|
||||
interface ExportResources {
|
||||
workspaces: AtLeast<Workspace, 'name' | 'id' | 'model'>[];
|
||||
environments: AtLeast<Environment, 'name' | 'id' | 'model' | 'workspaceId'>[];
|
||||
requests: AtLeast<HttpRequest, 'name' | 'id' | 'model' | 'workspaceId'>[];
|
||||
httpRequests: AtLeast<HttpRequest, 'name' | 'id' | 'model' | 'workspaceId'>[];
|
||||
folders: AtLeast<Folder, 'name' | 'id' | 'model' | 'workspaceId'>[];
|
||||
}
|
||||
|
||||
@@ -23,10 +23,12 @@ export function pluginHookImport(contents: string): { resources: ExportResources
|
||||
return;
|
||||
}
|
||||
|
||||
const globalAuth = importAuth(root.auth);
|
||||
|
||||
const exportResources: ExportResources = {
|
||||
workspaces: [],
|
||||
environments: [],
|
||||
requests: [],
|
||||
httpRequests: [],
|
||||
folders: [],
|
||||
};
|
||||
|
||||
@@ -35,6 +37,11 @@ export function pluginHookImport(contents: string): { resources: ExportResources
|
||||
id: generateId('wk'),
|
||||
name: info.name || 'Postman Import',
|
||||
description: info.description || '',
|
||||
variables:
|
||||
root.variable?.map((v: any) => ({
|
||||
name: v.key,
|
||||
value: v.value,
|
||||
})) ?? [],
|
||||
};
|
||||
exportResources.workspaces.push(workspace);
|
||||
|
||||
@@ -54,8 +61,9 @@ export function pluginHookImport(contents: string): { resources: ExportResources
|
||||
} else if (typeof v.name === 'string' && 'request' in v) {
|
||||
const r = toRecord(v.request);
|
||||
const bodyPatch = importBody(r.body);
|
||||
const authPatch = importAuth(r.auth);
|
||||
const request: ExportResources['requests'][0] = {
|
||||
const requestAuthPath = importAuth(r.auth);
|
||||
const authPatch = requestAuthPath.authenticationType == null ? globalAuth : requestAuthPath;
|
||||
const request: ExportResources['httpRequests'][0] = {
|
||||
model: 'http_request',
|
||||
id: generateId('rq'),
|
||||
workspaceId: workspace.id,
|
||||
@@ -79,7 +87,7 @@ export function pluginHookImport(contents: string): { resources: ExportResources
|
||||
}),
|
||||
],
|
||||
};
|
||||
exportResources.requests.push(request);
|
||||
exportResources.httpRequests.push(request);
|
||||
} else {
|
||||
console.log('Unknown item', v, folderId);
|
||||
}
|
||||
@@ -105,6 +113,14 @@ function importAuth(
|
||||
password: auth.basic.password || '',
|
||||
},
|
||||
};
|
||||
} else if ('bearer' in auth) {
|
||||
return {
|
||||
headers: [],
|
||||
authenticationType: 'bearer',
|
||||
authentication: {
|
||||
token: auth.bearer.token || '',
|
||||
},
|
||||
};
|
||||
} else {
|
||||
// TODO: support other auth types
|
||||
return { headers: [], authenticationType: null, authentication: {} };
|
||||
@@ -213,7 +229,7 @@ function convertTemplateSyntax<T>(obj: T): T {
|
||||
}
|
||||
}
|
||||
|
||||
function generateId(prefix: 'wk' | 'rq' | 'fl'): string {
|
||||
export function generateId(prefix: 'wk' | 'rq' | 'fl'): string {
|
||||
const alphabet = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
let id = `${prefix}_`;
|
||||
for (let i = 0; i < 10; i++) {
|
||||
|
||||
38
plugins/importer-postman/tests/fixtures/nested.json
vendored
Normal file
38
plugins/importer-postman/tests/fixtures/nested.json
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"info": {
|
||||
"_postman_id": "9e6dfada-256c-49ea-a38f-7d1b05b7ca2d",
|
||||
"name": "New Collection",
|
||||
"schema": "https://schema.getpostman.com/json/collection/v2.0.0/collection.json",
|
||||
"_exporter_id": "18798"
|
||||
},
|
||||
"item": [
|
||||
{
|
||||
"name": "Top Folder",
|
||||
"item": [
|
||||
{
|
||||
"name": "Nested Folder",
|
||||
"item": [
|
||||
{
|
||||
"name": "Request 1",
|
||||
"request": {
|
||||
"method": "GET"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Request 2",
|
||||
"request": {
|
||||
"method": "GET"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Request 3",
|
||||
"request": {
|
||||
"method": "GET"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
64
plugins/importer-postman/tests/index.test.ts
Normal file
64
plugins/importer-postman/tests/index.test.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import * as fs from 'node:fs';
|
||||
import * as path from 'node:path';
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
import { pluginHookImport } from '../src';
|
||||
|
||||
let originalRandom = Math.random;
|
||||
|
||||
describe('importer-postman', () => {
|
||||
beforeEach(() => {
|
||||
let i = 0;
|
||||
// Psuedo-random number generator to ensure consistent ID generation
|
||||
Math.random = vi.fn(() => ((i++ * 1000) % 133) / 100);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Math.random = originalRandom;
|
||||
});
|
||||
|
||||
const p = path.join(__dirname, 'fixtures');
|
||||
const fixtures = fs.readdirSync(p);
|
||||
|
||||
for (const fixture of fixtures) {
|
||||
test('Imports ' + fixture, () => {
|
||||
const contents = fs.readFileSync(path.join(p, fixture), 'utf-8');
|
||||
const imported = pluginHookImport(contents);
|
||||
expect(imported).toEqual({
|
||||
resources: expect.objectContaining({
|
||||
folders: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
name: 'Top Folder',
|
||||
workspaceId: 'wk_0G3J6M9QcT',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
name: 'Nested Folder',
|
||||
workspaceId: 'wk_0G3J6M9QcT',
|
||||
}),
|
||||
]),
|
||||
httpRequests: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
name: 'Request 1',
|
||||
workspaceId: 'wk_0G3J6M9QcT',
|
||||
folderId: 'fl_vundefinedyundefinedBundefinedE0H3',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
name: 'Request 2',
|
||||
workspaceId: 'wk_0G3J6M9QcT',
|
||||
folderId: 'fl_fWiZlundefinedoundefinedrundefined',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
name: 'Request 3',
|
||||
workspaceId: 'wk_0G3J6M9QcT',
|
||||
folderId: null,
|
||||
}),
|
||||
]),
|
||||
workspaces: [
|
||||
expect.objectContaining({
|
||||
name: 'New Collection',
|
||||
}),
|
||||
],
|
||||
}),
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
export function pluginHookImport(contents) {
|
||||
export function pluginHookImport(contents: string) {
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(contents);
|
||||
@@ -10,23 +10,20 @@ export function pluginHookImport(contents) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!('yaakSchema' in parsed)) {
|
||||
const isYaakExport = 'yaakSchema' in parsed;
|
||||
if (!isYaakExport) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Migrate v1 to v2 -- changes requests to httpRequests
|
||||
if (parsed.yaakSchema === 1) {
|
||||
if ('requests' in parsed.resources) {
|
||||
parsed.resources.httpRequests = parsed.resources.requests;
|
||||
parsed.yaakSchema = 2;
|
||||
delete parsed.resources.requests;
|
||||
}
|
||||
|
||||
if (parsed.yaakSchema === 2) {
|
||||
return { resources: parsed.resources }; // Should already be in the correct format
|
||||
}
|
||||
|
||||
return undefined;
|
||||
return { resources: parsed.resources }; // Should already be in the correct format
|
||||
}
|
||||
|
||||
export function isJSObject(obj) {
|
||||
export function isJSObject(obj: any) {
|
||||
return Object.prototype.toString.call(obj) === '[object Object]';
|
||||
}
|
||||
29
plugins/importer-yaak/tests/index.test.ts
Normal file
29
plugins/importer-yaak/tests/index.test.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import { pluginHookImport } from '../src';
|
||||
|
||||
describe('importer-yaak', () => {
|
||||
test('Skips invalid imports', () => {
|
||||
expect(pluginHookImport('not JSON')).toBeUndefined();
|
||||
expect(pluginHookImport('[]')).toBeUndefined();
|
||||
expect(pluginHookImport(JSON.stringify({ resources: {} }))).toBeUndefined();
|
||||
});
|
||||
|
||||
test('converts schema 1 to 2', () => {
|
||||
const imported = pluginHookImport(
|
||||
JSON.stringify({
|
||||
yaakSchema: 1,
|
||||
resources: {
|
||||
requests: [],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
expect(imported).toEqual(
|
||||
expect.objectContaining({
|
||||
resources: {
|
||||
httpRequests: [],
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -4,7 +4,7 @@ import { defineConfig } from 'vite';
|
||||
export default defineConfig({
|
||||
build: {
|
||||
lib: {
|
||||
entry: resolve(__dirname, 'src/index.js'),
|
||||
entry: resolve(__dirname, 'src/index.ts'),
|
||||
fileName: 'index',
|
||||
formats: ['es'],
|
||||
},
|
||||
|
||||
80
src-tauri/Cargo.lock
generated
80
src-tauri/Cargo.lock
generated
@@ -19,9 +19,9 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.8.6"
|
||||
version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91429305e9f0a25f6205c5b8e0d2db09e0708a7a6df0f42212bb56c32c8ac97a"
|
||||
checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"getrandom 0.2.11",
|
||||
@@ -161,16 +161,6 @@ dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atomic-write-file"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "edcdbedc2236483ab103a53415653d6b4442ea6141baf1ffa85df29635e88436"
|
||||
dependencies = [
|
||||
"nix",
|
||||
"rand 0.8.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.1.0"
|
||||
@@ -249,6 +239,12 @@ version = "0.21.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.22.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51"
|
||||
|
||||
[[package]]
|
||||
name = "base64ct"
|
||||
version = "1.6.0"
|
||||
@@ -2654,17 +2650,6 @@ version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54"
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.27.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053"
|
||||
dependencies = [
|
||||
"bitflags 2.4.1",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nodrop"
|
||||
version = "0.1.14"
|
||||
@@ -4314,9 +4299,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sqlx"
|
||||
version = "0.7.3"
|
||||
version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dba03c279da73694ef99763320dea58b51095dfe87d001b1d4b5fe78ba8763cf"
|
||||
checksum = "c9a2ccff1a000a5a59cd33da541d9f2fdcd9e6e8229cc200565942bff36d0aaa"
|
||||
dependencies = [
|
||||
"sqlx-core",
|
||||
"sqlx-macros",
|
||||
@@ -4327,9 +4312,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sqlx-core"
|
||||
version = "0.7.3"
|
||||
version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d84b0a3c3739e220d94b3239fd69fb1f74bc36e16643423bd99de3b43c21bfbd"
|
||||
checksum = "24ba59a9342a3d9bab6c56c118be528b27c9b60e490080e9711a04dccac83ef6"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
"atoi",
|
||||
@@ -4338,7 +4323,6 @@ dependencies = [
|
||||
"chrono",
|
||||
"crc",
|
||||
"crossbeam-queue",
|
||||
"dotenvy",
|
||||
"either",
|
||||
"event-listener",
|
||||
"futures-channel",
|
||||
@@ -4372,9 +4356,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sqlx-macros"
|
||||
version = "0.7.3"
|
||||
version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "89961c00dc4d7dffb7aee214964b065072bff69e36ddb9e2c107541f75e4f2a5"
|
||||
checksum = "4ea40e2345eb2faa9e1e5e326db8c34711317d2b5e08d0d5741619048a803127"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -4385,11 +4369,10 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sqlx-macros-core"
|
||||
version = "0.7.3"
|
||||
version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0bd4519486723648186a08785143599760f7cc81c52334a55d6a83ea1e20841"
|
||||
checksum = "5833ef53aaa16d860e92123292f1f6a3d53c34ba8b1969f152ef1a7bb803f3c8"
|
||||
dependencies = [
|
||||
"atomic-write-file",
|
||||
"dotenvy",
|
||||
"either",
|
||||
"heck 0.4.1",
|
||||
@@ -4412,9 +4395,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sqlx-mysql"
|
||||
version = "0.7.3"
|
||||
version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e37195395df71fd068f6e2082247891bc11e3289624bbc776a0cdfa1ca7f1ea4"
|
||||
checksum = "1ed31390216d20e538e447a7a9b959e06ed9fc51c37b514b46eb758016ecd418"
|
||||
dependencies = [
|
||||
"atoi",
|
||||
"base64 0.21.5",
|
||||
@@ -4456,9 +4439,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sqlx-postgres"
|
||||
version = "0.7.3"
|
||||
version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6ac0ac3b7ccd10cc96c7ab29791a7dd236bd94021f31eec7ba3d46a74aa1c24"
|
||||
checksum = "7c824eb80b894f926f89a0b9da0c7f435d27cdd35b8c655b114e58223918577e"
|
||||
dependencies = [
|
||||
"atoi",
|
||||
"base64 0.21.5",
|
||||
@@ -4484,7 +4467,6 @@ dependencies = [
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha1",
|
||||
"sha2",
|
||||
"smallvec",
|
||||
"sqlx-core",
|
||||
@@ -4497,9 +4479,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "sqlx-sqlite"
|
||||
version = "0.7.3"
|
||||
version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "210976b7d948c7ba9fced8ca835b11cbb2d677c59c79de41ac0d397e14547490"
|
||||
checksum = "b244ef0a8414da0bed4bb1910426e890b19e5e9bccc27ada6b797d05c55ae0aa"
|
||||
dependencies = [
|
||||
"atoi",
|
||||
"chrono",
|
||||
@@ -5123,9 +5105,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.34.0"
|
||||
version = "1.36.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0c014766411e834f7af5b8f4cf46257aab4036ca95e9d2c144a10f59ad6f5b9"
|
||||
checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"bytes",
|
||||
@@ -5181,9 +5163,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio-stream"
|
||||
version = "0.1.14"
|
||||
version = "0.1.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842"
|
||||
checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
@@ -6332,7 +6314,7 @@ dependencies = [
|
||||
name = "yaak-app"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"base64 0.21.5",
|
||||
"base64 0.22.0",
|
||||
"boa_engine",
|
||||
"boa_runtime",
|
||||
"chrono",
|
||||
@@ -6387,18 +6369,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.7.25"
|
||||
version = "0.7.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8cd369a67c0edfef15010f980c3cbe45d7f651deac2cd67ce097cd801de16557"
|
||||
checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.7.25"
|
||||
version = "0.7.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2f140bda219a26ccc0cdb03dba58af72590c53b22642577d88a927bc5c87d6b"
|
||||
checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
||||
@@ -22,7 +22,7 @@ cocoa = "0.25.0"
|
||||
openssl-sys = { version = "0.9", features = ["vendored"] } # For Ubuntu installation to work
|
||||
|
||||
[dependencies]
|
||||
base64 = "0.21.0"
|
||||
base64 = "0.22.0"
|
||||
boa_engine = { version = "0.17.3", features = ["annex-b"] }
|
||||
boa_runtime = { version = "0.17.3" }
|
||||
chrono = { version = "0.4.31", features = ["serde"] }
|
||||
@@ -33,7 +33,7 @@ reqwest = { version = "0.11.23", features = ["multipart", "cookies", "gzip", "br
|
||||
cookie = { version = "0.18.0" }
|
||||
serde = { version = "1.0.195", features = ["derive"] }
|
||||
serde_json = { version = "1.0.111", features = ["raw_value"] }
|
||||
sqlx = { version = "0.7.3", features = ["sqlite", "runtime-tokio-rustls", "json", "chrono", "time"] }
|
||||
sqlx = { version = "0.7.4", features = ["sqlite", "runtime-tokio-rustls", "json", "chrono", "time"] }
|
||||
tauri = { version = "1.5.4", features = [
|
||||
"config-toml",
|
||||
"path-all",
|
||||
@@ -56,14 +56,14 @@ tauri = { version = "1.5.4", features = [
|
||||
] }
|
||||
tauri-plugin-window-state = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1" }
|
||||
tauri-plugin-log = { git = "https://github.com/tauri-apps/plugins-workspace", branch = "v1", features = ["colored"] }
|
||||
tokio = { version = "1.25.0", features = ["sync"] }
|
||||
tokio = { version = "1.36.0", features = ["sync"] }
|
||||
uuid = "1.3.0"
|
||||
log = "0.4.20"
|
||||
datetime = "0.5.2"
|
||||
window-shadows = "0.2.2"
|
||||
reqwest_cookie_store = "0.6.0"
|
||||
grpc = { path = "./grpc" }
|
||||
tokio-stream = "0.1.14"
|
||||
tokio-stream = "0.1.15"
|
||||
|
||||
[features]
|
||||
# by default Tauri runs in production mode
|
||||
|
||||
@@ -8,6 +8,7 @@ use hyper_rustls::HttpsConnector;
|
||||
pub use prost_reflect::DynamicMessage;
|
||||
use prost_reflect::{DescriptorPool, MethodDescriptor, ServiceDescriptor};
|
||||
use serde_json::Deserializer;
|
||||
use tauri::AppHandle;
|
||||
use tokio_stream::wrappers::ReceiverStream;
|
||||
use tonic::body::BoxBody;
|
||||
use tonic::metadata::{MetadataKey, MetadataValue};
|
||||
@@ -166,13 +167,14 @@ impl GrpcConnection {
|
||||
}
|
||||
|
||||
pub struct GrpcHandle {
|
||||
app_handle: AppHandle,
|
||||
pools: HashMap<String, DescriptorPool>,
|
||||
}
|
||||
|
||||
impl Default for GrpcHandle {
|
||||
fn default() -> Self {
|
||||
impl GrpcHandle {
|
||||
pub fn new(app_handle: &AppHandle) -> Self {
|
||||
let pools = HashMap::new();
|
||||
Self { pools }
|
||||
Self { pools, app_handle: app_handle.clone() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,7 +185,7 @@ impl GrpcHandle {
|
||||
uri: &Uri,
|
||||
paths: Vec<PathBuf>,
|
||||
) -> Result<Vec<ServiceDefinition>, String> {
|
||||
let pool = fill_pool_from_files(paths).await?;
|
||||
let pool = fill_pool_from_files(&self.app_handle, paths).await?;
|
||||
self.pools.insert(self.get_pool_key(id, uri), pool.clone());
|
||||
Ok(self.services_from_pool(&pool))
|
||||
}
|
||||
@@ -237,7 +239,7 @@ impl GrpcHandle {
|
||||
None => match proto_files.len() {
|
||||
0 => fill_pool(&uri).await?,
|
||||
_ => {
|
||||
let pool = fill_pool_from_files(proto_files).await?;
|
||||
let pool = fill_pool_from_files(&self.app_handle, proto_files).await?;
|
||||
self.pools.insert(id.to_string(), pool.clone());
|
||||
pool
|
||||
}
|
||||
|
||||
@@ -4,33 +4,43 @@ use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use hyper::Client;
|
||||
use hyper::client::HttpConnector;
|
||||
use hyper::Client;
|
||||
use hyper_rustls::{HttpsConnector, HttpsConnectorBuilder};
|
||||
use log::{debug, info, warn};
|
||||
use prost::Message;
|
||||
use prost_reflect::{DescriptorPool, MethodDescriptor};
|
||||
use prost_types::{FileDescriptorProto, FileDescriptorSet};
|
||||
use tauri::api::process::{Command, CommandEvent};
|
||||
use tauri::AppHandle;
|
||||
use tokio::fs;
|
||||
use tokio_stream::StreamExt;
|
||||
use tonic::body::BoxBody;
|
||||
use tonic::codegen::http::uri::PathAndQuery;
|
||||
use tonic::Request;
|
||||
use tonic::transport::Uri;
|
||||
use tonic::Request;
|
||||
use tonic_reflection::pb::server_reflection_client::ServerReflectionClient;
|
||||
use tonic_reflection::pb::server_reflection_request::MessageRequest;
|
||||
use tonic_reflection::pb::server_reflection_response::MessageResponse;
|
||||
use tonic_reflection::pb::ServerReflectionRequest;
|
||||
|
||||
pub async fn fill_pool_from_files(paths: Vec<PathBuf>) -> Result<DescriptorPool, String> {
|
||||
pub async fn fill_pool_from_files(
|
||||
app_handle: &AppHandle,
|
||||
paths: Vec<PathBuf>,
|
||||
) -> Result<DescriptorPool, String> {
|
||||
let mut pool = DescriptorPool::new();
|
||||
let random_file_name = format!("{}.desc", uuid::Uuid::new_v4());
|
||||
let desc_path = temp_dir().join(random_file_name);
|
||||
let global_import_dir = app_handle
|
||||
.path_resolver()
|
||||
.resolve_resource("protoc-vendored/include")
|
||||
.expect("failed to resolve protoc include directory");
|
||||
|
||||
let mut args = vec![
|
||||
"--include_imports".to_string(),
|
||||
"--include_source_info".to_string(),
|
||||
"-I".to_string(),
|
||||
global_import_dir.to_string_lossy().to_string(),
|
||||
"-o".to_string(),
|
||||
desc_path.to_string_lossy().to_string(),
|
||||
];
|
||||
@@ -76,10 +86,7 @@ pub async fn fill_pool_from_files(paths: Vec<PathBuf>) -> Result<DescriptorPool,
|
||||
// success
|
||||
}
|
||||
Some(code) => {
|
||||
return Err(format!(
|
||||
"protoc failed with exit code: {}",
|
||||
code,
|
||||
));
|
||||
return Err(format!("protoc failed with exit code: {}", code,));
|
||||
}
|
||||
None => {
|
||||
return Err("protoc failed with no exit code".to_string());
|
||||
|
||||
@@ -1,27 +1,30 @@
|
||||
function S(e, t) {
|
||||
function g(e, n) {
|
||||
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,
|
||||
workspaceId: n,
|
||||
model: "environment",
|
||||
name: e.name,
|
||||
variables: Object.entries(e.data).map(([n, a]) => ({
|
||||
variables: Object.entries(e.data).map(([t, a]) => ({
|
||||
enabled: !0,
|
||||
name: n,
|
||||
name: t,
|
||||
value: `${a}`
|
||||
}))
|
||||
};
|
||||
}
|
||||
function I(e) {
|
||||
function S(e) {
|
||||
return m(e) && e._type === "workspace";
|
||||
}
|
||||
function y(e) {
|
||||
function I(e) {
|
||||
return m(e) && e._type === "request_group";
|
||||
}
|
||||
function g(e) {
|
||||
function y(e) {
|
||||
return m(e) && e._type === "request";
|
||||
}
|
||||
function h(e) {
|
||||
return m(e) && e._type === "grpc_request";
|
||||
}
|
||||
function f(e) {
|
||||
return m(e) && e._type === "environment";
|
||||
}
|
||||
@@ -32,103 +35,131 @@ function w(e) {
|
||||
return Object.prototype.toString.call(e) === "[object String]";
|
||||
}
|
||||
function O(e) {
|
||||
return Object.entries(e).map(([t, n]) => ({
|
||||
return Object.entries(e).map(([n, t]) => ({
|
||||
enabled: !0,
|
||||
name: t,
|
||||
value: `${n}`
|
||||
name: n,
|
||||
value: `${t}`
|
||||
}));
|
||||
}
|
||||
function l(e) {
|
||||
function d(e) {
|
||||
return w(e) ? e.replaceAll(/{{\s*(_\.)?([^}]+)\s*}}/g, "${[$2]}") : e;
|
||||
}
|
||||
function h(e, t, n = 0) {
|
||||
var c, o;
|
||||
function _(e, n, t = 0) {
|
||||
var l, r;
|
||||
console.log("IMPORTING REQUEST", e._id, e.name, JSON.stringify(e, null, 2));
|
||||
let a = null, r = null;
|
||||
((c = e.body) == null ? void 0 : c.mimeType) === "application/graphql" ? (a = "graphql", r = l(e.body.text)) : ((o = e.body) == null ? void 0 : o.mimeType) === "application/json" && (a = "application/json", r = l(e.body.text));
|
||||
let i = null, u = {};
|
||||
return e.authentication.type === "bearer" ? (i = "bearer", u = {
|
||||
token: l(e.authentication.token)
|
||||
}) : e.authentication.type === "basic" && (i = "basic", u = {
|
||||
username: l(e.authentication.username),
|
||||
password: l(e.authentication.password)
|
||||
let a = null, o = null;
|
||||
((l = e.body) == null ? void 0 : l.mimeType) === "application/graphql" ? (a = "graphql", o = d(e.body.text)) : ((r = e.body) == null ? void 0 : r.mimeType) === "application/json" && (a = "application/json", o = d(e.body.text));
|
||||
let s = null, p = {};
|
||||
return e.authentication.type === "bearer" ? (s = "bearer", p = {
|
||||
token: d(e.authentication.token)
|
||||
}) : e.authentication.type === "basic" && (s = "basic", p = {
|
||||
username: d(e.authentication.username),
|
||||
password: d(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,
|
||||
workspaceId: n,
|
||||
folderId: e.parentId === n ? null : e.parentId,
|
||||
model: "http_request",
|
||||
sortPriority: n,
|
||||
sortPriority: t,
|
||||
name: e.name,
|
||||
url: l(e.url),
|
||||
body: r,
|
||||
url: d(e.url),
|
||||
body: o,
|
||||
bodyType: a,
|
||||
authentication: u,
|
||||
authenticationType: i,
|
||||
authentication: p,
|
||||
authenticationType: s,
|
||||
method: e.method,
|
||||
headers: (e.headers ?? []).map(({ name: d, value: p, disabled: s }) => ({
|
||||
enabled: !s,
|
||||
name: d,
|
||||
value: p
|
||||
})).filter(({ name: d, value: p }) => d !== "" || p !== "")
|
||||
headers: (e.headers ?? []).map(({ name: u, value: c, disabled: i }) => ({
|
||||
enabled: !i,
|
||||
name: u,
|
||||
value: c
|
||||
})).filter(({ name: u, value: c }) => u !== "" || c !== "")
|
||||
};
|
||||
}
|
||||
function _(e, t) {
|
||||
function R(e, n) {
|
||||
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,
|
||||
folderId: e.parentId === n ? null : e.parentId,
|
||||
workspaceId: n,
|
||||
model: "folder",
|
||||
name: e.name
|
||||
};
|
||||
}
|
||||
function b(e) {
|
||||
console.log("RUNNING INSOMNIA");
|
||||
let t;
|
||||
function D(e, n, t = 0) {
|
||||
var p;
|
||||
console.log("IMPORTING GRPC REQUEST", e._id, e.name, JSON.stringify(e, null, 2));
|
||||
const a = e.protoMethodName.split("/").filter((l) => l !== ""), o = a[0] ?? null, s = a[1] ?? null;
|
||||
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: n,
|
||||
folderId: e.parentId === n ? null : e.parentId,
|
||||
model: "grpc_request",
|
||||
sortPriority: t,
|
||||
name: e.name,
|
||||
url: d(e.url),
|
||||
service: o,
|
||||
method: s,
|
||||
message: ((p = e.body) == null ? void 0 : p.text) ?? "",
|
||||
metadata: (e.metadata ?? []).map(({ name: l, value: r, disabled: u }) => ({
|
||||
enabled: !u,
|
||||
name: l,
|
||||
value: r
|
||||
})).filter(({ name: l, value: r }) => l !== "" || r !== "")
|
||||
};
|
||||
}
|
||||
function q(e) {
|
||||
let n;
|
||||
try {
|
||||
t = JSON.parse(e);
|
||||
n = JSON.parse(e);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
if (!m(t) || !Array.isArray(t.resources))
|
||||
if (!m(n) || !Array.isArray(n.resources))
|
||||
return;
|
||||
const n = {
|
||||
const t = {
|
||||
workspaces: [],
|
||||
requests: [],
|
||||
httpRequests: [],
|
||||
grpcRequests: [],
|
||||
environments: [],
|
||||
folders: []
|
||||
}, a = t.resources.filter(I);
|
||||
for (const r of a) {
|
||||
const i = t.resources.find(
|
||||
(o) => f(o) && o.parentId === r._id
|
||||
}, a = n.resources.filter(S);
|
||||
for (const o of a) {
|
||||
const s = n.resources.find(
|
||||
(r) => f(r) && r.parentId === o._id
|
||||
);
|
||||
n.workspaces.push({
|
||||
id: r._id,
|
||||
t.workspaces.push({
|
||||
id: o._id,
|
||||
createdAt: new Date(a.created ?? Date.now()).toISOString().replace("Z", ""),
|
||||
updatedAt: new Date(a.updated ?? Date.now()).toISOString().replace("Z", ""),
|
||||
model: "workspace",
|
||||
name: r.name,
|
||||
variables: i ? O(i.data) : []
|
||||
name: o.name,
|
||||
variables: s ? O(s.data) : []
|
||||
});
|
||||
const u = t.resources.filter(
|
||||
(o) => f(o) && o.parentId === (i == null ? void 0 : i._id)
|
||||
const p = n.resources.filter(
|
||||
(r) => f(r) && r.parentId === (s == null ? void 0 : s._id)
|
||||
);
|
||||
n.environments.push(
|
||||
...u.map((o) => S(o, r._id))
|
||||
t.environments.push(
|
||||
...p.map((r) => g(r, o._id))
|
||||
);
|
||||
const c = (o) => {
|
||||
const d = t.resources.filter((s) => s.parentId === o);
|
||||
let p = 0;
|
||||
for (const s of d)
|
||||
y(s) ? (n.folders.push(_(s, r._id)), c(s._id)) : g(s) && n.requests.push(h(s, r._id, p++));
|
||||
const l = (r) => {
|
||||
const u = n.resources.filter((i) => i.parentId === r);
|
||||
let c = 0;
|
||||
for (const i of u)
|
||||
I(i) ? (t.folders.push(R(i, o._id)), l(i._id)) : y(i) ? t.httpRequests.push(
|
||||
_(i, o._id, c++)
|
||||
) : h(i) && (console.log("GRPC", JSON.stringify(i, null, 1)), t.grpcRequests.push(
|
||||
D(i, o._id, c++)
|
||||
));
|
||||
};
|
||||
c(r._id);
|
||||
l(o._id);
|
||||
}
|
||||
return n.requests = n.requests.filter(Boolean), n.environments = n.environments.filter(Boolean), n.workspaces = n.workspaces.filter(Boolean), { resources: n };
|
||||
return t.httpRequests = t.httpRequests.filter(Boolean), t.grpcRequests = t.grpcRequests.filter(Boolean), t.environments = t.environments.filter(Boolean), t.workspaces = t.workspaces.filter(Boolean), { resources: t };
|
||||
}
|
||||
export {
|
||||
b as pluginHookImport
|
||||
q as pluginHookImport
|
||||
};
|
||||
|
||||
@@ -1,44 +1,49 @@
|
||||
const T = "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", w = "https://schema.getpostman.com/json/collection/v2.0.0/collection.json", A = [w, T];
|
||||
function q(e) {
|
||||
const t = b(e);
|
||||
if (t == null)
|
||||
const q = "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", S = "https://schema.getpostman.com/json/collection/v2.0.0/collection.json", _ = [S, q];
|
||||
function v(t) {
|
||||
var b;
|
||||
const e = w(t);
|
||||
if (e == null)
|
||||
return;
|
||||
const n = a(t.info);
|
||||
if (!A.includes(n.schema) || !Array.isArray(t.item))
|
||||
const n = o(e.info);
|
||||
if (!_.includes(n.schema) || !Array.isArray(e.item))
|
||||
return;
|
||||
const i = {
|
||||
const A = g(e.auth), i = {
|
||||
workspaces: [],
|
||||
environments: [],
|
||||
requests: [],
|
||||
httpRequests: [],
|
||||
folders: []
|
||||
}, c = {
|
||||
model: "workspace",
|
||||
id: m("wk"),
|
||||
name: n.name || "Postman Import",
|
||||
description: n.description || ""
|
||||
description: n.description || "",
|
||||
variables: ((b = e.variable) == null ? void 0 : b.map((r) => ({
|
||||
name: r.key,
|
||||
value: r.value
|
||||
}))) ?? []
|
||||
};
|
||||
i.workspaces.push(c);
|
||||
const f = (r, u = null) => {
|
||||
if (typeof r.name == "string" && Array.isArray(r.item)) {
|
||||
const o = {
|
||||
const a = {
|
||||
model: "folder",
|
||||
workspaceId: c.id,
|
||||
id: m("fl"),
|
||||
name: r.name,
|
||||
folderId: u
|
||||
};
|
||||
i.folders.push(o);
|
||||
i.folders.push(a);
|
||||
for (const s of r.item)
|
||||
f(s, o.id);
|
||||
f(s, a.id);
|
||||
} else if (typeof r.name == "string" && "request" in r) {
|
||||
const o = a(r.request), s = k(o.body), d = S(o.auth), g = {
|
||||
const a = o(r.request), s = O(a.body), T = g(a.auth), d = T.authenticationType == null ? A : T, k = {
|
||||
model: "http_request",
|
||||
id: m("rq"),
|
||||
workspaceId: c.id,
|
||||
folderId: u,
|
||||
name: r.name,
|
||||
method: o.method || "GET",
|
||||
url: typeof o.url == "string" ? o.url : a(o.url).raw,
|
||||
method: a.method || "GET",
|
||||
url: typeof a.url == "string" ? a.url : o(a.url).raw,
|
||||
body: s.body,
|
||||
bodyType: s.bodyType,
|
||||
authentication: d.authentication,
|
||||
@@ -46,35 +51,41 @@ function q(e) {
|
||||
headers: [
|
||||
...s.headers,
|
||||
...d.headers,
|
||||
...y(o.header).map((p) => ({
|
||||
...y(a.header).map((p) => ({
|
||||
name: p.key,
|
||||
value: p.value,
|
||||
enabled: !p.disabled
|
||||
}))
|
||||
]
|
||||
};
|
||||
i.requests.push(g);
|
||||
i.httpRequests.push(k);
|
||||
} else
|
||||
console.log("Unknown item", r, u);
|
||||
};
|
||||
for (const r of t.item)
|
||||
for (const r of e.item)
|
||||
f(r);
|
||||
return { resources: h(i) };
|
||||
}
|
||||
function S(e) {
|
||||
const t = a(e);
|
||||
return "basic" in t ? {
|
||||
function g(t) {
|
||||
const e = o(t);
|
||||
return "basic" in e ? {
|
||||
headers: [],
|
||||
authenticationType: "basic",
|
||||
authentication: {
|
||||
username: t.basic.username || "",
|
||||
password: t.basic.password || ""
|
||||
username: e.basic.username || "",
|
||||
password: e.basic.password || ""
|
||||
}
|
||||
} : "bearer" in e ? {
|
||||
headers: [],
|
||||
authenticationType: "bearer",
|
||||
authentication: {
|
||||
token: e.bearer.token || ""
|
||||
}
|
||||
} : { headers: [], authenticationType: null, authentication: {} };
|
||||
}
|
||||
function k(e) {
|
||||
const t = a(e);
|
||||
return "graphql" in t ? {
|
||||
function O(t) {
|
||||
const e = o(t);
|
||||
return "graphql" in e ? {
|
||||
headers: [
|
||||
{
|
||||
name: "Content-Type",
|
||||
@@ -85,12 +96,12 @@ function k(e) {
|
||||
bodyType: "graphql",
|
||||
body: {
|
||||
text: JSON.stringify(
|
||||
{ query: t.graphql.query, variables: b(t.graphql.variables) },
|
||||
{ query: e.graphql.query, variables: w(e.graphql.variables) },
|
||||
null,
|
||||
2
|
||||
)
|
||||
}
|
||||
} : "urlencoded" in t ? {
|
||||
} : "urlencoded" in e ? {
|
||||
headers: [
|
||||
{
|
||||
name: "Content-Type",
|
||||
@@ -100,13 +111,13 @@ function k(e) {
|
||||
],
|
||||
bodyType: "application/x-www-form-urlencoded",
|
||||
body: {
|
||||
form: y(t.urlencoded).map((n) => ({
|
||||
form: y(e.urlencoded).map((n) => ({
|
||||
enabled: !n.disabled,
|
||||
name: n.key ?? "",
|
||||
value: n.value ?? ""
|
||||
}))
|
||||
}
|
||||
} : "formdata" in t ? {
|
||||
} : "formdata" in e ? {
|
||||
headers: [
|
||||
{
|
||||
name: "Content-Type",
|
||||
@@ -116,7 +127,7 @@ function k(e) {
|
||||
],
|
||||
bodyType: "multipart/form-data",
|
||||
body: {
|
||||
form: y(t.formdata).map(
|
||||
form: y(e.formdata).map(
|
||||
(n) => n.src != null ? {
|
||||
enabled: !n.disabled,
|
||||
name: n.key ?? "",
|
||||
@@ -130,31 +141,32 @@ function k(e) {
|
||||
}
|
||||
} : { headers: [], bodyType: null, body: {} };
|
||||
}
|
||||
function b(e) {
|
||||
function w(t) {
|
||||
try {
|
||||
return a(JSON.parse(e));
|
||||
return o(JSON.parse(t));
|
||||
} catch {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function a(e) {
|
||||
return Object.prototype.toString.call(e) === "[object Object]" ? e : {};
|
||||
function o(t) {
|
||||
return Object.prototype.toString.call(t) === "[object Object]" ? t : {};
|
||||
}
|
||||
function y(e) {
|
||||
return Object.prototype.toString.call(e) === "[object Array]" ? e : [];
|
||||
function y(t) {
|
||||
return Object.prototype.toString.call(t) === "[object Array]" ? t : [];
|
||||
}
|
||||
function h(e) {
|
||||
return typeof e == "string" ? e.replace(/{{\s*(_\.)?([^}]+)\s*}}/g, "${[$2]}") : Array.isArray(e) && e != null ? e.map(h) : typeof e == "object" && e != null ? Object.fromEntries(
|
||||
Object.entries(e).map(([t, n]) => [t, h(n)])
|
||||
) : e;
|
||||
function h(t) {
|
||||
return typeof t == "string" ? t.replace(/{{\s*(_\.)?([^}]+)\s*}}/g, "${[$2]}") : Array.isArray(t) && t != null ? t.map(h) : typeof t == "object" && t != null ? Object.fromEntries(
|
||||
Object.entries(t).map(([e, n]) => [e, h(n)])
|
||||
) : t;
|
||||
}
|
||||
function m(e) {
|
||||
const t = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
let n = `${e}_`;
|
||||
function m(t) {
|
||||
const e = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
let n = `${t}_`;
|
||||
for (let l = 0; l < 10; l++)
|
||||
n += t[Math.floor(Math.random() * t.length)];
|
||||
n += e[Math.floor(Math.random() * e.length)];
|
||||
return n;
|
||||
}
|
||||
export {
|
||||
q as pluginHookImport
|
||||
m as generateId,
|
||||
v as pluginHookImport
|
||||
};
|
||||
|
||||
@@ -5,8 +5,8 @@ function u(r) {
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
if (t(e) && "yaakSchema" in e && (e.yaakSchema === 1 && (e.resources.httpRequests = e.resources.requests, e.yaakSchema = 2), e.yaakSchema === 2))
|
||||
return { resources: e.resources };
|
||||
if (!(!t(e) || !("yaakSchema" in e)))
|
||||
return "requests" in e.resources && (e.resources.httpRequests = e.resources.requests, delete e.resources.requests), { resources: e.resources };
|
||||
}
|
||||
function t(r) {
|
||||
return Object.prototype.toString.call(r) === "[object Object]";
|
||||
|
||||
162
src-tauri/protoc-vendored/include/google/protobuf/any.proto
Normal file
162
src-tauri/protoc-vendored/include/google/protobuf/any.proto
Normal file
@@ -0,0 +1,162 @@
|
||||
// Protocol Buffers - Google's data interchange format
|
||||
// Copyright 2008 Google Inc. All rights reserved.
|
||||
// https://developers.google.com/protocol-buffers/
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without
|
||||
// modification, are permitted provided that the following conditions are
|
||||
// met:
|
||||
//
|
||||
// * Redistributions of source code must retain the above copyright
|
||||
// notice, this list of conditions and the following disclaimer.
|
||||
// * Redistributions in binary form must reproduce the above
|
||||
// copyright notice, this list of conditions and the following disclaimer
|
||||
// in the documentation and/or other materials provided with the
|
||||
// distribution.
|
||||
// * Neither the name of Google Inc. nor the names of its
|
||||
// contributors may be used to endorse or promote products derived from
|
||||
// this software without specific prior written permission.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
syntax = "proto3";
|
||||
|
||||
package google.protobuf;
|
||||
|
||||
option go_package = "google.golang.org/protobuf/types/known/anypb";
|
||||
option java_package = "com.google.protobuf";
|
||||
option java_outer_classname = "AnyProto";
|
||||
option java_multiple_files = true;
|
||||
option objc_class_prefix = "GPB";
|
||||
option csharp_namespace = "Google.Protobuf.WellKnownTypes";
|
||||
|
||||
// `Any` contains an arbitrary serialized protocol buffer message along with a
|
||||
// URL that describes the type of the serialized message.
|
||||
//
|
||||
// Protobuf library provides support to pack/unpack Any values in the form
|
||||
// of utility functions or additional generated methods of the Any type.
|
||||
//
|
||||
// Example 1: Pack and unpack a message in C++.
|
||||
//
|
||||
// Foo foo = ...;
|
||||
// Any any;
|
||||
// any.PackFrom(foo);
|
||||
// ...
|
||||
// if (any.UnpackTo(&foo)) {
|
||||
// ...
|
||||
// }
|
||||
//
|
||||
// Example 2: Pack and unpack a message in Java.
|
||||
//
|
||||
// Foo foo = ...;
|
||||
// Any any = Any.pack(foo);
|
||||
// ...
|
||||
// if (any.is(Foo.class)) {
|
||||
// foo = any.unpack(Foo.class);
|
||||
// }
|
||||
// // or ...
|
||||
// if (any.isSameTypeAs(Foo.getDefaultInstance())) {
|
||||
// foo = any.unpack(Foo.getDefaultInstance());
|
||||
// }
|
||||
//
|
||||
// Example 3: Pack and unpack a message in Python.
|
||||
//
|
||||
// foo = Foo(...)
|
||||
// any = Any()
|
||||
// any.Pack(foo)
|
||||
// ...
|
||||
// if any.Is(Foo.DESCRIPTOR):
|
||||
// any.Unpack(foo)
|
||||
// ...
|
||||
//
|
||||
// Example 4: Pack and unpack a message in Go
|
||||
//
|
||||
// foo := &pb.Foo{...}
|
||||
// any, err := anypb.New(foo)
|
||||
// if err != nil {
|
||||
// ...
|
||||
// }
|
||||
// ...
|
||||
// foo := &pb.Foo{}
|
||||
// if err := any.UnmarshalTo(foo); err != nil {
|
||||
// ...
|
||||
// }
|
||||
//
|
||||
// The pack methods provided by protobuf library will by default use
|
||||
// 'type.googleapis.com/full.type.name' as the type URL and the unpack
|
||||
// methods only use the fully qualified type name after the last '/'
|
||||
// in the type URL, for example "foo.bar.com/x/y.z" will yield type
|
||||
// name "y.z".
|
||||
//
|
||||
// JSON
|
||||
// ====
|
||||
// The JSON representation of an `Any` value uses the regular
|
||||
// representation of the deserialized, embedded message, with an
|
||||
// additional field `@type` which contains the type URL. Example:
|
||||
//
|
||||
// package google.profile;
|
||||
// message Person {
|
||||
// string first_name = 1;
|
||||
// string last_name = 2;
|
||||
// }
|
||||
//
|
||||
// {
|
||||
// "@type": "type.googleapis.com/google.profile.Person",
|
||||
// "firstName": <string>,
|
||||
// "lastName": <string>
|
||||
// }
|
||||
//
|
||||
// If the embedded message type is well-known and has a custom JSON
|
||||
// representation, that representation will be embedded adding a field
|
||||
// `value` which holds the custom JSON in addition to the `@type`
|
||||
// field. Example (for message [google.protobuf.Duration][]):
|
||||
//
|
||||
// {
|
||||
// "@type": "type.googleapis.com/google.protobuf.Duration",
|
||||
// "value": "1.212s"
|
||||
// }
|
||||
//
|
||||
message Any {
|
||||
// A URL/resource name that uniquely identifies the type of the serialized
|
||||
// protocol buffer message. This string must contain at least
|
||||
// one "/" character. The last segment of the URL's path must represent
|
||||
// the fully qualified name of the type (as in
|
||||
// `path/google.protobuf.Duration`). The name should be in a canonical form
|
||||
// (e.g., leading "." is not accepted).
|
||||
//
|
||||
// In practice, teams usually precompile into the binary all types that they
|
||||
// expect it to use in the context of Any. However, for URLs which use the
|
||||
// scheme `http`, `https`, or no scheme, one can optionally set up a type
|
||||
// server that maps type URLs to message definitions as follows:
|
||||
//
|
||||
// * If no scheme is provided, `https` is assumed.
|
||||
// * An HTTP GET on the URL must yield a [google.protobuf.Type][]
|
||||
// value in binary format, or produce an error.
|
||||
// * Applications are allowed to cache lookup results based on the
|
||||
// URL, or have them precompiled into a binary to avoid any
|
||||
// lookup. Therefore, binary compatibility needs to be preserved
|
||||
// on changes to types. (Use versioned type names to manage
|
||||
// breaking changes.)
|
||||
//
|
||||
// Note: this functionality is not currently available in the official
|
||||
// protobuf release, and it is not used for type URLs beginning with
|
||||
// type.googleapis.com. As of May 2023, there are no widely used type server
|
||||
// implementations and no plans to implement one.
|
||||
//
|
||||
// Schemes other than `http`, `https` (or the empty scheme) might be
|
||||
// used with implementation specific semantics.
|
||||
//
|
||||
string type_url = 1;
|
||||
|
||||
// Must be a valid serialized protocol buffer of the above specified type.
|
||||
bytes value = 2;
|
||||
}
|
||||
207
src-tauri/protoc-vendored/include/google/protobuf/api.proto
Normal file
207
src-tauri/protoc-vendored/include/google/protobuf/api.proto
Normal file
@@ -0,0 +1,207 @@
|
||||
// Protocol Buffers - Google's data interchange format
|
||||
// Copyright 2008 Google Inc. All rights reserved.
|
||||
// https://developers.google.com/protocol-buffers/
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without
|
||||
// modification, are permitted provided that the following conditions are
|
||||
// met:
|
||||
//
|
||||
// * Redistributions of source code must retain the above copyright
|
||||
// notice, this list of conditions and the following disclaimer.
|
||||
// * Redistributions in binary form must reproduce the above
|
||||
// copyright notice, this list of conditions and the following disclaimer
|
||||
// in the documentation and/or other materials provided with the
|
||||
// distribution.
|
||||
// * Neither the name of Google Inc. nor the names of its
|
||||
// contributors may be used to endorse or promote products derived from
|
||||
// this software without specific prior written permission.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
syntax = "proto3";
|
||||
|
||||
package google.protobuf;
|
||||
|
||||
import "google/protobuf/source_context.proto";
|
||||
import "google/protobuf/type.proto";
|
||||
|
||||
option java_package = "com.google.protobuf";
|
||||
option java_outer_classname = "ApiProto";
|
||||
option java_multiple_files = true;
|
||||
option objc_class_prefix = "GPB";
|
||||
option csharp_namespace = "Google.Protobuf.WellKnownTypes";
|
||||
option go_package = "google.golang.org/protobuf/types/known/apipb";
|
||||
|
||||
// Api is a light-weight descriptor for an API Interface.
|
||||
//
|
||||
// Interfaces are also described as "protocol buffer services" in some contexts,
|
||||
// such as by the "service" keyword in a .proto file, but they are different
|
||||
// from API Services, which represent a concrete implementation of an interface
|
||||
// as opposed to simply a description of methods and bindings. They are also
|
||||
// sometimes simply referred to as "APIs" in other contexts, such as the name of
|
||||
// this message itself. See https://cloud.google.com/apis/design/glossary for
|
||||
// detailed terminology.
|
||||
message Api {
|
||||
// The fully qualified name of this interface, including package name
|
||||
// followed by the interface's simple name.
|
||||
string name = 1;
|
||||
|
||||
// The methods of this interface, in unspecified order.
|
||||
repeated Method methods = 2;
|
||||
|
||||
// Any metadata attached to the interface.
|
||||
repeated Option options = 3;
|
||||
|
||||
// A version string for this interface. If specified, must have the form
|
||||
// `major-version.minor-version`, as in `1.10`. If the minor version is
|
||||
// omitted, it defaults to zero. If the entire version field is empty, the
|
||||
// major version is derived from the package name, as outlined below. If the
|
||||
// field is not empty, the version in the package name will be verified to be
|
||||
// consistent with what is provided here.
|
||||
//
|
||||
// The versioning schema uses [semantic
|
||||
// versioning](http://semver.org) where the major version number
|
||||
// indicates a breaking change and the minor version an additive,
|
||||
// non-breaking change. Both version numbers are signals to users
|
||||
// what to expect from different versions, and should be carefully
|
||||
// chosen based on the product plan.
|
||||
//
|
||||
// The major version is also reflected in the package name of the
|
||||
// interface, which must end in `v<major-version>`, as in
|
||||
// `google.feature.v1`. For major versions 0 and 1, the suffix can
|
||||
// be omitted. Zero major versions must only be used for
|
||||
// experimental, non-GA interfaces.
|
||||
//
|
||||
string version = 4;
|
||||
|
||||
// Source context for the protocol buffer service represented by this
|
||||
// message.
|
||||
SourceContext source_context = 5;
|
||||
|
||||
// Included interfaces. See [Mixin][].
|
||||
repeated Mixin mixins = 6;
|
||||
|
||||
// The source syntax of the service.
|
||||
Syntax syntax = 7;
|
||||
}
|
||||
|
||||
// Method represents a method of an API interface.
|
||||
message Method {
|
||||
// The simple name of this method.
|
||||
string name = 1;
|
||||
|
||||
// A URL of the input message type.
|
||||
string request_type_url = 2;
|
||||
|
||||
// If true, the request is streamed.
|
||||
bool request_streaming = 3;
|
||||
|
||||
// The URL of the output message type.
|
||||
string response_type_url = 4;
|
||||
|
||||
// If true, the response is streamed.
|
||||
bool response_streaming = 5;
|
||||
|
||||
// Any metadata attached to the method.
|
||||
repeated Option options = 6;
|
||||
|
||||
// The source syntax of this method.
|
||||
Syntax syntax = 7;
|
||||
}
|
||||
|
||||
// Declares an API Interface to be included in this interface. The including
|
||||
// interface must redeclare all the methods from the included interface, but
|
||||
// documentation and options are inherited as follows:
|
||||
//
|
||||
// - If after comment and whitespace stripping, the documentation
|
||||
// string of the redeclared method is empty, it will be inherited
|
||||
// from the original method.
|
||||
//
|
||||
// - Each annotation belonging to the service config (http,
|
||||
// visibility) which is not set in the redeclared method will be
|
||||
// inherited.
|
||||
//
|
||||
// - If an http annotation is inherited, the path pattern will be
|
||||
// modified as follows. Any version prefix will be replaced by the
|
||||
// version of the including interface plus the [root][] path if
|
||||
// specified.
|
||||
//
|
||||
// Example of a simple mixin:
|
||||
//
|
||||
// package google.acl.v1;
|
||||
// service AccessControl {
|
||||
// // Get the underlying ACL object.
|
||||
// rpc GetAcl(GetAclRequest) returns (Acl) {
|
||||
// option (google.api.http).get = "/v1/{resource=**}:getAcl";
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// package google.storage.v2;
|
||||
// service Storage {
|
||||
// rpc GetAcl(GetAclRequest) returns (Acl);
|
||||
//
|
||||
// // Get a data record.
|
||||
// rpc GetData(GetDataRequest) returns (Data) {
|
||||
// option (google.api.http).get = "/v2/{resource=**}";
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// Example of a mixin configuration:
|
||||
//
|
||||
// apis:
|
||||
// - name: google.storage.v2.Storage
|
||||
// mixins:
|
||||
// - name: google.acl.v1.AccessControl
|
||||
//
|
||||
// The mixin construct implies that all methods in `AccessControl` are
|
||||
// also declared with same name and request/response types in
|
||||
// `Storage`. A documentation generator or annotation processor will
|
||||
// see the effective `Storage.GetAcl` method after inherting
|
||||
// documentation and annotations as follows:
|
||||
//
|
||||
// service Storage {
|
||||
// // Get the underlying ACL object.
|
||||
// rpc GetAcl(GetAclRequest) returns (Acl) {
|
||||
// option (google.api.http).get = "/v2/{resource=**}:getAcl";
|
||||
// }
|
||||
// ...
|
||||
// }
|
||||
//
|
||||
// Note how the version in the path pattern changed from `v1` to `v2`.
|
||||
//
|
||||
// If the `root` field in the mixin is specified, it should be a
|
||||
// relative path under which inherited HTTP paths are placed. Example:
|
||||
//
|
||||
// apis:
|
||||
// - name: google.storage.v2.Storage
|
||||
// mixins:
|
||||
// - name: google.acl.v1.AccessControl
|
||||
// root: acls
|
||||
//
|
||||
// This implies the following inherited HTTP annotation:
|
||||
//
|
||||
// service Storage {
|
||||
// // Get the underlying ACL object.
|
||||
// rpc GetAcl(GetAclRequest) returns (Acl) {
|
||||
// option (google.api.http).get = "/v2/acls/{resource=**}:getAcl";
|
||||
// }
|
||||
// ...
|
||||
// }
|
||||
message Mixin {
|
||||
// The fully qualified name of the interface which is included.
|
||||
string name = 1;
|
||||
|
||||
// If non-empty specifies a path under which inherited HTTP paths
|
||||
// are rooted.
|
||||
string root = 2;
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
// Protocol Buffers - Google's data interchange format
|
||||
// Copyright 2008 Google Inc. All rights reserved.
|
||||
//
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file or at
|
||||
// https://developers.google.com/open-source/licenses/bsd
|
||||
|
||||
// Author: kenton@google.com (Kenton Varda)
|
||||
//
|
||||
// protoc (aka the Protocol Compiler) can be extended via plugins. A plugin is
|
||||
// just a program that reads a CodeGeneratorRequest from stdin and writes a
|
||||
// CodeGeneratorResponse to stdout.
|
||||
//
|
||||
// Plugins written using C++ can use google/protobuf/compiler/plugin.h instead
|
||||
// of dealing with the raw protocol defined here.
|
||||
//
|
||||
// A plugin executable needs only to be placed somewhere in the path. The
|
||||
// plugin should be named "protoc-gen-$NAME", and will then be used when the
|
||||
// flag "--${NAME}_out" is passed to protoc.
|
||||
|
||||
syntax = "proto2";
|
||||
|
||||
package google.protobuf.compiler;
|
||||
option java_package = "com.google.protobuf.compiler";
|
||||
option java_outer_classname = "PluginProtos";
|
||||
|
||||
option csharp_namespace = "Google.Protobuf.Compiler";
|
||||
option go_package = "google.golang.org/protobuf/types/pluginpb";
|
||||
|
||||
import "google/protobuf/descriptor.proto";
|
||||
|
||||
// The version number of protocol compiler.
|
||||
message Version {
|
||||
optional int32 major = 1;
|
||||
optional int32 minor = 2;
|
||||
optional int32 patch = 3;
|
||||
// A suffix for alpha, beta or rc release, e.g., "alpha-1", "rc2". It should
|
||||
// be empty for mainline stable releases.
|
||||
optional string suffix = 4;
|
||||
}
|
||||
|
||||
// An encoded CodeGeneratorRequest is written to the plugin's stdin.
|
||||
message CodeGeneratorRequest {
|
||||
// The .proto files that were explicitly listed on the command-line. The
|
||||
// code generator should generate code only for these files. Each file's
|
||||
// descriptor will be included in proto_file, below.
|
||||
repeated string file_to_generate = 1;
|
||||
|
||||
// The generator parameter passed on the command-line.
|
||||
optional string parameter = 2;
|
||||
|
||||
// FileDescriptorProtos for all files in files_to_generate and everything
|
||||
// they import. The files will appear in topological order, so each file
|
||||
// appears before any file that imports it.
|
||||
//
|
||||
// Note: the files listed in files_to_generate will include runtime-retention
|
||||
// options only, but all other files will include source-retention options.
|
||||
// The source_file_descriptors field below is available in case you need
|
||||
// source-retention options for files_to_generate.
|
||||
//
|
||||
// protoc guarantees that all proto_files will be written after
|
||||
// the fields above, even though this is not technically guaranteed by the
|
||||
// protobuf wire format. This theoretically could allow a plugin to stream
|
||||
// in the FileDescriptorProtos and handle them one by one rather than read
|
||||
// the entire set into memory at once. However, as of this writing, this
|
||||
// is not similarly optimized on protoc's end -- it will store all fields in
|
||||
// memory at once before sending them to the plugin.
|
||||
//
|
||||
// Type names of fields and extensions in the FileDescriptorProto are always
|
||||
// fully qualified.
|
||||
repeated FileDescriptorProto proto_file = 15;
|
||||
|
||||
// File descriptors with all options, including source-retention options.
|
||||
// These descriptors are only provided for the files listed in
|
||||
// files_to_generate.
|
||||
repeated FileDescriptorProto source_file_descriptors = 17;
|
||||
|
||||
// The version number of protocol compiler.
|
||||
optional Version compiler_version = 3;
|
||||
}
|
||||
|
||||
// The plugin writes an encoded CodeGeneratorResponse to stdout.
|
||||
message CodeGeneratorResponse {
|
||||
// Error message. If non-empty, code generation failed. The plugin process
|
||||
// should exit with status code zero even if it reports an error in this way.
|
||||
//
|
||||
// This should be used to indicate errors in .proto files which prevent the
|
||||
// code generator from generating correct code. Errors which indicate a
|
||||
// problem in protoc itself -- such as the input CodeGeneratorRequest being
|
||||
// unparseable -- should be reported by writing a message to stderr and
|
||||
// exiting with a non-zero status code.
|
||||
optional string error = 1;
|
||||
|
||||
// A bitmask of supported features that the code generator supports.
|
||||
// This is a bitwise "or" of values from the Feature enum.
|
||||
optional uint64 supported_features = 2;
|
||||
|
||||
// Sync with code_generator.h.
|
||||
enum Feature {
|
||||
FEATURE_NONE = 0;
|
||||
FEATURE_PROTO3_OPTIONAL = 1;
|
||||
FEATURE_SUPPORTS_EDITIONS = 2;
|
||||
}
|
||||
|
||||
// Represents a single generated file.
|
||||
message File {
|
||||
// The file name, relative to the output directory. The name must not
|
||||
// contain "." or ".." components and must be relative, not be absolute (so,
|
||||
// the file cannot lie outside the output directory). "/" must be used as
|
||||
// the path separator, not "\".
|
||||
//
|
||||
// If the name is omitted, the content will be appended to the previous
|
||||
// file. This allows the generator to break large files into small chunks,
|
||||
// and allows the generated text to be streamed back to protoc so that large
|
||||
// files need not reside completely in memory at one time. Note that as of
|
||||
// this writing protoc does not optimize for this -- it will read the entire
|
||||
// CodeGeneratorResponse before writing files to disk.
|
||||
optional string name = 1;
|
||||
|
||||
// If non-empty, indicates that the named file should already exist, and the
|
||||
// content here is to be inserted into that file at a defined insertion
|
||||
// point. This feature allows a code generator to extend the output
|
||||
// produced by another code generator. The original generator may provide
|
||||
// insertion points by placing special annotations in the file that look
|
||||
// like:
|
||||
// @@protoc_insertion_point(NAME)
|
||||
// The annotation can have arbitrary text before and after it on the line,
|
||||
// which allows it to be placed in a comment. NAME should be replaced with
|
||||
// an identifier naming the point -- this is what other generators will use
|
||||
// as the insertion_point. Code inserted at this point will be placed
|
||||
// immediately above the line containing the insertion point (thus multiple
|
||||
// insertions to the same point will come out in the order they were added).
|
||||
// The double-@ is intended to make it unlikely that the generated code
|
||||
// could contain things that look like insertion points by accident.
|
||||
//
|
||||
// For example, the C++ code generator places the following line in the
|
||||
// .pb.h files that it generates:
|
||||
// // @@protoc_insertion_point(namespace_scope)
|
||||
// This line appears within the scope of the file's package namespace, but
|
||||
// outside of any particular class. Another plugin can then specify the
|
||||
// insertion_point "namespace_scope" to generate additional classes or
|
||||
// other declarations that should be placed in this scope.
|
||||
//
|
||||
// Note that if the line containing the insertion point begins with
|
||||
// whitespace, the same whitespace will be added to every line of the
|
||||
// inserted text. This is useful for languages like Python, where
|
||||
// indentation matters. In these languages, the insertion point comment
|
||||
// should be indented the same amount as any inserted code will need to be
|
||||
// in order to work correctly in that context.
|
||||
//
|
||||
// The code generator that generates the initial file and the one which
|
||||
// inserts into it must both run as part of a single invocation of protoc.
|
||||
// Code generators are executed in the order in which they appear on the
|
||||
// command line.
|
||||
//
|
||||
// If |insertion_point| is present, |name| must also be present.
|
||||
optional string insertion_point = 2;
|
||||
|
||||
// The file contents.
|
||||
optional string content = 15;
|
||||
|
||||
// Information describing the file content being inserted. If an insertion
|
||||
// point is used, this information will be appropriately offset and inserted
|
||||
// into the code generation metadata for the generated files.
|
||||
optional GeneratedCodeInfo generated_code_info = 16;
|
||||
}
|
||||
repeated File file = 15;
|
||||
}
|
||||
1218
src-tauri/protoc-vendored/include/google/protobuf/descriptor.proto
Normal file
1218
src-tauri/protoc-vendored/include/google/protobuf/descriptor.proto
Normal file
File diff suppressed because it is too large
Load Diff
115
src-tauri/protoc-vendored/include/google/protobuf/duration.proto
Normal file
115
src-tauri/protoc-vendored/include/google/protobuf/duration.proto
Normal file
@@ -0,0 +1,115 @@
|
||||
// Protocol Buffers - Google's data interchange format
|
||||
// Copyright 2008 Google Inc. All rights reserved.
|
||||
// https://developers.google.com/protocol-buffers/
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without
|
||||
// modification, are permitted provided that the following conditions are
|
||||
// met:
|
||||
//
|
||||
// * Redistributions of source code must retain the above copyright
|
||||
// notice, this list of conditions and the following disclaimer.
|
||||
// * Redistributions in binary form must reproduce the above
|
||||
// copyright notice, this list of conditions and the following disclaimer
|
||||
// in the documentation and/or other materials provided with the
|
||||
// distribution.
|
||||
// * Neither the name of Google Inc. nor the names of its
|
||||
// contributors may be used to endorse or promote products derived from
|
||||
// this software without specific prior written permission.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
syntax = "proto3";
|
||||
|
||||
package google.protobuf;
|
||||
|
||||
option cc_enable_arenas = true;
|
||||
option go_package = "google.golang.org/protobuf/types/known/durationpb";
|
||||
option java_package = "com.google.protobuf";
|
||||
option java_outer_classname = "DurationProto";
|
||||
option java_multiple_files = true;
|
||||
option objc_class_prefix = "GPB";
|
||||
option csharp_namespace = "Google.Protobuf.WellKnownTypes";
|
||||
|
||||
// A Duration represents a signed, fixed-length span of time represented
|
||||
// as a count of seconds and fractions of seconds at nanosecond
|
||||
// resolution. It is independent of any calendar and concepts like "day"
|
||||
// or "month". It is related to Timestamp in that the difference between
|
||||
// two Timestamp values is a Duration and it can be added or subtracted
|
||||
// from a Timestamp. Range is approximately +-10,000 years.
|
||||
//
|
||||
// # Examples
|
||||
//
|
||||
// Example 1: Compute Duration from two Timestamps in pseudo code.
|
||||
//
|
||||
// Timestamp start = ...;
|
||||
// Timestamp end = ...;
|
||||
// Duration duration = ...;
|
||||
//
|
||||
// duration.seconds = end.seconds - start.seconds;
|
||||
// duration.nanos = end.nanos - start.nanos;
|
||||
//
|
||||
// if (duration.seconds < 0 && duration.nanos > 0) {
|
||||
// duration.seconds += 1;
|
||||
// duration.nanos -= 1000000000;
|
||||
// } else if (duration.seconds > 0 && duration.nanos < 0) {
|
||||
// duration.seconds -= 1;
|
||||
// duration.nanos += 1000000000;
|
||||
// }
|
||||
//
|
||||
// Example 2: Compute Timestamp from Timestamp + Duration in pseudo code.
|
||||
//
|
||||
// Timestamp start = ...;
|
||||
// Duration duration = ...;
|
||||
// Timestamp end = ...;
|
||||
//
|
||||
// end.seconds = start.seconds + duration.seconds;
|
||||
// end.nanos = start.nanos + duration.nanos;
|
||||
//
|
||||
// if (end.nanos < 0) {
|
||||
// end.seconds -= 1;
|
||||
// end.nanos += 1000000000;
|
||||
// } else if (end.nanos >= 1000000000) {
|
||||
// end.seconds += 1;
|
||||
// end.nanos -= 1000000000;
|
||||
// }
|
||||
//
|
||||
// Example 3: Compute Duration from datetime.timedelta in Python.
|
||||
//
|
||||
// td = datetime.timedelta(days=3, minutes=10)
|
||||
// duration = Duration()
|
||||
// duration.FromTimedelta(td)
|
||||
//
|
||||
// # JSON Mapping
|
||||
//
|
||||
// In JSON format, the Duration type is encoded as a string rather than an
|
||||
// object, where the string ends in the suffix "s" (indicating seconds) and
|
||||
// is preceded by the number of seconds, with nanoseconds expressed as
|
||||
// fractional seconds. For example, 3 seconds with 0 nanoseconds should be
|
||||
// encoded in JSON format as "3s", while 3 seconds and 1 nanosecond should
|
||||
// be expressed in JSON format as "3.000000001s", and 3 seconds and 1
|
||||
// microsecond should be expressed in JSON format as "3.000001s".
|
||||
//
|
||||
message Duration {
|
||||
// Signed seconds of the span of time. Must be from -315,576,000,000
|
||||
// to +315,576,000,000 inclusive. Note: these bounds are computed from:
|
||||
// 60 sec/min * 60 min/hr * 24 hr/day * 365.25 days/year * 10000 years
|
||||
int64 seconds = 1;
|
||||
|
||||
// Signed fractions of a second at nanosecond resolution of the span
|
||||
// of time. Durations less than one second are represented with a 0
|
||||
// `seconds` field and a positive or negative `nanos` field. For durations
|
||||
// of one second or more, a non-zero value for the `nanos` field must be
|
||||
// of the same sign as the `seconds` field. Must be from -999,999,999
|
||||
// to +999,999,999 inclusive.
|
||||
int32 nanos = 2;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// Protocol Buffers - Google's data interchange format
|
||||
// Copyright 2008 Google Inc. All rights reserved.
|
||||
// https://developers.google.com/protocol-buffers/
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without
|
||||
// modification, are permitted provided that the following conditions are
|
||||
// met:
|
||||
//
|
||||
// * Redistributions of source code must retain the above copyright
|
||||
// notice, this list of conditions and the following disclaimer.
|
||||
// * Redistributions in binary form must reproduce the above
|
||||
// copyright notice, this list of conditions and the following disclaimer
|
||||
// in the documentation and/or other materials provided with the
|
||||
// distribution.
|
||||
// * Neither the name of Google Inc. nor the names of its
|
||||
// contributors may be used to endorse or promote products derived from
|
||||
// this software without specific prior written permission.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
syntax = "proto3";
|
||||
|
||||
package google.protobuf;
|
||||
|
||||
option go_package = "google.golang.org/protobuf/types/known/emptypb";
|
||||
option java_package = "com.google.protobuf";
|
||||
option java_outer_classname = "EmptyProto";
|
||||
option java_multiple_files = true;
|
||||
option objc_class_prefix = "GPB";
|
||||
option csharp_namespace = "Google.Protobuf.WellKnownTypes";
|
||||
option cc_enable_arenas = true;
|
||||
|
||||
// A generic empty message that you can re-use to avoid defining duplicated
|
||||
// empty messages in your APIs. A typical example is to use it as the request
|
||||
// or the response type of an API method. For instance:
|
||||
//
|
||||
// service Foo {
|
||||
// rpc Bar(google.protobuf.Empty) returns (google.protobuf.Empty);
|
||||
// }
|
||||
//
|
||||
message Empty {}
|
||||
@@ -0,0 +1,245 @@
|
||||
// Protocol Buffers - Google's data interchange format
|
||||
// Copyright 2008 Google Inc. All rights reserved.
|
||||
// https://developers.google.com/protocol-buffers/
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without
|
||||
// modification, are permitted provided that the following conditions are
|
||||
// met:
|
||||
//
|
||||
// * Redistributions of source code must retain the above copyright
|
||||
// notice, this list of conditions and the following disclaimer.
|
||||
// * Redistributions in binary form must reproduce the above
|
||||
// copyright notice, this list of conditions and the following disclaimer
|
||||
// in the documentation and/or other materials provided with the
|
||||
// distribution.
|
||||
// * Neither the name of Google Inc. nor the names of its
|
||||
// contributors may be used to endorse or promote products derived from
|
||||
// this software without specific prior written permission.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
syntax = "proto3";
|
||||
|
||||
package google.protobuf;
|
||||
|
||||
option java_package = "com.google.protobuf";
|
||||
option java_outer_classname = "FieldMaskProto";
|
||||
option java_multiple_files = true;
|
||||
option objc_class_prefix = "GPB";
|
||||
option csharp_namespace = "Google.Protobuf.WellKnownTypes";
|
||||
option go_package = "google.golang.org/protobuf/types/known/fieldmaskpb";
|
||||
option cc_enable_arenas = true;
|
||||
|
||||
// `FieldMask` represents a set of symbolic field paths, for example:
|
||||
//
|
||||
// paths: "f.a"
|
||||
// paths: "f.b.d"
|
||||
//
|
||||
// Here `f` represents a field in some root message, `a` and `b`
|
||||
// fields in the message found in `f`, and `d` a field found in the
|
||||
// message in `f.b`.
|
||||
//
|
||||
// Field masks are used to specify a subset of fields that should be
|
||||
// returned by a get operation or modified by an update operation.
|
||||
// Field masks also have a custom JSON encoding (see below).
|
||||
//
|
||||
// # Field Masks in Projections
|
||||
//
|
||||
// When used in the context of a projection, a response message or
|
||||
// sub-message is filtered by the API to only contain those fields as
|
||||
// specified in the mask. For example, if the mask in the previous
|
||||
// example is applied to a response message as follows:
|
||||
//
|
||||
// f {
|
||||
// a : 22
|
||||
// b {
|
||||
// d : 1
|
||||
// x : 2
|
||||
// }
|
||||
// y : 13
|
||||
// }
|
||||
// z: 8
|
||||
//
|
||||
// The result will not contain specific values for fields x,y and z
|
||||
// (their value will be set to the default, and omitted in proto text
|
||||
// output):
|
||||
//
|
||||
//
|
||||
// f {
|
||||
// a : 22
|
||||
// b {
|
||||
// d : 1
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// A repeated field is not allowed except at the last position of a
|
||||
// paths string.
|
||||
//
|
||||
// If a FieldMask object is not present in a get operation, the
|
||||
// operation applies to all fields (as if a FieldMask of all fields
|
||||
// had been specified).
|
||||
//
|
||||
// Note that a field mask does not necessarily apply to the
|
||||
// top-level response message. In case of a REST get operation, the
|
||||
// field mask applies directly to the response, but in case of a REST
|
||||
// list operation, the mask instead applies to each individual message
|
||||
// in the returned resource list. In case of a REST custom method,
|
||||
// other definitions may be used. Where the mask applies will be
|
||||
// clearly documented together with its declaration in the API. In
|
||||
// any case, the effect on the returned resource/resources is required
|
||||
// behavior for APIs.
|
||||
//
|
||||
// # Field Masks in Update Operations
|
||||
//
|
||||
// A field mask in update operations specifies which fields of the
|
||||
// targeted resource are going to be updated. The API is required
|
||||
// to only change the values of the fields as specified in the mask
|
||||
// and leave the others untouched. If a resource is passed in to
|
||||
// describe the updated values, the API ignores the values of all
|
||||
// fields not covered by the mask.
|
||||
//
|
||||
// If a repeated field is specified for an update operation, new values will
|
||||
// be appended to the existing repeated field in the target resource. Note that
|
||||
// a repeated field is only allowed in the last position of a `paths` string.
|
||||
//
|
||||
// If a sub-message is specified in the last position of the field mask for an
|
||||
// update operation, then new value will be merged into the existing sub-message
|
||||
// in the target resource.
|
||||
//
|
||||
// For example, given the target message:
|
||||
//
|
||||
// f {
|
||||
// b {
|
||||
// d: 1
|
||||
// x: 2
|
||||
// }
|
||||
// c: [1]
|
||||
// }
|
||||
//
|
||||
// And an update message:
|
||||
//
|
||||
// f {
|
||||
// b {
|
||||
// d: 10
|
||||
// }
|
||||
// c: [2]
|
||||
// }
|
||||
//
|
||||
// then if the field mask is:
|
||||
//
|
||||
// paths: ["f.b", "f.c"]
|
||||
//
|
||||
// then the result will be:
|
||||
//
|
||||
// f {
|
||||
// b {
|
||||
// d: 10
|
||||
// x: 2
|
||||
// }
|
||||
// c: [1, 2]
|
||||
// }
|
||||
//
|
||||
// An implementation may provide options to override this default behavior for
|
||||
// repeated and message fields.
|
||||
//
|
||||
// In order to reset a field's value to the default, the field must
|
||||
// be in the mask and set to the default value in the provided resource.
|
||||
// Hence, in order to reset all fields of a resource, provide a default
|
||||
// instance of the resource and set all fields in the mask, or do
|
||||
// not provide a mask as described below.
|
||||
//
|
||||
// If a field mask is not present on update, the operation applies to
|
||||
// all fields (as if a field mask of all fields has been specified).
|
||||
// Note that in the presence of schema evolution, this may mean that
|
||||
// fields the client does not know and has therefore not filled into
|
||||
// the request will be reset to their default. If this is unwanted
|
||||
// behavior, a specific service may require a client to always specify
|
||||
// a field mask, producing an error if not.
|
||||
//
|
||||
// As with get operations, the location of the resource which
|
||||
// describes the updated values in the request message depends on the
|
||||
// operation kind. In any case, the effect of the field mask is
|
||||
// required to be honored by the API.
|
||||
//
|
||||
// ## Considerations for HTTP REST
|
||||
//
|
||||
// The HTTP kind of an update operation which uses a field mask must
|
||||
// be set to PATCH instead of PUT in order to satisfy HTTP semantics
|
||||
// (PUT must only be used for full updates).
|
||||
//
|
||||
// # JSON Encoding of Field Masks
|
||||
//
|
||||
// In JSON, a field mask is encoded as a single string where paths are
|
||||
// separated by a comma. Fields name in each path are converted
|
||||
// to/from lower-camel naming conventions.
|
||||
//
|
||||
// As an example, consider the following message declarations:
|
||||
//
|
||||
// message Profile {
|
||||
// User user = 1;
|
||||
// Photo photo = 2;
|
||||
// }
|
||||
// message User {
|
||||
// string display_name = 1;
|
||||
// string address = 2;
|
||||
// }
|
||||
//
|
||||
// In proto a field mask for `Profile` may look as such:
|
||||
//
|
||||
// mask {
|
||||
// paths: "user.display_name"
|
||||
// paths: "photo"
|
||||
// }
|
||||
//
|
||||
// In JSON, the same mask is represented as below:
|
||||
//
|
||||
// {
|
||||
// mask: "user.displayName,photo"
|
||||
// }
|
||||
//
|
||||
// # Field Masks and Oneof Fields
|
||||
//
|
||||
// Field masks treat fields in oneofs just as regular fields. Consider the
|
||||
// following message:
|
||||
//
|
||||
// message SampleMessage {
|
||||
// oneof test_oneof {
|
||||
// string name = 4;
|
||||
// SubMessage sub_message = 9;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// The field mask can be:
|
||||
//
|
||||
// mask {
|
||||
// paths: "name"
|
||||
// }
|
||||
//
|
||||
// Or:
|
||||
//
|
||||
// mask {
|
||||
// paths: "sub_message"
|
||||
// }
|
||||
//
|
||||
// Note that oneof type names ("test_oneof" in this case) cannot be used in
|
||||
// paths.
|
||||
//
|
||||
// ## Field Mask Verification
|
||||
//
|
||||
// The implementation of any API method which has a FieldMask type field in the
|
||||
// request should verify the included field paths, and return an
|
||||
// `INVALID_ARGUMENT` error if any path is unmappable.
|
||||
message FieldMask {
|
||||
// The set of field mask paths.
|
||||
repeated string paths = 1;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
// Protocol Buffers - Google's data interchange format
|
||||
// Copyright 2008 Google Inc. All rights reserved.
|
||||
// https://developers.google.com/protocol-buffers/
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without
|
||||
// modification, are permitted provided that the following conditions are
|
||||
// met:
|
||||
//
|
||||
// * Redistributions of source code must retain the above copyright
|
||||
// notice, this list of conditions and the following disclaimer.
|
||||
// * Redistributions in binary form must reproduce the above
|
||||
// copyright notice, this list of conditions and the following disclaimer
|
||||
// in the documentation and/or other materials provided with the
|
||||
// distribution.
|
||||
// * Neither the name of Google Inc. nor the names of its
|
||||
// contributors may be used to endorse or promote products derived from
|
||||
// this software without specific prior written permission.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
syntax = "proto3";
|
||||
|
||||
package google.protobuf;
|
||||
|
||||
option java_package = "com.google.protobuf";
|
||||
option java_outer_classname = "SourceContextProto";
|
||||
option java_multiple_files = true;
|
||||
option objc_class_prefix = "GPB";
|
||||
option csharp_namespace = "Google.Protobuf.WellKnownTypes";
|
||||
option go_package = "google.golang.org/protobuf/types/known/sourcecontextpb";
|
||||
|
||||
// `SourceContext` represents information about the source of a
|
||||
// protobuf element, like the file in which it is defined.
|
||||
message SourceContext {
|
||||
// The path-qualified name of the .proto file that contained the associated
|
||||
// protobuf element. For example: `"google/protobuf/source_context.proto"`.
|
||||
string file_name = 1;
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
// Protocol Buffers - Google's data interchange format
|
||||
// Copyright 2008 Google Inc. All rights reserved.
|
||||
// https://developers.google.com/protocol-buffers/
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without
|
||||
// modification, are permitted provided that the following conditions are
|
||||
// met:
|
||||
//
|
||||
// * Redistributions of source code must retain the above copyright
|
||||
// notice, this list of conditions and the following disclaimer.
|
||||
// * Redistributions in binary form must reproduce the above
|
||||
// copyright notice, this list of conditions and the following disclaimer
|
||||
// in the documentation and/or other materials provided with the
|
||||
// distribution.
|
||||
// * Neither the name of Google Inc. nor the names of its
|
||||
// contributors may be used to endorse or promote products derived from
|
||||
// this software without specific prior written permission.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
syntax = "proto3";
|
||||
|
||||
package google.protobuf;
|
||||
|
||||
option cc_enable_arenas = true;
|
||||
option go_package = "google.golang.org/protobuf/types/known/structpb";
|
||||
option java_package = "com.google.protobuf";
|
||||
option java_outer_classname = "StructProto";
|
||||
option java_multiple_files = true;
|
||||
option objc_class_prefix = "GPB";
|
||||
option csharp_namespace = "Google.Protobuf.WellKnownTypes";
|
||||
|
||||
// `Struct` represents a structured data value, consisting of fields
|
||||
// which map to dynamically typed values. In some languages, `Struct`
|
||||
// might be supported by a native representation. For example, in
|
||||
// scripting languages like JS a struct is represented as an
|
||||
// object. The details of that representation are described together
|
||||
// with the proto support for the language.
|
||||
//
|
||||
// The JSON representation for `Struct` is JSON object.
|
||||
message Struct {
|
||||
// Unordered map of dynamically typed values.
|
||||
map<string, Value> fields = 1;
|
||||
}
|
||||
|
||||
// `Value` represents a dynamically typed value which can be either
|
||||
// null, a number, a string, a boolean, a recursive struct value, or a
|
||||
// list of values. A producer of value is expected to set one of these
|
||||
// variants. Absence of any variant indicates an error.
|
||||
//
|
||||
// The JSON representation for `Value` is JSON value.
|
||||
message Value {
|
||||
// The kind of value.
|
||||
oneof kind {
|
||||
// Represents a null value.
|
||||
NullValue null_value = 1;
|
||||
// Represents a double value.
|
||||
double number_value = 2;
|
||||
// Represents a string value.
|
||||
string string_value = 3;
|
||||
// Represents a boolean value.
|
||||
bool bool_value = 4;
|
||||
// Represents a structured value.
|
||||
Struct struct_value = 5;
|
||||
// Represents a repeated `Value`.
|
||||
ListValue list_value = 6;
|
||||
}
|
||||
}
|
||||
|
||||
// `NullValue` is a singleton enumeration to represent the null value for the
|
||||
// `Value` type union.
|
||||
//
|
||||
// The JSON representation for `NullValue` is JSON `null`.
|
||||
enum NullValue {
|
||||
// Null value.
|
||||
NULL_VALUE = 0;
|
||||
}
|
||||
|
||||
// `ListValue` is a wrapper around a repeated field of values.
|
||||
//
|
||||
// The JSON representation for `ListValue` is JSON array.
|
||||
message ListValue {
|
||||
// Repeated field of dynamically typed values.
|
||||
repeated Value values = 1;
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
// Protocol Buffers - Google's data interchange format
|
||||
// Copyright 2008 Google Inc. All rights reserved.
|
||||
// https://developers.google.com/protocol-buffers/
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without
|
||||
// modification, are permitted provided that the following conditions are
|
||||
// met:
|
||||
//
|
||||
// * Redistributions of source code must retain the above copyright
|
||||
// notice, this list of conditions and the following disclaimer.
|
||||
// * Redistributions in binary form must reproduce the above
|
||||
// copyright notice, this list of conditions and the following disclaimer
|
||||
// in the documentation and/or other materials provided with the
|
||||
// distribution.
|
||||
// * Neither the name of Google Inc. nor the names of its
|
||||
// contributors may be used to endorse or promote products derived from
|
||||
// this software without specific prior written permission.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
syntax = "proto3";
|
||||
|
||||
package google.protobuf;
|
||||
|
||||
option cc_enable_arenas = true;
|
||||
option go_package = "google.golang.org/protobuf/types/known/timestamppb";
|
||||
option java_package = "com.google.protobuf";
|
||||
option java_outer_classname = "TimestampProto";
|
||||
option java_multiple_files = true;
|
||||
option objc_class_prefix = "GPB";
|
||||
option csharp_namespace = "Google.Protobuf.WellKnownTypes";
|
||||
|
||||
// A Timestamp represents a point in time independent of any time zone or local
|
||||
// calendar, encoded as a count of seconds and fractions of seconds at
|
||||
// nanosecond resolution. The count is relative to an epoch at UTC midnight on
|
||||
// January 1, 1970, in the proleptic Gregorian calendar which extends the
|
||||
// Gregorian calendar backwards to year one.
|
||||
//
|
||||
// All minutes are 60 seconds long. Leap seconds are "smeared" so that no leap
|
||||
// second table is needed for interpretation, using a [24-hour linear
|
||||
// smear](https://developers.google.com/time/smear).
|
||||
//
|
||||
// The range is from 0001-01-01T00:00:00Z to 9999-12-31T23:59:59.999999999Z. By
|
||||
// restricting to that range, we ensure that we can convert to and from [RFC
|
||||
// 3339](https://www.ietf.org/rfc/rfc3339.txt) date strings.
|
||||
//
|
||||
// # Examples
|
||||
//
|
||||
// Example 1: Compute Timestamp from POSIX `time()`.
|
||||
//
|
||||
// Timestamp timestamp;
|
||||
// timestamp.set_seconds(time(NULL));
|
||||
// timestamp.set_nanos(0);
|
||||
//
|
||||
// Example 2: Compute Timestamp from POSIX `gettimeofday()`.
|
||||
//
|
||||
// struct timeval tv;
|
||||
// gettimeofday(&tv, NULL);
|
||||
//
|
||||
// Timestamp timestamp;
|
||||
// timestamp.set_seconds(tv.tv_sec);
|
||||
// timestamp.set_nanos(tv.tv_usec * 1000);
|
||||
//
|
||||
// Example 3: Compute Timestamp from Win32 `GetSystemTimeAsFileTime()`.
|
||||
//
|
||||
// FILETIME ft;
|
||||
// GetSystemTimeAsFileTime(&ft);
|
||||
// UINT64 ticks = (((UINT64)ft.dwHighDateTime) << 32) | ft.dwLowDateTime;
|
||||
//
|
||||
// // A Windows tick is 100 nanoseconds. Windows epoch 1601-01-01T00:00:00Z
|
||||
// // is 11644473600 seconds before Unix epoch 1970-01-01T00:00:00Z.
|
||||
// Timestamp timestamp;
|
||||
// timestamp.set_seconds((INT64) ((ticks / 10000000) - 11644473600LL));
|
||||
// timestamp.set_nanos((INT32) ((ticks % 10000000) * 100));
|
||||
//
|
||||
// Example 4: Compute Timestamp from Java `System.currentTimeMillis()`.
|
||||
//
|
||||
// long millis = System.currentTimeMillis();
|
||||
//
|
||||
// Timestamp timestamp = Timestamp.newBuilder().setSeconds(millis / 1000)
|
||||
// .setNanos((int) ((millis % 1000) * 1000000)).build();
|
||||
//
|
||||
// Example 5: Compute Timestamp from Java `Instant.now()`.
|
||||
//
|
||||
// Instant now = Instant.now();
|
||||
//
|
||||
// Timestamp timestamp =
|
||||
// Timestamp.newBuilder().setSeconds(now.getEpochSecond())
|
||||
// .setNanos(now.getNano()).build();
|
||||
//
|
||||
// Example 6: Compute Timestamp from current time in Python.
|
||||
//
|
||||
// timestamp = Timestamp()
|
||||
// timestamp.GetCurrentTime()
|
||||
//
|
||||
// # JSON Mapping
|
||||
//
|
||||
// In JSON format, the Timestamp type is encoded as a string in the
|
||||
// [RFC 3339](https://www.ietf.org/rfc/rfc3339.txt) format. That is, the
|
||||
// format is "{year}-{month}-{day}T{hour}:{min}:{sec}[.{frac_sec}]Z"
|
||||
// where {year} is always expressed using four digits while {month}, {day},
|
||||
// {hour}, {min}, and {sec} are zero-padded to two digits each. The fractional
|
||||
// seconds, which can go up to 9 digits (i.e. up to 1 nanosecond resolution),
|
||||
// are optional. The "Z" suffix indicates the timezone ("UTC"); the timezone
|
||||
// is required. A proto3 JSON serializer should always use UTC (as indicated by
|
||||
// "Z") when printing the Timestamp type and a proto3 JSON parser should be
|
||||
// able to accept both UTC and other timezones (as indicated by an offset).
|
||||
//
|
||||
// For example, "2017-01-15T01:30:15.01Z" encodes 15.01 seconds past
|
||||
// 01:30 UTC on January 15, 2017.
|
||||
//
|
||||
// In JavaScript, one can convert a Date object to this format using the
|
||||
// standard
|
||||
// [toISOString()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString)
|
||||
// method. In Python, a standard `datetime.datetime` object can be converted
|
||||
// to this format using
|
||||
// [`strftime`](https://docs.python.org/2/library/time.html#time.strftime) with
|
||||
// the time format spec '%Y-%m-%dT%H:%M:%S.%fZ'. Likewise, in Java, one can use
|
||||
// the Joda Time's [`ISODateTimeFormat.dateTime()`](
|
||||
// http://joda-time.sourceforge.net/apidocs/org/joda/time/format/ISODateTimeFormat.html#dateTime()
|
||||
// ) to obtain a formatter capable of generating timestamps in this format.
|
||||
//
|
||||
message Timestamp {
|
||||
// Represents seconds of UTC time since Unix epoch
|
||||
// 1970-01-01T00:00:00Z. Must be from 0001-01-01T00:00:00Z to
|
||||
// 9999-12-31T23:59:59Z inclusive.
|
||||
int64 seconds = 1;
|
||||
|
||||
// Non-negative fractions of a second at nanosecond resolution. Negative
|
||||
// second values with fractions must still have non-negative nanos values
|
||||
// that count forward in time. Must be from 0 to 999,999,999
|
||||
// inclusive.
|
||||
int32 nanos = 2;
|
||||
}
|
||||
193
src-tauri/protoc-vendored/include/google/protobuf/type.proto
Normal file
193
src-tauri/protoc-vendored/include/google/protobuf/type.proto
Normal file
@@ -0,0 +1,193 @@
|
||||
// Protocol Buffers - Google's data interchange format
|
||||
// Copyright 2008 Google Inc. All rights reserved.
|
||||
// https://developers.google.com/protocol-buffers/
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without
|
||||
// modification, are permitted provided that the following conditions are
|
||||
// met:
|
||||
//
|
||||
// * Redistributions of source code must retain the above copyright
|
||||
// notice, this list of conditions and the following disclaimer.
|
||||
// * Redistributions in binary form must reproduce the above
|
||||
// copyright notice, this list of conditions and the following disclaimer
|
||||
// in the documentation and/or other materials provided with the
|
||||
// distribution.
|
||||
// * Neither the name of Google Inc. nor the names of its
|
||||
// contributors may be used to endorse or promote products derived from
|
||||
// this software without specific prior written permission.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
syntax = "proto3";
|
||||
|
||||
package google.protobuf;
|
||||
|
||||
import "google/protobuf/any.proto";
|
||||
import "google/protobuf/source_context.proto";
|
||||
|
||||
option cc_enable_arenas = true;
|
||||
option java_package = "com.google.protobuf";
|
||||
option java_outer_classname = "TypeProto";
|
||||
option java_multiple_files = true;
|
||||
option objc_class_prefix = "GPB";
|
||||
option csharp_namespace = "Google.Protobuf.WellKnownTypes";
|
||||
option go_package = "google.golang.org/protobuf/types/known/typepb";
|
||||
|
||||
// A protocol buffer message type.
|
||||
message Type {
|
||||
// The fully qualified message name.
|
||||
string name = 1;
|
||||
// The list of fields.
|
||||
repeated Field fields = 2;
|
||||
// The list of types appearing in `oneof` definitions in this type.
|
||||
repeated string oneofs = 3;
|
||||
// The protocol buffer options.
|
||||
repeated Option options = 4;
|
||||
// The source context.
|
||||
SourceContext source_context = 5;
|
||||
// The source syntax.
|
||||
Syntax syntax = 6;
|
||||
// The source edition string, only valid when syntax is SYNTAX_EDITIONS.
|
||||
string edition = 7;
|
||||
}
|
||||
|
||||
// A single field of a message type.
|
||||
message Field {
|
||||
// Basic field types.
|
||||
enum Kind {
|
||||
// Field type unknown.
|
||||
TYPE_UNKNOWN = 0;
|
||||
// Field type double.
|
||||
TYPE_DOUBLE = 1;
|
||||
// Field type float.
|
||||
TYPE_FLOAT = 2;
|
||||
// Field type int64.
|
||||
TYPE_INT64 = 3;
|
||||
// Field type uint64.
|
||||
TYPE_UINT64 = 4;
|
||||
// Field type int32.
|
||||
TYPE_INT32 = 5;
|
||||
// Field type fixed64.
|
||||
TYPE_FIXED64 = 6;
|
||||
// Field type fixed32.
|
||||
TYPE_FIXED32 = 7;
|
||||
// Field type bool.
|
||||
TYPE_BOOL = 8;
|
||||
// Field type string.
|
||||
TYPE_STRING = 9;
|
||||
// Field type group. Proto2 syntax only, and deprecated.
|
||||
TYPE_GROUP = 10;
|
||||
// Field type message.
|
||||
TYPE_MESSAGE = 11;
|
||||
// Field type bytes.
|
||||
TYPE_BYTES = 12;
|
||||
// Field type uint32.
|
||||
TYPE_UINT32 = 13;
|
||||
// Field type enum.
|
||||
TYPE_ENUM = 14;
|
||||
// Field type sfixed32.
|
||||
TYPE_SFIXED32 = 15;
|
||||
// Field type sfixed64.
|
||||
TYPE_SFIXED64 = 16;
|
||||
// Field type sint32.
|
||||
TYPE_SINT32 = 17;
|
||||
// Field type sint64.
|
||||
TYPE_SINT64 = 18;
|
||||
}
|
||||
|
||||
// Whether a field is optional, required, or repeated.
|
||||
enum Cardinality {
|
||||
// For fields with unknown cardinality.
|
||||
CARDINALITY_UNKNOWN = 0;
|
||||
// For optional fields.
|
||||
CARDINALITY_OPTIONAL = 1;
|
||||
// For required fields. Proto2 syntax only.
|
||||
CARDINALITY_REQUIRED = 2;
|
||||
// For repeated fields.
|
||||
CARDINALITY_REPEATED = 3;
|
||||
}
|
||||
|
||||
// The field type.
|
||||
Kind kind = 1;
|
||||
// The field cardinality.
|
||||
Cardinality cardinality = 2;
|
||||
// The field number.
|
||||
int32 number = 3;
|
||||
// The field name.
|
||||
string name = 4;
|
||||
// The field type URL, without the scheme, for message or enumeration
|
||||
// types. Example: `"type.googleapis.com/google.protobuf.Timestamp"`.
|
||||
string type_url = 6;
|
||||
// The index of the field type in `Type.oneofs`, for message or enumeration
|
||||
// types. The first type has index 1; zero means the type is not in the list.
|
||||
int32 oneof_index = 7;
|
||||
// Whether to use alternative packed wire representation.
|
||||
bool packed = 8;
|
||||
// The protocol buffer options.
|
||||
repeated Option options = 9;
|
||||
// The field JSON name.
|
||||
string json_name = 10;
|
||||
// The string value of the default value of this field. Proto2 syntax only.
|
||||
string default_value = 11;
|
||||
}
|
||||
|
||||
// Enum type definition.
|
||||
message Enum {
|
||||
// Enum type name.
|
||||
string name = 1;
|
||||
// Enum value definitions.
|
||||
repeated EnumValue enumvalue = 2;
|
||||
// Protocol buffer options.
|
||||
repeated Option options = 3;
|
||||
// The source context.
|
||||
SourceContext source_context = 4;
|
||||
// The source syntax.
|
||||
Syntax syntax = 5;
|
||||
// The source edition string, only valid when syntax is SYNTAX_EDITIONS.
|
||||
string edition = 6;
|
||||
}
|
||||
|
||||
// Enum value definition.
|
||||
message EnumValue {
|
||||
// Enum value name.
|
||||
string name = 1;
|
||||
// Enum value number.
|
||||
int32 number = 2;
|
||||
// Protocol buffer options.
|
||||
repeated Option options = 3;
|
||||
}
|
||||
|
||||
// A protocol buffer option, which can be attached to a message, field,
|
||||
// enumeration, etc.
|
||||
message Option {
|
||||
// The option's name. For protobuf built-in options (options defined in
|
||||
// descriptor.proto), this is the short name. For example, `"map_entry"`.
|
||||
// For custom options, it should be the fully-qualified name. For example,
|
||||
// `"google.api.http"`.
|
||||
string name = 1;
|
||||
// The option's value packed in an Any message. If the value is a primitive,
|
||||
// the corresponding wrapper type defined in google/protobuf/wrappers.proto
|
||||
// should be used. If the value is an enum, it should be stored as an int32
|
||||
// value using the google.protobuf.Int32Value type.
|
||||
Any value = 2;
|
||||
}
|
||||
|
||||
// The syntax in which a protocol buffer element is defined.
|
||||
enum Syntax {
|
||||
// Syntax `proto2`.
|
||||
SYNTAX_PROTO2 = 0;
|
||||
// Syntax `proto3`.
|
||||
SYNTAX_PROTO3 = 1;
|
||||
// Syntax `editions`.
|
||||
SYNTAX_EDITIONS = 2;
|
||||
}
|
||||
123
src-tauri/protoc-vendored/include/google/protobuf/wrappers.proto
Normal file
123
src-tauri/protoc-vendored/include/google/protobuf/wrappers.proto
Normal file
@@ -0,0 +1,123 @@
|
||||
// Protocol Buffers - Google's data interchange format
|
||||
// Copyright 2008 Google Inc. All rights reserved.
|
||||
// https://developers.google.com/protocol-buffers/
|
||||
//
|
||||
// Redistribution and use in source and binary forms, with or without
|
||||
// modification, are permitted provided that the following conditions are
|
||||
// met:
|
||||
//
|
||||
// * Redistributions of source code must retain the above copyright
|
||||
// notice, this list of conditions and the following disclaimer.
|
||||
// * Redistributions in binary form must reproduce the above
|
||||
// copyright notice, this list of conditions and the following disclaimer
|
||||
// in the documentation and/or other materials provided with the
|
||||
// distribution.
|
||||
// * Neither the name of Google Inc. nor the names of its
|
||||
// contributors may be used to endorse or promote products derived from
|
||||
// this software without specific prior written permission.
|
||||
//
|
||||
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
//
|
||||
// Wrappers for primitive (non-message) types. These types are useful
|
||||
// for embedding primitives in the `google.protobuf.Any` type and for places
|
||||
// where we need to distinguish between the absence of a primitive
|
||||
// typed field and its default value.
|
||||
//
|
||||
// These wrappers have no meaningful use within repeated fields as they lack
|
||||
// the ability to detect presence on individual elements.
|
||||
// These wrappers have no meaningful use within a map or a oneof since
|
||||
// individual entries of a map or fields of a oneof can already detect presence.
|
||||
|
||||
syntax = "proto3";
|
||||
|
||||
package google.protobuf;
|
||||
|
||||
option cc_enable_arenas = true;
|
||||
option go_package = "google.golang.org/protobuf/types/known/wrapperspb";
|
||||
option java_package = "com.google.protobuf";
|
||||
option java_outer_classname = "WrappersProto";
|
||||
option java_multiple_files = true;
|
||||
option objc_class_prefix = "GPB";
|
||||
option csharp_namespace = "Google.Protobuf.WellKnownTypes";
|
||||
|
||||
// Wrapper message for `double`.
|
||||
//
|
||||
// The JSON representation for `DoubleValue` is JSON number.
|
||||
message DoubleValue {
|
||||
// The double value.
|
||||
double value = 1;
|
||||
}
|
||||
|
||||
// Wrapper message for `float`.
|
||||
//
|
||||
// The JSON representation for `FloatValue` is JSON number.
|
||||
message FloatValue {
|
||||
// The float value.
|
||||
float value = 1;
|
||||
}
|
||||
|
||||
// Wrapper message for `int64`.
|
||||
//
|
||||
// The JSON representation for `Int64Value` is JSON string.
|
||||
message Int64Value {
|
||||
// The int64 value.
|
||||
int64 value = 1;
|
||||
}
|
||||
|
||||
// Wrapper message for `uint64`.
|
||||
//
|
||||
// The JSON representation for `UInt64Value` is JSON string.
|
||||
message UInt64Value {
|
||||
// The uint64 value.
|
||||
uint64 value = 1;
|
||||
}
|
||||
|
||||
// Wrapper message for `int32`.
|
||||
//
|
||||
// The JSON representation for `Int32Value` is JSON number.
|
||||
message Int32Value {
|
||||
// The int32 value.
|
||||
int32 value = 1;
|
||||
}
|
||||
|
||||
// Wrapper message for `uint32`.
|
||||
//
|
||||
// The JSON representation for `UInt32Value` is JSON number.
|
||||
message UInt32Value {
|
||||
// The uint32 value.
|
||||
uint32 value = 1;
|
||||
}
|
||||
|
||||
// Wrapper message for `bool`.
|
||||
//
|
||||
// The JSON representation for `BoolValue` is JSON `true` and `false`.
|
||||
message BoolValue {
|
||||
// The bool value.
|
||||
bool value = 1;
|
||||
}
|
||||
|
||||
// Wrapper message for `string`.
|
||||
//
|
||||
// The JSON representation for `StringValue` is JSON string.
|
||||
message StringValue {
|
||||
// The string value.
|
||||
string value = 1;
|
||||
}
|
||||
|
||||
// Wrapper message for `bytes`.
|
||||
//
|
||||
// The JSON representation for `BytesValue` is JSON string.
|
||||
message BytesValue {
|
||||
// The bytes value.
|
||||
bytes value = 1;
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use log::{debug, warn};
|
||||
use log::{warn};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use sqlx::types::JsonValue;
|
||||
@@ -26,6 +26,7 @@ pub enum AnalyticsResource {
|
||||
KeyValue,
|
||||
Sidebar,
|
||||
Workspace,
|
||||
Setting,
|
||||
}
|
||||
|
||||
impl AnalyticsResource {
|
||||
@@ -182,7 +183,7 @@ pub async fn track_event(
|
||||
|
||||
// Disable analytics actual sending in dev
|
||||
if is_dev() {
|
||||
debug!("track: {} {} {:?}", event, attributes_json, params);
|
||||
// debug!("track: {} {} {:?}", event, attributes_json, params);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,15 +7,15 @@ use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use base64::Engine;
|
||||
use http::{HeaderMap, HeaderName, HeaderValue, Method};
|
||||
use http::header::{ACCEPT, USER_AGENT};
|
||||
use http::{HeaderMap, HeaderName, HeaderValue, Method};
|
||||
use log::{error, info, warn};
|
||||
use reqwest::{multipart, Url};
|
||||
use reqwest::redirect::Policy;
|
||||
use reqwest::{multipart, Url};
|
||||
use sqlx::types::{Json, JsonValue};
|
||||
use tauri::{Manager, Window};
|
||||
use tokio::sync::oneshot;
|
||||
use tokio::sync::watch::{Receiver};
|
||||
use tokio::sync::watch::Receiver;
|
||||
|
||||
use crate::{models, render, response_err};
|
||||
|
||||
@@ -244,6 +244,21 @@ pub async fn send_http_request(
|
||||
}
|
||||
}
|
||||
request_builder = request_builder.form(&form_params);
|
||||
} else if body_type == "binary" && request_body.contains_key("filePath") {
|
||||
let file_path = request_body
|
||||
.get("filePath")
|
||||
.ok_or("filePath not set")?
|
||||
.as_str()
|
||||
.unwrap_or_default();
|
||||
|
||||
match fs::read(file_path).map_err(|e| e.to_string()) {
|
||||
Ok(f) => {
|
||||
request_builder = request_builder.body(f);
|
||||
}
|
||||
Err(e) => {
|
||||
return response_err(response, e, window).await;
|
||||
}
|
||||
}
|
||||
} else if body_type == "multipart/form-data" && request_body.contains_key("form") {
|
||||
let mut multipart_form = multipart::Form::new();
|
||||
if let Some(form_definition) = request_body.get("form") {
|
||||
@@ -253,36 +268,64 @@ pub async fn send_http_request(
|
||||
.unwrap_or(empty_bool)
|
||||
.as_bool()
|
||||
.unwrap_or(false);
|
||||
let name = p
|
||||
let name_raw = p
|
||||
.get("name")
|
||||
.unwrap_or(empty_string)
|
||||
.as_str()
|
||||
.unwrap_or_default();
|
||||
if !enabled || name.is_empty() {
|
||||
|
||||
if !enabled || name_raw.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let file = p
|
||||
let file_path = p
|
||||
.get("file")
|
||||
.unwrap_or(empty_string)
|
||||
.as_str()
|
||||
.unwrap_or_default();
|
||||
let value = p
|
||||
let value_raw = p
|
||||
.get("value")
|
||||
.unwrap_or(empty_string)
|
||||
.as_str()
|
||||
.unwrap_or_default();
|
||||
multipart_form = multipart_form.part(
|
||||
render::render(name, &workspace, environment_ref),
|
||||
match !file.is_empty() {
|
||||
true => {
|
||||
multipart::Part::bytes(fs::read(file).map_err(|e| e.to_string())?)
|
||||
|
||||
let name = render::render(name_raw, &workspace, environment_ref);
|
||||
let part = if file_path.is_empty() {
|
||||
multipart::Part::text(render::render(
|
||||
value_raw,
|
||||
&workspace,
|
||||
environment_ref,
|
||||
))
|
||||
} else {
|
||||
match fs::read(file_path) {
|
||||
Ok(f) => multipart::Part::bytes(f),
|
||||
Err(e) => {
|
||||
return response_err(response, e.to_string(), window).await;
|
||||
}
|
||||
false => multipart::Part::text(render::render(
|
||||
value,
|
||||
&workspace,
|
||||
environment_ref,
|
||||
)),
|
||||
}
|
||||
};
|
||||
|
||||
let ct_raw = p
|
||||
.get("contentType")
|
||||
.unwrap_or(empty_string)
|
||||
.as_str()
|
||||
.unwrap_or_default();
|
||||
|
||||
multipart_form = multipart_form.part(
|
||||
name,
|
||||
if ct_raw.is_empty() {
|
||||
part
|
||||
} else {
|
||||
let content_type = render::render(ct_raw, &workspace, environment_ref);
|
||||
let filename = PathBuf::from(file_path)
|
||||
.file_name()
|
||||
.unwrap_or_default()
|
||||
.to_str()
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
part.file_name(filename)
|
||||
.mime_str(content_type.as_str())
|
||||
.map_err(|e| e.to_string())?
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -307,11 +350,11 @@ pub async fn send_http_request(
|
||||
let start = std::time::Instant::now();
|
||||
|
||||
let (resp_tx, resp_rx) = oneshot::channel();
|
||||
|
||||
|
||||
tokio::spawn(async move {
|
||||
let _ = resp_tx.send(client.execute(sendable_req).await);
|
||||
});
|
||||
|
||||
|
||||
let raw_response = tokio::select! {
|
||||
Ok(r) = resp_rx => {r}
|
||||
_ = cancel_rx.changed() => {
|
||||
|
||||
@@ -23,6 +23,7 @@ use log::{debug, error, info, warn};
|
||||
use rand::random;
|
||||
use serde_json::{json, Value};
|
||||
use sqlx::migrate::Migrator;
|
||||
use sqlx::sqlite::{SqliteConnectOptions};
|
||||
use sqlx::types::Json;
|
||||
use sqlx::{Pool, Sqlite, SqlitePool};
|
||||
#[cfg(target_os = "macos")]
|
||||
@@ -50,12 +51,13 @@ use crate::models::{
|
||||
get_cookie_jar, get_environment, get_folder, get_grpc_connection, get_grpc_request,
|
||||
get_http_request, get_http_response, get_key_value_raw, get_or_create_settings, get_workspace,
|
||||
get_workspace_export_resources, list_cookie_jars, list_environments, list_folders,
|
||||
list_grpc_connections, list_grpc_events, list_grpc_requests, list_requests, list_responses,
|
||||
list_workspaces, set_key_value_raw, update_response_if_id, update_settings, upsert_cookie_jar,
|
||||
upsert_environment, upsert_folder, upsert_grpc_connection, upsert_grpc_event,
|
||||
upsert_grpc_request, upsert_http_request, upsert_workspace, CookieJar, Environment,
|
||||
EnvironmentVariable, Folder, GrpcConnection, GrpcEvent, GrpcEventType, GrpcRequest,
|
||||
HttpRequest, HttpResponse, KeyValue, Settings, Workspace, WorkspaceExportResources,
|
||||
list_grpc_connections, list_grpc_events, list_grpc_requests, list_http_requests,
|
||||
list_responses, list_workspaces, set_key_value_raw, update_response_if_id, update_settings,
|
||||
upsert_cookie_jar, upsert_environment, upsert_folder, upsert_grpc_connection,
|
||||
upsert_grpc_event, upsert_grpc_request, upsert_http_request, upsert_workspace, CookieJar,
|
||||
Environment, EnvironmentVariable, Folder, GrpcConnection, GrpcEvent, GrpcEventType,
|
||||
GrpcRequest, HttpRequest, HttpRequestHeader, HttpResponse, KeyValue, Settings, Workspace,
|
||||
WorkspaceExportResources,
|
||||
};
|
||||
use crate::plugin::ImportResult;
|
||||
use crate::updates::{update_mode_from_str, UpdateMode, YaakUpdater};
|
||||
@@ -70,18 +72,6 @@ mod updates;
|
||||
mod window_ext;
|
||||
mod window_menu;
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
pub struct CustomResponse {
|
||||
status: u16,
|
||||
body: String,
|
||||
url: String,
|
||||
method: String,
|
||||
elapsed: u128,
|
||||
elapsed2: u128,
|
||||
headers: HashMap<String, String>,
|
||||
pub status_reason: Option<&'static str>,
|
||||
}
|
||||
|
||||
async fn migrate_db(app_handle: AppHandle, db: &Mutex<Pool<Sqlite>>) -> Result<(), String> {
|
||||
let pool = &*db.lock().await;
|
||||
let p = app_handle
|
||||
@@ -96,6 +86,26 @@ async fn migrate_db(app_handle: AppHandle, db: &Mutex<Pool<Sqlite>>) -> Result<(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
struct AppMetaData {
|
||||
is_dev: bool,
|
||||
version: String,
|
||||
name: String,
|
||||
app_data_dir: String,
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn cmd_metadata(app_handle: AppHandle) -> Result<AppMetaData, ()> {
|
||||
let p = app_handle.path_resolver();
|
||||
return Ok(AppMetaData {
|
||||
is_dev: is_dev(),
|
||||
version: app_handle.package_info().version.to_string(),
|
||||
name: app_handle.package_info().name.to_string(),
|
||||
app_data_dir: p.app_data_dir().unwrap().to_string_lossy().to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn cmd_grpc_reflect(
|
||||
request_id: &str,
|
||||
@@ -715,15 +725,13 @@ async fn cmd_filter_response(w: Window, response_id: &str, filter: &str) -> Resu
|
||||
#[tauri::command]
|
||||
async fn cmd_import_data(
|
||||
w: Window,
|
||||
file_paths: Vec<&str>,
|
||||
file_path: &str,
|
||||
_workspace_id: &str,
|
||||
) -> Result<WorkspaceExportResources, String> {
|
||||
let mut result: Option<ImportResult> = None;
|
||||
let plugins = vec!["importer-yaak", "importer-insomnia", "importer-postman"];
|
||||
for plugin_name in plugins {
|
||||
if let Some(r) =
|
||||
plugin::run_plugin_import(&w.app_handle(), plugin_name, file_paths.first().unwrap())
|
||||
.await
|
||||
{
|
||||
if let Some(r) = plugin::run_plugin_import(&w.app_handle(), plugin_name, file_path).await {
|
||||
analytics::track_event(
|
||||
&w.app_handle(),
|
||||
AnalyticsResource::App,
|
||||
@@ -737,29 +745,25 @@ async fn cmd_import_data(
|
||||
}
|
||||
|
||||
match result {
|
||||
None => Err("No importers found for the chosen file".to_string()),
|
||||
None => Err("No import handlers found".to_string()),
|
||||
Some(r) => {
|
||||
let mut imported_resources = WorkspaceExportResources::default();
|
||||
|
||||
info!("Importing resources");
|
||||
for v in r.resources.workspaces {
|
||||
let x = upsert_workspace(&w, v)
|
||||
.await
|
||||
.expect("Failed to create workspace");
|
||||
let x = upsert_workspace(&w, v).await.map_err(|e| e.to_string())?;
|
||||
imported_resources.workspaces.push(x.clone());
|
||||
info!("Imported workspace: {}", x.name);
|
||||
}
|
||||
|
||||
for v in r.resources.environments {
|
||||
let x = upsert_environment(&w, v)
|
||||
.await
|
||||
.expect("Failed to create environment");
|
||||
let x = upsert_environment(&w, v).await.map_err(|e| e.to_string())?;
|
||||
imported_resources.environments.push(x.clone());
|
||||
info!("Imported environment: {}", x.name);
|
||||
}
|
||||
|
||||
for v in r.resources.folders {
|
||||
let x = upsert_folder(&w, v).await.expect("Failed to create folder");
|
||||
let x = upsert_folder(&w, v).await.map_err(|e| e.to_string())?;
|
||||
imported_resources.folders.push(x.clone());
|
||||
info!("Imported folder: {}", x.name);
|
||||
}
|
||||
@@ -767,7 +771,7 @@ async fn cmd_import_data(
|
||||
for v in r.resources.http_requests {
|
||||
let x = upsert_http_request(&w, v)
|
||||
.await
|
||||
.expect("Failed to create HTTP request");
|
||||
.map_err(|e| e.to_string())?;
|
||||
imported_resources.http_requests.push(x.clone());
|
||||
info!("Imported request: {}", x.name);
|
||||
}
|
||||
@@ -775,7 +779,7 @@ async fn cmd_import_data(
|
||||
for v in r.resources.grpc_requests {
|
||||
let x = upsert_grpc_request(&w, &v)
|
||||
.await
|
||||
.expect("Failed to create GRPC request");
|
||||
.map_err(|e| e.to_string())?;
|
||||
imported_resources.grpc_requests.push(x.clone());
|
||||
info!("Imported request: {}", x.name);
|
||||
}
|
||||
@@ -789,9 +793,9 @@ async fn cmd_import_data(
|
||||
async fn cmd_export_data(
|
||||
app_handle: AppHandle,
|
||||
export_path: &str,
|
||||
workspace_id: &str,
|
||||
workspace_ids: Vec<&str>,
|
||||
) -> Result<(), String> {
|
||||
let export_data = get_workspace_export_resources(&app_handle, workspace_id).await;
|
||||
let export_data = get_workspace_export_resources(&app_handle, workspace_ids).await;
|
||||
let f = File::options()
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
@@ -828,14 +832,13 @@ async fn cmd_send_http_request(
|
||||
.expect("Failed to get request");
|
||||
|
||||
let environment = match environment_id {
|
||||
Some(id) =>
|
||||
match get_environment(&window, id).await {
|
||||
Ok(env) => Some(env),
|
||||
Err(e) => {
|
||||
warn!("Failed to find environment by id {id} {}", e);
|
||||
None
|
||||
}
|
||||
},
|
||||
Some(id) => match get_environment(&window, id).await {
|
||||
Ok(env) => Some(env),
|
||||
Err(e) => {
|
||||
warn!("Failed to find environment by id {id} {}", e);
|
||||
None
|
||||
}
|
||||
},
|
||||
None => None,
|
||||
};
|
||||
|
||||
@@ -920,16 +923,11 @@ async fn cmd_track_event(
|
||||
analytics::track_event(&window.app_handle(), resource, action, attributes).await;
|
||||
}
|
||||
(r, a) => {
|
||||
println!(
|
||||
"HttpRequest: {:?}",
|
||||
serde_json::to_string(&AnalyticsResource::HttpRequest)
|
||||
);
|
||||
println!("Send: {:?}", serde_json::to_string(&AnalyticsAction::Send));
|
||||
error!(
|
||||
"Invalid action/resource for track_event: {resource}.{action} = {:?}.{:?}",
|
||||
r, a
|
||||
);
|
||||
return Err("Invalid event".to_string());
|
||||
return Err("Invalid analytics event".to_string());
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
@@ -1054,6 +1052,7 @@ async fn cmd_create_http_request(
|
||||
sort_priority: f64,
|
||||
folder_id: Option<&str>,
|
||||
method: Option<&str>,
|
||||
headers: Option<Vec<HttpRequestHeader>>,
|
||||
body_type: Option<&str>,
|
||||
w: Window,
|
||||
) -> Result<HttpRequest, String> {
|
||||
@@ -1065,6 +1064,7 @@ async fn cmd_create_http_request(
|
||||
folder_id: folder_id.map(|s| s.to_string()),
|
||||
body_type: body_type.map(|s| s.to_string()),
|
||||
method: method.map(|s| s.to_string()).unwrap_or("GET".to_string()),
|
||||
headers: Json(headers.unwrap_or_default()),
|
||||
sort_priority,
|
||||
..Default::default()
|
||||
},
|
||||
@@ -1203,7 +1203,7 @@ async fn cmd_list_grpc_requests(workspace_id: &str, w: Window) -> Result<Vec<Grp
|
||||
|
||||
#[tauri::command]
|
||||
async fn cmd_list_http_requests(workspace_id: &str, w: Window) -> Result<Vec<HttpRequest>, String> {
|
||||
let requests = list_requests(&w, workspace_id)
|
||||
let requests = list_http_requests(&w, workspace_id)
|
||||
.await
|
||||
.expect("Failed to find requests");
|
||||
// .map_err(|e| e.to_string())
|
||||
@@ -1426,12 +1426,13 @@ fn main() {
|
||||
app.manage(Mutex::new(yaak_updater));
|
||||
|
||||
// Add GRPC manager
|
||||
let grpc_handle = GrpcHandle::default();
|
||||
let grpc_handle = GrpcHandle::new(&app.app_handle());
|
||||
app.manage(Mutex::new(grpc_handle));
|
||||
|
||||
// Add DB handle
|
||||
tauri::async_runtime::block_on(async move {
|
||||
let pool = SqlitePool::connect(p.to_str().unwrap())
|
||||
let opts = SqliteConnectOptions::from_str(p.to_str().unwrap()).unwrap();
|
||||
let pool = SqlitePool::connect_with(opts)
|
||||
.await
|
||||
.expect("Failed to connect to database");
|
||||
let m = Mutex::new(pool.clone());
|
||||
@@ -1454,26 +1455,26 @@ fn main() {
|
||||
cmd_create_grpc_request,
|
||||
cmd_create_http_request,
|
||||
cmd_create_workspace,
|
||||
cmd_delete_all_http_responses,
|
||||
cmd_delete_all_grpc_connections,
|
||||
cmd_delete_all_http_responses,
|
||||
cmd_delete_cookie_jar,
|
||||
cmd_delete_environment,
|
||||
cmd_delete_folder,
|
||||
cmd_delete_grpc_request,
|
||||
cmd_delete_grpc_connection,
|
||||
cmd_delete_grpc_request,
|
||||
cmd_delete_http_request,
|
||||
cmd_delete_http_response,
|
||||
cmd_delete_workspace,
|
||||
cmd_duplicate_http_request,
|
||||
cmd_duplicate_grpc_request,
|
||||
cmd_duplicate_http_request,
|
||||
cmd_export_data,
|
||||
cmd_filter_response,
|
||||
cmd_get_cookie_jar,
|
||||
cmd_get_environment,
|
||||
cmd_get_folder,
|
||||
cmd_get_key_value,
|
||||
cmd_get_http_request,
|
||||
cmd_get_grpc_request,
|
||||
cmd_get_http_request,
|
||||
cmd_get_key_value,
|
||||
cmd_get_settings,
|
||||
cmd_get_workspace,
|
||||
cmd_grpc_go,
|
||||
@@ -1482,12 +1483,13 @@ fn main() {
|
||||
cmd_list_cookie_jars,
|
||||
cmd_list_environments,
|
||||
cmd_list_folders,
|
||||
cmd_list_http_requests,
|
||||
cmd_list_grpc_requests,
|
||||
cmd_list_grpc_connections,
|
||||
cmd_list_grpc_events,
|
||||
cmd_list_grpc_requests,
|
||||
cmd_list_http_requests,
|
||||
cmd_list_http_responses,
|
||||
cmd_list_workspaces,
|
||||
cmd_metadata,
|
||||
cmd_new_window,
|
||||
cmd_send_ephemeral_request,
|
||||
cmd_send_http_request,
|
||||
|
||||
@@ -1115,7 +1115,7 @@ pub async fn upsert_http_request(
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn list_requests(
|
||||
pub async fn list_http_requests(
|
||||
mgr: &impl Manager<Wry>,
|
||||
workspace_id: &str,
|
||||
) -> Result<Vec<HttpRequest>, sqlx::Error> {
|
||||
@@ -1524,49 +1524,68 @@ pub fn generate_id(prefix: Option<&str>) -> String {
|
||||
#[derive(Default, Debug, Deserialize, Serialize)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct WorkspaceExport {
|
||||
pub yaak_version: String,
|
||||
pub yaak_schema: i64,
|
||||
pub timestamp: NaiveDateTime,
|
||||
pub resources: WorkspaceExportResources,
|
||||
pub yaak_version: String,
|
||||
pub yaak_schema: i64,
|
||||
pub timestamp: NaiveDateTime,
|
||||
pub resources: WorkspaceExportResources,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Deserialize, Serialize)]
|
||||
#[serde(default, rename_all = "camelCase")]
|
||||
pub struct WorkspaceExportResources {
|
||||
pub workspaces: Vec<Workspace>,
|
||||
pub environments: Vec<Environment>,
|
||||
pub folders: Vec<Folder>,
|
||||
pub http_requests: Vec<HttpRequest>,
|
||||
pub grpc_requests: Vec<GrpcRequest>,
|
||||
pub workspaces: Vec<Workspace>,
|
||||
pub environments: Vec<Environment>,
|
||||
pub folders: Vec<Folder>,
|
||||
pub http_requests: Vec<HttpRequest>,
|
||||
pub grpc_requests: Vec<GrpcRequest>,
|
||||
}
|
||||
|
||||
pub async fn get_workspace_export_resources(
|
||||
app_handle: &AppHandle,
|
||||
workspace_id: &str,
|
||||
workspace_ids: Vec<&str>,
|
||||
) -> WorkspaceExport {
|
||||
let workspace = get_workspace(app_handle, workspace_id)
|
||||
.await
|
||||
.expect("Failed to get workspace");
|
||||
return WorkspaceExport {
|
||||
let mut data = WorkspaceExport {
|
||||
yaak_version: app_handle.package_info().version.clone().to_string(),
|
||||
yaak_schema: 2,
|
||||
timestamp: chrono::Utc::now().naive_utc(),
|
||||
resources: WorkspaceExportResources {
|
||||
workspaces: vec![workspace],
|
||||
environments: list_environments(app_handle, workspace_id)
|
||||
.await
|
||||
.expect("Failed to get environments"),
|
||||
folders: list_folders(app_handle, workspace_id)
|
||||
.await
|
||||
.expect("Failed to get folders"),
|
||||
http_requests: list_requests(app_handle, workspace_id)
|
||||
.await
|
||||
.expect("Failed to get requests"),
|
||||
grpc_requests: list_grpc_requests(app_handle, workspace_id)
|
||||
.await
|
||||
.expect("Failed to get grpc requests"),
|
||||
workspaces: Vec::new(),
|
||||
environments: Vec::new(),
|
||||
folders: Vec::new(),
|
||||
http_requests: Vec::new(),
|
||||
grpc_requests: Vec::new(),
|
||||
},
|
||||
};
|
||||
|
||||
for workspace_id in workspace_ids {
|
||||
data.resources.workspaces.push(
|
||||
get_workspace(app_handle, workspace_id)
|
||||
.await
|
||||
.expect("Failed to get workspace"),
|
||||
);
|
||||
data.resources.environments.append(
|
||||
&mut list_environments(app_handle, workspace_id)
|
||||
.await
|
||||
.expect("Failed to get environments"),
|
||||
);
|
||||
data.resources.folders.append(
|
||||
&mut list_folders(app_handle, workspace_id)
|
||||
.await
|
||||
.expect("Failed to get folders"),
|
||||
);
|
||||
data.resources.http_requests.append(
|
||||
&mut list_http_requests(app_handle, workspace_id)
|
||||
.await
|
||||
.expect("Failed to get http requests"),
|
||||
);
|
||||
data.resources.grpc_requests.append(
|
||||
&mut list_grpc_requests(app_handle, workspace_id)
|
||||
.await
|
||||
.expect("Failed to get grpc requests"),
|
||||
);
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
fn emit_upserted_model<S: Serialize + Clone>(mgr: &impl Manager<Wry>, model: S) -> S {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
},
|
||||
"package": {
|
||||
"productName": "Yaak",
|
||||
"version": "2024.3.0"
|
||||
"version": "2024.3.9"
|
||||
},
|
||||
"tauri": {
|
||||
"windows": [],
|
||||
@@ -28,7 +28,7 @@
|
||||
"scope": [
|
||||
"$RESOURCE/*",
|
||||
"$APPDATA/responses/*"
|
||||
]
|
||||
]
|
||||
},
|
||||
"shell": {
|
||||
"all": false,
|
||||
@@ -76,7 +76,8 @@
|
||||
"longDescription": "The best cross-platform visual API client",
|
||||
"resources": [
|
||||
"migrations/*",
|
||||
"plugins/*"
|
||||
"plugins/*",
|
||||
"protoc-vendored/include/*"
|
||||
],
|
||||
"shortDescription": "The best API client",
|
||||
"targets": [
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { createBrowserRouter, Navigate, Outlet, RouterProvider, useParams } from 'react-router-dom';
|
||||
import { createBrowserRouter, Navigate, RouterProvider, useParams } from 'react-router-dom';
|
||||
import { routePaths, useAppRoutes } from '../hooks/useAppRoutes';
|
||||
import { DialogProvider } from './DialogContext';
|
||||
import { GlobalHooks } from './GlobalHooks';
|
||||
import { DefaultLayout } from './DefaultLayout';
|
||||
import { RedirectToLatestWorkspace } from './RedirectToLatestWorkspace';
|
||||
import RouteError from './RouteError';
|
||||
import Workspace from './Workspace';
|
||||
import { RedirectToLatestWorkspace } from './RedirectToLatestWorkspace';
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
@@ -58,7 +57,7 @@ function RedirectLegacyEnvironmentURLs() {
|
||||
}>();
|
||||
const environmentId = rawEnvironmentId === '__default__' ? undefined : rawEnvironmentId;
|
||||
|
||||
let to = '/';
|
||||
let to;
|
||||
if (workspaceId != null && requestId != null) {
|
||||
to = routes.paths.request({ workspaceId, environmentId, requestId });
|
||||
} else if (workspaceId != null) {
|
||||
@@ -69,12 +68,3 @@ function RedirectLegacyEnvironmentURLs() {
|
||||
|
||||
return <Navigate to={to} />;
|
||||
}
|
||||
|
||||
function DefaultLayout() {
|
||||
return (
|
||||
<DialogProvider>
|
||||
<Outlet />
|
||||
<GlobalHooks />
|
||||
</DialogProvider>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ export function BasicAuth<T extends HttpRequest | GrpcRequest>({ request }: Prop
|
||||
const updateGrpcRequest = useUpdateGrpcRequest(request.id);
|
||||
|
||||
return (
|
||||
<VStack className="my-2" space={2}>
|
||||
<VStack className="py-2 overflow-y-auto h-full" space={2}>
|
||||
<Input
|
||||
useTemplating
|
||||
autocompleteVariables
|
||||
|
||||
82
src-web/components/BinaryFileEditor.tsx
Normal file
82
src-web/components/BinaryFileEditor.tsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import { open } from '@tauri-apps/api/dialog';
|
||||
import mime from 'mime';
|
||||
import { useKeyValue } from '../hooks/useKeyValue';
|
||||
import type { HttpRequest } from '../lib/models';
|
||||
import { Banner } from './core/Banner';
|
||||
import { Button } from './core/Button';
|
||||
import { InlineCode } from './core/InlineCode';
|
||||
import { HStack, VStack } from './core/Stacks';
|
||||
|
||||
type Props = {
|
||||
requestId: string;
|
||||
contentType: string | null;
|
||||
body: HttpRequest['body'];
|
||||
onChange: (body: HttpRequest['body']) => void;
|
||||
onChangeContentType: (contentType: string | null) => void;
|
||||
};
|
||||
|
||||
export function BinaryFileEditor({
|
||||
contentType,
|
||||
body,
|
||||
onChange,
|
||||
onChangeContentType,
|
||||
requestId,
|
||||
}: Props) {
|
||||
const ignoreContentType = useKeyValue<boolean>({
|
||||
namespace: 'global',
|
||||
key: ['ignore_content_type', requestId],
|
||||
fallback: false,
|
||||
});
|
||||
|
||||
const handleClick = async () => {
|
||||
await ignoreContentType.set(false);
|
||||
const path = await open({
|
||||
title: 'Select File',
|
||||
multiple: false,
|
||||
});
|
||||
if (path) {
|
||||
onChange({ filePath: path });
|
||||
} else {
|
||||
onChange({ filePath: undefined });
|
||||
}
|
||||
};
|
||||
|
||||
const filePath = typeof body.filePath === 'string' ? body.filePath : undefined;
|
||||
const mimeType = mime.getType(filePath ?? '') ?? 'application/octet-stream';
|
||||
|
||||
return (
|
||||
<VStack space={2}>
|
||||
<HStack space={2} alignItems="center">
|
||||
<Button variant="border" color="gray" size="sm" onClick={handleClick}>
|
||||
Choose File
|
||||
</Button>
|
||||
<div className="text-xs font-mono truncate rtl pr-3 text-gray-800">
|
||||
{/* Special character to insert ltr text in rtl element without making things wonky */}
|
||||
‎
|
||||
{filePath ?? 'Select File'}
|
||||
</div>
|
||||
</HStack>
|
||||
{filePath != null && mimeType !== contentType && !ignoreContentType.value && (
|
||||
<Banner className="mt-3 !py-5">
|
||||
<div className="text-sm mb-4 text-center">
|
||||
<div>Set Content-Type header</div>
|
||||
<InlineCode>{mimeType}</InlineCode> for current request?
|
||||
</div>
|
||||
<HStack space={1.5} justifyContent="center">
|
||||
<Button
|
||||
variant="solid"
|
||||
color="gray"
|
||||
size="xs"
|
||||
onClick={() => onChangeContentType(mimeType)}
|
||||
>
|
||||
Set Header
|
||||
</Button>
|
||||
<Button size="xs" variant="border" onClick={() => ignoreContentType.set(true)}>
|
||||
Ignore
|
||||
</Button>
|
||||
</HStack>
|
||||
</Banner>
|
||||
)}
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
127
src-web/components/CommandPalette.tsx
Normal file
127
src-web/components/CommandPalette.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
import classNames from 'classnames';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useMemo, useCallback, useState } from 'react';
|
||||
import { useActiveEnvironmentId } from '../hooks/useActiveEnvironmentId';
|
||||
import { useAppRoutes } from '../hooks/useAppRoutes';
|
||||
import { getRecentEnvironments } from '../hooks/useRecentEnvironments';
|
||||
import { useRequests } from '../hooks/useRequests';
|
||||
import { useWorkspaces } from '../hooks/useWorkspaces';
|
||||
import { fallbackRequestName } from '../lib/fallbackRequestName';
|
||||
import { Input } from './core/Input';
|
||||
|
||||
export function CommandPalette({ onClose }: { onClose: () => void }) {
|
||||
const [selectedIndex, setSelectedIndex] = useState<number>(0);
|
||||
const routes = useAppRoutes();
|
||||
const activeEnvironmentId = useActiveEnvironmentId();
|
||||
const workspaces = useWorkspaces();
|
||||
const requests = useRequests();
|
||||
const [command, setCommand] = useState<string>('');
|
||||
|
||||
const items = useMemo<{ label: string; onSelect: () => void; key: string }[]>(() => {
|
||||
const items = [];
|
||||
for (const r of requests) {
|
||||
items.push({
|
||||
key: `switch-request-${r.id}`,
|
||||
label: `Switch Request → ${fallbackRequestName(r)}`,
|
||||
onSelect: () => {
|
||||
return routes.navigate('request', {
|
||||
workspaceId: r.workspaceId,
|
||||
requestId: r.id,
|
||||
environmentId: activeEnvironmentId ?? undefined,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
for (const w of workspaces) {
|
||||
items.push({
|
||||
key: `switch-workspace-${w.id}`,
|
||||
label: `Switch Workspace → ${w.name}`,
|
||||
onSelect: async () => {
|
||||
const environmentId = (await getRecentEnvironments(w.id))[0];
|
||||
return routes.navigate('workspace', {
|
||||
workspaceId: w.id,
|
||||
environmentId,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
return items;
|
||||
}, [activeEnvironmentId, requests, routes, workspaces]);
|
||||
|
||||
const filteredItems = useMemo(() => {
|
||||
return items.filter((v) => v.label.toLowerCase().includes(command.toLowerCase()));
|
||||
}, [command, items]);
|
||||
|
||||
const handleSelectAndClose = useCallback(
|
||||
(cb: () => void) => {
|
||||
onClose();
|
||||
cb();
|
||||
},
|
||||
[onClose],
|
||||
);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: KeyboardEvent) => {
|
||||
if (e.key === 'ArrowDown') {
|
||||
setSelectedIndex((prev) => prev + 1);
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
setSelectedIndex((prev) => prev - 1);
|
||||
} else if (e.key === 'Enter') {
|
||||
const item = filteredItems[selectedIndex];
|
||||
if (item) {
|
||||
handleSelectAndClose(item.onSelect);
|
||||
}
|
||||
}
|
||||
},
|
||||
[filteredItems, handleSelectAndClose, selectedIndex],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="h-full grid grid-rows-[auto_minmax(0,1fr)]">
|
||||
<div className="px-2 py-2 w-full">
|
||||
<Input
|
||||
hideLabel
|
||||
name="command"
|
||||
label="Command"
|
||||
placeholder="Type a command"
|
||||
defaultValue=""
|
||||
onChange={setCommand}
|
||||
onKeyDown={handleKeyDown}
|
||||
/>
|
||||
</div>
|
||||
<div className="h-full px-1.5 overflow-y-auto">
|
||||
{filteredItems.map((v, i) => (
|
||||
<CommandPaletteItem
|
||||
active={i === selectedIndex}
|
||||
key={v.key}
|
||||
onClick={() => handleSelectAndClose(v.onSelect)}
|
||||
>
|
||||
{v.label}
|
||||
</CommandPaletteItem>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CommandPaletteItem({
|
||||
children,
|
||||
active,
|
||||
onClick,
|
||||
}: {
|
||||
children: ReactNode;
|
||||
active: boolean;
|
||||
onClick: () => void;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className={classNames(
|
||||
'w-full h-xs flex items-center rounded px-1.5 text-gray-600',
|
||||
active && 'bg-highlightSecondary text-gray-800',
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -5,14 +5,9 @@ import { Dropdown } from './core/Dropdown';
|
||||
interface Props {
|
||||
hideFolder?: boolean;
|
||||
children: DropdownProps['children'];
|
||||
openOnHotKeyAction?: DropdownProps['openOnHotKeyAction'];
|
||||
}
|
||||
|
||||
export function CreateDropdown({ hideFolder, children, openOnHotKeyAction }: Props) {
|
||||
export function CreateDropdown({ hideFolder, children }: Props) {
|
||||
const items = useCreateDropdownItems({ hideFolder, hideIcons: true });
|
||||
return (
|
||||
<Dropdown openOnHotKeyAction={openOnHotKeyAction} items={items}>
|
||||
{children}
|
||||
</Dropdown>
|
||||
);
|
||||
return <Dropdown items={items}>{children}</Dropdown>;
|
||||
}
|
||||
|
||||
12
src-web/components/DefaultLayout.tsx
Normal file
12
src-web/components/DefaultLayout.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import { DialogProvider } from './DialogContext';
|
||||
import { GlobalHooks } from './GlobalHooks';
|
||||
|
||||
export function DefaultLayout() {
|
||||
return (
|
||||
<DialogProvider>
|
||||
<Outlet />
|
||||
<GlobalHooks />
|
||||
</DialogProvider>
|
||||
);
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import { Dialog } from './core/Dialog';
|
||||
type DialogEntry = {
|
||||
id: string;
|
||||
render: ({ hide }: { hide: () => void }) => React.ReactNode;
|
||||
} & Pick<DialogProps, 'title' | 'description' | 'hideX' | 'className' | 'size' | 'noPadding'>;
|
||||
} & Omit<DialogProps, 'onClose' | 'open' | 'children'>;
|
||||
|
||||
interface State {
|
||||
dialogs: DialogEntry[];
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
|
||||
import { useCreateEnvironment } from '../hooks/useCreateEnvironment';
|
||||
import { useDeleteEnvironment } from '../hooks/useDeleteEnvironment';
|
||||
import { useEnvironments } from '../hooks/useEnvironments';
|
||||
import { useKeyValue } from '../hooks/useKeyValue';
|
||||
import { usePrompt } from '../hooks/usePrompt';
|
||||
import { useUpdateEnvironment } from '../hooks/useUpdateEnvironment';
|
||||
import { useUpdateWorkspace } from '../hooks/useUpdateWorkspace';
|
||||
@@ -59,14 +60,16 @@ export const EnvironmentEditDialog = function ({ initialEnvironment }: Props) {
|
||||
<SidebarButton
|
||||
active={selectedEnvironment?.id == null}
|
||||
onClick={() => setSelectedEnvironmentId(null)}
|
||||
className="group"
|
||||
environment={null}
|
||||
rightSlot={
|
||||
<IconButton
|
||||
size="sm"
|
||||
iconSize="md"
|
||||
color="custom"
|
||||
title="Add sub environment"
|
||||
icon="plusCircle"
|
||||
iconClassName="text-gray-500 group-hover:text-gray-700"
|
||||
className="group"
|
||||
onClick={handleCreateEnvironment}
|
||||
/>
|
||||
}
|
||||
@@ -113,6 +116,11 @@ const EnvironmentEditor = function ({
|
||||
workspace: Workspace;
|
||||
className?: string;
|
||||
}) {
|
||||
const valueVisibility = useKeyValue<boolean>({
|
||||
namespace: 'global',
|
||||
key: 'environmentValueVisibility',
|
||||
fallback: true,
|
||||
});
|
||||
const environments = useEnvironments();
|
||||
const updateEnvironment = useUpdateEnvironment(environment?.id ?? null);
|
||||
const updateWorkspace = useUpdateWorkspace(workspace.id);
|
||||
@@ -164,15 +172,26 @@ const EnvironmentEditor = function ({
|
||||
return (
|
||||
<VStack space={4} className={classNames(className, 'pl-4')}>
|
||||
<HStack space={2} className="justify-between">
|
||||
<Heading className="w-full flex items-center">
|
||||
<Heading className="w-full flex items-center gap-1">
|
||||
<div>{environment?.name ?? 'Global Variables'}</div>
|
||||
<IconButton
|
||||
iconClassName="text-gray-600"
|
||||
size="sm"
|
||||
icon={valueVisibility.value ? 'eye' : 'eyeClosed'}
|
||||
title={valueVisibility.value ? 'Hide Values' : 'Reveal Values'}
|
||||
onClick={() => {
|
||||
return valueVisibility.set((v) => !v);
|
||||
}}
|
||||
/>
|
||||
</Heading>
|
||||
</HStack>
|
||||
<PairEditor
|
||||
className="pr-2"
|
||||
nameAutocomplete={nameAutocomplete}
|
||||
nameAutocompleteVariables={false}
|
||||
namePlaceholder="VAR_NAME"
|
||||
nameValidate={validateName}
|
||||
valueType={valueVisibility.value ? 'text' : 'password'}
|
||||
valueAutocompleteVariables={false}
|
||||
forceUpdateKey={environment?.id ?? workspace?.id ?? 'n/a'}
|
||||
pairs={variables}
|
||||
@@ -216,8 +235,8 @@ function SidebarButton({
|
||||
<div
|
||||
className={classNames(
|
||||
className,
|
||||
'w-full grid grid-cols-[minmax(0,1fr)_auto] items-center',
|
||||
'px-1', // Padding to show focus border
|
||||
'w-full grid grid-cols-[minmax(0,1fr)_auto] items-center gap-0.5',
|
||||
'px-2', // Padding to show focus border
|
||||
)}
|
||||
>
|
||||
<Button
|
||||
@@ -225,7 +244,7 @@ function SidebarButton({
|
||||
size="xs"
|
||||
className={classNames(
|
||||
'w-full',
|
||||
active ? 'text-gray-800' : 'text-gray-600 hover:text-gray-700',
|
||||
active ? 'text-gray-800 bg-highlightSecondary' : 'text-gray-600 hover:text-gray-700',
|
||||
)}
|
||||
justify="start"
|
||||
onClick={onClick}
|
||||
|
||||
107
src-web/components/ExportDataDialog.tsx
Normal file
107
src-web/components/ExportDataDialog.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { save } from '@tauri-apps/api/dialog';
|
||||
import { useState } from 'react';
|
||||
import slugify from 'slugify';
|
||||
import type { Workspace } from '../lib/models';
|
||||
import { count } from '../lib/pluralize';
|
||||
import { Button } from './core/Button';
|
||||
import { Checkbox } from './core/Checkbox';
|
||||
import { HStack, VStack } from './core/Stacks';
|
||||
|
||||
interface Props {
|
||||
onHide: () => void;
|
||||
activeWorkspace: Workspace;
|
||||
workspaces: Workspace[];
|
||||
}
|
||||
|
||||
export function ExportDataDialog({ onHide, activeWorkspace, workspaces: allWorkspaces }: Props) {
|
||||
const [selectedWorkspaces, setSelectedWorkspaces] = useState<Record<string, boolean>>({
|
||||
[activeWorkspace.id]: true,
|
||||
});
|
||||
|
||||
const workspaces = [activeWorkspace, ...allWorkspaces.filter((w) => w.id !== activeWorkspace.id)];
|
||||
|
||||
const handleToggleAll = () => {
|
||||
setSelectedWorkspaces(
|
||||
allSelected ? {} : workspaces.reduce((acc, w) => ({ ...acc, [w.id]: true }), {}),
|
||||
);
|
||||
};
|
||||
|
||||
const handleExport = async () => {
|
||||
const ids = Object.keys(selectedWorkspaces).filter((k) => selectedWorkspaces[k]);
|
||||
const workspace = ids.length === 1 ? workspaces.find((w) => w.id === ids[0]) : undefined;
|
||||
const slug = workspace ? slugify(workspace.name, { lower: true }) : 'workspaces';
|
||||
const exportPath = await save({
|
||||
title: 'Export Data',
|
||||
defaultPath: `yaak.${slug}.json`,
|
||||
});
|
||||
if (exportPath == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
await invoke('cmd_export_data', { workspaceIds: ids, exportPath });
|
||||
onHide();
|
||||
};
|
||||
|
||||
const allSelected = workspaces.every((w) => selectedWorkspaces[w.id]);
|
||||
const numSelected = Object.values(selectedWorkspaces).filter(Boolean).length;
|
||||
const noneSelected = numSelected === 0;
|
||||
return (
|
||||
<VStack space={3} className="w-full mb-3 px-4">
|
||||
<table className="w-full mb-auto min-w-full max-w-full divide-y">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="w-6 min-w-0 py-2 text-left pl-1">
|
||||
<Checkbox
|
||||
checked={allSelected}
|
||||
indeterminate={!allSelected && !noneSelected}
|
||||
hideLabel
|
||||
title="All workspaces"
|
||||
onChange={handleToggleAll}
|
||||
/>
|
||||
</th>
|
||||
<th className="py-2 text-left pl-4" onClick={handleToggleAll}>
|
||||
Workspace
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y">
|
||||
{workspaces.map((w) => (
|
||||
<tr key={w.id}>
|
||||
<td className="min-w-0 py-1 pl-1">
|
||||
<Checkbox
|
||||
checked={selectedWorkspaces[w.id] ?? false}
|
||||
title={w.name}
|
||||
hideLabel
|
||||
onChange={() =>
|
||||
setSelectedWorkspaces((prev) => ({ ...prev, [w.id]: !prev[w.id] }))
|
||||
}
|
||||
/>
|
||||
</td>
|
||||
<td
|
||||
className="py-1 pl-4 text-gray-700 whitespace-nowrap overflow-x-auto hide-scrollbars"
|
||||
onClick={() => setSelectedWorkspaces((prev) => ({ ...prev, [w.id]: !prev[w.id] }))}
|
||||
>
|
||||
{w.name} {w.id === activeWorkspace.id ? '(current workspace)' : ''}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
<HStack space={2} justifyContent="end">
|
||||
<Button className="focus" color="gray" onClick={onHide}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
className="focus"
|
||||
color="primary"
|
||||
disabled={noneSelected}
|
||||
onClick={handleExport}
|
||||
>
|
||||
Export {count('Workspace', numSelected, { omitSingle: true, noneWord: 'Nothing' })}
|
||||
</Button>
|
||||
</HStack>
|
||||
</VStack>
|
||||
);
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import { PairEditor } from './core/PairEditor';
|
||||
type Props = {
|
||||
forceUpdateKey: string;
|
||||
body: HttpRequest['body'];
|
||||
onChange: (headers: HttpRequest['body']) => void;
|
||||
onChange: (body: HttpRequest['body']) => void;
|
||||
};
|
||||
|
||||
export function FormMultipartEditor({ body, forceUpdateKey, onChange }: Props) {
|
||||
@@ -16,6 +16,7 @@ export function FormMultipartEditor({ body, forceUpdateKey, onChange }: Props) {
|
||||
enabled: p.enabled,
|
||||
name: p.name,
|
||||
value: p.file ?? p.value,
|
||||
contentType: p.contentType,
|
||||
isFile: !!p.file,
|
||||
})),
|
||||
[body.form],
|
||||
@@ -27,6 +28,7 @@ export function FormMultipartEditor({ body, forceUpdateKey, onChange }: Props) {
|
||||
form: pairs.map((p) => ({
|
||||
enabled: p.enabled,
|
||||
name: p.name,
|
||||
contentType: p.contentType,
|
||||
file: p.isFile ? p.value : undefined,
|
||||
value: p.isFile ? undefined : p.value,
|
||||
})),
|
||||
|
||||
@@ -2,7 +2,9 @@ import { useQueryClient } from '@tanstack/react-query';
|
||||
import { appWindow } from '@tauri-apps/api/window';
|
||||
import { useEffect } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { useCommandPalette } from '../hooks/useCommandPalette';
|
||||
import { cookieJarsQueryKey } from '../hooks/useCookieJars';
|
||||
import { useGlobalCommands } from '../hooks/useGlobalCommands';
|
||||
import { grpcConnectionsQueryKey } from '../hooks/useGrpcConnections';
|
||||
import { grpcEventsQueryKey } from '../hooks/useGrpcEvents';
|
||||
import { grpcRequestsQueryKey } from '../hooks/useGrpcRequests';
|
||||
@@ -18,7 +20,6 @@ import { settingsQueryKey } from '../hooks/useSettings';
|
||||
import { useSyncAppearance } from '../hooks/useSyncAppearance';
|
||||
import { useSyncWindowTitle } from '../hooks/useSyncWindowTitle';
|
||||
import { workspacesQueryKey } from '../hooks/useWorkspaces';
|
||||
import { NAMESPACE_NO_SYNC } from '../lib/keyValueStore';
|
||||
import type { Model } from '../lib/models';
|
||||
import { modelsEq } from '../lib/models';
|
||||
import { setPathname } from '../lib/persistPathname';
|
||||
@@ -33,8 +34,9 @@ export function GlobalHooks() {
|
||||
useRecentRequests();
|
||||
|
||||
useSyncAppearance();
|
||||
|
||||
useSyncWindowTitle();
|
||||
useGlobalCommands();
|
||||
useCommandPalette();
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
const { wasUpdatedExternally } = useRequestUpdateKey(null);
|
||||
@@ -142,7 +144,7 @@ function removeById<T extends { id: string }>(model: T) {
|
||||
|
||||
const shouldIgnoreModel = (payload: Model) => {
|
||||
if (payload.model === 'key_value') {
|
||||
return payload.namespace === NAMESPACE_NO_SYNC;
|
||||
return payload.namespace === 'no_sync';
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
@@ -124,7 +124,7 @@ export function GraphQLEditor({ defaultValue, onChange, baseRequest, ...extraEdi
|
||||
}
|
||||
{...extraEditorProps}
|
||||
/>
|
||||
<div className="grid min-h-[5rem]">
|
||||
<div className="grid grid-rows-[auto_minmax(0,1fr)] min-h-[5rem]">
|
||||
<Separator variant="primary" className="pb-1">
|
||||
Variables
|
||||
</Separator>
|
||||
|
||||
@@ -122,7 +122,7 @@ export function GrpcConnectionSetupPane({
|
||||
const handleSend = useCallback(async () => {
|
||||
if (activeRequest == null) return;
|
||||
onSend({ message: activeRequest.message });
|
||||
}, [activeRequest, onGo]);
|
||||
}, [activeRequest, onSend]);
|
||||
|
||||
const tabs: TabItem[] = useMemo(
|
||||
() => [
|
||||
@@ -195,7 +195,6 @@ export function GrpcConnectionSetupPane({
|
||||
shortLabel: o.label,
|
||||
}))}
|
||||
extraItems={[
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Refresh',
|
||||
type: 'default',
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import classNames from 'classnames';
|
||||
import { useMemo, useRef } from 'react';
|
||||
import { useKey, useKeyPressEvent } from 'react-use';
|
||||
import { useKeyPressEvent } from 'react-use';
|
||||
import { useActiveEnvironment } from '../hooks/useActiveEnvironment';
|
||||
import { useActiveRequest } from '../hooks/useActiveRequest';
|
||||
import { useActiveWorkspaceId } from '../hooks/useActiveWorkspaceId';
|
||||
import { useAppRoutes } from '../hooks/useAppRoutes';
|
||||
import { useGrpcRequests } from '../hooks/useGrpcRequests';
|
||||
import { useHotKey } from '../hooks/useHotKey';
|
||||
import { useHttpRequests } from '../hooks/useHttpRequests';
|
||||
import { useRecentRequests } from '../hooks/useRecentRequests';
|
||||
import { useRequests } from '../hooks/useRequests';
|
||||
import { fallbackRequestName } from '../lib/fallbackRequestName';
|
||||
import type { ButtonProps } from './core/Button';
|
||||
import { Button } from './core/Button';
|
||||
@@ -21,20 +20,10 @@ export function RecentRequestsDropdown({ className }: Pick<ButtonProps, 'classNa
|
||||
const activeRequest = useActiveRequest();
|
||||
const activeWorkspaceId = useActiveWorkspaceId();
|
||||
const activeEnvironment = useActiveEnvironment();
|
||||
const httpRequests = useHttpRequests();
|
||||
const grpcRequests = useGrpcRequests();
|
||||
const routes = useAppRoutes();
|
||||
const allRecentRequestIds = useRecentRequests();
|
||||
const recentRequestIds = useMemo(() => allRecentRequestIds.slice(1), [allRecentRequestIds]);
|
||||
const requests = useMemo(() => [...httpRequests, ...grpcRequests], [httpRequests, grpcRequests]);
|
||||
|
||||
// Toggle the menu on Cmd+k
|
||||
useKey('k', (e) => {
|
||||
if (e.metaKey) {
|
||||
e.preventDefault();
|
||||
dropdownRef.current?.toggle();
|
||||
}
|
||||
});
|
||||
const requests = useRequests();
|
||||
|
||||
// Handle key-up
|
||||
useKeyPressEvent('Control', undefined, () => {
|
||||
@@ -42,12 +31,12 @@ export function RecentRequestsDropdown({ className }: Pick<ButtonProps, 'classNa
|
||||
dropdownRef.current?.select?.();
|
||||
});
|
||||
|
||||
useHotKey('requestSwitcher.prev', () => {
|
||||
useHotKey('request_switcher.prev', () => {
|
||||
if (!dropdownRef.current?.isOpen) dropdownRef.current?.open();
|
||||
dropdownRef.current?.next?.();
|
||||
});
|
||||
|
||||
useHotKey('requestSwitcher.next', () => {
|
||||
useHotKey('request_switcher.next', () => {
|
||||
if (!dropdownRef.current?.isOpen) dropdownRef.current?.open();
|
||||
dropdownRef.current?.prev?.();
|
||||
});
|
||||
@@ -94,6 +83,7 @@ export function RecentRequestsDropdown({ className }: Pick<ButtonProps, 'classNa
|
||||
<Button
|
||||
data-tauri-drag-region
|
||||
size="sm"
|
||||
hotkeyAction="request_switcher.toggle"
|
||||
className={classNames(
|
||||
className,
|
||||
'text-gray-800 text-sm truncate pointer-events-auto',
|
||||
|
||||
@@ -3,17 +3,23 @@ import { useNavigate } from 'react-router-dom';
|
||||
import { useAppRoutes } from '../hooks/useAppRoutes';
|
||||
import { getRecentEnvironments } from '../hooks/useRecentEnvironments';
|
||||
import { getRecentRequests } from '../hooks/useRecentRequests';
|
||||
import { getRecentWorkspaces } from '../hooks/useRecentWorkspaces';
|
||||
import { useRecentWorkspaces } from '../hooks/useRecentWorkspaces';
|
||||
import { useWorkspaces } from '../hooks/useWorkspaces';
|
||||
|
||||
export function RedirectToLatestWorkspace() {
|
||||
const navigate = useNavigate();
|
||||
const routes = useAppRoutes();
|
||||
const workspaces = useWorkspaces();
|
||||
const recentWorkspaces = useRecentWorkspaces();
|
||||
|
||||
useEffect(() => {
|
||||
if (workspaces.length === 0) {
|
||||
console.log('No workspaces found to redirect to. Skipping.');
|
||||
return;
|
||||
}
|
||||
|
||||
(async function () {
|
||||
const workspaceId = (await getRecentWorkspaces())[0] ?? workspaces[0]?.id ?? 'n/a';
|
||||
const workspaceId = recentWorkspaces[0] ?? workspaces[0]?.id ?? 'n/a';
|
||||
const environmentId = (await getRecentEnvironments(workspaceId))[0];
|
||||
const requestId = (await getRecentRequests(workspaceId))[0];
|
||||
|
||||
@@ -23,7 +29,7 @@ export function RedirectToLatestWorkspace() {
|
||||
navigate(routes.paths.workspace({ workspaceId, environmentId }));
|
||||
}
|
||||
})();
|
||||
}, [navigate, routes.paths, workspaces, workspaces.length]);
|
||||
}, [navigate, recentWorkspaces, routes.paths, workspaces, workspaces.length]);
|
||||
|
||||
return <></>;
|
||||
}
|
||||
|
||||
@@ -6,24 +6,27 @@ import { useCancelHttpResponse } from '../hooks/useCancelHttpResponse';
|
||||
import { useIsResponseLoading } from '../hooks/useIsResponseLoading';
|
||||
import { usePinnedHttpResponse } from '../hooks/usePinnedHttpResponse';
|
||||
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
|
||||
import { useContentTypeFromHeaders } from '../hooks/useContentTypeFromHeaders';
|
||||
import { useSendRequest } from '../hooks/useSendRequest';
|
||||
import { useUpdateHttpRequest } from '../hooks/useUpdateHttpRequest';
|
||||
import { tryFormatJson } from '../lib/formatters';
|
||||
import type { HttpHeader, HttpRequest, HttpUrlParameter } from '../lib/models';
|
||||
import {
|
||||
BODY_TYPE_OTHER,
|
||||
AUTH_TYPE_BASIC,
|
||||
AUTH_TYPE_BEARER,
|
||||
AUTH_TYPE_NONE,
|
||||
BODY_TYPE_BINARY,
|
||||
BODY_TYPE_FORM_MULTIPART,
|
||||
BODY_TYPE_FORM_URLENCODED,
|
||||
BODY_TYPE_GRAPHQL,
|
||||
BODY_TYPE_JSON,
|
||||
BODY_TYPE_NONE,
|
||||
BODY_TYPE_OTHER,
|
||||
BODY_TYPE_XML,
|
||||
} from '../lib/models';
|
||||
import { BasicAuth } from './BasicAuth';
|
||||
import { BearerAuth } from './BearerAuth';
|
||||
import { BinaryFileEditor } from './BinaryFileEditor';
|
||||
import { CountBadge } from './core/CountBadge';
|
||||
import { Editor } from './core/Editor';
|
||||
import type { TabItem } from './core/Tabs/Tabs';
|
||||
@@ -56,6 +59,26 @@ export const RequestPane = memo(function RequestPane({
|
||||
const [activeTab, setActiveTab] = useActiveTab();
|
||||
const [forceUpdateHeaderEditorKey, setForceUpdateHeaderEditorKey] = useState<number>(0);
|
||||
const { updateKey: forceUpdateKey } = useRequestUpdateKey(activeRequest.id ?? null);
|
||||
const contentType = useContentTypeFromHeaders(activeRequest.headers);
|
||||
|
||||
const handleContentTypeChange = useCallback(
|
||||
async (contentType: string | null) => {
|
||||
const headers = activeRequest.headers.filter((h) => h.name.toLowerCase() !== 'content-type');
|
||||
|
||||
if (contentType != null) {
|
||||
headers.push({
|
||||
name: 'Content-Type',
|
||||
value: contentType,
|
||||
enabled: true,
|
||||
});
|
||||
}
|
||||
await updateRequest.mutateAsync({ headers });
|
||||
|
||||
// Force update header editor so any changed headers are reflected
|
||||
setTimeout(() => setForceUpdateHeaderEditorKey((u) => u + 1), 100);
|
||||
},
|
||||
[activeRequest.headers, updateRequest],
|
||||
);
|
||||
|
||||
const tabs: TabItem[] = useMemo(
|
||||
() => [
|
||||
@@ -68,19 +91,19 @@ export const RequestPane = memo(function RequestPane({
|
||||
{ label: 'Url Encoded', value: BODY_TYPE_FORM_URLENCODED },
|
||||
{ label: 'Multi-Part', value: BODY_TYPE_FORM_MULTIPART },
|
||||
{ type: 'separator', label: 'Text Content' },
|
||||
{ label: 'GraphQL', value: BODY_TYPE_GRAPHQL },
|
||||
{ label: 'JSON', value: BODY_TYPE_JSON },
|
||||
{ label: 'XML', value: BODY_TYPE_XML },
|
||||
{ label: 'GraphQL', value: BODY_TYPE_GRAPHQL },
|
||||
{ label: 'Other', value: BODY_TYPE_OTHER },
|
||||
{ type: 'separator', label: 'Other' },
|
||||
{ label: 'Binary File', value: BODY_TYPE_BINARY },
|
||||
{ label: 'No Body', shortLabel: 'Body', value: BODY_TYPE_NONE },
|
||||
],
|
||||
onChange: async (bodyType) => {
|
||||
const patch: Partial<HttpRequest> = { bodyType };
|
||||
let newContentType: string | null | undefined;
|
||||
if (bodyType === BODY_TYPE_NONE) {
|
||||
patch.headers = activeRequest.headers.filter(
|
||||
(h) => h.name.toLowerCase() !== 'content-type',
|
||||
);
|
||||
newContentType = null;
|
||||
} else if (
|
||||
bodyType === BODY_TYPE_FORM_URLENCODED ||
|
||||
bodyType === BODY_TYPE_FORM_MULTIPART ||
|
||||
@@ -89,32 +112,17 @@ export const RequestPane = memo(function RequestPane({
|
||||
bodyType === BODY_TYPE_XML
|
||||
) {
|
||||
patch.method = 'POST';
|
||||
patch.headers = [
|
||||
...(activeRequest.headers.filter((h) => h.name.toLowerCase() !== 'content-type') ??
|
||||
[]),
|
||||
{
|
||||
name: 'Content-Type',
|
||||
value: bodyType,
|
||||
enabled: true,
|
||||
},
|
||||
];
|
||||
newContentType = bodyType === BODY_TYPE_OTHER ? 'text/plain' : bodyType;
|
||||
} else if (bodyType == BODY_TYPE_GRAPHQL) {
|
||||
patch.method = 'POST';
|
||||
patch.headers = [
|
||||
...(activeRequest.headers.filter((h) => h.name.toLowerCase() !== 'content-type') ??
|
||||
[]),
|
||||
{
|
||||
name: 'Content-Type',
|
||||
value: 'application/json',
|
||||
enabled: true,
|
||||
},
|
||||
];
|
||||
newContentType = 'application/json';
|
||||
}
|
||||
|
||||
// Force update header editor so any changed headers are reflected
|
||||
setTimeout(() => setForceUpdateHeaderEditorKey((u) => u + 1), 100);
|
||||
await updateRequest.mutateAsync(patch);
|
||||
|
||||
updateRequest.mutate(patch);
|
||||
if (newContentType !== undefined) {
|
||||
await handleContentTypeChange(newContentType);
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -164,13 +172,28 @@ export const RequestPane = memo(function RequestPane({
|
||||
},
|
||||
},
|
||||
],
|
||||
[activeRequest, updateRequest],
|
||||
[
|
||||
activeRequest.authentication,
|
||||
activeRequest.authenticationType,
|
||||
activeRequest.bodyType,
|
||||
activeRequest.headers,
|
||||
activeRequest.urlParameters,
|
||||
handleContentTypeChange,
|
||||
updateRequest,
|
||||
],
|
||||
);
|
||||
|
||||
const handleBodyChange = useCallback(
|
||||
(body: HttpRequest['body']) => updateRequest.mutate({ body }),
|
||||
[updateRequest],
|
||||
);
|
||||
|
||||
const handleBinaryFileChange = useCallback(
|
||||
(body: HttpRequest['body']) => {
|
||||
updateRequest.mutate({ body });
|
||||
},
|
||||
[updateRequest],
|
||||
);
|
||||
const handleBodyTextChange = useCallback(
|
||||
(text: string) => updateRequest.mutate({ body: { text } }),
|
||||
[updateRequest],
|
||||
@@ -314,8 +337,16 @@ export const RequestPane = memo(function RequestPane({
|
||||
body={activeRequest.body}
|
||||
onChange={handleBodyChange}
|
||||
/>
|
||||
) : activeRequest.bodyType === BODY_TYPE_BINARY ? (
|
||||
<BinaryFileEditor
|
||||
requestId={activeRequest.id}
|
||||
contentType={contentType}
|
||||
body={activeRequest.body}
|
||||
onChange={handleBinaryFileChange}
|
||||
onChangeContentType={handleContentTypeChange}
|
||||
/>
|
||||
) : (
|
||||
<EmptyStateText>No Body</EmptyStateText>
|
||||
<EmptyStateText>Empty Body</EmptyStateText>
|
||||
)}
|
||||
</TabContent>
|
||||
</Tabs>
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { CSSProperties } from 'react';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { createGlobalState } from 'react-use';
|
||||
import { usePinnedHttpResponse } from '../hooks/usePinnedHttpResponse';
|
||||
import { useResponseContentType } from '../hooks/useResponseContentType';
|
||||
import { useContentTypeFromHeaders } from '../hooks/useContentTypeFromHeaders';
|
||||
import { useResponseViewMode } from '../hooks/useResponseViewMode';
|
||||
import type { HttpRequest } from '../lib/models';
|
||||
import { isResponseLoading } from '../lib/models';
|
||||
@@ -37,7 +37,7 @@ export const ResponsePane = memo(function ResponsePane({ style, className, activ
|
||||
const { activeResponse, setPinnedResponse, responses } = usePinnedHttpResponse(activeRequest);
|
||||
const [viewMode, setViewMode] = useResponseViewMode(activeResponse?.requestId);
|
||||
const [activeTab, setActiveTab] = useActiveTab();
|
||||
const contentType = useResponseContentType(activeResponse);
|
||||
const contentType = useContentTypeFromHeaders(activeResponse?.headers ?? null);
|
||||
|
||||
const tabs = useMemo<TabItem[]>(
|
||||
() => [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import classNames from 'classnames';
|
||||
import type { ForwardedRef, ReactNode } from 'react';
|
||||
import React, { Fragment, forwardRef, useCallback, useMemo, useRef, useState } 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';
|
||||
@@ -15,13 +15,12 @@ import { useDeleteRequest } from '../hooks/useDeleteRequest';
|
||||
import { useDuplicateGrpcRequest } from '../hooks/useDuplicateGrpcRequest';
|
||||
import { useDuplicateHttpRequest } from '../hooks/useDuplicateHttpRequest';
|
||||
import { useFolders } from '../hooks/useFolders';
|
||||
import { useGrpcRequests } from '../hooks/useGrpcRequests';
|
||||
import { useHotKey } from '../hooks/useHotKey';
|
||||
import { useHttpRequests } from '../hooks/useHttpRequests';
|
||||
import { useKeyValue } from '../hooks/useKeyValue';
|
||||
import { useLatestGrpcConnection } from '../hooks/useLatestGrpcConnection';
|
||||
import { useLatestHttpResponse } from '../hooks/useLatestHttpResponse';
|
||||
import { usePrompt } from '../hooks/usePrompt';
|
||||
import { useRequests } from '../hooks/useRequests';
|
||||
import { useSendManyRequests } from '../hooks/useSendFolder';
|
||||
import { useSendRequest } from '../hooks/useSendRequest';
|
||||
import { useSidebarHidden } from '../hooks/useSidebarHidden';
|
||||
@@ -31,7 +30,6 @@ import { useUpdateAnyHttpRequest } from '../hooks/useUpdateAnyHttpRequest';
|
||||
import { useUpdateGrpcRequest } from '../hooks/useUpdateGrpcRequest';
|
||||
import { useUpdateHttpRequest } from '../hooks/useUpdateHttpRequest';
|
||||
import { fallbackRequestName } from '../lib/fallbackRequestName';
|
||||
import { NAMESPACE_NO_SYNC } from '../lib/keyValueStore';
|
||||
import type { Folder, GrpcRequest, HttpRequest, Workspace } from '../lib/models';
|
||||
import { isResponseLoading } from '../lib/models';
|
||||
import type { DropdownItem } from './core/Dropdown';
|
||||
@@ -58,13 +56,12 @@ interface TreeNode {
|
||||
}
|
||||
|
||||
export function Sidebar({ className }: Props) {
|
||||
const { hidden, show, hide } = useSidebarHidden();
|
||||
const [hidden, setHidden] = useSidebarHidden();
|
||||
const sidebarRef = useRef<HTMLLIElement>(null);
|
||||
const activeRequest = useActiveRequest();
|
||||
const activeEnvironmentId = useActiveEnvironmentId();
|
||||
const httpRequests = useHttpRequests();
|
||||
const grpcRequests = useGrpcRequests();
|
||||
const folders = useFolders();
|
||||
const requests = useRequests();
|
||||
const activeWorkspace = useActiveWorkspace();
|
||||
const duplicateHttpRequest = useDuplicateHttpRequest({
|
||||
id: activeRequest?.id ?? null,
|
||||
@@ -87,7 +84,7 @@ export function Sidebar({ className }: Props) {
|
||||
const collapsed = useKeyValue<Record<string, boolean>>({
|
||||
key: ['sidebar_collapsed', activeWorkspace?.id ?? 'n/a'],
|
||||
fallback: {},
|
||||
namespace: NAMESPACE_NO_SYNC,
|
||||
namespace: 'no_sync',
|
||||
});
|
||||
|
||||
useHotKey('http_request.duplicate', async () => {
|
||||
@@ -136,7 +133,7 @@ export function Sidebar({ className }: Props) {
|
||||
selectedRequest = node.item;
|
||||
}
|
||||
|
||||
const childItems = [...httpRequests, ...grpcRequests, ...folders].filter((f) =>
|
||||
const childItems = [...requests, ...folders].filter((f) =>
|
||||
node.item.model === 'workspace' ? f.folderId == null : f.folderId === node.item.id,
|
||||
);
|
||||
|
||||
@@ -155,7 +152,7 @@ export function Sidebar({ className }: Props) {
|
||||
const tree = next({ item: activeWorkspace, children: [], depth: 0 });
|
||||
|
||||
return { tree, treeParentMap, selectableRequests, selectedRequest };
|
||||
}, [activeWorkspace, selectedId, httpRequests, grpcRequests, folders]);
|
||||
}, [activeWorkspace, selectedId, requests, folders]);
|
||||
|
||||
const deleteSelectedRequest = useDeleteRequest(selectedRequest);
|
||||
|
||||
@@ -244,16 +241,15 @@ export function Sidebar({ className }: Props) {
|
||||
useKeyPressEvent('Delete', handleDeleteKey);
|
||||
|
||||
useHotKey('sidebar.focus', async () => {
|
||||
console.log('sidebar.focus', { hidden, hasFocus });
|
||||
// Hide the sidebar if it's already focused
|
||||
if (!hidden && hasFocus) {
|
||||
await hide();
|
||||
await setHidden(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Show the sidebar if it's hidden
|
||||
if (hidden) {
|
||||
await show();
|
||||
await setHidden(false);
|
||||
}
|
||||
|
||||
// Select 0 index on focus if none selected
|
||||
@@ -420,6 +416,19 @@ export function Sidebar({ className }: Props) {
|
||||
],
|
||||
);
|
||||
|
||||
const [showMainContextMenu, setShowMainContextMenu] = useState<{
|
||||
x: number;
|
||||
y: number;
|
||||
} | null>(null);
|
||||
|
||||
const handleMainContextMenu = useCallback((e: React.MouseEvent<HTMLElement>) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setShowMainContextMenu({ x: e.clientX, y: e.clientY });
|
||||
}, []);
|
||||
|
||||
const mainContextMenuItems = useCreateDropdownItems();
|
||||
|
||||
// Not ready to render yet
|
||||
if (tree == null || collapsed.value == null) {
|
||||
return null;
|
||||
@@ -432,11 +441,17 @@ export function Sidebar({ className }: Props) {
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
tabIndex={hidden ? -1 : 0}
|
||||
onContextMenu={handleMainContextMenu}
|
||||
className={classNames(
|
||||
className,
|
||||
'h-full pb-3 overflow-y-scroll overflow-x-visible hide-scrollbars pt-2',
|
||||
)}
|
||||
>
|
||||
<ContextMenu
|
||||
show={showMainContextMenu}
|
||||
items={mainContextMenuItems}
|
||||
onClose={() => setShowMainContextMenu(null)}
|
||||
/>
|
||||
<SidebarItems
|
||||
treeParentMap={treeParentMap}
|
||||
selectedId={selectedId}
|
||||
@@ -496,7 +511,7 @@ function SidebarItems({
|
||||
className={classNames(
|
||||
tree.depth > 0 && 'border-l border-highlight',
|
||||
tree.depth === 0 && 'ml-0',
|
||||
tree.depth >= 1 && 'ml-[1.3em]',
|
||||
tree.depth >= 1 && 'ml-[1.2em]',
|
||||
)}
|
||||
>
|
||||
{tree.children.map((child, i) => (
|
||||
@@ -754,7 +769,7 @@ const SidebarItem = forwardRef(function SidebarItem(
|
||||
data-active={isActive}
|
||||
data-selected={selected}
|
||||
className={classNames(
|
||||
'w-full flex gap-2 items-center text-sm h-xs px-1.5 rounded-md transition-colors',
|
||||
'w-full flex gap-1.5 items-center text-sm h-xs px-1.5 rounded-md transition-colors',
|
||||
editing && 'ring-1 focus-within:ring-focus',
|
||||
isActive && 'bg-highlightSecondary text-gray-800',
|
||||
!isActive &&
|
||||
|
||||
@@ -1,12 +1,22 @@
|
||||
import { memo } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { useFloatingSidebarHidden } from '../hooks/useFloatingSidebarHidden';
|
||||
import { useShouldFloatSidebar } from '../hooks/useShouldFloatSidebar';
|
||||
import { useSidebarHidden } from '../hooks/useSidebarHidden';
|
||||
import { trackEvent } from '../lib/analytics';
|
||||
import { IconButton } from './core/IconButton';
|
||||
import { HStack } from './core/Stacks';
|
||||
import { CreateDropdown } from './CreateDropdown';
|
||||
|
||||
export const SidebarActions = memo(function SidebarActions() {
|
||||
const { hidden, show, hide } = useSidebarHidden();
|
||||
export function SidebarActions() {
|
||||
const floating = useShouldFloatSidebar();
|
||||
const [normalHidden, setNormalHidden] = useSidebarHidden();
|
||||
const [floatingHidden, setFloatingHidden] = useFloatingSidebarHidden();
|
||||
|
||||
const hidden = floating ? floatingHidden : normalHidden;
|
||||
const setHidden = useMemo(
|
||||
() => (floating ? setFloatingHidden : setNormalHidden),
|
||||
[floating, setFloatingHidden, setNormalHidden],
|
||||
);
|
||||
|
||||
return (
|
||||
<HStack className="h-full" alignItems="center">
|
||||
@@ -14,10 +24,9 @@ export const SidebarActions = memo(function SidebarActions() {
|
||||
onClick={async () => {
|
||||
trackEvent('sidebar', 'toggle');
|
||||
|
||||
// NOTE: We're not using `toggle` because it may be out of sync
|
||||
// from changes in other windows
|
||||
if (hidden) await show();
|
||||
else await hide();
|
||||
// NOTE: We're not using the (h) => !h pattern here because the data
|
||||
// might be different if another window changed it (out of sync)
|
||||
await setHidden(!hidden);
|
||||
}}
|
||||
className="pointer-events-auto"
|
||||
size="sm"
|
||||
@@ -25,9 +34,14 @@ export const SidebarActions = memo(function SidebarActions() {
|
||||
hotkeyAction="sidebar.toggle"
|
||||
icon={hidden ? 'leftPanelHidden' : 'leftPanelVisible'}
|
||||
/>
|
||||
<CreateDropdown openOnHotKeyAction="http_request.create">
|
||||
<IconButton size="sm" icon="plusCircle" title="Add Resource" />
|
||||
<CreateDropdown>
|
||||
<IconButton
|
||||
size="sm"
|
||||
icon="plusCircle"
|
||||
title="Add Resource"
|
||||
hotkeyAction="http_request.create"
|
||||
/>
|
||||
</CreateDropdown>
|
||||
</HStack>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ export const UrlBar = memo(function UrlBar({
|
||||
hideLabel
|
||||
useTemplating
|
||||
contentType="url"
|
||||
className="px-0 py-0.5"
|
||||
className="pl-0 pr-1.5 py-0.5"
|
||||
name="url"
|
||||
label="Enter URL"
|
||||
forceUpdateKey={forceUpdateKey}
|
||||
@@ -76,7 +76,7 @@ export const UrlBar = memo(function UrlBar({
|
||||
<RequestMethodDropdown
|
||||
method={method}
|
||||
onChange={onMethodChange}
|
||||
className="!h-auto my-0.5 ml-0.5"
|
||||
className="my-0.5 ml-0.5"
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -87,7 +87,7 @@ export const UrlBar = memo(function UrlBar({
|
||||
iconSize="md"
|
||||
title="Send Request"
|
||||
type="submit"
|
||||
className="w-8 !h-auto my-0.5 mr-0.5"
|
||||
className="w-8 my-0.5 mr-0.5"
|
||||
icon={isLoading ? 'x' : submitIcon}
|
||||
hotkeyAction="http_request.send"
|
||||
/>
|
||||
|
||||
@@ -6,16 +6,24 @@ import type {
|
||||
MouseEvent as ReactMouseEvent,
|
||||
ReactNode,
|
||||
} from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { useWindowSize } from 'react-use';
|
||||
import { useActiveRequest } from '../hooks/useActiveRequest';
|
||||
import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
|
||||
import { useActiveWorkspaceId } from '../hooks/useActiveWorkspaceId';
|
||||
import { useFloatingSidebarHidden } from '../hooks/useFloatingSidebarHidden';
|
||||
import { useImportData } from '../hooks/useImportData';
|
||||
import { useIsFullscreen } from '../hooks/useIsFullscreen';
|
||||
import { useOsInfo } from '../hooks/useOsInfo';
|
||||
import { useShouldFloatSidebar } from '../hooks/useShouldFloatSidebar';
|
||||
import { useSidebarHidden } from '../hooks/useSidebarHidden';
|
||||
import { useSidebarWidth } from '../hooks/useSidebarWidth';
|
||||
import { useWorkspaces } from '../hooks/useWorkspaces';
|
||||
import { Banner } from './core/Banner';
|
||||
import { Button } from './core/Button';
|
||||
import { HotKeyList } from './core/HotKeyList';
|
||||
import { InlineCode } from './core/InlineCode';
|
||||
import { FeedbackLink } from './core/Link';
|
||||
import { HStack } from './core/Stacks';
|
||||
import { CreateDropdown } from './CreateDropdown';
|
||||
import { GrpcConnectionLayout } from './GrpcConnectionLayout';
|
||||
@@ -31,31 +39,22 @@ const head = { gridArea: 'head' };
|
||||
const body = { gridArea: 'body' };
|
||||
const drag = { gridArea: 'drag' };
|
||||
|
||||
const WINDOW_FLOATING_SIDEBAR_WIDTH = 600;
|
||||
|
||||
export default function Workspace() {
|
||||
const workspaces = useWorkspaces();
|
||||
const activeWorkspace = useActiveWorkspace();
|
||||
const activeWorkspaceId = useActiveWorkspaceId();
|
||||
const { setWidth, width, resetWidth } = useSidebarWidth();
|
||||
const { hide, show, hidden } = useSidebarHidden();
|
||||
const [sidebarHidden, setSidebarHidden] = useSidebarHidden();
|
||||
const [floatingSidebarHidden, setFloatingSidebarHidden] = useFloatingSidebarHidden();
|
||||
const activeRequest = useActiveRequest();
|
||||
const windowSize = useWindowSize();
|
||||
const importData = useImportData();
|
||||
const [floating, setFloating] = useState<boolean>(false);
|
||||
const floating = useShouldFloatSidebar();
|
||||
const [isResizing, setIsResizing] = useState<boolean>(false);
|
||||
const moveState = useRef<{ move: (e: MouseEvent) => void; up: (e: MouseEvent) => void } | null>(
|
||||
null,
|
||||
);
|
||||
|
||||
// float/un-float sidebar on window resize
|
||||
useEffect(() => {
|
||||
const shouldHide = windowSize.width <= WINDOW_FLOATING_SIDEBAR_WIDTH;
|
||||
if (shouldHide && !floating) {
|
||||
setFloating(true);
|
||||
hide().catch(console.error);
|
||||
} else if (!shouldHide && floating) {
|
||||
setFloating(false);
|
||||
}
|
||||
}, [floating, hide, windowSize.width]);
|
||||
|
||||
const unsub = () => {
|
||||
if (moveState.current !== null) {
|
||||
document.documentElement.removeEventListener('mousemove', moveState.current.move);
|
||||
@@ -75,10 +74,10 @@ export default function Workspace() {
|
||||
e.preventDefault(); // Prevent text selection and things
|
||||
const newWidth = startWidth + (e.clientX - mouseStartX);
|
||||
if (newWidth < 50) {
|
||||
await hide();
|
||||
await setSidebarHidden(true);
|
||||
resetWidth();
|
||||
} else {
|
||||
await show();
|
||||
await setSidebarHidden(false);
|
||||
setWidth(newWidth);
|
||||
}
|
||||
},
|
||||
@@ -92,10 +91,10 @@ export default function Workspace() {
|
||||
document.documentElement.addEventListener('mouseup', moveState.current.up);
|
||||
setIsResizing(true);
|
||||
},
|
||||
[setWidth, resetWidth, width, hide, show],
|
||||
[width, setSidebarHidden, resetWidth, setWidth],
|
||||
);
|
||||
|
||||
const sideWidth = hidden ? 0 : width;
|
||||
const sideWidth = sidebarHidden ? 0 : width;
|
||||
const styles = useMemo<CSSProperties>(
|
||||
() => ({
|
||||
gridTemplate: floating
|
||||
@@ -119,6 +118,11 @@ export default function Workspace() {
|
||||
);
|
||||
}
|
||||
|
||||
// We're loading still
|
||||
if (workspaces.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={styles}
|
||||
@@ -130,7 +134,11 @@ export default function Workspace() {
|
||||
)}
|
||||
>
|
||||
{floating ? (
|
||||
<Overlay open={!hidden} portalName="sidebar" onClose={hide}>
|
||||
<Overlay
|
||||
open={!floatingSidebarHidden}
|
||||
portalName="sidebar"
|
||||
onClose={() => setFloatingSidebarHidden(true)}
|
||||
>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, x: -20 }}
|
||||
animate={{ opacity: 1, x: 0 }}
|
||||
@@ -163,7 +171,15 @@ export default function Workspace() {
|
||||
<HeaderSize data-tauri-drag-region style={head}>
|
||||
<WorkspaceHeader className="pointer-events-none" />
|
||||
</HeaderSize>
|
||||
{activeRequest == null ? (
|
||||
{activeWorkspace == null ? (
|
||||
<div className="m-auto">
|
||||
<Banner color="warning" className="max-w-[30rem]">
|
||||
The active workspace{' '}
|
||||
<InlineCode className="text-orange-800">{activeWorkspaceId}</InlineCode> was not found.
|
||||
Select a workspace from the header menu or report this bug to <FeedbackLink />
|
||||
</Banner>
|
||||
</div>
|
||||
) : activeRequest == null ? (
|
||||
<HotKeyList
|
||||
hotkeys={['http_request.create', 'sidebar.toggle', 'settings.show']}
|
||||
bottomSlot={
|
||||
|
||||
@@ -3,7 +3,7 @@ import classNames from 'classnames';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { useActiveWorkspace } from '../hooks/useActiveWorkspace';
|
||||
import { useAppRoutes } from '../hooks/useAppRoutes';
|
||||
import { useCreateWorkspace } from '../hooks/useCreateWorkspace';
|
||||
import { useCommand } from '../hooks/useCommands';
|
||||
import { useDeleteWorkspace } from '../hooks/useDeleteWorkspace';
|
||||
import { usePrompt } from '../hooks/usePrompt';
|
||||
import { getRecentEnvironments } from '../hooks/useRecentEnvironments';
|
||||
@@ -30,7 +30,7 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
|
||||
const activeWorkspaceId = activeWorkspace?.id ?? null;
|
||||
const updateWorkspace = useUpdateWorkspace(activeWorkspaceId);
|
||||
const deleteWorkspace = useDeleteWorkspace(activeWorkspace);
|
||||
const createWorkspace = useCreateWorkspace({ navigateAfter: true });
|
||||
const createWorkspace = useCommand('workspace.create');
|
||||
const dialog = useDialog();
|
||||
const prompt = usePrompt();
|
||||
const routes = useAppRoutes();
|
||||
@@ -167,10 +167,14 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
|
||||
<Dropdown items={items}>
|
||||
<Button
|
||||
size="sm"
|
||||
className={classNames(className, 'text-gray-800 !px-2 truncate')}
|
||||
className={classNames(
|
||||
className,
|
||||
'text-gray-800 !px-2 truncate',
|
||||
activeWorkspace === null && 'italic opacity-disabled',
|
||||
)}
|
||||
{...buttonProps}
|
||||
>
|
||||
{activeWorkspace?.name}
|
||||
{activeWorkspace?.name ?? 'Workspace'}
|
||||
</Button>
|
||||
</Dropdown>
|
||||
);
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { ReactNode } from 'react';
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
className?: string;
|
||||
color?: 'danger' | 'success' | 'gray';
|
||||
color?: 'danger' | 'warning' | 'success' | 'gray';
|
||||
}
|
||||
export function Banner({ children, className, color = 'gray' }: Props) {
|
||||
return (
|
||||
@@ -14,8 +14,9 @@ export function Banner({ children, className, color = 'gray' }: Props) {
|
||||
className,
|
||||
'border border-dashed italic px-3 py-2 rounded select-auto cursor-text',
|
||||
color === 'gray' && 'border-gray-500/60 bg-gray-300/10 text-gray-800',
|
||||
color === 'warning' && 'border-orange-500/60 bg-orange-300/10 text-orange-800',
|
||||
color === 'danger' && 'border-red-500/60 bg-red-300/10 text-red-800',
|
||||
color === 'success' && 'border-green-500/60 bg-green-300/10 text-green-800',
|
||||
color === 'success' && 'border-violet-500/60 bg-violet-300/10 text-violet-800',
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -60,17 +60,27 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(function Button
|
||||
size === 'sm' && 'h-sm px-2.5 text-sm',
|
||||
size === 'xs' && 'h-xs px-2 text-sm',
|
||||
// Solids
|
||||
variant === 'solid' && color === 'custom' && 'ring-blue-400',
|
||||
variant === 'solid' &&
|
||||
color === 'custom' &&
|
||||
'ring-blue-400 enabled:hocus:bg-highlightSecondary',
|
||||
variant === 'solid' &&
|
||||
color === 'default' &&
|
||||
'text-gray-700 enabled:hocus:bg-gray-700/10 enabled:hocus:text-gray-800 ring-blue-400',
|
||||
variant === 'solid' &&
|
||||
color === 'gray' &&
|
||||
'text-gray-800 bg-highlight enabled:hocus:text-gray-1000 ring-blue-400',
|
||||
variant === 'solid' && color === 'primary' && 'bg-blue-400 text-white ring-blue-700',
|
||||
variant === 'solid' && color === 'secondary' && 'bg-violet-400 text-white ring-violet-700',
|
||||
variant === 'solid' && color === 'warning' && 'bg-orange-400 text-white ring-orange-700',
|
||||
variant === 'solid' && color === 'danger' && 'bg-red-400 text-white ring-red-700',
|
||||
'text-gray-800 bg-gray-200/70 enabled:hocus:bg-gray-200 ring-blue-400',
|
||||
variant === 'solid' &&
|
||||
color === 'primary' &&
|
||||
'bg-blue-400 text-white ring-blue-700 enabled:hocus:bg-blue-500',
|
||||
variant === 'solid' &&
|
||||
color === 'secondary' &&
|
||||
'bg-violet-400 text-white ring-violet-700 enabled:hocus:bg-violet-500',
|
||||
variant === 'solid' &&
|
||||
color === 'warning' &&
|
||||
'bg-orange-400 text-white ring-orange-700 enabled:hocus:bg-orange-500',
|
||||
variant === 'solid' &&
|
||||
color === 'danger' &&
|
||||
'bg-red-400 text-white ring-red-700 enabled:hocus:bg-red-500',
|
||||
// Borders
|
||||
variant === 'border' && 'border',
|
||||
variant === 'border' &&
|
||||
|
||||
@@ -8,10 +8,21 @@ interface Props {
|
||||
onChange: (checked: boolean) => void;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
inputWrapperClassName?: string;
|
||||
indeterminate?: boolean;
|
||||
hideLabel?: boolean;
|
||||
}
|
||||
|
||||
export function Checkbox({ checked, onChange, className, disabled, title, hideLabel }: Props) {
|
||||
export function Checkbox({
|
||||
checked,
|
||||
indeterminate,
|
||||
onChange,
|
||||
className,
|
||||
inputWrapperClassName,
|
||||
disabled,
|
||||
title,
|
||||
hideLabel,
|
||||
}: Props) {
|
||||
return (
|
||||
<HStack
|
||||
as="label"
|
||||
@@ -19,35 +30,21 @@ export function Checkbox({ checked, onChange, className, disabled, title, hideLa
|
||||
alignItems="center"
|
||||
className={classNames(className, 'text-gray-900 text-sm', disabled && 'opacity-disabled')}
|
||||
>
|
||||
<div className="relative flex">
|
||||
<div className={classNames(inputWrapperClassName, 'relative flex')}>
|
||||
<input
|
||||
aria-hidden
|
||||
className="appearance-none w-4 h-4 flex-shrink-0 border border-gray-200 rounded focus:border-focus outline-none ring-0"
|
||||
className={classNames(
|
||||
'appearance-none w-4 h-4 flex-shrink-0 border border-highlight',
|
||||
'rounded hocus:border-focus hocus:bg-focus/[5%] outline-none ring-0',
|
||||
)}
|
||||
type="checkbox"
|
||||
disabled={disabled}
|
||||
onChange={() => onChange(!checked)}
|
||||
/>
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<Icon size="sm" icon={checked ? 'check' : 'empty'} />
|
||||
<Icon size="sm" icon={indeterminate ? 'minus' : checked ? 'check' : 'empty'} />
|
||||
</div>
|
||||
</div>
|
||||
{/*<button*/}
|
||||
{/* role="checkbox"*/}
|
||||
{/* aria-checked={checked ? 'true' : 'false'}*/}
|
||||
{/* disabled={disabled}*/}
|
||||
{/* onClick={handleClick}*/}
|
||||
{/* title={title}*/}
|
||||
{/* className={classNames(*/}
|
||||
{/* className,*/}
|
||||
{/* 'flex-shrink-0 w-4 h-4 border border-gray-200 rounded',*/}
|
||||
{/* 'focus:border-focus',*/}
|
||||
{/* 'disabled:opacity-disabled',*/}
|
||||
{/* checked && 'bg-gray-200/10',*/}
|
||||
{/* // Remove focus style*/}
|
||||
{/* 'outline-none',*/}
|
||||
{/* )}*/}
|
||||
{/*>*/}
|
||||
{/*</button>*/}
|
||||
{!hideLabel && title}
|
||||
</HStack>
|
||||
);
|
||||
|
||||
@@ -17,6 +17,7 @@ export interface DialogProps {
|
||||
size?: 'sm' | 'md' | 'lg' | 'full' | 'dynamic';
|
||||
hideX?: boolean;
|
||||
noPadding?: boolean;
|
||||
noScroll?: boolean;
|
||||
}
|
||||
|
||||
export function Dialog({
|
||||
@@ -29,6 +30,7 @@ export function Dialog({
|
||||
description,
|
||||
hideX,
|
||||
noPadding,
|
||||
noScroll,
|
||||
}: DialogProps) {
|
||||
const titleId = useMemo(() => Math.random().toString(36).slice(2), []);
|
||||
const descriptionId = useMemo(
|
||||
@@ -60,7 +62,7 @@ export function Dialog({
|
||||
animate={{ top: 0, scale: 1 }}
|
||||
className={classNames(
|
||||
className,
|
||||
'grid grid-rows-[auto_minmax(0,1fr)]',
|
||||
'grid grid-rows-[auto_auto_minmax(0,1fr)]',
|
||||
'relative bg-gray-50 pointer-events-auto',
|
||||
'rounded-lg',
|
||||
'dark:border border-highlight shadow shadow-black/10',
|
||||
@@ -79,15 +81,20 @@ export function Dialog({
|
||||
) : (
|
||||
<span />
|
||||
)}
|
||||
{description && (
|
||||
|
||||
{description ? (
|
||||
<p className="px-6 text-gray-700" id={descriptionId}>
|
||||
{description}
|
||||
</p>
|
||||
) : (
|
||||
<span />
|
||||
)}
|
||||
|
||||
<div
|
||||
className={classNames(
|
||||
'h-full w-full grid grid-cols-[minmax(0,1fr)] overflow-y-auto',
|
||||
'h-full w-full grid grid-cols-[minmax(0,1fr)]',
|
||||
!noPadding && 'px-6 py-2',
|
||||
!noScroll && 'overflow-y-auto',
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
|
||||
@@ -55,7 +55,6 @@ export type DropdownItem = DropdownItemDefault | DropdownItemSeparator;
|
||||
export interface DropdownProps {
|
||||
children: ReactElement<HTMLAttributes<HTMLButtonElement>>;
|
||||
items: DropdownItem[];
|
||||
openOnHotKeyAction?: HotkeyAction;
|
||||
onOpen?: () => void;
|
||||
onClose?: () => void;
|
||||
}
|
||||
@@ -71,7 +70,7 @@ export interface DropdownRef {
|
||||
}
|
||||
|
||||
export const Dropdown = forwardRef<DropdownRef, DropdownProps>(function Dropdown(
|
||||
{ children, items, openOnHotKeyAction, onOpen, onClose }: DropdownProps,
|
||||
{ children, items, onOpen, onClose }: DropdownProps,
|
||||
ref,
|
||||
) {
|
||||
const [isOpen, _setIsOpen] = useState<boolean>(false);
|
||||
@@ -88,11 +87,6 @@ export const Dropdown = forwardRef<DropdownRef, DropdownProps>(function Dropdown
|
||||
[onClose, onOpen],
|
||||
);
|
||||
|
||||
useHotKey(openOnHotKeyAction ?? null, () => {
|
||||
setIsOpen(true);
|
||||
menuRef.current?.next?.();
|
||||
});
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
...menuRef.current,
|
||||
isOpen: isOpen,
|
||||
@@ -289,17 +283,27 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle'>, MenuPro
|
||||
});
|
||||
}, [items]);
|
||||
|
||||
useKey('ArrowUp', (e) => {
|
||||
if (!isOpen) return;
|
||||
e.preventDefault();
|
||||
handlePrev();
|
||||
});
|
||||
useKey(
|
||||
'ArrowUp',
|
||||
(e) => {
|
||||
if (!isOpen) return;
|
||||
e.preventDefault();
|
||||
handlePrev();
|
||||
},
|
||||
{},
|
||||
[isOpen],
|
||||
);
|
||||
|
||||
useKey('ArrowDown', (e) => {
|
||||
if (!isOpen) return;
|
||||
e.preventDefault();
|
||||
handleNext();
|
||||
});
|
||||
useKey(
|
||||
'ArrowDown',
|
||||
(e) => {
|
||||
if (!isOpen) return;
|
||||
e.preventDefault();
|
||||
handleNext();
|
||||
},
|
||||
{},
|
||||
[isOpen],
|
||||
);
|
||||
|
||||
const handleSelect = useCallback(
|
||||
(i: DropdownItem) => {
|
||||
|
||||
14
src-web/components/core/Editor/BetterMatchDecorator.ts
Normal file
14
src-web/components/core/Editor/BetterMatchDecorator.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { type DecorationSet, MatchDecorator, type ViewUpdate } from '@codemirror/view';
|
||||
|
||||
/**
|
||||
* This is a custom MatchDecorator that will not decorate a match if the selection is inside it
|
||||
*/
|
||||
export class BetterMatchDecorator extends MatchDecorator {
|
||||
updateDeco(update: ViewUpdate, deco: DecorationSet): DecorationSet {
|
||||
if (!update.startState.selection.eq(update.state.selection)) {
|
||||
return super.createDeco(update.view);
|
||||
} else {
|
||||
return super.updateDeco(update, deco);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -55,7 +55,7 @@
|
||||
}
|
||||
|
||||
.placeholder-widget {
|
||||
@apply text-xs text-violet-700 dark:text-violet-700 px-1 rounded cursor-default dark:shadow;
|
||||
@apply text-xs text-violet-700 dark:text-violet-700 px-1 mx-[0.5px] rounded cursor-default dark:shadow;
|
||||
|
||||
/* NOTE: Background and border are translucent so we can see text selection through it */
|
||||
@apply bg-violet-500/20 border border-violet-500/20 border-opacity-40;
|
||||
@@ -69,6 +69,14 @@
|
||||
@apply text-red-700 dark:text-red-800 bg-red-300/30 border-red-300/80 border-opacity-40 hover:border-red-300 hover:bg-red-300/40;
|
||||
}
|
||||
}
|
||||
|
||||
.hyperlink-widget {
|
||||
& > * {
|
||||
@apply underline;
|
||||
}
|
||||
|
||||
-webkit-text-security: none;
|
||||
}
|
||||
}
|
||||
|
||||
&.cm-singleline {
|
||||
@@ -103,10 +111,10 @@
|
||||
@apply font-mono text-[0.75rem];
|
||||
|
||||
/*
|
||||
* Round corners or they'll stick out of the editor bounds of editor is rounded.
|
||||
* Could potentially be pushed up from the editor like we do with bg color but this
|
||||
* is probably fine.
|
||||
*/
|
||||
* Round corners or they'll stick out of the editor bounds of editor is rounded.
|
||||
* Could potentially be pushed up from the editor like we do with bg color but this
|
||||
* is probably fine.
|
||||
*/
|
||||
@apply rounded-lg;
|
||||
}
|
||||
}
|
||||
@@ -123,7 +131,8 @@
|
||||
}
|
||||
|
||||
.cm-editor .fold-gutter-icon {
|
||||
@apply pt-[0.25em] pl-[0.4em] px-[0.4em] h-4 cursor-pointer rounded;
|
||||
@apply pt-[0.25em] pl-[0.4em] px-[0.4em] h-4 rounded;
|
||||
@apply cursor-default !important;
|
||||
}
|
||||
|
||||
.cm-editor .fold-gutter-icon::after {
|
||||
@@ -144,8 +153,9 @@
|
||||
}
|
||||
|
||||
.cm-editor .cm-foldPlaceholder {
|
||||
@apply px-2 border border-gray-400/50 bg-gray-300/50 cursor-default;
|
||||
@apply px-2 border border-gray-400/50 bg-gray-300/50;
|
||||
@apply hover:text-gray-800 hover:border-gray-400;
|
||||
@apply cursor-default !important;
|
||||
}
|
||||
|
||||
.cm-editor .cm-activeLineGutter {
|
||||
@@ -167,8 +177,8 @@
|
||||
@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;
|
||||
@@ -176,9 +186,31 @@
|
||||
}
|
||||
}
|
||||
|
||||
.cm-tooltip.cm-tooltip-hover {
|
||||
@apply shadow-lg bg-gray-100 rounded text-gray-700 border border-gray-500 z-50 pointer-events-auto text-xs;
|
||||
@apply px-2 py-1;
|
||||
|
||||
a {
|
||||
@apply text-gray-800;
|
||||
|
||||
&:hover {
|
||||
@apply underline;
|
||||
}
|
||||
|
||||
&::after {
|
||||
@apply text-gray-800 bg-gray-800 h-3 w-3 ml-1;
|
||||
content: '';
|
||||
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100' fill='black' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M8.636 3.5a.5.5 0 0 0-.5-.5H1.5A1.5 1.5 0 0 0 0 4.5v10A1.5 1.5 0 0 0 1.5 16h10a1.5 1.5 0 0 0 1.5-1.5V7.864a.5.5 0 0 0-1 0V14.5a.5.5 0 0 1-.5.5h-10a.5.5 0 0 1-.5-.5v-10a.5.5 0 0 1 .5-.5h6.636a.5.5 0 0 0 .5-.5z'/%3E%3Cpath fill-rule='evenodd' d='M16 .5a.5.5 0 0 0-.5-.5h-5a.5.5 0 0 0 0 1h3.793L6.146 9.146a.5.5 0 1 0 .708.708L15 1.707V5.5a.5.5 0 0 0 1 0v-5z'/%3E%3C/svg%3E");
|
||||
-webkit-mask-size: contain;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* NOTE: Extra selector required to override default styles */
|
||||
.cm-tooltip.cm-tooltip {
|
||||
@apply shadow-lg bg-gray-50 rounded text-gray-700 border border-gray-200 z-50 pointer-events-auto text-[0.75rem];
|
||||
.cm-tooltip.cm-tooltip-autocomplete,
|
||||
.cm-tooltip.cm-completionInfo {
|
||||
@apply shadow-lg bg-gray-100 rounded text-gray-700 border border-gray-300 z-50 pointer-events-auto text-xs;
|
||||
|
||||
.cm-completionIcon {
|
||||
@apply italic font-mono;
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
cloneElement,
|
||||
forwardRef,
|
||||
isValidElement,
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useImperativeHandle,
|
||||
@@ -57,7 +56,7 @@ export interface EditorProps {
|
||||
actions?: ReactNode;
|
||||
}
|
||||
|
||||
const _Editor = forwardRef<EditorView | undefined, EditorProps>(function Editor(
|
||||
export const Editor = forwardRef<EditorView | undefined, EditorProps>(function Editor(
|
||||
{
|
||||
readOnly,
|
||||
type = 'text',
|
||||
@@ -179,7 +178,9 @@ const _Editor = forwardRef<EditorView | undefined, EditorProps>(function Editor(
|
||||
doc: `${defaultValue ?? ''}`,
|
||||
extensions: [
|
||||
languageCompartment.of(langExt),
|
||||
placeholderCompartment.current.of([]),
|
||||
placeholderCompartment.current.of(
|
||||
placeholderExt(placeholderElFromText(placeholder ?? '')),
|
||||
),
|
||||
wrapLinesCompartment.current.of([]),
|
||||
...getExtensions({
|
||||
container,
|
||||
@@ -293,8 +294,6 @@ const _Editor = forwardRef<EditorView | undefined, EditorProps>(function Editor(
|
||||
);
|
||||
});
|
||||
|
||||
export const Editor = memo(_Editor);
|
||||
|
||||
function getExtensions({
|
||||
container,
|
||||
readOnly,
|
||||
@@ -317,7 +316,12 @@ function getExtensions({
|
||||
undefined;
|
||||
|
||||
return [
|
||||
...baseExtensions,
|
||||
// NOTE: These *must* be anonymous functions so the references update properly
|
||||
EditorView.domEventHandlers({
|
||||
focus: () => onFocus.current?.(),
|
||||
blur: () => onBlur.current?.(),
|
||||
keydown: (e) => onKeyDown.current?.(e),
|
||||
}),
|
||||
tooltips({ parent }),
|
||||
keymap.of(singleLine ? defaultKeymap.filter((k) => k.key !== 'Enter') : defaultKeymap),
|
||||
...(singleLine ? [singleLineExt()] : []),
|
||||
@@ -326,20 +330,14 @@ function getExtensions({
|
||||
? [EditorState.readOnly.of(true), EditorView.contentAttributes.of({ tabindex: '-1' })]
|
||||
: []),
|
||||
|
||||
// Handle onFocus
|
||||
// NOTE: These *must* be anonymous functions so the references update properly
|
||||
EditorView.domEventHandlers({
|
||||
focus: () => onFocus.current?.(),
|
||||
blur: () => onBlur.current?.(),
|
||||
keydown: (e) => onKeyDown.current?.(e),
|
||||
}),
|
||||
|
||||
// Handle onChange
|
||||
EditorView.updateListener.of((update) => {
|
||||
if (onChange && update.docChanged) {
|
||||
onChange.current?.(update.state.doc.toString());
|
||||
}
|
||||
}),
|
||||
|
||||
...baseExtensions,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -122,11 +122,8 @@ export const baseExtensions = [
|
||||
history(),
|
||||
dropCursor(),
|
||||
drawSelection(),
|
||||
// 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, // For debugging in devtools without closing it
|
||||
closeOnBlur: true, // Set to `false` for debugging in devtools without closing it
|
||||
compareCompletions: (a, b) => {
|
||||
// Don't sort completions at all, only on boost
|
||||
return (a.boost ?? 0) - (b.boost ?? 0);
|
||||
|
||||
98
src-web/components/core/Editor/hyperlink/extension.ts
Normal file
98
src-web/components/core/Editor/hyperlink/extension.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import type { DecorationSet, ViewUpdate } from '@codemirror/view';
|
||||
import { Decoration, hoverTooltip, MatchDecorator, ViewPlugin } from '@codemirror/view';
|
||||
import { EditorView } from 'codemirror';
|
||||
|
||||
const REGEX =
|
||||
/(https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&/=]*))/g;
|
||||
|
||||
const tooltip = hoverTooltip(
|
||||
(view, pos, side) => {
|
||||
const { from, text } = view.state.doc.lineAt(pos);
|
||||
let match;
|
||||
let found: { start: number; end: number } | null = null;
|
||||
|
||||
while ((match = REGEX.exec(text))) {
|
||||
const start = from + match.index;
|
||||
const end = start + match[0].length;
|
||||
|
||||
if (pos >= start && pos <= end) {
|
||||
found = { start, end };
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (found == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ((found.start == pos && side < 0) || (found.end == pos && side > 0)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
pos: found.start,
|
||||
end: found.end,
|
||||
create() {
|
||||
const dom = document.createElement('a');
|
||||
dom.textContent = 'Open in browser';
|
||||
dom.href = text.substring(found!.start - from, found!.end - from);
|
||||
dom.target = '_blank';
|
||||
dom.rel = 'noopener noreferrer';
|
||||
return { dom };
|
||||
},
|
||||
};
|
||||
},
|
||||
{
|
||||
hoverTime: 100,
|
||||
},
|
||||
);
|
||||
|
||||
const decorator = function () {
|
||||
const placeholderMatcher = new MatchDecorator({
|
||||
regexp: REGEX,
|
||||
decoration(match, view, matchStartPos) {
|
||||
const matchEndPos = matchStartPos + match[0].length - 1;
|
||||
|
||||
// Don't decorate if the cursor is inside the match
|
||||
for (const r of view.state.selection.ranges) {
|
||||
if (r.from > matchStartPos && r.to <= matchEndPos) {
|
||||
return Decoration.replace({});
|
||||
}
|
||||
}
|
||||
|
||||
const groupMatch = match[1];
|
||||
if (groupMatch == null) {
|
||||
// Should never happen, but make TS happy
|
||||
console.warn('Group match was empty', match);
|
||||
return Decoration.replace({});
|
||||
}
|
||||
|
||||
return Decoration.mark({
|
||||
class: 'hyperlink-widget',
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return ViewPlugin.fromClass(
|
||||
class {
|
||||
placeholders: DecorationSet;
|
||||
|
||||
constructor(view: EditorView) {
|
||||
this.placeholders = placeholderMatcher.createDeco(view);
|
||||
}
|
||||
|
||||
update(update: ViewUpdate) {
|
||||
this.placeholders = placeholderMatcher.updateDeco(update, this.placeholders);
|
||||
}
|
||||
},
|
||||
{
|
||||
decorations: (instance) => instance.placeholders,
|
||||
provide: (plugin) =>
|
||||
EditorView.bidiIsolatedRanges.of((view) => {
|
||||
return view.plugin(plugin)?.placeholders || Decoration.none;
|
||||
}),
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
export const hyperlink = [tooltip, decorator()];
|
||||
@@ -1,11 +1,9 @@
|
||||
import type { DecorationSet, ViewUpdate } from '@codemirror/view';
|
||||
import { Decoration, EditorView, MatchDecorator, ViewPlugin, WidgetType } from '@codemirror/view';
|
||||
import { Decoration, EditorView, ViewPlugin, WidgetType } from '@codemirror/view';
|
||||
import { BetterMatchDecorator } from '../BetterMatchDecorator';
|
||||
|
||||
class PlaceholderWidget extends WidgetType {
|
||||
constructor(
|
||||
readonly name: string,
|
||||
readonly isExistingVariable: boolean,
|
||||
) {
|
||||
constructor(readonly name: string, readonly isExistingVariable: boolean) {
|
||||
super();
|
||||
}
|
||||
eq(other: PlaceholderWidget) {
|
||||
@@ -25,19 +23,6 @@ class PlaceholderWidget extends WidgetType {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a custom MatchDecorator that will not decorate a match if the selection is inside it
|
||||
*/
|
||||
class BetterMatchDecorator extends MatchDecorator {
|
||||
updateDeco(update: ViewUpdate, deco: DecorationSet): DecorationSet {
|
||||
if (!update.startState.selection.eq(update.state.selection)) {
|
||||
return super.createDeco(update.view);
|
||||
} else {
|
||||
return super.updateDeco(update, deco);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const placeholders = function (variables: { name: string }[]) {
|
||||
const placeholderMatcher = new BetterMatchDecorator({
|
||||
regexp: /\$\{\[\s*([^\]\s]+)\s*]}/g,
|
||||
|
||||
@@ -17,6 +17,7 @@ const icons = {
|
||||
arrowUpFromDot: lucide.ArrowUpFromDotIcon,
|
||||
box: lucide.BoxIcon,
|
||||
cake: lucide.CakeIcon,
|
||||
minus: lucide.MinusIcon,
|
||||
chat: lucide.MessageSquare,
|
||||
check: lucide.CheckIcon,
|
||||
chevronDown: lucide.ChevronDownIcon,
|
||||
|
||||
@@ -2,6 +2,7 @@ import classNames from 'classnames';
|
||||
import type { EditorView } from 'codemirror';
|
||||
import type { HTMLAttributes, ReactNode } from 'react';
|
||||
import { forwardRef, useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { useStateSyncDefault } from '../../hooks/useStateSyncDefault';
|
||||
import type { EditorProps } from './Editor';
|
||||
import { Editor } from './Editor';
|
||||
import { IconButton } from './IconButton';
|
||||
@@ -69,7 +70,7 @@ export const Input = forwardRef<EditorView | undefined, InputProps>(function Inp
|
||||
}: InputProps,
|
||||
ref,
|
||||
) {
|
||||
const [obscured, setObscured] = useState(type === 'password');
|
||||
const [obscured, setObscured] = useStateSyncDefault(type === 'password');
|
||||
const [currentValue, setCurrentValue] = useState(defaultValue ?? '');
|
||||
const [focused, setFocused] = useState(false);
|
||||
|
||||
@@ -181,9 +182,10 @@ export const Input = forwardRef<EditorView | undefined, InputProps>(function Inp
|
||||
<IconButton
|
||||
title={obscured ? `Show ${label}` : `Obscure ${label}`}
|
||||
size="xs"
|
||||
className="mr-0.5"
|
||||
className="mr-0.5 group/obscure !h-auto my-0.5"
|
||||
iconClassName="text-gray-500 group-hover/obscure:text-gray-800"
|
||||
iconSize="sm"
|
||||
icon={obscured ? 'eyeClosed' : 'eye'}
|
||||
icon={obscured ? 'eye' : 'eyeClosed'}
|
||||
onClick={() => setObscured((o) => !o)}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -33,3 +33,7 @@ export function Link({ href, children, className, ...other }: Props) {
|
||||
</RouterLink>
|
||||
);
|
||||
}
|
||||
|
||||
export function FeedbackLink() {
|
||||
return <Link href="https://yaak.canny.io">Feedback</Link>;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Fragment, memo, useCallback, useEffect, useMemo, useRef, useState } fro
|
||||
import type { XYCoord } from 'react-dnd';
|
||||
import { useDrag, useDrop } from 'react-dnd';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { usePrompt } from '../../hooks/usePrompt';
|
||||
import { DropMarker } from '../DropMarker';
|
||||
import { Button } from './Button';
|
||||
import { Checkbox } from './Checkbox';
|
||||
@@ -14,6 +15,7 @@ import { Icon } from './Icon';
|
||||
import { IconButton } from './IconButton';
|
||||
import type { InputProps } from './Input';
|
||||
import { Input } from './Input';
|
||||
import { RadioDropdown } from './RadioDropdown';
|
||||
|
||||
export type PairEditorProps = {
|
||||
pairs: Pair[];
|
||||
@@ -22,6 +24,7 @@ export type PairEditorProps = {
|
||||
className?: string;
|
||||
namePlaceholder?: string;
|
||||
valuePlaceholder?: string;
|
||||
valueType?: 'text' | 'password';
|
||||
nameAutocomplete?: GenericCompletionConfig;
|
||||
valueAutocomplete?: (name: string) => GenericCompletionConfig | undefined;
|
||||
nameAutocompleteVariables?: boolean;
|
||||
@@ -29,6 +32,7 @@ export type PairEditorProps = {
|
||||
allowFileValues?: boolean;
|
||||
nameValidate?: InputProps['validate'];
|
||||
valueValidate?: InputProps['validate'];
|
||||
noScroll?: boolean;
|
||||
};
|
||||
|
||||
export type Pair = {
|
||||
@@ -36,6 +40,7 @@ export type Pair = {
|
||||
enabled?: boolean;
|
||||
name: string;
|
||||
value: string;
|
||||
contentType?: string;
|
||||
isFile?: boolean;
|
||||
};
|
||||
|
||||
@@ -51,7 +56,9 @@ export const PairEditor = memo(function PairEditor({
|
||||
nameAutocompleteVariables,
|
||||
namePlaceholder,
|
||||
nameValidate,
|
||||
valueType,
|
||||
onChange,
|
||||
noScroll,
|
||||
pairs: originalPairs,
|
||||
valueAutocomplete,
|
||||
valueAutocompleteVariables,
|
||||
@@ -90,7 +97,7 @@ export const PairEditor = memo(function PairEditor({
|
||||
[onChange],
|
||||
);
|
||||
|
||||
const handleMove = useCallback<FormRowProps['onMove']>(
|
||||
const handleMove = useCallback<PairEditorRowProps['onMove']>(
|
||||
(id, side) => {
|
||||
const dragIndex = pairs.findIndex((r) => r.id === id);
|
||||
setHoveredIndex(side === 'above' ? dragIndex : dragIndex + 1);
|
||||
@@ -98,7 +105,7 @@ export const PairEditor = memo(function PairEditor({
|
||||
[pairs],
|
||||
);
|
||||
|
||||
const handleEnd = useCallback<FormRowProps['onEnd']>(
|
||||
const handleEnd = useCallback<PairEditorRowProps['onEnd']>(
|
||||
(id: string) => {
|
||||
if (hoveredIndex === null) return;
|
||||
setHoveredIndex(null);
|
||||
@@ -157,7 +164,8 @@ export const PairEditor = memo(function PairEditor({
|
||||
className={classNames(
|
||||
className,
|
||||
'@container',
|
||||
'pb-2 grid overflow-auto max-h-full',
|
||||
'pb-2 mb-auto',
|
||||
!noScroll && 'overflow-y-auto max-h-full',
|
||||
// Move over the width of the drag handle
|
||||
'-ml-3',
|
||||
// Pad to make room for the drag divider
|
||||
@@ -169,13 +177,14 @@ export const PairEditor = memo(function PairEditor({
|
||||
return (
|
||||
<Fragment key={p.id}>
|
||||
{hoveredIndex === i && <DropMarker />}
|
||||
<FormRow
|
||||
<PairEditorRow
|
||||
pairContainer={p}
|
||||
className="py-1"
|
||||
isLast={isLast}
|
||||
allowFileValues={allowFileValues}
|
||||
nameAutocompleteVariables={nameAutocompleteVariables}
|
||||
valueAutocompleteVariables={valueAutocompleteVariables}
|
||||
valueType={valueType}
|
||||
forceFocusPairId={forceFocusPairId}
|
||||
forceUpdateKey={forceUpdateKey}
|
||||
nameAutocomplete={nameAutocomplete}
|
||||
@@ -201,7 +210,7 @@ enum ItemTypes {
|
||||
ROW = 'pair-row',
|
||||
}
|
||||
|
||||
type FormRowProps = {
|
||||
type PairEditorRowProps = {
|
||||
className?: string;
|
||||
pairContainer: PairContainer;
|
||||
forceFocusPairId?: string | null;
|
||||
@@ -218,6 +227,7 @@ type FormRowProps = {
|
||||
| 'valueAutocomplete'
|
||||
| 'nameAutocompleteVariables'
|
||||
| 'valueAutocompleteVariables'
|
||||
| 'valueType'
|
||||
| 'namePlaceholder'
|
||||
| 'valuePlaceholder'
|
||||
| 'nameValidate'
|
||||
@@ -226,7 +236,7 @@ type FormRowProps = {
|
||||
| 'allowFileValues'
|
||||
>;
|
||||
|
||||
const FormRow = memo(function FormRow({
|
||||
function PairEditorRow({
|
||||
allowFileValues,
|
||||
className,
|
||||
forceFocusPairId,
|
||||
@@ -246,9 +256,11 @@ const FormRow = memo(function FormRow({
|
||||
valueAutocompleteVariables,
|
||||
valuePlaceholder,
|
||||
valueValidate,
|
||||
}: FormRowProps) {
|
||||
valueType,
|
||||
}: PairEditorRowProps) {
|
||||
const { id } = pairContainer;
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const prompt = usePrompt();
|
||||
const nameInputRef = useRef<EditorView>(null);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -278,6 +290,11 @@ const FormRow = memo(function FormRow({
|
||||
[onChange, id, pairContainer.pair],
|
||||
);
|
||||
|
||||
const handleChangeValueContentType = useMemo(
|
||||
() => (contentType: string) => onChange({ id, pair: { ...pairContainer.pair, contentType } }),
|
||||
[onChange, id, pairContainer.pair],
|
||||
);
|
||||
|
||||
const handleFocus = useCallback(() => onFocus?.(pairContainer), [onFocus, pairContainer]);
|
||||
const handleDelete = useCallback(
|
||||
() => onDelete?.(pairContainer, false),
|
||||
@@ -397,42 +414,76 @@ const FormRow = memo(function FormRow({
|
||||
name="value"
|
||||
onChange={handleChangeValueText}
|
||||
onFocus={handleFocus}
|
||||
type={isLast ? 'text' : valueType}
|
||||
placeholder={valuePlaceholder ?? 'value'}
|
||||
autocomplete={valueAutocomplete?.(pairContainer.pair.name)}
|
||||
autocompleteVariables={valueAutocompleteVariables}
|
||||
/>
|
||||
)}
|
||||
{allowFileValues && (
|
||||
<Dropdown
|
||||
items={[
|
||||
{ key: 'text', label: 'Text', onSelect: () => handleChangeValueText('') },
|
||||
{ key: 'file', label: 'File', onSelect: () => handleChangeValueFile('') },
|
||||
]}
|
||||
>
|
||||
<IconButton
|
||||
iconSize="sm"
|
||||
size="xs"
|
||||
icon={isLast ? 'empty' : 'chevronDown'}
|
||||
title="Select form data type"
|
||||
/>
|
||||
</Dropdown>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<IconButton
|
||||
aria-hidden={isLast}
|
||||
disabled={isLast}
|
||||
color="custom"
|
||||
icon={!isLast ? 'trash' : 'empty'}
|
||||
size="sm"
|
||||
iconSize="sm"
|
||||
title="Delete header"
|
||||
onClick={!isLast ? handleDelete : undefined}
|
||||
className="ml-0.5 opacity-0 group-hover:!opacity-100 focus-visible:!opacity-100"
|
||||
/>
|
||||
{allowFileValues ? (
|
||||
<RadioDropdown
|
||||
value={pairContainer.pair.isFile ? 'file' : 'text'}
|
||||
onChange={(v) => {
|
||||
if (v === 'file') handleChangeValueFile('');
|
||||
else handleChangeValueText('');
|
||||
}}
|
||||
items={[
|
||||
{ label: 'Text', value: 'text' },
|
||||
{ label: 'File', value: 'file' },
|
||||
]}
|
||||
extraItems={[
|
||||
{
|
||||
key: 'mime',
|
||||
label: 'Set Content-Type',
|
||||
leftSlot: <Icon icon="pencil" />,
|
||||
onSelect: async () => {
|
||||
const v = await prompt({
|
||||
id: 'content-type',
|
||||
require: false,
|
||||
title: 'Override Content-Type',
|
||||
label: 'Content-Type',
|
||||
placeholder: 'text/plain',
|
||||
defaultValue: pairContainer.pair.contentType ?? '',
|
||||
name: 'content-type',
|
||||
confirmLabel: 'Set',
|
||||
description: 'Leave blank to auto-detect',
|
||||
});
|
||||
handleChangeValueContentType(v);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'delete',
|
||||
label: 'Delete',
|
||||
onSelect: handleDelete,
|
||||
variant: 'danger',
|
||||
leftSlot: <Icon icon="trash" />,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<IconButton
|
||||
iconSize="sm"
|
||||
size="xs"
|
||||
icon={isLast ? 'empty' : 'chevronDown'}
|
||||
title="Select form data type"
|
||||
/>
|
||||
</RadioDropdown>
|
||||
) : (
|
||||
<Dropdown
|
||||
items={[{ key: 'delete', label: 'Delete', onSelect: handleDelete, variant: 'danger' }]}
|
||||
>
|
||||
<IconButton
|
||||
iconSize="sm"
|
||||
size="xs"
|
||||
icon={isLast ? 'empty' : 'chevronDown'}
|
||||
title="Select form data type"
|
||||
/>
|
||||
</Dropdown>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const newPairContainer = (initialPair?: Pair): PairContainer => {
|
||||
const id = initialPair?.id ?? uuid();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useMemo } from 'react';
|
||||
import type { DropdownItemSeparator, DropdownProps } from './Dropdown';
|
||||
import type { DropdownItem, DropdownItemSeparator, DropdownProps } from './Dropdown';
|
||||
import { Dropdown } from './Dropdown';
|
||||
import { Icon } from './Icon';
|
||||
|
||||
@@ -42,7 +42,7 @@ export function RadioDropdown<T = string | null>({
|
||||
};
|
||||
}
|
||||
}),
|
||||
...(extraItems ?? []),
|
||||
...((extraItems ? [{ type: 'separator' }, ...extraItems] : []) as DropdownItem[]),
|
||||
],
|
||||
[items, extraItems, value, onChange],
|
||||
);
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import classNames from 'classnames';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useContentTypeFromHeaders } from '../../hooks/useContentTypeFromHeaders';
|
||||
import { useDebouncedState } from '../../hooks/useDebouncedState';
|
||||
import { useFilterResponse } from '../../hooks/useFilterResponse';
|
||||
import { useResponseBodyText } from '../../hooks/useResponseBodyText';
|
||||
import { useResponseContentType } from '../../hooks/useResponseContentType';
|
||||
import { useToggle } from '../../hooks/useToggle';
|
||||
import { tryFormatJson, tryFormatXml } from '../../lib/formatters';
|
||||
import type { HttpResponse } from '../../lib/models';
|
||||
import { Editor } from '../core/Editor';
|
||||
import { hyperlink } from '../core/Editor/hyperlink/extension';
|
||||
import { IconButton } from '../core/IconButton';
|
||||
import { Input } from '../core/Input';
|
||||
|
||||
const extraExtensions = [hyperlink];
|
||||
|
||||
interface Props {
|
||||
response: HttpResponse;
|
||||
pretty: boolean;
|
||||
@@ -21,7 +24,7 @@ export function TextViewer({ response, pretty }: Props) {
|
||||
const [isSearching, toggleIsSearching] = useToggle();
|
||||
const [filterText, setDebouncedFilterText, setFilterText] = useDebouncedState<string>('', 400);
|
||||
|
||||
const contentType = useResponseContentType(response);
|
||||
const contentType = useContentTypeFromHeaders(response.headers);
|
||||
const rawBody = useResponseBodyText(response) ?? '';
|
||||
const formattedBody =
|
||||
pretty && contentType?.includes('json')
|
||||
@@ -87,6 +90,7 @@ export function TextViewer({ response, pretty }: Props) {
|
||||
defaultValue={body}
|
||||
contentType={contentType}
|
||||
actions={actions}
|
||||
extraExtensions={extraExtensions}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ export interface PromptProps {
|
||||
name: InputProps['name'];
|
||||
defaultValue: InputProps['defaultValue'];
|
||||
placeholder: InputProps['placeholder'];
|
||||
require?: InputProps['require'];
|
||||
confirmLabel?: string;
|
||||
}
|
||||
|
||||
@@ -22,6 +23,7 @@ export function Prompt({
|
||||
defaultValue,
|
||||
placeholder,
|
||||
onResult,
|
||||
require = true,
|
||||
confirmLabel = 'Save',
|
||||
}: PromptProps) {
|
||||
const [value, setValue] = useState<string>(defaultValue ?? '');
|
||||
@@ -41,8 +43,8 @@ export function Prompt({
|
||||
>
|
||||
<Input
|
||||
hideLabel
|
||||
require
|
||||
autoSelect
|
||||
require={require}
|
||||
placeholder={placeholder}
|
||||
label={label}
|
||||
name={name}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useEffect } from 'react';
|
||||
import { NAMESPACE_GLOBAL } from '../lib/keyValueStore';
|
||||
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
|
||||
import { useCookieJars } from './useCookieJars';
|
||||
import { useKeyValue } from './useKeyValue';
|
||||
@@ -9,7 +8,7 @@ export function useActiveCookieJar() {
|
||||
const cookieJars = useCookieJars();
|
||||
|
||||
const kv = useKeyValue<string | null>({
|
||||
namespace: NAMESPACE_GLOBAL,
|
||||
namespace: 'global',
|
||||
key: ['activeCookieJar', workspaceId ?? 'n/a'],
|
||||
fallback: null,
|
||||
});
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type { GrpcRequest, HttpRequest } from '../lib/models';
|
||||
import { useActiveRequestId } from './useActiveRequestId';
|
||||
import { useGrpcRequests } from './useGrpcRequests';
|
||||
import { useHttpRequests } from './useHttpRequests';
|
||||
import { useRequests } from './useRequests';
|
||||
|
||||
interface TypeMap {
|
||||
http_request: HttpRequest;
|
||||
@@ -12,16 +11,14 @@ export function useActiveRequest<T extends keyof TypeMap>(
|
||||
model?: T | undefined,
|
||||
): TypeMap[T] | null {
|
||||
const requestId = useActiveRequestId();
|
||||
const httpRequests = useHttpRequests();
|
||||
const grpcRequests = useGrpcRequests();
|
||||
const requests = useRequests();
|
||||
|
||||
if (model === 'http_request') {
|
||||
return (httpRequests.find((r) => r.id === requestId) ?? null) as TypeMap[T] | null;
|
||||
} else if (model === 'grpc_request') {
|
||||
return (grpcRequests.find((r) => r.id === requestId) ?? null) as TypeMap[T] | null;
|
||||
} else {
|
||||
return (grpcRequests.find((r) => r.id === requestId) ??
|
||||
httpRequests.find((r) => r.id === requestId) ??
|
||||
null) as TypeMap[T] | null;
|
||||
for (const request of requests) {
|
||||
const modelMatch = model == null ? true : request.model === model;
|
||||
if (modelMatch && request.id === requestId) {
|
||||
return request as TypeMap[T];
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import * as app from '@tauri-apps/api/app';
|
||||
import * as path from '@tauri-apps/api/path';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
|
||||
export function useAppInfo() {
|
||||
return useQuery(['appInfo'], async () => {
|
||||
const [version, appDataDir] = await Promise.all([app.getVersion(), path.appDataDir()]);
|
||||
return { version, appDataDir };
|
||||
return (await invoke('cmd_metadata')) as {
|
||||
isDev: boolean;
|
||||
version: string;
|
||||
name: string;
|
||||
appDataDir: string;
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
24
src-web/hooks/useCommandPalette.tsx
Normal file
24
src-web/hooks/useCommandPalette.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { CommandPalette } from '../components/CommandPalette';
|
||||
import { useDialog } from '../components/DialogContext';
|
||||
import { useAppInfo } from './useAppInfo';
|
||||
import { useHotKey } from './useHotKey';
|
||||
|
||||
export function useCommandPalette() {
|
||||
const dialog = useDialog();
|
||||
const appInfo = useAppInfo();
|
||||
useHotKey('command_palette.toggle', () => {
|
||||
// Disabled in production for now
|
||||
if (!appInfo.data?.isDev) {
|
||||
return;
|
||||
}
|
||||
|
||||
dialog.toggle({
|
||||
id: 'command_palette',
|
||||
size: 'md',
|
||||
hideX: true,
|
||||
noPadding: true,
|
||||
noScroll: true,
|
||||
render: ({ hide }) => <CommandPalette onClose={hide} />,
|
||||
});
|
||||
});
|
||||
}
|
||||
41
src-web/hooks/useCommands.ts
Normal file
41
src-web/hooks/useCommands.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import type { UseMutationOptions } from '@tanstack/react-query';
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { useEffect } from 'react';
|
||||
import { createGlobalState } from 'react-use';
|
||||
import type { TrackAction, TrackResource } from '../lib/analytics';
|
||||
import type { Workspace } from '../lib/models';
|
||||
|
||||
interface CommandInstance<T, V> extends UseMutationOptions<V, unknown, T> {
|
||||
track?: [TrackResource, TrackAction];
|
||||
name: string;
|
||||
}
|
||||
|
||||
export type Commands = {
|
||||
'workspace.create': CommandInstance<Partial<Pick<Workspace, 'name'>>, Workspace>;
|
||||
};
|
||||
|
||||
const useCommandState = createGlobalState<Commands>();
|
||||
|
||||
export function useRegisterCommand<K extends keyof Commands>(action: K, command: Commands[K]) {
|
||||
const [, setState] = useCommandState();
|
||||
|
||||
useEffect(() => {
|
||||
setState((commands) => {
|
||||
return { ...commands, [action]: command };
|
||||
});
|
||||
|
||||
// Remove action when it goes out of scope
|
||||
return () => {
|
||||
setState((commands) => {
|
||||
return { ...commands, [action]: undefined };
|
||||
});
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [action]);
|
||||
}
|
||||
|
||||
export function useCommand<K extends keyof Commands>(action: K) {
|
||||
const [commands] = useCommandState();
|
||||
const cmd = commands[action];
|
||||
return useMutation({ ...cmd });
|
||||
}
|
||||
9
src-web/hooks/useContentTypeFromHeaders.ts
Normal file
9
src-web/hooks/useContentTypeFromHeaders.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { useMemo } from 'react';
|
||||
import type { HttpHeader } from '../lib/models';
|
||||
|
||||
export function useContentTypeFromHeaders(headers: HttpHeader[] | null): string | null {
|
||||
return useMemo(
|
||||
() => headers?.find((h) => h.name.toLowerCase() === 'content-type')?.value ?? null,
|
||||
[headers],
|
||||
);
|
||||
}
|
||||
@@ -32,7 +32,12 @@ export function useCreateDropdownItems({
|
||||
label: 'GraphQL Query',
|
||||
leftSlot: hideIcons ? undefined : <Icon icon="plus" />,
|
||||
onSelect: () =>
|
||||
createHttpRequest.mutate({ folderId, bodyType: BODY_TYPE_GRAPHQL, method: 'POST' }),
|
||||
createHttpRequest.mutate({
|
||||
folderId,
|
||||
bodyType: BODY_TYPE_GRAPHQL,
|
||||
method: 'POST',
|
||||
headers: [{ name: 'Content-Type', value: 'application/json' }],
|
||||
}),
|
||||
},
|
||||
{
|
||||
key: 'create-grpc-request',
|
||||
|
||||
@@ -16,7 +16,9 @@ export function useCreateHttpRequest() {
|
||||
return useMutation<
|
||||
HttpRequest,
|
||||
unknown,
|
||||
Partial<Pick<HttpRequest, 'name' | 'sortPriority' | 'folderId' | 'bodyType' | 'method'>>
|
||||
Partial<
|
||||
Pick<HttpRequest, 'name' | 'sortPriority' | 'folderId' | 'bodyType' | 'method' | 'headers'>
|
||||
>
|
||||
>({
|
||||
mutationFn: (patch) => {
|
||||
if (workspaceId === null) {
|
||||
|
||||
@@ -1,31 +1,36 @@
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { save } from '@tauri-apps/api/dialog';
|
||||
import slugify from 'slugify';
|
||||
import { useDialog } from '../components/DialogContext';
|
||||
import { ExportDataDialog } from '../components/ExportDataDialog';
|
||||
import { useActiveWorkspace } from './useActiveWorkspace';
|
||||
import { useAlert } from './useAlert';
|
||||
import { useWorkspaces } from './useWorkspaces';
|
||||
|
||||
export function useExportData() {
|
||||
const workspace = useActiveWorkspace();
|
||||
const workspaces = useWorkspaces();
|
||||
const activeWorkspace = useActiveWorkspace();
|
||||
const alert = useAlert();
|
||||
const dialog = useDialog();
|
||||
|
||||
return useMutation({
|
||||
onError: (err: string) => {
|
||||
alert({ id: 'export-failed', title: 'Export Failed', body: err });
|
||||
},
|
||||
mutationFn: async () => {
|
||||
if (workspace == null) return;
|
||||
if (activeWorkspace == null || workspaces.length === 0) return;
|
||||
|
||||
const workspaceSlug = slugify(workspace.name, { lower: true });
|
||||
const exportPath = await save({
|
||||
title: 'Export Data',
|
||||
defaultPath: `yaak.${workspaceSlug}.json`,
|
||||
dialog.show({
|
||||
id: 'export-data',
|
||||
title: 'Export App Data',
|
||||
size: 'md',
|
||||
noPadding: true,
|
||||
render: ({ hide }) => (
|
||||
<ExportDataDialog
|
||||
onHide={hide}
|
||||
workspaces={workspaces}
|
||||
activeWorkspace={activeWorkspace}
|
||||
/>
|
||||
),
|
||||
});
|
||||
if (exportPath == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
await invoke('cmd_export_data', { workspaceId: workspace.id, exportPath });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
13
src-web/hooks/useFloatingSidebarHidden.ts
Normal file
13
src-web/hooks/useFloatingSidebarHidden.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
|
||||
import { useKeyValue } from './useKeyValue';
|
||||
|
||||
export function useFloatingSidebarHidden() {
|
||||
const activeWorkspaceId = useActiveWorkspaceId();
|
||||
const { set, value } = useKeyValue<boolean>({
|
||||
namespace: 'no_sync',
|
||||
key: ['floating_sidebar_hidden', activeWorkspaceId ?? 'n/a'],
|
||||
fallback: false,
|
||||
});
|
||||
|
||||
return [value, set] as const;
|
||||
}
|
||||
@@ -1,14 +1,18 @@
|
||||
import { useMutation } 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 { useRegisterCommand } from './useCommands';
|
||||
import { usePrompt } from './usePrompt';
|
||||
|
||||
export function useCreateWorkspace({ navigateAfter }: { navigateAfter: boolean }) {
|
||||
const routes = useAppRoutes();
|
||||
export function useGlobalCommands() {
|
||||
const prompt = usePrompt();
|
||||
return useMutation<Workspace, unknown, Partial<Pick<Workspace, 'name'>>>({
|
||||
const routes = useAppRoutes();
|
||||
|
||||
useRegisterCommand('workspace.create', {
|
||||
name: 'New Workspace',
|
||||
track: ['workspace', 'create'],
|
||||
onSuccess: async (workspace) => {
|
||||
routes.navigate('workspace', { workspaceId: workspace.id });
|
||||
},
|
||||
mutationFn: async ({ name: patchName }) => {
|
||||
const name =
|
||||
patchName ??
|
||||
@@ -23,11 +27,5 @@ export function useCreateWorkspace({ navigateAfter }: { navigateAfter: boolean }
|
||||
}));
|
||||
return invoke('cmd_create_workspace', { name });
|
||||
},
|
||||
onSettled: () => trackEvent('workspace', 'create'),
|
||||
onSuccess: async (workspace) => {
|
||||
if (navigateAfter) {
|
||||
routes.navigate('workspace', { workspaceId: workspace.id });
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
import { NAMESPACE_GLOBAL } from '../lib/keyValueStore';
|
||||
import { useKeyValue } from './useKeyValue';
|
||||
|
||||
export function protoFilesArgs(requestId: string | null) {
|
||||
return {
|
||||
namespace: NAMESPACE_GLOBAL,
|
||||
namespace: 'global' as const,
|
||||
key: ['proto_files', requestId ?? 'n/a'],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -13,12 +13,14 @@ export type HotkeyAction =
|
||||
| 'http_request.create'
|
||||
| 'http_request.duplicate'
|
||||
| 'http_request.send'
|
||||
| 'requestSwitcher.next'
|
||||
| 'requestSwitcher.prev'
|
||||
| 'request_switcher.next'
|
||||
| 'request_switcher.prev'
|
||||
| 'request_switcher.toggle'
|
||||
| 'settings.show'
|
||||
| 'sidebar.focus'
|
||||
| 'sidebar.toggle'
|
||||
| 'urlBar.focus';
|
||||
| 'urlBar.focus'
|
||||
| 'command_palette.toggle';
|
||||
|
||||
const hotkeys: Record<HotkeyAction, string[]> = {
|
||||
'environmentEditor.toggle': ['CmdCtrl+Shift+e'],
|
||||
@@ -27,12 +29,14 @@ const hotkeys: Record<HotkeyAction, string[]> = {
|
||||
'http_request.create': ['CmdCtrl+n'],
|
||||
'http_request.duplicate': ['CmdCtrl+d'],
|
||||
'http_request.send': ['CmdCtrl+Enter', 'CmdCtrl+r'],
|
||||
'requestSwitcher.next': ['Control+Shift+Tab'],
|
||||
'requestSwitcher.prev': ['Control+Tab'],
|
||||
'request_switcher.next': ['Control+Shift+Tab'],
|
||||
'request_switcher.prev': ['Control+Tab'],
|
||||
'request_switcher.toggle': ['CmdCtrl+p'],
|
||||
'settings.show': ['CmdCtrl+,'],
|
||||
'sidebar.focus': ['CmdCtrl+1'],
|
||||
'sidebar.toggle': ['CmdCtrl+b'],
|
||||
'urlBar.focus': ['CmdCtrl+l'],
|
||||
'command_palette.toggle': ['CmdCtrl+k'],
|
||||
};
|
||||
|
||||
const hotkeyLabels: Record<HotkeyAction, string> = {
|
||||
@@ -42,12 +46,14 @@ const hotkeyLabels: Record<HotkeyAction, string> = {
|
||||
'http_request.create': 'New Request',
|
||||
'http_request.duplicate': 'Duplicate Request',
|
||||
'http_request.send': 'Send Request',
|
||||
'requestSwitcher.next': 'Go To Previous Request',
|
||||
'requestSwitcher.prev': 'Go To Next Request',
|
||||
'request_switcher.next': 'Go To Previous Request',
|
||||
'request_switcher.prev': 'Go To Next Request',
|
||||
'request_switcher.toggle': 'Toggle Request Switcher',
|
||||
'settings.show': 'Open Settings',
|
||||
'sidebar.focus': 'Focus Sidebar',
|
||||
'sidebar.toggle': 'Toggle Sidebar',
|
||||
'urlBar.focus': 'Focus URL',
|
||||
'command_palette.toggle': 'Toggle Command Palette',
|
||||
};
|
||||
|
||||
export const hotkeyActions: HotkeyAction[] = Object.keys(hotkeys) as (keyof typeof hotkeys)[];
|
||||
@@ -135,7 +141,7 @@ export function useHotKey(
|
||||
document.removeEventListener('keydown', down, { capture: true });
|
||||
document.removeEventListener('keyup', up, { capture: true });
|
||||
};
|
||||
}, [options.enable, os]);
|
||||
}, [action, options.enable, os]);
|
||||
}
|
||||
|
||||
export function useHotKeyLabel(action: HotkeyAction): string {
|
||||
|
||||
@@ -7,6 +7,7 @@ import { VStack } from '../components/core/Stacks';
|
||||
import { useDialog } from '../components/DialogContext';
|
||||
import type { Environment, Folder, GrpcRequest, HttpRequest, Workspace } from '../lib/models';
|
||||
import { count } from '../lib/pluralize';
|
||||
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
|
||||
import { useAlert } from './useAlert';
|
||||
import { useAppRoutes } from './useAppRoutes';
|
||||
|
||||
@@ -19,6 +20,7 @@ export function useImportData() {
|
||||
const routes = useAppRoutes();
|
||||
const dialog = useDialog();
|
||||
const alert = useAlert();
|
||||
const activeWorkspaceId = useActiveWorkspaceId();
|
||||
|
||||
const importData = async () => {
|
||||
const selected = await open(openArgs);
|
||||
@@ -33,7 +35,8 @@ export function useImportData() {
|
||||
httpRequests: HttpRequest[];
|
||||
grpcRequests: GrpcRequest[];
|
||||
} = await invoke('cmd_import_data', {
|
||||
filePaths: Array.isArray(selected) ? selected : [selected],
|
||||
filePath: Array.isArray(selected) ? selected[0] : selected,
|
||||
workspaceId: activeWorkspaceId,
|
||||
});
|
||||
const importedWorkspace = imported.workspaces[0];
|
||||
|
||||
@@ -76,27 +79,40 @@ export function useImportData() {
|
||||
alert({ id: 'import-failed', title: 'Import Failed', body: err });
|
||||
},
|
||||
mutationFn: async () => {
|
||||
dialog.show({
|
||||
id: 'import',
|
||||
title: 'Import Data',
|
||||
size: 'sm',
|
||||
render: ({ hide }) => {
|
||||
return (
|
||||
<VStack space={3} className="pb-4">
|
||||
<p>Insomnia or Postman Collection v2/v2.1 formats are supported</p>
|
||||
<Button
|
||||
size="sm"
|
||||
color="primary"
|
||||
onClick={async () => {
|
||||
await importData();
|
||||
hide();
|
||||
}}
|
||||
>
|
||||
Select File
|
||||
</Button>
|
||||
</VStack>
|
||||
);
|
||||
},
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
dialog.show({
|
||||
id: 'import',
|
||||
title: 'Import Data',
|
||||
size: 'sm',
|
||||
render: ({ hide }) => {
|
||||
return (
|
||||
<VStack space={5} className="pb-4">
|
||||
<VStack space={1}>
|
||||
<p>Supported Formats:</p>
|
||||
<ul className="list-disc pl-5">
|
||||
<li>Postman Collection v2/v2.1</li>
|
||||
<li>Insomnia</li>
|
||||
</ul>
|
||||
</VStack>
|
||||
<Button
|
||||
size="sm"
|
||||
color="primary"
|
||||
onClick={async () => {
|
||||
try {
|
||||
await importData();
|
||||
resolve();
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
hide();
|
||||
}}
|
||||
>
|
||||
Select File
|
||||
</Button>
|
||||
</VStack>
|
||||
);
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -80,10 +80,14 @@ export function useIntrospectGraphQL(baseRequest: HttpRequest) {
|
||||
setRefetchKey((k) => k + 1);
|
||||
}, []);
|
||||
|
||||
const schema = useMemo(
|
||||
() => (introspection ? buildClientSchema(introspection) : undefined),
|
||||
[introspection],
|
||||
);
|
||||
const schema = useMemo(() => {
|
||||
try {
|
||||
return introspection ? buildClientSchema(introspection) : undefined;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
} catch (e: any) {
|
||||
setError('message' in e ? e.message : String(e));
|
||||
}
|
||||
}, [introspection]);
|
||||
|
||||
return { schema, isLoading, error, refetch };
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ export function useKeyValue<T extends Object | null>({
|
||||
key,
|
||||
fallback,
|
||||
}: {
|
||||
namespace?: string;
|
||||
namespace?: 'app' | 'no_sync' | 'global';
|
||||
key: string | string[];
|
||||
fallback: T;
|
||||
}) {
|
||||
|
||||
@@ -14,6 +14,7 @@ export function usePrompt() {
|
||||
defaultValue,
|
||||
placeholder,
|
||||
confirmLabel,
|
||||
require,
|
||||
}: Pick<DialogProps, 'title' | 'description'> &
|
||||
Omit<PromptProps, 'onResult' | 'onHide'> & { id: string }) =>
|
||||
new Promise((onResult: PromptProps['onResult']) => {
|
||||
@@ -24,7 +25,16 @@ export function usePrompt() {
|
||||
hideX: true,
|
||||
size: 'sm',
|
||||
render: ({ hide }) =>
|
||||
Prompt({ onHide: hide, onResult, name, label, defaultValue, placeholder, confirmLabel }),
|
||||
Prompt({
|
||||
onHide: hide,
|
||||
onResult,
|
||||
name,
|
||||
label,
|
||||
defaultValue,
|
||||
placeholder,
|
||||
confirmLabel,
|
||||
require,
|
||||
}),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { getKeyValue, NAMESPACE_GLOBAL } from '../lib/keyValueStore';
|
||||
import { getKeyValue } from '../lib/keyValueStore';
|
||||
import { useActiveEnvironmentId } from './useActiveEnvironmentId';
|
||||
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
|
||||
import { useEnvironments } from './useEnvironments';
|
||||
import { useKeyValue } from './useKeyValue';
|
||||
|
||||
const kvKey = (workspaceId: string) => 'recent_environments::' + workspaceId;
|
||||
const namespace = NAMESPACE_GLOBAL;
|
||||
const namespace = 'global';
|
||||
const fallback: string[] = [];
|
||||
|
||||
export function useRecentEnvironments() {
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { getKeyValue, NAMESPACE_GLOBAL } from '../lib/keyValueStore';
|
||||
import { getKeyValue } from '../lib/keyValueStore';
|
||||
import { useActiveRequestId } from './useActiveRequestId';
|
||||
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
|
||||
import { useGrpcRequests } from './useGrpcRequests';
|
||||
import { useHttpRequests } from './useHttpRequests';
|
||||
import { useKeyValue } from './useKeyValue';
|
||||
import { useRequests } from './useRequests';
|
||||
|
||||
const kvKey = (workspaceId: string) => 'recent_requests::' + workspaceId;
|
||||
const namespace = NAMESPACE_GLOBAL;
|
||||
const namespace = 'global';
|
||||
const fallback: string[] = [];
|
||||
|
||||
export function useRecentRequests() {
|
||||
const httpRequests = useHttpRequests();
|
||||
const grpcRequests = useGrpcRequests();
|
||||
const requests = useMemo(() => [...httpRequests, ...grpcRequests], [httpRequests, grpcRequests]);
|
||||
const requests = useRequests();
|
||||
const activeWorkspaceId = useActiveWorkspaceId();
|
||||
const activeRequestId = useActiveRequestId();
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { getKeyValue, NAMESPACE_GLOBAL } from '../lib/keyValueStore';
|
||||
import { getKeyValue } from '../lib/keyValueStore';
|
||||
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
|
||||
import { useKeyValue } from './useKeyValue';
|
||||
import { useWorkspaces } from './useWorkspaces';
|
||||
|
||||
const kvKey = () => 'recent_workspaces';
|
||||
const namespace = NAMESPACE_GLOBAL;
|
||||
const namespace = 'global';
|
||||
const fallback: string[] = [];
|
||||
|
||||
export function useRecentWorkspaces() {
|
||||
@@ -25,7 +25,7 @@ export function useRecentWorkspaces() {
|
||||
return [activeWorkspaceId, ...withoutCurrent];
|
||||
}).catch(console.error);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
}, [activeWorkspaceId]);
|
||||
|
||||
const onlyValidIds = useMemo(
|
||||
() => kv.value?.filter((id) => workspaces.some((w) => w.id === id)) ?? [],
|
||||
|
||||
9
src-web/hooks/useRequests.ts
Normal file
9
src-web/hooks/useRequests.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useGrpcRequests } from './useGrpcRequests';
|
||||
import { useHttpRequests } from './useHttpRequests';
|
||||
|
||||
export function useRequests() {
|
||||
const httpRequests = useHttpRequests();
|
||||
const grpcRequests = useGrpcRequests();
|
||||
return useMemo(() => [...httpRequests, ...grpcRequests], [httpRequests, grpcRequests]);
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
import type { HttpResponse } from '../lib/models';
|
||||
|
||||
export function useResponseContentType(response: HttpResponse | null): string | null {
|
||||
return useMemo(
|
||||
() => response?.headers.find((h) => h.name.toLowerCase() === 'content-type')?.value ?? null,
|
||||
[response],
|
||||
);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user