From 59ea7ac390ade95d7c5b5bb8ab971d57cf9b1862 Mon Sep 17 00:00:00 2001 From: pixel-hawk Date: Fri, 2 Jan 2026 14:41:52 +0530 Subject: [PATCH 1/5] feat: add response filter (JSONPath / Xpath) history management for requests --- .envrc | 12 + .gitignore | 11 + devenv.lock | 103 ++++ devenv.nix | 44 ++ .../src/bindings/gen_models.ts | 136 ++++- src-tauri/yaak-git/bindings/gen_models.ts | 118 ++++- src-tauri/yaak-models/bindings/gen_models.ts | 469 +++++++++++++++--- ...60101000000_max-filter-history-setting.sql | 2 + src-tauri/yaak-models/src/models.rs | 9 + .../yaak-models/src/queries/grpc_requests.rs | 1 + .../yaak-models/src/queries/http_requests.rs | 1 + .../yaak-models/src/queries/key_values.rs | 35 ++ .../src/queries/websocket_requests.rs | 1 + src-tauri/yaak-plugins/bindings/gen_models.ts | 136 ++++- src-tauri/yaak-sync/bindings/gen_models.ts | 131 ++++- .../yaak-templates/pkg/yaak_templates.d.ts | 2 +- .../yaak-templates/pkg/yaak_templates_bg.js | 12 +- .../yaak-templates/pkg/yaak_templates_bg.wasm | Bin 68448 -> 68832 bytes .../components/Settings/SettingsGeneral.tsx | 23 + .../responseViewers/FilterHistoryDropdown.tsx | 158 ++++++ .../responseViewers/HTMLOrTextViewer.tsx | 5 + .../components/responseViewers/TextViewer.tsx | 83 +++- src-web/hooks/useInputHistory.ts | 110 ++++ src-web/hooks/useTrackFilterHistory.ts | 84 ++++ src-web/lib/filterType.ts | 9 + src-web/lib/historyCleanup.ts | 56 +++ src-web/lib/historyConstants.ts | 10 + src-web/lib/sanitizeInput.ts | 34 ++ 28 files changed, 1658 insertions(+), 137 deletions(-) create mode 100644 .envrc create mode 100644 devenv.lock create mode 100644 devenv.nix create mode 100644 src-tauri/yaak-models/migrations/20260101000000_max-filter-history-setting.sql create mode 100644 src-web/components/responseViewers/FilterHistoryDropdown.tsx create mode 100644 src-web/hooks/useInputHistory.ts create mode 100644 src-web/hooks/useTrackFilterHistory.ts create mode 100644 src-web/lib/filterType.ts create mode 100644 src-web/lib/historyCleanup.ts create mode 100644 src-web/lib/historyConstants.ts create mode 100644 src-web/lib/sanitizeInput.ts diff --git a/.envrc b/.envrc new file mode 100644 index 000000000..cc5c18b36 --- /dev/null +++ b/.envrc @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +export DIRENV_WARN_TIMEOUT=20s + +eval "$(devenv direnvrc)" + +# `use devenv` supports the same options as the `devenv shell` command. +# +# To silence all output, use `--quiet`. +# +# Example usage: use devenv --quiet --impure --option services.postgres.enable:bool true +use devenv diff --git a/.gitignore b/.gitignore index e49954998..d488b4217 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,14 @@ out tmp .zed codebook.toml + +# Devenv +.devenv* +devenv.local.nix +devenv.local.yaml + +# direnv +.direnv + +# pre-commit +.pre-commit-config.yaml \ No newline at end of file diff --git a/devenv.lock b/devenv.lock new file mode 100644 index 000000000..16b57cae0 --- /dev/null +++ b/devenv.lock @@ -0,0 +1,103 @@ +{ + "nodes": { + "devenv": { + "locked": { + "dir": "src/modules", + "lastModified": 1767288951, + "owner": "cachix", + "repo": "devenv", + "rev": "7f7e03392c9ce626a9ef412d42b3bef2f7f8625e", + "type": "github" + }, + "original": { + "dir": "src/modules", + "owner": "cachix", + "repo": "devenv", + "type": "github" + } + }, + "flake-compat": { + "flake": false, + "locked": { + "lastModified": 1767039857, + "owner": "NixOS", + "repo": "flake-compat", + "rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab", + "type": "github" + }, + "original": { + "owner": "NixOS", + "repo": "flake-compat", + "type": "github" + } + }, + "git-hooks": { + "inputs": { + "flake-compat": "flake-compat", + "gitignore": "gitignore", + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1767281941, + "owner": "cachix", + "repo": "git-hooks.nix", + "rev": "f0927703b7b1c8d97511c4116eb9b4ec6645a0fa", + "type": "github" + }, + "original": { + "owner": "cachix", + "repo": "git-hooks.nix", + "type": "github" + } + }, + "gitignore": { + "inputs": { + "nixpkgs": [ + "git-hooks", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1762808025, + "owner": "hercules-ci", + "repo": "gitignore.nix", + "rev": "cb5e3fdca1de58ccbc3ef53de65bd372b48f567c", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "gitignore.nix", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1767052823, + "owner": "cachix", + "repo": "devenv-nixpkgs", + "rev": "538a5124359f0b3d466e1160378c87887e3b51a4", + "type": "github" + }, + "original": { + "owner": "cachix", + "ref": "rolling", + "repo": "devenv-nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "devenv": "devenv", + "git-hooks": "git-hooks", + "nixpkgs": "nixpkgs", + "pre-commit-hooks": [ + "git-hooks" + ] + } + } + }, + "root": "root", + "version": 7 +} diff --git a/devenv.nix b/devenv.nix new file mode 100644 index 000000000..82102d1ff --- /dev/null +++ b/devenv.nix @@ -0,0 +1,44 @@ +{ pkgs, lib, config, inputs, ... }: + +{ + + # https://devenv.sh/packages/ + packages = with pkgs; [ + git + lld + ]; + + languages.rust.enable = true; + + languages.javascript = { + enable = true; + package = pkgs.nodejs_24; + npm.enable = true; + }; + + scripts.setup.exec = '' + echo "🚀 Setting up Yaak development environment..." + npm install + npm run bootstrap + echo "✅ Setup complete! Run 'npm start' to begin development." + ''; + + enterShell = '' + echo "📦 Yaak Development Environment" + echo "================================" + echo "Node.js: $(node --version)" + echo "NPM: $(npm --version)" + echo "Rust: $(rustc --version)" + ''; + + enterTest = '' + echo "Running tests" + node --version | grep --color=auto "v20" + rustc --version | grep --color=auto "rustc" + ''; + + # https://devenv.sh/git-hooks/ + # git-hooks.hooks.shellcheck.enable = true; + + # See full reference at https://devenv.sh/reference/options/ +} diff --git a/packages/plugin-runtime-types/src/bindings/gen_models.ts b/packages/plugin-runtime-types/src/bindings/gen_models.ts index 454903fe2..d86755f6b 100644 --- a/packages/plugin-runtime-types/src/bindings/gen_models.ts +++ b/packages/plugin-runtime-types/src/bindings/gen_models.ts @@ -1,25 +1,139 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, parentModel: string, parentId: string | null, variables: Array, color: string | null, sortPriority: number, }; +export type Environment = { + model: "environment"; + id: string; + workspaceId: string; + createdAt: string; + updatedAt: string; + name: string; + public: boolean; + parentModel: string; + parentId: string | null; + variables: Array; + color: string | null; + sortPriority: number; +}; -export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, }; +export type EnvironmentVariable = { enabled?: boolean; name: string; value: string; id?: string }; -export type Folder = { model: "folder", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record, authenticationType: string | null, description: string, headers: Array, name: string, sortPriority: number, }; +export type Folder = { + model: "folder"; + id: string; + createdAt: string; + updatedAt: string; + workspaceId: string; + folderId: string | null; + authentication: Record; + authenticationType: string | null; + description: string; + headers: Array; + name: string; + sortPriority: number; +}; -export type GrpcRequest = { model: "grpc_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authenticationType: string | null, authentication: Record, description: string, message: string, metadata: Array, method: string | null, name: string, service: string | null, sortPriority: number, url: string, }; +export type GrpcRequest = { + model: "grpc_request"; + id: string; + createdAt: string; + updatedAt: string; + workspaceId: string; + folderId: string | null; + authenticationType: string | null; + authentication: Record; + description: string; + message: string; + metadata: Array; + method: string | null; + name: string; + service: string | null; + sortPriority: number; + url: string; +}; -export type HttpRequest = { model: "http_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record, authenticationType: string | null, body: Record, bodyType: string | null, description: string, headers: Array, method: string, name: string, sortPriority: number, url: string, urlParameters: Array, }; +export type HttpRequest = { + model: "http_request"; + id: string; + createdAt: string; + updatedAt: string; + workspaceId: string; + folderId: string | null; + authentication: Record; + authenticationType: string | null; + body: Record; + bodyType: string | null; + description: string; + headers: Array; + method: string; + name: string; + sortPriority: number; + url: string; + urlParameters: Array; +}; -export type HttpRequestHeader = { enabled?: boolean, name: string, value: string, id?: string, }; +export type HttpRequestHeader = { enabled?: boolean; name: string; value: string; id?: string }; -export type HttpResponse = { model: "http_response", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, bodyPath: string | null, contentLength: number | null, contentLengthCompressed: number | null, elapsed: number, elapsedHeaders: number, error: string | null, headers: Array, remoteAddr: string | null, requestContentLength: number | null, requestHeaders: Array, status: number, statusReason: string | null, state: HttpResponseState, url: string, version: string | null, }; +export type HttpResponse = { + model: "http_response"; + id: string; + createdAt: string; + updatedAt: string; + workspaceId: string; + requestId: string; + bodyPath: string | null; + contentLength: number | null; + contentLengthCompressed: number | null; + elapsed: number; + elapsedHeaders: number; + error: string | null; + headers: Array; + remoteAddr: string | null; + requestContentLength: number | null; + requestHeaders: Array; + status: number; + statusReason: string | null; + state: HttpResponseState; + url: string; + version: string | null; +}; -export type HttpResponseHeader = { name: string, value: string, }; +export type HttpResponseHeader = { name: string; value: string }; export type HttpResponseState = "initialized" | "connected" | "closed"; -export type HttpUrlParameter = { enabled?: boolean, name: string, value: string, id?: string, }; +export type HttpUrlParameter = { enabled?: boolean; name: string; value: string; id?: string }; -export type WebsocketRequest = { model: "websocket_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record, authenticationType: string | null, description: string, headers: Array, message: string, name: string, sortPriority: number, url: string, urlParameters: Array, }; +export type WebsocketRequest = { + model: "websocket_request"; + id: string; + createdAt: string; + updatedAt: string; + workspaceId: string; + folderId: string | null; + authentication: Record; + authenticationType: string | null; + description: string; + headers: Array; + message: string; + name: string; + sortPriority: number; + url: string; + urlParameters: Array; +}; -export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, authentication: Record, authenticationType: string | null, description: string, headers: Array, name: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, }; +export type Workspace = { + model: "workspace"; + id: string; + createdAt: string; + updatedAt: string; + authentication: Record; + authenticationType: string | null; + description: string; + headers: Array; + name: string; + encryptionKeyChallenge: string | null; + settingValidateCertificates: boolean; + settingFollowRedirects: boolean; + settingRequestTimeout: number; + settingMaxFilterHistory: number; +}; diff --git a/src-tauri/yaak-git/bindings/gen_models.ts b/src-tauri/yaak-git/bindings/gen_models.ts index 84a0f770c..440351f25 100644 --- a/src-tauri/yaak-git/bindings/gen_models.ts +++ b/src-tauri/yaak-git/bindings/gen_models.ts @@ -1,21 +1,119 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, parentModel: string, parentId: string | null, variables: Array, color: string | null, sortPriority: number, }; +export type Environment = { + model: "environment"; + id: string; + workspaceId: string; + createdAt: string; + updatedAt: string; + name: string; + public: boolean; + parentModel: string; + parentId: string | null; + variables: Array; + color: string | null; + sortPriority: number; +}; -export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, }; +export type EnvironmentVariable = { enabled?: boolean; name: string; value: string; id?: string }; -export type Folder = { model: "folder", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record, authenticationType: string | null, description: string, headers: Array, name: string, sortPriority: number, }; +export type Folder = { + model: "folder"; + id: string; + createdAt: string; + updatedAt: string; + workspaceId: string; + folderId: string | null; + authentication: Record; + authenticationType: string | null; + description: string; + headers: Array; + name: string; + sortPriority: number; +}; -export type GrpcRequest = { model: "grpc_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authenticationType: string | null, authentication: Record, description: string, message: string, metadata: Array, method: string | null, name: string, service: string | null, sortPriority: number, url: string, }; +export type GrpcRequest = { + model: "grpc_request"; + id: string; + createdAt: string; + updatedAt: string; + workspaceId: string; + folderId: string | null; + authenticationType: string | null; + authentication: Record; + description: string; + message: string; + metadata: Array; + method: string | null; + name: string; + service: string | null; + sortPriority: number; + url: string; +}; -export type HttpRequest = { model: "http_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record, authenticationType: string | null, body: Record, bodyType: string | null, description: string, headers: Array, method: string, name: string, sortPriority: number, url: string, urlParameters: Array, }; +export type HttpRequest = { + model: "http_request"; + id: string; + createdAt: string; + updatedAt: string; + workspaceId: string; + folderId: string | null; + authentication: Record; + authenticationType: string | null; + body: Record; + bodyType: string | null; + description: string; + headers: Array; + method: string; + name: string; + sortPriority: number; + url: string; + urlParameters: Array; +}; -export type HttpRequestHeader = { enabled?: boolean, name: string, value: string, id?: string, }; +export type HttpRequestHeader = { enabled?: boolean; name: string; value: string; id?: string }; -export type HttpUrlParameter = { enabled?: boolean, name: string, value: string, id?: string, }; +export type HttpUrlParameter = { enabled?: boolean; name: string; value: string; id?: string }; -export type SyncModel = { "type": "workspace" } & Workspace | { "type": "environment" } & Environment | { "type": "folder" } & Folder | { "type": "http_request" } & HttpRequest | { "type": "grpc_request" } & GrpcRequest | { "type": "websocket_request" } & WebsocketRequest; +export type SyncModel = + | ({ type: "workspace" } & Workspace) + | ({ type: "environment" } & Environment) + | ({ type: "folder" } & Folder) + | ({ type: "http_request" } & HttpRequest) + | ({ type: "grpc_request" } & GrpcRequest) + | ({ type: "websocket_request" } & WebsocketRequest); -export type WebsocketRequest = { model: "websocket_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record, authenticationType: string | null, description: string, headers: Array, message: string, name: string, sortPriority: number, url: string, urlParameters: Array, }; +export type WebsocketRequest = { + model: "websocket_request"; + id: string; + createdAt: string; + updatedAt: string; + workspaceId: string; + folderId: string | null; + authentication: Record; + authenticationType: string | null; + description: string; + headers: Array; + message: string; + name: string; + sortPriority: number; + url: string; + urlParameters: Array; +}; -export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, authentication: Record, authenticationType: string | null, description: string, headers: Array, name: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, }; +export type Workspace = { + model: "workspace"; + id: string; + createdAt: string; + updatedAt: string; + authentication: Record; + authenticationType: string | null; + description: string; + headers: Array; + name: string; + encryptionKeyChallenge: string | null; + settingValidateCertificates: boolean; + settingFollowRedirects: boolean; + settingRequestTimeout: number; + settingMaxFilterHistory: number; +}; diff --git a/src-tauri/yaak-models/bindings/gen_models.ts b/src-tauri/yaak-models/bindings/gen_models.ts index 844c0deb0..829a58728 100644 --- a/src-tauri/yaak-models/bindings/gen_models.ts +++ b/src-tauri/yaak-models/bindings/gen_models.ts @@ -1,96 +1,423 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type AnyModel = CookieJar | Environment | Folder | GraphQlIntrospection | GrpcConnection | GrpcEvent | GrpcRequest | HttpRequest | HttpResponse | HttpResponseEvent | KeyValue | Plugin | Settings | SyncState | WebsocketConnection | WebsocketEvent | WebsocketRequest | Workspace | WorkspaceMeta; - -export type ClientCertificate = { host: string, port: number | null, crtFile: string | null, keyFile: string | null, pfxFile: string | null, passphrase: string | null, enabled?: boolean, }; - -export type Cookie = { raw_cookie: string, domain: CookieDomain, expires: CookieExpires, path: [string, boolean], }; - -export type CookieDomain = { "HostOnly": string } | { "Suffix": string } | "NotPresent" | "Empty"; - -export type CookieExpires = { "AtUtc": string } | "SessionEnd"; - -export type CookieJar = { model: "cookie_jar", id: string, createdAt: string, updatedAt: string, workspaceId: string, cookies: Array, name: string, }; +export type AnyModel = + | CookieJar + | Environment + | Folder + | GraphQlIntrospection + | GrpcConnection + | GrpcEvent + | GrpcRequest + | HttpRequest + | HttpResponse + | HttpResponseEvent + | KeyValue + | Plugin + | Settings + | SyncState + | WebsocketConnection + | WebsocketEvent + | WebsocketRequest + | Workspace + | WorkspaceMeta; + +export type ClientCertificate = { + host: string; + port: number | null; + crtFile: string | null; + keyFile: string | null; + pfxFile: string | null; + passphrase: string | null; + enabled?: boolean; +}; + +export type Cookie = { + raw_cookie: string; + domain: CookieDomain; + expires: CookieExpires; + path: [string, boolean]; +}; + +export type CookieDomain = { HostOnly: string } | { Suffix: string } | "NotPresent" | "Empty"; + +export type CookieExpires = { AtUtc: string } | "SessionEnd"; + +export type CookieJar = { + model: "cookie_jar"; + id: string; + createdAt: string; + updatedAt: string; + workspaceId: string; + cookies: Array; + name: string; +}; export type EditorKeymap = "default" | "vim" | "vscode" | "emacs"; -export type EncryptedKey = { encryptedKey: string, }; - -export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, parentModel: string, parentId: string | null, variables: Array, color: string | null, sortPriority: number, }; - -export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, }; - -export type Folder = { model: "folder", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record, authenticationType: string | null, description: string, headers: Array, name: string, sortPriority: number, }; - -export type GraphQlIntrospection = { model: "graphql_introspection", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, content: string | null, }; - -export type GrpcConnection = { model: "grpc_connection", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, elapsed: number, error: string | null, method: string, service: string, status: number, state: GrpcConnectionState, trailers: { [key in string]?: string }, url: string, }; +export type EncryptedKey = { encryptedKey: string }; + +export type Environment = { + model: "environment"; + id: string; + workspaceId: string; + createdAt: string; + updatedAt: string; + name: string; + public: boolean; + parentModel: string; + parentId: string | null; + variables: Array; + color: string | null; + sortPriority: number; +}; + +export type EnvironmentVariable = { enabled?: boolean; name: string; value: string; id?: string }; + +export type Folder = { + model: "folder"; + id: string; + createdAt: string; + updatedAt: string; + workspaceId: string; + folderId: string | null; + authentication: Record; + authenticationType: string | null; + description: string; + headers: Array; + name: string; + sortPriority: number; +}; + +export type GraphQlIntrospection = { + model: "graphql_introspection"; + id: string; + createdAt: string; + updatedAt: string; + workspaceId: string; + requestId: string; + content: string | null; +}; + +export type GrpcConnection = { + model: "grpc_connection"; + id: string; + createdAt: string; + updatedAt: string; + workspaceId: string; + requestId: string; + elapsed: number; + error: string | null; + method: string; + service: string; + status: number; + state: GrpcConnectionState; + trailers: { [key in string]?: string }; + url: string; +}; export type GrpcConnectionState = "initialized" | "connected" | "closed"; -export type GrpcEvent = { model: "grpc_event", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, connectionId: string, content: string, error: string | null, eventType: GrpcEventType, metadata: { [key in string]?: string }, status: number | null, }; - -export type GrpcEventType = "info" | "error" | "client_message" | "server_message" | "connection_start" | "connection_end"; - -export type GrpcRequest = { model: "grpc_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authenticationType: string | null, authentication: Record, description: string, message: string, metadata: Array, method: string | null, name: string, service: string | null, sortPriority: number, url: string, }; - -export type HttpRequest = { model: "http_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record, authenticationType: string | null, body: Record, bodyType: string | null, description: string, headers: Array, method: string, name: string, sortPriority: number, url: string, urlParameters: Array, }; - -export type HttpRequestHeader = { enabled?: boolean, name: string, value: string, id?: string, }; - -export type HttpResponse = { model: "http_response", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, bodyPath: string | null, contentLength: number | null, contentLengthCompressed: number | null, elapsed: number, elapsedHeaders: number, error: string | null, headers: Array, remoteAddr: string | null, requestContentLength: number | null, requestHeaders: Array, status: number, statusReason: string | null, state: HttpResponseState, url: string, version: string | null, }; - -export type HttpResponseEvent = { model: "http_response_event", id: string, createdAt: string, updatedAt: string, workspaceId: string, responseId: string, event: HttpResponseEventData, }; +export type GrpcEvent = { + model: "grpc_event"; + id: string; + createdAt: string; + updatedAt: string; + workspaceId: string; + requestId: string; + connectionId: string; + content: string; + error: string | null; + eventType: GrpcEventType; + metadata: { [key in string]?: string }; + status: number | null; +}; + +export type GrpcEventType = + | "info" + | "error" + | "client_message" + | "server_message" + | "connection_start" + | "connection_end"; + +export type GrpcRequest = { + model: "grpc_request"; + id: string; + createdAt: string; + updatedAt: string; + workspaceId: string; + folderId: string | null; + authenticationType: string | null; + authentication: Record; + description: string; + message: string; + metadata: Array; + method: string | null; + name: string; + service: string | null; + sortPriority: number; + url: string; +}; + +export type HttpRequest = { + model: "http_request"; + id: string; + createdAt: string; + updatedAt: string; + workspaceId: string; + folderId: string | null; + authentication: Record; + authenticationType: string | null; + body: Record; + bodyType: string | null; + description: string; + headers: Array; + method: string; + name: string; + sortPriority: number; + url: string; + urlParameters: Array; +}; + +export type HttpRequestHeader = { enabled?: boolean; name: string; value: string; id?: string }; + +export type HttpResponse = { + model: "http_response"; + id: string; + createdAt: string; + updatedAt: string; + workspaceId: string; + requestId: string; + bodyPath: string | null; + contentLength: number | null; + contentLengthCompressed: number | null; + elapsed: number; + elapsedHeaders: number; + error: string | null; + headers: Array; + remoteAddr: string | null; + requestContentLength: number | null; + requestHeaders: Array; + status: number; + statusReason: string | null; + state: HttpResponseState; + url: string; + version: string | null; +}; + +export type HttpResponseEvent = { + model: "http_response_event"; + id: string; + createdAt: string; + updatedAt: string; + workspaceId: string; + responseId: string; + event: HttpResponseEventData; +}; /** * Serializable representation of HTTP response events for DB storage. * This mirrors `yaak_http::sender::HttpResponseEvent` but with serde support. * The `From` impl is in yaak-http to avoid circular dependencies. */ -export type HttpResponseEventData = { "type": "setting", name: string, value: string, } | { "type": "info", message: string, } | { "type": "redirect", url: string, status: number, behavior: string, } | { "type": "send_url", method: string, path: string, } | { "type": "receive_url", version: string, status: string, } | { "type": "header_up", name: string, value: string, } | { "type": "header_down", name: string, value: string, } | { "type": "chunk_sent", bytes: number, } | { "type": "chunk_received", bytes: number, }; - -export type HttpResponseHeader = { name: string, value: string, }; +export type HttpResponseEventData = + | { type: "setting"; name: string; value: string } + | { type: "info"; message: string } + | { type: "redirect"; url: string; status: number; behavior: string } + | { type: "send_url"; method: string; path: string } + | { type: "receive_url"; version: string; status: string } + | { type: "header_up"; name: string; value: string } + | { type: "header_down"; name: string; value: string } + | { type: "chunk_sent"; bytes: number } + | { type: "chunk_received"; bytes: number }; + +export type HttpResponseHeader = { name: string; value: string }; export type HttpResponseState = "initialized" | "connected" | "closed"; -export type HttpUrlParameter = { enabled?: boolean, name: string, value: string, id?: string, }; - -export type KeyValue = { model: "key_value", id: string, createdAt: string, updatedAt: string, key: string, namespace: string, value: string, }; - -export type ModelChangeEvent = { "type": "upsert", created: boolean, } | { "type": "delete" }; - -export type ModelPayload = { model: AnyModel, updateSource: UpdateSource, change: ModelChangeEvent, }; - -export type ParentAuthentication = { authentication: Record, authenticationType: string | null, }; - -export type ParentHeaders = { headers: Array, }; - -export type Plugin = { model: "plugin", id: string, createdAt: string, updatedAt: string, checkedAt: string | null, directory: string, enabled: boolean, url: string | null, }; - -export type PluginKeyValue = { model: "plugin_key_value", createdAt: string, updatedAt: string, pluginName: string, key: string, value: string, }; - -export type ProxySetting = { "type": "enabled", http: string, https: string, auth: ProxySettingAuth | null, bypass: string, disabled: boolean, } | { "type": "disabled" }; - -export type ProxySettingAuth = { user: string, password: string, }; - -export type Settings = { model: "settings", id: string, createdAt: string, updatedAt: string, appearance: string, clientCertificates: Array, coloredMethods: boolean, editorFont: string | null, editorFontSize: number, editorKeymap: EditorKeymap, editorSoftWrap: boolean, hideWindowControls: boolean, useNativeTitlebar: boolean, interfaceFont: string | null, interfaceFontSize: number, interfaceScale: number, openWorkspaceNewWindow: boolean | null, proxy: ProxySetting | null, themeDark: string, themeLight: string, updateChannel: string, hideLicenseBadge: boolean, autoupdate: boolean, autoDownloadUpdates: boolean, checkNotifications: boolean, }; - -export type SyncState = { model: "sync_state", id: string, workspaceId: string, createdAt: string, updatedAt: string, flushedAt: string, modelId: string, checksum: string, relPath: string, syncDir: string, }; - -export type UpdateSource = { "type": "background" } | { "type": "import" } | { "type": "plugin" } | { "type": "sync" } | { "type": "window", label: string, }; - -export type WebsocketConnection = { model: "websocket_connection", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, elapsed: number, error: string | null, headers: Array, state: WebsocketConnectionState, status: number, url: string, }; +export type HttpUrlParameter = { enabled?: boolean; name: string; value: string; id?: string }; + +export type KeyValue = { + model: "key_value"; + id: string; + createdAt: string; + updatedAt: string; + key: string; + namespace: string; + value: string; +}; + +export type ModelChangeEvent = { type: "upsert"; created: boolean } | { type: "delete" }; + +export type ModelPayload = { + model: AnyModel; + updateSource: UpdateSource; + change: ModelChangeEvent; +}; + +export type ParentAuthentication = { + authentication: Record; + authenticationType: string | null; +}; + +export type ParentHeaders = { headers: Array }; + +export type Plugin = { + model: "plugin"; + id: string; + createdAt: string; + updatedAt: string; + checkedAt: string | null; + directory: string; + enabled: boolean; + url: string | null; +}; + +export type PluginKeyValue = { + model: "plugin_key_value"; + createdAt: string; + updatedAt: string; + pluginName: string; + key: string; + value: string; +}; + +export type ProxySetting = + | { + type: "enabled"; + http: string; + https: string; + auth: ProxySettingAuth | null; + bypass: string; + disabled: boolean; + } + | { type: "disabled" }; + +export type ProxySettingAuth = { user: string; password: string }; + +export type Settings = { + model: "settings"; + id: string; + createdAt: string; + updatedAt: string; + appearance: string; + clientCertificates: Array; + coloredMethods: boolean; + editorFont: string | null; + editorFontSize: number; + editorKeymap: EditorKeymap; + editorSoftWrap: boolean; + hideWindowControls: boolean; + useNativeTitlebar: boolean; + interfaceFont: string | null; + interfaceFontSize: number; + interfaceScale: number; + openWorkspaceNewWindow: boolean | null; + proxy: ProxySetting | null; + themeDark: string; + themeLight: string; + updateChannel: string; + hideLicenseBadge: boolean; + autoupdate: boolean; + autoDownloadUpdates: boolean; + checkNotifications: boolean; +}; + +export type SyncState = { + model: "sync_state"; + id: string; + workspaceId: string; + createdAt: string; + updatedAt: string; + flushedAt: string; + modelId: string; + checksum: string; + relPath: string; + syncDir: string; +}; + +export type UpdateSource = + | { type: "background" } + | { type: "import" } + | { type: "plugin" } + | { type: "sync" } + | { type: "window"; label: string }; + +export type WebsocketConnection = { + model: "websocket_connection"; + id: string; + createdAt: string; + updatedAt: string; + workspaceId: string; + requestId: string; + elapsed: number; + error: string | null; + headers: Array; + state: WebsocketConnectionState; + status: number; + url: string; +}; export type WebsocketConnectionState = "initialized" | "connected" | "closing" | "closed"; -export type WebsocketEvent = { model: "websocket_event", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, connectionId: string, isServer: boolean, message: Array, messageType: WebsocketEventType, }; +export type WebsocketEvent = { + model: "websocket_event"; + id: string; + createdAt: string; + updatedAt: string; + workspaceId: string; + requestId: string; + connectionId: string; + isServer: boolean; + message: Array; + messageType: WebsocketEventType; +}; export type WebsocketEventType = "binary" | "close" | "frame" | "open" | "ping" | "pong" | "text"; export type WebsocketMessageType = "text" | "binary"; -export type WebsocketRequest = { model: "websocket_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record, authenticationType: string | null, description: string, headers: Array, message: string, name: string, sortPriority: number, url: string, urlParameters: Array, }; - -export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, authentication: Record, authenticationType: string | null, description: string, headers: Array, name: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, }; - -export type WorkspaceMeta = { model: "workspace_meta", id: string, workspaceId: string, createdAt: string, updatedAt: string, encryptionKey: EncryptedKey | null, settingSyncDir: string | null, }; +export type WebsocketRequest = { + model: "websocket_request"; + id: string; + createdAt: string; + updatedAt: string; + workspaceId: string; + folderId: string | null; + authentication: Record; + authenticationType: string | null; + description: string; + headers: Array; + message: string; + name: string; + sortPriority: number; + url: string; + urlParameters: Array; +}; + +export type Workspace = { + model: "workspace"; + id: string; + createdAt: string; + updatedAt: string; + authentication: Record; + authenticationType: string | null; + description: string; + headers: Array; + name: string; + encryptionKeyChallenge: string | null; + settingValidateCertificates: boolean; + settingFollowRedirects: boolean; + settingRequestTimeout: number; + settingMaxFilterHistory: number; +}; + +export type WorkspaceMeta = { + model: "workspace_meta"; + id: string; + workspaceId: string; + createdAt: string; + updatedAt: string; + encryptionKey: EncryptedKey | null; + settingSyncDir: string | null; +}; diff --git a/src-tauri/yaak-models/migrations/20260101000000_max-filter-history-setting.sql b/src-tauri/yaak-models/migrations/20260101000000_max-filter-history-setting.sql new file mode 100644 index 000000000..b70adcd0d --- /dev/null +++ b/src-tauri/yaak-models/migrations/20260101000000_max-filter-history-setting.sql @@ -0,0 +1,2 @@ +-- Add setting for maximum filter history items +ALTER TABLE workspaces ADD COLUMN setting_max_filter_history INTEGER NOT NULL DEFAULT 20; diff --git a/src-tauri/yaak-models/src/models.rs b/src-tauri/yaak-models/src/models.rs index 634188968..8cb833950 100644 --- a/src-tauri/yaak-models/src/models.rs +++ b/src-tauri/yaak-models/src/models.rs @@ -296,6 +296,12 @@ pub struct Workspace { #[serde(default = "default_true")] pub setting_follow_redirects: bool, pub setting_request_timeout: i32, + #[serde(default = "default_history_limit")] + pub setting_max_filter_history: i32, +} + +fn default_history_limit() -> i32 { + 20 } impl UpsertModelInfo for Workspace { @@ -336,6 +342,7 @@ impl UpsertModelInfo for Workspace { (SettingFollowRedirects, self.setting_follow_redirects.into()), (SettingRequestTimeout, self.setting_request_timeout.into()), (SettingValidateCertificates, self.setting_validate_certificates.into()), + (SettingMaxFilterHistory, self.setting_max_filter_history.into()), ]) } @@ -352,6 +359,7 @@ impl UpsertModelInfo for Workspace { WorkspaceIden::SettingFollowRedirects, WorkspaceIden::SettingRequestTimeout, WorkspaceIden::SettingValidateCertificates, + WorkspaceIden::SettingMaxFilterHistory, ] } @@ -375,6 +383,7 @@ impl UpsertModelInfo for Workspace { setting_follow_redirects: row.get("setting_follow_redirects")?, setting_request_timeout: row.get("setting_request_timeout")?, setting_validate_certificates: row.get("setting_validate_certificates")?, + setting_max_filter_history: row.get("setting_max_filter_history").unwrap_or(20), }) } } diff --git a/src-tauri/yaak-models/src/queries/grpc_requests.rs b/src-tauri/yaak-models/src/queries/grpc_requests.rs index 10289b44d..53aca78aa 100644 --- a/src-tauri/yaak-models/src/queries/grpc_requests.rs +++ b/src-tauri/yaak-models/src/queries/grpc_requests.rs @@ -20,6 +20,7 @@ impl<'a> DbContext<'a> { source: &UpdateSource, ) -> Result { self.delete_all_grpc_connections_for_request(m.id.as_str(), source)?; + self.delete_filter_history_for_request(m.id.as_str(), source)?; self.delete(m, source) } diff --git a/src-tauri/yaak-models/src/queries/http_requests.rs b/src-tauri/yaak-models/src/queries/http_requests.rs index a4d6fe21e..511416a99 100644 --- a/src-tauri/yaak-models/src/queries/http_requests.rs +++ b/src-tauri/yaak-models/src/queries/http_requests.rs @@ -20,6 +20,7 @@ impl<'a> DbContext<'a> { source: &UpdateSource, ) -> Result { self.delete_all_http_responses_for_request(m.id.as_str(), source)?; + self.delete_filter_history_for_request(m.id.as_str(), source)?; self.delete(m, source) } diff --git a/src-tauri/yaak-models/src/queries/key_values.rs b/src-tauri/yaak-models/src/queries/key_values.rs index 038627d16..44d74417d 100644 --- a/src-tauri/yaak-models/src/queries/key_values.rs +++ b/src-tauri/yaak-models/src/queries/key_values.rs @@ -6,6 +6,7 @@ use chrono::NaiveDateTime; use log::error; use sea_query::{Asterisk, Cond, Expr, Query, SqliteQueryBuilder}; use sea_query_rusqlite::RusqliteBinder; +use std::collections::HashMap; impl<'a> DbContext<'a> { pub fn list_key_values(&self) -> Result> { @@ -173,4 +174,38 @@ impl<'a> DbContext<'a> { self.delete(&kv, source)?; Ok(()) } + + /// Delete filter history for a request + pub fn delete_filter_history_for_request( + &self, + request_id: &str, + source: &UpdateSource, + ) -> Result<()> { + // Use the per-request pattern: request.{requestId}.filter% + let key_prefix = format!("request.{}.filter", request_id); + let search_pattern = format!("{}%", key_prefix); + + let (sql, params) = Query::select() + .from(KeyValueIden::Table) + .column(Asterisk) + .cond_where( + Cond::all() + .add(Expr::col(KeyValueIden::Namespace).eq("no_sync")) + .add(Expr::col(KeyValueIden::Key).like(search_pattern)), + ) + .build_rusqlite(SqliteQueryBuilder); + + let mut stmt = self.conn.prepare(sql.as_str())?; + let items: Vec = stmt + .query_map(&*params.as_params(), KeyValue::from_row)? + .filter_map(|v| v.ok()) + .collect(); + + // Delete each entry + for kv in items { + self.delete(&kv, source)?; + } + + Ok(()) + } } diff --git a/src-tauri/yaak-models/src/queries/websocket_requests.rs b/src-tauri/yaak-models/src/queries/websocket_requests.rs index c45498a92..521c299c3 100644 --- a/src-tauri/yaak-models/src/queries/websocket_requests.rs +++ b/src-tauri/yaak-models/src/queries/websocket_requests.rs @@ -20,6 +20,7 @@ impl<'a> DbContext<'a> { source: &UpdateSource, ) -> Result { self.delete_all_websocket_connections_for_request(websocket_request.id.as_str(), source)?; + self.delete_filter_history_for_request(websocket_request.id.as_str(), source)?; self.delete(websocket_request, source) } diff --git a/src-tauri/yaak-plugins/bindings/gen_models.ts b/src-tauri/yaak-plugins/bindings/gen_models.ts index 454903fe2..d86755f6b 100644 --- a/src-tauri/yaak-plugins/bindings/gen_models.ts +++ b/src-tauri/yaak-plugins/bindings/gen_models.ts @@ -1,25 +1,139 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, parentModel: string, parentId: string | null, variables: Array, color: string | null, sortPriority: number, }; +export type Environment = { + model: "environment"; + id: string; + workspaceId: string; + createdAt: string; + updatedAt: string; + name: string; + public: boolean; + parentModel: string; + parentId: string | null; + variables: Array; + color: string | null; + sortPriority: number; +}; -export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, }; +export type EnvironmentVariable = { enabled?: boolean; name: string; value: string; id?: string }; -export type Folder = { model: "folder", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record, authenticationType: string | null, description: string, headers: Array, name: string, sortPriority: number, }; +export type Folder = { + model: "folder"; + id: string; + createdAt: string; + updatedAt: string; + workspaceId: string; + folderId: string | null; + authentication: Record; + authenticationType: string | null; + description: string; + headers: Array; + name: string; + sortPriority: number; +}; -export type GrpcRequest = { model: "grpc_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authenticationType: string | null, authentication: Record, description: string, message: string, metadata: Array, method: string | null, name: string, service: string | null, sortPriority: number, url: string, }; +export type GrpcRequest = { + model: "grpc_request"; + id: string; + createdAt: string; + updatedAt: string; + workspaceId: string; + folderId: string | null; + authenticationType: string | null; + authentication: Record; + description: string; + message: string; + metadata: Array; + method: string | null; + name: string; + service: string | null; + sortPriority: number; + url: string; +}; -export type HttpRequest = { model: "http_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record, authenticationType: string | null, body: Record, bodyType: string | null, description: string, headers: Array, method: string, name: string, sortPriority: number, url: string, urlParameters: Array, }; +export type HttpRequest = { + model: "http_request"; + id: string; + createdAt: string; + updatedAt: string; + workspaceId: string; + folderId: string | null; + authentication: Record; + authenticationType: string | null; + body: Record; + bodyType: string | null; + description: string; + headers: Array; + method: string; + name: string; + sortPriority: number; + url: string; + urlParameters: Array; +}; -export type HttpRequestHeader = { enabled?: boolean, name: string, value: string, id?: string, }; +export type HttpRequestHeader = { enabled?: boolean; name: string; value: string; id?: string }; -export type HttpResponse = { model: "http_response", id: string, createdAt: string, updatedAt: string, workspaceId: string, requestId: string, bodyPath: string | null, contentLength: number | null, contentLengthCompressed: number | null, elapsed: number, elapsedHeaders: number, error: string | null, headers: Array, remoteAddr: string | null, requestContentLength: number | null, requestHeaders: Array, status: number, statusReason: string | null, state: HttpResponseState, url: string, version: string | null, }; +export type HttpResponse = { + model: "http_response"; + id: string; + createdAt: string; + updatedAt: string; + workspaceId: string; + requestId: string; + bodyPath: string | null; + contentLength: number | null; + contentLengthCompressed: number | null; + elapsed: number; + elapsedHeaders: number; + error: string | null; + headers: Array; + remoteAddr: string | null; + requestContentLength: number | null; + requestHeaders: Array; + status: number; + statusReason: string | null; + state: HttpResponseState; + url: string; + version: string | null; +}; -export type HttpResponseHeader = { name: string, value: string, }; +export type HttpResponseHeader = { name: string; value: string }; export type HttpResponseState = "initialized" | "connected" | "closed"; -export type HttpUrlParameter = { enabled?: boolean, name: string, value: string, id?: string, }; +export type HttpUrlParameter = { enabled?: boolean; name: string; value: string; id?: string }; -export type WebsocketRequest = { model: "websocket_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record, authenticationType: string | null, description: string, headers: Array, message: string, name: string, sortPriority: number, url: string, urlParameters: Array, }; +export type WebsocketRequest = { + model: "websocket_request"; + id: string; + createdAt: string; + updatedAt: string; + workspaceId: string; + folderId: string | null; + authentication: Record; + authenticationType: string | null; + description: string; + headers: Array; + message: string; + name: string; + sortPriority: number; + url: string; + urlParameters: Array; +}; -export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, authentication: Record, authenticationType: string | null, description: string, headers: Array, name: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, }; +export type Workspace = { + model: "workspace"; + id: string; + createdAt: string; + updatedAt: string; + authentication: Record; + authenticationType: string | null; + description: string; + headers: Array; + name: string; + encryptionKeyChallenge: string | null; + settingValidateCertificates: boolean; + settingFollowRedirects: boolean; + settingRequestTimeout: number; + settingMaxFilterHistory: number; +}; diff --git a/src-tauri/yaak-sync/bindings/gen_models.ts b/src-tauri/yaak-sync/bindings/gen_models.ts index 6a397665b..d1c51b664 100644 --- a/src-tauri/yaak-sync/bindings/gen_models.ts +++ b/src-tauri/yaak-sync/bindings/gen_models.ts @@ -1,23 +1,132 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. -export type Environment = { model: "environment", id: string, workspaceId: string, createdAt: string, updatedAt: string, name: string, public: boolean, parentModel: string, parentId: string | null, variables: Array, color: string | null, sortPriority: number, }; +export type Environment = { + model: "environment"; + id: string; + workspaceId: string; + createdAt: string; + updatedAt: string; + name: string; + public: boolean; + parentModel: string; + parentId: string | null; + variables: Array; + color: string | null; + sortPriority: number; +}; -export type EnvironmentVariable = { enabled?: boolean, name: string, value: string, id?: string, }; +export type EnvironmentVariable = { enabled?: boolean; name: string; value: string; id?: string }; -export type Folder = { model: "folder", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record, authenticationType: string | null, description: string, headers: Array, name: string, sortPriority: number, }; +export type Folder = { + model: "folder"; + id: string; + createdAt: string; + updatedAt: string; + workspaceId: string; + folderId: string | null; + authentication: Record; + authenticationType: string | null; + description: string; + headers: Array; + name: string; + sortPriority: number; +}; -export type GrpcRequest = { model: "grpc_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authenticationType: string | null, authentication: Record, description: string, message: string, metadata: Array, method: string | null, name: string, service: string | null, sortPriority: number, url: string, }; +export type GrpcRequest = { + model: "grpc_request"; + id: string; + createdAt: string; + updatedAt: string; + workspaceId: string; + folderId: string | null; + authenticationType: string | null; + authentication: Record; + description: string; + message: string; + metadata: Array; + method: string | null; + name: string; + service: string | null; + sortPriority: number; + url: string; +}; -export type HttpRequest = { model: "http_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record, authenticationType: string | null, body: Record, bodyType: string | null, description: string, headers: Array, method: string, name: string, sortPriority: number, url: string, urlParameters: Array, }; +export type HttpRequest = { + model: "http_request"; + id: string; + createdAt: string; + updatedAt: string; + workspaceId: string; + folderId: string | null; + authentication: Record; + authenticationType: string | null; + body: Record; + bodyType: string | null; + description: string; + headers: Array; + method: string; + name: string; + sortPriority: number; + url: string; + urlParameters: Array; +}; -export type HttpRequestHeader = { enabled?: boolean, name: string, value: string, id?: string, }; +export type HttpRequestHeader = { enabled?: boolean; name: string; value: string; id?: string }; -export type HttpUrlParameter = { enabled?: boolean, name: string, value: string, id?: string, }; +export type HttpUrlParameter = { enabled?: boolean; name: string; value: string; id?: string }; -export type SyncModel = { "type": "workspace" } & Workspace | { "type": "environment" } & Environment | { "type": "folder" } & Folder | { "type": "http_request" } & HttpRequest | { "type": "grpc_request" } & GrpcRequest | { "type": "websocket_request" } & WebsocketRequest; +export type SyncModel = + | ({ type: "workspace" } & Workspace) + | ({ type: "environment" } & Environment) + | ({ type: "folder" } & Folder) + | ({ type: "http_request" } & HttpRequest) + | ({ type: "grpc_request" } & GrpcRequest) + | ({ type: "websocket_request" } & WebsocketRequest); -export type SyncState = { model: "sync_state", id: string, workspaceId: string, createdAt: string, updatedAt: string, flushedAt: string, modelId: string, checksum: string, relPath: string, syncDir: string, }; +export type SyncState = { + model: "sync_state"; + id: string; + workspaceId: string; + createdAt: string; + updatedAt: string; + flushedAt: string; + modelId: string; + checksum: string; + relPath: string; + syncDir: string; +}; -export type WebsocketRequest = { model: "websocket_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record, authenticationType: string | null, description: string, headers: Array, message: string, name: string, sortPriority: number, url: string, urlParameters: Array, }; +export type WebsocketRequest = { + model: "websocket_request"; + id: string; + createdAt: string; + updatedAt: string; + workspaceId: string; + folderId: string | null; + authentication: Record; + authenticationType: string | null; + description: string; + headers: Array; + message: string; + name: string; + sortPriority: number; + url: string; + urlParameters: Array; +}; -export type Workspace = { model: "workspace", id: string, createdAt: string, updatedAt: string, authentication: Record, authenticationType: string | null, description: string, headers: Array, name: string, encryptionKeyChallenge: string | null, settingValidateCertificates: boolean, settingFollowRedirects: boolean, settingRequestTimeout: number, }; +export type Workspace = { + model: "workspace"; + id: string; + createdAt: string; + updatedAt: string; + authentication: Record; + authenticationType: string | null; + description: string; + headers: Array; + name: string; + encryptionKeyChallenge: string | null; + settingValidateCertificates: boolean; + settingFollowRedirects: boolean; + settingRequestTimeout: number; + settingMaxFilterHistory: number; +}; diff --git a/src-tauri/yaak-templates/pkg/yaak_templates.d.ts b/src-tauri/yaak-templates/pkg/yaak_templates.d.ts index 5d24deef5..df962dbc0 100644 --- a/src-tauri/yaak-templates/pkg/yaak_templates.d.ts +++ b/src-tauri/yaak-templates/pkg/yaak_templates.d.ts @@ -1,5 +1,5 @@ /* tslint:disable */ /* eslint-disable */ -export function unescape_template(template: string): any; export function escape_template(template: string): any; export function parse_template(template: string): any; +export function unescape_template(template: string): any; diff --git a/src-tauri/yaak-templates/pkg/yaak_templates_bg.js b/src-tauri/yaak-templates/pkg/yaak_templates_bg.js index 4d11efa69..900d46859 100644 --- a/src-tauri/yaak-templates/pkg/yaak_templates_bg.js +++ b/src-tauri/yaak-templates/pkg/yaak_templates_bg.js @@ -165,10 +165,10 @@ function takeFromExternrefTable0(idx) { * @param {string} template * @returns {any} */ -export function unescape_template(template) { +export function escape_template(template) { const ptr0 = passStringToWasm0(template, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); const len0 = WASM_VECTOR_LEN; - const ret = wasm.unescape_template(ptr0, len0); + const ret = wasm.escape_template(ptr0, len0); if (ret[2]) { throw takeFromExternrefTable0(ret[1]); } @@ -179,10 +179,10 @@ export function unescape_template(template) { * @param {string} template * @returns {any} */ -export function escape_template(template) { +export function parse_template(template) { const ptr0 = passStringToWasm0(template, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); const len0 = WASM_VECTOR_LEN; - const ret = wasm.escape_template(ptr0, len0); + const ret = wasm.parse_template(ptr0, len0); if (ret[2]) { throw takeFromExternrefTable0(ret[1]); } @@ -193,10 +193,10 @@ export function escape_template(template) { * @param {string} template * @returns {any} */ -export function parse_template(template) { +export function unescape_template(template) { const ptr0 = passStringToWasm0(template, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); const len0 = WASM_VECTOR_LEN; - const ret = wasm.parse_template(ptr0, len0); + const ret = wasm.unescape_template(ptr0, len0); if (ret[2]) { throw takeFromExternrefTable0(ret[1]); } diff --git a/src-tauri/yaak-templates/pkg/yaak_templates_bg.wasm b/src-tauri/yaak-templates/pkg/yaak_templates_bg.wasm index dfa7764dff420bc633b4f857ec6680244403f30d..e5203a182605e057146fda38838280488b857c7d 100644 GIT binary patch delta 23021 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

!zwJPKF9C*9YC`p$*-od+lJ$bG}n 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 delta 23589 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

X#m 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 diff --git a/src-web/components/Settings/SettingsGeneral.tsx b/src-web/components/Settings/SettingsGeneral.tsx index 0a6af1a9e..8a91d4324 100644 --- a/src-web/components/Settings/SettingsGeneral.tsx +++ b/src-web/components/Settings/SettingsGeneral.tsx @@ -4,6 +4,7 @@ import { useAtomValue } from 'jotai'; import { activeWorkspaceAtom } from '../../hooks/useActiveWorkspace'; import { useCheckForUpdates } from '../../hooks/useCheckForUpdates'; import { appInfo } from '../../lib/appInfo'; +import { HISTORY_LIMITS } from '../../lib/historyConstants'; import { revealInFinderText } from '../../lib/reveal'; import { CargoFeature } from '../CargoFeature'; import { Checkbox } from '../core/Checkbox'; @@ -119,6 +120,28 @@ export function SettingsGeneral() { type="number" /> + { + const num = Number.parseInt(value, 10); + return num >= HISTORY_LIMITS.MIN_MAX_ITEMS && num <= HISTORY_LIMITS.MAX_MAX_ITEMS; + }} + help={`Number of filter expressions to remember (${HISTORY_LIMITS.MIN_MAX_ITEMS}-${HISTORY_LIMITS.MAX_MAX_ITEMS}). Applies to JSONPath/XPath response filters.`} + onChange={(v) => + patchModel(workspace, { + settingMaxFilterHistory: Number.parseInt(v, 10) || HISTORY_LIMITS.DEFAULT_MAX_ITEMS, + }) + } + type="number" + /> + void; + onRemove: (value: string) => void; + onClearAll: () => void; +} + +export function FilterHistoryDropdown({ + history, + currentValue, + onSelect, + onRemove, + onClearAll, +}: Props) { + const groupedHistory = useMemo(() => { + const now = Date.now(); + const oneDayMs = 24 * 60 * 60 * 1000; + + const today: typeof history = []; + const yesterday: typeof history = []; + const older: typeof history = []; + + for (const item of history) { + const age = now - item.timestamp; + if (age < oneDayMs) { + today.push(item); + } else if (age < 2 * oneDayMs) { + yesterday.push(item); + } else { + older.push(item); + } + } + + return { today, yesterday, older }; + }, [history]); + + const historyItems = useMemo(() => { + const items: DropdownItem[] = []; + + if (history.length === 0) { + return items; + } + + // Actions + items.push({ + type: 'default', + label: 'Clear All History', + leftSlot: , + color: 'danger', + onSelect: onClearAll, + }); + + // Helper to create history item + const createHistoryItem = (item: (typeof history)[0], showDate = false) => { + const dateStr = showDate + ? new Date(item.timestamp).toLocaleDateString(undefined, { + month: 'short', + day: 'numeric', + year: + new Date(item.timestamp).getFullYear() !== new Date().getFullYear() + ? 'numeric' + : undefined, + }) + : null; + + const isSelected = item.value === currentValue; + + // Truncate long filter expressions + const MAX_DISPLAY_LENGTH = 80; + const displayValue = + item.value.length > MAX_DISPLAY_LENGTH + ? `${item.value.slice(0, MAX_DISPLAY_LENGTH)}...` + : item.value; + + return { + type: 'default' as const, + label: ( +

+
+
+ {displayValue} +
+ {dateStr &&
{dateStr}
} +
+
+ {/* biome-ignore lint/a11y/noStaticElementInteractions: Delete action within dropdown menu item + TODO: need to refactor the dropdown components should not render the item as button */} + { + e.stopPropagation(); + onRemove(item.value); + }} + > + + + {isSelected ? : } +
+
+ ), + onSelect: () => onSelect(item.value), + }; + }; + + // Add grouped items + const groups = [ + { label: 'Today', items: groupedHistory.today, showDate: false }, + { label: 'Yesterday', items: groupedHistory.yesterday, showDate: false }, + { label: 'Older', items: groupedHistory.older, showDate: true }, + ]; + + for (const group of groups) { + if (group.items.length > 0) { + items.push({ type: 'separator', label: group.label }); + for (const item of group.items) { + items.push(createHistoryItem(item, group.showDate)); + } + } + } + + return items; + }, [history, currentValue, onSelect, onRemove, onClearAll, groupedHistory]); + + if (history.length === 0) { + return null; + } + + return ( + +
+ + +
+
+ ); +} diff --git a/src-web/components/responseViewers/HTMLOrTextViewer.tsx b/src-web/components/responseViewers/HTMLOrTextViewer.tsx index f1bb5a920..edcbdf8f5 100644 --- a/src-web/components/responseViewers/HTMLOrTextViewer.tsx +++ b/src-web/components/responseViewers/HTMLOrTextViewer.tsx @@ -2,6 +2,7 @@ import type { HttpResponse } from '@yaakapp-internal/models'; import { useMemo, useState } from 'react'; import { useResponseBodyText } from '../../hooks/useResponseBodyText'; import { languageFromContentType } from '../../lib/contentType'; +import { getFilterType } from '../../lib/filterType'; import { getContentTypeFromHeaders } from '../../lib/model_util'; import type { EditorProps } from '../core/Editor/Editor'; import { EmptyStateText } from '../EmptyStateText'; @@ -64,11 +65,15 @@ function HttpTextViewer({ response, text, language, pretty, className }: HttpTex [filteredBody], ); + // Compute history key per request with filter type based on language + const historyStateKey = `request.${response.requestId}.filter.${getFilterType(language)}`; + return ( { @@ -27,10 +31,19 @@ interface Props { const useFilterText = createGlobalState>({}); -export function TextViewer({ language, text, stateKey, pretty, className, onFilter }: Props) { +export function TextViewer({ + language, + text, + stateKey, + historyStateKey, + pretty, + className, + onFilter, +}: Props) { const [filterTextMap, setFilterTextMap] = useFilterText(); const filterText = stateKey ? (filterTextMap[stateKey] ?? null) : null; const debouncedFilterText = useDebouncedValue(filterText); + const setFilterText = useCallback( (v: string | null) => { if (!stateKey) return; @@ -39,6 +52,10 @@ export function TextViewer({ language, text, stateKey, pretty, className, onFilt [setFilterTextMap, stateKey], ); + const { history, addToHistory, clearHistory, removeFromHistory } = useInputHistory({ + stateKey: historyStateKey ?? null, + }); + const isSearching = filterText != null; const filteredResponse = onFilter && debouncedFilterText @@ -55,6 +72,26 @@ export function TextViewer({ language, text, stateKey, pretty, className, onFilt const canFilter = onFilter && (language === 'json' || language === 'xml' || language === 'html'); + const handleFilterCommit = useCallback( + (value: string) => { + if (value.trim() && !filteredResponse.error) { + addToHistory(value); + } + }, + [addToHistory, filteredResponse.error], + ); + + const handleKeyDown = useCallback( + (e: KeyboardEvent) => { + if (e.key === 'Escape') { + toggleSearch(); + } else if (e.key === 'Enter' && filterText) { + handleFilterCommit(filterText); + } + }, + [toggleSearch, filterText, handleFilterCommit], + ); + const actions = useMemo(() => { const nodes: ReactNode[] = []; @@ -63,21 +100,30 @@ export function TextViewer({ language, text, stateKey, pretty, className, onFilt if (isSearching) { nodes.push(
- e.key === 'Escape' && toggleSearch()} - onChange={setFilterText} - stateKey={stateKey ? `filter.${stateKey}` : null} - /> + + + +
, ); } @@ -103,8 +149,13 @@ export function TextViewer({ language, text, stateKey, pretty, className, onFilt isSearching, language, stateKey, + historyStateKey, setFilterText, toggleSearch, + history, + removeFromHistory, + clearHistory, + handleKeyDown, ]); const formattedBody = useFormatText({ text, language, pretty: pretty ?? false }); diff --git a/src-web/hooks/useInputHistory.ts b/src-web/hooks/useInputHistory.ts new file mode 100644 index 000000000..282ba9fc1 --- /dev/null +++ b/src-web/hooks/useInputHistory.ts @@ -0,0 +1,110 @@ +import { useAtomValue } from 'jotai'; +import { useCallback, useMemo } from 'react'; +import { HISTORY_LIMITS } from '../lib/historyConstants'; +import { prepareHistoryInput } from '../lib/sanitizeInput'; +import { showToast } from '../lib/toast'; +import { activeWorkspaceAtom } from './useActiveWorkspace'; +import { useKeyValue } from './useKeyValue'; + +export interface HistoryItem { + value: string; + timestamp: number; +} + +interface UseInputHistoryOptions { + stateKey: string | null; + maxItems?: number; +} + +export function useInputHistory({ stateKey, maxItems }: UseInputHistoryOptions) { + const workspace = useAtomValue(activeWorkspaceAtom); + const defaultMaxItems = workspace?.settingMaxFilterHistory ?? HISTORY_LIMITS.DEFAULT_MAX_ITEMS; + const effectiveMaxItems = maxItems ?? defaultMaxItems; + + const { value: history, set: setHistory } = useKeyValue({ + namespace: 'no_sync', + key: stateKey ?? 'noop', + fallback: [], + }); + + const addToHistory = useCallback( + async (value: string) => { + if (!stateKey) return; + + try { + const sanitized = prepareHistoryInput(value); + if (!sanitized) return; + + // Use Set for deduplication (keep most recent) + const existingValues = new Set((history ?? []).map((item) => item.value)); + + // Remove if exists, will add to front + existingValues.delete(sanitized); + + // Create new history with timestamp + const newItem: HistoryItem = { + value: sanitized, + timestamp: Date.now(), + }; + + const filteredHistory = (history ?? []).filter((item) => item.value !== sanitized); + const updated = [newItem, ...filteredHistory].slice(0, effectiveMaxItems); + + await setHistory(updated); + } catch (error) { + console.error('Failed to add to history:', error); + showToast({ + message: 'Failed to save filter to history', + color: 'danger', + }); + } + }, + [history, stateKey, effectiveMaxItems, setHistory], + ); + + const clearHistory = useCallback(async () => { + if (!stateKey) return; + + try { + await setHistory([]); + showToast({ + message: 'Filter history cleared', + color: 'success', + }); + } catch (error) { + console.error('Failed to clear history:', error); + showToast({ + message: 'Failed to clear filter history', + color: 'danger', + }); + } + }, [stateKey, setHistory, history]); + + const removeFromHistory = useCallback( + async (value: string) => { + if (!stateKey) return; + + try { + const updated = (history ?? []).filter((item) => item.value !== value); + + await setHistory(updated); + } catch (error) { + console.error('Failed to remove from history:', error); + showToast({ + message: 'Failed to remove from history', + color: 'danger', + }); + } + }, + [history, stateKey, setHistory], + ); + + const historyItems = useMemo(() => history ?? [], [history]); + + return { + history: historyItems, + addToHistory, + clearHistory, + removeFromHistory, + }; +} diff --git a/src-web/hooks/useTrackFilterHistory.ts b/src-web/hooks/useTrackFilterHistory.ts new file mode 100644 index 000000000..f88058032 --- /dev/null +++ b/src-web/hooks/useTrackFilterHistory.ts @@ -0,0 +1,84 @@ +import { useEffect } from 'react'; +import { useKeyValue } from './useKeyValue'; + +/** + * Track filter history keys associated with a request for cleanup purposes + * Stores a registry of: requestId -> historyStateKeys[] + */ +export function useTrackFilterHistoryKeys( + requestId: string | null, + historyStateKey: string | null, +) { + const { value: registry, set: setRegistry } = useKeyValue>({ + namespace: 'no_sync', + key: 'filter_history_registry', + fallback: {}, + }); + + useEffect(() => { + if (!requestId || !historyStateKey) return; + + const currentKeys = registry?.[requestId] ?? []; + + // Only update if this key isn't already tracked + if (!currentKeys.includes(historyStateKey)) { + setRegistry({ + ...registry, + [requestId]: [...currentKeys, historyStateKey], + }); + } + }, [requestId, historyStateKey, registry, setRegistry]); +} + +/** + * Get all filter history keys for a request + */ +export async function getFilterHistoryKeysForRequest(requestId: string): Promise { + const { getKeyValue } = await import('../lib/keyValueStore'); + + const registry = getKeyValue>({ + namespace: 'no_sync', + key: 'filter_history_registry', + fallback: {}, + }); + + return registry[requestId] ?? []; +} + +/** + * Clean up filter history for deleted requests + */ +export async function cleanupFilterHistoryForRequest(requestId: string): Promise { + const { setKeyValue } = await import('../lib/keyValueStore'); + + const historyKeys = await getFilterHistoryKeysForRequest(requestId); + + // Clear each history key + for (const key of historyKeys) { + try { + await setKeyValue({ + namespace: 'no_sync', + key: `input_history.${key}`, + value: [], + }); + } catch (error) { + console.error(`Failed to cleanup history for key ${key}:`, error); + } + } + + // Remove from registry + const registry = (await import('../lib/keyValueStore')).getKeyValue>({ + namespace: 'no_sync', + key: 'filter_history_registry', + fallback: {}, + }); + + const updatedRegistry = { ...registry }; + delete updatedRegistry[requestId]; + + await setKeyValue({ + namespace: 'no_sync', + key: 'filter_history_registry', + value: updatedRegistry, + }); +} diff --git a/src-web/lib/filterType.ts b/src-web/lib/filterType.ts new file mode 100644 index 000000000..ba5ece8ea --- /dev/null +++ b/src-web/lib/filterType.ts @@ -0,0 +1,9 @@ +import type { EditorProps } from '../components/core/Editor/Editor'; + +export type FilterType = 'jsonpath' | 'xpath' | 'generic'; + +export function getFilterType(language: EditorProps['language']): FilterType { + if (language === 'json') return 'jsonpath'; + if (language === 'xml' || language === 'html') return 'xpath'; + return 'generic'; +} diff --git a/src-web/lib/historyCleanup.ts b/src-web/lib/historyCleanup.ts new file mode 100644 index 000000000..6ca9dd99f --- /dev/null +++ b/src-web/lib/historyCleanup.ts @@ -0,0 +1,56 @@ +import { keyValuesAtom } from '@yaakapp-internal/models'; +import { jotaiStore } from './jotai'; +import { buildKeyValueKey, setKeyValue } from './keyValueStore'; + +/** + * Clean up history entries for a deleted request/resource + */ +export async function cleanupHistoryForKey(stateKey: string): Promise { + const historyKey = `input_history.filter.${stateKey}`; + + try { + await setKeyValue({ + namespace: 'no_sync', + key: historyKey, + value: [], + }); + } catch (error) { + console.error('Failed to cleanup history:', error); + } +} + +/** + * Copy history from one request to another (for duplication) + */ +export async function copyHistoryForKey(fromStateKey: string, toStateKey: string): Promise { + const fromKey = `input_history.filter.${fromStateKey}`; + const toKey = `input_history.filter.${toStateKey}`; + + try { + const keyValues = jotaiStore.get(keyValuesAtom); + const fromKeyValue = keyValues?.find( + (kv) => buildKeyValueKey(kv.key) === buildKeyValueKey(fromKey), + ); + + if (fromKeyValue?.value) { + const history = JSON.parse(fromKeyValue.value); + await setKeyValue({ + namespace: 'no_sync', + key: toKey, + value: history, + }); + } + } catch (error) { + console.error('Failed to copy history:', error); + } +} + +/** + * Get all history keys for cleanup purposes + */ +export function getAllHistoryKeys(): string[] { + const keyValues = jotaiStore.get(keyValuesAtom); + if (!keyValues) return []; + + return keyValues.filter((kv) => kv.key.startsWith('input_history.')).map((kv) => kv.key); +} diff --git a/src-web/lib/historyConstants.ts b/src-web/lib/historyConstants.ts new file mode 100644 index 000000000..057d73779 --- /dev/null +++ b/src-web/lib/historyConstants.ts @@ -0,0 +1,10 @@ +export const HISTORY_LIMITS = { + DEFAULT_MAX_ITEMS: 20, + MIN_MAX_ITEMS: 10, + MAX_MAX_ITEMS: 200, +} as const; + +export const HISTORY_TYPES = { + FILTER: 'filter', + URL: 'url', +} as const; diff --git a/src-web/lib/sanitizeInput.ts b/src-web/lib/sanitizeInput.ts new file mode 100644 index 000000000..5785ddee1 --- /dev/null +++ b/src-web/lib/sanitizeInput.ts @@ -0,0 +1,34 @@ +/** + * For filter expressions, we use minimal processing + * React automatically escapes rendered content, so XSS is not a concern + */ +export function sanitizeInput(input: string): string { + return input; +} + +/** + * Basic validation for history input + * Note: Validation is minimal since React escapes all rendered content + * and these expressions are not executed as code + */ +export function isValidHistoryInput(input: string): boolean { + // Just ensure it's not empty or only whitespace + return input.trim().length > 0; +} + +/** + * Prepare input for history storage + * Minimal processing - just trim and limit length + */ +export function prepareHistoryInput(input: string): string | null { + const trimmed = input.trim(); + + if (!trimmed) { + return null; + } + + // Limit length to prevent storage abuse (1000 chars is plenty for filters) + const maxLength = 1000; + + return trimmed.slice(0, maxLength); +} From c2427ac8f7933958f6b942f1b610b4907adee5d4 Mon Sep 17 00:00:00 2001 From: pixel-hawk Date: Fri, 2 Jan 2026 14:50:54 +0530 Subject: [PATCH 2/5] resolved conflict --- src-tauri/yaak-templates/pkg/yaak_templates.d.ts | 2 +- src-tauri/yaak-templates/pkg/yaak_templates_bg.js | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src-tauri/yaak-templates/pkg/yaak_templates.d.ts b/src-tauri/yaak-templates/pkg/yaak_templates.d.ts index c881c571c..df962dbc0 100644 --- a/src-tauri/yaak-templates/pkg/yaak_templates.d.ts +++ b/src-tauri/yaak-templates/pkg/yaak_templates.d.ts @@ -1,5 +1,5 @@ /* tslint:disable */ /* eslint-disable */ -export function parse_template(template: string): any; export function escape_template(template: string): any; +export function parse_template(template: string): any; export function unescape_template(template: string): any; diff --git a/src-tauri/yaak-templates/pkg/yaak_templates_bg.js b/src-tauri/yaak-templates/pkg/yaak_templates_bg.js index 75a386480..900d46859 100644 --- a/src-tauri/yaak-templates/pkg/yaak_templates_bg.js +++ b/src-tauri/yaak-templates/pkg/yaak_templates_bg.js @@ -165,10 +165,10 @@ function takeFromExternrefTable0(idx) { * @param {string} template * @returns {any} */ -export function parse_template(template) { +export function escape_template(template) { const ptr0 = passStringToWasm0(template, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); const len0 = WASM_VECTOR_LEN; - const ret = wasm.parse_template(ptr0, len0); + const ret = wasm.escape_template(ptr0, len0); if (ret[2]) { throw takeFromExternrefTable0(ret[1]); } @@ -179,10 +179,10 @@ export function parse_template(template) { * @param {string} template * @returns {any} */ -export function escape_template(template) { +export function parse_template(template) { const ptr0 = passStringToWasm0(template, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); const len0 = WASM_VECTOR_LEN; - const ret = wasm.escape_template(ptr0, len0); + const ret = wasm.parse_template(ptr0, len0); if (ret[2]) { throw takeFromExternrefTable0(ret[1]); } From 09c8dc250acb574a249d3f055313b282d342824e Mon Sep 17 00:00:00 2001 From: pixel-hawk Date: Fri, 2 Jan 2026 15:34:10 +0530 Subject: [PATCH 3/5] chore: remove .envrc and devenv.lock files, update .gitignore to exclude .envrc --- .envrc | 12 ------ .gitignore | 2 + devenv.lock | 103 ---------------------------------------------------- 3 files changed, 2 insertions(+), 115 deletions(-) delete mode 100644 .envrc delete mode 100644 devenv.lock diff --git a/.envrc b/.envrc deleted file mode 100644 index cc5c18b36..000000000 --- a/.envrc +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash - -export DIRENV_WARN_TIMEOUT=20s - -eval "$(devenv direnvrc)" - -# `use devenv` supports the same options as the `devenv shell` command. -# -# To silence all output, use `--quiet`. -# -# Example usage: use devenv --quiet --impure --option services.postgres.enable:bool true -use devenv diff --git a/.gitignore b/.gitignore index 703ce003c..1466d3834 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,5 @@ devenv.local.yaml # pre-commit .pre-commit-config.yaml +.envrc + diff --git a/devenv.lock b/devenv.lock deleted file mode 100644 index 16b57cae0..000000000 --- a/devenv.lock +++ /dev/null @@ -1,103 +0,0 @@ -{ - "nodes": { - "devenv": { - "locked": { - "dir": "src/modules", - "lastModified": 1767288951, - "owner": "cachix", - "repo": "devenv", - "rev": "7f7e03392c9ce626a9ef412d42b3bef2f7f8625e", - "type": "github" - }, - "original": { - "dir": "src/modules", - "owner": "cachix", - "repo": "devenv", - "type": "github" - } - }, - "flake-compat": { - "flake": false, - "locked": { - "lastModified": 1767039857, - "owner": "NixOS", - "repo": "flake-compat", - "rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab", - "type": "github" - }, - "original": { - "owner": "NixOS", - "repo": "flake-compat", - "type": "github" - } - }, - "git-hooks": { - "inputs": { - "flake-compat": "flake-compat", - "gitignore": "gitignore", - "nixpkgs": [ - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1767281941, - "owner": "cachix", - "repo": "git-hooks.nix", - "rev": "f0927703b7b1c8d97511c4116eb9b4ec6645a0fa", - "type": "github" - }, - "original": { - "owner": "cachix", - "repo": "git-hooks.nix", - "type": "github" - } - }, - "gitignore": { - "inputs": { - "nixpkgs": [ - "git-hooks", - "nixpkgs" - ] - }, - "locked": { - "lastModified": 1762808025, - "owner": "hercules-ci", - "repo": "gitignore.nix", - "rev": "cb5e3fdca1de58ccbc3ef53de65bd372b48f567c", - "type": "github" - }, - "original": { - "owner": "hercules-ci", - "repo": "gitignore.nix", - "type": "github" - } - }, - "nixpkgs": { - "locked": { - "lastModified": 1767052823, - "owner": "cachix", - "repo": "devenv-nixpkgs", - "rev": "538a5124359f0b3d466e1160378c87887e3b51a4", - "type": "github" - }, - "original": { - "owner": "cachix", - "ref": "rolling", - "repo": "devenv-nixpkgs", - "type": "github" - } - }, - "root": { - "inputs": { - "devenv": "devenv", - "git-hooks": "git-hooks", - "nixpkgs": "nixpkgs", - "pre-commit-hooks": [ - "git-hooks" - ] - } - } - }, - "root": "root", - "version": 7 -} From 7d85c5487803b780961c02c4b2adec30b8b0aa61 Mon Sep 17 00:00:00 2001 From: pixel-hawk Date: Fri, 2 Jan 2026 18:13:08 +0530 Subject: [PATCH 4/5] feat: enhance FilterHistoryDropdown with pinning functionality and update useInputHistory hook --- src-web/components/core/Dropdown.tsx | 2 +- .../responseViewers/FilterHistoryDropdown.tsx | 62 ++++++++++++++----- .../components/responseViewers/TextViewer.tsx | 3 +- src-web/hooks/useInputHistory.ts | 50 ++++++++++++--- 4 files changed, 94 insertions(+), 23 deletions(-) diff --git a/src-web/components/core/Dropdown.tsx b/src-web/components/core/Dropdown.tsx index 6c9f8c983..40e9efdf1 100644 --- a/src-web/components/core/Dropdown.tsx +++ b/src-web/components/core/Dropdown.tsx @@ -655,7 +655,7 @@ function MenuItem({ className, focused, onFocus, item, onSelect, ...props }: Men color="custom" className={classNames( className, - 'h-xs', // More compact + 'group h-xs', // More compact 'min-w-[8rem] outline-none px-2 mx-1.5 flex whitespace-nowrap', 'focus:bg-surface-highlight focus:text rounded focus:outline-none focus-visible:outline-1', item.color === 'danger' && '!text-danger', diff --git a/src-web/components/responseViewers/FilterHistoryDropdown.tsx b/src-web/components/responseViewers/FilterHistoryDropdown.tsx index c6b8306d3..875a142ab 100644 --- a/src-web/components/responseViewers/FilterHistoryDropdown.tsx +++ b/src-web/components/responseViewers/FilterHistoryDropdown.tsx @@ -1,3 +1,4 @@ +import classNames from 'classnames'; import { useMemo } from 'react'; import type { HistoryItem } from '../../hooks/useInputHistory'; import { CountBadge } from '../core/CountBadge'; @@ -12,6 +13,7 @@ interface Props { onSelect: (value: string) => void; onRemove: (value: string) => void; onClearAll: () => void; + onTogglePin: (value: string) => void; } export function FilterHistoryDropdown({ @@ -20,16 +22,23 @@ export function FilterHistoryDropdown({ onSelect, onRemove, onClearAll, + onTogglePin, }: Props) { const groupedHistory = useMemo(() => { const now = Date.now(); const oneDayMs = 24 * 60 * 60 * 1000; + const pinned: typeof history = []; const today: typeof history = []; const yesterday: typeof history = []; const older: typeof history = []; for (const item of history) { + if (item.pinned) { + pinned.push(item); + continue; + } + const age = now - item.timestamp; if (age < oneDayMs) { today.push(item); @@ -40,7 +49,7 @@ export function FilterHistoryDropdown({ } } - return { today, yesterday, older }; + return { pinned, today, yesterday, older }; }, [history]); const historyItems = useMemo(() => { @@ -84,7 +93,7 @@ export function FilterHistoryDropdown({ return { type: 'default' as const, label: ( -
+
{dateStr}
}
+ {/* {isSelected ? : } */} {/* biome-ignore lint/a11y/noStaticElementInteractions: Delete action within dropdown menu item TODO: need to refactor the dropdown components should not render the item as button */} - { - e.stopPropagation(); - onRemove(item.value); - }} - > - - - {isSelected ? : }
), + leftSlot: isSelected ? : , + rightSlot: ( +
+ { + e.stopPropagation(); + onTogglePin(item.value); + }} + > + + + { + e.stopPropagation(); + onRemove(item.value); + }} + > + + +
+ ), onSelect: () => onSelect(item.value), }; }; // Add grouped items const groups = [ + { label: 'Pinned', items: groupedHistory.pinned, showDate: false }, { label: 'Today', items: groupedHistory.today, showDate: false }, { label: 'Yesterday', items: groupedHistory.yesterday, showDate: false }, { label: 'Older', items: groupedHistory.older, showDate: true }, @@ -132,7 +166,7 @@ export function FilterHistoryDropdown({ } return items; - }, [history, currentValue, onSelect, onRemove, onClearAll, groupedHistory]); + }, [history, currentValue, onSelect, onRemove, onClearAll, onTogglePin, groupedHistory]); if (history.length === 0) { return null; diff --git a/src-web/components/responseViewers/TextViewer.tsx b/src-web/components/responseViewers/TextViewer.tsx index 85bf888cd..17fb07710 100644 --- a/src-web/components/responseViewers/TextViewer.tsx +++ b/src-web/components/responseViewers/TextViewer.tsx @@ -52,7 +52,7 @@ export function TextViewer({ [setFilterTextMap, stateKey], ); - const { history, addToHistory, clearHistory, removeFromHistory } = useInputHistory({ + const { history, addToHistory, clearHistory, removeFromHistory, togglePin } = useInputHistory({ stateKey: historyStateKey ?? null, }); @@ -122,6 +122,7 @@ export function TextViewer({ onSelect={setFilterText} onRemove={removeFromHistory} onClearAll={clearHistory} + onTogglePin={togglePin} />
, diff --git a/src-web/hooks/useInputHistory.ts b/src-web/hooks/useInputHistory.ts index 282ba9fc1..799be1dc3 100644 --- a/src-web/hooks/useInputHistory.ts +++ b/src-web/hooks/useInputHistory.ts @@ -9,6 +9,7 @@ import { useKeyValue } from './useKeyValue'; export interface HistoryItem { value: string; timestamp: number; + pinned?: boolean; } interface UseInputHistoryOptions { @@ -35,20 +36,31 @@ export function useInputHistory({ stateKey, maxItems }: UseInputHistoryOptions) const sanitized = prepareHistoryInput(value); if (!sanitized) return; - // Use Set for deduplication (keep most recent) - const existingValues = new Set((history ?? []).map((item) => item.value)); + // Check if item already exists and is pinned - if so, skip silently + const existingItem = (history ?? []).find((item) => item.value === sanitized); + if (existingItem?.pinned) { + return; + } - // Remove if exists, will add to front - existingValues.delete(sanitized); - - // Create new history with timestamp + // Create new history item const newItem: HistoryItem = { value: sanitized, timestamp: Date.now(), }; const filteredHistory = (history ?? []).filter((item) => item.value !== sanitized); - const updated = [newItem, ...filteredHistory].slice(0, effectiveMaxItems); + + // Separate pinned and unpinned items + const pinnedItems = filteredHistory.filter((item) => item.pinned); + const unpinnedItems = filteredHistory.filter((item) => !item.pinned); + + // Add new item to unpinned and apply limit only to unpinned items + const limitedUnpinned = [newItem, ...unpinnedItems].slice(0, effectiveMaxItems); + + // Combine: pinned items always stay, unpinned items are limited + const updated = [...pinnedItems, ...limitedUnpinned]; + + await setHistory(updated); await setHistory(updated); } catch (error) { @@ -99,6 +111,29 @@ export function useInputHistory({ stateKey, maxItems }: UseInputHistoryOptions) [history, stateKey, setHistory], ); + const togglePin = useCallback( + async (value: string) => { + if (!stateKey) return; + + try { + const updated = (history ?? []).map((item) => + item.value === value ? { ...item, pinned: !item.pinned } : item, + ); + + await setHistory(updated); + + const item = updated.find((i) => i.value === value); + } catch (error) { + console.error('Failed to toggle pin:', error); + showToast({ + message: 'Failed to toggle pin', + color: 'danger', + }); + } + }, + [history, stateKey, setHistory], + ); + const historyItems = useMemo(() => history ?? [], [history]); return { @@ -106,5 +141,6 @@ export function useInputHistory({ stateKey, maxItems }: UseInputHistoryOptions) addToHistory, clearHistory, removeFromHistory, + togglePin, }; } From 9c22c2539efcac3888ffeabbdb67e2fe3e1e8805 Mon Sep 17 00:00:00 2001 From: pixel-hawk Date: Fri, 2 Jan 2026 21:39:43 +0530 Subject: [PATCH 5/5] feat: update FilterHistoryDropdown and TextViewer to support togglePin functionality; refactor useInputHistory for improved state management --- packages/plugin-runtime/src/PluginInstance.ts | 2 +- .../responseViewers/FilterHistoryDropdown.tsx | 11 ++++++----- src-web/components/responseViewers/TextViewer.tsx | 1 + src-web/hooks/useInputHistory.ts | 8 ++++++-- 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/packages/plugin-runtime/src/PluginInstance.ts b/packages/plugin-runtime/src/PluginInstance.ts index 990a9b434..dc4d634ce 100644 --- a/packages/plugin-runtime/src/PluginInstance.ts +++ b/packages/plugin-runtime/src/PluginInstance.ts @@ -308,7 +308,7 @@ export class PluginInstance { payload.values = applyFormInputDefaults(args, payload.values); const resolvedArgs = await applyDynamicFormInput(ctx, args, payload); const resolvedActions: HttpAuthenticationAction[] = []; - for (const { onSelect, ...action } of actions ?? []) { + for (const { onSelect: _onSelect, ...action } of actions ?? []) { resolvedActions.push(action); } diff --git a/src-web/components/responseViewers/FilterHistoryDropdown.tsx b/src-web/components/responseViewers/FilterHistoryDropdown.tsx index 875a142ab..272c7fb9f 100644 --- a/src-web/components/responseViewers/FilterHistoryDropdown.tsx +++ b/src-web/components/responseViewers/FilterHistoryDropdown.tsx @@ -103,17 +103,15 @@ export function FilterHistoryDropdown({ {dateStr &&
{dateStr}
} -
- {/* {isSelected ? : } */} - {/* biome-ignore lint/a11y/noStaticElementInteractions: Delete action within dropdown menu item - TODO: need to refactor the dropdown components should not render the item as button */} -
), leftSlot: isSelected ? : , rightSlot: (
+ {/* biome-ignore lint/a11y/useSemanticElements: Cannot use button as parent dropdown item is already a button */} + {/* biome-ignore lint/a11y/useSemanticElements: Cannot use button as parent dropdown item is already a button */} { diff --git a/src-web/components/responseViewers/TextViewer.tsx b/src-web/components/responseViewers/TextViewer.tsx index 17fb07710..2197c0d2a 100644 --- a/src-web/components/responseViewers/TextViewer.tsx +++ b/src-web/components/responseViewers/TextViewer.tsx @@ -156,6 +156,7 @@ export function TextViewer({ history, removeFromHistory, clearHistory, + togglePin, handleKeyDown, ]); diff --git a/src-web/hooks/useInputHistory.ts b/src-web/hooks/useInputHistory.ts index 799be1dc3..1babca001 100644 --- a/src-web/hooks/useInputHistory.ts +++ b/src-web/hooks/useInputHistory.ts @@ -90,7 +90,7 @@ export function useInputHistory({ stateKey, maxItems }: UseInputHistoryOptions) color: 'danger', }); } - }, [stateKey, setHistory, history]); + }, [stateKey, setHistory]); const removeFromHistory = useCallback( async (value: string) => { @@ -122,7 +122,11 @@ export function useInputHistory({ stateKey, maxItems }: UseInputHistoryOptions) await setHistory(updated); - const item = updated.find((i) => i.value === value); + const pinnedItem = updated.find((i) => i.value === value); + showToast({ + message: pinnedItem?.pinned ? 'Filter pinned' : 'Filter unpinned', + color: 'success', + }); } catch (error) { console.error('Failed to toggle pin:', error); showToast({