From c484dd404111d16d7da175b6ce9b58ae97494287 Mon Sep 17 00:00:00 2001 From: Gregory Schier Date: Wed, 14 Aug 2024 06:42:54 -0700 Subject: [PATCH] Start on plugin ctx API (#64) --- .../src/gen/GetHttpRequestByIdRequest.ts | 3 + .../src/gen/GetHttpRequestByIdResponse.ts | 4 + .../src/gen/InternalEventPayload.ts | 6 +- .../src/gen/SendHttpRequestRequest.ts | 4 + .../src/gen/SendHttpRequestResponse.ts | 4 + plugin-runtime-types/src/index.ts | 5 + plugin-runtime-types/src/plugins/context.ts | 13 +- plugin-runtime-types/src/plugins/index.ts | 1 + plugin-runtime/src/gen/plugins/runtime.ts | 359 ------------------ plugin-runtime/src/index.worker.ts | 97 ++++- src-tauri/Cargo.lock | 66 ++-- src-tauri/Cargo.toml | 8 +- .../20240814013812_fix-env-model.sql | 2 + src-tauri/src/analytics.rs | 58 ++- src-tauri/src/grpc.rs | 2 +- src-tauri/src/http_request.rs | 26 +- src-tauri/src/lib.rs | 212 +++++++---- src-tauri/src/notifications.rs | 22 +- src-tauri/src/render.rs | 2 +- src-tauri/{grpc => yaak_grpc}/Cargo.toml | 2 +- src-tauri/{grpc => yaak_grpc}/src/codec.rs | 0 .../{grpc => yaak_grpc}/src/json_schema.rs | 0 src-tauri/{grpc => yaak_grpc}/src/lib.rs | 0 src-tauri/{grpc => yaak_grpc}/src/manager.rs | 0 src-tauri/{grpc => yaak_grpc}/src/proto.rs | 0 src-tauri/yaak_models/src/queries.rs | 242 ++++++++---- src-tauri/yaak_plugin_runtime/src/events.rs | 53 ++- src-tauri/yaak_plugin_runtime/src/manager.rs | 32 +- src-tauri/yaak_plugin_runtime/src/plugin.rs | 10 +- src-tauri/yaak_plugin_runtime/src/server.rs | 37 +- .../{templates => yaak_templates}/Cargo.toml | 2 +- .../{templates => yaak_templates}/src/lib.rs | 0 .../src/parser.rs | 0 .../src/renderer.rs | 0 src-web/components/CommandPalette.tsx | 24 +- src-web/components/CookieDialog.tsx | 2 +- src-web/components/CookieDropdown.tsx | 4 +- .../components/EnvironmentActionsDropdown.tsx | 10 +- src-web/components/GlobalHooks.tsx | 43 ++- src-web/components/MoveToWorkspaceDialog.tsx | 4 +- src-web/components/RecentRequestsDropdown.tsx | 12 +- src-web/components/SettingsDropdown.tsx | 8 +- src-web/components/Sidebar.tsx | 12 +- src-web/components/Workspace.tsx | 8 +- src-web/components/core/Editor/Editor.tsx | 2 +- .../core/Editor/twig/placeholder.ts | 1 - src-web/components/core/SplitLayout.tsx | 9 +- src-web/hooks/useActiveCookieJar.ts | 93 ++++- src-web/hooks/useActiveEnvironment.ts | 40 +- src-web/hooks/useActiveEnvironmentId.ts | 13 - src-web/hooks/useActiveWorkspace.ts | 8 +- ...tsx => useActiveWorkspaceChangedToast.tsx} | 2 +- src-web/hooks/useActiveWorkspaceId.ts | 7 - src-web/hooks/useAppRoutes.tsx | 43 ++- src-web/hooks/useCookieJars.ts | 24 +- src-web/hooks/useCopyAsCurl.tsx | 9 +- src-web/hooks/useCreateCookieJar.ts | 18 +- src-web/hooks/useCreateEnvironment.ts | 28 +- src-web/hooks/useCreateFolder.ts | 19 +- src-web/hooks/useCreateGrpcRequest.ts | 22 +- src-web/hooks/useCreateHttpRequest.ts | 18 +- src-web/hooks/useDeleteAnyGrpcRequest.tsx | 14 +- src-web/hooks/useDeleteAnyHttpRequest.tsx | 17 +- src-web/hooks/useDeleteCookieJar.tsx | 12 +- src-web/hooks/useDeleteEnvironment.tsx | 14 +- src-web/hooks/useDeleteFolder.tsx | 17 +- src-web/hooks/useDeleteGrpcConnection.ts | 12 +- src-web/hooks/useDeleteGrpcConnections.ts | 8 +- src-web/hooks/useDeleteHttpResponse.ts | 11 +- src-web/hooks/useDeleteHttpResponses.ts | 8 +- src-web/hooks/useDeleteWorkspace.tsx | 20 +- src-web/hooks/useDuplicateGrpcRequest.ts | 16 +- src-web/hooks/useDuplicateHttpRequest.ts | 16 +- src-web/hooks/useEnvironments.ts | 14 +- src-web/hooks/useFloatingSidebarHidden.ts | 6 +- src-web/hooks/useFolders.ts | 12 +- src-web/hooks/useGrpc.ts | 8 +- src-web/hooks/useGrpcRequests.ts | 14 +- src-web/hooks/useHttpRequests.ts | 14 +- src-web/hooks/useImportCurl.ts | 6 +- src-web/hooks/useImportData.tsx | 8 +- src-web/hooks/useIntrospectGraphQL.ts | 13 +- src-web/hooks/useKeyValue.ts | 5 +- src-web/hooks/useMoveToWorkspace.tsx | 8 +- src-web/hooks/useOpenWorkspace.ts | 25 +- src-web/hooks/usePreferredAppearance.ts | 2 +- src-web/hooks/useRecentCookieJars.ts | 47 +++ src-web/hooks/useRecentEnvironments.ts | 18 +- src-web/hooks/useRecentRequests.ts | 6 +- src-web/hooks/useRecentWorkspaces.ts | 12 +- src-web/hooks/useSendAnyHttpRequest.ts | 22 +- src-web/hooks/useSidebarHidden.ts | 6 +- src-web/hooks/useSidebarWidth.ts | 9 +- src-web/hooks/useSyncThemeToDocument.ts | 4 +- src-web/hooks/useSyncWorkspaceRequestTitle.ts | 2 +- src-web/hooks/useUpdateAnyFolder.ts | 12 +- src-web/hooks/useUpdateAnyGrpcRequest.ts | 14 +- src-web/hooks/useUpdateAnyHttpRequest.ts | 14 +- src-web/hooks/useUpdateCookieJar.ts | 14 +- src-web/hooks/useUpdateEnvironment.ts | 13 +- src-web/hooks/useUpdateWorkspace.ts | 13 +- src-web/lib/keyValueStore.ts | 21 +- src-web/lib/models.ts | 3 + src-web/lib/theme/appearance.ts | 2 +- src-web/lib/theme/window.ts | 1 - src-web/lib/theme/yaakColor.ts | 17 + 106 files changed, 1086 insertions(+), 1219 deletions(-) create mode 100644 plugin-runtime-types/src/gen/GetHttpRequestByIdRequest.ts create mode 100644 plugin-runtime-types/src/gen/GetHttpRequestByIdResponse.ts create mode 100644 plugin-runtime-types/src/gen/SendHttpRequestRequest.ts create mode 100644 plugin-runtime-types/src/gen/SendHttpRequestResponse.ts create mode 100644 src-tauri/migrations/20240814013812_fix-env-model.sql rename src-tauri/{grpc => yaak_grpc}/Cargo.toml (97%) rename src-tauri/{grpc => yaak_grpc}/src/codec.rs (100%) rename src-tauri/{grpc => yaak_grpc}/src/json_schema.rs (100%) rename src-tauri/{grpc => yaak_grpc}/src/lib.rs (100%) rename src-tauri/{grpc => yaak_grpc}/src/manager.rs (100%) rename src-tauri/{grpc => yaak_grpc}/src/proto.rs (100%) rename src-tauri/{templates => yaak_templates}/Cargo.toml (76%) rename src-tauri/{templates => yaak_templates}/src/lib.rs (100%) rename src-tauri/{templates => yaak_templates}/src/parser.rs (100%) rename src-tauri/{templates => yaak_templates}/src/renderer.rs (100%) delete mode 100644 src-web/hooks/useActiveEnvironmentId.ts rename src-web/hooks/{useAtiveWorkspaceChangedToast.tsx => useActiveWorkspaceChangedToast.tsx} (94%) delete mode 100644 src-web/hooks/useActiveWorkspaceId.ts create mode 100644 src-web/hooks/useRecentCookieJars.ts diff --git a/plugin-runtime-types/src/gen/GetHttpRequestByIdRequest.ts b/plugin-runtime-types/src/gen/GetHttpRequestByIdRequest.ts new file mode 100644 index 00000000..fdf0a5b0 --- /dev/null +++ b/plugin-runtime-types/src/gen/GetHttpRequestByIdRequest.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 GetHttpRequestByIdRequest = { id: string, }; diff --git a/plugin-runtime-types/src/gen/GetHttpRequestByIdResponse.ts b/plugin-runtime-types/src/gen/GetHttpRequestByIdResponse.ts new file mode 100644 index 00000000..e0a8af6d --- /dev/null +++ b/plugin-runtime-types/src/gen/GetHttpRequestByIdResponse.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 GetHttpRequestByIdResponse = { httpRequest: HttpRequest | null, }; diff --git a/plugin-runtime-types/src/gen/InternalEventPayload.ts b/plugin-runtime-types/src/gen/InternalEventPayload.ts index 05b4a71c..68146544 100644 --- a/plugin-runtime-types/src/gen/InternalEventPayload.ts +++ b/plugin-runtime-types/src/gen/InternalEventPayload.ts @@ -6,7 +6,11 @@ import type { ExportHttpRequestRequest } from "./ExportHttpRequestRequest"; import type { ExportHttpRequestResponse } from "./ExportHttpRequestResponse"; import type { FilterRequest } from "./FilterRequest"; import type { FilterResponse } from "./FilterResponse"; +import type { GetHttpRequestByIdRequest } from "./GetHttpRequestByIdRequest"; +import type { GetHttpRequestByIdResponse } from "./GetHttpRequestByIdResponse"; import type { ImportRequest } from "./ImportRequest"; import type { ImportResponse } from "./ImportResponse"; +import type { SendHttpRequestRequest } from "./SendHttpRequestRequest"; +import type { SendHttpRequestResponse } from "./SendHttpRequestResponse"; -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": "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_by_id_request" } & GetHttpRequestByIdRequest | { "type": "get_http_request_by_id_response" } & GetHttpRequestByIdResponse | { "type": "empty_response" } & EmptyResponse; diff --git a/plugin-runtime-types/src/gen/SendHttpRequestRequest.ts b/plugin-runtime-types/src/gen/SendHttpRequestRequest.ts new file mode 100644 index 00000000..8979a592 --- /dev/null +++ b/plugin-runtime-types/src/gen/SendHttpRequestRequest.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 SendHttpRequestRequest = { httpRequest: HttpRequest, }; diff --git a/plugin-runtime-types/src/gen/SendHttpRequestResponse.ts b/plugin-runtime-types/src/gen/SendHttpRequestResponse.ts new file mode 100644 index 00000000..831483a9 --- /dev/null +++ b/plugin-runtime-types/src/gen/SendHttpRequestResponse.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 { HttpResponse } from "./HttpResponse"; + +export type SendHttpRequestResponse = { httpResponse: HttpResponse, }; diff --git a/plugin-runtime-types/src/index.ts b/plugin-runtime-types/src/index.ts index 1ee8377f..4cde94f7 100644 --- a/plugin-runtime-types/src/index.ts +++ b/plugin-runtime-types/src/index.ts @@ -31,5 +31,10 @@ export * from './gen/InternalEvent'; export * from './gen/InternalEventPayload'; export * from './gen/KeyValue'; export * from './gen/Model'; +export * from './gen/SendHttpRequestRequest'; +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 0bed6e71..cc1d10b5 100644 --- a/plugin-runtime-types/src/plugins/context.ts +++ b/plugin-runtime-types/src/plugins/context.ts @@ -1,12 +1,11 @@ -import { HttpRequest } from '../gen/HttpRequest'; -import { HttpResponse } from '../gen/HttpResponse'; +import { GetHttpRequestByIdRequest } from '../gen/GetHttpRequestByIdRequest'; +import { GetHttpRequestByIdResponse } from '../gen/GetHttpRequestByIdResponse'; +import { SendHttpRequestRequest } from '../gen/SendHttpRequestRequest'; +import { SendHttpRequestResponse } from '../gen/SendHttpRequestResponse'; export type YaakContext = { - metadata: { - getVersion(): Promise; - }; httpRequest: { - send(id: string): Promise; - getById(id: string): Promise; + send(args: SendHttpRequestRequest): Promise; + getById(args: GetHttpRequestByIdRequest): Promise; }; }; diff --git a/plugin-runtime-types/src/plugins/index.ts b/plugin-runtime-types/src/plugins/index.ts index db1da8e2..d59e1403 100644 --- a/plugin-runtime-types/src/plugins/index.ts +++ b/plugin-runtime-types/src/plugins/index.ts @@ -3,6 +3,7 @@ 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 diff --git a/plugin-runtime/src/gen/plugins/runtime.ts b/plugin-runtime/src/gen/plugins/runtime.ts index ba57cb5f..df82a7bb 100644 --- a/plugin-runtime/src/gen/plugins/runtime.ts +++ b/plugin-runtime/src/gen/plugins/runtime.ts @@ -10,369 +10,10 @@ import * as _m0 from "protobufjs/minimal"; export const protobufPackage = "yaak.plugins.runtime"; -export interface PluginInfo { - plugin: string; -} - -export interface HookResponse { - info: PluginInfo | undefined; - data: string; -} - -export interface HookImportRequest { - data: string; -} - -export interface HookResponseFilterRequest { - filter: string; - body: string; - contentType: string; -} - -export interface HookExportRequest { - request: string; -} - export interface EventStreamEvent { event: string; } -function createBasePluginInfo(): PluginInfo { - return { plugin: "" }; -} - -export const PluginInfo = { - encode(message: PluginInfo, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { - if (message.plugin !== "") { - writer.uint32(10).string(message.plugin); - } - return writer; - }, - - decode(input: _m0.Reader | Uint8Array, length?: number): PluginInfo { - const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); - let end = length === undefined ? reader.len : reader.pos + length; - const message = createBasePluginInfo(); - while (reader.pos < end) { - const tag = reader.uint32(); - switch (tag >>> 3) { - case 1: - if (tag !== 10) { - break; - } - - message.plugin = reader.string(); - continue; - } - if ((tag & 7) === 4 || tag === 0) { - break; - } - reader.skipType(tag & 7); - } - return message; - }, - - fromJSON(object: any): PluginInfo { - return { plugin: isSet(object.plugin) ? globalThis.String(object.plugin) : "" }; - }, - - toJSON(message: PluginInfo): unknown { - const obj: any = {}; - if (message.plugin !== "") { - obj.plugin = message.plugin; - } - return obj; - }, - - create(base?: DeepPartial): PluginInfo { - return PluginInfo.fromPartial(base ?? {}); - }, - fromPartial(object: DeepPartial): PluginInfo { - const message = createBasePluginInfo(); - message.plugin = object.plugin ?? ""; - return message; - }, -}; - -function createBaseHookResponse(): HookResponse { - return { info: undefined, data: "" }; -} - -export const HookResponse = { - encode(message: HookResponse, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { - if (message.info !== undefined) { - PluginInfo.encode(message.info, writer.uint32(10).fork()).ldelim(); - } - if (message.data !== "") { - writer.uint32(18).string(message.data); - } - return writer; - }, - - decode(input: _m0.Reader | Uint8Array, length?: number): HookResponse { - const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); - let end = length === undefined ? reader.len : reader.pos + length; - const message = createBaseHookResponse(); - while (reader.pos < end) { - const tag = reader.uint32(); - switch (tag >>> 3) { - case 1: - if (tag !== 10) { - break; - } - - message.info = PluginInfo.decode(reader, reader.uint32()); - continue; - case 2: - if (tag !== 18) { - break; - } - - message.data = reader.string(); - continue; - } - if ((tag & 7) === 4 || tag === 0) { - break; - } - reader.skipType(tag & 7); - } - return message; - }, - - fromJSON(object: any): HookResponse { - return { - info: isSet(object.info) ? PluginInfo.fromJSON(object.info) : undefined, - data: isSet(object.data) ? globalThis.String(object.data) : "", - }; - }, - - toJSON(message: HookResponse): unknown { - const obj: any = {}; - if (message.info !== undefined) { - obj.info = PluginInfo.toJSON(message.info); - } - if (message.data !== "") { - obj.data = message.data; - } - return obj; - }, - - create(base?: DeepPartial): HookResponse { - return HookResponse.fromPartial(base ?? {}); - }, - fromPartial(object: DeepPartial): HookResponse { - const message = createBaseHookResponse(); - message.info = (object.info !== undefined && object.info !== null) - ? PluginInfo.fromPartial(object.info) - : undefined; - message.data = object.data ?? ""; - return message; - }, -}; - -function createBaseHookImportRequest(): HookImportRequest { - return { data: "" }; -} - -export const HookImportRequest = { - encode(message: HookImportRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { - if (message.data !== "") { - writer.uint32(10).string(message.data); - } - return writer; - }, - - decode(input: _m0.Reader | Uint8Array, length?: number): HookImportRequest { - const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); - let end = length === undefined ? reader.len : reader.pos + length; - const message = createBaseHookImportRequest(); - while (reader.pos < end) { - const tag = reader.uint32(); - switch (tag >>> 3) { - case 1: - if (tag !== 10) { - break; - } - - message.data = reader.string(); - continue; - } - if ((tag & 7) === 4 || tag === 0) { - break; - } - reader.skipType(tag & 7); - } - return message; - }, - - fromJSON(object: any): HookImportRequest { - return { data: isSet(object.data) ? globalThis.String(object.data) : "" }; - }, - - toJSON(message: HookImportRequest): unknown { - const obj: any = {}; - if (message.data !== "") { - obj.data = message.data; - } - return obj; - }, - - create(base?: DeepPartial): HookImportRequest { - return HookImportRequest.fromPartial(base ?? {}); - }, - fromPartial(object: DeepPartial): HookImportRequest { - const message = createBaseHookImportRequest(); - message.data = object.data ?? ""; - return message; - }, -}; - -function createBaseHookResponseFilterRequest(): HookResponseFilterRequest { - return { filter: "", body: "", contentType: "" }; -} - -export const HookResponseFilterRequest = { - encode(message: HookResponseFilterRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { - if (message.filter !== "") { - writer.uint32(10).string(message.filter); - } - if (message.body !== "") { - writer.uint32(18).string(message.body); - } - if (message.contentType !== "") { - writer.uint32(26).string(message.contentType); - } - return writer; - }, - - decode(input: _m0.Reader | Uint8Array, length?: number): HookResponseFilterRequest { - const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); - let end = length === undefined ? reader.len : reader.pos + length; - const message = createBaseHookResponseFilterRequest(); - while (reader.pos < end) { - const tag = reader.uint32(); - switch (tag >>> 3) { - case 1: - if (tag !== 10) { - break; - } - - message.filter = reader.string(); - continue; - case 2: - if (tag !== 18) { - break; - } - - message.body = reader.string(); - continue; - case 3: - if (tag !== 26) { - break; - } - - message.contentType = reader.string(); - continue; - } - if ((tag & 7) === 4 || tag === 0) { - break; - } - reader.skipType(tag & 7); - } - return message; - }, - - fromJSON(object: any): HookResponseFilterRequest { - return { - filter: isSet(object.filter) ? globalThis.String(object.filter) : "", - body: isSet(object.body) ? globalThis.String(object.body) : "", - contentType: isSet(object.contentType) ? globalThis.String(object.contentType) : "", - }; - }, - - toJSON(message: HookResponseFilterRequest): unknown { - const obj: any = {}; - if (message.filter !== "") { - obj.filter = message.filter; - } - if (message.body !== "") { - obj.body = message.body; - } - if (message.contentType !== "") { - obj.contentType = message.contentType; - } - return obj; - }, - - create(base?: DeepPartial): HookResponseFilterRequest { - return HookResponseFilterRequest.fromPartial(base ?? {}); - }, - fromPartial(object: DeepPartial): HookResponseFilterRequest { - const message = createBaseHookResponseFilterRequest(); - message.filter = object.filter ?? ""; - message.body = object.body ?? ""; - message.contentType = object.contentType ?? ""; - return message; - }, -}; - -function createBaseHookExportRequest(): HookExportRequest { - return { request: "" }; -} - -export const HookExportRequest = { - encode(message: HookExportRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { - if (message.request !== "") { - writer.uint32(10).string(message.request); - } - return writer; - }, - - decode(input: _m0.Reader | Uint8Array, length?: number): HookExportRequest { - const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); - let end = length === undefined ? reader.len : reader.pos + length; - const message = createBaseHookExportRequest(); - while (reader.pos < end) { - const tag = reader.uint32(); - switch (tag >>> 3) { - case 1: - if (tag !== 10) { - break; - } - - message.request = reader.string(); - continue; - } - if ((tag & 7) === 4 || tag === 0) { - break; - } - reader.skipType(tag & 7); - } - return message; - }, - - fromJSON(object: any): HookExportRequest { - return { request: isSet(object.request) ? globalThis.String(object.request) : "" }; - }, - - toJSON(message: HookExportRequest): unknown { - const obj: any = {}; - if (message.request !== "") { - obj.request = message.request; - } - return obj; - }, - - create(base?: DeepPartial): HookExportRequest { - return HookExportRequest.fromPartial(base ?? {}); - }, - fromPartial(object: DeepPartial): HookExportRequest { - const message = createBaseHookExportRequest(); - message.request = object.request ?? ""; - return message; - }, -}; - function createBaseEventStreamEvent(): EventStreamEvent { return { event: "" }; } diff --git a/plugin-runtime/src/index.worker.ts b/plugin-runtime/src/index.worker.ts index a4a4670f..9d5d0a31 100644 --- a/plugin-runtime/src/index.worker.ts +++ b/plugin-runtime/src/index.worker.ts @@ -1,4 +1,11 @@ -import { ImportResponse, InternalEvent, InternalEventPayload } from '@yaakapp/api'; +import { + GetHttpRequestByIdResponse, + ImportResponse, + InternalEvent, + InternalEventPayload, + SendHttpRequestResponse, +} from '@yaakapp/api'; +import { YaakContext } from '@yaakapp/api/lib/plugins/context'; import interceptStdout from 'intercept-stdout'; import * as console from 'node:console'; import { readFileSync } from 'node:fs'; @@ -7,7 +14,7 @@ import * as util from 'node:util'; import { parentPort, workerData } from 'node:worker_threads'; new Promise(async (resolve, reject) => { - const { pluginDir /*, pluginRefId*/ } = workerData; + const { pluginDir, pluginRefId } = workerData; const pathPkg = path.join(pluginDir, 'package.json'); // NOTE: Use POSIX join because require() needs forward slash @@ -33,8 +40,64 @@ new Promise(async (resolve, reject) => { console.log('Plugin initialized', pkg.name, capabilities, Object.keys(mod)); + function buildEventToSend( + payload: InternalEventPayload, + replyId: string | null = null, + ): InternalEvent { + return { pluginRefId, id: genId(), replyId, payload }; + } + + function sendPayload(payload: InternalEventPayload, replyId: string | null = null): string { + const event = buildEventToSend(payload, replyId); + sendEvent(event); + return event.id; + } + + function sendEvent(event: InternalEvent) { + parentPort!.postMessage(event); + } + + async function sendAndWaitForReply>( + payload: InternalEventPayload, + ): Promise { + // 1. Build event to send + const eventToSend = buildEventToSend(payload, null); + + // 2. Spawn listener in background + const promise = new Promise(async (resolve) => { + const cb = (event: InternalEvent) => { + if (event.replyId === eventToSend.id) { + resolve(event.payload); // Not type-safe but oh well + parentPort!.off('message', cb); // Unlisten, now that we're done + } + }; + parentPort!.on('message', cb); + }); + + // 3. Send the event after we start listening (to prevent race) + sendEvent(eventToSend); + + // 4. Return the listener promise + return promise as unknown as Promise; + } + + const ctx: YaakContext = { + httpRequest: { + async getById({ id }) { + const payload = { type: 'get_http_request_by_id_request', id } as const; + const { httpRequest } = await sendAndWaitForReply(payload); + return httpRequest; + }, + async send({ httpRequest }) { + const payload = { type: 'send_http_request_request', httpRequest } as const; + const { httpResponse } = await sendAndWaitForReply(payload); + return httpResponse; + }, + }, + }; + // Message comes into the plugin to be processed - parentPort!.on('message', async ({ payload, pluginRefId, id: replyId }: InternalEvent) => { + parentPort!.on('message', async ({ payload, id: replyId }: InternalEvent) => { console.log(`Received ${payload.type}`); try { @@ -45,18 +108,18 @@ new Promise(async (resolve, reject) => { version: pkg.version, capabilities, }; - sendToServer({ id: genId(), pluginRefId, replyId, payload }); + sendPayload(payload, replyId); return; } if (payload.type === 'import_request' && typeof mod.pluginHookImport === 'function') { - const reply: ImportResponse | null = await mod.pluginHookImport({}, payload.content); + const reply: ImportResponse | null = await mod.pluginHookImport(ctx, payload.content); if (reply != null) { const replyPayload: InternalEventPayload = { type: 'import_response', resources: reply?.resources, }; - sendToServer({ id: genId(), pluginRefId, replyId, payload: replyPayload }); + sendPayload(replyPayload, replyId); return; } else { // Continue, to send back an empty reply @@ -67,25 +130,25 @@ new Promise(async (resolve, reject) => { payload.type === 'export_http_request_request' && typeof mod.pluginHookExport === 'function' ) { - const reply: string = await mod.pluginHookExport({}, payload.httpRequest); + const reply: string = await mod.pluginHookExport(ctx, payload.httpRequest); const replyPayload: InternalEventPayload = { type: 'export_http_request_response', content: reply, }; - sendToServer({ id: genId(), pluginRefId, replyId, payload: replyPayload }); + sendPayload(replyPayload, replyId); return; } if (payload.type === 'filter_request' && typeof mod.pluginHookResponseFilter === 'function') { - const reply: string = await mod.pluginHookResponseFilter( - {}, - { filter: payload.filter, body: payload.content }, - ); + const reply: string = await mod.pluginHookResponseFilter(ctx, { + filter: payload.filter, + body: payload.content, + }); const replyPayload: InternalEventPayload = { type: 'filter_response', content: reply, }; - sendToServer({ id: genId(), pluginRefId, replyId, payload: replyPayload }); + sendPayload(replyPayload, replyId); return; } } catch (err) { @@ -94,9 +157,7 @@ new Promise(async (resolve, reject) => { } // No matches, so send back an empty response so the caller doesn't block forever - const id = genId(); - console.log('Sending nothing back to', id, { replyId }); - sendToServer({ id, pluginRefId, replyId, payload: { type: 'empty_response' } }); + sendPayload({ type: 'empty_response' }, replyId); }); resolve(); @@ -104,10 +165,6 @@ new Promise(async (resolve, reject) => { console.log('failed to boot plugin', err); }); -function sendToServer(e: InternalEvent) { - parentPort!.postMessage(e); -} - function genId(len = 5): string { const alphabet = '01234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; let id = ''; diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 59c41e20..eed43487 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -2052,30 +2052,6 @@ dependencies = [ "system-deps", ] -[[package]] -name = "grpc" -version = "0.1.0" -dependencies = [ - "anyhow", - "dunce", - "hyper 0.14.30", - "hyper-rustls 0.24.2", - "log", - "md5", - "prost 0.12.6", - "prost-reflect", - "prost-types 0.12.6", - "serde", - "serde_json", - "tauri", - "tauri-plugin-shell", - "tokio", - "tokio-stream", - "tonic 0.10.2", - "tonic-reflection", - "uuid", -] - [[package]] name = "gtk" version = "0.18.1" @@ -6125,13 +6101,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "templates" -version = "0.1.0" -dependencies = [ - "log", -] - [[package]] name = "tendril" version = "0.4.3" @@ -7573,7 +7542,6 @@ dependencies = [ "chrono", "cocoa", "datetime", - "grpc", "hex_color", "http 1.1.0", "log", @@ -7597,13 +7565,38 @@ dependencies = [ "tauri-plugin-shell", "tauri-plugin-updater", "tauri-plugin-window-state", - "templates", "thiserror", "tokio", "tokio-stream", "uuid", + "yaak_grpc", "yaak_models", "yaak_plugin_runtime", + "yaak_templates", +] + +[[package]] +name = "yaak_grpc" +version = "0.1.0" +dependencies = [ + "anyhow", + "dunce", + "hyper 0.14.30", + "hyper-rustls 0.24.2", + "log", + "md5", + "prost 0.12.6", + "prost-reflect", + "prost-types 0.12.6", + "serde", + "serde_json", + "tauri", + "tauri-plugin-shell", + "tokio", + "tokio-stream", + "tonic 0.10.2", + "tonic-reflection", + "uuid", ] [[package]] @@ -7651,6 +7644,13 @@ dependencies = [ "yaak_models", ] +[[package]] +name = "yaak_templates" +version = "0.1.0" +dependencies = [ + "log", +] + [[package]] name = "zbus" version = "4.0.1" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 06aea631..706c0e02 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["grpc", "templates", "yaak_plugin_runtime", "yaak_models"] +members = ["yaak_grpc", "yaak_templates", "yaak_plugin_runtime", "yaak_models"] [package] @@ -26,8 +26,8 @@ cocoa = "0.25.0" openssl-sys = { version = "0.9", features = ["vendored"] } # For Ubuntu installation to work [dependencies] -grpc = { path = "./grpc" } -templates = { path = "./templates" } +yaak_grpc = { path = "yaak_grpc" } +yaak_templates = { path = "yaak_templates" } yaak_plugin_runtime = { path = "yaak_plugin_runtime" } anyhow = "1.0.86" base64 = "0.22.0" @@ -43,7 +43,7 @@ reqwest_cookie_store = "0.8.0" serde = { version = "1.0.198", features = ["derive"] } serde_json = { version = "1.0.116", features = ["raw_value"] } serde_yaml = "0.9.34" -tauri = { workspace = true } +tauri = { workspace = true, features = ["unstable"] } tauri-plugin-shell = { workspace = true } tauri-plugin-clipboard-manager = "2.0.0-rc.0" tauri-plugin-dialog = "2.0.0-rc.0" diff --git a/src-tauri/migrations/20240814013812_fix-env-model.sql b/src-tauri/migrations/20240814013812_fix-env-model.sql new file mode 100644 index 00000000..cacb6156 --- /dev/null +++ b/src-tauri/migrations/20240814013812_fix-env-model.sql @@ -0,0 +1,2 @@ +ALTER TABLE environments DROP COLUMN model; +ALTER TABLE environments ADD COLUMN model TEXT DEFAULT 'environment'; diff --git a/src-tauri/src/analytics.rs b/src-tauri/src/analytics.rs index 40db61fa..63ca5651 100644 --- a/src-tauri/src/analytics.rs +++ b/src-tauri/src/analytics.rs @@ -3,9 +3,11 @@ use std::fmt::Display; use log::{debug, info}; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; -use tauri::{AppHandle, Manager}; +use tauri::{Manager, Runtime, WebviewWindow}; -use yaak_models::queries::{generate_id, get_key_value_int, get_key_value_string, set_key_value_int, set_key_value_string}; +use yaak_models::queries::{ + generate_id, get_key_value_int, get_key_value_string, set_key_value_int, set_key_value_string, +}; use crate::is_dev; @@ -36,7 +38,7 @@ pub enum AnalyticsResource { impl AnalyticsResource { pub fn from_str(s: &str) -> serde_json::Result { - return serde_json::from_str(format!("\"{s}\"").as_str()); + serde_json::from_str(format!("\"{s}\"").as_str()) } } @@ -74,7 +76,7 @@ pub enum AnalyticsAction { impl AnalyticsAction { pub fn from_str(s: &str) -> serde_json::Result { - return serde_json::from_str(format!("\"{s}\"").as_str()); + serde_json::from_str(format!("\"{s}\"").as_str()) } } @@ -96,19 +98,18 @@ pub struct LaunchEventInfo { pub num_launches: i32, } -pub async fn track_launch_event(app: &AppHandle) -> LaunchEventInfo { +pub async fn track_launch_event(w: &WebviewWindow) -> LaunchEventInfo { let last_tracked_version_key = "last_tracked_version"; let mut info = LaunchEventInfo::default(); - info.num_launches = get_num_launches(app).await + 1; - info.previous_version = - get_key_value_string(app, NAMESPACE, last_tracked_version_key, "").await; - info.current_version = app.package_info().version.to_string(); + info.num_launches = get_num_launches(w).await + 1; + info.previous_version = get_key_value_string(w, NAMESPACE, last_tracked_version_key, "").await; + info.current_version = w.package_info().version.to_string(); if info.previous_version.is_empty() { track_event( - app, + w, AnalyticsResource::App, AnalyticsAction::LaunchFirst, None, @@ -118,7 +119,7 @@ pub async fn track_launch_event(app: &AppHandle) -> LaunchEventInfo { info.launched_after_update = info.current_version != info.previous_version; if info.launched_after_update { track_event( - app, + w, AnalyticsResource::App, AnalyticsAction::LaunchUpdate, Some(json!({ NUM_LAUNCHES_KEY: info.num_launches })), @@ -129,7 +130,7 @@ pub async fn track_launch_event(app: &AppHandle) -> LaunchEventInfo { // Track a launch event in all cases track_event( - app, + w, AnalyticsResource::App, AnalyticsAction::Launch, Some(json!({ NUM_LAUNCHES_KEY: info.num_launches })), @@ -139,27 +140,27 @@ pub async fn track_launch_event(app: &AppHandle) -> LaunchEventInfo { // Update key values set_key_value_string( - app, + w, NAMESPACE, last_tracked_version_key, info.current_version.as_str(), ) .await; - set_key_value_int(app, NAMESPACE, NUM_LAUNCHES_KEY, info.num_launches).await; + set_key_value_int(w, NAMESPACE, NUM_LAUNCHES_KEY, info.num_launches).await; info } -pub async fn track_event( - app_handle: &AppHandle, +pub async fn track_event( + w: &WebviewWindow, resource: AnalyticsResource, action: AnalyticsAction, attributes: Option, ) { - let id = get_id(app_handle).await; + let id = get_id(w).await; let event = format!("{}.{}", resource, action); let attributes_json = attributes.unwrap_or("{}".to_string().into()).to_string(); - let info = app_handle.package_info(); + let info = w.app_handle().package_info(); let tz = datetime::sys_timezone().unwrap_or("unknown".to_string()); let site = match is_dev() { true => "site_TkHWjoXwZPq3HfhERb", @@ -177,7 +178,7 @@ pub async fn track_event( ("v", info.version.clone().to_string()), ("os", get_os().to_string()), ("tz", tz), - ("xy", get_window_size(app_handle)), + ("xy", get_window_size(w)), ]; let req = reqwest::Client::builder() .build() @@ -208,13 +209,8 @@ fn get_os() -> &'static str { } } -fn get_window_size(app_handle: &AppHandle) -> String { - let window = match app_handle.webview_windows().into_values().next() { - Some(w) => w, - None => return "unknown".to_string(), - }; - - let current_monitor = match window.current_monitor() { +fn get_window_size(w: &WebviewWindow) -> String { + let current_monitor = match w.current_monitor() { Ok(Some(m)) => m, _ => return "unknown".to_string(), }; @@ -231,17 +227,17 @@ fn get_window_size(app_handle: &AppHandle) -> String { ) } -async fn get_id(app_handle: &AppHandle) -> String { - let id = get_key_value_string(app_handle, "analytics", "id", "").await; +async fn get_id(w: &WebviewWindow) -> String { + let id = get_key_value_string(w, "analytics", "id", "").await; if id.is_empty() { let new_id = generate_id(); - set_key_value_string(app_handle, "analytics", "id", new_id.as_str()).await; + set_key_value_string(w, "analytics", "id", new_id.as_str()).await; new_id } else { id } } -pub async fn get_num_launches(app: &AppHandle) -> i32 { - get_key_value_int(app, NAMESPACE, NUM_LAUNCHES_KEY, 0).await +pub async fn get_num_launches(w: &WebviewWindow) -> i32 { + get_key_value_int(w, NAMESPACE, NUM_LAUNCHES_KEY, 0).await } diff --git a/src-tauri/src/grpc.rs b/src-tauri/src/grpc.rs index 28996b8e..638f9c1f 100644 --- a/src-tauri/src/grpc.rs +++ b/src-tauri/src/grpc.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use KeyAndValueRef::{Ascii, Binary}; -use grpc::{KeyAndValueRef, MetadataMap}; +use yaak_grpc::{KeyAndValueRef, MetadataMap}; pub fn metadata_to_map(metadata: MetadataMap) -> HashMap { let mut entries = HashMap::new(); diff --git a/src-tauri/src/http_request.rs b/src-tauri/src/http_request.rs index 0706d259..8210409f 100644 --- a/src-tauri/src/http_request.rs +++ b/src-tauri/src/http_request.rs @@ -11,24 +11,25 @@ use crate::{render, response_err}; use base64::Engine; use http::header::{ACCEPT, USER_AGENT}; use http::{HeaderMap, HeaderName, HeaderValue}; -use log::{error, info, warn}; +use log::{error, warn}; use mime_guess::Mime; use reqwest::redirect::Policy; use reqwest::Method; use reqwest::{multipart, Url}; -use tauri::{Manager, WebviewWindow}; +use tauri::{Manager, Runtime, WebviewWindow}; use tokio::sync::oneshot; use tokio::sync::watch::Receiver; -use yaak_models::models::{Cookie, CookieJar, Environment, HttpRequest, HttpResponse, HttpResponseHeader}; +use yaak_models::models::{ + Cookie, CookieJar, Environment, HttpRequest, HttpResponse, HttpResponseHeader, +}; use yaak_models::queries::{get_workspace, update_response_if_id, upsert_cookie_jar}; -pub async fn send_http_request( - window: &WebviewWindow, +pub async fn send_http_request( + window: &WebviewWindow, request: HttpRequest, response: &HttpResponse, environment: Option, cookie_jar: Option, - download_path: Option, cancel_rx: &mut Receiver, ) -> Result { let environment_ref = environment.as_ref(); @@ -442,16 +443,6 @@ pub async fn send_http_request( .await .expect("Failed to update response"); - // Copy response to the download path, if specified - match (download_path, response.body_path.clone()) { - (Some(dl_path), Some(body_path)) => { - info!("Downloading response body to {}", dl_path.display()); - fs::copy(body_path, dl_path) - .expect("Failed to copy file for response download"); - } - _ => {} - }; - // Add cookie store if specified if let Some((cookie_store, mut cookie_jar)) = maybe_cookie_manager { // let cookies = response_headers.get_all(SET_COOKIE).iter().map(|h| { @@ -466,7 +457,8 @@ pub async fn send_http_request( .unwrap() .iter_any() .map(|c| { - let json_cookie = serde_json::to_value(&c).expect("Failed to serialize cookie"); + let json_cookie = + serde_json::to_value(&c).expect("Failed to serialize cookie"); serde_json::from_value(json_cookie).expect("Failed to deserialize cookie") }) .collect::>(); diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 152e19d5..acb8a713 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -16,17 +16,17 @@ use fern::colors::ColoredLevelConfig; use log::{debug, error, info, warn}; use rand::random; use serde_json::{json, Value}; -use tauri::Listener; #[cfg(target_os = "macos")] use tauri::TitleBarStyle; use tauri::{AppHandle, Emitter, LogicalSize, RunEvent, State, WebviewUrl, WebviewWindow}; +use tauri::{Listener, Runtime}; use tauri::{Manager, WindowEvent}; use tauri_plugin_log::{fern, Target, TargetKind}; use tauri_plugin_shell::ShellExt; -use tokio::sync::Mutex; +use tokio::sync::{watch, Mutex}; -use ::grpc::manager::{DynamicMessage, GrpcHandle}; -use ::grpc::{deserialize_message, serialize_message, Code, ServiceDefinition}; +use yaak_grpc::manager::{DynamicMessage, GrpcHandle}; +use yaak_grpc::{deserialize_message, serialize_message, Code, ServiceDefinition}; use yaak_plugin_runtime::manager::PluginManager; use crate::analytics::{AnalyticsAction, AnalyticsResource}; @@ -42,7 +42,7 @@ use yaak_models::models::{ GrpcRequest, HttpRequest, HttpResponse, KeyValue, ModelType, Settings, Workspace, }; use yaak_models::queries::{ - cancel_pending_grpc_connections, cancel_pending_responses, create_http_response, + cancel_pending_grpc_connections, cancel_pending_responses, create_default_http_response, delete_all_grpc_connections, delete_all_http_responses, delete_cookie_jar, delete_environment, delete_folder, delete_grpc_connection, delete_grpc_request, delete_http_request, delete_http_response, delete_workspace, duplicate_grpc_request, duplicate_http_request, @@ -54,7 +54,10 @@ use yaak_models::queries::{ upsert_cookie_jar, upsert_environment, upsert_folder, upsert_grpc_connection, upsert_grpc_event, upsert_grpc_request, upsert_http_request, upsert_workspace, }; -use yaak_plugin_runtime::events::FilterResponse; +use yaak_plugin_runtime::events::{ + FilterResponse, GetHttpRequestByIdResponse, InternalEvent, InternalEventPayload, + SendHttpRequestResponse, +}; mod analytics; mod export_resources; @@ -96,11 +99,15 @@ async fn cmd_metadata(app_handle: AppHandle) -> Result { #[tauri::command] async fn cmd_dismiss_notification( - app: AppHandle, + window: WebviewWindow, notification_id: &str, yaak_notifier: State<'_, Mutex>, ) -> Result<(), String> { - yaak_notifier.lock().await.seen(&app, notification_id).await + yaak_notifier + .lock() + .await + .seen(&window, notification_id) + .await } #[tauri::command] @@ -135,17 +142,21 @@ async fn cmd_grpc_go( request_id: &str, environment_id: Option<&str>, proto_files: Vec, - w: WebviewWindow, + window: WebviewWindow, grpc_handle: State<'_, Mutex>, ) -> Result { - let req = get_grpc_request(&w, request_id) + let req = get_grpc_request(&window, request_id) .await .map_err(|e| e.to_string())?; let environment = match environment_id { - Some(id) => Some(get_environment(&w, id).await.map_err(|e| e.to_string())?), + Some(id) => Some( + get_environment(&window, id) + .await + .map_err(|e| e.to_string())?, + ), None => None, }; - let workspace = get_workspace(&w, &req.workspace_id) + let workspace = get_workspace(&window, &req.workspace_id) .await .map_err(|e| e.to_string())?; let mut metadata = HashMap::new(); @@ -199,7 +210,7 @@ async fn cmd_grpc_go( let conn = { let req = req.clone(); upsert_grpc_connection( - &w, + &window, &GrpcConnection { workspace_id: req.workspace_id, request_id: req.id, @@ -256,7 +267,7 @@ async fn cmd_grpc_go( Ok(c) => c, Err(err) => { upsert_grpc_connection( - &w, + &window, &GrpcConnection { elapsed: start.elapsed().as_millis() as i32, error: Some(err.clone()), @@ -282,7 +293,7 @@ async fn cmd_grpc_go( let cb = { let cancelled_rx = cancelled_rx.clone(); - let w = w.clone(); + let w = window.clone(); let base_msg = base_msg.clone(); let method_desc = method_desc.clone(); let vars = vars.clone(); @@ -355,10 +366,10 @@ async fn cmd_grpc_go( } } }; - let event_handler = w.listen_any(format!("grpc_client_msg_{}", conn.id).as_str(), cb); + let event_handler = window.listen_any(format!("grpc_client_msg_{}", conn.id).as_str(), cb); let grpc_listen = { - let w = w.clone(); + let w = window.clone(); let base_event = base_msg.clone(); let req = req.clone(); let vars = vars.clone(); @@ -603,7 +614,7 @@ async fn cmd_grpc_go( { let conn_id = conn_id.clone(); tauri::async_runtime::spawn(async move { - let w = w.clone(); + let w = window.clone(); tokio::select! { _ = grpc_listen => { let events = list_grpc_events(&w, &conn_id) @@ -691,7 +702,6 @@ async fn cmd_send_ephemeral_request( &response, environment, cookie_jar, - None, &mut cancel_rx, ) .await @@ -701,7 +711,7 @@ async fn cmd_send_ephemeral_request( async fn cmd_filter_response( w: WebviewWindow, response_id: &str, - plugin_manager: State<'_, Mutex>, + plugin_manager: State<'_, PluginManager>, filter: &str, ) -> Result { let response = get_http_response(&w, response_id) @@ -724,8 +734,6 @@ async fn cmd_filter_response( // TODO: Have plugins register their own content type (regex?) plugin_manager - .lock() - .await .run_filter(filter, &body, &content_type) .await .map_err(|e| e.to_string()) @@ -734,15 +742,13 @@ async fn cmd_filter_response( #[tauri::command] async fn cmd_import_data( w: WebviewWindow, - plugin_manager: State<'_, Mutex>, + plugin_manager: State<'_, PluginManager>, file_path: &str, ) -> Result { let file = read_to_string(file_path).unwrap_or_else(|_| panic!("Unable to read file {}", file_path)); let file_contents = file.as_str(); let (import_result, plugin_name) = plugin_manager - .lock() - .await .run_import(file_contents) .await .map_err(|e| e.to_string())?; @@ -853,7 +859,7 @@ async fn cmd_import_data( ); analytics::track_event( - &w.app_handle(), + &w, AnalyticsResource::App, AnalyticsAction::Import, Some(json!({ "plugin": plugin_name })), @@ -867,7 +873,7 @@ async fn cmd_import_data( async fn cmd_request_to_curl( app: AppHandle, request_id: &str, - plugin_manager: State<'_, Mutex>, + plugin_manager: State<'_, PluginManager>, environment_id: Option<&str>, ) -> Result { let request = get_http_request(&app, request_id) @@ -883,8 +889,6 @@ async fn cmd_request_to_curl( let rendered = render_request(&request, &workspace, environment.as_ref()); let import_response = plugin_manager - .lock() - .await .run_export_curl(&rendered) .await .map_err(|e| e.to_string())?; @@ -894,19 +898,19 @@ async fn cmd_request_to_curl( #[tauri::command] async fn cmd_curl_to_request( command: &str, - plugin_manager: State<'_, Mutex>, + plugin_manager: State<'_, PluginManager>, workspace_id: &str, w: WebviewWindow, ) -> Result { - let (import_result, plugin_name) = plugin_manager - .lock() - .await - .run_import(command) - .await - .map_err(|e| e.to_string())?; + let (import_result, plugin_name) = { + plugin_manager + .run_import(command) + .await + .map_err(|e| e.to_string())? + }; analytics::track_event( - &w.app_handle(), + &w, AnalyticsResource::App, AnalyticsAction::Import, Some(json!({ "plugin": plugin_name })), @@ -947,7 +951,7 @@ async fn cmd_export_data( f.sync_all().expect("Failed to sync"); analytics::track_event( - &window.app_handle(), + &window, AnalyticsResource::App, AnalyticsAction::Export, None, @@ -984,7 +988,6 @@ async fn cmd_send_http_request( window: WebviewWindow, environment_id: Option<&str>, cookie_jar_id: Option<&str>, - download_dir: Option<&str>, // NOTE: We receive the entire request because to account for the race // condition where the user may have just edited a field before sending // that has not yet been saved in the DB. @@ -1010,28 +1013,9 @@ async fn cmd_send_http_request( None => None, }; - let response = create_http_response( - &window, - &request.id, - 0, - 0, - "", - 0, - None, - None, - None, - vec![], - None, - None, - ) - .await - .expect("Failed to create response"); - - let download_path = if let Some(p) = download_dir { - Some(std::path::Path::new(p).to_path_buf()) - } else { - None - }; + let response = create_default_http_response(&window, &request.id) + .await + .map_err(|e| e.to_string())?; let (cancel_tx, mut cancel_rx) = tokio::sync::watch::channel(false); window.listen_any( @@ -1047,16 +1031,15 @@ async fn cmd_send_http_request( &response, environment, cookie_jar, - download_path, &mut cancel_rx, ) .await } -async fn response_err( +async fn response_err( response: &HttpResponse, error: String, - w: &WebviewWindow, + w: &WebviewWindow, ) -> Result { warn!("Failed to send request: {}", error); let mut response = response.clone(); @@ -1080,7 +1063,7 @@ async fn cmd_track_event( AnalyticsAction::from_str(action), ) { (Ok(resource), Ok(action)) => { - analytics::track_event(&window.app_handle(), resource, action, attributes).await; + analytics::track_event(&window, resource, action, attributes).await; } (r, a) => { error!( @@ -1635,9 +1618,8 @@ pub fn run() { let grpc_handle = GrpcHandle::new(&app.app_handle()); app.manage(Mutex::new(grpc_handle)); - // Add plugin manager - let grpc_handle = GrpcHandle::new(&app.app_handle()); - app.manage(Mutex::new(grpc_handle)); + let app_handle = app.app_handle().clone(); + monitor_plugin_events(&app_handle); Ok(()) }) @@ -1715,10 +1697,9 @@ pub fn run() { .run(|app_handle, event| { match event { RunEvent::Ready => { - create_window(app_handle, "/"); - let h = app_handle.clone(); + let w = create_window(app_handle, "/"); tauri::async_runtime::spawn(async move { - let info = analytics::track_launch_event(&h).await; + let info = analytics::track_launch_event(&w).await; debug!("Launched Yaak {:?}", info); }); @@ -1743,10 +1724,12 @@ pub fn run() { let h = app_handle.clone(); tauri::async_runtime::spawn(async move { + let windows = h.webview_windows(); + let w = windows.values().next().unwrap(); tokio::time::sleep(Duration::from_millis(4000)).await; - let val: State<'_, Mutex> = h.state(); + let val: State<'_, Mutex> = w.state(); let mut n = val.lock().await; - if let Err(e) = n.check(&h).await { + if let Err(e) = n.check(&w).await { warn!("Failed to check for notifications {}", e) } }); @@ -1905,3 +1888,86 @@ fn safe_uri(endpoint: &str) -> String { format!("http://{}", endpoint) } } + +fn monitor_plugin_events(app_handle: &AppHandle) { + let app_handle = app_handle.clone(); + tauri::async_runtime::spawn(async move { + let plugin_manager: State<'_, PluginManager> = app_handle.state(); + let (_rx_id, mut rx) = plugin_manager.subscribe().await; + + let app_handle = app_handle.clone(); + while let Some(event) = rx.recv().await { + let payload = match handle_plugin_event(&app_handle, &event).await { + Some(e) => e, + None => continue, + }; + if let Err(e) = plugin_manager.reply(&event, &payload).await { + warn!("Failed to reply to plugin manager: {}", e) + } + } + }); +} + +async fn handle_plugin_event( + app_handle: &AppHandle, + event: &InternalEvent, +) -> Option { + 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, + }) + } + InternalEventPayload::SendHttpRequestRequest(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 url = w.url().unwrap(); + let mut query_pairs = url.query_pairs(); + + let cookie_jar_id = query_pairs + .find(|(k, _v)| k == "cookie_jar_id") + .map(|(_k, v)| v.to_string()); + let cookie_jar = match cookie_jar_id { + None => None, + Some(id) => get_cookie_jar(w, id.as_str()).await.ok(), + }; + + 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 resp = create_default_http_response(w, req.http_request.id.as_str()) + .await + .unwrap(); + + let result = send_http_request( + &w, + req.http_request, + &resp, + environment, + cookie_jar, + &mut watch::channel(false).1, // No-op cancel channel + ) + .await; + + let http_response = match result { + Ok(r) => r, + Err(_e) => return None, + }; + + InternalEventPayload::SendHttpRequestResponse(SendHttpRequestResponse { http_response }) + } + _ => return None, + }; + + Some(event) +} diff --git a/src-tauri/src/notifications.rs b/src-tauri/src/notifications.rs index ec2bb9de..25ed8e9b 100644 --- a/src-tauri/src/notifications.rs +++ b/src-tauri/src/notifications.rs @@ -5,7 +5,7 @@ use chrono::{DateTime, Duration, Utc}; use log::debug; use reqwest::Method; use serde::{Deserialize, Serialize}; -use tauri::{AppHandle, Emitter}; +use tauri::{Emitter, Manager, Runtime, WebviewWindow}; use yaak_models::queries::{get_key_value_raw, set_key_value_raw}; // Check for updates every hour @@ -42,16 +42,16 @@ impl YaakNotifier { } } - pub async fn seen(&mut self, app: &AppHandle, id: &str) -> Result<(), String> { - let mut seen = get_kv(app).await?; + pub async fn seen(&mut self, w: &WebviewWindow, id: &str) -> Result<(), String> { + let mut seen = get_kv(w).await?; seen.push(id.to_string()); debug!("Marked notification as seen {}", id); let seen_json = serde_json::to_string(&seen).map_err(|e| e.to_string())?; - set_key_value_raw(app, KV_NAMESPACE, KV_KEY, seen_json.as_str()).await; + set_key_value_raw(w, KV_NAMESPACE, KV_KEY, seen_json.as_str()).await; Ok(()) } - pub async fn check(&mut self, app: &AppHandle) -> Result<(), String> { + pub async fn check(&mut self, w: &WebviewWindow) -> Result<(), String> { let ignore_check = self.last_check.elapsed().unwrap().as_secs() < MAX_UPDATE_CHECK_SECONDS; if ignore_check { @@ -60,8 +60,8 @@ impl YaakNotifier { self.last_check = SystemTime::now(); - let num_launches = get_num_launches(app).await; - let info = app.package_info().clone(); + let num_launches = get_num_launches(w).await; + let info = w.app_handle().package_info().clone(); let req = reqwest::Client::default() .request(Method::GET, "https://notify.yaak.app/notifications") .query(&[ @@ -80,21 +80,21 @@ impl YaakNotifier { .map_err(|e| e.to_string())?; let age = notification.timestamp.signed_duration_since(Utc::now()); - let seen = get_kv(app).await?; + let seen = get_kv(w).await?; if seen.contains(¬ification.id) || (age > Duration::days(2)) { debug!("Already seen notification {}", notification.id); return Ok(()); } debug!("Got notification {:?}", notification); - let _ = app.emit("notification", notification.clone()); + let _ = w.emit("notification", notification.clone()); Ok(()) } } -async fn get_kv(app: &AppHandle) -> Result, String> { - match get_key_value_raw(app, "notifications", "seen").await { +async fn get_kv(w: &WebviewWindow) -> Result, String> { + match get_key_value_raw(w, "notifications", "seen").await { None => Ok(Vec::new()), Some(v) => serde_json::from_str(&v.value).map_err(|e| e.to_string()), } diff --git a/src-tauri/src/render.rs b/src-tauri/src/render.rs index eb4fed8b..e432c231 100644 --- a/src-tauri/src/render.rs +++ b/src-tauri/src/render.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use serde_json::Value; use crate::template_fns::timestamp; -use templates::parse_and_render; +use yaak_templates::parse_and_render; use yaak_models::models::{ Environment, EnvironmentVariable, HttpRequest, HttpRequestHeader, HttpUrlParameter, Workspace, }; diff --git a/src-tauri/grpc/Cargo.toml b/src-tauri/yaak_grpc/Cargo.toml similarity index 97% rename from src-tauri/grpc/Cargo.toml rename to src-tauri/yaak_grpc/Cargo.toml index 2df64654..a8f2ae39 100644 --- a/src-tauri/grpc/Cargo.toml +++ b/src-tauri/yaak_grpc/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "grpc" +name = "yaak_grpc" version = "0.1.0" edition = "2021" diff --git a/src-tauri/grpc/src/codec.rs b/src-tauri/yaak_grpc/src/codec.rs similarity index 100% rename from src-tauri/grpc/src/codec.rs rename to src-tauri/yaak_grpc/src/codec.rs diff --git a/src-tauri/grpc/src/json_schema.rs b/src-tauri/yaak_grpc/src/json_schema.rs similarity index 100% rename from src-tauri/grpc/src/json_schema.rs rename to src-tauri/yaak_grpc/src/json_schema.rs diff --git a/src-tauri/grpc/src/lib.rs b/src-tauri/yaak_grpc/src/lib.rs similarity index 100% rename from src-tauri/grpc/src/lib.rs rename to src-tauri/yaak_grpc/src/lib.rs diff --git a/src-tauri/grpc/src/manager.rs b/src-tauri/yaak_grpc/src/manager.rs similarity index 100% rename from src-tauri/grpc/src/manager.rs rename to src-tauri/yaak_grpc/src/manager.rs diff --git a/src-tauri/grpc/src/proto.rs b/src-tauri/yaak_grpc/src/proto.rs similarity index 100% rename from src-tauri/grpc/src/proto.rs rename to src-tauri/yaak_grpc/src/proto.rs diff --git a/src-tauri/yaak_models/src/queries.rs b/src-tauri/yaak_models/src/queries.rs index 3a6915ec..ff628bf5 100644 --- a/src-tauri/yaak_models/src/queries.rs +++ b/src-tauri/yaak_models/src/queries.rs @@ -15,10 +15,10 @@ use sea_query::Keyword::CurrentTimestamp; use sea_query::{Cond, Expr, OnConflict, Order, Query, SqliteQueryBuilder}; use sea_query_rusqlite::RusqliteBinder; use serde::Serialize; -use tauri::{AppHandle, Emitter, Manager, WebviewWindow, Wry}; +use tauri::{AppHandle, Emitter, Manager, Runtime, WebviewWindow}; -pub async fn set_key_value_string( - mgr: &impl Manager, +pub async fn set_key_value_string( + mgr: &WebviewWindow, namespace: &str, key: &str, value: &str, @@ -27,8 +27,8 @@ pub async fn set_key_value_string( set_key_value_raw(mgr, namespace, key, &encoded.unwrap()).await } -pub async fn set_key_value_int( - mgr: &impl Manager, +pub async fn set_key_value_int( + mgr: &WebviewWindow, namespace: &str, key: &str, value: i32, @@ -37,8 +37,8 @@ pub async fn set_key_value_int( set_key_value_raw(mgr, namespace, key, &encoded.unwrap()).await } -pub async fn get_key_value_string( - mgr: &impl Manager, +pub async fn get_key_value_string( + mgr: &impl Manager, namespace: &str, key: &str, default: &str, @@ -58,8 +58,8 @@ pub async fn get_key_value_string( } } -pub async fn get_key_value_int( - mgr: &impl Manager, +pub async fn get_key_value_int( + mgr: &impl Manager, namespace: &str, key: &str, default: i32, @@ -79,15 +79,15 @@ pub async fn get_key_value_int( } } -pub async fn set_key_value_raw( - mgr: &impl Manager, +pub async fn set_key_value_raw( + w: &WebviewWindow, namespace: &str, key: &str, value: &str, ) -> (KeyValue, bool) { - let existing = get_key_value_raw(mgr, namespace, key).await; + let existing = get_key_value_raw(w, namespace, key).await; - let dbm = &*mgr.state::(); + let dbm = &*w.state::(); let db = dbm.0.lock().await.get().unwrap(); let (sql, params) = Query::insert() .into_table(KeyValueIden::Table) @@ -119,11 +119,11 @@ pub async fn set_key_value_raw( let kv = stmt .query_row(&*params.as_params(), |row| row.try_into()) .expect("Failed to upsert KeyValue"); - (kv, existing.is_none()) + (emit_upserted_model(w, kv), existing.is_none()) } -pub async fn get_key_value_raw( - mgr: &impl Manager, +pub async fn get_key_value_raw( + mgr: &impl Manager, namespace: &str, key: &str, ) -> Option { @@ -143,7 +143,7 @@ pub async fn get_key_value_raw( .ok() } -pub async fn list_workspaces(mgr: &impl Manager) -> Result> { +pub async fn list_workspaces(mgr: &impl Manager) -> Result> { let dbm = &*mgr.state::(); let db = dbm.0.lock().await.get().unwrap(); let (sql, params) = Query::select() @@ -155,7 +155,7 @@ pub async fn list_workspaces(mgr: &impl Manager) -> Result> Ok(items.map(|v| v.unwrap()).collect()) } -pub async fn get_workspace(mgr: &impl Manager, id: &str) -> Result { +pub async fn get_workspace(mgr: &impl Manager, id: &str) -> Result { let dbm = &*mgr.state::(); let db = dbm.0.lock().await.get().unwrap(); let (sql, params) = Query::select() @@ -167,7 +167,10 @@ pub async fn get_workspace(mgr: &impl Manager, id: &str) -> Result Result { +pub async fn upsert_workspace( + window: &WebviewWindow, + workspace: Workspace, +) -> Result { let id = match workspace.id.as_str() { "" => generate_model_id(ModelType::TypeWorkspace), _ => workspace.id.to_string(), @@ -222,7 +225,10 @@ pub async fn upsert_workspace(window: &WebviewWindow, workspace: Workspace) -> R Ok(emit_upserted_model(window, m)) } -pub async fn delete_workspace(window: &WebviewWindow, id: &str) -> Result { +pub async fn delete_workspace( + window: &WebviewWindow, + id: &str, +) -> Result { let workspace = get_workspace(window, id).await?; let dbm = &*window.app_handle().state::(); @@ -241,7 +247,7 @@ pub async fn delete_workspace(window: &WebviewWindow, id: &str) -> Result, id: &str) -> Result { +pub async fn get_cookie_jar(mgr: &impl Manager, id: &str) -> Result { let dbm = &*mgr.state::(); let db = dbm.0.lock().await.get().unwrap(); @@ -254,8 +260,8 @@ pub async fn get_cookie_jar(mgr: &impl Manager, id: &str) -> Result, +pub async fn list_cookie_jars( + mgr: &impl Manager, workspace_id: &str, ) -> Result> { let dbm = &*mgr.state::(); @@ -270,7 +276,10 @@ pub async fn list_cookie_jars( Ok(items.map(|v| v.unwrap()).collect()) } -pub async fn delete_cookie_jar(window: &WebviewWindow, id: &str) -> Result { +pub async fn delete_cookie_jar( + window: &WebviewWindow, + id: &str, +) -> Result { let cookie_jar = get_cookie_jar(window, id).await?; let dbm = &*window.app_handle().state::(); let db = dbm.0.lock().await.get().unwrap(); @@ -284,13 +293,19 @@ pub async fn delete_cookie_jar(window: &WebviewWindow, id: &str) -> Result Result { +pub async fn duplicate_grpc_request( + window: &WebviewWindow, + id: &str, +) -> Result { let mut request = get_grpc_request(window, id).await?.clone(); request.id = "".to_string(); upsert_grpc_request(window, &request).await } -pub async fn delete_grpc_request(window: &WebviewWindow, id: &str) -> Result { +pub async fn delete_grpc_request( + window: &WebviewWindow, + id: &str, +) -> Result { let req = get_grpc_request(window, id).await?; let dbm = &*window.app_handle().state::(); @@ -304,8 +319,8 @@ pub async fn delete_grpc_request(window: &WebviewWindow, id: &str) -> Result( + window: &WebviewWindow, request: &GrpcRequest, ) -> Result { let id = match request.id.as_str() { @@ -346,7 +361,11 @@ pub async fn upsert_grpc_request( request.service.as_ref().map(|s| s.as_str()).into(), request.method.as_ref().map(|s| s.as_str()).into(), request.message.as_str().into(), - request .authentication_type .as_ref() .map(|s| s.as_str()) .into(), + request + .authentication_type + .as_ref() + .map(|s| s.as_str()) + .into(), serde_json::to_string(&request.authentication)?.into(), serde_json::to_string(&request.metadata)?.into(), ]) @@ -376,7 +395,7 @@ pub async fn upsert_grpc_request( Ok(emit_upserted_model(window, m)) } -pub async fn get_grpc_request(mgr: &impl Manager, id: &str) -> Result { +pub async fn get_grpc_request(mgr: &impl Manager, id: &str) -> Result { let dbm = &*mgr.state::(); let db = dbm.0.lock().await.get().unwrap(); @@ -389,8 +408,8 @@ pub async fn get_grpc_request(mgr: &impl Manager, id: &str) -> Result, +pub async fn list_grpc_requests( + mgr: &impl Manager, workspace_id: &str, ) -> Result> { let dbm = &*mgr.state::(); @@ -405,8 +424,8 @@ pub async fn list_grpc_requests( Ok(items.map(|v| v.unwrap()).collect()) } -pub async fn upsert_grpc_connection( - window: &WebviewWindow, +pub async fn upsert_grpc_connection( + window: &WebviewWindow, connection: &GrpcConnection, ) -> Result { let id = match connection.id.as_str() { @@ -467,7 +486,10 @@ pub async fn upsert_grpc_connection( Ok(emit_upserted_model(window, m)) } -pub async fn get_grpc_connection(mgr: &impl Manager, id: &str) -> Result { +pub async fn get_grpc_connection( + mgr: &impl Manager, + id: &str, +) -> Result { let dbm = &*mgr.state::(); let db = dbm.0.lock().await.get().unwrap(); let (sql, params) = Query::select() @@ -479,8 +501,8 @@ pub async fn get_grpc_connection(mgr: &impl Manager, id: &str) -> Result, +pub async fn list_grpc_connections( + mgr: &impl Manager, request_id: &str, ) -> Result> { let dbm = &*mgr.state::(); @@ -497,7 +519,10 @@ pub async fn list_grpc_connections( Ok(items.map(|v| v.unwrap()).collect()) } -pub async fn delete_grpc_connection(window: &WebviewWindow, id: &str) -> Result { +pub async fn delete_grpc_connection( + window: &WebviewWindow, + id: &str, +) -> Result { let resp = get_grpc_connection(window, id).await?; let dbm = &*window.app_handle().state::(); @@ -512,14 +537,20 @@ pub async fn delete_grpc_connection(window: &WebviewWindow, id: &str) -> Result< emit_deleted_model(window, resp) } -pub async fn delete_all_grpc_connections(window: &WebviewWindow, request_id: &str) -> Result<()> { +pub async fn delete_all_grpc_connections( + window: &WebviewWindow, + request_id: &str, +) -> Result<()> { for r in list_grpc_connections(window, request_id).await? { delete_grpc_connection(window, &r.id).await?; } Ok(()) } -pub async fn upsert_grpc_event(window: &WebviewWindow, event: &GrpcEvent) -> Result { +pub async fn upsert_grpc_event( + window: &WebviewWindow, + event: &GrpcEvent, +) -> Result { let id = match event.id.as_str() { "" => generate_model_id(ModelType::TypeGrpcEvent), _ => event.id.to_string(), @@ -575,7 +606,7 @@ pub async fn upsert_grpc_event(window: &WebviewWindow, event: &GrpcEvent) -> Res Ok(emit_upserted_model(window, m)) } -pub async fn get_grpc_event(mgr: &impl Manager, id: &str) -> Result { +pub async fn get_grpc_event(mgr: &impl Manager, id: &str) -> Result { let dbm = &*mgr.state::(); let db = dbm.0.lock().await.get().unwrap(); let (sql, params) = Query::select() @@ -587,8 +618,8 @@ pub async fn get_grpc_event(mgr: &impl Manager, id: &str) -> Result, +pub async fn list_grpc_events( + mgr: &impl Manager, connection_id: &str, ) -> Result> { let dbm = &*mgr.state::(); @@ -605,8 +636,8 @@ pub async fn list_grpc_events( Ok(items.map(|v| v.unwrap()).collect()) } -pub async fn upsert_cookie_jar( - window: &WebviewWindow, +pub async fn upsert_cookie_jar( + window: &WebviewWindow, cookie_jar: &CookieJar, ) -> Result { let id = match cookie_jar.id.as_str() { @@ -653,8 +684,8 @@ pub async fn upsert_cookie_jar( Ok(emit_upserted_model(window, m)) } -pub async fn list_environments( - mgr: &impl Manager, +pub async fn list_environments( + mgr: &impl Manager, workspace_id: &str, ) -> Result> { let dbm = &*mgr.state::(); @@ -671,7 +702,10 @@ pub async fn list_environments( Ok(items.map(|v| v.unwrap()).collect()) } -pub async fn delete_environment(window: &WebviewWindow, id: &str) -> Result { +pub async fn delete_environment( + window: &WebviewWindow, + id: &str, +) -> Result { let env = get_environment(window, id).await?; let dbm = &*window.app_handle().state::(); @@ -686,7 +720,7 @@ pub async fn delete_environment(window: &WebviewWindow, id: &str) -> Result) -> Result { +async fn get_settings(mgr: &impl Manager) -> Result { let dbm = &*mgr.state::(); let db = dbm.0.lock().await.get().unwrap(); @@ -699,7 +733,7 @@ async fn get_settings(mgr: &impl Manager) -> Result { Ok(stmt.query_row(&*params.as_params(), |row| row.try_into())?) } -pub async fn get_or_create_settings(mgr: &impl Manager) -> Settings { +pub async fn get_or_create_settings(mgr: &impl Manager) -> Settings { if let Ok(settings) = get_settings(mgr).await { return settings; } @@ -721,7 +755,10 @@ pub async fn get_or_create_settings(mgr: &impl Manager) -> Settings { .expect("Failed to insert Settings") } -pub async fn update_settings(window: &WebviewWindow, settings: Settings) -> Result { +pub async fn update_settings( + window: &WebviewWindow, + settings: Settings, +) -> Result { let dbm = &*window.app_handle().state::(); let db = dbm.0.lock().await.get().unwrap(); @@ -773,8 +810,8 @@ pub async fn update_settings(window: &WebviewWindow, settings: Settings) -> Resu Ok(emit_upserted_model(window, m)) } -pub async fn upsert_environment( - window: &WebviewWindow, +pub async fn upsert_environment( + window: &WebviewWindow, environment: Environment, ) -> Result { let id = match environment.id.as_str() { @@ -821,7 +858,7 @@ pub async fn upsert_environment( Ok(emit_upserted_model(window, m)) } -pub async fn get_environment(mgr: &impl Manager, id: &str) -> Result { +pub async fn get_environment(mgr: &impl Manager, id: &str) -> Result { let dbm = &*mgr.state::(); let db = dbm.0.lock().await.get().unwrap(); @@ -834,7 +871,7 @@ pub async fn get_environment(mgr: &impl Manager, id: &str) -> Result, id: &str) -> Result { +pub async fn get_folder(mgr: &impl Manager, id: &str) -> Result { let dbm = &*mgr.state::(); let db = dbm.0.lock().await.get().unwrap(); @@ -847,7 +884,10 @@ pub async fn get_folder(mgr: &impl Manager, id: &str) -> Result { Ok(stmt.query_row(&*params.as_params(), |row| row.try_into())?) } -pub async fn list_folders(mgr: &impl Manager, workspace_id: &str) -> Result> { +pub async fn list_folders( + mgr: &impl Manager, + workspace_id: &str, +) -> Result> { let dbm = &*mgr.state::(); let db = dbm.0.lock().await.get().unwrap(); @@ -862,7 +902,7 @@ pub async fn list_folders(mgr: &impl Manager, workspace_id: &str) -> Result Ok(items.map(|v| v.unwrap()).collect()) } -pub async fn delete_folder(window: &WebviewWindow, id: &str) -> Result { +pub async fn delete_folder(window: &WebviewWindow, id: &str) -> Result { let folder = get_folder(window, id).await?; let dbm = &*window.app_handle().state::(); @@ -877,7 +917,7 @@ pub async fn delete_folder(window: &WebviewWindow, id: &str) -> Result { emit_deleted_model(window, folder) } -pub async fn upsert_folder(window: &WebviewWindow, r: Folder) -> Result { +pub async fn upsert_folder(window: &WebviewWindow, r: Folder) -> Result { let id = match r.id.as_str() { "" => generate_model_id(ModelType::TypeFolder), _ => r.id.to_string(), @@ -925,13 +965,19 @@ pub async fn upsert_folder(window: &WebviewWindow, r: Folder) -> Result Ok(emit_upserted_model(window, m)) } -pub async fn duplicate_http_request(window: &WebviewWindow, id: &str) -> Result { +pub async fn duplicate_http_request( + window: &WebviewWindow, + id: &str, +) -> Result { let mut request = get_http_request(window, id).await?.clone(); request.id = "".to_string(); upsert_http_request(window, request).await } -pub async fn upsert_http_request(window: &WebviewWindow, r: HttpRequest) -> Result { +pub async fn upsert_http_request( + window: &WebviewWindow, + r: HttpRequest, +) -> Result { let id = match r.id.as_str() { "" => generate_model_id(ModelType::TypeHttpRequest), _ => r.id.to_string(), @@ -1004,8 +1050,8 @@ pub async fn upsert_http_request(window: &WebviewWindow, r: HttpRequest) -> Resu Ok(emit_upserted_model(window, m)) } -pub async fn list_http_requests( - mgr: &impl Manager, +pub async fn list_http_requests( + mgr: &impl Manager, workspace_id: &str, ) -> Result> { let dbm = &*mgr.state::(); @@ -1021,7 +1067,7 @@ pub async fn list_http_requests( Ok(items.map(|v| v.unwrap()).collect()) } -pub async fn get_http_request(mgr: &impl Manager, id: &str) -> Result { +pub async fn get_http_request(mgr: &impl Manager, id: &str) -> Result { let dbm = &*mgr.state::(); let db = dbm.0.lock().await.get().unwrap(); @@ -1034,7 +1080,10 @@ pub async fn get_http_request(mgr: &impl Manager, id: &str) -> Result Result { +pub async fn delete_http_request( + window: &WebviewWindow, + id: &str, +) -> Result { let req = get_http_request(window, id).await?; // DB deletes will cascade but this will delete the files @@ -1051,9 +1100,30 @@ pub async fn delete_http_request(window: &WebviewWindow, id: &str) -> Result( + window: &WebviewWindow, + request_id: &str, +) -> Result { + create_http_response( + &window, + request_id, + 0, + 0, + "", + 0, + None, + None, + None, + vec![], + None, + None, + ) + .await +} + #[allow(clippy::too_many_arguments)] -pub async fn create_http_response( - window: &WebviewWindow, +pub async fn create_http_response( + window: &WebviewWindow, request_id: &str, elapsed: i64, elapsed_headers: i64, @@ -1146,8 +1216,8 @@ pub async fn cancel_pending_responses(app: &AppHandle) -> Result<()> { Ok(()) } -pub async fn update_response_if_id( - window: &WebviewWindow, +pub async fn update_response_if_id( + window: &WebviewWindow, response: &HttpResponse, ) -> Result { if response.id.is_empty() { @@ -1157,8 +1227,8 @@ pub async fn update_response_if_id( } } -pub async fn update_response( - window: &WebviewWindow, +pub async fn update_response( + window: &WebviewWindow, response: &HttpResponse, ) -> Result { let dbm = &*window.app_handle().state::(); @@ -1211,7 +1281,10 @@ pub async fn update_response( Ok(emit_upserted_model(window, m)) } -pub async fn get_http_response(mgr: &impl Manager, id: &str) -> Result { +pub async fn get_http_response( + mgr: &impl Manager, + id: &str, +) -> Result { let dbm = &*mgr.state::(); let db = dbm.0.lock().await.get().unwrap(); let (sql, params) = Query::select() @@ -1223,7 +1296,10 @@ pub async fn get_http_response(mgr: &impl Manager, id: &str) -> Result Result { +pub async fn delete_http_response( + window: &WebviewWindow, + id: &str, +) -> Result { let resp = get_http_response(window, id).await?; // Delete the body file if it exists @@ -1244,15 +1320,18 @@ pub async fn delete_http_response(window: &WebviewWindow, id: &str) -> Result Result<()> { +pub async fn delete_all_http_responses( + window: &WebviewWindow, + request_id: &str, +) -> Result<()> { for r in list_responses(window, request_id, None).await? { delete_http_response(window, &r.id).await?; } Ok(()) } -pub async fn list_responses( - mgr: &impl Manager, +pub async fn list_responses( + mgr: &impl Manager, request_id: &str, limit: Option, ) -> Result> { @@ -1271,8 +1350,8 @@ pub async fn list_responses( Ok(items.map(|v| v.unwrap()).collect()) } -pub async fn list_responses_by_workspace_id( - mgr: &impl Manager, +pub async fn list_responses_by_workspace_id( + mgr: &impl Manager, workspace_id: &str, ) -> Result> { let dbm = &*mgr.state::(); @@ -1288,7 +1367,7 @@ pub async fn list_responses_by_workspace_id( Ok(items.map(|v| v.unwrap()).collect()) } -pub async fn debug_pool(mgr: &impl Manager) { +pub async fn debug_pool(mgr: &impl Manager) { let dbm = &*mgr.state::(); let db = dbm.0.lock().await; debug!("Debug database state: {:?}", db.state()); @@ -1310,7 +1389,7 @@ struct ModelPayload { pub window_label: String, } -fn emit_upserted_model(window: &WebviewWindow, model: M) -> M { +fn emit_upserted_model(window: &WebviewWindow, model: M) -> M { let payload = ModelPayload { model: model.clone(), window_label: window.label().to_string(), @@ -1320,7 +1399,10 @@ fn emit_upserted_model(window: &WebviewWindow, model: M) - model } -fn emit_deleted_model(window: &WebviewWindow, model: M) -> Result { +fn emit_deleted_model( + window: &WebviewWindow, + model: M, +) -> Result { let payload = ModelPayload { model: model.clone(), window_label: window.label().to_string(), diff --git a/src-tauri/yaak_plugin_runtime/src/events.rs b/src-tauri/yaak_plugin_runtime/src/events.rs index b934c6b1..bb0325c6 100644 --- a/src-tauri/yaak_plugin_runtime/src/events.rs +++ b/src-tauri/yaak_plugin_runtime/src/events.rs @@ -1,7 +1,10 @@ use serde::{Deserialize, Serialize}; use ts_rs::TS; -use yaak_models::models::{CookieJar, Environment, Folder, GrpcConnection, GrpcEvent, GrpcRequest, HttpRequest, HttpResponse, KeyValue, Settings, Workspace}; +use yaak_models::models::{ + CookieJar, Environment, Folder, GrpcConnection, GrpcEvent, GrpcRequest, HttpRequest, + HttpResponse, KeyValue, Settings, Workspace, +}; #[derive(Debug, Clone, Serialize, Deserialize, TS)] #[serde(rename_all = "camelCase")] @@ -20,12 +23,22 @@ pub struct InternalEvent { pub enum InternalEventPayload { BootRequest(BootRequest), BootResponse(BootResponse), + ImportRequest(ImportRequest), ImportResponse(ImportResponse), + FilterRequest(FilterRequest), FilterResponse(FilterResponse), + ExportHttpRequestRequest(ExportHttpRequestRequest), ExportHttpRequestResponse(ExportHttpRequestResponse), + + SendHttpRequestRequest(SendHttpRequestRequest), + SendHttpRequestResponse(SendHttpRequestResponse), + + GetHttpRequestByIdRequest(GetHttpRequestByIdRequest), + GetHttpRequestByIdResponse(GetHttpRequestByIdResponse), + /// Returned when a plugin doesn't get run, just so the server /// has something to listen for EmptyResponse(EmptyResponse), @@ -95,17 +108,33 @@ pub struct ExportHttpRequestResponse { pub content: String, } -// TODO: Migrate plugins to return this type -// #[derive(Debug, Clone, Serialize, Deserialize, TS)] -// #[serde(rename_all = "camelCase", untagged)] -// #[ts(export)] -// pub enum ExportableModel { -// Workspace(Workspace), -// Environment(Environment), -// Folder(Folder), -// HttpRequest(HttpRequest), -// GrpcRequest(GrpcRequest), -// } +#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] +#[serde(default, rename_all = "camelCase")] +#[ts(export)] +pub struct SendHttpRequestRequest { + pub http_request: HttpRequest, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] +#[serde(default, rename_all = "camelCase")] +#[ts(export)] +pub struct SendHttpRequestResponse { + pub http_response: HttpResponse, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] +#[serde(default, rename_all = "camelCase")] +#[ts(export)] +pub struct GetHttpRequestByIdRequest { + pub id: String, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] +#[serde(default, rename_all = "camelCase")] +#[ts(export)] +pub struct GetHttpRequestByIdResponse { + pub http_request: Option, +} #[derive(Debug, Clone, Default, Serialize, Deserialize, TS)] #[serde(default, rename_all = "camelCase")] diff --git a/src-tauri/yaak_plugin_runtime/src/manager.rs b/src-tauri/yaak_plugin_runtime/src/manager.rs index 0372f16d..64aa84bb 100644 --- a/src-tauri/yaak_plugin_runtime/src/manager.rs +++ b/src-tauri/yaak_plugin_runtime/src/manager.rs @@ -1,7 +1,7 @@ use crate::error::Result; use crate::events::{ - ExportHttpRequestRequest, ExportHttpRequestResponse, FilterRequest, FilterResponse, - ImportRequest, ImportResponse, InternalEventPayload, + ExportHttpRequestRequest, ExportHttpRequestResponse, FilterRequest, FilterResponse + , ImportRequest, ImportResponse, InternalEvent, InternalEventPayload, }; use crate::error::Error::PluginErr; @@ -10,6 +10,7 @@ use crate::plugin::start_server; use crate::server::PluginRuntimeGrpcServer; use std::time::Duration; use tauri::{AppHandle, Runtime}; +use tokio::sync::mpsc; use tokio::sync::watch::Sender; use yaak_models::models::HttpRequest; @@ -35,14 +36,33 @@ impl PluginManager { PluginManager { kill_tx, server } } - pub async fn cleanup(&mut self) { + pub async fn subscribe(&self) -> (String, mpsc::Receiver) { + self.server.subscribe().await + } + + pub async fn unsubscribe(&self, rx_id: &str) { + self.server.unsubscribe(rx_id).await + } + + pub async fn cleanup(&self) { self.kill_tx.send_replace(true); // Give it a bit of time to kill tokio::time::sleep(Duration::from_millis(500)).await; } - pub async fn run_import(&mut self, content: &str) -> Result<(ImportResponse, String)> { + pub async fn reply( + &self, + source_event: &InternalEvent, + payload: &InternalEventPayload, + ) -> Result<()> { + let reply_id = Some(source_event.clone().id); + self.server + .send(&payload, source_event.plugin_ref_id.as_str(), reply_id) + .await + } + + pub async fn run_import(&self, content: &str) -> Result<(ImportResponse, String)> { let reply_events = self .server .send_and_wait(&InternalEventPayload::ImportRequest(ImportRequest { @@ -67,7 +87,7 @@ impl PluginManager { } pub async fn run_export_curl( - &mut self, + &self, request: &HttpRequest, ) -> Result { let event = self @@ -90,7 +110,7 @@ impl PluginManager { } pub async fn run_filter( - &mut self, + &self, filter: &str, content: &str, content_type: &str, diff --git a/src-tauri/yaak_plugin_runtime/src/plugin.rs b/src-tauri/yaak_plugin_runtime/src/plugin.rs index 3be20c63..883f42a8 100644 --- a/src-tauri/yaak_plugin_runtime/src/plugin.rs +++ b/src-tauri/yaak_plugin_runtime/src/plugin.rs @@ -13,7 +13,6 @@ use tauri::plugin::{Builder, TauriPlugin}; use tauri::{Manager, RunEvent, Runtime, State}; use tokio::fs::read_dir; use tokio::net::TcpListener; -use tokio::sync::Mutex; use tonic::codegen::tokio_stream; use tonic::transport::Server; @@ -30,8 +29,7 @@ pub fn init() -> TauriPlugin { .await .expect("Failed to read plugins dir"); let manager = PluginManager::new(&app, plugin_dirs).await; - let manager_state = Mutex::new(manager); - app.manage(manager_state); + app.manage(manager); Ok(()) }) }) @@ -41,8 +39,8 @@ pub fn init() -> TauriPlugin { api.prevent_exit(); tauri::async_runtime::block_on(async move { info!("Exiting plugin runtime due to app exit"); - let manager: State> = app.state(); - manager.lock().await.cleanup().await; + let manager: State = app.state(); + manager.cleanup().await; exit(0); }); } @@ -79,7 +77,7 @@ pub async fn start_server( _ => {} }; } - server.unsubscribe(rx_id).await; + server.unsubscribe(rx_id.as_str()).await; }); }; diff --git a/src-tauri/yaak_plugin_runtime/src/server.rs b/src-tauri/yaak_plugin_runtime/src/server.rs index ac526f10..2a940591 100644 --- a/src-tauri/yaak_plugin_runtime/src/server.rs +++ b/src-tauri/yaak_plugin_runtime/src/server.rs @@ -95,8 +95,8 @@ impl PluginRuntimeGrpcServer { (id, rx) } - pub async fn unsubscribe(&self, rx_id: String) { - self.subscribers.lock().await.remove(rx_id.as_str()); + pub async fn unsubscribe(&self, rx_id: &str) { + self.subscribers.lock().await.remove(rx_id); } pub async fn remove_plugins(&self, plugin_ids: Vec) { @@ -214,6 +214,13 @@ impl PluginRuntimeGrpcServer { let msg = format!("Failed to find plugin for {plugin_name}"); Err(PluginNotFoundErr(msg)) } + + pub async fn send(&self, payload: &InternalEventPayload, plugin_ref_id: &str, reply_id: Option)-> Result<()> { + let plugin = self.plugin_by_ref_id(plugin_ref_id).await?; + let event = plugin.build_event_to_send(payload, reply_id); + plugin.send(&event).await + } + pub async fn send_to_plugin( &self, @@ -301,7 +308,7 @@ impl PluginRuntimeGrpcServer { break; } } - server.unsubscribe(rx_id).await; + server.unsubscribe(rx_id.as_str()).await; found_events }) @@ -321,30 +328,6 @@ impl PluginRuntimeGrpcServer { Ok(events) } - pub async fn send(&self, payload: InternalEventPayload) -> Result> { - let mut events: Vec = Vec::new(); - let plugins = self.plugin_ref_to_plugin.lock().await; - if plugins.is_empty() { - return Err(NoPluginsErr("Send failed because no plugins exist".into())); - } - - for ph in plugins.values() { - let event = ph.build_event_to_send(&payload, None); - self.send_to_plugin_handle(ph, &event).await?; - events.push(event); - } - - Ok(events) - } - - async fn send_to_plugin_handle( - &self, - plugin: &PluginHandle, - event: &InternalEvent, - ) -> Result<()> { - plugin.send(event).await - } - async fn load_plugins( &self, to_plugin_tx: mpsc::Sender>, diff --git a/src-tauri/templates/Cargo.toml b/src-tauri/yaak_templates/Cargo.toml similarity index 76% rename from src-tauri/templates/Cargo.toml rename to src-tauri/yaak_templates/Cargo.toml index 8f4e26d2..b8c9bb70 100644 --- a/src-tauri/templates/Cargo.toml +++ b/src-tauri/yaak_templates/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "templates" +name = "yaak_templates" version = "0.1.0" edition = "2021" diff --git a/src-tauri/templates/src/lib.rs b/src-tauri/yaak_templates/src/lib.rs similarity index 100% rename from src-tauri/templates/src/lib.rs rename to src-tauri/yaak_templates/src/lib.rs diff --git a/src-tauri/templates/src/parser.rs b/src-tauri/yaak_templates/src/parser.rs similarity index 100% rename from src-tauri/templates/src/parser.rs rename to src-tauri/yaak_templates/src/parser.rs diff --git a/src-tauri/templates/src/renderer.rs b/src-tauri/yaak_templates/src/renderer.rs similarity index 100% rename from src-tauri/templates/src/renderer.rs rename to src-tauri/yaak_templates/src/renderer.rs diff --git a/src-web/components/CommandPalette.tsx b/src-web/components/CommandPalette.tsx index 92bad6ed..a176bf05 100644 --- a/src-web/components/CommandPalette.tsx +++ b/src-web/components/CommandPalette.tsx @@ -4,9 +4,8 @@ import type { KeyboardEvent, ReactNode } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { useActiveCookieJar } from '../hooks/useActiveCookieJar'; import { useActiveEnvironment } from '../hooks/useActiveEnvironment'; -import { useActiveEnvironmentId } from '../hooks/useActiveEnvironmentId'; import { useActiveRequest } from '../hooks/useActiveRequest'; -import { useActiveWorkspaceId } from '../hooks/useActiveWorkspaceId'; +import { useActiveWorkspace } from '../hooks/useActiveWorkspace'; import { useAppRoutes } from '../hooks/useAppRoutes'; import { useCreateEnvironment } from '../hooks/useCreateEnvironment'; import { useCreateGrpcRequest } from '../hooks/useCreateGrpcRequest'; @@ -56,8 +55,8 @@ const MAX_PER_GROUP = 8; export function CommandPalette({ onClose }: { onClose: () => void }) { const [command, setCommand] = useDebouncedState('', 150); const [selectedItemKey, setSelectedItemKey] = useState(null); + const [activeEnvironment, setActiveEnvironmentId] = useActiveEnvironment(); const routes = useAppRoutes(); - const activeEnvironmentId = useActiveEnvironmentId(); const workspaces = useWorkspaces(); const environments = useEnvironments(); const recentEnvironments = useRecentEnvironments(); @@ -68,12 +67,11 @@ export function CommandPalette({ onClose }: { onClose: () => void }) { const openWorkspace = useOpenWorkspace(); const createWorkspace = useCreateWorkspace(); const createHttpRequest = useCreateHttpRequest(); - const { activeCookieJar } = useActiveCookieJar(); + const [activeCookieJar] = useActiveCookieJar(); const createGrpcRequest = useCreateGrpcRequest(); const createEnvironment = useCreateEnvironment(); const dialog = useDialog(); - const workspaceId = useActiveWorkspaceId(); - const activeEnvironment = useActiveEnvironment(); + const workspace = useActiveWorkspace(); const sendRequest = useSendAnyHttpRequest(); const renameRequest = useRenameRequest(activeRequest?.id ?? null); const deleteRequest = useDeleteRequest(activeRequest?.id ?? null); @@ -86,9 +84,9 @@ export function CommandPalette({ onClose }: { onClose: () => void }) { label: 'Open Settings', action: 'settings.show', onSelect: async () => { - if (workspaceId == null) return; + if (workspace == null) return; await invokeCmd('cmd_new_nested_window', { - url: routes.paths.workspaceSettings({ workspaceId }), + url: routes.paths.workspaceSettings({ workspaceId: workspace.id }), label: 'settings', title: 'Yaak Settings', }); @@ -190,7 +188,7 @@ export function CommandPalette({ onClose }: { onClose: () => void }) { routes.paths, sendRequest, setSidebarHidden, - workspaceId, + workspace, ]); const sortedRequests = useMemo(() => { @@ -271,7 +269,7 @@ export function CommandPalette({ onClose }: { onClose: () => void }) { return routes.navigate('request', { workspaceId: r.workspaceId, requestId: r.id, - environmentId: activeEnvironmentId ?? undefined, + environmentId: activeEnvironment?.id, }); }, }); @@ -290,7 +288,7 @@ export function CommandPalette({ onClose }: { onClose: () => void }) { environmentGroup.items.push({ key: `switch-environment-${e.id}`, label: e.name, - onSelect: () => routes.setEnvironment(e), + onSelect: () => setActiveEnvironmentId(e.id), }); } @@ -313,9 +311,9 @@ export function CommandPalette({ onClose }: { onClose: () => void }) { workspaceCommands, sortedRequests, routes, - activeEnvironmentId, + activeEnvironment, sortedEnvironments, - activeEnvironment?.id, + setActiveEnvironmentId, sortedWorkspaces, openWorkspace, ]); diff --git a/src-web/components/CookieDialog.tsx b/src-web/components/CookieDialog.tsx index 40245a36..4c5c7866 100644 --- a/src-web/components/CookieDialog.tsx +++ b/src-web/components/CookieDialog.tsx @@ -12,7 +12,7 @@ interface Props { export const CookieDialog = function ({ cookieJarId }: Props) { const updateCookieJar = useUpdateCookieJar(cookieJarId ?? null); - const cookieJars = useCookieJars(); + const cookieJars = useCookieJars().data ?? []; const cookieJar = cookieJars.find((c) => c.id === cookieJarId); if (cookieJar == null) { diff --git a/src-web/components/CookieDropdown.tsx b/src-web/components/CookieDropdown.tsx index de42c6af..1e59b823 100644 --- a/src-web/components/CookieDropdown.tsx +++ b/src-web/components/CookieDropdown.tsx @@ -12,8 +12,8 @@ import { InlineCode } from './core/InlineCode'; import { useDialog } from './DialogContext'; export function CookieDropdown() { - const cookieJars = useCookieJars(); - const { activeCookieJar, setActiveCookieJarId } = useActiveCookieJar(); + const cookieJars = useCookieJars().data ?? []; + const [activeCookieJar, setActiveCookieJarId] = useActiveCookieJar(); const updateCookieJar = useUpdateCookieJar(activeCookieJar?.id ?? null); const deleteCookieJar = useDeleteCookieJar(activeCookieJar ?? null); const createCookieJar = useCreateCookieJar(); diff --git a/src-web/components/EnvironmentActionsDropdown.tsx b/src-web/components/EnvironmentActionsDropdown.tsx index b31d03b4..91305813 100644 --- a/src-web/components/EnvironmentActionsDropdown.tsx +++ b/src-web/components/EnvironmentActionsDropdown.tsx @@ -1,7 +1,6 @@ import classNames from 'classnames'; import { memo, useCallback, useMemo } from 'react'; import { useActiveEnvironment } from '../hooks/useActiveEnvironment'; -import { useAppRoutes } from '../hooks/useAppRoutes'; import { useEnvironments } from '../hooks/useEnvironments'; import type { ButtonProps } from './core/Button'; import { Button } from './core/Button'; @@ -20,9 +19,8 @@ export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdo ...buttonProps }: Props) { const environments = useEnvironments(); - const activeEnvironment = useActiveEnvironment(); + const [activeEnvironment, setActiveEnvironmentId] = useActiveEnvironment(); const dialog = useDialog(); - const routes = useAppRoutes(); const showEnvironmentDialog = useCallback(() => { dialog.toggle({ @@ -43,9 +41,9 @@ export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdo leftSlot: e.id === activeEnvironment?.id ? : , onSelect: async () => { if (e.id !== activeEnvironment?.id) { - routes.setEnvironment(e); + setActiveEnvironmentId(e.id); } else { - routes.setEnvironment(null); + setActiveEnvironmentId(null); } }, }), @@ -62,7 +60,7 @@ export const EnvironmentActionsDropdown = memo(function EnvironmentActionsDropdo onSelect: showEnvironmentDialog, }, ], - [activeEnvironment?.id, environments, routes, showEnvironmentDialog], + [activeEnvironment?.id, environments, setActiveEnvironmentId, showEnvironmentDialog], ); return ( diff --git a/src-web/components/GlobalHooks.tsx b/src-web/components/GlobalHooks.tsx index 89b9fdc2..b948cb1b 100644 --- a/src-web/components/GlobalHooks.tsx +++ b/src-web/components/GlobalHooks.tsx @@ -2,7 +2,8 @@ import { useQueryClient } from '@tanstack/react-query'; import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'; import type { Model } from '@yaakapp/api'; import { useEffect } from 'react'; -import { useAtiveWorkspaceChangedToast } from '../hooks/useAtiveWorkspaceChangedToast'; +import { useEnsureActiveCookieJar, useMigrateActiveCookieJarId } from '../hooks/useActiveCookieJar'; +import { useActiveWorkspaceChangedToast } from '../hooks/useActiveWorkspaceChangedToast'; import { cookieJarsQueryKey } from '../hooks/useCookieJars'; import { useCopy } from '../hooks/useCopy'; import { environmentsQueryKey } from '../hooks/useEnvironments'; @@ -16,6 +17,7 @@ import { httpResponsesQueryKey } from '../hooks/useHttpResponses'; import { keyValueQueryKey } from '../hooks/useKeyValue'; import { useListenToTauriEvent } from '../hooks/useListenToTauriEvent'; import { useNotificationToast } from '../hooks/useNotificationToast'; +import { useRecentCookieJars } from '../hooks/useRecentCookieJars'; import { useRecentEnvironments } from '../hooks/useRecentEnvironments'; import { useRecentRequests } from '../hooks/useRecentRequests'; import { useRecentWorkspaces } from '../hooks/useRecentWorkspaces'; @@ -25,6 +27,7 @@ import { useSyncThemeToDocument } from '../hooks/useSyncThemeToDocument'; import { useToggleCommandPalette } from '../hooks/useToggleCommandPalette'; import { workspacesQueryKey } from '../hooks/useWorkspaces'; import { useZoom } from '../hooks/useZoom'; +import { extractKeyValue } from '../lib/keyValueStore'; import { modelsEq } from '../lib/models'; import { catppuccinMacchiato } from '../lib/theme/themes/catppuccin'; import { githubLight } from '../lib/theme/themes/github'; @@ -38,12 +41,17 @@ export function GlobalHooks() { // Include here so they always update, even if no component references them useRecentWorkspaces(); useRecentEnvironments(); + useRecentCookieJars(); useRecentRequests(); // Other useful things useSyncThemeToDocument(); useNotificationToast(); - useAtiveWorkspaceChangedToast(); + useActiveWorkspaceChangedToast(); + useEnsureActiveCookieJar(); + + // TODO: Remove in future version + useMigrateActiveCookieJarId(); const toggleCommandPalette = useToggleCommandPalette(); useHotKey('command_palette.toggle', toggleCommandPalette); @@ -96,21 +104,28 @@ export function GlobalHooks() { model.model, ); - if (shouldIgnoreModel(model)) return; + if (shouldIgnoreModel(model, windowLabel)) return; - queryClient.setQueryData(queryKey, (values = []) => { - const index = values.findIndex((v) => modelsEq(v, model)) ?? -1; - if (index >= 0) { - return [...values.slice(0, index), model, ...values.slice(index + 1)]; - } else { - return pushToFront ? [model, ...(values ?? [])] : [...(values ?? []), model]; + queryClient.setQueryData(queryKey, (current: unknown) => { + if (model.model === 'key_value') { + // Special-case for KeyValue + return extractKeyValue(model); + } + + if (Array.isArray(current)) { + const index = current.findIndex((v) => modelsEq(v, model)) ?? -1; + if (index >= 0) { + return [...current.slice(0, index), model, ...current.slice(index + 1)]; + } else { + return pushToFront ? [model, ...(current ?? [])] : [...(current ?? []), model]; + } } }); }); useListenToTauriEvent('deleted_model', ({ payload }) => { - const { model } = payload; - if (shouldIgnoreModel(model)) return; + const { model, windowLabel } = payload; + if (shouldIgnoreModel(model, windowLabel)) return; if (model.model === 'workspace') { queryClient.setQueryData(workspacesQueryKey(), removeById(model)); @@ -181,7 +196,11 @@ function removeById(model: T) { return (entries: T[] | undefined) => entries?.filter((e) => e.id !== model.id); } -const shouldIgnoreModel = (payload: Model) => { +const shouldIgnoreModel = (payload: Model, windowLabel: string) => { + if (windowLabel === getCurrentWebviewWindow().label) { + // Never ignore same-window updates + return false; + } if (payload.model === 'key_value') { return payload.namespace === 'no_sync'; } diff --git a/src-web/components/MoveToWorkspaceDialog.tsx b/src-web/components/MoveToWorkspaceDialog.tsx index 4cb2d0b6..82b92f8f 100644 --- a/src-web/components/MoveToWorkspaceDialog.tsx +++ b/src-web/components/MoveToWorkspaceDialog.tsx @@ -52,12 +52,12 @@ export function MoveToWorkspaceDialog({ onDone, request, activeWorkspaceId }: Pr if (request.model === 'http_request') { await updateHttpRequest.mutateAsync(args); - queryClient.invalidateQueries({ + await queryClient.invalidateQueries({ queryKey: httpRequestsQueryKey({ workspaceId: activeWorkspaceId }), }); } else if (request.model === 'grpc_request') { await updateGrpcRequest.mutateAsync(args); - queryClient.invalidateQueries({ + await queryClient.invalidateQueries({ queryKey: grpcRequestsQueryKey({ workspaceId: activeWorkspaceId }), }); } diff --git a/src-web/components/RecentRequestsDropdown.tsx b/src-web/components/RecentRequestsDropdown.tsx index cd5b607a..6e3638c9 100644 --- a/src-web/components/RecentRequestsDropdown.tsx +++ b/src-web/components/RecentRequestsDropdown.tsx @@ -3,7 +3,7 @@ import { useMemo, useRef } from 'react'; import { useKeyPressEvent } from 'react-use'; import { useActiveEnvironment } from '../hooks/useActiveEnvironment'; import { useActiveRequest } from '../hooks/useActiveRequest'; -import { useActiveWorkspaceId } from '../hooks/useActiveWorkspaceId'; +import { useActiveWorkspace } from '../hooks/useActiveWorkspace'; import { useAppRoutes } from '../hooks/useAppRoutes'; import { useHotKey } from '../hooks/useHotKey'; import { useRecentRequests } from '../hooks/useRecentRequests'; @@ -18,8 +18,8 @@ import { HttpMethodTag } from './core/HttpMethodTag'; export function RecentRequestsDropdown({ className }: Pick) { const dropdownRef = useRef(null); const activeRequest = useActiveRequest(); - const activeWorkspaceId = useActiveWorkspaceId(); - const activeEnvironment = useActiveEnvironment(); + const activeWorkspace = useActiveWorkspace(); + const [activeEnvironment] = useActiveEnvironment(); const routes = useAppRoutes(); const allRecentRequestIds = useRecentRequests(); const recentRequestIds = useMemo(() => allRecentRequestIds.slice(1), [allRecentRequestIds]); @@ -42,7 +42,7 @@ export function RecentRequestsDropdown({ className }: Pick(() => { - if (activeWorkspaceId === null) return []; + if (activeWorkspace === null) return []; const recentRequestItems: DropdownItem[] = []; for (const id of recentRequestIds) { @@ -58,7 +58,7 @@ export function RecentRequestsDropdown({ className }: Pick diff --git a/src-web/components/SettingsDropdown.tsx b/src-web/components/SettingsDropdown.tsx index 44b9dbbe..306ac1be 100644 --- a/src-web/components/SettingsDropdown.tsx +++ b/src-web/components/SettingsDropdown.tsx @@ -1,6 +1,6 @@ import { open } from '@tauri-apps/plugin-shell'; import { useRef } from 'react'; -import { useActiveWorkspaceId } from '../hooks/useActiveWorkspaceId'; +import { useActiveWorkspace } from '../hooks/useActiveWorkspace'; import { useAppInfo } from '../hooks/useAppInfo'; import { useAppRoutes } from '../hooks/useAppRoutes'; import { useCheckForUpdates } from '../hooks/useCheckForUpdates'; @@ -23,12 +23,12 @@ export function SettingsDropdown() { const dialog = useDialog(); const checkForUpdates = useCheckForUpdates(); const routes = useAppRoutes(); - const workspaceId = useActiveWorkspaceId(); + const workspace = useActiveWorkspace(); const showSettings = async () => { - if (!workspaceId) return; + if (!workspace) return; await invokeCmd('cmd_new_nested_window', { - url: routes.paths.workspaceSettings({ workspaceId }), + url: routes.paths.workspaceSettings({ workspaceId: workspace.id }), label: 'settings', title: 'Yaak Settings', }); diff --git a/src-web/components/Sidebar.tsx b/src-web/components/Sidebar.tsx index 90353ac3..3b04045c 100644 --- a/src-web/components/Sidebar.tsx +++ b/src-web/components/Sidebar.tsx @@ -1,11 +1,12 @@ +import type { Folder, GrpcRequest, HttpRequest, Workspace } from '@yaakapp/api'; import classNames from 'classnames'; import type { ReactNode } from 'react'; import React, { Fragment, useCallback, useMemo, useRef, useState } from 'react'; import type { XYCoord } from 'react-dnd'; import { useDrag, useDrop } from 'react-dnd'; import { useKey, useKeyPressEvent } from 'react-use'; +import { useActiveEnvironment } from '../hooks/useActiveEnvironment'; -import { useActiveEnvironmentId } from '../hooks/useActiveEnvironmentId'; import { useActiveRequest } from '../hooks/useActiveRequest'; import { useActiveWorkspace } from '../hooks/useActiveWorkspace'; import { useAppRoutes } from '../hooks/useAppRoutes'; @@ -32,7 +33,6 @@ import { useUpdateAnyGrpcRequest } from '../hooks/useUpdateAnyGrpcRequest'; import { useUpdateAnyHttpRequest } from '../hooks/useUpdateAnyHttpRequest'; import { useWorkspaces } from '../hooks/useWorkspaces'; import { fallbackRequestName } from '../lib/fallbackRequestName'; -import type { Folder, GrpcRequest, HttpRequest, Workspace } from '@yaakapp/api'; import { isResponseLoading } from '../lib/models'; import type { DropdownItem } from './core/Dropdown'; import { ContextMenu } from './core/Dropdown'; @@ -61,7 +61,7 @@ export function Sidebar({ className }: Props) { const [hidden, setHidden] = useSidebarHidden(); const sidebarRef = useRef(null); const activeRequest = useActiveRequest(); - const activeEnvironmentId = useActiveEnvironmentId(); + const [activeEnvironment] = useActiveEnvironment(); const folders = useFolders(); const requests = useRequests(); const activeWorkspace = useActiveWorkspace(); @@ -207,14 +207,14 @@ export function Sidebar({ className }: Props) { routes.navigate('request', { requestId: id, workspaceId: item.workspaceId, - environmentId: activeEnvironmentId ?? undefined, + environmentId: activeEnvironment?.id, }); setSelectedId(id); setSelectedTree(tree); if (!opts.noFocus) focusActiveRequest({ forced: { id, tree } }); } }, - [treeParentMap, collapsed, routes, activeEnvironmentId, focusActiveRequest], + [treeParentMap, collapsed, routes, activeEnvironment, focusActiveRequest], ); const handleClearSelected = useCallback(() => { @@ -260,7 +260,7 @@ export function Sidebar({ className }: Props) { routes.navigate('request', { requestId: selected.id, workspaceId: activeWorkspace?.id, - environmentId: activeEnvironmentId ?? undefined, + environmentId: activeEnvironment?.id, }); }); diff --git a/src-web/components/Workspace.tsx b/src-web/components/Workspace.tsx index 7e243f61..57f338e4 100644 --- a/src-web/components/Workspace.tsx +++ b/src-web/components/Workspace.tsx @@ -5,7 +5,6 @@ import { useCallback, useMemo, useRef, useState } from 'react'; import { useWindowSize } from 'react-use'; import { useActiveRequest } from '../hooks/useActiveRequest'; import { useActiveWorkspace } from '../hooks/useActiveWorkspace'; -import { useActiveWorkspaceId } from '../hooks/useActiveWorkspaceId'; import { useFloatingSidebarHidden } from '../hooks/useFloatingSidebarHidden'; import { useImportData } from '../hooks/useImportData'; import { useShouldFloatSidebar } from '../hooks/useShouldFloatSidebar'; @@ -16,7 +15,6 @@ import { useWorkspaces } from '../hooks/useWorkspaces'; import { Banner } from './core/Banner'; import { Button } from './core/Button'; import { HotKeyList } from './core/HotKeyList'; -import { InlineCode } from './core/InlineCode'; import { FeedbackLink } from './core/Link'; import { HStack } from './core/Stacks'; import { CreateDropdown } from './CreateDropdown'; @@ -38,7 +36,6 @@ export default function Workspace() { useSyncWorkspaceRequestTitle(); const workspaces = useWorkspaces(); const activeWorkspace = useActiveWorkspace(); - const activeWorkspaceId = useActiveWorkspaceId(); const { setWidth, width, resetWidth } = useSidebarWidth(); const [sidebarHidden, setSidebarHidden] = useSidebarHidden(); const [floatingSidebarHidden, setFloatingSidebarHidden] = useFloatingSidebarHidden(); @@ -176,9 +173,8 @@ export default function Workspace() { {activeWorkspace == null ? (
- The active workspace{' '} - {activeWorkspaceId} was not found. - Select a workspace from the header menu or report this bug to + The active workspace was not found. Select a workspace from the header menu or report + this bug to
) : activeRequest == null ? ( diff --git a/src-web/components/core/Editor/Editor.tsx b/src-web/components/core/Editor/Editor.tsx index 79f52c5a..7401c7a8 100644 --- a/src-web/components/core/Editor/Editor.tsx +++ b/src-web/components/core/Editor/Editor.tsx @@ -87,7 +87,7 @@ export const Editor = forwardRef(function E ref, ) { const s = useSettings(); - const e = useActiveEnvironment(); + const [e] = useActiveEnvironment(); const w = useActiveWorkspace(); const environment = autocompleteVariables ? e : null; const workspace = autocompleteVariables ? w : null; diff --git a/src-web/components/core/Editor/twig/placeholder.ts b/src-web/components/core/Editor/twig/placeholder.ts index e9de1b49..a24a0ee0 100644 --- a/src-web/components/core/Editor/twig/placeholder.ts +++ b/src-web/components/core/Editor/twig/placeholder.ts @@ -9,7 +9,6 @@ class PlaceholderWidget extends WidgetType { readonly exists: boolean, readonly type: 'function' | 'variable' = 'variable', ) { - console.log('PLACEHOLDER', { name, value }); super(); } diff --git a/src-web/components/core/SplitLayout.tsx b/src-web/components/core/SplitLayout.tsx index beb06fbb..4533214a 100644 --- a/src-web/components/core/SplitLayout.tsx +++ b/src-web/components/core/SplitLayout.tsx @@ -3,7 +3,7 @@ import classNames from 'classnames'; import type { CSSProperties, MouseEvent as ReactMouseEvent, ReactNode } from 'react'; import React, { useCallback, useMemo, useRef, useState } from 'react'; import { useLocalStorage } from 'react-use'; -import { useActiveWorkspaceId } from '../../hooks/useActiveWorkspaceId'; +import { useActiveWorkspace } from '../../hooks/useActiveWorkspace'; import { clamp } from '../../lib/clamp'; import { ResizeHandle } from '../ResizeHandle'; @@ -42,10 +42,13 @@ export function SplitLayout({ minWidthPx = 10, }: Props) { const containerRef = useRef(null); + const activeWorkspace = useActiveWorkspace(); const [verticalBasedOnSize, setVerticalBasedOnSize] = useState(false); - const [widthRaw, setWidth] = useLocalStorage(`${name}_width::${useActiveWorkspaceId()}`); + const [widthRaw, setWidth] = useLocalStorage( + `${name}_width::${activeWorkspace?.id ?? 'n/a'}`, + ); const [heightRaw, setHeight] = useLocalStorage( - `${name}_height::${useActiveWorkspaceId()}`, + `${name}_height::${activeWorkspace?.id ?? 'n/a'}`, ); const width = widthRaw ?? defaultRatio; let height = heightRaw ?? defaultRatio; diff --git a/src-web/hooks/useActiveCookieJar.ts b/src-web/hooks/useActiveCookieJar.ts index 0b520763..5e39fe07 100644 --- a/src-web/hooks/useActiveCookieJar.ts +++ b/src-web/hooks/useActiveCookieJar.ts @@ -1,36 +1,87 @@ -import { useEffect, useMemo } from 'react'; -import { useActiveWorkspaceId } from './useActiveWorkspaceId'; +import { useCallback, useEffect, useMemo } from 'react'; +import { useSearchParams } from 'react-router-dom'; +import { useActiveWorkspace } from './useActiveWorkspace'; import { useCookieJars } from './useCookieJars'; import { useKeyValue } from './useKeyValue'; +export const QUERY_COOKIE_JAR_ID = 'cookie_jar_id'; + export function useActiveCookieJar() { - const workspaceId = useActiveWorkspaceId(); + const [activeCookieJarId, setActiveCookieJarId] = useActiveCookieJarId(); const cookieJars = useCookieJars(); + const activeCookieJar = useMemo(() => { + if (cookieJars.data == null) return undefined; + return cookieJars.data.find((cookieJar) => cookieJar.id === activeCookieJarId) ?? null; + }, [activeCookieJarId, cookieJars.data]); + + return [activeCookieJar ?? null, setActiveCookieJarId] as const; +} + +export function useEnsureActiveCookieJar() { + const cookieJars = useCookieJars(); + const [activeCookieJarId, setActiveCookieJarId] = useActiveCookieJarId(); + useEffect(() => { + if (cookieJars.data == null) return; + + if (cookieJars.data.find((j) => j.id === activeCookieJarId)) { + return; // There's an active jar + } + + const firstJar = cookieJars.data[0]; + if (firstJar == null) { + console.log("Workspace doesn't have any cookie jars to activate"); + return; + } + + // There's no active jar, so set it to the first one + console.log('Setting active cookie jar to', firstJar.id); + setActiveCookieJarId(firstJar.id); + }, [activeCookieJarId, cookieJars, cookieJars.data, setActiveCookieJarId]); +} + +export function useMigrateActiveCookieJarId() { + const workspace = useActiveWorkspace(); + const [, setActiveCookieJarId] = useActiveCookieJarId(); const { - set: setActiveCookieJarId, - value: activeCookieJarId, - isLoading: isLoadingActiveCookieJarId, + set: setLegacyActiveCookieJarId, + value: legacyActiveCookieJarId, + isLoading: isLoadingLegacyActiveCookieJarId, } = useKeyValue({ namespace: 'global', - key: ['activeCookieJar', workspaceId ?? 'n/a'], + key: ['activeCookieJar', workspace?.id ?? 'n/a'], fallback: null, }); - const activeCookieJar = useMemo( - () => cookieJars.find((cookieJar) => cookieJar.id === activeCookieJarId), - [activeCookieJarId, cookieJars], + useEffect(() => { + if (legacyActiveCookieJarId == null || isLoadingLegacyActiveCookieJarId) return; + + console.log('Migrating active cookie jar ID to query param', legacyActiveCookieJarId); + setActiveCookieJarId(legacyActiveCookieJarId); + setLegacyActiveCookieJarId(null).catch(console.error); + }, [ + workspace, + isLoadingLegacyActiveCookieJarId, + legacyActiveCookieJarId, + setActiveCookieJarId, + setLegacyActiveCookieJarId, + ]); +} + +function useActiveCookieJarId() { + // NOTE: This query param is accessed from Rust side, so do not change + const [params, setParams] = useSearchParams(); + const id = params.get(QUERY_COOKIE_JAR_ID); + + const setId = useCallback( + (id: string) => { + setParams((p) => { + const existing = Object.fromEntries(p); + return { ...existing, [QUERY_COOKIE_JAR_ID]: id }; + }); + }, + [setParams], ); - // TODO: Make this not be called so many times (move to GlobalHooks?) - useEffect(() => { - if (!isLoadingActiveCookieJarId && activeCookieJar == null && cookieJars.length > 0) { - setActiveCookieJarId(cookieJars[0]?.id ?? null).catch(console.error); - } - }, [activeCookieJar, cookieJars, isLoadingActiveCookieJarId, setActiveCookieJarId]); - - return { - activeCookieJar: activeCookieJar ?? null, - setActiveCookieJarId: setActiveCookieJarId, - }; + return [id, setId] as const; } diff --git a/src-web/hooks/useActiveEnvironment.ts b/src-web/hooks/useActiveEnvironment.ts index e15deb94..339eeb0a 100644 --- a/src-web/hooks/useActiveEnvironment.ts +++ b/src-web/hooks/useActiveEnvironment.ts @@ -1,10 +1,38 @@ -import { useMemo } from 'react'; -import type { Environment } from '@yaakapp/api'; -import { useActiveEnvironmentId } from './useActiveEnvironmentId'; +import { useCallback, useMemo } from 'react'; +import { useSearchParams } from 'react-router-dom'; import { useEnvironments } from './useEnvironments'; -export function useActiveEnvironment(): Environment | null { - const id = useActiveEnvironmentId(); +export function useActiveEnvironment() { + const [id, setId] = useActiveEnvironmentId(); const environments = useEnvironments(); - return useMemo(() => environments.find((w) => w.id === id) ?? null, [environments, id]); + const environment = useMemo( + () => environments.find((w) => w.id === id) ?? null, + [environments, id], + ); + return [environment, setId] as const; +} + +export const QUERY_ENVIRONMENT_ID = 'environment_id'; + +export function useActiveEnvironmentId() { + // NOTE: This query param is accessed from Rust side, so do not change + const [params, setParams] = useSearchParams(); + const id = params.get(QUERY_ENVIRONMENT_ID); + + const setId = useCallback( + (id: string | null) => { + setParams((p) => { + const existing = Object.fromEntries(p); + if (id == null) { + delete existing[QUERY_ENVIRONMENT_ID]; + } else { + existing[QUERY_ENVIRONMENT_ID] = id; + } + return existing; + }); + }, + [setParams], + ); + + return [id, setId] as const; } diff --git a/src-web/hooks/useActiveEnvironmentId.ts b/src-web/hooks/useActiveEnvironmentId.ts deleted file mode 100644 index 71f14d33..00000000 --- a/src-web/hooks/useActiveEnvironmentId.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { useSearchParams } from 'react-router-dom'; - -export const QUERY_ENVIRONMENT_ID = 'environment_id'; - -export function useActiveEnvironmentId(): string | null { - const [params] = useSearchParams(); - const environmentId = params.get(QUERY_ENVIRONMENT_ID); - if (environmentId == null) { - return null; - } - - return environmentId; -} diff --git a/src-web/hooks/useActiveWorkspace.ts b/src-web/hooks/useActiveWorkspace.ts index e02563be..0e4a5c2c 100644 --- a/src-web/hooks/useActiveWorkspace.ts +++ b/src-web/hooks/useActiveWorkspace.ts @@ -1,6 +1,7 @@ import { useMemo } from 'react'; import type { Workspace } from '@yaakapp/api'; -import { useActiveWorkspaceId } from './useActiveWorkspaceId'; +import { useParams } from 'react-router-dom'; +import type { RouteParamsWorkspace } from './useAppRoutes'; import { useWorkspaces } from './useWorkspaces'; export function useActiveWorkspace(): Workspace | null { @@ -11,3 +12,8 @@ export function useActiveWorkspace(): Workspace | null { [workspaces, workspaceId], ); } + +function useActiveWorkspaceId(): string | null { + const { workspaceId } = useParams(); + return workspaceId ?? null; +} diff --git a/src-web/hooks/useAtiveWorkspaceChangedToast.tsx b/src-web/hooks/useActiveWorkspaceChangedToast.tsx similarity index 94% rename from src-web/hooks/useAtiveWorkspaceChangedToast.tsx rename to src-web/hooks/useActiveWorkspaceChangedToast.tsx index 2cb9d749..36caae6d 100644 --- a/src-web/hooks/useAtiveWorkspaceChangedToast.tsx +++ b/src-web/hooks/useActiveWorkspaceChangedToast.tsx @@ -3,7 +3,7 @@ import { InlineCode } from '../components/core/InlineCode'; import { useToast } from '../components/ToastContext'; import { useActiveWorkspace } from './useActiveWorkspace'; -export function useAtiveWorkspaceChangedToast() { +export function useActiveWorkspaceChangedToast() { const toast = useToast(); const activeWorkspace = useActiveWorkspace(); const [id, setId] = useState(activeWorkspace?.id ?? null); diff --git a/src-web/hooks/useActiveWorkspaceId.ts b/src-web/hooks/useActiveWorkspaceId.ts deleted file mode 100644 index 984a00c6..00000000 --- a/src-web/hooks/useActiveWorkspaceId.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { useParams } from 'react-router-dom'; -import type { RouteParamsWorkspace } from './useAppRoutes'; - -export function useActiveWorkspaceId(): string | null { - const { workspaceId } = useParams(); - return workspaceId ?? null; -} diff --git a/src-web/hooks/useAppRoutes.tsx b/src-web/hooks/useAppRoutes.tsx index 01cb29a3..d6cc47e7 100644 --- a/src-web/hooks/useAppRoutes.tsx +++ b/src-web/hooks/useAppRoutes.tsx @@ -1,13 +1,15 @@ +import type { Environment } from '@yaakapp/api'; import { useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; -import type { Environment } from '@yaakapp/api'; -import { QUERY_ENVIRONMENT_ID } from './useActiveEnvironmentId'; +import { QUERY_COOKIE_JAR_ID } from './useActiveCookieJar'; +import { QUERY_ENVIRONMENT_ID } from './useActiveEnvironment'; import { useActiveRequestId } from './useActiveRequestId'; -import { useActiveWorkspaceId } from './useActiveWorkspaceId'; +import { useActiveWorkspace } from './useActiveWorkspace'; export type RouteParamsWorkspace = { workspaceId: string; environmentId?: string; + cookieJarId?: string; }; export type RouteParamsRequest = RouteParamsWorkspace & { @@ -22,34 +24,35 @@ export const routePaths = { return `/workspaces/${workspaceId}/settings`; }, workspace( - { workspaceId, environmentId } = { + { workspaceId, environmentId, cookieJarId } = { workspaceId: ':workspaceId', environmentId: ':environmentId', + cookieJarId: ':cookieJarId', } as RouteParamsWorkspace, ) { - let path = `/workspaces/${workspaceId}`; - if (environmentId != null) { - path += `?${QUERY_ENVIRONMENT_ID}=${encodeURIComponent(environmentId)}`; - } - return path; + const path = `/workspaces/${workspaceId}`; + const params = new URLSearchParams(); + if (environmentId != null) params.set(QUERY_ENVIRONMENT_ID, environmentId); + if (cookieJarId != null) params.set(QUERY_COOKIE_JAR_ID, cookieJarId); + return `${path}?${params}`; }, request( - { workspaceId, environmentId, requestId } = { + { workspaceId, environmentId, requestId, cookieJarId } = { workspaceId: ':workspaceId', environmentId: ':environmentId', requestId: ':requestId', } as RouteParamsRequest, ) { - let path = `/workspaces/${workspaceId}/requests/${requestId}`; - if (environmentId != null) { - path += `?${QUERY_ENVIRONMENT_ID}=${encodeURIComponent(environmentId)}`; - } - return path; + const path = `/workspaces/${workspaceId}/requests/${requestId}`; + const params = new URLSearchParams(); + if (environmentId != null) params.set(QUERY_ENVIRONMENT_ID, environmentId); + if (cookieJarId != null) params.set(QUERY_COOKIE_JAR_ID, cookieJarId); + return `${path}?${params}`; }, }; export function useAppRoutes() { - const activeWorkspaceId = useActiveWorkspaceId(); + const activeWorkspace = useActiveWorkspace(); const requestId = useActiveRequestId(); const nav = useNavigate(); @@ -66,22 +69,22 @@ export function useAppRoutes() { const setEnvironment = useCallback( (environment: Environment | null) => { - if (activeWorkspaceId == null) { + if (activeWorkspace == null) { navigate('workspaces'); } else if (requestId == null) { navigate('workspace', { - workspaceId: activeWorkspaceId, + workspaceId: activeWorkspace.id, environmentId: environment == null ? undefined : environment.id, }); } else { navigate('request', { - workspaceId: activeWorkspaceId, + workspaceId: activeWorkspace.id, environmentId: environment == null ? undefined : environment.id, requestId, }); } }, - [navigate, activeWorkspaceId, requestId], + [navigate, activeWorkspace, requestId], ); return { diff --git a/src-web/hooks/useCookieJars.ts b/src-web/hooks/useCookieJars.ts index d17e0303..50949f4b 100644 --- a/src-web/hooks/useCookieJars.ts +++ b/src-web/hooks/useCookieJars.ts @@ -1,22 +1,22 @@ import { useQuery } from '@tanstack/react-query'; import type { CookieJar } from '../lib/models'; import { invokeCmd } from '../lib/tauri'; -import { useActiveWorkspaceId } from './useActiveWorkspaceId'; +import { useActiveWorkspace } from './useActiveWorkspace'; export function cookieJarsQueryKey({ workspaceId }: { workspaceId: string }) { return ['cookie_jars', { workspaceId }]; } export function useCookieJars() { - const workspaceId = useActiveWorkspaceId(); - return ( - useQuery({ - enabled: workspaceId != null, - queryKey: cookieJarsQueryKey({ workspaceId: workspaceId ?? 'n/a' }), - queryFn: async () => { - if (workspaceId == null) return []; - return (await invokeCmd('cmd_list_cookie_jars', { workspaceId })) as CookieJar[]; - }, - }).data ?? [] - ); + const workspace = useActiveWorkspace(); + return useQuery({ + enabled: workspace != null, + queryKey: cookieJarsQueryKey({ workspaceId: workspace?.id ?? 'n/a' }), + queryFn: async () => { + if (workspace == null) return []; + return (await invokeCmd('cmd_list_cookie_jars', { + workspaceId: workspace.id, + })) as CookieJar[]; + }, + }); } diff --git a/src-web/hooks/useCopyAsCurl.tsx b/src-web/hooks/useCopyAsCurl.tsx index 37fb18d2..9a31b048 100644 --- a/src-web/hooks/useCopyAsCurl.tsx +++ b/src-web/hooks/useCopyAsCurl.tsx @@ -1,15 +1,18 @@ import { useMutation } from '@tanstack/react-query'; import { invokeCmd } from '../lib/tauri'; -import { useActiveEnvironmentId } from './useActiveEnvironmentId'; +import { useActiveEnvironment } from './useActiveEnvironment'; import { useCopy } from './useCopy'; export function useCopyAsCurl(requestId: string) { const copy = useCopy(); - const environmentId = useActiveEnvironmentId(); + const [environment] = useActiveEnvironment(); return useMutation({ mutationKey: ['copy_as_curl', requestId], mutationFn: async () => { - const cmd: string = await invokeCmd('cmd_request_to_curl', { requestId, environmentId }); + const cmd: string = await invokeCmd('cmd_request_to_curl', { + requestId, + environmentId: environment?.id, + }); copy(cmd); return cmd; }, diff --git a/src-web/hooks/useCreateCookieJar.ts b/src-web/hooks/useCreateCookieJar.ts index 5054b618..c2622569 100644 --- a/src-web/hooks/useCreateCookieJar.ts +++ b/src-web/hooks/useCreateCookieJar.ts @@ -1,20 +1,18 @@ -import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useMutation } from '@tanstack/react-query'; import { trackEvent } from '../lib/analytics'; import type { CookieJar } from '../lib/models'; import { invokeCmd } from '../lib/tauri'; -import { useActiveWorkspaceId } from './useActiveWorkspaceId'; -import { cookieJarsQueryKey } from './useCookieJars'; +import { useActiveWorkspace } from './useActiveWorkspace'; import { usePrompt } from './usePrompt'; export function useCreateCookieJar() { - const workspaceId = useActiveWorkspaceId(); - const queryClient = useQueryClient(); + const workspace = useActiveWorkspace(); const prompt = usePrompt(); return useMutation({ mutationKey: ['create_cookie_jar'], mutationFn: async () => { - if (workspaceId === null) { + if (workspace === null) { throw new Error("Cannot create cookie jar when there's no active workspace"); } const name = await prompt({ @@ -25,14 +23,8 @@ export function useCreateCookieJar() { label: 'Name', defaultValue: 'My Jar', }); - return invokeCmd('cmd_create_cookie_jar', { workspaceId, name }); + return invokeCmd('cmd_create_cookie_jar', { workspaceId: workspace.id, name }); }, onSettled: () => trackEvent('cookie_jar', 'create'), - onSuccess: async (cookieJar) => { - queryClient.setQueryData( - cookieJarsQueryKey({ workspaceId: cookieJar.workspaceId }), - (items) => [...(items ?? []), cookieJar], - ); - }, }); } diff --git a/src-web/hooks/useCreateEnvironment.ts b/src-web/hooks/useCreateEnvironment.ts index 9cb8ab44..fe58e170 100644 --- a/src-web/hooks/useCreateEnvironment.ts +++ b/src-web/hooks/useCreateEnvironment.ts @@ -1,17 +1,15 @@ -import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { trackEvent } from '../lib/analytics'; +import { useMutation } from '@tanstack/react-query'; import type { Environment } from '@yaakapp/api'; +import { trackEvent } from '../lib/analytics'; import { invokeCmd } from '../lib/tauri'; -import { useActiveWorkspaceId } from './useActiveWorkspaceId'; -import { useAppRoutes } from './useAppRoutes'; -import { environmentsQueryKey } from './useEnvironments'; +import { useActiveEnvironment } from './useActiveEnvironment'; +import { useActiveWorkspace } from './useActiveWorkspace'; import { usePrompt } from './usePrompt'; export function useCreateEnvironment() { - const routes = useAppRoutes(); + const [, setActiveEnvironmentId] = useActiveEnvironment(); const prompt = usePrompt(); - const workspaceId = useActiveWorkspaceId(); - const queryClient = useQueryClient(); + const workspace = useActiveWorkspace(); return useMutation({ mutationKey: ['create_environment'], @@ -25,16 +23,16 @@ export function useCreateEnvironment() { placeholder: 'My Environment', defaultValue: 'My Environment', }); - return invokeCmd('cmd_create_environment', { name, variables: [], workspaceId }); + return invokeCmd('cmd_create_environment', { + name, + variables: [], + workspaceId: workspace?.id, + }); }, onSettled: () => trackEvent('environment', 'create'), onSuccess: async (environment) => { - if (workspaceId == null) return; - routes.setEnvironment(environment); - queryClient.setQueryData( - environmentsQueryKey({ workspaceId }), - (environments) => [...(environments ?? []), environment], - ); + if (workspace == null) return; + setActiveEnvironmentId(environment.id); }, }); } diff --git a/src-web/hooks/useCreateFolder.ts b/src-web/hooks/useCreateFolder.ts index d6ee29b8..470a7edc 100644 --- a/src-web/hooks/useCreateFolder.ts +++ b/src-web/hooks/useCreateFolder.ts @@ -1,22 +1,20 @@ -import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { trackEvent } from '../lib/analytics'; +import { useMutation } from '@tanstack/react-query'; import type { Folder } from '@yaakapp/api'; +import { trackEvent } from '../lib/analytics'; import { invokeCmd } from '../lib/tauri'; import { useActiveRequest } from './useActiveRequest'; -import { useActiveWorkspaceId } from './useActiveWorkspaceId'; -import { foldersQueryKey } from './useFolders'; +import { useActiveWorkspace } from './useActiveWorkspace'; import { usePrompt } from './usePrompt'; export function useCreateFolder() { - const workspaceId = useActiveWorkspaceId(); + const workspace = useActiveWorkspace(); const activeRequest = useActiveRequest(); - const queryClient = useQueryClient(); const prompt = usePrompt(); return useMutation>>({ mutationKey: ['create_folder'], mutationFn: async (patch) => { - if (workspaceId === null) { + if (workspace === null) { throw new Error("Cannot create folder when there's no active workspace"); } patch.name = @@ -32,13 +30,8 @@ export function useCreateFolder() { })); patch.sortPriority = patch.sortPriority || -Date.now(); patch.folderId = patch.folderId || activeRequest?.folderId; - return invokeCmd('cmd_create_folder', { workspaceId, ...patch }); + return invokeCmd('cmd_create_folder', { workspaceId: workspace.id, ...patch }); }, onSettled: () => trackEvent('folder', 'create'), - onSuccess: async (request) => { - await queryClient.invalidateQueries({ - queryKey: foldersQueryKey({ workspaceId: request.workspaceId }), - }); - }, }); } diff --git a/src-web/hooks/useCreateGrpcRequest.ts b/src-web/hooks/useCreateGrpcRequest.ts index 3758726e..174d8245 100644 --- a/src-web/hooks/useCreateGrpcRequest.ts +++ b/src-web/hooks/useCreateGrpcRequest.ts @@ -1,15 +1,15 @@ import { useMutation } from '@tanstack/react-query'; -import { trackEvent } from '../lib/analytics'; import type { GrpcRequest } from '@yaakapp/api'; +import { trackEvent } from '../lib/analytics'; import { invokeCmd } from '../lib/tauri'; -import { useActiveEnvironmentId } from './useActiveEnvironmentId'; +import { useActiveEnvironment } from './useActiveEnvironment'; import { useActiveRequest } from './useActiveRequest'; -import { useActiveWorkspaceId } from './useActiveWorkspaceId'; +import { useActiveWorkspace } from './useActiveWorkspace'; import { useAppRoutes } from './useAppRoutes'; export function useCreateGrpcRequest() { - const workspaceId = useActiveWorkspaceId(); - const activeEnvironmentId = useActiveEnvironmentId(); + const workspace = useActiveWorkspace(); + const [activeEnvironment] = useActiveEnvironment(); const activeRequest = useActiveRequest(); const routes = useAppRoutes(); @@ -20,12 +20,12 @@ export function useCreateGrpcRequest() { >({ mutationKey: ['create_grpc_request'], mutationFn: (patch) => { - if (workspaceId === null) { + if (workspace === null) { throw new Error("Cannot create grpc request when there's no active workspace"); } if (patch.sortPriority === undefined) { if (activeRequest != null) { - // Place above currently-active request + // Place above currently active request patch.sortPriority = activeRequest.sortPriority + 0.0001; } else { // Place at the very top @@ -33,14 +33,18 @@ export function useCreateGrpcRequest() { } } patch.folderId = patch.folderId || activeRequest?.folderId; - return invokeCmd('cmd_create_grpc_request', { workspaceId, name: '', ...patch }); + return invokeCmd('cmd_create_grpc_request', { + workspaceId: workspace.id, + name: '', + ...patch, + }); }, onSettled: () => trackEvent('grpc_request', 'create'), onSuccess: async (request) => { routes.navigate('request', { workspaceId: request.workspaceId, requestId: request.id, - environmentId: activeEnvironmentId ?? undefined, + environmentId: activeEnvironment?.id, }); }, }); diff --git a/src-web/hooks/useCreateHttpRequest.ts b/src-web/hooks/useCreateHttpRequest.ts index c7e62672..b5f20660 100644 --- a/src-web/hooks/useCreateHttpRequest.ts +++ b/src-web/hooks/useCreateHttpRequest.ts @@ -1,22 +1,22 @@ import { useMutation } from '@tanstack/react-query'; -import { trackEvent } from '../lib/analytics'; import type { HttpRequest } from '@yaakapp/api'; +import { trackEvent } from '../lib/analytics'; import { invokeCmd } from '../lib/tauri'; -import { useActiveEnvironmentId } from './useActiveEnvironmentId'; +import { useActiveEnvironment } from './useActiveEnvironment'; import { useActiveRequest } from './useActiveRequest'; -import { useActiveWorkspaceId } from './useActiveWorkspaceId'; +import { useActiveWorkspace } from './useActiveWorkspace'; import { useAppRoutes } from './useAppRoutes'; export function useCreateHttpRequest() { - const workspaceId = useActiveWorkspaceId(); - const activeEnvironmentId = useActiveEnvironmentId(); + const workspace = useActiveWorkspace(); + const [activeEnvironment] = useActiveEnvironment(); const activeRequest = useActiveRequest(); const routes = useAppRoutes(); return useMutation>({ mutationKey: ['create_http_request'], mutationFn: (patch = {}) => { - if (workspaceId === null) { + if (workspace === null) { throw new Error("Cannot create request when there's no active workspace"); } if (patch.sortPriority === undefined) { @@ -29,14 +29,16 @@ export function useCreateHttpRequest() { } } patch.folderId = patch.folderId || activeRequest?.folderId; - return invokeCmd('cmd_create_http_request', { request: { workspaceId, ...patch } }); + return invokeCmd('cmd_create_http_request', { + request: { workspaceId: workspace.id, ...patch }, + }); }, onSettled: () => trackEvent('http_request', 'create'), onSuccess: async (request) => { routes.navigate('request', { workspaceId: request.workspaceId, requestId: request.id, - environmentId: activeEnvironmentId ?? undefined, + environmentId: activeEnvironment?.id, }); }, }); diff --git a/src-web/hooks/useDeleteAnyGrpcRequest.tsx b/src-web/hooks/useDeleteAnyGrpcRequest.tsx index 308a6e8a..fa5416a2 100644 --- a/src-web/hooks/useDeleteAnyGrpcRequest.tsx +++ b/src-web/hooks/useDeleteAnyGrpcRequest.tsx @@ -1,15 +1,13 @@ -import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useMutation } from '@tanstack/react-query'; +import type { GrpcRequest } from '@yaakapp/api'; import { InlineCode } from '../components/core/InlineCode'; import { trackEvent } from '../lib/analytics'; import { fallbackRequestName } from '../lib/fallbackRequestName'; -import type { GrpcRequest } from '@yaakapp/api'; import { getGrpcRequest } from '../lib/store'; import { invokeCmd } from '../lib/tauri'; import { useConfirm } from './useConfirm'; -import { grpcRequestsQueryKey } from './useGrpcRequests'; export function useDeleteAnyGrpcRequest() { - const queryClient = useQueryClient(); const confirm = useConfirm(); return useMutation({ @@ -32,13 +30,5 @@ export function useDeleteAnyGrpcRequest() { return invokeCmd('cmd_delete_grpc_request', { requestId: id }); }, onSettled: () => trackEvent('grpc_request', 'delete'), - onSuccess: async (request) => { - if (request === null) return; - - const { workspaceId, id: requestId } = request; - queryClient.setQueryData(grpcRequestsQueryKey({ workspaceId }), (requests) => - (requests ?? []).filter((r) => r.id !== requestId), - ); - }, }); } diff --git a/src-web/hooks/useDeleteAnyHttpRequest.tsx b/src-web/hooks/useDeleteAnyHttpRequest.tsx index 98dd9ea9..920126d2 100644 --- a/src-web/hooks/useDeleteAnyHttpRequest.tsx +++ b/src-web/hooks/useDeleteAnyHttpRequest.tsx @@ -1,16 +1,13 @@ -import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useMutation } from '@tanstack/react-query'; +import type { HttpRequest } from '@yaakapp/api'; import { InlineCode } from '../components/core/InlineCode'; import { trackEvent } from '../lib/analytics'; import { fallbackRequestName } from '../lib/fallbackRequestName'; -import type { HttpRequest } from '@yaakapp/api'; import { getHttpRequest } from '../lib/store'; import { invokeCmd } from '../lib/tauri'; import { useConfirm } from './useConfirm'; -import { httpRequestsQueryKey } from './useHttpRequests'; -import { httpResponsesQueryKey } from './useHttpResponses'; export function useDeleteAnyHttpRequest() { - const queryClient = useQueryClient(); const confirm = useConfirm(); return useMutation({ @@ -33,15 +30,5 @@ export function useDeleteAnyHttpRequest() { return invokeCmd('cmd_delete_http_request', { requestId: id }); }, onSettled: () => trackEvent('http_request', 'delete'), - onSuccess: async (request) => { - // Was it cancelled? - if (request === null) return; - - const { workspaceId, id: requestId } = request; - queryClient.setQueryData(httpResponsesQueryKey({ requestId }), []); // Responses were deleted - queryClient.setQueryData(httpRequestsQueryKey({ workspaceId }), (requests) => - (requests ?? []).filter((r) => r.id !== requestId), - ); - }, }); } diff --git a/src-web/hooks/useDeleteCookieJar.tsx b/src-web/hooks/useDeleteCookieJar.tsx index 27572482..6de10169 100644 --- a/src-web/hooks/useDeleteCookieJar.tsx +++ b/src-web/hooks/useDeleteCookieJar.tsx @@ -1,13 +1,11 @@ -import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useMutation } from '@tanstack/react-query'; import { InlineCode } from '../components/core/InlineCode'; import { trackEvent } from '../lib/analytics'; import type { CookieJar } from '../lib/models'; import { invokeCmd } from '../lib/tauri'; import { useConfirm } from './useConfirm'; -import { cookieJarsQueryKey } from './useCookieJars'; export function useDeleteCookieJar(cookieJar: CookieJar | null) { - const queryClient = useQueryClient(); const confirm = useConfirm(); return useMutation({ @@ -27,13 +25,5 @@ export function useDeleteCookieJar(cookieJar: CookieJar | null) { return invokeCmd('cmd_delete_cookie_jar', { cookieJarId: cookieJar?.id }); }, onSettled: () => trackEvent('cookie_jar', 'delete'), - onSuccess: async (cookieJar) => { - if (cookieJar === null) return; - - const { id: cookieJarId, workspaceId } = cookieJar; - queryClient.setQueryData(cookieJarsQueryKey({ workspaceId }), (cookieJars) => - cookieJars?.filter((e) => e.id !== cookieJarId), - ); - }, }); } diff --git a/src-web/hooks/useDeleteEnvironment.tsx b/src-web/hooks/useDeleteEnvironment.tsx index a4478f74..938e32ed 100644 --- a/src-web/hooks/useDeleteEnvironment.tsx +++ b/src-web/hooks/useDeleteEnvironment.tsx @@ -1,13 +1,11 @@ -import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useMutation } from '@tanstack/react-query'; +import type { Environment } from '@yaakapp/api'; import { InlineCode } from '../components/core/InlineCode'; import { trackEvent } from '../lib/analytics'; -import type { Environment, Workspace } from '@yaakapp/api'; import { invokeCmd } from '../lib/tauri'; import { useConfirm } from './useConfirm'; -import { environmentsQueryKey } from './useEnvironments'; export function useDeleteEnvironment(environment: Environment | null) { - const queryClient = useQueryClient(); const confirm = useConfirm(); return useMutation({ @@ -27,13 +25,5 @@ export function useDeleteEnvironment(environment: Environment | null) { return invokeCmd('cmd_delete_environment', { environmentId: environment?.id }); }, onSettled: () => trackEvent('environment', 'delete'), - onSuccess: async (environment) => { - if (environment === null) return; - - const { id: environmentId, workspaceId } = environment; - queryClient.setQueryData(environmentsQueryKey({ workspaceId }), (environments) => - environments?.filter((e) => e.id !== environmentId), - ); - }, }); } diff --git a/src-web/hooks/useDeleteFolder.tsx b/src-web/hooks/useDeleteFolder.tsx index b13c8b83..12c26c9c 100644 --- a/src-web/hooks/useDeleteFolder.tsx +++ b/src-web/hooks/useDeleteFolder.tsx @@ -1,15 +1,12 @@ -import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useMutation } from '@tanstack/react-query'; +import type { Folder } from '@yaakapp/api'; import { InlineCode } from '../components/core/InlineCode'; import { trackEvent } from '../lib/analytics'; -import type { Folder } from '@yaakapp/api'; import { getFolder } from '../lib/store'; import { invokeCmd } from '../lib/tauri'; import { useConfirm } from './useConfirm'; -import { foldersQueryKey } from './useFolders'; -import { httpRequestsQueryKey } from './useHttpRequests'; export function useDeleteFolder(id: string | null) { - const queryClient = useQueryClient(); const confirm = useConfirm(); return useMutation({ @@ -30,15 +27,5 @@ export function useDeleteFolder(id: string | null) { return invokeCmd('cmd_delete_folder', { folderId: id }); }, onSettled: () => trackEvent('folder', 'delete'), - onSuccess: async (folder) => { - // Was it cancelled? - if (folder === null) return; - - const { workspaceId } = folder; - - // Nesting makes it hard to clean things up, so just clear everything that could have been deleted - await queryClient.invalidateQueries({ queryKey: httpRequestsQueryKey({ workspaceId }) }); - await queryClient.invalidateQueries({ queryKey: foldersQueryKey({ workspaceId }) }); - }, }); } diff --git a/src-web/hooks/useDeleteGrpcConnection.ts b/src-web/hooks/useDeleteGrpcConnection.ts index 842a1cff..c48bbf17 100644 --- a/src-web/hooks/useDeleteGrpcConnection.ts +++ b/src-web/hooks/useDeleteGrpcConnection.ts @@ -1,22 +1,14 @@ -import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { trackEvent } from '../lib/analytics'; +import { useMutation } from '@tanstack/react-query'; import type { GrpcConnection } from '@yaakapp/api'; +import { trackEvent } from '../lib/analytics'; import { invokeCmd } from '../lib/tauri'; -import { grpcConnectionsQueryKey } from './useGrpcConnections'; export function useDeleteGrpcConnection(id: string | null) { - const queryClient = useQueryClient(); return useMutation({ mutationKey: ['delete_grpc_connection', id], mutationFn: async () => { return await invokeCmd('cmd_delete_grpc_connection', { id: id }); }, onSettled: () => trackEvent('grpc_connection', 'delete'), - onSuccess: ({ requestId, id: connectionId }) => { - queryClient.setQueryData( - grpcConnectionsQueryKey({ requestId }), - (connections) => (connections ?? []).filter((c) => c.id !== connectionId), - ); - }, }); } diff --git a/src-web/hooks/useDeleteGrpcConnections.ts b/src-web/hooks/useDeleteGrpcConnections.ts index 296b8aef..173e734e 100644 --- a/src-web/hooks/useDeleteGrpcConnections.ts +++ b/src-web/hooks/useDeleteGrpcConnections.ts @@ -1,10 +1,8 @@ -import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useMutation } from '@tanstack/react-query'; import { trackEvent } from '../lib/analytics'; import { invokeCmd } from '../lib/tauri'; -import { grpcConnectionsQueryKey } from './useGrpcConnections'; export function useDeleteGrpcConnections(requestId?: string) { - const queryClient = useQueryClient(); return useMutation({ mutationKey: ['delete_grpc_connections', requestId], mutationFn: async () => { @@ -12,9 +10,5 @@ export function useDeleteGrpcConnections(requestId?: string) { await invokeCmd('cmd_delete_all_grpc_connections', { requestId }); }, onSettled: () => trackEvent('grpc_connection', 'delete_many'), - onSuccess: async () => { - if (requestId === undefined) return; - queryClient.setQueryData(grpcConnectionsQueryKey({ requestId }), []); - }, }); } diff --git a/src-web/hooks/useDeleteHttpResponse.ts b/src-web/hooks/useDeleteHttpResponse.ts index 20f581d9..108fc50b 100644 --- a/src-web/hooks/useDeleteHttpResponse.ts +++ b/src-web/hooks/useDeleteHttpResponse.ts @@ -1,21 +1,14 @@ -import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { trackEvent } from '../lib/analytics'; +import { useMutation } from '@tanstack/react-query'; import type { HttpResponse } from '@yaakapp/api'; +import { trackEvent } from '../lib/analytics'; import { invokeCmd } from '../lib/tauri'; -import { httpResponsesQueryKey } from './useHttpResponses'; export function useDeleteHttpResponse(id: string | null) { - const queryClient = useQueryClient(); return useMutation({ mutationKey: ['delete_http_response', id], mutationFn: async () => { return await invokeCmd('cmd_delete_http_response', { id: id }); }, onSettled: () => trackEvent('http_response', 'delete'), - onSuccess: ({ requestId, id: responseId }) => { - queryClient.setQueryData(httpResponsesQueryKey({ requestId }), (responses) => - (responses ?? []).filter((response) => response.id !== responseId), - ); - }, }); } diff --git a/src-web/hooks/useDeleteHttpResponses.ts b/src-web/hooks/useDeleteHttpResponses.ts index cc21dd9e..728c64d8 100644 --- a/src-web/hooks/useDeleteHttpResponses.ts +++ b/src-web/hooks/useDeleteHttpResponses.ts @@ -1,10 +1,8 @@ -import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useMutation } from '@tanstack/react-query'; import { trackEvent } from '../lib/analytics'; import { invokeCmd } from '../lib/tauri'; -import { httpResponsesQueryKey } from './useHttpResponses'; export function useDeleteHttpResponses(requestId?: string) { - const queryClient = useQueryClient(); return useMutation({ mutationKey: ['delete_http_responses', requestId], mutationFn: async () => { @@ -12,9 +10,5 @@ export function useDeleteHttpResponses(requestId?: string) { await invokeCmd('cmd_delete_all_http_responses', { requestId }); }, onSettled: () => trackEvent('http_response', 'delete_many'), - onSuccess: async () => { - if (requestId === undefined) return; - queryClient.setQueryData(httpResponsesQueryKey({ requestId }), []); - }, }); } diff --git a/src-web/hooks/useDeleteWorkspace.tsx b/src-web/hooks/useDeleteWorkspace.tsx index 7a160ba8..b136b45f 100644 --- a/src-web/hooks/useDeleteWorkspace.tsx +++ b/src-web/hooks/useDeleteWorkspace.tsx @@ -1,17 +1,14 @@ -import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useMutation } from '@tanstack/react-query'; +import type { Workspace } from '@yaakapp/api'; import { InlineCode } from '../components/core/InlineCode'; import { trackEvent } from '../lib/analytics'; -import type { Workspace } from '@yaakapp/api'; import { invokeCmd } from '../lib/tauri'; -import { useActiveWorkspaceId } from './useActiveWorkspaceId'; +import { useActiveWorkspace } from './useActiveWorkspace'; import { useAppRoutes } from './useAppRoutes'; import { useConfirm } from './useConfirm'; -import { httpRequestsQueryKey } from './useHttpRequests'; -import { workspacesQueryKey } from './useWorkspaces'; export function useDeleteWorkspace(workspace: Workspace | null) { - const queryClient = useQueryClient(); - const activeWorkspaceId = useActiveWorkspaceId(); + const activeWorkspace = useActiveWorkspace(); const routes = useAppRoutes(); const confirm = useConfirm(); @@ -36,16 +33,9 @@ export function useDeleteWorkspace(workspace: Workspace | null) { if (workspace === null) return; const { id: workspaceId } = workspace; - queryClient.setQueryData(workspacesQueryKey({}), (workspaces) => - workspaces?.filter((workspace) => workspace.id !== workspaceId), - ); - if (workspaceId === activeWorkspaceId) { + if (workspaceId === activeWorkspace?.id) { routes.navigate('workspaces'); } - - // Also clean up other things that may have been deleted - queryClient.setQueryData(httpRequestsQueryKey({ workspaceId }), []); - await queryClient.invalidateQueries({ queryKey: httpRequestsQueryKey({ workspaceId }) }); }, }); } diff --git a/src-web/hooks/useDuplicateGrpcRequest.ts b/src-web/hooks/useDuplicateGrpcRequest.ts index 11c6d858..d7d8868b 100644 --- a/src-web/hooks/useDuplicateGrpcRequest.ts +++ b/src-web/hooks/useDuplicateGrpcRequest.ts @@ -1,10 +1,10 @@ import { useMutation } from '@tanstack/react-query'; +import type { GrpcRequest } from '@yaakapp/api'; import { trackEvent } from '../lib/analytics'; import { setKeyValue } from '../lib/keyValueStore'; -import type { GrpcRequest } from '@yaakapp/api'; import { invokeCmd } from '../lib/tauri'; -import { useActiveEnvironmentId } from './useActiveEnvironmentId'; -import { useActiveWorkspaceId } from './useActiveWorkspaceId'; +import { useActiveEnvironment } from './useActiveEnvironment'; +import { useActiveWorkspace } from './useActiveWorkspace'; import { useAppRoutes } from './useAppRoutes'; import { protoFilesArgs, useGrpcProtoFiles } from './useGrpcProtoFiles'; @@ -15,8 +15,8 @@ export function useDuplicateGrpcRequest({ id: string | null; navigateAfter: boolean; }) { - const activeWorkspaceId = useActiveWorkspaceId(); - const activeEnvironmentId = useActiveEnvironmentId(); + const activeWorkspace = useActiveWorkspace(); + const [activeEnvironment] = useActiveEnvironment(); const routes = useAppRoutes(); const protoFiles = useGrpcProtoFiles(id); return useMutation({ @@ -30,11 +30,11 @@ export function useDuplicateGrpcRequest({ // Also copy proto files to new request await setKeyValue({ ...protoFilesArgs(request.id), value: protoFiles.value ?? [] }); - if (navigateAfter && activeWorkspaceId !== null) { + if (navigateAfter && activeWorkspace !== null) { routes.navigate('request', { - workspaceId: activeWorkspaceId, + workspaceId: activeWorkspace.id, requestId: request.id, - environmentId: activeEnvironmentId ?? undefined, + environmentId: activeEnvironment?.id, }); } }, diff --git a/src-web/hooks/useDuplicateHttpRequest.ts b/src-web/hooks/useDuplicateHttpRequest.ts index a44a2515..b48584f2 100644 --- a/src-web/hooks/useDuplicateHttpRequest.ts +++ b/src-web/hooks/useDuplicateHttpRequest.ts @@ -1,9 +1,9 @@ import { useMutation } from '@tanstack/react-query'; -import { trackEvent } from '../lib/analytics'; import type { HttpRequest } from '@yaakapp/api'; +import { trackEvent } from '../lib/analytics'; import { invokeCmd } from '../lib/tauri'; -import { useActiveEnvironmentId } from './useActiveEnvironmentId'; -import { useActiveWorkspaceId } from './useActiveWorkspaceId'; +import { useActiveEnvironment } from './useActiveEnvironment'; +import { useActiveWorkspace } from './useActiveWorkspace'; import { useAppRoutes } from './useAppRoutes'; export function useDuplicateHttpRequest({ @@ -13,8 +13,8 @@ export function useDuplicateHttpRequest({ id: string | null; navigateAfter: boolean; }) { - const activeWorkspaceId = useActiveWorkspaceId(); - const activeEnvironmentId = useActiveEnvironmentId(); + const activeWorkspace = useActiveWorkspace(); + const [activeEnvironment] = useActiveEnvironment(); const routes = useAppRoutes(); return useMutation({ mutationKey: ['duplicate_http_request', id], @@ -24,11 +24,11 @@ export function useDuplicateHttpRequest({ }, onSettled: () => trackEvent('http_request', 'duplicate'), onSuccess: async (request) => { - if (navigateAfter && activeWorkspaceId !== null) { + if (navigateAfter && activeWorkspace !== null) { routes.navigate('request', { - workspaceId: activeWorkspaceId, + workspaceId: activeWorkspace.id, requestId: request.id, - environmentId: activeEnvironmentId ?? undefined, + environmentId: activeEnvironment?.id, }); } }, diff --git a/src-web/hooks/useEnvironments.ts b/src-web/hooks/useEnvironments.ts index 4cfea2f0..fd7aa64f 100644 --- a/src-web/hooks/useEnvironments.ts +++ b/src-web/hooks/useEnvironments.ts @@ -1,21 +1,23 @@ import { useQuery } from '@tanstack/react-query'; import type { Environment } from '@yaakapp/api'; import { invokeCmd } from '../lib/tauri'; -import { useActiveWorkspaceId } from './useActiveWorkspaceId'; +import { useActiveWorkspace } from './useActiveWorkspace'; export function environmentsQueryKey({ workspaceId }: { workspaceId: string }) { return ['environments', { workspaceId }]; } export function useEnvironments() { - const workspaceId = useActiveWorkspaceId(); + const workspace = useActiveWorkspace(); return ( useQuery({ - enabled: workspaceId != null, - queryKey: environmentsQueryKey({ workspaceId: workspaceId ?? 'n/a' }), + enabled: workspace != null, + queryKey: environmentsQueryKey({ workspaceId: workspace?.id ?? 'n/a' }), queryFn: async () => { - if (workspaceId == null) return []; - return (await invokeCmd('cmd_list_environments', { workspaceId })) as Environment[]; + if (workspace == null) return []; + return (await invokeCmd('cmd_list_environments', { + workspaceId: workspace.id, + })) as Environment[]; }, }).data ?? [] ); diff --git a/src-web/hooks/useFloatingSidebarHidden.ts b/src-web/hooks/useFloatingSidebarHidden.ts index 610b40e5..3c8b5a4e 100644 --- a/src-web/hooks/useFloatingSidebarHidden.ts +++ b/src-web/hooks/useFloatingSidebarHidden.ts @@ -1,11 +1,11 @@ -import { useActiveWorkspaceId } from './useActiveWorkspaceId'; +import { useActiveWorkspace } from './useActiveWorkspace'; import { useKeyValue } from './useKeyValue'; export function useFloatingSidebarHidden() { - const activeWorkspaceId = useActiveWorkspaceId(); + const activeWorkspace = useActiveWorkspace(); const { set, value } = useKeyValue({ namespace: 'no_sync', - key: ['floating_sidebar_hidden', activeWorkspaceId ?? 'n/a'], + key: ['floating_sidebar_hidden', activeWorkspace?.id ?? 'n/a'], fallback: false, }); diff --git a/src-web/hooks/useFolders.ts b/src-web/hooks/useFolders.ts index ce869de1..f459f9f9 100644 --- a/src-web/hooks/useFolders.ts +++ b/src-web/hooks/useFolders.ts @@ -1,21 +1,21 @@ import { useQuery } from '@tanstack/react-query'; import type { Folder } from '@yaakapp/api'; import { invokeCmd } from '../lib/tauri'; -import { useActiveWorkspaceId } from './useActiveWorkspaceId'; +import { useActiveWorkspace } from './useActiveWorkspace'; export function foldersQueryKey({ workspaceId }: { workspaceId: string }) { return ['folders', { workspaceId }]; } export function useFolders() { - const workspaceId = useActiveWorkspaceId(); + const workspace = useActiveWorkspace(); return ( useQuery({ - enabled: workspaceId != null, - queryKey: foldersQueryKey({ workspaceId: workspaceId ?? 'n/a' }), + enabled: workspace != null, + queryKey: foldersQueryKey({ workspaceId: workspace?.id ?? 'n/a' }), queryFn: async () => { - if (workspaceId == null) return []; - return (await invokeCmd('cmd_list_folders', { workspaceId })) as Folder[]; + if (workspace == null) return []; + return (await invokeCmd('cmd_list_folders', { workspaceId: workspace.id })) as Folder[]; }, }).data ?? [] ); diff --git a/src-web/hooks/useGrpc.ts b/src-web/hooks/useGrpc.ts index 1413c111..d72f480e 100644 --- a/src-web/hooks/useGrpc.ts +++ b/src-web/hooks/useGrpc.ts @@ -1,10 +1,10 @@ import { useMutation, useQuery } from '@tanstack/react-query'; import { emit } from '@tauri-apps/api/event'; +import type { GrpcConnection, GrpcRequest } from '@yaakapp/api'; import { trackEvent } from '../lib/analytics'; import { minPromiseMillis } from '../lib/minPromiseMillis'; -import type { GrpcConnection, GrpcRequest } from '@yaakapp/api'; import { invokeCmd } from '../lib/tauri'; -import { useActiveEnvironmentId } from './useActiveEnvironmentId'; +import { useActiveEnvironment } from './useActiveEnvironment'; import { useDebouncedValue } from './useDebouncedValue'; export interface ReflectResponseService { @@ -18,12 +18,12 @@ export function useGrpc( protoFiles: string[], ) { const requestId = req?.id ?? 'n/a'; - const environmentId = useActiveEnvironmentId(); + const [environment] = useActiveEnvironment(); const go = useMutation({ mutationKey: ['grpc_go', conn?.id], mutationFn: async () => - await invokeCmd('cmd_grpc_go', { requestId, environmentId, protoFiles }), + await invokeCmd('cmd_grpc_go', { requestId, environmentId: environment?.id, protoFiles }), onSettled: () => trackEvent('grpc_request', 'send'), }); diff --git a/src-web/hooks/useGrpcRequests.ts b/src-web/hooks/useGrpcRequests.ts index 71620070..f0af64a2 100644 --- a/src-web/hooks/useGrpcRequests.ts +++ b/src-web/hooks/useGrpcRequests.ts @@ -1,21 +1,23 @@ import { useQuery } from '@tanstack/react-query'; import type { GrpcRequest } from '@yaakapp/api'; import { invokeCmd } from '../lib/tauri'; -import { useActiveWorkspaceId } from './useActiveWorkspaceId'; +import { useActiveWorkspace } from './useActiveWorkspace'; export function grpcRequestsQueryKey({ workspaceId }: { workspaceId: string }) { return ['grpc_requests', { workspaceId }]; } export function useGrpcRequests() { - const workspaceId = useActiveWorkspaceId(); + const workspace = useActiveWorkspace(); return ( useQuery({ - enabled: workspaceId != null, - queryKey: grpcRequestsQueryKey({ workspaceId: workspaceId ?? 'n/a' }), + enabled: workspace != null, + queryKey: grpcRequestsQueryKey({ workspaceId: workspace?.id ?? 'n/a' }), queryFn: async () => { - if (workspaceId == null) return []; - return (await invokeCmd('cmd_list_grpc_requests', { workspaceId })) as GrpcRequest[]; + if (workspace == null) return []; + return (await invokeCmd('cmd_list_grpc_requests', { + workspaceId: workspace.id, + })) as GrpcRequest[]; }, }).data ?? [] ); diff --git a/src-web/hooks/useHttpRequests.ts b/src-web/hooks/useHttpRequests.ts index 44f9fe1f..30226ece 100644 --- a/src-web/hooks/useHttpRequests.ts +++ b/src-web/hooks/useHttpRequests.ts @@ -1,21 +1,23 @@ import { useQuery } from '@tanstack/react-query'; import type { HttpRequest } from '@yaakapp/api'; import { invokeCmd } from '../lib/tauri'; -import { useActiveWorkspaceId } from './useActiveWorkspaceId'; +import { useActiveWorkspace } from './useActiveWorkspace'; export function httpRequestsQueryKey({ workspaceId }: { workspaceId: string }) { return ['http_requests', { workspaceId }]; } export function useHttpRequests() { - const workspaceId = useActiveWorkspaceId(); + const workspace = useActiveWorkspace(); return ( useQuery({ - enabled: workspaceId != null, - queryKey: httpRequestsQueryKey({ workspaceId: workspaceId ?? 'n/a' }), + enabled: workspace != null, + queryKey: httpRequestsQueryKey({ workspaceId: workspace?.id ?? 'n/a' }), queryFn: async () => { - if (workspaceId == null) return []; - return (await invokeCmd('cmd_list_http_requests', { workspaceId })) as HttpRequest[]; + if (workspace == null) return []; + return (await invokeCmd('cmd_list_http_requests', { + workspaceId: workspace.id, + })) as HttpRequest[]; }, }).data ?? [] ); diff --git a/src-web/hooks/useImportCurl.ts b/src-web/hooks/useImportCurl.ts index f90b8598..17e339f0 100644 --- a/src-web/hooks/useImportCurl.ts +++ b/src-web/hooks/useImportCurl.ts @@ -1,13 +1,13 @@ import { useMutation } from '@tanstack/react-query'; import { useToast } from '../components/ToastContext'; import { invokeCmd } from '../lib/tauri'; -import { useActiveWorkspaceId } from './useActiveWorkspaceId'; +import { useActiveWorkspace } from './useActiveWorkspace'; import { useCreateHttpRequest } from './useCreateHttpRequest'; import { useRequestUpdateKey } from './useRequestUpdateKey'; import { useUpdateAnyHttpRequest } from './useUpdateAnyHttpRequest'; export function useImportCurl() { - const workspaceId = useActiveWorkspaceId(); + const workspace = useActiveWorkspace(); const updateRequest = useUpdateAnyHttpRequest(); const createRequest = useCreateHttpRequest(); const { wasUpdatedExternally } = useRequestUpdateKey(null); @@ -24,7 +24,7 @@ export function useImportCurl() { }) => { const request: Record = await invokeCmd('cmd_curl_to_request', { command, - workspaceId, + workspaceId: workspace?.id, }); delete request.id; diff --git a/src-web/hooks/useImportData.tsx b/src-web/hooks/useImportData.tsx index 9585d7d2..b7d53b36 100644 --- a/src-web/hooks/useImportData.tsx +++ b/src-web/hooks/useImportData.tsx @@ -1,13 +1,13 @@ import { useMutation } from '@tanstack/react-query'; +import type { Environment, Folder, GrpcRequest, HttpRequest, Workspace } from '@yaakapp/api'; import { Button } from '../components/core/Button'; import { FormattedError } from '../components/core/FormattedError'; import { VStack } from '../components/core/Stacks'; import { useDialog } from '../components/DialogContext'; import { ImportDataDialog } from '../components/ImportDataDialog'; -import type { Environment, Folder, GrpcRequest, HttpRequest, Workspace } from '@yaakapp/api'; import { count } from '../lib/pluralize'; import { invokeCmd } from '../lib/tauri'; -import { useActiveWorkspaceId } from './useActiveWorkspaceId'; +import { useActiveWorkspace } from './useActiveWorkspace'; import { useAlert } from './useAlert'; import { useAppRoutes } from './useAppRoutes'; @@ -15,7 +15,7 @@ export function useImportData() { const routes = useAppRoutes(); const dialog = useDialog(); const alert = useAlert(); - const activeWorkspaceId = useActiveWorkspaceId(); + const activeWorkspace = useActiveWorkspace(); const importData = async (filePath: string): Promise => { const imported: { @@ -26,7 +26,7 @@ export function useImportData() { grpcRequests: GrpcRequest[]; } = await invokeCmd('cmd_import_data', { filePath, - workspaceId: activeWorkspaceId, + workspaceId: activeWorkspace?.id, }); const importedWorkspace = imported.workspaces[0]; diff --git a/src-web/hooks/useIntrospectGraphQL.ts b/src-web/hooks/useIntrospectGraphQL.ts index a2224a39..f23d5a41 100644 --- a/src-web/hooks/useIntrospectGraphQL.ts +++ b/src-web/hooks/useIntrospectGraphQL.ts @@ -1,11 +1,11 @@ +import type { HttpRequest } from '@yaakapp/api'; import type { IntrospectionQuery } from 'graphql'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { buildClientSchema, getIntrospectionQuery } from '../components/core/Editor'; import { minPromiseMillis } from '../lib/minPromiseMillis'; -import type { HttpRequest } from '@yaakapp/api'; import { getResponseBodyText } from '../lib/responseBody'; import { sendEphemeralRequest } from '../lib/sendEphemeralRequest'; -import { useActiveEnvironmentId } from './useActiveEnvironmentId'; +import { useActiveEnvironment } from './useActiveEnvironment'; import { useDebouncedValue } from './useDebouncedValue'; import { useKeyValue } from './useKeyValue'; @@ -18,7 +18,7 @@ export function useIntrospectGraphQL(baseRequest: HttpRequest) { // Debounce the request because it can change rapidly and we don't // want to send so too many requests. const request = useDebouncedValue(baseRequest); - const activeEnvironmentId = useActiveEnvironmentId(); + const [activeEnvironment] = useActiveEnvironment(); const [refetchKey, setRefetchKey] = useState(0); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(); @@ -40,7 +40,10 @@ export function useIntrospectGraphQL(baseRequest: HttpRequest) { bodyType: 'application/json', body: { text: introspectionRequestBody }, }; - const response = await minPromiseMillis(sendEphemeralRequest(args, activeEnvironmentId), 700); + const response = await minPromiseMillis( + sendEphemeralRequest(args, activeEnvironment?.id ?? null), + 700, + ); if (response.error) { throw new Error(response.error); @@ -72,7 +75,7 @@ export function useIntrospectGraphQL(baseRequest: HttpRequest) { runIntrospection(); // Run immediately // eslint-disable-next-line react-hooks/exhaustive-deps - }, [request.id, request.url, request.method, refetchKey, activeEnvironmentId]); + }, [request.id, request.url, request.method, refetchKey, activeEnvironment?.id]); const refetch = useCallback(() => { setRefetchKey((k) => k + 1); diff --git a/src-web/hooks/useKeyValue.ts b/src-web/hooks/useKeyValue.ts index 69ba44b7..e2a8f818 100644 --- a/src-web/hooks/useKeyValue.ts +++ b/src-web/hooks/useKeyValue.ts @@ -1,4 +1,4 @@ -import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { useMutation, useQuery } from '@tanstack/react-query'; import { useCallback, useMemo } from 'react'; import { buildKeyValueKey, getKeyValue, setKeyValue } from '../lib/keyValueStore'; @@ -24,7 +24,6 @@ export function useKeyValue({ key: string | string[]; fallback: T; }) { - const queryClient = useQueryClient(); const query = useQuery({ queryKey: keyValueQueryKey({ namespace, key }), queryFn: async () => getKeyValue({ namespace, key, fallback }), @@ -34,8 +33,6 @@ export function useKeyValue({ const mutate = useMutation({ mutationKey: ['set_key_value', namespace, key], mutationFn: (value) => setKeyValue({ namespace, key, value }), - // k/v should be as fast as possible, so optimistically update the cache - onMutate: (value) => queryClient.setQueryData(keyValueQueryKey({ namespace, key }), value), }); const set = useCallback( diff --git a/src-web/hooks/useMoveToWorkspace.tsx b/src-web/hooks/useMoveToWorkspace.tsx index 6a90ebfc..792706fc 100644 --- a/src-web/hooks/useMoveToWorkspace.tsx +++ b/src-web/hooks/useMoveToWorkspace.tsx @@ -2,19 +2,19 @@ import { useMutation } from '@tanstack/react-query'; import React from 'react'; import { useDialog } from '../components/DialogContext'; import { MoveToWorkspaceDialog } from '../components/MoveToWorkspaceDialog'; -import { useActiveWorkspaceId } from './useActiveWorkspaceId'; +import { useActiveWorkspace } from './useActiveWorkspace'; import { useRequests } from './useRequests'; export function useMoveToWorkspace(id: string) { const dialog = useDialog(); const requests = useRequests(); const request = requests.find((r) => r.id === id); - const activeWorkspaceId = useActiveWorkspaceId(); + const activeWorkspace = useActiveWorkspace(); return useMutation({ mutationKey: ['move_workspace', id], mutationFn: async () => { - if (request == null || activeWorkspaceId == null) return; + if (request == null || activeWorkspace == null) return; dialog.show({ id: 'change-workspace', @@ -24,7 +24,7 @@ export function useMoveToWorkspace(id: string) { ), }); diff --git a/src-web/hooks/useOpenWorkspace.ts b/src-web/hooks/useOpenWorkspace.ts index 3870675e..50737cd1 100644 --- a/src-web/hooks/useOpenWorkspace.ts +++ b/src-web/hooks/useOpenWorkspace.ts @@ -1,6 +1,7 @@ import { useMutation } from '@tanstack/react-query'; import { invokeCmd } from '../lib/tauri'; import { useAppRoutes } from './useAppRoutes'; +import { getRecentCookieJars } from './useRecentCookieJars'; import { getRecentEnvironments } from './useRecentEnvironments'; import { getRecentRequests } from './useRecentRequests'; @@ -16,29 +17,21 @@ export function useOpenWorkspace() { workspaceId: string; inNewWindow: boolean; }) => { + const environmentId = (await getRecentEnvironments(workspaceId))[0]; + const requestId = (await getRecentRequests(workspaceId))[0]; + const cookieJarId = (await getRecentCookieJars(workspaceId))[0]; + const baseArgs = { workspaceId, environmentId, cookieJarId } as const; if (inNewWindow) { - const environmentId = (await getRecentEnvironments(workspaceId))[0]; - const requestId = (await getRecentRequests(workspaceId))[0]; const path = requestId != null - ? routes.paths.request({ - workspaceId, - environmentId, - requestId, - }) - : routes.paths.workspace({ workspaceId, environmentId }); + ? routes.paths.request({ ...baseArgs, requestId }) + : routes.paths.workspace({ ...baseArgs }); await invokeCmd('cmd_new_window', { url: path }); } else { - const environmentId = (await getRecentEnvironments(workspaceId))[0]; - const requestId = (await getRecentRequests(workspaceId))[0]; if (requestId != null) { - routes.navigate('request', { - workspaceId: workspaceId, - environmentId, - requestId, - }); + routes.navigate('request', { ...baseArgs, requestId }); } else { - routes.navigate('workspace', { workspaceId, environmentId }); + routes.navigate('workspace', { ...baseArgs }); } } }, diff --git a/src-web/hooks/usePreferredAppearance.ts b/src-web/hooks/usePreferredAppearance.ts index 4023245d..50fa40f0 100644 --- a/src-web/hooks/usePreferredAppearance.ts +++ b/src-web/hooks/usePreferredAppearance.ts @@ -1,10 +1,10 @@ import { useEffect, useState } from 'react'; +import type { Appearance } from '../lib/theme/appearance'; import { getCSSAppearance, getWindowAppearance, subscribeToWindowAppearanceChange, } from '../lib/theme/appearance'; -import { type Appearance } from '../lib/theme/window'; export function usePreferredAppearance() { const [preferredAppearance, setPreferredAppearance] = useState(getCSSAppearance()); diff --git a/src-web/hooks/useRecentCookieJars.ts b/src-web/hooks/useRecentCookieJars.ts new file mode 100644 index 00000000..26bbcbea --- /dev/null +++ b/src-web/hooks/useRecentCookieJars.ts @@ -0,0 +1,47 @@ +import { useEffect, useMemo } from 'react'; +import { getKeyValue } from '../lib/keyValueStore'; +import { useActiveCookieJar } from './useActiveCookieJar'; +import { useActiveWorkspace } from './useActiveWorkspace'; +import { useCookieJars } from './useCookieJars'; +import { useKeyValue } from './useKeyValue'; + +const kvKey = (workspaceId: string) => 'recent_cookie_jars::' + workspaceId; +const namespace = 'global'; +const fallback: string[] = []; + +export function useRecentCookieJars() { + const cookieJars = useCookieJars(); + const activeWorkspace = useActiveWorkspace(); + const [activeCookieJar] = useActiveCookieJar(); + const activeCookieJarId = activeCookieJar?.id ?? null; + const kv = useKeyValue({ + key: kvKey(activeWorkspace?.id ?? 'n/a'), + namespace, + fallback, + }); + + // Set history when active request changes + useEffect(() => { + kv.set((currentHistory: string[]) => { + if (activeCookieJarId === null) return currentHistory; + const withoutCurrent = currentHistory.filter((id) => id !== activeCookieJarId); + return [activeCookieJarId, ...withoutCurrent]; + }).catch(console.error); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [activeCookieJarId]); + + const onlyValidIds = useMemo( + () => kv.value?.filter((id) => cookieJars.data?.some((e) => e.id === id)) ?? [], + [kv.value, cookieJars], + ); + + return onlyValidIds; +} + +export async function getRecentCookieJars(workspaceId: string) { + return getKeyValue({ + namespace, + key: kvKey(workspaceId), + fallback, + }); +} diff --git a/src-web/hooks/useRecentEnvironments.ts b/src-web/hooks/useRecentEnvironments.ts index 153db76c..949a7454 100644 --- a/src-web/hooks/useRecentEnvironments.ts +++ b/src-web/hooks/useRecentEnvironments.ts @@ -1,7 +1,7 @@ import { useEffect, useMemo } from 'react'; import { getKeyValue } from '../lib/keyValueStore'; -import { useActiveEnvironmentId } from './useActiveEnvironmentId'; -import { useActiveWorkspaceId } from './useActiveWorkspaceId'; +import { useActiveEnvironment } from './useActiveEnvironment'; +import { useActiveWorkspace } from './useActiveWorkspace'; import { useEnvironments } from './useEnvironments'; import { useKeyValue } from './useKeyValue'; @@ -11,10 +11,10 @@ const fallback: string[] = []; export function useRecentEnvironments() { const environments = useEnvironments(); - const activeWorkspaceId = useActiveWorkspaceId(); - const activeEnvironmentId = useActiveEnvironmentId(); + const activeWorkspace = useActiveWorkspace(); + const [activeEnvironment] = useActiveEnvironment(); const kv = useKeyValue({ - key: kvKey(activeWorkspaceId ?? 'n/a'), + key: kvKey(activeWorkspace?.id ?? 'n/a'), namespace, fallback, }); @@ -22,12 +22,12 @@ export function useRecentEnvironments() { // Set history when active request changes useEffect(() => { kv.set((currentHistory: string[]) => { - if (activeEnvironmentId === null) return currentHistory; - const withoutCurrentEnvironment = currentHistory.filter((id) => id !== activeEnvironmentId); - return [activeEnvironmentId, ...withoutCurrentEnvironment]; + if (activeEnvironment === null) return currentHistory; + const withoutCurrentEnvironment = currentHistory.filter((id) => id !== activeEnvironment.id); + return [activeEnvironment.id, ...withoutCurrentEnvironment]; }).catch(console.error); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [activeEnvironmentId]); + }, [activeEnvironment?.id]); const onlyValidIds = useMemo( () => kv.value?.filter((id) => environments.some((e) => e.id === id)) ?? [], diff --git a/src-web/hooks/useRecentRequests.ts b/src-web/hooks/useRecentRequests.ts index 95c6068e..d12d9f9a 100644 --- a/src-web/hooks/useRecentRequests.ts +++ b/src-web/hooks/useRecentRequests.ts @@ -1,7 +1,7 @@ import { useEffect, useMemo } from 'react'; import { getKeyValue } from '../lib/keyValueStore'; import { useActiveRequestId } from './useActiveRequestId'; -import { useActiveWorkspaceId } from './useActiveWorkspaceId'; +import { useActiveWorkspace } from './useActiveWorkspace'; import { useKeyValue } from './useKeyValue'; import { useRequests } from './useRequests'; @@ -11,11 +11,11 @@ const fallback: string[] = []; export function useRecentRequests() { const requests = useRequests(); - const activeWorkspaceId = useActiveWorkspaceId(); + const activeWorkspace = useActiveWorkspace(); const activeRequestId = useActiveRequestId(); const kv = useKeyValue({ - key: kvKey(activeWorkspaceId ?? 'n/a'), + key: kvKey(activeWorkspace?.id ?? 'n/a'), namespace, fallback, }); diff --git a/src-web/hooks/useRecentWorkspaces.ts b/src-web/hooks/useRecentWorkspaces.ts index 17fedf08..f75c1457 100644 --- a/src-web/hooks/useRecentWorkspaces.ts +++ b/src-web/hooks/useRecentWorkspaces.ts @@ -1,6 +1,6 @@ import { useEffect, useMemo } from 'react'; import { getKeyValue } from '../lib/keyValueStore'; -import { useActiveWorkspaceId } from './useActiveWorkspaceId'; +import { useActiveWorkspace } from './useActiveWorkspace'; import { useKeyValue } from './useKeyValue'; import { useWorkspaces } from './useWorkspaces'; @@ -10,7 +10,7 @@ const fallback: string[] = []; export function useRecentWorkspaces() { const workspaces = useWorkspaces(); - const activeWorkspaceId = useActiveWorkspaceId(); + const activeWorkspace = useActiveWorkspace(); const kv = useKeyValue({ key: kvKey(), namespace, @@ -20,12 +20,12 @@ export function useRecentWorkspaces() { // Set history when active request changes useEffect(() => { kv.set((currentHistory: string[]) => { - if (activeWorkspaceId === null) return currentHistory; - const withoutCurrent = currentHistory.filter((id) => id !== activeWorkspaceId); - return [activeWorkspaceId, ...withoutCurrent]; + if (activeWorkspace === null) return currentHistory; + const withoutCurrent = currentHistory.filter((id) => id !== activeWorkspace.id); + return [activeWorkspace.id, ...withoutCurrent]; }).catch(console.error); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [activeWorkspaceId]); + }, [activeWorkspace]); const onlyValidIds = useMemo( () => kv.value?.filter((id) => workspaces.some((w) => w.id === id)) ?? [], diff --git a/src-web/hooks/useSendAnyHttpRequest.ts b/src-web/hooks/useSendAnyHttpRequest.ts index 0e104eff..1029c018 100644 --- a/src-web/hooks/useSendAnyHttpRequest.ts +++ b/src-web/hooks/useSendAnyHttpRequest.ts @@ -1,18 +1,16 @@ import { useMutation } from '@tanstack/react-query'; -import { save } from '@tauri-apps/plugin-dialog'; -import slugify from 'slugify'; -import { trackEvent } from '../lib/analytics'; import type { HttpResponse } from '@yaakapp/api'; +import { trackEvent } from '../lib/analytics'; import { invokeCmd } from '../lib/tauri'; import { useActiveCookieJar } from './useActiveCookieJar'; import { useActiveEnvironment } from './useActiveEnvironment'; import { useAlert } from './useAlert'; import { useHttpRequests } from './useHttpRequests'; -export function useSendAnyHttpRequest(options: { download?: boolean } = {}) { - const environment = useActiveEnvironment(); +export function useSendAnyHttpRequest() { + const [environment] = useActiveEnvironment(); const alert = useAlert(); - const { activeCookieJar } = useActiveCookieJar(); + const [activeCookieJar] = useActiveCookieJar(); const requests = useHttpRequests(); return useMutation({ mutationKey: ['send_any_request'], @@ -22,21 +20,9 @@ export function useSendAnyHttpRequest(options: { download?: boolean } = {}) { return null; } - let downloadDir: string | null = null; - if (options.download) { - downloadDir = await save({ - title: 'Select Download Destination', - defaultPath: slugify(request.name, { lower: true, trim: true, strict: true }), - }); - if (downloadDir == null) { - return null; - } - } - return invokeCmd('cmd_send_http_request', { request, environmentId: environment?.id, - downloadDir: downloadDir, cookieJarId: activeCookieJar?.id, }); }, diff --git a/src-web/hooks/useSidebarHidden.ts b/src-web/hooks/useSidebarHidden.ts index 5a06ad55..3d077d9a 100644 --- a/src-web/hooks/useSidebarHidden.ts +++ b/src-web/hooks/useSidebarHidden.ts @@ -1,11 +1,11 @@ -import { useActiveWorkspaceId } from './useActiveWorkspaceId'; +import { useActiveWorkspace } from './useActiveWorkspace'; import { useKeyValue } from './useKeyValue'; export function useSidebarHidden() { - const activeWorkspaceId = useActiveWorkspaceId(); + const activeWorkspace = useActiveWorkspace(); const { set, value } = useKeyValue({ namespace: 'no_sync', - key: ['sidebar_hidden', activeWorkspaceId ?? 'n/a'], + key: ['sidebar_hidden', activeWorkspace?.id ?? 'n/a'], fallback: false, }); diff --git a/src-web/hooks/useSidebarWidth.ts b/src-web/hooks/useSidebarWidth.ts index 44b8de93..8291fd11 100644 --- a/src-web/hooks/useSidebarWidth.ts +++ b/src-web/hooks/useSidebarWidth.ts @@ -1,10 +1,13 @@ import { useCallback, useMemo } from 'react'; import { useLocalStorage } from 'react-use'; -import { useActiveWorkspaceId } from './useActiveWorkspaceId'; +import { useActiveWorkspace } from './useActiveWorkspace'; export function useSidebarWidth() { - const activeWorkspaceId = useActiveWorkspaceId(); - const [width, setWidth] = useLocalStorage(`sidebar_width::${activeWorkspaceId}`, 250); + const activeWorkspace = useActiveWorkspace(); + const [width, setWidth] = useLocalStorage( + `sidebar_width::${activeWorkspace?.id ?? 'n/a'}`, + 250, + ); const resetWidth = useCallback(() => setWidth(250), [setWidth]); return useMemo(() => ({ width, setWidth, resetWidth }), [width, setWidth, resetWidth]); } diff --git a/src-web/hooks/useSyncThemeToDocument.ts b/src-web/hooks/useSyncThemeToDocument.ts index 219582ac..d7089456 100644 --- a/src-web/hooks/useSyncThemeToDocument.ts +++ b/src-web/hooks/useSyncThemeToDocument.ts @@ -18,6 +18,6 @@ export function useSyncThemeToDocument() { } function emitBgChange(t: YaakTheme) { - if (t.background == null) return; - emit('yaak_bg_changed', t.background.hex()).catch(console.error); + if (t.surface == null) return; + emit('yaak_bg_changed', t.surface.hexNoAlpha()).catch(console.error); } diff --git a/src-web/hooks/useSyncWorkspaceRequestTitle.ts b/src-web/hooks/useSyncWorkspaceRequestTitle.ts index 4116318c..4ce50ec1 100644 --- a/src-web/hooks/useSyncWorkspaceRequestTitle.ts +++ b/src-web/hooks/useSyncWorkspaceRequestTitle.ts @@ -11,7 +11,7 @@ import { emit } from '@tauri-apps/api/event'; export function useSyncWorkspaceRequestTitle() { const activeRequest = useActiveRequest(); const activeWorkspace = useActiveWorkspace(); - const activeEnvironment = useActiveEnvironment(); + const [activeEnvironment] = useActiveEnvironment(); const osInfo = useOsInfo(); const appInfo = useAppInfo(); diff --git a/src-web/hooks/useUpdateAnyFolder.ts b/src-web/hooks/useUpdateAnyFolder.ts index 643f30e3..f830b7f9 100644 --- a/src-web/hooks/useUpdateAnyFolder.ts +++ b/src-web/hooks/useUpdateAnyFolder.ts @@ -1,12 +1,9 @@ -import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useMutation } from '@tanstack/react-query'; import type { Folder } from '@yaakapp/api'; import { getFolder } from '../lib/store'; import { invokeCmd } from '../lib/tauri'; -import { foldersQueryKey } from './useFolders'; export function useUpdateAnyFolder() { - const queryClient = useQueryClient(); - return useMutation Folder }>({ mutationKey: ['update_any_folder'], mutationFn: async ({ id, update }) => { @@ -17,12 +14,5 @@ export function useUpdateAnyFolder() { await invokeCmd('cmd_update_folder', { folder: update(folder) }); }, - onMutate: async ({ id, update }) => { - const folder = await getFolder(id); - if (folder === null) return; - queryClient.setQueryData(foldersQueryKey(folder), (folders) => - (folders ?? []).map((f) => (f.id === folder.id ? update(f) : f)), - ); - }, }); } diff --git a/src-web/hooks/useUpdateAnyGrpcRequest.ts b/src-web/hooks/useUpdateAnyGrpcRequest.ts index 4107467a..930f4f0b 100644 --- a/src-web/hooks/useUpdateAnyGrpcRequest.ts +++ b/src-web/hooks/useUpdateAnyGrpcRequest.ts @@ -1,12 +1,9 @@ -import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useMutation } from '@tanstack/react-query'; import type { GrpcRequest } from '@yaakapp/api'; import { getGrpcRequest } from '../lib/store'; import { invokeCmd } from '../lib/tauri'; -import { grpcRequestsQueryKey } from './useGrpcRequests'; export function useUpdateAnyGrpcRequest() { - const queryClient = useQueryClient(); - return useMutation< void, unknown, @@ -23,14 +20,5 @@ export function useUpdateAnyGrpcRequest() { typeof update === 'function' ? update(request) : { ...request, ...update }; await invokeCmd('cmd_update_grpc_request', { request: patchedRequest }); }, - onMutate: async ({ id, update }) => { - const request = await getGrpcRequest(id); - if (request === null) return; - const patchedRequest = - typeof update === 'function' ? update(request) : { ...request, ...update }; - queryClient.setQueryData(grpcRequestsQueryKey(request), (requests) => - (requests ?? []).map((r) => (r.id === patchedRequest.id ? patchedRequest : r)), - ); - }, }); } diff --git a/src-web/hooks/useUpdateAnyHttpRequest.ts b/src-web/hooks/useUpdateAnyHttpRequest.ts index c73fc651..73902de0 100644 --- a/src-web/hooks/useUpdateAnyHttpRequest.ts +++ b/src-web/hooks/useUpdateAnyHttpRequest.ts @@ -1,12 +1,9 @@ -import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useMutation } from '@tanstack/react-query'; import type { HttpRequest } from '@yaakapp/api'; import { getHttpRequest } from '../lib/store'; import { invokeCmd } from '../lib/tauri'; -import { httpRequestsQueryKey } from './useHttpRequests'; export function useUpdateAnyHttpRequest() { - const queryClient = useQueryClient(); - return useMutation< void, unknown, @@ -23,14 +20,5 @@ export function useUpdateAnyHttpRequest() { typeof update === 'function' ? update(request) : { ...request, ...update }; await invokeCmd('cmd_update_http_request', { request: patchedRequest }); }, - onMutate: async ({ id, update }) => { - const request = await getHttpRequest(id); - if (request === null) return; - const patchedRequest = - typeof update === 'function' ? update(request) : { ...request, ...update }; - queryClient.setQueryData(httpRequestsQueryKey(request), (requests) => - (requests ?? []).map((r) => (r.id === patchedRequest.id ? patchedRequest : r)), - ); - }, }); } diff --git a/src-web/hooks/useUpdateCookieJar.ts b/src-web/hooks/useUpdateCookieJar.ts index 7f28e600..4c8be267 100644 --- a/src-web/hooks/useUpdateCookieJar.ts +++ b/src-web/hooks/useUpdateCookieJar.ts @@ -1,11 +1,9 @@ -import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useMutation } from '@tanstack/react-query'; import type { CookieJar } from '../lib/models'; import { getCookieJar } from '../lib/store'; import { invokeCmd } from '../lib/tauri'; -import { cookieJarsQueryKey } from './useCookieJars'; export function useUpdateCookieJar(id: string | null) { - const queryClient = useQueryClient(); return useMutation | ((j: CookieJar) => CookieJar)>({ mutationKey: ['update_cookie_jar', id], mutationFn: async (v) => { @@ -15,17 +13,7 @@ export function useUpdateCookieJar(id: string | null) { } const newCookieJar = typeof v === 'function' ? v(cookieJar) : { ...cookieJar, ...v }; - console.log('NEW COOKIE JAR', newCookieJar.cookies.length); await invokeCmd('cmd_update_cookie_jar', { cookieJar: newCookieJar }); }, - onMutate: async (v) => { - const cookieJar = await getCookieJar(id); - if (cookieJar === null) return; - - const newCookieJar = typeof v === 'function' ? v(cookieJar) : { ...cookieJar, ...v }; - queryClient.setQueryData(cookieJarsQueryKey(cookieJar), (cookieJars) => - (cookieJars ?? []).map((j) => (j.id === newCookieJar.id ? newCookieJar : j)), - ); - }, }); } diff --git a/src-web/hooks/useUpdateEnvironment.ts b/src-web/hooks/useUpdateEnvironment.ts index 7228a477..f407e298 100644 --- a/src-web/hooks/useUpdateEnvironment.ts +++ b/src-web/hooks/useUpdateEnvironment.ts @@ -1,11 +1,9 @@ -import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useMutation } from '@tanstack/react-query'; import type { Environment } from '@yaakapp/api'; import { getEnvironment } from '../lib/store'; import { invokeCmd } from '../lib/tauri'; -import { environmentsQueryKey } from './useEnvironments'; export function useUpdateEnvironment(id: string | null) { - const queryClient = useQueryClient(); return useMutation | ((r: Environment) => Environment)>({ mutationKey: ['update_environment', id], mutationFn: async (v) => { @@ -17,14 +15,5 @@ export function useUpdateEnvironment(id: string | null) { const newEnvironment = typeof v === 'function' ? v(environment) : { ...environment, ...v }; await invokeCmd('cmd_update_environment', { environment: newEnvironment }); }, - onMutate: async (v) => { - const environment = await getEnvironment(id); - if (environment === null) return; - - const newEnvironment = typeof v === 'function' ? v(environment) : { ...environment, ...v }; - queryClient.setQueryData(environmentsQueryKey(environment), (environments) => - (environments ?? []).map((r) => (r.id === newEnvironment.id ? newEnvironment : r)), - ); - }, }); } diff --git a/src-web/hooks/useUpdateWorkspace.ts b/src-web/hooks/useUpdateWorkspace.ts index a1ed1bd3..51893328 100644 --- a/src-web/hooks/useUpdateWorkspace.ts +++ b/src-web/hooks/useUpdateWorkspace.ts @@ -1,11 +1,9 @@ -import { useMutation, useQueryClient } from '@tanstack/react-query'; +import { useMutation } from '@tanstack/react-query'; import type { Workspace } from '@yaakapp/api'; import { getWorkspace } from '../lib/store'; import { invokeCmd } from '../lib/tauri'; -import { workspacesQueryKey } from './useWorkspaces'; export function useUpdateWorkspace(id: string | null) { - const queryClient = useQueryClient(); return useMutation | ((w: Workspace) => Workspace)>({ mutationKey: ['update_workspace', id], mutationFn: async (v) => { @@ -17,14 +15,5 @@ export function useUpdateWorkspace(id: string | null) { const newWorkspace = typeof v === 'function' ? v(workspace) : { ...workspace, ...v }; await invokeCmd('cmd_update_workspace', { workspace: newWorkspace }); }, - onMutate: async (v) => { - const workspace = await getWorkspace(id); - if (workspace === null) return; - - const newWorkspace = typeof v === 'function' ? v(workspace) : { ...workspace, ...v }; - queryClient.setQueryData(workspacesQueryKey(workspace), (workspaces) => - (workspaces ?? []).map((w) => (w.id === newWorkspace.id ? newWorkspace : w)), - ); - }, }); } diff --git a/src-web/lib/keyValueStore.ts b/src-web/lib/keyValueStore.ts index 3fbc51c3..dd12121f 100644 --- a/src-web/lib/keyValueStore.ts +++ b/src-web/lib/keyValueStore.ts @@ -17,6 +17,20 @@ export async function setKeyValue({ }); } +export async function getKeyValueRaw({ + namespace = 'global', + key, +}: { + namespace?: string; + key: string | string[]; +}) { + const kv = (await invokeCmd('cmd_get_key_value', { + namespace, + key: buildKeyValueKey(key), + })) as KeyValue | null; + return kv; +} + export async function getKeyValue({ namespace = 'global', key, @@ -26,14 +40,11 @@ export async function getKeyValue({ key: string | string[]; fallback: T; }) { - const kv = (await invokeCmd('cmd_get_key_value', { - namespace, - key: buildKeyValueKey(key), - })) as KeyValue | null; + const kv = await getKeyValueRaw({ namespace, key }); return extractKeyValueOrFallback(kv, fallback); } -function extractKeyValue(kv: KeyValue | null): T | undefined { +export function extractKeyValue(kv: KeyValue | null): T | undefined { if (kv === null) return undefined; try { return JSON.parse(kv.value) as T; diff --git a/src-web/lib/models.ts b/src-web/lib/models.ts index 2e730d86..a4f4033f 100644 --- a/src-web/lib/models.ts +++ b/src-web/lib/models.ts @@ -36,6 +36,9 @@ export function isResponseLoading(response: HttpResponse | GrpcConnection): bool } export function modelsEq(a: Model, b: Model) { + if (a.model != b.model) { + return false; + } if (a.model === 'key_value' && b.model === 'key_value') { return a.key === b.key && a.namespace === b.namespace; } diff --git a/src-web/lib/theme/appearance.ts b/src-web/lib/theme/appearance.ts index 2fac6fac..ec2cb370 100644 --- a/src-web/lib/theme/appearance.ts +++ b/src-web/lib/theme/appearance.ts @@ -1,6 +1,6 @@ import { getCurrentWebviewWindow } from '@tauri-apps/api/webviewWindow'; -type Appearance = 'light' | 'dark'; +export type Appearance = 'light' | 'dark'; export function getCSSAppearance(): Appearance { return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; diff --git a/src-web/lib/theme/window.ts b/src-web/lib/theme/window.ts index fea9b9e1..dc73edea 100644 --- a/src-web/lib/theme/window.ts +++ b/src-web/lib/theme/window.ts @@ -310,7 +310,6 @@ export function completeTheme(theme: YaakTheme): YaakTheme { theme.border = theme.border ?? theme.surface?.lift(0.12); theme.borderSubtle = theme.borderSubtle ?? theme.border?.lower(0.08); - console.log('HELLO', { theme }); theme.text = theme.text ?? theme.border?.lift(1).lower(0.2); theme.textSubtle = theme.textSubtle ?? theme.text?.lower(0.3); diff --git a/src-web/lib/theme/yaakColor.ts b/src-web/lib/theme/yaakColor.ts index 3cf88555..3c6abb53 100644 --- a/src-web/lib/theme/yaakColor.ts +++ b/src-web/lib/theme/yaakColor.ts @@ -104,6 +104,15 @@ export class YaakColor { return rgbaToHex(r, g, b, a); } + hexNoAlpha(): string { + const h = this.hue; + const s = this.saturation; + const l = this.lightness; + + const [r, g, b] = parseColor(`hsl(${h},${s}%,${l}%)`).rgb; + return rgbaToHexNoAlpha(r, g, b); + } + private _lighten(mod: number): YaakColor { const c = this.clone(); c.lightness = this.lightness + (100 - this.lightness) * mod; @@ -125,6 +134,14 @@ function rgbaToHex(r: number, g: number, b: number, a: number): string { return '#' + [toHex(r), toHex(g), toHex(b), toHex(a * 255)].join('').toUpperCase(); } +function rgbaToHexNoAlpha(r: number, g: number, b: number): string { + const toHex = (n: number): string => { + const hex = Number(Math.round(n)).toString(16); + return hex.length === 1 ? `0${hex}` : hex; + }; + return '#' + [toHex(r), toHex(g), toHex(b)].join('').toUpperCase(); +} + function hexToRgba(hex: string): [number, number, number, number] { const fromHex = (h: string): number => { if (h === '') return 255;