From 92a8da03af767f85d7da7ca5fb3b9c6b1bd2ca78 Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Thu, 1 Jan 2026 09:32:48 -0800 Subject: [PATCH] (feat) Add ability to disable plugins and show bundled plugins (#337) --- .gitignore | 1 + biome.json | 8 +- package-lock.json | 84 +- package.json | 1 + .../src/bindings/gen_events.ts | 2 +- .../src/plugins/AuthenticationPlugin.ts | 7 +- .../src/plugins/FilterPlugin.ts | 2 +- .../src/plugins/GrpcRequestActionPlugin.ts | 2 +- .../src/plugins/ImporterPlugin.ts | 4 +- .../src/plugins/TemplateFunctionPlugin.ts | 7 +- .../src/plugins/ThemePlugin.ts | 2 +- .../plugin-runtime-types/src/plugins/index.ts | 4 +- packages/plugin-runtime/src/PluginHandle.ts | 4 +- packages/plugin-runtime/src/PluginInstance.ts | 64 +- packages/plugin-runtime/src/common.ts | 30 +- packages/plugin-runtime/src/index.ts | 13 +- .../plugin-runtime/src/interceptStdout.ts | 31 +- packages/plugin-runtime/src/migrations.ts | 13 +- packages/plugin-runtime/tests/common.test.ts | 4 +- plugins/action-send-folder/package.json | 16 + plugins/action-send-folder/src/index.ts | 74 ++ plugins/action-send-folder/tsconfig.json | 3 + src-tauri/src/lib.rs | 2 +- src-tauri/src/plugin_events.rs | 28 +- src-tauri/yaak-mac-window/Cargo.toml | 3 + src-tauri/yaak-models/src/queries/plugins.rs | 4 + src-tauri/yaak-plugins/bindings/gen_events.ts | 2 +- src-tauri/yaak-plugins/src/events.rs | 4 +- src-tauri/yaak-plugins/src/install.rs | 2 +- src-tauri/yaak-plugins/src/manager.rs | 192 ++-- src-tauri/yaak-plugins/src/plugin_handle.rs | 4 +- src-tauri/yaak-plugins/src/server_ws.rs | 24 +- .../yaak-templates/pkg/yaak_templates.d.ts | 4 +- .../yaak-templates/pkg/yaak_templates_bg.js | 8 +- .../yaak-templates/pkg/yaak_templates_bg.wasm | Bin 68401 -> 66268 bytes src-web/components/FolderLayout.tsx | 13 + .../components/Settings/SettingsPlugins.tsx | 95 +- src-web/components/Sidebar.tsx | 17 - src-web/hooks/usePlugins.ts | 6 +- target/rust-analyzer/flycheck0/stderr | 1 - target/rust-analyzer/flycheck0/stdout | 913 ------------------ 41 files changed, 515 insertions(+), 1183 deletions(-) create mode 100644 plugins/action-send-folder/package.json create mode 100644 plugins/action-send-folder/src/index.ts create mode 100644 plugins/action-send-folder/tsconfig.json delete mode 100644 target/rust-analyzer/flycheck0/stderr delete mode 100644 target/rust-analyzer/flycheck0/stdout diff --git a/.gitignore b/.gitignore index e4995499..d666f106 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,4 @@ out tmp .zed codebook.toml +target diff --git a/biome.json b/biome.json index 05318042..589f90ff 100644 --- a/biome.json +++ b/biome.json @@ -1,5 +1,5 @@ { - "$schema": "https://biomejs.dev/schemas/2.3.7/schema.json", + "$schema": "https://biomejs.dev/schemas/2.3.10/schema.json", "linter": { "enabled": true, "rules": { @@ -39,13 +39,13 @@ "!**/dist", "!**/build", "!scripts", - "!packages/plugin-runtime", - "!packages/plugin-runtime-types", "!src-tauri", "!src-web/tailwind.config.cjs", "!src-web/postcss.config.cjs", "!src-web/vite.config.ts", - "!src-web/routeTree.gen.ts" + "!src-web/routeTree.gen.ts", + "!packages/plugin-runtime-types/lib", + "!**/bindings" ] } } diff --git a/package-lock.json b/package-lock.json index 589518c2..1c665683 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "plugins-external/template-function-faker", "plugins/action-copy-curl", "plugins/action-copy-grpcurl", + "plugins/action-send-folder", "plugins/auth-apikey", "plugins/auth-aws", "plugins/auth-basic", @@ -4047,6 +4048,10 @@ "resolved": "plugins/action-copy-grpcurl", "link": true }, + "node_modules/@yaak/action-send-folder": { + "resolved": "plugins/action-send-folder", + "link": true + }, "node_modules/@yaak/auth-apikey": { "resolved": "plugins/auth-apikey", "link": true @@ -4690,9 +4695,9 @@ } }, "node_modules/async": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", - "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==", + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", "license": "MIT" }, "node_modules/async-each": { @@ -8719,9 +8724,9 @@ } }, "node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", "dev": true, "license": "ISC", "dependencies": { @@ -10773,12 +10778,12 @@ } }, "node_modules/jws": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", - "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.3.tgz", + "integrity": "sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g==", "license": "MIT", "dependencies": { - "jwa": "^1.4.1", + "jwa": "^1.4.2", "safe-buffer": "^5.0.1" } }, @@ -11591,9 +11596,9 @@ } }, "node_modules/mdast-util-to-hast": { - "version": "13.2.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz", - "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==", + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.0", @@ -13559,15 +13564,15 @@ } }, "node_modules/openapi-to-postmanv2": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/openapi-to-postmanv2/-/openapi-to-postmanv2-5.0.0.tgz", - "integrity": "sha512-ousMf9rXKen9tscJQ0H8BE+hfgOvFRb2SspYwGnQTmnjzkPNejHUQpNmRUIK/gZ6ZwiNWAnQCKWNogaZXUlFew==", + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/openapi-to-postmanv2/-/openapi-to-postmanv2-5.7.0.tgz", + "integrity": "sha512-fhwjpL+CF1kOKX5G1m5E/tnZWc9KuSXZA4oOGZv9c4NzKtC3ecUHxtp4KTvwzFh8/b+ssSZo+blC/3OK4/9ySA==", "license": "Apache-2.0", "dependencies": { - "ajv": "8.11.0", + "ajv": "^8.11.0", "ajv-draft-04": "1.0.0", "ajv-formats": "2.1.1", - "async": "3.2.4", + "async": "3.2.6", "commander": "2.20.3", "graphlib": "2.1.8", "js-yaml": "4.1.0", @@ -13589,22 +13594,6 @@ "node": ">=18" } }, - "node_modules/openapi-to-postmanv2/node_modules/ajv": { - "version": "8.11.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", - "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, "node_modules/openapi-to-postmanv2/node_modules/ajv-draft-04": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/ajv-draft-04/-/ajv-draft-04-1.0.0.tgz", @@ -14554,9 +14543,9 @@ } }, "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "version": "6.14.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", + "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -14786,16 +14775,16 @@ } }, "node_modules/react-syntax-highlighter": { - "version": "15.6.1", - "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.6.1.tgz", - "integrity": "sha512-OqJ2/vL7lEeV5zTJyG7kmARppUjiB9h9udl4qHQjjgEos66z00Ia0OckwYfRxCSFrW8RJIBnsBwQsHZbVPspqg==", + "version": "15.6.6", + "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.6.6.tgz", + "integrity": "sha512-DgXrc+AZF47+HvAPEmn7Ua/1p10jNoVZVI/LoPiYdtY+OM+/nG5yefLHKJwdKqY1adMuHFbeyBaG9j64ML7vTw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.3.1", "highlight.js": "^10.4.1", "highlightjs-vue": "^1.0.0", "lowlight": "^1.17.0", - "prismjs": "^1.27.0", + "prismjs": "^1.30.0", "refractor": "^3.6.0" }, "peerDependencies": { @@ -18029,15 +18018,6 @@ "browserslist": ">= 4.21.0" } }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, "node_modules/urix": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", @@ -19110,6 +19090,10 @@ "name": "@yaak/action-copy-grpcurl", "version": "0.1.0" }, + "plugins/action-send-folder": { + "name": "@yaak/action-send-folder", + "version": "0.1.0" + }, "plugins/auth-apikey": { "name": "@yaak/auth-apikey", "version": "0.1.0" diff --git a/package.json b/package.json index 360598e8..dfb8b480 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "plugins-external/template-function-faker", "plugins/action-copy-curl", "plugins/action-copy-grpcurl", + "plugins/action-send-folder", "plugins/auth-apikey", "plugins/auth-aws", "plugins/auth-basic", diff --git a/packages/plugin-runtime-types/src/bindings/gen_events.ts b/packages/plugin-runtime-types/src/bindings/gen_events.ts index ee150d4d..ecbd3c46 100644 --- a/packages/plugin-runtime-types/src/bindings/gen_events.ts +++ b/packages/plugin-runtime-types/src/bindings/gen_events.ts @@ -423,7 +423,7 @@ export type ListCookieNamesRequest = {}; export type ListCookieNamesResponse = { names: Array, }; -export type ListFoldersRequest = Record; +export type ListFoldersRequest = {}; export type ListFoldersResponse = { folders: Array, }; diff --git a/packages/plugin-runtime-types/src/plugins/AuthenticationPlugin.ts b/packages/plugin-runtime-types/src/plugins/AuthenticationPlugin.ts index 8e420b93..5da5f0b9 100644 --- a/packages/plugin-runtime-types/src/plugins/AuthenticationPlugin.ts +++ b/packages/plugin-runtime-types/src/plugins/AuthenticationPlugin.ts @@ -1,4 +1,4 @@ -import { +import type { CallHttpAuthenticationActionArgs, CallHttpAuthenticationRequest, CallHttpAuthenticationResponse, @@ -6,8 +6,8 @@ import { GetHttpAuthenticationSummaryResponse, HttpAuthenticationAction, } from '../bindings/gen_events'; -import { MaybePromise } from '../helpers'; -import { Context } from './Context'; +import type { MaybePromise } from '../helpers'; +import type { Context } from './Context'; type AddDynamicMethod = { dynamic?: ( @@ -16,6 +16,7 @@ type AddDynamicMethod = { ) => MaybePromise | null | undefined>; }; +// biome-ignore lint/suspicious/noExplicitAny: distributive conditional type pattern type AddDynamic = T extends any ? T extends { inputs?: FormInput[] } ? Omit & { diff --git a/packages/plugin-runtime-types/src/plugins/FilterPlugin.ts b/packages/plugin-runtime-types/src/plugins/FilterPlugin.ts index 0977a28e..d8a18a80 100644 --- a/packages/plugin-runtime-types/src/plugins/FilterPlugin.ts +++ b/packages/plugin-runtime-types/src/plugins/FilterPlugin.ts @@ -1,4 +1,4 @@ -import { FilterResponse } from '../bindings/gen_events'; +import type { FilterResponse } from '../bindings/gen_events'; import type { Context } from './Context'; export type FilterPlugin = { diff --git a/packages/plugin-runtime-types/src/plugins/GrpcRequestActionPlugin.ts b/packages/plugin-runtime-types/src/plugins/GrpcRequestActionPlugin.ts index f2e7ab82..881c907f 100644 --- a/packages/plugin-runtime-types/src/plugins/GrpcRequestActionPlugin.ts +++ b/packages/plugin-runtime-types/src/plugins/GrpcRequestActionPlugin.ts @@ -1,4 +1,4 @@ -import { CallGrpcRequestActionArgs, GrpcRequestAction } from '../bindings/gen_events'; +import type { CallGrpcRequestActionArgs, GrpcRequestAction } from '../bindings/gen_events'; import type { Context } from './Context'; export type GrpcRequestActionPlugin = GrpcRequestAction & { diff --git a/packages/plugin-runtime-types/src/plugins/ImporterPlugin.ts b/packages/plugin-runtime-types/src/plugins/ImporterPlugin.ts index 0bf34a19..52ecca11 100644 --- a/packages/plugin-runtime-types/src/plugins/ImporterPlugin.ts +++ b/packages/plugin-runtime-types/src/plugins/ImporterPlugin.ts @@ -1,5 +1,5 @@ -import { ImportResources } from '../bindings/gen_events'; -import { AtLeast, MaybePromise } from '../helpers'; +import type { ImportResources } from '../bindings/gen_events'; +import type { AtLeast, MaybePromise } from '../helpers'; import type { Context } from './Context'; type RootFields = 'name' | 'id' | 'model'; diff --git a/packages/plugin-runtime-types/src/plugins/TemplateFunctionPlugin.ts b/packages/plugin-runtime-types/src/plugins/TemplateFunctionPlugin.ts index e6482a2d..b589e14e 100644 --- a/packages/plugin-runtime-types/src/plugins/TemplateFunctionPlugin.ts +++ b/packages/plugin-runtime-types/src/plugins/TemplateFunctionPlugin.ts @@ -1,6 +1,6 @@ -import { CallTemplateFunctionArgs, FormInput, TemplateFunction } from '../bindings/gen_events'; -import { MaybePromise } from '../helpers'; -import { Context } from './Context'; +import type { CallTemplateFunctionArgs, FormInput, TemplateFunction } from '../bindings/gen_events'; +import type { MaybePromise } from '../helpers'; +import type { Context } from './Context'; type AddDynamicMethod = { dynamic?: ( @@ -9,6 +9,7 @@ type AddDynamicMethod = { ) => MaybePromise | null | undefined>; }; +// biome-ignore lint/suspicious/noExplicitAny: distributive conditional type pattern type AddDynamic = T extends any ? T extends { inputs?: FormInput[] } ? Omit & { diff --git a/packages/plugin-runtime-types/src/plugins/ThemePlugin.ts b/packages/plugin-runtime-types/src/plugins/ThemePlugin.ts index 23c1b689..3cbc2124 100644 --- a/packages/plugin-runtime-types/src/plugins/ThemePlugin.ts +++ b/packages/plugin-runtime-types/src/plugins/ThemePlugin.ts @@ -1,3 +1,3 @@ -import { Theme } from '../bindings/gen_events'; +import type { Theme } from '../bindings/gen_events'; export type ThemePlugin = Theme; diff --git a/packages/plugin-runtime-types/src/plugins/index.ts b/packages/plugin-runtime-types/src/plugins/index.ts index b17087f6..84d65fb8 100644 --- a/packages/plugin-runtime-types/src/plugins/index.ts +++ b/packages/plugin-runtime-types/src/plugins/index.ts @@ -1,8 +1,8 @@ -import { AuthenticationPlugin } from './AuthenticationPlugin'; +import type { AuthenticationPlugin } from './AuthenticationPlugin'; import type { Context } from './Context'; import type { FilterPlugin } from './FilterPlugin'; -import { GrpcRequestActionPlugin } from './GrpcRequestActionPlugin'; +import type { GrpcRequestActionPlugin } from './GrpcRequestActionPlugin'; import type { HttpRequestActionPlugin } from './HttpRequestActionPlugin'; import type { WebsocketRequestActionPlugin } from './WebsocketRequestActionPlugin'; import type { WorkspaceActionPlugin } from './WorkspaceActionPlugin'; diff --git a/packages/plugin-runtime/src/PluginHandle.ts b/packages/plugin-runtime/src/PluginHandle.ts index 29ef6874..e890430d 100644 --- a/packages/plugin-runtime/src/PluginHandle.ts +++ b/packages/plugin-runtime/src/PluginHandle.ts @@ -1,7 +1,7 @@ -import { PluginContext } from '@yaakapp-internal/plugins'; +import type { PluginContext } from '@yaakapp-internal/plugins'; import type { BootRequest, InternalEvent } from '@yaakapp/api'; import type { EventChannel } from './EventChannel'; -import { PluginInstance, PluginWorkerData } from './PluginInstance'; +import { PluginInstance, type PluginWorkerData } from './PluginInstance'; export class PluginHandle { #instance: PluginInstance; diff --git a/packages/plugin-runtime/src/PluginInstance.ts b/packages/plugin-runtime/src/PluginInstance.ts index 00a6eb77..0fd1059e 100644 --- a/packages/plugin-runtime/src/PluginInstance.ts +++ b/packages/plugin-runtime/src/PluginInstance.ts @@ -1,5 +1,8 @@ -import { applyFormInputDefaults, validateTemplateFunctionArgs } from '@yaakapp-internal/lib/templateFunction'; import { + applyFormInputDefaults, + validateTemplateFunctionArgs, +} from '@yaakapp-internal/lib/templateFunction'; +import type { BootRequest, DeleteKeyValueResponse, DeleteModelResponse, @@ -12,9 +15,13 @@ import { HttpAuthenticationAction, HttpRequest, HttpRequestAction, + ImportResources, InternalEvent, InternalEventPayload, ListCookieNamesResponse, + ListFoldersResponse, + ListHttpRequestsRequest, + ListHttpRequestsResponse, ListWorkspacesResponse, PluginContext, PromptTextResponse, @@ -22,11 +29,12 @@ import { RenderHttpRequestResponse, SendHttpRequestResponse, TemplateFunction, + TemplateRenderRequest, TemplateRenderResponse, UpsertModelResponse, WindowInfoResponse, } from '@yaakapp-internal/plugins'; -import { Context, PluginDefinition } from '@yaakapp/api'; +import type { Context, PluginDefinition } from '@yaakapp/api'; import console from 'node:console'; import { type Stats, statSync, watch } from 'node:fs'; import path from 'node:path'; @@ -56,7 +64,7 @@ export class PluginInstance { await this.#onMessage(event); }); - this.#mod = {} as any; + this.#mod = {}; const fileChangeCallback = async () => { await this.#mod?.dispose?.(); @@ -120,8 +128,7 @@ export class PluginInstance { if (reply != null) { const replyPayload: InternalEventPayload = { type: 'import_response', - // deno-lint-ignore no-explicit-any - resources: reply.resources as any, + resources: reply.resources as ImportResources, }; this.#sendPayload(context, replyPayload, replyId); return; @@ -262,7 +269,7 @@ export class PluginInstance { payload.type === 'get_template_function_config_request' && Array.isArray(this.#mod?.templateFunctions) ) { - let templateFunction = this.#mod.templateFunctions.find((f) => f.name === payload.name); + const templateFunction = this.#mod.templateFunctions.find((f) => f.name === payload.name); if (templateFunction == null) { this.#sendEmpty(context, replyId); return; @@ -381,10 +388,7 @@ export class PluginInstance { } } - if ( - payload.type === 'call_folder_action_request' && - Array.isArray(this.#mod.folderActions) - ) { + if (payload.type === 'call_folder_action_request' && Array.isArray(this.#mod.folderActions)) { const action = this.#mod.folderActions[payload.index]; if (typeof action?.onSelect === 'function') { await action.onSelect(ctx, payload.args); @@ -703,12 +707,15 @@ export class PluginInstance { return httpRequest; }, list: async (args?: { folderId?: string }) => { - const payload = { + const payload: InternalEventPayload = { type: 'list_http_requests_request', folderId: args?.folderId, - } as any; - const { httpRequests } = await this.#sendForReply(context, payload); - return httpRequests as any[]; + } satisfies ListHttpRequestsRequest & { type: 'list_http_requests_request' }; + const { httpRequests } = await this.#sendForReply( + context, + payload, + ); + return httpRequests; }, create: async (args) => { const payload = { @@ -747,11 +754,9 @@ export class PluginInstance { }, folder: { list: async () => { - const payload = { - type: 'list_folders_request', - } as any; - const { folders } = await this.#sendForReply(context, payload); - return folders as any[]; + const payload = { type: 'list_folders_request' } as const; + const { folders } = await this.#sendForReply(context, payload); + return folders; }, }, cookies: { @@ -774,9 +779,10 @@ export class PluginInstance { * Invoke Yaak's template engine to render a value. If the value is a nested type * (eg. object), it will be recursively rendered. */ - render: async (args) => { + render: async (args: TemplateRenderRequest) => { const payload = { type: 'template_render_request', ...args } as const; const result = await this.#sendForReply(context, payload); + // biome-ignore lint/suspicious/noExplicitAny: That's okay return result.data as any; }, }, @@ -809,15 +815,19 @@ export class PluginInstance { workspace: { list: async () => { const payload = { - type: 'list_workspaces_request' + type: 'list_workspaces_request', } as InternalEventPayload; const response = await this.#sendForReply(context, payload); - return response.workspaces.map((w) => ({ - id: w.id, - name: w.name, - // Hide label from plugin authors, but keep it for internal routing - _label: (w as any).label as string, - })); + return response.workspaces.map((w) => { + // Internal workspace info includes label field not in public API + type WorkspaceInfoInternal = typeof w & { label?: string }; + return { + id: w.id, + name: w.name, + // Hide label from plugin authors, but keep it for internal routing + _label: (w as WorkspaceInfoInternal).label as string, + }; + }); }, withContext: (workspaceHandle: { id: string; name: string; _label?: string }) => { // Create a new context with the workspace's window label diff --git a/packages/plugin-runtime/src/common.ts b/packages/plugin-runtime/src/common.ts index cfdcdf0f..d13ad38a 100644 --- a/packages/plugin-runtime/src/common.ts +++ b/packages/plugin-runtime/src/common.ts @@ -1,5 +1,8 @@ -import { CallHttpAuthenticationActionArgs, CallTemplateFunctionArgs } from '@yaakapp-internal/plugins'; -import { Context, DynamicAuthenticationArg, DynamicTemplateFunctionArg } from '@yaakapp/api'; +import type { Context, DynamicAuthenticationArg, DynamicTemplateFunctionArg } from '@yaakapp/api'; +import type { + CallHttpAuthenticationActionArgs, + CallTemplateFunctionArgs, +} from '@yaakapp-internal/plugins'; export async function applyDynamicFormInput( ctx: Context, @@ -18,15 +21,28 @@ export async function applyDynamicFormInput( args: (DynamicTemplateFunctionArg | DynamicAuthenticationArg)[], callArgs: CallTemplateFunctionArgs | CallHttpAuthenticationActionArgs, ): Promise<(DynamicTemplateFunctionArg | DynamicAuthenticationArg)[]> { - const resolvedArgs: any[] = []; + const resolvedArgs: (DynamicTemplateFunctionArg | DynamicAuthenticationArg)[] = []; for (const { dynamic, ...arg } of args) { - const newArg: any = { + const dynamicResult = + typeof dynamic === 'function' + ? await dynamic( + ctx, + callArgs as CallTemplateFunctionArgs & CallHttpAuthenticationActionArgs, + ) + : undefined; + + const newArg = { ...arg, - ...(typeof dynamic === 'function' ? await dynamic(ctx, callArgs as any) : undefined), - }; + ...dynamicResult, + } as DynamicTemplateFunctionArg | DynamicAuthenticationArg; + if ('inputs' in newArg && Array.isArray(newArg.inputs)) { try { - newArg.inputs = await applyDynamicFormInput(ctx, newArg.inputs, callArgs as any); + newArg.inputs = await applyDynamicFormInput( + ctx, + newArg.inputs as DynamicTemplateFunctionArg[], + callArgs as CallTemplateFunctionArgs & CallHttpAuthenticationActionArgs, + ); } catch (e) { console.error('Failed to apply dynamic form input', e); } diff --git a/packages/plugin-runtime/src/index.ts b/packages/plugin-runtime/src/index.ts index c1a363dc..de518771 100644 --- a/packages/plugin-runtime/src/index.ts +++ b/packages/plugin-runtime/src/index.ts @@ -5,12 +5,12 @@ import WebSocket from 'ws'; const port = process.env.PORT; if (!port) { - throw new Error('Plugin runtime missing PORT') + throw new Error('Plugin runtime missing PORT'); } const host = process.env.HOST; if (!host) { - throw new Error('Plugin runtime missing HOST') + throw new Error('Plugin runtime missing HOST'); } const pluginToAppEvents = new EventChannel(); @@ -26,7 +26,7 @@ ws.on('message', async (e: Buffer) => { } }); ws.on('open', () => console.log('Plugin runtime connected to websocket')); -ws.on('error', (err: any) => console.error('Plugin runtime websocket error', err)); +ws.on('error', (err: unknown) => console.error('Plugin runtime websocket error', err)); ws.on('close', (code: number) => console.log('Plugin runtime websocket closed', code)); // Listen for incoming events from plugins @@ -39,7 +39,12 @@ async function handleIncoming(msg: string) { const pluginEvent: InternalEvent = JSON.parse(msg); // Handle special event to bootstrap plugin if (pluginEvent.payload.type === 'boot_request') { - const plugin = new PluginHandle(pluginEvent.pluginRefId, pluginEvent.context, pluginEvent.payload, pluginToAppEvents); + const plugin = new PluginHandle( + pluginEvent.pluginRefId, + pluginEvent.context, + pluginEvent.payload, + pluginToAppEvents, + ); plugins[pluginEvent.pluginRefId] = plugin; } diff --git a/packages/plugin-runtime/src/interceptStdout.ts b/packages/plugin-runtime/src/interceptStdout.ts index 0f130b21..089d0a31 100644 --- a/packages/plugin-runtime/src/interceptStdout.ts +++ b/packages/plugin-runtime/src/interceptStdout.ts @@ -1,28 +1,20 @@ -import process from "node:process"; +import process from 'node:process'; -export function interceptStdout( - intercept: (text: string) => string, -) { +export function interceptStdout(intercept: (text: string) => string) { const old_stdout_write = process.stdout.write; const old_stderr_write = process.stderr.write; - process.stdout.write = (function (write) { - return function (text: string) { - arguments[0] = interceptor(text, intercept); - // deno-lint-ignore no-explicit-any - write.apply(process.stdout, arguments as any); + process.stdout.write = ((write) => + ((text: string, ...args: never[]) => { + write.call(process.stdout, interceptor(text, intercept), ...args); return true; - }; - })(process.stdout.write); + }) as typeof process.stdout.write)(process.stdout.write); - process.stderr.write = (function (write) { - return function (text: string) { - arguments[0] = interceptor(text, intercept); - // deno-lint-ignore no-explicit-any - write.apply(process.stderr, arguments as any); + process.stderr.write = ((write) => + ((text: string, ...args: never[]) => { + write.call(process.stderr, interceptor(text, intercept), ...args); return true; - }; - })(process.stderr.write); + }) as typeof process.stderr.write)(process.stderr.write); // puts back to original return function unhook() { @@ -32,6 +24,5 @@ export function interceptStdout( } function interceptor(text: string, fn: (text: string) => string) { - return fn(text).replace(/\n$/, "") + - (fn(text) && /\n$/.test(text) ? "\n" : ""); + return fn(text).replace(/\n$/, '') + (fn(text) && /\n$/.test(text) ? '\n' : ''); } diff --git a/packages/plugin-runtime/src/migrations.ts b/packages/plugin-runtime/src/migrations.ts index 0cde0eab..322d24f9 100644 --- a/packages/plugin-runtime/src/migrations.ts +++ b/packages/plugin-runtime/src/migrations.ts @@ -5,10 +5,15 @@ export function migrateTemplateFunctionSelectOptions( ): TemplateFunctionPlugin { const migratedArgs = f.args.map((a) => { if (a.type === 'select') { - a.options = a.options.map((o) => ({ - ...o, - label: o.label || (o as any).name, - })); + // Migrate old options that had 'name' instead of 'label' + type LegacyOption = { label?: string; value: string; name?: string }; + a.options = a.options.map((o) => { + const legacy = o as LegacyOption; + return { + label: legacy.label ?? legacy.name ?? '', + value: legacy.value, + }; + }); } return a; }); diff --git a/packages/plugin-runtime/tests/common.test.ts b/packages/plugin-runtime/tests/common.test.ts index de128a9a..700d1106 100644 --- a/packages/plugin-runtime/tests/common.test.ts +++ b/packages/plugin-runtime/tests/common.test.ts @@ -1,6 +1,6 @@ import { applyFormInputDefaults } from '@yaakapp-internal/lib/templateFunction'; -import { CallTemplateFunctionArgs } from '@yaakapp-internal/plugins'; -import { Context, DynamicTemplateFunctionArg } from '@yaakapp/api'; +import type { CallTemplateFunctionArgs } from '@yaakapp-internal/plugins'; +import type { Context, DynamicTemplateFunctionArg } from '@yaakapp/api'; import { describe, expect, test } from 'vitest'; import { applyDynamicFormInput } from '../src/common'; diff --git a/plugins/action-send-folder/package.json b/plugins/action-send-folder/package.json new file mode 100644 index 00000000..3b0f7570 --- /dev/null +++ b/plugins/action-send-folder/package.json @@ -0,0 +1,16 @@ +{ + "name": "@yaak/action-send-folder", + "displayName": "Send All", + "description": "Send all HTTP requests in a folder sequentially", + "repository": { + "type": "git", + "url": "https://github.com/mountain-loop/yaak.git", + "directory": "plugins/action-send-folder" + }, + "private": true, + "version": "0.1.0", + "scripts": { + "build": "yaakcli build", + "dev": "yaakcli dev" + } +} diff --git a/plugins/action-send-folder/src/index.ts b/plugins/action-send-folder/src/index.ts new file mode 100644 index 00000000..d0395c00 --- /dev/null +++ b/plugins/action-send-folder/src/index.ts @@ -0,0 +1,74 @@ +import type { PluginDefinition } from '@yaakapp/api'; + +export const plugin: PluginDefinition = { + folderActions: [ + { + label: 'Send All', + icon: 'send_horizontal', + async onSelect(ctx, args) { + const targetFolder = args.folder; + + // Get all folders and HTTP requests + const [allFolders, allRequests] = await Promise.all([ + ctx.folder.list(), + ctx.httpRequest.list(), + ]); + + // Build a set of all folder IDs that are descendants of the target folder + const folderIds = new Set([targetFolder.id]); + const addDescendants = (parentId: string) => { + for (const folder of allFolders) { + if (folder.folderId === parentId && !folderIds.has(folder.id)) { + folderIds.add(folder.id); + addDescendants(folder.id); + } + } + }; + addDescendants(targetFolder.id); + + // Filter HTTP requests to those in the target folder or its descendants + const requestsToSend = allRequests.filter( + (req) => req.folderId != null && folderIds.has(req.folderId), + ); + + if (requestsToSend.length === 0) { + await ctx.toast.show({ + message: 'No requests in folder', + icon: 'info', + color: 'info', + }); + return; + } + + // Send each request sequentially + let successCount = 0; + let errorCount = 0; + + for (const request of requestsToSend) { + try { + await ctx.httpRequest.send({ httpRequest: request }); + successCount++; + } catch (error) { + errorCount++; + console.error(`Failed to send request ${request.id}:`, error); + } + } + + // Show summary toast + if (errorCount === 0) { + await ctx.toast.show({ + message: `Sent ${successCount} request${successCount !== 1 ? 's' : ''}`, + icon: 'send_horizontal', + color: 'success', + }); + } else { + await ctx.toast.show({ + message: `Sent ${successCount}, failed ${errorCount}`, + icon: 'alert_triangle', + color: 'warning', + }); + } + }, + }, + ], +}; diff --git a/plugins/action-send-folder/tsconfig.json b/plugins/action-send-folder/tsconfig.json new file mode 100644 index 00000000..4082f16a --- /dev/null +++ b/plugins/action-send-folder/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../tsconfig.json" +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 1bd87743..eadb3a28 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1276,7 +1276,7 @@ async fn cmd_install_plugin( app_handle: AppHandle, window: WebviewWindow, ) -> YaakResult { - plugin_manager.add_plugin_by_dir(&PluginContext::new(&window), &directory).await?; + plugin_manager.add_plugin_by_dir(&PluginContext::new(&window), &directory, true).await?; Ok(app_handle.db().upsert_plugin( &Plugin { directory: directory.into(), url, ..Default::default() }, diff --git a/src-tauri/src/plugin_events.rs b/src-tauri/src/plugin_events.rs index b167b014..8566dcbe 100644 --- a/src-tauri/src/plugin_events.rs +++ b/src-tauri/src/plugin_events.rs @@ -21,10 +21,10 @@ use yaak_plugins::error::Error::PluginErr; use yaak_plugins::events::{ Color, DeleteKeyValueResponse, EmptyPayload, ErrorResponse, FindHttpResponsesResponse, GetCookieValueResponse, GetHttpRequestByIdResponse, GetKeyValueResponse, Icon, InternalEvent, - InternalEventPayload, ListCookieNamesResponse, ListHttpRequestsResponse, ListWorkspacesResponse, - RenderGrpcRequestResponse, RenderHttpRequestResponse, SendHttpRequestResponse, - SetKeyValueResponse, ShowToastRequest, TemplateRenderResponse, WindowInfoResponse, - WindowNavigateEvent, WorkspaceInfo, + InternalEventPayload, ListCookieNamesResponse, ListHttpRequestsResponse, + ListWorkspacesResponse, RenderGrpcRequestResponse, RenderHttpRequestResponse, + SendHttpRequestResponse, SetKeyValueResponse, ShowToastRequest, TemplateRenderResponse, + WindowInfoResponse, WindowNavigateEvent, WorkspaceInfo, }; use yaak_plugins::plugin_handle::PluginHandle; use yaak_plugins::template_callback::PluginTemplateCallback; @@ -107,7 +107,7 @@ pub(crate) async fn handle_plugin_event( Workspace(app_handle.db().upsert_workspace(m, &UpdateSource::Plugin)?) } _ => { - return Err(PluginErr("Upsert not supported for this model type".into()).into()) + return Err(PluginErr("Upsert not supported for this model type".into()).into()); } }; @@ -118,14 +118,10 @@ pub(crate) async fn handle_plugin_event( InternalEventPayload::DeleteModelRequest(req) => { let model = match req.model.as_str() { "http_request" => AnyModel::HttpRequest( - app_handle - .db() - .delete_http_request_by_id(&req.id, &UpdateSource::Plugin)?, + app_handle.db().delete_http_request_by_id(&req.id, &UpdateSource::Plugin)?, ), "grpc_request" => AnyModel::GrpcRequest( - app_handle - .db() - .delete_grpc_request_by_id(&req.id, &UpdateSource::Plugin)?, + app_handle.db().delete_grpc_request_by_id(&req.id, &UpdateSource::Plugin)?, ), "websocket_request" => AnyModel::WebsocketRequest( app_handle @@ -133,17 +129,13 @@ pub(crate) async fn handle_plugin_event( .delete_websocket_request_by_id(&req.id, &UpdateSource::Plugin)?, ), "folder" => AnyModel::Folder( - app_handle - .db() - .delete_folder_by_id(&req.id, &UpdateSource::Plugin)?, + app_handle.db().delete_folder_by_id(&req.id, &UpdateSource::Plugin)?, ), "environment" => AnyModel::Environment( - app_handle - .db() - .delete_environment_by_id(&req.id, &UpdateSource::Plugin)?, + app_handle.db().delete_environment_by_id(&req.id, &UpdateSource::Plugin)?, ), _ => { - return Err(PluginErr("Delete not supported for this model type".into()).into()) + return Err(PluginErr("Delete not supported for this model type".into()).into()); } }; diff --git a/src-tauri/yaak-mac-window/Cargo.toml b/src-tauri/yaak-mac-window/Cargo.toml index 382af7b1..19a5655a 100644 --- a/src-tauri/yaak-mac-window/Cargo.toml +++ b/src-tauri/yaak-mac-window/Cargo.toml @@ -5,6 +5,9 @@ version = "0.1.0" edition = "2024" publish = false +[lints.rust] +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(feature, values("cargo-clippy"))'] } + [build-dependencies] tauri-plugin = { workspace = true, features = ["build"] } diff --git a/src-tauri/yaak-models/src/queries/plugins.rs b/src-tauri/yaak-models/src/queries/plugins.rs index 2355d11a..21451b12 100644 --- a/src-tauri/yaak-models/src/queries/plugins.rs +++ b/src-tauri/yaak-models/src/queries/plugins.rs @@ -8,6 +8,10 @@ impl<'a> DbContext<'a> { self.find_one(PluginIden::Id, id) } + pub fn get_plugin_by_directory(&self, directory: &str) -> Option { + self.find_optional(PluginIden::Directory, directory) + } + pub fn list_plugins(&self) -> Result> { self.find_all() } diff --git a/src-tauri/yaak-plugins/bindings/gen_events.ts b/src-tauri/yaak-plugins/bindings/gen_events.ts index ee150d4d..ecbd3c46 100644 --- a/src-tauri/yaak-plugins/bindings/gen_events.ts +++ b/src-tauri/yaak-plugins/bindings/gen_events.ts @@ -423,7 +423,7 @@ export type ListCookieNamesRequest = {}; export type ListCookieNamesResponse = { names: Array, }; -export type ListFoldersRequest = Record; +export type ListFoldersRequest = {}; export type ListFoldersResponse = { folders: Array, }; diff --git a/src-tauri/yaak-plugins/src/events.rs b/src-tauri/yaak-plugins/src/events.rs index c53e221d..c6423467 100644 --- a/src-tauri/yaak-plugins/src/events.rs +++ b/src-tauri/yaak-plugins/src/events.rs @@ -1351,8 +1351,8 @@ pub struct ListHttpRequestsResponse { } #[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] -#[serde(default, rename_all = "camelCase")] -#[ts(export, export_to = "gen_events.ts")] +#[serde(default)] +#[ts(export, type = "{}", export_to = "gen_events.ts")] pub struct ListFoldersRequest {} #[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] diff --git a/src-tauri/yaak-plugins/src/install.rs b/src-tauri/yaak-plugins/src/install.rs index 69369438..cf067e46 100644 --- a/src-tauri/yaak-plugins/src/install.rs +++ b/src-tauri/yaak-plugins/src/install.rs @@ -55,7 +55,7 @@ pub async fn download_and_install( zip_extract::extract(Cursor::new(&bytes), &plugin_dir, true)?; info!("Extracted plugin {} to {}", plugin_version.id, plugin_dir_str); - plugin_manager.add_plugin_by_dir(&PluginContext::new(&window), &plugin_dir_str).await?; + plugin_manager.add_plugin_by_dir(&PluginContext::new(&window), &plugin_dir_str, true).await?; window.db().upsert_plugin( &Plugin { diff --git a/src-tauri/yaak-plugins/src/manager.rs b/src-tauri/yaak-plugins/src/manager.rs index 3d089f78..fdd5f0c4 100644 --- a/src-tauri/yaak-plugins/src/manager.rs +++ b/src-tauri/yaak-plugins/src/manager.rs @@ -1,5 +1,6 @@ use crate::error::Error::{ - AuthPluginNotFound, ClientNotInitializedErr, PluginErr, PluginNotFoundErr, UnknownEventErr, + self, AuthPluginNotFound, ClientNotInitializedErr, PluginErr, PluginNotFoundErr, + UnknownEventErr, }; use crate::error::Result; use crate::events::{ @@ -35,10 +36,10 @@ use tokio::net::TcpListener; use tokio::sync::mpsc::error::TrySendError; use tokio::sync::{Mutex, mpsc}; use tokio::time::{Instant, timeout}; -use yaak_models::models::Environment; +use yaak_models::models::{Environment, Plugin}; use yaak_models::query_manager::QueryManagerExt; use yaak_models::render::make_vars_hashmap; -use yaak_models::util::generate_id; +use yaak_models::util::{UpdateSource, generate_id}; use yaak_templates::error::Error::RenderError; use yaak_templates::error::Result as TemplateResult; use yaak_templates::{RenderErrorBehavior, RenderOptions, render_json_value_raw}; @@ -46,18 +47,13 @@ use yaak_templates::{RenderErrorBehavior, RenderOptions, render_json_value_raw}; #[derive(Clone)] pub struct PluginManager { subscribers: Arc>>>, - plugins: Arc>>, + plugin_handles: Arc>>, kill_tx: tokio::sync::watch::Sender, ws_service: Arc, vendored_plugin_dir: PathBuf, pub(crate) installed_plugin_dir: PathBuf, } -#[derive(Clone)] -struct PluginCandidate { - dir: String, -} - impl PluginManager { pub fn new(app_handle: AppHandle) -> PluginManager { let (events_tx, mut events_rx) = mpsc::channel(128); @@ -80,7 +76,7 @@ impl PluginManager { .join("installed-plugins"); let plugin_manager = PluginManager { - plugins: Default::default(), + plugin_handles: Default::default(), subscribers: Default::default(), ws_service: Arc::new(ws_service.clone()), kill_tx: kill_server_tx, @@ -109,7 +105,7 @@ impl PluginManager { // Handle when client plugin runtime disconnects tauri::async_runtime::spawn(async move { - while let Some(_) = client_disconnect_rx.recv().await { + while (client_disconnect_rx.recv().await).is_some() { // Happens when the app is closed info!("Plugin runtime client disconnected"); } @@ -163,10 +159,10 @@ impl PluginManager { plugin_manager } - async fn list_plugin_dirs( + async fn list_available_plugins( &self, app_handle: &AppHandle, - ) -> Vec { + ) -> Result> { let plugins_dir = if is_dev() { // Use plugins directly for easy development env::current_dir() @@ -178,18 +174,27 @@ impl PluginManager { info!("Loading bundled plugins from {plugins_dir:?}"); - let bundled_plugin_dirs: Vec = read_plugins_dir(&plugins_dir) + // Read bundled plugin directories from disk + let bundled_plugin_dirs: Vec = read_plugins_dir(&plugins_dir) .await - .expect(format!("Failed to read plugins dir: {:?}", plugins_dir).as_str()) - .iter() - .map(|d| PluginCandidate { dir: d.into() }) - .collect(); + .expect(&format!("Failed to read plugins dir: {:?}", plugins_dir)); - let plugins = app_handle.db().list_plugins().unwrap_or_default(); - let installed_plugin_dirs: Vec = - plugins.iter().map(|p| PluginCandidate { dir: p.directory.to_owned() }).collect(); + // Ensure all bundled plugins make it into the database + for dir in &bundled_plugin_dirs { + if app_handle.db().get_plugin_by_directory(dir).is_none() { + app_handle.db().upsert_plugin( + &Plugin { + directory: dir.clone(), + enabled: true, + url: None, + ..Default::default() + }, + &UpdateSource::Background, + )?; + } + } - [bundled_plugin_dirs, installed_plugin_dirs].concat() + Ok(app_handle.db().list_plugins()?) } pub async fn uninstall(&self, plugin_context: &PluginContext, dir: &str) -> Result<()> { @@ -202,16 +207,18 @@ impl PluginManager { plugin_context: &PluginContext, plugin: &PluginHandle, ) -> Result<()> { - // Terminate the plugin - self.send_to_plugin_and_wait( - plugin_context, - plugin, - &InternalEventPayload::TerminateRequest, - ) - .await?; + // Terminate the plugin if it's enabled + if plugin.enabled { + self.send_to_plugin_and_wait( + plugin_context, + plugin, + &InternalEventPayload::TerminateRequest, + ) + .await?; + } // Remove the plugin from the list - let mut plugins = self.plugins.lock().await; + let mut plugins = self.plugin_handles.lock().await; let pos = plugins.iter().position(|p| p.ref_id == plugin.ref_id); if let Some(pos) = pos { plugins.remove(pos); @@ -220,7 +227,12 @@ impl PluginManager { Ok(()) } - pub async fn add_plugin_by_dir(&self, plugin_context: &PluginContext, dir: &str) -> Result<()> { + pub async fn add_plugin_by_dir( + &self, + plugin_context: &PluginContext, + dir: &str, + enabled: bool, + ) -> Result<()> { info!("Adding plugin by dir {dir}"); let maybe_tx = self.ws_service.app_to_plugin_events_tx.lock().await; @@ -228,32 +240,32 @@ impl PluginManager { None => return Err(ClientNotInitializedErr), Some(tx) => tx, }; - let plugin_handle = PluginHandle::new(dir, tx.clone())?; + let plugin_handle = PluginHandle::new(dir, enabled, tx.clone())?; let dir_path = Path::new(dir); let is_vendored = dir_path.starts_with(self.vendored_plugin_dir.as_path()); let is_installed = dir_path.starts_with(self.installed_plugin_dir.as_path()); - // Boot the plugin - let event = timeout( - Duration::from_secs(5), - self.send_to_plugin_and_wait( - plugin_context, - &plugin_handle, - &InternalEventPayload::BootRequest(BootRequest { - dir: dir.to_string(), - watch: !is_vendored && !is_installed, - }), - ), - ) - .await??; + // Boot the plugin if it's enabled + if enabled { + let event = self + .send_to_plugin_and_wait( + plugin_context, + &plugin_handle, + &InternalEventPayload::BootRequest(BootRequest { + dir: dir.to_string(), + watch: !is_vendored && !is_installed, + }), + ) + .await?; - if !matches!(event.payload, InternalEventPayload::BootResponse) { - return Err(UnknownEventErr); + if !matches!(event.payload, InternalEventPayload::BootResponse) { + return Err(UnknownEventErr); + } } - let mut plugins = self.plugins.lock().await; - plugins.retain(|p| p.dir != dir); - plugins.push(plugin_handle.clone()); + let mut plugin_handles = self.plugin_handles.lock().await; + plugin_handles.retain(|p| p.dir != dir); + plugin_handles.push(plugin_handle.clone()); Ok(()) } @@ -263,22 +275,24 @@ impl PluginManager { app_handle: &AppHandle, plugin_context: &PluginContext, ) -> Result<()> { + info!("Initializing all plugins"); let start = Instant::now(); - let candidates = self.list_plugin_dirs(app_handle).await; - for candidate in candidates.clone() { - // First remove the plugin if it exists - if let Some(plugin) = self.get_plugin_by_dir(candidate.dir.as_str()).await { - if let Err(e) = self.remove_plugin(plugin_context, &plugin).await { - error!("Failed to remove plugin {} {e:?}", candidate.dir); + for plugin in self.list_available_plugins(app_handle).await?.clone() { + // First remove the plugin if it exists and is enabled + if let Some(plugin_handle) = self.get_plugin_by_dir(&plugin.directory).await { + if let Err(e) = self.remove_plugin(plugin_context, &plugin_handle).await { + error!("Failed to remove plugin {} {e:?}", plugin.directory); continue; } } - if let Err(e) = self.add_plugin_by_dir(plugin_context, candidate.dir.as_str()).await { - warn!("Failed to add plugin {} {e:?}", candidate.dir); + if let Err(e) = + self.add_plugin_by_dir(plugin_context, &plugin.directory, plugin.enabled).await + { + warn!("Failed to add plugin {} {e:?}", plugin.directory); } } - let plugins = self.plugins.lock().await; + let plugins = self.plugin_handles.lock().await; let names = plugins.iter().map(|p| p.dir.to_string()).collect::>(); info!( "Initialized {} plugins in {:?}:\n - {}", @@ -324,15 +338,15 @@ impl PluginManager { } pub async fn get_plugin_by_ref_id(&self, ref_id: &str) -> Option { - self.plugins.lock().await.iter().find(|p| p.ref_id == ref_id).cloned() + self.plugin_handles.lock().await.iter().find(|p| p.ref_id == ref_id).cloned() } pub async fn get_plugin_by_dir(&self, dir: &str) -> Option { - self.plugins.lock().await.iter().find(|p| p.dir == dir).cloned() + self.plugin_handles.lock().await.iter().find(|p| p.dir == dir).cloned() } pub async fn get_plugin_by_name(&self, name: &str) -> Option { - for plugin in self.plugins.lock().await.iter().cloned() { + for plugin in self.plugin_handles.lock().await.iter().cloned() { let info = plugin.info(); if info.name == name { return Some(plugin); @@ -347,9 +361,19 @@ impl PluginManager { plugin: &PluginHandle, payload: &InternalEventPayload, ) -> Result { + if !plugin.enabled { + return Err(Error::PluginErr(format!("Plugin {} is disabled", plugin.metadata.name))); + } + let events = self.send_to_plugins_and_wait(plugin_context, payload, vec![plugin.to_owned()]).await?; - Ok(events.first().unwrap().to_owned()) + Ok(events + .first() + .ok_or(Error::PluginErr(format!( + "No plugin events returned for: {}", + plugin.metadata.name + )))? + .to_owned()) } async fn send_and_wait( @@ -357,7 +381,7 @@ impl PluginManager { plugin_context: &PluginContext, payload: &InternalEventPayload, ) -> Result> { - let plugins = { self.plugins.lock().await.clone() }; + let plugins = { self.plugin_handles.lock().await.clone() }; self.send_to_plugins_and_wait(plugin_context, payload, plugins).await } @@ -373,6 +397,7 @@ impl PluginManager { // 1. Build the events with IDs and everything let events_to_send = plugins .iter() + .filter(|p| p.enabled) .map(|p| p.build_event_to_send(plugin_context, payload, None)) .collect::>(); @@ -383,19 +408,28 @@ impl PluginManager { tokio::spawn(async move { let mut found_events = Vec::new(); - while let Some(event) = rx.recv().await { - let matched_sent_event = events_to_send - .iter() - .find(|e| Some(e.id.to_owned()) == event.reply_id) - .is_some(); - if matched_sent_event { - found_events.push(event.clone()); - }; + let collect_events = async { + while let Some(event) = rx.recv().await { + let matched_sent_event = + events_to_send.iter().any(|e| Some(e.id.to_owned()) == event.reply_id); + if matched_sent_event { + found_events.push(event.clone()); + }; - let found_them_all = found_events.len() == events_to_send.len(); - if found_them_all { - break; + let found_them_all = found_events.len() == events_to_send.len(); + if found_them_all { + break; + } } + }; + + // Timeout after 10 seconds to prevent hanging forever if plugin doesn't respond + if timeout(Duration::from_secs(5), collect_events).await.is_err() { + warn!( + "Timeout waiting for plugin responses. Got {}/{} responses", + found_events.len(), + events_to_send.len() + ); } found_events @@ -586,7 +620,7 @@ impl PluginManager { // We don't want to fail for this op because the UI will not be able to list any auth types then let render_opt = RenderOptions { error_behavior: RenderErrorBehavior::ReturnEmpty }; let rendered_values = render_json_value_raw(json!(values), vars, &cb, &render_opt).await?; - let context_id = format!("{:x}", md5::compute(model_id.to_string())); + let context_id = format!("{:x}", md5::compute(model_id)); let event = self .send_to_plugin_and_wait( @@ -754,7 +788,7 @@ impl PluginManager { // We don't want to fail for this op because the UI will not be able to list any auth types then let render_opt = RenderOptions { error_behavior: RenderErrorBehavior::ReturnEmpty }; let rendered_values = render_json_value_raw(json!(values), vars, &cb, &render_opt).await?; - let context_id = format!("{:x}", md5::compute(model_id.to_string())); + let context_id = format!("{:x}", md5::compute(model_id)); let event = self .send_to_plugin_and_wait( &PluginContext::new(window), @@ -804,7 +838,7 @@ impl PluginManager { .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(model_id.to_string())); + let context_id = format!("{:x}", md5::compute(model_id)); self.send_to_plugin_and_wait( &PluginContext::new(window), &plugin, @@ -831,7 +865,7 @@ impl PluginManager { plugin_context: &PluginContext, ) -> Result { let disabled = match req.values.get("disabled") { - Some(JsonPrimitive::Boolean(v)) => v.clone(), + Some(JsonPrimitive::Boolean(v)) => *v, _ => false, }; diff --git a/src-tauri/yaak-plugins/src/plugin_handle.rs b/src-tauri/yaak-plugins/src/plugin_handle.rs index 1a9cad69..d361626a 100644 --- a/src-tauri/yaak-plugins/src/plugin_handle.rs +++ b/src-tauri/yaak-plugins/src/plugin_handle.rs @@ -10,12 +10,13 @@ use tokio::sync::{Mutex, mpsc}; pub struct PluginHandle { pub ref_id: String, pub dir: String, + pub enabled: bool, pub(crate) to_plugin_tx: Arc>>, pub(crate) metadata: PluginMetadata, } impl PluginHandle { - pub fn new(dir: &str, tx: mpsc::Sender) -> Result { + pub fn new(dir: &str, enabled: bool, tx: mpsc::Sender) -> Result { let ref_id = gen_id(); let metadata = get_plugin_meta(&Path::new(dir))?; @@ -23,6 +24,7 @@ impl PluginHandle { ref_id: ref_id.clone(), dir: dir.to_string(), to_plugin_tx: Arc::new(Mutex::new(tx)), + enabled, metadata, }) } diff --git a/src-tauri/yaak-plugins/src/server_ws.rs b/src-tauri/yaak-plugins/src/server_ws.rs index a76ffe8c..f8130232 100644 --- a/src-tauri/yaak-plugins/src/server_ws.rs +++ b/src-tauri/yaak-plugins/src/server_ws.rs @@ -73,10 +73,17 @@ impl PluginRuntimeServerWebsocket { // Skip non-text messages if !msg.is_text() { - return; + warn!("Received non-text message from plugin runtime"); + continue; } - let msg_text = msg.into_text().unwrap(); + let msg_text = match msg.into_text() { + Ok(text) => text, + Err(e) => { + error!("Failed to convert message to text: {e:?}"); + continue; + } + }; let event = match serde_json::from_str::(&msg_text) { Ok(e) => e, Err(e) => { @@ -117,9 +124,18 @@ impl PluginRuntimeServerWebsocket { return; }, Some(event) => { - let event_bytes = serde_json::to_string(&event).unwrap(); + let event_bytes = match serde_json::to_string(&event) { + Ok(bytes) => bytes, + Err(e) => { + error!("Failed to serialize event: {:?}", e); + continue; + } + }; let msg = Message::text(event_bytes); - ws_sender.send(msg).await.unwrap(); + if let Err(e) = ws_sender.send(msg).await { + error!("Failed to send message to plugin runtime: {:?}", e); + break; + } } } } diff --git a/src-tauri/yaak-templates/pkg/yaak_templates.d.ts b/src-tauri/yaak-templates/pkg/yaak_templates.d.ts index 5d24deef..c881c571 100644 --- a/src-tauri/yaak-templates/pkg/yaak_templates.d.ts +++ b/src-tauri/yaak-templates/pkg/yaak_templates.d.ts @@ -1,5 +1,5 @@ /* tslint:disable */ /* eslint-disable */ -export function unescape_template(template: string): any; -export function escape_template(template: string): any; export function parse_template(template: string): any; +export function escape_template(template: string): any; +export function unescape_template(template: string): any; diff --git a/src-tauri/yaak-templates/pkg/yaak_templates_bg.js b/src-tauri/yaak-templates/pkg/yaak_templates_bg.js index 4d11efa6..75a38648 100644 --- a/src-tauri/yaak-templates/pkg/yaak_templates_bg.js +++ b/src-tauri/yaak-templates/pkg/yaak_templates_bg.js @@ -165,10 +165,10 @@ function takeFromExternrefTable0(idx) { * @param {string} template * @returns {any} */ -export function unescape_template(template) { +export function parse_template(template) { const ptr0 = passStringToWasm0(template, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); const len0 = WASM_VECTOR_LEN; - const ret = wasm.unescape_template(ptr0, len0); + const ret = wasm.parse_template(ptr0, len0); if (ret[2]) { throw takeFromExternrefTable0(ret[1]); } @@ -193,10 +193,10 @@ export function escape_template(template) { * @param {string} template * @returns {any} */ -export function parse_template(template) { +export function unescape_template(template) { const ptr0 = passStringToWasm0(template, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); const len0 = WASM_VECTOR_LEN; - const ret = wasm.parse_template(ptr0, len0); + const ret = wasm.unescape_template(ptr0, len0); if (ret[2]) { throw takeFromExternrefTable0(ret[1]); } diff --git a/src-tauri/yaak-templates/pkg/yaak_templates_bg.wasm b/src-tauri/yaak-templates/pkg/yaak_templates_bg.wasm index 3259c3044eaab4cc6d4d149cb782a20d0e007c48..bc426c25bc0ef2d4f1fffe498e0bf25f4df78386 100644 GIT binary patch delta 22507 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 delta 24585 zcmc(H3w%`7x$jv1G88S%k5y>nZxccb5yvUZnxL% z@Yy`>QfFzY+wFEZoKAGwJr1|C%;|KNm6bWlC8yh7>bBX+9FAcQOtHDkoF4qR9d0}3 zxZN0Nvz5|6r=!$qcX-_8HngGDQBqRkv3Y%EWy8zLt18QF9*@siYV+8g4ja~SU_TrG zvs_a{CCqiA?yxbZ#F%7v%ZpfB8*j3O+j`0u89lvbN1wT1QJ2wYO1=Ii3(elNv8bqR zvA?5ZS!&+G%$4TCjs-?nS9e-k$l0MvSz5|B+CsTGzM=uWeMV28RO0TCC7pSa>)2ha z^IzDF>;ZNYyJXS@PcY8r-@=9O*9$61 zpJk_)$%?}_j{nZy$u{zH9OKx;fvX)dtEiCM>7T;%y>|`*JYSOE?tHvzbGyzKm&snc zNAc)iu*H+4hc2W;9=L~C>~x-Ru$GPR|QJw(G+l?u`(`C!MPj?U+vcU z|1Le@cx$)vWu7s^^nC!sH%pOQ8e*9de$*UM)WSCOu;STQw zm4#8IO67EA)#Hla*T8S`-omPQSy@|c)#D%l^KE1vKx5Z$qJ>TcRh+&Pc&cnsiT)vN zd?Mdh)?HdaY?;JYxMwpzf3SRX8N{cE25*+vGor*~UNfx1^=Scj(P3lQ3H;(=^`P!m zqWs{n+M12b%RGv#OPaiv;mI@{4$n1r@ZSy}KI|~JivhF@%mzKD0A`e`0ZUVtZl?p`I5s*8A=~Cs9Gj;rH!JqdE3wqAKE(+Nc$G@N z)fbpi@wno)kf2lsDinLbO`F-__m3fnY zTmx8j!yRB%yDu5^mw{zLZl-UfCgR=&{#U@;trbm-P2}fS{_t#!A;1>}9A2y~>#JBN zz7I*A`XF^F57j)!TR85n&-^tfkH86$0 zT-713y^|}$sdD1*0MKuU?;D4YcJ3$cIh#KpM@S?IYM;- zJ(~lSUOS&T;sUSJV{uHt?RE0aBWmVU5CbxO9Ua%<>%jSMAdU*Sr%QnfuMK^I3Jy@g zT1a1E^;4fj663?8L=#x`XtE16$dBE_8%H*b08wca#H$LA0`B>FBQKWM?B>smoL+l) z$Dq{NEK5Nrww40)DFLOZ$i3Cotb~uL4vusOWW@<-bc;b&*@+RCSI>}F@8*wJ$Gxj| zVWFU;*!0!A_#djrX=``4&ng2rWwI_cW4rdCbPCny>T;hxv~5YVY}c<^tk~vbi;{qo z<5+KD>k5YX)fL7i&(-uUrDUV&$^hNz^{2R?T69pbUi5^6PiE2W-T9R9TGLt)K!ZyL$m=57dEo65^#aT6dR${MGvoHQ)KJm z($EUz>aI@0n&Q+52oyjIKt_~CvpBeZz2a<^>y|Cn2(LB|=7f)0@cwh44MJxJA_sP;;IGP$QXtK@eL4FCV?qSKsbw9L`JX;-&6kr5q?F(jXp37a0b>W;H;|g#PSxf zrb9Mo^brx_l zaVn9qWoyln6;w3J|{Y!DjKw2lsAd?ZPp-B-XzLfM0r=ym@T5bU9`ViG-kUf z4~q6TipC6z@*dH?Z&!|>wg&Af>ewsFd&Qu4i^}^&dB14?u&DgLD8Db-KPoC86y<}W z{nMiIAyGcGYcK~C9Vt{$J}k@t3>-6QC=;|>x$Y} zi}G60{%FydwJ7H#eZA;-s%X%9QQjolw~4YfW|Jsy5$(H*#%vMg?V|nFqA}Y=c~G>! zQ8Z>yl=o13PTp5EXpboG6%*b)I!N9t%KJt8hec!di}L%T{iC8W?~C$5(f(=Cn1iBx zNVFdj<(xI>kf*>DWxbwqK_}*3b?L+7k_Qcg}D~5t4RN~2XPRn95NG5%S@Cg&b*lb?+)oi zvgfc9q6u?IH^5*44MR2#UKI2d8474>vJgT;(gR3yk&{I_hu=R|FUOHjErSWP>=ysS z*hV&nmyXi`obwpq;G4n|C;|?;KM$O(2sm3nCg4O<5U!j6j@BOn4o;(ukl-pn1yf;J zF@o6ZAhjte^)OASgTf#t%n#rMc5Q;}kM8EHTk7C%VJJBY+}k|LGJe93-p$qKu|DX1 zECf^E85D+`XPet{VwF6n!D+~^MguW)KCMP91rs0(7Q;lUgA92V!>F%tSUdbSTA;wQ z`9-iVjz!bSgAyF-trs={BZZ}=?Eob~h>@__Q(BrQVgjsKHfA`wvje4!kq|b!W*QQI`1Z*5UkxNfQP>3Tvq8h`)@UgJ?^t-b0_P;R4qfSNl056n`I0sxpzpgKrr{LRn#u9w*J*}EQc*dJ0`gV---O3lvOnNB< zwOQmn#0O@cR15z&Py+Bs+~A)thZ}|#(aQa2zICQuv7LfMh)o|PZ8OMDuZ7oVTO4qZ z&jm4*;r;7}N)`v1m?(6OU17pu2ZPxI%tpN6r2vSRpEbe>tvpn;7m8&R9=Y^fuU@iL zaq<3HBP)VTAufX-Yg3%_12)CZH_w`6gICPooAvC-5#hq=ManVt+N}j@!r`g=CE@bD&~0IM0QzUiA)l(>zl(v=hUz{{LFJ2 zF|gs>F=fppzIW4xcEI!CIpdOD5LiIVCP_dX70+H28^qcUL%j#>L3 z`BlT%{+x5i9X)p<53&U}_PO)iy2c@_sGT_ZUU2Gq0fwNIFT~$CcU;pPsE`1{REF&k z2wTJ8pWkQ!+=U}O#z&sl!d7$ryyLMa-!Y?hVDP*W;>|th6K^`cvkc$Yedj&reqzhB z`4tzyW*%ixqyu(eFqe(y<1Sp}6ULAMjUcWtERSC}7UuBH3umwZA8}DDDd`y(-I5Qq zU>#8Z^+kGVAa9D`CN$0Y&a}M`0*#aP{)e#Pm6AtaZPf?y5cO~F=XYE*lD|2p$^%mX zy8=%F_U=z}&Sl@_?HAWO^i_~xe%ZzQ-B#|1`KIt`mrTSt=3i16e%;9TPZ#^&;WNzKCdXQ^B9ePjBHT9{O_5`Z5$ zuf@7;2F2oJl|iF_q=}4UGL7|u!lYjWeTC~o&Iue(**At8nZ|Ol&e5rNWLleAti5Rt zMQ}M0A)uh?{S2>(vDoKN88Yn@NqQRmY(rG@}!>e|+)97v!jsx!+{NSP=}&}X?bg@Xh;$Z$H`23R2ip{S8?iUA^ z{$`h5IotIX!^urxUUFBlGu(uLkZ?+XNq)OTYE>dypeis9jupdQ$VuA?&vt#((qPHv z=__tnxoY+L!G3lpzPlj7yGx<}=_v&$rF1k{N4 zn#UoK7^*lH&)2BPwm_lW$Aj1!z8jCDfyf+5DvjL)qlsYbkrn8%GoZer6 zC3hqB2`VF2bqGn&T7Jma7yB^#q%wNgt`y>CQ|M|xbxxynwNKy*kKh~ z18R#(eQ<41BF(&<5bt}G9AvR5lsXoH+asoxaXz%)5K9EFFb8?}#~`WljDej|wg#gBR!Hch0WzZYN*K z=CeG+E0)oeYnHXt9qU@j?Ev$rm-zCs@qptG%YyI~%dV=%e&lG4zbc?SX_wuWSPV+@ zgEq*8jEf~pcxVxR$MTW0Ek#B*FG7(y$?dRG_>?6MxWEB+rGzx94cr7+OGtIDB42El zzS6q*ApL0lQyl!W<)d>p-1mS?$gdoxaAg(@3G8}8lNH6*Vp9WNXdZE;VtI=HZ19Tw zV9Goo7$JV3m=h!VI1k35d4tkr=g{8!N zhf+rBPekHgtK`iystCW!tu}xtmMYPkkmG{lg~#F&u?O~#U`rpZ{LN7tS z1^>*fEw@RG{y{xtdKiO zZ81=jtLaY9>O9&f*oyjKy2V+LA1MRV3Ra?1;&`B38NLclN|`8C)6FUMqchLp9x)YM zfM>2ssFw!vRczKFu?>~6Lw+Zy!MKe!5o{)QDm0OdRHM_HehVEHNFWOdB;t~X#$g|) zwHn|p)WuQ&_9Cl+0l)x5Y6$ED+aT^8p#X_}V5^CJNF4$M)#4taG`PoNo+CEq$}>;i zqQWuchn7_3?AB#1dFlDE9=ZeG2W&E^VOeP#Bn($kb_%xLg$}3^7a<52WJ3I0`l9@E zu6Q~-@dH!if{coLtP*fTutYcy#Gk}^aFuL*@HU9=NH>WEoz^TEC|r@#X~D`kEg1{Q z4cv6xDcXrW2|jS9Xa_eF6_K}|3BHD{6QKv)`Y@3ja9X{DXXy;Mg?+d`qv<)pa~5yY z&Dqd!%PFM0a%3CHDTKga1FTGU;!ssSD}U?71L6yj5bhIsSS_+IE;peBze&LZj$<7j zAWtsPMAwpXbS>*y*37&T;lqH>@A3;ff zo4Q*&^^d5sZ$0{EaO0X23o#fj=Zj-7c(aJXc3v|sIYvYrLR-+7AQB&lHLw=kt9)c8 z%7|e^QX0I4$jpYwtoGWX$jn;sv};4TmSbZW8Cn6JD-!57D~7@0^Dzu7%2BgIDu*r= z3pr{AFp6{3Ko21~pE-xaLmmTQMWcxE(EDhvILL!=VT=g(0EBfV0jAD}c;KQhhj@i$ zj)~Z;RS*ecuc1{SxU@gqT+cH$xg;m&ebpR9OJLK=hB5sRoQ{A9(7+rhVF_X57*U@( zfl-{Kp#*p z82W$|id(P~L5tiWx@jdLY;r*YA%-XmLqOTN03-w&2rh834~Z?E0cLB*$Wq}=LROi+ zK1T|PTtESm3!-Yn{L666k7o11 zd2%bIH)5WhRD)fph7u?eZ(i)Ny4%p#coGG#`Mv#LG8%Z_vfZ_J2$Xq!mkY z0rDPkXb86KdBVm@Nk=O4gHn^^ixIQz-O)~Yj%N7{6l7hR4qc5RwY-WJ8x{7T0Mb$B z7j4ush!(iWL~Il-0TV%jC?cq$h|Hn)=P1k9&t@X48gTFJR@_I1LC@$pNLzuAloBgQ z#z?r$Gbpqm7apN8?CuwACN7I84J#tqk^)h|SJn$Dh>`oCd?>ahXB>8-S{ztMG>ZZ( zo9_(b7re|t5{{A=tmptUIq~)n7?FZvzX1u5mxsN!JI^Y!t0h;s)Ks zr^we4%K>>PU{irH(tkiz0d+Ly1b|tDGU9)zOo1|sBe6Nwgu_CKG*AYXO<}n78Rn7r zy(?a#*BLIjA>%{B1g|rYs-agv@b=2+~B`C zkkF=!gfd()3Gkcc_n&XqJ$>l)^ab&bK@d)&4zG_Mavyc`w^oj-wFVXX_w*>jyH`SDt_6)(b zZ9D(3o9bme-db5r4{VcvRBqcYU_QDVx+mXMqiowgv>go*+dU)?ZPzTed*~V3jve4l zx13^KGdMtK=6i}Sx#eW-(|ZTO#XiM8oyB3r?7EX)m(mB(ym@8Z)(q~j>&=L^s_{gG za0DH`Mf~GimXER$qO{l=Y9xDwG4M|K&9|OE?C=hP4SY@vfwy-4*R7LEK{CY-3xTR~ z+q1dXWCrEh4#ov{65*x!y1Wz&3iP=Gc3Zm+^GyBVxgdieogO3$E(5`+`H*68E&vM#tZyI)y_w@h`1L*NLks5*1Gi08?2#HaxT1eK^QEGMNV7{fweMM&ZPm$ zZ_y^i`H+@23855Rok7-vXqS8q+xd zq0od!M=F|(18G#!aQtVtU7_sWDz3Mj`t{HRSolM!Yd*99(m1TIRocCkpL6@AG9J&| z-YmbomH+zo=GphivOUMhJVB{BnxK&ah!n)3pgAGnI0YOQXiG)LNzh=8fH1>z&*2&k zSDuSE6Y2@aCEU58CC4^jA9T};o6xYNBixVU@(7;N1j$yk?OK*E_I!d>M5AlU${ zl3KEnx&;l0XGAb%gEDfE7j!z^OhOkSS!`WpiW^NYz3SwIcpenqYycMNptK?JJb+2zLU&re! zSHsn{f*#=r$vQ4!KEL}6dDQ?v^&Un3?f{>A&p3JY0KfB|xP0>ffBl|Gb+-)+f@p#b zL1KTYgy=x)pV%Ls#Yyvjj2m4J%0$c3c!7sZvekHodM9L5d@GUG9da*M# zOXLB;Z5M_N&Ob$KFnDCI@{yQWs2O$4sKaCZ{RBKLF}Vy18W1mTLL2nv-|wB&x&$rg zFPgCDYbNX!6C}?keC7`(=d4$WkxlI+t?Z*x%i53T95a#H6zIk!roay^9Mj!9Hmyz!$3p^?O z{IvmO$d<01f&|#W+VKt{mi&XElK;LsB*iA*cZz&u4PSEKSp4$Dy8G%ecn7{W^8NR< zoI!2^vzDWz-=lY9n>bn!9Ic`9yZG|Wz@A8bkYBV8KDa&s1Hf~)QF#x(Xuw;1`vd2X z25o^eVH)UK1Ktht%FtX?f_Aod`o?qR!9W+@Dh37-1vcr(K|(u`>Z2LrHFk`oq&TG^ zF`07zuTmN)L_y1^17{E6^16DZ<~T-GNQI;p|=)Ew#wu?Zlnv=5=E+{)_vk|95BAU=h!6 z7?bmef)yh~VUG;|)4CSr4P2Hv#f=N7g-{(#izYDy(CppKwFe@(y>zWb?gSUki2-)JQZt!EeVv-Sn-Y zp7MVOwt#vY4?H}t%JL=Ql>jCN@x%S_gZ#mV8yg=*HeV0>o(5sih2PYm>^`yY50OKJ z^~pUr<(sSz>i5YOVOCgMz*WyL?q&Pc71^AL2zY!3-pe3h0XsEN#s! zhVWW%ItyNaoq_!IJaQ6Lqw(mt3J4GlA|9q`)cNrvf$E{R2B7=ITLZ6(w+1AB-6Ny8 z{%GSj&K~)d+54<_4(4xMFk=t=1HsGm?PVQ*1N>C_;o|x=a?$p~tM>-hgbA;Y*-J<413DWk@60NU~bQ zo{HqT8vG!)TmMAT5r_u9Ny$=rLXqJ$;a@=Lox_j!n(o-zJNwc3q#=pd*5nnv2*T!Zfmh~?tkReM-PpQ)c+?4U&?HhKtR+= zoJ998;vQ6R6(UhIgW@_9DI2^ z)5J{$TRg8==g9589gN;8Am%Fgn8N)c&GP6vk`QD{$>$^81G7Dm-0FtB%9$u6ML~}Y z78Y8GOw2aSg9Ezz$rgF_4&MJ{HUH5QV{#Cqt6_RceCddSk^OF=0ALLj6C=} zz?sS{*gkr$sB&AOXWUa8bN}SEF1)35)MjIS92xmhhA#*|wL40TWt8M~l(Eli>}Y(x`McKGEP>O_EpQE+{QpR}VLdci=JPzNdr_kG&zy|RkUG?jnAma30(U0ByQW%gf{_r%a+8H zJ@6*Mdk_nh`e9t@SO^q{7(&u-wvMX% z6l|$eNCLm*Lo&fp@trRP3CDmb_!99>7Rney2saNjh}sVyuv)=B_I-~^4*z#U59=ti;Nw$J;@`_ z#@P#e{<9NUH~;wA$^5lv+u5o7>1RhyoP+^nFySH#!<|eSHWN8%Lf{dRxaIEf1Jvmf#dc3X`p^mVKpMK1t%yB zf80M(5`f^9L`g3U;w!e**AyyWnc?Nzx3y?=0)_A;!YLfwVjf}d>7?W|d4e?Hn3YF9 zH?itjxHn(vxbV60b7;*55Y6`zy&wbJ5ibd=lgAxkMYsMgGO`p=l6dbIsgV6>fj9;! zmSmoh0xsQ6FEleWkLJI4t_cp>;pggWX%Epae&G)N7TmIL9`Z8J)lQ)PYOFPeDU3e8 zIf{%Y-i!7zPYrgb6=`Gq{@>-#SGOL#U;Gvinp))dMcGr2=_I~rIb76xh{}BD^AoG* zcswH{!VFoWA@zm?-ofX`SHo!gn&?+K#$s2`aI8ga<-41Ye<6&l!euXvW=(v>3sn=F zf`A@_B#$41D4xX^l$Q_qwOaDcXyLECFg&qa#Ko9Ir1{!8Sd7@17DLLzQ;8ypfBAOW zGrWCt?sn!W7yX1-*|!$RLKS=NWHreC7P@@XX&BQjk*1|etzoMWg1fEa6~hXRcN}eW zNFJ}~w(Od<%08$;yj_k}CSV!B=-UjYL=obTtw(u4X8hVCtTbwMRKtzm4&LzMsF`;& zPYF7Gr$}O3;wZ^#xm@x%K(t{Vr{sGX%h^zLNWLF1`o15(@(Pc~^M^KA4tmwzv#X8Y z@ZuT#>6cFCjk{X;cXmYi?w6wDmKnVZCguNH#3XfccydTpTYG!bt?BNCOU<6X*50mp zX|tEl+}Tl^?(Q)~-@HDvr?s!gnAg|aN@b&u`*)7wWIm}*lB8FeB(>vNsK18# zsG|B$P#=!^Z2DswJxXuilJxul>h=;JL|u}yMpv(y(xT(W9bMxg%xOU1K=tk`n0&2^ zFMqiqx8CKGs?k{3XblGWQEzUUDmi4Iv;gHbE=g)E8WY5r5vb$czM*+qe%@>x;99i* zu|$&GMg2QaZo%i$OD}D@_7eQJ`f}p5&!Iw_7J&E}7OO}7vg?kvy@WQxOkvCzluN&% zKJrWbXXWd5yLG6?v0kHewj?o|Pg*Dzk|dh+UTb9{(7E-=%|yk4bu-Zf@MfdGNKmKuHYmYQ8xOOJYZbTB+-4^|mpG?f7k;>`l6 zv}_q6V$K_2hFa7JeUtDhb^4?$@g!81<3pFy^fTA%L+$9#9Y7!#piYqZYe$SA@~Y)6Xirv-9VXSrqF*m%0#w8eJaWj zqoF#@#rieU8NC-6T}#Yn;_Hjbd=k;<3OpIVB_=tGIn)PqBJ;OT-kBQSbQb0HMZ z!ail*!g+m?v>oHn?rX`Hu}J{O$L- zyFUEa*)`nxYEw=s_X#~P98a3`37C__IRh*aPSVE0h24FMk%j{7RSe~Pp}UkbNp~sd z^>z0cSDMOFI)##1(lc-2l}dMyVq~EZm5kZj*Rv!|>aR^X8S9^h^%Q&xbPh;T4yIF* zM&m<2bbca!6FwxKLLXkcRwm#>`eO+Io=t6eY0?Fx1qnF1<|K?UdclnZ zkCHX!b(xtqh3|T`LH?10zx(R6`X6B>TKNTRL#R;jDS$ee$6sqKT|dl{06NRHasa<7=0&J^Z(?jh>qE`Gj`bhz%6<6+l%{E^H7&IgC#Pp9>*U zB>tqa>UryrI%TJe-~XclS%#(f@d()|56u1X_nG@7G%)5GUd2D(vmZ|EFMkrY+2V|s zzP_FPn7{V=1pb%T)r$U^_GF2_9YeixIZ)hE$`e1m208T?e|m|-`5Rd(9~k+@Ayy@o zW`|4NP>H1%l-^v*N>Ccf8{Ztm&w6toPFVmn4#0?kllZhL*D2R={jFtu_FHbg{jGjd z>U4Vg0f&M$#NSrp`relD+iQGM1=?T7lZbBhn+r2w-`?>xb+tZW zBf@wRH44ja8pYq;n;0Rb7Jj}yHSNC!Q~mM^GhVWx^ZkYui}?7THO)PI%Z3NuzU`HV z4n4!xkKSsmKk}@;LE5$AVQJrnhaY?Uz$4OcS8Q7Sf$`+t4~?ha|NVidr9W?YM*U>N zR!REc#rGe3ct_g{Kiu)~oAS;Bf4pJm<$wO+PU*0`tKswDE=f9R*UZZFt}UZ)#An|w z{`Ai}ryZzM4Ws^w&JFd8796PWUwEM5&>Mlq!JiwAZ~kgSWBUO^IrPB><@9I4}Lf8K;kKEC_sbtR?lM5@tLRQp0J}HI1+McZE;h z@2nAO0rRqEC>b{shMCo3p+qw5ZsYI17Z_#zT`uZ3w0Jre%@{_~Fe3o)#9tad)6_!o zOe&g+B+OJCPR^EJhAYwu94!`(q>~vf5jEr91Z`F={vsF_QVlf|1xmv4q?wMG2A}x$ zbYDCa(ITO6%1jwyBb~(G-FkZ>j-QA|lE7nBjb%cyIN$O17+)q8k4MuPKod%*jHJpx ze!Htei(_CSnn#vd(sYE&+i)1s|Bw){k(^Umbl~<&~QNv7Y@kCk+N3}?Nc#QiG zj|eO>G9B|4_L*1q7+r+_7!VmEKoS`<9S@I45FSTnVGz)p$VOAKkeR~jAq+D4OZ!v4 zu$f4O)5)Z%0>j~y!CQYly241s3^kDgt<7{)i$zCGq}^s?H!Ks4si}|&sIy?2a3oQa zfZN*EX8EjbZNh17YZLl^rg`<0^QX17Et}Uj7cIr&S}8NGIdqDlL>s)-UIuynPXW;NPmuOcZB{ zfDynNQB_MfoHONo#egNEzv-{e}r@$223VWmVOT#uR?{J53dtbU1EgBO#S= z8;hs`o$$^lzT@+!)3IzM8r3oyP^_9}kaxc~y#fw;IFpQJ)Mz*rN~(#`Q^bUi-tkN@ z)tC{ECDPegI2lcZl1(E!I?StJE*JKg*^WLV)n#^wvyEvNbH=~x^MP+t$#66j31`!B zEi#rr^=_gfsircKtd`1TqDDNCjE|efH@{oYE8p|`qnWG`iW?C%0)EdV)6G2gUW-2y zO2;Cmnu5%Og%aTw{?2>xwzOu1!*MOHCbU#K70Qn9nqm=%Vw$!#BV#N=p|`Cq-POGi z7PIXn@tkt{G;X|GJ;926x-s%}3^U*l0Mps-F5)C30-iHcQ8O7$$4!j~exp~I$#gOd z?uuzq;KfKxJcqacUg58Q7^pDJWGogBB>)~EF^x%Q@X#M4{KVguRD?5TD2DOrbT*;J z%+%!LceM_6RepJv+B=0$`E(SY{Ql{c2{n-fE|ZCb5l$pkBXqP6m`z5Fm>N={0umVn zTthHcBI;5h7};z*5l%!?Mr7d>e&C}KeD?3${I~xe;q(9GsfZ@yv9t+nX2MZ54GlA$ zq>EP_sPo6w6ht=-{Kvv3_$9{m0}+4BNQbmc1O!V%_`u}6^Mk1su@Dq=#t4J4vRXK! z#Vwim&4H?l7?f9B(^9E)C}PB8(7k;A2jlpJ4<`FFF=!hwMN~7=SuGk*@}GS$iodtd z)v&I;WWPWucLU~zbk$< zn+mI`6tsgHg{Gcz+@7gnLP|ZUg^s!1-Se?yI2*}Av@(!zoF%JHg0)=V-PiA2I`GM!1K)kL?emM z%~VVcC*x5NCu1b}wf~{{k{OsvlG>!9CE^K{zw#er*wj4HMh2jVqY)#erm~rk7B^$3 ze(A4E&fvMG>HD&DxBPxxYZE*UdIV00~FY6+|sIel_oMfDiV zNFhNi;I8-1?YPo1rXiTv1hITNoyme3&-f?fc8-lJ$si6$e3iY9$#k&qR zS0pqQc2>(K6Il})I&-$hU-{DrzU!cGRwA29z?w$G@Cvf&XlnKeMOr_#WL{TB=#-Pq z)RHUD6G1`&J>ZpODv{M-KSO3DkxD^vynAqpAI2>l%a9dGhZ1T$b1r}H;0b=PV<;Zh zqABbJ@8tcp2+F-{s zT^$)yknjRN@kpaT5sk%Tp=dT5O&hRQu?t0OC448VzMGqxF1WTJLgx?H`k;r$ed+Ly`=IW16XIT8DFi zxgfn@(G`=9=eG=@LLj2!8ndU{%osW!{7XHr`KZcos_^Ghu%fVnF(aBxeck41{=JV* z@yFn>Cg3+lve9HVnayPQ`yaJb8fLhumO!`L^JS2Vo0FPJxQov`Oo$KXwnQt z%}mOK;|F^hoyXNbkEsBe;3dUU5zsZA31vIa;@-~!eAS=pE23dFn}+vlz+(!BQla@n zUH&h3b@4lY-8?B1N@~z)ke^UE6iP%F%$o9bgtTH*{=kvodC^cj6#)bhO@%>;MHhbc zFV}WY{08!nzuA#8ubj8AW68p0h@=)x5>iLHguoCRT$p$J^bunH4g+CoPx9f8xLX5p!YG+4;x`3_&w2qiQF zGMUI?ax%de{kKv)CP$BOVLq@*S1%(L!oS!$R3a z6h28m>kt^Y>MvSFJd2bptfZQZCrRzEVEpC3jIYo_un}PdTQO_{Hn@S&Vh!Pr1xpB} zp%kPioFOKJyA_Vz$ih>`p3ehcG*-l8StATi0OU{vW^5(RR4cON@`Rvx;m4=oQr^V) zmcK^)VJ(}0#Y(D~n2}AX+RZ%i=^TF~l@fp&uv9p#c?+NZ*@P)-HVmVYh607*Oed17 z*w}C2Crk4QasMv6K%Dn?pH8ZPCxCD!rox&=BH2XZdlnp!z<4$Ui=KpVCsWy5`8EF; zsEDPq2sR^0C`XhM;oGdi(U1m{fC*+2L1Q|6JAe9b6UN=aMt?InS9WxFFZez`^*=}Z z5&ak%OkgHW;fa=AEm|W6vg2wd0hJ$0B9yq3KlPunO8Cbx4%sjiLkba5|Q?XaFaihM(hn1f;W|g>4>c5Cm)``?&FIN2P?G*ta3aG5H&Lt zRx>|fE&ME2!(To;jNQ-Q!nk$Z|9N}_lKx59UKm9ql}wrD14B%6VloLsiK$7rF`;P8 ze2}$#GcHM4;O>E9r-xW4Z~OGzQ3Y|2rQ^|zYQl+uwu1n#r_HOry!i&!QLLbA$n~i* z60nDfY}ia8Ll6(e9%k9%LD~qY+bALoghL5nFl;MijwpM$MQ}U?TfC8`Ig_(R(!_7rm#)xEi6NNg|j^K|z~Y zm`oL}wVK()#(V?Hgmx8X_g{!+{!khre+E%lBx}UOnaC6T`$yDV6v`|U4yO@MrjSoa zKFP*?!AaArc`2 z?3)fJB5-$}hKWau&X%n`{Od>up!`rz1iz$961+0Oa zrUWj#!iM%7Kj({P_B_AlixYgAFhY*7kxap;N3yXO_-kK8E8#o<%MnkEtGljZk>&r01E~c{gX1 zX6|JF#4jRLi~MGXV3A#H+VR%A*TdM@2`_zxCl{KOS3`IY*CPr2pNt@XrS4{9`Okha zeBxXLKj};q>>7#3H26&~AD^-AeSop~qe7EIO7qaQp%PZZu}PuOBsG3-KxRAt4|*N% A0RR91 diff --git a/src-web/components/FolderLayout.tsx b/src-web/components/FolderLayout.tsx index 04cd0cf7..e3bf3947 100644 --- a/src-web/components/FolderLayout.tsx +++ b/src-web/components/FolderLayout.tsx @@ -5,6 +5,7 @@ import { useAtomValue } from 'jotai'; import type { CSSProperties, ReactNode } from 'react'; import { useCallback, useMemo } from 'react'; import { allRequestsAtom } from '../hooks/useAllRequests'; +import { useFolderActions } from '../hooks/useFolderActions'; import { useLatestHttpResponse } from '../hooks/useLatestHttpResponse'; import { sendAnyHttpRequest } from '../hooks/useSendAnyHttpRequest'; import { showDialog } from '../lib/dialog'; @@ -30,6 +31,12 @@ interface Props { export function FolderLayout({ folder, style }: Props) { const folders = useAtomValue(foldersAtom); const requests = useAtomValue(allRequestsAtom); + const folderActions = useFolderActions(); + const sendAllAction = useMemo( + () => folderActions.find((a) => a.label === 'Send All'), + [folderActions], + ); + const children = useMemo(() => { return [ ...folders.filter((f) => f.folderId === folder.id), @@ -37,6 +44,10 @@ export function FolderLayout({ folder, style }: Props) { ]; }, [folder.id, folders, requests]); + const handleSendAll = useCallback(() => { + sendAllAction?.call(folder); + }, [sendAllAction, folder]); + return (
@@ -48,6 +59,8 @@ export function FolderLayout({ folder, style }: Props) { color="secondary" size="sm" variant="border" + onClick={handleSendAll} + disabled={sendAllAction == null} > Send All diff --git a/src-web/components/Settings/SettingsPlugins.tsx b/src-web/components/Settings/SettingsPlugins.tsx index 22f2fb53..2c458f03 100644 --- a/src-web/components/Settings/SettingsPlugins.tsx +++ b/src-web/components/Settings/SettingsPlugins.tsx @@ -1,7 +1,7 @@ import { useMutation, useQuery } from '@tanstack/react-query'; import { openUrl } from '@tauri-apps/plugin-opener'; import type { Plugin } from '@yaakapp-internal/models'; -import { pluginsAtom } from '@yaakapp-internal/models'; +import { patchModel, pluginsAtom } from '@yaakapp-internal/models'; import type { PluginVersion } from '@yaakapp-internal/plugins'; import { checkPluginUpdates, @@ -18,6 +18,7 @@ import { usePluginsKey, useRefreshPlugins } from '../../hooks/usePlugins'; import { showConfirmDelete } from '../../lib/confirm'; import { minPromiseMillis } from '../../lib/minPromiseMillis'; import { Button } from '../core/Button'; +import { Checkbox } from '../core/Checkbox'; import { CountBadge } from '../core/CountBadge'; import { Icon } from '../core/Icon'; import { IconButton } from '../core/IconButton'; @@ -34,6 +35,8 @@ import { SelectFile } from '../SelectFile'; export function SettingsPlugins() { const [directory, setDirectory] = useState(null); const plugins = useAtomValue(pluginsAtom); + const bundledPlugins = plugins.filter((p) => p.url == null); + const installedPlugins = plugins.filter((p) => p.url != null); const createPlugin = useInstallPlugin(); const refreshPlugins = useRefreshPlugins(); const [tab, setTab] = useState(); @@ -49,7 +52,12 @@ export function SettingsPlugins() { { label: 'Installed', value: 'installed', - rightSlot: , + rightSlot: , + }, + { + label: 'Bundled', + value: 'bundled', + rightSlot: , }, ]} > @@ -101,6 +109,9 @@ export function SettingsPlugins() {
+ + + ); @@ -119,6 +130,27 @@ function PluginTableRowForInstalledPlugin({ plugin }: { plugin: Plugin }) { name={info.name} displayName={info.displayName} url={plugin.url} + showCheckbox={true} + showUninstall={true} + /> + ); +} + +function PluginTableRowForBundledPlugin({ plugin }: { plugin: Plugin }) { + const info = usePluginInfo(plugin.id).data; + if (info == null) { + return null; + } + + return ( + ); } @@ -134,6 +166,7 @@ function PluginTableRowForRemotePluginVersion({ pluginVersion }: { pluginVersion name={pluginVersion.name} displayName={pluginVersion.displayName} url={pluginVersion.url} + showCheckbox={false} /> ); } @@ -144,12 +177,16 @@ function PluginTableRow({ version, displayName, url, + showCheckbox = true, + showUninstall = true, }: { plugin: Plugin | null; name: string; version: string; displayName: string; url: string | null; + showCheckbox?: boolean; + showUninstall?: boolean; }) { const updates = usePluginUpdates(); const latestVersion = updates.data?.plugins.find((u) => u.name === name)?.version; @@ -158,9 +195,26 @@ function PluginTableRow({ mutationFn: (name: string) => installPlugin(name, null), }); const uninstall = usePromptUninstall(plugin?.id ?? null, displayName); + const refreshPlugins = useRefreshPlugins(); return ( + {showCheckbox && ( + + { + if (plugin) { + await patchModel(plugin, { enabled }); + refreshPlugins.mutate(); + } + }} + /> + + )} {url ? ( @@ -170,6 +224,9 @@ function PluginTableRow({ displayName )} + + {name} + {version} @@ -206,7 +263,7 @@ function PluginTableRow({ Install ) : null} - {uninstall != null && ( + {showUninstall && uninstall != null && (