mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-23 01:08:28 +02:00
Better multi-window updates
This commit is contained in:
22
package-lock.json
generated
22
package-lock.json
generated
@@ -39,7 +39,6 @@
|
|||||||
"react-dnd-html5-backend": "^16.0.1",
|
"react-dnd-html5-backend": "^16.0.1",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-helmet-async": "^1.3.0",
|
"react-helmet-async": "^1.3.0",
|
||||||
"react-hook-form": "^7.43.8",
|
|
||||||
"react-router-dom": "^6.8.1",
|
"react-router-dom": "^6.8.1",
|
||||||
"react-use": "^17.4.0",
|
"react-use": "^17.4.0",
|
||||||
"uuid": "^9.0.0"
|
"uuid": "^9.0.0"
|
||||||
@@ -6335,21 +6334,6 @@
|
|||||||
"react-dom": "^16.6.0 || ^17.0.0 || ^18.0.0"
|
"react-dom": "^16.6.0 || ^17.0.0 || ^18.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/react-hook-form": {
|
|
||||||
"version": "7.43.8",
|
|
||||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.43.8.tgz",
|
|
||||||
"integrity": "sha512-BQm+Ge5KjTk1EchDBRhdP8Pkb7MArO2jFF+UWYr3rtvh6197khi22uloLqlWeuY02ItlCzPunPsFt1/q9wQKnw==",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=12.22.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/react-hook-form"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"react": "^16.8.0 || ^17 || ^18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/react-is": {
|
"node_modules/react-is": {
|
||||||
"version": "16.13.1",
|
"version": "16.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||||
@@ -12123,12 +12107,6 @@
|
|||||||
"shallowequal": "^1.1.0"
|
"shallowequal": "^1.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"react-hook-form": {
|
|
||||||
"version": "7.43.8",
|
|
||||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.43.8.tgz",
|
|
||||||
"integrity": "sha512-BQm+Ge5KjTk1EchDBRhdP8Pkb7MArO2jFF+UWYr3rtvh6197khi22uloLqlWeuY02ItlCzPunPsFt1/q9wQKnw==",
|
|
||||||
"requires": {}
|
|
||||||
},
|
|
||||||
"react-is": {
|
"react-is": {
|
||||||
"version": "16.13.1",
|
"version": "16.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ use std::fs::create_dir_all;
|
|||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
use http::header::{HeaderName, ACCEPT, USER_AGENT};
|
use http::header::{HeaderName, ACCEPT, USER_AGENT};
|
||||||
use http::{HeaderMap, HeaderValue, Method};
|
use http::{HeaderMap, HeaderValue, Method};
|
||||||
|
use objc::runtime::ivar_getOffset;
|
||||||
use reqwest::redirect::Policy;
|
use reqwest::redirect::Policy;
|
||||||
|
use serde::Serialize;
|
||||||
use sqlx::migrate::Migrator;
|
use sqlx::migrate::Migrator;
|
||||||
use sqlx::sqlite::SqlitePoolOptions;
|
use sqlx::sqlite::SqlitePoolOptions;
|
||||||
use sqlx::types::Json;
|
use sqlx::types::Json;
|
||||||
@@ -27,8 +29,6 @@ use tokio::sync::Mutex;
|
|||||||
|
|
||||||
use window_ext::WindowExt;
|
use window_ext::WindowExt;
|
||||||
|
|
||||||
use crate::models::generate_id;
|
|
||||||
|
|
||||||
mod models;
|
mod models;
|
||||||
mod runtime;
|
mod runtime;
|
||||||
mod window_ext;
|
mod window_ext;
|
||||||
@@ -101,6 +101,8 @@ async fn actually_send_ephemeral_request(
|
|||||||
url_string = format!("http://{}", url_string);
|
url_string = format!("http://{}", url_string);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
println!("Sending request to {}", url_string);
|
||||||
|
|
||||||
let client = reqwest::Client::builder()
|
let client = reqwest::Client::builder()
|
||||||
.redirect(Policy::none())
|
.redirect(Policy::none())
|
||||||
.build()
|
.build()
|
||||||
@@ -114,7 +116,7 @@ async fn actually_send_ephemeral_request(
|
|||||||
if h.name.is_empty() && h.value.is_empty() {
|
if h.name.is_empty() && h.value.is_empty() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if h.enabled == false {
|
if !h.enabled {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let header_name = match HeaderName::from_bytes(h.name.as_bytes()) {
|
let header_name = match HeaderName::from_bytes(h.name.as_bytes()) {
|
||||||
@@ -214,10 +216,7 @@ async fn actually_send_ephemeral_request(
|
|||||||
response = models::update_response_if_id(response, window.label(), pool)
|
response = models::update_response_if_id(response, window.label(), pool)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to update response");
|
.expect("Failed to update response");
|
||||||
window
|
emit_all_others(&window, "updated_response", &response);
|
||||||
.app_handle()
|
|
||||||
.emit_all("updated_response", &response)
|
|
||||||
.unwrap();
|
|
||||||
Ok(response)
|
Ok(response)
|
||||||
}
|
}
|
||||||
Err(e) => response_err(response, e.to_string(), window, pool).await,
|
Err(e) => response_err(response, e.to_string(), window, pool).await,
|
||||||
@@ -240,10 +239,7 @@ async fn send_request(
|
|||||||
models::create_response(&req.id, 0, "", 0, None, "", vec![], window.label(), pool)
|
models::create_response(&req.id, 0, "", 0, None, "", vec![], window.label(), pool)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to create response");
|
.expect("Failed to create response");
|
||||||
window
|
emit_all_others(&window, "updated_response", &response);
|
||||||
.app_handle()
|
|
||||||
.emit_all("updated_response", &response)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
actually_send_ephemeral_request(req, response, window, pool).await?;
|
actually_send_ephemeral_request(req, response, window, pool).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -259,10 +255,7 @@ async fn response_err(
|
|||||||
response = models::update_response_if_id(response, window.label(), pool)
|
response = models::update_response_if_id(response, window.label(), pool)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to update response");
|
.expect("Failed to update response");
|
||||||
window
|
emit_all_others(&window, "updated_response", &response);
|
||||||
.app_handle()
|
|
||||||
.emit_all("updated_response", &response)
|
|
||||||
.unwrap();
|
|
||||||
Ok(response)
|
Ok(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,7 +275,7 @@ async fn set_key_value(
|
|||||||
namespace: &str,
|
namespace: &str,
|
||||||
key: &str,
|
key: &str,
|
||||||
value: &str,
|
value: &str,
|
||||||
app_handle: AppHandle<Wry>,
|
window: Window<Wry>,
|
||||||
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
|
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let pool = &*db_instance.lock().await;
|
let pool = &*db_instance.lock().await;
|
||||||
@@ -290,9 +283,7 @@ async fn set_key_value(
|
|||||||
.await
|
.await
|
||||||
.expect("Failed to create key value");
|
.expect("Failed to create key value");
|
||||||
|
|
||||||
app_handle
|
emit_all_others(&window, "updated_key_value", &created_key_value);
|
||||||
.emit_all("updated_key_value", &created_key_value)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -308,10 +299,7 @@ async fn create_workspace(
|
|||||||
.await
|
.await
|
||||||
.expect("Failed to create workspace");
|
.expect("Failed to create workspace");
|
||||||
|
|
||||||
window
|
emit_all_others(&window, "updated_workspace", &created_workspace);
|
||||||
.app_handle()
|
|
||||||
.emit_all("updated_workspace", &created_workspace)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
Ok(created_workspace.id)
|
Ok(created_workspace.id)
|
||||||
}
|
}
|
||||||
@@ -344,10 +332,7 @@ async fn create_request(
|
|||||||
.await
|
.await
|
||||||
.expect("Failed to create request");
|
.expect("Failed to create request");
|
||||||
|
|
||||||
window
|
emit_all_others(&window, "updated_request", &created_request);
|
||||||
.app_handle()
|
|
||||||
.emit_all("updated_request", &created_request)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
Ok(created_request.id)
|
Ok(created_request.id)
|
||||||
}
|
}
|
||||||
@@ -362,10 +347,7 @@ async fn duplicate_request(
|
|||||||
let request = models::duplicate_request(id, window.label(), pool)
|
let request = models::duplicate_request(id, window.label(), pool)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to duplicate request");
|
.expect("Failed to duplicate request");
|
||||||
window
|
emit_all_others(&window, "updated_request", &request);
|
||||||
.app_handle()
|
|
||||||
.emit_all("updated_request", &request)
|
|
||||||
.unwrap();
|
|
||||||
Ok(request.id)
|
Ok(request.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -406,17 +388,14 @@ async fn update_request(
|
|||||||
.await
|
.await
|
||||||
.expect("Failed to update request");
|
.expect("Failed to update request");
|
||||||
|
|
||||||
window
|
emit_all_others(&window, "updated_request", updated_request);
|
||||||
.app_handle()
|
|
||||||
.emit_all("updated_request", updated_request)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn delete_request(
|
async fn delete_request(
|
||||||
app_handle: AppHandle<Wry>,
|
window: Window<Wry>,
|
||||||
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
|
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
|
||||||
request_id: &str,
|
request_id: &str,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
@@ -424,7 +403,7 @@ async fn delete_request(
|
|||||||
let req = models::delete_request(request_id, pool)
|
let req = models::delete_request(request_id, pool)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to delete request");
|
.expect("Failed to delete request");
|
||||||
app_handle.emit_all("deleted_model", req).unwrap();
|
emit_all_others(&window, "deleted_model", req);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -464,14 +443,14 @@ async fn responses(
|
|||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn delete_response(
|
async fn delete_response(
|
||||||
id: &str,
|
id: &str,
|
||||||
app_handle: AppHandle<Wry>,
|
window: Window<Wry>,
|
||||||
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
|
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
let pool = &*db_instance.lock().await;
|
let pool = &*db_instance.lock().await;
|
||||||
let response = models::delete_response(id, pool)
|
let response = models::delete_response(id, pool)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to delete response");
|
.expect("Failed to delete response");
|
||||||
app_handle.emit_all("deleted_model", response).unwrap();
|
emit_all_others(&window, "deleted_model", response);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -512,7 +491,7 @@ async fn workspaces(
|
|||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn delete_workspace(
|
async fn delete_workspace(
|
||||||
app_handle: AppHandle<Wry>,
|
window: Window<Wry>,
|
||||||
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
|
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
|
||||||
id: &str,
|
id: &str,
|
||||||
) -> Result<(), String> {
|
) -> Result<(), String> {
|
||||||
@@ -520,7 +499,7 @@ async fn delete_workspace(
|
|||||||
let workspace = models::delete_workspace(id, pool)
|
let workspace = models::delete_workspace(id, pool)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to delete workspace");
|
.expect("Failed to delete workspace");
|
||||||
app_handle.emit_all("deleted_model", workspace).unwrap();
|
emit_all_others(&window, "deleted_model", workspace);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -715,7 +694,7 @@ async fn get_or_create_client_id(pool: &Pool<Sqlite>) -> String {
|
|||||||
match models::get_key_value("global", "client_id", pool).await {
|
match models::get_key_value("global", "client_id", pool).await {
|
||||||
Some(kv) => kv.value,
|
Some(kv) => kv.value,
|
||||||
None => {
|
None => {
|
||||||
let id = &generate_id("yaak");
|
let id = &models::generate_id("yaak");
|
||||||
models::set_key_value("global", "client_id", id, pool)
|
models::set_key_value("global", "client_id", id, pool)
|
||||||
.await
|
.await
|
||||||
.expect("Failed to set client id")
|
.expect("Failed to set client id")
|
||||||
@@ -723,3 +702,14 @@ async fn get_or_create_client_id(pool: &Pool<Sqlite>) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Emit an event to all windows except the current one
|
||||||
|
fn emit_all_others<S: Serialize + Clone>(current_window: &Window<Wry>, event: &str, payload: S) {
|
||||||
|
let windows = current_window.app_handle().windows();
|
||||||
|
for window in windows.values() {
|
||||||
|
if window.label() == current_window.label() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
window.emit(event, &payload).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,33 +51,6 @@ await listen(
|
|||||||
}, UPDATE_DEBOUNCE_MILLIS),
|
}, UPDATE_DEBOUNCE_MILLIS),
|
||||||
);
|
);
|
||||||
|
|
||||||
await listen(
|
|
||||||
'updated_request',
|
|
||||||
debounce(({ payload: request }: { payload: HttpRequest }) => {
|
|
||||||
if (request.updatedBy === appWindow.label) return;
|
|
||||||
|
|
||||||
queryClient.setQueryData(
|
|
||||||
requestsQueryKey(request.workspaceId),
|
|
||||||
(requests: HttpRequest[] = []) => {
|
|
||||||
const newRequests = [];
|
|
||||||
let found = false;
|
|
||||||
for (const r of requests) {
|
|
||||||
if (r.id === request.id) {
|
|
||||||
found = true;
|
|
||||||
newRequests.push(request);
|
|
||||||
} else {
|
|
||||||
newRequests.push(r);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!found) {
|
|
||||||
newRequests.push(request);
|
|
||||||
}
|
|
||||||
return newRequests;
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}, UPDATE_DEBOUNCE_MILLIS),
|
|
||||||
);
|
|
||||||
|
|
||||||
await listen('updated_response', ({ payload: response }: { payload: HttpResponse }) => {
|
await listen('updated_response', ({ payload: response }: { payload: HttpResponse }) => {
|
||||||
queryClient.setQueryData(
|
queryClient.setQueryData(
|
||||||
responsesQueryKey(response.requestId),
|
responsesQueryKey(response.requestId),
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import type { Extension } from '@codemirror/state';
|
import type { Extension } from '@codemirror/state';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { useUniqueKey } from '../hooks/useUniqueKey';
|
|
||||||
import type { HttpRequest } from '../lib/models';
|
import type { HttpRequest } from '../lib/models';
|
||||||
import { sendEphemeralRequest } from '../lib/sendEphemeralRequest';
|
import { sendEphemeralRequest } from '../lib/sendEphemeralRequest';
|
||||||
import type { EditorProps } from './core/Editor';
|
import type { EditorProps } from './core/Editor';
|
||||||
@@ -13,7 +12,10 @@ import {
|
|||||||
} from './core/Editor';
|
} from './core/Editor';
|
||||||
import { Separator } from './core/Separator';
|
import { Separator } from './core/Separator';
|
||||||
|
|
||||||
type Props = Pick<EditorProps, 'heightMode' | 'onChange' | 'defaultValue' | 'className'> & {
|
type Props = Pick<
|
||||||
|
EditorProps,
|
||||||
|
'heightMode' | 'onChange' | 'defaultValue' | 'className' | 'forceUpdateKey'
|
||||||
|
> & {
|
||||||
baseRequest: HttpRequest;
|
baseRequest: HttpRequest;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -24,7 +26,6 @@ interface GraphQLBody {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function GraphQLEditor({ defaultValue, onChange, baseRequest, ...extraEditorProps }: Props) {
|
export function GraphQLEditor({ defaultValue, onChange, baseRequest, ...extraEditorProps }: Props) {
|
||||||
const queryKey = useUniqueKey();
|
|
||||||
const { query, variables } = useMemo<GraphQLBody>(() => {
|
const { query, variables } = useMemo<GraphQLBody>(() => {
|
||||||
if (!defaultValue) {
|
if (!defaultValue) {
|
||||||
return { query: '', variables: {} };
|
return { query: '', variables: {} };
|
||||||
@@ -79,7 +80,6 @@ export function GraphQLEditor({ defaultValue, onChange, baseRequest, ...extraEdi
|
|||||||
return (
|
return (
|
||||||
<div className="pb-2 h-full grid grid-rows-[minmax(0,100%)_auto_auto_minmax(0,auto)]">
|
<div className="pb-2 h-full grid grid-rows-[minmax(0,100%)_auto_auto_minmax(0,auto)]">
|
||||||
<Editor
|
<Editor
|
||||||
key={queryKey.key}
|
|
||||||
heightMode="auto"
|
heightMode="auto"
|
||||||
defaultValue={query ?? ''}
|
defaultValue={query ?? ''}
|
||||||
languageExtension={graphqlExtension}
|
languageExtension={graphqlExtension}
|
||||||
|
|||||||
@@ -9,15 +9,17 @@ import type { PairEditorProps } from './core/PairEditor';
|
|||||||
import { PairEditor } from './core/PairEditor';
|
import { PairEditor } from './core/PairEditor';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
forceUpdateKey: string;
|
||||||
headers: HttpRequest['headers'];
|
headers: HttpRequest['headers'];
|
||||||
onChange: (headers: HttpRequest['headers']) => void;
|
onChange: (headers: HttpRequest['headers']) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function HeaderEditor({ headers, onChange }: Props) {
|
export function HeaderEditor({ headers, onChange, forceUpdateKey }: Props) {
|
||||||
return (
|
return (
|
||||||
<PairEditor
|
<PairEditor
|
||||||
pairs={headers}
|
pairs={headers}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
forceUpdateKey={forceUpdateKey}
|
||||||
nameValidate={validateHttpHeader}
|
nameValidate={validateHttpHeader}
|
||||||
nameAutocomplete={nameAutocomplete}
|
nameAutocomplete={nameAutocomplete}
|
||||||
valueAutocomplete={valueAutocomplete}
|
valueAutocomplete={valueAutocomplete}
|
||||||
|
|||||||
@@ -2,10 +2,18 @@ import type { HttpRequest } from '../lib/models';
|
|||||||
import { PairEditor } from './core/PairEditor';
|
import { PairEditor } from './core/PairEditor';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
forceUpdateKey: string;
|
||||||
parameters: { name: string; value: string }[];
|
parameters: { name: string; value: string }[];
|
||||||
onChange: (headers: HttpRequest['headers']) => void;
|
onChange: (headers: HttpRequest['headers']) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function ParametersEditor({ parameters, onChange }: Props) {
|
export function ParametersEditor({ parameters, forceUpdateKey, onChange }: Props) {
|
||||||
return <PairEditor pairs={parameters} onChange={onChange} namePlaceholder="name" />;
|
return (
|
||||||
|
<PairEditor
|
||||||
|
forceUpdateKey={forceUpdateKey}
|
||||||
|
pairs={parameters}
|
||||||
|
onChange={onChange}
|
||||||
|
namePlaceholder="name"
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import type { CSSProperties } from 'react';
|
|||||||
import { memo, useCallback, useMemo, useState } from 'react';
|
import { memo, useCallback, useMemo, useState } from 'react';
|
||||||
import { useActiveRequest } from '../hooks/useActiveRequest';
|
import { useActiveRequest } from '../hooks/useActiveRequest';
|
||||||
import { useKeyValue } from '../hooks/useKeyValue';
|
import { useKeyValue } from '../hooks/useKeyValue';
|
||||||
|
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
|
||||||
import { useUpdateRequest } from '../hooks/useUpdateRequest';
|
import { useUpdateRequest } from '../hooks/useUpdateRequest';
|
||||||
import { useWindowFocus } from '../hooks/useWindowFocus';
|
|
||||||
import { tryFormatJson } from '../lib/formatters';
|
import { tryFormatJson } from '../lib/formatters';
|
||||||
import type { HttpHeader, HttpRequest } from '../lib/models';
|
import type { HttpHeader, HttpRequest } from '../lib/models';
|
||||||
import {
|
import {
|
||||||
@@ -125,13 +125,7 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
const visible = useWindowFocus();
|
const { updateKey: forceUpdateKey } = useRequestUpdateKey(activeRequest?.id ?? null);
|
||||||
const multiWindowKey = useMemo(() => {
|
|
||||||
// If the window has focus, don't ever force an update
|
|
||||||
if (visible) return undefined;
|
|
||||||
// If the window is not focused, force an update if the request has been updated
|
|
||||||
return activeRequest?.updatedAt;
|
|
||||||
}, [visible, activeRequest?.updatedAt]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -140,12 +134,7 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN
|
|||||||
>
|
>
|
||||||
{activeRequest && (
|
{activeRequest && (
|
||||||
<>
|
<>
|
||||||
<UrlBar
|
<UrlBar id={activeRequest.id} url={activeRequest.url} method={activeRequest.method} />
|
||||||
key={multiWindowKey}
|
|
||||||
id={activeRequest.id}
|
|
||||||
url={activeRequest.url}
|
|
||||||
method={activeRequest.method}
|
|
||||||
/>
|
|
||||||
<Tabs
|
<Tabs
|
||||||
value={activeTab.value}
|
value={activeTab.value}
|
||||||
onChangeValue={activeTab.set}
|
onChangeValue={activeTab.set}
|
||||||
@@ -156,13 +145,13 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN
|
|||||||
<TabContent value="auth">
|
<TabContent value="auth">
|
||||||
{activeRequest.authenticationType === AUTH_TYPE_BASIC ? (
|
{activeRequest.authenticationType === AUTH_TYPE_BASIC ? (
|
||||||
<BasicAuth
|
<BasicAuth
|
||||||
key={multiWindowKey}
|
key={forceUpdateKey}
|
||||||
requestId={activeRequest.id}
|
requestId={activeRequest.id}
|
||||||
authentication={activeRequest.authentication}
|
authentication={activeRequest.authentication}
|
||||||
/>
|
/>
|
||||||
) : activeRequest.authenticationType === AUTH_TYPE_BEARER ? (
|
) : activeRequest.authenticationType === AUTH_TYPE_BEARER ? (
|
||||||
<BearerAuth
|
<BearerAuth
|
||||||
key={multiWindowKey}
|
key={forceUpdateKey}
|
||||||
requestId={activeRequest.id}
|
requestId={activeRequest.id}
|
||||||
authentication={activeRequest.authentication}
|
authentication={activeRequest.authentication}
|
||||||
/>
|
/>
|
||||||
@@ -174,18 +163,22 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN
|
|||||||
</TabContent>
|
</TabContent>
|
||||||
<TabContent value="headers">
|
<TabContent value="headers">
|
||||||
<HeaderEditor
|
<HeaderEditor
|
||||||
key={`${forceUpdateHeaderEditorKey}::${multiWindowKey}`}
|
forceUpdateKey={`${forceUpdateHeaderEditorKey}::${forceUpdateKey}`}
|
||||||
headers={activeRequest.headers}
|
headers={activeRequest.headers}
|
||||||
onChange={handleHeadersChange}
|
onChange={handleHeadersChange}
|
||||||
/>
|
/>
|
||||||
</TabContent>
|
</TabContent>
|
||||||
<TabContent value="params">
|
<TabContent value="params">
|
||||||
<ParametersEditor key={multiWindowKey} parameters={[]} onChange={() => null} />
|
<ParametersEditor
|
||||||
|
forceUpdateKey={forceUpdateKey}
|
||||||
|
parameters={[]}
|
||||||
|
onChange={() => null}
|
||||||
|
/>
|
||||||
</TabContent>
|
</TabContent>
|
||||||
<TabContent value="body" className="pl-3 mt-1">
|
<TabContent value="body" className="pl-3 mt-1">
|
||||||
{activeRequest.bodyType === BODY_TYPE_JSON ? (
|
{activeRequest.bodyType === BODY_TYPE_JSON ? (
|
||||||
<Editor
|
<Editor
|
||||||
key={multiWindowKey}
|
forceUpdateKey={forceUpdateKey}
|
||||||
useTemplating
|
useTemplating
|
||||||
placeholder="..."
|
placeholder="..."
|
||||||
className="!bg-gray-50"
|
className="!bg-gray-50"
|
||||||
@@ -197,7 +190,7 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN
|
|||||||
/>
|
/>
|
||||||
) : activeRequest.bodyType === BODY_TYPE_XML ? (
|
) : activeRequest.bodyType === BODY_TYPE_XML ? (
|
||||||
<Editor
|
<Editor
|
||||||
key={multiWindowKey}
|
forceUpdateKey={forceUpdateKey}
|
||||||
useTemplating
|
useTemplating
|
||||||
placeholder="..."
|
placeholder="..."
|
||||||
className="!bg-gray-50"
|
className="!bg-gray-50"
|
||||||
@@ -208,7 +201,7 @@ export const RequestPane = memo(function RequestPane({ style, fullHeight, classN
|
|||||||
/>
|
/>
|
||||||
) : activeRequest.bodyType === BODY_TYPE_GRAPHQL ? (
|
) : activeRequest.bodyType === BODY_TYPE_GRAPHQL ? (
|
||||||
<GraphQLEditor
|
<GraphQLEditor
|
||||||
key={multiWindowKey}
|
forceUpdateKey={forceUpdateKey}
|
||||||
baseRequest={activeRequest}
|
baseRequest={activeRequest}
|
||||||
className="!bg-gray-50"
|
className="!bg-gray-50"
|
||||||
defaultValue={activeRequest?.body ?? ''}
|
defaultValue={activeRequest?.body ?? ''}
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro
|
|||||||
) : viewMode === 'pretty' && contentType.includes('json') ? (
|
) : viewMode === 'pretty' && contentType.includes('json') ? (
|
||||||
<Editor
|
<Editor
|
||||||
readOnly
|
readOnly
|
||||||
key={`${contentType}:${activeResponse.updatedAt}:pretty`}
|
forceUpdateKey={activeResponse.updatedAt}
|
||||||
className="bg-gray-50 dark:!bg-gray-100"
|
className="bg-gray-50 dark:!bg-gray-100"
|
||||||
defaultValue={tryFormatJson(activeResponse?.body)}
|
defaultValue={tryFormatJson(activeResponse?.body)}
|
||||||
contentType={contentType}
|
contentType={contentType}
|
||||||
@@ -130,7 +130,7 @@ export const ResponsePane = memo(function ResponsePane({ style, className }: Pro
|
|||||||
) : activeResponse?.body ? (
|
) : activeResponse?.body ? (
|
||||||
<Editor
|
<Editor
|
||||||
readOnly
|
readOnly
|
||||||
key={`${contentType}:${activeResponse.updatedAt}`}
|
forceUpdateKey={activeResponse.updatedAt}
|
||||||
className="bg-gray-50 dark:!bg-gray-100"
|
className="bg-gray-50 dark:!bg-gray-100"
|
||||||
defaultValue={activeResponse?.body}
|
defaultValue={activeResponse?.body}
|
||||||
contentType={contentType}
|
contentType={contentType}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import classnames from 'classnames';
|
|||||||
import type { FormEvent } from 'react';
|
import type { FormEvent } from 'react';
|
||||||
import { memo, useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import { useIsResponseLoading } from '../hooks/useIsResponseLoading';
|
import { useIsResponseLoading } from '../hooks/useIsResponseLoading';
|
||||||
|
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
|
||||||
import { useSendRequest } from '../hooks/useSendRequest';
|
import { useSendRequest } from '../hooks/useSendRequest';
|
||||||
import { useUpdateRequest } from '../hooks/useUpdateRequest';
|
import { useUpdateRequest } from '../hooks/useUpdateRequest';
|
||||||
import type { HttpRequest } from '../lib/models';
|
import type { HttpRequest } from '../lib/models';
|
||||||
@@ -19,6 +20,7 @@ export const UrlBar = memo(function UrlBar({ id: requestId, url, method, classNa
|
|||||||
const handleMethodChange = useCallback((method: string) => updateRequest.mutate({ method }), []);
|
const handleMethodChange = useCallback((method: string) => updateRequest.mutate({ method }), []);
|
||||||
const handleUrlChange = useCallback((url: string) => updateRequest.mutate({ url }), []);
|
const handleUrlChange = useCallback((url: string) => updateRequest.mutate({ url }), []);
|
||||||
const loading = useIsResponseLoading(requestId);
|
const loading = useIsResponseLoading(requestId);
|
||||||
|
const { updateKey } = useRequestUpdateKey(requestId);
|
||||||
|
|
||||||
const handleSubmit = useCallback(
|
const handleSubmit = useCallback(
|
||||||
async (e: FormEvent) => {
|
async (e: FormEvent) => {
|
||||||
@@ -32,13 +34,13 @@ export const UrlBar = memo(function UrlBar({ id: requestId, url, method, classNa
|
|||||||
<form onSubmit={handleSubmit} className={classnames('url-bar', className)}>
|
<form onSubmit={handleSubmit} className={classnames('url-bar', className)}>
|
||||||
<Input
|
<Input
|
||||||
size="sm"
|
size="sm"
|
||||||
key={requestId}
|
|
||||||
hideLabel
|
hideLabel
|
||||||
useTemplating
|
useTemplating
|
||||||
contentType="url"
|
contentType="url"
|
||||||
className="px-0"
|
className="px-0"
|
||||||
name="url"
|
name="url"
|
||||||
label="Enter URL"
|
label="Enter URL"
|
||||||
|
forceUpdateKey={updateKey}
|
||||||
containerClassName="shadow shadow-gray-100 dark:shadow-gray-50"
|
containerClassName="shadow shadow-gray-100 dark:shadow-gray-50"
|
||||||
onChange={handleUrlChange}
|
onChange={handleUrlChange}
|
||||||
defaultValue={url}
|
defaultValue={url}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { defaultKeymap } from '@codemirror/commands';
|
import { defaultKeymap } from '@codemirror/commands';
|
||||||
import type { Extension } from '@codemirror/state';
|
import type { Extension } from '@codemirror/state';
|
||||||
import { Compartment, EditorState } from '@codemirror/state';
|
import { Compartment, EditorState, Transaction } from '@codemirror/state';
|
||||||
|
import type { ViewUpdate } from '@codemirror/view';
|
||||||
import { keymap, placeholder as placeholderExt, tooltips } from '@codemirror/view';
|
import { keymap, placeholder as placeholderExt, tooltips } from '@codemirror/view';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { EditorView } from 'codemirror';
|
import { EditorView } from 'codemirror';
|
||||||
@@ -19,6 +20,7 @@ export { formatSdl } from 'format-graphql';
|
|||||||
|
|
||||||
export interface EditorProps {
|
export interface EditorProps {
|
||||||
id?: string;
|
id?: string;
|
||||||
|
forceUpdateKey?: string;
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
type?: 'text' | 'password';
|
type?: 'text' | 'password';
|
||||||
className?: string;
|
className?: string;
|
||||||
@@ -42,6 +44,7 @@ export function Editor({
|
|||||||
type = 'text',
|
type = 'text',
|
||||||
heightMode,
|
heightMode,
|
||||||
contentType,
|
contentType,
|
||||||
|
forceUpdateKey,
|
||||||
autoFocus,
|
autoFocus,
|
||||||
placeholder,
|
placeholder,
|
||||||
useTemplating,
|
useTemplating,
|
||||||
@@ -87,6 +90,15 @@ export function Editor({
|
|||||||
view.dispatch({ effects: languageCompartment.reconfigure(ext) });
|
view.dispatch({ effects: languageCompartment.reconfigure(ext) });
|
||||||
}, [contentType, autocomplete]);
|
}, [contentType, autocomplete]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (cm.current === null) return;
|
||||||
|
const { view, languageCompartment } = cm.current;
|
||||||
|
const newDoc = defaultValue;
|
||||||
|
view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: newDoc ?? '' } });
|
||||||
|
const ext = getLanguageExtension({ contentType, useTemplating, autocomplete });
|
||||||
|
view.dispatch({ effects: languageCompartment.reconfigure(ext) });
|
||||||
|
}, [forceUpdateKey]);
|
||||||
|
|
||||||
// Initialize the editor when ref mounts
|
// Initialize the editor when ref mounts
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (wrapperRef.current === null || cm.current !== null) return;
|
if (wrapperRef.current === null || cm.current !== null) return;
|
||||||
@@ -218,13 +230,27 @@ function getExtensions({
|
|||||||
|
|
||||||
// Handle onChange
|
// Handle onChange
|
||||||
EditorView.updateListener.of((update) => {
|
EditorView.updateListener.of((update) => {
|
||||||
if (onChange && update.docChanged) {
|
if (onChange && update.docChanged && isViewUpdateFromUserInput(update)) {
|
||||||
onChange.current?.(update.state.doc.toString());
|
onChange.current?.(update.state.doc.toString());
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isViewUpdateFromUserInput(viewUpdate: ViewUpdate) {
|
||||||
|
// Make sure document has changed, ensuring user events like selections don't count.
|
||||||
|
if (viewUpdate.docChanged) {
|
||||||
|
// Check transactions for any that are direct user input, not changes from Y.js or another extension.
|
||||||
|
for (const transaction of viewUpdate.transactions) {
|
||||||
|
// Not using Transaction.isUserEvent because that only checks for a specific User event type ( "input", "delete", etc.). Checking the annotation directly allows for any type of user event.
|
||||||
|
const userEventType = transaction.annotation(Transaction.userEvent);
|
||||||
|
if (userEventType) return userEventType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const syncGutterBg = ({
|
const syncGutterBg = ({
|
||||||
parent,
|
parent,
|
||||||
className = '',
|
className = '',
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { IconButton } from './IconButton';
|
|||||||
import { HStack, VStack } from './Stacks';
|
import { HStack, VStack } from './Stacks';
|
||||||
|
|
||||||
export type InputProps = Omit<HTMLAttributes<HTMLInputElement>, 'onChange' | 'onFocus'> &
|
export type InputProps = Omit<HTMLAttributes<HTMLInputElement>, 'onChange' | 'onFocus'> &
|
||||||
Pick<EditorProps, 'contentType' | 'useTemplating' | 'autocomplete'> & {
|
Pick<EditorProps, 'contentType' | 'useTemplating' | 'autocomplete' | 'forceUpdateKey'> & {
|
||||||
name: string;
|
name: string;
|
||||||
type?: 'text' | 'password';
|
type?: 'text' | 'password';
|
||||||
label: string;
|
label: string;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { Input } from './Input';
|
|||||||
export type PairEditorProps = {
|
export type PairEditorProps = {
|
||||||
pairs: Pair[];
|
pairs: Pair[];
|
||||||
onChange: (pairs: Pair[]) => void;
|
onChange: (pairs: Pair[]) => void;
|
||||||
|
forceUpdateKey?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
namePlaceholder?: string;
|
namePlaceholder?: string;
|
||||||
valuePlaceholder?: string;
|
valuePlaceholder?: string;
|
||||||
@@ -36,6 +37,7 @@ type PairContainer = {
|
|||||||
|
|
||||||
export const PairEditor = memo(function PairEditor({
|
export const PairEditor = memo(function PairEditor({
|
||||||
pairs: originalPairs,
|
pairs: originalPairs,
|
||||||
|
forceUpdateKey,
|
||||||
nameAutocomplete,
|
nameAutocomplete,
|
||||||
valueAutocomplete,
|
valueAutocomplete,
|
||||||
namePlaceholder,
|
namePlaceholder,
|
||||||
@@ -53,6 +55,15 @@ export const PairEditor = memo(function PairEditor({
|
|||||||
return [...pairs, newPairContainer()];
|
return [...pairs, newPairContainer()];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Remove empty headers on initial render
|
||||||
|
// TODO: Make this not refresh the entire editor when forceUpdateKey changes, using some
|
||||||
|
// sort of diff method or deterministic IDs based on array index and update key
|
||||||
|
const nonEmpty = originalPairs.filter((h) => !(h.name === '' && h.value === ''));
|
||||||
|
const pairs = nonEmpty.map((pair) => newPairContainer(pair));
|
||||||
|
setPairs([...pairs, newPairContainer()]);
|
||||||
|
}, [forceUpdateKey]);
|
||||||
|
|
||||||
const setPairsAndSave = useCallback(
|
const setPairsAndSave = useCallback(
|
||||||
(fn: (pairs: PairContainer[]) => PairContainer[]) => {
|
(fn: (pairs: PairContainer[]) => PairContainer[]) => {
|
||||||
setPairs((oldPairs) => {
|
setPairs((oldPairs) => {
|
||||||
@@ -139,6 +150,7 @@ export const PairEditor = memo(function PairEditor({
|
|||||||
pairContainer={p}
|
pairContainer={p}
|
||||||
className="py-1"
|
className="py-1"
|
||||||
isLast={isLast}
|
isLast={isLast}
|
||||||
|
forceUpdateKey={forceUpdateKey}
|
||||||
nameAutocomplete={nameAutocomplete}
|
nameAutocomplete={nameAutocomplete}
|
||||||
valueAutocomplete={valueAutocomplete}
|
valueAutocomplete={valueAutocomplete}
|
||||||
namePlaceholder={namePlaceholder}
|
namePlaceholder={namePlaceholder}
|
||||||
@@ -179,6 +191,7 @@ type FormRowProps = {
|
|||||||
| 'valuePlaceholder'
|
| 'valuePlaceholder'
|
||||||
| 'nameValidate'
|
| 'nameValidate'
|
||||||
| 'valueValidate'
|
| 'valueValidate'
|
||||||
|
| 'forceUpdateKey'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
const FormRow = memo(function FormRow({
|
const FormRow = memo(function FormRow({
|
||||||
@@ -190,6 +203,7 @@ const FormRow = memo(function FormRow({
|
|||||||
onMove,
|
onMove,
|
||||||
onEnd,
|
onEnd,
|
||||||
isLast,
|
isLast,
|
||||||
|
forceUpdateKey,
|
||||||
nameAutocomplete,
|
nameAutocomplete,
|
||||||
valueAutocomplete,
|
valueAutocomplete,
|
||||||
namePlaceholder,
|
namePlaceholder,
|
||||||
@@ -287,6 +301,7 @@ const FormRow = memo(function FormRow({
|
|||||||
require={!isLast && !!pairContainer.pair.enabled && !!pairContainer.pair.value}
|
require={!isLast && !!pairContainer.pair.enabled && !!pairContainer.pair.value}
|
||||||
validate={nameValidate}
|
validate={nameValidate}
|
||||||
useTemplating
|
useTemplating
|
||||||
|
forceUpdateKey={forceUpdateKey}
|
||||||
containerClassName={classnames(isLast && 'border-dashed')}
|
containerClassName={classnames(isLast && 'border-dashed')}
|
||||||
defaultValue={pairContainer.pair.name}
|
defaultValue={pairContainer.pair.name}
|
||||||
label="Name"
|
label="Name"
|
||||||
@@ -301,6 +316,7 @@ const FormRow = memo(function FormRow({
|
|||||||
size="sm"
|
size="sm"
|
||||||
containerClassName={classnames(isLast && 'border-dashed')}
|
containerClassName={classnames(isLast && 'border-dashed')}
|
||||||
validate={valueValidate}
|
validate={valueValidate}
|
||||||
|
forceUpdateKey={forceUpdateKey}
|
||||||
defaultValue={pairContainer.pair.value}
|
defaultValue={pairContainer.pair.value}
|
||||||
label="Value"
|
label="Value"
|
||||||
name="value"
|
name="value"
|
||||||
|
|||||||
14
src-web/hooks/useRequestUpdateKey.ts
Normal file
14
src-web/hooks/useRequestUpdateKey.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { createGlobalState } from 'react-use';
|
||||||
|
import { generateId } from '../lib/generateId';
|
||||||
|
|
||||||
|
const useGlobalState = createGlobalState<Record<string, string>>({});
|
||||||
|
|
||||||
|
export function useRequestUpdateKey(requestId: string | null) {
|
||||||
|
const [keys, setKeys] = useGlobalState();
|
||||||
|
return {
|
||||||
|
updateKey: `${requestId}::${keys[requestId ?? 'n/a']}`,
|
||||||
|
wasUpdatedExternally: (changedRequestId: string) => {
|
||||||
|
setKeys((m) => ({ ...m, [changedRequestId]: generateId() }));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,23 +1,62 @@
|
|||||||
import { listen } from '@tauri-apps/api/event';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { appWindow } from '@tauri-apps/api/window';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
import { debounce } from '../lib/debounce';
|
||||||
|
import type { HttpRequest } from '../lib/models';
|
||||||
|
import { requestsQueryKey } from './useRequests';
|
||||||
|
import { useRequestUpdateKey } from './useRequestUpdateKey';
|
||||||
import { useSidebarDisplay } from './useSidebarDisplay';
|
import { useSidebarDisplay } from './useSidebarDisplay';
|
||||||
|
|
||||||
const unsubFns: (() => void)[] = [];
|
const unsubFns: (() => void)[] = [];
|
||||||
|
const UPDATE_DEBOUNCE_MILLIS = 500;
|
||||||
|
|
||||||
export function useTauriListeners() {
|
export function useTauriListeners() {
|
||||||
const sidebarDisplay = useSidebarDisplay();
|
const sidebarDisplay = useSidebarDisplay();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
const { wasUpdatedExternally } = useRequestUpdateKey(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let unmounted = false;
|
let unmounted = false;
|
||||||
|
|
||||||
listen('toggle_sidebar', async () => {
|
appWindow
|
||||||
sidebarDisplay.toggle();
|
.listen('toggle_sidebar', async () => {
|
||||||
}).then((fn) => {
|
sidebarDisplay.toggle();
|
||||||
if (unmounted) {
|
})
|
||||||
fn();
|
.then((unsub) => {
|
||||||
} else {
|
if (unmounted) unsub();
|
||||||
unsubFns.push(fn);
|
else unsubFns.push(unsub);
|
||||||
}
|
});
|
||||||
});
|
|
||||||
|
appWindow
|
||||||
|
.listen(
|
||||||
|
'updated_request',
|
||||||
|
debounce(({ payload: request }: { payload: HttpRequest }) => {
|
||||||
|
queryClient.setQueryData(
|
||||||
|
requestsQueryKey(request.workspaceId),
|
||||||
|
(requests: HttpRequest[] = []) => {
|
||||||
|
const newRequests = [];
|
||||||
|
let found = false;
|
||||||
|
for (const r of requests) {
|
||||||
|
if (r.id === request.id) {
|
||||||
|
found = true;
|
||||||
|
newRequests.push(request);
|
||||||
|
} else {
|
||||||
|
newRequests.push(r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!found) {
|
||||||
|
newRequests.push(request);
|
||||||
|
}
|
||||||
|
setTimeout(() => wasUpdatedExternally(request.id), 50);
|
||||||
|
return newRequests;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}, UPDATE_DEBOUNCE_MILLIS),
|
||||||
|
)
|
||||||
|
.then((unsub) => {
|
||||||
|
if (unmounted) unsub();
|
||||||
|
else unsubFns.push(unsub);
|
||||||
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
unmounted = true;
|
unmounted = true;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useState } from 'react';
|
|||||||
|
|
||||||
export function useUniqueKey(len = 10): { key: string; regenerate: () => void } {
|
export function useUniqueKey(len = 10): { key: string; regenerate: () => void } {
|
||||||
const [key, setKey] = useState<string>(() => generate(len));
|
const [key, setKey] = useState<string>(() => generate(len));
|
||||||
return { key, regenerate: () => setKey(generate(len)) };
|
return { key, wasUpdatedExternally: () => setKey(generate(len)) };
|
||||||
}
|
}
|
||||||
|
|
||||||
const CHARS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
const CHARS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
||||||
|
|||||||
3
src-web/lib/generateId.ts
Normal file
3
src-web/lib/generateId.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export function generateId(): string {
|
||||||
|
return Math.random().toString(36).slice(2);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user