mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-11 17:46:41 -05:00
Better model updates
This commit is contained in:
@@ -25,6 +25,7 @@ use tauri::regex::Regex;
|
||||
use tauri::{AppHandle, Menu, MenuItem, RunEvent, State, Submenu, TitleBarStyle, Window, Wry};
|
||||
use tauri::{CustomMenuItem, Manager, SystemTray, SystemTrayEvent, SystemTrayMenu, WindowEvent};
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::task::spawn_local;
|
||||
|
||||
use window_ext::WindowExt;
|
||||
|
||||
@@ -63,18 +64,18 @@ async fn migrate_db(
|
||||
#[tauri::command]
|
||||
async fn send_ephemeral_request(
|
||||
request: models::HttpRequest,
|
||||
window: Window<Wry>,
|
||||
app_handle: AppHandle<Wry>,
|
||||
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
|
||||
) -> Result<models::HttpResponse, String> {
|
||||
let pool = &*db_instance.lock().await;
|
||||
let response = models::HttpResponse::default();
|
||||
return actually_send_ephemeral_request(request, response, window, pool).await;
|
||||
return actually_send_ephemeral_request(request, &response, &app_handle, pool).await;
|
||||
}
|
||||
|
||||
async fn actually_send_ephemeral_request(
|
||||
request: models::HttpRequest,
|
||||
mut response: models::HttpResponse,
|
||||
window: Window<Wry>,
|
||||
response: &models::HttpResponse,
|
||||
app_handle: &AppHandle<Wry>,
|
||||
pool: &Pool<Sqlite>,
|
||||
) -> Result<models::HttpResponse, String> {
|
||||
let start = std::time::Instant::now();
|
||||
@@ -183,22 +184,22 @@ async fn actually_send_ephemeral_request(
|
||||
let sendable_req = match sendable_req_result {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
return response_err(response, e.to_string(), window, pool).await;
|
||||
return response_err(response, e.to_string(), &app_handle, pool).await;
|
||||
}
|
||||
};
|
||||
|
||||
let resp = client.execute(sendable_req).await;
|
||||
let raw_response = client.execute(sendable_req).await;
|
||||
|
||||
let p = window
|
||||
.app_handle()
|
||||
let p = app_handle
|
||||
.path_resolver()
|
||||
.resolve_resource("plugins/plugin.ts")
|
||||
.expect("failed to resolve resource");
|
||||
|
||||
runtime::run_plugin_sync(p.to_str().unwrap()).unwrap();
|
||||
|
||||
match resp {
|
||||
match raw_response {
|
||||
Ok(v) => {
|
||||
let mut response = response.clone();
|
||||
response.status = v.status().as_u16() as i64;
|
||||
response.status_reason = v.status().canonical_reason().map(|s| s.to_string());
|
||||
response.headers = Json(
|
||||
@@ -216,12 +217,10 @@ async fn actually_send_ephemeral_request(
|
||||
response = models::update_response_if_id(response, pool)
|
||||
.await
|
||||
.expect("Failed to update response");
|
||||
window
|
||||
.emit_all("updated_model", &response)
|
||||
.expect("Failed to emit updated_model");
|
||||
emit_side_effect(app_handle, "updated_model", &response);
|
||||
Ok(response)
|
||||
}
|
||||
Err(e) => response_err(response, e.to_string(), window, pool).await,
|
||||
Err(e) => response_err(response, e.to_string(), app_handle, pool).await,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,7 +229,7 @@ async fn send_request(
|
||||
window: Window<Wry>,
|
||||
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
|
||||
request_id: &str,
|
||||
) -> Result<(), String> {
|
||||
) -> Result<models::HttpResponse, String> {
|
||||
let pool = &*db_instance.lock().await;
|
||||
|
||||
let req = models::get_request(request_id, pool)
|
||||
@@ -240,25 +239,31 @@ async fn send_request(
|
||||
let response = models::create_response(&req.id, 0, "", 0, None, "", vec![], pool)
|
||||
.await
|
||||
.expect("Failed to create response");
|
||||
window
|
||||
.emit_all("created_model", &response)
|
||||
.expect("Failed to emit updated_model");
|
||||
|
||||
actually_send_ephemeral_request(req, response, window, pool).await?;
|
||||
Ok(())
|
||||
let response2 = response.clone();
|
||||
let app_handle2 = window.app_handle().clone();
|
||||
let pool2 = pool.clone();
|
||||
tokio::spawn(async move {
|
||||
actually_send_ephemeral_request(req, &response2, &app_handle2, &pool2)
|
||||
.await
|
||||
.expect("Failed to send request");
|
||||
});
|
||||
|
||||
emit_and_return(&window, "created_model", response)
|
||||
}
|
||||
|
||||
async fn response_err(
|
||||
mut response: models::HttpResponse,
|
||||
response: &models::HttpResponse,
|
||||
error: String,
|
||||
window: Window<Wry>,
|
||||
app_handle: &AppHandle<Wry>,
|
||||
pool: &Pool<Sqlite>,
|
||||
) -> Result<models::HttpResponse, String> {
|
||||
let mut response = response.clone();
|
||||
response.error = Some(error.clone());
|
||||
response = models::update_response_if_id(response, pool)
|
||||
.await
|
||||
.expect("Failed to update response");
|
||||
emit_all_others(&window, "updated_model", &response);
|
||||
emit_side_effect(app_handle, "updated_model", &response);
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
@@ -280,15 +285,15 @@ async fn set_key_value(
|
||||
value: &str,
|
||||
window: Window<Wry>,
|
||||
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
|
||||
) -> Result<(), String> {
|
||||
) -> Result<models::KeyValue, String> {
|
||||
let pool = &*db_instance.lock().await;
|
||||
let created_key_value = models::set_key_value(namespace, key, value, pool)
|
||||
.await
|
||||
.expect("Failed to create key value");
|
||||
let (key_value, created) = models::set_key_value(namespace, key, value, pool).await;
|
||||
|
||||
emit_all_others(&window, "updated_model", &created_key_value);
|
||||
|
||||
Ok(())
|
||||
if created {
|
||||
emit_and_return(&window, "created_model", key_value)
|
||||
} else {
|
||||
emit_and_return(&window, "updated_model", key_value)
|
||||
}
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -296,17 +301,13 @@ async fn create_workspace(
|
||||
name: &str,
|
||||
window: Window<Wry>,
|
||||
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
|
||||
) -> Result<String, String> {
|
||||
) -> Result<models::Workspace, String> {
|
||||
let pool = &*db_instance.lock().await;
|
||||
let created_workspace = models::create_workspace(name, "", pool)
|
||||
.await
|
||||
.expect("Failed to create workspace");
|
||||
|
||||
window
|
||||
.emit_all("created_model", &created_workspace)
|
||||
.expect("Failed to emit event");
|
||||
|
||||
Ok(created_workspace.id)
|
||||
emit_and_return(&window, "created_model", created_workspace)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -316,7 +317,7 @@ async fn create_request(
|
||||
sort_priority: f64,
|
||||
window: Window<Wry>,
|
||||
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
|
||||
) -> Result<String, String> {
|
||||
) -> Result<models::HttpRequest, String> {
|
||||
let pool = &*db_instance.lock().await;
|
||||
let headers = Vec::new();
|
||||
let created_request = models::upsert_request(
|
||||
@@ -336,11 +337,7 @@ async fn create_request(
|
||||
.await
|
||||
.expect("Failed to create request");
|
||||
|
||||
window
|
||||
.emit_all("created_model", &created_request)
|
||||
.expect("Failed to emit event");
|
||||
|
||||
Ok(created_request.id)
|
||||
emit_and_return(&window, "created_model", created_request)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -348,13 +345,12 @@ async fn duplicate_request(
|
||||
id: &str,
|
||||
window: Window<Wry>,
|
||||
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
|
||||
) -> Result<String, String> {
|
||||
) -> Result<models::HttpRequest, String> {
|
||||
let pool = &*db_instance.lock().await;
|
||||
let request = models::duplicate_request(id, pool)
|
||||
.await
|
||||
.expect("Failed to duplicate request");
|
||||
emit_all_others(&window, "updated_model", &request);
|
||||
Ok(request.id)
|
||||
emit_and_return(&window, "updated_model", request)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -362,7 +358,7 @@ async fn update_request(
|
||||
request: models::HttpRequest,
|
||||
window: Window<Wry>,
|
||||
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
|
||||
) -> Result<(), String> {
|
||||
) -> Result<models::HttpRequest, String> {
|
||||
let pool = &*db_instance.lock().await;
|
||||
|
||||
// TODO: Figure out how to make this better
|
||||
@@ -393,9 +389,7 @@ async fn update_request(
|
||||
.await
|
||||
.expect("Failed to update request");
|
||||
|
||||
emit_all_others(&window, "updated_model", updated_request);
|
||||
|
||||
Ok(())
|
||||
emit_and_return(&window, "updated_model", updated_request)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -403,13 +397,12 @@ async fn delete_request(
|
||||
window: Window<Wry>,
|
||||
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
|
||||
request_id: &str,
|
||||
) -> Result<(), String> {
|
||||
) -> Result<models::HttpRequest, String> {
|
||||
let pool = &*db_instance.lock().await;
|
||||
let req = models::delete_request(request_id, pool)
|
||||
.await
|
||||
.expect("Failed to delete request");
|
||||
emit_all_others(&window, "deleted_model", req);
|
||||
Ok(())
|
||||
emit_and_return(&window, "deleted_model", req)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -450,13 +443,12 @@ async fn delete_response(
|
||||
id: &str,
|
||||
window: Window<Wry>,
|
||||
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
|
||||
) -> Result<(), String> {
|
||||
) -> Result<models::HttpResponse, String> {
|
||||
let pool = &*db_instance.lock().await;
|
||||
let response = models::delete_response(id, pool)
|
||||
.await
|
||||
.expect("Failed to delete response");
|
||||
emit_all_others(&window, "deleted_model", response);
|
||||
Ok(())
|
||||
emit_and_return(&window, "deleted_model", response)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -494,13 +486,12 @@ async fn delete_workspace(
|
||||
window: Window<Wry>,
|
||||
db_instance: State<'_, Mutex<Pool<Sqlite>>>,
|
||||
id: &str,
|
||||
) -> Result<(), String> {
|
||||
) -> Result<models::Workspace, String> {
|
||||
let pool = &*db_instance.lock().await;
|
||||
let workspace = models::delete_workspace(id, pool)
|
||||
.await
|
||||
.expect("Failed to delete workspace");
|
||||
emit_all_others(&window, "deleted_model", workspace);
|
||||
Ok(())
|
||||
emit_and_return(&window, "deleted_model", workspace)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -690,13 +681,17 @@ fn create_window(handle: &AppHandle<Wry>) -> Window<Wry> {
|
||||
win
|
||||
}
|
||||
|
||||
/// 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();
|
||||
}
|
||||
/// Emit an event to all windows, with a source window
|
||||
fn emit_and_return<S: Serialize + Clone, E>(
|
||||
current_window: &Window<Wry>,
|
||||
event: &str,
|
||||
payload: S,
|
||||
) -> Result<S, E> {
|
||||
current_window.emit_all(event, &payload).unwrap();
|
||||
Ok(payload)
|
||||
}
|
||||
|
||||
/// Emit an event to all windows, used for side-effects where there is no source window to attribute. This
|
||||
fn emit_side_effect<S: Serialize + Clone>(app_handle: &AppHandle<Wry>, event: &str, payload: S) {
|
||||
app_handle.emit_all(event, &payload).unwrap();
|
||||
}
|
||||
|
||||
@@ -86,7 +86,8 @@ pub async fn set_key_value(
|
||||
key: &str,
|
||||
value: &str,
|
||||
pool: &Pool<Sqlite>,
|
||||
) -> Option<KeyValue> {
|
||||
) -> (KeyValue, bool) {
|
||||
let existing = get_key_value(namespace, key, pool).await;
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO key_values (namespace, key, value)
|
||||
@@ -102,7 +103,10 @@ pub async fn set_key_value(
|
||||
.await
|
||||
.expect("Failed to insert key value");
|
||||
|
||||
get_key_value(namespace, key, pool).await
|
||||
let kv = get_key_value(namespace, key, pool)
|
||||
.await
|
||||
.expect("Failed to get key value");
|
||||
return (kv, existing.is_none());
|
||||
}
|
||||
|
||||
pub async fn get_key_value(namespace: &str, key: &str, pool: &Pool<Sqlite>) -> Option<KeyValue> {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Dialog } from './core/Dialog';
|
||||
type DialogEntry = {
|
||||
id: string;
|
||||
render: ({ hide }: { hide: () => void }) => React.ReactNode;
|
||||
} & Pick<DialogProps, 'title' | 'description' | 'hideX' | 'className'>;
|
||||
} & Pick<DialogProps, 'title' | 'description' | 'hideX' | 'className' | 'size'>;
|
||||
|
||||
type DialogEntryOptionalId = Omit<DialogEntry, 'id'> & { id?: string };
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useDuplicateRequest } from '../hooks/useDuplicateRequest';
|
||||
import { useRequest } from '../hooks/useRequest';
|
||||
import { Dropdown } from './core/Dropdown';
|
||||
import { Icon } from './core/Icon';
|
||||
import { InlineCode } from './core/InlineCode';
|
||||
|
||||
interface Props {
|
||||
requestId: string;
|
||||
@@ -30,9 +31,12 @@ export function RequestActionsDropdown({ requestId, children }: Props) {
|
||||
onSelect: async () => {
|
||||
const confirmed = await confirm({
|
||||
title: 'Delete Request',
|
||||
description: `Are you sure you want to delete "${request?.name}"?`,
|
||||
confirmButtonColor: 'danger',
|
||||
confirmButtonText: 'Delete',
|
||||
variant: 'delete',
|
||||
description: (
|
||||
<>
|
||||
Are you sure you want to delete <InlineCode>{request?.name}</InlineCode>?
|
||||
</>
|
||||
),
|
||||
});
|
||||
if (confirmed) {
|
||||
deleteRequest.mutate();
|
||||
|
||||
@@ -10,12 +10,13 @@ import { Button } from './core/Button';
|
||||
import type { DropdownItem } from './core/Dropdown';
|
||||
import { Dropdown } from './core/Dropdown';
|
||||
import { Icon } from './core/Icon';
|
||||
import { InlineCode } from './core/InlineCode';
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const WorkspaceDropdown = memo(function WorkspaceDropdown({ className }: Props) {
|
||||
export const WorkspaceActionsDropdown = memo(function WorkspaceDropdown({ className }: Props) {
|
||||
const workspaces = useWorkspaces();
|
||||
const activeWorkspace = useActiveWorkspace();
|
||||
const activeWorkspaceId = activeWorkspace?.id ?? null;
|
||||
@@ -51,9 +52,12 @@ export const WorkspaceDropdown = memo(function WorkspaceDropdown({ className }:
|
||||
onSelect: async () => {
|
||||
const confirmed = await confirm({
|
||||
title: 'Delete Workspace',
|
||||
description: `Are you sure you want to delete "${activeWorkspace?.name}"?`,
|
||||
confirmButtonColor: 'danger',
|
||||
confirmButtonText: 'Delete',
|
||||
variant: 'delete',
|
||||
description: (
|
||||
<>
|
||||
Are you sure you want to delete <InlineCode>{activeWorkspace?.name}</InlineCode>?
|
||||
</>
|
||||
),
|
||||
});
|
||||
if (confirmed) {
|
||||
deleteWorkspace.mutate();
|
||||
@@ -5,7 +5,7 @@ import { IconButton } from './core/IconButton';
|
||||
import { HStack } from './core/Stacks';
|
||||
import { RequestActionsDropdown } from './RequestActionsDropdown';
|
||||
import { SidebarActions } from './SidebarActions';
|
||||
import { WorkspaceDropdown } from './WorkspaceDropdown';
|
||||
import { WorkspaceActionsDropdown } from './WorkspaceActionsDropdown';
|
||||
|
||||
interface Props {
|
||||
className?: string;
|
||||
@@ -21,13 +21,12 @@ export const WorkspaceHeader = memo(function WorkspaceHeader({ className }: Prop
|
||||
>
|
||||
<HStack space={0.5} className="flex-1 pointer-events-none" alignItems="center">
|
||||
<SidebarActions />
|
||||
<WorkspaceDropdown className="pointer-events-auto" />
|
||||
<WorkspaceActionsDropdown className="pointer-events-auto" />
|
||||
</HStack>
|
||||
<div className="flex-[2] text-center text-gray-800 text-sm truncate pointer-events-none">
|
||||
{activeRequest?.name}
|
||||
</div>
|
||||
<div className="flex-1 flex justify-end -mr-2 pointer-events-none">
|
||||
<IconButton size="sm" title="" icon="magnifyingGlass" />
|
||||
{activeRequest && (
|
||||
<RequestActionsDropdown requestId={activeRequest?.id}>
|
||||
<IconButton
|
||||
|
||||
@@ -3,8 +3,8 @@ import { motion } from 'framer-motion';
|
||||
import type { ReactNode } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { Overlay } from '../Overlay';
|
||||
import { Heading } from './Heading';
|
||||
import { IconButton } from './IconButton';
|
||||
import { VStack } from './Stacks';
|
||||
|
||||
export interface DialogProps {
|
||||
children: ReactNode;
|
||||
@@ -13,14 +13,14 @@ export interface DialogProps {
|
||||
title: ReactNode;
|
||||
description?: ReactNode;
|
||||
className?: string;
|
||||
wide?: boolean;
|
||||
size?: 'sm' | 'md' | 'full' | 'dynamic';
|
||||
hideX?: boolean;
|
||||
}
|
||||
|
||||
export function Dialog({
|
||||
children,
|
||||
className,
|
||||
wide,
|
||||
size = 'full',
|
||||
open,
|
||||
onClose,
|
||||
title,
|
||||
@@ -48,9 +48,12 @@ export function Dialog({
|
||||
className={classnames(
|
||||
className,
|
||||
'relative bg-gray-50 pointer-events-auto',
|
||||
'w-[20rem] max-h-[80vh] p-5 rounded-lg overflow-auto',
|
||||
'max-h-[80vh] p-5 rounded-lg overflow-auto',
|
||||
'dark:border border-gray-200 shadow-md shadow-black/10',
|
||||
wide && 'w-[80vw] max-w-[50rem]',
|
||||
size === 'sm' && 'w-[25rem]',
|
||||
size === 'md' && 'w-[45rem]',
|
||||
size === 'full' && 'w-[80vw]',
|
||||
size === 'dynamic' && 'min-w-[30vw] max-w-[80vw]',
|
||||
)}
|
||||
>
|
||||
{!hideX && (
|
||||
@@ -63,13 +66,11 @@ export function Dialog({
|
||||
className="ml-auto absolute right-1 top-1"
|
||||
/>
|
||||
)}
|
||||
<VStack space={3}>
|
||||
<h1 className="text-xl font-semibold w-full" id={titleId}>
|
||||
{title}
|
||||
</h1>
|
||||
{description && <p id={descriptionId}>{description}</p>}
|
||||
<div>{children}</div>
|
||||
</VStack>
|
||||
<Heading className="text-xl font-semibold w-full" id={titleId}>
|
||||
{title}
|
||||
</Heading>
|
||||
{description && <p id={descriptionId}>{description}</p>}
|
||||
<div className="mt-6">{children}</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
import classnames from 'classnames';
|
||||
import type { ReactNode } from 'react';
|
||||
import type { HTMLAttributes } from 'react';
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
export function Heading({ className, children, ...props }: Props) {
|
||||
export function Heading({ className, children, ...props }: HTMLAttributes<HTMLHeadingElement>) {
|
||||
return (
|
||||
<h1 className={classnames(className, 'text-2xl font-semibold text-gray-900 mb-3')} {...props}>
|
||||
{children}
|
||||
|
||||
14
src-web/components/core/InlineCode.tsx
Normal file
14
src-web/components/core/InlineCode.tsx
Normal file
@@ -0,0 +1,14 @@
|
||||
import classnames from 'classnames';
|
||||
import type { HTMLAttributes } from 'react';
|
||||
|
||||
export function InlineCode({ className, ...props }: HTMLAttributes<HTMLSpanElement>) {
|
||||
return (
|
||||
<code
|
||||
className={classnames(
|
||||
className,
|
||||
'font-mono text-sm bg-highlight border-0 border-gray-200 px-1.5 py-0.5 rounded text-gray-800 shadow-inner',
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -2,20 +2,27 @@ import type { ButtonProps } from '../components/core/Button';
|
||||
import { Button } from '../components/core/Button';
|
||||
import { HStack } from '../components/core/Stacks';
|
||||
|
||||
interface Props {
|
||||
export interface ConfirmProps {
|
||||
hide: () => void;
|
||||
onResult: (result: boolean) => void;
|
||||
confirmButtonColor?: ButtonProps['color'];
|
||||
confirmButtonText?: string;
|
||||
variant?: 'delete' | 'confirm';
|
||||
}
|
||||
export function Confirm({
|
||||
hide,
|
||||
onResult,
|
||||
confirmButtonColor = 'primary',
|
||||
confirmButtonText = 'Confirm',
|
||||
}: Props) {
|
||||
|
||||
const colors: Record<NonNullable<ConfirmProps['variant']>, ButtonProps['color']> = {
|
||||
delete: 'danger',
|
||||
confirm: 'primary',
|
||||
};
|
||||
|
||||
const confirmButtonTexts: Record<NonNullable<ConfirmProps['variant']>, string> = {
|
||||
delete: 'Delete',
|
||||
confirm: 'Confirm',
|
||||
};
|
||||
|
||||
export function Confirm({ hide, onResult, variant = 'confirm' }: ConfirmProps) {
|
||||
const focusRef = (el: HTMLButtonElement | null) => {
|
||||
el?.focus();
|
||||
setTimeout(() => {
|
||||
el?.focus();
|
||||
});
|
||||
};
|
||||
|
||||
const handleHide = () => {
|
||||
@@ -33,8 +40,8 @@ export function Confirm({
|
||||
<Button className="focus" color="gray" onClick={handleHide}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button className="focus" ref={focusRef} color={confirmButtonColor} onClick={handleSuccess}>
|
||||
{confirmButtonText}
|
||||
<Button className="focus" ref={focusRef} color={colors[variant]} onClick={handleSuccess}>
|
||||
{confirmButtonTexts[variant]}
|
||||
</Button>
|
||||
</HStack>
|
||||
);
|
||||
|
||||
26
src-web/hooks/useConfirm.ts
Normal file
26
src-web/hooks/useConfirm.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import type { DialogProps } from '../components/core/Dialog';
|
||||
import { useDialog } from '../components/DialogContext';
|
||||
import type { ConfirmProps } from './Confirm';
|
||||
import { Confirm } from './Confirm';
|
||||
|
||||
export function useConfirm() {
|
||||
const dialog = useDialog();
|
||||
return ({
|
||||
title,
|
||||
description,
|
||||
variant,
|
||||
}: {
|
||||
title: DialogProps['title'];
|
||||
description?: DialogProps['description'];
|
||||
variant: ConfirmProps['variant'];
|
||||
}) =>
|
||||
new Promise((onResult: ConfirmProps['onResult']) => {
|
||||
dialog.show({
|
||||
title,
|
||||
description,
|
||||
hideX: true,
|
||||
size: 'sm',
|
||||
render: ({ hide }) => Confirm({ hide, variant, onResult }),
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import type { ButtonProps } from '../components/core/Button';
|
||||
import { useDialog } from '../components/DialogContext';
|
||||
import { Confirm } from './Confirm';
|
||||
|
||||
export function useConfirm() {
|
||||
const dialog = useDialog();
|
||||
return ({
|
||||
title,
|
||||
description,
|
||||
confirmButtonColor,
|
||||
confirmButtonText,
|
||||
}: {
|
||||
title: string;
|
||||
description?: string;
|
||||
confirmButtonColor?: ButtonProps['color'];
|
||||
confirmButtonText?: string;
|
||||
}) => {
|
||||
return new Promise((resolve: (r: boolean) => void) => {
|
||||
dialog.show({
|
||||
title,
|
||||
description,
|
||||
hideX: true,
|
||||
render: ({ hide }) => (
|
||||
<Confirm
|
||||
hide={hide}
|
||||
onResult={resolve}
|
||||
confirmButtonColor={confirmButtonColor}
|
||||
confirmButtonText={confirmButtonText}
|
||||
/>
|
||||
),
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -1,26 +1,31 @@
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import type { HttpRequest } from '../lib/models';
|
||||
import { useActiveWorkspace } from './useActiveWorkspace';
|
||||
import { useRequests } from './useRequests';
|
||||
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
|
||||
import { requestsQueryKey, useRequests } from './useRequests';
|
||||
import { useRoutes } from './useRoutes';
|
||||
|
||||
export function useCreateRequest({ navigateAfter }: { navigateAfter: boolean }) {
|
||||
const workspace = useActiveWorkspace();
|
||||
const workspaceId = useActiveWorkspaceId();
|
||||
const routes = useRoutes();
|
||||
const requests = useRequests();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<string, unknown, Partial<Pick<HttpRequest, 'name' | 'sortPriority'>>>({
|
||||
return useMutation<HttpRequest, unknown, Partial<Pick<HttpRequest, 'name' | 'sortPriority'>>>({
|
||||
mutationFn: (patch) => {
|
||||
if (workspace === null) {
|
||||
if (workspaceId === null) {
|
||||
throw new Error("Cannot create request when there's no active workspace");
|
||||
}
|
||||
const sortPriority = maxSortPriority(requests) + 1000;
|
||||
return invoke('create_request', { sortPriority, workspaceId: workspace.id, ...patch });
|
||||
return invoke('create_request', { sortPriority, workspaceId, ...patch });
|
||||
},
|
||||
onSuccess: async (requestId) => {
|
||||
if (navigateAfter && workspace !== null) {
|
||||
routes.navigate('request', { workspaceId: workspace.id, requestId });
|
||||
onSuccess: async (request) => {
|
||||
queryClient.setQueryData<HttpRequest[]>(
|
||||
requestsQueryKey({ workspaceId: request.workspaceId }),
|
||||
(requests) => [...(requests ?? []), request],
|
||||
);
|
||||
if (navigateAfter) {
|
||||
routes.navigate('request', { workspaceId: request.workspaceId, requestId: request.id });
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import type { Workspace } from '../lib/models';
|
||||
import { useRoutes } from './useRoutes';
|
||||
import { workspacesQueryKey } from './useWorkspaces';
|
||||
|
||||
export function useCreateWorkspace({ navigateAfter }: { navigateAfter: boolean }) {
|
||||
const routes = useRoutes();
|
||||
return useMutation<string, unknown, Pick<Workspace, 'name'>>({
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<Workspace, unknown, Pick<Workspace, 'name'>>({
|
||||
mutationFn: (patch) => {
|
||||
return invoke('create_workspace', patch);
|
||||
},
|
||||
onSuccess: async (workspaceId) => {
|
||||
onSuccess: async (workspace) => {
|
||||
queryClient.setQueryData<Workspace[]>(workspacesQueryKey({}), (workspaces) => [
|
||||
...(workspaces ?? []),
|
||||
workspace,
|
||||
]);
|
||||
if (navigateAfter) {
|
||||
routes.navigate('workspace', { workspaceId });
|
||||
routes.navigate('workspace', { workspaceId: workspace.id });
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,19 +1,25 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
|
||||
import type { HttpRequest } from '../lib/models';
|
||||
import { useActiveRequestId } from './useActiveRequestId';
|
||||
import { requestsQueryKey } from './useRequests';
|
||||
import { responsesQueryKey } from './useResponses';
|
||||
import { useRoutes } from './useRoutes';
|
||||
|
||||
export function useDeleteRequest(id: string | null) {
|
||||
const workspaceId = useActiveWorkspaceId();
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<void, string>({
|
||||
mutationFn: async () => {
|
||||
if (id === null) return;
|
||||
await invoke('delete_request', { requestId: id });
|
||||
},
|
||||
onSuccess: async () => {
|
||||
if (workspaceId === null || id === null) return;
|
||||
await queryClient.invalidateQueries(requestsQueryKey({ workspaceId }));
|
||||
const activeRequestId = useActiveRequestId();
|
||||
const routes = useRoutes();
|
||||
return useMutation<HttpRequest, string>({
|
||||
mutationFn: async () => invoke('delete_request', { requestId: id }),
|
||||
onSuccess: async ({ workspaceId, id: requestId }) => {
|
||||
queryClient.setQueryData(responsesQueryKey({ requestId }), []); // Responses were deleted
|
||||
queryClient.setQueryData<HttpRequest[]>(requestsQueryKey({ workspaceId }), (requests) =>
|
||||
(requests ?? []).filter((r) => r.id !== requestId),
|
||||
);
|
||||
if (activeRequestId === requestId) {
|
||||
routes.navigate('workspace', { workspaceId });
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import type { HttpResponse } from '../lib/models';
|
||||
import { responsesQueryKey } from './useResponses';
|
||||
|
||||
export function useDeleteResponse(id: string | null) {
|
||||
return useMutation({
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<HttpResponse>({
|
||||
mutationFn: async () => {
|
||||
if (id === null) return;
|
||||
await invoke('delete_response', { id: id });
|
||||
return await invoke('delete_response', { id: id });
|
||||
},
|
||||
onSuccess: ({ requestId, id: responseId }) => {
|
||||
queryClient.setQueryData<HttpResponse[]>(responsesQueryKey({ requestId }), (responses) =>
|
||||
(responses ?? []).filter((response) => response.id !== responseId),
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ export function useDeleteResponses(requestId?: string) {
|
||||
},
|
||||
onSuccess: async () => {
|
||||
if (!requestId) return;
|
||||
await queryClient.invalidateQueries(responsesQueryKey({ requestId }));
|
||||
queryClient.setQueryData(responsesQueryKey({ requestId }), []);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import type { Workspace } from '../lib/models';
|
||||
import { useActiveWorkspaceId } from './useActiveWorkspaceId';
|
||||
import { requestsQueryKey } from './useRequests';
|
||||
import { useRoutes } from './useRoutes';
|
||||
import { workspacesQueryKey } from './useWorkspaces';
|
||||
|
||||
@@ -8,17 +10,21 @@ export function useDeleteWorkspace(id: string | null) {
|
||||
const queryClient = useQueryClient();
|
||||
const activeWorkspaceId = useActiveWorkspaceId();
|
||||
const routes = useRoutes();
|
||||
return useMutation<void, string>({
|
||||
mutationFn: async () => {
|
||||
if (id === null) return;
|
||||
await invoke('delete_workspace', { id });
|
||||
return useMutation<Workspace, string>({
|
||||
mutationFn: () => {
|
||||
return invoke('delete_workspace', { id });
|
||||
},
|
||||
onSuccess: async () => {
|
||||
if (id === null) return;
|
||||
await queryClient.invalidateQueries(workspacesQueryKey());
|
||||
if (id === activeWorkspaceId) {
|
||||
onSuccess: async ({ id: workspaceId }) => {
|
||||
queryClient.setQueryData<Workspace[]>(workspacesQueryKey({}), (workspaces) =>
|
||||
workspaces?.filter((workspace) => workspace.id !== workspaceId),
|
||||
);
|
||||
if (workspaceId === activeWorkspaceId) {
|
||||
routes.navigate('workspaces');
|
||||
}
|
||||
|
||||
// Also clean up other things that may have been deleted
|
||||
queryClient.setQueryData(requestsQueryKey({ workspaceId }), []);
|
||||
await queryClient.invalidateQueries(requestsQueryKey({ workspaceId }));
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ export function useKeyValue<T extends Object | null>({
|
||||
const mutate = useMutation<void, unknown, T>({
|
||||
mutationFn: (value) => setKeyValue<T>({ namespace, key, value }),
|
||||
// k/v should be as fast as possible, so optimistically update the cache
|
||||
onMutate: (value) => queryClient.setQueryData(keyValueQueryKey({ namespace, key }), value),
|
||||
onMutate: (value) => queryClient.setQueryData<T>(keyValueQueryKey({ namespace, key }), value),
|
||||
});
|
||||
|
||||
const set = useCallback(
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import type { HttpResponse } from '../lib/models';
|
||||
import { responsesQueryKey } from './useResponses';
|
||||
|
||||
export function useSendRequest(id: string | null) {
|
||||
return useMutation<void, string>({
|
||||
const queryClient = useQueryClient();
|
||||
return useMutation<HttpResponse, string>({
|
||||
mutationFn: async () => {
|
||||
if (id === null) return;
|
||||
await invoke('send_request', { requestId: id });
|
||||
return invoke('send_request', { requestId: id });
|
||||
},
|
||||
onSuccess: (response) => {
|
||||
queryClient.setQueryData<HttpResponse[]>(responsesQueryKey(response), (responses) => [
|
||||
...(responses ?? []),
|
||||
response,
|
||||
]);
|
||||
},
|
||||
}).mutate;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { invoke } from '@tauri-apps/api';
|
||||
import type { EventCallback } from '@tauri-apps/api/event';
|
||||
import { listen as tauriListen } from '@tauri-apps/api/event';
|
||||
import { appWindow } from '@tauri-apps/api/window';
|
||||
import { matchPath } from 'react-router-dom';
|
||||
import { useEffectOnce } from 'react-use';
|
||||
import { DEFAULT_FONT_SIZE } from '../lib/constants';
|
||||
import { debounce } from '../lib/debounce';
|
||||
import { extractKeyValue, NAMESPACE_NO_SYNC } from '../lib/keyValueStore';
|
||||
import type { HttpRequest, HttpResponse, KeyValue, Model, Workspace } from '../lib/models';
|
||||
import { NAMESPACE_NO_SYNC } from '../lib/keyValueStore';
|
||||
import type { HttpRequest, HttpResponse, Model, Workspace } from '../lib/models';
|
||||
import { modelsEq } from '../lib/models';
|
||||
import { keyValueQueryKey } from './useKeyValue';
|
||||
import { requestsQueryKey } from './useRequests';
|
||||
@@ -30,7 +31,7 @@ export function useTauriListeners() {
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
function listen<T>(event: string, fn: EventCallback<T>) {
|
||||
appWindow.listen(event, fn).then((unsub) => {
|
||||
tauriListen(event, fn).then((unsub) => {
|
||||
if (unmounted) unsub();
|
||||
else unsubFns.push(unsub);
|
||||
});
|
||||
@@ -43,13 +44,10 @@ export function useTauriListeners() {
|
||||
listen('toggle_sidebar', sidebarDisplay.toggle);
|
||||
listen('refresh', () => location.reload());
|
||||
|
||||
listenDebounced('updated_key_value', ({ payload: keyValue }: { payload: KeyValue }) => {
|
||||
if (keyValue.namespace !== NAMESPACE_NO_SYNC) {
|
||||
queryClient.setQueryData(keyValueQueryKey(keyValue), extractKeyValue(keyValue));
|
||||
}
|
||||
});
|
||||
listenDebounced<Model>('created_model', ({ payload, windowLabel }) => {
|
||||
const cameFromSameWindow = windowLabel === appWindow.label;
|
||||
if (cameFromSameWindow) return;
|
||||
|
||||
listenDebounced('created_model', ({ payload }: { payload: Model }) => {
|
||||
const queryKey =
|
||||
payload.model === 'http_request'
|
||||
? requestsQueryKey(payload)
|
||||
@@ -71,11 +69,14 @@ export function useTauriListeners() {
|
||||
const skipSync = payload.model === 'key_value' && payload.namespace === NAMESPACE_NO_SYNC;
|
||||
|
||||
if (!skipSync) {
|
||||
queryClient.setQueryData(queryKey, (values: Model[] = []) => [...values, payload]);
|
||||
queryClient.setQueryData<Model[]>(queryKey, (values) => [...(values ?? []), payload]);
|
||||
}
|
||||
});
|
||||
|
||||
listenDebounced('updated_model', ({ payload }: { payload: Model }) => {
|
||||
listenDebounced<Model>('updated_model', ({ payload, windowLabel }) => {
|
||||
const cameFromSameWindow = windowLabel === appWindow.label;
|
||||
if (cameFromSameWindow) return;
|
||||
|
||||
const queryKey =
|
||||
payload.model === 'http_request'
|
||||
? requestsQueryKey(payload)
|
||||
@@ -94,26 +95,30 @@ export function useTauriListeners() {
|
||||
|
||||
const skipSync = payload.model === 'key_value' && payload.namespace === NAMESPACE_NO_SYNC;
|
||||
|
||||
if (payload.model === 'http_request') {
|
||||
wasUpdatedExternally(payload.id);
|
||||
}
|
||||
|
||||
if (!skipSync) {
|
||||
queryClient.setQueryData(queryKey, (values: Model[] = []) =>
|
||||
values.map((v) => (modelsEq(v, payload) ? payload : v)),
|
||||
queryClient.setQueryData<Model[]>(queryKey, (values) =>
|
||||
values?.map((v) => (modelsEq(v, payload) ? payload : v)),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
listen('deleted_model', ({ payload: model }: { payload: Model }) => {
|
||||
listen<Model>('deleted_model', ({ payload }) => {
|
||||
function removeById<T extends { id: string }>(model: T) {
|
||||
return (entries: T[] | undefined) => entries?.filter((e) => e.id !== model.id);
|
||||
}
|
||||
|
||||
if (model.model === 'workspace') {
|
||||
queryClient.setQueryData(workspacesQueryKey(), removeById<Workspace>(model));
|
||||
} else if (model.model === 'http_request') {
|
||||
queryClient.setQueryData(requestsQueryKey(model), removeById<HttpRequest>(model));
|
||||
} else if (model.model === 'http_response') {
|
||||
queryClient.setQueryData(responsesQueryKey(model), removeById<HttpResponse>(model));
|
||||
} else if (model.model === 'key_value') {
|
||||
queryClient.setQueryData(keyValueQueryKey(model), undefined);
|
||||
if (payload.model === 'workspace') {
|
||||
queryClient.setQueryData<Workspace[]>(workspacesQueryKey(), removeById(payload));
|
||||
} else if (payload.model === 'http_request') {
|
||||
queryClient.setQueryData<HttpRequest[]>(requestsQueryKey(payload), removeById(payload));
|
||||
} else if (payload.model === 'http_response') {
|
||||
queryClient.setQueryData<HttpResponse[]>(responsesQueryKey(payload), removeById(payload));
|
||||
} else if (payload.model === 'key_value') {
|
||||
queryClient.setQueryData(keyValueQueryKey(payload), undefined);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -19,8 +19,8 @@ export function useUpdateAnyRequest() {
|
||||
onMutate: async ({ id, update }) => {
|
||||
const request = await getRequest(id);
|
||||
if (request === null) return;
|
||||
queryClient.setQueryData(requestsQueryKey(request), (requests: HttpRequest[] | undefined) =>
|
||||
requests?.map((r) => (r.id === request.id ? update(r) : r)),
|
||||
queryClient.setQueryData<HttpRequest[]>(requestsQueryKey(request), (requests) =>
|
||||
(requests ?? []).map((r) => (r.id === request.id ? update(r) : r)),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -21,8 +21,8 @@ export function useUpdateRequest(id: string | null) {
|
||||
if (request === null) return;
|
||||
|
||||
const newRequest = typeof v === 'function' ? v(request) : { ...request, ...v };
|
||||
queryClient.setQueryData(requestsQueryKey(request), (requests: HttpRequest[] | undefined) =>
|
||||
requests?.map((r) => (r.id === newRequest.id ? newRequest : r)),
|
||||
queryClient.setQueryData<HttpRequest[]>(requestsQueryKey(request), (requests) =>
|
||||
(requests ?? []).map((r) => (r.id === newRequest.id ? newRequest : r)),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { getKeyValue, setKeyValue } from './keyValueStore';
|
||||
import { getKeyValue, NAMESPACE_NO_SYNC, setKeyValue } from './keyValueStore';
|
||||
|
||||
export async function getLastLocation(): Promise<string> {
|
||||
return getKeyValue({ key: 'last_location', fallback: '/' });
|
||||
return getKeyValue({ namespace: NAMESPACE_NO_SYNC, key: 'last_location', fallback: '/' });
|
||||
}
|
||||
|
||||
export async function setLastLocation(pathname: string): Promise<void> {
|
||||
return setKeyValue({ key: 'last_location', value: pathname });
|
||||
return setKeyValue({ namespace: NAMESPACE_NO_SYNC, key: 'last_location', value: pathname });
|
||||
}
|
||||
|
||||
export async function syncLastLocation(): Promise<void> {
|
||||
|
||||
Reference in New Issue
Block a user