This commit is contained in:
Gregory Schier
2025-01-26 13:13:45 -08:00
committed by GitHub
parent 82b1ad35ff
commit f678593903
99 changed files with 3492 additions and 1583 deletions
+44 -44
View File
@@ -20,7 +20,7 @@
"src-web" "src-web"
], ],
"devDependencies": { "devDependencies": {
"@tauri-apps/cli": "^2.2.4", "@tauri-apps/cli": "^2.2.5",
"@typescript-eslint/eslint-plugin": "^8.18.1", "@typescript-eslint/eslint-plugin": "^8.18.1",
"@typescript-eslint/parser": "^8.18.1", "@typescript-eslint/parser": "^8.18.1",
"eslint": "^8", "eslint": "^8",
@@ -2709,9 +2709,9 @@
} }
}, },
"node_modules/@tauri-apps/cli": { "node_modules/@tauri-apps/cli": {
"version": "2.2.4", "version": "2.2.5",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.2.4.tgz", "resolved": "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.2.5.tgz",
"integrity": "sha512-pihbuHEWJa9SEcN7JdEbMa0oq28MTTbk0nNNnRG8/irNQTKcjwM+KzxG2wuYZYbsXQVqwSu7PstdIEAnXqYHkw==", "integrity": "sha512-PaefTQUCYYqvZWdH8EhXQkyJEjQwtoy/OHGoPcZx7Gk3D3K6AtGSxZ9OlHIz3Bu5LDGgVBk36vKtHW0WYsWnbw==",
"dev": true, "dev": true,
"license": "Apache-2.0 OR MIT", "license": "Apache-2.0 OR MIT",
"bin": { "bin": {
@@ -2725,22 +2725,22 @@
"url": "https://opencollective.com/tauri" "url": "https://opencollective.com/tauri"
}, },
"optionalDependencies": { "optionalDependencies": {
"@tauri-apps/cli-darwin-arm64": "2.2.4", "@tauri-apps/cli-darwin-arm64": "2.2.5",
"@tauri-apps/cli-darwin-x64": "2.2.4", "@tauri-apps/cli-darwin-x64": "2.2.5",
"@tauri-apps/cli-linux-arm-gnueabihf": "2.2.4", "@tauri-apps/cli-linux-arm-gnueabihf": "2.2.5",
"@tauri-apps/cli-linux-arm64-gnu": "2.2.4", "@tauri-apps/cli-linux-arm64-gnu": "2.2.5",
"@tauri-apps/cli-linux-arm64-musl": "2.2.4", "@tauri-apps/cli-linux-arm64-musl": "2.2.5",
"@tauri-apps/cli-linux-x64-gnu": "2.2.4", "@tauri-apps/cli-linux-x64-gnu": "2.2.5",
"@tauri-apps/cli-linux-x64-musl": "2.2.4", "@tauri-apps/cli-linux-x64-musl": "2.2.5",
"@tauri-apps/cli-win32-arm64-msvc": "2.2.4", "@tauri-apps/cli-win32-arm64-msvc": "2.2.5",
"@tauri-apps/cli-win32-ia32-msvc": "2.2.4", "@tauri-apps/cli-win32-ia32-msvc": "2.2.5",
"@tauri-apps/cli-win32-x64-msvc": "2.2.4" "@tauri-apps/cli-win32-x64-msvc": "2.2.5"
} }
}, },
"node_modules/@tauri-apps/cli-darwin-arm64": { "node_modules/@tauri-apps/cli-darwin-arm64": {
"version": "2.2.4", "version": "2.2.5",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.2.4.tgz", "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-arm64/-/cli-darwin-arm64-2.2.5.tgz",
"integrity": "sha512-+sMLkQBFebn/UENyaXpyQqRkdFQie8RdEvYVz0AGthm2p0lMVlWiBmc4ImBJmfo8569zVeDX8B+5OWt4/AuZzA==", "integrity": "sha512-qdPmypQE7qj62UJy3Wl/ccCJZwsv5gyBByOrAaG7u5c/PB3QSxhNPegice2k4EHeIuApaVJOoe/CEYVgm/og2Q==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -2755,9 +2755,9 @@
} }
}, },
"node_modules/@tauri-apps/cli-darwin-x64": { "node_modules/@tauri-apps/cli-darwin-x64": {
"version": "2.2.4", "version": "2.2.5",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.2.4.tgz", "resolved": "https://registry.npmjs.org/@tauri-apps/cli-darwin-x64/-/cli-darwin-x64-2.2.5.tgz",
"integrity": "sha512-6fJvXVtQJh7H8q9sll2XC2wO5bpn7bzeh+MQxpcLq6F8SE02sFuNDLN+AqX0DQnuYV0V6jdzM2+bTYOlc1FBsw==", "integrity": "sha512-8JVlCAb2c3n0EcGW7n/1kU4Rq831SsoLDD/0hNp85Um8HGIH2Mg/qos/MLOc8Qv2mOaoKcRKf4hd0I1y0Rl9Cg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -2772,9 +2772,9 @@
} }
}, },
"node_modules/@tauri-apps/cli-linux-arm-gnueabihf": { "node_modules/@tauri-apps/cli-linux-arm-gnueabihf": {
"version": "2.2.4", "version": "2.2.5",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.2.4.tgz", "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm-gnueabihf/-/cli-linux-arm-gnueabihf-2.2.5.tgz",
"integrity": "sha512-QU6Ac6tx79iqkxsDUQesCBNq8RrVSkP9HhVzS2reKthK3xbdTCwNUXoRlfhudKMVrIxV4K7uTwUV99eAnwbm5Q==", "integrity": "sha512-mzxQCqZg7ljRVgekPpXQ5TOehCNgnXh/DNWU6kFjALaBvaw4fGzc369/hV94wOt29htNFyxf8ty2DaQaYljEHw==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -2789,9 +2789,9 @@
} }
}, },
"node_modules/@tauri-apps/cli-linux-arm64-gnu": { "node_modules/@tauri-apps/cli-linux-arm64-gnu": {
"version": "2.2.4", "version": "2.2.5",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.2.4.tgz", "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-gnu/-/cli-linux-arm64-gnu-2.2.5.tgz",
"integrity": "sha512-uZhp312s6VgJJDgUg+HuHZnhjGg93OT+q/aZMoccdZVQ6dvwH8kJzIkKt9zL1U126AXXoesb1EyYmsAruxaUKA==", "integrity": "sha512-M9nkzx5jsSJSNpp7aSza0qv0/N13SUNzH8ysYSZ7IaCN8coGeMg2KgQ5qC6tqUVij2rbg8A/X1n0pPo/gtLx0A==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -2806,9 +2806,9 @@
} }
}, },
"node_modules/@tauri-apps/cli-linux-arm64-musl": { "node_modules/@tauri-apps/cli-linux-arm64-musl": {
"version": "2.2.4", "version": "2.2.5",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.2.4.tgz", "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.2.5.tgz",
"integrity": "sha512-k6JCXd9E+XU0J48nVcFr3QO//bzKg/gp8ZKagBfI2wBpHOk14CnHNBQKNs11nMQUwko4bnPeqj4llcdkbmwIbw==", "integrity": "sha512-tFhZu950HNRLR1RM5Q9Xj5gAlA6AhyyiZgeoXGFAWto+s2jpWmmA3Qq2GUxnVDr7Xui8PF4UY5kANDIOschuwg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -2823,9 +2823,9 @@
} }
}, },
"node_modules/@tauri-apps/cli-linux-x64-gnu": { "node_modules/@tauri-apps/cli-linux-x64-gnu": {
"version": "2.2.4", "version": "2.2.5",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.2.4.tgz", "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-gnu/-/cli-linux-x64-gnu-2.2.5.tgz",
"integrity": "sha512-bUBPU46OF1pNfM6SsGbUlkCBh/pTzvFlEdUpDISsS40v9NVt+kqCy3tHzLGB412E3lSlA6FnshB6HxkdRRdTtg==", "integrity": "sha512-eaGhTQLr3EKeksGsp2tK/Ndi7/oyo3P53Pye6kg0zqXiqu8LQjg1CgvDm1l+5oit04S60zR4AqlDFpoeEtDGgw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -2840,9 +2840,9 @@
} }
}, },
"node_modules/@tauri-apps/cli-linux-x64-musl": { "node_modules/@tauri-apps/cli-linux-x64-musl": {
"version": "2.2.4", "version": "2.2.5",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.2.4.tgz", "resolved": "https://registry.npmjs.org/@tauri-apps/cli-linux-x64-musl/-/cli-linux-x64-musl-2.2.5.tgz",
"integrity": "sha512-vOrpsQDiMtP8q/ZeXfXqgNi3G4Yv5LVX2vI5XkB2yvVuVF1Dvky/hcCJfi9tZQD+IpeiYxjuj7+SxHp82eQ/kA==", "integrity": "sha512-NLAO/SymDxeGuOWWQZCpwoED1K1jaHUvW+u9ip+rTetnxFPLvf3zXthx4QVKfCZLdj2WLQz4cLjHyQdMDXAM+w==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -2857,9 +2857,9 @@
} }
}, },
"node_modules/@tauri-apps/cli-win32-arm64-msvc": { "node_modules/@tauri-apps/cli-win32-arm64-msvc": {
"version": "2.2.4", "version": "2.2.5",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.2.4.tgz", "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-arm64-msvc/-/cli-win32-arm64-msvc-2.2.5.tgz",
"integrity": "sha512-iEP/Cq0ts4Ln4Zh2NSC01lkYEAhr+LotbG4U2z+gxHfCdMrtYYtYdG05C2mpeIxShzL7uEIQb/lhVRBMd7robg==", "integrity": "sha512-yG5KFbqrHfGjkAQAaaCD4i7cJklBjmMxZ2C92DEnqCOujSsEuLxrwwoKxQ4+hqEHOmF3lyX0vfqhgZcS03H38w==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -2874,9 +2874,9 @@
} }
}, },
"node_modules/@tauri-apps/cli-win32-ia32-msvc": { "node_modules/@tauri-apps/cli-win32-ia32-msvc": {
"version": "2.2.4", "version": "2.2.5",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.2.4.tgz", "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-ia32-msvc/-/cli-win32-ia32-msvc-2.2.5.tgz",
"integrity": "sha512-YBbqF0wyHUT00zAGZTTbEbz/C5JDGPnT1Nodor96+tzEU6qAPRYfe5eFe/rpRARbalkpw1UkcVP0Ay8gnksAiA==", "integrity": "sha512-G5lq+2EdxOc8ttg3uhME5t9U3hMGTxwaKz0X4DplTG2Iv4lcNWqw/AESIJVHa5a+EB+ZCC8I+yOfIykp/Cd5mQ==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@@ -2891,9 +2891,9 @@
} }
}, },
"node_modules/@tauri-apps/cli-win32-x64-msvc": { "node_modules/@tauri-apps/cli-win32-x64-msvc": {
"version": "2.2.4", "version": "2.2.5",
"resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.2.4.tgz", "resolved": "https://registry.npmjs.org/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.2.5.tgz",
"integrity": "sha512-MMago/SfWZbUFrwFmPCXmmbb42h7u8Y5jvLvnK2mOpOfCAsei2tLO4hx+Inoai0l2DByuYO4Ef1xDyP6shCsZQ==", "integrity": "sha512-vw4fPVOo0rIQIlqw6xUvK2nwiRFBHNgayDE2Z/SomJlQJAJ1q4VgpHOPl12ouuicmTjK1gWKm7RTouQe3Nig0Q==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
+1 -1
View File
@@ -34,7 +34,7 @@
"tauri-before-dev": "npm run --workspaces --if-present dev" "tauri-before-dev": "npm run --workspaces --if-present dev"
}, },
"devDependencies": { "devDependencies": {
"@tauri-apps/cli": "^2.2.4", "@tauri-apps/cli": "^2.2.5",
"@typescript-eslint/eslint-plugin": "^8.18.1", "@typescript-eslint/eslint-plugin": "^8.18.1",
"@typescript-eslint/parser": "^8.18.1", "@typescript-eslint/parser": "^8.18.1",
"eslint": "^8", "eslint": "^8",
@@ -1,290 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { Environment } from "./models.js";
import type { Folder } from "./models.js";
import type { GrpcRequest } from "./models.js";
import type { HttpRequest } from "./models.js";
import type { HttpResponse } from "./models.js";
import type { JsonValue } from "./serde_json/JsonValue.js";
import type { Workspace } from "./models.js";
export type BootRequest = { dir: string, watch: boolean, };
export type BootResponse = { name: string, version: string, };
export type CallHttpAuthenticationRequest = { config: { [key in string]?: JsonValue }, method: string, url: string, headers: Array<HttpHeader>, };
export type CallHttpAuthenticationResponse = {
/**
* HTTP headers to add to the request. Existing headers will be replaced, while
* new headers will be added.
*/
setHeaders: Array<HttpHeader>, };
export type CallHttpRequestActionArgs = { httpRequest: HttpRequest, };
export type CallHttpRequestActionRequest = { key: string, pluginRefId: string, args: CallHttpRequestActionArgs, };
export type CallTemplateFunctionArgs = { purpose: RenderPurpose, values: { [key in string]?: string }, };
export type CallTemplateFunctionRequest = { name: string, args: CallTemplateFunctionArgs, };
export type CallTemplateFunctionResponse = { value: string | null, };
export type Color = "custom" | "default" | "primary" | "secondary" | "info" | "success" | "notice" | "warning" | "danger";
export type CopyTextRequest = { text: string, };
export type EditorLanguage = "text" | "javascript" | "json" | "html" | "xml" | "graphql" | "markdown";
export type EmptyPayload = {};
export type ErrorResponse = { error: string, };
export type ExportHttpRequestRequest = { httpRequest: HttpRequest, };
export type ExportHttpRequestResponse = { content: string, };
export type FileFilter = { name: string,
/**
* File extensions to require
*/
extensions: Array<string>, };
export type FilterRequest = { content: string, filter: string, };
export type FilterResponse = { content: string, };
export type FindHttpResponsesRequest = { requestId: string, limit?: number, };
export type FindHttpResponsesResponse = { httpResponses: Array<HttpResponse>, };
export type FormInput = { "type": "text" } & FormInputText | { "type": "editor" } & FormInputEditor | { "type": "select" } & FormInputSelect | { "type": "checkbox" } & FormInputCheckbox | { "type": "file" } & FormInputFile | { "type": "http_request" } & FormInputHttpRequest;
export type FormInputBase = { name: string,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, };
export type FormInputCheckbox = { name: string,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, };
export type FormInputEditor = {
/**
* Placeholder for the text input
*/
placeholder?: string | null,
/**
* Don't show the editor gutter (line numbers, folds, etc.)
*/
hideGutter?: boolean,
/**
* Language for syntax highlighting
*/
language?: EditorLanguage, name: string,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, };
export type FormInputFile = {
/**
* The title of the file selection window
*/
title: string,
/**
* Allow selecting multiple files
*/
multiple?: boolean, directory?: boolean, defaultPath?: string, filters?: Array<FileFilter>, name: string,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, };
export type FormInputHttpRequest = { name: string,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, };
export type FormInputSelect = {
/**
* The options that will be available in the select input
*/
options: Array<FormInputSelectOption>, name: string,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, };
export type FormInputSelectOption = { name: string, value: string, };
export type FormInputText = {
/**
* Placeholder for the text input
*/
placeholder?: string | null,
/**
* Placeholder for the text input
*/
password?: boolean, name: string,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, };
export type GetHttpAuthenticationResponse = { name: string, label: string, shortLabel: string, config: Array<FormInput>, };
export type GetHttpRequestActionsRequest = Record<string, never>;
export type GetHttpRequestActionsResponse = { actions: Array<HttpRequestAction>, pluginRefId: string, };
export type GetHttpRequestByIdRequest = { id: string, };
export type GetHttpRequestByIdResponse = { httpRequest: HttpRequest | null, };
export type GetTemplateFunctionsResponse = { functions: Array<TemplateFunction>, pluginRefId: string, };
export type HttpHeader = { name: string, value: string, };
export type HttpRequestAction = { key: string, label: string, icon?: Icon, };
export type Icon = "copy" | "info" | "check_circle" | "alert_triangle" | "_unknown";
export type ImportRequest = { content: string, };
export type ImportResources = { workspaces: Array<Workspace>, environments: Array<Environment>, folders: Array<Folder>, httpRequests: Array<HttpRequest>, grpcRequests: Array<GrpcRequest>, };
export type ImportResponse = { resources: ImportResources, };
export type InternalEvent = { id: string, pluginRefId: string, pluginName: string, replyId: string | null, windowContext: WindowContext, payload: InternalEventPayload, };
export type InternalEventPayload = { "type": "boot_request" } & BootRequest | { "type": "boot_response" } & BootResponse | { "type": "reload_request" } & EmptyPayload | { "type": "reload_response" } & EmptyPayload | { "type": "terminate_request" } | { "type": "terminate_response" } | { "type": "import_request" } & ImportRequest | { "type": "import_response" } & ImportResponse | { "type": "filter_request" } & FilterRequest | { "type": "filter_response" } & FilterResponse | { "type": "export_http_request_request" } & ExportHttpRequestRequest | { "type": "export_http_request_response" } & ExportHttpRequestResponse | { "type": "send_http_request_request" } & SendHttpRequestRequest | { "type": "send_http_request_response" } & SendHttpRequestResponse | { "type": "get_http_request_actions_request" } & EmptyPayload | { "type": "get_http_request_actions_response" } & GetHttpRequestActionsResponse | { "type": "call_http_request_action_request" } & CallHttpRequestActionRequest | { "type": "get_template_functions_request" } | { "type": "get_template_functions_response" } & GetTemplateFunctionsResponse | { "type": "call_template_function_request" } & CallTemplateFunctionRequest | { "type": "call_template_function_response" } & CallTemplateFunctionResponse | { "type": "get_http_authentication_request" } & EmptyPayload | { "type": "get_http_authentication_response" } & GetHttpAuthenticationResponse | { "type": "call_http_authentication_request" } & CallHttpAuthenticationRequest | { "type": "call_http_authentication_response" } & CallHttpAuthenticationResponse | { "type": "copy_text_request" } & CopyTextRequest | { "type": "render_http_request_request" } & RenderHttpRequestRequest | { "type": "render_http_request_response" } & RenderHttpRequestResponse | { "type": "template_render_request" } & TemplateRenderRequest | { "type": "template_render_response" } & TemplateRenderResponse | { "type": "show_toast_request" } & ShowToastRequest | { "type": "prompt_text_request" } & PromptTextRequest | { "type": "prompt_text_response" } & PromptTextResponse | { "type": "get_http_request_by_id_request" } & GetHttpRequestByIdRequest | { "type": "get_http_request_by_id_response" } & GetHttpRequestByIdResponse | { "type": "find_http_responses_request" } & FindHttpResponsesRequest | { "type": "find_http_responses_response" } & FindHttpResponsesResponse | { "type": "empty_response" } & EmptyPayload | { "type": "error_response" } & ErrorResponse;
export type PromptTextRequest = { id: string, title: string, label: string, description?: string, defaultValue?: string, placeholder?: string,
/**
* Text to add to the confirmation button
*/
confirmText?: string,
/**
* Text to add to the cancel button
*/
cancelText?: string,
/**
* Require the user to enter a non-empty value
*/
required?: boolean, };
export type PromptTextResponse = { value: string | null, };
export type RenderHttpRequestRequest = { httpRequest: HttpRequest, purpose: RenderPurpose, };
export type RenderHttpRequestResponse = { httpRequest: HttpRequest, };
export type RenderPurpose = "send" | "preview";
export type SendHttpRequestRequest = { httpRequest: HttpRequest, };
export type SendHttpRequestResponse = { httpResponse: HttpResponse, };
export type ShowToastRequest = { message: string, color?: Color, icon?: Icon, };
export type TemplateFunction = { name: string, description?: string,
/**
* Also support alternative names. This is useful for not breaking existing
* tags when changing the `name` property
*/
aliases?: Array<string>, args: Array<FormInput>, };
export type TemplateRenderRequest = { data: JsonValue, purpose: RenderPurpose, };
export type TemplateRenderResponse = { data: JsonValue, };
export type WindowContext = { "type": "none" } | { "type": "label", label: string, };
@@ -0,0 +1,405 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { Environment } from "./gen_models.js";
import type { Folder } from "./gen_models.js";
import type { GrpcRequest } from "./gen_models.js";
import type { HttpRequest } from "./gen_models.js";
import type { HttpResponse } from "./gen_models.js";
import type { JsonValue } from "./serde_json/JsonValue.js";
import type { Workspace } from "./gen_models.js";
export type BootRequest = { dir: string, watch: boolean, };
export type BootResponse = { name: string, version: string, };
export type CallHttpAuthenticationActionArgs = { contextId: string, values: { [key in string]?: JsonPrimitive }, };
export type CallHttpAuthenticationActionRequest = { index: number, pluginRefId: string, args: CallHttpAuthenticationActionArgs, };
export type CallHttpAuthenticationRequest = { contextId: string, values: { [key in string]?: JsonPrimitive }, method: string, url: string, headers: Array<HttpHeader>, };
export type CallHttpAuthenticationResponse = {
/**
* HTTP headers to add to the request. Existing headers will be replaced, while
* new headers will be added.
*/
setHeaders: Array<HttpHeader>, };
export type CallHttpRequestActionArgs = { httpRequest: HttpRequest, };
export type CallHttpRequestActionRequest = { index: number, pluginRefId: string, args: CallHttpRequestActionArgs, };
export type CallTemplateFunctionArgs = { purpose: RenderPurpose, values: { [key in string]?: string }, };
export type CallTemplateFunctionRequest = { name: string, args: CallTemplateFunctionArgs, };
export type CallTemplateFunctionResponse = { value: string | null, };
export type CloseWindowRequest = { label: string, };
export type Color = "primary" | "secondary" | "info" | "success" | "notice" | "warning" | "danger";
export type CompletionOptionType = "constant" | "variable";
export type Content = { "type": "text", content: string, } | { "type": "markdown", content: string, };
export type CopyTextRequest = { text: string, };
export type DeleteKeyValueRequest = { key: string, };
export type DeleteKeyValueResponse = { deleted: boolean, };
export type EditorLanguage = "text" | "javascript" | "json" | "html" | "xml" | "graphql" | "markdown";
export type EmptyPayload = {};
export type ErrorResponse = { error: string, };
export type ExportHttpRequestRequest = { httpRequest: HttpRequest, };
export type ExportHttpRequestResponse = { content: string, };
export type FileFilter = { name: string,
/**
* File extensions to require
*/
extensions: Array<string>, };
export type FilterRequest = { content: string, filter: string, };
export type FilterResponse = { content: string, };
export type FindHttpResponsesRequest = { requestId: string, limit?: number, };
export type FindHttpResponsesResponse = { httpResponses: Array<HttpResponse>, };
export type FormInput = { "type": "text" } & FormInputText | { "type": "editor" } & FormInputEditor | { "type": "select" } & FormInputSelect | { "type": "checkbox" } & FormInputCheckbox | { "type": "file" } & FormInputFile | { "type": "http_request" } & FormInputHttpRequest | { "type": "accordion" } & FormInputAccordion | { "type": "banner" } & FormInputBanner | { "type": "markdown" } & FormInputMarkdown;
export type FormInputAccordion = { label: string, inputs?: Array<FormInput>, hidden?: boolean, };
export type FormInputBanner = { inputs?: Array<FormInput>, hidden?: boolean, color?: Color, };
export type FormInputBase = {
/**
* The name of the input. The value will be stored at this object attribute in the resulting data
*/
name: string,
/**
* Whether this input is visible for the given configuration. Use this to
* make branching forms.
*/
hidden?: boolean,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, disabled?: boolean, };
export type FormInputCheckbox = {
/**
* The name of the input. The value will be stored at this object attribute in the resulting data
*/
name: string,
/**
* Whether this input is visible for the given configuration. Use this to
* make branching forms.
*/
hidden?: boolean,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, disabled?: boolean, };
export type FormInputEditor = {
/**
* Placeholder for the text input
*/
placeholder?: string | null,
/**
* Don't show the editor gutter (line numbers, folds, etc.)
*/
hideGutter?: boolean,
/**
* Language for syntax highlighting
*/
language?: EditorLanguage, readOnly?: boolean, completionOptions?: Array<GenericCompletionOption>,
/**
* The name of the input. The value will be stored at this object attribute in the resulting data
*/
name: string,
/**
* Whether this input is visible for the given configuration. Use this to
* make branching forms.
*/
hidden?: boolean,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, disabled?: boolean, };
export type FormInputFile = {
/**
* The title of the file selection window
*/
title: string,
/**
* Allow selecting multiple files
*/
multiple?: boolean, directory?: boolean, defaultPath?: string, filters?: Array<FileFilter>,
/**
* The name of the input. The value will be stored at this object attribute in the resulting data
*/
name: string,
/**
* Whether this input is visible for the given configuration. Use this to
* make branching forms.
*/
hidden?: boolean,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, disabled?: boolean, };
export type FormInputHttpRequest = {
/**
* The name of the input. The value will be stored at this object attribute in the resulting data
*/
name: string,
/**
* Whether this input is visible for the given configuration. Use this to
* make branching forms.
*/
hidden?: boolean,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, disabled?: boolean, };
export type FormInputMarkdown = { content: string, hidden?: boolean, };
export type FormInputSelect = {
/**
* The options that will be available in the select input
*/
options: Array<FormInputSelectOption>,
/**
* The name of the input. The value will be stored at this object attribute in the resulting data
*/
name: string,
/**
* Whether this input is visible for the given configuration. Use this to
* make branching forms.
*/
hidden?: boolean,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, disabled?: boolean, };
export type FormInputSelectOption = { label: string, value: string, };
export type FormInputText = {
/**
* Placeholder for the text input
*/
placeholder?: string | null,
/**
* Placeholder for the text input
*/
password?: boolean,
/**
* Whether to allow newlines in the input, like a <textarea/>
*/
multiLine?: boolean, completionOptions?: Array<GenericCompletionOption>,
/**
* The name of the input. The value will be stored at this object attribute in the resulting data
*/
name: string,
/**
* Whether this input is visible for the given configuration. Use this to
* make branching forms.
*/
hidden?: boolean,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, disabled?: boolean, };
export type GenericCompletionOption = { label: string, detail?: string, info?: string, type?: CompletionOptionType, boost?: number, };
export type GetHttpAuthenticationConfigRequest = { contextId: string, values: { [key in string]?: JsonPrimitive }, };
export type GetHttpAuthenticationConfigResponse = { args: Array<FormInput>, pluginRefId: string, actions?: Array<HttpAuthenticationAction>, };
export type GetHttpAuthenticationSummaryResponse = { name: string, label: string, shortLabel: string, };
export type GetHttpRequestActionsRequest = Record<string, never>;
export type GetHttpRequestActionsResponse = { actions: Array<HttpRequestAction>, pluginRefId: string, };
export type GetHttpRequestByIdRequest = { id: string, };
export type GetHttpRequestByIdResponse = { httpRequest: HttpRequest | null, };
export type GetKeyValueRequest = { key: string, };
export type GetKeyValueResponse = { value?: string, };
export type GetTemplateFunctionsResponse = { functions: Array<TemplateFunction>, pluginRefId: string, };
export type HttpAuthenticationAction = { label: string, icon?: Icon, };
export type HttpHeader = { name: string, value: string, };
export type HttpRequestAction = { label: string, icon?: Icon, };
export type Icon = "alert_triangle" | "check" | "check_circle" | "chevron_down" | "copy" | "info" | "pin" | "search" | "trash" | "_unknown";
export type ImportRequest = { content: string, };
export type ImportResources = { workspaces: Array<Workspace>, environments: Array<Environment>, folders: Array<Folder>, httpRequests: Array<HttpRequest>, grpcRequests: Array<GrpcRequest>, };
export type ImportResponse = { resources: ImportResources, };
export type InternalEvent = { id: string, pluginRefId: string, pluginName: string, replyId: string | null, windowContext: WindowContext, payload: InternalEventPayload, };
export type InternalEventPayload = { "type": "boot_request" } & BootRequest | { "type": "boot_response" } & BootResponse | { "type": "reload_request" } & EmptyPayload | { "type": "reload_response" } & EmptyPayload | { "type": "terminate_request" } | { "type": "terminate_response" } | { "type": "import_request" } & ImportRequest | { "type": "import_response" } & ImportResponse | { "type": "filter_request" } & FilterRequest | { "type": "filter_response" } & FilterResponse | { "type": "export_http_request_request" } & ExportHttpRequestRequest | { "type": "export_http_request_response" } & ExportHttpRequestResponse | { "type": "send_http_request_request" } & SendHttpRequestRequest | { "type": "send_http_request_response" } & SendHttpRequestResponse | { "type": "get_http_request_actions_request" } & EmptyPayload | { "type": "get_http_request_actions_response" } & GetHttpRequestActionsResponse | { "type": "call_http_request_action_request" } & CallHttpRequestActionRequest | { "type": "get_template_functions_request" } | { "type": "get_template_functions_response" } & GetTemplateFunctionsResponse | { "type": "call_template_function_request" } & CallTemplateFunctionRequest | { "type": "call_template_function_response" } & CallTemplateFunctionResponse | { "type": "get_http_authentication_summary_request" } & EmptyPayload | { "type": "get_http_authentication_summary_response" } & GetHttpAuthenticationSummaryResponse | { "type": "get_http_authentication_config_request" } & GetHttpAuthenticationConfigRequest | { "type": "get_http_authentication_config_response" } & GetHttpAuthenticationConfigResponse | { "type": "call_http_authentication_request" } & CallHttpAuthenticationRequest | { "type": "call_http_authentication_response" } & CallHttpAuthenticationResponse | { "type": "call_http_authentication_action_request" } & CallHttpAuthenticationActionRequest | { "type": "call_http_authentication_action_response" } & EmptyPayload | { "type": "copy_text_request" } & CopyTextRequest | { "type": "copy_text_response" } & EmptyPayload | { "type": "render_http_request_request" } & RenderHttpRequestRequest | { "type": "render_http_request_response" } & RenderHttpRequestResponse | { "type": "get_key_value_request" } & GetKeyValueRequest | { "type": "get_key_value_response" } & GetKeyValueResponse | { "type": "set_key_value_request" } & SetKeyValueRequest | { "type": "set_key_value_response" } & SetKeyValueResponse | { "type": "delete_key_value_request" } & DeleteKeyValueRequest | { "type": "delete_key_value_response" } & DeleteKeyValueResponse | { "type": "open_window_request" } & OpenWindowRequest | { "type": "window_navigate_event" } & WindowNavigateEvent | { "type": "close_window_request" } & CloseWindowRequest | { "type": "template_render_request" } & TemplateRenderRequest | { "type": "template_render_response" } & TemplateRenderResponse | { "type": "show_toast_request" } & ShowToastRequest | { "type": "show_toast_response" } & EmptyPayload | { "type": "prompt_text_request" } & PromptTextRequest | { "type": "prompt_text_response" } & PromptTextResponse | { "type": "get_http_request_by_id_request" } & GetHttpRequestByIdRequest | { "type": "get_http_request_by_id_response" } & GetHttpRequestByIdResponse | { "type": "find_http_responses_request" } & FindHttpResponsesRequest | { "type": "find_http_responses_response" } & FindHttpResponsesResponse | { "type": "empty_response" } & EmptyPayload | { "type": "error_response" } & ErrorResponse;
export type JsonPrimitive = string | number | boolean | null;
export type OpenWindowRequest = { url: string,
/**
* Label for the window. If not provided, a random one will be generated.
*/
label: string, title?: string, size?: WindowSize, };
export type PromptTextRequest = { id: string, title: string, label: string, description?: string, defaultValue?: string, placeholder?: string,
/**
* Text to add to the confirmation button
*/
confirmText?: string,
/**
* Text to add to the cancel button
*/
cancelText?: string,
/**
* Require the user to enter a non-empty value
*/
required?: boolean, };
export type PromptTextResponse = { value: string | null, };
export type RenderHttpRequestRequest = { httpRequest: HttpRequest, purpose: RenderPurpose, };
export type RenderHttpRequestResponse = { httpRequest: HttpRequest, };
export type RenderPurpose = "send" | "preview";
export type SendHttpRequestRequest = { httpRequest: Partial<HttpRequest>, };
export type SendHttpRequestResponse = { httpResponse: HttpResponse, };
export type SetKeyValueRequest = { key: string, value: string, };
export type SetKeyValueResponse = {};
export type ShowToastRequest = { message: string, color?: Color, icon?: Icon, };
export type TemplateFunction = { name: string, description?: string,
/**
* Also support alternative names. This is useful for not breaking existing
* tags when changing the `name` property
*/
aliases?: Array<string>, args: Array<FormInput>, };
export type TemplateRenderRequest = { data: JsonValue, purpose: RenderPurpose, };
export type TemplateRenderResponse = { data: JsonValue, };
export type WindowContext = { "type": "none" } | { "type": "label", label: string, };
export type WindowNavigateEvent = { url: string, };
export type WindowSize = { width: number, height: number, };
@@ -1 +1,2 @@
export type AtLeast<T, K extends keyof T> = Partial<T> & Pick<T, K>; export type AtLeast<T, K extends keyof T> = Partial<T> & Pick<T, K>;
export type MaybePromise<T> = Promise<T> | T;
+2 -2
View File
@@ -1,5 +1,5 @@
export type * from './plugins'; export type * from './plugins';
export type * from './themes'; export type * from './themes';
export * from './bindings/models'; export * from './bindings/gen_models';
export * from './bindings/events'; export * from './bindings/gen_events';
@@ -1,13 +1,29 @@
import { import {
CallHttpAuthenticationActionArgs,
CallHttpAuthenticationRequest, CallHttpAuthenticationRequest,
CallHttpAuthenticationResponse, CallHttpAuthenticationResponse,
GetHttpAuthenticationResponse, FormInput,
} from '../bindings/events'; GetHttpAuthenticationConfigRequest,
GetHttpAuthenticationSummaryResponse,
HttpAuthenticationAction,
} from '../bindings/gen_events';
import { MaybePromise } from '../helpers';
import { Context } from './Context'; import { Context } from './Context';
export type AuthenticationPlugin = Omit<GetHttpAuthenticationResponse, 'pluginName'> & { type DynamicFormInput = FormInput & {
dynamic(
ctx: Context,
args: GetHttpAuthenticationConfigRequest,
): MaybePromise<Partial<FormInput> | undefined | null>;
};
export type AuthenticationPlugin = GetHttpAuthenticationSummaryResponse & {
args: (FormInput | DynamicFormInput)[];
onApply( onApply(
ctx: Context, ctx: Context,
args: CallHttpAuthenticationRequest, args: CallHttpAuthenticationRequest,
): Promise<CallHttpAuthenticationResponse> | CallHttpAuthenticationResponse; ): MaybePromise<CallHttpAuthenticationResponse>;
actions?: (HttpAuthenticationAction & {
onSelect(ctx: Context, args: CallHttpAuthenticationActionArgs): Promise<void> | void;
})[];
}; };
@@ -3,6 +3,7 @@ import type {
FindHttpResponsesResponse, FindHttpResponsesResponse,
GetHttpRequestByIdRequest, GetHttpRequestByIdRequest,
GetHttpRequestByIdResponse, GetHttpRequestByIdResponse,
OpenWindowRequest,
PromptTextRequest, PromptTextRequest,
PromptTextResponse, PromptTextResponse,
RenderHttpRequestRequest, RenderHttpRequestRequest,
@@ -12,7 +13,7 @@ import type {
ShowToastRequest, ShowToastRequest,
TemplateRenderRequest, TemplateRenderRequest,
TemplateRenderResponse, TemplateRenderResponse,
} from "../bindings/events.ts"; } from '../bindings/gen_events.ts';
export interface Context { export interface Context {
clipboard: { clipboard: {
@@ -22,27 +23,27 @@ export interface Context {
show(args: ShowToastRequest): Promise<void>; show(args: ShowToastRequest): Promise<void>;
}; };
prompt: { prompt: {
text(args: PromptTextRequest): Promise<PromptTextResponse["value"]>; text(args: PromptTextRequest): Promise<PromptTextResponse['value']>;
};
store: {
set<T>(key: string, value: T): Promise<void>;
get<T>(key: string): Promise<T | undefined>;
delete(key: string): Promise<boolean>;
};
window: {
openUrl(
args: OpenWindowRequest & { onNavigate?: (args: { url: string }) => void },
): Promise<{ close: () => void }>;
}; };
httpRequest: { httpRequest: {
send( send(args: SendHttpRequestRequest): Promise<SendHttpRequestResponse['httpResponse']>;
args: SendHttpRequestRequest, getById(args: GetHttpRequestByIdRequest): Promise<GetHttpRequestByIdResponse['httpRequest']>;
): Promise<SendHttpRequestResponse["httpResponse"]>; render(args: RenderHttpRequestRequest): Promise<RenderHttpRequestResponse['httpRequest']>;
getById(
args: GetHttpRequestByIdRequest,
): Promise<GetHttpRequestByIdResponse["httpRequest"]>;
render(
args: RenderHttpRequestRequest,
): Promise<RenderHttpRequestResponse["httpRequest"]>;
}; };
httpResponse: { httpResponse: {
find( find(args: FindHttpResponsesRequest): Promise<FindHttpResponsesResponse['httpResponses']>;
args: FindHttpResponsesRequest,
): Promise<FindHttpResponsesResponse["httpResponses"]>;
}; };
templates: { templates: {
render( render(args: TemplateRenderRequest): Promise<TemplateRenderResponse['data']>;
args: TemplateRenderRequest,
): Promise<TemplateRenderResponse["data"]>;
}; };
} }
@@ -1,4 +1,4 @@
import type { CallHttpRequestActionArgs, HttpRequestAction } from '../bindings/events'; import type { CallHttpRequestActionArgs, HttpRequestAction } from '../bindings/gen_events';
import type { Context } from './Context'; import type { Context } from './Context';
export type HttpRequestActionPlugin = HttpRequestAction & { export type HttpRequestActionPlugin = HttpRequestAction & {
@@ -1,29 +1,14 @@
import { import { Environment, Folder, GrpcRequest, HttpRequest, Workspace } from '../bindings/gen_models';
Environment, import type { AtLeast } from '../helpers';
Folder, import type { Context } from './Context';
GrpcRequest,
HttpRequest,
Workspace,
} from "../bindings/models";
import type { AtLeast } from "../helpers";
import type { Context } from "./Context";
type ImportPluginResponse = null | { type ImportPluginResponse = null | {
resources: { resources: {
workspaces: AtLeast<Workspace, "name" | "id" | "model">[]; workspaces: AtLeast<Workspace, 'name' | 'id' | 'model'>[];
environments: AtLeast< environments: AtLeast<Environment, 'name' | 'id' | 'model' | 'workspaceId'>[];
Environment, folders: AtLeast<Folder, 'name' | 'id' | 'model' | 'workspaceId'>[];
"name" | "id" | "model" | "workspaceId" httpRequests: AtLeast<HttpRequest, 'name' | 'id' | 'model' | 'workspaceId'>[];
>[]; grpcRequests: AtLeast<GrpcRequest, 'name' | 'id' | 'model' | 'workspaceId'>[];
folders: AtLeast<Folder, "name" | "id" | "model" | "workspaceId">[];
httpRequests: AtLeast<
HttpRequest,
"name" | "id" | "model" | "workspaceId"
>[];
grpcRequests: AtLeast<
GrpcRequest,
"name" | "id" | "model" | "workspaceId"
>[];
}; };
}; };
@@ -1,7 +1,7 @@
import { import {
CallTemplateFunctionArgs, CallTemplateFunctionArgs,
TemplateFunction, TemplateFunction,
} from "../bindings/events"; } from "../bindings/gen_events";
import { Context } from "./Context"; import { Context } from "./Context";
export type TemplateFunctionPlugin = TemplateFunction & { export type TemplateFunctionPlugin = TemplateFunction & {
+166 -22
View File
@@ -1,11 +1,18 @@
// OAuth 2.0 spec -> https://datatracker.ietf.org/doc/html/rfc6749
import type { import type {
BootRequest, BootRequest,
Context, Context,
DeleteKeyValueResponse,
FindHttpResponsesResponse, FindHttpResponsesResponse,
FormInput,
GetHttpRequestByIdResponse, GetHttpRequestByIdResponse,
GetKeyValueResponse,
HttpAuthenticationAction,
HttpRequestAction, HttpRequestAction,
InternalEvent, InternalEvent,
InternalEventPayload, InternalEventPayload,
JsonPrimitive,
PluginDefinition, PluginDefinition,
PromptTextResponse, PromptTextResponse,
RenderHttpRequestResponse, RenderHttpRequestResponse,
@@ -19,8 +26,16 @@ import type { Stats } from 'node:fs';
import { readFileSync, statSync, watch } from 'node:fs'; import { readFileSync, statSync, watch } from 'node:fs';
import path from 'node:path'; import path from 'node:path';
import * as util from 'node:util'; import * as util from 'node:util';
import { parentPort as nullableParentPort, workerData } from 'node:worker_threads';
import Promise from '../../../../../Library/Caches/deno/npm/registry.npmjs.org/any-promise/1.3.0';
import { interceptStdout } from './interceptStdout'; import { interceptStdout } from './interceptStdout';
import { parentPort, workerData } from 'node:worker_threads'; import { migrateHttpRequestActionKey, migrateTemplateFunctionSelectOptions } from './migrations';
if (nullableParentPort == null) {
throw new Error('Worker does not have access to parentPort');
}
const parentPort = nullableParentPort;
export interface PluginWorkerData { export interface PluginWorkerData {
bootRequest: BootRequest; bootRequest: BootRequest;
@@ -73,7 +88,7 @@ function initialize(workerData: PluginWorkerData) {
if (event.payload.type !== 'empty_response') { if (event.payload.type !== 'empty_response') {
console.log('Sending event to app', event.id, event.payload.type); console.log('Sending event to app', event.id, event.payload.type);
} }
parentPort!.postMessage(event); parentPort.postMessage(event);
} }
function sendAndWaitForReply<T extends Omit<InternalEventPayload, 'type'>>( function sendAndWaitForReply<T extends Omit<InternalEventPayload, 'type'>>(
@@ -84,14 +99,15 @@ function initialize(workerData: PluginWorkerData) {
const eventToSend = buildEventToSend(windowContext, payload, null); const eventToSend = buildEventToSend(windowContext, payload, null);
// 2. Spawn listener in background // 2. Spawn listener in background
const promise = new Promise<InternalEventPayload>((resolve) => { const promise = new Promise<T>((resolve) => {
const cb = (event: InternalEvent) => { const cb = (event: InternalEvent) => {
if (event.replyId === eventToSend.id) { if (event.replyId === eventToSend.id) {
parentPort!.off('message', cb); // Unlisten, now that we're done parentPort.off('message', cb); // Unlisten, now that we're done
resolve(event.payload); // Not type-safe but oh well const { type: _, ...payload } = event.payload;
resolve(payload as T);
} }
}; };
parentPort!.on('message', cb); parentPort.on('message', cb);
}); });
// 3. Send the event after we start listening (to prevent race) // 3. Send the event after we start listening (to prevent race)
@@ -101,10 +117,29 @@ function initialize(workerData: PluginWorkerData) {
return promise as unknown as Promise<T>; return promise as unknown as Promise<T>;
} }
function sendAndListenForEvents(
windowContext: WindowContext,
payload: InternalEventPayload,
onEvent: (event: InternalEventPayload) => void,
): void {
// 1. Build event to send
const eventToSend = buildEventToSend(windowContext, payload, null);
// 2. Listen for replies in the background
parentPort.on('message', (event: InternalEvent) => {
if (event.replyId === eventToSend.id) {
onEvent(event.payload);
}
});
// 3. Send the event after we start listening (to prevent race)
sendEvent(eventToSend);
}
// Reload plugin if the JS or package.json changes // Reload plugin if the JS or package.json changes
const windowContextNone: WindowContext = { type: 'none' }; const windowContextNone: WindowContext = { type: 'none' };
const fileChangeCallback = async () => { const fileChangeCallback = async () => {
await importModule(); importModule();
return sendPayload(windowContextNone, { type: 'reload_response' }, null); return sendPayload(windowContextNone, { type: 'reload_response' }, null);
}; };
@@ -130,6 +165,27 @@ function initialize(workerData: PluginWorkerData) {
}); });
}, },
}, },
window: {
async openUrl({ onNavigate, ...args }) {
args.label = args.label || `${Math.random()}`;
const payload: InternalEventPayload = { type: 'open_window_request', ...args };
const onEvent = (event: InternalEventPayload) => {
if (event.type === 'window_navigate_event') {
onNavigate?.(event);
}
};
sendAndListenForEvents(event.windowContext, payload, onEvent);
return {
close: () => {
const closePayload: InternalEventPayload = {
type: 'close_window_request',
label: args.label,
};
sendPayload(event.windowContext, closePayload, null);
},
};
},
},
prompt: { prompt: {
async text(args) { async text(args) {
const reply: PromptTextResponse = await sendAndWaitForReply(event.windowContext, { const reply: PromptTextResponse = await sendAndWaitForReply(event.windowContext, {
@@ -201,6 +257,30 @@ function initialize(workerData: PluginWorkerData) {
return result.data; return result.data;
}, },
}, },
store: {
async get<T>(key: string) {
const payload = { type: 'get_key_value_request', key } as const;
const result = await sendAndWaitForReply<GetKeyValueResponse>(event.windowContext, payload);
return result.value ? (JSON.parse(result.value) as T) : undefined;
},
async set<T>(key: string, value: T) {
const valueStr = JSON.stringify(value);
const payload: InternalEventPayload = {
type: 'set_key_value_request',
key,
value: valueStr,
};
await sendAndWaitForReply<GetKeyValueResponse>(event.windowContext, payload);
},
async delete(key: string) {
const payload = { type: 'delete_key_value_request', key } as const;
const result = await sendAndWaitForReply<DeleteKeyValueResponse>(
event.windowContext,
payload,
);
return result.deleted;
},
},
}); });
let plug: PluginDefinition | null = null; let plug: PluginDefinition | null = null;
@@ -210,10 +290,11 @@ function initialize(workerData: PluginWorkerData) {
delete require.cache[id]; delete require.cache[id];
plug = require(id).plugin; plug = require(id).plugin;
} }
importModule(); importModule();
// Message comes into the plugin to be processed // Message comes into the plugin to be processed
parentPort!.on('message', async (event: InternalEvent) => { parentPort.on('message', async (event: InternalEvent) => {
const ctx = newCtx(event); const ctx = newCtx(event);
const { windowContext, payload, id: replyId } = event; const { windowContext, payload, id: replyId } = event;
try { try {
@@ -272,7 +353,7 @@ function initialize(workerData: PluginWorkerData) {
Array.isArray(plug?.httpRequestActions) Array.isArray(plug?.httpRequestActions)
) { ) {
const reply: HttpRequestAction[] = plug.httpRequestActions.map((a) => ({ const reply: HttpRequestAction[] = plug.httpRequestActions.map((a) => ({
...a, ...migrateHttpRequestActionKey(a),
// Add everything except onSelect // Add everything except onSelect
onSelect: undefined, onSelect: undefined,
})); }));
@@ -289,11 +370,13 @@ function initialize(workerData: PluginWorkerData) {
payload.type === 'get_template_functions_request' && payload.type === 'get_template_functions_request' &&
Array.isArray(plug?.templateFunctions) Array.isArray(plug?.templateFunctions)
) { ) {
const reply: TemplateFunction[] = plug.templateFunctions.map((a) => ({ const reply: TemplateFunction[] = plug.templateFunctions.map((templateFunction) => {
...a, return {
// Add everything except render ...migrateTemplateFunctionSelectOptions(templateFunction),
onRender: undefined, // Add everything except render
})); onRender: undefined,
};
});
const replyPayload: InternalEventPayload = { const replyPayload: InternalEventPayload = {
type: 'get_template_functions_response', type: 'get_template_functions_response',
pluginRefId, pluginRefId,
@@ -303,11 +386,42 @@ function initialize(workerData: PluginWorkerData) {
return; return;
} }
if (payload.type === 'get_http_authentication_request' && plug?.authentication) { if (payload.type === 'get_http_authentication_summary_request' && plug?.authentication) {
const { onApply: _, ...auth } = plug.authentication; const { name, shortLabel, label } = plug.authentication;
const replyPayload: InternalEventPayload = { const replyPayload: InternalEventPayload = {
...auth, type: 'get_http_authentication_summary_response',
type: 'get_http_authentication_response', name,
label,
shortLabel,
};
sendPayload(windowContext, replyPayload, replyId);
return;
}
if (payload.type === 'get_http_authentication_config_request' && plug?.authentication) {
const { args, actions } = plug.authentication;
const resolvedArgs: FormInput[] = [];
for (let i = 0; i < args.length; i++) {
let v = args[i];
if ('dynamic' in v) {
const dynamicAttrs = await v.dynamic(ctx, payload);
const { dynamic, ...other } = v;
resolvedArgs.push({ ...other, ...dynamicAttrs } as FormInput);
} else {
resolvedArgs.push(v);
}
}
const resolvedActions: HttpAuthenticationAction[] = [];
for (const { onSelect, ...action } of actions ?? []) {
resolvedActions.push(action);
}
const replyPayload: InternalEventPayload = {
type: 'get_http_authentication_config_response',
args: resolvedArgs,
actions: resolvedActions,
pluginRefId,
}; };
sendPayload(windowContext, replyPayload, replyId); sendPayload(windowContext, replyPayload, replyId);
@@ -317,12 +431,13 @@ function initialize(workerData: PluginWorkerData) {
if (payload.type === 'call_http_authentication_request' && plug?.authentication) { if (payload.type === 'call_http_authentication_request' && plug?.authentication) {
const auth = plug.authentication; const auth = plug.authentication;
if (typeof auth?.onApply === 'function') { if (typeof auth?.onApply === 'function') {
applyFormInputDefaults(auth.args, payload.values);
const result = await auth.onApply(ctx, payload); const result = await auth.onApply(ctx, payload);
sendPayload( sendPayload(
windowContext, windowContext,
{ {
...result,
type: 'call_http_authentication_response', type: 'call_http_authentication_response',
setHeaders: result.setHeaders,
}, },
replyId, replyId,
); );
@@ -330,11 +445,25 @@ function initialize(workerData: PluginWorkerData) {
} }
} }
if (
payload.type === 'call_http_authentication_action_request' &&
plug?.authentication != null
) {
const action = plug.authentication.actions?.find((a) => a.name === payload.name);
if (typeof action?.onSelect === 'function') {
await action.onSelect(ctx, payload.args);
sendEmpty(windowContext, replyId);
return;
}
}
if ( if (
payload.type === 'call_http_request_action_request' && payload.type === 'call_http_request_action_request' &&
Array.isArray(plug?.httpRequestActions) Array.isArray(plug?.httpRequestActions)
) { ) {
const action = plug.httpRequestActions.find((a) => a.key === payload.key); const action = plug.httpRequestActions.find(
(a) => migrateHttpRequestActionKey(a).name === payload.name,
);
if (typeof action?.onSelect === 'function') { if (typeof action?.onSelect === 'function') {
await action.onSelect(ctx, payload.args); await action.onSelect(ctx, payload.args);
sendEmpty(windowContext, replyId); sendEmpty(windowContext, replyId);
@@ -348,6 +477,7 @@ function initialize(workerData: PluginWorkerData) {
) { ) {
const action = plug.templateFunctions.find((a) => a.name === payload.name); const action = plug.templateFunctions.find((a) => a.name === payload.name);
if (typeof action?.onRender === 'function') { if (typeof action?.onRender === 'function') {
applyFormInputDefaults(action.args, payload.args.values);
const result = await action.onRender(ctx, payload.args); const result = await action.onRender(ctx, payload.args);
sendPayload( sendPayload(
windowContext, windowContext,
@@ -362,7 +492,7 @@ function initialize(workerData: PluginWorkerData) {
} }
if (payload.type === 'reload_request') { if (payload.type === 'reload_request') {
await importModule(); importModule();
} }
} catch (err) { } catch (err) {
console.log('Plugin call threw exception', payload.type, err); console.log('Plugin call threw exception', payload.type, err);
@@ -374,7 +504,7 @@ function initialize(workerData: PluginWorkerData) {
}, },
replyId, replyId,
); );
// TODO: Return errors to server return;
} }
// No matches, so send back an empty response so the caller doesn't block forever // No matches, so send back an empty response so the caller doesn't block forever
@@ -425,3 +555,17 @@ function watchFile(filepath: string, cb: (filepath: string) => void) {
watchedFiles[filepath] = stat; watchedFiles[filepath] = stat;
}); });
} }
/** Recursively apply form input defaults to a set of values */
function applyFormInputDefaults(
inputs: FormInput[],
values: { [p: string]: JsonPrimitive | undefined },
) {
for (const input of inputs) {
if ('inputs' in input) {
applyFormInputDefaults(input.inputs ?? [], values);
} else if ('defaultValue' in input && values[input.name] === undefined) {
values[input.name] = input.defaultValue;
}
}
}
+25
View File
@@ -0,0 +1,25 @@
import {HttpRequestAction, TemplateFunction} from '@yaakapp/api';
export function migrateTemplateFunctionSelectOptions(f: TemplateFunction): TemplateFunction {
const migratedArgs = f.args.map((a) => {
if (a.type === 'select') {
a.options = a.options.map((o) => ({
...o,
label: o.label || (o as any).name,
}));
}
return a;
});
return {
...f,
args: migratedArgs,
};
}
export function migrateHttpRequestActionKey(a: HttpRequestAction): HttpRequestAction {
return {
...a,
name: a.name || (a as any).key,
}
}
+30 -31
View File
@@ -712,7 +712,7 @@ dependencies = [
"semver", "semver",
"serde", "serde",
"serde_json", "serde_json",
"thiserror 2.0.7", "thiserror 2.0.11",
] ]
[[package]] [[package]]
@@ -5212,8 +5212,6 @@ dependencies = [
"serde_json", "serde_json",
"sha2", "sha2",
"sqlx-core", "sqlx-core",
"sqlx-mysql",
"sqlx-postgres",
"sqlx-sqlite", "sqlx-sqlite",
"syn 2.0.87", "syn 2.0.87",
"tempfile", "tempfile",
@@ -5252,7 +5250,6 @@ dependencies = [
"percent-encoding", "percent-encoding",
"rand 0.8.5", "rand 0.8.5",
"rsa", "rsa",
"serde",
"sha1", "sha1",
"sha2", "sha2",
"smallvec", "smallvec",
@@ -5556,9 +5553,9 @@ checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
[[package]] [[package]]
name = "tauri" name = "tauri"
version = "2.2.0" version = "2.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e2e3349fbb2be7af9fad1b43d61ac83ba55ab48d47fbe1b2732f0c8211610a9" checksum = "78f6efc261c7905839b4914889a5b25df07f0ff89c63fb4afd6ff8c96af15e4d"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bytes", "bytes",
@@ -5594,7 +5591,7 @@ dependencies = [
"tauri-runtime", "tauri-runtime",
"tauri-runtime-wry", "tauri-runtime-wry",
"tauri-utils", "tauri-utils",
"thiserror 2.0.7", "thiserror 2.0.11",
"tokio", "tokio",
"tray-icon", "tray-icon",
"url", "url",
@@ -5647,7 +5644,7 @@ dependencies = [
"sha2", "sha2",
"syn 2.0.87", "syn 2.0.87",
"tauri-utils", "tauri-utils",
"thiserror 2.0.7", "thiserror 2.0.11",
"time", "time",
"url", "url",
"uuid", "uuid",
@@ -5697,7 +5694,7 @@ dependencies = [
"serde_json", "serde_json",
"tauri", "tauri",
"tauri-plugin", "tauri-plugin",
"thiserror 2.0.7", "thiserror 2.0.11",
] ]
[[package]] [[package]]
@@ -5714,7 +5711,7 @@ dependencies = [
"tauri", "tauri",
"tauri-plugin", "tauri-plugin",
"tauri-plugin-fs", "tauri-plugin-fs",
"thiserror 2.0.7", "thiserror 2.0.11",
"url", "url",
] ]
@@ -5735,7 +5732,7 @@ dependencies = [
"tauri", "tauri",
"tauri-plugin", "tauri-plugin",
"tauri-utils", "tauri-utils",
"thiserror 2.0.7", "thiserror 2.0.11",
"toml 0.8.19", "toml 0.8.19",
"url", "url",
"uuid", "uuid",
@@ -5759,7 +5756,7 @@ dependencies = [
"swift-rs", "swift-rs",
"tauri", "tauri",
"tauri-plugin", "tauri-plugin",
"thiserror 2.0.7", "thiserror 2.0.11",
"time", "time",
] ]
@@ -5779,7 +5776,7 @@ dependencies = [
"serde_json", "serde_json",
"tauri", "tauri",
"tauri-plugin", "tauri-plugin",
"thiserror 2.0.7", "thiserror 2.0.11",
"url", "url",
"windows", "windows",
"zbus 5.3.0", "zbus 5.3.0",
@@ -5800,7 +5797,7 @@ dependencies = [
"sys-locale", "sys-locale",
"tauri", "tauri",
"tauri-plugin", "tauri-plugin",
"thiserror 2.0.7", "thiserror 2.0.11",
] ]
[[package]] [[package]]
@@ -5820,7 +5817,7 @@ dependencies = [
"shared_child", "shared_child",
"tauri", "tauri",
"tauri-plugin", "tauri-plugin",
"thiserror 2.0.7", "thiserror 2.0.11",
"tokio", "tokio",
] ]
@@ -5833,7 +5830,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"tauri", "tauri",
"thiserror 2.0.7", "thiserror 2.0.11",
"tracing", "tracing",
"windows-sys 0.59.0", "windows-sys 0.59.0",
"zbus 5.3.0", "zbus 5.3.0",
@@ -5861,7 +5858,7 @@ dependencies = [
"tauri", "tauri",
"tauri-plugin", "tauri-plugin",
"tempfile", "tempfile",
"thiserror 2.0.7", "thiserror 2.0.11",
"time", "time",
"tokio", "tokio",
"url", "url",
@@ -5881,7 +5878,7 @@ dependencies = [
"serde_json", "serde_json",
"tauri", "tauri",
"tauri-plugin", "tauri-plugin",
"thiserror 2.0.7", "thiserror 2.0.11",
] ]
[[package]] [[package]]
@@ -5898,7 +5895,7 @@ dependencies = [
"serde", "serde",
"serde_json", "serde_json",
"tauri-utils", "tauri-utils",
"thiserror 2.0.7", "thiserror 2.0.11",
"url", "url",
"windows", "windows",
] ]
@@ -5958,7 +5955,7 @@ dependencies = [
"serde_json", "serde_json",
"serde_with", "serde_with",
"swift-rs", "swift-rs",
"thiserror 2.0.7", "thiserror 2.0.11",
"toml 0.8.19", "toml 0.8.19",
"url", "url",
"urlpattern", "urlpattern",
@@ -6026,11 +6023,11 @@ dependencies = [
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "2.0.7" version = "2.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93605438cbd668185516ab499d589afb7ee1859ea3d5fc8f6b0755e1c7443767" checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc"
dependencies = [ dependencies = [
"thiserror-impl 2.0.7", "thiserror-impl 2.0.11",
] ]
[[package]] [[package]]
@@ -6046,9 +6043,9 @@ dependencies = [
[[package]] [[package]]
name = "thiserror-impl" name = "thiserror-impl"
version = "2.0.7" version = "2.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1d8749b4531af2117677a5fcd12b1348a3fe2b81e36e61ffeac5c4aa3273e36" checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -6458,7 +6455,7 @@ dependencies = [
"log", "log",
"rand 0.8.5", "rand 0.8.5",
"sha1", "sha1",
"thiserror 2.0.7", "thiserror 2.0.11",
"utf-8", "utf-8",
] ]
@@ -7406,7 +7403,7 @@ dependencies = [
"sha2", "sha2",
"soup3", "soup3",
"tao-macros", "tao-macros",
"thiserror 2.0.7", "thiserror 2.0.11",
"url", "url",
"webkit2gtk", "webkit2gtk",
"webkit2gtk-sys", "webkit2gtk-sys",
@@ -7497,6 +7494,7 @@ dependencies = [
"hex_color", "hex_color",
"http", "http",
"log", "log",
"md5",
"mime_guess", "mime_guess",
"objc", "objc",
"openssl-sys", "openssl-sys",
@@ -7571,7 +7569,7 @@ dependencies = [
"serde_json", "serde_json",
"tauri", "tauri",
"tauri-plugin", "tauri-plugin",
"thiserror 2.0.7", "thiserror 2.0.11",
"ts-rs", "ts-rs",
"yaak-models", "yaak-models",
] ]
@@ -7592,7 +7590,7 @@ dependencies = [
"serde_json", "serde_json",
"sqlx", "sqlx",
"tauri", "tauri",
"thiserror 1.0.63", "thiserror 2.0.11",
"ts-rs", "ts-rs",
] ]
@@ -7603,6 +7601,7 @@ dependencies = [
"dunce", "dunce",
"futures-util", "futures-util",
"log", "log",
"md5",
"path-slash", "path-slash",
"rand 0.8.5", "rand 0.8.5",
"regex", "regex",
@@ -7610,7 +7609,7 @@ dependencies = [
"serde_json", "serde_json",
"tauri", "tauri",
"tauri-plugin-shell", "tauri-plugin-shell",
"thiserror 2.0.7", "thiserror 2.0.11",
"tokio", "tokio",
"tokio-tungstenite", "tokio-tungstenite",
"ts-rs", "ts-rs",
@@ -7639,7 +7638,7 @@ dependencies = [
"sha1", "sha1",
"tauri", "tauri",
"tauri-plugin", "tauri-plugin",
"thiserror 2.0.7", "thiserror 2.0.11",
"tokio", "tokio",
"ts-rs", "ts-rs",
"yaak-models", "yaak-models",
+2 -1
View File
@@ -44,6 +44,7 @@ eventsource-client = { git = "https://github.com/yaakapp/rust-eventsource-client
hex_color = "3.0.0" hex_color = "3.0.0"
http = { version = "1.2.0", default-features = false } http = { version = "1.2.0", default-features = false }
log = "0.4.21" log = "0.4.21"
md5 = "0.7.0"
rand = "0.8.5" rand = "0.8.5"
regex = "1.10.2" regex = "1.10.2"
reqwest = { workspace = true, features = ["multipart", "cookies", "gzip", "brotli", "deflate", "json", "rustls-tls-manual-roots-no-provider"] } reqwest = { workspace = true, features = ["multipart", "cookies", "gzip", "brotli", "deflate", "json", "rustls-tls-manual-roots-no-provider"] }
@@ -85,7 +86,7 @@ yaak-plugins = { path = "yaak-plugins" }
serde = "1.0.215" serde = "1.0.215"
serde_json = "1.0.132" serde_json = "1.0.132"
tauri-plugin-shell = "2.2.0" tauri-plugin-shell = "2.2.0"
tauri = "2.2.0" tauri = "2.2.3"
thiserror = "2.0.3" thiserror = "2.0.3"
ts-rs = "10.0.0" ts-rs = "10.0.0"
reqwest = "0.12.12" reqwest = "0.12.12"
+7 -6
View File
@@ -30,10 +30,8 @@
} }
] ]
}, },
"opener:allow-open-url", "clipboard-manager:allow-read-text",
"opener:allow-open-path", "clipboard-manager:allow-write-text",
"opener:allow-default-urls",
"opener:allow-reveal-item-in-dir",
"core:webview:allow-set-webview-zoom", "core:webview:allow-set-webview-zoom",
"core:window:allow-close", "core:window:allow-close",
"core:window:allow-internal-toggle-maximize", "core:window:allow-internal-toggle-maximize",
@@ -47,8 +45,11 @@
"core:window:allow-start-dragging", "core:window:allow-start-dragging",
"core:window:allow-theme", "core:window:allow-theme",
"core:window:allow-unmaximize", "core:window:allow-unmaximize",
"clipboard-manager:allow-read-text", "opener:allow-default-urls",
"clipboard-manager:allow-write-text", "opener:allow-open-path",
"opener:allow-open-url",
"opener:allow-reveal-item-in-dir",
"shell:allow-open",
"yaak-license:default", "yaak-license:default",
"yaak-sync:default" "yaak-sync:default"
] ]
+1 -1
View File
@@ -1 +1 @@
{"main":{"identifier":"main","description":"Main permissions","local":true,"windows":["*"],"permissions":["core:event:allow-emit","core:event:allow-listen","core:event:allow-unlisten","os:allow-os-type","clipboard-manager:allow-clear","clipboard-manager:allow-write-text","clipboard-manager:allow-read-text","dialog:allow-open","dialog:allow-save","fs:allow-read-dir","fs:allow-read-file","fs:allow-read-text-file",{"identifier":"fs:scope","allow":[{"path":"$APPDATA"},{"path":"$APPDATA/**"}]},"opener:allow-open-url","opener:allow-open-path","opener:allow-default-urls","opener:allow-reveal-item-in-dir","core:webview:allow-set-webview-zoom","core:window:allow-close","core:window:allow-internal-toggle-maximize","core:window:allow-is-fullscreen","core:window:allow-is-maximized","core:window:allow-maximize","core:window:allow-minimize","core:window:allow-set-decorations","core:window:allow-set-title","core:window:allow-show","core:window:allow-start-dragging","core:window:allow-theme","core:window:allow-unmaximize","clipboard-manager:allow-read-text","clipboard-manager:allow-write-text","yaak-license:default","yaak-sync:default"]}} {"main":{"identifier":"main","description":"Main permissions","local":true,"windows":["*"],"permissions":["core:event:allow-emit","core:event:allow-listen","core:event:allow-unlisten","os:allow-os-type","clipboard-manager:allow-clear","clipboard-manager:allow-write-text","clipboard-manager:allow-read-text","dialog:allow-open","dialog:allow-save","fs:allow-read-dir","fs:allow-read-file","fs:allow-read-text-file",{"identifier":"fs:scope","allow":[{"path":"$APPDATA"},{"path":"$APPDATA/**"}]},"clipboard-manager:allow-read-text","clipboard-manager:allow-write-text","core:webview:allow-set-webview-zoom","core:window:allow-close","core:window:allow-internal-toggle-maximize","core:window:allow-is-fullscreen","core:window:allow-is-maximized","core:window:allow-maximize","core:window:allow-minimize","core:window:allow-set-decorations","core:window:allow-set-title","core:window:allow-show","core:window:allow-start-dragging","core:window:allow-theme","core:window:allow-unmaximize","opener:allow-default-urls","opener:allow-open-path","opener:allow-open-url","opener:allow-reveal-item-in-dir","shell:allow-open","yaak-license:default","yaak-sync:default"]}}
@@ -0,0 +1,11 @@
CREATE TABLE plugin_key_values
(
model TEXT DEFAULT 'plugin_key_value' NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL,
deleted_at DATETIME,
plugin_name TEXT NOT NULL,
key TEXT NOT NULL,
value TEXT NOT NULL,
PRIMARY KEY (plugin_name, key)
);
+10 -12
View File
@@ -70,7 +70,7 @@ pub async fn send_http_request<R: Runtime>(
if !url_string.starts_with("http://") && !url_string.starts_with("https://") { if !url_string.starts_with("http://") && !url_string.starts_with("https://") {
url_string = format!("http://{}", url_string); url_string = format!("http://{}", url_string);
} }
debug!("Sending request to {url_string}"); debug!("Sending request to {} {url_string}", request.method);
let mut client_builder = reqwest::Client::builder() let mut client_builder = reqwest::Client::builder()
.redirect(match workspace.setting_follow_redirects { .redirect(match workspace.setting_follow_redirects {
@@ -262,7 +262,7 @@ pub async fn send_http_request<R: Runtime>(
None => {} None => {}
Some(a) => { Some(a) => {
for p in a { for p in a {
let enabled = get_bool(p, "enabled"); let enabled = get_bool(p, "enabled", true);
let name = get_str(p, "name"); let name = get_str(p, "name");
if !enabled || name.is_empty() { if !enabled || name.is_empty() {
continue; continue;
@@ -296,7 +296,7 @@ pub async fn send_http_request<R: Runtime>(
None => {} None => {}
Some(fd) => { Some(fd) => {
for p in fd { for p in fd {
let enabled = get_bool(p, "enabled"); let enabled = get_bool(p, "enabled", true);
let name = get_str(p, "name").to_string(); let name = get_str(p, "name").to_string();
if !enabled || name.is_empty() { if !enabled || name.is_empty() {
@@ -376,13 +376,11 @@ pub async fn send_http_request<R: Runtime>(
if let Some(auth_name) = request.authentication_type.to_owned() { if let Some(auth_name) = request.authentication_type.to_owned() {
let req = CallHttpAuthenticationRequest { let req = CallHttpAuthenticationRequest {
config: serde_json::to_value(&request.authentication) context_id: format!("{:x}", md5::compute(request.id)),
.unwrap() values: serde_json::from_value(serde_json::to_value(&request.authentication).unwrap())
.as_object() .unwrap(),
.unwrap()
.to_owned(),
method: sendable_req.method().to_string(),
url: sendable_req.url().to_string(), url: sendable_req.url().to_string(),
method: sendable_req.method().to_string(),
headers: sendable_req headers: sendable_req
.headers() .headers()
.iter() .iter()
@@ -604,10 +602,10 @@ fn ensure_proto(url_str: &str) -> String {
format!("http://{url_str}") format!("http://{url_str}")
} }
fn get_bool(v: &Value, key: &str) -> bool { fn get_bool(v: &Value, key: &str, fallback: bool) -> bool {
match v.get(key) { match v.get(key) {
None => false, None => fallback,
Some(v) => v.as_bool().unwrap_or_default(), Some(v) => v.as_bool().unwrap_or(fallback),
} }
} }
+84 -335
View File
@@ -6,33 +6,25 @@ use crate::encoding::read_response_body;
use crate::grpc::metadata_to_map; use crate::grpc::metadata_to_map;
use crate::http_request::send_http_request; use crate::http_request::send_http_request;
use crate::notifications::YaakNotifier; use crate::notifications::YaakNotifier;
use crate::render::{render_grpc_request, render_http_request, render_json_value, render_template}; use crate::render::{render_grpc_request, render_template};
use crate::template_callback::PluginTemplateCallback; use crate::template_callback::PluginTemplateCallback;
use crate::updates::{UpdateMode, YaakUpdater}; use crate::updates::{UpdateMode, YaakUpdater};
use crate::window_menu::app_menu;
use chrono::Utc;
use eventsource_client::{EventParser, SSE}; use eventsource_client::{EventParser, SSE};
use log::{debug, error, info, warn}; use log::{debug, error, warn};
use rand::random; use rand::random;
use regex::Regex; use regex::Regex;
use serde::Serialize;
use serde_json::{json, Value}; use serde_json::{json, Value};
use std::collections::BTreeMap; use std::collections::{BTreeMap, HashMap};
use std::fs::{create_dir_all, File}; use std::fs::{create_dir_all, File};
use std::path::PathBuf; use std::path::PathBuf;
use std::process::exit;
use std::str::FromStr; use std::str::FromStr;
use std::time::Duration; use std::time::Duration;
use std::{fs, panic}; use std::{fs, panic};
#[cfg(target_os = "macos")] use tauri::{AppHandle, Emitter, RunEvent, State, WebviewWindow};
use tauri::TitleBarStyle;
use tauri::{AppHandle, Emitter, LogicalSize, RunEvent, State, WebviewUrl, WebviewWindow};
use tauri::{Listener, Runtime}; use tauri::{Listener, Runtime};
use tauri::{Manager, WindowEvent}; use tauri::{Manager, WindowEvent};
use tauri_plugin_clipboard_manager::ClipboardExt;
use tauri_plugin_log::fern::colors::ColoredLevelConfig; use tauri_plugin_log::fern::colors::ColoredLevelConfig;
use tauri_plugin_log::{Builder, Target, TargetKind}; use tauri_plugin_log::{Builder, Target, TargetKind};
use tauri_plugin_opener::OpenerExt;
use tauri_plugin_window_state::{AppHandleExt, StateFlags}; use tauri_plugin_window_state::{AppHandleExt, StateFlags};
use tokio::fs::read_to_string; use tokio::fs::read_to_string;
use tokio::sync::Mutex; use tokio::sync::Mutex;
@@ -51,28 +43,24 @@ use yaak_models::queries::{
delete_all_http_responses_for_workspace, delete_cookie_jar, delete_environment, delete_folder, delete_all_http_responses_for_workspace, delete_cookie_jar, delete_environment, delete_folder,
delete_grpc_connection, delete_grpc_request, delete_http_request, delete_http_response, delete_grpc_connection, delete_grpc_request, delete_http_request, delete_http_response,
delete_plugin, delete_workspace, duplicate_folder, duplicate_grpc_request, delete_plugin, delete_workspace, duplicate_folder, duplicate_grpc_request,
duplicate_http_request, ensure_base_environment, generate_id, generate_model_id, duplicate_http_request, ensure_base_environment, generate_model_id, get_base_environment,
get_base_environment, get_cookie_jar, get_environment, get_folder, get_grpc_connection, get_cookie_jar, get_environment, get_folder, get_grpc_connection, get_grpc_request,
get_grpc_request, get_http_request, get_http_response, get_key_value_raw, get_http_request, get_http_response, get_key_value_raw, get_or_create_settings,
get_or_create_settings, get_or_create_workspace_meta, get_plugin, get_workspace, get_or_create_workspace_meta, get_plugin, get_workspace, get_workspace_export_resources,
get_workspace_export_resources, list_cookie_jars, list_environments, list_folders, list_cookie_jars, list_environments, list_folders, list_grpc_connections_for_workspace,
list_grpc_connections_for_workspace, list_grpc_events, list_grpc_requests, list_http_requests, list_grpc_events, list_grpc_requests, list_http_requests, list_http_responses_for_workspace,
list_http_responses_for_request, list_http_responses_for_workspace, list_key_values_raw, list_key_values_raw, list_plugins, list_workspaces, set_key_value_raw, update_response_if_id,
list_plugins, list_workspaces, set_key_value_raw, update_response_if_id, update_settings, update_settings, upsert_cookie_jar, upsert_environment, upsert_folder, upsert_grpc_connection,
upsert_cookie_jar, upsert_environment, upsert_folder, upsert_grpc_connection,
upsert_grpc_event, upsert_grpc_request, upsert_http_request, upsert_plugin, upsert_workspace, upsert_grpc_event, upsert_grpc_request, upsert_http_request, upsert_plugin, upsert_workspace,
upsert_workspace_meta, BatchUpsertResult, UpdateSource, upsert_workspace_meta, BatchUpsertResult, UpdateSource,
}; };
use yaak_plugins::events::{ use yaak_plugins::events::{
BootResponse, CallHttpAuthenticationRequest, CallHttpRequestActionRequest, Color, BootResponse, CallHttpAuthenticationRequest, CallHttpRequestActionRequest, FilterResponse,
FilterResponse, FindHttpResponsesResponse, GetHttpAuthenticationResponse, GetHttpAuthenticationConfigResponse, GetHttpAuthenticationSummaryResponse,
GetHttpRequestActionsResponse, GetHttpRequestByIdResponse, GetTemplateFunctionsResponse, GetHttpRequestActionsResponse, GetTemplateFunctionsResponse, HttpHeader, InternalEvent,
HttpHeader, Icon, InternalEvent, InternalEventPayload, PromptTextResponse, InternalEventPayload, JsonPrimitive, RenderPurpose, WindowContext,
RenderHttpRequestResponse, RenderPurpose, SendHttpRequestResponse, ShowToastRequest,
TemplateRenderResponse, WindowContext,
}; };
use yaak_plugins::manager::PluginManager; use yaak_plugins::manager::PluginManager;
use yaak_plugins::plugin_handle::PluginHandle;
use yaak_sse::sse::ServerSentEvent; use yaak_sse::sse::ServerSentEvent;
use yaak_templates::format::format_json; use yaak_templates::format::format_json;
use yaak_templates::{Parser, Tokens}; use yaak_templates::{Parser, Tokens};
@@ -82,11 +70,13 @@ mod encoding;
mod grpc; mod grpc;
mod http_request; mod http_request;
mod notifications; mod notifications;
mod plugin_events;
mod render; mod render;
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
mod tauri_plugin_mac_window; mod tauri_plugin_mac_window;
mod template_callback; mod template_callback;
mod updates; mod updates;
mod window;
mod window_menu; mod window_menu;
const DEFAULT_WINDOW_WIDTH: f64 = 1100.0; const DEFAULT_WINDOW_WIDTH: f64 = 1100.0;
@@ -242,7 +232,8 @@ async fn cmd_grpc_go<R: Runtime>(
if let Some(auth_name) = request.authentication_type.clone() { if let Some(auth_name) = request.authentication_type.clone() {
let auth = request.authentication.clone(); let auth = request.authentication.clone();
let plugin_req = CallHttpAuthenticationRequest { let plugin_req = CallHttpAuthenticationRequest {
config: serde_json::to_value(&auth).unwrap().as_object().unwrap().to_owned(), context_id: format!("{:x}", md5::compute(request_id.to_string())),
values: serde_json::from_value(serde_json::to_value(&auth).unwrap()).unwrap(),
method: "POST".to_string(), method: "POST".to_string(),
url: request.url.clone(), url: request.url.clone(),
headers: metadata headers: metadata
@@ -969,15 +960,31 @@ async fn cmd_template_functions<R: Runtime>(
} }
#[tauri::command] #[tauri::command]
async fn cmd_get_http_authentication<R: Runtime>( async fn cmd_get_http_authentication_summaries<R: Runtime>(
window: WebviewWindow<R>, window: WebviewWindow<R>,
plugin_manager: State<'_, PluginManager>, plugin_manager: State<'_, PluginManager>,
) -> Result<Vec<GetHttpAuthenticationResponse>, String> { ) -> Result<Vec<GetHttpAuthenticationSummaryResponse>, String> {
let results = let results = plugin_manager
plugin_manager.get_http_authentication(&window).await.map_err(|e| e.to_string())?; .get_http_authentication_summaries(&window)
.await
.map_err(|e| e.to_string())?;
Ok(results.into_iter().map(|(_, a)| a).collect()) Ok(results.into_iter().map(|(_, a)| a).collect())
} }
#[tauri::command]
async fn cmd_get_http_authentication_config<R: Runtime>(
window: WebviewWindow<R>,
plugin_manager: State<'_, PluginManager>,
auth_name: &str,
values: HashMap<String, JsonPrimitive>,
request_id: &str,
) -> Result<GetHttpAuthenticationConfigResponse, String> {
plugin_manager
.get_http_authentication_config(&window, auth_name, values, request_id)
.await
.map_err(|e| e.to_string())
}
#[tauri::command] #[tauri::command]
async fn cmd_call_http_request_action<R: Runtime>( async fn cmd_call_http_request_action<R: Runtime>(
window: WebviewWindow<R>, window: WebviewWindow<R>,
@@ -987,6 +994,21 @@ async fn cmd_call_http_request_action<R: Runtime>(
plugin_manager.call_http_request_action(&window, req).await.map_err(|e| e.to_string()) plugin_manager.call_http_request_action(&window, req).await.map_err(|e| e.to_string())
} }
#[tauri::command]
async fn cmd_call_http_authentication_action<R: Runtime>(
window: WebviewWindow<R>,
plugin_manager: State<'_, PluginManager>,
auth_name: &str,
action_index: i32,
values: HashMap<String, JsonPrimitive>,
request_id: &str,
) -> Result<(), String> {
plugin_manager
.call_http_authentication_action(&window, auth_name, action_index, values, request_id)
.await
.map_err(|e| e.to_string())
}
#[tauri::command] #[tauri::command]
async fn cmd_curl_to_request<R: Runtime>( async fn cmd_curl_to_request<R: Runtime>(
window: WebviewWindow<R>, window: WebviewWindow<R>,
@@ -1175,7 +1197,7 @@ async fn cmd_install_plugin<R: Runtime>(
window: WebviewWindow<R>, window: WebviewWindow<R>,
) -> Result<Plugin, String> { ) -> Result<Plugin, String> {
plugin_manager plugin_manager
.add_plugin_by_dir(WindowContext::from_window(&window), &directory, true) .add_plugin_by_dir(&WindowContext::from_window(&window), &directory, true)
.await .await
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
@@ -1205,7 +1227,7 @@ async fn cmd_uninstall_plugin<R: Runtime>(
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
plugin_manager plugin_manager
.uninstall(WindowContext::from_window(&window), plugin.directory.as_str()) .uninstall(&WindowContext::from_window(&window), plugin.directory.as_str())
.await .await
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
@@ -1458,7 +1480,7 @@ async fn cmd_reload_plugins<R: Runtime>(
plugin_manager: State<'_, PluginManager>, plugin_manager: State<'_, PluginManager>,
) -> Result<(), String> { ) -> Result<(), String> {
plugin_manager plugin_manager
.initialize_all_plugins(window.app_handle(), WindowContext::from_window(&window)) .initialize_all_plugins(window.app_handle(), &WindowContext::from_window(&window))
.await .await
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
Ok(()) Ok(())
@@ -1656,15 +1678,17 @@ async fn cmd_new_child_window(
current_pos.y + current_size.height / 2.0 - inner_size.1 / 2.0, current_pos.y + current_size.height / 2.0 - inner_size.1 / 2.0,
); );
let config = CreateWindowConfig { let config = window::CreateWindowConfig {
label: label.as_str(), label: label.as_str(),
title, title,
url, url,
inner_size, inner_size: Some(inner_size),
position, position: Some(position),
navigation_tx: None,
hide_titlebar: true,
}; };
let child_window = create_window(&app_handle, config); let child_window = window::create_window(&app_handle, config);
// NOTE: These listeners will remain active even when the windows close. Unfortunately, // NOTE: These listeners will remain active even when the windows close. Unfortunately,
// there's no way to unlisten to events for now, so we just have to be defensive. // there's no way to unlisten to events for now, so we just have to be defensive.
@@ -1830,6 +1854,7 @@ pub fn run() {
Ok(()) Ok(())
}) })
.invoke_handler(tauri::generate_handler![ .invoke_handler(tauri::generate_handler![
cmd_call_http_authentication_action,
cmd_call_http_request_action, cmd_call_http_request_action,
cmd_check_for_updates, cmd_check_for_updates,
cmd_create_cookie_jar, cmd_create_cookie_jar,
@@ -1859,7 +1884,8 @@ pub fn run() {
cmd_get_environment, cmd_get_environment,
cmd_get_folder, cmd_get_folder,
cmd_get_grpc_request, cmd_get_grpc_request,
cmd_get_http_authentication, cmd_get_http_authentication_summaries,
cmd_get_http_authentication_config,
cmd_get_http_request, cmd_get_http_request,
cmd_get_key_value, cmd_get_key_value,
cmd_get_settings, cmd_get_settings,
@@ -1998,113 +2024,21 @@ fn create_main_window(handle: &AppHandle, url: &str) -> WebviewWindow {
} }
.expect("Failed to generate label for new window"); .expect("Failed to generate label for new window");
let config = CreateWindowConfig { let config = window::CreateWindowConfig {
url, url,
label: label.as_str(), label: label.as_str(),
title: "Yaak", title: "Yaak",
inner_size: (DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT), inner_size: Some((DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT)),
position: ( position: Some((
// Offset by random amount so it's easier to differentiate // Offset by random amount so it's easier to differentiate
100.0 + random::<f64>() * 20.0, 100.0 + random::<f64>() * 20.0,
100.0 + random::<f64>() * 20.0, 100.0 + random::<f64>() * 20.0,
), )),
navigation_tx: None,
hide_titlebar: true,
}; };
create_window(handle, config) window::create_window(handle, config)
}
struct CreateWindowConfig<'s> {
url: &'s str,
label: &'s str,
title: &'s str,
inner_size: (f64, f64),
position: (f64, f64),
}
fn create_window(handle: &AppHandle, config: CreateWindowConfig) -> WebviewWindow {
#[allow(unused_variables)]
let menu = app_menu(handle).unwrap();
// This causes the window to not be clickable (in AppImage), so disable on Linux
#[cfg(not(target_os = "linux"))]
handle.set_menu(menu).expect("Failed to set app menu");
info!("Create new window label={}", config.label);
let mut win_builder =
tauri::WebviewWindowBuilder::new(handle, config.label, WebviewUrl::App(config.url.into()))
.title(config.title)
.resizable(true)
.visible(false) // To prevent theme flashing, the frontend code calls show() immediately after configuring the theme
.fullscreen(false)
.disable_drag_drop_handler() // Required for frontend Dnd on windows
.inner_size(config.inner_size.0, config.inner_size.1)
.position(config.position.0, config.position.1)
.min_inner_size(MIN_WINDOW_WIDTH, MIN_WINDOW_HEIGHT);
// Add macOS-only things
#[cfg(target_os = "macos")]
{
win_builder = win_builder.hidden_title(true).title_bar_style(TitleBarStyle::Overlay);
}
// Add non-MacOS things
#[cfg(not(target_os = "macos"))]
{
// Doesn't seem to work from Rust, here, so we do it in main.tsx
win_builder = win_builder.decorations(false);
}
if let Some(w) = handle.webview_windows().get(config.label) {
info!("Webview with label {} already exists. Focusing existing", config.label);
w.set_focus().unwrap();
return w.to_owned();
}
let win = win_builder.build().unwrap();
let webview_window = win.clone();
win.on_menu_event(move |w, event| {
if !w.is_focused().unwrap() {
return;
}
let event_id = event.id().0.as_str();
match event_id {
"quit" => exit(0),
"close" => w.close().unwrap(),
"zoom_reset" => w.emit("zoom_reset", true).unwrap(),
"zoom_in" => w.emit("zoom_in", true).unwrap(),
"zoom_out" => w.emit("zoom_out", true).unwrap(),
"settings" => w.emit("settings", true).unwrap(),
"open_feedback" => {
if let Err(e) =
w.app_handle().opener().open_url("https://yaak.app/feedback", None::<&str>)
{
warn!("Failed to open feedback {e:?}")
}
}
// Commands for development
"dev.reset_size" => webview_window
.set_size(LogicalSize::new(DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT))
.unwrap(),
"dev.refresh" => webview_window.eval("location.reload()").unwrap(),
"dev.generate_theme_css" => {
w.emit("generate_theme_css", true).unwrap();
}
"dev.toggle_devtools" => {
if webview_window.is_devtools_open() {
webview_window.close_devtools();
} else {
webview_window.open_devtools();
}
}
_ => {}
}
});
win
} }
async fn get_update_mode(h: &AppHandle) -> UpdateMode { async fn get_update_mode(h: &AppHandle) -> UpdateMode {
@@ -2140,36 +2074,24 @@ fn monitor_plugin_events<R: Runtime>(app_handle: &AppHandle<R>) {
// We might have recursive back-and-forth calls between app and plugin, so we don't // We might have recursive back-and-forth calls between app and plugin, so we don't
// want to block here // want to block here
tauri::async_runtime::spawn(async move { tauri::async_runtime::spawn(async move {
handle_plugin_event(&app_handle, &event, &plugin).await; crate::plugin_events::handle_plugin_event(&app_handle, &event, &plugin).await;
}); });
} }
plugin_manager.unsubscribe(rx_id.as_str()).await; plugin_manager.unsubscribe(rx_id.as_str()).await;
}); });
} }
#[derive(Debug, Clone, Serialize)] async fn call_frontend<R: Runtime>(
#[serde(rename_all = "camelCase")]
struct FrontendCall<T: Serialize + Clone> {
args: T,
reply_id: String,
}
async fn call_frontend<T: Serialize + Clone, R: Runtime>(
window: WebviewWindow<R>, window: WebviewWindow<R>,
event_name: &str, event: &InternalEvent,
args: T, ) -> Option<InternalEventPayload> {
) -> PromptTextResponse { window.emit_to(window.label(), "plugin_event", event.clone()).unwrap();
let reply_id = format!("{event_name}_reply_{}", generate_id()); let (tx, mut rx) = tokio::sync::watch::channel(None);
let payload = FrontendCall {
args,
reply_id: reply_id.clone(),
};
window.emit_to(window.label(), event_name, payload).unwrap();
let (tx, mut rx) = tokio::sync::watch::channel(PromptTextResponse::default());
let reply_id = event.id.clone();
let event_id = window.clone().listen(reply_id, move |ev| { let event_id = window.clone().listen(reply_id, move |ev| {
let resp: PromptTextResponse = serde_json::from_str(ev.payload()).unwrap(); let resp: InternalEvent = serde_json::from_str(ev.payload()).unwrap();
if let Err(e) = tx.send(resp) { if let Err(e) = tx.send(Some(resp.payload)) {
warn!("Failed to prompt for text {e:?}"); warn!("Failed to prompt for text {e:?}");
} }
}); });
@@ -2181,180 +2103,7 @@ async fn call_frontend<T: Serialize + Clone, R: Runtime>(
window.unlisten(event_id); window.unlisten(event_id);
let v = rx.borrow(); let v = rx.borrow();
v.clone() v.to_owned()
}
async fn handle_plugin_event<R: Runtime>(
app_handle: &AppHandle<R>,
event: &InternalEvent,
plugin_handle: &PluginHandle,
) {
// info!("Got event to app {}", event.id);
let window_context = event.window_context.to_owned();
let response_event: Option<InternalEventPayload> = match event.clone().payload {
InternalEventPayload::CopyTextRequest(req) => {
app_handle
.clipboard()
.write_text(req.text.as_str())
.expect("Failed to write text to clipboard");
None
}
InternalEventPayload::ShowToastRequest(req) => {
match window_context {
WindowContext::Label { label } => app_handle
.emit_to(label, "show_toast", req)
.expect("Failed to emit show_toast to window"),
_ => app_handle.emit("show_toast", req).expect("Failed to emit show_toast"),
};
None
}
InternalEventPayload::PromptTextRequest(req) => {
let window = get_window_from_window_context(app_handle, &window_context)
.expect("Failed to find window for render");
let resp = call_frontend(window, "show_prompt", req).await;
Some(InternalEventPayload::PromptTextResponse(resp))
}
InternalEventPayload::FindHttpResponsesRequest(req) => {
let http_responses = list_http_responses_for_request(
app_handle,
req.request_id.as_str(),
req.limit.map(|l| l as i64),
)
.await
.unwrap_or_default();
Some(InternalEventPayload::FindHttpResponsesResponse(FindHttpResponsesResponse {
http_responses,
}))
}
InternalEventPayload::GetHttpRequestByIdRequest(req) => {
let http_request = get_http_request(app_handle, req.id.as_str()).await.unwrap();
Some(InternalEventPayload::GetHttpRequestByIdResponse(GetHttpRequestByIdResponse {
http_request,
}))
}
InternalEventPayload::RenderHttpRequestRequest(req) => {
let window = get_window_from_window_context(app_handle, &window_context)
.expect("Failed to find window for render http request");
let workspace = workspace_from_window(&window)
.await
.expect("Failed to get workspace_id from window URL");
let environment = environment_from_window(&window).await;
let base_environment = get_base_environment(&window, workspace.id.as_str())
.await
.expect("Failed to get base environment");
let cb = PluginTemplateCallback::new(app_handle, &window_context, req.purpose);
let http_request = render_http_request(
&req.http_request,
&base_environment,
environment.as_ref(),
&cb,
)
.await;
Some(InternalEventPayload::RenderHttpRequestResponse(RenderHttpRequestResponse {
http_request,
}))
}
InternalEventPayload::TemplateRenderRequest(req) => {
let window = get_window_from_window_context(app_handle, &window_context)
.expect("Failed to find window for render");
let workspace = workspace_from_window(&window)
.await
.expect("Failed to get workspace_id from window URL");
let environment = environment_from_window(&window).await;
let base_environment = get_base_environment(&window, workspace.id.as_str())
.await
.expect("Failed to get base environment");
let cb = PluginTemplateCallback::new(app_handle, &window_context, req.purpose);
let data =
render_json_value(req.data, &base_environment, environment.as_ref(), &cb).await;
Some(InternalEventPayload::TemplateRenderResponse(TemplateRenderResponse { data }))
}
InternalEventPayload::ErrorResponse(resp) => {
let window = get_window_from_window_context(app_handle, &window_context)
.expect("Failed to find window for plugin reload");
let toast_event = plugin_handle.build_event_to_send(
WindowContext::from_window(&window),
&InternalEventPayload::ShowToastRequest(ShowToastRequest {
message: resp.error,
color: Some(Color::Danger),
..Default::default()
}),
None,
);
Box::pin(handle_plugin_event(app_handle, &toast_event, plugin_handle)).await;
None
}
InternalEventPayload::ReloadResponse(_) => {
let window = get_window_from_window_context(app_handle, &window_context)
.expect("Failed to find window for plugin reload");
let plugins = list_plugins(app_handle).await.unwrap();
for plugin in plugins {
if plugin.directory != plugin_handle.dir {
continue;
}
let new_plugin = Plugin {
updated_at: Utc::now().naive_utc(), // TODO: Add reloaded_at field to use instead
..plugin
};
upsert_plugin(&window, new_plugin, &UpdateSource::Plugin).await.unwrap();
}
let toast_event = plugin_handle.build_event_to_send(
WindowContext::from_window(&window),
&InternalEventPayload::ShowToastRequest(ShowToastRequest {
message: format!("Reloaded plugin {}", plugin_handle.dir),
icon: Some(Icon::Info),
..Default::default()
}),
None,
);
Box::pin(handle_plugin_event(app_handle, &toast_event, plugin_handle)).await;
None
}
InternalEventPayload::SendHttpRequestRequest(req) => {
let window = get_window_from_window_context(app_handle, &window_context)
.expect("Failed to find window for sending HTTP request");
let cookie_jar = cookie_jar_from_window(&window).await;
let environment = environment_from_window(&window).await;
let resp = create_default_http_response(
&window,
req.http_request.id.as_str(),
&UpdateSource::Plugin,
)
.await
.unwrap();
let result = send_http_request(
&window,
&req.http_request,
&resp,
environment,
cookie_jar,
&mut tokio::sync::watch::channel(false).1, // No-op cancel channel
)
.await;
let http_response = match result {
Ok(r) => r,
Err(_e) => return,
};
Some(InternalEventPayload::SendHttpRequestResponse(SendHttpRequestResponse {
http_response,
}))
}
_ => None,
};
if let Some(e) = response_event {
let plugin_manager: State<'_, PluginManager> = app_handle.state();
if let Err(e) = plugin_manager.reply(&event, &e).await {
warn!("Failed to reply to plugin manager: {:?}", e)
}
}
} }
fn get_window_from_window_context<R: Runtime>( fn get_window_from_window_context<R: Runtime>(
+265
View File
@@ -0,0 +1,265 @@
use crate::http_request::send_http_request;
use crate::render::{render_http_request, render_json_value};
use crate::template_callback::PluginTemplateCallback;
use crate::window::{create_window, CreateWindowConfig};
use crate::{
call_frontend, cookie_jar_from_window, environment_from_window, get_window_from_window_context,
workspace_from_window,
};
use chrono::Utc;
use log::warn;
use tauri::{AppHandle, Emitter, Manager, Runtime, State};
use tauri_plugin_clipboard_manager::ClipboardExt;
use yaak_models::models::{HttpResponse, Plugin};
use yaak_models::queries::{
create_default_http_response, delete_plugin_key_value, get_base_environment, get_http_request,
get_plugin_key_value, list_http_responses_for_request, list_plugins, set_plugin_key_value,
upsert_plugin, UpdateSource,
};
use yaak_plugins::events::{
Color, DeleteKeyValueResponse, EmptyPayload, FindHttpResponsesResponse,
GetHttpRequestByIdResponse, GetKeyValueResponse, Icon, InternalEvent, InternalEventPayload,
RenderHttpRequestResponse, SendHttpRequestResponse, SetKeyValueResponse, ShowToastRequest,
TemplateRenderResponse, WindowContext, WindowNavigateEvent,
};
use yaak_plugins::manager::PluginManager;
use yaak_plugins::plugin_handle::PluginHandle;
pub(crate) async fn handle_plugin_event<R: Runtime>(
app_handle: &AppHandle<R>,
event: &InternalEvent,
plugin_handle: &PluginHandle,
) {
// info!("Got event to app {}", event.id);
let window_context = event.window_context.to_owned();
let response_event: Option<InternalEventPayload> = match event.clone().payload {
InternalEventPayload::CopyTextRequest(req) => {
app_handle
.clipboard()
.write_text(req.text.as_str())
.expect("Failed to write text to clipboard");
Some(InternalEventPayload::CopyTextResponse(EmptyPayload {}))
}
InternalEventPayload::ShowToastRequest(req) => {
match window_context {
WindowContext::Label { label } => app_handle
.emit_to(label, "show_toast", req)
.expect("Failed to emit show_toast to window"),
_ => app_handle.emit("show_toast", req).expect("Failed to emit show_toast"),
};
Some(InternalEventPayload::ShowToastResponse(EmptyPayload {}))
}
InternalEventPayload::PromptTextRequest(_) => {
let window = get_window_from_window_context(app_handle, &window_context)
.expect("Failed to find window for render");
call_frontend(window, event).await
}
InternalEventPayload::FindHttpResponsesRequest(req) => {
let http_responses = list_http_responses_for_request(
app_handle,
req.request_id.as_str(),
req.limit.map(|l| l as i64),
)
.await
.unwrap_or_default();
Some(InternalEventPayload::FindHttpResponsesResponse(FindHttpResponsesResponse {
http_responses,
}))
}
InternalEventPayload::GetHttpRequestByIdRequest(req) => {
let http_request = get_http_request(app_handle, req.id.as_str()).await.unwrap();
Some(InternalEventPayload::GetHttpRequestByIdResponse(GetHttpRequestByIdResponse {
http_request,
}))
}
InternalEventPayload::RenderHttpRequestRequest(req) => {
let window = get_window_from_window_context(app_handle, &window_context)
.expect("Failed to find window for render http request");
let workspace = workspace_from_window(&window)
.await
.expect("Failed to get workspace_id from window URL");
let environment = environment_from_window(&window).await;
let base_environment = get_base_environment(&window, workspace.id.as_str())
.await
.expect("Failed to get base environment");
let cb = PluginTemplateCallback::new(app_handle, &window_context, req.purpose);
let http_request = render_http_request(
&req.http_request,
&base_environment,
environment.as_ref(),
&cb,
)
.await;
Some(InternalEventPayload::RenderHttpRequestResponse(RenderHttpRequestResponse {
http_request,
}))
}
InternalEventPayload::TemplateRenderRequest(req) => {
let window = get_window_from_window_context(app_handle, &window_context)
.expect("Failed to find window for render");
let workspace = workspace_from_window(&window)
.await
.expect("Failed to get workspace_id from window URL");
let environment = environment_from_window(&window).await;
let base_environment = get_base_environment(&window, workspace.id.as_str())
.await
.expect("Failed to get base environment");
let cb = PluginTemplateCallback::new(app_handle, &window_context, req.purpose);
let data =
render_json_value(req.data, &base_environment, environment.as_ref(), &cb).await;
Some(InternalEventPayload::TemplateRenderResponse(TemplateRenderResponse { data }))
}
InternalEventPayload::ErrorResponse(resp) => {
let window = get_window_from_window_context(app_handle, &window_context)
.expect("Failed to find window for plugin reload");
let toast_event = plugin_handle.build_event_to_send(
&WindowContext::from_window(&window),
&InternalEventPayload::ShowToastRequest(ShowToastRequest {
message: format!(
"Plugin error from {}: {}",
plugin_handle.name().await,
resp.error
),
color: Some(Color::Danger),
..Default::default()
}),
None,
);
Box::pin(handle_plugin_event(app_handle, &toast_event, plugin_handle)).await;
None
}
InternalEventPayload::ReloadResponse(_) => {
let window = get_window_from_window_context(app_handle, &window_context)
.expect("Failed to find window for plugin reload");
let plugins = list_plugins(app_handle).await.unwrap();
for plugin in plugins {
if plugin.directory != plugin_handle.dir {
continue;
}
let new_plugin = Plugin {
updated_at: Utc::now().naive_utc(), // TODO: Add reloaded_at field to use instead
..plugin
};
upsert_plugin(&window, new_plugin, &UpdateSource::Plugin).await.unwrap();
}
let toast_event = plugin_handle.build_event_to_send(
&WindowContext::from_window(&window),
&InternalEventPayload::ShowToastRequest(ShowToastRequest {
message: format!("Reloaded plugin {}", plugin_handle.dir),
icon: Some(Icon::Info),
..Default::default()
}),
None,
);
Box::pin(handle_plugin_event(app_handle, &toast_event, plugin_handle)).await;
None
}
InternalEventPayload::SendHttpRequestRequest(req) => {
let window = get_window_from_window_context(app_handle, &window_context)
.expect("Failed to find window for sending HTTP request");
let mut http_request = req.http_request;
let workspace = workspace_from_window(&window)
.await
.expect("Failed to get workspace_id from window URL");
let cookie_jar = cookie_jar_from_window(&window).await;
let environment = environment_from_window(&window).await;
if http_request.workspace_id.is_empty() {
http_request.workspace_id = workspace.id;
}
let resp = if http_request.id.is_empty() {
HttpResponse::new()
} else {
create_default_http_response(
&window,
http_request.id.as_str(),
&UpdateSource::Plugin,
)
.await
.unwrap()
};
let result = send_http_request(
&window,
&http_request,
&resp,
environment,
cookie_jar,
&mut tokio::sync::watch::channel(false).1, // No-op cancel channel
)
.await;
let http_response = match result {
Ok(r) => r,
Err(_e) => return,
};
Some(InternalEventPayload::SendHttpRequestResponse(SendHttpRequestResponse {
http_response,
}))
}
InternalEventPayload::OpenWindowRequest(req) => {
let label = req.label;
let (tx, mut rx) = tokio::sync::mpsc::channel(128);
let win_config = CreateWindowConfig {
url: &req.url,
label: &label.clone(),
title: &req.title.unwrap_or_default(),
navigation_tx: Some(tx),
inner_size: req.size.map(|s| (s.width, s.height)),
position: None,
hide_titlebar: false,
};
create_window(app_handle, win_config);
let event_id = event.id.clone();
let plugin_handle = plugin_handle.clone();
tauri::async_runtime::spawn(async move {
while let Some(url) = rx.recv().await {
let label = label.clone();
let url = url.to_string();
let event_to_send = plugin_handle.build_event_to_send(
&WindowContext::Label { label },
&InternalEventPayload::WindowNavigateEvent(WindowNavigateEvent { url }),
Some(event_id.clone()),
);
plugin_handle.send(&event_to_send).await.unwrap();
}
});
None
}
InternalEventPayload::CloseWindowRequest(req) => {
if let Some(window) = app_handle.webview_windows().get(&req.label) {
window.close().expect("Failed to close window");
}
None
}
InternalEventPayload::SetKeyValueRequest(req) => {
let name = plugin_handle.name().await;
set_plugin_key_value(app_handle, &name, &req.key, &req.value).await;
Some(InternalEventPayload::SetKeyValueResponse(SetKeyValueResponse {}))
}
InternalEventPayload::GetKeyValueRequest(req) => {
let name = plugin_handle.name().await;
let value = get_plugin_key_value(app_handle, &name, &req.key).await.map(|v| v.value);
Some(InternalEventPayload::GetKeyValueResponse(GetKeyValueResponse { value }))
}
InternalEventPayload::DeleteKeyValueRequest(req) => {
let name = plugin_handle.name().await;
let deleted = delete_plugin_key_value(app_handle, &name, &req.key).await;
Some(InternalEventPayload::DeleteKeyValueResponse(DeleteKeyValueResponse { deleted }))
}
_ => None,
};
if let Some(e) = response_event {
let plugin_manager: State<'_, PluginManager> = app_handle.state();
if let Err(e) = plugin_manager.reply(&event, &e).await {
warn!("Failed to reply to plugin manager: {:?}", e)
}
}
}
+5 -3
View File
@@ -28,14 +28,13 @@ impl PluginTemplateCallback {
impl TemplateCallback for PluginTemplateCallback { impl TemplateCallback for PluginTemplateCallback {
async fn run(&self, fn_name: &str, args: HashMap<String, String>) -> Result<String, String> { async fn run(&self, fn_name: &str, args: HashMap<String, String>) -> Result<String, String> {
let window_context = self.window_context.to_owned();
// The beta named the function `Response` but was changed in stable. // The beta named the function `Response` but was changed in stable.
// Keep this here for a while because there's no easy way to migrate // Keep this here for a while because there's no easy way to migrate
let fn_name = if fn_name == "Response" { "response" } else { fn_name }; let fn_name = if fn_name == "Response" { "response" } else { fn_name };
let function = self let function = self
.plugin_manager .plugin_manager
.get_template_functions_with_context(window_context.to_owned()) .get_template_functions_with_context(&self.window_context)
.await .await
.map_err(|e| e.to_string())? .map_err(|e| e.to_string())?
.iter() .iter()
@@ -54,6 +53,9 @@ impl TemplateCallback for PluginTemplateCallback {
FormInput::Checkbox(a) => a.base, FormInput::Checkbox(a) => a.base,
FormInput::File(a) => a.base, FormInput::File(a) => a.base,
FormInput::HttpRequest(a) => a.base, FormInput::HttpRequest(a) => a.base,
FormInput::Accordion(_) => continue,
FormInput::Banner(_) => continue,
FormInput::Markdown(_) => continue,
}; };
if let None = args_with_defaults.get(base.name.as_str()) { if let None = args_with_defaults.get(base.name.as_str()) {
args_with_defaults.insert(base.name, base.default_value.unwrap_or_default()); args_with_defaults.insert(base.name, base.default_value.unwrap_or_default());
@@ -63,7 +65,7 @@ impl TemplateCallback for PluginTemplateCallback {
let resp = self let resp = self
.plugin_manager .plugin_manager
.call_template_function( .call_template_function(
window_context, &self.window_context,
fn_name, fn_name,
args_with_defaults, args_with_defaults,
self.render_purpose.to_owned(), self.render_purpose.to_owned(),
+129
View File
@@ -0,0 +1,129 @@
use crate::window_menu::app_menu;
use crate::{DEFAULT_WINDOW_HEIGHT, DEFAULT_WINDOW_WIDTH, MIN_WINDOW_HEIGHT, MIN_WINDOW_WIDTH};
use log::{info, warn};
use std::process::exit;
use tauri::{
AppHandle, Emitter, LogicalSize, Manager, Runtime, TitleBarStyle, WebviewUrl, WebviewWindow,
};
use tauri_plugin_opener::OpenerExt;
use tokio::sync::mpsc;
#[derive(Default, Debug)]
pub(crate) struct CreateWindowConfig<'s> {
pub url: &'s str,
pub label: &'s str,
pub title: &'s str,
pub inner_size: Option<(f64, f64)>,
pub position: Option<(f64, f64)>,
pub navigation_tx: Option<mpsc::Sender<String>>,
pub hide_titlebar: bool,
}
pub(crate) fn create_window<R: Runtime>(
handle: &AppHandle<R>,
config: CreateWindowConfig,
) -> WebviewWindow<R> {
#[allow(unused_variables)]
let menu = app_menu(handle).unwrap();
// This causes the window to not be clickable (in AppImage), so disable on Linux
#[cfg(not(target_os = "linux"))]
handle.set_menu(menu).expect("Failed to set app menu");
info!("Create new window label={}", config.label);
let mut win_builder =
tauri::WebviewWindowBuilder::new(handle, config.label, WebviewUrl::App(config.url.into()))
.title(config.title)
.resizable(true)
.visible(false) // To prevent theme flashing, the frontend code calls show() immediately after configuring the theme
.fullscreen(false)
.disable_drag_drop_handler() // Required for frontend Dnd on windows
.min_inner_size(MIN_WINDOW_WIDTH, MIN_WINDOW_HEIGHT);
if let Some((w, h)) = config.inner_size {
win_builder = win_builder.inner_size(w, h);
} else {
win_builder = win_builder.inner_size(600.0, 600.0);
}
if let Some((x, y)) = config.position {
win_builder = win_builder.position(x, y);
} else {
win_builder = win_builder.center();
}
if let Some(tx) = config.navigation_tx {
win_builder = win_builder.on_navigation(move |url| {
let url = url.to_string();
let tx = tx.clone();
tauri::async_runtime::block_on(async move {
tx.send(url).await.unwrap();
});
true
});
}
if config.hide_titlebar {
#[cfg(target_os = "macos")]
{
win_builder = win_builder.hidden_title(true).title_bar_style(TitleBarStyle::Overlay);
}
#[cfg(not(target_os = "macos"))]
{
// Doesn't seem to work from Rust, here, so we do it in main.tsx
win_builder = win_builder.decorations(false);
}
}
if let Some(w) = handle.webview_windows().get(config.label) {
info!("Webview with label {} already exists. Focusing existing", config.label);
w.set_focus().unwrap();
return w.to_owned();
}
let win = win_builder.build().unwrap();
let webview_window = win.clone();
win.on_menu_event(move |w, event| {
if !w.is_focused().unwrap() {
return;
}
let event_id = event.id().0.as_str();
match event_id {
"quit" => exit(0),
"close" => w.close().unwrap(),
"zoom_reset" => w.emit("zoom_reset", true).unwrap(),
"zoom_in" => w.emit("zoom_in", true).unwrap(),
"zoom_out" => w.emit("zoom_out", true).unwrap(),
"settings" => w.emit("settings", true).unwrap(),
"open_feedback" => {
if let Err(e) =
w.app_handle().opener().open_url("https://yaak.app/feedback", None::<&str>)
{
warn!("Failed to open feedback {e:?}")
}
}
// Commands for development
"dev.reset_size" => webview_window
.set_size(LogicalSize::new(DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT))
.unwrap(),
"dev.refresh" => webview_window.eval("location.reload()").unwrap(),
"dev.generate_theme_css" => {
w.emit("generate_theme_css", true).unwrap();
}
"dev.toggle_devtools" => {
if webview_window.is_devtools_open() {
webview_window.close_devtools();
} else {
webview_window.open_devtools();
}
}
_ => {}
}
});
win
}
+3 -3
View File
@@ -3,9 +3,9 @@ use tauri::menu::{
WINDOW_SUBMENU_ID, WINDOW_SUBMENU_ID,
}; };
pub use tauri::AppHandle; pub use tauri::AppHandle;
use tauri::Wry; use tauri::Runtime;
pub fn app_menu(app_handle: &AppHandle) -> tauri::Result<Menu<Wry>> { pub fn app_menu<R: Runtime>(app_handle: &AppHandle<R>) -> tauri::Result<Menu<R>> {
let pkg_info = app_handle.package_info(); let pkg_info = app_handle.package_info();
let config = app_handle.config(); let config = app_handle.config();
let about_metadata = AboutMetadata { let about_metadata = AboutMetadata {
@@ -37,7 +37,7 @@ pub fn app_menu(app_handle: &AppHandle) -> tauri::Result<Menu<Wry>> {
true, true,
&[ &[
#[cfg(not(target_os = "macos"))] #[cfg(not(target_os = "macos"))]
&PredefinedMenuItem::about(app_handle, None, Some(about_metadata))?, &PredefinedMenuItem::about(app_handle, None, Some(about_metadata.clone()))?,
#[cfg(target_os = "macos")] #[cfg(target_os = "macos")]
&MenuItemBuilder::with_id("open_feedback".to_string(), "Give Feedback") &MenuItemBuilder::with_id("open_feedback".to_string(), "Give Feedback")
.build(app_handle)?, .build(app_handle)?,
+3 -3
View File
@@ -28,7 +28,7 @@ var plugin = {
name: "basic", name: "basic",
label: "Basic Auth", label: "Basic Auth",
shortLabel: "Basic", shortLabel: "Basic",
config: [{ args: [{
type: "text", type: "text",
name: "username", name: "username",
label: "Username", label: "Username",
@@ -40,8 +40,8 @@ var plugin = {
optional: true, optional: true,
password: true password: true
}], }],
async onApply(_ctx, args) { async onApply(_ctx, { values }) {
const { username, password } = args.config; const { username, password } = values;
const value = "Basic " + Buffer.from(`${username}:${password}`).toString("base64"); const value = "Basic " + Buffer.from(`${username}:${password}`).toString("base64");
return { setHeaders: [{ name: "Authorization", value }] }; return { setHeaders: [{ name: "Authorization", value }] };
} }
+3 -3
View File
@@ -28,15 +28,15 @@ var plugin = {
name: "bearer", name: "bearer",
label: "Bearer Token", label: "Bearer Token",
shortLabel: "Bearer", shortLabel: "Bearer",
config: [{ args: [{
type: "text", type: "text",
name: "token", name: "token",
label: "Token", label: "Token",
optional: true, optional: true,
password: true password: true
}], }],
async onApply(_ctx, args) { async onApply(_ctx, { values }) {
const { token } = args.config; const { token } = values;
const value = `Bearer ${token}`.trim(); const value = `Bearer ${token}`.trim();
return { setHeaders: [{ name: "Authorization", value }] }; return { setHeaders: [{ name: "Authorization", value }] };
} }
+7 -6
View File
@@ -3814,21 +3814,22 @@ var plugin = {
name: "jwt", name: "jwt",
label: "JWT Bearer", label: "JWT Bearer",
shortLabel: "JWT", shortLabel: "JWT",
config: [ args: [
{ {
type: "select", type: "select",
name: "algorithm", name: "algorithm",
label: "Algorithm", label: "Algorithm",
hideLabel: true, hideLabel: true,
defaultValue: defaultAlgorithm, defaultValue: defaultAlgorithm,
options: algorithms.map((value) => ({ name: value === "none" ? "None" : value, value })) options: algorithms.map((value) => ({ label: value === "none" ? "None" : value, value }))
}, },
{ {
type: "editor", type: "text",
name: "secret", name: "secret",
label: "Secret or Private Key", label: "Secret or Private Key",
password: true,
optional: true, optional: true,
hideGutter: true multiLine: true
}, },
{ {
type: "checkbox", type: "checkbox",
@@ -3844,8 +3845,8 @@ var plugin = {
placeholder: "{ }" placeholder: "{ }"
} }
], ],
async onApply(_ctx, args) { async onApply(_ctx, { values }) {
const { algorithm, secret: _secret, secretBase64, payload } = args.config; const { algorithm, secret: _secret, secretBase64, payload } = values;
const secret = secretBase64 ? Buffer.from(`${_secret}`, "base64") : `${_secret}`; const secret = secretBase64 ? Buffer.from(`${_secret}`, "base64") : `${_secret}`;
const token = import_jsonwebtoken.default.sign(`${payload}`, secret, { algorithm }); const token = import_jsonwebtoken.default.sign(`${payload}`, secret, { algorithm });
const value = `Bearer ${token}`; const value = `Bearer ${token}`;
+673
View File
@@ -0,0 +1,673 @@
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var src_exports = {};
__export(src_exports, {
plugin: () => plugin
});
module.exports = __toCommonJS(src_exports);
// src/grants/authorizationCode.ts
var import_node_crypto = require("node:crypto");
// src/getAccessToken.ts
var import_node_fs = require("node:fs");
async function getAccessToken(ctx, {
accessTokenUrl,
scope,
params,
grantType,
credentialsInBody,
clientId,
clientSecret
}) {
console.log("Getting access token", accessTokenUrl);
const httpRequest = {
method: "POST",
url: accessTokenUrl,
bodyType: "application/x-www-form-urlencoded",
body: {
form: [
{ name: "grant_type", value: grantType },
...params
]
},
headers: [
{ name: "User-Agent", value: "yaak" },
{ name: "Accept", value: "application/x-www-form-urlencoded, application/json" },
{ name: "Content-Type", value: "application/x-www-form-urlencoded" }
]
};
if (scope) httpRequest.body.form.push({ name: "scope", value: scope });
if (credentialsInBody) {
httpRequest.body.form.push({ name: "client_id", value: clientId });
httpRequest.body.form.push({ name: "client_secret", value: clientSecret });
} else {
const value = "Basic " + Buffer.from(`${clientId}:${clientSecret}`).toString("base64");
httpRequest.headers.push({ name: "Authorization", value });
}
const resp = await ctx.httpRequest.send({ httpRequest });
if (resp.status < 200 || resp.status >= 300) {
throw new Error("Failed to fetch access token with status=" + resp.status);
}
const body = (0, import_node_fs.readFileSync)(resp.bodyPath ?? "", "utf8");
let response;
try {
response = JSON.parse(body);
} catch {
response = Object.fromEntries(new URLSearchParams(body));
}
if (response.error) {
throw new Error("Failed to fetch access token with " + response.error);
}
return response;
}
// src/getOrRefreshAccessToken.ts
var import_node_fs2 = require("node:fs");
// src/store.ts
async function storeToken(ctx, contextId, response) {
if (!response.access_token) {
throw new Error(`Token not found in response`);
}
const expiresAt = response.expires_in ? Date.now() + response.expires_in * 1e3 : null;
const token = {
response,
expiresAt
};
await ctx.store.set(tokenStoreKey(contextId), token);
return token;
}
async function getToken(ctx, contextId) {
return ctx.store.get(tokenStoreKey(contextId));
}
async function deleteToken(ctx, contextId) {
return ctx.store.delete(tokenStoreKey(contextId));
}
function tokenStoreKey(context_id) {
return ["token", context_id].join("::");
}
// src/getOrRefreshAccessToken.ts
async function getOrRefreshAccessToken(ctx, contextId, {
scope,
accessTokenUrl,
credentialsInBody,
clientId,
clientSecret,
forceRefresh
}) {
const token = await getToken(ctx, contextId);
if (token == null) {
return null;
}
const now = Date.now() / 1e3;
const isExpired = token.expiresAt && now > token.expiresAt;
if (!isExpired && !forceRefresh) {
return token;
}
if (!token.response.refresh_token) {
return null;
}
const httpRequest = {
method: "POST",
url: accessTokenUrl,
bodyType: "application/x-www-form-urlencoded",
body: {
form: [
{ name: "grant_type", value: "refresh_token" },
{ name: "refresh_token", value: token.response.refresh_token }
]
},
headers: [
{ name: "User-Agent", value: "yaak" },
{ name: "Accept", value: "application/x-www-form-urlencoded, application/json" },
{ name: "Content-Type", value: "application/x-www-form-urlencoded" }
]
};
if (scope) httpRequest.body.form.push({ name: "scope", value: scope });
if (credentialsInBody) {
httpRequest.body.form.push({ name: "client_id", value: clientId });
httpRequest.body.form.push({ name: "client_secret", value: clientSecret });
} else {
const value = "Basic " + Buffer.from(`${clientId}:${clientSecret}`).toString("base64");
httpRequest.headers.push({ name: "Authorization", value });
}
const resp = await ctx.httpRequest.send({ httpRequest });
if (resp.status === 401) {
console.log("Unauthorized refresh_token request");
await deleteToken(ctx, contextId);
return null;
}
if (resp.status < 200 || resp.status >= 300) {
throw new Error("Failed to fetch access token with status=" + resp.status);
}
const body = (0, import_node_fs2.readFileSync)(resp.bodyPath ?? "", "utf8");
let response;
try {
response = JSON.parse(body);
} catch {
response = Object.fromEntries(new URLSearchParams(body));
}
if (response.error) {
throw new Error(`Failed to fetch access token with ${response.error} -> ${response.error_description}`);
}
const newResponse = {
...response,
// Assign a new one or keep the old one,
refresh_token: response.refresh_token ?? token.response.refresh_token
};
return storeToken(ctx, contextId, newResponse);
}
// src/grants/authorizationCode.ts
var PKCE_SHA256 = "S256";
var PKCE_PLAIN = "plain";
var DEFAULT_PKCE_METHOD = PKCE_SHA256;
async function getAuthorizationCode(ctx, contextId, {
authorizationUrl: authorizationUrlRaw,
accessTokenUrl,
clientId,
clientSecret,
redirectUri,
scope,
state,
credentialsInBody,
pkce
}) {
const token = await getOrRefreshAccessToken(ctx, contextId, {
accessTokenUrl,
scope,
clientId,
clientSecret,
credentialsInBody
});
if (token != null) {
return token;
}
const authorizationUrl = new URL(`${authorizationUrlRaw ?? ""}`);
authorizationUrl.searchParams.set("response_type", "code");
authorizationUrl.searchParams.set("client_id", clientId);
if (redirectUri) authorizationUrl.searchParams.set("redirect_uri", redirectUri);
if (scope) authorizationUrl.searchParams.set("scope", scope);
if (state) authorizationUrl.searchParams.set("state", state);
if (pkce) {
const verifier = pkce.codeVerifier || createPkceCodeVerifier();
const challengeMethod = pkce.challengeMethod || DEFAULT_PKCE_METHOD;
authorizationUrl.searchParams.set("code_challenge", createPkceCodeChallenge(verifier, challengeMethod));
authorizationUrl.searchParams.set("code_challenge_method", challengeMethod);
}
return new Promise(async (resolve, reject) => {
const authorizationUrlStr = authorizationUrl.toString();
console.log("Authorizing", authorizationUrlStr);
let { close } = await ctx.window.openUrl({
url: authorizationUrlStr,
label: "oauth-authorization-url",
async onNavigate({ url: urlStr }) {
const url = new URL(urlStr);
if (url.searchParams.has("error")) {
return reject(new Error(`Failed to authorize: ${url.searchParams.get("error")}`));
}
const code = url.searchParams.get("code");
if (!code) {
return;
}
close();
const response = await getAccessToken(ctx, {
grantType: "authorization_code",
accessTokenUrl,
clientId,
clientSecret,
scope,
credentialsInBody,
params: [
{ name: "code", value: code },
...redirectUri ? [{ name: "redirect_uri", value: redirectUri }] : []
]
});
try {
resolve(await storeToken(ctx, contextId, response));
} catch (err) {
reject(err);
}
}
});
});
}
function createPkceCodeVerifier() {
return encodeForPkce((0, import_node_crypto.randomBytes)(32));
}
function createPkceCodeChallenge(verifier, method) {
if (method === "plain") {
return verifier;
}
const hash = encodeForPkce((0, import_node_crypto.createHash)("sha256").update(verifier).digest());
return hash.replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
}
function encodeForPkce(bytes) {
return bytes.toString("base64").replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
}
// src/grants/clientCredentials.ts
async function getClientCredentials(ctx, contextId, {
accessTokenUrl,
clientId,
clientSecret,
scope,
credentialsInBody
}) {
const token = await getToken(ctx, contextId);
if (token) {
}
const response = await getAccessToken(ctx, {
grantType: "client_credentials",
accessTokenUrl,
clientId,
clientSecret,
scope,
credentialsInBody,
params: []
});
return storeToken(ctx, contextId, response);
}
// src/grants/implicit.ts
function getImplicit(ctx, contextId, {
authorizationUrl: authorizationUrlRaw,
responseType,
clientId,
redirectUri,
scope,
state
}) {
return new Promise(async (resolve, reject) => {
const token = await getToken(ctx, contextId);
if (token) {
}
const authorizationUrl = new URL(`${authorizationUrlRaw ?? ""}`);
authorizationUrl.searchParams.set("response_type", "code");
authorizationUrl.searchParams.set("client_id", clientId);
if (redirectUri) authorizationUrl.searchParams.set("redirect_uri", redirectUri);
if (scope) authorizationUrl.searchParams.set("scope", scope);
if (state) authorizationUrl.searchParams.set("state", state);
if (responseType.includes("id_token")) {
authorizationUrl.searchParams.set("nonce", String(Math.floor(Math.random() * 9999999999999) + 1));
}
const authorizationUrlStr = authorizationUrl.toString();
let { close } = await ctx.window.openUrl({
url: authorizationUrlStr,
label: "oauth-authorization-url",
async onNavigate({ url: urlStr }) {
const url = new URL(urlStr);
if (url.searchParams.has("error")) {
return reject(Error(`Failed to authorize: ${url.searchParams.get("error")}`));
}
close();
const hash = url.hash.slice(1);
const params = new URLSearchParams(hash);
const idToken = params.get("id_token");
if (idToken) {
params.set("access_token", idToken);
params.delete("id_token");
}
const response = Object.fromEntries(params);
try {
resolve(await storeToken(ctx, contextId, response));
} catch (err) {
reject(err);
}
}
});
});
}
// src/grants/password.ts
async function getPassword(ctx, contextId, {
accessTokenUrl,
clientId,
clientSecret,
username,
password,
credentialsInBody,
scope
}) {
const token = await getOrRefreshAccessToken(ctx, contextId, {
accessTokenUrl,
scope,
clientId,
clientSecret,
credentialsInBody
});
if (token != null) {
return token;
}
const response = await getAccessToken(ctx, {
accessTokenUrl,
clientId,
clientSecret,
scope,
grantType: "password",
credentialsInBody,
params: [
{ name: "username", value: username },
{ name: "password", value: password }
]
});
return storeToken(ctx, contextId, response);
}
// src/index.ts
var grantTypes = [
{ label: "Authorization Code", value: "authorization_code" },
{ label: "Implicit", value: "implicit" },
{ label: "Resource Owner Password Credential", value: "password" },
{ label: "Client Credentials", value: "client_credentials" }
];
var defaultGrantType = grantTypes[0].value;
function hiddenIfNot(grantTypes2, ...other) {
return (_ctx, { values }) => {
const hasGrantType = grantTypes2.find((t) => t === String(values.grantType ?? defaultGrantType));
const hasOtherBools = other.every((t) => t(values));
const show = hasGrantType && hasOtherBools;
return { hidden: !show };
};
}
var authorizationUrls = [
"https://github.com/login/oauth/authorize",
"https://account.box.com/api/oauth2/authorize",
"https://accounts.google.com/o/oauth2/v2/auth",
"https://api.imgur.com/oauth2/authorize",
"https://bitly.com/oauth/authorize",
"https://gitlab.example.com/oauth/authorize",
"https://medium.com/m/oauth/authorize",
"https://public-api.wordpress.com/oauth2/authorize",
"https://slack.com/oauth/authorize",
"https://todoist.com/oauth/authorize",
"https://www.dropbox.com/oauth2/authorize",
"https://www.linkedin.com/oauth/v2/authorization",
"https://MY_SHOP.myshopify.com/admin/oauth/access_token"
];
var accessTokenUrls = [
"https://github.com/login/oauth/access_token",
"https://api-ssl.bitly.com/oauth/access_token",
"https://api.box.com/oauth2/token",
"https://api.dropboxapi.com/oauth2/token",
"https://api.imgur.com/oauth2/token",
"https://api.medium.com/v1/tokens",
"https://gitlab.example.com/oauth/token",
"https://public-api.wordpress.com/oauth2/token",
"https://slack.com/api/oauth.access",
"https://todoist.com/oauth/access_token",
"https://www.googleapis.com/oauth2/v4/token",
"https://www.linkedin.com/oauth/v2/accessToken",
"https://MY_SHOP.myshopify.com/admin/oauth/authorize"
];
var plugin = {
authentication: {
name: "oauth2",
label: "OAuth 2.0",
shortLabel: "OAuth 2",
actions: [
{
label: "Copy Current Token",
name: "copyCurrentToken",
icon: "copy",
async onSelect(ctx, { contextId }) {
const token = await getToken(ctx, contextId);
if (token == null) {
await ctx.toast.show({ message: "No token to copy", color: "warning" });
} else {
await ctx.clipboard.copyText(token.response.access_token);
await ctx.toast.show({ message: "Token copied to clipboard", icon: "copy", color: "success" });
}
}
},
{
label: "Delete Token",
name: "clearToken",
icon: "trash",
async onSelect(ctx, { contextId }) {
if (await deleteToken(ctx, contextId)) {
await ctx.toast.show({ message: "Token deleted", color: "success" });
} else {
await ctx.toast.show({ message: "No token to delete", color: "warning" });
}
}
}
],
args: [
{
type: "select",
name: "grantType",
label: "Grant Type",
hideLabel: true,
defaultValue: defaultGrantType,
options: grantTypes
},
// Always-present fields
{ type: "text", name: "clientId", label: "Client ID" },
{
type: "text",
name: "clientSecret",
label: "Client Secret",
password: true,
dynamic: hiddenIfNot(["authorization_code", "password", "client_credentials"])
},
{
type: "text",
name: "authorizationUrl",
label: "Authorization URL",
dynamic: hiddenIfNot(["authorization_code", "implicit"]),
placeholder: authorizationUrls[0],
completionOptions: authorizationUrls.map((url) => ({ label: url, value: url }))
},
{
type: "text",
name: "accessTokenUrl",
label: "Access Token URL",
placeholder: accessTokenUrls[0],
dynamic: hiddenIfNot(["authorization_code", "password", "client_credentials"]),
completionOptions: accessTokenUrls.map((url) => ({ label: url, value: url }))
},
{
type: "text",
name: "redirectUri",
label: "Redirect URI",
optional: true,
dynamic: hiddenIfNot(["authorization_code", "implicit"])
},
{
type: "text",
name: "state",
label: "State",
optional: true,
dynamic: hiddenIfNot(["authorization_code", "implicit"])
},
{
type: "checkbox",
name: "usePkce",
label: "Use PKCE",
dynamic: hiddenIfNot(["authorization_code"])
},
{
type: "select",
name: "pkceChallengeMethod",
label: "Code Challenge Method",
options: [{ label: "SHA-256", value: PKCE_SHA256 }, { label: "Plain", value: PKCE_PLAIN }],
defaultValue: DEFAULT_PKCE_METHOD,
dynamic: hiddenIfNot(["authorization_code"], ({ usePkce }) => !!usePkce)
},
{
type: "text",
name: "pkceCodeVerifier",
label: "Code Verifier",
placeholder: "Automatically generated if not provided",
optional: true,
dynamic: hiddenIfNot(["authorization_code"], ({ usePkce }) => !!usePkce)
},
{
type: "text",
name: "username",
label: "Username",
optional: true,
dynamic: hiddenIfNot(["password"])
},
{
type: "text",
name: "password",
label: "Password",
password: true,
optional: true,
dynamic: hiddenIfNot(["password"])
},
{
type: "select",
name: "responseType",
label: "Response Type",
defaultValue: "token",
options: [
{ label: "Access Token", value: "token" },
{ label: "ID Token", value: "id_token" },
{ label: "ID and Access Token", value: "id_token token" }
],
dynamic: hiddenIfNot(["implicit"])
},
{
type: "accordion",
label: "Advanced",
inputs: [
{ type: "text", name: "scope", label: "Scope", optional: true },
{ type: "text", name: "headerPrefix", label: "Header Prefix", optional: true, defaultValue: "Bearer" },
{
type: "select",
name: "credentials",
label: "Send Credentials",
defaultValue: "body",
options: [
{ label: "In Request Body", value: "body" },
{ label: "As Basic Authentication", value: "basic" }
]
}
]
},
{
type: "accordion",
label: "Access Token Response",
async dynamic(ctx, { contextId }) {
const token = await getToken(ctx, contextId);
if (token == null) {
return { hidden: true };
}
return {
label: "Access Token Response",
inputs: [
{
type: "editor",
defaultValue: JSON.stringify(token.response, null, 2),
hideLabel: true,
readOnly: true,
language: "json"
}
]
};
}
}
],
async onApply(ctx, { values, contextId }) {
const headerPrefix = optionalString(values, "headerPrefix") ?? "";
const grantType = requiredString(values, "grantType");
const credentialsInBody = values.credentials === "body";
console.log("Performing OAuth", values);
let token;
if (grantType === "authorization_code") {
const authorizationUrl = requiredString(values, "authorizationUrl");
const accessTokenUrl = requiredString(values, "accessTokenUrl");
token = await getAuthorizationCode(ctx, contextId, {
accessTokenUrl: accessTokenUrl.match(/^https?:\/\//) ? accessTokenUrl : `https://${accessTokenUrl}`,
authorizationUrl: authorizationUrl.match(/^https?:\/\//) ? authorizationUrl : `https://${authorizationUrl}`,
clientId: requiredString(values, "clientId"),
clientSecret: requiredString(values, "clientSecret"),
redirectUri: optionalString(values, "redirectUri"),
scope: optionalString(values, "scope"),
state: optionalString(values, "state"),
credentialsInBody,
pkce: values.usePkce ? {
challengeMethod: requiredString(values, "pkceChallengeMethod"),
codeVerifier: optionalString(values, "pkceCodeVerifier")
} : null
});
} else if (grantType === "implicit") {
const authorizationUrl = requiredString(values, "authorizationUrl");
token = await getImplicit(ctx, contextId, {
authorizationUrl: authorizationUrl.match(/^https?:\/\//) ? authorizationUrl : `https://${authorizationUrl}`,
clientId: requiredString(values, "clientId"),
redirectUri: optionalString(values, "redirectUri"),
responseType: requiredString(values, "responseType"),
scope: optionalString(values, "scope"),
state: optionalString(values, "state")
});
} else if (grantType === "client_credentials") {
const accessTokenUrl = requiredString(values, "accessTokenUrl");
token = await getClientCredentials(ctx, contextId, {
accessTokenUrl: accessTokenUrl.match(/^https?:\/\//) ? accessTokenUrl : `https://${accessTokenUrl}`,
clientId: requiredString(values, "clientId"),
clientSecret: requiredString(values, "clientSecret"),
scope: optionalString(values, "scope"),
credentialsInBody
});
} else if (grantType === "password") {
const accessTokenUrl = requiredString(values, "accessTokenUrl");
token = await getPassword(ctx, contextId, {
accessTokenUrl: accessTokenUrl.match(/^https?:\/\//) ? accessTokenUrl : `https://${accessTokenUrl}`,
clientId: requiredString(values, "clientId"),
clientSecret: requiredString(values, "clientSecret"),
username: requiredString(values, "username"),
password: requiredString(values, "password"),
scope: optionalString(values, "scope"),
credentialsInBody
});
} else {
throw new Error("Invalid grant type " + grantType);
}
const headerValue = `${headerPrefix} ${token.response.access_token}`.trim();
return {
setHeaders: [{
name: "Authorization",
value: headerValue
}]
};
}
}
};
function optionalString(values, name) {
const arg = values[name];
if (arg == null || arg == "") return null;
return `${arg}`;
}
function requiredString(values, name) {
const arg = optionalString(values, name);
if (!arg) throw new Error(`Missing required argument ${name}`);
return arg;
}
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
plugin
});
+9
View File
@@ -0,0 +1,9 @@
{
"name": "@yaakapp/auth-oauth2",
"private": true,
"version": "0.0.1",
"scripts": {
"build": "yaakcli build ./src/index.ts",
"dev": "yaakcli dev ./src/index.js"
}
}
+3 -3
View File
@@ -27,14 +27,14 @@ module.exports = __toCommonJS(src_exports);
var NEWLINE = "\\\n "; var NEWLINE = "\\\n ";
var plugin = { var plugin = {
httpRequestActions: [{ httpRequestActions: [{
key: "export-curl", name: "export-curl",
label: "Copy as Curl", label: "Copy as Curl",
icon: "copy", icon: "copy",
async onSelect(ctx, args) { async onSelect(ctx, args) {
const rendered_request = await ctx.httpRequest.render({ httpRequest: args.httpRequest, purpose: "preview" }); const rendered_request = await ctx.httpRequest.render({ httpRequest: args.httpRequest, purpose: "preview" });
const data = await convertToCurl(rendered_request); const data = await convertToCurl(rendered_request);
ctx.clipboard.copyText(data); await ctx.clipboard.copyText(data);
ctx.toast.show({ message: "Curl copied to clipboard", icon: "copy" }); await ctx.toast.show({ message: "Curl copied to clipboard", icon: "copy", color: "success" });
} }
}] }]
}; };
@@ -8824,8 +8824,8 @@ var behaviorArg = {
label: "Sending Behavior", label: "Sending Behavior",
defaultValue: "smart", defaultValue: "smart",
options: [ options: [
{ name: "When no responses", value: "smart" }, { label: "When no responses", value: "smart" },
{ name: "Always", value: "always" } { label: "Always", value: "always" }
] ]
}; };
var requestArg = { var requestArg = {
@@ -0,0 +1,3 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type CheckActivationRequestPayload = { activationId: string, };
+1 -1
View File
@@ -15,7 +15,7 @@ const TRIAL_SECONDS: u64 = 3600 * 24 * 14;
#[derive(Debug, Clone, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[ts(export, export_to = "models.ts")] #[ts(export, export_to = "gen_models.ts")]
pub struct CheckActivationRequestPayload { pub struct CheckActivationRequestPayload {
pub activation_id: String, pub activation_id: String,
} }
+2 -2
View File
@@ -15,7 +15,7 @@ sea-query = { version = "0.32.1", features = ["with-chrono", "attr"] }
sea-query-rusqlite = { version = "0.7.0", features = ["with-chrono"] } sea-query-rusqlite = { version = "0.7.0", features = ["with-chrono"] }
serde = { version = "1.0.204", features = ["derive"] } serde = { version = "1.0.204", features = ["derive"] }
serde_json = "1.0.122" serde_json = "1.0.122"
sqlx = { version = "0.8.0", features = ["sqlite", "runtime-tokio-rustls"] } sqlx = { version = "0.8.0", default-features = false, features = ["migrate", "sqlite", "runtime-tokio-rustls"] }
tauri = { workspace = true } tauri = { workspace = true }
thiserror = "1.0.63" thiserror = "2.0.11"
ts-rs = { workspace = true, features = ["chrono-impl", "serde-json-impl"] } ts-rs = { workspace = true, features = ["chrono-impl", "serde-json-impl"] }
+1 -1
View File
@@ -1 +1 @@
export * from './bindings/models'; export * from './bindings/gen_models';
+72 -30
View File
@@ -10,7 +10,7 @@ use ts_rs::TS;
#[derive(Debug, Clone, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase", tag = "type")] #[serde(rename_all = "camelCase", tag = "type")]
#[ts(export, export_to = "models.ts")] #[ts(export, export_to = "gen_models.ts")]
pub enum ProxySetting { pub enum ProxySetting {
Enabled { Enabled {
http: String, http: String,
@@ -22,7 +22,7 @@ pub enum ProxySetting {
#[derive(Debug, Clone, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[ts(export, export_to = "models.ts")] #[ts(export, export_to = "gen_models.ts")]
pub struct ProxySettingAuth { pub struct ProxySettingAuth {
pub user: String, pub user: String,
pub password: String, pub password: String,
@@ -30,7 +30,7 @@ pub struct ProxySettingAuth {
#[derive(Debug, Clone, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
#[ts(export, export_to = "models.ts")] #[ts(export, export_to = "gen_models.ts")]
pub enum EditorKeymap { pub enum EditorKeymap {
Default, Default,
Vim, Vim,
@@ -72,7 +72,7 @@ impl Default for EditorKeymap {
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)] #[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "models.ts")] #[ts(export, export_to = "gen_models.ts")]
pub struct Settings { pub struct Settings {
#[ts(type = "\"settings\"")] #[ts(type = "\"settings\"")]
pub model: String, pub model: String,
@@ -149,7 +149,7 @@ impl<'s> TryFrom<&Row<'s>> for Settings {
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)] #[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "models.ts")] #[ts(export, export_to = "gen_models.ts")]
pub struct Workspace { pub struct Workspace {
#[ts(type = "\"workspace\"")] #[ts(type = "\"workspace\"")]
pub model: String, pub model: String,
@@ -215,7 +215,7 @@ impl Workspace {
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)] #[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "models.ts")] #[ts(export, export_to = "gen_models.ts")]
pub struct WorkspaceMeta { pub struct WorkspaceMeta {
#[ts(type = "\"workspace_meta\"")] #[ts(type = "\"workspace_meta\"")]
pub model: String, pub model: String,
@@ -255,7 +255,7 @@ impl<'s> TryFrom<&Row<'s>> for WorkspaceMeta {
} }
#[derive(Debug, Clone, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[ts(export, export_to = "models.ts")] #[ts(export, export_to = "gen_models.ts")]
enum CookieDomain { enum CookieDomain {
HostOnly(String), HostOnly(String),
Suffix(String), Suffix(String),
@@ -264,14 +264,14 @@ enum CookieDomain {
} }
#[derive(Debug, Clone, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[ts(export, export_to = "models.ts")] #[ts(export, export_to = "gen_models.ts")]
enum CookieExpires { enum CookieExpires {
AtUtc(String), AtUtc(String),
SessionEnd, SessionEnd,
} }
#[derive(Debug, Clone, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[ts(export, export_to = "models.ts")] #[ts(export, export_to = "gen_models.ts")]
pub struct Cookie { pub struct Cookie {
raw_cookie: String, raw_cookie: String,
domain: CookieDomain, domain: CookieDomain,
@@ -281,7 +281,7 @@ pub struct Cookie {
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)] #[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "models.ts")] #[ts(export, export_to = "gen_models.ts")]
pub struct CookieJar { pub struct CookieJar {
#[ts(type = "\"cookie_jar\"")] #[ts(type = "\"cookie_jar\"")]
pub model: String, pub model: String,
@@ -327,7 +327,7 @@ impl<'s> TryFrom<&Row<'s>> for CookieJar {
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)] #[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "models.ts")] #[ts(export, export_to = "gen_models.ts")]
pub struct Environment { pub struct Environment {
#[ts(type = "\"environment\"")] #[ts(type = "\"environment\"")]
pub model: String, pub model: String,
@@ -376,7 +376,7 @@ impl<'s> TryFrom<&Row<'s>> for Environment {
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)] #[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "models.ts")] #[ts(export, export_to = "gen_models.ts")]
pub struct EnvironmentVariable { pub struct EnvironmentVariable {
#[serde(default = "default_true")] #[serde(default = "default_true")]
#[ts(optional, as = "Option<bool>")] #[ts(optional, as = "Option<bool>")]
@@ -389,7 +389,7 @@ pub struct EnvironmentVariable {
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)] #[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "models.ts")] #[ts(export, export_to = "gen_models.ts")]
pub struct Folder { pub struct Folder {
#[ts(type = "\"folder\"")] #[ts(type = "\"folder\"")]
pub model: String, pub model: String,
@@ -440,7 +440,7 @@ impl<'s> TryFrom<&Row<'s>> for Folder {
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)] #[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "models.ts")] #[ts(export, export_to = "gen_models.ts")]
pub struct HttpRequestHeader { pub struct HttpRequestHeader {
#[serde(default = "default_true")] #[serde(default = "default_true")]
#[ts(optional, as = "Option<bool>")] #[ts(optional, as = "Option<bool>")]
@@ -453,7 +453,7 @@ pub struct HttpRequestHeader {
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)] #[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "models.ts")] #[ts(export, export_to = "gen_models.ts")]
pub struct HttpUrlParameter { pub struct HttpUrlParameter {
#[serde(default = "default_true")] #[serde(default = "default_true")]
#[ts(optional, as = "Option<bool>")] #[ts(optional, as = "Option<bool>")]
@@ -466,7 +466,7 @@ pub struct HttpUrlParameter {
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)] #[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "models.ts")] #[ts(export, export_to = "gen_models.ts")]
pub struct HttpRequest { pub struct HttpRequest {
#[ts(type = "\"http_request\"")] #[ts(type = "\"http_request\"")]
pub model: String, pub model: String,
@@ -548,7 +548,7 @@ impl<'s> TryFrom<&Row<'s>> for HttpRequest {
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)] #[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "models.ts")] #[ts(export, export_to = "gen_models.ts")]
pub struct HttpResponseHeader { pub struct HttpResponseHeader {
pub name: String, pub name: String,
pub value: String, pub value: String,
@@ -556,7 +556,7 @@ pub struct HttpResponseHeader {
#[derive(Debug, Clone, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
#[ts(export, export_to = "models.ts")] #[ts(export, export_to = "gen_models.ts")]
pub enum HttpResponseState { pub enum HttpResponseState {
Initialized, Initialized,
Connected, Connected,
@@ -571,7 +571,7 @@ impl Default for HttpResponseState {
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)] #[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "models.ts")] #[ts(export, export_to = "gen_models.ts")]
pub struct HttpResponse { pub struct HttpResponse {
#[ts(type = "\"http_response\"")] #[ts(type = "\"http_response\"")]
pub model: String, pub model: String,
@@ -660,7 +660,7 @@ impl HttpResponse {
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)] #[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "models.ts")] #[ts(export, export_to = "gen_models.ts")]
pub struct GrpcMetadataEntry { pub struct GrpcMetadataEntry {
#[serde(default = "default_true")] #[serde(default = "default_true")]
#[ts(optional, as = "Option<bool>")] #[ts(optional, as = "Option<bool>")]
@@ -673,7 +673,7 @@ pub struct GrpcMetadataEntry {
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)] #[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "models.ts")] #[ts(export, export_to = "gen_models.ts")]
pub struct GrpcRequest { pub struct GrpcRequest {
#[ts(type = "\"grpc_request\"")] #[ts(type = "\"grpc_request\"")]
pub model: String, pub model: String,
@@ -748,7 +748,7 @@ impl<'s> TryFrom<&Row<'s>> for GrpcRequest {
#[derive(Debug, Clone, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
#[ts(export, export_to = "models.ts")] #[ts(export, export_to = "gen_models.ts")]
pub enum GrpcConnectionState { pub enum GrpcConnectionState {
Initialized, Initialized,
Connected, Connected,
@@ -763,7 +763,7 @@ impl Default for GrpcConnectionState {
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)] #[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "models.ts")] #[ts(export, export_to = "gen_models.ts")]
pub struct GrpcConnection { pub struct GrpcConnection {
#[ts(type = "\"grpc_connection\"")] #[ts(type = "\"grpc_connection\"")]
pub model: String, pub model: String,
@@ -831,7 +831,7 @@ impl<'s> TryFrom<&Row<'s>> for GrpcConnection {
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, TS)] #[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, TS)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
#[ts(export, export_to = "models.ts")] #[ts(export, export_to = "gen_models.ts")]
pub enum GrpcEventType { pub enum GrpcEventType {
Info, Info,
Error, Error,
@@ -849,7 +849,7 @@ impl Default for GrpcEventType {
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)] #[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "models.ts")] #[ts(export, export_to = "gen_models.ts")]
pub struct GrpcEvent { pub struct GrpcEvent {
#[ts(type = "\"grpc_event\"")] #[ts(type = "\"grpc_event\"")]
pub model: String, pub model: String,
@@ -911,7 +911,7 @@ impl<'s> TryFrom<&Row<'s>> for GrpcEvent {
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)] #[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "models.ts")] #[ts(export, export_to = "gen_models.ts")]
pub struct Plugin { pub struct Plugin {
#[ts(type = "\"plugin\"")] #[ts(type = "\"plugin\"")]
pub model: String, pub model: String,
@@ -959,7 +959,7 @@ impl<'s> TryFrom<&Row<'s>> for Plugin {
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)] #[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "models.ts")] #[ts(export, export_to = "gen_models.ts")]
pub struct SyncState { pub struct SyncState {
#[ts(type = "\"sync_state\"")] #[ts(type = "\"sync_state\"")]
pub model: String, pub model: String,
@@ -977,7 +977,7 @@ pub struct SyncState {
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)] #[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "models.ts")] #[ts(export, export_to = "gen_models.ts")]
pub struct SyncHistory { pub struct SyncHistory {
#[ts(type = "\"sync_history\"")] #[ts(type = "\"sync_history\"")]
pub model: String, pub model: String,
@@ -1029,7 +1029,7 @@ impl<'s> TryFrom<&Row<'s>> for SyncState {
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)] #[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "models.ts")] #[ts(export, export_to = "gen_models.ts")]
pub struct KeyValue { pub struct KeyValue {
#[ts(type = "\"key_value\"")] #[ts(type = "\"key_value\"")]
pub model: String, pub model: String,
@@ -1069,6 +1069,48 @@ impl<'s> TryFrom<&Row<'s>> for KeyValue {
} }
} }
#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_models.ts")]
pub struct PluginKeyValue {
#[ts(type = "\"plugin_key_value\"")]
pub model: String,
pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime,
pub plugin_name: String,
pub key: String,
pub value: String,
}
#[derive(Iden)]
pub enum PluginKeyValueIden {
#[iden = "plugin_key_values"]
Table,
Model,
CreatedAt,
UpdatedAt,
PluginName,
Key,
Value,
}
impl<'s> TryFrom<&Row<'s>> for PluginKeyValue {
type Error = rusqlite::Error;
fn try_from(r: &Row<'s>) -> Result<Self, Self::Error> {
Ok(PluginKeyValue {
model: r.get("model")?,
created_at: r.get("created_at")?,
updated_at: r.get("updated_at")?,
plugin_name: r.get("plugin_name")?,
key: r.get("key")?,
value: r.get("value")?,
})
}
}
fn default_true() -> bool { fn default_true() -> bool {
true true
} }
@@ -1114,7 +1156,7 @@ impl ModelType {
#[derive(Debug, Clone, Serialize, TS)] #[derive(Debug, Clone, Serialize, TS)]
#[serde(rename_all = "camelCase", untagged)] #[serde(rename_all = "camelCase", untagged)]
#[ts(export, export_to = "models.ts")] #[ts(export, export_to = "gen_models.ts")]
pub enum AnyModel { pub enum AnyModel {
CookieJar(CookieJar), CookieJar(CookieJar),
Environment(Environment), Environment(Environment),
+87 -10
View File
@@ -1,13 +1,6 @@
use crate::error::Error::ModelNotFound; use crate::error::Error::ModelNotFound;
use crate::error::Result; use crate::error::Result;
use crate::models::{ use crate::models::{AnyModel, CookieJar, CookieJarIden, Environment, EnvironmentIden, Folder, FolderIden, GrpcConnection, GrpcConnectionIden, GrpcConnectionState, GrpcEvent, GrpcEventIden, GrpcRequest, GrpcRequestIden, HttpRequest, HttpRequestIden, HttpResponse, HttpResponseHeader, HttpResponseIden, HttpResponseState, KeyValue, KeyValueIden, ModelType, Plugin, PluginIden, PluginKeyValue, PluginKeyValueIden, Settings, SettingsIden, SyncState, SyncStateIden, Workspace, WorkspaceIden, WorkspaceMeta, WorkspaceMetaIden};
AnyModel, CookieJar, CookieJarIden, Environment, EnvironmentIden, Folder, FolderIden,
GrpcConnection, GrpcConnectionIden, GrpcConnectionState, GrpcEvent, GrpcEventIden, GrpcRequest,
GrpcRequestIden, HttpRequest, HttpRequestIden, HttpResponse, HttpResponseHeader,
HttpResponseIden, HttpResponseState, KeyValue, KeyValueIden, ModelType, Plugin, PluginIden,
Settings, SettingsIden, SyncState, SyncStateIden, Workspace, WorkspaceIden, WorkspaceMeta,
WorkspaceMetaIden,
};
use crate::plugin::SqliteConnection; use crate::plugin::SqliteConnection;
use chrono::{NaiveDateTime, Utc}; use chrono::{NaiveDateTime, Utc};
use log::{debug, error, info, warn}; use log::{debug, error, info, warn};
@@ -165,6 +158,90 @@ pub async fn get_key_value_raw<R: Runtime>(
db.query_row(sql.as_str(), &*params.as_params(), |row| row.try_into()).ok() db.query_row(sql.as_str(), &*params.as_params(), |row| row.try_into()).ok()
} }
pub async fn get_plugin_key_value<R: Runtime>(
mgr: &impl Manager<R>,
plugin_name: &str,
key: &str,
) -> Option<PluginKeyValue> {
let dbm = &*mgr.state::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap();
let (sql, params) = Query::select()
.from(PluginKeyValueIden::Table)
.column(Asterisk)
.cond_where(
Cond::all()
.add(Expr::col(PluginKeyValueIden::PluginName).eq(plugin_name))
.add(Expr::col(PluginKeyValueIden::Key).eq(key)),
)
.build_rusqlite(SqliteQueryBuilder);
db.query_row(sql.as_str(), &*params.as_params(), |row| row.try_into()).ok()
}
pub async fn set_plugin_key_value<R: Runtime>(
mgr: &impl Manager<R>,
plugin_name: &str,
key: &str,
value: &str,
) -> (PluginKeyValue, bool) {
let existing = get_plugin_key_value(mgr, plugin_name, key).await;
let dbm = &*mgr.state::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap();
let (sql, params) = Query::insert()
.into_table(PluginKeyValueIden::Table)
.columns([
PluginKeyValueIden::CreatedAt,
PluginKeyValueIden::UpdatedAt,
PluginKeyValueIden::PluginName,
PluginKeyValueIden::Key,
PluginKeyValueIden::Value,
])
.values_panic([
CurrentTimestamp.into(),
CurrentTimestamp.into(),
plugin_name.into(),
key.into(),
value.into(),
])
.on_conflict(
OnConflict::new()
.update_columns([PluginKeyValueIden::UpdatedAt, PluginKeyValueIden::Value])
.to_owned(),
)
.returning_all()
.build_rusqlite(SqliteQueryBuilder);
let mut stmt = db.prepare(sql.as_str()).expect("Failed to prepare PluginKeyValue upsert");
let m: PluginKeyValue = stmt
.query_row(&*params.as_params(), |row| row.try_into())
.expect("Failed to upsert KeyValue");
(m, existing.is_none())
}
pub async fn delete_plugin_key_value<R: Runtime>(
mgr: &impl Manager<R>,
plugin_name: &str,
key: &str,
) -> bool {
if let None = get_plugin_key_value(mgr, plugin_name, key).await {
return false;
}
let dbm = &*mgr.state::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap();
let (sql, params) = Query::delete()
.from_table(PluginKeyValueIden::Table)
.cond_where(
Cond::all()
.add(Expr::col(PluginKeyValueIden::PluginName).eq(plugin_name))
.add(Expr::col(PluginKeyValueIden::Key).eq(key)),
)
.build_rusqlite(SqliteQueryBuilder);
db.execute(sql.as_str(), &*params.as_params()).expect("Failed to delete PluginKeyValue");
true
}
pub async fn list_workspaces<R: Runtime>(mgr: &impl Manager<R>) -> Result<Vec<Workspace>> { pub async fn list_workspaces<R: Runtime>(mgr: &impl Manager<R>) -> Result<Vec<Workspace>> {
let dbm = &*mgr.state::<SqliteConnection>(); let dbm = &*mgr.state::<SqliteConnection>();
let db = dbm.0.lock().await.get().unwrap(); let db = dbm.0.lock().await.get().unwrap();
@@ -1999,7 +2076,7 @@ pub fn generate_id() -> String {
#[derive(Debug, Clone, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[ts(export, export_to = "models.ts")] #[ts(export, export_to = "gen_models.ts")]
pub struct ModelPayload { pub struct ModelPayload {
pub model: AnyModel, pub model: AnyModel,
pub window_label: String, pub window_label: String,
@@ -2008,7 +2085,7 @@ pub struct ModelPayload {
#[derive(Debug, Clone, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
#[ts(export, export_to = "models.ts")] #[ts(export, export_to = "gen_models.ts")]
pub enum UpdateSource { pub enum UpdateSource {
Sync, Sync,
Window, Window,
+7 -6
View File
@@ -6,17 +6,18 @@ publish = false
[dependencies] [dependencies]
dunce = "1.0.4" dunce = "1.0.4"
futures-util = "0.3.30"
log = "0.4.21" log = "0.4.21"
md5 = "0.7.0"
path-slash = "0.2.1"
rand = "0.8.5" rand = "0.8.5"
regex = "1.10.6"
serde = { version = "1.0.198", features = ["derive"] } serde = { version = "1.0.198", features = ["derive"] }
serde_json = "1.0.113" serde_json = "1.0.113"
tauri = { workspace = true } tauri = { workspace = true }
tauri-plugin-shell = { workspace = true } tauri-plugin-shell = { workspace = true }
tokio = { version = "1.42.0", features = ["macros", "rt-multi-thread", "process"] }
ts-rs = { workspace = true, features = ["import-esm"] }
thiserror = "2.0.7" thiserror = "2.0.7"
yaak-models = { workspace = true } tokio = { version = "1.42.0", features = ["macros", "rt-multi-thread", "process"] }
regex = "1.10.6"
path-slash = "0.2.1"
tokio-tungstenite = "0.26.1" tokio-tungstenite = "0.26.1"
futures-util = "0.3.30" ts-rs = { workspace = true, features = ["import-esm"] }
yaak-models = { workspace = true }
-290
View File
@@ -1,290 +0,0 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { Environment } from "./models.js";
import type { Folder } from "./models.js";
import type { GrpcRequest } from "./models.js";
import type { HttpRequest } from "./models.js";
import type { HttpResponse } from "./models.js";
import type { JsonValue } from "./serde_json/JsonValue.js";
import type { Workspace } from "./models.js";
export type BootRequest = { dir: string, watch: boolean, };
export type BootResponse = { name: string, version: string, };
export type CallHttpAuthenticationRequest = { config: { [key in string]?: JsonValue }, method: string, url: string, headers: Array<HttpHeader>, };
export type CallHttpAuthenticationResponse = {
/**
* HTTP headers to add to the request. Existing headers will be replaced, while
* new headers will be added.
*/
setHeaders: Array<HttpHeader>, };
export type CallHttpRequestActionArgs = { httpRequest: HttpRequest, };
export type CallHttpRequestActionRequest = { key: string, pluginRefId: string, args: CallHttpRequestActionArgs, };
export type CallTemplateFunctionArgs = { purpose: RenderPurpose, values: { [key in string]?: string }, };
export type CallTemplateFunctionRequest = { name: string, args: CallTemplateFunctionArgs, };
export type CallTemplateFunctionResponse = { value: string | null, };
export type Color = "custom" | "default" | "primary" | "secondary" | "info" | "success" | "notice" | "warning" | "danger";
export type CopyTextRequest = { text: string, };
export type EditorLanguage = "text" | "javascript" | "json" | "html" | "xml" | "graphql" | "markdown";
export type EmptyPayload = {};
export type ErrorResponse = { error: string, };
export type ExportHttpRequestRequest = { httpRequest: HttpRequest, };
export type ExportHttpRequestResponse = { content: string, };
export type FileFilter = { name: string,
/**
* File extensions to require
*/
extensions: Array<string>, };
export type FilterRequest = { content: string, filter: string, };
export type FilterResponse = { content: string, };
export type FindHttpResponsesRequest = { requestId: string, limit?: number, };
export type FindHttpResponsesResponse = { httpResponses: Array<HttpResponse>, };
export type FormInput = { "type": "text" } & FormInputText | { "type": "editor" } & FormInputEditor | { "type": "select" } & FormInputSelect | { "type": "checkbox" } & FormInputCheckbox | { "type": "file" } & FormInputFile | { "type": "http_request" } & FormInputHttpRequest;
export type FormInputBase = { name: string,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, };
export type FormInputCheckbox = { name: string,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, };
export type FormInputEditor = {
/**
* Placeholder for the text input
*/
placeholder?: string | null,
/**
* Don't show the editor gutter (line numbers, folds, etc.)
*/
hideGutter?: boolean,
/**
* Language for syntax highlighting
*/
language?: EditorLanguage, name: string,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, };
export type FormInputFile = {
/**
* The title of the file selection window
*/
title: string,
/**
* Allow selecting multiple files
*/
multiple?: boolean, directory?: boolean, defaultPath?: string, filters?: Array<FileFilter>, name: string,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, };
export type FormInputHttpRequest = { name: string,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, };
export type FormInputSelect = {
/**
* The options that will be available in the select input
*/
options: Array<FormInputSelectOption>, name: string,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, };
export type FormInputSelectOption = { name: string, value: string, };
export type FormInputText = {
/**
* Placeholder for the text input
*/
placeholder?: string | null,
/**
* Placeholder for the text input
*/
password?: boolean, name: string,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, };
export type GetHttpAuthenticationResponse = { name: string, label: string, shortLabel: string, config: Array<FormInput>, };
export type GetHttpRequestActionsRequest = Record<string, never>;
export type GetHttpRequestActionsResponse = { actions: Array<HttpRequestAction>, pluginRefId: string, };
export type GetHttpRequestByIdRequest = { id: string, };
export type GetHttpRequestByIdResponse = { httpRequest: HttpRequest | null, };
export type GetTemplateFunctionsResponse = { functions: Array<TemplateFunction>, pluginRefId: string, };
export type HttpHeader = { name: string, value: string, };
export type HttpRequestAction = { key: string, label: string, icon?: Icon, };
export type Icon = "copy" | "info" | "check_circle" | "alert_triangle" | "_unknown";
export type ImportRequest = { content: string, };
export type ImportResources = { workspaces: Array<Workspace>, environments: Array<Environment>, folders: Array<Folder>, httpRequests: Array<HttpRequest>, grpcRequests: Array<GrpcRequest>, };
export type ImportResponse = { resources: ImportResources, };
export type InternalEvent = { id: string, pluginRefId: string, pluginName: string, replyId: string | null, windowContext: WindowContext, payload: InternalEventPayload, };
export type InternalEventPayload = { "type": "boot_request" } & BootRequest | { "type": "boot_response" } & BootResponse | { "type": "reload_request" } & EmptyPayload | { "type": "reload_response" } & EmptyPayload | { "type": "terminate_request" } | { "type": "terminate_response" } | { "type": "import_request" } & ImportRequest | { "type": "import_response" } & ImportResponse | { "type": "filter_request" } & FilterRequest | { "type": "filter_response" } & FilterResponse | { "type": "export_http_request_request" } & ExportHttpRequestRequest | { "type": "export_http_request_response" } & ExportHttpRequestResponse | { "type": "send_http_request_request" } & SendHttpRequestRequest | { "type": "send_http_request_response" } & SendHttpRequestResponse | { "type": "get_http_request_actions_request" } & EmptyPayload | { "type": "get_http_request_actions_response" } & GetHttpRequestActionsResponse | { "type": "call_http_request_action_request" } & CallHttpRequestActionRequest | { "type": "get_template_functions_request" } | { "type": "get_template_functions_response" } & GetTemplateFunctionsResponse | { "type": "call_template_function_request" } & CallTemplateFunctionRequest | { "type": "call_template_function_response" } & CallTemplateFunctionResponse | { "type": "get_http_authentication_request" } & EmptyPayload | { "type": "get_http_authentication_response" } & GetHttpAuthenticationResponse | { "type": "call_http_authentication_request" } & CallHttpAuthenticationRequest | { "type": "call_http_authentication_response" } & CallHttpAuthenticationResponse | { "type": "copy_text_request" } & CopyTextRequest | { "type": "render_http_request_request" } & RenderHttpRequestRequest | { "type": "render_http_request_response" } & RenderHttpRequestResponse | { "type": "template_render_request" } & TemplateRenderRequest | { "type": "template_render_response" } & TemplateRenderResponse | { "type": "show_toast_request" } & ShowToastRequest | { "type": "prompt_text_request" } & PromptTextRequest | { "type": "prompt_text_response" } & PromptTextResponse | { "type": "get_http_request_by_id_request" } & GetHttpRequestByIdRequest | { "type": "get_http_request_by_id_response" } & GetHttpRequestByIdResponse | { "type": "find_http_responses_request" } & FindHttpResponsesRequest | { "type": "find_http_responses_response" } & FindHttpResponsesResponse | { "type": "empty_response" } & EmptyPayload | { "type": "error_response" } & ErrorResponse;
export type PromptTextRequest = { id: string, title: string, label: string, description?: string, defaultValue?: string, placeholder?: string,
/**
* Text to add to the confirmation button
*/
confirmText?: string,
/**
* Text to add to the cancel button
*/
cancelText?: string,
/**
* Require the user to enter a non-empty value
*/
required?: boolean, };
export type PromptTextResponse = { value: string | null, };
export type RenderHttpRequestRequest = { httpRequest: HttpRequest, purpose: RenderPurpose, };
export type RenderHttpRequestResponse = { httpRequest: HttpRequest, };
export type RenderPurpose = "send" | "preview";
export type SendHttpRequestRequest = { httpRequest: HttpRequest, };
export type SendHttpRequestResponse = { httpResponse: HttpResponse, };
export type ShowToastRequest = { message: string, color?: Color, icon?: Icon, };
export type TemplateFunction = { name: string, description?: string,
/**
* Also support alternative names. This is useful for not breaking existing
* tags when changing the `name` property
*/
aliases?: Array<string>, args: Array<FormInput>, };
export type TemplateRenderRequest = { data: JsonValue, purpose: RenderPurpose, };
export type TemplateRenderResponse = { data: JsonValue, };
export type WindowContext = { "type": "none" } | { "type": "label", label: string, };
@@ -0,0 +1,405 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { Environment } from "./gen_models.js";
import type { Folder } from "./gen_models.js";
import type { GrpcRequest } from "./gen_models.js";
import type { HttpRequest } from "./gen_models.js";
import type { HttpResponse } from "./gen_models.js";
import type { JsonValue } from "./serde_json/JsonValue.js";
import type { Workspace } from "./gen_models.js";
export type BootRequest = { dir: string, watch: boolean, };
export type BootResponse = { name: string, version: string, };
export type CallHttpAuthenticationActionArgs = { contextId: string, values: { [key in string]?: JsonPrimitive }, };
export type CallHttpAuthenticationActionRequest = { index: number, pluginRefId: string, args: CallHttpAuthenticationActionArgs, };
export type CallHttpAuthenticationRequest = { contextId: string, values: { [key in string]?: JsonPrimitive }, method: string, url: string, headers: Array<HttpHeader>, };
export type CallHttpAuthenticationResponse = {
/**
* HTTP headers to add to the request. Existing headers will be replaced, while
* new headers will be added.
*/
setHeaders: Array<HttpHeader>, };
export type CallHttpRequestActionArgs = { httpRequest: HttpRequest, };
export type CallHttpRequestActionRequest = { index: number, pluginRefId: string, args: CallHttpRequestActionArgs, };
export type CallTemplateFunctionArgs = { purpose: RenderPurpose, values: { [key in string]?: string }, };
export type CallTemplateFunctionRequest = { name: string, args: CallTemplateFunctionArgs, };
export type CallTemplateFunctionResponse = { value: string | null, };
export type CloseWindowRequest = { label: string, };
export type Color = "primary" | "secondary" | "info" | "success" | "notice" | "warning" | "danger";
export type CompletionOptionType = "constant" | "variable";
export type Content = { "type": "text", content: string, } | { "type": "markdown", content: string, };
export type CopyTextRequest = { text: string, };
export type DeleteKeyValueRequest = { key: string, };
export type DeleteKeyValueResponse = { deleted: boolean, };
export type EditorLanguage = "text" | "javascript" | "json" | "html" | "xml" | "graphql" | "markdown";
export type EmptyPayload = {};
export type ErrorResponse = { error: string, };
export type ExportHttpRequestRequest = { httpRequest: HttpRequest, };
export type ExportHttpRequestResponse = { content: string, };
export type FileFilter = { name: string,
/**
* File extensions to require
*/
extensions: Array<string>, };
export type FilterRequest = { content: string, filter: string, };
export type FilterResponse = { content: string, };
export type FindHttpResponsesRequest = { requestId: string, limit?: number, };
export type FindHttpResponsesResponse = { httpResponses: Array<HttpResponse>, };
export type FormInput = { "type": "text" } & FormInputText | { "type": "editor" } & FormInputEditor | { "type": "select" } & FormInputSelect | { "type": "checkbox" } & FormInputCheckbox | { "type": "file" } & FormInputFile | { "type": "http_request" } & FormInputHttpRequest | { "type": "accordion" } & FormInputAccordion | { "type": "banner" } & FormInputBanner | { "type": "markdown" } & FormInputMarkdown;
export type FormInputAccordion = { label: string, inputs?: Array<FormInput>, hidden?: boolean, };
export type FormInputBanner = { inputs?: Array<FormInput>, hidden?: boolean, color?: Color, };
export type FormInputBase = {
/**
* The name of the input. The value will be stored at this object attribute in the resulting data
*/
name: string,
/**
* Whether this input is visible for the given configuration. Use this to
* make branching forms.
*/
hidden?: boolean,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, disabled?: boolean, };
export type FormInputCheckbox = {
/**
* The name of the input. The value will be stored at this object attribute in the resulting data
*/
name: string,
/**
* Whether this input is visible for the given configuration. Use this to
* make branching forms.
*/
hidden?: boolean,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, disabled?: boolean, };
export type FormInputEditor = {
/**
* Placeholder for the text input
*/
placeholder?: string | null,
/**
* Don't show the editor gutter (line numbers, folds, etc.)
*/
hideGutter?: boolean,
/**
* Language for syntax highlighting
*/
language?: EditorLanguage, readOnly?: boolean, completionOptions?: Array<GenericCompletionOption>,
/**
* The name of the input. The value will be stored at this object attribute in the resulting data
*/
name: string,
/**
* Whether this input is visible for the given configuration. Use this to
* make branching forms.
*/
hidden?: boolean,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, disabled?: boolean, };
export type FormInputFile = {
/**
* The title of the file selection window
*/
title: string,
/**
* Allow selecting multiple files
*/
multiple?: boolean, directory?: boolean, defaultPath?: string, filters?: Array<FileFilter>,
/**
* The name of the input. The value will be stored at this object attribute in the resulting data
*/
name: string,
/**
* Whether this input is visible for the given configuration. Use this to
* make branching forms.
*/
hidden?: boolean,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, disabled?: boolean, };
export type FormInputHttpRequest = {
/**
* The name of the input. The value will be stored at this object attribute in the resulting data
*/
name: string,
/**
* Whether this input is visible for the given configuration. Use this to
* make branching forms.
*/
hidden?: boolean,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, disabled?: boolean, };
export type FormInputMarkdown = { content: string, hidden?: boolean, };
export type FormInputSelect = {
/**
* The options that will be available in the select input
*/
options: Array<FormInputSelectOption>,
/**
* The name of the input. The value will be stored at this object attribute in the resulting data
*/
name: string,
/**
* Whether this input is visible for the given configuration. Use this to
* make branching forms.
*/
hidden?: boolean,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, disabled?: boolean, };
export type FormInputSelectOption = { label: string, value: string, };
export type FormInputText = {
/**
* Placeholder for the text input
*/
placeholder?: string | null,
/**
* Placeholder for the text input
*/
password?: boolean,
/**
* Whether to allow newlines in the input, like a <textarea/>
*/
multiLine?: boolean, completionOptions?: Array<GenericCompletionOption>,
/**
* The name of the input. The value will be stored at this object attribute in the resulting data
*/
name: string,
/**
* Whether this input is visible for the given configuration. Use this to
* make branching forms.
*/
hidden?: boolean,
/**
* Whether the user must fill in the argument
*/
optional?: boolean,
/**
* The label of the input
*/
label?: string,
/**
* Visually hide the label of the input
*/
hideLabel?: boolean,
/**
* The default value
*/
defaultValue?: string, disabled?: boolean, };
export type GenericCompletionOption = { label: string, detail?: string, info?: string, type?: CompletionOptionType, boost?: number, };
export type GetHttpAuthenticationConfigRequest = { contextId: string, values: { [key in string]?: JsonPrimitive }, };
export type GetHttpAuthenticationConfigResponse = { args: Array<FormInput>, pluginRefId: string, actions?: Array<HttpAuthenticationAction>, };
export type GetHttpAuthenticationSummaryResponse = { name: string, label: string, shortLabel: string, };
export type GetHttpRequestActionsRequest = Record<string, never>;
export type GetHttpRequestActionsResponse = { actions: Array<HttpRequestAction>, pluginRefId: string, };
export type GetHttpRequestByIdRequest = { id: string, };
export type GetHttpRequestByIdResponse = { httpRequest: HttpRequest | null, };
export type GetKeyValueRequest = { key: string, };
export type GetKeyValueResponse = { value?: string, };
export type GetTemplateFunctionsResponse = { functions: Array<TemplateFunction>, pluginRefId: string, };
export type HttpAuthenticationAction = { label: string, icon?: Icon, };
export type HttpHeader = { name: string, value: string, };
export type HttpRequestAction = { label: string, icon?: Icon, };
export type Icon = "alert_triangle" | "check" | "check_circle" | "chevron_down" | "copy" | "info" | "pin" | "search" | "trash" | "_unknown";
export type ImportRequest = { content: string, };
export type ImportResources = { workspaces: Array<Workspace>, environments: Array<Environment>, folders: Array<Folder>, httpRequests: Array<HttpRequest>, grpcRequests: Array<GrpcRequest>, };
export type ImportResponse = { resources: ImportResources, };
export type InternalEvent = { id: string, pluginRefId: string, pluginName: string, replyId: string | null, windowContext: WindowContext, payload: InternalEventPayload, };
export type InternalEventPayload = { "type": "boot_request" } & BootRequest | { "type": "boot_response" } & BootResponse | { "type": "reload_request" } & EmptyPayload | { "type": "reload_response" } & EmptyPayload | { "type": "terminate_request" } | { "type": "terminate_response" } | { "type": "import_request" } & ImportRequest | { "type": "import_response" } & ImportResponse | { "type": "filter_request" } & FilterRequest | { "type": "filter_response" } & FilterResponse | { "type": "export_http_request_request" } & ExportHttpRequestRequest | { "type": "export_http_request_response" } & ExportHttpRequestResponse | { "type": "send_http_request_request" } & SendHttpRequestRequest | { "type": "send_http_request_response" } & SendHttpRequestResponse | { "type": "get_http_request_actions_request" } & EmptyPayload | { "type": "get_http_request_actions_response" } & GetHttpRequestActionsResponse | { "type": "call_http_request_action_request" } & CallHttpRequestActionRequest | { "type": "get_template_functions_request" } | { "type": "get_template_functions_response" } & GetTemplateFunctionsResponse | { "type": "call_template_function_request" } & CallTemplateFunctionRequest | { "type": "call_template_function_response" } & CallTemplateFunctionResponse | { "type": "get_http_authentication_summary_request" } & EmptyPayload | { "type": "get_http_authentication_summary_response" } & GetHttpAuthenticationSummaryResponse | { "type": "get_http_authentication_config_request" } & GetHttpAuthenticationConfigRequest | { "type": "get_http_authentication_config_response" } & GetHttpAuthenticationConfigResponse | { "type": "call_http_authentication_request" } & CallHttpAuthenticationRequest | { "type": "call_http_authentication_response" } & CallHttpAuthenticationResponse | { "type": "call_http_authentication_action_request" } & CallHttpAuthenticationActionRequest | { "type": "call_http_authentication_action_response" } & EmptyPayload | { "type": "copy_text_request" } & CopyTextRequest | { "type": "copy_text_response" } & EmptyPayload | { "type": "render_http_request_request" } & RenderHttpRequestRequest | { "type": "render_http_request_response" } & RenderHttpRequestResponse | { "type": "get_key_value_request" } & GetKeyValueRequest | { "type": "get_key_value_response" } & GetKeyValueResponse | { "type": "set_key_value_request" } & SetKeyValueRequest | { "type": "set_key_value_response" } & SetKeyValueResponse | { "type": "delete_key_value_request" } & DeleteKeyValueRequest | { "type": "delete_key_value_response" } & DeleteKeyValueResponse | { "type": "open_window_request" } & OpenWindowRequest | { "type": "window_navigate_event" } & WindowNavigateEvent | { "type": "close_window_request" } & CloseWindowRequest | { "type": "template_render_request" } & TemplateRenderRequest | { "type": "template_render_response" } & TemplateRenderResponse | { "type": "show_toast_request" } & ShowToastRequest | { "type": "show_toast_response" } & EmptyPayload | { "type": "prompt_text_request" } & PromptTextRequest | { "type": "prompt_text_response" } & PromptTextResponse | { "type": "get_http_request_by_id_request" } & GetHttpRequestByIdRequest | { "type": "get_http_request_by_id_response" } & GetHttpRequestByIdResponse | { "type": "find_http_responses_request" } & FindHttpResponsesRequest | { "type": "find_http_responses_response" } & FindHttpResponsesResponse | { "type": "empty_response" } & EmptyPayload | { "type": "error_response" } & ErrorResponse;
export type JsonPrimitive = string | number | boolean | null;
export type OpenWindowRequest = { url: string,
/**
* Label for the window. If not provided, a random one will be generated.
*/
label: string, title?: string, size?: WindowSize, };
export type PromptTextRequest = { id: string, title: string, label: string, description?: string, defaultValue?: string, placeholder?: string,
/**
* Text to add to the confirmation button
*/
confirmText?: string,
/**
* Text to add to the cancel button
*/
cancelText?: string,
/**
* Require the user to enter a non-empty value
*/
required?: boolean, };
export type PromptTextResponse = { value: string | null, };
export type RenderHttpRequestRequest = { httpRequest: HttpRequest, purpose: RenderPurpose, };
export type RenderHttpRequestResponse = { httpRequest: HttpRequest, };
export type RenderPurpose = "send" | "preview";
export type SendHttpRequestRequest = { httpRequest: Partial<HttpRequest>, };
export type SendHttpRequestResponse = { httpResponse: HttpResponse, };
export type SetKeyValueRequest = { key: string, value: string, };
export type SetKeyValueResponse = {};
export type ShowToastRequest = { message: string, color?: Color, icon?: Icon, };
export type TemplateFunction = { name: string, description?: string,
/**
* Also support alternative names. This is useful for not breaking existing
* tags when changing the `name` property
*/
aliases?: Array<string>, args: Array<FormInput>, };
export type TemplateRenderRequest = { data: JsonValue, purpose: RenderPurpose, };
export type TemplateRenderResponse = { data: JsonValue, };
export type WindowContext = { "type": "none" } | { "type": "label", label: string, };
export type WindowNavigateEvent = { url: string, };
export type WindowSize = { width: number, height: number, };
+2 -2
View File
@@ -1,2 +1,2 @@
export * from './bindings/models'; export * from './bindings/gen_models';
export * from './bindings/events'; export * from './bindings/gen_events';
+3
View File
@@ -20,6 +20,9 @@ pub enum Error {
#[error("JSON error: {0}")] #[error("JSON error: {0}")]
JsonErr(#[from] serde_json::Error), JsonErr(#[from] serde_json::Error),
#[error("Timeout elapsed: {0}")]
TimeoutElapsed(#[from] tokio::time::error::Elapsed),
#[error("Plugin not found: {0}")] #[error("Plugin not found: {0}")]
PluginNotFoundErr(String), PluginNotFoundErr(String),
+333 -71
View File
@@ -1,5 +1,4 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap; use std::collections::HashMap;
use tauri::{Runtime, WebviewWindow}; use tauri::{Runtime, WebviewWindow};
use ts_rs::TS; use ts_rs::TS;
@@ -8,7 +7,7 @@ use yaak_models::models::{Environment, Folder, GrpcRequest, HttpRequest, HttpRes
#[derive(Debug, Clone, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")] #[ts(export, export_to = "gen_events.ts")]
pub struct InternalEvent { pub struct InternalEvent {
pub id: String, pub id: String,
pub plugin_ref_id: String, pub plugin_ref_id: String,
@@ -29,12 +28,12 @@ pub(crate) struct InternalEventRawPayload {
pub plugin_name: String, pub plugin_name: String,
pub reply_id: Option<String>, pub reply_id: Option<String>,
pub window_context: WindowContext, pub window_context: WindowContext,
pub payload: Value, pub payload: serde_json::Value,
} }
#[derive(Debug, Clone, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "snake_case", tag = "type")] #[serde(rename_all = "snake_case", tag = "type")]
#[ts(export, export_to = "events.ts")] #[ts(export, export_to = "gen_events.ts")]
pub enum WindowContext { pub enum WindowContext {
None, None,
Label { label: String }, Label { label: String },
@@ -50,7 +49,7 @@ impl WindowContext {
#[derive(Debug, Clone, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "snake_case", tag = "type")] #[serde(rename_all = "snake_case", tag = "type")]
#[ts(export, export_to = "events.ts")] #[ts(export, export_to = "gen_events.ts")]
pub enum InternalEventPayload { pub enum InternalEventPayload {
BootRequest(BootRequest), BootRequest(BootRequest),
BootResponse(BootResponse), BootResponse(BootResponse),
@@ -84,20 +83,38 @@ pub enum InternalEventPayload {
CallTemplateFunctionRequest(CallTemplateFunctionRequest), CallTemplateFunctionRequest(CallTemplateFunctionRequest),
CallTemplateFunctionResponse(CallTemplateFunctionResponse), CallTemplateFunctionResponse(CallTemplateFunctionResponse),
GetHttpAuthenticationRequest(EmptyPayload), // Http Authentication
GetHttpAuthenticationResponse(GetHttpAuthenticationResponse), GetHttpAuthenticationSummaryRequest(EmptyPayload),
GetHttpAuthenticationSummaryResponse(GetHttpAuthenticationSummaryResponse),
GetHttpAuthenticationConfigRequest(GetHttpAuthenticationConfigRequest),
GetHttpAuthenticationConfigResponse(GetHttpAuthenticationConfigResponse),
CallHttpAuthenticationRequest(CallHttpAuthenticationRequest), CallHttpAuthenticationRequest(CallHttpAuthenticationRequest),
CallHttpAuthenticationResponse(CallHttpAuthenticationResponse), CallHttpAuthenticationResponse(CallHttpAuthenticationResponse),
CallHttpAuthenticationActionRequest(CallHttpAuthenticationActionRequest),
CallHttpAuthenticationActionResponse(EmptyPayload),
CopyTextRequest(CopyTextRequest), CopyTextRequest(CopyTextRequest),
CopyTextResponse(EmptyPayload),
RenderHttpRequestRequest(RenderHttpRequestRequest), RenderHttpRequestRequest(RenderHttpRequestRequest),
RenderHttpRequestResponse(RenderHttpRequestResponse), RenderHttpRequestResponse(RenderHttpRequestResponse),
GetKeyValueRequest(GetKeyValueRequest),
GetKeyValueResponse(GetKeyValueResponse),
SetKeyValueRequest(SetKeyValueRequest),
SetKeyValueResponse(SetKeyValueResponse),
DeleteKeyValueRequest(DeleteKeyValueRequest),
DeleteKeyValueResponse(DeleteKeyValueResponse),
OpenWindowRequest(OpenWindowRequest),
WindowNavigateEvent(WindowNavigateEvent),
CloseWindowRequest(CloseWindowRequest),
TemplateRenderRequest(TemplateRenderRequest), TemplateRenderRequest(TemplateRenderRequest),
TemplateRenderResponse(TemplateRenderResponse), TemplateRenderResponse(TemplateRenderResponse),
ShowToastRequest(ShowToastRequest), ShowToastRequest(ShowToastRequest),
ShowToastResponse(EmptyPayload),
PromptTextRequest(PromptTextRequest), PromptTextRequest(PromptTextRequest),
PromptTextResponse(PromptTextResponse), PromptTextResponse(PromptTextResponse),
@@ -117,19 +134,19 @@ pub enum InternalEventPayload {
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default)] #[serde(default)]
#[ts(export, type = "{}", export_to = "events.ts")] #[ts(export, type = "{}", export_to = "gen_events.ts")]
pub struct EmptyPayload {} pub struct EmptyPayload {}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default)] #[serde(default)]
#[ts(export, export_to = "events.ts")] #[ts(export, export_to = "gen_events.ts")]
pub struct ErrorResponse { pub struct ErrorResponse {
pub error: String, pub error: String,
} }
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")] #[ts(export, export_to = "gen_events.ts")]
pub struct BootRequest { pub struct BootRequest {
pub dir: String, pub dir: String,
pub watch: bool, pub watch: bool,
@@ -137,7 +154,7 @@ pub struct BootRequest {
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")] #[ts(export, export_to = "gen_events.ts")]
pub struct BootResponse { pub struct BootResponse {
pub name: String, pub name: String,
pub version: String, pub version: String,
@@ -145,21 +162,21 @@ pub struct BootResponse {
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")] #[ts(export, export_to = "gen_events.ts")]
pub struct ImportRequest { pub struct ImportRequest {
pub content: String, pub content: String,
} }
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")] #[ts(export, export_to = "gen_events.ts")]
pub struct ImportResponse { pub struct ImportResponse {
pub resources: ImportResources, pub resources: ImportResources,
} }
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")] #[ts(export, export_to = "gen_events.ts")]
pub struct FilterRequest { pub struct FilterRequest {
pub content: String, pub content: String,
pub filter: String, pub filter: String,
@@ -167,49 +184,50 @@ pub struct FilterRequest {
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")] #[ts(export, export_to = "gen_events.ts")]
pub struct FilterResponse { pub struct FilterResponse {
pub content: String, pub content: String,
} }
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")] #[ts(export, export_to = "gen_events.ts")]
pub struct ExportHttpRequestRequest { pub struct ExportHttpRequestRequest {
pub http_request: HttpRequest, pub http_request: HttpRequest,
} }
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")] #[ts(export, export_to = "gen_events.ts")]
pub struct ExportHttpRequestResponse { pub struct ExportHttpRequestResponse {
pub content: String, pub content: String,
} }
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")] #[ts(export, export_to = "gen_events.ts")]
pub struct SendHttpRequestRequest { pub struct SendHttpRequestRequest {
#[ts(type = "Partial<HttpRequest>")]
pub http_request: HttpRequest, pub http_request: HttpRequest,
} }
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")] #[ts(export, export_to = "gen_events.ts")]
pub struct SendHttpRequestResponse { pub struct SendHttpRequestResponse {
pub http_response: HttpResponse, pub http_response: HttpResponse,
} }
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")] #[ts(export, export_to = "gen_events.ts")]
pub struct CopyTextRequest { pub struct CopyTextRequest {
pub text: String, pub text: String,
} }
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")] #[ts(export, export_to = "gen_events.ts")]
pub struct RenderHttpRequestRequest { pub struct RenderHttpRequestRequest {
pub http_request: HttpRequest, pub http_request: HttpRequest,
pub purpose: RenderPurpose, pub purpose: RenderPurpose,
@@ -217,14 +235,14 @@ pub struct RenderHttpRequestRequest {
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")] #[ts(export, export_to = "gen_events.ts")]
pub struct RenderHttpRequestResponse { pub struct RenderHttpRequestResponse {
pub http_request: HttpRequest, pub http_request: HttpRequest,
} }
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")] #[ts(export, export_to = "gen_events.ts")]
pub struct TemplateRenderRequest { pub struct TemplateRenderRequest {
pub data: serde_json::Value, pub data: serde_json::Value,
pub purpose: RenderPurpose, pub purpose: RenderPurpose,
@@ -232,25 +250,62 @@ pub struct TemplateRenderRequest {
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")] #[ts(export, export_to = "gen_events.ts")]
pub struct TemplateRenderResponse { pub struct TemplateRenderResponse {
pub data: serde_json::Value, pub data: serde_json::Value,
} }
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")] #[ts(export, export_to = "gen_events.ts")]
pub struct OpenWindowRequest {
pub url: String,
/// Label for the window. If not provided, a random one will be generated.
pub label: String,
#[ts(optional)]
pub title: Option<String>,
#[ts(optional)]
pub size: Option<WindowSize>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_events.ts")]
pub struct WindowSize {
pub width: f64,
pub height: f64,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_events.ts")]
pub struct CloseWindowRequest {
pub label: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_events.ts")]
pub struct WindowNavigateEvent {
pub url: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_events.ts")]
pub struct ShowToastRequest { pub struct ShowToastRequest {
pub message: String, pub message: String,
#[ts(optional)] #[ts(optional)]
pub color: Option<Color>, pub color: Option<Color>,
#[ts(optional)] #[ts(optional)]
pub icon: Option<Icon>, pub icon: Option<Icon>,
} }
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")] #[ts(export, export_to = "gen_events.ts")]
pub struct PromptTextRequest { pub struct PromptTextRequest {
// A unique ID to identify the prompt (eg. "enter-password") // A unique ID to identify the prompt (eg. "enter-password")
pub id: String, pub id: String,
@@ -277,17 +332,15 @@ pub struct PromptTextRequest {
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")] #[ts(export, export_to = "gen_events.ts")]
pub struct PromptTextResponse { pub struct PromptTextResponse {
pub value: Option<String>, pub value: Option<String>,
} }
#[derive(Debug, Clone, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
#[ts(export, export_to = "events.ts")] #[ts(export, export_to = "gen_events.ts")]
pub enum Color { pub enum Color {
Custom,
Default,
Primary, Primary,
Secondary, Secondary,
Info, Info,
@@ -299,18 +352,23 @@ pub enum Color {
impl Default for Color { impl Default for Color {
fn default() -> Self { fn default() -> Self {
Color::Default Color::Secondary
} }
} }
#[derive(Debug, Clone, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
#[ts(export, export_to = "events.ts")] #[ts(export, export_to = "gen_events.ts")]
pub enum Icon { pub enum Icon {
AlertTriangle,
Check,
CheckCircle,
ChevronDown,
Copy, Copy,
Info, Info,
CheckCircle, Pin,
AlertTriangle, Search,
Trash,
#[serde(untagged)] #[serde(untagged)]
#[ts(type = "\"_unknown\"")] #[ts(type = "\"_unknown\"")]
@@ -319,17 +377,45 @@ pub enum Icon {
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")] #[ts(export, export_to = "gen_events.ts")]
pub struct GetHttpAuthenticationResponse { pub struct GetHttpAuthenticationSummaryResponse {
pub name: String, pub name: String,
pub label: String, pub label: String,
pub short_label: String, pub short_label: String,
pub config: Vec<FormInput>,
} }
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")] #[ts(export, export_to = "gen_events.ts")]
pub struct HttpAuthenticationAction {
pub label: String,
#[ts(optional)]
pub icon: Option<Icon>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_events.ts")]
pub struct GetHttpAuthenticationConfigRequest {
pub context_id: String,
pub values: HashMap<String, JsonPrimitive>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_events.ts")]
pub struct GetHttpAuthenticationConfigResponse {
pub args: Vec<FormInput>,
pub plugin_ref_id: String,
#[ts(optional)]
pub actions: Option<Vec<HttpAuthenticationAction>>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_events.ts")]
pub struct HttpHeader { pub struct HttpHeader {
pub name: String, pub name: String,
pub value: String, pub value: String,
@@ -337,9 +423,10 @@ pub struct HttpHeader {
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")] #[ts(export, export_to = "gen_events.ts")]
pub struct CallHttpAuthenticationRequest { pub struct CallHttpAuthenticationRequest {
pub config: serde_json::Map<String, serde_json::Value>, pub context_id: String,
pub values: HashMap<String, JsonPrimitive>,
pub method: String, pub method: String,
pub url: String, pub url: String,
pub headers: Vec<HttpHeader>, pub headers: Vec<HttpHeader>,
@@ -347,7 +434,34 @@ pub struct CallHttpAuthenticationRequest {
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")] #[ts(export, export_to = "gen_events.ts")]
pub struct CallHttpAuthenticationActionRequest {
pub index: i32,
pub plugin_ref_id: String,
pub args: CallHttpAuthenticationActionArgs,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_events.ts")]
pub struct CallHttpAuthenticationActionArgs {
pub context_id: String,
pub values: HashMap<String, JsonPrimitive>,
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(untagged)]
#[ts(export, export_to = "gen_events.ts")]
pub enum JsonPrimitive {
String(String),
Number(f64),
Boolean(bool),
Null,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_events.ts")]
pub struct CallHttpAuthenticationResponse { pub struct CallHttpAuthenticationResponse {
/// HTTP headers to add to the request. Existing headers will be replaced, while /// HTTP headers to add to the request. Existing headers will be replaced, while
/// new headers will be added. /// new headers will be added.
@@ -356,7 +470,7 @@ pub struct CallHttpAuthenticationResponse {
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")] #[ts(export, export_to = "gen_events.ts")]
pub struct GetTemplateFunctionsResponse { pub struct GetTemplateFunctionsResponse {
pub functions: Vec<TemplateFunction>, pub functions: Vec<TemplateFunction>,
pub plugin_ref_id: String, pub plugin_ref_id: String,
@@ -364,9 +478,10 @@ pub struct GetTemplateFunctionsResponse {
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")] #[ts(export, export_to = "gen_events.ts")]
pub struct TemplateFunction { pub struct TemplateFunction {
pub name: String, pub name: String,
#[ts(optional)] #[ts(optional)]
pub description: Option<String>, pub description: Option<String>,
@@ -379,7 +494,7 @@ pub struct TemplateFunction {
#[derive(Debug, Clone, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "snake_case", tag = "type")] #[serde(rename_all = "snake_case", tag = "type")]
#[ts(export, export_to = "events.ts")] #[ts(export, export_to = "gen_events.ts")]
pub enum FormInput { pub enum FormInput {
Text(FormInputText), Text(FormInputText),
Editor(FormInputEditor), Editor(FormInputEditor),
@@ -387,14 +502,23 @@ pub enum FormInput {
Checkbox(FormInputCheckbox), Checkbox(FormInputCheckbox),
File(FormInputFile), File(FormInputFile),
HttpRequest(FormInputHttpRequest), HttpRequest(FormInputHttpRequest),
Accordion(FormInputAccordion),
Banner(FormInputBanner),
Markdown(FormInputMarkdown),
} }
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")] #[ts(export, export_to = "gen_events.ts")]
pub struct FormInputBase { pub struct FormInputBase {
/// The name of the input. The value will be stored at this object attribute in the resulting data
pub name: String, pub name: String,
/// Whether this input is visible for the given configuration. Use this to
/// make branching forms.
#[ts(optional)]
pub hidden: Option<bool>,
/// Whether the user must fill in the argument /// Whether the user must fill in the argument
#[ts(optional)] #[ts(optional)]
pub optional: Option<bool>, pub optional: Option<bool>,
@@ -410,11 +534,14 @@ pub struct FormInputBase {
/// The default value /// The default value
#[ts(optional)] #[ts(optional)]
pub default_value: Option<String>, pub default_value: Option<String>,
#[ts(optional)]
pub disabled: Option<bool>,
} }
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")] #[ts(export, export_to = "gen_events.ts")]
pub struct FormInputText { pub struct FormInputText {
#[serde(flatten)] #[serde(flatten)]
pub base: FormInputBase, pub base: FormInputBase,
@@ -426,11 +553,18 @@ pub struct FormInputText {
/// Placeholder for the text input /// Placeholder for the text input
#[ts(optional)] #[ts(optional)]
pub password: Option<bool>, pub password: Option<bool>,
/// Whether to allow newlines in the input, like a <textarea/>
#[ts(optional)]
pub multi_line: Option<bool>,
#[ts(optional)]
pub completion_options: Option<Vec<GenericCompletionOption>>,
} }
#[derive(Debug, Clone, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
#[ts(export, export_to = "events.ts")] #[ts(export, export_to = "gen_events.ts")]
pub enum EditorLanguage { pub enum EditorLanguage {
Text, Text,
Javascript, Javascript,
@@ -449,7 +583,7 @@ impl Default for EditorLanguage {
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")] #[ts(export, export_to = "gen_events.ts")]
pub struct FormInputEditor { pub struct FormInputEditor {
#[serde(flatten)] #[serde(flatten)]
pub base: FormInputBase, pub base: FormInputBase,
@@ -465,11 +599,45 @@ pub struct FormInputEditor {
/// Language for syntax highlighting /// Language for syntax highlighting
#[ts(optional)] #[ts(optional)]
pub language: Option<EditorLanguage>, pub language: Option<EditorLanguage>,
#[ts(optional)]
pub read_only: Option<bool>,
#[ts(optional)]
pub completion_options: Option<Vec<GenericCompletionOption>>,
} }
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")] #[ts(export, export_to = "gen_events.ts")]
pub struct GenericCompletionOption {
label: String,
#[ts(optional)]
detail: Option<String>,
#[ts(optional)]
info: Option<String>,
#[ts(optional)]
#[serde(rename = "type")]
pub type_: Option<CompletionOptionType>,
#[ts(optional)]
pub boost: Option<i32>,
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "snake_case")]
#[ts(export, export_to = "gen_events.ts")]
pub enum CompletionOptionType {
Constant,
Variable,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_events.ts")]
pub struct FormInputHttpRequest { pub struct FormInputHttpRequest {
#[serde(flatten)] #[serde(flatten)]
pub base: FormInputBase, pub base: FormInputBase,
@@ -477,7 +645,7 @@ pub struct FormInputHttpRequest {
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")] #[ts(export, export_to = "gen_events.ts")]
pub struct FormInputFile { pub struct FormInputFile {
#[serde(flatten)] #[serde(flatten)]
pub base: FormInputBase, pub base: FormInputBase,
@@ -504,7 +672,7 @@ pub struct FormInputFile {
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")] #[ts(export, export_to = "gen_events.ts")]
pub struct FileFilter { pub struct FileFilter {
pub name: String, pub name: String,
/// File extensions to require /// File extensions to require
@@ -513,7 +681,7 @@ pub struct FileFilter {
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")] #[ts(export, export_to = "gen_events.ts")]
pub struct FormInputSelect { pub struct FormInputSelect {
#[serde(flatten)] #[serde(flatten)]
pub base: FormInputBase, pub base: FormInputBase,
@@ -524,7 +692,7 @@ pub struct FormInputSelect {
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")] #[ts(export, export_to = "gen_events.ts")]
pub struct FormInputCheckbox { pub struct FormInputCheckbox {
#[serde(flatten)] #[serde(flatten)]
pub base: FormInputBase, pub base: FormInputBase,
@@ -532,15 +700,68 @@ pub struct FormInputCheckbox {
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")] #[ts(export, export_to = "gen_events.ts")]
pub struct FormInputSelectOption { pub struct FormInputSelectOption {
pub name: String, pub label: String,
pub value: String, pub value: String,
} }
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")] #[ts(export, export_to = "gen_events.ts")]
pub struct FormInputAccordion {
pub label: String,
#[ts(optional)]
pub inputs: Option<Vec<FormInput>>,
#[ts(optional)]
pub hidden: Option<bool>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_events.ts")]
pub struct FormInputBanner {
#[ts(optional)]
pub inputs: Option<Vec<FormInput>>,
#[ts(optional)]
pub hidden: Option<bool>,
#[ts(optional)]
pub color: Option<Color>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_events.ts")]
pub struct FormInputMarkdown {
pub content: String,
#[ts(optional)]
pub hidden: Option<bool>,
}
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "snake_case", tag = "type")]
#[ts(export, export_to = "gen_events.ts")]
pub enum Content {
Text { content: String },
Markdown { content: String },
}
impl Default for Content {
fn default() -> Self {
Self::Text {
content: String::default(),
}
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_events.ts")]
pub struct CallTemplateFunctionRequest { pub struct CallTemplateFunctionRequest {
pub name: String, pub name: String,
pub args: CallTemplateFunctionArgs, pub args: CallTemplateFunctionArgs,
@@ -548,14 +769,14 @@ pub struct CallTemplateFunctionRequest {
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")] #[ts(export, export_to = "gen_events.ts")]
pub struct CallTemplateFunctionResponse { pub struct CallTemplateFunctionResponse {
pub value: Option<String>, pub value: Option<String>,
} }
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")] #[ts(export, export_to = "gen_events.ts")]
pub struct CallTemplateFunctionArgs { pub struct CallTemplateFunctionArgs {
pub purpose: RenderPurpose, pub purpose: RenderPurpose,
pub values: HashMap<String, String>, pub values: HashMap<String, String>,
@@ -563,7 +784,7 @@ pub struct CallTemplateFunctionArgs {
#[derive(Debug, Clone, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
#[ts(export, export_to = "events.ts")] #[ts(export, export_to = "gen_events.ts")]
pub enum RenderPurpose { pub enum RenderPurpose {
Send, Send,
Preview, Preview,
@@ -577,12 +798,12 @@ impl Default for RenderPurpose {
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default)] #[serde(default)]
#[ts(export, export_to = "events.ts")] #[ts(export, export_to = "gen_events.ts")]
pub struct GetHttpRequestActionsRequest {} pub struct GetHttpRequestActionsRequest {}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")] #[ts(export, export_to = "gen_events.ts")]
pub struct GetHttpRequestActionsResponse { pub struct GetHttpRequestActionsResponse {
pub actions: Vec<HttpRequestAction>, pub actions: Vec<HttpRequestAction>,
pub plugin_ref_id: String, pub plugin_ref_id: String,
@@ -590,9 +811,8 @@ pub struct GetHttpRequestActionsResponse {
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")] #[ts(export, export_to = "gen_events.ts")]
pub struct HttpRequestAction { pub struct HttpRequestAction {
pub key: String,
pub label: String, pub label: String,
#[ts(optional)] #[ts(optional)]
pub icon: Option<Icon>, pub icon: Option<Icon>,
@@ -600,37 +820,37 @@ pub struct HttpRequestAction {
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")] #[ts(export, export_to = "gen_events.ts")]
pub struct CallHttpRequestActionRequest { pub struct CallHttpRequestActionRequest {
pub key: String, pub index: i32,
pub plugin_ref_id: String, pub plugin_ref_id: String,
pub args: CallHttpRequestActionArgs, pub args: CallHttpRequestActionArgs,
} }
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")] #[ts(export, export_to = "gen_events.ts")]
pub struct CallHttpRequestActionArgs { pub struct CallHttpRequestActionArgs {
pub http_request: HttpRequest, pub http_request: HttpRequest,
} }
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")] #[ts(export, export_to = "gen_events.ts")]
pub struct GetHttpRequestByIdRequest { pub struct GetHttpRequestByIdRequest {
pub id: String, pub id: String,
} }
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")] #[ts(export, export_to = "gen_events.ts")]
pub struct GetHttpRequestByIdResponse { pub struct GetHttpRequestByIdResponse {
pub http_request: Option<HttpRequest>, pub http_request: Option<HttpRequest>,
} }
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")] #[ts(export, export_to = "gen_events.ts")]
pub struct FindHttpResponsesRequest { pub struct FindHttpResponsesRequest {
pub request_id: String, pub request_id: String,
#[ts(optional)] #[ts(optional)]
@@ -639,14 +859,14 @@ pub struct FindHttpResponsesRequest {
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")] #[ts(export, export_to = "gen_events.ts")]
pub struct FindHttpResponsesResponse { pub struct FindHttpResponsesResponse {
pub http_responses: Vec<HttpResponse>, pub http_responses: Vec<HttpResponse>,
} }
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")] #[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "events.ts")] #[ts(export, export_to = "gen_events.ts")]
pub struct ImportResources { pub struct ImportResources {
pub workspaces: Vec<Workspace>, pub workspaces: Vec<Workspace>,
pub environments: Vec<Environment>, pub environments: Vec<Environment>,
@@ -654,3 +874,45 @@ pub struct ImportResources {
pub http_requests: Vec<HttpRequest>, pub http_requests: Vec<HttpRequest>,
pub grpc_requests: Vec<GrpcRequest>, pub grpc_requests: Vec<GrpcRequest>,
} }
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_events.ts")]
pub struct GetKeyValueRequest {
pub key: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_events.ts")]
pub struct GetKeyValueResponse {
#[ts(optional)]
pub value: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_events.ts")]
pub struct SetKeyValueRequest {
pub key: String,
pub value: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default)]
#[ts(export, type = "{}", export_to = "gen_events.ts")]
pub struct SetKeyValueResponse {}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default, rename_all = "camelCase")]
#[ts(export, export_to = "gen_events.ts")]
pub struct DeleteKeyValueRequest {
pub key: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)]
#[serde(default)]
#[ts(export, export_to = "gen_events.ts")]
pub struct DeleteKeyValueResponse {
pub deleted: bool,
}
+125 -73
View File
@@ -3,12 +3,13 @@ use crate::error::Error::{
}; };
use crate::error::Result; use crate::error::Result;
use crate::events::{ use crate::events::{
BootRequest, CallHttpAuthenticationRequest, CallHttpAuthenticationResponse, BootRequest, CallHttpAuthenticationActionArgs, CallHttpAuthenticationActionRequest,
CallHttpRequestActionRequest, CallTemplateFunctionArgs, CallTemplateFunctionRequest, CallHttpAuthenticationRequest, CallHttpAuthenticationResponse, CallHttpRequestActionRequest,
CallTemplateFunctionResponse, EmptyPayload, FilterRequest, FilterResponse, FormInput, CallTemplateFunctionArgs, CallTemplateFunctionRequest, CallTemplateFunctionResponse,
GetHttpAuthenticationResponse, GetHttpRequestActionsResponse, GetTemplateFunctionsResponse, EmptyPayload, FilterRequest, FilterResponse, GetHttpAuthenticationConfigRequest,
ImportRequest, ImportResponse, InternalEvent, InternalEventPayload, RenderPurpose, GetHttpAuthenticationConfigResponse, GetHttpAuthenticationSummaryResponse,
WindowContext, GetHttpRequestActionsResponse, GetTemplateFunctionsResponse, ImportRequest, ImportResponse,
InternalEvent, InternalEventPayload, JsonPrimitive, RenderPurpose, WindowContext,
}; };
use crate::nodejs::start_nodejs_plugin_runtime; use crate::nodejs::start_nodejs_plugin_runtime;
use crate::plugin_handle::PluginHandle; use crate::plugin_handle::PluginHandle;
@@ -24,6 +25,7 @@ use tauri::{AppHandle, Manager, Runtime, WebviewWindow};
use tokio::fs::read_dir; use tokio::fs::read_dir;
use tokio::net::TcpListener; use tokio::net::TcpListener;
use tokio::sync::{mpsc, Mutex}; use tokio::sync::{mpsc, Mutex};
use tokio::time::timeout;
use yaak_models::queries::{generate_id, list_plugins}; use yaak_models::queries::{generate_id, list_plugins};
#[derive(Clone)] #[derive(Clone)]
@@ -86,7 +88,7 @@ impl PluginManager {
let addr = listener.local_addr().expect("Failed to get local address"); let addr = listener.local_addr().expect("Failed to get local address");
// 1. Reload all plugins when the Node.js runtime connects // 1. Reload all plugins when the Node.js runtime connects
{ let init_plugins_task = {
let plugin_manager = plugin_manager.clone(); let plugin_manager = plugin_manager.clone();
let app_handle = app_handle.clone(); let app_handle = app_handle.clone();
tauri::async_runtime::spawn(async move { tauri::async_runtime::spawn(async move {
@@ -94,7 +96,7 @@ impl PluginManager {
Ok(_) => { Ok(_) => {
info!("Plugin runtime client connected!"); info!("Plugin runtime client connected!");
plugin_manager plugin_manager
.initialize_all_plugins(&app_handle, WindowContext::None) .initialize_all_plugins(&app_handle, &WindowContext::None)
.await .await
.expect("Failed to reload plugins"); .expect("Failed to reload plugins");
} }
@@ -102,7 +104,7 @@ impl PluginManager {
warn!("Failed to receive from client connection rx {e:?}"); warn!("Failed to receive from client connection rx {e:?}");
} }
} }
}); })
}; };
// 1. Spawn server in the background // 1. Spawn server in the background
@@ -114,8 +116,13 @@ impl PluginManager {
// 2. Start Node.js runtime and initialize plugins // 2. Start Node.js runtime and initialize plugins
tauri::async_runtime::block_on(async move { tauri::async_runtime::block_on(async move {
start_nodejs_plugin_runtime(&app_handle, addr, &kill_server_rx).await.unwrap(); start_nodejs_plugin_runtime(&app_handle, addr, &kill_server_rx).await.unwrap();
info!("Waiting for plugins to initialize");
init_plugins_task.await.unwrap();
}); });
// 3. Block waiting for plugins to initialize
tauri::async_runtime::block_on(async move {});
plugin_manager plugin_manager
} }
@@ -160,14 +167,14 @@ impl PluginManager {
[bundled_plugin_dirs, installed_plugin_dirs].concat() [bundled_plugin_dirs, installed_plugin_dirs].concat()
} }
pub async fn uninstall(&self, window_context: WindowContext, dir: &str) -> Result<()> { pub async fn uninstall(&self, window_context: &WindowContext, dir: &str) -> Result<()> {
let plugin = self.get_plugin_by_dir(dir).await.ok_or(PluginNotFoundErr(dir.to_string()))?; let plugin = self.get_plugin_by_dir(dir).await.ok_or(PluginNotFoundErr(dir.to_string()))?;
self.remove_plugin(window_context, &plugin).await self.remove_plugin(window_context, &plugin).await
} }
async fn remove_plugin( async fn remove_plugin(
&self, &self,
window_context: WindowContext, window_context: &WindowContext,
plugin: &PluginHandle, plugin: &PluginHandle,
) -> Result<()> { ) -> Result<()> {
// Terminate the plugin // Terminate the plugin
@@ -185,7 +192,7 @@ impl PluginManager {
pub async fn add_plugin_by_dir( pub async fn add_plugin_by_dir(
&self, &self,
window_context: WindowContext, window_context: &WindowContext,
dir: &str, dir: &str,
watch: bool, watch: bool,
) -> Result<()> { ) -> Result<()> {
@@ -197,20 +204,22 @@ impl PluginManager {
}; };
let plugin_handle = PluginHandle::new(dir, tx.clone()); let plugin_handle = PluginHandle::new(dir, tx.clone());
// Add the new plugin
self.plugins.lock().await.push(plugin_handle.clone());
// Boot the plugin // Boot the plugin
let event = self let event = timeout(
.send_to_plugin_and_wait( Duration::from_secs(1),
self.send_to_plugin_and_wait(
window_context, window_context,
&plugin_handle, &plugin_handle,
&InternalEventPayload::BootRequest(BootRequest { &InternalEventPayload::BootRequest(BootRequest {
dir: dir.to_string(), dir: dir.to_string(),
watch, watch,
}), }),
) ),
.await?; )
.await??;
// Add the new plugin
self.plugins.lock().await.push(plugin_handle.clone());
let resp = match event.payload { let resp = match event.payload {
InternalEventPayload::BootResponse(resp) => resp, InternalEventPayload::BootResponse(resp) => resp,
@@ -226,20 +235,21 @@ impl PluginManager {
pub async fn initialize_all_plugins<R: Runtime>( pub async fn initialize_all_plugins<R: Runtime>(
&self, &self,
app_handle: &AppHandle<R>, app_handle: &AppHandle<R>,
window_context: WindowContext, window_context: &WindowContext,
) -> Result<()> { ) -> Result<()> {
let dirs = self.list_plugin_dirs(app_handle).await; let candidates = self.list_plugin_dirs(app_handle).await;
for d in dirs.clone() { for candidate in candidates.clone() {
// First remove the plugin if it exists // First remove the plugin if it exists
if let Some(plugin) = self.get_plugin_by_dir(d.dir.as_str()).await { if let Some(plugin) = self.get_plugin_by_dir(candidate.dir.as_str()).await {
if let Err(e) = self.remove_plugin(window_context.to_owned(), &plugin).await { if let Err(e) = self.remove_plugin(window_context, &plugin).await {
warn!("Failed to remove plugin {} {e:?}", d.dir); warn!("Failed to remove plugin {} {e:?}", candidate.dir);
} }
} }
if let Err(e) = if let Err(e) = self
self.add_plugin_by_dir(window_context.to_owned(), d.dir.as_str(), d.watch).await .add_plugin_by_dir(window_context, candidate.dir.as_str(), candidate.watch)
.await
{ {
warn!("Failed to add plugin {} {e:?}", d.dir); warn!("Failed to add plugin {} {e:?}", candidate.dir);
} }
} }
@@ -280,13 +290,13 @@ impl PluginManager {
source_event: &InternalEvent, source_event: &InternalEvent,
payload: &InternalEventPayload, payload: &InternalEventPayload,
) -> Result<()> { ) -> Result<()> {
let window_label = source_event.to_owned().window_context; let window_context = source_event.to_owned().window_context;
let reply_id = Some(source_event.to_owned().id); let reply_id = Some(source_event.to_owned().id);
let plugin = self let plugin = self
.get_plugin_by_ref_id(source_event.plugin_ref_id.as_str()) .get_plugin_by_ref_id(source_event.plugin_ref_id.as_str())
.await .await
.ok_or(PluginNotFoundErr(source_event.plugin_ref_id.to_string()))?; .ok_or(PluginNotFoundErr(source_event.plugin_ref_id.to_string()))?;
let event = plugin.build_event_to_send_raw(window_label, &payload, reply_id); let event = plugin.build_event_to_send_raw(&window_context, &payload, reply_id);
plugin.send(&event).await plugin.send(&event).await
} }
@@ -310,7 +320,7 @@ impl PluginManager {
async fn send_to_plugin_and_wait( async fn send_to_plugin_and_wait(
&self, &self,
window_context: WindowContext, window_context: &WindowContext,
plugin: &PluginHandle, plugin: &PluginHandle,
payload: &InternalEventPayload, payload: &InternalEventPayload,
) -> Result<InternalEvent> { ) -> Result<InternalEvent> {
@@ -321,7 +331,7 @@ impl PluginManager {
async fn send_and_wait( async fn send_and_wait(
&self, &self,
window_context: WindowContext, window_context: &WindowContext,
payload: &InternalEventPayload, payload: &InternalEventPayload,
) -> Result<Vec<InternalEvent>> { ) -> Result<Vec<InternalEvent>> {
let plugins = { self.plugins.lock().await.clone() }; let plugins = { self.plugins.lock().await.clone() };
@@ -330,7 +340,7 @@ impl PluginManager {
async fn send_to_plugins_and_wait( async fn send_to_plugins_and_wait(
&self, &self,
window_context: WindowContext, window_context: &WindowContext,
payload: &InternalEventPayload, payload: &InternalEventPayload,
plugins: Vec<PluginHandle>, plugins: Vec<PluginHandle>,
) -> Result<Vec<InternalEvent>> { ) -> Result<Vec<InternalEvent>> {
@@ -340,7 +350,7 @@ impl PluginManager {
// 1. Build the events with IDs and everything // 1. Build the events with IDs and everything
let events_to_send = plugins let events_to_send = plugins
.iter() .iter()
.map(|p| p.build_event_to_send(window_context.to_owned(), payload, None)) .map(|p| p.build_event_to_send(window_context, payload, None))
.collect::<Vec<InternalEvent>>(); .collect::<Vec<InternalEvent>>();
// 2. Spawn thread to subscribe to incoming events and check reply ids // 2. Spawn thread to subscribe to incoming events and check reply ids
@@ -358,9 +368,9 @@ impl PluginManager {
if matched_sent_event { if matched_sent_event {
found_events.push(event.clone()); found_events.push(event.clone());
}; };
let found_them_all = found_events.len() == events_to_send.len(); let found_them_all = found_events.len() == events_to_send.len();
if found_them_all{ if found_them_all {
break; break;
} }
} }
@@ -393,7 +403,7 @@ impl PluginManager {
) -> Result<Vec<GetHttpRequestActionsResponse>> { ) -> Result<Vec<GetHttpRequestActionsResponse>> {
let reply_events = self let reply_events = self
.send_and_wait( .send_and_wait(
WindowContext::from_window(window), &WindowContext::from_window(window),
&InternalEventPayload::GetHttpRequestActionsRequest(EmptyPayload {}), &InternalEventPayload::GetHttpRequestActionsRequest(EmptyPayload {}),
) )
.await?; .await?;
@@ -412,12 +422,12 @@ impl PluginManager {
&self, &self,
window: &WebviewWindow<R>, window: &WebviewWindow<R>,
) -> Result<Vec<GetTemplateFunctionsResponse>> { ) -> Result<Vec<GetTemplateFunctionsResponse>> {
self.get_template_functions_with_context(WindowContext::from_window(window)).await self.get_template_functions_with_context(&WindowContext::from_window(window)).await
} }
pub async fn get_template_functions_with_context( pub async fn get_template_functions_with_context(
&self, &self,
window_context: WindowContext, window_context: &WindowContext,
) -> Result<Vec<GetTemplateFunctionsResponse>> { ) -> Result<Vec<GetTemplateFunctionsResponse>> {
let reply_events = self let reply_events = self
.send_and_wait(window_context, &InternalEventPayload::GetTemplateFunctionsRequest) .send_and_wait(window_context, &InternalEventPayload::GetTemplateFunctionsRequest)
@@ -442,7 +452,7 @@ impl PluginManager {
let plugin = let plugin =
self.get_plugin_by_ref_id(ref_id.as_str()).await.ok_or(PluginNotFoundErr(ref_id))?; self.get_plugin_by_ref_id(ref_id.as_str()).await.ok_or(PluginNotFoundErr(ref_id))?;
let event = plugin.build_event_to_send( let event = plugin.build_event_to_send(
WindowContext::from_window(window), &WindowContext::from_window(window),
&InternalEventPayload::CallHttpRequestActionRequest(req), &InternalEventPayload::CallHttpRequestActionRequest(req),
None, None,
); );
@@ -450,21 +460,22 @@ impl PluginManager {
Ok(()) Ok(())
} }
pub async fn get_http_authentication<R: Runtime>( pub async fn get_http_authentication_summaries<R: Runtime>(
&self, &self,
window: &WebviewWindow<R>, window: &WebviewWindow<R>,
) -> Result<Vec<(PluginHandle, GetHttpAuthenticationResponse)>> { ) -> Result<Vec<(PluginHandle, GetHttpAuthenticationSummaryResponse)>> {
let window_context = WindowContext::from_window(window); let window_context = WindowContext::from_window(window);
let reply_events = self let reply_events = self
.send_and_wait( .send_and_wait(
window_context, &window_context,
&InternalEventPayload::GetHttpAuthenticationRequest(EmptyPayload {}), &InternalEventPayload::GetHttpAuthenticationSummaryRequest(EmptyPayload {}),
) )
.await?; .await?;
let mut results = Vec::new(); let mut results = Vec::new();
for event in reply_events { for event in reply_events {
if let InternalEventPayload::GetHttpAuthenticationResponse(resp) = event.payload { if let InternalEventPayload::GetHttpAuthenticationSummaryResponse(resp) = event.payload
{
let plugin = self let plugin = self
.get_plugin_by_ref_id(&event.plugin_ref_id) .get_plugin_by_ref_id(&event.plugin_ref_id)
.await .await
@@ -476,43 +487,84 @@ impl PluginManager {
Ok(results) Ok(results)
} }
pub async fn get_http_authentication_config<R: Runtime>(
&self,
window: &WebviewWindow<R>,
auth_name: &str,
values: HashMap<String, JsonPrimitive>,
request_id: &str,
) -> Result<GetHttpAuthenticationConfigResponse> {
let results = self.get_http_authentication_summaries(window).await?;
let plugin = results
.iter()
.find_map(|(p, r)| if r.name == auth_name { Some(p) } else { None })
.ok_or(PluginNotFoundErr(auth_name.into()))?;
let context_id = format!("{:x}", md5::compute(request_id.to_string()));
let event = self
.send_to_plugin_and_wait(
&WindowContext::from_window(window),
&plugin,
&InternalEventPayload::GetHttpAuthenticationConfigRequest(
GetHttpAuthenticationConfigRequest { values, context_id },
),
)
.await?;
match event.payload {
InternalEventPayload::GetHttpAuthenticationConfigResponse(resp) => Ok(resp),
InternalEventPayload::EmptyResponse(_) => {
Err(PluginErr("Auth plugin returned empty".to_string()))
}
e => Err(PluginErr(format!("Auth plugin returned invalid event {:?}", e))),
}
}
pub async fn call_http_authentication_action<R: Runtime>(
&self,
window: &WebviewWindow<R>,
auth_name: &str,
action_index: i32,
values: HashMap<String, JsonPrimitive>,
request_id: &str,
) -> Result<()> {
let results = self.get_http_authentication_summaries(window).await?;
let plugin = results
.iter()
.find_map(|(p, r)| if r.name == auth_name { Some(p) } else { None })
.ok_or(PluginNotFoundErr(auth_name.into()))?;
let context_id = format!("{:x}", md5::compute(request_id.to_string()));
self
.send_to_plugin_and_wait(
&WindowContext::from_window(window),
&plugin,
&InternalEventPayload::CallHttpAuthenticationActionRequest(
CallHttpAuthenticationActionRequest {
index: action_index,
plugin_ref_id: plugin.clone().ref_id,
args: CallHttpAuthenticationActionArgs { context_id, values },
},
),
)
.await?;
Ok(())
}
pub async fn call_http_authentication<R: Runtime>( pub async fn call_http_authentication<R: Runtime>(
&self, &self,
window: &WebviewWindow<R>, window: &WebviewWindow<R>,
auth_name: &str, auth_name: &str,
req: CallHttpAuthenticationRequest, req: CallHttpAuthenticationRequest,
) -> Result<CallHttpAuthenticationResponse> { ) -> Result<CallHttpAuthenticationResponse> {
let handlers = self.get_http_authentication(window).await?; let handlers = self.get_http_authentication_summaries(window).await?;
let (plugin, authentication) = handlers let (plugin, _) = handlers
.iter() .iter()
.find(|(_, a)| a.name == auth_name) .find(|(_, a)| a.name == auth_name)
.ok_or(AuthPluginNotFound(auth_name.to_string()))?; .ok_or(AuthPluginNotFound(auth_name.to_string()))?;
// Clone for mutability
let mut req = req.clone();
// Fill in default values
for arg in authentication.config.clone() {
let base = match arg {
FormInput::Text(a) => a.base,
FormInput::Editor(a) => a.base,
FormInput::Select(a) => a.base,
FormInput::Checkbox(a) => a.base,
FormInput::File(a) => a.base,
FormInput::HttpRequest(a) => a.base,
};
if let None = req.config.get(base.name.as_str()) {
let default = match base.default_value {
None => serde_json::Value::Null,
Some(s) => serde_json::Value::String(s),
};
req.config.insert(base.name, default);
}
}
let event = self let event = self
.send_to_plugin_and_wait( .send_to_plugin_and_wait(
WindowContext::from_window(window), &WindowContext::from_window(window),
&plugin, &plugin,
&InternalEventPayload::CallHttpAuthenticationRequest(req), &InternalEventPayload::CallHttpAuthenticationRequest(req),
) )
@@ -528,7 +580,7 @@ impl PluginManager {
pub async fn call_template_function( pub async fn call_template_function(
&self, &self,
window_context: WindowContext, window_context: &WindowContext,
fn_name: &str, fn_name: &str,
args: HashMap<String, String>, args: HashMap<String, String>,
purpose: RenderPurpose, purpose: RenderPurpose,
@@ -562,7 +614,7 @@ impl PluginManager {
) -> Result<(ImportResponse, String)> { ) -> Result<(ImportResponse, String)> {
let reply_events = self let reply_events = self
.send_and_wait( .send_and_wait(
WindowContext::from_window(window), &WindowContext::from_window(window),
&InternalEventPayload::ImportRequest(ImportRequest { &InternalEventPayload::ImportRequest(ImportRequest {
content: content.to_string(), content: content.to_string(),
}), }),
@@ -607,7 +659,7 @@ impl PluginManager {
let event = self let event = self
.send_to_plugin_and_wait( .send_to_plugin_and_wait(
WindowContext::from_window(window), &WindowContext::from_window(window),
&plugin, &plugin,
&InternalEventPayload::FilterRequest(FilterRequest { &InternalEventPayload::FilterRequest(FilterRequest {
filter: filter.to_string(), filter: filter.to_string(),
+9 -5
View File
@@ -26,6 +26,10 @@ impl PluginHandle {
} }
} }
pub async fn name(&self) -> String {
self.boot_resp.lock().await.name.clone()
}
pub async fn info(&self) -> BootResponse { pub async fn info(&self) -> BootResponse {
let resp = &*self.boot_resp.lock().await; let resp = &*self.boot_resp.lock().await;
resp.clone() resp.clone()
@@ -33,7 +37,7 @@ impl PluginHandle {
pub fn build_event_to_send( pub fn build_event_to_send(
&self, &self,
window_context: WindowContext, window_context: &WindowContext,
payload: &InternalEventPayload, payload: &InternalEventPayload,
reply_id: Option<String>, reply_id: Option<String>,
) -> InternalEvent { ) -> InternalEvent {
@@ -42,7 +46,7 @@ impl PluginHandle {
pub(crate) fn build_event_to_send_raw( pub(crate) fn build_event_to_send_raw(
&self, &self,
window_context: WindowContext, window_context: &WindowContext,
payload: &InternalEventPayload, payload: &InternalEventPayload,
reply_id: Option<String>, reply_id: Option<String>,
) -> InternalEvent { ) -> InternalEvent {
@@ -53,18 +57,18 @@ impl PluginHandle {
plugin_name: dir.file_name().unwrap().to_str().unwrap().to_string(), plugin_name: dir.file_name().unwrap().to_str().unwrap().to_string(),
reply_id, reply_id,
payload: payload.clone(), payload: payload.clone(),
window_context, window_context: window_context.clone(),
} }
} }
pub async fn terminate(&self, window_context: WindowContext) -> Result<()> { pub async fn terminate(&self, window_context: &WindowContext) -> Result<()> {
info!("Terminating plugin {}", self.dir); info!("Terminating plugin {}", self.dir);
let event = let event =
self.build_event_to_send(window_context, &InternalEventPayload::TerminateRequest, None); self.build_event_to_send(window_context, &InternalEventPayload::TerminateRequest, None);
self.send(&event).await self.send(&event).await
} }
pub(crate) async fn send(&self, event: &InternalEvent) -> Result<()> { pub async fn send(&self, event: &InternalEvent) -> Result<()> {
self.to_plugin_tx.lock().await.send(event.to_owned()).await?; self.to_plugin_tx.lock().await.send(event.to_owned()).await?;
Ok(()) Ok(())
} }
+2 -1
View File
@@ -85,8 +85,9 @@ impl PluginRuntimeServerWebsocket {
}; };
// Parse everything but the payload so we can catch errors on that, specifically // Parse everything but the payload so we can catch errors on that, specifically
let payload = serde_json::from_value::<InternalEventPayload>(event.payload) let payload = serde_json::from_value::<InternalEventPayload>(event.payload.clone())
.unwrap_or_else(|e| { .unwrap_or_else(|e| {
warn!("Plugin error from {}: {:?} {}", event.plugin_name, e, event.payload);
InternalEventPayload::ErrorResponse(ErrorResponse { InternalEventPayload::ErrorResponse(ErrorResponse {
error: format!("Plugin error from {}: {e:?}", event.plugin_name), error: format!("Plugin error from {}: {e:?}", event.plugin_name),
}) })
@@ -1,6 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { SyncModel } from "./models.js"; import type { SyncModel } from "./gen_models.js";
import type { SyncState } from "./models.js"; import type { SyncState } from "./gen_models.js";
export type FsCandidate = { "type": "FsCandidate", model: SyncModel, relPath: string, checksum: string, }; export type FsCandidate = { "type": "FsCandidate", model: SyncModel, relPath: string, checksum: string, };
+2 -2
View File
@@ -1,7 +1,7 @@
import { Channel, invoke } from '@tauri-apps/api/core'; import { Channel, invoke } from '@tauri-apps/api/core';
import { emit } from '@tauri-apps/api/event'; import { emit } from '@tauri-apps/api/event';
import { SyncOp } from './bindings/sync'; import { SyncOp } from './bindings/gen_sync';
import { WatchEvent, WatchResult } from './bindings/watch'; import { WatchEvent, WatchResult } from './bindings/gen_watch';
export async function calculateSync(workspaceId: string, syncDir: string) { export async function calculateSync(workspaceId: string, syncDir: string) {
return invoke<SyncOp[]>('plugin:yaak-sync|calculate', { return invoke<SyncOp[]>('plugin:yaak-sync|calculate', {
+1 -1
View File
@@ -51,7 +51,7 @@ pub async fn apply<R: Runtime>(
#[derive(Debug, Clone, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[ts(export, export_to = "watch.ts")] #[ts(export, export_to = "gen_watch.ts")]
pub(crate) struct WatchResult { pub(crate) struct WatchResult {
unlisten_event: String, unlisten_event: String,
} }
+1 -1
View File
@@ -10,7 +10,7 @@ use yaak_models::models::{AnyModel, Environment, Folder, GrpcRequest, HttpReques
#[derive(Debug, Clone, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "snake_case", tag = "type")] #[serde(rename_all = "snake_case", tag = "type")]
#[ts(export, export_to = "models.ts")] #[ts(export, export_to = "gen_models.ts")]
pub enum SyncModel { pub enum SyncModel {
Workspace(Workspace), Workspace(Workspace),
Environment(Environment), Environment(Environment),
+2 -2
View File
@@ -21,7 +21,7 @@ use yaak_models::queries::{
#[derive(Debug, Clone, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase", tag = "type")] #[serde(rename_all = "camelCase", tag = "type")]
#[ts(export, export_to = "sync.ts")] #[ts(export, export_to = "gen_sync.ts")]
pub(crate) enum SyncOp { pub(crate) enum SyncOp {
FsCreate { FsCreate {
model: SyncModel, model: SyncModel,
@@ -98,7 +98,7 @@ impl DbCandidate {
#[derive(Debug, Clone, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase", tag = "type")] #[serde(rename_all = "camelCase", tag = "type")]
#[ts(export, export_to = "sync.ts")] #[ts(export, export_to = "gen_sync.ts")]
pub(crate) struct FsCandidate { pub(crate) struct FsCandidate {
pub(crate) model: SyncModel, pub(crate) model: SyncModel,
pub(crate) rel_path: PathBuf, pub(crate) rel_path: PathBuf,
+1 -1
View File
@@ -10,7 +10,7 @@ use ts_rs::TS;
#[derive(Debug, Clone, Serialize, Deserialize, TS)] #[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
#[ts(export, export_to = "watch.ts")] #[ts(export, export_to = "gen_watch.ts")]
pub(crate) struct WatchEvent { pub(crate) struct WatchEvent {
paths: Vec<PathBuf>, paths: Vec<PathBuf>,
kind: String, kind: String,
+3 -2
View File
@@ -153,9 +153,10 @@ export function CommandPaletteDialog({ onClose }: { onClose: () => void }) {
label: 'Send Request', label: 'Send Request',
onSelect: () => sendRequest(activeRequest.id), onSelect: () => sendRequest(activeRequest.id),
}); });
for (const a of httpRequestActions) { for (let i = 0; i < httpRequestActions.length; i++) {
const a = httpRequestActions[i]!;
commands.push({ commands.push({
key: a.key, key: `http_request_action.${i}`,
label: a.label, label: a.label,
onSelect: () => a.call(activeRequest), onSelect: () => a.call(activeRequest),
}); });
-1
View File
@@ -73,7 +73,6 @@ export const CookieDropdown = memo(function CookieDropdown() {
...(((cookieJars ?? []).length > 1 // Never delete the last one ...(((cookieJars ?? []).length > 1 // Never delete the last one
? [ ? [
{ {
key: 'delete',
label: 'Delete', label: 'Delete',
leftSlot: <Icon icon="trash" />, leftSlot: <Icon icon="trash" />,
color: 'danger', color: 'danger',
+135 -41
View File
@@ -7,6 +7,7 @@ import type {
FormInputHttpRequest, FormInputHttpRequest,
FormInputSelect, FormInputSelect,
FormInputText, FormInputText,
JsonPrimitive,
} from '@yaakapp-internal/plugins'; } from '@yaakapp-internal/plugins';
import classNames from 'classnames'; import classNames from 'classnames';
import { useCallback } from 'react'; import { useCallback } from 'react';
@@ -15,51 +16,93 @@ import { useFolders } from '../hooks/useFolders';
import { useHttpRequests } from '../hooks/useHttpRequests'; import { useHttpRequests } from '../hooks/useHttpRequests';
import { capitalize } from '../lib/capitalize'; import { capitalize } from '../lib/capitalize';
import { fallbackRequestName } from '../lib/fallbackRequestName'; import { fallbackRequestName } from '../lib/fallbackRequestName';
import { Banner } from './core/Banner';
import { Checkbox } from './core/Checkbox'; import { Checkbox } from './core/Checkbox';
import { Editor } from './core/Editor/Editor'; import { Editor } from './core/Editor/Editor';
import { Input } from './core/Input'; import { Input } from './core/Input';
import { Label } from './core/Label'; import { Label } from './core/Label';
import { Select } from './core/Select'; import { Select } from './core/Select';
import { VStack } from './core/Stacks'; import { VStack } from './core/Stacks';
import { Markdown } from './Markdown';
import { SelectFile } from './SelectFile'; import { SelectFile } from './SelectFile';
// eslint-disable-next-line react-refresh/only-export-components // eslint-disable-next-line react-refresh/only-export-components
export const DYNAMIC_FORM_NULL_ARG = '__NULL__'; export const DYNAMIC_FORM_NULL_ARG = '__NULL__';
const INPUT_SIZE = 'sm';
export function DynamicForm<T extends Record<string, string | boolean>>({ interface Props<T> {
config, inputs: FormInput[] | undefined | null;
data,
onChange,
useTemplating,
autocompleteVariables,
stateKey,
}: {
config: FormInput[];
onChange: (value: T) => void; onChange: (value: T) => void;
data: T; data: T;
useTemplating?: boolean; useTemplating?: boolean;
autocompleteVariables?: boolean; autocompleteVariables?: boolean;
stateKey: string; stateKey: string;
}) { disabled?: boolean;
}
export function DynamicForm<T extends Record<string, JsonPrimitive>>({
inputs,
data,
onChange,
useTemplating,
autocompleteVariables,
stateKey,
disabled,
}: Props<T>) {
const setDataAttr = useCallback( const setDataAttr = useCallback(
(name: string, value: string | boolean | null) => { (name: string, value: JsonPrimitive) => {
onChange({ ...data, [name]: value == DYNAMIC_FORM_NULL_ARG ? undefined : value }); onChange({ ...data, [name]: value == DYNAMIC_FORM_NULL_ARG ? undefined : value });
}, },
[data, onChange], [data, onChange],
); );
return (
<FormInputs
disabled={disabled}
inputs={inputs}
setDataAttr={setDataAttr}
stateKey={stateKey}
useTemplating={useTemplating}
autocompleteVariables={autocompleteVariables}
data={data}
/>
);
}
function FormInputs<T extends Record<string, JsonPrimitive>>({
inputs,
autocompleteVariables,
stateKey,
useTemplating,
setDataAttr,
data,
disabled,
}: Pick<Props<T>, 'inputs' | 'useTemplating' | 'autocompleteVariables' | 'stateKey' | 'data'> & {
setDataAttr: (name: string, value: JsonPrimitive) => void;
disabled?: boolean;
}) {
return ( return (
<VStack space={3} className="h-full overflow-auto"> <VStack space={3} className="h-full overflow-auto">
{config.map((a, i) => { {inputs?.map((input, i) => {
switch (a.type) { if ('hidden' in input && input.hidden) {
return null;
}
if ('disabled' in input && disabled != null) {
input.disabled = disabled;
}
switch (input.type) {
case 'select': case 'select':
return ( return (
<SelectArg <SelectArg
key={i + stateKey} key={i + stateKey}
arg={a} arg={input}
onChange={(v) => setDataAttr(a.name, v)} onChange={(v) => setDataAttr(input.name, v)}
value={ value={
data[a.name] ? String(data[a.name]) : (a.defaultValue ?? DYNAMIC_FORM_NULL_ARG) data[input.name]
? String(data[input.name])
: (input.defaultValue ?? DYNAMIC_FORM_NULL_ARG)
} }
/> />
); );
@@ -68,11 +111,13 @@ export function DynamicForm<T extends Record<string, string | boolean>>({
<TextArg <TextArg
key={i} key={i}
stateKey={stateKey} stateKey={stateKey}
arg={a} arg={input}
useTemplating={useTemplating || false} useTemplating={useTemplating || false}
autocompleteVariables={autocompleteVariables || false} autocompleteVariables={autocompleteVariables || false}
onChange={(v) => setDataAttr(a.name, v)} onChange={(v) => setDataAttr(input.name, v)}
value={data[a.name] ? String(data[a.name]) : (a.defaultValue ?? '')} value={
data[input.name] != null ? String(data[input.name]) : (input.defaultValue ?? '')
}
/> />
); );
case 'editor': case 'editor':
@@ -80,40 +125,79 @@ export function DynamicForm<T extends Record<string, string | boolean>>({
<EditorArg <EditorArg
key={i} key={i}
stateKey={stateKey} stateKey={stateKey}
arg={a} arg={input}
useTemplating={useTemplating || false} useTemplating={useTemplating || false}
autocompleteVariables={autocompleteVariables || false} autocompleteVariables={autocompleteVariables || false}
onChange={(v) => setDataAttr(a.name, v)} onChange={(v) => setDataAttr(input.name, v)}
value={data[a.name] ? String(data[a.name]) : (a.defaultValue ?? '')} value={
data[input.name] != null ? String(data[input.name]) : (input.defaultValue ?? '')
}
/> />
); );
case 'checkbox': case 'checkbox':
return ( return (
<CheckboxArg <CheckboxArg
key={i + stateKey} key={i + stateKey}
arg={a} arg={input}
onChange={(v) => setDataAttr(a.name, v)} onChange={(v) => setDataAttr(input.name, v)}
value={data[a.name] !== undefined ? data[a.name] === true : false} value={data[input.name] != null ? data[input.name] === true : false}
/> />
); );
case 'http_request': case 'http_request':
return ( return (
<HttpRequestArg <HttpRequestArg
key={i + stateKey} key={i + stateKey}
arg={a} arg={input}
onChange={(v) => setDataAttr(a.name, v)} onChange={(v) => setDataAttr(input.name, v)}
value={data[a.name] ? String(data[a.name]) : DYNAMIC_FORM_NULL_ARG} value={data[input.name] != null ? String(data[input.name]) : DYNAMIC_FORM_NULL_ARG}
/> />
); );
case 'file': case 'file':
return ( return (
<FileArg <FileArg
key={i + stateKey} key={i + stateKey}
arg={a} arg={input}
onChange={(v) => setDataAttr(a.name, v)} onChange={(v) => setDataAttr(input.name, v)}
filePath={data[a.name] ? String(data[a.name]) : DYNAMIC_FORM_NULL_ARG} filePath={
data[input.name] != null ? String(data[input.name]) : DYNAMIC_FORM_NULL_ARG
}
/> />
); );
case 'accordion':
return (
<Banner key={i} className={classNames('!p-0', disabled && 'opacity-disabled')}>
<details>
<summary className="px-3 py-1.5 text-text-subtle">{input.label}</summary>
<div className="mb-3 px-3">
<FormInputs
data={data}
disabled={disabled}
inputs={input.inputs}
setDataAttr={setDataAttr}
stateKey={stateKey}
/>
</div>
</details>
</Banner>
);
case 'banner':
return (
<Banner
key={i}
color={input.color}
className={classNames(disabled && 'opacity-disabled')}
>
<FormInputs
data={data}
disabled={disabled}
inputs={input.inputs}
setDataAttr={setDataAttr}
stateKey={stateKey}
/>
</Banner>
);
case 'markdown':
return <Markdown>{input.content}</Markdown>;
} }
})} })}
</VStack> </VStack>
@@ -145,13 +229,17 @@ function TextArg({
return ( return (
<Input <Input
name={arg.name} name={arg.name}
multiLine={arg.multiLine}
onChange={handleChange} onChange={handleChange}
defaultValue={value === DYNAMIC_FORM_NULL_ARG ? arg.defaultValue : value} defaultValue={value === DYNAMIC_FORM_NULL_ARG ? arg.defaultValue : value}
required={!arg.optional} required={!arg.optional}
disabled={arg.disabled}
type={arg.password ? 'password' : 'text'} type={arg.password ? 'password' : 'text'}
label={arg.label ?? arg.name} label={arg.label ?? arg.name}
size={INPUT_SIZE}
hideLabel={arg.label == null} hideLabel={arg.label == null}
placeholder={arg.placeholder ?? arg.defaultValue ?? ''} placeholder={arg.placeholder ?? arg.defaultValue ?? ''}
autocomplete={arg.completionOptions ? { options: arg.completionOptions } : undefined}
useTemplating={useTemplating} useTemplating={useTemplating}
autocompleteVariables={autocompleteVariables} autocompleteVariables={autocompleteVariables}
stateKey={stateKey} stateKey={stateKey}
@@ -184,23 +272,29 @@ function EditorArg({
const id = `input-${arg.name}`; const id = `input-${arg.name}`;
// Read-only editor force refresh for every defaultValue change
// Should this be built into the <Editor/> component?
const forceUpdateKey = arg.readOnly ? arg.defaultValue + stateKey : stateKey;
return ( return (
<div className=" w-full grid grid-cols-1 grid-rows-[auto_minmax(0,1fr)]"> <div className=" w-full grid grid-cols-1 grid-rows-[auto_minmax(0,1fr)]">
<Label <Label
htmlFor={id} htmlFor={id}
optional={arg.optional} required={!arg.optional}
visuallyHidden={arg.hideLabel} visuallyHidden={arg.hideLabel}
otherTags={arg.language ? [capitalize(arg.language)] : undefined} tags={arg.language ? [capitalize(arg.language)] : undefined}
> >
{arg.label} {arg.label}
</Label> </Label>
<Editor <Editor
id={id} id={id}
className={classNames( className={classNames(
'border border-border rounded-md overflow-hidden px-2 py-1.5', 'border border-border rounded-md overflow-hidden px-2 py-1',
'focus-within:border-border-focus', 'focus-within:border-border-focus',
'max-h-[15rem]', // So it doesn't take up too much space 'max-h-[15rem]', // So it doesn't take up too much space
)} )}
autocomplete={arg.completionOptions ? { options: arg.completionOptions } : undefined}
disabled={arg.disabled}
language={arg.language} language={arg.language}
onChange={handleChange} onChange={handleChange}
heightMode="auto" heightMode="auto"
@@ -209,7 +303,7 @@ function EditorArg({
useTemplating={useTemplating} useTemplating={useTemplating}
autocompleteVariables={autocompleteVariables} autocompleteVariables={autocompleteVariables}
stateKey={stateKey} stateKey={stateKey}
forceUpdateKey={stateKey} forceUpdateKey={forceUpdateKey}
hideGutter hideGutter
/> />
</div> </div>
@@ -232,12 +326,9 @@ function SelectArg({
onChange={onChange} onChange={onChange}
hideLabel={arg.hideLabel} hideLabel={arg.hideLabel}
value={value} value={value}
options={[ size={INPUT_SIZE}
...arg.options.map((a) => ({ disabled={arg.disabled}
label: a.name, options={arg.options}
value: a.value,
})),
]}
/> />
); );
} }
@@ -253,6 +344,7 @@ function FileArg({
}) { }) {
return ( return (
<SelectFile <SelectFile
disabled={arg.disabled}
onChange={({ filePath }) => onChange(filePath)} onChange={({ filePath }) => onChange(filePath)}
filePath={filePath === '__NULL__' ? null : filePath} filePath={filePath === '__NULL__' ? null : filePath}
directory={!!arg.directory} directory={!!arg.directory}
@@ -278,6 +370,7 @@ function HttpRequestArg({
name={arg.name} name={arg.name}
onChange={onChange} onChange={onChange}
value={value} value={value}
disabled={arg.disabled}
options={[ options={[
...httpRequests.map((r) => { ...httpRequests.map((r) => {
return { return {
@@ -323,6 +416,7 @@ function CheckboxArg({
<Checkbox <Checkbox
onChange={onChange} onChange={onChange}
checked={value} checked={value}
disabled={arg.disabled}
title={arg.label ?? arg.name} title={arg.label ?? arg.name}
hideLabel={arg.label == null} hideLabel={arg.label == null}
/> />
+2 -6
View File
@@ -1,4 +1,5 @@
import type { Environment } from '@yaakapp-internal/models'; import type { Environment } from '@yaakapp-internal/models';
import type { GenericCompletionOption } from '@yaakapp-internal/plugins';
import classNames from 'classnames'; import classNames from 'classnames';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import React, { useCallback, useMemo, useState } from 'react'; import React, { useCallback, useMemo, useState } from 'react';
@@ -11,10 +12,7 @@ import { showPrompt } from '../lib/prompt';
import { Banner } from './core/Banner'; import { Banner } from './core/Banner';
import { Button } from './core/Button'; import { Button } from './core/Button';
import { ContextMenu } from './core/Dropdown'; import { ContextMenu } from './core/Dropdown';
import type { import type { GenericCompletionConfig } from './core/Editor/genericCompletion';
GenericCompletionConfig,
GenericCompletionOption,
} from './core/Editor/genericCompletion';
import { Heading } from './core/Heading'; import { Heading } from './core/Heading';
import { Icon } from './core/Icon'; import { Icon } from './core/Icon';
import { IconButton } from './core/IconButton'; import { IconButton } from './core/IconButton';
@@ -255,7 +253,6 @@ function SidebarButton({
onClose={() => setShowContextMenu(null)} onClose={() => setShowContextMenu(null)}
items={[ items={[
{ {
key: 'rename',
label: 'Rename', label: 'Rename',
leftSlot: <Icon icon="pencil" size="sm" />, leftSlot: <Icon icon="pencil" size="sm" />,
onSelect: async () => { onSelect: async () => {
@@ -277,7 +274,6 @@ function SidebarButton({
}, },
}, },
{ {
key: 'delete-environment',
color: 'danger', color: 'danger',
label: 'Delete', label: 'Delete',
leftSlot: <Icon icon="trash" size="sm" />, leftSlot: <Icon icon="trash" size="sm" />,
+20 -10
View File
@@ -1,5 +1,5 @@
import { emit } from '@tauri-apps/api/event'; import { emit } from '@tauri-apps/api/event';
import type { PromptTextRequest, PromptTextResponse } from '@yaakapp-internal/plugins'; import type { InternalEvent } from '@yaakapp-internal/plugins';
import type { ShowToastRequest } from '@yaakapp/api'; import type { ShowToastRequest } from '@yaakapp/api';
import { useSubscribeActiveWorkspaceId } from '../hooks/useActiveWorkspace'; import { useSubscribeActiveWorkspaceId } from '../hooks/useActiveWorkspace';
import { useActiveWorkspaceChangedToast } from '../hooks/useActiveWorkspaceChangedToast'; import { useActiveWorkspaceChangedToast } from '../hooks/useActiveWorkspaceChangedToast';
@@ -12,6 +12,7 @@ import { useSyncModelStores } from '../hooks/useSyncModelStores';
import { useSyncWorkspaceChildModels } from '../hooks/useSyncWorkspaceChildModels'; import { useSyncWorkspaceChildModels } from '../hooks/useSyncWorkspaceChildModels';
import { useSyncZoomSetting } from '../hooks/useSyncZoomSetting'; import { useSyncZoomSetting } from '../hooks/useSyncZoomSetting';
import { useSubscribeTemplateFunctions } from '../hooks/useTemplateFunctions'; import { useSubscribeTemplateFunctions } from '../hooks/useTemplateFunctions';
import { generateId } from '../lib/generateId';
import { showPrompt } from '../lib/prompt'; import { showPrompt } from '../lib/prompt';
import { showToast } from '../lib/toast'; import { showToast } from '../lib/toast';
@@ -36,15 +37,24 @@ export function GlobalHooks() {
showToast({ ...event.payload }); showToast({ ...event.payload });
}); });
// Listen for prompts // Listen for plugin events
useListenToTauriEvent<{ replyId: string; args: PromptTextRequest }>( useListenToTauriEvent<InternalEvent>('plugin_event', async ({ payload: event }) => {
'show_prompt', if (event.payload.type === 'prompt_text_request') {
async (event) => { const value = await showPrompt(event.payload);
const value = await showPrompt(event.payload.args); const result: InternalEvent = {
const result: PromptTextResponse = { value }; id: generateId(),
await emit(event.payload.replyId, result); replyId: event.id,
}, pluginName: event.pluginName,
); pluginRefId: event.pluginRefId,
windowContext: event.windowContext,
payload: {
type: 'prompt_text_response',
value,
},
};
await emit(event.id, result);
}
});
return null; return null;
} }
-3
View File
@@ -69,13 +69,11 @@ export function GraphQLEditor({ request, onChange, baseRequest, ...extraEditorPr
<Dropdown <Dropdown
items={[ items={[
{ {
key: 'refresh',
label: 'Refetch', label: 'Refetch',
leftSlot: <Icon icon="refresh" />, leftSlot: <Icon icon="refresh" />,
onSelect: refetch, onSelect: refetch,
}, },
{ {
key: 'clear',
label: 'Clear', label: 'Clear',
onSelect: clear, onSelect: clear,
hidden: !schema, hidden: !schema,
@@ -84,7 +82,6 @@ export function GraphQLEditor({ request, onChange, baseRequest, ...extraEditorPr
}, },
{ type: 'separator', label: 'Setting' }, { type: 'separator', label: 'Setting' },
{ {
key: 'auto_fetch',
label: 'Automatic Introspection', label: 'Automatic Introspection',
onSelect: () => { onSelect: () => {
setAutoIntrospectDisabled({ setAutoIntrospectDisabled({
@@ -6,7 +6,7 @@ import type { CSSProperties } from 'react';
import React, { useCallback, useMemo, useRef } from 'react'; import React, { useCallback, useMemo, useRef } from 'react';
import { useContainerSize } from '../hooks/useContainerQuery'; import { useContainerSize } from '../hooks/useContainerQuery';
import type { ReflectResponseService } from '../hooks/useGrpc'; import type { ReflectResponseService } from '../hooks/useGrpc';
import { useHttpAuthentication } from '../hooks/useHttpAuthentication'; import { useHttpAuthenticationSummaries } from '../hooks/useHttpAuthentication';
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey'; import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
import { useUpdateAnyGrpcRequest } from '../hooks/useUpdateAnyGrpcRequest'; import { useUpdateAnyGrpcRequest } from '../hooks/useUpdateAnyGrpcRequest';
import { fallbackRequestName } from '../lib/fallbackRequestName'; import { fallbackRequestName } from '../lib/fallbackRequestName';
@@ -69,7 +69,7 @@ export function GrpcConnectionSetupPane({
onSend, onSend,
}: Props) { }: Props) {
const updateRequest = useUpdateAnyGrpcRequest(); const updateRequest = useUpdateAnyGrpcRequest();
const authentication = useHttpAuthentication(); const authentication = useHttpAuthenticationSummaries();
const [activeTabs, setActiveTabs] = useAtom(tabsAtom); const [activeTabs, setActiveTabs] = useAtom(tabsAtom);
const { updateKey: forceUpdateKey } = useRequestUpdateKey(activeRequest.id ?? null); const { updateKey: forceUpdateKey } = useRequestUpdateKey(activeRequest.id ?? null);
@@ -237,7 +237,6 @@ export function GrpcConnectionSetupPane({
{ {
label: 'Refresh', label: 'Refresh',
type: 'default', type: 'default',
key: 'custom',
leftSlot: <Icon className="text-text-subtlest" size="sm" icon="refresh" />, leftSlot: <Icon className="text-text-subtlest" size="sm" icon="refresh" />,
}, },
]} ]}
+2 -1
View File
@@ -1,4 +1,5 @@
import type { HttpRequest } from '@yaakapp-internal/models'; import type { HttpRequest } from '@yaakapp-internal/models';
import type { GenericCompletionOption } from '@yaakapp-internal/plugins';
import { charsets } from '../lib/data/charsets'; import { charsets } from '../lib/data/charsets';
import { connections } from '../lib/data/connections'; import { connections } from '../lib/data/connections';
import { encodings } from '../lib/data/encodings'; import { encodings } from '../lib/data/encodings';
@@ -44,7 +45,7 @@ const headerOptionsMap: Record<string, string[]> = {
const valueAutocomplete = (headerName: string): GenericCompletionConfig | undefined => { const valueAutocomplete = (headerName: string): GenericCompletionConfig | undefined => {
const name = headerName.toLowerCase().trim(); const name = headerName.toLowerCase().trim();
const options: GenericCompletionConfig['options'] = const options: GenericCompletionOption[] =
headerOptionsMap[name]?.map((o) => ({ headerOptionsMap[name]?.map((o) => ({
label: o, label: o,
type: 'constant', type: 'constant',
+46 -12
View File
@@ -1,8 +1,14 @@
import type { GrpcRequest, HttpRequest } from '@yaakapp-internal/models'; import type { GrpcRequest, HttpRequest } from '@yaakapp-internal/models';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { useHttpAuthentication } from '../hooks/useHttpAuthentication'; import { useHttpAuthenticationConfig } from '../hooks/useHttpAuthenticationConfig';
import { useUpdateAnyGrpcRequest } from '../hooks/useUpdateAnyGrpcRequest'; import { useUpdateAnyGrpcRequest } from '../hooks/useUpdateAnyGrpcRequest';
import { useUpdateAnyHttpRequest } from '../hooks/useUpdateAnyHttpRequest'; import { useUpdateAnyHttpRequest } from '../hooks/useUpdateAnyHttpRequest';
import { Checkbox } from './core/Checkbox';
import type { DropdownItem } from './core/Dropdown';
import { Dropdown } from './core/Dropdown';
import { Icon } from './core/Icon';
import { IconButton } from './core/IconButton';
import { HStack } from './core/Stacks';
import { DynamicForm } from './DynamicForm'; import { DynamicForm } from './DynamicForm';
import { EmptyStateText } from './EmptyStateText'; import { EmptyStateText } from './EmptyStateText';
@@ -13,11 +19,15 @@ interface Props {
export function HttpAuthenticationEditor({ request }: Props) { export function HttpAuthenticationEditor({ request }: Props) {
const updateHttpRequest = useUpdateAnyHttpRequest(); const updateHttpRequest = useUpdateAnyHttpRequest();
const updateGrpcRequest = useUpdateAnyGrpcRequest(); const updateGrpcRequest = useUpdateAnyGrpcRequest();
const auths = useHttpAuthentication(); const auth = useHttpAuthenticationConfig(
const auth = auths.find((a) => a.name === request.authenticationType); request.authenticationType,
request.authentication,
request.id,
);
const handleChange = useCallback( const handleChange = useCallback(
(authentication: Record<string, boolean>) => { (authentication: Record<string, boolean>) => {
console.log('UPDATE', authentication);
if (request.model === 'http_request') { if (request.model === 'http_request') {
updateHttpRequest.mutate({ updateHttpRequest.mutate({
id: request.id, id: request.id,
@@ -33,18 +43,42 @@ export function HttpAuthenticationEditor({ request }: Props) {
[request.id, request.model, updateGrpcRequest, updateHttpRequest], [request.id, request.model, updateGrpcRequest, updateHttpRequest],
); );
if (auth == null) { if (auth.data == null) {
return <EmptyStateText>No Authentication {request.authenticationType}</EmptyStateText>; return <EmptyStateText>No Authentication {request.authenticationType}</EmptyStateText>;
} }
return ( return (
<DynamicForm <div className="h-full grid grid-rows-[auto_minmax(0,1fr)]">
autocompleteVariables <HStack space={2} className="mb-1" alignItems="center">
useTemplating <Checkbox
stateKey={`auth.${request.id}.${request.authenticationType}`} className="w-full"
config={auth.config} checked={!request.authentication.disabled}
data={request.authentication} onChange={(disabled) => handleChange({ ...request.authentication, disabled: !disabled })}
onChange={handleChange} title="Enabled"
/> />
{auth.data.actions && (
<Dropdown
items={auth.data.actions.map(
(a): DropdownItem => ({
label: a.label,
leftSlot: a.icon ? <Icon icon={a.icon} /> : null,
onSelect: () => a.call(request),
}),
)}
>
<IconButton title="Authentication Actions" icon="settings" size="xs" />
</Dropdown>
)}
</HStack>
<DynamicForm
disabled={request.authentication.disabled}
autocompleteVariables
useTemplating
stateKey={`auth.${request.id}.${request.authenticationType}`}
inputs={auth.data.args}
data={request.authentication}
onChange={handleChange}
/>
</div>
); );
} }
+27
View File
@@ -0,0 +1,27 @@
import remarkGfm from 'remark-gfm';
import ReactMarkdown, { type Components } from 'react-markdown';
import { Prose } from './Prose';
export function Markdown({ children, className }: { children: string; className?: string }) {
return (
<Prose className={className}>
<ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
{children}
</ReactMarkdown>
</Prose>
);
}
const markdownComponents: Partial<Components> = {
// Ensure links open in external browser by adding target="_blank"
a: ({ href, children, ...rest }) => {
if (href && !href.match(/https?:\/\//)) {
href = `http://${href}`;
}
return (
<a target="_blank" rel="noreferrer noopener" href={href} {...rest}>
{children}
</a>
);
},
};
+4 -23
View File
@@ -1,12 +1,9 @@
import classNames from 'classnames'; import classNames from 'classnames';
import { useRef, useState } from 'react'; import { useRef, useState } from 'react';
import type { Components } from 'react-markdown';
import Markdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import type { EditorProps } from './core/Editor/Editor'; import type { EditorProps } from './core/Editor/Editor';
import { Editor } from './core/Editor/Editor'; import { Editor } from './core/Editor/Editor';
import { Prose } from './Prose';
import { SegmentedControl } from './core/SegmentedControl'; import { SegmentedControl } from './core/SegmentedControl';
import { Markdown } from './Markdown';
type ViewMode = 'edit' | 'preview'; type ViewMode = 'edit' | 'preview';
@@ -47,11 +44,9 @@ export function MarkdownEditor({
defaultValue.length === 0 ? ( defaultValue.length === 0 ? (
<p className="text-text-subtlest">No description</p> <p className="text-text-subtlest">No description</p>
) : ( ) : (
<Prose className="max-w-xl overflow-y-auto max-h-full [&_*]:cursor-auto [&_*]:select-auto"> <Markdown className="max-w-xl overflow-y-auto max-h-full [&_*]:cursor-auto [&_*]:select-auto">
<Markdown remarkPlugins={[remarkGfm]} components={markdownComponents}> {defaultValue}
{defaultValue} </Markdown>
</Markdown>
</Prose>
); );
const contents = viewMode === 'preview' ? preview : editor; const contents = viewMode === 'preview' ? preview : editor;
@@ -88,17 +83,3 @@ export function MarkdownEditor({
</div> </div>
); );
} }
const markdownComponents: Partial<Components> = {
// Ensure links open in external browser by adding target="_blank"
a: ({ href, children, ...rest }) => {
if (href && !href.match(/https?:\/\//)) {
href = `http://${href}`;
}
return (
<a target="_blank" rel="noreferrer noopener" href={href} {...rest}>
{children}
</a>
);
},
};
+5
View File
@@ -5,6 +5,10 @@
@apply mt-0; @apply mt-0;
} }
& > :last-child {
@apply mb-0;
}
img, img,
video, video,
p, p,
@@ -107,6 +111,7 @@
ul code { ul code {
@apply text-xs bg-surface-active text-info font-normal whitespace-nowrap; @apply text-xs bg-surface-active text-info font-normal whitespace-nowrap;
@apply px-1.5 py-0.5 rounded not-italic; @apply px-1.5 py-0.5 rounded not-italic;
@apply select-text;
} }
pre { pre {
@@ -27,13 +27,11 @@ export function RecentConnectionsDropdown({
<Dropdown <Dropdown
items={[ items={[
{ {
key: 'clear-single',
label: 'Clear Connection', label: 'Clear Connection',
onSelect: deleteConnection.mutate, onSelect: deleteConnection.mutate,
disabled: connections.length === 0, disabled: connections.length === 0,
}, },
{ {
key: 'clear-all',
label: `Clear ${pluralizeCount('Connection', connections.length)}`, label: `Clear ${pluralizeCount('Connection', connections.length)}`,
onSelect: deleteAllConnections.mutate, onSelect: deleteAllConnections.mutate,
hidden: connections.length <= 1, hidden: connections.length <= 1,
@@ -41,7 +39,6 @@ export function RecentConnectionsDropdown({
}, },
{ type: 'separator', label: 'History' }, { type: 'separator', label: 'History' },
...connections.slice(0, 20).map((c) => ({ ...connections.slice(0, 20).map((c) => ({
key: c.id,
label: ( label: (
<HStack space={2}> <HStack space={2}>
{formatDistanceToNowStrict(c.createdAt + 'Z')} ago &bull;{' '} {formatDistanceToNowStrict(c.createdAt + 'Z')} ago &bull;{' '}
@@ -58,7 +58,6 @@ export function RecentRequestsDropdown({ className }: Props) {
if (request === undefined) continue; if (request === undefined) continue;
recentRequestItems.push({ recentRequestItems.push({
key: request.id,
label: fallbackRequestName(request), label: fallbackRequestName(request),
// leftSlot: <CountBadge className="!ml-0 px-0 w-5" count={recentRequestItems.length} />, // leftSlot: <CountBadge className="!ml-0 px-0 w-5" count={recentRequestItems.length} />,
leftSlot: <HttpMethodTag className="text-right" shortNames request={request} />, leftSlot: <HttpMethodTag className="text-right" shortNames request={request} />,
@@ -32,7 +32,6 @@ export const RecentResponsesDropdown = function ResponsePane({
<Dropdown <Dropdown
items={[ items={[
{ {
key: 'save',
label: 'Save to File', label: 'Save to File',
onSelect: saveResponse.mutate, onSelect: saveResponse.mutate,
leftSlot: <Icon icon="save" />, leftSlot: <Icon icon="save" />,
@@ -40,7 +39,6 @@ export const RecentResponsesDropdown = function ResponsePane({
disabled: activeResponse.state !== 'closed' && activeResponse.status >= 100, disabled: activeResponse.state !== 'closed' && activeResponse.status >= 100,
}, },
{ {
key: 'copy',
label: 'Copy Body', label: 'Copy Body',
onSelect: copyResponse.mutate, onSelect: copyResponse.mutate,
leftSlot: <Icon icon="copy" />, leftSlot: <Icon icon="copy" />,
@@ -48,13 +46,11 @@ export const RecentResponsesDropdown = function ResponsePane({
disabled: activeResponse.state !== 'closed' && activeResponse.status >= 100, disabled: activeResponse.state !== 'closed' && activeResponse.status >= 100,
}, },
{ {
key: 'clear-single',
label: 'Delete', label: 'Delete',
leftSlot: <Icon icon="trash" />, leftSlot: <Icon icon="trash" />,
onSelect: deleteResponse.mutate, onSelect: deleteResponse.mutate,
}, },
{ {
key: 'unpin',
label: 'Unpin Response', label: 'Unpin Response',
onSelect: () => onPinnedResponseId(activeResponse.id), onSelect: () => onPinnedResponseId(activeResponse.id),
leftSlot: <Icon icon="unpin" />, leftSlot: <Icon icon="unpin" />,
@@ -63,7 +59,6 @@ export const RecentResponsesDropdown = function ResponsePane({
}, },
{ type: 'separator', label: 'History' }, { type: 'separator', label: 'History' },
{ {
key: 'clear-all',
label: `Delete ${responses.length} ${pluralize('Response', responses.length)}`, label: `Delete ${responses.length} ${pluralize('Response', responses.length)}`,
onSelect: deleteAllResponses.mutate, onSelect: deleteAllResponses.mutate,
hidden: responses.length === 0, hidden: responses.length === 0,
@@ -71,7 +66,6 @@ export const RecentResponsesDropdown = function ResponsePane({
}, },
{ type: 'separator' }, { type: 'separator' },
...responses.slice(0, 20).map((r: HttpResponse) => ({ ...responses.slice(0, 20).map((r: HttpResponse) => ({
key: r.id,
label: ( label: (
<HStack space={2}> <HStack space={2}>
<StatusTag className="text-sm" response={r} /> <StatusTag className="text-sm" response={r} />
+4 -6
View File
@@ -1,4 +1,5 @@
import type { HttpRequest } from '@yaakapp-internal/models'; import type { HttpRequest } from '@yaakapp-internal/models';
import type {GenericCompletionOption} from "@yaakapp-internal/plugins";
import classNames from 'classnames'; import classNames from 'classnames';
import { atom, useAtom, useAtomValue } from 'jotai'; import { atom, useAtom, useAtomValue } from 'jotai';
import { atomWithStorage } from 'jotai/utils'; import { atomWithStorage } from 'jotai/utils';
@@ -8,7 +9,7 @@ import { activeRequestIdAtom } from '../hooks/useActiveRequestId';
import { useCancelHttpResponse } from '../hooks/useCancelHttpResponse'; import { useCancelHttpResponse } from '../hooks/useCancelHttpResponse';
import { useContentTypeFromHeaders } from '../hooks/useContentTypeFromHeaders'; import { useContentTypeFromHeaders } from '../hooks/useContentTypeFromHeaders';
import { grpcRequestsAtom } from '../hooks/useGrpcRequests'; import { grpcRequestsAtom } from '../hooks/useGrpcRequests';
import { useHttpAuthentication } from '../hooks/useHttpAuthentication'; import { useHttpAuthenticationSummaries } from '../hooks/useHttpAuthentication';
import { httpRequestsAtom } from '../hooks/useHttpRequests'; import { httpRequestsAtom } from '../hooks/useHttpRequests';
import { useImportCurl } from '../hooks/useImportCurl'; import { useImportCurl } from '../hooks/useImportCurl';
import { useImportQuerystring } from '../hooks/useImportQuerystring'; import { useImportQuerystring } from '../hooks/useImportQuerystring';
@@ -36,10 +37,7 @@ import { showToast } from '../lib/toast';
import { BinaryFileEditor } from './BinaryFileEditor'; import { BinaryFileEditor } from './BinaryFileEditor';
import { CountBadge } from './core/CountBadge'; import { CountBadge } from './core/CountBadge';
import { Editor } from './core/Editor/Editor'; import { Editor } from './core/Editor/Editor';
import type { import type { GenericCompletionConfig } from './core/Editor/genericCompletion';
GenericCompletionConfig,
GenericCompletionOption,
} from './core/Editor/genericCompletion';
import { InlineCode } from './core/InlineCode'; import { InlineCode } from './core/InlineCode';
import type { Pair } from './core/PairEditor'; import type { Pair } from './core/PairEditor';
import { PlainInput } from './core/PlainInput'; import { PlainInput } from './core/PlainInput';
@@ -93,7 +91,7 @@ export const RequestPane = memo(function RequestPane({
const { updateKey: forceUpdateKey } = useRequestUpdateKey(activeRequest.id ?? null); const { updateKey: forceUpdateKey } = useRequestUpdateKey(activeRequest.id ?? null);
const [{ urlKey }] = useRequestEditor(); const [{ urlKey }] = useRequestEditor();
const contentType = useContentTypeFromHeaders(activeRequest.headers); const contentType = useContentTypeFromHeaders(activeRequest.headers);
const authentication = useHttpAuthentication(); const authentication = useHttpAuthenticationSummaries();
const handleContentTypeChange = useCallback( const handleContentTypeChange = useCallback(
async (contentType: string | null) => { async (contentType: string | null) => {
-7
View File
@@ -28,14 +28,12 @@ export function SettingsDropdown() {
ref={dropdownRef} ref={dropdownRef}
items={[ items={[
{ {
key: 'settings',
label: 'Settings', label: 'Settings',
hotKeyAction: 'settings.show', hotKeyAction: 'settings.show',
leftSlot: <Icon icon="settings" />, leftSlot: <Icon icon="settings" />,
onSelect: openSettings.mutate, onSelect: openSettings.mutate,
}, },
{ {
key: 'hotkeys',
label: 'Keyboard shortcuts', label: 'Keyboard shortcuts',
hotKeyAction: 'hotkeys.showHelp', hotKeyAction: 'hotkeys.showHelp',
leftSlot: <Icon icon="keyboard" />, leftSlot: <Icon icon="keyboard" />,
@@ -49,33 +47,28 @@ export function SettingsDropdown() {
}, },
}, },
{ {
key: 'import-data',
label: 'Import Data', label: 'Import Data',
leftSlot: <Icon icon="folder_input" />, leftSlot: <Icon icon="folder_input" />,
onSelect: () => importData.mutate(), onSelect: () => importData.mutate(),
}, },
{ {
key: 'export-data',
label: 'Export Data', label: 'Export Data',
leftSlot: <Icon icon="folder_output" />, leftSlot: <Icon icon="folder_output" />,
onSelect: () => exportData.mutate(), onSelect: () => exportData.mutate(),
}, },
{ type: 'separator', label: `Yaak v${appInfo.version}` }, { type: 'separator', label: `Yaak v${appInfo.version}` },
{ {
key: 'update-check',
label: 'Check for Updates', label: 'Check for Updates',
leftSlot: <Icon icon="update" />, leftSlot: <Icon icon="update" />,
onSelect: () => checkForUpdates.mutate(), onSelect: () => checkForUpdates.mutate(),
}, },
{ {
key: 'feedback',
label: 'Feedback', label: 'Feedback',
leftSlot: <Icon icon="chat" />, leftSlot: <Icon icon="chat" />,
rightSlot: <Icon icon="external_link" />, rightSlot: <Icon icon="external_link" />,
onSelect: () => openUrl('https://yaak.app/roadmap'), onSelect: () => openUrl('https://yaak.app/roadmap'),
}, },
{ {
key: 'changelog',
label: 'Changelog', label: 'Changelog',
leftSlot: <Icon icon="cake" />, leftSlot: <Icon icon="cake" />,
rightSlot: <Icon icon="external_link" />, rightSlot: <Icon icon="external_link" />,
@@ -46,13 +46,11 @@ export function SidebarItemContextMenu({ child, show, close }: Props) {
if (child.model === 'folder') { if (child.model === 'folder') {
return [ return [
{ {
key: 'send-all',
label: 'Send All', label: 'Send All',
leftSlot: <Icon icon="send_horizontal" />, leftSlot: <Icon icon="send_horizontal" />,
onSelect: () => sendManyRequests.mutate(child.children.map((c) => c.id)), onSelect: () => sendManyRequests.mutate(child.children.map((c) => c.id)),
}, },
{ {
key: 'folder-settings',
label: 'Settings', label: 'Settings',
leftSlot: <Icon icon="settings" />, leftSlot: <Icon icon="settings" />,
onSelect: () => onSelect: () =>
@@ -64,13 +62,11 @@ export function SidebarItemContextMenu({ child, show, close }: Props) {
}), }),
}, },
{ {
key: 'duplicateFolder',
label: 'Duplicate', label: 'Duplicate',
leftSlot: <Icon icon="copy" />, leftSlot: <Icon icon="copy" />,
onSelect: () => duplicateFolder.mutate(), onSelect: () => duplicateFolder.mutate(),
}, },
{ {
key: 'delete-folder',
label: 'Delete', label: 'Delete',
color: 'danger', color: 'danger',
leftSlot: <Icon icon="trash" />, leftSlot: <Icon icon="trash" />,
@@ -84,7 +80,6 @@ export function SidebarItemContextMenu({ child, show, close }: Props) {
child.model === 'http_request' child.model === 'http_request'
? [ ? [
{ {
key: 'send-request',
label: 'Send', label: 'Send',
hotKeyAction: 'http_request.send', hotKeyAction: 'http_request.send',
hotKeyLabelOnly: true, // Already bound in URL bar hotKeyLabelOnly: true, // Already bound in URL bar
@@ -92,7 +87,6 @@ export function SidebarItemContextMenu({ child, show, close }: Props) {
onSelect: () => sendRequest.mutate(child.id), onSelect: () => sendRequest.mutate(child.id),
}, },
...httpRequestActions.map((a) => ({ ...httpRequestActions.map((a) => ({
key: a.key,
label: a.label, label: a.label,
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
leftSlot: <Icon icon={(a.icon as any) ?? 'empty'} />, leftSlot: <Icon icon={(a.icon as any) ?? 'empty'} />,
@@ -107,13 +101,11 @@ export function SidebarItemContextMenu({ child, show, close }: Props) {
return [ return [
...requestItems, ...requestItems,
{ {
key: 'rename-request',
label: 'Rename', label: 'Rename',
leftSlot: <Icon icon="pencil" />, leftSlot: <Icon icon="pencil" />,
onSelect: renameRequest.mutate, onSelect: renameRequest.mutate,
}, },
{ {
key: 'duplicate-request',
label: 'Duplicate', label: 'Duplicate',
hotKeyAction: 'http_request.duplicate', hotKeyAction: 'http_request.duplicate',
hotKeyLabelOnly: true, // Would trigger for every request (bad) hotKeyLabelOnly: true, // Would trigger for every request (bad)
@@ -124,14 +116,12 @@ export function SidebarItemContextMenu({ child, show, close }: Props) {
: duplicateGrpcRequest.mutate(), : duplicateGrpcRequest.mutate(),
}, },
{ {
key: 'move-workspace',
label: 'Move', label: 'Move',
leftSlot: <Icon icon="arrow_right_circle" />, leftSlot: <Icon icon="arrow_right_circle" />,
hidden: workspaces.length <= 1, hidden: workspaces.length <= 1,
onSelect: moveToWorkspace.mutate, onSelect: moveToWorkspace.mutate,
}, },
{ {
key: 'delete-request',
color: 'danger', color: 'danger',
label: 'Delete', label: 'Delete',
hotKeyAction: 'http_request.delete', hotKeyAction: 'http_request.delete',
@@ -25,6 +25,10 @@ export function TemplateFunctionDialog({ templateFunction, hide, initialTokens,
? initialTokens.tokens[0]?.val.args ? initialTokens.tokens[0]?.val.args
: []; : [];
for (const arg of templateFunction.args) { for (const arg of templateFunction.args) {
if (!('name' in arg)) {
// Skip visual-only args
continue;
}
const initialArg = initialArgs.find((a) => a.name === arg.name); const initialArg = initialArgs.find((a) => a.name === arg.name);
const initialArgValue = const initialArgValue =
initialArg?.value.type === 'str' initialArg?.value.type === 'str'
@@ -79,7 +83,7 @@ export function TemplateFunctionDialog({ templateFunction, hide, initialTokens,
<VStack className="pb-3" space={4}> <VStack className="pb-3" space={4}>
<h1 className="font-mono !text-base">{templateFunction.name}()</h1> <h1 className="font-mono !text-base">{templateFunction.name}()</h1>
<DynamicForm <DynamicForm
config={templateFunction.args} inputs={templateFunction.args}
data={argValues} data={argValues}
onChange={setArgValues} onChange={setArgValues}
stateKey={`template_function.${templateFunction.name}`} stateKey={`template_function.${templateFunction.name}`}
@@ -46,7 +46,6 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
const extraItems: DropdownItem[] = [ const extraItems: DropdownItem[] = [
{ {
key: 'workspace-settings',
label: 'Workspace Settings', label: 'Workspace Settings',
leftSlot: <Icon icon="settings" />, leftSlot: <Icon icon="settings" />,
hotKeyAction: 'workspace_settings.show', hotKeyAction: 'workspace_settings.show',
@@ -62,7 +61,6 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
}, },
}, },
{ {
key: 'reveal-workspace-sync-dir',
label: revealInFinderText, label: revealInFinderText,
hidden: workspaceMeta == null || workspaceMeta.settingSyncDir == null, hidden: workspaceMeta == null || workspaceMeta.settingSyncDir == null,
leftSlot: <Icon icon="folder_open" />, leftSlot: <Icon icon="folder_open" />,
@@ -72,7 +70,6 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
}, },
}, },
{ {
key: 'delete-responses',
label: 'Clear Send History', label: 'Clear Send History',
color: 'warning', color: 'warning',
leftSlot: <Icon icon="history" />, leftSlot: <Icon icon="history" />,
@@ -80,13 +77,11 @@ export const WorkspaceActionsDropdown = memo(function WorkspaceActionsDropdown({
}, },
{ type: 'separator' }, { type: 'separator' },
{ {
key: 'create-workspace',
label: 'New Workspace', label: 'New Workspace',
leftSlot: <Icon icon="plus" />, leftSlot: <Icon icon="plus" />,
onSelect: createWorkspace, onSelect: createWorkspace,
}, },
{ {
key: 'open-workspace',
label: 'Open Workspace', label: 'Open Workspace',
leftSlot: <Icon icon="folder" />, leftSlot: <Icon icon="folder" />,
onSelect: openWorkspaceFromSyncDir.mutate, onSelect: openWorkspaceFromSyncDir.mutate,
+2 -2
View File
@@ -7,7 +7,7 @@ interface Props {
color?: 'primary' | 'secondary' | 'success' | 'notice' | 'warning' | 'danger' | 'info'; color?: 'primary' | 'secondary' | 'success' | 'notice' | 'warning' | 'danger' | 'info';
} }
export function Banner({ children, className, color = 'secondary' }: Props) { export function Banner({ children, className, color }: Props) {
return ( return (
<div> <div>
<div <div
@@ -16,7 +16,7 @@ export function Banner({ children, className, color = 'secondary' }: Props) {
`x-theme-banner--${color}`, `x-theme-banner--${color}`,
'whitespace-pre-wrap', 'whitespace-pre-wrap',
'border border-dashed border-border bg-surface', 'border border-dashed border-border bg-surface',
'px-3 py-2 rounded select-auto cursor-text', 'px-3 py-2 rounded select-auto',
'overflow-x-auto text-text', 'overflow-x-auto text-text',
)} )}
> >
+6 -8
View File
@@ -1,5 +1,5 @@
import classNames from 'classnames'; import classNames from 'classnames';
import type { ReactNode } from 'react'; import { type ReactNode } from 'react';
import { trackEvent } from '../../lib/analytics'; import { trackEvent } from '../../lib/analytics';
import { Icon } from './Icon'; import { Icon } from './Icon';
import { HStack } from './Stacks'; import { HStack } from './Stacks';
@@ -26,17 +26,15 @@ export function Checkbox({
event, event,
}: CheckboxProps) { }: CheckboxProps) {
return ( return (
<HStack <HStack as="label" space={2} className={classNames(className, 'text-text mr-auto')}>
as="label"
space={2}
className={classNames(className, 'text-text mr-auto', disabled && 'opacity-disabled')}
>
<div className={classNames(inputWrapperClassName, 'x-theme-input', 'relative flex')}> <div className={classNames(inputWrapperClassName, 'x-theme-input', 'relative flex')}>
<input <input
aria-hidden aria-hidden
className={classNames( className={classNames(
'appearance-none w-4 h-4 flex-shrink-0 border border-border', 'appearance-none w-4 h-4 flex-shrink-0 border border-border',
'rounded hocus:border-border-focus hocus:bg-focus/[5%] outline-none ring-0', 'rounded outline-none ring-0',
!disabled && 'hocus:border-border-focus hocus:bg-focus/[5%] ',
disabled && 'border-dotted',
)} )}
type="checkbox" type="checkbox"
disabled={disabled} disabled={disabled}
@@ -54,7 +52,7 @@ export function Checkbox({
/> />
</div> </div>
</div> </div>
{!hideLabel && title} <span className={classNames(disabled && 'opacity-disabled')}>{!hideLabel && title}</span>
</HStack> </HStack>
); );
} }
+5 -5
View File
@@ -44,7 +44,6 @@ export type DropdownItemSeparator = {
}; };
export type DropdownItemDefault = { export type DropdownItemDefault = {
key: string;
type?: 'default'; type?: 'default';
label: ReactNode; label: ReactNode;
keepOpen?: boolean; keepOpen?: boolean;
@@ -465,11 +464,12 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle' | 'items'
return ( return (
<> <>
{items.map( {items.map(
(item) => (item, i) =>
item.type !== 'separator' && item.type !== 'separator' &&
!item.hotKeyLabelOnly && ( !item.hotKeyLabelOnly &&
item.hotKeyAction && (
<MenuItemHotKey <MenuItemHotKey
key={item.key} key={`${item.hotKeyAction}::${i}`}
onSelect={handleSelect} onSelect={handleSelect}
item={item} item={item}
action={item.hotKeyAction} action={item.hotKeyAction}
@@ -542,7 +542,7 @@ const Menu = forwardRef<Omit<DropdownRef, 'open' | 'isOpen' | 'toggle' | 'items'
focused={i === selectedIndex} focused={i === selectedIndex}
onFocus={handleFocus} onFocus={handleFocus}
onSelect={handleSelect} onSelect={handleSelect}
key={item.key} key={`item_${i}`}
item={item} item={item}
/> />
); );
+3 -1
View File
@@ -5,6 +5,7 @@
@apply w-full block text-base; @apply w-full block text-base;
/* Regular cursor */ /* Regular cursor */
.cm-cursor { .cm-cursor {
@apply border-text !important; @apply border-text !important;
/* Widen the cursor a bit */ /* Widen the cursor a bit */
@@ -12,6 +13,7 @@
} }
/* Vim-mode cursor */ /* Vim-mode cursor */
.cm-fat-cursor { .cm-fat-cursor {
@apply outline-0 bg-text !important; @apply outline-0 bg-text !important;
@apply text-surface !important; @apply text-surface !important;
@@ -181,7 +183,7 @@
@apply hidden !important; @apply hidden !important;
} }
&.cm-singleline .cm-line { &.cm-singleline * {
@apply cursor-default; @apply cursor-default;
} }
} }
+60 -14
View File
@@ -1,4 +1,4 @@
import { defaultKeymap, historyField } from '@codemirror/commands'; import { defaultKeymap, historyField, indentWithTab } from '@codemirror/commands';
import { foldState, forceParsing } from '@codemirror/language'; import { foldState, forceParsing } from '@codemirror/language';
import type { EditorStateConfig, Extension } from '@codemirror/state'; import type { EditorStateConfig, Extension } from '@codemirror/state';
import { Compartment, EditorState } from '@codemirror/state'; import { Compartment, EditorState } from '@codemirror/state';
@@ -34,14 +34,22 @@ import { TemplateVariableDialog } from '../../TemplateVariableDialog';
import { IconButton } from '../IconButton'; import { IconButton } from '../IconButton';
import { HStack } from '../Stacks'; import { HStack } from '../Stacks';
import './Editor.css'; import './Editor.css';
import { baseExtensions, getLanguageExtension, multiLineExtensions } from './extensions'; import {
baseExtensions,
getLanguageExtension,
multiLineExtensions,
readonlyExtensions,
} from './extensions';
import type { GenericCompletionConfig } from './genericCompletion'; import type { GenericCompletionConfig } from './genericCompletion';
import { singleLineExtensions } from './singleLine'; import { singleLineExtensions } from './singleLine';
// VSCode's Tab actions mess with the single-line editor tab actions, so remove it.
const vsCodeWithoutTab = vscodeKeymap.filter((k) => k.key !== 'Tab');
const keymapExtensions: Record<EditorKeymap, Extension> = { const keymapExtensions: Record<EditorKeymap, Extension> = {
vim: vim(), vim: vim(),
emacs: emacs(), emacs: emacs(),
vscode: keymap.of(vscodeKeymap), vscode: keymap.of(vsCodeWithoutTab),
default: [], default: [],
}; };
@@ -68,6 +76,7 @@ export interface EditorProps {
onKeyDown?: (e: KeyboardEvent) => void; onKeyDown?: (e: KeyboardEvent) => void;
singleLine?: boolean; singleLine?: boolean;
wrapLines?: boolean; wrapLines?: boolean;
disableTabIndent?: boolean;
format?: (v: string) => Promise<string>; format?: (v: string) => Promise<string>;
autocomplete?: GenericCompletionConfig; autocomplete?: GenericCompletionConfig;
autocompleteVariables?: boolean; autocompleteVariables?: boolean;
@@ -85,9 +94,9 @@ const emptyExtension: Extension = [];
export const Editor = forwardRef<EditorView | undefined, EditorProps>(function Editor( export const Editor = forwardRef<EditorView | undefined, EditorProps>(function Editor(
{ {
readOnly, readOnly,
type = 'text', type,
heightMode, heightMode,
language = 'text', language,
autoFocus, autoFocus,
autoSelect, autoSelect,
placeholder, placeholder,
@@ -101,6 +110,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
onBlur, onBlur,
onKeyDown, onKeyDown,
className, className,
disabled,
singleLine, singleLine,
format, format,
autocomplete, autocomplete,
@@ -108,6 +118,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
autocompleteVariables, autocompleteVariables,
actions, actions,
wrapLines, wrapLines,
disableTabIndent,
hideGutter, hideGutter,
stateKey, stateKey,
}: EditorProps, }: EditorProps,
@@ -122,6 +133,20 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
wrapLines = settings.editorSoftWrap; wrapLines = settings.editorSoftWrap;
} }
if (disabled) {
readOnly = true;
}
if (
singleLine ||
language == null ||
language === 'text' ||
language === 'url' ||
language === 'pairs'
) {
disableTabIndent = true;
}
const cm = useRef<{ view: EditorView; languageCompartment: Compartment } | null>(null); const cm = useRef<{ view: EditorView; languageCompartment: Compartment } | null>(null);
useImperativeHandle(ref, () => cm.current?.view, []); useImperativeHandle(ref, () => cm.current?.view, []);
@@ -166,7 +191,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
useEffect( useEffect(
function configurePlaceholder() { function configurePlaceholder() {
if (cm.current === null) return; if (cm.current === null) return;
const ext = placeholderExt(placeholderElFromText(placeholder ?? '', type)); const ext = placeholderExt(placeholderElFromText(placeholder, type));
const effects = placeholderCompartment.current.reconfigure(ext); const effects = placeholderCompartment.current.reconfigure(ext);
cm.current?.view.dispatch({ effects }); cm.current?.view.dispatch({ effects });
}, },
@@ -209,6 +234,23 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
[wrapLines], [wrapLines],
); );
// Update tab indent
const tabIndentCompartment = useRef(new Compartment());
useEffect(
function configureTabIndent() {
if (cm.current === null) return;
const current = tabIndentCompartment.current.get(cm.current.view.state) ?? emptyExtension;
// PERF: This is expensive with hundreds of editors on screen, so only do it when necessary
if (disableTabIndent && current !== emptyExtension) return; // Nothing to do
if (!disableTabIndent && current === emptyExtension) return; // Nothing to do
const ext = !disableTabIndent ? keymap.of([indentWithTab]) : emptyExtension;
const effects = tabIndentCompartment.current.reconfigure(ext);
cm.current?.view.dispatch({ effects });
},
[disableTabIndent],
);
const onClickFunction = useCallback( const onClickFunction = useCallback(
async (fn: TemplateFunction, tagValue: string, startPos: number) => { async (fn: TemplateFunction, tagValue: string, startPos: number) => {
const initialTokens = await parseTemplate(tagValue); const initialTokens = await parseTemplate(tagValue);
@@ -342,9 +384,12 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
const extensions = [ const extensions = [
languageCompartment.of(langExt), languageCompartment.of(langExt),
placeholderCompartment.current.of( placeholderCompartment.current.of(
placeholderExt(placeholderElFromText(placeholder ?? '', type)), placeholderExt(placeholderElFromText(placeholder, type)),
), ),
wrapLinesCompartment.current.of(wrapLines ? EditorView.lineWrapping : emptyExtension), wrapLinesCompartment.current.of(wrapLines ? EditorView.lineWrapping : emptyExtension),
tabIndentCompartment.current.of(
!disableTabIndent ? keymap.of([indentWithTab]) : emptyExtension,
),
keymapCompartment.current.of( keymapCompartment.current.of(
keymapExtensions[settings.editorKeymap] ?? keymapExtensions['default'], keymapExtensions[settings.editorKeymap] ?? keymapExtensions['default'],
), ),
@@ -475,6 +520,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
className={classNames( className={classNames(
className, className,
'cm-wrapper text-base', 'cm-wrapper text-base',
disabled && 'opacity-disabled',
type === 'password' && 'cm-obscure-text', type === 'password' && 'cm-obscure-text',
heightMode === 'auto' ? 'cm-auto-height' : 'cm-full-height', heightMode === 'auto' ? 'cm-auto-height' : 'cm-full-height',
singleLine ? 'cm-singleline' : 'cm-multiline', singleLine ? 'cm-singleline' : 'cm-multiline',
@@ -557,10 +603,8 @@ function getExtensions({
tooltips({ parent }), tooltips({ parent }),
keymap.of(singleLine ? defaultKeymap.filter((k) => k.key !== 'Enter') : defaultKeymap), keymap.of(singleLine ? defaultKeymap.filter((k) => k.key !== 'Enter') : defaultKeymap),
...(singleLine ? [singleLineExtensions()] : []), ...(singleLine ? [singleLineExtensions()] : []),
...(!singleLine ? [multiLineExtensions({ hideGutter })] : []), ...(!singleLine ? multiLineExtensions({ hideGutter }) : []),
...(readOnly ...(readOnly ? readonlyExtensions : []),
? [EditorState.readOnly.of(true), EditorView.contentAttributes.of({ tabindex: '-1' })]
: []),
// ------------------------ // // ------------------------ //
// Things that must be last // // Things that must be last //
@@ -580,13 +624,15 @@ function getExtensions({
]; ];
} }
const placeholderElFromText = (text: string, type: EditorProps['type']) => { const placeholderElFromText = (text: string | undefined, type: EditorProps['type']) => {
const el = document.createElement('div'); const el = document.createElement('div');
if (type === 'password') { if (type === 'password') {
// Will be obscured (dots) so just needs to be something to take up space // Will be obscured (dots) so just needs to be something to take up space
el.innerHTML = 'something-cool'; el.innerHTML = 'something-cool';
el.setAttribute('aria-hidden', 'true'); el.setAttribute('aria-hidden', 'true');
} else { } else {
// Default to <SPACE> because codemirror needs it for sizing. I'm not sure why, but probably something
// to do with how Yaak "hacks" it with CSS for single line input.
el.innerHTML = text ? text.replaceAll('\n', '<br/>') : ' '; el.innerHTML = text ? text.replaceAll('\n', '<br/>') : ' ';
} }
return el; return el;
@@ -596,8 +642,8 @@ function saveCachedEditorState(stateKey: string | null, state: EditorState | nul
if (!stateKey || state == null) return; if (!stateKey || state == null) return;
const stateObj = state.toJSON(stateFields); const stateObj = state.toJSON(stateFields);
// Save state in sessionStorage by removing doc and saving the hash of it instead // Save state in sessionStorage by removing doc and saving the hash of it instead.
// This will be checked on restore and put back in if it matches // This will be checked on restore and put back in if it matches.
stateObj.docHash = md5(stateObj.doc); stateObj.docHash = md5(stateObj.doc);
delete stateObj.doc; delete stateObj.doc;
+7 -2
View File
@@ -4,7 +4,7 @@ import {
closeBracketsKeymap, closeBracketsKeymap,
completionKeymap, completionKeymap,
} from '@codemirror/autocomplete'; } from '@codemirror/autocomplete';
import { history, historyKeymap, indentWithTab } from '@codemirror/commands'; import { history, historyKeymap } from '@codemirror/commands';
import { javascript } from '@codemirror/lang-javascript'; import { javascript } from '@codemirror/lang-javascript';
import { json } from '@codemirror/lang-json'; import { json } from '@codemirror/lang-json';
import { markdown } from '@codemirror/lang-markdown'; import { markdown } from '@codemirror/lang-markdown';
@@ -142,6 +142,11 @@ export const baseExtensions = [
keymap.of([...historyKeymap, ...completionKeymap]), keymap.of([...historyKeymap, ...completionKeymap]),
]; ];
export const readonlyExtensions = [
EditorState.readOnly.of(true),
EditorView.contentAttributes.of({ tabindex: '-1' }),
];
export const multiLineExtensions = ({ hideGutter }: { hideGutter?: boolean }) => [ export const multiLineExtensions = ({ hideGutter }: { hideGutter?: boolean }) => [
hideGutter hideGutter
? [] ? []
@@ -208,5 +213,5 @@ export const multiLineExtensions = ({ hideGutter }: { hideGutter?: boolean }) =>
rectangularSelection(), rectangularSelection(),
crosshairCursor(), crosshairCursor(),
highlightActiveLineGutter(), highlightActiveLineGutter(),
keymap.of([indentWithTab, ...closeBracketsKeymap, ...searchKeymap, ...foldKeymap, ...lintKeymap]), keymap.of([...closeBracketsKeymap, ...searchKeymap, ...foldKeymap, ...lintKeymap]),
]; ];
@@ -1,16 +1,5 @@
import type { CompletionContext } from '@codemirror/autocomplete'; import type { CompletionContext } from '@codemirror/autocomplete';
import type { GenericCompletionOption } from '@yaakapp-internal/plugins';
export interface GenericCompletionOption {
label: string;
type: 'constant' | 'variable';
detail?: string;
info?: string;
/** When given, should be a number from -99 to 99 that adjusts
* how this completion is ranked compared to other completions
* that match the input as well as this one. A negative number
* moves it down the list, a positive number moves it up. */
boost?: number;
}
export interface GenericCompletionConfig { export interface GenericCompletionConfig {
minMatch?: number; minMatch?: number;
+25 -8
View File
@@ -16,6 +16,7 @@ export type InputProps = Pick<
| 'useTemplating' | 'useTemplating'
| 'autocomplete' | 'autocomplete'
| 'forceUpdateKey' | 'forceUpdateKey'
| 'disabled'
| 'autoFocus' | 'autoFocus'
| 'autoSelect' | 'autoSelect'
| 'autocompleteVariables' | 'autocompleteVariables'
@@ -75,6 +76,7 @@ export const Input = forwardRef<EditorView, InputProps>(function Input(
readOnly, readOnly,
stateKey, stateKey,
multiLine, multiLine,
disabled,
...props ...props
}: InputProps, }: InputProps,
ref, ref,
@@ -82,18 +84,26 @@ export const Input = forwardRef<EditorView, InputProps>(function Input(
const [obscured, setObscured] = useStateWithDeps(type === 'password', [type]); const [obscured, setObscured] = useStateWithDeps(type === 'password', [type]);
const [currentValue, setCurrentValue] = useState(defaultValue ?? ''); const [currentValue, setCurrentValue] = useState(defaultValue ?? '');
const [focused, setFocused] = useState(false); const [focused, setFocused] = useState(false);
const [hasChanged, setHasChanged] = useStateWithDeps<boolean>(false, [stateKey, forceUpdateKey]);
const editorRef = useRef<EditorView | null>(null); const editorRef = useRef<EditorView | null>(null);
useImperativeHandle<EditorView | null, EditorView | null>(ref, () => editorRef.current); useImperativeHandle<EditorView | null, EditorView | null>(ref, () => editorRef.current);
const handleFocus = useCallback(() => { const handleFocus = useCallback(() => {
if (readOnly) return; if (readOnly) return;
setFocused(true); setFocused(true);
// Select all text on focus
editorRef.current?.dispatch({
selection: { anchor: 0, head: editorRef.current.state.doc.length },
});
onFocus?.(); onFocus?.();
}, [onFocus, readOnly]); }, [onFocus, readOnly]);
const handleBlur = useCallback(() => { const handleBlur = useCallback(() => {
setFocused(false); setFocused(false);
editorRef.current?.dispatch({ selection: { anchor: 0 } }); // Move selection to the end on blur
editorRef.current?.dispatch({
selection: { anchor: editorRef.current.state.doc.length },
});
onBlur?.(); onBlur?.();
}, [onBlur]); }, [onBlur]);
@@ -114,13 +124,14 @@ export const Input = forwardRef<EditorView, InputProps>(function Input(
(value: string) => { (value: string) => {
setCurrentValue(value); setCurrentValue(value);
onChange?.(value); onChange?.(value);
setHasChanged(true);
}, },
[onChange], [onChange, setHasChanged],
); );
const wrapperRef = useRef<HTMLDivElement>(null); const wrapperRef = useRef<HTMLDivElement>(null);
// Submit nearest form on Enter key press // Submit the nearest form on Enter key press
const handleKeyDown = useCallback( const handleKeyDown = useCallback(
(e: KeyboardEvent) => { (e: KeyboardEvent) => {
if (e.key !== 'Enter') return; if (e.key !== 'Enter') return;
@@ -145,7 +156,7 @@ export const Input = forwardRef<EditorView, InputProps>(function Input(
> >
<Label <Label
htmlFor={id.current} htmlFor={id.current}
optional={!required} required={required}
visuallyHidden={hideLabel} visuallyHidden={hideLabel}
className={classNames(labelClassName)} className={classNames(labelClassName)}
> >
@@ -158,8 +169,9 @@ export const Input = forwardRef<EditorView, InputProps>(function Input(
'x-theme-input', 'x-theme-input',
'relative w-full rounded-md text', 'relative w-full rounded-md text',
'border', 'border',
focused ? 'border-border-focus' : 'border-border', focused && !disabled ? 'border-border-focus' : 'border-border',
!isValid && '!border-danger', disabled && 'border-dotted',
!isValid && hasChanged && '!border-danger',
size === 'md' && 'min-h-md', size === 'md' && 'min-h-md',
size === 'sm' && 'min-h-sm', size === 'sm' && 'min-h-sm',
size === 'xs' && 'min-h-xs', size === 'xs' && 'min-h-xs',
@@ -190,7 +202,12 @@ export const Input = forwardRef<EditorView, InputProps>(function Input(
onChange={handleChange} onChange={handleChange}
onPaste={onPaste} onPaste={onPaste}
onPasteOverwrite={onPasteOverwrite} onPasteOverwrite={onPasteOverwrite}
className={classNames(editorClassName, multiLine && 'py-1.5')} disabled={disabled}
className={classNames(
editorClassName,
multiLine && size === 'md' && 'py-1.5',
multiLine && size === 'sm' && 'py-1',
)}
onFocus={handleFocus} onFocus={handleFocus}
onBlur={handleBlur} onBlur={handleBlur}
readOnly={readOnly} readOnly={readOnly}
@@ -201,7 +218,7 @@ export const Input = forwardRef<EditorView, InputProps>(function Input(
<IconButton <IconButton
title={obscured ? `Show ${label}` : `Obscure ${label}`} title={obscured ? `Show ${label}` : `Obscure ${label}`}
size="xs" size="xs"
className="mr-0.5 group/obscure !h-auto my-0.5" className={classNames("mr-0.5 group/obscure !h-auto my-0.5", disabled && 'opacity-disabled')}
iconClassName="text-text-subtle group-hover/obscure:text" iconClassName="text-text-subtle group-hover/obscure:text"
iconSize="sm" iconSize="sm"
icon={obscured ? 'eye' : 'eye_closed'} icon={obscured ? 'eye' : 'eye_closed'}
+10 -8
View File
@@ -4,30 +4,32 @@ import type { HTMLAttributes } from 'react';
export function Label({ export function Label({
htmlFor, htmlFor,
className, className,
optional,
children, children,
visuallyHidden, visuallyHidden,
otherTags = [], tags = [],
required,
...props ...props
}: HTMLAttributes<HTMLLabelElement> & { }: HTMLAttributes<HTMLLabelElement> & {
htmlFor: string; htmlFor: string;
optional?: boolean; required?: boolean;
otherTags?: string[]; tags?: string[];
visuallyHidden?: boolean; visuallyHidden?: boolean;
}) { }) {
const tags = optional ? ['optional', ...otherTags] : otherTags;
return ( return (
<label <label
htmlFor={htmlFor}
className={classNames( className={classNames(
className, className,
visuallyHidden && 'sr-only', visuallyHidden && 'sr-only',
'flex-shrink-0', 'flex-shrink-0 text-sm',
'text-text-subtle whitespace-nowrap flex items-center gap-1', 'text-text-subtle whitespace-nowrap flex items-center gap-1',
)} )}
htmlFor={htmlFor}
{...props} {...props}
> >
{children} <span>
{children}
{required === true && <span className="text-text-subtlest">*</span>}
</span>
{tags.map((tag, i) => ( {tags.map((tag, i) => (
<span key={i} className="text-xs text-text-subtlest"> <span key={i} className="text-xs text-text-subtlest">
({tag}) ({tag})
+2 -5
View File
@@ -168,7 +168,8 @@ export const PairEditor = forwardRef<PairEditorRef, PairEditorProps>(function Pa
); );
const handleChange = useCallback( const handleChange = useCallback(
(pair: PairWithId) => setPairsAndSave((pairs) => pairs.map((p) => (pair.id !== p.id ? p : pair))), (pair: PairWithId) =>
setPairsAndSave((pairs) => pairs.map((p) => (pair.id !== p.id ? p : pair))),
[setPairsAndSave], [setPairsAndSave],
); );
@@ -344,7 +345,6 @@ function PairEditorRow({
const deleteItems = useMemo( const deleteItems = useMemo(
(): DropdownItem[] => [ (): DropdownItem[] => [
{ {
key: 'delete',
label: 'Delete', label: 'Delete',
onSelect: handleDelete, onSelect: handleDelete,
color: 'danger', color: 'danger',
@@ -570,7 +570,6 @@ function FileActionsDropdown({
const extraItems = useMemo<DropdownItem[]>( const extraItems = useMemo<DropdownItem[]>(
() => [ () => [
{ {
key: 'mime',
label: 'Set Content-Type', label: 'Set Content-Type',
leftSlot: <Icon icon="pencil" />, leftSlot: <Icon icon="pencil" />,
hidden: !pair.isFile, hidden: !pair.isFile,
@@ -589,7 +588,6 @@ function FileActionsDropdown({
}, },
}, },
{ {
key: 'clear-file',
label: 'Unset File', label: 'Unset File',
leftSlot: <Icon icon="x" />, leftSlot: <Icon icon="x" />,
hidden: pair.isFile, hidden: pair.isFile,
@@ -598,7 +596,6 @@ function FileActionsDropdown({
}, },
}, },
{ {
key: 'delete',
label: 'Delete', label: 'Delete',
onSelect: onDelete, onSelect: onDelete,
variant: 'danger', variant: 'danger',
+14 -19
View File
@@ -1,6 +1,6 @@
import classNames from 'classnames'; import classNames from 'classnames';
import type { HTMLAttributes, FocusEvent } from 'react'; import type { FocusEvent, HTMLAttributes } from 'react';
import { useCallback, useMemo, useRef, useState } from 'react'; import { useCallback, useRef, useState } from 'react';
import { useStateWithDeps } from '../../hooks/useStateWithDeps'; import { useStateWithDeps } from '../../hooks/useStateWithDeps';
import { IconButton } from './IconButton'; import { IconButton } from './IconButton';
import type { InputProps } from './Input'; import type { InputProps } from './Input';
@@ -41,8 +41,8 @@ export function PlainInput({
onFocusRaw, onFocusRaw,
}: PlainInputProps) { }: PlainInputProps) {
const [obscured, setObscured] = useStateWithDeps(type === 'password', [type]); const [obscured, setObscured] = useStateWithDeps(type === 'password', [type]);
const [currentValue, setCurrentValue] = useState(defaultValue ?? '');
const [focused, setFocused] = useState(false); const [focused, setFocused] = useState(false);
const [hasChanged, setHasChanged] = useState<boolean>(false);
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
const textareaRef = useRef<HTMLTextAreaElement>(null); const textareaRef = useRef<HTMLTextAreaElement>(null);
@@ -71,19 +71,19 @@ export function PlainInput({
'px-2 text-xs font-mono cursor-text', 'px-2 text-xs font-mono cursor-text',
); );
const isValid = useMemo(() => {
if (required && !validateRequire(currentValue)) return false;
if (typeof validate === 'boolean') return validate;
if (typeof validate === 'function' && !validate(currentValue)) return false;
return true;
}, [required, currentValue, validate]);
const handleChange = useCallback( const handleChange = useCallback(
(value: string) => { (value: string) => {
setCurrentValue(value);
onChange?.(value); onChange?.(value);
setHasChanged(true);
const isValid = (value: string) => {
if (required && !validateRequire(value)) return false;
if (typeof validate === 'boolean') return validate;
if (typeof validate === 'function' && !validate(value)) return false;
return true;
};
inputRef.current?.setCustomValidity(isValid(value) ? '' : 'Invalid value');
}, },
[onChange], [onChange, required, validate],
); );
const wrapperRef = useRef<HTMLDivElement>(null); const wrapperRef = useRef<HTMLDivElement>(null);
@@ -98,12 +98,7 @@ export function PlainInput({
labelPosition === 'top' && 'flex-row gap-0.5', labelPosition === 'top' && 'flex-row gap-0.5',
)} )}
> >
<Label <Label htmlFor={id} className={labelClassName} visuallyHidden={hideLabel} required={required}>
htmlFor={id}
className={labelClassName}
visuallyHidden={hideLabel}
optional={!required}
>
{label} {label}
</Label> </Label>
<HStack <HStack
@@ -114,7 +109,7 @@ export function PlainInput({
'relative w-full rounded-md text', 'relative w-full rounded-md text',
'border', 'border',
focused ? 'border-border-focus' : 'border-border-subtle', focused ? 'border-border-focus' : 'border-border-subtle',
!isValid && '!border-danger', hasChanged && 'has-[:invalid]:border-danger', // For built-in HTML validation
size === 'md' && 'min-h-md', size === 'md' && 'min-h-md',
size === 'sm' && 'min-h-sm', size === 'sm' && 'min-h-sm',
size === 'xs' && 'min-h-xs', size === 'xs' && 'min-h-xs',
+2 -3
View File
@@ -2,7 +2,7 @@ import type { PromptTextRequest } from '@yaakapp-internal/plugins';
import type { FormEvent, ReactNode } from 'react'; import type { FormEvent, ReactNode } from 'react';
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import { Button } from './Button'; import { Button } from './Button';
import { Input } from './Input'; import { PlainInput } from './PlainInput';
import { HStack } from './Stacks'; import { HStack } from './Stacks';
export type PromptProps = Omit<PromptTextRequest, 'id' | 'title' | 'description'> & { export type PromptProps = Omit<PromptTextRequest, 'id' | 'title' | 'description'> & {
@@ -35,7 +35,7 @@ export function Prompt({
className="grid grid-rows-[auto_auto] grid-cols-[minmax(0,1fr)] gap-4 mb-4" className="grid grid-rows-[auto_auto] grid-cols-[minmax(0,1fr)] gap-4 mb-4"
onSubmit={handleSubmit} onSubmit={handleSubmit}
> >
<Input <PlainInput
hideLabel hideLabel
autoSelect autoSelect
required={required} required={required}
@@ -43,7 +43,6 @@ export function Prompt({
label={label} label={label}
defaultValue={defaultValue} defaultValue={defaultValue}
onChange={setValue} onChange={setValue}
stateKey={null}
/> />
<HStack space={2} justifyContent="end"> <HStack space={2} justifyContent="end">
<Button onClick={onCancel} variant="border" color="secondary"> <Button onClick={onCancel} variant="border" color="secondary">
+7 -2
View File
@@ -23,12 +23,14 @@ export interface SelectProps<T extends string> {
size?: ButtonProps['size']; size?: ButtonProps['size'];
className?: string; className?: string;
event?: string; event?: string;
disabled?: boolean;
} }
export function Select<T extends string>({ export function Select<T extends string>({
labelPosition = 'top', labelPosition = 'top',
name, name,
labelClassName, labelClassName,
disabled,
hideLabel, hideLabel,
label, label,
value, value,
@@ -72,7 +74,8 @@ export function Select<T extends string>({
'w-full rounded-md text text-sm font-mono', 'w-full rounded-md text text-sm font-mono',
'pl-2', 'pl-2',
'border', 'border',
focused ? 'border-border-focus' : 'border-border', focused && !disabled ? 'border-border-focus' : 'border-border',
disabled && 'border-dotted',
isInvalidSelection && 'border-danger', isInvalidSelection && 'border-danger',
size === 'xs' && 'h-xs', size === 'xs' && 'h-xs',
size === 'sm' && 'h-sm', size === 'sm' && 'h-sm',
@@ -86,7 +89,8 @@ export function Select<T extends string>({
onChange={(e) => handleChange(e.target.value as T)} onChange={(e) => handleChange(e.target.value as T)}
onFocus={() => setFocused(true)} onFocus={() => setFocused(true)}
onBlur={() => setFocused(false)} onBlur={() => setFocused(false)}
className={classNames('pr-7 w-full outline-none bg-transparent')} className={classNames('pr-7 w-full outline-none bg-transparent disabled:opacity-disabled')}
disabled={disabled}
> >
{isInvalidSelection && <option value={'__NONE__'}>-- Select an Option --</option>} {isInvalidSelection && <option value={'__NONE__'}>-- Select an Option --</option>}
{options.map((o) => { {options.map((o) => {
@@ -109,6 +113,7 @@ export function Select<T extends string>({
variant="border" variant="border"
size={size} size={size}
leftSlot={leftSlot} leftSlot={leftSlot}
disabled={disabled}
forDropdown forDropdown
> >
{options.find((o) => o.type !== 'separator' && o.value === value)?.label ?? '--'} {options.find((o) => o.type !== 'separator' && o.value === value)?.label ?? '--'}
+2 -4
View File
@@ -20,9 +20,8 @@ export interface ToastProps {
color?: ShowToastRequest['color']; color?: ShowToastRequest['color'];
} }
const ICONS: Record<NonNullable<ToastProps['color']>, IconProps['icon'] | null> = { const ICONS: Record<NonNullable<ToastProps['color'] | 'custom'>, IconProps['icon'] | null> = {
custom: null, custom: null,
default: 'info',
danger: 'alert_triangle', danger: 'alert_triangle',
info: 'info', info: 'info',
notice: 'alert_triangle', notice: 'alert_triangle',
@@ -42,9 +41,8 @@ export function Toast({ children, open, onClose, timeout, action, icon, color }:
{}, {},
[open], [open],
); );
color = color ?? 'default';
const toastIcon = icon ?? (color in ICONS && ICONS[color]); const toastIcon = icon ?? (color && color in ICONS && ICONS[color]);
return ( return (
<motion.div <motion.div
+1 -7
View File
@@ -26,7 +26,6 @@ export function useCreateDropdownItems({
return [ return [
{ {
key: 'create-http-request',
label: 'HTTP Request', label: 'HTTP Request',
leftSlot: hideIcons ? undefined : <Icon icon="plus" />, leftSlot: hideIcons ? undefined : <Icon icon="plus" />,
onSelect: () => { onSelect: () => {
@@ -34,7 +33,6 @@ export function useCreateDropdownItems({
}, },
}, },
{ {
key: 'create-graphql-request',
label: 'GraphQL Query', label: 'GraphQL Query',
leftSlot: hideIcons ? undefined : <Icon icon="plus" />, leftSlot: hideIcons ? undefined : <Icon icon="plus" />,
onSelect: () => onSelect: () =>
@@ -46,7 +44,6 @@ export function useCreateDropdownItems({
}), }),
}, },
{ {
key: 'create-grpc-request',
label: 'gRPC Call', label: 'gRPC Call',
leftSlot: hideIcons ? undefined : <Icon icon="plus" />, leftSlot: hideIcons ? undefined : <Icon icon="plus" />,
onSelect: () => createGrpcRequest({ folderId }), onSelect: () => createGrpcRequest({ folderId }),
@@ -54,11 +51,8 @@ export function useCreateDropdownItems({
...((hideFolder ...((hideFolder
? [] ? []
: [ : [
{ type: 'separator' },
{ {
type: 'separator',
},
{
key: 'create-folder',
label: 'Folder', label: 'Folder',
leftSlot: hideIcons ? undefined : <Icon icon="plus" />, leftSlot: hideIcons ? undefined : <Icon icon="plus" />,
onSelect: () => createFolder.mutate({ folderId }), onSelect: () => createFolder.mutate({ folderId }),
+13 -10
View File
@@ -1,39 +1,42 @@
import { useQuery } from '@tanstack/react-query'; import { useQuery } from '@tanstack/react-query';
import type { GetHttpAuthenticationResponse } from '@yaakapp-internal/plugins'; import type { GetHttpAuthenticationSummaryResponse } from '@yaakapp-internal/plugins';
import { useAtomValue } from 'jotai'; import { useAtomValue } from 'jotai';
import { atom, useSetAtom } from 'jotai/index'; import { atom } from 'jotai/index';
import { useState } from 'react'; import { useState } from 'react';
import { jotaiStore } from '../lib/jotai';
import { invokeCmd } from '../lib/tauri'; import { invokeCmd } from '../lib/tauri';
import { showErrorToast } from '../lib/toast'; import { showErrorToast } from '../lib/toast';
import { usePluginsKey } from './usePlugins';
const httpAuthenticationAtom = atom<GetHttpAuthenticationResponse[]>([]); const httpAuthenticationSummariesAtom = atom<GetHttpAuthenticationSummaryResponse[]>([]);
const orderedHttpAuthenticationAtom = atom((get) => const orderedHttpAuthenticationAtom = atom((get) =>
get(httpAuthenticationAtom).sort((a, b) => a.name.localeCompare(b.name)), get(httpAuthenticationSummariesAtom)?.sort((a, b) => a.name.localeCompare(b.name)),
); );
export function useHttpAuthentication() { export function useHttpAuthenticationSummaries() {
return useAtomValue(orderedHttpAuthenticationAtom); return useAtomValue(orderedHttpAuthenticationAtom);
} }
export function useSubscribeHttpAuthentication() { export function useSubscribeHttpAuthentication() {
const [numResults, setNumResults] = useState<number>(0); const [numResults, setNumResults] = useState<number>(0);
const setAtom = useSetAtom(httpAuthenticationAtom); const pluginsKey = usePluginsKey();
useQuery({ useQuery({
queryKey: ['http_authentication'], queryKey: ['http_authentication_summaries', pluginsKey],
// Fetch periodically until functions are returned // Fetch periodically until functions are returned
// NOTE: visibilitychange (refetchOnWindowFocus) does not work on Windows, so we'll rely on this logic // NOTE: visibilitychange (refetchOnWindowFocus) does not work on Windows, so we'll rely on this logic
// to refetch things until that's working again // to refetch things until that's working again
// TODO: Update plugin system to wait for plugins to initialize before sending the first event to them // TODO: Update plugin system to wait for plugins to initialize before sending the first event to them
refetchInterval: numResults > 0 ? Infinity : 1000, refetchInterval: numResults > 0 ? Infinity : 1000,
refetchOnMount: true, refetchOnMount: true,
placeholderData: (prev) => prev, // Keep previous data on refetch
queryFn: async () => { queryFn: async () => {
try { try {
const result = await invokeCmd<GetHttpAuthenticationResponse[]>( const result = await invokeCmd<GetHttpAuthenticationSummaryResponse[]>(
'cmd_get_http_authentication', 'cmd_get_http_authentication_summaries',
); );
setNumResults(result.length); setNumResults(result.length);
setAtom(result); jotaiStore.set(httpAuthenticationSummariesAtom, result);
return result; return result;
} catch (err) { } catch (err) {
showErrorToast('http-authentication-error', err); showErrorToast('http-authentication-error', err);
@@ -0,0 +1,59 @@
import { useQuery } from '@tanstack/react-query';
import type { GrpcRequest, HttpRequest } from '@yaakapp-internal/models';
import type { GetHttpAuthenticationConfigResponse, JsonPrimitive } from '@yaakapp-internal/plugins';
import { md5 } from 'js-md5';
import { useState } from 'react';
import { invokeCmd } from '../lib/tauri';
import { useHttpResponses } from './useHttpResponses';
export function useHttpAuthenticationConfig(
authName: string | null,
values: Record<string, JsonPrimitive>,
requestId: string,
) {
const responses = useHttpResponses();
const [forceRefreshCounter, setForceRefreshCounter] = useState<number>(0);
// Some auth handlers like OAuth 2.0 show the current token after a successful request. To
// handle that, we'll force the auth to re-fetch after each new response closes
const responseKey = md5(
responses
.filter((r) => r.state === 'closed')
.map((r) => r.id)
.join(':'),
);
return useQuery({
queryKey: ['http_authentication_config', requestId, authName, values, responseKey, forceRefreshCounter],
placeholderData: (prev) => prev, // Keep previous data on refetch
queryFn: async () => {
const config = await invokeCmd<GetHttpAuthenticationConfigResponse>(
'cmd_get_http_authentication_config',
{
authName,
values,
requestId,
},
);
return {
...config,
actions: config.actions?.map((a, i) => ({
...a,
call: async ({ id: requestId }: HttpRequest | GrpcRequest) => {
await invokeCmd('cmd_call_http_authentication_action', {
pluginRefId: config.pluginRefId,
actionIndex: i,
authName,
values,
requestId,
});
// Ensure the config is refreshed after the action is done
setForceRefreshCounter((c) => c + 1);
},
})),
};
},
});
}
+4 -5
View File
@@ -5,11 +5,11 @@ import type {
GetHttpRequestActionsResponse, GetHttpRequestActionsResponse,
HttpRequestAction, HttpRequestAction,
} from '@yaakapp-internal/plugins'; } from '@yaakapp-internal/plugins';
import { useMemo } from 'react';
import { invokeCmd } from '../lib/tauri'; import { invokeCmd } from '../lib/tauri';
import { usePluginsKey } from './usePlugins'; import { usePluginsKey } from './usePlugins';
import { useMemo } from 'react';
export type CallableHttpRequestAction = Pick<HttpRequestAction, 'key' | 'label' | 'icon'> & { export type CallableHttpRequestAction = Pick<HttpRequestAction, 'label' | 'icon'> & {
call: (httpRequest: HttpRequest) => Promise<void>; call: (httpRequest: HttpRequest) => Promise<void>;
}; };
@@ -24,13 +24,12 @@ export function useHttpRequestActions() {
); );
return responses.flatMap((r) => return responses.flatMap((r) =>
r.actions.map((a) => ({ r.actions.map((a, i) => ({
key: a.key,
label: a.label, label: a.label,
icon: a.icon, icon: a.icon,
call: async (httpRequest: HttpRequest) => { call: async (httpRequest: HttpRequest) => {
const payload: CallHttpRequestActionRequest = { const payload: CallHttpRequestActionRequest = {
key: a.key, index: i,
pluginRefId: r.pluginRefId, pluginRefId: r.pluginRefId,
args: { httpRequest }, args: { httpRequest },
}; };
-1
View File
@@ -25,7 +25,6 @@ export function useNotificationToast() {
id: payload.id, id: payload.id,
timeout: null, timeout: null,
message: payload.message, message: payload.message,
color: 'custom',
onClose: () => markRead(payload.id), onClose: () => markRead(payload.id),
action: ({ hide }) => action: ({ hide }) =>
actionLabel && actionUrl ? ( actionLabel && actionUrl ? (
+4 -3
View File
@@ -3,7 +3,7 @@ import type { GetTemplateFunctionsResponse, TemplateFunction } from '@yaakapp-in
import { atom, useAtomValue } from 'jotai'; import { atom, useAtomValue } from 'jotai';
import { useSetAtom } from 'jotai/index'; import { useSetAtom } from 'jotai/index';
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import type {TwigCompletionOption} from "../components/core/Editor/twig/completion"; import type { TwigCompletionOption } from '../components/core/Editor/twig/completion';
import { invokeCmd } from '../lib/tauri'; import { invokeCmd } from '../lib/tauri';
import { usePluginsKey } from './usePlugins'; import { usePluginsKey } from './usePlugins';
@@ -17,8 +17,9 @@ export function useTemplateFunctionCompletionOptions(
return ( return (
templateFunctions.map((fn) => { templateFunctions.map((fn) => {
const NUM_ARGS = 2; const NUM_ARGS = 2;
const argsWithName = fn.args.filter((a) => 'name' in a);
const shortArgs = const shortArgs =
fn.args argsWithName
.slice(0, NUM_ARGS) .slice(0, NUM_ARGS)
.map((a) => a.name) .map((a) => a.name)
.join(', ') + (fn.args.length > NUM_ARGS ? ', …' : ''); .join(', ') + (fn.args.length > NUM_ARGS ? ', …' : '');
@@ -27,7 +28,7 @@ export function useTemplateFunctionCompletionOptions(
aliases: fn.aliases, aliases: fn.aliases,
type: 'function', type: 'function',
description: fn.description, description: fn.description,
args: fn.args.map((a) => ({ name: a.name })), args: argsWithName.map((a) => ({ name: a.name })),
value: null, value: null,
label: `${fn.name}(${shortArgs})`, label: `${fn.name}(${shortArgs})`,
onClick: (rawTag: string, startPos: number) => onClick(fn, rawTag, startPos), onClick: (rawTag: string, startPos: number) => onClick(fn, rawTag, startPos),
+1 -1
View File
@@ -1,4 +1,4 @@
import type { GenericCompletionOption } from '../../components/core/Editor/genericCompletion'; import type { GenericCompletionOption } from '@yaakapp-internal/plugins';
export const headerNames: (GenericCompletionOption | string)[] = [ export const headerNames: (GenericCompletionOption | string)[] = [
{ {
+9 -7
View File
@@ -2,17 +2,16 @@ import type { InvokeArgs } from '@tauri-apps/api/core';
import { invoke } from '@tauri-apps/api/core'; import { invoke } from '@tauri-apps/api/core';
type TauriCmd = type TauriCmd =
| 'cmd_call_http_authentication_action'
| 'cmd_call_http_request_action' | 'cmd_call_http_request_action'
| 'cmd_check_for_updates' | 'cmd_check_for_updates'
| 'cmd_create_cookie_jar' | 'cmd_create_cookie_jar'
| 'cmd_create_environment' | 'cmd_create_environment'
| 'cmd_template_tokens_to_string'
| 'cmd_create_grpc_request' | 'cmd_create_grpc_request'
| 'cmd_create_http_request' | 'cmd_create_http_request'
| 'cmd_curl_to_request' | 'cmd_curl_to_request'
| 'cmd_delete_all_grpc_connections' | 'cmd_delete_all_grpc_connections'
| 'cmd_delete_all_http_responses' | 'cmd_delete_all_http_responses'
| 'cmd_delete_send_history'
| 'cmd_delete_cookie_jar' | 'cmd_delete_cookie_jar'
| 'cmd_delete_environment' | 'cmd_delete_environment'
| 'cmd_delete_folder' | 'cmd_delete_folder'
@@ -20,6 +19,7 @@ type TauriCmd =
| 'cmd_delete_grpc_request' | 'cmd_delete_grpc_request'
| 'cmd_delete_http_request' | 'cmd_delete_http_request'
| 'cmd_delete_http_response' | 'cmd_delete_http_response'
| 'cmd_delete_send_history'
| 'cmd_delete_workspace' | 'cmd_delete_workspace'
| 'cmd_dismiss_notification' | 'cmd_dismiss_notification'
| 'cmd_duplicate_folder' | 'cmd_duplicate_folder'
@@ -32,11 +32,12 @@ type TauriCmd =
| 'cmd_get_environment' | 'cmd_get_environment'
| 'cmd_get_folder' | 'cmd_get_folder'
| 'cmd_get_grpc_request' | 'cmd_get_grpc_request'
| 'cmd_get_http_authentication' | 'cmd_get_http_authentication_config'
| 'cmd_get_http_authentication_summaries'
| 'cmd_get_http_request' | 'cmd_get_http_request'
| 'cmd_get_sse_events'
| 'cmd_get_key_value' | 'cmd_get_key_value'
| 'cmd_get_settings' | 'cmd_get_settings'
| 'cmd_get_sse_events'
| 'cmd_get_workspace' | 'cmd_get_workspace'
| 'cmd_get_workspace_meta' | 'cmd_get_workspace_meta'
| 'cmd_grpc_go' | 'cmd_grpc_go'
@@ -45,7 +46,6 @@ type TauriCmd =
| 'cmd_import_data' | 'cmd_import_data'
| 'cmd_install_plugin' | 'cmd_install_plugin'
| 'cmd_list_cookie_jars' | 'cmd_list_cookie_jars'
| 'cmd_list_key_values'
| 'cmd_list_environments' | 'cmd_list_environments'
| 'cmd_list_folders' | 'cmd_list_folders'
| 'cmd_list_grpc_connections' | 'cmd_list_grpc_connections'
@@ -53,21 +53,23 @@ type TauriCmd =
| 'cmd_list_grpc_requests' | 'cmd_list_grpc_requests'
| 'cmd_list_http_requests' | 'cmd_list_http_requests'
| 'cmd_list_http_responses' | 'cmd_list_http_responses'
| 'cmd_list_key_values'
| 'cmd_list_plugins' | 'cmd_list_plugins'
| 'cmd_list_workspaces' | 'cmd_list_workspaces'
| 'cmd_metadata' | 'cmd_metadata'
| 'cmd_new_main_window'
| 'cmd_new_child_window' | 'cmd_new_child_window'
| 'cmd_new_main_window'
| 'cmd_parse_template' | 'cmd_parse_template'
| 'cmd_plugin_info' | 'cmd_plugin_info'
| 'cmd_render_template'
| 'cmd_reload_plugins' | 'cmd_reload_plugins'
| 'cmd_render_template'
| 'cmd_save_response' | 'cmd_save_response'
| 'cmd_send_ephemeral_request' | 'cmd_send_ephemeral_request'
| 'cmd_send_http_request' | 'cmd_send_http_request'
| 'cmd_set_key_value' | 'cmd_set_key_value'
| 'cmd_set_update_mode' | 'cmd_set_update_mode'
| 'cmd_template_functions' | 'cmd_template_functions'
| 'cmd_template_tokens_to_string'
| 'cmd_track_event' | 'cmd_track_event'
| 'cmd_uninstall_plugin' | 'cmd_uninstall_plugin'
| 'cmd_update_cookie_jar' | 'cmd_update_cookie_jar'