From 637e5196c3cb39658e5c742a58577ec59f232bea Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Wed, 14 Aug 2024 15:31:52 -0700 Subject: [PATCH] Request actions (#65) --- README.md | 5 + package-lock.json | 8 +- package.json | 2 +- plugin-runtime-types/package.json | 2 +- .../src/gen/CallHttpRequestActionArgs.ts | 4 + .../src/gen/CallHttpRequestActionRequest.ts | 4 + .../src/gen/CopyTextRequest.ts | 3 + .../src/gen/GetHttpRequestActionsResponse.ts | 4 + .../src/gen/HttpRequestAction.ts | 3 + .../src/gen/InternalEventPayload.ts | 8 +- .../src/gen/RenderHttpRequestRequest.ts | 4 + .../src/gen/RenderHttpRequestResponse.ts | 4 + plugin-runtime-types/src/gen/RenderRequest.ts | 3 + .../src/gen/RenderResponse.ts | 3 + .../src/gen/ShowToastRequest.ts | 4 + plugin-runtime-types/src/gen/ToastVariant.ts | 3 + plugin-runtime-types/src/index.ts | 14 ++- plugin-runtime-types/src/plugins/context.ts | 10 ++ .../src/plugins/httpRequestAction.ts | 9 +- plugin-runtime-types/src/plugins/index.ts | 10 +- plugin-runtime/src/index.worker.ts | 56 ++++++++++- src-tauri/src/lib.rs | 96 +++++++++++++------ src-tauri/yaak_plugin_runtime/src/events.rs | 95 +++++++++++++++++- src-tauri/yaak_plugin_runtime/src/manager.rs | 65 ++++++------- src-web/components/Sidebar.tsx | 23 +++-- src-web/components/ToastContext.tsx | 6 ++ src-web/hooks/useCopyAsCurl.tsx | 20 ---- src-web/hooks/useHttpRequestActions.ts | 37 +++++++ src-web/lib/tauri.ts | 3 +- 29 files changed, 392 insertions(+), 116 deletions(-) create mode 100644 plugin-runtime-types/src/gen/CallHttpRequestActionArgs.ts create mode 100644 plugin-runtime-types/src/gen/CallHttpRequestActionRequest.ts create mode 100644 plugin-runtime-types/src/gen/CopyTextRequest.ts create mode 100644 plugin-runtime-types/src/gen/GetHttpRequestActionsResponse.ts create mode 100644 plugin-runtime-types/src/gen/HttpRequestAction.ts create mode 100644 plugin-runtime-types/src/gen/RenderHttpRequestRequest.ts create mode 100644 plugin-runtime-types/src/gen/RenderHttpRequestResponse.ts create mode 100644 plugin-runtime-types/src/gen/RenderRequest.ts create mode 100644 plugin-runtime-types/src/gen/RenderResponse.ts create mode 100644 plugin-runtime-types/src/gen/ShowToastRequest.ts create mode 100644 plugin-runtime-types/src/gen/ToastVariant.ts delete mode 100644 src-web/hooks/useCopyAsCurl.tsx create mode 100644 src-web/hooks/useHttpRequestActions.ts diff --git a/README.md b/README.md index 0311092a..cd999891 100644 --- a/README.md +++ b/README.md @@ -14,3 +14,8 @@ cargo sqlx migrate add ${MIGRATION_NAME} cargo sqlx migrate run --database-url 'sqlite://db.sqlite?mode=rw' cargo sqlx prepare --database-url 'sqlite://db.sqlite' ``` + +## Add App->Plugin API + +- Add event in `events.rs` +- Add handler to `index.worker.ts` diff --git a/package-lock.json b/package-lock.json index 41f5220d..49f9fb68 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,7 +27,7 @@ "@tauri-apps/plugin-log": "^2.0.0-rc.0", "@tauri-apps/plugin-os": "^2.0.0-rc.0", "@tauri-apps/plugin-shell": "^2.0.0-rc.0", - "@yaakapp/api": "^0.1.4", + "@yaakapp/api": "^0.1.6", "buffer": "^6.0.3", "classnames": "^2.3.2", "cm6-graphql": "^0.0.9", @@ -2989,9 +2989,9 @@ "integrity": "sha512-N8tkAACJx2ww8vFMneJmaAgmjAG1tnVBZJRLRcx061tmsLRZHSEZSLuGWnwPtunsSLvSqXQ2wfp7Mgqg1I+2dQ==" }, "node_modules/@yaakapp/api": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/@yaakapp/api/-/api-0.1.4.tgz", - "integrity": "sha512-dI5b2WPjTWXkaYBE/ltfxrJDIjIf/ETjMOzrfWDDcgT2GSBNlYmywZRTsk7j5cEbSQbSrDNgXYGJwG0I89aqSg==", + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@yaakapp/api/-/api-0.1.6.tgz", + "integrity": "sha512-5lYXKcOVmLzVUrkfU4JOCbz+CBV5Dm/cALoZvfbelvZWOVu3sTrBxS9cbNVQQq2B6WwLInSevk7pMq58GqIj5Q==", "dependencies": { "@types/node": "^22.0.0" } diff --git a/package.json b/package.json index 12817016..eb192631 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "@tauri-apps/plugin-os": "^2.0.0-rc.0", "@tauri-apps/plugin-shell": "^2.0.0-rc.0", "@tauri-apps/plugin-log": "^2.0.0-rc.0", - "@yaakapp/api": "^0.1.4", + "@yaakapp/api": "^0.1.6", "buffer": "^6.0.3", "classnames": "^2.3.2", "cm6-graphql": "^0.0.9", diff --git a/plugin-runtime-types/package.json b/plugin-runtime-types/package.json index b25cd4a9..0e1a641b 100644 --- a/plugin-runtime-types/package.json +++ b/plugin-runtime-types/package.json @@ -1,6 +1,6 @@ { "name": "@yaakapp/api", - "version": "0.1.4", + "version": "0.1.6", "main": "lib/index.js", "typings": "./lib/index.d.ts", "files": [ diff --git a/plugin-runtime-types/src/gen/CallHttpRequestActionArgs.ts b/plugin-runtime-types/src/gen/CallHttpRequestActionArgs.ts new file mode 100644 index 00000000..08c0ea75 --- /dev/null +++ b/plugin-runtime-types/src/gen/CallHttpRequestActionArgs.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { HttpRequest } from "./HttpRequest"; + +export type CallHttpRequestActionArgs = { httpRequest: HttpRequest, }; diff --git a/plugin-runtime-types/src/gen/CallHttpRequestActionRequest.ts b/plugin-runtime-types/src/gen/CallHttpRequestActionRequest.ts new file mode 100644 index 00000000..a34da579 --- /dev/null +++ b/plugin-runtime-types/src/gen/CallHttpRequestActionRequest.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { CallHttpRequestActionArgs } from "./CallHttpRequestActionArgs"; + +export type CallHttpRequestActionRequest = { key: string, pluginRefId: string, args: CallHttpRequestActionArgs, }; diff --git a/plugin-runtime-types/src/gen/CopyTextRequest.ts b/plugin-runtime-types/src/gen/CopyTextRequest.ts new file mode 100644 index 00000000..1f210dac --- /dev/null +++ b/plugin-runtime-types/src/gen/CopyTextRequest.ts @@ -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 CopyTextRequest = { text: string, }; diff --git a/plugin-runtime-types/src/gen/GetHttpRequestActionsResponse.ts b/plugin-runtime-types/src/gen/GetHttpRequestActionsResponse.ts new file mode 100644 index 00000000..e1619cd8 --- /dev/null +++ b/plugin-runtime-types/src/gen/GetHttpRequestActionsResponse.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { HttpRequestAction } from "./HttpRequestAction"; + +export type GetHttpRequestActionsResponse = { actions: Array, pluginRefId: string, }; diff --git a/plugin-runtime-types/src/gen/HttpRequestAction.ts b/plugin-runtime-types/src/gen/HttpRequestAction.ts new file mode 100644 index 00000000..fea59aed --- /dev/null +++ b/plugin-runtime-types/src/gen/HttpRequestAction.ts @@ -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 HttpRequestAction = { key: string, label: string, icon: string | null, }; diff --git a/plugin-runtime-types/src/gen/InternalEventPayload.ts b/plugin-runtime-types/src/gen/InternalEventPayload.ts index 68146544..f0269bed 100644 --- a/plugin-runtime-types/src/gen/InternalEventPayload.ts +++ b/plugin-runtime-types/src/gen/InternalEventPayload.ts @@ -1,16 +1,22 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { BootRequest } from "./BootRequest"; import type { BootResponse } from "./BootResponse"; +import type { CallHttpRequestActionRequest } from "./CallHttpRequestActionRequest"; +import type { CopyTextRequest } from "./CopyTextRequest"; import type { EmptyResponse } from "./EmptyResponse"; import type { ExportHttpRequestRequest } from "./ExportHttpRequestRequest"; import type { ExportHttpRequestResponse } from "./ExportHttpRequestResponse"; import type { FilterRequest } from "./FilterRequest"; import type { FilterResponse } from "./FilterResponse"; +import type { GetHttpRequestActionsResponse } from "./GetHttpRequestActionsResponse"; import type { GetHttpRequestByIdRequest } from "./GetHttpRequestByIdRequest"; import type { GetHttpRequestByIdResponse } from "./GetHttpRequestByIdResponse"; import type { ImportRequest } from "./ImportRequest"; import type { ImportResponse } from "./ImportResponse"; +import type { RenderHttpRequestRequest } from "./RenderHttpRequestRequest"; +import type { RenderHttpRequestResponse } from "./RenderHttpRequestResponse"; import type { SendHttpRequestRequest } from "./SendHttpRequestRequest"; import type { SendHttpRequestResponse } from "./SendHttpRequestResponse"; +import type { ShowToastRequest } from "./ShowToastRequest"; -export type InternalEventPayload = { "type": "boot_request" } & BootRequest | { "type": "boot_response" } & BootResponse | { "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_by_id_request" } & GetHttpRequestByIdRequest | { "type": "get_http_request_by_id_response" } & GetHttpRequestByIdResponse | { "type": "empty_response" } & EmptyResponse; +export type InternalEventPayload = { "type": "boot_request" } & BootRequest | { "type": "boot_response" } & BootResponse | { "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" } | { "type": "get_http_request_actions_response" } & GetHttpRequestActionsResponse | { "type": "call_http_request_action_request" } & CallHttpRequestActionRequest | { "type": "copy_text_request" } & CopyTextRequest | { "type": "render_http_request_request" } & RenderHttpRequestRequest | { "type": "render_http_request_response" } & RenderHttpRequestResponse | { "type": "show_toast_request" } & ShowToastRequest | { "type": "get_http_request_by_id_request" } & GetHttpRequestByIdRequest | { "type": "get_http_request_by_id_response" } & GetHttpRequestByIdResponse | { "type": "empty_response" } & EmptyResponse; diff --git a/plugin-runtime-types/src/gen/RenderHttpRequestRequest.ts b/plugin-runtime-types/src/gen/RenderHttpRequestRequest.ts new file mode 100644 index 00000000..cd70dc07 --- /dev/null +++ b/plugin-runtime-types/src/gen/RenderHttpRequestRequest.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { HttpRequest } from "./HttpRequest"; + +export type RenderHttpRequestRequest = { httpRequest: HttpRequest, }; diff --git a/plugin-runtime-types/src/gen/RenderHttpRequestResponse.ts b/plugin-runtime-types/src/gen/RenderHttpRequestResponse.ts new file mode 100644 index 00000000..06829a64 --- /dev/null +++ b/plugin-runtime-types/src/gen/RenderHttpRequestResponse.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { HttpRequest } from "./HttpRequest"; + +export type RenderHttpRequestResponse = { httpRequest: HttpRequest, }; diff --git a/plugin-runtime-types/src/gen/RenderRequest.ts b/plugin-runtime-types/src/gen/RenderRequest.ts new file mode 100644 index 00000000..790fda7b --- /dev/null +++ b/plugin-runtime-types/src/gen/RenderRequest.ts @@ -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 RenderRequest = { template: string, }; diff --git a/plugin-runtime-types/src/gen/RenderResponse.ts b/plugin-runtime-types/src/gen/RenderResponse.ts new file mode 100644 index 00000000..2b332f09 --- /dev/null +++ b/plugin-runtime-types/src/gen/RenderResponse.ts @@ -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 RenderResponse = { rendered: string, }; diff --git a/plugin-runtime-types/src/gen/ShowToastRequest.ts b/plugin-runtime-types/src/gen/ShowToastRequest.ts new file mode 100644 index 00000000..3cabd32b --- /dev/null +++ b/plugin-runtime-types/src/gen/ShowToastRequest.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { ToastVariant } from "./ToastVariant"; + +export type ShowToastRequest = { message: string, variant: ToastVariant, }; diff --git a/plugin-runtime-types/src/gen/ToastVariant.ts b/plugin-runtime-types/src/gen/ToastVariant.ts new file mode 100644 index 00000000..c302eed8 --- /dev/null +++ b/plugin-runtime-types/src/gen/ToastVariant.ts @@ -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 ToastVariant = "custom" | "copied" | "success" | "info" | "warning" | "error"; diff --git a/plugin-runtime-types/src/index.ts b/plugin-runtime-types/src/index.ts index 4cde94f7..a1d0e837 100644 --- a/plugin-runtime-types/src/index.ts +++ b/plugin-runtime-types/src/index.ts @@ -1,8 +1,11 @@ export type * from './plugins'; export type * from './themes'; +// TODO: The next ts-rs release includes the ability to put everything in 1 file! export * from './gen/BootRequest'; export * from './gen/BootResponse'; +export * from './gen/CallHttpRequestActionRequest'; +export * from './gen/CallHttpRequestActionArgs'; export * from './gen/Cookie'; export * from './gen/CookieDomain'; export * from './gen/CookieExpires'; @@ -15,11 +18,16 @@ export * from './gen/ExportHttpRequestResponse'; export * from './gen/FilterRequest'; export * from './gen/FilterResponse'; export * from './gen/Folder'; +export * from './gen/GetHttpRequestActionsResponse'; +export * from './gen/GetHttpRequestByIdRequest'; +export * from './gen/CopyTextRequest'; +export * from './gen/GetHttpRequestByIdResponse'; export * from './gen/GrpcConnection'; export * from './gen/GrpcEvent'; export * from './gen/GrpcMetadataEntry'; export * from './gen/GrpcRequest'; export * from './gen/HttpRequest'; +export * from './gen/HttpRequestAction'; export * from './gen/HttpRequestHeader'; export * from './gen/HttpResponse'; export * from './gen/HttpResponseHeader'; @@ -32,9 +40,11 @@ export * from './gen/InternalEventPayload'; export * from './gen/KeyValue'; export * from './gen/Model'; export * from './gen/SendHttpRequestRequest'; +export * from './gen/ToastVariant'; +export * from './gen/ShowToastRequest'; +export * from './gen/RenderHttpRequestRequest'; +export * from './gen/RenderHttpRequestResponse'; export * from './gen/SendHttpRequestResponse'; -export * from './gen/GetHttpRequestByIdRequest'; -export * from './gen/GetHttpRequestByIdResponse'; export * from './gen/SendHttpRequestResponse'; export * from './gen/Settings'; export * from './gen/Workspace'; diff --git a/plugin-runtime-types/src/plugins/context.ts b/plugin-runtime-types/src/plugins/context.ts index cc1d10b5..94bffa01 100644 --- a/plugin-runtime-types/src/plugins/context.ts +++ b/plugin-runtime-types/src/plugins/context.ts @@ -1,11 +1,21 @@ import { GetHttpRequestByIdRequest } from '../gen/GetHttpRequestByIdRequest'; import { GetHttpRequestByIdResponse } from '../gen/GetHttpRequestByIdResponse'; +import { RenderHttpRequestRequest } from '../gen/RenderHttpRequestRequest'; +import { RenderHttpRequestResponse } from '../gen/RenderHttpRequestResponse'; import { SendHttpRequestRequest } from '../gen/SendHttpRequestRequest'; import { SendHttpRequestResponse } from '../gen/SendHttpRequestResponse'; +import { ShowToastRequest } from '../gen/ShowToastRequest'; export type YaakContext = { + clipboard: { + copyText(text: string): void; + }; + toast: { + show(args: ShowToastRequest): void; + }; httpRequest: { send(args: SendHttpRequestRequest): Promise; getById(args: GetHttpRequestByIdRequest): Promise; + render(args: RenderHttpRequestRequest): Promise; }; }; diff --git a/plugin-runtime-types/src/plugins/httpRequestAction.ts b/plugin-runtime-types/src/plugins/httpRequestAction.ts index 297c5188..759b20cb 100644 --- a/plugin-runtime-types/src/plugins/httpRequestAction.ts +++ b/plugin-runtime-types/src/plugins/httpRequestAction.ts @@ -1,8 +1,7 @@ -import { HttpRequest } from '../gen/HttpRequest'; +import { CallHttpRequestActionArgs } from '../gen/CallHttpRequestActionArgs'; +import { HttpRequestAction } from '../gen/HttpRequestAction'; import { YaakContext } from './context'; -export type HttpRequestActionPlugin = { - key: string; - label: string; - onSelect(ctx: YaakContext, args: { httpRequest: HttpRequest }): void; +export type HttpRequestActionPlugin = HttpRequestAction & { + onSelect(ctx: YaakContext, args: CallHttpRequestActionArgs): Promise | void; }; diff --git a/plugin-runtime-types/src/plugins/index.ts b/plugin-runtime-types/src/plugins/index.ts index d59e1403..5621e2a9 100644 --- a/plugin-runtime-types/src/plugins/index.ts +++ b/plugin-runtime-types/src/plugins/index.ts @@ -1,16 +1,16 @@ -import { OneOrMany } from '../helpers'; import { FilterPlugin } from './filter'; import { HttpRequestActionPlugin } from './httpRequestAction'; import { ImporterPlugin } from './import'; import { ThemePlugin } from './theme'; + export { YaakContext } from './context'; /** * The global structure of a Yaak plugin */ export type YaakPlugin = { - importer?: OneOrMany; - theme?: OneOrMany; - filter?: OneOrMany; - httpRequestAction?: OneOrMany; + importer?: ImporterPlugin; + theme?: ThemePlugin; + filter?: FilterPlugin; + httpRequestActions?: HttpRequestActionPlugin[]; }; diff --git a/plugin-runtime/src/index.worker.ts b/plugin-runtime/src/index.worker.ts index 9d5d0a31..dbbc350d 100644 --- a/plugin-runtime/src/index.worker.ts +++ b/plugin-runtime/src/index.worker.ts @@ -1,11 +1,14 @@ import { GetHttpRequestByIdResponse, + HttpRequestAction, ImportResponse, InternalEvent, InternalEventPayload, + RenderHttpRequestResponse, SendHttpRequestResponse, } from '@yaakapp/api'; import { YaakContext } from '@yaakapp/api/lib/plugins/context'; +import { HttpRequestActionPlugin } from '@yaakapp/api/lib/plugins/httpRequestAction'; import interceptStdout from 'intercept-stdout'; import * as console from 'node:console'; import { readFileSync } from 'node:fs'; @@ -47,6 +50,10 @@ new Promise(async (resolve, reject) => { return { pluginRefId, id: genId(), replyId, payload }; } + function sendEmpty(replyId: string | null = null): string { + return sendPayload({ type: 'empty_response' }, replyId); + } + function sendPayload(payload: InternalEventPayload, replyId: string | null = null): string { const event = buildEventToSend(payload, replyId); sendEvent(event); @@ -82,6 +89,16 @@ new Promise(async (resolve, reject) => { } const ctx: YaakContext = { + clipboard: { + async copyText(text) { + await sendAndWaitForReply({ type: 'copy_text_request', text }); + }, + }, + toast: { + async show(args) { + await sendAndWaitForReply({ type: 'show_toast_request', ...args }); + }, + }, httpRequest: { async getById({ id }) { const payload = { type: 'get_http_request_by_id_request', id } as const; @@ -93,6 +110,11 @@ new Promise(async (resolve, reject) => { const { httpResponse } = await sendAndWaitForReply(payload); return httpResponse; }, + async render({ httpRequest }) { + const payload = { type: 'render_http_request_request', httpRequest } as const; + const result = await sendAndWaitForReply(payload); + return result.httpRequest; + }, }, }; @@ -151,13 +173,45 @@ new Promise(async (resolve, reject) => { sendPayload(replyPayload, replyId); return; } + + if ( + payload.type === 'get_http_request_actions_request' && + Array.isArray(mod.plugin?.httpRequestActions) + ) { + const reply: HttpRequestAction[] = mod.plugin.httpRequestActions.map( + (a: HttpRequestActionPlugin) => ({ + ...a, + onSelect: undefined, + // Add everything except onSelect + }), + ); + const replyPayload: InternalEventPayload = { + type: 'get_http_request_actions_response', + pluginRefId, + actions: reply, + }; + sendPayload(replyPayload, replyId); + return; + } + + if ( + payload.type === 'call_http_request_action_request' && + Array.isArray(mod.plugin?.httpRequestActions) + ) { + const action = mod.plugin.httpRequestActions.find((a) => a.key === payload.key); + if (typeof action?.onSelect === 'function') { + await action.onSelect(ctx, payload.args); + sendEmpty(replyId); + return; + } + } } catch (err) { console.log('Plugin call threw exception', payload.type, err); // TODO: Return errors to server } // No matches, so send back an empty response so the caller doesn't block forever - sendPayload({ type: 'empty_response' }, replyId); + sendEmpty(replyId); }); resolve(); diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index acb8a713..f5b41112 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -21,6 +21,7 @@ use tauri::TitleBarStyle; use tauri::{AppHandle, Emitter, LogicalSize, RunEvent, State, WebviewUrl, WebviewWindow}; use tauri::{Listener, Runtime}; use tauri::{Manager, WindowEvent}; +use tauri_plugin_clipboard_manager::ClipboardExt; use tauri_plugin_log::{fern, Target, TargetKind}; use tauri_plugin_shell::ShellExt; use tokio::sync::{watch, Mutex}; @@ -55,7 +56,8 @@ use yaak_models::queries::{ upsert_grpc_event, upsert_grpc_request, upsert_http_request, upsert_workspace, }; use yaak_plugin_runtime::events::{ - FilterResponse, GetHttpRequestByIdResponse, InternalEvent, InternalEventPayload, + CallHttpRequestActionRequest, FilterResponse, GetHttpRequestActionsResponse, + GetHttpRequestByIdResponse, InternalEvent, InternalEventPayload, RenderHttpRequestResponse, SendHttpRequestResponse, }; @@ -870,29 +872,24 @@ async fn cmd_import_data( } #[tauri::command] -async fn cmd_request_to_curl( - app: AppHandle, - request_id: &str, +async fn cmd_http_request_actions( plugin_manager: State<'_, PluginManager>, - environment_id: Option<&str>, -) -> Result { - let request = get_http_request(&app, request_id) +) -> Result, String> { + plugin_manager + .run_http_request_actions() .await - .map_err(|e| e.to_string())?; - let environment = match environment_id { - Some(id) => Some(get_environment(&app, id).await.map_err(|e| e.to_string())?), - None => None, - }; - let workspace = get_workspace(&app, &request.workspace_id) - .await - .map_err(|e| e.to_string())?; - let rendered = render_request(&request, &workspace, environment.as_ref()); + .map_err(|e| e.to_string()) +} - let import_response = plugin_manager - .run_export_curl(&rendered) +#[tauri::command] +async fn cmd_call_http_request_action( + req: CallHttpRequestActionRequest, + plugin_manager: State<'_, PluginManager>, +) -> Result<(), String> { + plugin_manager + .call_http_request_action(req) .await - .map_err(|e| e.to_string())?; - Ok(import_response.content) + .map_err(|e| e.to_string()) } #[tauri::command] @@ -1624,6 +1621,7 @@ pub fn run() { Ok(()) }) .invoke_handler(tauri::generate_handler![ + cmd_call_http_request_action, cmd_check_for_updates, cmd_create_cookie_jar, cmd_create_environment, @@ -1642,6 +1640,7 @@ pub fn run() { cmd_delete_http_request, cmd_delete_http_response, cmd_delete_workspace, + cmd_dismiss_notification, cmd_duplicate_grpc_request, cmd_duplicate_http_request, cmd_export_data, @@ -1656,6 +1655,7 @@ pub fn run() { cmd_get_workspace, cmd_grpc_go, cmd_grpc_reflect, + cmd_http_request_actions, cmd_import_data, cmd_list_cookie_jars, cmd_list_environments, @@ -1669,8 +1669,6 @@ pub fn run() { cmd_metadata, cmd_new_nested_window, cmd_new_window, - cmd_request_to_curl, - cmd_dismiss_notification, cmd_save_response, cmd_send_ephemeral_request, cmd_send_http_request, @@ -1915,9 +1913,49 @@ async fn handle_plugin_event( let event = match event.clone().payload { InternalEventPayload::GetHttpRequestByIdRequest(req) => { let http_request = get_http_request(app_handle, req.id.as_str()).await.ok(); - InternalEventPayload::GetHttpRequestByIdResponse(GetHttpRequestByIdResponse { - http_request, - }) + Some(InternalEventPayload::GetHttpRequestByIdResponse( + GetHttpRequestByIdResponse { http_request }, + )) + } + InternalEventPayload::CopyTextRequest(req) => { + app_handle + .clipboard() + .write_text(req.text.as_str()) + .expect("Failed to write text to clipboard"); + None + } + InternalEventPayload::ShowToastRequest(req) => { + app_handle + .emit("show_toast", req) + .expect("Failed to emit show_toast"); + None + } + InternalEventPayload::RenderHttpRequestRequest(req) => { + let webview_windows = app_handle.get_focused_window()?.webview_windows(); + let w = match webview_windows.iter().next() { + None => return None, + Some((_, w)) => w, + }; + let workspace = get_workspace(app_handle, req.http_request.workspace_id.as_str()) + .await + .expect("Failed to get workspace for request"); + + let url = w.url().unwrap(); + let mut query_pairs = url.query_pairs(); + let environment_id = query_pairs + .find(|(k, _v)| k == "environment_id") + .map(|(_k, v)| v.to_string()); + let environment = match environment_id { + None => None, + Some(id) => get_environment(w, id.as_str()).await.ok(), + }; + let rendered_http_request = + render_request(&req.http_request, &workspace, environment.as_ref()); + Some(InternalEventPayload::RenderHttpRequestResponse( + RenderHttpRequestResponse { + http_request: rendered_http_request, + }, + )) } InternalEventPayload::SendHttpRequestRequest(req) => { let webview_windows = app_handle.get_focused_window()?.webview_windows(); @@ -1964,10 +2002,12 @@ async fn handle_plugin_event( Err(_e) => return None, }; - InternalEventPayload::SendHttpRequestResponse(SendHttpRequestResponse { http_response }) + Some(InternalEventPayload::SendHttpRequestResponse( + SendHttpRequestResponse { http_response }, + )) } - _ => return None, + _ => None, }; - Some(event) + event } diff --git a/src-tauri/yaak_plugin_runtime/src/events.rs b/src-tauri/yaak_plugin_runtime/src/events.rs index bb0325c6..a5740b22 100644 --- a/src-tauri/yaak_plugin_runtime/src/events.rs +++ b/src-tauri/yaak_plugin_runtime/src/events.rs @@ -32,13 +32,24 @@ pub enum InternalEventPayload { ExportHttpRequestRequest(ExportHttpRequestRequest), ExportHttpRequestResponse(ExportHttpRequestResponse), - + SendHttpRequestRequest(SendHttpRequestRequest), SendHttpRequestResponse(SendHttpRequestResponse), + GetHttpRequestActionsRequest, + GetHttpRequestActionsResponse(GetHttpRequestActionsResponse), + CallHttpRequestActionRequest(CallHttpRequestActionRequest), + + CopyTextRequest(CopyTextRequest), + + RenderHttpRequestRequest(RenderHttpRequestRequest), + RenderHttpRequestResponse(RenderHttpRequestResponse), + + ShowToastRequest(ShowToastRequest), + GetHttpRequestByIdRequest(GetHttpRequestByIdRequest), GetHttpRequestByIdResponse(GetHttpRequestByIdResponse), - + /// Returned when a plugin doesn't get run, just so the server /// has something to listen for EmptyResponse(EmptyResponse), @@ -122,6 +133,86 @@ pub struct SendHttpRequestResponse { pub http_response: HttpResponse, } +#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] +#[serde(default, rename_all = "camelCase")] +#[ts(export)] +pub struct CopyTextRequest { + pub text: String, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] +#[serde(default, rename_all = "camelCase")] +#[ts(export)] +pub struct RenderHttpRequestRequest { + pub http_request: HttpRequest, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] +#[serde(default, rename_all = "camelCase")] +#[ts(export)] +pub struct RenderHttpRequestResponse { + pub http_request: HttpRequest, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] +#[serde(default, rename_all = "camelCase")] +#[ts(export)] +pub struct ShowToastRequest { + pub message: String, + pub variant: ToastVariant, +} + +#[derive(Debug, Clone, Serialize, Deserialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub enum ToastVariant { + Custom, + Copied, + Success, + Info, + Warning, + Error, +} + +impl Default for ToastVariant { + fn default() -> Self { + ToastVariant::Info + } +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] +#[serde(default, rename_all = "camelCase")] +#[ts(export)] +pub struct GetHttpRequestActionsResponse { + pub actions: Vec, + pub plugin_ref_id: String, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] +#[serde(default, rename_all = "camelCase")] +#[ts(export)] +pub struct HttpRequestAction { + pub key: String, + pub label: String, + pub icon: Option, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] +#[serde(default, rename_all = "camelCase")] +#[ts(export)] +pub struct CallHttpRequestActionRequest { + pub key: String, + pub plugin_ref_id: String, + pub args: CallHttpRequestActionArgs, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] +#[serde(default, rename_all = "camelCase")] +#[ts(export)] +pub struct CallHttpRequestActionArgs { + pub http_request: HttpRequest, +} + #[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] #[serde(default, rename_all = "camelCase")] #[ts(export)] diff --git a/src-tauri/yaak_plugin_runtime/src/manager.rs b/src-tauri/yaak_plugin_runtime/src/manager.rs index 64aa84bb..19d05faf 100644 --- a/src-tauri/yaak_plugin_runtime/src/manager.rs +++ b/src-tauri/yaak_plugin_runtime/src/manager.rs @@ -1,8 +1,5 @@ use crate::error::Result; -use crate::events::{ - ExportHttpRequestRequest, ExportHttpRequestResponse, FilterRequest, FilterResponse - , ImportRequest, ImportResponse, InternalEvent, InternalEventPayload, -}; +use crate::events::{CallHttpRequestActionRequest, FilterRequest, FilterResponse, GetHttpRequestActionsResponse, ImportRequest, ImportResponse, InternalEvent, InternalEventPayload}; use crate::error::Error::PluginErr; use crate::nodejs::start_nodejs_plugin_runtime; @@ -12,7 +9,6 @@ use std::time::Duration; use tauri::{AppHandle, Runtime}; use tokio::sync::mpsc; use tokio::sync::watch::Sender; -use yaak_models::models::HttpRequest; pub struct PluginManager { kill_tx: Sender, @@ -61,6 +57,29 @@ impl PluginManager { .send(&payload, source_event.plugin_ref_id.as_str(), reply_id) .await } + + pub async fn run_http_request_actions(&self) -> Result> { + let reply_events = self + .server + .send_and_wait(&InternalEventPayload::GetHttpRequestActionsRequest) + .await?; + + let mut all_actions = Vec::new(); + for event in reply_events { + if let InternalEventPayload::GetHttpRequestActionsResponse(resp) = event.payload { + all_actions.push(resp.clone()); + } + } + + Ok(all_actions) + } + + pub async fn call_http_request_action(&self, req: CallHttpRequestActionRequest) -> Result<()> { + let plugin = self.server.plugin_by_ref_id(req.plugin_ref_id.as_str()).await?; + let event = plugin.build_event_to_send(&InternalEventPayload::CallHttpRequestActionRequest(req), None); + plugin.send(&event).await?; + Ok(()) + } pub async fn run_import(&self, content: &str) -> Result<(ImportResponse, String)> { let reply_events = self @@ -72,43 +91,17 @@ impl PluginManager { // TODO: Don't just return the first valid response for event in reply_events { - match event.payload { - InternalEventPayload::ImportResponse(resp) => { - let ref_id = event.plugin_ref_id.as_str(); - let plugin = self.server.plugin_by_ref_id(ref_id).await?; - let plugin_name = plugin.name().await; - return Ok((resp, plugin_name)); - } - _ => {} + if let InternalEventPayload::ImportResponse(resp) = event.payload { + let ref_id = event.plugin_ref_id.as_str(); + let plugin = self.server.plugin_by_ref_id(ref_id).await?; + let plugin_name = plugin.name().await; + return Ok((resp, plugin_name)); } } Err(PluginErr("No import responses found".to_string())) } - pub async fn run_export_curl( - &self, - request: &HttpRequest, - ) -> Result { - let event = self - .server - .send_to_plugin_and_wait( - "exporter-curl", - &InternalEventPayload::ExportHttpRequestRequest(ExportHttpRequestRequest { - http_request: request.to_owned(), - }), - ) - .await?; - - match event.payload { - InternalEventPayload::ExportHttpRequestResponse(resp) => Ok(resp), - InternalEventPayload::EmptyResponse(_) => { - Err(PluginErr("Export returned empty".to_string())) - } - e => Err(PluginErr(format!("Export returned invalid event {:?}", e))), - } - } - pub async fn run_filter( &self, filter: &str, diff --git a/src-web/components/Sidebar.tsx b/src-web/components/Sidebar.tsx index 3b04045c..631b734b 100644 --- a/src-web/components/Sidebar.tsx +++ b/src-web/components/Sidebar.tsx @@ -10,7 +10,6 @@ import { useActiveEnvironment } from '../hooks/useActiveEnvironment'; import { useActiveRequest } from '../hooks/useActiveRequest'; import { useActiveWorkspace } from '../hooks/useActiveWorkspace'; import { useAppRoutes } from '../hooks/useAppRoutes'; -import { useCopyAsCurl } from '../hooks/useCopyAsCurl'; import { useCreateDropdownItems } from '../hooks/useCreateDropdownItems'; import { useDeleteFolder } from '../hooks/useDeleteFolder'; import { useDeleteRequest } from '../hooks/useDeleteRequest'; @@ -18,6 +17,7 @@ import { useDuplicateGrpcRequest } from '../hooks/useDuplicateGrpcRequest'; import { useDuplicateHttpRequest } from '../hooks/useDuplicateHttpRequest'; import { useFolders } from '../hooks/useFolders'; import { useHotKey } from '../hooks/useHotKey'; +import { useHttpRequestActions } from '../hooks/useHttpRequestActions'; import { useKeyValue } from '../hooks/useKeyValue'; import { useLatestGrpcConnection } from '../hooks/useLatestGrpcConnection'; import { useLatestHttpResponse } from '../hooks/useLatestHttpResponse'; @@ -34,6 +34,7 @@ import { useUpdateAnyHttpRequest } from '../hooks/useUpdateAnyHttpRequest'; import { useWorkspaces } from '../hooks/useWorkspaces'; import { fallbackRequestName } from '../lib/fallbackRequestName'; import { isResponseLoading } from '../lib/models'; +import { getHttpRequest } from '../lib/store'; import type { DropdownItem } from './core/Dropdown'; import { ContextMenu } from './core/Dropdown'; import { HttpMethodTag } from './core/HttpMethodTag'; @@ -653,7 +654,7 @@ function SidebarItem({ const renameRequest = useRenameRequest(itemId); const duplicateHttpRequest = useDuplicateHttpRequest({ id: itemId, navigateAfter: true }); const duplicateGrpcRequest = useDuplicateGrpcRequest({ id: itemId, navigateAfter: true }); - const copyAsCurl = useCopyAsCurl(itemId); + const httpRequestActions = useHttpRequestActions(); const sendRequest = useSendAnyHttpRequest(); const moveToWorkspace = useMoveToWorkspace(itemId); const sendManyRequests = useSendManyRequests(); @@ -782,12 +783,16 @@ function SidebarItem({ leftSlot: , onSelect: () => sendRequest.mutate(itemId), }, - { - key: 'copyCurl', - label: 'Copy as Curl', - leftSlot: , - onSelect: copyAsCurl.mutate, - }, + ...httpRequestActions.map((a) => ({ + key: a.key, + label: a.label, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + leftSlot: , + onSelect: async () => { + const request = await getHttpRequest(itemId); + if (request != null) await a.call(request); + }, + })), { type: 'separator' }, ] : []; @@ -829,12 +834,12 @@ function SidebarItem({ } }, [ child.children, - copyAsCurl.mutate, createDropdownItems, deleteFolder, deleteRequest, duplicateGrpcRequest, duplicateHttpRequest, + httpRequestActions, itemId, itemModel, itemName, diff --git a/src-web/components/ToastContext.tsx b/src-web/components/ToastContext.tsx index b49fb284..7989b09c 100644 --- a/src-web/components/ToastContext.tsx +++ b/src-web/components/ToastContext.tsx @@ -1,5 +1,7 @@ import type { ReactNode } from 'react'; import React, { createContext, useContext, useMemo, useRef, useState } from 'react'; +import type { ShowToastRequest } from '../../plugin-runtime-types/src'; +import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent'; import type { ToastProps } from './core/Toast'; import { Toast } from './core/Toast'; import { generateId } from '../lib/generateId'; @@ -61,6 +63,10 @@ export const ToastProvider = ({ children }: { children: React.ReactNode }) => { [], ); + useListenToTauriEvent('show_toast', (event) => { + actions.show({ ...event.payload }); + }); + const state: State = { toasts, actions }; return {children}; }; diff --git a/src-web/hooks/useCopyAsCurl.tsx b/src-web/hooks/useCopyAsCurl.tsx deleted file mode 100644 index 9a31b048..00000000 --- a/src-web/hooks/useCopyAsCurl.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { useMutation } from '@tanstack/react-query'; -import { invokeCmd } from '../lib/tauri'; -import { useActiveEnvironment } from './useActiveEnvironment'; -import { useCopy } from './useCopy'; - -export function useCopyAsCurl(requestId: string) { - const copy = useCopy(); - const [environment] = useActiveEnvironment(); - return useMutation({ - mutationKey: ['copy_as_curl', requestId], - mutationFn: async () => { - const cmd: string = await invokeCmd('cmd_request_to_curl', { - requestId, - environmentId: environment?.id, - }); - copy(cmd); - return cmd; - }, - }); -} diff --git a/src-web/hooks/useHttpRequestActions.ts b/src-web/hooks/useHttpRequestActions.ts new file mode 100644 index 00000000..d8d257c6 --- /dev/null +++ b/src-web/hooks/useHttpRequestActions.ts @@ -0,0 +1,37 @@ +import { useQuery } from '@tanstack/react-query'; +import type { + CallHttpRequestActionRequest, + GetHttpRequestActionsResponse, + HttpRequest, +} from '@yaakapp/api'; +import { invokeCmd } from '../lib/tauri'; + +export function useHttpRequestActions() { + const httpRequestActions = useQuery({ + queryKey: ['http_request_actions'], + queryFn: async () => { + const responses = (await invokeCmd( + 'cmd_http_request_actions', + )) as GetHttpRequestActionsResponse[]; + return responses; + }, + }); + + return ( + httpRequestActions.data?.flatMap((r) => + r.actions.map((a) => ({ + key: a.key, + label: a.label, + icon: a.icon, + call: async (httpRequest: HttpRequest) => { + const payload: CallHttpRequestActionRequest = { + key: a.key, + pluginRefId: r.pluginRefId, + args: { httpRequest }, + }; + await invokeCmd('cmd_call_http_request_action', { req: payload }); + }, + })), + ) ?? [] + ); +} diff --git a/src-web/lib/tauri.ts b/src-web/lib/tauri.ts index 942637e2..a2126ad8 100644 --- a/src-web/lib/tauri.ts +++ b/src-web/lib/tauri.ts @@ -2,6 +2,7 @@ import type { InvokeArgs } from '@tauri-apps/api/core'; import { invoke } from '@tauri-apps/api/core'; type TauriCmd = + | 'cmd_call_http_request_action' | 'cmd_check_for_updates' | 'cmd_create_cookie_jar' | 'cmd_create_environment' @@ -47,7 +48,6 @@ type TauriCmd = | 'cmd_metadata' | 'cmd_new_nested_window' | 'cmd_new_window' - | 'cmd_request_to_curl' | 'cmd_dismiss_notification' | 'cmd_save_response' | 'cmd_send_ephemeral_request' @@ -62,6 +62,7 @@ type TauriCmd = | 'cmd_update_http_request' | 'cmd_update_settings' | 'cmd_update_workspace' + | 'cmd_http_request_actions' | 'cmd_write_file_dev'; export async function invokeCmd(cmd: TauriCmd, args?: InvokeArgs): Promise {