mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-11-13 12:13:42 +01:00
Put the restored request in the store before refreshing editors
Restore told open editors to re-read the request before its change event had reached the store, so header and parameter rows could keep showing the old content. The restore RPC now returns the restored request, and the frontend puts it in the store before calling wasUpdatedExternally, the same way deleteModel applies its change locally once the RPC resolves. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a68f7a080c
commit
07662257c1
8 files changed
+90
-20
No files matched your search
@@ -0,0 +1,54 @@
|
||||
import type { ModelVersion } from "@yaakapp-internal/models";
|
||||
import { beforeEach, describe, expect, test, vi } from "vite-plus/test";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
calls: [] as string[],
|
||||
flushAllModelWrites: vi.fn(),
|
||||
mergeModelsInStore: vi.fn(),
|
||||
wasUpdatedExternally: vi.fn(),
|
||||
rpc: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@yaakapp-internal/models", () => ({
|
||||
flushAllModelWrites: mocks.flushAllModelWrites,
|
||||
mergeModelsInStore: mocks.mergeModelsInStore,
|
||||
}));
|
||||
vi.mock("../hooks/useRequestUpdateKey", () => ({
|
||||
wasUpdatedExternally: mocks.wasUpdatedExternally,
|
||||
}));
|
||||
vi.mock("./rpc", () => ({ rpc: mocks.rpc }));
|
||||
vi.mock("./toast", () => ({ showToast: vi.fn() }));
|
||||
|
||||
const restored = { model: "http_request", id: "rq_1", url: "https://example.com/one" };
|
||||
const version = { id: "mv_1", modelId: "rq_1" } as ModelVersion;
|
||||
|
||||
describe("applyRequestVersion", () => {
|
||||
beforeEach(() => {
|
||||
mocks.calls = [];
|
||||
mocks.flushAllModelWrites.mockImplementation(async () => mocks.calls.push("flush"));
|
||||
mocks.rpc.mockImplementation(async (cmd: string) => {
|
||||
mocks.calls.push(cmd);
|
||||
return restored;
|
||||
});
|
||||
mocks.mergeModelsInStore.mockImplementation(() => mocks.calls.push("store"));
|
||||
mocks.wasUpdatedExternally.mockImplementation(() => mocks.calls.push("editors"));
|
||||
});
|
||||
|
||||
test("puts the restored request in the store before editors re-read it", async () => {
|
||||
const { applyRequestVersion } = await import("./restoreRequestVersion");
|
||||
await applyRequestVersion(version);
|
||||
|
||||
expect(mocks.calls).toEqual(["flush", "models_restore_request_version", "store", "editors"]);
|
||||
expect(mocks.mergeModelsInStore).toHaveBeenCalledWith("http_request", [restored]);
|
||||
expect(mocks.wasUpdatedExternally).toHaveBeenCalledWith("rq_1");
|
||||
});
|
||||
|
||||
test("a failed restore leaves the store and editors alone", async () => {
|
||||
mocks.rpc.mockRejectedValue(new Error("no such version"));
|
||||
const { applyRequestVersion } = await import("./restoreRequestVersion");
|
||||
|
||||
await expect(applyRequestVersion(version)).rejects.toThrow("no such version");
|
||||
expect(mocks.mergeModelsInStore).not.toHaveBeenCalled();
|
||||
expect(mocks.wasUpdatedExternally).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { flushAllModelWrites } from "@yaakapp-internal/models";
|
||||
import type { ModelVersion } from "@yaakapp-internal/models";
|
||||
import { flushAllModelWrites, mergeModelsInStore } from "@yaakapp-internal/models";
|
||||
import type { AnyModel, ModelVersion } from "@yaakapp-internal/models";
|
||||
import { wasUpdatedExternally } from "../hooks/useRequestUpdateKey";
|
||||
import { fireAndForget } from "./fireAndForget";
|
||||
import { rpc } from "./rpc";
|
||||
@@ -16,15 +16,7 @@ import { showToast } from "./toast";
|
||||
export function restoreRequestVersion(version: ModelVersion) {
|
||||
fireAndForget(
|
||||
(async () => {
|
||||
// The backend restores from the database, so anything still sitting in a
|
||||
// debounce has to land first — otherwise it would overwrite the restore
|
||||
await flushAllModelWrites();
|
||||
const requestId = await rpc<string>("models_restore_request_version", {
|
||||
versionId: version.id,
|
||||
});
|
||||
// The write came from this window, so the store's echo suppression would
|
||||
// otherwise leave open editors showing what was there before
|
||||
wasUpdatedExternally(requestId);
|
||||
await applyRequestVersion(version);
|
||||
showToast({
|
||||
id: "request-version-restored",
|
||||
color: "success",
|
||||
@@ -33,3 +25,20 @@ export function restoreRequestVersion(version: ModelVersion) {
|
||||
})(),
|
||||
);
|
||||
}
|
||||
|
||||
/** The restore itself, in the order the editors need it. */
|
||||
export async function applyRequestVersion(version: ModelVersion): Promise<void> {
|
||||
// The backend restores from the database, so anything still sitting in a
|
||||
// debounce has to land first — otherwise it would overwrite the restore
|
||||
await flushAllModelWrites();
|
||||
const restored = await rpc<AnyModel>("models_restore_request_version", {
|
||||
versionId: version.id,
|
||||
});
|
||||
// Editors re-read from the store when told to, and the change event can
|
||||
// arrive after this resolves, so put the restored request there first. The
|
||||
// event re-applying it later changes nothing.
|
||||
mergeModelsInStore(restored.model, [restored]);
|
||||
// The write came from this window, so the store's echo suppression would
|
||||
// otherwise leave open editors showing what was there before
|
||||
wasUpdatedExternally(restored.id);
|
||||
}
|
||||
@@ -36,7 +36,7 @@ use yaak_grpc::ServiceDefinition;
|
||||
use yaak_grpc::manager::GrpcHandle;
|
||||
use yaak_models::blob_manager::BlobManager;
|
||||
use yaak_models::models::{
|
||||
GraphQlIntrospection, GrpcEvent, HttpRequest, HttpRequestHeader, HttpResponse,
|
||||
AnyModel, GraphQlIntrospection, GrpcEvent, HttpRequest, HttpRequestHeader, HttpResponse,
|
||||
HttpResponseEvent, ImportSource, ModelVersion, Plugin, RequestVersionComparison, Settings,
|
||||
WebsocketConnection, WebsocketEvent, WorkspaceMeta,
|
||||
};
|
||||
@@ -888,7 +888,7 @@ async fn models_request_version<R: Runtime>(
|
||||
async fn models_restore_request_version<R: Runtime>(
|
||||
ctx: ClientCtx<R>,
|
||||
req: ModelsRestoreRequestVersionReq,
|
||||
) -> Result<String> {
|
||||
) -> Result<AnyModel> {
|
||||
Ok(yaak_commands::models::models_restore_request_version(ctx, req).await?)
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -275,6 +275,6 @@ export type ModelsWebsocketEventsReq = { connectionId: string, };
|
||||
|
||||
export type ModelsWorkspaceModelsReq = { workspaceId: string | null, };
|
||||
|
||||
export type RpcSchema = { cmd_metadata: [CmdMetadataReq, AppMetaData], cmd_agent_setup_status: [CmdAgentSetupStatusReq, AgentSetupStatus], cmd_template_tokens_to_string: [CmdTemplateTokensToStringReq, string], cmd_render_template: [CmdRenderTemplateReq, string], cmd_send_feedback: [CmdSendFeedbackReq, null], cmd_dismiss_notification: [CmdDismissNotificationReq, null], cmd_grpc_reflect: [CmdGrpcReflectReq, Array<ServiceDefinition>], cmd_grpc_go: [CmdGrpcGoReq, string], cmd_restart: [CmdRestartReq, null], cmd_send_ephemeral_request: [CmdSendEphemeralRequestReq, EphemeralHttpResponse], cmd_format_json: [CmdFormatJsonReq, string], cmd_format_graphql: [CmdFormatGraphqlReq, string], cmd_http_response_body: [CmdHttpResponseBodyReq, FilterResponse], cmd_http_response_body_path: [CmdHttpResponseBodyPathReq, string | null], cmd_http_request_body: [CmdHttpRequestBodyReq, Array<number> | null], cmd_get_sse_events: [CmdGetSseEventsReq, Array<ServerSentEvent>], cmd_get_http_response_events: [CmdGetHttpResponseEventsReq, Array<HttpResponseEvent>], cmd_import_data: [CmdImportDataReq, ImportPlan], cmd_commit_import: [CmdCommitImportReq, BatchUpsertResult], cmd_list_import_sources: [CmdListImportSourcesReq, Array<ImportSource>], cmd_import_sources_for_origin: [CmdImportSourcesForOriginReq, Array<ImportSource>], cmd_detect_import_source: [CmdDetectImportSourceReq, string], cmd_http_request_actions: [CmdHttpRequestActionsReq, Array<GetHttpRequestActionsResponse>], cmd_websocket_request_actions: [CmdWebsocketRequestActionsReq, Array<GetWebsocketRequestActionsResponse>], cmd_call_websocket_request_action: [CmdCallWebsocketRequestActionReq, null], cmd_workspace_actions: [CmdWorkspaceActionsReq, Array<GetWorkspaceActionsResponse>], cmd_call_workspace_action: [CmdCallWorkspaceActionReq, null], cmd_folder_actions: [CmdFolderActionsReq, Array<GetFolderActionsResponse>], cmd_call_folder_action: [CmdCallFolderActionReq, null], cmd_grpc_request_actions: [CmdGrpcRequestActionsReq, Array<GetGrpcRequestActionsResponse>], cmd_template_function_summaries: [CmdTemplateFunctionSummariesReq, Array<GetTemplateFunctionSummaryResponse>], cmd_template_function_config: [CmdTemplateFunctionConfigReq, GetTemplateFunctionConfigResponse], cmd_get_http_authentication_summaries: [CmdGetHttpAuthenticationSummariesReq, Array<GetHttpAuthenticationSummaryResponse>], cmd_get_http_authentication_config: [CmdGetHttpAuthenticationConfigReq, GetHttpAuthenticationConfigResponse], cmd_call_http_request_action: [CmdCallHttpRequestActionReq, null], cmd_call_grpc_request_action: [CmdCallGrpcRequestActionReq, null], cmd_call_http_authentication_action: [CmdCallHttpAuthenticationActionReq, null], cmd_curl_to_request: [CmdCurlToRequestReq, HttpRequest], cmd_export_data: [CmdExportDataReq, string], cmd_create_example_workspace: [CmdCreateExampleWorkspaceReq, BatchUpsertResult], cmd_save_base64_to_binary: [CmdSaveBase64ToBinaryReq, null], cmd_save_response: [CmdSaveResponseReq, null], cmd_send_http_request: [CmdSendHttpRequestReq, HttpResponse], cmd_reload_plugins: [CmdReloadPluginsReq, Array<[string, string]>], cmd_plugin_info: [CmdPluginInfoReq, PluginMetadata], cmd_delete_all_grpc_connections: [CmdDeleteAllGrpcConnectionsReq, null], cmd_delete_send_history: [CmdDeleteSendHistoryReq, null], cmd_delete_all_http_responses: [CmdDeleteAllHttpResponsesReq, null], cmd_get_workspace_meta: [CmdGetWorkspaceMetaReq, WorkspaceMeta], cmd_new_child_window: [CmdNewChildWindowReq, null], cmd_new_main_window: [CmdNewMainWindowReq, null], cmd_check_for_updates: [CmdCheckForUpdatesReq, boolean], cmd_decrypt_template: [CmdDecryptTemplateReq, string], cmd_secure_template: [CmdSecureTemplateReq, string], cmd_get_themes: [CmdGetThemesReq, Array<GetThemesResponse>], cmd_enable_encryption: [CmdEnableEncryptionReq, null], cmd_reveal_workspace_key: [CmdRevealWorkspaceKeyReq, string], cmd_set_workspace_key: [CmdSetWorkspaceKeyReq, null], cmd_disable_encryption: [CmdDisableEncryptionReq, null], cmd_default_headers: [CmdDefaultHeadersReq, Array<HttpRequestHeader>], models_upsert: [ModelsUpsertReq, string], models_delete: [ModelsDeleteReq, string], models_duplicate: [ModelsDuplicateReq, string], models_snapshot_request: [ModelsSnapshotRequestReq, ModelVersion], models_request_version: [ModelsRequestVersionReq, RequestVersionComparison], models_restore_request_version: [ModelsRestoreRequestVersionReq, string], models_websocket_events: [ModelsWebsocketEventsReq, Array<WebsocketEvent>], models_grpc_events: [ModelsGrpcEventsReq, Array<GrpcEvent>], models_get_settings: [ModelsGetSettingsReq, Settings], models_get_graphql_introspection: [ModelsGetGraphqlIntrospectionReq, GraphQlIntrospection | null], models_upsert_graphql_introspection: [ModelsUpsertGraphqlIntrospectionReq, GraphQlIntrospection], models_workspace_models: [ModelsWorkspaceModelsReq, string], cmd_git_checkout: [CmdGitCheckoutReq, string], cmd_git_branch: [CmdGitBranchReq, null], cmd_git_delete_branch: [CmdGitDeleteBranchReq, BranchDeleteResulLine truncated
|
||||
export type RpcSchema = { cmd_metadata: [CmdMetadataReq, AppMetaData], cmd_agent_setup_status: [CmdAgentSetupStatusReq, AgentSetupStatus], cmd_template_tokens_to_string: [CmdTemplateTokensToStringReq, string], cmd_render_template: [CmdRenderTemplateReq, string], cmd_send_feedback: [CmdSendFeedbackReq, null], cmd_dismiss_notification: [CmdDismissNotificationReq, null], cmd_grpc_reflect: [CmdGrpcReflectReq, Array<ServiceDefinition>], cmd_grpc_go: [CmdGrpcGoReq, string], cmd_restart: [CmdRestartReq, null], cmd_send_ephemeral_request: [CmdSendEphemeralRequestReq, EphemeralHttpResponse], cmd_format_json: [CmdFormatJsonReq, string], cmd_format_graphql: [CmdFormatGraphqlReq, string], cmd_http_response_body: [CmdHttpResponseBodyReq, FilterResponse], cmd_http_response_body_path: [CmdHttpResponseBodyPathReq, string | null], cmd_http_request_body: [CmdHttpRequestBodyReq, Array<number> | null], cmd_get_sse_events: [CmdGetSseEventsReq, Array<ServerSentEvent>], cmd_get_http_response_events: [CmdGetHttpResponseEventsReq, Array<HttpResponseEvent>], cmd_import_data: [CmdImportDataReq, ImportPlan], cmd_commit_import: [CmdCommitImportReq, BatchUpsertResult], cmd_list_import_sources: [CmdListImportSourcesReq, Array<ImportSource>], cmd_import_sources_for_origin: [CmdImportSourcesForOriginReq, Array<ImportSource>], cmd_detect_import_source: [CmdDetectImportSourceReq, string], cmd_http_request_actions: [CmdHttpRequestActionsReq, Array<GetHttpRequestActionsResponse>], cmd_websocket_request_actions: [CmdWebsocketRequestActionsReq, Array<GetWebsocketRequestActionsResponse>], cmd_call_websocket_request_action: [CmdCallWebsocketRequestActionReq, null], cmd_workspace_actions: [CmdWorkspaceActionsReq, Array<GetWorkspaceActionsResponse>], cmd_call_workspace_action: [CmdCallWorkspaceActionReq, null], cmd_folder_actions: [CmdFolderActionsReq, Array<GetFolderActionsResponse>], cmd_call_folder_action: [CmdCallFolderActionReq, null], cmd_grpc_request_actions: [CmdGrpcRequestActionsReq, Array<GetGrpcRequestActionsResponse>], cmd_template_function_summaries: [CmdTemplateFunctionSummariesReq, Array<GetTemplateFunctionSummaryResponse>], cmd_template_function_config: [CmdTemplateFunctionConfigReq, GetTemplateFunctionConfigResponse], cmd_get_http_authentication_summaries: [CmdGetHttpAuthenticationSummariesReq, Array<GetHttpAuthenticationSummaryResponse>], cmd_get_http_authentication_config: [CmdGetHttpAuthenticationConfigReq, GetHttpAuthenticationConfigResponse], cmd_call_http_request_action: [CmdCallHttpRequestActionReq, null], cmd_call_grpc_request_action: [CmdCallGrpcRequestActionReq, null], cmd_call_http_authentication_action: [CmdCallHttpAuthenticationActionReq, null], cmd_curl_to_request: [CmdCurlToRequestReq, HttpRequest], cmd_export_data: [CmdExportDataReq, string], cmd_create_example_workspace: [CmdCreateExampleWorkspaceReq, BatchUpsertResult], cmd_save_base64_to_binary: [CmdSaveBase64ToBinaryReq, null], cmd_save_response: [CmdSaveResponseReq, null], cmd_send_http_request: [CmdSendHttpRequestReq, HttpResponse], cmd_reload_plugins: [CmdReloadPluginsReq, Array<[string, string]>], cmd_plugin_info: [CmdPluginInfoReq, PluginMetadata], cmd_delete_all_grpc_connections: [CmdDeleteAllGrpcConnectionsReq, null], cmd_delete_send_history: [CmdDeleteSendHistoryReq, null], cmd_delete_all_http_responses: [CmdDeleteAllHttpResponsesReq, null], cmd_get_workspace_meta: [CmdGetWorkspaceMetaReq, WorkspaceMeta], cmd_new_child_window: [CmdNewChildWindowReq, null], cmd_new_main_window: [CmdNewMainWindowReq, null], cmd_check_for_updates: [CmdCheckForUpdatesReq, boolean], cmd_decrypt_template: [CmdDecryptTemplateReq, string], cmd_secure_template: [CmdSecureTemplateReq, string], cmd_get_themes: [CmdGetThemesReq, Array<GetThemesResponse>], cmd_enable_encryption: [CmdEnableEncryptionReq, null], cmd_reveal_workspace_key: [CmdRevealWorkspaceKeyReq, string], cmd_set_workspace_key: [CmdSetWorkspaceKeyReq, null], cmd_disable_encryption: [CmdDisableEncryptionReq, null], cmd_default_headers: [CmdDefaultHeadersReq, Array<HttpRequestHeader>], models_upsert: [ModelsUpsertReq, string], models_delete: [ModelsDeleteReq, string], models_duplicate: [ModelsDuplicateReq, string], models_snapshot_request: [ModelsSnapshotRequestReq, ModelVersion], models_request_version: [ModelsRequestVersionReq, RequestVersionComparison], models_restore_request_version: [ModelsRestoreRequestVersionReq, AnyModel], models_websocket_events: [ModelsWebsocketEventsReq, Array<WebsocketEvent>], models_grpc_events: [ModelsGrpcEventsReq, Array<GrpcEvent>], models_get_settings: [ModelsGetSettingsReq, Settings], models_get_graphql_introspection: [ModelsGetGraphqlIntrospectionReq, GraphQlIntrospection | null], models_upsert_graphql_introspection: [ModelsUpsertGraphqlIntrospectionReq, GraphQlIntrospection], models_workspace_models: [ModelsWorkspaceModelsReq, string], cmd_git_checkout: [CmdGitCheckoutReq, string], cmd_git_branch: [CmdGitBranchReq, null], cmd_git_delete_branch: [CmdGitDeleteBranchReq, BranchDeleteResLine truncated
|
||||
|
||||
export type WatchResult = { unlistenEvent: string, };
|
||||
@@ -1043,7 +1043,7 @@ macro_rules! with_commands {
|
||||
models_duplicate(ModelsDuplicateReq) -> String,
|
||||
models_snapshot_request(ModelsSnapshotRequestReq) -> ModelVersion,
|
||||
models_request_version(ModelsRequestVersionReq) -> RequestVersionComparison,
|
||||
models_restore_request_version(ModelsRestoreRequestVersionReq) -> String,
|
||||
models_restore_request_version(ModelsRestoreRequestVersionReq) -> AnyModel,
|
||||
models_websocket_events(ModelsWebsocketEventsReq) -> Vec<WebsocketEvent>,
|
||||
models_grpc_events(ModelsGrpcEventsReq) -> Vec<GrpcEvent>,
|
||||
models_get_settings(ModelsGetSettingsReq) -> Settings,
|
||||
|
||||
@@ -73,15 +73,16 @@ pub async fn models_request_version<H: Host>(
|
||||
Ok(RequestVersionComparison { version, current_document, differs })
|
||||
}
|
||||
|
||||
/// Returns the id of the request that was restored.
|
||||
/// Returns the restored request, so the frontend can put it in its store
|
||||
/// without waiting for the change event to come back around.
|
||||
pub async fn models_restore_request_version<H: Host>(
|
||||
host: H,
|
||||
req: ModelsRestoreRequestVersionReq,
|
||||
) -> Result<String> {
|
||||
) -> Result<AnyModel> {
|
||||
let source = host.update_source();
|
||||
let restored =
|
||||
host.query_manager().with_tx(|tx| tx.restore_request_version(&req.version_id, &source))?;
|
||||
Ok(restored.id().to_string())
|
||||
Ok(restored.into())
|
||||
}
|
||||
|
||||
pub async fn models_websocket_events<H: Host>(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::client_db::ClientDb;
|
||||
use crate::error::Result;
|
||||
use crate::models::{GrpcRequest, HttpRequest, WebsocketRequest};
|
||||
use crate::models::{AnyModel, GrpcRequest, HttpRequest, WebsocketRequest};
|
||||
use serde_json::Value;
|
||||
|
||||
pub enum AnyRequest {
|
||||
@@ -39,6 +39,12 @@ impl AnyRequest {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AnyRequest> for AnyModel {
|
||||
fn from(request: AnyRequest) -> Self {
|
||||
with_request!(request, |request| request.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> ClientDb<'a> {
|
||||
pub fn get_any_request(&self, id: &str) -> Result<AnyRequest> {
|
||||
if let Ok(http_request) = self.get_http_request(id) {
|
||||
|
||||
@@ -379,7 +379,7 @@ fn dispatch(
|
||||
.queries
|
||||
.with_tx(|tx| tx.restore_request_version(&req.version_id, source))
|
||||
.map_err(js_error)?;
|
||||
to_json(restored.id().to_string())
|
||||
to_json(AnyModel::from(restored))
|
||||
}
|
||||
|
||||
"models_get_settings" => to_json(host.queries.connect().get_settings()),
|
||||
|
||||
Reference in new issue
Block a user